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:GameInfo/Skills: Difference between revisions

From SpiritVale Wiki
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("&#124;", "|"):gsub("&vert;", "|")
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
if decoded.schema ~= 1 then
 
return nil, "unsupported schema=" .. tostring(decoded.schema), raw
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(mw.text.nowiki(s))
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
local function _render_keyword_pill(frame, pill_text)
pill_text = _trim(pill_text)
if pill_text == "" then
return nil
end
local bar = pill_text:find("|", 1, true)
if bar then
local domain = _trim(pill_text:sub(1, bar - 1))
local key    = _trim(pill_text:sub(bar + 1))
if domain ~= "" and key ~= "" then
return frame:expandTemplate{ title = "def", args = { domain, key } }
end
end
return mw.text.nowiki(pill_text)
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
-- - max      = absolute cap (incl. gear) if provided, else base_max
-- - base_max = natural cap (no gear) if provided, else max
-- - default  = base_max (slider starts at natural cap)
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
-- NOTE: scaling_top.modifier intentionally not rendered (handled elsewhere).


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 = _render_keyword_pill(frame, kw)
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