Module:Sandbox/Vergir: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Created blank page |
Test copy of Module:GameData with optional Street Brawl variant argument for get_prop (with help from vergir-bot LLM) |
||
| Line 1: | Line 1: | ||
-- 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. | |||
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 | |||
-- Returns true if a record matches at least one of the given properties, | |||
-- using the same key-name-or-string-value semantics as get_entities. | |||
-- Public so other modules (e.g. Module:AbilityTable membership) can reuse it. | |||
function p.entity_matches(record, properties) | |||
for _, prop in ipairs(properties) do | |||
if 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 matched either as a key name (the | |||
-- property exists with a non-zero value) or as a plain string value anywhere | |||
-- in the record (e.g. "melee" matching Scale.Type = "melee"). | |||
-- @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 | |||