/* * Helping the player: pushing and carrying along, watching to learn, * being shown a place, joining in with wood, tidying up on the way and * copying what the player does. */ #include "people/ai_internal.h" #include "world/car.h" #include "world/ladder.h" #include "people/climb.h" /* ---------- Helping ---------- */ /* A free spot right at the object, not on top of the leader. */ /* Standing at p, where the load wants to go (dir, flat and unit): in front of it, not beside it. */ static bool inLoadsWay(const Object *target, Vec3 dir, Vec3 p) { const Collider *c = &target->colliders[0]; const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z }; Vec3 across = vec3_cross(dir, UP); float length = 0.0f, width = 0.0f; for (int k = 0; k < 3; k++) { length += fabsf(vec3_dot(c->axes[k], dir)) * half[k]; width += fabsf(vec3_dot(c->axes[k], across)) * half[k]; } Vec3 rel = flat(vec3_sub(p, Object_colliderCenter(target, 0))); float along = vec3_dot(rel, dir), side = fabsf(vec3_dot(rel, across)); return along > 0.0f && along < length + 1.0f && side < width + 0.35f; /* (a body in front, not beside) */ } static bool goodSpot(Object *self, Object *const *objects, int count, const Object *target, Vec3 p) { const Object *leader = self->ai.leader; if (flatDistance(p, leader->position) < PERSON_RADIUS + 0.1f) return false; float gap = Hands_gapFrom(p, self->look.height, target); if (gap > SPOT_GAP || gap < 0.05f) return false; /* at it, not in it */ if (self->ai.clear_time > 0.0f && inLoadsWay(target, self->ai.clear_dir, p)) return false; /* (we were in the way there) */ return !ai_blockedBy(self, leader, target, objects, count, p); } /* * Where to stand to push or carry the leader's object: beside the leader if * there is room there, else anywhere along the object (long logs), as close * to us as possible. */ /* Spots along an object: both sides of its long axis and its ends. For long things the far end from the leader is best: the load is shared evenly. */ static bool alongSpot(Object *self, Object *const *objects, int count, const Object *target, Vec3 *spot, float *best) { const Object *leader = self->ai.leader; const Collider *c = &target->colliders[0]; const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z }; int along = 0; float longest = 0.0f; for (int k = 0; k < 3; k++) { float reach = vec3_length(flat(c->axes[k])) * half[k]; if (reach > longest) { longest = reach; along = k; } } Vec3 u = vec3_normalize(flat(c->axes[along])); Vec3 v = vec3_cross(u, UP); float width = 0.0f; for (int k = 0; k < 3; k++) width += fabsf(vec3_dot(c->axes[k], v)) * half[k]; Vec3 center = Object_colliderCenter(target, 0); float leader_at = vec3_dot(flat(vec3_sub(leader->position, center)), u); bool is_long = longest > 0.6f; bool found = false; for (int k = -3; k <= 3; k++) { for (int side = -1; side <= 1; side += 2) { float t = (float)k / 3.0f * longest * 0.85f; Vec3 p = vec3_add(center, vec3_add(vec3_scale(u, t), vec3_scale(v, (float)side * (width + 0.4f)))); if (k == -3 || k == 3) { /* the ends */ t = (k < 0 ? -1.0f : 1.0f) * (longest + 0.4f); p = vec3_add(center, vec3_scale(u, t)); } p.y = self->position.y; if (!goodSpot(self, objects, count, target, p)) continue; float cost = flatDistance(p, self->position) * (is_long ? 0.3f : 1.0f) + 1.5f; if (is_long) cost -= 1.5f * fabsf(t - leader_at); /* away from the leader: even lifting */ if (cost < *best) { *best = cost; *spot = p; found = true; } } } return found; } /* * Where to stand to push or carry the leader's object: along a long object * the far end from the leader; else beside the leader if there is room * there, else anywhere along the object, as close to us as possible. */ static bool findHelpSpot(Object *self, Object *const *objects, int count, const Object *target, Vec3 *spot) { const Object *leader = self->ai.leader; Vec3 heading = leader->hands.grab_heading; Vec3 right = vec3_normalize(vec3_cross(heading, UP)); static const float OFFSETS[] = { 0.75f, 1.1f, 1.5f }; float best = 1e9f; const Collider *c = &target->colliders[0]; float longest = fmaxf(c->half_extents.x, fmaxf(c->half_extents.y, c->half_extents.z)); if (longest > 0.6f && alongSpot(self, objects, count, target, spot, &best)) return true; for (unsigned i = 0; i < sizeof(OFFSETS) / sizeof(OFFSETS[0]); i++) { for (int s = -1; s <= 1; s += 2) { Vec3 p = vec3_add(leader->position, vec3_scale(right, OFFSETS[i] * (float)s)); if (!goodSpot(self, objects, count, target, p)) continue; /* Keep to the side we chose unless it is impossible */ float cost = flatDistance(p, self->position) + ((float)s != self->ai.side ? 2.0f : 0.0f); if (cost < best) { best = cost; *spot = p; } } } if (best < 1e9f) return true; return alongSpot(self, objects, count, target, spot, &best); } static bool tryGrab(Object *self, Object *target) { Vec3 saved = Object_facing(self); setFacing(self, vec3_sub(Object_colliderCenter(target, 0), self->position)); if (Hands_grabObject(self, target)) return true; setFacing(self, saved); return false; } /* A tool lying around that can do the job. */ Object *ai_findTool(const Object *self, Object *const *objects, int count, Job job) { Object *best = NULL; float best_dist = 60.0f; /* (the tool room may be a walk away) */ for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o->is_held || !Tools_canDo(toolOf(o), job) || !Ai_mayUse(self, toolOf(o))) continue; if (ai_skipped(&self->ai, o)) continue; /* couldn't get at it */ float d = flatDistance(o->position, self->position); if (d < best_dist) { best_dist = d; best = o; } } return best; } /* ---------- Being shown something ---------- */ void ai_updateFollow(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; Object *player = findPlayer(objects, count); if (!player) { Ai_stopShowing(self); return; } /* Watching the player work: one finished job and they've got it */ if (ai->showing == SHOW_HOW && player->work_state.jobs_done > ai->seen_jobs) { ai->knows_wood = true; Ai_say(self, "Ah, I get it! I'll take it from here."); const Object *wood = player->work_state.last_target; Ai_order(self, ai->pending != ORDER_NONE ? ai->pending : ORDER_FIREWOOD, wood ? Object_colliderCenter(wood, 0) : player->position); return; } Vec3 facing = Object_facing(player), right = vec3_normalize(vec3_cross(facing, UP)); float sway = sinf(self->body.clock * 0.4f + ai->side) * 0.3f; Vec3 spot = ai->showing == SHOW_HOW ? vec3_add(player->position, vec3_add(vec3_scale(right, ai->side * (2.0f + sway)), vec3_scale(facing, 0.6f))) : vec3_add(player->position, vec3_add(vec3_scale(facing, -1.6f), vec3_scale(right, ai->side * (0.5f + sway)))); float dist = flatDistance(spot, self->position); Vec3 eye = vec3_add(self->position, vec3(0.0f, self->look.height * 0.93f, 0.0f)); Vec3 hands = vec3_add(player->position, vec3(0.0f, player->look.height * (ai->showing == SHOW_HOW ? 0.55f : 0.93f), 0.0f)); self->body.look_dir = vec3_normalize(vec3_sub(hands, eye)); self->body.look_weight = 1.0f; /* (arrived: stays put until the spot is clearly somewhere else - no shuffling on the spot) */ if (dist < ARRIVE_DISTANCE * 2.0f) ai->settled = SETTLED_UNTIL; else if (dist > ai->settled) ai->settled = 0.0f; if (ai->settled <= 0.0f) { Vec3 dir = ai_steer(self, player, objects, count, spot); if (vec3_length(dir) < 0.5f) { if (dist < BLOCKED_SETTLE) ai->settled = dist + SETTLED_UNTIL; /* blocked close by: stay */ stand(self); return; } ai_walk(self, dir, dist > RUN_TO_LEADER ? runSpeed(self) * 0.8f : fminf(WALK_SPEED, dist * 2.0f + 0.3f), dt); return; } stand(self); turnTowards(self, vec3_sub(player->position, self->position), dt * 0.5f); } /* ---------- Helping with wood ---------- */ /* * The leader works on wood: the helper understands it's about firewood and * carries on with whatever needs doing around there (fells along, takes the * branches off, cuts and splits), fetching fitting tools. Returns false if * there is no wood work to help with. */ static bool helpWood(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; const Object *leader = ai->leader; const Work *lw = &leader->work_state; bool recent = lw->last_target && lw->since < 10.0f && isWoodJob(lw->last_job); if (recent) { ai->work_area = Object_colliderCenter(lw->last_target, 0); ai->order = ORDER_FIREWOOD; /* that's what all this wood work is for */ } else if (ai->order == ORDER_NONE) { return false; } if (flatDistance(leader->position, ai->work_area) > 15.0f) { /* the leader moved on */ ai->job_target = NULL; ai->order = ORDER_NONE; return false; } if (Tools_isWorking(self)) { stand(self); return true; } WoodTask t; Object *tree = lw->last_target; if (recent && lw->last_job == JOB_FELL && tree->type == OBJ_TREE && !tree->toppled) { /* Felling together: at the trunk, out of the leader's swing */ t = ai_taskFor(self, tree, JOB_FELL); Vec3 trunk = Object_colliderCenter(tree, 0); Vec3 from = flat(vec3_sub(leader->position, trunk)); from = vec3_length(from) > 1e-3f ? vec3_normalize(from) : vec3(1.0f, 0.0f, 0.0f); t.spot = vec3_add(trunk, vec3_scale(rotateY(from, ai->side * 1.4f), tree->colliders[0].half_extents.x + STAND_OFF)); t.spot.y = self->position.y; } else if (!ai_nextWoodTask(self, objects, count, ai->work_area, false, &t)) { ai->job_target = NULL; ai->order = ORDER_NONE; /* all done here */ Ai_say(self, woodLeft(objects, count, ai->work_area) ? "I'd need the axe to split the rest." : "That's done!"); return false; } ai_workOn(self, objects, count, &t, dt); return true; } /* ---------- Tidy people clean up on the way ---------- */ #define TIDY_LIKE 0.35f /* liking tidying up this much ... */ #define TIDY_REACH 1.3f /* ... picks up trash this close on the way */ #define TIDY_BIN 50.0f /* takes it to a container this far away when there's time */ #define ASHTRAY_FULL 12 /* butts: the tidy empty the ashtray */ static Object *trashInHands(const Object *self) { for (int h = 0; h < 2; h++) if (self->hands.state[h] == HAND_HOLDING && self->hands.item[h] && Object_isTrash(self->hands.item[h])) return self->hands.item[h]; return NULL; } /* Bottles put back into the wrong crate: the tidy sort them - one out, over to a crate of its own kind with room, in (the crates go back to the shop sorted). */ #define SORT_LOOK 25.0f /* m: crates this near are looked at ... */ #define SORT_FAR 60.0f /* ... and a bottle's own crate this near is walked to */ #define SORT_GIVE 45.0f /* s: can't get there: given up */ static bool sortCrates(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; Object *b = ai->sort_bottle; if (b) { /* over to its own crate with it */ bool held = self->hands.item[HAND_LEFT] == b || self->hands.item[HAND_RIGHT] == b; Object *to = held && !b->broken ? Crate_rightFor(objects, count, Crate_bottleKind(b), self->position, SORT_FAR) : NULL; if (!to || (ai->sort_time += dt) > SORT_GIVE) { if (held) for (int h = 0; h < 2; h++) if (self->hands.item[h] == b) Hands_drop(self, h); ai->sort_bottle = NULL; return false; } if (!goTo(self, to, objects, count, 0.8f, dt)) return true; for (int h = 0; h < 2; h++) if (self->hands.item[h] == b) Hands_drop(self, h); Hands_touch(self, vec3_add(Object_colliderCenter(to, 0), vec3(0.0f, 0.2f, 0.0f))); Crate_putBottle(to, b); ai->sort_bottle = NULL; ai->tidy_pause = 0.6f; return true; } if (ai->mode != AI_IDLE && ai->mode != AI_WANDER) { ai->sort_from = NULL; return false; } Object *from = ai->sort_from; if (from) { /* to the crate, the bottle out */ int hand = Hands_freeHand(self); if (hand < 0 || from->is_held || !Crate_nextWrong(from) || (ai->sort_time += dt) > SORT_GIVE) { if (ai->sort_time > SORT_GIVE) skipTarget(ai, from); ai->sort_from = NULL; return false; } if (!goTo(self, from, objects, count, 0.8f, dt)) return true; ai->sort_from = NULL; Hands_touch(self, vec3_add(Object_colliderCenter(from, 0), vec3(0.0f, 0.2f, 0.0f))); Object *w = Crate_takeWrong(g_world, from); if (w && Hands_receive(self, hand, w)) { /* (straight out of the crate into the hand) */ ai->sort_bottle = w; ai->sort_time = 0.0f; if (random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "That doesn't go in there." : "Who puts these in the wrong crate?"); } ai->tidy_pause = 0.5f; return true; } if (Hands_freeHand(self) < 0 || trashInHands(self) || (ai->sort_look -= dt) > 0.0f) return false; ai->sort_look = 3.0f; for (int i = 0; i < count; i++) { /* a crate with one that doesn't belong, its own crate with room somewhere */ Object *c = objects[i]; int kind = c->type == OBJ_CRATE && !c->is_held ? Crate_nextWrong(c) : BOTTLE_KIND_NONE; if (!kind || ai_skipped(ai, c) || flatDistance(c->position, self->position) > SORT_LOOK) continue; if (!Crate_rightFor(objects, count, kind, c->position, SORT_FAR)) continue; bool taken = false; for (int j = 0; j < count && !taken; j++) taken = objects[j] != self && objects[j]->is_character && objects[j]->ai.sort_from == c; if (taken) continue; ai->sort_from = c; ai->sort_time = 0.0f; return true; } return false; } /* Returns true if it took over this frame (bending down, or off to the container). */ bool ai_tidyUp(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; if (ai->character.likes[ORDER_TRASH] < TIDY_LIKE || self->sitting || Camp_bagInHand(self)) return false; if (ai->tidy_pause > 0.0f) { ai->tidy_pause -= dt; stand(self); return true; } /* trash right there, roughly ahead: take it along */ int hand = Hands_freeHand(self); if (hand >= 0 && self->hands.state[1 - hand] != HAND_REACHING) { Vec3 f = Object_facing(self); for (int i = 0; i < count; i++) { Object *o = objects[i]; if (!Object_isTrash(o) || o->is_held || o->weight > 2.0f) continue; Vec3 to = flat(vec3_sub(o->position, self->position)); float d = vec3_length(to); if (d > TIDY_REACH || (d > 0.5f && vec3_dot(vec3_scale(to, 1.0f / d), f) < 0.2f)) continue; if (claimed(self, objects, count, o)) continue; if (!Hands_pickUpObject(self, hand, o)) { skipTarget(ai, o); continue; } /* (just out of reach: not again and again) */ stand(self); turnTowards(self, to, dt); ai->tidy_pause = 0.7f; if (random01(ai) < 0.25f) Ai_say(self, random01(ai) < 0.5f ? "Tsk. People..." : "That doesn't belong here."); return true; } } /* a full ashtray, nothing else to do: tipped out (into a scrap of paper), off to the bin with it */ if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && hand >= 0 && !trashInHands(self) && !Body_isSmoking(&self->body)) { Object *tin = NULL; for (int i = 0; i < count && !tin; i++) { Object *o = objects[i]; if (o->type == OBJ_FURNITURE && o->variant == FURN_ASHTRAY && o->stored >= ASHTRAY_FULL && flatDistance(o->position, self->position) < TIDY_BIN && !claimed(self, objects, count, o)) tin = o; } if (tin) { if ((ai->tray_time += dt) > 45.0f) { skipTarget(ai, tin); ai->tray_time = 0.0f; return false; } /* (can't get there) */ if (goTo(self, tin, objects, count, 0.75f, dt)) { ai->tray_time = 0.0f; turnTowards(self, flat(vec3_sub(tin->position, self->position)), dt); ai->empty_tray = tin; ai->tidy_pause = 1.0f; Ai_say(self, random01(ai) < 0.5f ? "Full to the brim again." : "Who smokes all this?"); } return true; } } /* bottles in the wrong crate: sorted */ if (sortCrates(self, objects, count, dt)) return true; /* nothing better to do: an empty bottle back into a crate, other trash to the container */ Object *held = trashInHands(self); if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && held && Object_isBottle(held) && !held->broken) { Object *crate = Crate_forBottle(objects, count, held, self->position, TIDY_BIN); /* (its own crate if there's one) */ if (crate && !ai_skipped(ai, crate)) { if ((ai->tray_time += dt) > 45.0f) { skipTarget(ai, crate); ai->tray_time = 0.0f; return false; } /* (can't get there) */ if (goTo(self, crate, objects, count, 0.8f, dt)) { ai->tray_time = 0.0f; for (int h = 0; h < 2; h++) if (self->hands.item[h] == held) Hands_drop(self, h); Hands_touch(self, vec3_add(Object_colliderCenter(crate, 0), vec3(0.0f, 0.2f, 0.0f))); Crate_putBottle(crate, held); ai->tidy_pause = 0.6f; } return true; } } if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && trashInHands(self)) { Object *bin = ai_nearestOf(self, objects, count, OBJ_BIN, OBJ_BOX); if (!bin || flatDistance(bin->position, self->position) > TIDY_BIN) return false; if (goTo(self, bin, objects, count, 0.9f, dt)) Camp_throwAway(self); return true; } return false; } #define PUT_AWAY_CARE 0.5f /* this careful (see ai_stackCare): tools go back where they belong */ #define PUT_AWAY_FAR 80.0f /* ... unless that's further than this */ /* Holding a tool with nothing to do with it: careful people take it back to its place (the tool room), the others put it down where they are. */ bool ai_putAway(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; int hand = -1; for (int h = 0; h < 2; h++) { const Object *it = self->hands.item[h]; if (self->hands.state[h] == HAND_HOLDING && it && it->has_home && (it->type == OBJ_AXE || it->type == OBJ_SHEARS || it->type == OBJ_CHAINSAW || it->type == OBJ_SCYTHE || (it->type == OBJ_UMBRELLA && g_world && g_world->rain < 0.02f))) hand = h; /* (an umbrella once the rain's over) */ } if (hand < 0 || ai->talking || ai->on_project) return false; /* (at their side job: it's what they work with) */ Object *tool = self->hands.item[hand]; float d = flatDistance(tool->home_spot, self->position); if (ai_stackCare(self) < PUT_AWAY_CARE || d > PUT_AWAY_FAR) { /* down it goes, right here */ Hands_drop(self, hand); return false; } if (d > 0.7f) { Vec3 dir = ai_steer(self, tool, objects, count, tool->home_spot); if (vec3_length(dir) < 0.5f) { stand(self); if (isStuck(ai, d, dt) && ai->stuck > 6.0f) Hands_drop(self, hand); /* (can't get there: here then) */ } else { ai_walk(self, dir, WALK_SPEED * 0.8f, dt); } return true; } stand(self); Hands_drop(self, hand); tool->position = tool->home_spot; /* (back on its hook, as it was) */ tool->rotation = tool->home_rot; tool->velocity = tool->angular_velocity = vec3_zero(); Object_syncColliders(tool); if (random01(ai) < 0.3f) Ai_say(self, "Back where it belongs."); return true; } /* ---------- Doing what the leader does ---------- */ /* What sort of thing it is, for "something like that" (0: nothing to copy). */ static int sortOf(const Object *o) { if (!o || o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_HOSE || o->type == OBJ_LADDER || o->type == OBJ_BAG) return 0; /* tools and one-offs: wood help, or nothing like it around */ if (Object_isFuel(o)) return 1; if (Object_isTrash(o)) return 2; if (Object_isBottle(o)) return 3; if (Object_isFood(o)) return 4; return 100 + (int)o->type; } static Object *leaderItem(const Object *leader) { for (int h = 0; h < 2; h++) if (leader->hands.state[h] == HAND_HOLDING && sortOf(leader->hands.item[h])) return leader->hands.item[h]; return NULL; } /* Our item of that sort (and the hand it's in), or NULL. */ static Object *mineOfSort(const Object *self, int sort, int *hand) { for (int h = 0; h < 2; h++) { Object *o = self->hands.item[h]; if (self->hands.state[h] == HAND_HOLDING && o && sortOf(o) == sort) { if (hand) *hand = h; return o; } } return NULL; } #define COPY_SEARCH 25.0f /* looks this far around the leader for something like theirs */ static Object *nearestOfSort(const Object *self, Object *const *objects, int count, int sort, Vec3 around) { Object *best = NULL; float best_d = 1e9f; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o->is_held || o->is_static || o->lifted || !o->mesh.vao || o->broken || o->weight > 10.0f || sortOf(o) != sort) continue; if (flatDistance(o->position, around) > COPY_SEARCH || (ai_skipped(&self->ai, o))) continue; if (sort == 1 && inAnyFire(objects, count, o)) continue; float d = flatDistance(o->position, self->position); if (d < best_d && !claimed(self, objects, count, o)) { best_d = d; best = o; } } return best; } /* Walks to p; true once there. */ static bool walkTo(Object *self, const Object *ignore, Object *const *objects, int count, Vec3 p, float reach, float dt) { if (flatDistance(p, self->position) < reach) { stand(self); return true; } Vec3 dir = ai_steer(self, ignore, objects, count, p); if (vec3_length(dir) < 0.5f) stand(self); else ai_walk(self, dir, WALK_SPEED * 0.85f, dt); return false; } /* * Not clear what the leader is up to: take something like what they carry * and do what they do with it - carry it along at their side, put it down * next to where they put theirs, into the fire or the bin like they did. */ static bool helpImitate(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; Object *leader = ai->leader; Object *theirs = leaderItem(leader); int hand = -1; if (theirs) { if (ai->copied != theirs) { ai->copied = theirs; ai->copy_sort = sortOf(theirs); ai->copy_step = 0; } if (mineOfSort(self, ai->copy_sort, NULL)) return false; /* got one: along at the leader's side */ if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; } Object *like = nearestOfSort(self, objects, count, ai->copy_sort, leader->position); if (!like) return false; if (ai->job_target != like) { ai->job_target = like; ai->stuck = 0.0f; if (ai->speech_time <= 0.0f) Ai_say(self, "I'll grab one too."); } if (Hands_freeHand(self) < 0) freeHands(self, NULL); if (goTo(self, like, objects, count, PICKUP_REACH, dt)) { int h = Hands_freeHand(self); if (h >= 0) Hands_pickUpObject(self, h, like); ai->job_target = NULL; } else if ((ai->stuck += dt) > 4.0f) { skipTarget(ai, like); } return true; } /* The leader let go of theirs: the same with ours */ Object *it = ai->copied, *mine = it ? mineOfSort(self, ai->copy_sort, &hand) : NULL; if (!it || !mine) { ai->copied = NULL; return false; } if (ai->copy_step == 0) { if (it->is_held) { ai->copied = NULL; return false; } /* handed to someone */ Object *bin = ai_nearestOf(self, objects, count, OBJ_BIN, OBJ_BOX); if (it->mesh.vao && ai->copy_sort == 1 && inAnyFire(objects, count, it)) { ai->copy_step = 2; ai->target = Camp_fireNear(objects, count, it->position, 3.0f, false); } else if (!it->mesh.vao && bin && flatDistance(bin->position, leader->position) < 3.0f) { ai->copy_step = 3; /* thrown away */ ai->target = bin; } else if (!it->mesh.vao) { ai->copied = NULL; /* eaten or drunk: keep ours */ return false; } else { /* put down: next to theirs, on our side */ Vec3 right = vec3_normalize(vec3_cross(Object_facing(leader), UP)); ai->copy_step = 1; ai->copy_put = vec3_add(it->position, vec3_scale(right, ai->side * 0.45f)); if (blockedAt(self, leader, objects, count, ai->copy_put)) ai->copy_put = vec3_add(it->position, vec3_scale(right, -ai->side * 0.45f)); } ai->closest = 1e9f; ai->stuck = 0.0f; if (ai->copy_step >= 2 && !ai->target) { /* (no fire or bin after all) */ ai->copy_step = 1; ai->copy_put = it->position; } } bool done = false; /* can't get there: down here then */ bool stuck = ai->copy_step == 1 && isStuck(ai, flatDistance(ai->copy_put, self->position), dt) && ai->stuck > 4.0f; if (ai->copy_step == 1 && (stuck || walkTo(self, leader, objects, count, ai->copy_put, 1.0f, dt))) { stand(self); turnTowards(self, vec3_sub(ai->copy_put, self->position), dt); Hands_drop(self, hand); Object *pile = NULL; /* theirs went onto a woodpile: ours too */ for (int i = 0; i < count && it->stored && mine->type == OBJ_FIREWOOD && mine->size.z >= 4.0f; i++) if (objects[i]->type == OBJ_WOODPILE && flatDistance(objects[i]->position, it->position) < 2.0f) pile = objects[i]; if (pile) ai_pileSlid(self, Object_stackFirewood(pile, mine, objects, count, ai_stackCare(self))); done = true; } else if (ai->copy_step == 2) { Object *fire = ai->target; if (walkTo(self, fire, objects, count, fire->position, fire->size.x * 0.5f + 0.9f, dt)) { turnTowards(self, vec3_sub(fire->position, self->position), dt); if (Camp_feed(self, fire)) Ai_practice(self, ORDER_FIRE, 0.01f); done = true; } } else if (ai->copy_step == 3 && goTo(self, ai->target, objects, count, 0.9f, dt)) { Camp_throwAway(self); done = true; } if (done) { ai->copied = NULL; ai->copy_step = 0; ai->target = NULL; if (random01(ai) < 0.4f) Ai_say(self, "Like that?"); } return true; } /* Pushing or pulling together and the load won't budge, because we stand where it wants to go: the quick-witted see it, let go and come round to another side; the slow keep at it (and wonder). Returns true while letting go. */ #define JAM_SPEED 0.2f /* m/s: the leader wants to go at least this fast ... */ #define JAM_MOVING 0.35f /* ... and the load does less than this share of it */ #define JAM_NOTICE 0.8f /* s before the sharpest notice (the slow take longer) */ #define JAM_WITS 0.35f /* less (drunk takes off): they don't get it */ #define CLEAR_TIME 6.0f /* s keeping off that side */ static bool makeRoom(Object *self, const Object *leader, const Object *target, float dt) { Ai *ai = &self->ai; ai->clear_time = fmaxf(0.0f, ai->clear_time - dt); Vec3 want = flat(leader->movement); float speed = vec3_length(want); if (!target || speed < JAM_SPEED) { ai->jam_time = 0.0f; return false; } Vec3 dir = vec3_scale(want, 1.0f / speed); if (!inLoadsWay(target, dir, self->position)) { ai->jam_time = 0.0f; return false; } if (ai->jam_time <= 0.0f) ai->jam_from = target->position; ai->jam_time += dt; float wits = self->wits - 0.6f * fminf(self->drunk, 1.0f); if (ai->jam_time < JAM_NOTICE + 2.5f * (1.0f - clampf(wits, 0.0f, 1.0f))) return false; float moved = vec3_dot(flat(vec3_sub(target->position, ai->jam_from)), dir); float time = ai->jam_time; ai->jam_time = 0.0f; if (ai->pushing && moved > JAM_MOVING * speed * time) return false; /* (holding on and it goes: all fine; not holding on, we're just shoved along) */ if (wits < JAM_WITS) { /* doesn't get it: keeps at it */ if (random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "Why won't it move?" : "Push harder!"); return false; } Ai_say(self, moved > 0.3f ? "Whoa - I'm in the way!" : random01(ai) < 0.5f ? "Wait - I'm in the way." : "Hang on, I'll come round."); if (Hands_isGrabbing(self)) Hands_release(self); ai->pushing = false; ai->retry = 0.0f; ai->closest = 1e9f; ai->clear_dir = dir; ai->clear_time = CLEAR_TIME; stand(self); return true; } void ai_updateHelp(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; Object *leader = ai->leader; Object *target = leader->hands.grabbed; /* Worn out: let go and catch breath before going on */ if (ai->resting || self->needs.stamina < 0.12f) { ai->resting = self->needs.stamina < 0.45f; if (ai->resting) { if (Hands_isGrabbing(self)) Hands_release(self); ai->pushing = false; stand(self); return; } } /* The leader went up a ladder (or came down): after them */ if (!target && !ai->pushing && ai_followByLadder(self, objects, count, leader, dt)) return; /* Wood work: understand what the leader is up to and carry on with it */ if (!target && !ai->pushing && helpWood(self, objects, count, dt)) return; /* Not sure what it's about: do the same as the leader */ if (!target && !ai->pushing && helpImitate(self, objects, count, dt)) return; if (!target && !Tools_isWorking(self)) { ToolKind held = Tools_held(self, NULL); if (held == TOOL_AXE || held == TOOL_CHAINSAW || held == TOOL_SHEARS) freeHands(self, NULL); /* done: tools down */ } /* Leader let go (or we lost our grip): walk along again; a lost grip means a moment's pause before trying again */ if (ai->pushing && (!target || self->hands.grabbed != target)) { if (Hands_isGrabbing(self)) Hands_release(self); if (target) ai->retry = 1.0f + random01(ai); ai->pushing = false; ai->closest = 1e9f; } if (makeRoom(self, leader, target, dt)) return; if (ai->retry > 0.0f) { ai->retry -= dt; stand(self); if (target) turnTowards(self, vec3_sub(Object_colliderCenter(target, 0), self->position), dt * 0.5f); return; } if (ai->pushing) { /* Same push / pull as the leader */ self->movement = leader->movement; self->move_local = leader->move_local; setFacing(self, self->hands.grab_heading); return; } Vec3 heading = target ? leader->hands.grab_heading : Object_facing(leader); Vec3 right = vec3_normalize(vec3_cross(heading, UP)); Vec3 spot; if (!target || !findHelpSpot(self, objects, count, target, &spot)) { /* Walking along: beside and a bit behind, never in a straight line */ float t = self->body.clock, phase = (float)(ai->rng & 255u) * 0.02f; float beside = FOLLOW_SPACING * (1.0f + 0.25f * sinf(t * 0.31f + phase)); float behind = 0.5f + 0.35f * sinf(t * 0.23f + 2.0f * phase); if (ai->copied && mineOfSort(self, ai->copy_sort, NULL)) { /* carrying the same: right at their side, in step */ beside = FOLLOW_SPACING * 0.75f; behind = 0.1f; } spot = vec3_add(leader->position, vec3_sub(vec3_scale(right, ai->side * beside), vec3_scale(heading, behind))); } float dist = flatDistance(spot, self->position); if (dist > ARRIVE_DISTANCE && isStuck(ai, dist, dt)) { ai->side = -ai->side; /* try the leader's other side */ ai->closest = 1e9f; ai->stuck = 0.0f; } if (target && dist < GRAB_TRY_RADIUS && tryGrab(self, target)) { ai->pushing = true; stand(self); return; } if (target) ai->settled = 0.0f; else if (dist < ARRIVE_DISTANCE) ai->settled = SETTLED_UNTIL; else if (dist > ai->settled) ai->settled = 0.0f; if (dist > ARRIVE_DISTANCE && ai->settled <= 0.0f) { /* Close to the object: walk right up to it */ const Object *near = target && dist < 2.0f ? target : NULL; Vec3 dir = ai_steerPast(self, leader, near, objects, count, spot); if (vec3_length(dir) < 0.5f) { /* Blocked (someone in the way) close by: good enough, stay put instead of trying again every frame */ if (dist < BLOCKED_SETTLE && !target) ai->settled = dist + SETTLED_UNTIL; stand(self); return; } float speed = dist > RUN_TO_LEADER ? runSpeed(self) : fminf(WALK_SPEED, dist * 2.5f + 0.2f); ai_walk(self, dir, speed, dt); if (dist < 1.0f) turnTowards(self, heading, dt); return; } /* At the spot: step up to the object if there is one; else stand easy, now and then looking over at the leader */ stand(self); if (target) { self->movement = vec3_scale(heading, APPROACH_SPEED); turnTowards(self, heading, dt); return; } turnTowards(self, heading, dt * 0.4f); if (sinf(self->body.clock * 0.5f + (float)(ai->rng & 63u)) > 0.3f) { Vec3 eye = vec3_add(self->position, vec3(0.0f, self->look.height * 0.93f, 0.0f)); self->body.look_dir = vec3_normalize(vec3_sub(vec3_add(leader->position, vec3(0.0f, leader->look.height * 0.93f, 0.0f)), eye)); self->body.look_weight = 0.8f; } } /* ---------- Holding a ladder ---------- */ #define HOLD_SEES 12.0f /* m: the helpful notice somebody on a wobbly ladder this far off */ #define HOLD_LINGER 3.0f /* s: nobody on it any more - they let go */ static Object *climberOn(Object *const *objects, int count, const Object *ladder) { for (int i = 0; i < count; i++) if (objects[i]->on_ladder == ladder) return objects[i]; return NULL; } bool ai_holdLadder(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; Object *l = self->holding_ladder; if (!l) { if (self->drunk > 0.6f || Hands_isGrabbing(self) || ai->talking) return false; bool helping = ai->mode == AI_HELP && ai->leader; bool free_to = ai->mode == AI_IDLE || ai->mode == AI_WANDER; if (!helping && !free_to) return false; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o == self || !o->on_ladder || o->on_ladder->held_by || !o->on_ladder->leaning) continue; Object *ladder = o->on_ladder; bool mine = helping && ai->leader == o; /* (the one we help is going up) */ bool sees = free_to && ai->character.willing > 0.6f && Ladder_steadiness(ladder) < 0.85f && flatDistance(ladder->position, self->position) < HOLD_SEES; if (!mine && !sees) continue; freeHands(self, NULL); if (self->hands.item[HAND_LEFT] || self->hands.item[HAND_RIGHT]) return false; /* (can't put it down) */ Ladder_hold(self, ladder); ai->hold_idle = 0.0f; Ai_say(self, mine ? "I'll hold the ladder." : "Wait - I'll hold the ladder for you!"); l = ladder; break; } if (!l) return false; } /* Nobody on it any more (or it's gone): let go */ Object *climber = climberOn(objects, count, l); ai->hold_idle = climber ? 0.0f : ai->hold_idle + dt; if (!l->leaning || l->held_by != self || ai->hold_idle > HOLD_LINGER || Body_isDown(&self->body)) { if (l->held_by == self && l->leaning && ai->hold_idle > HOLD_LINGER) Ai_say(self, "There you go."); Ladder_hold(self, NULL); return false; } Vec3 facing, spot = Ladder_holdSpot(l, &facing); float d = flatDistance(spot, self->position); if (d > (ai->hold_there ? 0.2f : 0.1f)) { /* (right up to it; once there, a little shove doesn't send them walking) */ ai->hold_there = false; Vec3 to = vec3_sub(spot, self->position); to.y = 0.0f; Vec3 dir = d < 0.7f ? vec3_scale(to, 1.0f / d) : ai_steer(self, NULL, objects, count, spot); /* (the last bit straight there) */ if (vec3_length(dir) < 0.5f) stand(self); else ai_walk(self, dir, fminf(WALK_SPEED, d * 2.5f + 0.3f), dt); return true; } ai->hold_there = true; stand(self); turnTowards(self, facing, dt * 2.0f); return true; } /* ---------- Up and down ladders ---------- */ #define LADDER_UP_FROM 1.2f /* the leader this much higher (or lower): by the ladder */ #define LADDER_SEEK 25.0f /* m: a ladder this near is worth walking to */ bool ai_climbing(Object *self, float dt) { Ai *ai = &self->ai; if (self->climbing && !self->on_ladder) { /* over the edge at the top */ if (g_world) Climb_update(g_world, self, dt); return true; } Object *l = self->on_ladder; if (!l) return false; if (!l->leaning && l->placing <= 0.0f) { Ladder_getOff(self); return false; } /* (it fell: see Ladder_topple) */ if (ai->ladder_dir == 0.0f) ai->ladder_dir = -1.0f; float steady = Ladder_steadiness(l); /* (an unsteady ladder may slip under them too: see the player's ladderSlips) */ if (steady < 0.98f && self->ladder_off <= 0.0f && g_world) { float risk = 0.9f * powf(1.0f - steady, 1.5f) * (0.3f + clampf(self->ladder_at / 3.0f, 0.0f, 1.0f)) * (1.0f + fminf(self->drunk, 1.0f)); if (random01(ai) < risk * dt) { if (l->ladder_wobble <= 0.0f) { l->ladder_wobble = 30.0f; l->shake = 1.2f; Ai_say(self, "Whoa! It's wobbling!"); } else { Vec3 across = vec3_cross(l->lean_normal, vec3(0.0f, 1.0f, 0.0f)); Vec3 fall = vec3_scale(across, random01(ai) < 0.5f ? 1.0f : -1.0f); float high = self->ladder_at; Ladder_topple(g_world, l, fall); self->velocity = vec3_add(vec3_scale(fall, 1.2f), vec3(0.0f, 0.5f, 0.0f)); if (high > 0.6f) Body_fall(&self->body, fall); Ai_say(self, "Aaah!"); return false; } } l->ladder_wobble = fmaxf(0.0f, l->ladder_wobble - dt); } int r = Ladder_climb(self, ai->ladder_dir, dt); if (r == CLIMB_AT_EDGE && (!g_world || !Climb_overLadderTop(g_world, self, l))) ai->ladder_dir = -1.0f; /* (nothing up there: down again) */ if (ai->ladder_dir > 0.0f && l->lean_top <= 0.0f && r != CLIMB_AT_EDGE && self->ladder_at > 0.5f) { /* (no getting off at the top: down again) */ ai->ladder_climb_t += dt; if (ai->ladder_climb_t > 8.0f) ai->ladder_dir = -1.0f; } return self->on_ladder != NULL || self->climbing; } /* A leaning ladder from about here (feet) up to about there (top), nobody on it. */ static Object *ladderBetween(const Object *self, Object *const *objects, int count, Vec3 low, Vec3 high, bool from_top) { Object *best = NULL; float best_d = LADDER_SEEK; for (int i = 0; i < count; i++) { Object *l = objects[i]; if (l->type != OBJ_LADDER || !l->leaning || l->lean_top <= 0.0f || l->tip_v > 0.0f || climberOn(objects, count, l)) continue; Vec3 edge = Ladder_edge(l); if (fabsf(l->position.y - low.y) > 0.6f || fabsf(edge.y - high.y) > 1.3f) continue; Vec3 near = from_top ? edge : l->position; float d = flatDistance(near, self->position); if (d < best_d) { best_d = d; best = l; } } return best; } bool ai_followByLadder(Object *self, Object *const *objects, int count, const Object *leader, float dt) { Ai *ai = &self->ai; if (!g_world || !leader || leader->on_ladder || leader->climbing || !leader->is_grounded || self->holding_ladder) return false; float up = leader->position.y - self->position.y; if (fabsf(up) < LADDER_UP_FROM) return false; if (ai->ladder_seek > 0.0f) { ai->ladder_seek -= dt; return false; } bool going_up = up > 0.0f; if (going_up && self->height_fear * (1.0f - 0.5f * fminf(self->drunk, 1.0f)) > 0.6f) { /* afraid of heights: not up there */ if (!ai->heights_said) { ai->heights_said = true; Ai_say(self, "No way - I'm not going up there. I'll wait down here."); } ai->ladder_seek = 20.0f; return false; } if (!going_up) ai->heights_said = false; Object *l = going_up ? ladderBetween(self, objects, count, self->position, leader->position, false) : ladderBetween(self, objects, count, leader->position, self->position, true); if (!l && going_up && up < CLIMB_REACH * self->look.height / 1.75f + 0.2f) { /* no ladder, but in reach: pull up */ Vec3 probe; float top = Climb_ledgeAhead(g_world, self, &probe); if (top > -1e8f && fabsf(top - leader->position.y) < 0.6f && self->needs.stamina > 0.2f && self->drunk < 0.6f) { freeHands(self, NULL); int how = Climb_start(g_world, self, top, probe); if (how == CLIMB_HARD || how == CLIMB_EASY_UP) { if (how == CLIMB_HARD && self->ai.speech_time <= 0.0f) Ai_say(self, "Hup!"); return true; } } return false; /* (walks on towards them: up against it, the ledge is ahead) */ } if (!l) { ai->ladder_seek = 3.0f; return false; } if (going_up) { /* to its foot, then up */ Vec3 foot = vec3_add(l->position, vec3_scale(l->lean_normal, 0.5f)); float d = flatDistance(foot, self->position); if (d > 0.45f) { Vec3 dir = ai_steer(self, l, objects, count, foot); if (vec3_length(dir) < 0.5f) stand(self); else ai_walk(self, dir, fminf(WALK_SPEED, d * 2.0f + 0.3f), dt); return true; } freeHands(self, NULL); Ladder_mount(self, l, false); } else { /* to its top, then down */ Vec3 edge = Ladder_edge(l); Vec3 spot = vec3_sub(edge, vec3_scale(l->lean_normal, 0.5f)); float d = flatDistance(spot, self->position); if (d > 0.5f) { /* (up there: straight to it) */ Vec3 dir = vec3(spot.x - self->position.x, 0.0f, spot.z - self->position.z); ai_walk(self, vec3_scale(dir, 1.0f / fmaxf(d, 1e-3f)), fminf(WALK_SPEED * 0.6f, d * 2.0f + 0.2f), dt); return true; } freeHands(self, NULL); Ladder_mount(self, l, true); } ai->ladder_dir = going_up ? 1.0f : -1.0f; stand(self); return true; }