Editing Module:AbilityTableGive feedback
Jump to navigation
Jump to search
The edit can be undone.
Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
| Latest revision | Your text | ||
| Line 1: | Line 1: | ||
-- Renders sortable wikitables of abilities filtered by a named stat. | -- Renders sortable wikitables of abilities filtered by a named stat. | ||
local | -- Maps friendly stat names to internal property keys or values. | ||
local stat_to_props = { | |||
["charges"] = { "AbilityCharges" }, | |||
["melee"] = { "melee", "heavy_melee" }, | |||
["wpndmg"] = { "weapon_damage_increase" }, | |||
["healreduce"] = { "HealAmpReceivePenaltyPercent", "HealAmpRegenPenaltyPercent", "DisableHealing" }, | |||
["debuffresist"] = { | |||
-- Do not insert: AbilityDuration, TickRate, SlowDuration | |||
-- they do more harm than good and include a lot of excessive abilities to the list | |||
"StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration", | |||
"ImmobilizeDuration", "LiftDuration", "DebuffDuration", "HoldInPlaceDuration", "ExplodeDebuffDuration", "SlowPercent", | |||
"DebuffAccuracy", "TimeSlowDuration", "LateCheckoutStun", "BulletDamageAmpDuration", "BulletResistReduction", "BurnDuration", | |||
"EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "StealDuration", "MaxSlowDuration", "EffectDuration", | |||
"BleedDuration", "VenomDuration", "TossDuration", "VictimStackDuration", "MovementSpeedSlow", "MovementSlowPct", "HealAmpReceivePenaltyPercent", | |||
-- can also just list abilities if they don't have a reusable property | |||
"Life Drain", "Combo", "Light Eater", "Spirit Lasso", "Card Trick", "Stalker's Mark", | |||
}, | |||
["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 = { | |||
local | |||
["charges"] = { | ["charges"] = { | ||
["Jar of Dead"] = "Charges by being near when anything dies.", | |||
}, | }, | ||
["melee"] = { | ["melee"] = { | ||
["Puddle Punch"] = "Can be parried.", | |||
}, | }, | ||
[" | ["debuffresist"] = { | ||
["Aura of Suffering"] = "T1 slow, T3 debuff; effect is negligible", | |||
["Hyper Beam"] = "effect is negligible", | |||
["Lightning Ball"] = "effect is negligible", | |||
["Static Charge"] = "stun duration only", | |||
["Hotel Guest"] = "post-checkout Time-Slow + T2 stun", | |||
["Grapple Arm"] = "T1 debuff", | |||
["Binding Word"] = "immobilize duration, escape time is not affected", | |||
["Soul Exchange"] = "cast micro-slow + T2 silence", | |||
["Heavy Barrage"] = "T1 slow + dash slow", | |||
}, | }, | ||
[" | ["moveslow"] = { | ||
["Grapple Arm"] = "Applies a short strong slow when the victim reaches Bebop.", | |||
["Heavy Barrage"] = "Slows McGinnis by unknown amount.", | |||
}, | }, | ||
[" | ["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. | |||
"Frozen Shelter", -- has SlowPercent but no duration to shave off. | |||
}, | }, | ||
["moveslow"] = { | ["moveslow"] = { | ||
"Riposte", -- has a 40% 4s slow somehow? (SlowPercent + SlowDuration) | |||
"Grasping Hands", -- has SlowPercent | |||
}, | }, | ||
["dashslow"] = { | ["dashslow"] = { | ||
"Itani Lo Sahn", -- has GroundDashReductionPercent but also disables stamina. | |||
}, | |||
["heal"] = { | |||
"Riposte", -- has AbilityLifestealPercentHero, but doesn't deal damage to heal off of. | |||
}, | }, | ||
[" | ["dispelmagic"] = { | ||
"Bookwyrm", -- has DebuffDuration for some reason | |||
"Slam Fire", -- has DebuffDuration for some reason | |||
}, | }, | ||
} | } | ||
-- 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 | ||
-- ============================================================ | -- ============================================================ | ||
local function num_val(v) | |||
if type(v) == "number" then return v end | |||
local function | if type(v) == "table" then return v["Value"] end | ||
if | return nil | ||
end | |||
local function find_in_upgrades(ability, field) | |||
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 | ||
if | return nil | ||
end | |||
local function find_first_value(ability, fields) | |||
for _, field in ipairs(fields) do | |||
local v = num_val(ability[field]) | |||
if v and v ~= 0 then return field, nil, math.abs(v) end | |||
end | |||
local upgrades = ability["Upgrades"] | |||
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 | |||
return nil, nil, nil | |||
end | |||
-- ============================================================ | |||
-- Human-note formatters | |||
-- ============================================================ | |||
-- Capitalises the first letter of a human note before merging. | |||
-- Only used for full-table mode. | |||
local function human_note_full(note) | |||
if not note or note == "" then return note end | |||
return note:sub(1,1):upper() .. note:sub(2) | |||
end | |||
-- Ensures the final merged Notes string ends with "." for full-table mode. | |||
-- Applied after all merging is done, so it covers auto-only, human-only, and combined. | |||
local function ensure_period(s) | |||
if not s or s == "" then return s end | |||
if s:sub(-1) ~= "." then s = s .. "." end | |||
return s | |||
end | |||
-- Compact mode: decapitalise first letter unless it looks like a tier prefix | |||
-- (e.g. "T1", "T2") — i.e. uppercase letter immediately followed by a digit. | |||
-- Also strips any trailing ".". | |||
local function human_note_compact(note) | |||
if not note or note == "" then return note end | |||
local first = note:sub(1,1) | |||
if first:match("%u") and not note:sub(2,2):match("%d") then | |||
note = first:lower() .. note:sub(2) | |||
end | end | ||
if note:sub(-1) == "." then note = note:sub(1, -2) end | |||
return note | |||
end | end | ||
-- | -- ============================================================ | ||
-- | -- Filtering | ||
-- | -- ============================================================ | ||
local function | |||
if | local function filter_abilities(stat, abilities, key_lookup) | ||
local | local excluded = {} | ||
if exclude_list[stat] then | |||
for _, name in ipairs(exclude_list[stat]) do | |||
excluded[name] = true | |||
end | |||
end | |||
local result = {} | |||
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 | |||
if stat == "dispelmagic" and info.abilityNum == 4 then | |||
skip = true | |||
end | |||
if not skip then | |||
table.insert(result, ability) | |||
end | |||
end | end | ||
end | end | ||
return result | |||
end | end | ||
-- ============================================================ | -- ============================================================ | ||
-- | -- Per-stat cell renderers | ||
-- ============================================================ | |||
-- | |||
local debuff_short_name = { | |||
["StunDuration"] = "stun", | |||
["SleepDuration"] = "sleep", | |||
["SilenceDuration"] = "silence", | |||
["SilenceDebuff"] = "silence", | |||
["PetrifyDuration"] = "petrify", | |||
["HexDuration"] = "hex", | |||
["ImmobilizeDuration"] = "immobilize", | |||
["LiftDuration"] = "lift", | |||
["HoldInPlaceDuration"] = "immobilize", | |||
["RestrictionDuration"] = "immobilize", | |||
["SlowPercent"] = "slow", | |||
["EnemySlowPct"] = "slow", | |||
["MoveSlowPercent"] = "slow", | |||
["GroundDashReductionPercent"] = "dash slow", | |||
["SlowDuration"] = "slow", | |||
["MaxSlowDuration"] = "slow", | |||
["BulletDamageAmpDuration"] = "bullet amp", | |||
["BulletResistReduction"] = "bullet resist reduction", | |||
["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 the record | |||
-- excluding the Upgrades key. Mirrors GameData.record_matches_prop logic. | |||
local function prop_in_base(record, prop) | |||
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 | |||
local function | -- Renderer shared by debuffresist and dispelmagic. | ||
-- Outputs a Notes cell describing which debuffs are added only by upgrades. | |||
-- If any prop from the stat's list is found in the base ability, outputs nothing. | |||
-- Otherwise lists per-tier debuff names: "T2 slow", "T1 slow, T2 silence", etc. | |||
local function debuff_cells(ability, stat) | |||
local props = stat_to_props[stat] | |||
local cells = {} | |||
-- If any prop exists in base, this is a base-level debuff — no note needed. | |||
for _, prop in ipairs(props) do | |||
for _, | if prop_in_base(ability, prop) then | ||
if | return cells | ||
end | end | ||
end | end | ||
local | local upgrades = ability["Upgrades"] | ||
if type(upgrades) ~= "table" then return cells end | |||
local | -- Collect debuff short names per upgrade tier. | ||
for | local tier_debuffs = {} | ||
if type( | local seen_names = {} -- deduplicate short names across all tiers | ||
local | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
local names_this_tier = {} | |||
local seen_this_tier = {} | |||
if | for _, prop in ipairs(props) do | ||
local v = tier[prop] | |||
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 | |||
if #names_this_tier > 0 then | |||
tier_debuffs[i] = names_this_tier | |||
end | end | ||
end | end | ||
end | end | ||
-- | -- Build note in tier order. Use sorted keys since pairs() order is not guaranteed. | ||
local | local tier_indices = {} | ||
for | for i in pairs(tier_debuffs) do | ||
if | table.insert(tier_indices, i) | ||
local | end | ||
table.sort(tier_indices) | |||
if | |||
and ( | local parts = {} | ||
for _, i in ipairs(tier_indices) do | |||
table.insert(parts, "T" .. i .. " " .. table.concat(tier_debuffs[i], " + ")) | |||
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 | |||
else | |||
local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge") | |||
if cd and cd.value and cd.value > 0 then upgrade_cooldown = cd.value end | |||
end | |||
local cells = {} | |||
if has_base then | |||
cells["Charges"] = tostring(base_charges) | |||
if cooldown then | |||
cells["Time Between Charges"] = tostring(cooldown) | |||
end | |||
end | |||
local note_parts = {} | |||
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 | |||
end | |||
table | return notes | ||
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 | |||
cells["Heal Reduction"] = "100%" .. (first_tier and " (T" .. first_tier .. ")" or "") | |||
elseif first_pct then | |||
cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "") | |||
end | end | ||
table | local upgrades = ability["Upgrades"] | ||
if type(upgrades) == "table" then | |||
local running = first_pct | |||
local first_pct_seen = (first_tier == nil) | |||
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 | |||
return | if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end | ||
return cells | |||
end | end | ||
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 | |||
if type(tier) == "table" then | |||
if first_tier == i then | |||
seen_first = true | |||
elseif seen_first then | |||
local delta = nil | |||
for _, field in ipairs(fields) do | |||
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 | ||
return | if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end | ||
return cells | |||
end | end | ||
local function moveslow_cells(ability) | |||
local cells = val_stat_cells(ability, "Move Slow", stat_to_props["moveslow"], "%") | |||
local | |||
if | local channel_slow = num_val(ability["ChannelSlowPercent"]) | ||
return | if channel_slow and channel_slow ~= 0 then | ||
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) | |||
local props = stat_to_props["dashslow"] | |||
local caster_dash_slow = nil | |||
local base_ground = num_val(ability["GroundDashReductionPercent"]) | |||
if base_ground and base_ground ~= 0 then | |||
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 | local cells = val_stat_cells(ability, "Dash Slow", props, "%") | ||
if | if caster_dash_slow then | ||
local note = "Gives " .. caster_dash_slow .. "% dash slow for the caster" | |||
cells["Notes"] = cells["Notes"] and (cells["Notes"] .. "; " .. note) or note | |||
end | end | ||
local | return cells | ||
end | |||
local extra_columns = { | |||
["charges"] = { | |||
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, | |||
}, | |||
} | |||
-- ============================================================ | |||
-- Hero lookup | |||
-- ============================================================ | |||
local _key_lookup = nil | |||
local _name_to_key = nil | |||
return | 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 215: | Line 739: | ||
local stat = mw.text.trim(frame.args[1] or ""):lower() | local stat = mw.text.trim(frame.args[1] or ""):lower() | ||
local internal_keys = | local internal_keys = stat_to_props[stat] | ||
if not internal_keys then | if not internal_keys then | ||
return '<span class="error">AbilityTable: unknown | return '<span class="error">AbilityTable: unknown stat "' .. stat .. '".</span>' | ||
end | end | ||
| Line 228: | Line 752: | ||
local compact = mw.text.trim(frame.args["compact"] or "") ~= "" | local compact = mw.text.trim(frame.args["compact"] or "") ~= "" | ||
local | local key_lookup, _ = build_lookup() | ||
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 = | local stat_notes = human_notes[stat] | ||
local | local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) | ||
local filtered = filter_abilities(stat, abilities, key_lookup) | |||
if #filtered == 0 then | if #filtered == 0 then | ||
return "No abilities found for | return "No abilities found for stat: " .. stat | ||
end | end | ||
local | table.sort(filtered, function(a, b) | ||
local | local ia = key_lookup[a["Key"]] | ||
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. " | ||
.. "[" .. tostring(edit_url) .. " Click here]" | |||
.. | .. " to add notes or correct the contents." | ||
.. | .. "</div>" | ||
.. | |||
) | ) | ||
| Line 262: | Line 787: | ||
local hero_order = {} | local hero_order = {} | ||
local hero_abilities = {} | local hero_abilities = {} | ||
for _, | for _, ability in ipairs(filtered) do | ||
local | local info = key_lookup[ability["Key"]] | ||
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], | table.insert(hero_abilities[hero], ability) | ||
end | end | ||
| Line 280: | Line 806: | ||
local extra = get_cells(ability) | local extra = get_cells(ability) | ||
if stat_notes and stat_notes[name] then | |||
local human = human_note_compact(stat_notes[name]) | |||
extra["Notes"] = extra["Notes"] and (extra["Notes"] .. "; " .. human) or human | |||
end | |||
local note = extra["Notes"] | local note = extra["Notes"] | ||
| Line 306: | Line 835: | ||
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 out = {} | local out = {} | ||
table.insert(out, disclaimer) | table.insert(out, disclaimer) | ||
table.insert(out, '{| class=" | table.insert(out, '{| class="wikitable sortable ability-table"') | ||
local header_row = '! class="ability-table-col-hero" | Hero' | local header_row = '! class="ability-table-col-hero" | Hero' | ||
| Line 327: | Line 848: | ||
table.insert(out, header_row) | table.insert(out, header_row) | ||
for _, ability in ipairs(filtered) do | |||
for _, | local info = key_lookup[ability["Key"]] | ||
local | |||
local hero_cell | |||
if info then | |||
hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { info.heroName } } | |||
else | |||
hero_cell = "—" | |||
end | |||
local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { ability["Name"] } } | |||
local ability_cell = frame:expandTemplate{ | |||
local extra = get_cells(ability) | local extra = get_cells(ability) | ||
if stat_notes and stat_notes[ability["Name"]] then | |||
local human = human_note_full(stat_notes[ability["Name"]]) | |||
extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human) or human | |||
end | end | ||
extra["Notes"] = ensure_period(extra["Notes"]) | |||
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 | ||
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 | ||