From 3f7f479ecc82e5dd3f4841e5c38c60f027280602 Mon Sep 17 00:00:00 2001 From: hjpalpha Date: Sun, 17 May 2026 18:18:15 +0200 Subject: [PATCH 1/3] feat: Add sc2 UnitStatisticsOverviewTable --- .../UnitStatisticsOverviewTable.lua | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua diff --git a/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua b/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua new file mode 100644 index 00000000000..04ff444f615 --- /dev/null +++ b/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua @@ -0,0 +1,298 @@ +--- +-- @Liquipedia +-- page=Module:UnitStatisticsOverviewTable +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Arguments = Lua.import('Module:Arguments') +local Array = Lua.import('Module:Array') +local Faction = Lua.import('Module:Faction') +local FnUtil = Lua.import('Module:FnUtil') +local Game = Lua.import('Module:Game') +local String = Lua.import('Module:StringUtils') +local Tabs = Lua.import('Module:Tabs') + +local Condition = Lua.import('Module:Condition') +local ConditionTree = Condition.Tree +local ConditionNode = Condition.Node +local Comparator = Condition.Comparator +local BooleanOperator = Condition.BooleanOperator +local ColumnName = Condition.ColumnName + +local Html = require('Module:Widget/Html') +local Image = Lua.import('Module:Widget/Image/Icon/Image') +local Link = Lua.import('Module:Widget/Basic/Link') +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local WidgetUtil = Lua.import('Module:Widget/Util') + +local SUPPLY = Lua.import('Module:Supply', {loadData = true}) +local GAS = Lua.import('Module:Gas', {loadData = true}) +local BUILDTIME = Lua.import('Module:Buildtime', {loadData = true}) +local MINERALS = Image{ + imageLight = 'Minerals.gif', + size = '18', + link = 'Minerals', + verticalAlignment = 'baseline', +} +local ARMOR = Image{ + imageLight = 'Icon Armor.png', + size = '18', + link = 'Armor', +} +local HELATH = Image{ + imageLight = 'Icon Hitpoints.png', + size = '18', + link = '', +} +local SHIELD = Image{ + imageLight = 'Icon Shields.png', + size = '18', + link = 'Plasma Shield', +} +local DPS = Html.Abbr{title = 'Damage', children = Link{link = 'DPS'}} + +local PROTOSS = Faction.read('protoss') +local EXCLUDE_PAGE_STRINGS = { + 'campaign', + 'coop', + 'terran_minor_units', + 'cut_features', +} +local EXCLUDE_NAME_STRINGS = { + 'floating', + 'burrowed', + 'cocoon', + 'warp prism deployed', + 'liberator defender mode', + 'siege tank siege mode', + 'thor high impact mode', + 'viking assault mode', + 'infested swarm egg', + 'uprooted', + 'primal', +} + +local UnitStats = {} + +---@param frame Frame +---@return VNode|Widget|string? +function UnitStats.wrapper(frame) + local args = Arguments.getArgs(frame) + local game = Game.name{game = args.game or 'lotv'} + assert(game, 'Invalid game: ' .. args.game) + + local tabArgs = {} + for index, faction in ipairs(Faction.coreFactions) do + tabArgs['name' .. index] = Faction.Icon{faction = faction} .. ' ' .. Faction.toName(faction) + tabArgs['content' .. index] = UnitStats._forFaction{game = game, faction = faction} + end + + return Tabs.dynamic(tabArgs) +end + +---@private +---@param args {game: string, faction: string} +---@return VNode? +function UnitStats._forFaction(args) + local units = UnitStats._queryUnits(args.game, args.faction) + + if not units or type(units[1]) ~= 'table' then + return + end + + --exclude some units + units = Array.filter(units, function(unitData) + local page = unitData.pagename:lower() + local name = unitData.name:lower() + return Array.all(EXCLUDE_PAGE_STRINGS, function(str) + return string.find(page, str) == nil + end) and Array.all(EXCLUDE_NAME_STRINGS, function(str) + return string.find(name, str) == nil + end) + end) + + return TableWidgets.Table{ + children = { + UnitStats._header(args.faction), + TableWidgets.TableBody{children = Array.flatMap(units, FnUtil.curry(UnitStats._row, args.game))}, + } + } +end + +---@private +---@param game string +---@param faction string +---@return table[] +function UnitStats._queryUnits(game, faction) + local lowercasedGame = game:lower():gsub(' ', '_') + + local conditions = ConditionTree(BooleanOperator.all):add{ + ConditionNode(ColumnName('type'), Comparator.eq, 'Unit'), + ConditionNode(ColumnName('race', 'extradata'), Comparator.eq, faction), + ConditionNode(ColumnName('iscampaignunit', 'extradata'), Comparator.eq, 'false'), + ConditionNode(ColumnName('deprecated', 'extradata'), Comparator.eq, 'false'), + ConditionNode(ColumnName('wasonlybeta', 'extradata'), Comparator.eq, 'false'), + } + + local units = mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = tostring(conditions), + order = 'name asc', + query = 'information, type, name, pagename, extradata', + limit = 5000, + }) + + return Array.filter(units, function(unit) + return unit.information == game + or String.contains(unit.pagename:lower(), lowercasedGame) + end) +end + +---@private +---@param faction string +---@return VNode +function UnitStats._header(faction) + local cells = WidgetUtil.collect( + TableWidgets.CellHeader{children = 'Name'}, + TableWidgets.CellHeader{children = SUPPLY[faction]}, + TableWidgets.CellHeader{children = MINERALS}, + TableWidgets.CellHeader{children = GAS[faction]}, + TableWidgets.CellHeader{children = WidgetUtil.collect( + BUILDTIME[faction], + faction == PROTOSS and { + ' / ', + Link{link = 'Warp Gate', children = 'WG'}, + } or nil + )}, + TableWidgets.CellHeader{children = 'Size'}, + TableWidgets.CellHeader{children = 'Cargo'}, + TableWidgets.CellHeader{children = ARMOR}, + TableWidgets.CellHeader{children = HELATH}, + faction == PROTOSS and TableWidgets.CellHeader{children = SHIELD} or nil, + TableWidgets.CellHeader{children = 'Attributes'}, + TableWidgets.CellHeader{children = Link{link = 'Speed'}}, + TableWidgets.CellHeader{children = Link{link = 'Sight'}}, + TableWidgets.CellHeader{children = 'Attack name'}, + TableWidgets.CellHeader{children = 'G. Attack'}, + TableWidgets.CellHeader{children = 'A. Attack'}, + TableWidgets.CellHeader{children = 'Bonus'}, + TableWidgets.CellHeader{children = {'G. ', DPS}}, + TableWidgets.CellHeader{children = {'A. ', DPS}}, + TableWidgets.CellHeader{children = {'Bonus ', DPS}}, + TableWidgets.CellHeader{children = Link{link = 'Cooldown'}}, + TableWidgets.CellHeader{children = Link{link = 'Range'}} + ) + + return TableWidgets.TableHeader{children = TableWidgets.Row{children = cells}} +end + +---@private +---@param game string +---@param unit table +---@return VNode +function UnitStats._row(game, unit) + local unitAttacks = UnitStats._queryUnitAttacks(game, unit.pagename) + unitAttacks[1] = unitAttacks[1] or {extradata = {}} + + return Array.map(unitAttacks, function(attack, index) + local cells = {} + if index == 1 then + cells = UnitStats._baseRow(game, unit, #unitAttacks > 1 and #unitAttacks or nil) + end + + local data = attack.extradata + + local target = (data.target or ''):lower() + local isVsBuildingsAttack = String.contains(target, 'buildings') + local isGroundAttack = isVsBuildingsAttack or String.contains(target, 'ground') + local isAirAttack = String.contains(target, 'air') + + local groundAttackDamage = isGroundAttack and data.damage or '-' + if isVsBuildingsAttack then + groundAttackDamage = groundAttackDamage .. ' vs Buildings' + end + + Array.appendWith(cells, + UnitStats._cell(attack.name, nil), + UnitStats._cell(groundAttackDamage),--ground attack + UnitStats._cell(isAirAttack and data.damage or nil),--air attack + UnitStats._cell(data.bonus),--bonus + UnitStats._cell(isGroundAttack and data.dps or nil),--ground DPS + UnitStats._cell(isAirAttack and data.dps or nil),--air DPS + UnitStats._cell(data.bonus_dps),--bonus DPS + UnitStats._cell(data.cooldown),--Cooldown + UnitStats._cell(data.range)--range + ) + + return TableWidgets.Row{children = cells} + end) +end + +---@private +---@param val VNode|Renderable? +---@param rowSpan integer? +---@return VNode +function UnitStats._cell(val, rowSpan) + return TableWidgets.Cell{rowspan = rowSpan, children = val or '-'} +end + +---@private +---@param game string +---@param unit table +---@param rowSpan integer? +---@return VNode[] +function UnitStats._baseRow(game, unit, rowSpan) + mw.logObject(rowSpan, 'rowSpan') + local extradata = unit.extradata or {} + + local name = unit.name + if unit.name == 'Liberator Fighter Mode' then + name = 'Liberator' + elseif unit.name == 'Siege Tank Tank Mode' then + name = 'Siege Tank' + elseif unit.name == 'Thor Explosive Payload' then + name = 'Thor' + elseif unit.name == 'Viking Fighter Mode' then + name = 'Viking' + end + + local minerals = extradata.minerals + local gas = extradata.gas + if unit.name == 'Archon' and extradata.minerals == 'varies' and extradata.gas == 'varies' then + minerals = Link{link = unit.pagename .. '#Cost', children = 'varies'} + gas = Link{link = unit.pagename .. '#Cost', children = 'varies'} + end + + return WidgetUtil.collect( + UnitStats._cell(Link{link = unit.pagename, children = name}, rowSpan), + UnitStats._cell(extradata.supply, rowSpan), + UnitStats._cell(minerals, rowSpan), + UnitStats._cell(gas, rowSpan), + UnitStats._cell(extradata.buildtime, rowSpan), + UnitStats._cell(extradata.size, rowSpan), + UnitStats._cell(extradata.cargo_size, rowSpan), + UnitStats._cell(extradata.armor, rowSpan), + UnitStats._cell(extradata.hp, rowSpan), + extradata.race == PROTOSS and UnitStats._cell(extradata.shield, rowSpan) or nil, + UnitStats._cell(extradata.attributes, rowSpan), + UnitStats._cell(extradata.speed, rowSpan), + UnitStats._cell(extradata.sight, rowSpan) + ) +end + +---@private +---@param game string +---@param page string +---@return table[] +function UnitStats._queryUnitAttacks(game, page) + return Array.filter(mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = tostring(ConditionNode(ColumnName('pagename'), Comparator.eq, page)), + order = 'name asc', + query = 'information, type, name, pagename, extradata', + limit = 5000, + }), function(item) return String.startsWith(item.type, 'Unit attack') end) +end + +return UnitStats From 45fde4330d8438e38a4b4b5155e2fd180d0d6347 Mon Sep 17 00:00:00 2001 From: hjpalpha <75081997+hjpalpha@users.noreply.github.com> Date: Tue, 19 May 2026 21:48:09 +0200 Subject: [PATCH 2/3] Update UnitStatisticsOverviewTable.lua --- lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua b/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua index 04ff444f615..c630d2532e0 100644 --- a/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua +++ b/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua @@ -82,7 +82,7 @@ local UnitStats = {} function UnitStats.wrapper(frame) local args = Arguments.getArgs(frame) local game = Game.name{game = args.game or 'lotv'} - assert(game, 'Invalid game: ' .. args.game) + assert(game, 'Invalid game: ' .. (args.game or 'lotv')) local tabArgs = {} for index, faction in ipairs(Faction.coreFactions) do From bf705d98ee8d7108d9a1f9ab3076cd9499e9a697 Mon Sep 17 00:00:00 2001 From: hjpalpha <75081997+hjpalpha@users.noreply.github.com> Date: Wed, 20 May 2026 05:09:29 +0200 Subject: [PATCH 3/3] Update UnitStatisticsOverviewTable.lua --- lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua b/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua index c630d2532e0..5fca40a59bb 100644 --- a/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua +++ b/lua/wikis/starcraft2/UnitStatisticsOverviewTable.lua @@ -115,6 +115,7 @@ function UnitStats._forFaction(args) end) return TableWidgets.Table{ + columns = Array.rep({}, args.faction == PROTOSS and 22 or 21), children = { UnitStats._header(args.faction), TableWidgets.TableBody{children = Array.flatMap(units, FnUtil.curry(UnitStats._row, args.game))},