Nimbin[12]?Games / Entropie / commits / 080d3ee

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (17 hours ago) · synced 8 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz

entropie 1.87.0: walking on things, balancing

Christian Immanuel · 2026-10-15 17:42 · 080d3ee71a3f2dc9fc1d6965325f03e287902b5c

- Planks, logs and crates are never straddled: flat ones you walk onto,
  higher ones are one step up, and you walk on along the top
- On something narrow you balance: arms out, a wobble (clumsy, drunk or
  hurrying: bigger), slower steps, now and then a step off
- Something you can't step over or onto (a tree, a post, a high crate) that
  you'd only just catch: you drift past it to the side
- Smoother lines: diagonal outlines and edges no longer step like stairs
- Fix: standing on a log you fell through it (it counted as "stepped over")
- Fix: the chainsaw floated above the ground, its bar in the air

Includes 1.86.0 (if not committed separately):
- Something solid that holds you (a log, a fallen tree, a heavy crate) you
  step onto and stand on, instead of straddling it; walking at the end of
  something long, you go round its end
- Fix: the ladder couldn't be leaned on a beam of the unfinished building
  (aiming missed the thin beam; stood up free it fell through it). Aiming
  is forgiving now, and a falling ladder rests on what it falls against
- A ladder held up tips down flat to get under a beam or doorway and goes
  up again after (before, you got stuck)

Includes 1.85.0 (if not committed separately):
- Somebody standing in front of a pushed load leans against it (it goes
  slower) and side-steps out of its way; the slowest just complain
- Fix: a person a pushed crate ran into got bulldozed along with it (their
  feet stalled against it)

Includes 1.84.0 (if not committed separately):
- A helper standing where the pushed or pulled load wants to go notices it,
  lets go and comes round to another side; quick-witted ones soon, slow
  ones (Ben) not at all ("Why won't it move?")
- New trait: wits (0 slow .. 1 sharp), also in the dev menu

Includes 1.83.0 (if not committed separately):
- Peeing standing: one hand holds it (the Urinella to the body), the other
  stays at the belt
- A full ashtray (12 butts) gets emptied by somebody tidy: into a scrap of
  paper, off to the bin
- Fix: a tidy person could stand still for minutes, reaching again and
  again for trash just out of reach

Includes 1.82.0 (if not committed separately):
- Walking styles: feet wider or narrower, toes out more or less, bouncier
  or flatter (livelier when lively), hips swaying more or less
- The ashtray stands on the cool box by the fire (on the ground it was in
  everybody's way)

Includes 1.81.0 (if not committed separately):
- Fix: following someone on a chat break (and walking up to things) went
  walk-stop-walk every frame, the whole body shook; now they slow down
  coming up and stay with them
- Fix: an elbow could flip over when a hand went up past the shoulder
  (scratching the head); it turns round the arm now

Includes 1.80.0 (if not committed separately):
- An old tin by the fire as ashtray: you and the tidy put butts in there
- The Urinella is drawn while it's used; the dress is lifted higher
- After peeing you take up again what you put down
- The others can slip off a wobbly ladder too

Includes 1.79.0 (if not committed separately):
- Helpers (G) follow you up a leaning ladder onto a roof or wall and down
  again; without a ladder they pull themselves up after you (if they reach)
  and jump down when you're down
- Pulling up is a module for anybody now (people/climb.c), not only you

Includes 1.78.0 (if not committed separately):
- Cold nights: away from the fire a jacket goes on (each their own colour)
- Working hard in the midday sun your shirt comes off too
- Women who never tried a Urinella may try one: some like it, some don't
- Dying for a smoke makes restless; a wobbling ladder rocks
- Moving heavy wood: tested with a called helper; the player is asked when
  nobody else is around

Includes 1.77.0 (if not committed separately):
- Ladders: carried nearly upright when out; stood up on their own they
  fall over; leaned, how steady they stand depends on angle, ground, feet
  and what holds them; a wobbly one wobbles, then slips and falls with you
- Somebody holds it (hands on the rails, a foot on the bottom rung):
  helpers (G) when you climb, helpful people when it looks wobbly
- The work place: the tidy clear away what lies where they'd stand; no room
  to swing, they take the piece somewhere with room (carried, or dragged
  with somebody called to help)
- The sporty pull up and climb ladders much quicker

Includes 1.76.0 (if not committed separately):
- Smoking: packs and tobacco in the pockets (used up); smokers get a
  craving and light up in a quiet moment - rolling one first, the lighter
  to it, drags (the tip glows), smoke breathed out, the butt flicked away
- Shared: "Got a light?", bumming a cigarette (or a no); talk to ask for
  one or offer one; K lights yours / puts it out; Tab shows your pockets
- Shadows: soft, smooth edges instead of stair steps

Includes 1.75.0 (if not committed separately):
- Urinellas: one-time use, up to 4 in the pocket; a box by the toilet
  refills it (the others fetch more when they run out, you click it)
- Peeing: what's in the hands is put down first (the others take it up
  again); standing, the fly is open or the skirt or dress lifted
- Lighter: needed to light the fire, runs empty; borrowed when there's
  none, given back (or not), owners ask for it; talk: borrow, give back,
  ask for yours back
- Working hard in the midday sun the shirt comes off (women keep a top)
- Dropping from high up: knees pulled up, legs reach for the ground, deep
  into the knees, a roll or a crash
- Ladders: one-handed climbing (you may slip), a lying ladder is stepped
  over, kicked it hardly moves and your toe hurts
- Dev menu: teleport to the parkour / yard; the player spawns 1 m up

Includes 1.74.0 (if not committed separately):
- Fix: legs showed through a skirt or dress standing normally; the thighs
  under it are cloth now and it's wider over the hips
- Mila's Urinellas come in a pack of 3 (then she squats); now and then a
  mishap (on the shoes); used ones go into the bag, are dropped or flung
  away (drunk) as trash

Includes 1.73.0 (if not committed separately):
- Clothes: trousers, shorts, skirt, dress; squatting to pee trousers come
  down, a skirt or dress is gathered up

Includes 1.72.0 (if not committed separately):
- Fix: walking off the other way from something half stepped over left
  one foot planted behind, sliding along

Includes 1.71.0 (if not committed separately):
- Two-handed fidgets: rubbing the hands, hands behind the head, both
  hands on the hips

Includes 1.70.0 (if not committed separately):
- More one-hand gestures (neck, nose, ear, hair, thumb in the belt, hand
  on the chest, shading the eyes); mood and personal habits pick them

Includes 1.69.0 (if not committed separately):
- Faster with a crowd: 750 people 22 -> 14 ms a frame (kind index, cell
  grid for company, no foot checks far from you)

Includes 1.68.0 (if not committed separately):
- The walking map is updated in place when fixed things change
  (0.3 ms instead of 10 ms)

Includes 1.67.0 (if not committed separately):
- Pour your drink on the fire (C): a small fire goes out, a big one
  hisses, vodka flares up

Includes 1.66.0 (if not committed separately):
- Careful people sort the woodpile by length (front even) and put the
  branches lying about onto a heap at the end of the woodpile

Includes 1.65.0 (if not committed separately):
- Smooth off the ladder (a step back, one foot after the other) and
  taking it down (the hand reaches for the rail at chest height)

Includes 1.64.0 (if not committed separately):
- Smooth ladder and climbs: getting on, over the edge at the top, feet
  from where they are; the easy 1 m climb no longer drops you off again

Includes 1.63.0 (if not committed separately):
- The hose melts in the fire (leaks, burns through); duct tape and the
  shears mend it

Includes 1.62.0 (if not committed separately):
- Smooth pull-ups: no hop at the end, feet and knees ease over the edge,
  elbows and hands taking hold blend in (audit walk: joint jumps 161 -> 58)

Includes 1.61.0 (if not committed separately):
- A step ladder in the tool room: stands on its own, at a wall with its
  back to it; climb onto the wall from its top

Includes 1.60.0 (if not committed separately):
- A folding ladder (two 2.5 m halves on a hinge, 5 m open) beside the
  telescopic one; one click at a wall unfolds it and leans it there

Includes 1.59.0 (if not committed separately):
- Stepping up onto blocks above knee high; set moves and jumps only for
  things standing on the ground in front of you

Includes 1.58.0 (if not committed separately):
- Walking over knee-high things is one smooth set move that always
  finishes; at a run: jump up onto blocks, catch climbable edges
 entropie/Makefile                  |  2 +-
 entropie/README.md                 | 13 +++++--
 entropie/STATE.md                  | 12 ++++--
 entropie/assets/shaders/ink.frag   | 30 +++++++++++++-
 entropie/commit_message.txt        | 15 ++++++-
 entropie/src/game/game.c           |  4 ++
 entropie/src/people/body.c         | 28 +++++++++++++
 entropie/src/people/body.h         |  4 ++
 entropie/src/people/body_pose.c    |  9 ++++-
 entropie/src/render/shapes_props.c |  2 +-
 entropie/src/world/object.c        |  4 +-
 entropie/src/world/object.h        |  2 +
 entropie/src/world/physics.c       | 80 +++++++++++++++++++++++++-------------
 entropie/src/world/physics.h       |  2 +
 entropie/src/world/world.c         |  8 ++++
 15 files changed, 173 insertions(+), 42 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index 57f525c..ced4e8d 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.86.0
+VERSION   := 1.87.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 02e41b3..0c72216 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -12,7 +12,8 @@ What the game can do so far, who is in it and what is still open:
 
 Painted in the rotoscope style of *Waking Life* (2001):
 
-- Wobbly geometry and boiling ink outlines, animated at 12 fps
+- Wobbly geometry and boiling ink outlines, animated at 12 fps; diagonal
+  lines and edges are smoothed (no pixel stairs)
 - Flat paint tones with hand-drawn shadow shapes (soft, smooth edges)
 - Color moods that change through the day, under a calm painted sky
 - People are painted figures with faces, hair and real arm and leg movement
@@ -150,9 +151,13 @@ and satellite pictures (about 340 x 340 m).
   stepping over a fence) or the foot comes up; sprinting, you leap
 - Stepping over, you lean into it and the leg swings round a little, out to
   the side and back in to land, while the momentum carries you on
-- Something solid that holds you (a log, a fallen tree, a heavy crate, from
-  about knee-low up) you step onto instead: you stand up there and walk on
-- Walking at the end of something long, you go round the end instead
+- Something solid that holds you (a plank, a log, a fallen tree, a crate) you
+  never straddle: flat ones you just walk onto, higher ones are one step up,
+  then you stand up there and walk on
+- On something narrow (a log) you balance: arms out, a wobble (bigger when
+  clumsy, drunk or hurrying), slower steps - and now and then a step off
+- Something you can't step over or onto (a tree, a post, a high crate, a
+  wall's corner) that you'd only just catch: your body drifts past it to the side
 - The sporty fit their steps to what's coming, so a foot lands at a good
   take-off spot
 - Jogging or faster, low things (up to about the knee) are jumped on their
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 04dc9bd..ccc319d 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -71,7 +71,13 @@ Making room (`ai_help.c` makeRoom, every frame in AI_HELP): the leader wants to
 
 Bystanders (`hands.c` inTheWay, in Hands_solveGrabs): people not holding a pushed load that it would go into (moved a step ahead) count against it with their weight (BYSTANDER_HOLD 1.0) and get `shoved` (the way it goes, `shove_load` its middle; cleared every frame). `ai.c` stepAside (not helpers, not sitting): after 0.25 + 1.5 x (1 - wits) s a quick side step to the side they're already off to (0.8 s at 1.6 m/s, no turning first); wits under 0.2: "Hey! Watch it!" and 4 s not minding it. While shoved, a standing leg against something doesn't hold the body back (`physics.c` steppingOver: their feet stalled against the load before, they got bulldozed along).
 
-Onto, not over (`physics.c`): `standsOn` (static or ONTO_HOLDS 40 kg+, a side facing straight up - boxes, logs and fallen trees lying; not bottles, furniture, bins, supplies, ladders, the fire): the player steps up onto it (startStepOver `up`, STAND_UP_TIME 0.4 s + 0.9 s/m) when it's STAND_LOWEST 0.22+ high with STAND_ROOM 0.28 m on top, instead of straddling it; lands in its middle. A lying tree (toppled, topple 1) is no longer neverOver. `pastTheEnd` (all characters): something 1.4 m+ long across your way that you'd only catch with up to 0.55 m of you, within 1.6 m: the walk bends past its end. NPCs still step over (and round) as before.
+Onto, not over (`physics.c`, the player only - NPCs still step over and round): `standsOn` (static or STAND_HOLDS 4 kg+, a side facing straight up - boxes, planks, logs and fallen trees lying; not bottles, furniture, bins, supplies, ladders, branches, bags, trash, tools, the fire). Lower than STAND_LOWEST 0.18: not stepped over (`Physics_stepsOver` false), you walk onto it (`tryStepUp` with that most, also for loose things). Higher (up to STEP_UP_MOST, STAND_ROOM 0.2 m on top, 0.12 m across): one step up (startStepOver `up`, STAND_UP_TIME 0.4 s + 0.9 s/m), landing in its middle. Standing on something (feet within STOOD_ON 0.08 of its top) it is never stepped over (it holds you); `standing_on` (set when landing on something, every step); the object you stand on is skipped by steppingOver / startStepOver / pastTheEnd, and no leg-wait up there. A lying tree (toppled, topple 1) is no longer neverOver. `pastTheEnd` (player): something you can't step over or onto (neverOver, or neither a low obstacle nor standable up to STEP_UP_MOST; bottom under 1.2 m, top over 0.1 m, not light loose things) that you'd catch with up to END_CLIP 0.7 m of you (END_BODY 0.4: half width and legs), within 1.6 m: you drift past it sideways (`veer_side`, up to 0.45 of the speed, eased 2.5/s). Wider: you walk round it yourself.
+
+Smooth lines (`ink.frag`): edgeAt looked at in 4 spots inside the pixel (rotated grid) and averaged (`smoothEdge`); the fill averaged over 4 taps where their luminance differs 0.04-0.14+ (`smoothFill`). 4x the edge texture reads: watch it on slow graphics.
+
+Balancing (`world.c` -> `body.balance_goal` from `Physics_footing`: 1 at a top 0.3 m wide or less, 0 from 0.75 m; `body.c` updateBalance): `teeter` is a sprung wobble (3.2 rad/s, damping 0.35) kicked at random (TEETER_KICK 40 x (0.25 + 0.6 clumsy + 1.6 drunk + 0.35 speed + 0.4 nerves) x (1.25 - 0.75 fitness)); past 1: Body_stagger sideways (`stepped_off` -> "Whoa - you lose your balance and step off"). Pose: arms out to the sides (the one on the tipping side lower), the hips with the wobble, the upper body against it; walking BALANCE_SLOW 0.55 slower.
+
+Chainsaw: collider and pivot fit the mesh (the motor's underside on the ground), the bar lower on the motor (it floated).
 
 Ladders and things in the way (`ladder.c`): aiming with the view also tries 0.05 / 0.1 rad above and below (thin beams); hitting a beam from below takes the side towards you. Falling over (`fallOver`, now with the world: `Ladder_update(world, l, dt)`) it stops against anything solid it goes into (`caught`: leaning there, CAUGHT_STEADY 0.45, `lean_top` if its top is over the thing; `just_caught` -> the game says so). Carried held up (`world.c` dipLadder): running into something -> `dipped` (raise goal RAISE_DIP 0.12, nearly flat); down and clear, up again once it would stand up at 1.36 without going into anything.
 
@@ -193,7 +199,7 @@ Most important first.
 - Joint jumps left in the audit: jog+stop 171, sprint 297 (worst 26 cm) - running into things: a hurried catch step swings through in a few frames (step 0.07 -> 0.34, the ankle up 20 cm). Capping the swing speed helped sprint (278) but made jumps, drunk walk and the NPCs worse - tried and reverted in 1.83; ladder 45: toes on the sloping roof (a landing step's toes snap onto the slope, 16 cm; blending them in made all walking worse), toes turning onto the rungs getting on from the top (14 cm)
 
 **Body and moving**
-- Obstacle approach, still to do: NPCs stepping onto solid things too (now only you); balancing on a log (arms out, a wobble); a step in between on top of something unstable (if clumsy); NPCs: pull up on their own for their own reasons (now only following a leader), jumping onto things
+- Obstacle approach, still to do: NPCs stepping onto solid things and balancing too (now only you); thin round logs (under 0.2 m) are still stepped over; a step in between on top of something unstable (if clumsy); NPCs: pull up on their own for their own reasons (now only following a leader), jumping onto things
 - Climbing rework: hands first (one reaches first, shoulder ahead), a knee up, the other foot next to it; or sit on it and lift the legs; or belly first and roll; by height, surface and person
 - Balance: hips over the feet, look for stable footing, play with balance
 - Personal movement styles, still to do: gesture variants; stepping over and climbing their own way; sporty ones more elegant (walking styles are in)
@@ -213,4 +219,4 @@ Most important first.
 - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene)
 
 ---
-State of `entropie 1.86.0 (build 92de65a)`
+State of `entropie 1.87.0 (build f24cb4e)`
diff --git a/entropie/assets/shaders/ink.frag b/entropie/assets/shaders/ink.frag
index 970db2d..7cc07a2 100644
--- a/entropie/assets/shaders/ink.frag
+++ b/entropie/assets/shaders/ink.frag
@@ -91,6 +91,32 @@ vec2 doubleOffset() {
     return vec2(sin(time * 0.7), 0.4 * cos(time * 0.5)) * 0.018 * drunk;
 }
 
+// Smooth lines: edges looked at in four spots inside the pixel (a rotated
+// grid) and averaged - a diagonal line covers a pixel partly, not all or
+// nothing, so it doesn't step like a staircase.
+const vec2 SUB[4] = vec2[4](vec2(-0.375, -0.125), vec2(0.125, -0.375), vec2(0.375, 0.125), vec2(-0.125, 0.375));
+
+float smoothEdge(vec2 uv, float width) {
+    float e = 0.0;
+    for (int i = 0; i < 4; i++) e += edgeAt(uv + SUB[i] * texel, width);
+    return e * 0.25;
+}
+
+// Smooth fills: where neighbouring pixels differ a lot (the border of a roof
+// against the sky), the color is averaged over the pixel's corners.
+vec3 smoothFill(vec2 uv) {
+    vec3 c = texture(colorTex, uv).rgb, sum = vec3(0.0);
+    float lo = 1e9, hi = -1e9;
+    for (int i = 0; i < 4; i++) {
+        vec3 s = texture(colorTex, uv + SUB[i] * 1.6 * texel).rgb;
+        float l = luminance(s);
+        lo = min(lo, l);
+        hi = max(hi, l);
+        sum += s;
+    }
+    return mix(c, sum * 0.25, smoothstep(0.04, 0.14, hi - lo));
+}
+
 void main() {
     vec2 uv0 = waver(UV);
     float sky = isSky(texture(normalDepthTex, uv0));
@@ -102,10 +128,10 @@ void main() {
 
     // Pen pressure: width varies, lines thin out and break here and there.
     float width = mix(0.8, 2.2, valueNoise(vec3(UV * 9.0, boilTime * 2.0 + 3.0)));
-    float edge  = edgeAt(lineUV, width);
+    float edge  = smoothEdge(lineUV, width);
     edge *= mix(0.45, 1.0, smoothstep(0.3, 0.45, valueNoise(vec3(UV * 30.0, boilTime * 4.0))));
 
-    vec3 color = texture(colorTex, fillUV).rgb;
+    vec3 color = smoothFill(fillUV);
     float blur = 0.0;
     if (drunk > 0.001) {
         blur  = blurPixels(UV);
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index 9888792..6ff81e6 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,5 +1,16 @@
-entropie 1.86.0: onto the log, and the ladder on the beam
-
+entropie 1.87.0: walking on things, balancing
+
+- Planks, logs and crates are never straddled: flat ones you walk onto,
+  higher ones are one step up, and you walk on along the top
+- On something narrow you balance: arms out, a wobble (clumsy, drunk or
+  hurrying: bigger), slower steps, now and then a step off
+- Something you can't step over or onto (a tree, a post, a high crate) that
+  you'd only just catch: you drift past it to the side
+- Smoother lines: diagonal outlines and edges no longer step like stairs
+- Fix: standing on a log you fell through it (it counted as "stepped over")
+- Fix: the chainsaw floated above the ground, its bar in the air
+
+Includes 1.86.0 (if not committed separately):
 - Something solid that holds you (a log, a fallen tree, a heavy crate) you
   step onto and stand on, instead of straddling it; walking at the end of
   something long, you go round its end
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index e608a3e..520548c 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -249,6 +249,10 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     game_climbLadder(game, input, dt);
     game_climb(game, input, dt);
     World_update(&game->world, dt, dt * game->daycycle.time_scale * speed);
+    if (game->player->body.stepped_off) {
+        game->player->body.stepped_off = false;
+        say(game, "Whoa - you lose your balance and step off");
+    }
     for (int i = 0; i < game->world.count; i++) { /* a ladder that fell against something near you */
         Object *l = game->world.objects[i];
         if (l->type != OBJ_LADDER || !l->just_caught) continue;
diff --git a/entropie/src/people/body.c b/entropie/src/people/body.c
index 60f89ee..524811e 100644
--- a/entropie/src/people/body.c
+++ b/entropie/src/people/body.c
@@ -881,6 +881,33 @@ static void inTheAir(Body *b, const BodyFrame *f, float s, float speed) {
     b->airborne = true;
 }
 
+/* On something narrow (a log): the arms go out and the body teeters from side
+   to side - a wobble that grows with clumsiness, drink and hurry and that the
+   sporty keep small. Past the edge of it: a step off sideways. */
+#define TEETER_OMEGA  3.2f  /* rad/s: how quickly a wobble swings back */
+#define TEETER_DAMP   0.35f /* (some is caught, the rest swings on) */
+#define TEETER_KICK   40.0f  /* how hard the wobbles come */
+
+static void updateBalance(Body *b, const BodyFrame *f, float speed, float dt) {
+    b->balance += (b->balance_goal - b->balance) * fminf(1.0f, (b->balance_goal > b->balance ? 4.0f : 2.0f) * dt);
+    if (b->balance < 0.02f || b->airborne || b->on_ladder || dt <= 0.0f) {
+        b->teeter *= fmaxf(0.0f, 1.0f - 3.0f * dt);
+        b->teeter_v = 0.0f;
+        return;
+    }
+    float shaky = (0.25f + 0.6f * Body_clumsiness(b) + 1.6f * b->unsteadiness + 0.35f * fminf(speed, 2.0f) + 0.4f * b->shown.nerves)
+                * (1.25f - 0.75f * b->fitness) * b->balance;
+    float kick = (random01(b) * 2.0f - 1.0f) * TEETER_KICK * shaky;
+    b->teeter_v += (kick - TEETER_OMEGA * TEETER_OMEGA * b->teeter - 2.0f * TEETER_DAMP * TEETER_OMEGA * b->teeter_v) * dt;
+    b->teeter += b->teeter_v * dt;
+    if (fabsf(b->teeter) > 1.0f && b->stumble <= 0.0f) { /* too far over: a step off, that side */
+        float side = b->teeter > 0.0f ? 1.0f : -1.0f;
+        Body_stagger(b, vec3_add(vec3_scale(rightOf(f->facing), side * 1.3f), vec3_scale(f->facing, 0.2f)));
+        b->teeter = b->teeter_v = 0.0f;
+        b->stepped_off = true;
+    }
+}
+
 void Body_update(Body *b, const BodyFrame *f) {
     const float s = f->height / REF_HEIGHT;
     const float dt = f->dt;
@@ -950,6 +977,7 @@ void Body_update(Body *b, const BodyFrame *f) {
     updateExpression(b, f, speed);
     updateSmoking(b, speed, dt);
     if (updateDown(b, f, s)) return;
+    updateBalance(b, f, speed, dt);
 
     /* Side steps and backward steps are shorter than steps forward; shy people take small steps */
     float sideways = speed > 0.05f ? fabsf(vec3_dot(b->velocity, rightOf(f->facing))) / speed : 0.0f;
diff --git a/entropie/src/people/body.h b/entropie/src/people/body.h
index 420dc91..e45315c 100644
--- a/entropie/src/people/body.h
+++ b/entropie/src/people/body.h
@@ -88,6 +88,10 @@ typedef struct {
     bool  hand_active[2];    /* hand reaches for hand_target */
     Vec3  hand_target[2];    /* world space palm position */
     float unsteadiness;      /* 0 = sober .. 1 = drunk: sway and misplaced steps */
+    float balance_goal;      /* 0 .. 1: standing on something narrow (a log) - set by the world */
+    float balance;           /* ... eased: arms out, teetering */
+    float teeter, teeter_v;  /* -1 .. 1 tipping to the left / right (past 1: a step off) */
+    bool  stepped_off;       /* lost the balance just now (the world says so; taken) */
     float exertion;          /* 0 = fresh .. 1 = exhausted: heavy breathing */
     float chin_goal;         /* head tipped back (radians), e.g. drinking */
     float urge;              /* 0..1: wants to move on (gets up faster after a fall) */
diff --git a/entropie/src/people/body_pose.c b/entropie/src/people/body_pose.c
index 90098a4..5f57623 100644
--- a/entropie/src/people/body_pose.c
+++ b/entropie/src/people/body_pose.c
@@ -480,6 +480,7 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt
     hip_y[0] += -0.024f * ws;
     hip_y[1] += 0.024f * ws;
     if (u > 0.0f) sway += sinf(b->clock * 1.3f) * 0.05f * u;
+    sway += 0.05f * b->teeter * b->balance; /* (balancing on a log: the hips go with the wobble) */
     /* Shaking a leg: all the weight over the other foot */
     if (shake > 0.001f) sway = lerpf(sway, ankle[1 - b->fidget_side].x * 0.85f, shake);
     /* Nobody stands quite still: a slow, slight sway, never the same */
@@ -557,7 +558,8 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt
         springCritical(&b->reach_bend, &b->reach_bend_v, want, POSE_OMEGA, dt);
         pitch += fmaxf(0.0f, b->reach_bend);
     }
-    float tilt = -sway * 0.4f + (hip_y[1] - hip_y[0]) * 1.2f; /* spine leans against the hip tilt */
+    float tilt = -sway * 0.4f + (hip_y[1] - hip_y[0]) * 1.2f /* spine leans against the hip tilt */
+               - 0.22f * b->teeter * b->balance;                /* (balancing: the upper body against the wobble) */
     /* A back with a waist: the lower spine bends a little, the upper more, and
        the upper body lags behind starting, stopping and turning (a spring) */
     {
@@ -735,6 +737,11 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt
             blendArm(&arm, palm, pole, g);
         }
 
+        /* Balancing: the arms out to the sides, the one on the side it tips to lower, the other up */
+        float bal = b->balance * (1.0f - b->hand_weight[side]);
+        if (bal > 0.001f)
+            blendArm(&arm, vec3(sign * 0.64f, PELVIS_Y + SPINE + 0.06f - 0.16f * b->teeter * sign, 0.06f), vec3(0.0f, -1.0f, -0.5f), bal);
+
         /* Fidgets with the arms: stretched up over the head, or folded */
         if (stretch > 0.001f)
             blendArm(&arm, vec3(sign * 0.17f, PELVIS_Y + SPINE + NECK + 0.62f, 0.04f), vec3(sign, 0.1f, -0.3f), stretch);
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 98b9e92..ecc2b71 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -188,7 +188,7 @@ Mesh Shapes_chainsaw(void) {
     MeshBuilder_init(&b);
     addBlock(&b, vec3(0.0f, 0.0f, 0.2f), vec3(0.16f, 0.18f, 0.32f), COLOR_ORANGE);      /* motor, at the back */
     addBlock(&b, vec3(0.0f, 0.12f, 0.24f), vec3(0.04f, 0.06f, 0.2f), COLOR_DARK);       /* top handle */
-    addBlock(&b, vec3(0.0f, -0.02f, -0.17f), vec3(0.025f, 0.07f, 0.42f), COLOR_STEEL);  /* bar, pointing forward (-Z) */
+    addBlock(&b, vec3(0.0f, -0.05f, -0.17f), vec3(0.025f, 0.07f, 0.42f), COLOR_STEEL);  /* bar, pointing forward (-Z), low on the motor: lying, it nearly touches the ground */
     addBlock(&b, vec3(0.0f, 0.0f, 0.03f), vec3(0.18f, 0.03f, 0.03f), COLOR_DARK);       /* front grip */
     return Mesh_fromBuilder(&b);
 }
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 75761dd..d54f55f 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -300,8 +300,8 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
 
     case OBJ_CHAINSAW:
         obj->mesh  = Shapes_chainsaw();
-        obj->pivot = vec3(0.0f, 0.12f, 0.0f);
-        addCollider(obj, vec3(0.0f, 0.0f, 0.02f), vec3(0.18f, 0.24f, 0.74f), vec3_zero());
+        obj->pivot = vec3(0.0f, 0.09f, 0.0f); /* (the motor's underside on the ground) */
+        addCollider(obj, vec3(0.0f, 0.03f, 0.02f), vec3(0.18f, 0.24f, 0.74f), vec3_zero());
         break;
 
     case OBJ_LOG:
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 39c69d2..8457d8e 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -166,6 +166,8 @@ typedef struct Object {
     float      thrown_time;
     float      impact;        /* speed lost in collisions during the last step */
     struct Object *touched;   /* the last thing it ran into during the last step (NULL: none, or the ground) */
+    struct Object *standing_on; /* characters: what they stood on in the last step (NULL: the ground) */
+    float      veer_side;     /* the player: drifting sideways past something lying there (share of the speed, eased) */
     bool       broken;        /* shattered bottle: only shards are left */
     int        variant;       /* trees: 1 = fruit tree; trash: what it is (the palette) */
     float      fuel;          /* fireplaces: kg of wood lying in it */
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index 28a5151..7cfbc7c 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -52,8 +52,11 @@
 #define STEP_UP_MOST        0.75f  /* walking up onto something this high at most (scaled; higher: climb) ... */
 #define STEP_UP_ROOM        0.45f  /* ... with this much room on top ... */
 #define STEP_UP_IN          0.2f   /* ... the first foot this far in from its edge */
-#define STAND_LOWEST        0.22f  /* solid things this high (scaled) you step onto rather than over ... */
-#define STAND_ROOM          0.28f  /* ... a foot's width on top will do (a log: balancing) ... */
+#define STAND_LOWEST        0.18f  /* solid things this high (scaled) you step onto rather than over (lower: you just walk onto them) ... */
+#define STAND_HOLDS         4.0f   /* ... kg at least (lying on the ground) ... */
+#define STAND_ROOM          0.2f   /* ... a foot's width on top will do (a log: balancing) ... */
+#define BALANCE_SLOW        0.55f  /* balancing all the way: this much slower */
+#define STOOD_ON            0.08f  /* m: feet no lower than this under its top: standing on it, not stepping over */
 #define STAND_UP_TIME       0.4f   /* ... one step up taking this long ... */
 #define STAND_UP_HIGH       0.9f   /* ... and this much more per meter high */
 #define ONTO_HIGHEST        0.95f  /* up onto things this high at most (scaled) ... */
@@ -112,9 +115,10 @@ static bool neverOver(const Object *o) {
    bottle, a table, a ladder or a bin. */
 static bool standsOn(const Object *o) {
     if (o->collider_count == 0 || neverOver(o) || o->is_held || o->lifted || o->broken) return false;
-    if (!o->is_static && o->weight < ONTO_HOLDS) return false;
+    if (!o->is_static && o->weight < STAND_HOLDS) return false; /* (lying on the ground it holds you; too light, it slips away) */
     switch (o->type) {
     case OBJ_BOTTLE: case OBJ_VODKA: case OBJ_FURNITURE: case OBJ_BIN: case OBJ_SUPPLY: case OBJ_LADDER: case OBJ_FIREPLACE:
+    case OBJ_BRANCH: case OBJ_BAG: case OBJ_TRASH: case OBJ_AXE: case OBJ_SHEARS: case OBJ_CHAINSAW:
         return false;
     default:
         break;
@@ -123,6 +127,13 @@ static bool standsOn(const Object *o) {
     return fabsf(c->axes[0].y) > 0.95f || fabsf(c->axes[1].y) > 0.95f || fabsf(c->axes[2].y) > 0.95f;
 }
 
+float Physics_footing(const Object *o) {
+    if (o->collider_count == 0) return 1e9f;
+    const Collider *c = &o->colliders[0];
+    if (fabsf(c->axes[0].y) < 0.95f && fabsf(c->axes[1].y) < 0.95f && fabsf(c->axes[2].y) < 0.95f) return 2.0f * halfAcross(c) * 0.6f; /* (rounded or tilted: less of it) */
+    return 2.0f * halfAcross(c);
+}
+
 float Physics_lowObstacle(const Object *o) {
     if (neverOver(o) || o->is_held || o->lifted || o->collider_count == 0 || o->broken || o->thrown_by) return 0.0f;
     const Collider *c = &o->colliders[0];
@@ -141,6 +152,8 @@ bool Physics_stepsOver(const Object *who, const Object *o) {
     const Collider *c = &o->colliders[0];
     float top = Object_colliderCenter(o, 0).y + projectRadius(c, WORLD_UP);
     float feet = Object_colliderCenter(who, 0).y - projectRadius(&who->colliders[0], WORLD_UP);
+    if (top - feet < STOOD_ON) return false; /* (up on it: it holds you) */
+    if (who->type == OBJ_PLAYER && standsOn(o) && top - feet < STAND_LOWEST * s) return false; /* (flat, a plank: you just walk onto it) */
     return top - feet <= STEP_OVER_HEIGHT * s && 2.0f * halfAcross(c) <= STEP_OVER_WIDTH * s;
 }
 
@@ -202,7 +215,7 @@ static void slideAlong(Object *obj, Vec3 normal, float depth) {
  * Characters walking into a low, flat-topped obstacle step onto it.
  * Returns true if the step was taken.
  */
-static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 normal) {
+static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 normal, float most) {
     if (!obj->is_character) return false;
     /* Only walking into it, not brushing past along its side */
     Vec3 mv = vec3(obj->movement.x, 0.0f, obj->movement.z), out = vec3(normal.x, 0.0f, normal.z);
@@ -214,7 +227,7 @@ static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 nor
     float top    = Object_colliderCenter(other, ic).y + projectRadius(c, WORLD_UP);
     float bottom = Object_colliderCenter(obj, io).y - projectRadius(&obj->colliders[io], WORLD_UP);
     float rise   = top - bottom;
-    if (rise <= 0.0f || rise > obj->step_height) return false;
+    if (rise <= 0.0f || rise > most) return false;
 
     land(obj, rise);
     return true;
@@ -328,6 +341,7 @@ static void resolveContact(Object *obj, int io, Object *other, int ic, Vec3 norm
        (Walking into something, a person doesn't pop up onto it: it's in the way.) */
     if (normal.y >= WALKABLE_SLOPE && !(obj->is_character && depth / normal.y > LAND_ON_MOST)) {
         land(obj, depth / normal.y);
+        if (obj->is_character && !other->is_character) obj->standing_on = other;
         return;
     }
     if (normal.y >= WALKABLE_SLOPE) {
@@ -353,10 +367,11 @@ static void resolveContact(Object *obj, int io, Object *other, int ic, Vec3 norm
 
     /* Wall or steep slope */
     if (other->is_static) {
-        if (tryStepUp(obj, io, other, ic, normal)) return;
+        if (tryStepUp(obj, io, other, ic, normal, obj->step_height)) return;
         noteBump(obj, other, normal);
         slideAlong(obj, normal, depth);
     } else {
+        if (obj->type == OBJ_PLAYER && standsOn(other) && tryStepUp(obj, io, other, ic, normal, STAND_LOWEST)) return; /* (a plank: onto it) */
         noteBump(obj, other, normal);
         push(obj, other, normal, depth);
     }
@@ -516,7 +531,7 @@ static Vec3 steppingOver(const Object *obj, Vec3 m, Object *const *objects, int
     float speed = sqrtf(m.x * m.x + m.z * m.z);
     /* A standing leg the body would drag through what it stands behind: the
        body waits there for the other foot to come down, then that leg comes up */
-    for (int k = 0; k < 2 && vec3_length(obj->shoved) < 0.5f; k++) { /* (a load shoving into us: the legs get out of its way anyhow) */
+    for (int k = 0; k < 2 && vec3_length(obj->shoved) < 0.5f && !obj->standing_on; k++) { /* (a load shoving into us: the legs get out of its way anyhow) */
         const Foot *ft = &obj->body.feet[k];
         if (!ft->stepping && ft->into > LEG_WAIT && obj->body.feet[1 - k].stepping && speed > LEG_WAIT_SPEED &&
             speed <= STEP_OVER_RUNNING) { /* (running, the foot leaps off instead) */
@@ -533,6 +548,7 @@ static Vec3 steppingOver(const Object *obj, Vec3 m, Object *const *objects, int
     const bool jumper = canJumpOver(obj, speed);
     for (int i = 0; i < count; i++) {
         const Object *o = objects[i];
+        if (o == obj->standing_on) continue; /* (walking along on top of it) */
         if ((o->weight < STEP_OVER_LIGHT && !o->is_static) || Physics_lowObstacle(o) <= 0.0f) continue; /* (low for its own sake) */
         float jv, jc;
         if (jumper && jumpOver(obj, o, dir, speed, &jv, &jc)) continue; /* (going to jump it: no slowing down, the run-up counts) */
@@ -657,7 +673,7 @@ static bool startStepOver(Object *obj, Object *const *objects, int count, const
     if (vec3_dot(dir, Object_facing(obj)) < 0.7f) return false; /* (backing or sidling: as it comes) */
     for (int i = 0; i < count; i++) {
         const Object *o = objects[i];
-        if (o->collider_count == 0 || o->is_character || o->is_held || o->lifted) continue;
+        if (o->collider_count == 0 || o->is_character || o->is_held || o->lifted || o == obj->standing_on) continue;
         /* over it (low for its own sake), or up onto it (higher, room on top, holds you) */
         bool over = (o->weight >= STEP_OVER_LIGHT || o->is_static) && Physics_lowObstacle(o) >= OVER_MOVE_LOWEST * s;
         const Collider *c = &o->colliders[0];
@@ -669,7 +685,7 @@ static bool startStepOver(Object *obj, Object *const *objects, int count, const
         float near = ahead - reach, far = ahead + reach;
         /* up onto it: higher than a stride over, or something solid you can just as well stand on (a log, a crate) -
            no straddling then, you stand up there and walk on (people walk round; nobody walks up onto tables) */
-        bool up = obj->type == OBJ_PLAYER && standsOn(o) && top <= STEP_UP_MOST * s && side >= 0.3f * s &&
+        bool up = obj->type == OBJ_PLAYER && standsOn(o) && top <= STEP_UP_MOST * s && side >= 0.12f * s && /* (a log end-on: a narrow step) */
                   ((!over && top > STEP_OVER_HEIGHT * s && 2.0f * reach >= STEP_UP_ROOM * s) ||
                    (top >= STAND_LOWEST * s && 2.0f * reach >= STAND_ROOM * s));
         if (up) over = false;
@@ -769,35 +785,44 @@ static void hurdle(Object *obj, Object *const *objects, int count) {
     }
 }
 
-/* Walking at the end of something long lying there (a log, a fallen tree) so
-   that you'd only just catch it: a little to the side, past its end, instead
-   of over it or up onto it. */
-#define END_CLIP   0.55f /* m of you that would still catch it at most */
-#define END_LONG   1.4f  /* m: only things at least this long across your way */
+/* Walking at something you can't step over or onto (a tree, a high crate, the
+   corner of a wall, the fire) that you'd only just catch: the body drifts a
+   little to the side, past it, instead of running into it. */
+#define END_CLIP   0.7f  /* m of you that would still catch it at most (wider: you walk round it yourself) */
+#define END_PUSH   0.45f /* sideways at most this share of the walking speed ... */
+#define END_EASE   2.5f  /* ... coming and going this quickly (per second) */
 #define END_LOOK   1.6f  /* m ahead */
-#define END_BODY   0.3f  /* (your half width) */
+#define END_BODY   0.4f  /* (your half width, and the legs swinging past) */
 
-static Vec3 pastTheEnd(const Object *obj, Vec3 m, Object *const *objects, int count) {
-    float speed = sqrtf(m.x * m.x + m.z * m.z);
-    if (speed < 0.3f || obj->sitting || obj->climbing || obj->on_ladder || obj->hands.grabbed || obj->type == OBJ_DOG) return m;
+static Vec3 pastTheEnd(Object *obj, Vec3 m, Object *const *objects, int count, float dt) {
+    float speed = sqrtf(m.x * m.x + m.z * m.z), want = 0.0f;
+    if (speed < 0.3f || obj->sitting || obj->climbing || obj->on_ladder || obj->hands.grabbed || obj->type != OBJ_PLAYER) {
+        obj->veer_side = 0.0f;
+        return m;
+    }
     Vec3 dir = vec3(m.x / speed, 0.0f, m.z / speed), right = vec3(-dir.z, 0.0f, dir.x);
-    for (int i = 0; i < count; i++) {
+    for (int i = 0; i < count && want == 0.0f; i++) {
         const Object *o = objects[i];
-        if (o->collider_count == 0 || o->is_character || o->is_held || (Physics_lowObstacle(o) <= 0.0f && !standsOn(o))) continue;
+        if (o->collider_count == 0 || o->is_character || o->is_held || o == obj->standing_on || o->type == OBJ_DECAL ||
+            o->type == OBJ_PUDDLE || o->type == OBJ_STINK || (!o->is_static && o->weight < STEP_OVER_LIGHT)) continue;
         const Collider *c = &o->colliders[0];
         float side = projectRadius(c, right), reach = projectRadius(c, dir);
-        if (2.0f * side < END_LONG) continue;
         float top = Object_colliderCenter(o, 0).y + projectRadius(c, WORLD_UP) - obj->position.y;
-        if (top > STEP_UP_MOST || top < 0.1f) continue;
+        float bottom = top - 2.0f * projectRadius(c, WORLD_UP);
+        if (top < 0.1f || bottom > 1.2f) continue; /* (flat on the ground, or overhead) */
+        /* only what you can't step over or onto (a tree, a high crate, a wall's corner, the fire) */
+        bool steps = !neverOver(o) && ((Physics_lowObstacle(o) > 0.0f && top <= STEP_OVER_HEIGHT * 1.15f) || (standsOn(o) && top <= STEP_UP_MOST));
+        if (steps) continue;
         Vec3 rel = vec3_sub(Object_colliderCenter(o, 0), obj->position);
         float ahead = vec3_dot(vec3(rel.x, 0.0f, rel.z), dir) - reach, across = vec3_dot(vec3(rel.x, 0.0f, rel.z), right);
         float clip = side + END_BODY - fabsf(across);
         if (ahead < -0.05f || ahead > END_LOOK || clip <= 0.0f || clip > END_CLIP) continue;
         /* away from its middle, more the closer it gets */
-        float push = fminf(1.0f, clip / fmaxf(ahead, 0.25f)) * speed;
-        return vec3_add(m, vec3_scale(right, across > 0.0f ? -push : push));
+        want = fminf(END_PUSH, clip / fmaxf(ahead, 0.4f)) * (across > 0.0f ? -1.0f : 1.0f);
     }
-    return m;
+    /* the body drifts over to it smoothly (and back) */
+    obj->veer_side += clampf(want - obj->veer_side, -END_EASE * dt, END_EASE * dt);
+    return vec3_add(m, vec3_scale(right, obj->veer_side * speed));
 }
 
 /* ---------- Public ---------- */
@@ -852,7 +877,9 @@ void Physics_step(Object *obj, Object *const *objects, int count, const Terrain
         if (n >= 0) { objects = near; count = n; }
     }
     if (obj->is_character && obj->on_terrain) obj->movement = slopeAdjusted(obj, terrain);
-    if (obj->is_character && obj->is_grounded) obj->movement = pastTheEnd(obj, obj->movement, objects, count);
+    if (obj->is_character && obj->is_grounded && obj->body.balance > 0.0f) /* (balancing on a log: careful steps) */
+        obj->movement = vec3_scale(obj->movement, 1.0f - BALANCE_SLOW * obj->body.balance);
+    if (obj->is_character && obj->is_grounded) obj->movement = pastTheEnd(obj, obj->movement, objects, count, dt);
     if (obj->is_character && obj->is_grounded) hurdle(obj, objects, count);
     if (obj->is_character && obj->is_grounded && obj->velocity.y <= 0.0f && startStepOver(obj, objects, count, terrain)) {
         stepOverMove(obj, terrain, dt);
@@ -867,6 +894,7 @@ void Physics_step(Object *obj, Object *const *objects, int count, const Terrain
 
     bool was_grounded = obj->is_grounded;
     obj->is_grounded = false;
+    obj->standing_on = NULL;
     obj->touched = NULL;
     obj->on_terrain  = false;
     Vec3 before = obj->velocity;
diff --git a/entropie/src/world/physics.h b/entropie/src/world/physics.h
index 70f90d0..060b6a7 100644
--- a/entropie/src/world/physics.h
+++ b/entropie/src/world/physics.h
@@ -16,6 +16,8 @@
 
 /* Something low and narrow enough to step over: its height (0 = not that). */
 float Physics_lowObstacle(const Object *o);
+/* How wide the top of o is to stand on, the narrow way (m; a log: about its diameter). */
+float Physics_footing(const Object *o);
 /* Is o something low that who steps over (no collision with the body)? */
 bool Physics_stepsOver(const Object *who, const Object *o);
 
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index b485727..d0e2fd9 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -11,6 +11,9 @@
 #include <stdlib.h>
 #include <string.h>
 
+#define BALANCE_NARROW 0.3f  /* m: a top this narrow is all balancing ... */
+#define BALANCE_WIDE   0.75f /* ... this wide: none */
+
 #define TRAMPLE_MIN_SPEED 0.5f /* standing still doesn't wear the grass */
 #define BREAK_IMPACT      2.0f /* thrown bottles break when stopped this hard (m/s) */
 #define BREAK_SPEED       5.0f /* ... or when they land while still this fast */
@@ -697,6 +700,11 @@ static void animateCharacter(World *world, Object *o, float dt) {
     o->body.squat_pee = o->female && !(o->urinella && o->urinellas > 0) && !o->toilet;
     o->body.urinella_use = o->female && !o->body.squat_pee && !o->toilet;
     o->body.sit_style = o->sit_style;
+    { /* on something narrow (a log): balancing */
+        const Object *on = o->standing_on;
+        float w = on && o->is_grounded && !o->on_ladder && !o->sitting ? Physics_footing(on) : 1e9f;
+        o->body.balance_goal = 1.0f - smoothstepf(BALANCE_NARROW, BALANCE_WIDE, w);
+    }
     BodyFrame frame = {
         .position   = o->position,
         .facing     = Object_facing(o),