Nimbin[12]?Games / Entropie / commits / a22466a

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.92.0: careful on roofs, breaks of all kinds, out of the climber's way

Christian Immanuel · 2026-10-19 09:10 · a22466a3cdadf10d25ebdabf58420e69dab84bbf

- Feet on the course: a step over or onto something lands the feet flat on
  it (they stood 12-22 cm above logs); firewood lying on its side is walked
  onto instead of being pushed along
- Holding a ladder: from the side, hands on one rail, a foot on the bottom
  rung; the climber gets on and off in front
- Up on a roof: NPCs walk slowly and carefully, arms out; afraid of heights
  (Ida) they don't follow up the ladder; drunk, the sharp ones are extra
  careful, the dim ones stroll about and stumble. New trait: afraid of
  heights (dev menu)
- Breaks: mostly their own kind, now and then another (a coffee at the
  stove, a walk, a chat, a sit), short or long; the tool goes down first
- Called away from a job to help, they go back to it after; the forgetful
  wait to be reminded ("Back to the firewood, please")
- Worn from the days before: hard work wears people down, only sleep takes
  it off, a hard day leaves some for the next; shown on Tab
- Woodpile: "Find a good place for it" - they pick a spot themselves (a pile
  nearby, else clear, level, off the roads, by a wall)
 entropie/Makefile                   |  2 +-
 entropie/README.md                  | 23 ++++++---
 entropie/RULES.md                   |  4 +-
 entropie/STATE.md                   | 18 ++++---
 entropie/commit_message.txt         | 40 +++++++---------
 entropie/src/game/game.c            |  1 +
 entropie/src/game/game_dev.c        |  4 +-
 entropie/src/people/ai.c            | 48 +++++++++++++++++++
 entropie/src/people/ai.h            | 13 ++++-
 entropie/src/people/ai_help.c       | 14 +++++-
 entropie/src/people/ai_work.c       | 96 ++++++++++++++++++++++++++++++-------
 entropie/src/people/body.h          |  3 ++
 entropie/src/people/body_internal.h |  2 +-
 entropie/src/people/condition.c     |  5 ++
 entropie/src/people/talk.c          | 79 +++++++++++++++++++++++++++---
 entropie/src/world/ladder.c         | 30 +++++++-----
 entropie/src/world/ladder.h         |  2 +-
 entropie/src/world/object.c         | 21 +++++++-
 entropie/src/world/object.h         | 11 ++++-
 entropie/src/world/physics.c        |  6 +--
 entropie/src/world/scene.c          |  4 ++
 entropie/src/world/world.c          |  2 +
 22 files changed, 347 insertions(+), 81 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index 720cebd..0e2ae0e 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.91.0
+VERSION   := 1.92.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index d4b0c8b..96e5945 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -357,7 +357,10 @@ Hold the left button to keep working.
 - Everyone works their own way: Jonas is quick, gets right to it and takes
   no breaks; Ida thinks things over first and takes a little walk now and
   then; Mila stops for a chat with someone; Ben is slow, ponders and sits
-  down a lot
+  down a lot. Now and then they take another kind of break (a coffee at the
+  stove, a walk, a chat, a sit), short or long, and put the tool down first
+- Called away from a job to help you, they go back to it after; the
+  forgetful wait to be reminded ("Back to the firewood, please")
 - Helpers (G) understand what the wood is for: they fell along, take the
   branches off, cut and split, fetching fitting tools. Without an axe they say so
 - Using or taking something shows a short line about it
@@ -398,13 +401,15 @@ A hand that can't reach its grip lets go and takes hold again.
   what holds it (a bush, something light). The message says when it's wobbly
 - An unsteady ladder may slip when you climb it: a wobble first (it rocks), then it
   falls over and you with it (the higher, the harder you land)
-- 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
+- Somebody holding it (beside its foot, hands on a rail, a foot on the bottom
+  rung: out of the climber's way) keeps it steady: helpers (G) hold it when you
+  climb, helpful people come over on their own when it looks wobbly
 - The others can slip off a wobbly ladder too
 - 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
+  jump down when you're down. Ida is afraid of heights and stays down. Up
+  there they go slowly and carefully, arms out; drunk, the sharp ones are
+  extra careful, the dim ones stroll about and stumble
 - 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
@@ -442,6 +447,9 @@ say no more often and don't come to you for a bit.
   may answer "In a minute!"
 - "The firewood goes somewhere else now": show them a new place; the old
   stack gets carried over to the new woodpile
+- Asked where the firewood goes: show them, let them find a good place (an
+  existing pile nearby, else a clear, level spot off the roads, by a wall),
+  or leave it lying
 - The work place: things lying where they'd stand to split or cut, the tidy
   clear away first; no room to swing (a wall, the car), they take the piece
   somewhere with room - carried, or dragged with somebody called to help
@@ -533,8 +541,9 @@ say no more often and don't come to you for a bit.
 - Fresh ones stink: stink lines rise, faces scrunch up, people remark on it
   and walk off
 - Sleep with Z, hold Space to sleep faster
-- Tab shows your stats: energy, hunger, thirst, bladder, sleepiness, drink,
-  dirt, how fast you sprint and how high you jump; the arrow keys turn to
+- Tab shows your stats: energy, hunger, thirst, bladder, sleepiness, how worn
+  out you are from the days' work (only sleep takes it off; a hard day leaves
+  some for the next), drink, dirt, how fast you sprint and how high you jump; the arrow keys turn to
   your skills. Only pressing needs show otherwise
 - The others live the same day: they eat, drink, pee and sleep at night
 
diff --git a/entropie/RULES.md b/entropie/RULES.md
index d1503f6..080be8a 100644
--- a/entropie/RULES.md
+++ b/entropie/RULES.md
@@ -18,7 +18,9 @@ Structure: [ARCHITECTURE.md](ARCHITECTURE.md).
 - Rebuild a mesh only when what it shows changes.
 - Compile without warnings: `-Wall -Wextra -Wpedantic -Wshadow`.
 - Characters moved by script (stepping over, onto, sitting down) use the set
-  moves, never a jump of the position.
+  moves, never a jump of the position. Feet set down by script: the surface
+  plus `BODY_ANKLE_Y` (scaled), not the ankle's height of the moment.
+- A collider's "up" face can be any of its axes (things lie on their side).
 - Every version: bump `VERSION`, update `STATE.md` (done items out of the to-do
   list, version line at the end), keep `README.md` short, write
   `commit_message.txt` with this version's changes only.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 8648275..fa5b9de 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -21,6 +21,8 @@ Updated with every version (see the version line at the end).
 | Lighter in the pocket (lights) | 20 | 40 | – | – | 40 |
 | Smokes (craving speed) / pack / roll-ups | .3 / 10 / 0 | .6 / 0 / 12 | – | .25 / 0 / 0 | .9 / 6 / 20 |
 | Wits | .6 | .45 | .8 | .6 | .25 |
+| Afraid of heights | .2 | .1 | .85 | .15 | .5 |
+| Worn from the days before (start) | .15 | .2 | .1 | .25 | .55 |
 
 Alice the dog: follows, can be petted and called.
 
@@ -93,6 +95,15 @@ Trees and stumps (1.91): a tree's foot flares out (FOOT_FLARE 0.12 up to 0.45 m)
 
 Shared code (1.91): one box ray (`Collider_ray`, `Object_rayToCollider`; the ladder's copy is gone, world.c's down ray uses it), `World_rayCast`, `Object_reach`. ARCHITECTURE.md (modules, the frame, conventions) and RULES.md (do's and don'ts), kept current.
 
+Round 1.92:
+- Feet on the course (`physics.c` beginStepOver): the landing spots of a step over / onto are the surface plus the ankle's own height (`BODY_ANKLE_Y`), not the ankle as it was (up on the toes: it stood 12-22 cm over a log). Planted feet over 3 cm off the surface now at most 5-9 cm, rarely (all three lanes). `tryStepUp` takes whichever collider axis points up (firewood lies on its side: you were bulldozing a half log 2.5 m down the course instead of walking onto it).
+- Holding a ladder (`ladder.c`): from the side (HOLD_OUT 0.3 out, HOLD_BESIDE 0.48 aside, `hold_side`: the side they came from), both hands on that rail (one higher), the near foot on the end of the bottom rung; the climber gets on and off in front. `ai_help.c` ai_holdLadder: the last 0.7 m straight to the spot, there within 0.1 m, walks back once 0.2 m off (`hold_there`).
+- Up high (`Object_upHigh`: on a roof slab, or 1.6 m+ over the ground): `Object_caution` = 0.35 + 0.5 height_fear - 0.15 fitness + drunk x (wits - 0.45) x 0.8. NPCs walk at most HIGH_PACE 1.5 m/s x (1 - 0.5 caution), arms out (balance 0.5 x caution); careful ones say "Don't look down...", drunk and careless "Woo! Look at me up here!" and stumble (HIGH_STUMBLE 0.12/s x drunk). `height_fear` (dev menu row): over 0.6 (less when drunk) they don't follow up a ladder ("No way - I'm not going up there").
+- Breaks (`ai_work.c` takeBreak): each break their own kind (BREAK_OWN 0.6) or another (sit, walk, chat, BREAK_COFFEE: to the nearest stove within 70 m, sips, back); 30 % long (x1.8), else short (x0.7), longer and sooner when worn; the tool goes down first (`Hands_setAside`, taken again after). A walk back early: the rest sitting.
+- Called away (`Ai_help` keeps `paused_order` / `paused_area`): `Ai_stopHelping` sends them back to it when 0.6 wits + 0.4 willing - 0.3 drunk > 0.42; the others forget it, and "Back to the <job>, please" (talk, first choice) reminds them (`Ai_resumePaused`).
+- Worn from the days before (`worn`, condition.c): + WORN_PER_EFFORT 0.012 per stamina used, - WORN_SLEEP_HOUR 0.075 per game hour asleep; adds up to WORN_FEEL 0.6 to `Object_tiredness`. Tab shows it ("Worn (days)").
+- Woodpile: "Find a good place for it" (instead of "Just pile it up here"): a woodpile within 40 m of the work, else the best spot 3-9 m round it (clear of things, level, off roads, by something big and fixed); they think it over first.
+
 Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves.
 
 Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built); the coat rack (FURN_COATRACK, in your kitchen by the doors, garage coords 20.6 / 2.4) shows the jackets hanging on it (`Shapes_coatRack`, up to 6, a bitmask of whose in `shown`). `jacket_hung` (characters, true at the start): cold (chill 0.6+) and the jacket with them: on it goes (as before); hanging: NPCs `ai.want_jacket` -> `ai.c` fetchJacket (idle / wander: goTo the rack, on with it, "Ah, better."; gives up after 90 s or warm again); you are told once ("your jacket hangs on the rack in your garage") and click the rack (`game_actions.c` coatRack: on, or back onto it). Warm again: it comes off and stays with them.
@@ -226,11 +237,6 @@ Most important first.
 
 **From the 15th of October (to do)**
 - Trees: push / pull fallen trees; branches break or snag on obstacles (with enough momentum); much easier to carry without branches
-- Steps on the course (middle lane): check the feet land on what they stand on (flat ground checked: planted feet within 1 cm)
-- Holding a ladder: don't get in the way of whoever's climbing (wait a moment, or hold it from the side). NPCs on a roof: slow and careful; afraid of heights: don't go up; drunk: more careful, or dumber
-- Breaks: put the tool down, a coffee, a walk, a chat; short and long ones; called away, they go back to the old task afterwards (or you remind them)
-- Tired from the day before, by what they did
-- Woodpiles: drop "show me where" (you can place it yourself), maybe "find a good place"; people think between tasks
 - Alice: doesn't fit the art style, and dumb; she tries to catch things thrown, eats a sausage dropped on the ground, may catch a thrown apple (and not bring it back). NPC pathfinding has problems
 - NPCs sitting at the meeting table (chairs work for them, nothing sends them there yet)
 
@@ -255,4 +261,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.91.0 (build 286f693)`
+State of `entropie 1.92.0 (build 9eb26b9)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index 8445eb3..8000583 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,23 +1,19 @@
-entropie 1.91.0: a basement, chairs you can sit on, the camera stays out of walls
+entropie 1.92.0: careful on roofs, breaks of all kinds, out of the climber's way
 
-- Main building: the porch only along the right part of the front; the
-  right half's roof turned (ridge front to back); chairs, counter, stove
-  and shelf face the right way (one yaw convention: yaw_facing / yaw_along
-  / yaw_front in linalg.h); kitchen with a shelf and two chairs
-- Basement: stairs down on the right of the entrance, old loose brick
-  walls, open rooms, shelves, loose bricks; the ground has a hole there
-  (Terrain_addPit, not drawn inside); brick courses on the shell's walls
-- Camera: pulled in by walls between your head and it, eases back out;
-  your own body is hidden when it's right behind your head
-- Chairs: pulled out to sit (a hand on the backrest), pushed back in after
-  you leave; sitting down no longer gets stuck on the table
-- Ground contact: loose things rest on their real lowest points and lie
-  along the slope; logs on their round side; things slide down steeper
-  slopes (round ones sooner)
-- Walking: the hips rise and fall with the steps; strafing no longer
-  swings the pelvis out (springs following a moving goal lagged behind it)
-- Edges higher than a stair (the ramp's side, crates): a step up, no jolt
-- Trees have a flared foot that becomes the stump: no pop when felled; the
-  felled trunk shows its sawn end instead of being hollow
-- One shared box ray (Collider_ray, World_rayCast)
-- ARCHITECTURE.md and RULES.md: how the code is built, do's and don'ts
+- Feet on the course: a step over or onto something lands the feet flat on
+  it (they stood 12-22 cm above logs); firewood lying on its side is walked
+  onto instead of being pushed along
+- Holding a ladder: from the side, hands on one rail, a foot on the bottom
+  rung; the climber gets on and off in front
+- Up on a roof: NPCs walk slowly and carefully, arms out; afraid of heights
+  (Ida) they don't follow up the ladder; drunk, the sharp ones are extra
+  careful, the dim ones stroll about and stumble. New trait: afraid of
+  heights (dev menu)
+- Breaks: mostly their own kind, now and then another (a coffee at the
+  stove, a walk, a chat, a sit), short or long; the tool goes down first
+- Called away from a job to help, they go back to it after; the forgetful
+  wait to be reminded ("Back to the firewood, please")
+- Worn from the days before: hard work wears people down, only sleep takes
+  it off, a hard day leaves some for the next; shown on Tab
+- Woodpile: "Find a good place for it" - they pick a spot themselves (a pile
+  nearby, else clear, level, off the roads, by a wall)
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index c09147e..2be9401 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -174,6 +174,7 @@ int Game_stats(const Game *game, StatBar *bars, int max, const char **title, cha
         { "Thirst",  n->thirst,               true },
         { "Bladder", n->bladder,              true },
         { "Sleepy",  n->sleepiness,           true },
+        { "Worn (days)", p->worn,             true },
         { "Drunk",   fminf(p->drunk, 1.0f),   true },
         { "Dirty",   p->dirt,                 true },
         { "Sporty",  p->fitness,              false },
diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c
index 4145e7c..0cb7d6d 100644
--- a/entropie/src/game/game_dev.c
+++ b/entropie/src/game/game_dev.c
@@ -11,7 +11,7 @@
 typedef enum {
     DEV_WHO, DEV_TELEPORT, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_DRUNK, DEV_ENERGY,
     DEV_BLADDER, DEV_HUNGER, DEV_THIRST, DEV_SLEEPY, DEV_STRENGTH, DEV_ANNOYED,
-    DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_WILLING,
+    DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING,
     DEV_LIKES,                       /* likes for each job ... */
     DEV_SKILLS = DEV_LIKES + 4,      /* ... skill at each job */
     DEV_ROWS = DEV_SKILLS + 4
@@ -122,6 +122,7 @@ void game_dev(Game *game, const Input *input) {
     case DEV_NERVES:   o->temperament.nerves = step01(o->temperament.nerves, dir, 0.1f); break;
     case DEV_CLUMSY:   o->body.quirk[3] = stepSigned(o->body.quirk[3], dir, 0.2f); break; /* (clumsiness = 0.5 + 0.5 x this) */
     case DEV_WITS:     o->wits = step01(o->wits, dir, 0.05f); break;
+    case DEV_HEIGHTS:  o->height_fear = step01(o->height_fear, dir, 0.05f); break;
     case DEV_WILLING:  c->willing = step01(c->willing, dir, 0.1f); break;
     default: break;
     }
@@ -158,6 +159,7 @@ static void devLine(const Object *o, int row, char *out) {
     case DEV_NERVES:   snprintf(out, 96, "Nerves (calm .. restless):  %.1f", o->temperament.nerves); break;
     case DEV_CLUMSY:   snprintf(out, 96, "Clumsy:  %.0f%%", (0.5f + 0.5f * o->body.quirk[3]) * 100.0f); break;
     case DEV_WITS:     snprintf(out, 96, "Wits:  %.2f", o->wits); break;
+    case DEV_HEIGHTS:  snprintf(out, 96, "Afraid of heights:  %.2f", o->height_fear); break;
     case DEV_WILLING:  snprintf(out, 96, "Willing:  %.0f%%", c->willing * 100.0f); break;
     default:           out[0] = '\0'; break;
     }
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 433eaae..3c92f0b 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -9,13 +9,40 @@
 #include "world/physics.h"
 #include "world/nav.h"
 #include "world/grid.h"
+#include "world/world.h"
 #include "render/shapes.h"
 
 /* ---------- Small helpers ---------- */
 
 /* Walks along dir (unit, horizontal) at speed; the body turns after it. */
+/* Up on a roof: careful steps (the fearful creep, the drunk and dim stroll about and stumble). */
+#define HIGH_PACE     1.5f  /* m/s up there at most (nobody runs on a roof) ... */
+#define HIGH_SLOW     0.5f  /* ... slower by this much at most (all caution) */
+#define HIGH_STUMBLE  0.12f /* stumbles per second, drunk and careless */
+
+static float upHighSpeed(Object *o, float speed, float dt) {
+    Ai *ai = &o->ai;
+    if (!g_world || !Object_upHigh(o, Terrain_height(&g_world->terrain, o->position.x, o->position.z))) {
+        ai->high_time = fmaxf(0.0f, ai->high_time - dt);
+        if (ai->high_time <= 0.0f) ai->high_said = false;
+        return speed;
+    }
+    ai->high_time = fminf(ai->high_time + dt, 5.0f);
+    float c = Object_caution(o), d = fminf(o->drunk, 1.0f);
+    if (!ai->high_said && ai->high_time > 1.0f && ai->speech_time <= 0.0f) {
+        ai->high_said = true;
+        if (c > 0.6f) Ai_say(o, "Don't look down... don't look down...");
+        else if (c < 0.2f && d > 0.5f) Ai_say(o, "Woo! Look at me up here!");
+    }
+    if (c < 0.25f && d > 0.5f && o->body.stumble <= 0.0f && dt > 0.0f &&
+        random01(ai) < HIGH_STUMBLE * d * dt)
+        Body_trip(&o->body, 0, Object_facing(o), 0.4f + 0.3f * d);
+    return fminf(speed, HIGH_PACE * (1.0f - HIGH_SLOW * c));
+}
+
 void ai_walk(Object *o, Vec3 dir, float speed, float dt) {
     speed *= Object_vigor(o) * (o->tempo > 0.0f ? 0.8f + 0.2f * o->tempo : 1.0f); /* (their own pace) */
+    speed = upHighSpeed(o, speed, dt);
     /* Drunk people wobble off course */
     float wobble = sinf(o->body.clock * 0.7f) + 0.6f * sinf(o->body.clock * 1.9f + 1.3f);
     dir = rotateY(dir, wobble * 0.5f * fminf(o->drunk, 1.0f));
@@ -955,6 +982,10 @@ void Ai_help(Object *self, Object *leader) {
         if (self->hands.state[hand] == HAND_HOLDING && !toolOf(self->hands.item[hand])) Hands_drop(self, hand);
 
     Vec3 right = vec3_normalize(vec3_cross(Object_facing(leader), UP));
+    if (ai->order != ORDER_NONE && (ai->mode == AI_WORK || ai->before == AI_WORK)) { /* (called away from a job: remembered) */
+        ai->paused_order = ai->order;
+        ai->paused_area = ai->work_area;
+    }
     ai->mode    = AI_HELP;
     ai->order   = ORDER_NONE;
     ai->leader  = leader;
@@ -974,6 +1005,22 @@ void Ai_stopHelping(Object *self) {
     ai->home    = self->position; /* hang around where we helped */
     stand(self);
     rest(ai, PAUSE_MIN, PAUSE_MAX);
+    /* Called away from a job: back to it - unless they forget (then they wait to be reminded) */
+    if (ai->paused_order != ORDER_NONE && 0.6f * self->wits + 0.4f * ai->character.willing - 0.3f * fminf(self->drunk, 1.0f) > 0.42f) {
+        Order back = ai->paused_order;
+        Ai_order(self, back, ai->paused_area);
+        Ai_say(self, back == ORDER_FIRE ? "I'll get back to the fire then." : back == ORDER_TRASH ? "Back to the tidying then."
+                   : back == ORDER_TREES ? "Back to the trees then." : "Right, back to the firewood.");
+    }
+}
+
+bool Ai_resumePaused(Object *self) {
+    Ai *ai = &self->ai;
+    if (ai->paused_order == ORDER_NONE || ai->mode == AI_WORK) return false;
+    Order back = ai->paused_order;
+    if (ai->mode == AI_HELP) Ai_stopHelping(self);
+    if (ai->mode != AI_WORK) Ai_order(self, back, ai->paused_area);
+    return true;
 }
 
 bool Ai_isHelping(const Object *self) {
@@ -992,6 +1039,7 @@ void Ai_order(Object *self, Order order, Vec3 area) {
     dropEverything(self);
     ai->mode      = AI_WORK;
     ai->order     = order;
+    ai->paused_order = ORDER_NONE;
     ai->pending   = ORDER_NONE;
     ai->showing   = SHOW_NONE;
     ai->leader    = NULL;
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index 48c2780..da6e775 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -67,7 +67,7 @@ typedef enum {
 typedef enum { ORDER_NONE, ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE, ORDER_COUNT } Order;
 
 /* How someone takes a break from work */
-typedef enum { BREAK_NONE, BREAK_SIT, BREAK_WALK, BREAK_CHAT } BreakKind;
+typedef enum { BREAK_NONE, BREAK_SIT, BREAK_WALK, BREAK_CHAT, BREAK_COFFEE } BreakKind;
 
 /* Who someone is */
 typedef struct {
@@ -188,8 +188,14 @@ typedef struct {
     float          prep_time;
     bool           chat_near;   /* on a chat break: close enough (left again only well beyond) */
     float          hold_idle;
+    bool     hold_there;   /* ... at the spot (stands there till pushed well off it) */
     float          ladder_dir;  /* on a ladder: +1 going up, -1 down */
     float          ladder_seek; /* until they look for a way up / down again */
+    Order          paused_order; /* called away from this job (to help): back to it after, or when reminded ... */
+    Vec3           paused_area;  /* ... there */
+    float          high_time;   /* seconds up high (a roof) ... */
+    bool           high_said;   /* ... said something about it */
+    bool           heights_said; /* told they won't go up there (once each time they're asked) */
     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 */
@@ -202,6 +208,9 @@ typedef struct {
     float          on_break;    /* on a break (seconds left) */
     Vec3           break_from;  /* BREAK_WALK: where they set out from (and come back to) */
     bool           break_back;  /* ... on the way back */
+    BreakKind      break_now;   /* this break: mostly their own kind, now and then another */
+    struct Object *break_at;    /* BREAK_COFFEE: the stove (a coffee there) */
+    float          break_sip;   /* ... until the next sip */
     struct Object *chat_with;   /* BREAK_CHAT: who they're chatting with */
     float          chat_line;   /* ... until the next thing said */
     int            chat_said;
@@ -211,6 +220,8 @@ typedef struct {
 void Ai_help(struct Object *self, struct Object *leader);
 void Ai_stopHelping(struct Object *self);
 bool Ai_isHelping(const struct Object *self);
+/* Called away from a job and didn't go back to it on their own: back to it now (reminded). False: no such job. */
+bool Ai_resumePaused(struct Object *self);
 
 /* Says something (a speech bubble for a few seconds). */
 void Ai_say(struct Object *self, const char *text);
diff --git a/entropie/src/people/ai_help.c b/entropie/src/people/ai_help.c
index 43d13d8..e95491b 100644
--- a/entropie/src/people/ai_help.c
+++ b/entropie/src/people/ai_help.c
@@ -700,12 +700,16 @@ bool ai_holdLadder(Object *self, Object *const *objects, int count, float dt) {
     }
     Vec3 facing, spot = Ladder_holdSpot(l, &facing);
     float d = flatDistance(spot, self->position);
-    if (d > 0.35f) {
-        Vec3 dir = ai_steer(self, NULL, objects, count, spot);
+    if (d > (ai->hold_there ? 0.2f : 0.1f)) { /* (right up to it; once there, a little shove doesn't send them walking) */
+        ai->hold_there = false;
+        Vec3 to = vec3_sub(spot, self->position);
+        to.y = 0.0f;
+        Vec3 dir = d < 0.7f ? vec3_scale(to, 1.0f / d) : ai_steer(self, NULL, objects, count, spot); /* (the last bit straight there) */
         if (vec3_length(dir) < 0.5f) stand(self);
         else ai_walk(self, dir, fminf(WALK_SPEED, d * 2.5f + 0.3f), dt);
         return true;
     }
+    ai->hold_there = true;
     stand(self);
     turnTowards(self, facing, dt * 2.0f);
     return true;
@@ -776,6 +780,12 @@ bool ai_followByLadder(Object *self, Object *const *objects, int count, const Ob
     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;
+    if (going_up && self->height_fear * (1.0f - 0.5f * fminf(self->drunk, 1.0f)) > 0.6f) { /* afraid of heights: not up there */
+        if (!ai->heights_said) { ai->heights_said = true; Ai_say(self, "No way - I'm not going up there. I'll wait down here."); }
+        ai->ladder_seek = 20.0f;
+        return false;
+    }
+    if (!going_up) ai->heights_said = false;
     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 */
diff --git a/entropie/src/people/ai_work.c b/entropie/src/people/ai_work.c
index eb3fc64..0d73ec3 100644
--- a/entropie/src/people/ai_work.c
+++ b/entropie/src/people/ai_work.c
@@ -827,53 +827,117 @@ static Object *chatPartner(const Object *self, Object *const *objects, int count
     return best;
 }
 
-static void nextBreak(Ai *ai) {
-    ai->streak = ai->character.break_every * (0.7f + 0.6f * random01(ai));
+#define BREAK_OWN     0.6f   /* their own kind of break this often, else another */
+#define BREAK_LONG    0.3f   /* a long one this often (BREAK_LONGER x), else short (BREAK_SHORTER x) */
+#define BREAK_LONGER  1.8f
+#define BREAK_SHORTER 0.7f
+#define COFFEE_RANGE  70.0f  /* m: a stove this near is worth a coffee */
+
+static void nextBreak(Object *self) {
+    Ai *ai = &self->ai;
+    ai->streak = ai->character.break_every * (0.7f + 0.6f * random01(ai)) * (1.0f - 0.4f * self->worn); /* (worn out: sooner) */
 }
 
 static void endBreak(Object *self) {
     Ai *ai = &self->ai;
     ai->on_break = 0.0f;
     ai->chat_with = NULL;
+    ai->break_at = NULL;
     ai->task_seen = NULL; /* (whoever thinks things over does so again) */
     self->sitting = false;
-    nextBreak(ai);
+    nextBreak(self);
     Ai_say(self, random01(ai) < 0.5f ? "Right, back to it!" : "Okay, back to work.");
 }
 
-/* Now and then a break, their way: sitting down, a little walk, a chat. True while on it. */
+static Object *stoveNear(const Object *self, Object *const *objects, int count) {
+    Object *best = NULL;
+    float best_d = COFFEE_RANGE;
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o->type != OBJ_FURNITURE || o->variant != FURN_STOVE) continue;
+        float d = flatDistance(o->position, self->position);
+        if (d < best_d) { best = o; best_d = d; }
+    }
+    return best;
+}
+
+/* Which break this time: mostly their own way, now and then a coffee, a walk, a chat or a sit. */
+static BreakKind pickBreak(Object *self, Object *const *objects, int count) {
+    Ai *ai = &self->ai;
+    if (random01(ai) < BREAK_OWN) return ai->character.break_kind;
+    static const BreakKind KINDS[] = { BREAK_SIT, BREAK_WALK, BREAK_CHAT, BREAK_COFFEE };
+    BreakKind k = KINDS[(int)(random01(ai) * 4.0f) % 4];
+    if (k == BREAK_COFFEE && !stoveNear(self, objects, count)) k = BREAK_SIT;
+    if (k == BREAK_CHAT && !chatPartner(self, objects, count)) k = BREAK_SIT;
+    return k;
+}
+
+/* Now and then a break, short or long, their way or another: sitting down,
+   a little walk, a chat, a coffee at the stove. The tool goes down first. True while on it. */
 static bool takeBreak(Object *self, Object *const *objects, int count, float dt) {
     Ai *ai = &self->ai;
     const Character *c = &ai->character;
     if (c->break_kind == BREAK_NONE || c->break_every <= 0.0f) return false;
     if (ai->on_break <= 0.0f) {
-        if (ai->streak <= 0.0f) nextBreak(ai);
+        if (ai->streak <= 0.0f) nextBreak(self);
         if ((ai->streak -= dt) > 0.0f || Tools_isWorking(self) || firewoodInHands(self) > 0) return false;
-        ai->on_break = c->break_length * (0.7f + 0.6f * random01(ai));
+        ai->break_now = pickBreak(self, objects, count);
+        bool long_one = random01(ai) < BREAK_LONG;
+        ai->on_break = c->break_length * (long_one ? BREAK_LONGER : BREAK_SHORTER) * (0.8f + 0.4f * random01(ai)) * (1.0f + 0.5f * self->worn);
         ai->break_from = self->position;
         ai->break_back = false;
-        ai->chat_with = c->break_kind == BREAK_CHAT ? chatPartner(self, objects, count) : NULL;
+        ai->break_at = NULL;
+        ai->break_sip = 0.0f;
+        ai->chat_with = ai->break_now == BREAK_CHAT ? chatPartner(self, objects, count) : NULL;
         ai->chat_line = 0.0f;
         ai->chat_said = 0;
-        if (c->break_kind == BREAK_WALK) {
-            float a = random01(ai) * 2.0f * PI_F, d = 7.0f + 5.0f * random01(ai);
+        if (self->hands.item[HAND_LEFT] || self->hands.item[HAND_RIGHT]) Hands_setAside(self, ai->set_aside); /* (the tool down: taken again after) */
+        if (self->worn > 0.45f && random01(ai) < 0.5f) Ai_say(self, "Ugh, I'm still beat from yesterday.");
+        else if (ai->break_now == BREAK_WALK) Ai_say(self, long_one ? "I need some air. Back in a bit." : "I'll stretch my legs for a bit.");
+        else if (ai->break_now == BREAK_COFFEE) Ai_say(self, "Coffee time.");
+        else if (!ai->chat_with) Ai_say(self, long_one ? "I need a proper break." : "Time for a little sit-down.");
+        if (ai->break_now == BREAK_WALK) {
+            float a = random01(ai) * 2.0f * PI_F, d = (long_one ? 12.0f : 7.0f) + 5.0f * random01(ai);
             setGoal(ai, vec3_add(self->position, vec3(cosf(a) * d, 0.0f, sinf(a) * d)));
-            Ai_say(self, "I'll stretch my legs for a bit.");
-        } else if (!ai->chat_with) {
-            Ai_say(self, "Time for a little sit-down.");
+        } else if (ai->break_now == BREAK_COFFEE) {
+            ai->break_at = stoveNear(self, objects, count);
+            if (ai->break_at) setGoal(ai, ai->break_at->position);
+            else ai->break_now = BREAK_SIT;
         }
     }
     ai->on_break -= dt;
 
-    if (c->break_kind == BREAK_WALK) { /* out a bit, and back */
+    if (ai->break_now == BREAK_COFFEE && ai->break_at) { /* over to the stove, a coffee there, and back */
+        Object *stove = ai->break_at;
+        Vec3 front = vec3_add(stove->position, vec3_scale(stove->colliders[0].axes[2], stove->size.z * 0.5f + 0.5f));
+        Vec3 goal = ai->break_back ? ai->break_from : front;
+        float d = flatDistance(goal, self->position);
+        if (!ai->break_back && d < 0.6f) { /* there: drinking it, a sip now and then */
+            stand(self);
+            turnTowards(self, vec3_sub(stove->position, self->position), dt);
+            if ((ai->break_sip -= dt) <= 0.0f) {
+                ai->break_sip = 6.0f + 6.0f * random01(ai);
+                if (ai->speech_time <= 0.0f && random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "Ahh. Good coffee." : "Mm.");
+            }
+            if (ai->on_break <= 0.0f) { ai->break_back = true; setGoal(ai, ai->break_from); }
+            return true;
+        }
+        if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) { endBreak(self); return false; }
+        if (!ai->break_back && isStuck(ai, d, dt)) { ai->break_back = true; setGoal(ai, ai->break_from); }
+        Vec3 dir = ai_steer(self, NULL, objects, count, goal);
+        if (vec3_length(dir) < 0.5f) stand(self);
+        else ai_walk(self, dir, WALK_SPEED * 0.6f, dt);
+        return true;
+    }
+    if (ai->break_now == BREAK_WALK) { /* out a bit, and back */
         Vec3 goal = ai->break_back ? ai->break_from : ai->goal;
         float d = flatDistance(goal, self->position);
-        if (!ai->break_back && (d < 1.0f || isStuck(ai, d, dt) || ai->on_break < c->break_length * 0.4f)) {
+        if (!ai->break_back && (d < 1.0f || isStuck(ai, d, dt) || ai->on_break < c->break_length * 0.3f)) {
             ai->break_back = true;
             setGoal(ai, ai->break_from);
         } else if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) {
-            endBreak(self);
-            return false;
+            if (ai->on_break > 3.0f) ai->break_now = BREAK_SIT; /* (back early: the rest of it sitting) */
+            else { endBreak(self); return false; }
         }
         Vec3 dir = ai_steer(self, NULL, objects, count, goal);
         if (vec3_length(dir) < 0.5f) stand(self);
diff --git a/entropie/src/people/body.h b/entropie/src/people/body.h
index e45315c..0744e53 100644
--- a/entropie/src/people/body.h
+++ b/entropie/src/people/body.h
@@ -26,7 +26,10 @@
 
 enum { HAND_LEFT = 0, HAND_RIGHT = 1 };
 
+#define BODY_ANKLE_Y 0.075f /* the ankle over the sole, standing flat (a 1.75 m person; scale with height) */
+
 /* How a person carries themselves. All -1..1 (nerves 0..1); 0 is neutral. */
+
 typedef struct {
     float pride;   /* -1 = shy, closed up .. 1 = proud, chest out */
     float energy;  /* -1 = tired, slumped .. 1 = lively */
diff --git a/entropie/src/people/body_internal.h b/entropie/src/people/body_internal.h
index 8a0f3f5..0f5ac50 100644
--- a/entropie/src/people/body_internal.h
+++ b/entropie/src/people/body_internal.h
@@ -16,7 +16,7 @@
 #define HIP_DROP      0.05f
 #define THIGH         0.44f
 #define SHIN          0.42f
-#define ANKLE_Y       0.075f
+#define ANKLE_Y       BODY_ANKLE_Y
 #define FOOT_LENGTH   0.15f
 #define FOOT_X        0.10f
 #define SPINE         0.36f
diff --git a/entropie/src/people/condition.c b/entropie/src/people/condition.c
index a264738..d7d67be 100644
--- a/entropie/src/people/condition.c
+++ b/entropie/src/people/condition.c
@@ -33,6 +33,9 @@
 #define REST_STANDING     0.030f
 #define REST_WALKING      0.008f
 #define REST_SLEEPING     0.050f
+/* Worn out over the days (see Object.worn) */
+#define WORN_PER_EFFORT   0.012f  /* per stamina used up (an afternoon of hard work: about 0.7) */
+#define WORN_SLEEP_HOUR   0.075f  /* taken off per game hour asleep (a night: about 0.6) */
 
 static unsigned rng = 0x2545F491u;
 
@@ -107,6 +110,8 @@ static void needs(Object *self, float dt, float game_dt) {
     effort *= 1.0f + TIRED_EFFORT * tired;
     if (!asleep) rest *= 1.0f - TIRED_REST * tired;
     n->stamina = clampf(n->stamina + (effort > 0.0f ? -effort : rest) * dt, 0.0f, fmaxf(cap, 0.0f));
+    if (effort > 0.0f) self->worn = fminf(1.0f, self->worn + WORN_PER_EFFORT * effort * dt * (1.15f - 0.3f * self->fitness));
+    if (asleep) self->worn = fmaxf(0.0f, self->worn - WORN_SLEEP_HOUR * hours * (1.0f + 0.25f * self->temperament.energy));
     if (n->stamina > cap) n->stamina = cap;
 
     float hard = effort > 0.0f ? WORK_THIRST : 1.0f;
diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c
index 9bb8c82..d2ff540 100644
--- a/entropie/src/people/talk.c
+++ b/entropie/src/people/talk.c
@@ -2,6 +2,7 @@
 #include "world/camp.h"
 #include "world/tools.h"
 #include "people/pockets.h"
+#include "world/site.h"
 
 #include <stdio.h>
 #include <string.h>
@@ -43,7 +44,7 @@ static const char *const CHOICES[NODE_COUNT][TALK_CHOICES] = {
     [NODE_ASK]      = { TASK_CHOICES, "No, thanks" },
     [NODE_SUGGEST]  = { "Good idea, go ahead", "Help me instead", "Not now" },
     [NODE_HOW]      = { "I'll show you", "Never mind" },
-    [NODE_WHERE]    = { "I'll show you where", "Just pile it up here", "Leave it where it is" },
+    [NODE_WHERE]    = { "I'll show you where", "Find a good place for it", "Leave it where it is" },
     [NODE_PLACE]    = { "Yes, stack it here", "Not yet, keep following", "Forget it" },
     [NODE_WATCHING] = { "Watch closely", "Never mind" },
     [NODE_ASK_HAND] = { "Sure", "Not now" },
@@ -93,6 +94,12 @@ static const char *lineFor(const Object *o, int node) {
     }
 }
 
+/* A job they were called away from and haven't gone back to (ORDER_NONE: none). */
+static const char *const JOB_WORD[ORDER_COUNT] = { "", "firewood", "trees", "tidying", "fire" };
+static Order pausedJob(const Object *npc) {
+    return npc->ai.mode != AI_WORK ? npc->ai.paused_order : ORDER_NONE;
+}
+
 static void go(Talk *talk, int node) {
     talk->node = node;
     talk->waited = 0.0f;
@@ -143,6 +150,54 @@ static Object *woodpileHere(World *world, const Object *player) {
     return World_add(world, pile) ? pile : NULL;
 }
 
+/* Where they'd stack it themselves: a woodpile there already (within PILE_SEEK), else a
+   spot near the work: clear, level, off the roads, by a wall if there's one. */
+#define PILE_SEEK  40.0f
+#define PILE_RING0 3.0f   /* m from the work: the ring searched */
+#define PILE_RING1 9.0f
+static float pileSpotScore(World *world, Vec3 p, Vec3 work) {
+    float g = Terrain_height(&world->terrain, p.x, p.z);
+    if (World_groundHeight(world, p.x, p.z, g + 3.0f) > g + 0.05f) return -1.0f;              /* (something stands there) */
+    float slope = fabsf(Terrain_height(&world->terrain, p.x + 0.8f, p.z) - Terrain_height(&world->terrain, p.x - 0.8f, p.z))
+                + fabsf(Terrain_height(&world->terrain, p.x, p.z + 0.8f) - Terrain_height(&world->terrain, p.x, p.z - 0.8f));
+    if (slope > 0.25f || Site_road(p.x, p.z) > 0.2f) return -1.0f;
+    float score = 1.0f - slope - 0.03f * flatDistance(p, work);
+    for (int i = 0; i < world->count; i++) {
+        const Object *o = world->objects[i];
+        if (o->collider_count == 0 || o->is_character || o->type == OBJ_DECAL || o->type == OBJ_PUDDLE || o->type == OBJ_STINK) continue;
+        float d = flatDistance(o->position, p) - Object_reach(o);
+        if (d > 2.0f) continue;
+        if (!o->is_static && d < 0.8f) return -1.0f;                       /* (loose things lying there) */
+        if (o->is_static && d < 0.6f) return -1.0f;
+        if (o->is_static && Object_reach(o) > 1.5f) score += 0.5f;            /* (a wall to stack it against) */
+    }
+    return score;
+}
+
+static Object *pileOwnPlace(World *world, const Object *npc) {
+    Vec3 work = npc->ai.work_area;
+    for (int i = 0; i < world->count; i++) {
+        Object *o = world->objects[i];
+        if (o->type == OBJ_WOODPILE && flatDistance(o->position, work) < PILE_SEEK) return o;
+    }
+    Vec3 best = work;
+    float best_score = -1.0f;
+    for (int r = 0; r < 4; r++)
+        for (int k = 0; k < 16; k++) {
+            float rad = PILE_RING0 + (PILE_RING1 - PILE_RING0) * (float)r / 3.0f, a = (float)k * PI_F / 8.0f;
+            Vec3 p = vec3_add(work, vec3(cosf(a) * rad, 0.0f, sinf(a) * rad));
+            float sc = pileSpotScore(world, p, work);
+            if (sc > best_score) { best_score = sc; best = p; }
+        }
+    if (best_score < 0.0f) return NULL;
+    best.y = Terrain_height(&world->terrain, best.x, best.z);
+    const Vec3 size = WOODPILE_SIZE;
+    Object *pile = Object_create(OBJ_WOODPILE, PALETTE_WARM, best, size, 0.0f);
+    if (!pile) return NULL;
+    Object_setRotation(pile, vec3(0.0f, yaw_along(vec3_sub(best, work)) + PI_F * 0.5f, 0.0f)); /* (lengthwise across the way to it) */
+    return World_add(world, pile) ? pile : NULL;
+}
+
 static bool woodJob(Order what) { return what == ORDER_FIREWOOD || what == ORDER_TREES; }
 
 /* A job: they may say no. Wood jobs: unless they need to be shown how, or
@@ -254,7 +309,13 @@ static void answer(Talk *talk, World *world, Object *player, int choice) {
     Object *npc = talk->with;
     switch (talk->node) {
     case NODE_HELLO:
-        if (choice == 1) go(talk, NODE_TASKS);
+        if (choice == 1 && pausedJob(npc)) {
+            Order job = pausedJob(npc);
+            Ai_resumePaused(npc);
+            char reply[64];
+            snprintf(reply, sizeof reply, shy(npc) ? "Oh... sorry. The %s." : "Oh right - the %s!", JOB_WORD[job]);
+            wrapUp(talk, reply);
+        } else if (choice == 1) go(talk, NODE_TASKS);
         else if (choice == 2) {
             if (npc->ai.character.willing < 0.3f && npc->needs.stamina < 0.5f) wrapUp(talk, "Maybe later, I'm beat.");
             else { Ai_help(npc, player); wrapUp(talk, shy(npc) ? "Okay..." : "Sure, I'm with you."); }
@@ -311,10 +372,11 @@ static void answer(Talk *talk, World *world, Object *player, int choice) {
         break;
     case NODE_WHERE:
         if (choice == 1) { Ai_show(npc, SHOW_PLACE, player); wrapUp(talk, "Lead the way. Click where it should go."); }
-        else if (choice == 2) {
-            Object *pile = woodpileHere(world, player);
-            if (pile) Ai_setWoodpile(npc, pile);
-            wrapUp(talk, "Got it.");
+        else if (choice == 2) { /* they think it over, then pick a place */
+            Object *pile = pileOwnPlace(world, npc);
+            Body_think(&npc->body, 1.0f + npc->ai.character.ponder);
+            if (pile) { Ai_setWoodpile(npc, pile); wrapUp(talk, pile->stored ? "There's a pile already - I'll put it there." : "Hm... I'll stack it over there."); }
+            else { npc->ai.no_pile = true; wrapUp(talk, "Hm, no good spot here. I'll leave it lying."); }
         } else { npc->ai.no_pile = true; wrapUp(talk, "Okay, I'll leave it there."); }
         break;
     case NODE_PLACE:
@@ -445,6 +507,11 @@ int Talk_choices(const Talk *talk, const char *out[TALK_CHOICES]) {
     for (int k = 0; k < TALK_CHOICES; k++)
         if (CHOICES[talk->node][k]) out[n++] = CHOICES[talk->node][k];
     if (talk->node == NODE_WORKER && !woodJob(talk->with->ai.order)) out[1] = "Do something else instead";
+    if (talk->node == NODE_HELLO && pausedJob(talk->with)) { /* called away from a job, and forgot it: a reminder */
+        static char line[64];
+        snprintf(line, sizeof line, "Back to the %s, please", JOB_WORD[pausedJob(talk->with)]);
+        out[0] = line;
+    }
     if (talk->node == NODE_HELLO && g_talk_player) {
         static const char *const LIGHTER_LINE[] = { NULL, "Can I have my lighter back?", "Here's your lighter back", "Can I borrow your lighter?",
                                                     "Can I have a cigarette?", "Want a cigarette?" };
diff --git a/entropie/src/world/ladder.c b/entropie/src/world/ladder.c
index 2af8047..eda10c4 100644
--- a/entropie/src/world/ladder.c
+++ b/entropie/src/world/ladder.c
@@ -36,7 +36,8 @@
 #define STEP_WALL_GAP 0.08f   /* ... its back feet this far from that wall */
 #define STEP_OFF_UP   0.9f    /* ... from its top you get onto that if it's no higher than this over the top */
 #define FREE_AHEAD    0.6f    /* stood up on its own (nothing to lean on): this far in front of you */
-#define HOLD_OUT      0.6f    /* somebody holding it stands this far out from its foot */
+#define HOLD_OUT      0.3f    /* somebody holding it stands this far out from its foot ... */
+#define HOLD_BESIDE   0.48f   /* ... and this far to the side of its middle (the climber gets on and off in front) */
 
 static const Vec3 UP = { 0.0f, 1.0f, 0.0f };
 
@@ -226,12 +227,18 @@ float Ladder_steadiness(const Object *l) {
 void Ladder_hold(Object *who, Object *l) {
     if (who->holding_ladder && who->holding_ladder->held_by == who) who->holding_ladder->held_by = NULL;
     who->holding_ladder = l;
-    if (l) l->held_by = who;
+    if (!l) return;
+    l->held_by = who;
+    Vec3 across = vec3_normalize(vec3_cross(l->lean_normal, UP));
+    l->hold_side = vec3_dot(vec3_sub(who->position, l->position), across) >= 0.0f ? 1.0f : -1.0f; /* (the side they come from) */
 }
 
 Vec3 Ladder_holdSpot(const Object *l, Vec3 *facing) {
-    *facing = vec3_scale(l->lean_normal, -1.0f);
-    Vec3 at = vec3_add(l->position, vec3_scale(l->lean_normal, HOLD_OUT));
+    Vec3 across = vec3_normalize(vec3_cross(l->lean_normal, UP));
+    float side = l->hold_side != 0.0f ? l->hold_side : 1.0f;
+    /* beside its foot, a little out, turned to it (half facing the wall) */
+    *facing = vec3_normalize(vec3_add(vec3_scale(across, -side), vec3_scale(l->lean_normal, -0.6f)));
+    Vec3 at = vec3_add(l->position, vec3_add(vec3_scale(l->lean_normal, HOLD_OUT), vec3_scale(across, side * HOLD_BESIDE)));
     at.y = l->position.y;
     return at;
 }
@@ -720,12 +727,12 @@ void Ladder_poseHands(Object *who, float dt) {
     if (!l && atHold(who)) { /* holding it steady: both hands on the rails at chest height */
         const Object *hl = who->holding_ladder;
         Vec3 across = vec3_normalize(vec3_cross(hl->lean_normal, UP));
-        float s = who->look.height / 1.75f;
-        for (int h = 0; h < 2; h++) {
-            float side = h == HAND_LEFT ? 1.0f : -1.0f;
-            Vec3 at = pointAt(hl, 1.15f * s);
+        float s = who->look.height / 1.75f, side = hl->hold_side != 0.0f ? hl->hold_side : 1.0f;
+        for (int h = 0; h < 2; h++) { /* both on the rail on their side, one higher (the hand nearer the wall) */
+            bool high = (h == HAND_LEFT) == (side > 0.0f);
+            Vec3 at = pointAt(hl, (high ? 1.3f : 0.95f) * s);
             who->body.hand_active[h] = true;
-            who->body.hand_target[h] = vec3_add(at, vec3_add(vec3_scale(across, side * LADDER_WIDTH * 0.5f), vec3_scale(hl->lean_normal, 0.03f)));
+            who->body.hand_target[h] = vec3_add(at, vec3_add(vec3_scale(across, side * (LADDER_WIDTH * 0.5f + 0.03f)), vec3_scale(hl->lean_normal, 0.02f)));
         }
         return;
     }
@@ -755,9 +762,10 @@ void Ladder_poseFeet(Object *who, float dt) {
     if (!l && atHold(who) && who->body.pose == POSE_UP) { /* holding it: a foot on the bottom rung */
         const Object *hl = who->holding_ladder;
         float s = who->look.height / 1.75f;
-        Foot *foot = &who->body.feet[HAND_RIGHT];
         Vec3 across = vec3_normalize(vec3_cross(hl->lean_normal, UP));
-        Vec3 goal = vec3_add(pointAt(hl, rung(hl, 0)), vec3_add(vec3_scale(hl->lean_normal, 0.06f * s), vec3_scale(across, -0.06f * s)));
+        float side = hl->hold_side != 0.0f ? hl->hold_side : 1.0f;
+        Foot *foot = &who->body.feet[side > 0.0f ? HAND_RIGHT : HAND_LEFT]; /* (the foot nearer it: on the end of the bottom rung) */
+        Vec3 goal = vec3_add(pointAt(hl, rung(hl, 0)), vec3_add(vec3_scale(hl->lean_normal, 0.06f * s), vec3_scale(across, side * (LADDER_WIDTH * 0.5f - 0.08f))));
         goal.y += 0.06f * s; /* (ankle over the rung) */
         float k = fminf(1.0f, 6.0f * dt);
         foot->ankle = vec3_lerp(foot->ankle, goal, k);
diff --git a/entropie/src/world/ladder.h b/entropie/src/world/ladder.h
index 4a07f5b..5073734 100644
--- a/entropie/src/world/ladder.h
+++ b/entropie/src/world/ladder.h
@@ -51,7 +51,7 @@ float       Ladder_steadiness(const Object *ladder);
 void        Ladder_topple(World *world, Object *ladder, Vec3 dir);
 /* Holding it steady at its foot (both hands on the rails, a foot on the bottom rung), or not (NULL). */
 void        Ladder_hold(Object *who, Object *ladder);
-/* Where somebody stands to hold it (behind its foot), and which way they face. */
+/* Where somebody stands to hold it (beside its foot, out of the climber's way), and which way they face. */
 Vec3        Ladder_holdSpot(const Object *ladder, Vec3 *facing);
 
 /* Climbing: hands on the rungs (after Hands_update) and feet on them (after Body_update);
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 75ffbfb..bb64bb9 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -102,6 +102,7 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
     obj->top_speed = SPRINT_SPEED;
     obj->fitness = 0.5f;
     obj->wits    = 0.5f;
+    obj->height_fear = 0.3f;
     obj->jump_speed = JUMP_SPEED;
     obj->tap_timer = 1e3f;
     obj->position = position;
@@ -551,6 +552,23 @@ float Object_reach(const Object *obj) {
     return r;
 }
 
+#define HIGH_UP 1.6f /* m over the ground: up high */
+
+bool Object_upHigh(const Object *obj, float ground_y) {
+    const Object *on = obj->standing_on;
+    if (on && on->type == OBJ_WALL && on->variant == 3) return true; /* (a roof) */
+    return obj->is_grounded && obj->position.y - ground_y > HIGH_UP;
+}
+
+float Object_caution(const Object *obj) {
+    float d = fminf(obj->drunk, 1.0f);
+    float c = 0.35f + 0.5f * obj->height_fear - 0.15f * obj->fitness; /* (sporty: surer-footed) */
+    c += d * (obj->wits - 0.45f) * 0.8f; /* drunk: the sharp ones extra careful, the dim ones careless */
+    return clampf(c, 0.0f, 1.0f);
+}
+
+#define WORN_FEEL 0.6f /* worn out from the days before: this much tireder at most */
+
 /* Worn down by the day: how sleepy, felt more by the sluggish, the unfit and
    the drunk, hardly by the lively and sporty (0 = fresh .. 1 = done in). */
 float Object_tiredness(const Object *obj) {
@@ -558,7 +576,8 @@ float Object_tiredness(const Object *obj) {
     float sleepy = clampf((obj->needs.sleepiness - 0.25f) / 0.75f, 0.0f, 1.0f);
     float feels = (1.0f - 0.35f * clampf(obj->temperament.energy, -1.0f, 1.0f)) * (1.2f - 0.4f * obj->fitness)
                 + 0.4f * fminf(obj->drunk, 1.0f);
-    return clampf(sleepy * feels, 0.0f, 1.0f);
+    float worn = WORN_FEEL * obj->worn * (1.15f - 0.3f * obj->fitness); /* (yesterday still in the bones) */
+    return clampf(sleepy * feels + worn, 0.0f, 1.0f);
 }
 
 float Object_vigor(const Object *obj) {
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index bd1a75e..c1c8dca 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -273,7 +273,9 @@ typedef struct Object {
     float      chill;         /* characters: 0..1 cold (night, away from a fire): a jacket on over 0.6 */
     float      shake;         /* ladders: wobbling (seconds left) */
     float      stability;     /* ladders: how steady it stands, leaning (0..1; somebody holding it: steady) */
-    struct Object *held_by;   /* ladders: somebody holding it steady at its foot ... */
+    struct Object *held_by;   /* ladders: somebody holding it steady at its foot (beside it, hold_side: +1 / -1 along
+                                 cross(lean_normal, up), out of the climber's way) ... */
+    float      hold_side;
     struct Object *holding_ladder; /* characters: ... this one */
     float      tip, tip_v;    /* ladders: falling over: tipped this far from where it stood (rad), this fast ... */
     Vec3       tip_from, tip_dir; /* ... from that axis, towards that (horizontal) way */
@@ -311,6 +313,8 @@ typedef struct Object {
     float      top_speed;     /* sprinting flat out, fresh and sober (m/s) */
     float      fitness;       /* characters: how sporty (0..1), whatever the build: quicker, surer, smoother over things */
     float      wits;          /* characters: 0 = slow on the uptake .. 1 = sharp (sees what's wrong and sorts it out) */
+    float      worn;          /* characters: worn by the work of the days (0..1): sleep takes it off, a hard day leaves some for the next */
+    float      height_fear;   /* characters: 0 = no head for heights needed .. 1 = won't go up a ladder at all */
     float      jump_speed;    /* take-off speed of a standing jump, fresh and sober (m/s) */
     float      run_speed;     /* player: current speed, eased (m/s) */
     float      sprint_boost;  /* player: 0 = jogging .. 1 = flat out (tapping Shift) */
@@ -338,6 +342,11 @@ float   Object_rayToCollider(const Object *obj, int index, Vec3 origin, Vec3 dir
 /* How far obj's colliders reach from its position at most (a bounding sphere). */
 float   Object_reach(const Object *obj);
 
+/* Characters: up high (on a roof, or well over the ground)? */
+bool    Object_upHigh(const Object *obj, float ground_y);
+/* Characters up high: how careful (0 not at all .. 1 creeping): fear, drink and wits (drunk and dim: careless). */
+float   Object_caution(const Object *obj);
+
 /* Characters: 1 when fresh, down to 0.5 when exhausted (speed, strength, work). */
 float   Object_vigor(const Object *obj);
 /* Worn down by the day (sleepiness, felt by personality, fitness and drink): 0 fresh .. 1 done in. */
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index fbe5e19..f760c95 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -248,7 +248,7 @@ static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 nor
     if (vec3_length(mv) < 0.1f || vec3_length(out) < 1e-4f) return false;
     if (vec3_dot(vec3_normalize(mv), vec3_normalize(out)) > -0.4f) return false;
     const Collider *c = &other->colliders[ic];
-    if (c->axes[1].y < WALKABLE_SLOPE) return false; /* top face not walkable */
+    if (fmaxf(fabsf(c->axes[0].y), fmaxf(fabsf(c->axes[1].y), fabsf(c->axes[2].y))) < WALKABLE_SLOPE) return false; /* no face up flat enough (whichever axis: firewood lies on its side) */
 
     float top    = Object_colliderCenter(other, ic).y + projectRadius(c, WORLD_UP);
     if (fmaxf(fabsf(c->axes[0].y), fmaxf(fabsf(c->axes[1].y), fabsf(c->axes[2].y))) < 0.98f) {
@@ -880,8 +880,8 @@ static void beginStepOver(Object *obj, const Terrain *terrain, Vec3 dir, float n
         if (fabsf(lateral) < 0.08f * s) lateral = (k == 0 ? -0.1f : 0.1f) * s; /* (feet apart) */
         float at = k == obj->over_lead ? land_at : land_at + 0.08f * s;
         Vec3 land = vec3_add(obj->position, vec3_add(vec3_scale(dir, at), vec3_scale(right, lateral)));
-        land.y = up || !terrain ? obj->position.y + top + (b->feet[k].ankle.y - obj->position.y)
-                    : Terrain_height(terrain, land.x, land.z) + (b->feet[k].ankle.y - Terrain_height(terrain, b->feet[k].ankle.x, b->feet[k].ankle.z));
+        /* (standing flat there: the ankle its own height over the surface - not lifted as it may be now, on the toes) */
+        land.y = (up || !terrain ? obj->position.y + top : Terrain_height(terrain, land.x, land.z)) + BODY_ANKLE_Y * s;
         obj->over_land[k] = land;
     }
     obj->movement = vec3_zero();
diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c
index 1fefce5..4a7caab 100644
--- a/entropie/src/world/scene.c
+++ b/entropie/src/world/scene.c
@@ -87,6 +87,8 @@ static const bool LIGHTER[]  = { true, true, false, false, true }; /* (a lighter
 /* Smoking: how much (0 = not at all), a pack, tobacco to roll (Mila smokes now and then, and bums them). */
 static const float SMOKER[]     = { 0.3f, 0.6f, 0.0f, 0.25f, 0.9f };
 static const float WITS[]       = { 0.6f, 0.45f, 0.8f, 0.6f, 0.25f }; /* (Ben is slow on the uptake) */
+static const float HEIGHT_FEAR[] = { 0.2f, 0.1f, 0.85f, 0.15f, 0.5f }; /* (Ida won't go up a ladder) */
+static const float YESTERDAY[]   = { 0.15f, 0.2f, 0.1f, 0.25f, 0.55f }; /* worn from the days before (Ben: a long one) */
 static const int   CIGARETTES[] = { 10, 0, 0, 0, 6 };
 static const int   TOBACCO[]    = { 0, 12, 0, 0, 20 };
 static const int  GARMENT[]  = { GARMENT_TROUSERS, GARMENT_SHORTS, GARMENT_SKIRT, GARMENT_DRESS, GARMENT_TROUSERS };
@@ -195,6 +197,8 @@ Object *Scene_load(World *world) {
             obj->lighter_uses = obj->lighter ? LIGHTER_USES * (1 + characters % 3) / 3 : 0; /* (some nearly empty) */
             obj->smoker     = SMOKER[characters % TEMPERAMENT_COUNT];
             obj->wits       = WITS[characters % TEMPERAMENT_COUNT];
+            obj->height_fear = HEIGHT_FEAR[characters % TEMPERAMENT_COUNT];
+            obj->worn       = YESTERDAY[characters % TEMPERAMENT_COUNT];
             obj->jacket_hung = true; /* (on the rack in your garage) */
             obj->cigarettes = CIGARETTES[characters % TEMPERAMENT_COUNT];
             obj->tobacco    = TOBACCO[characters % TEMPERAMENT_COUNT];
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index be54359..1073da4 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -811,6 +811,8 @@ static void animateCharacter(World *world, Object *o, float dt) {
         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);
+        if (o->type == OBJ_HUMAN && !o->sitting && Object_upHigh(o, Terrain_height(&world->terrain, o->position.x, o->position.z)))
+            o->body.balance_goal = fmaxf(o->body.balance_goal, 0.5f * Object_caution(o)); /* (up on a roof: arms out a little, the careful more) */
     }
     BodyFrame frame = {
         .position   = o->position,