Module:Spell
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-fire-hit">%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-fire-hit">%s</span> (%s):</p>',
data.name,
table.concat(titleParts, ", ")))
else
table.insert(html, string.format('<p class="font-bitcell color-fire-hit">%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