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>
145 lines
3.7 KiB
Lua
145 lines
3.7 KiB
Lua
-- surroundings.lua — probe the tiles around the player without moving anything.
|
|
--
|
|
-- The game's examine mode (ingame_observe) already walks a cursor around the map
|
|
-- and is now narrated, but entering it, stepping, and leaving is heavy when you
|
|
-- just want to know what is immediately next to you. This answers that in one
|
|
-- keypress, with no state change and no game side effects.
|
|
|
|
local speech = require("accessibility.speech")
|
|
local describe = require("accessibility.describe")
|
|
|
|
local surroundings = {}
|
|
|
|
-- Screen coordinates: +y is south.
|
|
local numpadDirections = {
|
|
kp1 = {-1, 1},
|
|
kp2 = {0, 1},
|
|
kp3 = {1, 1},
|
|
kp4 = {-1, 0},
|
|
kp5 = {0, 0},
|
|
kp6 = {1, 0},
|
|
kp7 = {-1, -1},
|
|
kp8 = {0, -1},
|
|
kp9 = {1, -1},
|
|
}
|
|
|
|
-- Clockwise from north, which is how people scan a compass.
|
|
local scanOrder = {
|
|
{0, -1, "north"},
|
|
{1, -1, "north-east"},
|
|
{1, 0, "east"},
|
|
{1, 1, "south-east"},
|
|
{0, 1, "south"},
|
|
{-1, 1, "south-west"},
|
|
{-1, 0, "west"},
|
|
{-1, -1, "north-west"},
|
|
}
|
|
|
|
local function getLevel()
|
|
local ok, world = pcall(require, "models.world")
|
|
if not ok or not world.currentLevel then
|
|
return nil
|
|
end
|
|
return world.currentLevel
|
|
end
|
|
|
|
local function playerPosition(level)
|
|
local actor = level and level.playerActor
|
|
if not actor or not actor.x or not actor.y then
|
|
return nil
|
|
end
|
|
return actor.x, actor.y
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Probing
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Speak a single tile at an offset from the player.
|
|
function surroundings.probe(dx, dy)
|
|
local level = getLevel()
|
|
local px, py = playerPosition(level)
|
|
|
|
if not px then
|
|
return
|
|
end
|
|
|
|
if dx == 0 and dy == 0 then
|
|
speech.say("Standing on: " .. describe.tile(level, px, py), true)
|
|
return
|
|
end
|
|
|
|
speech.say(describe.tile(level, px + dx, py + dy, px, py), true)
|
|
end
|
|
|
|
-- Speak all eight neighbours, clockwise from north. Terse by design: eight
|
|
-- descriptions at normal verbosity is a wall of speech.
|
|
function surroundings.scan()
|
|
local level = getLevel()
|
|
local px, py = playerPosition(level)
|
|
|
|
if not px then
|
|
return
|
|
end
|
|
|
|
local previousVerbosity = describe.verbosity
|
|
describe.verbosity = "terse"
|
|
|
|
local parts = {}
|
|
for _, entry in ipairs(scanOrder) do
|
|
local dx, dy, name = entry[1], entry[2], entry[3]
|
|
local text = describe.tile(level, px + dx, py + dy)
|
|
parts[#parts + 1] = name .. ", " .. text
|
|
end
|
|
|
|
describe.verbosity = previousVerbosity
|
|
|
|
speech.say(table.concat(parts, ". "), true)
|
|
end
|
|
|
|
-- Where am I: level name, in-game time, coordinates.
|
|
function surroundings.whereAmI()
|
|
local level = getLevel()
|
|
local px, py = playerPosition(level)
|
|
|
|
if not px then
|
|
return
|
|
end
|
|
|
|
local parts = {}
|
|
|
|
if level.name then
|
|
parts[#parts + 1] = level.name
|
|
end
|
|
|
|
local ok, world = pcall(require, "models.world")
|
|
if ok and world.printableDatetime then
|
|
parts[#parts + 1] = world.printableDatetime
|
|
end
|
|
|
|
parts[#parts + 1] = "position " .. px .. ", " .. py
|
|
|
|
speech.say(table.concat(parts, ". "), true)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Key registration
|
|
--------------------------------------------------------------------------------
|
|
|
|
function surroundings.registerKeys(hotkeys)
|
|
-- Numpad probing is modifier-gated: kp1-kp9 are movement in game, so an
|
|
-- unmodified numpad key must keep moving the player. Ctrl is the game's
|
|
-- moveCamera modifier but that only acts on direction commands we consume
|
|
-- here, so Ctrl+numpad is free.
|
|
for scancode, offset in pairs(numpadDirections) do
|
|
hotkeys.bind(scancode, "ctrl", function()
|
|
surroundings.probe(offset[1], offset[2])
|
|
end)
|
|
end
|
|
|
|
hotkeys.bind("/", nil, surroundings.scan)
|
|
hotkeys.bind(";", nil, surroundings.whereAmI)
|
|
end
|
|
|
|
return surroundings
|