Nimbin[12]?Games / Entropie / commits / 1467eae

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.45.0: the why questions

Christian Immanuel · 2026-10-28 14:59 · 1467eaed672b1688389d9358b6ec8cd521f71555

- Why do festivals hand out Urinellas, have an awareness team, say "no
  shirt, no service"? Each comes up when you see the reason on the ground
  (or somebody asks it at the Herberge)
- Ask who knows (X, 4, then 5): they explain it; who doesn't sends you on
- The next Herberge decides: do it or not this year (moods and cash follow)
- Your notebook (J): the festival, who leads the floors, the cash box and
  the why questions as far as each has got
- Floors asked about at one Herberge are asked again at the next
 commit_message.txt                |  21 +++----
 entropie/ARCHITECTURE.md          |   4 +-
 entropie/Makefile                 |   2 +-
 entropie/README.md                |   6 +-
 entropie/STATE.md                 |  30 ++++++++-
 entropie/commit_message.txt       |  21 +++----
 entropie/src/core/input.c         |   1 +
 entropie/src/core/input.h         |   1 +
 entropie/src/game/game.c          |   1 +
 entropie/src/game/game.h          |   7 ++-
 entropie/src/game/game_actions.c  |   1 +
 entropie/src/game/game_internal.h |   2 +
 entropie/src/game/game_meeting.c  |  57 +++++++++++++++--
 entropie/src/game/game_why.c      | 125 ++++++++++++++++++++++++++++++++++++++
 entropie/src/game/save.c          |   2 +
 entropie/src/main.c               |  16 ++++-
 entropie/src/people/talk.c        |  12 ++++
 entropie/src/world/why.c          |  59 ++++++++++++++++++
 entropie/src/world/why.h          |  34 +++++++++++
 entropie/src/world/world.h        |   5 ++
 20 files changed, 369 insertions(+), 38 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index cf82a87..f1799b2 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,13 +1,10 @@
-entropie 2.44.0: Berlin - the meetings and the weekends
+entropie 2.45.0: the why questions
 
-- 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
+- Why do festivals hand out Urinellas, have an awareness team, say "no
+  shirt, no service"? Each comes up when you see the reason on the ground
+  (or somebody asks it at the Herberge)
+- Ask who knows (X, 4, then 5): they explain it; who doesn't sends you on
+- The next Herberge decides: do it or not this year (moods and cash follow)
+- Your notebook (J): the festival, who leads the floors, the cash box and
+  the why questions as far as each has got
+- Floors asked about at one Herberge are asked again at the next
diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index e48ac28..7ee68f9 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` - 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/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), the why questions' content (`why.c`), 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`), the meetings in Berlin and the weekends (`game_meeting.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`), the why questions coming up and your notebook (`game_why.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 951dcda..d43025e 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.44.0
+VERSION   := 2.45.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index fff6c0e..fc9d1d2 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -30,6 +30,9 @@ people living there.
   lead and helpers - or by you (hold left click at a stage). Talk to people
   about it (X, then 4): praise them, ask someone to lead, take a lead over.
   Fed up, people give up leading or helping
+- **Why questions:** why do festivals hand out Urinellas, have an awareness
+  team, say "no shirt, no service"? See it happen, ask who knows, decide at
+  the Herberge. Your notebook (J) keeps track
 - **Side jobs:** mow the Kartoffelacker with the scythe, rescue the
   protected vineyard snails before the regulatory office comes (or pay a
   fine), bring Kerry logs to carve into figures with the chainsaw
@@ -94,6 +97,7 @@ people living there.
 | V             | Third / first person |
 | I             | Show all keys in game |
 | Tab           | Your stats (Left / Right: other page) |
+| J             | Your notebook: the festival, the why questions |
 | F1            | Dev menu: pick anyone, change body, needs, temperament, likes and skills (Up / Down, Left / Right); teleport you to the parkour or the yard; time of day, game speed, weather, wind, skip the start |
 | F2            | Dev: the crew overview - who's who, what they do now, the projects (Left / Right) |
 | Esc           | Menu: resume, save, load, keys, quit (the game waits meanwhile) |
@@ -142,5 +146,5 @@ Dependencies point downward: `main → game → world/people → render → core
 
 ## Version
 
-`make version` prints e.g. `entropie 2.44.0 (build 3012ea9)`. The build id is
+`make version` prints e.g. `entropie 2.45.0 (build db5ab7c)`. The build id is
 an md5 of all sources and assets.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 18bcbd8..a52a8db 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -83,7 +83,7 @@ things the way they do.
    chainsaw art. (2.43)
 5. Berlin: Grossbeerenstrasse and the Herberge as places, the meetings, going
    back and forth between weekends. (2.44)
-6. The "why" questions and their options.
+6. The "why" questions and their options. (2.45)
 7. The festival (guests, the floors running, the storm and the evacuation),
    the clean-up with the supporters, the drive home.
 
@@ -352,6 +352,22 @@ Painted in the rotoscope style of *Waking Life* (2001):
 - Backspace skips a meeting (the first answer to everything). No saving
   during a meeting
 
+### The why questions (J: your notebook)
+
+- Things festivals do today, found out on the way: free Urinellas, an
+  awareness team, "no shirt, no service". Each comes up when you see the
+  reason - a woman peeing standing up with a Urinella, somebody down drunk
+  that nobody noticed, a guy working shirtless (within 25 m) - or, if it
+  hasn't by then, somebody asks it at the Herberge (one a meeting)
+- Your notebook (J) says what made you wonder and who could tell you. Ask
+  them (X, 4, then 5 - "Why do festivals ...?"): who knows explains it (and
+  likes being asked), who doesn't sends you to someone who does. Understood,
+  the notebook has the whole reason
+- The next Herberge decides: do we do it, or not this year. Yes: those who
+  told you are happier (the Urinellas cost 60 EUR); no: a bit less
+- The notebook also has who leads the floors, the cash box and what the
+  weekend is for
+
 ### Arriving
 
 - The game starts in Volkram's car (he drives, Jana beside him, you and Toni
@@ -1464,6 +1480,13 @@ Round 2.44 (Berlin: the meetings and the weekends):
 - 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.
 
+Round 2.45 (the why questions):
+- `world/why.c` (WhyInfo: the question, what made you wonder, who knows, their answer, the whole reason, the option, who raises it at the Herberge and how, its cost): WHY_URINELLA (Jana, Julia, Stella, Doro; 60 EUR), WHY_AWARENESS (Stella, Doro, Julia, Volkram), WHY_SHIRT (Jana, Julia, Kilian, Stella). `World.why[]` WHY_UNKNOWN / RAISED / UNDERSTOOD / YES / NO, `why_heard` (came up at the Herberge); saved (`why` line).
+- `game/game_why.c`: every WHY_CHECK 1 s on the ground, within SEE_NEAR 25 m: a woman with Urinellas peeing (AI_PEE, busy), somebody down drunk (passed out or drunk over 0.7), a man shirtless (TOP_BARE) - it comes up (a message, WHY_SHOWN 7 s). The notebook (`Game_notesLines`, J: `input.notes`, drawn like the crew overview, `main.c` drawNotes; not with F2): leads, cash, the weekend's focus, the questions (wrapped at 100 characters).
+- `talk.c`: about the festival, answer 5 is the open why question (else "Bye"): a knower answers (UNDERSTOOD, +0.03), else "No idea. Ask ...".
+- `game_meeting.c` (later Herberges): an unknown question raised by its raiser (one a meeting), then each understood one asked (Q_WHY: the option or "Not this year"; knowers +0.05 / -0.04; the cost from the cash box). `asked_mask` now cleared for every meeting (floors asked last time weren't asked again).
+- Checked: shirtless Toni next to you raised it; Jana explained it; the Herberge decided yes; Stella raised the Urinellas there, Doro explained them, the next Herberge decided yes (60 EUR); Julia raised the awareness team; the notebook through all of it.
+
 ## Open tasks
 
 Most important first.
@@ -1475,7 +1498,8 @@ Most important first.
 - 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
 - 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
+- Why questions, still to do: more of them (deposit on cups, free water, a quiet place, ...); what each decision does at the festival
+- Next: 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)
@@ -1509,4 +1533,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.44.0 (build 3012ea9)`
+State of `entropie 2.45.0 (build db5ab7c)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index cf82a87..f1799b2 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,13 +1,10 @@
-entropie 2.44.0: Berlin - the meetings and the weekends
+entropie 2.45.0: the why questions
 
-- 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
+- Why do festivals hand out Urinellas, have an awareness team, say "no
+  shirt, no service"? Each comes up when you see the reason on the ground
+  (or somebody asks it at the Herberge)
+- Ask who knows (X, 4, then 5): they explain it; who doesn't sends you on
+- The next Herberge decides: do it or not this year (moods and cash follow)
+- Your notebook (J): the festival, who leads the floors, the cash box and
+  the why questions as far as each has got
+- Floors asked about at one Herberge are asked again at the next
diff --git a/entropie/src/core/input.c b/entropie/src/core/input.c
index bb1e44e..6807f7f 100644
--- a/entropie/src/core/input.c
+++ b/entropie/src/core/input.c
@@ -38,6 +38,7 @@ void Input_poll(Input *input) {
         case SDL_SCANCODE_P:      input->pee = true;         break;
         case SDL_SCANCODE_K:      input->smoke = true;       break;
         case SDL_SCANCODE_B:      input->pockets = true;     break;
+        case SDL_SCANCODE_J:      input->notes = true;       break;
         case SDL_SCANCODE_Z:      input->sleep = true;       break;
         case SDL_SCANCODE_LSHIFT: input->sprint_tap = true;  break;
         case SDL_SCANCODE_1:      input->choice = 1;         break;
diff --git a/entropie/src/core/input.h b/entropie/src/core/input.h
index 045b77d..c66b012 100644
--- a/entropie/src/core/input.h
+++ b/entropie/src/core/input.h
@@ -44,6 +44,7 @@ typedef struct {
     bool  dev_menu;      /* F1 pressed: dev menu on / off */
     bool  crew_menu;     /* F2 pressed: the crew overview on / off (dev) */
     bool  pockets;       /* B pressed: your pockets open / shut */
+    bool  notes;         /* J pressed: your notebook open / shut (the festival, the why questions) */
     char  text[32];      /* typed this frame (only while typing a name: see Platform_typing) */
     bool  erase;         /* Backspace pressed (typing: one letter back) */
     bool  quick_save;    /* F5 pressed */
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index c8dc88f..a691995 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -377,6 +377,7 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     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;
+    if (!game->meeting.kind) game_why(game, dt);
     Ai_setSettled(game->arrival.task == TASK_NONE);
     tooHeavy(game);
     bool warm = game->player->look.top == TOP_JACKET;
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index 1de8806..4d2cfc8 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -119,7 +119,7 @@ typedef struct {
     float    blow_told;     /* "your tent is blowing about" said: not again for a while */
     float    story_hours;   /* game hours since the start on the ground (the crew turns up by it: game_story.c) */
     float    story_check;   /* until the next look at who's due */
-    char     story_line[128]; /* what was said about it (shown as the message) */
+    char     story_line[192]; /* what was said about it (shown as the message) */
     bool     show_crew;     /* the crew overview (F2, dev) ... */
     int      crew_page;     /* ... which page (Left / Right): who's who, now, the projects */
     Errand   errand;        /* a shopping run */
@@ -133,6 +133,8 @@ typedef struct {
     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 */
+    bool     show_notes;    /* your notebook (J): the festival, the why questions */
+    float    why_check;     /* until the next look round for something that makes you wonder (game_why.c) */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
@@ -191,6 +193,9 @@ void Game_renderAir(Game *game, Renderer *renderer);
 /* Your pockets as lines of text: a heading, what's in them, what Enter does; *picked: the line picked. */
 int Game_pocketLines(Game *game, char lines[][96], int max, int *picked);
 
+/* Your notebook (J) as lines of text, its title. Returns how many. */
+int Game_notesLines(const Game *game, char lines[][128], int max, const char **title);
+
 #define DEV_LINES 13
 /* The crew overview (F2) as lines of text, its title. Returns how many. */
 int Game_crewLines(const Game *game, char lines[][128], int max, const char **title);
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index 9f160c3..c79678c 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -362,6 +362,7 @@ static void usePocket(Game *game, int row) {
 /* B: open or shut your pockets; Up / Down picks, Enter takes it out (gives it to who's in front). */
 static void game_pockets(Game *game, const Input *input) {
     if (input->pockets) game->show_pockets = !game->show_pockets;
+    if (input->notes) { game->show_notes = !game->show_notes; if (game->show_notes) game->show_crew = false; }
     if (!game->show_pockets || game->show_dev) return;
     if (input->menu_move) {
         for (int k = 1; k <= POCKET_ROWS; k++) { /* (to the next one with something in it) */
diff --git a/entropie/src/game/game_internal.h b/entropie/src/game/game_internal.h
index ea1a2e9..52b6dae 100644
--- a/entropie/src/game/game_internal.h
+++ b/entropie/src/game/game_internal.h
@@ -45,6 +45,8 @@ 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);
+/* game_why.c: looking round for what makes you wonder (the why questions coming up). */
+void game_why(Game *game, float dt);
 
 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
index 5864c96..0410690 100644
--- a/entropie/src/game/game_meeting.c
+++ b/entropie/src/game/game_meeting.c
@@ -25,6 +25,7 @@
 #include "world/field.h"
 #include "world/manor.h"
 #include "world/stage.h"
+#include "world/why.h"
 
 #define FADE_TIME    1.4f   /* s to dark (and back) */
 #define LINE_GAP     4.2f   /* s between lines */
@@ -38,7 +39,7 @@
 #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 };
+enum { Q_NONE, Q_JOIN, Q_NAME, Q_FLOOR, Q_CASH, Q_FOCUS, Q_TOP_UP, Q_WHY };
 
 static const char *const NAMES[] = { "Entropie", "Kartoffelrave", "Wriezen Calling" };
 static const int FLOORS[] = { PROJECT_OBSTWIESE, PROJECT_HOFBUEHNE, PROJECT_KARTOFFELACKER, PROJECT_HIDDEN };
@@ -185,6 +186,7 @@ static void gather(Game *game) {
     m->step = 0;
     m->wait = FIRST_WAIT;
     m->asking = Q_NONE;
+    m->asked_mask = 0;
 }
 
 /* ---------- Questions ---------- */
@@ -335,6 +337,17 @@ static void answer(Game *game, int pick) {
             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_WHY: {
+        int k = m->asked_floor;
+        game->world.why[k] = v;
+        if (v == WHY_YES) game->world.cash -= (float)Why_get(k)->cost;
+        for (int i = 0; i < game->world.count; i++) { /* (those who told you care most) */
+            Object *o = game->world.objects[i];
+            if (crew(o) && Why_knows(o, k)) lift(o, v == WHY_YES ? 0.05f : -0.04f);
+        }
+        line(game, NULL, text);
+        break;
+    }
     case Q_TOP_UP:
         if (v) {
             game->world.cash += TOP_UP;
@@ -344,7 +357,7 @@ static void answer(Game *game, int pick) {
         break;
     default: break;
     }
-    bool again = m->asking == Q_FLOOR && m->kind == MEET_HERBERGE && m->number > 1; /* (the next floor nobody leads, if any) */
+    bool again = (m->asking == Q_FLOOR || m->asking == Q_WHY) && m->kind == MEET_HERBERGE && m->number > 1; /* (the next one, if any) */
     m->asking = Q_NONE;
     if (!again) m->step++;
 }
@@ -416,6 +429,24 @@ static void report(Game *game, int k) {
     line(game, lead && lead != game->player ? lead->name : "Volkram", text);
 }
 
+/* A why question you found out about: does the festival do it? */
+static bool whyQuestion(Game *game) {
+    Meeting *m = &game->meeting;
+    for (int k = 0; k < WHY_COUNT; k++) {
+        if (game->world.why[k] != WHY_UNDERSTOOD || (m->asked_mask & (16 << k))) continue;
+        m->asked_mask |= 16 << k;
+        m->asked_floor = k; /* (the question asked about) */
+        ask(game, Q_WHY);
+        option(game, WHY_YES, Why_get(k)->option);
+        option(game, WHY_NO, "Not this year");
+        char text[96];
+        snprintf(text, sizeof text, "%s - you found out why. Do we do it?", Why_get(k)->title);
+        line(game, "Volkram", text);
+        return true;
+    }
+    return false;
+}
+
 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; }
@@ -444,21 +475,37 @@ static bool herbergeLater(Game *game, int step) {
         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:
+    case N_FLOORS + 4: { /* a why question that hasn't come up on the ground: somebody asks it here */
+        World *w = &game->world;
+        for (int k = 0; k < WHY_COUNT; k++) {
+            if (w->why[k] != WHY_UNKNOWN || !named(game, Why_get(k)->raiser)) continue;
+            w->why[k] = WHY_RAISED;
+            w->why_heard |= 1 << k;
+            line(game, Why_get(k)->raiser, Why_get(k)->raise);
+            return true;
+        }
+        game->meeting.wait = 0.0f;
+        return true;
+    }
+    case N_FLOORS + 5: /* (found out why: do we? one after the other) */
+        if (whyQuestion(game)) return true;
+        game->meeting.wait = 0.0f;
+        return true;
+    case N_FLOORS + 6:
         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:
+    case N_FLOORS + 7:
         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;
+    case N_FLOORS + 8: line(game, "Volkram", "Friday at five at Kurti's. See you there!"); return true;
     default: return false;
     }
 }
diff --git a/entropie/src/game/game_why.c b/entropie/src/game/game_why.c
new file mode 100644
index 0000000..929e190
--- /dev/null
+++ b/entropie/src/game/game_why.c
@@ -0,0 +1,125 @@
+/*
+ * The why questions coming up on the ground: you see something that makes you
+ * wonder - a woman peeing standing up with a Urinella, somebody down drunk
+ * that nobody noticed, a guy working shirtless - and it goes into your
+ * notebook (J) with who could tell you why. Asked (talk.c, about the
+ * festival), it's understood; the Herberge decides (game_meeting.c).
+ *
+ * The notebook: the festival (its name, who leads what, the cash box, what
+ * this weekend is for) and the why questions, as far as each has got.
+ */
+
+#include <math.h>
+
+#include "game/game_internal.h"
+#include "world/cast.h"
+#include "world/why.h"
+
+#define WHY_CHECK  1.0f  /* s between looks round */
+#define SEE_NEAR   25.0f /* m: what you notice */
+#define WHY_SHOWN  7.0f  /* s it stays on screen */
+
+static bool near(const Game *game, const Object *o) {
+    return hypotf(o->position.x - game->player->position.x, o->position.z - game->player->position.z) < SEE_NEAR;
+}
+
+static void comeUp(Game *game, int k, const char *what) {
+    game->world.why[k] = WHY_RAISED;
+    snprintf(game->story_line, sizeof game->story_line, "%s (J: your notebook)", what);
+    say(game, game->story_line);
+    game->message_time = WHY_SHOWN;
+}
+
+void game_why(Game *game, float dt) {
+    if ((game->why_check -= dt) > 0.0f || Game_inBerlin(game->player)) return;
+    game->why_check = WHY_CHECK;
+    World *w = &game->world;
+    char what[128];
+    for (int i = 0; i < w->count; i++) {
+        const Object *o = w->objects[i];
+        if (o->type != OBJ_HUMAN || !near(game, o)) continue;
+        if (w->why[WHY_URINELLA] == WHY_UNKNOWN && o->female && o->urinella && o->urinellas > 0 && o->ai.mode == AI_PEE && Body_isBusy(&o->body)) {
+            snprintf(what, sizeof what, "%s pees standing up with a Urinella. Why would a festival hand those out?", o->name);
+            comeUp(game, WHY_URINELLA, what);
+            return;
+        }
+        if (w->why[WHY_AWARENESS] == WHY_UNKNOWN && Body_isDown(&o->body) && (o->passed_out || o->drunk > 0.7f)) {
+            snprintf(what, sizeof what, "%s is down, out cold - and nobody noticed. Who looks out for people at a festival?", o->name);
+            comeUp(game, WHY_AWARENESS, what);
+            return;
+        }
+        if (w->why[WHY_SHIRT] == WHY_UNKNOWN && !o->female && o->look.top == TOP_BARE) {
+            snprintf(what, sizeof what, "%s works shirtless. Festival bars that say \"no shirt, no service\" - why?", o->name);
+            comeUp(game, WHY_SHIRT, what);
+            return;
+        }
+    }
+}
+
+/* ---------- The notebook (J) ---------- */
+
+/* A long text into lines of at most `width` characters (at spaces); returns how many. */
+static int wrap(const char *text, const char *indent, char lines[][128], int max, int width) {
+    int n = 0;
+    const char *p = text;
+    while (*p && n < max) {
+        int len = (int)strlen(p), take = len;
+        if (take > width) {
+            take = width;
+            while (take > 0 && p[take] != ' ') take--;
+            if (take == 0) take = width;
+        }
+        snprintf(lines[n++], 128, "%s%.*s", indent, take, p);
+        p += take;
+        while (*p == ' ') p++;
+    }
+    return n;
+}
+
+int Game_notesLines(const Game *game, char lines[][128], int max, const char **title) {
+    static char head[128];
+    const World *w = &game->world;
+    int n = 0;
+    snprintf(head, sizeof head, "Your notebook - %s   (J: close)", Game_festivalName(game));
+    *title = head;
+    /* who leads the floors */
+    static const int FLOORS[] = { PROJECT_OBSTWIESE, PROJECT_HOFBUEHNE, PROJECT_KARTOFFELACKER, PROJECT_HIDDEN };
+    char who[128] = "Leading the floors:";
+    size_t len = strlen(who);
+    for (int k = 0; k < 4 && len < sizeof who - 30; k++) {
+        const Object *lead = Cast_leadOf(w->objects, w->count, (Project)FLOORS[k]);
+        if (game->player->leads == FLOORS[k]) lead = game->player;
+        static const char *const SHORT[] = { "Obstwiese", "Hofbuehne", "Kartoffelacker", "techno" };
+        snprintf(who + len, sizeof who - len, "%s %s %s", k ? "," : "", SHORT[k], lead ? (lead == game->player ? "you" : lead->name) : "nobody");
+        len = strlen(who);
+    }
+    if (n < max) snprintf(lines[n++], 128, "%s", who);
+    static const char *const FOCUS[] = { "-", "the floors", "the side jobs", "a party" };
+    if (n < max) snprintf(lines[n++], 128, "Cash box: %d EUR   Weekend %d, for: %s", (int)w->cash, game->weekend, FOCUS[(unsigned)game->focus < 4 ? game->focus : 0]);
+    if (n < max) snprintf(lines[n++], 128, " ");
+    if (n < max) snprintf(lines[n++], 128, "Why do festivals do that?");
+    for (int k = 0; k < WHY_COUNT && n < max; k++) {
+        const WhyInfo *q = Why_get(k);
+        switch (w->why[k]) {
+        case WHY_UNKNOWN: snprintf(lines[n++], 128, "-  ? (hasn't come up yet)"); break;
+        case WHY_RAISED:
+            snprintf(lines[n++], 128, "-  %s", q->ask);
+            if (n < max) {
+                char heard[128];
+                snprintf(heard, sizeof heard, "%s asked it at the Herberge: \"%s\"", q->raiser, q->raise);
+                n += wrap(w->why_heard & (1 << k) ? heard : q->seen, "      ", lines + n, max - n, 100);
+            }
+            if (n < max) snprintf(lines[n++], 128, "      Ask %s, %s, %s or %s (X, 4, then 5)", q->knowers[0], q->knowers[1], q->knowers[2], q->knowers[3]);
+            break;
+        case WHY_UNDERSTOOD:
+            snprintf(lines[n++], 128, "-  %s", q->ask);
+            if (n < max) n += wrap(q->because, "      ", lines + n, max - n, 100);
+            if (n < max) snprintf(lines[n++], 128, "      Decided at the next Herberge");
+            break;
+        default:
+            snprintf(lines[n++], 128, "-  %s: %s", q->title, w->why[k] == WHY_YES ? "yes - we do it" : "not this year");
+            break;
+        }
+    }
+    return n;
+}
diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c
index 4888680..5e09b95 100644
--- a/entropie/src/game/save.c
+++ b/entropie/src/game/save.c
@@ -185,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, "why %d %d %d %d\n", w->why[0], w->why[1], w->why[2], w->why_heard);
     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);
@@ -458,6 +459,7 @@ 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, "why")) { for (int k = 0; k < WHY_COUNT; k++) w->why[k] = numI(&s); w->why_heard = 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);
         }
diff --git a/entropie/src/main.c b/entropie/src/main.c
index a767973..3a1cd35 100644
--- a/entropie/src/main.c
+++ b/entropie/src/main.c
@@ -71,7 +71,7 @@ static const char *const KEYS[] = { /* proportional font: no column padding */
     "F1:  dev menu (change anybody's stats)",
     "F2:  the crew overview (Left / Right: who's who, now, the projects)",
     "Tab:  your stats (arrow keys: next page)",
-    "K:  smoke;   B:  your pockets (Up / Down, Enter: take out or give)",
+    "K:  smoke;   B:  your pockets (Up / Down, Enter: take out or give);   J:  your notebook",
     "Esc:  menu (resume, save, load, keys, quit);  F5 quick save, F9 quick load;  M sound off / on",
 };
 #define KEY_COUNT ((int)(sizeof(KEYS) / sizeof(KEYS[0])))
@@ -133,6 +133,19 @@ static void drawCrew(Ui *ui, const Game *game, int width, int height) {
         Ui_drawTextLeft(ui, CREW_LABEL + 1 + i, lines[i], x + 22.0f, top - 72.0f - row * (float)i, width, height);
 }
 
+/* Your notebook (J): the same sheet (the crew overview's places: never both). */
+static void drawNotes(Ui *ui, const Game *game, int width, int height) {
+    char lines[CREW_LINES][128];
+    const char *title = "";
+    int n = Game_notesLines(game, lines, CREW_LINES - 1, &title);
+    const float row = 28.0f, pw = fminf(1240.0f, (float)width - 36.0f), ph = 60.0f + row * (float)n;
+    const float x = ((float)width - pw) * 0.5f, top = (float)height * 0.5f + ph * 0.5f;
+    Ui_drawPatch(ui, x, top - ph, pw, ph, 0.93f, 0.91f, 0.82f, 0.95f, 1.0f, width, height);
+    Ui_drawTextLeft(ui, CREW_LABEL, title, x + 22.0f, top - 38.0f, width, height);
+    for (int i = 0; i < n && CREW_LABEL + 1 + i < UI_LABELS; i++)
+        Ui_drawTextLeft(ui, CREW_LABEL + 1 + i, lines[i], x + 22.0f, top - 72.0f - row * (float)i, width, height);
+}
+
 /* Your pockets (B) on a sheet of paper on the right. */
 static void drawPockets(Ui *ui, Game *game, int width, int height) {
     char lines[POCKET_LINES][96];
@@ -459,6 +472,7 @@ int main(int argc, char **argv) {
         else if (needs_text[0]) Ui_drawTextLine(&ui, NEEDS_LABEL, needs_text, 1, UI_TOP_RIGHT, width, height);
         if (game.show_dev) drawDev(&ui, &game, width, height);
         if (game.show_crew) drawCrew(&ui, &game, width, height);
+        else if (game.show_notes) drawNotes(&ui, &game, width, height);
         if (game.show_pockets) drawPockets(&ui, &game, width, height);
         if (game.show_keys)
             for (int k = 0; k < KEY_COUNT; k++) Ui_drawTextLine(&ui, KEYS_LABEL + k, KEYS[k], k, UI_TOP_LEFT, width, height);
diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c
index c85f787..94234c2 100644
--- a/entropie/src/people/talk.c
+++ b/entropie/src/people/talk.c
@@ -5,6 +5,7 @@
 #include "world/site.h"
 #include "world/stage.h"
 #include "world/car.h"
+#include "world/why.h"
 
 #include <stdio.h>
 #include <string.h>
@@ -429,6 +430,16 @@ static void festivalAnswer(Talk *talk, Object *player, int choice) {
             memset(talk->shop, 0, sizeof talk->shop);
             go(talk, NODE_SHOP);
         }
+    } else if (g_talk_world && Why_open(g_talk_world) >= 0) { /* why do festivals ...? */
+        int k = Why_open(g_talk_world);
+        if (Why_knows(npc, k)) {
+            g_talk_world->why[k] = WHY_UNDERSTOOD;
+            npc->commitment = fminf(1.0f, npc->commitment + 0.03f); /* (asked: they like that) */
+            wrapUp(talk, Why_get(k)->answer);
+        } else {
+            snprintf(reply, sizeof reply, "No idea. Ask %s - or %s.", Why_get(k)->knowers[0], Why_get(k)->knowers[2]);
+            wrapUp(talk, reply);
+        }
     } else wrapUp(talk, "See you.");
 }
 
@@ -674,6 +685,7 @@ int Talk_choices(const Talk *talk, const char *out[TALK_CHOICES]) {
         else snprintf(take_line, sizeof take_line, "Help me with something");
         out[1] = lead_line;
         out[2] = take_line;
+        if (g_talk_world && Why_open(g_talk_world) >= 0) out[4] = Why_get(Why_open(g_talk_world))->ask; /* (a why question come up: ask it) */
     }
     if (talk->node == NODE_SHOP) {
         static char lines[TALK_CHOICES][64];
diff --git a/entropie/src/world/why.c b/entropie/src/world/why.c
new file mode 100644
index 0000000..517f398
--- /dev/null
+++ b/entropie/src/world/why.c
@@ -0,0 +1,59 @@
+#include "world/why.h"
+
+#include <string.h>
+
+static const WhyInfo WHY[WHY_COUNT] = {
+    [WHY_URINELLA] = {
+        .title = "Free Urinellas",
+        .ask = "Why do festivals hand out Urinellas?",
+        .seen = "A woman peed standing up with a Urinella. Why would a festival hand those out?",
+        .knowers = { "Jana", "Julia", "Stella", "Doro" },
+        .answer = "The queues! With one I'm as quick as the guys - no squatting in the dark.",
+        .because = "Women wait far longer at festival toilets, and at night they squat in the dark bushes. "
+                   "A Urinella lets them pee standing: quicker, safer, shorter queues for everybody.",
+        .option = "Yes - free Urinellas at every toilet (60 EUR)",
+        .raiser = "Stella",
+        .raise = "At the last festival there were free Urinellas everywhere. Anybody know why?",
+        .cost = 60,
+    },
+    [WHY_AWARENESS] = {
+        .title = "An awareness team",
+        .ask = "Why do festivals have an awareness team?",
+        .seen = "Somebody was down, out cold - and nobody noticed. Who looks out for people at a festival?",
+        .knowers = { "Stella", "Doro", "Julia", "Volkram" },
+        .answer = "Sober people on shift who look out for anyone not okay - drink, creeps, panic.",
+        .because = "Too much to drink, somebody getting too close, a bad trip, panic in a crowd: an awareness team "
+                   "is sober people on shift, easy to find, who look out for anyone who isn't okay and help.",
+        .option = "Yes - an awareness team, the crew takes turns on shift",
+        .raiser = "Julia",
+        .raise = "Fusion has an awareness team. Do we need one too? Why do they have it?",
+        .cost = 0,
+    },
+    [WHY_SHIRT] = {
+        .title = "No shirt, no service",
+        .ask = "Why do some festivals say \"no shirt, no service\"?",
+        .seen = "Shirtless at work again. Some festival bars say \"no shirt, no service\" - why?",
+        .knowers = { "Jana", "Julia", "Kilian", "Stella" },
+        .answer = "Guys go shirtless anywhere, women can't without stares. It keeps it fair.",
+        .because = "Men walk about shirtless everywhere while women can't without being stared at - it makes "
+                   "the place feel like theirs. No shirt, no service at the bars keeps it the same for everybody.",
+        .option = "Yes - no shirt, no service at our bars",
+        .raiser = "Kilian",
+        .raise = "Somebody told me: no shirt, no service at the bar. What's that about?",
+        .cost = 0,
+    },
+};
+
+const WhyInfo *Why_get(int k) { return &WHY[k < 0 ? 0 : k >= WHY_COUNT ? WHY_COUNT - 1 : k]; }
+
+bool Why_knows(const Object *who, int k) {
+    if (!who || !who->is_character) return false;
+    for (int i = 0; i < 4; i++)
+        if (WHY[k].knowers[i] && !strcmp(who->name, WHY[k].knowers[i])) return true;
+    return false;
+}
+
+int Why_open(const World *world) {
+    for (int k = 0; k < WHY_COUNT; k++) if (world->why[k] == WHY_RAISED) return k;
+    return -1;
+}
diff --git a/entropie/src/world/why.h b/entropie/src/world/why.h
new file mode 100644
index 0000000..deda913
--- /dev/null
+++ b/entropie/src/world/why.h
@@ -0,0 +1,34 @@
+#ifndef WORLD_WHY_H
+#define WORLD_WHY_H
+
+/*
+ * The why questions (see the story): things festivals do today - free
+ * Urinellas, an awareness team, "no shirt, no service" - that you find out
+ * the reason for on the way. Each one comes up when you see something (or is
+ * asked about at the Herberge), is understood by asking someone who knows
+ * (talk.c), then decided at the Herberge (game_meeting.c): yes or not this
+ * year. How far each is: World.why (WHY_UNKNOWN .. WHY_NO, world.h).
+ */
+
+#include "world/world.h"
+
+typedef struct {
+    const char *title;          /* in the notebook: "Free Urinellas" */
+    const char *ask;            /* you ask it: "Why do festivals hand out Urinellas?" */
+    const char *seen;           /* what made you wonder (shown when it comes up) */
+    const char *knowers[4];     /* who can tell you (cast names) */
+    const char *answer;         /* what they say (a speech bubble) */
+    const char *because;        /* the whole reason (in the notebook, once understood) */
+    const char *option;         /* the decision at the Herberge: doing it */
+    const char *raiser;         /* at the Herberge, if it hasn't come up yet: who asks ... */
+    const char *raise;          /* ... and what */
+    int         cost;           /* EUR from the cash box if you do it */
+} WhyInfo;
+
+const WhyInfo *Why_get(int k);
+/* Can this person tell you why? */
+bool Why_knows(const Object *who, int k);
+/* The first question that has come up and isn't understood yet, or -1. */
+int  Why_open(const World *world);
+
+#endif /* WORLD_WHY_H */
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index b4c7493..be69dc4 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -43,6 +43,9 @@ typedef struct { NoiseKind kind; Vec3 at; float loud; int surface; } Noise;
 #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 };
+/* The why questions (game_why.c): what festivals do today, found out on the way. */
+enum { WHY_URINELLA, WHY_AWARENESS, WHY_SHIRT, WHY_COUNT };
+enum { WHY_UNKNOWN, WHY_RAISED, WHY_UNDERSTOOD, WHY_YES, WHY_NO }; /* (how far each is: World.why) */
 
 typedef struct World {
     float    dt_props;   /* (this frame's step, for the props that move: gates) */
@@ -86,6 +89,8 @@ typedef struct World {
     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) */
+    int      why[WHY_COUNT]; /* the why questions: WHY_UNKNOWN .. WHY_NO (game_why.c; talk.c answers them) ... */
+    int      why_heard;    /* ... bit k: came up at the Herberge (not seen on the ground) */
 } World;
 
 /* A sound made here (dropped when the list is full: nobody's listening). */