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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -124,6 +124,12 @@ needs a human at the keyboard — Talon to test.
|
||||
| `Shift+[` / `Shift+]` | Oldest / newest line |
|
||||
| `\` | Repeat current line |
|
||||
| `Shift+\` | Copy current line to clipboard |
|
||||
| `/` | Scan all 8 neighbours, clockwise from north (terse) |
|
||||
| `;` | Where am I — level, in-game time, coordinates |
|
||||
| `Ctrl`+numpad 1–9 | Probe that direction without moving (`Ctrl+kp5` = standing on) |
|
||||
|
||||
Game keys worth knowing: **`x`** enters examine mode (not `e`), `f` finds exits
|
||||
while examining, `j` journal, `i` inventory, `c` character, `a` actions menu.
|
||||
|
||||
Reserved for the tracker (Phase 3): `PageUp`/`PageDown`, `Home`, with Ctrl/Shift.
|
||||
|
||||
@@ -239,9 +245,43 @@ where you're standing now. `playing` is cleared on level change.
|
||||
|
||||
*(Mono matters: OpenAL only spatialises mono sources. Stereo would not pan.)*
|
||||
|
||||
### Done — Phase 2: tile examination + surroundings
|
||||
|
||||
- `accessibility/describe.lua` — shared spatial formatting. 8-point compass,
|
||||
**Chebyshev** distance (the game moves 8-directionally, so Euclidean would
|
||||
overstate diagonals), and `tileFacts()` / `tile()`. Verbosity setting
|
||||
(`terse`/`normal`/`verbose`) since sweeping 8 neighbours and inspecting one
|
||||
tile want different amounts of detail.
|
||||
- `accessibility/states/ingame_observe.lua` — narrates the game's examine cursor.
|
||||
- `accessibility/surroundings.lua` — probe/scan/where-am-I without moving.
|
||||
- `accessibility/patches/popups.lua` — wraps `inventoryCursorPopup.set`, covering
|
||||
**inventory, craft, refine, trade** in one patch.
|
||||
|
||||
**Q3 answered by default: augment, not just relay.** The game's descriptions
|
||||
("A wooden chair.") carry no position — fine on screen, useless by ear. We append
|
||||
distance + direction, and blocked/has-items/water at normal verbosity.
|
||||
|
||||
#### Design note: why examine mode is polled, not popup-driven
|
||||
`levelCursorPopup.set` is called from an **animation-completion callback**, so
|
||||
unless the player enables the game's `quickExaminationPopup` option the text
|
||||
lands a full keypress late — in testing you heard the *previous* tile while
|
||||
standing on the next. The `ingame_observe` descriptor polls `level.cursorX/Y`
|
||||
instead: same-frame, setting-independent, no game edit. `levelCursorPopup` is
|
||||
therefore deliberately **not** patched; only `inventoryCursorPopup` is.
|
||||
|
||||
*(The level model is a closure instance, not a module, so it can't be patched at
|
||||
require time — reading its public fields is the decoupled route.)*
|
||||
|
||||
#### Testability seam
|
||||
`hotkeys.isCtrlDown` / `isShiftDown` are overridable fields, not direct calls to
|
||||
`love.keyboard.isScancodeDown`. Real hardware state can't be faked by
|
||||
`love.event.push`, so without this seam every modifier-gated hotkey would be
|
||||
untestable. The testbed swaps them per keypress.
|
||||
|
||||
### Known gaps (next up)
|
||||
- `ingame` itself has no descriptor — that's Phase 2 (tile examination).
|
||||
- **Phase 3: object tracker** — the headline feature, not started.
|
||||
- Wall tone retrigger fix not yet confirmed by ear.
|
||||
- `ingame` itself has no descriptor (player status: HP, statuses, current action).
|
||||
- `worldmap`, `journal`, `character`, `research`, `inventory`, `craft`, `refine`,
|
||||
`trade`, `actions_menu`, `alchemy_table`, `keybindings_menu`, `config_menu`,
|
||||
`menu_text`, `ingame_message`, `menu_message` still have no descriptors.
|
||||
|
||||
@@ -48,14 +48,26 @@ end
|
||||
-- Modifier helpers
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function ctrlDown()
|
||||
-- Indirected through overridable fields so the testbed can drive modified
|
||||
-- hotkeys. love.keyboard.isScancodeDown reads real hardware state, which a
|
||||
-- synthetic love.event.push cannot influence -- without this seam, every
|
||||
-- modifier-gated key would be untestable.
|
||||
hotkeys.isCtrlDown = function()
|
||||
return love.keyboard.isScancodeDown("lctrl", "rctrl")
|
||||
end
|
||||
|
||||
local function shiftDown()
|
||||
hotkeys.isShiftDown = function()
|
||||
return love.keyboard.isScancodeDown("lshift", "rshift")
|
||||
end
|
||||
|
||||
local function ctrlDown()
|
||||
return hotkeys.isCtrlDown()
|
||||
end
|
||||
|
||||
local function shiftDown()
|
||||
return hotkeys.isShiftDown()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Bindings
|
||||
--
|
||||
|
||||
@@ -30,6 +30,7 @@ local describedStates = {
|
||||
"new_game",
|
||||
"narration",
|
||||
"ingame_dialogue",
|
||||
"ingame_observe",
|
||||
}
|
||||
|
||||
local initialized = false
|
||||
@@ -218,6 +219,11 @@ function accessibility.initialize()
|
||||
initialized = true
|
||||
|
||||
accessibility.registerPatch("console", require("accessibility.patches.console"))
|
||||
accessibility.registerPatch("popups", require("accessibility.patches.popups"))
|
||||
|
||||
pcall(function()
|
||||
require("accessibility.surroundings").registerKeys(hotkeys)
|
||||
end)
|
||||
|
||||
local ok, err = pcall(function()
|
||||
speech.initialize()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
-- Patch: views/levelCursorPopup.lua and views/inventoryCursorPopup.lua
|
||||
--
|
||||
-- `inventoryCursorPopup.set` is the convergence point for focus text in four
|
||||
-- screens: inventory, craft, refine and trade. Patching it narrates all four
|
||||
-- without touching any state file.
|
||||
--
|
||||
-- inventoryCursorPopup.set(lines, x, y, alignX, alignY) -- lines is a TABLE
|
||||
-- of {text=, icon=}
|
||||
--
|
||||
-- NOTE: levelCursorPopup (the examine cursor) is deliberately NOT handled here.
|
||||
-- It is set from an animation-completion callback, so its text lands a keypress
|
||||
-- late unless the player enables the game's "quickExaminationPopup" option.
|
||||
-- accessibility/states/ingame_observe.lua polls the cursor position instead,
|
||||
-- which is immediate and setting-independent.
|
||||
|
||||
local speech = require("accessibility.speech")
|
||||
|
||||
-- Suppress repeats: the setter is called on every mouse move, often with
|
||||
-- unchanged content.
|
||||
local lastInventoryText
|
||||
|
||||
local function linesToText(lines)
|
||||
if type(lines) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
for _, line in ipairs(lines) do
|
||||
if type(line) == "table" and line.text then
|
||||
parts[#parts + 1] = line.text
|
||||
elseif type(line) == "string" then
|
||||
parts[#parts + 1] = line
|
||||
end
|
||||
end
|
||||
|
||||
if #parts == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return table.concat(parts, ". ")
|
||||
end
|
||||
|
||||
return function()
|
||||
----------------------------------------------------------------------------
|
||||
-- inventoryCursorPopup: inventory, craft, refine, trade
|
||||
----------------------------------------------------------------------------
|
||||
local inventoryPopup = require("views.inventoryCursorPopup")
|
||||
|
||||
local realInventorySet = inventoryPopup.set
|
||||
if not realInventorySet then
|
||||
error("views.inventoryCursorPopup has no set function -- game structure changed")
|
||||
end
|
||||
|
||||
inventoryPopup.set = function(lines, ...)
|
||||
local result = realInventorySet(lines, ...)
|
||||
|
||||
pcall(function()
|
||||
local text = linesToText(lines)
|
||||
|
||||
if not text then
|
||||
lastInventoryText = nil
|
||||
return
|
||||
end
|
||||
|
||||
text = speech.normalise(text)
|
||||
if not text or text == lastInventoryText then
|
||||
return
|
||||
end
|
||||
lastInventoryText = text
|
||||
|
||||
speech.say(text, true)
|
||||
end)
|
||||
|
||||
return result
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
-- states/ingame_observe.lua — the examine cursor.
|
||||
--
|
||||
-- Reads the cursor position straight off the level model rather than listening to
|
||||
-- levelCursorPopup.set.
|
||||
--
|
||||
-- Why: the popup is set from an animation-completion callback (`onFinish`), so
|
||||
-- unless the player has turned on the game's "quickExaminationPopup" option, the
|
||||
-- text arrives only after the cursor finishes gliding. In testing that put each
|
||||
-- announcement a full keypress behind -- you would hear the previous tile while
|
||||
-- standing on the next one. Polling cursorX/cursorY announces on the same frame
|
||||
-- the cursor moves, regardless of that setting, and needs no game edit.
|
||||
--
|
||||
-- The level model is a closure instance rather than a module, so it cannot be
|
||||
-- patched at require time; reading its public fields is the decoupled route.
|
||||
|
||||
local describe = require("accessibility.describe")
|
||||
|
||||
return {
|
||||
read = function()
|
||||
local ok, world = pcall(require, "models.world")
|
||||
if not ok or not world.currentLevel then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local level = world.currentLevel
|
||||
local x, y = level.cursorX, level.cursorY
|
||||
|
||||
if type(x) ~= "number" or type(y) ~= "number" then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local player = level.playerActor
|
||||
local px = player and player.x
|
||||
local py = player and player.y
|
||||
|
||||
local text = describe.tile(level, x, y, px, py)
|
||||
if not text or text == "" then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
return text, x .. "," .. y
|
||||
end,
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
-- 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
|
||||
@@ -50,16 +50,30 @@ end
|
||||
|
||||
-- Presses a key and waits long enough for the event to be pumped, handled, and
|
||||
-- for our end-of-frame speech flush to run.
|
||||
local function press(scancode, note)
|
||||
local function press(scancode, note, mods)
|
||||
local before = focusText()
|
||||
print(string.format("[testbed] (%s) press %-8s focus_before=%s %s",
|
||||
currentState(), scancode, before and ('"' .. before .. '"') or "nil", note or ""))
|
||||
print(string.format("[testbed] (%s) press %-8s%s focus_before=%s %s",
|
||||
currentState(), scancode,
|
||||
mods and (mods.ctrl and " +ctrl" or mods.shift and " +shift" or "") or "",
|
||||
before and ('"' .. before .. '"') or "nil", note or ""))
|
||||
|
||||
-- Modifier state is read from real hardware, which synthetic events cannot
|
||||
-- affect -- so override the hotkey layer's modifier probes for this press.
|
||||
local hotkeys = require("accessibility.hotkeys")
|
||||
local realCtrl, realShift = hotkeys.isCtrlDown, hotkeys.isShiftDown
|
||||
|
||||
if mods then
|
||||
hotkeys.isCtrlDown = function() return mods.ctrl == true end
|
||||
hotkeys.isShiftDown = function() return mods.shift == true end
|
||||
end
|
||||
|
||||
love.event.push("keypressed", scancode, scancode, false)
|
||||
love.event.push("keyreleased", scancode, scancode)
|
||||
|
||||
wait(6)
|
||||
|
||||
hotkeys.isCtrlDown, hotkeys.isShiftDown = realCtrl, realShift
|
||||
|
||||
local after = focusText()
|
||||
print(string.format("[testbed] -> state=%s focus_after=%s",
|
||||
currentState(), after and ('"' .. after .. '"') or "nil"))
|
||||
@@ -202,6 +216,28 @@ local function scenario()
|
||||
press("[", "review previous")
|
||||
press("]", "review next")
|
||||
press("\\", "repeat current")
|
||||
|
||||
-- Phase 2: surroundings probing (no state change, no game side effects).
|
||||
press("/", "scan all eight neighbours")
|
||||
press(";", "where am I")
|
||||
press("kp8", "probe north", {ctrl = true})
|
||||
press("kp6", "probe east", {ctrl = true})
|
||||
press("kp5", "probe standing-on", {ctrl = true})
|
||||
|
||||
-- The game's own examine mode, now narrated via levelCursorPopup.
|
||||
press("x", "enter examine mode")
|
||||
wait(10)
|
||||
check("entered examine mode", currentState() == "ingame_observe",
|
||||
"got " .. currentState())
|
||||
|
||||
if currentState() == "ingame_observe" then
|
||||
press("right", "examine: cursor east")
|
||||
press("down", "examine: cursor south")
|
||||
press("escape", "leave examine mode")
|
||||
wait(10)
|
||||
check("left examine mode", currentState() == "ingame",
|
||||
"got " .. currentState())
|
||||
end
|
||||
end
|
||||
|
||||
print(string.format("[testbed] === %d checks, %d failures ===", checks, failures))
|
||||
|
||||
Reference in New Issue
Block a user