Files
modolith/mods/eye_spy/tools.lua
T

283 lines
8.9 KiB
Lua

-- eye_spy/tools.lua
-- Tool detection and dig-hint helpers.
-- Extracted from functions.lua. Runs after functions.lua, so eye_spy.S and
-- eye_spy.cg are already available when this file is executed.
local S = eye_spy.S
------------------------------------------------------------
-- Group → tool-type mappings for Minetest Game
------------------------------------------------------------
-- Maps dig group names to human-readable tool-type labels (MTG).
local GROUP_TO_TOOL_MTG = {
cracky = S("Pickaxe"),
crumbly = S("Shovel"),
choppy = S("Axe"),
snappy = S("Sword"),
dig_immediate = S("Hand"),
}
-- Priority order for MTG group lookup (most-specific first).
local GROUP_ORDER_MTG = { "dig_immediate", "snappy", "crumbly", "choppy", "cracky" }
------------------------------------------------------------
-- Group → tool-type mappings for MineClone / VoxeLibre
------------------------------------------------------------
-- Maps dig group names to human-readable tool-type labels (MCL).
local GROUP_TO_TOOL_MC = {
swordy = S("Sword"),
handy = S("Hand"),
pickaxey = S("Pickaxe"),
shovely = S("Shovel"),
axey = S("Axe"),
shearsy = S("Shears"),
dig_immediate = S("Hand"),
}
-- Priority order for MCL group lookup (most-specific first).
local GROUP_ORDER_MC = { "dig_immediate", "handy", "swordy", "shearsy", "shovely", "axey", "pickaxey" }
------------------------------------------------------------
-- Internal helpers
------------------------------------------------------------
-- Returns a shallow copy of an ipairs-iterable list.
local function clone_list(values)
local result = {}
for index, value in ipairs(values or {}) do
result[index] = value
end
return result
end
-- Appends `value` to `list` only when it is not already present.
-- Ignores nil and empty-string values.
local function append_unique(list, value)
if not value or value == "" then
return
end
for _, existing in ipairs(list) do
if existing == value then
return
end
end
list[#list + 1] = value
end
-- Returns the group→tool map and ordered group list appropriate for the
-- currently-running game. Uses eye_spy.cg (set by init.lua).
local function get_tool_mapping(game_id)
if game_id == "mineclone" then
return GROUP_TO_TOOL_MC, GROUP_ORDER_MC
end
return GROUP_TO_TOOL_MTG, GROUP_ORDER_MTG
end
-- Collects every tool-type label that applies to `node_groups`, in priority
-- order, de-duplicated.
local function collect_tool_types(node_groups, group_to_tool, group_order)
local result = {}
for _, group in ipairs(group_order) do
if node_groups[group] and group_to_tool[group] then
append_unique(result, group_to_tool[group])
end
end
return result
end
-- Returns the single highest-priority dig group and its tool-type label for
-- `node_groups`. Returns nil, S("None") when no matching group is found.
local function get_preferred_tool_group(node_groups, group_to_tool, group_order)
for _, group in ipairs(group_order) do
if node_groups[group] and group_to_tool[group] then
return group, group_to_tool[group]
end
end
return nil, S("None")
end
-- Builds a display string from the full set of required tool types and the
-- single best/preferred tool type.
--
-- Examples:
-- { "Pickaxe" }, "Pickaxe" → "Pickaxe"
-- { "Axe", "Pickaxe" }, "Pickaxe" → "Axe / Pickaxe | Pickaxe"
local function format_tool_summary(minimum_types, best_type)
local minimum_label = table.concat(minimum_types or {}, " / ")
if best_type and best_type ~= "" and best_type ~= S("None") and best_type ~= S("Unknown") then
if minimum_label == "" then
return best_type
end
-- Single entry that matches best_type exactly — no need to repeat it.
if #minimum_types == 1 and minimum_types[1] == best_type then
return best_type
end
-- If best_type is already listed in minimum_types, just show the list.
for _, minimum_type in ipairs(minimum_types or {}) do
if minimum_type == best_type then
return minimum_label
end
end
-- best_type is not in the minimum list — append it after a separator.
return minimum_label .. " | " .. best_type
end
if minimum_label ~= "" then
return minimum_label
end
return S("Unknown")
end
-- Returns true when `itemdef` has groupcaps for `preferred_group`.
-- dig_immediate is never considered a "match" because any item can break those
-- nodes instantly.
local function tool_matches_group(itemdef, preferred_group)
if not preferred_group or preferred_group == "dig_immediate" then
return false
end
local caps = itemdef and itemdef.tool_capabilities
local groupcaps = caps and caps.groupcaps
return groupcaps and groupcaps[preferred_group] ~= nil or false
end
------------------------------------------------------------
-- Public API
------------------------------------------------------------
-- Returns whether `toolstack` can dig `nodename`, plus the dig time.
-- Returns false, nil when `toolstack` is nil or the node is unknown.
function eye_spy.tool_can_dig_node(toolstack, nodename)
-- Nil-guard: no stack means no digging ability.
if not toolstack then
return false, nil
end
local ndef = minetest.registered_nodes[nodename]
if not ndef then
return false, nil
end
local tool_caps = toolstack:get_tool_capabilities()
local node_groups = ndef.groups or {}
local dig_params = minetest.get_dig_params(node_groups, tool_caps)
return dig_params.diggable, dig_params.time
end
-- Returns a filtered copy of `source` that omits every entry whose `.name`
-- field contains any substring listed in `blocked`.
function eye_spy.filter_out_by_name(source, blocked)
local result = {}
for _, entry in ipairs(source) do
local name = entry.name or ""
local keep = true
for _, word in ipairs(blocked) do
if string.find(name, word, 1, true) then
keep = false
break
end
end
if keep then
table.insert(result, entry)
end
end
return result
end
-- Returns the highest `maxlevel` value found across all groupcaps of
-- `itemdef`. Returns 0 when `itemdef` has no tool_capabilities.
function eye_spy.get_tool_maxlevel(itemdef)
local maxlevel = 0
if not itemdef.tool_capabilities then
return 0
end
for _, gc in pairs(itemdef.tool_capabilities.groupcaps or {}) do
if gc.maxlevel and gc.maxlevel > maxlevel then
maxlevel = gc.maxlevel
end
end
return maxlevel
end
-- Returns a human-readable tool-hint string for `node_name` (or nil when no
-- hint is applicable) and a boolean indicating whether the currently wielded
-- `toolstack` is an appropriate tool for digging that node.
function eye_spy.get_node_tool_hint_and_match(node_name, toolstack)
if not node_name or node_name == "" then
return nil, false
end
local def = minetest.registered_nodes[node_name]
if not def or not def.groups then
return nil, false
end
local node_groups = def.groups
local group_to_tool, group_order = get_tool_mapping(eye_spy.cg)
local minimum_types = collect_tool_types(node_groups, group_to_tool, group_order)
local preferred_group, tooltype = get_preferred_tool_group(node_groups, group_to_tool, group_order)
local summary = format_tool_summary(minimum_types, tooltype)
-- Suppress generic/unhelpful labels.
if summary == S("Unknown") or summary == S("None") then
summary = nil
end
-- Nodes with no preferred dig group (or dig_immediate) are considered
-- compatible with any held item for colour-state purposes.
if not preferred_group or preferred_group == "dig_immediate" then
return summary, true
end
local itemdef = toolstack and toolstack.get_definition and toolstack:get_definition() or nil
local caps = itemdef and itemdef.tool_capabilities or nil
local groupcaps = caps and caps.groupcaps or nil
local matches = groupcaps and groupcaps[preferred_group] ~= nil or false
return summary, matches
end
-- Returns the effective reach range for `player` based on the wielded item
-- definition, falling back to creative-mode range (10) or default range (4).
function eye_spy.get_player_range(player)
local wield = player:get_wielded_item()
local def = wield:get_definition()
-- Nil-guard: definition may be absent for unknown item types.
if def and def.range then
return def.range
end
if minetest.is_creative_enabled(player:get_player_name()) then
return 10
end
return 4
end