-- 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 -- All eight neighbours, clockwise from north, as one line. Terse by design: -- eight descriptions at normal verbosity is a wall of speech. -- -- Returns the text rather than speaking it, because melee targeting needs the -- same sentence on entry (accessibility/states/ingame_targeting_melee.lua). function surroundings.describeNeighbours() local level = getLevel() local px, py = playerPosition(level) if not px then return nil 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 return table.concat(parts, ". ") end -- Speak all eight neighbours. function surroundings.scan() local text = surroundings.describeNeighbours() if text then speech.say(text, true) end 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