Nimbin[12]?Games / Entropie / entropie/src/game/game_festival.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_festival.c 20.6 KB · 436 lines 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;
}