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
 
(16 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:GameInfo/Skills
-- Module:GameInfo/Skills
-- Phase 4.1 Native Schema 2 rendering (Schema 1 + Schema 2, NO adapter glue)
-- Phase 4.2 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=
-- - Mechanics / keywords are JSON-driven; the renderer does not infer future
--    * double-escaped pipes (\\u007C)
--  mechanic definition tokens from labels.
--    * 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).
-- - Stat scaling (skill_scaling.stat_scaling) renders WITH icons.
--
-- 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):
-- - Schema2 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).


local p = {}
local p = {}
Line 32: Line 16:


local _file_exists_cache = {}
local _file_exists_cache = {}
local _STAT_ORDER = { "str", "vit", "dex", "agi", "int", "luk" }
local _STAT_GRID_ORDER = { "str", "agi", "vit", "int", "dex", "luk" }
local _STAT_LABELS = {
str = "STR",
vit = "VIT",
dex = "DEX",
agi = "AGI",
int = "INT",
luk = "LUK",
}
local _STAT_DEF_TOKENS = {
str = "Stat|Str",
vit = "Stat|Vit",
dex = "Stat|Dex",
agi = "Stat|Agi",
int = "Stat|Int",
luk = "Stat|Luk",
}
local _STAT_TOKEN_TO_KEY = {
["stat|str"] = "str",
["stat|vit"] = "vit",
["stat|dex"] = "dex",
["stat|agi"] = "agi",
["stat|int"] = "int",
["stat|luk"] = "luk",
}
local _STAT_ICON_FILES = {
str = "File:Strength.png",
vit = "File:Vitality.png",
dex = "File:Dexterity.png",
agi = "File:Agility.png",
int = "File:Intelligence.png",
luk = "File:Luck.png",
}
local _CORE_ORDER = {
{ field = "cost",      label = "Cost",      def_tok = "Skill|Cost" },
{ field = "cast_time", label = "Cast Time", def_tok = "Skill|CastTime" },
{ field = "cooldown",  label = "Cooldown",  def_tok = "Skill|Cooldown" },
{ field = "range",    label = "Range",    def_tok = "Skill|Range" },
{ field = "area",      label = "Area",      def_tok = "Skill|Area" },
{ field = "duration",  label = "Duration",  def_tok = "Skill|Duration" },
}


local function _trim(s)
local function _trim(s)
Line 40: Line 72:
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 58: Line 92:
s = s:gsub("[^%w]+", "-"):gsub("%-+", "-"):gsub("^%-", ""):gsub("%-$", "")
s = s:gsub("[^%w]+", "-"):gsub("%-+", "-"):gsub("^%-", ""):gsub("%-$", "")
if s == "" then s = "item" end
if s == "" then s = "item" end
return s
end
local function _humanize_key(s)
s = _trim(s)
if s == "" then return "" end
if s:find("%s") then return s end
s = s:gsub("([A-Z]+)([A-Z][a-z])", "%1 %2")
s = s:gsub("([a-z0-9])([A-Z])", "%1 %2")
return s
return s
end
end


local function _err(msg, preview)
local function _err(msg, preview)
local box = mw.html.create("div"):addClass("sv-gi-error")
local box = mw.html.create("div")
:addClass("sv-card")
:addClass("sv-gi-error")
 
box:wikitext("GameInfo/Skills error: " .. tostring(msg))
box:wikitext("GameInfo/Skills error: " .. tostring(msg))
if preview and preview ~= "" then
if preview and preview ~= "" then
Line 180: Line 226:
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 208: Line 262:


return frame:expandTemplate{ title = "def", args = args }
return frame:expandTemplate{ title = "def", args = args }
end
local function _display_text_from_token(s)
s = _trim(s)
if s == "" then return "" end
local _, key = _split_def_token(s)
if key then return _humanize_key(key) end
return s
end
end


Line 228: Line 291:
end
end


local function _render_keyword_pill(frame, pill_text)
local function _build_hitbox(parent, class_name, frame, token)
pill_text = _trim(pill_text)
token = _safe_str(token, "")
if pill_text == "" then
if not _has_pipe(token) then return false end
return nil
 
local wt = _render_def_token(frame, token, true, { pill = "1", fill = "1" })
if not wt then return false end
 
parent:tag("span")
:addClass(class_name)
:attr("aria-hidden", "true")
:wikitext(wt)
 
return true
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
return not _is_blankish(val)
end
 
local series = val.series
if type(series) == "table" then
for _, item in ipairs(series) do
if not _is_blankish(item) then
return true
end
end
return false
end
end


local wt = _render_def_token(frame, pill_text, true)
if val.text ~= nil then
if wt then return wt end
return not _is_blankish(val.text)
end


return mw.text.nowiki(pill_text)
return false
end
 
local function _stat_key_from_token(token)
token = _trim(token):lower()
if token == "" then return nil end
return _STAT_TOKEN_TO_KEY[token]
end
 
local function _normalize_stat_scaling_slots(stat_scaling)
local slots = {}
for _, key in ipairs(_STAT_ORDER) do
slots[key] = nil
end
 
for _, it in ipairs(_safe_tbl(stat_scaling)) do
it = _safe_tbl(it)
 
local legacy_tok = _safe_str(it.stat_wt or it.stat or "", "")
if legacy_tok ~= "" then
local key = _stat_key_from_token(legacy_tok)
if key then
slots[key] = it.value
end
end
 
for k, v in pairs(it) do
local lk = _trim(k):lower()
if _STAT_LABELS[lk] ~= nil then
slots[lk] = v
end
end
end
 
return slots
end
end


Line 337: Line 467:
:addClass("sv-tip-btn")
:addClass("sv-tip-btn")
:addClass("sv-tip-btn--icon")
:addClass("sv-tip-btn--icon")
:addClass("sv-hover-lift")
:attr("role", "button")
:attr("role", "button")
:attr("tabindex", "0")
:attr("tabindex", "0")
Line 431: Line 562:
:addClass("sv-tip-btn--pill")
:addClass("sv-tip-btn--pill")
:addClass("sv-overcap-btn")
:addClass("sv-overcap-btn")
:addClass("sv-hover-lift")
:attr("role", "button")
:attr("role", "button")
:attr("tabindex", "0")
:attr("tabindex", "0")
Line 485: Line 617:
local s = _trim(it)
local s = _trim(it)
if s == "" then return nil end
if s == "" then return nil end
return { text = s }
return { text = s, page = s }
end
end


Line 494: Line 626:
if text == "" then return nil end
if text == "" then return nil end
local page = _safe_str(it.page or "", "")
local page = _safe_str(it.page or "", "")
if page == "" then page = text end
local suffix = _safe_str(it.suffix or "", "")
local suffix = _safe_str(it.suffix or "", "")
return { text = text, page = (page ~= "" and page or nil), suffix = (suffix ~= "" and suffix or nil) }
return { text = text, page = (page ~= "" and page or nil), suffix = (suffix ~= "" and suffix or nil) }
Line 545: Line 678:
:addClass("sv-disclose-btn")
:addClass("sv-disclose-btn")
:addClass("sv-disclose-btn--compact")
:addClass("sv-disclose-btn--compact")
:addClass("sv-hover-lift")
:attr("role", "button")
:attr("role", "button")
:attr("tabindex", "0")
:attr("tabindex", "0")
Line 624: Line 758:
local root = mw.html.create("div"):addClass("sv-skill-head")
local root = mw.html.create("div"):addClass("sv-skill-head")


local icon_div = root:tag("div"):addClass("sv-skill-icon")
local icon_div = root:tag("div")
:addClass("sv-skill-icon")
:addClass("sv-tile")
 
local sprite = _safe_tbl(identity.sprite)
local sprite = _safe_tbl(identity.sprite)
local sprite_page = _safe_str(sprite.page, "")
local sprite_page = _safe_str(sprite.page, "")
Line 650: Line 787:


return root
return root
end
local function _build_meta_card(frame, parent, spec)
spec = _safe_tbl(spec)
local card = parent:tag("div")
:addClass("sv-tile")
:addClass("sv-meta-card")
:addClass("sv-hover-lift")
local icon_div = card:tag("div")
:addClass("sv-meta-icon")
:addClass("sv-tile")
local icon_page = _safe_str(spec.icon_page, "")
local icon_alt = _safe_str(spec.icon_alt, "")
if icon_page ~= "" then
icon_div:node(_render_file_image(icon_page, icon_alt, 24))
else
icon_div:node(_question_badge())
end
local wrap = card:tag("div"):addClass("sv-meta-textwrap")
local text = wrap:tag("div"):addClass("sv-meta-text")
local lines = _safe_tbl(spec.lines)
if #lines > 0 then
text:node(_meta_lines_def(frame, lines))
else
text:wikitext("—")
end
_build_hitbox(card, "sv-meta-hit", frame, spec.hit_tok)
end
end


Line 658: Line 828:
for i = 1, 4 do
for i = 1, 4 do
local cell = _safe_tbl(meta_row[i])
local cell = _safe_tbl(meta_row[i])
local card = meta:tag("div"):addClass("sv-meta-card")
local icon = _safe_tbl(cell.icon)
local icon = _safe_tbl(cell.icon)
local icon_page = _safe_str(icon.page, "")
local icon_alt = _safe_str(icon.alt, "")
local icon_div = card:tag("div"):addClass("sv-meta-icon")
if icon_page ~= "" then
icon_div:node(_render_file_image(icon_page, icon_alt, 24))
else
icon_div:node(_question_badge())
end
local wrap = card:tag("div"):addClass("sv-meta-textwrap")
local text = wrap:tag("div"):addClass("sv-meta-text")
local lines = _safe_tbl(cell.label_lines)
local lines = _safe_tbl(cell.label_lines)
if #lines > 0 then
local hit_tok = ""
text:node(_meta_lines_def(frame, lines))
else
text:wikitext("")
end


local hit_tok = ""
for _, ln in ipairs(lines) do
for _, ln in ipairs(lines) do
local s = _safe_str(ln, "")
local s = _safe_str(ln, "")
Line 689: Line 839:
end
end
end
end
if hit_tok ~= "" then
 
local wt = _render_def_token(frame, hit_tok, true, { pill = "1", fill = "1" })
_build_meta_card(frame, meta, {
if wt then
icon_page = _safe_str(icon.page, ""),
card:tag("span")
icon_alt = _safe_str(icon.alt, ""),
:addClass("sv-meta-hit")
lines = lines,
:attr("aria-hidden", "true")
hit_tok = hit_tok,
:wikitext(wt)
})
end
end
end
end


Line 703: Line 851:
end
end


local function _build_meta_row_schema2(frame, skill_meta)
local function _build_meta_row_native(frame, skill_meta)
skill_meta = _safe_tbl(skill_meta)
skill_meta = _safe_tbl(skill_meta)


Line 709: Line 857:
for i = 1, 4 do
for i = 1, 4 do
local cell = _safe_tbl(skill_meta[i])
local cell = _safe_tbl(skill_meta[i])
local card = meta:tag("div"):addClass("sv-meta-card")
local lines = _safe_tbl(cell.label_lines)
local hit_tok = _safe_str(cell.label_wt, "")


local icon_div = card:tag("div"):addClass("sv-meta-icon")
if #lines == 0 and hit_tok ~= "" then
local icon_page = _safe_str(cell.icon, "")
lines = { hit_tok }
if icon_page ~= "" then
icon_div:node(_render_file_image(icon_page, "", 24))
else
icon_div:node(_question_badge())
end
 
local wrap = card:tag("div"):addClass("sv-meta-textwrap")
local text = wrap:tag("div"):addClass("sv-meta-text")
 
local display_lines = _safe_tbl(cell.label_lines)
if #display_lines == 0 then
local label_wt = _safe_str(cell.label_wt, "")
if label_wt ~= "" then display_lines = { label_wt } end
end
 
if #display_lines > 0 then
text:node(_meta_lines_def(frame, display_lines))
else
text:wikitext("—")
end
end


local hit_tok = _safe_str(cell.label_wt, "")
_build_meta_card(frame, meta, {
if _has_pipe(hit_tok) then
icon_page = _safe_str(cell.icon, ""),
local wt = _render_def_token(frame, hit_tok, true, { pill = "1", fill = "1" })
icon_alt = "",
if wt then
lines = lines,
card:tag("span")
hit_tok = hit_tok,
:addClass("sv-meta-hit")
})
:attr("aria-hidden", "true")
:wikitext(wt)
end
end
end
end


Line 766: Line 892:
end
end


local function _normalize_level_schema2(level_obj)
local function _normalize_level_native(level_obj)
level_obj = _safe_tbl(level_obj)
level_obj = _safe_tbl(level_obj)


Line 794: Line 920:


local root = mw.html.create("div")
local root = mw.html.create("div")
:addClass("sv-card")
:addClass("sv-skill-level")
:addClass("sv-skill-level")
:attr("data-sv-level-boundary", "1")
:attr("data-sv-level-boundary", "1")
Line 845: Line 972:
scaling = _safe_tbl(scaling)
scaling = _safe_tbl(scaling)


local root = mw.html.create("div"):addClass("sv-skill-scaling")
local root = mw.html.create("div")
:addClass("sv-card")
:addClass("sv-skill-scaling")
 
local row = root:tag("div"):addClass("sv-scaling-row")
local row = root:tag("div"):addClass("sv-scaling-row")
local grid = row:tag("div"):addClass("sv-scaling-grid")
local grid = row:tag("div"):addClass("sv-scaling-grid")
Line 865: Line 995:
local val = _safe_str(it.value or "", "")
local val = _safe_str(it.value or "", "")
if tok ~= "" and val ~= "" then
if tok ~= "" and val ~= "" then
local item = list:tag("div"):addClass("sv-scaling-item sv-scaling-item--stat")
local item = list:tag("div")
:addClass("sv-tile")
:addClass("sv-scaling-item")
:addClass("sv-scaling-item--stat")
local wt = _render_def_token(frame, tok, false) or mw.text.nowiki(tok)
local wt = _render_def_token(frame, tok, false) or mw.text.nowiki(tok)
item:tag("span"):addClass("sv-scale-stat"):wikitext(wt)
item:tag("span"):addClass("sv-scale-stat"):wikitext(wt)
Line 873: Line 1,006:


for _, ln in ipairs(_safe_tbl(scaling.scaling_lines)) do
for _, ln in ipairs(_safe_tbl(scaling.scaling_lines)) do
local item = list:tag("div"):addClass("sv-scaling-item")
local item = list:tag("div")
:addClass("sv-tile")
:addClass("sv-scaling-item")
_apply_value(item, ln, level)
_apply_value(item, ln, level)
end
end
return root
end
local function _build_scaling_top_schema2(frame, skill_scaling, level)
skill_scaling = _safe_tbl(skill_scaling)
local dmg = _safe_tbl(skill_scaling.damage)
local dmg_label = _safe_str(dmg.label, "Damage")
local dmg_val = _safe_tbl(dmg.value)
local root = mw.html.create("div"):addClass("sv-skill-scaling")
local row = root:tag("div"):addClass("sv-scaling-row")
local grid = row:tag("div"):addClass("sv-scaling-grid")
do
local col = grid:tag("div"):addClass("sv-scaling-col")
local v = col:tag("div"):addClass("sv-scaling-value")
_apply_value(v, dmg_val, level)
col:tag("div"):addClass("sv-scaling-label"):wikitext(mw.text.nowiki(dmg_label))
end
do
local col = grid:tag("div"):addClass("sv-scaling-col sv-scaling-col--scaling")
local list = col:tag("div"):addClass("sv-scaling-list")
for _, it in ipairs(_safe_tbl(skill_scaling.stat_scaling)) do
it = _safe_tbl(it)
local tok = _safe_str(it.stat_wt or it.stat or "", "")
local val = _safe_str(it.value or "", "")
if tok ~= "" and val ~= "" then
local item = list:tag("div"):addClass("sv-scaling-item sv-scaling-item--stat")
local wt = _render_def_token(frame, tok, false) or mw.text.nowiki(tok)
item:tag("span"):addClass("sv-scale-stat"):wikitext(wt)
item:tag("span"):addClass("sv-scale-val"):wikitext(mw.text.nowiki(val))
end
end
end
end
end
Line 922: Line 1,019:
core_stats = _safe_tbl(core_stats)
core_stats = _safe_tbl(core_stats)


local root = mw.html.create("div"):addClass("sv-skill-core")
local root = mw.html.create("div")
:addClass("sv-card")
:addClass("sv-skill-core")
 
local row = root:tag("div"):addClass("sv-core-row")
local row = root:tag("div"):addClass("sv-core-row")
local grid = row:tag("div"):addClass("sv-core-grid")
local grid = row:tag("div"):addClass("sv-core-grid")
Line 929: Line 1,029:
cell = _safe_tbl(cell)
cell = _safe_tbl(cell)


local c = grid:tag("div"):addClass("sv-core-cell")
local c = grid:tag("div")
:addClass("sv-core-cell")
:addClass("sv-tile")


local top = c:tag("div"):addClass("sv-core-top")
local top = c:tag("div"):addClass("sv-core-top")
Line 948: Line 1,050:
end
end


local function _build_core_stats_schema2(skill_scaling, level)
local function _build_skill_scaling_stat_pill(frame, parent, stat_key, raw_val, level)
local label = _STAT_LABELS[stat_key] or stat_key:upper()
local def_tok = _STAT_DEF_TOKENS[stat_key] or label
local icon_file = _STAT_ICON_FILES[stat_key]
local active = _value_has_content(raw_val)
 
local pill = parent:tag("div")
:addClass("sv-pill")
:addClass("sv-pill--compact")
:addClass("sv_skill_scaling__pill")
:addClass("sv_skill_scaling__pill--stat")
:addClass("sv_skill_scaling__stat-pill")
:addClass("sv_skill_scaling__stat-pill--" .. stat_key)
:attr("data-stat-key", stat_key)
:attr("aria-label", label)
 
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 visual = main:tag("span"):addClass("sv_skill_scaling__stat-visual")
 
if active then
local icon = visual:tag("span"):addClass("sv_skill_scaling__stat-icon")
if icon_file and icon_file ~= "" then
icon:node(_render_file_image(icon_file, label, 14))
else
icon:node(_question_badge())
end
end
 
local val = visual:tag("span"):addClass("sv_skill_scaling__stat-value")
_apply_value(val, raw_val, level)
 
if active then
_build_hitbox(pill, "sv_skill_scaling__stat-hit", frame, def_tok)
end
end
 
local function _build_skill_scaling_core_pill(frame, parent, spec, fld, level)
fld = _safe_tbl(fld)
local active = _value_has_content(fld.value)
local resolved_label = _safe_str(fld.label or fld.key, spec.label)
local resolved_tok = _safe_str(fld.label_wt or fld.def_wt, spec.def_tok)
 
local pill = parent:tag("div")
:addClass("sv-tile")
:addClass("sv_skill_scaling__pill")
:addClass("sv_skill_scaling__pill--core")
:addClass("sv_skill_scaling__core-pill")
:addClass("sv_skill_scaling__core-pill--" .. spec.field)
:addClass("sv-hover-lift")
:attr("data-core-key", spec.field)
:attr("aria-label", resolved_label)
 
pill:addClass(active and "is-active" or "is-inactive")
 
local top = pill:tag("div"):addClass("sv_skill_scaling__core-main")
local value = top:tag("span"):addClass("sv_skill_scaling__core-value")
_apply_value(value, fld.value, level)
 
local unit = _safe_str(fld.unit, "")
if unit ~= "" then
top:tag("span")
:addClass("sv_skill_scaling__core-unit")
:wikitext(mw.text.nowiki(unit))
end
 
pill:tag("div")
:addClass("sv_skill_scaling__core-label")
:wikitext(mw.text.nowiki(resolved_label))
 
_build_hitbox(pill, "sv_skill_scaling__core-hit", frame, resolved_tok)
end
 
local function _build_skill_scaling_native(frame, root_id, skill_scaling, default_level, max_level, min_level, overcap_obj)
skill_scaling = _safe_tbl(skill_scaling)
skill_scaling = _safe_tbl(skill_scaling)


local function has_value_field(fld)
local root = mw.html.create("div")
return type(fld) == "table" and type(fld.value) == "table"
:addClass("sv-card")
:addClass("sv_skill_scaling")
 
local level_panel, actual_default = _build_level(
root_id,
default_level,
max_level,
min_level,
overcap_obj
)
level_panel:addClass("sv_skill_scaling__level")
root:node(level_panel)
 
local body = root:tag("div"):addClass("sv_skill_scaling__body")
 
local damage = _safe_tbl(skill_scaling.damage)
local damage_label = _safe_str(damage.label, "Damage")
local damage_value = _safe_tbl(damage.value)
 
do
local primary_group = body:tag("div")
:addClass("sv_skill_scaling__group")
:addClass("sv_skill_scaling__group--primary-stats")
 
local cluster = primary_group:tag("div"):addClass("sv_skill_scaling__cluster")
 
do
local primary = cluster:tag("div")
:addClass("sv_skill_scaling__column")
:addClass("sv_skill_scaling__column--primary")
:addClass("sv_skill_scaling__primary")
 
local v = primary:tag("div"):addClass("sv_skill_scaling__primary-value")
_apply_value(v, damage_value, actual_default)
 
primary:tag("div")
:addClass("sv_skill_scaling__primary-label")
:wikitext(mw.text.nowiki(damage_label))
end
 
do
local stats_col = cluster:tag("div")
:addClass("sv_skill_scaling__column")
:addClass("sv_skill_scaling__column--stats")
:addClass("sv_skill_scaling__stats")
 
local stats_grid = stats_col:tag("div"):addClass("sv_skill_scaling__stats-grid")
local stat_slots = _normalize_stat_scaling_slots(skill_scaling.stat_scaling)
 
for _, stat_key in ipairs(_STAT_GRID_ORDER) do
_build_skill_scaling_stat_pill(frame, stats_grid, stat_key, stat_slots[stat_key], actual_default)
end
end
end
end


local order = {
do
{ key = "Cost",      field = "cost"     },
local core_group = body:tag("div")
{ key = "Cast Time", field = "cast_time" },
:addClass("sv_skill_scaling__group")
{ key = "Cooldown", field = "cooldown"  },
:addClass("sv_skill_scaling__group--core")
{ key = "Range",     field = "range"     },
 
{ key = "Area",      field = "area"     },
local core_col = core_group:tag("div")
{ key = "Duration", field = "duration"  },
:addClass("sv_skill_scaling__column")
:addClass("sv_skill_scaling__column--core")
:addClass("sv_skill_scaling__core")
 
local core_grid = core_col:tag("div"):addClass("sv_skill_scaling__core-grid")
 
for _, spec in ipairs(_CORE_ORDER) do
_build_skill_scaling_core_pill(frame, core_grid, spec, skill_scaling[spec.field], actual_default)
end
end
 
return root, actual_default
end
 
local function _normalize_modifier_spec(item)
item = _safe_tbl(item)
local explicit_tok = _safe_str(item.label_wt or item.def_wt, "")
local label_text = _safe_str(item.label or item.key or item.text, "")
if label_text == "" and explicit_tok ~= "" then
label_text = _display_text_from_token(explicit_tok)
end
return {
label_tok = explicit_tok,
label_text = label_text,
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 has_value = _value_has_content(spec.value)
local pill = parent:tag("div")
:addClass("sv-tile")
:addClass("sv-mech-mod-pill")
:attr("aria-label", spec.label_text)


local root = mw.html.create("div"):addClass("sv-skill-core")
if has_value then
local row = root:tag("div"):addClass("sv-core-row")
pill:addClass("is-active")
local grid = row:tag("div"):addClass("sv-core-grid")
else
pill:addClass("is-inactive")
end
 
if has_def or has_value then
pill:addClass("sv-hover-lift")
end


for _, spec in ipairs(order) do
pill:tag("div")
local fld = _safe_tbl(skill_scaling[spec.field])
:addClass("sv-mech-mod-pill__label")
if has_value_field(fld) then
:wikitext(mw.text.nowiki(spec.label_text))
local c = grid:tag("div"):addClass("sv-core-cell")


local top = c:tag("div"):addClass("sv-core-top")
local value = pill:tag("div"):addClass("sv-mech-mod-pill__value")
local num = top:tag("span"):addClass("sv-core-num")
_apply_value(value, spec.value, level)
_apply_value(num, fld.value, level)


local unit = _safe_str(fld.unit, "")
if has_def then
if unit ~= "" then
_build_hitbox(pill, "sv-mech-mod-pill__hit", frame, spec.label_tok)
top:tag("span"):addClass("sv-core-unit"):wikitext(mw.text.nowiki(unit))
end
end
end


local label = c:tag("div"):addClass("sv-core-label"):wikitext(mw.text.nowiki(spec.key))
local function _normalize_keyword_spec(item)
if spec.key:len() >= 9 then label:addClass("sv-core-label--tight") end
if type(item) == "string" then
local tok = _safe_str(item, "")
local label = tok
if _has_pipe(tok) then
label = _display_text_from_token(tok)
end
end
return {
label_tok = (_has_pipe(tok) and tok or ""),
label_text = label,
}
end
end


return root
item = _safe_tbl(item)
local tok = _safe_str(item.label_wt or item.def_wt, "")
local label = _safe_str(item.label or item.text or item.key, "")
if label == "" and tok ~= "" then
label = _display_text_from_token(tok)
end
return {
label_tok = tok,
label_text = label,
}
end
 
local function _build_keyword_pill(frame, parent, item)
local spec = _normalize_keyword_spec(item)
local display_text = _safe_str(spec.label_text, "")
local hit_tok = _safe_str(spec.label_tok, "")
if display_text == "" then return end
 
local has_def = _has_pipe(hit_tok)
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))
 
if has_def then
_build_hitbox(pill, "sv-mech-keyword-pill__hit", frame, hit_tok)
end
end
end


Line 994: Line 1,304:


local mechanics = _safe_tbl(tabs_obj.mechanics)
local mechanics = _safe_tbl(tabs_obj.mechanics)
local keywords  = _safe_tbl(tabs_obj.keywords)
local effects  = _safe_tbl(tabs_obj.effects)
local effects  = _safe_tbl(tabs_obj.effects)
local events    = _safe_tbl(tabs_obj.events)


local mechanics_mods = _safe_tbl(mechanics.mods)
local mechanics_keywords = _safe_tbl(mechanics.keywords)
local effects_cards = _safe_tbl(effects.cards)
local effects_cards = _safe_tbl(effects.cards)
local events_cards  = _safe_tbl(events.cards)


local effects_count = _to_int(effects.count, #effects_cards)
if #mechanics_mods == 0 then
local events_count  = _to_int(events.count, #events_cards)
mechanics_mods = _safe_tbl(mechanics.grid)
end
if #mechanics_keywords == 0 then
local old_keywords = _safe_tbl(tabs_obj.keywords)
mechanics_keywords = _safe_tbl(old_keywords.pills)
end
if #effects_cards == 0 then
local old_events = _safe_tbl(tabs_obj.events)
local merged = {}
 
for _, card in ipairs(_safe_tbl(effects.cards)) do
table.insert(merged, card)
end
for _, card in ipairs(_safe_tbl(old_events.cards)) do
table.insert(merged, card)
end
 
effects_cards = merged
end
 
local mechanics_count = _to_int(
mechanics.count,
#mechanics_mods + #mechanics_keywords
)
 
local effects_count = _to_int(
effects.count,
#effects_cards
)


local tab_specs = {
local tab_specs = {
{ key = "mechanics", title = "Mechanics" },
{ key = "mechanics", title = "Mechanics (" .. tostring(mechanics_count) .. ")" },
{ key = "keywords",  title = "Keywords"  },
{ key = "effects",  title = "Effects (" .. tostring(effects_count) .. ")" },
{ key = "effects",  title = "Effects (" .. tostring(effects_count) .. ")" },
{ key = "events",    title = "Events (" .. tostring(events_count) .. ")" },
}
}


Line 1,033: Line 1,368:
end
end


do
local function render_ref_icon(parent, icon)
local panel = panels:tag("div")
icon = _safe_tbl(icon)
:addClass("sv-tabpanel")
local page = _safe_str(icon.page, "")
:attr("role", "tabpanel")
local alt = _safe_str(icon.alt, "")
:attr("data-panel", "mechanics")
if page ~= "" then
 
parent:node(_render_file_image(page, alt, 52))
local gridwrap = panel:tag("div"):addClass("sv-kw-grid")
else
for _, item in ipairs(_safe_tbl(mechanics.grid)) do
parent:node(_question_badge())
item = _safe_tbl(item)
local cell = gridwrap:tag("div"):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


do
local function render_effect_stats(parent, stats, kind)
local panel = panels:tag("div")
stats = _safe_tbl(stats)
:addClass("sv-tabpanel")
kind = _safe_str(kind, ""):lower()
:attr("role", "tabpanel")
:attr("data-panel", "keywords")
:attr("hidden", "hidden")


local pills = panel:tag("div"):addClass("sv-tab-pills")
if kind == "event" then
for _, kw in ipairs(_safe_tbl(keywords.pills)) do
local sub = parent:tag("div"):addClass("sv-ref-sub")
local wt = _render_keyword_pill(frame, kw)
if stats.text ~= nil then
if wt then
_apply_value(sub, stats, level)
pills:tag("span"):addClass("sv-pill"):wikitext(wt)
else
_apply_value(sub, { text = "" }, level)
end
end
return
end
end
end


local function render_ref_icon(parent, icon)
if next(stats) ~= nil then
icon = _safe_tbl(icon)
local srow = parent:tag("div"):addClass("sv-ref-stats")
local page = _safe_str(icon.page, "")
 
local alt = _safe_str(icon.alt, "")
local d = srow:tag("span")
if page ~= "" then parent:node(_render_file_image(page, alt, 52)) else parent:node(_question_badge()) end
:addClass("sv-pill")
:addClass("sv-pill--value")
:addClass("sv-ref-stat")
_apply_value(d, stats.duration, level)
 
local c = srow:tag("span")
:addClass("sv-pill")
:addClass("sv-pill--value")
:addClass("sv-ref-stat")
_apply_value(c, stats.chance, level)
 
local st = srow:tag("span")
:addClass("sv-pill")
:addClass("sv-pill--value")
:addClass("sv-ref-stat")
_apply_value(st, stats.stacks, level)
end
end
end


local function render_ref_card(parent, card, is_event)
local function render_ref_card(parent, card)
card = _safe_tbl(card)
card = _safe_tbl(card)


local kind = _safe_str(card.kind, "")
local title = _safe_str(card.title, "—")
local title = _safe_str(card.title, "—")
local page = _safe_str(card.page, "")
local page = _safe_str(card.page, "")
local icon = _safe_tbl(card.icon)
local icon = _safe_tbl(card.icon)


local container = parent:tag("div"):addClass("sv-ref-card")
local container = parent:tag("div")
:addClass("sv-tile")
:addClass("sv-hover-lift")
:addClass("sv-ref-card")
if kind ~= "" then
container:addClass("sv-ref-card--" .. _slugify(kind))
end


local ico = container:tag("div"):addClass("sv-ref-ico")
local ico = container:tag("div")
:addClass("sv-ref-ico")
:addClass("sv-tile")
render_ref_icon(ico, icon)
render_ref_icon(ico, icon)


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,093: Line 1,445:
end
end


if is_event then
render_effect_stats(text, card.stats, kind)
local sub = text:tag("div"):addClass("sv-ref-sub")
_apply_value(sub, card.type, level)
return
end
 
local stats = card.stats
if type(stats) == "table" and next(stats) ~= nil then
local srow = text:tag("div"):addClass("sv-ref-stats")
local d = srow:tag("span"):addClass("sv-ref-stat"); _apply_value(d, stats.duration, level)
local c = srow:tag("span"):addClass("sv-ref-stat"); _apply_value(c, stats.chance, level)
local st = srow:tag("span"):addClass("sv-ref-stat"); _apply_value(st, stats.stacks, level)
end
end
end


Line 1,111: Line 1,451:
local panel = panels:tag("div")
local panel = panels:tag("div")
:addClass("sv-tabpanel")
:addClass("sv-tabpanel")
:addClass("sv-mech-panel")
:attr("role", "tabpanel")
:attr("role", "tabpanel")
:attr("data-panel", "effects")
:attr("data-panel", "mechanics")
:attr("hidden", "hidden")
 
local mods_wrap = panel:tag("div")
:addClass("sv-mech-panel__group")
:addClass("sv-mech-panel__group--mods")
:addClass("sv-mech-panel__mods")
 
local keys_wrap = panel:tag("div")
:addClass("sv-mech-panel__group")
:addClass("sv-mech-panel__group--keywords")
:addClass("sv-mech-panel__keywords")
 
if #mechanics_mods > 0 then
mods_wrap:tag("div"):addClass("sv-tab-section-title"):wikitext("Modifiers")
 
local gridwrap = mods_wrap:tag("div"):addClass("sv-mech-mod-grid")
for _, item in ipairs(mechanics_mods) do
_build_modifier_pill(frame, gridwrap, item, level)
end
end
 
if #mechanics_keywords > 0 then
keys_wrap:tag("div"):addClass("sv-tab-section-title"):wikitext("Keywords")
 
local pills = keys_wrap:tag("div"):addClass("sv-tab-pills")
for _, kw in ipairs(mechanics_keywords) do
_build_keyword_pill(frame, pills, kw)
end
end


local grid = panel:tag("div"):addClass("sv-ref-grid")
if #mechanics_mods == 0 and #mechanics_keywords == 0 then
for _, card in ipairs(effects_cards) do
panel:tag("div")
render_ref_card(grid, card, false)
:addClass("sv-tile")
:addClass("sv-tab-empty")
:wikitext("—")
end
end
end
end
Line 1,124: Line 1,494:
local panel = panels:tag("div")
local panel = panels:tag("div")
:addClass("sv-tabpanel")
:addClass("sv-tabpanel")
:addClass("sv-hidden")
:attr("role", "tabpanel")
:attr("role", "tabpanel")
:attr("data-panel", "events")
:attr("data-panel", "effects")
:attr("hidden", "hidden")
:attr("hidden", "hidden")


local grid = panel:tag("div"):addClass("sv-ref-grid")
if #effects_cards > 0 then
for _, card in ipairs(events_cards) do
local grid = panel:tag("div"):addClass("sv-ref-grid")
render_ref_card(grid, card, true)
for _, card in ipairs(effects_cards) do
render_ref_card(grid, card)
end
else
panel:tag("div")
:addClass("sv-tile")
:addClass("sv-tab-empty")
:wikitext("—")
end
end
end
end
Line 1,201: Line 1,579:
end
end


local function _build_req_users_schema2(root_id, requirements_map, users_map)
local function _build_req_users_native(root_id, requirements_map, users_map)
local req_groups = _map_to_groups(requirements_map, { "Skills", "Weapon Types", "Stances" })
local req_groups = _map_to_groups(requirements_map, { "Skills", "Weapon Types", "Stances" })
local usr_groups = _map_to_groups(users_map,        { "Classes", "Summons" })
local usr_groups = _map_to_groups(users_map,        { "Classes", "Monsters", "Summons" })


local req_box, req_n = _build_grouped_disclose(root_id, "req", "Requirements", req_groups)
local req_box, req_n = _build_grouped_disclose(root_id, "req", "Requirements", req_groups)
Line 1,235: Line 1,613:
end
end


local function _render_schema1(frame, payload, notes_wt)
local function _append_meta_req_bubble(wrap, mod, node)
if not node then return false end
 
wrap:tag("div")
:addClass("sv-skill-meta-block__bubble")
:addClass("sv-skill-meta-block__bubble--" .. mod)
:node(node)
 
return true
end
 
local function _build_meta_req_block(meta_node, req_node)
if not meta_node and not req_node then return nil end
 
local wrap = mw.html.create("div")
:addClass("sv-skill-meta-block")
 
local count = 0
if _append_meta_req_bubble(wrap, "meta", meta_node) then count = count + 1 end
if _append_meta_req_bubble(wrap, "req", req_node) then count = count + 1 end
 
if count == 1 then
wrap:addClass("sv-skill-meta-block--single")
end
 
return wrap
end
 
local function _render_legacy(frame, payload, notes_wt)
local identity = _safe_tbl(payload.identity)
local identity = _safe_tbl(payload.identity)
local display_name = _safe_str(identity.display_name, "Unknown Skill")
local display_name = _safe_str(identity.display_name, "Unknown Skill")
Line 1,261: Line 1,667:


box.top:node(_build_identity(frame, root_id, identity, notes_wt))
box.top:node(_build_identity(frame, root_id, identity, notes_wt))
box.top:node(_build_meta_row(frame, payload.meta_row))


local meta_block = _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)
if reqrow then box.top:node(reqrow) end
local meta_req_block = _build_meta_req_block(meta_block, reqrow)
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,276: Line 1,682:
end
end


local function _render_schema2(frame, payload, notes_wt)
local function _render_native(frame, payload, notes_wt)
local identity = _safe_tbl(payload.identity)
local identity = _safe_tbl(payload.identity)
local display_name = _safe_str(identity.display_name, "Unknown Skill")
local display_name = _safe_str(identity.display_name, "Unknown Skill")
Line 1,288: Line 1,694:
end
end


local min_level, default_level, max_level = _normalize_level_schema2(payload.level)
local min_level, default_level, max_level = _normalize_level_native(payload.level)


local box = GI.box({
local box = GI.box({
Line 1,311: Line 1,717:


box.top:node(_build_identity(frame, root_id, identity, notes_wt))
box.top:node(_build_identity(frame, root_id, identity, notes_wt))
box.top:node(_build_meta_row_schema2(frame, payload.skill_meta))


local reqrow = _build_req_users_schema2(root_id, payload.requirements, payload.users)
local meta_block = _build_meta_row_native(frame, payload.skill_meta)
if reqrow then box.top:node(reqrow) end
local reqrow = _build_req_users_native(root_id, payload.requirements, payload.users)
local meta_req_block = _build_meta_req_block(meta_block, reqrow)
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, min_level, _safe_tbl(payload.level).overcap)
local scaling_block, actual_default = _build_skill_scaling_native(
frame,
root_id,
payload.skill_scaling,
default_level,
max_level,
min_level,
_safe_tbl(payload.level).overcap
)


box.bottom:node(level_panel)
box.bottom:node(scaling_block)
box.bottom:node(_build_scaling_top_schema2(frame, payload.skill_scaling, actual_default))
box.bottom:node(_build_core_stats_schema2(payload.skill_scaling, actual_default))
box.bottom:node(_build_tabs(frame, root_id, payload.tabs, actual_default))
box.bottom:node(_build_tabs(frame, root_id, payload.tabs, actual_default))


Line 1,337: Line 1,750:


if payload.schema == 2 then
if payload.schema == 2 then
return _render_schema2(frame, payload, notes_wt)
return _render_native(frame, payload, notes_wt)
end
end


return _render_schema1(frame, payload, notes_wt)
return _render_legacy(frame, payload, notes_wt)
end
end


return p
return p