Nimbin[12]?Games / Entropie / commits / 31b672e

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.41.0: materials and money, the start shows the way, pavilions, GPU rests

Christian Immanuel · 2026-10-27 14:56 · 31b672ec4c4cb86b263a279ed901b141176decdb

- Cash box (1200 EUR, saved, on the F2 projects page)
- Building material: bundles of boards, nails, screws, paint (OBJ_MATERIAL).
  Stage steps need it lying by them: deck and roof boards plus nails or
  screws, decoration paint; used up when the step is done
- Shopping run: ask whoever has the car keys (Volkram), pick the list; he
  drives off and comes back an hour later, the crew unloads (material to a
  store in front of the car room, food into the kitchen)
- Food runs out now (food crate 40 portions, cool box 24 sausages)
- Crew fetches material from the store to a stage that waits for it
- Arrival: a golden marker over Kurti, the car, Kurti again; Kurti walks
  you to your camping spot, then the marker shows your tent spot
- Yard: the roof on posts replaced by two party pavilions with beer table sets
- Kartoffelacker moved behind the road to the chicken farm, a strip of wood
  right of it
- GPU: vsync plus a 60 fps frame cap (it ran flat out before)
 commit_message.txt                 |  29 +++---
 entropie/ARCHITECTURE.md           |   2 +-
 entropie/Makefile                  |   2 +-
 entropie/README.md                 |   8 +-
 entropie/STATE.md                  |  66 +++++++++---
 entropie/commit_message.txt        |  29 +++---
 entropie/src/core/platform.c       |   9 +-
 entropie/src/core/platform.h       |   5 +-
 entropie/src/game/game.c           |  11 ++
 entropie/src/game/game.h           |  18 ++++
 entropie/src/game/game_actions.c   |   8 +-
 entropie/src/game/game_arrival.c   | 120 ++++++++++++++++++++--
 entropie/src/game/game_internal.h  |   8 ++
 entropie/src/game/game_shop.c      | 202 +++++++++++++++++++++++++++++++++++++
 entropie/src/game/game_story.c     |  12 ++-
 entropie/src/game/game_talk.c      |   4 +
 entropie/src/game/save.c           |  17 +++-
 entropie/src/people/ai.c           |  17 +++-
 entropie/src/people/ai.h           |   9 +-
 entropie/src/people/ai_internal.h  |   1 +
 entropie/src/people/ai_project.c   | 130 ++++++++++++++++++++++--
 entropie/src/people/talk.c         |  81 ++++++++++++++-
 entropie/src/people/talk.h         |   8 ++
 entropie/src/render/shapes.h       |   7 ++
 entropie/src/render/shapes_props.c |  79 +++++++++++++++
 entropie/src/world/car.c           |  35 ++++++-
 entropie/src/world/car.h           |  17 +++-
 entropie/src/world/manor.c         |  51 +++++-----
 entropie/src/world/manor.h         |   2 +-
 entropie/src/world/object.c        |  26 ++++-
 entropie/src/world/object.h        |   5 +
 entropie/src/world/site.c          |  14 ++-
 entropie/src/world/site.h          |   2 +
 entropie/src/world/stage.c         |  62 +++++++++---
 entropie/src/world/stage.h         |  17 ++--
 entropie/src/world/world.c         |   1 +
 entropie/src/world/world.h         |   4 +
 37 files changed, 987 insertions(+), 131 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index 9d9cf74..fbd34bf 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,14 +1,17 @@
-entropie 2.40.0: the floors get built
+entropie 2.41.0: materials and money, the start shows the way, pavilions, GPU rests
 
-- Four stages marked out: Hofbuehne (yard), Obstwiese (orchard),
-  Kartoffelacker (the field behind the tents), the hidden floor (behind
-  the main building)
-- Built step by step: ground cleared, frame (needs three logs lying by
-  it), deck, back wall and roof, paint and banners in the floor's colour
-- Leads and helpers go and build in working hours; you build too (hold
-  left click at a stage)
-- Commitment: lifted by progress and praise, worn down by being held up,
-  working worn out, nobody leading, annoyance; too low, leads quit
-- Talk about the festival (X, then 4): praise, ask someone to lead an open
-  floor, take over a lead, get help with yours
-- Late arrivals say hello standing by the fire (no more walking into it)
+- Cash box (1200 EUR, saved, on the F2 projects page)
+- Building material: bundles of boards, nails, screws, paint (OBJ_MATERIAL).
+  Stage steps need it lying by them: deck and roof boards plus nails or
+  screws, decoration paint; used up when the step is done
+- Shopping run: ask whoever has the car keys (Volkram), pick the list; he
+  drives off and comes back an hour later, the crew unloads (material to a
+  store in front of the car room, food into the kitchen)
+- Food runs out now (food crate 40 portions, cool box 24 sausages)
+- Crew fetches material from the store to a stage that waits for it
+- Arrival: a golden marker over Kurti, the car, Kurti again; Kurti walks
+  you to your camping spot, then the marker shows your tent spot
+- Yard: the roof on posts replaced by two party pavilions with beer table sets
+- Kartoffelacker moved behind the road to the chicken farm, a strip of wood
+  right of it
+- GPU: vsync plus a 60 fps frame cap (it ran flat out before)
diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index ef67c5e..e196815 100644
--- a/entropie/ARCHITECTURE.md
+++ b/entropie/ARCHITECTURE.md
@@ -12,7 +12,7 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL
 | `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`), everybody in the story (`cast.c`), the floors' stages (`stage.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, the story (`game_story.c`: the crew turning up, the crew overview), 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), the shopping run (`game_shop.c`), 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 e69e7f1..2b1095f 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.40.0
+VERSION   := 2.41.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 51cf93d..9953134 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -16,7 +16,8 @@ people living there.
   map (about 340 x 340 m): the farmyard, the house, your garages, the barn,
   forest, roads. Painted in the rotoscope style of *Waking Life*
 - **Arriving:** by car with Volkram, Jana and Toni. Say hello to Kurti and
-  his sons, unload the car, pitch the tents
+  his sons, unload the car, pitch the tents. A golden marker shows who or
+  what's next; Kurti walks you to your camping spot
 - **People:** everyone has their own temperament, build, skills, walk and
   habits. They eat, drink, smoke, pee, sleep, work, chat and say no
 - **The festival:** the crew turns up over the first day. Four floors get
@@ -24,6 +25,9 @@ people living there.
   lead and helpers - or by you (hold left click at a stage). Talk to people
   about it (X, then 4): praise them, ask someone to lead, take a lead over.
   Fed up, people give up leading or helping
+- **Money and shopping:** a cash box of 1200 EUR. Send Volkram to the
+  hardware store for boards, nails, screws, paint and food. The stages need
+  that material; the crew fetches it from the store
 - **Moving:** real steps, stepping over and onto things, jumping, climbing
   (several ways up), ladders, stumbles and falls
 - **Work:** fell trees, cut and split firewood, stack it, clean up, carry
@@ -130,5 +134,5 @@ Dependencies point downward: `main → game → world/people → render → core
 
 ## Version
 
-`make version` prints e.g. `entropie 2.40.0 (build 43375ee)`. The build id is
+`make version` prints e.g. `entropie 2.41.0 (build 6232605)`. The build id is
 an md5 of all sources and assets.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 14e2f44..0ad72ab 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -29,9 +29,14 @@ things the way they do.
 - 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
+- Kartoffelacker (the potato field): world music, small talks. Behind the
+  road to the chicken farm, well past the orchard and the barn. The grass is
   cut with a scythe (the ground is too uneven for a mower: Toni). The group
-  from Darmstadt brings a geo-dome.
+  from Darmstadt brings a geo-dome. Seats to build, a chill-out platform up
+  a tree (about 1.8 m), hammocks to hang.
+- Right of the Kartoffelacker a strip of wood (about 10 m through): a way
+  cut through it one day (axe and spade); beyond it, above the chicken farm,
+  the guests' camping.
 - The hidden floor behind the main building: techno.
 
 **The crew** (who leads what is settled in the Herberge; it can change)
@@ -73,7 +78,7 @@ things the way they do.
    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.
+   nails, screws, paint. (2.41)
 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
@@ -336,6 +341,11 @@ Painted in the rotoscope style of *Waking Life* (2001):
 - Then the work: the boot opens, the others unload. Crates go into the
   kitchen in your garages, the camping things up to the meadow behind them.
   Help: left click at the boot takes out the next thing (crates: both hands)
+- A golden diamond floats over who or what to go to next: Kurti (say hello,
+  the chat), the car ("get your things from the car - then come to me"),
+  Kurti again (with something of yours in your hands, or all of it out: he
+  walks you up to the meadow, waiting when you fall behind: "this is a good
+  spot for your tent"), then your tent's spot till it stands
 - In the boot: 2 crates of beer, 2 of water, one of soda (cola and orange),
   one of Mate, everybody's sleeping bag and mat, the big tent (Volkram and Jana) and two small ones (Toni, you)
 - A crate is heavy: carrying it tires you (the weaker sooner). Worn out, you
@@ -375,12 +385,26 @@ Painted in the rotoscope style of *Waking Life* (2001):
   the gravel road, over to the fire to say hello, then on with the day
 - The four floors' stages stand marked out from the start: the Hofbuehne
   in the yard (its back to the barn), the Obstwiese at the far end of the
-  orchard, the Kartoffelacker on the field behind the tents' meadow, the
-  hidden floor behind the main building. Each is built in steps - the
-  ground cleared, the frame (three logs lying by it), the deck (stand on
-  it), the back wall and the roof, paint and banners in the floor's colour.
+  orchard, the Kartoffelacker behind the road to the chicken farm (a strip
+  of wood right of it), the hidden floor behind the main building. Each is
+  built in steps, each step needing its material lying by it - the ground
+  cleared, the frame (three logs), the deck (two bundles of boards, a box of
+  nails; stand on it), the back wall and the roof (two bundles of boards, a
+  box of screws), paint and banners in the floor's colour (a bucket of paint).
   The lead and the helpers go to it in working hours (8:30-19:30) for goes
   of two hours; you build too: a hand free, hold left click at it
+- The shopping: ask whoever has the car keys (Volkram: X, 4, "We need some
+  shopping done"). Pick what to get (each number again: one more; past the
+  most, none): bundles of boards 25 EUR, nails and screws 20, paint 35, food
+  and drinks for the crew 90 (a food crate of 40 portions, a cool box of 24
+  sausages, a crate of water, one of beer). The cash box starts at 1200 EUR
+  (F2, the projects page). They walk to the car, back out, turn and drive
+  off; an hour later (game time) they're back, the boot full: they and a
+  couple of idle people unload it - material to the store in front of the
+  car room of your garages, food into the kitchen. Food runs out now
+  ("The food's all gone. Somebody has to go shopping.")
+- A stage waiting for material that lies in the store: one of those at it
+  fetches it over (logs are yours to bring)
 - Talk to anybody about the festival (X, then 4): how their part goes and
   how they feel about it; "Great work - thanks!" (once an hour) lifts them;
   ask them to lead a floor nobody leads (if they're up for it); take over
@@ -402,8 +426,8 @@ Painted in the rotoscope style of *Waking Life* (2001):
   leans over the two of them
 - People get out of it: Kurti and his sons into the house, the others into
   their tent (at night, or when it's nearer), your garages' kitchen or the
-  shelter in the yard (a roof on posts by the fire, two log benches and a
-  table under it) - the nearest with a seat free. Raining hard, they sit it
+  two party pavilions in the yard (side by side by the fire, a beer table
+  set under each) - the nearest with a seat free. Raining hard, they sit it
   out there (else on the floor); out again when it stops
 - Out in the rain your clothes get wet (darker; Tab: Wet) and you get cold.
   They dry by a fire quickest, in the day slower, at night slowest. Wet
@@ -1353,13 +1377,28 @@ Round 2.40 (projects and leads: the floors get built):
 - F2 projects page: each floor's step and % ("needs logs"); F1: commitment. Checked (60x time, fed): Kilian cleared the Obstwiese in 1.5 game h, the frame held up till three logs lay there, then on; the Hofbuehne's helpers cleared it without a lead and gave up on the frame ("No logs, no frame"); talks: praise +0.06, Julia took the Hofbuehne, you took Pascal's light, Pascal took the Kartoffelacker.
 - The "## Open tasks" heading lost in 2.39 is back.
 
+Round 2.41 (materials and money; the start shows the way; the pavilions; the GPU rests):
+- The cash box: `World.cash` (WORLD_CASH 1200 EUR at the start, saved `cash`); F2's projects page shows it.
+- OBJ_MATERIAL (`MaterialKind`): a bundle of 8 boards (0.6 m, cut to length: longer ones get stuck carried past things, see Open tasks; 17 kg, both hands), a box of nails, a box of screws (1 kg), a bucket of paint (6 kg); `Shapes_material`. Stages (`stage.c` NEED_LOGS / NEED_MAT): frame 3 logs; deck 2 bundles + nails; roof 2 bundles + screws; decoration 1 paint - lying within STAGE_LOG_REACH 6 m, used up when the step is done (`Stage_missing` says what's missing, `Stage_missingMaterial` the bought kind).
+- Shopping (`talk.c` NODE_SHOP, the festival answer 4 to whoever has "Car keys"; refused: car away or not empty, drunk, done in): SHOP_PRICE 25 / 20 / 35 / 90, SHOP_MAX 8 / 4 / 4 / 2, over the cash: refused. `game/game_shop.c` (Errand, saved `errand`): ERRAND_TO_CAR (the driver walks to the driver's door: `ai.errand` in `ai_project.c` ai_errand) -> in, the money out of the box -> LEAVING (backs out BACK_OUT 10 m, turns round in TURN_TIME 3 s, off along the way in) -> AWAY (the car HIDDEN_DEPTH under the ground, SHOP_AWAY 1 game h) -> BACK (in again, parks) -> boot open, the driver and up to HELPERS 2 idle people near unload. `world.shopping` while it's on. Loaded mid-run: on the way over again, gone, away, or parked at once.
+- Cargo: CARGO_BOARDS / NAILS / SCREWS / PAINT (OBJ_MATERIAL), CARGO_FOOD / SAUSAGES (OBJ_SUPPLY made with a negative weight: carried, not static; `stock` FOOD_PORTIONS 40 / SAUSAGE_PORTIONS 24, -1 empty, 0 the scene's never run out; saved). CAR_LOAD_MAX 32. `Car_dropSpot`: material in rows at `Site_materialSpot` (in front of the car room's doorway), food boxes beside the drinks in the kitchen. ai.c unloadCar takes them all (`isCargo`); eating from a food crate uses a portion (you and them).
+- Fetching (`ai_project.c` fetchMaterial): held up for material that lies elsewhere (not at a stage, nobody bringing it, within FETCH_SPAN 150 m), a helper (the lead only when alone) carries it over and puts it down out in front (`Stage_workSpot` + 1.3 m: a side spot was behind the orchard's hedge); the others wait meanwhile; given up after FETCH_GIVE_UP 240 s. Held up with nothing to fetch: "We need ... - somebody go shopping!".
+- The start shows the way: `Game_questMarker` (game_arrival.c): a golden diamond (`Shapes_questMarker`, glowing, bobbing, turning) over Kurti (hello, chat), the car's boot (your things in it), Kurti (something in your hands or yours all out; within GUIDE_NEAR 4 m he takes you: `ai.errand` with `errand_with`, waiting when you're over GUIDE_WAIT 6 m behind), your tent's spot till it's up. `Arrival.guide` 0 / 1 / 2 (saved `guide`); Kurti and his sons stay out till it's shown. New lines: Jonas ("Dad will show you where the tents go"), Kurti at the end of the chat.
+- The yard: the roof on posts is gone; two party pavilions side by side (`manor.c` Shelter_build: 3 x 3 m, eaves 2.3 m, a canvas pyramid to 3.1 m, `Shapes_pavilion`; an unseen cover at 2.5 m keeps the rain off) with a beer table set each (table 2.2 x 0.5 m, two benches).
+- The Kartoffelacker moved to court(107.5, 45.9): behind the gravel road to the chicken farm, past the orchard and the barn, facing the road; FOREST_STRIP (about 10 m through) right of it.
+- The GPU: vsync (adaptive if offered) and a frame cap of FRAME_CAP 60 (`platform.c` Platform_swap: never quicker than 1/60 s a frame) - it ran flat out before.
+- Checked: a run with 4 bundles, nails and screws, paint, food (245 EUR): walked over in 1 s, off, away, back, all 11 things unloaded in 4 min (store 7, kitchen 2 food boxes); Kilian fetched 2 bundles and nails to the Obstwiese and the deck got going; saved and loaded while away (back as it should); the start: marker on Kurti, the car, Kurti, the tent spot - Kurti walked you up there.
+
 ## Open tasks
 
 Most important first.
 
 **The story** (see "The story (plan)": the roadmap)
-- Projects, still to do: the side jobs as projects of their own (decoration, light, mowing, snails, wood art); leads deciding more (where, what first, who does what); NPCs fetching the logs themselves; the crates and apples run out (food and drink: with the shopping)
-- Next: 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
+- Projects, still to do: the side jobs as projects of their own (decoration, light, mowing, snails, wood art); leads deciding more (where, what first, who does what); NPCs fetching the logs themselves; drinks crates running out (bottles: with the shopping)
+- Money, still to do: money coming in (tickets, the web things); you shopping yourself; paying for cigarettes
+- The Kartoffelacker: seats to build, a chill-out platform up a tree (about 1.8 m), hammocks to hang; the way through the strip of wood cut with an axe and a spade; the guests' camping beyond it (above the chicken farm) - check the places on the ground
+- Compost toilets: delivered for the festival, built together, put up at places decided later
+- Next: 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)
@@ -1375,7 +1414,8 @@ Most important first.
 **People**
 - The work place, still to do: trash jobs; walking a heavy piece round tight corners
 - Ladders, still to do: a holder for a ladder standing free; NPCs taking a ladder somewhere for their own work
-- Smoking, still to do: paying for a pack (no money in the game yet)
+- Smoking, still to do: paying for a pack (the cash box is the crew's, no money of your own yet)
+- Carrying long things (over 0.62 m across both arms): NPCs steer by their body only, the load catches on posts and they stand stuck (seen with 1 m board bundles at a pavilion post; the bundles are 0.6 m now)
 - "No shirt no service" policy (decision open)
 - Clothing, still to do: more outfits for extra people (only the performance test has any: they all wear trousers)
 - Tents, still to do: NPCs taking theirs down on their own (leaving)
@@ -1392,4 +1432,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.40.0 (build 43375ee)`
+State of `entropie 2.41.0 (build 6232605)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index 9d9cf74..fbd34bf 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,14 +1,17 @@
-entropie 2.40.0: the floors get built
+entropie 2.41.0: materials and money, the start shows the way, pavilions, GPU rests
 
-- Four stages marked out: Hofbuehne (yard), Obstwiese (orchard),
-  Kartoffelacker (the field behind the tents), the hidden floor (behind
-  the main building)
-- Built step by step: ground cleared, frame (needs three logs lying by
-  it), deck, back wall and roof, paint and banners in the floor's colour
-- Leads and helpers go and build in working hours; you build too (hold
-  left click at a stage)
-- Commitment: lifted by progress and praise, worn down by being held up,
-  working worn out, nobody leading, annoyance; too low, leads quit
-- Talk about the festival (X, then 4): praise, ask someone to lead an open
-  floor, take over a lead, get help with yours
-- Late arrivals say hello standing by the fire (no more walking into it)
+- Cash box (1200 EUR, saved, on the F2 projects page)
+- Building material: bundles of boards, nails, screws, paint (OBJ_MATERIAL).
+  Stage steps need it lying by them: deck and roof boards plus nails or
+  screws, decoration paint; used up when the step is done
+- Shopping run: ask whoever has the car keys (Volkram), pick the list; he
+  drives off and comes back an hour later, the crew unloads (material to a
+  store in front of the car room, food into the kitchen)
+- Food runs out now (food crate 40 portions, cool box 24 sausages)
+- Crew fetches material from the store to a stage that waits for it
+- Arrival: a golden marker over Kurti, the car, Kurti again; Kurti walks
+  you to your camping spot, then the marker shows your tent spot
+- Yard: the roof on posts replaced by two party pavilions with beer table sets
+- Kartoffelacker moved behind the road to the chicken farm, a strip of wood
+  right of it
+- GPU: vsync plus a 60 fps frame cap (it ran flat out before)
diff --git a/entropie/src/core/platform.c b/entropie/src/core/platform.c
index 4b3a095..427105c 100644
--- a/entropie/src/core/platform.c
+++ b/entropie/src/core/platform.c
@@ -59,6 +59,8 @@ bool Platform_init(Platform *p, const char *title, int width, int height) {
     if (!Gl_load()) return false;
 
     SDL_Log("OpenGL %s", (const char *)glGetString(GL_VERSION));
+    /* vsync (adaptive if there is it): no more frames than the screen shows */
+    if (!SDL_GL_SetSwapInterval(-1) && !SDL_GL_SetSwapInterval(1)) SDL_Log("No vsync: %s (the frame cap keeps it at %d fps)", SDL_GetError(), FRAME_CAP);
 
     SDL_SetWindowRelativeMouseMode(p->window, true);
 
@@ -89,8 +91,13 @@ void Platform_drawableSize(const Platform *p, int *width, int *height) {
     if (*height < 1) *height = 1;
 }
 
-void Platform_swap(const Platform *p) {
+void Platform_swap(Platform *p) {
     SDL_GL_SwapWindow(p->window);
+    /* the cap: a frame never quicker than 1 / FRAME_CAP s (vsync off or ignored: the GPU would run flat out) */
+    const Uint64 frame = 1000000000ull / FRAME_CAP;
+    Uint64 now = SDL_GetTicksNS(), spent = now - p->frame_start_ns;
+    if (p->frame_start_ns && spent < frame) SDL_DelayPrecise(frame - spent);
+    p->frame_start_ns = SDL_GetTicksNS();
 }
 
 void Platform_windowSize(const Platform *p, int *width, int *height) {
diff --git a/entropie/src/core/platform.h b/entropie/src/core/platform.h
index 126aa51..8ace92e 100644
--- a/entropie/src/core/platform.h
+++ b/entropie/src/core/platform.h
@@ -11,6 +11,7 @@ typedef struct {
     SDL_Window   *window;
     SDL_GLContext gl_context;
     Uint64        last_ticks_ns;
+    Uint64        frame_start_ns; /* (the frame cap: when the last frame was shown) */
     bool          sdl_ready;
 } Platform;
 
@@ -22,7 +23,9 @@ float Platform_frameDelta(Platform *p);
 
 /* Framebuffer size in pixels (HiDPI aware). */
 void  Platform_drawableSize(const Platform *p, int *width, int *height);
-void  Platform_swap(const Platform *p);
+/* Shows the frame: vsync, and never more than FRAME_CAP frames a second (the GPU gets to rest). */
+#define FRAME_CAP 60
+void  Platform_swap(Platform *p);
 /* Window size in window coordinates (the mouse uses these). */
 void  Platform_windowSize(const Platform *p, int *width, int *height);
 /* Mouse captured for looking around (hidden), or a free pointer (menus). */
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index aea6dc9..7f2fcff 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -256,6 +256,7 @@ bool Game_initWorld(Game *game, bool arrive) {
         }
     }
     game->marker = Shapes_marker(1.8f, 0.8f);
+    game->quest_mesh = Shapes_questMarker(0.7f, vec3(1.0f, 0.82f, 0.15f));
     for (int i = 0; i < game->world.count; i++) { /* (what the scene made: a save finds it again by this) */
         Object *o = game->world.objects[i];
         o->scene_id = i + 1;
@@ -272,6 +273,7 @@ void Game_shutdown(Game *game) {
     Meadow_destroy(&game->meadow);
     World_destroy(&game->world);
     Mesh_destroy(&game->marker);
+    Mesh_destroy(&game->quest_mesh);
     Dog_shutdown();
     game->player = NULL;
 }
@@ -372,6 +374,8 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     } else if (game->player->look.wet < 0.2f) game->wet_told = false;
     game_arrival(game, dt);
     game_story(game, game->world.game_dt);
+    game_shop(game, dt);
+    game->quest_t += dt;
     Ai_setSettled(game->arrival.task == TASK_NONE);
     tooHeavy(game);
     bool warm = game->player->look.top == TOP_JACKET;
@@ -519,4 +523,11 @@ void Game_render(Game *game, Renderer *renderer) {
         Mat4 model = mat4_mul(mat4_translate(at), mat4_rotate_y(p->rotation.y));
         Renderer_drawDecal(renderer, &game->marker, model, TERRAIN_SIZE);
     }
+    /* Who or what to go to next: a golden diamond over it, bobbing and turning */
+    Vec3 quest;
+    if (game->quest_mesh.vao && Game_questMarker(game, &quest)) {
+        float t = game->quest_t;
+        Mat4 model = mat4_mul(mat4_translate(vec3_add(quest, vec3(0.0f, 0.08f * sinf(t * 2.6f), 0.0f))), mat4_rotate_y(t * 1.8f));
+        Renderer_drawMeshTinted(renderer, &game->quest_mesh, model, vec3(1.0f, 1.0f, 1.0f), 0.55f);
+    }
 }
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index ade740f..e7d2054 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -31,8 +31,19 @@ typedef struct {
     float    chat_wait;             /* ... since the hello */
     int      welcome;               /* Kurti and his sons welcoming you at the fire: lines said so far ... */
     float    welcome_t;             /* ... until the next */
+    int      guide;                 /* Kurti showing you where to camp: 0 not yet, 1 on the way, 2 shown */
 } Arrival;
 
+/* A shopping run (game_shop.c): over to the car, out along the way in, away, back, parked, the boot open. */
+typedef enum { ERRAND_NONE, ERRAND_TO_CAR, ERRAND_LEAVING, ERRAND_AWAY, ERRAND_BACK } ErrandStage;
+typedef struct {
+    ErrandStage    stage;
+    struct Object *driver;
+    int            list[SHOP_ITEMS]; /* what to get (talk.h SHOP_*) */
+    float          time;             /* in this stage (s; away: game s) */
+    float          s, speed;         /* where on the way in (m), how fast */
+} Errand;
+
 typedef struct {
     World    world;
     Object  *player;
@@ -89,6 +100,9 @@ typedef struct {
     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 */
+    Errand   errand;        /* a shopping run */
+    Mesh     quest_mesh;    /* the marker over who or what to go to next (Game_questMarker) ... */
+    float    quest_t;       /* ... its bob and turn */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
@@ -102,6 +116,10 @@ int  Game_bubbles(const Game *game, Bubble *out, int max, int screen_w, int scre
 /* Answers the player can give right now (1..5): in a conversation or the shout menu; else 0. */
 int  Game_choices(const Game *game, const char *out[TALK_CHOICES]);
 
+/* Where the marker floats: over who or what to go to next (the start: Kurti, the car, Kurti showing
+   you where to camp, your tent's spot); false: nothing to point at now. */
+bool Game_questMarker(const Game *game, Vec3 *at);
+
 /* A line about what to do right now (someone follows or watches you), or NULL. */
 const char *Game_hint(Game *game);
 
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index 75ab6c2..e8467f8 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -597,6 +597,8 @@ static bool takeFromSupply(Game *game) {
             if (bottle && Hands_pickUpObject(p, hand, bottle)) { tell(game, bottle); World_noise(&game->world, NOISE_CLINK, o->position, 0.5f); }
             return true;
         }
+        if (o->stock < 0) { say(game, o->dispenses == OBJ_APPLE ? "The food crate is empty - time for a shopping run" : "The cool box is empty"); return true; }
+        if (o->stock > 0 && --o->stock == 0) o->stock = -1; /* (bought: it runs out) */
         ObjectType what = o->dispenses;
         Vec3 top = vec3_add(Object_colliderCenter(o, 0), vec3(0.0f, o->colliders[0].half_extents.y + 0.1f, 0.0f));
         Vec3 size = what == OBJ_APPLE ? vec3(0.08f, 0.08f, 0.08f) : what == OBJ_SAUSAGE ? vec3(0.0f, 0.16f, 0.0f)
@@ -787,8 +789,10 @@ static bool workStage(Game *game) {
     int got = Stage_work(&game->world, s, (0.6f + 0.8f * handy) * game->world.game_dt / 3600.0f);
     p->needs.stamina = fmaxf(0.0f, p->needs.stamina - STAGE_TIRES * game->dt);
     if (got == -2) {
-        snprintf(game->info, sizeof game->info, "The frame needs %d more log%s lying by it (carry some here: F with a helper)",
-                 Stage_logsMissing(&game->world, s), Stage_logsMissing(&game->world, s) == 1 ? "" : "s");
+        char what[96];
+        Stage_missing(&game->world, s, what, sizeof what);
+        snprintf(game->info, sizeof game->info, s->build_stage == BUILD_FRAME ? "The frame needs %s lying by it (carry some here: F with a helper)"
+                                                : "It needs %s lying by it (the shopping: ask whoever has the car keys)", what);
         say(game, game->info);
         return false;
     }
diff --git a/entropie/src/game/game_arrival.c b/entropie/src/game/game_arrival.c
index eb3c0fe..d3541b8 100644
--- a/entropie/src/game/game_arrival.c
+++ b/entropie/src/game/game_arrival.c
@@ -45,9 +45,9 @@ static const char *const FRIENDS[] = { "Volkram", "Jana", "Toni" };
 static void chat(Game *game, float dt);
 static void settled(Game *game);
 
-/* While you're still to say hello and have a chat: Kurti and his sons stay at the fire. */
+/* While you're still to say hello and have a chat, and till Kurti has shown you where to camp: Kurti and his sons stay out. */
 static void hostsStay(Game *game) {
-    if (game->arrival.task != TASK_HELLO && game->arrival.task != TASK_CHAT) return;
+    if (game->arrival.task != TASK_HELLO && game->arrival.task != TASK_CHAT && !(game->arrival.task == TASK_UNLOAD && game->arrival.guide < 2)) return;
     for (int i = 0; i < game->world.count; i++) {
         Object *o = game->world.objects[i];
         if (o->type == OBJ_HUMAN && o->lives_here) { o->ai.going_in = false; o->indoors = 0.0f; }
@@ -60,18 +60,25 @@ static Object *fireplace(Game *game) {
     return NULL;
 }
 
-void game_arrivalStart(Game *game) {
+bool game_drivePath(Game *game) {
     Arrival *a = &game->arrival;
+    if (a->length > 0.0f) return true;
     Vec3 pts[ARRIVAL_POINTS];
     int n = 0;
     Site_arrivalPath(pts, &n, &a->park_yaw);
-    if (n < 2) return;
+    if (n < 2) return false;
     a->length = 0.0f;
     for (int k = 0; k < PATH_SAMPLES; k++) { /* (the curve, sampled; how far along each sample is) */
         a->path[k] = along(pts, n, (float)k / (float)(PATH_SAMPLES - 1));
         if (k > 0) a->length += vec3_length(vec3_sub(vec3(a->path[k].x, 0.0f, a->path[k].z), vec3(a->path[k - 1].x, 0.0f, a->path[k - 1].z)));
         a->dist[k] = a->length;
     }
+    return a->length > 0.0f;
+}
+
+void game_arrivalStart(Game *game) {
+    Arrival *a = &game->arrival;
+    if (!game_drivePath(game)) return;
     Vec3 head = vec3_sub(a->path[1], a->path[0]);
     Object *car = Car_create(a->path[0], yaw_facing(vec3_normalize(vec3(head.x, 0.0f, head.z))));
     if (!car || !World_add(&game->world, car)) return;
@@ -105,7 +112,7 @@ void game_arrivalStart(Game *game) {
 }
 
 /* Where the car is s meters along, and which way it heads. */
-static void at(const Arrival *a, float s, Vec3 *pos, Vec3 *dir) {
+void game_driveAt(const Arrival *a, float s, Vec3 *pos, Vec3 *dir) {
     int k = 1;
     while (k < PATH_SAMPLES - 1 && a->dist[k] < s) k++;
     float span = fmaxf(a->dist[k] - a->dist[k - 1], 1e-4f), t = clampf((s - a->dist[k - 1]) / span, 0.0f, 1.0f);
@@ -129,7 +136,7 @@ static void welcome(Game *game, float dt) {
     static const struct { const char *who, *line; } LINES[] = {
         { "Kurti",  "Welcome, welcome! Good to see you all - sit down, warm up." },
         { "Martin", "You brought your own beer? Smart. Ours is gone." },
-        { "Jonas",  "The tents go up on the meadow behind the garages - the little path left of them." },
+        { "Jonas",  "Make yourselves at home. Dad will show you where the tents go." },
         { "Kurti",  "The trees and bushes up there stay, mind. The toilet's in the house. Sit down a moment!" },
     };
     const int n = (int)(sizeof LINES / sizeof LINES[0]);
@@ -150,10 +157,100 @@ static void welcome(Game *game, float dt) {
     if (a->welcome >= n) { a->task = TASK_CHAT; a->chat = 0; a->chat_wait = 0.0f; a->welcome_t = 0.0f; }
 }
 
+/* ---------- Kurti shows you where to camp ---------- */
+
+#define GUIDE_NEAR  4.0f /* m: by Kurti with your things, he takes you there */
+#define GUIDE_THERE 6.0f /* m: you by him (he's there): shown */
+#define MARK_ABOVE  0.75f /* m: the marker over a head */
+
+static bool carrying(const Object *p) {
+    return p->hands.state[HAND_LEFT] == HAND_HOLDING || p->hands.state[HAND_RIGHT] == HAND_HOLDING;
+}
+
+/* Nothing of yours left in the car. */
+static bool yourThingsOut(const Game *game) {
+    const Object *car = game->arrival.car;
+    if (!car || !car->load) return true;
+    for (int k = 0; k < car->load->n; k++) if (car->load->owner[k] == game->player) return false;
+    return true;
+}
+
+static void guide(Game *game) {
+    Arrival *a = &game->arrival;
+    if (a->task != TASK_UNLOAD || a->guide >= 2) return;
+    Object *kurti = person(game, "Kurti"), *p = game->player;
+    if (!kurti) { a->guide = 2; return; }
+    Vec3 spot = Site_campSpot(0);
+    if (a->guide == 0) {
+        if (!carrying(p) && !yourThingsOut(game)) return;
+        if (hypotf(kurti->position.x - p->position.x, kurti->position.z - p->position.z) > GUIDE_NEAR || p->riding) return;
+        Ai_say(kurti, "Got your things? Come with me - I'll show you where you can camp.");
+        a->guide = 1;
+        say(game, "Follow Kurti - he shows you where you can camp");
+    }
+    if (!kurti->ai.errand) { /* (off he goes - again after loading) */
+        if (Ai_isHelping(kurti)) Ai_stopHelping(kurti);
+        kurti->ai.mode = AI_IDLE;
+        kurti->ai.errand = true;
+        kurti->ai.errand_there = false;
+        kurti->ai.errand_to = vec3_add(spot, vec3_scale(Car_campDoor(), CAMP_BEDS_OUT + 1.5f)); /* (in front of where the tent's door will be) */
+        kurti->ai.errand_with = p;
+    }
+    if (kurti->ai.errand_there && hypotf(kurti->position.x - p->position.x, kurti->position.z - p->position.z) < GUIDE_THERE) {
+        Ai_say(kurti, "Here - this is a good spot for your tent. The others go up next to you.");
+        kurti->ai.errand = false;
+        kurti->ai.errand_with = NULL;
+        a->guide = 2;
+        say(game, "Your tent goes here - put it down and hold left click on it to put it up");
+    }
+}
+
+/* Your tent: standing, or where it's to go up. */
+static const Object *yourTent(const Game *game, bool *up) {
+    *up = false;
+    for (int i = 0; i < game->world.count; i++) {
+        const Object *o = game->world.objects[i];
+        if (o->type == OBJ_TENT && o->owner == game->player && !o->broken) { *up = o->tent_stage >= TENT_UP; return o; }
+    }
+    return NULL;
+}
+
+bool Game_questMarker(const Game *game, Vec3 *at) {
+    const Arrival *a = &game->arrival;
+    const Object *p = game->player, *kurti = NULL;
+    if (a->stage == ARRIVAL_DRIVING || p->riding) return false;
+    for (int i = 0; i < game->world.count && !kurti; i++)
+        if (game->world.objects[i]->type == OBJ_HUMAN && !strcmp(game->world.objects[i]->name, "Kurti")) kurti = game->world.objects[i];
+    bool to_kurti = false;
+    switch (a->task) {
+    case TASK_HELLO: case TASK_CHAT: to_kurti = true; break;
+    case TASK_UNLOAD:
+        if (a->guide == 1 || (a->guide == 0 && (carrying(p) || yourThingsOut(game)))) to_kurti = true;
+        else if (a->guide == 0 && a->car) { /* your things are still in the car */
+            *at = vec3_add(Car_bootSpot(a->car), vec3(0.0f, 2.3f, 0.0f));
+            return true;
+        } else if (a->guide == 2) { /* your tent, till it's up */
+            bool up;
+            const Object *t = yourTent(game, &up);
+            if (up) return false;
+            Vec3 spot = t ? t->position : Site_campSpot(0);
+            spot.y = World_groundHeight(&game->world, spot.x, spot.z, spot.y + 3.0f);
+            *at = vec3_add(spot, vec3(0.0f, 1.6f, 0.0f));
+            return true;
+        }
+        break;
+    default: return false;
+    }
+    if (!to_kurti || !kurti || kurti->indoors > 0.0f) return false;
+    *at = vec3_add(kurti->position, vec3(0.0f, kurti->look.height + MARK_ABOVE, 0.0f));
+    return true;
+}
+
 void game_arrival(Game *game, float dt) {
     Arrival *a = &game->arrival;
     Object *car = a->car;
     hostsStay(game);
+    guide(game);
     welcome(game, dt);
     chat(game, dt);
     settled(game);
@@ -165,7 +262,7 @@ void game_arrival(Game *game, float dt) {
         a->speed = fminf(fminf(a->speed + DRIVE_ACCEL * dt, DRIVE_TOP), brake + 0.15f);
         a->s = fminf(a->length, a->s + a->speed * dt);
         Vec3 pos, dir;
-        at(a, a->s, &pos, &dir);
+        game_driveAt(a, a->s, &pos, &dir);
         float yaw = yaw_facing(dir);
         if (a->length - a->s < 4.0f) /* (the last bit: straight in to its spot) */
             yaw = lerpf(yaw, a->park_yaw, smoothstepf(4.0f, 0.0f, a->length - a->s));
@@ -229,6 +326,7 @@ static void chat(Game *game, float dt) {
         { "Kurti",   "So. Four of you this year - and a whole festival to build." },
         { "Kurti",   "Build what you like, just keep the fire in its ring. And mind the old trees." },
         { "Volkram", "Deal. Right - let's get the car empty and the tents up before it gets dark." },
+        { "Kurti",   "Go and get your things from the car - then come to me, I'll show you where you can camp." },
     };
     const int n = (int)(sizeof LINES / sizeof LINES[0]);
     Object *p = game->player, *kurti = person(game, "Kurti");
@@ -264,7 +362,10 @@ const char *game_task(Game *game) {
     switch (a->task) {
     case TASK_HELLO:  return "To do: say hello to Kurti and his sons at the fire";
     case TASK_CHAT:   return "To do: sit down with them (C) and have a chat";
-    case TASK_UNLOAD: return "To do: unload the car (crates into the garages' kitchen, camping things up behind the garages) and put the tents up";
+    case TASK_UNLOAD:
+        if (a->guide == 0) return "To do: get your things from the car (left click at the boot), then over to Kurti - he shows you where to camp";
+        if (a->guide == 1) return "To do: follow Kurti - he shows you where you can camp";
+        return "To do: put your tent up (hold left click on it), help the others empty the car";
     default:          return NULL;
     }
 }
@@ -304,6 +405,7 @@ void game_skipArrival(Game *game) {
         if (o) { o->ai.greet = false; o->ai.unload = false; o->sitting = false; }
     }
     a->task = TASK_NONE;
+    a->guide = 2;
     say(game, "Dev: the start skipped");
 }
 
@@ -335,6 +437,8 @@ bool game_unloadCar(Game *game) {
     Hands_pickUpObject(p, hand < 0 ? HAND_RIGHT : hand, item);
     snprintf(game->info, sizeof(game->info), "You take out %s - %s", next,
              item->type == OBJ_CRATE ? "the crates go into the kitchen in your garages"
+             : item->type == OBJ_SUPPLY ? "the food goes into the kitchen in your garages"
+             : item->type == OBJ_MATERIAL ? "the material goes in front of the car room of your garages (the store)"
                                        : "the camping things go up to the meadow behind your garages");
     game->info_about = NULL;
     say(game, game->info);
diff --git a/entropie/src/game/game_internal.h b/entropie/src/game/game_internal.h
index 7f52a3b..3f8b37c 100644
--- a/entropie/src/game/game_internal.h
+++ b/entropie/src/game/game_internal.h
@@ -28,6 +28,14 @@ bool game_arriving(const Game *game);
 bool game_unloadCar(Game *game);
 const char *game_task(Game *game);
 void game_skipArrival(Game *game);
+/* The way in by car, sampled (once); where on it s meters along, which way it heads. */
+bool game_drivePath(Game *game);
+void game_driveAt(const Arrival *a, float s, Vec3 *pos, Vec3 *dir);
+/* game_shop.c: the shopping run - the one with the car keys off with the list, back an hour later, the boot full. */
+void game_shopStart(Game *game, Object *driver, const int list[SHOP_ITEMS]);
+void game_shop(Game *game, float dt);
+/* After loading: a run that was on its way picked up again. */
+void game_shopLoaded(Game *game);
 
 void game_callHelp(Game *game);
 void game_callOver(Game *game);
diff --git a/entropie/src/game/game_shop.c b/entropie/src/game/game_shop.c
new file mode 100644
index 0000000..d739627
--- /dev/null
+++ b/entropie/src/game/game_shop.c
@@ -0,0 +1,202 @@
+/*
+ * The shopping run: asked in a talk (talk.c NODE_SHOP), the one with the car
+ * keys walks over to the car, backs it out of the yard, turns and drives off
+ * along the way in; an hour later (game time) the car comes back the same way
+ * with the boot full - boards, nails, screws, paint, food and drink - parks
+ * where it stood and the boot opens: the driver and whoever is idle nearby
+ * unload it (ai.c unloadCar; material in front of the car room, food into the
+ * kitchen: car.c Car_dropSpot). The money comes out of the cash box as they
+ * leave.
+ */
+
+#include <math.h>
+
+#include "game/game_internal.h"
+#include "world/car.h"
+
+#define SHOP_AWAY     3600.0f /* game s at the shops (there and back included) */
+#define TO_CAR_MAX    120.0f  /* s walking to the car: then they're in it anyway (stuck somewhere) */
+#define BACK_OUT      10.0f   /* m backed out of the parking spot ... */
+#define BACK_SPEED    1.6f    /* ... this fast ... */
+#define TURN_TIME     3.0f    /* ... turned round in this long (s) ... */
+#define DRIVE_TOP     6.0f    /* ... then off (m/s) */
+#define DRIVE_ACCEL   1.6f
+#define DRIVE_BRAKE   1.4f
+#define HIDDEN_DEPTH  300.0f  /* m under the ground while it's away (out of sight, out of the way) */
+#define HELPERS       2       /* idle people near who come and unload too */
+#define HELPER_NEAR   45.0f
+
+static Object *theCar(Game *game) {
+    if (game->arrival.car) return game->arrival.car;
+    for (int i = 0; i < game->world.count; i++)
+        if (game->world.objects[i]->type == OBJ_CAR) return game->arrival.car = game->world.objects[i];
+    return NULL;
+}
+
+static int total(const int list[SHOP_ITEMS]) {
+    int sum = 0;
+    for (int k = 0; k < SHOP_ITEMS; k++) sum += list[k] * SHOP_PRICE[k];
+    return sum;
+}
+
+void game_shopStart(Game *game, Object *driver, const int list[SHOP_ITEMS]) {
+    Errand *e = &game->errand;
+    Object *car = theCar(game);
+    if (!car || !driver || e->stage != ERRAND_NONE || !game_drivePath(game)) return;
+    memset(e, 0, sizeof *e);
+    for (int k = 0; k < SHOP_ITEMS; k++) e->list[k] = list[k];
+    e->driver = driver;
+    e->stage = ERRAND_TO_CAR;
+    if (Ai_isHelping(driver)) Ai_stopHelping(driver);
+    driver->ai.mode = AI_IDLE;
+    driver->ai.errand = true;
+    driver->ai.errand_there = false;
+    driver->ai.errand_to = Car_door(car, 0);
+    game->world.shopping = true;
+}
+
+/* Out of sight while it's away: down under the ground (and the driver in it). */
+static void hide(Game *game, Object *car) {
+    Car_place(&game->world, car, game->arrival.path[0], car->rotation.y);
+    car->position.y -= HIDDEN_DEPTH;
+    Object_syncColliders(car);
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        if (o->riding != car) continue;
+        o->position.y -= HIDDEN_DEPTH;
+        o->sit_at = o->position;
+        Object_syncColliders(o);
+    }
+}
+
+/* The boot full of what was on the list. */
+static void loadUp(Object *car, const int list[SHOP_ITEMS]) {
+    for (int k = 0; k < list[SHOP_FOOD]; k++) { /* (food and drink at the bottom, the material on top) */
+        Car_load(car, CARGO_WATER, NULL);
+        Car_load(car, CARGO_BEER, NULL);
+        Car_load(car, CARGO_SAUSAGES, NULL);
+        Car_load(car, CARGO_FOOD, NULL);
+    }
+    for (int k = 0; k < list[SHOP_PAINT]; k++) Car_load(car, CARGO_PAINT, NULL);
+    for (int k = 0; k < list[SHOP_FIXINGS]; k++) { Car_load(car, CARGO_SCREWS, NULL); Car_load(car, CARGO_NAILS, NULL); }
+    for (int k = 0; k < list[SHOP_BOARDS]; k++) Car_load(car, CARGO_BOARDS, NULL);
+}
+
+/* Parked again: out, the boot up, unloading (the driver and a couple of idle people near). */
+static void arrived(Game *game, Object *car) {
+    Errand *e = &game->errand;
+    Object *d = e->driver;
+    Car_place(&game->world, car, game->arrival.path[ARRIVAL_SAMPLES - 1], game->arrival.park_yaw);
+    if (d && d->riding) Car_getOut(d);
+    Car_openBoot(car, true);
+    int n = 0;
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        bool idle = o->ai.mode == AI_IDLE || o->ai.mode == AI_WANDER;
+        if (o->type != OBJ_HUMAN || o->riding || o->lives_here || Body_isDown(&o->body) || o->in_tent || o->indoors > 0.0f) continue;
+        if (o != d && (n >= HELPERS || !idle || o->ai.on_project || hypotf(o->position.x - car->position.x, o->position.z - car->position.z) > HELPER_NEAR)) continue;
+        o->ai.unload = true;
+        o->ai.unload_car = car;
+        o->ai.unload_time = 0.0f;
+        o->ai.side = (float)(n % 3 - 1);
+        if (o != d) n++;
+    }
+    if (d) Ai_say(d, "We're back! Come on, help me unload - the boot's full.");
+    static char line[96];
+    snprintf(line, sizeof line, "%s is back from the hardware store - help unload the car", d ? d->name : "The car");
+    say(game, line);
+    game->world.shopping = false;
+    memset(e, 0, sizeof *e);
+}
+
+void game_shop(Game *game, float dt) {
+    Errand *e = &game->errand;
+    if (e->stage == ERRAND_NONE) return;
+    Object *car = theCar(game), *d = e->driver;
+    Arrival *a = &game->arrival;
+    if (!car || !game_drivePath(game)) { memset(e, 0, sizeof *e); game->world.shopping = false; return; }
+    e->time += e->stage == ERRAND_AWAY ? game->world.game_dt : dt;
+    Vec3 pos, dir;
+    static char line[96];
+    switch (e->stage) {
+    case ERRAND_TO_CAR: /* walking over (ai_project.c ai_errand); then in, the money out of the box, off */
+        if (!d || Body_isDown(&d->body)) {
+            if (d) d->ai.errand = false;
+            memset(e, 0, sizeof *e);
+            game->world.shopping = false;
+            return;
+        }
+        if (!d->ai.errand_there && e->time < TO_CAR_MAX) return;
+        d->ai.errand = d->ai.errand_there = false;
+        Car_openBoot(car, false);
+        Car_getIn(car, d, 0);
+        game->world.cash -= (float)total(e->list);
+        e->stage = ERRAND_LEAVING;
+        e->time = 0.0f;
+        e->s = a->length;
+        e->speed = 0.0f;
+        snprintf(line, sizeof line, "%s drives off to the hardware store (%d EUR from the cash box)", d->name, total(e->list));
+        say(game, line);
+        break;
+    case ERRAND_LEAVING: {
+        float back_to = a->length - BACK_OUT;
+        if (e->s > back_to) { /* backing out, still nose to the barn */
+            e->s = fmaxf(back_to, e->s - BACK_SPEED * dt);
+            game_driveAt(a, e->s, &pos, &dir);
+            Car_place(&game->world, car, pos, yaw_facing(dir));
+            e->time = 0.0f;
+            return;
+        }
+        game_driveAt(a, e->s, &pos, &dir);
+        if (e->time < TURN_TIME) { /* turning round where it is */
+            float t = smoothstepf(0.0f, TURN_TIME, e->time);
+            Car_place(&game->world, car, pos, yaw_facing(dir) + PI_F * t);
+            return;
+        }
+        e->speed = fminf(e->speed + DRIVE_ACCEL * dt, DRIVE_TOP);
+        e->s = fmaxf(0.0f, e->s - e->speed * dt);
+        game_driveAt(a, e->s, &pos, &dir);
+        Car_place(&game->world, car, pos, yaw_facing(vec3_scale(dir, -1.0f)));
+        if (e->s <= 0.0f) { hide(game, car); e->stage = ERRAND_AWAY; e->time = 0.0f; }
+        break;
+    }
+    case ERRAND_AWAY:
+        if (e->time < SHOP_AWAY) return;
+        loadUp(car, e->list);
+        e->stage = ERRAND_BACK;
+        e->time = 0.0f;
+        e->s = 0.0f;
+        e->speed = 3.0f;
+        break;
+    case ERRAND_BACK: { /* in again the way it came the first time, braking for its spot */
+        float left = a->length - e->s;
+        float brake = sqrtf(fmaxf(0.0f, 2.0f * DRIVE_BRAKE * left));
+        e->speed = fminf(fminf(e->speed + DRIVE_ACCEL * dt, DRIVE_TOP), brake + 0.15f);
+        e->s = fminf(a->length, e->s + e->speed * dt);
+        game_driveAt(a, e->s, &pos, &dir);
+        float yaw = yaw_facing(dir);
+        if (left < 4.0f) yaw = lerpf(yaw, a->park_yaw, smoothstepf(4.0f, 0.0f, left));
+        Car_place(&game->world, car, pos, yaw);
+        if (e->s >= a->length - 0.01f) arrived(game, car);
+        break;
+    }
+    default: break;
+    }
+}
+
+void game_shopLoaded(Game *game) {
+    Errand *e = &game->errand;
+    Object *car = theCar(game);
+    game->world.shopping = e->stage != ERRAND_NONE;
+    if (e->stage == ERRAND_NONE || !car || !game_drivePath(game)) return;
+    if (e->stage == ERRAND_TO_CAR && e->driver) { /* (on the way over again) */
+        e->driver->ai.errand = true;
+        e->driver->ai.errand_there = false;
+        e->driver->ai.errand_to = Car_door(car, 0);
+    } else if (e->stage == ERRAND_LEAVING) { /* (gone, as good as) */
+        hide(game, car);
+        e->stage = ERRAND_AWAY;
+        e->time = 0.0f;
+    } else if (e->stage == ERRAND_AWAY) hide(game, car);
+    else if (e->stage == ERRAND_BACK) arrived(game, car);
+}
diff --git a/entropie/src/game/game_story.c b/entropie/src/game/game_story.c
index 47db44c..d295215 100644
--- a/entropie/src/game/game_story.c
+++ b/entropie/src/game/game_story.c
@@ -95,7 +95,7 @@ static void doing(const Object *o, char *out, size_t n) {
 }
 
 int Game_crewLines(const Game *game, char lines[][128], int max, const char **title) {
-    static char head[96];
+    static char head[128];
     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) {
@@ -119,7 +119,8 @@ int Game_crewLines(const Game *game, char lines[][128], int max, const char **ti
                      (double)(o->ai.annoyed * 100.0f), (double)(o->needs.stamina * 100.0f));
         }
     } else {
-        snprintf(head, sizeof head, "Projects - who leads, who helps   (Left: back)");
+        snprintf(head, sizeof head, "Projects - who leads, who helps   cash box %d EUR%s   (Left: back)", (int)game->world.cash,
+                 game->errand.stage != ERRAND_NONE ? ", shopping run on" : "");
         for (int p = PROJECT_ORGA; p < PROJECT_COUNT && n < max; p++) {
             char lead[48] = "nobody yet", helpers[80] = "";
             size_t len = 0;
@@ -133,12 +134,13 @@ int Game_crewLines(const Game *game, char lines[][128], int max, const char **ti
                     while (helpers[len]) len++;
                 }
             }
-            char built[48] = "";
+            char built[112] = "";
             const Object *s = Stage_of(game->world.objects, game->world.count, (Project)p);
             if (s && s->build_stage >= BUILD_DONE) snprintf(built, sizeof built, "  [built]");
             else if (s) {
-                int logs = Stage_logsMissing(&game->world, s);
-                snprintf(built, sizeof built, "  [%s %d%%%s]", Stage_stepName(s->build_stage), (int)(Stage_part(s) * 100.0f), logs ? ", needs logs" : "");
+                char what[64];
+                bool needs = Stage_missing(&game->world, s, what, sizeof what) > 0;
+                snprintf(built, sizeof built, "  [%s %d%%%s%s]", Stage_stepName(s->build_stage), (int)(Stage_part(s) * 100.0f), needs ? ", needs " : "", needs ? what : "");
             }
             snprintf(lines[n++], 128, "%s%s:  %s%s%s", Cast_projectName((Project)p), built, lead, len ? "   helping: " : "", helpers);
         }
diff --git a/entropie/src/game/game_talk.c b/entropie/src/game/game_talk.c
index 02c1710..bf6f8c6 100644
--- a/entropie/src/game/game_talk.c
+++ b/entropie/src/game/game_talk.c
@@ -196,4 +196,8 @@ void game_talk(Game *game, const Input *input, float dt) {
         }
     }
     Talk_update(t, &game->world, game->player, input->choice, leave, dt);
+    if (t->errand) { /* a shopping run agreed: off they go */
+        game_shopStart(game, t->errand, t->shop);
+        t->errand = NULL;
+    }
 }
diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c
index 2fa0744..f3a5215 100644
--- a/entropie/src/game/save.c
+++ b/entropie/src/game/save.c
@@ -44,7 +44,7 @@ static const Field THING[] = {
     FL("wake", wake), IN("stored", stored), FL("work", work), FL("cut_at", cut_at), FL("mark", mark), VE("mark_at", mark_at),
     VE("mark_out", mark_out), IN("branches", branches), BO("trunk_cut", trunk_cut), BO("toppled", toppled), FL("topple", topple),
     VE("fall_dir", fall_dir), FL("fall_base_y", fall_base_y), VE("fall_origin", fall_origin), FL("fall_ground_y", fall_ground_y),
-    FL("fall_slide", fall_slide), VE("fell_plan", fell_plan), BO("fell_blocked", fell_blocked), BO("keep", keep), FL("charred", charred), BO("umbrella_ruined", umbrella_ruined), IN("build_stage", build_stage), FL("build_work", build_work), VE("hat_tint", look.hat_color), IN("umbrella_flips", umbrella_flips),
+    FL("fall_slide", fall_slide), VE("fell_plan", fell_plan), BO("fell_blocked", fell_blocked), BO("keep", keep), FL("charred", charred), BO("umbrella_ruined", umbrella_ruined), IN("build_stage", build_stage), FL("build_work", build_work), IN("stock", stock), VE("hat_tint", look.hat_color), IN("umbrella_flips", umbrella_flips),
     FL("glow", glow), FL("fuel", fuel), BO("burning", burning), FL("water", water), FL("grill", grill), BO("grilled", grilled),
     BO("burnt", burnt), FL("extend", extend), FL("raised", raised), BO("leaning", leaning), VE("lean_normal", lean_normal),
     FL("lean_top", lean_top), FL("stability", stability), IN("crate_empties", crate_empties), IN("tent_stage", tent_stage),
@@ -183,8 +183,14 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err,
     const Arrival *a = &game->arrival;
     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, "guide %d\n", a->guide);
     fprintf(f, "told %d %d\n", game->rain_told, game->wet_told);
     fprintf(f, "story %.9g\n", (double)game->story_hours);
+    fprintf(f, "cash %.9g\n", (double)w->cash);
+    const Errand *e = &game->errand;
+    if (e->stage != ERRAND_NONE)
+        fprintf(f, "errand %d %d %d %d %d %d %.9g %.9g\n", e->stage, idOf(&ids, e->driver), e->list[0], e->list[1], e->list[2], e->list[3],
+                (double)e->time, (double)e->s);
 
     for (int i = 0; i < w->count; i++) {
         const Object *o = w->objects[i];
@@ -445,6 +451,14 @@ static void applyGame(Game *game, const Parsed *p) {
             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);
+        else if (!strcmp(key, "cash")) game->world.cash = numF(&s);
+        else if (!strcmp(key, "guide")) game->arrival.guide = numI(&s);
+        else if (!strcmp(key, "errand")) {
+            Errand *e = &game->errand;
+            e->stage = (ErrandStage)numI(&s); e->driver = byId(p, numI(&s));
+            for (int k = 0; k < SHOP_ITEMS; k++) e->list[k] = numI(&s);
+            e->time = numF(&s); e->s = numF(&s);
+        }
     }
 }
 
@@ -685,6 +699,7 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
         if (p.rec[k].obj && p.rec[k].obj->type == OBJ_CLOTHES) Clothes_rebuild(p.rec[k].obj);
     if (game->arrival.car) Car_place(w, game->arrival.car, game->arrival.car->position, game->arrival.car->rotation.y);
     applyGame(game, &p);
+    game_shopLoaded(game);
 
     /* The ground: worn paths, then the wet (it lies on the paths) */
     FILE *g = fopen(ground, "rb");
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 9ea861d..e712660 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -523,7 +523,7 @@ static Object *findSupply(const Object *self, Object *const *objects, int count,
     float best_dist = 1e9f;
     for (int i = 0; i < count; i++) {
         Object *o = objects[i];
-        if (o->type != OBJ_SUPPLY || o->dispenses != (want == WANT_FOOD ? OBJ_APPLE : OBJ_WATER)) continue;
+        if (o->type != OBJ_SUPPLY || o->dispenses != (want == WANT_FOOD ? OBJ_APPLE : OBJ_WATER) || o->stock < 0 || o->is_held) continue;
         float d = flatDistance(o->position, self->position);
         if (d < best_dist) {
             best_dist = d;
@@ -863,6 +863,8 @@ static void updateSupply(Object *self, Object *const *objects, int count, float
         self->needs.thirst = fmaxf(0.0f, self->needs.thirst - 0.6f);
         self->needs.bladder = fminf(1.0f, self->needs.bladder + 0.25f);
     } else {
+        if (supply->stock < 0) { ai->target = NULL; Ai_say(self, "The food's all gone. Somebody has to go shopping."); rest(ai, 2.0f, 4.0f); return; }
+        if (supply->stock > 0 && --supply->stock == 0) supply->stock = -1; /* (bought: it runs out) */
         self->needs.hunger = fmaxf(0.0f, self->needs.hunger - 0.5f);
     }
     self->needs.stamina = fminf(1.0f, self->needs.stamina + 0.2f);
@@ -1718,11 +1720,16 @@ static bool fetchPullover(Object *self, Object *const *objects, int count, float
 #define UNLOAD_TIRED 0.25f /* stamina: a heavy thing put down for a breather ... */
 #define UNLOAD_ON    0.55f /* ... and taken up again */
 
+/* Something that comes out of the car's boot. */
+static bool isCargo(const Object *o) {
+    return o->type == OBJ_CRATE || o->type == OBJ_GEAR || o->type == OBJ_MATERIAL || (o->type == OBJ_SUPPLY && !o->is_static);
+}
+
 static Object *carriedCargo(const Object *self) {
     for (int h = 0; h < 2; h++) {
         Object *it = self->hands.item[h];
         if (it && (self->hands.state[h] == HAND_HOLDING || self->hands.state[h] == HAND_REACHING) &&
-            (it->type == OBJ_CRATE || it->type == OBJ_GEAR)) return it;
+            isCargo(it)) return it;
     }
     return NULL;
 }
@@ -1747,7 +1754,7 @@ static bool unloadCar(Object *self, Object *const *objects, int count, float dt)
     if (!ai->unload || !car) { ai->unload = false; return false; }
     Object *item = carriedCargo(self);
     bool empty = !car->load || car->load->n == 0;
-    bool waiting = ai->target && (ai->target->type == OBJ_CRATE || ai->target->type == OBJ_GEAR) && !ai->target->is_held;
+    bool waiting = ai->target && isCargo(ai->target) && !ai->target->is_held;
     if (!item && !waiting && empty) {
         ai->unload = false;
         if (random01(ai) < 0.5f) Ai_say(self, "That's everything out of the car.");
@@ -2265,7 +2272,7 @@ static bool shelter(Object *self, Object *const *objects, int count, float dt) {
         if (random01(ai) < 0.6f) Ai_say(self, random01(ai) < 0.5f ? "It's pouring - inside, quick!" : self->lives_here ? "Rain! Into the house." : "Rain! I'm off under a roof.");
         for (int h = 0; h < 2; h++) { /* (crates and gear down where they are; tools along) */
             const Object *it = self->hands.item[h];
-            if (it && (it->type == OBJ_CRATE || it->type == OBJ_GEAR)) Hands_drop(self, h);
+            if (it && (isCargo(it) || it == ai->fetch)) Hands_drop(self, h);
         }
     }
     if (self->sitting && !ai->rain_seated) self->sitting = false;
@@ -2577,6 +2584,8 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
         shelter(self, objects, count, dt)) return;
     /* Wet through: by the fire to dry */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && dryOff(self, objects, count, dt)) return;
+    /* A shopping run: over to the car */
+    if (ai_errand(self, objects, count, dt)) return;
     /* Unloading the car */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && unloadCar(self, objects, count, dt)) return;
     /* Just arrived: hello at the fire */
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index def3ec6..8424455 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -197,7 +197,14 @@ typedef struct {
     bool           on_project;  /* at work on their floor's stage (ai_project.c) ... */
     float          shift;       /* ... game s into this go at it (then a break) ... */
     float          project_off; /* ... game s till they go back to it ... */
-    float          held_up;     /* ... game s it's been held up (logs missing) ... */
+    float          held_up;     /* ... game s it's been held up (material missing) ... */
+    struct Object *fetch;       /* ... material they're bringing over to it (this long so far) */
+    float          fetch_time;
+    bool           errand;      /* off somewhere on purpose: errand_to (a shopping run: the car's driver door;
+                                   showing the way: the spot) ... */
+    bool           errand_there; /* ... and there */
+    Vec3           errand_to;
+    struct Object *errand_with; /* ... showing whoever this is the way: waiting for them when they fall behind */
     float          remark;      /* ... real s till the next remark about it */
     float          praised;     /* game s till a "great work" counts again */
     struct Object *share;       /* rain: walking under this one's umbrella, at their side ... */
diff --git a/entropie/src/people/ai_internal.h b/entropie/src/people/ai_internal.h
index 9309513..47c12f7 100644
--- a/entropie/src/people/ai_internal.h
+++ b/entropie/src/people/ai_internal.h
@@ -106,6 +106,7 @@ bool ai_goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int
    (quitting a lead, or helping at all, when it's gone); and the work on their floor's stage. */
 void ai_commitment(Object *self, Object *const *objects, int count, float dt);
 bool ai_projectWork(Object *self, Object *const *objects, int count, float dt);
+bool ai_errand(Object *self, Object *const *objects, int count, float dt);
 
 static inline float random01(Ai *ai) {
     ai->rng = ai->rng * 1664525u + 1013904223u;
diff --git a/entropie/src/people/ai_project.c b/entropie/src/people/ai_project.c
index 955ff9e..1066a56 100644
--- a/entropie/src/people/ai_project.c
+++ b/entropie/src/people/ai_project.c
@@ -3,6 +3,9 @@
  *
  * The lead and the helpers of a floor go to its stage in working hours and
  * build it, in goes of a couple of hours (then a break, eating, the loo...).
+ * Held up for bought material that lies somewhere else (by the barn, where the
+ * shopping gets unloaded), one of them goes and carries it over; logs are
+ * yours to bring. Off on a shopping run (game_shop.c): over to the car.
  * How into it they are goes with what happens: a step finished lifts them;
  * being held up (no logs for the frame), working on worn out, nobody leading,
  * being annoyed wear it down. Too low, a lead gives up leading ("somebody
@@ -35,6 +38,8 @@
 #define WEAR_NO_LEAD   0.03f  /* a helper with nobody leading it */
 #define WEAR_ANNOYED   0.04f
 #define LIFT_EVENING   0.02f  /* sitting at the fire of an evening */
+#define FETCH_SPAN     150.0f /* m: material lying this far off is fetched for the stage */
+#define FETCH_GIVE_UP  240.0f /* real s at fetching one thing: given up */
 
 static bool floorProject(int p) {
     return p == PROJECT_HOFBUEHNE || p == PROJECT_OBSTWIESE || p == PROJECT_KARTOFFELACKER || p == PROJECT_HIDDEN;
@@ -100,12 +105,110 @@ static bool stop(Object *self, const char *say) {
     Ai *ai = &self->ai;
     if (ai->on_project && say && self->ai.speech_time <= 0.0f) Ai_say(self, say);
     if (ai->on_project) ai->project_off = BREAK_TIME; /* (a break before the next go: only after one) */
+    if (ai->fetch) { /* (whatever they were bringing: down where they are) */
+        for (int h = 0; h < 2; h++) if (self->hands.item[h] == ai->fetch) Hands_drop(self, h);
+        ai->fetch = NULL;
+    }
     ai->on_project = false;
     ai->shift = 0.0f;
     ai->held_up = 0.0f;
     return false;
 }
 
+/* Others at work on it with them. */
+static bool othersAt(const Object *self, Object *const *objects, int count, int project) {
+    for (int i = 0; i < count; i++)
+        if (objects[i] != self && objects[i]->is_character && objects[i]->ai.on_project && theirFloor(objects[i]) == project) return true;
+    return false;
+}
+
+/* Lying by a stage (it's for that one). */
+static bool atAStage(const Object *o, Object *const *objects, int count) {
+    for (int i = 0; i < count; i++)
+        if (objects[i]->type == OBJ_STAGE && flatDistance(objects[i]->position, o->position) < STAGE_LOG_REACH) return true;
+    return false;
+}
+
+static bool fetchedByOther(const Object *self, const Object *it, Object *const *objects, int count) {
+    for (int i = 0; i < count; i++)
+        if (objects[i] != self && objects[i]->is_character && objects[i]->ai.fetch == it) return true;
+    return false;
+}
+
+/* The nearest of that material lying about (not at a stage, nobody bringing it), or NULL. */
+static Object *materialFor(const Object *self, Object *const *objects, int count, int kind) {
+    Object *best = NULL;
+    float best_d = FETCH_SPAN;
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o->type != OBJ_MATERIAL || o->variant != kind || o->is_held || o->broken) continue;
+        float d = flatDistance(o->position, self->position);
+        if (d < best_d && !atAStage(o, objects, count) && !fetchedByOther(self, o, objects, count)) { best_d = d; best = o; }
+    }
+    return best;
+}
+
+static bool holds(const Object *self, const Object *it) {
+    for (int h = 0; h < 2; h++)
+        if (self->hands.item[h] == it && (self->hands.state[h] == HAND_HOLDING || self->hands.state[h] == HAND_REACHING)) return true;
+    return false;
+}
+
+/* Bringing material over to the stage: true while at it. */
+static bool fetchMaterial(Object *self, Object *stage, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    Object *it = ai->fetch;
+    if (!it) return false;
+    bool mine = holds(self, it);
+    if (it->broken || (it->is_held && !mine) || (ai->fetch_time += dt) > FETCH_GIVE_UP) {
+        for (int h = 0; h < 2; h++) if (mine && self->hands.item[h] == it) Hands_drop(self, h);
+        ai->fetch = NULL;
+        return false;
+    }
+    if (mine) {
+        for (int h = 0; h < 2; h++) if (self->hands.item[h] == it && self->hands.state[h] == HAND_REACHING) { stand(self); return true; }
+        Vec3 front = yaw_front(stage->rotation.y);
+        Vec3 drop = vec3_add(Stage_workSpot(stage, ai->side < 0.0f ? -0.3f : 0.3f), vec3_scale(front, 1.3f)); /* (out in front, behind those at work) */
+        if (!ai_goToPoint(self, drop, 0.5f, objects, count, dt)) return true;
+        for (int h = 0; h < 2; h++) if (self->hands.item[h] == it) Hands_drop(self, h);
+        ai->fetch = NULL;
+        if (ai->speech_time <= 0.0f) Ai_say(self, it->variant == MATERIAL_BOARDS ? "Boards are here!" : it->variant == MATERIAL_PAINT ? "Paint's here!" : "Got it - here.");
+        return true;
+    }
+    if (Hands_gapFrom(self->position, self->look.height, it) < PICKUP_REACH) {
+        stand(self);
+        turnTowards(self, vec3_sub(it->position, self->position), dt);
+        freeHands(self, NULL);
+        if (!Hands_pickUpObject(self, self->main_hand, it) && !Hands_pickUpObject(self, 1 - self->main_hand, it)) ai->fetch = NULL;
+        return true;
+    }
+    ai_goToPoint(self, it->position, 0.3f, objects, count, dt);
+    return true;
+}
+
+/* Off somewhere on purpose: a shopping run (over to the car, the driver's door: game_shop.c gets
+   them in), showing somebody the way (waiting for them when they fall behind: game_arrival.c). */
+#define GUIDE_WAIT 6.0f /* m: the one shown the way further behind than this: wait for them */
+bool ai_errand(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    if (!ai->errand) return false;
+    if (self->sitting) stand(self);
+    self->indoors = 0.0f;
+    for (int h = 0; h < 2; h++) if (self->hands.item[h] && self->hands.state[h] == HAND_HOLDING && !ai->errand_with) Hands_drop(self, h);
+    const Object *w = ai->errand_with;
+    if (w && !ai->errand_there && flatDistance(w->position, self->position) > GUIDE_WAIT) { /* (they're coming? waiting, looking back) */
+        stand(self);
+        turnTowards(self, vec3_sub(w->position, self->position), dt);
+        return true;
+    }
+    ai->errand_there = ai_goToPoint(self, ai->errand_to, w ? 1.0f : 0.6f, objects, count, dt);
+    if (ai->errand_there) {
+        stand(self);
+        if (w) turnTowards(self, vec3_sub(w->position, self->position), dt);
+    }
+    return true;
+}
+
 static const char *doneLine(int project, int step) {
     static const char *const ANY[] = { "", "Ground's clear - now the frame.", "The frame stands!", "Deck's in - try it, it holds.",
                                        "Roof's on. Now make it pretty.", "Done! Look at it!" };
@@ -131,7 +234,7 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) {
     if (hour < WORK_FROM || hour > WORK_TO) return stop(self, lead ? "That's it for today." : NULL);
     if (self->needs.stamina < TIRED) return stop(self, "I need a break.");
     if (self->needs.hunger > NEED_STOP || self->needs.thirst > NEED_STOP || self->needs.bladder > NEED_STOP) return stop(self, NULL);
-    if (self->drunk > 0.5f || self->body.asleep || self->in_tent || self->riding || self->indoors > 0.0f || ai->sheltering) return stop(self, NULL);
+    if (self->drunk > 0.5f || self->body.asleep || self->in_tent || self->riding || self->indoors > 0.0f || ai->sheltering || ai->errand) return stop(self, NULL);
     if (!ai->on_project) {
         if (ai->project_off > 0.0f || ai->greet || ai->unload) return false;
         float chance = START_CHANCE * (lead || boss_near ? 1.0f : 0.25f) * (0.5f + self->commitment);
@@ -146,6 +249,7 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) {
     }
     if ((ai->shift += gdt) > SHIFT) return stop(self, lead ? "Break! Back in a bit." : NULL);
     if (self->sitting) stand(self);
+    if (fetchMaterial(self, stage, objects, count, dt)) return true;
     freeHands(self, NULL);
     float side = ai->side != 0.0f ? ai->side * 0.8f : (lead ? 0.0f : 0.6f);
     Vec3 spot = Stage_workSpot(stage, side);
@@ -157,16 +261,30 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) {
     float handy = 0.5f * (self->skill[ORDER_FIREWOOD] + self->skill[ORDER_TREES]);
     float rate = (0.6f + 0.8f * handy) * self->tempo * (lead ? 1.1f : boss_near ? 1.0f : 0.6f) * (1.0f - 0.5f * fminf(self->drunk, 1.0f));
     int got = Stage_work(g_world, stage, rate * gdt / 3600.0f);
-    if (got == -2) { /* held up: logs missing for the frame */
+    if (got == -2) { /* held up: material missing */
+        int kind = Stage_missingMaterial(g_world, stage);
+        Object *there = kind >= 0 ? materialFor(self, objects, count, kind) : NULL;
+        if (there && (!lead || !othersAt(self, objects, count, project))) { /* (lying somewhere else: fetched) */
+            ai->fetch = there;
+            ai->fetch_time = 0.0f;
+            if (ai->speech_time <= 0.0f) Ai_say(self, kind == MATERIAL_BOARDS ? "I'll get the boards." : "I'll fetch it.");
+            return true;
+        }
+        if (kind >= 0) { /* (somebody's on the way with it: wait for it) */
+            for (int i = 0; i < count; i++)
+                if (objects[i]->is_character && objects[i]->ai.fetch && objects[i]->ai.fetch->variant == kind && theirFloor(objects[i]) == project) return true;
+        }
         ai->held_up += gdt;
         if (ai->remark <= 0.0f && firstAt(self, objects, count, project)) {
-            char line[AI_SPEECH_MAX];
-            snprintf(line, sizeof line, "We need %d more log%s here for the frame!", Stage_logsMissing(g_world, stage),
-                     Stage_logsMissing(g_world, stage) == 1 ? "" : "s");
+            char line[AI_SPEECH_MAX], what[40];
+            Stage_missing(g_world, stage, what, sizeof what);
+            if (stage->build_stage == BUILD_FRAME) snprintf(line, sizeof line, "We need %s here for the frame!", what);
+            else snprintf(line, sizeof line, "We need %s - somebody go shopping!", what);
             Ai_say(self, line);
             ai->remark = REMARK_EVERY;
         }
-        if (ai->held_up > HELD_UP_LEAVE) return stop(self, "No logs, no frame. I'm off till there are some.");
+        if (ai->held_up > HELD_UP_LEAVE)
+            return stop(self, stage->build_stage == BUILD_FRAME ? "No logs, no frame. I'm off till there are some." : "Nothing to build with. I'm off till the shopping's here.");
         return true;
     }
     ai->held_up = 0.0f;
diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c
index 935dfec..c85f787 100644
--- a/entropie/src/people/talk.c
+++ b/entropie/src/people/talk.c
@@ -4,6 +4,7 @@
 #include "people/pockets.h"
 #include "world/site.h"
 #include "world/stage.h"
+#include "world/car.h"
 
 #include <stdio.h>
 #include <string.h>
@@ -31,6 +32,7 @@ enum {
     NODE_ASK_HAND, /* a worker asks the player to join in */
     NODE_TOOL,     /* a worker asks for the player's axe */
     NODE_FESTIVAL, /* about the festival: their part in it, who leads what */
+    NODE_SHOP,     /* the one with the car keys: what to get at the shops */
     NODE_COUNT
 };
 
@@ -51,10 +53,12 @@ static const char *const CHOICES[NODE_COUNT][TALK_CHOICES] = {
     [NODE_WATCHING] = { "Watch closely", "Never mind" },
     [NODE_ASK_HAND] = { "Sure", "Not now" },
     [NODE_TOOL]     = { "Here you go", "No, I still need it" },
-    [NODE_FESTIVAL] = { "Great work - thanks!", "(who leads what)", "(you take it over)", "Never mind", "Bye" }, /* (2 and 3: see Talk_choices) */
+    [NODE_FESTIVAL] = { "Great work - thanks!", "(who leads what)", "(you take it over)", "We need some shopping done", "Bye" }, /* (2 and 3: see Talk_choices) */
+    [NODE_SHOP]     = { "(boards)", "(nails, screws)", "(paint)", "(food)", "(go)" }, /* (see Talk_choices) */
 };
 
 static const Object *g_talk_player; /* (for the choices: what's between them) */
+static const Talk *g_talk;          /* (for what they say: the shopping list) */
 static World *g_talk_world;         /* (for the choices: who leads what) */
 
 static const Order TASKS[] = { ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE };
@@ -99,6 +103,29 @@ static const char *festivalLine(const Object *o) {
     return line;
 }
 
+static int shopTotal(const Talk *t) {
+    int sum = 0;
+    for (int k = 0; k < SHOP_ITEMS; k++) sum += t->shop[k] * SHOP_PRICE[k];
+    return sum;
+}
+
+/* Who has the car keys (here), or NULL. */
+static Object *keyHolder(void) {
+    for (int i = 0; g_talk_world && i < g_talk_world->count; i++) {
+        Object *o = g_talk_world->objects[i];
+        if (o->is_character && o->keys && !strcmp(o->keys, "Car keys")) return o;
+    }
+    return NULL;
+}
+
+static const char *shopLine(void) {
+    static char line[AI_SPEECH_MAX];
+    int cash = g_talk_world ? (int)g_talk_world->cash : 0, total = g_talk ? shopTotal(g_talk) : 0;
+    if (total == 0) snprintf(line, sizeof line, "Sure. What do we need? There's %d EUR in the cash box.", cash);
+    else snprintf(line, sizeof line, "So far that's %d EUR - %d in the box. Anything else?", total, cash);
+    return line;
+}
+
 /* What they say when the conversation gets to a node. */
 static const char *lineFor(const Object *o, int node) {
     switch (node) {
@@ -128,6 +155,7 @@ static const char *lineFor(const Object *o, int node) {
     case NODE_ASK_HAND: return "Want to give me a hand with the wood?";
     case NODE_TOOL:    return shy(o) ? "Um... could I maybe use the axe?" : "Can I have the axe? I'd like to try myself.";
     case NODE_FESTIVAL: return festivalLine(o);
+    case NODE_SHOP:    return shopLine();
     default:           return "";
     }
 }
@@ -139,6 +167,7 @@ static Order pausedJob(const Object *npc) {
 }
 
 static void go(Talk *talk, int node) {
+    g_talk = talk;
     talk->node = node;
     talk->waited = 0.0f;
     talk->nudged = false;
@@ -384,8 +413,40 @@ static void festivalAnswer(Talk *talk, Object *player, int choice) {
             snprintf(reply, sizeof reply, "Sure, I'll help with the %s.", Cast_projectName((Project)player->leads));
             wrapUp(talk, reply);
         } else wrapUp(talk, "Lead a floor yourself first - then I'll help you with it.");
-    } else if (choice == 4) go(talk, NODE_HELLO);
-    else wrapUp(talk, "See you.");
+    } else if (choice == 4) { /* the shopping: whoever has the car keys */
+        Object *keys = keyHolder();
+        Object *car = NULL;
+        for (int i = 0; g_talk_world && i < g_talk_world->count; i++) if (g_talk_world->objects[i]->type == OBJ_CAR) car = g_talk_world->objects[i];
+        if (keys != npc) {
+            if (keys) snprintf(reply, sizeof reply, "%s has the car keys - ask %s.", keys->name, keys->female ? "her" : "him");
+            else snprintf(reply, sizeof reply, "Nobody here has a car right now.");
+            wrapUp(talk, reply);
+        } else if (!car || (g_talk_world && g_talk_world->shopping)) wrapUp(talk, "The car's not here - wait till it's back.");
+        else if (car->load && car->load->n > 0) wrapUp(talk, "Let's get the car empty first.");
+        else if (npc->drunk > 0.3f) wrapUp(talk, "I've had a few - I'm not driving now.");
+        else if (npc->commitment < 0.2f || npc->needs.stamina < 0.15f) wrapUp(talk, "Not today. I'm done in.");
+        else {
+            memset(talk->shop, 0, sizeof talk->shop);
+            go(talk, NODE_SHOP);
+        }
+    } else wrapUp(talk, "See you.");
+}
+
+static void shopAnswer(Talk *talk, int choice) {
+    if (choice >= 1 && choice <= SHOP_ITEMS) { /* one more of it (past the most: none) */
+        int k = choice - 1;
+        talk->shop[k] = (talk->shop[k] + 1) % (SHOP_MAX[k] + 1);
+        if (g_talk_world && shopTotal(talk) > (int)g_talk_world->cash) {
+            talk->shop[k] = 0;
+            Ai_say(talk->with, "That's more than we've got in the box.");
+            talk->node = NODE_SHOP;
+            return;
+        }
+        go(talk, NODE_SHOP);
+    } else if (shopTotal(talk) > 0) {
+        talk->errand = talk->with;
+        wrapUp(talk, "Right - I'm off to the hardware store. Back in an hour or so.");
+    } else wrapUp(talk, "Okay - another time then.");
 }
 
 static void answer(Talk *talk, World *world, Object *player, int choice) {
@@ -475,6 +536,7 @@ static void answer(Talk *talk, World *world, Object *player, int choice) {
         wrapUp(talk, choice == 1 ? "Great! I'll cut, you split." : "No problem.");
         break;
     case NODE_FESTIVAL: festivalAnswer(talk, player, choice); break;
+    case NODE_SHOP:     shopAnswer(talk, choice); break;
     case NODE_TOOL:
         if (choice == 1 && handOver(player, npc, JOB_SPLIT)) wrapUp(talk, "Thanks! I'll be careful.");
         else { npc->ai.tool_waiting = true; wrapUp(talk, "Okay, I'll wait till you're done."); }
@@ -613,6 +675,19 @@ int Talk_choices(const Talk *talk, const char *out[TALK_CHOICES]) {
         out[1] = lead_line;
         out[2] = take_line;
     }
+    if (talk->node == NODE_SHOP) {
+        static char lines[TALK_CHOICES][64];
+        static const char *const WHAT[SHOP_ITEMS] = { "Boards, a bundle of 8", "Nails and screws, a box each", "Paint, a bucket", "Food and drinks for the crew" };
+        for (int k = 0; k < SHOP_ITEMS; k++) {
+            if (talk->shop[k]) snprintf(lines[k], sizeof lines[k], "%s - %d EUR   [%d]", WHAT[k], SHOP_PRICE[k], talk->shop[k]);
+            else snprintf(lines[k], sizeof lines[k], "%s - %d EUR", WHAT[k], SHOP_PRICE[k]);
+            out[k] = lines[k];
+        }
+        if (shopTotal(talk) > 0) snprintf(lines[4], sizeof lines[4], "That's it - off you go (%d EUR)", shopTotal(talk));
+        else snprintf(lines[4], sizeof lines[4], "Never mind");
+        out[4] = lines[4];
+        n = TALK_CHOICES;
+    }
     if (talk->node == NODE_WORKER && woodJob(talk->with->ai.order) && n < TALK_CHOICES) { /* about the tool they use */
         static char line[64];
         const Object *npc = talk->with;
diff --git a/entropie/src/people/talk.h b/entropie/src/people/talk.h
index 9b21dd3..c5653d5 100644
--- a/entropie/src/people/talk.h
+++ b/entropie/src/people/talk.h
@@ -22,6 +22,12 @@
 #define TALK_CHOICES 5
 #define TALK_PILE_AHEAD 1.3f  /* a new woodpile goes this far in front of the player */
 
+/* The shopping list: asked of whoever has the car keys (talk.c), then the run (game/game_shop.c). */
+#define SHOP_ITEMS 4
+enum { SHOP_BOARDS, SHOP_FIXINGS, SHOP_PAINT, SHOP_FOOD };
+static const int SHOP_PRICE[SHOP_ITEMS] = { 25, 20, 35, 90 }; /* € each: a bundle of 8 boards; a box of nails and one of screws; a bucket of paint; food and drink for the crew */
+static const int SHOP_MAX[SHOP_ITEMS]   = { 8, 4, 4, 2 };
+
 typedef struct {
     Object     *with;          /* NULL = no conversation */
     int         node;          /* where in the conversation (talk.c) */
@@ -30,6 +36,8 @@ typedef struct {
     float       player_time;   /* the player's answer shows this long */
     float       waited;        /* seconds they've been waiting for an answer ... */
     bool        nudged;        /* ... and asked again once */
+    int         shop[SHOP_ITEMS]; /* the shopping list being made (how many of each) ... */
+    Object     *errand;        /* ... agreed: they're off with it (the game takes it from here and clears this) */
 } Talk;
 
 /* Starts talking with someone: what they say first depends on what they're
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index 1b3f9a7..ab9ed0a 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -165,6 +165,13 @@ Mesh  Shapes_hat(int kind, Vec3 color);
 /* Camping things (centred, lying along X): kind 0 a sleeping bag in its sack, 1 a foam mat rolled up,
    2 a small tent in its bag, 3 a big one. */
 Mesh  Shapes_gear(int kind, Vec3 color);
+/* The marker over who or what to go to next: a diamond, centred, this tall. */
+Mesh  Shapes_questMarker(float height, Vec3 color);
+/* A party pavilion on y = 0 (centred): side x side, the eaves and the roof's peak this high, its canvas colour. */
+Mesh  Shapes_pavilion(float side, float eaves, float peak, Vec3 canvas);
+/* Building material (centred, size = its box): kind 0 a strapped bundle of 8 boards (long along X),
+   1 a box of nails, 2 a box of screws, 3 a bucket of paint (size.x across). */
+Mesh  Shapes_material(int kind, Vec3 size);
 /* A dome tent standing on y = 0, the door to +Z: big 0 small (two people
    squeezed), 1 the big one. How far it's up: stage 0 laid out flat, 1 the
    poles going in and up (progress 0..1), 2 the cover going over it (from the
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index eceb39e..4c01882 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -1213,6 +1213,85 @@ Mesh Shapes_gear(int kind, Vec3 color) {
     return Mesh_fromBuilder(&b);
 }
 
+/* The marker over who to go to next: a diamond (two pyramids), centred, this tall. */
+Mesh Shapes_questMarker(float height, Vec3 color) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const float r = height * 0.32f, h = height * 0.5f;
+    for (int e = 0; e < 4; e++)
+        for (int up = 0; up < 2; up++) {
+            float a0 = (float)e * PI_F * 0.5f, a1 = a0 + PI_F * 0.5f;
+            Vec3 p0 = vec3(r * cosf(a0), 0.0f, r * sinf(a0)), p1 = vec3(r * cosf(a1), 0.0f, r * sinf(a1)), tip = vec3(0.0f, up ? h : -h, 0.0f);
+            Vec3 n = vec3_normalize(vec3_cross(vec3_sub(p1, p0), vec3_sub(tip, p0)));
+            Vec3 mid = vec3_scale(vec3_add(vec3_add(p0, p1), tip), 1.0f / 3.0f);
+            if (vec3_dot(n, mid) < 0.0f) n = vec3_scale(n, -1.0f);
+            Vec3 c = vec3_scale(color, up ? 1.0f : 0.8f);
+            MeshBuilder_triangle(&b, MeshBuilder_vertex(&b, p0, n, c), MeshBuilder_vertex(&b, p1, n, c), MeshBuilder_vertex(&b, tip, n, c));
+        }
+    return Mesh_fromBuilder(&b);
+}
+
+/* A party pavilion standing on y = 0 (centred): four thin legs, a valance round the top,
+   a pyramid roof of white canvas (both sides drawn: it's seen from under it too). */
+Mesh Shapes_pavilion(float side, float eaves, float peak, Vec3 canvas) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const float h = side * 0.5f, leg = 0.045f, skirt = 0.24f;
+    const Vec3 steel = { 0.72f, 0.73f, 0.75f }, under = vec3_scale(canvas, 0.8f);
+    for (int i = -1; i <= 1; i += 2)
+        for (int k = -1; k <= 1; k += 2) {
+            addBlock(&b, vec3((float)i * (h - leg), eaves * 0.5f, (float)k * (h - leg)), vec3(leg, eaves, leg), steel);
+            addBlock(&b, vec3((float)i * (h - leg), 0.01f, (float)k * (h - leg)), vec3(0.16f, 0.02f, 0.16f), steel); /* (foot plates) */
+        }
+    for (int s = -1; s <= 1; s += 2) { /* the valance: four strips of canvas hanging from the eaves */
+        addBlock(&b, vec3(0.0f, eaves - skirt * 0.5f, (float)s * h), vec3(side, skirt, 0.01f), canvas);
+        addBlock(&b, vec3((float)s * h, eaves - skirt * 0.5f, 0.0f), vec3(0.01f, skirt, side), canvas);
+    }
+    const Vec3 top = vec3(0.0f, peak, 0.0f);
+    const float cx[4] = { -h, h, h, -h }, cz[4] = { -h, -h, h, h };
+    for (int e = 0; e < 4; e++) { /* each face: outside up, the inside a shade darker */
+        Vec3 a = vec3(cx[e], eaves, cz[e]), c = vec3(cx[(e + 1) % 4], eaves, cz[(e + 1) % 4]);
+        Vec3 n = vec3_normalize(vec3_cross(vec3_sub(c, a), vec3_sub(top, a)));
+        if (n.y < 0.0f) n = vec3_scale(n, -1.0f);
+        GLuint i0 = MeshBuilder_vertex(&b, a, n, canvas), i1 = MeshBuilder_vertex(&b, c, n, canvas), i2 = MeshBuilder_vertex(&b, top, n, canvas);
+        MeshBuilder_triangle(&b, i0, i1, i2);
+        Vec3 m = vec3_scale(n, -1.0f), down = vec3(0.0f, -0.01f, 0.0f);
+        GLuint j0 = MeshBuilder_vertex(&b, vec3_add(a, down), m, under), j1 = MeshBuilder_vertex(&b, vec3_add(c, down), m, under);
+        GLuint j2 = MeshBuilder_vertex(&b, vec3_add(top, down), m, under);
+        MeshBuilder_triangle(&b, j0, j1, j2);
+    }
+    return Mesh_fromBuilder(&b);
+}
+
+/* Building material (centred): a bundle of deck boards strapped together,
+   a box of nails, a box of screws, a bucket of paint. */
+Mesh Shapes_material(int kind, Vec3 size) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const Vec3 card = { 0.66f, 0.52f, 0.34f }, strap = { 0.12f, 0.12f, 0.14f };
+    if (kind == 0) { /* 2 x 4 boards, a hair apart, the grain a shade different each */
+        float w = size.z / 2.0f, h = size.y / 4.0f;
+        for (int k = 0; k < 8; k++) {
+            float shade = 0.92f + 0.05f * (float)((k * 5) % 3);
+            Vec3 c = vec3(0.80f * shade, 0.64f * shade, 0.42f * shade);
+            addBlock(&b, vec3(0.0f, -size.y * 0.5f + h * ((float)(k / 2) + 0.5f), -size.z * 0.5f + w * ((float)(k % 2) + 0.5f)),
+                     vec3(size.x, h - 0.004f, w - 0.004f), c);
+        }
+        for (int k = -1; k <= 1; k += 2) addBlock(&b, vec3((float)k * size.x * 0.3f, 0.0f, 0.0f), vec3(0.025f, size.y + 0.006f, size.z + 0.006f), strap);
+    } else if (kind == 1 || kind == 2) { /* a cardboard box, its label: grey nails, a blue box of screws */
+        Vec3 label = kind == 1 ? vec3(0.55f, 0.56f, 0.58f) : vec3(0.18f, 0.36f, 0.62f);
+        addBlock(&b, vec3_zero(), size, kind == 1 ? card : vec3(0.85f, 0.82f, 0.74f));
+        addBlock(&b, vec3(0.0f, 0.0f, size.z * 0.5f + 0.002f), vec3(size.x * 0.8f, size.y * 0.6f, 0.004f), label);
+        addBlock(&b, vec3(0.0f, size.y * 0.5f + 0.002f, 0.0f), vec3(size.x * 0.5f, 0.004f, size.z * 0.5f), label);
+    } else { /* a white bucket, its lid, the handle */
+        float r = size.x * 0.5f;
+        shapes_addTube(&b, -size.y * 0.5f, r * 0.88f, r, size.y * 0.92f, 16, vec3(0.92f, 0.92f, 0.9f), true);
+        shapes_addTube(&b, size.y * 0.42f, r * 1.02f, r * 1.02f, size.y * 0.08f, 16, vec3(0.85f, 0.35f, 0.12f), true);
+        addBlock(&b, vec3(0.0f, size.y * 0.5f + 0.012f, 0.0f), vec3(size.x * 0.9f, 0.012f, 0.012f), strap);
+    }
+    return Mesh_fromBuilder(&b);
+}
+
 /* An old tin can as ashtray: the butts stick out of the ash, more and more,
    a little heap of them when it's full. */
 static void buildAshtray(MeshBuilder *b, Vec3 size, int butts) {
diff --git a/entropie/src/world/car.c b/entropie/src/world/car.c
index 6bd1d90..adc4a77 100644
--- a/entropie/src/world/car.c
+++ b/entropie/src/world/car.c
@@ -45,6 +45,8 @@ static const char *const CARGO_NAME[] = {
     [CARGO_BEER] = "a crate of beer", [CARGO_WATER] = "a crate of water", [CARGO_SLEEPING_BAG] = "a sleeping bag",
     [CARGO_MAT] = "a sleeping mat", [CARGO_TENT_SMALL] = "a tent", [CARGO_TENT_BIG] = "the big tent",
     [CARGO_SODA] = "a crate of soda", [CARGO_MATE] = "a crate of mate",
+    [CARGO_BOARDS] = "a bundle of boards", [CARGO_NAILS] = "a box of nails", [CARGO_SCREWS] = "a box of screws",
+    [CARGO_PAINT] = "a bucket of paint", [CARGO_FOOD] = "a crate of food", [CARGO_SAUSAGES] = "a cool box of sausages",
 };
 
 int Crate_bottles(int variant) { return variant == 1 || variant == 2 ? 12 : 20; }
@@ -64,7 +66,8 @@ const char *Car_nextName(const Object *car) {
 
 bool Car_nextHeavy(const Object *car) {
     const CarLoad *l = car->load;
-    return l && l->n > 0 && (isCrate(l->kind[l->n - 1]) || l->kind[l->n - 1] == CARGO_TENT_BIG);
+    CargoKind k = l && l->n > 0 ? l->kind[l->n - 1] : CARGO_MAT;
+    return l && l->n > 0 && (isCrate(k) || k == CARGO_TENT_BIG || k == CARGO_BOARDS || k == CARGO_FOOD || k == CARGO_SAUSAGES);
 }
 
 /* Everybody's own colours for their things (by name: you, Volkram, Jana, Toni, anybody else). */
@@ -115,6 +118,13 @@ Object *Car_takeOut(World *world, Object *car) {
     Object *item;
     if (isCrate(kind)) {
         item = Object_create(OBJ_CRATE, PALETTE_STANDARD, at, vec3_zero(), (float)crateOf(kind));
+    } else if (kind >= CARGO_BOARDS && kind <= CARGO_PAINT) {
+        item = Object_create(OBJ_MATERIAL, PALETTE_STANDARD, at, vec3_zero(), (float)(kind - CARGO_BOARDS));
+    } else if (kind == CARGO_FOOD || kind == CARGO_SAUSAGES) { /* (bought: carried in, it runs out) */
+        bool food = kind == CARGO_FOOD;
+        item = Object_create(OBJ_SUPPLY, food ? PALETTE_WARM : PALETTE_STANDARD, at, food ? vec3(0.5f, 0.32f, 0.36f) : vec3(0.5f, 0.34f, 0.32f),
+                             food ? -13.0f : -9.0f);
+        if (item) item->stock = food ? FOOD_PORTIONS : SAUSAGE_PORTIONS;
     } else {
         GearKind g = kind == CARGO_SLEEPING_BAG ? GEAR_SLEEPING_BAG : kind == CARGO_MAT ? GEAR_MAT
                    : kind == CARGO_TENT_SMALL ? GEAR_TENT_SMALL : GEAR_TENT_BIG;
@@ -205,9 +215,29 @@ static int campOf(const Object *owner) {
     return w == 1 || w == 2 ? 1 : w == 3 ? 2 : 0;
 }
 
+/* How many of that type lie by a spot already (not this one, not carried). */
+static int lyingAt(const World *world, const Object *item, Vec3 base, float r) {
+    int n = 0;
+    for (int i = 0; i < world->count; i++) {
+        const Object *o = world->objects[i];
+        if (o != item && o->type == item->type && !o->is_held && !o->broken && hypotf(o->position.x - base.x, o->position.z - base.z) < r) n++;
+    }
+    return n;
+}
+
 Vec3 Car_dropSpot(const World *world, const Object *item) {
+    Vec3 along = Site_campAlong(), across = Car_campDoor();
+    if (item->type == OBJ_MATERIAL) { /* the store: rows in front of the car room, out from its wall */
+        Vec3 base = Site_materialSpot();
+        int n = lyingAt(world, item, base, 4.5f);
+        return vec3_add(base, vec3_add(vec3_scale(along, 0.6f * (float)(n % 6) - 1.5f), vec3_scale(across, -1.3f * (float)(n / 6 % 3))));
+    }
+    if (item->type == OBJ_SUPPLY) { /* the food: into the kitchen, beside the drinks */
+        Vec3 base = vec3_sub(Site_drinksSpot(), vec3_scale(along, 1.4f));
+        return vec3_sub(base, vec3_scale(along, 0.6f * (float)(lyingAt(world, item, base, 2.5f) % 4)));
+    }
     if (item->type == OBJ_CRATE) {
-        Vec3 base = Site_drinksSpot(), along = Site_campAlong();
+        Vec3 base = Site_drinksSpot();
         int n = 0;
         for (int i = 0; i < world->count; i++) {
             const Object *o = world->objects[i];
@@ -215,7 +245,6 @@ Vec3 Car_dropSpot(const World *world, const Object *item) {
         }
         return vec3_add(base, vec3_scale(along, 0.5f * (float)n));
     }
-    Vec3 along = Site_campAlong(), across = Car_campDoor();
     Vec3 at = Site_campSpot(campOf(item->owner));
     if (item->variant >= GEAR_TENT_SMALL) return at; /* (the tent where it'll stand, the rest in front of its door) */
     float a = (item->variant == GEAR_MAT ? 0.55f : 0.0f) + (whose(item->owner) == 2 ? 1.2f : 0.0f) - 0.6f; /* (Jana's beside Volkram's) */
diff --git a/entropie/src/world/car.h b/entropie/src/world/car.h
index 523c879..7cf614f 100644
--- a/entropie/src/world/car.h
+++ b/entropie/src/world/car.h
@@ -5,7 +5,8 @@
  * Your car (OBJ_CAR, an estate): it drives in at the start (moved by hand,
  * see game_arrival.c), the ones in it riding (hidden, no physics, no AI).
  * Its boot holds what you brought - crates of beer and water (OBJ_CRATE),
- * sleeping bags, mats and tents (OBJ_GEAR, each somebody's) - taken out one
+ * sleeping bags, mats and tents (OBJ_GEAR, each somebody's), later the
+ * shopping (OBJ_MATERIAL, food boxes: game_shop.c) - taken out one
  * by one: you click at the open boot, the others unload (ai.c unloadCar).
  * Crates hand out bottles: you click one, the others ask (ai.take_from).
  */
@@ -14,14 +15,19 @@
 
 
 typedef enum { GEAR_SLEEPING_BAG, GEAR_MAT, GEAR_TENT_SMALL, GEAR_TENT_BIG } GearKind;
-typedef enum { CARGO_BEER, CARGO_WATER, CARGO_SLEEPING_BAG, CARGO_MAT, CARGO_TENT_SMALL, CARGO_TENT_BIG, CARGO_SODA, CARGO_MATE } CargoKind;
+typedef enum { CARGO_BEER, CARGO_WATER, CARGO_SLEEPING_BAG, CARGO_MAT, CARGO_TENT_SMALL, CARGO_TENT_BIG, CARGO_SODA, CARGO_MATE,
+               CARGO_BOARDS, CARGO_NAILS, CARGO_SCREWS, CARGO_PAINT, /* (from the hardware store: OBJ_MATERIAL) */
+               CARGO_FOOD, CARGO_SAUSAGES /* (a food crate, a cool box of sausages: OBJ_SUPPLY that run out) */ } CargoKind;
+
+#define FOOD_PORTIONS    40 /* a food crate: this many go at it */
+#define SAUSAGE_PORTIONS 24
 
 /* Crates: 0 beer, 1 water, 2 soda (cola and orange), 3 mate - how many bottles, how heavy each, its name. */
 int         Crate_bottles(int variant);
 float       Crate_bottleKg(int variant);
 const char *Crate_name(int variant);
 
-#define CAR_LOAD_MAX 20
+#define CAR_LOAD_MAX 32
 typedef struct CarLoad {
     int       n;
     CargoKind kind[CAR_LOAD_MAX];
@@ -48,8 +54,9 @@ void        Car_getIn(Object *car, Object *who, int seat);
 void        Car_getOut(Object *who);
 void        Car_openBoot(Object *car, bool open);
 
-/* Where an unloaded thing goes: crates by the fire, the camping things where its owner's tent will be
-   (the tent bag on the spot, sleeping bags and mats CAMP_BEDS_OUT in front of its door). */
+/* Where an unloaded thing goes: crates and food into the kitchen, material in front of the car room (the store),
+   the camping things where its owner's tent will be (the tent bag on the spot, sleeping bags and mats
+   CAMP_BEDS_OUT in front of its door). */
 #define CAMP_BEDS_OUT 2.8f
 Vec3        Car_campDoor(void);
 Vec3        Car_dropSpot(const World *world, const Object *item);
diff --git a/entropie/src/world/manor.c b/entropie/src/world/manor.c
index fee4c7e..d3c89b4 100644
--- a/entropie/src/world/manor.c
+++ b/entropie/src/world/manor.c
@@ -480,10 +480,10 @@ bool Shell_build(World *world, Vec3 origin, float yaw) {
 }
 
 /* ---------- The shelter: a roof on posts in the yard, benches under it ---------- */
-#define SHELTER_LEN   5.0f  /* m along the ridge */
-#define SHELTER_WID   3.6f  /* m across */
-#define SHELTER_EAVES 2.25f /* m: the eaves (headroom at the sides) */
-#define SHELTER_RISE  1.0f  /* m: the ridge over the eaves */
+#define PAVILION_SIDE  3.0f  /* m: a party pavilion, square */
+#define PAVILION_EAVES 2.3f  /* m: under the valance */
+#define PAVILION_PEAK  3.1f  /* m: the top of its roof */
+#define PAVILION_COVER 2.5f  /* m: the (unseen) roof that keeps the rain off, under the canvas */
 
 bool Shelter_build(World *world, Vec3 origin, float yaw) {
     static Plan plan;
@@ -493,28 +493,29 @@ bool Shelter_build(World *world, Vec3 origin, float yaw) {
     p->origin = origin;
     p->yaw = yaw;
     p->ok = true;
-    const Vec3 WOOD = { 0.40f, 0.29f, 0.18f }, FLOOR = { 0.52f, 0.49f, 0.44f };
-    const float hx = SHELTER_LEN * 0.5f - 0.1f, hz = SHELTER_WID * 0.5f - 0.1f, w = 0.08f;
-    for (int i = -1; i <= 1; i++) /* six posts: the corners and the middle of each long side */
-        for (int s = -1; s <= 1; s += 2)
-            box(p, vec3((float)i * hx - w, 0.0f, (float)s * hz - w), vec3((float)i * hx + w, SHELTER_EAVES, (float)s * hz + w), WOOD, SOLID);
-    for (int s = -1; s <= 1; s += 2) /* the beams along the eaves */
-        box(p, vec3(-hx - 0.15f, SHELTER_EAVES - 0.16f, (float)s * hz - w), vec3(hx + 0.15f, SHELTER_EAVES, (float)s * hz + w), WOOD, LOOK_ONLY);
-    box(p, vec3(-hx, 0.0f, -hz), vec3(hx, 0.03f, hz), FLOOR, LOOK_ONLY); /* (paving slabs under it) */
-    p->roofs[p->n_roofs++] = (ShapeRoof){ vec3(0.0f, 0.0f, 0.0f), SHELTER_LEN + 0.6f, SHELTER_WID + 0.6f, SHELTER_EAVES, SHELTER_RISE,
-                                          BUILDING_PLASTER_DARK, false, 0.0f };
-    roofSlabs(p, &p->roofs[0]);
-    if (!p->ok) return false;
-    Mesh mesh = Shapes_structure(p->slabs, p->n_slabs, p->openings, p->n_openings, p->roofs, p->n_roofs);
-    if (!mesh.vao) return false;
-    Object *look = Object_createStructure(mesh, origin, yaw, vec3(SHELTER_LEN, SHELTER_EAVES + SHELTER_RISE, SHELTER_WID));
-    if (!look || !World_add(world, look)) return false;
-    for (int s = -1; s <= 1; s += 2) { /* a log bench along each long side, a table between them */
-        Object *bench = Object_createFurniture(FURN_BENCH, toWorld(p, vec3(0.0f, 0.0f, (float)s * (hz - 0.4f))), vec3(3.4f, SEAT_HEIGHT, 0.36f), yaw);
-        if (!bench || !World_add(world, bench)) return false;
+    const Vec3 CANVAS = { 0.93f, 0.93f, 0.9f };
+    const float h = PAVILION_SIDE * 0.5f, leg = 0.045f;
+    for (int k = -1; k <= 1; k += 2) { /* two side by side along the yard */
+        float x = (float)k * (h + 0.02f);
+        for (int i = -1; i <= 1; i += 2) /* the legs */
+            for (int j = -1; j <= 1; j += 2) {
+                Vec3 at = vec3(x + (float)i * (h - leg), 0.0f, (float)j * (h - leg));
+                solid(p, vec3(at.x - leg, 0.0f, at.z - leg), vec3(at.x + leg, PAVILION_EAVES, at.z + leg), SOLID, NULL);
+            }
+        solid(p, vec3(x - h + 0.2f, PAVILION_COVER, -h + 0.2f), vec3(x + h - 0.2f, PAVILION_COVER + 0.04f, h - 0.2f), SOLID, NULL);
+        Mesh mesh = Shapes_pavilion(PAVILION_SIDE, PAVILION_EAVES, PAVILION_PEAK, CANVAS);
+        if (!mesh.vao) return false;
+        Object *look = Object_createStructure(mesh, toWorld(p, vec3(x, 0.0f, 0.0f)), yaw, vec3(PAVILION_SIDE, PAVILION_PEAK, PAVILION_SIDE));
+        if (!look || !World_add(world, look)) return false;
+        /* a beer table set under it: the table, a bench either side */
+        Object *table = Object_createFurniture(FURN_TABLE, toWorld(p, vec3(x, 0.0f, 0.0f)), vec3(2.2f, 0.77f, 0.5f), yaw);
+        if (!table || !World_add(world, table)) return false;
+        for (int s = -1; s <= 1; s += 2) {
+            Object *bench = Object_createFurniture(FURN_BENCH, toWorld(p, vec3(x, 0.0f, (float)s * 0.62f)), vec3(2.2f, SEAT_HEIGHT, 0.25f), yaw);
+            if (!bench || !World_add(world, bench)) return false;
+        }
     }
-    Object *table = Object_createFurniture(FURN_TABLE, toWorld(p, vec3(0.0f, 0.0f, 0.0f)), vec3(2.6f, 0.75f, 0.8f), yaw);
-    return table && World_add(world, table);
+    return p->ok;
 }
 
 bool Manor_build(World *world, Vec3 origin, float yaw) {
diff --git a/entropie/src/world/manor.h b/entropie/src/world/manor.h
index 96ab35d..f66c173 100644
--- a/entropie/src/world/manor.h
+++ b/entropie/src/world/manor.h
@@ -26,7 +26,7 @@ bool Manor_build(World *world, Vec3 origin, float yaw);
 /* Your unfinished building: a brick shell under a roof, open at the front
    (-z) to its extension (the frame where the bar gets built); +z against the barn. */
 bool Shell_build(World *world, Vec3 origin, float yaw);
-/* A shelter in the yard: a roof on six posts, two log benches and a table under it (out of the rain together). */
+/* In the yard: two party pavilions side by side, a beer table set under each (out of the rain together). */
 bool Shelter_build(World *world, Vec3 origin, float yaw);
 
 #endif /* WORLD_MANOR_H */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 7fafdd2..989339e 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -200,7 +200,8 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
                        : palette == PALETTE_STANDARD ? Shapes_coolBox(size)
                        : palette == PALETTE_GROUND ? Shapes_bagCarton(size) : Shapes_barrel(size.x, size.y);
         obj->pivot     = centered;
-        obj->is_static = true;
+        obj->is_static = weight >= 0.0f; /* (weight < 0: a bought one, -weight kg: carried in from the car) */
+        if (weight < 0.0f) obj->weight = -weight;
         obj->dispenses = palette == PALETTE_WARM ? OBJ_APPLE : palette == PALETTE_STANDARD ? OBJ_SAUSAGE
                        : palette == PALETTE_GROUND ? OBJ_BAG : OBJ_WATER;
         addCollider(obj, vec3_zero(), round ? vec3(size.x, size.y, size.x) : size, vec3_zero());
@@ -408,6 +409,18 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         obj->collider_count = 0;
         break;
 
+    case OBJ_MATERIAL: { /* kind = weight */
+        static const Vec3  SIZE[MATERIAL_KINDS] = { { 0.6f, 0.18f, 0.24f }, /* (deck boards cut to length: short enough to carry past things) */ { 0.16f, 0.09f, 0.11f }, { 0.16f, 0.09f, 0.11f }, { 0.27f, 0.3f, 0.27f } };
+        static const float KG[MATERIAL_KINDS]   = { 17.0f, 1.0f, 1.0f, 6.0f };
+        obj->variant = (int)weight < 0 ? 0 : (int)weight >= MATERIAL_KINDS ? MATERIAL_KINDS - 1 : (int)weight;
+        obj->size    = SIZE[obj->variant];
+        obj->weight  = KG[obj->variant];
+        obj->mesh    = Shapes_material(obj->variant, obj->size);
+        obj->pivot   = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
+        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
+        break;
+    }
+
     case OBJ_HAT: /* (grey here: its colour once it's known - world.c, showProps) */
         obj->size    = vec3(0.21f, 0.1f, 0.24f);
         obj->weight  = 0.1f;
@@ -1523,6 +1536,12 @@ void Object_describe(const Object *obj, char *out, size_t size) {
         else snprintf(out, size, "The %s stage - %s %d%% (hold left click to work on it)", Cast_projectName((Project)obj->variant), Stage_stepName(obj->build_stage), pct);
         break;
     }
+    case OBJ_MATERIAL: {
+        static const char *const WHAT[MATERIAL_KINDS] = { "A bundle of boards (8, cut to length, for a deck or a roof) - both hands", "A box of nails (for a deck)",
+                                                          "A box of screws (for a roof)", "A bucket of paint" };
+        snprintf(out, size, "%s - %d kg", WHAT[obj->variant % MATERIAL_KINDS], kg);
+        break;
+    }
     case OBJ_HAT: {
         const char *what = obj->variant == 2 ? "beanie" : "cap";
         if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s - pick it up to put it back on", what);
@@ -1578,6 +1597,11 @@ void Object_describe(const Object *obj, char *out, size_t size) {
         else snprintf(out, size, "Tree stump - chop it out with the axe or the chainsaw");
         break;
     case OBJ_SUPPLY:
+        if (obj->stock < 0) { snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Apple crate - empty" : "Cool box - empty"); break; }
+        if (obj->stock > 0) {
+            snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Food crate - %d portions left (take an apple)" : "Cool box - %d sausages left (take one)", obj->stock);
+            break;
+        }
         snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Apple crate - take an apple"
                  : obj->dispenses == OBJ_SAUSAGE ? "Cool box - take a sausage"
                  : obj->dispenses == OBJ_BAG ? "Garbage bags - take one"
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 2ad3368..b2c777c 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -90,8 +90,12 @@ typedef enum {
     OBJ_UMBRELLA,        /* an umbrella: size.y long (folded); opens held in the rain (umbrella_open); variant = colour */
     OBJ_HAT,             /* a hat off somebody's head (blown off): variant = HAT_*, look.hat_color, owner = whose */
     OBJ_STAGE,           /* static: a floor's stage being built (world/stage.h): variant = Project, build_stage, build_work */
+    OBJ_MATERIAL,        /* building material from the hardware store: variant = MaterialKind (weight given) */
 } ObjectType;
 
+/* Building material (OBJ_MATERIAL): what the stages are built with besides the logs. */
+typedef enum { MATERIAL_BOARDS, MATERIAL_NAILS, MATERIAL_SCREWS, MATERIAL_PAINT, MATERIAL_KINDS } MaterialKind;
+
 /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */
 typedef struct {
     float stamina;      /* 1 = fresh .. 0 = exhausted; work drains it, rest brings it back */
@@ -308,6 +312,7 @@ typedef struct Object {
     int        cast_id;       /* people: who in the story (world/cast.c), -1 nobody of it */
     int        build_stage;   /* a stage (OBJ_STAGE): how far it's built (BuildStage) ... */
     float      build_work;    /* ... person-hours of work into the next bit */
+    int        stock;         /* a bought food box (OBJ_SUPPLY): portions left (-1: empty); 0: never runs out (the scene's) */
     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 */
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index a95e42a..b07e61a 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -315,7 +315,7 @@ static const CourtDoorBuilding COURT[] = {
 #define YARD_B   12.0f
 #define TAP_A     3.0f   /* the tap: on the right end of the main building */
 #define TAP_B    17.25f
-#define SHELTER_A 24.5f  /* the shelter: a roof on posts by the fire, towards the barn */
+#define SHELTER_A 24.5f  /* the pavilions: two side by side, beer tables under them, by the fire towards the barn */
 #define SHELTER_B  3.5f
 #define FIRE_A   16.0f   /* the fireplace: near the main building and your garages */
 #define FIRE_B   -4.0f
@@ -415,6 +415,8 @@ static const Px FOREST_SOUTHWEST[] = {
     { 250, 800 }, { 110, 800 }, { 60, 690 }, { 30, 600 }, { -30, 560 },
 };
 static const Px FOREST_SOUTHEAST[] = { { 760, 520 }, { 830, 500 }, { 830, 800 }, { 745, 800 }, { 745, 560 } };
+/* The strip of wood right of the Kartoffelacker (about 10 m through), the guests' camping beyond it, above the chicken farm */
+static const Px FOREST_STRIP[] = { { 388, 588 }, { 412, 588 }, { 412, 648 }, { 388, 648 } };
 
 static const Forest FORESTS[] = {
     { FOREST_NW,        COUNT(FOREST_NW),        10.0f },
@@ -422,6 +424,7 @@ static const Forest FORESTS[] = {
     { FOREST_NE,        COUNT(FOREST_NE),        11.0f },
     { FOREST_SOUTHWEST, COUNT(FOREST_SOUTHWEST), 13.0f },
     { FOREST_SOUTHEAST, COUNT(FOREST_SOUTHEAST), 12.0f },
+    { FOREST_STRIP,     COUNT(FOREST_STRIP),      4.0f },
 };
 
 #define YARD_CLEARING  24.0f  /* no forest this close to the yard */
@@ -576,12 +579,17 @@ Vec3 Site_drinksSpot(void) { /* in the kitchen of your garages, along its front
     return placed(vec3(20.1f, 0.0f, 6.0f), WALLED_CENTER, g_garages.origin, g_garages.yaw, g_garages.zsign);
 }
 
+Vec3 Site_materialSpot(void) { /* out in front of the car room of your garages (its doorway): the store */
+    if (!g_garages.set) return court(FIRE_A + 6.0f, FIRE_B + 6.0f);
+    return placed(vec3(16.6f, 0.0f, -9.75f), WALLED_CENTER, g_garages.origin, g_garages.yaw, g_garages.zsign);
+}
+
 Vec3 Site_rainRoom(void) { /* the middle of the kitchen in your garages: where the guests wait out the rain */
     if (!g_garages.set) return Site_drinksSpot();
     return placed(vec3(23.6f, 0.0f, 1.2f), WALLED_CENTER, g_garages.origin, g_garages.yaw, g_garages.zsign);
 }
 
-int Site_rainPlaces(Vec3 *out, int max) { /* under a roof together: the kitchen, the shelter in the yard */
+int Site_rainPlaces(Vec3 *out, int max) { /* under a roof together: the kitchen, the pavilions in the yard */
     int n = 0;
     if (n < max) out[n++] = Site_rainRoom();
     if (n < max) out[n++] = court(SHELTER_A, SHELTER_B);
@@ -1270,7 +1278,7 @@ void Site_courseFrame(Vec3 *start, Vec3 *along) {
 static const struct { Project project; float a, b, front; } FLOORS[] = {
     { PROJECT_HOFBUEHNE,      36.0f,   5.0f, -1.0f }, /* in the yard, its back to the barn, facing the main building */
     { PROJECT_OBSTWIESE,      42.0f,  38.0f, -1.0f }, /* at the far end of the orchard, facing the apple trees */
-    { PROJECT_KARTOFFELACKER, 30.0f, -95.0f,  0.0f }, /* on the potato field behind the tents' meadow, facing the tents */
+    { PROJECT_KARTOFFELACKER, 107.5f, 45.9f, -1.0f }, /* behind the road to the chicken farm, far past the orchard and the barn, facing the road */
     { PROJECT_HIDDEN,        -20.0f,  -2.0f,  1.0f }, /* behind the main building, facing its back */
 };
 
diff --git a/entropie/src/world/site.h b/entropie/src/world/site.h
index 056b0e8..ac9b991 100644
--- a/entropie/src/world/site.h
+++ b/entropie/src/world/site.h
@@ -27,6 +27,8 @@
    along: the way things are laid out side by side there; door: the way the
    tents' doors face. */
 Vec3  Site_drinksSpot(void);
+/* Where the shopping's material goes: out in front of the car room of your garages (the store). */
+Vec3  Site_materialSpot(void);
 /* The middle of the kitchen in your garages: a roof to wait out the rain under, seats round it. */
 Vec3 Site_rainRoom(void);
 /* Where a floor's stage stands (the floors: world/stage.h). */
diff --git a/entropie/src/world/stage.c b/entropie/src/world/stage.c
index e33879e..167e915 100644
--- a/entropie/src/world/stage.c
+++ b/entropie/src/world/stage.c
@@ -2,6 +2,7 @@
 #include "render/shapes.h"
 
 #include <math.h>
+#include <stdio.h>
 
 #define STAGE_W 6.0f  /* m wide (along its front) */
 #define STAGE_D 4.0f  /* m deep */
@@ -52,34 +53,71 @@ float Stage_part(const Object *s) {
     return fminf(1.0f, s->build_work / NEED[s->build_stage]);
 }
 
-/* Logs lying by it (not held, not cut up). */
-static int logsBy(const World *world, const Object *s, Object **found, int max) {
+/* What each step takes, lying by it: logs, then the bought material (MaterialKind). */
+static const int NEED_LOGS[BUILD_DONE] = { [BUILD_FRAME] = STAGE_LOGS };
+static const int NEED_MAT[BUILD_DONE][MATERIAL_KINDS] = {
+    [BUILD_FLOOR] = { [MATERIAL_BOARDS] = 2, [MATERIAL_NAILS] = 1 },
+    [BUILD_ROOF]  = { [MATERIAL_BOARDS] = 2, [MATERIAL_SCREWS] = 1 },
+    [BUILD_DECOR] = { [MATERIAL_PAINT] = 1 },
+};
+
+/* Things of a kind lying by it (not held, not used up): logs (kind -1) or material. */
+static int lyingBy(const World *world, const Object *s, int kind, Object **found, int max) {
     int n = 0;
     for (int i = 0; i < world->count && n < max; i++) {
         Object *o = world->objects[i];
-        if (o->type != OBJ_LOG || o->is_held || o->broken) continue;
+        if (o->is_held || o->broken) continue;
+        if (kind < 0 ? o->type != OBJ_LOG : o->type != OBJ_MATERIAL || o->variant != kind) continue;
         float dx = o->position.x - s->position.x, dz = o->position.z - s->position.z;
         if (dx * dx + dz * dz < STAGE_LOG_REACH * STAGE_LOG_REACH) { if (found) found[n] = o; n++; }
     }
     return n;
 }
 
-int Stage_logsMissing(const World *world, const Object *s) {
-    if (s->build_stage != BUILD_FRAME) return 0;
-    int have = logsBy(world, s, NULL, STAGE_LOGS);
-    return have >= STAGE_LOGS ? 0 : STAGE_LOGS - have;
+static int needOf(int stage, int kind) {
+    if (stage < 0 || stage >= BUILD_DONE) return 0;
+    return kind < 0 ? NEED_LOGS[stage] : NEED_MAT[stage][kind];
+}
+
+int Stage_missing(const World *world, const Object *s, char *what, size_t size) {
+    static const char *const ONE[] = { "log", "bundle of boards", "box of nails", "box of screws", "bucket of paint" };
+    static const char *const MORE[] = { "logs", "bundles of boards", "boxes of nails", "boxes of screws", "buckets of paint" };
+    int total = 0;
+    size_t len = 0;
+    if (what && size) what[0] = 0;
+    for (int kind = -1; kind < MATERIAL_KINDS; kind++) {
+        int need = needOf(s->build_stage, kind);
+        if (!need) continue;
+        int have = lyingBy(world, s, kind, NULL, need), gap = need - have;
+        if (gap <= 0) continue;
+        total += gap;
+        if (what && len < size) {
+            if (gap == 1) snprintf(what + len, size - len, "%sa %s", len ? " and " : "", ONE[kind + 1]);
+            else snprintf(what + len, size - len, "%s%d %s", len ? " and " : "", gap, MORE[kind + 1]);
+            while (len < size && what[len]) len++;
+        }
+    }
+    return total;
+}
+
+int Stage_missingMaterial(const World *world, const Object *s) {
+    for (int kind = 0; kind < MATERIAL_KINDS; kind++) {
+        int need = needOf(s->build_stage, kind);
+        if (need && lyingBy(world, s, kind, NULL, need) < need) return kind;
+    }
+    return -1;
 }
 
 int Stage_work(World *world, Object *s, float person_hours) {
     if (s->build_stage >= BUILD_DONE) return -1;
-    if (Stage_logsMissing(world, s) > 0) return -2;
+    if (Stage_missing(world, s, NULL, 0) > 0) return -2;
     s->build_work += person_hours;
     int done = -1;
     if (s->build_work >= NEED[s->build_stage]) {
-        if (s->build_stage == BUILD_FRAME) { /* the logs went into it */
-            Object *logs[STAGE_LOGS];
-            int n = logsBy(world, s, logs, STAGE_LOGS);
-            for (int k = 0; k < n; k++) Object_useUp(logs[k]);
+        for (int kind = -1; kind < MATERIAL_KINDS; kind++) { /* (what lay by it went into it) */
+            Object *used[STAGE_LOGS > 4 ? STAGE_LOGS : 4];
+            int n = lyingBy(world, s, kind, used, needOf(s->build_stage, kind));
+            for (int k = 0; k < n; k++) Object_useUp(used[k]);
         }
         s->build_stage++;
         s->build_work = 0.0f;
diff --git a/entropie/src/world/stage.h b/entropie/src/world/stage.h
index 0809849..a8ae48a 100644
--- a/entropie/src/world/stage.h
+++ b/entropie/src/world/stage.h
@@ -4,8 +4,10 @@
 /*
  * The floors' stages, built step by step (OBJ_STAGE, one per floor: Hofbuehne,
  * Obstwiese, Kartoffelacker, the hidden floor). Each step takes work (person-
- * hours, quicker for the skilled) and the frame takes logs lying by it; the
- * look grows as the work goes on, the deck can be stood on once it's in.
+ * hours, quicker for the skilled) and its material lying by it: the frame
+ * logs, the deck boards and nails, the roof boards and screws, the decoration
+ * paint (bought: game_shop.c), used up when the step is done. The look grows
+ * as the work goes on, the deck can be stood on once it's in.
  */
 
 #include "world/world.h"
@@ -14,7 +16,7 @@
 typedef enum { BUILD_MARKED, BUILD_FRAME, BUILD_FLOOR, BUILD_ROOF, BUILD_DECOR, BUILD_DONE } BuildStage;
 
 #define STAGE_LOGS      3      /* logs lying by it for the frame */
-#define STAGE_LOG_REACH 6.0f   /* m round its middle */
+#define STAGE_LOG_REACH 6.0f   /* m round its middle: material lying this near counts */
 
 /* A stage marked out (nothing built yet) for a floor project, its front turned to yaw. */
 Object     *Stage_create(Project project, Vec3 at, float yaw);
@@ -23,10 +25,13 @@ Object     *Stage_of(Object *const *objects, int count, Project project);
 /* What's being done now ("the frame"), said plainly; and how far (0..1). */
 const char *Stage_stepName(int stage);
 float       Stage_part(const Object *stage);
-/* Logs still missing for this step (0: none needed, or all there). */
-int         Stage_logsMissing(const World *world, const Object *stage);
+/* Material still missing for this step (0: none needed, or all there); what: said plainly
+   ("2 bundles of boards and a box of nails"), may be NULL. */
+int         Stage_missing(const World *world, const Object *stage, char *what, size_t size);
+/* The bought material it waits for (MaterialKind), or -1 (nothing, or only logs). */
+int         Stage_missingMaterial(const World *world, const Object *stage);
 /* Work on it: person_hours of work done (by somebody of that skill). Returns the
-   step just finished (BUILD_FRAME ...), -1 nothing new, -2 held up (logs missing). */
+   step just finished (BUILD_FRAME ...), -1 nothing new, -2 held up (material missing). */
 int         Stage_work(World *world, Object *stage, float person_hours);
 /* Where somebody works on it (side -1 .. 1 along the front), and where their hands are. */
 Vec3        Stage_workSpot(const Object *stage, float side);
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 947dbaa..f647a07 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -1285,6 +1285,7 @@ void World_init(World *world) {
     Object_seePeople(NULL, 0); /* (not the people of a world gone: loading a save builds a new one) */
     world->rng = 0x2545F491u;
     world->hour = 12.0f;
+    world->cash = WORLD_CASH;
     Weather_init(world);
 }
 
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index 9e17675..2f3b7d2 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -38,6 +38,8 @@ enum { VOICE_DEEP, VOICE_MAN, VOICE_WOMAN };
 typedef struct { NoiseKind kind; Vec3 at; float loud; int surface; } Noise;
 #define WORLD_NOISES 96
 
+#define WORLD_CASH 1200.0f /* € in the cash box at the start */
+
 typedef struct World {
     float    dt_props;   /* (this frame's step, for the props that move: gates) */
     float    game_dt;    /* (this frame's step in game seconds) */
@@ -73,6 +75,8 @@ typedef struct World {
     Noise    noises[WORLD_NOISES]; /* sounds made this frame (see Noise) */
     int      noise_count;
     struct RainDrops *drops;
+    float    cash;         /* € in the festival's cash box (shopping runs: game_shop.c) ... */
+    bool     shopping;     /* ... one on its way (the car's off) */
 } World;
 
 /* A sound made here (dropped when the list is full: nobody's listening). */