Nimbin[12]?Games / Entropie / entropie/src/people/ai_help.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_help.c 42.7 KB · 912 lines raw
/*
 * Helping the player: pushing and carrying along, watching to learn,
 * being shown a place, joining in with wood, tidying up on the way and
 * copying what the player does.
 */

#include "people/ai_internal.h"
#include "world/car.h"
#include "world/ladder.h"
#include "people/climb.h"

/* ---------- Helping ---------- */

/* A free spot right at the object, not on top of the leader. */
/* Standing at p, where the load wants to go (dir, flat and unit): in front of it, not beside it. */
static bool inLoadsWay(const Object *target, Vec3 dir, Vec3 p) {
    const Collider *c = &target->colliders[0];
    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
    Vec3 across = vec3_cross(dir, UP);
    float length = 0.0f, width = 0.0f;
    for (int k = 0; k < 3; k++) {
        length += fabsf(vec3_dot(c->axes[k], dir)) * half[k];
        width  += fabsf(vec3_dot(c->axes[k], across)) * half[k];
    }
    Vec3 rel = flat(vec3_sub(p, Object_colliderCenter(target, 0)));
    float along = vec3_dot(rel, dir), side = fabsf(vec3_dot(rel, across));
    return along > 0.0f && along < length + 1.0f && side < width + 0.35f; /* (a body in front, not beside) */
}

static bool goodSpot(Object *self, Object *const *objects, int count, const Object *target, Vec3 p) {
    const Object *leader = self->ai.leader;
    if (flatDistance(p, leader->position) < PERSON_RADIUS + 0.1f) return false;
    float gap = Hands_gapFrom(p, self->look.height, target);
    if (gap > SPOT_GAP || gap < 0.05f) return false; /* at it, not in it */
    if (self->ai.clear_time > 0.0f && inLoadsWay(target, self->ai.clear_dir, p)) return false; /* (we were in the way there) */
    return !ai_blockedBy(self, leader, target, objects, count, p);
}

/*
 * Where to stand to push or carry the leader's object: beside the leader if
 * there is room there, else anywhere along the object (long logs), as close
 * to us as possible.
 */
/* Spots along an object: both sides of its long axis and its ends. For
   long things the far end from the leader is best: the load is shared evenly. */
static bool alongSpot(Object *self, Object *const *objects, int count, const Object *target, Vec3 *spot, float *best) {
    const Object *leader = self->ai.leader;
    const Collider *c = &target->colliders[0];
    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
    int along = 0;
    float longest = 0.0f;
    for (int k = 0; k < 3; k++) {
        float reach = vec3_length(flat(c->axes[k])) * half[k];
        if (reach > longest) { longest = reach; along = k; }
    }
    Vec3 u = vec3_normalize(flat(c->axes[along]));
    Vec3 v = vec3_cross(u, UP);
    float width = 0.0f;
    for (int k = 0; k < 3; k++) width += fabsf(vec3_dot(c->axes[k], v)) * half[k];
    Vec3 center = Object_colliderCenter(target, 0);
    float leader_at = vec3_dot(flat(vec3_sub(leader->position, center)), u);
    bool is_long = longest > 0.6f;
    bool found = false;
    for (int k = -3; k <= 3; k++) {
        for (int side = -1; side <= 1; side += 2) {
            float t = (float)k / 3.0f * longest * 0.85f;
            Vec3 p = vec3_add(center, vec3_add(vec3_scale(u, t), vec3_scale(v, (float)side * (width + 0.4f))));
            if (k == -3 || k == 3) { /* the ends */
                t = (k < 0 ? -1.0f : 1.0f) * (longest + 0.4f);
                p = vec3_add(center, vec3_scale(u, t));
            }
            p.y = self->position.y;
            if (!goodSpot(self, objects, count, target, p)) continue;
            float cost = flatDistance(p, self->position) * (is_long ? 0.3f : 1.0f) + 1.5f;
            if (is_long) cost -= 1.5f * fabsf(t - leader_at); /* away from the leader: even lifting */
            if (cost < *best) {
                *best = cost;
                *spot = p;
                found = true;
            }
        }
    }
    return found;
}

/*
 * Where to stand to push or carry the leader's object: along a long object
 * the far end from the leader; else beside the leader if there is room
 * there, else anywhere along the object, as close to us as possible.
 */
static bool findHelpSpot(Object *self, Object *const *objects, int count, const Object *target, Vec3 *spot) {
    const Object *leader = self->ai.leader;
    Vec3 heading = leader->hands.grab_heading;
    Vec3 right = vec3_normalize(vec3_cross(heading, UP));
    static const float OFFSETS[] = { 0.75f, 1.1f, 1.5f };

    float best = 1e9f;
    const Collider *c = &target->colliders[0];
    float longest = fmaxf(c->half_extents.x, fmaxf(c->half_extents.y, c->half_extents.z));
    if (longest > 0.6f && alongSpot(self, objects, count, target, spot, &best)) return true;

    for (unsigned i = 0; i < sizeof(OFFSETS) / sizeof(OFFSETS[0]); i++) {
        for (int s = -1; s <= 1; s += 2) {
            Vec3 p = vec3_add(leader->position, vec3_scale(right, OFFSETS[i] * (float)s));
            if (!goodSpot(self, objects, count, target, p)) continue;
            /* Keep to the side we chose unless it is impossible */
            float cost = flatDistance(p, self->position) + ((float)s != self->ai.side ? 2.0f : 0.0f);
            if (cost < best) {
                best = cost;
                *spot = p;
            }
        }
    }
    if (best < 1e9f) return true;
    return alongSpot(self, objects, count, target, spot, &best);
}

static bool tryGrab(Object *self, Object *target) {
    Vec3 saved = Object_facing(self);
    setFacing(self, vec3_sub(Object_colliderCenter(target, 0), self->position));
    if (Hands_grabObject(self, target)) return true;
    setFacing(self, saved);
    return false;
}

/* A tool lying around that can do the job. */
Object *ai_findTool(const Object *self, Object *const *objects, int count, Job job) {
    Object *best = NULL;
    float best_dist = 60.0f; /* (the tool room may be a walk away) */
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->is_held || !Tools_canDo(toolOf(o), job) || !Ai_mayUse(self, toolOf(o))) continue;
        if (ai_skipped(&self->ai, o)) continue; /* couldn't get at it */
        float d = flatDistance(o->position, self->position);
        if (d < best_dist) {
            best_dist = d;
            best = o;
        }
    }
    return best;
}

/* ---------- Being shown something ---------- */

void ai_updateFollow(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *player = findPlayer(objects, count);
    if (!player) { Ai_stopShowing(self); return; }

    /* Watching the player work: one finished job and they've got it */
    if (ai->showing == SHOW_HOW && player->work_state.jobs_done > ai->seen_jobs) {
        ai->knows_wood = true;
        Ai_say(self, "Ah, I get it! I'll take it from here.");
        const Object *wood = player->work_state.last_target;
        Ai_order(self, ai->pending != ORDER_NONE ? ai->pending : ORDER_FIREWOOD,
                 wood ? Object_colliderCenter(wood, 0) : player->position);
        return;
    }

    Vec3 facing = Object_facing(player), right = vec3_normalize(vec3_cross(facing, UP));
    float sway = sinf(self->body.clock * 0.4f + ai->side) * 0.3f;
    Vec3 spot = ai->showing == SHOW_HOW
              ? vec3_add(player->position, vec3_add(vec3_scale(right, ai->side * (2.0f + sway)), vec3_scale(facing, 0.6f)))
              : vec3_add(player->position, vec3_add(vec3_scale(facing, -1.6f), vec3_scale(right, ai->side * (0.5f + sway))));
    float dist = flatDistance(spot, self->position);
    Vec3 eye = vec3_add(self->position, vec3(0.0f, self->look.height * 0.93f, 0.0f));
    Vec3 hands = vec3_add(player->position, vec3(0.0f, player->look.height * (ai->showing == SHOW_HOW ? 0.55f : 0.93f), 0.0f));
    self->body.look_dir = vec3_normalize(vec3_sub(hands, eye));
    self->body.look_weight = 1.0f;
    /* (arrived: stays put until the spot is clearly somewhere else - no shuffling on the spot) */
    if (dist < ARRIVE_DISTANCE * 2.0f) ai->settled = SETTLED_UNTIL;
    else if (dist > ai->settled) ai->settled = 0.0f;
    if (ai->settled <= 0.0f) {
        Vec3 dir = ai_steer(self, player, objects, count, spot);
        if (vec3_length(dir) < 0.5f) {
            if (dist < BLOCKED_SETTLE) ai->settled = dist + SETTLED_UNTIL; /* blocked close by: stay */
            stand(self);
            return;
        }
        ai_walk(self, dir, dist > RUN_TO_LEADER ? runSpeed(self) * 0.8f : fminf(WALK_SPEED, dist * 2.0f + 0.3f), dt);
        return;
    }
    stand(self);
    turnTowards(self, vec3_sub(player->position, self->position), dt * 0.5f);
}

/* ---------- Helping with wood ---------- */

/*
 * The leader works on wood: the helper understands it's about firewood and
 * carries on with whatever needs doing around there (fells along, takes the
 * branches off, cuts and splits), fetching fitting tools. Returns false if
 * there is no wood work to help with.
 */
static bool helpWood(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    const Object *leader = ai->leader;
    const Work *lw = &leader->work_state;
    bool recent = lw->last_target && lw->since < 10.0f && isWoodJob(lw->last_job);
    if (recent) {
        ai->work_area = Object_colliderCenter(lw->last_target, 0);
        ai->order = ORDER_FIREWOOD; /* that's what all this wood work is for */
    } else if (ai->order == ORDER_NONE) {
        return false;
    }
    if (flatDistance(leader->position, ai->work_area) > 15.0f) { /* the leader moved on */
        ai->job_target = NULL;
        ai->order = ORDER_NONE;
        return false;
    }
    if (Tools_isWorking(self)) { stand(self); return true; }

    WoodTask t;
    Object *tree = lw->last_target;
    if (recent && lw->last_job == JOB_FELL && tree->type == OBJ_TREE && !tree->toppled) {
        /* Felling together: at the trunk, out of the leader's swing */
        t = ai_taskFor(self, tree, JOB_FELL);
        Vec3 trunk = Object_colliderCenter(tree, 0);
        Vec3 from = flat(vec3_sub(leader->position, trunk));
        from = vec3_length(from) > 1e-3f ? vec3_normalize(from) : vec3(1.0f, 0.0f, 0.0f);
        t.spot = vec3_add(trunk, vec3_scale(rotateY(from, ai->side * 1.4f), tree->colliders[0].half_extents.x + STAND_OFF));
        t.spot.y = self->position.y;
    } else if (!ai_nextWoodTask(self, objects, count, ai->work_area, false, &t)) {
        ai->job_target = NULL;
        ai->order = ORDER_NONE; /* all done here */
        Ai_say(self, woodLeft(objects, count, ai->work_area) ? "I'd need the axe to split the rest." : "That's done!");
        return false;
    }
    ai_workOn(self, objects, count, &t, dt);
    return true;
}

/* ---------- Tidy people clean up on the way ---------- */

#define TIDY_LIKE    0.35f  /* liking tidying up this much ... */
#define TIDY_REACH   1.3f   /* ... picks up trash this close on the way */
#define TIDY_BIN     50.0f  /* takes it to a container this far away when there's time */
#define ASHTRAY_FULL 12     /* butts: the tidy empty the ashtray */

static Object *trashInHands(const Object *self) {
    for (int h = 0; h < 2; h++)
        if (self->hands.state[h] == HAND_HOLDING && self->hands.item[h] && Object_isTrash(self->hands.item[h])) return self->hands.item[h];
    return NULL;
}

/* Bottles put back into the wrong crate: the tidy sort them - one out, over to
   a crate of its own kind with room, in (the crates go back to the shop sorted). */
#define SORT_LOOK  25.0f /* m: crates this near are looked at ... */
#define SORT_FAR   60.0f /* ... and a bottle's own crate this near is walked to */
#define SORT_GIVE  45.0f /* s: can't get there: given up */
static bool sortCrates(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *b = ai->sort_bottle;
    if (b) { /* over to its own crate with it */
        bool held = self->hands.item[HAND_LEFT] == b || self->hands.item[HAND_RIGHT] == b;
        Object *to = held && !b->broken ? Crate_rightFor(objects, count, Crate_bottleKind(b), self->position, SORT_FAR) : NULL;
        if (!to || (ai->sort_time += dt) > SORT_GIVE) {
            if (held) for (int h = 0; h < 2; h++) if (self->hands.item[h] == b) Hands_drop(self, h);
            ai->sort_bottle = NULL;
            return false;
        }
        if (!goTo(self, to, objects, count, 0.8f, dt)) return true;
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == b) Hands_drop(self, h);
        Hands_touch(self, vec3_add(Object_colliderCenter(to, 0), vec3(0.0f, 0.2f, 0.0f)));
        Crate_putBottle(to, b);
        ai->sort_bottle = NULL;
        ai->tidy_pause = 0.6f;
        return true;
    }
    if (ai->mode != AI_IDLE && ai->mode != AI_WANDER) { ai->sort_from = NULL; return false; }
    Object *from = ai->sort_from;
    if (from) { /* to the crate, the bottle out */
        int hand = Hands_freeHand(self);
        if (hand < 0 || from->is_held || !Crate_nextWrong(from) || (ai->sort_time += dt) > SORT_GIVE) {
            if (ai->sort_time > SORT_GIVE) skipTarget(ai, from);
            ai->sort_from = NULL;
            return false;
        }
        if (!goTo(self, from, objects, count, 0.8f, dt)) return true;
        ai->sort_from = NULL;
        Hands_touch(self, vec3_add(Object_colliderCenter(from, 0), vec3(0.0f, 0.2f, 0.0f)));
        Object *w = Crate_takeWrong(g_world, from);
        if (w && Hands_receive(self, hand, w)) { /* (straight out of the crate into the hand) */
            ai->sort_bottle = w;
            ai->sort_time = 0.0f;
            if (random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "That doesn't go in there." : "Who puts these in the wrong crate?");
        }
        ai->tidy_pause = 0.5f;
        return true;
    }
    if (Hands_freeHand(self) < 0 || trashInHands(self) || (ai->sort_look -= dt) > 0.0f) return false;
    ai->sort_look = 3.0f;
    for (int i = 0; i < count; i++) { /* a crate with one that doesn't belong, its own crate with room somewhere */
        Object *c = objects[i];
        int kind = c->type == OBJ_CRATE && !c->is_held ? Crate_nextWrong(c) : BOTTLE_KIND_NONE;
        if (!kind || ai_skipped(ai, c) || flatDistance(c->position, self->position) > SORT_LOOK) continue;
        if (!Crate_rightFor(objects, count, kind, c->position, SORT_FAR)) continue;
        bool taken = false;
        for (int j = 0; j < count && !taken; j++) taken = objects[j] != self && objects[j]->is_character && objects[j]->ai.sort_from == c;
        if (taken) continue;
        ai->sort_from = c;
        ai->sort_time = 0.0f;
        return true;
    }
    return false;
}

/* Returns true if it took over this frame (bending down, or off to the container). */
bool ai_tidyUp(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (ai->character.likes[ORDER_TRASH] < TIDY_LIKE || self->sitting || Camp_bagInHand(self)) return false;
    if (ai->tidy_pause > 0.0f) {
        ai->tidy_pause -= dt;
        stand(self);
        return true;
    }
    /* trash right there, roughly ahead: take it along */
    int hand = Hands_freeHand(self);
    if (hand >= 0 && self->hands.state[1 - hand] != HAND_REACHING) {
        Vec3 f = Object_facing(self);
        for (int i = 0; i < count; i++) {
            Object *o = objects[i];
            if (!Object_isTrash(o) || o->is_held || o->weight > 2.0f) continue;
            Vec3 to = flat(vec3_sub(o->position, self->position));
            float d = vec3_length(to);
            if (d > TIDY_REACH || (d > 0.5f && vec3_dot(vec3_scale(to, 1.0f / d), f) < 0.2f)) continue;
            if (claimed(self, objects, count, o)) continue;
            if (!Hands_pickUpObject(self, hand, o)) { skipTarget(ai, o); continue; } /* (just out of reach: not again and again) */
            stand(self);
            turnTowards(self, to, dt);
            ai->tidy_pause = 0.7f;
            if (random01(ai) < 0.25f) Ai_say(self, random01(ai) < 0.5f ? "Tsk. People..." : "That doesn't belong here.");
            return true;
        }
    }
    /* a full ashtray, nothing else to do: tipped out (into a scrap of paper), off to the bin with it */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && hand >= 0 && !trashInHands(self) && !Body_isSmoking(&self->body)) {
        Object *tin = NULL;
        for (int i = 0; i < count && !tin; i++) {
            Object *o = objects[i];
            if (o->type == OBJ_FURNITURE && o->variant == FURN_ASHTRAY && o->stored >= ASHTRAY_FULL &&
                flatDistance(o->position, self->position) < TIDY_BIN && !claimed(self, objects, count, o)) tin = o;
        }
        if (tin) {
            if ((ai->tray_time += dt) > 45.0f) { skipTarget(ai, tin); ai->tray_time = 0.0f; return false; } /* (can't get there) */
            if (goTo(self, tin, objects, count, 0.75f, dt)) {
                ai->tray_time = 0.0f;
                turnTowards(self, flat(vec3_sub(tin->position, self->position)), dt);
                ai->empty_tray = tin;
                ai->tidy_pause = 1.0f;
                Ai_say(self, random01(ai) < 0.5f ? "Full to the brim again." : "Who smokes all this?");
            }
            return true;
        }
    }
    /* bottles in the wrong crate: sorted */
    if (sortCrates(self, objects, count, dt)) return true;
    /* nothing better to do: an empty bottle back into a crate, other trash to the container */
    Object *held = trashInHands(self);
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && held && Object_isBottle(held) && !held->broken) {
        Object *crate = Crate_forBottle(objects, count, held, self->position, TIDY_BIN); /* (its own crate if there's one) */
        if (crate && !ai_skipped(ai, crate)) {
            if ((ai->tray_time += dt) > 45.0f) { skipTarget(ai, crate); ai->tray_time = 0.0f; return false; } /* (can't get there) */
            if (goTo(self, crate, objects, count, 0.8f, dt)) {
                ai->tray_time = 0.0f;
                for (int h = 0; h < 2; h++) if (self->hands.item[h] == held) Hands_drop(self, h);
                Hands_touch(self, vec3_add(Object_colliderCenter(crate, 0), vec3(0.0f, 0.2f, 0.0f)));
                Crate_putBottle(crate, held);
                ai->tidy_pause = 0.6f;
            }
            return true;
        }
    }
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && trashInHands(self)) {
        Object *bin = ai_nearestOf(self, objects, count, OBJ_BIN, OBJ_BOX);
        if (!bin || flatDistance(bin->position, self->position) > TIDY_BIN) return false;
        if (goTo(self, bin, objects, count, 0.9f, dt)) Camp_throwAway(self);
        return true;
    }
    return false;
}

#define PUT_AWAY_CARE 0.5f   /* this careful (see ai_stackCare): tools go back where they belong */
#define PUT_AWAY_FAR  80.0f  /* ... unless that's further than this */

/* Holding a tool with nothing to do with it: careful people take it back to
   its place (the tool room), the others put it down where they are. */
bool ai_putAway(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    int hand = -1;
    for (int h = 0; h < 2; h++) {
        const Object *it = self->hands.item[h];
        if (self->hands.state[h] == HAND_HOLDING && it && it->has_home &&
            (it->type == OBJ_AXE || it->type == OBJ_SHEARS || it->type == OBJ_CHAINSAW || it->type == OBJ_SCYTHE ||
             (it->type == OBJ_UMBRELLA && g_world && g_world->rain < 0.02f))) hand = h; /* (an umbrella once the rain's over) */
    }
    if (hand < 0 || ai->talking || ai->on_project) return false; /* (at their side job: it's what they work with) */
    Object *tool = self->hands.item[hand];
    float d = flatDistance(tool->home_spot, self->position);
    if (ai_stackCare(self) < PUT_AWAY_CARE || d > PUT_AWAY_FAR) { /* down it goes, right here */
        Hands_drop(self, hand);
        return false;
    }
    if (d > 0.7f) {
        Vec3 dir = ai_steer(self, tool, objects, count, tool->home_spot);
        if (vec3_length(dir) < 0.5f) {
            stand(self);
            if (isStuck(ai, d, dt) && ai->stuck > 6.0f) Hands_drop(self, hand); /* (can't get there: here then) */
        } else {
            ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
        }
        return true;
    }
    stand(self);
    Hands_drop(self, hand);
    tool->position = tool->home_spot; /* (back on its hook, as it was) */
    tool->rotation = tool->home_rot;
    tool->velocity = tool->angular_velocity = vec3_zero();
    Object_syncColliders(tool);
    if (random01(ai) < 0.3f) Ai_say(self, "Back where it belongs.");
    return true;
}

/* ---------- Doing what the leader does ---------- */

/* What sort of thing it is, for "something like that" (0: nothing to copy). */
static int sortOf(const Object *o) {
    if (!o || o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_HOSE ||
        o->type == OBJ_LADDER || o->type == OBJ_BAG)
        return 0; /* tools and one-offs: wood help, or nothing like it around */
    if (Object_isFuel(o))   return 1;
    if (Object_isTrash(o))  return 2;
    if (Object_isBottle(o)) return 3;
    if (Object_isFood(o))   return 4;
    return 100 + (int)o->type;
}

static Object *leaderItem(const Object *leader) {
    for (int h = 0; h < 2; h++)
        if (leader->hands.state[h] == HAND_HOLDING && sortOf(leader->hands.item[h])) return leader->hands.item[h];
    return NULL;
}

/* Our item of that sort (and the hand it's in), or NULL. */
static Object *mineOfSort(const Object *self, int sort, int *hand) {
    for (int h = 0; h < 2; h++) {
        Object *o = self->hands.item[h];
        if (self->hands.state[h] == HAND_HOLDING && o && sortOf(o) == sort) {
            if (hand) *hand = h;
            return o;
        }
    }
    return NULL;
}

#define COPY_SEARCH 25.0f  /* looks this far around the leader for something like theirs */

static Object *nearestOfSort(const Object *self, Object *const *objects, int count, int sort, Vec3 around) {
    Object *best = NULL;
    float best_d = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->is_held || o->is_static || o->lifted || !o->mesh.vao || o->broken || o->weight > 10.0f || sortOf(o) != sort) continue;
        if (flatDistance(o->position, around) > COPY_SEARCH || (ai_skipped(&self->ai, o))) continue;
        if (sort == 1 && inAnyFire(objects, count, o)) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d && !claimed(self, objects, count, o)) {
            best_d = d;
            best = o;
        }
    }
    return best;
}

/* Walks to p; true once there. */
static bool walkTo(Object *self, const Object *ignore, Object *const *objects, int count, Vec3 p, float reach, float dt) {
    if (flatDistance(p, self->position) < reach) { stand(self); return true; }
    Vec3 dir = ai_steer(self, ignore, objects, count, p);
    if (vec3_length(dir) < 0.5f) stand(self);
    else ai_walk(self, dir, WALK_SPEED * 0.85f, dt);
    return false;
}

/*
 * Not clear what the leader is up to: take something like what they carry
 * and do what they do with it - carry it along at their side, put it down
 * next to where they put theirs, into the fire or the bin like they did.
 */
static bool helpImitate(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *leader = ai->leader;
    Object *theirs = leaderItem(leader);
    int hand = -1;
    if (theirs) {
        if (ai->copied != theirs) {
            ai->copied = theirs;
            ai->copy_sort = sortOf(theirs);
            ai->copy_step = 0;
        }
        if (mineOfSort(self, ai->copy_sort, NULL)) return false; /* got one: along at the leader's side */
        if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; }
        Object *like = nearestOfSort(self, objects, count, ai->copy_sort, leader->position);
        if (!like) return false;
        if (ai->job_target != like) {
            ai->job_target = like;
            ai->stuck = 0.0f;
            if (ai->speech_time <= 0.0f) Ai_say(self, "I'll grab one too.");
        }
        if (Hands_freeHand(self) < 0) freeHands(self, NULL);
        if (goTo(self, like, objects, count, PICKUP_REACH, dt)) {
            int h = Hands_freeHand(self);
            if (h >= 0) Hands_pickUpObject(self, h, like);
            ai->job_target = NULL;
        } else if ((ai->stuck += dt) > 4.0f) {
            skipTarget(ai, like);
        }
        return true;
    }
    /* The leader let go of theirs: the same with ours */
    Object *it = ai->copied, *mine = it ? mineOfSort(self, ai->copy_sort, &hand) : NULL;
    if (!it || !mine) { ai->copied = NULL; return false; }
    if (ai->copy_step == 0) {
        if (it->is_held) { ai->copied = NULL; return false; } /* handed to someone */
        Object *bin = ai_nearestOf(self, objects, count, OBJ_BIN, OBJ_BOX);
        if (it->mesh.vao && ai->copy_sort == 1 && inAnyFire(objects, count, it)) {
            ai->copy_step = 2;
            ai->target = Camp_fireNear(objects, count, it->position, 3.0f, false);
        } else if (!it->mesh.vao && bin && flatDistance(bin->position, leader->position) < 3.0f) {
            ai->copy_step = 3; /* thrown away */
            ai->target = bin;
        } else if (!it->mesh.vao) {
            ai->copied = NULL; /* eaten or drunk: keep ours */
            return false;
        } else {
            /* put down: next to theirs, on our side */
            Vec3 right = vec3_normalize(vec3_cross(Object_facing(leader), UP));
            ai->copy_step = 1;
            ai->copy_put = vec3_add(it->position, vec3_scale(right, ai->side * 0.45f));
            if (blockedAt(self, leader, objects, count, ai->copy_put))
                ai->copy_put = vec3_add(it->position, vec3_scale(right, -ai->side * 0.45f));
        }
        ai->closest = 1e9f;
        ai->stuck = 0.0f;
        if (ai->copy_step >= 2 && !ai->target) { /* (no fire or bin after all) */
            ai->copy_step = 1;
            ai->copy_put = it->position;
        }
    }
    bool done = false;
    /* can't get there: down here then */
    bool stuck = ai->copy_step == 1 && isStuck(ai, flatDistance(ai->copy_put, self->position), dt) && ai->stuck > 4.0f;
    if (ai->copy_step == 1 && (stuck || walkTo(self, leader, objects, count, ai->copy_put, 1.0f, dt))) {
        stand(self);
        turnTowards(self, vec3_sub(ai->copy_put, self->position), dt);
        Hands_drop(self, hand);
        Object *pile = NULL; /* theirs went onto a woodpile: ours too */
        for (int i = 0; i < count && it->stored && mine->type == OBJ_FIREWOOD && mine->size.z >= 4.0f; i++)
            if (objects[i]->type == OBJ_WOODPILE && flatDistance(objects[i]->position, it->position) < 2.0f) pile = objects[i];
        if (pile) ai_pileSlid(self, Object_stackFirewood(pile, mine, objects, count, ai_stackCare(self)));
        done = true;
    } else if (ai->copy_step == 2) {
        Object *fire = ai->target;
        if (walkTo(self, fire, objects, count, fire->position, fire->size.x * 0.5f + 0.9f, dt)) {
            turnTowards(self, vec3_sub(fire->position, self->position), dt);
            if (Camp_feed(self, fire)) Ai_practice(self, ORDER_FIRE, 0.01f);
            done = true;
        }
    } else if (ai->copy_step == 3 && goTo(self, ai->target, objects, count, 0.9f, dt)) {
        Camp_throwAway(self);
        done = true;
    }
    if (done) {
        ai->copied = NULL;
        ai->copy_step = 0;
        ai->target = NULL;
        if (random01(ai) < 0.4f) Ai_say(self, "Like that?");
    }
    return true;
}

/* Pushing or pulling together and the load won't budge, because we stand
   where it wants to go: the quick-witted see it, let go and come round to
   another side; the slow keep at it (and wonder). Returns true while letting go. */
#define JAM_SPEED   0.2f  /* m/s: the leader wants to go at least this fast ... */
#define JAM_MOVING  0.35f /* ... and the load does less than this share of it */
#define JAM_NOTICE  0.8f  /* s before the sharpest notice (the slow take longer) */
#define JAM_WITS    0.35f /* less (drunk takes off): they don't get it */
#define CLEAR_TIME  6.0f  /* s keeping off that side */

static bool makeRoom(Object *self, const Object *leader, const Object *target, float dt) {
    Ai *ai = &self->ai;
    ai->clear_time = fmaxf(0.0f, ai->clear_time - dt);
    Vec3 want = flat(leader->movement);
    float speed = vec3_length(want);
    if (!target || speed < JAM_SPEED) { ai->jam_time = 0.0f; return false; }
    Vec3 dir = vec3_scale(want, 1.0f / speed);
    if (!inLoadsWay(target, dir, self->position)) { ai->jam_time = 0.0f; return false; }
    if (ai->jam_time <= 0.0f) ai->jam_from = target->position;
    ai->jam_time += dt;
    float wits = self->wits - 0.6f * fminf(self->drunk, 1.0f);
    if (ai->jam_time < JAM_NOTICE + 2.5f * (1.0f - clampf(wits, 0.0f, 1.0f))) return false;
    float moved = vec3_dot(flat(vec3_sub(target->position, ai->jam_from)), dir);
    float time = ai->jam_time;
    ai->jam_time = 0.0f;
    if (ai->pushing && moved > JAM_MOVING * speed * time) return false; /* (holding on and it goes: all fine; not holding on, we're just shoved along) */
    if (wits < JAM_WITS) { /* doesn't get it: keeps at it */
        if (random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "Why won't it move?" : "Push harder!");
        return false;
    }
    Ai_say(self, moved > 0.3f ? "Whoa - I'm in the way!" : random01(ai) < 0.5f ? "Wait - I'm in the way." : "Hang on, I'll come round.");
    if (Hands_isGrabbing(self)) Hands_release(self);
    ai->pushing    = false;
    ai->retry      = 0.0f;
    ai->closest    = 1e9f;
    ai->clear_dir  = dir;
    ai->clear_time = CLEAR_TIME;
    stand(self);
    return true;
}

void ai_updateHelp(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *leader = ai->leader;
    Object *target = leader->hands.grabbed;

    /* Worn out: let go and catch breath before going on */
    if (ai->resting || self->needs.stamina < 0.12f) {
        ai->resting = self->needs.stamina < 0.45f;
        if (ai->resting) {
            if (Hands_isGrabbing(self)) Hands_release(self);
            ai->pushing = false;
            stand(self);
            return;
        }
    }

    /* The leader went up a ladder (or came down): after them */
    if (!target && !ai->pushing && ai_followByLadder(self, objects, count, leader, dt)) return;

    /* Wood work: understand what the leader is up to and carry on with it */
    if (!target && !ai->pushing && helpWood(self, objects, count, dt)) return;
    /* Not sure what it's about: do the same as the leader */
    if (!target && !ai->pushing && helpImitate(self, objects, count, dt)) return;
    if (!target && !Tools_isWorking(self)) {
        ToolKind held = Tools_held(self, NULL);
        if (held == TOOL_AXE || held == TOOL_CHAINSAW || held == TOOL_SHEARS) freeHands(self, NULL); /* done: tools down */
    }

    /* Leader let go (or we lost our grip): walk along again; a lost grip
       means a moment's pause before trying again */
    if (ai->pushing && (!target || self->hands.grabbed != target)) {
        if (Hands_isGrabbing(self)) Hands_release(self);
        if (target) ai->retry = 1.0f + random01(ai);
        ai->pushing = false;
        ai->closest = 1e9f;
    }
    if (makeRoom(self, leader, target, dt)) return;
    if (ai->retry > 0.0f) {
        ai->retry -= dt;
        stand(self);
        if (target) turnTowards(self, vec3_sub(Object_colliderCenter(target, 0), self->position), dt * 0.5f);
        return;
    }

    if (ai->pushing) {
        /* Same push / pull as the leader */
        self->movement   = leader->movement;
        self->move_local = leader->move_local;
        setFacing(self, self->hands.grab_heading);
        return;
    }

    Vec3 heading = target ? leader->hands.grab_heading : Object_facing(leader);
    Vec3 right = vec3_normalize(vec3_cross(heading, UP));
    Vec3 spot;
    if (!target || !findHelpSpot(self, objects, count, target, &spot)) {
        /* Walking along: beside and a bit behind, never in a straight line */
        float t = self->body.clock, phase = (float)(ai->rng & 255u) * 0.02f;
        float beside = FOLLOW_SPACING * (1.0f + 0.25f * sinf(t * 0.31f + phase));
        float behind = 0.5f + 0.35f * sinf(t * 0.23f + 2.0f * phase);
        if (ai->copied && mineOfSort(self, ai->copy_sort, NULL)) { /* carrying the same: right at their side, in step */
            beside = FOLLOW_SPACING * 0.75f;
            behind = 0.1f;
        }
        spot = vec3_add(leader->position, vec3_sub(vec3_scale(right, ai->side * beside), vec3_scale(heading, behind)));
    }

    float dist = flatDistance(spot, self->position);
    if (dist > ARRIVE_DISTANCE && isStuck(ai, dist, dt)) {
        ai->side = -ai->side; /* try the leader's other side */
        ai->closest = 1e9f;
        ai->stuck = 0.0f;
    }

    if (target && dist < GRAB_TRY_RADIUS && tryGrab(self, target)) {
        ai->pushing = true;
        stand(self);
        return;
    }

    if (target) ai->settled = 0.0f;
    else if (dist < ARRIVE_DISTANCE) ai->settled = SETTLED_UNTIL;
    else if (dist > ai->settled) ai->settled = 0.0f;
    if (dist > ARRIVE_DISTANCE && ai->settled <= 0.0f) {
        /* Close to the object: walk right up to it */
        const Object *near = target && dist < 2.0f ? target : NULL;
        Vec3 dir = ai_steerPast(self, leader, near, objects, count, spot);
        if (vec3_length(dir) < 0.5f) {
            /* Blocked (someone in the way) close by: good enough, stay put
               instead of trying again every frame */
            if (dist < BLOCKED_SETTLE && !target) ai->settled = dist + SETTLED_UNTIL;
            stand(self);
            return;
        }
        float speed = dist > RUN_TO_LEADER ? runSpeed(self) : fminf(WALK_SPEED, dist * 2.5f + 0.2f);
        ai_walk(self, dir, speed, dt);
        if (dist < 1.0f) turnTowards(self, heading, dt);
        return;
    }

    /* At the spot: step up to the object if there is one; else stand easy,
       now and then looking over at the leader */
    stand(self);
    if (target) {
        self->movement = vec3_scale(heading, APPROACH_SPEED);
        turnTowards(self, heading, dt);
        return;
    }
    turnTowards(self, heading, dt * 0.4f);
    if (sinf(self->body.clock * 0.5f + (float)(ai->rng & 63u)) > 0.3f) {
        Vec3 eye = vec3_add(self->position, vec3(0.0f, self->look.height * 0.93f, 0.0f));
        self->body.look_dir = vec3_normalize(vec3_sub(vec3_add(leader->position, vec3(0.0f, leader->look.height * 0.93f, 0.0f)), eye));
        self->body.look_weight = 0.8f;
    }
}

/* ---------- Holding a ladder ---------- */

#define HOLD_SEES   12.0f  /* m: the helpful notice somebody on a wobbly ladder this far off */
#define HOLD_LINGER 3.0f   /* s: nobody on it any more - they let go */

static Object *climberOn(Object *const *objects, int count, const Object *ladder) {
    for (int i = 0; i < count; i++)
        if (objects[i]->on_ladder == ladder) return objects[i];
    return NULL;
}

bool ai_holdLadder(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *l = self->holding_ladder;
    if (!l) {
        if (self->drunk > 0.6f || Hands_isGrabbing(self) || ai->talking) return false;
        bool helping = ai->mode == AI_HELP && ai->leader;
        bool free_to = ai->mode == AI_IDLE || ai->mode == AI_WANDER;
        if (!helping && !free_to) return false;
        for (int i = 0; i < count; i++) {
            Object *o = objects[i];
            if (o == self || !o->on_ladder || o->on_ladder->held_by || !o->on_ladder->leaning) continue;
            Object *ladder = o->on_ladder;
            bool mine = helping && ai->leader == o; /* (the one we help is going up) */
            bool sees = free_to && ai->character.willing > 0.6f && Ladder_steadiness(ladder) < 0.85f &&
                        flatDistance(ladder->position, self->position) < HOLD_SEES;
            if (!mine && !sees) continue;
            freeHands(self, NULL);
            if (self->hands.item[HAND_LEFT] || self->hands.item[HAND_RIGHT]) return false; /* (can't put it down) */
            Ladder_hold(self, ladder);
            ai->hold_idle = 0.0f;
            Ai_say(self, mine ? "I'll hold the ladder." : "Wait - I'll hold the ladder for you!");
            l = ladder;
            break;
        }
        if (!l) return false;
    }
    /* Nobody on it any more (or it's gone): let go */
    Object *climber = climberOn(objects, count, l);
    ai->hold_idle = climber ? 0.0f : ai->hold_idle + dt;
    if (!l->leaning || l->held_by != self || ai->hold_idle > HOLD_LINGER || Body_isDown(&self->body)) {
        if (l->held_by == self && l->leaning && ai->hold_idle > HOLD_LINGER) Ai_say(self, "There you go.");
        Ladder_hold(self, NULL);
        return false;
    }
    Vec3 facing, spot = Ladder_holdSpot(l, &facing);
    float d = flatDistance(spot, self->position);
    if (d > (ai->hold_there ? 0.2f : 0.1f)) { /* (right up to it; once there, a little shove doesn't send them walking) */
        ai->hold_there = false;
        Vec3 to = vec3_sub(spot, self->position);
        to.y = 0.0f;
        Vec3 dir = d < 0.7f ? vec3_scale(to, 1.0f / d) : ai_steer(self, NULL, objects, count, spot); /* (the last bit straight there) */
        if (vec3_length(dir) < 0.5f) stand(self);
        else ai_walk(self, dir, fminf(WALK_SPEED, d * 2.5f + 0.3f), dt);
        return true;
    }
    ai->hold_there = true;
    stand(self);
    turnTowards(self, facing, dt * 2.0f);
    return true;
}

/* ---------- Up and down ladders ---------- */

#define LADDER_UP_FROM  1.2f   /* the leader this much higher (or lower): by the ladder */
#define LADDER_SEEK     25.0f  /* m: a ladder this near is worth walking to */

bool ai_climbing(Object *self, float dt) {
    Ai *ai = &self->ai;
    if (self->climbing && !self->on_ladder) { /* over the edge at the top */
        if (g_world) Climb_update(g_world, self, dt);
        return true;
    }
    Object *l = self->on_ladder;
    if (!l) return false;
    if (!l->leaning && l->placing <= 0.0f) { Ladder_getOff(self); return false; } /* (it fell: see Ladder_topple) */
    if (ai->ladder_dir == 0.0f) ai->ladder_dir = -1.0f;
    float steady = Ladder_steadiness(l); /* (an unsteady ladder may slip under them too: see the player's ladderSlips) */
    if (steady < 0.98f && self->ladder_off <= 0.0f && g_world) {
        float risk = 0.9f * powf(1.0f - steady, 1.5f) * (0.3f + clampf(self->ladder_at / 3.0f, 0.0f, 1.0f)) * (1.0f + fminf(self->drunk, 1.0f));
        if (random01(ai) < risk * dt) {
            if (l->ladder_wobble <= 0.0f) { l->ladder_wobble = 30.0f; l->shake = 1.2f; Ai_say(self, "Whoa! It's wobbling!"); }
            else {
                Vec3 across = vec3_cross(l->lean_normal, vec3(0.0f, 1.0f, 0.0f));
                Vec3 fall = vec3_scale(across, random01(ai) < 0.5f ? 1.0f : -1.0f);
                float high = self->ladder_at;
                Ladder_topple(g_world, l, fall);
                self->velocity = vec3_add(vec3_scale(fall, 1.2f), vec3(0.0f, 0.5f, 0.0f));
                if (high > 0.6f) Body_fall(&self->body, fall);
                Ai_say(self, "Aaah!");
                return false;
            }
        }
        l->ladder_wobble = fmaxf(0.0f, l->ladder_wobble - dt);
    }
    int r = Ladder_climb(self, ai->ladder_dir, dt);
    if (r == CLIMB_AT_EDGE && (!g_world || !Climb_overLadderTop(g_world, self, l))) ai->ladder_dir = -1.0f; /* (nothing up there: down again) */
    if (ai->ladder_dir > 0.0f && l->lean_top <= 0.0f && r != CLIMB_AT_EDGE && self->ladder_at > 0.5f) { /* (no getting off at the top: down again) */
        ai->ladder_climb_t += dt;
        if (ai->ladder_climb_t > 8.0f) ai->ladder_dir = -1.0f;
    }
    return self->on_ladder != NULL || self->climbing;
}

/* A leaning ladder from about here (feet) up to about there (top), nobody on it. */
static Object *ladderBetween(const Object *self, Object *const *objects, int count, Vec3 low, Vec3 high, bool from_top) {
    Object *best = NULL;
    float best_d = LADDER_SEEK;
    for (int i = 0; i < count; i++) {
        Object *l = objects[i];
        if (l->type != OBJ_LADDER || !l->leaning || l->lean_top <= 0.0f || l->tip_v > 0.0f || climberOn(objects, count, l)) continue;
        Vec3 edge = Ladder_edge(l);
        if (fabsf(l->position.y - low.y) > 0.6f || fabsf(edge.y - high.y) > 1.3f) continue;
        Vec3 near = from_top ? edge : l->position;
        float d = flatDistance(near, self->position);
        if (d < best_d) { best_d = d; best = l; }
    }
    return best;
}

bool ai_followByLadder(Object *self, Object *const *objects, int count, const Object *leader, float dt) {
    Ai *ai = &self->ai;
    if (!g_world || !leader || leader->on_ladder || leader->climbing || !leader->is_grounded || self->holding_ladder) return false;
    float up = leader->position.y - self->position.y;
    if (fabsf(up) < LADDER_UP_FROM) return false;
    if (ai->ladder_seek > 0.0f) { ai->ladder_seek -= dt; return false; }
    bool going_up = up > 0.0f;
    if (going_up && self->height_fear * (1.0f - 0.5f * fminf(self->drunk, 1.0f)) > 0.6f) { /* afraid of heights: not up there */
        if (!ai->heights_said) { ai->heights_said = true; Ai_say(self, "No way - I'm not going up there. I'll wait down here."); }
        ai->ladder_seek = 20.0f;
        return false;
    }
    if (!going_up) ai->heights_said = false;
    Object *l = going_up ? ladderBetween(self, objects, count, self->position, leader->position, false)
                         : ladderBetween(self, objects, count, leader->position, self->position, true);
    if (!l && going_up && up < CLIMB_REACH * self->look.height / 1.75f + 0.2f) { /* no ladder, but in reach: pull up */
        Vec3 probe;
        float top = Climb_ledgeAhead(g_world, self, &probe);
        if (top > -1e8f && fabsf(top - leader->position.y) < 0.6f && self->needs.stamina > 0.2f && self->drunk < 0.6f) {
            freeHands(self, NULL);
            int how = Climb_start(g_world, self, top, probe);
            if (how == CLIMB_HARD || how == CLIMB_EASY_UP) {
                if (how == CLIMB_HARD && self->ai.speech_time <= 0.0f) Ai_say(self, "Hup!");
                return true;
            }
        }
        return false; /* (walks on towards them: up against it, the ledge is ahead) */
    }
    if (!l) { ai->ladder_seek = 3.0f; return false; }
    if (going_up) { /* to its foot, then up */
        Vec3 foot = vec3_add(l->position, vec3_scale(l->lean_normal, 0.5f));
        float d = flatDistance(foot, self->position);
        if (d > 0.45f) {
            Vec3 dir = ai_steer(self, l, objects, count, foot);
            if (vec3_length(dir) < 0.5f) stand(self);
            else ai_walk(self, dir, fminf(WALK_SPEED, d * 2.0f + 0.3f), dt);
            return true;
        }
        freeHands(self, NULL);
        Ladder_mount(self, l, false);
    } else { /* to its top, then down */
        Vec3 edge = Ladder_edge(l);
        Vec3 spot = vec3_sub(edge, vec3_scale(l->lean_normal, 0.5f));
        float d = flatDistance(spot, self->position);
        if (d > 0.5f) { /* (up there: straight to it) */
            Vec3 dir = vec3(spot.x - self->position.x, 0.0f, spot.z - self->position.z);
            ai_walk(self, vec3_scale(dir, 1.0f / fmaxf(d, 1e-3f)), fminf(WALK_SPEED * 0.6f, d * 2.0f + 0.2f), dt);
            return true;
        }
        freeHands(self, NULL);
        Ladder_mount(self, l, true);
    }
    ai->ladder_dir = going_up ? 1.0f : -1.0f;
    stand(self);
    return true;
}