Module:GameSkills: Difference between revisions
From SpiritVale Wiki
More actions
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 | |||
skillsCache = GameData.loadSkills() | |||
end | |||
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 | |||
return nil | |||
end | |||
return table.concat(list, sep or ", ") | |||
end | end | ||
local function resolveDisplayName(v, kind) | |||
local function | if v == nil then return nil end | ||
local function firstString(keys, source) | |||
for _, key in ipairs(keys) do | |||
local candidate = source[key] | |||
if type(candidate) == "string" and candidate ~= "" then | |||
end | return candidate | ||
end | |||
end | |||
return nil | |||
end | |||
if type(v) == "table" then | |||
local | local primaryKeys = { "External Name", "Display Name", "Name" } | ||
local extendedKeys = { "Skill External Name", "Status External Name" } | |||
local internalKeys = { "Internal Name", "Internal ID", "ID", "InternalID", "Skill Internal Name", "InternalID" } | |||
return firstString(primaryKeys, v) | |||
or firstString(extendedKeys, v) | |||
or firstString(internalKeys, v) | |||
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 | |||
local function resolveEventName(v) | |||
local resolved = resolveDisplayName(v, "event") | |||
if type(resolved) == "string" then | |||
return resolved | |||
end | |||
return (resolved ~= nil) and tostring(resolved) or nil | |||
end | end | ||
local function resolveSkillNameFromEvent(ev) | |||
local function | if type(ev) ~= "table" then | ||
return resolveDisplayName(ev, "skill") or "Unknown skill" | |||
end | |||
local displayKeys = { | |||
"Skill External Name", | |||
"External Name", | |||
"Display Name", | |||
"Name", | |||
"Skill Name", | |||
} | |||
for _, key in ipairs(displayKeys) do | |||
local candidate = resolveDisplayName(ev[key], "skill") | |||
if candidate then | |||
return candidate | |||
end | |||
end | |||
local internalKeys = { | |||
"Skill Internal Name", | |||
"Skill ID", | |||
"Internal Name", | |||
"Internal ID", | |||
"ID", | |||
} | |||
for _, key in ipairs(internalKeys) do | |||
local candidate = ev[key] | |||
if type(candidate) == "string" and candidate ~= "" then | |||
return candidate | |||
end | |||
end | |||
return "Unknown skill" | |||
end | |||
-- 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 | local row = tbl:tag("tr") | ||
row:addClass("sv-row") | |||
if rowClass then row:addClass(rowClass) end | |||
if dataKey then row:attr("data-field", dataKey) end | |||
row:tag("th"):wikitext(label):done() | |||
row:tag("td"):wikitext(value):done() | |||
end | end | ||
-- | -- formatUnitValue: format {Value, Unit} blocks (or scalar) for display. | ||
local function | local function formatUnitValue(v) | ||
if type( | if type(v) == "table" and v.Value ~= nil then | ||
return | local unit = v.Unit | ||
local val = v.Value | |||
if unit == "percent_decimal" or unit == "percent_whole" or unit == "percent" then | |||
return tostring(val) .. "%" | |||
return | 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 | |||
return (v ~= nil) and tostring(v) or nil | |||
end | end | ||
-- | ---------------------------------------------------------------------- | ||
local function | -- Dynamic spans (JS-driven) | ||
if | ---------------------------------------------------------------------- | ||
-- dynSpan: render a JS-updated span for a level series. | |||
local function dynSpan(series, level) | |||
if type(series) ~= "table" or #series == 0 then | |||
return | 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 | local first = tostring(list[1]) | ||
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 | local reqSkillsRaw = (type(req["Required Skills"]) == "table") and req["Required Skills"] or {} | ||
local | local reqWeaponsRaw = (type(req["Required Weapons"]) == "table") and req["Required Weapons"] or {} | ||
local | 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 = | 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 | 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") | ||
titleRow:node(notesBtn) | |||
end | end | ||
if hasReq then | |||
local pillRow = wrap:tag("div") | |||
if hasReq then | |||
local pillRow = | |||
pillRow:addClass("sv-pill-row") | pillRow:addClass("sv-pill-row") | ||
local pill = | 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") | ||
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") | ||
if #reqSkills > 0 then | if #reqSkills > 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(reqSkills, "<br />")) | |||
end | end | ||
if #reqWeapons > 0 then | if #reqWeapons > 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(reqWeapons, ", ")) | |||
end | end | ||
if #reqStances > 0 then | if #reqStances > 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(reqStances, ", ")) | |||
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 | |||
local users = rec.Users or {} | |||
addRow(root, "Classes", listToText(users.Classes), "sv-row-users", "Users.Classes") | |||
addRow(root, "Summons", listToText(users.Summons), "sv-row-users", "Users.Summons") | |||
addRow(root, "Monsters", listToText(users.Monsters), "sv-row-users", "Users.Monsters") | |||
do | |||
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 | |||
local function formatEvents(list) | |||
if type(list) ~= "table" or #list == 0 then return nil end | |||
local parts = {} | |||
for _, ev in ipairs(list) do | |||
if type(ev) == "table" then | |||
local action = resolveDisplayName(ev.Action, "event") or ev.Action or "On event" | |||
local name = resolveSkillNameFromEvent(ev) | |||
table.insert(parts, string.format("%s → %s", mw.text.nowiki(action), mw.text.nowiki(name))) | |||
end | |||
end | |||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | |||
local eventsText = formatEvents(rec.Events) | local eventsText = formatEvents(rec.Events) | ||