From 510360b002d4f3cff6912cceaed7979a1274808a Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 01:33:19 +0300 Subject: [PATCH] feat(vehicles): 16.7 - a boat is a buoyant hull in the game's physics (VehicleHull), rowed along its bow and turned by the oars; the scripted bob, the wind's drift and the shore refusal are gone The water floats the hull, the wind pushes every boat as a force, the lake bed stops it at the shore; the swell takes the outward share of a stroke. ludic.physics: phys_hull_add (a buoyant box), phys_row, phys_bow_speed, and a test that floats one a quarter under, rows, turns and grounds it. The vehicles tests row a real Jolt hull on the fake shore; the horse moved to horse.ludic. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.physics/hull.ludic | 26 ++++ packages/ludic.physics/index.ludic | 1 + .../ludic.physics/tests/motion_test.ludic | 30 +++++ packages/ludic.vehicles/README.md | 19 ++- packages/ludic.vehicles/call.ludic | 13 +- packages/ludic.vehicles/horse.ludic | 45 +++++++ packages/ludic.vehicles/index.ludic | 1 + packages/ludic.vehicles/kinds.ludic | 1 - packages/ludic.vehicles/ports.ludic | 20 +++- packages/ludic.vehicles/ride.ludic | 111 ++++++----------- packages/ludic.vehicles/system.ludic | 7 +- .../ludic.vehicles/tests/vehicles_test.ludic | 113 +++++++++++++++--- 12 files changed, 284 insertions(+), 103 deletions(-) create mode 100644 packages/ludic.physics/hull.ludic create mode 100644 packages/ludic.vehicles/horse.ludic diff --git a/packages/ludic.physics/hull.ludic b/packages/ludic.physics/hull.ludic new file mode 100644 index 00000000..42a41e31 --- /dev/null +++ b/packages/ludic.physics/hull.ludic @@ -0,0 +1,26 @@ +# hull.ludic - a boat: a buoyant box floated by PhysWater, rowed along its bow and turned about its +# middle. Its bow is -z at yaw 0, the way the valley faces (forward is (-sin yaw, -cos yaw)). + +# hx, hy, hz its half width, depth and length; buoyancy over 1 floats it (4: a quarter under) +export function phys_hull_add(physics_st: mut PhysicsState, x: float, y: float, z: float, yaw: float, hx: float, hy: float, hz: float, mass: float, buoyancy: float, drag: float) -> int { + let id = phys_body_add(physics_st, phys_box(physics_st, hx, hy, hz), x, y, z, yaw, mass) + if id < 0 { return -1 } + phys_float(physics_st, id, buoyancy, drag, drag * 1.5) + return id +} + +# a stroke of `forward` newtons along the bow and a turn of `turn` newton-metres about y, over the +# next step +export function phys_row(physics_st: mut PhysicsState, id: int, forward: float, turn: float) -> void { + let p = phys_pose(physics_st, id) + if p == null { return } + let yaw = phys_yaw_of(p) + phys_force(physics_st, id, -Math.sin(yaw) * forward, 0.0, -Math.cos(yaw) * forward) + phys_torque(physics_st, id, 0.0, turn, 0.0) +} + +# how fast it goes along its bow (backwards is negative) +export function phys_bow_speed(p: PhysPose) -> float { + let yaw = phys_yaw_of(p) + return -Math.sin(yaw) * p.vx - Math.cos(yaw) * p.vz +} diff --git a/packages/ludic.physics/index.ludic b/packages/ludic.physics/index.ludic index 72c200c5..d269641f 100644 --- a/packages/ludic.physics/index.ludic +++ b/packages/ludic.physics/index.ludic @@ -10,4 +10,5 @@ import "bodies.ludic" import "queries.ludic" import "water.ludic" import "walker.ludic" +import "hull.ludic" import "system.ludic" diff --git a/packages/ludic.physics/tests/motion_test.ludic b/packages/ludic.physics/tests/motion_test.ludic index 1d7f2d12..e1fad8cc 100644 --- a/packages/ludic.physics/tests/motion_test.ludic +++ b/packages/ludic.physics/tests/motion_test.ludic @@ -127,4 +127,34 @@ program MotionTest { expect_eq(float_bits(a.z), float_bits(b.z)) expect_eq(float_bits(a.qw), float_bits(b.qw)) } + + test "a hull floats a quarter under, is rowed along its bow, turned, and stopped by the lake bed" (physics_st: mut PhysicsState) { + world(physics_st) + let b = phys_hull_add(physics_st, 40.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0) + sim(physics_st, 4.0) + let p0 = phys_pose(physics_st, b) + expect(p0.y > -0.2) + expect(p0.y < 0.2) + for i in 0 .. 240 { + phys_row(physics_st, b, 420.0, 0.0) + phys_step(physics_st) + } + let p1 = phys_pose(physics_st, b) + expect(p1.x < 34.0) + expect(phys_bow_speed(p1) > 1.0) + for i in 0 .. 120 { + phys_row(physics_st, b, 0.0, 520.0) + phys_step(physics_st) + } + expect(Math.abs(phys_yaw_of(phys_pose(physics_st, b)) - phys_yaw_of(p1)) > 0.5) + phys_close(physics_st) + world(physics_st) + let c = phys_hull_add(physics_st, 24.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0) + for i in 0 .. 600 { + phys_row(physics_st, c, 420.0, 0.0) + phys_step(physics_st) + } + expect(phys_pose(physics_st, c).x > 19.5) + phys_close(physics_st) + } } diff --git a/packages/ludic.vehicles/README.md b/packages/ludic.vehicles/README.md index 888da964..6723980b 100644 --- a/packages/ludic.vehicles/README.md +++ b/packages/ludic.vehicles/README.md @@ -23,11 +23,19 @@ through the game). `safe_spot` - a wade of up to 1.25 m within its search from a boat, beside the right flank from a horse - and a boat too far out refuses and says so (`VEHICLE_NO_LANDING`) rather than dropping the rider in the lake. -- **Steering follows signed speed**: backing up with the stick left swings the nose right. +- **A boat is a body in the game's physics** (`VehicleHull`, phase 16; a buoyant Jolt hull in Maroon + Lake). The package rows it - the stick forward and back is a stroke along the bow, left and right + 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'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`. ## The ports +`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. + ```ludic export port VehicleWorld { ground: fn(float, float) -> float # required @@ -52,7 +60,7 @@ export port VehicleRider { | | | | --- | --- | | `VEHICLE_HORSE`, `VEHICLE_BOAT`, `VEHICLE_PLACES`, `VEHICLE_HAY_MIN` | the kinds, four places each, and the hay a horse needs to go | -| `Vehicle { kind, nid, x, y, z, yaw, speed, fuel, food, seat_h, bob, owned, owner, rider, home_x, home_z }` | a vehicle (change one through the verbs) | +| `Vehicle { kind, nid, x, y, z, yaw, speed, fuel, food, seat_h, owned, owner, rider, home_x, home_z }` | a vehicle (change one through the verbs) | | `vehicles_dock_at(x, z, yaw)`, `vehicles_rail_at(x, z, yaw)`, `vehicles_dock_yaw()` | where the places are measured from | | `vehicle_berth_x/z(k)`, `vehicle_post_x/z(k)`, `vehicle_bay_x/z(k)`, `vehicle_place_x/z(kind, k)` | the places | | `vehicle_call_for(pid, kind, k) -> bool`, `vehicle_in_place(kind, k)`, `vehicle_free_place(kind)`, `vehicle_mine_in_place(kind)` | calling one to a place | @@ -60,7 +68,7 @@ export port VehicleRider { | `vehicle_mount(v) -> bool`, `vehicle_dismount() -> bool`, `vehicle_drop()` | on and off | | `vehicle_update(ix, iz, run, dt)` | a frame of riding from the rider's stick (bind it where the body hands its stick over) | | `vehicle_own(v)`, `vehicle_feed(v, hay)`, `vehicle_follow(v, x, y, z, yaw)`, `vehicles_release(pid)`, `vehicle_restore(kind, owned, food)` | hiring, hay, a tied horse's wandering, a player leaving, a saved word | -| `vehicles_tick(dt)` | the idle boats' bob (the game calls it where the world is run) | +| `vehicles_tick(dt)` | every boat's hull read back and pushed by the wind (the game calls it where the world is run) | | `vehicle_facts() -> Queue` | `{ what, nid, kind, owner, x, z, v }`: `VEHICLE_MOUNTED`, `_DISMOUNTED`, `_NO_LANDING`, `_MOVED` (v: metres), `_SWELL` (every 14 s), `_HUNGRY`, `_FED` | | `vehicles_reset()`, `vehicles_system() -> System` | `"vehicles"`: this player's own saved by kind (hired, hay) - where they lie is not saved | @@ -71,5 +79,6 @@ ludic test packages/ludic.vehicles ``` A fake shore with a pier head and a trough: berths and bays, calling and fetching, a taken place, a -player leaving, a hungry horse, riding a horse into a trunk and to the water's edge, rowing to the -shore, the swell and the wind, getting off near a shore and far from one, and the save. +player leaving, a hungry horse, riding a horse into a trunk and to the water's edge; a real Jolt hull +rowed out, losing its stroke to the swell, turned, stopped by the shore's bed and drifted by the +wind; getting off near a shore and far from one, and the save. diff --git a/packages/ludic.vehicles/call.ludic b/packages/ludic.vehicles/call.ludic index c4f54b5d..9c9ab3f8 100644 --- a/packages/ludic.vehicles/call.ludic +++ b/packages/ludic.vehicles/call.ludic @@ -15,10 +15,20 @@ function ve_add(vehicles_st: mut VehiclesState, kind: int, x: float, z: float, y } ve_put(v, x, z) push(ve_all(vehicles_st), v) + ve_settle(v) VehicleWorld.made(v) 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) } } + +# gone from the world: its model and, for a boat, its hull +function ve_gone(v: Vehicle) -> void { + if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } + VehicleWorld.gone(v) +} + # at rest at (x, z): a boat on the water, a horse on the ground and tied there function ve_put(v: Vehicle, x: float, z: float) -> void { v.x = x @@ -48,6 +58,7 @@ export function vehicle_call_for(vehicles_st: mut VehiclesState, pid: int, kind: if v.rider >= 0 { return false } ve_put(v, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k)) v.yaw = yaw + ve_settle(v) VehicleWorld.placed(v) return true } @@ -58,7 +69,7 @@ export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> vo let keep = new []Vehicle let l = ve_all(vehicles_st) for i in 0 .. len(l) { - if l[i].owner != pid { push(keep, l[i]) } else { VehicleWorld.gone(l[i]) } + if l[i].owner != pid { push(keep, l[i]) } else { ve_gone(l[i]) } } vehicles_st.ve_list = keep } diff --git a/packages/ludic.vehicles/horse.ludic b/packages/ludic.vehicles/horse.ludic new file mode 100644 index 00000000..283102c3 --- /dev/null +++ b/packages/ludic.vehicles/horse.ludic @@ -0,0 +1,45 @@ +# 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 +const VE_HORSE_WALK: float = 4.5 +const VE_HORSE_GALLOP: float = 11.0 +const VE_HORSE_ACC: float = 4.0 +const VE_HORSE_TURN: float = 1.6 + +function ve_ride_horse(vehicles_st: mut VehiclesState, v: Vehicle, ix: float, iz: float, run: bool, dt: float) -> void { + var top = VE_HORSE_WALK + if run and v.fuel > 5.0 { top = VE_HORSE_GALLOP } + v.speed = v.speed + Math.clamp(iz * top - v.speed, -VE_HORSE_ACC * dt, VE_HORSE_ACC * dt) + # Steering follows SIGNED speed: backing up with the stick left swings the nose right + if Math.abs(v.speed) > 0.3 { + var rate = Math.clamp(Math.abs(v.speed) / 3.0, 0.4, 1.0) + if v.speed < 0.0 { rate = -rate } + v.yaw = v.yaw - ix * VE_HORSE_TURN * (dt * rate) + } + 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: mut 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 { + 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)) } +} + +# a gallop spends stamina and a walk gives it back; every metre costs hay +function ve_stamina(v: Vehicle, d: float, dt: float) -> void { + if Math.abs(v.speed) > 6.0 { v.fuel = Math.max(v.fuel - 6.0 * dt, 0.0) } else { v.fuel = Math.min(v.fuel + 3.0 * dt, 100.0) } + v.food = Math.max(v.food - Math.abs(d) * 0.02, 0.0) +} + diff --git a/packages/ludic.vehicles/index.ludic b/packages/ludic.vehicles/index.ludic index b3ca745b..c7bdf9fe 100644 --- a/packages/ludic.vehicles/index.ludic +++ b/packages/ludic.vehicles/index.ludic @@ -11,4 +11,5 @@ import "places.ludic" import "call.ludic" import "mount.ludic" import "ride.ludic" +import "horse.ludic" import "system.ludic" diff --git a/packages/ludic.vehicles/kinds.ludic b/packages/ludic.vehicles/kinds.ludic index fa604780..9b78a6d4 100644 --- a/packages/ludic.vehicles/kinds.ludic +++ b/packages/ludic.vehicles/kinds.ludic @@ -14,7 +14,6 @@ export property Vehicle { fuel: float = 0.0 # a horse's stamina, 0..100 food: float = 0.0 # a horse's hay, 0..100 seat_h: float = 0.0 # the rider sits this far above it - bob: float = 0.0 owned: bool = false # hired or rented for the trip owner: int = 0 # the player it belongs to (0: this machine's) rider: int = -1 # the player on it, -1 nobody diff --git a/packages/ludic.vehicles/ports.ludic b/packages/ludic.vehicles/ports.ludic index 9ab2c2c2..fcaf703c 100644 --- a/packages/ludic.vehicles/ports.ludic +++ b/packages/ludic.vehicles/ports.ludic @@ -1,5 +1,6 @@ -# ports.ludic - what a vehicle asks the world and its rider. Unbound, the world is dry flat ground -# with nothing in the way and no wind, and the rider is put exactly where asked. +# ports.ludic - what a vehicle asks the world, its hull and its rider. Unbound, the world is dry +# flat ground with nothing in the way and no wind, and the rider is put exactly where asked; the +# hull has no default - a boat is a body in the game's physics. export port VehicleWorld { ground: fn(float, float) -> float water: fn(float, float) -> float = fn ve__dry # the surface over a point @@ -13,6 +14,21 @@ export port VehicleWorld { gone: fn(Vehicle) -> void = fn ve__nothing # its owner left: take it away } +# a boat's hull in the world's physics (Maroon Lake: a buoyant Jolt body), named by the vehicle's +# nid. The water floats it and the lake bed stops it; the package rows it and reads where it is. +export port VehicleHull { + put: fn(int, float, float, float, float) -> void # (nid, x, y, z, yaw): at rest there, made if new + row: fn(int, float, float) -> void # (nid, forward N, turn N.m) over the next step + drift: fn(int, float, float) -> void # (nid, fx, fz): a push across the water (wind) + read: fn(int) -> bool # (nid): its pose into x .. speed; false if none + x: fn() -> float + y: fn() -> float + z: fn() -> float + yaw: fn() -> float + speed: fn() -> float # along its bow, m/s + gone: fn(int) -> void # (nid): out of the world +} + # the body on board: the game's walking body (ludic.character, through the game) export port VehicleRider { board: fn(float, float, float) -> void = fn ve__board # (x, z, yaw) diff --git a/packages/ludic.vehicles/ride.ludic b/packages/ludic.vehicles/ride.ludic index 25ed2a32..96286c15 100644 --- a/packages/ludic.vehicles/ride.ludic +++ b/packages/ludic.vehicles/ride.ludic @@ -1,106 +1,69 @@ # ride.ludic - the rider's stick steers what they are on: a horse walks, canters on stamina and -# stops at water, a slope or a trunk; a boat rows, drifts on the wind and stops at the shore -const VE_HORSE_WALK: float = 4.5 -const VE_HORSE_GALLOP: float = 11.0 -const VE_HORSE_ACC: float = 4.0 -const VE_HORSE_TURN: float = 1.6 -const VE_BOAT_TOP: float = 3.2 -const VE_BOAT_ACC: float = 1.2 -const VE_BOAT_TURN: float = 1.1 +# stops at water, a slope or a trunk; a boat is rowed - a stroke and a turn handed to its hull, +# which the water floats, the wind drifts and the lake bed stops +const VE_ROW: float = 630.0 # N: a full stroke forward, about 3 m/s on Maroon Lake's hull +const VE_ROW_TURN: float = 270.0 # N.m: the oars pulled against each other, about 1.1 rad/s +const VE_WIND: float = 70.0 # N on a boat in a full wind export function vehicle_update(vehicles_st: mut VehiclesState, ix: float, iz: float, run: bool, dt: float) -> void { let v = vehicles_st.ve_cur if v == null { return } - var top = VE_BOAT_TOP - var acc = VE_BOAT_ACC - var turn = VE_BOAT_TURN - if v.kind == VEHICLE_HORSE { - acc = VE_HORSE_ACC - turn = VE_HORSE_TURN - top = VE_HORSE_WALK - if run and v.fuel > 5.0 { top = VE_HORSE_GALLOP } - } - v.speed = v.speed + Math.clamp(iz * top - v.speed, -acc * dt, acc * dt) - if v.kind == VEHICLE_BOAT { ve_swell(vehicles_st, v, dt) } - # Steering follows SIGNED speed: backing up with the stick left swings the nose right - if Math.abs(v.speed) > 0.3 { - var rate = Math.clamp(Math.abs(v.speed) / 3.0, 0.4, 1.0) - if v.speed < 0.0 { rate = -rate } - v.yaw = v.yaw - ix * turn * (dt * rate) - } - 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) - if v.kind == VEHICLE_HORSE { ve_stamina(v, d, dt) } else { ve_float(v, d, dt) } + if v.kind == VEHICLE_BOAT { ve_row(vehicles_st, v, ix, iz, dt) } else { ve_ride_horse(vehicles_st, v, ix, iz, run, dt) } VehicleWorld.placed(v) VehicleRider.ride_at(v.x, v.y + v.seat_h, v.z, v.yaw, Math.abs(v.speed)) } -# a boat only where there is water under it; a horse only on dry ground, up a walkable rise, clear -function ve_travel(vehicles_st: mut VehiclesState, v: Vehicle, nx: float, nz: float, d: float) -> void { - let nh = VehicleWorld.ground(nx, nz) - var ok = true - if v.kind == VEHICLE_BOAT { ok = nh < VehicleWorld.water(nx, nz) - 0.35 } else { - 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 { - v.speed = 0.0 - return - } - v.x = nx - v.z = nz - if v.kind != VEHICLE_BOAT { v.y = nh } - if Math.abs(d) > 0.0 { ve_say(vehicles_st, VEHICLE_MOVED, v, Math.abs(d)) } +# the stick is the oars: forward and back a stroke, left and right the two pulled against each +# other; the swell at the edge takes the outward share of a stroke +function ve_row(vehicles_st: mut VehiclesState, v: Vehicle, ix: float, iz: float, dt: float) -> void { + ve_hull_read(v) + let take = ve_swell(vehicles_st, v, iz, dt) + VehicleHull.row(v.nid, iz * VE_ROW * (1.0 - take), -ix * VE_ROW_TURN) + let d = Math.abs(v.speed) * dt + if d > 0.0 { ve_say(vehicles_st, VEHICLE_MOVED, v, d) } +} + +# where the hull is now, and how fast it goes along its bow +function ve_hull_read(v: Vehicle) -> void { + if not VehicleHull.read(v.nid) { return } + v.x = VehicleHull.x() + v.y = VehicleHull.y() + v.z = VehicleHull.z() + v.yaw = VehicleHull.yaw() + v.speed = VehicleHull.speed() } # a boat gets a longer leash than a swimmer and the same current at the end of it: the outward -# part of the stroke only, so a boat at the limit can always be put about -function ve_swell(vehicles_st: mut VehiclesState, v: Vehicle, dt: float) -> void { +# part of the stroke only, so a boat at the limit can always be put about. The share it takes. +function ve_swell(vehicles_st: mut VehiclesState, v: Vehicle, iz: float, dt: float) -> float { var fx = -Math.sin(v.yaw) var fz = -Math.cos(v.yaw) - if v.speed < 0.0 { + if iz < 0.0 { fx = -fx fz = -fz } let take = Math.clamp(VehicleWorld.swell(v.x, v.z, fx, fz), 0.0, 1.0) - v.speed = v.speed * (1.0 - take) if take > 0.55 and vehicles_st.ve_swell_said < 0.0 { vehicles_st.ve_swell_said = 14.0 ve_say(vehicles_st, VEHICLE_SWELL, v, take) } vehicles_st.ve_swell_said = vehicles_st.ve_swell_said - dt + return take } -# a gallop spends stamina and a walk gives it back; every metre costs hay -function ve_stamina(v: Vehicle, d: float, dt: float) -> void { - if Math.abs(v.speed) > 6.0 { v.fuel = Math.max(v.fuel - 6.0 * dt, 0.0) } else { v.fuel = Math.min(v.fuel + 3.0 * dt, 100.0) } - v.food = Math.max(v.food - Math.abs(d) * 0.02, 0.0) -} - -# the bob on the water, higher in a wind, and the wind pushing the boat across the lake -function ve_float(v: Vehicle, d: float, dt: float) -> void { - v.bob = v.bob + dt - let w = VehicleWorld.wind() - v.y = VehicleWorld.water(v.x, v.z) + (0.02 + (0.05 + 0.12 * w) * Math.sin(v.bob * 1.4)) - let push = w * 0.6 * dt - let wx = v.x - Math.sin(VehicleWorld.wind_yaw()) * push - let wz = v.z - Math.cos(VehicleWorld.wind_yaw()) * push - if VehicleWorld.ground(wx, wz) < VehicleWorld.water(wx, wz) - 0.35 { - v.x = wx - v.z = wz - } -} - -# the boats nobody is in ride the swell where they lie +# every boat: its hull read back, and the wind pushing it across the lake, ridden or not export function vehicles_tick(vehicles_st: mut VehiclesState, dt: float) -> void { let l = ve_all(vehicles_st) + let w = VehicleWorld.wind() * VE_WIND + let wy = VehicleWorld.wind_yaw() for i in 0 .. len(l) { let v = l[i] - if v.kind == VEHICLE_BOAT and v.rider < 0 { - v.bob = v.bob + dt - v.y = VehicleWorld.water(v.x, v.z) + (0.02 + 0.04 * Math.sin(v.bob * 1.1)) - VehicleWorld.placed(v) + if v.kind == VEHICLE_BOAT { + if w > 0.0 { VehicleHull.drift(v.nid, -Math.sin(wy) * w, -Math.cos(wy) * w) } + if v.rider < 0 { + ve_hull_read(v) + VehicleWorld.placed(v) + } } } } diff --git a/packages/ludic.vehicles/system.ludic b/packages/ludic.vehicles/system.ludic index 1dcb6840..93c5da13 100644 --- a/packages/ludic.vehicles/system.ludic +++ b/packages/ludic.vehicles/system.ludic @@ -1,7 +1,10 @@ # system.ludic - vehicles as a system: a reset, and this player's own saved by kind (hired or not, -# and the horse's hay); where they lie is not saved - they are called again. The idle boats' bob is -# vehicles_tick, which the game calls where the world is run +# and the horse's hay); where they lie is not saved - they are called again. The boats' hulls are +# 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) } } + } vehicles_st.ve_list = new []Vehicle vehicles_st.ve_cur = null vehicles_st.ve_have_dock = false diff --git a/packages/ludic.vehicles/tests/vehicles_test.ludic b/packages/ludic.vehicles/tests/vehicles_test.ludic index 20f0e598..be171fdc 100644 --- a/packages/ludic.vehicles/tests/vehicles_test.ludic +++ b/packages/ludic.vehicles/tests/vehicles_test.ludic @@ -1,9 +1,11 @@ # 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. +# 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. import "ludic.vehicles" import "ludic.base" +import "ludic.physics" program VehiclesTest { numbers float @@ -28,6 +30,14 @@ program VehiclesTest { boarded: int = 0 sx: float = 0.0 sz: float = 0.0 + hull_nid: int = -1 + hull: int = -1 + hx: float = 0.0 + hy: float = 0.0 + hz: float = 0.0 + hyaw: float = 0.0 + hspeed: float = 0.0 + hulls: 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 } @@ -65,6 +75,52 @@ program VehiclesTest { function fk_sx(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.sx } function fk_sz(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.sz } + # the hull: one boat's body in a Jolt lake - the shore's bed as a heightfield, the water at 0 + 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 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)) + } + 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 { + phys_place(physics_st, vehicles_test_st.hull, x, y + 0.12, z, yaw) + phys_set_velocity(physics_st, vehicles_test_st.hull, 0.0, 0.0, 0.0) + return + } + vehicles_test_st.hull = phys_hull_add(physics_st, x, y + 0.12, z, yaw, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0) + vehicles_test_st.hull_nid = nid + vehicles_test_st.hulls += 1 + } + function fk_row(physics_st: mut PhysicsState, vehicles_test_st: VehiclesTestState, nid: int, f: float, t: float) -> void { phys_row(physics_st, vehicles_test_st.hull, f, t) } + function fk_drift(physics_st: mut PhysicsState, vehicles_test_st: VehiclesTestState, nid: int, fx: float, fz: float) -> void { phys_force(physics_st, vehicles_test_st.hull, fx, 0.0, fz) } + function fk_read(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int) -> bool { + if vehicles_test_st.hull < 0 or vehicles_test_st.hull_nid != nid { return false } + let p = phys_pose(physics_st, vehicles_test_st.hull) + if p == null { return false } + vehicles_test_st.hx = p.x + vehicles_test_st.hy = p.y + vehicles_test_st.hz = p.z + vehicles_test_st.hyaw = phys_yaw_of(p) + vehicles_test_st.hspeed = phys_bow_speed(p) + return true + } + function fk_hull_gone(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int) -> void { + if vehicles_test_st.hull_nid != nid { return } + phys_remove(physics_st, vehicles_test_st.hull) + vehicles_test_st.hull = -1 + } + function fk_hx(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.hx } + function fk_hy(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.hy } + function fk_hz(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.hz } + function fk_hyaw(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.hyaw } + 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 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 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 } @@ -85,8 +141,12 @@ program VehiclesTest { for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } - function ride(vehicles_st: mut VehiclesState, ix: float, iz: float, run: bool, secs: float) -> void { - for i in 0 .. int(secs * 60.0) { vehicle_update(vehicles_st, ix, iz, run, 1.0 / 60.0) } + function ride(physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, ix: float, iz: float, run: bool, secs: float) -> void { + for i in 0 .. int(secs * 60.0) { + vehicle_update(vehicles_st, ix, iz, run, 1.0 / 60.0) + vehicles_tick(vehicles_st, 1.0 / 60.0) + phys_step(physics_st) + } } test "a berth is off the pier head in the water, a bay on the meadow" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) { @@ -147,22 +207,22 @@ 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" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) { + 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) { fresh(vehicles_st, vehicles_test_st) vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) let h = vehicle_find(vehicles_st, VEHICLE_HORSE) vehicle_mount(vehicles_st, h) - ride(vehicles_st, 0.0, 1.0, false, 3.0) + ride(physics_st, vehicles_st, 0.0, 1.0, false, 3.0) expect_near(h.speed, 4.5, 0.05) expect(count(facts(vehicles_st), VEHICLE_MOVED) > 100) - ride(vehicles_st, 0.0, 1.0, true, 3.0) + ride(physics_st, vehicles_st, 0.0, 1.0, true, 3.0) expect_near(h.speed, 11.0, 0.1) expect(h.fuel < 100.0) 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 - ride(vehicles_st, 0.0, 1.0, true, 6.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) # and the lake to the east does too @@ -171,29 +231,46 @@ program VehiclesTest { h.z = 0.0 h.yaw = EAST vehicle_mount(vehicles_st, h) - ride(vehicles_st, 0.0, 1.0, false, 4.0) + ride(physics_st, vehicles_st, 0.0, 1.0, false, 4.0) expect(h.x < 9.6) } - test "a boat rows on water, stops at the shore, loses its stroke to the swell, drifts on the wind" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) { + 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) { fresh(vehicles_st, vehicles_test_st) vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0) let b = vehicle_find(vehicles_st, VEHICLE_BOAT) + expect_eq(vehicles_test_st.hulls, 1) vehicle_mount(vehicles_st, b) - ride(vehicles_st, 0.0, 1.0, false, 2.0) - let x2 = b.x - expect(x2 > 18.0) + let x0 = b.x + ride(physics_st, vehicles_st, 0.0, 1.0, false, 6.0) + let x6 = b.x + expect(x6 > x0 + 8.0) + expect(b.speed > 2.0) + expect(b.y > -0.2) + ride(physics_st, vehicles_st, 0.0, 0.0, false, 6.0) vehicles_test_st.swell = 1.0 - ride(vehicles_st, 0.0, 1.0, false, 1.0) - expect_near(b.x, x2, 0.2) + let xs = b.x + ride(physics_st, vehicles_st, 0.0, 1.0, false, 2.0) + expect(b.x - xs < 1.0) expect_eq(count(facts(vehicles_st), VEHICLE_SWELL), 1) vehicles_test_st.swell = 0.0 - ride(vehicles_st, 0.0, -1.0, false, 12.0) - expect(b.x > 10.3) + let yaw0 = b.yaw + ride(physics_st, vehicles_st, 1.0, 0.0, false, 2.0) + expect(Math.abs(char_turn(b.yaw - yaw0)) > 1.0) + ride(physics_st, vehicles_st, 0.0, -1.0, false, 25.0) + ride(physics_st, vehicles_st, 0.0, 0.0, false, 3.0) + expect(b.x > 9.5) vehicles_test_st.wind = 1.0 let z0 = b.z - ride(vehicles_st, 0.0, 0.0, false, 1.0) - expect(b.z < z0) + ride(physics_st, vehicles_st, 0.0, 0.0, false, 3.0) + expect(b.z < z0 - 0.2) + } + + function char_turn(a: float) -> float { + var d = a + while d > 3.14159265 { d -= 6.2831853 } + while d < -3.14159265 { d += 6.2831853 } + return d } test "getting off a boat needs a shore near enough; a horse is left tied where it stands" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {