Nimbin[12]?Games / Entropie / commits / 3eee790

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.6.0: arriving by car

Christian Immanuel · 2026-10-20 06:23 · 3eee790607f9e65e54f8b118e5efb4d206e11080

- The game starts in your car: you, Volkram, Jana and Toni drive in along
  the gravel road and park in the yard (seen from the driver's seat)
- Everybody gets out, Kurti greets you, the boot opens
- The boot: 2 crates of beer, 2 of water, everybody's sleeping bag and mat,
  the big tent (Volkram and Jana) and two small ones (Toni, you)
- The others unload: crates by the fire, the camping things to the orchard
  where the tents will go; left click at the boot to help
- Crates: left click takes a bottle out, E / Q carries the crate; the
  others take their beer and water from them too
- Fix: the fire's ground slab stuck out past the stones; people beside the
  fire stepped on and off its corners and jittered
- ARRIVE_BY_CAR in src/game/game.c (testing) skips the drive
 entropie/ARCHITECTURE.md           |   4 +-
 entropie/Makefile                  |   2 +-
 entropie/README.md                 |  15 ++-
 entropie/STATE.md                  |  13 ++-
 entropie/commit_message.txt        |  21 ++--
 entropie/src/game/game.c           |   6 ++
 entropie/src/game/game.h           |  15 +++
 entropie/src/game/game_actions.c   |  39 ++++++-
 entropie/src/game/game_arrival.c   | 182 +++++++++++++++++++++++++++++++
 entropie/src/game/game_internal.h  |   6 ++
 entropie/src/game/player.c         |   2 +-
 entropie/src/people/ai.c           |  97 ++++++++++++++++-
 entropie/src/people/ai.h           |   4 +
 entropie/src/people/hands.c        |   4 +
 entropie/src/render/shapes.h       |  15 +++
 entropie/src/render/shapes_props.c | 109 +++++++++++++++++++
 entropie/src/world/car.c           | 212 +++++++++++++++++++++++++++++++++++++
 entropie/src/world/car.h           |  57 ++++++++++
 entropie/src/world/object.c        |  59 ++++++++++-
 entropie/src/world/object.h        |  10 ++
 entropie/src/world/physics.c       |   2 +-
 entropie/src/world/site.c          |  20 ++++
 entropie/src/world/site.h          |  10 ++
 entropie/src/world/world.c         |  22 +++-
 24 files changed, 899 insertions(+), 27 deletions(-)

diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index e8ae814..e4ab575 100644
--- a/entropie/ARCHITECTURE.md
+++ b/entropie/ARCHITECTURE.md
@@ -10,9 +10,9 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL
 | `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input | core |
 | `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math |
 | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render |
-| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, clothes, water, grass, grid, day | everything but game |
+| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, clothes, car, water, 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 | all |
+| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start | 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 f2a1637..cb27b9d 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.5.0
+VERSION   := 2.6.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 360b6f4..0b674dc 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -23,6 +23,19 @@ Painted in the rotoscope style of *Waking Life* (2001):
   with only a faint paper edge
 - Stats on a painted sheet of paper (Tab)
 
+## Arriving
+
+- The game starts in your car: you, Volkram, Jana and Toni come in along the
+  gravel road and park in the yard (a short drive, look around with the mouse)
+- Everybody gets out, Kurti greets you, the boot opens
+- In the boot: 2 crates of beer, 2 of water, everybody's sleeping bag and
+  mat, the big tent (Volkram and Jana) and two small ones (Toni, you)
+- The others unload: crates by the fire, the camping things over to the
+  orchard where the tents will go. Help them: left click at the boot takes
+  out the next thing (crates need both hands)
+- Crates: left click takes a bottle out (the others help themselves too),
+  E / Q carries the whole crate
+
 ## The site
 
 The map is Homburgshöhe at Geflügelhof Oderland, traced from the real map
@@ -59,7 +72,7 @@ and satellite pictures (about 340 x 340 m).
   place with an old car behind the wide door
 - The woodpile by the barn: everybody stacks firewood
   there unless you show them another place
-- The fireplace is a ring of field stones; its tripod of long wooden poles
+- The fireplace is a ring of field stones (its ground slab no wider than the ring); its tripod of long wooden poles
   (3.9 m up, the grill on a long chain) stands well away from the fire; you
   walk round them
 - Alice the dachshund lives here (painted like the people): she trots after you, sniffs around, begs
diff --git a/entropie/STATE.md b/entropie/STATE.md
index b23e4f6..7cb7c71 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -180,6 +180,13 @@ Round 2.5:
 - `ai.c` binButt (idle, wander, at a table): with `butt_held`, the tin, a bin or a burning fire within BUTT_WALK 30 m: there (gives up after 40 s: skipped) and in; none: asks somebody within 6 m with a pocket ashtray (NPCs: "Sure, put it in." - into theirs; you: `asking_you` POCKET_ASHTRAY, the hint line, B Enter "hold it out"); nothing after BUTT_WAIT 25 s or held BUTT_KEEP 90 s: wrapped up into the pocket ("I'll keep it till I find a bin."). Pocket butts are emptied passing within BUTT_PASS 2 m of the tin, a bin or the fire; a pocket ashtray with TRAY_FULL 8 is taken there on purpose.
 - Your pockets: row "Pocket ashtray - n butts": Enter looking at somebody holding a butt: it goes into yours ("Thanks - much better than on the ground."); else at the tin, a bin or the fire (within 1 m): emptied.
 
+Round 2.6:
+- Arrival (`game_arrival.c`, `ARRIVE_BY_CAR`): your car (OBJ_CAR, `car.c`, an estate: `Shapes_car`, from the seat `Shapes_carInside` - the body round the eye hid everything) on `Site_arrivalPath` (gravel road, into the yard, parked at court 24 / 19 facing the garages; Catmull-Rom, ARRIVAL_SAMPLES 160), DRIVE_TOP 5.5 m/s, braking 1.4 m/s^2, about 12 s. You (driver), Volkram, Jana, Toni `riding` (hidden, no physics, no AI, moved with it; first person from the seat, nothing to do but look). Parked: out one after the other (0.6-2.6 s, a line each), Kurti "There you are!", the boot open at 3.2 s, the three friends `ai.unload`.
+- The boot (`CarLoad`, last in first out): sleeping bags, mats, tents (big: Volkram and Jana; small: Toni, you), then 2 crates of beer, 2 of water. `Car_takeOut`: set down behind it. Unloading (`ai.c` unloadCar, idle / wander): to the boot (side by side, `ai.side`), `ai.take_from` = the car -> `Car_serve` puts the next thing there for them (`ai.target`), picked up (crates and the big tent with both hands), carried to `Car_dropSpot` (crates: `Site_drinksSpot` by the fire, 0.5 m apart; the camping things: `Site_campSpot` in the orchard - yours, Volkram and Jana's, Toni's - side by side by kind), put down there; gives up a trip after 70 s. All 15 things out in about 110 s. You: left click at the boot (`game_unloadCar`, 1.1 m): the next thing into your hands; hint lines at the boot and carrying.
+- Crates (OBJ_CRATE: beer 20 bottles, water 12; 19 / 16 kg; `Shapes_crate` shows what's left; set down standing). Left click: a bottle (`Crate_takeBottle`); E / Q: the crate. NPCs wanting a beer or a drink count crates like bottles (`findConsumable`), at it `ai.take_from` -> a bottle on top for them.
+- Camping gear (OBJ_GEAR: sleeping bag, mat, small and big tent bag; `owner`; colours by owner, `Shapes_gear`).
+- The fireplace's ground slab is 0.6 of the ring wide (its corners at the stones): people beside the fire stepped on and off its corners (Kurti's joint jumps in the NPC audit 1465 -> 421).
+
 Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves.
 
 Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built).
@@ -299,6 +306,7 @@ Ground: meadow wears into paths; tar, concrete, gravel and tracks are part of th
 - Esc: menu (the game waits): Resume, Keys (show / hide), Quit; mouse or Up / Down + Enter
 - Dev menu (F1), second line: teleport you to the parkour (Right) or the yard (Left). Then pick anyone (Left / Right on the first line) and change: female, Urinella, sporty, drunk, energy, bladder, hunger, thirst, sleepy, strength, annoyed, pride, lively, nerves, clumsy, willing, likes and skill for each job (Up / Down: line, the list scrolls; Left / Right: value)
 - `START_AT_COURSE` in `src/game/game.c`: start at the obstacle course (1) or in the courtyard (0, default); the course is there either way
+- `ARRIVE_BY_CAR` in `src/game/game.c`: 1 (default) the start in the car; 0: standing in the yard, the car on the road with everything in it
 
 ## Speed
 
@@ -311,7 +319,7 @@ Most important first.
 **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)
 
-- NPC audit: Kurti idles by the fire with the chainsaw in his hand; it knocks against the tripod poles and he jitters (about 1400 of the 3500 joint jumps)
+- NPC audit: Kurti walks about with the chainsaw (putting it away?) and jitters at the garage gates and in the tool room (300-900 joint jumps a run)
 
 **Body and moving**
 - Obstacle approach, still to do: thin round logs (under 0.2 m) are still stepped over; a step in between on top of something unstable (if clumsy); NPCs: pull up on their own for their own reasons (now only following a leader), jumping onto things
@@ -326,7 +334,6 @@ Most important first.
 - Smoking, still to do: paying for a pack (no money in the game yet)
 - "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); the pullover over a railing NPCs never do on their own
-- Arrival, next: the first start by car (you, Volkram, Jana, Toni), unloading 2 crates of beer, 2 of water, sleeping bags, mats, tents
 - Tents: Volkram and Jana share one, Toni and you a small one each; pitching together (poles, the cover, pegs); sleeping in them
 - Rain: showers; people go into the house or their tents
 
@@ -337,4 +344,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.5.0 (build 74f2023)`
+State of `entropie 2.6.0 (build 963c4fd)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index f1efa7b..ffdb5fa 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,9 +1,14 @@
-entropie 2.5.0: cigarette butts go somewhere
+entropie 2.6.0: arriving by car
 
-- Finished cigarettes go into the fire when close, else the tin or the bin
-  when near; tidy people walk there with the butt
-- Pocket ashtrays (you and Jana): the butt goes in there
-- No bin near: the tidy ask for a pocket ashtray (from you too: B, look at
-  them, Enter holds yours out), else they keep it wrapped up till they pass
-  a bin; the others still flick it away
-- Your pocket ashtray: B, Enter at the tin, a bin or the fire empties it
+- The game starts in your car: you, Volkram, Jana and Toni drive in along
+  the gravel road and park in the yard (seen from the driver's seat)
+- Everybody gets out, Kurti greets you, the boot opens
+- The boot: 2 crates of beer, 2 of water, everybody's sleeping bag and mat,
+  the big tent (Volkram and Jana) and two small ones (Toni, you)
+- The others unload: crates by the fire, the camping things to the orchard
+  where the tents will go; left click at the boot to help
+- Crates: left click takes a bottle out, E / Q carries the crate; the
+  others take their beer and water from them too
+- Fix: the fire's ground slab stuck out past the stones; people beside the
+  fire stepped on and off its corners and jittered
+- ARRIVE_BY_CAR in src/game/game.c (testing) skips the drive
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 0f925cb..0ab49bd 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -5,8 +5,10 @@
 
 #include "game/game_internal.h"
 #include "world/clothes.h"
+#include "world/car.h"
 
 #define START_HOUR   13.0f
+#define ARRIVE_BY_CAR   1   /* (testing) 0: start standing in the yard, no car */
 #define START_AT_COURSE 0   /* (testing) 1: start at the obstacle course at the chicken farm; 0: in the yard (the course is there either way) */
 #define TIME_SCALE    3.0f  /* game seconds per real second */
 #define CAMERA_FOV   70.0f
@@ -52,6 +54,7 @@ static void followPlayer(Game *game, float dt) {
     Camera *cam = &game->camera;
     const Object *p = game->player;
     Vec3 eye = vec3_add(p->position, vec3(0, PLAYER_EYE_HEIGHT, 0));
+    if (p->riding) { cam->position = Car_eye(p->riding, p->ride_seat); return; } /* (in the car: from the seat) */
     if (Body_isDown(&p->body)) {
         /* On the ground: see from the head, wherever it is */
         Vec3 facing = Object_facing(p);
@@ -239,6 +242,8 @@ bool Game_init(Game *game) {
     }
     Camera_init(&game->camera, game->camera.position, look * 180.0f / PI_F, 0.0f, CAMERA_FOV);
     game->marker = Shapes_marker(1.8f, 0.8f);
+    memset(&game->arrival, 0, sizeof(game->arrival));
+    if (ARRIVE_BY_CAR && !START_AT_COURSE) game_arrivalStart(game);
     followPlayer(game, 0.0f);
     return true;
 }
@@ -287,6 +292,7 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     puttingUp(game);
     game_climbLadder(game, input, dt);
     game_climb(game, input, dt);
+    game_arrival(game, dt);
     bool warm = game->player->look.top == TOP_JACKET;
     World_update(&game->world, dt, dt * game->daycycle.time_scale * speed);
     Object *you = game->player;
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index c05f699..f217356 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -13,6 +13,20 @@
 #include "render/camera.h"
 #include "render/renderer.h"
 
+/* The start: arriving by car (see game_arrival.c). */
+#define ARRIVAL_POINTS  12
+#define ARRIVAL_SAMPLES 160
+typedef enum { ARRIVAL_NONE, ARRIVAL_DRIVING, ARRIVAL_PARKED, ARRIVAL_DONE } ArrivalStage;
+typedef struct {
+    ArrivalStage stage;
+    struct Object *car;
+    Vec3     path[ARRIVAL_SAMPLES]; /* the way in, sampled ... */
+    float    dist[ARRIVAL_SAMPLES]; /* ... how far along each sample is */
+    float    length, s, speed, time, park_yaw;
+    int      step;                  /* ... who's out of the car */
+    bool     greeted;
+} Arrival;
+
 typedef struct {
     World    world;
     Object  *player;
@@ -52,6 +66,7 @@ typedef struct {
     float    brace_hold;    /* hands on an edge you walked up to: kept there this much longer ... */
     Vec3     brace_grip;    /* ... here */
     Object  *putting_up;    /* the ladder you are putting up (hands on it, standing still) */
+    Arrival  arrival;       /* the start: in by car */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index aa4cbfa..f9be548 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -6,6 +6,7 @@
 #include "game/game_internal.h"
 #include "people/pockets.h"
 #include "world/clothes.h"
+#include "world/car.h"
 #include "people/climb.h"
 
 #include <stdlib.h>
@@ -554,7 +555,7 @@ static bool takeFromSupply(Game *game) {
     Vec3 facing = Object_facing(p);
     for (int i = 0; i < game->world.count; i++) {
         Object *o = game->world.objects[i];
-        if (o->type != OBJ_SUPPLY || Hands_gapFrom(p->position, p->look.height, o) > SUPPLY_REACH) continue;
+        if ((o->type != OBJ_SUPPLY && (o->type != OBJ_CRATE || o->is_held)) || Hands_gapFrom(p->position, p->look.height, o) > SUPPLY_REACH) continue;
         Vec3 to = vec3_sub(o->position, p->position);
         to.y = 0.0f;
         if (vec3_length(to) > 1e-3f && vec3_dot(vec3_normalize(to), facing) < 0.3f) continue;
@@ -573,6 +574,12 @@ static bool takeFromSupply(Game *game) {
         int hand = p->hands.state[HAND_RIGHT] == HAND_FREE ? HAND_RIGHT
                  : p->hands.state[HAND_LEFT] == HAND_FREE ? HAND_LEFT : -1;
         if (hand < 0) { say(game, "Your hands are full"); return true; }
+        if (o->type == OBJ_CRATE) { /* a bottle out of the crate (E / Q: the crate itself) */
+            if (o->stored <= 0) { say(game, "The crate is empty"); return true; }
+            Object *bottle = Crate_takeBottle(&game->world, o);
+            if (bottle && Hands_pickUpObject(p, hand, bottle)) tell(game, bottle);
+            return true;
+        }
         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)
@@ -785,6 +792,26 @@ static int useWater(Game *game) {
     return CLICK_WATER;
 }
 
+/* At the car's open boot, or carrying something out of it: what to do with it. */
+static bool game_bootHint(Game *game, char *out, size_t size) {
+    const Object *p = game->player, *car = game->arrival.car;
+    for (int h = 0; h < 2; h++) {
+        const Object *it = p->hands.item[h];
+        if (!it || p->hands.state[h] != HAND_HOLDING) continue;
+        if (it->type == OBJ_CRATE) { snprintf(out, size, "The crates go by the fire, next to each other  -  right click: put it down"); return true; }
+        if (it->type == OBJ_GEAR) {
+            snprintf(out, size, "%s  -  over to the orchard, where the tents go  -  right click: put it down",
+                     it->owner == p ? "Yours" : it->owner ? it->owner->name : "Somebody's");
+            return true;
+        }
+    }
+    if (!car || !car->boot_open || !car->load || car->load->n == 0) return false;
+    Vec3 boot = Car_bootSpot(car);
+    if (hypotf(boot.x - p->position.x, boot.z - p->position.z) > 1.6f) return false;
+    snprintf(out, size, "The boot: left click - take out %s%s", Car_nextName(car), Car_nextHeavy(car) ? " (both hands)" : "");
+    return true;
+}
+
 /* Somebody near who asked you for a paper, a filter or tobacco (and is still waiting). */
 static const Object *askingYou(const Game *game) {
     const Object *p = game->player;
@@ -811,6 +838,9 @@ const char *Game_hint(Game *game) {
         snprintf(game->hint, sizeof(game->hint), "%s asks you for %s  -  B: your pockets, look at %s, Enter: %s",
                  asking->name, Pockets_needName((PocketNeed)asking->ai.asking_you), asking->female ? "her" : "him",
                  asking->ai.asking_you == POCKET_ASHTRAY ? "hold it out" : "give it");
+    else if (game->player->riding)
+        snprintf(game->hint, sizeof(game->hint), "Nearly there - Homburgsh\xC3\xB6he. Look around (mouse)");
+    else if (game_bootHint(game, game->hint, sizeof(game->hint))) {}
     else if (shown)
         snprintf(game->hint, sizeof(game->hint), "%s follows you: walk to where the firewood should go and left click  -  Backspace: never mind", shown->name);
     else if (watcher)
@@ -972,7 +1002,7 @@ static int leftClick(Game *game) {
         Talk_showHere(&game->world, p, shown);
         return CLICK_NONE;
     }
-    if (mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || takePullover(game) || smokesMachine(game) || swingGate(game)) return CLICK_NONE;
+    if (game_unloadCar(game) || mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || takePullover(game) || smokesMachine(game) || swingGate(game)) return CLICK_NONE;
     int watering = useWater(game);
     if (watering >= 0) return watering;
     if (bagTrash(game)) return CLICK_BAG;
@@ -1073,6 +1103,11 @@ void game_handleActions(Game *game, const Input *input) {
     Object *const *objects = game->world.objects;
     int count = game->world.count;
 
+    if (p->riding) { /* (in the car: looking about only) */
+        game->click_action = CLICK_NONE;
+        if (input->show_keys) game->show_keys = !game->show_keys;
+        return;
+    }
     if (input->toggle_view) game->third_person = !game->third_person;
     if (input->show_keys) game->show_keys = !game->show_keys;
     if (input->show_stats) game->show_stats = !game->show_stats;
diff --git a/entropie/src/game/game_arrival.c b/entropie/src/game/game_arrival.c
new file mode 100644
index 0000000..547bfbf
--- /dev/null
+++ b/entropie/src/game/game_arrival.c
@@ -0,0 +1,182 @@
+/*
+ * The start: you, Volkram, Jana and Toni come in by car along the gravel road
+ * and park in the yard (you at the wheel, the car drives itself: a short
+ * scene), get out one after the other, the boot opens and the three start
+ * unloading: crates of beer and water by the fire, the camping things over to
+ * the orchard where the tents will go. You help: left click at the boot.
+ */
+
+#include "game/game_internal.h"
+#include "world/car.h"
+#include "world/site.h"
+
+#define DRIVE_TOP     5.5f  /* m/s on the gravel road ... */
+#define DRIVE_ACCEL   1.6f  /* ... speeding up ... */
+#define DRIVE_BRAKE   1.4f  /* ... and slowing down to park (m/s^2) */
+#define PATH_SAMPLES  ARRIVAL_SAMPLES
+
+/* Catmull-Rom through the points (the ends doubled). */
+static Vec3 along(const Vec3 *p, int n, float u) {
+    float f = clampf(u, 0.0f, 1.0f) * (float)(n - 1);
+    int i = (int)f;
+    if (i >= n - 1) i = n - 2;
+    float t = f - (float)i;
+    Vec3 p0 = p[i > 0 ? i - 1 : 0], p1 = p[i], p2 = p[i + 1], p3 = p[i + 2 < n ? i + 2 : n - 1];
+    float t2 = t * t, t3 = t2 * t;
+    Vec3 r = vec3_zero();
+    r = vec3_add(r, vec3_scale(p1, 2.0f));
+    r = vec3_add(r, vec3_scale(vec3_sub(p2, p0), t));
+    r = vec3_add(r, vec3_scale(vec3_add(vec3_sub(vec3_scale(p0, 2.0f), vec3_scale(p1, 5.0f)), vec3_sub(vec3_scale(p2, 4.0f), p3)), t2));
+    r = vec3_add(r, vec3_scale(vec3_add(vec3_sub(vec3_scale(p1, 3.0f), p0), vec3_sub(p3, vec3_scale(p2, 3.0f))), t3));
+    return vec3_scale(r, 0.5f);
+}
+
+static Object *person(Game *game, const char *name) {
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        if (o->type == OBJ_HUMAN && !strcmp(o->name, name)) return o;
+    }
+    return NULL;
+}
+
+static const char *const FRIENDS[] = { "Volkram", "Jana", "Toni" };
+
+void game_arrivalStart(Game *game) {
+    Arrival *a = &game->arrival;
+    Vec3 pts[ARRIVAL_POINTS];
+    int n = 0;
+    Site_arrivalPath(pts, &n, &a->park_yaw);
+    if (n < 2) return;
+    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;
+    }
+    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;
+    a->car = car;
+    Object *friends[3];
+    for (int k = 0; k < 3; k++) friends[k] = person(game, FRIENDS[k]);
+    /* the boot, loaded last first: the camping things on top, the crates under them */
+    Car_load(car, CARGO_WATER, NULL);
+    Car_load(car, CARGO_WATER, NULL);
+    Car_load(car, CARGO_BEER, NULL);
+    Car_load(car, CARGO_BEER, NULL);
+    Car_load(car, CARGO_TENT_BIG, friends[0]);
+    Car_load(car, CARGO_TENT_SMALL, friends[2]);
+    Car_load(car, CARGO_TENT_SMALL, game->player);
+    Object *everybody[4] = { game->player, friends[0], friends[1], friends[2] };
+    for (int k = 0; k < 4; k++) if (everybody[k]) Car_load(car, CARGO_MAT, everybody[k]);
+    for (int k = 0; k < 4; k++) if (everybody[k]) Car_load(car, CARGO_SLEEPING_BAG, everybody[k]);
+    for (int k = 0; k < 4; k++) if (everybody[k]) Car_getIn(car, everybody[k], k);
+    Car_place(&game->world, car, a->path[0], car->rotation.y);
+    a->stage = ARRIVAL_DRIVING;
+    a->speed = 3.0f;
+    a->s = 0.0f;
+    a->time = 0.0f;
+    game->third_person = false;
+    float yaw = atan2f(head.z, head.x) * 180.0f / PI_F;
+    Camera_init(&game->camera, Car_eye(car, 0), yaw, -4.0f, game->camera.fov);
+}
+
+/* Where the car is s meters along, and which way it heads. */
+static void at(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);
+    *pos = vec3_lerp(a->path[k - 1], a->path[k], t);
+    Vec3 d = vec3_sub(a->path[k], a->path[k - 1]);
+    *dir = vec3_normalize(vec3(d.x, 0.0f, d.z));
+}
+
+static void getOut(Game *game, const char *name, const char *line) {
+    Object *o = name ? person(game, name) : game->player;
+    if (!o || !o->riding) return;
+    Car_getOut(o);
+    if (line && o->type == OBJ_HUMAN) Ai_say(o, line);
+}
+
+void game_arrival(Game *game, float dt) {
+    Arrival *a = &game->arrival;
+    Object *car = a->car;
+    if (a->stage == ARRIVAL_NONE || a->stage == ARRIVAL_DONE || !car) return;
+    a->time += dt;
+    if (a->stage == ARRIVAL_DRIVING) {
+        float left = a->length - a->s;
+        float brake = sqrtf(fmaxf(0.0f, 2.0f * DRIVE_BRAKE * left));
+        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);
+        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));
+        Car_place(&game->world, car, pos, yaw);
+        if (a->s >= a->length - 0.01f) { a->stage = ARRIVAL_PARKED; a->time = 0.0f; }
+        return;
+    }
+    /* parked: out one after the other, the boot up, unloading */
+    static const struct { float at; int who; const char *line; } STEPS[] = {
+        { 0.6f, -1, NULL },
+        { 1.2f, 0, "Made it! Right - beer first." },
+        { 1.9f, 1, "Oh, it's lovely here." },
+        { 2.6f, 2, "My back... that seat is a crime." },
+    };
+    for (int k = 0; k < 4; k++)
+        if (a->step == k && a->time >= STEPS[k].at) { getOut(game, STEPS[k].who < 0 ? NULL : FRIENDS[STEPS[k].who], STEPS[k].line); a->step++; }
+    if (a->step == 1 && !a->greeted) {
+        Object *kurti = person(game, "Kurti");
+        if (kurti) Ai_say(kurti, "There you are! Took your time.");
+        say(game, "You're here. Left click at the boot to help unloading");
+        game->third_person = true;
+        a->greeted = true;
+    }
+    if (a->step == 4 && a->time >= 3.2f) {
+        Car_openBoot(car, true);
+        for (int k = 0; k < 3; k++) {
+            Object *o = person(game, FRIENDS[k]);
+            if (!o) continue;
+            o->ai.mode = AI_IDLE;
+            o->ai.unload = true;
+            o->ai.unload_car = car;
+            o->ai.unload_time = 0.0f;
+            o->ai.side = (float)(k - 1);
+        }
+        a->stage = ARRIVAL_DONE;
+    }
+}
+
+bool game_arriving(const Game *game) {
+    return game->arrival.stage == ARRIVAL_DRIVING || (game->arrival.stage == ARRIVAL_PARKED && game->player->riding);
+}
+
+/* Left click at the open boot: the next thing out, into your hands. */
+bool game_unloadCar(Game *game) {
+    Object *car = game->arrival.car, *p = game->player;
+    if (!car || !car->boot_open) return false;
+    Vec3 boot = Car_bootSpot(car);
+    if (hypotf(boot.x - p->position.x, boot.z - p->position.z) > 1.1f) return false;
+    const char *next = Car_nextName(car);
+    if (!next) { say(game, "The boot is empty"); return true; }
+    bool heavy = Car_nextHeavy(car);
+    bool both = p->hands.state[HAND_LEFT] == HAND_FREE && p->hands.state[HAND_RIGHT] == HAND_FREE;
+    int hand = Hands_freeHand(p);
+    if (heavy ? !both : hand < 0) {
+        snprintf(game->info, sizeof(game->info), "Next is %s - %s", next, heavy ? "both hands free for it" : "a hand free for it");
+        game->info_about = NULL;
+        say(game, game->info);
+        return true;
+    }
+    Object *item = Car_takeOut(&game->world, car);
+    if (!item) return true;
+    item->position = vec3_add(p->position, vec3_add(vec3_scale(Object_facing(p), 0.45f), vec3(0.0f, 0.5f, 0.0f))); /* (right at your hands) */
+    Object_syncColliders(item);
+    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 by the fire" : "the camping things go over to the orchard");
+    game->info_about = NULL;
+    say(game, game->info);
+    return true;
+}
diff --git a/entropie/src/game/game_internal.h b/entropie/src/game/game_internal.h
index 87725b0..379adeb 100644
--- a/entropie/src/game/game_internal.h
+++ b/entropie/src/game/game_internal.h
@@ -21,6 +21,12 @@
 
 #define MESSAGE_TIME 2.5f /* seconds a hint stays on screen */
 
+/* game_arrival.c: in by car at the start; still in it (no walking, no hands); the boot clicked. */
+void game_arrivalStart(Game *game);
+void game_arrival(Game *game, float dt);
+bool game_arriving(const Game *game);
+bool game_unloadCar(Game *game);
+
 void game_callHelp(Game *game);
 void game_callOver(Game *game);
 Object *game_personInFront(const Game *game);
diff --git a/entropie/src/game/player.c b/entropie/src/game/player.c
index 3ed7ac8..9070d1d 100644
--- a/entropie/src/game/player.c
+++ b/entropie/src/game/player.c
@@ -48,7 +48,7 @@ void Player_update(Object *player, const Input *input, const Camera *camera, flo
     player->body.look_dir = camera->front;
     player->body.look_weight = 1.0f;
     player->body.urge = clampf(input->move_forward, 0.0f, 1.0f); /* W gets you up sooner after a fall */
-    if (Body_isBusy(&player->body) || player->on_ladder) { /* (climbing: see the game) */
+    if (Body_isBusy(&player->body) || player->on_ladder || player->riding) { /* (climbing: see the game; in the car) */
         player->movement = vec3_zero();
         player->move_local = vec3_zero();
         return;
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 9b9ec72..fc83009 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -11,6 +11,7 @@
 #include "world/grid.h"
 #include "world/world.h"
 #include "world/clothes.h"
+#include "world/car.h"
 #include "render/shapes.h"
 
 /* ---------- Small helpers ---------- */
@@ -490,16 +491,19 @@ static Object *findConsumable(const Object *self, Object *const *objects, int co
     float best_dist = want == WANT_BEER ? DRINK_RANGE : DRINK_RANGE * 2.0f;
     for (int i = 0; i < count; i++) {
         Object *o = objects[i];
+        bool crate = o->type == OBJ_CRATE; /* (a crate: a bottle out of it) */
         bool fits = want == WANT_FOOD ? Object_isFood(o) : Object_isBottle(o);
         if (want == WANT_BEER && o->alcohol <= 0.0f) fits = false;
-        if (!fits || o->is_held || o->sips <= 0 || o->broken) continue;
+        if (crate) fits = want != WANT_FOOD && o->stored > 0 && (want != WANT_BEER || o->variant == 0);
+        if (!fits || o->is_held || (!crate && o->sips <= 0) || o->broken) continue;
         if (ai_skipped(&self->ai, o)) continue; /* couldn't get at it */
         bool taken = false;
         for (int j = 0; j < count; j++) {
             const Object *c = objects[j];
             if (c != self && c->is_character && c->ai.target == o) taken = true;
         }
-        float d = flatDistance(o->position, self->position) + (want == WANT_DRINK && o->alcohol > 0.0f ? 8.0f : 0.0f);
+        float d = flatDistance(o->position, self->position) + (want == WANT_DRINK && (crate ? o->variant == 0 : o->alcohol > 0.0f) ? 8.0f : 0.0f);
+        if (crate) taken = false; /* (enough in it for all) */
         if (!taken && d < best_dist) {
             best_dist = d;
             best = o;
@@ -742,7 +746,8 @@ static void updateFetch(Object *self, Object *const *objects, int count, float d
     Ai *ai = &self->ai;
     Object *bottle = ai->target;
     ai->timer += dt;
-    if (!bottle || bottle->is_held || bottle->sips <= 0 || ai->timer > FETCH_TIMEOUT ||
+    bool crate = bottle && bottle->type == OBJ_CRATE;
+    if (!bottle || bottle->is_held || (crate ? bottle->stored <= 0 : bottle->sips <= 0) || ai->timer > FETCH_TIMEOUT ||
         ai_skipped(ai, bottle)) { /* gone, or can't get at it */
         stand(self);
         giveUpBottle(ai);
@@ -752,6 +757,11 @@ static void updateFetch(Object *self, Object *const *objects, int count, float d
     if (Hands_gapFrom(self->position, self->look.height, bottle) < PICKUP_REACH) {
         stand(self);
         turnTowards(self, vec3_sub(bottle->position, self->position), dt);
+        if (crate) { /* one out of the crate (car.c Car_serve: then that's the one) */
+            Hands_touch(self, Object_colliderCenter(bottle, 0));
+            ai->take_from = bottle;
+            return;
+        }
         if (Hands_pickUpObject(self, self->main_hand, bottle)) {
             ai->mode = AI_DRINK;
             ai->timer = 0.0f;
@@ -1665,6 +1675,85 @@ static bool fetchPullover(Object *self, Object *const *objects, int count, float
     return true;
 }
 
+/* Unloading the car (after the arrival): to the boot, the next thing out of
+   it, carried to where it goes (crates by the fire, the camping things where
+   the tents will be), put down; again, till the boot is empty. */
+#define UNLOAD_TRIP 70.0f /* s on one trip: stuck, give it up */
+
+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;
+    }
+    return NULL;
+}
+
+static bool goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int count, float dt) {
+    float d = flatDistance(p, self->position);
+    if (d < reach) { stand(self); return true; }
+    Vec3 dir = ai_steer(self, NULL, objects, count, p);
+    if (vec3_length(dir) < 0.5f) { stand(self); return false; }
+    ai_walk(self, dir, fminf(WALK_SPEED, d * 2.0f + 0.3f), dt);
+    return false;
+}
+
+static bool unloadCar(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    Object *car = ai->unload_car;
+    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;
+    if (!item && !waiting && empty) {
+        ai->unload = false;
+        if (random01(ai) < 0.5f) Ai_say(self, "That's everything out of the car.");
+        return false;
+    }
+    if ((ai->unload_time += dt) > UNLOAD_TRIP) {
+        for (int h = 0; h < 2; h++) if (self->hands.item[h] == item && item) Hands_drop(self, h);
+        ai->unload = false;
+        return false;
+    }
+    if (self->sitting) self->sitting = false;
+    if (item) { /* carrying it there */
+        for (int h = 0; h < 2; h++) if (self->hands.item[h] == item && self->hands.state[h] == HAND_REACHING) { stand(self); return true; }
+        Vec3 spot = Car_dropSpot(g_world, item);
+        Vec3 from = vec3_sub(self->position, spot);
+        from.y = 0.0f;
+        from = vec3_length(from) > 0.1f ? vec3_normalize(from) : vec3_scale(Object_facing(self), -1.0f);
+        if (!goToPoint(self, vec3_add(spot, vec3_scale(from, 0.5f)), 0.25f, objects, count, dt)) return true; /* (the hands over the spot) */
+        turnTowards(self, vec3_scale(from, -1.0f), dt);
+        for (int h = 0; h < 2; h++) if (self->hands.item[h] == item) Hands_drop(self, h);
+        item->position.x = spot.x; /* (right there: side by side, not on top of each other) */
+        item->position.z = spot.z;
+        Object_syncColliders(item);
+        ai->target = NULL;
+        ai->unload_time = 0.0f;
+        return true;
+    }
+    if (waiting) { /* out of the boot, lying there: take it */
+        Object *it = ai->target;
+        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->target = NULL;
+            return true;
+        }
+        goToPoint(self, it->position, 0.3f, objects, count, dt);
+        return true;
+    }
+    /* to the boot (a little to one side: room for the others) */
+    Vec3 back = vec3_sub(Car_bootSpot(car), car->position), across = vec3_normalize(vec3(-back.z, 0.0f, back.x));
+    Vec3 spot = vec3_add(Car_bootSpot(car), vec3_scale(across, ai->side * 0.4f));
+    if (!goToPoint(self, spot, 0.45f, objects, count, dt)) return true;
+    turnTowards(self, vec3_sub(car->position, self->position), dt);
+    freeHands(self, NULL);
+    if (car->boot_open && !ai->take_from) { ai->take_from = car; Hands_touch(self, vec3_add(car->position, vec3_scale(back, 0.45f))); }
+    return true;
+}
+
 /* A finished cigarette in the fingers (the tidy): to the tin, a bin or the
    fire with it; none near: somebody's pocket ashtray (asked for), else wrapped
    up and into the pocket. Butts in the pocket (ashtray) go into the tin, a
@@ -1799,6 +1888,8 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_HELP) && ai_holdLadder(self, objects, count, dt)) return;
     /* Done with a tool: careful people put it back where it belongs, the others leave it lying */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && ai_putAway(self, objects, count, dt)) return;
+    /* Unloading the car */
+    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && unloadCar(self, objects, count, dt)) return;
     /* A finished cigarette in the fingers: to the tin, a bin, the fire */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && binButt(self, objects, count, dt)) return;
     /* Cold, the pullover put down somewhere: off to get it */
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index 72a3659..2d68ede 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -176,6 +176,10 @@ typedef struct {
     bool           want_smokes; /* out of cigarettes, nobody gives one: off to the machine */
     float          smokes_time; /* ... on the way this long */
     bool           asked_you;   /* asked the player for one (nothing came of it: the machine next time) */
+    struct Object *take_from;   /* a bottle from this crate, or the next thing out of this car's boot: please (see car.c Car_serve) */
+    bool           unload;      /* unloading the car (ai.c unloadCar) ... */
+    struct Object *unload_car;
+    float          unload_time; /* ... on this trip this long */
     float          butt_time;   /* holding a finished cigarette (Object.butt_held) this long ... */
     bool           butt_asked;  /* ... and asked somebody for their pocket ashtray already */
     int            asking_you;  /* asked the player for this (PocketNeed: a paper, a filter, tobacco), 0: nothing */
diff --git a/entropie/src/people/hands.c b/entropie/src/people/hands.c
index 2a37649..5cfa984 100644
--- a/entropie/src/people/hands.c
+++ b/entropie/src/people/hands.c
@@ -167,6 +167,10 @@ void Hands_drop(Object *who, int hand) {
     }
     item->is_held  = false;
     item->velocity = vec3(who->velocity.x, 0.0f, who->velocity.z);
+    if (item->type == OBJ_CRATE) { /* (set down standing, not tipped over) */
+        item->rotation = vec3(0.0f, who->rotation.y, 0.0f);
+        Object_syncColliders(item);
+    }
     if (item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER) { /* put down flat, where it was */
         Vec3 center = Object_colliderCenter(item, 0);
         item->rotation = vec3(PI_F / 2.0f, who->rotation.y, 0.0f);
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index e1d422f..e68c8ed 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -126,6 +126,21 @@ Mesh  Shapes_furniture(FurnitureKind kind, Vec3 size);
 Mesh  Shapes_ashtray(Vec3 size, int butts);
 /* Ivy up a wall (at z = 0, facing +Z), this wide and high. */
 Mesh  Shapes_ivy(float width, float height, unsigned seed);
+/* Your estate car standing on y = 0, nose to +Z (CAR_WIDTH x about 1.45 m x CAR_LENGTH); the boot open:
+   the hatch up. */
+#define CAR_WIDTH  1.78f
+#define CAR_LENGTH 4.6f
+Mesh  Shapes_car(Vec3 color, bool boot_open);
+/* ... seen from the driver's seat: the bonnet, the dashboard, the wheel, the pillars, the roof. */
+Mesh  Shapes_carInside(Vec3 color);
+/* A crate of bottles (centred): variant 0 beer, 1 water; this many bottles in it. */
+#define CRATE_SIZE_X 0.40f
+#define CRATE_SIZE_Y 0.30f
+#define CRATE_SIZE_Z 0.30f
+Mesh  Shapes_crate(int variant, int bottles);
+/* 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);
 /* A pullover put down (see clothes.h), its colour: lay 0 spread out flat on the ground (y = 0 under it,
    the neck to +Z, sleeves out), 1 hung over an edge along X (y = 0 its top; gap: how thick that edge is),
    2 folded up (y = 0 under it). */
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 8548503..e72d0e5 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -777,6 +777,115 @@ static void addWheel(MeshBuilder *b, Vec3 center, float radius, float width) {
     transformSince(b, first, mat4_mul(mat4_translate(center), mat4_rotate_z(PI_F / 2.0f)));
 }
 
+/* Your estate car (see shapes.h). */
+Mesh Shapes_car(Vec3 color, bool boot_open) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const float w = CAR_WIDTH, l = CAR_LENGTH, wheel = 0.31f, back = -l * 0.5f;
+    const Vec3 glass = { 0.30f, 0.40f, 0.52f }, dark = { 0.10f, 0.10f, 0.11f }, trim = vec3_scale(color, 0.7f);
+    addBlock(&b, vec3(0.0f, 0.62f, 0.0f), vec3(w, 0.6f, l), color);                                       /* the body */
+    addBlock(&b, vec3(0.0f, 0.35f, 0.0f), vec3(w * 0.96f, 0.1f, l * 0.98f), dark);                        /* under it */
+    const float cab0 = back + 0.12f, cab1 = 0.45f;                                                      /* the cabin, right to the back: an estate */
+    addBlock(&b, vec3(0.0f, 1.18f, (cab0 + cab1) * 0.5f), vec3(w * 0.9f, 0.54f, cab1 - cab0), color);
+    addBlock(&b, vec3(0.0f, 1.2f, (cab0 + cab1) * 0.5f + 0.05f), vec3(w * 0.91f, 0.34f, cab1 - cab0 - 0.3f), glass); /* side windows */
+    size_t first = b.vertex_count;                                                                      /* the windscreen, leaning back */
+    addBlock(&b, vec3(0.0f, 0.0f, 0.0f), vec3(w * 0.86f, 0.58f, 0.03f), glass);
+    transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, 1.16f, cab1 + 0.12f)), mat4_rotate_x(0.55f)));
+    addBlock(&b, vec3(0.0f, 1.2f, 0.4f), vec3(w * 0.92f, 0.04f, 0.05f), trim);                            /* door pillars */
+    addBlock(&b, vec3(0.0f, 1.2f, -0.65f), vec3(w * 0.92f, 0.34f, 0.07f), trim);
+    addBlock(&b, vec3(0.0f, 0.55f, l * 0.5f + 0.03f), vec3(w * 0.92f, 0.13f, 0.07f), dark);              /* bumpers */
+    addBlock(&b, vec3(0.0f, 0.55f, back - 0.03f), vec3(w * 0.92f, 0.13f, 0.07f), dark);
+    for (int s = -1; s <= 1; s += 2) {
+        addBlock(&b, vec3((float)s * w * 0.36f, 0.75f, l * 0.5f + 0.005f), vec3(0.26f, 0.1f, 0.02f), (Vec3){ 0.95f, 0.93f, 0.80f }); /* headlights */
+        addBlock(&b, vec3((float)s * w * 0.42f, 0.95f, back - 0.005f), vec3(0.14f, 0.2f, 0.02f), (Vec3){ 0.75f, 0.12f, 0.10f });  /* tail lights */
+        addBlock(&b, vec3((float)s * (w * 0.5f + 0.06f), 1.0f, 0.35f), vec3(0.1f, 0.08f, 0.14f), dark);                           /* mirrors */
+    }
+    for (int k = 0; k < 4; k++)
+        addWheel(&b, vec3(k % 2 ? w * 0.43f : -w * 0.43f, wheel, k / 2 ? l * 0.31f : -l * 0.31f), wheel, 0.22f);
+    if (boot_open) { /* the hatch up (hinged at the roof's back edge), the boot open: dark inside */
+        addBlock(&b, vec3(0.0f, 1.0f, back + 0.005f), vec3(w * 0.82f, 0.62f, 0.02f), dark);
+        first = b.vertex_count;
+        addBlock(&b, vec3(0.0f, 0.0f, -0.36f), vec3(w * 0.86f, 0.04f, 0.72f), color);
+        addBlock(&b, vec3(0.0f, -0.025f, -0.38f), vec3(w * 0.72f, 0.02f, 0.5f), glass);
+        transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, 1.45f, cab0)), mat4_rotate_x(-0.35f)));
+    } else {
+        addBlock(&b, vec3(0.0f, 1.0f, back + 0.005f), vec3(w * 0.86f, 0.62f, 0.02f), color);
+        addBlock(&b, vec3(0.0f, 1.2f, cab0 - 0.005f), vec3(w * 0.72f, 0.3f, 0.02f), glass); /* the back window */
+    }
+    return Mesh_fromBuilder(&b);
+}
+
+/* The car seen from the driver's seat (the body round you would hide it all):
+   the bonnet, the dashboard, the wheel, the pillars and the roof. */
+Mesh Shapes_carInside(Vec3 color) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const float w = CAR_WIDTH, l = CAR_LENGTH;
+    const Vec3 dash = { 0.16f, 0.16f, 0.17f }, roof = { 0.62f, 0.60f, 0.56f };
+    addBlock(&b, vec3(0.0f, 0.84f, (0.8f + l * 0.5f) * 0.5f), vec3(w, 0.06f, l * 0.5f - 0.8f), color);      /* the bonnet */
+    addBlock(&b, vec3(0.0f, 0.86f, 0.66f), vec3(w * 0.92f, 0.12f, 0.3f), dash);                             /* the dashboard */
+    size_t first = b.vertex_count;                                                                          /* the wheel, the driver's side */
+    shapes_addTube(&b, -0.02f, 0.19f, 0.19f, 0.03f, 18, dash, false);
+    shapes_addTube(&b, -0.02f, 0.03f, 0.03f, 0.04f, 8, dash, true);
+    transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.38f, 0.95f, 0.42f)), mat4_rotate_x(PI_F * 0.5f - 0.45f)));
+    for (int s = -1; s <= 1; s += 2) { /* the pillars beside the windscreen */
+        first = b.vertex_count;
+        addBlock(&b, vec3(0.0f, 0.3f, 0.0f), vec3(0.05f, 0.62f, 0.05f), color);
+        transformSince(&b, first, mat4_mul(mat4_translate(vec3((float)s * w * 0.44f, 0.9f, 0.8f)), mat4_rotate_x(-0.55f)));
+    }
+    addBlock(&b, vec3(0.0f, 1.5f, -0.9f), vec3(w * 0.92f, 0.04f, 2.2f), roof);                            /* the roof lining */
+    addBlock(&b, vec3(0.0f, 1.47f, 0.2f), vec3(0.25f, 0.05f, 0.06f), dash);                                /* the mirror */
+    return Mesh_fromBuilder(&b);
+}
+
+/* A crate of bottles, centred: beer (brown bottles, gold caps, a red crate)
+   or water (green glass, blue caps, a blue crate); the first `bottles` slots full. */
+Mesh Shapes_crate(int variant, int bottles) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const float x = CRATE_SIZE_X, h = CRATE_SIZE_Y, z = CRATE_SIZE_Z, wall = 0.015f;
+    const Vec3 crate = variant == 0 ? (Vec3){ 0.62f, 0.16f, 0.12f } : (Vec3){ 0.22f, 0.42f, 0.72f };
+    const Vec3 glass = variant == 0 ? (Vec3){ 0.38f, 0.22f, 0.08f } : (Vec3){ 0.55f, 0.75f, 0.62f };
+    const Vec3 cap   = variant == 0 ? (Vec3){ 0.85f, 0.70f, 0.25f } : (Vec3){ 0.20f, 0.35f, 0.75f };
+    const Vec3 hole  = vec3_scale(crate, 0.45f);
+    addBlock(&b, vec3(0.0f, -h * 0.5f + 0.01f, 0.0f), vec3(x, 0.02f, z), crate);
+    addBlock(&b, vec3(0.0f, 0.0f, z * 0.5f - wall * 0.5f), vec3(x, h, wall), crate);
+    addBlock(&b, vec3(0.0f, 0.0f, -z * 0.5f + wall * 0.5f), vec3(x, h, wall), crate);
+    addBlock(&b, vec3(x * 0.5f - wall * 0.5f, 0.0f, 0.0f), vec3(wall, h, z), crate);
+    addBlock(&b, vec3(-x * 0.5f + wall * 0.5f, 0.0f, 0.0f), vec3(wall, h, z), crate);
+    for (int s = -1; s <= 1; s += 2) /* the handle holes in the ends */
+        addBlock(&b, vec3((float)s * (x * 0.5f + 0.001f), h * 0.32f, 0.0f), vec3(0.004f, 0.035f, 0.11f), hole);
+    const int cols = variant == 0 ? 5 : 4, rows = variant == 0 ? 4 : 3;
+    for (int k = 0; k < bottles && k < cols * rows; k++) {
+        float cx = -x * 0.5f + x * ((float)(k % cols) + 0.5f) / (float)cols, cz = -z * 0.5f + z * ((float)(k / cols) + 0.5f) / (float)rows;
+        size_t first = b.vertex_count;
+        shapes_addTube(&b, h * 0.5f - 0.07f, 0.024f, 0.012f, 0.1f, 6, glass, false); /* the neck out of the top */
+        shapes_addTube(&b, h * 0.5f + 0.03f, 0.014f, 0.014f, 0.012f, 6, cap, true);
+        transformSince(&b, first, mat4_translate(vec3(cx, 0.0f, cz)));
+    }
+    return Mesh_fromBuilder(&b);
+}
+
+/* Camping things, centred, lying along X (see shapes.h). */
+Mesh Shapes_gear(int kind, Vec3 color) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const float len[] = { 0.42f, 0.6f, 0.5f, 0.6f }, rad[] = { 0.13f, 0.09f, 0.08f, 0.1f };
+    float l = len[kind & 3], r = rad[kind & 3];
+    Vec3 dark = vec3_scale(color, 0.65f);
+    size_t first = b.vertex_count;
+    shapes_addTube(&b, -l * 0.5f, r, r, l, 12, color, true);
+    if (kind == 1) { /* a foam mat rolled up: two straps */
+        shapes_addTube(&b, -l * 0.3f, r * 1.04f, r * 1.04f, 0.03f, 12, dark, false);
+        shapes_addTube(&b, l * 0.27f, r * 1.04f, r * 1.04f, 0.03f, 12, dark, false);
+    } else { /* a stuff sack: the drawstring end gathered, a toggle */
+        shapes_addTube(&b, l * 0.5f, r * 0.8f, r * 0.25f, 0.04f, 12, dark, true);
+        shapes_addTube(&b, l * 0.5f + 0.04f, 0.015f, 0.015f, 0.03f, 6, (Vec3){ 0.1f, 0.1f, 0.1f }, true);
+    }
+    transformSince(&b, first, mat4_rotate_z(-PI_F * 0.5f));
+    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
new file mode 100644
index 0000000..9c7ce3f
--- /dev/null
+++ b/entropie/src/world/car.c
@@ -0,0 +1,212 @@
+#include "world/car.h"
+#include "world/site.h"
+#include "render/shapes.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define SEAT_X     0.38f  /* seats this far left (+X: the driver's side) or right of the middle */
+#define SEAT_Z    -0.95f  /* the back seats this far back from the front ones */
+#define EYE_UP     1.18f  /* the eyes this high, sitting */
+#define DOOR_OUT   0.6f   /* out of the door: standing this far beside the car */
+#define BOOT_BACK  0.8f   /* standing this far behind it to reach into the boot */
+#define SET_DOWN   0.3f   /* what comes out is set down this far behind it */
+#define BOTTLE_KG  0.6f
+
+static const Vec3 CRATE_BOTTLE = { 0.07f, 0.25f, 0.0f };
+
+/* Local (nose to +Z, the driver's side +X) to world, on the car's ground. */
+static Vec3 local(const Object *car, Vec3 p) {
+    return vec3_add(car->position, mat4_mul_dir(mat4_rotate_y(car->rotation.y), p));
+}
+
+Object *Car_create(Vec3 position, float yaw) {
+    Object *car = Object_create(OBJ_CAR, PALETTE_STANDARD, position, vec3(CAR_WIDTH, 1.45f, CAR_LENGTH), 1300.0f);
+    if (!car) return NULL;
+    car->load = calloc(1, sizeof(*car->load));
+    snprintf(car->name, sizeof(car->name), "Car");
+    Object_setRotation(car, vec3(0.0f, yaw, 0.0f));
+    return car;
+}
+
+bool Car_load(Object *car, CargoKind kind, Object *owner) {
+    CarLoad *l = car->load;
+    if (!l || l->n >= CAR_LOAD_MAX) return false;
+    l->kind[l->n] = kind;
+    l->owner[l->n] = owner;
+    l->n++;
+    return true;
+}
+
+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",
+};
+
+const char *Car_nextName(const Object *car) {
+    const CarLoad *l = car->load;
+    return l && l->n > 0 ? CARGO_NAME[l->kind[l->n - 1]] : NULL;
+}
+
+bool Car_nextHeavy(const Object *car) {
+    const CarLoad *l = car->load;
+    return l && l->n > 0 && (l->kind[l->n - 1] == CARGO_BEER || l->kind[l->n - 1] == CARGO_WATER || l->kind[l->n - 1] == CARGO_TENT_BIG);
+}
+
+/* Everybody's own colours for their things (by name: you, Volkram, Jana, Toni, anybody else). */
+static int whose(const Object *owner) {
+    if (!owner || owner->type == OBJ_PLAYER) return 0;
+    if (!strcmp(owner->name, "Volkram")) return 1;
+    if (!strcmp(owner->name, "Jana")) return 2;
+    if (!strcmp(owner->name, "Toni")) return 3;
+    return 4;
+}
+
+static Vec3 gearColor(GearKind kind, const Object *owner) {
+    static const Vec3 BAGS[] = { { 0.70f, 0.18f, 0.16f }, { 0.20f, 0.36f, 0.22f }, { 0.48f, 0.26f, 0.58f }, { 0.88f, 0.48f, 0.12f }, { 0.4f, 0.4f, 0.45f } };
+    static const Vec3 MATS[] = { { 0.20f, 0.40f, 0.75f }, { 0.85f, 0.75f, 0.20f }, { 0.25f, 0.65f, 0.60f }, { 0.75f, 0.30f, 0.45f }, { 0.5f, 0.5f, 0.5f } };
+    switch (kind) {
+    case GEAR_SLEEPING_BAG: return BAGS[whose(owner)];
+    case GEAR_MAT:          return MATS[whose(owner)];
+    case GEAR_TENT_SMALL:   return vec3(0.30f, 0.45f, 0.25f);
+    case GEAR_TENT_BIG:     return vec3(0.42f, 0.42f, 0.24f);
+    }
+    return vec3(0.5f, 0.5f, 0.5f);
+}
+
+Object *Car_takeOut(World *world, Object *car) {
+    CarLoad *l = car->load;
+    if (!l || l->n == 0 || !car->boot_open) return NULL;
+    CargoKind kind = l->kind[l->n - 1];
+    Object *owner = l->owner[l->n - 1];
+    Vec3 at = local(car, vec3(0.0f, 0.0f, -CAR_LENGTH * 0.5f - SET_DOWN));
+    at.y = World_groundHeight(world, at.x, at.z, at.y + 1.0f) + 0.02f;
+    Object *item;
+    if (kind == CARGO_BEER || kind == CARGO_WATER) {
+        item = Object_create(OBJ_CRATE, PALETTE_STANDARD, at, vec3_zero(), kind == CARGO_WATER ? 1.0f : 0.0f);
+    } 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;
+        item = Object_create(OBJ_GEAR, PALETTE_STANDARD, at, vec3_zero(), (float)g);
+        if (item) {
+            item->owner = owner;
+            Mesh_destroy(&item->mesh);
+            item->mesh = Shapes_gear((int)g, gearColor(g, owner));
+        }
+    }
+    if (!item) return NULL;
+    item->rotation.y = car->rotation.y + PI_F * 0.5f; /* (lying across the back of it) */
+    Object_syncColliders(item);
+    if (!World_add(world, item)) return NULL;
+    l->n--;
+    return item;
+}
+
+Vec3 Car_bootSpot(const Object *car) {
+    return local(car, vec3(0.0f, 0.0f, -CAR_LENGTH * 0.5f - BOOT_BACK));
+}
+
+static Vec3 seatAt(int seat) {
+    return vec3(seat % 2 == 0 ? SEAT_X : -SEAT_X, 0.0f, seat >= 2 ? SEAT_Z : 0.0f);
+}
+
+Vec3 Car_eye(const Object *car, int seat) {
+    Vec3 s = seatAt(seat);
+    s.y = EYE_UP;
+    return local(car, s);
+}
+
+Vec3 Car_door(const Object *car, int seat) {
+    Vec3 s = seatAt(seat);
+    s.x = (s.x > 0.0f ? 1.0f : -1.0f) * (CAR_WIDTH * 0.5f + DOOR_OUT);
+    return local(car, s);
+}
+
+void Car_place(World *world, Object *car, Vec3 at, float yaw) {
+    at.y = world->has_terrain ? Terrain_height(&world->terrain, at.x, at.z) : at.y;
+    car->position = at;
+    car->rotation = vec3(0.0f, yaw, 0.0f);
+    Object_syncColliders(car);
+    for (int i = 0; i < world->count; i++) {
+        Object *o = world->objects[i];
+        if (o->riding != car) continue;
+        Vec3 p = local(car, seatAt(o->ride_seat));
+        o->position = vec3(p.x, at.y + 0.3f, p.z);
+        o->rotation.y = yaw;
+        o->velocity = vec3_zero();
+        Object_syncColliders(o);
+    }
+}
+
+void Car_getIn(Object *car, Object *who, int seat) {
+    who->riding = car;
+    who->ride_seat = seat;
+    who->velocity = vec3_zero();
+    who->movement = vec3_zero();
+    who->sitting = false;
+}
+
+void Car_getOut(Object *who) {
+    Object *car = who->riding;
+    if (!car) return;
+    Vec3 door = Car_door(car, who->ride_seat);
+    who->riding = NULL;
+    who->position = vec3(door.x, car->position.y, door.z);
+    Vec3 out = vec3_sub(door, car->position);
+    who->rotation.y = atan2f(out.z, out.x) + PI_F / 2.0f; /* (facing away from the car) */
+    who->velocity = vec3_zero();
+    who->body.replant = true;
+    Object_syncColliders(who);
+}
+
+void Car_openBoot(Object *car, bool open) {
+    if (car->boot_open == open) return;
+    car->boot_open = open;
+    Mesh_destroy(&car->mesh);
+    car->mesh = Shapes_car(vec3(0.24f, 0.34f, 0.52f), open);
+}
+
+/* Where the camping things of this many go: by whose tent (you, Volkram and Jana, Toni). */
+static int campOf(const Object *owner) {
+    int w = whose(owner);
+    return w == 1 || w == 2 ? 1 : w == 3 ? 2 : 0;
+}
+
+Vec3 Car_dropSpot(const World *world, const Object *item) {
+    if (item->type == OBJ_CRATE) {
+        Vec3 base = Site_drinksSpot(), along = Site_campAlong();
+        int n = 0;
+        for (int i = 0; i < world->count; i++) {
+            const Object *o = world->objects[i];
+            if (o != item && o->type == OBJ_CRATE && !o->is_held && vec3_length(vec3_sub(o->position, base)) < 1.6f) n++;
+        }
+        return vec3_add(base, vec3_scale(along, 0.5f * (float)n));
+    }
+    Vec3 along = Site_campAlong(), across = vec3(-along.z, 0.0f, along.x);
+    Vec3 at = Site_campSpot(campOf(item->owner));
+    float a = item->variant == GEAR_SLEEPING_BAG ? 0.0f : item->variant == GEAR_MAT ? 0.55f : 1.1f;
+    float b = whose(item->owner) == 2 ? 0.7f : 0.0f; /* (Jana's beside Volkram's) */
+    return vec3_add(at, vec3_add(vec3_scale(along, a), vec3_scale(across, b)));
+}
+
+Object *Crate_takeBottle(World *world, Object *crate) {
+    if (crate->type != OBJ_CRATE || crate->stored <= 0) return NULL;
+    Vec3 top = vec3_add(Object_colliderCenter(crate, 0), vec3(0.0f, CRATE_SIZE_Y * 0.5f + 0.16f, 0.0f));
+    Object *b = Object_create(crate->variant ? OBJ_WATER : OBJ_BOTTLE, PALETTE_WARM, top, CRATE_BOTTLE, BOTTLE_KG);
+    if (!b || !World_add(world, b)) return NULL;
+    crate->stored--;
+    crate->weight = fmaxf(2.0f, crate->weight - (crate->variant ? 1.2f : 0.85f));
+    return b;
+}
+
+void Car_serve(World *world) {
+    for (int i = 0; i < world->count; i++) {
+        Object *o = world->objects[i];
+        if (!o->is_character || !o->ai.take_from) continue;
+        Object *from = o->ai.take_from;
+        o->ai.take_from = NULL;
+        Object *got = from->type == OBJ_CRATE ? Crate_takeBottle(world, from) : from->type == OBJ_CAR ? Car_takeOut(world, from) : NULL;
+        if (got) o->ai.target = got;
+    }
+}
diff --git a/entropie/src/world/car.h b/entropie/src/world/car.h
new file mode 100644
index 0000000..a98bfb6
--- /dev/null
+++ b/entropie/src/world/car.h
@@ -0,0 +1,57 @@
+#ifndef WORLD_CAR_H
+#define WORLD_CAR_H
+
+/*
+ * 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
+ * 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).
+ */
+
+#include "world/world.h"
+
+#define CRATE_BEER  20  /* bottles in a crate of beer */
+#define CRATE_WATER 12  /* ... of water */
+
+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 } CargoKind;
+
+#define CAR_LOAD_MAX 20
+typedef struct CarLoad {
+    int       n;
+    CargoKind kind[CAR_LOAD_MAX];
+    Object   *owner[CAR_LOAD_MAX];
+} CarLoad;
+
+/* A car with an empty boot (shut). */
+Object     *Car_create(Vec3 position, float yaw);
+bool        Car_load(Object *car, CargoKind kind, Object *owner);
+/* What comes out next ("A crate of beer"), NULL: the boot is empty. */
+const char *Car_nextName(const Object *car);
+/* ... and whether it takes both hands. */
+bool        Car_nextHeavy(const Object *car);
+/* The next thing out of the (open) boot, lying at its edge: NULL if empty or shut. */
+Object     *Car_takeOut(World *world, Object *car);
+/* Where one stands to reach into the boot (behind it). */
+Vec3        Car_bootSpot(const Object *car);
+/* Seat k (0 the driver, 1 beside, 2 and 3 behind): the eye, and where they stand once out. */
+Vec3        Car_eye(const Object *car, int seat);
+Vec3        Car_door(const Object *car, int seat);
+/* Moves it (and whoever rides in it) here, facing yaw (rotation.y), on the ground. */
+void        Car_place(World *world, Object *car, Vec3 at, float yaw);
+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. */
+Vec3        Car_dropSpot(const World *world, const Object *item);
+
+/* A bottle out of a crate (on top of it, for the taking): NULL if it's empty. */
+Object     *Crate_takeBottle(World *world, Object *crate);
+/* Every frame: whoever asked for a bottle from a crate or the next thing from
+   the boot (ai.take_from) gets it lying there for them (ai.target). */
+void        Car_serve(World *world);
+
+#endif /* WORLD_CAR_H */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 1907b88..74f3036 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -1,4 +1,5 @@
 #include "world/object.h"
+#include "world/car.h"
 
 #include <stdio.h>
 #include <stdlib.h>
@@ -218,8 +219,11 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         obj->grill_mesh = Shapes_grill();
         obj->is_static = true;
         obj->pivot     = vec3_zero();
-        /* Low enough to step over; wood put in rests on the ashes */
-        addCollider(obj, vec3(0.0f, 0.03f, 0.0f), vec3(size.x, 0.06f, size.x), vec3_zero());
+        /* Low enough to step over; wood put in rests on the ashes. Inside the
+           ring of stones (its corners at the stones: a square as wide as the
+           ring stuck out past them, and people beside the fire stepped on and
+           off its corners, jittering) */
+        addCollider(obj, vec3(0.0f, 0.03f, 0.0f), vec3(size.x * 0.6f, 0.06f, size.x * 0.6f), vec3_zero());
         break;
 
     case OBJ_TRASH:
@@ -347,6 +351,40 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         break;
     }
 
+    case OBJ_CAR: { /* (see car.c Car_create) */
+        const float back = -size.z * 0.5f;
+        obj->mesh      = Shapes_car(vec3(0.24f, 0.34f, 0.52f), false);
+        obj->inside    = Shapes_carInside(vec3(0.24f, 0.34f, 0.52f));
+        obj->is_static = true;
+        obj->weight    = 1300.0f;
+        addCollider(obj, vec3(0.0f, 0.62f, 0.0f), vec3(size.x, 0.74f, size.z), vec3_zero());
+        addCollider(obj, vec3(0.0f, 1.2f, (back + 0.12f + 0.45f) * 0.5f), vec3(size.x * 0.9f, 0.5f, 0.45f - back - 0.12f), vec3_zero());
+        break;
+    }
+
+    case OBJ_CRATE:
+        obj->variant = weight > 0.5f ? 1 : 0;
+        obj->stored  = obj->variant ? CRATE_WATER : CRATE_BEER;
+        obj->size    = vec3(CRATE_SIZE_X, CRATE_SIZE_Y, CRATE_SIZE_Z);
+        obj->weight  = 2.0f + (float)obj->stored * (obj->variant ? 1.2f : 0.85f);
+        obj->mesh    = Shapes_crate(obj->variant, obj->stored);
+        obj->shown   = obj->stored;
+        obj->pivot   = vec3(0.0f, CRATE_SIZE_Y * 0.5f, 0.0f);
+        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
+        break;
+
+    case OBJ_GEAR: { /* kind = weight; its colour: see car.c */
+        static const Vec3  SIZE[] = { { 0.42f, 0.26f, 0.26f }, { 0.6f, 0.18f, 0.18f }, { 0.5f, 0.16f, 0.16f }, { 0.6f, 0.2f, 0.2f } };
+        static const float KG[]   = { 1.6f, 0.6f, 2.4f, 4.5f };
+        obj->variant = (int)weight & 3;
+        obj->size    = SIZE[obj->variant];
+        obj->weight  = KG[obj->variant];
+        obj->mesh    = Shapes_gear(obj->variant, vec3(0.5f, 0.5f, 0.5f));
+        obj->pivot   = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
+        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
+        break;
+    }
+
     case OBJ_CLOTHES: /* (made in clothes.c, its mesh given) */
         obj->is_static = true;
         break;
@@ -495,6 +533,8 @@ void Object_destroy(Object *obj) {
     Mesh_destroy(&obj->mesh);
     Mesh_destroy(&obj->flames);
     Mesh_destroy(&obj->grill_mesh);
+    Mesh_destroy(&obj->inside);
+    free(obj->load);
     free(obj);
 }
 
@@ -1219,6 +1259,21 @@ void Object_describe(const Object *obj, char *out, size_t size) {
                               : "Ladder (slid together) - left click: slide it out, then lean it against something");
         break;
     case OBJ_STONE: snprintf(out, size, kg >= 1 ? "A stone - %d kg" : "A pebble", kg); break;
+    case OBJ_CAR:
+        if (!obj->load || obj->load->n == 0) snprintf(out, size, "Your car - the boot is empty");
+        else if (!obj->boot_open) snprintf(out, size, "Your car - full of your things");
+        else snprintf(out, size, "Your car - left click at the boot: take out %s", Car_nextName(obj));
+        break;
+    case OBJ_CRATE:
+        snprintf(out, size, "Crate of %s - %d bottle%s left%s", obj->variant ? "water" : "beer", obj->stored, obj->stored == 1 ? "" : "s",
+                 obj->stored > 0 && !obj->is_held ? " (left click: take one)" : "");
+        break;
+    case OBJ_GEAR: {
+        static const char *const WHAT[] = { "sleeping bag", "sleeping mat", "tent", "tent (the big one)" };
+        if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s", WHAT[obj->variant & 3]);
+        else snprintf(out, size, "%s's %s", obj->owner ? obj->owner->name : "Somebody", WHAT[obj->variant & 3]);
+        break;
+    }
     case OBJ_CLOTHES:
         if (obj->cloth_of && obj->cloth_of->type == OBJ_PLAYER) snprintf(out, size, "Your pullover - click: take it");
         else snprintf(out, size, "%s's pullover", obj->cloth_of ? obj->cloth_of->name : "Somebody");
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 718d281..9f05da7 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -82,6 +82,10 @@ typedef enum {
     OBJ_STRUCTURE,       /* static: how a building made of parts looks (one mesh, no collision: see OBJ_WALL) */
     OBJ_TOY,             /* Alice's toys: weight 0 = a ball (size.x across), 1 = a rope to bite (size.x long, size.y thick) */
     OBJ_CLOTHES,         /* somebody's pullover put down (see clothes.h): variant = ClothLay; no collision */
+    OBJ_CAR,             /* static (moved by hand while it drives): your estate car, size = (width, height, length), nose to +Z;
+                            what's in its boot: load (see car.h) */
+    OBJ_CRATE,           /* a crate of bottles: weight 0 = beer (CRATE_BEER bottles), 1 = water (CRATE_WATER); stored = bottles in it */
+    OBJ_GEAR,            /* camping things: variant = GearKind (see car.h), owner = whose */
 } ObjectType;
 
 /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */
@@ -153,6 +157,12 @@ typedef struct Object {
     float      nourish;       /* hunger taken away per bite */
     float      refresh;       /* stamina given back per sip / bite */
     ObjectType dispenses;     /* supplies: what you get from it */
+    struct CarLoad *load;     /* cars: what's in the boot (malloc'd, freed with the car) */
+    bool       boot_open;     /* cars: the boot lid up */
+    Mesh       inside;        /* cars: what you see of it from the driver's seat */
+    struct Object *owner;     /* camping gear: whose */
+    struct Object *riding;    /* characters: sitting in this car (hidden, no physics, no AI: see car.c) ... */
+    int        ride_seat;     /* ... on this seat (0 the driver) */
     int        branches;      /* trees: branches left on the crown */
     int        stored;        /* woodpiles: pieces stacked on it; firewood: 1 = lies on a woodpile */
     float      work;          /* trees and logs: work done on the current job (see tools.h) */
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index b901075..ad9f3cf 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -1076,7 +1076,7 @@ void Physics_step(Object *obj, Object *const *objects, int count, const Terrain
     if (obj->over_t > 0.0f) { stepOverMove(obj, terrain, dt); return; } /* (a set move) */
 
     /* At rest: skip most steps (a push or a fall wakes it) */
-    if (obj->climbing || obj->on_grill || obj->toilet) return; /* moved by hand, or out of sight */
+    if (obj->climbing || obj->on_grill || obj->toilet || obj->riding) return; /* moved by hand, or out of sight */
     if (obj->sitting && obj->sit_moving && obj->seat) return; /* (stepping over to a seat, turning, back onto it: a set move) */
     bool still = !obj->is_character && !obj->lifted && !obj->thrown_by && obj->is_grounded &&
                  vec3_length(obj->movement) < 0.01f &&
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index 3b81b87..6fd60cf 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -525,6 +525,26 @@ Vec3 Site_groundColor(float x, float z, Vec3 meadow) {
 
 /* ---------- Building it ---------- */
 
+Vec3 Site_court(float a, float b) { return court(a, b); }
+
+Vec3 Site_drinksSpot(void) { return court(FIRE_A + 4.5f, FIRE_B + 3.0f); }
+
+Vec3 Site_campSpot(int k) {
+    static const float AT[3][2] = { { 19.0f, 35.0f }, { 42.0f, 35.0f }, { 30.0f, 33.5f } }; /* (clear of the two apple trees) */
+    k = k < 0 ? 0 : k > 2 ? 2 : k;
+    return court(AT[k][0], AT[k][1]);
+}
+
+Vec3 Site_campAlong(void) { return mapDir(COURT_ANGLE); }
+
+void Site_arrivalPath(Vec3 *points, int *n, float *park_yaw) {
+    static const float AT[][2] = { { 80.0f, 38.5f }, { 62.0f, 27.5f }, { 45.0f, 26.2f }, { 33.0f, 25.8f }, { 26.5f, 24.0f },
+                                   { 24.3f, 21.5f }, { 24.0f, 19.0f } }; /* (in off the road, clear of the frame for the bar) */
+    *n = COUNT(AT);
+    for (int i = 0; i < *n; i++) points[i] = court(AT[i][0], AT[i][1]);
+    *park_yaw = yaw_facing(mapDir(COURT_ANGLE + 90.0f)); /* (nose to the garages, the boot to the road) */
+}
+
 Vec3 Site_yard(void) {
     return court(YARD_A, YARD_B);
 }
diff --git a/entropie/src/world/site.h b/entropie/src/world/site.h
index 7a9c6c5..8d05d56 100644
--- a/entropie/src/world/site.h
+++ b/entropie/src/world/site.h
@@ -22,6 +22,16 @@
 
 #define SITE_SCALE 0.426f  /* meters per pixel of the traced map */
 
+/* A point of the courtyard: a along the garages' long side, b across (towards the gravel road). */
+Vec3  Site_court(float a, float b);
+/* Where the crates of drinks go (by the fire), and where the tents go (the
+   orchard): k 0 yours, 1 Volkram and Jana's, 2 Toni's; along: the way things
+   are laid out side by side there. */
+Vec3  Site_drinksSpot(void);
+Vec3  Site_campSpot(int k);
+Vec3  Site_campAlong(void);
+/* Your car: where it comes in on the gravel road, the way in, and where it's parked (facing). */
+void  Site_arrivalPath(Vec3 *points, int *n, float *park_yaw);
 /* Where the yard is (world); the things to play with lie around it. */
 Vec3  Site_yard(void);
 /* The main building of the courtyard: the first thing you look at. */
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index f03aba6..d9e7133 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -1,6 +1,7 @@
 #include "world/world.h"
 #include "world/camp.h"
 #include "world/clothes.h"
+#include "world/car.h"
 #include "people/pockets.h"
 #include "people/condition.h"
 #include "people/dog.h"
@@ -799,6 +800,11 @@ static float scootTo(const World *world, const Object *chair) {
 static void showProps(World *world) {
     for (int i = 0; i < world->count; i++) {
         Object *f = world->objects[i];
+        if (f->type == OBJ_CRATE && f->shown != f->stored) { /* (a bottle taken: one fewer shows) */
+            Mesh_destroy(&f->mesh);
+            f->mesh = Shapes_crate(f->variant, f->stored);
+            f->shown = f->stored;
+        }
         if (f->type != OBJ_FURNITURE) continue;
         if (f->variant == FURN_GATE) { if (f->gate_t != (f->gate_open ? 1.0f : 0.0f)) Object_swingGate(f, world->dt_props); continue; }
         if (f->variant == FURN_CHAIR) { if (f->chair_out != f->chair_goal && !chairWaits(world, f)) slideChair(world, f); continue; }
@@ -1225,6 +1231,7 @@ void World_update(World *world, float dt, float game_dt) {
     Object_indexKinds(world->objects, world->count);
     world->dt_props = dt;
     Clothes_update(world);
+    Car_serve(world);
     showProps(world); /* (who's a person, where the fires are: looked up again and again) */
     for (int i = 0; i < world->count; i++) {
         Object *o = world->objects[i];
@@ -1241,7 +1248,7 @@ void World_update(World *world, float dt, float game_dt) {
         }
         Condition_useGrid(&world->grid);
         Condition_update(o, world->objects, world->count, dt, game_dt);
-        if (o->type == OBJ_HUMAN) { Ai_useGrid(&world->grid); Ai_useWorld(world); Ai_update(o, world->objects, world->count, dt); }
+        if (o->type == OBJ_HUMAN && !o->riding) { Ai_useGrid(&world->grid); Ai_useWorld(world); Ai_update(o, world->objects, world->count, dt); }
         if (o->type == OBJ_HUMAN && o->ai.wants_bag) handOutBag(world, o);
         if (o->type == OBJ_HUMAN && o->ai.empty_tray) emptyTray(world, o);
         if (o->type == OBJ_HUMAN && o->body.stepped_off) { /* lost the balance up on something */
@@ -1378,10 +1385,17 @@ static void drawCharred(Renderer *renderer, const Object *o) {
     Renderer_drawMeshTinted(renderer, &o->mesh, model, vec3(dark, dark * 0.95f, dark * 0.9f), o->glow);
 }
 
+/* You're sitting in this car. */
+static bool playerIn(const World *world, const Object *car) {
+    for (int i = 0; i < world->count; i++)
+        if (world->objects[i]->type == OBJ_PLAYER) return world->objects[i]->riding == car;
+    return false;
+}
+
 void World_drawShadows(const World *world, Renderer *renderer) {
     for (int i = 0; i < world->count; i++) {
         const Object *obj = world->objects[i];
-        if (flatOnGround(obj) || (obj->is_character && obj->toilet) || (!obj->is_character && !obj->mesh.vao)) continue; /* flat on the ground, hidden, nothing to see: no shadow */
+        if (flatOnGround(obj) || (obj->is_character && (obj->toilet || obj->riding)) || (!obj->is_character && !obj->mesh.vao)) continue; /* flat on the ground, hidden, nothing to see: no shadow */
         bool big = fmaxf(obj->size.x, obj->size.z) >= BIG_SIZE * 3.0f;
         if (!big && flatDistance2(obj->position, renderer->shadow_focus) > SHADOW_REACH * SHADOW_REACH) continue;
         if (obj->is_character) {
@@ -1435,7 +1449,7 @@ void World_draw(World *world, Renderer *renderer, bool draw_player) {
         if (obj->type == OBJ_PLAYER && !draw_player) continue; /* first person */
         if (!obj->is_character && obj->type != OBJ_DOG && !obj->mesh.vao) continue; /* (invisible: a building's walls) */
         if (outOfSight(obj, renderer->viewer_pos)) continue;
-        if (obj->is_character && obj->toilet) continue; /* in the toilet */
+        if (obj->is_character && (obj->toilet || obj->riding)) continue; /* in the toilet, in the car */
         if (obj->is_character) {
             FigurePose pose = figurePose(obj);
             Renderer_drawFigure(renderer, &pose);
@@ -1444,6 +1458,8 @@ void World_draw(World *world, Renderer *renderer, bool draw_player) {
                 drawFlames(renderer, &world->flames, vec3_add(obj->position, vec3(0.0f, down ? 0.0f : 0.3f, 0.0f)),
                            down ? 1.3f : 0.8f, down ? 0.9f : 2.0f, (float)i);
             }
+        } else if (obj->type == OBJ_CAR && !draw_player && playerIn(world, obj)) { /* (from the driver's seat: inside it) */
+            Renderer_drawMesh(renderer, &obj->inside, Object_modelMatrix(obj));
         } else if (obj->type == OBJ_DOG) {
             Dog_draw(renderer, obj);
         } else if (obj->type == OBJ_HOSE) {