From bb2a4c10af749a23ad677f003971bf6801276d23 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 15:05:03 +0300 Subject: [PATCH 1/9] 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) { From dd86dce1719c3cbec07f9731916f219596245b24 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 15:22:30 +0300 Subject: [PATCH 2/9] feat(physics): 16 - a rock's own shape: phys_convex (Jolt's convex hull of a point cloud) and phys_scaled (one hull, every rock of that shape at its own size); the libraries rebuilt on the Mac and the PC (still KERNEL32 alone); hull_shape_test holds the scale and that a yaw turns a body the way a renderer turns an instance Co-Authored-By: Claude Opus 5.5 --- packages/ludic.physics/README.md | 2 +- .../lib/macos-arm64/libludicjolt.dylib | 4 +- .../lib/windows-x64/ludicjolt.dll | 4 +- .../lib/windows-x64/ludicjolt.lib | 4 +- packages/ludic.physics/native.ludic | 2 + .../ludic.physics/native/shim/jph_shapes.inl | 13 ++++++ .../ludic.physics/native/shim/jph_shim.cpp | 2 + packages/ludic.physics/shapes.ludic | 12 +++++ .../ludic.physics/tests/hull_shape_test.ludic | 44 +++++++++++++++++++ 9 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 packages/ludic.physics/tests/hull_shape_test.ludic diff --git a/packages/ludic.physics/README.md b/packages/ludic.physics/README.md index 167eee0c..c8768c7d 100644 --- a/packages/ludic.physics/README.md +++ b/packages/ludic.physics/README.md @@ -29,7 +29,7 @@ import "ludic.physics" | | | | --- | --- | | `phys_open(st, max_bodies, threads) -> bool`, `phys_close(st)`, `phys_is_open(st)` | a world, and everything in it let go | -| `phys_box`, `phys_sphere`, `phys_capsule`, `phys_cylinder`, `phys_dome(r, h)`, `phys_offset(shape, x, y, z, yaw)` | shapes, each an int (`-1` refused); a dome is a boulder standing on its origin | +| `phys_box`, `phys_sphere`, `phys_capsule`, `phys_cylinder`, `phys_dome(r, h)`, `phys_convex(points, n)`, `phys_scaled(shape, k)`, `phys_offset(shape, x, y, z, yaw)` | shapes, each an int (`-1` refused); a dome is a boulder standing on its origin; a convex hull of n points (x, y, z) is a rock's own shape, made once and scaled for every rock of that shape; a body's yaw turns it as a renderer turns an instance (x' = cos x + sin z, z' = -sin x + cos z) | | `phys_heightfield(st, h, n, ox, oz, cell)`, `phys_mesh(st, v, nv, tri, nt)` | the ground (`PHYS_HOLE` is none) and a dock or cabin | | `phys_ground_add`, `phys_static_add`, `phys_kinematic_add`, `phys_body_add(..., mass)` | bodies: the ground, still things, what the game moves (the player), what falls and floats | | `phys_settle(st)` | after adding many still things, before the first query | diff --git a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib index 6830f514..36c149dd 100755 --- a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib +++ b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:203b9c83905942e8da10d3e0a1dfe82949360dfa134b5b43c90113fc2e9acae7 -size 1161200 +oid sha256:b87a8d51bcc30d4479d60d28456cc6cc529820f90281feac9b69d420e0ccb623 +size 1161296 diff --git a/packages/ludic.physics/lib/windows-x64/ludicjolt.dll b/packages/ludic.physics/lib/windows-x64/ludicjolt.dll index 89416318..169c634e 100644 --- a/packages/ludic.physics/lib/windows-x64/ludicjolt.dll +++ b/packages/ludic.physics/lib/windows-x64/ludicjolt.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee8a0b0b909f87b7579917c01690add38638d4eada5c62e9ed1bdd388d72f831 -size 1720320 +oid sha256:0d4d413e11175d6ebcd62fdc25e7ef1ed54687b7850a0b804ac9cdc21615b728 +size 1721856 diff --git a/packages/ludic.physics/lib/windows-x64/ludicjolt.lib b/packages/ludic.physics/lib/windows-x64/ludicjolt.lib index 6ad76060..9bc6b13d 100644 --- a/packages/ludic.physics/lib/windows-x64/ludicjolt.lib +++ b/packages/ludic.physics/lib/windows-x64/ludicjolt.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32c6013b31d4ce2edee1a6901dafae5754638fb234d6f11e06130c0c9f7c60e4 -size 10050 +oid sha256:4d206c9b857d9832c730b8159fda0f327cec0339f4327ba39e031e4934154342 +size 10466 diff --git a/packages/ludic.physics/native.ludic b/packages/ludic.physics/native.ludic index 1961db78..5f47798d 100644 --- a/packages/ludic.physics/native.ludic +++ b/packages/ludic.physics/native.ludic @@ -14,6 +14,8 @@ extern function jph_shape_capsule(half_h: float, r: float) -> pointer = "jph_sha extern function jph_shape_cylinder(half_h: float, r: float) -> pointer = "jph_shape_cylinder" extern function jph_shape_dome(r: float, h: float) -> pointer = "jph_shape_dome" extern function jph_shape_offset(s: pointer, x: float, y: float, z: float, yaw: float) -> pointer = "jph_shape_offset" +extern function jph_shape_convex(v: pointer, n: int, radius: float) -> pointer = "jph_shape_convex" +extern function jph_shape_scaled(s: pointer, k: float) -> pointer = "jph_shape_scaled" extern function jph_shape_heightfield(h: pointer, n: int, ox: float, oz: float, cell: float) -> pointer = "jph_shape_heightfield" extern function jph_shape_mesh(v: pointer, nv: int, tri: pointer, nt: int) -> pointer = "jph_shape_mesh" extern function jph_shape_free(s: pointer) -> void = "jph_shape_free" diff --git a/packages/ludic.physics/native/shim/jph_shapes.inl b/packages/ludic.physics/native/shim/jph_shapes.inl index f43416e8..e61a0f87 100644 --- a/packages/ludic.physics/native/shim/jph_shapes.inl +++ b/packages/ludic.physics/native/shim/jph_shapes.inl @@ -17,6 +17,19 @@ JPH_SHIM void *jph_shape_dome(float r, float h) { return keep(RotatedTranslatedShapeSettings(Vec3(0.0f, h - big, 0.0f), Quat::sIdentity(), s).Create()); } +// a rock: the convex hull of n points (x, y, z) about its origin, its edges rounded by radius +JPH_SHIM void *jph_shape_convex(const float *v, int n, float radius) { + Array pts; + pts.reserve(n); + for (int i = 0; i < n; ++i) pts.push_back(Vec3(v[3 * i], v[3 * i + 1], v[3 * i + 2])); + return keep(ConvexHullShapeSettings(pts, radius).Create()); +} + +// another shape, k times its size on every axis (one hull, every boulder of that shape) +JPH_SHIM void *jph_shape_scaled(void *s, float k) { + return keep(ScaledShapeSettings(static_cast(s), Vec3::sReplicate(k)).Create()); +} + // a shape moved and turned about y, inside its body JPH_SHIM void *jph_shape_offset(void *s, float x, float y, float z, float yaw) { return keep(RotatedTranslatedShapeSettings(Vec3(x, y, z), yaw_q(yaw), static_cast(s)).Create()); diff --git a/packages/ludic.physics/native/shim/jph_shim.cpp b/packages/ludic.physics/native/shim/jph_shim.cpp index 4346538f..ab3f5da9 100644 --- a/packages/ludic.physics/native/shim/jph_shim.cpp +++ b/packages/ludic.physics/native/shim/jph_shim.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/packages/ludic.physics/shapes.ludic b/packages/ludic.physics/shapes.ludic index 3011970e..2e9598b4 100644 --- a/packages/ludic.physics/shapes.ludic +++ b/packages/ludic.physics/shapes.ludic @@ -59,6 +59,18 @@ export function phys_cylinder(physics_st: mut PhysicsState, half_h: float, r: fl # a boulder: a dome r across and h tall, standing on its origin export function phys_dome(physics_st: mut PhysicsState, r: float, h: float) -> int { return ph_keep(physics_st, jph_shape_dome(r, h)) } +# a rock: the convex hull of n points (x, y, z) about its origin, its edges rounded by 2 cm +export function phys_convex(physics_st: mut PhysicsState, v: []float, n: int) -> int { + if n < 4 or len(v) < n * 3 { return -1 } + return ph_keep(physics_st, jph_shape_convex(v, n, 0.02)) +} +# another shape k times its size, so one hull serves every boulder of its shape +export function phys_scaled(physics_st: mut PhysicsState, shape: int, k: float) -> int { + let s = ph_shape(physics_st, shape) + if s == null { return -1 } + return ph_keep(physics_st, jph_shape_scaled(s, k)) +} + # another shape moved and turned about y inside its body (a post whose foot is its origin) export function phys_offset(physics_st: mut PhysicsState, shape: int, x: float, y: float, z: float, yaw: float) -> int { let s = ph_shape(physics_st, shape) diff --git a/packages/ludic.physics/tests/hull_shape_test.ludic b/packages/ludic.physics/tests/hull_shape_test.ludic new file mode 100644 index 00000000..b8717015 --- /dev/null +++ b/packages/ludic.physics/tests/hull_shape_test.ludic @@ -0,0 +1,44 @@ +# hull_shape_test.ludic - a rock's shape: a convex hull of points, scaled for one boulder and +# turned by its yaw the way a renderer turns an instance (x' = cos x + sin z, z' = -sin x + cos z) +import "ludic.physics" +import "ludic.base" +program HullShapeTest { + numbers float + + # a cube two metres across, its centre two metres along x from its origin + function cube(physics_st: mut PhysicsState) -> int { + let v = floats(8 * 3) + for i in 0 .. 8 { + v[i * 3] = 2.0 + float(i % 2) * 2.0 - 1.0 + v[i * 3 + 1] = float((i / 2) % 2) * 2.0 - 1.0 + v[i * 3 + 2] = float(i / 4) * 2.0 - 1.0 + } + return phys_convex(physics_st, v, 8) + } + function down(physics_st: mut PhysicsState, x: float, z: float) -> PhysHit { return phys_ray(physics_st, x, 10.0, z, 0.0, -20.0, 0.0, PHYS_M_ALL) } + + test "a hull stands where its points are, and too few points are no shape" (physics_st: mut PhysicsState) { + expect(phys_open(physics_st, 16, 1)) + phys_static_add(physics_st, cube(physics_st), 0.0, 0.0, 0.0, 0.0) + phys_settle(physics_st) + let h = down(physics_st, 2.0, 0.0) + expect(h.body >= 0) + expect(Math.abs(h.y - 1.0) < 0.01) + expect(down(physics_st, 0.0, 0.0).body < 0) + expect_eq(phys_convex(physics_st, floats(9), 3), -1) + phys_close(physics_st) + } + + test "scaled twice as big, and turned a quarter the renderer's way" (physics_st: mut PhysicsState) { + expect(phys_open(physics_st, 16, 1)) + phys_static_add(physics_st, phys_scaled(physics_st, cube(physics_st), 2.0), 0.0, 0.0, 0.0, 1.5707963) + phys_settle(physics_st) + # (4, 0, 0) turned by +90 degrees about y is (0, 0, -4): x' = cos x + sin z, z' = -sin x + cos z + let h = down(physics_st, 0.0, -4.0) + expect(h.body >= 0) + expect(Math.abs(h.y - 2.0) < 0.01) + expect(down(physics_st, 4.0, 0.0).body < 0) + expect(down(physics_st, 0.0, 4.0).body < 0) + phys_close(physics_st) + } +} From 2c578c6f1c57b96ac1a7e2d07c508dfcae63c6cb Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 15:28:06 +0300 Subject: [PATCH 3/9] feat(jobs): phase 3 - a job can be given up: jobs_give_up puts a written job back on offer from the next day (the day back is saved with it, so giving up is never a reroll), jobs_post_give_up takes a post down for the morning to replace; an auto board's posts are not taken and not given up; JOBS_F_GIVEN_UP tells the game to take down what it placed Co-Authored-By: Claude Opus 5.5 --- packages/ludic.jobs/README.md | 3 +- packages/ludic.jobs/defs.ludic | 1 + packages/ludic.jobs/giveup.ludic | 25 ++++ packages/ludic.jobs/index.ludic | 1 + packages/ludic.jobs/jobs.ludic | 4 +- packages/ludic.jobs/save.ludic | 9 +- packages/ludic.jobs/state.ludic | 1 + packages/ludic.jobs/tests/giveup_test.ludic | 132 ++++++++++++++++++++ 8 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 packages/ludic.jobs/giveup.ludic create mode 100644 packages/ludic.jobs/tests/giveup_test.ludic diff --git a/packages/ludic.jobs/README.md b/packages/ludic.jobs/README.md index 7561670a..809b3028 100644 --- a/packages/ludic.jobs/README.md +++ b/packages/ludic.jobs/README.md @@ -64,7 +64,8 @@ export port JobsPay { reward: fn(int, int, int) -> void } # (scope, money, rep | `jobs_morning()`, `jobs_roll(lo, hi)`, `jobs_post(b, s, kind, param, need, money, rep)`, `jobs_config_seed(seed)` | posting | | `jobs_slots(b)`, `jobs_posted(b)`, `jobs_post_kind / _param / _need / _have / _money / _rep / _state / _day(b, s)`, `jobs_posts_done(b)`, `jobs_total(b)` | the boards | | `jobs_post_hand_in(b, s)`, `jobs_post_set(b, s, ...)`, `jobs_set_total(b, n)` | a post's verbs | -| `jobs_facts() -> Queue` | `{ what: JOBS_F_OFFERED / _PROGRESSED / _COMPLETED / _HANDED_IN, job, board, slot, kind, param, have, need, money, rep, scope }` | +| `jobs_give_up(i)`, `jobs_back_on(i)`, `jobs_post_give_up(b, s)` | letting one go: a written job is offered again from the next day (the day back is saved with it), a post comes down and the morning replaces it; an auto board's posts are never taken, so never given up | +| `jobs_facts() -> Queue` | `{ what: JOBS_F_OFFERED / _PROGRESSED / _COMPLETED / _HANDED_IN / _GIVEN_UP, job, board, slot, kind, param, have, need, money, rep, scope }` | | `jobs_system()`, `jobs_party_system()` | `"jobs"` (yours, the tick) and `"jobs.party"` (the party's), each saving its scope's jobs and boards by key | ## Tests diff --git a/packages/ludic.jobs/defs.ludic b/packages/ludic.jobs/defs.ludic index 4da15821..657fc49a 100644 --- a/packages/ludic.jobs/defs.ludic +++ b/packages/ludic.jobs/defs.ludic @@ -49,6 +49,7 @@ export const JOBS_F_OFFERED: int = 0 export const JOBS_F_PROGRESSED: int = 1 export const JOBS_F_COMPLETED: int = 2 export const JOBS_F_HANDED_IN: int = 3 +export const JOBS_F_GIVEN_UP: int = 4 # taken and let go: what the game placed for it comes down # `job` a written job, or -1 and `board` / `slot` a post; what it counted rides along, since a # post handed in is replaced before anyone reads the fact diff --git a/packages/ludic.jobs/giveup.ludic b/packages/ludic.jobs/giveup.ludic new file mode 100644 index 00000000..95b9c972 --- /dev/null +++ b/packages/ludic.jobs/giveup.ludic @@ -0,0 +1,25 @@ +# giveup.ludic - letting a job go. A written job goes back on offer, but not until tomorrow, so +# giving up is never a reroll; a post comes down and the morning puts up another in its place. +# Either way the fact says so, and the game takes down what it had placed for it. +export function jobs_give_up(jobs_st: mut JobsState, i: int) -> bool { + if not jobs_ok(i) or not jobs_under_way(jobs_st, i) { return false } + jobs__fact(jobs_st, JOBS_F_GIVEN_UP, i, -1, -1, jobs_st.jobs__hv[i], Jobs[i].need) + jobs_st.jobs__st[i] = JOBS_OFFERED + jobs_st.jobs__hv[i] = 0 + jobs_st.jobs__back[i] = JobsWorld.day() + 1 + return true +} + +# the day a written job given up may be taken again, 0 never given up +export function jobs_back_on(jobs_st: JobsState, i: int) -> int { return jobs_st.jobs__back[i] } + +# a post taken and not yet paid; an auto board's posts are not taken, so they are not given up +export function jobs_post_give_up(jobs_st: mut JobsState, b: int, s: int) -> bool { + if not jobs_board_ok(b) or s < 0 or s >= JobBoards[b].slots or JobBoards[b].auto { return false } + let k = jobs_st.jobs__off[b] + s + let st = jobs_st.jobs__ps[k] + if jobs_st.jobs__pk[k] < 0 or (st != JOBS_ACTIVE and st != JOBS_READY) { return false } + jobs__fact(jobs_st, JOBS_F_GIVEN_UP, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k]) + jobs__clear(jobs_st, k) + return true +} diff --git a/packages/ludic.jobs/index.ludic b/packages/ludic.jobs/index.ludic index b29ea912..6ff8a86a 100644 --- a/packages/ludic.jobs/index.ludic +++ b/packages/ludic.jobs/index.ludic @@ -7,6 +7,7 @@ import "defs.ludic" import "ports.ludic" import "state.ludic" import "jobs.ludic" +import "giveup.ludic" import "count.ludic" import "lists.ludic" import "boards.ludic" diff --git a/packages/ludic.jobs/jobs.ludic b/packages/ludic.jobs/jobs.ludic index 2ec1c933..2be089f8 100644 --- a/packages/ludic.jobs/jobs.ludic +++ b/packages/ludic.jobs/jobs.ludic @@ -20,12 +20,12 @@ export function jobs_under_way(jobs_st: JobsState, i: int) -> bool { # on offer: not taken, the scope's standing and the story far enough, and the game allowing it export function jobs_offered(jobs_st: JobsState, i: int) -> bool { let d = Jobs[i] - if jobs_state(jobs_st, i) != JOBS_OFFERED { return false } + if jobs_state(jobs_st, i) != JOBS_OFFERED or JobsWorld.day() < jobs_st.jobs__back[i] { return false } return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i) } export function jobs_take(jobs_st: mut JobsState, i: int) -> bool { - if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED { return false } + if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED or JobsWorld.day() < jobs_st.jobs__back[i] { return false } jobs_st.jobs__st[i] = JOBS_ACTIVE jobs_st.jobs__hv[i] = 0 jobs__recheck_one(jobs_st, i) diff --git a/packages/ludic.jobs/save.ludic b/packages/ludic.jobs/save.ludic index 46a15a0e..fedc10a3 100644 --- a/packages/ludic.jobs/save.ludic +++ b/packages/ludic.jobs/save.ludic @@ -5,6 +5,7 @@ function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void { if Jobs[i].scope != sc { continue } jobs_st.jobs__st[i] = JOBS_OFFERED jobs_st.jobs__hv[i] = 0 + jobs_st.jobs__back[i] = 0 } for b in 0 .. BOARD_COUNT { if JobBoards[b].scope != sc { continue } @@ -19,8 +20,8 @@ function jobs__save_scope(jobs_st: JobsState, sc: int) -> Val { let v = Value.object() let js = Value.object() for i in 0 .. JOB_COUNT { - if Jobs[i].scope != sc or jobs_st.jobs__st[i] == JOBS_OFFERED { continue } - sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i]]) + if Jobs[i].scope != sc or (jobs_st.jobs__st[i] == JOBS_OFFERED and jobs_st.jobs__back[i] == 0) { continue } + sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i], jobs_st.jobs__back[i]]) } Value.put(v, "jobs", js) let bs = Value.object() @@ -48,7 +49,9 @@ function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void { let js = Value.get(v, "jobs") for i in 0 .. JOB_COUNT { let p = sv_ints(js, Jobs[i].key) - if Jobs[i].scope == sc and len(p) == 2 { jobs_set(jobs_st, i, p[0], p[1]) } + if Jobs[i].scope != sc or len(p) < 2 { continue } + jobs_set(jobs_st, i, p[0], p[1]) + if len(p) > 2 { jobs_st.jobs__back[i] = p[2] } } } if Value.has(v, "boards") == 0 { return } diff --git a/packages/ludic.jobs/state.ludic b/packages/ludic.jobs/state.ludic index c3c2b7d8..370701f2 100644 --- a/packages/ludic.jobs/state.ludic +++ b/packages/ludic.jobs/state.ludic @@ -4,6 +4,7 @@ export state JobsState { jobs__st: []int = jobs__ints(JOB_COUNT, JOBS_OFFERED) jobs__hv: []int = jobs__ints(JOB_COUNT, 0) jobs__seen: []bool = jobs__bools(JOB_COUNT) # an offer already said + jobs__back: []int = jobs__ints(JOB_COUNT, 0) # given up: the first day it may be taken again jobs__primed: bool = false # the first tick after a reset learns the offers quietly jobs__done_n: []int = jobs__ints(JOBS_GROUPS, 0) # handed in, per group jobs__off: []int = jobs__offsets() diff --git a/packages/ludic.jobs/tests/giveup_test.ludic b/packages/ludic.jobs/tests/giveup_test.ludic new file mode 100644 index 00000000..96b83c66 --- /dev/null +++ b/packages/ludic.jobs/tests/giveup_test.ludic @@ -0,0 +1,132 @@ +# giveup_test.ludic - letting a job go: a written job back on offer tomorrow, not today; a post taken +# down and replaced in the morning; a daily never taken; the day back saved with the job +import "ludic.jobs" +import "ludic.base" +program JobsGiveUpTest { + numbers float + const K_FISH: int = 0 + const K_PHOTO: int = 1 + const K_WOOD: int = 2 + + def Jobs catch { group: 0, kind: K_FISH, need: 3, money: 30, rep: 5 } + def Jobs deer { group: 0, kind: K_PHOTO, param: 4, need: 1, money: 20, rep: 2, minrep: 10 } + def Jobs ten { group: 0, kind: K_FISH, need: 10, money: 70, rep: 12, scope: JOBS_PARTY } + def Jobs still { group: 1, need: 0, money: 60, rep: 12, minstage: 2 } + def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false } + def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY } + + state JobsTestState { + rep: int = 0 + stage: int = 0 + day: int = 1 + guest: bool = false + fish: int = 0 # the state's count of fish + paid: int = 0 + paid_rep: int = 0 + paid_scope: int = -1 + } + + function world_rep(jobs_test_st: JobsTestState, sc: int) -> int { return jobs_test_st.rep } + function world_stage(jobs_test_st: JobsTestState) -> int { return jobs_test_st.stage } + function world_day(jobs_test_st: JobsTestState) -> int { return jobs_test_st.day } + function world_owns(jobs_test_st: JobsTestState, sc: int) -> bool { return sc == JOBS_SELF or not jobs_test_st.guest } + function world_evidence(jobs_test_st: JobsTestState, kind: int, param: int) -> int { + if kind == K_FISH { return jobs_test_st.fish } + return -1 + } + function world_roll(jobs_st: mut JobsState, b: int, s: int) -> void { + let need = jobs_roll(jobs_st, 2, 4) + if b == BOARD_DAILY { jobs_post(jobs_st, b, s, K_WOOD, -1, need, 7, 2) } else { jobs_post(jobs_st, b, s, K_FISH, -1, need, 9 * need, 3) } + } + function pay(jobs_test_st: mut JobsTestState, sc: int, money: int, r: int) -> void { + jobs_test_st.paid += money + jobs_test_st.paid_rep += r + jobs_test_st.paid_scope = sc + } + + bind JobsWorld { rep: fn world_rep, stage: fn world_stage, day: fn world_day, owns: fn world_owns, evidence: fn world_evidence, roll: fn world_roll } + bind JobsPay { reward: fn pay } + + function fresh(jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) -> void { + jobs_test_st.rep = 0 + jobs_test_st.stage = 0 + jobs_test_st.day = 1 + jobs_test_st.guest = false + jobs_test_st.fish = 0 + jobs_test_st.paid = 0 + jobs_test_st.paid_rep = 0 + jobs_reset(jobs_st) + jobs_party_reset(jobs_st) + } + + function count_of(jobs_st: JobsState, what: int) -> int { + let fs = q_drain(jobs_facts(jobs_st)) + var n = 0 + for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } + return n + } + + test "a job given up is offered again tomorrow, not today" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + expect(jobs_take(jobs_st, JOB_CATCH)) + jobs_count(jobs_st, K_FISH, 0, 2) + count_of(jobs_st, JOBS_F_GIVEN_UP) + expect(jobs_give_up(jobs_st, JOB_CATCH)) + expect_eq(count_of(jobs_st, JOBS_F_GIVEN_UP), 1) + expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_OFFERED) + expect_eq(jobs_have(jobs_st, JOB_CATCH), 0) + expect(not jobs_offered(jobs_st, JOB_CATCH)) + expect(not jobs_take(jobs_st, JOB_CATCH)) + expect(not jobs_give_up(jobs_st, JOB_CATCH)) + jobs_test_st.day = 2 + expect(jobs_offered(jobs_st, JOB_CATCH)) + expect(jobs_take(jobs_st, JOB_CATCH)) + expect_eq(jobs_test_st.paid, 0) + } + + test "a ready job can be let go too, and nothing is paid" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_take(jobs_st, JOB_CATCH) + jobs_count(jobs_st, K_FISH, 0, 3) + expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_READY) + expect(jobs_give_up(jobs_st, JOB_CATCH)) + expect(not jobs_hand_in(jobs_st, JOB_CATCH)) + expect_eq(jobs_test_st.paid, 0) + } + + test "a post given up comes down, and the morning puts up another" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_morning(jobs_st) + count_of(jobs_st, JOBS_F_OFFERED) + expect(jobs_post_give_up(jobs_st, BOARD_ADA, 0)) + expect_eq(count_of(jobs_st, JOBS_F_GIVEN_UP), 1) + expect_eq(jobs_post_kind(jobs_st, BOARD_ADA, 0), -1) + expect(not jobs_post_give_up(jobs_st, BOARD_ADA, 0)) + expect(jobs_post_kind(jobs_st, BOARD_ADA, 1) >= 0) + jobs_test_st.day = 2 + jobs_morning(jobs_st) + expect(jobs_post_kind(jobs_st, BOARD_ADA, 0) >= 0) + expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 2) + } + + test "a daily is not taken, so it is not given up" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_morning(jobs_st) + expect(not jobs_post_give_up(jobs_st, BOARD_DAILY, 0)) + expect(jobs_post_kind(jobs_st, BOARD_DAILY, 0) >= 0) + } + + test "the day back is saved with the job" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_take(jobs_st, JOB_CATCH) + jobs_give_up(jobs_st, JOB_CATCH) + let body = jobs_save(jobs_st) + fresh(jobs_st, jobs_test_st) + expect(jobs_offered(jobs_st, JOB_CATCH)) + jobs_load(jobs_st, body, 1) + expect_eq(jobs_back_on(jobs_st, JOB_CATCH), 2) + expect(not jobs_offered(jobs_st, JOB_CATCH)) + jobs_test_st.day = 2 + expect(jobs_offered(jobs_st, JOB_CATCH)) + } +} From 9b8606483d8663f53f8d544449d9cc7f2cd5a332 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 15:29:11 +0300 Subject: [PATCH 4/9] feat(nav): 17.1/17.3 - ludic.nav over Recast & Detour v1.6.0 (zlib), built here from the pinned tag on the Mac and the PC (the DLL imports KERNEL32 alone): a navmesh per kind of walker from triangles with an area byte each and cylinders nothing stands in, saved and loaded as bytes; the nearest point, a path as corners (partial when the end cannot be reached), whether a straight line stays walkable, a cost per kind of ground. Tests on a hand-built meadow: round a post, over a ford, stopped at a bank, a saved mesh answering alike - Mac and PC Co-Authored-By: Claude Opus 5.5 --- packages/README.md | 1 + packages/ludic.nav/README.md | 56 +++++++++ packages/ludic.nav/build.ludic | 37 ++++++ packages/ludic.nav/index.ludic | 10 ++ .../lib/macos-arm64/libludicnav.dylib | 3 + .../ludic.nav/lib/windows-x64/ludicnav.dll | 3 + .../ludic.nav/lib/windows-x64/ludicnav.lib | 3 + packages/ludic.nav/native.ludic | 12 ++ .../ludic.nav/native/LICENSE-recastnavigation | 18 +++ packages/ludic.nav/native/build.sh | 27 +++++ packages/ludic.nav/native/shim/nav_build.inl | 108 +++++++++++++++++ packages/ludic.nav/native/shim/nav_query.inl | 53 +++++++++ packages/ludic.nav/native/shim/nav_shim.cpp | 50 ++++++++ packages/ludic.nav/package.ludic | 7 ++ packages/ludic.nav/query.ludic | 43 +++++++ packages/ludic.nav/state.ludic | 46 ++++++++ packages/ludic.nav/tests/nav_test.ludic | 110 ++++++++++++++++++ 17 files changed, 587 insertions(+) create mode 100644 packages/ludic.nav/README.md create mode 100644 packages/ludic.nav/build.ludic create mode 100644 packages/ludic.nav/index.ludic create mode 100755 packages/ludic.nav/lib/macos-arm64/libludicnav.dylib create mode 100644 packages/ludic.nav/lib/windows-x64/ludicnav.dll create mode 100644 packages/ludic.nav/lib/windows-x64/ludicnav.lib create mode 100644 packages/ludic.nav/native.ludic create mode 100644 packages/ludic.nav/native/LICENSE-recastnavigation create mode 100755 packages/ludic.nav/native/build.sh create mode 100644 packages/ludic.nav/native/shim/nav_build.inl create mode 100644 packages/ludic.nav/native/shim/nav_query.inl create mode 100644 packages/ludic.nav/native/shim/nav_shim.cpp create mode 100644 packages/ludic.nav/package.ludic create mode 100644 packages/ludic.nav/query.ludic create mode 100644 packages/ludic.nav/state.ludic create mode 100644 packages/ludic.nav/tests/nav_test.ludic diff --git a/packages/README.md b/packages/README.md index 444d193b..44237109 100644 --- a/packages/README.md +++ b/packages/README.md @@ -30,6 +30,7 @@ section. The rules are in [ludic.base](ludic.base/README.md). | [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse | | [ludic.npc](ludic.npc/README.md) | the other people in a place: a routine by the hour, walking round what is in the way, facing a player who comes near, lines as data, a guest's copy | | [ludic.physics](ludic.physics/README.md) | rigid bodies, removable still shapes, queries and buoyancy over Jolt Physics (a native library, phase 16) | +| [ludic.nav](ludic.nav/README.md) | a navmesh per kind of walker and the ways across it, over Recast & Detour (a native library, phase 17) | | [ludic.photo](ludic.photo/README.md) | a camera's photographs: what is in the frame, a grade on size, framing, light and the moment, the roll, its worth, the best of each subject | | [ludic.save](ludic.save/README.md) | versioned save files: a migration chain the game declares, torn writes told apart, a backup, a newer file refused and read-only | | [ludic.settings](ludic.settings/README.md) | a game's settings as data: one store, a fact per change, ranges, a safe set | diff --git a/packages/ludic.nav/README.md b/packages/ludic.nav/README.md new file mode 100644 index 00000000..73528849 --- /dev/null +++ b/packages/ludic.nav/README.md @@ -0,0 +1,56 @@ +# ludic.nav + +A walkable mesh of the world and the ways across it, over +[Recast & Detour](https://github.com/recastnavigation/recastnavigation) (zlib) built here from a +pinned tag (phase 17). Uses `ludic.base` and nothing else. + +```ludic +import "ludic.nav" +``` + +## The rules it keeps + +- **One mesh per kind of walker.** `NAV_PERSON` (0.35 m wide, the hiker's 0.55 m step), + `NAV_LARGE` (an elk, a bear, a horse) and `NAV_SMALL` (a hare, a marmot) each have their own, + because what a hare slips between a bear walks round. A `NavConfig` says what a mesh is built + for: its voxels and its walker's height, radius, step and steepest slope. +- **Built from triangles, or loaded from a bake.** `nav_build` rasterizes triangles - each with an + area byte, 0 not walkable and 1..62 a kind of ground - and marks every cylinder given (a trunk, a + post) as somewhere nothing stands; ground steeper than the slope is cleared whatever its area. A + game bakes its maps once and loads the bytes (`nav_save_file` / `nav_load_file`): a map is never + built at start-up. +- **A path is corners.** `nav_path` answers the way from one point to another as the corners + where it turns, the first the start and the last the end. When the end cannot be reached the + path stops as near it as the mesh allows and `nav_partial` says so - a walker goes there and + gives up, never through a river. Either end off the mesh is `-1`. +- **A kind of ground has a cost** (`nav_area_cost`): a trail cheaper than a meadow, scree dearer. +- **The same question gets the same answer.** Detour is deterministic for the same mesh and the + same points, so co-op's order of dice is untouched by asking it. + +## API + +| | | +| --- | --- | +| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_CORNERS` | the kinds of walker, and a path's most corners (256) | +| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default) | +| `nav_build(kind, v, nv, t, nt, area, cyl, nc, config) -> bool`, `nav_polygons(kind)` | a mesh from triangles and cylinders (`x, y, z, r, h` each) | +| `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go | +| `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres | +| `nav_path(kind, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners | +| `nav_straight(kind, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable | +| `nav_area_cost(kind, area, cost)` | how dear a kind of ground is to cross | + +## Tests + +```bash +ludic test packages/ludic.nav +``` + +A 40 m meadow built by hand: a path round a post, over a river's ford, stopping at the bank of a +river with none, the nearest point, and a saved mesh answering as the built one did. + +## The native library + +`native/build.sh` fetches Recast & Detour v1.6.0, checks its SHA-256, and builds Recast, Detour +and the shim (`native/shim/nav_shim.cpp`) into `lib//` - the same script on the Mac and on +the PC (Git Bash, the LLVM installer's clang). `native/LICENSE-recastnavigation` ships with it. diff --git a/packages/ludic.nav/build.ludic b/packages/ludic.nav/build.ludic new file mode 100644 index 00000000..b92ef905 --- /dev/null +++ b/packages/ludic.nav/build.ludic @@ -0,0 +1,37 @@ +# build.ludic - a kind's mesh from triangles, or from a file a bake wrote +# v: nv points (x, y, z); t: nt triangles (three indices each); area: one byte per triangle, 0 not +# walkable, 1..62 a kind of ground; cyl: nc cylinders (x, y, z, r, h) nothing stands in +export function nav_build(nav_st: mut NavState, kind: int, v: []float, nv: int, t: []int, nt: int, area: []byte, cyl: []float, nc: int, c: NavConfig) -> bool { + if len(v) < nv * 3 or len(t) < nt * 3 or len(area) < nt or len(cyl) < nc * 5 { return false } + let cfg = floats(6) + cfg[0] = c.cell + cfg[1] = c.cell_h + cfg[2] = c.height + cfg[3] = c.radius + cfg[4] = c.climb + cfg[5] = c.slope + return nv_set(nav_st, kind, nvc_build(v, nv, t, nt, area, cyl, nc, cfg)) +} + +# the polygons a kind's mesh has (0: none) +export function nav_polygons(nav_st: NavState, kind: int) -> int { + let h = nv_mesh(nav_st, kind) + if h == null { return 0 } + return nvc_polys(h) +} + +# a kind's mesh written to a file, and read back +export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool { + let h = nv_mesh(nav_st, kind) + if h == null { return false } + let n = nvc_save(h, null, 0) + let buf = buffer(n) + if nvc_save(h, buf, n) != n { return false } + return Fs.write_bytes(path, buf, n) +} +export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool { + if not Fs.exists(path) { return false } + let buf = Fs.read_bytes(path) + if buf == null or len(buf) == 0 { return false } + return nv_set(nav_st, kind, nvc_load(buf, len(buf))) +} diff --git a/packages/ludic.nav/index.ludic b/packages/ludic.nav/index.ludic new file mode 100644 index 00000000..3f380a8d --- /dev/null +++ b/packages/ludic.nav/index.ludic @@ -0,0 +1,10 @@ +# ludic.nav - a navmesh per kind of walker (a person, a large animal, a small one): built from +# triangles and the cylinders nothing stands in, or loaded from the bytes a bake saved; asked for +# the nearest walkable point, a path as corners, and whether a straight line stays walkable. +module ludic_nav uses ludic_base +numbers float +import "ludic.base" +import "native.ludic" +import "state.ludic" +import "build.ludic" +import "query.ludic" diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib new file mode 100755 index 00000000..9d6ee7d3 --- /dev/null +++ b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dd50af00e11ecd47c3c284818f153932b51ed4042be11447f17f44ed9a8e737 +size 159424 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.dll b/packages/ludic.nav/lib/windows-x64/ludicnav.dll new file mode 100644 index 00000000..95d31837 --- /dev/null +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef9b8a4cb718f3c47b0d7e042f9307b605288646f523df84777d6c53db00cf37 +size 333824 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.lib b/packages/ludic.nav/lib/windows-x64/ludicnav.lib new file mode 100644 index 00000000..e8a20a66 --- /dev/null +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.lib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14ea139aeddc712a0c580947ec3d4a90339aa92b54d80b1a8039bb0e88093405 +size 2922 diff --git a/packages/ludic.nav/native.ludic b/packages/ludic.nav/native.ludic new file mode 100644 index 00000000..c53a9a8f --- /dev/null +++ b/packages/ludic.nav/native.ludic @@ -0,0 +1,12 @@ +# native.ludic - the shim's symbols (native/shim/nav_shim.cpp). A navmesh is a handle the package +# keeps; points come back through a float buffer it owns. None of this is exported. +extern function nvc_build(v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, c: pointer) -> pointer = "nav_build" +extern function nvc_save(h: pointer, buf: pointer, cap: int) -> int = "nav_save" +extern function nvc_load(buf: pointer, size: int) -> pointer = "nav_load" +extern function nvc_free(h: pointer) -> void = "nav_free" +extern function nvc_polys(h: pointer) -> int = "nav_polys" +extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: pointer) -> int = "nav_nearest" +extern function nvc_area_cost(h: pointer, area: int, cost: float) -> void = "nav_area_cost" +extern function nvc_path(h: pointer, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path" +extern function nvc_partial(h: pointer) -> int = "nav_partial" +extern function nvc_raycast(h: pointer, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast" diff --git a/packages/ludic.nav/native/LICENSE-recastnavigation b/packages/ludic.nav/native/LICENSE-recastnavigation new file mode 100644 index 00000000..c259ddf6 --- /dev/null +++ b/packages/ludic.nav/native/LICENSE-recastnavigation @@ -0,0 +1,18 @@ +Copyright (c) 2009 Mikko Mononen memon@inside.org + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not +claim that you wrote the original software. If you use this software +in a product, an acknowledgment in the product documentation would be +appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + diff --git a/packages/ludic.nav/native/build.sh b/packages/ludic.nav/native/build.sh new file mode 100755 index 00000000..c63bed78 --- /dev/null +++ b/packages/ludic.nav/native/build.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# builds lib// for ludic.nav: Recast & Detour at a pinned tag, and the shim over them. +# Recast builds the mesh, Detour answers it; DetourTileCache and DetourCrowd come with phases 17.8 +# and 18. Objects go to build/native; a few seconds on eight cores. +set -eu +PKG="$(cd "$(dirname "$0")/.." && pwd)" +. "$PKG/../../tools/native/lib.sh" +RC_TAG=v1.6.0 +RC_SHA=d48ca0121962fa0639502c0f56c4e3ae72f98e55d88727225444f500775c0074 +SRC="$PKG/build/src/recast-$RC_TAG" +native_fetch "$SRC" "https://github.com/recastnavigation/recastnavigation/archive/refs/tags/$RC_TAG.tar.gz" "$RC_SHA" + +OBJ="$PKG/build/native/$(native_target)" +mkdir -p "$OBJ" +CXX="$(native_cxx)" +INC="-I$SRC/Recast/Include -I$SRC/Detour/Include" +FLAGS="$(native_cflags) -std=c++17 -ffp-contract=off -fno-exceptions -fno-rtti -DNDEBUG $INC" +case "$(native_target)" in windows-*) FLAGS="$FLAGS -D_CRT_SECURE_NO_WARNINGS" ;; esac +JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)" + +( cd "$SRC" && ls Recast/Source/*.cpp Detour/Source/*.cpp ) | while read -r f; do + o="$OBJ/$(basename "$f" .cpp).o" + [ "$o" -nt "$SRC/$f" ] || echo "$f $o" +done | xargs -P "$JOBS" -n 2 sh -c '"$0" '"$FLAGS"' -c "'"$SRC"'/$1" -o "$2"' "$CXX" +"$CXX" $FLAGS -I"$PKG/native/shim" -c "$PKG/native/shim/nav_shim.cpp" -o "$OBJ/nav_shim.o" + +native_link "$PKG" ludicnav "$OBJ"/*.o diff --git a/packages/ludic.nav/native/shim/nav_build.inl b/packages/ludic.nav/native/shim/nav_build.inl new file mode 100644 index 00000000..efda5577 --- /dev/null +++ b/packages/ludic.nav/native/shim/nav_build.inl @@ -0,0 +1,108 @@ +// nav_build.inl - a navmesh from triangles: Recast's solo pipeline (rasterize, filter, erode, +// regions, contours, polygons, detail) and one Detour tile. c[] is the configuration: +// cell size, cell height, agent height, agent radius, max climb, max slope (degrees). + +// cylinders (x, y, z, r, h each) no agent may stand in: trunks, posts, tents +static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const float *cyl, int nc) { + for (int i = 0; i < nc; ++i) { + float pos[3] = {cyl[i * 5], cyl[i * 5 + 1], cyl[i * 5 + 2]}; + rcMarkCylinderArea(&ctx, pos, cyl[i * 5 + 3], cyl[i * 5 + 4], RC_NULL_AREA, chf); + } +} + +// v: nv points (x, y, z); t: nt triangles; area: one per triangle, 0 not walkable, 1..62 a kind of +// ground (its cost is the filter's); the slope clears what is too steep whatever its area +NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const unsigned char *area, + const float *cyl, int nc, const float *c) { + rcContext ctx(false); + rcConfig cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.cs = c[0]; + cfg.ch = c[1]; + cfg.walkableSlopeAngle = c[5]; + cfg.walkableHeight = (int)ceilf(c[2] / cfg.ch); + cfg.walkableClimb = (int)floorf(c[4] / cfg.ch); + cfg.walkableRadius = (int)ceilf(c[3] / cfg.cs); + cfg.maxEdgeLen = (int)(12.0f / cfg.cs); + cfg.maxSimplificationError = 1.3f; + cfg.minRegionArea = 8 * 8; + cfg.mergeRegionArea = 20 * 20; + cfg.maxVertsPerPoly = 6; + cfg.detailSampleDist = cfg.cs * 6.0f; + cfg.detailSampleMaxError = cfg.ch; + rcCalcBounds(v, nv, cfg.bmin, cfg.bmax); + rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height); + + std::vector areas(area, area + nt); + rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, v, nv, t, nt, areas.data()); + rcHeightfield *hf = rcAllocHeightfield(); + rcCompactHeightfield *chf = rcAllocCompactHeightfield(); + rcContourSet *cs = rcAllocContourSet(); + rcPolyMesh *pm = rcAllocPolyMesh(); + rcPolyMeshDetail *dm = rcAllocPolyMeshDetail(); + bool ok = hf && chf && cs && pm && dm && + rcCreateHeightfield(&ctx, *hf, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch) && + rcRasterizeTriangles(&ctx, v, nv, t, areas.data(), nt, *hf, cfg.walkableClimb); + if (ok) { + rcFilterLowHangingWalkableObstacles(&ctx, cfg.walkableClimb, *hf); + rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf); + rcFilterWalkableLowHeightSpans(&ctx, cfg.walkableHeight, *hf); + ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf); + } + if (ok) { + nav_mark_cylinders(ctx, *chf, cyl, nc); + ok = rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf) && rcBuildDistanceField(&ctx, *chf) && + rcBuildRegions(&ctx, *chf, 0, cfg.minRegionArea, cfg.mergeRegionArea) && + rcBuildContours(&ctx, *chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *cs) && + rcBuildPolyMesh(&ctx, *cs, cfg.maxVertsPerPoly, *pm) && + rcBuildPolyMeshDetail(&ctx, *pm, *chf, cfg.detailSampleDist, cfg.detailSampleMaxError, *dm); + } + unsigned char *data = nullptr; + int size = 0; + if (ok) { + for (int i = 0; i < pm->npolys; ++i) pm->flags[i] = pm->areas[i] ? 1 : 0; + dtNavMeshCreateParams p; + memset(&p, 0, sizeof(p)); + p.verts = pm->verts; p.vertCount = pm->nverts; p.polys = pm->polys; p.polyAreas = pm->areas; + p.polyFlags = pm->flags; p.polyCount = pm->npolys; p.nvp = pm->nvp; + p.detailMeshes = dm->meshes; p.detailVerts = dm->verts; p.detailVertsCount = dm->nverts; + p.detailTris = dm->tris; p.detailTriCount = dm->ntris; + p.walkableHeight = c[2]; p.walkableRadius = c[3]; p.walkableClimb = c[4]; + rcVcopy(p.bmin, pm->bmin); rcVcopy(p.bmax, pm->bmax); + p.cs = cfg.cs; p.ch = cfg.ch; p.buildBvTree = true; + ok = dtCreateNavMeshData(&p, &data, &size); + } + rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm); + if (!ok) return nullptr; + return nav_from(data, size); +} + +// the tile's bytes, to write to a file: first the size (buf null), then the copy +NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) { + Nav *n = static_cast