# 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. import "ludic.character" import "ludic.base" program CharacterTest { numbers float # 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 > 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 } if x > 4.0 and x < 8.0 and z > -2.0 and z < 2.0 { return 0.45 } 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 eyes: bool = true mz: float = 0.0 } function fk_ring(charactertest_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) charactertest_st.px = cx + dx * k charactertest_st.pz = cz + dz * k return true } function fk_push(charactertest_st: mut CharactertestState, x: float, z: float, r: float, y0: float, y1: float) -> bool { if y0 < 0.4 and fk_ring(charactertest_st, x, z, 0.0, 6.0, 1.0 + r) { return true } return fk_ring(charactertest_st, x, z, -6.0, 0.0, 0.5 + r) } function fk_px(charactertest_st: CharactertestState) -> float { return charactertest_st.px } function fk_pz(charactertest_st: CharactertestState) -> float { return charactertest_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 } function fk_riding(charactertest_st: CharactertestState) -> bool { return charactertest_st.riding } function fk_ride(charactertest_st: mut CharactertestState, ix: float, iz: float, run: bool, dt: float) -> void { charactertest_st.rides += 1 } function fk_sneak(charactertest_st: CharactertestState) -> bool { return charactertest_st.sneaking } function fk_pace(charactertest_st: CharactertestState) -> float { return charactertest_st.pace } function fk_eyes(charactertest_st: CharactertestState) -> bool { return charactertest_st.eyes } function fk_move_z(charactertest_st: CharactertestState) -> float { return charactertest_st.mz } 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 } 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(base_st: mut BaseState, character_st: mut CharacterState, x: float, z: float, yaw: float) -> void { char_reset(base_st, character_st) char_release(character_st, CHAR_HOLD_SCREEN) char_teleport(character_st, x, z, yaw) } function facts(base_st: mut BaseState, character_st: mut CharacterState) -> []CharacterFact { return q_drain(base_st, char_facts(base_st, 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(base_st: mut BaseState, character_st: mut CharacterState, secs: float, hz: float, run: bool) -> void { let n = int(secs * hz) for i in 0 .. n { char_update(base_st, 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" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, character_st, 1.0, 0.0, EAST) walk(base_st, 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(base_st, character_st, 1.0, 0.0, EAST) walk(base_st, 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(base_st, character_st), CHAR_TOO_STEEP), 0) } test "a cliff is refused at the same place whatever the frame rate, and said once" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, character_st, 0.0, -8.0, 0.0) walk(base_st, character_st, 3.0, 30.0, false) let at30 = body_z(character_st) expect(body_steep(character_st) > 0.0) expect_eq(count(facts(base_st, character_st), CHAR_TOO_STEEP), 1) fresh(base_st, character_st, 0.0, -8.0, 0.0) walk(base_st, 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" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, character_st, 0.0, 8.0, SOUTH) walk(base_st, 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" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, character_st, 0.0, 8.0, SOUTH) walk(base_st, character_st, 0.2, 60.0, false) expect(not char_slope_blocks(character_st, body_y(character_st) + 0.1, 0.0, 1.0)) } test "the walk reaches the walk speed, the run the run speed, a sneak its own" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { fresh(base_st, character_st, 0.0, 0.0, SOUTH) walk(base_st, character_st, 2.0, 60.0, false) expect_near(body_speed(character_st), 3.2, 0.05) fresh(base_st, character_st, 0.0, 0.0, SOUTH) walk(base_st, character_st, 2.0, 60.0, true) expect_near(body_speed(character_st), 6.8, 0.05) charactertest_st.pace = 0.5 fresh(base_st, character_st, 0.0, 0.0, SOUTH) walk(base_st, character_st, 2.0, 60.0, false) expect_near(body_speed(character_st), 1.6, 0.05) charactertest_st.pace = 1.0 charactertest_st.sneaking = true fresh(base_st, character_st, 0.0, 0.0, SOUTH) walk(base_st, character_st, 2.0, 60.0, true) expect_near(body_speed(character_st), 1.3, 0.05) } test "a boulder is a wall to a body below its step and a step onto its top" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, character_st, 0.0, 3.0, SOUTH) walk(base_st, character_st, 2.0, 60.0, false) expect(body_z(character_st) < 5.1) 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" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, character_st, 18.0, 0.0, EAST) walk(base_st, character_st, 3.0, 60.0, false) let fs = facts(base_st, 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(base_st, character_st, 8.0, 60.0, false) expect(not body_swimming(character_st)) expect_eq(count(facts(base_st, character_st), CHAR_LEFT_WATER), 1) } test "the jump held dives, the air runs out, and the body surfaces" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, 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(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) } let fs = facts(base_st, character_st) expect(count(fs, CHAR_OUT_OF_AIR) > 0) for i in 0 .. 300 { char_update(base_st, 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" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, character_st, 0.0, 0.0, 0.0) char_update(base_st, 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(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) guard += 1 } let fs = facts(base_st, 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(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) expect(not body_airborne(character_st)) } 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" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { fresh(base_st, character_st, 0.0, 0.0, 0.0) expect_eq(char_sit_here(character_st), CHAR_SAT) for i in 0 .. 30 { char_update(base_st, 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(base_st, character_st, 0.0, -14.0, 0.0) expect_eq(char_sit_here(character_st), CHAR_NOT_STEEP) fresh(base_st, 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) charactertest_st.riding = true fresh(base_st, character_st, 0.0, 0.0, 0.0) expect_eq(char_sit_here(character_st), CHAR_NOT_RIDING) charactertest_st.riding = false } test "a hold stops the stick, and only its own reason lets it go" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { fresh(base_st, character_st, 0.0, 0.0, 0.0) charactertest_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(base_st, 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(base_st, character_st, 1.0 / 60.0) } expect(body_z(character_st) < -1.0) charactertest_st.mz = 0.0 } test "riding hands the stick over and leaves the body where the ride puts it" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { fresh(base_st, character_st, 0.0, 0.0, 0.0) charactertest_st.riding = true charactertest_st.rides = 0 char_board(character_st, 3.0, 3.0, 0.0) for i in 0 .. 10 { char_update(base_st, character_st, 0.0, 1.0, false, false, 0.0, 0.0, 1.0 / 60.0) } expect_eq(charactertest_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) charactertest_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" (base_st: mut BaseState, character_st: mut CharacterState) { fresh(base_st, 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.0, 0.0, CHAR_SIT_SEAT) expect_eq(char_fps_toggle(character_st), CHAR_EYES_SITTING) } }