-- 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 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 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 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 end local blocks = {} 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 end, }