Nimbin[12]?Games / Entropie / entropie/src/game/game_meeting.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 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/src/game/game_meeting.c 35.4 KB · 768 lines raw
/*
 * 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"
#include "world/why.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 FESTIVAL_ARRIVE 10.0f /* the festival weekend: Friday at ten (game_festival.c H_ARRIVE) */
#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, AFTER_END };
enum { Q_NONE, Q_JOIN, Q_NAME, Q_FLOOR, Q_CASH, Q_FOCUS, Q_TOP_UP, Q_WHY, Q_AGAIN };

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 || kind == MEET_END ? 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 || kind == MEET_END) {
        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 || m->kind == MEET_END) {
        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 || c->far_away) continue; /* (the hosts, and Enrico: Darmstadt's far) */
            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_END) {
        game->daycycle.seconds = 22.0f * 3600.0f;
        snprintf(game->story_line, sizeof game->story_line, "Grossbeerenstrasse 37 - Monday night, home again");
    } else 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;
    m->asked_mask = 0;
}

/* ---------- 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_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_AGAIN:
        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->asking == Q_WHY) && m->kind == MEET_HERBERGE && m->number > 1; /* (the next one, 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);
}

/* 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];
    bool last = game->weekend == FESTIVAL_WEEKEND - 1; /* (the festival next: no more building after) */
    if (step == 0) {
        snprintf(text, sizeof text, last ? "Weekend %d done - and next weekend's the festival! Where are we?"
                                    : game->weekend == FESTIVAL_WEEKEND - 2 ? "Weekend %d done. One more, then it's the festival. Where are we?"
                                    : "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: { /* 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:
        if (last) { line(game, "Volkram", "What's built by Friday is what we've got. Friday morning, all of us at Kurti's!"); return true; }
        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 + 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 + 8: { /* the big old tree, once it's started */
        const BigTree *bt = &game->world.bigtree;
        if (bt->step == BT_STANDING || game->festival.stage) { game->meeting.wait = 0.0f; return true; }
        if (bt->step < BT_FALLING) snprintf(text, sizeof text, "Enrico says the big tree's next weekend. %s", bt->rope_h > 0.0f ? "The rope's up already." : "Somebody has to get the rope up.");
        else if (bt->lines_down) snprintf(text, sizeof text, "The big tree's down - and the phone line with it. Kurti's still cross.");
        else snprintf(text, sizeof text, "The big tree's down, the phone line's fine. Good work on that rope!");
        line(game, "Volkram", text);
        return true;
    }
    case N_FLOORS + 9: line(game, "Volkram", last ? "See you Friday morning. Our festival!" : "Friday at five at Kurti's. See you there!"); return true;
    default: return false;
    }
}

/* Back home after the drive: how it all went, and next year? */
static bool ending(Game *game, int step) {
    const Festival *fe = &game->festival;
    const World *w = &game->world;
    char text[160];
    switch (step) {
    case 0: line(game, "Volkram", "Home. I still can't believe we actually did it."); return true;
    case 1:
        snprintf(text, sizeof text, "%s: %d guests, %d%%. %s", Game_festivalName(game), fe->came, fe->result < 0 ? 0 : fe->result,
                 fe->result >= 70 ? "Not bad for a bunch of amateurs." : fe->result >= 45 ? "Not perfect - but ours." : "Well... we learned a lot.");
        line(game, "Jana", text);
        return true;
    case 2:
        line(game, "Toni", fe->verdict == 0 ? "And Kurti says we're welcome back. Cleanest field he's ever had."
                         : fe->verdict == 1 ? "Kurti's boys are picking up the last bits. He wasn't cross."
                         : fe->verdict == 2 ? "Kurti made us pay for the waste removal. Fair enough, honestly." : "We never even cleaned up...");
        return true;
    case 3:
        if (w->bigtree.step == BT_DOWN)
            line(game, "Volkram", w->bigtree.lines_down ? "And the phone line... Enrico still owes the Telekom an apology."
                                                        : "And Enrico's tree came down right where it should. I'll never forget that sound.");
        else line(game, "Volkram", "And the big old tree's still standing. Next year, Enrico.");
        return true;
    case 4:
        if (fe->harassed > fe->stepped) line(game, "Jana", "Next time we look out for each other better. Nobody gets groped at our festival.");
        else if (fe->supported2 > 0) line(game, "Toni", "And the supporters - without them we'd still be picking up cups.");
        else line(game, "Toni", "I'm going to sleep for a week.");
        return true;
    case 5:
        line(game, "Jana", "So... next year again?");
        ask(game, Q_AGAIN);
        option(game, 0, "Yes! Next year, same place.");
        option(game, 1, "Let me sleep for a week first.");
        option(game, 2, "Never again. ...Well. Maybe.");
        return true;
    case 6: line(game, "Volkram", "Ha. We'll talk about it at the Herberge. To us!"); return true;
    case 7:
        snprintf(game->story_line, sizeof game->story_line, "THE END - %s, %d%%. Thanks for playing!", Game_festivalName(game), fe->result < 0 ? 0 : fe->result);
        say(game, game->story_line);
        game->message_time = 30.0f;
        game->meeting.wait = 2.0f;
        return true;
    default: return false;
    }
}

static bool agenda(Game *game) {
    Meeting *m = &game->meeting;
    if (m->kind == MEET_END) return ending(game, m->step);
    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++;
    bool festival = game_festivalWeekend(game); /* (the festival: everybody there Friday morning) */
    float arrive = festival ? FESTIVAL_ARRIVE : NEXT_ARRIVE;
    game->weekend_from = game->story_hours - arrive;
    game->weekend_warned = false;
    game->daycycle.seconds = arrive * 3600.0f;
    if (festival) game_festivalStart(game);
    if (festival) snprintf(game->story_line, sizeof game->story_line, "The festival weekend - Friday morning at Kurti's. The gates open at six");
    else 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;
    case AFTER_END:           m->kind = MEET_END; gather(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;
    if (game_festivalWeekend(game)) return; /* (the festival: no Herberge after it - the clean-up, then home) */
    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);
}

void game_meetingHome(Game *game) { fadeTo(game, AFTER_END); }

/* The agenda's done: off to the ground (or on to the Herberge from Grossbeerenstrasse; the end: that's it). */
static void finish(Game *game) {
    Meeting *m = &game->meeting;
    m->skipping = false;
    if (m->kind == MEET_END) { m->ended = true; m->speaker = NULL; return; }
    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 || m->ended) 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->ended) return "The end. Thanks for playing! (Esc: the menu)";
    if (m->asking) return "Your turn: answer with 1 - 5 (Backspace: skip the meeting)";
    if (m->kind == MEET_END) return "Home again: listen (Backspace: skip it)";
    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) */
}