Nimbin[12]?Games / Entropie / commits / 2e70b67

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.79.0: helpers follow you up ladders and walls

Christian Immanuel · 2026-10-15 16:40 · 2e70b677c7ac961a272545889a2028fcbf79792d

- 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                |   3 +
 entropie/STATE.md                 |   9 +-
 entropie/commit_message.txt       |   8 +-
 entropie/src/game/game.h          |   6 -
 entropie/src/game/game_actions.c  | 276 +++-----------------------------------
 entropie/src/people/ai.c          |   3 +
 entropie/src/people/ai.h          |   8 +-
 entropie/src/people/ai_help.c     |  95 +++++++++++++
 entropie/src/people/ai_internal.h |   5 +
 entropie/src/people/climb.c       | 254 +++++++++++++++++++++++++++++++++++
 entropie/src/people/climb.h       |  31 +++++
 entropie/src/world/object.h       |  11 ++
 entropie/src/world/world.c        |   2 +-
 14 files changed, 444 insertions(+), 269 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index 3c5b0a3..5484ab5 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.78.0
+VERSION   := 1.79.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 6e90bf0..224e40e 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -359,6 +359,9 @@ A hand that can't reach its grip lets go and takes hold again.
 - Somebody holding it (hands on the rails, a foot on the bottom rung) keeps
   it steady: helpers (G) hold it when you climb, helpful people come over
   on their own when it looks wobbly
+- Helpers 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
 - Click a leaning ladder to climb: W / S up and down, at the top W gets you
   over the edge, Space jumps off. From up there, click its top to climb down
 - One hand free is enough (something up to 5 kg in the other), but you may
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 4530ef6..a6d7020 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -66,6 +66,8 @@ Smoke puffs (`chips.c` Chips_puff): small blocks drifting up, growing, drawn wit
 
 Shadows: 3x3 PCF with each tap blended bilinearly between its 4 texels (`common.glsl` shadowVisibility, 16 lookups): no stair steps at the edges.
 
+Climbing for anybody (`people/climb.c`, moved out of the game): `Climb_ledgeAhead`, `Climb_start` (says why not: hands, strength, drunk), `Climb_update` (the pull up, its feet and hands), `Climb_overLadderTop`; the move lives in the person (`climb_move`). Helpers (`ai_help.c` ai_followByLadder): the leader 1.2 m higher (standing, not climbing): a leaning ladder within 25 m from their level to the leader's (top within 1.3 m) - to its foot, up (`ai_climbing`: on a ladder or pulling over its top this runs first, whatever the mode), over the edge; none, and the leader within reach (CLIMB_REACH): walk at it and pull up where the edge ahead is at the leader's level ("Hup!"). The leader 1.2 m lower: to the top of a ladder from up there and down it; none: they just walk off (the landing does the rest). A ladder leading nowhere (no top to get off at): down again after 8 s. `Ai_useWorld` gives the AI the world for this.
+
 Night clothes (`world.c` dressForNight): `chill` 0..1 grows 1/60 s at night (21:00-7:00) away from a burning fire (drunk: slower), falls by a fire (1/60 s) or in the day (2/60 s); over 0.6 the jacket (`look.top` TOP_JACKET, colour per person: `Figure_wardrobe`) goes on, under 0.15 off. Drawn a bit bulkier, long sleeves, a zip and a darker collar. The sun logic only takes off / puts on the shirt, never the jacket. You too (`game.c` sunShirt: working = a stroke in the last 3 s).
 
 Urinella, trying: a woman who never had one (`urinella_tried` 0), bladder 0.35-0.65, chance 0.1 + 0.2 energy per think: fetches some from the box (30 m). The first one used decides: like = 0.6 - 0.3 shyness - 0.2 (pride over 0.5) + 0.2 energy; not liked: back to squatting ("not for me"). Craving: dying for a smoke (0.8-1) adds up to 0.5 x smoker to the nerves shown. A wobble rocks the ladder (`shake`, 1.2 s). A heavy piece to drag and nobody to call: the player is asked ("grab it with F").
@@ -176,7 +178,7 @@ Most important first.
 - Joint jumps left in the audit: jog+stop 191, sprint 358 (worst 28 cm) - running into things; 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: prefer a step in between on top of something stable (or unstable, if clumsy) over a long stride; NPCs: pull up or jump onto things on their own (the player does when running at them)
+- Obstacle approach, still to do: prefer a step in between on top of something stable (or unstable, if clumsy) over a long stride; 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: gesture variants, each person a bit their own; stepping over and climbing their own way; sporty ones more elegant
@@ -184,11 +186,10 @@ Most important first.
 
 **People**
 - The work place, still to do: other jobs than split / cut (felling room, trash); walking a heavy piece round tight corners
-- Ladders, still to do: a holder for a ladder standing free; NPCs climbing
+- Ladders, still to do: a holder for a ladder standing free; NPCs taking a ladder somewhere for their own work; slipping for NPCs
 - Smoking, still to do: buying more (a kiosk), an ashtray
 - Peeing, still to do: a Urinella in the hand (not drawn); the player-woman picks up her things again herself
 - "No shirt no service" policy (decision open)
-- NPCs on ladders (only you climb them)
 - Clothing, still to do: more outfits for extra people (they all wear trousers); fetching the jacket from somewhere
 
 **Tools for us**
@@ -198,4 +199,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.78.0 (build 9a82b8f)`
+State of `entropie 1.79.0 (build c478b41)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index d8e8141..29a0fce 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,5 +1,11 @@
-entropie 1.78.0: jackets at night, your shirt off, trying a Urinella
+entropie 1.79.0: helpers follow you up ladders and walls
 
+- 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
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index c1f18e2..7f3c0ca 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -46,14 +46,8 @@ typedef struct {
     Mesh     marker;        /* where a woodpile would go, shown on the ground */
     Meadow   meadow;        /* grass blades near the eye */
     float    climb_push;    /* seconds walking against a ledge */
-    float    climb_t, climb_time;
-    Vec3     climb_from, climb_to, climb_edge;
-    Vec3     climb_across;  /* along the edge (the hands hold it a shoulder width apart) */
-    float    climb_hang;    /* feet this high while hanging on before the pull up */
     float    brace_hold;    /* hands on an edge you walked up to: kept there this much longer ... */
     Vec3     brace_grip;    /* ... here */
-    float    climb_face;    /* how far ahead of where you started the face of it is (m) */
-    Vec3     climb_ankle0[2], climb_toe0[2]; /* where the feet were when the climb started (eased from there) */
     Object  *putting_up;    /* the ladder you are putting up (hands on it, standing still) */
 } Game;
 
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index 0dec5fa..787e080 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -5,6 +5,7 @@
 
 #include "game/game_internal.h"
 #include "people/pockets.h"
+#include "people/climb.h"
 
 #include <stdlib.h>
 
@@ -16,33 +17,8 @@
 enum { CLICK_NONE, CLICK_WORK, CLICK_DRINK, CLICK_BAG, CLICK_WATER };
 
 #define BUSH_OVER   1.3f   /* bushes lower than this (scaled): you try to get over, and land in it */
-#define CLIMB_EASY  1.35f  /* ledges up to this high (scaled) are climbed with a hand on them */
-#define CLIMB_REACH 2.05f  /* the hands reach this high over the feet, arms up (scaled): anything up there can be climbed */
 #define CLIMB_PUSH  0.3f   /* walking against it this long starts the climb */
 
-/* What's in front at climbing distance and could be climbed from feet_y:
-   its top (the edge), or -1e9 if nothing (too high, no room up there). */
-static float ledgeAhead(Game *game, const Object *p, Vec3 *probe_out) {
-    const float s = p->look.height / 1.75f;
-    Vec3 f = Object_facing(p);
-    float reach = p->position.y + CLIMB_REACH * s;
-    Vec3 side = vec3(-f.z, 0.0f, f.x);
-    /* right in front, or a little further (a sloping roof); or to one side (up against a corner) */
-    static const float PROBE[][2] = { { 0.45f, 0.0f }, { 0.6f, 0.0f }, { 0.45f, -0.25f }, { 0.45f, 0.25f } };
-    for (int k = 0; k < (int)(sizeof(PROBE) / sizeof(PROBE[0])); k++) {
-        Vec3 probe = vec3_add(p->position, vec3_add(vec3_scale(f, PROBE[k][0] * s), vec3_scale(side, PROBE[k][1] * s)));
-        float top = World_groundHeight(&game->world, probe.x, probe.z, reach + 0.05f);
-        float rise = top - p->position.y;
-        if (rise <= p->step_height * 0.9f || rise > CLIMB_REACH * s) continue;
-        if (PROBE[k][1] != 0.0f && rise > CLIMB_EASY * s) continue; /* (to one side: only something low enough to step up on) */
-        /* room to stand up there, and not a step beside us (the edge is ahead, not underfoot) */
-        if (World_groundHeight(&game->world, probe.x, probe.z, top + 1.8f * s) > top + 0.05f) continue;
-        *probe_out = probe;
-        return top;
-    }
-    return -1e9f;
-}
-
 #define JUST_OUT_OF_REACH 0.35f  /* an edge this much higher than the hands get: they still reach for it */
 
 /* Jumping at an edge just too high: the hands reach for it at the top of the jump, and you drop back. */
@@ -63,164 +39,6 @@ static void reachForIt(Game *game, Object *p, float s) {
     }
 }
 
-/* Where the feet are as a climb starts: they go from there (no jump onto the climb's own path). */
-static void noteFeet(Game *game, const Object *p) {
-    for (int i = 0; i < 2; i++) {
-        game->climb_ankle0[i] = p->body.feet[i].ankle;
-        game->climb_toe0[i] = p->body.feet[i].toe;
-    }
-}
-
-/* Where the body will stand up there: the highest point under its footprint,
-   as the physics stands you - no hop once up. */
-static float standHeight(Game *game, const Object *p, Vec3 to, float s) {
-    const Collider *pc = &p->colliders[0];
-    float under = to.y;
-    for (int cx = -1; cx <= 1; cx += 2)
-        for (int cz = -1; cz <= 1; cz += 2) {
-            Vec3 q = vec3_add(to, vec3_add(vec3_scale(pc->axes[0], (float)cx * pc->half_extents.x), vec3_scale(pc->axes[2], (float)cz * pc->half_extents.z)));
-            float g = World_groundHeight(&game->world, q.x, q.z, to.y + 0.4f * s);
-            if (g > under && g < to.y + 0.3f * s) under = g;
-        }
-    return under;
-}
-
-/* Hands on the edge: climb up. Easy ledges with a hand on them; higher up
-   (as far as the hands reach, standing or jumping) hanging on and pulling up. */
-static bool startClimb(Game *game, float top, Vec3 probe) {
-    Object *p = game->player;
-    const float s = p->look.height / 1.75f;
-    Vec3 f = Object_facing(p);
-    float rise = top - p->position.y;
-    bool hard = rise > CLIMB_EASY * s || !p->is_grounded;
-    if (hard) {
-        if (p->hands.state[HAND_LEFT] != HAND_FREE || p->hands.state[HAND_RIGHT] != HAND_FREE) {
-            if (game->message_time <= 0.0f) say(game, "You need both hands free to pull yourself up there");
-            return false;
-        }
-        if (p->needs.stamina < 0.12f) {
-            if (game->message_time <= 0.0f) say(game, "Too worn out to pull yourself up");
-            return false;
-        }
-        if (p->drunk > 0.75f) {
-            if (game->message_time <= 0.0f) say(game, "You hang on a moment... and drop again");
-            return false;
-        }
-    }
-    Vec3 to = vec3(probe.x + f.x * 0.25f * s, top, probe.z + f.z * 0.25f * s);
-    to.y = World_groundHeight(&game->world, to.x, to.z, top + 0.6f * s); /* (a sloping roof goes on up) */
-    if (to.y < top - 0.3f || to.y > top + 0.4f * s) to.y = top;
-    to.y = standHeight(game, p, to, s);
-    game->climb_from = p->position;
-    game->climb_to = to;
-
-    game->climb_across = vec3_normalize(vec3_cross(f, vec3(0.0f, 1.0f, 0.0f)));
-    game->climb_hang = fmaxf(p->position.y, top - 1.95f * s); /* hanging on, arms straight up */
-    game->climb_face = 0.45f * s; /* where the face of it is: the first point ahead that stands up */
-    for (float d = 0.05f; d < 0.8f * s; d += 0.02f) {
-        Vec3 q = vec3_add(p->position, vec3_scale(f, d));
-        if (World_groundHeight(&game->world, q.x, q.z, top + 0.05f) > p->position.y + 0.25f * s) {
-            game->climb_face = d;
-            break;
-        }
-    }
-    float grip = game->climb_face + 0.1f * s; /* the hands on it, just over the edge */
-    game->climb_edge = vec3(p->position.x + f.x * grip, top + 0.01f, p->position.z + f.z * grip);
-    float vigor = Object_vigor(p) * (1.0f - 0.3f * fminf(p->drunk, 1.0f));
-    float sport = 0.6f + 0.9f * p->fitness; /* (the sporty pull up much quicker, the hard ones most of all) */
-    game->climb_time = (hard ? 1.3f + 0.7f * fmaxf(rise / s - CLIMB_EASY, 0.0f) : 0.6f + rise * 0.6f) / fmaxf(vigor, 0.4f)
-                     / (hard ? sport : sqrtf(sport));
-    game->climb_t = 0.0f;
-    game->climb_push = 0.0f;
-    noteFeet(game, p);
-    p->climbing = true;
-    p->velocity = vec3_zero();
-    if (hard) p->needs.stamina = fmaxf(0.0f, p->needs.stamina - 0.04f * rise / s);
-    say(game, hard ? "You grab the edge and pull yourself up" : "You pull yourself up");
-    return true;
-}
-
-/* The feet while pulling up: against the face of it below the edge (pushing
-   up), then one knee comes up and that foot goes over onto the top, then the
-   other. Never through what you climb. */
-#define KNEE_LEG 0.45f  /* thigh and shin (scaled) ... */
-#define KNEE_HIP 0.86f  /* ... the hips this high above the feet ... */
-#define KNEE_GAP 0.08f  /* ... and the knee kept this clear of the face */
-#define LOW_RISE 0.65f /* ... the body up on it by then (of the climb) ... */
-#define LOW_EASE 0.0f  /* ... (0: rising fastest at the start) */
-#define LOW_BACK 0.25f /* climbing onto something low: the foot that pushes off stays this far back from its face */
-
-/* A foot below the edge: where the knee would be (bent forward, in the plane
-   of the climb). Too close to the face under the edge: the foot goes back
-   until the knee is clear of it. */
-static float kneeClear(float along, float y, float root_along, float root_y, float face, float top, float s) {
-    const float leg = KNEE_LEG * s, hip_a = root_along + 0.04f * s, hip_y = root_y + KNEE_HIP * s;
-    float pushed = along, knee_y = top;
-    for (int k = 0; k < 42; k++, pushed -= 0.01f * s) {
-        float dx = pushed - hip_a, dy = y - hip_y, d = sqrtf(dx * dx + dy * dy);
-        if (d < 1e-3f) continue;
-        float h = d < 2.0f * leg ? sqrtf(leg * leg - 0.25f * d * d) : 0.0f;
-        float px = dy / d, py = -dx / d; /* (perpendicular, the forward one) */
-        if (px < 0.0f) { px = -px; py = -py; }
-        float ka = hip_a + 0.5f * dx + px * h, ky = hip_y + 0.5f * dy + py * h;
-        if (k == 0) knee_y = ky;
-        if (ka < face - KNEE_GAP * s) break;
-    }
-    /* (the knee up over the top already: no need to keep the foot back - eased off, no jump) */
-    return lerpf(pushed, along, smoothstepf(top - 0.08f * s, top + 0.06f * s, knee_y));
-}
-
-static void climbFeet(Game *game, Object *p, float t, bool high, float s) {
-    const Vec3 a = game->climb_from, b = game->climb_to;
-    Vec3 f = vec3(b.x - a.x, 0.0f, b.z - a.z);
-    f = vec3_length(f) > 1e-3f ? vec3_normalize(f) : Object_facing(p);
-    const Vec3 side = game->climb_across;
-    const float top = b.y, face = game->climb_face;
-    const float ground = a.y;
-    float root_along = vec3_dot(vec3(p->position.x - a.x, 0.0f, p->position.z - a.z), f);
-    for (int i = 0; i < 2; i++) {
-        float sign = i == 0 ? -1.0f : 1.0f;
-        bool lead = i == 1; /* (the right knee comes up first) */
-        float u = high ? (lead ? smoothstepf(0.56f, 0.86f, t) : smoothstepf(0.74f, 1.0f, t)) /* (once the hips are up near the edge) */
-                       : (lead ? smoothstepf(0.12f, 0.4f, t) : smoothstepf(0.5f, 0.85f, t));
-        /* below: under you, but in front of the face, no higher than just under the edge */
-        float along_below = fminf(root_along - 0.06f * s, face - (!high && top - ground < 1.0f * s && !lead ? LOW_BACK : 0.25f) * s); /* (the knees bend forward: room for them; low, the push-off foot further back) */
-        float y_below = clampf(p->position.y + 0.08f * s, ground + 0.08f * s, top - 0.12f * s);
-        /* on top: a little over the edge, or under you once you're up there */
-        float along_top = fmaxf(face + 0.2f * s, root_along);
-        float y_top = top + 0.08f * s;
-        { /* (on what's under that foot where it ends up - the top isn't always level; the same all along: no jumps) */
-            Vec3 q = vec3_add(b, vec3_scale(side, sign * 0.11f * s));
-            float g = World_groundHeight(&game->world, q.x, q.z, top + 1.0f * s); /* (from well above: a roof slope may rise there) */
-            if (g > top - 0.6f * s) y_top = clampf(g, top - 0.3f * s, top + 0.15f * s) + 0.08f * s; /* (clamped, not cut off: no jumps on a slope) */
-        }
-        /* up first (clear of the edge), then over */
-        float up = smoothstepf(0.0f, 0.6f, u), across = smoothstepf(0.35f, 1.0f, u);
-        if (top - ground < 1.0f * s) { /* low: up and over in one swing (the knee comes over the edge) */
-            up = smoothstepf(0.0f, 0.3f, u);
-            across = smoothstepf(0.25f, 0.85f, u);
-        }
-        float along = lerpf(along_below, along_top, across);
-        float yy = lerpf(y_below, y_top, up) + 0.12f * s * sinf(PI_F * u);
-        /* over it: on it, not in it; below the edge: the knee kept clear of the face - one into the other, no jump */
-        float over = smoothstepf(face - 0.14f * s, face - 0.03f * s, along);
-        yy = lerpf(yy, fmaxf(yy, y_top), over);
-        /* (the foot up over the top already needs no room for the knee) */
-        float free = fmaxf(over, smoothstepf(top - 0.04f * s, top + 0.08f * s, yy));
-        if (free < 1.0f) along = lerpf(kneeClear(along, yy, root_along, p->position.y, face, top, s), along, free);
-        Vec3 ankle = vec3_add(a, vec3_add(vec3_scale(f, along), vec3_scale(side, sign * 0.11f * s)));
-        ankle.y = yy;
-        /* toes: at the wall pointing into it (on the toes), on top flat ahead */
-        Vec3 toe = vec3_add(ankle, vec3_scale(f, 0.14f * s));
-        toe.y = ankle.y - lerpf(0.06f, 0.05f, across) * s;
-        /* (from where they were when it started: eased onto the climb's path) */
-        float onto = smoothstepf(0.0f, 0.15f, t);
-        p->climb_ankle[i] = vec3_lerp(game->climb_ankle0[i], ankle, onto);
-        p->climb_toe[i] = vec3_lerp(game->climb_toe0[i], toe, onto);
-    }
-    p->climb_feet = true;
-}
-
 #define BRACE_FROM  0.7f   /* walking up to something this high (scaled, above the feet) ... */
 #define BRACE_NEAR  0.6f   /* ... this close: the hands go onto its edge */
 
@@ -247,58 +65,29 @@ static void braceOnEdge(Game *game, Object *p, const Input *in, float s, float d
     }
 }
 
+/* Pulls you up onto the edge at top (people/climb.c), or says why not. */
+static bool startClimb(Game *game, float top, Vec3 probe) {
+    int how = Climb_start(&game->world, game->player, top, probe);
+    static const char *const WHY[] = { [CLIMB_NO_HANDS] = "You need both hands free to pull yourself up there",
+                                       [CLIMB_NO_STRENGTH] = "Too worn out to pull yourself up",
+                                       [CLIMB_NO_GRIP] = "You hang on a moment... and drop again" };
+    if (how >= CLIMB_NO_HANDS) {
+        if (game->message_time <= 0.0f) say(game, WHY[how]);
+        return false;
+    }
+    game->climb_push = 0.0f;
+    say(game, how == CLIMB_HARD ? "You grab the edge and pull yourself up" : "You pull yourself up");
+    return true;
+}
+
 /* Walking (or jumping) against something: as long as the hands reach its
    edge you can get up there. */
 void game_climb(Game *game, const Input *in, float dt) {
     Object *p = game->player;
     if (p->on_ladder) return; /* see game_climbLadder */
     const float s = p->look.height / 1.75f;
-    if (p->climbing) {
-        game->climb_t += dt / game->climb_time;
-        float t = clampf(game->climb_t, 0.0f, 1.0f);
-        const Vec3 a = game->climb_from, b = game->climb_to;
-        const float hang = game->climb_hang;
-        bool high = hang > a.y + 0.02f || game->climb_time > 1.2f;
-        float y, over;
-        float knees = 0.0f;
-        if (high) { /* up to hang on (a little hop), hang a moment, pull up, then over the edge */
-            /* pulled up till the arms press down on it (hips near the edge), then
-               the knee comes up and you rise onto it - the hands stay on it all along */
-            float grab = smoothstepf(0.0f, 0.18f, t), pull = smoothstepf(0.28f, 0.62f, t), rise = smoothstepf(0.56f, 0.88f, t);
-            float press = fmaxf(hang, fminf(b.y, b.y - 0.75f * s));
-            y = lerpf(lerpf(lerpf(a.y, hang, grab), press, pull), b.y + 0.02f, rise);
-            over = smoothstepf(0.62f, 1.0f, t);
-            knees = 0.7f * smoothstepf(0.6f, 0.8f, t); /* a knee comes up onto the edge */
-        } else {
-            if (b.y - a.y < 1.0f * s) { /* low: up quickly first (the hips over the knee coming up, not the knee into the face) */
-                float k = clampf(t / LOW_RISE, 0.0f, 1.0f);
-                y = lerpf(a.y, b.y + 0.02f, 1.0f - (1.0f - k) * (1.0f - k) * (1.0f - LOW_EASE * k));
-            } else {
-                y = lerpf(a.y, b.y + 0.02f, smoothstepf(0.0f, 0.7f, t));
-            }
-            over = smoothstepf(0.45f, 1.0f, t);
-        }
-        p->position = vec3(lerpf(a.x, b.x, over), y, lerpf(a.z, b.z, over));
-        p->velocity = vec3_zero();
-        Object_syncColliders(p);
-        climbFeet(game, p, t, high, s);
-        if (high && t < 0.84f) { /* the hands come up in front of the face first, then over onto the edge */
-            Vec3 fwd = vec3(b.x - a.x, 0.0f, b.z - a.z);
-            fwd = vec3_length(fwd) > 1e-3f ? vec3_normalize(fwd) : Object_facing(p);
-            /* (up wide, outside the shoulders - pressed against it there's no room in front - then in onto it) */
-            float onto = smoothstepf(0.05f, 0.16f, t);
-            Vec3 before = vec3_add(game->climb_edge, vec3_add(vec3_scale(fwd, -0.12f * s), vec3(0.0f, 0.1f * s, 0.0f)));
-            /* (coming over the edge the hands go on ahead onto the top: pressed flat on it, under the shoulders) */
-            Vec3 on = vec3_add(game->climb_edge, vec3_scale(fwd, 0.35f * s * smoothstepf(0.55f, 0.9f, t)));
-            Hands_holdEdge(p, vec3_lerp(before, on, onto),vec3_scale(game->climb_across, lerpf(1.9f, 1.0f, onto)), knees);
-        }
-        else if (t < 0.8f) Hands_touch(p, game->climb_edge);
-        if (t >= 1.0f) {
-            p->climbing = false;
-            p->is_grounded = true;
-            /* (the feet are on it already - climbFeet - and stay put: no replanting, no jump) */
-            p->body.velocity = vec3_zero(); /* (standing there, not a stride ahead) */
-        }
+    if (p->climbing) { /* pulling up (people/climb.c) */
+        Climb_update(&game->world, p, dt);
         return;
     }
     if (Body_isBusy(&p->body) || Hands_isGrabbing(p) || p->sitting) { game->climb_push = 0.0f; return; }
@@ -309,7 +98,7 @@ void game_climb(Game *game, const Input *in, float dt) {
         game->climb_push = 0.0f;
         /* (walking at it, or holding Space: trying to grab it) */
         if ((in->move_forward < 0.3f && !in->jump) || p->body.air_time < 0.08f) return;
-        float top = ledgeAhead(game, p, &probe);
+        float top = Climb_ledgeAhead(&game->world, p, &probe);
         if (top > -1e8f && top > p->position.y + 0.3f * s) { startClimb(game, top, probe); return; }
         reachForIt(game, p, s);
         return;
@@ -327,7 +116,7 @@ void game_climb(Game *game, const Input *in, float dt) {
         say(game, snapped ? "You try to step over the bush... and end up in it. Twigs snap" : "You end up in the bush");
         return;
     }
-    float top = ledgeAhead(game, p, &probe);
+    float top = Climb_ledgeAhead(&game->world, p, &probe);
     if (top < -1e8f) {
         /* just out of reach: say so (once) */
         if (game->climb_push < 1.0f || game->climb_push - dt >= 1.0f) return;
@@ -436,33 +225,10 @@ void game_climbLadder(Game *game, const Input *in, float dt) {
     if (oneHandSlip(game, p, l, in->move_forward != 0.0f, dt)) return;
     if (Ladder_climb(p, in->move_forward, dt) != CLIMB_AT_EDGE) return;
     /* Over the edge onto whatever it leans on, if there's room to stand */
-    Vec3 n = l->lean_normal, edge = Ladder_edge(l), over = edge;
-    float top = -1e9f;
-    static const float INWARD[] = { 1.0f, 0.6f, 0.35f }; /* well onto it if there's room (a roof), else a narrow top */
-    for (int k = 0; k < 3; k++) {
-        over = vec3_sub(edge, vec3_scale(n, INWARD[k]));
-        top = World_groundHeight(&game->world, over.x, over.z, edge.y + 1.5f);
-        if (top > edge.y - 0.6f && World_groundHeight(&game->world, over.x, over.z, top + 1.8f) <= top + 0.05f) break;
-        top = -1e9f;
-    }
-    if (top < -1e8f) {
+    if (!Climb_overLadderTop(&game->world, p, l)) {
         if (game->message_time <= 0.0f) say(game, "Nothing to stand on up there");
         return;
     }
-    noteFeet(game, p);
-    Ladder_getOff(p);
-    p->climbing = true; /* pulled over the edge like any ledge (see game_climb) */
-    game->climb_from = p->position;
-    game->climb_to = vec3(over.x, standHeight(game, p, vec3(over.x, top, over.z), p->look.height / 1.75f), over.z);
-    game->climb_edge = edge;
-    game->climb_hang = p->position.y; /* (already up there: no hanging on) */
-    game->climb_across = vec3_normalize(vec3_cross(Object_facing(p), vec3(0.0f, 1.0f, 0.0f)));
-    { /* (the face of it: where the ladder's top rests) */
-        Vec3 to_edge = vec3_sub(edge, p->position);
-        game->climb_face = fmaxf(0.05f, -(to_edge.x * n.x + to_edge.z * n.z));
-    }
-    game->climb_time = 1.6f / (0.6f + 0.9f * p->fitness); /* (pulled up over it like a high ledge: from the ladder the feet start well below; quicker sporty) */
-    game->climb_t = 0.0f;
     say(game, "You climb over the edge");
 }
 
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 6b26780..b99f9ee 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -62,6 +62,8 @@ static bool findPeeSpot(Object *self, Object *const *objects, int count);
 static struct Grid *g_grid;
 
 void Ai_useGrid(struct Grid *grid) { g_grid = grid; }
+struct World *g_world;
+void Ai_useWorld(struct World *world) { g_world = world; }
 
 bool ai_blockedBy(const Object *self, const Object *ignore, const Object *ignore2,
                       Object *const *objects, int count, Vec3 p) {
@@ -1280,6 +1282,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     if (takeBack(self)) return;
     lighterManners(self, objects, count, dt);
     smokeBreak(self, objects, count, dt);
+    if (ai_climbing(self, dt)) return; /* (up or down a ladder, over an edge: that first) */
     if (ai->mode != AI_IDLE && !(ai->mode == AI_WORK && ai->order == ORDER_FIRE)) self->sitting = false; /* up again to do anything */
     smell(self, objects, count, dt);
     if (ai->talking) {
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index a76d7c9..12910cb 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -175,7 +175,10 @@ typedef struct {
     struct Object *prep_item;   /* ... what's being cleared away */
     struct Object *prep_helper; /* ... who was called over to help move it */
     float          prep_time;
-    float          hold_idle;   /* holding a ladder nobody is on: seconds of that (then they let go) */
+    float          hold_idle;
+    float          ladder_dir;  /* on a ladder: +1 going up, -1 down */
+    float          ladder_seek; /* until they look for a way up / down again */
+    float          ladder_climb_t; /* seconds up a ladder that leads nowhere */   /* holding a ladder nobody is on: seconds of that (then they let go) */
     float          sun_heat;    /* seconds of hard work in the midday sun (the shirt comes off) */
     float          sniff_cooldown; /* until the next remark about a smell */
     AiMode         before;      /* mode to go back to after being shown something */
@@ -235,7 +238,10 @@ bool  Ai_call(struct Object *self, const struct Object *player);
 
 /* Where things are (the world's cell grid): steering only looks at what's near. NULL: everything. */
 struct Grid;
+struct World;
 void Ai_useGrid(struct Grid *grid);
+/* The world (for climbing: ladders, edges). */
+void Ai_useWorld(struct World *world);
 
 /* Before physics: chooses what to do and sets movement and facing. */
 void Ai_update(struct Object *self, struct Object *const *objects, int count, float dt);
diff --git a/entropie/src/people/ai_help.c b/entropie/src/people/ai_help.c
index d3a4bc5..318c996 100644
--- a/entropie/src/people/ai_help.c
+++ b/entropie/src/people/ai_help.c
@@ -6,6 +6,7 @@
 
 #include "people/ai_internal.h"
 #include "world/ladder.h"
+#include "people/climb.h"
 
 /* ---------- Helping ---------- */
 
@@ -475,6 +476,9 @@ void ai_updateHelp(Object *self, Object *const *objects, int count, float dt) {
         }
     }
 
+    /* The leader went up a ladder (or came down): after them */
+    if (!target && !ai->pushing && ai_followByLadder(self, objects, count, leader, dt)) return;
+
     /* Wood work: understand what the leader is up to and carry on with it */
     if (!target && !ai->pushing && helpWood(self, objects, count, dt)) return;
     /* Not sure what it's about: do the same as the leader */
@@ -628,3 +632,94 @@ bool ai_holdLadder(Object *self, Object *const *objects, int count, float dt) {
     turnTowards(self, facing, dt * 2.0f);
     return true;
 }
+
+/* ---------- Up and down ladders ---------- */
+
+#define LADDER_UP_FROM  1.2f   /* the leader this much higher (or lower): by the ladder */
+#define LADDER_SEEK     25.0f  /* m: a ladder this near is worth walking to */
+
+bool ai_climbing(Object *self, float dt) {
+    Ai *ai = &self->ai;
+    if (self->climbing && !self->on_ladder) { /* over the edge at the top */
+        if (g_world) Climb_update(g_world, self, dt);
+        return true;
+    }
+    Object *l = self->on_ladder;
+    if (!l) return false;
+    if (!l->leaning && l->placing <= 0.0f) { Ladder_getOff(self); return false; } /* (it fell: see Ladder_topple) */
+    if (ai->ladder_dir == 0.0f) ai->ladder_dir = -1.0f;
+    int r = Ladder_climb(self, ai->ladder_dir, dt);
+    if (r == CLIMB_AT_EDGE && (!g_world || !Climb_overLadderTop(g_world, self, l))) ai->ladder_dir = -1.0f; /* (nothing up there: down again) */
+    if (ai->ladder_dir > 0.0f && l->lean_top <= 0.0f && r != CLIMB_AT_EDGE && self->ladder_at > 0.5f) { /* (no getting off at the top: down again) */
+        ai->ladder_climb_t += dt;
+        if (ai->ladder_climb_t > 8.0f) ai->ladder_dir = -1.0f;
+    }
+    return self->on_ladder != NULL || self->climbing;
+}
+
+/* A leaning ladder from about here (feet) up to about there (top), nobody on it. */
+static Object *ladderBetween(const Object *self, Object *const *objects, int count, Vec3 low, Vec3 high, bool from_top) {
+    Object *best = NULL;
+    float best_d = LADDER_SEEK;
+    for (int i = 0; i < count; i++) {
+        Object *l = objects[i];
+        if (l->type != OBJ_LADDER || !l->leaning || l->lean_top <= 0.0f || l->tip_v > 0.0f || climberOn(objects, count, l)) continue;
+        Vec3 edge = Ladder_edge(l);
+        if (fabsf(l->position.y - low.y) > 0.6f || fabsf(edge.y - high.y) > 1.3f) continue;
+        Vec3 near = from_top ? edge : l->position;
+        float d = flatDistance(near, self->position);
+        if (d < best_d) { best_d = d; best = l; }
+    }
+    return best;
+}
+
+bool ai_followByLadder(Object *self, Object *const *objects, int count, const Object *leader, float dt) {
+    Ai *ai = &self->ai;
+    if (!g_world || !leader || leader->on_ladder || leader->climbing || !leader->is_grounded || self->holding_ladder) return false;
+    float up = leader->position.y - self->position.y;
+    if (fabsf(up) < LADDER_UP_FROM) return false;
+    if (ai->ladder_seek > 0.0f) { ai->ladder_seek -= dt; return false; }
+    bool going_up = up > 0.0f;
+    Object *l = going_up ? ladderBetween(self, objects, count, self->position, leader->position, false)
+                         : ladderBetween(self, objects, count, leader->position, self->position, true);
+    if (!l && going_up && up < CLIMB_REACH * self->look.height / 1.75f + 0.2f) { /* no ladder, but in reach: pull up */
+        Vec3 probe;
+        float top = Climb_ledgeAhead(g_world, self, &probe);
+        if (top > -1e8f && fabsf(top - leader->position.y) < 0.6f && self->needs.stamina > 0.2f && self->drunk < 0.6f) {
+            freeHands(self, NULL);
+            int how = Climb_start(g_world, self, top, probe);
+            if (how == CLIMB_HARD || how == CLIMB_EASY_UP) {
+                if (how == CLIMB_HARD && self->ai.speech_time <= 0.0f) Ai_say(self, "Hup!");
+                return true;
+            }
+        }
+        return false; /* (walks on towards them: up against it, the ledge is ahead) */
+    }
+    if (!l) { ai->ladder_seek = 3.0f; return false; }
+    if (going_up) { /* to its foot, then up */
+        Vec3 foot = vec3_add(l->position, vec3_scale(l->lean_normal, 0.5f));
+        float d = flatDistance(foot, self->position);
+        if (d > 0.45f) {
+            Vec3 dir = ai_steer(self, l, objects, count, foot);
+            if (vec3_length(dir) < 0.5f) stand(self);
+            else ai_walk(self, dir, fminf(WALK_SPEED, d * 2.0f + 0.3f), dt);
+            return true;
+        }
+        freeHands(self, NULL);
+        Ladder_mount(self, l, false);
+    } else { /* to its top, then down */
+        Vec3 edge = Ladder_edge(l);
+        Vec3 spot = vec3_sub(edge, vec3_scale(l->lean_normal, 0.5f));
+        float d = flatDistance(spot, self->position);
+        if (d > 0.5f) { /* (up there: straight to it) */
+            Vec3 dir = vec3(spot.x - self->position.x, 0.0f, spot.z - self->position.z);
+            ai_walk(self, vec3_scale(dir, 1.0f / fmaxf(d, 1e-3f)), fminf(WALK_SPEED * 0.6f, d * 2.0f + 0.2f), dt);
+            return true;
+        }
+        freeHands(self, NULL);
+        Ladder_mount(self, l, true);
+    }
+    ai->ladder_dir = going_up ? 1.0f : -1.0f;
+    stand(self);
+    return true;
+}
diff --git a/entropie/src/people/ai_internal.h b/entropie/src/people/ai_internal.h
index fa19c43..b3b1f5c 100644
--- a/entropie/src/people/ai_internal.h
+++ b/entropie/src/people/ai_internal.h
@@ -94,6 +94,11 @@ void ai_updateHelp(Object *self, Object *const *objects, int count, float dt);
 /* Holding a ladder steady for somebody on it: helpers for their leader, the
    helpful on their own when it looks wobbly. True while they're at it. */
 bool ai_holdLadder(Object *self, Object *const *objects, int count, float dt);
+/* Up or down a ladder, or pulling up over its top edge: carries on with it. True while at it. */
+bool ai_climbing(Object *self, float dt);
+/* Following someone up there (a roof) by a leaning ladder, or down again. True while on the way. */
+bool ai_followByLadder(Object *self, Object *const *objects, int count, const Object *leader, float dt);
+extern struct World *g_world;
 void ai_ensureReady(Object *self);
 
 static inline float random01(Ai *ai) {
diff --git a/entropie/src/people/climb.c b/entropie/src/people/climb.c
new file mode 100644
index 0000000..424f66a
--- /dev/null
+++ b/entropie/src/people/climb.c
@@ -0,0 +1,254 @@
+#include "people/climb.h"
+#include "world/ladder.h"
+
+#include <math.h>
+
+/* What's in front at climbing distance and could be climbed from feet_y:
+   its top (the edge), or -1e9 if nothing (too high, no room up there). */
+float Climb_ledgeAhead(const World *world, const Object *p, Vec3 *probe_out) {
+    const float s = p->look.height / 1.75f;
+    Vec3 f = Object_facing(p);
+    float reach = p->position.y + CLIMB_REACH * s;
+    Vec3 side = vec3(-f.z, 0.0f, f.x);
+    /* right in front, or a little further (a sloping roof); or to one side (up against a corner) */
+    static const float PROBE[][2] = { { 0.45f, 0.0f }, { 0.6f, 0.0f }, { 0.45f, -0.25f }, { 0.45f, 0.25f } };
+    for (int k = 0; k < (int)(sizeof(PROBE) / sizeof(PROBE[0])); k++) {
+        Vec3 probe = vec3_add(p->position, vec3_add(vec3_scale(f, PROBE[k][0] * s), vec3_scale(side, PROBE[k][1] * s)));
+        float top = World_groundHeight(world, probe.x, probe.z, reach + 0.05f);
+        float rise = top - p->position.y;
+        if (rise <= p->step_height * 0.9f || rise > CLIMB_REACH * s) continue;
+        if (PROBE[k][1] != 0.0f && rise > CLIMB_EASY * s) continue; /* (to one side: only something low enough to step up on) */
+        /* room to stand up there, and not a step beside us (the edge is ahead, not underfoot) */
+        if (World_groundHeight(world, probe.x, probe.z, top + 1.8f * s) > top + 0.05f) continue;
+        *probe_out = probe;
+        return top;
+    }
+    return -1e9f;
+}
+
+/* Where the feet are as a climb starts: they go from there (no jump onto the climb's own path). */
+void Climb_noteFeet(Object *p) {
+    for (int i = 0; i < 2; i++) {
+        p->climb_move.ankle0[i] = p->body.feet[i].ankle;
+        p->climb_move.toe0[i] = p->body.feet[i].toe;
+    }
+}
+
+/* Where the body will stand up there: the highest point under its footprint,
+   as the physics stands you - no hop once up. */
+float Climb_standHeight(const World *world, const Object *p, Vec3 to, float s) {
+    const Collider *pc = &p->colliders[0];
+    float under = to.y;
+    for (int cx = -1; cx <= 1; cx += 2)
+        for (int cz = -1; cz <= 1; cz += 2) {
+            Vec3 q = vec3_add(to, vec3_add(vec3_scale(pc->axes[0], (float)cx * pc->half_extents.x), vec3_scale(pc->axes[2], (float)cz * pc->half_extents.z)));
+            float g = World_groundHeight(world, q.x, q.z, to.y + 0.4f * s);
+            if (g > under && g < to.y + 0.3f * s) under = g;
+        }
+    return under;
+}
+
+/* Hands on the edge: climb up. Easy ledges with a hand on them; higher up
+   (as far as the hands reach, standing or jumping) hanging on and pulling up. */
+int Climb_start(const World *world, Object *p, float top, Vec3 probe) {
+    const float s = p->look.height / 1.75f;
+    Vec3 f = Object_facing(p);
+    float rise = top - p->position.y;
+    bool hard = rise > CLIMB_EASY * s || !p->is_grounded;
+    if (hard) {
+        if (p->hands.state[HAND_LEFT] != HAND_FREE || p->hands.state[HAND_RIGHT] != HAND_FREE) return CLIMB_NO_HANDS;
+        if (p->needs.stamina < 0.12f) return CLIMB_NO_STRENGTH;
+        if (p->drunk > 0.75f) return CLIMB_NO_GRIP;
+    }
+    Vec3 to = vec3(probe.x + f.x * 0.25f * s, top, probe.z + f.z * 0.25f * s);
+    to.y = World_groundHeight(world, to.x, to.z, top + 0.6f * s); /* (a sloping roof goes on up) */
+    if (to.y < top - 0.3f || to.y > top + 0.4f * s) to.y = top;
+    to.y = Climb_standHeight(world, p, to, s);
+    p->climb_move.from = p->position;
+    p->climb_move.to = to;
+
+    p->climb_move.across = vec3_normalize(vec3_cross(f, vec3(0.0f, 1.0f, 0.0f)));
+    p->climb_move.hang = fmaxf(p->position.y, top - 1.95f * s); /* hanging on, arms straight up */
+    p->climb_move.face = 0.45f * s; /* where the face of it is: the first point ahead that stands up */
+    for (float d = 0.05f; d < 0.8f * s; d += 0.02f) {
+        Vec3 q = vec3_add(p->position, vec3_scale(f, d));
+        if (World_groundHeight(world, q.x, q.z, top + 0.05f) > p->position.y + 0.25f * s) {
+            p->climb_move.face = d;
+            break;
+        }
+    }
+    float grip = p->climb_move.face + 0.1f * s; /* the hands on it, just over the edge */
+    p->climb_move.edge = vec3(p->position.x + f.x * grip, top + 0.01f, p->position.z + f.z * grip);
+    float vigor = Object_vigor(p) * (1.0f - 0.3f * fminf(p->drunk, 1.0f));
+    float sport = 0.6f + 0.9f * p->fitness; /* (the sporty pull up much quicker, the hard ones most of all) */
+    p->climb_move.time = (hard ? 1.3f + 0.7f * fmaxf(rise / s - CLIMB_EASY, 0.0f) : 0.6f + rise * 0.6f) / fmaxf(vigor, 0.4f)
+                     / (hard ? sport : sqrtf(sport));
+    p->climb_move.t = 0.0f;
+    Climb_noteFeet(p);
+    p->climbing = true;
+    p->velocity = vec3_zero();
+    if (hard) p->needs.stamina = fmaxf(0.0f, p->needs.stamina - 0.04f * rise / s);
+    return hard ? CLIMB_HARD : CLIMB_EASY_UP;
+}
+
+/* The feet while pulling up: against the face of it below the edge (pushing
+   up), then one knee comes up and that foot goes over onto the top, then the
+   other. Never through what you climb. */
+#define KNEE_LEG 0.45f  /* thigh and shin (scaled) ... */
+#define KNEE_HIP 0.86f  /* ... the hips this high above the feet ... */
+#define KNEE_GAP 0.08f  /* ... and the knee kept this clear of the face */
+#define LOW_RISE 0.65f /* ... the body up on it by then (of the climb) ... */
+#define LOW_EASE 0.0f  /* ... (0: rising fastest at the start) */
+#define LOW_BACK 0.25f /* climbing onto something low: the foot that pushes off stays this far back from its face */
+
+/* A foot below the edge: where the knee would be (bent forward, in the plane
+   of the climb). Too close to the face under the edge: the foot goes back
+   until the knee is clear of it. */
+static float kneeClear(float along, float y, float root_along, float root_y, float face, float top, float s) {
+    const float leg = KNEE_LEG * s, hip_a = root_along + 0.04f * s, hip_y = root_y + KNEE_HIP * s;
+    float pushed = along, knee_y = top;
+    for (int k = 0; k < 42; k++, pushed -= 0.01f * s) {
+        float dx = pushed - hip_a, dy = y - hip_y, d = sqrtf(dx * dx + dy * dy);
+        if (d < 1e-3f) continue;
+        float h = d < 2.0f * leg ? sqrtf(leg * leg - 0.25f * d * d) : 0.0f;
+        float px = dy / d, py = -dx / d; /* (perpendicular, the forward one) */
+        if (px < 0.0f) { px = -px; py = -py; }
+        float ka = hip_a + 0.5f * dx + px * h, ky = hip_y + 0.5f * dy + py * h;
+        if (k == 0) knee_y = ky;
+        if (ka < face - KNEE_GAP * s) break;
+    }
+    /* (the knee up over the top already: no need to keep the foot back - eased off, no jump) */
+    return lerpf(pushed, along, smoothstepf(top - 0.08f * s, top + 0.06f * s, knee_y));
+}
+
+static void climbFeet(const World *world, Object *p, float t, bool high, float s) {
+    const Vec3 a = p->climb_move.from, b = p->climb_move.to;
+    Vec3 f = vec3(b.x - a.x, 0.0f, b.z - a.z);
+    f = vec3_length(f) > 1e-3f ? vec3_normalize(f) : Object_facing(p);
+    const Vec3 side = p->climb_move.across;
+    const float top = b.y, face = p->climb_move.face;
+    const float ground = a.y;
+    float root_along = vec3_dot(vec3(p->position.x - a.x, 0.0f, p->position.z - a.z), f);
+    for (int i = 0; i < 2; i++) {
+        float sign = i == 0 ? -1.0f : 1.0f;
+        bool lead = i == 1; /* (the right knee comes up first) */
+        float u = high ? (lead ? smoothstepf(0.56f, 0.86f, t) : smoothstepf(0.74f, 1.0f, t)) /* (once the hips are up near the edge) */
+                       : (lead ? smoothstepf(0.12f, 0.4f, t) : smoothstepf(0.5f, 0.85f, t));
+        /* below: under you, but in front of the face, no higher than just under the edge */
+        float along_below = fminf(root_along - 0.06f * s, face - (!high && top - ground < 1.0f * s && !lead ? LOW_BACK : 0.25f) * s); /* (the knees bend forward: room for them; low, the push-off foot further back) */
+        float y_below = clampf(p->position.y + 0.08f * s, ground + 0.08f * s, top - 0.12f * s);
+        /* on top: a little over the edge, or under you once you're up there */
+        float along_top = fmaxf(face + 0.2f * s, root_along);
+        float y_top = top + 0.08f * s;
+        { /* (on what's under that foot where it ends up - the top isn't always level; the same all along: no jumps) */
+            Vec3 q = vec3_add(b, vec3_scale(side, sign * 0.11f * s));
+            float g = World_groundHeight(world, q.x, q.z, top + 1.0f * s); /* (from well above: a roof slope may rise there) */
+            if (g > top - 0.6f * s) y_top = clampf(g, top - 0.3f * s, top + 0.15f * s) + 0.08f * s; /* (clamped, not cut off: no jumps on a slope) */
+        }
+        /* up first (clear of the edge), then over */
+        float up = smoothstepf(0.0f, 0.6f, u), across = smoothstepf(0.35f, 1.0f, u);
+        if (top - ground < 1.0f * s) { /* low: up and over in one swing (the knee comes over the edge) */
+            up = smoothstepf(0.0f, 0.3f, u);
+            across = smoothstepf(0.25f, 0.85f, u);
+        }
+        float along = lerpf(along_below, along_top, across);
+        float yy = lerpf(y_below, y_top, up) + 0.12f * s * sinf(PI_F * u);
+        /* over it: on it, not in it; below the edge: the knee kept clear of the face - one into the other, no jump */
+        float over = smoothstepf(face - 0.14f * s, face - 0.03f * s, along);
+        yy = lerpf(yy, fmaxf(yy, y_top), over);
+        /* (the foot up over the top already needs no room for the knee) */
+        float free = fmaxf(over, smoothstepf(top - 0.04f * s, top + 0.08f * s, yy));
+        if (free < 1.0f) along = lerpf(kneeClear(along, yy, root_along, p->position.y, face, top, s), along, free);
+        Vec3 ankle = vec3_add(a, vec3_add(vec3_scale(f, along), vec3_scale(side, sign * 0.11f * s)));
+        ankle.y = yy;
+        /* toes: at the wall pointing into it (on the toes), on top flat ahead */
+        Vec3 toe = vec3_add(ankle, vec3_scale(f, 0.14f * s));
+        toe.y = ankle.y - lerpf(0.06f, 0.05f, across) * s;
+        /* (from where they were when it started: eased onto the climb's path) */
+        float onto = smoothstepf(0.0f, 0.15f, t);
+        p->climb_ankle[i] = vec3_lerp(p->climb_move.ankle0[i], ankle, onto);
+        p->climb_toe[i] = vec3_lerp(p->climb_move.toe0[i], toe, onto);
+    }
+    p->climb_feet = true;
+}
+
+
+bool Climb_update(const World *world, Object *p, float dt) {
+    const float s = p->look.height / 1.75f;
+    ClimbMove *c = &p->climb_move;
+    c->t += dt / c->time;
+    float t = clampf(c->t, 0.0f, 1.0f);
+    const Vec3 a = c->from, b = c->to;
+    const float hang = c->hang;
+    bool high = hang > a.y + 0.02f || c->time > 1.2f;
+    float y, over;
+    float knees = 0.0f;
+    if (high) { /* up to hang on (a little hop), hang a moment, pull up, then over the edge */
+        /* pulled up till the arms press down on it (hips near the edge), then
+           the knee comes up and you rise onto it - the hands stay on it all along */
+        float grab = smoothstepf(0.0f, 0.18f, t), pull = smoothstepf(0.28f, 0.62f, t), rise = smoothstepf(0.56f, 0.88f, t);
+        float press = fmaxf(hang, fminf(b.y, b.y - 0.75f * s));
+        y = lerpf(lerpf(lerpf(a.y, hang, grab), press, pull), b.y + 0.02f, rise);
+        over = smoothstepf(0.62f, 1.0f, t);
+        knees = 0.7f * smoothstepf(0.6f, 0.8f, t); /* a knee comes up onto the edge */
+    } else {
+        if (b.y - a.y < 1.0f * s) { /* low: up quickly first (the hips over the knee coming up, not the knee into the face) */
+            float k = clampf(t / LOW_RISE, 0.0f, 1.0f);
+            y = lerpf(a.y, b.y + 0.02f, 1.0f - (1.0f - k) * (1.0f - k) * (1.0f - LOW_EASE * k));
+        } else {
+            y = lerpf(a.y, b.y + 0.02f, smoothstepf(0.0f, 0.7f, t));
+        }
+        over = smoothstepf(0.45f, 1.0f, t);
+    }
+    p->position = vec3(lerpf(a.x, b.x, over), y, lerpf(a.z, b.z, over));
+    p->velocity = vec3_zero();
+    Object_syncColliders(p);
+    climbFeet(world, p, t, high, s);
+    if (high && t < 0.84f) { /* the hands come up in front of the face first, then over onto the edge */
+        Vec3 fwd = vec3(b.x - a.x, 0.0f, b.z - a.z);
+        fwd = vec3_length(fwd) > 1e-3f ? vec3_normalize(fwd) : Object_facing(p);
+        /* (up wide, outside the shoulders - pressed against it there's no room in front - then in onto it) */
+        float onto = smoothstepf(0.05f, 0.16f, t);
+        Vec3 before = vec3_add(c->edge, vec3_add(vec3_scale(fwd, -0.12f * s), vec3(0.0f, 0.1f * s, 0.0f)));
+        /* (coming over the edge the hands go on ahead onto the top: pressed flat on it, under the shoulders) */
+        Vec3 on = vec3_add(c->edge, vec3_scale(fwd, 0.35f * s * smoothstepf(0.55f, 0.9f, t)));
+        Hands_holdEdge(p, vec3_lerp(before, on, onto), vec3_scale(c->across, lerpf(1.9f, 1.0f, onto)), knees);
+    }
+    else if (t < 0.8f) Hands_touch(p, c->edge);
+    if (t >= 1.0f) {
+        p->climbing = false;
+        p->is_grounded = true;
+        /* (the feet are on it already - climbFeet - and stay put: no replanting, no jump) */
+        p->body.velocity = vec3_zero(); /* (standing there, not a stride ahead) */
+    }
+    return !p->climbing;
+}
+
+bool Climb_overLadderTop(const World *world, Object *p, const Object *ladder) {
+    Vec3 n = ladder->lean_normal, edge = Ladder_edge(ladder), over = edge;
+    float top = -1e9f;
+    static const float INWARD[] = { 1.0f, 0.6f, 0.35f }; /* well onto it if there's room (a roof), else a narrow top */
+    for (int k = 0; k < 3; k++) {
+        over = vec3_sub(edge, vec3_scale(n, INWARD[k]));
+        top = World_groundHeight(world, over.x, over.z, edge.y + 1.5f);
+        if (top > edge.y - 0.6f && World_groundHeight(world, over.x, over.z, top + 1.8f) <= top + 0.05f) break;
+        top = -1e9f;
+    }
+    if (top < -1e8f) return false;
+    Climb_noteFeet(p);
+    Ladder_getOff(p);
+    p->climbing = true; /* pulled over the edge like any ledge */
+    ClimbMove *c = &p->climb_move;
+    c->from = p->position;
+    c->to = vec3(over.x, Climb_standHeight(world, p, vec3(over.x, top, over.z), p->look.height / 1.75f), over.z);
+    c->edge = edge;
+    c->hang = p->position.y; /* (already up there: no hanging on) */
+    c->across = vec3_normalize(vec3_cross(Object_facing(p), vec3(0.0f, 1.0f, 0.0f)));
+    { /* (the face of it: where the ladder's top rests) */
+        Vec3 to_edge = vec3_sub(edge, p->position);
+        c->face = fmaxf(0.05f, -(to_edge.x * n.x + to_edge.z * n.z));
+    }
+    c->time = 1.6f / (0.6f + 0.9f * p->fitness); /* (pulled up over it like a high ledge: from the ladder the feet start well below; quicker sporty) */
+    c->t = 0.0f;
+    return true;
+}
diff --git a/entropie/src/people/climb.h b/entropie/src/people/climb.h
new file mode 100644
index 0000000..04d0109
--- /dev/null
+++ b/entropie/src/people/climb.h
@@ -0,0 +1,31 @@
+#ifndef PEOPLE_CLIMB_H
+#define PEOPLE_CLIMB_H
+
+/*
+ * Pulling up onto something (a ledge, a wall, over the top of a ladder):
+ * anybody, the player or the others. Climb_start sets the move up (in the
+ * person's climb_move), Climb_update plays it: up, the hands on the edge,
+ * the feet against the face, a knee over, standing on top.
+ */
+
+#include "world/world.h"
+
+#define CLIMB_EASY  1.35f  /* ledges up to this high (scaled) are climbed with a hand on them */
+#define CLIMB_REACH 2.05f  /* the hands reach this high over the feet, arms up (scaled): anything up there can be climbed */
+
+enum { CLIMB_EASY_UP, CLIMB_HARD, CLIMB_NO_HANDS, CLIMB_NO_STRENGTH, CLIMB_NO_GRIP };
+
+/* What's in front at climbing distance and could be climbed: its top (the edge), or -1e9. */
+float Climb_ledgeAhead(const World *world, const Object *p, Vec3 *probe_out);
+/* Where the feet are as a climb starts (it eases on from there). */
+void  Climb_noteFeet(Object *p);
+/* Where the body will stand up there (the highest point under its footprint). */
+float Climb_standHeight(const World *world, const Object *p, Vec3 to, float s);
+/* Starts pulling up onto the edge at top (probe: where it was found). CLIMB_EASY_UP / CLIMB_HARD: started; else why not. */
+int   Climb_start(const World *world, Object *p, float top, Vec3 probe);
+/* From the top of a leaning ladder over its edge onto what it leans on: false if there's nothing to stand on. */
+bool  Climb_overLadderTop(const World *world, Object *p, const Object *ladder);
+/* Plays the pull up; true when it's done (standing on top). */
+bool  Climb_update(const World *world, Object *p, float dt);
+
+#endif /* PEOPLE_CLIMB_H */
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index cb8cc95..30e9d55 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -102,6 +102,16 @@ typedef struct {
 #define POCKET_URINELLAS 4   /* disposable Urinellas a pocket holds (one-time use) */
 #define LIGHTER_USES     60   /* a full lighter lights this many times */
 
+/* A pull up in progress (people/climb.c): from where, onto where, over which edge. */
+typedef struct {
+    float t, time;          /* how far (0..1 over time seconds) */
+    Vec3  from, to, edge;
+    Vec3  across;           /* along the edge (the hands hold it a shoulder width apart) */
+    float hang;             /* feet this high while hanging on before the pull up */
+    float face;             /* how far ahead of where it started the face of it is (m) */
+    Vec3  ankle0[2], toe0[2]; /* where the feet were when it started (eased from there) */
+} ClimbMove;
+
 typedef struct Object {
     ObjectType type;
     Vec3       position;      /* base point (bottom center) */
@@ -198,6 +208,7 @@ typedef struct Object {
     bool       climbing;      /* characters: pulling themselves up a ledge (no physics meanwhile) */
     bool       climb_feet;    /* ... the feet go here meanwhile (world; set by whoever moves the climb) */
     Vec3       climb_ankle[2], climb_toe[2];
+    ClimbMove  climb_move;    /* ... how the pull up goes */
     /* Stepping over something knee high (walking): a set move, not left to the
        feet - the leading leg over, the body over it, the other leg after (Physics) */
     float      over_t, over_time;  /* 0 = not; else how far (0..1) and how long it takes */
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 476cfab..a6fcc09 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -962,7 +962,7 @@ void World_update(World *world, float dt, float game_dt) {
         }
         Condition_useGrid(&world->grid);
         Condition_update(o, world->objects, world->count, dt, game_dt);
-        if (o->type == OBJ_HUMAN) { Ai_useGrid(&world->grid); Ai_update(o, world->objects, world->count, dt); }
+        if (o->type == OBJ_HUMAN) { Ai_useGrid(&world->grid); Ai_useWorld(world); Ai_update(o, world->objects, world->count, dt); }
         if (o->type == OBJ_HUMAN && o->ai.wants_bag) handOutBag(world, o);
     }