Module:GearSet: Difference between revisions
No edit summary |
No edit summary |
||
| Line 565: | Line 565: | ||
local function extractDropMobs(dropsfrom) | local function extractDropMobs(dropsfrom) | ||
local drops = | local drops = tostring(dropsfrom or '') | ||
drops = drops:gsub('\r\n', '\n') | drops = drops:gsub('\r\n', '\n') | ||
| Line 572: | Line 571: | ||
drops = drops:gsub('<!%-%-.-%-%->', '') | drops = drops:gsub('<!%-%-.-%-%->', '') | ||
local | local allDrops = {} | ||
local seen = {} | local seen = {} | ||
-- | -- Collect unique bullet-list entries only. | ||
for line in drops:gmatch('[^\n]+') do | for line in drops:gmatch('[^\n]+') do | ||
local mob = | local mob = line:match( | ||
'^%s*%*+%s*(.-)%s*$' | |||
) | |||
if mob | if mob | ||
| Line 588: | Line 585: | ||
then | then | ||
seen[mob] = true | seen[mob] = true | ||
table.insert(allDrops, mob) | |||
end | |||
end | |||
local displayedDrops = {} | |||
table.insert( | -- Show no more than the first two entries. | ||
for index = 1, math.min(2, #allDrops) do | |||
table.insert( | |||
displayedDrops, | |||
allDrops[index] | |||
) | |||
end | |||
-- A third line indicates additional drop sources. | |||
if #allDrops > 2 then | |||
table.insert( | |||
displayedDrops, | |||
"''and more''" | |||
) | |||
end | end | ||
return table.concat( | return table.concat( | ||
displayedDrops, | |||
'<br>' | |||
) | |||
end | end | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
-- Item loading and hover-link rendering | -- Item loading and hover-link rendering | ||
Revision as of 01:43, 14 July 2026
Documentation for this module may be created at Module:GearSet/doc
local p = {}
------------------------------------------------------------------------
-- Table columns
------------------------------------------------------------------------
local NUMERIC_COLUMNS = {
{
key = 'ac',
header = 'AC',
labels = { 'AC' },
},
{
key = 'hp',
header = 'HP',
labels = { 'HP' },
},
{
key = 'mana',
header = 'MANA',
labels = { 'MANA' },
},
{
key = 'endurance',
header = 'END',
labels = {
'ENDURANCE',
'ENDUR',
'END',
},
},
{
key = 'str',
header = 'STR',
labels = { 'STR' },
},
{
key = 'sta',
header = 'STA',
labels = { 'STA' },
},
{
key = 'int',
header = 'INT',
labels = { 'INT' },
},
{
key = 'wis',
header = 'WIS',
labels = { 'WIS' },
},
{
key = 'agi',
header = 'AGI',
labels = { 'AGI' },
},
{
key = 'dex',
header = 'DEX',
labels = { 'DEX' },
},
{
key = 'cha',
header = 'CHA',
labels = { 'CHA' },
},
{
key = 'mr',
header = 'MR',
labels = {
'SV MAGIC',
'SV MAG',
'MR',
},
},
{
key = 'fr',
header = 'FR',
labels = {
'SV FIRE',
'FR',
},
},
{
key = 'cr',
header = 'CR',
labels = {
'SV COLD',
'CR',
},
},
{
key = 'dr',
header = 'DR',
labels = {
'SV DISEASE',
'SV DIS',
'DR',
},
},
{
key = 'pr',
header = 'PR',
labels = {
'SV POISON',
'SV POI',
'PR',
},
},
{
key = 'vr',
header = 'VR',
labels = {
'SV VR',
'VR',
},
},
}
-- Item, Slot, numeric columns, Weight, Effect, Dropped By.
local TOTAL_COLUMN_COUNT =
2 +
#NUMERIC_COLUMNS +
3
------------------------------------------------------------------------
-- General helpers
------------------------------------------------------------------------
local function trim(value)
return mw.text.trim(tostring(value or ''))
end
local function escapePattern(value)
return (
tostring(value or ''):gsub(
'([%^%$%(%)%%%.%[%]%*%+%-%?])',
'%%%1'
)
)
end
local function formatPlainNumber(value)
if value == nil then
return ''
end
if math.floor(value) == value then
return tostring(math.floor(value))
end
local formatted = string.format('%.2f', value)
formatted = formatted:gsub('0+$', '')
formatted = formatted:gsub('%.$', '')
return formatted
end
local function formatWeight(value)
if value == nil then
return ''
end
local formatted = string.format('%.2f', value)
formatted = formatted:gsub('0+$', '')
formatted = formatted:gsub('%.$', '')
-- Preserve at least one decimal place for whole-number weights.
-- The plain-string search must look for "." rather than "%.".
if not formatted:find('.', 1, true) then
formatted = formatted .. '.0'
end
return formatted
end
------------------------------------------------------------------------
-- Itempage template parser
------------------------------------------------------------------------
-- Reads the parameters inside the first {{Itempage ...}} invocation.
-- Nested templates, links, and wiki tables are accounted for so their
-- internal pipes are not mistaken for Itempage parameter separators.
local function parseItempageParameters(content)
local startPosition =
content:find('{{%s*[Ii]tempage[%s|]')
if not startPosition then
return nil
end
local segments = {}
local buffer = {}
local curlyDepth = 1
local squareDepth = 0
local tableDepth = 0
local position = startPosition + 2
local contentLength = #content
while position <= contentLength do
local pair =
content:sub(position, position + 1)
local character =
content:sub(position, position)
if pair == '{{' then
curlyDepth = curlyDepth + 1
table.insert(buffer, pair)
position = position + 2
elseif pair == '}}' then
if curlyDepth == 1 then
table.insert(
segments,
table.concat(buffer)
)
break
end
curlyDepth = curlyDepth - 1
table.insert(buffer, pair)
position = position + 2
elseif pair == '[[' then
squareDepth = squareDepth + 1
table.insert(buffer, pair)
position = position + 2
elseif pair == ']]' and squareDepth > 0 then
squareDepth = squareDepth - 1
table.insert(buffer, pair)
position = position + 2
elseif pair == '{|' then
tableDepth = tableDepth + 1
table.insert(buffer, pair)
position = position + 2
elseif pair == '|}' and tableDepth > 0 then
tableDepth = tableDepth - 1
table.insert(buffer, pair)
position = position + 2
elseif character == '|'
and curlyDepth == 1
and squareDepth == 0
and tableDepth == 0
then
table.insert(
segments,
table.concat(buffer)
)
buffer = {}
position = position + 1
else
table.insert(buffer, character)
position = position + 1
end
end
if #segments < 2 then
return nil
end
local parameters = {}
-- The first segment is the template name.
for index = 2, #segments do
local parameterName, parameterValue =
segments[index]:match(
'^%s*([%w_%-]+)%s*=(.*)$'
)
if parameterName then
parameters[parameterName:lower()] =
trim(parameterValue)
end
end
return parameters
end
------------------------------------------------------------------------
-- Page loading
------------------------------------------------------------------------
local function getPageContent(pageName)
local title = mw.title.new(pageName)
-- Follow ordinary redirects.
for _ = 1, 4 do
if not title then
return nil
end
local content = title:getContent()
if not content then
return nil
end
local redirectTarget =
content:match(
'^%s*#[Rr][Ee][Dd][Ii][Rr][Ee][Cc][Tt]' ..
'%s*%[%[([^%]|#]+)'
)
if not redirectTarget then
return content
end
title = mw.title.new(
trim(redirectTarget)
)
end
return nil
end
------------------------------------------------------------------------
-- Statsblock parsing
------------------------------------------------------------------------
local function normalizeStatsblock(statsblock)
local stats = tostring(statsblock or '')
stats = stats:gsub('\r\n', '\n')
stats = stats:gsub('\r', '\n')
-- Remove comments.
stats = stats:gsub('<!%-%-.-%-%->', '')
-- Convert every common form of <br> into a newline.
stats = stats:gsub(
'<[bB][rR]%s*/?%s*>',
'\n'
)
stats = stats:gsub(' ', ' ')
stats = stats:gsub('[ \t]+\n', '\n')
return stats
end
local function extractNumericValue(
statsblock,
labels,
allowDecimal
)
local uppercaseStats =
tostring(statsblock or ''):upper()
local valuePattern
if allowDecimal then
valuePattern =
'([%+%-]?%d+%.?%d*)'
else
valuePattern =
'([%+%-]?%d+)'
end
for _, label in ipairs(labels) do
local pattern =
escapePattern(label:upper()) ..
'%s*:%s*' ..
valuePattern
local value =
uppercaseStats:match(pattern)
if value then
return tonumber(value)
end
end
return nil
end
-- Returns all complete line values belonging to the supplied labels.
--
-- For:
-- Effect: [[Swift Spirit]] (Worn)
--
-- this returns:
-- [[Swift Spirit]] (Worn)
local function extractLabeledLines(
statsblock,
labels
)
local results = {}
local seen = {}
for line in tostring(statsblock or ''):gmatch('[^\n]+') do
local cleanLine = trim(line)
local uppercaseLine = cleanLine:upper()
for _, label in ipairs(labels) do
local pattern =
'^' ..
escapePattern(label:upper()) ..
'%s*:%s*'
local _, valueStart =
uppercaseLine:find(pattern)
if valueStart then
local value =
trim(
cleanLine:sub(
valueStart + 1
)
)
if value ~= ''
and not seen[value]
then
seen[value] = true
table.insert(
results,
value
)
end
break
end
end
end
return results
end
local function extractLine(statsblock, labels)
local values =
extractLabeledLines(
statsblock,
labels
)
return values[1] or ''
end
------------------------------------------------------------------------
-- Item-effect extraction
------------------------------------------------------------------------
local function buildEffect(
parameters,
statsblock
)
local results = {}
local seen = {}
local function addEffect(value)
value = trim(value)
if value == ''
or seen[value]
then
return
end
seen[value] = true
table.insert(results, value)
end
--------------------------------------------------------------------
-- Explicit effect lines
--------------------------------------------------------------------
local explicitEffects =
extractLabeledLines(
statsblock,
{
'Effect',
'Click Effect',
'Worn Effect',
'Proc Effect',
'Combat Effect',
}
)
for _, effect in ipairs(explicitEffects) do
addEffect(effect)
end
--------------------------------------------------------------------
-- Focus effect stored as a separate Itempage parameter
--------------------------------------------------------------------
local focusEffect =
trim(parameters.focus_effect)
if focusEffect ~= '' then
if focusEffect:find('%[%[') then
addEffect(
'Focus: ' .. focusEffect
)
else
addEffect(
'Focus: [[' ..
focusEffect ..
']]'
)
end
end
--------------------------------------------------------------------
-- Passive effects represented as statsblock lines
--------------------------------------------------------------------
local passiveEffectLabels = {
'Haste',
'HP Regen',
'Mana Regen',
'Endurance Regen',
}
for _, label in ipairs(passiveEffectLabels) do
local values =
extractLabeledLines(
statsblock,
{ label }
)
for _, value in ipairs(values) do
addEffect(
label .. ': ' .. value
)
end
end
return table.concat(results, '<br>')
end
------------------------------------------------------------------------
-- Drop-source extraction
------------------------------------------------------------------------
local function extractDropMobs(dropsfrom)
local drops = tostring(dropsfrom or '')
drops = drops:gsub('\r\n', '\n')
drops = drops:gsub('\r', '\n')
drops = drops:gsub('<!%-%-.-%-%->', '')
local allDrops = {}
local seen = {}
-- Collect unique bullet-list entries only.
for line in drops:gmatch('[^\n]+') do
local mob = line:match(
'^%s*%*+%s*(.-)%s*$'
)
if mob
and mob ~= ''
and not seen[mob]
then
seen[mob] = true
table.insert(allDrops, mob)
end
end
local displayedDrops = {}
-- Show no more than the first two entries.
for index = 1, math.min(2, #allDrops) do
table.insert(
displayedDrops,
allDrops[index]
)
end
-- A third line indicates additional drop sources.
if #allDrops > 2 then
table.insert(
displayedDrops,
"''and more''"
)
end
return table.concat(
displayedDrops,
'<br>'
)
end
------------------------------------------------------------------------
-- Item loading and hover-link rendering
------------------------------------------------------------------------
local function createHoverLink(
frame,
pageName
)
local success, rendered =
pcall(
function()
return frame:preprocess(
'{{:' .. pageName .. '}}'
)
end
)
if success
and trim(rendered) ~= ''
then
return rendered
end
return '[[' .. pageName .. ']]'
end
local function readItem(
frame,
pageName
)
local content =
getPageContent(pageName)
if not content then
return nil,
'Item page does not exist or could not be read.'
end
local parameters =
parseItempageParameters(content)
if not parameters then
return nil,
'No Itempage template was found on the item page.'
end
local statsblock =
normalizeStatsblock(
parameters.statsblock
)
if statsblock == '' then
return nil,
'The item has no statsblock.'
end
local item = {
pageName = pageName,
itemName =
trim(parameters.itemname) ~= ''
and trim(parameters.itemname)
or pageName,
hover =
createHoverLink(
frame,
pageName
),
slot =
extractLine(
statsblock,
{ 'Slot' }
),
weight =
extractNumericValue(
statsblock,
{
'WT',
'Weight',
},
true
),
effect =
buildEffect(
parameters,
statsblock
),
droppedBy =
extractDropMobs(
parameters.dropsfrom
),
}
for _, column in ipairs(NUMERIC_COLUMNS) do
item[column.key] =
extractNumericValue(
statsblock,
column.labels,
false
)
end
return item
end
------------------------------------------------------------------------
-- HTML helpers
------------------------------------------------------------------------
local function addCell(
row,
value,
alignLeft
)
local cell = row:tag('td')
if alignLeft then
cell:css(
'text-align',
'left'
)
end
if value ~= nil
and value ~= ''
then
cell:wikitext(
tostring(value)
)
end
return cell
end
local function addNumericCell(
row,
numericValue,
displayedValue
)
local cell = row:tag('td')
if numericValue ~= nil then
cell:attr(
'data-sort-value',
tostring(numericValue)
)
cell:wikitext(
displayedValue
or formatPlainNumber(
numericValue
)
)
end
return cell
end
local function addHeader(row, text)
return row
:tag('th')
:attr('scope', 'col')
:wikitext(text)
end
------------------------------------------------------------------------
-- Table rendering
------------------------------------------------------------------------
local function renderTable(
frame,
args,
itemNames
)
local output =
mw.html.create('table')
output
:addClass('wikitable')
:addClass('sortable')
:addClass('eql-gear-set-table')
:css('text-align', 'center')
--------------------------------------------------------------------
-- Caption
--------------------------------------------------------------------
local caption = trim(args.caption)
if caption == '' then
caption = trim(args.name)
end
if caption == '' then
caption =
mw.title.getCurrentTitle().text
end
output
:tag('caption')
:wikitext(caption)
--------------------------------------------------------------------
-- Column headings
--------------------------------------------------------------------
local headerRow = output:tag('tr')
addHeader(headerRow, 'Item')
addHeader(headerRow, 'Slot')
for _, column in ipairs(NUMERIC_COLUMNS) do
addHeader(
headerRow,
column.header
)
end
addHeader(headerRow, 'Weight')
addHeader(headerRow, 'Effect')
addHeader(headerRow, 'Dropped By')
--------------------------------------------------------------------
-- Totals
--------------------------------------------------------------------
local totals = {}
for _, column in ipairs(NUMERIC_COLUMNS) do
totals[column.key] = 0
end
local totalWeight = 0
--------------------------------------------------------------------
-- Item rows
--------------------------------------------------------------------
for _, pageName in ipairs(itemNames) do
local item, errorMessage =
readItem(
frame,
pageName
)
if not item then
local errorRow =
output:tag('tr')
errorRow
:tag('td')
:attr(
'colspan',
TOTAL_COLUMN_COUNT
)
:addClass('error')
:css(
'text-align',
'left'
)
:wikitext(
'[[' ..
pageName ..
']]: ' ..
errorMessage
)
else
local row = output:tag('tr')
addCell(
row,
item.hover,
true
)
addCell(
row,
item.slot,
true
)
for _, column in ipairs(NUMERIC_COLUMNS) do
local value =
item[column.key]
addNumericCell(
row,
value
)
if value ~= nil then
totals[column.key] =
totals[column.key] +
value
end
end
addNumericCell(
row,
item.weight,
formatWeight(item.weight)
)
if item.weight ~= nil then
totalWeight =
totalWeight +
item.weight
end
addCell(
row,
item.effect,
true
)
addCell(
row,
item.droppedBy,
true
)
end
end
--------------------------------------------------------------------
-- Totals row
--------------------------------------------------------------------
local totalRow = output:tag('tr')
totalRow
:addClass('sortbottom')
:css('font-weight', 'bold')
totalRow
:tag('td')
:attr('colspan', 2)
:css('text-align', 'right')
:wikitext('Totals')
for _, column in ipairs(NUMERIC_COLUMNS) do
addNumericCell(
totalRow,
totals[column.key]
)
end
addNumericCell(
totalRow,
totalWeight,
formatWeight(totalWeight)
)
addCell(totalRow, '', false)
addCell(totalRow, '', false)
return tostring(output)
end
------------------------------------------------------------------------
-- Automatic category metadata
------------------------------------------------------------------------
local function splitMetadata(value)
local values = {}
for part in tostring(value or ''):gmatch(
'[^,;]+'
) do
part = trim(part)
if part ~= '' then
table.insert(values, part)
end
end
return values
end
local function addCategory(
categories,
seen,
categoryName
)
categoryName = trim(categoryName)
if categoryName == ''
or seen[categoryName]
then
return
end
seen[categoryName] = true
table.insert(
categories,
'[[Category:' ..
categoryName ..
']]'
)
end
local function addGearSetCategories(
categories,
seen,
value
)
for _, entry in ipairs(
splitMetadata(value)
) do
addCategory(
categories,
seen,
entry .. ' Gear Sets'
)
end
end
local function buildCategories(args)
local categories = {}
local seen = {}
addCategory(
categories,
seen,
'Gear Sets'
)
local setType =
trim(args.type):lower()
if setType == 'weapon'
or setType == 'weapons'
then
addCategory(
categories,
seen,
'Weapon Sets'
)
else
addCategory(
categories,
seen,
'Armor Sets'
)
end
addGearSetCategories(
categories,
seen,
args.era
)
addGearSetCategories(
categories,
seen,
args.source
)
addGearSetCategories(
categories,
seen,
args.armor
)
addGearSetCategories(
categories,
seen,
args.class
)
addGearSetCategories(
categories,
seen,
args.region
)
addGearSetCategories(
categories,
seen,
args.craft
)
addGearSetCategories(
categories,
seen,
args.series
)
local cultures =
splitMetadata(args.culture)
if #cultures > 0 then
addCategory(
categories,
seen,
'Cultural Gear Sets'
)
for _, culture in ipairs(cultures) do
addCategory(
categories,
seen,
culture ..
' Cultural Gear Sets'
)
end
end
-- Direct additional category names.
for _, categoryName in ipairs(
splitMetadata(args.categories)
) do
addCategory(
categories,
seen,
categoryName
)
end
return table.concat(categories)
end
------------------------------------------------------------------------
-- Module entry point
------------------------------------------------------------------------
function p.main(frame)
local parent = frame:getParent()
local args =
parent and parent.args
or frame.args
local indexedItems = {}
for argumentName, argumentValue
in pairs(args)
do
local numericIndex =
tonumber(argumentName)
local itemName =
trim(argumentValue)
if numericIndex
and itemName ~= ''
then
table.insert(
indexedItems,
{
index = numericIndex,
name = itemName,
}
)
end
end
table.sort(
indexedItems,
function(left, right)
return left.index < right.index
end
)
local itemNames = {}
for _, item in ipairs(indexedItems) do
table.insert(
itemNames,
item.name
)
end
if #itemNames == 0 then
return
'<strong class="error">' ..
'Gear Set requires at least one item page.' ..
'</strong>'
end
return
renderTable(
frame,
args,
itemNames
) ..
buildCategories(args)
end
return p