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

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 2.39.0: the story starts - the crew

Christian Immanuel · 2026-10-27 12:32 · c39a1aededa0a4c2701c5cfad764cf1d257c10e6

- The plan for the game in STATE.md: from the first meeting in Kreuzberg
  to the drive home after the clean-up, the four floors, the crew, the
  roadmap
- Everybody is one entry of a cast table (world/cast.c) with their part in
  the festival: role, the project they lead or help with, commitment
- The crew turns up over the first day on the ground: Kilian and Julia,
  Stella and Doro, Kerry, Rouven and Pascal; over to the fire to say hello
- F2 (dev): the crew overview - who's who, what everybody does now, the
  projects with their leads and helpers
 commit_message.txt                |  14 ++-
 entropie/ARCHITECTURE.md          |   4 +-
 entropie/Makefile                 |   2 +-
 entropie/README.md                |   3 +-
 entropie/STATE.md                 | 100 ++++++++++++++++++-
 entropie/commit_message.txt       |  14 ++-
 entropie/src/core/input.c         |   1 +
 entropie/src/core/input.h         |   1 +
 entropie/src/game/game.c          |   1 +
 entropie/src/game/game.h          |   8 ++
 entropie/src/game/game_actions.c  |   4 +-
 entropie/src/game/game_dev.c      |   5 +-
 entropie/src/game/game_internal.h |   2 +
 entropie/src/game/game_story.c    | 140 +++++++++++++++++++++++++++
 entropie/src/game/save.c          |  10 +-
 entropie/src/main.c               |  17 ++++
 entropie/src/people/ai.c          |   8 +-
 entropie/src/people/ai.h          |   1 +
 entropie/src/render/figure.c      |  10 +-
 entropie/src/render/ui.h          |   2 +-
 entropie/src/world/cast.c         | 199 ++++++++++++++++++++++++++++++++++++++
 entropie/src/world/cast.h         |  60 ++++++++++++
 entropie/src/world/object.c       |   2 +
 entropie/src/world/object.h       |   3 +
 entropie/src/world/scene.c        | 140 ++-------------------------
 25 files changed, 592 insertions(+), 159 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index c9d5195..4664bdf 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,5 +1,11 @@
-entropie 2.38.0: a shelter in the yard
+entropie 2.39.0: the story starts - the crew
 
-- A shelter by the fire: a roof on posts, two log benches and a table
-- In hard rain the guests sit it out there or in your garages' kitchen,
-  whichever is nearer with a seat free
+- The plan for the game in STATE.md: from the first meeting in Kreuzberg
+  to the drive home after the clean-up, the four floors, the crew, the
+  roadmap
+- Everybody is one entry of a cast table (world/cast.c) with their part in
+  the festival: role, the project they lead or help with, commitment
+- The crew turns up over the first day on the ground: Kilian and Julia,
+  Stella and Doro, Kerry, Rouven and Pascal; over to the fire to say hello
+- F2 (dev): the crew overview - who's who, what everybody does now, the
+  projects with their leads and helpers
diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index 2c99e45..3167797 100644
--- a/entropie/ARCHITECTURE.md
+++ b/entropie/ARCHITECTURE.md
@@ -10,9 +10,9 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL
 | `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input, sound out (`audio.c`: a synthesizer and mixer, no sound files) | core |
 | `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math |
 | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render |
-| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game |
+| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), everybody in the story (`cast.c`), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game |
 | `src/people/` | bodies (animation), hands, AI, talk, condition, dog, climbing | everything but game |
-| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, saves (`save.c`), what you hear (`sound.c`) | all |
+| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, the story (`game_story.c`: the crew turning up, the crew overview), saves (`save.c`), what you hear (`sound.c`) | all |
 
 - `render` knows nothing of the world: it gets meshes, matrices and plain numbers.
 - `world` and `people` are one layer: an `Object` owns its `Body`, `Ai` and `Hands`.
diff --git a/entropie/Makefile b/entropie/Makefile
index 15eaaa1..ebf5bd3 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.38.0
+VERSION   := 2.39.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 67c8158..d32963a 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -78,6 +78,7 @@ people living there.
 | I             | Show all keys in game |
 | Tab           | Your stats (Left / Right: other page) |
 | F1            | Dev menu: pick anyone, change body, needs, temperament, likes and skills (Up / Down, Left / Right); teleport you to the parkour or the yard; time of day, game speed, weather, wind, skip the start |
+| F2            | Dev: the crew overview - who's who, what they do now, the projects (Left / Right) |
 | Esc           | Menu: resume, save, load, keys, quit (the game waits meanwhile) |
 | F5 / F9       | Quick save / quick load |
 | M             | Sound off / on |
@@ -124,5 +125,5 @@ Dependencies point downward: `main → game → world/people → render → core
 
 ## Version
 
-`make version` prints e.g. `entropie 2.38.0 (build ee5055b)`. The build id is
+`make version` prints e.g. `entropie 2.39.0 (build 2e7d256)`. The build id is
 an md5 of all sources and assets.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 7837b1a..61fd6c2 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -3,6 +3,85 @@
 Where the game stands: everything there is, and what is still open.
 Updated with every version (see the version line at the end).
 
+## The story (plan)
+
+The game is the making of a small festival on a farm in Wriezen, from the
+first idea to driving home after the clean-up. What's fun: getting it built
+with people who have their own minds, and finding out why festivals today do
+things the way they do.
+
+**Chapters**
+1. Kreuzberg, Grossbeerenstrasse 37, 3rd floor: the first meeting - "we do a
+   festival".
+2. The "Herberge" bar, several evenings: where (Wriezen), who leads what.
+   Between weekends on the ground, back to the Herberge for the next
+   decisions. The base ideas are settled here, not on the ground.
+3. The build-up on the ground (three months, played as weekends there): the
+   floors, the snail rescue, the mowing, the wood, the shopping. Small
+   decisions are made on the spot by whoever leads that task.
+4. The festival, Friday to Sunday. Saturday afternoon a bad storm: get
+   everybody out of harm's way (evacuate).
+5. The clean-up: whoever is still happy to help, and the supporters (a free
+   ticket for 2 x 6 h of work each, during the festival and after).
+6. The end: into the car and off home.
+
+**The floors**
+- Hofbuehne (the yard): bands, poetry slam, breakdance, rock, punk. Pascal:
+  the light, and a proper floor for the stage.
+- Obstwiese (the orchard): psytrance. Kilian builds it.
+- Kartoffelacker (the potato field): world music, small talks. The grass is
+  cut with a scythe (the ground is too uneven for a mower: Toni). The group
+  from Darmstadt brings a geo-dome.
+- The hidden floor behind the main building: techno.
+
+**The crew** (who leads what is settled in the Herberge; it can change)
+- Volkram: leads the festival organisation; the web things with you.
+- You: the web things; you support or take over any task someone drops.
+- Toni: artist organisation with Rouven; builds along; the scythe.
+- Rouven: artist organisation.
+- Stella and Doro: art and decoration; they lead the snail rescue.
+- Pascal: the light, the floor of the main stage.
+- Kilian: builds the Obstwiese (psytrance) stage.
+- Kerry: wood art with the chainsaw (needs logs and the chainsaw).
+- Julia: quick at every job, very good at building.
+- The ES-Bar collective: the food stand. The Darmstadt group: the geo-dome.
+- Jana: one of the crew (her task not settled yet).
+- Kurti (the farm is his) and his sons Martin and Jonas: the hosts.
+
+**How it plays**
+- Leads decide the small things of their task. Mood and commitment change
+  with what happens (good or bad turns, how they're treated); someone fed
+  up may refuse or quit - then the task needs a new lead (you, or someone
+  else), and the best person for a task may not end up doing it. The
+  festival is as good as what got done.
+- Projects: each floor and side job has stages (ground ready, frame, roof,
+  floor, paint, decoration, light and sound). Materials: wood from the
+  forest cut to beams and boards; nails, screws and paint bought at the
+  hardware store (by car, from the crew's cash box).
+- Side jobs: mow the Kartoffelacker (scythe); pick up the vineyard snails
+  (protected) before the regulatory office comes; Kerry's chainsaw art.
+- The "why" questions (an awareness team, Urinellas, "no shirt, no
+  service", ...): found out through what happens on the way; each answer
+  opens an option for the festival.
+- Dev tools: an overview of everybody - role, lead of what, where, doing
+  what, mood and commitment.
+
+**Roadmap** (one release each, every one playable)
+1. The crew: one cast table with roles, the new people arriving over the
+   first days, the crew overview (dev). (2.39)
+2. Projects and leads: the floors and side jobs as projects with stages, a
+   lead and helpers; leads decide; mood and commitment; quitting and
+   taking over.
+3. Materials and money: the cash box, the hardware store, boards and beams,
+   nails, screws, paint.
+4. The side jobs: the scythe on the Kartoffelacker, the snail rescue, Kerry's
+   chainsaw art.
+5. Berlin: Grossbeerenstrasse and the Herberge as places, the meetings, going
+   back and forth between weekends.
+6. The "why" questions and their options.
+7. The festival (guests, the floors running, the storm and the evacuation),
+   the clean-up with the supporters, the drive home.
+
 ## People
 
 | | You | Volkram | Jana | Toni | Kurti | Martin | Jonas |
@@ -27,6 +106,8 @@ Updated with every version (see the version line at the end).
 | Afraid of heights | .2 | .15 | .5 | .1 | .7 | .05 | .2 |
 | Worn from the days before (start) | .15 | .2 | .1 | .35 | .4 | .2 | .1 |
 
+Everybody is one entry of `world/cast.c` (CastMember: look, bearing, skills, pockets, and their part: `role`, the Project they `leads` / `helps`, `commitment`, when they `arrives`); the table above is the first seven. The crew turning up later (hours on the ground): Kilian and Julia (2 h), Stella and Doro (5 h), Kerry (20 h), Rouven and Pascal (26 h) - roles as in "The story (plan)". Hats: Toni a red cap, Kurti a green one, Jonas and Kerry beanies, Pascal a black cap.
+
 Alice the dog: follows, can be petted and called.
 
 ### What each trait does
@@ -289,6 +370,12 @@ Painted in the rotoscope style of *Waking Life* (2001):
 - A strong gust may blow an open umbrella inside out (no use for a moment,
   then pushed back right); the third time, or in a storm, the ribs bend and
   it's broken (the others drop theirs and get in out of the rain)
+- The crew turns up over the first day on the ground (Kilian and Julia, then
+  Stella and Doro, the next morning Kerry, then Rouven and Pascal): in from
+  the gravel road, over to the fire to say hello, then on with the day
+- F2 (dev): the crew overview - who's who and what they're there for, what
+  each is doing now (into it / annoyed / energy), the projects with who
+  leads and who helps (Left / Right)
 - Some wear a hat: Toni a red cap, Kurti an old green one, Jonas a beanie.
   A gust can take a cap off (a beanie hardly ever, none under a hood): it
   bowls along the ground, they run after it and put it back on - pulled down
@@ -1235,10 +1322,19 @@ Round 2.38 (a shelter in the yard):
 - `manor.c` Shelter_build: a roof on six posts (5 x 3.6 m, eaves 2.25 m, a gable 1 m up; its roof slabs solid, so `Weather_covered` holds under it), paving slabs, a log bench along each long side (3.4 m) and a table between them; in the yard at court(SHELTER_A 24.5, SHELTER_B 3.5), about 11 m from the fire towards the barn, along the garages.
 - Rain places (`Site_rainPlaces`: the garages' kitchen, the shelter): sheltering guests pick the nearest with a free seat (`ai.rain_place`, chosen once per shower); a bench is walked up to from the front where along it they are (round its end they got stuck outside a post and never sat). Checked: hard rain at midday - all three guests on one shelter bench within 20-60 s, dry, up when it stopped.
 
-## Open tasks
+Round 2.39 (the story starts: the crew):
+- The plan for the game (story, chapters, floors, crew, how it plays, the roadmap): "The story (plan)" at the top.
+- `world/cast.c`: one entry per person (CastMember) instead of a dozen parallel tables in scene.c; `Cast_create` makes the person (scene.c for the first seven, and later arrivals); `Object.cast_id` (-1 none), `commitment`, `leads`, `helps` (saved). Projects (`Project`): organisation, web, artists, decoration, light, the four floors, mowing, the snail rescue, wood art.
+- `game/game_story.c`: `game.story_hours` (game hours since the start on the ground, saved); who's due turns up at the gravel road (the 5th point of the way in by car), `ai.greet` with `greet_left` HELLO_TIME 90 s near the fire (greetHosts: then on with the day; the first friends keep theirs till the start says so). Arrived crew is made again on loading (`cast` in the save).
+- F2 crew overview (`Game_crewLines`, three pages; `main.c` drawCrew, labels 120-139). Checked: all seven turned up at 2 / 5 / 20 / 26 h, at the fire, then about; saved and loaded with all 14 there, no doubles.
+
 
 Most important first.
 
+**The story** (see "The story (plan)": the roadmap)
+- Next: projects and leads (the floors and side jobs as projects with stages, a lead and helpers; leads decide; mood and commitment; quitting and taking over)
+- Then: materials and money; the side jobs (scythe, snails, chainsaw art); Berlin (Grossbeerenstrasse, the Herberge); the "why" questions; the festival, the storm, the clean-up, the drive home
+
 **Bugs**
 - Joint jumps left in the audit: jog+stop 171, sprint 297 (worst 26 cm) - running into things: a hurried catch step swings through in a few frames (step 0.07 -> 0.34, the ankle up 20 cm). Capping the swing speed helped sprint (278) but made jumps, drunk walk and the NPCs worse - tried and reverted in 1.83; ladder 45: toes on the sloping roof (a landing step's toes snap onto the slope, 16 cm; blending them in made all walking worse), toes turning onto the rungs getting on from the top (14 cm). Note (2.35): the audit is chaotic - each part starts from where the one before left you, so one small change anywhere reshuffles the later parts (a fall in the drunk walk or not, the ladder test not getting on the ladder); compare parts run on their own (the harness: LADDERONLY / JUMPSONLY / JOGONLY). Tried in 2.35: easing the running swing's ends (sprint worse), a swing not hurrying when stopping (no better on its own)
 
@@ -1270,4 +1366,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 2.38.0 (build ee5055b)`
+State of `entropie 2.39.0 (build 2e7d256)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index c9d5195..4664bdf 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,5 +1,11 @@
-entropie 2.38.0: a shelter in the yard
+entropie 2.39.0: the story starts - the crew
 
-- A shelter by the fire: a roof on posts, two log benches and a table
-- In hard rain the guests sit it out there or in your garages' kitchen,
-  whichever is nearer with a seat free
+- The plan for the game in STATE.md: from the first meeting in Kreuzberg
+  to the drive home after the clean-up, the four floors, the crew, the
+  roadmap
+- Everybody is one entry of a cast table (world/cast.c) with their part in
+  the festival: role, the project they lead or help with, commitment
+- The crew turns up over the first day on the ground: Kilian and Julia,
+  Stella and Doro, Kerry, Rouven and Pascal; over to the fire to say hello
+- F2 (dev): the crew overview - who's who, what everybody does now, the
+  projects with their leads and helpers
diff --git a/entropie/src/core/input.c b/entropie/src/core/input.c
index 8fa40c1..bb1e44e 100644
--- a/entropie/src/core/input.c
+++ b/entropie/src/core/input.c
@@ -50,6 +50,7 @@ void Input_poll(Input *input) {
         case SDL_SCANCODE_DOWN:   input->menu_move = 1;      break;
         case SDL_SCANCODE_UP:     input->menu_move = -1;     break;
         case SDL_SCANCODE_F1:     input->dev_menu = true;    break;
+        case SDL_SCANCODE_F2:     input->crew_menu = true;   break;
         case SDL_SCANCODE_F5:     input->quick_save = true;  break;
         case SDL_SCANCODE_F9:     input->quick_load = true;  break;
         case SDL_SCANCODE_M:      input->mute = true;        break;
diff --git a/entropie/src/core/input.h b/entropie/src/core/input.h
index f77a2d8..045b77d 100644
--- a/entropie/src/core/input.h
+++ b/entropie/src/core/input.h
@@ -42,6 +42,7 @@ typedef struct {
     int   page;          /* Right / Left arrow pressed: next / previous page (stats) */
     int   menu_move;     /* Down / Up arrow pressed: next / previous line (dev menu) */
     bool  dev_menu;      /* F1 pressed: dev menu on / off */
+    bool  crew_menu;     /* F2 pressed: the crew overview on / off (dev) */
     bool  pockets;       /* B pressed: your pockets open / shut */
     char  text[32];      /* typed this frame (only while typing a name: see Platform_typing) */
     bool  erase;         /* Backspace pressed (typing: one letter back) */
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 69f68cd..aea6dc9 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -371,6 +371,7 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
         say(game, "You're wet through - by a fire you dry quickest");
     } else if (game->player->look.wet < 0.2f) game->wet_told = false;
     game_arrival(game, dt);
+    game_story(game, game->world.game_dt);
     Ai_setSettled(game->arrival.task == TASK_NONE);
     tooHeavy(game);
     bool warm = game->player->look.top == TOP_JACKET;
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index 5ce9c36..d60815b 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -83,6 +83,11 @@ typedef struct {
     bool     rain_told;     /* told it's raining (this shower) */
     bool     wet_told;      /* "wet through" said (till dry again) */
     float    blow_told;     /* "your tent is blowing about" said: not again for a while */
+    float    story_hours;   /* game hours since the start on the ground (the crew turns up by it: game_story.c) */
+    float    story_check;   /* until the next look at who's due */
+    char     story_line[128]; /* what was said about it (shown as the message) */
+    bool     show_crew;     /* the crew overview (F2, dev) ... */
+    int      crew_page;     /* ... which page (Left / Right): who's who, now, the projects */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
@@ -134,6 +139,9 @@ void Game_renderAir(Game *game, Renderer *renderer);
 int Game_pocketLines(Game *game, char lines[][96], int max, int *picked);
 
 #define DEV_LINES 13
+/* The crew overview (F2) as lines of text, its title. Returns how many. */
+int Game_crewLines(const Game *game, char lines[][128], int max, const char **title);
+
 /* The dev menu as lines of text; *picked: the line picked. Returns how many. */
 int Game_devLines(const Game *game, char lines[][96], int max, int *picked);
 
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index cf9c432..eb7e41b 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -1362,6 +1362,7 @@ void game_handleActions(Game *game, const Input *input) {
     Object *const *objects = game->world.objects;
     int count = game->world.count;
 
+    game_dev(game, input); /* (the dev menus: in the car too) */
     if (p->riding) { /* (in the car: looking about only) */
         game->click_action = CLICK_NONE;
         if (input->show_keys) game->show_keys = !game->show_keys;
@@ -1370,9 +1371,8 @@ void game_handleActions(Game *game, const Input *input) {
     if (input->toggle_view) game->third_person = !game->third_person;
     if (input->show_keys) game->show_keys = !game->show_keys;
     if (input->show_stats) game->show_stats = !game->show_stats;
-    game_dev(game, input);
     game_pockets(game, input);
-    if (input->page && game->show_stats && !game->show_dev) game->stats_page = (game->stats_page + input->page + STATS_PAGES) % STATS_PAGES;
+    if (input->page && game->show_stats && !game->show_dev && !game->show_crew) game->stats_page = (game->stats_page + input->page + STATS_PAGES) % STATS_PAGES;
     if (input->call_help) game_callHelp(game);
     if (input->dismiss_help) dismissHelp(game);
     if (input->cancel) callOff(game);
diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c
index 0b8e8b2..39d8a71 100644
--- a/entropie/src/game/game_dev.c
+++ b/entropie/src/game/game_dev.c
@@ -19,6 +19,7 @@ typedef enum {
 } DevRow;
 
 static const char *const JOB[4] = { "firewood", "trees", "trash", "fire" };
+#define CREW_PAGES 3
 
 /* The people (you first), and the one picked. */
 static int people(const Game *game, Object **out, int max) {
@@ -78,7 +79,9 @@ static void setFitness(Object *o, float fit) {
 }
 
 void game_dev(Game *game, const Input *input) {
-    if (input->dev_menu) game->show_dev = !game->show_dev;
+    if (input->crew_menu) { game->show_crew = !game->show_crew; if (game->show_crew) game->show_dev = false; }
+    if (game->show_crew && input->page) { game->crew_page = (game->crew_page + input->page + CREW_PAGES) % CREW_PAGES; return; }
+    if (input->dev_menu) { game->show_dev = !game->show_dev; if (game->show_dev) game->show_crew = false; }
     if (!game->show_dev) return;
     if (input->menu_move) game->dev_row = (game->dev_row + input->menu_move + DEV_ROWS) % DEV_ROWS;
     int dir = input->page;
diff --git a/entropie/src/game/game_internal.h b/entropie/src/game/game_internal.h
index be9bda5..7f52a3b 100644
--- a/entropie/src/game/game_internal.h
+++ b/entropie/src/game/game_internal.h
@@ -38,6 +38,8 @@ void game_climbLadder(Game *game, const Input *in, float dt);
 void game_holdHose(Game *game);
 void game_handleActions(Game *game, const Input *input);
 void game_dev(Game *game, const Input *input);
+/* The story: hours on the ground counted, the crew turning up when they're due (game_story.c). */
+void game_story(Game *game, float game_dt);
 
 /* H: everybody stops helping. */
 static inline void dismissHelp(Game *game) {
diff --git a/entropie/src/game/game_story.c b/entropie/src/game/game_story.c
new file mode 100644
index 0000000..9426749
--- /dev/null
+++ b/entropie/src/game/game_story.c
@@ -0,0 +1,140 @@
+/*
+ * The story, as far as it goes: the hours on the ground counted; the crew
+ * (world/cast.c) turns up when they're due - in from the gravel road, over to
+ * the fire to say hello, then on with the day.
+ */
+
+#include <math.h>
+#include <stdio.h>
+
+#include "game/game_internal.h"
+#include "world/cast.h"
+#include "world/site.h"
+
+#define STORY_CHECK  1.0f   /* real s between looks at who's due */
+#define WALK_IN_AT   4      /* where they turn up: this point of the way in by car (the gravel road by the main building) */
+#define WALK_IN_GAP  1.2f   /* m between people turning up together */
+#define HELLO_TIME   90.0f  /* s at the fire saying hello before they get on with things */
+#define CREW_PAGES_SHOWN 3 /* the overview: who's who, now, the projects (game_dev.c CREW_PAGES the same) */
+
+static bool here(const Game *game, int id) {
+    for (int i = 0; i < game->world.count; i++)
+        if (game->world.objects[i]->is_character && game->world.objects[i]->cast_id == id) return true;
+    return false;
+}
+
+void game_story(Game *game, float game_dt) {
+    if (game->arrival.stage == ARRIVAL_DRIVING) return; /* (the clock starts once you're there) */
+    game->story_hours += game_dt / 3600.0f;
+    if ((game->story_check -= game->dt) > 0.0f) return;
+    game->story_check = STORY_CHECK;
+    Vec3 path[16];
+    int n = 0;
+    float yaw = 0.0f;
+    Site_arrivalPath(path, &n, &yaw);
+    if (n <= WALK_IN_AT) return;
+    char names[96] = "";
+    int come = 0;
+    for (int id = 1; id < Cast_count(); id++) {
+        const CastMember *c = Cast_get(id);
+        if (c->arrives <= 0.0f || game->story_hours < c->arrives || here(game, id)) continue;
+        Vec3 at = path[WALK_IN_AT];
+        at.x += WALK_IN_GAP * (float)come;
+        at.y = World_groundHeight(&game->world, at.x, at.z, at.y + 3.0f);
+        Object *o = Cast_create(id, at);
+        if (!o) continue;
+        if (!World_add(&game->world, o)) { Object_destroy(o); continue; }
+        o->ai.mode = AI_IDLE;
+        o->ai.greet = true;              /* (over to the fire, hello) */
+        o->ai.greet_left = HELLO_TIME;
+        o->ai.side = (float)(come % 3 - 1);
+        Ai_say(o, come == 0 ? "Hello everybody - we're here!" : "Hi! Where's the fire?");
+        size_t len = 0;
+        while (names[len]) len++;
+        snprintf(names + len, sizeof names - len, "%s%s", come ? " and " : "", c->name);
+        come++;
+    }
+    if (come) {
+        snprintf(game->story_line, sizeof game->story_line, "%s %s turned up", names, come > 1 ? "have" : "has");
+        say(game, game->story_line);
+    }
+}
+
+/* ---------- The crew overview (F2) ---------- */
+
+static Object *personOf(const Game *game, int id) {
+    for (int i = 0; i < game->world.count; i++)
+        if (game->world.objects[i]->is_character && game->world.objects[i]->cast_id == id) return game->world.objects[i];
+    return NULL;
+}
+
+/* What they're up to, said plainly. */
+static void doing(const Object *o, char *out, size_t n) {
+    const Ai *ai = &o->ai;
+    static const char *const JOBS[ORDER_COUNT] = { "-", "firewood", "trees", "the trash", "the fire" };
+    if (o->type == OBJ_PLAYER) { snprintf(out, n, "(you)"); return; }
+    if (Body_isDown(&o->body))      snprintf(out, n, o->body.asleep ? "asleep" : "down on the ground");
+    else if (o->in_tent)            snprintf(out, n, "in the tent");
+    else if (o->riding)             snprintf(out, n, "in the car");
+    else if (o->indoors > 0.0f)     snprintf(out, n, "in the house");
+    else if (ai->greet)             snprintf(out, n, "saying hello at the fire");
+    else if (ai->share)             snprintf(out, n, "under %s's umbrella", ai->share->name);
+    else if (ai->sheltering)        snprintf(out, n, o->sitting ? "sitting out the rain" : "out of the rain");
+    else if (ai->mode == AI_WORK)   snprintf(out, n, "working: %s", JOBS[ai->order >= 0 && ai->order < ORDER_COUNT ? ai->order : 0]);
+    else if (ai->mode == AI_HELP)   snprintf(out, n, "helping %s", ai->leader ? ai->leader->name : "somebody");
+    else if (ai->mode == AI_FOLLOW) snprintf(out, n, "with you");
+    else if (ai->mode == AI_PEE)    snprintf(out, n, "off for a pee");
+    else if (ai->mode == AI_DRINK || ai->mode == AI_SUPPLY) snprintf(out, n, "eating or drinking");
+    else if (ai->mode == AI_TABLE)  snprintf(out, n, "at a table");
+    else if (ai->mode == AI_WAKE)   snprintf(out, n, "waking somebody");
+    else if (ai->mode == AI_APPROACH) snprintf(out, n, "coming over to you");
+    else if (ai->mode == AI_FETCH)  snprintf(out, n, "fetching something");
+    else if (ai->mode == AI_WANDER) snprintf(out, n, "strolling about");
+    else                            snprintf(out, n, o->sitting ? "sitting" : "standing about");
+}
+
+int Game_crewLines(const Game *game, char lines[][128], int max, const char **title) {
+    static char head[96];
+    int n = 0, page = ((game->crew_page % CREW_PAGES_SHOWN) + CREW_PAGES_SHOWN) % CREW_PAGES_SHOWN;
+    int day = 1 + (int)(game->story_hours / 24.0f);
+    if (page == 0) {
+        snprintf(head, sizeof head, "Crew - who's who  (day %d on the ground)   Right: what they do now", day);
+        for (int id = 0; id < Cast_count() && n < max; id++) {
+            const CastMember *c = Cast_get(id);
+            snprintf(lines[n++], 128, "%s:  %s", c->name, c->role);
+        }
+    } else if (page == 1) {
+        snprintf(head, sizeof head, "Crew - now  (%.1f h on the ground)   into it / annoyed / energy", (double)game->story_hours);
+        for (int id = 0; id < Cast_count() && n < max; id++) {
+            const CastMember *c = Cast_get(id);
+            const Object *o = personOf(game, id);
+            if (!o) {
+                snprintf(lines[n++], 128, "%s:  not here yet (turns up in %.0f h)", c->name, (double)fmaxf(0.0f, c->arrives - game->story_hours));
+                continue;
+            }
+            char what[64];
+            doing(o, what, sizeof what);
+            snprintf(lines[n++], 128, "%s:  %s   -   %.0f%% / %.0f%% / %.0f%%", c->name, what, (double)(o->commitment * 100.0f),
+                     (double)(o->ai.annoyed * 100.0f), (double)(o->needs.stamina * 100.0f));
+        }
+    } else {
+        snprintf(head, sizeof head, "Projects - who leads, who helps   (Left: back)");
+        for (int p = PROJECT_ORGA; p < PROJECT_COUNT && n < max; p++) {
+            char lead[48] = "nobody yet", helpers[80] = "";
+            size_t len = 0;
+            for (int id = 0; id < Cast_count(); id++) {
+                const CastMember *c = Cast_get(id);
+                const Object *o = personOf(game, id);
+                int leads = o ? o->leads : (int)c->leads, helps = o ? o->helps : (int)c->helps;
+                if (leads == p) snprintf(lead, sizeof lead, "%s%s", c->name, o ? "" : " (not here yet)");
+                else if (helps == p && len < sizeof helpers - 12) {
+                    snprintf(helpers + len, sizeof helpers - len, "%s%s", len ? ", " : "", c->name);
+                    while (helpers[len]) len++;
+                }
+            }
+            snprintf(lines[n++], 128, "%s:  %s%s%s", Cast_projectName((Project)p), lead, len ? "   helping: " : "", helpers);
+        }
+    }
+    *title = head;
+    return n;
+}
diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c
index 76fcde3..c7bad6d 100644
--- a/entropie/src/game/save.c
+++ b/entropie/src/game/save.c
@@ -2,6 +2,7 @@
 #include "game/save.h"
 #include "game/game_internal.h"
 #include "world/car.h"
+#include "world/cast.h"
 #include "world/clothes.h"
 #include "world/ladder.h"
 #include "world/tent.h"
@@ -64,7 +65,7 @@ static const Field PERSON[] = {
     VE("skin", look.skin), VE("hair", look.hair), VE("shirt", look.shirt), VE("pants", look.pants), VE("shoes", look.shoes),
     FL("height", look.height), IN("hair_style", look.hair_style), BO("long_sleeves", look.long_sleeves), IN("garment", look.garment),
     IN("top", look.top), VE("jacket", look.jacket), BO("jacket_tied", look.jacket_tied), FL("belly", look.belly),
-    BO("keeps_shirt", look.keeps_shirt), FL("wet", look.wet), BO("hoodie", look.hoodie), IN("hat", look.hat), IN("hats_lost", hats_lost), VE("hat_color", look.hat_color),
+    BO("keeps_shirt", look.keeps_shirt), FL("wet", look.wet), BO("hoodie", look.hoodie), IN("hat", look.hat), IN("cast", cast_id), FL("commitment", commitment), IN("leads", leads), IN("helps", helps), IN("hats_lost", hats_lost), VE("hat_color", look.hat_color),
     FL("ladder_skill", ladder_skill), FL("fitness", fitness), FL("wits", wits), FL("worn", worn), FL("height_fear", height_fear),
     FL("top_speed", top_speed), FL("jump_speed", jump_speed), FL("pace", pace), FL("tempo", tempo), IN("main_hand", main_hand),
     BO("female", female), BO("urinella", urinella), IN("urinella_tried", urinella_tried), IN("urinellas", urinellas),
@@ -183,6 +184,7 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err,
     fprintf(f, "arrival %d %d %d %.9g %d %.9g %d %.9g %d %d\n", a->stage, a->task, a->step, (double)a->time, a->welcome,
             (double)a->welcome_t, a->chat, (double)a->chat_wait, a->greeted, idOf(&ids, a->car));
     fprintf(f, "told %d %d\n", game->rain_told, game->wet_told);
+    fprintf(f, "story %.9g\n", (double)game->story_hours);
 
     for (int i = 0; i < w->count; i++) {
         const Object *o = w->objects[i];
@@ -442,6 +444,7 @@ static void applyGame(Game *game, const Parsed *p) {
             a->welcome = numI(&s); a->welcome_t = numF(&s); a->chat = numI(&s); a->chat_wait = numF(&s); a->greeted = numI(&s) != 0;
             a->car = byId(p, numI(&s));
         } else if (!strcmp(key, "told")) { game->rain_told = numI(&s) != 0; game->wet_told = numI(&s) != 0; }
+        else if (!strcmp(key, "story")) game->story_hours = numF(&s);
     }
 }
 
@@ -475,6 +478,7 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
     for (int k = 0; k < p.recs; k++) {
         Rec *r = &p.rec[k];
         Vec3 pos = vec3_zero(), size = vec3_zero(), rot = vec3_zero();
+        int cast = -1;
         for (int i = r->first; i < r->first + r->lines; i++) {
             char buf[256], *s = buf;
             snprintf(buf, sizeof buf, "%s", p.line[i]);
@@ -482,6 +486,7 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
             if (!strcmp(key, "pos")) pos = numV(&s);
             else if (!strcmp(key, "size")) size = numV(&s);
             else if (!strcmp(key, "rot")) rot = numV(&s);
+            else if (!strcmp(key, "cast")) cast = numI(&s);
         }
         if (r->scene_id > 0) {
             r->obj = taken ? findScene(w, r, taken) : NULL;
@@ -495,7 +500,8 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
         switch (r->type) {
         case OBJ_CAR:      o = Car_create(pos, rot.y); break;
         case OBJ_CLOTHES:  o = Object_createDecal((Mesh){ 0 }, pos); if (o) o->type = OBJ_CLOTHES; break;
-        case OBJ_DECAL: case OBJ_STRUCTURE: case OBJ_WALL: case OBJ_HUMAN: case OBJ_PLAYER: case OBJ_DOG: break; /* (made by the scene only) */
+        case OBJ_HUMAN:    if (cast > 0) o = Cast_create(cast, pos); break; /* (the crew turned up after the start; the rest: the scene's) */
+        case OBJ_DECAL: case OBJ_STRUCTURE: case OBJ_WALL: case OBJ_PLAYER: case OBJ_DOG: break; /* (made by the scene only) */
         default:           o = Object_create(r->type, r->palette, pos, size, r->make); break;
         }
         if (!o) continue;
diff --git a/entropie/src/main.c b/entropie/src/main.c
index 0838773..e5619cf 100644
--- a/entropie/src/main.c
+++ b/entropie/src/main.c
@@ -38,6 +38,8 @@
 #define DEV_LABEL     64  /* the dev menu (F1) */
 #define MENU_LABEL    104 /* the menus (start, Esc, load, save as): up to 16 slots */
 #define POCKET_LABEL  88  /* your pockets (B) */
+#define CREW_LABEL    120 /* the crew overview (F2) */
+#define CREW_LINES    20
 
 static const char *const KEYS[] = { /* proportional font: no column padding */
     "W A S D:  walk,   mouse:  look around",
@@ -67,6 +69,7 @@ static const char *const KEYS[] = { /* proportional font: no column padding */
     "V:  first / third person",
     "I:  show / hide keys",
     "F1:  dev menu (change anybody's stats)",
+    "F2:  the crew overview (Left / Right: who's who, now, the projects)",
     "Tab:  your stats (arrow keys: next page)",
     "K:  smoke;   B:  your pockets (Up / Down, Enter: take out or give)",
     "Esc:  menu (resume, save, load, keys, quit);  F5 quick save, F9 quick load;  M sound off / on",
@@ -117,6 +120,19 @@ static void drawDev(Ui *ui, const Game *game, int width, int height) {
     }
 }
 
+/* The crew overview (F2): a big sheet in the middle. */
+static void drawCrew(Ui *ui, const Game *game, int width, int height) {
+    char lines[CREW_LINES][128];
+    const char *title = "";
+    int n = Game_crewLines(game, lines, CREW_LINES - 1, &title);
+    const float row = 28.0f, pw = fminf(1240.0f, (float)width - 36.0f), ph = 60.0f + row * (float)n;
+    const float x = ((float)width - pw) * 0.5f, top = (float)height * 0.5f + ph * 0.5f;
+    Ui_drawPatch(ui, x, top - ph, pw, ph, 0.90f, 0.88f, 0.78f, 0.95f, 1.0f, width, height);
+    Ui_drawTextLeft(ui, CREW_LABEL, title, x + 22.0f, top - 38.0f, width, height);
+    for (int i = 0; i < n && CREW_LABEL + 1 + i < UI_LABELS; i++)
+        Ui_drawTextLeft(ui, CREW_LABEL + 1 + i, lines[i], x + 22.0f, top - 72.0f - row * (float)i, width, height);
+}
+
 /* Your pockets (B) on a sheet of paper on the right. */
 static void drawPockets(Ui *ui, Game *game, int width, int height) {
     char lines[POCKET_LINES][96];
@@ -442,6 +458,7 @@ int main(int argc, char **argv) {
         if (game.show_stats) drawStats(&ui, &game, width, height);
         else if (needs_text[0]) Ui_drawTextLine(&ui, NEEDS_LABEL, needs_text, 1, UI_TOP_RIGHT, width, height);
         if (game.show_dev) drawDev(&ui, &game, width, height);
+        if (game.show_crew) drawCrew(&ui, &game, width, height);
         if (game.show_pockets) drawPockets(&ui, &game, width, height);
         if (game.show_keys)
             for (int k = 0; k < KEY_COUNT; k++) Ui_drawTextLine(&ui, KEYS_LABEL + k, KEYS[k], k, UI_TOP_LEFT, width, height);
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index ab6b836..c13602f 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -2338,12 +2338,18 @@ static Vec3 standByFire(const Object *self, const Object *fire, Object *const *o
 /* Just arrived: over to the fire, sitting down on a bench there with Kurti and
    his sons (the others move up for them - the unwilling only when asked); no
    room anywhere: standing by the fire with them. Then the hello. */
+#define GREET_NEAR 6.0f /* m from the fire: there, saying hello */
+
 static bool greetHosts(Object *self, Object *const *objects, int count, float dt) {
     Ai *ai = &self->ai;
     if (!ai->greet) { ai->greet_stand = ai->greet_asked = false; return false; }
-    if (self->sitting) return false; /* (there: whatever they do sitting) */
     Object *fire = Camp_fireNear(objects, count, self->position, 120.0f, false);
     if (!fire) { ai->greet = false; return false; }
+    if (ai->greet_left > 0.0f && flatDistance(fire->position, self->position) < GREET_NEAR && (ai->greet_left -= dt) <= 0.0f) {
+        ai->greet = ai->greet_stand = false; /* (turned up later: a while at the fire, then on with the day) */
+        return false;
+    }
+    if (self->sitting) return false; /* (there: whatever they do sitting) */
     if (ai->greet_stand) { /* standing with the others, facing the fire */
         if (!goToPoint(self, ai->greet_at, 0.35f, objects, count, dt)) return true;
         stand(self);
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index b2cd12c..8220936 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -193,6 +193,7 @@ typedef struct {
     Vec3           dodge_to;    /* warned off (a tree coming down): out to here ... */
     float          dodge_t;     /* ... for this long */
     struct Object *umbrella;    /* rain: off to get this umbrella instead of going under a roof (ai.c shelter) */
+    float          greet_left;  /* just turned up: this long at the fire, then on with the day (0: till told otherwise) */
     struct Object *share;       /* rain: walking under this one's umbrella, at their side ... */
     float share_time;           /* ... this long so far */
     struct Object *blown;       /* the wind took this out of their hand: after it (ai.c fetchBlown) ... */
diff --git a/entropie/src/render/figure.c b/entropie/src/render/figure.c
index 9532c29..bd3787b 100644
--- a/entropie/src/render/figure.c
+++ b/entropie/src/render/figure.c
@@ -27,10 +27,12 @@ FigureLook Figure_wardrobe(int index) {
     FigureLook look = WARDROBE[((index % n) + n) % n];
     look.jacket = JACKETS[((index % 5) + 5) % 5];
     look.hoodie = index % 5 == 1 || index % 5 == 3 || index % 5 == 4; /* (the parka, the hoodie, the red rain jacket) */
-    switch (((index % 7) + 7) % 7) { /* hats: Toni a red cap, Kurti an old green one, Jonas a beanie (and so on for more people) */
-    case 3: look.hat = HAT_CAP;    look.hat_color = vec3(0.72f, 0.13f, 0.12f); break;
-    case 4: look.hat = HAT_CAP;    look.hat_color = vec3(0.26f, 0.32f, 0.22f); break;
-    case 6: look.hat = HAT_BEANIE; look.hat_color = vec3(0.20f, 0.28f, 0.46f); break;
+    switch (index) { /* hats (by who: world/cast.c): Toni a red cap, Kurti an old green one, Jonas a beanie, Kerry a grey one, Pascal a black cap */
+    case 3:  look.hat = HAT_CAP;    look.hat_color = vec3(0.72f, 0.13f, 0.12f); break;
+    case 4:  look.hat = HAT_CAP;    look.hat_color = vec3(0.26f, 0.32f, 0.22f); break;
+    case 6:  look.hat = HAT_BEANIE; look.hat_color = vec3(0.20f, 0.28f, 0.46f); break;
+    case 11: look.hat = HAT_BEANIE; look.hat_color = vec3(0.42f, 0.40f, 0.36f); break;
+    case 13: look.hat = HAT_CAP;    look.hat_color = vec3(0.12f, 0.12f, 0.13f); break;
     default: break;
     }
     return look;
diff --git a/entropie/src/render/ui.h b/entropie/src/render/ui.h
index 395f77b..a29550e 100644
--- a/entropie/src/render/ui.h
+++ b/entropie/src/render/ui.h
@@ -9,7 +9,7 @@
 #include "core/gl.h"
 
 #define UI_TEXT_MAX 96
-#define UI_LABELS   140
+#define UI_LABELS   141
 
 typedef enum {
     UI_TOP_LEFT,
diff --git a/entropie/src/world/cast.c b/entropie/src/world/cast.c
new file mode 100644
index 0000000..07d9387
--- /dev/null
+++ b/entropie/src/world/cast.c
@@ -0,0 +1,199 @@
+#include "world/cast.h"
+
+#include <stdio.h>
+
+/* Who they are: likes (firewood, trees, trash, fire), how willing, skills, a few words; tempo, thinking it over, breaks. */
+#define SKILLS(wood, trees, trash, fire) \
+    { [ORDER_FIREWOOD] = wood, [ORDER_TREES] = trees, [ORDER_TRASH] = trash, [ORDER_FIRE] = fire }
+#define LOOK(sr, sg, sb, hr, hg, hb, tr, tg, tb, pr, pg, pb, fr, fg, fb, tall, hairdo, sleeves) \
+    { .skin = { sr, sg, sb }, .hair = { hr, hg, hb }, .shirt = { tr, tg, tb }, .pants = { pr, pg, pb }, .shoes = { fr, fg, fb }, \
+      .height = tall, .hair_style = hairdo, .long_sleeves = sleeves }
+
+static const CastMember CAST[] = {
+    { .name = "You", .temperament = { .pride = 0.0f, .energy = 0.2f, .nerves = 0.1f },
+      .look = LOOK(0.93f, 0.74f, 0.60f, 0.30f, 0.19f, 0.12f, 0.86f, 0.42f, 0.32f, 0.24f, 0.30f, 0.48f, 0.22f, 0.16f, 0.13f, 1.78f, HAIR_SHORT, false),
+      .character = { { 0 }, 1.0f, SKILLS(0.3f, 0.3f, 0.3f, 0.3f), NULL, 1.0f, 0.0f, BREAK_NONE, 0.0f, 0.0f },
+      .kg = 75, .age = 28, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true, .filter_habit = true, .pocket_tray = true,
+      .smoker = 0.3f, .wits = 0.6f, .height_fear = 0.2f, .worn = 0.15f, .fitness = 0.5f, .tolerance = 1.0f,
+      .tobacco = POUCH_ROLLUPS, .papers = PAPERS_PACK, .filters = FILTERS_BAG,
+      .role = "the web things with Volkram; helps or takes over wherever needed", .leads = PROJECT_WEB, .helps = PROJECT_NONE, .commitment = 1.0f },
+    { .name = "Volkram", .temperament = { .pride = 0.5f, .energy = 0.1f, .nerves = 0.0f },
+      .look = LOOK(0.90f, 0.70f, 0.56f, 0.42f, 0.30f, 0.18f, 0.36f, 0.46f, 0.36f, 0.30f, 0.27f, 0.24f, 0.25f, 0.18f, 0.12f, 1.86f, HAIR_LONG, true),
+      .character = { SKILLS(0.8f, 0.3f, 0.0f, 0.6f), 0.7f, SKILLS(0.6f, 0.4f, 0.3f, 0.6f),
+                     "Volkram. Jana and I share a tent. Splitting wood clears my head.", 1.0f, 0.8f, BREAK_COFFEE, 90.0f, 30.0f },
+      .kg = 88, .age = 31, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true, .filter_habit = true,
+      .smoker = 0.6f, .wits = 0.7f, .height_fear = 0.15f, .worn = 0.2f, .fitness = 0.55f, .tolerance = 1.2f,
+      .tobacco = 40, .papers = 20, .filters = 60, .keys = "Car keys",
+      .role = "leads the festival organisation; the web things with you", .leads = PROJECT_ORGA, .helps = PROJECT_WEB, .commitment = 0.95f },
+    { .name = "Jana", .temperament = { .pride = 0.1f, .energy = 0.7f, .nerves = 0.3f },
+      .look = LOOK(0.96f, 0.80f, 0.68f, 0.70f, 0.36f, 0.20f, 0.92f, 0.78f, 0.36f, 0.42f, 0.30f, 0.46f, 0.85f, 0.82f, 0.74f, 1.66f, HAIR_LONG, false),
+      .character = { SKILLS(0.0f, -0.5f, 0.6f, 0.9f), 0.8f, SKILLS(0.3f, 0.2f, 0.5f, 0.6f),
+                     "Jana! I'm here for the fire and the people. And I like it tidy.", 1.1f, 0.5f, BREAK_CHAT, 70.0f, 30.0f },
+      .kg = 60, .age = 29, .hand = HAND_LEFT, .garment = GARMENT_SKIRT, .female = true, .urinella = true, .filter_habit = true, .pocket_tray = true,
+      .smoker = 0.2f, .wits = 0.65f, .height_fear = 0.5f, .worn = 0.1f, .fitness = 0.7f, .tolerance = 0.8f,
+      .tobacco = 10, .papers = 5,
+      .role = "one of the crew (her task not settled yet)", .leads = PROJECT_NONE, .helps = PROJECT_DECORATION, .commitment = 0.75f },
+    { .name = "Toni", .temperament = { .pride = -0.3f, .energy = 0.4f, .nerves = 0.6f },
+      .look = LOOK(0.72f, 0.52f, 0.38f, 0.12f, 0.10f, 0.10f, 0.30f, 0.62f, 0.60f, 0.55f, 0.48f, 0.36f, 0.32f, 0.22f, 0.16f, 1.74f, HAIR_SHORT, false),
+      .character = { SKILLS(0.4f, 0.6f, -0.4f, 0.3f), 0.5f, SKILLS(0.4f, 0.4f, 0.2f, 0.3f),
+                     "Toni. First time out here. Show me how and I'll do it.", 1.2f, 0.3f, BREAK_WALK, 80.0f, 25.0f },
+      .kg = 70, .age = 26, .hand = HAND_RIGHT, .garment = GARMENT_SHORTS, .lighter = true, .filter_habit = true,
+      .smoker = 0.7f, .wits = 0.5f, .height_fear = 0.1f, .worn = 0.35f, .fitness = 0.8f, .tolerance = 0.9f,
+      .tobacco = 25, .papers = 0, .filters = 15,
+      .role = "artist organisation with Rouven; builds along; mows the Kartoffelacker with the scythe", .leads = PROJECT_MOWING, .helps = PROJECT_ARTISTS,
+      .commitment = 0.8f },
+    { .name = "Kurti", .temperament = { .pride = 0.3f, .energy = -0.6f, .nerves = 0.0f },
+      .look = { .skin = { 0.90f, 0.72f, 0.62f }, .hair = { 0.82f, 0.82f, 0.80f }, .shirt = { 0.55f, 0.50f, 0.42f }, .pants = { 0.33f, 0.36f, 0.30f },
+                .shoes = { 0.20f, 0.16f, 0.12f }, .height = 1.70f, .hair_style = HAIR_SHORT, .long_sleeves = true, .belly = 1.0f, .keeps_shirt = true },
+      .character = { SKILLS(0.6f, 0.7f, 0.2f, 0.8f), 0.4f, SKILLS(0.8f, 0.9f, 0.4f, 0.8f),
+                     "Kurti. This is my house - you're welcome. I've felled more trees than you've seen.", 0.7f, 2.2f, BREAK_SIT, 45.0f, 50.0f },
+      .kg = 72, .age = 74, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true, .lives_here = true,
+      .smoker = 0.8f, .wits = 0.75f, .height_fear = 0.7f, .worn = 0.4f, .fitness = 0.12f, .tolerance = 1.4f,
+      .tobacco = 60, .papers = 30, .keys = "House keys",
+      .role = "the farm is his: the host", .leads = PROJECT_NONE, .helps = PROJECT_NONE, .commitment = 0.6f },
+    { .name = "Martin", .temperament = { .pride = 0.8f, .energy = 0.3f, .nerves = 0.1f },
+      .look = LOOK(0.88f, 0.66f, 0.50f, 0.30f, 0.22f, 0.16f, 0.20f, 0.24f, 0.40f, 0.24f, 0.26f, 0.30f, 0.18f, 0.14f, 0.12f, 1.84f, HAIR_SHORT, false),
+      .character = { SKILLS(0.4f, 0.9f, -0.6f, 0.3f), 0.5f, SKILLS(0.7f, 0.8f, 0.2f, 0.4f),
+                     "Martin. Kurti's eldest. Give me a chainsaw, not a broom.", 1.2f, 0.2f, BREAK_NONE, 0.0f, 0.0f },
+      .kg = 92, .age = 30, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true, .lives_here = true,
+      .smoker = 0.4f, .wits = 0.55f, .height_fear = 0.05f, .worn = 0.2f, .fitness = 0.6f, .tolerance = 1.3f, .cigarettes = 12,
+      .role = "Kurti's eldest: one of the hosts", .leads = PROJECT_NONE, .helps = PROJECT_NONE, .commitment = 0.5f },
+    { .name = "Jonas", .temperament = { .pride = -0.1f, .energy = 0.6f, .nerves = 0.2f },
+      .look = LOOK(0.90f, 0.70f, 0.55f, 0.36f, 0.26f, 0.18f, 0.80f, 0.20f, 0.18f, 0.50f, 0.46f, 0.38f, 0.60f, 0.24f, 0.20f, 1.80f, HAIR_SHORT, false),
+      .character = { SKILLS(0.6f, 0.4f, 0.3f, 0.5f), 0.85f, SKILLS(0.4f, 0.4f, 0.4f, 0.4f),
+                     "Jonas. The younger one. Dad says I talk too much.", 1.15f, 0.4f, BREAK_CHAT, 80.0f, 30.0f },
+      .kg = 76, .age = 25, .hand = HAND_LEFT, .garment = GARMENT_SHORTS, .lives_here = true,
+      .smoker = 0.0f, .wits = 0.45f, .height_fear = 0.2f, .worn = 0.1f, .fitness = 0.85f, .tolerance = 1.0f,
+      .role = "Kurti's younger son: one of the hosts", .leads = PROJECT_NONE, .helps = PROJECT_NONE, .commitment = 0.6f },
+    /* ----- the crew, turning up over the first days ----- */
+    { .name = "Kilian", .temperament = { .pride = 0.3f, .energy = 0.5f, .nerves = 0.2f },
+      .look = LOOK(0.92f, 0.73f, 0.60f, 0.55f, 0.40f, 0.22f, 0.48f, 0.24f, 0.58f, 0.18f, 0.18f, 0.20f, 0.30f, 0.26f, 0.22f, 1.83f, HAIR_LONG, false),
+      .character = { SKILLS(0.6f, 0.4f, 0.0f, 0.5f), 0.6f, SKILLS(0.6f, 0.5f, 0.3f, 0.5f),
+                     "Kilian. The Obstwiese is going to be a psy floor like you've never seen. Give me wood and a week.", 1.1f, 0.6f, BREAK_COFFEE, 100.0f, 25.0f },
+      .kg = 78, .age = 33, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true,
+      .smoker = 0.5f, .wits = 0.7f, .height_fear = 0.1f, .worn = 0.25f, .fitness = 0.65f, .tolerance = 1.0f,
+      .tobacco = 50, .papers = 30, .filters = 40,
+      .role = "builds the Obstwiese stage (psytrance)", .leads = PROJECT_OBSTWIESE, .helps = PROJECT_HOFBUEHNE, .commitment = 0.9f, .arrives = 2.0f },
+    { .name = "Julia", .temperament = { .pride = 0.2f, .energy = 0.8f, .nerves = 0.1f },
+      .look = LOOK(0.94f, 0.78f, 0.66f, 0.20f, 0.14f, 0.10f, 0.36f, 0.52f, 0.30f, 0.30f, 0.30f, 0.34f, 0.40f, 0.30f, 0.20f, 1.70f, HAIR_LONG, false),
+      .character = { SKILLS(0.6f, 0.4f, 0.4f, 0.4f), 0.95f, SKILLS(0.7f, 0.6f, 0.7f, 0.6f),
+                     "Julia. Tell me what needs doing - building's what I like best.", 1.35f, 0.2f, BREAK_NONE, 0.0f, 0.0f },
+      .kg = 62, .age = 27, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .female = true, .filter_habit = true,
+      .smoker = 0.0f, .wits = 0.8f, .height_fear = 0.05f, .worn = 0.1f, .fitness = 0.75f, .tolerance = 0.9f,
+      .role = "quick at every job, very good at building: wherever she's needed", .leads = PROJECT_NONE, .helps = PROJECT_HOFBUEHNE, .commitment = 0.9f,
+      .arrives = 2.0f },
+    { .name = "Stella", .temperament = { .pride = 0.1f, .energy = 0.5f, .nerves = 0.3f },
+      .look = LOOK(0.95f, 0.78f, 0.66f, 0.85f, 0.62f, 0.30f, 0.90f, 0.50f, 0.66f, 0.30f, 0.50f, 0.52f, 0.70f, 0.60f, 0.40f, 1.68f, HAIR_LONG, false),
+      .character = { SKILLS(-0.3f, -0.4f, 0.4f, 0.5f), 0.7f, SKILLS(0.2f, 0.1f, 0.5f, 0.4f),
+                     "Stella. Doro and I do the art - and the snails, they're protected, you know.", 1.0f, 0.7f, BREAK_CHAT, 70.0f, 35.0f },
+      .kg = 58, .age = 30, .hand = HAND_RIGHT, .garment = GARMENT_DRESS, .female = true, .urinella = true, .filter_habit = true, .pocket_tray = true,
+      .smoker = 0.3f, .wits = 0.7f, .height_fear = 0.4f, .worn = 0.1f, .fitness = 0.6f, .tolerance = 0.8f,
+      .tobacco = 20, .papers = 15, .filters = 30,
+      .role = "art and decoration with Doro; the snail rescue", .leads = PROJECT_DECORATION, .helps = PROJECT_SNAILS, .commitment = 0.85f, .arrives = 5.0f },
+    { .name = "Doro", .temperament = { .pride = 0.0f, .energy = 0.3f, .nerves = 0.2f },
+      .look = LOOK(0.88f, 0.66f, 0.52f, 0.25f, 0.16f, 0.12f, 0.30f, 0.30f, 0.56f, 0.58f, 0.40f, 0.30f, 0.28f, 0.22f, 0.18f, 1.72f, HAIR_SHORT, true),
+      .character = { SKILLS(-0.2f, -0.3f, 0.5f, 0.4f), 0.75f, SKILLS(0.2f, 0.1f, 0.5f, 0.4f),
+                     "Doro. Paint, fabric, lights in the trees - and nobody steps on a snail.", 0.95f, 0.8f, BREAK_SIT, 80.0f, 35.0f },
+      .kg = 64, .age = 32, .hand = HAND_LEFT, .garment = GARMENT_TROUSERS, .female = true, .lighter = true,
+      .smoker = 0.0f, .wits = 0.75f, .height_fear = 0.3f, .worn = 0.15f, .fitness = 0.55f, .tolerance = 0.9f,
+      .role = "leads the snail rescue; art and decoration with Stella", .leads = PROJECT_SNAILS, .helps = PROJECT_DECORATION, .commitment = 0.85f,
+      .arrives = 5.0f },
+    { .name = "Kerry", .temperament = { .pride = 0.4f, .energy = 0.0f, .nerves = 0.0f },
+      .look = LOOK(0.86f, 0.64f, 0.48f, 0.50f, 0.42f, 0.36f, 0.34f, 0.30f, 0.26f, 0.26f, 0.30f, 0.22f, 0.26f, 0.20f, 0.14f, 1.79f, HAIR_LONG, true),
+      .character = { SKILLS(0.6f, 0.9f, -0.5f, 0.6f), 0.5f, SKILLS(0.8f, 0.9f, 0.2f, 0.6f),
+                     "Kerry. Give me a log and a chainsaw and I'll find what's in it.", 0.9f, 1.2f, BREAK_COFFEE, 120.0f, 40.0f },
+      .kg = 84, .age = 41, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true,
+      .smoker = 0.6f, .wits = 0.7f, .height_fear = 0.1f, .worn = 0.2f, .fitness = 0.6f, .tolerance = 1.3f, .cigarettes = 8,
+      .role = "wood art with the chainsaw (needs logs and the chainsaw)", .leads = PROJECT_WOOD_ART, .helps = PROJECT_DECORATION, .commitment = 0.7f,
+      .arrives = 20.0f },
+    { .name = "Rouven", .temperament = { .pride = 0.3f, .energy = 0.4f, .nerves = 0.3f },
+      .look = LOOK(0.93f, 0.75f, 0.62f, 0.24f, 0.18f, 0.12f, 0.20f, 0.20f, 0.22f, 0.20f, 0.22f, 0.28f, 0.82f, 0.82f, 0.80f, 1.81f, HAIR_SHORT, false),
+      .character = { SKILLS(0.0f, -0.3f, 0.2f, 0.6f), 0.6f, SKILLS(0.3f, 0.2f, 0.4f, 0.5f),
+                     "Rouven. Toni and I do the artists - who plays when, and that they're happy.", 1.0f, 0.6f, BREAK_CHAT, 70.0f, 30.0f },
+      .kg = 74, .age = 29, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true, .filter_habit = true,
+      .smoker = 0.4f, .wits = 0.75f, .height_fear = 0.3f, .worn = 0.15f, .fitness = 0.5f, .tolerance = 1.0f,
+      .tobacco = 30, .papers = 20, .filters = 50,
+      .role = "artist organisation with Toni", .leads = PROJECT_ARTISTS, .helps = PROJECT_HOFBUEHNE, .commitment = 0.8f, .arrives = 26.0f },
+    { .name = "Pascal", .temperament = { .pride = 0.2f, .energy = 0.2f, .nerves = 0.1f },
+      .look = LOOK(0.90f, 0.70f, 0.56f, 0.18f, 0.14f, 0.12f, 0.22f, 0.22f, 0.24f, 0.26f, 0.28f, 0.34f, 0.20f, 0.18f, 0.16f, 1.77f, HAIR_SHORT, true),
+      .character = { SKILLS(0.3f, 0.2f, 0.0f, 0.3f), 0.7f, SKILLS(0.5f, 0.3f, 0.3f, 0.4f),
+                     "Pascal. Light is what I do - and the main stage gets a proper floor.", 1.05f, 0.9f, BREAK_COFFEE, 110.0f, 30.0f },
+      .kg = 80, .age = 35, .hand = HAND_RIGHT, .garment = GARMENT_TROUSERS, .lighter = true,
+      .smoker = 0.2f, .wits = 0.8f, .height_fear = 0.05f, .worn = 0.15f, .fitness = 0.55f, .tolerance = 1.1f, .cigarettes = 6,
+      .role = "the light, and a proper floor for the main stage", .leads = PROJECT_LIGHT, .helps = PROJECT_HOFBUEHNE, .commitment = 0.85f,
+      .arrives = 26.0f },
+};
+#define CAST_COUNT ((int)(sizeof(CAST) / sizeof(CAST[0])))
+
+int Cast_count(void) { return CAST_COUNT; }
+
+const CastMember *Cast_get(int id) { return id >= 0 && id < CAST_COUNT ? &CAST[id] : NULL; }
+
+Object *Cast_create(int id, Vec3 at) {
+    const CastMember *c = Cast_get(id);
+    if (!c) return NULL;
+    Object *obj = Object_create(id == 0 ? OBJ_PLAYER : OBJ_HUMAN, PALETTE_MODERN, at, vec3(0.5f, 1.75f, 0.25f), c->kg);
+    if (!obj) return NULL;
+    obj->cast_id     = id;
+    obj->temperament = c->temperament;
+    obj->tolerance   = c->tolerance;
+    obj->fitness     = c->fitness;
+    FigureLook worn = Figure_wardrobe(id); /* (the jacket, its hood and a hat from the wardrobe; the rest their own) */
+    obj->look = c->look;
+    obj->look.jacket = worn.jacket;
+    obj->look.hoodie = worn.hoodie;
+    obj->look.hat = worn.hat;
+    obj->look.hat_color = worn.hat_color;
+    obj->look.garment = c->garment;
+    Object_setPerson(obj, c->name, c->hand);
+    obj->ai.character = c->character;
+    obj->female       = c->female;
+    obj->urinella     = c->urinella;
+    obj->urinellas    = c->urinella ? POCKET_URINELLAS - 1 : 0;
+    obj->urinella_tried = c->urinella ? 1 : 0; /* (who has them knows she likes them) */
+    obj->lighter      = c->lighter;
+    obj->lighter_uses = c->lighter ? LIGHTER_USES * (1 + id % 3) / 3 : 0; /* (some nearly empty) */
+    obj->smoker       = c->smoker;
+    obj->wits         = c->wits;
+    obj->height_fear  = c->height_fear;
+    obj->worn         = c->worn;
+    obj->cigarettes   = c->cigarettes;
+    obj->tobacco      = c->tobacco;
+    obj->papers       = c->papers;
+    obj->filters      = c->filters;
+    obj->filter_habit = c->filter_habit;
+    obj->pocket_tray  = c->pocket_tray;
+    obj->lives_here   = c->lives_here;
+    obj->keys         = c->keys;
+    obj->work_trousers = id == 0; /* (you: work trousers, plenty of pockets) */
+    obj->craving      = 0.3f + 0.15f * (float)(id % 3);
+    obj->commitment   = c->commitment;
+    obj->leads        = (int)c->leads;
+    obj->helps        = (int)c->helps;
+    for (int k = 0; k < ORDER_COUNT; k++) obj->skill[k] = c->character.skill[k];
+    obj->tempo = c->character.tempo;
+    Object_setHeight(obj, obj->look.height); /* (the body box their own size) */
+    float years = clampf(1.0f - (float)(c->age - 45) / 60.0f, 0.5f, 1.0f); /* (old legs) */
+    obj->top_speed  *= years;
+    obj->jump_speed *= years;
+    obj->pace = years * (1.0f + 0.12f * obj->temperament.energy); /* (Kurti: old and calm - he takes his time) */
+    return obj;
+}
+
+const char *Cast_projectName(Project p) {
+    static const char *const NAMES[PROJECT_COUNT] = {
+        [PROJECT_NONE] = "-", [PROJECT_ORGA] = "organisation", [PROJECT_WEB] = "web and tickets", [PROJECT_ARTISTS] = "artists",
+        [PROJECT_DECORATION] = "decoration", [PROJECT_LIGHT] = "light", [PROJECT_HOFBUEHNE] = "Hofbuehne (bands)",
+        [PROJECT_OBSTWIESE] = "Obstwiese (psytrance)", [PROJECT_KARTOFFELACKER] = "Kartoffelacker (world music)",
+        [PROJECT_HIDDEN] = "hidden floor (techno)", [PROJECT_MOWING] = "mowing (scythe)", [PROJECT_SNAILS] = "snail rescue",
+        [PROJECT_WOOD_ART] = "wood art (chainsaw)",
+    };
+    return p >= 0 && p < PROJECT_COUNT && NAMES[p] ? NAMES[p] : "?";
+}
+
+Object *Cast_leadOf(Object *const *objects, int count, Project p) {
+    if (p == PROJECT_NONE) return NULL;
+    for (int i = 0; i < count; i++)
+        if (objects[i]->is_character && objects[i]->cast_id >= 0 && objects[i]->leads == (int)p) return objects[i];
+    return NULL;
+}
diff --git a/entropie/src/world/cast.h b/entropie/src/world/cast.h
new file mode 100644
index 0000000..be1add8
--- /dev/null
+++ b/entropie/src/world/cast.h
@@ -0,0 +1,60 @@
+#ifndef WORLD_CAST_H
+#define WORLD_CAST_H
+
+/*
+ * Everybody in the story, one entry each: how they look and are, what they're
+ * good at, their part in the festival (the projects they lead or help with)
+ * and when they turn up on the ground. You are entry 0. Kurti and his sons
+ * live here; the crew comes over the first days (`arrives`).
+ */
+
+#include "world/object.h"
+
+/* The festival's projects: the floors and the jobs round them. */
+typedef enum {
+    PROJECT_NONE,
+    PROJECT_ORGA,           /* the festival organisation */
+    PROJECT_WEB,            /* the website, tickets */
+    PROJECT_ARTISTS,        /* booking and looking after the artists */
+    PROJECT_DECORATION,     /* art and decoration */
+    PROJECT_LIGHT,          /* the light (and the main stage's floor) */
+    PROJECT_HOFBUEHNE,      /* the yard stage: bands, poetry slam, breakdance, rock, punk */
+    PROJECT_OBSTWIESE,      /* the orchard: psytrance */
+    PROJECT_KARTOFFELACKER, /* the potato field: world music, small talks (the geo-dome) */
+    PROJECT_HIDDEN,         /* the hidden floor behind the main building: techno */
+    PROJECT_MOWING,         /* the Kartoffelacker's grass, with the scythe */
+    PROJECT_SNAILS,         /* the vineyard snails picked up before the regulatory office comes */
+    PROJECT_WOOD_ART,       /* art cut from logs with the chainsaw */
+    PROJECT_COUNT
+} Project;
+
+typedef struct {
+    const char *name;
+    Mood        temperament;
+    FigureLook  look;           /* (the jacket, its hood and a hat: from Figure_wardrobe) */
+    Character   character;
+    float       kg;
+    int         age;            /* past 45 slower on their feet */
+    int         hand;           /* HAND_LEFT / HAND_RIGHT */
+    int         garment;        /* GARMENT_* */
+    bool        female, urinella, lighter, filter_habit, pocket_tray, lives_here;
+    float       smoker, wits, height_fear, worn, fitness, tolerance;
+    int         cigarettes, tobacco, papers, filters;
+    const char *keys;           /* a key in the pocket, or NULL */
+    /* their part in the festival */
+    const char *role;           /* said plainly */
+    Project     leads, helps;   /* the project they lead, one they help with */
+    float       commitment;     /* 0..1 how much they're into it, to begin with */
+    float       arrives;        /* hours after the start on the ground they turn up (0: there from the start) */
+} CastMember;
+
+int               Cast_count(void);
+const CastMember *Cast_get(int id);
+/* A person made from their entry, standing at `at` (not in the world yet). */
+Object           *Cast_create(int id, Vec3 at);
+/* A project said plainly ("Obstwiese (psytrance)"). */
+const char       *Cast_projectName(Project p);
+/* Who leads it now (the people in the world), or NULL. */
+Object           *Cast_leadOf(Object *const *objects, int count, Project p);
+
+#endif /* WORLD_CAST_H */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index ea64f3d..8837dbb 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -99,6 +99,7 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
     obj->palette  = palette;
     obj->make_arg = weight;
     obj->hands.drink_hand = -1;
+    obj->cast_id = -1;
     obj->work_state.since = 1e3f; /* hasn't worked yet */
     obj->main_hand = HAND_RIGHT;
     obj->top_speed = SPRINT_SPEED;
@@ -506,6 +507,7 @@ Object *Object_createDecal(Mesh mesh, Vec3 position) {
     obj->mesh      = mesh;
     obj->is_static = true;
     obj->hands.drink_hand = -1;
+    obj->cast_id   = -1;
     return obj;
 }
 
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 0f63d48..f2a91b6 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -304,6 +304,9 @@ typedef struct Object {
     bool       over_up;            /* ... up onto it rather than over it */
     Vec3       over_foot0[2], over_land[2];
     int        sit_style;     /* characters: SIT_GROUND (cross-legged) or SIT_SEAT */
+    int        cast_id;       /* people: who in the story (world/cast.c), -1 nobody of it */
+    float      commitment;    /* people: 0..1 how much they're into their part of the festival */
+    int        leads, helps;  /* people: the Project they lead, one they help with (world/cast.h; PROJECT_NONE 0) */
     struct {                  /* dogs */
         int   mode;
         float timer, phase, wag, lie, bark;
diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c
index ac46e75..1ae6f5d 100644
--- a/entropie/src/world/scene.c
+++ b/entropie/src/world/scene.c
@@ -1,5 +1,6 @@
 #include "world/scene.h"
 #include "world/site.h"
+#include "world/cast.h"
 
 #include <SDL3/SDL.h>
 
@@ -54,90 +55,8 @@ static const Spawn TESTING[] = {
     { OBJ_BOX,      PALETTE_MODERN, {  1, 0, 2 }, { 2, 1, 2 },   20, NO_ROTATION },
 };
 
-/* Who's here, in spawn order: you; Volkram, Jana and Toni (your friends, here
-   with you); Kurti (an old man, the house is his) and his sons Martin (about
-   30) and Jonas (about 25). */
-static const Mood TEMPERAMENTS[] = {
-    { .pride =  0.0f, .energy =  0.2f, .nerves = 0.1f },  /* you: relaxed */
-    { .pride =  0.5f, .energy =  0.1f, .nerves = 0.0f },  /* Volkram: calm, sure of himself */
-    { .pride =  0.1f, .energy =  0.7f, .nerves = 0.3f },  /* Jana: lively */
-    { .pride = -0.3f, .energy =  0.4f, .nerves = 0.6f },  /* Toni: restless, a bit unsure */
-    { .pride =  0.3f, .energy = -0.6f, .nerves = 0.0f },  /* Kurti: old and slow, nothing rattles him */
-    { .pride =  0.8f, .energy =  0.3f, .nerves = 0.1f },  /* Martin: proud, chest out */
-    { .pride = -0.1f, .energy =  0.6f, .nerves = 0.2f },  /* Jonas: keen */
-};
-/* Names and main hands, in spawn order. */
-static const char *const NAMES[] = { "You", "Volkram", "Jana", "Toni", "Kurti", "Martin", "Jonas" };
-static const int MAIN_HAND[] = { HAND_RIGHT, HAND_RIGHT, HAND_LEFT, HAND_RIGHT, HAND_RIGHT, HAND_RIGHT, HAND_LEFT };
-/* Jana is a woman; she brought a Urinella (she pees standing). */
-static const bool FEMALE[]   = { false, false, true, false, false, false, false };
-static const bool URINELLA[] = { false, false, true, false, false, false, false };
-static const bool LIGHTER[]  = { true, true, false, true, true, true, false };
-/* Smoking: how much (0 = not at all), ready ones, tobacco to roll (most roll their own). */
-static const float SMOKER[]     = { 0.3f, 0.6f, 0.2f, 0.7f, 0.8f, 0.4f, 0.0f };
-static const float WITS[]       = { 0.6f, 0.7f, 0.65f, 0.5f, 0.75f, 0.55f, 0.45f };
-static const float HEIGHT_FEAR[] = { 0.2f, 0.15f, 0.5f, 0.1f, 0.7f, 0.05f, 0.2f }; /* (Kurti stays off ladders these days) */
-static const float YESTERDAY[]   = { 0.15f, 0.2f, 0.1f, 0.35f, 0.4f, 0.2f, 0.1f }; /* worn from the days before (you came a long way) */
-static const int   CIGARETTES[] = { 0, 0, 0, 0, 0, 12, 0 };      /* (Martin smokes a pack) */
-static const int   TOBACCO[]    = { POUCH_ROLLUPS, 40, 10, 25, 60, 0, 0 };
-static const int   PAPERS[]     = { PAPERS_PACK, 20, 5, 0, 30, 0, 0 };   /* (Toni forgot his) */
-static const int   FILTERS[]    = { FILTERS_BAG, 60, 0, 15, 0, 0, 0 };   /* (Jana is out) */
-static const bool  FILTER_HABIT[] = { true, true, true, true, false, false, false }; /* (Kurti: none, ever) */
-static const bool  POCKET_TRAY[]  = { true, false, true, false, false, false, false }; /* a pocket ashtray: you and Jana */
-static const char *const KEYS_IN_POCKET[] = { NULL, "Car keys", NULL, NULL, "House keys", NULL, NULL }; /* (Volkram's car) */
-static const int  GARMENT[]  = { GARMENT_TROUSERS, GARMENT_TROUSERS, GARMENT_SKIRT, GARMENT_SHORTS, GARMENT_TROUSERS, GARMENT_TROUSERS, GARMENT_SHORTS };
-/* Years: past 45 slower on their feet (Kurti is 74). */
-static const int AGE[] = { 28, 31, 29, 26, 74, 30, 25 };
-/* How sporty each is (0..1): not a matter of build. */
-static const float FITNESS[] = { 0.5f, 0.55f, 0.7f, 0.8f, 0.12f, 0.6f, 0.85f };
-/* How well each holds their drink (on top of body weight). */
-static const float TOLERANCE[] = { 1.0f, 1.2f, 0.8f, 0.9f, 1.4f, 1.3f, 1.0f };
-#define TEMPERAMENT_COUNT ((int)(sizeof(TEMPERAMENTS) / sizeof(TEMPERAMENTS[0])))
-
-/* How they look (skin, hair, shirt, trousers, shoes, height, hair style, long sleeves). */
-static const FigureLook LOOKS[] = {
-    { .skin = { 0.93f, 0.74f, 0.60f }, .hair = { 0.30f, 0.19f, 0.12f }, .shirt = { 0.86f, 0.42f, 0.32f },
-      .pants = { 0.24f, 0.30f, 0.48f }, .shoes = { 0.22f, 0.16f, 0.13f }, .height = 1.78f, .hair_style = HAIR_SHORT, .long_sleeves = false }, /* you */
-    { .skin = { 0.90f, 0.70f, 0.56f }, .hair = { 0.42f, 0.30f, 0.18f }, .shirt = { 0.36f, 0.46f, 0.36f },
-      .pants = { 0.30f, 0.27f, 0.24f }, .shoes = { 0.25f, 0.18f, 0.12f }, .height = 1.86f, .hair_style = HAIR_LONG, .long_sleeves = true }, /* Volkram: tall, long hair */
-    { .skin = { 0.96f, 0.80f, 0.68f }, .hair = { 0.70f, 0.36f, 0.20f }, .shirt = { 0.92f, 0.78f, 0.36f },
-      .pants = { 0.42f, 0.30f, 0.46f }, .shoes = { 0.85f, 0.82f, 0.74f }, .height = 1.66f, .hair_style = HAIR_LONG, .long_sleeves = false }, /* Jana */
-    { .skin = { 0.72f, 0.52f, 0.38f }, .hair = { 0.12f, 0.10f, 0.10f }, .shirt = { 0.30f, 0.62f, 0.60f },
-      .pants = { 0.55f, 0.48f, 0.36f }, .shoes = { 0.32f, 0.22f, 0.16f }, .height = 1.74f, .hair_style = HAIR_SHORT, .long_sleeves = false }, /* Toni */
-    { .skin = { 0.90f, 0.72f, 0.62f }, .hair = { 0.82f, 0.82f, 0.80f }, .shirt = { 0.55f, 0.50f, 0.42f },
-      .pants = { 0.33f, 0.36f, 0.30f }, .shoes = { 0.20f, 0.16f, 0.12f }, .height = 1.70f, .hair_style = HAIR_SHORT, .long_sleeves = true,
-      .belly = 1.0f, .keeps_shirt = true }, /* Kurti: grey, a worn shirt (always on), a belly */
-    { .skin = { 0.88f, 0.66f, 0.50f }, .hair = { 0.30f, 0.22f, 0.16f }, .shirt = { 0.20f, 0.24f, 0.40f },
-      .pants = { 0.24f, 0.26f, 0.30f }, .shoes = { 0.18f, 0.14f, 0.12f }, .height = 1.84f, .hair_style = HAIR_SHORT, .long_sleeves = false }, /* Martin: broad, work clothes */
-    { .skin = { 0.90f, 0.70f, 0.55f }, .hair = { 0.36f, 0.26f, 0.18f }, .shirt = { 0.80f, 0.20f, 0.18f },
-      .pants = { 0.50f, 0.46f, 0.38f }, .shoes = { 0.60f, 0.24f, 0.20f }, .height = 1.80f, .hair_style = HAIR_SHORT, .long_sleeves = false }, /* Jonas */
-};
-
-/* Who they are: likes (firewood, trees, trash, fire), how willing, skills, a few words;
-   tempo, thinking it over, breaks. */
-#define SKILLS(wood, trees, trash, fire) \
-    { [ORDER_FIREWOOD] = wood, [ORDER_TREES] = trees, [ORDER_TRASH] = trash, [ORDER_FIRE] = fire }
-static const Character CHARACTERS[] = {
-    { { 0 }, 1.0f, SKILLS(0.3f, 0.3f, 0.3f, 0.3f), NULL, 1.0f, 0.0f, BREAK_NONE, 0.0f, 0.0f }, /* you */
-    { SKILLS(0.8f, 0.3f, 0.0f, 0.6f), 0.7f, SKILLS(0.6f, 0.4f, 0.3f, 0.6f),
-      "Volkram. Jana and I share a tent. Splitting wood clears my head.",
-      1.0f, 0.8f, BREAK_COFFEE, 90.0f, 30.0f },   /* steady; a coffee now and then */
-    { SKILLS(0.0f, -0.5f, 0.6f, 0.9f), 0.8f, SKILLS(0.3f, 0.2f, 0.5f, 0.6f),
-      "Jana! I'm here for the fire and the people. And I like it tidy.",
-      1.1f, 0.5f, BREAK_CHAT, 70.0f, 30.0f },     /* lively: a chat in between */
-    { SKILLS(0.4f, 0.6f, -0.4f, 0.3f), 0.5f, SKILLS(0.4f, 0.4f, 0.2f, 0.3f),
-      "Toni. First time out here. Show me how and I'll do it.",
-      1.2f, 0.3f, BREAK_WALK, 80.0f, 25.0f },     /* quick, a bit careless; a little walk */
-    { SKILLS(0.6f, 0.7f, 0.2f, 0.8f), 0.4f, SKILLS(0.8f, 0.9f, 0.4f, 0.8f),
-      "Kurti. This is my house - you're welcome. I've felled more trees than you've seen.",
-      0.7f, 2.2f, BREAK_SIT, 45.0f, 50.0f },      /* old: slow, thinks it over, sits down a lot */
-    { SKILLS(0.4f, 0.9f, -0.6f, 0.3f), 0.5f, SKILLS(0.7f, 0.8f, 0.2f, 0.4f),
-      "Martin. Kurti's eldest. Give me a chainsaw, not a broom.",
-      1.2f, 0.2f, BREAK_NONE, 0.0f, 0.0f },       /* gets right to it, no breaks */
-    { SKILLS(0.6f, 0.4f, 0.3f, 0.5f), 0.85f, SKILLS(0.4f, 0.4f, 0.4f, 0.4f),
-      "Jonas. The younger one. Dad says I talk too much.",
-      1.15f, 0.4f, BREAK_CHAT, 80.0f, 30.0f },    /* keen, helpful, a chat in between */
-};
+/* Who they are, how they look and their part in it: world/cast.c (the people
+   above, in that order: you, Volkram, Jana, Toni, Kurti, Martin, Jonas). */
 
 /* Where the things to try out stand: beside the obstacle course, left of its lanes. */
 #define TEST_ACROSS -9.5f /* m across from the middle lane (the lanes are 3.5 apart) */
@@ -228,62 +147,15 @@ Object *Scene_load(World *world) {
 
     for (size_t i = 0; i < SDL_arraysize(SCENE); i++) {
         const Spawn *s = &SCENE[i];
-        Object *obj = Object_create(s->type, s->palette, vec3_add(s->position, yard), s->size, s->weight);
+        bool person = s->type == OBJ_PLAYER || s->type == OBJ_HUMAN;
+        Object *obj = person ? Cast_create(characters++, vec3_add(s->position, yard))
+                             : Object_create(s->type, s->palette, vec3_add(s->position, yard), s->size, s->weight);
         if (!obj) return NULL;
 
         Vec3 r = s->rotation_deg;
         if (r.x != 0.0f || r.y != 0.0f || r.z != 0.0f)
             Object_setRotation(obj, vec3(deg2rad(r.x), deg2rad(r.y), deg2rad(r.z)));
 
-        if (obj->is_character) {
-            obj->temperament = TEMPERAMENTS[characters % TEMPERAMENT_COUNT];
-            obj->tolerance   = TOLERANCE[characters % TEMPERAMENT_COUNT];
-            obj->fitness     = FITNESS[characters % TEMPERAMENT_COUNT];
-            obj->look = Figure_wardrobe(characters);
-            if (characters < (int)(sizeof(LOOKS) / sizeof(LOOKS[0]))) { /* (their own clothes; extra people: the wardrobe) */
-                Vec3 jacket = obj->look.jacket;
-                bool hoodie = obj->look.hoodie;
-                int hat = obj->look.hat;
-                Vec3 hat_color = obj->look.hat_color;
-                obj->look = LOOKS[characters];
-                obj->look.jacket = jacket;
-                obj->look.hoodie = hoodie;
-                obj->look.hat = hat;
-                obj->look.hat_color = hat_color;
-            }
-            obj->look.garment = GARMENT[characters % TEMPERAMENT_COUNT];
-            Object_setPerson(obj, NAMES[characters % TEMPERAMENT_COUNT], MAIN_HAND[characters % TEMPERAMENT_COUNT]);
-            obj->ai.character = CHARACTERS[characters % TEMPERAMENT_COUNT];
-            obj->female   = FEMALE[characters % TEMPERAMENT_COUNT];
-            obj->urinella = URINELLA[characters % TEMPERAMENT_COUNT];
-            obj->urinellas = obj->urinella ? POCKET_URINELLAS - 1 : 0;
-            obj->urinella_tried = obj->urinella ? 1 : 0; /* (Jana knows she likes them) */
-            obj->lighter = LIGHTER[characters % TEMPERAMENT_COUNT];
-            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->cigarettes = CIGARETTES[characters % TEMPERAMENT_COUNT];
-            obj->tobacco    = TOBACCO[characters % TEMPERAMENT_COUNT];
-            obj->papers     = PAPERS[characters % TEMPERAMENT_COUNT];
-            obj->filters    = FILTERS[characters % TEMPERAMENT_COUNT];
-            obj->filter_habit = FILTER_HABIT[characters % TEMPERAMENT_COUNT];
-            obj->pocket_tray  = POCKET_TRAY[characters % TEMPERAMENT_COUNT];
-            obj->lives_here   = characters >= 4 && characters <= 6; /* (Kurti and his sons) */
-            obj->keys       = KEYS_IN_POCKET[characters % TEMPERAMENT_COUNT];
-            obj->work_trousers = characters == 0; /* (you: work trousers, plenty of pockets) */
-            obj->craving    = 0.3f + 0.15f * (float)(characters % 3);
-            for (int k = 0; k < ORDER_COUNT; k++) obj->skill[k] = obj->ai.character.skill[k];
-            obj->tempo    = obj->ai.character.tempo;
-            Object_setHeight(obj, obj->look.height); /* (the body box their own size: 1.66 .. 1.86 m) */
-            float years = clampf(1.0f - (float)(AGE[characters % TEMPERAMENT_COUNT] - 45) / 60.0f, 0.5f, 1.0f); /* (old legs) */
-            obj->top_speed  *= years;
-            obj->jump_speed *= years;
-            obj->pace = years * (1.0f + 0.12f * obj->temperament.energy); /* (Kurti: old and calm - he takes his time) */
-            characters++;
-        }
-
         if (!World_add(world, obj)) return NULL;
         if (s->type == OBJ_PLAYER && !player) player = obj;
     }