|
|
| 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") |
| ["heavymelee"] = {"heavy_melee" },
| | |
| ["wpndmg"] = { "weapon_damage_increase" },
| | local p = {} |
| ["healreduce"] = { "HealAmpReceivePenaltyPercent", "HealAmpRegenPenaltyPercent", "DisableHealing" },
| | |
| ["debuffresist"] = {
| | -- ============================================================ |
| -- Do not insert: AbilityDuration, TickRate, SlowDuration
| | -- Extra columns: maps stat names to their column headers and |
| -- they do more harm than good and include a lot of excessive abilities to the list
| | -- cell renderer functions from ComplexRenderers. |
| "StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration",
| | -- Stats not listed here get a plain Hero + Ability + Notes table. |
| "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 extra_columns = { |
| 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 = { "Light Melee Scaling", "Notes" }, |
| | get_cells = Renderers.RenderMelee, |
| }, | | }, |
| ["heavymelee"] = { | | ["healreduce"] = { |
| ["Bashdown"] = "Becomes Heavy Melee only after T3 upgrade.",
| | headers = { "Heal Reduction", "Notes" }, |
| ["Rend"] = "Becomes Heavy Melee only after T3 upgrade.",
| | get_cells = Renderers.RenderHealReduce, |
| ["Puddle Punch"] = "Becomes Heavy Melee only after T3 upgrade.",
| |
| }, | | }, |
| ["debuffresist"] = { | | ["debuffresist"] = { |
| ["Aura of Suffering"] = "T1 slow, T3 debuff; effect is negligible", | | headers = { "Notes" }, |
| ["Hyper Beam"] = "effect is negligible",
| | get_cells = function(ability) return Renderers.RenderDebuff(ability, "debuffresist") end, |
| ["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"] = { | | ["dispelmagic"] = { |
| ["Grapple Arm"] = "Applies a short strong slow when the victim reaches Bebop.", | | headers = { "Notes" }, |
| ["Heavy Barrage"] = "Slows McGinnis by unknown amount.",
| | get_cells = function(ability) return Renderers.RenderDebuff(ability, "dispelmagic") end, |
| },
| |
| ["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) | | 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"] = {
| |
| "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
| |
| if type(v) == "table" then return v["Value"] end
| |
| 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
| |
| 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
| |
|
| |
|
| -- ============================================================ | | -- ============================================================ |
| Line 196: |
Line 49: |
| -- ============================================================ | | -- ============================================================ |
|
| |
|
| -- Capitalises the first letter of a human note before merging. | | -- Capitalises the first letter of a human note before merging (full-table mode). |
| -- Only used for full-table mode.
| |
| local function human_note_full(note) | | local function human_note_full(note) |
| if not note or note == "" then return note end | | if not note or note == "" then return note end |
| Line 203: |
Line 55: |
| end | | end |
|
| |
|
| -- Ensures the final merged Notes string ends with "." for full-table mode. | | -- Ensures the final merged Notes string ends with "." (full-table mode). |
| -- Applied after all merging is done, so it covers auto-only, human-only, and combined.
| |
| local function ensure_period(s) | | local function ensure_period(s) |
| if not s or s == "" then return s end | | if not s or s == "" then return s end |
| Line 212: |
Line 63: |
|
| |
|
| -- Compact mode: decapitalise first letter unless it looks like a tier prefix | | -- 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. | | -- (e.g. "T1", "T2"). Also strips any trailing ".". |
| -- Also strips any trailing ".".
| |
| local function human_note_compact(note) | | local function human_note_compact(note) |
| if not note or note == "" then return note end | | if not note or note == "" then return note end |
| Line 230: |
Line 80: |
| local function filter_abilities(stat, abilities, key_lookup) | | local function filter_abilities(stat, abilities, key_lookup) |
| local excluded = {} | | local excluded = {} |
| if exclude_list[stat] then | | if Lists.exclude_abilities[stat] then |
| for _, name in ipairs(exclude_list[stat]) do | | for _, name in ipairs(Lists.exclude_abilities[stat]) do |
| excluded[name] = true | | excluded[name] = true |
| end | | end |
| Line 252: |
Line 102: |
| return result | | 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
| |
|
| |
| -- 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
| |
| if prop_in_base(ability, prop) then
| |
| return cells
| |
| end
| |
| end
| |
|
| |
| local upgrades = ability["Upgrades"]
| |
| if type(upgrades) ~= "table" then return cells end
| |
|
| |
| -- Collect debuff short names per upgrade tier.
| |
| local tier_debuffs = {}
| |
| local seen_names = {} -- deduplicate short names across all tiers
| |
|
| |
| for i, tier in ipairs(upgrades) do
| |
| if type(tier) == "table" then
| |
| local names_this_tier = {}
| |
| local seen_this_tier = {}
| |
| 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
| |
| if #names_this_tier > 0 then
| |
| tier_debuffs[i] = names_this_tier
| |
| end
| |
| end
| |
| end
| |
|
| |
| -- Build note in tier order. Use sorted keys since pairs() order is not guaranteed.
| |
| local tier_indices = {}
| |
| for i in pairs(tier_debuffs) do
| |
| table.insert(tier_indices, i)
| |
| end
| |
| table.sort(tier_indices)
| |
|
| |
| 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
| |
| 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
| |
|
| |
| 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
| |
|
| |
| local upgrades = ability["Upgrades"]
| |
| if type(upgrades) == "table" then
| |
| local running = first_pct
| |
| local first_pct_seen = (first_tier == nil)
| |
|
| |
| 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 cells
| |
| 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
| |
|
| |
| if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
| |
| return cells
| |
| end
| |
|
| |
| local function moveslow_cells(ability)
| |
| local cells = val_stat_cells(ability, "Move Slow", stat_to_props["moveslow"], "%")
| |
|
| |
| local channel_slow = num_val(ability["ChannelSlowPercent"])
| |
| 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
| |
|
| |
| 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 cells = val_stat_cells(ability, "Dash Slow", props, "%")
| |
|
| |
| 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
| |
|
| |
| 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,
| |
| },
| |
| }
| |
|
| |
|
| -- ============================================================ | | -- ============================================================ |
| Line 745: |
Line 158: |
| 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 762: |
Line 175: |
| 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 abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) |
| Line 768: |
Line 181: |
|
| |
|
| if #filtered == 0 then | | if #filtered == 0 then |
| return "No abilities found for stat: " .. stat | | return "No abilities found for list: " .. stat |
| end | | end |
|
| |
|
| Line 878: |
Line 291: |
| 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 |