ludic/packages/ludic.character
2026-09-28 16:02:42 +03:00
..
tests ludic.physics: queries fill a record the caller keeps, and allocate nothing 2026-09-27 23:18:26 +03:00
camera.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
fact_pool.ludic frame allocs (25.2): a fact record past the 64 made at the start is @alloc_ok, in its own function, in npc, character, physics, vehicles and wildlife 2026-09-28 16:02:42 +03:00
ground.ludic 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 2026-09-27 00:47:14 +03:00
index.ludic fix(character, physics, vehicles, wildlife): fact records made at the start (64) with a free list of room for all, as ludic.npc's 2026-09-28 16:02:42 +03:00
package.ludic feat(packages): ludic.character - a third-person walking body: walk and run with their acceleration, a jump and gravity, a step within CHAR_STEP taken and a slope above it measured over a fixed probe, wading that shelves into a swim, the dive and the breath, sitting, one safe put-down spot, holds by reason, being carried, and the orbit camera with first-person eyes; ground, body and stick through ports; landed, jumped, footfall, too steep, the water's facts 2026-09-25 08:26:31 +03:00
ports.ludic fix(character, physics, vehicles, wildlife): fact records made at the start (64) with a free list of room for all, as ludic.npc's 2026-09-28 16:02:42 +03:00
queries.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
README.md 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 2026-09-27 02:07:33 +03:00
rules.ludic 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 2026-09-27 00:47:14 +03:00
safe.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
sit.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
state.ludic frame allocs (25.2): a fact record past the 64 made at the start is @alloc_ok, in its own function, in npc, character, physics, vehicles and wildlife 2026-09-28 16:02:42 +03:00
steer.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00
swim.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00
system.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00
update.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00
verbs.ludic 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 2026-09-27 02:07:33 +03:00
walk.ludic 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 2026-09-27 02:07:33 +03:00
water.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00

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, 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 and nothing else.

import "ludic.character"

The package never draws. It moves a body and works out where a camera goes; the game poses a figure from the queries (body_phase(), body_sit_t(), body_swim_phase(), ...) and hands body_eye_* to its renderer. What happened is a fact (char_facts()); the game says the words, plays the sounds and charges the energy.

The rules it keeps

  • 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.
  • 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 could step onto.
  • Real time, not frames: every rate is multiplied by dt.

The ports

export port CharacterGround {
  height: fn(float, float) -> float                          # the world's ground, pure (required)
  top_at: fn(float, float, float, float, float, float) -> float   # (x, z, r, from_y, step, base)
  bed: fn(float, float) -> float                             # under water (unbound: height)
  water: fn(float, float) -> float                           # the surface (unbound: never wet)
  push: fn(float, float, float, float, float) -> bool        # (x, z, r, y0, y1) out of the colliders
  pushed_x: fn() -> float                                    # ... to here
  pushed_z: fn() -> float
  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
}
export port CharacterInput {
  move_x, move_z, run, jump, look_yaw, look_pitch, zoom, look_always
}

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

char_update(ix, iz, run, jump, dyaw, dpitch, dt) a frame from explicit input; char_tick(dt) reads CharacterInput (nothing while held)
char_teleport(x, z, yaw), char_stand_on, char_push_to, char_face, char_halt, char_go_limp, char_resume_swim(dive, depth, breath), char_map_changed() placing and stopping the body
char_config(walk, run), char_allow_run(on) 3.2 and 6.8 m/s by default; a spent body cannot run or jump
char_sit(x, z, yaw, mode), char_sit_here() -> CHAR_SAT / _STOOD / _NOT_RIDING / _NOT_AIRBORNE / _NOT_STEEP / _NOT_WET, char_stand() sitting (CHAR_SIT_SEAT, CHAR_SIT_GROUND)
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
char_wrap(a), char_toward(cur, target, rate), char_ease(t) the helpers a pose needs
char_facts() -> Queue<CharacterFact> { what, x, y, z, v }: CHAR_LANDED, _JUMPED, _FOOTFALL, _TOO_STEEP (once per refusal), _STARTED_SWIMMING, _LEFT_WATER, _OUT_OF_AIR (v: seconds), _CURRENT (every 12 s)
char_reset(), char_system() -> System "character", PH_INPUT, nothing saved: where a body stands is the game's to write

Tests

ludic test packages/ludic.character

A fake world of a meadow, a 45 cm deck, a cliff, a gentle rise, a boulder and a shelving lake: the step and the cliff at 30, 60 and 240 Hz, speeds and the sneak, wading into a swim and out, the dive and the breath, the jump, the safe spot, sitting and its refusals, holds, riding and the camera.