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 1: Line 1:
-- Module:GameInfo/Skills
-- Module:GameInfo/Skills
-- Phase 4.1 — Native format rendering (format 1 + format 2, no adapter glue)
-- Phase 4.1 — Final native-first rendering (schema 1 + schema 2)
--
--
-- DEFINITIONS POLICY (strict):
-- Notes:
-- - Only accept Definition tokens as JSON strings using \u007C, e.g. "Damage\u007CPiercing".
-- - JSON Definition tokens must be plain strings that decode to Domain|Key.
-- - After jsonDecode, this becomes "Damage|Piercing" and is expanded via {{def|Damage|Piercing}}.
-- - Interactivity is provided by sitewide popup systems in Common.js.
-- - No support for:
-- - Schema 2 is the primary layout target; schema 1 remains supported.
--    * <nowiki>...</nowiki> wrapped |data=
--    * double-escaped pipes (\\u007C)
--    * template-shaped strings inside JSON (e.g. "{{def|Damage|Piercing}}")
--
-- Icons:
-- - Meta row has its own icon slot, so meta labels always use noicon=1.
-- - Keyword pills also use noicon=1 (no extra icons in pills).
-- - Native-format scaling stat pills render visible short labels/icons,
--  and use a full-pill invisible Definitions hitbox only when active.
--
-- Popups:
-- - Source nodes (.sv-tip-pop / .sv-disclose-pop) are hidden content only.
-- - All interaction is handled by Universal Popups (Common.js). No legacy popup system.
--
-- Requirements / Users (dynamic headers):
-- - Native format accepts requirements/users as maps (title -> items[]).
-- - Titles are treated as dynamic display text (no hardcoded header logic).
-- - Optional __order = ["Title A","Title B", ...] may be provided to force section order.
--  If omitted, ordering is deterministic: (preferred_order first, then remaining keys sorted).
--
-- SITEWIDE CSS ALIGNMENT:
-- - Shared UI ownership lives in Common.css + Citizen.css.
-- - This module keeps semantic GameInfo/Skills hooks, while also opting into
--  shared sitewide classes where the role already matches:
--    * sv-tip / sv-tip-btn
--    * sv-disclose / sv-disclose-btn
--    * sv-tabs / sv-tab / sv-tabpanel
--    * sv-level-* shared slider anatomy
--    * sv-card / sv-tile / sv-pill / sv-hover-lift where appropriate
-- - The CSS pass can now simplify Module:GameInfo/styles.css by leaning on
--  these shared classes instead of duplicating sitewide surface styling.


local p = {}
local p = {}
Line 86: Line 55:


local _CORE_ORDER = {
local _CORE_ORDER = {
{ field = "cost",      label = "Cost"      },
{ field = "cost",      label = "Cost",     def_tok = "Cast|Cost" },
{ field = "cast_time", label = "Cast Time" },
{ field = "cast_time", label = "Cast Time", def_tok = "Cast|Cast Time" },
{ field = "cooldown",  label = "Cooldown"  },
{ field = "cooldown",  label = "Cooldown", def_tok = "Cast|Cooldown" },
{ field = "range",    label = "Range"    },
{ field = "range",    label = "Range",     def_tok = "Cast|Range" },
{ field = "area",      label = "Area"      },
{ field = "area",      label = "Area",     def_tok = "Cast|Area" },
{ field = "duration",  label = "Duration"  },
{ field = "duration",  label = "Duration", def_tok = "Cast|Duration" },
}
}


Line 101: Line 70:
end
end


local function _safe_tbl(t) return type(t) == "table" and t or {} end
local function _safe_tbl(t)
return type(t) == "table" and t or {}
end


local function _safe_str(s, fallback)
local function _safe_str(s, fallback)
Line 244: Line 215:
end
end


local function _render_def_token(frame, s, noicon, extra_args)
local function _split_def_token(s)
s = _trim(s)
s = _trim(s)
if s == "" then return nil end
if s == "" then return nil, nil end


local bar = s:find("|", 1, true)
local bar = s:find("|", 1, true)
if not bar then
if not bar then return nil, nil end
return mw.text.nowiki(s)
end


local domain = _trim(s:sub(1, bar - 1))
local domain = _trim(s:sub(1, bar - 1))
local key   = _trim(s:sub(bar + 1))
local key = _trim(s:sub(bar + 1))
if domain == "" or key == "" then
if domain == "" or key == "" then return nil, nil end
 
return domain, key
end
 
local function _render_def_token(frame, s, noicon, extra_args)
s = _trim(s)
if s == "" then return nil end
 
local domain, key = _split_def_token(s)
if not domain or not key then
return mw.text.nowiki(s)
return mw.text.nowiki(s)
end
end
Line 274: Line 253:
end
end


local function _meta_lines_def(frame, lines)
local function _display_text_from_token(s)
lines = _safe_tbl(lines)
s = _trim(s)
if s == "" then return "" end
 
local _, key = _split_def_token(s)
if key then return key end
return s
end
 
local function _meta_lines_def(frame, 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
Line 292: Line 280:
end
end


local function _split_def_token(s)
local function _build_hitbox(parent, class_name, frame, token)
s = _trim(s)
token = _safe_str(token, "")
if s == "" then return nil, nil end
if not _has_pipe(token) then return false end


local bar = s:find("|", 1, true)
local wt = _render_def_token(frame, token, true, { pill = "1", fill = "1" })
if not bar then return nil, nil end
if not wt then return false end


local domain = _trim(s:sub(1, bar - 1))
parent:tag("span")
local key = _trim(s:sub(bar + 1))
:addClass(class_name)
if domain == "" or key == "" then return nil, nil end
:attr("aria-hidden", "true")
:wikitext(wt)


return domain, key
return true
end
end


local function _keyword_display_text(pill_text)
local function _is_blankish(v)
pill_text = _trim(pill_text)
if v == nil then return true end
if pill_text == "" then return "" end
local s = _trim(v)
 
return s == "" or s == "—" or s == "-" or s == "--"
local domain, key = _split_def_token(pill_text)
if not domain or not key then
return pill_text
end
 
return key
end
end


local function _build_keyword_pill(frame, parent, pill_text)
local function _value_has_content(val)
pill_text = _trim(pill_text)
if val == nil then return false end
if pill_text == "" then return end
 
local visible_text = _keyword_display_text(pill_text)
local has_def = _has_pipe(pill_text)
 
local pill = parent:tag("span")
:addClass("sv-pill")
:addClass("sv-pill--value")
:addClass("sv-mech-keyword-pill")
 
pill:tag("span")
:addClass("sv-mech-keyword-pill__label")
:wikitext(mw.text.nowiki(visible_text ~= "" and visible_text or pill_text))
 
if has_def then
local wt = _render_def_token(frame, pill_text, true, { pill = "1", fill = "1" })
if wt then
pill:tag("span")
:addClass("sv-mech-keyword-pill__hit")
:attr("aria-hidden", "true")
:wikitext(wt)
end
end
end
 
local function _is_blankish(v)
if v == nil then return true end
local s = _trim(v)
return s == "" or s == "—" or s == "-" or s == "--"
end
 
local function _value_has_content(val)
if val == nil then return false end


if type(val) ~= "table" then
if type(val) ~= "table" then
Line 857: Line 807:
end
end


local hit_tok = _safe_str(spec.hit_tok, "")
_build_hitbox(card, "sv-meta-hit", frame, spec.hit_tok)
if _has_pipe(hit_tok) then
local wt = _render_def_token(frame, hit_tok, true, { pill = "1", fill = "1" })
if wt then
card:tag("span")
:addClass("sv-meta-hit")
:attr("aria-hidden", "true")
:wikitext(wt)
end
end
end
end


Line 1,114: Line 1,055:


pill:addClass(active and "is-active" or "is-inactive")
pill:addClass(active and "is-active" or "is-inactive")
if active then pill:addClass("sv-hover-lift") end


local main = pill:tag("div"):addClass("sv_skill_scaling__stat-main")
local main = pill:tag("div"):addClass("sv_skill_scaling__stat-main")
Line 1,131: Line 1,073:


if active then
if active then
local wt = _render_def_token(frame, def_tok, false, { pill = "1", fill = "1" })
_build_hitbox(pill, "sv_skill_scaling__stat-hit", frame, def_tok)
if wt then
pill:tag("span")
:addClass("sv_skill_scaling__stat-hit")
:attr("aria-hidden", "true")
:wikitext(wt)
end
end
end
end
end


local function _build_skill_scaling_core_pill(parent, spec, fld, level)
local function _build_skill_scaling_core_pill(frame, parent, spec, fld, level)
fld = _safe_tbl(fld)
fld = _safe_tbl(fld)
local active = _value_has_content(fld.value)
local active = _value_has_content(fld.value)
Line 1,151: Line 1,087:
:addClass("sv_skill_scaling__core-pill")
:addClass("sv_skill_scaling__core-pill")
:addClass("sv_skill_scaling__core-pill--" .. spec.field)
:addClass("sv_skill_scaling__core-pill--" .. spec.field)
:addClass("sv-hover-lift")
:attr("data-core-key", spec.field)
:attr("data-core-key", spec.field)
:attr("aria-label", spec.label)


pill:addClass(active and "is-active" or "is-inactive")
pill:addClass(active and "is-active" or "is-inactive")


local top = pill:tag("div"):addClass("sv_skill_scaling__core-main")
local top = pill:tag("div"):addClass("sv_skill_scaling__core-main")
local value = top:tag("span"):addClass("sv_skill_scaling__core-value")
local value = top:tag("span"):addClass("sv_skill_scaling__core-value")
_apply_value(value, fld.value, level)
_apply_value(value, fld.value, level)
Line 1,170: Line 1,107:
:addClass("sv_skill_scaling__core-label")
:addClass("sv_skill_scaling__core-label")
:wikitext(mw.text.nowiki(spec.label))
:wikitext(mw.text.nowiki(spec.label))
_build_hitbox(pill, "sv_skill_scaling__core-hit", frame, spec.def_tok)
end
end


Line 1,230: Line 1,169:
end
end
end
end
 
 
do
do
local core_group = body:tag("div")
local core_group = body:tag("div")
:addClass("sv_skill_scaling__group")
:addClass("sv_skill_scaling__group")
:addClass("sv_skill_scaling__group--core")
:addClass("sv_skill_scaling__group--core")
 
 
local core_col = core_group:tag("div")
local core_col = core_group:tag("div")
:addClass("sv_skill_scaling__column")
:addClass("sv_skill_scaling__column")
:addClass("sv_skill_scaling__column--core")
:addClass("sv_skill_scaling__column--core")
:addClass("sv_skill_scaling__core")
:addClass("sv_skill_scaling__core")
 
 
local core_grid = core_col:tag("div"):addClass("sv_skill_scaling__core-grid")
local core_grid = core_col:tag("div"):addClass("sv_skill_scaling__core-grid")
 
 
for _, spec in ipairs(_CORE_ORDER) do
for _, spec in ipairs(_CORE_ORDER) do
_build_skill_scaling_core_pill(core_grid, spec, skill_scaling[spec.field], actual_default)
_build_skill_scaling_core_pill(frame, core_grid, spec, skill_scaling[spec.field], actual_default)
end
end
end
 
return root, actual_default
end
 
local function _normalize_modifier_spec(item)
item = _safe_tbl(item)
local label_tok = _safe_str(item.label_wt or item.label or item.key or "", "")
return {
label_tok = label_tok,
label_text = _display_text_from_token(label_tok),
value = item.value,
}
end
 
local function _build_modifier_pill(frame, parent, item, level)
local spec = _normalize_modifier_spec(item)
if spec.label_text == "" then return end
 
local has_def = _has_pipe(spec.label_tok)
local pill = parent:tag("div")
:addClass("sv-tile")
:addClass("sv-mech-mod-pill")
 
if has_def then pill:addClass("sv-hover-lift") end
 
pill:tag("div")
:addClass("sv-mech-mod-pill__label")
:wikitext(mw.text.nowiki(spec.label_text))
 
local value = pill:tag("div"):addClass("sv-mech-mod-pill__value")
_apply_value(value, spec.value, level)
 
if has_def then
_build_hitbox(pill, "sv-mech-mod-pill__hit", frame, spec.label_tok)
end
end
 
local function _build_keyword_pill(frame, parent, pill_text)
pill_text = _trim(pill_text)
if pill_text == "" then return end
 
local has_def = _has_pipe(pill_text)
local pill = parent:tag("span")
:addClass("sv-pill")
:addClass("sv-pill--value")
:addClass("sv-mech-keyword-pill")
 
if has_def then pill:addClass("sv-hover-lift") end
 
pill:tag("span")
:addClass("sv-mech-keyword-pill__label")
:wikitext(mw.text.nowiki(_display_text_from_token(pill_text)))
 
if has_def then
_build_hitbox(pill, "sv-mech-keyword-pill__hit", frame, pill_text)
end
end
return root, actual_default
end
end


Line 1,336: Line 1,329:
if kind == "event" then
if kind == "event" then
local sub = parent:tag("div"):addClass("sv-ref-sub")
local sub = parent:tag("div"):addClass("sv-ref-sub")
if stats.text ~= nil then
if stats.text ~= nil then
_apply_value(sub, stats, level)
_apply_value(sub, stats, level)
Line 1,390: Line 1,382:


local text = container:tag("div"):addClass("sv-ref-text")
local text = container:tag("div"):addClass("sv-ref-text")
local title_div = text:tag("div"):addClass("sv-ref-title")
local title_div = text:tag("div"):addClass("sv-ref-title")
if page ~= "" then
if page ~= "" then
Line 1,423: Line 1,414:
mods_wrap:tag("div"):addClass("sv-tab-section-title"):wikitext("Modifiers")
mods_wrap:tag("div"):addClass("sv-tab-section-title"):wikitext("Modifiers")


local gridwrap = mods_wrap:tag("div"):addClass("sv-kw-grid")
local gridwrap = mods_wrap:tag("div"):addClass("sv-mech-mod-grid")
for _, item in ipairs(mechanics_mods) do
for _, item in ipairs(mechanics_mods) do
item = _safe_tbl(item)
_build_modifier_pill(frame, gridwrap, item, level)
 
local cell = gridwrap:tag("div")
:addClass("sv-tile")
:addClass("sv-kw-cell")
cell:tag("div")
:addClass("sv-kw-label")
:wikitext(mw.text.nowiki(_safe_str(item.label, "")))
 
local v = cell:tag("div"):addClass("sv-kw-value")
_apply_value(v, item.value, level)
end
end
end
end
Line 1,635: Line 1,616:
local reqrow = _build_req_users(root_id, payload.requirements, payload.users)
local reqrow = _build_req_users(root_id, payload.requirements, payload.users)
local meta_req_block = _build_meta_req_block(meta_block, reqrow)
local meta_req_block = _build_meta_req_block(meta_block, reqrow)
 
if meta_req_block then box.top:node(meta_req_block) end
if meta_req_block then
box.top:node(meta_req_block)
end


local level_panel, actual_default = _build_level(root_id, default_level, max_level, 1, nil)
local level_panel, actual_default = _build_level(root_id, default_level, max_level, 1, nil)
box.bottom:node(level_panel)
box.bottom:node(level_panel)
box.bottom:node(_build_scaling_top(frame, payload.scaling_top, actual_default))
box.bottom:node(_build_scaling_top(frame, payload.scaling_top, actual_default))
Line 1,689: Line 1,666:
local reqrow = _build_req_users_native(root_id, payload.requirements, payload.users)
local reqrow = _build_req_users_native(root_id, payload.requirements, payload.users)
local meta_req_block = _build_meta_req_block(meta_block, reqrow)
local meta_req_block = _build_meta_req_block(meta_block, reqrow)
 
if meta_req_block then box.top:node(meta_req_block) end
if meta_req_block then
box.top:node(meta_req_block)
end


local scaling_block, actual_default = _build_skill_scaling_native(
local scaling_block, actual_default = _build_skill_scaling_native(