Phase 2: tile examination and surroundings
Game-file footprint unchanged: still one line in main.lua. - describe.lua: shared spatial formatting. 8-point compass and Chebyshev distance, since the game moves 8-directionally and Euclidean would overstate diagonal travel. Verbosity setting, because sweeping eight neighbours and inspecting a single tile want different amounts of detail. - states/ingame_observe.lua: narrates the game's existing examine cursor. - surroundings.lua: probe one direction, scan all eight, or report location, without moving anything or touching game state. - patches/popups.lua: wraps inventoryCursorPopup.set, covering inventory, craft, refine and trade in a single patch. Q3 resolved as augment rather than relay: the game's descriptions carry no position, which is fine on screen and useless by ear, so distance and direction are appended. Examine mode is polled rather than driven by levelCursorPopup. That popup is set from an animation-completion callback, so unless the player has enabled the game's quickExaminationPopup option the text arrives a full keypress late -- in testing you heard the previous tile while standing on the next. Polling level.cursorX/Y is same-frame and setting-independent. hotkeys.isCtrlDown/isShiftDown are now overridable fields rather than direct love.keyboard calls: real hardware state cannot be faked by love.event.push, so without that seam every modifier-gated hotkey would be untestable. 11/11 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
da512fc67e
commit
8b1eb44c38
@@ -0,0 +1,196 @@
|
||||
-- describe.lua — shared spatial text formatting.
|
||||
--
|
||||
-- The game's own descriptions ("A wooden chair.") carry no spatial information,
|
||||
-- which is fine on screen where position is obvious and useless by ear. Per Q3
|
||||
-- we augment rather than merely relay: distance, direction, and passability.
|
||||
--
|
||||
-- Verbosity is a setting because the right amount differs between sweeping the
|
||||
-- surroundings (terse) and inspecting one tile (verbose).
|
||||
|
||||
local describe = {}
|
||||
|
||||
-- "terse" | "normal" | "verbose"
|
||||
describe.verbosity = "normal"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Direction
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Eight-point compass. Screen coordinates: +y is south, +x is east.
|
||||
function describe.direction(dx, dy)
|
||||
if dx == 0 and dy == 0 then
|
||||
return "here"
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
|
||||
if dy < 0 then
|
||||
parts[#parts + 1] = "north"
|
||||
elseif dy > 0 then
|
||||
parts[#parts + 1] = "south"
|
||||
end
|
||||
|
||||
if dx < 0 then
|
||||
parts[#parts + 1] = "west"
|
||||
elseif dx > 0 then
|
||||
parts[#parts + 1] = "east"
|
||||
end
|
||||
|
||||
return table.concat(parts, "-")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Distance
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Chebyshev distance: the number of steps on an 8-directional grid, which is what
|
||||
-- this game uses. Euclidean would overstate diagonal travel.
|
||||
function describe.distance(dx, dy)
|
||||
return math.max(math.abs(dx), math.abs(dy))
|
||||
end
|
||||
|
||||
function describe.tiles(n)
|
||||
return n == 1 and "1 tile" or (n .. " tiles")
|
||||
end
|
||||
|
||||
-- "3 tiles north-east" / "here"
|
||||
function describe.offset(dx, dy)
|
||||
if dx == 0 and dy == 0 then
|
||||
return "here"
|
||||
end
|
||||
|
||||
return describe.tiles(describe.distance(dx, dy)) .. " " .. describe.direction(dx, dy)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Tile inspection
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- What the player would see standing on / looking at (x,y).
|
||||
-- Returns a table of facts rather than a sentence, so callers choose the wording.
|
||||
function describe.tileFacts(level, x, y)
|
||||
local facts = {
|
||||
inBounds = false,
|
||||
seen = false,
|
||||
visible = false,
|
||||
blocked = false,
|
||||
blocksVision = false,
|
||||
water = false,
|
||||
hasLoot = false,
|
||||
interactable = false,
|
||||
actor = nil,
|
||||
description = nil,
|
||||
}
|
||||
|
||||
if not level or not level.grid or not level.grid[x] or not level.grid[x][y] then
|
||||
return facts
|
||||
end
|
||||
|
||||
facts.inBounds = true
|
||||
|
||||
local visible = level.FoV and level.FoV[x] and level.FoV[x][y]
|
||||
local remembered = level.rememberedFoV and level.rememberedFoV[x] and level.rememberedFoV[x][y]
|
||||
|
||||
facts.visible = visible and true or false
|
||||
facts.seen = (visible or remembered) and true or false
|
||||
|
||||
if not facts.seen then
|
||||
return facts
|
||||
end
|
||||
|
||||
if visible then
|
||||
local ok, actor = pcall(level.getActorOnCoordinates, x, y, function(a)
|
||||
return a.visibilityAlpha ~= 0
|
||||
end)
|
||||
if ok and actor then
|
||||
facts.actor = actor
|
||||
end
|
||||
end
|
||||
|
||||
for i = #level.grid[x][y], 1, -1 do
|
||||
local tile = level.grid[x][y][i]
|
||||
|
||||
if tile and not tile.isHidden then
|
||||
if tile.blocksMovement then
|
||||
facts.blocked = true
|
||||
end
|
||||
if tile.blocksVision then
|
||||
facts.blocksVision = true
|
||||
end
|
||||
if tile.tryInteract then
|
||||
facts.interactable = true
|
||||
end
|
||||
if tile.loot and tile.loot[1] and not tile.unlootable then
|
||||
facts.hasLoot = true
|
||||
end
|
||||
if not facts.description and tile.description then
|
||||
facts.description = tile.description
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ok, isWater = pcall(level.isWaterTile, x, y)
|
||||
facts.water = (ok and isWater) and true or false
|
||||
|
||||
return facts
|
||||
end
|
||||
|
||||
-- One spoken line for a tile, relative to an origin (usually the player).
|
||||
function describe.tile(level, x, y, originX, originY)
|
||||
local facts = describe.tileFacts(level, x, y)
|
||||
|
||||
local dx = originX and (x - originX) or 0
|
||||
local dy = originY and (y - originY) or 0
|
||||
local where = originX and describe.offset(dx, dy) or nil
|
||||
|
||||
if not facts.inBounds then
|
||||
return (where and (where .. ", ") or "") .. "edge of the map"
|
||||
end
|
||||
|
||||
if not facts.seen then
|
||||
return (where and (where .. ", ") or "") .. "unknown"
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
|
||||
-- Creatures first: they matter more than the floor they stand on, and the
|
||||
-- game's own getCursorMessage prioritises them the same way.
|
||||
if facts.actor then
|
||||
parts[#parts + 1] = facts.actor.nameObserved or facts.actor.name or "someone"
|
||||
end
|
||||
|
||||
if facts.description then
|
||||
parts[#parts + 1] = facts.description
|
||||
elseif not facts.actor then
|
||||
parts[#parts + 1] = facts.blocked and "wall" or "floor"
|
||||
end
|
||||
|
||||
if describe.verbosity ~= "terse" then
|
||||
if facts.blocked then
|
||||
parts[#parts + 1] = "blocked"
|
||||
end
|
||||
if facts.hasLoot then
|
||||
parts[#parts + 1] = "has items"
|
||||
end
|
||||
if facts.water then
|
||||
parts[#parts + 1] = "water"
|
||||
end
|
||||
end
|
||||
|
||||
if describe.verbosity == "verbose" then
|
||||
if facts.interactable then
|
||||
parts[#parts + 1] = "can interact"
|
||||
end
|
||||
if not facts.visible then
|
||||
parts[#parts + 1] = "remembered"
|
||||
end
|
||||
end
|
||||
|
||||
if where then
|
||||
parts[#parts + 1] = where
|
||||
end
|
||||
|
||||
return table.concat(parts, ", ")
|
||||
end
|
||||
|
||||
return describe
|
||||
Reference in New Issue
Block a user