/* * 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 #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) */ }