d2a4cf0d23
ADChainData leverer data.fieldGoods (vindue-varer, spillets konvertere, tørrede varianter og kendte markvarer der findes som fill type). En markvare uden producent/stald får kilden "fra marker" uden afgrødeplan og uden problem, så fermenteringssiloernes hø/lucerne/kløver/hakkelse ikke meldes som køb.
168 lines
7.5 KiB
Lua
168 lines
7.5 KiB
Lua
-- AD Chain Model (ren logik)
|
|
-- Sporer baglæns fra de produktioner spilleren selv har tændt (rødder) gennem gårdens egne fabrikker og stalde
|
|
-- til råvarerne. Alle mængder er liter pr. måned; varer og afgrøder er NAVNE. Se spec 2026-09-30-produktionskaeder.
|
|
|
|
ADChainModel = {}
|
|
ADChainModel.MAX_DEPTH = 8
|
|
|
|
local function byKey(a, b) return tostring(a.key) < tostring(b.key) end
|
|
|
|
-- producenter pr. vare: tændte først, så stabil rækkefølge
|
|
local function indexProducers(recipes)
|
|
local index = {}
|
|
for _, recipe in ipairs(recipes or {}) do
|
|
for name, amount in pairs(recipe.outputs or {}) do
|
|
if (amount or 0) > 0 and (recipe.cyclesPerMonth or 0) > 0 then
|
|
index[name] = index[name] or {}
|
|
table.insert(index[name], recipe)
|
|
end
|
|
end
|
|
end
|
|
for _, list in pairs(index) do
|
|
table.sort(list, function(a, b)
|
|
if (a.enabled == true) ~= (b.enabled == true) then return a.enabled == true end
|
|
return byKey(a, b)
|
|
end)
|
|
end
|
|
return index
|
|
end
|
|
|
|
local function indexHerds(husbandry)
|
|
local index = {}
|
|
for _, herd in ipairs(husbandry or {}) do
|
|
for name, perMonth in pairs(herd.outputs or {}) do
|
|
if (perMonth or 0) > 0 then
|
|
index[name] = index[name] or {}
|
|
table.insert(index[name], {building = herd.building, buildingName = herd.buildingName, left = perMonth})
|
|
end
|
|
end
|
|
end
|
|
return index
|
|
end
|
|
|
|
local function sortedNames(map)
|
|
local names = {}
|
|
for name in pairs(map or {}) do table.insert(names, name) end
|
|
table.sort(names)
|
|
return names
|
|
end
|
|
|
|
-- fieldGoods = {[vare] = true}: varer der høstes/laves på marken uden at være en afgrødes hovedvare (hø, hakkelse,
|
|
-- halm …). Uden producent/stald er kilden "field" uden afgrøde: intet markbehov og intet problem.
|
|
function ADChainModel.build(recipes, husbandry, crops, fieldGoods)
|
|
local chain = {roots = {}, demand = {}, fieldDemand = {}, needRecipes = {}, problems = {}}
|
|
local producers, herds = indexProducers(recipes), indexHerds(husbandry)
|
|
-- Rødderne kører for sig selv med fuld fart: deres egne input kræves af deres eget træ. Som leverandør til en
|
|
-- anden rod har de derfor fuld kapacitet, men deres input regnes ikke med igen (standalone = ingen rekursion).
|
|
-- En pauset rod (recipe.paused) kører også for sig selv, men er ikke rod: kæden skaber intet nyt for den.
|
|
local capacity, roots, standalone = {}, {}, {}
|
|
for _, recipe in ipairs(recipes or {}) do
|
|
capacity[recipe.key] = recipe.cyclesPerMonth or 0
|
|
if recipe.enabled and not recipe.byChain then
|
|
if not recipe.paused then table.insert(roots, recipe) end
|
|
standalone[recipe.key] = true
|
|
end
|
|
end
|
|
table.sort(roots, byKey)
|
|
|
|
-- detail = manglende liter/md (flaskehals/køb), need = varens behov pr. måned i den gren
|
|
local function problem(kind, fillType, rootKey, detail, need)
|
|
table.insert(chain.problems, {kind = kind, fillType = fillType, rootKey = rootKey, detail = detail, need = need})
|
|
end
|
|
|
|
local need
|
|
local function inputsOf(recipe, cycles, depth, path, rootKey)
|
|
local children = {}
|
|
for _, name in ipairs(sortedNames(recipe.inputs)) do
|
|
local amount = recipe.inputs[name] or 0
|
|
if amount > 0 and cycles > 0 then
|
|
local child = need(name, amount * cycles, depth + 1, path, rootKey)
|
|
child.consumer, child.consumerName = recipe.building, recipe.buildingName
|
|
table.insert(children, child)
|
|
end
|
|
end
|
|
return children
|
|
end
|
|
|
|
need = function(name, perMonth, depth, path, rootKey)
|
|
local node = {fillType = name, perMonth = perMonth, coveredPerMonth = 0, producers = {}, herds = {}, children = {}}
|
|
chain.demand[name] = (chain.demand[name] or 0) + perMonth
|
|
if depth > ADChainModel.MAX_DEPTH or path[name] then
|
|
node.source = "cycle"
|
|
problem("cycle", name, rootKey, nil, perMonth)
|
|
return node
|
|
end
|
|
path[name] = true
|
|
local rest = perMonth
|
|
-- a) gårdens fabrikker
|
|
local makers = producers[name]
|
|
if makers ~= nil then
|
|
node.source = "production"
|
|
for _, recipe in ipairs(makers) do
|
|
local perCycle = recipe.outputs[name]
|
|
local cycles = math.min(rest / perCycle, capacity[recipe.key] or 0)
|
|
if cycles > 0 then
|
|
capacity[recipe.key] = capacity[recipe.key] - cycles
|
|
rest = rest - cycles * perCycle
|
|
table.insert(node.producers, {key = recipe.key, building = recipe.building, buildingName = recipe.buildingName, cycles = cycles})
|
|
if not standalone[recipe.key] then
|
|
chain.needRecipes[recipe.key] = (chain.needRecipes[recipe.key] or 0) + cycles
|
|
for _, child in ipairs(inputsOf(recipe, cycles, depth, path, rootKey)) do table.insert(node.children, child) end
|
|
end
|
|
end
|
|
if rest <= 1e-9 then break end
|
|
end
|
|
if rest > 1e-9 and #node.producers > 0 then
|
|
problem("bottleneck", name, rootKey, rest, perMonth)
|
|
node.shortPerMonth = rest
|
|
end
|
|
if rest > 1e-9 and #node.producers == 0 then
|
|
node.source = nil
|
|
end
|
|
end
|
|
-- b) stalde
|
|
if rest > 1e-9 and #node.producers == 0 and herds[name] ~= nil then
|
|
node.source = "husbandry"
|
|
for _, herd in ipairs(herds[name]) do
|
|
local take = math.min(rest, herd.left)
|
|
if take > 0 then
|
|
herd.left = herd.left - take
|
|
rest = rest - take
|
|
table.insert(node.herds, {building = herd.building, buildingName = herd.buildingName, perMonth = take})
|
|
end
|
|
end
|
|
end
|
|
-- c) mark
|
|
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and crops ~= nil and crops[name] ~= nil then
|
|
node.source, node.fruit = "field", crops[name]
|
|
chain.fieldDemand[node.fruit] = (chain.fieldDemand[node.fruit] or 0) + rest
|
|
rest = 0
|
|
end
|
|
-- c2) markvare uden egen afgrøde (fx hø, hakkelse): kommer fra markerne, ingen afgrødeplan
|
|
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and fieldGoods ~= nil and fieldGoods[name] then
|
|
node.source, node.fruit = "field", nil
|
|
rest = 0
|
|
end
|
|
-- d) ingen kilde (eller rest efter stald), eller exhausted producers
|
|
if rest > 1e-9 and makers ~= nil and #node.producers == 0 and #node.herds == 0 then
|
|
node.source = "production"
|
|
node.shortPerMonth = rest
|
|
problem("bottleneck", name, rootKey, rest, perMonth)
|
|
elseif rest > 1e-9 and (node.source == nil or node.source == "husbandry") then
|
|
if node.source == nil then node.source = "buy" end
|
|
node.shortPerMonth = rest
|
|
problem("buy", name, rootKey, rest, perMonth)
|
|
end
|
|
node.coveredPerMonth = perMonth - (node.shortPerMonth or 0)
|
|
path[name] = nil
|
|
return node
|
|
end
|
|
|
|
for _, root in ipairs(roots) do
|
|
local cycles = root.cyclesPerMonth or 0
|
|
table.insert(chain.roots, {key = root.key, name = root.name, building = root.building, buildingName = root.buildingName,
|
|
children = inputsOf(root, cycles, 0, {}, root.key)})
|
|
end
|
|
return chain
|
|
end
|