Nimbin[12]?Games / Entropie / entropie/src/people/ai_work.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_work.c 65 KB · 1378 lines raw
/*
 * Work: firewood, trees, trash and the fire, on an order or on their own,
 * and whether someone feels like it at all.
 */

#include "people/ai_internal.h"
#include "people/pockets.h"
#include "world/world.h"

/* ---------- Wood work ---------- */

#define LIGHTER_ASK 30.0f /* m: they go and borrow a lighter from somebody this near */
#define UNEVEN_CUT 0.2f /* m a piece may come out longer or shorter, cut by someone unpractised */

/* ---------- Felling room ---------- */
/* A tree falls away from whoever fells it. Before that: which way is there
   room for it (no house, wall, car, tent, table, fire in the way, from the
   stump out to its top and a crown wide)? The feller stands on the other side.
   Somebody in the way when it's nearly through: "Timber!" and they get out. */
#define FELL_DIRS    12    /* ways round it looked at */
#define FELL_STEP    1.0f  /* m between the spots looked at along the way it falls */
#define FELL_WARN    0.6f  /* the cut this far through: anybody in the way is warned off */
#define FELL_CLEAR   1.2f  /* m past the crown's side: out of the way */

static float treeTall(const Object *tree) {
    int top = tree->collider_count > 1 ? 1 : 0;
    return Object_colliderCenter(tree, top).y + tree->colliders[top].half_extents.y - tree->position.y;
}

static float crownR(const Object *tree) { return fmaxf(tree->size.x * 0.5f, 0.6f); }

static bool notInTheWay(const Object *o, const void *tree) {
    if (o == tree || o->is_character || o->type == OBJ_DOG || !o->is_static) return true;
    switch (o->type) { /* (what a tree mustn't come down on) */
    case OBJ_HOUSE: case OBJ_BARN: case OBJ_STRUCTURE: case OBJ_WALL: case OBJ_CAR: case OBJ_TENT:
    case OBJ_FURNITURE: case OBJ_FIREPLACE: case OBJ_WOODPILE: return false;
    default: return true;
    }
}

/* What it would come down on, falling that way (counted; the first in *first). */
static int fallHits(const Object *tree, Vec3 dir, Object **first) {
    if (first) *first = NULL;
    if (!g_world) return 0;
    Vec3 side = vec3(-dir.z, 0.0f, dir.x);
    float tall = treeTall(tree), r = crownR(tree);
    int hits = 0;
    for (float along = 1.0f; along <= tall + 0.5f; along += FELL_STEP)
        for (int s = -1; s <= 1; s++) {
            float w = (along > tall * 0.45f ? r : 0.4f) * (float)s * 0.8f; /* (the trunk narrow, the crown wide) */
            Vec3 p = vec3_add(tree->position, vec3_add(vec3_scale(dir, along), vec3_scale(side, w)));
            p.y = tree->position.y + 14.0f;
            Object *hit = NULL;
            if (World_rayCast(g_world, p, vec3(0.0f, -1.0f, 0.0f), 14.5f, notInTheWay, tree, &hit, NULL) >= 0.0f) {
                hits++;
                if (first && !*first) *first = hit;
            }
        }
    return hits;
}

/* Which way it should fall: the clear way nearest to coming down away from
   `from` (where the feller is), worked out once. */
static Vec3 fellPlan(Object *tree, Vec3 from) {
    if (vec3_length(tree->fell_plan) > 0.5f) return tree->fell_plan;
    Vec3 want = vec3_scale(from, -1.0f), best = want;
    int best_hits = 1 << 20;
    for (int k = 0; k < FELL_DIRS; k++) { /* (nearest first: 0, +30, -30, +60 ...) */
        float a = (float)((k + 1) / 2) * (2.0f * PI_F / (float)FELL_DIRS) * (k % 2 ? 1.0f : -1.0f);
        Vec3 d = vec3(want.x * cosf(a) - want.z * sinf(a), 0.0f, want.x * sinf(a) + want.z * cosf(a));
        int hits = fallHits(tree, d, NULL);
        if (hits < best_hits) { best_hits = hits; best = d; }
        if (hits == 0) break;
    }
    tree->fell_plan = best;
    tree->fell_blocked = best_hits > 0;
    return best;
}

const Object *Ai_fallsOnto(const Object *tree, Vec3 dir) {
    Object *first = NULL;
    dir = vec3_normalize(vec3(dir.x, 0.0f, dir.z));
    fallHits(tree, dir, &first);
    return first;
}

/* Anybody standing where it'll come down: warned off (they step out sideways). */
bool Ai_warnFellingRoom(Object *self, Object *const *objects, int count, Object *tree, Vec3 dir) {
    dir = vec3_normalize(vec3(dir.x, 0.0f, dir.z));
    Vec3 side = vec3(-dir.z, 0.0f, dir.x);
    float tall = treeTall(tree) + 1.5f, r = crownR(tree) + 0.6f;
    bool told = false, anybody = false;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o == self || !o->is_character || o->type == OBJ_DOG) continue;
        Vec3 rel = flat(vec3_sub(o->position, tree->position));
        float along = vec3_dot(rel, dir), across = vec3_dot(rel, side);
        if (along < -0.5f || along > tall || fabsf(across) > r) continue;
        anybody = true;
        if (!told && self->type != OBJ_PLAYER && self->ai.speech_time <= 0.0f) {
            Ai_say(self, o->type == OBJ_PLAYER ? "Timber! Get out of the way - it's coming down there!" : "Timber! Out of the way!");
            told = true;
        }
        if (o->type == OBJ_HUMAN && o->ai.dodge_t <= 0.0f) { /* (out sideways, past the crown) */
            float out = across < 0.0f ? -1.0f : 1.0f;
            o->ai.dodge_to = vec3_add(o->position, vec3_scale(side, out * (r - fabsf(across) + FELL_CLEAR)));
            o->ai.dodge_t = 8.0f;
        }
    }
    return anybody;
}

/* Where to stand for a job on o, and what to face. */
WoodTask ai_taskFor(const Object *self, Object *o, Job job) {
    WoodTask t = { .job = job, .target = o };
    if (job == JOB_FELL) {
        Vec3 c = Object_colliderCenter(o, 0);
        Vec3 from = flat(vec3_sub(self->position, c));
        from = vec3_length(from) > 1e-3f ? vec3_normalize(from) : vec3(1.0f, 0.0f, 0.0f);
        from = vec3_scale(fellPlan(o, from), -1.0f); /* (on the far side from where it'll fall) */
        t.spot = vec3_add(c, vec3_scale(from, o->colliders[0].half_extents.x + STAND_OFF));
        t.aim = c;
    } else if (job == JOB_DELIMB) {
        int crown = o->collider_count > 1 ? 1 : 0;
        Vec3 near = closestOn(o, crown, self->position);
        Vec3 out = flat(vec3_sub(self->position, near));
        out = vec3_length(out) > 1e-3f ? vec3_normalize(out) : vec3(1.0f, 0.0f, 0.0f);
        t.spot = vec3_add(near, vec3_scale(out, 0.6f));
        t.aim = near;
    } else {
        /* Beside the log or piece, facing the spot to cut or split */
        Vec3 a, b;
        Tools_logEnds(o, &a, &b);
        Vec3 axis = vec3_sub(b, a);
        float length = vec3_length(axis);
        axis = vec3_scale(axis, 1.0f / fmaxf(length, 1e-4f));
        Vec3 u = flat(axis);
        u = vec3_length(u) > 1e-3f ? vec3_normalize(u) : vec3(1.0f, 0.0f, 0.0f);
        Vec3 v = vec3_normalize(vec3_cross(u, UP));
        float along = job == JOB_CUT ? Tools_cutPoint(o, self->position) : length * 0.5f;
        if (job == JOB_CUT) { /* the practised cut even pieces; the others by eye, some longer, some shorter */
            unsigned h = (unsigned)(int)(o->position.x * 731.0f) * 2654435761u ^ (unsigned)(int)(o->position.z * 557.0f) * 40503u;
            float r = (float)((h >> 8) & 1023u) / 1023.0f * 2.0f - 1.0f;
            along += r * UNEVEN_CUT * (1.0f - self->skill[ORDER_FIREWOOD]);
            along = clampf(along, 0.3f, length - 0.3f);
        }
        Vec3 point = vec3_add(a, vec3_scale(axis, along));
        float side = vec3_dot(vec3_sub(self->position, point), v) < 0.0f ? -1.0f : 1.0f;
        float reach = o->colliders[0].half_extents.x;
        t.spot = vec3_add(point, vec3_scale(v, side * (reach + STAND_OFF)));
        t.aim = point;
    }
    t.spot.y = self->position.y;
    return t;
}

/* Holds a fitting tool already, or one lies around. */
static bool canGetTool(const Object *self, Object *const *objects, int count, Job job) {
    return Tools_canDo(Tools_held(self, NULL), job) || ai_findTool(self, objects, count, job) != NULL;
}

/*
 * The next thing to do with the wood around area, in the order that makes
 * firewood: split what is short, cut what is long, take branches off fallen
 * trees, and (if asked to) fell more trees.
 */
bool ai_nextWoodTask(Object *self, Object *const *objects, int count, Vec3 area, bool trees, WoodTask *out) {
    Ai *ai = &self->ai;
    Object *best = NULL;
    Job best_job = JOB_NONE;
    int best_rank = 99;
    float best_dist = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (ai_skipped(ai, o) || o->is_held || o->broken || o->collider_count == 0) continue;
        float from_area = flatDistance(Object_colliderCenter(o, 0), area);
        Job job = JOB_NONE;
        int rank = 0;
        if (Tools_splittable(o) && from_area < WOOD_RADIUS) { job = JOB_SPLIT; rank = 0; }
        else if (Tools_cuttable(o) && from_area < WOOD_RADIUS) { job = JOB_CUT; rank = 1; }
        else if (o->type == OBJ_TREE && o->toppled && o->topple >= 1.0f && o->branches > 0 && from_area < WOOD_RADIUS) { job = JOB_DELIMB; rank = 2; }
        else if (trees && o->type == OBJ_TREE && !o->toppled && !o->variant && !o->keep && from_area < TREE_RADIUS) { job = JOB_FELL; rank = 3; } /* never the apple trees (or the ones that stay) */
        if (job == JOB_NONE) continue;
        if ((job == JOB_SPLIT || job == JOB_CUT) && claimed(self, objects, count, o)) continue;
        if (!canGetTool(self, objects, count, job)) continue; /* (the axe may be in someone else's hands) */
        float d = flatDistance(Object_colliderCenter(o, 0), self->position);
        if (rank < best_rank || (rank == best_rank && d < best_dist)) {
            best = o;
            best_job = job;
            best_rank = rank;
            best_dist = d;
        }
    }
    if (!best) return false;
    *out = ai_taskFor(self, best, best_job);
    return true;
}

/* ---------- Tools they shouldn't use ---------- */

#define SAW_PRACTICE  0.4f   /* tree skill below this: the chainsaw is too much for them */
#define AXE_DRUNK     0.6f   /* this drunk: hands off the axe and the saw */
#define CLOSE_CALL    8.0f   /* about this many seconds of work between close calls with one */

bool Ai_shouldntUse(const Object *self, int tool) {
    if (tool == TOOL_CHAINSAW && self->skill[ORDER_TREES] < SAW_PRACTICE) return true;
    if ((tool == TOOL_AXE || tool == TOOL_CHAINSAW) && self->drunk > AXE_DRUNK) return true;
    return false;
}

bool Ai_mayUse(const Object *self, int tool) {
    unsigned bit = 1u << tool;
    return !(self->ai.banned & bit) || (self->ai.defies & bit);
}

/* How likely they ignore being told: proud, unwilling, drunk. */
static float ignorance(const Object *self) {
    return clampf(0.1f + 0.45f * self->temperament.pride + 0.5f * (0.5f - self->ai.character.willing)
                  + 0.4f * fminf(self->drunk, 1.0f), 0.0f, 0.9f);
}

bool Ai_banTool(Object *self, int tool, bool ban) {
    unsigned bit = 1u << tool;
    Ai *ai = &self->ai;
    if (!ban) {
        ai->banned &= (unsigned char)~bit;
        ai->defies &= (unsigned char)~bit;
        return true;
    }
    ai->banned |= (unsigned char)bit;
    if (random01(&self->ai) < ignorance(self)) { /* doesn't listen */
        ai->defies |= (unsigned char)bit;
        return false;
    }
    ai->defies &= (unsigned char)~bit;
    for (int h = 0; h < 2; h++) /* put it down right here */
        if (self->hands.state[h] == HAND_HOLDING && toolOf(self->hands.item[h]) == (ToolKind)tool) Hands_drop(self, h);
    if (ai->tool && toolOf(ai->tool) == (ToolKind)tool) ai->tool = NULL;
    return true;
}

/* Working with a tool they shouldn't use: a word about it first, and now
   and then a close call (it kicks back, the axe glances off): a stagger. */
static void riskyTool(Object *self, ToolKind held, float dt) {
    Ai *ai = &self->ai;
    if (!Ai_shouldntUse(self, held)) return;
    unsigned bit = 1u << held;
    if (!(ai->warned & bit)) {
        ai->warned |= (unsigned char)bit;
        ai->risky = CLOSE_CALL * (0.5f + random01(&self->ai));
        Ai_say(self, self->drunk > AXE_DRUNK ? "I'm fine, I'm fine! Watch this..."
                     : held == TOOL_CHAINSAW ? "Never used one of these... how hard can it be?" : "Here goes nothing.");
        return;
    }
    if (!Tools_isWorking(self) || (ai->risky -= dt) > 0.0f) return;
    ai->risky = CLOSE_CALL * (0.5f + random01(&self->ai));
    Body_stagger(&self->body, vec3_scale(Object_facing(self), -0.8f));
    Ai_say(self, held == TOOL_CHAINSAW ? "Whoa! It kicked back at me!" : "Whoa, that one glanced off!");
}

/* Holds a tool that can do the job, fetching one if needed.
   0 = ready, 1 = busy getting it, -1 = there is none. */
static int getTool(Object *self, Object *const *objects, int count, Job job, float dt) {
    Ai *ai = &self->ai;
    int hand;
    ToolKind held = Tools_held(self, &hand);
    if (held >= TOOL_AXE && !Ai_mayUse(self, held) && hand >= 0) { /* told to leave that one: down it goes */
        Hands_drop(self, hand);
        return 1;
    }
    bool saw_ok = held != TOOL_CHAINSAW || (hand >= 0 && self->hands.state[1 - hand] == HAND_FREE);
    if (Tools_canDo(held, job) && saw_ok) return 0;
    if (held == TOOL_CHAINSAW && Tools_canDo(held, job)) { /* both hands for the saw */
        freeHands(self, self->hands.item[hand]);
        return 1;
    }

    Object *tool = ai->tool && !ai->tool->is_held && Tools_canDo(toolOf(ai->tool), job)
                 ? ai->tool : ai_findTool(self, objects, count, job);
    ai->tool = tool;
    if (!tool) return -1;
    if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) {
        stand(self);
        return 1;
    }
    if (Hands_gapFrom(self->position, self->look.height, tool) < PICKUP_REACH) {
        stand(self);
        freeHands(self, NULL);
        turnTowards(self, vec3_sub(tool->position, self->position), dt);
        Hands_pickUpObject(self, self->main_hand, tool);
        return 1;
    }
    Vec3 dir = ai_steer(self, tool, objects, count, tool->position);
    if (vec3_length(dir) < 0.5f) { stand(self); return 1; }
    ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
    return 1;
}

/* Walks to the spot, faces the wood and works it. Gives up on it if it doesn't work out. */
void ai_workOn(Object *self, Object *const *objects, int count, const WoodTask *t, float dt) {
    Ai *ai = &self->ai;
    if (ai->job_target != t->target) {
        ai->job_target = t->target;
        ai->job_time = 0.0f;
        ai->closest = 1e9f;
        ai->stuck = 0.0f;
        ai->timer = 0.0f;
    }
    ai->job_time += dt;
    if (ai->job_time > JOB_PATIENCE) { skipTarget(ai, t->target); return; }

    int tool = getTool(self, objects, count, t->job, dt);
    if (tool < 0) { skipTarget(ai, t->target); stand(self); return; }
    if (tool > 0) return;
    riskyTool(self, Tools_held(self, NULL), dt);
    if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return; }

    float dist = flatDistance(t->spot, self->position);
    if (dist > WORK_ARRIVE + (ai->timer > 0.5f ? 0.45f : 0.0f)) { /* (edging closer: may leave the spot) */
        Vec3 dir = ai_steerPast(self, t->target, NULL, objects, count, t->spot);
        if (vec3_length(dir) < 0.5f || isStuck(ai, dist, dt * 0.5f)) {
            stand(self);
            if (ai->stuck > STUCK_TIME * 4.0f) skipTarget(ai, t->target);
            return;
        }
        ai_walk(self, dir, fminf(WALK_SPEED, dist * 2.0f + 0.3f), dt);
        return;
    }
    stand(self);
    Vec3 to = flat(vec3_sub(t->aim, self->position));
    turnTowards(self, to, dt * 2.0f);
    Vec3 eye = vec3_add(self->position, vec3(0.0f, self->look.height * 0.93f, 0.0f));
    self->body.look_dir = vec3_normalize(vec3_sub(t->aim, eye));
    self->body.look_weight = 1.0f;
    if (vec3_length(to) > 1e-3f && vec3_dot(Object_facing(self), vec3_normalize(to)) < 0.93f) return;
    if (Tools_isWorking(self)) return;
    if (!Tools_hasJob(self, objects, count)) { /* can't get at it from here: edge closer, then give up */
        ai->timer += dt;
        if (ai->timer > 3.0f) skipTarget(ai, t->target);
        else if (ai->timer > 0.5f) ai_walk(self, vec3_normalize(to), 0.35f, dt);
        return;
    }
    ai->timer = 0.0f;
    Tools_use(self, objects, count);
}

/* A quarter of firewood lying around the work area, not yet on the pile. */
static Object *loosePiece(const Object *self, Object *const *objects, int count, Vec3 area) {
    Object *best = NULL;
    float best_dist = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_FIREWOOD || o->size.z < 4.0f || o->is_held || o->broken) continue;
        bool to_move = false; /* on an old woodpile: goes over to the new one */
        if (o->stored && self->ai.woodpile) {
            for (int k = 0; k < count && !to_move; k++)
                to_move = objects[k]->type == OBJ_WOODPILE && objects[k]->moved_to == self->ai.woodpile &&
                          flatDistance(objects[k]->position, o->position) < 1.5f;
        }
        if (!to_move && flatDistance(o->position, area) > WOOD_RADIUS) continue;
        if (o->stored && !to_move) continue; /* already on a woodpile */
        if (claimed(self, objects, count, o)) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_dist) {
            best_dist = d;
            best = o;
        }
    }
    return best;
}

static int firewoodInHands(const Object *self) {
    int n = 0;
    for (int i = 0; i < 2; i++)
        if (self->hands.state[i] == HAND_HOLDING && self->hands.item[i]->type == OBJ_FIREWOOD) n++;
    return n;
}

/* Two pieces at a time to the woodpile (the tool stays where the work was). */
static bool carryFirewood(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *pile = ai->woodpile;
    if (!pile) return false;
    if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; }

    int carried = firewoodInHands(self);
    Object *next = carried < 2 ? loosePiece(self, objects, count, ai->work_area) : NULL;
    if (next && (carried == 0 || flatDistance(next->position, self->position) < 3.0f)) {
        ToolKind held = Tools_held(self, NULL);
        if (held == TOOL_AXE || held == TOOL_CHAINSAW || held == TOOL_SHEARS) { /* hands free for carrying */
            for (int hand = 0; hand < 2; hand++)
                if (self->hands.state[hand] == HAND_HOLDING && self->hands.item[hand]->type != OBJ_FIREWOOD) {
                    ai->tool = self->hands.item[hand];
                    Hands_drop(self, hand);
                }
            stand(self);
            return true;
        }
        ai->job_target = next;
        if (Hands_gapFrom(self->position, self->look.height, next) < PICKUP_REACH) {
            stand(self);
            turnTowards(self, vec3_sub(next->position, self->position), dt);
            int hand = Hands_freeHand(self);
            if (hand >= 0) Hands_pickUpObject(self, hand, next);
            return true;
        }
        Vec3 dir = ai_steer(self, next, objects, count, next->position);
        if (vec3_length(dir) < 0.5f) { stand(self); return true; }
        ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
        return true;
    }
    if (carried == 0) return false;

    /* To the pile and stack them */
    ai->job_target = NULL;
    float dist = flatDistance(pile->position, self->position);
    if (dist > pile->size.z * 0.5f + 0.7f) {
        Vec3 dir = ai_steer(self, pile, objects, count, pile->position);
        if (vec3_length(dir) < 0.5f) { stand(self); return true; }
        ai_walk(self, dir, WALK_SPEED * 0.75f, dt);
        return true;
    }
    stand(self);
    turnTowards(self, vec3_sub(pile->position, self->position), dt);
    for (int hand = 0; hand < 2; hand++) {
        Object *piece = self->hands.item[hand];
        if (self->hands.state[hand] != HAND_HOLDING || !piece || piece->type != OBJ_FIREWOOD) continue;
        Hands_drop(self, hand);
        ai_pileSlid(self, Object_stackFirewood(pile, piece, objects, count, ai_stackCare(self)));
    }
    return true;
}

/* ---------- Tidying up: branches and twigs onto a heap by the woodpile ---------- */

#define TIDY_CARE   0.6f  /* this careful (ai_stackCare): branches lying about go onto a heap */
#define TIDY_HEAVY  3.0f  /* (kg; heavier ones stay where they are) */

/* Where the branches go: a heap just off the end of the woodpile. */
static Vec3 brushHeap(const Object *pile) {
    Vec3 along = pile->colliders[0].axes[0];
    return vec3_add(pile->position, vec3_scale(along, pile->size.x * 0.5f + 0.8f));
}

static int branchesInHands(const Object *self) {
    int n = 0;
    for (int i = 0; i < 2; i++)
        if (self->hands.state[i] == HAND_HOLDING && self->hands.item[i]->type == OBJ_BRANCH) n++;
    return n;
}

/* A branch or twig lying about the work area (not on the heap yet). */
static Object *looseBranch(const Object *self, Object *const *objects, int count, Vec3 area, Vec3 heap) {
    Object *best = NULL;
    float best_dist = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_BRANCH || o->stored || o->is_held || o->broken || o->weight > TIDY_HEAVY) continue;
        if (flatDistance(o->position, area) > WOOD_RADIUS || flatDistance(o->position, heap) < 1.0f) continue;
        if (claimed(self, objects, count, o)) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_dist) { best_dist = d; best = o; }
    }
    return best;
}

/* Careful people don't leave the branches lying about: two at a time onto the heap. */
static bool tidyBranches(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *pile = ai->woodpile;
    if (!pile || ai_stackCare(self) < TIDY_CARE) return false;
    if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; }
    const Vec3 heap = brushHeap(pile);
    int carried = branchesInHands(self);
    Object *next = carried < 2 ? looseBranch(self, objects, count, ai->work_area, heap) : NULL;
    if (next && (carried == 0 || flatDistance(next->position, self->position) < 3.0f)) {
        for (int hand = 0; hand < 2; hand++) /* (hands free for it: the tool stays here) */
            if (self->hands.state[hand] == HAND_HOLDING && self->hands.item[hand]->type != OBJ_BRANCH) {
                if (toolOf(self->hands.item[hand]) != TOOL_NONE) ai->tool = self->hands.item[hand];
                Hands_drop(self, hand);
                stand(self);
                return true;
            }
        ai->job_target = next;
        if (Hands_gapFrom(self->position, self->look.height, next) < PICKUP_REACH) {
            stand(self);
            turnTowards(self, vec3_sub(next->position, self->position), dt);
            int hand = Hands_freeHand(self);
            if (hand >= 0) Hands_pickUpObject(self, hand, next);
            return true;
        }
        Vec3 dir = ai_steer(self, next, objects, count, next->position);
        if (vec3_length(dir) < 0.5f) { stand(self); return true; }
        ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
        return true;
    }
    if (carried == 0) return false;
    ai->job_target = NULL;
    Vec3 to = vec3_sub(heap, self->position);
    to.y = 0.0f;
    if (vec3_length(to) > 0.5f) {
        Vec3 dir = ai_steer(self, NULL, objects, count, heap);
        if (vec3_length(dir) < 0.5f) { stand(self); return true; }
        ai_walk(self, dir, WALK_SPEED * 0.75f, dt);
        return true;
    }
    stand(self);
    turnTowards(self, to, dt);
    for (int hand = 0; hand < 2; hand++) {
        Object *b = self->hands.item[hand];
        if (self->hands.state[hand] != HAND_HOLDING || !b || b->type != OBJ_BRANCH) continue;
        Hands_drop(self, hand);
        b->stored = 1; /* (on the heap) */
    }
    return true;
}

/* Skilled, sure-handed, sober and unhurried: neat, stable piles. The others
   just put it on (and a high loose pile comes down now and then). */
float ai_stackCare(const Object *self) {
    const Character *c = &self->ai.character;
    float care = 0.3f + 0.55f * self->skill[ORDER_FIREWOOD] + 0.1f * c->likes[ORDER_FIREWOOD]
               - 0.5f * (Body_clumsiness(&self->body) - 0.5f) - 0.3f * (c->tempo - 1.0f)
               - 0.6f * fminf(self->drunk, 1.0f) - 0.1f * self->temperament.nerves + 0.05f * self->temperament.pride
               - 0.2f * (1.0f - self->needs.stamina);
    return clampf(care, 0.0f, 1.0f);
}

void ai_pileSlid(Object *self, int slid) {
    if (slid <= 0) return;
    Ai_say(self, slid > 3 ? "Whoa - there goes the pile!" : "Oops... that came down.");
}

/* ---------- Other jobs: trash and the fire ---------- */

#define PICK_TIME       1.4f   /* bending down for a piece of trash (seconds, at a normal pace) */
#define TRASH_AREA     45.0f   /* how far from where they were asked they look for trash */
#define FIRE_WANTED     6.0f   /* kg of wood a looked-after fire should have */
#define WOOD_SEARCH    60.0f   /* how far they go for wood for the fire */
#define BAG_PIECES     24      /* see Object_fillBag */

static float g_hour = 12.0f;

void Ai_setHour(float hour) { g_hour = hour; }

static bool evening(void) { return g_hour >= 18.0f || g_hour < 3.0f; }

#define SUN_SHIRT_OFF 90.0f  /* s of hard work in the sun before the shirt comes off ... */
#define SUN_SHIRT_ON  40.0f  /* ... and on again when it's cooled down to this */

void ai_sunShirt(Object *self, float dt) {
    Ai *ai = &self->ai;
    bool sunny = g_hour >= 11.0f && g_hour < 17.5f;
    bool hard = (ai->mode == AI_WORK && (ai->order == ORDER_FIREWOOD || ai->order == ORDER_TREES)) ||
                (ai->mode == AI_HELP && Tools_held(self, NULL) >= TOOL_AXE);
    float warm = sunny && hard ? 1.0f + 0.5f * (1.0f - self->needs.stamina) : sunny ? -0.5f : -3.0f;
    ai->sun_heat = clampf(ai->sun_heat + warm * dt, 0.0f, SUN_SHIRT_OFF * 1.5f);
    if (self->look.top == TOP_SHIRT && ai->sun_heat >= SUN_SHIRT_OFF && self->temperament.pride > -0.3f && !self->look.keeps_shirt) {
        self->look.top = self->female ? TOP_CROP : TOP_BARE;
        if (ai->speech_time <= 0.0f) Ai_say(self, "Phew, it's hot. Off with the shirt.");
    } else if ((self->look.top == TOP_BARE || self->look.top == TOP_CROP) && ai->sun_heat < SUN_SHIRT_ON) {
        self->look.top = TOP_SHIRT;
    }
}


Object *ai_nearestOf(const Object *self, Object *const *objects, int count, ObjectType type, ObjectType dispenses) {
    Object *best = NULL;
    float best_d = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != type || (type == OBJ_SUPPLY && o->dispenses != dispenses)) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) {
            best_d = d;
            best = o;
        }
    }
    return best;
}

static Object *nearestTrash(const Object *self, Object *const *objects, int count, Vec3 area) {
    Object *best = NULL;
    float best_d = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (!Object_isTrash(o) || o->is_held || flatDistance(o->position, area) > TRASH_AREA) continue;
        if (ai_skipped(&self->ai, o)) continue; /* couldn't get to it */
        float d = flatDistance(o->position, self->position);
        if (d < best_d && !claimed(self, objects, count, o)) {
            best_d = d;
            best = o;
        }
    }
    return best;
}

static int trashAround(Object *const *objects, int count, Vec3 area) {
    int n = 0;
    for (int i = 0; i < count; i++)
        if (Object_isTrash(objects[i]) && !objects[i]->is_held && flatDistance(objects[i]->position, area) < TRASH_AREA) n++;
    return n;
}

/* Firewood quarters there are, not yet burning. */
static int firewoodStock(Object *const *objects, int count) {
    int n = 0;
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (o->type == OBJ_FIREWOOD && o->mesh.vao && o->charred <= 0.0f) n++;
    }
    return n;
}

/* Wood for the fire: firewood first (the woodpile too), else branches. */
static Object *woodForFire(const Object *self, Object *const *objects, int count, const Object *fire) {
    Object *best = NULL;
    float best_d = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (!Object_isFuel(o) || o->is_held || o->lifted || o->weight > 10.0f) continue;
        if (flatDistance(o->position, fire->position) > WOOD_SEARCH) continue;
        float d = flatDistance(o->position, self->position) + (o->type == OBJ_FIREWOOD ? 0.0f : 15.0f);
        if (d < best_d && !inAnyFire(objects, count, o) && !claimed(self, objects, count, o)) {
            best_d = d;
            best = o;
        }
    }
    return best;
}

/* Collecting trash: a bag from the box, every piece into it, full bags to the container. */
static void workTrash(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *bag = Camp_bagInHand(self);
    if (!bag) {
        Object *box = ai_nearestOf(self, objects, count, OBJ_SUPPLY, OBJ_BAG);
        if (!box) { Ai_say(self, "There are no garbage bags anywhere."); Ai_stopWork(self); return; }
        if (goTo(self, box, objects, count, 0.8f, dt)) {
            freeHands(self, NULL);
            ai->wants_bag = true;
        }
        return;
    }
    freeHands(self, bag); /* nothing else in the hands */
    Object *trash = bag->stored < BAG_PIECES ? nearestTrash(self, objects, count, ai->work_area) : NULL;
    if (!trash) {
        if (bag->stored == 0) { /* nothing left: put the empty bag down */
            Ai_say(self, "All clean!");
            for (int h = 0; h < 2; h++) if (self->hands.item[h] == bag) Hands_drop(self, h);
            Ai_stopWork(self);
            return;
        }
        Object *bin = ai_nearestOf(self, objects, count, OBJ_BIN, OBJ_BOX);
        if (!bin) { Ai_say(self, "Where does the trash go?"); Ai_stopWork(self); return; }
        if (goTo(self, bin, objects, count, 0.9f, dt)) {
            Camp_throwAway(self);
            ai->job_target = NULL;
            if (!nearestTrash(self, objects, count, ai->work_area)) { Ai_say(self, "All clean!"); Ai_stopWork(self); }
        }
        return;
    }
    if (ai->job_target != trash) { ai->job_time = 0.0f; ai->stuck = 0.0f; }
    ai->job_target = trash;
    if (flatDistance(trash->position, self->position) > 0.95f) {
        Vec3 dir = ai_steer(self, trash, objects, count, trash->position);
        if (vec3_length(dir) < 0.5f) {
            stand(self);
            if ((ai->stuck += dt) > 3.0f) skipTarget(ai, trash); /* can't get there: another one */
        } else {
            ai_walk(self, dir, WALK_SPEED * 0.7f, dt);
        }
        return;
    }
    stand(self);
    turnTowards(self, vec3_sub(trash->position, self->position), dt);
    Hands_touch(self, trash->position); /* bends down for it */
    ai->job_time += dt * self->work_rate;
    if (ai->job_time >= PICK_TIME) {
        Camp_bag(bag, trash);
        Ai_practice(self, ORDER_TRASH, 0.01f);
        ai->job_target = NULL;
        ai->job_time = 0.0f;
        self->needs.stamina = fmaxf(0.0f, self->needs.stamina - 0.06f); /* bending down again and again */
    }
}

/* Looking after the fire: wood in, lit when it gets dark, then sit by it. */
static void workFire(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *fire = Camp_fireNear(objects, count, ai->work_area, 80.0f, false);
    if (!fire) { Ai_say(self, "Where's the fireplace?"); Ai_stopWork(self); return; }
    const float near = fire->size.x * 0.5f + 0.9f;

    /* Wood in the hands goes into the fire */
    for (int h = 0; h < 2; h++) {
        Object *held = self->hands.item[h];
        if (self->hands.state[h] == HAND_HOLDING && held && Object_isFuel(held)) {
            if (flatDistance(fire->position, self->position) > near) {
                Vec3 dir = ai_steer(self, fire, objects, count, fire->position);
                if (vec3_length(dir) < 0.5f) stand(self);
                else ai_walk(self, dir, WALK_SPEED * 0.75f, dt);
            } else {
                stand(self);
                turnTowards(self, vec3_sub(fire->position, self->position), dt);
                if (Camp_feed(self, fire)) Ai_practice(self, ORDER_FIRE, 0.015f);
            }
            return;
        }
        if (self->hands.state[h] == HAND_REACHING) { stand(self); return; }
    }
    if (fire->fuel < FIRE_WANTED) {
        Object *wood = woodForFire(self, objects, count, fire);
        if (wood) {
            ai->job_target = wood;
            freeHands(self, NULL);
            if (goTo(self, wood, objects, count, PICKUP_REACH, dt)) {
                int hand = Hands_freeHand(self);
                if (hand >= 0) Hands_pickUpObject(self, hand, wood);
            }
            return;
        }
        if (fire->fuel <= 0.0f) {
            Ai_say(self, "There's no more wood for the fire. Someone should make firewood.");
            Ai_stopWork(self);
            return;
        }
    }
    ai->job_target = NULL;
    if (!fire->burning && evening()) {
        if (!self->lighter || self->lighter_uses <= 0) { /* a lighter first: who's got one? */
            Object *lender = Pockets_lighterNear(self, objects, count, LIGHTER_ASK);
            if (!lender) {
                if (ai->lighter_ask <= 0.0f) {
                    Ai_say(self, self->lighter ? "My lighter's empty. Has anyone got a light?" : "Has anyone got a lighter?");
                    ai->lighter_ask = 45.0f;
                }
                stand(self);
                return;
            }
            ai->job_target = lender;
            if (!goTo(self, lender, objects, count, 1.2f, dt)) return;
            ai->job_target = NULL;
            static char line[AI_SPEECH_MAX];
            if (lender->type == OBJ_PLAYER) snprintf(line, sizeof line, "I'll borrow your lighter - thanks!");
            else snprintf(line, sizeof line, "Lend me your lighter, %s? Thanks!", lender->name);
            Ai_say(self, line);
            if (lender->type == OBJ_HUMAN && lender->ai.speech_time <= 0.0f) Ai_say(lender, "Sure, here.");
            if (self->lighter) { self->lighter = false; self->lighter_of = NULL; } /* (the empty one is thrown away) */
            Pockets_passLighter(lender, self);
            return; /* (back to the fire) */
        }
        if (flatDistance(fire->position, self->position) > near) {
            Vec3 dir = ai_steer(self, fire, objects, count, fire->position);
            if (vec3_length(dir) < 0.5f) stand(self);
            else ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
            return;
        }
        if (Pockets_light(self) && Camp_light(fire)) {
            Ai_say(self, self->lighter_uses == 0 ? "There we go! ...and that was the last of the lighter." : "There we go!");
            Ai_practice(self, ORDER_FIRE, 0.03f);
        }
    }
    /* Nothing to do: wait by the fire */
    float d = flatDistance(fire->position, self->position);
    if (d > FIRE_SIT_DISTANCE + 1.0f) {
        if (ai->goal.x == 0.0f || flatDistance(ai->goal, fire->position) > FIRE_SIT_DISTANCE + 0.5f) pickFireSpot(self, objects, count, fire);
        Vec3 dir = ai_steer(self, NULL, objects, count, ai->goal);
        if (vec3_length(dir) < 0.5f) stand(self);
        else ai_walk(self, dir, WALK_SPEED * 0.6f, dt);
        return;
    }
    stand(self);
    if (!self->sitting) Object_sit(self, Object_seatNear(objects, count, self->position, 1.2f), fire->position);
    if (self->sit_style != SIT_SEAT) turnTowards(self, vec3_sub(fire->position, self->position), dt * 0.5f);
}

/* ---------- Who they are: likes, needs, yes or no ---------- */

bool Ai_needed(const Object *self, Object *const *objects, int count, Order order) {
    switch (order) {
    case ORDER_FIREWOOD: return firewoodStock(objects, count) < 16;
    case ORDER_TREES: {
        if (firewoodStock(objects, count) >= 16) return false;
        for (int i = 0; i < count; i++) /* wood lying around comes first */
            if (Tools_cuttable(objects[i]) || Tools_splittable(objects[i]) ||
                (objects[i]->type == OBJ_TREE && objects[i]->toppled)) return false;
        return true;
    }
    case ORDER_TRASH: return trashAround(objects, count, self->position) > 3;
    case ORDER_FIRE: {
        if (!evening() && g_hour < 16.5f) return false;
        Object *fire = Camp_fireNear(objects, count, self->position, 80.0f, false);
        return fire && (!fire->burning || fire->fuel < 2.0f) && (fire->fuel > 0.0f || firewoodStock(objects, count) > 0);
    }
    default: return false;
    }
}

static const char *const DISLIKE[ORDER_COUNT] = {
    [ORDER_FIREWOOD] = "Chopping wood? Not really my thing.",
    [ORDER_TREES]    = "Felling trees? No, that's not for me.",
    [ORDER_TRASH]    = "Picking up other people's trash? No thanks.",
    [ORDER_FIRE]     = "Babysitting a fire? Nah.",
};
static const char *const NOT_NEEDED[ORDER_COUNT] = {
    [ORDER_FIREWOOD] = "We've got plenty of firewood already.",
    [ORDER_TREES]    = "We've got enough wood for now, haven't we?",
    [ORDER_TRASH]    = "It's pretty clean already.",
    [ORDER_FIRE]     = "Now? It's not even evening.",
};

bool Ai_willDo(Object *self, Object *const *objects, int count, Order order, bool their_idea, const char **reply) {
    ai_ensureReady(self);
    if (self->lives_here) { /* (Kurti and his sons: it's their land, your festival - your work) */
        *reply = random01(&self->ai) < 0.5f ? "Ha - it's your festival, lad. I'll watch." : "Not my job - we just lend you the land.";
        return false;
    }
    const Character *c = &self->ai.character;
    float like = c->likes[order], tired = 1.0f - self->needs.stamina, drunk = fminf(self->drunk, 1.0f);
    bool needed = Ai_needed(self, objects, count, order);
    float score = c->willing * 0.7f + like * 0.6f - tired * 0.5f - drunk * 0.8f - self->ai.annoyed * 0.8f + (random01(&self->ai) - 0.5f) * 0.2f;
    if (their_idea) score += 0.4f;
    if (!needed) score -= 0.3f;
    if (score >= 0.25f) {
        *reply = like > 0.45f ? "Oh yes, gladly!" : like < -0.3f ? "Hmm... okay, if I must." : "Sure, I can do that.";
        return true;
    }
    *reply = drunk > 0.5f ? "I'm in no shape for that..." : tired > 0.7f ? "I'm too worn out right now."
           : !needed ? NOT_NEEDED[order] : like < 0.0f ? DISLIKE[order] : "Not right now, sorry.";
    return false;
}

Order Ai_job(const Object *self) {
    const Ai *ai = &self->ai;
    if (self->type == OBJ_PLAYER) return self->work_state.job == JOB_FELL ? ORDER_TREES : ORDER_FIREWOOD;
    return ai->mode == AI_WORK ? ai->order : ai->mode == AI_HELP ? ORDER_FIREWOOD : ORDER_NONE;
}

float Ai_workRate(const Object *self) {
    Order job = Ai_job(self);
    if (job == ORDER_NONE) return 1.0f;
    float like = self->type == OBJ_HUMAN ? self->ai.character.likes[job] : 0.0f;
    return (1.0f + 0.35f * like) * (0.7f + 0.6f * self->skill[job]); /* practice makes it quicker */
}

void Ai_practice(Object *self, Order job, float amount) {
    if (job <= ORDER_NONE || job >= ORDER_COUNT) return;
    self->skill[job] = fminf(1.0f, self->skill[job] + amount * (1.0f - self->skill[job]));
}

bool Ai_call(Object *self, const Object *player) {
    ai_ensureReady(self);
    Ai *ai = &self->ai;
    if (Body_isBusy(&self->body) || self->on_fire > 0.0f || ai->talking || ai->mode == AI_FOLLOW) return false;
    if (ai->mode == AI_WORK && ai->character.willing < 0.4f && random01(ai) < 0.5f) {
        Ai_say(self, "In a minute!");
        return true;
    }
    Ai_say(self, ai->mode == AI_WORK ? "Coming!" : self->temperament.pride < -0.3f ? "Me? ...okay." : "Yes? Coming!");
    self->sitting = false;
    approach(self, TOPIC_NONE);
    ai->talk_cooldown = 0.0f;
    (void)player;
    return true;
}

/* ---------- On an order ---------- */

static Object *closestLoosePiece(const Object *self, Object *const *objects, int count) {
    return loosePiece(self, objects, count, self->ai.work_area);
}

#define TOOL_WAIT_RANGE 30.0f

/* The player has the axe we need: the bold ask for it (once), the others
   wait nearby until it's put down (then it's the nearest one to fetch). */
static bool waitForTool(Object *self, Object *const *objects, int count, Object *player, float dt) {
    Ai *ai = &self->ai;
    float d = flatDistance(player->position, self->position);
    if (!Tools_canDo(Tools_held(player, NULL), JOB_SPLIT) || d > TOOL_WAIT_RANGE) {
        ai->tool_waiting = false;
        return false;
    }
    bool bold = self->temperament.pride + 0.5f * self->temperament.energy > 0.05f;
    if (bold && !ai->tool_asked && !Body_isBusy(&player->body)) {
        ai->tool_asked = true;
        approach(self, TOPIC_TOOL);
        return true;
    }
    if (!ai->tool_waiting) {
        ai->tool_waiting = true;
        Ai_say(self, ai->tool_asked ? "I'll wait till you're done with it." : "I'll wait for the axe...");
    }
    Vec3 to = flat(vec3_sub(player->position, self->position));
    if (d > 3.5f) {
        Vec3 dir = ai_steer(self, player, objects, count, vec3_sub(player->position, vec3_scale(vec3_normalize(to), 2.5f)));
        if (vec3_length(dir) < 0.5f) stand(self);
        else ai_walk(self, dir, WALK_SPEED * 0.7f, dt);
    } else {
        stand(self);
        turnTowards(self, to, dt * 0.5f);
    }
    return true;
}

/* ---------- Their own way of working: thinking it over, breaks ---------- */

static const char *const CHAT_OPEN[] = { "Hey! How's it going?", "Taking a little break - you?", "Nice day for it, huh?",
                                         "Got a minute?" };
static const char *const CHAT_MORE[] = { "Did you see the size of that tree?", "I could do with a cold one later.",
                                         "This place is coming along.", "My arms will hurt tomorrow.",
                                         "Who brought the sausages?", "Fire tonight, right?" };
static const char *const CHAT_REPLY[] = { "Ha, yes.", "Not bad, not bad.", "Tell me about it.", "Mhm.", "Right?", "True!" };
#define PICK(list, ai) (list)[(int)(random01(ai) * (float)(sizeof(list) / sizeof((list)[0]))) % (int)(sizeof(list) / sizeof((list)[0]))]

#define CHAT_RANGE  30.0f /* they go this far for someone to chat with */
#define CHAT_CLOSE  1.7f  /* ... and stand this close */
#define CHAT_PAUSE  3.0f  /* seconds between lines */

/* Someone to chat with: the nearest person awake and not up a ladder. */
static Object *chatPartner(const Object *self, Object *const *objects, int count) {
    Object *best = NULL;
    float best_d = CHAT_RANGE;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o == self || !o->is_character || o->type == OBJ_DOG || Body_isDown(&o->body) || o->on_ladder || o->toilet) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) { best = o; best_d = d; }
    }
    return best;
}

#define BREAK_OWN     0.6f   /* their own kind of break this often, else another */
#define BREAK_LONG    0.3f   /* a long one this often (BREAK_LONGER x), else short (BREAK_SHORTER x) */
#define BREAK_LONGER  1.8f
#define BREAK_SHORTER 0.7f
#define COFFEE_RANGE  70.0f  /* m: a stove this near is worth a coffee */

static void nextBreak(Object *self) {
    Ai *ai = &self->ai;
    ai->streak = ai->character.break_every * (0.7f + 0.6f * random01(ai)) * (1.0f - 0.4f * self->worn); /* (worn out: sooner) */
}

static void endBreak(Object *self) {
    Ai *ai = &self->ai;
    ai->on_break = 0.0f;
    ai->chat_with = NULL;
    ai->break_at = NULL;
    ai->task_seen = NULL; /* (whoever thinks things over does so again) */
    self->sitting = false;
    nextBreak(self);
    Ai_say(self, random01(ai) < 0.5f ? "Right, back to it!" : "Okay, back to work.");
}

static Object *stoveNear(const Object *self, Object *const *objects, int count) {
    Object *best = NULL;
    float best_d = COFFEE_RANGE;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_FURNITURE || o->variant != FURN_STOVE) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) { best = o; best_d = d; }
    }
    return best;
}

/* Which break this time: mostly their own way, now and then a coffee, a walk, a chat or a sit. */
static BreakKind pickBreak(Object *self, Object *const *objects, int count) {
    Ai *ai = &self->ai;
    if (random01(ai) < BREAK_OWN) return ai->character.break_kind;
    static const BreakKind KINDS[] = { BREAK_SIT, BREAK_WALK, BREAK_CHAT, BREAK_COFFEE };
    BreakKind k = KINDS[(int)(random01(ai) * 4.0f) % 4];
    if (k == BREAK_COFFEE && !stoveNear(self, objects, count)) k = BREAK_SIT;
    if (k == BREAK_CHAT && !chatPartner(self, objects, count)) k = BREAK_SIT;
    return k;
}

/* Now and then a break, short or long, their way or another: sitting down,
   a little walk, a chat, a coffee at the stove. The tool goes down first. True while on it. */
/* A break: what they work with put down - side by side, square, a step back
   from where they worked (the orderly), or just dropped any old way. */
#define TIDY_TOOLS 0.5f /* this careful (ai_stackCare): put down neatly */

static void putToolsDown(Object *self) {
    Ai *ai = &self->ai;
    Hands_setAside(self, ai->set_aside);
    bool tidy = ai_stackCare(self) > TIDY_TOOLS;
    Vec3 f = Object_facing(self), side = vec3(-f.z, 0.0f, f.x);
    for (int k = 0; k < 2; k++) {
        Object *item = ai->set_aside[k];
        if (!item) continue;
        bool flat = item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER;
        if (tidy) {
            Vec3 at = vec3_add(self->position, vec3_add(vec3_scale(f, -0.9f), vec3_scale(side, k ? 0.35f : -0.35f)));
            item->rotation = flat ? (item->type == OBJ_LADDER ? vec3(PI_F * 0.5f, self->rotation.y + PI_F * 0.5f, 0.0f)
                                                              : vec3(0.0f, self->rotation.y, PI_F * 0.5f)) /* (on its side, the handle across) */
                                  : vec3(0.0f, self->rotation.y, 0.0f);
            Object_syncColliders(item);
            item->position = vec3(at.x, item->position.y, at.z);
        } else {
            float turn = (random01(ai) - 0.5f) * 2.5f;
            item->rotation.y += turn;
            item->position = vec3_add(item->position, vec3((random01(ai) - 0.5f) * 0.5f, 0.0f, (random01(ai) - 0.5f) * 0.5f));
        }
        item->velocity = vec3_zero();
        Object_syncColliders(item);
    }
}

static bool takeBreak(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    const Character *c = &ai->character;
    if (c->break_kind == BREAK_NONE || c->break_every <= 0.0f) return false;
    if (ai->on_break <= 0.0f) {
        if (ai->streak <= 0.0f) nextBreak(self);
        if ((ai->streak -= dt) > 0.0f || Tools_isWorking(self) || firewoodInHands(self) > 0) return false;
        ai->break_now = pickBreak(self, objects, count);
        bool long_one = random01(ai) < BREAK_LONG;
        ai->on_break = c->break_length * (long_one ? BREAK_LONGER : BREAK_SHORTER) * (0.8f + 0.4f * random01(ai)) * (1.0f + 0.5f * self->worn);
        ai->break_from = self->position;
        ai->break_back = false;
        ai->break_at = NULL;
        ai->break_sip = 0.0f;
        ai->chat_with = ai->break_now == BREAK_CHAT ? chatPartner(self, objects, count) : NULL;
        ai->chat_line = 0.0f;
        ai->chat_said = 0;
        if (self->hands.item[HAND_LEFT] || self->hands.item[HAND_RIGHT]) putToolsDown(self); /* (taken again after) */
        if (self->worn > 0.45f && random01(ai) < 0.5f) Ai_say(self, "Ugh, I'm still beat from yesterday.");
        else if (ai->break_now == BREAK_WALK) Ai_say(self, long_one ? "I need some air. Back in a bit." : "I'll stretch my legs for a bit.");
        else if (ai->break_now == BREAK_COFFEE) Ai_say(self, "Coffee time.");
        else if (!ai->chat_with) Ai_say(self, long_one ? "I need a proper break." : "Time for a little sit-down.");
        if (ai->break_now == BREAK_WALK) {
            float a = random01(ai) * 2.0f * PI_F, d = (long_one ? 12.0f : 7.0f) + 5.0f * random01(ai);
            setGoal(ai, vec3_add(self->position, vec3(cosf(a) * d, 0.0f, sinf(a) * d)));
        } else if (ai->break_now == BREAK_COFFEE) {
            ai->break_at = stoveNear(self, objects, count);
            if (ai->break_at) setGoal(ai, ai->break_at->position);
            else ai->break_now = BREAK_SIT;
        }
    }
    ai->on_break -= dt;

    if (ai->break_now == BREAK_COFFEE && ai->break_at) { /* over to the stove, a coffee there, and back */
        Object *stove = ai->break_at;
        Vec3 front = vec3_add(stove->position, vec3_scale(stove->colliders[0].axes[2], stove->size.z * 0.5f + 0.5f));
        Vec3 goal = ai->break_back ? ai->break_from : front;
        float d = flatDistance(goal, self->position);
        if (!ai->break_back && d < 0.6f) { /* there: drinking it, a sip now and then */
            stand(self);
            turnTowards(self, vec3_sub(stove->position, self->position), dt);
            if ((ai->break_sip -= dt) <= 0.0f) {
                ai->break_sip = 6.0f + 6.0f * random01(ai);
                if (ai->speech_time <= 0.0f && random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "Ahh. Good coffee." : "Mm.");
            }
            if (ai->on_break <= 0.0f) { ai->break_back = true; setGoal(ai, ai->break_from); }
            return true;
        }
        if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) { endBreak(self); return false; }
        if (!ai->break_back && isStuck(ai, d, dt)) { ai->break_back = true; setGoal(ai, ai->break_from); }
        Vec3 dir = ai_steer(self, NULL, objects, count, goal);
        if (vec3_length(dir) < 0.5f) stand(self);
        else ai_walk(self, dir, WALK_SPEED * 0.6f, dt);
        return true;
    }
    if (ai->break_now == BREAK_WALK) { /* out a bit, and back */
        Vec3 goal = ai->break_back ? ai->break_from : ai->goal;
        float d = flatDistance(goal, self->position);
        if (!ai->break_back && (d < 1.0f || isStuck(ai, d, dt) || ai->on_break < c->break_length * 0.3f)) {
            ai->break_back = true;
            setGoal(ai, ai->break_from);
        } else if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) {
            if (ai->on_break > 3.0f) ai->break_now = BREAK_SIT; /* (back early: the rest of it sitting) */
            else { endBreak(self); return false; }
        }
        Vec3 dir = ai_steer(self, NULL, objects, count, goal);
        if (vec3_length(dir) < 0.5f) stand(self);
        else ai_walk(self, dir, STROLL_SPEED, dt);
        return true;
    }
    if (ai->on_break <= 0.0f) { endBreak(self); return false; }

    Object *other = ai->chat_with;
    if (other && (Body_isDown(&other->body) || flatDistance(other->position, self->position) > CHAT_RANGE * 1.5f)) other = ai->chat_with = NULL;
    if (other) { /* a chat: over to them, then a word now and then (they answer) */
        /* (over to them; walking along with them while they move - no stop-go at the edge of chatting distance) */
        float gap = Hands_gapFrom(self->position, self->look.height, other);
        ai->chat_near = gap < (ai->chat_near ? CHAT_CLOSE * 1.6f : CHAT_CLOSE);
        if (!ai->chat_near) {
            Vec3 dir = ai_steer(self, other, objects, count, other->position);
            if (vec3_length(dir) < 0.5f) stand(self);
            else ai_walk(self, dir, fminf(WALK_SPEED * 0.8f, (gap - CHAT_CLOSE) * 2.0f + 0.3f), dt);
            return true;
        }
        stand(self);
        turnTowards(self, vec3_sub(other->position, self->position), dt);
        if ((ai->chat_line -= dt) > 0.0f) return true;
        ai->chat_line = CHAT_PAUSE * (0.8f + 0.4f * random01(ai));
        if (ai->chat_said % 2 == 0) Ai_say(self, ai->chat_said == 0 ? PICK(CHAT_OPEN, ai) : PICK(CHAT_MORE, ai));
        else if (other->type == OBJ_HUMAN && other->ai.speech_time <= 0.0f && !other->ai.talking) Ai_say(other, PICK(CHAT_REPLY, ai));
        ai->chat_said++;
        return true;
    }
    /* sitting down (also a chat with nobody around) */
    stand(self);
    if (!self->sitting) Object_sit(self, Object_seatNear(objects, count, self->position, 2.0f), self->position);
    return true;
}

/* Before starting on something new: some think it over first (the hand to the chin). True while thinking. */
static bool thinkItOver(Object *self, const Object *target, float dt) {
    Ai *ai = &self->ai;
    if (target != ai->task_seen) {
        ai->task_seen = target;
        float think = ai->character.ponder * (0.5f + random01(ai));
        if (think > 0.4f && random01(ai) < 0.6f) {
            ai->ponder = think;
            Body_think(&self->body, think);
        }
    }
    if (ai->ponder <= 0.0f) return false;
    ai->ponder -= dt;
    stand(self);
    turnTowards(self, vec3_sub(target->position, self->position), dt);
    return true;
}

/* ---------- The work place ---------- */

/* Before splitting or cutting: is there room? Things lying in the way: the
   tidy clear them away first. Cramped (a wall, a car, a pile of stuff right
   there): the others take the work somewhere with room - carried if it can
   be, else dragged, with somebody called over to help when it's heavy. */
enum { PREP_NONE, PREP_CLEAR, PREP_CARRY, PREP_DONE };
#define PREP_CLUTTER  0.9f   /* m round where they stand: loose things here are in the way */
#define PREP_WALL     0.35f  /* gap to something fixed (or heavy) from where they stand: too close to swing */
#define PREP_CARRY_KG 25.0f  /* things in the way up to this are carried off */
#define PREP_AWAY     2.2f   /* ... this far */
#define PREP_TIMEOUT  45.0f
#define PREP_HELP     20.0f  /* m: somebody this near is called to help drag it */

static bool ignoredHere(const Object *o) {
    return o->type == OBJ_DECAL || o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->collider_count == 0 || o->is_held;
}

static bool inTheWay(const Object *self, const Object *o, const WoodTask *t) {
    if (o == self || o == t->target || o->is_character || o->is_static || o->stored || ignoredHere(o)) return false;
    if (o->weight > PREP_CARRY_KG || o->type == OBJ_WOODPILE || toolOf(o) != TOOL_NONE) return false; /* (tools lying there are for this) */
    if (Tools_splittable(o) || Tools_cuttable(o) || o->type == OBJ_FIREWOOD) return false; /* (the wood is the work) */
    return flatDistance(Object_colliderCenter(o, 0), t->spot) < PREP_CLUTTER;
}

static Object *clutterAt(Object *self, Object *const *objects, int count, const WoodTask *t) {
    Object *best = NULL;
    float best_d = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (!inTheWay(self, o, t) || ai_skipped(&self->ai, o)) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) { best_d = d; best = o; }
    }
    return best;
}

/* Something fixed (or too heavy to shift) right where they'd stand to swing. */
static bool cramped(const Object *self, Object *const *objects, int count, Vec3 spot, const Object *target) {
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (o == self || o == target || o->is_character || ignoredHere(o) || o->type == OBJ_TREE) continue;
        if ((!o->is_static && o->weight < 60.0f) || Tools_cuttable(o) || Tools_splittable(o)) continue; /* (other wood: more work, not a wall) */
        if (Hands_gapFrom(spot, self->look.height, o) < PREP_WALL) return true;
    }
    return false;
}

/* An open spot near the piece: nothing fixed within a swing, nothing lying about. */
static bool roomyPlace(const Object *self, Object *const *objects, int count, const Object *target, Vec3 *out) {
    Vec3 from = Object_colliderCenter(target, 0);
    for (int ring = 0; ring < 3; ring++) {
        float r = 2.5f + 1.2f * (float)ring;
        for (int k = 0; k < 8; k++) {
            float a = (float)k * PI_F / 4.0f + 0.3f * (float)ring;
            Vec3 p = vec3(from.x + r * cosf(a), self->position.y, from.z + r * sinf(a));
            bool ok = true;
            for (int i = 0; i < count && ok; i++) {
                const Object *o = objects[i];
                if (o == target || o == self || ignoredHere(o)) continue;
                if (Hands_gapFrom(p, self->look.height, o) < (o->is_static || o->weight >= 60.0f ? 1.3f : 0.9f)) ok = false;
            }
            if (ok) { *out = p; return true; }
        }
    }
    return false;
}

/* Somebody idle and willing near, to help drag it. */
static Object *helperFor(const Object *self, Object *const *objects, int count) {
    Object *best = NULL;
    float best_d = PREP_HELP;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o == self || o->type != OBJ_HUMAN || o->ai.talking || o->drunk > 0.6f || Body_isBusy(&o->body)) continue;
        if ((o->ai.mode != AI_IDLE && o->ai.mode != AI_WANDER) || o->ai.character.willing < 0.4f) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) { best_d = d; best = o; }
    }
    return best;
}

static void prepDone(Object *self) {
    Ai *ai = &self->ai;
    if (Hands_isGrabbing(self)) Hands_release(self);
    if (ai->prep_helper && ai->prep_helper->ai.mode == AI_HELP && ai->prep_helper->ai.leader == self) {
        Ai_stopHelping(ai->prep_helper);
        Ai_say(ai->prep_helper, "No problem.");
    }
    ai->prep_helper = NULL;
    ai->prep_item = NULL;
    ai->prep_stage = PREP_DONE;
}

/* True while busy getting the place ready (t: what they want to work on). */
static bool prepareSpot(Object *self, Object *const *objects, int count, const WoodTask *t, float dt) {
    Ai *ai = &self->ai;
    if (t->job != JOB_SPLIT && t->job != JOB_CUT) return false;
    if (ai->prep_for != t->target) { /* a new piece: look at the place (once, whoever does) */
        if (t->target->spot_checked) return false;
        t->target->spot_checked = true;
        ai->prep_for = t->target;
        ai->prep_time = 0.0f;
        ai->prep_item = ai->prep_helper = NULL;
        bool tidy = ai->character.likes[ORDER_TRASH] > 0.5f || ai_stackCare(self) >= 0.65f; /* (who likes things tidy) */
        bool messy = clutterAt(self, objects, count, t) != NULL;
        bool tight = cramped(self, objects, count, t->spot, t->target);
        /* (no room: elsewhere with it; things lying about: the tidy clear them, the others don't mind) */
        ai->prep_stage = tight && roomyPlace(self, objects, count, t->target, &ai->prep_spot) ? PREP_CARRY
                       : tidy && messy ? PREP_CLEAR : PREP_DONE;
        if (ai->prep_stage == PREP_CLEAR) Ai_say(self, "Let me clear this away first.");
        else if (ai->prep_stage == PREP_CARRY) Ai_say(self, "No room to swing here - over there's better.");
    }
    if (ai->prep_stage == PREP_DONE || ai->prep_stage == PREP_NONE) return false;
    if ((ai->prep_time += dt) > PREP_TIMEOUT) { /* (not getting anywhere: here then) */
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == t->target || self->hands.item[h] == ai->prep_item) Hands_drop(self, h);
        prepDone(self);
        return false;
    }
    if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; }

    if (ai->prep_stage == PREP_CLEAR) {
        Object *item = ai->prep_item;
        bool carrying = item && (self->hands.item[0] == item || self->hands.item[1] == item);
        if (carrying) { /* off to the side with it, away from the work */
            Vec3 away = flat(vec3_sub(t->spot, t->aim));
            away = vec3_length(away) > 0.05f ? vec3_normalize(away) : vec3_scale(Object_facing(self), -1.0f);
            Vec3 side = vec3(-away.z, 0.0f, away.x);
            Vec3 drop = vec3_add(t->spot, vec3_add(vec3_scale(away, 0.6f), vec3_scale(side, PREP_AWAY)));
            if (flatDistance(drop, self->position) > 0.4f) {
                Vec3 dir = ai_steer(self, NULL, objects, count, drop);
                if (vec3_length(dir) < 0.5f) stand(self);
                else ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
                return true;
            }
            for (int h = 0; h < 2; h++) if (self->hands.item[h] == item) Hands_drop(self, h);
            ai->prep_item = NULL;
            return true;
        }
        item = ai->prep_item = clutterAt(self, objects, count, t);
        if (!item) { prepDone(self); return false; } /* (clear: to work) */
        if (!goTo(self, item, objects, count, PICKUP_REACH, dt)) return true;
        int hand = Hands_freeHand(self);
        if (hand < 0 || !Hands_pickUpObject(self, hand, item)) {
            freeHands(self, NULL); /* (the tool down a moment: fetched again after) */
            hand = Hands_freeHand(self);
            if (hand < 0 || !Hands_pickUpObject(self, hand, item)) { skipTarget(ai, item); ai->prep_item = NULL; }
        }
        return true;
    }

    /* Moving the work: carried if it can be, else dragged (with help if it's heavy and somebody's there) */
    Object *piece = t->target;
    bool carrying = self->hands.item[0] == piece || self->hands.item[1] == piece;
    bool dragging = self->hands.grabbed == piece;
    if (carrying || dragging) {
        if (flatDistance(Object_colliderCenter(piece, 0), ai->prep_spot) < 0.7f) { /* there: down with it, and to work */
            for (int h = 0; h < 2; h++) if (self->hands.item[h] == piece) Hands_drop(self, h);
            if (dragging) Ai_say(self, ai->prep_helper ? "Thanks! That's better." : "There.");
            prepDone(self);
            return true;
        }
        Vec3 dir = ai_steer(self, piece, objects, count, ai->prep_spot);
        if (vec3_length(dir) < 0.5f) { stand(self); return true; }
        ai_walk(self, dir, dragging ? WALK_SPEED * 0.45f : WALK_SPEED * 0.7f, dt);
        return true;
    }
    if (!goTo(self, piece, objects, count, PICKUP_REACH, dt)) return true;
    freeHands(self, NULL);
    if (Hands_pickUpObject(self, HAND_RIGHT, piece) || Hands_pickUpObject(self, HAND_LEFT, piece)) return true;
    if (Hands_grabObject(self, piece)) { /* too big to lift: drag it, and call somebody to help */
        Object *helper = piece->weight > 30.0f ? helperFor(self, objects, count) : NULL;
        Object *player = piece->weight > 30.0f && !helper ? findPlayer(objects, count) : NULL;
        if (player && flatDistance(player->position, self->position) < PREP_HELP)
            Ai_say(self, "Can you give me a hand moving this? (grab it with F)");
        if (helper) {
            static char line[AI_SPEECH_MAX];
            snprintf(line, sizeof line, "%s, give me a hand moving this?", helper->name);
            Ai_say(self, line);
            Ai_say(helper, "Sure.");
            Ai_help(helper, self);
            ai->prep_helper = helper;
        }
        return true;
    }
    prepDone(self); /* (can't move it: here then) */
    return false;
}

void ai_updateWork(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    /* Worn out: a breather first */
    if (ai->resting || self->needs.stamina < 0.12f) {
        if (!ai->resting && self->needs.stamina < 0.12f) Ai_say(self, "Phew... I need a break.");
        ai->resting = self->needs.stamina < 0.45f;
        if (ai->resting) { stand(self); if (!self->sitting) Object_sit(self, NULL, self->position); return; }
    }
    if (ai->order != ORDER_FIRE && takeBreak(self, objects, count, dt)) return; /* (tending a fire they sit by it anyway) */
    if (ai->order == ORDER_TRASH) { workTrash(self, objects, count, dt); return; }
    if (ai->order == ORDER_FIRE) { workFire(self, objects, count, dt); return; }
    if (Tools_isWorking(self)) { riskyTool(self, Tools_held(self, NULL), dt); stand(self); return; }

    /* Lively people working near the player like company */
    Object *player = findPlayer(objects, count);
    if (player && ai->talk_cooldown <= 0.0f && self->temperament.energy > 0.5f &&
        flatDistance(player->position, self->position) < 8.0f && player->work_state.since > 5.0f &&
        random01(ai) < 0.004f * dt) {
        approach(self, TOPIC_ASK_HAND);
        return;
    }

    Object *moving = ai->prep_for;
    if (ai->prep_stage == PREP_CARRY && moving && (self->hands.item[0] == moving || self->hands.item[1] == moving || self->hands.grabbed == moving)) {
        WoodTask mt = ai_taskFor(self, moving, Tools_splittable(moving) ? JOB_SPLIT : JOB_CUT); /* (taking the work elsewhere: not to the pile) */
        if (prepareSpot(self, objects, count, &mt, dt)) return;
    }
    if (firewoodInHands(self) > 0 && carryFirewood(self, objects, count, dt)) return;
    if (branchesInHands(self) > 0 && tidyBranches(self, objects, count, dt)) return;
    WoodTask t;
    ai->timber = false;
    if (ai_nextWoodTask(self, objects, count, ai->work_area, ai->order == ORDER_TREES, &t)) {
        if (thinkItOver(self, t.target, dt)) return;
        if (t.job == JOB_FELL) {
            ai->work_area = Object_colliderCenter(t.target, 0);
            if (t.target->fell_blocked) { /* (nowhere for it to come down) */
                if (random01(ai) < 0.7f) Ai_say(self, "No room for that one to come down - I'll leave it.");
                skipTarget(ai, t.target);
                return;
            }
            ai->timber = Tools_fellDone(t.target) > FELL_WARN && !Tools_isWorking(self) && Ai_warnFellingRoom(self, objects, count, t.target, t.target->fell_plan);
            if (ai->timber) { stand(self); return; } /* (not the last strokes with somebody under it) */
        }
        if (prepareSpot(self, objects, count, &t, dt)) return;
        ai_workOn(self, objects, count, &t, dt);
        return;
    }
    if (carryFirewood(self, objects, count, dt)) return;
    if (tidyBranches(self, objects, count, dt)) return;

    /* Done here. Firewood left lying because nobody said where it goes: ask. */
    if (!ai->woodpile && !ai->no_pile && closestLoosePiece(self, objects, count) && player) {
        Ai_say(self, "Done! Where should the firewood go?");
        approach(self, TOPIC_WHERE);
        ai->before = AI_WORK;
        return;
    }
    /* Only splitting left, and the axe is in the player's hands: ask for it, or wait */
    if (player && woodLeft(objects, count, ai->work_area) && waitForTool(self, objects, count, player, dt)) return;
    Ai_say(self, woodLeft(objects, count, ai->work_area) ? "That's all I can do without an axe." : "All done!");
    Ai_stopWork(self);
}