#include "people/talk.h" #include "world/camp.h" #include "world/tools.h" #include "people/pockets.h" #include "world/site.h" #include "world/stage.h" #include "world/car.h" #include "world/why.h" #include #include #define CLOSE_TIME 2.4f /* the last reply shows this long before the talk ends */ #define PLAYER_LINE 2.6f #define WALK_AWAY 5.0f /* further apart than this: the talk is over */ #define RECENT_WOOD 60.0f /* seconds the player's last wood work counts as "that" */ #define WOODPILE_SIZE { 1.6f, 0.0f, 0.6f } #define WOODPILE_AHEAD TALK_PILE_AHEAD enum { NODE_END, NODE_OFFER, /* they offer to finish the player's wood work */ NODE_HELLO, /* the player clicked on someone idle */ NODE_TASKS, /* the player wants something done */ NODE_HELPER, /* ... on someone helping */ NODE_WORKER, /* ... on someone working on an order */ NODE_ASK, /* they ask if there's anything to do */ NODE_SUGGEST, /* they suggest a job that needs doing */ NODE_HOW, /* they don't know how: "can you show me?" */ NODE_WHERE, /* they don't know where the firewood goes */ NODE_PLACE, /* being shown the place: "here?" */ NODE_WATCHING, /* being shown how */ NODE_ASK_HAND, /* a worker asks the player to join in */ NODE_TOOL, /* a worker asks for the player's axe */ NODE_FESTIVAL, /* about the festival: their part in it, who leads what */ NODE_SHOP, /* the one with the car keys: what to get at the shops */ NODE_COUNT }; #define TASK_CHOICES "Make firewood", "Cut some trees", "Collect the trash", "Look after the fire" static const char *const CHOICES[NODE_COUNT][TALK_CHOICES] = { [NODE_OFFER] = { "Yes, make firewood of it", "Yes, and cut more trees", "Help me instead", "No, thanks" }, [NODE_HELLO] = { "Could you do something for me?", "Come and help me", "Who are you?", "About the festival...", "Nothing, bye" }, [NODE_TASKS] = { TASK_CHOICES, "Never mind" }, [NODE_HELPER] = { TASK_CHOICES, "That's all, thanks" }, [NODE_WORKER] = { "Keep going", "I'll show you where the firewood goes", "Come and help me instead", "Take a break" }, [NODE_ASK] = { TASK_CHOICES, "No, thanks" }, [NODE_SUGGEST] = { "Good idea, go ahead", "Help me instead", "Not now" }, [NODE_HOW] = { "I'll show you", "Never mind" }, [NODE_WHERE] = { "I'll show you where", "Find a good place for it", "Leave it where it is" }, [NODE_PLACE] = { "Yes, stack it here", "Not yet, keep following", "Forget it" }, [NODE_WATCHING] = { "Watch closely", "Never mind" }, [NODE_ASK_HAND] = { "Sure", "Not now" }, [NODE_TOOL] = { "Here you go", "No, I still need it" }, [NODE_FESTIVAL] = { "Great work - thanks!", "(who leads what)", "(you take it over)", "We need some shopping done", "Bye" }, /* (2 and 3: see Talk_choices) */ [NODE_SHOP] = { "(boards)", "(nails, screws)", "(paint)", "(food)", "(go)" }, /* (see Talk_choices) */ }; static const Object *g_talk_player; /* (for the choices: what's between them) */ static const Talk *g_talk; /* (for what they say: the shopping list) */ static World *g_talk_world; /* (for the choices: who leads what) */ static const Order TASKS[] = { ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE }; static float flatDistance(Vec3 a, Vec3 b) { return sqrtf((a.x - b.x) * (a.x - b.x) + (a.z - b.z) * (a.z - b.z)); } static bool shy(const Object *o) { return o->temperament.pride < -0.3f; } static bool lively(const Object *o) { return o->temperament.energy > 0.5f; } #define PRAISE_AGAIN 3600.0f /* game s: "great work" counts once in this long */ #define PRAISE_LIFT 0.06f #define TAKE_ON 0.45f /* commitment to take on leading something */ static bool floorProject(int p) { return p == PROJECT_HOFBUEHNE || p == PROJECT_OBSTWIESE || p == PROJECT_KARTOFFELACKER || p == PROJECT_HIDDEN; } /* A floor nobody leads (of the people here), or PROJECT_NONE. */ static int openFloor(void) { if (!g_talk_world) return PROJECT_NONE; static const int FLOORS[] = { PROJECT_HOFBUEHNE, PROJECT_OBSTWIESE, PROJECT_KARTOFFELACKER, PROJECT_HIDDEN }; for (int k = 0; k < 4; k++) if (!Cast_leadOf(g_talk_world->objects, g_talk_world->count, (Project)FLOORS[k])) return FLOORS[k]; return PROJECT_NONE; } /* Their part in it, how it's going and how they feel about it. */ static const char *festivalLine(const Object *o) { static char line[AI_SPEECH_MAX]; const char *mood = o->commitment > 0.7f ? "Going great!" : o->commitment > 0.45f ? "It's okay." : o->commitment > 0.3f ? "Honestly, I'm getting fed up." : "I'm about done with it."; if (o->guest) { /* (a guest: how they like it) */ snprintf(line, sizeof line, "%s", o->fun > 0.85f ? "Best festival ever! I'm coming back next year." : o->fun > 0.65f ? "Great music, great people. Love it!" : o->fun > 0.45f ? "It's nice. Could be more going on." : "Honestly? Bit boring so far."); return line; } int p = o->leads != PROJECT_NONE ? o->leads : o->helps; if (p == PROJECT_NONE) { snprintf(line, sizeof line, "Me? Nothing really. %s", o->commitment > 0.45f ? "Could do something, though." : ""); return line; } const Object *s = g_talk_world && floorProject(p) ? Stage_of(g_talk_world->objects, g_talk_world->count, (Project)p) : NULL; if (s && s->build_stage < BUILD_DONE) snprintf(line, sizeof line, "%s the %s: %s, %d%%. %s", o->leads == p ? "I lead" : "I help with", Cast_projectName((Project)p), Stage_stepName(s->build_stage), (int)(Stage_part(s) * 100.0f), mood); else if (s) snprintf(line, sizeof line, "The %s stage is built! %s", Cast_projectName((Project)p), mood); else snprintf(line, sizeof line, "%s the %s. %s", o->leads == p ? "I lead" : "I help with", Cast_projectName((Project)p), mood); return line; } static int shopTotal(const Talk *t) { int sum = 0; for (int k = 0; k < SHOP_ITEMS; k++) sum += t->shop[k] * SHOP_PRICE[k]; return sum; } /* Who has the car keys (here), or NULL. */ static Object *keyHolder(void) { for (int i = 0; g_talk_world && i < g_talk_world->count; i++) { Object *o = g_talk_world->objects[i]; if (o->is_character && o->keys && !strcmp(o->keys, "Car keys")) return o; } return NULL; } static const char *shopLine(void) { static char line[AI_SPEECH_MAX]; int cash = g_talk_world ? (int)g_talk_world->cash : 0, total = g_talk ? shopTotal(g_talk) : 0; if (total == 0) snprintf(line, sizeof line, "Sure. What do we need? There's %d EUR in the cash box.", cash); else snprintf(line, sizeof line, "So far that's %d EUR - %d in the box. Anything else?", total, cash); return line; } /* What they say when the conversation gets to a node. */ static const char *lineFor(const Object *o, int node) { switch (node) { case NODE_OFFER: return shy(o) ? "Um... should I finish that for you?" : "Do you want me to finish that?"; case NODE_HELLO: return shy(o) ? "Oh... hi." : lively(o) ? "Hey! What's up?" : o->temperament.pride > 0.5f ? "Yes?" : "Hi! What's up?"; case NODE_TASKS: return "What do you need?"; case NODE_HELPER: return "What should I do?"; case NODE_WORKER: switch (o->ai.order) { case ORDER_TREES: return "Cutting trees for firewood. All good?"; case ORDER_TRASH: return "Cleaning up. All good?"; case ORDER_FIRE: return "I'm looking after the fire. All good?"; default: return "Making firewood. All good?"; } case NODE_ASK: return shy(o) ? "Um... can I help with anything?" : "Can I help you with something?"; case NODE_SUGGEST: switch (o->ai.suggested) { case ORDER_TREES: return "We're running out of wood. Shall I cut some trees?"; case ORDER_TRASH: return "There's trash everywhere. Shall I clean up a bit?"; case ORDER_FIRE: return "It's getting dark. Shall I get the fire going?"; default: return "We're low on firewood. Shall I make some?"; } case NODE_HOW: return "I've never done that. Can you show me?"; case NODE_WHERE: return "Where should the firewood go?"; case NODE_PLACE: return "Here?"; case NODE_WATCHING: return "I'm watching..."; case NODE_ASK_HAND: return "Want to give me a hand with the wood?"; case NODE_TOOL: return shy(o) ? "Um... could I maybe use the axe?" : "Can I have the axe? I'd like to try myself."; case NODE_FESTIVAL: return festivalLine(o); case NODE_SHOP: return shopLine(); default: return ""; } } /* A job they were called away from and haven't gone back to (ORDER_NONE: none). */ static const char *const JOB_WORD[ORDER_COUNT] = { "", "firewood", "trees", "tidying", "fire" }; static Order pausedJob(const Object *npc) { return npc->ai.mode != AI_WORK ? npc->ai.paused_order : ORDER_NONE; } static void go(Talk *talk, int node) { g_talk = talk; talk->node = node; talk->waited = 0.0f; talk->nudged = false; Ai_say(talk->with, lineFor(talk->with, node)); } /* They answer and the conversation ends. */ static void wrapUp(Talk *talk, const char *reply) { if (reply) Ai_say(talk->with, reply); talk->node = NODE_END; talk->closing = CLOSE_TIME; } /* Where an order's work happens: the wood the player was just working on, else the nearest wood (or, for trees, the nearest tree) around the player. */ static Vec3 workArea(const World *world, const Object *player, Order order) { const Work *w = &player->work_state; if (w->last_target && w->since < RECENT_WOOD) return Object_colliderCenter(w->last_target, 0); const Object *best = NULL; float best_dist = 25.0f; for (int i = 0; i < world->count; i++) { const Object *o = world->objects[i]; bool wood = Tools_splittable(o) || Tools_cuttable(o) || (o->type == OBJ_TREE && o->toppled); if (order == ORDER_TREES) wood = wood || (o->type == OBJ_TREE && !o->toppled); if (!wood || o->is_held) continue; float d = flatDistance(o->position, player->position); if (d < best_dist) { best_dist = d; best = o; } } return best ? Object_colliderCenter(best, 0) : player->position; } /* A woodpile in front of the player (or one that is already there). */ static Object *woodpileHere(World *world, const Object *player) { Vec3 at = vec3_add(player->position, vec3_scale(Object_facing(player), WOODPILE_AHEAD)); for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o->type == OBJ_WOODPILE && flatDistance(o->position, at) < 3.0f) return o; } at.y = Terrain_height(&world->terrain, at.x, at.z); const Vec3 size = WOODPILE_SIZE; Object *pile = Object_create(OBJ_WOODPILE, PALETTE_WARM, at, size, 0.0f); if (!pile) return NULL; Object_setRotation(pile, vec3(0.0f, player->rotation.y, 0.0f)); return World_add(world, pile) ? pile : NULL; } /* Where they'd stack it themselves: a woodpile there already (within PILE_SEEK), else a spot near the work: clear, level, off the roads, by a wall if there's one. */ #define PILE_SEEK 40.0f #define PILE_RING0 3.0f /* m from the work: the ring searched */ #define PILE_RING1 9.0f static float pileSpotScore(World *world, Vec3 p, Vec3 work) { float g = Terrain_height(&world->terrain, p.x, p.z); if (World_groundHeight(world, p.x, p.z, g + 3.0f) > g + 0.05f) return -1.0f; /* (something stands there) */ float slope = fabsf(Terrain_height(&world->terrain, p.x + 0.8f, p.z) - Terrain_height(&world->terrain, p.x - 0.8f, p.z)) + fabsf(Terrain_height(&world->terrain, p.x, p.z + 0.8f) - Terrain_height(&world->terrain, p.x, p.z - 0.8f)); if (slope > 0.25f || Site_road(p.x, p.z) > 0.2f) return -1.0f; float score = 1.0f - slope - 0.03f * flatDistance(p, work); for (int i = 0; i < world->count; i++) { const Object *o = world->objects[i]; if (o->collider_count == 0 || o->is_character || o->type == OBJ_DECAL || o->type == OBJ_PUDDLE || o->type == OBJ_STINK) continue; float d = flatDistance(o->position, p) - Object_reach(o); if (d > 2.0f) continue; if (!o->is_static && d < 0.8f) return -1.0f; /* (loose things lying there) */ if (o->is_static && d < 0.6f) return -1.0f; if (o->is_static && Object_reach(o) > 1.5f) score += 0.5f; /* (a wall to stack it against) */ } return score; } static Object *pileOwnPlace(World *world, const Object *npc) { Vec3 work = npc->ai.work_area; for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o->type == OBJ_WOODPILE && flatDistance(o->position, work) < PILE_SEEK) return o; } Vec3 best = work; float best_score = -1.0f; for (int r = 0; r < 4; r++) for (int k = 0; k < 16; k++) { float rad = PILE_RING0 + (PILE_RING1 - PILE_RING0) * (float)r / 3.0f, a = (float)k * PI_F / 8.0f; Vec3 p = vec3_add(work, vec3(cosf(a) * rad, 0.0f, sinf(a) * rad)); float sc = pileSpotScore(world, p, work); if (sc > best_score) { best_score = sc; best = p; } } if (best_score < 0.0f) return NULL; best.y = Terrain_height(&world->terrain, best.x, best.z); const Vec3 size = WOODPILE_SIZE; Object *pile = Object_create(OBJ_WOODPILE, PALETTE_WARM, best, size, 0.0f); if (!pile) return NULL; Object_setRotation(pile, vec3(0.0f, yaw_along(vec3_sub(best, work)) + PI_F * 0.5f, 0.0f)); /* (lengthwise across the way to it) */ return World_add(world, pile) ? pile : NULL; } /* Kurti and his sons lend you the land - your festival, your work: no help from them. */ static bool ownerSaysNo(Talk *talk, const Object *npc) { if (!npc->lives_here) return false; wrapUp(talk, "Your festival, your work - I'll be at the fire."); return true; } static bool woodJob(Order what) { return what == ORDER_FIREWOOD || what == ORDER_TREES; } /* A job: they may say no. Wood jobs: unless they need to be shown how, or where the firewood goes. their_idea: they asked for something to do. */ static void order(Talk *talk, World *world, const Object *player, Order what, bool their_idea) { Object *npc = talk->with; const char *reply = NULL; if (!Ai_willDo(npc, world->objects, world->count, what, their_idea, &reply)) { wrapUp(talk, reply); return; } if (what == ORDER_TRASH || what == ORDER_FIRE) { Object *fire = Camp_fireNear(world->objects, world->count, player->position, 80.0f, false); Ai_order(npc, what, what == ORDER_FIRE && fire ? fire->position : player->position); wrapUp(talk, reply); return; } if (!npc->ai.knows_wood) { npc->ai.pending = what; go(talk, NODE_HOW); return; } Ai_order(npc, what, workArea(world, player, what)); if (!npc->ai.woodpile && !npc->ai.no_pile) { Ai_say(npc, "Sure. Where should the firewood go?"); talk->node = NODE_WHERE; return; } wrapUp(talk, reply); } void Talk_showHere(World *world, Object *player, Object *npc) { Object *old = npc->ai.woodpile; Object *pile = woodpileHere(world, player); if (!pile) return; /* A new place: everybody stacks there now, and carries the old stack over */ if (old && old != pile) { old->moved_to = pile; for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o->type == OBJ_HUMAN && o->ai.woodpile == old && o != npc) o->ai.woodpile = pile; } } Ai_setWoodpile(npc, pile); npc->ai.no_pile = false; Ai_say(npc, "Got it! I'll stack it here."); } /* The player gives their tool for this job to npc (into a free hand). */ static bool handOver(Object *player, Object *npc, Job job) { int hand = -1; if (!Tools_canDo(Tools_held(player, &hand), job) || hand < 0) return false; Object *tool = player->hands.item[hand]; Hands_drop(player, hand); for (int h = 0; h < 2; h++) if (npc->hands.state[h] == HAND_HOLDING) Hands_drop(npc, h); return Hands_receive(npc, npc->main_hand, tool); } /* The tool a worker's talk is about: the one in their hand, else the chainsaw. */ static int talkTool(const Object *npc) { ToolKind held = Tools_held(npc, NULL); return held == TOOL_AXE || held == TOOL_CHAINSAW ? (int)held : (int)TOOL_CHAINSAW; } /* The lighter between the two: what the player can say about it (instead of "Who are you?"). */ /* ... or a cigarette: asking them for one, offering one. */ enum { LIGHTER_NONE, LIGHTER_MINE_BACK, LIGHTER_RETURN, LIGHTER_BORROW, CIG_ASK, CIG_OFFER }; static int lighterTopic(const Object *player, const Object *npc) { if (npc->lighter && npc->lighter_of == player && !player->lighter) return LIGHTER_MINE_BACK; if (player->lighter && player->lighter_of == npc && !npc->lighter) return LIGHTER_RETURN; if (npc->smoker > 0.0f && Pockets_hasSmoke(player) && (!Pockets_hasSmoke(npc) || npc->craving > 0.6f)) return CIG_OFFER; if (!Pockets_hasSmoke(player) && Pockets_hasSmoke(npc)) return CIG_ASK; if ((!player->lighter || player->lighter_uses <= 0) && npc->lighter && npc->lighter_uses > 0) return LIGHTER_BORROW; return LIGHTER_NONE; } static void talkLighter(Talk *talk, Object *player, int topic) { Object *npc = talk->with; if (topic == LIGHTER_MINE_BACK) { if (npc->drunk > 0.6f && npc->ai.character.willing < 0.5f) wrapUp(talk, "Lighter? What lighter?"); else { Pockets_passLighter(npc, player); wrapUp(talk, shy(npc) ? "Oh! Sorry... here." : "Oh, right - here you go."); } } else if (topic == CIG_OFFER) { Pockets_giveSmoke(player, npc); npc->craving = fmaxf(npc->craving, 0.8f); /* (lit soon) */ npc->ai.annoyed = fmaxf(0.0f, npc->ai.annoyed - 0.3f); wrapUp(talk, shy(npc) ? "Oh... thank you!" : "You're a star. Thanks!"); } else if (topic == CIG_ASK) { int has = npc->cigarettes + npc->tobacco; if (has < 3 || npc->ai.annoyed > 0.5f) wrapUp(talk, has < 3 ? "Sorry, those are my last ones." : "Buy your own."); else { Pockets_giveSmoke(npc, player); wrapUp(talk, npc->cigarettes == 0 ? "Here, I rolled you one." : "Sure, have one."); } } else if (topic == LIGHTER_RETURN) { Pockets_passLighter(player, npc); wrapUp(talk, "Thanks!"); } else if (npc->ai.annoyed > 0.6f) { wrapUp(talk, "Get your own."); } else { if (player->lighter) { player->lighter = false; player->lighter_of = NULL; } /* (the empty one: away with it) */ Pockets_passLighter(npc, player); wrapUp(talk, npc->lighter_uses < 5 ? "Here - it's nearly empty though." : "Sure. I want it back though!"); } } static void festivalAnswer(Talk *talk, Object *player, int choice) { Object *npc = talk->with; char reply[AI_SPEECH_MAX]; if (choice == 1) { /* praise */ if (npc->ai.praised > 0.0f) { wrapUp(talk, lively(npc) ? "Ha, you said that already!" : "Thanks."); return; } npc->commitment = fminf(1.0f, npc->commitment + PRAISE_LIFT); npc->ai.praised = PRAISE_AGAIN; wrapUp(talk, shy(npc) ? "Oh... thanks. That helps." : "Thanks! Good to hear that."); } else if (choice == 2) { /* can you lead the open floor? */ int open = openFloor(); if (open == PROJECT_NONE) { wrapUp(talk, "Every floor has somebody leading it. Good, isn't it?"); return; } if (npc->leads != PROJECT_NONE) { snprintf(reply, sizeof reply, "I've got the %s already.", Cast_projectName((Project)npc->leads)); wrapUp(talk, reply); return; } if (npc->lives_here || npc->commitment < TAKE_ON || npc->ai.character.willing < 0.4f) { wrapUp(talk, shy(npc) ? "Um... not me, sorry." : "Not me - I'm not up for that."); return; } npc->leads = open; if (npc->helps == open) npc->helps = PROJECT_NONE; npc->commitment = fminf(1.0f, npc->commitment + 0.05f); snprintf(reply, sizeof reply, "Alright - I'll lead the %s!", Cast_projectName((Project)open)); wrapUp(talk, reply); } else if (choice == 3) { if (npc->leads != PROJECT_NONE) { /* you take over theirs */ int p = npc->leads; if (npc->temperament.pride > 0.4f && npc->commitment > 0.5f) { wrapUp(talk, "No way - that one's mine."); return; } player->leads = p; npc->leads = PROJECT_NONE; npc->helps = p; npc->commitment = clampf(npc->commitment + (npc->commitment > 0.5f ? -0.1f : 0.08f), 0.0f, 1.0f); /* (proud of it: it stings; fed up: a relief) */ snprintf(reply, sizeof reply, npc->commitment > 0.45f ? "Okay... the %s is yours. I'll help." : "Thank god. The %s is yours.", Cast_projectName((Project)p)); wrapUp(talk, reply); } else if (floorProject(player->leads)) { /* come and help with yours */ if (npc->commitment < 0.2f || npc->lives_here) { wrapUp(talk, "Nah, not today."); return; } npc->helps = player->leads; snprintf(reply, sizeof reply, "Sure, I'll help with the %s.", Cast_projectName((Project)player->leads)); wrapUp(talk, reply); } else wrapUp(talk, "Lead a floor yourself first - then I'll help you with it."); } else if (choice == 4) { /* the shopping: whoever has the car keys */ Object *keys = keyHolder(); Object *car = NULL; for (int i = 0; g_talk_world && i < g_talk_world->count; i++) if (g_talk_world->objects[i]->type == OBJ_CAR) car = g_talk_world->objects[i]; if (keys != npc) { if (keys) snprintf(reply, sizeof reply, "%s has the car keys - ask %s.", keys->name, keys->female ? "her" : "him"); else snprintf(reply, sizeof reply, "Nobody here has a car right now."); wrapUp(talk, reply); } else if (!car || (g_talk_world && g_talk_world->shopping)) wrapUp(talk, "The car's not here - wait till it's back."); else if (car->load && car->load->n > 0) wrapUp(talk, "Let's get the car empty first."); else if (npc->drunk > 0.3f) wrapUp(talk, "I've had a few - I'm not driving now."); else if (npc->commitment < 0.2f || npc->needs.stamina < 0.15f) wrapUp(talk, "Not today. I'm done in."); else { memset(talk->shop, 0, sizeof talk->shop); go(talk, NODE_SHOP); } } else if (g_talk_world && Why_open(g_talk_world) >= 0) { /* why do festivals ...? */ int k = Why_open(g_talk_world); if (Why_knows(npc, k)) { g_talk_world->why[k] = WHY_UNDERSTOOD; npc->commitment = fminf(1.0f, npc->commitment + 0.03f); /* (asked: they like that) */ wrapUp(talk, Why_get(k)->answer); } else { snprintf(reply, sizeof reply, "No idea. Ask %s - or %s.", Why_get(k)->knowers[0], Why_get(k)->knowers[2]); wrapUp(talk, reply); } } else wrapUp(talk, "See you."); } static void shopAnswer(Talk *talk, int choice) { if (choice >= 1 && choice <= SHOP_ITEMS) { /* one more of it (past the most: none) */ int k = choice - 1; talk->shop[k] = (talk->shop[k] + 1) % (SHOP_MAX[k] + 1); if (g_talk_world && shopTotal(talk) > (int)g_talk_world->cash) { talk->shop[k] = 0; Ai_say(talk->with, "That's more than we've got in the box."); talk->node = NODE_SHOP; return; } go(talk, NODE_SHOP); } else if (shopTotal(talk) > 0) { talk->errand = talk->with; wrapUp(talk, "Right - I'm off to the hardware store. Back in an hour or so."); } else wrapUp(talk, "Okay - another time then."); } static void answer(Talk *talk, World *world, Object *player, int choice) { Object *npc = talk->with; switch (talk->node) { case NODE_HELLO: if (choice == 1 && pausedJob(npc)) { Order job = pausedJob(npc); Ai_resumePaused(npc); char reply[64]; snprintf(reply, sizeof reply, shy(npc) ? "Oh... sorry. The %s." : "Oh right - the %s!", JOB_WORD[job]); wrapUp(talk, reply); } else if (choice == 1) go(talk, NODE_TASKS); else if (choice == 2) { if (npc->ai.character.willing < 0.3f && npc->needs.stamina < 0.5f) wrapUp(talk, "Maybe later, I'm beat."); else { if (!ownerSaysNo(talk, npc)) { Ai_help(npc, player); wrapUp(talk, shy(npc) ? "Okay..." : "Sure, I'm with you."); } } } else if (choice == 3 && lighterTopic(player, npc) != LIGHTER_NONE) { talkLighter(talk, player, lighterTopic(player, npc)); } else if (choice == 3) { Ai_say(npc, npc->ai.character.about ? npc->ai.character.about : npc->guest ? "Here for the festival! A friend told me about it." : "Me? Nobody special."); } else if (choice == 4) go(talk, NODE_FESTIVAL); else wrapUp(talk, "See you."); break; case NODE_TASKS: case NODE_HELPER: case NODE_ASK: if (choice >= 1 && choice <= 4) { if (talk->node == NODE_HELPER) Ai_stopHelping(npc); order(talk, world, player, TASKS[choice - 1], talk->node == NODE_ASK); } else if (talk->node == NODE_HELPER) { Ai_stopHelping(npc); wrapUp(talk, "Anytime!"); } else wrapUp(talk, talk->node == NODE_ASK ? "Alright." : "Okay."); break; case NODE_OFFER: if (choice == 1) order(talk, world, player, ORDER_FIREWOOD, true); else if (choice == 2) order(talk, world, player, ORDER_TREES, true); else if (choice == 3) { if (!ownerSaysNo(talk, npc)) { Ai_help(npc, player); wrapUp(talk, "Sure, I'm with you."); } } else wrapUp(talk, "Alright."); break; case NODE_SUGGEST: if (choice == 1) order(talk, world, player, npc->ai.suggested != ORDER_NONE ? npc->ai.suggested : ORDER_FIREWOOD, true); else if (choice == 2) { if (!ownerSaysNo(talk, npc)) { Ai_help(npc, player); wrapUp(talk, "Sure, I'm with you."); } } else wrapUp(talk, "Maybe later then."); break; case NODE_WORKER: if (choice == 1) wrapUp(talk, "Will do."); else if (choice == 2 && woodJob(npc->ai.order)) { Ai_show(npc, SHOW_PLACE, player); wrapUp(talk, "Lead the way."); } else if (choice == 2) { Ai_stopWork(npc); go(talk, NODE_TASKS); } else if (choice == 3) { Ai_stopWork(npc); if (!ownerSaysNo(talk, npc)) { Ai_help(npc, player); wrapUp(talk, "Sure!"); } } else if (choice == 5) { int tool = talkTool(npc); if (!(npc->ai.banned & (1u << tool))) { bool obeys = Ai_banTool(npc, tool, true); wrapUp(talk, obeys ? (shy(npc) ? "Oh... okay, sorry." : "Okay, I'll leave it.") : npc->temperament.pride > 0.3f ? "Pfft. I know what I'm doing." : "Yeah, yeah..."); } else { Ai_banTool(npc, tool, false); wrapUp(talk, "Alright!"); } } else { Ai_stopWork(npc); wrapUp(talk, "Thanks, I could use one."); } break; case NODE_HOW: if (choice == 1) { Ai_show(npc, SHOW_HOW, player); wrapUp(talk, "Okay, I'll watch you."); } else { npc->ai.pending = ORDER_NONE; wrapUp(talk, "Okay."); } break; case NODE_WHERE: if (choice == 1) { Ai_show(npc, SHOW_PLACE, player); wrapUp(talk, "Lead the way. Click where it should go."); } else if (choice == 2) { /* they think it over, then pick a place */ Object *pile = pileOwnPlace(world, npc); Body_think(&npc->body, 1.0f + npc->ai.character.ponder); if (pile) { Ai_setWoodpile(npc, pile); wrapUp(talk, pile->stored ? "There's a pile already - I'll put it there." : "Hm... I'll stack it over there."); } else { npc->ai.no_pile = true; wrapUp(talk, "Hm, no good spot here. I'll leave it lying."); } } else { npc->ai.no_pile = true; wrapUp(talk, "Okay, I'll leave it there."); } break; case NODE_PLACE: if (choice == 1) { Talk_showHere(world, player, npc); wrapUp(talk, NULL); } else if (choice == 2) wrapUp(talk, "Okay."); else { Ai_stopShowing(npc); wrapUp(talk, "Okay."); } break; case NODE_WATCHING: if (choice == 1) wrapUp(talk, "Okay!"); else { Ai_stopShowing(npc); wrapUp(talk, "Okay."); } break; case NODE_ASK_HAND: wrapUp(talk, choice == 1 ? "Great! I'll cut, you split." : "No problem."); break; case NODE_FESTIVAL: festivalAnswer(talk, player, choice); break; case NODE_SHOP: shopAnswer(talk, choice); break; case NODE_TOOL: if (choice == 1 && handOver(player, npc, JOB_SPLIT)) wrapUp(talk, "Thanks! I'll be careful."); else { npc->ai.tool_waiting = true; wrapUp(talk, "Okay, I'll wait till you're done."); } break; default: break; } } static int nodeFor(const Object *npc, Topic topic) { switch (topic) { case TOPIC_OFFER: return NODE_OFFER; case TOPIC_ASK: return NODE_ASK; case TOPIC_SUGGEST: return NODE_SUGGEST; case TOPIC_WHERE: return NODE_WHERE; case TOPIC_ASK_HAND: return NODE_ASK_HAND; case TOPIC_TOOL: return NODE_TOOL; case TOPIC_NONE: break; } const Ai *ai = &npc->ai; if (ai->mode == AI_FOLLOW) return ai->showing == SHOW_PLACE ? NODE_PLACE : NODE_WATCHING; if (ai->mode == AI_WORK) return NODE_WORKER; if (ai->mode == AI_HELP) return NODE_HELPER; return NODE_HELLO; } void Talk_begin(Talk *talk, World *world, Object *player, Object *with, Topic topic) { g_talk_world = world; g_talk_player = player; if (talk->with) Ai_setTalking(talk->with, false); memset(talk, 0, sizeof(*talk)); talk->with = with; Ai_setTalking(with, true); go(talk, nodeFor(with, topic)); } static void end(Talk *talk) { if (talk->with) Ai_setTalking(talk->with, false); talk->with = NULL; talk->node = NODE_END; } /* How long someone waits for an answer: the lively and the proud not long, the shy and the slow a good while. */ static float patience(const Object *o) { const Mood *m = &o->temperament; return clampf(12.0f * (1.0f - 0.35f * m->energy) * (1.0f - 0.3f * m->pride), 5.0f, 25.0f); } static const char *nudgeLine(const Object *o) { const Mood *m = &o->temperament; if (m->pride < -0.4f) return "Um...?"; if (m->pride > 0.5f) return "So?"; if (m->energy > 0.5f) return "Hello? Anybody home?"; return "Hello?"; } static const char *giveUpLine(const Object *o) { const Mood *m = &o->temperament; if (m->pride < -0.4f) return "Sorry... I'll leave you alone."; if (m->pride > 0.5f) return "Forget it."; if (m->energy > 0.5f) return "Okay, whatever!"; if (m->energy < -0.4f) return "...never mind."; return "Never mind then."; } void Talk_update(Talk *talk, World *world, Object *player, int choice, bool leave, float dt) { talk->player_time = fmaxf(0.0f, talk->player_time - dt); if (!talk->with) return; Object *npc = talk->with; if (talk->node == NODE_END) { if ((talk->closing -= dt) <= 0.0f) end(talk); return; } bool apart = flatDistance(npc->position, player->position) > WALK_AWAY; if (leave || apart || Body_isDown(&npc->body) || Body_isDown(&player->body)) { Ai_say(npc, apart ? "Oh... okay then." : "Okay."); if (npc->ai.mode == AI_APPROACH) npc->ai.talk_cooldown = 90.0f; end(talk); return; } const char *choices[TALK_CHOICES]; int n = Talk_choices(talk, choices); if (n > 0 && (choice < 1 || choice > n)) { /* no answer: they don't wait forever */ talk->waited += dt; float wait = patience(npc); if (!talk->nudged && talk->waited > 0.55f * wait) { Ai_say(npc, nudgeLine(npc)); talk->nudged = true; } if (talk->waited > wait) { Ai_say(npc, giveUpLine(npc)); npc->ai.annoyed = fminf(1.0f, npc->ai.annoyed + 0.4f); npc->ai.talk_cooldown = fmaxf(npc->ai.talk_cooldown, 120.0f); end(talk); } return; } if (choice < 1 || choice > n) return; snprintf(talk->player_line, sizeof(talk->player_line), "%s", choices[choice - 1]); talk->player_time = PLAYER_LINE; answer(talk, world, player, choice); } bool Talk_active(const Talk *talk) { return talk->with != NULL; } int Talk_choices(const Talk *talk, const char *out[TALK_CHOICES]) { if (!talk->with || talk->node <= NODE_END || talk->node >= NODE_COUNT) return 0; int n = 0; for (int k = 0; k < TALK_CHOICES; k++) if (CHOICES[talk->node][k]) out[n++] = CHOICES[talk->node][k]; if (talk->node == NODE_WORKER && !woodJob(talk->with->ai.order)) out[1] = "Do something else instead"; if (talk->node == NODE_HELLO && pausedJob(talk->with)) { /* called away from a job, and forgot it: a reminder */ static char line[64]; snprintf(line, sizeof line, "Back to the %s, please", JOB_WORD[pausedJob(talk->with)]); out[0] = line; } if (talk->node == NODE_HELLO && g_talk_player) { static const char *const LIGHTER_LINE[] = { NULL, "Can I have my lighter back?", "Here's your lighter back", "Can I borrow your lighter?", "Can I have a cigarette?", "Want a cigarette?" }; int topic = lighterTopic(g_talk_player, talk->with); if (topic != LIGHTER_NONE) out[2] = LIGHTER_LINE[topic]; } if (talk->node == NODE_FESTIVAL) { static char lead_line[64], take_line[64]; const Object *npc = talk->with; int open = openFloor(); if (open != PROJECT_NONE) snprintf(lead_line, sizeof lead_line, "Can you lead the %s?", Cast_projectName((Project)open)); else snprintf(lead_line, sizeof lead_line, "Who leads what?"); if (npc->leads != PROJECT_NONE) snprintf(take_line, sizeof take_line, "I'll take over the %s", Cast_projectName((Project)npc->leads)); else if (g_talk_player && floorProject(g_talk_player->leads)) snprintf(take_line, sizeof take_line, "Come and help me with the %s", Cast_projectName((Project)g_talk_player->leads)); else snprintf(take_line, sizeof take_line, "Help me with something"); out[1] = lead_line; out[2] = take_line; if (g_talk_world && Why_open(g_talk_world) >= 0) out[4] = Why_get(Why_open(g_talk_world))->ask; /* (a why question come up: ask it) */ } if (talk->node == NODE_SHOP) { static char lines[TALK_CHOICES][64]; static const char *const WHAT[SHOP_ITEMS] = { "Boards, a bundle of 8", "Nails and screws, a box each", "Paint, a bucket", "Food and drinks for the crew" }; for (int k = 0; k < SHOP_ITEMS; k++) { if (talk->shop[k]) snprintf(lines[k], sizeof lines[k], "%s - %d EUR [%d]", WHAT[k], SHOP_PRICE[k], talk->shop[k]); else snprintf(lines[k], sizeof lines[k], "%s - %d EUR", WHAT[k], SHOP_PRICE[k]); out[k] = lines[k]; } if (shopTotal(talk) > 0) snprintf(lines[4], sizeof lines[4], "That's it - off you go (%d EUR)", shopTotal(talk)); else snprintf(lines[4], sizeof lines[4], "Never mind"); out[4] = lines[4]; n = TALK_CHOICES; } if (talk->node == NODE_WORKER && woodJob(talk->with->ai.order) && n < TALK_CHOICES) { /* about the tool they use */ static char line[64]; const Object *npc = talk->with; int tool = talkTool(npc); snprintf(line, sizeof line, npc->ai.banned & (1u << tool) ? "You can use the %s again" : "Leave the %s alone", tool == TOOL_CHAINSAW ? "chainsaw" : "axe"); out[n++] = line; } return n; }