diff --git a/FS25_ADSmartPickup/adPrices.lua b/FS25_ADSmartPickup/adPrices.lua new file mode 100644 index 0000000..2756e36 --- /dev/null +++ b/FS25_ADSmartPickup/adPrices.lua @@ -0,0 +1,93 @@ +-- AD Prices +-- Prisregel for udkørsel: hvor tæt er salgsstedets pris på årets toppris for varen? +-- Toppris = stationens grundpris lige nu × (årets højeste periodefaktor ÷ nuværende periodes +-- faktor) — dvs. det Priser-menuen ville vise som årets bedste måned for netop den station. +-- Effektiv pris (inkl. prisfald efter salg og stor efterspørgsel) ÷ toppris = pris-%; +-- stor efterspørgsel giver > 100 % og opfylder dermed altid reglen. +-- De rene funktioner øverst har ingen spil-API; getQuote er spil-adapteren. + +ADPrices = {} + +ADPrices.LOG_PREFIX = "ADSmartPickup: " + +function ADPrices.maxFactor(factors) + local best = nil + for _, factor in pairs(factors or {}) do + if type(factor) == "number" and (best == nil or factor > best) then + best = factor + end + end + return best or 1 +end + +function ADPrices.topPrice(baseNow, factorNow, maxFactor) + if baseNow == nil or baseNow <= 0 then + return 0 + end + if factorNow == nil or factorNow <= 0 or maxFactor == nil or maxFactor <= 0 then + return baseNow + end + return baseNow * maxFactor / factorNow +end + +function ADPrices.percentOfTop(price, top) + if top == nil or top <= 0 or price == nil then + return 0 + end + return math.floor(100 * price / top + 0.5) +end + +-- Rigtige salgssteder: SellingStation der sælger (fabrikkers aflæsningsstationer er +-- også SellingStation-objekter, men med isSellingPoint = false) og vises i Priser-menuen. +function ADPrices.isSellingStation(station) + return station ~= nil and station.isa ~= nil and SellingStation ~= nil and station:isa(SellingStation) == true + and station.isSellingPoint == true and not station.hideFromPricesMenu +end + +local function getCurrentPeriod() + local environment = g_currentMission ~= nil and g_currentMission.environment or nil + return environment ~= nil and environment.currentPeriod or nil +end + +local function getDifficultyMultiplier() + local economy = g_currentMission ~= nil and g_currentMission.economyManager or nil + if economy ~= nil and economy.getPriceMultiplier ~= nil then + local ok, value = pcall(economy.getPriceMultiplier, economy) + if ok and type(value) == "number" then + return value + end + end + return 1 +end + +-- {price, top, percent} for varen ved stationen; nil hvis stationen ikke kan prissætte den. +function ADPrices.getQuote(station, fillType) + if station == nil or station.getEffectiveFillTypePrice == nil then + return nil + end + local ok, effective = pcall(station.getEffectiveFillTypePrice, station, fillType) + if not ok or type(effective) ~= "number" then + return nil + end + local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil + local factors = fillTypeDesc ~= nil and fillTypeDesc.economy ~= nil and fillTypeDesc.economy.factors or nil + local period = getCurrentPeriod() + local factorNow = factors ~= nil and period ~= nil and tonumber(factors[period]) or 1 + local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil + if baseNow == nil or baseNow <= 0 then + baseNow = (fillTypeDesc ~= nil and tonumber(fillTypeDesc.pricePerLiter) or 0) * factorNow * getDifficultyMultiplier() + end + local top = ADPrices.topPrice(baseNow, factorNow, ADPrices.maxFactor(factors)) + return {price = effective, top = top, percent = ADPrices.percentOfTop(effective, top)} +end + +-- Højeste pris-%, ved lighed højeste pris. nil ved tom liste. +function ADPrices.bestQuote(quotes) + local best = nil + for _, quote in ipairs(quotes or {}) do + if best == nil or quote.percent > best.percent or (quote.percent == best.percent and quote.price > best.price) then + best = quote + end + end + return best +end diff --git a/tests/test_adPrices.lua b/tests/test_adPrices.lua new file mode 100644 index 0000000..ad2ec65 --- /dev/null +++ b/tests/test_adPrices.lua @@ -0,0 +1,65 @@ +-- Kør: luajit tests/test_adPrices.lua (fra repo-roden) +-- Prisregel: toppris fra årets prisforløb, pris-% pr. salgssted, bedste tilbud. + +dofile("FS25_ADSmartPickup/adPrices.lua") + +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 + +-- P1: højeste faktor +check("P1 maxFactor", ADPrices.maxFactor({EARLY_SPRING = 0.9, LATE_AUTUMN = 1.2, MID_SUMMER = 1.0}), 1.2) +check("P1 uden faktorer = 1", ADPrices.maxFactor(nil), 1) + +-- P2: toppris = grundpris nu × (maks ÷ nu) +check("P2 topPrice", ADPrices.topPrice(500, 1.0, 1.25), 625) +check("P2 factorNow 0 -> grundpris", ADPrices.topPrice(500, 0, 1.25), 500) +check("P2 grundpris 0 -> 0", ADPrices.topPrice(0, 1, 1.25), 0) + +-- P3: procent, afrundet; stor efterspørgsel > 100 +check("P3 75 %", ADPrices.percentOfTop(470, 625), 75) +check("P3 112 %", ADPrices.percentOfTop(700, 625), 112) +check("P3 top 0 -> 0", ADPrices.percentOfTop(1, 0), 0) + +-- P4: spil-adapter med stationens grundpris +SellingStation = {} +local SUGAR = 5 +g_fillTypeManager = {getFillTypeByIndex = function(_, index) + if index == SUGAR then return {economy = {factors = {A = 1.0, B = 1.25}}, pricePerLiter = 400} end +end} +g_currentMission = {environment = {currentPeriod = "A"}, economyManager = {getPriceMultiplier = function() return 1.0 end}} +local station = { + fillTypePrices = {[SUGAR] = 500}, + getEffectiveFillTypePrice = function(_, fillType) return 470 end, + isa = function(_, class) return class == SellingStation end, + isSellingPoint = true, +} +local quote = ADPrices.getQuote(station, SUGAR) +check("P4 pris", quote.price, 470) +check("P4 top", quote.top, 625) +check("P4 procent", quote.percent, 75) +check("P4 er salgssted", ADPrices.isSellingStation(station), true) +check("P4 skjult = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, hideFromPricesMenu = true}), false) +check("P4 fabrik-station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = false}), false) + +-- P5: uden stationens grundpris: pricePerLiter × faktor nu × sværhedsgrad +local bare = {getEffectiveFillTypePrice = function() return 470 end} +local quote2 = ADPrices.getQuote(bare, SUGAR) +check("P5 top fra pricePerLiter", quote2.top, 500) +check("P5 procent", quote2.percent, 94) +check("P5 fejlende station -> nil", ADPrices.getQuote({getEffectiveFillTypePrice = function() error("x") end}, SUGAR), nil) + +-- P6: bedste tilbud = højeste procent, dernæst pris +local best = ADPrices.bestQuote({{percent = 80, price = 1}, {percent = 92, price = 2}, {percent = 92, price = 3}}) +check("P6 højeste procent", best.percent, 92) +check("P6 ved lighed højeste pris", best.price, 3) +check("P6 tom -> nil", ADPrices.bestQuote({}), nil) + +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)