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

From SpiritVale Wiki
Created page with "-- Module:GameInfo -- Base container + shared scaffolding for all GameInfo.* renderers (Phase 4.1). local p = {} -- Shared TemplateStyles page (Module space per Phase 4.1). local DEFAULT_STYLE_SRC = "Module:GameInfo/gameinfo.css" local function _to_int(v, fallback) if v == nil then return fallback end local n = tonumber(v) if not n then return fallback end n = math.floor(n + 0.0) return n end local function _is_html_node(x) -- mw.html nodes are Lua tables with..."
 
No edit summary
Tags: Mobile edit Mobile web edit
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:GameInfo
local p = {}
-- Base container + shared scaffolding for all GameInfo.* renderers (Phase 4.1).


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",
}


-- Shared TemplateStyles page (Module space per Phase 4.1).
local function _trim(v)
local DEFAULT_STYLE_SRC = "Module:GameInfo/gameinfo.css"
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)
if v == nil then return fallback end
local n = tonumber(v)
local n = tonumber(v)
if not n then return fallback end
if not n then return fallback end
n = math.floor(n + 0.0)
return math.floor(n + 0.0)
return n
end
end


local function _is_html_node(x)
local function _is_html_node(x)
-- mw.html nodes are Lua tables with common builder methods.
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 _add_content(container, content)
local function _append_content(node, value)
if content == nil then return end
if value == nil then return end
if _is_html_node(content) then
if _is_html_node(value) then
container:node(content)
node:node(value)
return
else
node:wikitext(tostring(value))
end
end
container:wikitext(tostring(content))
end
end


-- Emit TemplateStyles. Submodules should include this once at the top of output.
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)
function p.styles(frame, src)
frame = frame or mw.getCurrentFrame()
frame = frame or mw.getCurrentFrame()
Line 36: Line 78:
end
end


-- Core builder: returns { root = <div>, top = <div>, bottom = <div> }
function p.box(opts)
-- Caller fills top/bottom and tostring(root) to return.
function p.new_box(opts)
opts = opts or {}
opts = opts or {}


Line 50: 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


-- Optional variant class for category modules (e.g. "sv-gi--skills").
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 }
root = root,
top = top,
bottom = bottom,
}
end
end


-- Convenience wrapper: build a full box in one call.
p.new_box = p.box
-- opts.top / opts.bottom can be strings or mw.html nodes.
 
function p.render_box(opts)
function p.render_box(opts)
local box = p.new_box(opts)
local box = p.box(opts)
_add_content(box.top, opts and opts.top)
if opts then
_add_content(box.bottom, opts and opts.bottom)
_append_content(box.top, opts.top)
_append_content(box.bottom, opts.bottom)
end
return tostring(box.root)
return tostring(box.root)
end
end


-- Optional: #invoke test entrypoint to verify box + CSS wiring.
local function _require_submodule(module_title)
-- {{#invoke:GameInfo|skeleton|id=sv-gi-test-1|level=1|max=10}}
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)
function p.skeleton(frame)
frame = frame or mw.getCurrentFrame()
frame = frame or mw.getCurrentFrame()
local args = frame.args or {}


local id = args.id or "sv-gi-skeleton-1"
local id = p.arg(frame, "id", "sv-gi-skeleton-1")
local level = args.level or args.data_level or 1
local level = p.int(frame, "level", 1, 1, 999)
local max_level = args.max or args.max_level or 10
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 97: Line 188:


return p.styles(frame) .. p.render_box({
return p.styles(frame) .. p.render_box({
root_id = id,
id = id,
level = level,
level = level,
max_level = max_level,
max_level = max_level,
variant = args.variant,
variant = variant,
top = tostring(top),
top = top,
bottom = tostring(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:

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 (or root_id)
  • variant adds class sv-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