Entropie git · main
Entropie festival game; first steps
C 88.5% Markdown 9.1% GLSL 2.3%git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gzentropie/src/people/talk.c raw
#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 <stdio.h>
#include <string.h>
#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;
}