local M = dofile(assert(arg[1], "movement.lua path required")) local total = 0 local function test(name, fn) local ok, err = pcall(fn) assert(ok, name .. ": " .. tostring(err)); total = total + 1 end local function fixture(stamina) return {}, {stamina = stamina or 100, food = 80, water = 80, wet = 0}, {aux1 = true, up = true}, {grounded = true, velocity = {x = 3, y = 0, z = 0}} end local function near(a, b) assert(math.abs(a - b) < 0.00001, tostring(a) .. " ~= " .. tostring(b)) end test("zero negative and invalid dt cannot change speed or stamina", function() local s, state, c, e = fixture() for _, dt in ipairs({0, -1, 0 / 0}) do local r = M.step(s, state, c, dt, e); near(r.speed, 1); near(state.stamina, 100) end end) test("stall clamps speed and stamina without replay", function() local s, state, c, e = fixture() local r = M.step(s, state, c, 100, e) near(r.speed, 1.3); near(state.stamina, 98.7) end) test("speed smoothly reaches target without overshoot", function() local s, state, c, e = fixture() local r = M.step(s, state, c, 0.05, e); near(r.speed, 1.15) for _ = 1, 8 do r = M.step(s, state, c, 0.05, e); assert(r.speed <= 1.45) end near(r.speed, 1.45) c.aux1 = false; r = M.step(s, state, c, 0.05, e); near(r.speed, 1.3) end) test("exhaustion cannot stutter while aux remains held", function() local s, state, c, e = fixture(8.5) assert(not M.step(s, state, c, 0.1, e).sprinting and s.exhausted) for _ = 1, 90 do assert(not M.step(s, state, c, 0.1, e).sprinting) end assert(state.stamina >= 30) c.aux1 = false; M.step(s, state, c, 0.1, e) c.aux1 = true; assert(M.step(s, state, c, 0.1, e).sprinting) end) test("release remembers intent but still requires thirty reserve", function() local s, state, c, e = fixture(8) M.step(s, state, c, 0, e); c.aux1 = false; M.step(s, state, c, 0.1, e) c.aux1 = true; state.stamina = 29.99 assert(not M.step(s, state, c, 0, e).sprinting) state.stamina = 30; assert(M.step(s, state, c, 0, e).sprinting) -- A release at zero must survive subsequent held frames below the threshold. s, state, c, e = fixture(0); c.aux1 = false; M.step(s, state, c, 0.1, e) c.aux1 = true for _ = 1, 34 do M.step(s, state, c, 0.1, e) end assert(M.step(s, state, c, 0, e).sprinting) end) test("backward diagonal backwards and sneak never sprint", function() local s, state, c, e = fixture() c.down = true; assert(not M.step(s, state, c, 0.1, e).sprinting) c.down, c.up = false, false; assert(not M.step(s, state, c, 0.1, e).sprinting) c.up, c.sneak = true, true; assert(not M.step(s, state, c, 0.1, e).sprinting) end) test("spear brace requires wield and takes priority over sprint", function() local s, state, c, e = fixture(); c.RMB = true; e.weapon = "hearth:spear" local r = M.step(s, state, c, 0.1, e) assert(r.blocking and not r.sprinting); near(r.speed, 0.7) r = M.step(s, state, c, 0.1, e); near(r.speed, 0.6) e.weapon = "hearth:knife"; r = M.step(s, state, c, 0.1, e) assert(not r.blocking and r.sprinting) end) test("real vertical velocity and support determine grounded", function() local s, state, c, e = fixture(); e.velocity.y = 4 assert(not M.step(s, state, c, 0.1, e).grounded) e.velocity.y = -4; assert(not M.step(s, state, c, 0.1, e).sprinting) e.velocity.y, e.grounded = 0, false; assert(not M.step(s, state, c, 0.1, e).sprinting) end) test("wet speed does not change wetness or other survival values", function() local s, state, c, e = fixture(); state.wet = 75; c.aux1 = false local r = M.step(s, state, c, 0.1, e); near(r.speed, 0.95) assert(state.wet == 75 and state.food == 80 and state.water == 80) end) test("swimming forbids sprint brace and uses swim pace", function() local s, state, c, e = fixture(); e.swimming, e.weapon, c.RMB = true, "hearth:spear", true local r = M.step(s, state, c, 0.1, e) assert(r.swimming and not r.sprinting and not r.blocking); near(r.speed, 0.75) end) test("distance motion is velocity derived not a held direction", function() local s, state, c, e = fixture(); e.velocity.x = 0 assert(not M.step(s, state, c, 0.1, e).moving) e.velocity.z = 1; c.up = false; assert(M.step(s, state, c, 0.1, e).moving) end) test("recovery stays bounded and respects depleted needs", function() local s, state, c, e = fixture(50); c.aux1 = false; state.food = 0 M.step(s, state, c, 0.1, e); near(state.stamina, 50.2) state.stamina, state.food = 99.9, 80; M.step(s, state, c, 0.1, e); near(state.stamina, 100) end) assert(total == 12, "Unexpected movement regression count") print("PASS " .. total .. " movement tests")