feat(character): 16.8 - on land a physics walker moves the body (CharacterGround.walk); the package's own gravity, ballistic air and slope-probe refusal are gone

The package keeps the rules and hands them over each step: speed and heading, whether a jump
leaves, CHAR_STEP and the boots' slope limit. The walker (Jolt CharacterVirtual) does gravity,
landing, steps, slopes and following the ground down. Too steep is ground too steep facing the way
the body wanted to go; stepping off an edge is not. ludic.physics: phys_walker_move (follow the body
if something else moved it, then step), the slope limit, the jump always honoured. The character's
tests run on a real Jolt walker over the same fake world: 15 pass, a knee-high boulder now a step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 00:47:14 +03:00
parent d2cd775309
commit e909ec122c
11 changed files with 169 additions and 89 deletions

View file

@ -1,7 +1,7 @@
# ludic.character
One walking body under a player's stick, seen from over its shoulder: the gait's walk and run with
their acceleration, a jump with gravity, the step-versus-slope rule, wading that shelves into a swim,
their acceleration, a jump, the step-versus-slope rule, wading that shelves into a swim,
diving on a breath, sitting, one answer to "where can this body be put down", holds, being carried,
and the orbit camera with its first-person eyes. Uses [`ludic.base`](../ludic.base/README.md) and
nothing else.
@ -19,10 +19,13 @@ sounds and charges the energy.
- **A pure query of the world stays pure.** `CharacterGround.height` is the world's ground and never
the body's; what the feet can stand on is `char_stand_at(x, z, from_y)`, and only the movement asks.
- **A step up and a slope up are different refusals** (`char_slope_blocks`). Within `CHAR_STEP`
(0.55 m) of the feet is a step, taken whatever its gradient; above it the boots
(`slope_limit`, a rise per metre) decide over `CHAR_SLOPE_PROBE` (0.6 m), never over the frame's
own step - so 30 Hz and 240 Hz refuse at the same place.
- **On land the game's physics walker moves the body** (`CharacterGround.walk`, phase 16: a Jolt
CharacterVirtual in Maroon Lake). The package keeps the rules and hands them over each step: the
speed and heading, whether a jump leaves (at 5 m/s), `CHAR_STEP` (0.55 m) as the step taken
whatever its gradient, and the boots' `slope_limit` (a rise per metre) as the steepest ground.
The walker does gravity, landing, steps, slopes and following the ground down, so 30 Hz and
240 Hz refuse at the same place. `CHAR_WALK_STEEP` - ground too steep facing the way it wanted
to go - is the refusal and the `CHAR_TOO_STEEP` fact; stepping off an edge is not.
- **Where a body is put down is one question with one answer** (`char_safe_spot`): clear of every
collider, no deeper than asked, within `CHAR_DROP` of the height left, on ground it can walk off.
- **A collider has a height**: `push` takes the body's span (y0, y1), and `top_at` reports a top it
@ -43,6 +46,8 @@ export port CharacterGround {
slope: fn(float, float) -> float # 0 flat .. 1 wall (for sitting)
slope_limit: fn() -> float # the boots: rise a metre (1.05)
current: fn(float, float, float, float) -> float # (x, z, fx, fz): a stroke's share lost
walk: fn(x, y, z, vx, vz, jump, step, max_slope_deg, dt) -> int # the land mover (required)
walked_x / walked_y / walked_z: fn() -> float # ... to here
}
export port CharacterBody {
pace, sneaking, breath, riding, ride(ix, iz, run, dt), holding, hold_x, hold_z, eyes_ok
@ -52,7 +57,7 @@ export port CharacterInput {
}
```
Only `height` is required; unbound, the rest is a dry plain with no colliders, a fresh body and a
`height`, `walk` and `walked_*` are required (the tests bind a real `ludic.physics` walker); unbound, the rest is a dry plain with no colliders, a fresh body and a
still stick.
## API
@ -66,7 +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_stand_at(x, z, from_y)`, `char_slope_blocks(nh, fx, fz)` | the step rule, for a game's own movers |
| `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 |
| `char_wrap(a)`, `char_toward(cur, target, rate)`, `char_ease(t)` | the helpers a pose needs |