Entropie git · main
Entropie festival game; first steps
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.gzentropie/src/game/game_meeting.c 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) */
}