Nimbin[12]?Games / Entropie / commits / 318558a

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.44.0: Berlin - the meetings and the weekends

Christian Immanuel · 2026-10-28 14:58 · 318558a38682548dc4708b2fa725d385b3f39271

- A new game starts in Grossbeerenstrasse 37, 3rd floor: you, Volkram, Jana
  and Toni decide on the festival and pick its name (answer with 1 - 5)
- The Herberge bar with the whole crew: who leads which floor (picked: keen;
  passed over: less so), how much goes into the cash box; then the drive in
- Weekends end Sunday at six: back to the Herberge - how far it got, floors
  nobody leads, what next weekend is for (floors, side jobs or a party),
  topping up the cash box; Friday at five everybody's back on the ground
- Crew who come later wait in Berlin till they're due
- Backspace skips a meeting; no saving during meetings
- The regulatory office's fine is capped at 600 EUR
- No shin scrapes when stepping over a bench to sit down
 commit_message.txt                 |  24 +-
 entropie/ARCHITECTURE.md           |   4 +-
 entropie/Makefile                  |   2 +-
 entropie/README.md                 |   9 +-
 entropie/STATE.md                  |  40 ++-
 entropie/commit_message.txt        |  24 +-
 entropie/src/game/game.c           |   7 +-
 entropie/src/game/game.h           |  30 ++
 entropie/src/game/game_arrival.c   |   3 +
 entropie/src/game/game_internal.h  |   9 +
 entropie/src/game/game_meeting.c   | 639 +++++++++++++++++++++++++++++++++++++
 entropie/src/game/game_story.c     |  31 +-
 entropie/src/game/game_talk.c      |   3 +
 entropie/src/game/save.c           |   6 +
 entropie/src/main.c                |   2 +-
 entropie/src/people/ai.c           |   5 +
 entropie/src/people/ai.h           |   1 +
 entropie/src/people/ai_project.c   |   3 +
 entropie/src/render/shapes.h       |   2 +-
 entropie/src/render/shapes_props.c |  11 +
 entropie/src/world/manor.c         |  70 ++++
 entropie/src/world/manor.h         |   6 +
 entropie/src/world/object.c        |   1 +
 entropie/src/world/site.c          |   1 +
 entropie/src/world/world.c         |   5 +-
 entropie/src/world/world.h         |   4 +
 26 files changed, 899 insertions(+), 43 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index df222f7..cf82a87 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,13 +1,13 @@
-entropie 2.43.0: the side jobs - the scythe, the snails, Kerry's art
+entropie 2.44.0: Berlin - the meetings and the weekends
 
-- Kartoffelacker grass stands knee high; mow it with the scythe (in the tool
-  room): hold left click in the field and walk. Toni mows on his own; a
-  careful lead waits until the snails are out, a careless one mows them over
-- Vineyard snails (30 in the field, 10 in the orchard): pick them into your
-  bucket and let them go in the long grass by the wood left of the
-  Kartoffelacker. Doro and Stella do it too
-- The regulatory office comes 44 h after you arrive (warned the day before
-  and 3 h before): 40 EUR per snail still out there, 80 per dead one
-- Kerry carves a log lying in the yard by the pavilions into a figure (owl,
-  bear, mushroom man) with the chainsaw
-- F2 projects page shows the side jobs' progress
+- A new game starts in Grossbeerenstrasse 37, 3rd floor: you, Volkram, Jana
+  and Toni decide on the festival and pick its name (answer with 1 - 5)
+- The Herberge bar with the whole crew: who leads which floor (picked: keen;
+  passed over: less so), how much goes into the cash box; then the drive in
+- Weekends end Sunday at six: back to the Herberge - how far it got, floors
+  nobody leads, what next weekend is for (floors, side jobs or a party),
+  topping up the cash box; Friday at five everybody's back on the ground
+- Crew who come later wait in Berlin till they're due
+- Backspace skips a meeting; no saving during meetings
+- The regulatory office's fine is capped at 600 EUR
+- No shin scrapes when stepping over a bench to sit down
diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index 5ee09b4..e48ac28 100644
--- a/entropie/ARCHITECTURE.md
+++ b/entropie/ARCHITECTURE.md
@@ -10,9 +10,9 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL
 | `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input, sound out (`audio.c`: a synthesizer and mixer, no sound files) | core |
 | `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math |
 | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render |
-| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), everybody in the story (`cast.c`), the floors' stages (`stage.c`), the side jobs' ground (`field.c`: the Kartoffelacker's grass, the snails, Kerry's art), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game |
+| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c` - also the Berlin rooms off the map, `course.c`), everybody in the story (`cast.c`), the floors' stages (`stage.c`), the side jobs' ground (`field.c`: the Kartoffelacker's grass, the snails, Kerry's art), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game |
 | `src/people/` | bodies (animation), hands, AI (the projects: `ai_project.c`, the side jobs: `ai_jobs.c`), 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), the shopping run (`game_shop.c`), 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`), the meetings in Berlin and the weekends (`game_meeting.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 be709a1..951dcda 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.43.0
+VERSION   := 2.44.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index a112f95..fff6c0e 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -15,6 +15,11 @@ people living there.
 - **The site:** Homburgshöhe at Geflügelhof Oderland, traced from the real
   map (about 340 x 340 m): the farmyard, the house, your garages, the barn,
   forest, roads. Painted in the rotoscope style of *Waking Life*
+- **Berlin:** the story starts at the kitchen table in Grossbeerenstrasse
+  37 (a festival - where, its name), then the Herberge bar with the whole
+  crew (who leads which floor, the cash box). Every weekend on the ground
+  ends Sunday at six: back to the Herberge (how far it got, open floors,
+  what next weekend is for), Friday at five on the ground again
 - **Arriving:** by car with Volkram, Jana and Toni. Say hello to Kurti and
   his sons, unload the car, pitch the tents. A golden marker shows who or
   what's next; Kurti shows you all the way up to the camping meadow
@@ -84,7 +89,7 @@ people living there.
 | Space (asleep, hold) | Sleep faster |
 | G             | One more person helps you |
 | H             | Everybody stops helping |
-| Backspace     | Never mind: call off showing someone how / where |
+| Backspace     | Never mind: call off showing someone how / where; at a meeting: skip it |
 | R             | Call someone (or Alice) over |
 | V             | Third / first person |
 | I             | Show all keys in game |
@@ -137,5 +142,5 @@ Dependencies point downward: `main → game → world/people → render → core
 
 ## Version
 
-`make version` prints e.g. `entropie 2.43.0 (build 70cb9cf)`. The build id is
+`make version` prints e.g. `entropie 2.44.0 (build 3012ea9)`. The build id is
 an md5 of all sources and assets.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index fed71a8..18bcbd8 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -82,7 +82,7 @@ things the way they do.
 4. The side jobs: the scythe on the Kartoffelacker, the snail rescue, Kerry's
    chainsaw art. (2.43)
 5. Berlin: Grossbeerenstrasse and the Herberge as places, the meetings, going
-   back and forth between weekends.
+   back and forth between weekends. (2.44)
 6. The "why" questions and their options.
 7. The festival (guests, the floors running, the storm and the evacuation),
    the clean-up with the supporters, the drive home.
@@ -327,6 +327,31 @@ Painted in the rotoscope style of *Waking Life* (2001):
   with only a faint paper edge
 - Stats on a painted sheet of paper (Tab)
 
+### Berlin and the weekends
+
+- A new game starts in Kreuzberg, Grossbeerenstrasse 37, 3rd floor: you,
+  Volkram, Jana and Toni at the kitchen table on a Wednesday evening. They
+  talk (speech bubbles); when it's your turn, answer with 1 - 5: are you in
+  (keen lifts everybody a little), the festival's name (Entropie,
+  Kartoffelrave or Wriezen Calling)
+- Then the Herberge bar a week later, the whole crew on two benches at the
+  long table: who does what is said (artists, decoration, light, the food
+  stand, the geo-dome), then each floor is asked about - the one keen on it
+  says so, you pick who leads it (or yourself, or nobody yet). Picked, they're
+  into it; passed over, a bit less. Then the cash box: 50, 80 or 30 each
+- Friday: the drive in (Arriving, below). Those who come later wait in
+  Berlin till they're due
+- Sunday afternoon Volkram calls it: at six everybody goes back to Berlin.
+  The Herberge again: each floor's lead says how far it got, Toni the
+  mowing and the snails, Jana the cash box and the mood; a floor nobody
+  leads is asked about (the three most into it, you, nobody); what next
+  weekend is for - the floors (they go to work on them more often), the
+  side jobs, or a party (everybody happier, less work); the cash box nearly
+  empty: another 20 each? Then Friday at five on the ground, everybody at
+  the fire, fresh from home
+- Backspace skips a meeting (the first answer to everything). No saving
+  during a meeting
+
 ### Arriving
 
 - The game starts in Volkram's car (he drives, Jana beside him, you and Toni
@@ -1431,6 +1456,14 @@ Round 2.43 (the side jobs: the scythe, the snails, Kerry's art):
 - F2 projects page: mowing %, snails safe / out / dead / office in, Kerry's figures and whether a log is there.
 - Checked (fed, 10:30): Toni waited for the snails, Doro and Stella got all 39 to the refuge in about 5 min, then Toni mowed; with a careless mower 6 died; Kerry fetched the chainsaw and set to carving once a log lay there (the three figures looked at made directly); the office: no fine with all out; saved and loaded mid-way (mown cells, counts and a bucket of 5 kept).
 
+Round 2.44 (Berlin: the meetings and the weekends):
+- `manor.c` Berlin_build (from `Site_finish`): two closed rooms off the map at BERLIN_X 700 (the site, 530 m+ away, is past the camera's far plane): the flat in Grossbeerenstrasse (8 x 6 m, the kitchen table with six chairs, counter, stove, shelf, a bench) and the Herberge (BERLIN_BAR 24 m along: 12 x 8 m, the bar - FURN_BAR, a counter with taps and glasses - shelves, the long table 4.4 m with two benches linked to it, two small tables). Locked doors. World code treats x > WORLD_BERLIN_FROM 400 as indoors in Berlin (no wind on hats and things).
+- `game/game_meeting.c`: Meeting (kind, Herberge number, agenda step, line timer, question and its answers, who spoke last, fade). New game: `game_meetingStart` (Grossbeerenstrasse) instead of the drive in; `gather` sits everybody for it (chairs, or bench places from the middle out; `ai.meeting`: Ai_update does nothing else; fresh from home: fed, rested, dry, sober), says the place and day (CARD_TIME 6 s), 18:30. Agendas as steps (`kreuzberg`, `herbergeFirst`, `herbergeLater`); questions Q_JOIN, Q_NAME (`Game_festivalName`), Q_FLOOR (the first Herberge: eager one first; `leadsOther`: whoever leads another floor or a side job isn't offered; picked +0.08, passed-over eager -0.06; later: the floors nobody leads, one after the other, the three most into it), Q_CASH (1200 / 1900 / 700), Q_FOCUS (FOCUS_FLOORS / SIDE: START_CHANCE x1.6 for those, PARTY: x0.5 and +0.08 for all; `world.focus`), Q_TOP_UP (cash under 600: +400, -0.02). Answers through `Game_choices` (1-5); the talk keys wait meanwhile. Fades (FADE_TIME 1.4 s, `renderer->dark`): AFTER_KREUZBERG / HERBERGE / FIRST_WEEKEND (the car arrival at 13:00, weekend 1) / NEXT_WEEKEND (everybody on a circle round the fire, you in the yard, 17:00). Weekend end: Sunday 18:00 (WEEKEND_END 66 h from the weekend's Friday midnight, warned at 65; not while driving in or shopping). Saved: `weekend` (number, start, warned, name, focus); not during a meeting (`Save_blocked`).
+- `game_story.c`: the story clock only on the ground; crew waiting in Berlin since the Herberge come over when due (moved, not made again); F2 says "in Berlin". The regulatory office's fine at most FINE_MOST 600 EUR.
+- Dev: `game_skipArrival` skips the meetings first (`game_meetingSkipStart`: the first answers).
+- Sitting down onto a seat (over a bench) scrapes no shins.
+- Checked: the harness through Grossbeerenstrasse (name Entropie), the first Herberge (Kilian, Julia, Jana, Rouven lead the four floors; Toni keeps the mowing), the drive in, Sunday 18:00 to the Herberge (the warning first), a party weekend picked, Friday 17:00 back with nobody left in Berlin; the real game started into the first meeting and answered with 1.
+
 ## Open tasks
 
 Most important first.
@@ -1441,7 +1474,8 @@ Most important first.
 - 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
 - Side jobs, still to do: decoration and light as jobs of their own; the man from the regulatory office as a person walking the field; Kerry's figures placed where they go; helpers raking the mown grass
-- Next: Berlin (Grossbeerenstrasse, the Herberge); the "why" questions; the festival, the storm, the clean-up, the drive home
+- Berlin, still to do: the streets and the way between (the rooms are closed boxes); ordering at the bar; more on the agenda (the "why" questions); the crew missing a weekend now and then
+- Next: 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)
@@ -1475,4 +1509,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.43.0 (build 70cb9cf)`
+State of `entropie 2.44.0 (build 3012ea9)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index df222f7..cf82a87 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,13 +1,13 @@
-entropie 2.43.0: the side jobs - the scythe, the snails, Kerry's art
+entropie 2.44.0: Berlin - the meetings and the weekends
 
-- Kartoffelacker grass stands knee high; mow it with the scythe (in the tool
-  room): hold left click in the field and walk. Toni mows on his own; a
-  careful lead waits until the snails are out, a careless one mows them over
-- Vineyard snails (30 in the field, 10 in the orchard): pick them into your
-  bucket and let them go in the long grass by the wood left of the
-  Kartoffelacker. Doro and Stella do it too
-- The regulatory office comes 44 h after you arrive (warned the day before
-  and 3 h before): 40 EUR per snail still out there, 80 per dead one
-- Kerry carves a log lying in the yard by the pavilions into a figure (owl,
-  bear, mushroom man) with the chainsaw
-- F2 projects page shows the side jobs' progress
+- A new game starts in Grossbeerenstrasse 37, 3rd floor: you, Volkram, Jana
+  and Toni decide on the festival and pick its name (answer with 1 - 5)
+- The Herberge bar with the whole crew: who leads which floor (picked: keen;
+  passed over: less so), how much goes into the cash box; then the drive in
+- Weekends end Sunday at six: back to the Herberge - how far it got, floors
+  nobody leads, what next weekend is for (floors, side jobs or a party),
+  topping up the cash box; Friday at five everybody's back on the ground
+- Crew who come later wait in Berlin till they're due
+- Backspace skips a meeting; no saving during meetings
+- The regulatory office's fine is capped at 600 EUR
+- No shin scrapes when stepping over a bench to sit down
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 7f2fcff..c8dc88f 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -264,7 +264,7 @@ bool Game_initWorld(Game *game, bool arrive) {
         o->spawn_type = o->type;
     }
     memset(&game->arrival, 0, sizeof(game->arrival));
-    if (arrive && ARRIVE_BY_CAR && !START_AT_COURSE) game_arrivalStart(game);
+    if (arrive && ARRIVE_BY_CAR && !START_AT_COURSE) game_meetingStart(game); /* (the story starts in Berlin; then the drive in) */
     followPlayer(game, 0.0f);
     return true;
 }
@@ -373,7 +373,8 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
         say(game, "You're wet through - by a fire you dry quickest");
     } else if (game->player->look.wet < 0.2f) game->wet_told = false;
     game_arrival(game, dt);
-    game_story(game, game->world.game_dt);
+    game_meeting(game, input, dt);
+    if (!game->meeting.kind && !Game_inBerlin(game->player)) game_story(game, game->world.game_dt); /* (the hours on the ground: on the ground) */
     game_shop(game, dt);
     game->quest_t += dt;
     Ai_setSettled(game->arrival.task == TASK_NONE);
@@ -510,7 +511,7 @@ void Game_render(Game *game, Renderer *renderer) {
     Renderer_endShadowPass(renderer);
 
     renderer->drunk = fminf(game->player->drunk, 1.0f);
-    renderer->dark  = game->dark * (game->third_person ? 0.55f : 0.92f); /* eyes shut */
+    renderer->dark  = fmaxf(game->dark * (game->third_person ? 0.55f : 0.92f), game->meeting.fade); /* eyes shut; going to Berlin and back */
     Renderer_beginScenePass(renderer, &game->camera, light);
     World_draw(&game->world, renderer, game->third_person && game->camera_reach > 0.6f); /* (pulled right in: from the head) */
     Meadow_draw(&game->meadow, &game->world, renderer, game->camera.position);
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index cc7f1d6..1de8806 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -46,6 +46,26 @@ typedef struct {
     float          s, speed;         /* where on the way in (m), how fast */
 } Errand;
 
+/* A meeting in Berlin (game_meeting.c): Grossbeerenstrasse at the start, the Herberge before every weekend. */
+typedef enum { MEET_NONE, MEET_KREUZBERG, MEET_HERBERGE } MeetingKind;
+typedef struct {
+    MeetingKind kind;
+    int    number;           /* the Herberge: which (1: before the first weekend - who leads what) */
+    int    step;             /* the agenda so far */
+    float  wait;             /* until the next line */
+    int    asking;           /* a question waiting for your answer (Q_*: game_meeting.c), 0 none ... */
+    int    n_options;        /* ... the answers */
+    char   option_text[TALK_CHOICES][72];
+    int    option_value[TALK_CHOICES];
+    int    asked_floor;      /* (who leads which floor: the floor asked about ... */
+    int    asked_mask;       /* ... the floors asked about this time) */
+    struct Object *speaker;  /* who spoke last (everybody looks at them) */
+    float  fade;             /* 0..1 how dark (going there, coming back) ... */
+    int    fading;           /* ... +1 getting dark, -1 light again */
+    int    after;            /* what happens once it's dark (AFTER_*: game_meeting.c) */
+    bool   skipping;         /* Backspace: the rest of it at once, the first answers */
+} Meeting;
+
 typedef struct {
     World    world;
     Object  *player;
@@ -107,6 +127,12 @@ typedef struct {
     float    quest_t;       /* ... its bob and turn */
     float    mow_t;         /* into this swing of the scythe (mowing: left button held) */
     int      snail_warned;  /* the regulatory office: told of it (1 the day before, 2 on the day) */
+    Meeting  meeting;       /* a meeting in Berlin (game_meeting.c) ... */
+    int      weekend;       /* ... the weekends on the ground, the first 1 ... */
+    float    weekend_from;  /* ... this one's Friday midnight (story hours) ... */
+    bool     weekend_warned; /* ... "we drive back at six" said */
+    int      fest_name;     /* the festival's name (chosen in Grossbeerenstrasse: game_meeting.c) */
+    int      focus;         /* what this weekend is for (the Herberge): FOCUS_* in world.h */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
@@ -124,6 +150,10 @@ int  Game_choices(const Game *game, const char *out[TALK_CHOICES]);
    you where to camp, your tent's spot); false: nothing to point at now. */
 bool Game_questMarker(const Game *game, Vec3 *at);
 
+/* The festival's name (chosen at the first meeting); in Berlin (a meeting, or waiting there between weekends)? */
+const char *Game_festivalName(const Game *game);
+bool Game_inBerlin(const struct Object *o);
+
 /* 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_arrival.c b/entropie/src/game/game_arrival.c
index 241f52e..81af54f 100644
--- a/entropie/src/game/game_arrival.c
+++ b/entropie/src/game/game_arrival.c
@@ -405,6 +405,8 @@ static void settled(Game *game) {
 
 const char *game_task(Game *game) {
     Arrival *a = &game->arrival;
+    const char *meeting = game_meetingTask(game);
+    if (meeting) return meeting;
     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";
@@ -419,6 +421,7 @@ const char *game_task(Game *game) {
 /* Dev: the start skipped - parked, everybody out, hello and chat said, the car unloaded and the tents up. */
 void game_skipArrival(Game *game) {
     Arrival *a = &game->arrival;
+    if (game->weekend == 0) game_meetingSkipStart(game); /* (the meetings in Berlin first) */
     if (!a->car) return;
     Car_place(&game->world, a->car, a->path[ARRIVAL_SAMPLES - 1], a->park_yaw);
     for (int i = 0; i < game->world.count; i++) if (game->world.objects[i]->riding) Car_getOut(game->world.objects[i]);
diff --git a/entropie/src/game/game_internal.h b/entropie/src/game/game_internal.h
index 3f8b37c..ea1a2e9 100644
--- a/entropie/src/game/game_internal.h
+++ b/entropie/src/game/game_internal.h
@@ -36,6 +36,15 @@ 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);
+/* game_meeting.c: the meetings in Berlin and the weekends between them. Start: a new game (Grossbeerenstrasse);
+   every frame; the answers offered; a line about it for the hint; after loading. */
+void game_meetingStart(Game *game);
+void game_meeting(Game *game, const Input *input, float dt);
+int  Game_meetingChoices(const Game *game, const char *out[TALK_CHOICES]);
+const char *game_meetingTask(const Game *game);
+void game_meetingLoaded(Game *game);
+/* Dev: the meetings at the start skipped (the first answers): on to the drive in. */
+void game_meetingSkipStart(Game *game);
 
 void game_callHelp(Game *game);
 void game_callOver(Game *game);
diff --git a/entropie/src/game/game_meeting.c b/entropie/src/game/game_meeting.c
new file mode 100644
index 0000000..5864c96
--- /dev/null
+++ b/entropie/src/game/game_meeting.c
@@ -0,0 +1,639 @@
+/*
+ * Berlin and the weekends (see the story: chapters 1-3).
+ *
+ * A new game starts in Grossbeerenstrasse 37, 3rd floor: you, Volkram, Jana
+ * and Toni at the kitchen table - "we do a festival", where, its name. Then
+ * the Herberge with the whole crew: who leads which floor (the eager one isn't
+ * always the one you pick: picked, they're into it; passed over, a bit less),
+ * how much goes into the cash box. Then the drive to Wriezen (game_arrival.c).
+ *
+ * A weekend on the ground ends Sunday at six: everybody back to Berlin, the
+ * Herberge again - how far it all got, floors nobody leads, what next weekend
+ * is for (the floors, the side jobs, or a party), topping up the cash box -
+ * then Friday at five on the ground again, everybody there.
+ *
+ * The meeting places are rooms off the map (manor.c Berlin_build); who's at a
+ * meeting sits at its table (ai.meeting: nothing else). Crew who turn up on
+ * the ground later wait in Berlin till they're due (game_story.c).
+ */
+
+#include <math.h>
+
+#include "game/game_internal.h"
+#include "world/car.h"
+#include "world/cast.h"
+#include "world/field.h"
+#include "world/manor.h"
+#include "world/stage.h"
+
+#define FADE_TIME    1.4f   /* s to dark (and back) */
+#define LINE_GAP     4.2f   /* s between lines */
+#define FIRST_WAIT   2.0f   /* s after it's light before the first line */
+#define CARD_TIME    6.0f   /* s the place and the day stay on screen */
+#define MEETING_HOUR 18.5f
+#define WEEKEND_WARN 65.0f  /* hours from the weekend's Friday midnight: "we drive back at six" ... */
+#define WEEKEND_END  66.0f  /* ... Sunday six o'clock: back to Berlin */
+#define NEXT_ARRIVE  17.0f  /* the weekends after the first: on the ground Friday at five */
+#define START_HOUR_FIRST 13.0f /* (the first: in by car at one, game.c START_HOUR) */
+#define TOP_UP       400.0f /* EUR: another 20 each */
+
+enum { AFTER_NONE, AFTER_KREUZBERG, AFTER_HERBERGE, AFTER_FIRST_WEEKEND, AFTER_NEXT_WEEKEND };
+enum { Q_NONE, Q_JOIN, Q_NAME, Q_FLOOR, Q_CASH, Q_FOCUS, Q_TOP_UP };
+
+static const char *const NAMES[] = { "Entropie", "Kartoffelrave", "Wriezen Calling" };
+static const int FLOORS[] = { PROJECT_OBSTWIESE, PROJECT_HOFBUEHNE, PROJECT_KARTOFFELACKER, PROJECT_HIDDEN };
+#define N_FLOORS 4
+
+const char *Game_festivalName(const Game *game) {
+    return NAMES[(unsigned)game->fest_name < 3 ? game->fest_name : 0];
+}
+
+static Object *named(const Game *game, const char *name) {
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        if (o->is_character && !strcmp(o->name, name)) return o;
+    }
+    return NULL;
+}
+
+static Object *castMember(const Game *game, int id) {
+    if (id == 0) return game->player;
+    for (int i = 0; i < game->world.count; i++)
+        if (game->world.objects[i]->is_character && game->world.objects[i]->cast_id == id) return game->world.objects[i];
+    return NULL;
+}
+
+/* The crew (not Kurti and his sons, not you). */
+static bool crew(const Object *o) { return o->type == OBJ_HUMAN && o->cast_id > 0 && !o->lives_here; }
+
+bool Game_inBerlin(const Object *o) { return o->position.x > WORLD_BERLIN_FROM; }
+
+static void line(Game *game, const char *name, const char *text) {
+    Object *o = name ? named(game, name) : NULL;
+    if (!o) o = game->player;
+    if (o == game->player) { snprintf(game->info, sizeof game->info, "You: %s", text); game->info_about = NULL; say(game, game->info); }
+    else Ai_say(o, text);
+    game->meeting.speaker = o;
+    game->meeting.wait = LINE_GAP + 0.03f * (float)strlen(text);
+}
+
+/* ---------- Sitting down at the table ---------- */
+
+static Vec3 tableAt(MeetingKind kind) {
+    return kind == MEET_KREUZBERG ? vec3(BERLIN_X, 0.0f, 0.0f) : vec3(BERLIN_X + BERLIN_BAR + 0.6f, 0.0f, 0.0f);
+}
+
+static void unseat(Object *o) {
+    if (o->riding) Car_getOut(o);
+    o->sitting = false;
+    o->seat = NULL;
+    o->sit_moving = false;
+    o->velocity = o->movement = vec3_zero();
+    if (o->type == OBJ_HUMAN) {
+        for (int h = 0; h < 2; h++) if (o->hands.item[h]) Hands_drop(o, h);
+        o->ai.unload = o->ai.greet = o->ai.errand = o->ai.on_project = false;
+        o->ai.errand_with = NULL;
+        o->ai.fetch = NULL;
+        o->ai.target = o->ai.leader = NULL;
+        o->ai.mode = AI_IDLE;
+        o->ai.path_len = 0;
+    }
+}
+
+static void put(Object *o, Vec3 at) {
+    o->position = at;
+    o->body.replant = true;
+    Object_syncColliders(o);
+}
+
+/* k-th place at the table: a chair (Grossbeerenstrasse), a place on a bench (the Herberge). */
+static void sitAt(Game *game, Object *o, int k) {
+    MeetingKind kind = game->meeting.kind;
+    Vec3 table = tableAt(kind);
+    Object *seat = NULL;
+    Vec3 at = table;
+    if (kind == MEET_KREUZBERG) {
+        int n = 0;
+        for (int i = 0; i < game->world.count && !seat; i++) {
+            Object *c = game->world.objects[i];
+            if (c->type != OBJ_FURNITURE || c->variant != FURN_CHAIR || hypotf(c->position.x - table.x, c->position.z - table.z) > 2.0f) continue;
+            if (n++ == k) seat = c;
+        }
+        if (seat) at = vec3_add(seat->position, vec3_scale(vec3_normalize(vec3_sub(seat->position, table)), 0.3f));
+    } else {
+        int side = k % 2, slot = k / 2;
+        for (int i = 0; i < game->world.count && !seat; i++) {
+            Object *b = game->world.objects[i];
+            if (b->type != OBJ_FURNITURE || b->variant != FURN_BENCH || !b->table || hypotf(b->position.x - table.x, b->position.z - table.z) > 2.0f) continue;
+            if ((b->position.z > table.z) == (side == 1)) seat = b;
+        }
+        if (seat) {
+            float along = ((float)(slot % 7) - 3.0f) * 0.57f;
+            Vec3 out = vec3_normalize(vec3(0.0f, 0.0f, seat->position.z - table.z));
+            at = vec3_add(seat->position, vec3_add(vec3(along, 0.0f, 0.0f), vec3_scale(out, 0.45f)));
+        }
+    }
+    unseat(o);
+    at.y = 0.02f;
+    put(o, at);
+    if (o->type == OBJ_HUMAN) o->ai.meeting = true;
+    if (seat) Object_sit(o, seat, table);
+}
+
+/* Fresh from home: fed, rested, dry, sober. */
+static void freshen(Object *o) {
+    o->needs.hunger = 0.2f;
+    o->needs.thirst = 0.2f;
+    o->needs.bladder = 0.1f;
+    o->needs.sleepiness = 0.15f;
+    o->needs.stamina = fmaxf(o->needs.stamina, 0.9f);
+    o->drunk = 0.0f;
+    o->look.wet = 0.0f;
+}
+
+/* Everybody there for it sat down; the time and the place said. */
+static void gather(Game *game) {
+    Meeting *m = &game->meeting;
+    int k = 0;
+    sitAt(game, game->player, k++);
+    if (m->kind == MEET_KREUZBERG) {
+        static const char *const FRIENDS[] = { "Volkram", "Jana", "Toni" };
+        for (int f = 0; f < 3; f++) { Object *o = named(game, FRIENDS[f]); if (o) { freshen(o); sitAt(game, o, k++); } }
+    } else {
+        for (int id = 1; id < Cast_count(); id++) {
+            const CastMember *c = Cast_get(id);
+            if (c->lives_here) continue;
+            Object *o = castMember(game, id);
+            if (!o) { /* (not on the ground yet: here they are, for the Herberge) */
+                o = Cast_create(id, tableAt(MEET_HERBERGE));
+                if (!o || !World_add(&game->world, o)) { if (o) Object_destroy(o); continue; }
+            }
+            freshen(o);
+            sitAt(game, o, k++);
+        }
+    }
+    freshen(game->player);
+    game->daycycle.seconds = MEETING_HOUR * 3600.0f;
+    game->third_person = true;
+    if (m->kind == MEET_KREUZBERG)
+        snprintf(game->story_line, sizeof game->story_line, "Kreuzberg, Grossbeerenstrasse 37, 3rd floor - a Wednesday evening in March");
+    else if (m->number == 1)
+        snprintf(game->story_line, sizeof game->story_line, "The Herberge - a week later, everybody's there");
+    else snprintf(game->story_line, sizeof game->story_line, "The Herberge - Wednesday, after weekend %d", game->weekend);
+    say(game, game->story_line);
+    game->message_time = CARD_TIME;
+    m->step = 0;
+    m->wait = FIRST_WAIT;
+    m->asking = Q_NONE;
+}
+
+/* ---------- Questions ---------- */
+
+static void ask(Game *game, int q) {
+    Meeting *m = &game->meeting;
+    m->asking = q;
+    m->n_options = 0;
+}
+
+static void option(Game *game, int value, const char *text) {
+    Meeting *m = &game->meeting;
+    if (m->n_options >= TALK_CHOICES) return;
+    snprintf(m->option_text[m->n_options], sizeof m->option_text[0], "%s", text);
+    m->option_value[m->n_options++] = value;
+}
+
+static void lift(Object *o, float by) { if (o && o != NULL) o->commitment = clampf(o->commitment + by, 0.0f, 1.0f); }
+
+/* Who leads it now gives it up (helps instead); the one picked leads it. id: 0 you, -1 nobody. */
+static void setLead(Game *game, int project, int id) {
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        if (!o->is_character || o->leads != project) continue;
+        o->leads = PROJECT_NONE;
+        if (o->helps == PROJECT_NONE) o->helps = project;
+    }
+    Object *o = id >= 0 ? castMember(game, id) : NULL;
+    if (!o) return;
+    if (o->helps == project) o->helps = PROJECT_NONE;
+    o->leads = project;
+}
+
+/* Already leads something that's work on the ground (another floor, a side job): not this one too. */
+static bool leadsOther(const Object *o, int project) {
+    if (o->leads == project) return false;
+    for (int k = 0; k < N_FLOORS; k++) if (o->leads == FLOORS[k]) return true;
+    return o->leads == PROJECT_MOWING || o->leads == PROJECT_SNAILS || o->leads == PROJECT_WOOD_ART;
+}
+
+/* The first Herberge: who leads a floor - the eager one first, then the others who'd do it. */
+static void floorQuestion(Game *game, int floor_k) {
+    static const struct { const char *eager, *pitch, *other; const char *why_eager, *why_other; } WHO[N_FLOORS] = {
+        { "Kilian", "Me! I've built three psy floors - give me the Obstwiese.", "Julia", "Kilian - he's built psy floors before", "Julia - quick at everything" },
+        { "Julia", "I'll take the Hofbuehne. Bands, slam, breakdance - I can build that.", "Pascal", "Julia - she builds fast", "Pascal - he does the light anyway" },
+        { "Jana", "World music and little talks in a field? That's me - I'll take the Kartoffelacker.", "Rouven", "Jana - she's good with people", "Rouven - he knows the artists" },
+        { "Rouven", "The hidden techno floor... that one's mine, I think.", "Pascal", "Rouven - techno's his thing", "Pascal - light and sound, in the dark" },
+    };
+    Meeting *m = &game->meeting;
+    m->asked_floor = floor_k;
+    ask(game, Q_FLOOR);
+    char text[72];
+    Object *e = named(game, WHO[floor_k].eager), *o = named(game, WHO[floor_k].other);
+    const int project = FLOORS[floor_k];
+    if (e && !leadsOther(e, project)) option(game, e->cast_id, WHO[floor_k].why_eager);
+    if (o && !leadsOther(o, project)) option(game, o->cast_id, WHO[floor_k].why_other);
+    if (!leadsOther(game->player, project)) option(game, 0, "Me - I'll lead it");
+    snprintf(text, sizeof text, "Nobody yet - we'll see on the ground");
+    option(game, -1, text);
+    if (e && !leadsOther(e, project)) line(game, WHO[floor_k].eager, WHO[floor_k].pitch);
+}
+
+/* Later Herberges: a floor nobody leads - the three most into it who lead nothing. */
+static bool openFloorQuestion(Game *game) {
+    Meeting *m = &game->meeting;
+    for (int k = 0; k < N_FLOORS; k++) {
+        if (m->asked_mask & (1 << k)) continue;
+        if (Cast_leadOf(game->world.objects, game->world.count, (Project)FLOORS[k]) || game->player->leads == FLOORS[k]) continue;
+        const Object *s = Stage_of(game->world.objects, game->world.count, (Project)FLOORS[k]);
+        if (s && s->build_stage >= BUILD_DONE) continue;
+        m->asked_floor = k;
+        m->asked_mask |= 1 << k;
+        ask(game, Q_FLOOR);
+        Object *best[3] = { NULL, NULL, NULL };
+        for (int i = 0; i < game->world.count; i++) {
+            Object *o = game->world.objects[i];
+            if (!crew(o) || leadsOther(o, FLOORS[k]) || o->commitment < 0.35f) continue;
+            for (int b = 0; b < 3; b++) {
+                if (best[b] && best[b]->commitment >= o->commitment) continue;
+                for (int c = 2; c > b; c--) best[c] = best[c - 1];
+                best[b] = o;
+                break;
+            }
+        }
+        char text[72];
+        for (int b = 0; b < 3; b++) {
+            if (!best[b]) continue;
+            snprintf(text, sizeof text, "%s (into it: %.0f%%)", best[b]->name, (double)(best[b]->commitment * 100.0f));
+            option(game, best[b]->cast_id, text);
+        }
+        if (!leadsOther(game->player, FLOORS[k])) option(game, 0, "Me - I'll lead it");
+        option(game, -1, "Nobody - leave it");
+        snprintf(text, sizeof text, "Nobody leads the %s. Who takes it?", Cast_projectName((Project)FLOORS[k]));
+        line(game, "Volkram", text);
+        return true;
+    }
+    return false;
+}
+
+static void answer(Game *game, int pick) {
+    Meeting *m = &game->meeting;
+    if (pick < 0 || pick >= m->n_options) return;
+    int v = m->option_value[pick];
+    char text[96];
+    snprintf(text, sizeof text, "%s", m->option_text[pick]);
+    switch (m->asking) {
+    case Q_JOIN:
+        for (int i = 0; i < game->world.count; i++) if (crew(game->world.objects[i])) lift(game->world.objects[i], v == 0 ? 0.05f : v == 2 ? 0.03f : 0.0f);
+        line(game, NULL, text);
+        break;
+    case Q_NAME:
+        game->fest_name = v;
+        line(game, NULL, text);
+        break;
+    case Q_FLOOR: {
+        int project = FLOORS[m->asked_floor];
+        Object *was_eager = NULL;
+        if (m->n_options > 0 && m->option_value[0] > 0) was_eager = castMember(game, m->option_value[0]);
+        setLead(game, project, v);
+        Object *got = v >= 0 ? castMember(game, v) : NULL;
+        if (got && got != game->player) {
+            lift(got, 0.08f);
+            snprintf(text, sizeof text, "Great - the %s is mine. You won't regret it!", Cast_projectName((Project)project));
+            line(game, got->name, text);
+        } else if (got) {
+            snprintf(text, sizeof text, "I'll lead the %s.", Cast_projectName((Project)project));
+            line(game, NULL, text);
+        } else line(game, "Volkram", "Okay - it stays open for now.");
+        if (was_eager && was_eager != got && m->number == 1) { /* (wanted it, didn't get it) */
+            lift(was_eager, -0.06f);
+            m->wait += 1.0f;
+        }
+        break;
+    }
+    case Q_CASH:
+        game->world.cash = (float)v;
+        for (int i = 0; i < game->world.count; i++) {
+            Object *o = game->world.objects[i];
+            if (crew(o)) lift(o, v > 1500 ? -0.04f : v < 1000 ? 0.02f : 0.0f);
+        }
+        line(game, NULL, text);
+        if (v > 1500) line(game, "Jana", "Eighty... okay. But that's a lot for some of us.");
+        break;
+    case Q_FOCUS:
+        game->focus = v;
+        game->world.focus = v;
+        if (v == FOCUS_PARTY)
+            for (int i = 0; i < game->world.count; i++) if (crew(game->world.objects[i])) lift(game->world.objects[i], 0.08f);
+        line(game, NULL, text);
+        break;
+    case Q_TOP_UP:
+        if (v) {
+            game->world.cash += TOP_UP;
+            for (int i = 0; i < game->world.count; i++) if (crew(game->world.objects[i])) lift(game->world.objects[i], -0.02f);
+        }
+        line(game, NULL, text);
+        break;
+    default: break;
+    }
+    bool again = m->asking == Q_FLOOR && m->kind == MEET_HERBERGE && m->number > 1; /* (the next floor nobody leads, if any) */
+    m->asking = Q_NONE;
+    if (!again) m->step++;
+}
+
+/* ---------- The agendas: one step at a time (false: it's over) ---------- */
+
+static bool kreuzberg(Game *game, int step) {
+    switch (step) {
+    case 0: line(game, "Volkram", "So. Every summer we say it - this year we do our own festival."); return true;
+    case 1: line(game, "Jana", "Small. Our friends, good music, no sponsors, no assholes."); return true;
+    case 2: line(game, "Toni", "And where? Not in a Kreuzberg backyard."); return true;
+    case 3: line(game, "Volkram", "Kurti's farm, near Wriezen. He said we can use the land. What do you say?");
+        ask(game, Q_JOIN);
+        option(game, 0, "I'm in - let's do it!");
+        option(game, 1, "That's a lot of work... but yes, I'm in.");
+        option(game, 2, "Only if we do it properly - toilets, awareness, all of it.");
+        return true;
+    case 4: line(game, "Jana", "Then we need a name.");
+        ask(game, Q_NAME);
+        for (int k = 0; k < 3; k++) option(game, k, NAMES[k]);
+        return true;
+    case 5: {
+        char text[96];
+        snprintf(text, sizeof text, "%s it is. At Kurti's, this summer!", Game_festivalName(game));
+        line(game, "Volkram", text);
+        return true;
+    }
+    case 6: line(game, "Jana", "Next week at the Herberge - everybody comes, and we sort out who does what."); return true;
+    default: return false;
+    }
+}
+
+static bool herbergeFirst(Game *game, int step) {
+    char text[96];
+    switch (step) {
+    case 0: snprintf(text, sizeof text, "Welcome, everybody! %s - this summer, at Kurti's in Wriezen.", Game_festivalName(game)); line(game, "Volkram", text); return true;
+    case 1: line(game, "Toni", "Rouven and I do the artists."); return true;
+    case 2: line(game, "Stella", "Doro and I do the decoration. And I hear there are snails to save."); return true;
+    case 3: line(game, "Pascal", "I'll do the light - and a proper floor for the main stage."); return true;
+    case 4: line(game, "Volkram", "The ES-Bar people do a food stand, the Darmstadt lot bring their geo-dome for the Kartoffelacker."); return true;
+    case 5: line(game, "Volkram", "Now the floors. Who leads the Obstwiese - the psytrance floor?"); return true;
+    case 6: floorQuestion(game, 0); return true;
+    case 7: line(game, "Volkram", "The Hofbuehne, in the yard - bands, slam, breakdance?"); return true;
+    case 8: floorQuestion(game, 1); return true;
+    case 9: line(game, "Volkram", "The Kartoffelacker - world music and the small talks?"); return true;
+    case 10: floorQuestion(game, 2); return true;
+    case 11: line(game, "Volkram", "And the hidden one behind the house - techno?"); return true;
+    case 12: floorQuestion(game, 3); return true;
+    case 13: line(game, "Volkram", "Money. How much does everybody put in the cash box?");
+        ask(game, Q_CASH);
+        option(game, 1200, "50 each - about 1200 EUR");
+        option(game, 1900, "80 each - about 1900 EUR");
+        option(game, 700, "30 each - about 700 EUR");
+        return true;
+    case 14: line(game, "Volkram", "Friday we drive out - you, me, Jana and Toni in my car. The rest come over the weekend."); return true;
+    default: return false;
+    }
+}
+
+/* What each floor has got to, said by who leads it. */
+static void report(Game *game, int k) {
+    char text[96];
+    const Object *s = Stage_of(game->world.objects, game->world.count, (Project)FLOORS[k]);
+    Object *lead = Cast_leadOf(game->world.objects, game->world.count, (Project)FLOORS[k]);
+    if (game->player->leads == FLOORS[k]) lead = game->player;
+    if (!s) return;
+    if (s->build_stage >= BUILD_DONE) snprintf(text, sizeof text, "The %s is built!", Cast_projectName((Project)FLOORS[k]));
+    else snprintf(text, sizeof text, "The %s: %s, %d%%.", Cast_projectName((Project)FLOORS[k]), Stage_stepName(s->build_stage), (int)(Stage_part(s) * 100.0f));
+    line(game, lead && lead != game->player ? lead->name : "Volkram", text);
+}
+
+static bool herbergeLater(Game *game, int step) {
+    char text[96];
+    if (step == 0) { snprintf(text, sizeof text, "Weekend %d done. Where are we?", game->weekend); line(game, "Volkram", text); return true; }
+    if (step >= 1 && step <= N_FLOORS) { report(game, step - 1); return true; }
+    switch (step) {
+    case N_FLOORS + 1: {
+        float mown = Field_done(&game->world);
+        if (game->world.inspected)
+            snprintf(text, sizeof text, "The Kartoffelacker's %d%% mown. %s", (int)(mown * 100.0f),
+                     game->world.snails_killed || Field_snailsLeft(&game->world) ? "And the snail fine still hurts." : "And the office was happy with the snails.");
+        else snprintf(text, sizeof text, "The Kartoffelacker's %d%% mown. %d snails are safe.", (int)(mown * 100.0f), game->world.snails_rescued);
+        line(game, "Toni", text);
+        return true;
+    }
+    case N_FLOORS + 2: {
+        float sum = 0.0f;
+        int n = 0;
+        for (int i = 0; i < game->world.count; i++) if (crew(game->world.objects[i])) { sum += game->world.objects[i]->commitment; n++; }
+        float mood = n ? sum / (float)n : 0.5f;
+        snprintf(text, sizeof text, "%d EUR left in the box. %s", (int)game->world.cash,
+                 mood > 0.65f ? "And everybody's buzzing." : mood > 0.45f ? "People are okay - a bit tired." : "Honestly, people are fed up.");
+        line(game, "Jana", text);
+        return true;
+    }
+    case N_FLOORS + 3: /* (a floor nobody leads: asked about, one at a time) */
+        if (openFloorQuestion(game)) return true; /* (asked again after the answer: the next open one) */
+        game->meeting.wait = 0.0f;
+        return true;
+    case N_FLOORS + 4:
+        line(game, "Volkram", "And next weekend - what first?");
+        ask(game, Q_FOCUS);
+        option(game, FOCUS_FLOORS, "The floors - we need stages");
+        option(game, FOCUS_SIDE, "The side jobs - mowing, snails, the art");
+        option(game, FOCUS_PARTY, "A party - we've earned it");
+        return true;
+    case N_FLOORS + 5:
+        if (game->world.cash >= 600.0f) { game->meeting.wait = 0.0f; return true; }
+        line(game, "Jana", "The box is nearly empty. Another 20 each?");
+        ask(game, Q_TOP_UP);
+        option(game, 1, "Yes - another 20 each (400 EUR)");
+        option(game, 0, "No, we'll manage");
+        return true;
+    case N_FLOORS + 6: line(game, "Volkram", "Friday at five at Kurti's. See you there!"); return true;
+    default: return false;
+    }
+}
+
+static bool agenda(Game *game) {
+    Meeting *m = &game->meeting;
+    if (m->kind == MEET_KREUZBERG) return kreuzberg(game, m->step);
+    if (m->number == 1) return herbergeFirst(game, m->step);
+    return herbergeLater(game, m->step);
+}
+
+/* ---------- Going there and coming back ---------- */
+
+static void fadeTo(Game *game, int after) {
+    game->meeting.fading = 1;
+    game->meeting.after = after;
+}
+
+/* The first weekend: the four of you in Volkram's car, the rest wait in Berlin till they're due. */
+static void firstWeekend(Game *game) {
+    static const char *const CAR[] = { "Volkram", "Jana", "Toni" };
+    for (int f = 0; f < 3; f++) {
+        Object *o = named(game, CAR[f]);
+        if (o) { unseat(o); o->ai.meeting = false; }
+    }
+    unseat(game->player);
+    game->meeting.kind = MEET_NONE;
+    game->weekend = 1;
+    game->weekend_from = -START_HOUR_FIRST;
+    game->story_hours = 0.0f;
+    game->daycycle.seconds = START_HOUR_FIRST * 3600.0f;
+    game_arrivalStart(game);
+    snprintf(game->story_line, sizeof game->story_line, "Friday - off to Wriezen, to Kurti's farm");
+    say(game, game->story_line);
+    game->message_time = CARD_TIME;
+}
+
+/* The weekends after: everybody at the fire on Friday at five. */
+static void nextWeekend(Game *game) {
+    Object *fire = NULL;
+    for (int i = 0; i < game->world.count && !fire; i++) if (game->world.objects[i]->type == OBJ_FIREPLACE) fire = game->world.objects[i];
+    Vec3 middle = fire ? fire->position : Site_yard();
+    int k = 0;
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        if (!crew(o) || !Game_inBerlin(o)) continue;
+        unseat(o);
+        o->ai.meeting = false;
+        float a = 2.399963f * (float)k++;
+        Vec3 at = vec3_add(middle, vec3(cosf(a) * 4.2f, 0.0f, sinf(a) * 4.2f));
+        at.y = World_groundHeight(&game->world, at.x, at.z, at.y + 3.0f);
+        put(o, at);
+    }
+    unseat(game->player);
+    Vec3 you = Site_yard();
+    you.y = World_groundHeight(&game->world, you.x, you.z, you.y + 3.0f);
+    put(game->player, you);
+    game->meeting.kind = MEET_NONE;
+    game->weekend++;
+    game->weekend_from = game->story_hours - NEXT_ARRIVE;
+    game->weekend_warned = false;
+    game->daycycle.seconds = NEXT_ARRIVE * 3600.0f;
+    snprintf(game->story_line, sizeof game->story_line, "Weekend %d - Friday, five o'clock, back at Kurti's", game->weekend);
+    say(game, game->story_line);
+    game->message_time = CARD_TIME;
+}
+
+static void dark(Game *game) {
+    Meeting *m = &game->meeting;
+    switch (m->after) {
+    case AFTER_KREUZBERG: m->kind = MEET_KREUZBERG; gather(game); break;
+    case AFTER_HERBERGE:  m->kind = MEET_HERBERGE; m->number++; gather(game); break;
+    case AFTER_FIRST_WEEKEND: firstWeekend(game); break;
+    case AFTER_NEXT_WEEKEND:  nextWeekend(game); break;
+    default: break;
+    }
+    m->after = AFTER_NONE;
+    m->fading = -1;
+}
+
+void game_meetingStart(Game *game) {
+    memset(&game->meeting, 0, sizeof game->meeting);
+    game->meeting.fade = 1.0f;
+    game->meeting.after = AFTER_KREUZBERG;
+    dark(game);
+}
+
+/* The weekend's end: Sunday at six (not in the middle of the drive in or a shopping run). */
+static void weekendEnd(Game *game) {
+    if (game->meeting.kind || game->meeting.fading || game->weekend <= 0 || game->arrival.stage == ARRIVAL_DRIVING || game->errand.stage != ERRAND_NONE) return;
+    float h = game->story_hours - game->weekend_from;
+    if (h >= WEEKEND_WARN && !game->weekend_warned) {
+        game->weekend_warned = true;
+        Object *v = named(game, "Volkram");
+        if (v) Ai_say(v, "We drive back at six - pack up your things!");
+        say(game, "Sunday afternoon: at six it's back to Berlin");
+    }
+    if (h < WEEKEND_END) return;
+    game->arrival.task = TASK_NONE;
+    fadeTo(game, AFTER_HERBERGE);
+}
+
+/* The agenda's done: off to the ground (or on to the Herberge from Grossbeerenstrasse). */
+static void finish(Game *game) {
+    Meeting *m = &game->meeting;
+    m->skipping = false;
+    fadeTo(game, m->kind == MEET_KREUZBERG ? AFTER_HERBERGE : m->number == 1 ? AFTER_FIRST_WEEKEND : AFTER_NEXT_WEEKEND);
+}
+
+void game_meeting(Game *game, const Input *input, float dt) {
+    Meeting *m = &game->meeting;
+    weekendEnd(game);
+    if (m->fading > 0 && (m->fade += dt / FADE_TIME) >= 1.0f) { m->fade = 1.0f; dark(game); }
+    else if (m->fading < 0 && (m->fade -= dt / FADE_TIME) <= 0.0f) { m->fade = 0.0f; m->fading = 0; }
+    if (m->kind == MEET_NONE || m->fading > 0) return;
+    if (input->cancel && !m->skipping) { m->skipping = true; say(game, "Skipping the meeting (the first answers)"); }
+    /* everybody looks at who's speaking */
+    if (m->speaker)
+        for (int i = 0; i < game->world.count; i++) {
+            Object *o = game->world.objects[i];
+            if (o->type != OBJ_HUMAN || !o->ai.meeting || o == m->speaker || !Game_inBerlin(o)) continue;
+            Vec3 to = vec3_sub(vec3_add(m->speaker->position, vec3(0.0f, 1.2f, 0.0f)), vec3_add(o->position, vec3(0.0f, 1.2f, 0.0f)));
+            if (vec3_length(to) > 0.1f) o->body.look_dir = vec3_normalize(to);
+        }
+    if (m->asking) {
+        if (m->skipping) answer(game, 0);
+        else if (input->choice >= 1 && input->choice <= m->n_options) answer(game, input->choice - 1);
+        return;
+    }
+    if (m->fading == 0 && (m->wait -= dt) > 0.0f && !m->skipping) return;
+    if (agenda(game)) {
+        if (!m->asking) m->step++;
+        if (m->skipping) m->wait = 0.0f;
+        return;
+    }
+    finish(game);
+}
+
+void game_meetingSkipStart(Game *game) {
+    Meeting *m = &game->meeting;
+    for (int guard = 0; game->weekend == 0 && guard < 500; guard++) {
+        m->skipping = true;
+        if (m->fading > 0) { m->fade = 1.0f; dark(game); continue; }
+        m->fading = 0;
+        m->fade = 0.0f;
+        if (!m->kind) break;
+        if (m->asking) { answer(game, 0); continue; }
+        if (agenda(game)) { if (!m->asking) m->step++; continue; }
+        finish(game);
+    }
+    m->fade = 0.0f;
+    m->fading = 0;
+    m->skipping = false;
+}
+
+int Game_meetingChoices(const Game *game, const char *out[TALK_CHOICES]) {
+    const Meeting *m = &game->meeting;
+    if (!m->kind || !m->asking) return 0;
+    for (int k = 0; k < m->n_options; k++) out[k] = m->option_text[k];
+    return m->n_options;
+}
+
+const char *game_meetingTask(const Game *game) {
+    const Meeting *m = &game->meeting;
+    if (!m->kind) return NULL;
+    if (m->asking) return "Your turn: answer with 1 - 5 (Backspace: skip the meeting)";
+    return m->kind == MEET_KREUZBERG ? "The first meeting: listen (Backspace: skip it)" : "At the Herberge: listen (Backspace: skip the meeting)";
+}
+
+/* After loading: whoever is in Berlin waits there (only the first weekend: the late ones). */
+void game_meetingLoaded(Game *game) {
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        if (o->type == OBJ_HUMAN && Game_inBerlin(o)) o->ai.meeting = true;
+    }
+    game->world.focus = game->focus;
+    if (game->weekend <= 0 && !game->meeting.kind) { game->weekend = 1; game->weekend_from = -START_HOUR_FIRST; } /* (a save from before the weekends) */
+}
diff --git a/entropie/src/game/game_story.c b/entropie/src/game/game_story.c
index e3c3932..579183a 100644
--- a/entropie/src/game/game_story.c
+++ b/entropie/src/game/game_story.c
@@ -19,15 +19,25 @@
 #define HELLO_TIME   90.0f  /* s at the fire saying hello before they get on with things */
 #define CREW_PAGES_SHOWN 3 /* the overview: who's who, now, the projects (game_dev.c CREW_PAGES the same) */
 
+/* On the ground (not waiting in Berlin). */
 static bool here(const Game *game, int id) {
     for (int i = 0; i < game->world.count; i++)
-        if (game->world.objects[i]->is_character && game->world.objects[i]->cast_id == id) return true;
+        if (game->world.objects[i]->is_character && game->world.objects[i]->cast_id == id) return !Game_inBerlin(game->world.objects[i]);
     return false;
 }
 
+static Object *inBerlin(const Game *game, int id) {
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        if (o->is_character && o->cast_id == id && Game_inBerlin(o)) return o;
+    }
+    return NULL;
+}
+
 /* The regulatory office: warned of the day before and on the day; then it comes and looks at where you build. */
 #define FINE_LEFT 40 /* EUR a snail still out there */
 #define FINE_DEAD 80 /* ... one mown over */
+#define FINE_MOST 600 /* ... at most */
 static void inspection(Game *game) {
     World *w = &game->world;
     if (w->inspected) return;
@@ -50,6 +60,7 @@ static void inspection(Game *game) {
     if (left > 0.0f) return;
     w->inspected = true;
     int out = Field_snailsLeft(w), dead = w->snails_killed, fine = FINE_LEFT * out + FINE_DEAD * dead;
+    if (fine > FINE_MOST) fine = FINE_MOST;
     if (fine == 0) {
         snprintf(game->story_line, sizeof game->story_line, "The man from the regulatory office walks the field: not a snail where you build. All fine - well done!");
         for (int i = 0; i < w->count; i++) {
@@ -90,9 +101,20 @@ void game_story(Game *game, float game_dt) {
         Vec3 at = path[WALK_IN_AT];
         at.x += WALK_IN_GAP * (float)come;
         at.y = World_groundHeight(&game->world, at.x, at.z, at.y + 3.0f);
-        Object *o = Cast_create(id, at);
-        if (!o) continue;
-        if (!World_add(&game->world, o)) { Object_destroy(o); continue; }
+        Object *o = inBerlin(game, id); /* (waiting in Berlin since the Herberge: over they come) */
+        if (o) {
+            o->sitting = false;
+            o->seat = NULL;
+            o->ai.meeting = false;
+            o->position = at;
+            o->body.replant = true;
+            o->velocity = o->movement = vec3_zero();
+            Object_syncColliders(o);
+        } else {
+            o = Cast_create(id, at);
+            if (!o) continue;
+            if (!World_add(&game->world, o)) { Object_destroy(o); continue; }
+        }
         o->ai.mode = AI_IDLE;
         o->ai.greet = true;              /* (over to the fire, hello) */
         o->ai.greet_left = HELLO_TIME;
@@ -123,6 +145,7 @@ static void doing(const Object *o, char *out, size_t n) {
     const Ai *ai = &o->ai;
     static const char *const JOBS[ORDER_COUNT] = { "-", "firewood", "trees", "the trash", "the fire" };
     if (o->type == OBJ_PLAYER) { snprintf(out, n, "(you)"); return; }
+    if (Game_inBerlin(o)) { snprintf(out, n, ai->meeting && o->sitting ? "in Berlin (at the table)" : "in Berlin"); return; }
     if (Body_isDown(&o->body))      snprintf(out, n, o->body.asleep ? "asleep" : "down on the ground");
     else if (o->in_tent)            snprintf(out, n, "in the tent");
     else if (o->riding)             snprintf(out, n, "in the car");
diff --git a/entropie/src/game/game_talk.c b/entropie/src/game/game_talk.c
index bf6f8c6..35e482c 100644
--- a/entropie/src/game/game_talk.c
+++ b/entropie/src/game/game_talk.c
@@ -147,6 +147,8 @@ static void shout(Game *game, int choice) {
 }
 
 int Game_choices(const Game *game, const char *out[TALK_CHOICES]) {
+    int meeting = Game_meetingChoices(game, out); /* (a question at a meeting: that first) */
+    if (meeting) return meeting;
     if (!game->shouting) return Talk_choices(&game->talk, out);
     Game *g = (Game *)game; /* (the lines live in the game) */
     int n = 0;
@@ -168,6 +170,7 @@ int Game_choices(const Game *game, const char *out[TALK_CHOICES]) {
    walked up to say something get to say it. */
 void game_talk(Game *game, const Input *input, float dt) {
     Talk *t = &game->talk;
+    if (game->meeting.kind && game->meeting.asking) return; /* (the keys answer the meeting) */
     bool leave = input->right_click;
     if (input->talk) {
         if (game->shouting) game->shouting = false;
diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c
index 4a366a6..4888680 100644
--- a/entropie/src/game/save.c
+++ b/entropie/src/game/save.c
@@ -143,6 +143,7 @@ static bool makeDir(const char *dir) {
 
 const char *Save_blocked(const Game *game) {
     if (game->arrival.stage == ARRIVAL_DRIVING) return "Not while you're driving in";
+    if (game->meeting.kind || game->meeting.fading) return "Not during a meeting in Berlin";
     if (game->player && game->player->riding) return "Not while you're in the car";
     return NULL;
 }
@@ -184,6 +185,7 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err,
     fprintf(f, "arrival %d %d %d %.9g %d %.9g %d %.9g %d %d\n", a->stage, a->task, a->step, (double)a->time, a->welcome,
             (double)a->welcome_t, a->chat, (double)a->chat_wait, a->greeted, idOf(&ids, a->car));
     fprintf(f, "guide %d\n", a->guide);
+    fprintf(f, "weekend %d %.9g %d %d %d\n", game->weekend, (double)game->weekend_from, game->weekend_warned, game->fest_name, game->focus);
     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);
@@ -456,6 +458,9 @@ static void applyGame(Game *game, const Parsed *p) {
         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, "weekend")) {
+            game->weekend = numI(&s); game->weekend_from = numF(&s); game->weekend_warned = numI(&s) != 0; game->fest_name = numI(&s); game->focus = numI(&s);
+        }
         else if (!strcmp(key, "field")) {
             w->inspected = numI(&s) != 0; w->snails_rescued = numI(&s); w->snails_killed = numI(&s); game->snail_warned = numI(&s);
             while (*s == ' ') s++;
@@ -712,6 +717,7 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
     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);
+    game_meetingLoaded(game);
 
     /* The ground: worn paths, then the wet (it lies on the paths) */
     FILE *g = fopen(ground, "rb");
diff --git a/entropie/src/main.c b/entropie/src/main.c
index e5619cf..a767973 100644
--- a/entropie/src/main.c
+++ b/entropie/src/main.c
@@ -65,7 +65,7 @@ static const char *const KEYS[] = { /* proportional font: no column padding */
     "C:  sit down (by the fire) / get up",
     "G:  one more person helps you,   R:  call someone over",
     "H:  everybody stops helping",
-    "Backspace:  call off showing someone how / where",
+    "Backspace:  call off showing someone how / where; at a meeting: skip it",
     "V:  first / third person",
     "I:  show / hide keys",
     "F1:  dev menu (change anybody's stats)",
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 43569b0..352690c 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -2523,6 +2523,11 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     ai->annoyed       = fmaxf(0.0f, ai->annoyed - dt / ANNOYED_FADE);
     g_dt = dt;
     ai_commitment(self, objects, count, dt);
+    if (ai->meeting) { /* (in Berlin: sat at the table, listening) */
+        stand(self);
+        ai->mode = AI_IDLE;
+        return;
+    }
     if (Body_isBusy(&self->body)) { /* on the ground, or being sick */
         stand(self);
         ai->pushing = false;
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index 8424455..0ac855c 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -205,6 +205,7 @@ typedef struct {
     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 */
+    bool           meeting;     /* in Berlin: at a meeting (or there between weekends) - sitting, listening, nothing else */
     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_project.c b/entropie/src/people/ai_project.c
index 6c8262e..68abfbf 100644
--- a/entropie/src/people/ai_project.c
+++ b/entropie/src/people/ai_project.c
@@ -247,6 +247,9 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) {
     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);
+        int focus = g_world->focus; /* (what the Herberge said this weekend is for) */
+        if ((focus == FOCUS_FLOORS && floor) || (focus == FOCUS_SIDE && !floor)) chance *= 1.6f;
+        else if (focus == FOCUS_PARTY) chance *= 0.5f;
         if (random01(ai) >= chance * dt) return false;
         ai->on_project = true;
         ai->shift = 0.0f;
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index 8825059..b474122 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -117,7 +117,7 @@ Mesh  Shapes_grill(void);
 
 typedef enum {
     FURN_WORKBENCH, FURN_TOOLWALL, FURN_SHELF, FURN_STOVE, FURN_COUNTER, FURN_TABLE,
-    FURN_CHAIR, FURN_BENCH, FURN_CAR, FURN_TIRES, FURN_TOILET, FURN_TAP, FURN_ASHTRAY, FURN_GATE, FURN_SMOKES, FURN_COUNT
+    FURN_CHAIR, FURN_BENCH, FURN_CAR, FURN_TIRES, FURN_TOILET, FURN_TAP, FURN_ASHTRAY, FURN_GATE, FURN_SMOKES, FURN_BAR, FURN_COUNT
 } FurnitureKind;
 #define SEAT_HEIGHT 0.45f  /* chairs and log benches */
 
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 2cf1ed6..4057975 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -1498,6 +1498,17 @@ Mesh Shapes_furniture(FurnitureKind kind, Vec3 size) {
         transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, 0.4f, -hz + 0.1f)), mat4_rotate_x(PI_F / 2.0f)));
         break;
     }
+    case FURN_BAR: { /* a bar counter (its front to +Z): dark wood, a lighter top, two beer taps, glasses */
+        const Vec3 wood = { 0.30f, 0.19f, 0.12f }, top = { 0.52f, 0.36f, 0.22f }, brass = { 0.78f, 0.64f, 0.30f }, glass = { 0.80f, 0.86f, 0.84f };
+        addBlock(&b, vec3(0.0f, (size.y - 0.05f) * 0.5f, 0.0f), vec3(size.x, size.y - 0.05f, size.z), wood);
+        addBlock(&b, vec3(0.0f, size.y - 0.025f, 0.05f), vec3(size.x + 0.06f, 0.05f, size.z + 0.1f), top);
+        for (int k = -1; k <= 1; k += 2) {
+            addBlock(&b, vec3((float)k * 0.4f, size.y + 0.2f, -hz + 0.15f), vec3(0.06f, 0.4f, 0.06f), brass);
+            addBlock(&b, vec3((float)k * 0.4f, size.y + 0.36f, -hz + 0.22f), vec3(0.04f, 0.04f, 0.14f), brass);
+        }
+        for (int k = 0; k < 5; k++) addBlock(&b, vec3(-hx + 0.4f + 0.25f * (float)k, size.y + 0.07f, hz - 0.1f), vec3(0.07f, 0.14f, 0.07f), glass);
+        break;
+    }
     case FURN_SMOKES: { /* an old cigarette machine on the wall (back at z = -hz): a row of packs behind glass, pull knobs, the coin slot, the tray */
         const Vec3 body = { 0.66f, 0.13f, 0.12f }, cream = { 0.90f, 0.86f, 0.74f }, glass = { 0.30f, 0.38f, 0.44f };
         const float bottom = 0.95f, h = 0.78f, d = 0.22f, front = -hz + d;
diff --git a/entropie/src/world/manor.c b/entropie/src/world/manor.c
index 078ad1e..82ee5e9 100644
--- a/entropie/src/world/manor.c
+++ b/entropie/src/world/manor.c
@@ -540,3 +540,73 @@ bool Manor_build(World *world, Vec3 origin, float yaw) {
     Object *look = Object_createStructure(mesh, origin, yaw, vec3(2.0f * HALF_LEN, WALL_TOP, PORCH_Z1 - LEFT_Z0));
     return look && World_add(world, look);
 }
+
+/* ---------- Berlin: the flat in Grossbeerenstrasse and the Herberge ----------
+   Two rooms off the map (BERLIN_X: the site is out of sight from there), closed
+   in, windows drawn on the walls as in the farm's house. */
+
+#define ROOM_H 3.2f /* (an old Berlin house: high ceilings) */
+
+static Object *furniture(Plan *p, FurnitureKind kind, Vec3 at, Vec3 size, Vec3 face) {
+    Object *o = Object_createFurniture(kind, toWorld(p, at), size, p->yaw + yaw_facing(face));
+    if (!o || !World_add(p->world, o)) { p->ok = false; return NULL; }
+    return o;
+}
+
+/* A closed room: floor, ceiling, four walls (x0..x1, z0..z1 inside), a locked door on its -z wall, windows on +z. */
+static void room(Plan *p, float x0, float x1, float z0, float z1, Vec3 wall, Vec3 floor, const char *door) {
+    box(p, vec3(x0 - T, -0.2f, z0 - T), vec3(x1 + T, 0.0f, z1 + T), floor, SOLID);
+    box(p, vec3(x0, ROOM_H, z0), vec3(x1, ROOM_H + SLAB, z1), CEILING, SOLID); /* (inside the walls: touching their tops it wasn't drawn) */
+    wallX(p, x0 - T, x1 + T, z1, z1 + T, 0.0f, ROOM_H + SLAB, NULL, 0, wall, SOLID);
+    wallX(p, x0 - T, x1 + T, z0 - T, z0, 0.0f, ROOM_H + SLAB, NULL, 0, wall, SOLID);
+    wallZ(p, z0, z1, x0 - T, x0, 0.0f, ROOM_H + SLAB, NULL, 0, wall, SOLID);
+    wallZ(p, z0, z1, x1, x1 + T, 0.0f, ROOM_H + SLAB, NULL, 0, wall, SOLID);
+    float xs[4];
+    int n = (int)((x1 - x0) / 2.6f);
+    if (n > 4) n = 4;
+    for (int i = 0; i < n; i++) xs[i] = x0 + (x1 - x0) * ((float)i + 0.5f) / (float)n;
+    for (int i = 0; i < n; i++) opening(p, OPENING_PANE, vec3(xs[i], 1.0f + 0.85f, z1), FACE_NZ, 1.1f, 1.7f);
+    lockedDoor(p, vec3((x0 + x1) * 0.5f + 1.5f, 0.0f, z0), FACE_PZ, 1.0f, door);
+}
+
+bool Berlin_build(World *world) {
+    static Plan plan;
+    Plan *p = &plan;
+    memset(p, 0, sizeof(*p));
+    p->world = world;
+    p->origin = vec3(BERLIN_X, 0.0f, 0.0f);
+    p->yaw = 0.0f;
+    p->ok = true;
+    const Vec3 WALL_FLAT = { 0.90f, 0.88f, 0.80f }, WALL_BAR = { 0.55f, 0.32f, 0.22f }, PLANKS = { 0.55f, 0.38f, 0.24f }, BOARDS = { 0.32f, 0.22f, 0.15f };
+    const Vec3 chair = { 0.45f, 0.9f, 0.45f }, shelf = { 1.2f, 1.9f, 0.4f };
+    /* The flat, 3rd floor: a big kitchen with the table in the middle */
+    room(p, -4.0f, 4.0f, -3.0f, 3.0f, WALL_FLAT, PLANKS, "The stairwell door - the meeting's here");
+    furniture(p, FURN_COUNTER, vec3(-3.6f, 0.0f, 0.4f), vec3(2.4f, 0.9f, 0.6f), FACE_PX);
+    furniture(p, FURN_STOVE, vec3(-3.6f, 0.0f, -1.5f), vec3(0.6f, 0.9f, 0.6f), FACE_PX);
+    furniture(p, FURN_SHELF, vec3(3.7f, 0.0f, 1.2f), shelf, FACE_NX);
+    furniture(p, FURN_BENCH, vec3(3.6f, 0.0f, -1.6f), vec3(1.8f, SEAT_HEIGHT, 0.5f), FACE_NX);
+    furniture(p, FURN_TABLE, vec3(0.0f, 0.0f, 0.0f), vec3(2.2f, 0.75f, 0.9f), FACE_PZ);
+    for (int k = -1; k <= 1; k++) {
+        furniture(p, FURN_CHAIR, vec3((float)k * 0.7f, 0.0f, -0.85f), chair, FACE_PZ);
+        furniture(p, FURN_CHAIR, vec3((float)k * 0.7f, 0.0f, 0.85f), chair, FACE_NZ);
+    }
+    /* The Herberge: the bar, a long table with two benches, two small tables */
+    const float hx = BERLIN_BAR;
+    room(p, hx - 6.0f, hx + 6.0f, -4.0f, 4.0f, WALL_BAR, BOARDS, "The door out to the street - the meeting's in here");
+    furniture(p, FURN_BAR, vec3(hx - 5.2f, 0.0f, 0.0f), vec3(4.5f, 1.1f, 0.6f), FACE_PX);
+    for (int k = -1; k <= 1; k += 2) furniture(p, FURN_SHELF, vec3(hx - 5.75f, 0.0f, (float)k * 1.3f), shelf, FACE_PX);
+    Object *table = furniture(p, FURN_TABLE, vec3(hx + 0.6f, 0.0f, 0.0f), vec3(4.4f, 0.77f, 0.9f), FACE_PZ);
+    for (int s = -1; s <= 1; s += 2) {
+        Object *bench = furniture(p, FURN_BENCH, vec3(hx + 0.6f, 0.0f, (float)s * 0.85f), vec3(4.4f, SEAT_HEIGHT, 0.3f), FACE_PZ);
+        if (bench) bench->table = table;
+    }
+    for (int k = -1; k <= 1; k += 2) {
+        furniture(p, FURN_TABLE, vec3(hx + 4.6f, 0.0f, (float)k * 2.4f), vec3(0.8f, 0.75f, 0.8f), FACE_PZ);
+        furniture(p, FURN_CHAIR, vec3(hx + 4.6f, 0.0f, (float)k * 2.4f - (float)k * 0.75f), chair, (Vec3){ 0.0f, 0.0f, (float)k });
+    }
+    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, p->origin, 0.0f, vec3(BERLIN_BAR + 14.0f, ROOM_H, 10.0f));
+    return look && World_add(world, look);
+}
diff --git a/entropie/src/world/manor.h b/entropie/src/world/manor.h
index f66c173..a65a2dd 100644
--- a/entropie/src/world/manor.h
+++ b/entropie/src/world/manor.h
@@ -29,4 +29,10 @@ bool Shell_build(World *world, Vec3 origin, float yaw);
 /* 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);
 
+/* Berlin, off the map: the flat in Grossbeerenstrasse 37 (3rd floor; its kitchen table at (BERLIN_X, 0, 0))
+   and the Herberge bar (its long table at BERLIN_X + BERLIN_BAR + 0.6). Where the meetings are (game_meeting.c). */
+#define BERLIN_X   700.0f
+#define BERLIN_BAR 24.0f
+bool Berlin_build(World *world);
+
 #endif /* WORLD_MANOR_H */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 45f5d8f..4644300 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -1525,6 +1525,7 @@ void Object_describe(const Object *obj, char *out, size_t size) {
             [FURN_TABLE] = "Table", [FURN_CHAIR] = "Chair - C to sit down", [FURN_BENCH] = "Log bench - C to sit down",
             [FURN_CAR] = "An old car, up for repairs", [FURN_TIRES] = "Old tyres",
             [FURN_TOILET] = "Toilet - P to use it", [FURN_TAP] = "Tap - the garden hose; fill the watering can here",
+            [FURN_BAR] = "The bar",
         };
         if (obj->variant == FURN_ASHTRAY) { snprintf(out, size, "Ashtray (an old tin) - %d butts in it", obj->stored); break; }
         if (obj->variant == FURN_SMOKES) { snprintf(out, size, "Cigarette machine - click: a pack of %d", SMOKES_PACKS); break; }
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index 4708f9a..b256195 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -1305,6 +1305,7 @@ bool Site_finish(World *world) {
         if (!s || !World_add(world, s)) return false;
     }
     if (!Field_prepare(world)) return false; /* (the Kartoffelacker's grass round its stage, the snails) */
+    if (!Berlin_build(world)) return false; /* (the meeting places, off the map) */
 
     /* Trees stand a little into the ground, wherever it is */
     for (int i = 0; i < world->count; i++) {
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 5e465f6..cbcc96e 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -549,6 +549,7 @@ static void snapBranch(World *world, Object *branch) {
    tired): a scraped shin. It hurts a moment; at a run it may trip you. */
 static void scraped(World *world, Object *who, Vec3 facing, float dt) {
     who->scrape_cool = fmaxf(0.0f, who->scrape_cool - dt);
+    if (who->sitting && who->sit_moving) return; /* (sitting down: over a bench, carefully) */
     const float s = who->look.height / 1.75f;
     /* (taking care, a leg only brushes it; in a hurry or clumsy it scrapes) */
     float careless = fmaxf(smoothstepf(1.5f, 3.0f, who->ground_speed) * (1.2f - 0.6f * who->fitness), Body_clumsiness(&who->body));
@@ -1412,7 +1413,7 @@ static void windBlows(World *world, Object *o, float dt) {
 #define SNATCH_GUST  0.3f  /* ... only in a gust this strong (0..1) */
 #define SNATCH_RATE  0.35f /* chance a second for every 4 m/s over it, in a full gust */
 static void windSnatches(World *world, Object *o, float dt) {
-    if (!o->is_character || o->indoors > 0.0f || o->in_tent || o->riding || world->gust < SNATCH_GUST) return;
+    if (!o->is_character || o->indoors > 0.0f || o->in_tent || o->riding || world->gust < SNATCH_GUST || o->position.x > WORLD_BERLIN_FROM) return;
     float speed = hypotf(world->wind.x, world->wind.z);
     if (speed < SNATCH_PAPER) return;
     for (int h = 0; h < 2; h++) {
@@ -1482,7 +1483,7 @@ static void windUmbrella(World *world, Object *o, float dt) {
 #define HAT_RATE  0.3f  /* ... chance a second for every 3 m/s over it, in a full gust (a beanie: a tenth) */
 
 static void windHat(World *world, Object *o, float dt) {
-    if (!o->look.hat || o->look.hood_up || o->indoors > 0.0f || o->in_tent || o->riding || world->gust < HAT_GUST) return;
+    if (!o->look.hat || o->look.hood_up || o->indoors > 0.0f || o->in_tent || o->riding || world->gust < HAT_GUST || o->position.x > WORLD_BERLIN_FROM) return; /* (Berlin: indoors) */
     float speed = hypotf(world->wind.x, world->wind.z), over = speed - HAT_WIND;
     if (over <= 0.0f) return;
     float rate = HAT_RATE * over / 3.0f * world->gust * (o->look.hat == HAT_BEANIE ? 0.1f : 1.0f) / (1.0f + 3.0f * (float)o->hats_lost); /* (lost it before: pulled down tight) */
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index 77d24e1..b4c7493 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -40,6 +40,9 @@ typedef struct { NoiseKind kind; Vec3 at; float loud; int surface; } Noise;
 
 #define WORLD_CASH 1200.0f /* € in the cash box at the start */
 #define WORLD_FIELD_CELLS 520 /* the Kartoffelacker's cells (field.h FIELD_CELLS) */
+#define WORLD_BERLIN_FROM 400.0f /* x beyond this: Berlin, off the map (manor.h BERLIN_X) */
+/* What a weekend is for (decided at the Herberge): the floors, the side jobs, or a party. */
+enum { FOCUS_NONE, FOCUS_FLOORS, FOCUS_SIDE, FOCUS_PARTY };
 
 typedef struct World {
     float    dt_props;   /* (this frame's step, for the props that move: gates) */
@@ -82,6 +85,7 @@ typedef struct World {
     int      mow_rev;      /* ... changed this often (the grass is built again) */
     int      snails_rescued, snails_killed; /* the vineyard snails (field.c) ... */
     bool     inspected;    /* ... the regulatory office has been */
+    int      focus;        /* FOCUS_*: what this weekend is for (the game sets it) */
 } World;
 
 /* A sound made here (dropped when the list is full: nobody's listening). */