|
|
| (31 intermediate revisions by 6 users not shown) |
| Line 1: |
Line 1: |
| -- Renders sortable wikitables of abilities filtered by a named stat. | | -- Renders sortable wikitables of abilities filtered by a named stat. |
|
| |
|
| -- Maps friendly stat names to internal property keys or values.
| | local Lists = require("Module:AbilityTable/Lists") |
| local stat_to_props = { | | local Notes = require("Module:AbilityTable/Notes") |
| ["charges"] = { "AbilityCharges" }, | | local Renderers = require("Module:AbilityTable/ComplexRenderers") |
| ["melee"] = { "melee", "heavy_melee" }, | | local GameData = require("Module:GameData") |
| ["wpndmg"] = { "weapon_damage_increase" },
| | |
| ["healreduce"] = { "HealAmpReceivePenaltyPercent", "HealAmpRegenPenaltyPercent", "DisableHealing" },
| | local p = {} |
| ["debuffresist"] = {
| | |
| -- Do not insert: AbilityDuration, TickRate, SlowDuration
| | local extra_columns = { |
| -- they do more harm than good and include a lot of excessive abilities to the list
| | ["barrier"] = { |
| "StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration",
| | headers = { "Barrier" }, |
| "ImmobilizeDuration", "LiftDuration", "DebuffDuration", "HoldInPlaceDuration", "ExplodeDebuffDuration", "SlowPercent",
| | get_cells = Renderers.RenderBarrier, |
| "DebuffAccuracy", "TimeSlowDuration", "LateCheckoutStun", "BulletDamageAmpDuration", "BulletResistReduction", "BurnDuration",
| |
| "EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "StealDuration", "MaxSlowDuration", "EffectDuration",
| |
| "BleedDuration", "VenomDuration", "TossDuration",
| |
| -- can also just list abilities if they don't have a reusable property
| |
| "Life Drain", "Combo", "Light Eater", "Spirit Lasso", "Card Trick"
| |
| },
| |
| ["moveslow"] = {
| |
| "SlowPercent", "EnemySlowPct", "MoveSlowPercent", "MaxSlowPercent", "MoveSpeedSlowPct", "MovementSpeedSlow",
| |
| "MovementSlowPct", "ClubSlowPercent", "MoveSpeedPenaltyPerStack", "MovementSlow"
| |
| },
| |
| ["dashslow"] = { "GroundDashReductionPercent", "MaxGroundDashReductionPercent", "MoveSpeedAndDashSlowPct", "EnemyDashSlowPercent" },
| |
| ["reactivebarrier"] = { | |
| -- WIP, do not use yet. | |
| "StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration",
| |
| "ImmobilizeDuration", "LiftDuration", "HoldInPlaceDuration", "RestrictionDuration", "PullDuration", "PullDownDuration",
| |
| -- engine-side CCs with no matching data prop
| |
| "Itani Lo Sahn", "Dazzling Trick", "Ground Strike", "Hotel Guest",
| |
| },
| |
| ["dispelmagic"] = {
| |
| "SilenceDuration", "SilenceDebuff", "ImmobilizeDuration", "LiftDuration", "HoldInPlaceDuration",
| |
| "RestrictionDuration",
| |
| "SlowPercent", "EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "SlowDuration",
| |
| "DebuffDuration", "ExplodeDebuffDuration", "BulletDamageAmpDuration", "BulletResistReduction",
| |
| "StealDuration", "EffectDuration", "DebuffAccuracy", "TimeSlowDuration",
| |
| "BurnDuration", "BleedDuration", "VenomDuration", "MaxSlowDuration", | |
| -- raw abilities
| |
| "Life Drain", "Card Trick", "Sticky Bomb", "Light Eater", "Luggage Cart", "Essence Theft"
| |
| },
| |
| ["heal"] = {
| |
| "HealingPerSecond", "HealAmount", "HealFixedHealth", "FlatHealthHealing", "HealingFactor", "MaxHealthRegen",
| |
| "ExternalBonusHealthRegen", "BonusHealthRegen", "LifeStealPercentOnHit", "LifestealPercentHero", "MissingHealthPercentHeal", "MissingHealthBurstPct",
| |
| "MissingHPHeal", "TechLifestealPercent", "AbilityLifestealPercentHero", "HealthStealPctHero", "BulletLifestealPercent", "TotalHealthRegen",
| |
| "DamageHealMult", "HealPctVsHeroes", "InfestHeal", "HealOnKillPct", "HealMaxHealth", "HealMaxHealthPercent",
| |
| "RegenIncomingDamagePercent",
| |
| -- raw abilities
| |
| "Rake", "Card Trick",
| |
| }, | | }, |
| }
| |
|
| |
| -- Editor-authored notes go here.
| |
| local human_notes = {
| |
| ["charges"] = { | | ["charges"] = { |
| ["Jar of Dead"] = "Charges by being near when anything dies.", | | headers = { "Charges", "Time Between Charges", "Notes" }, |
| | get_cells = Renderers.RenderCharges, |
| }, | | }, |
| ["melee"] = { | | ["melee"] = { |
| ["Puddle Punch"] = "Can be parried.", | | headers = { "Base Damage", "Light Melee Scaling", "Notes" }, |
| | get_cells = Renderers.RenderMelee, |
| }, | | }, |
| ["debuffresist"] = { | | ["heavymelee"] = { |
| ["Aura of Suffering"] = "T1 slow, T3 extra damage. The effect is negligible.", | | headers = { "Base Damage", "Heavy Melee Scaling", "Notes" }, |
| ["Lightning Ball"] = "T2 Slow. The effect is negligible.",
| | get_cells = Renderers.RenderHeavyMelee, |
| ["Static Charge"] = "Stun duration. Delay before stun is not affected.",
| |
| ["Enchanter's Satchel"] = "T3 Slow.",
| |
| ["Scorn"] = "T3 extra damage from Mo & Krill.",
| |
| ["Hotel Guest"] = "Only T2 late check-out stun can be reduced.",
| |
| ["Jar of Dead"] = "T2 slow.",
| |
| ["Grapple Arm"] = "T1 extra damage from Bebop.",
| |
| ["Binding Word"] = "Immobilize Duration. Escape time is not affected.",
| |
| ["Power Slash"] = "T1 slow.",
| |
| ["Kinetic Pulse"] = "T2 bullet resist reduction and slow.",
| |
| ["Pain Battery"] = "T1 slow.",
| |
| ["Power Surge"] = "T3 Spirit Resist reduction.",
| |
| ["Rain of Arrows"] = "T2 slow.",
| |
| ["Soul Exchange"] = "T2 silence.", | |
| ["Ground Strike"] = "T2 slow.",
| |
| ["Flog"] = "T2 Fire rate slow.",
| |
| }, | | }, |
| ["moveslow"] = { | | ["scalingspirit"] = { |
| ["Grapple Arm"] = "Applies a short strong slow when the victim reaches Bebop.", | | headers = { "Spirit Scaling", "Notes" }, |
| ["Heavy Barrage"] = "Slows McGinnis by unknown amount.", | | get_cells = Renderers.RenderSpiritScaling, |
| }, | | }, |
| ["reactivebarrier"] = { | | ["healreduce"] = { |
| ["Rend"] = "Triggers on T3 upgrade only.", | | headers = { "Heal Reduction", "Notes" }, |
| ["Soul Exchange"] = "Triggers on T2 upgrade only.",
| | get_cells = Renderers.RenderHealReduce, |
| ["Air Drop"] = "Triggers on T3 upgrade only.",
| |
| ["Entangling Thorns"] = "Triggers on T3 upgrade only.",
| |
| ["Ground Strike"] = "Triggers only when slamming a mid-air enemy into the ground.",
| |
| ["Djinn's Mark"] = "Triggers on T3 upgrade only.",
| |
| ["Time Wall"] = "Triggers on T2 upgrade only.",
| |
| },
| |
| ["heal"] = {
| |
| ["Siphon Life"] = "Lifesteals off damage dealt while active.",
| |
| ["Infernal Resilience"] = "Passively restores health for a percentage of all incoming damage.",
| |
| ["Flawless Advance"] = "Heals a flat amount per hit. Unlocked by T1 upgrade.",
| |
| ["Exploding Uppercut"] = "Restores a portion of missing health on use. Unlocked by T3 upgrade.",
| |
| ["Leaping Slash"] = "Heals a flat amount on hero hit. Unlocked by T1 upgrade.",
| |
| ["Return to Shadows"] = "Heals a flat amount on use. Unlocked by T3 upgrade.",
| |
| ["Light Eater"] = "Grants a buff that gives spirit lifesteal while active.",
| |
| ["Bloodscent"] = "Restores a portion of missing health on kill.",
| |
| ["Rain of Arrows"] = "Grants bullet and spirit lifesteal while active. Unlocked by T3 upgrade.",
| |
| ["Napalm"] = "Lifesteals off all damage dealt to napalmed targets. Unlocked by T2 upgrade.",
| |
| ["Concussive Combustion"] = "Lifesteals off damage dealt. Unlocked by T2 upgrade.",
| |
| ["Kudzu Connection"] = "Grants bullet lifesteal. Also replicates a portion of healing.",
| |
| ["Rejuvenating Aurora"] = "Heals per second while channeling. T3 upgrade also adds passive max health regen.",
| |
| ["Stone Form"] = "Heals a percentage of max HP on cast.",
| |
| ["Frost Grenade"] = "Heals Kelvin and nearby allies for a flat amount.",
| |
| ["Frozen Shelter"] = "Regenerates health for all allies inside the dome.",
| |
| ["Flog"] = "Heals Lash for a percentage of damage dealt to heroes.",
| |
| ["Medicinal Specter"] = "Heals nearby allies per second.",
| |
| ["Rake"] = "Heals a flat amount on kill.",
| |
| ["Scorn"] = "Heals for a percentage of damage dealt.",
| |
| ["Combo"] = "Lifesteals off damage dealt. Unlocked by T1 upgrade.",
| |
| ["Rallying Charge"] = "Heal affected allies and Paige for a flat amount.",
| |
| ["Tag Along"] = "Restores a portion of target's missing health on attachment, then heals per second for a short duration.",
| |
| ["Lil Helpers"] = "Regenerates health for any trooper the helpers are attached to.",
| |
| ["Lycan Curse"] = "Restores a percentage of missing health on activation.",
| |
| ["Go For The Throat"] = "Lifesteals off melee damage dealt.",
| |
| ["Pain Battery"] = "Restores a portion of missing health on hero hit. Unlocked by T3 upgrade.", | |
| ["Jumpstart"] = "Grants a flat health regen buff on use.",
| |
| ["The Cube"] = "Regenerates health for the target.",
| |
| ["Last Stand"] = "Lifesteals off all damage dealt while active.",
| |
| ["Card Trick"] = "Heals Wraith for a flat amount when a Heart or Joker card hits.",
| |
| ["Crimson Slash"] = "Heals a flat amount on hero hit.",
| |
| ["Shadow Transformation"] = "Restores a percentage of max health per hero kill.",
| |
| },
| |
| }
| |
| | |
| -- Type here to exclude ability from a list. If adding please include a comment.
| |
| local exclude_list = {
| |
| ["debuffresist"] = {
| |
| "Bookwyrm", -- has DebuffDuration for some reason
| |
| "Itani Lo Sahn", -- has DebuffDuration but does not get reduced
| |
| "Slam Fire", -- has DebuffDuration for some reason
| |
| "Splatter", -- has SlowPercent but no duration to shave off.
| |
| }, | | }, |
| ["moveslow"] = { | | ["moveslow"] = { |
| "Riposte", -- has a 40% 4s slow somehow? (SlowPercent + SlowDuration) | | headers = { "Move Slow", "Notes" }, |
| "Grasping Hands", -- has SlowPercent | | get_cells = Renderers.RenderMoveSlow, |
| }, | | }, |
| ["dashslow"] = { | | ["dashslow"] = { |
| "Itani Lo Sahn", -- has GroundDashReductionPercent but also disables stamina. | | headers = { "Dash Slow", "Notes" }, |
| },
| | get_cells = Renderers.RenderDashSlow, |
| ["heal"] = {
| |
| "Riposte", -- has AbilityLifestealPercentHero, but doesn't deal damage to heal off of.
| |
| }, | | }, |
| ["dispelmagic"] = { | | ["extradebuffresist"] = { |
| "Bookwyrm", -- has DebuffDuration for some reason | | headers = { "Debuff Resist" }, |
| "Slam Fire", -- has DebuffDuration for some reason | | get_cells = Renderers.RenderExtraDebuffResist, |
| }, | | }, |
| } | | } |
|
| |
| -- Unit strings appended to cell values by column name.
| |
| local column_suffix = {
| |
| ["Time Between Charges"] = "s",
| |
| }
| |
|
| |
| -- Actual code below
| |
|
| |
| local p = {}
| |
| local GameData = require("Module:GameData")
| |
|
| |
|
| -- ============================================================ | | -- ============================================================ |
| -- Shared helpers | | -- Note enrichment |
| -- ============================================================ | | -- ============================================================ |
|
| |
|
| local function num_val(v) | | -- Merges auto-generated and human notes for full-table mode. |
| if type(v) == "number" then return v end | | -- Capitalizes human note, joins with ". ", ensures trailing period. |
| if type(v) == "table" then return v["Value"] end
| | local function enrich_notes_full(extra, human_note) |
| return nil | | if human_note then |
| end | | human_note = human_note:sub(1,1):upper() .. human_note:sub(2) |
| | | extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human_note) or human_note |
| -- Searches Upgrades[] for a field and returns { tier = N, value = V } at its
| | end |
| -- first appearance, or nil if the field is absent from all upgrade tiers.
| | if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then |
| local function find_in_upgrades(ability, field)
| | extra["Notes"] = extra["Notes"] .. "." |
| local upgrades = ability["Upgrades"]
| |
| if type(upgrades) ~= "table" then return nil end
| |
| for i, tier in ipairs(upgrades) do
| |
| if type(tier) == "table" and tier[field] ~= nil then | |
| return { tier = i, value = num_val(tier[field]) }
| |
| end
| |
| end | | end |
| return nil
| |
| end | | end |
|
| |
|
| -- Finds the first non-zero value for any of the given fields, checking base | | -- Merges auto-generated and human notes for compact mode. |
| -- then upgrades in order. Returns field, tier (nil=base, N=upgrade), abs value. | | -- Decapitalizes human note (skips T{N} prefixes like "T1 slow"), |
| local function find_first_value(ability, fields) | | -- joins with "; ", strips trailing period. |
| for _, field in ipairs(fields) do | | local function enrich_notes_compact(extra, human_note) |
| local v = num_val(ability[field]) | | if human_note then |
| if v and v ~= 0 then return field, nil, math.abs(v) end | | local first = human_note:sub(1,1) |
| end
| | if first:match("%u") and not human_note:sub(2,2):match("%d") then |
| local upgrades = ability["Upgrades"]
| | human_note = first:lower() .. human_note:sub(2) |
| if type(upgrades) == "table" then
| |
| for i, tier in ipairs(upgrades) do
| |
| if type(tier) == "table" then
| |
| for _, field in ipairs(fields) do
| |
| local v = num_val(tier[field])
| |
| if v and v ~= 0 then return field, i, math.abs(v) end
| |
| end
| |
| end
| |
| end | | end |
| | if human_note:sub(-1) == "." then human_note = human_note:sub(1, -2) end |
| | extra["Notes"] = extra["Notes"] and (extra["Notes"] .. "; " .. human_note) or human_note |
| end | | end |
| return nil, nil, nil
| |
| end | | end |
|
| |
|
| -- ============================================================ | | -- ============================================================ |
| -- Filtering | | -- Ability views |
| -- ============================================================ | | -- |
| | | -- In-game Ability may containe 1 or more records in AbilityData.json |
| -- Filters a list of prop-matched abilities down to those that should appear | | -- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt). |
| -- in the table for the given stat. Handles:
| |
| -- 1. key_lookup membership (ability belongs to an active released hero) | |
| -- 2. exclude_list (manual false-positive suppressions)
| |
| -- 3. stat-specific logic (e.g. dispelmagic strips slot-4 ultimates)
| |
| local function filter_abilities(stat, abilities, key_lookup)
| |
| local excluded = {}
| |
| if exclude_list[stat] then
| |
| for _, name in ipairs(exclude_list[stat]) do
| |
| excluded[name] = true
| |
| end
| |
| end
| |
|
| |
|
| local result = {}
| | -- "Ability View" is a main-record + any relevant sub-ability records. |
| for _, ability in ipairs(abilities) do
| |
| local key = ability["Key"]
| |
| local info = key and key_lookup[key]
| |
| if info and not excluded[ability["Name"]] then
| |
| local skip = false
| |
| -- dispelmagic: skip ultimates (slot 4)
| |
| if stat == "dispelmagic" and info.abilityNum == 4 then
| |
| skip = true
| |
| end
| |
| if not skip then
| |
| table.insert(result, ability)
| |
| end
| |
| end
| |
| end
| |
| return result
| |
| end
| |
| | |
| -- ============================================================
| |
| -- Per-stat cell renderers
| |
| -- Each function receives a guaranteed-matching ability record
| |
| -- and returns { ["Column Name"] = value }. | |
| -- Human notes are appended by the framework, not here.
| |
| -- ============================================================ | | -- ============================================================ |
|
| |
|
| -- Short display names for debuff properties. | | local _views = nil -- card key -> { view = record, heroName = ..., abilityNum = ... } |
| local debuff_short_name = {
| | local _order = nil -- array of card keys, sorted by hero then slot |
| ["StunDuration"] = "stun",
| |
| ["SleepDuration"] = "sleep",
| |
| ["SilenceDuration"] = "silence",
| |
| ["SilenceDebuff"] = "silence",
| |
| ["PetrifyDuration"] = "petrify",
| |
| ["HexDuration"] = "hex",
| |
| ["ImmobilizeDuration"] = "immobilize",
| |
| ["LiftDuration"] = "lift",
| |
| ["HoldInPlaceDuration"] = "immobilize",
| |
| ["RestrictionDuration"] = "immobilize",
| |
| ["DebuffDuration"] = "debuff",
| |
| ["ExplodeDebuffDuration"] = "debuff",
| |
| ["SlowPercent"] = "slow",
| |
| ["EnemySlowPct"] = "slow",
| |
| ["MoveSlowPercent"] = "slow",
| |
| ["GroundDashReductionPercent"] = "dash slow",
| |
| ["SlowDuration"] = "slow",
| |
| ["MaxSlowDuration"] = "slow",
| |
| ["BulletDamageAmpDuration"] = "bullet amp",
| |
| ["BulletResistReduction"] = "bullet resist reduction",
| |
| ["StealDuration"] = "debuff",
| |
| ["EffectDuration"] = "debuff",
| |
| ["DebuffAccuracy"] = "debuff",
| |
| ["TimeSlowDuration"] = "time slow",
| |
| ["LateCheckoutStun"] = "stun",
| |
| ["BurnDuration"] = "burn",
| |
| ["BleedDuration"] = "bleed",
| |
| ["VenomDuration"] = "venom",
| |
| ["TossDuration"] = "Knockback",
| |
| }
| |
|
| |
|
| -- Returns true if prop is found with a non-zero value anywhere in record,
| | local function build_views() |
| -- excluding the Upgrades key. Mirrors GameData.record_matches_prop logic.
| | if _views then return _views, _order end |
| local function prop_in_base(record, prop) | | _views, _order = {}, {} |
| for k, v in pairs(record) do | |
| if k == "Upgrades" then
| |
| -- skip
| |
| elseif k == prop then
| |
| if type(v) == "table" then v = v["Value"] end
| |
| if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
| |
| return true
| |
| end
| |
| elseif type(v) == "table" then
| |
| if prop_in_base(v, prop) then return true end
| |
| elseif type(v) == "string" then
| |
| if v == prop then return true end
| |
| end
| |
| end
| |
| return false
| |
| end
| |
|
| |
|
| -- Renderer shared by debuffresist and dispelmagic.
| | local hero_data = mw.loadJsonData("Data:HeroData.json") |
| -- Outputs a Notes cell describing which debuffs are added only by upgrades.
| | local active_heroes = {} |
| -- If any prop from the stat's list is found in the base ability, outputs nothing.
| | for _, hero in pairs(hero_data) do |
| -- Otherwise lists per-tier debuff names: "T2 slow", "T1 disarm, T2 slow", etc.
| | if type(hero) == "table" |
| local function debuff_cells(ability, stat)
| | and hero["IsDisabled"] == false |
| local props = stat_to_props[stat] | | and type(hero["Name"]) == "string" |
| local cells = {}
| | and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false) |
| | | then |
| -- Check if any prop is present in base (outside Upgrades).
| | active_heroes[hero["Name"]] = true |
| -- If so, this is a full-ability debuff — no note needed.
| |
| for _, prop in ipairs(props) do | |
| if prop_in_base(ability, prop) then | |
| return cells | |
| end | | end |
| end | | end |
|
| |
|
| -- Scan upgrades: collect props found per tier. | | local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES) |
| local upgrades = ability["Upgrades"] | | local cards = mw.loadJsonData("Data:AbilityCards.json") |
| if type(upgrades) ~= "table" then return cells end
| |
|
| |
|
| -- tier_debuffs[i] = list of short names found in tier i
| | local card_of = {} |
| local tier_debuffs = {} | | for _, hero_entry in pairs(cards) do |
| local seen_names = {} -- deduplicate short names across tiers
| | if type(hero_entry) == "table" then |
| | | local hero_name = hero_entry["Name"] |
| for i, tier in ipairs(upgrades) do | | if hero_name and active_heroes[hero_name] then |
| if type(tier) == "table" then | | for slot_str, ability in pairs(hero_entry) do |
| local names_this_tier = {} | | local slot_num = tonumber(slot_str) |
| local seen_this_tier = {} | | local key = type(ability) == "table" and ability["Key"] or nil |
| for _, prop in ipairs(props) do
| | if slot_num and key and ability_data[key] then |
| local v = tier[prop]
| | card_of[key] = { heroName = hero_name, abilityNum = slot_num } |
| if v ~= nil then
| |
| if type(v) == "table" then v = v["Value"] end
| |
| if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then | |
| local short = debuff_short_name[prop] or "debuff"
| |
| if not seen_this_tier[short] then | |
| seen_this_tier[short] = true
| |
| if not seen_names[short] then
| |
| seen_names[short] = true
| |
| table.insert(names_this_tier, short)
| |
| end
| |
| end
| |
| end | | end |
| end | | end |
| end | | end |
| if #names_this_tier > 0 then
| |
| tier_debuffs[i] = names_this_tier
| |
| end
| |
| end
| |
| end
| |
|
| |
| -- Build note: "T1 slow, T2 silence + burn"
| |
| local parts = {}
| |
| for i = 1, #upgrades do
| |
| if tier_debuffs[i] then
| |
| table.insert(parts, "T" .. i .. " " .. table.concat(tier_debuffs[i], " + "))
| |
| end
| |
| end
| |
|
| |
| if #parts > 0 then
| |
| cells["Notes"] = table.concat(parts, ", ")
| |
| end
| |
| return cells
| |
| end
| |
|
| |
| local function charges_cells(ability)
| |
| local base_charges = num_val(ability["AbilityCharges"])
| |
| local base_cooldown = ability["AbilityCooldownBetweenCharge"]
| |
| local has_base = base_charges ~= nil and base_charges > 0
| |
| local upgrade_info = find_in_upgrades(ability, "AbilityCharges")
| |
|
| |
| local cooldown, upgrade_cooldown
| |
| if has_base then
| |
| if type(base_cooldown) == "number" and base_cooldown > 0 then
| |
| cooldown = base_cooldown
| |
| else
| |
| local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge")
| |
| if cd and cd.value and cd.value > 0 then cooldown = cd.value end
| |
| end | | end |
| else
| |
| local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge")
| |
| if cd and cd.value and cd.value > 0 then upgrade_cooldown = cd.value end
| |
| end | | end |
|
| |
|
| local cells = {} | | -- Attach each non-card record to the longest card key it prefixes. |
| | | local subs = {} |
| if has_base then
| | for key, record in pairs(ability_data) do |
| cells["Charges"] = tostring(base_charges)
| | if not card_of[key] and record["IsDisabled"] ~= true then |
| if cooldown then
| | local parent = nil |
| cells["Time Between Charges"] = tostring(cooldown)
| | for card_key in pairs(card_of) do |
| end
| | if key:sub(1, #card_key + 1) == card_key .. "_" |
| end
| | and (parent == nil or #card_key > #parent) |
| | | then |
| local note_parts = {} | | parent = card_key |
| if upgrade_info then
| |
| local n = upgrade_info.value or "?"
| |
| local charge_str = n .. " charge" .. (n ~= 1 and "s" or "")
| |
| if has_base then
| |
| table.insert(note_parts, "+" .. charge_str .. " on T" .. upgrade_info.tier .. " upgrade.")
| |
| else
| |
| local cd_str = upgrade_cooldown and (upgrade_cooldown .. "s") or "unknown"
| |
| table.insert(note_parts, "Becomes charged on T" .. upgrade_info.tier
| |
| .. " upgrade with " .. charge_str
| |
| .. " and " .. cd_str .. " time between charges.")
| |
| end
| |
| end
| |
| if ability["AbilityChargesConditionally"] ~= nil then
| |
| table.insert(note_parts, "Has a conditional charge.")
| |
| end
| |
| if #note_parts > 0 then
| |
| cells["Notes"] = table.concat(note_parts, " ")
| |
| end
| |
| | |
| return cells
| |
| end
| |
| | |
| local function find_base_melee_scale(ability)
| |
| for _, v in pairs(ability) do | |
| if type(v) == "table" then | |
| local scale = v["Scale"]
| |
| if type(scale) == "table"
| |
| and scale["Type"] == "melee"
| |
| and type(scale["Value"]) == "number"
| |
| and scale["Value"] > 0
| |
| then
| |
| return scale["Value"]
| |
| end | |
| end
| |
| end
| |
| return nil
| |
| end
| |
| | |
| local function build_melee_upgrade_notes(ability, base_melee_scale)
| |
| local upgrades = ability["Upgrades"]
| |
| if type(upgrades) ~= "table" then return {} end
| |
| local notes = {}
| |
| for i, tier in ipairs(upgrades) do
| |
| if type(tier) == "table" then
| |
| for _, v in pairs(tier) do | |
| if type(v) == "table" then | |
| local scale = v["Scale"]
| |
| if type(scale) == "table" and type(scale["Value"]) == "number" then | |
| local stype = scale["Type"]
| |
| local delta = scale["Value"]
| |
| if stype == "heavy_melee" and delta > 0 then
| |
| table.insert(notes, "T" .. i .. " upgrade changes scaling to ×"
| |
| .. delta .. " of Heavy Melee damage.")
| |
| elseif stype == "melee" and delta ~= 0 and base_melee_scale then
| |
| local final = math.floor((base_melee_scale + delta) * 1000 + 0.5) / 1000
| |
| table.insert(notes, "T" .. i .. " upgrade adds ×"
| |
| .. delta .. " scaling (total ×" .. final .. ").")
| |
| end
| |
| end
| |
| end | | end |
| end | | end |
| end
| | if parent then |
| end
| | subs[parent] = subs[parent] or {} |
| return notes
| | table.insert(subs[parent], record) |
| end
| |
| | |
| local function melee_cells(ability)
| |
| local cells = {}
| |
| local base_scale = find_base_melee_scale(ability)
| |
| if base_scale then cells["Light Melee Scaling"] = "×" .. base_scale end
| |
| local upgrade_notes = build_melee_upgrade_notes(ability, base_scale)
| |
| if #upgrade_notes > 0 then cells["Notes"] = table.concat(upgrade_notes, " ") end
| |
| return cells
| |
| end
| |
| | |
| local function healreduce_cells(ability)
| |
| local cells = {}
| |
| local notes = {}
| |
| local props = stat_to_props["healreduce"]
| |
| | |
| local first_pct, first_tier, first_is_disable = nil, nil, false
| |
| | |
| for _, prop in ipairs(props) do
| |
| local v = num_val(ability[prop])
| |
| if v and v ~= 0 then
| |
| if prop == "DisableHealing" then
| |
| first_is_disable = true | |
| else
| |
| first_pct = math.abs(v)
| |
| end
| |
| break
| |
| end
| |
| end
| |
| if not first_pct and not first_is_disable then
| |
| local upgrades = ability["Upgrades"]
| |
| if type(upgrades) == "table" then
| |
| for i, tier in ipairs(upgrades) do
| |
| if type(tier) == "table" then
| |
| for _, prop in ipairs(props) do
| |
| local v = num_val(tier[prop])
| |
| if v and v ~= 0 then
| |
| if prop == "DisableHealing" then
| |
| first_is_disable, first_tier = true, i
| |
| else
| |
| first_pct, first_tier = math.abs(v), i
| |
| end
| |
| break
| |
| end
| |
| end
| |
| end
| |
| if first_pct or first_is_disable then break end
| |
| end | | end |
| end | | end |
| end | | end |
|
| |
|
| if first_is_disable then | | for key, info in pairs(card_of) do |
| cells["Heal Reduction"] = "100%" .. (first_tier and " (T" .. first_tier .. ")" or "") | | local view = {} |
| elseif first_pct then
| | for k, v in pairs(ability_data[key]) do view[k] = v end |
| cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "") | | view["_subabilities"] = subs[key] |
| | _views[key] = { view = view, heroName = info.heroName, abilityNum = info.abilityNum } |
| | table.insert(_order, key) |
| end | | end |
|
| |
|
| local upgrades = ability["Upgrades"] | | table.sort(_order, function(a, b) |
| if type(upgrades) == "table" then
| | local ia, ib = _views[a], _views[b] |
| local running = first_pct
| | if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end |
| local first_pct_seen = (first_tier == nil)
| | return ia.abilityNum < ib.abilityNum |
| | | end) |
| for i, tier in ipairs(upgrades) do
| |
| if type(tier) == "table" then
| |
| if first_tier == i then
| |
| first_pct_seen = true
| |
| if first_pct then running = first_pct end
| |
| elseif first_pct_seen then
| |
| local delta = nil
| |
| for _, prop in ipairs(props) do
| |
| if prop ~= "DisableHealing" then
| |
| local v = num_val(tier[prop])
| |
| if v and v ~= 0 then delta = math.abs(v); break end
| |
| end
| |
| end
| |
| if delta then
| |
| local total = (running or 0) + delta
| |
| table.insert(notes, "T" .. i .. " upgrade increases heal reduction by "
| |
| .. delta .. "% (total " .. total .. "%).")
| |
| running = total
| |
| end
| |
| end
| |
| end
| |
| end
| |
| end | |
|
| |
|
| if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end | | return _views, _order |
| return cells
| |
| end | | end |
|
| |
|
| -- Generic renderer for a numeric stat column. | | -- ============================================================ |
| -- col_name: the column header string | | -- Membership |
| -- fields: ordered list of property names (first non-zero wins) | | -- Pieces of code that help in telling "is ability X a member of list Y". |
| -- suffix: string appended to values in cell and notes (e.g. "%" or "")
| | -- main function is: p.is_member() |
| -- Upgrade deltas are additive; notes track running totals. | | -- ============================================================ |
| local function val_stat_cells(ability, col_name, fields, suffix)
| |
| suffix = suffix or ""
| |
| local cells = {}
| |
| local notes = {}
| |
| | |
| local first_field, first_tier, first_val = find_first_value(ability, fields)
| |
| if not first_field then return cells end
| |
| | |
| local cell_str = tostring(first_val) .. suffix
| |
| if first_tier then cell_str = cell_str .. " (T" .. first_tier .. ")" end
| |
| cells[col_name] = cell_str
| |
| | |
| local upgrades = ability["Upgrades"]
| |
| if type(upgrades) == "table" then
| |
| local running = first_val
| |
| local seen_first = (first_tier == nil)
| |
|
| |
|
| for i, tier in ipairs(upgrades) do
| | -- Memoized name-exclusion set per list (from Lists.exclude_abilities). |
| if type(tier) == "table" then
| | local _excluded = {} |
| if first_tier == i then
| | local function excluded_set(stat) |
| seen_first = true
| | if _excluded[stat] then return _excluded[stat] end |
| elseif seen_first then
| | local set = {} |
| local delta = nil
| | for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do |
| for _, field in ipairs(fields) do
| | set[name] = true |
| local v = num_val(tier[field])
| |
| if v and v ~= 0 then delta = math.abs(v); break end
| |
| end
| |
| if delta then
| |
| local total = running + delta
| |
| table.insert(notes, "T" .. i .. " upgrade increases "
| |
| .. col_name:lower() .. " by " .. delta .. suffix
| |
| .. " (total " .. total .. suffix .. ").")
| |
| running = total
| |
| end
| |
| end
| |
| end
| |
| end
| |
| end | | end |
| | | _excluded[stat] = set |
| if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end | | return set |
| return cells | |
| end | | end |
|
| |
|
| local function moveslow_cells(ability)
| | -- Per-list rules that can't be expressed as prop lists or exclusions. |
| local cells = val_stat_cells(ability, "Move Slow", stat_to_props["moveslow"], "%")
| | -- Returns true to keep the ability, false to drop it. |
| | | local function special_ok(stat, record, ability_num) |
| local channel_slow = num_val(ability["ChannelSlowPercent"]) | | if stat == "dispelmagic" and ability_num == 4 then return false end |
| if channel_slow and channel_slow ~= 0 then
| | return true |
| local note = "Also slows the caster for " .. math.abs(channel_slow) .. "%."
| |
| cells["Notes"] = cells["Notes"] and (cells["Notes"] .. " " .. note) or note
| |
| end
| |
| | |
| return cells | |
| end | | end |
|
| |
|
| local function dashslow_cells(ability)
| | -- Public: does ability `ability_key` belong to list `stat`? |
| local props = stat_to_props["dashslow"] | | function p.is_member(stat, ability_key, ability_num) |
| | local props = Lists.lists[stat] |
| | if not props then return false end |
|
| |
|
| local caster_dash_slow = nil | | local views = build_views() |
| local base_ground = num_val(ability["GroundDashReductionPercent"])
| | local entry = views[ability_key] |
| if base_ground and base_ground ~= 0 then | | local view = entry and entry.view |
| local has_enemy_prop = false
| |
| for _, p in ipairs(stat_to_props["dashslow"]) do
| |
| if p ~= "GroundDashReductionPercent" then
| |
| local v = num_val(ability[p])
| |
| if v and v ~= 0 then has_enemy_prop = true; break end
| |
| end
| |
| end
| |
| if not has_enemy_prop then
| |
| local upgrades = ability["Upgrades"]
| |
| if type(upgrades) == "table" then
| |
| for _, tier in ipairs(upgrades) do
| |
| if type(tier) == "table" then
| |
| for _, p in ipairs(stat_to_props["dashslow"]) do
| |
| if p ~= "GroundDashReductionPercent" then
| |
| local v = num_val(tier[p])
| |
| if v and v ~= 0 then has_enemy_prop = true; break end
| |
| end
| |
| end
| |
| end
| |
| if has_enemy_prop then break end
| |
| end
| |
| end
| |
| end
| |
| if has_enemy_prop then
| |
| caster_dash_slow = math.abs(base_ground)
| |
| local filtered = {}
| |
| for _, p in ipairs(props) do
| |
| if p ~= "GroundDashReductionPercent" then
| |
| table.insert(filtered, p)
| |
| end
| |
| end
| |
| props = filtered
| |
| end
| |
| end
| |
| | |
| local cells = val_stat_cells(ability, "Dash Slow", props, "%")
| |
|
| |
|
| if caster_dash_slow then | | if not view then |
| local note = "Gives " .. caster_dash_slow .. "% dash slow for the caster." | | view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key] |
| cells["Notes"] = cells["Notes"] and (cells["Notes"] .. " " .. note) or note | | if not view then return false end |
| end | | end |
|
| |
|
| return cells | | local name = view["Name"] |
| end | | if name and excluded_set(stat)[name] then return false end |
|
| |
|
| local extra_columns = {
| | ability_num = ability_num or (entry and entry.abilityNum) |
| ["charges"] = {
| | if not special_ok(stat, view, ability_num) then return false end |
| headers = { "Charges", "Time Between Charges", "Notes" },
| |
| get_cells = charges_cells,
| |
| },
| |
| ["melee"] = {
| |
| headers = { "Light Melee Scaling", "Notes" },
| |
| get_cells = melee_cells,
| |
| },
| |
| ["healreduce"] = {
| |
| headers = { "Heal Reduction", "Notes" },
| |
| get_cells = healreduce_cells,
| |
| },
| |
| ["debuffresist"] = { | |
| headers = { "Notes" },
| |
| get_cells = function(ability) return debuff_cells(ability, "debuffresist") end,
| |
| }, | |
| ["dispelmagic"] = {
| |
| headers = { "Notes" },
| |
| get_cells = function(ability) return debuff_cells(ability, "dispelmagic") end,
| |
| },
| |
| ["moveslow"] = {
| |
| headers = { "Move Slow", "Notes" },
| |
| get_cells = moveslow_cells,
| |
| },
| |
| ["dashslow"] = {
| |
| headers = { "Dash Slow", "Notes" },
| |
| get_cells = dashslow_cells,
| |
| },
| |
| }
| |
|
| |
|
| -- ============================================================
| | return GameData.entity_matches(view, props) |
| -- Hero lookup
| |
| -- _key_lookup : ability key -> { heroName, abilityNum }
| |
| -- _name_to_key : ability name -> ability key
| |
| -- Both built together from HeroData + AbilityCards.
| |
| -- ============================================================
| |
| | |
| local _key_lookup = nil
| |
| local _name_to_key = nil
| |
| | |
| local function build_lookup()
| |
| if _key_lookup then return _key_lookup, _name_to_key end | |
| _key_lookup = {}
| |
| _name_to_key = {}
| |
| | |
| local hero_data = mw.loadJsonData("Data:HeroData.json")
| |
| local active_heroes = {}
| |
| for _, hero in pairs(hero_data) do
| |
| if type(hero) == "table"
| |
| and hero["IsDisabled"] == false
| |
| and type(hero["Name"]) == "string"
| |
| and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
| |
| then
| |
| active_heroes[hero["Name"]] = true
| |
| end
| |
| end
| |
| | |
| local cards = mw.loadJsonData("Data:AbilityCards.json")
| |
| for _, hero_entry in pairs(cards) do
| |
| if type(hero_entry) == "table" then
| |
| local hero_name = hero_entry["Name"]
| |
| if hero_name and active_heroes[hero_name] then
| |
| for slot_str, ability in pairs(hero_entry) do
| |
| local slot_num = tonumber(slot_str)
| |
| if slot_num and type(ability) == "table" then
| |
| local info = { heroName = hero_name, abilityNum = slot_num }
| |
| if ability["Key"] then
| |
| _key_lookup[ability["Key"]] = info
| |
| end
| |
| if ability["Name"] and ability["Key"] then
| |
| _name_to_key[ability["Name"]] = ability["Key"]
| |
| end
| |
| end
| |
| end
| |
| end
| |
| end
| |
| end
| |
| | |
| return _key_lookup, _name_to_key
| |
| end | | end |
|
| |
|
| Line 748: |
Line 215: |
| local stat = mw.text.trim(frame.args[1] or ""):lower() | | local stat = mw.text.trim(frame.args[1] or ""):lower() |
|
| |
|
| local internal_keys = stat_to_props[stat] | | local internal_keys = Lists.lists[stat] |
| if not internal_keys then | | if not internal_keys then |
| return '<span class="error">AbilityTable: unknown stat "' .. stat .. '".</span>' | | return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>' |
| end | | end |
|
| |
|
| Line 761: |
Line 228: |
| local compact = mw.text.trim(frame.args["compact"] or "") ~= "" | | local compact = mw.text.trim(frame.args["compact"] or "") ~= "" |
|
| |
|
| local key_lookup, _ = build_lookup() | | local collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower() |
| | |
| | local views, order = build_views() |
| local spec = extra_columns[stat] | | local spec = extra_columns[stat] |
| local headers = spec and spec.headers or {} | | local headers = spec and spec.headers or {} |
| local get_cells = spec and spec.get_cells or function() return {} end | | local get_cells = spec and spec.get_cells or function() return {} end |
| local stat_notes = human_notes[stat] | | local stat_notes = Notes[stat] |
|
| |
|
| local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) | | local filtered = {} |
| local filtered = filter_abilities(stat, abilities, key_lookup)
| | for _, key in ipairs(order) do |
| | if p.is_member(stat, key) then |
| | table.insert(filtered, key) |
| | end |
| | end |
|
| |
|
| if #filtered == 0 then | | if #filtered == 0 then |
| return "No abilities found for stat: " .. stat | | return "No abilities found for list: " .. stat |
| end | | end |
|
| |
|
| table.sort(filtered, function(a, b) | | local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists") |
| local ia = key_lookup[a["Key"]]
| | local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" }) |
| local ib = key_lookup[b["Key"]]
| |
| if not ia and not ib then return (a["Name"] or "") < (b["Name"] or "") end
| |
| if not ia then return false end
| |
| if not ib then return true end
| |
| if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
| |
| return ia.abilityNum < ib.abilityNum
| |
| end)
| |
| | |
| local edit_url = mw.uri.fullUrl("Module:AbilityTable", { action = "edit" }) | |
| local disclaimer = frame:preprocess( | | local disclaimer = frame:preprocess( |
| '<div class="ability-table-disclaimer">' | | '<div class="ability-table-disclaimer">' |
| .. "This is an automatic list. " | | .. 'This is a dynamic list ' |
| .. "[" .. tostring(edit_url) .. " Click here]" | | .. '<span class="ability-table-list-name">"' .. stat .. '"</span>. ' |
| .. " to add notes or correct the contents." | | .. '[' .. tostring(lists_url) .. ' Edit contents]' |
| .. "</div>" | | .. ' · ' |
| | .. '[' .. tostring(notes_url) .. ' Edit notes].' |
| | .. '</div>' |
| ) | | ) |
|
| |
|
| Line 796: |
Line 262: |
| local hero_order = {} | | local hero_order = {} |
| local hero_abilities = {} | | local hero_abilities = {} |
| for _, ability in ipairs(filtered) do | | for _, key in ipairs(filtered) do |
| local info = key_lookup[ability["Key"]] | | local hero = views[key].heroName |
| local hero = info and info.heroName or "—"
| |
| if not hero_abilities[hero] then | | if not hero_abilities[hero] then |
| table.insert(hero_order, hero) | | table.insert(hero_order, hero) |
| hero_abilities[hero] = {} | | hero_abilities[hero] = {} |
| end | | end |
| table.insert(hero_abilities[hero], ability["Name"]) | | table.insert(hero_abilities[hero], views[key].view) |
| end | | end |
|
| |
|
| Line 810: |
Line 275: |
| local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } } | | local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } } |
| local ability_parts = {} | | local ability_parts = {} |
| for _, name in ipairs(hero_abilities[hero]) do | | for _, ability in ipairs(hero_abilities[hero]) do |
| | local name = ability["Name"] |
| local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } } | | local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } } |
| local note = stat_notes and stat_notes[name] | | |
| | local extra = get_cells(ability) |
| | enrich_notes_compact(extra, stat_notes and stat_notes[name]) |
| | |
| | local note = extra["Notes"] |
| if note then | | if note then |
| table.insert(ability_parts, ability_cell .. " (" .. note .. ")") | | table.insert(ability_parts, ability_cell .. " (" .. note .. ")") |
| Line 836: |
Line 306: |
| local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set["Notes"] | | local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set["Notes"] |
| if needs_notes then table.insert(final_headers, "Notes") end | | if needs_notes then table.insert(final_headers, "Notes") end |
| | |
| | local table_class = 'wikitable sortable ability-table' |
| | if collapsed ~= "" then |
| | table_class = table_class .. ' mw-collapsible' |
| | if collapsed == "yes" or collapsed == "true" or collapsed == "1" then |
| | table_class = table_class .. ' mw-collapsed' |
| | end |
| | end |
|
| |
|
| local out = {} | | local out = {} |
| table.insert(out, disclaimer) | | table.insert(out, disclaimer) |
| table.insert(out, '{| class="wikitable sortable ability-table"') | | table.insert(out, '{| class="' .. table_class .. '"') |
|
| |
|
| local header_row = '! class="ability-table-col-hero" | Hero' | | local header_row = '! class="ability-table-col-hero" | Hero' |
| Line 849: |
Line 327: |
| table.insert(out, header_row) | | table.insert(out, header_row) |
|
| |
|
| for _, ability in ipairs(filtered) do | | local hero_counts = {} |
| local info = key_lookup[ability["Key"]] | | for _, key in ipairs(filtered) do |
| | local hero = views[key].heroName |
| | hero_counts[hero] = (hero_counts[hero] or 0) + 1 |
| | end |
| | |
| | local hero_seen = {} |
| | |
| | for _, key in ipairs(filtered) do |
| | local entry = views[key] |
| | local ability = entry.view |
| | local hero = entry.heroName |
|
| |
|
| local hero_cell | | local ability_cell = frame:expandTemplate{ |
| if info then
| | title = "AbilityIcon", |
| hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { info.heroName } }
| | args = { ability["Name"] } |
| else
| | } |
| hero_cell = "—" | |
| end
| |
| local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { ability["Name"] } }
| |
|
| |
|
| local extra = get_cells(ability) | | local extra = get_cells(ability) |
| | enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]]) |
|
| |
|
| if stat_notes and stat_notes[ability["Name"]] then | | local row |
| local human = stat_notes[ability["Name"]] | | |
| extra["Notes"] = extra["Notes"] and (extra["Notes"] .. " " .. human) or human
| | if not hero_seen[hero] then |
| | local hero_cell = frame:expandTemplate{ |
| | title = "HeroIcon", |
| | args = { hero } |
| | } |
| | |
| | if hero_counts[hero] > 1 then |
| | row = '| rowspan="' .. hero_counts[hero] .. '" class="ability-table-cell-hero" | ' |
| | .. hero_cell |
| | .. ' || class="ability-table-cell-ability" | ' .. ability_cell |
| | else |
| | row = '| class="ability-table-cell-hero" | ' |
| | .. hero_cell |
| | .. ' || class="ability-table-cell-ability" | ' .. ability_cell |
| | end |
| | |
| | hero_seen[hero] = true |
| | else |
| | row = '| class="ability-table-cell-ability" | ' .. ability_cell |
| end | | end |
|
| |
|
| local row = '| class="ability-table-cell-hero" | ' .. hero_cell
| |
| .. ' || class="ability-table-cell-ability" | ' .. ability_cell
| |
| for _, header in ipairs(final_headers) do | | for _, header in ipairs(final_headers) do |
| local value = extra[header] | | local value = extra[header] |
| local cls = "ability-table-cell-" .. header:lower():gsub("%s+", "-") | | local cls = "ability-table-cell-" .. header:lower():gsub("%s+", "-") |
| local content | | local content = value ~= nil and tostring(value) or "—" |
| if value ~= nil then
| |
| local suffix = column_suffix[header] or ""
| |
| content = (type(value) == "string" and value:sub(1,1) == "(")
| |
| and value or (value .. suffix)
| |
| else
| |
| content = "—"
| |
| end
| |
| row = row .. ' || class="' .. cls .. '" | ' .. content | | row = row .. ' || class="' .. cls .. '" | ' .. content |
| end | | end |