-- AD Supply Planner -- Rene funktioner (ingen spil-API) til behovsstyret forsyning: hvilken ingrediens -- har aflæsningsmålet mest brug for, og hvilken ingrediensliste gælder for køretøjet. ADSupplyPlanner = {} -- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af). ADSupplyPlanner.MIN_FREE_LITERS = 1000 -- Varer hvis kilder LØBER FULDE (dyr/produktion fylder tanken løbende og går i stå -- når den er fuld): her hentes den fyldeste kilde først. Alt andet er lagervarer, -- hvor den mindst fyldte silo tømmes først så rester ryddes og siloer frigives. ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES = { MILK = true, GOATMILK = true, BUFFALOMILK = true, SHEEPMILK = true, LIQUIDMANURE = true, DIGESTATE = true, } -- fillTypeNames: navnene på de varer der hentes på turen. Kun når ALLE er -- "løber fuld"-varer vendes strategien; blandet eller ukendt valg = mindst først. function ADSupplyPlanner.prefersFullestSource(fillTypeNames) if #fillTypeNames == 0 then return false end for _, name in ipairs(fillTypeNames) do if not ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES[name] then return false end end return true end -- Er en kilde med dette niveau bedre end den hidtil bedste (nil = ingen endnu)? function ADSupplyPlanner.isBetterSource(level, bestLevel, prefersFullest) if bestLevel == nil then return true end if prefersFullest then return level > bestLevel end return level < bestLevel end function ADSupplyPlanner.copyList(list) local copy = {} for index, value in ipairs(list) do copy[index] = value end return copy end -- Ønsker en produktion input? Tændt, eller slukket af kortet fordi den MANGLEDE -- input (Dakotas ExtendedProductionPoint slår den fra automatisk, men lader -- status stå på MISSING_INPUTS). Manuelt slukket = status INACTIVE -> nej. function ADSupplyPlanner.isProductionWanted(isEnabled, status, missingInputsStatus) if isEnabled then return true end return status ~= nil and status == missingInputsStatus end -- productions: produktionspunktets liste ({id, cyclesPerMinute, inputs = {{type, amount}}}). -- isWanted(production) afgør om den tæller; canCarry[fillType] = det vognen kan bære. -- Returnerer {[fillType] = liter pr. minut} summeret over ønskede produktioner. function ADSupplyPlanner.collectProductionDemand(productions, isWanted, canCarry) local demand = {} for _, production in ipairs(productions) do if isWanted(production) then local cyclesPerMinute = production.cyclesPerMinute or 0 for _, input in ipairs(production.inputs or {}) do if canCarry[input.type] then demand[input.type] = (demand[input.type] or 0) + (input.amount or 0) * cyclesPerMinute end end end end return demand end -- Er varen et INPUT til fabrikken (og ikke også et output)? Så må den aldrig -- hentes derfra — det ville tømme fabrikken for det, den skal bruge. function ADSupplyPlanner.isProductionInput(productions, fillType) local isInput, isOutput = false, false for _, production in ipairs(productions) do for _, input in ipairs(production.inputs or {}) do if input.type == fillType then isInput = true end end for _, output in ipairs(production.outputs or {}) do if output.type == fillType then isOutput = true end end end return isInput and not isOutput end -- Minutter før målets lager af varen er tomt ved nuværende forbrug; nil uden forbrug. function ADSupplyPlanner.minutesToEmpty(level, litersPerMinute) if litersPerMinute == nil or litersPerMinute <= 0 then return nil end return level / litersPerMinute end -- Hvor meget trænger målet til denne ingrediens? Højere = hentes først. -- Alle tal er liter i aflæsningsmålets lager. Standard: relativ tomhed (0..1), -- så en lille næsten-tom tank vinder over en stor halvfuld. Kendes forbruget -- (fabriksstyret), tæller i stedet tid til tom: score i ]1, 2], så varer med -- forbrug altid går forud for varer uden, og kortest tid vinder. function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace, minutesToEmpty) if minutesToEmpty ~= nil then return 1 + 1 / (1 + minutesToEmpty) end if capacity <= 0 then return 0 end return freeSpace / capacity end -- needs: {{fillType, level, capacity[, litersPerMinute]}, ...}. Returnerer NY liste, -- mest trængende først; ingredienser uden plads i målet er sorteret fra. function ADSupplyPlanner.rankNeeds(needs) local ranked = {} for _, need in ipairs(needs) do local freeSpace = need.capacity - need.level if freeSpace >= ADSupplyPlanner.MIN_FREE_LITERS then local minutesToEmpty = ADSupplyPlanner.minutesToEmpty(need.level, need.litersPerMinute) table.insert(ranked, { fillType = need.fillType, level = need.level, capacity = need.capacity, freeSpace = freeSpace, litersPerMinute = need.litersPerMinute, minutesToEmpty = minutesToEmpty, score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty), }) end end table.sort(ranked, function(a, b) if a.score ~= b.score then return a.score > b.score end return a.fillType < b.fillType end) return ranked end -- state: nil eller {ingredients, narrowedTo} fra forrige tur. selected: AD's aktuelle varevalg. -- >1 valgt = (ny) ingrediensliste. Præcis den vare vi selv indsnævrede til = gemt liste. -- Alt andet = brugeren har valgt noget andet -> nil (behovsstyring fra). function ADSupplyPlanner.resolveIngredients(state, selected) if type(selected) ~= "table" then return nil end if #selected > 1 then return ADSupplyPlanner.copyList(selected) end if state ~= nil and #selected == 1 and selected[1] == state.narrowedTo then return ADSupplyPlanner.copyList(state.ingredients) end return nil end