Files
Accessibility ModandClaude Opus 5 c17c5277d4 Phase 3: object tracker
Game-file footprint unchanged: still one line in main.lua.

Three-level navigation as specified in D2: Ctrl+PageUp/Down cycles categories,
PageUp/Down cycles kinds, Shift+PageUp/Down cycles instances of that kind, Home
announces where it is.

- categories.lua: flag-first classification into Creatures, Items, Alchemy,
  Interactables, Exits and Terrain features, reusing the game's own 'exit' tag
  and excluding the six nameless mechanical templates.
- tracker.lua: scan, cache, navigation, announcements.

Caching keys on level name, player position and turn: position because
everything sorts by distance to the player, turn because creatures move and
containers empty. Scans run on keypress, never per frame.

FoV is respected throughout -- creatures only when currently visible, tiles when
visible or remembered -- so the tracker never becomes a cheat.

Home reports path length alongside the offset, via the game's own
calculatePathFromPlayer. Straight-line distance is misleading indoors, where a
chest three tiles north through a wall can be twenty steps away.

Corrects D4 in docs/05-decisions.md. I had claimed tile.tryInteract cleanly
separates 'the player can do something here' from scenery. It does not: 'wall'
carries a flavour-only tryInteractFunction ('You touch the wall. It's cold and
rough.'), so every wall segment was promoted into Interactables -- 13 in the
starting house, burying the chest and cauldron. Structural tiles
(blocksMovement and blocksVision) are now excluded before the tryInteract test;
containers and furniture block movement but not sight, so they survive.
12 kinds/19 instances instead of 13/32.

13/13 testbed checks, history unit tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:34:13 +02:00

174 lines
5.3 KiB
Lua

-- categories.lua — classifies map contents into tracker categories.
--
-- The game's own `templatesByTag` defines exactly one tag ("exit"), so the
-- tracker needs its own scheme. Per docs/05-decisions.md D4 this is deliberately
-- FLAG-FIRST rather than a hand-listed set of template IDs:
--
-- * flags are authoritative -- modules/tile.lua:234 defines
-- `tile.tryInteract = (loot or tryInteractFunction) and ...`, which is
-- exactly "the player can do something here" versus scenery;
-- * new content in a future game version falls through to a sensible category
-- instead of vanishing from the tracker.
--
-- The only hand-maintained parts are the exclusion list and the exit tag reuse.
local categories = {}
--------------------------------------------------------------------------------
-- Exclusions
--------------------------------------------------------------------------------
-- The six templates with no name, no description and no parent: invisible
-- mechanics that must never be tracked. (See docs/05-decisions.md D4.)
local excludedTemplates = {
blank = true,
throw_blocker = true,
movement_blocker = true,
fire_detector = true,
wall_features_position_dummy = true,
ladder_down_hidden = true,
}
--------------------------------------------------------------------------------
-- Category list (order is the Ctrl+PageUp/PageDown cycle order)
--------------------------------------------------------------------------------
categories.list = {
{key = "creatures", name = "Creatures"},
{key = "items", name = "Items"},
{key = "alchemy", name = "Alchemy"},
{key = "interactables", name = "Interactables"},
{key = "exits", name = "Exits"},
{key = "terrain", name = "Terrain features"},
}
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local exitTemplates
local function getExitTemplates()
if exitTemplates then
return exitTemplates
end
local ok, templatesByTag = pcall(require, "modules.tile.templatesByTag")
exitTemplates = (ok and templatesByTag and templatesByTag.exit) or {}
return exitTemplates
end
local itemTemplates
local function getItemTemplates()
if itemTemplates then
return itemTemplates
end
local ok, templates = pcall(require, "modules.item.templates")
itemTemplates = (ok and templates) or {}
return itemTemplates
end
-- Item category 3 is "Ingredients" (modules/item/categories.lua).
local INGREDIENT_CATEGORY = 3
local function lootContainsIngredient(loot)
if type(loot) ~= "table" then
return false
end
local templates = getItemTemplates()
for _, entry in ipairs(loot) do
local templateId = type(entry) == "table" and entry.templateId
local template = templateId and templates[templateId]
if template and template.category == INGREDIENT_CATEGORY then
return true
end
end
return false
end
local function isLootable(tile)
return tile.loot and tile.loot[1] and not tile.unlootable
end
--------------------------------------------------------------------------------
-- Classification
--------------------------------------------------------------------------------
-- Returns a category key, or nil if the tile should not be tracked.
function categories.classifyTile(tile)
if not tile or tile.isHidden or not tile.templateId then
return nil
end
if excludedTemplates[tile.templateId] then
return nil
end
if getExitTemplates()[tile.templateId] then
return "exits"
end
if isLootable(tile) then
return lootContainsIngredient(tile.loot) and "alchemy" or "items"
end
-- Structural tiles are never tracked, even though many of them report
-- tryInteract.
--
-- `wall` carries a flavour-only tryInteractFunction ("You touch the wall.
-- It's cold and rough."), so the flag-first rule promoted every wall segment
-- into Interactables -- 13 of them in the starting house alone, burying the
-- chest and the cauldron. tryInteract means "something happens if you press
-- interact", NOT "this is worth walking to".
--
-- blocksMovement AND blocksVision identifies the structural shell (walls,
-- solid rock). Containers and furniture block movement but not sight, so they
-- survive this filter. Walls are covered by the wall tones and the
-- surroundings scan anyway.
if tile.blocksMovement and tile.blocksVision then
return nil
end
if tile.tryInteract then
return "interactables"
end
-- Terrain is intentionally narrow: hazards and water only. Including every
-- wall would bury the useful entries under hundreds of identical ones -- and
-- walls are already covered by the wall tones and the surroundings scan.
if tile.isThreatening then
return "terrain"
end
return nil
end
-- Water is a property of the level's water map rather than of a tile, so it is
-- classified separately during the scan.
categories.WATER_CATEGORY = "terrain"
function categories.tileName(tile)
return tile.name or tile.templateId or "something"
end
function categories.actorName(actor)
return actor.nameObserved or actor.name or actor.templateId or "someone"
end
-- Kind key: groups instances of the same thing together. Template id is stabler
-- than the display name (two templates can share a name).
function categories.tileKind(tile)
return tile.templateId
end
function categories.actorKind(actor)
return actor.templateIdOnExamination or actor.templateId
end
return categories