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 2.40.0: the floors get built
- Four stages marked out: Hofbuehne (yard), Obstwiese (orchard), Kartoffelacker (the field behind the tents), the hidden floor (behind the main building) - Built step by step: ground cleared, frame (needs three logs lying by it), deck, back wall and roof, paint and banners in the floor's colour - Leads and helpers go and build in working hours; you build too (hold left click at a stage) - Commitment: lifted by progress and praise, worn down by being held up, working worn out, nobody leading, annoyance; too low, leads quit - Talk about the festival (X, then 4): praise, ask someone to lead an open floor, take over a lead, get help with yours - Late arrivals say hello standing by the fire (no more walking into it)
commit_message.txt | 23 +++-- entropie/ARCHITECTURE.md | 2 +- entropie/Makefile | 2 +- entropie/README.md | 7 +- entropie/STATE.md | 32 ++++++- entropie/commit_message.txt | 23 +++-- entropie/src/game/game.h | 1 + entropie/src/game/game_actions.c | 48 +++++++++- entropie/src/game/game_dev.c | 4 +- entropie/src/game/game_story.c | 10 +- entropie/src/game/save.c | 2 +- entropie/src/people/ai.c | 11 +++ entropie/src/people/ai.h | 6 ++ entropie/src/people/ai_internal.h | 6 ++ entropie/src/people/ai_project.c | 181 +++++++++++++++++++++++++++++++++++++ entropie/src/people/talk.c | 100 ++++++++++++++++++-- entropie/src/render/shapes.h | 5 + entropie/src/render/shapes_props.c | 64 +++++++++++++ entropie/src/world/object.c | 17 ++++ entropie/src/world/object.h | 3 + entropie/src/world/site.c | 23 +++++ entropie/src/world/site.h | 2 + entropie/src/world/stage.c | 118 ++++++++++++++++++++++++ entropie/src/world/stage.h | 37 ++++++++ entropie/src/world/world.c | 2 + 25 files changed, 693 insertions(+), 36 deletions(-) diff --git a/commit_message.txt b/commit_message.txt index 4664bdf..9d9cf74 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1,11 +1,14 @@ -entropie 2.39.0: the story starts - the crew +entropie 2.40.0: the floors get built -- The plan for the game in STATE.md: from the first meeting in Kreuzberg - to the drive home after the clean-up, the four floors, the crew, the - roadmap -- Everybody is one entry of a cast table (world/cast.c) with their part in - the festival: role, the project they lead or help with, commitment -- The crew turns up over the first day on the ground: Kilian and Julia, - Stella and Doro, Kerry, Rouven and Pascal; over to the fire to say hello -- F2 (dev): the crew overview - who's who, what everybody does now, the - projects with their leads and helpers +- Four stages marked out: Hofbuehne (yard), Obstwiese (orchard), + Kartoffelacker (the field behind the tents), the hidden floor (behind + the main building) +- Built step by step: ground cleared, frame (needs three logs lying by + it), deck, back wall and roof, paint and banners in the floor's colour +- Leads and helpers go and build in working hours; you build too (hold + left click at a stage) +- Commitment: lifted by progress and praise, worn down by being held up, + working worn out, nobody leading, annoyance; too low, leads quit +- Talk about the festival (X, then 4): praise, ask someone to lead an open + floor, take over a lead, get help with yours +- Late arrivals say hello standing by the fire (no more walking into it) diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md index 3167797..ef67c5e 100644 --- a/entropie/ARCHITECTURE.md +++ b/entropie/ARCHITECTURE.md @@ -10,7 +10,7 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL | `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input, sound out (`audio.c`: a synthesizer and mixer, no sound files) | core | | `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math | | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render | -| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), everybody in the story (`cast.c`), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game | +| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), everybody in the story (`cast.c`), the floors' stages (`stage.c`), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game | | `src/people/` | bodies (animation), hands, AI, talk, condition, dog, climbing | everything but game | | `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, the story (`game_story.c`: the crew turning up, the crew overview), saves (`save.c`), what you hear (`sound.c`) | all | diff --git a/entropie/Makefile b/entropie/Makefile index ebf5bd3..e69e7f1 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 2.39.0 +VERSION := 2.40.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index d32963a..51cf93d 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -19,6 +19,11 @@ people living there. his sons, unload the car, pitch the tents - **People:** everyone has their own temperament, build, skills, walk and habits. They eat, drink, smoke, pee, sleep, work, chat and say no +- **The festival:** the crew turns up over the first day. Four floors get + built step by step (clearing, frame from logs, deck, roof, paint) by their + lead and helpers - or by you (hold left click at a stage). Talk to people + about it (X, then 4): praise them, ask someone to lead, take a lead over. + Fed up, people give up leading or helping - **Moving:** real steps, stepping over and onto things, jumping, climbing (several ways up), ladders, stumbles and falls - **Work:** fell trees, cut and split firewood, stack it, clean up, carry @@ -125,5 +130,5 @@ Dependencies point downward: `main → game → world/people → render → core ## Version -`make version` prints e.g. `entropie 2.39.0 (build 2e7d256)`. The build id is +`make version` prints e.g. `entropie 2.40.0 (build 43375ee)`. The build id is an md5 of all sources and assets. diff --git a/entropie/STATE.md b/entropie/STATE.md index 61fd6c2..14e2f44 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -373,6 +373,23 @@ Painted in the rotoscope style of *Waking Life* (2001): - The crew turns up over the first day on the ground (Kilian and Julia, then Stella and Doro, the next morning Kerry, then Rouven and Pascal): in from the gravel road, over to the fire to say hello, then on with the day +- The four floors' stages stand marked out from the start: the Hofbuehne + in the yard (its back to the barn), the Obstwiese at the far end of the + orchard, the Kartoffelacker on the field behind the tents' meadow, the + hidden floor behind the main building. Each is built in steps - the + ground cleared, the frame (three logs lying by it), the deck (stand on + it), the back wall and the roof, paint and banners in the floor's colour. + The lead and the helpers go to it in working hours (8:30-19:30) for goes + of two hours; you build too: a hand free, hold left click at it +- Talk to anybody about the festival (X, then 4): how their part goes and + how they feel about it; "Great work - thanks!" (once an hour) lifts them; + ask them to lead a floor nobody leads (if they're up for it); take over + what they lead (the proud may refuse), or have them help with yours +- How into it everybody is goes with what happens: a step finished lifts + those at it; no logs for the frame, working worn out, nobody leading, + being annoyed wear it down; evenings at the fire bring some back. Too + low, a lead gives it up ("somebody else can lead it") and stays to help; + lower still, they're just there for the party - F2 (dev): the crew overview - who's who and what they're there for, what each is doing now (into it / annoyed / energy), the projects with who leads and who helps (Left / Right) @@ -1329,11 +1346,20 @@ Round 2.39 (the story starts: the crew): - F2 crew overview (`Game_crewLines`, three pages; `main.c` drawCrew, labels 120-139). Checked: all seven turned up at 2 / 5 / 20 / 26 h, at the fire, then about; saved and loaded with all 14 there, no doubles. +Round 2.40 (projects and leads: the floors get built): +- `world/stage.c` (OBJ_STAGE, one per floor, made in `Site_finish` at `FLOORS`: Hofbuehne court(36, 5), Obstwiese (42, 38), Kartoffelacker (30, -95), hidden floor (-20, -2); fronts `yaw_front`): `build_stage` BUILD_MARKED .. BUILD_DONE, `build_work` person-hours into the step (NEED 1.5 / 4 / 4 / 3 / 2.5); the frame needs STAGE_LOGS 3 logs within STAGE_LOG_REACH 6 m (used up when it's done; `Stage_work` -2 meanwhile). `Shapes_stage` (stakes and string; posts and beams; the deck in boards; back wall and roof; the floor's colour painted and banners - each growing in 8 steps); the deck (STAGE_DECK 0.5 m) solid from the roof step on. Saved (`build_stage`, `build_work`). +- `people/ai_project.c`: `ai_projectWork` (idle or wandering): the lead and the helpers of a floor go to its stage in working hours, START_CHANCE 0.03/s (helpers a quarter without the lead near), goes of SHIFT 2 game h, breaks BREAK_TIME 40 min; not tired, hungry, thirsty, drunk, sheltering; at a spot along its front, hands hammering (`Stage_handsAt`); rate (0.6 + 0.8 x handy) x tempo x (lead 1.1, lead near 1.0, else 0.6). Held up for logs: one voice says what's missing every 90 s; after 25 game min they leave it. `ai_commitment` (every frame, cast members): see "Playing it"; QUIT_LEAD 0.3, QUIT_HELP 0.15. Late arrivals say hello standing by the fire (greetHosts: they walked through the fireplace to a full bench, scraping their shins). +- Talk: NODE_FESTIVAL (HELLO answer 4; "The firewood goes somewhere else now" is only in the worker's answers now): their line (lead / help, step and %, mood by commitment), praise (PRAISE_LIFT 0.06, once per game hour), lead the open floor (not those living here, TAKE_ON 0.45 and willing 0.4), take over / help with yours. You build: CLICK_STAGE (`stageAt`: within 1.3 m of its edge, a hand free, nothing to pick up in front). +- F2 projects page: each floor's step and % ("needs logs"); F1: commitment. Checked (60x time, fed): Kilian cleared the Obstwiese in 1.5 game h, the frame held up till three logs lay there, then on; the Hofbuehne's helpers cleared it without a lead and gave up on the frame ("No logs, no frame"); talks: praise +0.06, Julia took the Hofbuehne, you took Pascal's light, Pascal took the Kartoffelacker. +- The "## Open tasks" heading lost in 2.39 is back. + +## Open tasks + Most important first. **The story** (see "The story (plan)": the roadmap) -- Next: projects and leads (the floors and side jobs as projects with stages, a lead and helpers; leads decide; mood and commitment; quitting and taking over) -- Then: materials and money; the side jobs (scythe, snails, chainsaw art); Berlin (Grossbeerenstrasse, the Herberge); the "why" questions; the festival, the storm, the clean-up, the drive home +- Projects, still to do: the side jobs as projects of their own (decoration, light, mowing, snails, wood art); leads deciding more (where, what first, who does what); NPCs fetching the logs themselves; the crates and apples run out (food and drink: with the shopping) +- Next: materials and money; the side jobs (scythe, snails, chainsaw art); Berlin (Grossbeerenstrasse, the Herberge); the "why" questions; the festival, the storm, the clean-up, the drive home **Bugs** - Joint jumps left in the audit: jog+stop 171, sprint 297 (worst 26 cm) - running into things: a hurried catch step swings through in a few frames (step 0.07 -> 0.34, the ankle up 20 cm). Capping the swing speed helped sprint (278) but made jumps, drunk walk and the NPCs worse - tried and reverted in 1.83; ladder 45: toes on the sloping roof (a landing step's toes snap onto the slope, 16 cm; blending them in made all walking worse), toes turning onto the rungs getting on from the top (14 cm). Note (2.35): the audit is chaotic - each part starts from where the one before left you, so one small change anywhere reshuffles the later parts (a fall in the drunk walk or not, the ladder test not getting on the ladder); compare parts run on their own (the harness: LADDERONLY / JUMPSONLY / JOGONLY). Tried in 2.35: easing the running swing's ends (sprint worse), a swing not hurrying when stopping (no better on its own) @@ -1366,4 +1392,4 @@ Most important first. - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene) --- -State of `entropie 2.39.0 (build 2e7d256)` +State of `entropie 2.40.0 (build 43375ee)` diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index 4664bdf..9d9cf74 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,11 +1,14 @@ -entropie 2.39.0: the story starts - the crew +entropie 2.40.0: the floors get built -- The plan for the game in STATE.md: from the first meeting in Kreuzberg - to the drive home after the clean-up, the four floors, the crew, the - roadmap -- Everybody is one entry of a cast table (world/cast.c) with their part in - the festival: role, the project they lead or help with, commitment -- The crew turns up over the first day on the ground: Kilian and Julia, - Stella and Doro, Kerry, Rouven and Pascal; over to the fire to say hello -- F2 (dev): the crew overview - who's who, what everybody does now, the - projects with their leads and helpers +- Four stages marked out: Hofbuehne (yard), Obstwiese (orchard), + Kartoffelacker (the field behind the tents), the hidden floor (behind + the main building) +- Built step by step: ground cleared, frame (needs three logs lying by + it), deck, back wall and roof, paint and banners in the floor's colour +- Leads and helpers go and build in working hours; you build too (hold + left click at a stage) +- Commitment: lifted by progress and praise, worn down by being held up, + working worn out, nobody leading, annoyance; too low, leads quit +- Talk about the festival (X, then 4): praise, ask someone to lead an open + floor, take over a lead, get help with yours +- Late arrivals say hello standing by the fire (no more walking into it) diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h index d60815b..ade740f 100644 --- a/entropie/src/game/game.h +++ b/entropie/src/game/game.h @@ -75,6 +75,7 @@ typedef struct { Arrival arrival; /* the start: in by car */ float overcast; /* how clouded over the sky is (eases: clouds come before the rain) */ struct Object *tent; /* the tent you're working on (left button held) */ + struct Object *stage; /* the floor's stage you're building on (left button held) */ float tent_hold; /* ... held this long at a standing one (a moment: you start taking it down) */ struct Object *tent_drag; /* a tent with no pegs in you're moving (F held) ... */ Vec3 drag_off; /* ... where it is from you (your right, up, ahead) ... */ diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c index eb7e41b..75ab6c2 100644 --- a/entropie/src/game/game_actions.c +++ b/entropie/src/game/game_actions.c @@ -4,6 +4,7 @@ */ #include "game/game_internal.h" +#include "world/stage.h" #include "people/pockets.h" #include "world/clothes.h" #include "world/car.h" @@ -17,7 +18,7 @@ #define APPLE_REACH 1.6f /* from the trunk of an apple tree */ #define BAG_INTERVAL 0.35f /* holding the button: the next piece into the bag */ -enum { CLICK_NONE, CLICK_WORK, CLICK_DRINK, CLICK_BAG, CLICK_WATER, CLICK_TENT }; +enum { CLICK_NONE, CLICK_WORK, CLICK_DRINK, CLICK_BAG, CLICK_WATER, CLICK_TENT, CLICK_STAGE }; #define BUSH_OVER 1.3f /* bushes lower than this (scaled): you try to get over, and land in it */ #define CLIMB_PUSH 0.3f /* walking against it this long starts the climb */ @@ -761,6 +762,44 @@ static int tentClick(Game *game) { return CLICK_TENT; } +/* A floor's stage you're at (beside it or on its deck, facing it): left click held, you build on it. */ +#define STAGE_REACH 1.3f /* m out from its edge */ +#define STAGE_TIRES 0.0015f /* stamina a real second of building */ + +static Object *stageAt(const Game *game) { + const Object *p = game->player; + for (int i = 0; i < game->world.count; i++) { + Object *s = game->world.objects[i]; + if (s->type != OBJ_STAGE) continue; + Vec3 f = Object_facing(s), r = vec3(-f.z, 0.0f, f.x), d = vec3_sub(p->position, s->position); + float x = vec3_dot(d, r), z = vec3_dot(d, f); + if (fabsf(x) < s->size.x * 0.5f + STAGE_REACH && fabsf(z) < s->size.z * 0.5f + STAGE_REACH) return s; + } + return NULL; +} + +static bool workStage(Game *game) { + Object *p = game->player, *s = game->stage; + if (!s || stageAt(game) != s) return false; + if (s->build_stage >= BUILD_DONE) { say(game, "It's built!"); return false; } + Hands_touch(p, Stage_handsAt(s, p->position, p->body.clock)); + float handy = 0.5f * (p->skill[ORDER_FIREWOOD] + p->skill[ORDER_TREES]); + int got = Stage_work(&game->world, s, (0.6f + 0.8f * handy) * game->world.game_dt / 3600.0f); + p->needs.stamina = fmaxf(0.0f, p->needs.stamina - STAGE_TIRES * game->dt); + if (got == -2) { + snprintf(game->info, sizeof game->info, "The frame needs %d more log%s lying by it (carry some here: F with a helper)", + Stage_logsMissing(&game->world, s), Stage_logsMissing(&game->world, s) == 1 ? "" : "s"); + say(game, game->info); + return false; + } + if (got > 0) { + snprintf(game->info, sizeof game->info, "The %s stage: %s - next: %s", Cast_projectName((Project)s->variant), + got == BUILD_DONE ? "built!" : "a step done", Stage_stepName(got)); + say(game, game->info); + } else if (game->message_time <= 0.0f) tell(game, s); + return true; +} + /* Left button held at a tent: the next bit of it. */ #define TAKE_DOWN_HOLD 1.0f /* s: held this long at a standing tent, you start taking it down */ @@ -1197,6 +1236,10 @@ static int leftClick(Game *game) { } int tent = tentClick(game); if (tent >= 0) return tent; + if (Hands_freeHand(p) >= 0 && !Hands_inFront(p, objects, count) && (game->stage = stageAt(game)) != NULL) { /* (a hand free at a stage: building) */ + tell(game, game->stage); + return CLICK_STAGE; + } if (game_unloadCar(game) || mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || takePullover(game) || smokesMachine(game) || swingGate(game)) return CLICK_NONE; int watering = useWater(game); if (watering >= 0) return watering; @@ -1248,6 +1291,9 @@ static void keepClicking(Game *game) { case CLICK_TENT: if (!workTent(game)) game->click_action = CLICK_NONE; break; + case CLICK_STAGE: + if (!workStage(game)) game->click_action = CLICK_NONE; + break; default: break; } diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c index 39d8a71..7248d47 100644 --- a/entropie/src/game/game_dev.c +++ b/entropie/src/game/game_dev.c @@ -12,7 +12,7 @@ typedef enum { DEV_WHO, DEV_TELEPORT, DEV_TIME, DEV_SPEED, DEV_WEATHER, DEV_WIND, DEV_START, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_DRUNK, DEV_ENERGY, DEV_BLADDER, DEV_HUNGER, DEV_THIRST, DEV_SLEEPY, DEV_STRENGTH, DEV_ANNOYED, - DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING, + DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING, DEV_COMMIT, DEV_LIKES, /* likes for each job ... */ DEV_SKILLS = DEV_LIKES + 4, /* ... skill at each job */ DEV_ROWS = DEV_SKILLS + 4 @@ -155,6 +155,7 @@ void game_dev(Game *game, const Input *input) { case DEV_WITS: o->wits = step01(o->wits, dir, 0.05f); break; case DEV_HEIGHTS: o->height_fear = step01(o->height_fear, dir, 0.05f); break; case DEV_WILLING: c->willing = step01(c->willing, dir, 0.1f); break; + case DEV_COMMIT: o->commitment = step01(o->commitment, dir, 0.1f); break; default: break; } } @@ -202,6 +203,7 @@ static void devLine(const Game *game, const Object *o, int row, char *out) { case DEV_WITS: snprintf(out, 96, "Wits: %.2f", o->wits); break; case DEV_HEIGHTS: snprintf(out, 96, "Afraid of heights: %.2f", o->height_fear); break; case DEV_WILLING: snprintf(out, 96, "Willing: %.0f%%", c->willing * 100.0f); break; + case DEV_COMMIT: snprintf(out, 96, "Into the festival (commitment): %.0f%%", (double)(o->commitment * 100.0f)); break; default: out[0] = '\0'; break; } } diff --git a/entropie/src/game/game_story.c b/entropie/src/game/game_story.c index 9426749..47db44c 100644 --- a/entropie/src/game/game_story.c +++ b/entropie/src/game/game_story.c @@ -10,6 +10,7 @@ #include "game/game_internal.h" #include "world/cast.h" #include "world/site.h" +#include "world/stage.h" #define STORY_CHECK 1.0f /* real s between looks at who's due */ #define WALK_IN_AT 4 /* where they turn up: this point of the way in by car (the gravel road by the main building) */ @@ -132,7 +133,14 @@ int Game_crewLines(const Game *game, char lines[][128], int max, const char **ti while (helpers[len]) len++; } } - snprintf(lines[n++], 128, "%s: %s%s%s", Cast_projectName((Project)p), lead, len ? " helping: " : "", helpers); + char built[48] = ""; + const Object *s = Stage_of(game->world.objects, game->world.count, (Project)p); + if (s && s->build_stage >= BUILD_DONE) snprintf(built, sizeof built, " [built]"); + else if (s) { + int logs = Stage_logsMissing(&game->world, s); + snprintf(built, sizeof built, " [%s %d%%%s]", Stage_stepName(s->build_stage), (int)(Stage_part(s) * 100.0f), logs ? ", needs logs" : ""); + } + snprintf(lines[n++], 128, "%s%s: %s%s%s", Cast_projectName((Project)p), built, lead, len ? " helping: " : "", helpers); } } *title = head; diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c index c7bad6d..2fa0744 100644 --- a/entropie/src/game/save.c +++ b/entropie/src/game/save.c @@ -44,7 +44,7 @@ static const Field THING[] = { FL("wake", wake), IN("stored", stored), FL("work", work), FL("cut_at", cut_at), FL("mark", mark), VE("mark_at", mark_at), VE("mark_out", mark_out), IN("branches", branches), BO("trunk_cut", trunk_cut), BO("toppled", toppled), FL("topple", topple), VE("fall_dir", fall_dir), FL("fall_base_y", fall_base_y), VE("fall_origin", fall_origin), FL("fall_ground_y", fall_ground_y), - FL("fall_slide", fall_slide), VE("fell_plan", fell_plan), BO("fell_blocked", fell_blocked), BO("keep", keep), FL("charred", charred), BO("umbrella_ruined", umbrella_ruined), VE("hat_tint", look.hat_color), IN("umbrella_flips", umbrella_flips), + FL("fall_slide", fall_slide), VE("fell_plan", fell_plan), BO("fell_blocked", fell_blocked), BO("keep", keep), FL("charred", charred), BO("umbrella_ruined", umbrella_ruined), IN("build_stage", build_stage), FL("build_work", build_work), VE("hat_tint", look.hat_color), IN("umbrella_flips", umbrella_flips), FL("glow", glow), FL("fuel", fuel), BO("burning", burning), FL("water", water), FL("grill", grill), BO("grilled", grilled), BO("burnt", burnt), FL("extend", extend), FL("raised", raised), BO("leaning", leaning), VE("lean_normal", lean_normal), FL("lean_top", lean_top), FL("stability", stability), IN("crate_empties", crate_empties), IN("tent_stage", tent_stage), diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c index c13602f..9ea861d 100644 --- a/entropie/src/people/ai.c +++ b/entropie/src/people/ai.c @@ -1737,6 +1737,10 @@ static bool goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, return false; } +bool ai_goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int count, float dt) { + return goToPoint(self, p, reach, objects, count, dt); +} + static bool unloadCar(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; Object *car = ai->unload_car; @@ -2350,6 +2354,10 @@ static bool greetHosts(Object *self, Object *const *objects, int count, float dt return false; } if (self->sitting) return false; /* (there: whatever they do sitting) */ + if (ai->greet_left > 0.0f && !ai->greet_stand) { /* (turned up later: hello standing by the fire - the benches are the hosts') */ + ai->greet_stand = true; + ai->greet_at = standByFire(self, fire, objects, count); + } if (ai->greet_stand) { /* standing with the others, facing the fire */ if (!goToPoint(self, ai->greet_at, 0.35f, objects, count, dt)) return true; stand(self); @@ -2506,6 +2514,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { ai->boxed = fmaxf(0.0f, ai->boxed - dt); ai->annoyed = fmaxf(0.0f, ai->annoyed - dt / ANNOYED_FADE); g_dt = dt; + ai_commitment(self, objects, count, dt); if (Body_isBusy(&self->body)) { /* on the ground, or being sick */ stand(self); ai->pushing = false; @@ -2579,6 +2588,8 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { goToBed(self, objects, count, dt)) return; /* Tents: their own up, a hand with others', the bed things in */ if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && pitchTents(self, objects, count, dt)) return; + /* Their floor's stage: building it, in working hours */ + if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && ai_projectWork(self, objects, count, dt)) return; /* A finished cigarette in the fingers: to the tin, a bin, the fire */ if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && binButt(self, objects, count, dt)) return; /* Warm: the pullover over a railing or a chair (the tidy) */ diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h index 8220936..def3ec6 100644 --- a/entropie/src/people/ai.h +++ b/entropie/src/people/ai.h @@ -194,6 +194,12 @@ typedef struct { float dodge_t; /* ... for this long */ struct Object *umbrella; /* rain: off to get this umbrella instead of going under a roof (ai.c shelter) */ float greet_left; /* just turned up: this long at the fire, then on with the day (0: till told otherwise) */ + bool on_project; /* at work on their floor's stage (ai_project.c) ... */ + float shift; /* ... game s into this go at it (then a break) ... */ + float project_off; /* ... game s till they go back to it ... */ + float held_up; /* ... game s it's been held up (logs missing) ... */ + float remark; /* ... real s till the next remark about it */ + float praised; /* game s till a "great work" counts again */ struct Object *share; /* rain: walking under this one's umbrella, at their side ... */ float share_time; /* ... this long so far */ struct Object *blown; /* the wind took this out of their hand: after it (ai.c fetchBlown) ... */ diff --git a/entropie/src/people/ai_internal.h b/entropie/src/people/ai_internal.h index bebcce4..9309513 100644 --- a/entropie/src/people/ai_internal.h +++ b/entropie/src/people/ai_internal.h @@ -100,6 +100,12 @@ bool ai_climbing(Object *self, float dt); bool ai_followByLadder(Object *self, Object *const *objects, int count, const Object *leader, float dt); extern struct World *g_world; void ai_ensureReady(Object *self); +/* Off to a point (ai.c goToPoint): true there. */ +bool ai_goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int count, float dt); +/* Their part of the festival (ai_project.c): how into it they are, going with what happens + (quitting a lead, or helping at all, when it's gone); and the work on their floor's stage. */ +void ai_commitment(Object *self, Object *const *objects, int count, float dt); +bool ai_projectWork(Object *self, Object *const *objects, int count, float dt); static inline float random01(Ai *ai) { ai->rng = ai->rng * 1664525u + 1013904223u; diff --git a/entropie/src/people/ai_project.c b/entropie/src/people/ai_project.c new file mode 100644 index 0000000..955ff9e --- /dev/null +++ b/entropie/src/people/ai_project.c @@ -0,0 +1,181 @@ +/* + * 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...). + * 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 <stdio.h> + +#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 */ + +static bool floorProject(int p) { + return p == PROJECT_HOFBUEHNE || p == PROJECT_OBSTWIESE || p == PROJECT_KARTOFFELACKER || p == PROJECT_HIDDEN; +} + +static int theirFloor(const Object *self) { + if (floorProject(self->leads)) return self->leads; + if (floorProject(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 || theirFloor(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 != theirFloor(self) && !leadOf(objects, count, theirFloor(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) */ + ai->on_project = false; + ai->shift = 0.0f; + ai->held_up = 0.0f; + return false; +} + +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 = theirFloor(self); + Object *stage = project ? Stage_of(objects, count, (Project)project) : NULL; + ai->project_off = fmaxf(0.0f, ai->project_off - gdt); + if (!stage || stage->build_stage >= BUILD_DONE) return stop(self, NULL); + 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->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 (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) 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); + 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]; + snprintf(line, sizeof line, "Off to the %s - %s. Who's coming?", Cast_projectName((Project)project), Stage_stepName(stage->build_stage)); + Ai_say(self, line); + } + } + if ((ai->shift += gdt) > SHIFT) return stop(self, lead ? "Break! Back in a bit." : NULL); + if (self->sitting) stand(self); + 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: logs missing for the frame */ + ai->held_up += gdt; + if (ai->remark <= 0.0f && firstAt(self, objects, count, project)) { + char line[AI_SPEECH_MAX]; + snprintf(line, sizeof line, "We need %d more log%s here for the frame!", Stage_logsMissing(g_world, stage), + Stage_logsMissing(g_world, stage) == 1 ? "" : "s"); + Ai_say(self, line); + ai->remark = REMARK_EVERY; + } + if (ai->held_up > HELD_UP_LEAVE) return stop(self, "No logs, no frame. I'm off till there are some."); + 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 && theirFloor(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; +} diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c index 6df0805..935dfec 100644 --- a/entropie/src/people/talk.c +++ b/entropie/src/people/talk.c @@ -3,6 +3,7 @@ #include "world/tools.h" #include "people/pockets.h" #include "world/site.h" +#include "world/stage.h" #include <stdio.h> #include <string.h> @@ -29,6 +30,7 @@ enum { 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_COUNT }; @@ -37,7 +39,7 @@ enum { 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?", - "The firewood goes somewhere else now", "Nothing, bye" }, + "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" }, @@ -49,9 +51,11 @@ static const char *const CHOICES[NODE_COUNT][TALK_CHOICES] = { [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)", "Never mind", "Bye" }, /* (2 and 3: see Talk_choices) */ }; static const Object *g_talk_player; /* (for the choices: what's between them) */ +static World *g_talk_world; /* (for the choices: who leads what) */ static const Order TASKS[] = { ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE }; @@ -62,6 +66,39 @@ static float flatDistance(Vec3 a, Vec3 b) { 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."; + 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; +} + /* What they say when the conversation gets to a node. */ static const char *lineFor(const Object *o, int node) { switch (node) { @@ -90,6 +127,7 @@ static const char *lineFor(const Object *o, int node) { 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); default: return ""; } } @@ -312,6 +350,44 @@ static void talkLighter(Talk *talk, Object *player, int topic) { } } +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) go(talk, NODE_HELLO); + else wrapUp(talk, "See you."); +} + static void answer(Talk *talk, World *world, Object *player, int choice) { Object *npc = talk->with; switch (talk->node) { @@ -330,10 +406,8 @@ static void answer(Talk *talk, World *world, Object *player, int choice) { talkLighter(talk, player, lighterTopic(player, npc)); } else if (choice == 3) { Ai_say(npc, npc->ai.character.about ? npc->ai.character.about : "Me? Nobody special."); - } else if (choice == 4) { - Ai_show(npc, SHOW_PLACE, player); - wrapUp(talk, "Okay, show me where. Click on the spot."); - } else wrapUp(talk, "See you."); + } else if (choice == 4) go(talk, NODE_FESTIVAL); + else wrapUp(talk, "See you."); break; case NODE_TASKS: case NODE_HELPER: @@ -400,6 +474,7 @@ static void answer(Talk *talk, World *world, Object *player, int choice) { 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_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."); } @@ -427,7 +502,7 @@ static int nodeFor(const Object *npc, Topic topic) { } void Talk_begin(Talk *talk, World *world, Object *player, Object *with, Topic topic) { - (void)world; + g_talk_world = world; g_talk_player = player; if (talk->with) Ai_setTalking(talk->with, false); memset(talk, 0, sizeof(*talk)); @@ -525,6 +600,19 @@ int Talk_choices(const Talk *talk, const char *out[TALK_CHOICES]) { 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 (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; diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h index 198f1d4..1b3f9a7 100644 --- a/entropie/src/render/shapes.h +++ b/entropie/src/render/shapes.h @@ -155,6 +155,11 @@ Mesh Shapes_crate(int variant, const unsigned char *slots, int n); /* shape: UMBRELLA_FOLDED, UMBRELLA_OPEN, UMBRELLA_INSIDE_OUT (blown: the canopy up like a cup, ribs bent) */ enum { UMBRELLA_FOLDED, UMBRELLA_OPEN, UMBRELLA_INSIDE_OUT }; Mesh Shapes_umbrella(float length, float radius, int shape, Vec3 color); +/* A floor's stage as far as it's built: stage (0 marked out .. 5 done) and how far into + it (0..1); width along X, depth along Z (the front at +Z), its colour for the paint and + the banners. The platform is STAGE_DECK high once the floor is in. */ +#define STAGE_DECK 0.5f +Mesh Shapes_stage(int stage, float part, float width, float depth, Vec3 accent); /* A hat lying about (HAT_CAP, HAT_BEANIE), its crown up, the cap's brim along +Z. */ Mesh Shapes_hat(int kind, Vec3 color); /* Camping things (centred, lying along X): kind 0 a sleeping bag in its sack, 1 a foam mat rolled up, diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c index 7e62f17..eceb39e 100644 --- a/entropie/src/render/shapes_props.c +++ b/entropie/src/render/shapes_props.c @@ -1112,6 +1112,70 @@ Mesh Shapes_umbrella(float length, float radius, int shape, Vec3 color) { return Mesh_fromBuilder(&b); } +/* A stage going up: stakes and string, then posts and beams, the deck, the roof and the + back wall, then paint and banners. Each stage shows bit by bit (part). */ +Mesh Shapes_stage(int stage, float part, float width, float depth, Vec3 accent) { + MeshBuilder b; + MeshBuilder_init(&b); + const float hx = width * 0.5f, hz = depth * 0.5f, deck = STAGE_DECK, top = 3.2f; + const Vec3 STAKE = { 0.72f, 0.60f, 0.40f }, STRING = { 0.92f, 0.30f, 0.20f }, POST = COLOR_TIMBER, BOARD = COLOR_PLYWOOD; + const Vec3 corner[4] = { { -hx, 0.0f, -hz }, { hx, 0.0f, -hz }, { hx, 0.0f, hz }, { -hx, 0.0f, hz } }; + if (stage <= 0) { /* marked out: a stake at each corner, string round it (more of it cleared as it goes) */ + for (int k = 0; k < 4; k++) addBlock(&b, vec3(corner[k].x, 0.3f, corner[k].z), vec3(0.05f, 0.6f, 0.05f), STAKE); + addBlock(&b, vec3(0.0f, 0.5f, -hz), vec3(width, 0.01f, 0.01f), STRING); + addBlock(&b, vec3(0.0f, 0.5f, hz), vec3(width, 0.01f, 0.01f), STRING); + addBlock(&b, vec3(-hx, 0.5f, 0.0f), vec3(0.01f, 0.01f, depth), STRING); + addBlock(&b, vec3(hx, 0.5f, 0.0f), vec3(0.01f, 0.01f, depth), STRING); + return Mesh_fromBuilder(&b); + } + /* the frame: posts (back ones taller), beams round the top and under the deck */ + int posts = stage == 1 ? (int)(part * 6.0f + 1.0f) : 6; + const Vec3 at[6] = { { -hx, 0, -hz }, { 0, 0, -hz }, { hx, 0, -hz }, { -hx, 0, hz }, { 0, 0, hz }, { hx, 0, hz } }; + for (int k = 0; k < posts && k < 6; k++) { + float h = at[k].z < 0.0f ? top + 0.4f : top; + addBlock(&b, vec3(at[k].x, h * 0.5f, at[k].z), vec3(0.16f, h, 0.16f), POST); + } + if (stage > 1 || part > 0.5f) { + addBlock(&b, vec3(0.0f, top + 0.3f, -hz), vec3(width + 0.2f, 0.14f, 0.12f), POST); /* top beams */ + addBlock(&b, vec3(0.0f, top - 0.1f, hz), vec3(width + 0.2f, 0.14f, 0.12f), POST); + } + if (stage > 1 || part > 0.8f) { + addBlock(&b, vec3(0.0f, deck - 0.08f, -hz), vec3(width, 0.12f, 0.1f), POST); /* under the deck */ + addBlock(&b, vec3(0.0f, deck - 0.08f, hz), vec3(width, 0.12f, 0.1f), POST); + } + if (stage >= 2) { /* the deck: boards across, laid front to back */ + int boards = (int)(width / 0.3f), laid = stage == 2 ? (int)((float)boards * part) : boards; + for (int k = 0; k < laid; k++) { + float x = -hx + 0.15f + 0.3f * (float)k; + addBlock(&b, vec3(x, deck - 0.02f, 0.0f), vec3(0.28f, 0.04f, depth), k % 2 ? BOARD : vec3_scale(BOARD, 0.92f)); + } + if (stage > 2) addBlock(&b, vec3(0.0f, deck * 0.5f - 0.02f, hz), vec3(width, deck - 0.04f, 0.03f), vec3_scale(BOARD, 0.8f)); /* the front skirt */ + } + if (stage >= 3) { /* the back wall and the roof */ + float wall = stage == 3 ? fminf(1.0f, part * 2.0f) : 1.0f, roof = stage == 3 ? fmaxf(0.0f, part * 2.0f - 1.0f) : 1.0f; + float wh = (top + 0.3f - deck) * wall; + Vec3 back = stage >= 4 ? vec3_lerp(BOARD, accent, 0.75f) : BOARD; + if (wh > 0.05f) addBlock(&b, vec3(0.0f, deck + wh * 0.5f, -hz + 0.05f), vec3(width, wh, 0.04f), back); + if (roof > 0.0f) { + size_t first = b.vertex_count; + addBlock(&b, vec3(0.0f, 0.0f, 0.0f), vec3(width + 0.4f, 0.05f, (depth + 0.6f) * roof), stage >= 4 ? vec3_scale(accent, 0.6f) : BOARD); + /* (sloping down to the front, from the back's top) */ + float slope = atanf(0.4f / depth); + transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, top + 0.25f, -hz - 0.3f + (depth + 0.6f) * roof * 0.5f)), mat4_rotate_x(slope))); + } + } + if (stage >= 4) { /* paint and banners: triangles along the front beam, a band of paint on the deck's front */ + int flags = stage == 4 ? (int)(part * 10.0f) : 10; + for (int k = 0; k < flags; k++) { + float x = -hx + width * ((float)k + 0.5f) / 10.0f; + Vec3 c = k % 2 ? accent : vec3(1.0f - accent.x * 0.5f, 0.85f, 0.3f + accent.z * 0.5f); + addBlock(&b, vec3(x, top - 0.35f, hz + 0.04f), vec3(width / 14.0f, 0.3f, 0.02f), c); + } + if (stage > 4 || part > 0.5f) addBlock(&b, vec3(0.0f, deck * 0.5f, hz + 0.02f), vec3(width, 0.12f, 0.02f), accent); + } + return Mesh_fromBuilder(&b); +} + /* A hat: a dome of rings (a cap: a brim out in front; a beanie: taller, a cuff). */ Mesh Shapes_hat(int kind, Vec3 color) { MeshBuilder b; diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c index 8837dbb..7fafdd2 100644 --- a/entropie/src/world/object.c +++ b/entropie/src/world/object.c @@ -1,4 +1,5 @@ #include "world/object.h" +#include "world/stage.h" #include "world/car.h" #include <stdio.h> @@ -397,6 +398,16 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size break; } + case OBJ_STAGE: /* (world/stage.c: its look by how far it's built; the deck solid once it's in) */ + obj->variant = (int)weight; + obj->weight = 500.0f; + obj->is_static = true; + obj->size = size; + obj->mesh = Shapes_stage(0, 0.0f, size.x, size.z, vec3(0.5f, 0.5f, 0.5f)); + addCollider(obj, vec3(0.0f, STAGE_DECK * 0.5f, 0.0f), vec3(size.x, STAGE_DECK, size.z), vec3_zero()); + obj->collider_count = 0; + break; + case OBJ_HAT: /* (grey here: its colour once it's known - world.c, showProps) */ obj->size = vec3(0.21f, 0.1f, 0.24f); obj->weight = 0.1f; @@ -1506,6 +1517,12 @@ void Object_describe(const Object *obj, char *out, size_t size) { else snprintf(out, size, "%s's tent - %s", obj->owner ? obj->owner->name : "Somebody", STAGE[obj->tent_stage % 5]); break; } + case OBJ_STAGE: { + int pct = (int)(Stage_part(obj) * 100.0f); + if (obj->build_stage >= BUILD_DONE) snprintf(out, size, "The %s stage - built!", Cast_projectName((Project)obj->variant)); + else snprintf(out, size, "The %s stage - %s %d%% (hold left click to work on it)", Cast_projectName((Project)obj->variant), Stage_stepName(obj->build_stage), pct); + break; + } case OBJ_HAT: { const char *what = obj->variant == 2 ? "beanie" : "cap"; if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s - pick it up to put it back on", what); diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h index f2a91b6..2ad3368 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -89,6 +89,7 @@ typedef enum { OBJ_TENT, /* static: a dome tent, weight 0 small / 1 big (see tent.h); stored = pegs in */ OBJ_UMBRELLA, /* an umbrella: size.y long (folded); opens held in the rain (umbrella_open); variant = colour */ OBJ_HAT, /* a hat off somebody's head (blown off): variant = HAT_*, look.hat_color, owner = whose */ + OBJ_STAGE, /* static: a floor's stage being built (world/stage.h): variant = Project, build_stage, build_work */ } ObjectType; /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */ @@ -305,6 +306,8 @@ typedef struct Object { Vec3 over_foot0[2], over_land[2]; int sit_style; /* characters: SIT_GROUND (cross-legged) or SIT_SEAT */ int cast_id; /* people: who in the story (world/cast.c), -1 nobody of it */ + int build_stage; /* a stage (OBJ_STAGE): how far it's built (BuildStage) ... */ + float build_work; /* ... person-hours of work into the next bit */ float commitment; /* people: 0..1 how much they're into their part of the festival */ int leads, helps; /* people: the Project they lead, one they help with (world/cast.h; PROJECT_NONE 0) */ struct { /* dogs */ diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c index 5b26bc2..a95e42a 100644 --- a/entropie/src/world/site.c +++ b/entropie/src/world/site.c @@ -1,4 +1,5 @@ #include "world/site.h" +#include "world/stage.h" #include "world/manor.h" #include "world/ladder.h" @@ -1265,7 +1266,29 @@ void Site_courseFrame(Vec3 *start, Vec3 *along) { *start = vec3_add(a, vec3_scale(*along, 2.0f)); } +/* The floors' stages, marked out where they're going to be built (a, b; which way their front looks along a). */ +static const struct { Project project; float a, b, front; } FLOORS[] = { + { PROJECT_HOFBUEHNE, 36.0f, 5.0f, -1.0f }, /* in the yard, its back to the barn, facing the main building */ + { PROJECT_OBSTWIESE, 42.0f, 38.0f, -1.0f }, /* at the far end of the orchard, facing the apple trees */ + { PROJECT_KARTOFFELACKER, 30.0f, -95.0f, 0.0f }, /* on the potato field behind the tents' meadow, facing the tents */ + { PROJECT_HIDDEN, -20.0f, -2.0f, 1.0f }, /* behind the main building, facing its back */ +}; + +Vec3 Site_floorSpot(int project) { + for (int k = 0; k < COUNT(FLOORS); k++) + if ((int)FLOORS[k].project == project) return court(FLOORS[k].a, FLOORS[k].b); + return Site_yard(); +} + bool Site_finish(World *world) { + for (int k = 0; k < COUNT(FLOORS); k++) { /* the stages, marked out on the ground */ + Vec3 at = court(FLOORS[k].a, FLOORS[k].b); + at.y = Terrain_height(&world->terrain, at.x, at.z); + Vec3 front = FLOORS[k].front != 0.0f ? vec3_scale(mapDir(COURT_ANGLE), FLOORS[k].front) : mapDir(COURT_ANGLE - 90.0f); + Object *s = Stage_create(FLOORS[k].project, at, yaw_facing(front)); + if (!s || !World_add(world, s)) return false; + } + /* Trees stand a little into the ground, wherever it is */ for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; diff --git a/entropie/src/world/site.h b/entropie/src/world/site.h index 4fb3265..056b0e8 100644 --- a/entropie/src/world/site.h +++ b/entropie/src/world/site.h @@ -29,6 +29,8 @@ Vec3 Site_drinksSpot(void); /* The middle of the kitchen in your garages: a roof to wait out the rain under, seats round it. */ Vec3 Site_rainRoom(void); +/* Where a floor's stage stands (the floors: world/stage.h). */ +Vec3 Site_floorSpot(int project); /* Every place like that (the kitchen, the shelter in the yard): their middles, how many. */ int Site_rainPlaces(Vec3 *out, int max); Vec3 Site_campSpot(int k); diff --git a/entropie/src/world/stage.c b/entropie/src/world/stage.c new file mode 100644 index 0000000..e33879e --- /dev/null +++ b/entropie/src/world/stage.c @@ -0,0 +1,118 @@ +#include "world/stage.h" +#include "render/shapes.h" + +#include <math.h> + +#define STAGE_W 6.0f /* m wide (along its front) */ +#define STAGE_D 4.0f /* m deep */ +#define LOOK_STEPS 8 /* the look changes in this many steps through each part of the work */ + +/* Person-hours each step takes (someone of middling skill). */ +static const float NEED[BUILD_DONE] = { + [BUILD_MARKED] = 1.5f, /* the ground cleared, levelled */ + [BUILD_FRAME] = 4.0f, /* posts in, beams on (and the logs for it) */ + [BUILD_FLOOR] = 4.0f, /* the deck */ + [BUILD_ROOF] = 3.0f, /* the back wall, the roof */ + [BUILD_DECOR] = 2.5f, /* paint, banners */ +}; + +/* Each floor's colour (the paint on the back wall, the banners). */ +static Vec3 accent(int project) { + switch (project) { + case PROJECT_HOFBUEHNE: return vec3(0.78f, 0.18f, 0.16f); /* bands: red */ + case PROJECT_OBSTWIESE: return vec3(0.52f, 0.20f, 0.70f); /* psytrance: purple */ + case PROJECT_KARTOFFELACKER: return vec3(0.90f, 0.55f, 0.15f); /* world music: orange */ + case PROJECT_HIDDEN: return vec3(0.16f, 0.22f, 0.50f); /* techno: deep blue */ + default: return vec3(0.5f, 0.5f, 0.5f); + } +} + +Object *Stage_create(Project project, Vec3 at, float yaw) { + Object *s = Object_create(OBJ_STAGE, PALETTE_WARM, at, vec3(STAGE_W, 3.6f, STAGE_D), (float)project); + if (!s) return NULL; + s->rotation = vec3(0.0f, yaw, 0.0f); + Object_syncColliders(s); + Stage_show(s); + return s; +} + +Object *Stage_of(Object *const *objects, int count, Project project) { + for (int i = 0; i < count; i++) + if (objects[i]->type == OBJ_STAGE && objects[i]->variant == (int)project) return objects[i]; + return NULL; +} + +const char *Stage_stepName(int stage) { + static const char *const NAME[] = { "clearing the ground", "the frame", "the deck", "the back wall and the roof", "paint and banners", "done" }; + return NAME[stage < 0 ? 0 : stage > BUILD_DONE ? BUILD_DONE : stage]; +} + +float Stage_part(const Object *s) { + if (s->build_stage >= BUILD_DONE) return 1.0f; + return fminf(1.0f, s->build_work / NEED[s->build_stage]); +} + +/* Logs lying by it (not held, not cut up). */ +static int logsBy(const World *world, const Object *s, Object **found, int max) { + int n = 0; + for (int i = 0; i < world->count && n < max; i++) { + Object *o = world->objects[i]; + if (o->type != OBJ_LOG || o->is_held || o->broken) continue; + float dx = o->position.x - s->position.x, dz = o->position.z - s->position.z; + if (dx * dx + dz * dz < STAGE_LOG_REACH * STAGE_LOG_REACH) { if (found) found[n] = o; n++; } + } + return n; +} + +int Stage_logsMissing(const World *world, const Object *s) { + if (s->build_stage != BUILD_FRAME) return 0; + int have = logsBy(world, s, NULL, STAGE_LOGS); + return have >= STAGE_LOGS ? 0 : STAGE_LOGS - have; +} + +int Stage_work(World *world, Object *s, float person_hours) { + if (s->build_stage >= BUILD_DONE) return -1; + if (Stage_logsMissing(world, s) > 0) return -2; + s->build_work += person_hours; + int done = -1; + if (s->build_work >= NEED[s->build_stage]) { + if (s->build_stage == BUILD_FRAME) { /* the logs went into it */ + Object *logs[STAGE_LOGS]; + int n = logsBy(world, s, logs, STAGE_LOGS); + for (int k = 0; k < n; k++) Object_useUp(logs[k]); + } + s->build_stage++; + s->build_work = 0.0f; + done = s->build_stage; + } + Stage_show(s); + return done; +} + +Vec3 Stage_workSpot(const Object *s, float side) { + Vec3 f = yaw_front(s->rotation.y), r = vec3(-f.z, 0.0f, f.x); /* (its front: +Z of the mesh) */ + float out = s->build_stage >= BUILD_ROOF ? STAGE_D * 0.5f - 0.6f : STAGE_D * 0.5f + 0.5f; /* (on the deck once there's a roof to it) */ + Vec3 p = vec3_add(s->position, vec3_add(vec3_scale(f, out), vec3_scale(r, side * STAGE_W * 0.4f))); + p.y = s->position.y; + return p; +} + +Vec3 Stage_handsAt(const Object *s, Vec3 spot, float t) { + Vec3 to = vec3_sub(s->position, spot); + to.y = 0.0f; + float len = vec3_length(to); + to = len > 1e-3f ? vec3_scale(to, 1.0f / len) : yaw_front(s->rotation.y); + float h = s->build_stage == BUILD_MARKED ? 0.3f : s->build_stage == BUILD_FRAME ? 1.2f + 0.4f * sinf(t * 0.7f) : s->build_stage == BUILD_FLOOR ? STAGE_DECK : 1.4f; + float knock = 0.06f * fabsf(sinf(t * 6.0f)); /* (hammering) */ + return vec3_add(spot, vec3_add(vec3_scale(to, 0.55f), vec3(0.0f, h + knock, 0.0f))); +} + +void Stage_show(Object *s) { + int look = s->build_stage * LOOK_STEPS + (int)(Stage_part(s) * (float)LOOK_STEPS); + if (s->shown == look + 1) return; + Mesh_destroy(&s->mesh); + s->mesh = Shapes_stage(s->build_stage, Stage_part(s), STAGE_W, STAGE_D, accent(s->variant)); + s->shown = look + 1; + int solid = s->build_stage > BUILD_FLOOR ? 1 : 0; /* (the deck can be stood on once it's in) */ + if (s->collider_count != solid) { s->collider_count = solid; Object_syncColliders(s); } +} diff --git a/entropie/src/world/stage.h b/entropie/src/world/stage.h new file mode 100644 index 0000000..0809849 --- /dev/null +++ b/entropie/src/world/stage.h @@ -0,0 +1,37 @@ +#ifndef WORLD_STAGE_H +#define WORLD_STAGE_H + +/* + * The floors' stages, built step by step (OBJ_STAGE, one per floor: Hofbuehne, + * Obstwiese, Kartoffelacker, the hidden floor). Each step takes work (person- + * hours, quicker for the skilled) and the frame takes logs lying by it; the + * look grows as the work goes on, the deck can be stood on once it's in. + */ + +#include "world/world.h" +#include "world/cast.h" + +typedef enum { BUILD_MARKED, BUILD_FRAME, BUILD_FLOOR, BUILD_ROOF, BUILD_DECOR, BUILD_DONE } BuildStage; + +#define STAGE_LOGS 3 /* logs lying by it for the frame */ +#define STAGE_LOG_REACH 6.0f /* m round its middle */ + +/* A stage marked out (nothing built yet) for a floor project, its front turned to yaw. */ +Object *Stage_create(Project project, Vec3 at, float yaw); +/* The stage of a project in the world, or NULL. */ +Object *Stage_of(Object *const *objects, int count, Project project); +/* What's being done now ("the frame"), said plainly; and how far (0..1). */ +const char *Stage_stepName(int stage); +float Stage_part(const Object *stage); +/* Logs still missing for this step (0: none needed, or all there). */ +int Stage_logsMissing(const World *world, const Object *stage); +/* Work on it: person_hours of work done (by somebody of that skill). Returns the + step just finished (BUILD_FRAME ...), -1 nothing new, -2 held up (logs missing). */ +int Stage_work(World *world, Object *stage, float person_hours); +/* Where somebody works on it (side -1 .. 1 along the front), and where their hands are. */ +Vec3 Stage_workSpot(const Object *stage, float side); +Vec3 Stage_handsAt(const Object *stage, Vec3 spot, float t); +/* Its look after work or loading (only rebuilt when it changes). */ +void Stage_show(Object *stage); + +#endif /* WORLD_STAGE_H */ diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c index 12e531a..947dbaa 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -7,6 +7,7 @@ #include "people/pockets.h" #include "people/condition.h" #include "people/dog.h" +#include "world/stage.h" #include "world/forestry.h" #include "world/ladder.h" #include "world/site.h" @@ -889,6 +890,7 @@ static void showProps(World *world) { if (open != f->heard_open) World_noise(world, f->type == OBJ_CAR ? NOISE_SLAM : NOISE_CREAK, f->position, 0.8f); f->heard_open = open; } + if (f->type == OBJ_STAGE) { Stage_show(f); continue; } /* (after loading: its look and deck) */ if (f->type == OBJ_HAT && !f->broken && f->shown != 1) { /* (its look: the colour it had on the head) */ Mesh_destroy(&f->mesh); f->mesh = Shapes_hat(f->variant, f->look.hat_color);