loot overlay: speak the contents of visible containers
The tutorial teaches "press s to see the contents of containers", and it was silent. For a sighted player s sets levelWindow.itemsHighlightEnabled, which draws the top item's icon on every loot-bearing tile in FoV. We watch that flag rather than binding s ourselves -- it is game-claimed, and keybindings.getCommand() ignores modifiers -- and speak on its rising edge, so the player's own binding, hold vs toggle mode and the toolbar icon all work. The flag is checked both at end of frame and inside the keypressed wrapper, because in hold mode a quick tap goes up and down within one frame. Reads the whole tile.loot list, not just the icon's top entry, nearest first, with each container pushed to the console history for [ ] review. 71 testbed checks, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dddd8ed9de
commit
93ef26349a
@@ -87,6 +87,30 @@ yourself is spoken first, and any key that acts in the game clears them.
|
||||
|
||||
Nothing outside your current field of view is ever reported.
|
||||
|
||||
#### Seeing inside containers (the game's own `s`)
|
||||
|
||||
The game's tutorial tells you to press **`s`** to see the contents of containers.
|
||||
For a sighted player that draws the icon of whatever is inside every chest, sack
|
||||
and pile you can see, straight onto the map. Pressing it now **speaks the same
|
||||
thing**, nearest first:
|
||||
|
||||
```
|
||||
A wooden chest, 2 tiles east, A bunch of ivy leaves, 3, A torch.
|
||||
A burlap sack, 5 tiles north-west, Coarse salt
|
||||
```
|
||||
|
||||
or `No loot in view` when there is nothing to find, so you can tell the key
|
||||
worked rather than wondering whether it was dropped.
|
||||
|
||||
The icons can only show one item per container; you get the **whole list**. Each
|
||||
container is also pushed into the message history, so `[`, `]` and `\` walk back
|
||||
through a long list after it has been read out.
|
||||
|
||||
This is your own key — whatever you have `Highlight loot` bound to in the game's
|
||||
controls, and however you have set *"highlight overlays require holding the
|
||||
key"*. Clicking the toolbar icon works too. It also works in examine mode and
|
||||
while aiming. Only containers you can currently see are reported.
|
||||
|
||||
The game's own examine mode (**`x`**) is also fully spoken: the arrow keys move a
|
||||
cursor around the map and each tile is described with its distance and direction
|
||||
from you. Press `f` in examine mode to jump between exits.
|
||||
|
||||
@@ -7,7 +7,7 @@ fixing what Talon hits. Remaining work is polish, mod settings and distribution.
|
||||
|
||||
**Game is playable start-to-finish by ear** from main menu through the intro
|
||||
conversation into gameplay, plus journal, character, actions and inventory —
|
||||
verified by the testbed, **59/59 checks**.
|
||||
verified by the testbed, **71/71 checks**.
|
||||
|
||||
**Not set up yet, or unsure how the two trees relate?** →
|
||||
[00-dev-setup.md](00-dev-setup.md).
|
||||
@@ -684,6 +684,62 @@ indistinguishable from a dropped key).
|
||||
prologue inventory is empty and craft/refine/trade need an alchemy table. The
|
||||
live path is Talon's to confirm.
|
||||
|
||||
### Done — the loot overlay key `s` (Talon request)
|
||||
|
||||
**Symptom:** the tutorial says *"Press 's' to see the contents of containers"*
|
||||
and nothing was spoken. That is the one mechanic the game explicitly teaches for
|
||||
finding reagents, and alchemy starts with finding reagents.
|
||||
|
||||
**What `s` actually does.** It is the `lootOverlay` command, and it sets a single
|
||||
flag — `levelWindow.itemsHighlightEnabled`. `views/levelWindow.lua:818` then
|
||||
draws, over every tile in **current FoV**, the icon of
|
||||
`levelModel.getItemOnCoordinates(x,y)` — i.e. the **topmost item of the topmost
|
||||
loot-bearing tile**. So the sighted affordance is "there is something in that
|
||||
chest", one icon per tile, not a contents list.
|
||||
|
||||
**`accessibility/lootoverlay.lua`** speaks the same set of tiles, nearest first,
|
||||
and reads out the **whole `tile.loot` list** rather than just the top entry —
|
||||
the icon can only show one, we are not so limited. Blocks are pushed into the
|
||||
console history so `[` / `]` / `\` walk a long list back. `No loot in view` when
|
||||
empty: silence would be indistinguishable from a dropped key.
|
||||
|
||||
Live text: `Tiled man-made floor, here, A bunch of ivy leaves, 3`.
|
||||
|
||||
Decisions worth not relitigating:
|
||||
|
||||
- **No key of ours.** `s` is game-claimed and `keybindings.getCommand()` ignores
|
||||
modifiers, so binding it in our layer would collide. We watch the *flag*
|
||||
instead, on its **rising edge**. That covers the key in both hold and toggle
|
||||
mode (`holdForItemHighlight`, which defaults to **hold**), the toolbar icon,
|
||||
the player's own rebinding, and all four ingame states — they share one
|
||||
`levelWindow` instance, created once in `ingame.open` and passed on by params.
|
||||
- **The flag is checked in TWO places.** The end-of-frame poll is not enough on
|
||||
its own: in hold mode a quick tap pushes keypressed and keyreleased inside one
|
||||
frame, so the poll only ever sees it back down. `init.lua`'s `states.keypressed`
|
||||
wrapper therefore also calls `lootoverlay.update()` *between* the game's
|
||||
keypressed and its keyreleased. The poll still earns its place — it is what
|
||||
catches the mouse and the key being released.
|
||||
- **`levelWindow` is reached by `menus.getAnyUpvalue` on `states/ingame`** and
|
||||
cached (it is created once, not per level). Covered by the startup self-test
|
||||
(`[accessibility] loot overlay: ok`) as the version-bump canary.
|
||||
- **Loot entries are item *params*, not items** — `{templateId, quantity}`;
|
||||
`modules/lootPopup.lua` only builds real items when the popup opens. Names come
|
||||
from `modules/item/templates`, so nothing is allocated to say a name.
|
||||
- **Membership matches levelWindow's test exactly**: current `FoV` only, not
|
||||
hidden, `visibilityAlpha ~= 0`, non-empty loot. `conditionalLoot` is *not*
|
||||
read — the overlay does not draw it either. Invariant 4 holds: nothing is said
|
||||
that is not being drawn on screen at that moment.
|
||||
- `unlootable` containers are still reported (the overlay draws them) but tagged
|
||||
`cannot be taken`. Scan is bounded to a 20-tile box and runs on the edge only,
|
||||
never per frame — invariant 5.
|
||||
|
||||
**71 testbed checks, 0 failures** (was 59). The prologue has no loot at all
|
||||
(`chest_tutorial` is empty by design), so the live path is exercised by
|
||||
**planting `ivy_leaves` on the tile under the player, scanning, and putting the
|
||||
tile back** — otherwise only the "nothing here" branch would ever run. Plus the
|
||||
pure phrasing checks, the FoV invariant over the real grid, the `levelWindow`
|
||||
probe, and an end-to-end `s` press.
|
||||
|
||||
### Known gaps (next up)
|
||||
- **Phase 5**: player status key (HP, statuses, current action), popup
|
||||
announcements (`popupStack.add`), damage alerts
|
||||
@@ -748,7 +804,7 @@ utterance — speech is verifiable from the log, not only by ear. `!` after
|
||||
## HANDOFF — read this if you are resuming
|
||||
|
||||
### State of play
|
||||
Phases 0–5 complete. **23 state descriptors, 6 patches, 59/59 testbed checks**,
|
||||
Phases 0–5 complete. **23 state descriptors, 6 patches, 71/71 testbed checks**,
|
||||
history unit tests passing, **one line changed in the game** (`main.lua`).
|
||||
|
||||
Everything in the original brief is implemented: console speech with
|
||||
@@ -758,7 +814,7 @@ exploration, wall tones, and keyboard+speech access to every screen.
|
||||
### First thing to do after any change
|
||||
```
|
||||
lua accessibility/tests/test_history.lua # pure Lua, instant
|
||||
bash accessibility/tests/run-testbed.sh # ~4 min, 59 checks
|
||||
bash accessibility/tests/run-testbed.sh # ~4 min, 71 checks
|
||||
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' ':!CLAUDE.md' # must stay: main.lua | 1 +
|
||||
```
|
||||
|
||||
@@ -803,6 +859,9 @@ These are blocked on content the prologue does not contain, **not** known broken
|
||||
`tutorial_bed` has an empty `tryInteractFunction` by design, and
|
||||
craft/refine/trade need an alchemy table and real items. The testbed already
|
||||
has `walkTo()` and tracker-based bed lookup ready for when one is reachable.
|
||||
- **The loot overlay (`s`) against real loot.** The prologue contains none, so
|
||||
live it has only ever said `No loot in view`. The contents path is proven by
|
||||
the testbed's planted-loot check, not by a real chest.
|
||||
- **Popup announcements** (`patches/popupstack.lua`) — loot/quest/recipe popups
|
||||
did not fire during the prologue run.
|
||||
|
||||
|
||||
@@ -99,6 +99,9 @@ function accessibility.selfTest()
|
||||
end
|
||||
end
|
||||
|
||||
local lootOk, lootStatus = pcall(require("accessibility.lootoverlay").probeStatus)
|
||||
print("[accessibility] loot overlay: " .. (lootOk and tostring(lootStatus) or ("FAILED: " .. tostring(lootStatus))))
|
||||
|
||||
print("[accessibility] --- end self test ---")
|
||||
end
|
||||
|
||||
@@ -165,6 +168,7 @@ local function hookStates()
|
||||
pcall(require("accessibility.viewwatch").update, dt)
|
||||
pcall(require("accessibility.status").update)
|
||||
pcall(require("accessibility.details").update, dt)
|
||||
pcall(require("accessibility.lootoverlay").update)
|
||||
pcall(speech.flush)
|
||||
if testbedActive then
|
||||
pcall(testbed.update, dt)
|
||||
@@ -190,6 +194,12 @@ local function hookStates()
|
||||
|
||||
local keyResult = realKeypressed(key, scancode)
|
||||
|
||||
-- The loot overlay is edge-triggered off a flag the game sets, and in
|
||||
-- hold-to-highlight mode a quick tap can set it and clear it again inside
|
||||
-- one frame -- the end-of-frame poll would then never see it up. Checking
|
||||
-- here, between the game's keypressed and its keyreleased, catches the tap.
|
||||
pcall(require("accessibility.lootoverlay").update)
|
||||
|
||||
-- After the game handler, so we read the focus it just moved to.
|
||||
pcall(function()
|
||||
menus.announceFocusIfChanged(states.getCurrent())
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
-- lootoverlay.lua — speak what the game's loot highlight shows.
|
||||
--
|
||||
-- The tutorial says: "Press 's' to see the contents of containers." For a
|
||||
-- sighted player that turns on `levelWindow.itemsHighlightEnabled`, which draws
|
||||
-- the icon of the topmost item of every visible container or pile straight onto
|
||||
-- the map. By ear it did nothing at all — so the one mechanic the game
|
||||
-- explicitly teaches for finding reagents was unusable, and alchemy starts with
|
||||
-- finding reagents.
|
||||
--
|
||||
-- This announces the same thing in words, and goes one better: the overlay can
|
||||
-- only draw ONE icon per tile, so it tells a sighted player "there is something
|
||||
-- in that chest" but not what. We read the whole `tile.loot` list.
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
-- Why this is polled rather than key-bound
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- `s` is a game-claimed scancode, and keybindings.getCommand() dispatches on
|
||||
-- scancode ignoring modifiers, so binding anything on it in our layer would
|
||||
-- either eat the game's own command or fire alongside a modifier we don't want.
|
||||
-- Instead we watch `levelWindow.itemsHighlightEnabled` and speak on its rising
|
||||
-- edge. That is the single flag both `lootOverlayClick` and its mouse-button
|
||||
-- twin set, so this covers:
|
||||
--
|
||||
-- * the key, in hold mode (config `holdForItemHighlight`) and in toggle mode,
|
||||
-- * clicking the toolbar icon,
|
||||
-- * `ingame`, `ingame_observe` and both targeting states, which all share the
|
||||
-- one levelWindow instance (it is created once in ingame.open and passed on
|
||||
-- through params).
|
||||
--
|
||||
-- No game file is touched, and the player's own key stays whatever they have
|
||||
-- rebound it to.
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
-- FoV
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Membership matches levelWindow's own test exactly: current `FoV` only (not
|
||||
-- rememberedFoV), tile not hidden, `visibilityAlpha ~= 0`, loot list non-empty.
|
||||
-- Nothing is reported that is not being drawn on screen at that moment.
|
||||
|
||||
local speech = require("accessibility.speech")
|
||||
local describe = require("accessibility.describe")
|
||||
local history = require("accessibility.history")
|
||||
local menus = require("accessibility.menus")
|
||||
|
||||
local lootoverlay = {}
|
||||
|
||||
local config = {
|
||||
-- Half-width of the scanned box around the player, in tiles. Sight range is
|
||||
-- well inside this; the overlay itself only draws within the camera view.
|
||||
radius = 20,
|
||||
|
||||
-- Longest utterance worth producing in one go. A warehouse floor covered in
|
||||
-- piles would otherwise read for a minute with no way to stop partway.
|
||||
maxTiles = 12,
|
||||
}
|
||||
|
||||
lootoverlay.config = config
|
||||
|
||||
local wasEnabled = false
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Reading the level
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
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 itemTemplates()
|
||||
local ok, templates = pcall(require, "modules.item.templates")
|
||||
return ok and templates or nil
|
||||
end
|
||||
|
||||
-- Loot entries are item *parameters* ({templateId, quantity}), not constructed
|
||||
-- items — modules/lootPopup.lua builds real items from them only when the popup
|
||||
-- opens. So the name comes from the template, and constructing an item here
|
||||
-- (which allocates canvases) is avoided.
|
||||
local function itemText(entry, templates)
|
||||
if type(entry) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local template = entry.templateId and templates and templates[entry.templateId]
|
||||
local name = (template and template.name) or entry.name or entry.templateId
|
||||
|
||||
if not name then
|
||||
return nil
|
||||
end
|
||||
|
||||
local quantity = tonumber(entry.quantity) or 1
|
||||
|
||||
-- Quantities can be fractional (distilling yields x0.2), and floatfix leaves
|
||||
-- long tails behind. Two decimals is what the game's own quantity label shows.
|
||||
quantity = math.floor(quantity * 100 + 0.5) / 100
|
||||
|
||||
if quantity == 1 then
|
||||
return tostring(name)
|
||||
end
|
||||
|
||||
return tostring(name) .. ", " .. tostring(quantity)
|
||||
end
|
||||
|
||||
-- The tiles the overlay is drawing an icon on, nearest first.
|
||||
-- Each entry: {x, y, distance, container, items = {text, ...}}
|
||||
function lootoverlay.scan()
|
||||
local level = getLevel()
|
||||
local actor = level and level.playerActor
|
||||
|
||||
if not actor or not actor.x or not actor.y then
|
||||
return nil
|
||||
end
|
||||
|
||||
local templates = itemTemplates()
|
||||
local px, py = actor.x, actor.y
|
||||
|
||||
local xMin = math.max(1, px - config.radius)
|
||||
local xMax = math.min(level.xMax or 0, px + config.radius)
|
||||
local yMin = math.max(1, py - config.radius)
|
||||
local yMax = math.min(level.yMax or 0, py + config.radius)
|
||||
|
||||
local found = {}
|
||||
|
||||
for x = xMin, xMax do
|
||||
if level.grid[x] then
|
||||
for y = yMin, yMax do
|
||||
local visible = level.FoV and level.FoV[x] and level.FoV[x][y]
|
||||
local stack = visible and level.grid[x][y]
|
||||
|
||||
if stack then
|
||||
-- Top down, and stop at the first loot-bearing tile: that is the
|
||||
-- one whose icon levelWindow draws.
|
||||
for i = #stack, 1, -1 do
|
||||
local tile = stack[i]
|
||||
|
||||
if tile and not tile.isHidden and tile.visibilityAlpha ~= 0
|
||||
and tile.loot and tile.loot[1]
|
||||
then
|
||||
local items = {}
|
||||
for _, entry in ipairs(tile.loot) do
|
||||
local text = itemText(entry, templates)
|
||||
if text then
|
||||
items[#items + 1] = text
|
||||
end
|
||||
end
|
||||
|
||||
found[#found + 1] = {
|
||||
x = x,
|
||||
y = y,
|
||||
distance = describe.distance(x - px, y - py),
|
||||
container = tile.description or tile.name,
|
||||
unlootable = tile.unlootable and true or false,
|
||||
items = items,
|
||||
}
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(found, function(a, b)
|
||||
if a.distance ~= b.distance then
|
||||
return a.distance < b.distance
|
||||
end
|
||||
if a.x ~= b.x then
|
||||
return a.x < b.x
|
||||
end
|
||||
return a.y < b.y
|
||||
end)
|
||||
|
||||
return found, px, py
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Phrasing
|
||||
--
|
||||
-- Pure and exposed, so the testbed can assert the wording without capturing
|
||||
-- speech.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- One block per container, so the console history can walk them back with [ and ]
|
||||
-- after the utterance has gone by — a chest with six reagents in it is more than
|
||||
-- anyone holds in their head on one hearing.
|
||||
function lootoverlay.blocks(found, px, py)
|
||||
if not found then
|
||||
return nil
|
||||
end
|
||||
|
||||
if #found == 0 then
|
||||
return {"No loot in view"}
|
||||
end
|
||||
|
||||
local blocks = {}
|
||||
local shown = math.min(#found, config.maxTiles)
|
||||
|
||||
for i = 1, shown do
|
||||
local entry = found[i]
|
||||
local parts = {}
|
||||
|
||||
if entry.container then
|
||||
-- Descriptions are sentences ("A wooden chest."); the trailing stop would
|
||||
-- land mid-clause once we append the position.
|
||||
parts[#parts + 1] = (entry.container:gsub("%.%s*$", ""))
|
||||
else
|
||||
parts[#parts + 1] = "something"
|
||||
end
|
||||
|
||||
parts[#parts + 1] = describe.offset(entry.x - px, entry.y - py)
|
||||
|
||||
if entry.unlootable then
|
||||
parts[#parts + 1] = "cannot be taken"
|
||||
end
|
||||
|
||||
if #entry.items > 0 then
|
||||
parts[#parts + 1] = table.concat(entry.items, ", ")
|
||||
else
|
||||
parts[#parts + 1] = "contents unknown"
|
||||
end
|
||||
|
||||
blocks[#blocks + 1] = table.concat(parts, ", ")
|
||||
end
|
||||
|
||||
if #found > shown then
|
||||
blocks[#blocks + 1] = string.format("and %d more", #found - shown)
|
||||
end
|
||||
|
||||
return blocks
|
||||
end
|
||||
|
||||
function lootoverlay.speak()
|
||||
local found, px, py = lootoverlay.scan()
|
||||
|
||||
if not found then
|
||||
return
|
||||
end
|
||||
|
||||
local blocks = lootoverlay.blocks(found, px, py)
|
||||
|
||||
-- Interrupting: this is a direct answer to a key the player just pressed.
|
||||
speech.say(table.concat(blocks, ". "), true)
|
||||
|
||||
if #found > 0 then
|
||||
for _, block in ipairs(blocks) do
|
||||
pcall(history.append, block, "loot")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Watching the game's flag
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local levelWindow
|
||||
|
||||
-- The instance is a file-local of states/ingame, created once in open() and
|
||||
-- handed to the other ingame states through params. Cached, because
|
||||
-- getAnyUpvalue walks every function in the state table.
|
||||
local function getLevelWindow()
|
||||
if levelWindow then
|
||||
return levelWindow
|
||||
end
|
||||
|
||||
local stateTable = menus.getStateTable("ingame")
|
||||
levelWindow = menus.getAnyUpvalue(stateTable, "levelWindow")
|
||||
|
||||
return levelWindow
|
||||
end
|
||||
|
||||
-- Only the ingame family draws the overlay, and only there is levelWindow even
|
||||
-- assigned — so this also keeps getAnyUpvalue from being retried every frame
|
||||
-- while the player is sitting in the main menu.
|
||||
local function inGameplay()
|
||||
local ok, states = pcall(require, "states")
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
local current = states.getCurrent()
|
||||
return type(current) == "string" and current:sub(1, 6) == "ingame"
|
||||
end
|
||||
|
||||
-- Version-bump canary for the startup self-test. If `levelWindow` is ever
|
||||
-- renamed in states/ingame.lua this reports it at startup, instead of the loot
|
||||
-- key going quietly silent halfway through a playthrough.
|
||||
function lootoverlay.probeStatus()
|
||||
local stateTable = menus.getStateTable("ingame")
|
||||
|
||||
if not stateTable then
|
||||
return "BROKEN: cannot load states/ingame"
|
||||
end
|
||||
|
||||
if not menus.hasUpvalue(stateTable, "levelWindow") then
|
||||
return "PROBE BROKEN: no upvalue 'levelWindow' in states/ingame"
|
||||
end
|
||||
|
||||
return menus.getAnyUpvalue(stateTable, "levelWindow")
|
||||
and "ok" or "ok (unset until gameplay starts)"
|
||||
end
|
||||
|
||||
function lootoverlay.update()
|
||||
if not inGameplay() then
|
||||
return
|
||||
end
|
||||
|
||||
local window = getLevelWindow()
|
||||
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
local enabled = window.itemsHighlightEnabled and true or false
|
||||
|
||||
if enabled and not wasEnabled then
|
||||
lootoverlay.speak()
|
||||
end
|
||||
|
||||
wasEnabled = enabled
|
||||
end
|
||||
|
||||
return lootoverlay
|
||||
@@ -753,6 +753,113 @@ local function scenario()
|
||||
print("[testbed] viewwatch: " .. tostring(text))
|
||||
end
|
||||
|
||||
|
||||
------------------------------------------------------------------
|
||||
-- Loot overlay ("s"): speak the contents of visible containers
|
||||
--
|
||||
-- The tutorial house is the awkward case: chest_tutorial has no loot
|
||||
-- by design ("you find nothing of use for now"), so a live scan here
|
||||
-- may legitimately be empty. The phrasing is therefore asserted on
|
||||
-- the pure seam with a synthetic container, and the live scan is used
|
||||
-- for the FoV invariant and for "it does not throw".
|
||||
------------------------------------------------------------------
|
||||
if currentState() == "ingame" then
|
||||
local lootoverlay = require("accessibility.lootoverlay")
|
||||
|
||||
local found, px, py = lootoverlay.scan()
|
||||
check("loot overlay scan returns a list", type(found) == "table",
|
||||
type(found) .. ", " .. tostring(found and #found) .. " containers")
|
||||
|
||||
if found then
|
||||
local ok, level = pcall(require, "models.world")
|
||||
level = ok and level.currentLevel
|
||||
|
||||
-- Invariant 4: the overlay draws only inside current FoV, and so
|
||||
-- must we -- this must never become an x-ray of sealed rooms.
|
||||
local leaked
|
||||
for _, entry in ipairs(found) do
|
||||
if not (level and level.FoV and level.FoV[entry.x]
|
||||
and level.FoV[entry.x][entry.y]) then
|
||||
leaked = entry.container
|
||||
end
|
||||
end
|
||||
check("loot overlay reports nothing outside FoV", leaked == nil,
|
||||
leaked and ("leaked " .. tostring(leaked)) or "clean")
|
||||
|
||||
local liveBlocks = lootoverlay.blocks(found, px, py)
|
||||
check("loot overlay always says something",
|
||||
type(liveBlocks) == "table" and #liveBlocks > 0,
|
||||
table.concat(liveBlocks or {}, " | "))
|
||||
print("[testbed] loot overlay: " .. table.concat(liveBlocks or {}, ". "))
|
||||
end
|
||||
|
||||
-- Synthetic container, two tiles east of the player: name, quantity,
|
||||
-- position, and the trailing full stop stripped off the description.
|
||||
local synthetic = {{
|
||||
x = px + 2, y = py, distance = 2,
|
||||
container = "A wooden chest.",
|
||||
items = {"Ivy leaves, 3", "A torch"},
|
||||
}}
|
||||
local blocks = lootoverlay.blocks(synthetic, px, py)
|
||||
local text = table.concat(blocks, ". ")
|
||||
|
||||
check("loot overlay names the container and where it is",
|
||||
text:find("A wooden chest, 2 tiles east", 1, true) ~= nil, text)
|
||||
check("loot overlay lists the contents, not just the top item",
|
||||
text:find("Ivy leaves, 3", 1, true) and text:find("A torch", 1, true) ~= nil,
|
||||
text)
|
||||
check("loot overlay says so when there is nothing to see",
|
||||
lootoverlay.blocks({}, px, py)[1] == "No loot in view",
|
||||
tostring(lootoverlay.blocks({}, px, py)[1]))
|
||||
|
||||
-- Live scan of the REAL grid, which the prologue cannot otherwise
|
||||
-- exercise: chest_tutorial is empty by design, so without this the
|
||||
-- only path ever run is the "nothing here" one. Loot is planted on
|
||||
-- the tile the player is standing on (guaranteed visible) and put
|
||||
-- back immediately -- this must not alter the playthrough.
|
||||
do
|
||||
local ok, world = pcall(require, "models.world")
|
||||
local level = ok and world.currentLevel
|
||||
local tile = level and level.grid[px] and level.grid[px][py]
|
||||
and level.grid[px][py][1]
|
||||
|
||||
if tile then
|
||||
local realLoot = tile.loot
|
||||
tile.loot = {{templateId = "ivy_leaves", quantity = 3}}
|
||||
|
||||
local planted, ppx, ppy = lootoverlay.scan()
|
||||
local plantedText = table.concat(
|
||||
lootoverlay.blocks(planted, ppx, ppy) or {}, ". ")
|
||||
|
||||
tile.loot = realLoot
|
||||
|
||||
check("loot overlay finds loot on the real grid",
|
||||
#planted == 1, #planted .. " containers")
|
||||
-- Loot entries are item PARAMS, not items: the name has to come
|
||||
-- from the template table, which is the part most likely to move.
|
||||
check("loot overlay resolves item names from templates",
|
||||
plantedText:lower():find("ivy", 1, true) ~= nil, plantedText)
|
||||
check("loot overlay reports the quantity",
|
||||
plantedText:find(", 3", 1, true) ~= nil, plantedText)
|
||||
check("loot overlay restored the tile it borrowed",
|
||||
tile.loot == realLoot, "restored")
|
||||
end
|
||||
end
|
||||
|
||||
-- The probe the whole feature hangs on: states/ingame's levelWindow.
|
||||
check("loot overlay can still reach levelWindow",
|
||||
lootoverlay.probeStatus() == "ok", tostring(lootoverlay.probeStatus()))
|
||||
|
||||
-- And end to end: the game's own key must reach the rising edge.
|
||||
local history = require("accessibility.history")
|
||||
local baseline = history.count()
|
||||
press("s", "loot overlay key")
|
||||
check("the loot key did not leave gameplay", currentState() == "ingame",
|
||||
"got " .. currentState())
|
||||
print("[testbed] loot key added " .. (history.count() - baseline) ..
|
||||
" history lines (0 is correct when nothing is lootable nearby)")
|
||||
end
|
||||
|
||||
if sliderTested then
|
||||
check("slider announces value changes", true, "via ingame_wait")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user