Module:GameData: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Remove redundant caching wrappers, mw.loadJsonData already caches (with help from vergir-bot LLM) |
Support "Key:Value" and dot-path prop targets in entity matching (with help from vergir-bot LLM) |
||
| (8 intermediate revisions by 3 users not shown) | |||
| Line 1: | Line 1: | ||
-- Provides generic access to game data files | -- Provides generic access to game data files | ||
local p = {} | local p = {} | ||
| Line 11: | Line 10: | ||
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 18: | Line 24: | ||
-- - 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 29: | Line 38: | ||
-- - 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 v = v["Value"] end | 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 | |||
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 ~= " | if type(v) == "table" and k ~= "DisabledStateMask" 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 49: | Line 186: | ||
-- 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 | -- given properties. A property may be a key name (the property exists with a | ||
-- non-zero value), a plain string value anywhere in the record (e.g. "melee" | |||
-- matching Scale.Type = "melee"), a "Key:Value" pair, or a dotted path with an | |||
-- 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 60: | Line 198: | ||
for _, record in pairs(data) do | for _, record in pairs(data) do | ||
if is_active(record) | if is_active(record) and p.entity_matches(record, properties) then | ||
table.insert(results, record) | |||
end | end | ||
end | end | ||
| Line 81: | Line 214: | ||
-- Find an entity by display name or internal key. | -- Find an entity by display name or internal key. | ||
-- Uses ResourceLookup type field to go directly to the right dataset. | -- Uses ResourceLookup type field to go directly to the right dataset. | ||
-- Returns (entity_record, type_string) or (nil, nil). | -- Returns (entity_record, type_string, internal_key) or (nil, nil, nil). | ||
local function find_entity(identifier) | local function find_entity(identifier) | ||
local resource = mw.loadJsonData("Data:ResourceLookup.json")[identifier:lower()] | local resource = mw.loadJsonData("Data:ResourceLookup.json")[identifier:lower()] | ||
| Line 94: | Line 227: | ||
end | end | ||
if data and data[resource.key] then | if data and data[resource.key] then | ||
return data[resource.key], resource.type | return data[resource.key], resource.type, resource.key | ||
end | end | ||
end | end | ||
| Line 106: | Line 239: | ||
for _, ds in ipairs(datasets) do | for _, ds in ipairs(datasets) do | ||
local data = mw.loadJsonData(ds[1]) | local data = mw.loadJsonData(ds[1]) | ||
if data[identifier] then return data[identifier], ds[2] end | if data[identifier] then return data[identifier], ds[2], identifier end | ||
end | end | ||
return nil, nil | return nil, nil, 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 118: | Line 252: | ||
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 | ||
element = element[ | local next = 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 127: | Line 266: | ||
end | end | ||
-- {{#invoke:GameData|get_prop|ENTITY_NAME|PROPERTY}} | -- Looks up a console variable in Data:Convars.json. A convar value is stored | ||
-- Finds entity via ResourceLookup, returns the raw value (no formatting). | -- either as a plain scalar (e.g. "adsp_alley_min": 122) or as a table holding | ||
-- the value plus a description (e.g. { value = 75, description = "..." }), in | |||
-- which case only the value is returned. The exact key is tried first, then a | |||
-- 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 | local arg1 = frame.args[1] | ||
local | local arg2 = frame.args[2] | ||
if not name | local arg3 = frame.args[3] | ||
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 = find_entity(name) | local entity, etype, ekey = find_entity(name) | ||
if not entity then | if not entity then | ||
return '<span style="color:red;">Entity not found: ' .. name .. '</span>' | return '<span style="color:red;">Entity not found: ' .. name .. '</span>' | ||
end | |||
-- Street Brawl changes a handful of ability properties. The overlay lists | |||
-- only what changed, so a miss here just means "unchanged" and the base | |||
-- record answers instead. That includes the empty string resolve_prop | |||
-- returns for a table with no Value field: the overlay records are partial, | |||
-- so an overridden table often holds nothing but a nested Scale. | |||
if arg3 and arg3 ~= "" then | |||
local mode = mw.ustring.lower(mw.text.trim(arg3)) | |||
mode = mode:gsub("%s+", "") | |||
if mode ~= "streetbrawl" then | |||
return '<span style="color:red;">Unknown variant: ' .. arg3 .. '</span>' | |||
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 | ||
local result = resolve_prop(entity, prop) | local result = resolve_prop(entity, prop) | ||
if result ~= nil then return result end | if result ~= nil then return result end | ||
return '<span style="color:red;">Prop not found: ' .. name .. '/' .. prop .. '</span>' | return '<span style="color:red;">Prop not found: ' .. name .. '/' .. prop .. '</span>' | ||
Latest revision as of 00:05, 10 August 2026
Documentation for this module may be created at Module:GameData/doc
-- Provides generic access to game data files
local p = {}
-- Dataset descriptors. Values are the data file paths passed to mw.loadJsonData.
-- Callers should use these constants rather than raw strings.
p.Dataset = {
ITEMS = "Data:ItemData.json",
ABILITIES = "Data:AbilityData.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.
-- Applies consistently across all datasets:
-- - IsDisabled must not be true
-- - Name must not be nil
-- - 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)
if record["IsDisabled"] == true then return false end
if record["Name"] == nil then return false end
if record["IsSelectable"] ~= nil and record["IsSelectable"] == false then return false end
return true
end
-- Recursively searches a record for a match against prop, which may be:
-- - a key name: matches if the key exists with a non-zero/non-empty value
-- - a plain value: matches if the string appears as a value anywhere in the record
-- 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)
for k, v in pairs(record) do
-- Key match: property name exists with a meaningful value
if k == prop then
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
if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
return true
end
end
if type(v) == "table" and k ~= "DisabledStateMask" then
if record_matches_prop(v, prop) then return true end
elseif type(v) == "string" then
-- Value match: prop string appears as a value (e.g. Scale.Type = "melee")
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
return false
end
-- 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
-- non-zero value), a plain string value anywhere in the record (e.g. "melee"
-- matching Scale.Type = "melee"), a "Key:Value" pair, or a dotted path with an
-- optional value ("Scale.Type:cooldown"). See p.entity_matches for details.
-- @param dataset string one of the GameData.Dataset constants
-- @param properties table array of property strings
-- @return table array of matching entity records
function p.get_entities(dataset, properties)
local data = mw.loadJsonData(dataset)
local results = {}
for _, record in pairs(data) do
if is_active(record) and p.entity_matches(record, properties) then
table.insert(results, record)
end
end
return results
end
--------------------------------------------------------------------------------
-- Unified property lookup: get_prop
-- Uses ResourceLookup to route directly to the correct dataset.
-- Returns raw values for use in templates and expressions.
--------------------------------------------------------------------------------
-- Find an entity by display name or internal key.
-- Uses ResourceLookup type field to go directly to the right dataset.
-- Returns (entity_record, type_string, internal_key) or (nil, nil, nil).
local function find_entity(identifier)
local resource = mw.loadJsonData("Data:ResourceLookup.json")[identifier:lower()]
if resource then
local data
if resource.type == "ability" then
data = mw.loadJsonData(p.Dataset.ABILITIES)
elseif resource.type == "hero" then
data = mw.loadJsonData(p.Dataset.HEROES)
elseif resource.type == "item" then
data = mw.loadJsonData(p.Dataset.ITEMS)
end
if data and data[resource.key] then
return data[resource.key], resource.type, resource.key
end
end
-- Fallback: try as direct internal key
local datasets = {
{ p.Dataset.ABILITIES, "ability" },
{ p.Dataset.HEROES, "hero" },
{ p.Dataset.ITEMS, "item" },
}
for _, ds in ipairs(datasets) do
local data = mw.loadJsonData(ds[1])
if data[identifier] then return data[identifier], ds[2], identifier end
end
return nil, nil, nil
end
-- 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)
if not prop or prop == "" then return nil end
local element = tbl
for segment in string.gmatch(prop, "[^%.]+") do
if type(element) ~= "table" then return nil end
local next = 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
end
if type(element) == "table" then
return element.Value or ""
end
return element
end
-- Looks up a console variable in Data:Convars.json. A convar value is stored
-- either as a plain scalar (e.g. "adsp_alley_min": 122) or as a table holding
-- the value plus a description (e.g. { value = 75, description = "..." }), in
-- which case only the value is returned. The exact key is tried first, then a
-- 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)
local arg1 = frame.args[1]
local arg2 = frame.args[2]
local arg3 = frame.args[3]
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)
if not entity then
return '<span style="color:red;">Entity not found: ' .. name .. '</span>'
end
-- Street Brawl changes a handful of ability properties. The overlay lists
-- only what changed, so a miss here just means "unchanged" and the base
-- record answers instead. That includes the empty string resolve_prop
-- returns for a table with no Value field: the overlay records are partial,
-- so an overridden table often holds nothing but a nested Scale.
if arg3 and arg3 ~= "" then
local mode = mw.ustring.lower(mw.text.trim(arg3))
mode = mode:gsub("%s+", "")
if mode ~= "streetbrawl" then
return '<span style="color:red;">Unknown variant: ' .. arg3 .. '</span>'
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
local result = resolve_prop(entity, prop)
if result ~= nil then return result end
return '<span style="color:red;">Prop not found: ' .. name .. '/' .. prop .. '</span>'
end
return p