Entropie git · main
Entropie festival game; first steps
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.gzentropie/src/people/ai.c 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);
}