/* * 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); }