Module:GameInfo: Difference between revisions
More actions
No edit summary |
No edit summary Tags: Mobile edit Mobile web edit |
||
| (5 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
local p = {} | |||
local | -- Sitewide color/system ownership now lives in Common.css + Citizen.css. | ||
-- This module still loads its scoped TemplateStyles entrypoint, but that | |||
-- stylesheet should defer to shared sitewide tokens/components whenever possible. | |||
local DEFAULT_STYLE_SRC = "Module:GameInfo/styles.css" | |||
local ROUTES = { | |||
Skills = "Module:GameInfo/Skills", | |||
} | |||
local function _trim(v) | |||
if v == nil then return "" end | |||
local s = tostring(v) | |||
if mw.text and mw.text.trim then | |||
return mw.text.trim(s) | |||
end | |||
return (s:gsub("^%s+", ""):gsub("%s+$", "")) | |||
end | |||
local function _to_int(v, fallback) | local function _to_int(v, fallback) | ||
local n = tonumber(v) | local n = tonumber(v) | ||
if not n then return fallback end | if not n then return fallback end | ||
return math.floor(n + 0.0) | |||
end | end | ||
local function _is_html_node(x) | local function _is_html_node(x) | ||
return type(x) == "table" | |||
return type(x) == "table" and type(x.tag) == "function" and type(x.wikitext) == "function" | and type(x.tag) == "function" | ||
and type(x.wikitext) == "function" | |||
end | end | ||
local function | local function _append_content(node, value) | ||
if | if value == nil then return end | ||
if _is_html_node( | if _is_html_node(value) then | ||
node:node(value) | |||
else | |||
node:wikitext(tostring(value)) | |||
end | end | ||
end | end | ||
local function _error_box(msg) | local function _error_box(msg) | ||
return tostring( | |||
mw.html.create("div") | |||
:addClass("sv-card") | |||
:addClass("sv-gi-error") | |||
return | :wikitext(tostring(msg)) | ||
) | |||
end | |||
function p.arg(frame, key, fallback) | |||
local args = frame and frame.args or {} | |||
local v = _trim(args[key]) | |||
if v ~= "" then return v end | |||
return fallback | |||
end | |||
function p.int(frame, key, fallback, minv, maxv) | |||
local n = _to_int(p.arg(frame, key, nil), fallback) | |||
if minv ~= nil and n < minv then n = minv end | |||
if maxv ~= nil and n > maxv then n = maxv end | |||
return n | |||
end | |||
function p.bool(frame, key, fallback) | |||
local v = p.arg(frame, key, nil) | |||
if v == nil then return fallback end | |||
v = _trim(v):lower() | |||
if v == "1" or v == "true" or v == "yes" or v == "y" then return true end | |||
if v == "0" or v == "false" or v == "no" or v == "n" then return false end | |||
return fallback | |||
end | end | ||
function p.styles(frame, src) | function p.styles(frame, src) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
| Line 62: | Line 78: | ||
end | end | ||
function p.box(opts) | |||
function p. | |||
opts = opts or {} | opts = opts or {} | ||
| Line 76: | Line 90: | ||
local root = mw.html.create("div") | local root = mw.html.create("div") | ||
:addClass("sv-card") | |||
:addClass("sv-gi-card") | :addClass("sv-gi-card") | ||
:attr("data-gi", "1") | :attr("data-gi", "1") | ||
:attr("data-sv-card", "1") | |||
:attr("data-gi-phase", "4.1") | |||
:attr("data-level", tostring(level)) | :attr("data-level", tostring(level)) | ||
:attr("data-max-level", tostring(max_level)) | :attr("data-max-level", tostring(max_level)) | ||
if root_id and root_id ~= "" then | if root_id and tostring(root_id) ~= "" then | ||
root:attr("id", root_id) | root:attr("id", tostring(root_id)) | ||
end | end | ||
if opts.variant and tostring(opts.variant) ~= "" then | |||
if opts.variant and opts.variant ~= "" then | |||
root:addClass("sv-gi--" .. tostring(opts.variant)) | root:addClass("sv-gi--" .. tostring(opts.variant)) | ||
root:attr("data-sv-card-variant", tostring(opts.variant)) | |||
end | end | ||
local top = root:tag("div"):addClass("sv-gi-top") | local top = root:tag("div"):addClass("sv-gi-top") | ||
local bottom = root:tag("div"):addClass("sv-gi-bottom") | local bottom = root:tag("div") | ||
:addClass("sv-gi-bottom") | |||
:attr("data-sv-level-scope", "1") | |||
return { | return { root = root, top = top, bottom = bottom } | ||
end | end | ||
p.new_box = p.box | |||
function p.render_box(opts) | function p.render_box(opts) | ||
local box = p. | local box = p.box(opts) | ||
if opts then | |||
_append_content(box.top, opts.top) | |||
_append_content(box.bottom, opts.bottom) | |||
end | |||
return tostring(box.root) | return tostring(box.root) | ||
end | end | ||
local function _require_submodule(module_title) | |||
local ok, mod = pcall(require, module_title) | |||
if not ok then | |||
return nil, "GameInfo: failed to require " .. tostring(module_title) | |||
local | end | ||
if type(mod) ~= "table" then | |||
return nil, "GameInfo: " .. tostring(module_title) .. " did not return a table" | |||
end | |||
if type(mod.render) ~= "function" then | |||
return nil, "GameInfo: " .. tostring(module_title) .. " must export render(frame)" | |||
end | |||
return mod, nil | |||
if type(mod) ~= "table" then return nil | |||
if type(mod.render) | |||
return nil | |||
end | end | ||
local function | local function _invoke(frame, module_title) | ||
local | local mod, err = _require_submodule(module_title) | ||
if not | if not mod then | ||
return | return p.styles(frame) .. _error_box(err) | ||
end | end | ||
local | local style_src = mod.STYLE_SRC or DEFAULT_STYLE_SRC | ||
local | local ok, out = pcall(mod.render, frame) | ||
if not | if not ok then | ||
return _error_box("GameInfo: error inside " .. module_title) | return p.styles(frame, style_src) .. _error_box("GameInfo: error inside " .. tostring(module_title)) | ||
end | end | ||
return out | return p.styles(frame, style_src) .. tostring(out or "") | ||
end | end | ||
function p.Skills(frame) | function p.Skills(frame) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
return | return _invoke(frame, ROUTES.Skills) | ||
end | end | ||
function p.Category(frame) | function p.Category(frame) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
local | local name = p.arg(frame, "name", p.arg(frame, "category", nil)) | ||
if not name or name == "" then | if not name or name == "" then | ||
return _error_box("GameInfo.Category: missing |name= | return p.styles(frame) .. _error_box("GameInfo.Category: missing |name=") | ||
end | end | ||
local module_title = ROUTES[name] | local module_title = ROUTES[name] | ||
if not module_title then | if not module_title then | ||
return p.styles(frame) .. _error_box("GameInfo.Category: unsupported name=" .. tostring(name)) | |||
end | end | ||
return | return _invoke(frame, module_title) | ||
end | end | ||
function p.skeleton(frame) | function p.skeleton(frame) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
local id = | local id = p.arg(frame, "id", "sv-gi-skeleton-1") | ||
local level = | local level = p.int(frame, "level", 1, 1, 999) | ||
local max_level = | local max_level = p.int(frame, "max", 10, 1, 999) | ||
local variant = p.arg(frame, "variant", nil) | |||
local top = mw.html.create("div"):wikitext("GameInfo Top (locked container)") | local top = mw.html.create("div"):wikitext("GameInfo Top (locked container)") | ||
| Line 210: | Line 188: | ||
return p.styles(frame) .. p.render_box({ | return p.styles(frame) .. p.render_box({ | ||
id = id, | |||
level = level, | level = level, | ||
max_level = max_level, | max_level = max_level, | ||
variant = | variant = variant, | ||
top = | top = top, | ||
bottom = | bottom = bottom, | ||
}) | }) | ||
end | end | ||
return p | return p | ||
Latest revision as of 16:32, 6 March 2026
Module:GameInfo is the stable entrypoint for SpiritVale “GameInfo” renderers (Phase 4.1).
Contract
Pages are generated/updated by SVWT + pywikibot (compiled artifacts).
This module reads arguments from frame.args only (no parent-frame wrappers).
Submodules are pure renderers and must export render(frame).
Entry points
{{#invoke:GameInfo|Skills|id=...|index=...|notes=...|data=...}}
{{#invoke:GameInfo|Category|name=Skills|...}}
The Category router is allowlist-only. Users cannot force arbitrary require() targets.
Routing
GameInfo routes only to modules listed in the internal allowlist. Currently supported:
- Skills → Module:GameInfo/Skills
TemplateStyles
GameInfo always emits one TemplateStyles tag per invocation. Default CSS:
Module:GameInfo/styles.css
Submodules may override by setting:
STYLE_SRC = "Module:GameInfo/skills.css"
Shared scaffolding
Args helpers
arg(frame, key, fallback)trims and returns a string.int(frame, key, fallback, min, max)clamps integers.bool(frame, key, fallback)supports 1/0, true/false, yes/no.
= Container builder
box(opts) creates the canonical card shell:
- root:
div.sv-gi-card - top:
div.sv-gi-top - bottom:
div.sv-gi-bottom
Data attributes:
data-gi="1"data-gi-phase="4.1"data-level,data-max-level
Optional:
id(orroot_id)variantadds classsv-gi--<variant>
Convenience renderer
render_box(opts) builds the box and fills top/bottom with either HTML nodes or wikitext.
Error handling
If a route cannot be loaded or throws, GameInfo returns TemplateStyles plus a wiki-friendly error box:
div.sv-gi-error
Debug / wiring test
A simple sanity check exists:
{{#invoke:GameInfo|skeleton|id=sv-gi-test-1|level=1|max=10|variant=skills}}
local p = {}
-- Sitewide color/system ownership now lives in Common.css + Citizen.css.
-- This module still loads its scoped TemplateStyles entrypoint, but that
-- stylesheet should defer to shared sitewide tokens/components whenever possible.
local DEFAULT_STYLE_SRC = "Module:GameInfo/styles.css"
local ROUTES = {
Skills = "Module:GameInfo/Skills",
}
local function _trim(v)
if v == nil then return "" end
local s = tostring(v)
if mw.text and mw.text.trim then
return mw.text.trim(s)
end
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
local function _to_int(v, fallback)
local n = tonumber(v)
if not n then return fallback end
return math.floor(n + 0.0)
end
local function _is_html_node(x)
return type(x) == "table"
and type(x.tag) == "function"
and type(x.wikitext) == "function"
end
local function _append_content(node, value)
if value == nil then return end
if _is_html_node(value) then
node:node(value)
else
node:wikitext(tostring(value))
end
end
local function _error_box(msg)
return tostring(
mw.html.create("div")
:addClass("sv-card")
:addClass("sv-gi-error")
:wikitext(tostring(msg))
)
end
function p.arg(frame, key, fallback)
local args = frame and frame.args or {}
local v = _trim(args[key])
if v ~= "" then return v end
return fallback
end
function p.int(frame, key, fallback, minv, maxv)
local n = _to_int(p.arg(frame, key, nil), fallback)
if minv ~= nil and n < minv then n = minv end
if maxv ~= nil and n > maxv then n = maxv end
return n
end
function p.bool(frame, key, fallback)
local v = p.arg(frame, key, nil)
if v == nil then return fallback end
v = _trim(v):lower()
if v == "1" or v == "true" or v == "yes" or v == "y" then return true end
if v == "0" or v == "false" or v == "no" or v == "n" then return false end
return fallback
end
function p.styles(frame, src)
frame = frame or mw.getCurrentFrame()
local style_src = src or DEFAULT_STYLE_SRC
return frame:extensionTag("templatestyles", "", { src = style_src })
end
function p.box(opts)
opts = opts or {}
local root_id = opts.root_id or opts.id
local level = _to_int(opts.level, 1)
local max_level = _to_int(opts.max_level, 1)
if max_level < 1 then max_level = 1 end
if level < 1 then level = 1 end
if level > max_level then level = max_level end
local root = mw.html.create("div")
:addClass("sv-card")
:addClass("sv-gi-card")
:attr("data-gi", "1")
:attr("data-sv-card", "1")
:attr("data-gi-phase", "4.1")
:attr("data-level", tostring(level))
:attr("data-max-level", tostring(max_level))
if root_id and tostring(root_id) ~= "" then
root:attr("id", tostring(root_id))
end
if opts.variant and tostring(opts.variant) ~= "" then
root:addClass("sv-gi--" .. tostring(opts.variant))
root:attr("data-sv-card-variant", tostring(opts.variant))
end
local top = root:tag("div"):addClass("sv-gi-top")
local bottom = root:tag("div")
:addClass("sv-gi-bottom")
:attr("data-sv-level-scope", "1")
return { root = root, top = top, bottom = bottom }
end
p.new_box = p.box
function p.render_box(opts)
local box = p.box(opts)
if opts then
_append_content(box.top, opts.top)
_append_content(box.bottom, opts.bottom)
end
return tostring(box.root)
end
local function _require_submodule(module_title)
local ok, mod = pcall(require, module_title)
if not ok then
return nil, "GameInfo: failed to require " .. tostring(module_title)
end
if type(mod) ~= "table" then
return nil, "GameInfo: " .. tostring(module_title) .. " did not return a table"
end
if type(mod.render) ~= "function" then
return nil, "GameInfo: " .. tostring(module_title) .. " must export render(frame)"
end
return mod, nil
end
local function _invoke(frame, module_title)
local mod, err = _require_submodule(module_title)
if not mod then
return p.styles(frame) .. _error_box(err)
end
local style_src = mod.STYLE_SRC or DEFAULT_STYLE_SRC
local ok, out = pcall(mod.render, frame)
if not ok then
return p.styles(frame, style_src) .. _error_box("GameInfo: error inside " .. tostring(module_title))
end
return p.styles(frame, style_src) .. tostring(out or "")
end
function p.Skills(frame)
frame = frame or mw.getCurrentFrame()
return _invoke(frame, ROUTES.Skills)
end
function p.Category(frame)
frame = frame or mw.getCurrentFrame()
local name = p.arg(frame, "name", p.arg(frame, "category", nil))
if not name or name == "" then
return p.styles(frame) .. _error_box("GameInfo.Category: missing |name=")
end
local module_title = ROUTES[name]
if not module_title then
return p.styles(frame) .. _error_box("GameInfo.Category: unsupported name=" .. tostring(name))
end
return _invoke(frame, module_title)
end
function p.skeleton(frame)
frame = frame or mw.getCurrentFrame()
local id = p.arg(frame, "id", "sv-gi-skeleton-1")
local level = p.int(frame, "level", 1, 1, 999)
local max_level = p.int(frame, "max", 10, 1, 999)
local variant = p.arg(frame, "variant", nil)
local top = mw.html.create("div"):wikitext("GameInfo Top (locked container)")
local bottom = mw.html.create("div"):wikitext("GameInfo Bottom (locked container)")
return p.styles(frame) .. p.render_box({
id = id,
level = level,
max_level = max_level,
variant = variant,
top = top,
bottom = bottom,
})
end
return p