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.43.0: the side jobs - the scythe, the snails, Kerry's art
- Kartoffelacker grass stands knee high; mow it with the scythe (in the tool room): hold left click in the field and walk. Toni mows on his own; a careful lead waits until the snails are out, a careless one mows them over - Vineyard snails (30 in the field, 10 in the orchard): pick them into your bucket and let them go in the long grass by the wood left of the Kartoffelacker. Doro and Stella do it too - The regulatory office comes 44 h after you arrive (warned the day before and 3 h before): 40 EUR per snail still out there, 80 per dead one - Kerry carves a log lying in the yard by the pavilions into a figure (owl, bear, mushroom man) with the chainsaw - F2 projects page shows the side jobs' progress
commit_message.txt | 23 ++-- entropie/ARCHITECTURE.md | 4 +- entropie/Makefile | 2 +- entropie/README.md | 5 +- entropie/STATE.md | 31 +++++- entropie/commit_message.txt | 23 ++-- entropie/src/game/game.h | 2 + entropie/src/game/game_actions.c | 91 +++++++++++++++- entropie/src/game/game_story.c | 60 ++++++++++- entropie/src/game/save.c | 16 ++- entropie/src/game/sound.c | 3 + entropie/src/people/ai_help.c | 4 +- entropie/src/people/ai_internal.h | 5 + entropie/src/people/ai_jobs.c | 214 +++++++++++++++++++++++++++++++++++++ entropie/src/people/ai_project.c | 60 ++++++++--- entropie/src/people/hands.c | 20 +++- entropie/src/render/shapes.h | 5 + entropie/src/render/shapes_props.c | 66 ++++++++++++ entropie/src/world/field.c | 184 +++++++++++++++++++++++++++++++ entropie/src/world/field.h | 63 +++++++++++ entropie/src/world/meadow.c | 19 +++- entropie/src/world/meadow.h | 1 + entropie/src/world/object.c | 36 +++++++ entropie/src/world/object.h | 6 ++ entropie/src/world/site.c | 7 ++ entropie/src/world/site.h | 3 + entropie/src/world/world.c | 2 +- entropie/src/world/world.h | 5 + 28 files changed, 907 insertions(+), 53 deletions(-) diff --git a/commit_message.txt b/commit_message.txt index 0aab3e9..df222f7 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1,12 +1,13 @@ -entropie 2.42.0: pavilions by the house, beer table sitting, Kurti shows you all the way +entropie 2.43.0: the side jobs - the scythe, the snails, Kerry's art -- Pavilions turned 90 degrees and moved in front of the main building's - right half, left of its porch -- Beer benches: you step over the bench and sit facing the table (turned - away from it: back to the table as before); the others always face it -- Benches have evenly spaced places, so a third person always fits -- People no longer slide along the ground while seated when they set off - (Kurti showing the way, project work, sharing an umbrella) -- Kurti calls everybody to the end of the garages by the barn, points up - the path and explains the rest; then the marker shows your tent spot -- The chat at the fire waits until everybody has sat down +- Kartoffelacker grass stands knee high; mow it with the scythe (in the tool + room): hold left click in the field and walk. Toni mows on his own; a + careful lead waits until the snails are out, a careless one mows them over +- Vineyard snails (30 in the field, 10 in the orchard): pick them into your + bucket and let them go in the long grass by the wood left of the + Kartoffelacker. Doro and Stella do it too +- The regulatory office comes 44 h after you arrive (warned the day before + and 3 h before): 40 EUR per snail still out there, 80 per dead one +- Kerry carves a log lying in the yard by the pavilions into a figure (owl, + bear, mushroom man) with the chainsaw +- F2 projects page shows the side jobs' progress diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md index e196815..5ee09b4 100644 --- a/entropie/ARCHITECTURE.md +++ b/entropie/ARCHITECTURE.md @@ -10,8 +10,8 @@ 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`), 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/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`), the side jobs' ground (`field.c`: the Kartoffelacker's grass, the snails, Kerry's art), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game | +| `src/people/` | bodies (animation), hands, AI (the projects: `ai_project.c`, the side jobs: `ai_jobs.c`), 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), the shopping run (`game_shop.c`), saves (`save.c`), what you hear (`sound.c`) | all | - `render` knows nothing of the world: it gets meshes, matrices and plain numbers. diff --git a/entropie/Makefile b/entropie/Makefile index 56b75f6..be709a1 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 2.42.0 +VERSION := 2.43.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index 041205d..a112f95 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -25,6 +25,9 @@ people living there. 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 +- **Side jobs:** mow the Kartoffelacker with the scythe, rescue the + protected vineyard snails before the regulatory office comes (or pay a + fine), bring Kerry logs to carve into figures with the chainsaw - **Money and shopping:** a cash box of 1200 EUR. Send Volkram to the hardware store for boards, nails, screws, paint and food. The stages need that material; the crew fetches it from the store @@ -134,5 +137,5 @@ Dependencies point downward: `main → game → world/people → render → core ## Version -`make version` prints e.g. `entropie 2.42.0 (build e597e7a)`. The build id is +`make version` prints e.g. `entropie 2.43.0 (build 70cb9cf)`. The build id is an md5 of all sources and assets. diff --git a/entropie/STATE.md b/entropie/STATE.md index cf3c317..fed71a8 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -80,7 +80,7 @@ things the way they do. 3. Materials and money: the cash box, the hardware store, boards and beams, nails, screws, paint. (2.41) 4. The side jobs: the scythe on the Kartoffelacker, the snail rescue, Kerry's - chainsaw art. + chainsaw art. (2.43) 5. Berlin: Grossbeerenstrasse and the Herberge as places, the meetings, going back and forth between weekends. 6. The "why" questions and their options. @@ -408,6 +408,21 @@ Painted in the rotoscope style of *Waking Life* (2001): ("The food's all gone. Somebody has to go shopping.") - A stage waiting for material that lies in the store: one of those at it fetches it over (logs are yours to bring) +- The side jobs, in working hours like the floors: + - Mowing the Kartoffelacker (Toni): its grass stands knee high. The + scythe is in the tool room; with it in your hand, hold left click in the + field and walk along as you swing. A careful lead waits till the snails + are out of the field; a careless one mows over them (dead) + - The vineyard snails (Doro, Stella): about 30 in the Kartoffelacker, 10 + round the Obstwiese stage. Left click: into your bucket (8 at most); + walk to the long grass by the wood left of the Kartoffelacker and they + go free. The regulatory office comes 44 hours after you arrive (told + the day before and three hours before): every snail still out there + costs 40 EUR, every dead one 80 - none, and the snail crew is proud + - Kerry's wood art: a log lying in the yard by the pavilions (bring one), + the chainsaw - he carves it into a figure (an owl, a bear, a mushroom + man); without either he says what he needs + - F2, the projects page: how far each has got - 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 @@ -1407,6 +1422,15 @@ Round 2.42 (the pavilions in front of the house; sitting at a beer table; Kurti - The chat with Kurti waits till the three friends sit (or stand with the others) - the slowest (Volkram, the driver) was still walking up when it started and was left standing. - Checked: Jana and Volkram stepped over and sat facing the table, one each side; the start: all four at the foot of the path in 25 s, his three lines, then the tent spot marked; at the fire everybody sat before the chat (You, Toni, Kurti / Volkram, Jana, Martin / Jonas); hard rain: Volkram and Jana at the beer tables. +Round 2.43 (the side jobs: the scythe, the snails, Kerry's art): +- `world/field.c`: the Kartoffelacker's grass as cells (FIELD_NX 20 x FIELD_NZ 26 of 1 m from FIELD_X0/Z0; `World.mown` 0 .. FIELD_MOWN 3 swings, FIELD_OFF round the stage; saved in the `field` line as hex, with `inspected`, the rescued and dead counts and the warnings). `Field_mow` (a swing: cells within the radius a step on; wild snails there dead), `Field_done`, `Field_nextUnmown` (nearest, roughly row by row). `meadow.c`: grass in the field knee high (0.38-0.68 m), short once mown; the field's squares built again when `mow_rev` changed (every MOW_REBUILD 20 frames at most). +- OBJ_SCYTHE (`Shapes_scythe`; in the tool room, one hand, a home: put away): carried over the shoulder, mowing: the blade swept along the ground in front (`Object.mowing`, `hands.c`). You: left click with it in the field (CLICK_MOW, a swing every MOW_SWING_YOU 1.3 s, 0.9 m ahead). NPCs (`people/ai_jobs.c` mow): the scythe fetched, cell by cell, a swing every MOW_SWING 1.5 s / tempo; a careful lead (ai_stackCare >= MOW_CAREFUL 0.45, Toni 0.52) waits while snails are in the field. +- OBJ_SNAIL (`Shapes_snail`; FIELD_SNAILS 30 in the field, ORCHARD_SNAILS 10 round the Obstwiese, from `Field_prepare` in `Site_finish`): picked up into `Object.snails` (SNAIL_BUCKET 8, saved), let go at `Site_snailRefuge` (you: within REFUGE_YOU 3 m; they come back there as variant 1). NPCs (snails): snail to snail (crouched, SNAIL_BEND 1.2 s each), full or none within SNAIL_LOOK 30 m: to the refuge. The regulatory office (`game_story.c` inspection): FIELD_INSPECTION 44 h, warnings at 24 h and 3 h before; fine FINE_LEFT 40 / FINE_DEAD 80 EUR from the cash box, commitment down (the snail lead most) - or none and the snail crew lifted. Doro arrives warning about them. +- Kerry (`ai_jobs.c` carve): a log (0.8 m+) within ART_REACH 5 m of `Site_artSpot` (court(14, 12)) and the chainsaw: he carves it (CARVE_NEED 2.5 person-hours in its `build_work`; `Object.carving`: the saw's sound) into OBJ_SCULPTURE (`Shapes_sculpture`: owl, bear, mushroom man by count). +- Projects (`ai_project.c`): `theirProject` takes the side jobs too (`workable`); the side job's frame is `ai_job` (SIDE_HELD: the remark, held up; SIDE_STEP: a line, a little lift; SIDE_FINISHED: everybody on it lifted); a finished snail job with snails still in the bucket: let go first. Tools for the job stay in hand (`ai_putAway` not while on it). +- F2 projects page: mowing %, snails safe / out / dead / office in, Kerry's figures and whether a log is there. +- Checked (fed, 10:30): Toni waited for the snails, Doro and Stella got all 39 to the refuge in about 5 min, then Toni mowed; with a careless mower 6 died; Kerry fetched the chainsaw and set to carving once a log lay there (the three figures looked at made directly); the office: no fine with all out; saved and loaded mid-way (mown cells, counts and a bucket of 5 kept). + ## Open tasks Most important first. @@ -1416,7 +1440,8 @@ Most important first. - Money, still to do: money coming in (tickets, the web things); you shopping yourself; paying for cigarettes - The Kartoffelacker: seats to build, a chill-out platform up a tree (about 1.8 m), hammocks to hang; the way through the strip of wood cut with an axe and a spade; the guests' camping beyond it (above the chicken farm) - check the places on the ground - Compost toilets: delivered for the festival, built together, put up at places decided later -- Next: the side jobs (scythe, snails, chainsaw art); Berlin (Grossbeerenstrasse, the Herberge); the "why" questions; the festival, the storm, the clean-up, the drive home +- Side jobs, still to do: decoration and light as jobs of their own; the man from the regulatory office as a person walking the field; Kerry's figures placed where they go; helpers raking the mown grass +- Next: 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) @@ -1450,4 +1475,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.42.0 (build e597e7a)` +State of `entropie 2.43.0 (build 70cb9cf)` diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index 0aab3e9..df222f7 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,12 +1,13 @@ -entropie 2.42.0: pavilions by the house, beer table sitting, Kurti shows you all the way +entropie 2.43.0: the side jobs - the scythe, the snails, Kerry's art -- Pavilions turned 90 degrees and moved in front of the main building's - right half, left of its porch -- Beer benches: you step over the bench and sit facing the table (turned - away from it: back to the table as before); the others always face it -- Benches have evenly spaced places, so a third person always fits -- People no longer slide along the ground while seated when they set off - (Kurti showing the way, project work, sharing an umbrella) -- Kurti calls everybody to the end of the garages by the barn, points up - the path and explains the rest; then the marker shows your tent spot -- The chat at the fire waits until everybody has sat down +- Kartoffelacker grass stands knee high; mow it with the scythe (in the tool + room): hold left click in the field and walk. Toni mows on his own; a + careful lead waits until the snails are out, a careless one mows them over +- Vineyard snails (30 in the field, 10 in the orchard): pick them into your + bucket and let them go in the long grass by the wood left of the + Kartoffelacker. Doro and Stella do it too +- The regulatory office comes 44 h after you arrive (warned the day before + and 3 h before): 40 EUR per snail still out there, 80 per dead one +- Kerry carves a log lying in the yard by the pavilions into a figure (owl, + bear, mushroom man) with the chainsaw +- F2 projects page shows the side jobs' progress diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h index ece0ad4..cc7f1d6 100644 --- a/entropie/src/game/game.h +++ b/entropie/src/game/game.h @@ -105,6 +105,8 @@ typedef struct { Errand errand; /* a shopping run */ Mesh quest_mesh; /* the marker over who or what to go to next (Game_questMarker) ... */ float quest_t; /* ... its bob and turn */ + float mow_t; /* into this swing of the scythe (mowing: left button held) */ + int snail_warned; /* the regulatory office: told of it (1 the day before, 2 on the day) */ } Game; /* A line of speech above someone's head, in screen pixels (y up from the bottom). */ diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c index e8467f8..9f160c3 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/field.h" #include "world/stage.h" #include "people/pockets.h" #include "world/clothes.h" @@ -18,7 +19,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, CLICK_STAGE }; +enum { CLICK_NONE, CLICK_WORK, CLICK_DRINK, CLICK_BAG, CLICK_WATER, CLICK_TENT, CLICK_STAGE, CLICK_MOW }; #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 */ @@ -631,7 +632,85 @@ static bool pickApple(Game *game) { return false; } +/* ---------- The side jobs: the scythe, the snails ---------- */ + +#define MOW_SWING_YOU 1.3f /* s a swing of the scythe */ +#define MOW_AHEAD 0.9f /* m in front: where the blade goes through */ +#define SNAIL_REACH 1.3f /* m: a snail this near in front is picked up */ +#define REFUGE_YOU 3.0f /* m from the refuge: your snails go free */ + +static Object *scytheIn(const Object *p) { + for (int h = 0; h < 2; h++) + if (p->hands.item[h] && p->hands.item[h]->type == OBJ_SCYTHE && p->hands.state[h] == HAND_HOLDING) return p->hands.item[h]; + return NULL; +} + +/* Left click with the scythe in the field: mowing (hold it and walk along). */ +static int startMowing(Game *game) { + Object *p = game->player; + if (!scytheIn(p)) return -1; + Vec3 ahead = vec3_add(p->position, vec3_scale(Object_facing(p), MOW_AHEAD)); + if (!Field_inside(ahead.x, ahead.z)) { say(game, "The scythe's for the Kartoffelacker's long grass - out behind the road to the chicken farm"); return CLICK_NONE; } + game->mow_t = 0.0f; + return CLICK_MOW; +} + +static bool mowStep(Game *game) { + Object *p = game->player; + if (!scytheIn(p)) return false; + Vec3 ahead = vec3_add(p->position, vec3_scale(Object_facing(p), MOW_AHEAD)); + if (!Field_inside(ahead.x, ahead.z)) return false; + game->mow_t += game->dt; + p->mowing = 0.001f + 3.1416f * game->mow_t / MOW_SWING_YOU; + if (game->mow_t < MOW_SWING_YOU) return true; + game->mow_t = 0.0f; + int killed = 0; + Field_mow(&game->world, ahead, 0.8f, &killed); + World_noise(&game->world, NOISE_RUSTLE, p->position, 0.4f); + p->needs.stamina = fmaxf(0.0f, p->needs.stamina - 0.004f); + if (killed) { say(game, "Crunch - a snail in the grass! They're protected: pick them up before you mow"); return true; } + snprintf(game->info, sizeof game->info, "Mowing the Kartoffelacker - %d%% done (walk on as you swing)", (int)(Field_done(&game->world) * 100.0f)); + game->info_about = NULL; + say(game, game->info); + return true; +} + +/* A vineyard snail in front: into your bucket. */ +static bool pickSnail(Game *game) { + Object *p = game->player, *best = NULL; + float best_d = SNAIL_REACH; + for (int i = 0; i < game->world.count; i++) { + Object *o = game->world.objects[i]; + if (o->type != OBJ_SNAIL || o->variant || o->broken || o->is_held) continue; + Vec3 to = vec3(o->position.x - p->position.x, 0.0f, o->position.z - p->position.z); + float d = vec3_length(to); + if (d > 0.3f && vec3_dot(vec3_scale(to, 1.0f / d), Object_facing(p)) < 0.5f) continue; + if (d < best_d) { best_d = d; best = o; } + } + if (!best) return false; + Hands_touch(p, best->position); + if (!Field_pickSnail(&game->world, p, best)) { say(game, "Your bucket's full - let them go by the wood next to the Kartoffelacker"); return true; } + snprintf(game->info, sizeof game->info, "A vineyard snail into your bucket (%d) - let them go by the wood next to the Kartoffelacker", p->snails); + game->info_about = NULL; + say(game, game->info); + return true; +} + +/* At the refuge with snails: off they go. */ +static void freeSnails(Game *game) { + Object *p = game->player; + if (p->snails <= 0) return; + Vec3 r = Site_snailRefuge(); + if (hypotf(r.x - p->position.x, r.z - p->position.z) > REFUGE_YOU) return; + int n = Field_release(&game->world, p); + snprintf(game->info, sizeof game->info, "You let %d snail%s go in the long grass by the wood - safe here (%d rescued so far)", n, n == 1 ? "" : "s", + game->world.snails_rescued); + game->info_about = NULL; + say(game, game->info); +} + static void takeInFront(Game *game) { + if (pickSnail(game)) return; if (takeFromSupply(game)) return; if (!Hands_inFront(game->player, game->world.objects, game->world.count) && pickApple(game)) return; Object *p = game->player; @@ -1244,7 +1323,10 @@ static int leftClick(Game *game) { 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; + if (pickSnail(game)) return CLICK_NONE; /* (a snail in the grass: picked up, not mown) */ + int mow = startMowing(game); + if (mow >= 0) return mow; + 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; if (bagTrash(game)) return CLICK_BAG; @@ -1298,6 +1380,9 @@ static void keepClicking(Game *game) { case CLICK_STAGE: if (!workStage(game)) game->click_action = CLICK_NONE; break; + case CLICK_MOW: + if (!mowStep(game)) game->click_action = CLICK_NONE; + break; default: break; } @@ -1478,6 +1563,8 @@ void game_handleActions(Game *game, const Input *input) { /* Left click (held: again and again), right click puts down */ p->spraying = false; + game->player->mowing = 0.0f; /* (swinging the scythe: only while it's held down) */ + freeSnails(game); if (input->click) game->click_action = leftClick(game); else if (input->click_held) keepClicking(game); else game->click_action = CLICK_NONE; diff --git a/entropie/src/game/game_story.c b/entropie/src/game/game_story.c index d295215..e3c3932 100644 --- a/entropie/src/game/game_story.c +++ b/entropie/src/game/game_story.c @@ -11,6 +11,7 @@ #include "world/cast.h" #include "world/site.h" #include "world/stage.h" +#include "world/field.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) */ @@ -24,9 +25,56 @@ static bool here(const Game *game, int id) { return false; } +/* The regulatory office: warned of the day before and on the day; then it comes and looks at where you build. */ +#define FINE_LEFT 40 /* EUR a snail still out there */ +#define FINE_DEAD 80 /* ... one mown over */ +static void inspection(Game *game) { + World *w = &game->world; + if (w->inspected) return; + float left = FIELD_INSPECTION - game->story_hours; + Object *doro = NULL; + for (int i = 0; i < w->count && !doro; i++) + if (w->objects[i]->is_character && w->objects[i]->leads == PROJECT_SNAILS) doro = w->objects[i]; + if (left <= 24.0f && !(game->snail_warned & 1)) { + game->snail_warned |= 1; + if (doro) Ai_say(doro, "The regulatory office comes tomorrow! Every snail out of where we build by then."); + say(game, "Tomorrow the regulatory office comes: the vineyard snails have to be out of the building places"); + } + if (left <= 3.0f && !(game->snail_warned & 2)) { + game->snail_warned |= 2; + if (doro) Ai_say(doro, "Three hours till the office is here - how many snails are still out there?"); + snprintf(game->story_line, sizeof game->story_line, "The regulatory office comes in 3 hours - %d snail%s still out there", Field_snailsLeft(w), + Field_snailsLeft(w) == 1 ? "" : "s"); + say(game, game->story_line); + } + if (left > 0.0f) return; + w->inspected = true; + int out = Field_snailsLeft(w), dead = w->snails_killed, fine = FINE_LEFT * out + FINE_DEAD * dead; + if (fine == 0) { + snprintf(game->story_line, sizeof game->story_line, "The man from the regulatory office walks the field: not a snail where you build. All fine - well done!"); + for (int i = 0; i < w->count; i++) { + Object *o = w->objects[i]; + if (o->is_character && (o->leads == PROJECT_SNAILS || o->helps == PROJECT_SNAILS)) o->commitment = fminf(1.0f, o->commitment + 0.1f); + } + if (doro) Ai_say(doro, "He's happy! Not a single snail where we build. Thanks, everybody!"); + } else { + w->cash -= (float)fine; + snprintf(game->story_line, sizeof game->story_line, "The regulatory office: %d snail%s still where you build, %d dead - a fine of %d EUR from the cash box", + out, out == 1 ? "" : "s", dead, fine); + for (int i = 0; i < w->count; i++) { + Object *o = w->objects[i]; + if (!o->is_character || o->cast_id < 0) continue; + o->commitment = clampf(o->commitment - (o->leads == PROJECT_SNAILS ? 0.12f : 0.03f), 0.0f, 1.0f); + } + if (doro) Ai_say(doro, dead ? "A fine - and dead snails. That's on all of us." : "A fine. We should have been quicker with the snails."); + } + say(game, game->story_line); +} + void game_story(Game *game, float game_dt) { if (game->arrival.stage == ARRIVAL_DRIVING) return; /* (the clock starts once you're there) */ game->story_hours += game_dt / 3600.0f; + inspection(game); if ((game->story_check -= game->dt) > 0.0f) return; game->story_check = STORY_CHECK; Vec3 path[16]; @@ -49,7 +97,8 @@ void game_story(Game *game, float game_dt) { o->ai.greet = true; /* (over to the fire, hello) */ o->ai.greet_left = HELLO_TIME; o->ai.side = (float)(come % 3 - 1); - Ai_say(o, come == 0 ? "Hello everybody - we're here!" : "Hi! Where's the fire?"); + if (c->leads == PROJECT_SNAILS) Ai_say(o, "Hi all! Mind the vineyard snails - they're protected, and the office comes soon!"); + else Ai_say(o, come == 0 ? "Hello everybody - we're here!" : "Hi! Where's the fire?"); size_t len = 0; while (names[len]) len++; snprintf(names + len, sizeof names - len, "%s%s", come ? " and " : "", c->name); @@ -135,6 +184,15 @@ int Game_crewLines(const Game *game, char lines[][128], int max, const char **ti } } char built[112] = ""; + const World *w = &game->world; + if (p == PROJECT_MOWING) snprintf(built, sizeof built, " [%d%% mown]", (int)(Field_done(w) * 100.0f)); + if (p == PROJECT_SNAILS) { + if (w->inspected) snprintf(built, sizeof built, " [safe %d, dead %d, office: been]", w->snails_rescued, w->snails_killed); + else snprintf(built, sizeof built, " [safe %d, out %d, dead %d, office in %.0f h]", w->snails_rescued, Field_snailsLeft(w), w->snails_killed, + (double)fmaxf(0.0f, FIELD_INSPECTION - game->story_hours)); + } + if (p == PROJECT_WOOD_ART) snprintf(built, sizeof built, " [%d figure%s; %s]", Field_artPieces(w), Field_artPieces(w) == 1 ? "" : "s", + Field_artLog(w) ? "a log there" : "no log"); 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) { diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c index f3a5215..4a366a6 100644 --- a/entropie/src/game/save.c +++ b/entropie/src/game/save.c @@ -76,7 +76,7 @@ static const Field PERSON[] = { BO("rain_nap", rain_nap), RE("riding", riding), IN("ride_seat", ride_seat), FL("indoors", indoors), BO("lives_here", lives_here), IN("ai_mode", ai.mode), IN("ai_order", ai.order), VE("ai_area", ai.work_area), VE("ai_home", ai.home), RE("ai_woodpile", ai.woodpile), BY("ai_banned", ai.banned), BY("ai_defies", ai.defies), BO("ai_unload", ai.unload), - BO("ai_greet", ai.greet), FL("willing", ai.character.willing), + BO("ai_greet", ai.greet), FL("willing", ai.character.willing), IN("snails", snails), }; #define COUNT(a) ((int)(sizeof(a) / sizeof((a)[0]))) @@ -187,6 +187,9 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err, fprintf(f, "told %d %d\n", game->rain_told, game->wet_told); fprintf(f, "story %.9g\n", (double)game->story_hours); fprintf(f, "cash %.9g\n", (double)w->cash); + fprintf(f, "field %d %d %d %d ", w->inspected, w->snails_rescued, w->snails_killed, game->snail_warned); + for (int c = 0; c < WORLD_FIELD_CELLS; c++) fprintf(f, "%02x", w->mown[c]); + fprintf(f, "\n"); const Errand *e = &game->errand; if (e->stage != ERRAND_NONE) fprintf(f, "errand %d %d %d %d %d %d %.9g %.9g\n", e->stage, idOf(&ids, e->driver), e->list[0], e->list[1], e->list[2], e->list[3], @@ -434,7 +437,7 @@ static Object *findScene(World *w, const Rec *r, bool *taken) { static void applyGame(Game *game, const Parsed *p) { World *w = &game->world; for (int i = p->game_first; i < p->game_first + p->game_lines; i++) { - char buf[512], *s = buf; + char buf[2048], *s = buf; /* (the field line is long) */ snprintf(buf, sizeof buf, "%s", p->line[i]); char *key = word(&s); if (!strcmp(key, "time")) game->daycycle.seconds = numF(&s); @@ -453,6 +456,15 @@ static void applyGame(Game *game, const Parsed *p) { else if (!strcmp(key, "story")) game->story_hours = numF(&s); else if (!strcmp(key, "cash")) game->world.cash = numF(&s); else if (!strcmp(key, "guide")) game->arrival.guide = numI(&s); + else if (!strcmp(key, "field")) { + w->inspected = numI(&s) != 0; w->snails_rescued = numI(&s); w->snails_killed = numI(&s); game->snail_warned = numI(&s); + while (*s == ' ') s++; + for (int c = 0; c < WORLD_FIELD_CELLS && s[0] && s[1]; c++, s += 2) { + char hex[3] = { s[0], s[1], 0 }; + w->mown[c] = (unsigned char)strtol(hex, NULL, 16); + } + w->mow_rev++; + } else if (!strcmp(key, "errand")) { Errand *e = &game->errand; e->stage = (ErrandStage)numI(&s); e->driver = byId(p, numI(&s)); diff --git a/entropie/src/game/sound.c b/entropie/src/game/sound.c index 2cfb836..0fefc2d 100644 --- a/entropie/src/game/sound.c +++ b/entropie/src/game/sound.c @@ -202,6 +202,9 @@ void Sound_update(Game *game, float dt, bool paused) { if (ws->tool == TOOL_CHAINSAW && Tools_held(o, NULL) == TOOL_CHAINSAW && (Tools_isWorking(o) || ws->saw_in > 0.05f)) { g = hear(&ear, o->position, &pan, NULL); if (g > saw_g) { saw_g = g; saw_pan = pan; saw_k = ws->saw_in; } + } else if (o->carving && Tools_held(o, NULL) == TOOL_CHAINSAW) { /* (Kerry carving: in and out of the wood) */ + g = hear(&ear, o->position, &pan, NULL); + if (g > saw_g) { saw_g = g; saw_pan = pan; saw_k = 0.5f + 0.4f * sinf(o->body.clock * 1.7f); } } if (o->spraying) { g = hear(&ear, o->position, &pan, NULL); diff --git a/entropie/src/people/ai_help.c b/entropie/src/people/ai_help.c index 7564ef9..fe76d35 100644 --- a/entropie/src/people/ai_help.c +++ b/entropie/src/people/ai_help.c @@ -391,10 +391,10 @@ bool ai_putAway(Object *self, Object *const *objects, int count, float dt) { for (int h = 0; h < 2; h++) { const Object *it = self->hands.item[h]; if (self->hands.state[h] == HAND_HOLDING && it && it->has_home && - (it->type == OBJ_AXE || it->type == OBJ_SHEARS || it->type == OBJ_CHAINSAW || + (it->type == OBJ_AXE || it->type == OBJ_SHEARS || it->type == OBJ_CHAINSAW || it->type == OBJ_SCYTHE || (it->type == OBJ_UMBRELLA && g_world && g_world->rain < 0.02f))) hand = h; /* (an umbrella once the rain's over) */ } - if (hand < 0 || ai->talking) return false; + if (hand < 0 || ai->talking || ai->on_project) return false; /* (at their side job: it's what they work with) */ Object *tool = self->hands.item[hand]; float d = flatDistance(tool->home_spot, self->position); if (ai_stackCare(self) < PUT_AWAY_CARE || d > PUT_AWAY_FAR) { /* down it goes, right here */ diff --git a/entropie/src/people/ai_internal.h b/entropie/src/people/ai_internal.h index 47c12f7..409298c 100644 --- a/entropie/src/people/ai_internal.h +++ b/entropie/src/people/ai_internal.h @@ -107,6 +107,11 @@ bool ai_goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int void ai_commitment(Object *self, Object *const *objects, int count, float dt); bool ai_projectWork(Object *self, Object *const *objects, int count, float dt); bool ai_errand(Object *self, Object *const *objects, int count, float dt); +/* ai_jobs.c: the side jobs (PROJECT_MOWING, _SNAILS, _WOOD_ART): one frame of the work. Returns SIDE_*; why: what to say. */ +enum { SIDE_HELD = -2, SIDE_FINISHED = -1, SIDE_WORKING = 0, SIDE_STEP = 1 }; +int ai_job(Object *self, int project, Object *const *objects, int count, float dt, char *why, size_t size); +bool ai_jobDone(int project); +const char *ai_jobStart(int project); static inline float random01(Ai *ai) { ai->rng = ai->rng * 1664525u + 1013904223u; diff --git a/entropie/src/people/ai_jobs.c b/entropie/src/people/ai_jobs.c new file mode 100644 index 0000000..92b2cfd --- /dev/null +++ b/entropie/src/people/ai_jobs.c @@ -0,0 +1,214 @@ +/* + * The side jobs, one frame of the work at a time (ai_project.c decides when + * they're at it: working hours, breaks, how into it they are): + * + * - Mowing the Kartoffelacker (Toni): the scythe fetched, then cell by cell + * along the field, a swing at a time (world/field.c). A snail in the grass + * there is dead. + * - The snail rescue (Doro, Stella): from snail to snail, bending down for + * each, into the bucket; full (or none left near), over to the refuge by + * the wood and let go. + * - Kerry's art: a log lying at the art spot and the chainsaw - he carves it + * into a figure; without either he says what he needs. + */ + +#include <stdio.h> + +#include "people/ai_internal.h" +#include "world/field.h" +#include "world/site.h" +#include "world/stage.h" + +#define MOW_REACH 1.2f /* m from the cell: mowing it from here */ +#define MOW_SWING 1.5f /* real s a swing of the scythe (quicker for the quick) */ +#define MOW_RADIUS 0.8f /* m: what a swing takes */ +#define MOW_CAREFUL 0.45f /* how careful (ai_stackCare): the snails out of the field before they mow */ +#define SNAIL_LOOK 30.0f /* m: snails this near are gone for (none: to the refuge with what's in the bucket) */ +#define SNAIL_BEND 1.2f /* s bending down for one */ +#define REFUGE_NEAR 1.8f /* m: at the refuge */ +#define CARVE_SIDE 0.8f /* m out from the log's middle: where Kerry stands */ + +/* Something of that kind in their hands. */ +static Object *heldOf(const Object *self, ObjectType type) { + for (int h = 0; h < 2; h++) { + Object *it = self->hands.item[h]; + if (it && it->type == type && self->hands.state[h] == HAND_HOLDING) return it; + } + return NULL; +} + +static bool takenBy(const Object *self, const Object *it, Object *const *objects, int count) { + for (int i = 0; i < count; i++) + if (objects[i] != self && objects[i]->is_character && objects[i]->ai.fetch == it) return true; + return false; +} + +/* The nearest of a kind lying about (not held, nobody else after it). */ +static Object *nearestFree(const Object *self, Object *const *objects, int count, ObjectType type, int variant, float within) { + Object *best = NULL; + float best_d = within; + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o->type != type || o->is_held || o->broken || (variant >= 0 && o->variant != variant)) continue; + float d = flatDistance(o->position, self->position); + if (d < best_d && !takenBy(self, o, objects, count)) { best_d = d; best = o; } + } + return best; +} + +/* Over to it and up it comes (true: in the hands). */ +static bool take(Object *self, Object *it, Object *const *objects, int count, float dt) { + for (int h = 0; h < 2; h++) if (self->hands.item[h] == it) return self->hands.state[h] == HAND_HOLDING; + if (Hands_gapFrom(self->position, self->look.height, it) < PICKUP_REACH) { + stand(self); + turnTowards(self, vec3_sub(it->position, self->position), dt); + freeHands(self, NULL); + if (!Hands_pickUpObject(self, self->main_hand, it) && !Hands_pickUpObject(self, 1 - self->main_hand, it)) self->ai.fetch = NULL; + return false; + } + ai_goToPoint(self, it->position, 0.3f, objects, count, dt); + return false; +} + +static float handy(const Object *self) { + return (0.6f + 0.8f * 0.5f * (self->skill[ORDER_FIREWOOD] + self->skill[ORDER_TREES])) * self->tempo * (1.0f - 0.5f * fminf(self->drunk, 1.0f)); +} + +/* ---------- The scythe ---------- */ + +static int mow(Object *self, Object *const *objects, int count, float dt, char *why, size_t size) { + Ai *ai = &self->ai; + if (Field_done(g_world) >= 1.0f) { snprintf(why, size, "The Kartoffelacker's mown! Looks like a festival ground now."); return SIDE_FINISHED; } + if (ai_stackCare(self) >= MOW_CAREFUL && Field_snailsInField(g_world) > 0 && !g_world->inspected) { /* (the careful: the snails out first) */ + snprintf(why, size, "I'll wait with the mowing till the snails are out of the field - %d still in the grass.", Field_snailsInField(g_world)); + return SIDE_HELD; + } + Object *scythe = heldOf(self, OBJ_SCYTHE); + if (!scythe) { /* the scythe first */ + Object *s = ai->fetch && ai->fetch->type == OBJ_SCYTHE && !ai->fetch->is_held ? ai->fetch : nearestFree(self, objects, count, OBJ_SCYTHE, -1, 400.0f); + if (!s) { snprintf(why, size, "Who's got the scythe? I can't mow without it."); return SIDE_HELD; } + if (ai->fetch != s && ai->speech_time <= 0.0f) Ai_say(self, "Scythe first - it's in the tool room."); + ai->fetch = s; + take(self, s, objects, count, dt); + return SIDE_WORKING; + } + if (ai->fetch == scythe) ai->fetch = NULL; + Vec3 cell; + if (!Field_nextUnmown(g_world, self->position, &cell)) return SIDE_FINISHED; + if (flatDistance(cell, self->position) > MOW_REACH) { + ai_goToPoint(self, cell, MOW_REACH * 0.8f, objects, count, dt); + return SIDE_WORKING; + } + stand(self); + turnTowards(self, vec3_sub(cell, self->position), dt); + float swing = MOW_SWING / fmaxf(self->tempo, 0.5f); + ai->fetch_time += dt; + self->mowing = 0.001f + 3.1416f * ai->fetch_time / swing; /* (the blade across, right to left) */ + if (ai->fetch_time < swing) return SIDE_WORKING; + ai->fetch_time = 0.0f; + float before = Field_done(g_world); + int killed = 0; + Field_mow(g_world, vec3_add(self->position, vec3_scale(Object_facing(self), 0.9f)), MOW_RADIUS, &killed); + World_noise(g_world, NOISE_RUSTLE, self->position, 0.4f); + float after = Field_done(g_world); + if (killed) { snprintf(why, size, "Oh no - I got a snail with the scythe! They should be out of here first."); return SIDE_STEP; } + if ((int)(after * 4.0f) > (int)(before * 4.0f) && after < 1.0f) { + snprintf(why, size, "%d%% of the Kartoffelacker mown.", (int)(after * 4.0f) * 25); + return SIDE_STEP; + } + return SIDE_WORKING; +} + +/* ---------- The snails ---------- */ + +static int snails(Object *self, Object *const *objects, int count, float dt, char *why, size_t size) { + Ai *ai = &self->ai; + Object *sn = ai->fetch && ai->fetch->type == OBJ_SNAIL && !ai->fetch->broken ? ai->fetch : nearestFree(self, objects, count, OBJ_SNAIL, 0, SNAIL_LOOK); + if (self->snails >= SNAIL_BUCKET || (self->snails > 0 && !sn)) { /* the bucket over to the refuge */ + ai->fetch = NULL; + Vec3 refuge = Site_snailRefuge(); + if (!ai_goToPoint(self, refuge, REFUGE_NEAR, objects, count, dt)) return SIDE_WORKING; + int n = Field_release(g_world, self); + snprintf(why, size, "There you go, %d of you - safe here.", n); + return SIDE_STEP; + } + if (!sn) { /* none near: further off, or all done */ + sn = nearestFree(self, objects, count, OBJ_SNAIL, 0, 1000.0f); + if (!sn) { snprintf(why, size, "That's all the snails out. The office can come!"); return SIDE_FINISHED; } + } + if (ai->fetch != sn) ai->fetch_time = 0.0f; + ai->fetch = sn; + if (flatDistance(sn->position, self->position) > 0.55f) { + ai_goToPoint(self, sn->position, 0.45f, objects, count, dt); + return SIDE_WORKING; + } + stand(self); + turnTowards(self, vec3_sub(sn->position, self->position), dt); + self->body.crouch_goal = fmaxf(self->body.crouch_goal, 0.85f); /* (down to it) */ + Hands_touch(self, vec3_add(sn->position, vec3(0.0f, 0.04f, 0.0f))); + if ((ai->fetch_time += dt) < SNAIL_BEND) return SIDE_WORKING; + Field_pickSnail(g_world, self, sn); + ai->fetch = NULL; + ai->fetch_time = 0.0f; + return SIDE_WORKING; +} + +/* ---------- Kerry's art ---------- */ + +static int carve(Object *self, Object *const *objects, int count, float dt, char *why, size_t size) { + Ai *ai = &self->ai; + Object *log = Field_artLog(g_world); + if (!log) { snprintf(why, size, "Bring me a log to the yard by the pavilions - I'll make something of it!"); return SIDE_HELD; } + Object *saw = heldOf(self, OBJ_CHAINSAW); + if (!saw) { + Object *c = ai->fetch && ai->fetch->type == OBJ_CHAINSAW && !ai->fetch->is_held ? ai->fetch : nearestFree(self, objects, count, OBJ_CHAINSAW, -1, 400.0f); + if (!c) { snprintf(why, size, "Somebody's got the chainsaw - I need it for the art."); return SIDE_HELD; } + ai->fetch = c; + take(self, c, objects, count, dt); + return SIDE_WORKING; + } + if (ai->fetch == saw) ai->fetch = NULL; + Vec3 c = Object_colliderCenter(log, 0), along = log->colliders[0].axes[1]; + Vec3 across = vec3_normalize(vec3(-along.z, 0.0f, along.x)); + if (vec3_dot(vec3_sub(self->position, c), across) < 0.0f) across = vec3_scale(across, -1.0f); + Vec3 spot = vec3_add(c, vec3_scale(across, CARVE_SIDE)); + if (!ai_goToPoint(self, spot, 0.35f, objects, count, dt)) return SIDE_WORKING; + stand(self); + turnTowards(self, vec3_sub(c, self->position), dt); + float t = self->body.clock; + Hands_touch(self, vec3_add(c, vec3(0.15f * sinf(t * 1.3f), 0.25f + 0.1f * sinf(t * 0.7f), 0.1f * cosf(t * 1.1f)))); + self->carving = true; + log->build_work += handy(self) * g_world->game_dt / 3600.0f; + if (log->build_work < CARVE_NEED) return SIDE_WORKING; + self->carving = false; + Field_carve(g_world, log); + snprintf(why, size, "Done! What do you think? Another log and I'll do another one."); + return SIDE_STEP; +} + +int ai_job(Object *self, int project, Object *const *objects, int count, float dt, char *why, size_t size) { + switch (project) { + case PROJECT_MOWING: return mow(self, objects, count, dt, why, size); + case PROJECT_SNAILS: return snails(self, objects, count, dt, why, size); + case PROJECT_WOOD_ART: return carve(self, objects, count, dt, why, size); + default: return SIDE_FINISHED; + } +} + +bool ai_jobDone(int project) { + if (!g_world) return true; + switch (project) { + case PROJECT_MOWING: return Field_done(g_world) >= 1.0f; + case PROJECT_SNAILS: return Field_snailsLeft(g_world) == 0 || g_world->inspected; + default: return false; /* (Kerry: as long as there are logs) */ + } +} + +const char *ai_jobStart(int project) { + switch (project) { + case PROJECT_MOWING: return "Right - the Kartoffelacker. Where's my scythe?"; + case PROJECT_SNAILS: return "Snail time! Gently, everybody - they're protected."; + case PROJECT_WOOD_ART: return "Time to make some art. Chainsaw, log, go!"; + default: return "Off to work."; + } +} diff --git a/entropie/src/people/ai_project.c b/entropie/src/people/ai_project.c index a9987da..6c8262e 100644 --- a/entropie/src/people/ai_project.c +++ b/entropie/src/people/ai_project.c @@ -6,6 +6,8 @@ * Held up for bought material that lies somewhere else (by the barn, where the * shopping gets unloaded), one of them goes and carries it over; logs are * yours to bring. Off on a shopping run (game_shop.c): over to the car. + * The side jobs (mowing, the snails, Kerry's art) go the same way, their + * work itself in ai_jobs.c. * How into it they are goes with what happens: a step finished lifts them; * being held up (no logs for the frame), working on worn out, nobody leading, * being annoyed wear it down. Too low, a lead gives up leading ("somebody @@ -45,9 +47,14 @@ 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; +/* Something to go and do on the ground: a floor, or a side job (ai_jobs.c). */ +static bool workable(int p) { + return floorProject(p) || p == PROJECT_MOWING || p == PROJECT_SNAILS || p == PROJECT_WOOD_ART; +} + +static int theirProject(const Object *self) { + if (workable(self->leads)) return self->leads; + if (workable(self->helps)) return self->helps; return PROJECT_NONE; } @@ -62,7 +69,7 @@ static bool firstAt(const Object *self, Object *const *objects, int count, int p 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 == self || !o->is_character || !o->ai.on_project || theirProject(o) != project) continue; if (o->leads == project || o->cast_id < self->cast_id) return false; } return true; @@ -80,7 +87,7 @@ void ai_commitment(Object *self, Object *const *objects, int count, float dt) { 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 (ai->on_project && self->leads != theirProject(self) && !leadOf(objects, count, theirProject(self))) lose(self, WEAR_NO_LEAD, gdt); if (self->sitting && (g_world->hour > 19.0f || g_world->hour < 1.0f) && Camp_fireNear(objects, count, self->position, 5.0f, true)) self->commitment = fminf(1.0f, self->commitment + LIFT_EVENING * gdt / 3600.0f); (void)dt; @@ -118,7 +125,7 @@ static bool stop(Object *self, const char *say) { /* Others at work on it with them. */ static bool othersAt(const Object *self, Object *const *objects, int count, int project) { for (int i = 0; i < count; i++) - if (objects[i] != self && objects[i]->is_character && objects[i]->ai.on_project && theirFloor(objects[i]) == project) return true; + if (objects[i] != self && objects[i]->is_character && objects[i]->ai.on_project && theirProject(objects[i]) == project) return true; return false; } @@ -220,14 +227,17 @@ 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; + int project = theirProject(self); + bool floor = floorProject(project); + Object *stage = floor ? Stage_of(objects, count, (Project)project) : NULL; + self->carving = false; + self->mowing = 0.0f; ai->project_off = fmaxf(0.0f, ai->project_off - gdt); - if (!stage || stage->build_stage >= BUILD_DONE) return stop(self, NULL); + if (!project || (floor && (!stage || stage->build_stage >= BUILD_DONE)) || (!floor && ai_jobDone(project) && self->snails == 0)) return stop(self, NULL); /* (snails still in the bucket: let go first) */ ai->remark = fmaxf(0.0f, ai->remark - dt); bool lead = self->leads == project; Object *boss = leadOf(objects, count, project); - bool boss_near = boss && flatDistance(boss->position, stage->position) < LEAD_NEAR; + bool boss_near = boss && flatDistance(boss->position, stage ? stage->position : self->position) < LEAD_NEAR; float hour = g_world->hour; /* not now */ if (hour < WORK_FROM || hour > WORK_TO) return stop(self, lead ? "That's it for today." : NULL); @@ -242,12 +252,36 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) { 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)); + if (floor) snprintf(line, sizeof line, "Off to the %s - %s. Who's coming?", Cast_projectName((Project)project), Stage_stepName(stage->build_stage)); + else snprintf(line, sizeof line, "%s", ai_jobStart(project)); Ai_say(self, line); } } if ((ai->shift += gdt) > SHIFT) return stop(self, lead ? "Break! Back in a bit." : NULL); if (self->sitting) self->sitting = false; /* (up first) */ + if (!floor) { /* a side job */ + char why[AI_SPEECH_MAX] = ""; + int got = ai_job(self, project, objects, count, dt, why, sizeof why); + if (got == SIDE_HELD) { + ai->held_up += gdt; + if (ai->remark <= 0.0f && firstAt(self, objects, count, project) && why[0]) { Ai_say(self, why); ai->remark = REMARK_EVERY; } + if (ai->held_up > HELD_UP_LEAVE) return stop(self, "Can't get on with it like this. Later."); + return true; + } + ai->held_up = 0.0f; + if (got == SIDE_STEP) { + self->commitment = fminf(1.0f, self->commitment + LIFT_DONE * 0.5f); + if (why[0]) Ai_say(self, why); + } + if (got == SIDE_FINISHED) { + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o->is_character && theirProject(o) == project) o->commitment = fminf(1.0f, o->commitment + LIFT_DONE); + } + return stop(self, why[0] ? why : NULL); + } + return true; + } if (fetchMaterial(self, stage, objects, count, dt)) return true; freeHands(self, NULL); float side = ai->side != 0.0f ? ai->side * 0.8f : (lead ? 0.0f : 0.6f); @@ -271,7 +305,7 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) { } if (kind >= 0) { /* (somebody's on the way with it: wait for it) */ for (int i = 0; i < count; i++) - if (objects[i]->is_character && objects[i]->ai.fetch && objects[i]->ai.fetch->variant == kind && theirFloor(objects[i]) == project) return true; + if (objects[i]->is_character && objects[i]->ai.fetch && objects[i]->ai.fetch->variant == kind && theirProject(objects[i]) == project) return true; } ai->held_up += gdt; if (ai->remark <= 0.0f && firstAt(self, objects, count, project)) { @@ -290,7 +324,7 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) { 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 (o->is_character && o->ai.on_project && theirProject(o) == project) o->commitment = fminf(1.0f, o->commitment + LIFT_DONE); } if (firstAt(self, objects, count, project)) Ai_say(self, doneLine(project, got)); } diff --git a/entropie/src/people/hands.c b/entropie/src/people/hands.c index f24ec7a..9f447ff 100644 --- a/entropie/src/people/hands.c +++ b/entropie/src/people/hands.c @@ -92,7 +92,7 @@ static Vec3 rightOf(Vec3 facing) { return vec3_normalize(vec3_cross(facing, UP)) static float scaleOf(const Object *who) { return who->look.height / 1.75f; } static bool liftableOneHand(const Object *o) { - bool tool = o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_LADDER || o->type == OBJ_UMBRELLA; /* long, but made for one hand */ + bool tool = o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_LADDER || o->type == OBJ_UMBRELLA || o->type == OBJ_SCYTHE; /* long, but made for one hand */ return o->weight <= ONE_HAND_MAX_WEIGHT && (tool || largestSize(o) <= ONE_HAND_MAX_SIZE); } @@ -1223,6 +1223,22 @@ void Hands_placeHeld(Object *who) { item->rotation = euler_yxz_from_axes(along, vec3_normalize(vec3_cross(vec3_cross(along, facing), along))); Object_syncColliders(item); center = vec3_add(hand, vec3_scale(along, 0.06f)); + } else if (item->type == OBJ_SCYTHE) { /* carried over the shoulder, the blade up behind; mowing: the blade sweeping the grass in front */ + Vec3 along, right = vec3_normalize(vec3_cross(facing, UP)); + const float s = who->look.height / 1.75f; + if (who->mowing > 0.0f) { + Vec3 bottom = vec3_add(who->position, vec3_add(vec3_scale(facing, 1.05f * s), vec3_scale(right, 0.7f * s * sinf(who->mowing)))); + bottom.y += 0.06f; + along = vec3_normalize(vec3_sub(hand, bottom)); + center = vec3_add(bottom, vec3_scale(along, 0.8f)); + } else { + along = vec3_normalize(vec3_add(vec3_scale(facing, 0.75f), vec3(0.0f, -0.55f, 0.0f))); + center = vec3_sub(hand, vec3_scale(along, 0.15f)); + } + Vec3 side = vec3_cross(along, facing); + Vec3 ahead = vec3_length(side) > 1e-3f ? vec3_normalize(vec3_cross(vec3_normalize(side), along)) : facing; + item->rotation = euler_yxz_from_axes(along, ahead); + Object_syncColliders(item); } else if (Object_isBottle(item)) { /* The hand around the bottle where it grips it */ center = vec3_sub(hand, vec3_scale(item->colliders[0].axes[1], bottleGrip(who, i, item))); @@ -1234,6 +1250,6 @@ void Hands_placeHeld(Object *who) { item->position = vec3_sub(center, item->colliders[0].offset); item->velocity = vec3_zero(); if (!twoHanded(h)) settleInHand(who, i, item); - if (h->drink_hand != i && !(item->type == OBJ_UMBRELLA && item->umbrella_open)) keepOffBody(who, item); + if (h->drink_hand != i && !(item->type == OBJ_UMBRELLA && item->umbrella_open) && item->type != OBJ_SCYTHE) keepOffBody(who, item); } } diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h index ab9ed0a..8825059 100644 --- a/entropie/src/render/shapes.h +++ b/entropie/src/render/shapes.h @@ -165,6 +165,11 @@ 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, 2 a small tent in its bag, 3 a big one. */ Mesh Shapes_gear(int kind, Vec3 color); +/* The scythe (centred, its snath along Y, the blade at the bottom); a vineyard snail (on y = 0); + a log carved into a figure (on y = 0): kind 0 an owl, 1 a bear, 2 a mushroom man. */ +Mesh Shapes_scythe(float length); +Mesh Shapes_snail(void); +Mesh Shapes_sculpture(int kind, float diameter, float height); /* The marker over who or what to go to next: a diamond, centred, this tall. */ Mesh Shapes_questMarker(float height, Vec3 color); /* A party pavilion on y = 0 (centred): side x side, the eaves and the roof's peak this high, its canvas colour. */ diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c index 4c01882..2cf1ed6 100644 --- a/entropie/src/render/shapes_props.c +++ b/entropie/src/render/shapes_props.c @@ -1213,6 +1213,72 @@ Mesh Shapes_gear(int kind, Vec3 color) { return Mesh_fromBuilder(&b); } +/* The scythe (centred, the snath along Y): the long wooden snath with its two grips, the blade at the + bottom end, curving off to one side (+X) and a little forward. */ +Mesh Shapes_scythe(float length) { + MeshBuilder b; + MeshBuilder_init(&b); + const float h = length * 0.5f; + const Vec3 steel = { 0.62f, 0.64f, 0.68f }; + shapes_addTube(&b, -h, 0.018f, 0.016f, length, 8, COLOR_WOOD, true); + for (int k = 0; k < 2; k++) { /* the grips sticking out */ + float y = k ? h * 0.15f : h * 0.75f; + addBlock(&b, vec3(0.0f, y, 0.06f), vec3(0.025f, 0.025f, 0.12f), COLOR_WOOD); + } + for (int k = 0; k < 6; k++) { /* the blade: a curve of flat pieces */ + float t = (float)k / 6.0f, x = 0.06f + 0.6f * t, z = 0.05f + 0.12f * sinf(t * 3.0f) - 0.1f * t; + addBlock(&b, vec3(x, -h + 0.03f, z), vec3(0.11f, 0.008f, 0.07f - 0.05f * t), steel); + } + return Mesh_fromBuilder(&b); +} + +/* A vineyard snail (on y = 0, the head to +Z): the round shell with its whorl, the body under it. */ +Mesh Shapes_snail(void) { + MeshBuilder b; + MeshBuilder_init(&b); + const Vec3 shell = { 0.72f, 0.58f, 0.38f }, line = { 0.48f, 0.36f, 0.22f }, body = { 0.66f, 0.62f, 0.52f }; + addBlock(&b, vec3(0.0f, 0.006f, 0.008f), vec3(0.022f, 0.012f, 0.07f), body); + addBlock(&b, vec3(0.0f, 0.016f, 0.04f), vec3(0.012f, 0.012f, 0.012f), body); /* the head, the feelers */ + addBlock(&b, vec3(0.006f, 0.026f, 0.046f), vec3(0.003f, 0.016f, 0.003f), body); + addBlock(&b, vec3(-0.006f, 0.026f, 0.046f), vec3(0.003f, 0.016f, 0.003f), body); + size_t first = b.vertex_count; + shapes_addTube(&b, -0.016f, 0.022f, 0.022f, 0.032f, 10, shell, true); + shapes_addTube(&b, 0.0165f, 0.012f, 0.012f, 0.003f, 10, line, true); + transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, 0.03f, -0.006f)), mat4_rotate_z(PI_F * 0.5f))); + return Mesh_fromBuilder(&b); +} + +/* Kerry's art: a log stood on end, carved (on y = 0): kind 0 an owl, 1 a bear, 2 a mushroom man. */ +Mesh Shapes_sculpture(int kind, float diameter, float height) { + MeshBuilder b; + MeshBuilder_init(&b); + const float r = diameter * 0.5f; + const Vec3 wood = { 0.70f, 0.52f, 0.32f }, dark = { 0.38f, 0.26f, 0.15f }, bark = { 0.36f, 0.27f, 0.19f }; + shapes_addTube(&b, 0.0f, r * 1.05f, r * 1.0f, 0.12f, 12, bark, false); /* (the foot left rough) */ + if (kind == 0) { /* an owl: a body narrowing up, a round head, the ear tufts, two big eyes */ + shapes_addTube(&b, 0.12f, r, r * 0.8f, height * 0.6f, 12, wood, false); + shapes_addTube(&b, 0.12f + height * 0.6f, r * 0.85f, r * 0.75f, height * 0.28f - 0.12f, 12, wood, true); + for (int s = -1; s <= 1; s += 2) { + addBlock(&b, vec3((float)s * r * 0.45f, height - 0.06f, 0.0f), vec3(r * 0.25f, 0.14f, r * 0.3f), wood); + addBlock(&b, vec3((float)s * r * 0.33f, height * 0.78f, r * 0.72f), vec3(r * 0.32f, r * 0.32f, 0.03f), dark); + } + addBlock(&b, vec3(0.0f, height * 0.7f, r * 0.8f), vec3(r * 0.14f, r * 0.24f, 0.05f), dark); /* the beak */ + } else if (kind == 1) { /* a bear sitting up: a big belly, the head, ears, the snout */ + shapes_addTube(&b, 0.12f, r, r * 0.9f, height * 0.55f, 12, wood, false); + shapes_addTube(&b, 0.12f + height * 0.55f, r * 0.75f, r * 0.6f, height * 0.33f - 0.12f, 12, wood, true); + for (int s = -1; s <= 1; s += 2) addBlock(&b, vec3((float)s * r * 0.5f, height * 0.9f, 0.0f), vec3(r * 0.25f, r * 0.25f, r * 0.2f), wood); + addBlock(&b, vec3(0.0f, height * 0.72f, r * 0.62f), vec3(r * 0.35f, r * 0.28f, r * 0.3f), wood); + addBlock(&b, vec3(0.0f, height * 0.74f, r * 0.8f), vec3(r * 0.12f, r * 0.1f, 0.03f), dark); + addBlock(&b, vec3(0.0f, height * 0.35f, r * 0.9f), vec3(r * 1.1f, r * 0.25f, r * 0.2f), wood); /* the paws */ + } else { /* a mushroom man: a stem with a face, a wide cap */ + shapes_addTube(&b, 0.12f, r * 0.8f, r * 0.7f, height * 0.62f, 12, wood, false); + shapes_addTube(&b, 0.12f + height * 0.62f, r * 1.5f, r * 0.2f, height * 0.38f - 0.12f, 12, dark, true); + for (int s = -1; s <= 1; s += 2) addBlock(&b, vec3((float)s * r * 0.25f, height * 0.5f, r * 0.7f), vec3(r * 0.12f, r * 0.12f, 0.03f), dark); + addBlock(&b, vec3(0.0f, height * 0.38f, r * 0.68f), vec3(r * 0.4f, r * 0.06f, 0.03f), dark); + } + return Mesh_fromBuilder(&b); +} + /* The marker over who to go to next: a diamond (two pyramids), centred, this tall. */ Mesh Shapes_questMarker(float height, Vec3 color) { MeshBuilder b; diff --git a/entropie/src/world/field.c b/entropie/src/world/field.c new file mode 100644 index 0000000..b7eeed9 --- /dev/null +++ b/entropie/src/world/field.c @@ -0,0 +1,184 @@ +#include "world/field.h" +#include "world/site.h" +#include "world/cast.h" + +#include <math.h> +#include <string.h> + +_Static_assert(FIELD_CELLS == WORLD_FIELD_CELLS, "world.h WORLD_FIELD_CELLS"); + +#define REFUGE_SPREAD 2.5f /* m: let-go snails round the refuge */ +#define STAGE_CLEAR 3.6f /* m round the stage: no grass to mow there */ + +static unsigned mix(unsigned h) { + h ^= h >> 16; h *= 0x7feb352du; h ^= h >> 15; h *= 0x846ca68bu; h ^= h >> 16; + return h; +} +static float dice(unsigned k) { return (float)(mix(k) & 0xffffu) / 65535.0f; } + +static int cellOf(float x, float z) { + int ix = (int)floorf((x - FIELD_X0) / FIELD_CELL), iz = (int)floorf((z - FIELD_Z0) / FIELD_CELL); + return ix < 0 || iz < 0 || ix >= FIELD_NX || iz >= FIELD_NZ ? -1 : iz * FIELD_NX + ix; +} + +static Vec3 middleOf(const World *world, int c) { + float x = FIELD_X0 + ((float)(c % FIELD_NX) + 0.5f) * FIELD_CELL, z = FIELD_Z0 + ((float)(c / FIELD_NX) + 0.5f) * FIELD_CELL; + return vec3(x, world->has_terrain ? Terrain_height(&world->terrain, x, z) : 0.0f, z); +} + +bool Field_inside(float x, float z) { return cellOf(x, z) >= 0; } + +float Field_mownAt(const World *world, float x, float z) { + int c = cellOf(x, z); + if (c < 0) return 0.0f; + return world->mown[c] == FIELD_OFF ? 1.0f : (float)world->mown[c] / (float)FIELD_MOWN; +} + +static bool snailWild(const Object *o) { return o->type == OBJ_SNAIL && o->variant == 0 && !o->broken && !o->is_held; } + +int Field_mow(World *world, Vec3 at, float radius, int *killed) { + int done = 0, dead = 0; + for (int dz = -2; dz <= 2; dz++) + for (int dx = -2; dx <= 2; dx++) { + int c = cellOf(at.x + (float)dx * FIELD_CELL, at.z + (float)dz * FIELD_CELL); + if (c < 0 || world->mown[c] >= FIELD_MOWN) continue; + Vec3 m = middleOf(world, c); + if (hypotf(m.x - at.x, m.z - at.z) > radius + FIELD_CELL * 0.5f) continue; + if (++world->mown[c] >= FIELD_MOWN) done++; + world->mow_rev++; + for (int i = 0; i < world->count; i++) { /* (a snail in the grass there: too late) */ + Object *o = world->objects[i]; + if (!snailWild(o) || cellOf(o->position.x, o->position.z) != c) continue; + Object_useUp(o); + world->snails_killed++; + dead++; + } + } + if (killed) *killed = dead; + return done; +} + +float Field_done(const World *world) { + int all = 0, mown = 0; + for (int c = 0; c < FIELD_CELLS; c++) { + if (world->mown[c] == FIELD_OFF) continue; + all++; + if (world->mown[c] >= FIELD_MOWN) mown++; + } + return all ? (float)mown / (float)all : 1.0f; +} + +bool Field_nextUnmown(const World *world, Vec3 from, Vec3 *out) { + int best = -1; + float best_d = 1e9f; + for (int c = 0; c < FIELD_CELLS; c++) { + if (world->mown[c] >= FIELD_MOWN) continue; + Vec3 m = middleOf(world, c); + float d = hypotf(m.x - from.x, m.z - from.z) + 0.3f * (float)(c / FIELD_NX); /* (row by row, roughly) */ + if (d < best_d) { best_d = d; best = c; } + } + if (best < 0) return false; + *out = middleOf(world, best); + return true; +} + +static bool addSnail(World *world, Vec3 at, int variant) { + at.y = World_groundHeight(world, at.x, at.z, at.y + 2.0f) + 0.02f; + Object *s = Object_create(OBJ_SNAIL, PALETTE_WARM, at, vec3_zero(), (float)variant); + if (!s) return false; + s->rotation.y = 6.2832f * dice((unsigned)(at.x * 977.0f) ^ (unsigned)(at.z * 131.0f)); + Object_syncColliders(s); + return World_add(world, s); +} + +bool Field_prepare(World *world) { + memset(world->mown, 0, sizeof world->mown); + for (int i = 0; i < world->count; i++) { /* (under and round the stage: nothing to mow) */ + const Object *s = world->objects[i]; + if (s->type != OBJ_STAGE) continue; + for (int c = 0; c < FIELD_CELLS; c++) { + Vec3 m = middleOf(world, c); + if (hypotf(m.x - s->position.x, m.z - s->position.z) < STAGE_CLEAR) world->mown[c] = FIELD_OFF; + } + } + for (int k = 0; k < FIELD_SNAILS; k++) { /* the snails: about the field ... */ + float x = FIELD_X0 + 0.5f + dice(1000u + (unsigned)k) * ((float)FIELD_NX * FIELD_CELL - 1.0f); + float z = FIELD_Z0 + 0.5f + dice(2000u + (unsigned)k) * ((float)FIELD_NZ * FIELD_CELL - 1.0f); + int c = cellOf(x, z); + if (c >= 0 && world->mown[c] == FIELD_OFF) continue; + if (!addSnail(world, vec3(x, 0.0f, z), 0)) return false; + } + Vec3 orchard = Site_floorSpot(PROJECT_OBSTWIESE); /* ... and in the orchard, round where its stage goes */ + for (int k = 0; k < ORCHARD_SNAILS; k++) { + float a = 6.2832f * dice(3000u + (unsigned)k), r = 4.5f + 6.0f * dice(4000u + (unsigned)k); + if (!addSnail(world, vec3(orchard.x + cosf(a) * r, orchard.y, orchard.z + sinf(a) * r), 0)) return false; + } + return true; +} + +int Field_snailsInField(const World *world) { + int n = 0; + for (int i = 0; i < world->count; i++) + if (snailWild(world->objects[i]) && Field_inside(world->objects[i]->position.x, world->objects[i]->position.z)) n++; + return n; +} + +int Field_snailsLeft(const World *world) { + int n = 0; + for (int i = 0; i < world->count; i++) if (snailWild(world->objects[i])) n++; + return n; +} + +bool Field_pickSnail(World *world, Object *who, Object *snail) { + if (!snailWild(snail) || who->snails >= SNAIL_BUCKET) return false; + (void)world; + Object_useUp(snail); + who->snails++; + return true; +} + +int Field_release(World *world, Object *who) { + int n = who->snails; + if (n <= 0) return 0; + Vec3 refuge = Site_snailRefuge(); + for (int k = 0; k < n; k++) { + unsigned seed = (unsigned)(world->snails_rescued + k) * 7919u; + float a = 6.2832f * dice(seed), r = REFUGE_SPREAD * sqrtf(dice(seed + 1u)); + addSnail(world, vec3(refuge.x + cosf(a) * r, refuge.y, refuge.z + sinf(a) * r), 1); + } + world->snails_rescued += n; + who->snails = 0; + return n; +} + +Object *Field_artLog(const World *world) { + Vec3 spot = Site_artSpot(); + Object *best = NULL; + float best_d = ART_REACH; + for (int i = 0; i < world->count; i++) { + Object *o = world->objects[i]; + if (o->type != OBJ_LOG || o->is_held || o->lifted || o->broken || o->size.y < 0.8f || o->size.x < 0.2f) continue; + float d = hypotf(o->position.x - spot.x, o->position.z - spot.z); + if (d < best_d) { best_d = d; best = o; } + } + return best; +} + +Object *Field_carve(World *world, Object *log) { + Vec3 at = Object_colliderCenter(log, 0); + float d = log->size.x, h = fminf(fmaxf(log->size.y, 0.9f), 1.7f); + at.y = World_groundHeight(world, at.x, at.z, at.y + 1.0f); + Object *art = Object_create(OBJ_SCULPTURE, PALETTE_WARM, at, vec3(d, h, d), (float)(Field_artPieces(world) % 3)); + if (!art) return NULL; + art->rotation.y = log->rotation.y; + Object_syncColliders(art); + if (!World_add(world, art)) { Object_destroy(art); return NULL; } + Object_useUp(log); + return art; +} + +int Field_artPieces(const World *world) { + int n = 0; + for (int i = 0; i < world->count; i++) if (world->objects[i]->type == OBJ_SCULPTURE && !world->objects[i]->broken) n++; + return n; +} diff --git a/entropie/src/world/field.h b/entropie/src/world/field.h new file mode 100644 index 0000000..e75fc79 --- /dev/null +++ b/entropie/src/world/field.h @@ -0,0 +1,63 @@ +#ifndef WORLD_FIELD_H +#define WORLD_FIELD_H + +/* + * The side jobs on the ground (see the story, roadmap 4): + * + * - The Kartoffelacker's grass: knee high, mown with the scythe (the ground + * is too uneven for a mower). The field is cut into cells + * (World.mown: 0 standing .. FIELD_MOWN mown, FIELD_OFF not to be mown: + * under the stage); each swing over a cell takes it a step down. + * - The vineyard snails (OBJ_SNAIL, protected): in the field and round the + * Obstwiese. Picked up into a bucket (Object.snails) and let go at the + * refuge by the strip of wood; mown over, a snail is dead. The regulatory + * office comes at FIELD_INSPECTION hours on the ground (game_story.c). + * - Kerry's wood art: a log lying at the art spot, carved with the chainsaw + * into a figure (OBJ_SCULPTURE). + */ + +#include "world/world.h" + +#define FIELD_X0 -25.6f /* the mown area, on the ground (world x, z) ... */ +#define FIELD_Z0 89.5f +#define FIELD_CELL 1.0f /* ... in cells this wide */ +#define FIELD_NX 20 +#define FIELD_NZ 26 +#define FIELD_CELLS (FIELD_NX * FIELD_NZ) +#define FIELD_MOWN 3 /* swings over a cell till it's mown */ +#define FIELD_OFF 255 /* (not to be mown) */ +#define FIELD_SNAILS 30 /* snails in the field ... */ +#define ORCHARD_SNAILS 10 /* ... and round the Obstwiese */ +#define SNAIL_BUCKET 8 /* in one bucket at most */ +#define FIELD_INSPECTION 44.0f /* hours on the ground: the regulatory office comes */ +#define ART_REACH 5.0f /* m round the art spot: a log here is Kerry's */ +#define CARVE_NEED 2.5f /* person-hours to carve a log into a figure */ + +/* Is (x, z) in the field, and how far is it mown there (0..1)? */ +bool Field_inside(float x, float z); +float Field_mownAt(const World *world, float x, float z); +/* A swing of the scythe: cells within radius of at a step further mown. Returns how many got + mown just now; snails there are dead (*killed, may be NULL). */ +int Field_mow(World *world, Vec3 at, float radius, int *killed); +/* 0..1 how much of it is mown. */ +float Field_done(const World *world); +/* The nearest cell still to be mown (its middle on the ground), false: all mown. */ +bool Field_nextUnmown(const World *world, Vec3 from, Vec3 *out); +/* At the start: the cells under the stage left out, the snails put out. */ +bool Field_prepare(World *world); + +/* Snails still out where the building goes (not rescued, alive). */ +int Field_snailsLeft(const World *world); +/* ... of them in the Kartoffelacker's grass. */ +int Field_snailsInField(const World *world); +/* who is at the refuge with snails in the bucket: let go there. Returns how many. */ +int Field_release(World *world, Object *who); +/* Picked up: into who's bucket (false: full). */ +bool Field_pickSnail(World *world, Object *who, Object *snail); + +/* Kerry's art: a log lying at the art spot (not yet carved), or NULL; and done: the log a figure. */ +Object *Field_artLog(const World *world); +Object *Field_carve(World *world, Object *log); +int Field_artPieces(const World *world); + +#endif /* WORLD_FIELD_H */ diff --git a/entropie/src/world/meadow.c b/entropie/src/world/meadow.c index f01a980..8057afa 100644 --- a/entropie/src/world/meadow.c +++ b/entropie/src/world/meadow.c @@ -1,4 +1,5 @@ #include "world/meadow.h" +#include "world/field.h" #include "world/site.h" #include <string.h> @@ -33,12 +34,14 @@ static Mesh buildChunk(const World *world, int cx, int cz) { float x = x0 + ((float)ix + hash(gx, gz, 1)) * TUFT_SPACING; float z = z0 + ((float)iz + hash(gx, gz, 2)) * TUFT_SPACING; if (fabsf(x) > TERRAIN_SIZE * 0.5f - 0.5f || fabsf(z) > TERRAIN_SIZE * 0.5f - 0.5f) continue; - float grow = Site_grass(x, z); + bool field = Field_inside(x, z); /* (the Kartoffelacker: knee high till it's mown) */ + float grow = field ? fmaxf(Site_grass(x, z), 0.9f) : Site_grass(x, z); if (hash(gx, gz, 3) > grow) continue; float ground = Terrain_baseHeight(&world->terrain, x, z); if (covered(world, x, z, ground)) continue; Vec3 color = Site_groundColor(x, z, Terrain_grassColor(x, z)); float tall = (0.10f + 0.18f * hash(gx, gz, 4)) * (0.6f + 0.4f * grow); + if (field) tall = lerpf(0.38f + 0.3f * hash(gx, gz, 4), 0.05f + 0.04f * hash(gx, gz, 4), Field_mownAt(world, x, z)); for (int k = 0; k < BLADES; k++) { float a = 6.2832f * hash(gx, gz, 10 + k), lean = 0.06f + 0.1f * hash(gx, gz, 20 + k); float bx = x + (hash(gx, gz, 30 + k) - 0.5f) * 0.3f, bz = z + (hash(gx, gz, 40 + k) - 0.5f) * 0.3f; @@ -63,8 +66,22 @@ static Mesh buildChunk(const World *world, int cx, int cz) { return Mesh_fromBuilder(&b); } +/* Does this square reach into the Kartoffelacker? */ +static bool overField(int cx, int cz) { + const float x0 = -TERRAIN_SIZE * 0.5f + (float)cx * MEADOW_CHUNK, z0 = -TERRAIN_SIZE * 0.5f + (float)cz * MEADOW_CHUNK; + return x0 < FIELD_X0 + FIELD_NX * FIELD_CELL && x0 + MEADOW_CHUNK > FIELD_X0 && z0 < FIELD_Z0 + FIELD_NZ * FIELD_CELL && z0 + MEADOW_CHUNK > FIELD_Z0; +} + +#define MOW_REBUILD 20 /* frames between building the field's squares again while it's being mown */ + void Meadow_draw(Meadow *m, const World *world, Renderer *r, Vec3 eye) { int builds = 0; + if (world->mow_rev != m->mow_rev && ++m->mow_wait >= MOW_REBUILD) { /* (mown a bit more: its grass again) */ + m->mow_rev = world->mow_rev; + m->mow_wait = 0; + for (int i = 0; i < MEADOW_SIDE * MEADOW_SIDE; i++) + if (m->built[i] && overField(i % MEADOW_SIDE, i / MEADOW_SIDE)) { Mesh_destroy(&m->chunks[i]); m->built[i] = false; } + } for (int cz = 0; cz < MEADOW_SIDE; cz++) { for (int cx = 0; cx < MEADOW_SIDE; cx++) { int i = cz * MEADOW_SIDE + cx; diff --git a/entropie/src/world/meadow.h b/entropie/src/world/meadow.h index 8d4a693..1e84824 100644 --- a/entropie/src/world/meadow.h +++ b/entropie/src/world/meadow.h @@ -19,6 +19,7 @@ typedef struct { Mesh chunks[MEADOW_SIDE * MEADOW_SIDE]; bool built[MEADOW_SIDE * MEADOW_SIDE]; + int mow_rev, mow_wait; /* the field's mowing last built in, frames since it changed */ } Meadow; /* Draws the grass around eye, building squares that come into view. */ diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c index cfc16c5..45f5d8f 100644 --- a/entropie/src/world/object.c +++ b/entropie/src/world/object.c @@ -421,6 +421,33 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size break; } + case OBJ_SCYTHE: + obj->size = vec3(0.06f, 1.6f, 0.06f); + obj->weight = 2.5f; + obj->mesh = Shapes_scythe(1.6f); + obj->pivot = vec3(0.0f, 0.8f, 0.0f); + addCollider(obj, vec3_zero(), obj->size, vec3_zero()); + break; + + case OBJ_SNAIL: + obj->variant = (int)weight ? 1 : 0; + obj->size = vec3(0.05f, 0.045f, 0.08f); + obj->weight = 0.02f; + obj->mesh = Shapes_snail(); + obj->pivot = vec3_zero(); + addCollider(obj, vec3(0.0f, 0.022f, 0.0f), obj->size, vec3_zero()); + break; + + case OBJ_SCULPTURE: + obj->variant = (int)weight % 3; + obj->size = size; + obj->weight = 200.0f; + obj->is_static = true; + obj->mesh = Shapes_sculpture(obj->variant, size.x, size.y); + obj->pivot = vec3_zero(); + addCollider(obj, vec3(0.0f, size.y * 0.5f, 0.0f), vec3(size.x, size.y, size.x), vec3_zero()); + 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; @@ -1563,6 +1590,15 @@ void Object_describe(const Object *obj, char *out, size_t size) { snprintf(out, size, "%s - %d kg", WHAT[obj->variant % MATERIAL_KINDS], kg); break; } + case OBJ_SCYTHE: snprintf(out, size, "Scythe - for the Kartoffelacker's grass (in the field: hold left click and walk)"); break; + case OBJ_SNAIL: + snprintf(out, size, obj->variant ? "A vineyard snail - safe here" : "A vineyard snail (protected) - left click: into your bucket, let go at the wood by the Kartoffelacker"); + break; + case OBJ_SCULPTURE: { + static const char *const WHAT[] = { "an owl", "a bear", "a mushroom man" }; + snprintf(out, size, "Kerry's wood art - %s carved with the chainsaw", WHAT[obj->variant % 3]); + 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 759eac6..fc9be94 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -91,6 +91,9 @@ typedef enum { 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 */ OBJ_MATERIAL, /* building material from the hardware store: variant = MaterialKind (weight given) */ + OBJ_SCYTHE, /* the scythe: mows the Kartoffelacker's grass (world/field.h); carried in one hand */ + OBJ_SNAIL, /* a vineyard snail: variant 0 out there, 1 let go at the refuge (weight given: the variant) */ + OBJ_SCULPTURE, /* static: a log carved into a figure with the chainsaw (Kerry); size = (diameter, height, -), variant = which */ } ObjectType; /* Building material (OBJ_MATERIAL): what the stages are built with besides the logs. */ @@ -314,6 +317,9 @@ typedef struct Object { float build_work; /* ... person-hours of work into the next bit */ int stock; /* a bought food box (OBJ_SUPPLY): portions left (-1: empty); 0: never runs out (the scene's) */ struct Object *table; /* a bench of a beer table set: its table (sat at facing it, stepped over) */ + int snails; /* characters: vineyard snails in their bucket (let go at the refuge) */ + bool carving; /* characters: carving with the chainsaw just now (its sound) */ + float mowing; /* characters: > 0 mowing with the scythe: where in the swing (rad) */ 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 9d30cf0..4708f9a 100644 --- a/entropie/src/world/site.c +++ b/entropie/src/world/site.c @@ -1,5 +1,6 @@ #include "world/site.h" #include "world/stage.h" +#include "world/field.h" #include "world/manor.h" #include "world/ladder.h" @@ -836,6 +837,7 @@ static bool furnishGarages(World *world, Vec3 origin, float yaw, float zsign) { { OBJ_LADDER, { 20.9f, 0.1f, 13.55f }, { 0.0f, 0.0f, 0.0f }, { 90, 90, 0 }, 9.5f, 0 }, /* slid together, along the end wall */ { OBJ_LADDER, { 23.2f, 0.1f, 13.55f }, { 0.0f, 0.0f, 0.0f }, { 90, 90, 0 }, 9.8f, LADDER_FOLDING }, /* folded, beside it */ { OBJ_LADDER, { 21.0f, 0.1f, 12.6f }, { 0.0f, 0.0f, 0.0f }, { 90, 90, 0 }, 7.0f, LADDER_STEP }, /* folded, in front of them */ + { OBJ_SCYTHE, { 24.6f, 0.1f, 11.0f }, { 0.0f, 0.0f, 0.0f }, { 90, 90, 0 }, 2.5f, 0 }, /* on the floor, along the room */ }; for (unsigned i = 0; i < sizeof(TOOLS) / sizeof(TOOLS[0]); i++) { Vec3 pos = placed(TOOLS[i].at, WALLED_CENTER, origin, yaw, zsign); @@ -1284,6 +1286,10 @@ static const struct { Project project; float a, b, front; } FLOORS[] = { Vec3 Site_campPath(void) { return court(36.0f, -11.0f); } /* (the foot of the path up behind your garages: their end by the barn) */ +Vec3 Site_snailRefuge(void) { return at((Px){ 325.0f, 612.0f }); } /* (between the Kartoffelacker and the wood left of it: long grass, bushes) */ + +Vec3 Site_artSpot(void) { return court(14.0f, 12.0f); } /* (in the yard, between the pavilions and where the cars stop) */ + 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); @@ -1298,6 +1304,7 @@ bool Site_finish(World *world) { Object *s = Stage_create(FLOORS[k].project, at, yaw_facing(front)); if (!s || !World_add(world, s)) return false; } + if (!Field_prepare(world)) return false; /* (the Kartoffelacker's grass round its stage, the snails) */ /* Trees stand a little into the ground, wherever it is */ for (int i = 0; i < world->count; i++) { diff --git a/entropie/src/world/site.h b/entropie/src/world/site.h index 1c799f5..fdcac8e 100644 --- a/entropie/src/world/site.h +++ b/entropie/src/world/site.h @@ -35,6 +35,9 @@ Vec3 Site_rainRoom(void); Vec3 Site_floorSpot(int project); /* The foot of the path up to the tents' meadow (the end of your garages by the barn). */ Vec3 Site_campPath(void); +/* Where the vineyard snails are let go (out of the way of the building), and where Kerry carves (world/field.h). */ +Vec3 Site_snailRefuge(void); +Vec3 Site_artSpot(void); /* 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/world.c b/entropie/src/world/world.c index f647a07..5e465f6 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -1317,7 +1317,7 @@ bool World_add(World *world, Object *obj) { world->objects = grown; world->capacity = cap; } - if (!obj->has_home && (obj->type == OBJ_AXE || obj->type == OBJ_SHEARS || obj->type == OBJ_CHAINSAW || obj->type == OBJ_UMBRELLA)) { + if (!obj->has_home && (obj->type == OBJ_AXE || obj->type == OBJ_SHEARS || obj->type == OBJ_CHAINSAW || obj->type == OBJ_UMBRELLA || obj->type == OBJ_SCYTHE)) { obj->has_home = true; /* (where it is at first is where it belongs) */ obj->home_spot = obj->position; obj->home_rot = obj->rotation; diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h index 2f3b7d2..77d24e1 100644 --- a/entropie/src/world/world.h +++ b/entropie/src/world/world.h @@ -39,6 +39,7 @@ typedef struct { NoiseKind kind; Vec3 at; float loud; int surface; } Noise; #define WORLD_NOISES 96 #define WORLD_CASH 1200.0f /* € in the cash box at the start */ +#define WORLD_FIELD_CELLS 520 /* the Kartoffelacker's cells (field.h FIELD_CELLS) */ typedef struct World { float dt_props; /* (this frame's step, for the props that move: gates) */ @@ -77,6 +78,10 @@ typedef struct World { struct RainDrops *drops; float cash; /* € in the festival's cash box (shopping runs: game_shop.c) ... */ bool shopping; /* ... one on its way (the car's off) */ + unsigned char mown[WORLD_FIELD_CELLS]; /* the Kartoffelacker's grass: how far each cell is mown (field.c) ... */ + int mow_rev; /* ... changed this often (the grass is built again) */ + int snails_rescued, snails_killed; /* the vineyard snails (field.c) ... */ + bool inspected; /* ... the regulatory office has been */ } World; /* A sound made here (dropped when the list is full: nobody's listening). */