355 lines
17 KiB
Text
355 lines
17 KiB
Text
# character_test.ludic - a body on a fake world: flat meadow, a 45 cm deck, a gentle rise and a cliff,
|
|
# a boulder with a step on top, and a lake that shelves. The step against the slope at every frame
|
|
# rate, wading into a swim and out, the dive and the breath, the jump, sitting, holds, the safe
|
|
# spot, riding and the camera. The walk is a real Jolt walker (ludic.physics) on the same world.
|
|
import "ludic.character"
|
|
import "ludic.base"
|
|
import "ludic.physics"
|
|
program CharacterTest {
|
|
numbers float
|
|
function wpose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
|
|
let p = new PhysWalk
|
|
if not phys_walker_pose(physics_st, id, p) { return null }
|
|
return p
|
|
}
|
|
|
|
# the world: x in 4..8 is a deck 45 cm up; z < -10 a cliff; z > 10 a gentle rise; x > 20 the lake
|
|
function fk_height(x: float, z: float) -> float {
|
|
if x > 4.0 and x < 8.0 and z > -2.0 and z < 2.0 { return 0.45 }
|
|
return fk_land(x, z)
|
|
}
|
|
function fk_land(x: float, z: float) -> float {
|
|
if x > 20.0 { return -(x - 20.0) * 0.5 }
|
|
if z < -10.0 { return (-10.0 - z) * 2.0 }
|
|
if z > 10.0 { return (z - 10.0) * 0.3 }
|
|
return 0.0
|
|
}
|
|
function fk_water(x: float, z: float) -> float {
|
|
if x > 19.0 { return 0.0 }
|
|
return -10000.0
|
|
}
|
|
# a boulder at (0, 6), a metre round and 40 cm high, and a pillar at (-6, 0)
|
|
state CharacterTestState {
|
|
px: float = 0.0
|
|
pz: float = 0.0
|
|
riding: bool = false
|
|
rides: int = 0
|
|
sneaking: bool = false
|
|
pace: float = 1.0
|
|
mz: float = 0.0
|
|
boots: float = 1.05
|
|
walker: int = -1
|
|
wx: float = 0.0
|
|
wy: float = 0.0
|
|
wz: float = 0.0
|
|
}
|
|
function fk_ring(character_test_st: mut CharacterTestState, x: float, z: float, cx: float, cz: float, r: float) -> bool {
|
|
let dx = x - cx
|
|
let dz = z - cz
|
|
let d = Math.sqrt(dx * dx + dz * dz)
|
|
if d >= r { return false }
|
|
let k = r / Math.max(d, 0.001)
|
|
character_test_st.px = cx + dx * k
|
|
character_test_st.pz = cz + dz * k
|
|
return true
|
|
}
|
|
function fk_push(character_test_st: mut CharacterTestState, x: float, z: float, r: float, y0: float, y1: float) -> bool {
|
|
if y0 < 0.4 and fk_ring(character_test_st, x, z, 0.0, 6.0, 1.0 + r) { return true }
|
|
return fk_ring(character_test_st, x, z, -6.0, 0.0, 0.5 + r)
|
|
}
|
|
function fk_px(character_test_st: CharacterTestState) -> float { return character_test_st.px }
|
|
function fk_pz(character_test_st: CharacterTestState) -> float { return character_test_st.pz }
|
|
function fk_top(x: float, z: float, r: float, from_y: float, step: float, base: float) -> float {
|
|
let dx = x
|
|
let dz = z - 6.0
|
|
if dx * dx + dz * dz < 1.0 and 0.4 - from_y <= step { return 0.4 }
|
|
return base
|
|
}
|
|
|
|
let eyes: bool = true
|
|
function fk_riding(character_test_st: CharacterTestState) -> bool { return character_test_st.riding }
|
|
function fk_ride(character_test_st: mut CharacterTestState, ix: float, iz: float, run: bool, dt: float) -> void { character_test_st.rides += 1 }
|
|
function fk_sneak(character_test_st: CharacterTestState) -> bool { return character_test_st.sneaking }
|
|
function fk_pace(character_test_st: CharacterTestState) -> float { return character_test_st.pace }
|
|
function fk_eyes() -> bool { return eyes }
|
|
|
|
function fk_move_z(character_test_st: CharacterTestState) -> float { return character_test_st.mz }
|
|
|
|
# the same world in Jolt: the land as a heightfield, the deck a box, the boulder a 40 cm drum and
|
|
# the pillar a post
|
|
function fk_jolt(physics_st: mut PhysicsState) -> int {
|
|
phys_open(physics_st, 256, 1)
|
|
let n = 128
|
|
let h = floats(n * n)
|
|
for j in 0 .. n { for i in 0 .. n { h[j * n + i] = fk_land(float(i) - 40.0, float(j) - 60.0) } }
|
|
phys_ground_add(physics_st, phys_heightfield(physics_st, h, n, -40.0, -60.0, 1.0))
|
|
phys_static_add(physics_st, phys_offset(physics_st, phys_box(physics_st, 2.0, 0.225, 2.0), 0.0, 0.225, 0.0, 0.0), 6.0, 0.0, 0.0, 0.0)
|
|
phys_static_add(physics_st, phys_offset(physics_st, phys_cylinder(physics_st, 0.2, 1.0), 0.0, 0.2, 0.0, 0.0), 0.0, 0.0, 6.0, 0.0)
|
|
phys_static_add(physics_st, phys_offset(physics_st, phys_cylinder(physics_st, 3.0, 0.5), 0.0, 3.0, 0.0, 0.0), -6.0, 0.0, 0.0, 0.0)
|
|
phys_settle(physics_st)
|
|
return phys_walker_add(physics_st, 0.35, CHAR_BODY, 0.0, 0.0, 0.0, 50.0, 70.0)
|
|
}
|
|
function fk_walk(physics_st: mut PhysicsState, character_test_st: mut CharacterTestState, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, slope: float, dt: float) -> int {
|
|
if character_test_st.walker < 0 { character_test_st.walker = fk_jolt(physics_st) }
|
|
let g = phys_walker_move(physics_st, character_test_st.walker, x, y, z, vx, vz, jump, step, slope, dt)
|
|
let p = wpose(physics_st, character_test_st.walker)
|
|
character_test_st.wx = p.x
|
|
character_test_st.wy = p.y
|
|
character_test_st.wz = p.z
|
|
return g
|
|
}
|
|
function fk_wx(character_test_st: CharacterTestState) -> float { return character_test_st.wx }
|
|
function fk_wy(character_test_st: CharacterTestState) -> float { return character_test_st.wy }
|
|
function fk_wz(character_test_st: CharacterTestState) -> float { return character_test_st.wz }
|
|
function fk_boots(character_test_st: CharacterTestState) -> float { return character_test_st.boots }
|
|
|
|
bind CharacterGround { height: fn fk_height, water: fn fk_water, push: fn fk_push, pushed_x: fn fk_px, pushed_z: fn fk_pz, top_at: fn fk_top, walk: fn fk_walk, walked_x: fn fk_wx, walked_y: fn fk_wy, walked_z: fn fk_wz, slope_limit: fn fk_boots }
|
|
bind CharacterBody { riding: fn fk_riding, ride: fn fk_ride, sneaking: fn fk_sneak, pace: fn fk_pace, eyes_ok: fn fk_eyes }
|
|
bind CharacterInput { move_z: fn fk_move_z }
|
|
|
|
function fresh(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
|
|
char_reset(character_st)
|
|
char_release(character_st, CHAR_HOLD_SCREEN)
|
|
char_teleport(character_st, x, z, yaw)
|
|
}
|
|
function facts(character_st: CharacterState) -> []CharacterFact { return q_drain(char_facts(character_st)) }
|
|
function count(fs: []CharacterFact, what: int) -> int {
|
|
var n = 0
|
|
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
|
|
return n
|
|
}
|
|
# walk the stick straight ahead (the camera looks where the body faces) for `secs`
|
|
function walk(character_st: mut CharacterState, secs: float, hz: float, run: bool) -> void {
|
|
let n = int(secs * hz)
|
|
for i in 0 .. n { char_update(character_st, 0.0, 1.0, run, false, 0.0, 0.0, 1.0 / hz) }
|
|
}
|
|
# yaw 0 faces -z; the heading of a walk toward +x is -pi/2
|
|
const EAST: float = -1.5707963
|
|
const SOUTH: float = 3.14159265
|
|
|
|
test "a 45 cm deck is a step at 60 Hz and at 240" (character_st: mut CharacterState) {
|
|
fresh(character_st, 1.0, 0.0, EAST)
|
|
walk(character_st, 2.0, 60.0, false)
|
|
expect(body_x(character_st) > 5.0)
|
|
expect_near(body_y(character_st), 0.45, 0.01)
|
|
fresh(character_st, 1.0, 0.0, EAST)
|
|
walk(character_st, 2.0, 240.0, false)
|
|
expect(body_x(character_st) > 5.0)
|
|
expect_near(body_y(character_st), 0.45, 0.01)
|
|
expect_eq(count(facts(character_st), CHAR_TOO_STEEP), 0)
|
|
}
|
|
|
|
test "a cliff is refused at the same place whatever the frame rate, and said once" (character_st: mut CharacterState) {
|
|
fresh(character_st, 0.0, -8.0, 0.0)
|
|
walk(character_st, 3.0, 30.0, false)
|
|
let at30 = body_z(character_st)
|
|
expect(body_steep(character_st) > 0.0)
|
|
expect_eq(count(facts(character_st), CHAR_TOO_STEEP), 1)
|
|
fresh(character_st, 0.0, -8.0, 0.0)
|
|
walk(character_st, 3.0, 240.0, false)
|
|
expect_near(body_z(character_st), at30, 0.25)
|
|
expect(body_z(character_st) > -10.0)
|
|
expect(body_y(character_st) < 0.3)
|
|
}
|
|
|
|
test "a gentle rise is walked up" (character_st: mut CharacterState) {
|
|
fresh(character_st, 0.0, 8.0, SOUTH)
|
|
walk(character_st, 3.0, 60.0, false)
|
|
expect(body_z(character_st) > 14.0)
|
|
expect(body_y(character_st) > 1.0)
|
|
}
|
|
|
|
test "the boots set the limit: a rise they cannot take is refused" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
|
|
character_test_st.boots = 0.2
|
|
fresh(character_st, 0.0, 8.0, SOUTH)
|
|
walk(character_st, 3.0, 60.0, false)
|
|
expect(body_z(character_st) < 11.5)
|
|
expect(body_y(character_st) < 0.45)
|
|
expect(count(facts(character_st), CHAR_TOO_STEEP) >= 1)
|
|
}
|
|
|
|
test "the walk reaches the walk speed, the run the run speed, a sneak its own" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
|
|
fresh(character_st, 0.0, 0.0, SOUTH)
|
|
walk(character_st, 2.0, 60.0, false)
|
|
expect_near(body_speed(character_st), 3.2, 0.05)
|
|
fresh(character_st, 0.0, 0.0, SOUTH)
|
|
walk(character_st, 2.0, 60.0, true)
|
|
expect_near(body_speed(character_st), 6.8, 0.05)
|
|
character_test_st.pace = 0.5
|
|
fresh(character_st, 0.0, 0.0, SOUTH)
|
|
walk(character_st, 2.0, 60.0, false)
|
|
expect_near(body_speed(character_st), 1.6, 0.05)
|
|
character_test_st.pace = 1.0
|
|
character_test_st.sneaking = true
|
|
fresh(character_st, 0.0, 0.0, SOUTH)
|
|
walk(character_st, 2.0, 60.0, true)
|
|
expect_near(body_speed(character_st), 1.3, 0.05)
|
|
}
|
|
|
|
test "a knee-high boulder is a step onto its top, and a post is a wall" (character_st: mut CharacterState) {
|
|
fresh(character_st, 0.0, 3.0, SOUTH)
|
|
walk(character_st, 1.3, 60.0, false)
|
|
expect(body_z(character_st) > 5.2)
|
|
expect_near(body_y(character_st), 0.4, 0.03)
|
|
fresh(character_st, -3.0, 0.0, -EAST)
|
|
walk(character_st, 2.0, 60.0, false)
|
|
expect(body_x(character_st) > -5.2)
|
|
expect_near(char_stand_at(0.0, 6.0, 0.0), 0.4, 0.001)
|
|
expect_near(char_stand_at(3.0, 6.0, 0.0), 0.0, 0.001)
|
|
# and the world's own ground never includes it
|
|
expect_near(fk_height(0.0, 6.0), 0.0, 0.001)
|
|
}
|
|
|
|
test "wading shelves into a swim, and the shallows give the ground back" (character_st: mut CharacterState) {
|
|
fresh(character_st, 18.0, 0.0, EAST)
|
|
walk(character_st, 3.0, 60.0, false)
|
|
let fs = facts(character_st)
|
|
expect(body_swimming(character_st))
|
|
expect_eq(count(fs, CHAR_STARTED_SWIMMING), 1)
|
|
expect_near(body_y(character_st), -0.30, 0.06)
|
|
expect(body_breath(character_st) > 40.0)
|
|
char_face(character_st, -EAST)
|
|
char_look(character_st, -EAST, 0.0)
|
|
walk(character_st, 8.0, 60.0, false)
|
|
expect(not body_swimming(character_st))
|
|
expect_eq(count(facts(character_st), CHAR_LEFT_WATER), 1)
|
|
}
|
|
|
|
test "the jump held dives, the air runs out, and the body surfaces" (character_st: mut CharacterState) {
|
|
fresh(character_st, 30.0, 0.0, 0.0)
|
|
char_resume_swim(character_st, false, 0.0, 2.0)
|
|
for i in 0 .. 600 { char_update(character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) }
|
|
let fs = facts(character_st)
|
|
expect(count(fs, CHAR_OUT_OF_AIR) > 0)
|
|
for i in 0 .. 300 { char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
|
|
expect(body_depth(character_st) < 0.05)
|
|
expect(body_breath(character_st) > 10.0)
|
|
}
|
|
|
|
test "a jump leaves the ground, falls, lands, and says both" (character_st: mut CharacterState) {
|
|
fresh(character_st, 0.0, 0.0, 0.0)
|
|
char_update(character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
|
|
expect(body_airborne(character_st))
|
|
var guard = 0
|
|
while body_airborne(character_st) and guard < 200 {
|
|
char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0)
|
|
guard += 1
|
|
}
|
|
let fs = facts(character_st)
|
|
expect(not body_airborne(character_st))
|
|
expect_eq(count(fs, CHAR_JUMPED), 1)
|
|
expect_eq(count(fs, CHAR_LANDED), 1)
|
|
expect(guard > 30)
|
|
char_allow_run(character_st, false)
|
|
char_update(character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
|
|
expect(not body_airborne(character_st))
|
|
}
|
|
|
|
test "a knock-back carries the body off its feet, and a post stops it" (character_st: mut CharacterState) {
|
|
fresh(character_st, 0.0, -2.0, 0.0)
|
|
char_knock(character_st, 0.0, 5.0, 3.0)
|
|
char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0)
|
|
expect(body_airborne(character_st))
|
|
for i in 0 .. 120 { char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
|
|
expect(not body_airborne(character_st))
|
|
expect(body_z(character_st) > 0.5)
|
|
fresh(character_st, -3.5, 0.0, 0.0)
|
|
char_knock(character_st, -8.0, 0.0, 2.0)
|
|
for i in 0 .. 120 { char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
|
|
expect(body_x(character_st) > -5.2)
|
|
}
|
|
|
|
test "the safe spot clears a collider, keeps out of deep water, and allows a wade when asked" (character_st: mut CharacterState) {
|
|
expect(char_safe_spot(character_st, -6.0, 0.0, 0.0, 0.0))
|
|
let dx = body_safe_x(character_st) + 6.0
|
|
let dz = body_safe_z(character_st)
|
|
expect(dx * dx + dz * dz > 0.81)
|
|
expect(not char_safe_spot(character_st, 40.0, 0.0, 0.0, 0.0))
|
|
expect(char_safe_spot(character_st, 20.5, 0.0, 0.0, 1.25))
|
|
expect(char_safe_spot(character_st, 15.0, 0.0, 0.0, 0.0))
|
|
expect_near(body_safe_x(character_st), 15.0, 0.001)
|
|
}
|
|
|
|
test "sitting where you stand, and the refusals" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
|
|
fresh(character_st, 0.0, 0.0, 0.0)
|
|
expect_eq(char_sit_here(character_st), CHAR_SAT)
|
|
for i in 0 .. 30 { char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
|
|
expect(body_sitting(character_st))
|
|
expect_near(body_sit_t(character_st), 1.0, 0.01)
|
|
expect_eq(char_sit_here(character_st), CHAR_STOOD)
|
|
expect(not body_sitting(character_st))
|
|
fresh(character_st, 0.0, -14.0, 0.0)
|
|
expect_eq(char_sit_here(character_st), CHAR_NOT_STEEP)
|
|
fresh(character_st, 24.0, 0.0, 0.0)
|
|
char_resume_swim(character_st, false, 0.0, 10.0)
|
|
expect_eq(char_sit_here(character_st), CHAR_NOT_WET)
|
|
character_test_st.riding = true
|
|
fresh(character_st, 0.0, 0.0, 0.0)
|
|
expect_eq(char_sit_here(character_st), CHAR_NOT_RIDING)
|
|
character_test_st.riding = false
|
|
}
|
|
|
|
test "a seat against the boulder holds the body still: the walker never pushes it off the seat" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
|
|
fresh(character_st, 0.0, 4.5, 0.0)
|
|
char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0)
|
|
char_sit(character_st, 0.0, body_y(character_st) + 0.45, 5.7, 0.0, CHAR_SIT_SEAT, 0.45)
|
|
for i in 0 .. 90 { char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
|
|
var most = 0.0
|
|
for i in 0 .. 120 {
|
|
let x0 = body_x(character_st)
|
|
let y0 = body_y(character_st)
|
|
let z0 = body_z(character_st)
|
|
char_update(character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0)
|
|
most = Math.max(most, Math.abs(body_x(character_st) - x0) + Math.abs(body_y(character_st) - y0) + Math.abs(body_z(character_st) - z0))
|
|
}
|
|
expect(body_sitting(character_st))
|
|
expect(most < 0.0005)
|
|
expect_near(body_z(character_st), 5.7, 0.01)
|
|
}
|
|
|
|
test "a hold stops the stick, and only its own reason lets it go" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
|
|
fresh(character_st, 0.0, 0.0, 0.0)
|
|
character_test_st.mz = 1.0
|
|
char_hold(character_st, CHAR_HOLD_STAGE)
|
|
char_hold(character_st, CHAR_HOLD_SCREEN)
|
|
for i in 0 .. 60 { char_tick(character_st, 1.0 / 60.0) }
|
|
expect_near(body_z(character_st), 0.0, 0.001)
|
|
char_release(character_st, CHAR_HOLD_SCREEN)
|
|
char_release(character_st, CHAR_HOLD_SCREEN)
|
|
expect(body_held(character_st))
|
|
char_release(character_st, CHAR_HOLD_STAGE)
|
|
for i in 0 .. 60 { char_tick(character_st, 1.0 / 60.0) }
|
|
expect(body_z(character_st) < -1.0)
|
|
character_test_st.mz = 0.0
|
|
}
|
|
|
|
test "riding hands the stick over and leaves the body where the ride puts it" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
|
|
fresh(character_st, 0.0, 0.0, 0.0)
|
|
character_test_st.riding = true
|
|
character_test_st.rides = 0
|
|
char_board(character_st, 3.0, 3.0, 0.0)
|
|
for i in 0 .. 10 { char_update(character_st, 0.0, 1.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
|
|
expect_eq(character_test_st.rides, 10)
|
|
char_ride_at(character_st, 4.0, 1.2, 3.0, 0.0, 2.0)
|
|
expect_near(body_x(character_st), 4.0, 0.001)
|
|
expect_near(body_y(character_st), 1.2, 0.001)
|
|
character_test_st.riding = false
|
|
char_step_off(character_st, 5.0, 0.0)
|
|
expect_near(body_y(character_st), 0.45, 0.001)
|
|
}
|
|
|
|
test "the orbit sits behind and above the ground; the eyes at head height, refused when sat" (character_st: mut CharacterState) {
|
|
fresh(character_st, 0.0, 0.0, 0.0)
|
|
char_camera(character_st, 1.0)
|
|
expect(body_eye_z(character_st) > 1.0)
|
|
expect(body_eye_y(character_st) > 0.55)
|
|
expect(not body_eyes_now(character_st))
|
|
expect_eq(char_fps_toggle(character_st), CHAR_EYES_ON)
|
|
char_camera(character_st, 1.0)
|
|
expect(body_eyes_now(character_st))
|
|
expect_near(body_eye_y(character_st), 1.62, 0.05)
|
|
expect_eq(char_fps_toggle(character_st), CHAR_EYES_OFF)
|
|
char_sit(character_st, 0.0, 0.45, 0.0, 0.0, CHAR_SIT_SEAT, 0.45)
|
|
expect_eq(char_fps_toggle(character_st), CHAR_EYES_SITTING)
|
|
}
|
|
}
|