-- 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 -- "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 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