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