Nimbin[12]?Games / Entropie / entropie/src/people/ai_project.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/people/ai_project.c 17.6 KB · 336 lines raw
/*
 * Their part of the festival (world/cast.h: leads, helps, commitment).
 *
 * The lead and the helpers of a floor go to its stage in working hours and
 * build it, in goes of a couple of hours (then a break, eating, the loo...).
 * Held up for bought material that lies somewhere else (by the barn, where the
 * shopping gets unloaded), one of them goes and carries it over; logs are
 * yours to bring. Off on a shopping run (game_shop.c): over to the car.
 * The side jobs (mowing, the snails, Kerry's art) go the same way, their
 * work itself in ai_jobs.c.
 * How into it they are goes with what happens: a step finished lifts them;
 * being held up (no logs for the frame), working on worn out, nobody leading,
 * being annoyed wear it down. Too low, a lead gives up leading ("somebody
 * else can do it") and stays on as a helper; lower still, they stop helping
 * at all. Evenings at the fire bring a bit of it back.
 */

#include <stdio.h>

#include "people/ai_internal.h"
#include "world/stage.h"
#include "world/camp.h"

#define WORK_FROM      8.5f   /* o'clock: the working day ... */
#define WORK_TO        19.5f  /* ... till */
#define SHIFT          7200.0f /* game s at it, then a break ... */
#define BREAK_TIME     2400.0f /* ... this long */
#define START_CHANCE   0.03f  /* a second: idle in working hours, off to it (helpers: less without the lead there) */
#define TIRED          0.3f   /* stamina below this: they stop */
#define NEED_STOP      0.65f  /* hunger, thirst or bladder over this: they stop */
#define LEAD_NEAR      40.0f  /* m: the lead about (helpers keep at it better) */
#define HELD_UP_LEAVE  1500.0f /* game s held up: they give it up for now */
#define REMARK_EVERY   90.0f  /* real s between remarks about it */
#define QUIT_LEAD      0.3f   /* commitment below this: the lead gives it up ... */
#define QUIT_HELP      0.15f  /* ... below this they stop helping at all */
/* commitment, per game hour */
#define LIFT_DONE      0.08f  /* (once) a step of it finished, for everybody at it */
#define WEAR_HELD_UP   0.06f
#define WEAR_WORN      0.04f  /* working on worn out */
#define WEAR_NO_LEAD   0.03f  /* a helper with nobody leading it */
#define WEAR_ANNOYED   0.04f
#define LIFT_EVENING   0.02f  /* sitting at the fire of an evening */
#define FETCH_SPAN     150.0f /* m: material lying this far off is fetched for the stage */
#define FETCH_GIVE_UP  240.0f /* real s at fetching one thing: given up */

static bool floorProject(int p) {
    return p == PROJECT_HOFBUEHNE || p == PROJECT_OBSTWIESE || p == PROJECT_KARTOFFELACKER || p == PROJECT_HIDDEN;
}

/* Something to go and do on the ground: a floor, or a side job (ai_jobs.c). */
static bool workable(int p) {
    return floorProject(p) || p == PROJECT_MOWING || p == PROJECT_SNAILS || p == PROJECT_WOOD_ART || p == PROJECT_BIG_TREE;
}

static int theirProject(const Object *self) {
    if (workable(self->leads)) return self->leads;
    if (workable(self->helps)) return self->helps;
    return PROJECT_NONE;
}

static Object *leadOf(Object *const *objects, int count, int project) {
    for (int i = 0; i < count; i++)
        if (objects[i]->is_character && objects[i]->leads == project) return objects[i];
    return NULL;
}

/* Who speaks for those at it: the lead if there, else the first of them (one voice, not a chorus). */
static bool firstAt(const Object *self, Object *const *objects, int count, int project) {
    if (self->leads == project) return true;
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (o == self || !o->is_character || !o->ai.on_project || theirProject(o) != project) continue;
        if (o->leads == project || o->cast_id < self->cast_id) return false;
    }
    return true;
}

static void lose(Object *self, float per_hour, float game_dt) {
    self->commitment = clampf(self->commitment - per_hour * game_dt / 3600.0f, 0.0f, 1.0f);
}

void ai_commitment(Object *self, Object *const *objects, int count, float dt) {
    if (self->cast_id < 0 || !g_world) return;
    Ai *ai = &self->ai;
    const float gdt = g_world->game_dt;
    ai->praised = fmaxf(0.0f, ai->praised - gdt);
    if (ai->annoyed > 0.5f) lose(self, WEAR_ANNOYED, gdt);
    if (ai->on_project && self->needs.stamina < TIRED + 0.1f) lose(self, WEAR_WORN, gdt);
    if (ai->on_project && ai->held_up > 0.0f) lose(self, WEAR_HELD_UP, gdt);
    if (ai->on_project && self->leads != theirProject(self) && !leadOf(objects, count, theirProject(self))) lose(self, WEAR_NO_LEAD, gdt);
    if (self->sitting && (g_world->hour > 19.0f || g_world->hour < 1.0f) && Camp_fireNear(objects, count, self->position, 5.0f, true))
        self->commitment = fminf(1.0f, self->commitment + LIFT_EVENING * gdt / 3600.0f);
    (void)dt;
    /* gone too low: the lead given up, or helping at all */
    if (self->leads != PROJECT_NONE && self->commitment < QUIT_LEAD) {
        char line[AI_SPEECH_MAX];
        const char *why = ai->held_up > 0.0f ? "Nobody brings what we need." : self->needs.stamina < TIRED + 0.1f ? "I'm wrecked." :
                          ai->annoyed > 0.5f ? "I've had enough of all this." : "It's not working for me.";
        snprintf(line, sizeof line, "%s I'm out - somebody else can lead the %s.", why, Cast_projectName((Project)self->leads));
        Ai_say(self, line);
        self->helps = self->commitment > QUIT_HELP ? self->leads : PROJECT_NONE;
        self->leads = PROJECT_NONE;
    } else if (self->helps != PROJECT_NONE && self->leads == PROJECT_NONE && self->commitment < QUIT_HELP) {
        Ai_say(self, "Count me out of the work. I'm just here for the party now.");
        self->helps = PROJECT_NONE;
        ai->on_project = false;
    }
}

/* Stop for now (a break, the day's over, held up too long...). */
static bool stop(Object *self, const char *say) {
    Ai *ai = &self->ai;
    if (ai->on_project && say && self->ai.speech_time <= 0.0f) Ai_say(self, say);
    if (ai->on_project) ai->project_off = BREAK_TIME; /* (a break before the next go: only after one) */
    if (ai->fetch) { /* (whatever they were bringing: down where they are) */
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == ai->fetch) Hands_drop(self, h);
        ai->fetch = NULL;
    }
    ai->on_project = false;
    ai->shift = 0.0f;
    ai->held_up = 0.0f;
    return false;
}

/* Others at work on it with them. */
static bool othersAt(const Object *self, Object *const *objects, int count, int project) {
    for (int i = 0; i < count; i++)
        if (objects[i] != self && objects[i]->is_character && objects[i]->ai.on_project && theirProject(objects[i]) == project) return true;
    return false;
}

/* Lying by a stage (it's for that one). */
static bool atAStage(const Object *o, Object *const *objects, int count) {
    for (int i = 0; i < count; i++)
        if (objects[i]->type == OBJ_STAGE && flatDistance(objects[i]->position, o->position) < STAGE_LOG_REACH) return true;
    return false;
}

static bool fetchedByOther(const Object *self, const Object *it, Object *const *objects, int count) {
    for (int i = 0; i < count; i++)
        if (objects[i] != self && objects[i]->is_character && objects[i]->ai.fetch == it) return true;
    return false;
}

/* The nearest of that material lying about (not at a stage, nobody bringing it), or NULL. */
static Object *materialFor(const Object *self, Object *const *objects, int count, int kind) {
    Object *best = NULL;
    float best_d = FETCH_SPAN;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_MATERIAL || o->variant != kind || o->is_held || o->broken) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d && !atAStage(o, objects, count) && !fetchedByOther(self, o, objects, count)) { best_d = d; best = o; }
    }
    return best;
}

static bool holds(const Object *self, const Object *it) {
    for (int h = 0; h < 2; h++)
        if (self->hands.item[h] == it && (self->hands.state[h] == HAND_HOLDING || self->hands.state[h] == HAND_REACHING)) return true;
    return false;
}

/* Bringing material over to the stage: true while at it. */
static bool fetchMaterial(Object *self, Object *stage, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *it = ai->fetch;
    if (!it) return false;
    bool mine = holds(self, it);
    if (it->broken || (it->is_held && !mine) || (ai->fetch_time += dt) > FETCH_GIVE_UP) {
        for (int h = 0; h < 2; h++) if (mine && self->hands.item[h] == it) Hands_drop(self, h);
        ai->fetch = NULL;
        return false;
    }
    if (mine) {
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == it && self->hands.state[h] == HAND_REACHING) { stand(self); return true; }
        Vec3 front = yaw_front(stage->rotation.y);
        Vec3 drop = vec3_add(Stage_workSpot(stage, ai->side < 0.0f ? -0.3f : 0.3f), vec3_scale(front, 1.3f)); /* (out in front, behind those at work) */
        if (!ai_goToPoint(self, drop, 0.5f, objects, count, dt)) return true;
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == it) Hands_drop(self, h);
        ai->fetch = NULL;
        if (ai->speech_time <= 0.0f) Ai_say(self, it->variant == MATERIAL_BOARDS ? "Boards are here!" : it->variant == MATERIAL_PAINT ? "Paint's here!" : "Got it - here.");
        return true;
    }
    if (Hands_gapFrom(self->position, self->look.height, it) < PICKUP_REACH) {
        stand(self);
        turnTowards(self, vec3_sub(it->position, self->position), dt);
        freeHands(self, NULL);
        if (!Hands_pickUpObject(self, self->main_hand, it) && !Hands_pickUpObject(self, 1 - self->main_hand, it)) ai->fetch = NULL;
        return true;
    }
    ai_goToPoint(self, it->position, 0.3f, objects, count, dt);
    return true;
}

/* Off somewhere on purpose: a shopping run (over to the car, the driver's door: game_shop.c gets
   them in), showing somebody the way (waiting for them when they fall behind: game_arrival.c). */
#define GUIDE_WAIT 6.0f /* m: the one shown the way further behind than this: wait for them */
bool ai_errand(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (!ai->errand) return false;
    if (self->sitting) self->sitting = false; /* (up first) */
    self->indoors = 0.0f;
    const Object *w = ai->errand_with;
    if (w && !ai->errand_there && flatDistance(w->position, self->position) > GUIDE_WAIT) { /* (they're coming? waiting, looking back) */
        stand(self);
        turnTowards(self, vec3_sub(w->position, self->position), dt);
        return true;
    }
    ai->errand_there = ai_goToPoint(self, ai->errand_to, w ? 1.0f : 0.6f, objects, count, dt);
    if (ai->errand_there) {
        stand(self);
        if (w) turnTowards(self, vec3_sub(w->position, self->position), dt);
    }
    return true;
}

static const char *doneLine(int project, int step) {
    static const char *const ANY[] = { "", "Ground's clear - now the frame.", "The frame stands!", "Deck's in - try it, it holds.",
                                       "Roof's on. Now make it pretty.", "Done! Look at it!" };
    if (project == PROJECT_OBSTWIESE && step == BUILD_DONE) return "Done - the trees will glow on this one. Psy heaven!";
    if (project == PROJECT_HIDDEN && step == BUILD_ROOF) return "Back wall's up - nobody will see it from the yard. Perfect.";
    return ANY[step < 0 ? 0 : step > BUILD_DONE ? BUILD_DONE : step];
}

bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (!g_world || self->cast_id < 0) return false;
    const float gdt = g_world->game_dt;
    int project = theirProject(self);
    bool floor = floorProject(project);
    Object *stage = floor ? Stage_of(objects, count, (Project)project) : NULL;
    self->carving = false;
    self->mowing = 0.0f;
    ai->project_off = fmaxf(0.0f, ai->project_off - gdt);
    if (!project || (floor && (!stage || stage->build_stage >= BUILD_DONE)) || (!floor && ai_jobDone(project) && self->snails == 0)) return stop(self, NULL); /* (snails still in the bucket: let go first) */
    ai->remark = fmaxf(0.0f, ai->remark - dt);
    bool lead = self->leads == project;
    Object *boss = leadOf(objects, count, project);
    bool boss_near = boss && flatDistance(boss->position, stage ? stage->position : self->position) < LEAD_NEAR;
    float hour = g_world->hour;
    /* not now */
    if (hour < WORK_FROM || hour > WORK_TO) return stop(self, lead ? "That's it for today." : NULL);
    if (g_world->fest.stage >= FEST_OPEN) return stop(self, NULL); /* (the festival's on: what's built is built) */
    if (self->needs.stamina < TIRED) return stop(self, "I need a break.");
    if (self->needs.hunger > NEED_STOP || self->needs.thirst > NEED_STOP || self->needs.bladder > NEED_STOP) return stop(self, NULL);
    if (self->drunk > 0.5f || self->body.asleep || self->in_tent || self->riding || self->indoors > 0.0f || ai->sheltering || ai->errand) return stop(self, NULL);
    if (!ai->on_project) {
        if (ai->project_off > 0.0f || ai->greet || ai->unload) return false;
        float chance = START_CHANCE * (lead || boss_near ? 1.0f : 0.25f) * (0.5f + self->commitment);
        int focus = g_world->focus; /* (what the Herberge said this weekend is for) */
        if ((focus == FOCUS_FLOORS && floor) || (focus == FOCUS_SIDE && !floor)) chance *= 1.6f;
        else if (focus == FOCUS_PARTY) chance *= 0.5f;
        if (random01(ai) >= chance * dt) return false;
        ai->on_project = true;
        ai->shift = 0.0f;
        if (lead && ai->speech_time <= 0.0f) {
            char line[AI_SPEECH_MAX];
            if (floor) snprintf(line, sizeof line, "Off to the %s - %s. Who's coming?", Cast_projectName((Project)project), Stage_stepName(stage->build_stage));
            else snprintf(line, sizeof line, "%s", ai_jobStart(project));
            Ai_say(self, line);
        }
    }
    if ((ai->shift += gdt) > SHIFT) return stop(self, lead ? "Break! Back in a bit." : NULL);
    if (self->sitting) self->sitting = false; /* (up first) */
    if (!floor) { /* a side job */
        char why[AI_SPEECH_MAX] = "";
        int got = ai_job(self, project, objects, count, dt, why, sizeof why);
        if (got == SIDE_HELD) {
            ai->held_up += gdt;
            if (ai->remark <= 0.0f && firstAt(self, objects, count, project) && why[0]) { Ai_say(self, why); ai->remark = REMARK_EVERY; }
            if (ai->held_up > HELD_UP_LEAVE) return stop(self, "Can't get on with it like this. Later.");
            return true;
        }
        ai->held_up = 0.0f;
        if (got == SIDE_STEP) {
            self->commitment = fminf(1.0f, self->commitment + LIFT_DONE * 0.5f);
            if (why[0]) Ai_say(self, why);
        }
        if (got == SIDE_FINISHED) {
            for (int i = 0; i < count; i++) {
                Object *o = objects[i];
                if (o->is_character && theirProject(o) == project) o->commitment = fminf(1.0f, o->commitment + LIFT_DONE);
            }
            return stop(self, why[0] ? why : NULL);
        }
        return true;
    }
    if (fetchMaterial(self, stage, objects, count, dt)) return true;
    freeHands(self, NULL);
    float side = ai->side != 0.0f ? ai->side * 0.8f : (lead ? 0.0f : 0.6f);
    Vec3 spot = Stage_workSpot(stage, side);
    if (!ai_goToPoint(self, spot, 0.4f, objects, count, dt)) return true;
    Vec3 hands = Stage_handsAt(stage, spot, self->body.clock);
    turnTowards(self, vec3_sub(stage->position, self->position), dt);
    Hands_touch(self, hands);
    /* the work: quicker the handier they are, and with the lead about */
    float handy = 0.5f * (self->skill[ORDER_FIREWOOD] + self->skill[ORDER_TREES]);
    float rate = (0.6f + 0.8f * handy) * self->tempo * (lead ? 1.1f : boss_near ? 1.0f : 0.6f) * (1.0f - 0.5f * fminf(self->drunk, 1.0f));
    int got = Stage_work(g_world, stage, rate * gdt / 3600.0f);
    if (got == -2) { /* held up: material missing */
        int kind = Stage_missingMaterial(g_world, stage);
        Object *there = kind >= 0 ? materialFor(self, objects, count, kind) : NULL;
        if (there && (!lead || !othersAt(self, objects, count, project))) { /* (lying somewhere else: fetched) */
            ai->fetch = there;
            ai->fetch_time = 0.0f;
            if (ai->speech_time <= 0.0f) Ai_say(self, kind == MATERIAL_BOARDS ? "I'll get the boards." : "I'll fetch it.");
            return true;
        }
        if (kind >= 0) { /* (somebody's on the way with it: wait for it) */
            for (int i = 0; i < count; i++)
                if (objects[i]->is_character && objects[i]->ai.fetch && objects[i]->ai.fetch->variant == kind && theirProject(objects[i]) == project) return true;
        }
        ai->held_up += gdt;
        if (ai->remark <= 0.0f && firstAt(self, objects, count, project)) {
            char line[AI_SPEECH_MAX], what[40];
            Stage_missing(g_world, stage, what, sizeof what);
            if (stage->build_stage == BUILD_FRAME) snprintf(line, sizeof line, "We need %s here for the frame!", what);
            else snprintf(line, sizeof line, "We need %s - somebody go shopping!", what);
            Ai_say(self, line);
            ai->remark = REMARK_EVERY;
        }
        if (ai->held_up > HELD_UP_LEAVE)
            return stop(self, stage->build_stage == BUILD_FRAME ? "No logs, no frame. I'm off till there are some." : "Nothing to build with. I'm off till the shopping's here.");
        return true;
    }
    ai->held_up = 0.0f;
    if (got > 0) { /* a step done: everybody at it is lifted */
        for (int i = 0; i < count; i++) {
            Object *o = objects[i];
            if (o->is_character && o->ai.on_project && theirProject(o) == project) o->commitment = fminf(1.0f, o->commitment + LIFT_DONE);
        }
        if (firstAt(self, objects, count, project)) Ai_say(self, doneLine(project, got));
    }
    return true;
}