Module:GameData: Difference between revisions

Jump to navigation Jump to search
get_prop: optional third argument for Street Brawl ability values, falling back to base data when unchanged (with help from vergir-bot LLM)
Vergir (talk | contribs)
Support "Key:Value" and dot-path prop targets in entity matching (with help from vergir-bot LLM)
 
Line 38: 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
Line 64: Line 66:
end
end


-- Returns true if a record matches at least one of the given properties,
-- True if a value counts as "present": non-zero, non-empty, and not a
-- using the same key-name-or-string-value semantics as get_entities.
-- 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.
-- Public so other modules (e.g. Module:AbilityTable membership) can reuse it.
function p.entity_matches(record, properties)
function p.entity_matches(record, properties)
     for _, prop in ipairs(properties) do
     for _, prop in ipairs(properties) do
         if record_matches_prop(record, prop) then
        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
             return true
         end
         end
Line 77: 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 matched either as a key name (the
-- given properties. A property may be a key name (the property exists with a
-- property exists with a non-zero value) or as a plain string value anywhere
-- non-zero value), a plain string value anywhere in the record (e.g. "melee"
-- in the record (e.g. "melee" matching Scale.Type = "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