feat(kæde): manglende led forklares i Kræver handling, fanen og som besked

problemText giver årsag og råd for køb/flaskehals/kredsløb og 'slukket af dig'
(userOff-kopi på ADChain.last); modellens problemer får need. Fanen Kæder viser
teksten som rød tekstrække under varen (foldet ud). Kræver handling får én rød
post pr. kæde (højst 2, score 3500+alvor mellem rød tom og rød fuld); klik åbner
fanen med kæden foldet ud. Engangsbesked pr. nyt led (rod|vare|art) kun fra
tick, ikke i opstartsfristen eller på delvise data/ufærdig markscanning.
Manualer og CHANGELOG opdateret.
This commit is contained in:
2026-09-30 14:40:29 +02:00
parent 1e7b2ec699
commit 19a10ab67c
12 changed files with 254 additions and 20 deletions
+66 -3
View File
@@ -4,7 +4,9 @@
-- vises altid (under roden eller under afgrøden). Pausede rødder (som modellen ikke længere ser som rødder)
-- får en grå linje med [Genoptag]. Nederst frie marker + antal automatiske skift (+ "marker scannes endnu"). Ingen spiladgang:
-- titleOf(navn) -> titel, fmt(liter) -> kompakt tal, textOf(l10n-nøgle) -> tekst gives udefra.
-- Række: {kind="header"|"root"|"node"|"suggest"|"auto"|"info"|"empty", indent, level, cells={c1..c5}, text,
-- Manglende led (Task 10): foldet ud står problemteksten (problemText) som en rød tekstrække ("problem", ingen celler)
-- lige under varen — teksten er for lang til en celle og ombrydes af fanen.
-- Række: {kind="header"|"root"|"node"|"suggest"|"auto"|"info"|"empty"|"problem", indent, level, cells={c1..c5}, text,
-- actions={[celle]={kind="approve"|"dismiss"|"pause"|"resume", fieldId|rootKey}}, toggle=rootKey, fillType}
ADChainView = {}
@@ -23,7 +25,8 @@ ADChainView.TEXT_KEYS = {"spu_chain_empty", "spu_chain_approve", "spu_chain_dism
"spu_chain_buy", "spu_chain_cycle", "spu_chain_fieldShort", "spu_chain_tight", "spu_chain_noField", "spu_chain_buySource",
"spu_chain_pause", "spu_chain_resume", "spu_chain_scanning", "spu_chain_rootOk", "spu_chain_rootError", "spu_chain_rootWaiting",
"spu_chain_rootPaused", "spu_chain_rootSummary", "spu_chain_free", "spu_chain_switches", "spu_chain_gone", "spu_chain_col_item",
"spu_chain_col_source", "spu_chain_col_perMonth", "spu_chain_col_status", "spu_chain_col_stock"}
"spu_chain_col_source", "spu_chain_col_perMonth", "spu_chain_col_status", "spu_chain_col_stock",
"spu_chain_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle"}
local FLOW_KEYS = {ok = "spu_chain_flowOk", missing = "spu_chain_flowNone", stopped = "spu_chain_flowStopped", noVehicle = "spu_chain_flowMissing",
pallet = "spu_chain_flowPallet"}
@@ -31,6 +34,14 @@ local ROOT_STATUS = {running = "spu_chain_rootOk", error = "spu_chain_rootError"
local function identity(value) return tostring(value) end
-- samme som SmartPickupFrame.formatCompact (standard-fmt når ingen gives, fx i beskeden fra kædens tick)
function ADChainView.compact(liters)
liters = math.floor((liters or 0) + 0.5)
if liters >= 1000000 then return string.format("%.1fM", liters / 1000000)
elseif liters >= 1000 then return string.format("%.1fk", liters / 1000) end
return string.format("%d", liters)
end
-- ---- UTF-8-sikker længde/afkortning (æøå og → er flere bytes) ----
function ADChainView.textLength(text)
@@ -127,6 +138,42 @@ local function joinCapped(names)
return text
end
-- Opskrifter (alle, også slukkede) der laver varen, i datarækkefølge.
local function producersOf(last, fillType)
local list = {}
for _, recipe in ipairs(last.data ~= nil and last.data.recipes or {}) do
if ((recipe.outputs or {})[fillType] or 0) > 0 then table.insert(list, recipe) end
end
return list
end
-- Årsag i klart sprog med råd for et problem fra kædemodellen ({kind="buy"|"bottleneck"|"cycle", fillType, rootKey,
-- detail = manglende l/md, need = behov l/md}). En producent spilleren selv har slukket (last.userOff, stadig
-- slukket) slår igennem som "slukket af dig" — det er den reelle årsag, uanset om modellen så køb eller flaskehals.
function ADChainView.problemText(problem, last, textOf, titleOf, fmt)
last, textOf, fmt = last or {}, textOf or identity, fmt or ADChainView.compact
titleOf = titleOf or function(name) return (last.titles or {})[name] or tostring(name) end
local item = titleOf(problem.fillType)
if problem.kind == "cycle" then return string.format(textOf("spu_chain_problem_cycle"), item) end
local userOff, names, seen = last.userOff or {}, {}, {}
for _, recipe in ipairs(producersOf(last, problem.fillType)) do
if userOff[recipe.key] and not recipe.enabled then
return string.format(textOf("spu_chain_problem_off"), tostring(recipe.buildingName), titleOf(recipe.name))
end
local name = tostring(recipe.buildingName)
if (recipe.cyclesPerMonth or 0) > 0 and not seen[name] then
seen[name] = true
table.insert(names, name)
end
end
if problem.kind == "bottleneck" and #names > 0 then
local need = problem.need or problem.detail or 0
local made = math.max(0, need - (problem.detail or 0))
return string.format(textOf("spu_chain_problem_bottleneck"), joinCapped(names), fmt(math.floor(made + 0.5)), fmt(math.floor(need + 0.5)))
end
return string.format(textOf("spu_chain_problem_buy"), item)
end
local function sourceText(node, last, textOf)
local names = {}
if node.source == "production" then
@@ -225,7 +272,20 @@ local function rootRows(rows, root, last, ctx, isExpanded)
for _, action in ipairs(suggestions) do table.insert(rows, suggestRow(action, 1, ctx.titleOf, ctx.textOf)) end
return
end
local shown = {}
local shown, problems, told = {}, {}, {}
for _, problem in ipairs((last.chain or {}).problems or {}) do
if problem.rootKey == root.key then table.insert(problems, problem) end
end
local function problemRows(fillType, indent)
for _, problem in ipairs(problems) do
local key = tostring(problem.fillType) .. "|" .. tostring(problem.kind)
if (fillType == nil or problem.fillType == fillType) and not told[key] then
told[key] = true
table.insert(rows, {kind = "problem", indent = indent, level = "error",
text = string.rep(ADChainView.INDENT, indent) .. ADChainView.problemText(problem, last, ctx.textOf, ctx.titleOf, ctx.fmt)})
end
end
end
eachNode(root.children, function(node, depth)
local text, nodeLevel = nodeStatus(node, last, ctx.fmt, ctx.textOf, ctx.short)
local stock = node.source == "field" and stockOf(last, node.fruit) or nil
@@ -233,6 +293,7 @@ local function rootRows(rows, root, last, ctx, isExpanded)
sourceText(node, last, ctx.textOf), ctx.fmt(node.perMonth or 0), text, stock ~= nil and ctx.fmt(stock) or ""})
row.fillType = node.fillType
table.insert(rows, row)
problemRows(node.fillType, depth + 1)
if node.source == "field" and node.fruit ~= nil and not shown[node.fruit] then
shown[node.fruit] = true
for _, action in ipairs(actionsFor(last, node.fruit, {suggest = true, auto = true})) do
@@ -240,6 +301,8 @@ local function rootRows(rows, root, last, ctx, isExpanded)
end
end
end)
-- problemer uden en vist vare (bør ikke ske) står sidst under roden
problemRows(nil, 1)
end
-- Pausede rødder: modelRecipes markerer dem paused, så modellen ser dem ikke som rødder -> grå linje med [Genoptag].