Module:AbilityTable: Difference between revisions

Jump to navigation Jump to search
Vergir (talk | contribs)
mNo edit summary
Vergir (talk | contribs)
Drop Effects column from debuff; manual_entries becomes array of names; restore key-based lookup; name→key resolution for manuals (with help from vergir-bot LLM)
Line 25: Line 25:
}
}


-- Manually specified entries for abilities whose effects cannot be inferred from
-- Manually specified abilities to include even if they match no props.
-- data alone (e.g. engine-side CC with no matching data fields).
-- Listed by ability name; the key and hero are resolved from AbilityCards.json.
-- The hero is resolved automatically from AbilityCards.json via the ability name.
-- Each entry requires:
--  effects = { "EffectName", ... }  (supports "Effect (TN)" for upgrade-only)
local manual_entries = {
local manual_entries = {
     ["debuff"] = {
     ["debuff"] = {
         ["Combo"] = {
         "Combo",
            effects = { "Stun" },
         "Sand Blast",
         },
         "Spirit Lasso",
        ["Sand Blast"] = {
            effects = { "Disarm" },
         },
        ["Spirit Lasso"] = {
            effects = { "Stun" },
        },
     },
     },
}
}
Line 274: Line 265:


     if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
     if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
    return cells
end
-- Maps AbilityData property names to human-readable effect names for the debuff table.
-- Props not listed here fall back to using the prop name itself as the effect label.
-- DebuffDuration is absent: it signals "(See notes)" on Duration rather than a named effect.
local debuff_prop_to_effect = {
    StunDuration        = "Stun",
    SleepDuration      = "Sleep",
    SilenceDuration    = "Silence",
    SilenceDebuff      = "Silence",
    PetrifyDuration    = "Petrify",
    HexDuration        = "Hex",
    ImmobilizeDuration  = "Root",
    LiftDuration        = "Lift",
    SlowPercent        = "Slow",
    HoldInPlaceDuration = "Stop",
    DebuffAccuracy      = "Inaccuracy",
    TimeSlowDuration    = "Time Slow",
    LateCheckoutStun    = "Stun",
}
local function debuff_cells(ability, stat_manual)
    local cells = {}
    -- Manual entry takes full precedence.
    local manual = stat_manual and stat_manual[ability["Name"]]
    if manual then
        if manual.effects and #manual.effects > 0 then
            cells["Effects"] = table.concat(manual.effects, ", ")
        end
        return cells
    end
    local effects_seen = {}  -- ordered list of { name, tier }
    local effects_set  = {}  -- effect name -> true, for dedup
    -- Iterate stat_to_props["debuff"], skipping DebuffDuration (it has no named effect).
    -- Use debuff_prop_to_effect for the display name; fall back to the prop name itself.
    local function collect_effects(obj, tier)
        for _, prop in ipairs(stat_to_props["debuff"]) do
            local val = obj[prop]
                if val and val ~= 0 then
                    local effect = debuff_prop_to_effect[prop] or prop
                    if not effects_set[effect] then
                        table.insert(effects_seen, { name = effect, tier = tier })
                        effects_set[effect] = true
                    end
                end
        end
    end
    collect_effects(ability, nil)
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
        for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then collect_effects(tier, i) end
        end
    end
    local effect_parts = {}
    for _, e in ipairs(effects_seen) do
        table.insert(effect_parts, e.tier and (e.name .. " (T" .. e.tier .. ")") or e.name)
    end
    if #effect_parts > 0 then
        cells["Effects"] = table.concat(effect_parts, ", ")
    end
     return cells
     return cells
end
end
Line 359: Line 282:
     },
     },
     ["debuff"] = {
     ["debuff"] = {
         headers  = { "Effects" },
         headers  = {},
         get_cells = debuff_cells,
         get_cells = function() return {} end,
     },
     },
}
}


-- ============================================================
-- ============================================================
-- Name lookup: ability name -> { heroName, abilityNum }
-- Hero lookup
-- Built from HeroData (active hero filter) + AbilityCards.
--  _key_lookup  : ability key  -> { heroName, abilityNum }
-- Used for both prop-matched and manual abilities.
--   _name_to_key : ability name -> ability key
-- Both built together from HeroData + AbilityCards.
-- manual_entries resolve name -> key via _name_to_key, then use _key_lookup normally.
-- ============================================================
-- ============================================================


local _name_lookup = nil
local _key_lookup  = nil
local _name_to_key = nil


local function build_name_lookup()
local function build_lookup()
     if _name_lookup then return _name_lookup end
     if _key_lookup then return _key_lookup, _name_to_key end
     _name_lookup = {}
     _key_lookup  = {}
    _name_to_key = {}


     local hero_data = mw.loadJsonData("Data:HeroData.json")
     local hero_data = mw.loadJsonData("Data:HeroData.json")
Line 395: Line 322:
                 for slot_str, ability in pairs(hero_entry) do
                 for slot_str, ability in pairs(hero_entry) do
                     local slot_num = tonumber(slot_str)
                     local slot_num = tonumber(slot_str)
                     if slot_num and type(ability) == "table" and ability["Name"] then
                     if slot_num and type(ability) == "table" then
                        _name_lookup[ability["Name"]] = {
                        local info = { heroName = hero_name, abilityNum = slot_num }
                            heroName  = hero_name,
                        if ability["Key"] then
                             abilityNum = slot_num,
                            _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
Line 406: Line 336:
     end
     end


     return _name_lookup
     return _key_lookup, _name_to_key
end
end


Line 427: Line 357:
     end
     end


     local name_lookup = build_name_lookup()
     local key_lookup, name_to_key = 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 stat_notes  = human_notes[stat]
     local stat_notes  = human_notes[stat]
     local stat_manual = manual_entries[stat]
     local stat_manual = manual_entries[stat]
    local get_cells
    if spec then
        if stat == "debuff" then
            get_cells = function(ability) return debuff_cells(ability, stat_manual) end
        else
            get_cells = spec.get_cells
        end
    else
        get_cells = function() return {} end
    end


     local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
     local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
Line 448: Line 368:
     -- Keep only abilities belonging to active, released heroes
     -- Keep only abilities belonging to active, released heroes
     local filtered = {}
     local filtered = {}
     local filtered_names = {}
     local filtered_keys = {}
     for _, ability in ipairs(abilities) do
     for _, ability in ipairs(abilities) do
         if ability["Name"] and name_lookup[ability["Name"]] then
         local key = ability["Key"]
        if key and key_lookup[key] then
             table.insert(filtered, ability)
             table.insert(filtered, ability)
             filtered_names[ability["Name"]] = true
             filtered_keys[key] = true
         end
         end
     end
     end


     -- Inject manual entries not already covered by prop-matching.
     -- Inject manual entries not already covered by prop-matching.
     -- Hero and slot are resolved via name_lookup; if the ability isn't in AbilityCards
     -- Resolve ability name -> key via name_to_key, then verify key is in key_lookup.
    -- the row shows "—" in the Hero column and sorts to the end.
     if stat_manual then
     if stat_manual then
         for name, _ in pairs(stat_manual) do
         for _, name in ipairs(stat_manual) do
             if not filtered_names[name] then
            local key = name_to_key[name]
                 table.insert(filtered, { Name = name })
             if key and key_lookup[key] and not filtered_keys[key] then
                -- Inject a minimal record; no cell renderer needs fields from it.
                 table.insert(filtered, { Key = key, Name = name })
                filtered_keys[key] = true
             end
             end
         end
         end
Line 472: Line 395:


     -- Sort by hero name, then ability slot number.
     -- Sort by hero name, then ability slot number.
    -- Abilities not in name_lookup sort to the end alphabetically.
     table.sort(filtered, function(a, b)
     table.sort(filtered, function(a, b)
         local ia = name_lookup[a["Name"]]
         local ia = key_lookup[a["Key"]]
         local ib = name_lookup[b["Name"]]
         local ib = key_lookup[b["Key"]]
         if not ia and not ib then return (a["Name"] or "") < (b["Name"] or "") end
         if not ia and not ib then return (a["Name"] or "") < (b["Name"] or "") end
         if not ia then return false end
         if not ia then return false end
Line 519: Line 441:


     for _, ability in ipairs(filtered) do
     for _, ability in ipairs(filtered) do
         local info = name_lookup[ability["Name"]]
         local info = key_lookup[ability["Key"]]


         local hero_cell
         local hero_cell