Nimbin[12]?Games / Entropie / entropie/src/game/game_cleanup.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_cleanup.c 15.6 KB · 345 lines raw
/*
 * The clean-up and the drive home (see the story: chapters 5 and 6).
 *
 * The guests drop litter all weekend (ai_fest.c: cups, cans, paper - less
 * where people look out for the place), and the ones who didn't care leave
 * some at their tent. One guest pair in four are supporters: a free ticket
 * for two shifts of work. The first is Sunday from ten to four, while the
 * others pack up: the camping and the floors. Who had no fun doesn't turn up.
 *
 * Monday at ten the clean-up: the crew still happy to help (commitment;
 * Volkram, Jana and Toni always - it's their car home) and the supporters'
 * second shift, each with a bag to where the most litter is (the camping,
 * the floors that played, the yard). The others go home first thing. Clean
 * (a tenth left) or four o'clock: Kurti has a look round - clean, a bit
 * left (his sons do the rest), or a mess (the waste removal: 300 EUR from
 * the cash box). Then home: Volkram, Jana, Toni and you in Volkram's car
 * (walk up to it), out the way you came in, and back in Grossbeerenstrasse
 * the end (game_meeting.c, MEET_END).
 */

#include <math.h>

#include "game/game_internal.h"
#include "world/car.h"
#include "world/site.h"
#include "world/tent.h"

/* Hours from the weekend's Friday midnight */
#define H_SHIFT      58.0f /* Sunday ten: the supporters' first shift ... */
#define H_SHIFT_END  64.0f /* ... till four */
#define H_CLEAN      82.0f /* Monday ten: the clean-up ... */
#define H_CLEAN_END  88.0f /* ... four at the latest */
#define CLEAN_ENOUGH 0.1f  /* of the litter left: clean */
#define COUNT_EVERY  5.0f  /* real s between counts */
#define KEEN         0.35f /* commitment: still happy to help on Monday */
#define SHOW_UP      0.3f  /* fun: a supporter turns up for a shift */
#define MESS_FINE    300.0f /* EUR: the waste removal */
#define CAR_REACH    1.8f  /* m from your door: you get in */
#define CAR_WAIT     90.0f /* real s the others walk to the car (then they're in anyway) */
#define BACK_OUT     10.0f /* the drive: backed out this far ... */
#define BACK_SPEED   1.6f
#define TURN_TIME    3.0f  /* ... turned round ... */
#define DRIVE_TOP    6.0f  /* ... and off */
#define DRIVE_ACCEL  1.6f

static const char *const CAR_CREW[] = { "Volkram", "Jana", "Toni" };
static const int CAR_SEATS[] = { 0, 1, 3 }; /* (you: 2, in the back) */

static float hours(const Game *game) { return game->story_hours - game->weekend_from; }

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 carCrew(const Object *o) {
    for (int k = 0; k < 3; k++) if (!strcmp(o->name, CAR_CREW[k])) return true;
    return false;
}

/* Here and able: not gone, not in Berlin, not in hospital. */
static bool here(const Object *o) {
    return o->type == OBJ_HUMAN && !o->gone_home && !o->heading_home && !Game_inBerlin(o) && o->hospital_until <= 0.0f && !o->injured && !o->riding;
}

static bool litter(const Object *o) {
    return o->type == OBJ_TRASH && o->mesh.vao && !o->is_held && o->owner && o->owner->guest > 0 && !Game_inBerlin(o);
}

static int countLitter(const Game *game) {
    int n = 0;
    for (int i = 0; i < game->world.count; i++) if (litter(game->world.objects[i])) n++;
    return n;
}

static bool onIt(const Object *o) { return o->ai.mode == AI_WORK && o->ai.order == ORDER_TRASH; }

#define SEND_NEAR    30.0f /* m: a helper working this near a piece already has it */
#define SEND_AGAIN   0.05f /* game h (a real minute) before somebody done (or stuck) is sent again */

/* Everybody on it who isn't yet: to the litter nobody's working near, the nearest first. */
static void sendOut(Game *game, bool (*who)(const Object *)) {
    World *w = &game->world;
    for (int i = 0; i < w->count; i++) {
        Object *o = w->objects[i];
        if (!here(o) || !who(o) || onIt(o) || Body_isDown(&o->body) || o->ai.talking) continue;
        if (o->clean_sent > 0.0f && game->story_hours - o->clean_sent < SEND_AGAIN) continue;
        Object *best = NULL;
        float best_d = 1e9f;
        for (int t = 0; t < w->count; t++) {
            Object *l = w->objects[t];
            if (!litter(l)) continue;
            bool taken = false;
            for (int j = 0; j < w->count && !taken; j++) {
                const Object *h = w->objects[j];
                taken = h != o && h->type == OBJ_HUMAN && onIt(h) && hypotf(h->ai.work_area.x - l->position.x, h->ai.work_area.z - l->position.z) < SEND_NEAR;
            }
            float d = hypotf(l->position.x - o->position.x, l->position.z - o->position.z) + (taken ? 200.0f : 0.0f);
            if (d < best_d) { best_d = d; best = l; }
        }
        if (!best) return; /* (nothing left anywhere) */
        if (o->in_tent) { o->rain_nap = false; Tent_crawlOut(o); }
        if (o->body.asleep) Body_getUp(&o->body);
        o->needs.sleepiness = fminf(o->needs.sleepiness, 0.5f);
        o->drunk = fminf(o->drunk, 0.3f);
        o->sitting = false;
        Ai_order(o, ORDER_TRASH, best->position);
        o->clean_sent = game->story_hours;
    }
}

static bool supporter(const Object *o) { return o->supporter; }
static bool helper(const Object *o) { return o->supporter || (o->cast_id > 0 && !o->lives_here); }

static void goHome(Object *o, const char *line) {
    if (onIt(o)) Ai_stopWork(o);
    o->ai.mode = AI_IDLE;
    o->heading_home = true;
    if (line) Ai_say(o, line);
}

static void stopAll(Game *game, bool (*who)(const Object *)) {
    for (int i = 0; i < game->world.count; i++) {
        Object *o = game->world.objects[i];
        if (o->type == OBJ_HUMAN && who(o) && onIt(o)) Ai_stopWork(o);
    }
}

/* ---------- The stages ---------- */

static void shiftStart(Game *game) {
    int came = 0, no = 0;
    for (int i = 0; i < game->world.count; i++) {
        Object *o = game->world.objects[i];
        if (!o->supporter || !here(o)) continue;
        if (o->fun < SHOW_UP) { goHome(o, "My shift? After that weekend? No thanks - I'm off."); no++; continue; }
        came++;
        Ai_say(o, (came % 2) ? "My shift. Right - a bag, and the camping." : "Free ticket, fair deal. Where are the bags?");
    }
    game->festival.supported = came;
    sendOut(game, supporter);
    snprintf(game->story_line, sizeof game->story_line, no ? "Sunday ten: %d supporters start their shift (picking up) - %d didn't turn up"
                                                           : "Sunday ten: %d supporters start their first shift - picking up the camping and the floors", came, no);
    say(game, game->story_line);
    game->message_time = 6.0f;
}

static void cleanStart(Game *game) {
    Festival *fe = &game->festival;
    fe->litter_start = fe->litter_left = countLitter(game);
    fe->helpers = fe->left_early = fe->supported2 = 0;
    for (int i = 0; i < game->world.count; i++) {
        Object *o = game->world.objects[i];
        if (!here(o)) continue;
        if (o->supporter) {
            if (o->fun < SHOW_UP) { goHome(o, "Sorry - I'm going home. Find somebody else."); continue; }
            fe->supported2++;
        } else if (o->cast_id > 0 && !o->lives_here) {
            if (!carCrew(o) && o->commitment < KEEN) { goHome(o, "Sorry, I've got to get back to Berlin - work tomorrow."); fe->left_early++; continue; }
            fe->helpers++;
        }
    }
    Object *v = named(game, "Volkram");
    if (v && here(v)) Ai_say(v, "Right, everybody - bags from the box by the container. We leave it like we found it!");
    sendOut(game, helper);
    snprintf(game->story_line, sizeof game->story_line, "Monday ten: the clean-up - %d of the crew and %d supporters, %d pieces of litter to pick up",
             fe->helpers, fe->supported2, fe->litter_start);
    say(game, game->story_line);
    game->message_time = 7.0f;
}

/* Kurti has a look round. */
static void look(Game *game) {
    Festival *fe = &game->festival;
    World *w = &game->world;
    fe->litter_left = countLitter(game);
    float left = fe->litter_start > 0 ? (float)fe->litter_left / (float)fe->litter_start : 0.0f;
    fe->verdict = fe->litter_left <= 3 || left <= CLEAN_ENOUGH ? 0 : fe->litter_left <= 12 || left <= 0.4f ? 1 : 2;
    stopAll(game, helper);
    Object *kurti = named(game, "Kurti");
    static const char *const SAYS[] = { "Clean as you found it - cleaner! You're welcome back next year.",
                                        "A bit's still lying about. The boys will see to the rest.",
                                        "Look at this mess! The waste removal comes out of your box - 300 euros." };
    if (kurti) {
        Ai_say(kurti, SAYS[fe->verdict]);
        kurti->commitment = clampf(kurti->commitment + (fe->verdict == 0 ? 0.15f : fe->verdict == 2 ? -0.2f : 0.0f), 0.0f, 1.0f);
    }
    if (fe->verdict == 2) w->cash -= MESS_FINE;
    snprintf(game->story_line, sizeof game->story_line, "Kurti: %s", SAYS[fe->verdict]);
    say(game, game->story_line);
    game->message_time = 7.0f;
    /* everybody off home; the four of you to the car */
    for (int i = 0; i < w->count; i++) {
        Object *o = w->objects[i];
        if (!here(o) || o->lives_here || !(o->supporter || o->cast_id > 0)) continue;
        if (carCrew(o)) continue;
        goHome(o, o->supporter ? "Shift done - see you next year!" : "Bye! What a summer.");
    }
    Object *car = game->arrival.car;
    for (int k = 0; k < 3; k++) {
        Object *o = named(game, CAR_CREW[k]);
        if (!o || !here(o) || !car) continue;
        if (onIt(o)) Ai_stopWork(o);
        for (int h = 0; h < 2; h++) if (o->hands.item[h]) Hands_drop(o, h);
        o->ai.mode = AI_IDLE;
        o->ai.errand = true;
        o->ai.errand_with = NULL;
        o->ai.errand_there = false;
        o->ai.errand_to = Car_door(car, CAR_SEATS[k]);
    }
    Object *v = named(game, "Volkram");
    if (v && here(v)) Ai_say(v, "That's it. Into the car - home!");
    fe->clean = CLEAN_TO_CAR;
    fe->clean_t = 0.0f;
}

static void toCar(Game *game, float dt) {
    Festival *fe = &game->festival;
    Object *car = game->arrival.car, *p = game->player;
    if (!car || game->errand.stage != ERRAND_NONE) return; /* (off at the shops: wait) */
    fe->clean_t += dt;
    bool all = true;
    for (int k = 0; k < 3; k++) {
        Object *o = named(game, CAR_CREW[k]);
        if (!o || o->gone_home || Game_inBerlin(o) || o->hospital_until > 0.0f || o->injured) continue;
        if (o->riding) continue;
        if (o->ai.errand_there || fe->clean_t > CAR_WAIT) {
            o->ai.errand = o->ai.errand_there = false;
            Car_getIn(car, o, CAR_SEATS[k]);
        } else all = false;
    }
    if (!p->riding) {
        Vec3 door = Car_door(car, 2);
        if (hypotf(door.x - p->position.x, door.z - p->position.z) > CAR_REACH || Body_isBusy(&p->body) || p->on_ladder) return;
        for (int h = 0; h < 2; h++) if (p->hands.item[h] && p->hands.state[h] == HAND_HOLDING) Hands_drop(p, h);
        for (int h = 0; h < 2; h++) if (p->hands.state[h] != HAND_FREE) Hands_letGo(p, h);
        Car_openBoot(car, false);
        Car_getIn(car, p, 2);
        game->third_person = false;
        say(game, "In you get. Last look at Kurti's farm...");
        game->message_time = 5.0f;
    }
    if (!all) return;
    fe->clean = CLEAN_DRIVING;
    fe->clean_t = 0.0f;
    fe->drive_s = game->arrival.length;
    fe->drive_speed = 0.0f;
    Object *kurti = named(game, "Kurti");
    if (kurti) Ai_say(kurti, "Drive safely! And come back!");
}

static void drive(Game *game, float dt) {
    Festival *fe = &game->festival;
    Arrival *a = &game->arrival;
    Object *car = a->car;
    if (!car) { fe->clean = CLEAN_HOME; game_meetingHome(game); return; }
    Vec3 pos, dir;
    fe->clean_t += dt;
    float back_to = a->length - BACK_OUT;
    if (fe->drive_s > back_to) { /* backing out, still nose to the barn */
        fe->drive_s = fmaxf(back_to, fe->drive_s - BACK_SPEED * dt);
        game_driveAt(a, fe->drive_s, &pos, &dir);
        Car_place(&game->world, car, pos, yaw_facing(dir));
        fe->clean_t = 0.0f;
        return;
    }
    game_driveAt(a, fe->drive_s, &pos, &dir);
    if (fe->clean_t < TURN_TIME) { /* turning round */
        Car_place(&game->world, car, pos, yaw_facing(dir) + PI_F * smoothstepf(0.0f, TURN_TIME, fe->clean_t));
        return;
    }
    fe->drive_speed = fminf(fe->drive_speed + DRIVE_ACCEL * dt, DRIVE_TOP);
    fe->drive_s = fmaxf(0.0f, fe->drive_s - fe->drive_speed * dt);
    game_driveAt(a, fe->drive_s, &pos, &dir);
    Car_place(&game->world, car, pos, yaw_facing(vec3_scale(dir, -1.0f)));
    if (fe->drive_s <= 0.0f) { fe->clean = CLEAN_HOME; game_meetingHome(game); }
}

/* ---------- Every frame ---------- */

void game_cleanup(Game *game, float dt) {
    Festival *fe = &game->festival;
    World *w = &game->world;
    if (!game_festivalWeekend(game) || game->meeting.kind || fe->stage < FEST_OPEN) return;
    float h = hours(game);
    bool counting = fe->clean < CLEAN_TO_CAR && (fe->clean_t -= dt) <= 0.0f; /* (in the car: clean_t is the drive's clock) */
    if (counting) { fe->clean_t = COUNT_EVERY; w->fest.litter = countLitter(game); }
    switch (fe->clean) {
    case CLEAN_NONE:
        if (h >= H_SHIFT && fe->stage >= FEST_LEAVING) { fe->clean = CLEAN_SHIFT; shiftStart(game); }
        break;
    case CLEAN_SHIFT:
        if (h >= H_SHIFT_END) { stopAll(game, supporter); fe->clean = CLEAN_WAIT; break; }
        if (counting) sendOut(game, supporter);
        break;
    case CLEAN_WAIT:
        if (h >= H_CLEAN) { fe->clean = CLEAN_ON; cleanStart(game); }
        break;
    case CLEAN_ON:
        if (!counting) break;
        fe->litter_left = w->fest.litter;
        if (fe->litter_left <= 3 || fe->litter_left <= (int)((float)fe->litter_start * CLEAN_ENOUGH) || h >= H_CLEAN_END) {
            if (h >= H_CLEAN_END && fe->litter_left > 3) { Object *v = named(game, "Volkram"); if (v && here(v)) Ai_say(v, "That'll have to do - we have to get going."); }
            fe->clean = CLEAN_LOOK;
            fe->clean_t = 4.0f;
            say(game, fe->litter_left <= 3 ? "All picked up! Kurti comes over for a look" : "Kurti comes over for a look");
            break;
        }
        sendOut(game, helper);
        break;
    case CLEAN_LOOK:
        if (fe->clean_t <= 0.0f || counting) look(game);
        break;
    case CLEAN_TO_CAR: toCar(game, dt); break;
    case CLEAN_DRIVING: drive(game, dt); break;
    default: break;
    }
}

const char *game_cleanupTask(const Game *game) {
    const Festival *fe = &game->festival;
    static char text[128];
    switch (fe->clean) {
    case CLEAN_WAIT: return "Monday at ten: the clean-up";
    case CLEAN_ON:
        snprintf(text, sizeof text, "The clean-up: %d of %d pieces of litter left (a bag from the box by the container)", fe->litter_left, fe->litter_start);
        return text;
    case CLEAN_TO_CAR: return game->player->riding ? "In the car - waiting for the others" : "Home: get into Volkram's car (walk up to the back door)";
    case CLEAN_DRIVING: return "Off home to Berlin";
    default: return NULL;
    }
}

int game_cleanupNotes(const Game *game, char lines[][128], int max) {
    const Festival *fe = &game->festival;
    int n = 0;
    if (fe->clean >= CLEAN_SHIFT && n < max) snprintf(lines[n++], 128, "-  Supporters: %d did the Sunday shift%s", fe->supported, fe->clean >= CLEAN_ON ? "" : " (the second: Monday's clean-up)");
    if (fe->clean >= CLEAN_ON && n < max) snprintf(lines[n++], 128, "-  The clean-up: %d of the crew, %d supporters; %d of %d pieces left", fe->helpers, fe->supported2, fe->litter_left, fe->litter_start);
    if (fe->clean >= CLEAN_TO_CAR && n < max)
        snprintf(lines[n++], 128, "-  Kurti: %s", fe->verdict == 0 ? "clean - welcome back" : fe->verdict == 1 ? "a bit left (his sons do the rest)" : "a mess (300 EUR for the waste removal)");
    return n;
}