merge lang/horse-walker: the horse on legs (VehicleLegs), convex and scaled shapes in ludic.physics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 15:24:07 +03:00
commit e397e4eee9
18 changed files with 236 additions and 81 deletions

View file

@ -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_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_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_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 | | `phys_settle(st)` | after adding many still things, before the first query |

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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_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_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_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_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_mesh(v: pointer, nv: int, tri: pointer, nt: int) -> pointer = "jph_shape_mesh"
extern function jph_shape_free(s: pointer) -> void = "jph_shape_free" extern function jph_shape_free(s: pointer) -> void = "jph_shape_free"

View file

@ -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()); 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<Vec3> 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<Shape *>(s), Vec3::sReplicate(k)).Create());
}
// a shape moved and turned about y, inside its body // 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) { 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<Shape *>(s)).Create()); return keep(RotatedTranslatedShapeSettings(Vec3(x, y, z), yaw_q(yaw), static_cast<Shape *>(s)).Create());

View file

@ -16,6 +16,8 @@
#include <Jolt/Physics/Collision/Shape/HeightFieldShape.h> #include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h> #include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h> #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
#include <Jolt/Physics/Collision/Shape/ScaledShape.h>
#include <Jolt/Physics/Collision/RayCast.h> #include <Jolt/Physics/Collision/RayCast.h>
#include <Jolt/Physics/Collision/CastResult.h> #include <Jolt/Physics/Collision/CastResult.h>
#include <Jolt/Physics/Collision/ShapeCast.h> #include <Jolt/Physics/Collision/ShapeCast.h>

View file

@ -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 # 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)) } 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) # 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 { 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) let s = ph_shape(physics_st, shape)

View file

@ -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)
}
}

View file

@ -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 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 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. 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 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`. - 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, `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. 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 ```ludic
export port VehicleWorld { export port VehicleWorld {
ground: fn(float, float) -> float # required ground: fn(float, float) -> float # required
water: fn(float, float) -> float # the surface (unbound: never wet) 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 swell: fn(float, float, float, float) -> float # (x, z, fx, fz): a stroke's share lost
wind: fn() -> float # 0..1 wind: fn() -> float # 0..1
wind_yaw: fn() -> float # the yaw the wind pushes toward wind_yaw: fn() -> float # the yaw the wind pushes toward

View file

@ -20,12 +20,14 @@ function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw:
return v return v
} }
# a boat's hull put where the vehicle now is, at rest # 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) } } 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 { 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) VehicleWorld.gone(v)
} }
@ -73,40 +75,3 @@ export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> vo
} }
vehicles_st.ve_list = keep 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 }
}

View file

@ -1,5 +1,5 @@
# horse.ludic - a horse under the stick: a walk, a canter on stamina, hay for every metre; it stops at # horse.ludic - a horse under the stick: a walk, a canter on stamina, hay for every metre; it will
# water, at a rise it cannot take and at a still thing in its way # 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_WALK: float = 4.5
const VE_HORSE_GALLOP: float = 11.0 const VE_HORSE_GALLOP: float = 11.0
const VE_HORSE_ACC: float = 4.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 } if v.speed < 0.0 { rate = -rate }
v.yaw = v.yaw - ix * VE_HORSE_TURN * (dt * 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 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) let nh = VehicleWorld.ground(v.x + fx * d, v.z + fz * d)
ve_stamina(v, d, dt) if nh < VehicleWorld.water(v.x + fx * d, v.z + fz * d) + 0.4 {
}
# 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 {
v.speed = 0.0 v.speed = 0.0
return return
} }
v.x = nx ve_legs_walk(vehicles_st, v, fx * v.speed, fz * v.speed, dt)
v.z = nz ve_stamina(v, d, dt)
v.y = nh }
if Math.abs(d) > 0.0 { ve_say(vehicles_st, VEHICLE_MOVED, v, Math.abs(d)) }
# 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 # a gallop spends stamina and a walk gives it back; every metre costs hay

View file

@ -9,7 +9,9 @@ import "ports.ludic"
import "state.ludic" import "state.ludic"
import "places.ludic" import "places.ludic"
import "call.ludic" import "call.ludic"
import "keep.ludic"
import "mount.ludic" import "mount.ludic"
import "ride.ludic" import "ride.ludic"
import "horse.ludic" import "horse.ludic"
import "legs.ludic"
import "system.ludic" import "system.ludic"

View file

@ -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 }
}

View file

@ -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 { }

View file

@ -4,7 +4,6 @@
export port VehicleWorld { export port VehicleWorld {
ground: fn(float, float) -> float ground: fn(float, float) -> float
water: fn(float, float) -> float = fn ve__dry # the surface over a point 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 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: fn() -> float = fn ve__zero # 0..1
wind_yaw: fn() -> float = fn ve__zero # the yaw the wind pushes toward 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_rail_yaw: float = 0.0
ve_have_dock: bool = false ve_have_dock: bool = false
ve_swell_said: float = 0.0 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<VehicleFact> = ve_fact_q__new() ve_fact_q: Queue<VehicleFact> = ve_fact_q__new()
} }
function ve__dry(x: float, z: float) -> float { return -10000.0 } 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__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 }
function ve__zero() -> float { return 0.0 } function ve__zero() -> float { return 0.0 }
function ve__next(vehicles_st: mut VehiclesState) -> int { function ve__next(vehicles_st: mut VehiclesState) -> int {

View file

@ -3,7 +3,10 @@
# read back and pushed by the wind in vehicles_tick, which the game calls where the world is run # 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 { export function vehicles_reset(vehicles_st: mut VehiclesState) -> void {
if vehicles_st.ve_list != null { 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_list = new []Vehicle
vehicles_st.ve_cur = null vehicles_st.ve_cur = null

View file

@ -1,8 +1,8 @@
# vehicles_test.ludic - a fake shore: land at x < 10, a lake past it deepening a metre a metre, a # 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 # 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, # 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 # getting off near a shore and far from one, a player leaving, and the save. The boat's hull and the
# real ludic.physics body on the same lake. # horse's legs are real ludic.physics bodies on the same ground, the trunk a post in it.
import "ludic.vehicles" import "ludic.vehicles"
import "ludic.base" import "ludic.base"
import "ludic.physics" import "ludic.physics"
@ -17,7 +17,6 @@ program VehiclesTest {
if x > 9.5 { return 0.0 } if x > 9.5 { return 0.0 }
return -10000.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 { state VehiclesTestState {
swell: float = 0.0 swell: float = 0.0
wind: float = 0.0 wind: float = 0.0
@ -38,6 +37,11 @@ program VehiclesTest {
hyaw: float = 0.0 hyaw: float = 0.0
hspeed: float = 0.0 hspeed: float = 0.0
hulls: int = 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 { 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 } if fx > 0.0 { return vehicles_test_st.swell }
@ -79,11 +83,28 @@ program VehiclesTest {
function fk_lake(physics_st: mut PhysicsState) -> void { function fk_lake(physics_st: mut PhysicsState) -> void {
if phys_is_open(physics_st) { return } if phys_is_open(physics_st) { return }
phys_open(physics_st, 64, 1) phys_open(physics_st, 64, 1)
let n = 96 let n = 160
let h = floats(n * n) 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) } } 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, -48.0, 1.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 { 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) fk_lake(physics_st)
if vehicles_test_st.hull >= 0 and vehicles_test_st.hull_nid == nid { 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 } 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 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 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 } 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 const EAST: float = -1.5707963
@ -207,7 +229,7 @@ program VehiclesTest {
expect(vehicle_in_place(vehicles_st, VEHICLE_HORSE, 0) == null) 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) fresh(vehicles_st, vehicles_test_st)
vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0)
let h = vehicle_find(vehicles_st, VEHICLE_HORSE) let h = vehicle_find(vehicles_st, VEHICLE_HORSE)
@ -221,10 +243,14 @@ program VehiclesTest {
expect(h.z < -15.0) expect(h.z < -15.0)
expect_near(vehicles_test_st.rz, h.z, 0.001) expect_near(vehicles_test_st.rz, h.z, 0.001)
expect_near(vehicles_test_st.ry, 1.35, 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) ride(physics_st, vehicles_st, 0.0, 1.0, true, 6.0)
expect(h.z > -60.0 + 1.7) expect(h.z > -59.0 - 0.85)
expect_near(h.speed, 0.0, 0.001) 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 # and the lake to the east does too
vehicle_drop(vehicles_st) vehicle_drop(vehicles_st)
h.x = 7.0 h.x = 7.0
@ -233,6 +259,11 @@ program VehiclesTest {
vehicle_mount(vehicles_st, h) vehicle_mount(vehicles_st, h)
ride(physics_st, 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) 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) { 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) {