feat(lagre): 'Kræver handling' → flow: løber tør = åbn/opret forsyningsflow (eller 'producér/køb' når et flow ikke hjælper), sælg nu = åbn/opret salgsflow; hint på hver linje (v1.60.0.0)

This commit is contained in:
2026-09-29 18:30:54 +02:00
parent ccd019d32a
commit 3702073c8c
8 changed files with 213 additions and 2 deletions
+1
View File
@@ -32,6 +32,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupAttentionFlow.lua")
end
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
+37
View File
@@ -283,3 +283,40 @@ function ADStorageStats.fraction(percent)
if percent == nil then return 0 end
return math.max(0, math.min(1, percent / 100))
end
-- ---- "kræver handling" -> flow ----
-- Bygninger der BRUGER varen (fabrik-input, stald-foder) — silo/objektlager lagrer kun. -> {{name, placeable, level}}
function ADStorageStats.consumersOf(inventory, fillType)
local result = {}
for _, building in ipairs(inventory or {}) do
if building.kind == "factory" or building.kind == "husbandry" then
for _, entry in ipairs(building.entries or {}) do
if entry.fillType == fillType and not entry.isOutput then
table.insert(result, {name = building.name, placeable = building.placeable, level = entry.level or 0})
end
end
end
end
return result
end
-- Hjælper et flow? Et flow flytter kun varer: det nytter når varen ligger andre steder end hos forbrugerne.
-- s = {farmLevel, consumerLevel, perDay, consumerCount, existingRunId} -> "open" | "flow" | "produce" | "none"
ADStorageStats.MIN_FLOW_LITERS = 5000
ADStorageStats.MIN_FLOW_DAY_SHARE = 0.25 -- mindst en kvart dags forbrug skal kunne flyttes
function ADStorageStats.flowAdvice(s)
if (s.consumerCount or 0) == 0 then return "none" end
if s.existingRunId ~= nil then return "open" end
local elsewhere = (s.farmLevel or 0) - (s.consumerLevel or 0)
local needed = math.max(ADStorageStats.MIN_FLOW_LITERS, (s.perDay or 0) * ADStorageStats.MIN_FLOW_DAY_SHARE)
return elsewhere >= needed and "flow" or "produce"
end
-- "Sælg nu" -> udkørsels-flow: s = {sourceCount, existingRunId} -> "open" | "flow" | "none"
function ADStorageStats.sellAdvice(s)
if (s.sourceCount or 0) == 0 then return "none" end
if s.existingRunId ~= nil then return "open" end
return "flow"
end
@@ -0,0 +1,130 @@
-- Smart Pickup — "Kræver handling" -> flow (v1.60)
-- En "løber tør"-linje i lagerfanerne kan klikkes: findes der et flow der leverer varen til forbrugerne, åbnes
-- det; ligger varen andre steder på gården, åbnes "Nyt flow" udfyldt (ind til forbrugerne, varen valgt); ellers
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
local function fillTypeName(fillType)
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
end
-- Forsyningsflow der allerede leverer varen til en af forbrugerne -> run eller nil.
local function existingSupplyRun(consumerIds, name)
for _, run in ipairs(ADRunsController.store.runs) do
if run.kind ~= "outbound" then
local carries = run.ingredientMode ~= "manual"
for _, ingredient in ipairs(run.ingredients or {}) do
if ingredient == name then carries = true end
end
if carries then
for _, id in ipairs(run.targetBuildingIds or {}) do
if consumerIds[id] then return run end
end
end
end
end
return nil
end
-- Udkørselsflow der allerede sælger varen fra en af bygningerne -> run eller nil.
local function existingSellRun(sourceIds, name)
for _, run in ipairs(ADRunsController.store.runs) do
if run.kind == "outbound" then
local sells = false
for _, product in ipairs(run.products or {}) do
if product.fillType == name and (product.policy == "sell" or product.policy == "sellElseStore") then sells = true end
end
if sells then
for _, id in ipairs(run.sourceBuildingIds or {}) do
if sourceIds[id] then return run end
end
end
end
end
return nil
end
-- "Sælg nu": kilder = fanens bygninger med varen.
local function sellAdviceFor(stat)
local sourceIds, ids = {}, {}
for _, building in ipairs(stat.buildings or {}) do
local id = building.placeable ~= nil and ADBuildings.getId(building.placeable) or nil
if id ~= nil and not sourceIds[id] then
sourceIds[id] = true
table.insert(ids, id)
end
end
local name = fillTypeName(stat.fillType)
local run = existingSellRun(sourceIds, name)
local advice = ADStorageStats.sellAdvice({sourceCount = #ids, existingRunId = run ~= nil and run.id or nil})
return advice, {buildingIds = ids, run = run, name = name, stat = stat, sell = true}
end
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
function SmartPickupFrame:attentionAdvice(entry)
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
local stat = entry.stat
local consumers = ADStorageStats.consumersOf(self.lastFullInventory, stat.fillType)
local consumerIds, consumerLevel, ids = {}, 0, {}
for _, consumer in ipairs(consumers) do
consumerLevel = consumerLevel + consumer.level
local id = consumer.placeable ~= nil and ADBuildings.getId(consumer.placeable) or nil
if id ~= nil and not consumerIds[id] then
consumerIds[id] = true
table.insert(ids, id)
end
end
local name = fillTypeName(stat.fillType)
local run = existingSupplyRun(consumerIds, name)
local advice = ADStorageStats.flowAdvice({farmLevel = stat.farmLevel or stat.level, consumerLevel = consumerLevel,
perDay = stat.consumption, consumerCount = #ids, existingRunId = run ~= nil and run.id or nil})
return advice, {consumers = consumers, buildingIds = ids, run = run, name = name, stat = stat}
end
function SmartPickupFrame:attentionHint(advice, info)
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
return ""
end
function SmartPickupFrame:onAttentionClick(entry)
self:safe(function(frame)
local adEnv = frame.getAdEnv()
local advice, info = frame:attentionAdvice(entry)
if adEnv == nil or advice == "none" then return end
if advice == "open" then
frame.selectedRunId = info.run.id
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
frame:openEditPanel("flow", info.run.id)
return
end
if advice == "produce" then
local text = string.format(frame.i18n:getText("spu_attention_produce"), tostring(info.stat.title),
SmartPickupFrame.formatCompact(info.stat.farmLevel or 0))
frame.statusOverride, frame.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
return
end
local guide
if info.sell then
-- udfyldt "Nyt flow": ud af bygningerne med varen, varen til salg
local name = string.format(frame.i18n:getText("spu_attention_sellFlowName"), tostring(info.stat.title))
guide = ADFlowGuide.openNew(name, {})
guide.draft.kind = "outbound"
guide.draft.sourceBuildingIds = info.buildingIds
guide.draft.products = {{fillType = info.name, policy = "sell"}}
else
-- udfyldt "Nyt flow": ind til forbrugerne, kun denne vare; guiden åbnes på oversigten (Gem/Start eller [Ret])
local name = string.format(frame.i18n:getText("spu_attention_flowName"), tostring(info.stat.title), tostring(info.consumers[1].name))
guide = ADFlowGuide.openNew(name, {})
guide.draft.targetBuildingIds = info.buildingIds
guide.draft.ingredientMode = "manual"
guide.draft.ingredients = {info.name}
end
guide = ADGuide.new(ADFlowGuide.DEF, guide.draft, frame:flowGuideContext(adEnv, guide.draft), {step = "summary"})
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
frame:openGuide("flow", guide)
end)
end
@@ -76,6 +76,7 @@ end
-- Fanens varer med statistik; gårdens totaler/kapacitet/rater regnes over HELE gården.
function SmartPickupFrame:computeItemStats(adEnv, inventory, fullInventory)
self.lastFullInventory = fullInventory -- til "kræver handling" -> flow (hvem bruger varen)
local farmId = g_currentMission:getFarmId()
local totals = ADStorageStats.farmTotals(fullInventory)
local capacities = ADStorageStats.farmCapacities(fullInventory)
@@ -196,7 +197,13 @@ function SmartPickupFrame:alertText(entry)
end
function SmartPickupFrame:setAlertRow(row, entry)
row.c1:setText(self:alertText(entry))
row.entry = entry
local hint = ""
if SmartPickupFrame.attentionAdvice ~= nil then
local ok, advice, info = pcall(self.attentionAdvice, self, entry)
if ok then hint = self:attentionHint(advice, info) end
end
row.c1:setText(self:alertText(entry) .. hint)
if row.dot ~= nil and row.dot.setImageColor ~= nil then
row.dot:setImageColor(nil, unpackFn(SmartPickupFrame.LEVEL_COLORS[entry.level] or SmartPickupFrame.LEVEL_COLORS.green))
end
@@ -267,6 +274,8 @@ function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
local element = self:addRow(self.alertPrefab, {kind = "text"})
local row = {c1 = element:getDescendantByName("c1"), dot = element:getDescendantByName("dot")}
self:setAlertRow(row, entry)
-- klik: åbn/opret flow til varen (gui/SmartPickupAttentionFlow.lua)
self:makeRowClickable(element, function(frame) frame:onAttentionClick(row.entry) end)
table.insert(self.itemView.alertRows, row)
end
self:addTextRow(i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))) .. " " .. i18n:getText("spu_items_hint"))
+7
View File
@@ -549,6 +549,13 @@
<text name="spu_kpi_full" text="Næsten fuldt"/>
<text name="spu_attention" text="— Kræver handling —"/>
<text name="spu_attention_none" text="Intet kræver handling lige nu"/>
<text name="spu_attention_hintFlow" text=" → klik: opret flow"/>
<text name="spu_attention_hintOpen" text=" → klik: åbn flow '%s'"/>
<text name="spu_attention_hintProduce" text=" → producér/køb"/>
<text name="spu_attention_produce" text="Et flow hjælper ikke — gården har kun %2$s %1$s i alt. Producér eller køb mere."/>
<text name="spu_attention_flowName" text="%s til %s"/>
<text name="spu_attention_hintSellFlow" text=" → klik: opret salgsflow"/>
<text name="spu_attention_sellFlowName" text="Sælg %s"/>
<text name="spu_alert_empty" text="%s løber tør om %s — gården har %s, bruger %s/døgn"/>
<text name="spu_alert_fullIn" text="%s er %d %% fuld — fuld om %s"/>
<text name="spu_alert_full" text="%s er %d %% fuld"/>
+7
View File
@@ -549,6 +549,13 @@
<text name="spu_kpi_full" text="Almost full"/>
<text name="spu_attention" text="— Needs attention —"/>
<text name="spu_attention_none" text="Nothing needs attention right now"/>
<text name="spu_attention_hintFlow" text=" → click: create flow"/>
<text name="spu_attention_hintOpen" text=" → click: open flow '%s'"/>
<text name="spu_attention_hintProduce" text=" → produce/buy"/>
<text name="spu_attention_produce" text="A flow won't help — the farm only has %2$s %1$s in total. Produce or buy more."/>
<text name="spu_attention_flowName" text="%s to %s"/>
<text name="spu_attention_hintSellFlow" text=" → click: create sell flow"/>
<text name="spu_attention_sellFlowName" text="Sell %s"/>
<text name="spu_alert_empty" text="%s runs out in %s — farm has %s, uses %s/day"/>
<text name="spu_alert_fullIn" text="%s is %d %% full — full in %s"/>
<text name="spu_alert_full" text="%s is %d %% full"/>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.59.11.0</version>
<version>1.60.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>