Editing Module:GameData

Jump to navigation Jump to search
Warning: You are not logged in. Once you make an edit, a temporary account will be created for you. Learn more. Log in or create an account to continue receiving notifications after this account expires, and to access other features.
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:
 
-- Module:GameData
-- Provides generic access to game data files
-- Provides generic access to game data files, with consistent active-entity filtering.
-- Used by ItemTables, and intended for future AbilityTables, HeroTables, etc.


local p = {}
local p = {}
Line 10: Line 11:
     ABILITIES = "Data:AbilityData.json",
     ABILITIES = "Data:AbilityData.json",
     HEROES    = "Data:HeroData.json",
     HEROES    = "Data:HeroData.json",
    CONVARS  = "Data:Convars.json",
}
}
-- Street Brawl ships a partial overlay of ability properties rather than a full
-- dataset, so it is deliberately kept out of p.Dataset: its top-level keys are
-- sections ("ability-changes", "item-buckets"), not entity records, and it is
-- not usable with get_entities. Only get_prop consults it.
local STREET_BRAWL = "Data:StreetBrawlData.json"


-- Returns true if a record represents an active, usable entity.
-- Returns true if a record represents an active, usable entity.
Line 24: Line 18:
--  - Name must not be nil
--  - Name must not be nil
--  - IsSelectable, if present, must not be false
--  - IsSelectable, if present, must not be false
-- Specifically for hero abilities, some of them are split into two entities, eg. Vexing Bolt has a sub-ability: Redirect Bolt.
-- The secondary sub-abilities most often have nil name and get filtered out by this function
local function is_active(record)
local function is_active(record)
     if record["IsDisabled"] == true then return false end
     if record["IsDisabled"] == true then return false end
Line 38: Line 29:
--  - a plain value: matches if the string appears as a value anywhere in the record
--  - a plain value: matches if the string appears as a value anywhere in the record
-- Returns true if a match is found, false otherwise.
-- Returns true if a match is found, false otherwise.
-- This handles plain single-word targets; the "Key:Value" and dotted-path forms
-- are handled by record_matches_path below.
local function record_matches_prop(record, prop)
local function record_matches_prop(record, prop)
     for k, v in pairs(record) do
     for k, v in pairs(record) do
         -- Key match: property name exists with a meaningful value
         -- Key match: property name exists with a meaningful value
         if k == prop then
         if k == prop then
             if type(v) == "table" then
             if type(v) == "table" then v = v["Value"] end
                if v["Value"] ~= nil then
                    v = v["Value"]
                else
                    for _ in pairs(v) do return true end
                    return false
                end
            end
             if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
             if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
                 return true
                 return true
             end
             end
         end
         end
         if type(v) == "table" and k ~= "DisabledStateMask" then
         if type(v) == "table" and k ~= "AutoRegisterModifierValueFromAbilityPropertyName" then
             if record_matches_prop(v, prop) then return true end
             if record_matches_prop(v, prop) then return true end
         elseif type(v) == "string" then
         elseif type(v) == "string" then
             -- Value match: prop string appears as a value (e.g. Scale.Type = "melee")
             -- Value match: prop string appears as a value (e.g. Scale.Type = "melee")
             if v == prop then return true end
             if v == prop then return true end
        end
    end
    return false
end
-- True if a value counts as "present": non-zero, non-empty, and not a
-- placeholder distance. A table is judged by its .Value field when it has one,
-- otherwise by whether it holds anything at all.
local function value_is_meaningful(v)
    if type(v) == "table" then
        if v["Value"] ~= nil then
            v = v["Value"]
        else
            for _ in pairs(v) do return true end
            return false
        end
    end
    return v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m"
end
-- True if a value equals `expected`, which must already be lower-cased. A table
-- is unwrapped to its .Value field, so "Damage:90" matches
-- { Value = 90, Scale = {...} }. A table with no .Value never matches, which is
-- what keeps "Damage:spirit" from matching on a nested Damage.Scale.Type.
local function value_equals(v, expected)
    if type(v) == "table" then v = v["Value"] end
    if v == nil or type(v) == "table" then return false end
    return mw.ustring.lower(tostring(v)) == expected
end
-- Follows path segments from `index` onward, starting at `node`. Segments after
-- the first are followed strictly: no searching, one step per segment. Numeric
-- segments fall back to array indices ("Upgrades.1.Damage"), and array elements
-- are stepped through transparently, so "Upgrades.Damage" reaches the Damage
-- field of any upgrade entry.
local function follow_path(node, segments, index, value)
    if index > #segments then
        if value ~= nil then return value_equals(node, value) end
        return value_is_meaningful(node)
    end
    if type(node) ~= "table" then return false end
    local segment = segments[index]
    local next_node = node[segment]
    if next_node == nil then
        local num = tonumber(segment)
        if num then next_node = node[num] end
    end
    if next_node ~= nil and follow_path(next_node, segments, index + 1, value) then
        return true
    end
    for _, element in ipairs(node) do
        if type(element) == "table" and follow_path(element, segments, index, value) then
            return true
        end
    end
    return false
end
-- Recursively locates the first path segment at any depth, then follows the rest
-- from there. Searching for the first segment is what lets a path reach into
-- sub-ability records the same way plain key targets already do.
local function record_matches_path(record, segments, value)
    for k, v in pairs(record) do
        if k == segments[1] and follow_path(v, segments, 2, value) then
            return true
        end
        if type(v) == "table" and k ~= "DisabledStateMask" then
            if record_matches_path(v, segments, value) then return true end
        end
    end
    return false
end
-- Splits a target into path segments plus an optional expected value. The first
-- colon separates the two halves, so dots inside the value are safe
-- ("Radius:3.5"). Returns nil for a plain single-word target with no colon,
-- which keeps its existing key-name-or-string-value meaning.
local function parse_target(prop)
    if type(prop) ~= "string" then return nil end
    local keys, value = prop, nil
    local colon = prop:find(":", 1, true)
    if colon then
        keys  = mw.text.trim(prop:sub(1, colon - 1))
        value = mw.text.trim(prop:sub(colon + 1))
        if keys == "" or value == "" then return nil end
        value = mw.ustring.lower(value)
    end
    local segments = {}
    for segment in keys:gmatch("[^%.]+") do table.insert(segments, segment) end
    if #segments == 0 then return nil end
    if #segments == 1 and value == nil then return nil end
    return segments, value
end
-- Returns true if a record matches at least one of the given properties. A
-- property may take any of four forms:
--  "Key"          the key exists anywhere in the record with a meaningful
--                  value, or the string appears as a value anywhere
--  "Key:Value"    the key exists anywhere and its own value equals Value,
--                  compared case-insensitively
--  "A.B.C"        that exact nested chain exists with a meaningful value
--  "A.B.C:Value"  that chain exists and ends in Value
-- Only the first segment of a path is searched for; the rest are followed one
-- step at a time, so a value nested deeper under the key does not match. A
-- structured target that resolves to nothing falls back to plain matching, so a
-- value that happens to contain a dot ("38.1 50.8") still works as a target.
-- Public so other modules (e.g. Module:AbilityTable membership) can reuse it.
function p.entity_matches(record, properties)
    for _, prop in ipairs(properties) do
        local segments, value = parse_target(prop)
        if segments and record_matches_path(record, segments, value) then
            return true
        elseif record_matches_prop(record, prop) then
            return true
         end
         end
     end
     end
Line 186: Line 49:


-- Returns all active entities from a dataset that match at least one of the
-- Returns all active entities from a dataset that match at least one of the
-- given properties. A property may be a key name (the property exists with a
-- given properties. A property may be matched either as a key name (the
-- non-zero value), a plain string value anywhere in the record (e.g. "melee"
-- property exists with a non-zero value) or as a plain string value anywhere
-- matching Scale.Type = "melee"), a "Key:Value" pair, or a dotted path with an
-- in the record (e.g. "melee" matching Scale.Type = "melee").
-- optional value ("Scale.Type:cooldown"). See p.entity_matches for details.
-- @param  dataset    string  one of the GameData.Dataset constants
-- @param  dataset    string  one of the GameData.Dataset constants
-- @param  properties  table  array of property strings
-- @param  properties  table  array of property strings
Line 198: Line 60:


     for _, record in pairs(data) do
     for _, record in pairs(data) do
         if is_active(record) and p.entity_matches(record, properties) then
         if is_active(record) then
            table.insert(results, record)
            for _, prop in ipairs(properties) do
                if record_matches_prop(record, prop) then
                    table.insert(results, record)
                    break
                end
            end
         end
         end
     end
     end
Line 208: Line 75:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Unified property lookup: get_prop
-- Unified property lookup: get_prop
-- Uses ResourceLookup to route directly to the correct dataset.
-- Searches Abilities → Heroes → Items, returns raw values for use in templates.
-- Returns raw values for use in templates and expressions.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Find an entity by display name or internal key.
-- Lazy-loaded data caches
-- Uses ResourceLookup type field to go directly to the right dataset.
local _ability_data, _item_data, _hero_data, _resource_lookup
-- Returns (entity_record, type_string, internal_key) or (nil, nil, nil).
 
local function find_entity(identifier)
local function load_ability_data()
     local resource = mw.loadJsonData("Data:ResourceLookup.json")[identifier:lower()]
     if not _ability_data then _ability_data = mw.loadJsonData(p.Dataset.ABILITIES) end
     if resource then
     return _ability_data
        local data
end
        if resource.type == "ability" then
local function load_item_data()
            data = mw.loadJsonData(p.Dataset.ABILITIES)
    if not _item_data then _item_data = mw.loadJsonData(p.Dataset.ITEMS) end
        elseif resource.type == "hero" then
    return _item_data
            data = mw.loadJsonData(p.Dataset.HEROES)
end
        elseif resource.type == "item" then
local function load_hero_data()
            data = mw.loadJsonData(p.Dataset.ITEMS)
    if not _hero_data then _hero_data = mw.loadJsonData(p.Dataset.HEROES) end
        end
    return _hero_data
        if data and data[resource.key] then
end
            return data[resource.key], resource.type, resource.key
 
        end
-- Find an ability by key or display name (mirrors Module:Abilities logic)
local function find_ability(identifier)
    local data = load_ability_data()
    if data[identifier] then return data[identifier] end
    if not _resource_lookup then
        _resource_lookup = mw.loadJsonData("Data:ResourceLookup.json")
     end
     end
    local resource = _resource_lookup[identifier:lower()]
    if resource then return data[resource.key] end
    return nil
end


    -- Fallback: try as direct internal key
-- Find an item by display name (mirrors Module:ItemData logic, prefers active)
     local datasets = {
local function find_item(name)
        { p.Dataset.ABILITIES, "ability" },
     local data = load_item_data()
         { p.Dataset.HEROES,    "hero" },
    for _, v in pairs(data) do
        { p.Dataset.ITEMS,    "item" },
         if v["Name"] == name and is_active(v) then return v end
     }
     end
     for _, ds in ipairs(datasets) do
     for _, v in pairs(data) do
        local data = mw.loadJsonData(ds[1])
         if v["Name"] == name then return v end
         if data[identifier] then return data[identifier], ds[2], identifier end
     end
     end
    return nil
end


     return nil, nil, nil
-- Find a hero by key or display name (mirrors Module:HeroData logic)
local function find_hero(identifier)
     local data = load_hero_data()
    if data[identifier] then return data[identifier] end
    for _, v in pairs(data) do
        if v["Name"] == identifier then return v end
    end
    return nil
end
end


-- Traverse a table using dot notation, unwrap tables with a .Value field.
-- Traverse a table using dot notation, unwrap tables with a .Value field
-- Supports numeric indices for arrays (e.g. "Upgrades.1.WeaponDamageBonus").
local function resolve_prop(tbl, prop)
local function resolve_prop(tbl, prop)
     if not prop or prop == "" then return nil end
     if not prop or prop == "" then return nil end
Line 252: Line 134:
     for segment in string.gmatch(prop, "[^%.]+") do
     for segment in string.gmatch(prop, "[^%.]+") do
         if type(element) ~= "table" then return nil end
         if type(element) ~= "table" then return nil end
         local next = element[segment]
         element = element[segment]
        if next == nil then
            local num = tonumber(segment)
            if num then next = element[num] end
        end
        element = next
         if element == nil then return nil end
         if element == nil then return nil end
     end
     end
Line 266: Line 143:
end
end


-- Looks up a console variable in Data:Convars.json. A convar value is stored
-- {{#invoke:GameData|get_prop|ENTITY_NAME|PROPERTY}}
-- either as a plain scalar (e.g. "adsp_alley_min": 122) or as a table holding
-- Searches Abilities → Heroes → Items. Returns the raw value (no formatting).
-- the value plus a description (e.g. { value = 75, description = "..." }), in
-- Supports dot notation for nested properties (e.g. "Scale.Value").
-- which case only the value is returned. The exact key is tried first, then a
-- Tables with a .Value field are automatically unwrapped.
-- lower-case form. Returns (value, found).
local function lookup_convar(key)
    local data = mw.loadJsonData(p.Dataset.CONVARS)
    local entry = data[key]
    if entry == nil then
        entry = data[mw.ustring.lower(key)]
    end
    if entry == nil then
        return nil, false
    end
    if type(entry) == "table" then
        return entry.value, true
    end
    return entry, true
end
 
-- {{#invoke:GameData|get_prop|...}}
-- Two modes:
--  Entity:  {{#invoke:GameData|get_prop|ENTITY_NAME|PROPERTY|VARIANT}}
--           Finds entity via ResourceLookup, returns the raw value (no
--           formatting). Supports dot notation for nested properties
--            (e.g. "Scale.Value"); tables with a .Value field are unwrapped.
--            The optional VARIANT selects a game mode variant. "Street Brawl"
--            (case and spacing insensitive) returns the value changed for
--            Street Brawl, falling back to the base value when Street Brawl
--            does not change that property.
--  Convar:  {{#invoke:GameData|get_prop|Convar|CONVAR_NAME}}
--            Looks up the convar in Data:Convars.json and returns its value,
--            unwrapping the { value, description } form when present.
function p.get_prop(frame)
function p.get_prop(frame)
     local arg1 = frame.args[1]
     local name = frame.args[1]
     local arg2 = frame.args[2]
     local prop = frame.args[2]
    local arg3 = frame.args[3]
     if not name or not prop then return "" end
     if not arg1 then return "" end
 
    -- Convar mode: first argument is the literal keyword "Convar".
    if mw.ustring.lower(arg1) == "convar" then
        if not arg2 or arg2 == "" then return "" end
        local value, found = lookup_convar(arg2)
        if not found then
            return '<span style="color:red;">Convar not found: ' .. arg2 .. '</span>'
        end
        if value == nil then return "" end
        return value
    end
 
    -- Entity mode: search abilities, heroes, and items.
    local name = arg1
    local prop = arg2
    if not prop then return "" end


     local entity, etype, ekey = find_entity(name)
    -- 1. Abilities
     if not entity then
     local entity = find_ability(name)
         return '<span style="color:red;">Entity not found: ' .. name .. '</span>'
     if entity then
         local result = resolve_prop(entity, prop)
        if result ~= nil then return result end
     end
     end


     -- Street Brawl changes a handful of ability properties. The overlay lists
     -- 2. Heroes (also checks Weapon / Weapon.AltFire)
    -- only what changed, so a miss here just means "unchanged" and the base
     entity = find_hero(name)
    -- record answers instead. That includes the empty string resolve_prop
     if entity then
     -- returns for a table with no Value field: the overlay records are partial,
         local result = resolve_prop(entity, prop)
    -- so an overridden table often holds nothing but a nested Scale.
         if result ~= nil then return result end
     if arg3 and arg3 ~= "" then
         if entity.Weapon then
         local mode = mw.ustring.lower(mw.text.trim(arg3))
             result = resolve_prop(entity.Weapon, prop)
        mode = mode:gsub("%s+", "")
             if result ~= nil then return result end
         if mode ~= "streetbrawl" then
            if entity.Weapon.AltFire then
            return '<span style="color:red;">Unknown variant: ' .. arg3 .. '</span>'
                 result = resolve_prop(entity.Weapon.AltFire, prop)
        end
                 if result ~= nil then return result end
         if etype == "ability" then
             local changed = mw.loadJsonData(STREET_BRAWL)["ability-changes"][ekey]
             if changed then
                 local value = resolve_prop(changed, prop)
                 if value ~= nil and value ~= "" then return value end
             end
             end
         end
         end
     end
     end


     local result = resolve_prop(entity, prop)
     -- 3. Items
    if result ~= nil then return result end
    entity = find_item(name)
    if entity then
        local result = resolve_prop(entity, prop)
        if result ~= nil then return result end
    end


     return '<span style="color:red;">Prop not found: ' .. name .. '/' .. prop .. '</span>'
     return ""
end
end


return p
return p
Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see Deadlock:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)
Preview page with this template

Page included on this page: