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:
commit
e397e4eee9
18 changed files with 236 additions and 81 deletions
|
|
@ -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 |
|
||||||
|
|
|
||||||
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.dll
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.dll
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.lib
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.lib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
44
packages/ludic.physics/tests/hull_shape_test.ludic
Normal file
44
packages/ludic.physics/tests/hull_shape_test.ludic
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
39
packages/ludic.vehicles/keep.ludic
Normal file
39
packages/ludic.vehicles/keep.ludic
Normal 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 }
|
||||||
|
}
|
||||||
30
packages/ludic.vehicles/legs.ludic
Normal file
30
packages/ludic.vehicles/legs.ludic
Normal 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 { }
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue