Jump to content

Module:Spell

From Apogea Wiki
Revision as of 23:15, 30 January 2026 by Jayarrowz (talk | contribs)

Documentation for this module may be created at Module:Spell/doc

local p = {}

--[[
    Spell Module for Apogea Wiki
    
    Displays spell tooltips matching in-game styling:
    - Orange spell name (font-bitcell)
    - White cost/cooldown in parentheses
    - White description
    - Orange/coral spell type (e.g., "Fire Spell")
    - Green formula values
    - White requirement text
    
    Uses existing color classes from Tokens.css
]]

-- Spell type colors (matching in-game - uses existing color classes)
-- Fire Spell appears orange/coral in-game
local typeColors = {
    light = "columbia",      -- light blue
    blade = "coral",         -- red/orange
    physical = "coral",      -- red/orange
    conjure = "conifer",     -- green
    death = "orchid",        -- purple
    fire = "coral",          -- red/orange (Fire Spell is orange in screenshot)
    arrow = "coral",         -- red/orange
    time = "gold",           -- yellow/gold
    energy = "coral",        -- red/orange
    heal = "columbia",       -- light blue
    holy = "gold",           -- yellow/gold
    earth = "gold",          -- yellow/gold
    mystic = "columbia",     -- light blue
    defense = "gold",        -- yellow/gold
    water = "columbia"       -- light blue
}

-- Type to plural category mapping
local typePlurals = {
    ["Light"] = "Light Spells",
    ["Blade"] = "Blade Spells",
    ["Physical"] = "Physical Spells",
    ["Conjure"] = "Conjure Spells",
    ["Death"] = "Death Spells",
    ["Fire"] = "Fire Spells",
    ["Arrow"] = "Arrow Spells",
    ["Time"] = "Time Spells",
    ["Energy"] = "Energy Spells",
    ["Heal"] = "Heal Spells",
    ["Holy"] = "Holy Spells",
    ["Earth"] = "Earth Spells",
    ["Mystic"] = "Mystic Spells",
    ["Defense"] = "Defense Spells",
    ["Water"] = "Water Spells"
}

-- Pluralize a type name for category
local function pluralize(singular)
    return typePlurals[singular] or (singular .. " Spells")
end

-- Format description - handles line breaks (white text)
local function formatDescription(desc)
    if not desc or desc == "" then
        return {}
    end
    
    local lines = {}
    -- Split by newlines and create paragraph for each
    for line in desc:gmatch("[^\n]+") do
        local trimmed = line:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            -- White text for description
            table.insert(lines, string.format('<p>%s</p>', trimmed))
        end
    end
    
    return lines
end

-- Format the cost display (mana = white, health = orange)
local function formatCost(cost, hpCast)
    if not cost or cost == "" then
        return nil
    end
    
    local costStr = tostring(cost)
    
    if hpCast and (hpCast == "yes" or hpCast == "true" or hpCast == "1") then
        -- Health cost - orange (same color as spell name)
        return string.format('<span class="color-pumpkin">%s health</span>', costStr)
    else
        -- Mana cost - white (no special color class needed)
        return string.format('%s mana', costStr)
    end
end

-- Format the cooldown display (white)
local function formatCooldown(cd)
    if not cd or cd == "" then
        return nil
    end
    return string.format('%ss cooldown', cd)
end

-- Format the spell formula with correct colors using separate fields
-- base_damage = "20 Base Damage" (gold)
-- magic_scaling = "75% Magic" (purple/orchid)
-- damage_scaling = "65% Damage" (purple/orchid)
-- attack_speed = "+10 Attack Speed" (gold)
local function formatFormula(baseDamage, magicScaling, damageScaling, attackSpeed)
    if (not baseDamage or baseDamage == "") and 
       (not magicScaling or magicScaling == "") and 
       (not damageScaling or damageScaling == "") and
       (not attackSpeed or attackSpeed == "") then
        return nil
    end
    
    local parts = {}
    
    -- Base damage in gold
    if baseDamage and baseDamage ~= "" then
        table.insert(parts, string.format('<span class="color-gold">%s</span>', baseDamage))
    end
    
    -- Magic scaling in purple/orchid
    if magicScaling and magicScaling ~= "" then
        if #parts > 0 then
            table.insert(parts, string.format(' + <span class="color-orchid">%s</span>', magicScaling))
        else
            table.insert(parts, string.format('<span class="color-orchid">%s</span>', magicScaling))
        end
    end
    
    -- Damage scaling in purple/orchid
    if damageScaling and damageScaling ~= "" then
        if #parts > 0 then
            table.insert(parts, string.format(' + <span class="color-orchid">%s</span>', damageScaling))
        else
            table.insert(parts, string.format('<span class="color-orchid">%s</span>', damageScaling))
        end
    end
    
    -- Attack speed in gold
    if attackSpeed and attackSpeed ~= "" then
        if #parts > 0 then
            table.insert(parts, string.format(' + <span class="color-gold">%s</span>', attackSpeed))
        else
            table.insert(parts, string.format('<span class="color-gold">%s</span>', attackSpeed))
        end
    end
    
    if #parts == 0 then
        return nil
    end
    
    return string.format('<p>Spell Formula: %s</p>', table.concat(parts, ""))
end

-- Format magic requirement text
local function formatMagicRequirement(magic)
    if not magic or magic == "" or magic == "0" or tonumber(magic) == 0 then
        return nil
    end
    return string.format('<p>You need %s Magic to use this spell.</p>', magic)
end

-- Format ability requirement text
local function formatAbilityRequirement(ability)
    if not ability or ability == "" or ability == "0" or tonumber(ability) == 0 then
        return nil
    end
    return string.format('<p>You need %s Ability to use this spell.</p>', ability)
end

-- Query spell data from Cargo
local function getSpellData(name)
    local tables = 'Spells'
    local fields = 'name,sprite,type,magic,ability,cost,hp_cast,cooldown,description,base_damage,magic_scaling,damage_scaling,attack_speed'
    local args = {
        where = 'name="' .. name .. '"',
        limit = 1
    }
    
    local result = mw.ext.cargo.query(tables, fields, args)
    
    if result and result[1] then
        return result[1]
    end
    return nil
end

-- Main spell infobox function
function p.spell(frame)
    local args = frame:getParent().args
    local spellName = args.name or args[1] or mw.title.getCurrentTitle().text
    local float = args.float or "right"
    local spriteSize = args.spriteSize or "64"
    local previewMode = args.preview == "true" or args.preview == "1"
    
    local data
    
    -- If preview mode or direct data provided, use args instead of Cargo
    if previewMode or args.type or args.cost then
        data = {
            name = spellName,
            sprite = args.sprite,
            type = args.type,
            magic = args.magic,
            ability = args.ability,
            cost = args.cost,
            hp_cast = args.hp_cast or args.hpCast,
            cooldown = args.cooldown or args.cd,
            description = args.description or args.desc,
            base_damage = args.base_damage or args.baseDamage,
            magic_scaling = args.magic_scaling or args.magicScaling,
            damage_scaling = args.damage_scaling or args.damageScaling,
            attack_speed = args.attack_speed or args.attackSpeed
        }
    else
        -- Query Cargo for spell data
        data = getSpellData(spellName)
        
        if not data then
            return '<span class="error">Spell not found: ' .. (spellName or 'nil') .. '</span>'
        end
    end
    
    local spellType = string.lower(data.type or "physical")
    local typeColor = typeColors[spellType] or "coral"
    -- Sprite must be explicitly provided (like ItemEntry does with sprite=Red Book)
    local spellbookSprite = data.sprite or data.name
    
    local html = {}
    
    -- Container with tooltip-panel styling (uses existing class from Common.css)
    -- Add text-align:left to override the centered default
    if float == "none" then
        table.insert(html, '<div class="tooltip-panel font-apogea-long infobox" style="text-align:left;">')
    else
        table.insert(html, string.format('<div class="tooltip-panel font-apogea-long infobox" style="float:%s; margin-%s:15px; margin-bottom:10px; text-align:left;">', 
            float, 
            float == "right" and "left" or "right"))
    end
    
    -- Sprite
    table.insert(html, string.format('{{Sprite|%s|%s|class=pageimage}}', spellbookSprite, spriteSize))
    
    -- Spell name (orange, bitcell font) with cost and cooldown in parentheses (white)
    local costDisplay = formatCost(data.cost, data.hp_cast)
    local cooldownDisplay = formatCooldown(data.cooldown)
    
    local titleParts = {}
    if costDisplay then
        table.insert(titleParts, costDisplay)
    end
    if cooldownDisplay then
        table.insert(titleParts, cooldownDisplay)
    end
    
    if #titleParts > 0 then
        table.insert(html, string.format('<p class="font-bitcell"><span class="color-pumpkin">%s</span> (%s):</p>', 
            data.name, 
            table.concat(titleParts, ", ")))
    else
        table.insert(html, string.format('<p class="font-bitcell color-pumpkin">%s</p>', data.name))
    end
    
    -- Description (white)
    local descLines = formatDescription(data.description)
    for _, line in ipairs(descLines) do
        table.insert(html, line)
    end
    
    -- Spell type (colored based on type - e.g., "Fire Spell" in orange/coral)
    local typeDisplay = data.type or "Physical"
    typeDisplay = typeDisplay:sub(1,1):upper() .. typeDisplay:sub(2):lower()
    table.insert(html, string.format('<p class="color-%s">%s Spell</p>', typeColor, typeDisplay))
    
    -- Spell formula (base damage & attack speed in gold, magic/damage scaling in purple)
    local formulaDisplay = formatFormula(data.base_damage, data.magic_scaling, data.damage_scaling, data.attack_speed)
    if formulaDisplay then
        table.insert(html, formulaDisplay)
    end
    
    -- Magic requirement (white)
    local magicReq = formatMagicRequirement(data.magic)
    if magicReq then
        table.insert(html, magicReq)
    end
    
    -- Ability requirement (white)
    local abilityReq = formatAbilityRequirement(data.ability)
    if abilityReq then
        table.insert(html, abilityReq)
    end
    
    table.insert(html, '</div>')
    
    -- Categories (skip in preview mode)
    local categories = ''
    if not previewMode then
        categories = '[[Category:Spells]]'
        if data.type and data.type ~= "" then
            local typeCapitalized = data.type:sub(1,1):upper() .. data.type:sub(2):lower()
            categories = categories .. '[[Category:' .. pluralize(typeCapitalized) .. ']]'
        end
    end
    
    return frame:preprocess(table.concat(html, '\n')) .. categories
end

-- Compact spell display for lists/tables
function p.spellLink(frame)
    local args = frame:getParent().args
    local spellName = args.name or args[1]
    
    if not spellName then
        return '<span class="error">No spell name provided</span>'
    end
    
    local data = getSpellData(spellName)
    
    if not data then
        return '[[' .. spellName .. ']]'
    end
    
    -- Use sprite from Cargo data
    local sprite = data.sprite or spellName
    
    local html = string.format('{{Sprite|%s|16}} [[%s]]', sprite, spellName)
    
    return frame:preprocess(html)
end

return p