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.46.0: the festival
- Weekend 6 is the festival: the Herberges before it say so, everybody's on the ground Friday at ten, the gates open at six - 36 guests come in from the chicken farm's yard (Friday evening, Saturday late morning), camp right of the strip of wood and pitch their tents - Floors with at least their deck play, each its own music and times; guests dance to the beat (dancing is new to the bodies), better floors are more fun - Saturday at two the storm warning: get everybody inside (main building, barn, garages' kitchen) - shout it (X, then 1), the crew help, a floor with a roof announces it. At its worst, who's still out is counted - The awareness team, free Urinellas and "no shirt, no service" now do something at the festival - Sunday the guests pack up and leave; at four the crew sums it up at the fire (a result in %); the notebook has a festival part - Dev menu: jump to the festival and through it - Saves keep the guests and the festival
commit_message.txt | 26 ++- entropie/ARCHITECTURE.md | 6 +- entropie/Makefile | 2 +- entropie/README.md | 6 +- entropie/STATE.md | 76 ++++++- entropie/commit_message.txt | 26 ++- entropie/src/core/audio.c | 109 +++++++++- entropie/src/core/audio.h | 3 +- entropie/src/game/game.c | 1 + entropie/src/game/game.h | 15 ++ entropie/src/game/game_arrival.c | 2 + entropie/src/game/game_dev.c | 7 +- entropie/src/game/game_festival.c | 426 ++++++++++++++++++++++++++++++++++++++ entropie/src/game/game_internal.h | 12 ++ entropie/src/game/game_meeting.c | 24 ++- entropie/src/game/game_talk.c | 13 +- entropie/src/game/game_why.c | 1 + entropie/src/game/save.c | 17 +- entropie/src/game/sound.c | 13 ++ entropie/src/people/ai.c | 5 +- entropie/src/people/ai.h | 7 + entropie/src/people/ai_fest.c | 312 ++++++++++++++++++++++++++++ entropie/src/people/ai_internal.h | 2 + entropie/src/people/ai_project.c | 1 + entropie/src/people/body.c | 2 + entropie/src/people/body.h | 4 + entropie/src/people/body_pose.c | 31 ++- entropie/src/people/talk.c | 7 +- entropie/src/world/guests.c | 94 +++++++++ entropie/src/world/guests.h | 26 +++ entropie/src/world/object.h | 6 + entropie/src/world/site.c | 2 + entropie/src/world/site.h | 2 + entropie/src/world/tent.c | 5 + entropie/src/world/world.c | 5 +- entropie/src/world/world.h | 18 ++ 36 files changed, 1269 insertions(+), 45 deletions(-) diff --git a/commit_message.txt b/commit_message.txt index f1799b2..97c18ed 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1,10 +1,18 @@ -entropie 2.45.0: the why questions +entropie 2.46.0: the festival -- Why do festivals hand out Urinellas, have an awareness team, say "no - shirt, no service"? Each comes up when you see the reason on the ground - (or somebody asks it at the Herberge) -- Ask who knows (X, 4, then 5): they explain it; who doesn't sends you on -- The next Herberge decides: do it or not this year (moods and cash follow) -- Your notebook (J): the festival, who leads the floors, the cash box and - the why questions as far as each has got -- Floors asked about at one Herberge are asked again at the next +- Weekend 6 is the festival: the Herberges before it say so, everybody's + on the ground Friday at ten, the gates open at six +- 36 guests come in from the chicken farm's yard (Friday evening, Saturday + late morning), camp right of the strip of wood and pitch their tents +- Floors with at least their deck play, each its own music and times; + guests dance to the beat (dancing is new to the bodies), better floors + are more fun +- Saturday at two the storm warning: get everybody inside (main building, + barn, garages' kitchen) - shout it (X, then 1), the crew help, a floor + with a roof announces it. At its worst, who's still out is counted +- The awareness team, free Urinellas and "no shirt, no service" now do + something at the festival +- Sunday the guests pack up and leave; at four the crew sums it up at the + fire (a result in %); the notebook has a festival part +- Dev menu: jump to the festival and through it +- Saves keep the guests and the festival diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md index 7ee68f9..333bb35 100644 --- a/entropie/ARCHITECTURE.md +++ b/entropie/ARCHITECTURE.md @@ -10,9 +10,9 @@ 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` - also the Berlin rooms off the map, `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), the why questions' content (`why.c`), 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`), the meetings in Berlin and the weekends (`game_meeting.c`), the why questions coming up and your notebook (`game_why.c`), saves (`save.c`), what you hear (`sound.c`) | all | +| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c` - also the Berlin rooms off the map, `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), the why questions' content (`why.c`), the festival's guests (`guests.c`), 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`, the festival weekend: `ai_fest.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`), the meetings in Berlin and the weekends (`game_meeting.c`), the why questions coming up and your notebook (`game_why.c`), the festival weekend and the storm (`game_festival.c`), saves (`save.c`), what you hear (`sound.c`) | all | - `render` knows nothing of the world: it gets meshes, matrices and plain numbers. - `world` and `people` are one layer: an `Object` owns its `Body`, `Ai` and `Hands`. diff --git a/entropie/Makefile b/entropie/Makefile index d43025e..1c21b91 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 2.45.0 +VERSION := 2.46.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index fc9d1d2..e431334 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -33,6 +33,10 @@ people living there. - **Why questions:** why do festivals hand out Urinellas, have an awareness team, say "no shirt, no service"? See it happen, ask who knows, decide at the Herberge. Your notebook (J) keeps track +- **The festival:** the sixth weekend. 36 guests camp beyond the wood and + dance at every floor that got built, each with its own music. Saturday + afternoon a storm: get everybody inside in time (X, then 1: shout it). + Sunday they leave, and the crew sums it up at the fire - **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 @@ -146,5 +150,5 @@ Dependencies point downward: `main → game → world/people → render → core ## Version -`make version` prints e.g. `entropie 2.45.0 (build db5ab7c)`. The build id is +`make version` prints e.g. `entropie 2.46.0 (build 9a90412)`. The build id is an md5 of all sources and assets. diff --git a/entropie/STATE.md b/entropie/STATE.md index a52a8db..037aa72 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -84,8 +84,8 @@ things the way they do. 5. Berlin: Grossbeerenstrasse and the Herberge as places, the meetings, going back and forth between weekends. (2.44) 6. The "why" questions and their options. (2.45) -7. The festival (guests, the floors running, the storm and the evacuation), - the clean-up with the supporters, the drive home. +7. The festival (guests, the floors running, the storm and the evacuation) + (2.46), the clean-up with the supporters, the drive home. ## People @@ -368,6 +368,62 @@ Painted in the rotoscope style of *Waking Life* (2001): - The notebook also has who leads the floors, the cash box and what the weekend is for +### The festival (the sixth weekend) + +- Weekend 6 is the festival. The Herberge before weekend 4 says two more + to go, the one before weekend 6 says it's next - no "what first" then: + what's built by Friday is what you've got. Everybody's on the ground + Friday at ten; there's no Herberge after it +- Six o'clock Friday the gates open. 36 guests come on foot from the + chicken farm's yard, two by two: 24 on Friday evening, 12 more Saturday + late morning. They go to the camping right of the strip of wood (between + the chicken halls, three rows of six), get their tent up together, then + head for the music. Their tents are all colours +- A floor plays only if its stage has its deck at least (the deck 45%, + the roof 65%, paint 85%, done 100% good). Each has its own music + (Hofbuehne: a rock band, Obstwiese: psytrance, Kartoffelacker: hand + drums, hidden floor: techno; far off only the bass and kick carry) and + its times: Hofbuehne Fri 19-01, Sat 12:30-14 and 17:30-01; Obstwiese + Fri 22 - Sat 08 and Sat 21 - Sun 10; Kartoffelacker Sat 10-14 and + 17:30-20:30; hidden floor Sat 00:30-07 and Sun 01-08 +- Guests dance in front of a playing stage (the better floors and their + favourite style pull more), to its beat, each their own way (bouncing, + swaying, both hands up now and then, a fist in the air), a few minutes + at a go, then a break. Dancing raises their fun; nothing playing by day + lowers it a little. They bring their own food and drink; in the evening + the party people drink, a few until they pass out. Sleepy, they go back + to their tent. The crew dance too, evenings and nights. Nothing gets + built any more once the gates are open +- Talk to a guest (X, then 4): how they like it +- The awareness team (if the Herberge said yes): the crew see to anybody + lying about drunk (water, sat up, back on their feet); Stella, Doro, + Julia and Volkram stay up till four on shift +- What else the Herberge said yes to: the women get free Urinellas; with + "no shirt, no service" the men keep their shirts on (the women like it) +- Saturday at two the storm warning: a heavy thunderstorm at three. The + music stops. The crew know; a floor with a roof (65% and up) announces it + from its stage to those near. Everybody else has to be told: you (X with + nobody in front, then 1: "Storm! Everybody inside!" - 35 m) and the crew + on their way in (25 m; the awareness team goes round everywhere and gets + people out of their tents). Told, people go to the nearest door - the + main building, the barn (its yard side, left of the Hofbuehne), your + garages' kitchen - and stay in. Not told, they shelter as from any rain: + their tent, the pavilions - not safe +- Rain from twenty to three, the storm at three (pouring, gale, thunder). + At a quarter past it's at its worst: everybody still out is counted + (guests lose fun, the crew a little heart; some get knocked over). Half + past four it's over, everybody comes out; the music's back at half past + five +- Sunday from ten the guests pack up (the tent goes with them) and leave + at the gate, two by two, the last by half past two. At four, at the fire: + how it went - the floors, the storm, the awareness team, what the guests + say, a result in %. The notebook (J) has a festival part: guests here, + what's playing, floors that don't play, the storm, the result +- F1 dev menu, "The festival": Right before the festival weekend jumps to + the last Herberge; on the weekend, each Right jumps to the next moment + (the gates, the warning, the storm at its worst, the storm over, Sunday + leaving, the review) + ### Arriving - The game starts in Volkram's car (he drives, Jana beside him, you and Toni @@ -1487,6 +1543,15 @@ Round 2.45 (the why questions): - `game_meeting.c` (later Herberges): an unknown question raised by its raiser (one a meeting), then each understood one asked (Q_WHY: the option or "Not this year"; knowers +0.05 / -0.04; the cost from the cash box). `asked_mask` now cleared for every meeting (floors asked last time weren't asked again). - Checked: shirtless Toni next to you raised it; Jana explained it; the Herberge decided yes; Stella raised the Urinellas there, Doro explained them, the next Herberge decided yes (60 EUR); Julia raised the awareness team; the notebook through all of it. +Round 2.46 (the festival): +- `game/game_festival.c` (Game.festival: stage, came, caught, inside, counted, played, review, result, helped; saved in the `festival` line): weekend FESTIVAL_WEEKEND 6 is the festival. Hours from the weekend's Friday midnight: H_GATES 18, two waves (FIRST_WAVE 24 till 23, the rest 34-37), H_WARN 38, H_CLOUDS 38.66 (WEATHER_SHOWER), H_STORM 39 (WEATHER_RAIN, WIND_STORM), H_WORST 39.25 (counted), H_CLEAR 40.5, H_LEAVE 58, H_REVIEW 64 (six lines, REVIEW_GAP 4.5 s). The weather is forced dry otherwise (AUTO again once it's over). Every frame it fills `World.fest` (Fest: stage, floors playing, how good, the dance floor DANCE_OUT 6 m in front of each stage along `yaw_front`, tempos, clock, the storm doors, the why answers, helped). +- Floors: good 0 / 0.45 / 0.65 / 0.85 / 1 by build stage (deck at least), PLAYS times per floor. `core/audio.c`: BED_FLOOR + 0..3, a drum machine each (rock 118, psytrance 144, hand drums 100, techno 130 BPM; param muffles it with distance); `sound.c` sets them from the dance floors. +- `world/guests.c`: Guest_create(k) - name, look, temperament from the number alone (a save keeps `guest`, the loader makes them anew); camp spots (3 rows of 6 at x 9-31, z 86-94), the gate (63, 89.5). Object: guest, fun, camp_spot, gone_home, storm_told, inside_door (all saved). Guest tents coloured by pair (`tent.c`). +- `people/ai_fest.c` (`ai_festival`, before the rain shelter in `Ai_update`; Ai.fest_floor / fest_spot / fest_time / fest_who / fest_walk): the storm (told: the crew warn on the way - TELL_LOOK 25 m, the awareness team TELL_LOOK_TEAM 140 m and tents - then the nearest door: `indoors` at `inside_door`, kept while warned or stormy: `world.c`), the awareness team (HELP_LOOK 45 m, drunk down to 0.45; knowers awake till four), guests camping (a big tent bag at the spot, unpacked with the mate as owner2; `pitchTents` puts it up), leaving (tent packed, out at the gate: gone_home, indoors for good; lost on the way 400 s: gone anyway), dancing (FUN_DANCING 0.0009 a s x good, DANCE_MIN 150 + up to 300 s, breaks DANCE_BREAK 60-120 s), party drinking in the evening. Guests' hunger and thirst stay low. +- Body: `dance_goal`, `dance_beat`, `dance_style` -> `dance` (eased): knees down on the beat (deeper for stompers), hips side to side, shoulders against them, arms by style (`body_pose.c`). +- `game_meeting.c`: the Herberges before it say so (no focus question the last time), nextWeekend: Friday at ten for the festival (FESTIVAL_ARRIVE), no weekend end on it. `game_talk.c`: the storm shout first in the shout menu ("Hey, everybody" calls the crew only). `ai_project.c`: no building once the gates are open. `talk.c`: guests say how they like it. Guests don't come to you with jobs. Dev menu row DEV_FESTIVAL. Notebook: the festival part (`game_festivalNotes`). +- Checked (the harness, time sped up): the jump through the last Herberge to Friday 10:00; 36 guests in, camping, tents up, dancing in front of the Hofbuehne; 47 of 50 warned, 36 inside at the worst, 14 out; the all-clear; all 36 gone by Sunday three; the review (62%); the awareness team helping one; a save from Friday night loaded (18 guests back). The real game: through the dev jump to the festival weekend and the gates. + ## Open tasks Most important first. @@ -1499,7 +1564,9 @@ Most important first. - 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 - Berlin, still to do: the streets and the way between (the rooms are closed boxes); ordering at the bar; more on the agenda (the "why" questions); the crew missing a weekend now and then - Why questions, still to do: more of them (deposit on cups, free water, a quiet place, ...); what each decision does at the festival -- Next: the festival, the storm, the clean-up, the drive home +- Next: the clean-up (with the supporters: a free ticket for 2 x 6 h of work), the drive home, the end +- The festival, still to do: bands and DJs on the stages; the ES-Bar's food stand and the bars; compost toilets for the guests; tickets (money in); guests leaving early when it's bad; the storm blowing tents and pavilions over; the camping's place (check it: now right of the strip of wood, between the chicken halls) +- Crowds: lots of "Ouch! My shin" where many people mill about (the shin scrapes) **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) @@ -1517,7 +1584,6 @@ Most important first. - Ladders, still to do: a holder for a ladder standing free; NPCs taking a ladder somewhere for their own work - Smoking, still to do: paying for a pack (the cash box is the crew's, no money of your own yet) - Carrying long things (over 0.62 m across both arms): NPCs steer by their body only, the load catches on posts and they stand stuck (seen with 1 m board bundles at a pavilion post; the bundles are 0.6 m now) -- "No shirt no service" policy (decision open) - Clothing, still to do: more outfits for extra people (only the performance test has any: they all wear trousers) - Tents, still to do: NPCs taking theirs down on their own (leaving) - Weather, still to do: umbrellas while working (now only idle people fetch one) @@ -1533,4 +1599,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.45.0 (build db5ab7c)` +State of `entropie 2.46.0 (build 9a90412)` diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index f1799b2..97c18ed 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,10 +1,18 @@ -entropie 2.45.0: the why questions +entropie 2.46.0: the festival -- Why do festivals hand out Urinellas, have an awareness team, say "no - shirt, no service"? Each comes up when you see the reason on the ground - (or somebody asks it at the Herberge) -- Ask who knows (X, 4, then 5): they explain it; who doesn't sends you on -- The next Herberge decides: do it or not this year (moods and cash follow) -- Your notebook (J): the festival, who leads the floors, the cash box and - the why questions as far as each has got -- Floors asked about at one Herberge are asked again at the next +- Weekend 6 is the festival: the Herberges before it say so, everybody's + on the ground Friday at ten, the gates open at six +- 36 guests come in from the chicken farm's yard (Friday evening, Saturday + late morning), camp right of the strip of wood and pitch their tents +- Floors with at least their deck play, each its own music and times; + guests dance to the beat (dancing is new to the bodies), better floors + are more fun +- Saturday at two the storm warning: get everybody inside (main building, + barn, garages' kitchen) - shout it (X, then 1), the crew help, a floor + with a roof announces it. At its worst, who's still out is counted +- The awareness team, free Urinellas and "no shirt, no service" now do + something at the festival +- Sunday the guests pack up and leave; at four the crew sums it up at the + fire (a result in %); the notebook has a festival part +- Dev menu: jump to the festival and through it +- Saves keep the guests and the festival diff --git a/entropie/src/core/audio.c b/entropie/src/core/audio.c index 6fa52ef..e4a2c0c 100644 --- a/entropie/src/core/audio.c +++ b/entropie/src/core/audio.c @@ -32,6 +32,9 @@ typedef struct { Low low, low2, high; float mod, mod_v; /* a slow wander (wind gusts, a fire breathing) */ unsigned rng; + double t; /* music: s it's been playing */ + float kick, osc[3], hiss; /* ... the kick's phase, the other tones' phases, a noise burst's state */ + Low tone, tone2; /* ... their filters */ } BedState; static SDL_AudioStream *g_stream; @@ -102,6 +105,108 @@ static void mixVoices(float *out, int frames) { /* ---------- Beds ---------- */ +/* The floors' music: a drum machine each, its own groove (made up on the spot, like everything here). + 0 the Hofbuehne: a rock band - kick, snare on two and four, a chugging power chord; + 1 the Obstwiese: psytrance - four on the floor, a rolling bass on the sixteenths, open hats; + 2 the Kartoffelacker: hand drums - low and high toms in a pattern, a shaker; + 3 the hidden floor: techno - kick, an off-beat hat, a clap, an off-beat bass. */ +static const float FLOOR_BPM[4] = { 118.0f, 144.0f, 100.0f, 130.0f }; /* (the dancers move to these: game_festival.c FLOOR_BPM) */ + +static float sweptKick(BedState *b, float since, float from, float to) { /* (a sine sweeping down fast: the thump) */ + float f = to + (from - to) * expf(-since * 28.0f); + b->kick += f / RATE; + if (b->kick > 1.0f) b->kick -= 1.0f; + return sinf(TWO_PI * b->kick) * expf(-since * 7.0f); +} + +static float sawAt(float *phase, float f) { + *phase += f / RATE; + if (*phase > 1.0f) *phase -= 1.0f; + return 2.0f * *phase - 1.0f; +} + +static float floorSample(int style, BedState *b) { + float bpm = FLOOR_BPM[style], spb = 60.0f / bpm; + b->t += 1.0 / RATE; + if (b->t > 3600.0) b->t -= 3600.0 - fmod(3600.0, (double)(spb * 16.0f)); /* (whole bars: no jump in the groove) */ + float beats = (float)(b->t / spb); + int beat = (int)beats % 16; /* (16 beats: four bars) */ + float in_beat = (beats - floorf(beats)) * spb; /* s since this beat */ + float sixteenth = beats * 4.0f, in_six = (sixteenth - floorf(sixteenth)) * spb * 0.25f; + int six = (int)sixteenth % 4; + float noise_v = noise(&b->rng), x = 0.0f; + switch (style) { + case 0: { /* rock */ + bool kick = beat % 4 == 0 || beat % 4 == 2 || (beat % 8 == 7 && six == 2); + if (kick) x += 0.9f * sweptKick(b, beat % 8 == 7 ? in_six : in_beat, 110.0f, 50.0f); + if (beat % 2 == 1) { /* the snare: a crack of noise and a body */ + b->tone.a = coef(2500.0f); + x += 0.5f * lowpass(&b->tone, noise_v) * expf(-in_beat * 14.0f) + 0.2f * sinf(TWO_PI * 190.0f * in_beat) * expf(-in_beat * 20.0f); + } + static const float ROOT[4] = { 82.4f, 82.4f, 98.0f, 73.4f }; /* (E, E, G, D) */ + float root = ROOT[(beat / 4) % 4]; + float chug = expf(-fmodf(in_beat, spb * 0.5f) * 6.0f) * 0.8f + 0.2f; + float gtr = sawAt(&b->osc[0], root) + sawAt(&b->osc[1], root * 1.5f) + 0.6f * sawAt(&b->osc[2], root * 2.0f); + gtr = tanhf(gtr * 2.5f) * chug; /* (overdriven) */ + b->tone2.a = coef(1800.0f); + x += 0.28f * lowpass(&b->tone2, gtr); + b->hiss = b->hiss * 0.5f + 0.5f * noise_v; /* (the hi-hat on the eighths) */ + x += 0.06f * (noise_v - b->hiss) * expf(-fmodf(in_beat, spb * 0.5f) * 40.0f); + break; + } + case 1: { /* psytrance */ + x += 1.0f * sweptKick(b, in_beat, 140.0f, 48.0f); + if (six != 0) { /* the rolling bass: three on the sixteenths after each kick */ + static const float NOTE[4] = { 46.2f, 46.2f, 51.9f, 43.6f }; + float f = NOTE[(beat / 4) % 4]; + b->tone.a = coef(300.0f + 1400.0f * expf(-in_six * 18.0f)); + x += 0.55f * lowpass(&b->tone, sawAt(&b->osc[0], f)) * expf(-in_six * 10.0f); + } + if (six == 2) { /* the open hat on the off-beat */ + b->hiss = b->hiss * 0.3f + 0.7f * noise_v; + x += 0.12f * (noise_v - b->hiss) * expf(-in_six * 18.0f); + } + float acid = sinf(TWO_PI * (float)fmod(b->t * 0.07, 1.0)); /* (a squelchy lead now and then, every other four bars) */ + if (beat >= 8 && six % 2 == 1) { + b->tone2.a = coef(600.0f + 1800.0f * (0.5f + 0.5f * acid)); + x += 0.12f * lowpass(&b->tone2, sawAt(&b->osc[1], 185.0f * (six == 3 ? 1.5f : 1.0f))) * expf(-in_six * 12.0f); + } + break; + } + case 2: { /* hand drums */ + static const signed char LOW[16] = { 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0 }; /* (on the sixteenths of a bar) */ + static const signed char HIGH[16] = { 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1 }; + int step = ((int)sixteenth) % 16; + if (LOW[step]) x += 0.7f * sinf(TWO_PI * (95.0f + 40.0f * expf(-in_six * 30.0f)) * in_six) * expf(-in_six * 9.0f); + if (HIGH[step]) { + b->tone.a = coef(1800.0f); + x += 0.4f * sinf(TWO_PI * 330.0f * in_six) * expf(-in_six * 16.0f) + 0.15f * lowpass(&b->tone, noise_v) * expf(-in_six * 40.0f); + } + b->hiss = b->hiss * 0.2f + 0.8f * noise_v; /* (the shaker on every sixteenth, the off ones louder) */ + x += (six % 2 ? 0.07f : 0.04f) * (noise_v - b->hiss) * expf(-in_six * 25.0f); + float drone = sinf(TWO_PI * (float)fmod(b->t * 110.0, 1.0)) + 0.5f * sinf(TWO_PI * (float)fmod(b->t * 165.0, 1.0)); /* (a drone under it) */ + x += 0.05f * drone * (0.7f + 0.3f * sinf(TWO_PI * (float)fmod(b->t * 0.1, 1.0))); + break; + } + default: { /* techno */ + x += 1.0f * sweptKick(b, in_beat, 120.0f, 46.0f); + if (six == 2) { + b->hiss = b->hiss * 0.4f + 0.6f * noise_v; + x += 0.1f * (noise_v - b->hiss) * expf(-in_six * 30.0f); + x += 0.4f * tanhf(1.5f * sawAt(&b->osc[0], 49.0f)) * expf(-in_six * 8.0f); /* (the off-beat bass) */ + } + if (beat % 2 == 1) { /* the clap: noise in bursts */ + b->tone.a = coef(1600.0f); + float burst = in_beat < 0.03f ? (fmodf(in_beat, 0.01f) < 0.004f ? 1.0f : 0.3f) : expf(-(in_beat - 0.03f) * 12.0f); + x += 0.35f * lowpass(&b->tone, noise_v) * burst; + } + break; + } + } + b->low.a = coef(9000.0f - 8300.0f * b->k); /* (far off: only the low end carries) */ + return lowpass(&b->low, x) * 1.2f; +} + static float bedSample(Bed which, BedState *b) { float x = 0.0f, k = b->k; b->mod_v += (noise(&b->rng) * 0.6f - b->mod * 0.4f - b->mod_v * 0.9f) / RATE; /* (a slow wander, -1..1) */ @@ -177,7 +282,9 @@ static float bedSample(Bed which, BedState *b) { x *= 1.6f; break; } - default: break; + default: + if (which >= BED_FLOOR && which < BED_FLOOR + 4) x = floorSample(which - BED_FLOOR, b); + break; } return x; } diff --git a/entropie/src/core/audio.h b/entropie/src/core/audio.h index ab3df7a..ebd6a57 100644 --- a/entropie/src/core/audio.h +++ b/entropie/src/core/audio.h @@ -37,7 +37,8 @@ typedef enum { BED_ENGINE, /* param: 0..1 how fast (the car) */ BED_CRICKETS, /* param: unused */ BED_HOSE, /* param: unused */ - BED_COUNT + BED_FLOOR, /* a floor's music (BED_FLOOR + 0..3: rock, psytrance, hand drums, techno); param: 0 near .. 1 far off (muffled) */ + BED_COUNT = BED_FLOOR + 4 } Bed; /* Opens the sound device (false: none - the game goes on silent). */ diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c index a691995..48318ea 100644 --- a/entropie/src/game/game.c +++ b/entropie/src/game/game.c @@ -378,6 +378,7 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) { game_shop(game, dt); game->quest_t += dt; if (!game->meeting.kind) game_why(game, dt); + game_festival(game, dt); Ai_setSettled(game->arrival.task == TASK_NONE); tooHeavy(game); bool warm = game->player->look.top == TOP_JACKET; diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h index 4d2cfc8..8afea56 100644 --- a/entropie/src/game/game.h +++ b/entropie/src/game/game.h @@ -66,6 +66,20 @@ typedef struct { bool skipping; /* Backspace: the rest of it at once, the first answers */ } Meeting; +/* The festival weekend (game_festival.c): the last weekend on the ground, the guests, the storm. */ +typedef struct { + int stage; /* FEST_* (world.h) */ + int came; /* guests come so far */ + int caught; /* the storm at its worst: out in it ... */ + int inside; /* ... and safe inside ... */ + bool counted; /* ... counted */ + unsigned played; /* bit k: floor k played at all */ + int review; /* Sunday at the fire: lines said so far ... */ + float review_t; /* ... till the next */ + int result; /* how good it was, 0..100 (-1: not over yet) */ + int helped; /* the awareness team saw to people (kept here: the world's counter is set every frame) */ +} Festival; + typedef struct { World world; Object *player; @@ -135,6 +149,7 @@ typedef struct { int focus; /* what this weekend is for (the Herberge): FOCUS_* in world.h */ bool show_notes; /* your notebook (J): the festival, the why questions */ float why_check; /* until the next look round for something that makes you wonder (game_why.c) */ + Festival festival; /* the festival weekend (game_festival.c) */ } Game; /* A line of speech above someone's head, in screen pixels (y up from the bottom). */ diff --git a/entropie/src/game/game_arrival.c b/entropie/src/game/game_arrival.c index 81af54f..fb83b95 100644 --- a/entropie/src/game/game_arrival.c +++ b/entropie/src/game/game_arrival.c @@ -407,6 +407,8 @@ const char *game_task(Game *game) { Arrival *a = &game->arrival; const char *meeting = game_meetingTask(game); if (meeting) return meeting; + const char *fest = game_festivalTask(game); + if (fest) return fest; switch (a->task) { case TASK_HELLO: return "To do: say hello to Kurti and his sons at the fire"; case TASK_CHAT: return "To do: sit down with them (C) and have a chat"; diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c index 7248d47..ef85fac 100644 --- a/entropie/src/game/game_dev.c +++ b/entropie/src/game/game_dev.c @@ -10,7 +10,7 @@ #include "world/weather.h" 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_WHO, DEV_TELEPORT, DEV_TIME, DEV_SPEED, DEV_WEATHER, DEV_WIND, DEV_START, DEV_FESTIVAL, 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_COMMIT, DEV_LIKES, /* likes for each job ... */ @@ -121,6 +121,10 @@ void game_dev(Game *game, const Input *input) { if (dir > 0) game_skipArrival(game); return; } + if (game->dev_row == DEV_FESTIVAL) { + if (dir > 0) game_festivalJump(game); + return; + } Object *o = picked(game); if (!o) return; Needs *n = &o->needs; @@ -184,6 +188,7 @@ static void devLine(const Game *game, const Object *o, int row, char *out) { case DEV_WEATHER: snprintf(out, 96, "Weather: %s (raining %.0f%%, wet %.0f%%)", Weather_name((WeatherKind)game->world.weather), game->world.rain * 100.0f, game->world.wet * 100.0f); break; case DEV_WIND: snprintf(out, 96, "Wind: %s (%.0f m/s)", Weather_windName((WindKind)game->world.wind_kind), Weather_windSpeed(&game->world)); break; + case DEV_FESTIVAL: snprintf(out, 96, "The festival: %s", game_festivalJumpText(game)); break; case DEV_START: snprintf(out, 96, "The start: %s", game->arrival.task == TASK_NONE && game->arrival.stage != ARRIVAL_DRIVING ? "done" : "skip it ->"); break; case DEV_FEMALE: snprintf(out, 96, "Female: %s", o->female ? "yes" : "no"); break; case DEV_URINELLA: snprintf(out, 96, "Urinella: %s (%d left)", o->urinella ? "yes" : "no", o->urinellas); break; diff --git a/entropie/src/game/game_festival.c b/entropie/src/game/game_festival.c new file mode 100644 index 0000000..2e9dbd8 --- /dev/null +++ b/entropie/src/game/game_festival.c @@ -0,0 +1,426 @@ +/* + * The festival (see the story: chapter 4) - the last weekend on the ground. + * + * Friday morning everybody's there for the last bits; at six the gates open + * and the guests come in from the chicken farm's yard (two waves: Friday + * evening, Saturday late morning), up to the camping beyond the strip of + * wood, tents up, then off to whatever floor is playing. A floor plays (its + * own music, world/../core/audio.c) only if its stage got far enough - its + * deck at least; the better built, the more fun. Each has its times. + * + * Saturday at two the storm warning: a heavy thunderstorm at three. The crew + * know; a floor with a proper stage announces it; everybody else has to be + * told - by you (X, then 1: shout it) and the crew (the awareness team, if + * there is one, goes round everywhere, the tents too). Inside is safe: the + * main building, the barn, your garages' kitchen. A tent or a pavilion is + * not. At a quarter past three it's at its worst: who's still out in it is + * counted (and some get knocked over). At half past four it's over, and the + * music starts again an hour later. + * + * Sunday from ten the guests pack up and go. At four everybody at the fire: + * how it went (the guests' fun, the storm, the awareness team) - the result. + */ + +#include <math.h> + +#include "game/game_internal.h" +#include "world/cast.h" +#include "world/guests.h" +#include "world/stage.h" +#include "world/weather.h" +#include "world/why.h" + +/* Hours from the weekend's Friday midnight */ +/* (everybody is on the ground from Friday at ten: game_meeting.c FESTIVAL_ARRIVE) */ +#define H_GATES 18.0f /* the gates open */ +#define H_WAVE1_END 23.0f /* the first guests in by then ... */ +#define FIRST_WAVE 24 +#define H_WAVE2 34.0f /* ... the rest Saturday late morning */ +#define H_WAVE2_END 37.0f +#define H_WARN 38.0f /* Saturday two o'clock: the storm warning */ +#define H_CLOUDS 38.66f /* ... the first rain */ +#define H_STORM 39.0f /* ... three: the storm */ +#define H_WORST 39.25f /* ... at its worst: who's out in it is counted */ +#define H_CLEAR 40.5f /* ... over */ +#define H_LEAVE 58.0f /* Sunday ten: the guests go */ +#define H_REVIEW 64.0f /* Sunday four: at the fire, how it went */ +#define REVIEW_GAP 4.5f /* s between the lines */ +#define ANNOUNCE 45.0f /* m: a stage with a sound system tells those this near about the storm */ +#define DANCE_OUT 6.0f /* m in front of a stage: the dance floor's middle */ + +static const float FLOOR_BPM[FEST_FLOORS] = { 118.0f, 144.0f, 100.0f, 130.0f }; /* (as the music: core/audio.c FLOOR_BPM) */ +static const int FLOOR_PROJECT[FEST_FLOORS] = { PROJECT_HOFBUEHNE, PROJECT_OBSTWIESE, PROJECT_KARTOFFELACKER, PROJECT_HIDDEN }; +static const char *const FLOOR_NAME[FEST_FLOORS] = { "the Hofbuehne", "the Obstwiese", "the Kartoffelacker", "the hidden floor" }; +/* When each floor plays (weekend hours): the bands in the evenings, psytrance through the nights, the + Kartoffelacker by day, techno late at night */ +static const float PLAYS[FEST_FLOORS][3][2] = { + { { 19.0f, 25.0f }, { 36.5f, 38.0f }, { 41.5f, 49.0f } }, + { { 22.0f, 32.0f }, { 45.0f, 58.0f }, { 0.0f, 0.0f } }, + { { 34.0f, 38.0f }, { 41.5f, 44.5f }, { 0.0f, 0.0f } }, + { { 24.5f, 31.0f }, { 49.0f, 56.0f }, { 0.0f, 0.0f } }, +}; + +static float hours(const Game *game) { return game->story_hours - game->weekend_from; } + +bool game_festivalWeekend(const Game *game) { return game->weekend >= FESTIVAL_WEEKEND; } + +static Object *named(const Game *game, const char *name) { + for (int i = 0; i < game->world.count; i++) { + Object *o = game->world.objects[i]; + if (o->is_character && !strcmp(o->name, name)) return o; + } + return NULL; +} + +static bool onGround(const Object *o) { + return o->type == OBJ_HUMAN && !o->gone_home && !Game_inBerlin(o); +} + +/* How good a floor is: what got built (the deck at least, else it doesn't play). */ +static float goodness(const Object *stage) { + if (!stage) return 0.0f; + switch (stage->build_stage) { + case BUILD_FLOOR: return 0.45f; + case BUILD_ROOF: return 0.65f; + case BUILD_DECOR: return 0.85f; + case BUILD_DONE: return 1.0f; + default: return 0.0f; + } +} + +static bool playsAt(int k, float h) { + for (int s = 0; s < 3; s++) if (h >= PLAYS[k][s][0] && h < PLAYS[k][s][1]) return true; + return false; +} + +/* What the people need to know (the world's Fest), every frame. */ +static void tellWorld(Game *game, float dt) { + World *w = &game->world; + Fest *f = &w->fest; + Festival *fe = &game->festival; + float h = hours(game); + f->stage = fe->stage; + f->on = 0; + f->clock += dt; + if (f->clock > 3600.0f) f->clock -= 3600.0f; + for (int k = 0; k < FEST_FLOORS; k++) { + const Object *s = Stage_of(w->objects, w->count, (Project)FLOOR_PROJECT[k]); + f->good[k] = goodness(s); + f->bpm[k] = FLOOR_BPM[k]; + if (s) { + f->front[k] = yaw_front(s->rotation.y); /* (a stage's front: its local +Z) */ + f->dance[k] = vec3_add(s->position, vec3_scale(f->front[k], DANCE_OUT)); + f->dance[k].y = World_groundHeight(w, f->dance[k].x, f->dance[k].z, s->position.y + 3.0f); + } + if (fe->stage == FEST_OPEN && f->good[k] > 0.0f && playsAt(k, h)) f->on |= 1u << k; + } + fe->played |= f->on; + f->shelter[0] = Site_houseDoor(); + f->shelter[1] = Site_barnDoor(); + f->shelter[2] = Site_rainRoom(); + f->awareness = w->why[WHY_AWARENESS] == WHY_YES; + f->shirts = w->why[WHY_SHIRT] == WHY_YES; + if (f->helped > fe->helped) fe->helped = f->helped; /* (counted by the people) */ + f->helped = fe->helped; +} + +/* ---------- The guests ---------- */ + +static float dueAt(int k) { + if (k <= FIRST_WAVE) return H_GATES + (H_WAVE1_END - H_GATES) * (float)((k - 1) / 2 * 2) / (float)FIRST_WAVE; + return H_WAVE2 + (H_WAVE2_END - H_WAVE2) * (float)((k - FIRST_WAVE - 1) / 2 * 2) / (float)(GUESTS - FIRST_WAVE); +} + +static void comeIn(Game *game, int k) { + World *w = &game->world; + Vec3 gate = Guest_gate(); + gate.x += (k % 2 ? 0.7f : -0.7f); + gate.y = World_groundHeight(w, gate.x, gate.z, 50.0f); + Object *o = Guest_create(k, gate); + if (!o) return; + /* what the Herberge said yes to */ + if (w->why[WHY_URINELLA] == WHY_YES && o->female) { o->urinella = true; o->urinellas = 3; o->urinella_tried = 1; o->fun += 0.04f; } + if (w->why[WHY_SHIRT] == WHY_YES) { if (o->female) o->fun += 0.04f; else o->look.keeps_shirt = true; } + if (w->why[WHY_AWARENESS] == WHY_YES) o->fun += 0.02f; + Vec3 to = vec3_sub(Guest_campSpot(k), gate); + o->rotation.y = atan2f(to.z, to.x) + PI_F / 2.0f; /* (a person's yaw: Object_facing) */ + Object_syncColliders(o); + if (!World_add(w, o)) return; + game->festival.came = k; +} + +static void guestsIn(Game *game, float h) { + Festival *fe = &game->festival; + while (fe->came < GUESTS && h >= dueAt(fe->came + 1)) { + comeIn(game, fe->came + 1); + if (fe->came == 2) say(game, "The first guests are coming in - from the chicken farm's yard, up to the camping beyond the wood"); + if (fe->came == FIRST_WAVE + 2) say(game, "More guests coming in for Saturday"); + } +} + +/* ---------- The storm ---------- */ + +static int tellStorm(Game *game, Vec3 from, float within, bool heard_only) { + int n = 0; + for (int i = 0; i < game->world.count; i++) { + Object *o = game->world.objects[i]; + if (!onGround(o) || o->storm_told || o->indoors > 0.0f) continue; + if (heard_only && (o->body.asleep || o->in_tent)) continue; + if (hypotf(o->position.x - from.x, o->position.z - from.z) > within) continue; + o->storm_told = true; + o->ai.talking = false; + n++; + } + return n; +} + +int game_shoutStorm(Game *game) { + say(game, "\"Storm! Everybody inside - the barn, the house, the garages!\""); + return tellStorm(game, game->player->position, 35.0f, true); +} + +static void warn(Game *game, float h) { + World *w = &game->world; + for (int i = 0; i < w->count; i++) { /* the crew know (Volkram's phone) */ + Object *o = w->objects[i]; + if (onGround(o) && o->cast_id > 0) o->storm_told = true; + } + for (int k = 0; k < FEST_FLOORS; k++) { /* a stage with a sound system: announced from it */ + if (!playsAt(k, h - 0.02f) || w->fest.good[k] < 0.65f) continue; + tellStorm(game, w->fest.dance[k], ANNOUNCE, true); + Object *lead = Cast_leadOf(w->objects, w->count, (Project)FLOOR_PROJECT[k]); + if (lead && lead != game->player && onGround(lead)) Ai_say(lead, "Storm warning! Everybody off the floor and inside - the barn, the house!"); + } + Object *v = named(game, "Volkram"); + if (v) Ai_say(v, "Storm warning - a big one at three! Everybody inside, tell the guests!"); + say(game, "Storm warning: a heavy thunderstorm at three. Get everybody inside - the main building, the barn, " + "your garages (X, then 1: shout it). Not the tents, not the pavilions!"); + game->message_time = 9.0f; +} + +static void worst(Game *game) { + Festival *fe = &game->festival; + World *w = &game->world; + fe->caught = fe->inside = 0; + int knocked = 0; + for (int i = 0; i < w->count; i++) { + Object *o = w->objects[i]; + if (!onGround(o)) continue; + if (o->indoors > 0.0f) { fe->inside++; continue; } + fe->caught++; + if (o->guest) o->fun = fmaxf(0.0f, o->fun - 0.3f); + else o->commitment = fmaxf(0.0f, o->commitment - 0.1f); + if (!o->in_tent && !o->body.asleep && !Body_isDown(&o->body) && ((i * 7 + fe->caught) % 4) == 0) { /* (a gust, flying things: down) */ + Body_fall(&o->body, vec3_normalize(vec3(w->wind.x + 0.01f, 0.0f, w->wind.z))); + knocked++; + } + } + if (game->player->indoors <= 0.0f && !game->player->in_tent) fe->caught++; + static char line[160]; + if (fe->caught == 0) snprintf(line, sizeof line, "The storm's at its worst - and everybody's inside. Well done!"); + else snprintf(line, sizeof line, "The storm's at its worst: %d still out in it%s", fe->caught, + knocked ? " - people knocked over by the gusts!" : ""); + say(game, line); + game->message_time = 7.0f; + fe->counted = true; +} + +static void allClear(Game *game) { + World *w = &game->world; + for (int i = 0; i < w->count; i++) { + Object *o = w->objects[i]; + o->storm_told = false; + if (o->type == OBJ_HUMAN && !o->gone_home && o->indoors > 3.0f && vec3_length(o->inside_door) > 0.1f) + o->indoors = 2.0f + 0.4f * (float)(i % 20); /* (out again, a few at a time) */ + } + say(game, "The storm's passed. Out you come - the music's back at half past five"); +} + +/* ---------- Sunday: how it went ---------- */ + +static float guestFun(const Game *game, int *n) { + float sum = 0.0f; + *n = 0; + for (int i = 0; i < game->world.count; i++) { + const Object *o = game->world.objects[i]; + if (o->type == OBJ_HUMAN && o->guest) { sum += o->fun; (*n)++; } + } + return *n ? sum / (float)*n : 0.0f; +} + +static int score(const Game *game) { + int n; + float fun = guestFun(game, &n); + int played = 0; + for (int k = 0; k < FEST_FLOORS; k++) if (game->festival.played & (1u << k)) played++; + float s = 0.6f * fun + 0.1f * (float)played / (float)FEST_FLOORS + 0.15f * (n ? (float)n / (float)GUESTS : 0.0f); + s += game->festival.caught == 0 ? 0.15f : 0.15f * fmaxf(0.0f, 1.0f - (float)game->festival.caught / 10.0f); + return (int)lroundf(clampf(s, 0.0f, 1.0f) * 100.0f); +} + +static void line(Game *game, const char *who, const char *text) { + Object *o = named(game, who); + if (o && onGround(o)) Ai_say(o, text); + snprintf(game->story_line, sizeof game->story_line, "%s: %s", who, text); + say(game, game->story_line); + game->message_time = REVIEW_GAP; +} + +static bool review(Game *game, int step) { + Festival *fe = &game->festival; + World *w = &game->world; + char text[160]; + switch (step) { + case 0: + snprintf(text, sizeof text, "That was %s! %d guests came - and we built it all ourselves.", Game_festivalName(game), fe->came); + line(game, "Volkram", text); + return true; + case 1: { + int best = -1, missing = -1; + for (int k = 0; k < FEST_FLOORS; k++) { + if (!(fe->played & (1u << k))) { if (missing < 0) missing = k; continue; } + if (best < 0 || w->fest.good[k] > w->fest.good[best]) best = k; + } + if (best < 0) snprintf(text, sizeof text, "Not one floor was ready to play. People brought their own music..."); + else if (missing >= 0) snprintf(text, sizeof text, "The %s rocked. The %s never played - not built.", FLOOR_NAME[best] + 4, FLOOR_NAME[missing] + 4); + else snprintf(text, sizeof text, "All four floors played - %s was the best.", FLOOR_NAME[best]); + line(game, "Jana", text); + return true; + } + case 2: + if (!fe->counted) return false; + if (fe->caught == 0) snprintf(text, sizeof text, "The storm: everybody got inside in time. That's what I call organised."); + else snprintf(text, sizeof text, "The storm, though: %d were out in it. That could have gone badly.", fe->caught); + line(game, "Volkram", text); + return true; + case 3: + if (w->fest.awareness) snprintf(text, sizeof text, "The awareness team looked after %d who'd had too much.", fe->helped); + else snprintf(text, sizeof text, "People lay drunk in the grass and nobody looked. Next time: an awareness team."); + line(game, w->fest.awareness ? "Stella" : "Julia", text); + return true; + case 4: { + int n; + float fun = guestFun(game, &n); + fe->result = score(game); + snprintf(text, sizeof text, "The guests? %s (%d%%)", fun > 0.8f ? "They loved it - they want to come back!" + : fun > 0.6f ? "They had a good time." : fun > 0.4f ? "It was okay for them. Just okay." : "Honestly, most were disappointed.", fe->result); + line(game, "Toni", text); + return true; + } + case 5: line(game, "Volkram", "Tomorrow we clean up. But tonight - to us!"); return true; + default: return false; + } +} + +/* ---------- Every frame ---------- */ + +void game_festival(Game *game, float dt) { + Festival *fe = &game->festival; + World *w = &game->world; + if (!game_festivalWeekend(game) || game->meeting.kind) { if (fe->stage == FEST_NONE) { w->fest.stage = FEST_NONE; w->fest.on = 0; } return; } + float h = hours(game); + int was = fe->stage; + if (fe->stage == FEST_NONE) fe->stage = FEST_SETUP; + if (fe->stage == FEST_SETUP && h >= H_GATES) { fe->stage = FEST_OPEN; say(game, "Six o'clock: the gates are open! The festival's on"); game->message_time = 6.0f; } + if (fe->stage == FEST_OPEN && h >= H_WARN && h < H_CLEAR && !fe->counted) { fe->stage = FEST_WARNED; } + if (fe->stage == FEST_WARNED && h >= H_STORM) fe->stage = FEST_STORM; + if (fe->stage == FEST_STORM && h >= H_WORST && !fe->counted) worst(game); + if (fe->stage == FEST_STORM && h >= H_CLEAR) { fe->stage = FEST_OPEN; allClear(game); } + if (fe->stage == FEST_OPEN && h >= H_LEAVE) { fe->stage = FEST_LEAVING; say(game, "Sunday: the guests start packing up and going home"); } + if (fe->stage == FEST_LEAVING && h >= H_REVIEW) { fe->stage = FEST_OVER; fe->review = 0; fe->review_t = 1.0f; } + tellWorld(game, dt); + if (was < FEST_WARNED && (fe->stage == FEST_WARNED || fe->stage == FEST_STORM)) warn(game, h); + /* the weather: fine, but for the storm */ + if (fe->stage == FEST_WARNED) { w->weather = h >= H_CLOUDS ? WEATHER_SHOWER : WEATHER_DRY; w->wind_kind = WIND_BREEZE; } + else if (fe->stage == FEST_STORM) { w->weather = WEATHER_RAIN; w->wind_kind = WIND_STORM; } + else if (fe->stage == FEST_OVER) { w->weather = WEATHER_AUTO; w->wind_kind = WIND_AUTO; } + else { w->weather = WEATHER_DRY; w->wind_kind = h >= H_CLEAR && h < H_CLEAR + 1.0f ? WIND_BREEZE : WIND_AUTO; } + if (fe->stage >= FEST_OPEN && fe->stage <= FEST_LEAVING) guestsIn(game, h); + if (fe->stage == FEST_OVER && fe->review >= 0 && (fe->review_t -= dt) <= 0.0f) { + if (review(game, fe->review)) { fe->review_t = REVIEW_GAP; fe->review++; } + else if (fe->review < 6) fe->review++; + else { + fe->review = -1; + snprintf(game->story_line, sizeof game->story_line, "%s is over - %d%%. The clean-up and the drive home are still to come", + Game_festivalName(game), fe->result); + say(game, game->story_line); + game->message_time = 10.0f; + } + } +} + +/* Friday morning: everybody's there (the meetings' nextWeekend did the rest). */ +void game_festivalStart(Game *game) { + memset(&game->festival, 0, sizeof game->festival); + game->festival.result = -1; + game->festival.stage = FEST_SETUP; + game->world.fest.helped = 0; +} + +/* Dev: the next thing in it. */ +static const float JUMPS[] = { H_GATES - 0.05f, H_WARN - 0.05f, H_STORM + 0.2f, H_CLEAR - 0.05f, H_LEAVE - 0.05f, H_REVIEW - 0.05f }; +static const char *const JUMP_NAME[] = { "the gates open", "the storm warning", "the storm at its worst", "the storm over", + "Sunday: leaving", "the review" }; + +const char *game_festivalJumpText(const Game *game) { + if (!game_festivalWeekend(game)) return "go to it ->"; + float h = hours(game); + static char text[64]; + for (int k = 0; k < (int)(sizeof JUMPS / sizeof JUMPS[0]); k++) + if (h < JUMPS[k]) { snprintf(text, sizeof text, "on to %s ->", JUMP_NAME[k]); return text; } + return "over"; +} + +void game_festivalJump(Game *game) { + if (!game_festivalWeekend(game)) { /* (the Herberge before it: straight on to the festival weekend) */ + if (game->meeting.kind || game->weekend <= 0) return; + game->weekend = FESTIVAL_WEEKEND - 1; + game->story_hours = game->weekend_from + 66.0f; /* (Sunday six: back to Berlin, the last Herberge) */ + return; + } + float h = hours(game); + for (int k = 0; k < (int)(sizeof JUMPS / sizeof JUMPS[0]); k++) { + if (h >= JUMPS[k]) continue; + float add = JUMPS[k] - h; + game->story_hours += add; + game->daycycle.seconds = fmodf(game->daycycle.seconds + add * 3600.0f, 24.0f * 3600.0f); + return; + } +} + +const char *game_festivalTask(const Game *game) { + const Festival *fe = &game->festival; + switch (fe->stage) { + case FEST_SETUP: return "The festival weekend: the last bits before the gates open at six"; + case FEST_WARNED: return "The storm's coming at three: get everybody inside (X, then 1: shout it)"; + case FEST_STORM: return "The storm: stay inside till it's over"; + default: return NULL; + } +} + +int game_festivalNotes(const Game *game, char lines[][128], int max) { + const Festival *fe = &game->festival; + const World *w = &game->world; + int n = 0; + if (!game_festivalWeekend(game) || n >= max) return 0; + snprintf(lines[n++], 128, " "); + if (n < max) snprintf(lines[n++], 128, "The festival:"); + if (n < max) { + int here = 0; + for (int i = 0; i < w->count; i++) if (w->objects[i]->type == OBJ_HUMAN && w->objects[i]->guest && !w->objects[i]->gone_home) here++; + snprintf(lines[n++], 128, "- Guests: %d came, %d here now", fe->came, here); + } + char play[128] = "- Playing now:"; + size_t len = strlen(play); + bool any = false; + for (int k = 0; k < FEST_FLOORS; k++) + if (w->fest.on & (1u << k)) { snprintf(play + len, sizeof play - len, "%s %s", any ? "," : "", FLOOR_NAME[k]); len = strlen(play); any = true; } + if (!any) snprintf(play + len, sizeof play - len, " nothing"); + if (n < max) snprintf(lines[n++], 128, "%s", play); + for (int k = 0; k < FEST_FLOORS && n < max; k++) + if (w->fest.good[k] <= 0.0f) snprintf(lines[n++], 128, "- %c%s doesn't play: its stage hasn't got its deck", FLOOR_NAME[k][0] - 32, FLOOR_NAME[k] + 1); + if (fe->counted && n < max) snprintf(lines[n++], 128, "- The storm: %d inside, %d out in it", fe->inside, fe->caught); + if (w->fest.awareness && n < max) snprintf(lines[n++], 128, "- The awareness team helped %d people", fe->helped); + if (fe->stage == FEST_OVER && (fe->review < 0 || fe->review > 4) && n < max) snprintf(lines[n++], 128, "- How it went: %d%%", fe->result); + return n; +} diff --git a/entropie/src/game/game_internal.h b/entropie/src/game/game_internal.h index 52b6dae..1ba4a87 100644 --- a/entropie/src/game/game_internal.h +++ b/entropie/src/game/game_internal.h @@ -47,6 +47,18 @@ void game_meetingLoaded(Game *game); void game_meetingSkipStart(Game *game); /* game_why.c: looking round for what makes you wonder (the why questions coming up). */ void game_why(Game *game, float dt); +/* game_festival.c: the festival weekend (the last on the ground): every frame; is it this weekend (or after); + the weekend starting (Friday morning); dev: on to it, or on to what's next in it; the storm shouted; + the notebook's lines about it. */ +#define FESTIVAL_WEEKEND 6 +void game_festival(Game *game, float dt); +bool game_festivalWeekend(const Game *game); +void game_festivalStart(Game *game); +void game_festivalJump(Game *game); +const char *game_festivalJumpText(const Game *game); +int game_shoutStorm(Game *game); +int game_festivalNotes(const Game *game, char lines[][128], int max); +const char *game_festivalTask(const Game *game); void game_callHelp(Game *game); void game_callOver(Game *game); diff --git a/entropie/src/game/game_meeting.c b/entropie/src/game/game_meeting.c index 0410690..967ba1c 100644 --- a/entropie/src/game/game_meeting.c +++ b/entropie/src/game/game_meeting.c @@ -35,6 +35,7 @@ #define WEEKEND_WARN 65.0f /* hours from the weekend's Friday midnight: "we drive back at six" ... */ #define WEEKEND_END 66.0f /* ... Sunday six o'clock: back to Berlin */ #define NEXT_ARRIVE 17.0f /* the weekends after the first: on the ground Friday at five */ +#define FESTIVAL_ARRIVE 10.0f /* the festival weekend: Friday at ten (game_festival.c H_ARRIVE) */ #define START_HOUR_FIRST 13.0f /* (the first: in by car at one, game.c START_HOUR) */ #define TOP_UP 400.0f /* EUR: another 20 each */ @@ -449,7 +450,14 @@ static bool whyQuestion(Game *game) { static bool herbergeLater(Game *game, int step) { char text[96]; - if (step == 0) { snprintf(text, sizeof text, "Weekend %d done. Where are we?", game->weekend); line(game, "Volkram", text); return true; } + bool last = game->weekend == FESTIVAL_WEEKEND - 1; /* (the festival next: no more building after) */ + if (step == 0) { + snprintf(text, sizeof text, last ? "Weekend %d done - and next weekend's the festival! Where are we?" + : game->weekend == FESTIVAL_WEEKEND - 2 ? "Weekend %d done. One more, then it's the festival. Where are we?" + : "Weekend %d done. Where are we?", game->weekend); + line(game, "Volkram", text); + return true; + } if (step >= 1 && step <= N_FLOORS) { report(game, step - 1); return true; } switch (step) { case N_FLOORS + 1: { @@ -492,6 +500,7 @@ static bool herbergeLater(Game *game, int step) { game->meeting.wait = 0.0f; return true; case N_FLOORS + 6: + if (last) { line(game, "Volkram", "What's built by Friday is what we've got. Friday morning, all of us at Kurti's!"); return true; } line(game, "Volkram", "And next weekend - what first?"); ask(game, Q_FOCUS); option(game, FOCUS_FLOORS, "The floors - we need stages"); @@ -505,7 +514,7 @@ static bool herbergeLater(Game *game, int step) { option(game, 1, "Yes - another 20 each (400 EUR)"); option(game, 0, "No, we'll manage"); return true; - case N_FLOORS + 8: line(game, "Volkram", "Friday at five at Kurti's. See you there!"); return true; + case N_FLOORS + 8: line(game, "Volkram", last ? "See you Friday morning. Our festival!" : "Friday at five at Kurti's. See you there!"); return true; default: return false; } } @@ -565,10 +574,14 @@ static void nextWeekend(Game *game) { put(game->player, you); game->meeting.kind = MEET_NONE; game->weekend++; - game->weekend_from = game->story_hours - NEXT_ARRIVE; + bool festival = game_festivalWeekend(game); /* (the festival: everybody there Friday morning) */ + float arrive = festival ? FESTIVAL_ARRIVE : NEXT_ARRIVE; + game->weekend_from = game->story_hours - arrive; game->weekend_warned = false; - game->daycycle.seconds = NEXT_ARRIVE * 3600.0f; - snprintf(game->story_line, sizeof game->story_line, "Weekend %d - Friday, five o'clock, back at Kurti's", game->weekend); + game->daycycle.seconds = arrive * 3600.0f; + if (festival) game_festivalStart(game); + if (festival) snprintf(game->story_line, sizeof game->story_line, "The festival weekend - Friday morning at Kurti's. The gates open at six"); + else snprintf(game->story_line, sizeof game->story_line, "Weekend %d - Friday, five o'clock, back at Kurti's", game->weekend); say(game, game->story_line); game->message_time = CARD_TIME; } @@ -596,6 +609,7 @@ void game_meetingStart(Game *game) { /* The weekend's end: Sunday at six (not in the middle of the drive in or a shopping run). */ static void weekendEnd(Game *game) { if (game->meeting.kind || game->meeting.fading || game->weekend <= 0 || game->arrival.stage == ARRIVAL_DRIVING || game->errand.stage != ERRAND_NONE) return; + if (game_festivalWeekend(game)) return; /* (the festival: no Herberge after it - the clean-up, then home) */ float h = game->story_hours - game->weekend_from; if (h >= WEEKEND_WARN && !game->weekend_warned) { game->weekend_warned = true; diff --git a/entropie/src/game/game_talk.c b/entropie/src/game/game_talk.c index 35e482c..5ac87aa 100644 --- a/entropie/src/game/game_talk.c +++ b/entropie/src/game/game_talk.c @@ -127,13 +127,21 @@ static void shout(Game *game, int choice) { static char line[64]; game->shouting = false; int heard = 0; + bool storm = game->world.fest.stage == FEST_WARNED || game->world.fest.stage == FEST_STORM; + if (storm && choice == 1) { /* (the storm coming: that first) */ + heard = game_shoutStorm(game); + if (!heard) tellMore(game, "nobody near who doesn't know already"); + return; + } + if (storm) choice--; + int shown = game->shout_count < TALK_CHOICES - 1 - (storm ? 2 : 1) ? game->shout_count : TALK_CHOICES - 1 - (storm ? 2 : 1); /* (names on the menu) */ if (choice == 1) { say(game, "Hey, everybody! Over here!"); for (int i = 0; i < game->world.count; i++) { /* (your lot: not Kurti and his sons - call them by name) */ Object *o = game->world.objects[i]; - if (o->type == OBJ_HUMAN && !o->lives_here && callOne(game, o)) heard++; + if (o->type == OBJ_HUMAN && !o->lives_here && !o->guest && callOne(game, o)) heard++; /* (the crew: not the guests) */ } - } else if (choice - 2 < game->shout_count) { + } else if (choice - 2 < shown) { Object *o = game->shout_to[choice - 2]; snprintf(line, sizeof(line), "%s! Come here!", o->name); say(game, line); @@ -152,6 +160,7 @@ int Game_choices(const Game *game, const char *out[TALK_CHOICES]) { if (!game->shouting) return Talk_choices(&game->talk, out); Game *g = (Game *)game; /* (the lines live in the game) */ int n = 0; + if (game->world.fest.stage == FEST_WARNED || game->world.fest.stage == FEST_STORM) out[n++] = "Shout: \"Storm! Everybody inside!\""; out[n++] = "Shout: \"Hey, everybody! Over here!\""; for (int k = 0; k < game->shout_count && n < TALK_CHOICES - 1; k++) { snprintf(g->shout_lines[k], sizeof(g->shout_lines[k]), "Call %s (%.0f m)", game->shout_to[k]->name, diff --git a/entropie/src/game/game_why.c b/entropie/src/game/game_why.c index 929e190..cbe39ae 100644 --- a/entropie/src/game/game_why.c +++ b/entropie/src/game/game_why.c @@ -121,5 +121,6 @@ int Game_notesLines(const Game *game, char lines[][128], int max, const char **t break; } } + n += game_festivalNotes(game, lines + n, max - n); return n; } diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c index 5e09b95..54fb12a 100644 --- a/entropie/src/game/save.c +++ b/entropie/src/game/save.c @@ -3,6 +3,7 @@ #include "game/game_internal.h" #include "world/car.h" #include "world/cast.h" +#include "world/guests.h" #include "world/clothes.h" #include "world/ladder.h" #include "world/tent.h" @@ -77,6 +78,8 @@ static const Field PERSON[] = { 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), IN("snails", snails), + IN("guest", guest), FL("fun", fun), IN("camp_spot", camp_spot), BO("gone_home", gone_home), BO("storm_told", storm_told), + VE("inside_door", inside_door), }; #define COUNT(a) ((int)(sizeof(a) / sizeof((a)[0]))) @@ -187,6 +190,9 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err, fprintf(f, "guide %d\n", a->guide); fprintf(f, "why %d %d %d %d\n", w->why[0], w->why[1], w->why[2], w->why_heard); fprintf(f, "weekend %d %.9g %d %d %d\n", game->weekend, (double)game->weekend_from, game->weekend_warned, game->fest_name, game->focus); + const Festival *fe = &game->festival; + fprintf(f, "festival %d %d %d %d %d %u %d %.9g %d %d\n", fe->stage, fe->came, fe->caught, fe->inside, fe->counted, fe->played, + fe->review, (double)fe->review_t, fe->result, fe->helped); 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); @@ -456,6 +462,12 @@ static void applyGame(Game *game, const Parsed *p) { a->welcome = numI(&s); a->welcome_t = numF(&s); a->chat = numI(&s); a->chat_wait = numF(&s); a->greeted = numI(&s) != 0; a->car = byId(p, numI(&s)); } else if (!strcmp(key, "told")) { game->rain_told = numI(&s) != 0; game->wet_told = numI(&s) != 0; } + else if (!strcmp(key, "festival")) { + Festival *fe = &game->festival; + fe->stage = numI(&s); fe->came = numI(&s); fe->caught = numI(&s); fe->inside = numI(&s); fe->counted = numI(&s) != 0; + fe->played = (unsigned)numI(&s); fe->review = numI(&s); fe->review_t = numF(&s); fe->result = numI(&s); fe->helped = numI(&s); + w->fest.helped = fe->helped; + } 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); @@ -511,7 +523,7 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t for (int k = 0; k < p.recs; k++) { Rec *r = &p.rec[k]; Vec3 pos = vec3_zero(), size = vec3_zero(), rot = vec3_zero(); - int cast = -1; + int cast = -1, guest = 0; for (int i = r->first; i < r->first + r->lines; i++) { char buf[256], *s = buf; snprintf(buf, sizeof buf, "%s", p.line[i]); @@ -520,6 +532,7 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t else if (!strcmp(key, "size")) size = numV(&s); else if (!strcmp(key, "rot")) rot = numV(&s); else if (!strcmp(key, "cast")) cast = numI(&s); + else if (!strcmp(key, "guest")) guest = numI(&s); } if (r->scene_id > 0) { r->obj = taken ? findScene(w, r, taken) : NULL; @@ -533,7 +546,7 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t switch (r->type) { case OBJ_CAR: o = Car_create(pos, rot.y); break; case OBJ_CLOTHES: o = Object_createDecal((Mesh){ 0 }, pos); if (o) o->type = OBJ_CLOTHES; break; - case OBJ_HUMAN: if (cast > 0) o = Cast_create(cast, pos); break; /* (the crew turned up after the start; the rest: the scene's) */ + case OBJ_HUMAN: if (cast > 0) o = Cast_create(cast, pos); else if (guest > 0) o = Guest_create(guest, pos); break; /* (the crew turned up after the start, the guests; the rest: the scene's) */ case OBJ_DECAL: case OBJ_STRUCTURE: case OBJ_WALL: case OBJ_PLAYER: case OBJ_DOG: break; /* (made by the scene only) */ default: o = Object_create(r->type, r->palette, pos, size, r->make); break; } diff --git a/entropie/src/game/sound.c b/entropie/src/game/sound.c index 0fefc2d..8ccd1b7 100644 --- a/entropie/src/game/sound.c +++ b/entropie/src/game/sound.c @@ -223,6 +223,19 @@ void Sound_update(Game *game, float dt, bool paused) { Audio_bed(BED_ENGINE, g, you && you->riding ? 0.0f : pan, game->arrival.speed / 5.5f); } else Audio_bed(BED_ENGINE, 0.0f, 0.0f, 0.0f); + /* the floors playing: each its own music, from its stage (far off only the bass and the kick carry) */ + for (int k = 0; k < FEST_FLOORS; k++) { + float pan = 0.0f, g = 0.0f, far = 1.0f; + if (w->fest.on & (1u << k)) { + Vec3 at = w->fest.dance[k]; + float d = hypotf(at.x - ear.eye.x, at.z - ear.eye.z); + hear(&ear, at, &pan, NULL); + g = 0.75f / (1.0f + d / 14.0f) * (tent ? 0.6f : 1.0f) * (Game_inBerlin(you) ? 0.0f : 1.0f); + far = clampf(d / 90.0f, 0.0f, 1.0f); + } + Audio_bed((Bed)(BED_FLOOR + k), g, pan, far); + } + /* night: crickets; day: birds (not in the rain, less in the wind) */ float hour = w->hour, calm = (1.0f - smoothstepf(0.05f, 0.4f, w->rain)) * (1.0f - 0.7f * wind); bool night = hour > 21.0f || hour < 5.0f; diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c index 352690c..199ef00 100644 --- a/entropie/src/people/ai.c +++ b/entropie/src/people/ai.c @@ -1143,7 +1143,7 @@ void Ai_setSettled(bool settled) { g_settled = settled; } static bool maybeApproach(Object *self, Object *const *objects, int count, const Object *player, float dt) { Ai *ai = &self->ai; - if (!g_settled || self->lives_here) return false; /* (settling in first; the owners don't do your jobs) */ + if (!g_settled || self->lives_here || self->guest) return false; /* (settling in first; the owners don't do your jobs, the guests neither) */ if (!player || ai->talk_cooldown > 0.0f || Body_isBusy(&player->body) || ai->annoyed > 0.3f) return false; /* (annoyed: not now) */ const Mood *m = &self->temperament; const Character *c = &ai->character; @@ -2514,6 +2514,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { ai_ensureReady(self); Ai *ai = &self->ai; self->body.look_weight = 0.0f; + self->body.dance_goal = 0.0f; /* (dancing: ai_fest.c, every frame) */ ai->speech_time = fmaxf(0.0f, ai->speech_time - dt); ai->talk_cooldown = fmaxf(0.0f, ai->talk_cooldown - dt); for (int i = 0; i < AI_SKIPS; i++) ai->skip_time[i] = fmaxf(0.0f, ai->skip_time[i] - dt); @@ -2585,6 +2586,8 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { /* The wind took something out of their hand: after it */ if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || ai->mode == AI_WORK || ai->mode == AI_FETCH) && fetchBlown(self, objects, count, dt)) return; + /* The festival: the storm, guests camping and leaving, dancing, the awareness team */ + if (ai->mode != AI_FOLLOW && ai->mode != AI_APPROACH && ai_festival(self, objects, count, dt)) return; /* Rain: inside */ if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || ai->mode == AI_WORK || ai->mode == AI_FETCH) && shelter(self, objects, count, dt)) return; diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h index 0ac855c..5d57384 100644 --- a/entropie/src/people/ai.h +++ b/entropie/src/people/ai.h @@ -271,6 +271,13 @@ typedef struct { struct Object *chat_with; /* BREAK_CHAT: who they're chatting with */ float chat_line; /* ... until the next thing said */ int chat_said; + + /* The festival (ai_fest.c) */ + int fest_floor; /* dancing at this floor (+1; 0 none) ... */ + Vec3 fest_spot; /* ... their place on it ... */ + float fest_time; /* ... real s at it so far (negative: till they feel like dancing again) */ + struct Object *fest_who; /* the storm: off to warn this one; the awareness team: off to see to this one */ + float fest_walk; /* ... on the way this long */ } Ai; /* Starts helping the leader; drops a bottle if needed. */ diff --git a/entropie/src/people/ai_fest.c b/entropie/src/people/ai_fest.c new file mode 100644 index 0000000..6488211 --- /dev/null +++ b/entropie/src/people/ai_fest.c @@ -0,0 +1,312 @@ +/* + * The festival weekend, for the people (the game says what's on: World.fest, + * game_festival.c): + * + * - Guests come in from the chicken farm's yard, over to their place at the + * camping, the tent out of its bag and up (ai.c pitchTents, the two of them). + * - A floor playing: over to it, a place on the dance floor in front of the + * stage, dancing to its beat for a good while, then a break (a drink, a pee, + * a sit down) and back. The better built the floor, the more fun. Sleepy: + * off to the tent. Evenings the party people drink; some too much. + * - The awareness team (said yes to at the Herberge): the crew see to anybody + * lying about drunk - water, sat up, okay again. + * - The storm: those who know it's coming (told by you, the crew, from a + * stage) go in - the main building, the barn, your garages' kitchen - and + * stay in till it's over. The crew tell the guests on their way in; with an + * awareness team they go round the whole place, the tents too. + * - Sunday: the guests' tents down (packed away), off out at the gate. + */ + +#include <math.h> +#include <stdio.h> + +#include "people/ai_internal.h" +#include "world/car.h" +#include "world/guests.h" +#include "world/tent.h" +#include "world/why.h" + +#define DANCE_SPREAD 4.5f /* m: places on the dance floor round its middle */ +#define DANCE_MIN 150.0f /* real s dancing at a go ... */ +#define DANCE_MORE 300.0f /* ... up to this much longer */ +#define DANCE_BREAK 60.0f /* real s off between goes (up to twice that) */ +#define FUN_DANCING 0.0009f /* fun a real second dancing (x how good the floor is) */ +#define FUN_BORED 0.00015f /* ... lost a real second awake with nothing playing (day and evening) */ +#define TELL_REACH 4.0f /* m: near enough to tell somebody about the storm */ +#define TELL_LOOK 25.0f /* m: the crew warn guests this near on their way in ... */ +#define TELL_LOOK_TEAM 140.0f /* ... the awareness team goes round everywhere */ +#define HELP_LOOK 45.0f /* m: the awareness team sees somebody lying drunk this near */ +#define GATE_REACH 1.5f + +static bool crewMember(const Object *o) { return o->type == OBJ_HUMAN && o->cast_id > 0; } + +static Object *guestNumber(Object *const *objects, int count, int k) { + for (int i = 0; i < count; i++) if (objects[i]->type == OBJ_HUMAN && objects[i]->guest == k) return objects[i]; + return NULL; +} + +static void stopDancing(Object *self) { + if (self->ai.fest_floor) self->ai.fest_time = -(DANCE_BREAK + DANCE_BREAK * random01(&self->ai)); + self->ai.fest_floor = 0; + self->body.dance_goal = 0.0f; +} + +static bool safeInside(const Object *o) { return o->indoors > 0.0f; } + +/* ---------- The storm ---------- */ + +/* The nearest who doesn't know yet (awake, or - the awareness team - asleep in a tent too), nobody else after them. */ +static Object *untold(const Object *self, Object *const *objects, int count, float look, bool tents) { + Object *best = NULL; + float best_d = look; + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o == self || o->type != OBJ_HUMAN || o->storm_told || o->gone_home || safeInside(o) || o->ai.meeting) continue; + if ((o->in_tent || o->body.asleep) && !tents) continue; + float d = flatDistance(o->position, self->position); + if (d >= best_d) continue; + bool taken = false; + for (int j = 0; j < count && !taken; j++) taken = objects[j] != self && objects[j]->type == OBJ_HUMAN && objects[j]->ai.fest_who == o; + if (!taken) { best_d = d; best = o; } + } + return best; +} + +static void tellStorm(Object *self, Object *o) { + o->storm_told = true; + if (o->in_tent) { o->rain_nap = false; Tent_crawlOut(o); } + if (o->body.asleep) Body_getUp(&o->body); + o->ai.talking = false; + static const char *const SAY[] = { "A storm's coming - get inside, quick! The barn or the house.", + "Storm warning! Not in the tent - into the barn!", + "Come on, inside - a big storm, any minute now!" }; + Ai_say(self, SAY[(int)(random01(&self->ai) * 3.0f) % 3]); + if (o->ai.speech_time <= 0.0f) Ai_say(o, random01(&o->ai) < 0.5f ? "What? Okay, okay - I'm going." : "Oh no. Right, inside."); +} + +static Vec3 nearestDoor(const Object *self) { + const Fest *f = &g_world->fest; + int best = 0; + for (int k = 1; k < 3; k++) + if (flatDistance(f->shelter[k], self->position) < flatDistance(f->shelter[best], self->position)) best = k; + return f->shelter[best]; +} + +static bool storm(Object *self, Object *const *objects, int count, float dt) { + Ai *ai = &self->ai; + const Fest *f = &g_world->fest; + if (f->stage != FEST_WARNED && f->stage != FEST_STORM) { ai->fest_who = NULL; return false; } + if (!self->storm_told || safeInside(self) || self->body.asleep || self->in_tent) return false; + stopDancing(self); + self->sitting = false; + /* the crew: telling the guests on the way (the awareness team: everybody, everywhere, the tents too) */ + if (crewMember(self) && f->stage == FEST_WARNED && !self->lives_here) { + Object *who = ai->fest_who; + if (who && (who->storm_told || safeInside(who) || (ai->fest_walk += dt) > 90.0f)) who = ai->fest_who = NULL; + if (!who) { + who = ai->fest_who = untold(self, objects, count, f->awareness ? TELL_LOOK_TEAM : TELL_LOOK, f->awareness); + ai->fest_walk = 0.0f; + } + if (who) { + Vec3 at = who->in_tent ? Tent_door(who->in_tent) : who->position; + if (ai_goToPoint(self, at, TELL_REACH, objects, count, dt)) { tellStorm(self, who); ai->fest_who = NULL; } + return true; + } + } + /* in: the nearest door */ + for (int h = 0; h < 2; h++) if (self->hands.item[h] && self->hands.item[h]->type != OBJ_UMBRELLA) Hands_drop(self, h); + Vec3 door = nearestDoor(self); + if (!ai_goToPoint(self, door, 1.2f, objects, count, dt)) { + self->movement = vec3_scale(self->movement, 1.3f); /* (hurrying) */ + return true; + } + self->inside_door = door; + self->indoors = 30.0f; + ai->sheltering = false; + return true; +} + +/* ---------- The awareness team ---------- */ + +static bool seeTo(Object *self, Object *const *objects, int count, float dt) { + Ai *ai = &self->ai; + if (!g_world->fest.awareness || !crewMember(self) || self->lives_here || self->drunk > 0.4f) return false; + if (Why_knows(self, WHY_AWARENESS) && (g_world->hour >= 18.0f || g_world->hour < 4.0f)) /* (on shift: awake till four) */ + self->needs.sleepiness = fminf(self->needs.sleepiness, 0.72f); + Object *who = ai->fest_who; + if (who && (!who->passed_out || !Body_isDown(&who->body) || (ai->fest_walk += dt) > 120.0f)) who = ai->fest_who = NULL; + if (!who) { + float best_d = HELP_LOOK; + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o == self || o->type != OBJ_HUMAN || !o->passed_out || !Body_isDown(&o->body) || o->in_tent) continue; + float d = flatDistance(o->position, self->position); + bool taken = false; + for (int j = 0; j < count && !taken; j++) taken = objects[j] != self && objects[j]->type == OBJ_HUMAN && objects[j]->ai.fest_who == o; + if (d < best_d && !taken) { best_d = d; who = o; } + } + if (!who) return false; + ai->fest_who = who; + ai->fest_walk = 0.0f; + Ai_say(self, "Somebody's down over there - I'll have a look."); + } + stopDancing(self); + if (!ai_goToPoint(self, who->position, 1.7f, objects, count, dt)) return true; + Hands_touch(self, vec3_add(who->position, vec3(0.0f, 0.3f, 0.0f))); + Ai_say(self, random01(ai) < 0.5f ? "Hey, are you okay? Here - drink some water." : "Easy now. Sit up - water, then you'll be fine."); + who->drunk = fminf(who->drunk, 0.45f); /* (water, sat up: back on their feet soon) */ + who->needs.sleepiness = fminf(who->needs.sleepiness, 0.5f); + who->fun = fminf(1.0f, who->fun + 0.08f); + g_world->fest.helped++; + ai->fest_who = NULL; + return true; +} + +/* ---------- Guests ---------- */ + +/* The evening: the party people drink (their own), some too much. */ +static void partyDrinks(Object *self) { + float h = g_world->hour, game_h = g_world->game_dt / 3600.0f; + bool evening = h >= 20.0f || h < 3.0f; + if (!evening || self->body.asleep) return; + float party = (float)((self->guest * 2654435761u) >> 24 & 255u) / 255.0f; /* (how much of a party person: their own) */ + float most = party < 0.6f ? 0.35f : 0.6f + 0.6f * (party - 0.6f) / 0.4f; /* (a few end up over 1: down) */ + if (self->drunk < most) self->drunk = fminf(most, self->drunk + 0.22f * game_h); +} + +/* Their place at the camping: over there, the tent out (the first of the two to get there). */ +static bool camp(Object *self, Object *const *objects, int count, float dt) { + if (self->camp_spot) return false; + Vec3 spot = Guest_campSpot(self->guest); + spot.y = self->position.y; + Vec3 side = vec3(self->guest % 2 ? -1.2f : 1.2f, 0.0f, 1.6f); /* (beside where the tent goes) */ + if (!ai_goToPoint(self, vec3_add(spot, side), 0.8f, objects, count, dt)) return true; + self->camp_spot = 1; + Object *mate = guestNumber(objects, count, Guest_mate(self->guest)); + bool there = Tent_of(objects, count, self) != NULL; + for (int i = 0; i < count && !there; i++) { + const Object *o = objects[i]; + there = (o->type == OBJ_TENT && !o->broken && (o->owner == self || (mate && o->owner == mate))) + || (o->type == OBJ_GEAR && o->variant >= GEAR_TENT_SMALL && !o->broken && (o->owner == self || (mate && o->owner == mate))); + } + if (!there) { + spot.y = World_groundHeight(g_world, spot.x, spot.z, self->position.y + 2.0f) + 0.02f; + Object *bag = Gear_create(GEAR_TENT_BIG, self, spot); + if (bag && !World_add(g_world, bag)) bag = NULL; + if (bag) { + Object *tent = Tent_unpack(g_world, bag, Guest_campDoor()); + if (tent) tent->owner2 = mate; + Ai_say(self, random01(&self->ai) < 0.5f ? "Here we are - tent up, then the music!" : "This is our spot. Right, the tent."); + } + } + return true; +} + +static float leaveHour(const Object *self) { return 10.0f + 4.5f * (float)((self->guest - 1) / 2) / (float)GUEST_SPOTS; } /* (two by two, from ten) */ + +/* Sunday: off home - the tent down and gone, out at the gate. */ +static bool leave(Object *self, Object *const *objects, int count, float dt) { + if (g_world->fest.stage != FEST_LEAVING) return false; + if (g_world->hour < leaveHour(self)) return false; + stopDancing(self); + self->sitting = false; + if (self->in_tent) { self->rain_nap = false; Tent_crawlOut(self); } + if (self->body.asleep) { Body_getUp(&self->body); return true; } + Object *tent = Tent_of(objects, count, self); + if (tent) { /* (packed up and away with them) */ + if (!ai_goToPoint(self, Tent_door(tent), 1.2f, objects, count, dt)) return true; + Tent_takeDown(objects, count, tent); + Object_useUp(tent); + Ai_say(self, "That's the tent packed. What a weekend!"); + return true; + } + Vec3 gate = Guest_gate(); + gate.y = self->position.y; + self->needs.sleepiness = fminf(self->needs.sleepiness, 0.6f); /* (they'll sleep in the car) */ + if (!ai_goToPoint(self, gate, GATE_REACH, objects, count, dt) && (self->ai.fest_walk += dt) < 400.0f) return true; /* (lost on the way: gone anyway) */ + freeHands(self, NULL); + self->gone_home = true; + self->inside_door = Guest_gate(); + self->indoors = 1e7f; + return true; +} + +/* A floor playing: dancing there a while. */ +static bool dance(Object *self, Object *const *objects, int count, float dt) { + Ai *ai = &self->ai; + const Fest *f = &g_world->fest; + if (ai->fest_floor && !(f->on & (1u << (ai->fest_floor - 1)))) stopDancing(self); /* (the music stopped) */ + if (ai->fest_time < 0.0f && !ai->fest_floor) { ai->fest_time += dt; return false; } + bool guest = self->guest > 0; + if (!guest && !ai->fest_floor && (g_world->hour > 3.0f && g_world->hour < 19.0f)) return false; /* (the crew: evenings and nights) */ + if (self->needs.sleepiness > 0.8f || self->needs.bladder > 0.8f || self->drunk > 0.9f || Body_isSmoking(&self->body)) { stopDancing(self); return false; } + if (!ai->fest_floor) { + if (!f->on) return false; + float pick = random01(ai) * 1.2f, sum = 0.0f; /* (the better floors pull more; each guest a favourite) */ + int fav = guest ? (self->guest * 7) % FEST_FLOORS : self->cast_id % FEST_FLOORS; + float w[FEST_FLOORS], all = 0.0f; + for (int k = 0; k < FEST_FLOORS; k++) { w[k] = (f->on & (1u << k)) ? f->good[k] * (k == fav ? 2.0f : 1.0f) : 0.0f; all += w[k]; } + if (all <= 0.0f) return false; + for (int k = 0; k < FEST_FLOORS && !ai->fest_floor; k++) { sum += w[k] / all * 1.2f; if (pick <= sum) ai->fest_floor = k + 1; } + if (!ai->fest_floor) { ai->fest_time = -DANCE_BREAK; return false; } /* (not now) */ + float a = 6.2832f * random01(ai), r = DANCE_SPREAD * sqrtf(random01(ai)); + ai->fest_spot = vec3_add(f->dance[ai->fest_floor - 1], vec3(cosf(a) * r, 0.0f, sinf(a) * r)); + ai->fest_time = 0.0f; + if (random01(ai) < 0.3f) Ai_say(self, ai->fest_floor == 2 ? "The Obstwiese's going off - come on!" : "I love this one - let's dance!"); + } + int k = ai->fest_floor - 1; + self->sitting = false; + if (flatDistance(self->position, ai->fest_spot) > 0.9f) { + self->body.dance_goal = 0.0f; + if (!ai_goToPoint(self, ai->fest_spot, 0.6f, objects, count, dt)) { + if ((ai->fest_walk += dt) > 150.0f) { stopDancing(self); ai->fest_walk = 0.0f; } /* (can't get there) */ + return true; + } + } + ai->fest_walk = 0.0f; + stand(self); + turnTowards(self, vec3_scale(f->front[k], -1.0f), dt); + self->body.dance_goal = 1.0f; + self->body.dance_beat = f->clock * f->bpm[k] / 60.0f + 0.03f * (float)(self->guest % 3); + self->body.dance_style = guest ? self->guest % 4 : self->cast_id % 4; + if (guest) self->fun = fminf(1.0f, self->fun + FUN_DANCING * f->good[k] * dt); + if ((ai->fest_time += dt) > DANCE_MIN + DANCE_MORE * (float)((self->guest * 13 + self->cast_id * 7) % 10) / 10.0f) stopDancing(self); + return true; +} + +bool ai_festival(Object *self, Object *const *objects, int count, float dt) { + if (!g_world) return false; + const Fest *f = &g_world->fest; + if (f->stage == FEST_NONE) { self->body.dance_goal = 0.0f; self->ai.fest_floor = 0; return false; } + if (self->guest) { /* (they brought their own food and drink: the crew's is the crew's) */ + self->needs.hunger = fminf(self->needs.hunger, 0.3f); + self->needs.thirst = fminf(self->needs.thirst, 0.3f); + partyDrinks(self); + if (!f->on && !self->body.asleep && g_world->hour > 9.0f && f->stage == FEST_OPEN) + self->fun = fmaxf(0.0f, self->fun - FUN_BORED * dt); + } + if (self->guest && f->stage == FEST_LEAVING && (self->in_tent || self->body.asleep) && g_world->hour >= leaveHour(self)) { + if (self->in_tent) { self->rain_nap = false; Tent_crawlOut(self); } /* (time to go: up, even after a long night) */ + if (self->body.asleep) { self->drunk = fminf(self->drunk, 0.5f); self->needs.sleepiness = fminf(self->needs.sleepiness, 0.6f); Body_getUp(&self->body); } + return true; + } + if (Body_isDown(&self->body) || self->in_tent || self->body.asleep) return false; + if (storm(self, objects, count, dt)) return true; + if (f->stage == FEST_WARNED || f->stage == FEST_STORM) { stopDancing(self); return false; } /* (not told: they carry on - the rain, a tent...) */ + AiMode m = self->ai.mode; + if (m != AI_IDLE && m != AI_WANDER && m != AI_TABLE) { stopDancing(self); return false; } /* (busy with something) */ + if (seeTo(self, objects, count, dt)) return true; + if (self->guest) { + if (leave(self, objects, count, dt)) return true; + if (camp(self, objects, count, dt)) return true; + Object *tent = Tent_of(objects, count, self); + if (tent && tent->tent_stage < TENT_UP) { stopDancing(self); return false; } /* (the tent first: ai.c pitchTents) */ + if (self->needs.sleepiness > 0.8f && tent && tent->tent_stage >= TENT_UP && flatDistance(tent->position, self->position) > 50.0f) { + stopDancing(self); /* (sleepy: back to the tent - ai.c goToBed takes it from there) */ + ai_goToPoint(self, Tent_door(tent), 2.0f, objects, count, dt); + return true; + } + } + if (f->stage != FEST_OPEN) { stopDancing(self); return false; } + return dance(self, objects, count, dt); +} diff --git a/entropie/src/people/ai_internal.h b/entropie/src/people/ai_internal.h index 409298c..7feb2d4 100644 --- a/entropie/src/people/ai_internal.h +++ b/entropie/src/people/ai_internal.h @@ -107,6 +107,8 @@ 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_fest.c: the festival weekend - guests camping, dancing, leaving; the storm; the awareness team. True: that's what they do now. */ +bool ai_festival(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); diff --git a/entropie/src/people/ai_project.c b/entropie/src/people/ai_project.c index 68abfbf..f9c4775 100644 --- a/entropie/src/people/ai_project.c +++ b/entropie/src/people/ai_project.c @@ -241,6 +241,7 @@ bool ai_projectWork(Object *self, Object *const *objects, int count, float dt) { float hour = g_world->hour; /* not now */ if (hour < WORK_FROM || hour > WORK_TO) return stop(self, lead ? "That's it for today." : NULL); + if (g_world->fest.stage >= FEST_OPEN) return stop(self, NULL); /* (the festival's on: what's built is built) */ if (self->needs.stamina < TIRED) return stop(self, "I need a break."); if (self->needs.hunger > NEED_STOP || self->needs.thirst > NEED_STOP || self->needs.bladder > NEED_STOP) return stop(self, NULL); if (self->drunk > 0.5f || self->body.asleep || self->in_tent || self->riding || self->indoors > 0.0f || ai->sheltering || ai->errand) return stop(self, NULL); diff --git a/entropie/src/people/body.c b/entropie/src/people/body.c index 15909e2..0a78517 100644 --- a/entropie/src/people/body.c +++ b/entropie/src/people/body.c @@ -962,6 +962,8 @@ static void inTheAir(Body *b, const BodyFrame *f, float s, float speed) { static void updateBalance(Body *b, const BodyFrame *f, float speed, float dt) { b->balance += (b->balance_goal - b->balance) * fminf(1.0f, (b->balance_goal > b->balance ? 4.0f : 2.0f) * dt); + b->dance += (b->dance_goal - b->dance) * fminf(1.0f, 2.0f * dt); + if (b->dance < 0.002f) b->dance = 0.0f; if (b->balance < 0.02f || b->airborne || b->on_ladder || dt <= 0.0f) { b->teeter *= fmaxf(0.0f, 1.0f - 3.0f * dt); b->teeter_v = 0.0f; diff --git a/entropie/src/people/body.h b/entropie/src/people/body.h index cbffead..91740f6 100644 --- a/entropie/src/people/body.h +++ b/entropie/src/people/body.h @@ -94,7 +94,11 @@ typedef struct { Vec3 hand_target[2]; /* world space palm position */ float unsteadiness; /* 0 = sober .. 1 = drunk: sway and misplaced steps */ float balance_goal; /* 0 .. 1: standing on something narrow (a log) - set by the world */ + float dance_goal; /* 0 .. 1: dancing (a floor playing: knees on the beat, arms going) ... */ + float dance_beat; /* ... where in the music (beats, runs on) ... */ + int dance_style; /* ... how they dance (0 .. 3: bouncing, swaying, arms up, stomping) */ float balance; /* ... eased: arms out, teetering */ + float dance; /* ... eased: how much they're dancing */ float teeter, teeter_v; /* -1 .. 1 tipping to the left / right (past 1: a step off) */ bool stepped_off; /* lost the balance just now (the world says so; taken) */ float exertion; /* 0 = fresh .. 1 = exhausted: heavy breathing */ diff --git a/entropie/src/people/body_pose.c b/entropie/src/people/body_pose.c index fce93a3..00c7ce0 100644 --- a/entropie/src/people/body_pose.c +++ b/entropie/src/people/body_pose.c @@ -498,6 +498,14 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt float bob = WALK_BOB * walk * (2.0f * swing - 1.0f) * (1.2f - 0.4f * b->fitness) * (1.0f + 0.3f * b->style[2]); goal_y = fminf(goal_y + bob, reach_y); } + /* Dancing: down into the knees on every beat (stompers deeper, swayers softer) */ + const float dance = b->dance * still, beat = b->dance_beat - floorf(b->dance_beat); + const float on_beat = 0.5f + 0.5f * cosf(2.0f * PI_F * beat); /* (1 on the beat, 0 between) */ + const float two = sinf(PI_F * fmodf(b->dance_beat, 2.0f)); /* (-1..1 over two beats: side to side) */ + if (dance > 0.001f) { + static const float DEPTH[4] = { 0.05f, 0.025f, 0.045f, 0.075f }; + goal_y = fminf(goal_y - dance * (0.015f + DEPTH[b->dance_style & 3] * on_beat), reach_y); + } /* The hips follow on a spring: they speed up and slow down, never jolt */ { const float was = b->pelvis_y; @@ -528,6 +536,7 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt float drift_x = still * (0.016f * sinf(b->clock * 0.83f + 3.0f * b->quirk[0]) + 0.007f * sinf(b->clock * 1.91f + 1.0f)); float drift_z = still * (0.011f * sinf(b->clock * 0.61f + 2.0f * b->quirk[1]) + 0.005f * sinf(b->clock * 1.37f)); sway += drift_x; + sway += dance * (b->dance_style == 1 ? 0.04f : 0.022f) * two; /* (dancing: the hips from side to side) */ /* Weight moving from foot to foot on springs: steps beginning and ending never jolt the hips */ springCritical(&b->sway_s, &b->sway_v, sway, SWAY_OMEGA, dt); sway = b->sway_s; @@ -574,7 +583,8 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt /* Hips turn a little with the forward leg, shoulders counter-turn */ float hip_turn = clampf(stride * 0.2f, -0.12f, 0.12f); - float chest_turn = -0.9f * hip_turn + clampf(b->turn_rate * 0.06f, -0.15f, 0.15f) + b->reach_turn; + float chest_turn = -0.9f * hip_turn + clampf(b->turn_rate * 0.06f, -0.15f, 0.15f) + b->reach_turn + + dance * 0.16f * sinf(PI_F * fmodf(b->dance_beat + 0.5f, 2.0f)); /* (dancing: the shoulders go the other way) */ float swing_yaw = 0.0f; /* (the head goes along a little: below) */ { /* Both hands busy off to one side (a swing, a saw): the shoulders turn that way, the hips a little too; up high (winding up) the back arches, down low (the hit) it bends into it */ @@ -837,6 +847,25 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt if (smoke > 0.001f) blendArm(&arm, palm, pole, smoke); } + /* Dancing: fists pumping at the chest, swinging low, both up in the air now and then, one fist up on the beat */ + float dw = dance * (1.0f - b->hand_weight[side]) * (1.0f - fmaxf(bent, pee)); + if (dw > 0.001f) { + float bar = fmodf(b->dance_beat, 16.0f), up = smoothstepf(7.5f, 8.5f, bar) * (1.0f - smoothstepf(15.0f, 16.0f, bar)); + float pump = 0.07f * (on_beat - 0.5f) * (side ? 1.0f : -1.0f) * (b->dance_style == 0 ? 1.0f : 0.6f); + Vec3 palm = vec3(sign * 0.21f, PELVIS_Y + SPINE - 0.22f + pump, 0.24f), pole = vec3(sign, -0.6f, -0.3f); + switch (b->dance_style & 3) { + case 1: palm = vec3(sign * 0.24f + 0.1f * two, PELVIS_Y + SPINE - 0.36f + 0.03f * on_beat, 0.18f); break; + case 2: palm = vec3_lerp(palm, vec3(sign * 0.27f, PELVIS_Y + SPINE + 0.55f + 0.04f * on_beat, 0.08f + 0.04f * two * sign), up); + pole = vec3_lerp(pole, vec3(sign, 0.2f, -0.2f), up); break; + case 3: if (sign * (b->quirk[0] >= 0.0f ? 1.0f : -1.0f) > 0.0f) { /* (the fist up in the air: on their stronger side) */ + palm = vec3(sign * 0.18f, PELVIS_Y + SPINE + 0.3f + 0.16f * on_beat, 0.3f); + pole = vec3(sign, 0.0f, -0.4f); + } break; + default: break; + } + blendArm(&arm, palm, pole, dw); + } + /* Actions: reaching, carrying, pushing */ float w = b->hand_weight[side]; Vec3 target = vec3_scale(Figure_toLocal(position, facing, b->hand_target[side]), 1.0f / s); diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c index 94234c2..07025c9 100644 --- a/entropie/src/people/talk.c +++ b/entropie/src/people/talk.c @@ -93,6 +93,11 @@ static const char *festivalLine(const Object *o) { static char line[AI_SPEECH_MAX]; const char *mood = o->commitment > 0.7f ? "Going great!" : o->commitment > 0.45f ? "It's okay." : o->commitment > 0.3f ? "Honestly, I'm getting fed up." : "I'm about done with it."; + if (o->guest) { /* (a guest: how they like it) */ + snprintf(line, sizeof line, "%s", o->fun > 0.85f ? "Best festival ever! I'm coming back next year." : o->fun > 0.65f ? "Great music, great people. Love it!" + : o->fun > 0.45f ? "It's nice. Could be more going on." : "Honestly? Bit boring so far."); + return line; + } int p = o->leads != PROJECT_NONE ? o->leads : o->helps; if (p == PROJECT_NONE) { snprintf(line, sizeof line, "Me? Nothing really. %s", o->commitment > 0.45f ? "Could do something, though." : ""); return line; } const Object *s = g_talk_world && floorProject(p) ? Stage_of(g_talk_world->objects, g_talk_world->count, (Project)p) : NULL; @@ -477,7 +482,7 @@ static void answer(Talk *talk, World *world, Object *player, int choice) { } else if (choice == 3 && lighterTopic(player, npc) != LIGHTER_NONE) { talkLighter(talk, player, lighterTopic(player, npc)); } else if (choice == 3) { - Ai_say(npc, npc->ai.character.about ? npc->ai.character.about : "Me? Nobody special."); + Ai_say(npc, npc->ai.character.about ? npc->ai.character.about : npc->guest ? "Here for the festival! A friend told me about it." : "Me? Nobody special."); } else if (choice == 4) go(talk, NODE_FESTIVAL); else wrapUp(talk, "See you."); break; diff --git a/entropie/src/world/guests.c b/entropie/src/world/guests.c new file mode 100644 index 0000000..e4885a9 --- /dev/null +++ b/entropie/src/world/guests.c @@ -0,0 +1,94 @@ +#include "world/guests.h" +#include "world/cast.h" +#include "render/figure.h" + +#include <math.h> +#include <stdio.h> + +static const char *const WOMEN[] = { "Lena", "Mia", "Hanna", "Sophie", "Lea", "Marie", "Nele", "Clara", "Ida", "Frieda", + "Paula", "Greta", "Emma", "Luisa", "Merle", "Ronja", "Alma", "Jule" }; +static const char *const MEN[] = { "Jonas", "Felix", "Paul", "Ben", "Lukas", "Moritz", "Finn", "Emil", "Theo", "Anton", + "Jakob", "Niklas", "Ole", "Malte", "Hannes", "Bruno", "Leon", "Max" }; +#define COUNTOF(a) ((int)(sizeof(a) / sizeof((a)[0]))) + +static unsigned mix(unsigned h) { + h ^= h >> 16; h *= 0x7feb352du; h ^= h >> 15; h *= 0x846ca68bu; h ^= h >> 16; + return h; +} +/* 0..1, the same for the same guest and question */ +static float dice(int k, unsigned q) { return (float)(mix((unsigned)k * 7919u + q * 104729u) & 0xffffu) / 65535.0f; } +static Vec3 pick(const Vec3 *from, int n, float d) { return from[(int)(d * (float)n) % n]; } + +static const Vec3 SKINS[] = { { 0.96f, 0.80f, 0.68f }, { 0.92f, 0.74f, 0.60f }, { 0.86f, 0.66f, 0.50f }, { 0.72f, 0.52f, 0.38f }, + { 0.56f, 0.38f, 0.26f }, { 0.40f, 0.27f, 0.19f } }; +static const Vec3 HAIRS[] = { { 0.12f, 0.10f, 0.10f }, { 0.30f, 0.19f, 0.12f }, { 0.55f, 0.38f, 0.22f }, { 0.80f, 0.66f, 0.40f }, + { 0.70f, 0.30f, 0.16f }, { 0.20f, 0.45f, 0.55f }, { 0.75f, 0.35f, 0.60f } }; +static const Vec3 SHIRTS[] = { { 0.88f, 0.86f, 0.80f }, { 0.12f, 0.12f, 0.14f }, { 0.86f, 0.30f, 0.30f }, { 0.25f, 0.45f, 0.75f }, + { 0.95f, 0.70f, 0.20f }, { 0.40f, 0.65f, 0.40f }, { 0.65f, 0.40f, 0.75f }, { 0.95f, 0.55f, 0.65f }, + { 0.20f, 0.70f, 0.70f } }; +static const Vec3 PANTS[] = { { 0.22f, 0.26f, 0.40f }, { 0.15f, 0.15f, 0.16f }, { 0.50f, 0.44f, 0.34f }, { 0.35f, 0.38f, 0.30f }, + { 0.60f, 0.25f, 0.30f }, { 0.75f, 0.72f, 0.65f } }; + +int Guest_mate(int k) { return k % 2 ? k + 1 : k - 1; } + +Object *Guest_create(int k, Vec3 at) { + bool female = dice(k, 1) < 0.5f; + float kg = female ? 55.0f + 20.0f * dice(k, 2) : 68.0f + 25.0f * dice(k, 2); + Object *o = Object_create(OBJ_HUMAN, PALETTE_MODERN, at, vec3(0.5f, 1.75f, 0.25f), kg); + if (!o) return NULL; + o->guest = k; + char name[32]; + snprintf(name, sizeof name, "%s", female ? WOMEN[(int)(dice(k, 3) * COUNTOF(WOMEN)) % COUNTOF(WOMEN)] + : MEN[(int)(dice(k, 3) * COUNTOF(MEN)) % COUNTOF(MEN)]); + Object_setPerson(o, name, dice(k, 4) < 0.12f ? HAND_LEFT : HAND_RIGHT); + FigureLook worn = Figure_wardrobe(5 + k); + o->look = worn; + o->look.skin = pick(SKINS, COUNTOF(SKINS), dice(k, 5)); + o->look.hair = pick(HAIRS, COUNTOF(HAIRS), dice(k, 6) * (dice(k, 7) < 0.8f ? 0.57f : 1.0f)); /* (mostly natural) */ + o->look.shirt = pick(SHIRTS, COUNTOF(SHIRTS), dice(k, 8)); + o->look.pants = pick(PANTS, COUNTOF(PANTS), dice(k, 9)); + o->look.shoes = vec3(0.2f + 0.6f * dice(k, 10), 0.18f + 0.5f * dice(k, 10), 0.16f + 0.4f * dice(k, 11)); + o->look.height = female ? 1.58f + 0.17f * dice(k, 12) : 1.70f + 0.18f * dice(k, 12); + o->look.hair_style = female ? (dice(k, 13) < 0.8f ? HAIR_LONG : HAIR_SHORT) : (dice(k, 13) < 0.2f ? HAIR_LONG : HAIR_SHORT); + o->look.long_sleeves = dice(k, 14) < 0.3f; + float g = dice(k, 15); + o->look.garment = female ? (g < 0.3f ? GARMENT_SKIRT : g < 0.5f ? GARMENT_DRESS : g < 0.75f ? GARMENT_SHORTS : GARMENT_TROUSERS) + : (g < 0.45f ? GARMENT_SHORTS : GARMENT_TROUSERS); + o->look.hat = dice(k, 16) < 0.2f ? HAT_CAP : dice(k, 16) < 0.28f ? HAT_BEANIE : HAT_NONE; + o->look.hat_color = pick(SHIRTS, COUNTOF(SHIRTS), dice(k, 17)); + o->female = female; + o->urinella = false; + o->urinellas = 0; + o->temperament.pride = dice(k, 18) - 0.5f; + o->temperament.energy = 0.2f + 0.7f * dice(k, 19); /* (festival people: lively) */ + o->temperament.nerves = 0.6f * dice(k, 20) - 0.3f; + o->tolerance = 0.7f + 0.6f * dice(k, 21); + o->fitness = 0.3f + 0.6f * dice(k, 22); + o->wits = 0.4f + 0.4f * dice(k, 23); + o->height_fear = 0.5f * dice(k, 24); + o->smoker = dice(k, 25) < 0.4f ? 0.3f + 0.5f * dice(k, 26) : 0.0f; + o->cigarettes = o->smoker > 0.0f ? 6 + (int)(14.0f * dice(k, 27)) : 0; + o->lighter = o->smoker > 0.0f; + o->craving = 0.3f; + o->commitment = 0.0f; + o->fun = 0.55f + 0.15f * dice(k, 28); + o->ai.character.willing = 0.2f + 0.3f * dice(k, 29); + o->ai.character.tempo = 1.0f; + o->ai.character.ponder = 0.5f; + o->tempo = 1.0f; + for (int q = 0; q < ORDER_COUNT; q++) o->skill[q] = 0.2f + 0.3f * dice(k, 30 + (unsigned)q); + Object_setHeight(o, o->look.height); + o->pace = 1.0f + 0.12f * o->temperament.energy; + return o; +} + +/* Up on the meadow right of the strip of wood, below the lane to the chicken halls: three rows of six. */ +Vec3 Guest_campSpot(int k) { + int pair = ((k - 1) / 2) % GUEST_SPOTS; + int col = pair % 6, row = pair / 6; + return vec3(9.0f + 4.0f * (float)col + (row % 2 ? 1.3f : 0.0f), 0.0f, 86.0f + 3.8f * (float)row); +} + +Vec3 Guest_campDoor(void) { return vec3(0.0f, 0.0f, -1.0f); } + +Vec3 Guest_gate(void) { return vec3(63.0f, 0.0f, 89.5f); } diff --git a/entropie/src/world/guests.h b/entropie/src/world/guests.h new file mode 100644 index 0000000..d381f6a --- /dev/null +++ b/entropie/src/world/guests.h @@ -0,0 +1,26 @@ +#ifndef WORLD_GUESTS_H +#define WORLD_GUESTS_H + +/* + * The festival's guests: made up from their number (a name, how they look, + * how they are), always the same for the same number - a save only needs + * the number. Two and two share a tent at the camping beyond the strip of + * wood, above the chicken farm. + */ + +#include "world/world.h" + +#define GUESTS 36 /* how many come */ +#define GUEST_SPOTS (GUESTS / 2) + +/* Guest number k (1 .. GUESTS) standing at at. */ +Object *Guest_create(int k, Vec3 at); +/* The tent spot of guest k's pair (on the ground's height: y 0), the way its door looks. */ +Vec3 Guest_campSpot(int k); +Vec3 Guest_campDoor(void); +/* Where they come in on foot (they parked in the chicken farm's yard) and go again. */ +Vec3 Guest_gate(void); +/* The other one of the pair (shares the tent): its number. */ +int Guest_mate(int k); + +#endif /* WORLD_GUESTS_H */ diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h index fc9be94..eb7a8e8 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -321,6 +321,12 @@ typedef struct Object { 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 guest; /* people: a guest at the festival (which: world/guests.c), 0 none ... */ + float fun; /* ... 0..1 how much they enjoy it ... */ + int camp_spot; /* ... got to their place at the camping (1; 0 not yet) ... */ + bool gone_home; /* ... left on Sunday (out of sight at the gate, nothing more) */ + bool storm_told; /* people: told the storm's coming - in under a roof (the festival) */ + Vec3 inside_door; /* people: indoors (above), at this door (zero: the main building's) */ int leads, helps; /* people: the Project they lead, one they help with (world/cast.h; PROJECT_NONE 0) */ struct { /* dogs */ int mode; diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c index b256195..5e71dc2 100644 --- a/entropie/src/world/site.c +++ b/entropie/src/world/site.c @@ -1288,6 +1288,8 @@ Vec3 Site_campPath(void) { return court(36.0f, -11.0f); } /* (the foot of the pa 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_barnDoor(void) { return court(BARN_A - BARN_WIDTH * 0.5f - 0.9f, BARN_B - 10.0f); } /* (its side to the yard, left of the Hofbuehne's stage) */ + 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) { diff --git a/entropie/src/world/site.h b/entropie/src/world/site.h index fdcac8e..4e2e58c 100644 --- a/entropie/src/world/site.h +++ b/entropie/src/world/site.h @@ -35,6 +35,8 @@ 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); +/* The storm: in at the barn's door here (out of sight inside, like the main building). */ +Vec3 Site_barnDoor(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); diff --git a/entropie/src/world/tent.c b/entropie/src/world/tent.c index f7dd374..0c0599c 100644 --- a/entropie/src/world/tent.c +++ b/entropie/src/world/tent.c @@ -34,6 +34,11 @@ static Vec3 toLocal(const Object *tent, Vec3 w) { /* Everybody's own cover colour (by name, like their other things). */ static Vec3 flyColor(const Object *tent) { const Object *o = tent->owner; + if (o && o->guest) { /* the guests': all sorts */ + static const Vec3 FLY[] = { { 0.78f, 0.22f, 0.20f }, { 0.20f, 0.42f, 0.70f }, { 0.85f, 0.62f, 0.15f }, { 0.25f, 0.55f, 0.35f }, + { 0.55f, 0.30f, 0.62f }, { 0.15f, 0.60f, 0.62f }, { 0.60f, 0.60f, 0.58f }, { 0.88f, 0.42f, 0.18f } }; + return FLY[((o->guest - 1) / 2) % 8]; + } if (tent->variant) return vec3(0.40f, 0.44f, 0.24f); /* the big one: olive */ if (!o || o->type == OBJ_PLAYER) return vec3(0.22f, 0.36f, 0.56f); /* yours: blue */ if (!strcmp(o->name, "Toni")) return vec3(0.78f, 0.42f, 0.16f); /* Toni's: orange */ diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c index cbcc96e..12a606e 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -1546,11 +1546,12 @@ void World_update(World *world, float dt, float game_dt) { if (o->indoors > 0.0f) { /* in the house: out of sight a while, then out at the door */ if (o->ai.sheltering && world->rain > 0.1f) o->indoors = fmaxf(o->indoors, 25.0f + 10.0f * (float)(i % 4)); /* (till the rain's over) */ o->indoors -= dt; - Vec3 door = Site_houseDoor(); + if (world->fest.stage == FEST_STORM || world->fest.stage == FEST_WARNED) o->indoors = fmaxf(o->indoors, 20.0f); /* (the storm: in till it's over) */ + Vec3 door = vec3_length(o->inside_door) > 0.1f ? o->inside_door : Site_houseDoor(); o->position = vec3(door.x, Terrain_height(&world->terrain, door.x, door.z), door.z); o->velocity = o->movement = vec3_zero(); Object_syncColliders(o); - if (o->indoors <= 0.0f) { o->indoors = 0.0f; o->body.replant = true; o->ai.sheltering = false; } + if (o->indoors <= 0.0f) { o->indoors = 0.0f; o->body.replant = true; o->ai.sheltering = false; o->inside_door = vec3_zero(); } continue; } if (o->type == OBJ_HUMAN && !o->riding) { Ai_useGrid(&world->grid); Ai_useWorld(world); Ai_update(o, world->objects, world->count, dt); } diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h index be69dc4..93d9981 100644 --- a/entropie/src/world/world.h +++ b/entropie/src/world/world.h @@ -47,6 +47,23 @@ enum { FOCUS_NONE, FOCUS_FLOORS, FOCUS_SIDE, FOCUS_PARTY }; enum { WHY_URINELLA, WHY_AWARENESS, WHY_SHIRT, WHY_COUNT }; enum { WHY_UNKNOWN, WHY_RAISED, WHY_UNDERSTOOD, WHY_YES, WHY_NO }; /* (how far each is: World.why) */ +/* The festival weekend (game_festival.c): what's on, for the people (ai_fest.c). */ +enum { FEST_NONE, FEST_SETUP, FEST_OPEN, FEST_WARNED, FEST_STORM, FEST_LEAVING, FEST_OVER }; +#define FEST_FLOORS 4 +typedef struct { + int stage; /* FEST_*: none (a build weekend), the last bits before the gates open, open, ... */ + unsigned on; /* bit k: floor k playing now (0 Hofbuehne, 1 Obstwiese, 2 Kartoffelacker, 3 hidden) */ + float good[FEST_FLOORS]; /* how good each floor is (0: not built enough to play) */ + Vec3 dance[FEST_FLOORS]; /* the middle of the dance floor in front of each stage ... */ + Vec3 front[FEST_FLOORS]; /* ... and which way the stage looks */ + float bpm[FEST_FLOORS]; /* each floor's tempo */ + float clock; /* real s: the beat's clock (everybody on a floor moves to it) */ + Vec3 shelter[3]; /* the storm: doors to get in at (the main building, the barn, your garages' kitchen) */ + bool awareness; /* the why questions said yes to: an awareness team on shift ... */ + bool shirts; /* ... no shirt, no service */ + int helped; /* the awareness team saw to somebody lying drunk: this often (counted by them, ai_fest.c) */ +} Fest; + typedef struct World { float dt_props; /* (this frame's step, for the props that move: gates) */ float game_dt; /* (this frame's step in game seconds) */ @@ -91,6 +108,7 @@ typedef struct World { int focus; /* FOCUS_*: what this weekend is for (the game sets it) */ int why[WHY_COUNT]; /* the why questions: WHY_UNKNOWN .. WHY_NO (game_why.c; talk.c answers them) ... */ int why_heard; /* ... bit k: came up at the Herberge (not seen on the ground) */ + Fest fest; /* the festival weekend (set by the game every frame) */ } World; /* A sound made here (dropped when the list is full: nobody's listening). */