-- AD Field Model -- Markarbejdets regler som ren data (ingen spil-API): en marks plan og trin, hvornår den er klar, -- hvilket trin der følger, hvor mange sæt et trin må have, og bredde-reglerne. Alle funktioner -- returnerer nye tabeller; job-løkken (adFieldJobs) og GUI'et kalder kun herind. ADFieldModel = {} ADFieldModel.READY_RATIO = 0.9 ADFieldModel.CUT_RATIO = 0.5 ADFieldModel.CP_MAX_WIDTH = 50 ADFieldModel.WIDTH_TOLERANCE = 0.05 ADFieldModel.RETRY_MS = 300000 ADFieldModel.MAX_COLLECTORS = 2 ADFieldModel.ROLES = {mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector"} local KNOWN_STAGES = {idle = true, ready = true, mowing = true, tedding = true, raking = true, collecting = true, done = true} local function copyList(list) local copy = {} for index, value in ipairs(list or {}) do copy[index] = value end return copy end local function copyField(field) local copy = {} for key, value in pairs(field) do copy[key] = value end copy.plan = { mode = field.plan.mode, product = field.plan.product, rake = field.plan.rake, storeBuildingIds = copyList(field.plan.storeBuildingIds), } return copy end function ADFieldModel.newField(id) return { id = id, stage = "idle", mowWidth = nil, retryAt = 0, reason = nil, startRequested = false, plan = {mode = "manual", product = "silage", rake = false, storeBuildingIds = {}}, } end function ADFieldModel.workStages(plan) if plan.product == "hay" then if plan.rake then return {"mowing", "tedding", "raking", "collecting"} end return {"mowing", "tedding", "collecting"} end return {"mowing", "collecting"} end function ADFieldModel.roleFor(stage) return ADFieldModel.ROLES[stage] end function ADFieldModel.isWorking(field) return ADFieldModel.ROLES[field.stage] ~= nil end function ADFieldModel.maxRigs(stage) return stage == "collecting" and ADFieldModel.MAX_COLLECTORS or 1 end -- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis marken blev slået uden os. -- Arbejdende marker styres af job-løkken, ikke af vækst. function ADFieldModel.onReadiness(field, ratio) local copy = copyField(field) if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then copy.stage = "ready" elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO then copy.stage = "idle" copy.startRequested = false end return copy end function ADFieldModel.shouldStart(field) return field.stage == "ready" and (field.plan.mode == "auto" or field.startRequested == true) end -- Start nu fra menuen på en mark der ikke er nået 90 %: den behandles som klar. function ADFieldModel.forceReady(field) if field.stage == "idle" or field.stage == "done" then return ADFieldModel.with(field, {stage = "ready"}) end return field end function ADFieldModel.start(field) local copy = copyField(field) copy.stage = ADFieldModel.workStages(field.plan)[1] copy.startRequested = false copy.retryAt = 0 copy.reason = nil copy.mowWidth = nil copy.mowVehicleId = nil return copy end function ADFieldModel.advance(field) local copy = copyField(field) local stages = ADFieldModel.workStages(field.plan) copy.stage = "done" for index, stage in ipairs(stages) do if stage == field.stage and stages[index + 1] ~= nil then copy.stage = stages[index + 1] end end copy.retryAt = 0 copy.reason = nil return copy end -- Stop fra menuen: arbejdet afbrydes; marken står klar igen (manuel start kræves). function ADFieldModel.stop(field) local copy = copyField(field) if ADFieldModel.isWorking(field) then copy.stage = "ready" end copy.startRequested = false copy.reason = nil return copy end -- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst slåbredden. function ADFieldModel.checkWidth(role, width, mowWidth) if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH} end if role ~= "mower" and mowWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < mowWidth then return false, "tooNarrow", {width, mowWidth} end return true end function ADFieldModel.wantsRig(field, assignedCount, now) local role = ADFieldModel.roleFor(field.stage) if role == nil or (field.retryAt or 0) > now or assignedCount >= ADFieldModel.maxRigs(field.stage) then return nil end return role end function ADFieldModel.with(field, changes) local copy = copyField(field) for key, value in pairs(changes) do if key == "plan" then for planKey, planValue in pairs(value) do copy.plan[planKey] = planValue end else copy[key] = value end end return copy end function ADFieldModel.toRecord(field) return { id = field.id, stage = field.stage, mode = field.plan.mode, product = field.plan.product, rake = field.plan.rake and 1 or 0, store = table.concat(field.plan.storeBuildingIds, " "), mowWidth = field.mowWidth or 0, mowVehicle = field.mowVehicleId or "", } end function ADFieldModel.fromRecord(record) local field = ADFieldModel.newField(record.id) field.stage = KNOWN_STAGES[record.stage] and record.stage or "idle" field.plan.mode = record.mode == "auto" and "auto" or "manual" field.plan.product = record.product == "hay" and "hay" or "silage" field.plan.rake = record.rake == 1 or record.rake == true for id in string.gmatch(record.store or "", "%S+") do table.insert(field.plan.storeBuildingIds, id) end field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil field.mowVehicleId = record.mowVehicle ~= nil and record.mowVehicle ~= "" and record.mowVehicle or nil return field end