Nimbin[12]?Games / Entropie / entropie/src/people/ai.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.c 129.2 KB · 2642 lines raw
/*
 * People on their own: walking about without bumping into things, pausing,
 * fetching a beer, waking sleepers, coming over to talk. Ai_update decides
 * what everybody does each frame.
 */

#include "people/ai_internal.h"
#include "people/pockets.h"
#include "people/condition.h"
#include "world/physics.h"
#include "world/nav.h"
#include "world/grid.h"
#include "world/world.h"
#include "world/clothes.h"
#include "world/car.h"
#include "world/tent.h"
#include "world/site.h"
#include "render/shapes.h"

/* ---------- Small helpers ---------- */

/* Walks along dir (unit, horizontal) at speed; the body turns after it. */
/* Up on a roof: careful steps (the fearful creep, the drunk and dim stroll about and stumble). */
#define HIGH_PACE     1.5f  /* m/s up there at most (nobody runs on a roof) ... */
#define HIGH_SLOW     0.5f  /* ... slower by this much at most (all caution) */
#define HIGH_STUMBLE  0.12f /* stumbles per second, drunk and careless */

static float upHighSpeed(Object *o, float speed, float dt) {
    Ai *ai = &o->ai;
    if (!g_world || !Object_upHigh(o, Terrain_height(&g_world->terrain, o->position.x, o->position.z))) {
        ai->high_time = fmaxf(0.0f, ai->high_time - dt);
        if (ai->high_time <= 0.0f) ai->high_said = false;
        return speed;
    }
    ai->high_time = fminf(ai->high_time + dt, 5.0f);
    float c = Object_caution(o), d = fminf(o->drunk, 1.0f);
    if (!ai->high_said && ai->high_time > 1.0f && ai->speech_time <= 0.0f) {
        ai->high_said = true;
        if (c > 0.6f) Ai_say(o, "Don't look down... don't look down...");
        else if (c < 0.2f && d > 0.5f) Ai_say(o, "Woo! Look at me up here!");
    }
    if (c < 0.25f && d > 0.5f && o->body.stumble <= 0.0f && dt > 0.0f &&
        random01(ai) < HIGH_STUMBLE * d * dt)
        Body_trip(&o->body, 0, Object_facing(o), 0.4f + 0.3f * d);
    return fminf(speed, HIGH_PACE * (1.0f - HIGH_SLOW * c));
}

void ai_walk(Object *o, Vec3 dir, float speed, float dt) {
    speed *= Object_vigor(o) * (o->tempo > 0.0f ? 0.8f + 0.2f * o->tempo : 1.0f); /* (their own pace) */
    speed = upHighSpeed(o, speed, dt);
    /* Drunk people wobble off course */
    float wobble = sinf(o->body.clock * 0.7f) + 0.6f * sinf(o->body.clock * 1.9f + 1.3f);
    dir = rotateY(dir, wobble * 0.5f * fminf(o->drunk, 1.0f));

    /* The way you walk turns smoothly: no zigzag from one frame to the next */
    Ai *ai = &o->ai;
    if (vec3_length(ai->walk_dir) > 0.5f && dt > 0.0f) {
        float a0 = atan2f(ai->walk_dir.z, ai->walk_dir.x), a1 = atan2f(dir.z, dir.x);
        float d = remainderf(a1 - a0, 2.0f * PI_F), most = WALK_TURN * dt;
        if (fabsf(d) > most) {
            float a = a0 + (d > 0.0f ? most : -most);
            dir = vec3(cosf(a), 0.0f, sinf(a));
        }
    }
    ai->walk_dir = dir;
    if (o->pace > 0.05f) speed *= fminf(o->pace, 1.1f); /* (the old and the calm walk slower) */
    speed *= Hands_moveFactor(o); /* (a crate in both arms, something dragged: slower, like you) */
    o->movement = vec3_scale(dir, speed);
    turnTowards(o, dir, dt);
    Vec3 f = Object_facing(o);
    Vec3 r = vec3_normalize(vec3_cross(f, UP));
    float amount = fminf(speed / WALK_SPEED, 1.0f);
    o->move_local = vec3(vec3_dot(dir, r) * amount, 0.0f, vec3_dot(dir, f) * amount);
}

/* ---------- Obstacles ---------- */

static bool insideCollider(const Object *o, int i, Vec3 p, float margin) {
    const Collider *c = &o->colliders[i];
    Vec3 rel = vec3_sub(p, Object_colliderCenter(o, i));
    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
    for (int k = 0; k < 3; k++)
        if (fabsf(vec3_dot(rel, c->axes[k])) > half[k] + margin) return false;
    return true;
}

/* Would a person standing at p bump into something? Ignores self and ignore. */
static bool  g_avoid_small;  /* steering: keep off small things lying on the ground too */
static float g_dt;           /* this frame */
#define STALL_TIME 3.0f      /* no closer to something for this long: leave it */

#define PEE_SEARCH    40.0f  /* how far they go looking for a spot to pee (m) */
#define TOILET_WALK   40.0f  /* the toilet further than this: not worth it */
#define CROWD_RANGE   30.0f  /* who they hide from */
static bool findPeeSpot(Object *self, Object *const *objects, int count);

static struct Grid *g_grid;

void Ai_useGrid(struct Grid *grid) { g_grid = grid; }
struct World *g_world;
void Ai_useWorld(struct World *world) { g_world = world; }

/* Something solid in the way, long (going round is a detour) and low enough to
   step onto: people step up onto it and on (see physics: startStepOver). Up to
   ONTO_HIGH anything (they'd stride over it anyway); higher, as high as they go
   up (Object_stepUpMost), if it's deep enough to land on (ONTO_DEEP). */
#define ONTO_LONG  1.5f  /* m along at least */
#define ONTO_HIGH  0.55f /* m above the feet: what they would stride over anyway */
#define ONTO_DEEP  0.5f  /* m across at least, to step up onto it when higher */

static bool longToStepOnto(const Object *self, const Object *o) {
    if (o->type == OBJ_WALL || o->is_held || o->is_character || o->collider_count == 0 || !Physics_standsOn(o)) return false; /* (a building's steps: see the stairs) */
    const Collider *c = &o->colliders[0];
    float top = Object_colliderCenter(o, 0).y, longest = 0.0f, shortest = 1e9f;
    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
    for (int k = 0; k < 3; k++) {
        top += fabsf(c->axes[k].y) * half[k];
        if (fabsf(c->axes[k].y) > 0.95f) continue; /* (the upright one) */
        float flat = 2.0f * half[k] * sqrtf(c->axes[k].x * c->axes[k].x + c->axes[k].z * c->axes[k].z);
        longest = fmaxf(longest, flat);
        shortest = fminf(shortest, flat);
    }
    float high = top - self->position.y;
    if (longest < ONTO_LONG) return false;
    return high <= ONTO_HIGH || (high <= Object_stepUpMost(self) && shortest >= ONTO_DEEP * self->look.height / 1.75f);
}

#define WALL_RADIUS 0.24f /* m kept from a building's walls (a doorway 0.9 m wide leaves 0.4 m to aim through) */
#define STAIR_FIRST 0.3f  /* m: a step up right here ... */
#define STAIR_SLOPE 0.65f /* ... and this much more per m further on: stairs, not a wall */

bool ai_blockedBy(const Object *self, const Object *ignore, const Object *ignore2,
                      Object *const *objects, int count, Vec3 p) {
    Object *near[512]; /* (only what's near p, from the cell grid) */
    if (g_grid) {
        int n = Grid_nearObjects(g_grid, objects, count, p.x, p.z, PERSON_RADIUS + 0.5f, near, 512);
        if (n >= 0) { objects = near; count = n; }
    }
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (o == self || o == ignore || o == ignore2) continue;
        if (longToStepOnto(self, o)) continue; /* (a log, a fallen tree: onto it and on, no long way round) */
        if (o->is_held) { /* what others carry: a ladder or a log is walked round, a bottle isn't */
            if (o->collider_count == 0 || self->hands.item[0] == o || self->hands.item[1] == o) continue;
            Vec3 e = o->colliders[0].half_extents;
            if (2.0f * fmaxf(e.x, fmaxf(e.y, e.z)) < 0.62f) continue;
            for (int c = 0; c < o->collider_count; c++)
                for (int k = 0; k < 3; k++)
                    if (insideCollider(o, c, vec3(p.x, self->position.y + 0.4f + 0.6f * (float)k, p.z), BODY_RADIUS)) return true;
            continue;
        }
        if (!o->is_static && !o->is_character && o->weight < 10.0f) { /* small things on the ground */
            if (!g_avoid_small || o->collider_count == 0 || o->type == OBJ_DOG) continue;
            if (Object_bottom(o) > self->position.y + 0.3f) continue; /* (on a table or so) */
            if (flatDistance(Object_colliderCenter(o, 0), p) < 0.32f) return true; /* rather not step on it */
            continue;
        }
        if (o->is_character) {
            if (flatDistance(o->position, p) < PERSON_RADIUS) return true;
            continue;
        }
        int parts = o->type == OBJ_TREE && !o->toppled ? 1 : o->collider_count; /* (a standing tree: its trunk; under the crown you just duck) */
        for (int c = 0; c < parts; c++) {
            /* Low things (a woodpile's sleepers, a stump) are stepped over */
            const Collider *box = &o->colliders[c];
            if (flatDistance(Object_colliderCenter(o, c), p) > vec3_length(box->half_extents) + BODY_RADIUS) continue;
            float top = Object_colliderCenter(o, c).y + fabsf(box->axes[0].y) * box->half_extents.x +
                        fabsf(box->axes[1].y) * box->half_extents.y + fabsf(box->axes[2].y) * box->half_extents.z;
            if (top < self->position.y + 0.25f) continue;
            /* Over our heads here and a way off (a floor above a stair going down): there we'll be lower */
            float bottom = 2.0f * Object_colliderCenter(o, c).y - top;
            if (o->type == OBJ_WALL && bottom > self->position.y + 0.3f && flatDistance(self->position, p) > 0.6f) continue;
            /* A building's floor or steps: walked up (a stair rises about STAIR_SLOPE) */
            if (o->type == OBJ_WALL && o->variant != 3 && box->axes[1].y > 0.99f && box->half_extents.x >= 0.1f && box->half_extents.z >= 0.1f &&
                top <= self->position.y + STAIR_FIRST + STAIR_SLOPE * flatDistance(self->position, p))
                continue;
            /* Knee, waist and head height above the feet (a building's walls: closer, or no doorway is wide enough) */
            float clear = o->type == OBJ_WALL ? WALL_RADIUS : BODY_RADIUS;
            for (int k = 0; k < 3; k++) {
                Vec3 q = vec3(p.x, self->position.y + 0.4f + 0.6f * (float)k, p.z);
                if (insideCollider(o, c, q, clear)) return true;
            }
        }
    }
    return false;
}

static bool pathFree(const Object *self, const Object *ignore, const Object *ignore2,
                     Object *const *objects, int count, Vec3 dir, float distance) {
    for (int k = 1; k <= 3; k++) {
        Vec3 p = vec3_add(self->position, vec3_scale(dir, distance * (float)k / 3.0f));
        if (ai_blockedBy(self, ignore, ignore2, objects, count, p)) return false;
    }
    return true;
}

/*
 * Direction towards goal that is free for the next stretch: straight if
 * possible, else the smallest detour, preferring the side we went last time
 * (so we don't dither in front of an obstacle). Zero if boxed in.
 */
Vec3 ai_steerPast(Object *self, const Object *ignore, const Object *ignore2,
                      Object *const *objects, int count, Vec3 goal) {
    Vec3 to = flat(vec3_sub(goal, self->position));
    float dist = vec3_length(to);
    if (dist < 1e-3f) return vec3_zero();
    Vec3 dir = vec3_scale(to, 1.0f / dist);
    float ahead = fminf(dist, LOOK_AHEAD);
    if (ahead < 0.3f) return dir;

    static const float DETOURS[] = { 0.0f, 25.0f, 50.0f, 80.0f, 115.0f, 150.0f };
    Ai *ai = &self->ai;
    /* Keep going round an obstacle for a moment once a way round was chosen
       (re-deciding every frame made people dither left and right) */
    if (ai->steer_hold > 0.0f && vec3_dot(ai->steer_dir, dir) > -0.9f &&
        !pathFree(self, ignore, ignore2, objects, count, dir, ahead) &&
        pathFree(self, ignore, ignore2, objects, count, ai->steer_dir, ahead))
        return ai->steer_dir;
    for (unsigned i = 0; i < sizeof(DETOURS) / sizeof(DETOURS[0]); i++) {
        for (int s = 0; s < (i ? 2 : 1); s++) {
            float side = s ? -ai->detour : ai->detour;
            Vec3 d = rotateY(dir, deg2rad(DETOURS[i]) * side);
            if (pathFree(self, ignore, ignore2, objects, count, d, ahead)) {
                if (i) {
                    ai->detour = side;
                    ai->steer_dir = d;
                    ai->steer_hold = 0.4f + 0.1f * (float)i;
                }
                return d;
            }
        }
    }
    /* Already too close to something (squeezed in by a box, a trunk): out the way that's free further on */
    if (ai_blockedBy(self, ignore, ignore2, objects, count, self->position)) {
        for (unsigned i = 0; i < sizeof(DETOURS) / sizeof(DETOURS[0]); i++)
            for (int s = 0; s < (i ? 2 : 1); s++) {
                Vec3 d = rotateY(dir, deg2rad(DETOURS[i]) * (s ? -ai->detour : ai->detour));
                if (!ai_blockedBy(self, ignore, ignore2, objects, count, vec3_add(self->position, vec3_scale(d, ahead * 2.0f / 3.0f))) &&
                    !ai_blockedBy(self, ignore, ignore2, objects, count, vec3_add(self->position, vec3_scale(d, ahead))))
                    return d;
            }
    }
    /* Boxed in: next time try the other side first */
    ai->detour = -ai->detour;
    ai->steer_hold = 0.0f;
    return vec3_zero();
}

/* Ways round fixed things (see world/nav.h) */
#define PATH_REACHED 0.7f   /* this close to a waypoint: on to the next */
#define PATH_MOVED   1.5f   /* the goal moved this far: the way is old */
#define PATH_FROM    1.5f   /* closer than this: no looking for a way round, just go */
#define PATH_NONE    4.0f   /* no way there: don't look again for this long */

/* How far there still is to go: along the way round if there is one. */
static float stillToGo(const Object *self, Vec3 goal) {
    const Ai *ai = &self->ai;
    if (!ai->path_len) return flatDistance(goal, self->position);
    float d = flatDistance(ai->path[ai->path_at], self->position);
    for (int i = ai->path_at + 1; i < ai->path_len; i++) d += flatDistance(ai->path[i], ai->path[i - 1]);
    return d + flatDistance(goal, ai->path[ai->path_len - 1]);
}

/* Looks for a way round to goal (not again for a while if there is none). */
static bool findWay(Object *self, Vec3 goal) {
    Ai *ai = &self->ai;
    if (ai->path_none > 0.0f && flatDistance(goal, ai->path_goal) < PATH_MOVED) return false;
    ai->path_goal = goal;
    ai->path_at = 0;
    ai->path_len = Nav_findPath(self->position, goal, ai->path, AI_PATH_MAX);
    if (!ai->path_len) ai->path_none = PATH_NONE;
    ai->steer_best = stillToGo(self, goal); /* (a way round is longer: no "not getting closer") */
    ai->steer_stall = 0.0f;
    return ai->path_len > 0;
}

static Vec3 steerTowards(Object *self, const Object *ignore, Object *const *objects, int count, Vec3 aim) {
    g_avoid_small = true;
    Vec3 dir = ai_steerPast(self, ignore, NULL, objects, count, aim);
    g_avoid_small = false;
    if (vec3_length(dir) < 0.5f) dir = ai_steerPast(self, ignore, NULL, objects, count, aim);
    return dir;
}

/*
 * Steering towards a goal (ignore: what we're going for). First a way round
 * that doesn't step on the small things lying about; if there's none, any way.
 * Walking straight leads into a corner (or gets no closer): a way round the
 * fixed things from the walking map. Not getting any closer to something we
 * want to pick up for a few seconds (it lies where we can't get at it):
 * leave it and take another one.
 */
Vec3 ai_steer(Object *self, const Object *ignore, Object *const *objects, int count, Vec3 goal) {
    Ai *ai = &self->ai;
    /* An old way (the goal is somewhere else now) is forgotten; a waypoint reached: on to the next */
    if (ai->path_len && flatDistance(goal, ai->path_goal) > PATH_MOVED) ai->path_len = 0;
    while (ai->path_len && flatDistance(ai->path[ai->path_at], self->position) < PATH_REACHED)
        if (++ai->path_at >= ai->path_len) ai->path_len = 0;

    float d = stillToGo(self, goal);
    if (ignore != ai->steer_target || d < ai->steer_best - 0.1f) {
        ai->steer_target = ignore;
        ai->steer_best = d;
        ai->steer_stall = 0.0f;
    } else if ((ai->steer_stall += g_dt) > STALL_TIME * 0.5f && !ai->path_len && d > PATH_FROM && findWay(self, goal)) {
        /* sliding along a wall without getting closer: the way round */
    } else if (ai->steer_stall > STALL_TIME && ignore && !ignore->is_character && !ignore->is_static) {
        skipTarget(ai, (Object *)ignore);
        if (ai->tool == ignore) ai->tool = NULL;
        ai->steer_stall = 0.0f;
        ai->steer_best = 1e9f;
        ai->path_len = 0;
        return vec3_zero();
    }
    if (ai->boxed > 0.0f) return vec3_zero();
    Vec3 dir = steerTowards(self, ignore, objects, count, ai->path_len ? ai->path[ai->path_at] : goal);
    if (vec3_length(dir) < 0.5f && !ai->path_len && d > PATH_FROM && findWay(self, goal))
        dir = steerTowards(self, ignore, objects, count, ai->path[ai->path_at]);
    if (vec3_length(dir) < 0.5f) ai->boxed = 0.5f; /* stands a moment instead of edging back and forth */
    return dir;
}

/* ---------- Idle and wander ---------- */

static void rest(Ai *ai, float min, float max) {
    ai->mode = AI_IDLE;
    ai->timer = min + (max - min) * random01(ai);
}

/* ---------- At a table ---------- */

#define TABLE_WALK     35.0f /* m: a free chair at a table this near is worth the walk */
#define TABLE_EAT      0.75f /* chance food they pick up is eaten sitting at a table (a free chair there) ... */
#define TABLE_BEER     0.3f  /* ... a beer drunk there */
#define TABLE_SIT      0.06f /* idle (no fire): now and then over to a table to sit a while */
#define TABLE_STAY_MIN 25.0f
#define TABLE_STAY_MAX 70.0f
#define TABLE_CHAT     14.0f /* s between words with others sitting there */
#define TABLE_NEAR     2.6f  /* m: sitting this close counts as at the same table */
#define TABLE_SEAT     0.5f  /* m: within this of the chair they sit down */

#define PICK_ONE(list, ai) (list)[(int)(random01(ai) * (float)(sizeof(list) / sizeof((list)[0]))) % (int)(sizeof(list) / sizeof((list)[0]))]
static const char *const TABLE_OPEN[]  = { "Ah, good to sit for a bit.", "Mind if I join you?", "That's better.", "Nice table, this." };
static const char *const TABLE_EATING[] = { "Mmh. Not bad at all.", "I was starving.", "Good stuff.", "Want a bite?" };
static const char *const TABLE_REPLY[] = { "Mhm.", "Sure, sit down.", "Ha, yes.", "Enjoy.", "Right?" };

/* A chair standing at a table (facing it, on the same floor). */
static bool chairAtTable(const Object *chair, Object *const *objects, int count) {
    Vec3 front = chair->colliders[0].axes[2];
    for (int i = 0; i < count; i++) {
        const Object *t = objects[i];
        if (t->type != OBJ_FURNITURE || t->variant != FURN_TABLE || t->collider_count == 0) continue;
        if (fabsf(t->position.y - chair->position.y) > 0.4f) continue;
        const Collider *c = &t->colliders[0];
        Vec3 rel = vec3_sub(chair->position, Object_colliderCenter(t, 0));
        if (fabsf(vec3_dot(rel, c->axes[0])) < c->half_extents.x + 0.8f && fabsf(vec3_dot(rel, c->axes[2])) < c->half_extents.z + 0.8f &&
            vec3_dot(front, rel) < 0.0f)
            return true;
    }
    return false;
}

static bool chairTaken(const Object *self, const Object *chair, Object *const *objects, int count) {
    for (int i = 0; i < count; i++) {
        const Object *c = objects[i];
        if (c == self || !c->is_character) continue;
        if (c->seat == chair || (c->type == OBJ_HUMAN && c->ai.mode == AI_TABLE && c->ai.target == chair)) return true;
    }
    return false;
}

/* The nearest free chair at a table (another floor counts as further). */
static Object *freeTableChair(const Object *self, Object *const *objects, int count, float range) {
    Object *best = NULL;
    float best_d = range;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_FURNITURE || o->variant != FURN_CHAIR || ai_skipped(&self->ai, o)) continue;
        float d = flatDistance(o->position, self->position) + 3.0f * fabsf(o->position.y - self->position.y);
        if (d >= best_d || chairTaken(self, o, objects, count) || !chairAtTable(o, objects, count)) continue;
        best_d = d;
        best = o;
    }
    return best;
}

static void goToTable(Object *self, Object *chair) {
    Ai *ai = &self->ai;
    ai->mode = AI_TABLE;
    ai->target = chair;
    ai->timer = 0.0f;
    ai->table_stay = TABLE_STAY_MIN + (TABLE_STAY_MAX - TABLE_STAY_MIN) * random01(ai);
    ai->table_pause = DRINK_PAUSE;
    ai->table_chat = 2.0f + 4.0f * random01(ai);
    setGoal(ai, chair->position);
}

/* Food or a bottle in the main hand (what they eat or drink at the table). */
static Object *mealInHand(const Object *self) {
    const Hands *h = &self->hands;
    Object *m = h->state[self->main_hand] == HAND_HOLDING ? h->item[self->main_hand] : NULL;
    return m && (Object_isFood(m) || Object_isBottle(m)) ? m : NULL;
}

static void leaveTable(Object *self) {
    self->sitting = false; /* (gets up: Object_leaveSeat) */
    self->ai.target = NULL;
    rest(&self->ai, 1.0f, 3.0f);
}

/* Somebody else sitting at the same table. */
static Object *tableMate(const Object *self, Object *const *objects, int count) {
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o != self && o->is_character && o->sitting && o->seat && o->seat->type == OBJ_FURNITURE &&
            flatDistance(o->position, self->position) < TABLE_NEAR && fabsf(o->position.y - self->position.y) < 0.5f)
            return o;
    }
    return NULL;
}

static void updateTable(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *chair = ai->target;
    Object *meal = mealInHand(self);
    const int hand = self->main_hand;
    if (self->hands.state[hand] == HAND_REACHING) { stand(self); return; } /* (still picking it up) */
    if (!chair) { leaveTable(self); return; }

    /* Over to the chair, then sit down on it */
    if (!self->sitting || self->seat != chair) {
        if (chairTaken(self, chair, objects, count)) { /* somebody was quicker: another one, or eat standing */
            Object *other = freeTableChair(self, objects, count, TABLE_WALK * 0.5f);
            if (other) { ai->target = other; setGoal(ai, other->position); return; }
            ai->target = NULL;
            if (meal) { ai->mode = AI_DRINK; ai->timer = 0.0f; ai->target = meal; } else rest(ai, 1.0f, 2.0f);
            return;
        }
        ai->timer += dt;
        if (ai->timer > FETCH_TIMEOUT * 2.0f) { /* can't get there */
            skipTarget(ai, chair);
            ai->target = NULL;
            if (meal) { ai->mode = AI_DRINK; ai->timer = 0.0f; ai->target = meal; } else rest(ai, 1.0f, 2.0f);
            return;
        }
        float gap = Hands_gapFrom(self->position, self->look.height, chair);
        if (gap < TABLE_SEAT && fabsf(chair->position.y - self->position.y) < 0.4f) {
            stand(self);
            Object_sit(self, chair, self->position);
            ai->timer = 0.0f;
            return;
        }
        Vec3 dir = ai_steer(self, chair, objects, count, chair->position);
        if (vec3_length(dir) < 0.5f) { stand(self); return; }
        ai_walk(self, dir, fminf(WALK_SPEED * 0.7f, gap * 1.5f + 0.3f), dt);
        return;
    }

    stand(self);
    if (self->sit_moving) return; /* (sitting down) */
    ai->timer += dt;
    Object *mate = tableMate(self, objects, count);
    if (mate) { /* look at them now and then */
        Vec3 to = vec3_sub(vec3_add(mate->position, vec3(0.0f, mate->look.height * 0.9f, 0.0f)),
                           vec3_add(self->position, vec3(0.0f, self->look.height * 0.6f, 0.0f)));
        self->body.look_dir = vec3_normalize(to);
        self->body.look_weight = fmodf(ai->timer, 9.0f) < 4.0f ? 0.7f : 0.0f;
    }

    /* A bite or a sip now and then */
    if (meal && !Hands_isDrinking(self) && (ai->table_pause -= dt) <= 0.0f) {
        ai->table_pause = DRINK_PAUSE * (Object_isFood(meal) ? 2.5f : 4.0f) * (0.7f + 0.6f * random01(ai));
        if (ai->sips_left > 0 && meal->sips > 0 && Hands_drink(self)) ai->sips_left--;
        else if (Camp_doneWithBottle(self, hand, objects, count, random01(ai)) == DONE_NONE) Hands_drop(self, hand); /* (on the table, mostly) */
    }

    /* A word with the others */
    if ((ai->table_chat -= dt) <= 0.0f) {
        ai->table_chat = TABLE_CHAT * (0.7f + 0.6f * random01(ai));
        if (mate && ai->speech_time <= 0.0f && !mate->ai.talking) {
            Ai_say(self, meal && Object_isFood(meal) ? PICK_ONE(TABLE_EATING, ai) : PICK_ONE(TABLE_OPEN, ai));
            if (mate->type == OBJ_HUMAN && mate->ai.speech_time <= 0.0f && random01(ai) < 0.6f) Ai_say(mate, PICK_ONE(TABLE_REPLY, ai));
        }
    }

    /* Up again: sat long enough (and finished), or something more pressing */
    const Needs *n = &self->needs;
    bool pressing = n->bladder > 0.8f || n->sleepiness > 0.85f || (!meal && (n->hunger > 0.6f || n->thirst > 0.6f));
    if (pressing || (ai->timer > ai->table_stay && !meal)) leaveTable(self);
}

/* Nearest loose bottle (or food) with something left that nobody else is going for.
   Thirsty people prefer water. */
static Object *findConsumable(const Object *self, Object *const *objects, int count, Want want) {
    Object *best = NULL;
    float best_dist = want == WANT_BEER ? DRINK_RANGE : DRINK_RANGE * 2.0f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        bool crate = o->type == OBJ_CRATE; /* (a crate: a bottle out of it) */
        bool fits = want == WANT_FOOD ? Object_isFood(o) : Object_isBottle(o);
        if (want == WANT_BEER && o->alcohol <= 0.0f) fits = false;
        if (crate) fits = want != WANT_FOOD && o->stored - o->crate_empties > 0 && (want != WANT_BEER || o->variant == 0); /* (thirsty: water, soda or mate too) */
        if (!fits || o->is_held || (!crate && o->sips <= 0) || o->broken) continue;
        if (ai_skipped(&self->ai, o)) continue; /* couldn't get at it */
        bool taken = false;
        for (int j = 0; j < count; j++) {
            const Object *c = objects[j];
            if (c != self && c->is_character && c->ai.target == o) taken = true;
        }
        float d = flatDistance(o->position, self->position) + (want == WANT_DRINK && (crate ? o->variant == 0 : o->alcohol > 0.0f) ? 8.0f : 0.0f);
        if (crate) taken = false; /* (enough in it for all) */
        if (!taken && d < best_dist) {
            best_dist = d;
            best = o;
        }
    }
    return best;
}

/* Nearest apple crate (food) or water barrel (drink). */
static Object *findSupply(const Object *self, Object *const *objects, int count, Want want) {
    Object *best = NULL;
    float best_dist = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_SUPPLY || o->dispenses != (want == WANT_FOOD ? OBJ_APPLE : OBJ_WATER) || o->stock < 0 || o->is_held) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_dist) {
            best_dist = d;
            best = o;
        }
    }
    return best;
}

/* Thirsty or hungry: go for something lying around, else to the supplies. */
static bool seekNeed(Object *self, Object *const *objects, int count, Want want) {
    Ai *ai = &self->ai;
    Object *item = findConsumable(self, objects, count, want);
    if (item) {
        ai->mode = AI_FETCH;
        ai->target = item;
        ai->timer = 0.0f;
        setGoal(ai, item->position);
        return true;
    }
    Object *supply = findSupply(self, objects, count, want);
    if (!supply) return false;
    ai->mode = AI_SUPPLY;
    ai->target = supply;
    ai->timer = 0.0f;
    ai->sips_left = 0; /* 0 = on the way, 1 = eating / drinking */
    setGoal(ai, supply->position);
    return true;
}

#define FIRE_GATHER 45.0f  /* people come over to a fire burning this close */

static bool pickWanderGoal(Object *self, Object *const *objects, int count) {
    Ai *ai = &self->ai;
    for (int tries = 0; tries < 6; tries++) {
        float angle = random01(ai) * 2.0f * PI_F;
        float radius = 2.0f + random01(ai) * (WANDER_RADIUS - 2.0f);
        Vec3 goal = vec3_add(ai->home, vec3(cosf(angle) * radius, 0.0f, sinf(angle) * radius));
        if (Object_stinkAt(objects, count, goal) > 0.1f) continue; /* not over there */
        if (!self->lives_here && Site_inHouse(goal.x, goal.z)) continue; /* (the house is theirs: not for a stroll) */
        if (!blockedAt(self, NULL, objects, count, goal)) {
            setGoal(ai, goal);
            return true;
        }
    }
    return false;
}

/* Wood to start on: lying wood (logs, felled trees), or a standing pine. */
static Object *nearestWood(const Object *self, Object *const *objects, int count, bool standing) {
    Object *best = NULL;
    float best_d = 60.0f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        bool fits = standing ? (o->type == OBJ_TREE && !o->toppled && !o->variant)
                             : (Tools_cuttable(o) || Tools_splittable(o) || (o->type == OBJ_TREE && o->toppled));
        if (!fits || o->is_held) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) {
            best_d = d;
            best = o;
        }
    }
    return best;
}

static void updateIdle(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    stand(self);

    /* By a burning fire: sit down, look into the flames */
    Object *fire = Camp_fireNear(objects, count, self->position, FIRE_GATHER, true);
    bool by_fire = fire && flatDistance(fire->position, self->position) < FIRE_SIT_DISTANCE + 1.2f;
    if (by_fire && !self->sitting) Object_sit(self, Object_seatNear(objects, count, self->position, 1.2f), fire->position);
    if (!by_fire && !self->seat && !ai->greet) self->sitting = false; /* (on a bench or a chair, or with the others at the fire: they stay put a while) */
    Object *player = findPlayer(objects, count);
    if (by_fire) {
        Vec3 to = vec3_sub(vec3_add(fire->position, vec3(0.0f, 0.4f, 0.0f)),
                           vec3_add(self->position, vec3(0.0f, self->look.height * 0.6f, 0.0f)));
        self->body.look_dir = vec3_normalize(to);
        self->body.look_weight = 0.8f;
        if (self->sit_style != SIT_SEAT) turnTowards(self, to, dt * 0.5f);
    } else if (player && flatDistance(player->position, self->position) < NOTICE_DISTANCE) {
        /* Somebody near: look at them (the shy only half way), turn slowly towards them */
        Vec3 to = vec3_sub(vec3_add(player->position, vec3(0.0f, player->look.height * 0.93f, 0.0f)),
                           vec3_add(self->position, vec3(0.0f, self->look.height * 0.93f, 0.0f)));
        self->body.look_dir = vec3_normalize(to);
        self->body.look_weight = self->temperament.pride < -0.3f ? 0.4f : 1.0f;
        turnTowards(self, to, dt * 0.25f);
    }

    if ((ai->timer -= dt) > 0.0f) return;

    /* What the body asks for first */
    const Needs *n = &self->needs;
    bool hands_free = self->hands.state[HAND_LEFT] == HAND_FREE && self->hands.state[HAND_RIGHT] == HAND_FREE;
    if (n->sleepiness > 0.8f) { /* bedtime: lie down right here */
        Body_lieDown(&self->body);
        rest(ai, 1.0f, 2.0f);
        return;
    }
    if (n->bladder > 0.7f) { /* the toilet if there's one, else behind something */
        Object *toilet = NULL;
        float best = TOILET_WALK;
        for (int i = 0; i < count; i++) {
            Object *o = objects[i];
            if (o->type != OBJ_FURNITURE || o->variant != FURN_TOILET) continue;
            float d = flatDistance(o->position, self->position);
            if (d < best) { best = d; toilet = o; }
        }
        /* in use, somebody already on the way, or too urgent: a decent spot outside */
        bool queue = false;
        for (int i = 0; i < count && toilet; i++)
            if (objects[i] != self && objects[i]->is_character && objects[i]->ai.mode == AI_PEE && objects[i]->ai.target == toilet) queue = true;
        if ((!toilet || toilet->dog.mode || queue || n->bladder > 0.95f) && findPeeSpot(self, objects, count)) return;
        if (toilet) {
            Vec3 front = toilet->colliders[0].axes[2];
            ai->mode = AI_PEE;
            ai->target = toilet;
            ai->timer = 0.0f;
            setGoal(ai, vec3_add(toilet->position, vec3_scale(vec3(front.x, 0.0f, front.z), toilet->size.z * 0.5f + 0.6f)));
            return;
        }
        if (pickWanderGoal(self, objects, count)) {
            ai->mode = AI_PEE;
            ai->target = NULL;
            return;
        }
    }
    if (hands_free && n->thirst > 0.5f && seekNeed(self, objects, count, WANT_DRINK)) return;
    if (hands_free && n->hunger > 0.5f && seekNeed(self, objects, count, WANT_FOOD)) return;
    bool curious = self->female && !self->urinella && self->urinella_tried == 0 && n->bladder > 0.35f && n->bladder < 0.65f &&
                   random01(ai) < 0.1f + 0.2f * fmaxf(self->temperament.energy, 0.0f); /* (never tried one: why not) */
    if ((self->urinella && self->urinellas == 0 && n->bladder < 0.6f) || curious) { /* out of Urinellas (or curious): the box */
        Object *box = ai_nearestOf(self, objects, count, OBJ_SUPPLY, OBJ_URINELLA);
        if (box && flatDistance(box->position, self->position) < (curious ? 30.0f : 80.0f)) {
            if (curious) Ai_say(self, "Maybe I'll try one of those Urinellas.");
            ai->mode = AI_SUPPLY;
            ai->target = box;
            ai->timer = 0.0f;
            ai->sips_left = 0;
            return;
        }
    }

    /* Their own idea: a job they love that needs doing */
    if (hands_free && self->needs.stamina > 0.55f && self->drunk < 0.3f && random01(ai) < 0.06f) {
        const Character *c = &ai->character;
        for (int o = ORDER_FIREWOOD; o < ORDER_COUNT; o++) {
            if (c->likes[o] < 0.55f || !Ai_needed(self, objects, count, (Order)o)) continue;
            if ((o == ORDER_FIREWOOD || o == ORDER_TREES) && !ai->knows_wood) continue;
            Vec3 area = self->position;
            Object *any_fire = Camp_fireNear(objects, count, self->position, 80.0f, false);
            if (o == ORDER_FIRE && any_fire) area = any_fire->position;
            if (o == ORDER_FIREWOOD || o == ORDER_TREES) {
                Object *wood = nearestWood(self, objects, count, o == ORDER_TREES);
                if (!wood) continue;
                area = Object_colliderCenter(wood, 0);
            }
            static const char *const START[ORDER_COUNT] = {
                [ORDER_FIREWOOD] = "I'll make some firewood.", [ORDER_TREES] = "I'll go and cut a tree or two.",
                [ORDER_TRASH] = "I can't stand this mess. I'll clean up.", [ORDER_FIRE] = "I'll get the fire going.",
            };
            Ai_say(self, START[o]);
            Ai_order(self, (Order)o, area);
            return;
        }
    }

    /* A fire burns: go and sit by it, and stay a while */
    if (by_fire && random01(ai) < 0.85f) {
        rest(ai, 20.0f, 60.0f);
        return;
    }
    if (fire && !by_fire && random01(ai) < 0.8f && pickFireSpot(self, objects, count, fire)) {
        ai->mode = AI_WANDER;
        return;
    }

    /* Over to a table to sit a while */
    if (!fire && random01(ai) < TABLE_SIT) {
        Object *chair = freeTableChair(self, objects, count, TABLE_WALK);
        if (chair) {
            goToTable(self, chair);
            ai->sips_left = 0;
            return;
        }
    }

    Object *bottle = NULL;
    if (hands_free && self->drunk < DRINK_MAX_DRUNK && random01(ai) < DRINK_CHANCE)
        bottle = findConsumable(self, objects, count, WANT_BEER);
    if (bottle) {
        ai->mode = AI_FETCH;
        ai->target = bottle;
        ai->timer = 0.0f;
        setGoal(ai, bottle->position);
    } else if (pickWanderGoal(self, objects, count)) {
        ai->mode = AI_WANDER;
    } else {
        rest(ai, 1.0f, 2.0f);
    }
}

static void updateWander(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    float dist = flatDistance(ai->goal, self->position);
    if (dist < ARRIVE_DISTANCE * 2.0f || isStuck(ai, stillToGo(self, ai->goal), dt)) { /* (along the way round, if any) */
        rest(ai, PAUSE_MIN, PAUSE_MAX);
        stand(self);
        return;
    }
    Vec3 dir = ai_steer(self, NULL, objects, count, ai->goal);
    if (vec3_length(dir) < 0.5f) { stand(self); return; }
    ai_walk(self, dir, fminf(STROLL_SPEED, dist * 1.5f + 0.3f), dt);
}

/* ---------- Beer ---------- */

static void giveUpBottle(Ai *ai) {
    ai->target = NULL;
    rest(ai, 1.0f, 3.0f);
}

static void updateFetch(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *bottle = ai->target;
    ai->timer += dt;
    bool crate = bottle && bottle->type == OBJ_CRATE;
    if (!bottle || bottle->is_held || (crate ? bottle->stored - bottle->crate_empties <= 0 : bottle->sips <= 0) || ai->timer > FETCH_TIMEOUT ||
        ai_skipped(ai, bottle)) { /* gone, or can't get at it */
        stand(self);
        giveUpBottle(ai);
        return;
    }

    if (Hands_gapFrom(self->position, self->look.height, bottle) < PICKUP_REACH) {
        stand(self);
        turnTowards(self, vec3_sub(bottle->position, self->position), dt);
        if (crate) { /* one out of the crate (car.c Car_serve: then that's the one) */
            Hands_touch(self, Object_colliderCenter(bottle, 0));
            ai->take_from = bottle;
            return;
        }
        if (Hands_pickUpObject(self, self->main_hand, bottle)) {
            ai->mode = AI_DRINK;
            ai->timer = 0.0f;
            /* Beer: a sip or two. Thirsty or hungry: until it's better. */
            bool need = self->needs.thirst > 0.3f || Object_isFood(bottle);
            ai->sips_left = need ? bottle->sips : 1 + (random01(ai) < 0.5f ? 1 : 0);
            /* food (and now and then a beer) at a table if there's a chair free */
            if (random01(ai) < (Object_isFood(bottle) ? TABLE_EAT : TABLE_BEER)) {
                Object *chair = freeTableChair(self, objects, count, TABLE_WALK);
                if (chair) {
                    int sips = ai->sips_left;
                    goToTable(self, chair);
                    ai->sips_left = sips;
                }
            }
        }
        return;
    }

    float dist = flatDistance(bottle->position, self->position);
    Vec3 dir = ai_steer(self, bottle, objects, count, bottle->position);
    if (vec3_length(dir) < 0.5f) { stand(self); return; }
    ai_walk(self, dir, fminf(WALK_SPEED * 0.6f, dist * 1.5f + 0.3f), dt);
}

static void updateDrink(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Hands *h = &self->hands;
    Object *bottle = ai->target;
    const int hand = self->main_hand;
    stand(self);

    if (h->state[hand] == HAND_REACHING) return; /* still picking it up */
    if (h->state[hand] != HAND_HOLDING || h->item[hand] != bottle) {
        giveUpBottle(ai);
        return;
    }
    if (Hands_isDrinking(self)) {
        ai->timer = 0.0f;
        return;
    }

    ai->timer += dt;
    if (ai->timer < DRINK_PAUSE) return;
    if (ai->sips_left > 0 && bottle->sips > 0 && Hands_drink(self)) {
        ai->sips_left--;
        return;
    }
    /* empty: stood with the others, on a table, in the bin, down, or tossed away (their way) */
    if (Camp_doneWithBottle(self, hand, objects, count, random01(ai)) == DONE_NONE) Hands_drop(self, hand);
    giveUpBottle(ai);
}

/* At the apple crate or water barrel: a few handfuls. */
static void updateSupply(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *supply = ai->target;
    if (!supply) { rest(ai, 1.0f, 2.0f); return; }
    if (ai->sips_left == 0 && Hands_gapFrom(self->position, self->look.height, supply) > PICKUP_REACH) {
        ai->timer += dt;
        if (ai->timer > FETCH_TIMEOUT * 1.5f) { ai->target = NULL; rest(ai, 1.0f, 2.0f); return; }
        float dist = flatDistance(supply->position, self->position);
        Vec3 dir = ai_steer(self, supply, objects, count, supply->position);
        if (vec3_length(dir) < 0.5f) { stand(self); return; }
        ai_walk(self, dir, fminf(WALK_SPEED * 0.7f, dist * 1.5f + 0.3f), dt);
        return;
    }
    /* Hand from the supply to the mouth, over and over */
    stand(self);
    turnTowards(self, vec3_sub(supply->position, self->position), dt);
    if (ai->sips_left == 0) { /* start eating / drinking */
        ai->sips_left = 1;
        ai->timer = supply->dispenses == OBJ_URINELLA ? 1.2f : 4.0f;
    }
    if (supply->dispenses == OBJ_URINELLA) { /* a handful into the pocket */
        Hands_touch(self, Object_colliderCenter(supply, 0));
        if ((ai->timer -= dt) > 0.0f) return;
        Pockets_refill(self);
        Ai_say(self, self->urinella ? "Some Urinellas for later." : "Let's see how these work.");
        self->urinella = true; /* (to try, or more of them) */
        ai->target = NULL;
        rest(ai, 1.0f, 2.0f);
        return;
    }
    ai->timer -= dt;
    float s = self->look.height / 1.75f;
    bool to_mouth = fmodf(ai->timer, 1.3f) < 0.6f;
    Vec3 at = Figure_toWorld(self->position, Object_facing(self),
                             to_mouth ? vec3(0.05f * s, 1.5f * s, 0.12f * s) : vec3(0.0f, 0.95f * s, 0.45f * s));
    Hands_touch(self, at);
    if (ai->timer > 0.0f) return;
    if (supply->dispenses == OBJ_WATER) {
        self->needs.thirst = fmaxf(0.0f, self->needs.thirst - 0.6f);
        self->needs.bladder = fminf(1.0f, self->needs.bladder + 0.25f);
    } else {
        if (supply->stock < 0) { ai->target = NULL; Ai_say(self, "The food's all gone. Somebody has to go shopping."); rest(ai, 2.0f, 4.0f); return; }
        if (supply->stock > 0 && --supply->stock == 0) supply->stock = -1; /* (bought: it runs out) */
        self->needs.hunger = fmaxf(0.0f, self->needs.hunger - 0.5f);
    }
    self->needs.stamina = fminf(1.0f, self->needs.stamina + 0.2f);
    ai->target = NULL;
    rest(ai, 2.0f, 4.0f);
}

/* Off a few steps, then a pee. */
/* ---------- Peeing somewhere decent ---------- */


/* How wide something is on the ground (m, from its middle). */
static float footprint(const Object *o) {
    if (o->type == OBJ_TREE) return 0.3f;
    if (o->type == OBJ_BUSH) return o->size.x * 0.45f;
    float r = 0.0f;
    for (int c = 0; c < o->collider_count; c++) {
        const Collider *col = &o->colliders[c];
        Vec3 off = vec3_sub(Object_colliderCenter(o, c), o->position);
        float e = 0.0f;
        for (int k = 0; k < 3; k++)
            e = fmaxf(e, sqrtf(col->axes[k].x * col->axes[k].x + col->axes[k].z * col->axes[k].z) *
                         (k == 0 ? col->half_extents.x : k == 1 ? col->half_extents.y : col->half_extents.z));
        r = fmaxf(r, e + sqrtf(off.x * off.x + off.z * off.z));
    }
    return r;
}

/* Something to pee behind: a bush, a tree, a wall, a car. */
static bool isCover(const Object *o) {
    if (o->collider_count == 0 || o->broken) return false;
    switch (o->type) {
    case OBJ_BUSH: return o->size.y > 0.6f;
    case OBJ_TREE: return !o->toppled;
    case OBJ_HOUSE: case OBJ_BARN: case OBJ_WALL: return true;
    case OBJ_BOX_STATIC: return o->size.y > 1.2f;
    case OBJ_FURNITURE: return o->variant == FURN_CAR;
    default: return false;
    }
}

/* How bad a spot is: far to walk, people near, the fire near, out in the open;
   where others went before is fine (and smells like it anyway). */
static float spotCost(const Object *self, Object *const *objects, int count, Vec3 spot, bool hidden, bool used) {
    float urgent = self->needs.bladder > 0.93f ? 2.0f : 1.0f;
    float cost = flatDistance(spot, self->position) / 12.0f * urgent;
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (o == self) continue;
        float d = flatDistance(o->position, spot);
        if (o->is_character && o->type != OBJ_DOG && d < 10.0f) cost += 1.5f * (10.0f - d) / 10.0f;
        if (o->is_character && o->ai.mode == AI_PEE && flatDistance(o->ai.goal, spot) < 5.0f) cost += 4.0f; /* (taken) */
        if (o->type == OBJ_FIREPLACE && d < 12.0f) cost += 2.0f;
    }
    if (!hidden) cost += 2.5f;
    if (used) cost -= 1.5f;
    return cost;
}

/* A decently hidden place for a pee: behind something, on the side away from
   everybody, or a spot others have used (by something to hide behind if it is). */
static bool findPeeSpot(Object *self, Object *const *objects, int count) {
    Ai *ai = &self->ai;
    /* who to hide from: the others around */
    Vec3 crowd = vec3_zero();
    int seen = 0;
    int people = 0;
    Object *const *everyone = Object_kind(KIND_CHARACTERS, objects, count, &people);
    for (int i = 0; i < people; i++) {
        const Object *o = everyone[i];
        if (o == self || !o->is_character || o->type == OBJ_DOG) continue;
        if (flatDistance(o->position, self->position) > CROWD_RANGE) continue;
        crowd = vec3_add(crowd, o->position);
        seen++;
    }
    crowd = seen ? vec3_scale(crowd, 1.0f / (float)seen) : ai->home;
    float best = 1e9f;
    Vec3 best_spot = vec3_zero(), best_cover = vec3_zero();
    bool best_hidden = false;
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        bool used = o->type == OBJ_STINK && o->variant == 1 && !o->broken;
        if (!used && !isCover(o)) continue;
        if (flatDistance(o->position, self->position) > PEE_SEARCH) continue;
        if (used) { /* where somebody went before: by the nearest thing to hide behind there */
            Vec3 spot = o->position, cover = spot;
            bool hidden = false;
            float near = 2.5f;
            for (int j = 0; j < count; j++) {
                float d = flatDistance(objects[j]->position, spot) - footprint(objects[j]);
                if (isCover(objects[j]) && d < near) { near = d; cover = Object_colliderCenter(objects[j], 0); hidden = true; }
            }
            float c = spotCost(self, objects, count, spot, hidden, true);
            if (c < best && Nav_free(spot)) { best = c; best_spot = spot; best_cover = cover; best_hidden = hidden; }
            continue;
        }
        /* behind it, seen from the others (big things: round the side that faces away) */
        Vec3 center = Object_colliderCenter(o, 0);
        Vec3 away = vec3(center.x - crowd.x, 0.0f, center.z - crowd.z);
        if (vec3_length(away) < 0.5f) continue;
        away = vec3_normalize(away);
        float r = footprint(o);
        for (int k = 0; k < (r > 3.0f ? 8 : 1); k++) {
            Vec3 dir = away;
            if (r > 3.0f) {
                float a = (float)k * PI_F / 4.0f;
                dir = vec3(cosf(a), 0.0f, sinf(a));
                if (vec3_dot(dir, away) < 0.3f) continue;
            }
            float reach = r > 3.0f ? fabsf(vec3_dot(dir, o->colliders[0].axes[0])) * o->colliders[0].half_extents.x +
                                         fabsf(vec3_dot(dir, o->colliders[0].axes[2])) * o->colliders[0].half_extents.z
                                   : r;
            Vec3 spot = vec3_add(center, vec3_scale(dir, reach + 0.45f));
            spot.y = self->position.y;
            if (!Nav_free(spot) || blockedAt(self, NULL, objects, count, spot)) continue;
            Vec3 cover = vec3_add(center, vec3_scale(dir, reach * 0.9f));
            bool used_here = Object_stinkAt(objects, count, spot) > 0.05f;
            float c = spotCost(self, objects, count, spot, true, used_here);
            if (c < best) { best = c; best_spot = spot; best_cover = cover; best_hidden = true; }
        }
    }
    if (best > 1e8f) return false;
    ai->mode = AI_PEE;
    ai->target = NULL;
    ai->timer = 0.0f;
    ai->pee_cover = best_cover;
    ai->pee_hidden = best_hidden;
    setGoal(ai, best_spot);
    return true;
}

static void updatePee(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    float dist = flatDistance(ai->goal, self->position);
    Object *toilet = ai->target && ai->target->type == OBJ_FURNITURE ? ai->target : NULL;
    if (toilet && dist < 1.0f) { /* at the toilet door: in, or wait a bit (not long when it's urgent) */
        stand(self);
        if (!toilet->dog.mode) { Object_useToilet(self, toilet); ai->target = NULL; rest(ai, 2.0f, 4.0f); return; }
        float patience = 6.0f + 20.0f * fmaxf(0.0f, 0.97f - self->needs.bladder) / 0.27f;
        if ((ai->timer += dt) > patience) {
            ai->target = NULL;
            if (!findPeeSpot(self, objects, count)) pickWanderGoal(self, objects, count);
            else Ai_say(self, "Can't wait any longer...");
        }
        return;
    }
    if (dist < ARRIVE_DISTANCE * 2.0f || (!toilet && isStuck(ai, dist, dt))) {
        stand(self);
        bool squat = self->female && !(self->urinella && self->urinellas > 0);
        self->body.pee_aim = vec3_zero();
        if (ai->pee_hidden && dist < 1.5f) { /* face it (or squat with the back to it), aim at its foot */
            Vec3 to = vec3(ai->pee_cover.x - self->position.x, 0.0f, ai->pee_cover.z - self->position.z);
            if (vec3_length(to) > 0.05f) {
                to = vec3_normalize(to);
                Vec3 want = squat ? vec3_scale(to, -1.0f) : to;
                turnTowards(self, want, dt);
                if (vec3_dot(want, Object_facing(self)) < 0.94f && (ai->timer += dt) < 1.5f) return;
                if (!squat) self->body.pee_aim = vec3_normalize(vec3_add(to, vec3(0.0f, -0.6f, 0.0f)));
            }
        }
        Hands_setAside(self, ai->set_aside); /* hands free first (taken again after) */
        Body_pee(&self->body, Object_peeTime(self));
        ai->pee_hidden = false;
        rest(ai, 2.0f, 4.0f);
        return;
    }
    if (toilet && isStuck(ai, dist, dt)) { ai->target = NULL; return; }
    Vec3 dir = ai_steer(self, NULL, objects, count, ai->goal);
    if (vec3_length(dir) < 0.5f) { stand(self); return; }
    ai_walk(self, dir, self->needs.bladder > 0.93f ? WALK_SPEED : STROLL_SPEED, dt);
}

/* ---------- Waking people up ---------- */

static Vec3 chestOf(const Object *o) {
    return Figure_toWorld(o->position, Object_facing(o), o->skeleton.joints[JOINT_CHEST]);
}

/* Somebody sleeping it off for a while that nobody is waking yet. */
static Object *findSleeper(const Object *self, Object *const *objects, int count) {
    objects = Object_kind(KIND_CHARACTERS, objects, count, &count); /* (only people sleep) */
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o == self || !o->is_character || !o->body.asleep || o->body.pose != POSE_DOWN) continue;
        if (!o->passed_out) continue; /* a proper night's sleep is left alone */
        if (o->body.pose_time < WAKE_AFTER || flatDistance(o->position, self->position) > WAKE_RANGE) continue;
        bool taken = false;
        for (int j = 0; j < count; j++) {
            const Object *c = objects[j];
            if (c != self && c->is_character && c->ai.mode == AI_WAKE && c->ai.target == o) taken = true;
        }
        if (!taken) return o;
    }
    return NULL;
}

static void updateWake(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *sleeper = ai->target;
    if (!sleeper || !sleeper->body.asleep) { /* woke up on their own */
        ai->target = NULL;
        rest(ai, 1.0f, 3.0f);
        return;
    }

    Vec3 chest = chestOf(sleeper);
    float dist = flatDistance(chest, self->position);
    if (dist > WAKE_REACH) {
        ai->timer = 0.0f;
        Vec3 spot = vec3_add(chest, vec3_scale(vec3_normalize(flat(vec3_sub(self->position, chest))), 0.55f));
        Vec3 dir = ai_steer(self, sleeper, objects, count, spot);
        if (vec3_length(dir) < 0.5f) { stand(self); return; }
        ai_walk(self, dir, dist > RUN_TO_LEADER ? runSpeed(self) * 0.7f : WALK_SPEED * 0.6f, dt);
        return;
    }

    /* Kneel down and shake them by the shoulder */
    stand(self);
    turnTowards(self, vec3_sub(chest, self->position), dt);
    self->body.look_dir = vec3_normalize(vec3_sub(chest, vec3_add(self->position, vec3(0.0f, 1.2f, 0.0f))));
    self->body.look_weight = 1.0f;
    ai->timer += dt;
    Vec3 shake = vec3(0.0f, 0.0f, 0.0f);
    shake.x = sinf(ai->timer * 14.0f) * 0.05f;
    Hands_touch(self, vec3_add(chest, vec3_add(shake, vec3(0.0f, 0.05f, 0.0f))));
    if (ai->timer >= SHAKE_TIME) {
        Body_getUp(&sleeper->body);
        sleeper->drunk = fminf(sleeper->drunk, 0.85f); /* groggy, but awake */
        ai->target = NULL;
        rest(ai, 2.0f, 4.0f);
    }
}

/* ---------- Walking up to the player ---------- */

/* Back to what they did before walking over (or being shown something). */
static void resume(Object *self) {
    Ai *ai = &self->ai;
    AiMode back = ai->before;
    if (back == AI_WORK && ai->order != ORDER_NONE) ai->mode = AI_WORK;
    else if (back == AI_HELP && ai->leader) ai->mode = AI_HELP;
    else rest(ai, 1.0f, 3.0f);
}

static void updateApproach(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *player = findPlayer(objects, count);
    ai->timer += dt;
    if (!player || ai->timer > APPROACH_TIMEOUT || Body_isDown(&player->body)) {
        ai->talk_cooldown = 30.0f;
        stand(self);
        resume(self);
        return;
    }
    Vec3 to = flat(vec3_sub(player->position, self->position));
    float dist = vec3_length(to);
    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(player->position, vec3(0.0f, player->look.height * 0.93f, 0.0f)), eye));
    self->body.look_weight = 1.0f;
    if (dist > TALK_DISTANCE + 0.2f) {
        Vec3 spot = vec3_sub(player->position, vec3_scale(vec3_normalize(to), TALK_DISTANCE));
        Vec3 dir = ai_steer(self, player, objects, count, spot);
        if (vec3_length(dir) < 0.5f) { stand(self); return; }
        ai_walk(self, dir, dist > 6.0f ? runSpeed(self) * 0.6f : WALK_SPEED * 0.7f, dt);
        return;
    }
    stand(self);
    turnTowards(self, to, dt);
    ai->ready_to_talk = true;
}

/* Now and then, depending on who they are: offer to take over the player's
   wood work, or suggest a job that needs doing and that they like. */
static bool g_settled = true; /* (just arrived: no jobs offered till the car is empty and the tents stand) */
void Ai_setSettled(bool settled) { g_settled = settled; }

static bool maybeApproach(Object *self, Object *const *objects, int count, const Object *player, float dt) {
    Ai *ai = &self->ai;
    if (!g_settled || self->lives_here || self->guest) return false; /* (settling in first; the owners don't do your jobs, the guests neither) */
    if (!player || ai->talk_cooldown > 0.0f || Body_isBusy(&player->body) || ai->annoyed > 0.3f) return false; /* (annoyed: not now) */
    const Mood *m = &self->temperament;
    const Character *c = &ai->character;
    float d = flatDistance(player->position, self->position);
    bool player_on_wood = player->work_state.since < 4.0f && isWoodJob(player->work_state.last_job);
    float helpful = clampf(0.4f + 0.5f * m->energy + 0.4f * c->willing, 0.1f, 1.2f);
    if (player_on_wood && d < 12.0f && c->likes[ORDER_FIREWOOD] > -0.2f && random01(ai) < 0.12f * helpful * dt &&
        Ai_needed(self, objects, count, ORDER_FIREWOOD)) {
        approach(self, TOPIC_OFFER);
        return true;
    }
    if (d > 9.0f || random01(ai) > 0.015f * helpful * dt) return false;
    /* The job they'd most like to do, of those that need doing */
    Order best = ORDER_NONE;
    float best_like = -0.2f;
    for (int o = ORDER_FIREWOOD; o < ORDER_COUNT; o++) {
        if (c->likes[o] > best_like && Ai_needed(self, objects, count, (Order)o)) {
            best_like = c->likes[o];
            best = (Order)o;
        }
    }
    if (best != ORDER_NONE) {
        ai->suggested = best;
        approach(self, TOPIC_SUGGEST);
        return true;
    }
    if (c->willing > 0.6f && random01(ai) < 0.3f) { /* nothing obvious: just ask */
        approach(self, TOPIC_ASK);
        return true;
    }
    ai->talk_cooldown = 40.0f;
    return false;
}

/* ---------- Public ---------- */

void ai_ensureReady(Object *self) {
    Ai *ai = &self->ai;
    if (ai->ready) return;
    ai->ready  = true;
    ai->home   = self->position;
    ai->detour = 1.0f;
    ai->side   = 1.0f;
    ai->rng    = 0x9e3779b9u ^ (unsigned)(self->position.x * 1000.0f) ^ ((unsigned)(self->position.z * 1000.0f) << 16);
    /* Some know how to make firewood, some don't (yet) */
    ai->knows_wood = random01(ai) < 0.45f + 0.25f * self->temperament.energy;
    ai->talk_cooldown = 8.0f + 25.0f * random01(ai);
    if (!ai->character.about) { /* nobody special: a bit of everything */
        for (int o = ORDER_FIREWOOD; o < ORDER_COUNT; o++) ai->character.likes[o] = random01(ai) - 0.5f;
        ai->character.willing = 0.5f;
        ai->character.about = "Just here for the festival.";
    }
    float set = ai->timer; /* (a while set beforehand - sitting at the fire at the start - stays) */
    rest(ai, 1.0f, PAUSE_MAX);
    ai->timer = fmaxf(ai->timer, set);
}

/* Leaves whatever they were doing: lets go, no job, no tools to fetch. */
static void dropEverything(Object *self) {
    Ai *ai = &self->ai;
    if (Hands_isGrabbing(self)) Hands_release(self);
    ai->pushing    = false;
    ai->resting    = false;
    ai->job_target = NULL;
    ai->retry      = 0.0f;
}

void Ai_help(Object *self, Object *leader) {
    ai_ensureReady(self);
    Ai *ai = &self->ai;
    dropEverything(self);
    for (int hand = 0; hand < 2; hand++)
        if (self->hands.state[hand] == HAND_HOLDING && !toolOf(self->hands.item[hand])) Hands_drop(self, hand);

    Vec3 right = vec3_normalize(vec3_cross(Object_facing(leader), UP));
    if (ai->order != ORDER_NONE && (ai->mode == AI_WORK || ai->before == AI_WORK)) { /* (called away from a job: remembered) */
        ai->paused_order = ai->order;
        ai->paused_area = ai->work_area;
    }
    ai->mode    = AI_HELP;
    ai->order   = ORDER_NONE;
    ai->leader  = leader;
    ai->target  = NULL;
    ai->copied  = NULL;
    ai->copy_step = 0;
    ai->side    = vec3_dot(vec3_sub(self->position, leader->position), right) < 0.0f ? -1.0f : 1.0f;
    setGoal(ai, self->position);
}

void Ai_stopHelping(Object *self) {
    Ai *ai = &self->ai;
    if (ai->mode != AI_HELP) return;
    dropEverything(self);
    ai->leader  = NULL;
    ai->tool    = NULL;
    ai->home    = self->position; /* hang around where we helped */
    stand(self);
    rest(ai, PAUSE_MIN, PAUSE_MAX);
    /* Called away from a job: back to it - unless they forget (then they wait to be reminded) */
    if (ai->paused_order != ORDER_NONE && 0.6f * self->wits + 0.4f * ai->character.willing - 0.3f * fminf(self->drunk, 1.0f) > 0.42f) {
        Order back = ai->paused_order;
        Ai_order(self, back, ai->paused_area);
        Ai_say(self, back == ORDER_FIRE ? "I'll get back to the fire then." : back == ORDER_TRASH ? "Back to the tidying then."
                   : back == ORDER_TREES ? "Back to the trees then." : "Right, back to the firewood.");
    }
}

bool Ai_resumePaused(Object *self) {
    Ai *ai = &self->ai;
    if (ai->paused_order == ORDER_NONE || ai->mode == AI_WORK) return false;
    Order back = ai->paused_order;
    if (ai->mode == AI_HELP) Ai_stopHelping(self);
    if (ai->mode != AI_WORK) Ai_order(self, back, ai->paused_area);
    return true;
}

bool Ai_isHelping(const Object *self) {
    return self->ai.mode == AI_HELP;
}

void Ai_say(Object *self, const char *text) {
    Ai *ai = &self->ai;
    snprintf(ai->speech, sizeof(ai->speech), "%s", text);
    ai->speech_time = 2.0f + 0.06f * (float)strlen(ai->speech);
    if (!g_world) return;
    Vec3 head = vec3_add(self->position, vec3(0.0f, self->look.height * (self->type == OBJ_DOG ? 0.25f : 0.9f), 0.0f));
    if (self->type == OBJ_DOG) { /* (her woofs, and a crunch) */
        int woofs = 0;
        for (const char *s = text; (s = strstr(s, "Woof")) != NULL; s += 4) woofs++;
        if (woofs) World_noiseOn(g_world, NOISE_BARK, head, 0.9f, woofs);
        else if (strstr(text, "crunch")) World_noiseOn(g_world, NOISE_GULP, head, 0.5f, 1);
        return;
    }
    if (text[0] == '*') return; /* (a sound written out, not words) */
    { /* a hand goes with it: waving hello / goodbye, a shrug saying no, pointing over there */
        static const char *const WAVE[]  = { "Hello", "hello", "Hi ", "Hey ", "Night", "Bye", "Morning" };
        static const char *const SHRUG[] = { "No ", "no idea", "Not ", "can't", "Can't", "don't", "Never mind", "forget it", "Nope", "Sorry" };
        static const char *const POINT[] = { "over there", "Over there", "Look", "That one", "this way", "This way" };
        int kind = -1;
        for (unsigned k = 0; k < sizeof WAVE / sizeof WAVE[0] && kind < 0; k++) if (strstr(text, WAVE[k])) kind = TALK_WAVE;
        for (unsigned k = 0; k < sizeof SHRUG / sizeof SHRUG[0] && kind < 0; k++) if (strstr(text, SHRUG[k])) kind = TALK_SHRUG;
        for (unsigned k = 0; k < sizeof POINT / sizeof POINT[0] && kind < 0; k++) if (strstr(text, POINT[k])) kind = TALK_POINT;
        if (kind >= 0 && random01(ai) < 0.8f) Body_talkGesture(&self->body, kind);
    }
    int syllables = (int)strlen(text) / 5;
    syllables = syllables < 2 ? 2 : syllables > 10 ? 10 : syllables;
    int voice = self->female ? VOICE_WOMAN : self->look.height > 1.82f || self->weight > 85.0f ? VOICE_DEEP : VOICE_MAN;
    World_noiseOn(g_world, NOISE_VOICE, head, strchr(text, '!') ? 0.9f : 0.6f, voice | (syllables << 2));
}

void Ai_order(Object *self, Order order, Vec3 area) {
    ai_ensureReady(self);
    Ai *ai = &self->ai;
    dropEverything(self);
    ai->mode      = AI_WORK;
    ai->order     = order;
    ai->paused_order = ORDER_NONE;
    ai->pending   = ORDER_NONE;
    ai->showing   = SHOW_NONE;
    ai->leader    = NULL;
    ai->work_area = area;
    for (int i = 0; i < AI_SKIPS; i++) ai->skip[i] = NULL;
    ai->home      = area;
    ai->tool_asked = ai->tool_waiting = false;
    ai->on_break  = ai->ponder = ai->streak = 0.0f; /* (first break after a while) */
    ai->task_seen = NULL;
    ai->chat_with = NULL;
}

void Ai_stopWork(Object *self) {
    Ai *ai = &self->ai;
    dropEverything(self);
    ai->order = ORDER_NONE;
    ai->home  = self->position;
    if (ai->mode == AI_WORK) rest(ai, PAUSE_MIN, PAUSE_MAX);
}

void Ai_show(Object *self, Showing what, const Object *player) {
    ai_ensureReady(self);
    Ai *ai = &self->ai;
    if (ai->mode != AI_FOLLOW && ai->mode != AI_APPROACH) ai->before = ai->mode;
    dropEverything(self);
    ai->mode      = AI_FOLLOW;
    ai->showing   = what;
    ai->seen_jobs = player->work_state.jobs_done;
    Vec3 right = vec3_normalize(vec3_cross(Object_facing(player), UP));
    ai->side = vec3_dot(vec3_sub(self->position, player->position), right) < 0.0f ? -1.0f : 1.0f;
}

void Ai_stopShowing(Object *self) {
    Ai *ai = &self->ai;
    ai->showing = SHOW_NONE;
    ai->pending = ORDER_NONE;
    if (ai->mode == AI_FOLLOW) resume(self);
}

void Ai_setWoodpile(Object *self, Object *woodpile) {
    Ai *ai = &self->ai;
    ai->woodpile = woodpile;
    if (ai->mode == AI_FOLLOW && ai->showing == SHOW_PLACE) {
        ai->showing = SHOW_NONE;
        if (ai->order != ORDER_NONE) ai->mode = AI_WORK;
        else resume(self);
    }
}

void Ai_setTalking(Object *self, bool talking) {
    Ai *ai = &self->ai;
    ai->talking = talking;
    if (talking) return;
    ai->ready_to_talk = false;
    ai->talk_cooldown = 60.0f + 60.0f * random01(ai);
    if (ai->mode == AI_APPROACH) resume(self);
}

static const char *const STINK_REMARKS[] = {
    "Ugh! What's that smell?", "Eww, it stinks here.", "Oh no, who did that?", "Phew...",
};

/* Something stinks nearby: a face, a remark now and then, and idle people walk off. */
static void smell(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    ai->sniff_cooldown = fmaxf(0.0f, ai->sniff_cooldown - dt);
    float stink = Object_stinkAt(objects, count, self->position);
    Object *player = findPlayer(objects, count);
    if (stink < 0.15f && player && player->dirt > 0.55f && ai->sniff_cooldown <= 0.0f && !ai->talking &&
        flatDistance(player->position, self->position) < 2.5f) { /* you're a sight */
        Ai_say(self, self->temperament.pride < -0.3f ? "Um... you've got a bit of... everywhere." : "No offence, but you could do with a wash.");
        ai->sniff_cooldown = 60.0f + 40.0f * random01(ai);
    }
    if (stink < 0.15f) return;
    self->body.disgust = fmaxf(self->body.disgust, fminf(1.0f, stink * 1.5f));
    if (ai->sniff_cooldown <= 0.0f && !ai->talking) {
        Ai_say(self, STINK_REMARKS[(ai->rng >> 8) % (sizeof(STINK_REMARKS) / sizeof(STINK_REMARKS[0]))]);
        ai->sniff_cooldown = 25.0f + 20.0f * random01(ai);
    }
    if ((ai->mode == AI_IDLE || (ai->mode == AI_WANDER && Object_stinkAt(objects, count, ai->goal) > 0.1f)) &&
        stink > 0.3f && pickWanderGoal(self, objects, count))
        ai->mode = AI_WANDER;
}

/* In a conversation: stands, faces the player and listens. */
static void listen(Object *self, Object *const *objects, int count, float dt) {
    stand(self);
    Object *player = findPlayer(objects, count);
    if (!player) return;
    turnTowards(self, vec3_sub(player->position, self->position), dt);
    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(player->position, vec3(0.0f, player->look.height * 0.93f, 0.0f)), eye));
    self->body.look_weight = 1.0f;
}

/* ---------- Watchdog ---------- */

#define WATCH_STILL  25.0f  /* nothing at all happening for this long: stuck */
#define WATCH_BUSY  150.0f  /* working on something without it ever getting done: stuck too */
#define WATCH_MOVED   1.0f  /* walking this far counts as something happening */

/* Waiting is fine here: for the leader, by the fire, on a break, for an answer. */
static bool waitingIsFine(const Object *self) {
    const Ai *ai = &self->ai;
    return ai->mode == AI_IDLE || ai->mode == AI_HELP || ai->mode == AI_FOLLOW || ai->mode == AI_DRINK || (ai->mode == AI_TABLE && self->sitting) ||
           (ai->mode == AI_APPROACH && ai->ready_to_talk) || ai->talking || self->sitting || self->body.asleep ||
           ai->on_break > 0.0f || ai->ponder > 0.0f || ai->tool_waiting || ai->timber;
}

static int itemCount(const Object *self) {
    int n = 0;
    for (int h = 0; h < 2; h++)
        if (self->hands.item[h]) n += 1 + self->hands.item[h]->stored * 2 + self->hands.item[h]->sips * 3;
    return n;
}

/*
 * Every behaviour can get stuck somewhere nobody thought of (a corner, a
 * thing that can't be reached, two things they keep switching between).
 * Nothing happening for a long while: first let go of what they're after
 * and look again; still nothing: give it up.
 */
static void watchdog(Object *self) {
    Ai *ai = &self->ai;
    int mode = (int)ai->mode * 16 + (int)ai->order, items = itemCount(self);
    bool working = ai->job_time > ai->watch_job;
    ai->watch_job = ai->job_time;
    if (waitingIsFine(self) || mode != ai->watch_mode || items != ai->watch_count ||
        self->hands.item[0] != ai->watch_items[0] || self->hands.item[1] != ai->watch_items[1] ||
        flatDistance(self->position, ai->watch_pos) > WATCH_MOVED) {
        if (!waitingIsFine(self) && mode == ai->watch_mode) ai->watch_strikes = 0; /* (got somewhere) */
        ai->watch_time = 0.0f;
        ai->watch_pos = self->position;
        ai->watch_mode = mode;
        ai->watch_count = items;
        ai->watch_items[0] = self->hands.item[0];
        ai->watch_items[1] = self->hands.item[1];
        return;
    }
    ai->watch_time += g_dt;
    if (ai->watch_time < (working ? WATCH_BUSY : WATCH_STILL)) return;
    ai->watch_time = 0.0f;
    ai->path_len = 0;
    ai->path_none = 0.0f;
    if (ai->watch_strikes++ == 0) { /* let go of it, look again */
        if (ai->job_target) skipTarget(ai, ai->job_target);
        if (ai->target && !ai->target->is_character) skipTarget(ai, ai->target);
        ai->detour = -ai->detour;
        Ai_say(self, "Hm. That's not working.");
        return;
    }
    ai->watch_strikes = 0; /* give it up */
    if (ai->mode == AI_WORK) {
        Ai_say(self, "I'm stuck here. I'll leave it.");
        Ai_stopWork(self);
    } else {
        ai->target = NULL;
        rest(ai, PAUSE_MIN, PAUSE_MAX);
    }
}

/* Put down for a pee: taken up again (if it's still there and nobody took it). */
static bool takeBack(Object *self) {
    Ai *ai = &self->ai;
    for (int k = 0; k < 2; k++) {
        Object *item = ai->set_aside[k];
        if (!item) continue;
        if (item->is_held || flatDistance(item->position, self->position) > 1.6f || self->hands.grabbed) { ai->set_aside[k] = NULL; continue; }
        int hand = self->hands.state[k] == HAND_FREE ? k : Hands_freeHand(self);
        if (hand < 0) { ai->set_aside[k] = NULL; continue; }
        if (self->hands.state[HAND_LEFT] == HAND_REACHING || self->hands.state[HAND_RIGHT] == HAND_REACHING) { stand(self); return true; }
        ai->set_aside[k] = NULL;
        turnTowards(self, vec3_sub(item->position, self->position), 1.0f);
        if (Hands_pickUpObject(self, hand, item)) { stand(self); return true; }
    }
    return false;
}

/* Lighters: a borrowed one goes back (when they think of it), and the owner
   asks for theirs. */
static const char *withName(const char *format, const Object *o) {
    static char line[AI_SPEECH_MAX];
    snprintf(line, sizeof line, format, o->name);
    return line;
}

static void lighterManners(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    ai->lighter_ask = fmaxf(0.0f, ai->lighter_ask - dt);
    if ((ai->lighter_time -= dt) > 0.0f) return;
    ai->lighter_time = 3.0f + 4.0f * random01(ai);
    if (ai->speech_time > 0.0f) return;
    Object *owner = self->lighter ? self->lighter_of : NULL;
    bool needs_it = ai->mode == AI_WORK && ai->order == ORDER_FIRE;
    if (owner && !owner->lighter && !needs_it && flatDistance(owner->position, self->position) < 2.5f) {
        /* the careful think of it themselves; the player has to ask them (mostly) */
        float chance = (owner->type == OBJ_PLAYER ? 0.12f : 0.45f) * (1.0f - fminf(self->drunk, 1.0f)) * (0.6f + 0.6f * ai_stackCare(self));
        if (random01(ai) < chance && Pockets_giveBack(self)) {
            Ai_say(self, owner->type == OBJ_PLAYER ? "Oh - your lighter. Thanks!" : withName("Oh - your lighter, %s. Thanks!", owner));
            return;
        }
    }
    if (self->lighter) return;
    for (int i = 0; i < count; i++) { /* somebody near with mine */
        Object *o = objects[i];
        if (o == self || !o->lighter || o->lighter_of != self || flatDistance(o->position, self->position) > 4.0f) continue;
        if (o->type == OBJ_PLAYER) {
            if (random01(ai) < 0.35f) Ai_say(self, "Hey - can I have my lighter back?");
        } else if (o->type == OBJ_HUMAN && !o->ai.talking && o->ai.speech_time <= 0.0f && random01(ai) < 0.5f) {
            Ai_say(self, withName("%s, my lighter, please?", o));
            if (o->drunk > 0.6f && random01(ai) < 0.5f) Ai_say(o, "Lighter? What lighter?");
            else if (Pockets_giveBack(o)) Ai_say(o, "Oh, sorry - here.");
        }
        return;
    }
}

/* Smokers: the craving grows; when it's strong and there's a quiet moment, a
   cigarette (rolled if there's tobacco and both hands are free), lit with
   their lighter or somebody's near ("Got a light?"). None left: they bum one
   off somebody near (the player is asked). */
static bool quietMoment(const Object *self) {
    const Ai *ai = &self->ai;
    if (ai->talking || self->climbing || self->on_ladder || Body_isBusy(&self->body)) return false;
    if (ai->mode == AI_IDLE || ai->mode == AI_WANDER || self->sitting) return true;
    return ai->mode == AI_WORK && (ai->resting || ai->order == ORDER_FIRE);
}

#define SMOKES_WALK 150.0f /* m: the cigarette machine this near is worth the walk */
#define SMOKES_GIVE_UP 120.0f

static Object *smokesMachine(Object *const *objects, int count) {
    for (int i = 0; i < count; i++)
        if (objects[i]->type == OBJ_FURNITURE && objects[i]->variant == FURN_SMOKES) return objects[i];
    return NULL;
}

/* None left and nobody gives one: off to the cigarette machine (when free to). */
static void goForSmokes(Object *self, Object *const *objects, int count) {
    Ai *ai = &self->ai;
    Object *m = smokesMachine(objects, count);
    if (ai->want_smokes || !m || flatDistance(m->position, self->position) > SMOKES_WALK) return;
    if (ai->mode != AI_IDLE && ai->mode != AI_WANDER && ai->mode != AI_TABLE) return; /* (busy: later) */
    ai->want_smokes = true;
    ai->smokes_time = 0.0f;
    Ai_say(self, "Right, I'll get a pack from the machine.");
}

/* At the machine: a pack, and back to whatever. */
static bool buySmokes(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (!ai->want_smokes) return false;
    Object *m = smokesMachine(objects, count);
    if (!m || Pockets_hasSmoke(self) || (ai->smokes_time += dt) > SMOKES_GIVE_UP) {
        ai->want_smokes = false;
        return false;
    }
    self->sitting = false;
    if (!goTo(self, m, objects, count, 0.6f, dt)) return true;
    Hands_touch(self, vec3_add(Object_colliderCenter(m, 0), vec3(0.0f, 0.2f, 0.0f)));
    self->cigarettes += SMOKES_PACKS;
    ai->want_smokes = false;
    self->smoke_ask = 1.0f; /* (and lights one in a moment) */
    if (random01(ai) < 0.5f) Ai_say(self, "*clunk* - there we go.");
    rest(ai, 1.0f, 2.0f);
    return true;
}

static void smokeBreak(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (self->smoker <= 0.0f) return;
    self->smoke_ask = fmaxf(0.0f, self->smoke_ask - dt);
    if (Body_isSmoking(&self->body)) return; /* (the craving: see the world, smoke) */
    if (self->craving < 0.75f || self->smoke_ask > 0.0f || ai->speech_time > 0.0f) return;
    if (!quietMoment(self) && self->craving < 0.98f) return; /* (dying for one: whatever they're doing) */
    int hand = Hands_freeHand(self);
    bool both = self->hands.state[HAND_LEFT] == HAND_FREE && self->hands.state[HAND_RIGHT] == HAND_FREE;
    /* Rolling their own and something's missing (papers, tobacco, a filter): asked for first */
    PocketNeed need = Pockets_need(self);
    if (need != POCKET_NOTHING && !(need == POCKET_FILTERS && ai->filter_asked)) {
        Object *giver = Pockets_hasNear(self, need, objects, count, 6.0f);
        if (need == POCKET_FILTERS) ai->filter_asked = true; /* (once: no filter, then without) */
        if (giver) {
            char line[80];
            self->smoke_ask = 30.0f + 30.0f * random01(ai);
            if (giver->type == OBJ_PLAYER) {
                snprintf(line, sizeof(line), "Hey - got %s for me?", Pockets_needName(need));
                Ai_say(self, line);
                ai->asking_you = (int)need; /* (see the pockets: B, give it) */
                return;
            }
            snprintf(line, sizeof(line), "%s, got %s for me?", giver->name, Pockets_needName(need));
            Ai_say(self, line);
            if (giver->type == OBJ_HUMAN && (giver->ai.annoyed > 0.5f || random01(ai) > 0.5f + 0.5f * giver->ai.character.willing)) {
                Ai_say(giver, need == POCKET_TOBACCO ? "I'm nearly out myself, sorry." : "Sorry, I need them myself.");
            } else if (Pockets_give(giver, self, need) > 0) {
                if (giver->type == OBJ_HUMAN) Ai_say(giver, need == POCKET_TOBACCO ? "Here, take a pinch." : "Sure, here.");
                self->smoke_ask = 2.0f; /* (and rolls one in a moment) */
            }
            return;
        }
    }
    if (!Pockets_hasSmoke(self)) {
        Object *giver = Pockets_smokesNear(self, objects, count, 6.0f);
        self->smoke_ask = 40.0f + 40.0f * random01(ai);
        if (!giver) {
            if (self->craving > 0.95f) Ai_say(self, "I could really use a smoke...");
            goForSmokes(self, objects, count);
            return;
        }
        if (giver->type == OBJ_PLAYER) {
            if (ai->asked_you) { goForSmokes(self, objects, count); ai->asked_you = false; return; }
            Ai_say(self, "Hey - got a cigarette for me?");
            ai->asked_you = true;
            return;
        }
        Ai_say(self, withName("Can I bum a cigarette, %s?", giver));
        int has = giver->cigarettes + giver->tobacco;
        if (giver->type == OBJ_HUMAN && (has < 3 || giver->ai.annoyed > 0.5f || random01(ai) > 0.4f + 0.6f * giver->ai.character.willing)) {
            Ai_say(giver, has < 3 ? "Sorry, those are my last ones." : "Buy your own for once.");
            goForSmokes(self, objects, count);
        }
        else if (Pockets_giveSmoke(giver, self)) {
            if (giver->type == OBJ_HUMAN) Ai_say(giver, giver->tobacco > 0 && giver->cigarettes == 0 ? "Here, I rolled you one." : "Sure, here.");
            self->smoke_ask = 2.0f; /* (and lights it in a moment) */
        }
        return;
    }
    ai->asked_you = false; /* (got some) */
    ai->asking_you = 0;
    ai->filter_asked = false;
    Object *light = self->lighter && self->lighter_uses > 0 ? self : Pockets_lighterNear(self, objects, count, 4.0f);
    if (!light) {
        Ai_say(self, "Anyone got a light?");
        self->smoke_ask = 45.0f;
        return;
    }
    bool rolled = false;
    self->smoke_ask = 20.0f; /* (whatever happens: not again right away) */
    if (!Pockets_takeSmoke(self, both, &rolled)) return; /* (only tobacco and the hands are busy: later) */
    if (!Body_smoke(&self->body, rolled, hand >= 0 ? (self->hands.state[self->main_hand] == HAND_FREE ? self->main_hand : hand) : 2)) {
        if (rolled) { self->tobacco++; self->papers++; } else self->cigarettes++; /* (back in the pocket) */
        return;
    }
    Pockets_light(light);
    if (light != self) {
        Ai_say(self, light->type == OBJ_PLAYER ? "Thanks for the light!" : withName("Got a light, %s?", light));
        if (light->type == OBJ_HUMAN && light->ai.speech_time <= 0.0f) Ai_say(light, "Here.");
    }
}

/* A pushed load shoving us along: out of its way to one side, after a moment
   to see what's going on (the slower, the longer; the slowest just get shoved).
   Helpers sort it out their own way (ai_help.c makeRoom). */
#define DODGE_TIME  0.8f
#define DODGE_SPEED 1.6f /* m/s */

static bool stepAside(Object *self, float dt) {
    Ai *ai = &self->ai;
    if (ai->dodge > 0.0f) {
        ai->dodge -= dt; /* (a quick side step, no turning round first) */
        self->movement = vec3_scale(ai->dodge_dir, DODGE_SPEED * Object_vigor(self));
        Vec3 f = Object_facing(self), r = vec3_normalize(vec3_cross(f, UP));
        self->move_local = vec3(vec3_dot(ai->dodge_dir, r), 0.0f, vec3_dot(ai->dodge_dir, f));
        ai->walk_dir = vec3_zero();
        return true;
    }
    if (ai->shove_time < 0.0f) { ai->shove_time = fminf(0.0f, ai->shove_time + dt); return false; }
    if (vec3_length(self->shoved) < 0.5f || ai->mode == AI_HELP || self->sitting) { ai->shove_time = 0.0f; return false; }
    float wits = clampf(self->wits - 0.6f * fminf(self->drunk, 1.0f), 0.0f, 1.0f);
    if ((ai->shove_time += dt) < 0.25f + 1.5f * (1.0f - wits)) return false;
    if (wits < 0.2f) { /* doesn't get it: stands against it */
        Ai_say(self, "Hey! Watch it!");
        ai->shove_time = -4.0f;
        return false;
    }
    Vec3 across = vec3_cross(self->shoved, UP);
    float off = vec3_dot(vec3_sub(self->position, self->shove_load), across); /* (off to the side it's nearer) */
    float side = fabsf(off) > 0.05f ? (off > 0.0f ? 1.0f : -1.0f) : (random01(ai) < 0.5f ? -1.0f : 1.0f);
    ai->dodge_dir = vec3_scale(across, side);
    ai->dodge = DODGE_TIME;
    ai->shove_time = 0.0f;
    if (random01(ai) < 0.6f) Ai_say(self, random01(ai) < 0.5f ? "Whoa - careful!" : "Oops, I'm in the way.");
    return true;
}

/* Cold, and the pullover is put down somewhere (over a chair, on the ground): there, and on with it. */
#define JACKET_GIVE_UP 90.0f  /* s on the way: forget it */
#define PULLOVER_WALK  80.0f  /* m: further off than this it's forgotten (for now) */

static bool fetchPullover(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (!ai->want_jacket) return false;
    Object *pullover = self->pullover;
    if (!pullover || !self->pullover_down || self->look.top == TOP_JACKET || (ai->jacket_time += dt) > JACKET_GIVE_UP ||
        self->chill < 0.3f || flatDistance(pullover->position, self->position) > PULLOVER_WALK) {
        ai->want_jacket = false;
        return false;
    }
    bool at_hand = self->sitting && (pullover->cloth_host == self->seat ||
                                     (!self->seat && flatDistance(pullover->position, self->position) < 0.5f)); /* (on their chair, under them) */
    if (!at_hand) {
        if (self->sitting) self->sitting = false;
        if (!goTo(self, pullover, objects, count, 0.45f, dt)) return true;
    }
    Clothes_takeBack(self, true);
    ai->want_jacket = false;
    if (random01(ai) < 0.5f) Ai_say(self, "Ah, better.");
    rest(ai, 1.0f, 2.0f);
    return true;
}

/* Unloading the car (after the arrival): to the boot, the next thing out of
   it, carried to where it goes (crates by the fire, the camping things where
   the tents will be), put down; again, till the boot is empty. */
#define UNLOAD_TRIP 70.0f /* s on one trip: stuck, give it up */
#define UNLOAD_TIRED 0.25f /* stamina: a heavy thing put down for a breather ... */
#define UNLOAD_ON    0.55f /* ... and taken up again */

/* Something that comes out of the car's boot. */
static bool isCargo(const Object *o) {
    return o->type == OBJ_CRATE || o->type == OBJ_GEAR || o->type == OBJ_MATERIAL || (o->type == OBJ_SUPPLY && !o->is_static);
}

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

static bool goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int count, float dt) {
    float d = flatDistance(p, self->position);
    if (d < reach) { stand(self); return true; }
    if (!self->ai.path_len && d > 6.0f) findWay(self, p); /* (further: the way round from the start - hedges, walls) */
    Vec3 dir = ai_steer(self, NULL, objects, count, p);
    if (vec3_length(dir) < 0.5f) { stand(self); return false; }
    ai_walk(self, dir, fminf(self->ai.greet ? STROLL_SPEED * 1.15f : WALK_SPEED, d * 2.0f + 0.3f), dt); /* (going over to say hello: no hurry) */
    return false;
}

bool ai_goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int count, float dt) {
    return goToPoint(self, p, reach, objects, count, dt);
}

static bool unloadCar(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *car = ai->unload_car;
    if (!ai->unload || !car) { ai->unload = false; return false; }
    Object *item = carriedCargo(self);
    bool empty = !car->load || car->load->n == 0;
    bool waiting = ai->target && isCargo(ai->target) && !ai->target->is_held;
    if (!item && !waiting && empty) {
        ai->unload = false;
        if (random01(ai) < 0.5f) Ai_say(self, "That's everything out of the car.");
        return false;
    }
    if ((ai->unload_time += dt) > UNLOAD_TRIP) {
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == item && item) Hands_drop(self, h);
        ai->unload = false;
        return false;
    }
    if (self->sitting) self->sitting = false;
    if (ai->unload_rest) { /* put it down for a breather: up again when there's some strength back */
        stand(self);
        if (self->needs.stamina < UNLOAD_ON) return true;
        ai->unload_rest = false;
        ai->unload_time = 0.0f;
    }
    if (item) { /* carrying it there */
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == item && self->hands.state[h] == HAND_REACHING) { stand(self); return true; }
        if (self->needs.stamina < UNLOAD_TIRED && Condition_load(self) > 0.15f) { /* too heavy for the whole way: down for a moment */
            for (int h = 0; h < 2; h++) if (self->hands.item[h] == item) Hands_drop(self, h);
            ai->target = item;
            ai->unload_rest = true;
            stand(self);
            Ai_say(self, random01(ai) < 0.5f ? "Phew - heavy one. A moment." : "Hang on, my arms...");
            return true;
        }
        Vec3 spot = Car_dropSpot(g_world, item);
        Vec3 from = vec3_sub(self->position, spot);
        from.y = 0.0f;
        from = vec3_length(from) > 0.1f ? vec3_normalize(from) : vec3_scale(Object_facing(self), -1.0f);
        if (!goToPoint(self, vec3_add(spot, vec3_scale(from, 0.5f)), 0.25f, objects, count, dt)) return true; /* (the hands over the spot) */
        turnTowards(self, vec3_scale(from, -1.0f), dt);
        for (int h = 0; h < 2; h++) if (self->hands.item[h] == item) Hands_drop(self, h);
        item->position.x = spot.x; /* (right there: side by side, not on top of each other) */
        item->position.z = spot.z;
        Object_syncColliders(item);
        ai->target = NULL;
        ai->unload_time = 0.0f;
        return true;
    }
    if (waiting) { /* out of the boot, lying there: take it */
        Object *it = ai->target;
        if (Hands_gapFrom(self->position, self->look.height, it) < PICKUP_REACH) {
            stand(self);
            turnTowards(self, vec3_sub(it->position, self->position), dt);
            freeHands(self, NULL);
            if (!Hands_pickUpObject(self, self->main_hand, it) && !Hands_pickUpObject(self, 1 - self->main_hand, it)) ai->target = NULL;
            return true;
        }
        goToPoint(self, it->position, 0.3f, objects, count, dt);
        return true;
    }
    /* to the boot (a little to one side: room for the others) */
    Vec3 back = vec3_sub(Car_bootSpot(car), car->position), across = vec3_normalize(vec3(-back.z, 0.0f, back.x));
    Vec3 spot = vec3_add(Car_bootSpot(car), vec3_scale(across, ai->side * 0.4f));
    if (!goToPoint(self, spot, 0.45f, objects, count, dt)) return true;
    turnTowards(self, vec3_sub(car->position, self->position), dt);
    freeHands(self, NULL);
    if (car->boot_open && !ai->take_from) { ai->take_from = car; Hands_touch(self, vec3_add(car->position, vec3_scale(back, 0.45f))); }
    return true;
}

/* Tents: their own spread out where its bag lies and pitched; somebody at
   work on one near (you too): a hand lent, the willing; their own up: the
   sleeping bag and mat in at its door. Sleepy, their tent up: to bed in it. */
#define TENT_WALK   70.0f /* m: their tent (or its bag) this near */
#define TENT_HELP   25.0f /* m: somebody pitching one this near ... */
#define HELP_CHANCE 0.12f /* ... gets a hand this often a second (x willing) */
#define HELP_LONG   150.0f/* s: helping, then they've had enough */
#define BED_WALK    12.0f /* m: their sleeping bag or mat lying this near their tent: brought in */

static Object *ownGear(Object *self, Object *const *objects, int count, bool tent_bag) {
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_GEAR || o->owner != self || o->broken) continue;
        if (tent_bag == (o->variant >= GEAR_TENT_SMALL)) return o;
    }
    return NULL;
}

static Object *bedThing(Object *self, Object *const *objects, int count, const Object *tent) {
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type == OBJ_GEAR && o->owner == self && !o->broken && o->variant <= GEAR_MAT &&
            (!o->is_held || Hands_carries(self, OBJ_GEAR)) && flatDistance(o->position, tent->position) < BED_WALK) return o;
    }
    return NULL;
}

static void workOnTent(Object *self, Object *tent, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    float side = Tent_isTheirs(tent, self) ? (tent->owner2 == self ? -1.0f : 1.0f) : (ai->side != 0.0f ? -ai->side : -1.0f);
    Vec3 spot = Tent_workSpot(tent, side);
    if (tent->tent_stage == TENT_PEGS && !Tent_isTheirs(tent, self)) { /* (a helper: the next peg round from the owner's) */
        Vec3 out = vec3_sub(spot, tent->position);
        spot = vec3_add(spot, vec3_scale(vec3_normalize(vec3(-out.z, 0.0f, out.x)), 0.6f));
    }
    if (!goToPoint(self, spot, 0.35f, objects, count, dt)) return;
    freeHands(self, NULL);
    Vec3 hands = Tent_handsAt(tent, self, tent->tent_work * 18.0f);
    turnTowards(self, vec3_sub(hands, self->position), dt);
    Hands_touch(self, hands);
    int got = Tent_work(tent, self, dt);
    if (got >= TENT_DOWN) {
        static const char *const DOWN[] = { "Folding it up now.", "Poles down.", "Cover's off.", "", "" };
        if (got < TENT_DOWN_PACKED && random01(ai) < 0.5f) Ai_say(self, DOWN[got - TENT_DOWN]);
        else if (got == TENT_DOWN_PACKED && random01(ai) < 0.7f) Ai_say(self, "Packed. That one's done.");
    } else if (got >= 0 && random01(ai) < 0.6f) {
        static const char *const SAY[] = { "", "Poles in - up it goes.", "Now the cover over it.", "Pegs next - who's got a stone?", "There - it stands!" };
        Ai_say(self, SAY[got % 5]);
    }
}

/* Warm, the pullover tied round the waist: the tidier now and then hang it
   over a railing or a chair back near by instead (and fetch it when cold:
   fetchPullover). */
#define HANG_LOOK    30.0f  /* m: a railing, a bench or a chair this near */
#define HANG_CHANCE  0.015f /* a second (x tidiness), idle or wandering */
#define HANG_AGAIN   240.0f /* s: tried (or done): not again for this long */
#define HANG_TIDY    0.35f  /* this careful (ai_stackCare) at least */

/* A railing: a thin wall piece at about hip to chest height. */
static bool railing(const Object *o, float feet) {
    if (o->type != OBJ_WALL || o->collider_count < 1 || o->broken) return false;
    Vec3 h = o->colliders[0].half_extents;
    float thin = fminf(h.x, h.z), top = Object_colliderCenter(o, 0).y + h.y - feet;
    return thin <= 0.08f && fmaxf(h.x, h.z) > 0.3f && top > 0.6f && top < 1.4f;
}

static bool hangPullover(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    ai->hang_wait = fmaxf(0.0f, ai->hang_wait - dt);
    Object *on = ai->hang_on;
    if (!on) {
        if (!self->look.jacket_tied || self->pullover_down || self->chill > 0.1f || ai->hang_wait > 0.0f || !g_world || g_world->rain > 0.05f ||
            ai->want_jacket || self->sitting || ai_stackCare(self) < HANG_TIDY || random01(ai) > HANG_CHANCE * ai_stackCare(self) * dt) return false;
        ai->hang_wait = HANG_AGAIN;
        float best = HANG_LOOK;
        for (int i = 0; i < count; i++) {
            Object *o = objects[i];
            bool seat = o->type == OBJ_FURNITURE && (o->variant == FURN_CHAIR || o->variant == FURN_BENCH) && !o->broken && o->collider_count;
            if ((!seat && !railing(o, self->position.y)) || ai_skipped(ai, o)) continue;
            if (seat && o->variant == FURN_CHAIR && chairTaken(self, o, objects, count)) continue;
            float d = flatDistance(Object_colliderCenter(o, 0), self->position);
            if (d < best && fabsf(o->position.y - self->position.y) < 1.5f) { best = d; on = o; }
        }
        if (!on) { ai->hang_wait = HANG_AGAIN * 0.25f; return false; }
        ai->hang_on = on;
    }
    if (!self->look.jacket_tied || self->pullover_down) { ai->hang_on = NULL; return false; }
    /* beside it: a step off its middle (a railing: off its long side, our side; a chair: behind its back) */
    Vec3 c = Object_colliderCenter(on, 0), spot, aim;
    if (on->type == OBJ_WALL || on->variant == FURN_BENCH) { /* (a railing, a bench: over its long edge) */
        Vec3 h = on->colliders[0].half_extents;
        Vec3 across = h.x < h.z ? on->colliders[0].axes[0] : on->colliders[0].axes[2];
        Vec3 along  = h.x < h.z ? on->colliders[0].axes[2] : on->colliders[0].axes[0];
        Vec3 to = vec3_sub(self->position, c);
        if (vec3_dot(to, across) < 0.0f) across = vec3_scale(across, -1.0f);
        float len = fmaxf(h.x, h.z), run = clampf(vec3_dot(to, along), -len + 0.3f, len - 0.3f);
        Vec3 on_it = vec3_add(c, vec3_scale(along, run));
        spot = vec3_add(on_it, vec3_scale(across, 0.5f));
        aim = vec3(on_it.x, c.y + h.y - 0.05f, on_it.z);
    } else {
        spot = vec3_add(on->position, mat4_mul_dir(mat4_rotate_y(on->rotation.y), vec3(0.0f, 0.0f, -on->size.z * 0.5f - 0.45f)));
        aim = vec3_add(on->position, vec3(0.0f, on->size.y, 0.0f));
    }
    spot.y = self->position.y;
    if (!goToPoint(self, spot, 0.3f, objects, count, dt)) return true;
    Vec3 eye = vec3_add(self->position, vec3(0.0f, self->look.height * 0.92f, 0.0f));
    Vec3 look = vec3_normalize(vec3_sub(aim, eye));
    turnTowards(self, vec3(look.x, 0.0f, look.z), dt);
    ClothSpot s;
    if (Clothes_spot(g_world, self, look, &s) && s.lay == CLOTH_HUNG && Clothes_putDown(g_world, self, &s)) {
        if (random01(ai) < 0.4f) Ai_say(self, "Too warm for this - it can hang here.");
    } else {
        skipTarget(ai, on);
    }
    ai->hang_on = NULL;
    rest(ai, 1.0f, 2.0f);
    return true;
}

/* Their tent blowing away: after it (whatever they were doing), and hold it down. */
static bool chaseTent(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (self->body.asleep || self->in_tent || self->indoors > 0.0f || self->riding || ai->unload) return false;
    Object *tent = Tent_of(objects, count, self);
    if (!tent || tent->tent_blown <= 0.0f || flatDistance(tent->position, self->position) > TENT_WALK) return false;
    if (tent->tent_idle > 3.0f && ai->speech_time <= 0.0f && random01(ai) < 0.5f * dt)
        Ai_say(self, random01(ai) < 0.5f ? "Hey - my tent!" : "No no no - come back here!");
    if (self->sitting) self->sitting = false;
    workOnTent(self, tent, objects, count, dt);
    return true;
}

/* The wind took something out of their hand: after it and pick it up (not
   for ever: gone too far, or somebody else has it, never mind). */
#define BLOWN_GIVE_UP 25.0f /* s */
#define BLOWN_FAR     30.0f /* m */
static bool fetchBlown(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *it = ai->blown;
    if (!it) return false;
    ai->blown_t += dt;
    if (it->is_held || it->broken || ai->blown_t > BLOWN_GIVE_UP || flatDistance(it->position, self->position) > BLOWN_FAR ||
        self->body.asleep || self->in_tent || self->indoors > 0.0f || self->riding) {
        if (!it->is_held && ai->blown_t > BLOWN_GIVE_UP && ai->speech_time <= 0.0f) Ai_say(self, "Oh, forget it.");
        ai->blown = NULL;
        return false;
    }
    if (self->sitting) self->sitting = false;
    int hand = Hands_freeHand(self);
    if (hand < 0) { ai->blown = NULL; return false; }
    if (it->is_grounded && Hands_gapFrom(self->position, self->look.height, it) < PICKUP_REACH) {
        stand(self);
        turnTowards(self, vec3_sub(it->position, self->position), 1.0f);
        if (Hands_pickUpObject(self, hand, it)) ai->blown = NULL;
        return true;
    }
    goToPoint(self, it->position, 0.3f, objects, count, dt);
    return true;
}

static bool pitchTents(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (ai->unload || self->body.asleep || self->in_tent) return false;
    Object *tent = Tent_of(objects, count, self);
    /* their own tent, not up yet */
    if (tent && tent->tent_stage < TENT_UP && flatDistance(tent->position, self->position) < TENT_WALK) {
        workOnTent(self, tent, objects, count, dt);
        return true;
    }
    if (!tent) { /* still in its bag: spread it out where it lies */
        Object *bag = ownGear(self, objects, count, true);
        if (bag && !bag->is_held && !bag->keep && flatDistance(bag->position, self->position) < TENT_WALK) {
            if (self->sitting) self->sitting = false;
            if (!goToPoint(self, bag->position, 0.6f, objects, count, dt)) return true;
            freeHands(self, NULL);
            Hands_touch(self, bag->position);
            if (Tent_unpack(g_world, bag, Car_campDoor()) && random01(ai) < 0.7f) Ai_say(self, "Right - let's get the tent up.");
            return true;
        }
    }
    /* somebody pitching one near: a hand */
    Object *job = ai->tent_job;
    if (job && (job->broken || (job->tent_stage >= TENT_UP && !job->tent_down) || (ai->tent_time += dt) > HELP_LONG || job->tent_idle > 20.0f)) job = ai->tent_job = NULL;
    if (!job && (ai->mode == AI_IDLE || ai->mode == AI_WANDER)) {
        for (int i = 0; i < count && !job; i++) {
            Object *t = objects[i];
            if (self->lives_here) break; /* (the owners: not their job) */
            if (t->type != OBJ_TENT || t->tent_stage >= TENT_UP || t->tent_idle > 1.5f || Tent_isTheirs(t, self) ||
                flatDistance(t->position, self->position) > TENT_HELP) continue;
            if (random01(ai) < HELP_CHANCE * dt * (0.5f + ai->character.willing)) job = t;
        }
        if (job) {
            ai->tent_job = job;
            ai->tent_time = 0.0f;
            if (random01(ai) < 0.8f) Ai_say(self, "Wait - I'll give you a hand with that.");
        }
    }
    if (job) { workOnTent(self, job, objects, count, dt); return true; }
    /* their own up: the sleeping bag and the mat in at the door */
    if (tent && tent->tent_stage >= TENT_COVER) {
        Object *thing = bedThing(self, objects, count, tent);
        if (!thing) return false;
        if (thing->is_held) {
            Vec3 door = Tent_door(tent);
            if (!goToPoint(self, door, 0.3f, objects, count, dt)) return true;
            for (int h = 0; h < 2; h++) if (self->hands.item[h] == thing) Hands_drop(self, h);
            thing->position.x = door.x; /* (in at the door) */
            thing->position.z = door.z;
            Object_syncColliders(thing);
            return true;
        }
        if (Hands_gapFrom(self->position, self->look.height, thing) < PICKUP_REACH) {
            stand(self);
            if (!Hands_pickUpObject(self, Hands_freeHand(self) >= 0 ? Hands_freeHand(self) : self->main_hand, thing)) freeHands(self, NULL);
            return true;
        }
        goToPoint(self, thing->position, 0.3f, objects, count, dt);
        return true;
    }
    return false;
}

/* Kurti and his sons live here: now and then they go into the house for a
   while (out of sight; called, they come out). */
#define INSIDE_CHANCE 0.005f  /* a second, idle or wandering */
#define INSIDE_MIN    90.0f   /* s in there ... */
#define INSIDE_MORE   300.0f  /* ... up to this much longer */
#define INSIDE_WALK   60.0f   /* m: the door this near */

static bool goInside(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (!self->lives_here || self->indoors > 0.0f || self->body.asleep) return false;
    Vec3 door = Site_houseDoor();
    if (!ai->going_in) {
        if (random01(ai) > INSIDE_CHANCE * dt || flatDistance(door, self->position) > INSIDE_WALK || Hands_freeHand(self) < 0) return false;
        ai->going_in = true;
        if (random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "I'll be inside for a bit." : "Back in a while.");
    }
    if (self->sitting) self->sitting = false;
    if (!goToPoint(self, door, 0.5f, objects, count, dt)) return true;
    ai->going_in = false;
    self->indoors = INSIDE_MIN + INSIDE_MORE * random01(ai);
    rest(ai, 1.0f, 2.0f);
    return true;
}

/* Rain (out in it): into the house till it's over (people camp in tents,
   but sit out a shower inside). */
#define SHELTER_RAIN 0.3f

#define UMBRELLA_FETCH 25.0f /* m: an umbrella lying this near is fetched when it starts to rain */

static bool holdsUmbrella(const Object *self) {
    for (int h = 0; h < 2; h++)
        if (self->hands.item[h] && self->hands.item[h]->type == OBJ_UMBRELLA && !self->hands.item[h]->umbrella_ruined && self->hands.state[h] == HAND_HOLDING) return true;
    return false;
}

/* The open umbrella this one holds up, if any. */
static const Object *openUmbrella(const Object *o) {
    for (int h = 0; h < 2; h++) {
        const Object *u = o->hands.item[h];
        if (u && u->type == OBJ_UMBRELLA && u->umbrella_open && o->hands.state[h] == HAND_HOLDING) return u;
    }
    return NULL;
}

#define SHARE_SEE   12.0f  /* m: somebody under an umbrella this near, nobody with them: "can I come under?" */
#define SHARE_MAX  240.0f  /* s at most along with them (then off under a roof after all) */
#define SHARE_GAP    0.53f /* m beside them (shoulder to shoulder: their bodies just clear) */

/* Somebody near holding an umbrella up, nobody under it with them yet. */
static Object *umbrellaToShare(const Object *self, Object *const *objects, int count) {
    Object *best = NULL;
    float best_d = SHARE_SEE;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o == self || !o->is_character || o->type == OBJ_DOG || !openUmbrella(o) || Body_isDown(&o->body)) continue;
        if (o->umbrella_guest && o->umbrella_guest != self && o->umbrella_guest_t > 0.0f) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) { best_d = d; best = o; }
    }
    return best;
}

/* Under somebody's umbrella: at their side (the side they hold it on), keeping up with them. */
static bool walkUnder(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    Object *h = ai->share;
    const Object *u = h ? openUmbrella(h) : NULL;
    if (!h || !u || !g_world || g_world->rain < SHELTER_RAIN * 0.5f || (ai->share_time += dt) > SHARE_MAX || self->indoors > 0.0f ||
        self->in_tent || Body_isDown(&self->body) || Body_isDown(&h->body)) {
        if (h && h->umbrella_guest == self) h->umbrella_guest = NULL;
        ai->share = NULL;
        return false;
    }
    h->umbrella_guest = self;
    h->umbrella_guest_t = 0.5f;
    if (self->sitting) self->sitting = false; /* (up first) */
    Vec3 facing = Object_facing(h), right = vec3_normalize(vec3_cross(facing, UP));
    float side = h->hands.item[1] == u ? 1.0f : -1.0f; /* (the umbrella between them) */
    Vec3 spot = vec3_add(h->position, vec3_add(vec3_scale(right, side * SHARE_GAP * h->look.height / 1.75f), vec3_scale(facing, -0.05f)));
    float d = flatDistance(spot, self->position);
    float with = vec3_length(vec3(h->body.velocity.x, 0.0f, h->body.velocity.z)); /* (keeping up with them) */
    if (d < (with > 0.3f ? 0.12f : 0.3f)) { stand(self); turnTowards(self, facing, dt); return true; } /* (they stand: so do we - no circling round the spot) */
    float speed = fminf(d > 3.0f ? runSpeed(self) * 0.8f : WALK_SPEED * 1.3f, with + d * 1.5f + 0.1f);
    if (d < 1.5f) { /* close: straight onto the spot, a step aside or along, facing the way they go (no circling round it) */
        Vec3 to = vec3_scale(flat(vec3_sub(spot, self->position)), 1.0f / d);
        self->movement = vec3_scale(to, speed);
        turnTowards(self, with > 0.3f ? flat(h->body.velocity) : facing, dt);
        Vec3 f = Object_facing(self), r = vec3_normalize(vec3_cross(f, UP));
        self->move_local = vec3(vec3_dot(to, r), 0.0f, vec3_dot(to, f));
        ai->walk_dir = vec3_zero();
        return true;
    }
    Vec3 dir = ai_steer(self, h, objects, count, spot);
    if (vec3_length(dir) < 0.5f) { stand(self); return true; }
    ai_walk(self, dir, speed, dt);
    return true;
}

#define RAIN_SIT    0.45f /* rain this hard: down on a seat in there to wait it out (lighter: they stand about) */
#define SEAT_LOOK   6.5f  /* m round the middle of the room: the seats in it */

/* A chair nobody sits on, or a bench with room on it, in the room: the nearest. */
static Object *freeSeatIn(const Object *self, Object *const *objects, int count, Vec3 room) {
    Object *best = NULL;
    float best_d = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->type != OBJ_FURNITURE || (o->variant != FURN_CHAIR && o->variant != FURN_BENCH) || flatDistance(o->position, room) > SEAT_LOOK) continue;
        if (o->variant == FURN_CHAIR) {
            bool taken = false;
            for (int j = 0; j < count && !taken; j++) taken = objects[j] != self && objects[j]->is_character && objects[j]->seat == o;
            if (taken) continue;
        } else if (Object_benchRoom(self, o, self->position, false) == ROOM_NONE) continue;
        float d = flatDistance(o->position, self->position);
        if (d < best_d) { best_d = d; best = o; }
    }
    return best;
}

/* In under the roof of your garages' kitchen; raining hard, down on a free seat (or on the floor) till it's over. */
static bool rainRoom(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (self->sitting && ai->rain_seated) { rest(ai, 3.0f, 6.0f); return true; } /* (sitting it out) */
    ai->rain_seated = false;
    if (self->sitting) self->sitting = false;
    Vec3 places[4];
    int n = Site_rainPlaces(places, 4);
    if (ai->rain_place <= 0 || ai->rain_place > n) { /* where to: the nearest with a seat free (else just the nearest) */
        float best = 1e9f;
        for (int k = 0; k < n; k++) {
            float d = flatDistance(places[k], self->position) + (freeSeatIn(self, objects, count, places[k]) ? 0.0f : 60.0f);
            if (d < best) { best = d; ai->rain_place = k + 1; }
        }
    }
    Vec3 room = places[ai->rain_place - 1];
    Vec3 spot = vec3_add(room, vec3(0.8f * ai->side, 0.0f, 0.8f * ai->side));
    if (flatDistance(room, self->position) > 3.5f || g_world->rain < RAIN_SIT) {
        if (goToPoint(self, spot, 0.7f, objects, count, dt)) rest(ai, 3.0f, 6.0f);
        return true;
    }
    Object *seat = freeSeatIn(self, objects, count, room);
    if (seat) {
        Vec3 at = seat->position; /* (a chair: up to it; a bench: in front of it, where along it we are - not round its end, through a post) */
        float near = 0.9f;
        if (seat->variant == FURN_BENCH) {
            Vec3 axis = seat->colliders[0].axes[0], front = seat->table ? vec3_sub(seat->position, seat->table->position) /* (a beer bench: from behind it, over it) */
                                                                         : vec3_sub(room, seat->position);
            float half = fmaxf(0.0f, 0.5f * seat->size.x - 0.4f), along = clampf(vec3_dot(vec3_sub(self->position, seat->position), axis), -half, half);
            front = flat(vec3_sub(front, vec3_scale(axis, vec3_dot(front, axis))));
            front = vec3_length(front) > 1e-3f ? vec3_normalize(front) : vec3(0.0f, 0.0f, 1.0f);
            at = vec3_add(vec3_add(seat->position, vec3_scale(axis, along)), vec3_scale(front, 0.5f));
            near = 0.3f;
        }
        if (flatDistance(at, self->position) > near && !goToPoint(self, at, near, objects, count, dt)) return true;
        stand(self);
        if (Object_sit(self, seat, room)) {
            ai->rain_seated = true;
            if (random01(ai) < 0.3f) Ai_say(self, random01(ai) < 0.5f ? "We'll sit this one out." : "Might as well sit down.");
            rest(ai, 3.0f, 6.0f);
            return true;
        }
    }
    /* no seat free: down on the floor where they stand (near the middle) */
    if (!goToPoint(self, spot, 0.7f, objects, count, dt)) return true;
    Object_sit(self, NULL, room);
    ai->rain_seated = true;
    rest(ai, 3.0f, 6.0f);
    return true;
}

static bool shelter(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (holdsUmbrella(self)) { ai->sheltering = false; ai->umbrella = NULL; ai->share = NULL; return false; } /* (out in it under that) */
    if (ai->share && walkUnder(self, objects, count, dt)) return true;
    Object *u = ai->umbrella;
    if (u && (u->is_held || !g_world || g_world->rain < SHELTER_RAIN * 0.5f || Hands_freeHand(self) < 0)) u = ai->umbrella = NULL;
    if (u) { /* over to it and up with it */
        if (self->sitting) self->sitting = false;
        if (Hands_gapFrom(self->position, self->look.height, u) < PICKUP_REACH) {
            stand(self);
            if (Hands_pickUpObject(self, Hands_freeHand(self), u) && random01(ai) < 0.6f)
                Ai_say(self, random01(ai) < 0.5f ? "I'll take the umbrella." : "Good thing there's an umbrella.");
            ai->umbrella = NULL;
            return true;
        }
        if ((ai->timer += dt) > 40.0f) { skipTarget(ai, u); ai->umbrella = NULL; return true; } /* (can't get at it) */
        goToPoint(self, u->position, 0.3f, objects, count, dt);
        return true;
    }
    if (ai->rain_seated && self->sitting && g_world && g_world->rain >= SHELTER_RAIN * 0.5f) { rest(ai, 3.0f, 6.0f); return true; } /* (sat down: till it's nearly over) */
    if (!g_world || g_world->rain < SHELTER_RAIN || self->indoors > 0.0f || self->in_tent || self->body.asleep || self->riding) {
        if (!g_world || g_world->rain < SHELTER_RAIN * 0.5f) {
            ai->sheltering = false;
            ai->rain_place = 0;
            if (ai->rain_seated) { /* (over: up again) */
                ai->rain_seated = false;
                if (self->sitting && random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "It's stopped raining." : "Right, the rain's over.");
                self->sitting = false;
            }
        }
        return false;
    }
    if (!ai->sheltering) {
        if (!self->rain_out) return false;
        Object *holder = umbrellaToShare(self, objects, count);
        if (holder && !self->lives_here && !Body_isDown(&self->body)) { /* "can I come under?" - "sure, come here" */
            ai->share = holder;
            ai->share_time = 0.0f;
            holder->umbrella_guest = self;
            holder->umbrella_guest_t = 2.0f;
            Ai_say(self, random01(ai) < 0.5f ? "Room under there for me?" : "Can I come under your umbrella?");
            if (holder->type != OBJ_PLAYER && holder->ai.speech_time <= 0.0f) Ai_say(holder, random01(ai) < 0.5f ? "Sure, come here." : "Come on, squeeze in.");
            return walkUnder(self, objects, count, dt);
        }
        if (Hands_freeHand(self) >= 0 && ai->mode != AI_WORK && !self->lives_here) { /* an umbrella near: that instead */
            Object *best = NULL;
            float best_d = UMBRELLA_FETCH;
            for (int i = 0; i < count; i++) {
                Object *o = objects[i];
                if (o->type != OBJ_UMBRELLA || o->is_held || o->umbrella_ruined || ai_skipped(ai, o)) continue;
                bool claimed = false;
                for (int j = 0; j < count && !claimed; j++) claimed = objects[j] != self && objects[j]->is_character && objects[j]->ai.umbrella == o;
                float d = flatDistance(o->position, self->position);
                if (!claimed && d < best_d) { best_d = d; best = o; }
            }
            if (best) { ai->umbrella = best; ai->timer = 0.0f; return true; }
        }
        ai->sheltering = true;
        if (random01(ai) < 0.6f) Ai_say(self, random01(ai) < 0.5f ? "It's pouring - inside, quick!" : self->lives_here ? "Rain! Into the house." : "Rain! I'm off under a roof.");
        for (int h = 0; h < 2; h++) { /* (crates and gear down where they are; tools along) */
            const Object *it = self->hands.item[h];
            if (it && (isCargo(it) || it == ai->fetch)) Hands_drop(self, h);
        }
    }
    if (self->sitting && !ai->rain_seated) self->sitting = false;
    if (!self->lives_here) { /* (the house is Kurti's: you lot wait it out in your tent, or in your garages' kitchen) */
        Object *tent = Tent_of(objects, count, self);
        bool night = g_world->hour >= 21.0f || g_world->hour < 7.0f;
        if (tent && tent->tent_stage >= TENT_UP && self->body.pose == POSE_UP
            && (night || flatDistance(tent->position, self->position) < flatDistance(Site_rainRoom(), self->position))) {
            if (!goToPoint(self, Tent_door(tent), 0.4f, objects, count, dt)) return true;
            freeHands(self, NULL);
            Tent_crawlIn(tent, self);
            self->rain_nap = true;
            rest(ai, 2.0f, 3.0f);
            return true;
        }
        return rainRoom(self, objects, count, dt);
    }
    if (!goToPoint(self, Site_houseDoor(), 0.6f, objects, count, dt)) return true;
    self->indoors = 30.0f; /* (kept while it rains: see world.c) */
    rest(ai, 1.0f, 2.0f);
    return true;
}

/* Wet through after the rain: over to a burning fire, sitting by it till dry. */
#define DRY_START  0.45f /* wet this much ... */
#define DRY_DONE   0.1f  /* ... dry enough */
#define DRY_WALK   60.0f /* m: a burning fire this near */

static bool dryOff(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (!g_world || self->body.asleep || self->in_tent || self->indoors > 0.0f || self->riding) return false;
    Object *fire = Camp_fireNear(objects, count, self->position, DRY_WALK, true);
    if (!ai->drying) {
        if (self->look.wet < DRY_START || g_world->rain > 0.1f || !fire || random01(ai) > 0.3f * dt) return false;
        ai->drying = true;
        if (random01(ai) < 0.6f) Ai_say(self, random01(ai) < 0.5f ? "Soaked. I'm going to dry off by the fire." : "Wet through - fire, here I come.");
    }
    if (self->look.wet < DRY_DONE || !fire || g_world->rain > 0.3f) { ai->drying = false; return false; }
    float d = flatDistance(fire->position, self->position);
    if (self->sitting && d < 4.0f) { rest(ai, 5.0f, 10.0f); return true; }
    Vec3 to = vec3_sub(self->position, fire->position);
    to.y = 0.0f;
    to = d > 0.1f ? vec3_scale(to, 1.0f / d) : vec3(1.0f, 0.0f, 0.0f);
    if (d > 3.4f && !goToPoint(self, vec3_add(fire->position, vec3_scale(to, 2.4f)), 1.0f, objects, count, dt)) return true;
    stand(self);
    Object_sit(self, Object_seatNear(objects, count, self->position, 1.5f), fire->position);
    rest(ai, 5.0f, 10.0f);
    return true;
}

/* Just arrived: over to the fire to say hello to Kurti and his sons, and sit
   down with them till the work starts (game_arrival.c). */
#define GREET_BENCH 5.0f /* m: benches this near the fire are sat on */
#define GREET_STAND 3.7f /* m from the fire: where they stand if there's no room on them */
#define GREET_ASK   3.0f /* m: a bench where they'd have to ask counts as this much further away */

/* No room on the benches: a free spot in the ring round the fire, nearest first. */
static Vec3 standByFire(const Object *self, const Object *fire, Object *const *objects, int count) {
    Vec3 to = flat(vec3_sub(self->position, fire->position));
    float a0 = atan2f(to.z, to.x);
    for (int k = 0; k < 24; k++) {
        float a = a0 + 0.26f * (float)((k + 1) / 2) * (k % 2 ? 1.0f : -1.0f);
        Vec3 at = vec3_add(fire->position, vec3(cosf(a) * GREET_STAND, 0.0f, sinf(a) * GREET_STAND));
        bool free = !ai_blockedBy(self, NULL, NULL, objects, count, at);
        for (int i = 0; i < count && free; i++) {
            const Object *o = objects[i];
            if (o != self && o->is_character && o->ai.greet_stand && flatDistance(o->ai.greet_at, at) < 0.9f) free = false;
        }
        if (free) return at;
    }
    return vec3_add(fire->position, vec3_scale(vec3_length(to) > 0.1f ? vec3_normalize(to) : vec3(1.0f, 0.0f, 0.0f), GREET_STAND));
}

/* Just arrived: over to the fire, sitting down on a bench there with Kurti and
   his sons (the others move up for them - the unwilling only when asked); no
   room anywhere: standing by the fire with them. Then the hello. */
#define GREET_NEAR 6.0f /* m from the fire: there, saying hello */

static bool greetHosts(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (!ai->greet) { ai->greet_stand = ai->greet_asked = false; return false; }
    Object *fire = Camp_fireNear(objects, count, self->position, 120.0f, false);
    if (!fire) { ai->greet = false; return false; }
    if (ai->greet_left > 0.0f && flatDistance(fire->position, self->position) < GREET_NEAR && (ai->greet_left -= dt) <= 0.0f) {
        ai->greet = ai->greet_stand = false; /* (turned up later: a while at the fire, then on with the day) */
        return false;
    }
    if (self->sitting) return false; /* (there: whatever they do sitting) */
    if (ai->greet_left > 0.0f && !ai->greet_stand) { /* (turned up later: hello standing by the fire - the benches are the hosts') */
        ai->greet_stand = true;
        ai->greet_at = standByFire(self, fire, objects, count);
    }
    if (ai->greet_stand) { /* standing with the others, facing the fire */
        if (!goToPoint(self, ai->greet_at, 0.35f, objects, count, dt)) return true;
        stand(self);
        turnTowards(self, flat(vec3_sub(fire->position, self->position)), dt);
        return true;
    }
    Object *best = NULL;
    bool best_asked = false;
    float best_d = 1e9f;
    for (int i = 0; i < count; i++) {
        Object *b = objects[i];
        if (b->type != OBJ_FURNITURE || b->variant != FURN_BENCH || flatDistance(b->position, fire->position) > GREET_BENCH) continue;
        int room = Object_benchRoom(self, b, self->position, false);
        bool asked = false;
        if (room == ROOM_NONE) { room = Object_benchRoom(self, b, self->position, true); asked = true; }
        if (room == ROOM_NONE) continue;
        float d = flatDistance(b->position, self->position) + (asked ? GREET_ASK : 0.0f);
        if (d < best_d) { best_d = d; best = b; best_asked = asked; }
    }
    if (!best) {
        ai->greet_stand = true;
        ai->greet_at = standByFire(self, fire, objects, count);
        return true;
    }
    /* in front of it, on the fire's side, where along it they come to it */
    Vec3 axis = flat(best->colliders[0].axes[0]), out = flat(vec3_sub(fire->position, best->position));
    axis = vec3_length(axis) > 1e-3f ? vec3_normalize(axis) : vec3(1.0f, 0.0f, 0.0f);
    out = vec3_length(out) > 1e-3f ? vec3_normalize(out) : vec3(0.0f, 0.0f, 1.0f);
    float half = fmaxf(best->colliders[0].half_extents.x - 0.28f, 0.0f);
    float along = clampf(vec3_dot(vec3_sub(self->position, best->position), axis), -half, half);
    Vec3 spot = vec3_add(best->position, vec3_add(vec3_scale(axis, along), vec3_scale(out, 0.6f)));
    if (!goToPoint(self, spot, 0.5f, objects, count, dt)) return true;
    if (best_asked && !ai->greet_asked) { /* "move up a bit?" - and they do, the grumpy grumbling */
        ai->greet_asked = true;
        Ai_say(self, random01(ai) < 0.5f ? "Move up a bit?" : "Room for one more?");
        for (int i = 0; i < count; i++) {
            Object *o = objects[i];
            if (o == self || !o->is_character || o->seat != best || !o->sitting || o->type == OBJ_PLAYER) continue;
            Ai_say(o, o->ai.character.willing > 0.5f ? "Sure, sit down." : "Hm. Alright then.");
            break;
        }
    }
    Object_benchMakeRoom(self, best, self->position, best_asked);
    if (!Object_sit(self, best, fire->position)) return true; /* (not yet: again next time) */
    rest(ai, 300.0f, 400.0f);
    if (random01(ai) < 0.7f) Ai_say(self, random01(ai) < 0.5f ? "Hello hello! Good to be back." : "Kurti! Still the king of the fire?");
    return true;
}

/* Sleepy, their tent up: over to it and in. */
#define WORK_BEDTIME 0.9f /* sleepiness: working, they keep going till this (idle: 0.8) */

static bool goToBed(Object *self, Object *const *objects, int count, float dt) {
    if (self->needs.sleepiness < 0.8f || self->body.asleep || self->in_tent || self->body.pose != POSE_UP) return false;
    Object *tent = Tent_of(objects, count, self);
    if (!tent || tent->tent_stage < TENT_UP || flatDistance(tent->position, self->position) > TENT_WALK) return false;
    if (self->sitting) self->sitting = false;
    if (self->ai.mode == AI_WORK) freeHands(self, NULL); /* (the work put down where it is) */
    if (!goToPoint(self, Tent_door(tent), 0.4f, objects, count, dt)) return true;
    freeHands(self, NULL);
    if (random01(&self->ai) < 0.5f) Ai_say(self, self->ai.mode == AI_WORK ? "That's it, I'm done in. Night." : "Night, everybody.");
    Tent_crawlIn(tent, self);
    rest(&self->ai, 2.0f, 3.0f);
    return true;
}

/* A finished cigarette in the fingers (the tidy): to the tin, a bin or the
   fire with it; none near: somebody's pocket ashtray (asked for), else wrapped
   up and into the pocket. Butts in the pocket (ashtray) go into the tin, a
   bin or the fire when passing by - or, the ashtray filling up, on purpose. */
#define BUTT_WALK   30.0f /* m: a place for it this near: walked over */
#define BUTT_ASK    6.0f  /* m: somebody this near with a pocket ashtray: asked */
#define BUTT_WAIT   25.0f /* s: asked and nothing came: into the pocket */
#define BUTT_KEEP   90.0f /* s: still holding it: into the pocket */
#define BUTT_PASS   2.0f  /* m: passing this near, the pocket's emptied */
#define TRAY_FULL   8     /* butts: off to empty the pocket ashtray */

static bool buttGoesIn(const Object *o) {
    return (o->type == OBJ_FURNITURE && o->variant == FURN_ASHTRAY) || o->type == OBJ_BIN || (o->type == OBJ_FIREPLACE && o->burning);
}

static Object *buttPlaceNear(const Object *self, Object *const *objects, int count, float range) {
    Object *best = NULL;
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (!buttGoesIn(o) || ai_skipped(&self->ai, o)) continue;
        float d = flatDistance(o->position, self->position);
        if (d < range) { range = d; best = o; }
    }
    return best;
}

static void buttsInto(Object *self, Object *place, int n) {
    if (place->type == OBJ_FURNITURE) place->stored += n; /* (the tin fills up; a bin or the fire: gone) */
    Hands_touch(self, vec3_add(Object_colliderCenter(place, 0), vec3(0.0f, place->type == OBJ_FIREPLACE ? 0.0f : 0.3f, 0.0f)));
}

static bool binButt(Object *self, Object *const *objects, int count, float dt) {
    Ai *ai = &self->ai;
    if (self->pocket_butts > 0) { /* passing by: emptied */
        Object *at = buttPlaceNear(self, objects, count, BUTT_PASS);
        if (at && !self->sitting) { buttsInto(self, at, self->pocket_butts); self->pocket_butts = 0; }
    }
    if (!self->butt_held) {
        if (!self->pocket_tray || self->pocket_butts < TRAY_FULL || ai->mode == AI_TABLE) return false;
        Object *place = buttPlaceNear(self, objects, count, BUTT_WALK); /* (the ashtray full: emptied) */
        if (!place) return false;
        if (goTo(self, place, objects, count, 0.6f, dt)) { buttsInto(self, place, self->pocket_butts); self->pocket_butts = 0; }
        return true;
    }
    ai->butt_time += dt;
    if (ai->butt_time > BUTT_KEEP || (ai->butt_asked && ai->butt_time > BUTT_WAIT && !ai->asking_you)) {
        Pockets_keepButt(self);
        if (random01(ai) < 0.4f) Ai_say(self, "I'll keep it till I find a bin.");
        return false;
    }
    if (ai->butt_asked) { /* (waiting for the player's ashtray) */
        if (ai->butt_time > BUTT_WAIT) { ai->asking_you = 0; Pockets_keepButt(self); Ai_say(self, "Never mind, I'll keep it."); }
        return false;
    }
    Object *place = buttPlaceNear(self, objects, count, BUTT_WALK);
    if (place) {
        if (self->sitting) self->sitting = false;
        if (ai->butt_time > 40.0f) { skipTarget(ai, place); return false; } /* (can't get there) */
        if (!goTo(self, place, objects, count, place->type == OBJ_FIREPLACE ? 0.4f : 0.6f, dt)) return true;
        buttsInto(self, place, 1);
        self->butt_held = false;
        if (place->type == OBJ_BIN && random01(ai) < 0.25f) Ai_say(self, "There we go.");
        return true;
    }
    ai->butt_asked = true;
    Object *giver = Pockets_hasNear(self, POCKET_ASHTRAY, objects, count, BUTT_ASK);
    if (!giver) return false; /* (keeps it a while: maybe they pass a bin) */
    if (giver->type == OBJ_PLAYER) {
        Ai_say(self, "No bin anywhere - got your pocket ashtray?");
        ai->asking_you = (int)POCKET_ASHTRAY; /* (see the pockets: B, hold it out) */
        return false;
    }
    Ai_say(self, withName("%s, can I use your pocket ashtray?", giver));
    Pockets_give(giver, self, POCKET_ASHTRAY);
    if (giver->type == OBJ_HUMAN) Ai_say(giver, "Sure, put it in.");
    return false;
}

void Ai_update(Object *self, Object *const *objects, int count, float dt) {
    ai_ensureReady(self);
    Ai *ai = &self->ai;
    self->body.look_weight = 0.0f;
    self->body.dance_goal = 0.0f; /* (dancing: ai_fest.c, every frame) */
    ai->speech_time   = fmaxf(0.0f, ai->speech_time - dt);
    ai->talk_cooldown = fmaxf(0.0f, ai->talk_cooldown - dt);
    for (int i = 0; i < AI_SKIPS; i++) ai->skip_time[i] = fmaxf(0.0f, ai->skip_time[i] - dt);
    ai->path_none     = fmaxf(0.0f, ai->path_none - dt);
    ai->steer_hold    = fmaxf(0.0f, ai->steer_hold - dt);
    ai->boxed         = fmaxf(0.0f, ai->boxed - dt);
    ai->annoyed       = fmaxf(0.0f, ai->annoyed - dt / ANNOYED_FADE);
    g_dt = dt;
    ai_commitment(self, objects, count, dt);
    if (ai->meeting) { /* (in Berlin: sat at the table, listening) */
        stand(self);
        ai->mode = AI_IDLE;
        return;
    }
    if (Body_isBusy(&self->body)) { /* on the ground, or being sick */
        stand(self);
        ai->pushing = false;
        self->sitting = false;
        return;
    }
    if (ai_climbing(self, dt)) return; /* (up or down a ladder, over an edge: that first - whatever it was for) */
    if (ai->dodge_t > 0.0f && !self->body.asleep && !self->in_tent && self->indoors <= 0.0f) { /* warned off: out of the way first */
        ai->dodge_t -= dt;
        self->sitting = false;
        if (!goToPoint(self, ai->dodge_to, 0.4f, objects, count, dt)) return;
        ai->dodge_t = 0.0f;
        stand(self);
    }
    if (self->on_fire > 0.0f) { /* burning: run, then throw themselves down and roll */
        self->sitting = false;
        if (self->on_fire < dt * 1.5f) Ai_say(self, "Aaah! I'm on fire!");
        if (self->on_fire < 1.6f) {
            Vec3 away = rotateY(Object_facing(self), sinf(self->on_fire * 5.0f) * 0.8f);
            ai_walk(self, away, runSpeed(self), dt);
        } else {
            stand(self);
            Body_dropAndRoll(&self->body, Object_facing(self));
        }
        return;
    }
    ai_sunShirt(self, dt);
    if (takeBack(self)) return;
    lighterManners(self, objects, count, dt);
    smokeBreak(self, objects, count, dt);
    if (stepAside(self, dt)) return;   /* (a pushed load coming at us) */
    if (ai->mode != AI_IDLE && ai->mode != AI_TABLE && !(ai->mode == AI_WORK && ai->order == ORDER_FIRE)) self->sitting = false; /* up again to do anything */
    smell(self, objects, count, dt);
    if (ai->talking) {
        listen(self, objects, count, dt);
        return;
    }
    if (ai->mode == AI_IDLE || ai->mode == AI_WANDER) {
        Object *sleeper = findSleeper(self, objects, count);
        if (sleeper) {
            ai->mode = AI_WAKE;
            ai->target = sleeper;
            ai->timer = 0.0f;
        } else if (maybeApproach(self, objects, count, findPlayer(objects, count), dt)) {
            stand(self);
        }
    }
    /* Somebody on a ladder: hold it steady (helpers, the helpful) */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_HELP) && ai_holdLadder(self, objects, count, dt)) return;
    /* Done with a tool: careful people put it back where it belongs, the others leave it lying */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && ai_putAway(self, objects, count, dt)) return;
    /* Their tent blowing away: after it */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || ai->mode == AI_WORK || ai->mode == AI_FETCH) &&
        chaseTent(self, objects, count, dt)) return;
    /* The wind took something out of their hand: after it */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || ai->mode == AI_WORK || ai->mode == AI_FETCH) &&
        fetchBlown(self, objects, count, dt)) return;
    /* People and people: hand in hand, stepping in, bothering somebody */
    if (ai->mode != AI_FOLLOW && ai_touch(self, objects, count, dt)) return;
    /* The big tree's rope: out to their place on it, pulling when it goes */
    if (ai->mode != AI_FOLLOW && ai_onRope(self, objects, count, dt)) return;
    /* The festival: the storm, guests camping and leaving, dancing, the awareness team */
    if (ai->mode != AI_FOLLOW && ai->mode != AI_APPROACH && ai_festival(self, objects, count, dt)) return;
    /* Rain: inside */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || ai->mode == AI_WORK || ai->mode == AI_FETCH) &&
        shelter(self, objects, count, dt)) return;
    /* Wet through: by the fire to dry */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && dryOff(self, objects, count, dt)) return;
    /* A shopping run: over to the car */
    if (ai_errand(self, objects, count, dt)) return;
    /* Unloading the car */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && unloadCar(self, objects, count, dt)) return;
    /* Just arrived: hello at the fire */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && greetHosts(self, objects, count, dt)) return;
    /* Kurti and his sons: into the house for a while (not while you're being welcomed) */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && goInside(self, objects, count, dt)) return;
    /* Sleepy: to bed in their tent (busy working: later, once really done in) */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || (ai->mode == AI_WORK && self->needs.sleepiness > WORK_BEDTIME)) &&
        goToBed(self, objects, count, dt)) return;
    /* Tents: their own up, a hand with others', the bed things in */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && pitchTents(self, objects, count, dt)) return;
    /* Their floor's stage: building it, in working hours */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && ai_projectWork(self, objects, count, dt)) return;
    /* A finished cigarette in the fingers: to the tin, a bin, the fire */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && binButt(self, objects, count, dt)) return;
    /* Warm: the pullover over a railing or a chair (the tidy) */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && hangPullover(self, objects, count, dt)) return;
    /* Cold, the pullover put down somewhere: off to get it */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && fetchPullover(self, objects, count, dt)) return;
    /* Out of cigarettes: to the machine */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && buySmokes(self, objects, count, dt)) return;
    /* Tidy people pick things up on the way */
    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || (ai->mode == AI_HELP && !ai->pushing && !ai->copied) ||
         ai->mode == AI_SUPPLY || ai->mode == AI_FETCH) && ai_tidyUp(self, objects, count, dt))
        return;
    switch (ai->mode) {
    case AI_WAKE:     updateWake(self, objects, count, dt);     break;
    case AI_SUPPLY:   updateSupply(self, objects, count, dt);   break;
    case AI_PEE:      updatePee(self, objects, count, dt);      break;
    case AI_IDLE:     updateIdle(self, objects, count, dt);     break;
    case AI_WANDER:   updateWander(self, objects, count, dt);   break;
    case AI_HELP:     ai_updateHelp(self, objects, count, dt);     break;
    case AI_FETCH:    updateFetch(self, objects, count, dt);    break;
    case AI_DRINK:    updateDrink(self, objects, count, dt);    break;
    case AI_WORK:     ai_updateWork(self, objects, count, dt);     break;
    case AI_FOLLOW:   ai_updateFollow(self, objects, count, dt);   break;
    case AI_APPROACH: updateApproach(self, objects, count, dt); break;
    case AI_TABLE:    updateTable(self, objects, count, dt);    break;
    }
    watchdog(self);
}