Editing Module:Sandbox/VergirGive feedback
Jump to navigation
Jump to search
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: | ||
-- Provides generic access to game data files | |||
local p = {} | 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 | return p | ||