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 1: | Line 1: | ||
-- Module:GameInfo/Skills | -- Module:GameInfo/Skills | ||
-- Phase 4.1 — Schema 1 | -- Phase 4.1 — Dual schema renderer (Schema 1 + Schema 2) | ||
-- | -- | ||
-- | -- DEFINITIONS POLICY (strict): | ||
-- | -- - Only accept Definition tokens as JSON strings using \u007C, e.g. "Damage\u007CPiercing". | ||
-- | -- - After jsonDecode, this becomes "Damage|Piercing" and is expanded via {{def|Damage|Piercing}}. | ||
-- | -- - No support for: | ||
-- | -- * <nowiki>...</nowiki> wrapped |data= | ||
-- - | -- * double-escaped pipes (\\u007C) | ||
-- - "{{def| | -- * template-shaped strings inside JSON (e.g. "{{def|Damage|Piercing}}") | ||
-- | -- | ||
-- | -- Schema 2 is adapted into the internal Schema-1-ish render shape to preserve DOM/CSS/JS contracts. | ||
local p = {} | local p = {} | ||
| Line 68: | Line 68: | ||
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 | ||
| Line 153: | Line 156: | ||
end | end | ||
local function | -- Strict def token renderer (Domain|Key only). | ||
local function _render_def_token(frame, s, noicon) | |||
s = _trim(s) | |||
if s == "" then return nil end | |||
local bar = s:find("|", 1, true) | |||
if not bar then | |||
return mw.text.nowiki(s) | |||
end | |||
local domain = _trim(s:sub(1, bar - 1)) | |||
local key = _trim(s:sub(bar + 1)) | |||
if domain == "" or key == "" then | |||
return mw.text.nowiki(s) | |||
end | |||
local args = { domain, key } | |||
if noicon then | |||
args.noicon = "1" | |||
end | |||
return frame:expandTemplate{ title = "def", args = args } | |||
end | |||
local function _meta_lines_plain(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") | ||
| Line 170: | Line 197: | ||
end | end | ||
-- Keywords pill policy: ONLY Domain|Key ( | local function _meta_lines_def(frame, lines) | ||
lines = _safe_tbl(lines) | |||
local wrap = mw.html.create("span"):addClass("sv-meta-lines") | |||
local any = false | |||
for _, ln in ipairs(lines) do | |||
local s = _trim(ln) | |||
if s ~= "" then | |||
-- Meta row has its own icon, so def should be noicon=1 when token is used. | |||
local wt = _render_def_token(frame, s, true) or mw.text.nowiki(s) | |||
wrap:tag("span"):wikitext(wt) | |||
any = true | |||
end | |||
end | |||
if not any then | |||
wrap:tag("span"):wikitext("—") | |||
end | |||
return wrap | |||
end | |||
-- Keywords pill policy: ONLY Domain|Key tokens (from Domain\u007CKey in JSON). | |||
local function _render_keyword_pill(frame, pill_text) | local function _render_keyword_pill(frame, pill_text) | ||
pill_text = _trim(pill_text) | pill_text = _trim(pill_text) | ||
| Line 177: | Line 223: | ||
end | end | ||
local | local wt = _render_def_token(frame, pill_text, false) | ||
if | if wt then return wt end | ||
return mw.text.nowiki(pill_text) | return mw.text.nowiki(pill_text) | ||
| Line 281: | Line 321: | ||
local li = ul:tag("li") | local li = ul:tag("li") | ||
local label_node = | local label_node = _meta_lines_plain(it.label_lines) | ||
local link = _safe_tbl(it.link) | local link = _safe_tbl(it.link) | ||
| Line 343: | Line 383: | ||
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 363: | Line 403: | ||
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( | wrap:tag("div"):addClass("sv-meta-text"):node(_meta_lines_def(frame, cell.label_lines)) | ||
end | end | ||
| Line 369: | Line 409: | ||
end | end | ||
-- | -- Schema 1 level normalization: | ||
-- - max = absolute cap (incl. gear) if provided, else base_max | -- - max = absolute cap (incl. gear) if provided, else base_max | ||
-- - base_max = natural cap (no gear) if provided, else max | -- - base_max = natural cap (no gear) if provided, else max | ||
| Line 633: | Line 673: | ||
return root | return root | ||
end | |||
-- Schema 2 -> internal shape adapter (still strict \u007C token policy) | |||
local function _adapt_schema2(p2) | |||
local out = {} | |||
out.schema = 1 | |||
out.identity = _safe_tbl(p2.identity) | |||
out.tabs = _safe_tbl(p2.tabs) | |||
-- skill_meta -> meta_row | |||
out.meta_row = {} | |||
local sm = _safe_tbl(p2.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, "") | |||
out.meta_row[i] = { | |||
icon = { page = icon_page, alt = "" }, | |||
-- label_wt should be a strict token: Domain|Key (from Domain\u007CKey) | |||
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 = {} | |||
for _, k in ipairs(preferred_order or {}) do | |||
if m[k] ~= nil then | |||
used[k] = true | |||
table.insert(groups, tostring(k)) | |||
end | |||
end | |||
local rest = {} | |||
for k, _ in pairs(m) do | |||
k = tostring(k) | |||
if not used[k] then table.insert(rest, k) end | |||
end | |||
table.sort(rest) | |||
for _, k in ipairs(rest) do table.insert(groups, k) end | |||
local out_groups = {} | |||
for _, title in ipairs(groups) do | |||
local items = _safe_tbl(m[title]) | |||
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 | |||
-- array form: ["Sacrifice","(Lv 1)"] | |||
if #it > 0 then | |||
local lines = {} | |||
for _, ln in ipairs(it) do table.insert(lines, tostring(ln)) end | |||
table.insert(out_items, { label_lines = lines }) | |||
else | |||
-- object form (optional future): { page="X", label_lines=[...] } | |||
local lines = _safe_tbl(it.label_lines) | |||
local page = _safe_str(it.page, "") | |||
local out_it = { label_lines = lines } | |||
if page ~= "" then out_it.link = { page = page } end | |||
table.insert(out_items, out_it) | |||
end | |||
end | |||
end | |||
table.insert(out_groups, { title = title, items = out_items }) | |||
end | |||
return { groups = out_groups } | |||
end | |||
out.requirements = map_to_groups(p2.requirements, { "Skills", "Weapon Types", "Stances" }) | |||
out.users = map_to_groups(p2.users, { "Classes", "Summons" }) | |||
-- level: base/default/overcap.max -> base_max/max | |||
do | |||
local lv = _safe_tbl(p2.level) | |||
local base = _to_int(lv.base, 1) | |||
local natural = _to_int(lv.default, base) | |||
local oc = _safe_tbl(lv.overcap) | |||
local max = _to_int(oc.max, natural) | |||
out.level = { base_max = natural, max = max } | |||
end | |||
-- skill_scaling -> scaling_top + core_stats | |||
do | |||
local sc = _safe_tbl(p2.skill_scaling) | |||
out.scaling_top = { | |||
damage = _safe_tbl(_safe_tbl(sc.damage).value), | |||
scaling_lines = {}, | |||
} | |||
-- stat_scaling -> "INT: 2% • STR: 2%" (token-safe) | |||
local parts = {} | |||
for _, it in ipairs(_safe_tbl(sc.stat_scaling)) do | |||
it = _safe_tbl(it) | |||
local tok = _safe_str(it.stat_wt, "") | |||
local val = _safe_str(it.value, "") | |||
if tok ~= "" and val ~= "" then | |||
local bar = tok:find("|", 1, true) | |||
local key = bar and _trim(tok:sub(bar + 1)) or tok | |||
if key ~= "" then | |||
table.insert(parts, string.upper(key) .. ": " .. val) | |||
end | |||
end | |||
end | |||
if #parts > 0 then | |||
table.insert(out.scaling_top.scaling_lines, { text = table.concat(parts, " • ") }) | |||
end | |||
local function add_core(label, field_key) | |||
local fld = _safe_tbl(sc[field_key]) | |||
local v = fld.value | |||
if type(v) ~= "table" then return end | |||
table.insert(out.core_stats, { | |||
key = label, | |||
unit = _safe_str(fld.unit, ""), | |||
value = _safe_tbl(v), | |||
}) | |||
end | |||
out.core_stats = {} | |||
add_core("Cost", "cost") | |||
add_core("Cast Time", "cast_time") | |||
add_core("Cooldown", "cooldown") | |||
add_core("Range", "range") | |||
add_core("Area", "area") | |||
add_core("Duration", "duration") | |||
end | |||
-- events cards: if schema2 uses stats.cause, map to card.type for existing 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 | |||
elseif stats.type ~= nil then | |||
c.type = stats.type | |||
end | |||
end | |||
end | |||
end | |||
return out | |||
end | end | ||
| Line 644: | Line 841: | ||
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 | |||
if payload.schema == 2 then | |||
payload = _adapt_schema2(payload) | |||
end | end | ||
| Line 671: | Line 872: | ||
box.top:node(_build_identity(root_id, identity, notes_wt)) | box.top:node(_build_identity(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(root_id, payload.requirements, payload.users) | ||