Module:GameInfo/Skills: Difference between revisions
From SpiritVale Wiki
More actions
No edit summary Tags: Mobile edit Mobile web edit |
No edit summary Tags: Mobile edit Mobile web edit |
||
| Line 43: | Line 43: | ||
end | end | ||
-- --------------------------------------------------------------------------- | |||
-- NOWIKI / STRIP MARKERS | |||
-- MediaWiki may pass nowiki content as strip markers (UNIQ...). We must unstrip | |||
-- before jsonDecode if users/bots wrap |data= in <nowiki>...</nowiki>. | |||
-- --------------------------------------------------------------------------- | |||
local function _clean_control_chars(s) | |||
if type(s) ~= "string" then return s end | |||
-- Remove ASCII control chars except tab/newline/carriage return. | |||
return (s:gsub("[%z\1-\8\11\12\14-\31\127]", "")) | |||
end | |||
local function _unstrip_nowiki(s) | |||
if type(s) ~= "string" then return s end | |||
s = _clean_control_chars(s) | |||
if mw.text and type(mw.text.unstripNoWiki) == "function" then | |||
local ok, out = pcall(mw.text.unstripNoWiki, s) | |||
if ok and type(out) == "string" then return out end | |||
end | |||
if mw.text and type(mw.text.unstrip) == "function" then | |||
local ok, out = pcall(mw.text.unstrip, s) | |||
if ok and type(out) == "string" then return out end | |||
end | |||
return s | |||
end | |||
-- --------------------------------------------------------------------------- | |||
-- Definitions-safe rendering | |||
-- We only expand the {{def}} template (never arbitrary templates). | |||
-- We also normalize literal "\u007C" sequences (double-escaped pipes). | |||
-- --------------------------------------------------------------------------- | |||
local DEF_DOMAINS = { | |||
Cast = true, Damage = true, Element = true, Aura = true, | |||
Event = true, Stance = true, Stat = true, Target = true, | |||
} | |||
local function _normalize_pipe_tokens(s) | |||
s = _trim(s) | |||
if s == "" then return "" end | |||
-- Convert literal backslash-u pipes into real pipes (handles "\\u007C" cases after jsonDecode). | |||
s = s:gsub("\\u007C", "|") | |||
-- Handle common HTML entities (just in case). | |||
s = s:gsub("|", "|"):gsub("|", "|") | |||
return s | |||
end | |||
local function _parse_def_wt(s) | |||
-- Accept: {{def|Domain|Key|noicon=1}} (also tolerates whitespace) | |||
s = _trim(s) | |||
s = _normalize_pipe_tokens(s) | |||
if not s:match("^%{%{%s*[Dd][Ee][Ff]%s*%|") then return nil end | |||
if not s:match("%}%}%s*$") then return nil end | |||
-- Strip outer braces | |||
local inner = s:gsub("^%{%{%s*[Dd][Ee][Ff]%s*%|", "") | |||
inner = inner:gsub("%}%}%s*$", "") | |||
local parts = {} | |||
for part in inner:gmatch("([^|]+)") do | |||
part = _trim(part) | |||
if part ~= "" then table.insert(parts, part) end | |||
end | |||
if #parts < 2 then return nil end | |||
local args = { parts[1], parts[2] } -- domain, key | |||
for i = 3, #parts do | |||
local p = parts[i] | |||
local eq = p:find("=", 1, true) | |||
if eq then | |||
local k = _trim(p:sub(1, eq - 1)) | |||
local v = _trim(p:sub(eq + 1)) | |||
if k ~= "" then args[k] = v end | |||
else | |||
table.insert(args, p) | |||
end | |||
end | |||
return args | |||
end | |||
local function _render_def_or_text(frame, s) | |||
s = _trim(s) | |||
if s == "" then return nil end | |||
s = _normalize_pipe_tokens(s) | |||
-- 1) Template-shaped: {{def|...}} | |||
local def_args = _parse_def_wt(s) | |||
if def_args then | |||
return frame:expandTemplate{ title = "def", args = def_args } | |||
end | |||
-- 2) Domain|Key token (only for known domains) | |||
local bar = s:find("|", 1, true) | |||
if bar then | |||
local domain = _trim(s:sub(1, bar - 1)) | |||
local key = _trim(s:sub(bar + 1)) | |||
if DEF_DOMAINS[domain] and key ~= "" then | |||
return frame:expandTemplate{ title = "def", args = { domain, key } } | |||
end | |||
end | |||
-- 3) Plain text, fully escaped. | |||
return mw.text.nowiki(s) | |||
end | |||
-- --------------------------------------------------------------------------- | |||
-- JSON decode + schema routing | |||
-- --------------------------------------------------------------------------- | |||
local function _decode_payload(frame) | local function _decode_payload(frame) | ||
local raw = GI.arg(frame, "data", "") | local raw = GI.arg(frame, "data", "") | ||
raw = _trim(raw) | raw = _trim(raw) | ||
if raw == "" then return nil, "missing |data=", "" end | if raw == "" then return nil, "missing |data=", "" end | ||
-- Restore nowiki-wrapped JSON if present. | |||
raw = _unstrip_nowiki(raw) | |||
local ok, decoded = pcall(mw.text.jsonDecode, raw) | local ok, decoded = pcall(mw.text.jsonDecode, raw) | ||
| Line 55: | Line 170: | ||
return nil, "json is not object", raw | return nil, "json is not object", raw | ||
end | end | ||
return nil, "unsupported schema=" .. tostring( | local schema = decoded.schema | ||
if schema ~= 1 and schema ~= 2 then | |||
return nil, "unsupported schema=" .. tostring(schema), raw | |||
end | end | ||
return decoded, nil, "" | return decoded, nil, "" | ||
end | end | ||
-- --------------------------------------------------------------------------- | |||
-- Schema 2 -> internal normalization (keeps existing DOM/layout code) | |||
-- --------------------------------------------------------------------------- | |||
local function _normalize_file_title(page) | local function _normalize_file_title(page) | ||
page = _trim(page) | page = _trim(page) | ||
| Line 140: | Line 261: | ||
end | end | ||
local function _meta_lines(lines) | local function _meta_lines(frame, lines) | ||
lines = _safe_tbl(lines) | lines = _safe_tbl(lines) | ||
local wrap = mw.html.create("span"):addClass("sv-meta-lines") | local wrap = mw.html.create("span"):addClass("sv-meta-lines") | ||
local any = false | local any = false | ||
for _, ln in ipairs(lines) do | for _, ln in ipairs(lines) do | ||
local s = _trim(ln) | local s = _trim(ln) | ||
if s ~= "" then | if s ~= "" then | ||
wrap:tag("span"):wikitext( | local wt = _render_def_or_text(frame, s) or mw.text.nowiki("—") | ||
wrap:tag("span"):wikitext(wt) | |||
any = true | any = true | ||
end | end | ||
end | end | ||
if not any then | if not any then | ||
wrap:tag("span"):wikitext("—") | wrap:tag("span"):wikitext("—") | ||
end | end | ||
return wrap | return wrap | ||
end | end | ||
| Line 222: | Line 329: | ||
end | end | ||
local function _build_grouped_disclose(root_id, key, label, groups) | local function _build_grouped_disclose(frame, root_id, key, label, groups) | ||
local total = _count_group_items(groups) | local total = _count_group_items(groups) | ||
if total == 0 then return nil, 0 end | if total == 0 then return nil, 0 end | ||
| Line 267: | Line 374: | ||
local li = ul:tag("li") | local li = ul:tag("li") | ||
local label_node = _meta_lines(it.label_lines) | local label_node = _meta_lines(frame, it.label_lines) | ||
local link = _safe_tbl(it.link) | local link = _safe_tbl(it.link) | ||
| Line 282: | Line 389: | ||
end | end | ||
local function _build_req_users(root_id, requirements, users) | local function _build_req_users(frame, root_id, requirements, users) | ||
requirements = _safe_tbl(requirements) | requirements = _safe_tbl(requirements) | ||
users = _safe_tbl(users) | users = _safe_tbl(users) | ||
local req_box, req_n = _build_grouped_disclose(root_id, "req", "Requirements", _safe_tbl(requirements.groups)) | local req_box, req_n = _build_grouped_disclose(frame, root_id, "req", "Requirements", _safe_tbl(requirements.groups)) | ||
local usr_box, usr_n = _build_grouped_disclose(root_id, "usr", "Users", _safe_tbl(users.groups)) | local usr_box, usr_n = _build_grouped_disclose(frame, root_id, "usr", "Users", _safe_tbl(users.groups)) | ||
if req_n == 0 and usr_n == 0 then return nil end | if req_n == 0 and usr_n == 0 then return nil end | ||
| Line 297: | Line 404: | ||
end | end | ||
local function _build_identity(root_id, identity, notes_wt) | local function _build_identity(frame, root_id, identity, notes_wt) | ||
identity = _safe_tbl(identity) | identity = _safe_tbl(identity) | ||
local root = mw.html.create("div"):addClass("sv-skill-head") | local root = mw.html.create("div"):addClass("sv-skill-head") | ||
| Line 329: | Line 436: | ||
end | end | ||
local function _build_meta_row(meta_row) | local function _build_meta_row(frame, meta_row) | ||
meta_row = _safe_tbl(meta_row) | meta_row = _safe_tbl(meta_row) | ||
| Line 349: | Line 456: | ||
local wrap = card:tag("div"):addClass("sv-meta-textwrap") | local wrap = card:tag("div"):addClass("sv-meta-textwrap") | ||
wrap:tag("div"):addClass("sv-meta-text"):node(_meta_lines(cell.label_lines)) | wrap:tag("div"):addClass("sv-meta-text"):node(_meta_lines(frame, cell.label_lines)) | ||
end | end | ||
| Line 355: | Line 462: | ||
end | end | ||
-- Level normalization | -- Level normalization (schema 1): base_max + max | ||
local function _normalize_level(level_obj) | local function _normalize_level(level_obj) | ||
level_obj = _safe_tbl(level_obj) | level_obj = _safe_tbl(level_obj) | ||
| Line 432: | Line 536: | ||
col:tag("div"):addClass("sv-scaling-label"):wikitext("Damage") | col:tag("div"):addClass("sv-scaling-label"):wikitext("Damage") | ||
end | end | ||
do | do | ||
| Line 544: | Line 646: | ||
local pills = panel:tag("div"):addClass("sv-tab-pills") | local pills = panel:tag("div"):addClass("sv-tab-pills") | ||
for _, kw in ipairs(_safe_tbl(keywords.pills)) do | for _, kw in ipairs(_safe_tbl(keywords.pills)) do | ||
local wt = | local wt = _render_def_or_text(frame, kw) | ||
if wt then | if wt then | ||
pills:tag("span"):addClass("sv-pill"):wikitext(wt) | pills:tag("span"):addClass("sv-pill"):wikitext(wt) | ||
| Line 621: | Line 723: | ||
return root | return root | ||
end | |||
-- Schema 2 adapter: convert to schema-1-like structure for rendering. | |||
local function _normalize_schema2(payload) | |||
local out = {} | |||
out.schema = 2 | |||
out.identity = _safe_tbl(payload.identity) | |||
-- skill_meta -> meta_row (4) | |||
out.meta_row = {} | |||
local sm = _safe_tbl(payload.skill_meta) | |||
for i = 1, 4 do | |||
local cell = _safe_tbl(sm[i]) | |||
local icon_page = _safe_str(cell.icon, "") | |||
local label_wt = _safe_str(cell.label_wt, _safe_str(cell.label, "")) | |||
out.meta_row[i] = { | |||
icon = { page = icon_page, alt = "" }, | |||
label_lines = label_wt ~= "" and { label_wt } or {}, | |||
} | |||
end | |||
-- requirements/users: map -> groups[] | |||
local function map_to_groups(map_tbl, preferred_order) | |||
local m = _safe_tbl(map_tbl) | |||
local used = {} | |||
local groups = {} | |||
-- preferred keys first | |||
for _, k in ipairs(preferred_order or {}) do | |||
if m[k] ~= nil then | |||
used[k] = true | |||
table.insert(groups, { title = k, items = m[k] }) | |||
end | |||
end | |||
-- remaining keys sorted | |||
local keys = {} | |||
for k, _ in pairs(m) do | |||
if not used[k] then table.insert(keys, tostring(k)) end | |||
end | |||
table.sort(keys) | |||
for _, k in ipairs(keys) do | |||
table.insert(groups, { title = k, items = m[k] }) | |||
end | |||
-- convert items to {label_lines=[...]} | |||
local out_groups = {} | |||
for _, g in ipairs(groups) do | |||
local title = _safe_str(g.title, "") | |||
local items = _safe_tbl(g.items) | |||
local out_items = {} | |||
for _, it in ipairs(items) do | |||
if type(it) == "string" then | |||
table.insert(out_items, { label_lines = { it } }) | |||
elseif type(it) == "table" then | |||
local lines = {} | |||
for _, ln in ipairs(it) do table.insert(lines, tostring(ln)) end | |||
table.insert(out_items, { label_lines = lines }) | |||
end | |||
end | |||
table.insert(out_groups, { title = title, items = out_items }) | |||
end | |||
return { groups = out_groups } | |||
end | |||
out.requirements = map_to_groups(payload.requirements, { "Skills", "Weapon Types", "Stances" }) | |||
out.users = map_to_groups(payload.users, { "Classes", "Summons" }) | |||
-- level: base/default/overcap.max -> base_max/max for renderer | |||
do | |||
local lv = _safe_tbl(payload.level) | |||
local base = _to_int(lv.base, 1) | |||
local natural = _to_int(lv.default, base) | |||
local overcap = _safe_tbl(lv.overcap) | |||
local max = _to_int(overcap.max, natural) | |||
out.level = { | |||
base_max = natural, | |||
max = max, | |||
} | |||
end | |||
-- skill_scaling -> scaling_top + core_stats | |||
do | |||
local sc = _safe_tbl(payload.skill_scaling) | |||
-- scaling_top.damage | |||
out.scaling_top = { | |||
damage = _safe_tbl(_safe_tbl(sc.damage).value), | |||
scaling_lines = {}, | |||
} | |||
-- stat_scaling -> a single friendly line for now | |||
local ss = _safe_tbl(sc.stat_scaling) | |||
if #ss > 0 then | |||
local parts = {} | |||
for _, it in ipairs(ss) do | |||
it = _safe_tbl(it) | |||
local stat = _safe_str(it.stat_wt, "") | |||
local val = _safe_str(it.value, "") | |||
if stat ~= "" and val ~= "" then | |||
-- Keep as text; later we can render stat_wt as defs/icons. | |||
table.insert(parts, stat .. ": " .. val) | |||
end | |||
end | |||
if #parts > 0 then | |||
table.insert(out.scaling_top.scaling_lines, { text = table.concat(parts, " • ") }) | |||
end | |||
end | |||
-- core_stats from named fields | |||
local function core_cell(key, unit, vobj) | |||
return { key = key, unit = unit, value = vobj } | |||
end | |||
out.core_stats = { | |||
core_cell("Cost", _safe_str(_safe_tbl(sc.cost).unit, ""), _safe_tbl(_safe_tbl(sc.cost).value)), | |||
core_cell("Cast Time", _safe_str(_safe_tbl(sc.cast_time).unit, ""), _safe_tbl(_safe_tbl(sc.cast_time).value)), | |||
core_cell("Cooldown", _safe_str(_safe_tbl(sc.cooldown).unit, ""), _safe_tbl(_safe_tbl(sc.cooldown).value)), | |||
core_cell("Range", _safe_str(_safe_tbl(sc.range).unit, ""), _safe_tbl(_safe_tbl(sc.range).value)), | |||
core_cell("Area", _safe_str(_safe_tbl(sc.area).unit, ""), _safe_tbl(_safe_tbl(sc.area).value)), | |||
core_cell("Duration", _safe_str(_safe_tbl(sc.duration).unit, ""), _safe_tbl(_safe_tbl(sc.duration).value)), | |||
} | |||
end | |||
-- tabs: mostly compatible; add an adapter for events card subtype | |||
out.tabs = _safe_tbl(payload.tabs) | |||
-- events cards: if schema2 uses stats.cause, map to card.type for renderer | |||
do | |||
local t = _safe_tbl(out.tabs) | |||
local ev = _safe_tbl(t.events) | |||
local cards = _safe_tbl(ev.cards) | |||
for _, c in ipairs(cards) do | |||
c = _safe_tbl(c) | |||
if c.type == nil then | |||
local stats = _safe_tbl(c.stats) | |||
if stats.cause ~= nil then | |||
c.type = stats.cause | |||
end | |||
end | |||
end | |||
end | |||
return out | |||
end | end | ||
| Line 632: | Line 885: | ||
if not payload then | if not payload then | ||
return _err(err, debug and preview or nil) | return _err(err, debug and preview or nil) | ||
end | |||
-- Schema 2 normalization (schema 1 passes through unchanged) | |||
if payload.schema == 2 then | |||
payload = _normalize_schema2(payload) | |||
end | end | ||
| Line 659: | Line 917: | ||
box.bottom:addClass("sv-skill-bottom") | box.bottom:addClass("sv-skill-bottom") | ||
box.top:node(_build_identity(root_id, identity, notes_wt)) | box.top:node(_build_identity(frame, root_id, identity, notes_wt)) | ||
box.top:node(_build_meta_row(payload.meta_row)) | box.top:node(_build_meta_row(frame, payload.meta_row)) | ||
local reqrow = _build_req_users(root_id, payload.requirements, payload.users) | local reqrow = _build_req_users(frame, root_id, payload.requirements, payload.users) | ||
if reqrow then box.top:node(reqrow) end | if reqrow then box.top:node(reqrow) end | ||