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 |
||
| (33 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Module:GameInfo/Skills | |||
-- Phase 4.2 — Final native-first rendering (schema 1 + schema 2) | |||
-- | |||
-- Notes: | |||
-- - JSON Definition tokens must be plain strings that decode to Domain|Key. | |||
-- - Interactivity is provided by sitewide popup systems in Common.js. | |||
-- - Schema 2 is the primary layout target; schema 1 remains supported. | |||
-- - Mechanics / keywords are JSON-driven; the renderer does not infer future | |||
-- mechanic definition tokens from labels. | |||
local p = {} | local p = {} | ||
| Line 6: | 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) | ||
if s == nil then return "" end | if s == nil then return "" end | ||
s = tostring(s) | s = tostring(s) | ||
return (mw.text and mw.text.trim) and mw.text.trim(s) or (s:gsub("^%s+", ""):gsub("%s+$", "")) | return (mw.text and mw.text.trim) and mw.text.trim(s) | ||
or (s:gsub("^%s+", ""):gsub("%s+$", "")) | |||
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 31: | 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 55: | Line 128: | ||
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 109: | Line 185: | ||
return mw.html.create("span") | return mw.html.create("span") | ||
:addClass("sv-img") | :addClass("sv-img") | ||
:wikitext(string.format("[[%s|%s|link=|alt=%s]]", title, size, mw.text.nowiki(_safe_str(alt, "")))) | :wikitext(string.format( | ||
"[[%s|%s|link=|alt=%s]]", | |||
title, | |||
size, | |||
mw.text.nowiki(_safe_str(alt, "")) | |||
)) | |||
end | end | ||
| Line 140: | Line 221: | ||
end | end | ||
local function | local function _has_pipe(s) | ||
s = _trim(s) | |||
return s ~= "" and s:find("|", 1, true) ~= nil | |||
end | |||
local function _split_def_token(s) | |||
s = _trim(s) | |||
if s == "" then return nil, nil end | |||
local bar = s:find("|", 1, true) | |||
if not bar then return nil, nil end | |||
local domain = _trim(s:sub(1, bar - 1)) | |||
local key = _trim(s:sub(bar + 1)) | |||
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) | |||
end | |||
local args = { domain, key } | |||
if noicon then | |||
args.noicon = "1" | |||
end | |||
if type(extra_args) == "table" then | |||
for k, v in pairs(extra_args) do | |||
if v ~= nil and tostring(v) ~= "" then | |||
args[k] = tostring(v) | |||
end | |||
end | |||
end | |||
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 | |||
local function _meta_lines_def(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") | ||
| Line 147: | Line 280: | ||
local s = _trim(ln) | local s = _trim(ln) | ||
if s ~= "" then | if s ~= "" then | ||
wrap:tag("span"):wikitext( | local wt = _render_def_token(frame, s, true) or mw.text.nowiki(s) | ||
wrap:tag("span"):wikitext(wt) | |||
any = true | any = true | ||
end | end | ||
| Line 157: | Line 291: | ||
end | end | ||
local function | local function _build_hitbox(parent, class_name, frame, token) | ||
token = _safe_str(token, "") | |||
if | if not _has_pipe(token) then return false end | ||
return | |||
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 | end | ||
local | 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 | end | ||
return false | |||
end | |||
if val.text ~= nil then | |||
return not _is_blankish(val.text) | |||
end | end | ||
return | return false | ||
end | end | ||
local function _build_notes_tip(root_id, notes_wt) | 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 | |||
local function _split_lines(s) | |||
if s == nil then return {} end | |||
s = tostring(s) | |||
s = s:gsub("\r\n", "\n"):gsub("\r", "\n") | |||
if mw.text and mw.text.split then | |||
return mw.text.split(s, "\n", true) | |||
end | |||
local out = {} | |||
for line in (s .. "\n"):gmatch("(.-)\n") do | |||
table.insert(out, line) | |||
end | |||
return out | |||
end | |||
local function _render_notes_body(parent, notes_wt) | |||
notes_wt = _trim(notes_wt) | |||
if notes_wt == "" then | |||
parent:wikitext("—") | |||
return | |||
end | |||
notes_wt = notes_wt:gsub("(^|\n)%s+([*#])", "%1%2") | |||
local lines = _split_lines(notes_wt) | |||
local i = 1 | |||
local wrote_any = false | |||
local function is_list_line(ln) | |||
return type(ln) == "string" and ln:match("^[*#]") ~= nil | |||
end | |||
while i <= #lines do | |||
local ln = lines[i] or "" | |||
if _trim(ln) == "" then | |||
i = i + 1 | |||
elseif is_list_line(ln) then | |||
local mark = ln:sub(1, 1) | |||
local tagname = (mark == "#") and "ol" or "ul" | |||
local items = {} | |||
while i <= #lines do | |||
local l = lines[i] or "" | |||
if _trim(l) == "" then | |||
i = i + 1 | |||
break | |||
end | |||
if not l:match("^" .. mark) then break end | |||
local txt = l:gsub("^" .. mark .. "+%s*", "") | |||
txt = _trim(txt) | |||
if txt ~= "" then table.insert(items, txt) end | |||
i = i + 1 | |||
end | |||
if #items > 0 then | |||
local list = parent:tag(tagname) | |||
for _, txt in ipairs(items) do | |||
list:tag("li"):wikitext(txt) | |||
end | |||
wrote_any = true | |||
end | |||
else | |||
local block = {} | |||
while i <= #lines do | |||
local l = lines[i] or "" | |||
if _trim(l) == "" then | |||
i = i + 1 | |||
break | |||
end | |||
if is_list_line(l) then break end | |||
table.insert(block, l) | |||
i = i + 1 | |||
end | |||
if #block > 0 then | |||
local joined = table.concat(block, "<br />\n") | |||
parent:tag("div"):addClass("sv-note-par"):wikitext(joined) | |||
wrote_any = true | |||
end | |||
end | |||
end | |||
if not wrote_any then | |||
parent:wikitext(mw.text.nowiki(notes_wt)) | |||
end | |||
end | |||
local function _build_notes_tip(frame, root_id, notes_wt) | |||
notes_wt = _trim(notes_wt) | notes_wt = _trim(notes_wt) | ||
if notes_wt == "" then return nil end | if notes_wt == "" then return nil end | ||
local tip_id = root_id .. "-notes" | local tip_id = root_id .. "-notes" | ||
local wrap = mw.html.create("div"):addClass("sv-tip") | local wrap = mw.html.create("div"):addClass("sv-tip") | ||
local btn = wrap:tag("span") | local btn = wrap:tag("span") | ||
:addClass("sv-tip-btn") | :addClass("sv-tip-btn") | ||
:addClass("sv-tip-btn--icon") | |||
:addClass("sv-hover-lift") | |||
:attr("role", "button") | :attr("role", "button") | ||
:attr("tabindex", "0") | :attr("tabindex", "0") | ||
:attr("data-sv-toggle", "1") | :attr("data-sv-toggle", "1") | ||
:attr("data-sv-pop", "hover") | |||
:attr("data-sv-pop-size", "sm") | |||
:attr("aria-label", "Notes") | :attr("aria-label", "Notes") | ||
:attr("aria-controls", tip_id) | :attr("aria-controls", tip_id) | ||
| Line 201: | Line 486: | ||
:addClass("sv-tip-pop") | :addClass("sv-tip-pop") | ||
:addClass("sv-hidden") | :addClass("sv-hidden") | ||
:attr("hidden", "hidden") | |||
:attr("id", tip_id) | :attr("id", tip_id) | ||
:attr("aria-label", "Notes") | :attr("aria-label", "Notes") | ||
local head = pop:tag("div"):addClass("sv-tip-pop-head") | local head = pop:tag("div"):addClass("sv-tip-pop-head") | ||
head:tag("div"):addClass("sv-tip-pop-title"):wikitext("Notes") | head:tag("div"):addClass("sv-tip-pop-title"):wikitext("Notes") | ||
pop:tag("div"):addClass("sv-tip-pop-body") | local body = pop:tag("div"):addClass("sv-tip-pop-body") | ||
_render_notes_body(body, notes_wt) | |||
return wrap | return wrap | ||
end | |||
local function _normalize_overcap_sources(sources, total_bonus) | |||
sources = _safe_tbl(sources) | |||
local out = {} | |||
local missing = 0 | |||
local explicit_sum = 0 | |||
for _, s in ipairs(sources) do | |||
local page = "" | |||
local lv = nil | |||
if type(s) == "string" then | |||
page = _trim(s) | |||
elseif type(s) == "table" then | |||
page = _safe_str(s.page or s[1] or "", "") | |||
lv = _to_int(s.levels or s.level or s[2], nil) | |||
end | |||
if page ~= "" then | |||
if lv and lv > 0 then | |||
explicit_sum = explicit_sum + lv | |||
else | |||
lv = nil | |||
missing = missing + 1 | |||
end | |||
table.insert(out, { page = page, levels = lv }) | |||
end | |||
end | |||
local remaining = (_to_int(total_bonus, 0) or 0) - explicit_sum | |||
if remaining < 0 then remaining = 0 end | |||
if missing == 1 and remaining > 0 then | |||
for _, it in ipairs(out) do | |||
if it.levels == nil then | |||
it.levels = remaining | |||
break | |||
end | |||
end | |||
end | |||
return out | |||
end | |||
local function _build_overcap_tip(root_id, default_cap, max_cap, sources) | |||
default_cap = _to_int(default_cap, 1) or 1 | |||
max_cap = _to_int(max_cap, default_cap) or default_cap | |||
if max_cap <= default_cap then return nil end | |||
local total = max_cap - default_cap | |||
local tip_id = root_id .. "-overcap" | |||
local norm = _normalize_overcap_sources(sources, total) | |||
local show_unknown = (#norm == 0) | |||
local wrap = mw.html.create("span") | |||
:addClass("sv-tip") | |||
:addClass("sv-overcap-tip") | |||
local btn = wrap:tag("span") | |||
:addClass("sv-tip-btn") | |||
:addClass("sv-tip-btn--pill") | |||
:addClass("sv-overcap-btn") | |||
:addClass("sv-hover-lift") | |||
:attr("role", "button") | |||
:attr("tabindex", "0") | |||
:attr("data-sv-toggle", "1") | |||
:attr("data-sv-pop", "click") | |||
:attr("data-sv-pop-size", "sm") | |||
:attr("aria-label", "Overcap") | |||
:attr("aria-controls", tip_id) | |||
:attr("aria-expanded", "false") | |||
:wikitext(mw.text.nowiki("Overcap +" .. tostring(total))) | |||
local pop = wrap:tag("div") | |||
:addClass("sv-tip-pop") | |||
:addClass("sv-hidden") | |||
:attr("hidden", "hidden") | |||
:attr("id", tip_id) | |||
:attr("aria-label", "Overcap") | |||
local head = pop:tag("div"):addClass("sv-tip-pop-head") | |||
head:tag("div"):addClass("sv-tip-pop-title"):wikitext("Overcap sources") | |||
local body = pop:tag("div"):addClass("sv-tip-pop-body") | |||
body:tag("div"):addClass("sv-overcap-summary"):wikitext(mw.text.nowiki( | |||
"Raises max from " .. tostring(default_cap) .. " to " .. tostring(max_cap) .. " (+" .. tostring(total) .. ")." | |||
)) | |||
local ul = body:tag("ul"):addClass("sv-disclose-list") | |||
if show_unknown then | |||
ul:tag("li"):wikitext("Source unknown") | |||
else | |||
for _, it in ipairs(norm) do | |||
local li = ul:tag("li") | |||
local page = _safe_str(it.page, "") | |||
if page ~= "" then | |||
li:wikitext(string.format("[[%s|%s]]", page, mw.text.nowiki(page))) | |||
else | |||
li:wikitext("—") | |||
end | |||
if it.levels ~= nil and it.levels > 0 then | |||
li:wikitext(mw.text.nowiki(" (+" .. tostring(it.levels) .. ")")) | |||
else | |||
li:wikitext(mw.text.nowiki(" (+?)")) | |||
end | |||
end | |||
end | |||
return wrap | |||
end | |||
local function _normalize_disclose_item(it) | |||
if type(it) == "string" then | |||
local s = _trim(it) | |||
if s == "" then return nil end | |||
return { text = s, page = s } | |||
end | |||
if type(it) ~= "table" then return nil end | |||
if it.text ~= nil or it.page ~= nil or it.suffix ~= nil or it.name ~= nil or it.label ~= nil then | |||
local text = _safe_str(it.text or it.label or it.name or "", "") | |||
if text == "" then return nil end | |||
local page = _safe_str(it.page or "", "") | |||
if page == "" then page = text end | |||
local suffix = _safe_str(it.suffix or "", "") | |||
return { text = text, page = (page ~= "" and page or nil), suffix = (suffix ~= "" and suffix or nil) } | |||
end | |||
if #it >= 1 then | |||
local name = _safe_str(it[1], "") | |||
if name == "" then return nil end | |||
local suffix = _safe_str(it[2], "") | |||
return { | |||
text = name, | |||
page = name, | |||
suffix = (suffix ~= "" and suffix or nil) | |||
} | |||
end | |||
local lines = _safe_tbl(it.label_lines) | |||
local text = "" | |||
for _, ln in ipairs(lines) do | |||
local s = _trim(ln) | |||
if s ~= "" then | |||
if text ~= "" then text = text .. " " end | |||
text = text .. s | |||
end | |||
end | |||
if text == "" then return nil end | |||
local link = _safe_tbl(it.link) | |||
local page = _safe_str(link.page or it.page or "", "") | |||
return { text = text, page = (page ~= "" and page or nil) } | |||
end | end | ||
| Line 217: | Line 661: | ||
local n = 0 | local n = 0 | ||
for _, g in ipairs(_safe_tbl(groups)) do | for _, g in ipairs(_safe_tbl(groups)) do | ||
for _, | for _, it in ipairs(_safe_tbl(g.items)) do | ||
if _normalize_disclose_item(it) then n = n + 1 end | |||
end | |||
end | end | ||
return n | return n | ||
| Line 227: | Line 673: | ||
local pop_id = root_id .. "-" .. key | local pop_id = root_id .. "-" .. key | ||
local wrap = mw.html.create("div"):addClass("sv-disclose") | local wrap = mw.html.create("div"):addClass("sv-disclose") | ||
local btn = wrap:tag("span") | local btn = wrap:tag("span") | ||
:addClass("sv-disclose-btn") | :addClass("sv-disclose-btn") | ||
:addClass("sv-disclose-btn--compact") | |||
:addClass("sv-hover-lift") | |||
:attr("role", "button") | :attr("role", "button") | ||
:attr("tabindex", "0") | :attr("tabindex", "0") | ||
:attr("data-sv-toggle", "1") | :attr("data-sv-toggle", "1") | ||
:attr("data-sv-pop", "click") | |||
:attr("data-sv-pop-size", "sm") | |||
:attr("aria-label", label) | :attr("aria-label", label) | ||
:attr("aria-controls", pop_id) | :attr("aria-controls", pop_id) | ||
| Line 245: | Line 694: | ||
:addClass("sv-disclose-pop") | :addClass("sv-disclose-pop") | ||
:addClass("sv-hidden") | :addClass("sv-hidden") | ||
:attr("hidden", "hidden") | |||
:attr("id", pop_id) | :attr("id", pop_id) | ||
:attr("aria-label", label) | :attr("aria-label", label) | ||
local head = pop:tag("div"):addClass("sv-disclose-pop-head") | local head = pop:tag("div"):addClass("sv-disclose-pop-head") | ||
head:tag("div"):addClass("sv-disclose-pop-title"):wikitext(mw.text.nowiki(label)) | head:tag("div"):addClass("sv-disclose-pop-title"):wikitext(mw.text.nowiki(label)) | ||
local | local body = pop:tag("div"):addClass("sv-disclose-pop-body") | ||
local secs = body:tag("div"):addClass("sv-disclose-sections") | |||
local function render_item(parent, norm) | |||
local item = parent:tag("span"):addClass("sv-disclose-item") | |||
if norm.page then | |||
item:wikitext(string.format( | |||
"[[%s|%s]]", | |||
norm.page, | |||
mw.text.nowiki(norm.text) | |||
)) | |||
else | |||
item:wikitext(mw.text.nowiki(norm.text)) | |||
end | |||
local sfx = _safe_str(norm.suffix or "", "") | |||
if sfx ~= "" then | |||
if not sfx:match("^%s") then sfx = " " .. sfx end | |||
item:tag("span"):addClass("sv-disclose-sfx"):wikitext(mw.text.nowiki(sfx)) | |||
end | |||
end | |||
for _, g in ipairs(_safe_tbl(groups)) do | for _, g in ipairs(_safe_tbl(groups)) do | ||
local title = _safe_str(g.title, "") | local title = _safe_str(g.title, "—") | ||
local items = _safe_tbl(g.items) | local items = _safe_tbl(g.items) | ||
if | if #items > 0 then | ||
local sec = secs:tag("div"):addClass("sv-disclose-sec") | |||
sec:tag("div"):addClass("sv-disclose-sec-title"):wikitext(mw.text.nowiki(title)) | |||
local line = sec:tag("div"):addClass("sv-disclose-sec-items") | |||
local | local norms = {} | ||
for _, it in ipairs(items) do | |||
local norm = _normalize_disclose_item(it) | |||
if norm then table.insert(norms, norm) end | |||
end | |||
if #norms == 0 then | |||
line:tag("span"):addClass("sv-disclose-item"):wikitext("—") | |||
else | else | ||
for i, norm in ipairs(norms) do | |||
render_item(line, norm) | |||
if i < #norms then line:wikitext(", ") end | |||
end | |||
end | end | ||
end | end | ||
| Line 282: | Line 754: | ||
end | end | ||
local function | 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") | ||
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 319: | Line 779: | ||
:wikitext(mw.text.nowiki(_safe_str(identity.display_name, "Unknown Skill"))) | :wikitext(mw.text.nowiki(_safe_str(identity.display_name, "Unknown Skill"))) | ||
local tip = _build_notes_tip(root_id, notes_wt) | local tip = _build_notes_tip(frame, root_id, notes_wt) | ||
if tip then title_row:node(tip) end | if tip then title_row:node(tip) end | ||
| Line 329: | Line 789: | ||
end | end | ||
local function _build_meta_row(meta_row) | 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 | |||
local function _build_meta_row(frame, meta_row) | |||
meta_row = _safe_tbl(meta_row) | meta_row = _safe_tbl(meta_row) | ||
| Line 335: | 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 | local icon = _safe_tbl(cell.icon) | ||
local lines = _safe_tbl(cell.label_lines) | |||
local hit_tok = "" | |||
local | for _, ln in ipairs(lines) do | ||
local s = _safe_str(ln, "") | |||
if _has_pipe(s) then | |||
hit_tok = s | |||
break | |||
end | |||
end | |||
_build_meta_card(frame, meta, { | |||
icon_page = _safe_str(icon.page, ""), | |||
icon_alt = _safe_str(icon.alt, ""), | |||
lines = lines, | |||
hit_tok = hit_tok, | |||
}) | |||
end | |||
return meta | |||
end | |||
local function _build_meta_row_native(frame, skill_meta) | |||
skill_meta = _safe_tbl(skill_meta) | |||
local meta = mw.html.create("div"):addClass("sv-skill-meta") | |||
for i = 1, 4 do | |||
local cell = _safe_tbl(skill_meta[i]) | |||
local lines = _safe_tbl(cell.label_lines) | |||
local hit_tok = _safe_str(cell.label_wt, "") | |||
if #lines == 0 and hit_tok ~= "" then | |||
lines = { hit_tok } | |||
end | end | ||
_build_meta_card(frame, meta, { | |||
icon_page = _safe_str(cell.icon, ""), | |||
icon_alt = "", | |||
lines = lines, | |||
hit_tok = hit_tok, | |||
}) | |||
end | end | ||
| Line 355: | Line 875: | ||
end | end | ||
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 376: | Line 892: | ||
end | end | ||
local function _build_level(default_level, max_level) | local function _normalize_level_native(level_obj) | ||
level_obj = _safe_tbl(level_obj) | |||
local min_level = _to_int(level_obj.base, 1) | |||
if not min_level or min_level < 1 then min_level = 1 end | |||
local default = _to_int(level_obj.default, min_level) | |||
local oc = _safe_tbl(level_obj.overcap) | |||
local max_level = _to_int(oc.max, default) | |||
if not max_level or max_level < min_level then max_level = min_level end | |||
if not default or default < min_level then default = min_level end | |||
if default > max_level then default = max_level end | |||
return min_level, default, max_level | |||
end | |||
local function _build_level(root_id, default_level, max_level, min_level, overcap_obj) | |||
local min = _to_int(min_level, 1) | |||
local default = _to_int(default_level, 1) | local default = _to_int(default_level, 1) | ||
local max = _to_int(max_level, 1) | local max = _to_int(max_level, 1) | ||
if max < | if min < 1 then min = 1 end | ||
if default < | if max < min then max = min end | ||
if default < min then default = min end | |||
if default > max then default = max end | if default > max then default = max end | ||
local root = mw.html.create("div"):addClass("sv-skill-level") | local root = mw.html.create("div") | ||
:addClass("sv-card") | |||
:addClass("sv-skill-level") | |||
:attr("data-sv-level-boundary", "1") | |||
:attr("data-sv-level-hide-ticks", "1") | |||
local ui = root:tag("div"):addClass("sv-level-ui") | local ui = root:tag("div"):addClass("sv-level-ui") | ||
local label = ui:tag("div"):addClass("sv-level-label") | local label = ui:tag("div") | ||
:addClass("sv-level-label") | |||
:attr("data-sv-helper-text", "Slide to set level") | |||
label:wikitext("Level ") | label:wikitext("Level ") | ||
label:tag("span"):addClass("sv-level-num"):wikitext(tostring(default)) | label:tag("span"):addClass("sv-level-num"):wikitext(tostring(default)) | ||
label:wikitext(" / ") | label:wikitext(" / ") | ||
label:tag("span"):addClass("sv-level-max"):wikitext(tostring(max)) | label:tag("span"):addClass("sv-level-max"):wikitext(tostring(max)) | ||
if type(overcap_obj) == "table" and max > default then | |||
local oc = _safe_tbl(overcap_obj) | |||
local tip = _build_overcap_tip(root_id, default, max, oc.sources) | |||
if tip then ui:node(tip) end | |||
elseif max > default then | |||
local tip = _build_overcap_tip(root_id, default, max, nil) | |||
if tip then ui:node(tip) end | |||
end | |||
local slider_wrap = root:tag("div"):addClass("sv-level-slider") | local slider_wrap = root:tag("div"):addClass("sv-level-slider") | ||
| Line 403: | Line 953: | ||
:attr("tabindex", "0") | :attr("tabindex", "0") | ||
:attr("aria-label", "Skill level select") | :attr("aria-label", "Skill level select") | ||
:attr("aria-valuemin", | :attr("aria-valuemin", tostring(min)) | ||
:attr("aria-valuemax", tostring(max)) | :attr("aria-valuemax", tostring(max)) | ||
:attr("aria-valuenow", tostring(default)) | :attr("aria-valuenow", tostring(default)) | ||
:attr("data-min", | :attr("data-min", tostring(min)) | ||
:attr("data-max", tostring(max)) | :attr("data-max", tostring(max)) | ||
:attr("data-value", tostring(default)) | :attr("data-value", tostring(default)) | ||
| Line 416: | Line 966: | ||
slider_wrap:tag("div"):addClass("sv-level-ticklabels"):attr("aria-hidden", "true") | slider_wrap:tag("div"):addClass("sv-level-ticklabels"):attr("aria-hidden", "true") | ||
return root, default, max | return root, default, max, min | ||
end | end | ||
local function _build_scaling_top(scaling, level) | local function _build_scaling_top(frame, scaling, level) | ||
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 432: | Line 985: | ||
col:tag("div"):addClass("sv-scaling-label"):wikitext("Damage") | col:tag("div"):addClass("sv-scaling-label"):wikitext("Damage") | ||
end | end | ||
do | do | ||
local col = grid:tag("div"):addClass("sv-scaling-col sv-scaling-col--scaling") | local col = grid:tag("div"):addClass("sv-scaling-col sv-scaling-col--scaling") | ||
local list = col:tag("div"):addClass("sv-scaling-list") | local list = col:tag("div"):addClass("sv-scaling-list") | ||
for _, it in ipairs(_safe_tbl(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-tile") | |||
:addClass("sv-scaling-item") | |||
:addClass("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 | |||
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 | ||
| Line 450: | 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 457: | 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 474: | Line 1,048: | ||
return root | return root | ||
end | |||
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) | |||
local root = mw.html.create("div") | |||
: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 | |||
do | |||
local core_group = body:tag("div") | |||
:addClass("sv_skill_scaling__group") | |||
:addClass("sv_skill_scaling__group--core") | |||
local core_col = core_group:tag("div") | |||
: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) | |||
if has_value then | |||
pill:addClass("is-active") | |||
else | |||
pill:addClass("is-inactive") | |||
end | |||
if has_def or has_value 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 _normalize_keyword_spec(item) | |||
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 | |||
return { | |||
label_tok = (_has_pipe(tok) and tok or ""), | |||
label_text = label, | |||
} | |||
end | |||
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 480: | Line 1,304: | ||
local mechanics = _safe_tbl(tabs_obj.mechanics) | local mechanics = _safe_tbl(tabs_obj.mechanics) | ||
local effects = _safe_tbl(tabs_obj.effects) | local effects = _safe_tbl(tabs_obj.effects) | ||
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 | if #mechanics_mods == 0 then | ||
local | 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 = "effects", title = "Effects (" .. tostring(effects_count) .. ")" }, | { key = "effects", title = "Effects (" .. tostring(effects_count) .. ")" }, | ||
} | } | ||
| Line 519: | Line 1,368: | ||
end | end | ||
local function render_ref_icon(parent, icon) | |||
icon = _safe_tbl(icon) | |||
local page = _safe_str(icon.page, "") | |||
local alt = _safe_str(icon.alt, "") | |||
if page ~= "" then | |||
parent:node(_render_file_image(page, alt, 52)) | |||
local | else | ||
parent:node(_question_badge()) | |||
end | end | ||
end | end | ||
local function render_effect_stats(parent, stats, kind) | |||
stats = _safe_tbl(stats) | |||
kind = _safe_str(kind, ""):lower() | |||
local | if kind == "event" then | ||
local sub = parent:tag("div"):addClass("sv-ref-sub") | |||
if stats.text ~= nil then | |||
_apply_value(sub, stats, level) | |||
else | |||
_apply_value(sub, { text = "—" }, level) | |||
end | end | ||
return | |||
end | end | ||
if next(stats) ~= nil then | |||
local srow = parent:tag("div"):addClass("sv-ref-stats") | |||
local d = srow:tag("span") | |||
: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 | 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 579: | Line 1,445: | ||
end | end | ||
render_effect_stats(text, card.stats, kind) | |||
end | end | ||
| Line 597: | 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", " | :attr("data-panel", "mechanics") | ||
: | |||
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 | |||
if #mechanics_mods == 0 and #mechanics_keywords == 0 then | |||
panel:tag("div") | |||
:addClass("sv-tile") | |||
:addClass("sv-tab-empty") | |||
:wikitext("—") | |||
end | end | ||
end | end | ||
| Line 610: | 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", " | :attr("data-panel", "effects") | ||
:attr("hidden", "hidden") | :attr("hidden", "hidden") | ||
local grid = panel:tag("div"):addClass("sv-ref-grid") | if #effects_cards > 0 then | ||
local grid = panel:tag("div"):addClass("sv-ref-grid") | |||
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 623: | Line 1,515: | ||
end | end | ||
function | local function _map_to_groups(map_tbl, preferred_order) | ||
local m = _safe_tbl(map_tbl) | |||
local function normalize_items(v) | |||
if type(v) == "table" then | |||
return v | |||
end | |||
if v == nil then | |||
return {} | |||
end | |||
return { v } | |||
end | |||
local used = {} | |||
local group_keys = {} | |||
local function push_key(k) | |||
k = tostring(k) | |||
if k ~= "" and not used[k] and m[k] ~= nil then | |||
used[k] = true | |||
table.insert(group_keys, k) | |||
end | |||
end | |||
local explicit = m.__order or m.order | |||
if type(explicit) == "table" then | |||
for _, k in ipairs(explicit) do | |||
push_key(k) | |||
end | |||
end | |||
if type(preferred_order) == "table" then | |||
for _, k in ipairs(preferred_order) do | |||
push_key(k) | |||
end | |||
end | |||
local rest = {} | |||
for k, _ in pairs(m) do | |||
k = tostring(k) | |||
if k ~= "__order" and k ~= "order" and not used[k] then | |||
table.insert(rest, k) | |||
end | |||
end | |||
table.sort(rest) | |||
for _, k in ipairs(rest) do | |||
table.insert(group_keys, k) | |||
end | |||
local out_groups = {} | |||
for _, title in ipairs(group_keys) do | |||
local items = normalize_items(m[title]) | |||
local out_items = {} | |||
for _, it in ipairs(_safe_tbl(items)) do | |||
table.insert(out_items, it) | |||
end | |||
table.insert(out_groups, { title = title, items = out_items }) | |||
end | |||
return out_groups | |||
end | |||
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 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 usr_box, usr_n = _build_grouped_disclose(root_id, "usr", "Users", usr_groups) | |||
if req_n == 0 and usr_n == 0 then return nil end | |||
local row = mw.html.create("div"):addClass("sv-reqrow") | |||
if req_box then row:node(req_box) end | |||
if usr_box then row:node(usr_box) end | |||
return row | |||
end | |||
local function _build_req_users(root_id, requirements, users) | |||
requirements = _safe_tbl(requirements) | |||
users = _safe_tbl(users) | |||
local req_box, req_n = _build_grouped_disclose( | |||
root_id, "req", "Requirements", _safe_tbl(requirements.groups) | |||
) | |||
local usr_box, usr_n = _build_grouped_disclose( | |||
root_id, "usr", "Users", _safe_tbl(users.groups) | |||
) | |||
if req_n == 0 and usr_n == 0 then return nil end | |||
local row = mw.html.create("div"):addClass("sv-reqrow") | |||
if req_box then row:node(req_box) end | |||
if usr_box then row:node(usr_box) end | |||
return row | |||
end | |||
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 | 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 | 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 645: | Line 1,653: | ||
end | end | ||
local default_level, _base_max, max_level = _normalize_level(payload.level) | local default_level, _base_max, max_level = _normalize_level(payload.level) | ||
| Line 659: | Line 1,666: | ||
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)) | ||
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 | 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) | |||
box.bottom:node(level_panel) | box.bottom:node(level_panel) | ||
box.bottom:node(_build_scaling_top(payload.scaling_top, actual_default)) | box.bottom:node(_build_scaling_top(frame, payload.scaling_top, actual_default)) | ||
box.bottom:node(_build_core_stats(payload.core_stats, actual_default)) | box.bottom:node(_build_core_stats(payload.core_stats, 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)) | ||
return tostring(box.root) | return tostring(box.root) | ||
end | |||
local function _render_native(frame, payload, notes_wt) | |||
local identity = _safe_tbl(payload.identity) | |||
local display_name = _safe_str(identity.display_name, "Unknown Skill") | |||
local idx = _to_int(GI.arg(frame, "index", "1"), 1) | |||
if idx < 1 then idx = 1 end | |||
local root_id = GI.arg(frame, "id", "") | |||
if root_id == "" then | |||
root_id = "sv-skill-" .. _slugify(display_name) .. "-" .. tostring(idx) | |||
end | |||
local min_level, default_level, max_level = _normalize_level_native(payload.level) | |||
local box = GI.box({ | |||
root_id = root_id, | |||
level = default_level, | |||
max_level = max_level, | |||
variant = "skills", | |||
}) | |||
box.root:addClass("sv-skill-card") | |||
box.top:addClass("sv-skill-top") | |||
box.bottom:addClass("sv-skill-bottom") | |||
box.root:attr("data-level-default", tostring(default_level)) | |||
do | |||
local oc = _safe_tbl(_safe_tbl(payload.level).overcap) | |||
if next(oc) ~= nil then | |||
local safe_oc = { max = _to_int(oc.max, max_level), sources = _safe_tbl(oc.sources) } | |||
box.root:attr("data-level-overcap", mw.text.jsonEncode(safe_oc)) | |||
end | |||
end | |||
box.top:node(_build_identity(frame, root_id, identity, notes_wt)) | |||
local meta_block = _build_meta_row_native(frame, payload.skill_meta) | |||
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 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(scaling_block) | |||
box.bottom:node(_build_tabs(frame, root_id, payload.tabs, actual_default)) | |||
return tostring(box.root) | |||
end | |||
function p.render(frame) | |||
frame = frame or mw.getCurrentFrame() | |||
local notes_wt = GI.arg(frame, "notes", "") | |||
local payload, err, preview = _decode_payload(frame) | |||
if not payload then | |||
local debug = (GI.arg(frame, "debug", "0") == "1") | |||
return _err(err, debug and preview or nil) | |||
end | |||
if payload.schema == 2 then | |||
return _render_native(frame, payload, notes_wt) | |||
end | |||
return _render_legacy(frame, payload, notes_wt) | |||
end | end | ||
return p | return p | ||