Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Join the Playtest on Steam Now: SpiritVale

Module:GameSkills: Difference between revisions

From SpiritVale Wiki
No edit summary
No edit summary
Line 1: Line 1:
-- Module:GameSkills
-- Module:GameSkills
--
-- Renders active skill data (Data:skills.json) into an infobox-style table,
-- and can also list all skills for a given user/class (page name).
--
--
-- Upgrades:
-- Upgrades:
--  - Adds a per-skill Level Select slider (client-side JS updates fields)
--  - Per-skill Level Select slider (client-side JS updates .sv-dyn spans)
--  - Default level = Max Level
--  - Default level = Max Level
--  - Adds .sv-skill-card + data-max-level / data-level hooks for JS
--  - .sv-skill-card + data-max-level / data-level hooks for JS
--  - Replaces large Lv1/Lv2/... lists with data-series dynamic spans
--  - Uses dynamic spans instead of long Lv1/Lv2/... lists
--  - Removes "General" + "Type" section bars and merges Level Select + Type into one unified top band
--  - Unified top band: Level Select (left) + Type/Element/Target/Cast (right)
--    (Level Select on the left; Type/Element/Target/Cast Type on the right)
--  - List-mode: wraps all skills in one wrapper panel + stable .sv-skill-item divs
--  - NEW (list-mode): Wraps all skills in ONE wrapper panel and each skill in a stable .sv-skill-item
--    so CSS dividers work reliably under Citizen/table wrappers.
--
--
-- Requires the JS you installed in MediaWiki:Common.js.
-- Requires the JS you installed in MediaWiki:Common.js.
Line 40: Line 41:
end
end
return frame.args
return frame.args
end
local function trim(s)
if type(s) ~= "string" then
return nil
end
s = mw.text.trim(s)
if s == "" then
return nil
end
return s
end
end


Line 56: Line 68:
row:tag("th"):wikitext(label):done()
row:tag("th"):wikitext(label):done()
row:tag("td"):wikitext(value):done()
row:tag("td"):wikitext(value):done()
end
local function addSectionHeader(tbl, label)
local row = tbl:tag("tr")
local cell = row:tag("th")
cell:attr("colspan", 2)
cell:addClass("spiritvale-infobox-section-header")
cell:wikitext(label)
end
local function trim(s)
if type(s) ~= "string" then
return nil
end
s = mw.text.trim(s)
if s == "" then
return nil
end
return s
end
end


Line 119: Line 112:


-- Handles either a scalar OR { Value = ..., Unit = ... }
-- Handles either a scalar OR { Value = ..., Unit = ... }
-- IMPORTANT: do NOT convert percent_decimal / percent_whole; just format around stored values.
-- IMPORTANT: wikiprep often converts unit-wrapped pairs into strings already.
local function formatUnitValue(v)
local function formatUnitValue(v)
if type(v) == "table" and v.Value ~= nil then
if type(v) == "table" and v.Value ~= nil then
Line 198: Line 191:
end
end


-- Like your old valuePairLines/valuePairText, but:
-- Base/Per Level renderer:
-- - if Per Level is a list, render a dynamic span instead of "v1 / v2 / ..."
-- - Per Level list -> dynamic span (or single value if flat)
-- - scalar Per Level stays as the old "Base" + "Per Level" lines (still short)
-- - Per Level scalar -> "Base" + "Per Level" lines (short)
local function valuePairDynamicLines(name, block, maxLevel, level)
local function valuePairDynamicLines(name, block, maxLevel, level)
if type(block) ~= "table" then
if type(block) ~= "table" then
Line 209: Line 202:
local per  = block["Per Level"]
local per  = block["Per Level"]


-- Per Level list (expanded)
-- Per Level list (expanded by wikiprep)
if type(per) == "table" then
if type(per) == "table" then
-- empty list -> show Base only
-- empty list -> show Base only
Line 244: Line 237:
end
end


-- scalar Per Level (keep old style)
-- scalar Per Level (old style)
local lines = {}
local lines = {}
local baseText = formatUnitValue(base)
local baseText = formatUnitValue(base)
Line 308: Line 301:
-- Formatting helpers
-- Formatting helpers
----------------------------------------------------------------------
----------------------------------------------------------------------
-- Forward declarations (avoid nil upvalue issues)
local basisLabel
local valuePairRawText


local function valuePairDynamicValueOnly(block, maxLevel, level)
local function valuePairDynamicValueOnly(block, maxLevel, level)
Line 341: Line 338:
end
end


-- Source (new unified Damage/Flat/Reflect/Healing) formatter
local function formatSource(src, maxLevel, level)
local function formatSource(src, maxLevel, level)
if type(src) ~= "table" then
if type(src) ~= "table" then
Line 351: Line 349:
local basis = basisLabel(src, isHealing)
local basis = basisLabel(src, isHealing)
if basis and mw.ustring.lower(tostring(basis)) == mw.ustring.lower(tostring(kind)) then
if basis and mw.ustring.lower(tostring(basis)) == mw.ustring.lower(tostring(kind)) then
basis = nil -- avoid "Healing: X Healing"
basis = nil
end
end


Line 367: Line 365:
end
end


local function basisLabel(entry, isHealing)
basisLabel = function(entry, isHealing)
if isHealing then
if isHealing then
return "Healing"
return "Healing"
Line 386: Line 384:
end
end


-- Dynamic damage entry:
-- BACKCOMPAT: old damage list formatter (safe to remove once all JSON is migrated)
-- Build a series for Lv1..LvMax, and show only the selected one.
local function formatDamageEntry(entry, maxLevel, level)
local function formatDamageEntry(entry, maxLevel, level)
if type(entry) ~= "table" then
if type(entry) ~= "table" then
Line 394: Line 391:


local isHealing = (entry.Type == "Healing")
local isHealing = (entry.Type == "Healing")
local basis = isHealing and "Healing" or basisLabel(entry)
local basis = isHealing and "Healing" or basisLabel(entry, false)


local baseRaw = entry["Base %"]
local baseRaw = entry["Base %"]
Line 470: Line 467:
end
end


-- Scaling supports either:
--  - single dict: {Percent=..., Scaling ID/Name...}
--  - list of dicts: [{...},{...}]
local function formatScaling(scaling, basisOverride)
local function formatScaling(scaling, basisOverride)
-- Accept {Percent=...} OR [{...},{...}]
if type(scaling) ~= "table" then
if type(scaling) ~= "table" then
return nil
return nil
Line 478: Line 477:
local list = scaling
local list = scaling
if #list == 0 then
if #list == 0 then
-- treat as single entry if it looks like one
if scaling.Percent ~= nil or scaling["Scaling ID"] or scaling["Scaling Name"] then
if scaling.Percent ~= nil or scaling["Scaling ID"] or scaling["Scaling Name"] then
list = { scaling }
list = { scaling }
Line 551: Line 549:


add("Cast Time", "Cast Time")
add("Cast Time", "Cast Time")
add("Cooldown", "Cooldown")
add("Cooldown", "Cooldown")
add("Duration", "Duration")
add("Duration", "Duration")


if bt["Effect Cast Time"] ~= nil then
if bt["Effect Cast Time"] ~= nil then
Line 622: Line 620:
end
end


local function valuePairRawText(block)
valuePairRawText = function(block)
if type(block) ~= "table" then
if type(block) ~= "table" then
return nil
return nil
Line 685: Line 683:
end
end


local pair = { Base = block.Base, ["Per Level"] = block["Per Level"] }
local txt = valuePairRawText(block)
local txt = valuePairRawText(pair)
return txt and mw.text.nowiki(txt) or nil
return txt and mw.text.nowiki(txt) or nil
end
end
Line 760: Line 757:
for _, s in ipairs(list) do
for _, s in ipairs(list) do
if type(s) == "table" then
if type(s) == "table" then
-- Scope renamed to Type; keep fallback for older JSON
local typ  = s.Type or s.Scope or "Target"
local typ  = s.Type or s.Scope or "Target"
local name = s["Status External Name"] or s["Status Internal Name"] or "Unknown status"
local name = s["Status External Name"] or s["Status Internal Name"] or "Unknown status"
Line 811: Line 809:
end
end


local amt = nil
local amt
if type(r["Per Level"]) == "table" and #r["Per Level"] > 0 and not isFlatList(r["Per Level"]) then
if type(r["Per Level"]) == "table" and #r["Per Level"] > 0 and not isFlatList(r["Per Level"]) then
local series = {}
local series = {}
Line 932: Line 930:


local function buildLevelSelectUI(level, maxLevel)
local function buildLevelSelectUI(level, maxLevel)
-- JS expectations:
--  .sv-level-slider => placeholder for the <input type="range">
--  .sv-level-num    => span for updating the number
local wrap = mw.html.create("div")
local wrap = mw.html.create("div")
wrap:addClass("sv-level-ui")
wrap:addClass("sv-level-ui")
Line 987: Line 982:
end
end


-- NEW keys (with fallback to old keys so you don't hard-break on older JSON)
-- NEW keys (with fallback to old keys)
addChunk("Damage",  typeBlock.Damage  or typeBlock["Damage Type"])
addChunk("Damage",  typeBlock.Damage  or typeBlock["Damage Type"])
addChunk("Element", typeBlock.Element or typeBlock["Element Type"])
addChunk("Element", typeBlock.Element or typeBlock["Element Type"])
Line 1,010: Line 1,005:
cell:addClass("sv-topband-cell")
cell:addClass("sv-topband-cell")


-- Nested table (what Common.css targets)
local inner = cell:tag("table")
local inner = cell:tag("table")
inner:addClass("sv-topband-table")
inner:addClass("sv-topband-table")
Line 1,039: Line 1,033:


local root = mw.html.create("table")
local root = mw.html.create("table")
-- IMPORTANT:
-- Don't use "wikitable" here; it triggers default borders + Citizen wrapper styling.
root:addClass("spiritvale-skill-infobox")
root:addClass("spiritvale-skill-infobox")


Line 1,049: Line 1,040:
root:attr("data-level", tostring(level))
root:attr("data-level", tostring(level))


-- Helpful flag for list-mode overrides (optional)
if opts.inList then
if opts.inList then
root:addClass("sv-skill-inlist")
root:addClass("sv-skill-inlist")
end
end


-- Top "hero" rows:
-- Hero rows
-- 1) Title row: icon + name centered (single cell)
-- 2) Description row: its own row below (still part of hero band)
local icon  = rec.Icon
local icon  = rec.Icon
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill"
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill"
local desc  = rec.Description or ""
local desc  = rec.Description or ""


-- Row 1: icon + title (single column)
local heroRow = root:tag("tr")
local heroRow = root:tag("tr")
heroRow:addClass("spiritvale-infobox-main")
heroRow:addClass("spiritvale-infobox-main")
Line 1,071: Line 1,058:


local heroInner = heroCell:tag("div")
local heroInner = heroCell:tag("div")
heroInner:addClass("spiritvale-infobox-main-left-inner") -- reuse centered flex column
heroInner:addClass("spiritvale-infobox-main-left-inner")


if icon and icon ~= "" then
if icon and icon ~= "" then
Line 1,081: Line 1,068:
:wikitext(title)
:wikitext(title)


-- Row 2: description (below, still hero-styled)
if desc ~= "" then
if desc ~= "" then
local descRow = root:tag("tr")
local descRow = root:tag("tr")
Line 1,155: Line 1,141:
end
end


addRow(root, "Timing",         formatTimingBlock(mech["Basic Timings"], maxLevel, level))
addRow(root, "Timing",           formatTimingBlock(mech["Basic Timings"], maxLevel, level))
addRow(root, "Resource Cost", formatResourceCost(mech["Resource Cost"], maxLevel, level))
addRow(root, "Resource Cost",     formatResourceCost(mech["Resource Cost"], maxLevel, level))
addRow(root, "Combo",         formatCombo(mech.Combo))
addRow(root, "Combo",             formatCombo(mech.Combo))
addRow(root, "Special Mechanics", formatMechanicEffects(mech.Effects, maxLevel, level))
addRow(root, "Special Mechanics", formatMechanicEffects(mech.Effects, maxLevel, level))
end
end


------------------------------------------------------------------
------------------------------------------------------------------
-- Source (new unified Damage/Flat/Reflect/Healing) + Scaling
-- Source (new) + Scaling, with backcompat for old Damage
------------------------------------------------------------------
------------------------------------------------------------------
if type(rec.Source) == "table" then
if type(rec.Source) == "table" then
addRow(root, "Source", formatSource(rec.Source, maxLevel, level))
addRow(root, "Source", formatSource(rec.Source, maxLevel, level))


local basisOverride = (rec.Source.Type == "Healing" or rec.Source.Healing == true) and "Healing" or nil
local basisOverride =
addRow(root, "Scaling", formatScaling(rec.Source.Scaling, basisOverride))
(rec.Source.Type == "Healing" or rec.Source.Healing == true) and "Healing" or nil


else
addRow(root, "Scaling", formatScaling(rec.Source.Scaling, basisOverride))
-- BACKCOMPAT: old Damage block (safe to delete once all JSON is migrated)
else
local dmg = rec.Damage or {}
-- BACKCOMPAT: old Damage block (safe to delete once migrated)
if next(dmg) ~= nil then
local dmg = rec.Damage or {}
local main = dmg["Main Damage"]
if next(dmg) ~= nil then
local mainNonHeal, healOnly = {}, {}
local main = dmg["Main Damage"]
local mainNonHeal, healOnly = {}, {}


if type(main) == "table" then
if type(main) == "table" then
for _, d in ipairs(main) do
for _, d in ipairs(main) do
if type(d) == "table" and d.Type == "Healing" then
if type(d) == "table" and d.Type == "Healing" then
table.insert(healOnly, d)
table.insert(healOnly, d)
else
else
table.insert(mainNonHeal, d)
table.insert(mainNonHeal, d)
end
end
end
end
end
end


local flatList = dmg["Flat Damage"]
local flatList = dmg["Flat Damage"]
local reflList = dmg["Reflect Damage"]
local reflList = dmg["Reflect Damage"]


local flatHas = (type(flatList) == "table" and #flatList > 0)
local flatHas = (type(flatList) == "table" and #flatList > 0)
local reflHas = (type(reflList) == "table" and #reflList > 0)
local reflHas = (type(reflList) == "table" and #reflList > 0)


local pureHealing = (#healOnly > 0) and (#mainNonHeal == 0) and (not flatHas) and (not reflHas)
local pureHealing =
(#healOnly > 0) and (#mainNonHeal == 0) and (not flatHas) and (not reflHas)


addRow(root, "Main Damage",    formatDamageList(mainNonHeal, maxLevel, level, (#mainNonHeal > 1)))
addRow(root, "Main Damage",    formatDamageList(mainNonHeal, maxLevel, level, (#mainNonHeal > 1)))
addRow(root, "Flat Damage",    formatDamageList(flatList, maxLevel, level, false))
addRow(root, "Flat Damage",    formatDamageList(flatList, maxLevel, level, false))
addRow(root, "Reflect Damage", formatDamageList(reflList, maxLevel, level, false))
addRow(root, "Reflect Damage", formatDamageList(reflList, maxLevel, level, false))
addRow(root, "Healing",        formatDamageList(healOnly, maxLevel, level, false))
addRow(root, "Healing",        formatDamageList(healOnly, maxLevel, level, false))


addRow(root, "Scaling", formatScaling(dmg.Scaling, pureHealing and "Healing" or nil))
addRow(root, "Scaling", formatScaling(dmg.Scaling, pureHealing and "Healing" or nil))
end
end
end
end


------------------------------------------------------------------
------------------------------------------------------------------
Line 1,272: Line 1,260:
end
end


-- ONE wrapper panel (table-like list mode)
local root = mw.html.create("div")
local root = mw.html.create("div")
root:addClass("sv-skill-collection")
root:addClass("sv-skill-collection")


for _, rec in ipairs(matches) do
for _, rec in ipairs(matches) do
-- Stable per-skill wrapper so dividers work even under Citizen wrappers
local item = root:tag("div"):addClass("sv-skill-item")
local item = root:tag("div"):addClass("sv-skill-item")
item:wikitext(buildInfobox(rec, { showUsers = false, inList = true }))
item:wikitext(buildInfobox(rec, { showUsers = false, inList = true }))