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