Module:ModuleView
Shows the game's modules from the game's own data, for {{Module}} and {{Module row}}. Used on Modules and the region pages.
The data comes live from the game's browser build on the game's CDN. The server reads it, and Lua gets it through the library mw.ext.gameData: getVersion(), getModule(key) (by id or by name), getModules(category) and getCategories(). It updates with each game build, without edits on the wiki. Text from the game is escaped. The styles are in Template:Module/styles.css (TemplateStyles).
Entry points
main(frame), for {{Module}}:idorname.textreplaces the shown name. Names link to the module database on perfecttower2.com, or withlinkto a wiki page.viewis one of:icon(default): the name in the text, with a category swatch and a tooltip (category, type, max tier);compact: a card with the short description;full: a panel with the description, max tier, levels per tier, prices, target, tags and game version (numbers through Module:FormatNumber).
row(frame), for {{Module row}}: one table row.layoutpicks the columns: standard (default; withlocation,condition,formula),infinity(addsmaxtier,gridlocation,gridcoordinates,unlockcost) orregion(type, name andcondition).noteadds the wiki's note under the description._view(m, args)and_row(m, args, key, available): the same from a module table, for tests.
Values the game fills in at runtime show in brackets, such as [chance]; a parameter with that name (chance=20%) puts the wiki's value in its place. The templates' own parameter names (id, view, formula and so on) don't work for this.
An id or name the game doesn't have shows "Unknown module" and adds Category:Pages with unknown modules, except in the Template and Module namespaces. When the game data can't be read, the id or name shows as plain text.
--[[----------------------------------------------------------------------------
Module:ModuleView
The game's modules for {{Module|id=…}}, from the browser build's data on
gamescdn (mw.ext.gameData, read by the server). They update with each game
build, without edits here.
Views: icon (the name with its category color, inline), compact (a card)
and full (a panel with description, numbers and tags).
Text from the game is escaped. Names link to the module database on
perfecttower2.com, or to a wiki page with link=.
----------------------------------------------------------------------------]]--
local p = {}
local STYLES = 'Template:Module/styles.css'
local UNKNOWN_CATEGORY = 'Pages with unknown modules'
local function esc(text)
return mw.text.nowiki(text or '')
end
local function nonEmpty(value)
if value and mw.text.trim(value) ~= '' then
return mw.text.trim(value)
end
end
-- "Offensive · Passive · Max tier 250"
local function metaLine(m)
return string.format('%s · %s · %s', m.categoryName, m.type,
m.maxTier and ('Max tier ' .. m.maxTier) or 'No upgrades')
end
local function nameLink(m, args)
local label = esc(nonEmpty(args.text) or m.name)
local page = nonEmpty(args.link)
if page then
return string.format('[[%s|%s]]', page, label)
end
return string.format('[%s %s]', m.url, label)
end
local function swatch()
return mw.html.create('span'):addClass('tpt-module-swatch'):attr('aria-hidden', 'true')
end
-- Parameters of the templates themselves; never taken as values for the game's placeholders.
local RESERVED = {
id = true, name = true, view = true, text = true, link = true, layout = true, note = true,
location = true, condition = true, formula = true,
maxtier = true, gridlocation = true, gridcoordinates = true, unlockcost = true,
}
-- Game text, escaped. Values the game fills in at runtime show as [chance], [duration] and so on;
-- a parameter of that name (chance=20%) puts the wiki's value in their place.
local function withValues(text, args)
local out, pos = {}, 1
for from, name, to in mw.ustring.gmatch(text or '', '()%[([%a_][%w_.]*)%]()') do
out[#out + 1] = esc(mw.ustring.sub(text, pos, from - 1))
out[#out + 1] = (not RESERVED[name] and nonEmpty(args[name])) or esc('[' .. name .. ']')
pos = to
end
out[#out + 1] = esc(mw.ustring.sub(text or '', pos))
return table.concat(out)
end
-- The description's main text: lines with superscript kept apart.
local function descriptionText(description, args)
local lines = {}
for _, line in ipairs(description.lines or {}) do
local parts = {}
for _, part in ipairs(line) do
local text = withValues(part.text, args)
parts[#parts + 1] = part.sup and ('<sup>' .. text .. '</sup>') or text
end
lines[#lines + 1] = table.concat(parts)
end
return table.concat(lines, '<br>')
end
local function iconView(m, args)
return mw.html.create('span')
:addClass('wiki-module wiki-module--icon tpt-module-' .. m.category)
:attr('title', metaLine(m))
:node(swatch())
:wikitext(' ' .. nameLink(m, args))
end
local function compactView(m, args)
local card = mw.html.create('div')
:addClass('wiki-module wiki-module--compact tpt-module-' .. m.category)
card:tag('div'):addClass('wiki-module__head')
:node(swatch())
:tag('span'):addClass('wiki-module__name'):wikitext(nameLink(m, args))
card:tag('div'):addClass('wiki-module__meta'):wikitext(esc(metaLine(m)))
if m.description.short ~= '' then
card:tag('div'):addClass('wiki-module__desc'):wikitext(withValues(m.description.short, args))
end
return card
end
local function stat(parent, label, value)
local cell = parent:tag('div'):addClass('wiki-module__stat')
cell:tag('span'):addClass('tpt-label'):wikitext(label)
cell:tag('span'):addClass('wiki-module__value'):wikitext(value)
end
local function fullView(m, args)
local number = require('Module:FormatNumber').format
local panel = mw.html.create('div')
:addClass('wiki-module wiki-module--full tpt-module-' .. m.category)
local top = panel:tag('div'):addClass('wiki-module__top')
top:node(swatch())
top:tag('span'):addClass('wiki-module__name'):wikitext(esc(nonEmpty(args.text) or m.name))
top:tag('code'):addClass('wiki-module__id'):wikitext(esc(m.id))
local tags = top:tag('span'):addClass('wiki-module__tags')
tags:tag('span'):addClass('tpt-tag'):wikitext(esc(m.categoryName))
tags:tag('span'):addClass('tpt-tag'):wikitext(esc(m.type))
local body = panel:tag('div'):addClass('wiki-module__body')
local desc = body:tag('div'):addClass('wiki-module__desc'):wikitext(descriptionText(m.description, args))
if m.description.warning ~= '' then
desc:tag('span'):addClass('wiki-module__warning'):wikitext(withValues(m.description.warning, args))
end
if m.description.note ~= '' then
desc:tag('span'):addClass('wiki-module__credit'):wikitext(withValues(m.description.note, args))
end
local stats = body:tag('div'):addClass('wiki-module__stats')
stat(stats, 'Max tier', m.maxTier and number(m.maxTier) or 'No upgrades')
stat(stats, 'Levels per tier', m.levelsPerTier and number(m.levelsPerTier) or '—')
stat(stats, 'Base price', number(m.basePrice))
stat(stats, 'Price per level', number(m.pricePerLevel))
if m.target then
stat(stats, 'Target', esc(m.target))
end
if m.tags and #m.tags > 0 then
local chips = body:tag('div'):addClass('wiki-module__chips')
for _, tag in ipairs(m.tags) do
chips:tag('span'):addClass('tpt-tag'):wikitext(esc(tag))
end
end
panel:tag('div'):addClass('wiki-module__links')
:wikitext(string.format('[%s Open in the module database]', m.url))
:tag('span'):addClass('wiki-module__version'):wikitext('Game ' .. esc(m.version or ''))
return panel
end
local VIEWS = { icon = iconView, compact = compactView, full = fullView }
-- Renders a module table from mw.ext.gameData.getModule (also used by tests).
function p._view(m, args)
local view = VIEWS[nonEmpty(args.view) or 'icon'] or iconView
return tostring(view(m, args))
end
-- A key the game doesn't know: a visible note, and a maintenance category on content pages.
local function unknown(key, available)
if not available then
-- The game data can't be read right now; show the key as text.
return esc(key)
end
local out = '<span class="wiki-module wiki-module--missing">Unknown module: ' .. esc(key) .. '</span>'
local ns = mw.title.getCurrentTitle().namespace
if ns ~= 10 and ns ~= 828 then
out = out .. '[[Category:' .. UNKNOWN_CATEGORY .. ']]'
end
return out
end
function p.main(frame)
local args = frame:getParent().args
local key = nonEmpty(args.id) or nonEmpty(args.name) or ''
local gameData = require('mw.ext.gameData')
local styles = frame:extensionTag('templatestyles', '', { src = STYLES })
local m = key ~= '' and gameData.getModule(key) or nil
if not m then
return styles .. unknown(key, #gameData.getCategories() > 0)
end
return styles .. p._view(m, args)
end
--[[ {{Module row}}: one row of a module table. The game supplies the category icon, name, active,
description and tags; the wiki supplies what the game data doesn't know, as parameters.
standard layout: Type | Name | Active | Unlock location | Unlock condition | Description | Tags | Formula
(location=, condition=, formula=)
infinity layout: Type | Name | Max. tier | Active | Grid location | Grid coordinates | Unlock cost |
Description | Tags | Formula
(maxtier= overrides the game's max tier; gridlocation=, gridcoordinates=, unlockcost=,
formula=)
note= adds the wiki's own note under the description. ]]
local function cell(attrs, content)
return (attrs and (attrs .. ' | ') or '') .. (content or '')
end
function p._row(m, args, key, available)
local param = function (name) return nonEmpty(args[name]) or '' end
local typeCell, nameCell, active, maxTier, description, tags
if m then
typeCell = cell(string.format('class="wiki-module-row__type" data-sort-value="%s"', esc(m.categoryName)),
string.format('[[File:%s Module.png|%s|x30px|link=|alt=%s module]]', m.categoryName, m.categoryName, m.categoryName))
nameCell = cell(string.format('class="wiki-module-row__name" data-sort-value="%s"', esc(m.name)),
"'''" .. nameLink(m, args) .. "'''")
active = m.type == 'Passive' and '' or (m.type == 'Toggle' and 'Toggle' or 'Yes')
maxTier = nonEmpty(args.maxtier) or (m.maxTier and tostring(m.maxTier)) or '—'
description = descriptionText(m.description, args)
if m.description.warning ~= '' then
description = description .. '<span class="wiki-module__warning">' .. withValues(m.description.warning, args) .. '</span>'
end
if m.description.note ~= '' then
description = description .. '<span class="wiki-module__credit">' .. withValues(m.description.note, args) .. '</span>'
end
if nonEmpty(args.note) then
description = description .. '<span class="wiki-module__note">Note: ' .. nonEmpty(args.note) .. '</span>'
end
tags = esc(mw.ustring.upper(table.concat(m.tags or {}, ' ')))
else
typeCell, active, description, tags = '', '', '', ''
maxTier = param('maxtier')
nameCell = unknown(key, available ~= false)
end
local cells
if nonEmpty(args.layout) == 'infinity' then
cells = { typeCell, nameCell, maxTier, active, param('gridlocation'), param('gridcoordinates'),
param('unlockcost'), description, tags, param('formula') }
else
cells = { typeCell, nameCell, active, param('location'), param('condition'), description, tags,
param('formula') }
end
return '|-\n| ' .. table.concat(cells, '\n| ')
end
function p.row(frame)
local args = frame:getParent().args
local key = nonEmpty(args.id) or nonEmpty(args.name) or ''
local gameData = require('mw.ext.gameData')
local m = key ~= '' and gameData.getModule(key) or nil
local available = m ~= nil or #gameData.getCategories() > 0
-- The styles end up in the row's last cell; MediaWiki writes them once per page.
return p._row(m, args, key, available) .. frame:extensionTag('templatestyles', '', { src = STYLES })
end
return p