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
 
(2 intermediate revisions by the same user not shown)
Line 23: Line 23:


local skillsCache
local skillsCache
local eventsCache


-- getSkills: lazy-load + cache skill dataset from GameData.
-- getSkills: lazy-load + cache skill dataset from GameData.
local function getSkills()
local function getSkills()
if not skillsCache then
        if not skillsCache then
skillsCache = GameData.loadSkills()
                skillsCache = GameData.loadSkills()
end
        end
return skillsCache
        return skillsCache
end
 
local function getEvents()
        if eventsCache == nil then
                if type(GameData.loadEvents) == "function" then
                        eventsCache = GameData.loadEvents()
                else
                        eventsCache = false
                end
        end
 
        return eventsCache
end
end


Line 93: Line 106:
-- listToText: join an array into a readable string.
-- listToText: join an array into a readable string.
local function listToText(list, sep)
local function listToText(list, sep)
if type(list) ~= "table" or #list == 0 then
        if type(list) ~= "table" or #list == 0 then
return nil
                return nil
end
        end
return table.concat(list, sep or ", ")
        return table.concat(list, sep or ", ")
end
end


-- isNoneLike: treat common "none" spellings as empty.
local function resolveDisplayName(v, kind)
local function isNoneLike(v)
        if v == nil then return nil end
if v == nil then return true end
 
local s = mw.text.trim(tostring(v))
        local function firstString(keys, source)
if s == "" then return true end
                for _, key in ipairs(keys) do
s = mw.ustring.lower(s)
                        local candidate = source[key]
return (s == "none" or s == "no" or s == "n/a" or s == "na" or s == "null")
                        if type(candidate) == "string" and candidate ~= "" then
end
                                return candidate
                        end
                end
                return nil
        end


-- addRow: add a standard <tr><th>Label</th><td>Value</td></tr> row.
        if type(v) == "table" then
local function addRow(tbl, label, value, rowClass, dataKey)
                local primaryKeys = { "External Name", "Display Name", "Name" }
if value == nil or value == "" then
                local extendedKeys = { "Skill External Name", "Status External Name" }
return
                local internalKeys = { "Internal Name", "Internal ID", "ID", "InternalID", "Skill Internal Name", "InternalID" }
end


local row = tbl:tag("tr")
                return firstString(primaryKeys, v)
row:addClass("sv-row")
                        or firstString(extendedKeys, v)
if rowClass then row:addClass(rowClass) end
                        or firstString(internalKeys, v)
if dataKey then row:attr("data-field", dataKey) end
        end
 
        if type(v) == "string" then
                if kind == "event" then
                        local events = getEvents()
                        if events and events.byId and events.byId[v] then
                                local mapped = resolveDisplayName(events.byId[v], "event")
                                if mapped then
                                        return mapped
                                end
                        end
                end
 
                return v
        end
 
        return tostring(v)
end


row:tag("th"):wikitext(label):done()
local function resolveEventName(v)
row:tag("td"):wikitext(value):done()
        local resolved = resolveDisplayName(v, "event")
        if type(resolved) == "string" then
                return resolved
        end
        return (resolved ~= nil) and tostring(resolved) or nil
end
end


-- formatUnitValue: format {Value, Unit} blocks (or scalar) for display.
local function resolveSkillNameFromEvent(ev)
local function formatUnitValue(v)
        if type(ev) ~= "table" then
if type(v) == "table" and v.Value ~= nil then
                return resolveDisplayName(ev, "skill") or "Unknown skill"
local unit = v.Unit
        end
local val  = v.Value
 
        local displayKeys = {
                "Skill External Name",
                "External Name",
                "Display Name",
                "Name",
                "Skill Name",
        }


if unit == "percent_decimal" or unit == "percent_whole" or unit == "percent" then
        for _, key in ipairs(displayKeys) do
return tostring(val) .. "%"
                local candidate = resolveDisplayName(ev[key], "skill")
elseif unit == "seconds" then
                if candidate then
return tostring(val) .. "s"
                        return candidate
elseif unit == "meters" then
                end
return tostring(val) .. "m"
        end
elseif unit == "tiles" then
return tostring(val) .. " tiles"
elseif unit and unit ~= "" then
return tostring(val) .. " " .. tostring(unit)
else
return tostring(val)
end
end


return (v ~= nil) and tostring(v) or nil
        local internalKeys = {
end
                "Skill Internal Name",
                "Skill ID",
                "Internal Name",
                "Internal ID",
                "ID",
        }


----------------------------------------------------------------------
        for _, key in ipairs(internalKeys) do
-- Dynamic spans (JS-driven)
                local candidate = ev[key]
----------------------------------------------------------------------
                if type(candidate) == "string" and candidate ~= "" then
                        return candidate
                end
        end


-- dynSpan: render a JS-updated span for a level series.
        return "Unknown skill"
local function dynSpan(series, level)
end
if type(series) ~= "table" or #series == 0 then
return nil
end


level = clamp(level or #series, 1, #series)
-- isNoneLike: treat common "none" spellings as empty.
local function isNoneLike(v)
if v == nil then return true end
local s = mw.text.trim(tostring(v))
if s == "" then return true end
s = mw.ustring.lower(s)
return (s == "none" or s == "no" or s == "n/a" or s == "na" or s == "null")
end
 
-- addRow: add a standard <tr><th>Label</th><td>Value</td></tr> row.
local function addRow(tbl, label, value, rowClass, dataKey)
if value == nil or value == "" then
return
end


local span = mw.html.create("span")
local row = tbl:tag("tr")
span:addClass("sv-dyn")
row:addClass("sv-row")
span:attr("data-series", mw.text.jsonEncode(series))
if rowClass then row:addClass(rowClass) end
span:wikitext(mw.text.nowiki(series[level] or ""))
if dataKey then row:attr("data-field", dataKey) end


return tostring(span)
row:tag("th"):wikitext(label):done()
row:tag("td"):wikitext(value):done()
end
end


-- isFlatList: true if all values in list are identical.
-- formatUnitValue: format {Value, Unit} blocks (or scalar) for display.
local function isFlatList(list)
local function formatUnitValue(v)
if type(list) ~= "table" or #list == 0 then
if type(v) == "table" and v.Value ~= nil then
return false
local unit = v.Unit
end
local val  = v.Value
local first = tostring(list[1])
 
for i = 2, #list do
if unit == "percent_decimal" or unit == "percent_whole" or unit == "percent" then
if tostring(list[i]) ~= first then
return tostring(val) .. "%"
return false
elseif unit == "seconds" then
return tostring(val) .. "s"
elseif unit == "meters" then
return tostring(val) .. "m"
elseif unit == "tiles" then
return tostring(val) .. " tiles"
elseif unit and unit ~= "" then
return tostring(val) .. " " .. tostring(unit)
else
return tostring(val)
end
end
end
end
return true
 
return (v ~= nil) and tostring(v) or nil
end
end


-- isNonZeroScalar: detect if a value is present and not effectively zero.
----------------------------------------------------------------------
local function isNonZeroScalar(v)
-- Dynamic spans (JS-driven)
if v == nil then return false end
----------------------------------------------------------------------
if type(v) == "number" then return v ~= 0 end
 
if type(v) == "string" then
-- dynSpan: render a JS-updated span for a level series.
local n = tonumber(v)
local function dynSpan(series, level)
if n == nil then return v ~= "" end
if type(series) ~= "table" or #series == 0 then
return n ~= 0
return nil
end
 
level = clamp(level or #series, 1, #series)
 
local span = mw.html.create("span")
span:addClass("sv-dyn")
span:attr("data-series", mw.text.jsonEncode(series))
span:wikitext(mw.text.nowiki(series[level] or ""))
 
return tostring(span)
end
 
-- isFlatList: true if all values in list are identical.
local function isFlatList(list)
if type(list) ~= "table" or #list == 0 then
return false
end
end
if type(v) == "table" and v.Value ~= nil then
local first = tostring(list[1])
return isNonZeroScalar(v.Value)
for i = 2, #list do
if tostring(list[i]) ~= first then
return false
end
end
end
return true
return true
end
end


-- isZeroish: aggressively treat common “zero” text forms as zero.
-- isNonZeroScalar: detect if a value is present and not effectively zero.
local function isNonZeroScalar(v)
if v == nil then return false end
if type(v) == "number" then return v ~= 0 end
if type(v) == "string" then
local n = tonumber(v)
if n == nil then return v ~= "" end
return n ~= 0
end
if type(v) == "table" and v.Value ~= nil then
return isNonZeroScalar(v.Value)
end
return true
end
 
-- isZeroish: aggressively treat common “zero” text forms as zero.
local function isZeroish(v)
local function isZeroish(v)
if v == nil then return true end
if v == nil then return true end
Line 892: Line 991:


local req = rec.Requirements or {}
local req = rec.Requirements or {}
local reqSkills = (type(req["Required Skills"]) == "table") and req["Required Skills"] or {}
local reqSkillsRaw = (type(req["Required Skills"]) == "table") and req["Required Skills"] or {}
local reqWeapons = (type(req["Required Weapons"]) == "table") and req["Required Weapons"] or {}
local reqWeaponsRaw = (type(req["Required Weapons"]) == "table") and req["Required Weapons"] or {}
local reqStances = (type(req["Required Stances"]) == "table") and req["Required Stances"] or {}
local reqStancesRaw = (type(req["Required Stances"]) == "table") and req["Required Stances"] or {}
 
local reqSkills = {}
for _, rs in ipairs(reqSkillsRaw) do
if type(rs) == "table" then
local nameReq = rs["Skill External Name"] or rs["Skill Internal Name"] or "Unknown"
local lvlReq  = rs["Required Level"]
if lvlReq then
table.insert(reqSkills, string.format("%s (Lv.%s)", mw.text.nowiki(nameReq), mw.text.nowiki(tostring(lvlReq))))
else
table.insert(reqSkills, mw.text.nowiki(nameReq))
end
end
end
 
local reqWeapons = {}
for _, w in ipairs(reqWeaponsRaw) do
local wn = trim(w)
if wn then table.insert(reqWeapons, mw.text.nowiki(wn)) end
end
 
local reqStances = {}
for _, s in ipairs(reqStancesRaw) do
local sn = trim(s)
if sn then table.insert(reqStances, mw.text.nowiki(sn)) end
end


local hasNotes = (#notesList > 0)
local hasNotes = (#notesList > 0)
local hasReq = (#reqSkills > 0) or (#reqWeapons > 0) or (#reqStances > 0)
local hasReq = (#reqSkills > 0) or (#reqWeapons > 0) or (#reqStances > 0)


local wrap = mw.html.create("div")
local wrap = mw.html.create("div")
Line 913: Line 1,038:
textBox:addClass("sv-herobar-text")
textBox:addClass("sv-herobar-text")


local titleBox = textBox:tag("div")
local titleRow = textBox:tag("div")
titleRow:addClass("sv-herobar-title-row")
 
local titleBox = titleRow:tag("div")
titleBox:addClass("spiritvale-infobox-title")
titleBox:addClass("spiritvale-infobox-title")
titleBox:wikitext(title)


if hasNotes and icon and icon ~= "" then
if hasNotes then
local notesBtn = mw.html.create("span")
local notesBtn = mw.html.create("span")
notesBtn:addClass("sv-tip-btn sv-tip-btn--notes")
notesBtn:addClass("sv-tip-btn sv-tip-btn--notes")
Line 925: Line 1,054:
notesBtn:attr("aria-expanded", "false")
notesBtn:attr("aria-expanded", "false")
notesBtn:tag("span"):addClass("sv-ico sv-ico--info"):attr("aria-hidden", "true"):wikitext("i")
notesBtn:tag("span"):addClass("sv-ico sv-ico--info"):attr("aria-hidden", "true"):wikitext("i")
iconBox:node(notesBtn)
titleRow:node(notesBtn)
end
end


if hasNotes and (not icon or icon == "") then
if hasReq then
local notesBtn = mw.html.create("span")
local pillRow = wrap:tag("div")
notesBtn:addClass("sv-tip-btn sv-tip-btn--notes")
notesBtn:attr("role", "button")
notesBtn:attr("tabindex", "0")
notesBtn:attr("data-sv-tip", "notes")
notesBtn:attr("aria-label", "Notes")
notesBtn:attr("aria-expanded", "false")
notesBtn:tag("span"):addClass("sv-ico sv-ico--info"):attr("aria-hidden", "true"):wikitext("i")
titleBox:node(notesBtn)
end
 
titleBox:wikitext(title)
 
if hasReq then
local pillRow = textBox:tag("div")
pillRow:addClass("sv-pill-row")
pillRow:addClass("sv-pill-row")
local pill = mw.html.create("span")
pillRow:addClass("sv-pill-row--req")
local pill = pillRow:tag("span")
pill:addClass("sv-pill sv-pill--req sv-tip-btn")
pill:addClass("sv-pill sv-pill--req sv-tip-btn")
pill:attr("role", "button")
pill:attr("role", "button")
Line 953: Line 1,069:
pill:attr("aria-expanded", "false")
pill:attr("aria-expanded", "false")
pill:wikitext("Requirements")
pill:wikitext("Requirements")
pillRow:node(pill)
end
end


Line 968: Line 1,083:
reqContent:addClass("sv-tip-content")
reqContent:addClass("sv-tip-content")
reqContent:attr("data-sv-tip-content", "req")
reqContent:attr("data-sv-tip-content", "req")
reqContent:tag("div"):addClass("sv-tip-title"):wikitext("Requirements")


if #reqSkills > 0 then
if #reqSkills > 0 then
local skillLines = {}
local section = reqContent:tag("div")
for _, rs in ipairs(reqSkills) do
section:addClass("sv-tip-section")
if type(rs) == "table" then
section:tag("span"):addClass("sv-tip-label"):wikitext("Required Skills")
local nameReq = rs["Skill External Name"] or rs["Skill Internal Name"] or "Unknown"
section:tag("div"):wikitext(table.concat(reqSkills, "<br />"))
local lvlReq  = rs["Required Level"]
if lvlReq then
table.insert(skillLines, string.format("%s (Lv.%s)", mw.text.nowiki(nameReq), mw.text.nowiki(tostring(lvlReq))))
else
table.insert(skillLines, mw.text.nowiki(nameReq))
end
end
end
if #skillLines > 0 then
local section = reqContent:tag("div")
section:addClass("sv-tip-section")
section:tag("span"):addClass("sv-tip-label"):wikitext("Required Skills")
section:tag("div"):wikitext(table.concat(skillLines, "<br />"))
end
end
end


if #reqWeapons > 0 then
if #reqWeapons > 0 then
local weapons = {}
local section = reqContent:tag("div")
for _, w in ipairs(reqWeapons) do
section:addClass("sv-tip-section")
local wn = trim(w)
section:tag("span"):addClass("sv-tip-label"):wikitext("Required Weapons")
if wn then table.insert(weapons, mw.text.nowiki(wn)) end
section:tag("div"):wikitext(table.concat(reqWeapons, ", "))
end
if #weapons > 0 then
local section = reqContent:tag("div")
section:addClass("sv-tip-section")
section:tag("span"):addClass("sv-tip-label"):wikitext("Required Weapons")
section:tag("div"):wikitext(table.concat(weapons, ", "))
end
end
end


if #reqStances > 0 then
if #reqStances > 0 then
local stances = {}
local section = reqContent:tag("div")
for _, s in ipairs(reqStances) do
section:addClass("sv-tip-section")
local sn = trim(s)
section:tag("span"):addClass("sv-tip-label"):wikitext("Required Stances")
if sn then table.insert(stances, mw.text.nowiki(sn)) end
section:tag("div"):wikitext(table.concat(reqStances, ", "))
end
if #stances > 0 then
local section = reqContent:tag("div")
section:addClass("sv-tip-section")
section:tag("span"):addClass("sv-tip-label"):wikitext("Required Stances")
section:tag("div"):wikitext(table.concat(stances, ", "))
end
end
end
end
end
Line 1,865: Line 1,951:


-- Users (hide on direct skill page)
-- Users (hide on direct skill page)
if showUsers then
        if showUsers then
local users = rec.Users or {}
                local users = rec.Users or {}
addRow(root, "Classes",  listToText(users.Classes),  "sv-row-users", "Users.Classes")
                addRow(root, "Classes",  listToText(users.Classes),  "sv-row-users", "Users.Classes")
addRow(root, "Summons",  listToText(users.Summons),  "sv-row-users", "Users.Summons")
                addRow(root, "Summons",  listToText(users.Summons),  "sv-row-users", "Users.Summons")
addRow(root, "Monsters", listToText(users.Monsters), "sv-row-users", "Users.Monsters")
                addRow(root, "Monsters", listToText(users.Monsters), "sv-row-users", "Users.Monsters")
addRow(root, "Events",   listToText(users.Events),   "sv-row-users", "Users.Events")
                do
end
                        local eventsList = {}
                        if type(users.Events) == "table" then
                                for _, ev in ipairs(users.Events) do
                                        local name = resolveEventName(ev) or ev
                                        if name ~= nil then
                                                table.insert(eventsList, mw.text.nowiki(tostring(name)))
                                        end
                                end
                        end
                        addRow(root, "Events", listToText(eventsList), "sv-row-users", "Users.Events")
                end
        end


         -- Mechanics (keep small extras only)
         -- Mechanics (keep small extras only)
Line 1,977: Line 2,074:
end
end


-- Events
        -- Events
local function formatEvents(list)
        local function formatEvents(list)
if type(list) ~= "table" or #list == 0 then return nil end
                if type(list) ~= "table" or #list == 0 then return nil end
local parts = {}
                local parts = {}
for _, ev in ipairs(list) do
                for _, ev in ipairs(list) do
if type(ev) == "table" then
                        if type(ev) == "table" then
local action = ev.Action or "On event"
                                local action = resolveDisplayName(ev.Action, "event") or ev.Action or "On event"
local name  = ev["Skill Internal Name"] or ev["Skill External Name"] or "Unknown skill"
                                local name  = resolveSkillNameFromEvent(ev)
table.insert(parts, string.format("%s → %s", action, name))
                                table.insert(parts, string.format("%s → %s", mw.text.nowiki(action), mw.text.nowiki(name)))
end
                        end
end
                end
return (#parts > 0) and table.concat(parts, "<br />") or nil
                return (#parts > 0) and table.concat(parts, "<br />") or nil
end
        end


local eventsText = formatEvents(rec.Events)
local eventsText = formatEvents(rec.Events)