Files
alchemist-accessibility/accessibility/states/worldmap.lua
T

184 lines
5.9 KiB
Lua
Raw Normal View History

2026-08-01 22:00:52 +02:00
-- states/worldmap.lua — travel destinations.
--
-- The screen is a painted map of Rimesia with area icons at real coordinates,
-- plus a description window carrying the area's blurb and the travel time. The
-- list on the side is only half of it: the icon says what KIND of place each area
-- is, the position says where it lies, and the description window says how long
-- getting there costs. All three were silent.
--
-- Why read() rather than probe/render: travel time needs the screen's
-- `currentAreaId`, and render(entry, index, total) is handed no state table.
-- `options`, `selectedOption` and `currentAreaId` are all file-locals in the same
-- chunk (states/worldmap.lua) and all are upvalues of exported functions, so
-- menus.getAnyUpvalue reaches them. `currentAreaId` is nil until the screen has
-- been opened once -- treat that as "omit the travel time", never as an error.
--
-- The blurb goes through details() on `=` rather than the focus line: it runs to
-- several sentences, and arrowing through nine areas would be unbearable.
local describe = require("accessibility.describe")
local worldmapModel = require("models.worldmap")
-- The 20 icon ids (models/interface/mapIcons.lua) are already readable words once
-- the underscores go: town, ruined_house, pirate_grave, witchwood... so this is a
-- transform rather than a lookup table, which also means a new id in a future
-- game version reads sensibly instead of vanishing.
--
-- Deliberately NOT an addition to mapIcons: icons.preprocess iterates that table
-- with pairs() and would try to treat any new table in it as an image.
local function areaType(iconId)
if type(iconId) ~= "string" then
return nil
end
return (iconId:gsub("_", " "))
end
-- The model area behind a list entry, or nil. worldmap.areas holds `false` in the
-- slots of erased areas (models/worldmap.lua:173), hence the type check.
local function modelArea(areaId)
if type(areaId) ~= "number" then
return nil
end
local area = worldmapModel.areas and worldmapModel.areas[areaId]
if type(area) ~= "table" then
return nil
end
return area
end
-- Compass bearing from one area to another in 0..1 map space.
--
-- describe.direction only tests the sign of each delta, so on floating-point
-- coordinates it would call almost everything diagonal. The deadzone below
-- suppresses a component that is small next to the other one, so a place almost
-- due north reads as "north" rather than "north-east". The 4/3 on x matches the
-- aspect correction the game's own distance maths uses
-- (models/worldmap.lua:64) so the bearing agrees with the travel time.
local DEADZONE = 0.25
local function bearing(fromArea, toArea)
local dx = (toArea.x - fromArea.x) * 4 / 3
local dy = toArea.y - fromArea.y
local ax, ay = math.abs(dx), math.abs(dy)
if ax < ay * DEADZONE then
dx = 0
elseif ay < ax * DEADZONE then
dy = 0
end
if dx == 0 and dy == 0 then
return nil
end
return describe.direction(dx, dy)
end
-- The focused list entry, plus the id of the area the player is standing in.
local function focus(stateTable, menus)
local options = menus.getAnyUpvalue(stateTable, "options")
local selected = menus.getAnyUpvalue(stateTable, "selectedOption")
if type(options) ~= "table" or type(selected) ~= "number" then
return nil
end
local entry = options[selected]
if type(entry) ~= "table" then
return nil
end
return entry, selected, #options, menus.getAnyUpvalue(stateTable, "currentAreaId")
end
2026-08-01 22:00:52 +02:00
return {
-- Checked by menus.selfTest at startup: a game version that renames any of
-- these says so on the console instead of going quiet on the travel screen.
probeNames = {"options", "selectedOption", "currentAreaId"},
read = function(stateTable, menus)
local entry, index, total, currentAreaId = focus(stateTable, menus)
if not entry then
return nil, nil
end
2026-08-01 22:00:52 +02:00
local parts = {entry.name or "Unknown area"}
-- entry.icon is already an image object (states/worldmap.lua:240); the id
-- string lives on the model area.
local area = modelArea(entry.id)
local kind = area and areaType(area.icon)
if kind then
parts[#parts + 1] = kind
end
parts[#parts + 1] = index .. " of " .. total
local here = modelArea(currentAreaId)
if here and area and entry.id ~= currentAreaId then
local which = bearing(here, area)
if which then
parts[#parts + 1] = which
end
end
-- The game's own string, already finished English, including "You are
-- currently here." Same call the screen makes at states/worldmap.lua:360.
local travel
if currentAreaId and entry.id then
local ok, message = pcall(worldmapModel.getDistanceMessage, currentAreaId, entry.id)
if ok and message and message ~= "" then
travel = message
parts[#parts + 1] = travel
end
end
-- "Visited 3 days ago": on a map of similar-sounding place names this is
-- the main way to tell where you have been.
--
-- Skipped when it duplicates the travel line. For the area you are
-- standing in, getDistanceMessage and getLastVisitedMessage return the
-- SAME sentence -- "You are currently here." (models/worldmap.lua:75-77
-- and :106-107) -- which the screen gets away with because they are drawn
-- in different panels, and which by ear is just said twice.
if entry.visitedMessage and entry.visitedMessage ~= ""
and entry.visitedMessage ~= travel
then
parts[#parts + 1] = entry.visitedMessage
end
2026-08-01 22:00:52 +02:00
if entry.isCompleted then
parts[#parts + 1] = "completed"
end
if entry.isFavourite then
parts[#parts + 1] = "favourite"
end
return table.concat(parts, ", "), index .. "/" .. total .. ":" .. tostring(entry.id)
end,
details = function(stateTable, menus, verbose)
local entry = focus(stateTable, menus)
if not entry then
return nil
2026-08-01 22:00:52 +02:00
end
local blocks = {}
2026-08-01 22:00:52 +02:00
if verbose then
blocks[#blocks + 1] = entry.name or "Unknown area"
end
if entry.description and entry.description ~= "" then
blocks[#blocks + 1] = entry.description
end
if #blocks == 0 then
return nil
end
return blocks
2026-08-01 22:00:52 +02:00
end,
}