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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b1eb44c38
commit
c17c5277d4
@@ -0,0 +1,173 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user