Nimbin[12]?Games / Entropie / entropie/src/people/ai_jobs.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_jobs.c 18.2 KB · 354 lines raw
/*
 * The side jobs, one frame of the work at a time (ai_project.c decides when
 * they're at it: working hours, breaks, how into it they are):
 *
 * - Mowing the Kartoffelacker (Toni): the scythe fetched, then cell by cell
 *   along the field, a swing at a time (world/field.c). A snail in the grass
 *   there is dead.
 * - The snail rescue (Doro, Stella): from snail to snail, bending down for
 *   each, into the bucket; full (or none left near), over to the refuge by
 *   the wood and let go.
 * - Kerry's art: a log lying at the art spot and the chainsaw - he carves it
 *   into a figure; without either he says what he needs.
 */

#include <stdio.h>

#include "people/ai_internal.h"
#include "world/field.h"
#include "world/site.h"
#include "world/stage.h"
#include "world/bigtree.h"

#define MOW_REACH   1.2f  /* m from the cell: mowing it from here */
#define MOW_SWING   1.5f  /* real s a swing of the scythe (quicker for the quick) */
#define MOW_RADIUS  0.8f  /* m: what a swing takes */
#define MOW_CAREFUL 0.45f /* how careful (ai_stackCare): the snails out of the field before they mow */
#define SNAIL_LOOK  30.0f /* m: snails this near are gone for (none: to the refuge with what's in the bucket) */
#define SNAIL_BEND  1.2f  /* s bending down for one */
#define REFUGE_NEAR 1.8f  /* m: at the refuge */
#define CARVE_SIDE  0.8f  /* m out from the log's middle: where Kerry stands */

/* Something of that kind in their hands. */
static Object *heldOf(const Object *self, ObjectType type) {
    for (int h = 0; h < 2; h++) {
        Object *it = self->hands.item[h];
        if (it && it->type == type && self->hands.state[h] == HAND_HOLDING) return it;
    }
    return NULL;
}

static bool takenBy(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 a kind lying about (not held, nobody else after it). */
static Object *nearestFree(const Object *self, Object *const *objects, int count, ObjectType type, int variant, float within) {
    Object *best = NULL;
    float best_d = within;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != type || o->is_held || o->broken || (variant >= 0 && o->variant != variant)) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d && !takenBy(self, o, objects, count)) { best_d = d; best = o; }
    }
    return best;
}

/* Over to it and up it comes (true: in the hands). */
static bool take(Object *self, Object *it, Object *const *objects, int count, float dt) {
    for (int h = 0; h < 2; h++) if (self->hands.item[h] == it) return self->hands.state[h] == HAND_HOLDING;
    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)) self->ai.fetch = NULL;
        return false;
    }
    ai_goToPoint(self, it->position, 0.3f, objects, count, dt);
    return false;
}

static float handy(const Object *self) {
    return (0.6f + 0.8f * 0.5f * (self->skill[ORDER_FIREWOOD] + self->skill[ORDER_TREES])) * self->tempo * (1.0f - 0.5f * fminf(self->drunk, 1.0f));
}

/* ---------- The scythe ---------- */

static int mow(Object *self, Object *const *objects, int count, float dt, char *why, size_t size) {
    Ai *ai = &self->ai;
    if (Field_done(g_world) >= 1.0f) { snprintf(why, size, "The Kartoffelacker's mown! Looks like a festival ground now."); return SIDE_FINISHED; }
    if (ai_stackCare(self) >= MOW_CAREFUL && Field_snailsInField(g_world) > 0 && !g_world->inspected) { /* (the careful: the snails out first) */
        snprintf(why, size, "I'll wait with the mowing till the snails are out of the field - %d still in the grass.", Field_snailsInField(g_world));
        return SIDE_HELD;
    }
    Object *scythe = heldOf(self, OBJ_SCYTHE);
    if (!scythe) { /* the scythe first */
        Object *s = ai->fetch && ai->fetch->type == OBJ_SCYTHE && !ai->fetch->is_held ? ai->fetch : nearestFree(self, objects, count, OBJ_SCYTHE, -1, 400.0f);
        if (!s) { snprintf(why, size, "Who's got the scythe? I can't mow without it."); return SIDE_HELD; }
        if (ai->fetch != s && ai->speech_time <= 0.0f) Ai_say(self, "Scythe first - it's in the tool room.");
        ai->fetch = s;
        take(self, s, objects, count, dt);
        return SIDE_WORKING;
    }
    if (ai->fetch == scythe) ai->fetch = NULL;
    Vec3 cell;
    if (!Field_nextUnmown(g_world, self->position, &cell)) return SIDE_FINISHED;
    if (flatDistance(cell, self->position) > MOW_REACH) {
        ai_goToPoint(self, cell, MOW_REACH * 0.8f, objects, count, dt);
        return SIDE_WORKING;
    }
    stand(self);
    turnTowards(self, vec3_sub(cell, self->position), dt);
    float swing = MOW_SWING / fmaxf(self->tempo, 0.5f);
    ai->fetch_time += dt;
    self->mowing = 0.001f + 3.1416f * ai->fetch_time / swing; /* (the blade across, right to left) */
    if (ai->fetch_time < swing) return SIDE_WORKING;
    ai->fetch_time = 0.0f;
    float before = Field_done(g_world);
    int killed = 0;
    Field_mow(g_world, vec3_add(self->position, vec3_scale(Object_facing(self), 0.9f)), MOW_RADIUS, &killed);
    World_noise(g_world, NOISE_RUSTLE, self->position, 0.4f);
    float after = Field_done(g_world);
    if (killed) { snprintf(why, size, "Oh no - I got a snail with the scythe! They should be out of here first."); return SIDE_STEP; }
    if ((int)(after * 4.0f) > (int)(before * 4.0f) && after < 1.0f) {
        snprintf(why, size, "%d%% of the Kartoffelacker mown.", (int)(after * 4.0f) * 25);
        return SIDE_STEP;
    }
    return SIDE_WORKING;
}

/* ---------- The snails ---------- */

static int snails(Object *self, Object *const *objects, int count, float dt, char *why, size_t size) {
    Ai *ai = &self->ai;
    Object *sn = ai->fetch && ai->fetch->type == OBJ_SNAIL && !ai->fetch->broken ? ai->fetch : nearestFree(self, objects, count, OBJ_SNAIL, 0, SNAIL_LOOK);
    if (self->snails >= SNAIL_BUCKET || (self->snails > 0 && !sn)) { /* the bucket over to the refuge */
        ai->fetch = NULL;
        Vec3 refuge = Site_snailRefuge();
        if (!ai_goToPoint(self, refuge, REFUGE_NEAR, objects, count, dt)) return SIDE_WORKING;
        int n = Field_release(g_world, self);
        snprintf(why, size, "There you go, %d of you - safe here.", n);
        return SIDE_STEP;
    }
    if (!sn) { /* none near: further off, or all done */
        sn = nearestFree(self, objects, count, OBJ_SNAIL, 0, 1000.0f);
        if (!sn) { snprintf(why, size, "That's all the snails out. The office can come!"); return SIDE_FINISHED; }
    }
    if (ai->fetch != sn) ai->fetch_time = 0.0f;
    ai->fetch = sn;
    if (flatDistance(sn->position, self->position) > 0.55f) {
        ai_goToPoint(self, sn->position, 0.45f, objects, count, dt);
        return SIDE_WORKING;
    }
    stand(self);
    turnTowards(self, vec3_sub(sn->position, self->position), dt);
    self->body.crouch_goal = fmaxf(self->body.crouch_goal, 0.85f); /* (down to it) */
    Hands_touch(self, vec3_add(sn->position, vec3(0.0f, 0.04f, 0.0f)));
    if ((ai->fetch_time += dt) < SNAIL_BEND) return SIDE_WORKING;
    Field_pickSnail(g_world, self, sn);
    ai->fetch = NULL;
    ai->fetch_time = 0.0f;
    return SIDE_WORKING;
}

/* ---------- Kerry's art ---------- */

static int carve(Object *self, Object *const *objects, int count, float dt, char *why, size_t size) {
    Ai *ai = &self->ai;
    Object *log = Field_artLog(g_world);
    if (!log) { snprintf(why, size, "Bring me a log to the yard by the pavilions - I'll make something of it!"); return SIDE_HELD; }
    Object *saw = heldOf(self, OBJ_CHAINSAW);
    if (!saw) {
        Object *c = ai->fetch && ai->fetch->type == OBJ_CHAINSAW && !ai->fetch->is_held ? ai->fetch : nearestFree(self, objects, count, OBJ_CHAINSAW, -1, 400.0f);
        if (!c) { snprintf(why, size, "Somebody's got the chainsaw - I need it for the art."); return SIDE_HELD; }
        ai->fetch = c;
        take(self, c, objects, count, dt);
        return SIDE_WORKING;
    }
    if (ai->fetch == saw) ai->fetch = NULL;
    Vec3 c = Object_colliderCenter(log, 0), along = log->colliders[0].axes[1];
    Vec3 across = vec3_normalize(vec3(-along.z, 0.0f, along.x));
    if (vec3_dot(vec3_sub(self->position, c), across) < 0.0f) across = vec3_scale(across, -1.0f);
    Vec3 spot = vec3_add(c, vec3_scale(across, CARVE_SIDE));
    if (!ai_goToPoint(self, spot, 0.35f, objects, count, dt)) return SIDE_WORKING;
    stand(self);
    turnTowards(self, vec3_sub(c, self->position), dt);
    float t = self->body.clock;
    Hands_touch(self, vec3_add(c, vec3(0.15f * sinf(t * 1.3f), 0.25f + 0.1f * sinf(t * 0.7f), 0.1f * cosf(t * 1.1f))));
    self->carving = true;
    log->build_work += handy(self) * g_world->game_dt / 3600.0f;
    if (log->build_work < CARVE_NEED) return SIDE_WORKING;
    self->carving = false;
    Field_carve(g_world, log);
    snprintf(why, size, "Done! What do you think? Another log and I'll do another one.");
    return SIDE_STEP;
}

/* ---------- The big old tree (Enrico) ---------- */

#define ROPE_WAIT   5400.0f /* game s waiting for somebody to throw the line up: then he does it himself (not as high) */
#define CLEAR_WAIT  45.0f   /* real s shouting at people in the way before he cuts anyway */
#define FALL_ZONE   24.0f   /* m round the tree on the side it falls to (and some of the rest): nobody but the pullers */

static void sayEvery(Object *self, const char *line) {
    if (self->ai.remark > 0.0f || self->ai.speech_time > 0.0f) return;
    Ai_say(self, line);
    self->ai.remark = 60.0f;
}

/* Somebody standing where it could come down (not the pullers out at the rope's end). */
static Object *inTheWay(const Object *self, Object *const *objects, int count) {
    Vec3 t = Site_bigTree(), safe = Site_bigTreeSafe();
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o == self || !o->is_character || o->type == OBJ_DOG || o->puller > 0.0f || o->indoors > 0.0f || o->riding) continue;
        Vec3 d = vec3_sub(o->position, t);
        d.y = 0.0f;
        float dist = vec3_length(d), ahead = vec3_dot(d, safe);
        if (dist < 6.0f || (dist < FALL_ZONE && ahead > -4.0f)) return o;
    }
    return NULL;
}

static int bigTree(Object *self, Object *const *objects, int count, float dt, char *why, size_t size) {
    Ai *ai = &self->ai;
    BigTree *bt = &g_world->bigtree;
    Object *tree = BigTree_tree(g_world);
    if (!tree || bt->step >= BT_FALLING) { snprintf(why, size, "%s", bt->lines_down ? "Down. And the line with it... sorry, Kurti." : "Down it is - and the line's fine!"); return SIDE_FINISHED; }
    Vec3 t = Site_bigTree();
    self->carving = false;
    switch (bt->step) {
    case BT_STANDING: { /* first a good look at it, the throw line put down at its foot */
        if (!ai_goToPoint(self, BigTree_notchSpot(g_world), 1.0f, objects, count, dt)) return SIDE_WORKING;
        Vec3 at = vec3_add(BigTree_notchSpot(g_world), vec3_scale(Site_bigTreeSafe(), 1.5f));
        at.y = World_groundHeight(g_world, at.x, at.z, at.y + 2.0f) + 0.02f;
        Object *bag = Object_create(OBJ_ROPE, PALETTE_WARM, at, vec3_zero(), 0.0f);
        if (bag && !World_add(g_world, bag)) bag = NULL;
        bt->step = BT_ROPE;
        snprintf(why, size, "It leans to the road - the phone line! A rope up high, then we pull it the other way.");
        return SIDE_STEP;
    }
    case BT_ROPE: /* the rope up over a branch: you (the throw line lies at its foot), or after a while he does it */
        if (bt->rope_h > 0.0f) { bt->step = BT_NOTCH; bt->work = 0.0f; return SIDE_WORKING; }
        if (!ai_goToPoint(self, vec3_add(BigTree_notchSpot(g_world), vec3_scale(Site_bigTreeSafe(), 3.0f)), 1.0f, objects, count, dt)) return SIDE_WORKING;
        stand(self);
        turnTowards(self, vec3_sub(t, self->position), dt);
        self->body.look_dir = vec3_normalize(vec3_add(vec3_sub(t, self->position), vec3(0.0f, 12.0f, 0.0f)));
        self->body.look_weight = 0.8f;
        if ((bt->rope_wait += g_world->game_dt) < ROPE_WAIT) {
            sayEvery(self, "Who throws the line up over a branch? As high as you can - the bag's at the foot.");
            return SIDE_WORKING;
        }
        for (int i = 0; i < count; i++) if (objects[i]->type == OBJ_ROPE && !objects[i]->broken) Object_useUp(objects[i]);
        bt->rope_h = 6.0f + 1.5f * random01(ai);
        snprintf(why, size, "Right, I'll do it myself... %.1f m. Not great - but it'll have to do.", bt->rope_h);
        return SIDE_STEP;
    case BT_NOTCH: { /* the chainsaw, the notch on the side it should fall to */
        Object *saw = heldOf(self, OBJ_CHAINSAW);
        if (!saw) {
            Object *c = ai->fetch && ai->fetch->type == OBJ_CHAINSAW && !ai->fetch->is_held ? ai->fetch : nearestFree(self, objects, count, OBJ_CHAINSAW, -1, 400.0f);
            if (!c) { snprintf(why, size, "Who's got the chainsaw? I need it for the big tree."); return SIDE_HELD; }
            ai->fetch = c;
            take(self, c, objects, count, dt);
            return SIDE_WORKING;
        }
        if (ai->fetch == saw) ai->fetch = NULL;
        if (!ai_goToPoint(self, BigTree_notchSpot(g_world), 0.35f, objects, count, dt)) return SIDE_WORKING;
        stand(self);
        turnTowards(self, vec3_sub(t, self->position), dt);
        Hands_touch(self, vec3_add(vec3_lerp(t, self->position, 0.45f), vec3(0.0f, 0.55f, 0.0f)));
        self->carving = true;
        self->body.crouch_goal = fmaxf(self->body.crouch_goal, 0.45f);
        if ((bt->work += handy(self) * g_world->game_dt / 3600.0f) < BT_NOTCH_WORK) return SIDE_WORKING;
        bt->step = BT_PULL;
        bt->work = 0.0f;
        snprintf(why, size, "Notch is in! Now - everybody on the rope, out at the far end. Three at least!");
        return SIDE_STEP;
    }
    case BT_PULL: case BT_CUT: { /* the back cut, once there are enough on the rope and the way's clear */
        if (!heldOf(self, OBJ_CHAINSAW)) { bt->step = BT_NOTCH; return SIDE_WORKING; } /* (somebody took it: back for it) */
        if (!ai_goToPoint(self, BigTree_backSpot(g_world), 0.35f, objects, count, dt)) return SIDE_WORKING;
        stand(self);
        turnTowards(self, vec3_sub(t, self->position), dt);
        if (bt->step == BT_PULL) {
            if (bt->pullers < 3) { sayEvery(self, "I need three on the rope - at least! Out at the end of it."); return SIDE_WORKING; }
            Object *way = inTheWay(self, objects, count);
            if (way && (ai->fetch_time += dt) < CLEAR_WAIT) {
                char line[AI_SPEECH_MAX];
                snprintf(line, sizeof line, "%s! Out of there - that's where it comes down!", way->type == OBJ_PLAYER ? "Hey, you" : way->name);
                if (ai->speech_time <= 0.0f) Ai_say(self, line);
                return SIDE_WORKING;
            }
            ai->fetch_time = 0.0f;
            bt->step = BT_CUT;
            bt->work = 0.0f;
            Ai_say(self, "Back cut! On the rope - get ready...");
        }
        Hands_touch(self, vec3_add(vec3_lerp(t, self->position, 0.45f), vec3(0.0f, 0.6f, 0.0f)));
        self->carving = true;
        float was = bt->work;
        bt->work += dt;
        if (was < BT_CUT_GOES && bt->work >= BT_CUT_GOES) Ai_say(self, "It's going - PULL! PULL!");
        if (bt->work < BT_CUT_TIME) return SIDE_WORKING;
        self->carving = false;
        bt->step = BT_FALLING; /* (game_bigtree.c: which way, down it comes) */
        return SIDE_WORKING;
    }
    default: return SIDE_FINISHED;
    }
}

/* On the rope (game_bigtree.c picks them: puller = their place + 1): out to it, hands on, and when it goes: pull. */
bool ai_onRope(Object *self, Object *const *objects, int count, float dt) {
    if (self->puller <= 0.0f || !g_world) return false;
    BigTree *bt = &g_world->bigtree;
    if (bt->step < BT_PULL || bt->step > BT_FALLING || Body_isBusy(&self->body)) { self->puller = 0.0f; return false; }
    if (self->sitting) self->sitting = false;
    for (int h = 0; h < 2; h++) if (self->hands.item[h]) Hands_drop(self, h); /* (both hands on it) */
    Vec3 spot = BigTree_pullerSpot(g_world, (int)self->puller - 1);
    if (!ai_goToPoint(self, spot, 0.35f, objects, count, dt)) return true;
    stand(self);
    Vec3 t = Site_bigTree();
    turnTowards(self, vec3_sub(t, self->position), dt);
    Vec3 at = BigTree_ropeAt(g_world, self->position);
    self->hands.rope = bt->step == BT_CUT && bt->work >= BT_CUT_GOES - 3.0f ? 1.0f : 0.05f;
    self->hands.rope_at = at;
    self->hands.rope_dir = vec3_normalize(vec3_sub(BigTree_ropeEnd(g_world), BigTree_attach(g_world)));
    if (bt->step == BT_CUT && bt->work >= BT_CUT_GOES && self->ai.speech_time <= 0.0f && random01(&self->ai) < 0.5f * dt)
        Ai_say(self, random01(&self->ai) < 0.5f ? "Hnnngh!" : "Pull!");
    (void)objects; (void)count;
    return true;
}

int ai_job(Object *self, int project, Object *const *objects, int count, float dt, char *why, size_t size) {
    switch (project) {
    case PROJECT_MOWING:   return mow(self, objects, count, dt, why, size);
    case PROJECT_SNAILS:   return snails(self, objects, count, dt, why, size);
    case PROJECT_WOOD_ART: return carve(self, objects, count, dt, why, size);
    case PROJECT_BIG_TREE: return self->leads == PROJECT_BIG_TREE ? bigTree(self, objects, count, dt, why, size) : SIDE_FINISHED; /* (helpers: on the rope when it's time) */
    default:               return SIDE_FINISHED;
    }
}

bool ai_jobDone(int project) {
    if (!g_world) return true;
    switch (project) {
    case PROJECT_MOWING: return Field_done(g_world) >= 1.0f;
    case PROJECT_SNAILS: return Field_snailsLeft(g_world) == 0 || g_world->inspected;
    case PROJECT_BIG_TREE: return g_world->bigtree.step >= BT_FALLING;
    default:             return false; /* (Kerry: as long as there are logs) */
    }
}

const char *ai_jobStart(int project) {
    switch (project) {
    case PROJECT_MOWING:   return "Right - the Kartoffelacker. Where's my scythe?";
    case PROJECT_SNAILS:   return "Snail time! Gently, everybody - they're protected.";
    case PROJECT_WOOD_ART: return "Time to make some art. Chainsaw, log, go!";
    case PROJECT_BIG_TREE: return "Right - the big tree. Let's have a look at it.";
    default:               return "Off to work.";
    }
}