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

Revision as of 04:01, 19 February 2026 by Eviand (talk | contribs)

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

-- Module:GameInfo
-- Base container + shared scaffolding for all GameInfo.* renderers (Phase 4.1).
--
-- Routing:
--   Skill pages (recommended):
--     {{#invoke:GameInfo|Skills|notes=...|data=...}}
--
--   Category pages (generic):
--     {{#invoke:GameInfo|Category|name=Skills|...}} -> Module:GameInfo/Skills
--     {{#invoke:GameInfo|Category|name=Monsters|...}} -> Module:GameInfo/Monsters (future)
--
-- Notes:
-- - This module is the stable entrypoint. Render logic lives in submodules.
-- - Submodules are expected to return a string of wikitext/HTML.

local p = {}

-- Shared TemplateStyles page (Module space per Phase 4.1).
local DEFAULT_STYLE_SRC = "Module:GameInfo/styles.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 _trim(v)
	if v == nil then return nil end
	local s = tostring(v)
	return mw.text and mw.text.trim(s) or s:match("^%s*(.-)%s*$")
end

local function _is_html_node(x)
	-- mw.html nodes are Lua tables with common builder methods.
	return type(x) == "table" and type(x.tag) == "function" and type(x.wikitext) == "function"
end

local function _add_content(container, content)
	if content == nil then return end
	if _is_html_node(content) then
		container:node(content)
		return
	end
	container:wikitext(tostring(content))
end

local function _error_box(msg)
	-- Keep this simple and obvious on-wiki while troubleshooting.
	local div = mw.html.create("div")
		:addClass("error")
		:wikitext(tostring(msg))
	return tostring(div)
end

-- Emit TemplateStyles. Submodules should include this once at the top of output.
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

-- Core builder: returns { root = <div>, top = <div>, bottom = <div> }
-- Caller fills top/bottom and tostring(root) to return.
function p.new_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-gi-card")
		:attr("data-gi", "1")
		:attr("data-level", tostring(level))
		:attr("data-max-level", tostring(max_level))

	if root_id and root_id ~= "" then
		root:attr("id", root_id)
	end

	-- Optional variant class for category modules (e.g. "sv-gi--skills").
	if opts.variant and opts.variant ~= "" then
		root:addClass("sv-gi--" .. tostring(opts.variant))
	end

	local top = root:tag("div"):addClass("sv-gi-top")
	local bottom = root:tag("div"):addClass("sv-gi-bottom")

	return {
		root = root,
		top = top,
		bottom = bottom,
	}
end

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

-- -----------------------------------------------------------------------------
-- Routing / Dispatch
-- -----------------------------------------------------------------------------

local ROUTES = {
	-- Canonical category -> submodule
	["Skills"] = "Module:GameInfo/Skills",
	-- Future:
	-- ["Monsters"] = "Module:GameInfo/Monsters",
	-- ["Equips"]   = "Module:GameInfo/Equips",
}

local function _pick_entry(mod)
	-- Support a few common entrypoint names.
	if type(mod) ~= "table" then return nil end
	if type(mod.main) == "function" then return mod.main end
	if type(mod.render) == "function" then return mod.render end
	if type(mod.Skills) == "function" then return mod.Skills end
	return nil
end

local function _invoke_submodule(frame, module_title)
	local ok_mod, mod = pcall(require, module_title)
	if not ok_mod then
		return _error_box("GameInfo: failed to require " .. module_title)
	end

	local fn = _pick_entry(mod)
	if type(fn) ~= "function" then
		return _error_box("GameInfo: " .. module_title .. " has no entry function (main/render).")
	end

	local ok_call, out = pcall(fn, frame)
	if not ok_call then
		return _error_box("GameInfo: error inside " .. module_title)
	end

	return out
end

-- Explicit entrypoint for skill pages.
function p.Skills(frame)
	frame = frame or mw.getCurrentFrame()
	return _invoke_submodule(frame, "Module:GameInfo/Skills")
end

-- Generic entrypoint for category pages (not limited to Skills).
-- Usage:
--   {{#invoke:GameInfo|Category|name=Skills|...}}
-- If name matches ROUTES, we use that mapping.
-- Otherwise, we attempt Module:GameInfo/<name> (safe, limited characters).
function p.Category(frame)
	frame = frame or mw.getCurrentFrame()
	local args = frame.args or {}

	local name = _trim(args.name or args.category or args[1])
	if not name or name == "" then
		return _error_box("GameInfo.Category: missing |name=CategoryName")
	end

	-- First: known route table.
	local module_title = ROUTES[name]

	-- Second: case-insensitive match on known routes.
	if not module_title and mw.ustring then
		local want = mw.ustring.lower(name)
		for k, v in pairs(ROUTES) do
			if mw.ustring.lower(k) == want then
				module_title = v
				break
			end
		end
	end

	-- Third: dynamic fallback to Module:GameInfo/<name>
	if not module_title then
		-- Allow letters, numbers, spaces, underscores, hyphens, and slashes.
		-- (Spaces are fine in page titles; MediaWiki normalizes.)
		if not name:match("^[%w %_%-/]+$") then
			return _error_box("GameInfo.Category: invalid name=" .. tostring(name))
		end
		module_title = "Module:GameInfo/" .. name
	end

	return _invoke_submodule(frame, module_title)
end

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

	local id = args.id or "sv-gi-skeleton-1"
	local level = args.level or args.data_level or 1
	local max_level = args.max or args.max_level or 10

	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({
		root_id = id,
		level = level,
		max_level = max_level,
		variant = args.variant,
		top = tostring(top),
		bottom = tostring(bottom),
	})
end

return p