2026-08-01 21:26:35 +02:00
|
|
|
-- 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
|
2026-08-03 16:05:15 +02:00
|
|
|
|
|
|
|
|
-- "remembered" is a safety flag, not a nicety, so it earns its place at
|
|
|
|
|
-- normal verbosity. tileFacts reads actors only when the tile is
|
|
|
|
|
-- currently VISIBLE (see the `if visible` guard above), so a remembered
|
|
|
|
|
-- tile is silent about whatever may be standing on it now. Without this
|
|
|
|
|
-- word that silence is indistinguishable from "nothing there".
|
|
|
|
|
if not facts.visible then
|
|
|
|
|
parts[#parts + 1] = "remembered"
|
|
|
|
|
end
|
2026-08-01 21:26:35 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if describe.verbosity == "verbose" then
|
|
|
|
|
if facts.interactable then
|
|
|
|
|
parts[#parts + 1] = "can interact"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if where then
|
|
|
|
|
parts[#parts + 1] = where
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return table.concat(parts, ", ")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return describe
|