From 5c50755652f4efe311f6bcfe4a0c3f496ef0b74c Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 02:07:33 +0300 Subject: [PATCH] feat(character): 16.9 - char_knock, a knock-back the walker carries (never into a trunk); a landing keeps only the air's speed along the body's facing Co-Authored-By: Claude Opus 5.5 --- packages/ludic.character/README.md | 1 + packages/ludic.character/ports.ludic | 1 + .../ludic.character/tests/character_test.ludic | 14 ++++++++++++++ packages/ludic.character/verbs.ludic | 11 +++++++++++ packages/ludic.character/walk.ludic | 15 ++++++++++++--- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/ludic.character/README.md b/packages/ludic.character/README.md index 062fbf43..53859451 100644 --- a/packages/ludic.character/README.md +++ b/packages/ludic.character/README.md @@ -71,6 +71,7 @@ still stick. | `char_hold(reason)`, `char_release(reason)` (`CHAR_HOLD_SCREEN`, `CHAR_HOLD_STAGE`) | holds by reason: two holders cannot release each other | | `char_board(x, z, yaw)`, `char_ride_at(x, y, z, yaw, speed)`, `char_step_off(x, z)` | carried by something (`CharacterBody.riding`/`ride`) | | `char_safe_spot(x, z, from_y, max_depth) -> bool`, `body_safe_x()`, `body_safe_z()` | somewhere to put a body down | +| `char_knock(vx, vz, up)` | knocked off its feet (a shove): the walker carries it and it lands where the world lets it | | `char_stand_at(x, z, from_y)` | the ground with a collider's top when it is a step, for a put-down | | `char_camera(dt)`, `char_camera_third(dt)`, `char_look(yaw, pitch)`, `char_look_pitch`, `char_orbit(dist)`, `char_lock_orbit`, `char_set_fps`, `char_fps_toggle() -> CHAR_EYES_*` | the camera; read it back with `body_eye_x/y/z/yaw/pitch()` and `body_eyes_now()` | | `body_x()`, `body_y()`, `body_z()`, `body_yaw()`, `body_speed()`, `body_moving()`, `body_run()`, `body_phase()`, `body_time()`, `body_water()`, `body_airborne()`, `body_vy()`, `body_landing()`, `body_steep()`, `body_step()`, `body_sitting()`, `body_sit_mode()`, `body_sit_t()`, `body_swimming()`, `body_diving()`, `body_depth()`, `body_breath()`, `body_breath_max()`, `body_floating()`, `body_swim_phase()`, `body_can_run()`, `body_held()`, `body_fps()`, `body_cam_yaw()`, `body_cam_pitch()`, `body_cam_dist()` | the queries | diff --git a/packages/ludic.character/ports.ludic b/packages/ludic.character/ports.ludic index 0bd768f4..d9a7c5c7 100644 --- a/packages/ludic.character/ports.ludic +++ b/packages/ludic.character/ports.ludic @@ -67,6 +67,7 @@ export state CharacterState { chr_air: bool = false chr_air_vx: float = 0.0 chr_air_vz: float = 0.0 + chr_knock: float = 0.0 # a knock-back's speed up, handed to the walker on the next step chr_land: float = 0.0 # seconds of the landing's crouch left chr_steep: float = 0.0 # seconds of "that was too steep" left to say chr_can_run: bool = true diff --git a/packages/ludic.character/tests/character_test.ludic b/packages/ludic.character/tests/character_test.ludic index d4e384a2..54bbc468 100644 --- a/packages/ludic.character/tests/character_test.ludic +++ b/packages/ludic.character/tests/character_test.ludic @@ -240,6 +240,20 @@ program CharacterTest { 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 diff --git a/packages/ludic.character/verbs.ludic b/packages/ludic.character/verbs.ludic index 8797dcb8..335c076f 100644 --- a/packages/ludic.character/verbs.ludic +++ b/packages/ludic.character/verbs.ludic @@ -123,3 +123,14 @@ export function char_step_off(character_st: mut CharacterState, x: float, z: flo character_st.chr_z = z character_st.chr_y = CharacterGround.height(x, z) } + +# knocked off its feet at (vx, vz) across and `up` upward (a bear's shove): the walker carries it, +# so it lands wherever the world lets it - never inside a trunk. Not while swimming or riding. +export function char_knock(character_st: mut CharacterState, vx: float, vz: float, up: float) -> void { + if character_st.chr_swim or CharacterBody.riding() { return } + character_st.chr_sit = false + character_st.chr_air = true + character_st.chr_air_vx = vx + character_st.chr_air_vz = vz + character_st.chr_knock = up +} diff --git a/packages/ludic.character/walk.ludic b/packages/ludic.character/walk.ludic index ee82a287..d36897ec 100644 --- a/packages/ludic.character/walk.ludic +++ b/packages/ludic.character/walk.ludic @@ -14,7 +14,12 @@ function chr_walk(character_st: mut CharacterState, fx: float, fz: float, dx: fl var vx = fx * character_st.chr_speed var vz = fz * character_st.chr_speed var up = 0.0 - if character_st.chr_air { + if character_st.chr_knock > 0.0 { + up = character_st.chr_knock + character_st.chr_knock = 0.0 + vx = character_st.chr_air_vx + vz = character_st.chr_air_vz + } else if character_st.chr_air { character_st.chr_air_vx = character_st.chr_air_vx + dx * (4.0 * dt) character_st.chr_air_vz = character_st.chr_air_vz + dz * (4.0 * dt) vx = character_st.chr_air_vx @@ -60,6 +65,10 @@ function chr_walked(character_st: mut CharacterState, g: int, jumped: bool, dt: function chr_landed(character_st: mut CharacterState) -> void { character_st.chr_land = 0.22 - character_st.chr_speed = Math.sqrt(character_st.chr_air_vx * character_st.chr_air_vx + character_st.chr_air_vz * character_st.chr_air_vz) - chr_say(character_st, CHAR_LANDED, character_st.chr_speed) + let carried = Math.sqrt(character_st.chr_air_vx * character_st.chr_air_vx + character_st.chr_air_vz * character_st.chr_air_vz) + # the walk goes on only as fast as the air was carrying it the way it faces (a knock from the + # front does not walk it back into what knocked it) + let along = -Math.sin(character_st.chr_yaw) * character_st.chr_air_vx - Math.cos(character_st.chr_yaw) * character_st.chr_air_vz + character_st.chr_speed = Math.max(along, 0.0) + chr_say(character_st, CHAR_LANDED, carried) }