feat(gui): fanen Måned — marker i gang med fremdrift, næste trin og om høst-/såvinduet lukker ved månedsskiftet; dom 'du kan sove' + marker der kan sås nu (v1.35.0.0)

This commit is contained in:
2026-09-27 09:12:49 +02:00
parent 5274925f2a
commit 5df4fdc7c3
8 changed files with 394 additions and 4 deletions
+141
View File
@@ -0,0 +1,141 @@
-- AD Month View
-- Rene regler for fanen Måned: hvilke marker har arbejde i gang, hvor langt er de, hvad kommer bagefter,
-- og lukker vinduet (høst/såning) ved månedsskiftet? GUI'et oversætter nøglerne; spillet kaldes kun via
-- ctx-funktionerne (testbart uden FS).
ADMonthView = {}
ADMonthView.PERIODS = 12
-- Trin der kræver afgrøden eller såvinduet: sorteres før pleje/klargøring
ADMonthView.MAIN_STAGES = {harvesting = true, mowing = true, sowing = true}
-- Trin før såningen i klargøringskæden (lukker såvinduet, bliver marken ikke sået)
ADMonthView.BEFORE_SOWING = {mowing = true, tedding = true, raking = true, collecting = true, harvesting = true,
strawCollecting = true, baling = true, baleCollecting = true, mulching = true,
liming = true, plowing = true, cultivating = true, sowing = true}
function ADMonthView.nextPeriod(period)
if type(period) ~= "number" or period < 1 then return 1 end
return period % ADMonthView.PERIODS + 1
end
-- Visse FS25-sæsondata gemmer overgangene som lister af {startState, endState}; strukturen er ikke
-- dokumenteret, så alle tabeller i periodens data gennemsøges. Ukendt = nej (ingen falsk alarm).
local function findsTransition(data, fromState, toState, depth)
if type(data) ~= "table" or depth > 3 then return false end
if data.startState == fromState and data.endState == toState then return true end
for _, value in pairs(data) do
if type(value) == "table" and findsTransition(value, fromState, toState, depth + 1) then return true end
end
return false
end
-- Visner afgrøden (stadie state) ved overgangen til periode nextPeriod?
function ADMonthView.withersIn(fruit, nextPeriod, state)
if fruit == nil or type(state) ~= "number" or state <= 0 then return false end
local withered = fruit.witheredState
if type(withered) ~= "number" or withered <= 0 then return false end
local periods = fruit.growthDataSeasonal ~= nil and fruit.growthDataSeasonal.periods or nil
local data = periods ~= nil and periods[nextPeriod] or nil
return findsTransition(data, state, withered, 0)
end
-- Såvindue for en afgrøde: "closes" (kan sås nu, ikke næste måned), "open" (også næste måned), "closed".
function ADMonthView.sowWindow(plantableIn, fruitName, period)
if fruitName == nil or fruitName == "" or plantableIn == nil then return "open" end
local now = plantableIn(fruitName, period)
if not now then return "closed" end
return plantableIn(fruitName, ADMonthView.nextPeriod(period)) and "open" or "closes"
end
-- Er marken i gang (vises i tabellen)? Venter/færdig og passiv "klar" (manuel uden Start nu) er ikke.
function ADMonthView.isActive(field)
if field == nil then return false end
if field.stage == "idle" or field.stage == "done" then return false end
if field.stage == "ready" then return ADFieldModel.shouldStart(field) end
return true
end
-- Næste trin i kæden (uden at ændre marken). nil = intet (pleje går tilbage til venter, sidste trin -> færdig).
function ADMonthView.nextStage(field, info)
if field.stage == "ready" then
local stages = ADFieldModel.workStages(ADFieldModel.applyTarget(field.plan, info.kind, info.fruitName), info.kind, info.needs)
return stages[1]
end
if ADFieldModel.isCare(field.stage) then return nil end
local nextField = ADFieldModel.advance(field, info.needs)
if nextField.stage == "done" or nextField.stage == "idle" then return nil end
return nextField.stage
end
-- Vindue for en aktiv mark: "harvestCloses" | "lastSowMonth" | "ok".
-- ctx = {period, plantableIn(name, period), withers(info, nextPeriod)}
function ADMonthView.window(field, info, ctx)
local stage = field.stage
local cropStage = stage == "harvesting" or stage == "mowing" or stage == "ready"
if cropStage and info.kind ~= "bare" and ctx.withers ~= nil and ctx.withers(info, ADMonthView.nextPeriod(ctx.period)) then
return "harvestCloses"
end
local sowsLater = field.plan ~= nil and field.plan.sow and ADMonthView.BEFORE_SOWING[stage]
if sowsLater and ADMonthView.sowWindow(ctx.plantableIn, ADFieldModel.sowFruit(field), ctx.period) == "closes" then
return "lastSowMonth"
end
return "ok"
end
-- Fremdrift 0-1: Courseplays egen (gennemsnit af sættene) vinder; høst/slåning ellers ud fra hvor meget
-- af afgrøden der er væk; ellers ukendt (nil).
function ADMonthView.progress(field, info, cpProgress)
if type(cpProgress) == "number" then return math.max(0, math.min(1, cpProgress)) end
if (field.stage == "harvesting" or field.stage == "mowing") and type(info.readyRatio) == "number" then
return math.max(0, math.min(1, 1 - info.readyRatio))
end
return nil
end
local function priority(row)
if row.window ~= "ok" then return 0 end
if ADMonthView.MAIN_STAGES[row.stage] then return 1 end
return 2
end
-- Hele fanen: {rows = {...}, critical = n, upcoming = {...}}.
-- infos = ADFields.list(), fieldsById = ADFieldJobs.state.fields,
-- ctx = {period, plantableIn(name, period), withers(info, nextPeriod), cpProgress(fieldId) -> 0-1|nil}
function ADMonthView.build(infos, fieldsById, ctx)
local rows, upcoming, critical = {}, {}, 0
for _, info in ipairs(infos or {}) do
local field = fieldsById[info.id]
if field ~= nil and ADMonthView.isActive(field) then
local row = {id = info.id, stage = field.stage, crop = info.cropTitle, sowFruit = nil,
progress = ADMonthView.progress(field, info, ctx.cpProgress ~= nil and ctx.cpProgress(info.id) or nil),
nextStage = ADMonthView.nextStage(field, info), window = ADMonthView.window(field, info, ctx)}
if field.stage == "sowing" or (info.kind == "bare" and ADMonthView.BEFORE_SOWING[field.stage]) then
row.sowFruit = field.plan.sow and ADFieldModel.sowFruit(field) or nil
end
if row.window ~= "ok" then critical = critical + 1 end
table.insert(rows, row)
elseif field ~= nil and info.kind == "bare" and (field.stage == "idle" or field.stage == "done") then
local target = field.plan.targetFruit
if target ~= nil and target ~= "" and ctx.plantableIn ~= nil and ctx.plantableIn(target, ctx.period) then
table.insert(upcoming, {id = info.id, fruit = target,
closes = ADMonthView.sowWindow(ctx.plantableIn, target, ctx.period) == "closes"})
end
end
end
table.sort(rows, function(a, b)
local pa, pb = priority(a), priority(b)
if pa ~= pb then return pa < pb end
return a.id < b.id
end)
table.sort(upcoming, function(a, b)
if a.closes ~= b.closes then return a.closes end
return a.id < b.id
end)
return {rows = rows, critical = critical, upcoming = upcoming}
end
-- "50 %" / "–"
function ADMonthView.percentText(progress)
if progress == nil then return "–" end
return string.format("%d %%", math.floor(progress * 100 + 0.5))
end
+1
View File
@@ -25,6 +25,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
+25 -3
View File
@@ -11,10 +11,12 @@ SmartPickupFrame.STATUS_OVERRIDE_TICKS = 6
SmartPickupFrame.MAX_STATUS_REASONS = 3
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS = 1, 2, 3
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH = 1, 2, 3, 4
-- Måned-fanen genopbygges hvert N. sekund (ingen klikbare rækker, så fokus går ikke tabt)
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
SmartPickupFrame.TAB_KINDS = {[4] = "factory", [5] = "silo", [6] = "husbandry", [7] = "objectStorage"}
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
SmartPickupFrame.TAB_KINDS = {[5] = "factory", [6] = "silo", [7] = "husbandry", [8] = "objectStorage"}
function SmartPickupFrame.getTabKind(tab)
return SmartPickupFrame.TAB_KINDS[tab]
@@ -49,6 +51,7 @@ function SmartPickupFrame.new(i18n, getAdEnv)
-- Oversigt: Start/Stop alle i stedet for knapperne der virker på ét valgt flow.
self.startAllButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_startAll"), callback = function() self:onClickStartAll() end}
self.stopAllButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_stopAll"), callback = function() self:onClickStopAll() end}
self.monthButtons = {self.backButtonInfo}
self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo}
-- Marker: Start nu / Stop for den valgte mark
@@ -409,6 +412,17 @@ function SmartPickupFrame:rebuild()
self:restoreFocusIfLost()
return
end
if self.currentTab == SmartPickupFrame.TAB_MONTH then
if adEnv ~= nil then
self:fillMonthRows(adEnv)
end
self.monthRebuildTicks = SmartPickupFrame.MONTH_REBUILD_TICKS
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshButtons()
self:restoreFocusIfLost()
return
end
if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
if adEnv ~= nil then
self:fillOverviewRows(adEnv)
@@ -540,6 +554,8 @@ function SmartPickupFrame:refreshButtons()
wanted = self.overviewButtons
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
wanted = self:getFieldButtons()
elseif self.currentTab == SmartPickupFrame.TAB_MONTH then
wanted = self.monthButtons
end
if self.menuButtonInfo ~= wanted then
self.menuButtonInfo = wanted
@@ -560,6 +576,12 @@ function SmartPickupFrame:refreshLive()
self:setStatusText(self.i18n:getText("spu_status_noAd"))
return
end
if self.currentTab == SmartPickupFrame.TAB_MONTH then
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
self:refreshButtons()
return
end
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
if isOverview then
self:refreshOverview(adEnv)
@@ -0,0 +1,107 @@
-- Smart Pickup — fanen Måned
-- Hvad skal nås før månedsskiftet? Høst og såning har korte vinduer; fanen viser markerne med arbejde i
-- gang, fremdrift, næste trin og om vinduet lukker. Kun visning — reglerne ligger i ADMonthView.
-- Periodens navn: spillets egen tekst (fx "Sen sommer") hvis den findes, ellers "Periode 6".
-- Periode 1 = tidligt forår = marts (nordlig halvkugle) -> "(aug)".
function SmartPickupFrame:monthPeriodText(period)
local month = (period + 1) % 12 + 1
local names = {}
for name in string.gmatch(self.i18n:getText("spu_month_months"), "[^,]+") do table.insert(names, name) end
local suffix = names[month] ~= nil and (" (" .. names[month] .. ")") or ""
local ok, text = pcall(function() return g_i18n:formatPeriod(period, false) end)
if ok and type(text) == "string" and text ~= "" then return text .. suffix end
return string.format(self.i18n:getText("spu_month_period"), period) .. suffix
end
-- {period, day, days, hour, minute}; nil hvis miljøet ikke kan læses.
function SmartPickupFrame.monthClock()
local ok, clock = pcall(function()
local environment = g_currentMission.environment
local dayTime = environment.dayTime or 0
return {period = environment.currentPeriod, day = environment.currentDayInPeriod, days = environment.daysPerPeriod,
hour = math.floor(dayTime / 3600000), minute = math.floor(dayTime / 60000) % 60}
end)
if ok and type(clock) == "table" and type(clock.period) == "number" then return clock end
return nil
end
-- Courseplays fremdrift for markens sæt (gennemsnit), nil hvis ingen melder noget.
function SmartPickupFrame.monthCpProgress(fieldId)
local sum, count = 0, 0
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
local vehicle = assignment.vehicle
if not assignment.helper and vehicle ~= nil and vehicle.getCpFieldWorkProgress ~= nil then
local ok, progress = pcall(vehicle.getCpFieldWorkProgress, vehicle)
if ok and type(progress) == "number" then
sum, count = sum + progress, count + 1
end
end
end
return count > 0 and sum / count or nil
end
function SmartPickupFrame.monthContext(period)
return {
period = period,
plantableIn = function(name, forPeriod)
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByName(name) or nil
return fruit ~= nil and ADCpBridge.isPlantableIn(fruit, forPeriod)
end,
withers = function(info, nextPeriod)
local fruit = info.fruitIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex) or nil
local ok, result = pcall(ADMonthView.withersIn, fruit, nextPeriod, info.growthState)
return ok and result == true
end,
cpProgress = SmartPickupFrame.monthCpProgress,
}
end
function SmartPickupFrame:monthStageText(stage)
if stage == nil then return "–" end
return self.i18n:getText("spu_field_stage_" .. tostring(stage))
end
function SmartPickupFrame:monthCells(row)
local crop = row.sowFruit ~= nil and self:fruitTitle(row.sowFruit) or (row.crop or "")
return {"F" .. tostring(row.id), self:monthStageText(row.stage), crop, ADMonthView.percentText(row.progress),
self:monthStageText(row.nextStage), self.i18n:getText("spu_month_window_" .. row.window)}
end
function SmartPickupFrame:fillMonthRows(adEnv)
local i18n = self.i18n
local clock = SmartPickupFrame.monthClock()
if clock == nil then
self:addTextRow(i18n:getText("spu_month_noClock"))
return
end
self:addTextRow(string.format(i18n:getText("spu_month_header"), self:monthPeriodText(clock.period),
clock.day or 1, clock.days or 1, clock.hour, clock.minute))
local infos = ADFields.list()
if #infos == 0 then
self:addTextRow(i18n:getText("spu_field_scanning"))
return
end
local view = ADMonthView.build(infos, ADFieldJobs.state.fields, SmartPickupFrame.monthContext(clock.period))
if view.critical > 0 then
self:addTextRow(string.format(i18n:getText("spu_month_verdictUrgent"), view.critical))
else
self:addTextRow(i18n:getText("spu_month_verdictCalm"))
end
if #view.rows == 0 then
self:addTextRow(i18n:getText("spu_month_noWork"))
else
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_month_col_work"), i18n:getText("spu_col_crop"),
i18n:getText("spu_month_col_progress"), i18n:getText("spu_month_col_next"), i18n:getText("spu_month_col_window")})
for _, row in ipairs(view.rows) do
self:addCellRow(self.fieldPrefab, self:monthCells(row))
end
end
if #view.upcoming > 0 then
self:addTextRow(i18n:getText("spu_month_upcoming"))
for _, entry in ipairs(view.upcoming) do
local key = entry.closes and "spu_month_upcomingLast" or "spu_month_upcomingOpen"
self:addTextRow(string.format(i18n:getText(key), entry.id, self:fruitTitle(entry.fruit)))
end
end
end
+18
View File
@@ -40,6 +40,24 @@
<text name="spu_status_stoppedAll" text="Stoppet %d traktorer"/>
<text name="spu_overview_hint" text="Klik på et flow for at redigere det"/>
<text name="spu_tab_fields" text="Marker"/>
<text name="spu_tab_month" text="Måned"/>
<text name="spu_month_months" text="jan,feb,mar,apr,maj,jun,jul,aug,sep,okt,nov,dec"/>
<text name="spu_month_period" text="Periode %d"/>
<text name="spu_month_header" text="%s · dag %d af %d · kl. %02d:%02d"/>
<text name="spu_month_noClock" text="Kan ikke læse spillets kalender"/>
<text name="spu_month_verdictCalm" text="Intet haster — du kan roligt sove til næste måned"/>
<text name="spu_month_verdictUrgent" text="OBS: %d marker skal nå det før månedsskiftet"/>
<text name="spu_month_noWork" text="Ingen marker har arbejde i gang"/>
<text name="spu_month_col_work" text="Arbejde"/>
<text name="spu_month_col_progress" text="Færdig"/>
<text name="spu_month_col_next" text="Næste"/>
<text name="spu_month_col_window" text="Vindue"/>
<text name="spu_month_window_ok" text="ok"/>
<text name="spu_month_window_harvestCloses" text="OBS: visner snart"/>
<text name="spu_month_window_lastSowMonth" text="OBS: sidste såmåned"/>
<text name="spu_month_upcoming" text="Kan sås nu, ikke gået i gang:"/>
<text name="spu_month_upcomingOpen" text="F%d: %s — kan også sås næste måned"/>
<text name="spu_month_upcomingLast" text="F%d: %s — OBS: sidste såmåned"/>
<text name="spu_field_button_start" text="Start nu"/>
<text name="spu_field_button_stop" text="Stop mark"/>
<text name="spu_field_noMarker" text="! mangler markør F%d"/>
+18
View File
@@ -40,6 +40,24 @@
<text name="spu_status_stoppedAll" text="Stopped %d tractors"/>
<text name="spu_overview_hint" text="Click a flow to edit it"/>
<text name="spu_tab_fields" text="Fields"/>
<text name="spu_tab_month" text="Month"/>
<text name="spu_month_months" text="Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec"/>
<text name="spu_month_period" text="Period %d"/>
<text name="spu_month_header" text="%s · day %d of %d · %02d:%02d"/>
<text name="spu_month_noClock" text="Cannot read the game calendar"/>
<text name="spu_month_verdictCalm" text="Nothing urgent — safe to sleep to next month"/>
<text name="spu_month_verdictUrgent" text="NOTE: %d fields must finish before the month ends"/>
<text name="spu_month_noWork" text="No fields have work in progress"/>
<text name="spu_month_col_work" text="Work"/>
<text name="spu_month_col_progress" text="Done"/>
<text name="spu_month_col_next" text="Next"/>
<text name="spu_month_col_window" text="Window"/>
<text name="spu_month_window_ok" text="ok"/>
<text name="spu_month_window_harvestCloses" text="NOTE: withers soon"/>
<text name="spu_month_window_lastSowMonth" text="NOTE: last sow month"/>
<text name="spu_month_upcoming" text="Can be sown now, not started:"/>
<text name="spu_month_upcomingOpen" text="F%d: %s — can also be sown next month"/>
<text name="spu_month_upcomingLast" text="F%d: %s — NOTE: last sowing month"/>
<text name="spu_field_button_start" text="Start now"/>
<text name="spu_field_button_stop" text="Stop field"/>
<text name="spu_field_noMarker" text="! missing marker F%d"/>
+2 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.34.2.0</version>
<version>1.35.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
@@ -39,6 +39,7 @@
<sourceFile filename="adFieldStorage.lua"/>
<sourceFile filename="adFieldWork.lua"/>
<sourceFile filename="adFieldJobs.lua"/>
<sourceFile filename="adMonthView.lua"/>
<sourceFile filename="adProfiler.lua"/>
<sourceFile filename="adSmartPickup.lua"/>
</extraSourceFiles>
+82
View File
@@ -0,0 +1,82 @@
-- Kør: luajit tests/test_adMonthView.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adMonthView.lua")
local V, M = ADMonthView, ADFieldModel
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- MV1: perioder
check("MV1 næste periode", V.nextPeriod(5), 6)
check("MV1 vinter -> forår", V.nextPeriod(12), 1)
-- MV2: såvindue (hvede sås i periode 6-7)
local function plantableIn(name, period) return name == "WHEAT" and (period == 6 or period == 7) end
check("MV2 lukker", V.sowWindow(plantableIn, "WHEAT", 7), "closes")
check("MV2 åbent", V.sowWindow(plantableIn, "WHEAT", 6), "open")
check("MV2 lukket", V.sowWindow(plantableIn, "WHEAT", 8), "closed")
-- MV3: visning (Dakota-kartofler: harvestReady 6 -> dead 7 i EARLY_AUTUMN = periode 7)
local potato = {witheredState = 7, growthDataSeasonal = {periods = {[7] = {updates = {{startState = 6, endState = 7}}}}}}
check("MV3 visner næste måned", V.withersIn(potato, 7, 6), true)
check("MV3 ikke måneden før", V.withersIn(potato, 6, 6), false)
check("MV3 ukendte data", V.withersIn({witheredState = 7}, 7, 6), false)
-- MV4: fremdrift
local harvest = M.with(M.newField(15), {stage = "harvesting", kind = "harvest"})
check("MV4 CP vinder", V.progress(harvest, {readyRatio = 0.9}, 0.5), 0.5)
check("MV4 høst ud fra afgrøde", V.progress(harvest, {readyRatio = 0.25}, nil), 0.75)
check("MV4 kultivering ukendt", V.progress(M.with(harvest, {stage = "cultivating"}), {readyRatio = 0}, nil), nil)
check("MV4 tekst", V.percentText(0.504), "50 %")
check("MV4 tekst ukendt", V.percentText(nil), "–")
-- MV5: hel fane
local fields = {
[15] = M.with(harvest, {plan = {sow = true, sowFruit = "WHEAT", soil = "cultivate", straw = "chop"}}),
[16] = M.with(M.newField(16), {stage = "harvesting", kind = "harvest"}),
[18] = M.with(M.newField(18), {stage = "sowing", kind = "harvest", plan = {sow = true, sowFruit = "WHEAT"}}),
[20] = M.with(M.newField(20), {stage = "fertilizing"}),
[21] = M.newField(21),
[22] = M.with(M.newField(22), {plan = {targetFruit = "WHEAT"}}),
[23] = M.with(M.newField(23), {stage = "ready"}),
}
local infos = {
{id = 15, kind = "harvest", readyRatio = 0.5, cropTitle = "Hvede", needs = {}},
{id = 16, kind = "harvest", readyRatio = 1, cropTitle = "Kartofler", growthState = 6},
{id = 18, kind = "bare", readyRatio = 0},
{id = 20, kind = "harvest", readyRatio = 0},
{id = 21, kind = "bare"},
{id = 22, kind = "bare"},
{id = 23, kind = "harvest", readyRatio = 1},
}
local ctx = {period = 7, plantableIn = plantableIn,
withers = function(info, nextPeriod) return info.id == 16 and nextPeriod == 8 end,
cpProgress = function(id) return id == 18 and 0.1 or nil end}
local view = V.build(infos, fields, ctx)
check("MV5 aktive marker (21 venter, 22 kommer, 23 passiv klar)", #view.rows, 4)
check("MV5 kritiske", view.critical, 3)
check("MV5 høst før sidste såmåned først (id)", view.rows[1].id, 15)
check("MV5 høst -> såning senere lukker", view.rows[1].window, "lastSowMonth")
check("MV5 høst næste trin", view.rows[1].nextStage, "cultivating")
check("MV5 høst der visner", view.rows[2].id, 16)
check("MV5 vindue høst", view.rows[2].window, "harvestCloses")
check("MV5 sidste såmåned", view.rows[3].id, 18)
check("MV5 vindue såning", view.rows[3].window, "lastSowMonth")
check("MV5 såafgrøde", view.rows[3].sowFruit, "WHEAT")
check("MV5 CP-fremdrift", view.rows[3].progress, 0.1)
check("MV5 pleje sidst", view.rows[4].id, 20)
check("MV5 pleje intet næste", view.rows[4].nextStage, nil)
check("MV5 kommer: F22 hvede", view.upcoming[1] and view.upcoming[1].id, 22)
check("MV5 kommer: lukker", view.upcoming[1] and view.upcoming[1].closes, true)
-- MV6: klargøring før såning med lukkende vindue er kritisk
local cultivating = M.with(M.newField(30), {stage = "cultivating", plan = {sow = true, sowFruit = "WHEAT"}})
check("MV6 kultivering før sidste såmåned", V.window(cultivating, {kind = "bare"}, ctx), "lastSowMonth")
check("MV6 aktiv klar (auto)", V.isActive(M.with(M.newField(31), {stage = "ready", plan = {mode = "auto"}})), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)