From bb2a4c10af749a23ad677f003971bf6801276d23 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 15:05:03 +0300 Subject: [PATCH] feat(vehicles): 16 - a horse walks on legs in the game's physics (VehicleLegs): the package keeps its pace, stamina, hay and its refusal of water; the legs meet the ground, its steps and slopes and whatever is in the way, and what they cover along its heading is its speed, so a fence stops it and a trunk it glances pushes it aside. VehicleWorld.blocked is gone; the hire, hay and follow verbs move to keep.ludic Co-Authored-By: Claude Opus 5.5 --- packages/ludic.vehicles/README.md | 8 ++- packages/ludic.vehicles/call.ludic | 47 +++------------- packages/ludic.vehicles/horse.ludic | 41 +++++++------- packages/ludic.vehicles/index.ludic | 2 + packages/ludic.vehicles/keep.ludic | 39 ++++++++++++++ packages/ludic.vehicles/legs.ludic | 30 +++++++++++ packages/ludic.vehicles/ports.ludic | 5 +- packages/ludic.vehicles/system.ludic | 5 +- .../ludic.vehicles/tests/vehicles_test.ludic | 53 +++++++++++++++---- 9 files changed, 156 insertions(+), 74 deletions(-) create mode 100644 packages/ludic.vehicles/keep.ludic create mode 100644 packages/ludic.vehicles/legs.ludic diff --git a/packages/ludic.vehicles/README.md b/packages/ludic.vehicles/README.md index 6723980b..fd8dece7 100644 --- a/packages/ludic.vehicles/README.md +++ b/packages/ludic.vehicles/README.md @@ -28,6 +28,11 @@ through the game). the oars pulled against each other - and reads back where the hull is and how fast it goes. The water floats it, the wind (`VehicleWorld.wind`) drifts every boat, ridden or not, and the lake bed stops it at the shore; the swell at the sail limit takes the outward share of a stroke. +- **A horse walks on legs in the game's physics** (`VehicleLegs`; a Jolt walker in Maroon Lake). The + package decides its pace - a walk, a canter on stamina, hay for every metre - and never steps it + into water; the legs meet the ground, its steps and slopes, and whatever stands in its way. What + they cover along its heading is its speed, so a fence brings it to a stand and a trunk it glances + pushes it aside. A horse called to its bay or led round where it is tied is stood there (`stand`). - **A horse's steering follows signed speed**: backing up with the stick left swings the nose right. - A stale rider locks a vehicle for ever, so getting off is `vehicle_dismount` or `vehicle_drop`. @@ -35,12 +40,13 @@ through the game). `VehicleHull { put(nid, x, y, z, yaw), row(nid, forward, turn), drift(nid, fx, fz), read(nid) -> bool, x, y, z, yaw, speed, gone(nid) }` is required: the boat's body, named by the vehicle's nid. +`VehicleLegs { walk(nid, x, y, z, vx, vz, dt) -> bool, x, y, z, stand(nid, x, y, z), gone(nid) }` is the +horse's: unbound, the ground is walked as it is and only a rise steeper than it can take stops it. ```ludic export port VehicleWorld { ground: fn(float, float) -> float # required water: fn(float, float) -> float # the surface (unbound: never wet) - blocked: fn(float, float, float) -> bool # (x, z, r): a collider in a horse's way swell: fn(float, float, float, float) -> float # (x, z, fx, fz): a stroke's share lost wind: fn() -> float # 0..1 wind_yaw: fn() -> float # the yaw the wind pushes toward diff --git a/packages/ludic.vehicles/call.ludic b/packages/ludic.vehicles/call.ludic index 75bb3ae4..168d0c92 100644 --- a/packages/ludic.vehicles/call.ludic +++ b/packages/ludic.vehicles/call.ludic @@ -20,12 +20,14 @@ function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw: return v } -# a boat's hull put where the vehicle now is, at rest -function ve_settle(v: Vehicle) -> void { if v.kind == VEHICLE_BOAT { VehicleHull.put(v.nid, v.x, v.y, v.z, v.yaw) } } +# a boat's hull or a horse's legs put where the vehicle now is, at rest +function ve_settle(v: Vehicle) -> void { + if v.kind == VEHICLE_BOAT { VehicleHull.put(v.nid, v.x, v.y, v.z, v.yaw) } else { VehicleLegs.stand(v.nid, v.x, v.y, v.z) } +} -# gone from the world: its model and, for a boat, its hull +# gone from the world: its model and its hull or legs function ve_gone(v: Vehicle) -> void { - if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } + if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) } VehicleWorld.gone(v) } @@ -73,40 +75,3 @@ export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> vo } vehicles_st.ve_list = keep } - -# hired or rented for the trip -export function vehicle_own(v: Vehicle) -> void { if v != null { v.owned = true } } - -# hay for a horse, up to a full belly -export function vehicle_feed(vehicles_st: VehiclesState, v: Vehicle, amount: float) -> void { - if v == null { return } - v.food = Math.min(v.food + amount, 100.0) - ve_say(vehicles_st, VEHICLE_FED, v, amount) -} - -# a horse nobody rides goes where its animal goes, round the home it is tied at (home_x, home_z) -export function vehicle_follow(v: Vehicle, x: float, y: float, z: float, yaw: float) -> void { - if v == null or v.rider >= 0 { return } - v.x = x - v.y = y - v.z = z - v.yaw = yaw -} - -# a saved trip's word on this player's own: hired, and how fed -export function vehicle_restore(vehicles_st: mut VehiclesState, kind: int, owned: bool, food: float) -> void { - if kind == VEHICLE_HORSE { - vehicles_st.ve_kept_horse = owned - vehicles_st.ve_kept_food = food - } else { vehicles_st.ve_kept_boat = owned } - let v = vehicle_find(vehicles_st, kind) - if v != null { ve_apply_kept(vehicles_st, v) } -} - - -function ve_apply_kept(vehicles_st: VehiclesState, v: Vehicle) -> void { - if v.kind == VEHICLE_HORSE { - v.owned = vehicles_st.ve_kept_horse - v.food = vehicles_st.ve_kept_food - } else { v.owned = vehicles_st.ve_kept_boat } -} diff --git a/packages/ludic.vehicles/horse.ludic b/packages/ludic.vehicles/horse.ludic index 399eada4..50ace149 100644 --- a/packages/ludic.vehicles/horse.ludic +++ b/packages/ludic.vehicles/horse.ludic @@ -1,5 +1,5 @@ -# horse.ludic - a horse under the stick: a walk, a canter on stamina, hay for every metre; it stops at -# water, at a rise it cannot take and at a still thing in its way +# horse.ludic - a horse under the stick: a walk, a canter on stamina, hay for every metre; it will +# not step into water, and its legs (VehicleLegs) meet the ground and whatever stands in its way const VE_HORSE_WALK: float = 4.5 const VE_HORSE_GALLOP: float = 11.0 const VE_HORSE_ACC: float = 4.0 @@ -15,26 +15,31 @@ function ve_ride_horse(vehicles_st: VehiclesState, v: Vehicle, ix: float, iz: fl if v.speed < 0.0 { rate = -rate } v.yaw = v.yaw - ix * VE_HORSE_TURN * (dt * rate) } + let fx = -Math.sin(v.yaw) + let fz = -Math.cos(v.yaw) let d = v.speed * dt - ve_travel(vehicles_st, v, v.x - Math.sin(v.yaw) * d, v.z - Math.cos(v.yaw) * d, d) - ve_stamina(v, d, dt) -} - -# a horse only on dry ground, up a walkable rise, clear of what is in its way -function ve_travel(vehicles_st: VehiclesState, v: Vehicle, nx: float, nz: float, d: float) -> void { - let nh = VehicleWorld.ground(nx, nz) - var ok = true - if nh < VehicleWorld.water(nx, nz) + 0.4 { ok = false } - if nh - v.y > Math.abs(d) * 0.9 { ok = false } - if VehicleWorld.blocked(nx, nz, 0.8) { ok = false } - if not ok { + let nh = VehicleWorld.ground(v.x + fx * d, v.z + fz * d) + if nh < VehicleWorld.water(v.x + fx * d, v.z + fz * d) + 0.4 { v.speed = 0.0 return } - v.x = nx - v.z = nz - v.y = nh - if Math.abs(d) > 0.0 { ve_say(vehicles_st, VEHICLE_MOVED, v, Math.abs(d)) } + ve_legs_walk(vehicles_st, v, fx * v.speed, fz * v.speed, dt) + ve_stamina(v, d, dt) +} + +# the legs carry it from where it is; what they covered along its heading is its speed now, never +# more than was asked, so a trunk or a rise it cannot take brings it to a stand +function ve_legs_walk(vehicles_st: VehiclesState, v: Vehicle, vx: float, vz: float, dt: float) -> void { + if not VehicleLegs.walk(v.nid, v.x, v.y, v.z, vx, vz, dt) { return } + let dx = VehicleLegs.x() - v.x + let dz = VehicleLegs.z() - v.z + v.x = VehicleLegs.x() + v.y = VehicleLegs.y() + v.z = VehicleLegs.z() + let got = (dx * -Math.sin(v.yaw) + dz * -Math.cos(v.yaw)) / dt + if v.speed > 0.0 { v.speed = Math.clamp(got, 0.0, v.speed) } else { v.speed = Math.clamp(got, v.speed, 0.0) } + let moved = Math.sqrt(dx * dx + dz * dz) + if moved > 0.0 { ve_say(vehicles_st, VEHICLE_MOVED, v, moved) } } # a gallop spends stamina and a walk gives it back; every metre costs hay diff --git a/packages/ludic.vehicles/index.ludic b/packages/ludic.vehicles/index.ludic index c7bdf9fe..ab502dd3 100644 --- a/packages/ludic.vehicles/index.ludic +++ b/packages/ludic.vehicles/index.ludic @@ -9,7 +9,9 @@ import "ports.ludic" import "state.ludic" import "places.ludic" import "call.ludic" +import "keep.ludic" import "mount.ludic" import "ride.ludic" import "horse.ludic" +import "legs.ludic" import "system.ludic" diff --git a/packages/ludic.vehicles/keep.ludic b/packages/ludic.vehicles/keep.ludic new file mode 100644 index 00000000..8f0f20ca --- /dev/null +++ b/packages/ludic.vehicles/keep.ludic @@ -0,0 +1,39 @@ +# keep.ludic - what a player does with their own between rides: hires it, feeds the horse, lets it +# wander where it is tied, and what a saved trip says of them +# hired or rented for the trip +export function vehicle_own(v: Vehicle) -> void { if v != null { v.owned = true } } + +# hay for a horse, up to a full belly +export function vehicle_feed(vehicles_st: VehiclesState, v: Vehicle, amount: float) -> void { + if v == null { return } + v.food = Math.min(v.food + amount, 100.0) + ve_say(vehicles_st, VEHICLE_FED, v, amount) +} + +# a horse nobody rides goes where its animal goes, round the home it is tied at (home_x, home_z) +export function vehicle_follow(v: Vehicle, x: float, y: float, z: float, yaw: float) -> void { + if v == null or v.rider >= 0 { return } + v.x = x + v.y = y + v.z = z + v.yaw = yaw + VehicleLegs.stand(v.nid, x, y, z) +} + +# a saved trip's word on this player's own: hired, and how fed +export function vehicle_restore(vehicles_st: mut VehiclesState, kind: int, owned: bool, food: float) -> void { + if kind == VEHICLE_HORSE { + vehicles_st.ve_kept_horse = owned + vehicles_st.ve_kept_food = food + } else { vehicles_st.ve_kept_boat = owned } + let v = vehicle_find(vehicles_st, kind) + if v != null { ve_apply_kept(vehicles_st, v) } +} + + +function ve_apply_kept(vehicles_st: VehiclesState, v: Vehicle) -> void { + if v.kind == VEHICLE_HORSE { + v.owned = vehicles_st.ve_kept_horse + v.food = vehicles_st.ve_kept_food + } else { v.owned = vehicles_st.ve_kept_boat } +} diff --git a/packages/ludic.vehicles/legs.ludic b/packages/ludic.vehicles/legs.ludic new file mode 100644 index 00000000..d1fb04f9 --- /dev/null +++ b/packages/ludic.vehicles/legs.ludic @@ -0,0 +1,30 @@ +# legs.ludic - a horse's legs in the world's physics (Maroon Lake: a Jolt walker), named by the +# vehicle's nid. Gravity, steps, slopes and whatever stands in its way are theirs; unbound, the +# ground is walked as it is, a rise steeper than it can take stops it and nothing else is in the way. +export port VehicleLegs { + walk: fn(int, float, float, float, float, float, float) -> bool = fn ve__walk # (nid, x, y, z, vx, vz, dt): its pose into x, y, z + x: fn() -> float = fn ve__lx + y: fn() -> float = fn ve__ly + z: fn() -> float = fn ve__lz + stand: fn(int, float, float, float) -> void = fn ve__stand # (nid, x, y, z): tied or led there + gone: fn(int) -> void = fn ve__legs_gone +} + +function ve__walk(vehicles_st: mut VehiclesState, nid: int, x: float, y: float, z: float, vx: float, vz: float, dt: float) -> bool { + vehicles_st.ve_lx = x + vehicles_st.ve_ly = y + vehicles_st.ve_lz = z + let nx = x + vx * dt + let nz = z + vz * dt + let nh = VehicleWorld.ground(nx, nz) + if nh - y > Math.sqrt(vx * vx + vz * vz) * dt * 0.9 { return true } + vehicles_st.ve_lx = nx + vehicles_st.ve_ly = nh + vehicles_st.ve_lz = nz + return true +} +function ve__lx(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_lx } +function ve__ly(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_ly } +function ve__lz(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_lz } +function ve__stand(nid: int, x: float, y: float, z: float) -> void { } +function ve__legs_gone(nid: int) -> void { } diff --git a/packages/ludic.vehicles/ports.ludic b/packages/ludic.vehicles/ports.ludic index f26a70ed..690eccca 100644 --- a/packages/ludic.vehicles/ports.ludic +++ b/packages/ludic.vehicles/ports.ludic @@ -4,7 +4,6 @@ export port VehicleWorld { ground: fn(float, float) -> float water: fn(float, float) -> float = fn ve__dry # the surface over a point - blocked: fn(float, float, float) -> bool = fn ve__open # (x, z, r): a collider in a horse's way swell: fn(float, float, float, float) -> float = fn ve__calm # (x, z, fx, fz): 0..1 of a stroke lost wind: fn() -> float = fn ve__zero # 0..1 wind_yaw: fn() -> float = fn ve__zero # the yaw the wind pushes toward @@ -56,10 +55,12 @@ export state VehiclesState { ve_rail_yaw: float = 0.0 ve_have_dock: bool = false ve_swell_said: float = 0.0 + ve_lx: float = 0.0 # the unbound legs' pose + ve_ly: float = 0.0 + ve_lz: float = 0.0 ve_fact_q: Queue = ve_fact_q__new() } function ve__dry(x: float, z: float) -> float { return -10000.0 } -function ve__open(x: float, z: float, r: float) -> bool { return false } function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 } function ve__zero() -> float { return 0.0 } function ve__next(vehicles_st: mut VehiclesState) -> int { diff --git a/packages/ludic.vehicles/system.ludic b/packages/ludic.vehicles/system.ludic index 44a6107b..fb6712f0 100644 --- a/packages/ludic.vehicles/system.ludic +++ b/packages/ludic.vehicles/system.ludic @@ -3,7 +3,10 @@ # read back and pushed by the wind in vehicles_tick, which the game calls where the world is run export function vehicles_reset(vehicles_st: mut VehiclesState) -> void { if vehicles_st.ve_list != null { - for i in 0 .. len(vehicles_st.ve_list) { if vehicles_st.ve_list[i].kind == VEHICLE_BOAT { VehicleHull.gone(vehicles_st.ve_list[i].nid) } } + for i in 0 .. len(vehicles_st.ve_list) { + let v = vehicles_st.ve_list[i] + if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) } + } } vehicles_st.ve_list = new []Vehicle vehicles_st.ve_cur = null diff --git a/packages/ludic.vehicles/tests/vehicles_test.ludic b/packages/ludic.vehicles/tests/vehicles_test.ludic index 04e124fa..62b2c790 100644 --- a/packages/ludic.vehicles/tests/vehicles_test.ludic +++ b/packages/ludic.vehicles/tests/vehicles_test.ludic @@ -1,8 +1,8 @@ # vehicles_test.ludic - a fake shore: land at x < 10, a lake past it deepening a metre a metre, a # pier head at the shore facing out and a trough on the meadow, a trunk in a horse's way. Calling to # berths and bays, the owner and the rider, mounting a hungry horse, riding both, the swell, the wind, -# getting off near a shore and far from one, a player leaving, and the save. The boat's hull is a -# real ludic.physics body on the same lake. +# getting off near a shore and far from one, a player leaving, and the save. The boat's hull and the +# horse's legs are real ludic.physics bodies on the same ground, the trunk a post in it. import "ludic.vehicles" import "ludic.base" import "ludic.physics" @@ -17,7 +17,6 @@ program VehiclesTest { if x > 9.5 { return 0.0 } return -10000.0 } - function fk_blocked(x: float, z: float, r: float) -> bool { return Math.abs(x) < 1.0 + r and Math.abs(z + 60.0) < 1.0 + r } state VehiclesTestState { swell: float = 0.0 wind: float = 0.0 @@ -38,6 +37,11 @@ program VehiclesTest { hyaw: float = 0.0 hspeed: float = 0.0 hulls: int = 0 + legs: int = -1 + lx: float = 0.0 + ly: float = 0.0 + lz: float = 0.0 + stood: int = 0 } function fk_swell(vehicles_test_st: VehiclesTestState, x: float, z: float, fx: float, fz: float) -> float { if fx > 0.0 { return vehicles_test_st.swell } @@ -79,11 +83,28 @@ program VehiclesTest { function fk_lake(physics_st: mut PhysicsState) -> void { if phys_is_open(physics_st) { return } phys_open(physics_st, 64, 1) - let n = 96 + let n = 160 let h = floats(n * n) - for j in 0 .. n { for i in 0 .. n { h[j * n + i] = fk_ground(float(i) - 20.0, float(j) - 48.0) } } - phys_ground_add(physics_st, phys_heightfield(physics_st, h, n, -20.0, -48.0, 1.0)) + for j in 0 .. n { for i in 0 .. n { h[j * n + i] = fk_ground(float(i) - 20.0, float(j) - 100.0) } } + phys_ground_add(physics_st, phys_heightfield(physics_st, h, n, -20.0, -100.0, 1.0)) + phys_static_add(physics_st, phys_box(physics_st, 15.0, 5.0, 1.0), 0.0, 0.0, -60.0, 0.0) } + + # the legs: the horse's own walker on that ground, made on its first step + function fk_walk(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int, x: float, y: float, z: float, vx: float, vz: float, dt: float) -> bool { + fk_lake(physics_st) + if vehicles_test_st.legs < 0 { vehicles_test_st.legs = phys_walker_add(physics_st, 0.8, 1.7, x, y, z, 40.0, 450.0) } + phys_walker_move(physics_st, vehicles_test_st.legs, x, y, z, vx, vz, 0.0, 0.45, 40.0, dt) + let p = phys_walker_pose(physics_st, vehicles_test_st.legs) + vehicles_test_st.lx = p.x + vehicles_test_st.ly = p.y + vehicles_test_st.lz = p.z + return true + } + function fk_lx(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.lx } + function fk_ly(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.ly } + function fk_lz(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.lz } + function fk_stand(vehicles_test_st: mut VehiclesTestState, nid: int, x: float, y: float, z: float) -> void { vehicles_test_st.stood += 1 } function fk_put(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int, x: float, y: float, z: float, yaw: float) -> void { fk_lake(physics_st) if vehicles_test_st.hull >= 0 and vehicles_test_st.hull_nid == nid { @@ -120,8 +141,9 @@ program VehiclesTest { function fk_hspeed(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.hspeed } bind VehicleHull { put: fn fk_put, row: fn fk_row, drift: fn fk_drift, read: fn fk_read, x: fn fk_hx, y: fn fk_hy, z: fn fk_hz, yaw: fn fk_hyaw, speed: fn fk_hspeed, gone: fn fk_hull_gone } bind PhysWater { height: fn fk_water } + bind VehicleLegs { walk: fn fk_walk, x: fn fk_lx, y: fn fk_ly, z: fn fk_lz, stand: fn fk_stand } - bind VehicleWorld { ground: fn fk_ground, water: fn fk_water, blocked: fn fk_blocked, swell: fn fk_swell, wind: fn fk_wind, made: fn fk_made, gone: fn fk_gone, placed: fn fk_placed } + bind VehicleWorld { ground: fn fk_ground, water: fn fk_water, swell: fn fk_swell, wind: fn fk_wind, made: fn fk_made, gone: fn fk_gone, placed: fn fk_placed } bind VehicleRider { board: fn fk_board, ride_at: fn fk_ride, step_off: fn fk_off, safe_spot: fn fk_safe, safe_x: fn fk_sx, safe_z: fn fk_sz } const EAST: float = -1.5707963 @@ -207,7 +229,7 @@ program VehiclesTest { expect(vehicle_in_place(vehicles_st, VEHICLE_HORSE, 0) == null) } - test "a horse walks, gallops on stamina, stops at the water and at a trunk" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) { + test "a horse walks, gallops on stamina, is pushed aside by a trunk it glances, stops at a fence and the water; called, it stands at its bay" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) { fresh(vehicles_st, vehicles_test_st) vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) let h = vehicle_find(vehicles_st, VEHICLE_HORSE) @@ -221,10 +243,14 @@ program VehiclesTest { expect(h.z < -15.0) expect_near(vehicles_test_st.rz, h.z, 0.001) expect_near(vehicles_test_st.ry, 1.35, 0.001) - # the trunk at z = -60 stops it + # a trunk half a metre off its line pushes it aside and lets it by; the fence at z = -60 stops it + let x0 = h.x + phys_static_add(physics_st, phys_cylinder(physics_st, 3.0, 0.3), x0 + 0.5, 0.0, -35.0, 0.0) ride(physics_st, vehicles_st, 0.0, 1.0, true, 6.0) - expect(h.z > -60.0 + 1.7) - expect_near(h.speed, 0.0, 0.001) + expect(h.z > -59.0 - 0.85) + expect(h.z < -50.0) + expect(Math.abs(h.x - x0) > 0.2) + expect_near(h.speed, 0.0, 0.05) # and the lake to the east does too vehicle_drop(vehicles_st) h.x = 7.0 @@ -233,6 +259,11 @@ program VehiclesTest { vehicle_mount(vehicles_st, h) ride(physics_st, vehicles_st, 0.0, 1.0, false, 4.0) expect(h.x < 9.6) + # a horse brought to its bay is stood there + vehicle_drop(vehicles_st) + let before = vehicles_test_st.stood + expect(vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 1)) + expect_eq(vehicles_test_st.stood, before + 1) } test "a boat is rowed out, loses its stroke to the swell, turns, is stopped by the shore's bed, drifts on the wind" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {