Module:GameData: Difference between revisions
From The Ninja Warriors Again RX Wiki
More actions
LongJump bot (talk | contribs) Game data from 7223a3d (demo build of v2.2) |
LongJump bot (talk | contribs) Game data from 645555d |
||
| (4 intermediate revisions by the same user not shown) | |||
| Line 65: | Line 65: | ||
end | end | ||
-- A link to a page, labelled with its name shown literally. | -- The in-game name of each page whose title differs from it ("Arrowstorm (skill)" is | ||
-- named "Arrowstorm"), written by the generator. Links show the name, not the title. | |||
local namesLoaded, names = pcall( mw.loadData, 'Module:GameData/names' ) | |||
if not namesLoaded then | |||
names = {} | |||
end | |||
-- A link to a page, labelled with its in-game name shown literally. | |||
local function pageLink( title ) | local function pageLink( title ) | ||
if tostring( | local label = names[title] or title | ||
return '[[' .. title .. '|' .. nameText( | if label ~= title or tostring( label ):find( "''", 1, true ) then | ||
return '[[' .. title .. '|' .. nameText( label ) .. ']]' | |||
end | end | ||
return '[[' .. title .. ']]' | return '[[' .. title .. ']]' | ||
| Line 165: | Line 173: | ||
local row = fetchRow( t, page ) | local row = fetchRow( t, page ) | ||
if not row then | if not row then | ||
return | return '' | ||
end | end | ||
local out = { '<infobox>', '<title><default>' .. infoboxText( row.name or page ) .. '</default></title>' } | local out = { '<infobox>', '<title><default>' .. infoboxText( row.name or page ) .. '</default></title>' } | ||
| Line 174: | Line 182: | ||
if f.label then | if f.label then | ||
local value = formatValue( f, row[f.name], row ) | local value = formatValue( f, row[f.name], row ) | ||
-- Plain words start with a capital, as the game shows them ("Monster"). | |||
if value and f.type == 'String' and not f.link and not f.list then | |||
value = mw.ustring.upper( mw.ustring.sub( value, 1, 1 ) ) .. mw.ustring.sub( value, 2 ) | |||
end | |||
if value then | if value then | ||
out[#out + 1] = '<data><label>' .. f.label .. '</label><default>' .. value .. '</default></data>' | out[#out + 1] = '<data><label>' .. f.label .. '</label><default>' .. value .. '</default></data>' | ||
end | end | ||
end | end | ||
end | end | ||
out[#out + 1] = '</infobox>' | out[#out + 1] = '</infobox>' | ||
| Line 203: | Line 212: | ||
end | end | ||
local groups, order = {}, {} | local groups, order = {}, {} | ||
local shown = {} | |||
for _, row in ipairs( rows ) do | |||
-- A line with a slot for its value and a value of 0 does nothing: left out. | |||
if not ( row.text:find( '{0}', 1, true ) and tonumber( row.value ) == 0 ) then | |||
shown[#shown + 1] = row | |||
end | |||
end | |||
if #shown == 0 then | |||
return '' | |||
end | |||
rows = shown | |||
for _, row in ipairs( rows ) do | for _, row in ipairs( rows ) do | ||
local scaling = row.scaling or '' | local scaling = row.scaling or '' | ||
| Line 337: | Line 357: | ||
end ) | end ) | ||
return frame:preprocess( text ) | return frame:preprocess( text ) | ||
end | |||
-- Gallery images fit a square box of this many pixels, scaled by a whole number (up by | |||
-- the largest factor that fits, or down by the smallest divisor that fits), so pixel art | |||
-- keeps square pixels. | |||
local GALLERY_BOX = 96 | |||
local function galleryImage( file, page ) | |||
local title = mw.title.new( 'File:' .. file ) | |||
-- Reading a file's size is an "expensive" call, limited per page; past the limit the | |||
-- image is fitted to the box by the browser instead. | |||
local ok, info = pcall( function () return title and title.file end ) | |||
local width, height = ok and info and info.width, ok and info and info.height | |||
if not width or width == 0 or height == 0 then | |||
return '[[File:' .. file .. '|' .. GALLERY_BOX .. 'x' .. GALLERY_BOX .. 'px|link=' .. page .. ']]' | |||
end | |||
local side = math.max( width, height ) | |||
if side <= GALLERY_BOX then | |||
local factor = math.floor( GALLERY_BOX / side ) | |||
width, height = width * factor, height * factor | |||
else | |||
local divisor = math.ceil( side / GALLERY_BOX ) | |||
width, height = math.floor( width / divisor ), math.floor( height / divisor ) | |||
end | |||
return '<span class="gd-gallery-fit" style="width:' .. width .. 'px;height:' .. height .. 'px">[[File:' | |||
.. file .. '|link=' .. page .. ']]</span>' | |||
end | end | ||
| Line 348: | Line 394: | ||
local out = { '<div class="gd-gallery">' } | local out = { '<div class="gd-gallery">' } | ||
for _, row in ipairs( rows ) do | for _, row in ipairs( rows ) do | ||
local image = | local image = '' | ||
if row.icon and row.icon ~= '' then | |||
image = '<div class="gd-gallery-image">' .. galleryImage( row.icon, row.wiki_page ) .. '</div>' | |||
end | |||
out[#out + 1] = '<div class="gd-gallery-item">' .. image .. pageLink( row.wiki_page ) .. '</div>' | out[#out + 1] = '<div class="gd-gallery-item">' .. image .. pageLink( row.wiki_page ) .. '</div>' | ||
end | end | ||
| Line 413: | Line 462: | ||
local out = { '== Modifiers ==', '{| class="wikitable gd-unique-mods"', '! Modifier !! Mild !! Spicy !! Inferno !! Nightmare' } | local out = { '== Modifiers ==', '{| class="wikitable gd-unique-mods"', '! Modifier !! Mild !! Spicy !! Inferno !! Nightmare' } | ||
for _, mod in ipairs( mods ) do | for _, mod in ipairs( mods ) do | ||
local text = colorize( withValue( mod.text or '', | -- The wording's slot shows the whole range across the difficulty columns. | ||
local low, high | |||
for _, value in ipairs( { mod.mild, mod.spicy, mod.inferno, mod.nightmare } ) do | |||
for number in tostring( value or '' ):gmatch( '%-?%d+%.?%d*' ) do | |||
number = tonumber( number ) | |||
if number then | |||
low = low and math.min( low, number ) or number | |||
high = high and math.max( high, number ) or number | |||
end | |||
end | |||
end | |||
local range = low and ( low == high and tostring( low ) or ( low .. '-' .. high ) ) or '#' | |||
local text = colorize( withValue( mod.text or '', range ) ) | |||
if mod.implicit == '1' then | if mod.implicit == '1' then | ||
text = text .. " <small>''(implicit)''</small>" | text = text .. " <small>''(implicit)''</small>" | ||
| Line 521: | Line 582: | ||
end | end | ||
want( v.group ) | want( v.group ) | ||
if v.gallery then | |||
want( v.gallery.image ) | |||
want( v.gallery.caption ) | |||
end | |||
local match = v.match or 'wiki_page' | local match = v.match or 'wiki_page' | ||
local rows = mw.ext.cargo.query( v.table, table.concat( names, ',' ), { | local rows = mw.ext.cargo.query( v.table, table.concat( names, ',' ), { | ||
| Line 533: | Line 598: | ||
if v.note then | if v.note then | ||
out[#out + 1] = "''" .. v.note .. "''" | out[#out + 1] = "''" .. v.note .. "''" | ||
end | |||
-- A view with "gallery" shows its rows as an image gallery: the file and caption fields. | |||
if v.gallery then | |||
local lines = {} | |||
for _, row in ipairs( rows ) do | |||
lines[#lines + 1] = 'File:' .. row[v.gallery.image] .. '|' .. ( v.gallery.caption_prefix or '' ) .. ( row[v.gallery.caption] or '' ) | |||
end | |||
out[#out + 1] = frame:extensionTag( 'gallery', table.concat( lines, '\n' ), | |||
{ mode = 'traditional', widths = v.gallery.width, heights = v.gallery.height } ) | |||
return frame:preprocess( table.concat( out, '\n' ) ) | |||
end | end | ||
local header = {} | local header = {} | ||
for _, c in ipairs( v.columns ) do | for _, c in ipairs( v.columns ) do | ||
header[#header + 1] = c.label | header[#header + 1] = c.label | ||
end | |||
-- Tables with many columns start below the infobox, so they get the page's full width. | |||
-- (Counted from header: "#" doesn't work on tables from mw.loadData.) | |||
local wide = #header >= 7 | |||
if wide then | |||
table.insert( out, 1, '<div class="gd-wide">' ) | |||
end | end | ||
out[#out + 1] = '{| class="wikitable' .. ( v.group and '' or ' sortable' ) .. '"' | out[#out + 1] = '{| class="wikitable' .. ( v.group and '' or ' sortable' ) .. '"' | ||
| Line 544: | Line 625: | ||
if v.group and row[v.group] ~= current then | if v.group and row[v.group] ~= current then | ||
current = row[v.group] | current = row[v.group] | ||
out[#out + 1] = '|-\n! colspan="' .. # | out[#out + 1] = '|-\n! colspan="' .. #header .. '" | ' .. cell( { field = v.group }, defs[v.group], current ) | ||
end | end | ||
local cells = {} | local cells = {} | ||
| Line 553: | Line 634: | ||
end | end | ||
out[#out + 1] = '|}' | out[#out + 1] = '|}' | ||
if wide then | |||
out[#out + 1] = '</div>' | |||
end | |||
return frame:preprocess( table.concat( out, '\n' ) ) | return frame:preprocess( table.concat( out, '\n' ) ) | ||
end | end | ||
| Line 587: | Line 671: | ||
end | end | ||
local args = frame:getParent().args | local args = frame:getParent().args | ||
local out = { | local out = { '{| class="wikitable"' } | ||
for _, f in ipairs( t.fields ) do | for _, f in ipairs( t.fields ) do | ||
local value = args[f.name] | local value = args[f.name] | ||
Latest revision as of 00:29, 4 November 2026
Documentation for this module may be created at Module:GameData/doc
-- Renders LongJump game data stored in Cargo: infoboxes, stat lines, lists of
-- related objects, and the record table shown on Data: pages. Which fields each
-- type has, and how they display, comes from Module:GameData/schema, which the
-- generator writes from the game's types file.
local p = {}
local schema = mw.loadData( 'Module:GameData/schema' )
-- The game's keyword colors, loaded on first use.
local keywordIndex
local function isWordChar( c )
return c ~= '' and c:match( '[%w_]' ) ~= nil
end
-- Colors keywords the way the game does (TextService.ColorizeText): one pass, longest
-- keyword first, whole words only where the keyword starts or ends with a word
-- character, written with the keyword's own capitalisation.
local function colorize( text )
if not keywordIndex then
keywordIndex = {}
for _, entry in ipairs( mw.loadData( 'Module:GameData/keywords' ) ) do
local first = entry[1]:sub( 1, 1 )
keywordIndex[first] = keywordIndex[first] or {}
table.insert( keywordIndex[first], entry )
end
end
-- Byte-wise lower case keeps positions aligned with the original text.
local lower = text:lower()
local out, i = {}, 1
while i <= #text do
local matched
for _, entry in ipairs( keywordIndex[lower:sub( i, i )] or {} ) do
local key = entry[1]
if lower:sub( i, i + #key - 1 ) == key
and not ( isWordChar( key:sub( 1, 1 ) ) and isWordChar( text:sub( i - 1, i - 1 ) ) )
and not ( isWordChar( key:sub( -1 ) ) and isWordChar( text:sub( i + #key, i + #key ) ) ) then
matched = entry
break
end
end
if matched then
out[#out + 1] = '<span class="kw" style="--kw:#' .. matched[3] .. '">' .. matched[2] .. '</span>'
i = i + #matched[1]
else
out[#out + 1] = text:sub( i, i )
i = i + 1
end
end
return table.concat( out )
end
-- A name as text. Page names can hold wiki markup: two apostrophes in a row
-- ("Kunoichi''") would start italics, so they are written as character references.
-- Names without them come back unchanged.
local function nameText( name )
return ( tostring( name ):gsub( "''", '''' ) )
end
-- A name inside an infobox tag. PortableInfobox decodes character references before
-- parsing its content, so the apostrophes are kept in nowiki instead.
local function infoboxText( name )
return ( tostring( name ):gsub( "''", "<nowiki>''</nowiki>" ) )
end
-- The in-game name of each page whose title differs from it ("Arrowstorm (skill)" is
-- named "Arrowstorm"), written by the generator. Links show the name, not the title.
local namesLoaded, names = pcall( mw.loadData, 'Module:GameData/names' )
if not namesLoaded then
names = {}
end
-- A link to a page, labelled with its in-game name shown literally.
local function pageLink( title )
local label = names[title] or title
if label ~= title or tostring( label ):find( "''", 1, true ) then
return '[[' .. title .. '|' .. nameText( label ) .. ']]'
end
return '[[' .. title .. ']]'
end
local function quote( value )
return "'" .. ( tostring( value ):gsub( "'", "''" ) ) .. "'"
end
local function trim( s )
return ( s:gsub( '^%s+', '' ):gsub( '%s+$', '' ) )
end
local function split( value )
local items = {}
for item in tostring( value ):gmatch( '[^,]+' ) do
item = trim( item )
if item ~= '' then
items[#items + 1] = item
end
end
return items
end
local function fieldNames( t )
local names = {}
for _, f in ipairs( t.fields ) do
names[#names + 1] = f.name
end
return table.concat( names, ',' )
end
local function currentPage( frame )
local page = frame.args.page
if page and page ~= '' then
return page
end
return mw.title.getCurrentTitle().text
end
-- Formats one stored value for display, or returns nil when there is nothing to show.
local function formatValue( f, value, row )
if value == nil or value == '' then
return nil
end
if f.type == 'Boolean' then
-- Yes/no fields only show when true.
return ( value == '1' or value == 1 or value == true ) and 'Yes' or nil
end
-- Wording with a slot for the value ("+{0}% to All Resistances") shows "#" there.
value = ( tostring( value ):gsub( '{0}', '#' ) )
local items = f.list and split( value ) or { tostring( value ) }
local levels = f.levels and row and row[f.levels] and split( row[f.levels] ) or {}
for i, item in ipairs( items ) do
-- References to things that have no page in the wiki stay plain text.
if f.link and mw.title.new( item ) and mw.title.new( item ).exists then
item = pageLink( item )
end
if levels[i] then
item = item .. ' (level ' .. levels[i] .. ')'
end
items[i] = item .. ( f.suffix or '' )
end
return table.concat( items, ', ' )
end
-- Every query passes orderBy: Cargo 3.7's default order names list fields with the
-- full table name while the query uses a short alias, which MariaDB rejects.
local function fetchRow( t, page )
local rows = mw.ext.cargo.query( t.table, fieldNames( t ), {
where = 'wiki_page=' .. quote( page ),
orderBy = 'wiki_page',
limit = 1,
} )
return rows[1]
end
-- Reads the object's Data: page so MediaWiki records it as embedded in the current
-- page: when the export rewrites the Data: page, MediaWiki re-renders this page.
-- (Cargo records that dependency only for #cargo_query, not for Lua queries.)
local function dependOnData( t, page )
local title = mw.title.new( 'Data:' .. t.label .. '/' .. page )
if title then
title:getContent()
end
end
-- {{#invoke:GameData|infobox|type=Skill}} on an article.
function p.infobox( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local row = fetchRow( t, page )
if not row then
return ''
end
local out = { '<infobox>', '<title><default>' .. infoboxText( row.name or page ) .. '</default></title>' }
if row.icon and row.icon ~= '' then
out[#out + 1] = '<image><default>' .. row.icon .. '</default></image>'
end
for _, f in ipairs( t.fields ) do
if f.label then
local value = formatValue( f, row[f.name], row )
-- Plain words start with a capital, as the game shows them ("Monster").
if value and f.type == 'String' and not f.link and not f.list then
value = mw.ustring.upper( mw.ustring.sub( value, 1, 1 ) ) .. mw.ustring.sub( value, 2 )
end
if value then
out[#out + 1] = '<data><label>' .. f.label .. '</label><default>' .. value .. '</default></data>'
end
end
end
out[#out + 1] = '</infobox>'
return frame:preprocess( table.concat( out ) ) .. '[[Category:' .. t.category .. ']]'
end
-- {{#invoke:GameData|statlines|type=Skill}}: the game's own effect lines for this page,
-- each linking to every other source of the same stat, under an "Effects" heading.
function p.statlines( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local rows = mw.ext.cargo.query( 'StatLine', 'stat,text,value,scaling,ordinal', {
where = 'wiki_page=' .. quote( page ),
orderBy = 'ordinal',
} )
if #rows == 0 then
return ''
end
local groups, order = {}, {}
local shown = {}
for _, row in ipairs( rows ) do
-- A line with a slot for its value and a value of 0 does nothing: left out.
if not ( row.text:find( '{0}', 1, true ) and tonumber( row.value ) == 0 ) then
shown[#shown + 1] = row
end
end
if #shown == 0 then
return ''
end
rows = shown
for _, row in ipairs( rows ) do
local scaling = row.scaling or ''
if not groups[scaling] then
groups[scaling] = {}
order[#order + 1] = scaling
end
local value = tostring( tonumber( row.value ) or row.value ):gsub( '%.0$', '' )
local line = colorize( ( row.text:gsub( '{0}', ( value:gsub( '%%', '%%%%' ) ) ) ) )
-- Some of the game's lines are fixed text with no slot for the value. Show the
-- text as players see it, and the data's own value beside it unless it's a switch.
if not row.text:find( '{0}', 1, true ) and value ~= '1' then
line = line .. " <small>(game data value: " .. value .. ")</small>"
end
local url = mw.title.new( 'Special:Drilldown/StatLine' ):fullUrl( { stat = row.stat } )
table.insert( groups[scaling], '* <span class="plainlinks">[' .. url .. ' ' .. line .. ']</span>' )
end
-- Like the game's own skill tooltips: one-time effects first, then the per-point ones.
table.sort( order, function ( a, b )
return ( a == 'per point' and 1 or 0 ) < ( b == 'per point' and 1 or 0 )
end )
local out = { '== Effects ==' }
for _, scaling in ipairs( order ) do
if scaling ~= 'flat' and scaling ~= '' then
-- "Per point invested:" as in the game's skill tooltips; "Per pick:", "Per stack:".
local header = scaling:gsub( '^%l', string.upper ) .. ( scaling:find( 'point' ) and ' invested' or '' )
out[#out + 1] = "''" .. header .. ":''"
end
out[#out + 1] = table.concat( groups[scaling], '\n' )
end
return frame:preprocess( table.concat( out, '\n' ) )
end
-- {{#invoke:GameData|related|type=SkillTree}}: lists of other objects that refer
-- to this one, as declared in the schema ("Skills" on a skill tree page).
function p.related( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
local out = {}
for _, rel in ipairs( t.related or {} ) do
local other = schema.types[rel.type]
local field
for _, f in ipairs( other.fields ) do
if f.name == rel.field then
field = f
end
end
local rows
if field.list then
-- List values live in a helper table (<table>__<field>); HOLDS has the same
-- alias problem as the default order, so join the helper table directly.
local helper = other.table .. '__' .. rel.field
rows = mw.ext.cargo.query( other.table .. ',' .. helper, other.table .. '.wiki_page=wiki_page', {
join = other.table .. '._ID=' .. helper .. '._rowID',
where = helper .. '._value=' .. quote( page ),
orderBy = other.table .. '.wiki_page',
limit = 500,
} )
else
rows = mw.ext.cargo.query( other.table, 'wiki_page', {
where = rel.field .. '=' .. quote( page ),
orderBy = 'wiki_page',
limit = 500,
} )
end
if #rows > 0 then
out[#out + 1] = '== ' .. rel.label .. ' =='
for _, row in ipairs( rows ) do
out[#out + 1] = '* ' .. pageLink( row.wiki_page )
end
end
end
-- List fields shown as their own section rather than in the infobox.
local own
for _, f in ipairs( t.fields ) do
if f.section then
own = own or fetchRow( t, page )
local items = own and own[f.name] and own[f.name] ~= '' and split( own[f.name] ) or {}
if #items > 0 then
out[#out + 1] = '== ' .. f.section .. ' =='
for _, item in ipairs( items ) do
out[#out + 1] = '* ' .. ( f.link and pageLink( item ) or item )
end
end
end
end
return frame:preprocess( table.concat( out, '\n' ) )
end
-- {{#invoke:GameData|text|type=Ability|field=description}}: text written in the
-- game itself, quoted as players see it.
function p.text( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local row = fetchRow( t, page )
local value = row and row[frame.args.field or 'description']
if not value or value == '' then
return ''
end
local label = frame.args.label or t.text_label or 'In-game text'
return frame:preprocess( '<blockquote class="game-text">' .. colorize( value ) .. "<br><small>''" .. label .. "''</small></blockquote>" )
end
-- {{#invoke:GameData|intro|type=Skill}}: the type's intro sentence from the schema,
-- with {field} placeholders filled from the current data.
function p.intro( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local row = fetchRow( t, page )
if not row or not t.intro then
return ''
end
local fields = {}
for _, f in ipairs( t.fields ) do
fields[f.name] = f
end
local text = t.intro:gsub( '{([%w_]+)}', function ( name )
local value = fields[name] and formatValue( fields[name], row[name], row )
if value and name == 'name' then
value = nameText( value )
end
return value and ( value:gsub( '%%', '%%%%' ) ) or ''
end )
return frame:preprocess( text )
end
-- Gallery images fit a square box of this many pixels, scaled by a whole number (up by
-- the largest factor that fits, or down by the smallest divisor that fits), so pixel art
-- keeps square pixels.
local GALLERY_BOX = 96
local function galleryImage( file, page )
local title = mw.title.new( 'File:' .. file )
-- Reading a file's size is an "expensive" call, limited per page; past the limit the
-- image is fitted to the box by the browser instead.
local ok, info = pcall( function () return title and title.file end )
local width, height = ok and info and info.width, ok and info and info.height
if not width or width == 0 or height == 0 then
return '[[File:' .. file .. '|' .. GALLERY_BOX .. 'x' .. GALLERY_BOX .. 'px|link=' .. page .. ']]'
end
local side = math.max( width, height )
if side <= GALLERY_BOX then
local factor = math.floor( GALLERY_BOX / side )
width, height = width * factor, height * factor
else
local divisor = math.ceil( side / GALLERY_BOX )
width, height = math.floor( width / divisor ), math.floor( height / divisor )
end
return '<span class="gd-gallery-fit" style="width:' .. width .. 'px;height:' .. height .. 'px">[[File:'
.. file .. '|link=' .. page .. ']]</span>'
end
-- {{#invoke:GameData|gallery|type=Class}}: every object of a type as a grid of icons.
function p.gallery( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local rows = mw.ext.cargo.query( t.table, 'wiki_page,icon', { orderBy = 'wiki_page', limit = 500 } )
local out = { '<div class="gd-gallery">' }
for _, row in ipairs( rows ) do
local image = ''
if row.icon and row.icon ~= '' then
image = '<div class="gd-gallery-image">' .. galleryImage( row.icon, row.wiki_page ) .. '</div>'
end
out[#out + 1] = '<div class="gd-gallery-item">' .. image .. pageLink( row.wiki_page ) .. '</div>'
end
out[#out + 1] = '</div>'
return frame:preprocess( table.concat( out, '\n' ) )
end
local function rowsFor( tableName, fields, page )
return mw.ext.cargo.query( tableName, fields, {
where = 'wiki_page=' .. quote( page ),
orderBy = 'ordinal',
limit = 500,
} )
end
-- Puts a value range into a modifier's wording: "+{0}% to All Resistances" with "4-6"
-- gives "+(4-6)% to All Resistances".
local function withValue( text, value )
value = tostring( value or '' )
local shown = value:find( '-', 2, true ) and ( '(' .. value .. ')' ) or value
return ( text:gsub( '{0}', ( shown:gsub( '%%', '%%%%' ) ) ) )
end
-- {{Modifier tiers|type=Modifier}}: the modifier's tiers for each slot or crystal color,
-- as the in-game Mod Dictionary shows them (T1 is the best).
function p.tiers( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local row = fetchRow( t, page )
local tiers = rowsFor( 'ModifierTier', 'applies_to,tier,value,level,weight', page )
if not row or #tiers == 0 then
return ''
end
local out = { '== Tiers ==', '{| class="wikitable gd-tiers"', '! Tier !! Effect !! Required item level' }
local current
for _, tier in ipairs( tiers ) do
if tier.applies_to ~= current then
current = tier.applies_to
out[#out + 1] = '|-\n! colspan="3" | ' .. current .. ' (weight ' .. ( tier.weight or '' ) .. ')'
end
out[#out + 1] = '|-\n| T' .. tier.tier .. ' || ' .. colorize( withValue( row.text or '', tier.value ) ) .. ' || ' .. ( tier.level or '' )
end
out[#out + 1] = '|}'
return frame:preprocess( table.concat( out, '\n' ) )
end
-- {{Unique modifiers|type=UniqueItem}}: a unique's fixed modifiers, with their value
-- range on each difficulty.
function p.uniquemods( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local mods = rowsFor( 'UniqueModifier', 'text,implicit,mild,spicy,inferno,nightmare', page )
if #mods == 0 then
return ''
end
local out = { '== Modifiers ==', '{| class="wikitable gd-unique-mods"', '! Modifier !! Mild !! Spicy !! Inferno !! Nightmare' }
for _, mod in ipairs( mods ) do
-- The wording's slot shows the whole range across the difficulty columns.
local low, high
for _, value in ipairs( { mod.mild, mod.spicy, mod.inferno, mod.nightmare } ) do
for number in tostring( value or '' ):gmatch( '%-?%d+%.?%d*' ) do
number = tonumber( number )
if number then
low = low and math.min( low, number ) or number
high = high and math.max( high, number ) or number
end
end
end
local range = low and ( low == high and tostring( low ) or ( low .. '-' .. high ) ) or '#'
local text = colorize( withValue( mod.text or '', range ) )
if mod.implicit == '1' then
text = text .. " <small>''(implicit)''</small>"
end
out[#out + 1] = '|-\n| ' .. text .. ' || ' .. ( mod.mild or '' ) .. ' || ' .. ( mod.spicy or '' )
.. ' || ' .. ( mod.inferno or '' ) .. ' || ' .. ( mod.nightmare or '' )
end
out[#out + 1] = '|}'
return frame:preprocess( table.concat( out, '\n' ) )
end
-- {{Skill item modifiers|type=Skill}}: the item modifiers that add levels to this skill.
function p.skillmods( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local mods = rowsFor( 'SkillItemModifier', 'slot,affix,tiers,weight', page )
if #mods == 0 then
return ''
end
local out = { '== Rolls on items ==', '{| class="wikitable"', '! Slot !! Affix !! Skill levels by tier !! Weight' }
for _, mod in ipairs( mods ) do
out[#out + 1] = '|-\n| ' .. ( mod.slot or '' ) .. ' || ' .. ( mod.affix or '' ) .. ' || ' .. ( mod.tiers or '' ) .. ' || ' .. ( mod.weight or '' )
end
out[#out + 1] = '|}'
return frame:preprocess( table.concat( out, '\n' ) )
end
-- {{Craft offers|type=Craft}}: every place that offers a craft, with its base costs.
function p.craftoffers( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local offers = rowsFor( 'CraftOffer',
'offered_by,location,description,gold,essence,crafting_tokens,ultimate_tokens,chaos_added,success_rate,rare_offer', page )
if #offers == 0 then
return ''
end
local out = { '== Where to craft it ==',
"''Base costs: the game raises them with the item's Chaos and level.''",
'{| class="wikitable"', '! Offered by !! Where !! Cost !! Chaos added !! Success rate !! Effect' }
for _, offer in ipairs( offers ) do
local cost = {}
for _, part in ipairs( { { 'gold', 'Gold' }, { 'essence', 'Nightmare Essence' },
{ 'crafting_tokens', 'Crafting Tokens' }, { 'ultimate_tokens', 'Ultimate Tokens' } } ) do
if offer[part[1]] and offer[part[1]] ~= '' and offer[part[1]] ~= '0' then
cost[#cost + 1] = offer[part[1]] .. ' ' .. part[2]
end
end
local who = offer.offered_by .. ( offer.rare_offer == '1' and " <small>''(rare offer)''</small>" or '' )
out[#out + 1] = '|-\n| ' .. who .. ' || ' .. ( offer.location or '' ) .. ' || ' .. ( #cost > 0 and table.concat( cost, ', ' ) or 'Free' )
.. ' || ' .. ( offer.chaos_added or '0' ) .. ' || ' .. ( offer.success_rate or '' ) .. '% || ' .. colorize( offer.description or '' )
end
out[#out + 1] = '|}'
return frame:preprocess( table.concat( out, '\n' ) )
end
-- One cell of a table view: links to pages that exist, game keyword colors on request.
local function cell( column, def, value )
if value == nil or value == '' then
return ''
end
if def and def.type == 'Boolean' then
return ( value == '1' or value == 1 ) and 'Yes' or ''
end
value = tostring( value )
if column.colorize then
value = colorize( value )
end
local link = column.link or ( def and def.link )
if ( link or column.field == 'wiki_page' ) and mw.title.new( value ) and mw.title.new( value ).exists then
value = pageLink( value )
end
return value .. ( column.suffix or '' )
end
-- {{Data table|type=Monster|view=MonsterSpawn}}: a row table as declared in the type's
-- blocks. A view shows the page's own rows, or with "match" the rows of other pages
-- whose given field names this page. "group" starts a header row whenever its value changes.
function p.datatable( frame )
local t = schema.types[frame.args.type]
local v = t and t.views and t.views[frame.args.view]
if not v then
return ''
end
local page = currentPage( frame )
dependOnData( t, page )
local defs = {}
for _, f in ipairs( schema.tables[v.table] or {} ) do
defs[f.name] = f
end
local names, seen = {}, {}
local function want( name )
if name and not seen[name] then
seen[name] = true
names[#names + 1] = name
end
end
for _, c in ipairs( v.columns ) do
want( c.field )
end
want( v.group )
if v.gallery then
want( v.gallery.image )
want( v.gallery.caption )
end
local match = v.match or 'wiki_page'
local rows = mw.ext.cargo.query( v.table, table.concat( names, ',' ), {
where = match .. '=' .. quote( page ),
orderBy = v.order or ( match == 'wiki_page' and 'ordinal' or 'wiki_page,ordinal' ),
limit = 5000,
} )
if #rows == 0 then
return ''
end
local out = { '== ' .. v.heading .. ' ==' }
if v.note then
out[#out + 1] = "''" .. v.note .. "''"
end
-- A view with "gallery" shows its rows as an image gallery: the file and caption fields.
if v.gallery then
local lines = {}
for _, row in ipairs( rows ) do
lines[#lines + 1] = 'File:' .. row[v.gallery.image] .. '|' .. ( v.gallery.caption_prefix or '' ) .. ( row[v.gallery.caption] or '' )
end
out[#out + 1] = frame:extensionTag( 'gallery', table.concat( lines, '\n' ),
{ mode = 'traditional', widths = v.gallery.width, heights = v.gallery.height } )
return frame:preprocess( table.concat( out, '\n' ) )
end
local header = {}
for _, c in ipairs( v.columns ) do
header[#header + 1] = c.label
end
-- Tables with many columns start below the infobox, so they get the page's full width.
-- (Counted from header: "#" doesn't work on tables from mw.loadData.)
local wide = #header >= 7
if wide then
table.insert( out, 1, '<div class="gd-wide">' )
end
out[#out + 1] = '{| class="wikitable' .. ( v.group and '' or ' sortable' ) .. '"'
out[#out + 1] = '! ' .. table.concat( header, ' !! ' )
local current
for _, row in ipairs( rows ) do
if v.group and row[v.group] ~= current then
current = row[v.group]
out[#out + 1] = '|-\n! colspan="' .. #header .. '" | ' .. cell( { field = v.group }, defs[v.group], current )
end
local cells = {}
for _, c in ipairs( v.columns ) do
cells[#cells + 1] = cell( c, defs[c.field], row[c.field] )
end
out[#out + 1] = '|-\n| ' .. table.concat( cells, ' || ' )
end
out[#out + 1] = '|}'
if wide then
out[#out + 1] = '</div>'
end
return frame:preprocess( table.concat( out, '\n' ) )
end
-- {{#invoke:GameData|skillmods_all}}: every "+N to <skill>" item modifier, by slot.
function p.skillmods_all( frame )
local mods = mw.ext.cargo.query( 'SkillItemModifier', 'wiki_page,slot,affix,tiers', {
orderBy = 'slot,wiki_page',
limit = 5000,
} )
local out, current = {}, nil
for _, mod in ipairs( mods ) do
if mod.slot ~= current then
if current then
out[#out + 1] = '|}'
end
current = mod.slot
out[#out + 1] = '== ' .. current .. ' =='
out[#out + 1] = '{| class="wikitable sortable"\n! Skill !! Affix !! Skill levels by tier'
end
out[#out + 1] = '|-\n| ' .. pageLink( mod.wiki_page ) .. ' || ' .. ( mod.affix or '' ) .. ' || ' .. ( mod.tiers or '' )
end
if current then
out[#out + 1] = '|}'
end
return frame:preprocess( table.concat( out, '\n' ) )
end
-- Used by the "<Label> data" templates: shows the stored record on its Data: page.
function p.record( frame )
local t = schema.types[frame.args.type]
if not t then
return ''
end
local args = frame:getParent().args
local out = { '{| class="wikitable"' }
for _, f in ipairs( t.fields ) do
local value = args[f.name]
if value and value ~= '' then
out[#out + 1] = '|-\n! ' .. f.name .. '\n| ' .. ( formatValue( f, value, args ) or '' )
end
end
out[#out + 1] = '|}'
if args.wiki_page and args.wiki_page ~= '' then
out[#out + 1] = 'Article: ' .. pageLink( args.wiki_page )
end
return frame:preprocess( table.concat( out, '\n' ) )
end
return p