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_work.c raw
/*
* Work: firewood, trees, trash and the fire, on an order or on their own,
* and whether someone feels like it at all.
*/
#include "people/ai_internal.h"
#include "people/pockets.h"
#include "world/world.h"
/* ---------- Wood work ---------- */
#define LIGHTER_ASK 30.0f /* m: they go and borrow a lighter from somebody this near */
#define UNEVEN_CUT 0.2f /* m a piece may come out longer or shorter, cut by someone unpractised */
/* ---------- Felling room ---------- */
/* A tree falls away from whoever fells it. Before that: which way is there
room for it (no house, wall, car, tent, table, fire in the way, from the
stump out to its top and a crown wide)? The feller stands on the other side.
Somebody in the way when it's nearly through: "Timber!" and they get out. */
#define FELL_DIRS 12 /* ways round it looked at */
#define FELL_STEP 1.0f /* m between the spots looked at along the way it falls */
#define FELL_WARN 0.6f /* the cut this far through: anybody in the way is warned off */
#define FELL_CLEAR 1.2f /* m past the crown's side: out of the way */
static float treeTall(const Object *tree) {
int top = tree->collider_count > 1 ? 1 : 0;
return Object_colliderCenter(tree, top).y + tree->colliders[top].half_extents.y - tree->position.y;
}
static float crownR(const Object *tree) { return fmaxf(tree->size.x * 0.5f, 0.6f); }
static bool notInTheWay(const Object *o, const void *tree) {
if (o == tree || o->is_character || o->type == OBJ_DOG || !o->is_static) return true;
switch (o->type) { /* (what a tree mustn't come down on) */
case OBJ_HOUSE: case OBJ_BARN: case OBJ_STRUCTURE: case OBJ_WALL: case OBJ_CAR: case OBJ_TENT:
case OBJ_FURNITURE: case OBJ_FIREPLACE: case OBJ_WOODPILE: return false;
default: return true;
}
}
/* What it would come down on, falling that way (counted; the first in *first). */
static int fallHits(const Object *tree, Vec3 dir, Object **first) {
if (first) *first = NULL;
if (!g_world) return 0;
Vec3 side = vec3(-dir.z, 0.0f, dir.x);
float tall = treeTall(tree), r = crownR(tree);
int hits = 0;
for (float along = 1.0f; along <= tall + 0.5f; along += FELL_STEP)
for (int s = -1; s <= 1; s++) {
float w = (along > tall * 0.45f ? r : 0.4f) * (float)s * 0.8f; /* (the trunk narrow, the crown wide) */
Vec3 p = vec3_add(tree->position, vec3_add(vec3_scale(dir, along), vec3_scale(side, w)));
p.y = tree->position.y + 14.0f;
Object *hit = NULL;
if (World_rayCast(g_world, p, vec3(0.0f, -1.0f, 0.0f), 14.5f, notInTheWay, tree, &hit, NULL) >= 0.0f) {
hits++;
if (first && !*first) *first = hit;
}
}
return hits;
}
/* Which way it should fall: the clear way nearest to coming down away from
`from` (where the feller is), worked out once. */
static Vec3 fellPlan(Object *tree, Vec3 from) {
if (vec3_length(tree->fell_plan) > 0.5f) return tree->fell_plan;
Vec3 want = vec3_scale(from, -1.0f), best = want;
int best_hits = 1 << 20;
for (int k = 0; k < FELL_DIRS; k++) { /* (nearest first: 0, +30, -30, +60 ...) */
float a = (float)((k + 1) / 2) * (2.0f * PI_F / (float)FELL_DIRS) * (k % 2 ? 1.0f : -1.0f);
Vec3 d = vec3(want.x * cosf(a) - want.z * sinf(a), 0.0f, want.x * sinf(a) + want.z * cosf(a));
int hits = fallHits(tree, d, NULL);
if (hits < best_hits) { best_hits = hits; best = d; }
if (hits == 0) break;
}
tree->fell_plan = best;
tree->fell_blocked = best_hits > 0;
return best;
}
const Object *Ai_fallsOnto(const Object *tree, Vec3 dir) {
Object *first = NULL;
dir = vec3_normalize(vec3(dir.x, 0.0f, dir.z));
fallHits(tree, dir, &first);
return first;
}
/* Anybody standing where it'll come down: warned off (they step out sideways). */
bool Ai_warnFellingRoom(Object *self, Object *const *objects, int count, Object *tree, Vec3 dir) {
dir = vec3_normalize(vec3(dir.x, 0.0f, dir.z));
Vec3 side = vec3(-dir.z, 0.0f, dir.x);
float tall = treeTall(tree) + 1.5f, r = crownR(tree) + 0.6f;
bool told = false, anybody = false;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o == self || !o->is_character || o->type == OBJ_DOG) continue;
Vec3 rel = flat(vec3_sub(o->position, tree->position));
float along = vec3_dot(rel, dir), across = vec3_dot(rel, side);
if (along < -0.5f || along > tall || fabsf(across) > r) continue;
anybody = true;
if (!told && self->type != OBJ_PLAYER && self->ai.speech_time <= 0.0f) {
Ai_say(self, o->type == OBJ_PLAYER ? "Timber! Get out of the way - it's coming down there!" : "Timber! Out of the way!");
told = true;
}
if (o->type == OBJ_HUMAN && o->ai.dodge_t <= 0.0f) { /* (out sideways, past the crown) */
float out = across < 0.0f ? -1.0f : 1.0f;
o->ai.dodge_to = vec3_add(o->position, vec3_scale(side, out * (r - fabsf(across) + FELL_CLEAR)));
o->ai.dodge_t = 8.0f;
}
}
return anybody;
}
/* Where to stand for a job on o, and what to face. */
WoodTask ai_taskFor(const Object *self, Object *o, Job job) {
WoodTask t = { .job = job, .target = o };
if (job == JOB_FELL) {
Vec3 c = Object_colliderCenter(o, 0);
Vec3 from = flat(vec3_sub(self->position, c));
from = vec3_length(from) > 1e-3f ? vec3_normalize(from) : vec3(1.0f, 0.0f, 0.0f);
from = vec3_scale(fellPlan(o, from), -1.0f); /* (on the far side from where it'll fall) */
t.spot = vec3_add(c, vec3_scale(from, o->colliders[0].half_extents.x + STAND_OFF));
t.aim = c;
} else if (job == JOB_DELIMB) {
int crown = o->collider_count > 1 ? 1 : 0;
Vec3 near = closestOn(o, crown, self->position);
Vec3 out = flat(vec3_sub(self->position, near));
out = vec3_length(out) > 1e-3f ? vec3_normalize(out) : vec3(1.0f, 0.0f, 0.0f);
t.spot = vec3_add(near, vec3_scale(out, 0.6f));
t.aim = near;
} else {
/* Beside the log or piece, facing the spot to cut or split */
Vec3 a, b;
Tools_logEnds(o, &a, &b);
Vec3 axis = vec3_sub(b, a);
float length = vec3_length(axis);
axis = vec3_scale(axis, 1.0f / fmaxf(length, 1e-4f));
Vec3 u = flat(axis);
u = vec3_length(u) > 1e-3f ? vec3_normalize(u) : vec3(1.0f, 0.0f, 0.0f);
Vec3 v = vec3_normalize(vec3_cross(u, UP));
float along = job == JOB_CUT ? Tools_cutPoint(o, self->position) : length * 0.5f;
if (job == JOB_CUT) { /* the practised cut even pieces; the others by eye, some longer, some shorter */
unsigned h = (unsigned)(int)(o->position.x * 731.0f) * 2654435761u ^ (unsigned)(int)(o->position.z * 557.0f) * 40503u;
float r = (float)((h >> 8) & 1023u) / 1023.0f * 2.0f - 1.0f;
along += r * UNEVEN_CUT * (1.0f - self->skill[ORDER_FIREWOOD]);
along = clampf(along, 0.3f, length - 0.3f);
}
Vec3 point = vec3_add(a, vec3_scale(axis, along));
float side = vec3_dot(vec3_sub(self->position, point), v) < 0.0f ? -1.0f : 1.0f;
float reach = o->colliders[0].half_extents.x;
t.spot = vec3_add(point, vec3_scale(v, side * (reach + STAND_OFF)));
t.aim = point;
}
t.spot.y = self->position.y;
return t;
}
/* Holds a fitting tool already, or one lies around. */
static bool canGetTool(const Object *self, Object *const *objects, int count, Job job) {
return Tools_canDo(Tools_held(self, NULL), job) || ai_findTool(self, objects, count, job) != NULL;
}
/*
* The next thing to do with the wood around area, in the order that makes
* firewood: split what is short, cut what is long, take branches off fallen
* trees, and (if asked to) fell more trees.
*/
bool ai_nextWoodTask(Object *self, Object *const *objects, int count, Vec3 area, bool trees, WoodTask *out) {
Ai *ai = &self->ai;
Object *best = NULL;
Job best_job = JOB_NONE;
int best_rank = 99;
float best_dist = 1e9f;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (ai_skipped(ai, o) || o->is_held || o->broken || o->collider_count == 0) continue;
float from_area = flatDistance(Object_colliderCenter(o, 0), area);
Job job = JOB_NONE;
int rank = 0;
if (Tools_splittable(o) && from_area < WOOD_RADIUS) { job = JOB_SPLIT; rank = 0; }
else if (Tools_cuttable(o) && from_area < WOOD_RADIUS) { job = JOB_CUT; rank = 1; }
else if (o->type == OBJ_TREE && o->toppled && o->topple >= 1.0f && o->branches > 0 && from_area < WOOD_RADIUS) { job = JOB_DELIMB; rank = 2; }
else if (trees && o->type == OBJ_TREE && !o->toppled && !o->variant && !o->keep && from_area < TREE_RADIUS) { job = JOB_FELL; rank = 3; } /* never the apple trees (or the ones that stay) */
if (job == JOB_NONE) continue;
if ((job == JOB_SPLIT || job == JOB_CUT) && claimed(self, objects, count, o)) continue;
if (!canGetTool(self, objects, count, job)) continue; /* (the axe may be in someone else's hands) */
float d = flatDistance(Object_colliderCenter(o, 0), self->position);
if (rank < best_rank || (rank == best_rank && d < best_dist)) {
best = o;
best_job = job;
best_rank = rank;
best_dist = d;
}
}
if (!best) return false;
*out = ai_taskFor(self, best, best_job);
return true;
}
/* ---------- Tools they shouldn't use ---------- */
#define SAW_PRACTICE 0.4f /* tree skill below this: the chainsaw is too much for them */
#define AXE_DRUNK 0.6f /* this drunk: hands off the axe and the saw */
#define CLOSE_CALL 8.0f /* about this many seconds of work between close calls with one */
bool Ai_shouldntUse(const Object *self, int tool) {
if (tool == TOOL_CHAINSAW && self->skill[ORDER_TREES] < SAW_PRACTICE) return true;
if ((tool == TOOL_AXE || tool == TOOL_CHAINSAW) && self->drunk > AXE_DRUNK) return true;
return false;
}
bool Ai_mayUse(const Object *self, int tool) {
unsigned bit = 1u << tool;
return !(self->ai.banned & bit) || (self->ai.defies & bit);
}
/* How likely they ignore being told: proud, unwilling, drunk. */
static float ignorance(const Object *self) {
return clampf(0.1f + 0.45f * self->temperament.pride + 0.5f * (0.5f - self->ai.character.willing)
+ 0.4f * fminf(self->drunk, 1.0f), 0.0f, 0.9f);
}
bool Ai_banTool(Object *self, int tool, bool ban) {
unsigned bit = 1u << tool;
Ai *ai = &self->ai;
if (!ban) {
ai->banned &= (unsigned char)~bit;
ai->defies &= (unsigned char)~bit;
return true;
}
ai->banned |= (unsigned char)bit;
if (random01(&self->ai) < ignorance(self)) { /* doesn't listen */
ai->defies |= (unsigned char)bit;
return false;
}
ai->defies &= (unsigned char)~bit;
for (int h = 0; h < 2; h++) /* put it down right here */
if (self->hands.state[h] == HAND_HOLDING && toolOf(self->hands.item[h]) == (ToolKind)tool) Hands_drop(self, h);
if (ai->tool && toolOf(ai->tool) == (ToolKind)tool) ai->tool = NULL;
return true;
}
/* Working with a tool they shouldn't use: a word about it first, and now
and then a close call (it kicks back, the axe glances off): a stagger. */
static void riskyTool(Object *self, ToolKind held, float dt) {
Ai *ai = &self->ai;
if (!Ai_shouldntUse(self, held)) return;
unsigned bit = 1u << held;
if (!(ai->warned & bit)) {
ai->warned |= (unsigned char)bit;
ai->risky = CLOSE_CALL * (0.5f + random01(&self->ai));
Ai_say(self, self->drunk > AXE_DRUNK ? "I'm fine, I'm fine! Watch this..."
: held == TOOL_CHAINSAW ? "Never used one of these... how hard can it be?" : "Here goes nothing.");
return;
}
if (!Tools_isWorking(self) || (ai->risky -= dt) > 0.0f) return;
ai->risky = CLOSE_CALL * (0.5f + random01(&self->ai));
Body_stagger(&self->body, vec3_scale(Object_facing(self), -0.8f));
Ai_say(self, held == TOOL_CHAINSAW ? "Whoa! It kicked back at me!" : "Whoa, that one glanced off!");
}
/* Holds a tool that can do the job, fetching one if needed.
0 = ready, 1 = busy getting it, -1 = there is none. */
static int getTool(Object *self, Object *const *objects, int count, Job job, float dt) {
Ai *ai = &self->ai;
int hand;
ToolKind held = Tools_held(self, &hand);
if (held >= TOOL_AXE && !Ai_mayUse(self, held) && hand >= 0) { /* told to leave that one: down it goes */
Hands_drop(self, hand);
return 1;
}
bool saw_ok = held != TOOL_CHAINSAW || (hand >= 0 && self->hands.state[1 - hand] == HAND_FREE);
if (Tools_canDo(held, job) && saw_ok) return 0;
if (held == TOOL_CHAINSAW && Tools_canDo(held, job)) { /* both hands for the saw */
freeHands(self, self->hands.item[hand]);
return 1;
}
Object *tool = ai->tool && !ai->tool->is_held && Tools_canDo(toolOf(ai->tool), job)
? ai->tool : ai_findTool(self, objects, count, job);
ai->tool = tool;
if (!tool) return -1;
if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) {
stand(self);
return 1;
}
if (Hands_gapFrom(self->position, self->look.height, tool) < PICKUP_REACH) {
stand(self);
freeHands(self, NULL);
turnTowards(self, vec3_sub(tool->position, self->position), dt);
Hands_pickUpObject(self, self->main_hand, tool);
return 1;
}
Vec3 dir = ai_steer(self, tool, objects, count, tool->position);
if (vec3_length(dir) < 0.5f) { stand(self); return 1; }
ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
return 1;
}
/* Walks to the spot, faces the wood and works it. Gives up on it if it doesn't work out. */
void ai_workOn(Object *self, Object *const *objects, int count, const WoodTask *t, float dt) {
Ai *ai = &self->ai;
if (ai->job_target != t->target) {
ai->job_target = t->target;
ai->job_time = 0.0f;
ai->closest = 1e9f;
ai->stuck = 0.0f;
ai->timer = 0.0f;
}
ai->job_time += dt;
if (ai->job_time > JOB_PATIENCE) { skipTarget(ai, t->target); return; }
int tool = getTool(self, objects, count, t->job, dt);
if (tool < 0) { skipTarget(ai, t->target); stand(self); return; }
if (tool > 0) return;
riskyTool(self, Tools_held(self, NULL), dt);
if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return; }
float dist = flatDistance(t->spot, self->position);
if (dist > WORK_ARRIVE + (ai->timer > 0.5f ? 0.45f : 0.0f)) { /* (edging closer: may leave the spot) */
Vec3 dir = ai_steerPast(self, t->target, NULL, objects, count, t->spot);
if (vec3_length(dir) < 0.5f || isStuck(ai, dist, dt * 0.5f)) {
stand(self);
if (ai->stuck > STUCK_TIME * 4.0f) skipTarget(ai, t->target);
return;
}
ai_walk(self, dir, fminf(WALK_SPEED, dist * 2.0f + 0.3f), dt);
return;
}
stand(self);
Vec3 to = flat(vec3_sub(t->aim, self->position));
turnTowards(self, to, dt * 2.0f);
Vec3 eye = vec3_add(self->position, vec3(0.0f, self->look.height * 0.93f, 0.0f));
self->body.look_dir = vec3_normalize(vec3_sub(t->aim, eye));
self->body.look_weight = 1.0f;
if (vec3_length(to) > 1e-3f && vec3_dot(Object_facing(self), vec3_normalize(to)) < 0.93f) return;
if (Tools_isWorking(self)) return;
if (!Tools_hasJob(self, objects, count)) { /* can't get at it from here: edge closer, then give up */
ai->timer += dt;
if (ai->timer > 3.0f) skipTarget(ai, t->target);
else if (ai->timer > 0.5f) ai_walk(self, vec3_normalize(to), 0.35f, dt);
return;
}
ai->timer = 0.0f;
Tools_use(self, objects, count);
}
/* A quarter of firewood lying around the work area, not yet on the pile. */
static Object *loosePiece(const Object *self, Object *const *objects, int count, Vec3 area) {
Object *best = NULL;
float best_dist = 1e9f;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o->type != OBJ_FIREWOOD || o->size.z < 4.0f || o->is_held || o->broken) continue;
bool to_move = false; /* on an old woodpile: goes over to the new one */
if (o->stored && self->ai.woodpile) {
for (int k = 0; k < count && !to_move; k++)
to_move = objects[k]->type == OBJ_WOODPILE && objects[k]->moved_to == self->ai.woodpile &&
flatDistance(objects[k]->position, o->position) < 1.5f;
}
if (!to_move && flatDistance(o->position, area) > WOOD_RADIUS) continue;
if (o->stored && !to_move) continue; /* already on a woodpile */
if (claimed(self, objects, count, o)) continue;
float d = flatDistance(o->position, self->position);
if (d < best_dist) {
best_dist = d;
best = o;
}
}
return best;
}
static int firewoodInHands(const Object *self) {
int n = 0;
for (int i = 0; i < 2; i++)
if (self->hands.state[i] == HAND_HOLDING && self->hands.item[i]->type == OBJ_FIREWOOD) n++;
return n;
}
/* Two pieces at a time to the woodpile (the tool stays where the work was). */
static bool carryFirewood(Object *self, Object *const *objects, int count, float dt) {
Ai *ai = &self->ai;
Object *pile = ai->woodpile;
if (!pile) return false;
if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; }
int carried = firewoodInHands(self);
Object *next = carried < 2 ? loosePiece(self, objects, count, ai->work_area) : NULL;
if (next && (carried == 0 || flatDistance(next->position, self->position) < 3.0f)) {
ToolKind held = Tools_held(self, NULL);
if (held == TOOL_AXE || held == TOOL_CHAINSAW || held == TOOL_SHEARS) { /* hands free for carrying */
for (int hand = 0; hand < 2; hand++)
if (self->hands.state[hand] == HAND_HOLDING && self->hands.item[hand]->type != OBJ_FIREWOOD) {
ai->tool = self->hands.item[hand];
Hands_drop(self, hand);
}
stand(self);
return true;
}
ai->job_target = next;
if (Hands_gapFrom(self->position, self->look.height, next) < PICKUP_REACH) {
stand(self);
turnTowards(self, vec3_sub(next->position, self->position), dt);
int hand = Hands_freeHand(self);
if (hand >= 0) Hands_pickUpObject(self, hand, next);
return true;
}
Vec3 dir = ai_steer(self, next, objects, count, next->position);
if (vec3_length(dir) < 0.5f) { stand(self); return true; }
ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
return true;
}
if (carried == 0) return false;
/* To the pile and stack them */
ai->job_target = NULL;
float dist = flatDistance(pile->position, self->position);
if (dist > pile->size.z * 0.5f + 0.7f) {
Vec3 dir = ai_steer(self, pile, objects, count, pile->position);
if (vec3_length(dir) < 0.5f) { stand(self); return true; }
ai_walk(self, dir, WALK_SPEED * 0.75f, dt);
return true;
}
stand(self);
turnTowards(self, vec3_sub(pile->position, self->position), dt);
for (int hand = 0; hand < 2; hand++) {
Object *piece = self->hands.item[hand];
if (self->hands.state[hand] != HAND_HOLDING || !piece || piece->type != OBJ_FIREWOOD) continue;
Hands_drop(self, hand);
ai_pileSlid(self, Object_stackFirewood(pile, piece, objects, count, ai_stackCare(self)));
}
return true;
}
/* ---------- Tidying up: branches and twigs onto a heap by the woodpile ---------- */
#define TIDY_CARE 0.6f /* this careful (ai_stackCare): branches lying about go onto a heap */
#define TIDY_HEAVY 3.0f /* (kg; heavier ones stay where they are) */
/* Where the branches go: a heap just off the end of the woodpile. */
static Vec3 brushHeap(const Object *pile) {
Vec3 along = pile->colliders[0].axes[0];
return vec3_add(pile->position, vec3_scale(along, pile->size.x * 0.5f + 0.8f));
}
static int branchesInHands(const Object *self) {
int n = 0;
for (int i = 0; i < 2; i++)
if (self->hands.state[i] == HAND_HOLDING && self->hands.item[i]->type == OBJ_BRANCH) n++;
return n;
}
/* A branch or twig lying about the work area (not on the heap yet). */
static Object *looseBranch(const Object *self, Object *const *objects, int count, Vec3 area, Vec3 heap) {
Object *best = NULL;
float best_dist = 1e9f;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o->type != OBJ_BRANCH || o->stored || o->is_held || o->broken || o->weight > TIDY_HEAVY) continue;
if (flatDistance(o->position, area) > WOOD_RADIUS || flatDistance(o->position, heap) < 1.0f) continue;
if (claimed(self, objects, count, o)) continue;
float d = flatDistance(o->position, self->position);
if (d < best_dist) { best_dist = d; best = o; }
}
return best;
}
/* Careful people don't leave the branches lying about: two at a time onto the heap. */
static bool tidyBranches(Object *self, Object *const *objects, int count, float dt) {
Ai *ai = &self->ai;
Object *pile = ai->woodpile;
if (!pile || ai_stackCare(self) < TIDY_CARE) return false;
if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; }
const Vec3 heap = brushHeap(pile);
int carried = branchesInHands(self);
Object *next = carried < 2 ? looseBranch(self, objects, count, ai->work_area, heap) : NULL;
if (next && (carried == 0 || flatDistance(next->position, self->position) < 3.0f)) {
for (int hand = 0; hand < 2; hand++) /* (hands free for it: the tool stays here) */
if (self->hands.state[hand] == HAND_HOLDING && self->hands.item[hand]->type != OBJ_BRANCH) {
if (toolOf(self->hands.item[hand]) != TOOL_NONE) ai->tool = self->hands.item[hand];
Hands_drop(self, hand);
stand(self);
return true;
}
ai->job_target = next;
if (Hands_gapFrom(self->position, self->look.height, next) < PICKUP_REACH) {
stand(self);
turnTowards(self, vec3_sub(next->position, self->position), dt);
int hand = Hands_freeHand(self);
if (hand >= 0) Hands_pickUpObject(self, hand, next);
return true;
}
Vec3 dir = ai_steer(self, next, objects, count, next->position);
if (vec3_length(dir) < 0.5f) { stand(self); return true; }
ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
return true;
}
if (carried == 0) return false;
ai->job_target = NULL;
Vec3 to = vec3_sub(heap, self->position);
to.y = 0.0f;
if (vec3_length(to) > 0.5f) {
Vec3 dir = ai_steer(self, NULL, objects, count, heap);
if (vec3_length(dir) < 0.5f) { stand(self); return true; }
ai_walk(self, dir, WALK_SPEED * 0.75f, dt);
return true;
}
stand(self);
turnTowards(self, to, dt);
for (int hand = 0; hand < 2; hand++) {
Object *b = self->hands.item[hand];
if (self->hands.state[hand] != HAND_HOLDING || !b || b->type != OBJ_BRANCH) continue;
Hands_drop(self, hand);
b->stored = 1; /* (on the heap) */
}
return true;
}
/* Skilled, sure-handed, sober and unhurried: neat, stable piles. The others
just put it on (and a high loose pile comes down now and then). */
float ai_stackCare(const Object *self) {
const Character *c = &self->ai.character;
float care = 0.3f + 0.55f * self->skill[ORDER_FIREWOOD] + 0.1f * c->likes[ORDER_FIREWOOD]
- 0.5f * (Body_clumsiness(&self->body) - 0.5f) - 0.3f * (c->tempo - 1.0f)
- 0.6f * fminf(self->drunk, 1.0f) - 0.1f * self->temperament.nerves + 0.05f * self->temperament.pride
- 0.2f * (1.0f - self->needs.stamina);
return clampf(care, 0.0f, 1.0f);
}
void ai_pileSlid(Object *self, int slid) {
if (slid <= 0) return;
Ai_say(self, slid > 3 ? "Whoa - there goes the pile!" : "Oops... that came down.");
}
/* ---------- Other jobs: trash and the fire ---------- */
#define PICK_TIME 1.4f /* bending down for a piece of trash (seconds, at a normal pace) */
#define TRASH_AREA 45.0f /* how far from where they were asked they look for trash */
#define FIRE_WANTED 6.0f /* kg of wood a looked-after fire should have */
#define WOOD_SEARCH 60.0f /* how far they go for wood for the fire */
#define BAG_PIECES 24 /* see Object_fillBag */
static float g_hour = 12.0f;
void Ai_setHour(float hour) { g_hour = hour; }
static bool evening(void) { return g_hour >= 18.0f || g_hour < 3.0f; }
#define SUN_SHIRT_OFF 90.0f /* s of hard work in the sun before the shirt comes off ... */
#define SUN_SHIRT_ON 40.0f /* ... and on again when it's cooled down to this */
void ai_sunShirt(Object *self, float dt) {
Ai *ai = &self->ai;
bool sunny = g_hour >= 11.0f && g_hour < 17.5f;
bool hard = (ai->mode == AI_WORK && (ai->order == ORDER_FIREWOOD || ai->order == ORDER_TREES)) ||
(ai->mode == AI_HELP && Tools_held(self, NULL) >= TOOL_AXE);
float warm = sunny && hard ? 1.0f + 0.5f * (1.0f - self->needs.stamina) : sunny ? -0.5f : -3.0f;
ai->sun_heat = clampf(ai->sun_heat + warm * dt, 0.0f, SUN_SHIRT_OFF * 1.5f);
if (self->look.top == TOP_SHIRT && ai->sun_heat >= SUN_SHIRT_OFF && self->temperament.pride > -0.3f && !self->look.keeps_shirt) {
self->look.top = self->female ? TOP_CROP : TOP_BARE;
if (ai->speech_time <= 0.0f) Ai_say(self, "Phew, it's hot. Off with the shirt.");
} else if ((self->look.top == TOP_BARE || self->look.top == TOP_CROP) && ai->sun_heat < SUN_SHIRT_ON) {
self->look.top = TOP_SHIRT;
}
}
Object *ai_nearestOf(const Object *self, Object *const *objects, int count, ObjectType type, ObjectType dispenses) {
Object *best = NULL;
float best_d = 1e9f;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o->type != type || (type == OBJ_SUPPLY && o->dispenses != dispenses)) continue;
float d = flatDistance(o->position, self->position);
if (d < best_d) {
best_d = d;
best = o;
}
}
return best;
}
static Object *nearestTrash(const Object *self, Object *const *objects, int count, Vec3 area) {
Object *best = NULL;
float best_d = 1e9f;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (!Object_isTrash(o) || o->is_held || flatDistance(o->position, area) > TRASH_AREA) continue;
if (ai_skipped(&self->ai, o)) continue; /* couldn't get to it */
float d = flatDistance(o->position, self->position);
if (d < best_d && !claimed(self, objects, count, o)) {
best_d = d;
best = o;
}
}
return best;
}
static int trashAround(Object *const *objects, int count, Vec3 area) {
int n = 0;
for (int i = 0; i < count; i++)
if (Object_isTrash(objects[i]) && !objects[i]->is_held && flatDistance(objects[i]->position, area) < TRASH_AREA) n++;
return n;
}
/* Firewood quarters there are, not yet burning. */
static int firewoodStock(Object *const *objects, int count) {
int n = 0;
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (o->type == OBJ_FIREWOOD && o->mesh.vao && o->charred <= 0.0f) n++;
}
return n;
}
/* Wood for the fire: firewood first (the woodpile too), else branches. */
static Object *woodForFire(const Object *self, Object *const *objects, int count, const Object *fire) {
Object *best = NULL;
float best_d = 1e9f;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (!Object_isFuel(o) || o->is_held || o->lifted || o->weight > 10.0f) continue;
if (flatDistance(o->position, fire->position) > WOOD_SEARCH) continue;
float d = flatDistance(o->position, self->position) + (o->type == OBJ_FIREWOOD ? 0.0f : 15.0f);
if (d < best_d && !inAnyFire(objects, count, o) && !claimed(self, objects, count, o)) {
best_d = d;
best = o;
}
}
return best;
}
/* Collecting trash: a bag from the box, every piece into it, full bags to the container. */
static void workTrash(Object *self, Object *const *objects, int count, float dt) {
Ai *ai = &self->ai;
Object *bag = Camp_bagInHand(self);
if (!bag) {
Object *box = ai_nearestOf(self, objects, count, OBJ_SUPPLY, OBJ_BAG);
if (!box) { Ai_say(self, "There are no garbage bags anywhere."); Ai_stopWork(self); return; }
if (goTo(self, box, objects, count, 0.8f, dt)) {
freeHands(self, NULL);
ai->wants_bag = true;
}
return;
}
freeHands(self, bag); /* nothing else in the hands */
Object *trash = bag->stored < BAG_PIECES ? nearestTrash(self, objects, count, ai->work_area) : NULL;
if (!trash) {
if (bag->stored == 0) { /* nothing left: put the empty bag down */
Ai_say(self, "All clean!");
for (int h = 0; h < 2; h++) if (self->hands.item[h] == bag) Hands_drop(self, h);
Ai_stopWork(self);
return;
}
Object *bin = ai_nearestOf(self, objects, count, OBJ_BIN, OBJ_BOX);
if (!bin) { Ai_say(self, "Where does the trash go?"); Ai_stopWork(self); return; }
if (goTo(self, bin, objects, count, 0.9f, dt)) {
Camp_throwAway(self);
ai->job_target = NULL;
if (!nearestTrash(self, objects, count, ai->work_area)) { Ai_say(self, "All clean!"); Ai_stopWork(self); }
}
return;
}
if (ai->job_target != trash) { ai->job_time = 0.0f; ai->stuck = 0.0f; }
ai->job_target = trash;
if (flatDistance(trash->position, self->position) > 0.95f) {
Vec3 dir = ai_steer(self, trash, objects, count, trash->position);
if (vec3_length(dir) < 0.5f) {
stand(self);
if ((ai->stuck += dt) > 3.0f) skipTarget(ai, trash); /* can't get there: another one */
} else {
ai_walk(self, dir, WALK_SPEED * 0.7f, dt);
}
return;
}
stand(self);
turnTowards(self, vec3_sub(trash->position, self->position), dt);
Hands_touch(self, trash->position); /* bends down for it */
ai->job_time += dt * self->work_rate;
if (ai->job_time >= PICK_TIME) {
Camp_bag(bag, trash);
Ai_practice(self, ORDER_TRASH, 0.01f);
ai->job_target = NULL;
ai->job_time = 0.0f;
self->needs.stamina = fmaxf(0.0f, self->needs.stamina - 0.06f); /* bending down again and again */
}
}
/* Looking after the fire: wood in, lit when it gets dark, then sit by it. */
static void workFire(Object *self, Object *const *objects, int count, float dt) {
Ai *ai = &self->ai;
Object *fire = Camp_fireNear(objects, count, ai->work_area, 80.0f, false);
if (!fire) { Ai_say(self, "Where's the fireplace?"); Ai_stopWork(self); return; }
const float near = fire->size.x * 0.5f + 0.9f;
/* Wood in the hands goes into the fire */
for (int h = 0; h < 2; h++) {
Object *held = self->hands.item[h];
if (self->hands.state[h] == HAND_HOLDING && held && Object_isFuel(held)) {
if (flatDistance(fire->position, self->position) > near) {
Vec3 dir = ai_steer(self, fire, objects, count, fire->position);
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, WALK_SPEED * 0.75f, dt);
} else {
stand(self);
turnTowards(self, vec3_sub(fire->position, self->position), dt);
if (Camp_feed(self, fire)) Ai_practice(self, ORDER_FIRE, 0.015f);
}
return;
}
if (self->hands.state[h] == HAND_REACHING) { stand(self); return; }
}
if (fire->fuel < FIRE_WANTED) {
Object *wood = woodForFire(self, objects, count, fire);
if (wood) {
ai->job_target = wood;
freeHands(self, NULL);
if (goTo(self, wood, objects, count, PICKUP_REACH, dt)) {
int hand = Hands_freeHand(self);
if (hand >= 0) Hands_pickUpObject(self, hand, wood);
}
return;
}
if (fire->fuel <= 0.0f) {
Ai_say(self, "There's no more wood for the fire. Someone should make firewood.");
Ai_stopWork(self);
return;
}
}
ai->job_target = NULL;
if (!fire->burning && evening()) {
if (!self->lighter || self->lighter_uses <= 0) { /* a lighter first: who's got one? */
Object *lender = Pockets_lighterNear(self, objects, count, LIGHTER_ASK);
if (!lender) {
if (ai->lighter_ask <= 0.0f) {
Ai_say(self, self->lighter ? "My lighter's empty. Has anyone got a light?" : "Has anyone got a lighter?");
ai->lighter_ask = 45.0f;
}
stand(self);
return;
}
ai->job_target = lender;
if (!goTo(self, lender, objects, count, 1.2f, dt)) return;
ai->job_target = NULL;
static char line[AI_SPEECH_MAX];
if (lender->type == OBJ_PLAYER) snprintf(line, sizeof line, "I'll borrow your lighter - thanks!");
else snprintf(line, sizeof line, "Lend me your lighter, %s? Thanks!", lender->name);
Ai_say(self, line);
if (lender->type == OBJ_HUMAN && lender->ai.speech_time <= 0.0f) Ai_say(lender, "Sure, here.");
if (self->lighter) { self->lighter = false; self->lighter_of = NULL; } /* (the empty one is thrown away) */
Pockets_passLighter(lender, self);
return; /* (back to the fire) */
}
if (flatDistance(fire->position, self->position) > near) {
Vec3 dir = ai_steer(self, fire, objects, count, fire->position);
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
return;
}
if (Pockets_light(self) && Camp_light(fire)) {
Ai_say(self, self->lighter_uses == 0 ? "There we go! ...and that was the last of the lighter." : "There we go!");
Ai_practice(self, ORDER_FIRE, 0.03f);
}
}
/* Nothing to do: wait by the fire */
float d = flatDistance(fire->position, self->position);
if (d > FIRE_SIT_DISTANCE + 1.0f) {
if (ai->goal.x == 0.0f || flatDistance(ai->goal, fire->position) > FIRE_SIT_DISTANCE + 0.5f) pickFireSpot(self, objects, count, fire);
Vec3 dir = ai_steer(self, NULL, objects, count, ai->goal);
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, WALK_SPEED * 0.6f, dt);
return;
}
stand(self);
if (!self->sitting) Object_sit(self, Object_seatNear(objects, count, self->position, 1.2f), fire->position);
if (self->sit_style != SIT_SEAT) turnTowards(self, vec3_sub(fire->position, self->position), dt * 0.5f);
}
/* ---------- Who they are: likes, needs, yes or no ---------- */
bool Ai_needed(const Object *self, Object *const *objects, int count, Order order) {
switch (order) {
case ORDER_FIREWOOD: return firewoodStock(objects, count) < 16;
case ORDER_TREES: {
if (firewoodStock(objects, count) >= 16) return false;
for (int i = 0; i < count; i++) /* wood lying around comes first */
if (Tools_cuttable(objects[i]) || Tools_splittable(objects[i]) ||
(objects[i]->type == OBJ_TREE && objects[i]->toppled)) return false;
return true;
}
case ORDER_TRASH: return trashAround(objects, count, self->position) > 3;
case ORDER_FIRE: {
if (!evening() && g_hour < 16.5f) return false;
Object *fire = Camp_fireNear(objects, count, self->position, 80.0f, false);
return fire && (!fire->burning || fire->fuel < 2.0f) && (fire->fuel > 0.0f || firewoodStock(objects, count) > 0);
}
default: return false;
}
}
static const char *const DISLIKE[ORDER_COUNT] = {
[ORDER_FIREWOOD] = "Chopping wood? Not really my thing.",
[ORDER_TREES] = "Felling trees? No, that's not for me.",
[ORDER_TRASH] = "Picking up other people's trash? No thanks.",
[ORDER_FIRE] = "Babysitting a fire? Nah.",
};
static const char *const NOT_NEEDED[ORDER_COUNT] = {
[ORDER_FIREWOOD] = "We've got plenty of firewood already.",
[ORDER_TREES] = "We've got enough wood for now, haven't we?",
[ORDER_TRASH] = "It's pretty clean already.",
[ORDER_FIRE] = "Now? It's not even evening.",
};
bool Ai_willDo(Object *self, Object *const *objects, int count, Order order, bool their_idea, const char **reply) {
ai_ensureReady(self);
if (self->lives_here) { /* (Kurti and his sons: it's their land, your festival - your work) */
*reply = random01(&self->ai) < 0.5f ? "Ha - it's your festival, lad. I'll watch." : "Not my job - we just lend you the land.";
return false;
}
const Character *c = &self->ai.character;
float like = c->likes[order], tired = 1.0f - self->needs.stamina, drunk = fminf(self->drunk, 1.0f);
bool needed = Ai_needed(self, objects, count, order);
float score = c->willing * 0.7f + like * 0.6f - tired * 0.5f - drunk * 0.8f - self->ai.annoyed * 0.8f + (random01(&self->ai) - 0.5f) * 0.2f;
if (their_idea) score += 0.4f;
if (!needed) score -= 0.3f;
if (score >= 0.25f) {
*reply = like > 0.45f ? "Oh yes, gladly!" : like < -0.3f ? "Hmm... okay, if I must." : "Sure, I can do that.";
return true;
}
*reply = drunk > 0.5f ? "I'm in no shape for that..." : tired > 0.7f ? "I'm too worn out right now."
: !needed ? NOT_NEEDED[order] : like < 0.0f ? DISLIKE[order] : "Not right now, sorry.";
return false;
}
Order Ai_job(const Object *self) {
const Ai *ai = &self->ai;
if (self->type == OBJ_PLAYER) return self->work_state.job == JOB_FELL ? ORDER_TREES : ORDER_FIREWOOD;
return ai->mode == AI_WORK ? ai->order : ai->mode == AI_HELP ? ORDER_FIREWOOD : ORDER_NONE;
}
float Ai_workRate(const Object *self) {
Order job = Ai_job(self);
if (job == ORDER_NONE) return 1.0f;
float like = self->type == OBJ_HUMAN ? self->ai.character.likes[job] : 0.0f;
return (1.0f + 0.35f * like) * (0.7f + 0.6f * self->skill[job]); /* practice makes it quicker */
}
void Ai_practice(Object *self, Order job, float amount) {
if (job <= ORDER_NONE || job >= ORDER_COUNT) return;
self->skill[job] = fminf(1.0f, self->skill[job] + amount * (1.0f - self->skill[job]));
}
bool Ai_call(Object *self, const Object *player) {
ai_ensureReady(self);
Ai *ai = &self->ai;
if (Body_isBusy(&self->body) || self->on_fire > 0.0f || ai->talking || ai->mode == AI_FOLLOW) return false;
if (ai->mode == AI_WORK && ai->character.willing < 0.4f && random01(ai) < 0.5f) {
Ai_say(self, "In a minute!");
return true;
}
Ai_say(self, ai->mode == AI_WORK ? "Coming!" : self->temperament.pride < -0.3f ? "Me? ...okay." : "Yes? Coming!");
self->sitting = false;
approach(self, TOPIC_NONE);
ai->talk_cooldown = 0.0f;
(void)player;
return true;
}
/* ---------- On an order ---------- */
static Object *closestLoosePiece(const Object *self, Object *const *objects, int count) {
return loosePiece(self, objects, count, self->ai.work_area);
}
#define TOOL_WAIT_RANGE 30.0f
/* The player has the axe we need: the bold ask for it (once), the others
wait nearby until it's put down (then it's the nearest one to fetch). */
static bool waitForTool(Object *self, Object *const *objects, int count, Object *player, float dt) {
Ai *ai = &self->ai;
float d = flatDistance(player->position, self->position);
if (!Tools_canDo(Tools_held(player, NULL), JOB_SPLIT) || d > TOOL_WAIT_RANGE) {
ai->tool_waiting = false;
return false;
}
bool bold = self->temperament.pride + 0.5f * self->temperament.energy > 0.05f;
if (bold && !ai->tool_asked && !Body_isBusy(&player->body)) {
ai->tool_asked = true;
approach(self, TOPIC_TOOL);
return true;
}
if (!ai->tool_waiting) {
ai->tool_waiting = true;
Ai_say(self, ai->tool_asked ? "I'll wait till you're done with it." : "I'll wait for the axe...");
}
Vec3 to = flat(vec3_sub(player->position, self->position));
if (d > 3.5f) {
Vec3 dir = ai_steer(self, player, objects, count, vec3_sub(player->position, vec3_scale(vec3_normalize(to), 2.5f)));
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, WALK_SPEED * 0.7f, dt);
} else {
stand(self);
turnTowards(self, to, dt * 0.5f);
}
return true;
}
/* ---------- Their own way of working: thinking it over, breaks ---------- */
static const char *const CHAT_OPEN[] = { "Hey! How's it going?", "Taking a little break - you?", "Nice day for it, huh?",
"Got a minute?" };
static const char *const CHAT_MORE[] = { "Did you see the size of that tree?", "I could do with a cold one later.",
"This place is coming along.", "My arms will hurt tomorrow.",
"Who brought the sausages?", "Fire tonight, right?" };
static const char *const CHAT_REPLY[] = { "Ha, yes.", "Not bad, not bad.", "Tell me about it.", "Mhm.", "Right?", "True!" };
#define PICK(list, ai) (list)[(int)(random01(ai) * (float)(sizeof(list) / sizeof((list)[0]))) % (int)(sizeof(list) / sizeof((list)[0]))]
#define CHAT_RANGE 30.0f /* they go this far for someone to chat with */
#define CHAT_CLOSE 1.7f /* ... and stand this close */
#define CHAT_PAUSE 3.0f /* seconds between lines */
/* Someone to chat with: the nearest person awake and not up a ladder. */
static Object *chatPartner(const Object *self, Object *const *objects, int count) {
Object *best = NULL;
float best_d = CHAT_RANGE;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o == self || !o->is_character || o->type == OBJ_DOG || Body_isDown(&o->body) || o->on_ladder || o->toilet) continue;
float d = flatDistance(o->position, self->position);
if (d < best_d) { best = o; best_d = d; }
}
return best;
}
#define BREAK_OWN 0.6f /* their own kind of break this often, else another */
#define BREAK_LONG 0.3f /* a long one this often (BREAK_LONGER x), else short (BREAK_SHORTER x) */
#define BREAK_LONGER 1.8f
#define BREAK_SHORTER 0.7f
#define COFFEE_RANGE 70.0f /* m: a stove this near is worth a coffee */
static void nextBreak(Object *self) {
Ai *ai = &self->ai;
ai->streak = ai->character.break_every * (0.7f + 0.6f * random01(ai)) * (1.0f - 0.4f * self->worn); /* (worn out: sooner) */
}
static void endBreak(Object *self) {
Ai *ai = &self->ai;
ai->on_break = 0.0f;
ai->chat_with = NULL;
ai->break_at = NULL;
ai->task_seen = NULL; /* (whoever thinks things over does so again) */
self->sitting = false;
nextBreak(self);
Ai_say(self, random01(ai) < 0.5f ? "Right, back to it!" : "Okay, back to work.");
}
static Object *stoveNear(const Object *self, Object *const *objects, int count) {
Object *best = NULL;
float best_d = COFFEE_RANGE;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o->type != OBJ_FURNITURE || o->variant != FURN_STOVE) continue;
float d = flatDistance(o->position, self->position);
if (d < best_d) { best = o; best_d = d; }
}
return best;
}
/* Which break this time: mostly their own way, now and then a coffee, a walk, a chat or a sit. */
static BreakKind pickBreak(Object *self, Object *const *objects, int count) {
Ai *ai = &self->ai;
if (random01(ai) < BREAK_OWN) return ai->character.break_kind;
static const BreakKind KINDS[] = { BREAK_SIT, BREAK_WALK, BREAK_CHAT, BREAK_COFFEE };
BreakKind k = KINDS[(int)(random01(ai) * 4.0f) % 4];
if (k == BREAK_COFFEE && !stoveNear(self, objects, count)) k = BREAK_SIT;
if (k == BREAK_CHAT && !chatPartner(self, objects, count)) k = BREAK_SIT;
return k;
}
/* Now and then a break, short or long, their way or another: sitting down,
a little walk, a chat, a coffee at the stove. The tool goes down first. True while on it. */
/* A break: what they work with put down - side by side, square, a step back
from where they worked (the orderly), or just dropped any old way. */
#define TIDY_TOOLS 0.5f /* this careful (ai_stackCare): put down neatly */
static void putToolsDown(Object *self) {
Ai *ai = &self->ai;
Hands_setAside(self, ai->set_aside);
bool tidy = ai_stackCare(self) > TIDY_TOOLS;
Vec3 f = Object_facing(self), side = vec3(-f.z, 0.0f, f.x);
for (int k = 0; k < 2; k++) {
Object *item = ai->set_aside[k];
if (!item) continue;
bool flat = item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER;
if (tidy) {
Vec3 at = vec3_add(self->position, vec3_add(vec3_scale(f, -0.9f), vec3_scale(side, k ? 0.35f : -0.35f)));
item->rotation = flat ? (item->type == OBJ_LADDER ? vec3(PI_F * 0.5f, self->rotation.y + PI_F * 0.5f, 0.0f)
: vec3(0.0f, self->rotation.y, PI_F * 0.5f)) /* (on its side, the handle across) */
: vec3(0.0f, self->rotation.y, 0.0f);
Object_syncColliders(item);
item->position = vec3(at.x, item->position.y, at.z);
} else {
float turn = (random01(ai) - 0.5f) * 2.5f;
item->rotation.y += turn;
item->position = vec3_add(item->position, vec3((random01(ai) - 0.5f) * 0.5f, 0.0f, (random01(ai) - 0.5f) * 0.5f));
}
item->velocity = vec3_zero();
Object_syncColliders(item);
}
}
static bool takeBreak(Object *self, Object *const *objects, int count, float dt) {
Ai *ai = &self->ai;
const Character *c = &ai->character;
if (c->break_kind == BREAK_NONE || c->break_every <= 0.0f) return false;
if (ai->on_break <= 0.0f) {
if (ai->streak <= 0.0f) nextBreak(self);
if ((ai->streak -= dt) > 0.0f || Tools_isWorking(self) || firewoodInHands(self) > 0) return false;
ai->break_now = pickBreak(self, objects, count);
bool long_one = random01(ai) < BREAK_LONG;
ai->on_break = c->break_length * (long_one ? BREAK_LONGER : BREAK_SHORTER) * (0.8f + 0.4f * random01(ai)) * (1.0f + 0.5f * self->worn);
ai->break_from = self->position;
ai->break_back = false;
ai->break_at = NULL;
ai->break_sip = 0.0f;
ai->chat_with = ai->break_now == BREAK_CHAT ? chatPartner(self, objects, count) : NULL;
ai->chat_line = 0.0f;
ai->chat_said = 0;
if (self->hands.item[HAND_LEFT] || self->hands.item[HAND_RIGHT]) putToolsDown(self); /* (taken again after) */
if (self->worn > 0.45f && random01(ai) < 0.5f) Ai_say(self, "Ugh, I'm still beat from yesterday.");
else if (ai->break_now == BREAK_WALK) Ai_say(self, long_one ? "I need some air. Back in a bit." : "I'll stretch my legs for a bit.");
else if (ai->break_now == BREAK_COFFEE) Ai_say(self, "Coffee time.");
else if (!ai->chat_with) Ai_say(self, long_one ? "I need a proper break." : "Time for a little sit-down.");
if (ai->break_now == BREAK_WALK) {
float a = random01(ai) * 2.0f * PI_F, d = (long_one ? 12.0f : 7.0f) + 5.0f * random01(ai);
setGoal(ai, vec3_add(self->position, vec3(cosf(a) * d, 0.0f, sinf(a) * d)));
} else if (ai->break_now == BREAK_COFFEE) {
ai->break_at = stoveNear(self, objects, count);
if (ai->break_at) setGoal(ai, ai->break_at->position);
else ai->break_now = BREAK_SIT;
}
}
ai->on_break -= dt;
if (ai->break_now == BREAK_COFFEE && ai->break_at) { /* over to the stove, a coffee there, and back */
Object *stove = ai->break_at;
Vec3 front = vec3_add(stove->position, vec3_scale(stove->colliders[0].axes[2], stove->size.z * 0.5f + 0.5f));
Vec3 goal = ai->break_back ? ai->break_from : front;
float d = flatDistance(goal, self->position);
if (!ai->break_back && d < 0.6f) { /* there: drinking it, a sip now and then */
stand(self);
turnTowards(self, vec3_sub(stove->position, self->position), dt);
if ((ai->break_sip -= dt) <= 0.0f) {
ai->break_sip = 6.0f + 6.0f * random01(ai);
if (ai->speech_time <= 0.0f && random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "Ahh. Good coffee." : "Mm.");
}
if (ai->on_break <= 0.0f) { ai->break_back = true; setGoal(ai, ai->break_from); }
return true;
}
if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) { endBreak(self); return false; }
if (!ai->break_back && isStuck(ai, d, dt)) { ai->break_back = true; setGoal(ai, ai->break_from); }
Vec3 dir = ai_steer(self, NULL, objects, count, goal);
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, WALK_SPEED * 0.6f, dt);
return true;
}
if (ai->break_now == BREAK_WALK) { /* out a bit, and back */
Vec3 goal = ai->break_back ? ai->break_from : ai->goal;
float d = flatDistance(goal, self->position);
if (!ai->break_back && (d < 1.0f || isStuck(ai, d, dt) || ai->on_break < c->break_length * 0.3f)) {
ai->break_back = true;
setGoal(ai, ai->break_from);
} else if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) {
if (ai->on_break > 3.0f) ai->break_now = BREAK_SIT; /* (back early: the rest of it sitting) */
else { endBreak(self); return false; }
}
Vec3 dir = ai_steer(self, NULL, objects, count, goal);
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, STROLL_SPEED, dt);
return true;
}
if (ai->on_break <= 0.0f) { endBreak(self); return false; }
Object *other = ai->chat_with;
if (other && (Body_isDown(&other->body) || flatDistance(other->position, self->position) > CHAT_RANGE * 1.5f)) other = ai->chat_with = NULL;
if (other) { /* a chat: over to them, then a word now and then (they answer) */
/* (over to them; walking along with them while they move - no stop-go at the edge of chatting distance) */
float gap = Hands_gapFrom(self->position, self->look.height, other);
ai->chat_near = gap < (ai->chat_near ? CHAT_CLOSE * 1.6f : CHAT_CLOSE);
if (!ai->chat_near) {
Vec3 dir = ai_steer(self, other, objects, count, other->position);
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, fminf(WALK_SPEED * 0.8f, (gap - CHAT_CLOSE) * 2.0f + 0.3f), dt);
return true;
}
stand(self);
turnTowards(self, vec3_sub(other->position, self->position), dt);
if ((ai->chat_line -= dt) > 0.0f) return true;
ai->chat_line = CHAT_PAUSE * (0.8f + 0.4f * random01(ai));
if (ai->chat_said % 2 == 0) Ai_say(self, ai->chat_said == 0 ? PICK(CHAT_OPEN, ai) : PICK(CHAT_MORE, ai));
else if (other->type == OBJ_HUMAN && other->ai.speech_time <= 0.0f && !other->ai.talking) Ai_say(other, PICK(CHAT_REPLY, ai));
ai->chat_said++;
return true;
}
/* sitting down (also a chat with nobody around) */
stand(self);
if (!self->sitting) Object_sit(self, Object_seatNear(objects, count, self->position, 2.0f), self->position);
return true;
}
/* Before starting on something new: some think it over first (the hand to the chin). True while thinking. */
static bool thinkItOver(Object *self, const Object *target, float dt) {
Ai *ai = &self->ai;
if (target != ai->task_seen) {
ai->task_seen = target;
float think = ai->character.ponder * (0.5f + random01(ai));
if (think > 0.4f && random01(ai) < 0.6f) {
ai->ponder = think;
Body_think(&self->body, think);
}
}
if (ai->ponder <= 0.0f) return false;
ai->ponder -= dt;
stand(self);
turnTowards(self, vec3_sub(target->position, self->position), dt);
return true;
}
/* ---------- The work place ---------- */
/* Before splitting or cutting: is there room? Things lying in the way: the
tidy clear them away first. Cramped (a wall, a car, a pile of stuff right
there): the others take the work somewhere with room - carried if it can
be, else dragged, with somebody called over to help when it's heavy. */
enum { PREP_NONE, PREP_CLEAR, PREP_CARRY, PREP_DONE };
#define PREP_CLUTTER 0.9f /* m round where they stand: loose things here are in the way */
#define PREP_WALL 0.35f /* gap to something fixed (or heavy) from where they stand: too close to swing */
#define PREP_CARRY_KG 25.0f /* things in the way up to this are carried off */
#define PREP_AWAY 2.2f /* ... this far */
#define PREP_TIMEOUT 45.0f
#define PREP_HELP 20.0f /* m: somebody this near is called to help drag it */
static bool ignoredHere(const Object *o) {
return o->type == OBJ_DECAL || o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->collider_count == 0 || o->is_held;
}
static bool inTheWay(const Object *self, const Object *o, const WoodTask *t) {
if (o == self || o == t->target || o->is_character || o->is_static || o->stored || ignoredHere(o)) return false;
if (o->weight > PREP_CARRY_KG || o->type == OBJ_WOODPILE || toolOf(o) != TOOL_NONE) return false; /* (tools lying there are for this) */
if (Tools_splittable(o) || Tools_cuttable(o) || o->type == OBJ_FIREWOOD) return false; /* (the wood is the work) */
return flatDistance(Object_colliderCenter(o, 0), t->spot) < PREP_CLUTTER;
}
static Object *clutterAt(Object *self, Object *const *objects, int count, const WoodTask *t) {
Object *best = NULL;
float best_d = 1e9f;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (!inTheWay(self, o, t) || ai_skipped(&self->ai, o)) continue;
float d = flatDistance(o->position, self->position);
if (d < best_d) { best_d = d; best = o; }
}
return best;
}
/* Something fixed (or too heavy to shift) right where they'd stand to swing. */
static bool cramped(const Object *self, Object *const *objects, int count, Vec3 spot, const Object *target) {
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (o == self || o == target || o->is_character || ignoredHere(o) || o->type == OBJ_TREE) continue;
if ((!o->is_static && o->weight < 60.0f) || Tools_cuttable(o) || Tools_splittable(o)) continue; /* (other wood: more work, not a wall) */
if (Hands_gapFrom(spot, self->look.height, o) < PREP_WALL) return true;
}
return false;
}
/* An open spot near the piece: nothing fixed within a swing, nothing lying about. */
static bool roomyPlace(const Object *self, Object *const *objects, int count, const Object *target, Vec3 *out) {
Vec3 from = Object_colliderCenter(target, 0);
for (int ring = 0; ring < 3; ring++) {
float r = 2.5f + 1.2f * (float)ring;
for (int k = 0; k < 8; k++) {
float a = (float)k * PI_F / 4.0f + 0.3f * (float)ring;
Vec3 p = vec3(from.x + r * cosf(a), self->position.y, from.z + r * sinf(a));
bool ok = true;
for (int i = 0; i < count && ok; i++) {
const Object *o = objects[i];
if (o == target || o == self || ignoredHere(o)) continue;
if (Hands_gapFrom(p, self->look.height, o) < (o->is_static || o->weight >= 60.0f ? 1.3f : 0.9f)) ok = false;
}
if (ok) { *out = p; return true; }
}
}
return false;
}
/* Somebody idle and willing near, to help drag it. */
static Object *helperFor(const Object *self, Object *const *objects, int count) {
Object *best = NULL;
float best_d = PREP_HELP;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o == self || o->type != OBJ_HUMAN || o->ai.talking || o->drunk > 0.6f || Body_isBusy(&o->body)) continue;
if ((o->ai.mode != AI_IDLE && o->ai.mode != AI_WANDER) || o->ai.character.willing < 0.4f) continue;
float d = flatDistance(o->position, self->position);
if (d < best_d) { best_d = d; best = o; }
}
return best;
}
static void prepDone(Object *self) {
Ai *ai = &self->ai;
if (Hands_isGrabbing(self)) Hands_release(self);
if (ai->prep_helper && ai->prep_helper->ai.mode == AI_HELP && ai->prep_helper->ai.leader == self) {
Ai_stopHelping(ai->prep_helper);
Ai_say(ai->prep_helper, "No problem.");
}
ai->prep_helper = NULL;
ai->prep_item = NULL;
ai->prep_stage = PREP_DONE;
}
/* True while busy getting the place ready (t: what they want to work on). */
static bool prepareSpot(Object *self, Object *const *objects, int count, const WoodTask *t, float dt) {
Ai *ai = &self->ai;
if (t->job != JOB_SPLIT && t->job != JOB_CUT) return false;
if (ai->prep_for != t->target) { /* a new piece: look at the place (once, whoever does) */
if (t->target->spot_checked) return false;
t->target->spot_checked = true;
ai->prep_for = t->target;
ai->prep_time = 0.0f;
ai->prep_item = ai->prep_helper = NULL;
bool tidy = ai->character.likes[ORDER_TRASH] > 0.5f || ai_stackCare(self) >= 0.65f; /* (who likes things tidy) */
bool messy = clutterAt(self, objects, count, t) != NULL;
bool tight = cramped(self, objects, count, t->spot, t->target);
/* (no room: elsewhere with it; things lying about: the tidy clear them, the others don't mind) */
ai->prep_stage = tight && roomyPlace(self, objects, count, t->target, &ai->prep_spot) ? PREP_CARRY
: tidy && messy ? PREP_CLEAR : PREP_DONE;
if (ai->prep_stage == PREP_CLEAR) Ai_say(self, "Let me clear this away first.");
else if (ai->prep_stage == PREP_CARRY) Ai_say(self, "No room to swing here - over there's better.");
}
if (ai->prep_stage == PREP_DONE || ai->prep_stage == PREP_NONE) return false;
if ((ai->prep_time += dt) > PREP_TIMEOUT) { /* (not getting anywhere: here then) */
for (int h = 0; h < 2; h++) if (self->hands.item[h] == t->target || self->hands.item[h] == ai->prep_item) Hands_drop(self, h);
prepDone(self);
return false;
}
if (self->hands.state[0] == HAND_REACHING || self->hands.state[1] == HAND_REACHING) { stand(self); return true; }
if (ai->prep_stage == PREP_CLEAR) {
Object *item = ai->prep_item;
bool carrying = item && (self->hands.item[0] == item || self->hands.item[1] == item);
if (carrying) { /* off to the side with it, away from the work */
Vec3 away = flat(vec3_sub(t->spot, t->aim));
away = vec3_length(away) > 0.05f ? vec3_normalize(away) : vec3_scale(Object_facing(self), -1.0f);
Vec3 side = vec3(-away.z, 0.0f, away.x);
Vec3 drop = vec3_add(t->spot, vec3_add(vec3_scale(away, 0.6f), vec3_scale(side, PREP_AWAY)));
if (flatDistance(drop, self->position) > 0.4f) {
Vec3 dir = ai_steer(self, NULL, objects, count, drop);
if (vec3_length(dir) < 0.5f) stand(self);
else ai_walk(self, dir, WALK_SPEED * 0.8f, dt);
return true;
}
for (int h = 0; h < 2; h++) if (self->hands.item[h] == item) Hands_drop(self, h);
ai->prep_item = NULL;
return true;
}
item = ai->prep_item = clutterAt(self, objects, count, t);
if (!item) { prepDone(self); return false; } /* (clear: to work) */
if (!goTo(self, item, objects, count, PICKUP_REACH, dt)) return true;
int hand = Hands_freeHand(self);
if (hand < 0 || !Hands_pickUpObject(self, hand, item)) {
freeHands(self, NULL); /* (the tool down a moment: fetched again after) */
hand = Hands_freeHand(self);
if (hand < 0 || !Hands_pickUpObject(self, hand, item)) { skipTarget(ai, item); ai->prep_item = NULL; }
}
return true;
}
/* Moving the work: carried if it can be, else dragged (with help if it's heavy and somebody's there) */
Object *piece = t->target;
bool carrying = self->hands.item[0] == piece || self->hands.item[1] == piece;
bool dragging = self->hands.grabbed == piece;
if (carrying || dragging) {
if (flatDistance(Object_colliderCenter(piece, 0), ai->prep_spot) < 0.7f) { /* there: down with it, and to work */
for (int h = 0; h < 2; h++) if (self->hands.item[h] == piece) Hands_drop(self, h);
if (dragging) Ai_say(self, ai->prep_helper ? "Thanks! That's better." : "There.");
prepDone(self);
return true;
}
Vec3 dir = ai_steer(self, piece, objects, count, ai->prep_spot);
if (vec3_length(dir) < 0.5f) { stand(self); return true; }
ai_walk(self, dir, dragging ? WALK_SPEED * 0.45f : WALK_SPEED * 0.7f, dt);
return true;
}
if (!goTo(self, piece, objects, count, PICKUP_REACH, dt)) return true;
freeHands(self, NULL);
if (Hands_pickUpObject(self, HAND_RIGHT, piece) || Hands_pickUpObject(self, HAND_LEFT, piece)) return true;
if (Hands_grabObject(self, piece)) { /* too big to lift: drag it, and call somebody to help */
Object *helper = piece->weight > 30.0f ? helperFor(self, objects, count) : NULL;
Object *player = piece->weight > 30.0f && !helper ? findPlayer(objects, count) : NULL;
if (player && flatDistance(player->position, self->position) < PREP_HELP)
Ai_say(self, "Can you give me a hand moving this? (grab it with F)");
if (helper) {
static char line[AI_SPEECH_MAX];
snprintf(line, sizeof line, "%s, give me a hand moving this?", helper->name);
Ai_say(self, line);
Ai_say(helper, "Sure.");
Ai_help(helper, self);
ai->prep_helper = helper;
}
return true;
}
prepDone(self); /* (can't move it: here then) */
return false;
}
void ai_updateWork(Object *self, Object *const *objects, int count, float dt) {
Ai *ai = &self->ai;
/* Worn out: a breather first */
if (ai->resting || self->needs.stamina < 0.12f) {
if (!ai->resting && self->needs.stamina < 0.12f) Ai_say(self, "Phew... I need a break.");
ai->resting = self->needs.stamina < 0.45f;
if (ai->resting) { stand(self); if (!self->sitting) Object_sit(self, NULL, self->position); return; }
}
if (ai->order != ORDER_FIRE && takeBreak(self, objects, count, dt)) return; /* (tending a fire they sit by it anyway) */
if (ai->order == ORDER_TRASH) { workTrash(self, objects, count, dt); return; }
if (ai->order == ORDER_FIRE) { workFire(self, objects, count, dt); return; }
if (Tools_isWorking(self)) { riskyTool(self, Tools_held(self, NULL), dt); stand(self); return; }
/* Lively people working near the player like company */
Object *player = findPlayer(objects, count);
if (player && ai->talk_cooldown <= 0.0f && self->temperament.energy > 0.5f &&
flatDistance(player->position, self->position) < 8.0f && player->work_state.since > 5.0f &&
random01(ai) < 0.004f * dt) {
approach(self, TOPIC_ASK_HAND);
return;
}
Object *moving = ai->prep_for;
if (ai->prep_stage == PREP_CARRY && moving && (self->hands.item[0] == moving || self->hands.item[1] == moving || self->hands.grabbed == moving)) {
WoodTask mt = ai_taskFor(self, moving, Tools_splittable(moving) ? JOB_SPLIT : JOB_CUT); /* (taking the work elsewhere: not to the pile) */
if (prepareSpot(self, objects, count, &mt, dt)) return;
}
if (firewoodInHands(self) > 0 && carryFirewood(self, objects, count, dt)) return;
if (branchesInHands(self) > 0 && tidyBranches(self, objects, count, dt)) return;
WoodTask t;
ai->timber = false;
if (ai_nextWoodTask(self, objects, count, ai->work_area, ai->order == ORDER_TREES, &t)) {
if (thinkItOver(self, t.target, dt)) return;
if (t.job == JOB_FELL) {
ai->work_area = Object_colliderCenter(t.target, 0);
if (t.target->fell_blocked) { /* (nowhere for it to come down) */
if (random01(ai) < 0.7f) Ai_say(self, "No room for that one to come down - I'll leave it.");
skipTarget(ai, t.target);
return;
}
ai->timber = Tools_fellDone(t.target) > FELL_WARN && !Tools_isWorking(self) && Ai_warnFellingRoom(self, objects, count, t.target, t.target->fell_plan);
if (ai->timber) { stand(self); return; } /* (not the last strokes with somebody under it) */
}
if (prepareSpot(self, objects, count, &t, dt)) return;
ai_workOn(self, objects, count, &t, dt);
return;
}
if (carryFirewood(self, objects, count, dt)) return;
if (tidyBranches(self, objects, count, dt)) return;
/* Done here. Firewood left lying because nobody said where it goes: ask. */
if (!ai->woodpile && !ai->no_pile && closestLoosePiece(self, objects, count) && player) {
Ai_say(self, "Done! Where should the firewood go?");
approach(self, TOPIC_WHERE);
ai->before = AI_WORK;
return;
}
/* Only splitting left, and the axe is in the player's hands: ask for it, or wait */
if (player && woodLeft(objects, count, ai->work_area) && waitForTool(self, objects, count, player, dt)) return;
Ai_say(self, woodLeft(objects, count, ai->work_area) ? "That's all I can do without an axe." : "All done!");
Ai_stopWork(self);
}