/* * Their part of the festival (world/cast.h: leads, helps, commitment). * * The lead and the helpers of a floor go to its stage in working hours and * build it, in goes of a couple of hours (then a break, eating, the loo...). * Held up for bought material that lies somewhere else (by the barn, where the * shopping gets unloaded), one of them goes and carries it over; logs are * yours to bring. Off on a shopping run (game_shop.c): over to the car. * The side jobs (mowing, the snails, Kerry's art) go the same way, their * work itself in ai_jobs.c. * How into it they are goes with what happens: a step finished lifts them; * being held up (no logs for the frame), working on worn out, nobody leading, * being annoyed wear it down. Too low, a lead gives up leading ("somebody * else can do it") and stays on as a helper; lower still, they stop helping * at all. Evenings at the fire bring a bit of it back. */ #include #include "people/ai_internal.h" #include "world/stage.h" #include "world/camp.h" #define WORK_FROM 8.5f /* o'clock: the working day ... */ #define WORK_TO 19.5f /* ... till */ #define SHIFT 7200.0f /* game s at it, then a break ... */ #define BREAK_TIME 2400.0f /* ... this long */ #define START_CHANCE 0.03f /* a second: idle in working hours, off to it (helpers: less without the lead there) */ #define TIRED 0.3f /* stamina below this: they stop */ #define NEED_STOP 0.65f /* hunger, thirst or bladder over this: they stop */ #define LEAD_NEAR 40.0f /* m: the lead about (helpers keep at it better) */ #define HELD_UP_LEAVE 1500.0f /* game s held up: they give it up for now */ #define REMARK_EVERY 90.0f /* real s between remarks about it */ #define QUIT_LEAD 0.3f /* commitment below this: the lead gives it up ... */ #define QUIT_HELP 0.15f /* ... below this they stop helping at all */ /* commitment, per game hour */ #define LIFT_DONE 0.08f /* (once) a step of it finished, for everybody at it */ #define WEAR_HELD_UP 0.06f #define WEAR_WORN 0.04f /* working on worn out */ #define WEAR_NO_LEAD 0.03f /* a helper with nobody leading it */ #define WEAR_ANNOYED 0.04f #define LIFT_EVENING 0.02f /* sitting at the fire of an evening */ #define FETCH_SPAN 150.0f /* m: material lying this far off is fetched for the stage */ #define FETCH_GIVE_UP 240.0f /* real s at fetching one thing: given up */ static bool floorProject(int p) { return p == PROJECT_HOFBUEHNE || p == PROJECT_OBSTWIESE || p == PROJECT_KARTOFFELACKER || p == PROJECT_HIDDEN; } /* Something to go and do on the ground: a floor, or a side job (ai_jobs.c). */ static bool workable(int p) { return floorProject(p) || p == PROJECT_MOWING || p == PROJECT_SNAILS || p == PROJECT_WOOD_ART || p == PROJECT_BIG_TREE; } static int theirProject(const Object *self) { if (workable(self->leads)) return self->leads; if (workable(self->helps)) return self->helps; return PROJECT_NONE; } static Object *leadOf(Object *const *objects, int count, int project) { for (int i = 0; i < count; i++) if (objects[i]->is_character && objects[i]->leads == project) return objects[i]; return NULL; } /* Who speaks for those at it: the lead if there, else the first of them (one voice, not a chorus). */ static bool firstAt(const Object *self, Object *const *objects, int count, int project) { if (self->leads == project) return true; for (int i = 0; i < count; i++) { const Object *o = objects[i]; if (o == self || !o->is_character || !o->ai.on_project || theirProject(o) != project) continue; if (o->leads == project || o->cast_id < self->cast_id) return false; } return true; } static void lose(Object *self, float per_hour, float game_dt) { self->commitment = clampf(self->commitment - per_hour * game_dt / 3600.0f, 0.0f, 1.0f); } void ai_commitment(Object *self, Object *const *objects, int count, float dt) { if (self->cast_id < 0 || !g_world) return; Ai *ai = &self->ai; const float gdt = g_world->game_dt; ai->praised = fmaxf(0.0f, ai->praised - gdt); if (ai->annoyed > 0.5f) lose(self, WEAR_ANNOYED, gdt); if (ai->on_project && self->needs.stamina < TIRED + 0.1f) lose(self, WEAR_WORN, gdt); if (ai->on_project && ai->held_up > 0.0f) lose(self, WEAR_HELD_UP, gdt); if (ai->on_project && self->leads != theirProject(self) && !leadOf(objects, count, theirProject(self))) lose(self, WEAR_NO_LEAD, gdt); if (self->sitting && (g_world->hour > 19.0f || g_world->hour < 1.0f) && Camp_fireNear(objects, count, self->position, 5.0f, true)) self->commitment = fminf(1.0f, self->commitment + LIFT_EVENING * gdt / 3600.0f); (void)dt; /* gone too low: the lead given up, or helping at all */ if (self->leads != PROJECT_NONE && self->commitment < QUIT_LEAD) { char line[AI_SPEECH_MAX]; const char *why = ai->held_up > 0.0f ? "Nobody brings what we need." : self->needs.stamina < TIRED + 0.1f ? "I'm wrecked." : ai->annoyed > 0.5f ? "I've had enough of all this." : "It's not working for me."; snprintf(line, sizeof line, "%s I'm out - somebody else can lead the %s.", why, Cast_projectName((Project)self->leads)); Ai_say(self, line); self->helps = self->commitment > QUIT_HELP ? self->leads : PROJECT_NONE; self->leads = PROJECT_NONE; } else if (self->helps != PROJECT_NONE && self->leads == PROJECT_NONE && self->commitment < QUIT_HELP) { Ai_say(self, "Count me out of the work. I'm just here for the party now."); self->helps = PROJECT_NONE; ai->on_project = false; } } /* Stop for now (a break, the day's over, held up too long...). */ static bool stop(Object *self, const char *say) { Ai *ai = &self->ai; if (ai->on_project && say && self->ai.speech_time <= 0.0f) Ai_say(self, say); if (ai->on_project) ai->project_off = BREAK_TIME; /* (a break before the next go: only after one) */ if (ai->fetch) { /* (whatever they were bringing: down where they are) */ for (int h = 0; h < 2; h++) if (self->hands.item[h] == ai->fetch) Hands_drop(self, h); ai->fetch = NULL; } ai->on_project = false; ai->shift = 0.0f; ai->held_up = 0.0f; return false; } /* Others at work on it with them. */ static bool othersAt(const Object *self, Object *const *objects, int count, int project) { for (int i = 0; i < count; i++) if (objects[i] != self && objects[i]->is_character && objects[i]->ai.on_project && theirProject(objects[i]) == project) return true; return false; } /* Lying by a stage (it's for that one). */ static bool atAStage(const Object *o, Object *const *objects, int count) { for (int i = 0; i < count; i++) if (objects[i]->type == OBJ_STAGE && flatDistance(objects[i]->position, o->position) < STAGE_LOG_REACH) return true; return false; } static bool fetchedByOther(const Object *self, const Object *it, Object *const *objects, int count) { for (int i = 0; i < count; i++) if (objects[i] != self && objects[i]->is_character && objects[i]->ai.fetch == it) return true; return false; } /* The nearest of that material lying about (not at a stage, nobody bringing it), or NULL. */ static Object *materialFor(const Object *self, Object *const *objects, int count, int kind) { Object *best = NULL; float best_d = FETCH_SPAN; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o->type != OBJ_MATERIAL || o->variant != kind || o->is_held || o->broken) continue; float d = flatDistance(o->position, self->position); if (d < best_d && !atAStage(o, objects, count) && !fetchedByOther(self, o, objects, count)) { best_d = d; best = o; } } return best; } static bool holds(const Object *self, const Object *it) { for (int h = 0; h < 2; h++) if (self->hands.item[h] == it && (self->hands.state[h] == HAND_HOLDING || self->hands.state[h] == HAND_REACHING)) return true; return false; } /* Bringing material over to the stage: true while at it. */ static bool fetchMaterial(Object *self, Object *stage, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; Object *it = ai->fetch; if (!it) return false; bool mine = holds(self, it); if (it->broken || (it->is_held && !mine) || (ai->fetch_time += dt) > FETCH_GIVE_UP) { for (int h = 0; h < 2; h++) if (mine && self->hands.item[h] == it) Hands_drop(self, h); ai->fetch = NULL; return false; } if (mine) { for (int h = 0; h < 2; h++) if (self->hands.item[h] == it && self->hands.state[h] == HAND_REACHING) { stand(self); return true; } Vec3 front = yaw_front(stage->rotation.y); Vec3 drop = vec3_add(Stage_workSpot(stage, ai->side < 0.0f ? -0.3f : 0.3f), vec3_scale(front, 1.3f)); /* (out in front, behind those at work) */ if (!ai_goToPoint(self, drop, 0.5f, objects, count, dt)) return true; for (int h = 0; h < 2; h++) if (self->hands.item[h] == it) Hands_drop(self, h); ai->fetch = NULL; if (ai->speech_time <= 0.0f) Ai_say(self, it->variant == MATERIAL_BOARDS ? "Boards are here!" : it->variant == MATERIAL_PAINT ? "Paint's here!" : "Got it - here."); return true; } if (Hands_gapFrom(self->position, self->look.height, it) < PICKUP_REACH) { stand(self); turnTowards(self, vec3_sub(it->position, self->position), dt); freeHands(self, NULL); if (!Hands_pickUpObject(self, self->main_hand, it) && !Hands_pickUpObject(self, 1 - self->main_hand, it)) ai->fetch = NULL; return true; } ai_goToPoint(self, it->position, 0.3f, objects, count, dt); return true; } /* Off somewhere on purpose: a shopping run (over to the car, the driver's door: game_shop.c gets them in), showing somebody the way (waiting for them when they fall behind: game_arrival.c). */ #define GUIDE_WAIT 6.0f /* m: the one shown the way further behind than this: wait for them */ bool ai_errand(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; if (!ai->errand) return false; if (self->sitting) self->sitting = false; /* (up first) */ self->indoors = 0.0f; const Object *w = ai->errand_with; if (w && !ai->errand_there && flatDistance(w->position, self->position) > GUIDE_WAIT) { /* (they're coming? waiting, looking back) */ stand(self); turnTowards(self, vec3_sub(w->position, self->position), dt); return true; } ai->errand_there = ai_goToPoint(self, ai->errand_to, w ? 1.0f : 0.6f, objects, count, dt); if (ai->errand_there) { stand(self); if (w) turnTowards(self, vec3_sub(w->position, self->position), dt); } return true; } static const char *doneLine(int project, int step) { static const char *const ANY[] = { "", "Ground's clear - now the frame.", "The frame stands!", "Deck's in - try it, it holds.", "Roof's on. Now make it pretty.", "Done! Look at it!" }; if (project == PROJECT_OBSTWIESE && step == BUILD_DONE) return "Done - the trees will glow on this one. Psy heaven!"; if (project == PROJECT_HIDDEN && step == BUILD_ROOF) return "Back wall's up - nobody will see it from the yard. Perfect."; return ANY[step < 0 ? 0 : step > BUILD_DONE ? BUILD_DONE : step]; } bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; if (!g_world || self->cast_id < 0) return false; const float gdt = g_world->game_dt; int project = theirProject(self); bool floor = floorProject(project); Object *stage = floor ? Stage_of(objects, count, (Project)project) : NULL; self->carving = false; self->mowing = 0.0f; ai->project_off = fmaxf(0.0f, ai->project_off - gdt); if (!project || (floor && (!stage || stage->build_stage >= BUILD_DONE)) || (!floor && ai_jobDone(project) && self->snails == 0)) return stop(self, NULL); /* (snails still in the bucket: let go first) */ ai->remark = fmaxf(0.0f, ai->remark - dt); bool lead = self->leads == project; Object *boss = leadOf(objects, count, project); bool boss_near = boss && flatDistance(boss->position, stage ? stage->position : self->position) < LEAD_NEAR; float hour = g_world->hour; /* not now */ if (hour < WORK_FROM || hour > WORK_TO) return stop(self, lead ? "That's it for today." : NULL); if (g_world->fest.stage >= FEST_OPEN) return stop(self, NULL); /* (the festival's on: what's built is built) */ if (self->needs.stamina < TIRED) return stop(self, "I need a break."); if (self->needs.hunger > NEED_STOP || self->needs.thirst > NEED_STOP || self->needs.bladder > NEED_STOP) return stop(self, NULL); if (self->drunk > 0.5f || self->body.asleep || self->in_tent || self->riding || self->indoors > 0.0f || ai->sheltering || ai->errand) return stop(self, NULL); if (!ai->on_project) { if (ai->project_off > 0.0f || ai->greet || ai->unload) return false; float chance = START_CHANCE * (lead || boss_near ? 1.0f : 0.25f) * (0.5f + self->commitment); int focus = g_world->focus; /* (what the Herberge said this weekend is for) */ if ((focus == FOCUS_FLOORS && floor) || (focus == FOCUS_SIDE && !floor)) chance *= 1.6f; else if (focus == FOCUS_PARTY) chance *= 0.5f; if (random01(ai) >= chance * dt) return false; ai->on_project = true; ai->shift = 0.0f; if (lead && ai->speech_time <= 0.0f) { char line[AI_SPEECH_MAX]; if (floor) snprintf(line, sizeof line, "Off to the %s - %s. Who's coming?", Cast_projectName((Project)project), Stage_stepName(stage->build_stage)); else snprintf(line, sizeof line, "%s", ai_jobStart(project)); Ai_say(self, line); } } if ((ai->shift += gdt) > SHIFT) return stop(self, lead ? "Break! Back in a bit." : NULL); if (self->sitting) self->sitting = false; /* (up first) */ if (!floor) { /* a side job */ char why[AI_SPEECH_MAX] = ""; int got = ai_job(self, project, objects, count, dt, why, sizeof why); if (got == SIDE_HELD) { ai->held_up += gdt; if (ai->remark <= 0.0f && firstAt(self, objects, count, project) && why[0]) { Ai_say(self, why); ai->remark = REMARK_EVERY; } if (ai->held_up > HELD_UP_LEAVE) return stop(self, "Can't get on with it like this. Later."); return true; } ai->held_up = 0.0f; if (got == SIDE_STEP) { self->commitment = fminf(1.0f, self->commitment + LIFT_DONE * 0.5f); if (why[0]) Ai_say(self, why); } if (got == SIDE_FINISHED) { for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o->is_character && theirProject(o) == project) o->commitment = fminf(1.0f, o->commitment + LIFT_DONE); } return stop(self, why[0] ? why : NULL); } return true; } if (fetchMaterial(self, stage, objects, count, dt)) return true; freeHands(self, NULL); float side = ai->side != 0.0f ? ai->side * 0.8f : (lead ? 0.0f : 0.6f); Vec3 spot = Stage_workSpot(stage, side); if (!ai_goToPoint(self, spot, 0.4f, objects, count, dt)) return true; Vec3 hands = Stage_handsAt(stage, spot, self->body.clock); turnTowards(self, vec3_sub(stage->position, self->position), dt); Hands_touch(self, hands); /* the work: quicker the handier they are, and with the lead about */ float handy = 0.5f * (self->skill[ORDER_FIREWOOD] + self->skill[ORDER_TREES]); float rate = (0.6f + 0.8f * handy) * self->tempo * (lead ? 1.1f : boss_near ? 1.0f : 0.6f) * (1.0f - 0.5f * fminf(self->drunk, 1.0f)); int got = Stage_work(g_world, stage, rate * gdt / 3600.0f); if (got == -2) { /* held up: material missing */ int kind = Stage_missingMaterial(g_world, stage); Object *there = kind >= 0 ? materialFor(self, objects, count, kind) : NULL; if (there && (!lead || !othersAt(self, objects, count, project))) { /* (lying somewhere else: fetched) */ ai->fetch = there; ai->fetch_time = 0.0f; if (ai->speech_time <= 0.0f) Ai_say(self, kind == MATERIAL_BOARDS ? "I'll get the boards." : "I'll fetch it."); return true; } if (kind >= 0) { /* (somebody's on the way with it: wait for it) */ for (int i = 0; i < count; i++) if (objects[i]->is_character && objects[i]->ai.fetch && objects[i]->ai.fetch->variant == kind && theirProject(objects[i]) == project) return true; } ai->held_up += gdt; if (ai->remark <= 0.0f && firstAt(self, objects, count, project)) { char line[AI_SPEECH_MAX], what[40]; Stage_missing(g_world, stage, what, sizeof what); if (stage->build_stage == BUILD_FRAME) snprintf(line, sizeof line, "We need %s here for the frame!", what); else snprintf(line, sizeof line, "We need %s - somebody go shopping!", what); Ai_say(self, line); ai->remark = REMARK_EVERY; } if (ai->held_up > HELD_UP_LEAVE) return stop(self, stage->build_stage == BUILD_FRAME ? "No logs, no frame. I'm off till there are some." : "Nothing to build with. I'm off till the shopping's here."); return true; } ai->held_up = 0.0f; if (got > 0) { /* a step done: everybody at it is lifted */ for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o->is_character && o->ai.on_project && theirProject(o) == project) o->commitment = fminf(1.0f, o->commitment + LIFT_DONE); } if (firstAt(self, objects, count, project)) Ai_say(self, doneLine(project, got)); } return true; }