Files

182 lines
5.9 KiB
Lua
Raw Permalink Normal View History

-- states/research.lua — the research tree.
--
-- Not a list: a 2D graph of nodes navigated with the arrow keys, where up/down
-- jump to the nearest node on the adjacent row. Nodes are drawn as icons with no
-- text label, so the focus line has to carry the name itself, and the useful
-- facts are whether the node is already unlocked, whether it CAN be unlocked
-- yet, and whether you can afford it.
--
-- Without that last part the screen is unusable by ear: pressing enter on an
-- unaffordable node simply does nothing.
--
-- Everything else a sighted player gets -- what the node costs in knowledge,
-- which recipes it unlocks, the flavour description, and their own knowledge
-- stock -- is drawn in the hint windows down the side, and is served here through
-- details() on `=`, the same key the item grids use. The state assembles all of
-- it into one `currentNode` table (states/research.lua, updateCurrentNode), so
-- that upvalue is the single place to read rather than reassembling it from
-- models.research, whose `nodes` table is file-local and whose unlock ids are
-- recipe ids rather than the item names the window shows.
local researchModel = require("models.research")
-- The order the game draws the knowledge icons in, so cost and stock read in a
-- stable order rather than pairs() order, which varies between runs.
local knowledgeTypes = {"practical", "ecclesiastic", "academic", "occult"}
-- "alchemy_basics_2" -> "alchemy basics 2". Only a fallback now: currentNode.title
-- is the real name, and this is what is left if the hint has not been built yet.
local function humanise(id)
return (tostring(id):gsub("_", " "))
end
local function currentNode(stateTable, menus)
local node = menus.getAnyUpvalue(stateTable, "currentNode")
if type(node) ~= "table" then
return nil
end
return node
end
-- The player's knowledge stock. Required lazily: models.journal drags in a lot of
-- the game, and a require at load time would run before the game is up.
local function knowledgeStock()
local ok, journalModel = pcall(require, "models.journal")
if not ok or type(journalModel) ~= "table" then
return {}
end
return journalModel.knowledge or {}
end
-- "4 practical, 2 occult", in draw order, or nil if the node is free.
local function costPhrase(cost)
if type(cost) ~= "table" then
return nil
end
local parts = {}
for _, knowledgeType in ipairs(knowledgeTypes) do
local amount = cost[knowledgeType]
if amount and amount > 0 then
parts[#parts + 1] = amount .. " " .. knowledgeType
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, ", ")
end
return {
probeNames = {"currentNodeId", "currentNode"},
read = function(stateTable, menus)
local nodeId = menus.getAnyUpvalue(stateTable, "currentNodeId")
if nodeId == nil then
return nil, nil
end
local node = currentNode(stateTable, menus)
local parts = {(node and node.title) or humanise(nodeId)}
local unlocked = researchModel.unlockedNodes and researchModel.unlockedNodes[nodeId]
if unlocked then
parts[#parts + 1] = "researched"
else
local okUnlockable, unlockable = pcall(researchModel.isUnlockable, nodeId)
local okAfford, affordable = pcall(researchModel.canAffordNode, nodeId)
if okUnlockable and not unlockable then
parts[#parts + 1] = "locked"
elseif okAfford and not affordable then
parts[#parts + 1] = "cannot afford"
else
parts[#parts + 1] = "available"
end
end
return table.concat(parts, ", "), tostring(nodeId) .. ":" .. tostring(unlocked)
end,
-- One block per part of the hint window -- cost, unlocks, description -- so
-- that [ and ] can walk back through them after the utterance has gone by.
details = function(stateTable, menus, verbose)
local node = currentNode(stateTable, menus)
if not node then
return nil
end
local blocks = {}
if verbose then
blocks[#blocks + 1] = node.title or humanise(node.id)
end
local unlocked = researchModel.unlockedNodes and researchModel.unlockedNodes[node.id]
local cost = costPhrase(node.knowledgeCost)
if unlocked then
blocks[#blocks + 1] = "Researched already"
elseif cost then
-- Say the shortfall, not just the price: "cannot afford" on the focus
-- line doesn't say by how much, and the stock it is being compared
-- against is drawn in a separate window the player would otherwise
-- have to hold in their head.
local stock = knowledgeStock()
local missing = {}
for _, knowledgeType in ipairs(knowledgeTypes) do
local amount = node.knowledgeCost[knowledgeType]
if amount and amount > 0 then
local have = stock[knowledgeType] or 0
if have < amount then
missing[#missing + 1] = (amount - have) .. " " .. knowledgeType
end
end
end
if #missing > 0 then
blocks[#blocks + 1] = "Costs " .. cost .. " knowledge, short " .. table.concat(missing, ", ")
else
blocks[#blocks + 1] = "Costs " .. cost .. " knowledge, affordable"
end
else
blocks[#blocks + 1] = "Costs nothing"
end
-- node.unlocks is already the display list the hint window draws under
-- "Unlocks": the ITEM each recipe produces, named for display.
if type(node.unlocks) == "table" and #node.unlocks > 0 then
local names = {}
for _, unlock in ipairs(node.unlocks) do
names[#names + 1] = unlock.title or "?"
end
blocks[#blocks + 1] = "Unlocks " .. table.concat(names, ", ")
end
if node.description then
blocks[#blocks + 1] = node.description
end
-- Verbose only: the whole knowledge purse, including types this node does
-- not want. Worth having when planning, noise the rest of the time.
if verbose then
local stock = knowledgeStock()
local held = {}
for _, knowledgeType in ipairs(knowledgeTypes) do
held[#held + 1] = (stock[knowledgeType] or 0) .. " " .. knowledgeType
end
blocks[#blocks + 1] = "Knowledge available, " .. table.concat(held, ", ")
end
if #blocks == 0 then
return nil
end
return blocks
end,
}