feat(kobling): rummelig koblingsrækkevidde (3 m) og bakning de sidste 3-8 m hen til vognens trækøje ved Connect-markører; lagervisning: bredere mængde-/rate-kolonner så teksten ikke klippes (v1.53.0.0)

This commit is contained in:
2026-09-29 13:44:21 +02:00
parent a305de25a5
commit 417a44841d
7 changed files with 262 additions and 15 deletions
+24 -4
View File
@@ -27,6 +27,8 @@ ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling efter ankomst
ADHitch.DETACH_TIMEOUT_MS = 20000 -- afkobling: redskabet skal måske foldes ud først (plov: "unfold the tool") ADHitch.DETACH_TIMEOUT_MS = 20000 -- afkobling: redskabet skal måske foldes ud først (plov: "unfold the tool")
ADHitch.TICK_MS = 250 ADHitch.TICK_MS = 250
ADHitch.ROUTE_WAIT_MS = 10000 -- pause ved almindelige markører midt i en tur ADHitch.ROUTE_WAIT_MS = 10000 -- pause ved almindelige markører midt i en tur
ADHitch.ROOMY_ATTACH_DISTANCE = 3 -- m: koblingsrækkevidde når spillets egen ikke rækker
ADHitch.CREPT_EXTRA_DISTANCE = 8 -- m: ekstra ankomstafstand når traktoren har bakket hen til vognen
-- ---- ren logik ---- -- ---- ren logik ----
@@ -91,9 +93,13 @@ function ADHitch.findMarker(markers, name)
return nil return nil
end end
-- state = {adActive, distance, speed} -- state = {adActive, distance, speed, arriveDistance?} (arriveDistance: udvidet, når traktoren har bakket hen til vognen)
local function arriveDistanceOf(state)
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
end
function ADHitch.hasArrived(state) function ADHitch.hasArrived(state)
return not state.adActive and state.distance <= ADHitch.ARRIVE_DISTANCE and state.speed <= ADHitch.STILL_SPEED return not state.adActive and state.distance <= arriveDistanceOf(state) and state.speed <= ADHitch.STILL_SPEED
end end
function ADHitch.newJob(markerId, markerName, action) function ADHitch.newJob(markerId, markerName, action)
@@ -110,7 +116,7 @@ function ADHitch.step(job, state, dt, succeeded)
job.seenActive = true job.seenActive = true
elseif ADHitch.hasArrived(state) then elseif ADHitch.hasArrived(state) then
job.state = "acting" job.state = "acting"
elseif state.distance > ADHitch.ARRIVE_DISTANCE and (job.seenActive or job.elapsed >= ADHitch.START_GRACE_MS) then elseif state.distance > arriveDistanceOf(state) and (job.seenActive or job.elapsed >= ADHitch.START_GRACE_MS) then
job.state, job.reason = "failed", "stoppedAway" job.state, job.reason = "failed", "stoppedAway"
end end
elseif job.state == "acting" then elseif job.state == "acting" then
@@ -181,9 +187,14 @@ function ADHitch.tryAttach(vehicle)
if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then
return false, "noJoints" return false, "noJoints"
end end
-- spillets egen rækkevidde først, så en rummeligere (spillet retter selv vognen ind ved koblingen)
AttacherJoints.updateVehiclesInAttachRange(vehicle, AttacherJoints.MAX_ATTACH_DISTANCE_SQ, AttacherJoints.updateVehiclesInAttachRange(vehicle, AttacherJoints.MAX_ATTACH_DISTANCE_SQ,
AttacherJoints.MAX_ATTACH_ANGLE, true) AttacherJoints.MAX_ATTACH_ANGLE, true)
local info = spec.attachableInfo local info = spec.attachableInfo
if (info == nil or info.attachable == nil) and ADHitch.ROOMY_ATTACH_DISTANCE ^ 2 > AttacherJoints.MAX_ATTACH_DISTANCE_SQ then
AttacherJoints.updateVehiclesInAttachRange(vehicle, ADHitch.ROOMY_ATTACH_DISTANCE ^ 2, AttacherJoints.MAX_ATTACH_ANGLE, true)
info = spec.attachableInfo
end
if info == nil or info.attachable == nil then return false, "noAttachable" end if info == nil or info.attachable == nil then return false, "noAttachable" end
local attacher = info.attacherVehicle or vehicle local attacher = info.attacherVehicle or vehicle
local farmId = vehicle.getActiveFarm ~= nil and vehicle:getActiveFarm() or vehicle:getOwnerFarmId() local farmId = vehicle.getActiveFarm ~= nil and vehicle:getActiveFarm() or vehicle:getOwnerFarmId()
@@ -272,6 +283,13 @@ local function start(adEnv, vehicle, markerId, action, onDone)
return job return job
end end
-- Kørende kobl-på-job for traktoren (til bakningen de sidste meter), ellers nil.
function ADHitch.pendingAttach(vehicle)
local job = jobs[vehicle]
if job ~= nil and job.action == "attach" and job.state == "driving" then return job end
return nil
end
-- Afbryd et hente-/koble-job (Stop alt, høsten slut, spilleren overtager): intet kobles på bagefter, og -- Afbryd et hente-/koble-job (Stop alt, høsten slut, spilleren overtager): intet kobles på bagefter, og
-- onDone kaldes ikke. -> true hvis der var et job. -- onDone kaldes ikke. -> true hvis der var et job.
function ADHitch.cancel(vehicle) function ADHitch.cancel(vehicle)
@@ -410,7 +428,9 @@ function ADHitch.tick(adEnv, dt)
succeeded, detail = ACTIONS[job.action](vehicle) succeeded, detail = ACTIONS[job.action](vehicle)
if not succeeded then job.lastReason = detail end if not succeeded then job.lastReason = detail end
end end
ADHitch.step(job, vehicleState(adEnv, vehicle, job.markerId), elapsed, succeeded) local state = vehicleState(adEnv, vehicle, job.markerId)
if job.crept then state.arriveDistance = ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
ADHitch.step(job, state, elapsed, succeeded)
if job.state == "done" then if job.state == "done" then
finish(vehicle, job, true, detail) finish(vehicle, job, true, detail)
elseif job.state == "failed" then elseif job.state == "failed" then
+193
View File
@@ -0,0 +1,193 @@
-- AD Hitch Creep
-- Bak de sidste meter hen til vognen: når AD "Kør til" når en Connect-markør, og vognens trækøje står 3-8 m
-- lige bag traktorens krog, lægges en lille bakke-opgave i AD's kø før stop. Traktoren bakker i krybefart og
-- standser, så snart spillet ser vognen inden for koblingsrækkevidde; ADHitch kobler derefter på som før.
-- AD's egen bak-kollisionssensor bruges ikke: den ser vognen selv som forhindring.
ADHitchCreep = {}
ADHitchCreep.LOG_PREFIX = "ADSmartPickup: kobling: "
ADHitchCreep.ROOMY_ATTACH_DISTANCE = 3 -- m: herinden for kobler ADHitch direkte (samme som ADHitch.ROOMY_ATTACH_DISTANCE)
ADHitchCreep.CREEP_MAX_DISTANCE = 8 -- m: længere væk end det bakkes ikke — markøren ligger forkert
ADHitchCreep.CREEP_MAX_LATERAL = 1.5 -- m til siden: mere end det rammer en lige bakning ikke trækøjet
ADHitchCreep.CREEP_TIMEOUT_MS = 25000
ADHitchCreep.CREEP_SPEED = 2 -- km/t
ADHitchCreep.AWAY_TOLERANCE = 1 -- m: længere væk end ved start = den bakker forkert
ADHitchCreep.RANGE_CHECK_MS = 250
-- ---- ren logik ----
-- offset = {x, z}: vognens trækøje i traktorens rum set fra krogen (z < 0 = bagved) -> "attach" | "creep" | "none"
function ADHitchCreep.plan(offset)
if offset == nil then return "none" end
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
if distance <= ADHitchCreep.ROOMY_ATTACH_DISTANCE then return "attach" end
if offset.z >= 0 or distance > ADHitchCreep.CREEP_MAX_DISTANCE or math.abs(offset.x) > ADHitchCreep.CREEP_MAX_LATERAL then
return "none"
end
return "creep"
end
-- s = {inRange, distance, startDistance, elapsedMs, passed} -> "done" | "drive" | "giveUp"
function ADHitchCreep.step(s)
if s.inRange then return "done" end
if (s.elapsedMs or 0) >= ADHitchCreep.CREEP_TIMEOUT_MS then return "giveUp" end
if s.passed then return "giveUp" end
if s.distance > s.startDistance + ADHitchCreep.AWAY_TOLERANCE then return "giveUp" end
return "drive"
end
-- ---- spil ----
local function worldPos(node)
local x, y, z = getWorldTranslation(node)
return {x = x, y = y, z = z}
end
local function vehicleName(vehicle)
return vehicle ~= nil and vehicle.getName ~= nil and vehicle:getName() or "?"
end
-- Traktorens ledige bagerste krog-noder.
local function freeRearJoints(vehicle)
local spec = vehicle.spec_attacherJoints
if spec == nil or spec.attacherJoints == nil or vehicle.rootNode == nil then return {} end
local used = {}
for _, implement in ipairs(spec.attachedImplements or {}) do
if implement.jointDescIndex ~= nil then used[implement.jointDescIndex] = true end
end
local joints = {}
for index, joint in ipairs(spec.attacherJoints) do
if not used[index] and joint.jointTransform ~= nil then
local p = worldPos(joint.jointTransform)
local _, _, lz = worldToLocal(vehicle.rootNode, p.x, p.y, p.z)
if lz < 0 then table.insert(joints, joint.jointTransform) end
end
end
return joints
end
-- Vognens trækøje i traktorens rum set fra krogen.
local function offsetOf(vehicle, jointNode, targetNode)
local j, t = worldPos(jointNode), worldPos(targetNode)
local x, _, z = worldDirectionToLocal(vehicle.rootNode, t.x - j.x, 0, t.z - j.z)
return {x = x, z = z}
end
-- Nærmeste frakoblede redskab (samme gård) med et trækøje bag en ledig krog. -> {wagon, joint, target, offset, plan} | nil
function ADHitchCreep.findTarget(vehicle)
local joints = freeRearJoints(vehicle)
if #joints == 0 or g_currentMission == nil or g_currentMission.vehicleSystem == nil then return nil end
local farmId = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() or nil
local best, bestDistance = nil, math.huge
for _, object in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
local attachable = object.spec_attachable
if object ~= vehicle and attachable ~= nil and object.spec_motorized == nil and attachable.inputAttacherJoints ~= nil
and (object.getAttacherVehicle == nil or object:getAttacherVehicle() == nil)
and (farmId == nil or object.getOwnerFarmId == nil or object:getOwnerFarmId() == farmId) then
for _, input in ipairs(attachable.inputAttacherJoints) do
if input.node ~= nil then
for _, joint in ipairs(joints) do
local offset = offsetOf(vehicle, joint, input.node)
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
if distance < bestDistance and distance <= ADHitchCreep.CREEP_MAX_DISTANCE + 2 then
bestDistance = distance
best = {wagon = object, joint = joint, target = input.node, offset = offset}
end
end
end
end
end
end
if best ~= nil then best.plan = ADHitchCreep.plan(best.offset) end
return best
end
local function isInRange(vehicle, wagon)
local spec = vehicle.spec_attacherJoints
if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then return false end
AttacherJoints.updateVehiclesInAttachRange(vehicle, ADHitchCreep.ROOMY_ATTACH_DISTANCE ^ 2, AttacherJoints.MAX_ATTACH_ANGLE, true)
local info = spec.attachableInfo
return info ~= nil and info.attachable == wagon
end
-- AD-opgave (samme metoder som AD's AbstractTask).
local CreepTask = {}
CreepTask.__index = CreepTask
function CreepTask.new(adEnv, vehicle, target)
return setmetatable({adEnv = adEnv, vehicle = vehicle, target = target, elapsedMs = 0, checkMs = 0, inRange = false}, CreepTask)
end
function CreepTask:setUp()
local offset = offsetOf(self.vehicle, self.target.joint, self.target.target)
self.startDistance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
Logging.info(ADHitchCreep.LOG_PREFIX .. "'%s' bakker %.1f m hen til '%s' (%.1f m til siden)", vehicleName(self.vehicle),
self.startDistance, vehicleName(self.target.wagon), offset.x)
end
function CreepTask:finish(action, distance)
local sdm = self.vehicle.ad.specialDrivingModule
if sdm ~= nil and sdm.stopVehicle ~= nil then sdm:stopVehicle() end
if action == "done" then
Logging.info(ADHitchCreep.LOG_PREFIX .. "'%s' står ved '%s' — kobler på", vehicleName(self.vehicle), vehicleName(self.target.wagon))
else
Logging.warning(ADHitchCreep.LOG_PREFIX .. "'%s' opgav at bakke hen til '%s' (%.1f m tilbage efter %d s)", vehicleName(self.vehicle),
vehicleName(self.target.wagon), distance, math.floor(self.elapsedMs / 1000))
end
local taskModule = self.adEnv.ADTaskModule
self.vehicle.ad.taskModule:setCurrentTaskFinished(taskModule ~= nil and taskModule.DONT_PROPAGATE or 1)
end
function CreepTask:update(dt)
self.elapsedMs = self.elapsedMs + dt
self.checkMs = self.checkMs + dt
if self.target.wagon.getAttacherVehicle ~= nil and self.target.wagon:getAttacherVehicle() ~= nil then
return self:finish("giveUp", 0) -- en anden har taget vognen
end
local offset = offsetOf(self.vehicle, self.target.joint, self.target.target)
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
if self.checkMs >= ADHitchCreep.RANGE_CHECK_MS then
self.checkMs = 0
self.inRange = isInRange(self.vehicle, self.target.wagon)
end
local action = ADHitchCreep.step({inRange = self.inRange, distance = distance, startDistance = self.startDistance,
elapsedMs = self.elapsedMs, passed = offset.z > 0.5})
if action ~= "drive" then return self:finish(action, distance) end
local autoDrive, vehicle = self.adEnv.AutoDrive, self.vehicle
local p = worldPos(self.target.target)
local lx, lz = autoDrive.getDriveDirection(vehicle, p.x, p.y, p.z)
local sdm = vehicle.ad.specialDrivingModule
if sdm ~= nil and sdm.releaseVehicle ~= nil then sdm:releaseVehicle() end
if vehicle.startMotor ~= nil and not vehicle:getIsMotorStarted() and vehicle:getCanMotorRun() then vehicle:startMotor() end
-- som AD's ADSpecialDrivingModule:driveReverse (ikke-guidet): retning spejles ved bakning
autoDrive.driveInDirection(vehicle, dt, 30, 0.3, 0.2, 20, true, false, -lx, -lz, ADHitchCreep.CREEP_SPEED, 1)
end
function CreepTask:abort() end
function CreepTask:finished() end
function CreepTask:getInfoText() return "Bakker hen til vognen" end
function CreepTask:getI18nInfo() return "" end
function CreepTask:getExcludedVehiclesForCollisionCheck() return {self.target.wagon} end
local isInstalled = false
-- DriveToMode:handleFinishedTask lægger StopAndDisableADTask i køen; bakningen lægges ind lige før.
function ADHitchCreep.install(adEnv)
local mode = adEnv ~= nil and adEnv.DriveToMode or nil
if isInstalled or mode == nil or mode.handleFinishedTask == nil or ADHitch == nil or ADHitch.pendingAttach == nil then return end
isInstalled = true
local original = mode.handleFinishedTask
mode.handleFinishedTask = function(self)
local ok, err = pcall(function()
local job = self.driveToDestinationTask ~= nil and ADHitch.pendingAttach(self.vehicle) or nil
if job == nil or job.markerId ~= self.destinationID then return end
local target = ADHitchCreep.findTarget(self.vehicle)
if target == nil or target.plan ~= "creep" then return end
job.crept = true
self.vehicle.ad.taskModule:addTask(CreepTask.new(adEnv, self.vehicle, target))
end)
if not ok then Logging.warning(ADHitchCreep.LOG_PREFIX .. "bakning til vognen: %s", tostring(err)) end
return original(self)
end
end
+3
View File
@@ -1138,6 +1138,9 @@ local function installHook(adEnv)
if ADOutbound ~= nil and ADOutbound.install ~= nil then if ADOutbound ~= nil and ADOutbound.install ~= nil then
ADOutbound.install(adEnv) ADOutbound.install(adEnv)
end end
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
ADHitchCreep.install(adEnv)
end
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)") Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
end end
+10 -10
View File
@@ -126,20 +126,20 @@
<!-- vare: +/– navn · bjælke · mængde · værdi · netto/døgn og rækkevidde (klikbar) --> <!-- vare: +/– navn · bjælke · mængde · værdi · netto/døgn og rækkevidde (klikbar) -->
<Bitmap profile="fs25_multiTextOptionContainer" id="itemPrefab"> <Bitmap profile="fs25_multiTextOptionContainer" id="itemPrefab">
<Text profile="spuCellBold" name="c1" position="20px 0px" size="170px 40px" textMaxWidth="165px"/> <Text profile="spuCellBold" name="c1" position="20px 0px" size="170px 40px" textMaxWidth="165px"/>
<Bitmap profile="spuBarBg" name="barBg" position="190px 0px"/> <Bitmap profile="spuBarBg" name="barBg" position="190px 0px" size="140px 16px"/>
<Bitmap profile="spuBar" name="bar" position="190px 0px"/> <Bitmap profile="spuBar" name="bar" position="190px 0px" size="140px 16px"/>
<Text profile="spuCell" name="c2" position="400px 0px" size="140px 40px" textMaxWidth="135px"/> <Text profile="spuCell" name="c2" position="340px 0px" size="160px 40px" textMaxWidth="155px"/>
<Text profile="spuCell" name="c3" position="540px 0px" size="110px 40px" textMaxWidth="105px"/> <Text profile="spuCell" name="c3" position="500px 0px" size="105px 40px" textMaxWidth="100px"/>
<Text profile="spuCell" name="c4" position="650px 0px" size="160px 40px" textMaxWidth="155px"/> <Text profile="spuCell" name="c4" position="605px 0px" size="225px 40px" textMaxWidth="220px"/>
</Bitmap> </Bitmap>
<!-- bygning under en udfoldet vare: indrykket navn · tynd bjælke · mængde · værdi · procent --> <!-- bygning under en udfoldet vare: indrykket navn · tynd bjælke · mængde · værdi · procent -->
<Bitmap profile="fs25_multiTextOptionContainer" id="subPrefab"> <Bitmap profile="fs25_multiTextOptionContainer" id="subPrefab">
<Text profile="spuCellDim" name="c1" position="45px 0px" size="145px 40px" textMaxWidth="140px"/> <Text profile="spuCellDim" name="c1" position="45px 0px" size="145px 40px" textMaxWidth="140px"/>
<Bitmap profile="spuBarThinBg" name="barBg" position="190px 0px"/> <Bitmap profile="spuBarThinBg" name="barBg" position="190px 0px" size="140px 10px"/>
<Bitmap profile="spuBarThin" name="bar" position="190px 0px"/> <Bitmap profile="spuBarThin" name="bar" position="190px 0px" size="140px 10px"/>
<Text profile="spuCellDim" name="c2" position="400px 0px" size="140px 40px" textMaxWidth="135px"/> <Text profile="spuCellDim" name="c2" position="340px 0px" size="160px 40px" textMaxWidth="155px"/>
<Text profile="spuCellDim" name="c3" position="540px 0px" size="110px 40px" textMaxWidth="105px"/> <Text profile="spuCellDim" name="c3" position="500px 0px" size="105px 40px" textMaxWidth="100px"/>
<Text profile="spuCellDim" name="c4" position="650px 0px" size="160px 40px" textMaxWidth="155px"/> <Text profile="spuCellDim" name="c4" position="605px 0px" size="225px 40px" textMaxWidth="220px"/>
</Bitmap> </Bitmap>
<!-- udkørsel: én række pr. vare med politik Fra/Lager/Salg/Salg, ellers lager --> <!-- udkørsel: én række pr. vare med politik Fra/Lager/Salg/Salg, ellers lager -->
<Bitmap profile="fs25_multiTextOptionContainer" id="productPrefab"> <Bitmap profile="fs25_multiTextOptionContainer" id="productPrefab">
+2 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107"> <modDesc descVersion="107">
<author>masterdraco</author> <author>masterdraco</author>
<version>1.52.1.0</version> <version>1.53.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
@@ -47,6 +47,7 @@
<sourceFile filename="adFieldSilos.lua"/> <sourceFile filename="adFieldSilos.lua"/>
<sourceFile filename="adCpResume.lua"/> <sourceFile filename="adCpResume.lua"/>
<sourceFile filename="adHitch.lua"/> <sourceFile filename="adHitch.lua"/>
<sourceFile filename="adHitchCreep.lua"/>
<sourceFile filename="adPool.lua"/> <sourceFile filename="adPool.lua"/>
<sourceFile filename="adFlowWagons.lua"/> <sourceFile filename="adFlowWagons.lua"/>
<sourceFile filename="adFleetNames.lua"/> <sourceFile filename="adFleetNames.lua"/>
+4
View File
@@ -30,6 +30,10 @@ check("ankomst: stoppet ved markør", H.hasArrived({adActive = false, distance =
check("ankomst: AD kører stadig", H.hasArrived({adActive = true, distance = 0.5, speed = 0}), false) check("ankomst: AD kører stadig", H.hasArrived({adActive = true, distance = 0.5, speed = 0}), false)
check("ankomst: for langt væk", H.hasArrived({adActive = false, distance = 9, speed = 0}), false) check("ankomst: for langt væk", H.hasArrived({adActive = false, distance = 9, speed = 0}), false)
check("ankomst: ruller stadig", H.hasArrived({adActive = false, distance = 1, speed = 3}), false) check("ankomst: ruller stadig", H.hasArrived({adActive = false, distance = 1, speed = 3}), false)
-- efter at have bakket hen til vognen står traktoren længere fra markøren
check("ankomst: bakket til vognen, udvidet afstand", H.hasArrived({adActive = false, distance = 9, speed = 0, arriveDistance = 13}), true)
local crept = H.step(H.newJob(1, "Connect x", "attach"), {adActive = false, distance = 9, speed = 3, arriveDistance = 13}, 4000, false)
check("vagt: bakket, ruller lidt -> ikke 'stoppedAway'", crept.state, "driving")
-- vagten: efter ankomst forsøges koblingen et stykke tid, derefter opgives den -- vagten: efter ankomst forsøges koblingen et stykke tid, derefter opgives den
local job = H.newJob(25, "Connect Bredal", "attach") local job = H.newJob(25, "Connect Bredal", "attach")
check("job: venter på AD", job.state, "driving") check("job: venter på AD", job.state, "driving")
+26
View File
@@ -0,0 +1,26 @@
-- Kør: luajit tests/test_adHitchCreep.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adHitchCreep.lua")
local C = ADHitchCreep
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
-- plan: vognens trækøje i traktorens rum set fra krogen (x = til siden, z < 0 = bagved)
check("plan: inden for rummelig rækkevidde -> kobl direkte", C.plan({x = 0.4, z = -2.0}), "attach")
check("plan: 5 m lige bagved -> bak", C.plan({x = 0.3, z = -5.0}), "creep")
check("plan: lidt til siden -> bak", C.plan({x = -1.2, z = -6.0}), "creep")
check("plan: for langt til siden -> intet", C.plan({x = 2.5, z = -5.0}), "none")
check("plan: for langt bagved -> intet", C.plan({x = 0, z = -12.0}), "none")
check("plan: foran traktoren -> intet", C.plan({x = 0, z = 5.0}), "none")
check("plan: ingen vogn -> intet", C.plan(nil), "none")
-- skridt under bakningen
check("skridt: vognen inden for rækkevidde -> færdig", C.step({inRange = true, distance = 2.5, startDistance = 6, elapsedMs = 3000}), "done")
check("skridt: på vej -> kør", C.step({inRange = false, distance = 4, startDistance = 6, elapsedMs = 3000}), "drive")
check("skridt: for længe -> giv op", C.step({inRange = false, distance = 4, startDistance = 6, elapsedMs = C.CREEP_TIMEOUT_MS}), "giveUp")
check("skridt: kører væk fra vognen -> giv op", C.step({inRange = false, distance = 7.5, startDistance = 6, elapsedMs = 3000}), "giveUp")
check("skridt: forbi vognen uden kobling -> giv op", C.step({inRange = false, distance = 0.3, startDistance = 6, elapsedMs = 3000, passed = true}), "giveUp")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)