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_festival.c raw
/*
* The festival (see the story: chapter 4) - the last weekend on the ground.
*
* Friday morning everybody's there for the last bits; at six the gates open
* and the guests come in from the chicken farm's yard (two waves: Friday
* evening, Saturday late morning), up to the camping beyond the strip of
* wood, tents up, then off to whatever floor is playing. A floor plays (its
* own music, world/../core/audio.c) only if its stage got far enough - its
* deck at least; the better built, the more fun. Each has its times.
*
* Saturday at two the storm warning: a heavy thunderstorm at three. The crew
* know; a floor with a proper stage announces it; everybody else has to be
* told - by you (X, then 1: shout it) and the crew (the awareness team, if
* there is one, goes round everywhere, the tents too). Inside is safe: the
* main building, the barn, your garages' kitchen. A tent or a pavilion is
* not. At a quarter past three it's at its worst: who's still out in it is
* counted (and some get knocked over). At half past four it's over, and the
* music starts again an hour later.
*
* Sunday from ten the guests pack up and go. At four everybody at the fire:
* how it went (the guests' fun, the storm, the awareness team) - the result.
*/
#include <math.h>
#include "game/game_internal.h"
#include "world/cast.h"
#include "world/guests.h"
#include "world/stage.h"
#include "world/weather.h"
#include "world/why.h"
/* Hours from the weekend's Friday midnight */
/* (everybody is on the ground from Friday at ten: game_meeting.c FESTIVAL_ARRIVE) */
#define H_GATES 18.0f /* the gates open */
#define H_WAVE1_END 23.0f /* the first guests in by then ... */
#define FIRST_WAVE 24
#define H_WAVE2 34.0f /* ... the rest Saturday late morning */
#define H_WAVE2_END 37.0f
#define H_WARN 38.0f /* Saturday two o'clock: the storm warning */
#define H_CLOUDS 38.66f /* ... the first rain */
#define H_STORM 39.0f /* ... three: the storm */
#define H_WORST 39.25f /* ... at its worst: who's out in it is counted */
#define H_CLEAR 40.5f /* ... over */
#define H_LEAVE 58.0f /* Sunday ten: the guests go */
#define H_REVIEW 64.0f /* Sunday four: at the fire, how it went */
#define REVIEW_GAP 4.5f /* s between the lines */
#define ANNOUNCE 45.0f /* m: a stage with a sound system tells those this near about the storm */
#define DANCE_OUT 6.0f /* m in front of a stage: the dance floor's middle */
static const float FLOOR_BPM[FEST_FLOORS] = { 118.0f, 144.0f, 100.0f, 130.0f }; /* (as the music: core/audio.c FLOOR_BPM) */
static const int FLOOR_PROJECT[FEST_FLOORS] = { PROJECT_HOFBUEHNE, PROJECT_OBSTWIESE, PROJECT_KARTOFFELACKER, PROJECT_HIDDEN };
static const char *const FLOOR_NAME[FEST_FLOORS] = { "the Hofbuehne", "the Obstwiese", "the Kartoffelacker", "the hidden floor" };
/* When each floor plays (weekend hours): the bands in the evenings, psytrance through the nights, the
Kartoffelacker by day, techno late at night */
static const float PLAYS[FEST_FLOORS][3][2] = {
{ { 19.0f, 25.0f }, { 36.5f, 38.0f }, { 41.5f, 49.0f } },
{ { 22.0f, 32.0f }, { 45.0f, 58.0f }, { 0.0f, 0.0f } },
{ { 34.0f, 38.0f }, { 41.5f, 44.5f }, { 0.0f, 0.0f } },
{ { 24.5f, 31.0f }, { 49.0f, 56.0f }, { 0.0f, 0.0f } },
};
static float hours(const Game *game) { return game->story_hours - game->weekend_from; }
bool game_festivalWeekend(const Game *game) { return game->weekend >= FESTIVAL_WEEKEND; }
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 bool onGround(const Object *o) {
return o->type == OBJ_HUMAN && !o->gone_home && !Game_inBerlin(o);
}
/* How good a floor is: what got built (the deck at least, else it doesn't play). */
static float goodness(const Object *stage) {
if (!stage) return 0.0f;
switch (stage->build_stage) {
case BUILD_FLOOR: return 0.45f;
case BUILD_ROOF: return 0.65f;
case BUILD_DECOR: return 0.85f;
case BUILD_DONE: return 1.0f;
default: return 0.0f;
}
}
static bool playsAt(int k, float h) {
for (int s = 0; s < 3; s++) if (h >= PLAYS[k][s][0] && h < PLAYS[k][s][1]) return true;
return false;
}
/* What the people need to know (the world's Fest), every frame. */
static void tellWorld(Game *game, float dt) {
World *w = &game->world;
Fest *f = &w->fest;
Festival *fe = &game->festival;
float h = hours(game);
f->stage = fe->stage;
f->on = 0;
f->clock += dt;
if (f->clock > 3600.0f) f->clock -= 3600.0f;
for (int k = 0; k < FEST_FLOORS; k++) {
const Object *s = Stage_of(w->objects, w->count, (Project)FLOOR_PROJECT[k]);
f->good[k] = goodness(s);
f->bpm[k] = FLOOR_BPM[k];
if (s) {
f->front[k] = yaw_front(s->rotation.y); /* (a stage's front: its local +Z) */
f->dance[k] = vec3_add(s->position, vec3_scale(f->front[k], DANCE_OUT));
f->dance[k].y = World_groundHeight(w, f->dance[k].x, f->dance[k].z, s->position.y + 3.0f);
}
if (fe->stage == FEST_OPEN && f->good[k] > 0.0f && playsAt(k, h)) f->on |= 1u << k;
}
fe->played |= f->on;
f->shelter[0] = Site_houseDoor();
f->shelter[1] = Site_barnDoor();
f->shelter[2] = Site_rainRoom();
f->awareness = w->why[WHY_AWARENESS] == WHY_YES;
f->shirts = w->why[WHY_SHIRT] == WHY_YES;
if (f->helped > fe->helped) fe->helped = f->helped; /* (counted by the people) */
f->helped = fe->helped;
}
/* ---------- The guests ---------- */
static float dueAt(int k) {
if (k <= FIRST_WAVE) return H_GATES + (H_WAVE1_END - H_GATES) * (float)((k - 1) / 2 * 2) / (float)FIRST_WAVE;
return H_WAVE2 + (H_WAVE2_END - H_WAVE2) * (float)((k - FIRST_WAVE - 1) / 2 * 2) / (float)(GUESTS - FIRST_WAVE);
}
static void comeIn(Game *game, int k) {
World *w = &game->world;
Vec3 gate = Guest_gate();
gate.x += (k % 2 ? 0.7f : -0.7f);
gate.y = World_groundHeight(w, gate.x, gate.z, 50.0f);
Object *o = Guest_create(k, gate);
if (!o) return;
/* what the Herberge said yes to */
if (w->why[WHY_URINELLA] == WHY_YES && o->female) { o->urinella = true; o->urinellas = 3; o->urinella_tried = 1; o->fun += 0.04f; }
if (w->why[WHY_SHIRT] == WHY_YES) { if (o->female) o->fun += 0.04f; else o->look.keeps_shirt = true; }
if (w->why[WHY_AWARENESS] == WHY_YES) o->fun += 0.02f;
Vec3 to = vec3_sub(Guest_campSpot(k), gate);
o->rotation.y = atan2f(to.z, to.x) + PI_F / 2.0f; /* (a person's yaw: Object_facing) */
Object_syncColliders(o);
if (!World_add(w, o)) return;
game->festival.came = k;
}
static void guestsIn(Game *game, float h) {
Festival *fe = &game->festival;
while (fe->came < GUESTS && h >= dueAt(fe->came + 1)) {
comeIn(game, fe->came + 1);
if (fe->came == 2) say(game, "The first guests are coming in - from the chicken farm's yard, up to the camping beyond the wood");
if (fe->came == FIRST_WAVE + 2) say(game, "More guests coming in for Saturday");
}
}
/* ---------- The storm ---------- */
static int tellStorm(Game *game, Vec3 from, float within, bool heard_only) {
int n = 0;
for (int i = 0; i < game->world.count; i++) {
Object *o = game->world.objects[i];
if (!onGround(o) || o->storm_told || o->indoors > 0.0f) continue;
if (heard_only && (o->body.asleep || o->in_tent)) continue;
if (hypotf(o->position.x - from.x, o->position.z - from.z) > within) continue;
o->storm_told = true;
o->ai.talking = false;
n++;
}
return n;
}
int game_shoutStorm(Game *game) {
say(game, "\"Storm! Everybody inside - the barn, the house, the garages!\"");
return tellStorm(game, game->player->position, 35.0f, true);
}
static void warn(Game *game, float h) {
World *w = &game->world;
for (int i = 0; i < w->count; i++) { /* the crew know (Volkram's phone) */
Object *o = w->objects[i];
if (onGround(o) && o->cast_id > 0) o->storm_told = true;
}
for (int k = 0; k < FEST_FLOORS; k++) { /* a stage with a sound system: announced from it */
if (!playsAt(k, h - 0.02f) || w->fest.good[k] < 0.65f) continue;
tellStorm(game, w->fest.dance[k], ANNOUNCE, true);
Object *lead = Cast_leadOf(w->objects, w->count, (Project)FLOOR_PROJECT[k]);
if (lead && lead != game->player && onGround(lead)) Ai_say(lead, "Storm warning! Everybody off the floor and inside - the barn, the house!");
}
Object *v = named(game, "Volkram");
if (v) Ai_say(v, "Storm warning - a big one at three! Everybody inside, tell the guests!");
say(game, "Storm warning: a heavy thunderstorm at three. Get everybody inside - the main building, the barn, "
"your garages (X, then 1: shout it). Not the tents, not the pavilions!");
game->message_time = 9.0f;
}
static void worst(Game *game) {
Festival *fe = &game->festival;
World *w = &game->world;
fe->caught = fe->inside = 0;
int knocked = 0;
for (int i = 0; i < w->count; i++) {
Object *o = w->objects[i];
if (!onGround(o)) continue;
if (o->indoors > 0.0f) { fe->inside++; continue; }
fe->caught++;
if (o->guest) o->fun = fmaxf(0.0f, o->fun - 0.3f);
else o->commitment = fmaxf(0.0f, o->commitment - 0.1f);
if (!o->in_tent && !o->body.asleep && !Body_isDown(&o->body) && ((i * 7 + fe->caught) % 4) == 0) { /* (a gust, flying things: down) */
Body_fall(&o->body, vec3_normalize(vec3(w->wind.x + 0.01f, 0.0f, w->wind.z)));
knocked++;
}
}
if (game->player->indoors <= 0.0f && !game->player->in_tent) fe->caught++;
static char line[160];
if (fe->caught == 0) snprintf(line, sizeof line, "The storm's at its worst - and everybody's inside. Well done!");
else snprintf(line, sizeof line, "The storm's at its worst: %d still out in it%s", fe->caught,
knocked ? " - people knocked over by the gusts!" : "");
say(game, line);
game->message_time = 7.0f;
fe->counted = true;
}
static void allClear(Game *game) {
World *w = &game->world;
for (int i = 0; i < w->count; i++) {
Object *o = w->objects[i];
o->storm_told = false;
if (o->type == OBJ_HUMAN && !o->gone_home && o->indoors > 3.0f && vec3_length(o->inside_door) > 0.1f)
o->indoors = 2.0f + 0.4f * (float)(i % 20); /* (out again, a few at a time) */
}
say(game, "The storm's passed. Out you come - the music's back at half past five");
}
/* ---------- Sunday: how it went ---------- */
static float guestFun(const Game *game, int *n) {
float sum = 0.0f;
*n = 0;
for (int i = 0; i < game->world.count; i++) {
const Object *o = game->world.objects[i];
if (o->type == OBJ_HUMAN && o->guest) { sum += o->fun; (*n)++; }
}
return *n ? sum / (float)*n : 0.0f;
}
static int score(const Game *game) {
int n;
float fun = guestFun(game, &n);
int played = 0;
for (int k = 0; k < FEST_FLOORS; k++) if (game->festival.played & (1u << k)) played++;
float s = 0.6f * fun + 0.1f * (float)played / (float)FEST_FLOORS + 0.15f * (n ? (float)n / (float)GUESTS : 0.0f);
s += game->festival.caught == 0 ? 0.15f : 0.15f * fmaxf(0.0f, 1.0f - (float)game->festival.caught / 10.0f);
return (int)lroundf(clampf(s, 0.0f, 1.0f) * 100.0f);
}
static void line(Game *game, const char *who, const char *text) {
Object *o = named(game, who);
if (o && onGround(o)) Ai_say(o, text);
snprintf(game->story_line, sizeof game->story_line, "%s: %s", who, text);
say(game, game->story_line);
game->message_time = REVIEW_GAP;
}
static bool review(Game *game, int step) {
Festival *fe = &game->festival;
World *w = &game->world;
char text[160];
switch (step) {
case 0:
snprintf(text, sizeof text, "That was %s! %d guests came - and we built it all ourselves.", Game_festivalName(game), fe->came);
line(game, "Volkram", text);
return true;
case 1: {
int best = -1, missing = -1;
for (int k = 0; k < FEST_FLOORS; k++) {
if (!(fe->played & (1u << k))) { if (missing < 0) missing = k; continue; }
if (best < 0 || w->fest.good[k] > w->fest.good[best]) best = k;
}
if (best < 0) snprintf(text, sizeof text, "Not one floor was ready to play. People brought their own music...");
else if (missing >= 0) snprintf(text, sizeof text, "The %s rocked. The %s never played - not built.", FLOOR_NAME[best] + 4, FLOOR_NAME[missing] + 4);
else snprintf(text, sizeof text, "All four floors played - %s was the best.", FLOOR_NAME[best]);
line(game, "Jana", text);
return true;
}
case 2:
if (!fe->counted) return false;
if (fe->caught == 0) snprintf(text, sizeof text, "The storm: everybody got inside in time. That's what I call organised.");
else snprintf(text, sizeof text, "The storm, though: %d were out in it. That could have gone badly.", fe->caught);
line(game, "Volkram", text);
return true;
case 3:
if (w->fest.awareness) snprintf(text, sizeof text, "The awareness team looked after %d who'd had too much.", fe->helped);
else snprintf(text, sizeof text, "People lay drunk in the grass and nobody looked. Next time: an awareness team.");
line(game, w->fest.awareness ? "Stella" : "Julia", text);
return true;
case 4:
if (fe->harassed == 0) { snprintf(text, sizeof text, "And nobody got bothered or groped all weekend. That's the festival I want."); line(game, "Jana", text); return true; }
snprintf(text, sizeof text, "%d times somebody got groped or bothered. %d times somebody stepped in.", fe->harassed, fe->stepped);
line(game, "Jana", text);
return true;
case 5: {
int n;
float fun = guestFun(game, &n);
fe->result = score(game);
snprintf(text, sizeof text, "The guests? %s (%d%%)", fun > 0.8f ? "They loved it - they want to come back!"
: fun > 0.6f ? "They had a good time." : fun > 0.4f ? "It was okay for them. Just okay." : "Honestly, most were disappointed.", fe->result);
line(game, "Toni", text);
return true;
}
case 6: line(game, "Volkram", "Tomorrow at ten we clean up - all of it. But tonight - to us!"); return true;
default: return false;
}
}
/* ---------- Every frame ---------- */
void game_festival(Game *game, float dt) {
Festival *fe = &game->festival;
World *w = &game->world;
if (!game_festivalWeekend(game) || game->meeting.kind) { if (fe->stage == FEST_NONE) { w->fest.stage = FEST_NONE; w->fest.on = 0; } return; }
float h = hours(game);
int was = fe->stage;
if (fe->stage == FEST_NONE) fe->stage = FEST_SETUP;
if (fe->stage == FEST_SETUP && h >= H_GATES) { fe->stage = FEST_OPEN; say(game, "Six o'clock: the gates are open! The festival's on"); game->message_time = 6.0f; }
if (fe->stage == FEST_OPEN && h >= H_WARN && h < H_CLEAR && !fe->counted) { fe->stage = FEST_WARNED; }
if (fe->stage == FEST_WARNED && h >= H_STORM) fe->stage = FEST_STORM;
if (fe->stage == FEST_STORM && h >= H_WORST && !fe->counted) worst(game);
if (fe->stage == FEST_STORM && h >= H_CLEAR) { fe->stage = FEST_OPEN; allClear(game); }
if (fe->stage == FEST_OPEN && h >= H_LEAVE) { fe->stage = FEST_LEAVING; say(game, "Sunday: the guests start packing up and going home"); }
if (fe->stage == FEST_LEAVING && h >= H_REVIEW) { fe->stage = FEST_OVER; fe->review = 0; fe->review_t = 1.0f; }
tellWorld(game, dt);
if (was < FEST_WARNED && (fe->stage == FEST_WARNED || fe->stage == FEST_STORM)) warn(game, h);
/* the weather: fine, but for the storm */
if (fe->stage == FEST_WARNED) { w->weather = h >= H_CLOUDS ? WEATHER_SHOWER : WEATHER_DRY; w->wind_kind = WIND_BREEZE; }
else if (fe->stage == FEST_STORM) { w->weather = WEATHER_RAIN; w->wind_kind = WIND_STORM; }
else if (fe->stage == FEST_OVER) { w->weather = WEATHER_AUTO; w->wind_kind = WIND_AUTO; }
else { w->weather = WEATHER_DRY; w->wind_kind = h >= H_CLEAR && h < H_CLEAR + 1.0f ? WIND_BREEZE : WIND_AUTO; }
if (fe->stage >= FEST_OPEN && fe->stage <= FEST_LEAVING) guestsIn(game, h);
if (fe->stage == FEST_OVER && fe->review >= 0 && (fe->review_t -= dt) <= 0.0f) {
if (review(game, fe->review)) { fe->review_t = REVIEW_GAP; fe->review++; }
else if (fe->review < 7) fe->review++;
else {
fe->review = -1;
snprintf(game->story_line, sizeof game->story_line, "%s is over - %d%%. Monday at ten: the clean-up, then home",
Game_festivalName(game), fe->result);
say(game, game->story_line);
game->message_time = 10.0f;
}
}
}
/* Friday morning: everybody's there (the meetings' nextWeekend did the rest). */
void game_festivalStart(Game *game) {
memset(&game->festival, 0, sizeof game->festival);
game->festival.result = -1;
game->festival.verdict = -1;
game->festival.stage = FEST_SETUP;
game->world.fest.helped = 0;
}
/* Dev: the next thing in it. */
static const float JUMPS[] = { H_GATES - 0.05f, H_WARN - 0.05f, H_STORM + 0.2f, H_CLEAR - 0.05f, H_LEAVE - 0.05f, H_REVIEW - 0.05f, 81.95f };
static const char *const JUMP_NAME[] = { "the gates open", "the storm warning", "the storm at its worst", "the storm over",
"Sunday: leaving", "the review", "Monday: the clean-up" };
const char *game_festivalJumpText(const Game *game) {
if (!game_festivalWeekend(game)) return "go to it ->";
float h = hours(game);
static char text[64];
for (int k = 0; k < (int)(sizeof JUMPS / sizeof JUMPS[0]); k++)
if (h < JUMPS[k]) { snprintf(text, sizeof text, "on to %s ->", JUMP_NAME[k]); return text; }
return "over";
}
void game_festivalJump(Game *game) {
if (!game_festivalWeekend(game)) { /* (the Herberge before it: straight on to the festival weekend) */
if (game->meeting.kind || game->weekend <= 0) return;
game->weekend = FESTIVAL_WEEKEND - 1;
game->story_hours = game->weekend_from + 66.0f; /* (Sunday six: back to Berlin, the last Herberge) */
return;
}
float h = hours(game);
for (int k = 0; k < (int)(sizeof JUMPS / sizeof JUMPS[0]); k++) {
if (h >= JUMPS[k]) continue;
float add = JUMPS[k] - h;
game->story_hours += add;
game->daycycle.seconds = fmodf(game->daycycle.seconds + add * 3600.0f, 24.0f * 3600.0f);
return;
}
}
const char *game_festivalTask(const Game *game) {
const Festival *fe = &game->festival;
if (!game_festivalWeekend(game)) return NULL;
const char *clean = game_cleanupTask(game);
if (clean) return clean;
switch (fe->stage) {
case FEST_SETUP: return "The festival weekend: the last bits before the gates open at six";
case FEST_WARNED: return "The storm's coming at three: get everybody inside (X, then 1: shout it)";
case FEST_STORM: return "The storm: stay inside till it's over";
default: return NULL;
}
}
int game_festivalNotes(const Game *game, char lines[][128], int max) {
const Festival *fe = &game->festival;
const World *w = &game->world;
int n = 0;
if (!game_festivalWeekend(game) || n >= max) return 0;
snprintf(lines[n++], 128, " ");
if (n < max) snprintf(lines[n++], 128, "The festival:");
if (n < max) {
int here = 0;
for (int i = 0; i < w->count; i++) if (w->objects[i]->type == OBJ_HUMAN && w->objects[i]->guest && !w->objects[i]->gone_home) here++;
snprintf(lines[n++], 128, "- Guests: %d came, %d here now", fe->came, here);
}
char play[128] = "- Playing now:";
size_t len = strlen(play);
bool any = false;
for (int k = 0; k < FEST_FLOORS; k++)
if (w->fest.on & (1u << k)) { snprintf(play + len, sizeof play - len, "%s %s", any ? "," : "", FLOOR_NAME[k]); len = strlen(play); any = true; }
if (!any) snprintf(play + len, sizeof play - len, " nothing");
if (n < max) snprintf(lines[n++], 128, "%s", play);
for (int k = 0; k < FEST_FLOORS && n < max; k++)
if (w->fest.good[k] <= 0.0f) snprintf(lines[n++], 128, "- %c%s doesn't play: its stage hasn't got its deck", FLOOR_NAME[k][0] - 32, FLOOR_NAME[k] + 1);
if (fe->counted && n < max) snprintf(lines[n++], 128, "- The storm: %d inside, %d out in it", fe->inside, fe->caught);
if (w->fest.awareness && n < max) snprintf(lines[n++], 128, "- The awareness team helped %d people", fe->helped);
if (fe->stage == FEST_OVER && (fe->review < 0 || fe->review > 5) && n < max) snprintf(lines[n++], 128, "- How it went: %d%%", fe->result);
n += game_cleanupNotes(game, lines + n, max - n);
return n;
}