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.28.0: sound, sorted crates, hoods and umbrellas
- Sound: steps on every ground, wind, rain, thunder, fire, axe, chainsaw, the car, bottles, glass, falls, a tree coming down, birds and crickets - all made up on the fly (M: off / on) - Arrival: everybody gets a seat by the fire (people move up on the bench; no room: they stand with the others) - Crates know each bottle: put a bottle down next to a crate and it goes in; right click at a crate takes out one that does not belong; the tidy sort the crates - Rain and smoke show against the sky; the sky clouds over with the weather - Rain soaks the ground wherever people walk: mud on trodden paths - Hoods go up in the rain; two umbrellas to fetch and carry - The very sporty roll off drops of 5 m and more
commit_message.txt | 18 ++- entropie/ARCHITECTURE.md | 31 +++- entropie/Makefile | 2 +- entropie/README.md | 15 +- entropie/STATE.md | 53 ++++++- entropie/assets/shaders/figure.frag | 19 ++- entropie/assets/shaders/skybox.frag | 18 ++- entropie/commit_message.txt | 18 ++- entropie/src/core/audio.c | 301 ++++++++++++++++++++++++++++++++++++ entropie/src/core/audio.h | 55 +++++++ entropie/src/core/input.c | 1 + entropie/src/core/input.h | 1 + entropie/src/game/game.c | 19 +++ entropie/src/game/game.h | 3 + entropie/src/game/game_actions.c | 69 ++++++++- entropie/src/game/save.c | 27 +++- entropie/src/game/sound.c | 179 +++++++++++++++++++++ entropie/src/game/sound.h | 16 ++ entropie/src/main.c | 13 +- entropie/src/people/ai.c | 118 ++++++++++++-- entropie/src/people/ai.h | 10 +- entropie/src/people/ai_help.c | 69 ++++++++- entropie/src/people/body.c | 10 +- entropie/src/people/body.h | 5 +- entropie/src/people/hands.c | 12 +- entropie/src/render/figure.c | 3 +- entropie/src/render/figure.h | 2 + entropie/src/render/renderer.c | 9 ++ entropie/src/render/renderer.h | 5 +- entropie/src/render/shapes.h | 12 +- entropie/src/render/shapes_props.c | 58 +++++-- entropie/src/render/skybox.c | 2 + entropie/src/render/skybox.h | 5 +- entropie/src/world/car.c | 148 ++++++++++++++++-- entropie/src/world/car.h | 27 +++- entropie/src/world/forestry.c | 1 + entropie/src/world/object.c | 171 ++++++++++++++++++-- entropie/src/world/object.h | 21 ++- entropie/src/world/scene.c | 2 + entropie/src/world/weather.c | 18 ++- entropie/src/world/wetness.c | 6 + entropie/src/world/wetness.h | 2 + entropie/src/world/world.c | 83 ++++++++-- entropie/src/world/world.h | 24 +++ 44 files changed, 1551 insertions(+), 130 deletions(-) diff --git a/commit_message.txt b/commit_message.txt index e90f32f..ff4e5ee 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1,6 +1,14 @@ -entropie 2.27.0: a hand on the bank +entropie 2.28.0: sound, sorted crates, hoods and umbrellas -- Walking up a steep bank you bend forward and put a hand down on the - slope ahead (everybody with a hand free) -- An NPC told to get out of the way while climbing no longer gets stuck - half way up +- Sound: steps on every ground, wind, rain, thunder, fire, axe, chainsaw, + the car, bottles, glass, falls, a tree coming down, birds and crickets - + all made up on the fly (M: off / on) +- Arrival: everybody gets a seat by the fire (people move up on the bench; + no room: they stand with the others) +- Crates know each bottle: put a bottle down next to a crate and it goes in; + right click at a crate takes out one that does not belong; the tidy sort + the crates +- Rain and smoke show against the sky; the sky clouds over with the weather +- Rain soaks the ground wherever people walk: mud on trodden paths +- Hoods go up in the rain; two umbrellas to fetch and carry +- The very sporty roll off drops of 5 m and more diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md index 125ed5c..ba2ba67 100644 --- a/entropie/ARCHITECTURE.md +++ b/entropie/ARCHITECTURE.md @@ -7,12 +7,12 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL | Folder | What it is | May include | |---|---|---| -| `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input | core | +| `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input, sound out (`audio.c`: a synthesizer and mixer, no sound files) | core | | `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math | | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render | | `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game | | `src/people/` | bodies (animation), hands, AI, talk, condition, dog, climbing | everything but game | -| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, saves (`save.c`) | all | +| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, 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`. @@ -24,8 +24,10 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL physics for every object (near ones from the `Grid`) → grid rebuilt → felling and cutting → hands (pushing, carrying) → hose → AI → bodies animated → props (gates, chairs, ashtray, pullovers put down). Then the camera follows (`game.c`), and -`Game_render`: shadow pass, scene pass (terrain, objects, figures, grass, smoke), -ink pass, UI. +`Game_render`: shadow pass, scene pass (terrain, objects, figures, grass), the sky, +`Game_renderAir` (rain, smoke, chips: after the sky, so they show against it), +ink pass, UI. `Sound_update` (main.c) after the update: the world's noises of the +frame and the steady sounds. Budget: the update takes about 1 ms for ~950 objects. Drawing skips what's out of view (objects, 8×8 terrain tiles). @@ -78,6 +80,25 @@ of view (objects, 8×8 terrain tiles). pass draws the outlines over it all. - New GL calls: add them to the list in `core/gl.h`. +## Sound + +- `core/audio.c`: SDL3 audio stream, mixed on the audio thread. Short voices + (noise, sine, saw or crackle; filters; attack and decay; pan; delay) and steady + beds (wind, rain, fire, chainsaw, engine, crickets, hose) turned up and down. + No device: silent, nothing else changes. +- The world queues what makes a sound this frame: `World_noise` (steps with + what's underfoot, chops, clinks, glass, thuds, a tree coming down, twigs). + `game/sound.c` empties the list each frame and plays them by distance and side + from the camera; the beds follow the weather, the fires, a chainsaw, the car. +- M mutes; menus pause the beds. + +## Crates + +- Each slot is known (`Object.crate_slot`: kind and sips, `CRATE_SLOT`): + `stored`, `crate_empties` and the weight follow from it (`Crate_recount`). + Bottles of another kind put into a crate stay there as such until somebody + sorts them (`Crate_takeWrong`, the tidy: `ai_help.c` sortCrates). + ## Saves - `src/game/save.c`: `<name>.sav` (text: `key value` lines, `[game]`, then `[o id]` per object) and `<name>.ground` (binary: the wear map, the wet chunks). In the user's data folder (`Platform_saveDir`). @@ -88,7 +109,7 @@ of view (objects, 8×8 terrain tiles). - A key renamed or its meaning changed: raise `SAVE_FORMAT` (save.h) and add a step to `migrate()` that rewrites the old lines. - A save from a newer format is refused, never guessed at. - A thing the scene no longer makes is dropped; one it newly makes keeps its fresh state. -- Not kept (comes back fresh): what anybody was in the middle of (AI plans, paths, poses), sparks and smoke. Kept specially: the hose's rope (`rope` line: every point, melted, taped), who sits where (`sit` line: sat down again, staying a while), who's up a ladder (`ladder` line: you back on it as high, `Ladder_mountAt`; the others at its foot). +- Not kept (comes back fresh): what anybody was in the middle of (AI plans, paths, poses), sparks and smoke. Kept specially: the hose's rope (`rope` line: every point, melted, taped), who sits where (`sit` line: sat down again, staying a while), who's up a ladder (`ladder` line: you back on it as high, `Ladder_mountAt`; the others at its foot), what's in each crate slot (`slots` line; older saves: their own bottles, so many full and empty). ## Versions diff --git a/entropie/Makefile b/entropie/Makefile index 05394ed..d8c0ee3 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 2.27.0 +VERSION := 2.28.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index 278443a..3caa43e 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -25,8 +25,13 @@ people living there. heavy things together. Ask for help (G) or give someone a job (X) - **Campfire:** build it, light it, grill on it, put it out, sit round it - **Water:** hose, watering can, puddles and mud -- **Weather:** dry spells, rain, thunderstorms, and wind that blows tents, - paper and pullovers away +- **Weather:** dry spells, clouds coming over, rain, thunderstorms, and wind + that blows tents, paper and pullovers away. Hoods go up, umbrellas get + fetched; long rain turns the trodden paths to mud +- **Drinks:** crates of beer, water, soda and mate. Empties go back into + their crates; bottles in the wrong crate get sorted out +- **Sound:** steps on every ground, wind, rain, thunder, the fire, axe and + chainsaw, glass, birds by day and crickets at night (all made up on the fly) - **Drink:** too much and you sway, stumble, get sick or pass out - **Alice:** the dachshund. She fetches, begs and loves being petted - **Saves:** autosave, quick save and named saves. Old saves keep working @@ -44,7 +49,8 @@ people living there. | Left click (hold) | Keep going: next stroke, next sip | | X | Talk to who's in front; else shout for someone | | 1 - 5 | Answer when talking / pick from the shout menu | -| Right click | Put down / let go | +| Right click | Put down / let go (a bottle next to a crate: into it) | +| Right click at a crate, hands empty | Take out a bottle that doesn't belong in it | | E / Q | Take / drop with right / left hand (by a table or crate: set it on top) | | F (hold) | Push / pull (walk forward / back); look left / right to turn it | | Ctrl (hold) | Look around; holding on: without turning it (the view swings back after) | @@ -73,6 +79,7 @@ people living there. | F1 | Dev menu: pick anyone, change body, needs, temperament, likes and skills (Up / Down, Left / Right); teleport you to the parkour or the yard; time of day, game speed, weather, wind, skip the start | | Esc | Menu: resume, save, load, keys, quit (the game waits meanwhile) | | F5 / F9 | Quick save / quick load | +| M | Sound off / on | The game starts in third person. Heavy things need help: the strength of everyone holding adds up. @@ -116,5 +123,5 @@ Dependencies point downward: `main → game → world/people → render → core ## Version -`make version` prints e.g. `entropie 2.26.0 (build 183cb96)`. The build id is +`make version` prints e.g. `entropie 2.28.0 (build e2f9b0f)`. The build id is an md5 of all sources and assets. diff --git a/entropie/STATE.md b/entropie/STATE.md index 32d9a67..a144987 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -249,6 +249,9 @@ Painted in the rotoscope style of *Waking Life* (2001): - Out of the car everybody has a good stretch, then you walk over together - Kurti and his sons sit at the fire. First say hello: go over, they welcome you. Then sit down with them (C) for a little chat with Kurti +- Three fit on a bench by the fire: those on it move up for one more (the + nice ones on their own, the others when asked: "Move up a bit?"). No room + anywhere: they stand by the fire with the others - Then the work: the boot opens, the others unload. Crates go into the kitchen in your garages, the camping things up to the meadow behind them. Help: left click at the boot takes out the next thing (crates: both hands) @@ -258,8 +261,12 @@ Painted in the rotoscope style of *Waking Life* (2001): put it down for a moment; or two carry it (F to hold on, E to lift together). The others put theirs down for a breather too - Crates: left click takes a bottle out (the others help themselves too), - E / Q carries the whole crate. An empty bottle in your hand: left click at a - crate puts it back in (the tidy bring theirs back too) + E / Q carries the whole crate. A bottle in your hand (empty or not) put down + next to a crate (right click, E / Q, or a left click at it) goes into it: its + own crate if one is there, else any with room. Each crate shows what's in it, + and a bottle of another kind stays a wrong one: right click at the crate with + empty hands takes it out. The tidy (Jana) sort wrong bottles into their own + crates on their own (the crates go back to the shop sorted) - The hint line says what's to do next - Nobody asks you for other jobs (wood and so on) until the tents stand and everything is unloaded @@ -267,8 +274,17 @@ Painted in the rotoscope style of *Waking Life* (2001): ### Weather - Dry spells of hours, then showers now and then (the first few hours stay dry) -- Rain falls round you, the sky greys over, everything looks wetter; the - ground gets real water (puddles, mud), a fire burns down faster +- Clouds come over before the rain and the sky greys (no sun, moon or + stars behind them). Rain falls round you, against the sky too; everything + looks wetter; the ground gets real water (puddles, mud) where you are and + wherever anybody walks - long hard rain turns trodden paths and bare ground + to mud (slower, dirty shoes, slipping at a run); a fire burns down faster +- A hood to your jacket (the parka, the hoodie, the red rain jacket): out in + the rain the jacket goes on and the hood up +- Two umbrellas (by the main building's door, in your garages' kitchen): + carried, one opens in the rain and keeps you dry. The others fetch one when + it starts to rain (if they're not busy working) and stay out under it; the + careful put it back after - People get out of it: Kurti and his sons into the house, the others into their tent (at night, or when it's nearer) or your garages' kitchen; out again when it stops @@ -500,7 +516,8 @@ and satellite pictures (about 340 x 340 m). - Dropping from high up: the knees come up to the belly, the legs reach down just before the ground. Over about 1.3 m you go deep into the knees; over about 2.5 m the sporty roll over a shoulder the way they were going, - the others crash (energy, dirt) + the others crash (energy, dirt). The very sporty (fresh, sober) roll it off + from 5 m and more without falling - Everyone moves at their own size: taller people step higher and run a little faster; each has a stride of their own (long steps or quick ones) - Bottles, cigarettes and the like sit in the fist, the wrist kept straight @@ -906,6 +923,17 @@ say no more often and don't come to you for a bit. your skills. Only pressing needs show otherwise - The others live the same day: they eat, drink, pee and sleep at night +### Sound + +- Everything is made up on the fly (no sound files): steps by what's + underfoot (grass, gravel, tar, earth, mud, puddles, wood), wind (gustier the + stronger), rain (drumming and dull in your tent), thunder after the + lightning, the fire crackling, axe strokes, the chainsaw, the car coming in, + bottles clinking, glass breaking, somebody hitting the ground, a tree + coming down, twigs snapping, the hose, birds by day, crickets at night +- Further away is quieter and duller, and comes from its side +- M: sound off / on; the menus pause it + ### Saving - At the start: continue (the autosave), a new game, or load a save @@ -1130,6 +1158,15 @@ Round 2.27: - NPCs: a climb going on is played before anything else in their update (a warned-off climber stalled half way up: Climb_update wasn't reached). - Tried and dropped: NPCs taking a straight way up a bank instead of the way round - the walking map already goes straight up it where it matters. +Round 2.28: +- Arrival: the friends sit down on a bench by the fire (`ai.c` greetHosts: the nearest bench with room, going to its front on the fire's side); those on it move up (`object.c` Object_benchRoom / Object_benchMakeRoom: spread out along it, SEAT_SPACE apart, as little moved as can be; you, sleepers and the unwilling - willing under 0.35, or annoyed - stay put unless asked: "Move up a bit?", one of them answers), sliding along it (`sit_scoot`, Object_scootOnSeat, SCOOT_SPEED 0.35 m/s). Object_sit makes room on its own too (you, anybody). No room: standing in the ring round the fire (GREET_STAND 3.7 m, a free spot nearest first), facing it. freeAlong allows 6 cm less than SEAT_SPACE. Checked: all three seated, Kurti moved up. +- Crates know each slot (`crate_slot`, CRATE_SLOT: kind and sips; `car.c` Crate_fill / recount / takeBottle / takeWrong / putBottle / forBottle / rightFor / nextWrong / belongs). A half-drunk bottle goes back half-drunk. You: a bottle put down next to a crate (right click, E / Q, CRATE_REACH 1.4 m) goes in (its own first); right click with empty hands at a crate takes a wrong one out (Hands_receive). The tidy sort (`ai_help.c` sortCrates in ai_tidyUp: SORT_LOOK 25 m, SORT_FAR 60 m, SORT_GIVE 45 s). The crate mesh shows each slot (wrong ones in their own colours, empties without a cap). Saves: a `slots` line (older saves: own bottles, full then empty). Checked: a water bottle in a beer crate, Jana took it to the water crate. +- Sound (`core/audio.c`, `game/sound.c`, `World_noise`): see ARCHITECTURE "Sound". Checked through SDL's disk driver: levels (no clipping, no NaN), the car's engine at the start; nothing listened to. +- Rain and smoke after the sky (`World_drawAir`, `Renderer_beginAirPass`): they show against the sky now (it was drawn over them). The sky clouds over (`Lighting.overcast`, `skybox.frag`: more and darker clouds, a grey veil, no sun, moon or stars; `game.overcast` eases over 10 game minutes, ahead of the rain). +- Rain soaks the ground round everybody out in it too (`weather.c`: PEOPLE_RADIUS 6 m, 2 splashes a frame; RAIN_MM_HOUR 9): mud where people walk, not only round the camera. +- Hoods (`look.hoodie`: jackets 1, 3, 4 of the five; `hood_up`; `figure.frag` hoodDistance, in the jacket's colour): out in the rain the jacket goes on, the hood up. Umbrellas (OBJ_UMBRELLA, two: by the house door, in the garage kitchen; one-handed): open held in the rain (`world.c` underUmbrella: held up over the head, `hands.c`), dry under it (rain_out off); NPCs fetch one when the rain starts (`ai.c` shelter: idle, a hand free, UMBRELLA_FETCH 25 m) and stay out; ai_putAway takes it home once it's dry. +- Landing: the very sporty (`sporty`: fitness 0.6..0.95, sober, fresh) roll off up to LAND_BREAK + LAND_SPORT (10.8 m/s, about 6 m), the risk mostly gone. Checked: fitness 0.95 from 3, 4.5, 5.5 m: no crash in 24; 0.6: crashes from 3 m on. + ## Open tasks Most important first. @@ -1152,7 +1189,9 @@ Most important first. - "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: a sound for thunder, wind and rain (no sound in the game yet); hats (nobody wears one yet) blown off +- Weather, still to do: hats (nobody wears one yet) blown off; umbrellas while working (now only idle people fetch one); an umbrella blown inside out +- Sound, still to do: voices (talking is text only), Alice barking, the toilet, eating and drinking +- Smoke: drawn as pale boxes (no softness) - a softer look would be nicer **Saves, still to do** - Things being done come back fresh: a person's part of a job half way (its progress on the thing is kept, and their order), carrying something together @@ -1164,4 +1203,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.27.0 (build a2db897)` +State of `entropie 2.28.0 (build e2f9b0f)` diff --git a/entropie/assets/shaders/figure.frag b/entropie/assets/shaders/figure.frag index 8216055..7859d55 100644 --- a/entropie/assets/shaders/figure.frag +++ b/entropie/assets/shaders/figure.frag @@ -42,6 +42,7 @@ uniform int streamKind; // 0 none, 1 pee, 2 sick, 3 water uniform float streamWidth; uniform int garment; // 0 trousers, 1 shorts, 2 skirt, 3 dress uniform float soaked; // 0..1 wet from the rain: clothes and hair darker +uniform bool hood; // the jacket's hood up over the head (rain) uniform float undress; // to pee: squatting trousers down / skirt up (0..1); standing fly open / skirt lifted in front (-1..0) uniform vec3 cigA, cigTip; // a cigarette (figure space): the end at the hand / lips, the burning end uniform float cigGlow; @@ -280,6 +281,18 @@ float hairDistance(vec3 world_p) { return d; } +// The hood up: a shell round the head, open for the face, down the back of the neck. +float hoodDistance(vec3 world_p) { + vec3 p = toHead(world_p); + float shell = length(p - vec3(0.0, 0.020, -0.018)) - 0.126; + float open = dot(p - vec3(0.0, 0.0, 0.050), normalize(vec3(0.0, -0.30, 1.0))); // (the face out of it) + float d = max(shell, open); + vec3 a = vec3(0.0, -0.030, -0.050), b = vec3(0.0, -0.160, -0.070), ba = b - a; + float t = clamp(dot(p - a, ba) / dot(ba, ba), 0.0, 1.0); + float back = max(length(p - a - ba * t) - mix(0.090, 0.075, t), p.z - 0.005); + return smin(d, back, 0.03); +} + float body(vec3 p, out int mat) { float d = 1e9, nearest = 1e9; mat = M_SKIN; @@ -290,8 +303,8 @@ float body(vec3 p, out int mat) { if (di < nearest) { nearest = di; mat = partMat[i]; } } if (hairCandidate) { - float dh = hairDistance(p); - if (dh < nearest + 0.004) mat = M_HAIR; // hair covers the scalp + float dh = hood ? hoodDistance(p) : hairDistance(p); + if (dh < nearest + 0.004) mat = hood ? M_JACKET : M_HAIR; // hair (or the hood) covers the scalp d = smin(d, dh, 0.008); } return d; @@ -441,7 +454,7 @@ void main() { if (length(cross(c - origin, ray)) < r) candidates |= 1 << i; } vec3 head = onHead(vec3(0.0, hairStyle == 1 ? -0.08 : 0.0, -0.02)); - hairCandidate = species == 0 && length(cross(head - origin, ray)) < (hairStyle == 1 ? 0.22 : 0.15); + hairCandidate = species == 0 && length(cross(head - origin, ray)) < (hairStyle == 1 || hood ? 0.22 : 0.15); if (candidates == 0 && !hairCandidate) discard; // March the ray through the body diff --git a/entropie/assets/shaders/skybox.frag b/entropie/assets/shaders/skybox.frag index 9584bf5..854d2d5 100644 --- a/entropie/assets/shaders/skybox.frag +++ b/entropie/assets/shaders/skybox.frag @@ -12,6 +12,7 @@ uniform vec3 skyTop; uniform vec3 skyHorizon; uniform float dayFactor; uniform float skyTime; +uniform float overcast; // 0 clear .. 1 clouded over (rain) const float SKY_DEPTH = 1000.0; // the ink pass treats this depth as sky @@ -27,7 +28,7 @@ void main() { // Warm glow around a low sun float lowSun = 1.0 - smoothstep(0.0, 0.6, sun.y); - color += lightColor * 0.25 * pow(max(sunDot, 0.0), 6.0) * lowSun * step(-0.2, sun.y); + color += lightColor * 0.25 * pow(max(sunDot, 0.0), 6.0) * lowSun * step(-0.2, sun.y) * (1.0 - overcast); // Soft clouds: the noise field is warped by itself and both drift, // so shapes flow and fold like slow watercolor instead of sliding. @@ -36,22 +37,27 @@ void main() { float t = skyTime * 0.01; vec2 warp = vec2(fbm3(vec3(plane, t)), fbm3(vec3(plane + 5.2, t + 1.3))) - 0.5; float c = fbm3(vec3(plane + warp * 1.4 + vec2(t * 0.8, t * 0.3), t * 0.5)); - float cloud = smoothstep(0.45, 0.8, c) * smoothstep(0.02, 0.3, dir.y); + // clouding over: more of them, thicker, a darker grey underneath + float cloud = smoothstep(0.45 - 0.4 * overcast, 0.8 - 0.35 * overcast, c) * smoothstep(0.02 - 0.15 * overcast, 0.3, dir.y); vec3 tint = mix(skyHorizon, vec3(1.0), 0.35) * mix(vec3(1.0), lightColor, 0.4); - color = mix(color, tint, cloud * 0.55); + vec3 grey = skyHorizon * mix(0.95, 0.6, smoothstep(0.4, 0.9, c)); // (heavy rain clouds: dark bellies) + tint = mix(tint, grey, overcast); + color = mix(color, tint, cloud * mix(0.55, 0.95, overcast)); + color = mix(color, skyHorizon * 0.85, 0.45 * overcast); // (the whole sky a veil) } // Sun: steady disc with a soft halo float sunDisc = smoothstep(0.9975, 0.9985, sunDot) + pow(max(sunDot, 0.0), 64.0) * 0.35; - color += (lightColor * 0.5 + 0.5) * sunDisc * smoothstep(-0.1, 0.05, sun.y); + float clear = 1.0 - smoothstep(0.2, 0.7, overcast); // (behind the clouds: no sun, moon or stars) + color += (lightColor * 0.5 + 0.5) * sunDisc * smoothstep(-0.1, 0.05, sun.y) * clear; // Moon opposite the sun, at night float moonDisc = smoothstep(0.9988, 0.9993, dot(dir, -sun)); - color = mix(color, vec3(0.92, 0.90, 0.80), moonDisc * (1.0 - dayFactor)); + color = mix(color, vec3(0.92, 0.90, 0.80), moonDisc * (1.0 - dayFactor) * clear); // Sparse, steady stars float star = step(0.997, hash13(floor(dir * 120.0))) * smoothstep(0.0, 0.2, dir.y); - color += star * 0.5 * (1.0 - dayFactor); + color += star * 0.5 * (1.0 - dayFactor) * clear; OutColor = vec4(color, 1.0); OutNormalDepth = vec4(0.0, 0.0, 0.0, SKY_DEPTH); diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index e90f32f..ff4e5ee 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,6 +1,14 @@ -entropie 2.27.0: a hand on the bank +entropie 2.28.0: sound, sorted crates, hoods and umbrellas -- Walking up a steep bank you bend forward and put a hand down on the - slope ahead (everybody with a hand free) -- An NPC told to get out of the way while climbing no longer gets stuck - half way up +- Sound: steps on every ground, wind, rain, thunder, fire, axe, chainsaw, + the car, bottles, glass, falls, a tree coming down, birds and crickets - + all made up on the fly (M: off / on) +- Arrival: everybody gets a seat by the fire (people move up on the bench; + no room: they stand with the others) +- Crates know each bottle: put a bottle down next to a crate and it goes in; + right click at a crate takes out one that does not belong; the tidy sort + the crates +- Rain and smoke show against the sky; the sky clouds over with the weather +- Rain soaks the ground wherever people walk: mud on trodden paths +- Hoods go up in the rain; two umbrellas to fetch and carry +- The very sporty roll off drops of 5 m and more diff --git a/entropie/src/core/audio.c b/entropie/src/core/audio.c new file mode 100644 index 0000000..6fa52ef --- /dev/null +++ b/entropie/src/core/audio.c @@ -0,0 +1,301 @@ +#include "core/audio.h" + +#include <SDL3/SDL.h> +#include <math.h> +#include <string.h> + +#define RATE 44100 +#define VOICES_MAX 96 +#define CHUNK 512 /* frames mixed at a time */ +#define MASTER 0.55f +#define BED_EASE 3.0f /* 1/s: beds turn up and down this quickly */ +#define TWO_PI 6.28318531f + +typedef struct { /* a one-pole lowpass (cascaded twice: steeper) */ + float a, y1, y2; +} Low; + +typedef struct { + Voice v; + float t; /* s since it started (negative: still waiting) */ + float phase; + float click; /* crackle: the click sounding now */ + Low low, high; + unsigned rng; + bool on; +} Playing; + +typedef struct { + float gain, pan, param; /* where it's going ... */ + float g, p, k; /* ... where it is */ + float phase, phase2, click; + Low low, low2, high; + float mod, mod_v; /* a slow wander (wind gusts, a fire breathing) */ + unsigned rng; +} BedState; + +static SDL_AudioStream *g_stream; +static SDL_Mutex *g_lock; +static Playing g_voices[VOICES_MAX]; +static BedState g_beds[BED_COUNT]; +static bool g_muted, g_paused; +static float g_fade = 1.0f; /* (muting fades, no click) */ + +static float coef(float hz) { return hz <= 0.0f ? 1.0f : 1.0f - expf(-TWO_PI * fminf(hz, RATE * 0.45f) / RATE); } +static float lowpass(Low *f, float x) { f->y1 += f->a * (x - f->y1); f->y2 += f->a * (f->y1 - f->y2); return f->y2; } +static float onePole(Low *f, float x) { f->y1 += f->a * (x - f->y1); return f->y1; } +static float noise(unsigned *r) { + *r ^= *r << 13; *r ^= *r >> 17; *r ^= *r << 5; + return (float)(*r & 0xFFFFFFu) / 8388608.0f - 1.0f; +} +static float chance(unsigned *r) { return 0.5f * (noise(r) + 1.0f); } +static void panGains(float pan, float *l, float *r) { + float a = (fmaxf(-1.0f, fminf(1.0f, pan)) + 1.0f) * 0.25f * 3.14159265f; + *l = cosf(a); + *r = sinf(a); +} + +/* ---------- Voices ---------- */ + +static float voiceSample(Playing *p) { + const Voice *v = &p->v; + float t = p->t, x = 0.0f; + float f = v->freq; + if (v->freq_end > 0.0f && v->freq > 0.0f) { + float span = v->length > 0.0f ? v->length : v->attack + 3.0f * v->decay; + f = v->freq * powf(v->freq_end / v->freq, fminf(t / fmaxf(span, 1e-3f), 1.0f)); + } + switch (v->wave) { + case WAVE_NOISE: x = noise(&p->rng); break; + case WAVE_SINE: p->phase += f / RATE; x = sinf(TWO_PI * p->phase); break; + case WAVE_SAW: p->phase += f / RATE; x = 2.0f * (p->phase - floorf(p->phase)) - 1.0f; break; + case WAVE_CRACKLE: + if (chance(&p->rng) < f / RATE) p->click = noise(&p->rng) > 0.0f ? 1.0f : -1.0f; + x = p->click + 0.15f * noise(&p->rng); + p->click *= 0.82f; + break; + } + if (p->phase > 1.0f) p->phase -= floorf(p->phase); + if (v->low > 0.0f) x = lowpass(&p->low, x); + if (v->high > 0.0f) x -= onePole(&p->high, x); + float env = t < v->attack ? t / fmaxf(v->attack, 1e-4f) : expf(-(t - v->attack) / fmaxf(v->decay, 1e-3f)); + return x * env * v->gain; +} + +static void mixVoices(float *out, int frames) { + for (int i = 0; i < VOICES_MAX; i++) { + Playing *p = &g_voices[i]; + if (!p->on) continue; + float l, r; + panGains(p->v.pan, &l, &r); + float end = p->v.length > 0.0f ? p->v.length : p->v.attack + 6.0f * p->v.decay; + for (int k = 0; k < frames; k++) { + p->t += 1.0f / RATE; + if (p->t < 0.0f) continue; + if (p->t > end) { p->on = false; break; } + float s = voiceSample(p); + out[2 * k] += s * l; + out[2 * k + 1] += s * r; + } + } +} + +/* ---------- Beds ---------- */ + +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) */ + b->mod = fmaxf(-1.0f, fminf(1.0f, b->mod + b->mod_v * 4.0f / RATE)); + switch (which) { + case BED_WIND: { /* noise through a lowpass that opens with the gusts */ + float cut = 180.0f + 900.0f * k + 400.0f * k * (0.5f + 0.5f * b->mod); + b->low.a = coef(cut); + x = lowpass(&b->low, noise(&b->rng)) * (0.8f + 0.5f * (0.5f + 0.5f * b->mod) * k) * 2.2f; + break; + } + case BED_RAIN: { /* hiss and drops; through a tent: dull, drumming */ + b->low.a = coef(7000.0f - 5500.0f * k); + b->high.a = coef(600.0f - 400.0f * k); + float hiss = noise(&b->rng); + if (chance(&b->rng) < (400.0f + 900.0f * k) / RATE) b->click = noise(&b->rng); + float drop = b->click; + b->click *= 0.9f - 0.08f * k; + x = lowpass(&b->low, hiss * (0.5f - 0.3f * k) + drop * (0.6f + 0.6f * k)); + x -= onePole(&b->high, x); + x *= 1.4f; + break; + } + case BED_FIRE: { /* a low roar, crackles and now and then a pop */ + b->low.a = coef(250.0f + 250.0f * k); + float roar = lowpass(&b->low, noise(&b->rng)) * (0.6f + 0.3f * b->mod) * 2.0f; + if (chance(&b->rng) < (6.0f + 30.0f * k) / RATE) b->click = noise(&b->rng) * (chance(&b->rng) < 0.1f ? 2.0f : 0.8f); + b->high.a = coef(1500.0f); + float c = b->click; + b->click *= 0.7f; + c -= onePole(&b->high, c); + x = roar * (0.4f + 0.6f * k) + c; + break; + } + case BED_SAW: { /* a two-stroke buzz, lower and rougher cutting */ + float f = 140.0f - 35.0f * k + 6.0f * b->mod; + b->phase += f / RATE; + if (b->phase > 1.0f) b->phase -= 1.0f; + b->phase2 += 0.5f * f / RATE; + if (b->phase2 > 1.0f) b->phase2 -= 1.0f; + float saw = 2.0f * b->phase - 1.0f, chug = 0.75f + 0.25f * sinf(TWO_PI * b->phase2); + b->low.a = coef(1800.0f + 1500.0f * k); + x = lowpass(&b->low, saw * chug + 0.35f * k * noise(&b->rng)) * 1.3f; + break; + } + case BED_ENGINE: { /* a low hum that rises with the speed */ + float f = 32.0f + 45.0f * k; + b->phase += f / RATE; + if (b->phase > 1.0f) b->phase -= 1.0f; + b->low.a = coef(220.0f + 250.0f * k); + x = lowpass(&b->low, (2.0f * b->phase - 1.0f) + 0.3f * noise(&b->rng)) * 1.8f; + break; + } + case BED_CRICKETS: { /* a few crickets: bursts of a high tone, three pulses then a rest, each its own time */ + b->phase += 4500.0f / RATE; + if (b->phase > 1.0f) b->phase -= 1.0f; + b->phase2 += 1.0f / RATE; + float s = 0.0f; + for (int c = 0; c < 3; c++) { + float cyc = 0.55f + 0.17f * (float)c, tt = fmodf(b->phase2 + 0.31f * (float)c, cyc); + float pulse = tt < 0.12f ? fmaxf(0.0f, sinf(tt / 0.12f * 3.0f * TWO_PI * 0.5f)) : 0.0f; + s += pulse * (c == 1 ? 0.7f : 1.0f); + } + x = sinf(TWO_PI * b->phase) * s * 0.5f; + if (b->phase2 > 1000.0f) b->phase2 -= 1000.0f; + break; + } + case BED_HOSE: { /* a steady spray */ + b->low.a = coef(6000.0f); + b->high.a = coef(1500.0f); + x = lowpass(&b->low, noise(&b->rng)); + x -= onePole(&b->high, x); + x *= 1.6f; + break; + } + default: break; + } + return x; +} + +static void mixBeds(float *out, int frames) { + for (int i = 0; i < BED_COUNT; i++) { + BedState *b = &g_beds[i]; + float goal = g_paused ? 0.0f : b->gain; + if (b->g < 1e-4f && goal < 1e-4f) continue; + float ease = 1.0f - expf(-BED_EASE / RATE); + for (int k = 0; k < frames; k++) { + b->g += (goal - b->g) * ease; + b->p += (b->pan - b->p) * ease; + b->k += (b->param - b->k) * ease; + float l, r, s = bedSample((Bed)i, b) * b->g; + panGains(b->p, &l, &r); + out[2 * k] += s * l; + out[2 * k + 1] += s * r; + } + } +} + +/* ---------- The device ---------- */ + +static void SDLCALL fill(void *user, SDL_AudioStream *stream, int additional, int total) { + (void)user; + (void)total; + static float buf[CHUNK * 2]; + int frames = additional / (int)(2 * sizeof(float)); + while (frames > 0) { + int n = frames < CHUNK ? frames : CHUNK; + memset(buf, 0, sizeof(float) * 2 * (size_t)n); + SDL_LockMutex(g_lock); + mixVoices(buf, n); + mixBeds(buf, n); + bool muted = g_muted; + SDL_UnlockMutex(g_lock); + for (int k = 0; k < 2 * n; k++) { + if (k % 2 == 0) g_fade += ((muted ? 0.0f : 1.0f) - g_fade) * 0.002f; + float s = buf[k] * MASTER * g_fade; + buf[k] = s / (1.0f + fabsf(s)); /* (soft: never clipping) */ + } + SDL_PutAudioStreamData(stream, buf, (int)(sizeof(float) * 2 * (size_t)n)); + frames -= n; + } +} + +bool Audio_init(void) { + if (g_stream) return true; + if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { + SDL_Log("No sound: %s", SDL_GetError()); + return false; + } + g_lock = SDL_CreateMutex(); + for (int i = 0; i < BED_COUNT; i++) g_beds[i].rng = 0x9E3779B9u * (unsigned)(i + 1); + SDL_AudioSpec spec = { SDL_AUDIO_F32, 2, RATE }; + g_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, fill, NULL); + if (!g_stream) { + SDL_Log("No sound device: %s", SDL_GetError()); + SDL_DestroyMutex(g_lock); + g_lock = NULL; + SDL_QuitSubSystem(SDL_INIT_AUDIO); + return false; + } + SDL_ResumeAudioStreamDevice(g_stream); + return true; +} + +void Audio_shutdown(void) { + if (!g_stream) return; + SDL_DestroyAudioStream(g_stream); + g_stream = NULL; + SDL_DestroyMutex(g_lock); + g_lock = NULL; + SDL_QuitSubSystem(SDL_INIT_AUDIO); +} + +void Audio_play(const Voice *voice) { + if (!g_stream || voice->gain < 0.002f) return; + static unsigned seed = 0x1234567u; + SDL_LockMutex(g_lock); + int pick = -1; + float oldest = -1.0f; + for (int i = 0; i < VOICES_MAX; i++) { /* (a free one, else the one playing longest) */ + if (!g_voices[i].on) { pick = i; break; } + if (g_voices[i].t > oldest) { oldest = g_voices[i].t; pick = i; } + } + Playing *p = &g_voices[pick]; + memset(p, 0, sizeof(*p)); + p->v = *voice; + p->t = -fmaxf(voice->delay, 0.0f); + p->low.a = coef(voice->low); + p->high.a = coef(voice->high); + seed = seed * 1664525u + 1013904223u; + p->rng = seed | 1u; + p->on = true; + SDL_UnlockMutex(g_lock); +} + +void Audio_bed(Bed bed, float gain, float pan, float param) { + if (!g_stream || bed < 0 || bed >= BED_COUNT) return; + SDL_LockMutex(g_lock); + g_beds[bed].gain = fmaxf(0.0f, fminf(1.0f, gain)); + g_beds[bed].pan = pan; + g_beds[bed].param = fmaxf(0.0f, fminf(1.0f, param)); + SDL_UnlockMutex(g_lock); +} + +void Audio_mute(bool mute) { + if (g_lock) SDL_LockMutex(g_lock); + g_muted = mute; + if (g_lock) SDL_UnlockMutex(g_lock); +} + +bool Audio_muted(void) { return g_muted; } + +void Audio_pause(bool paused) { + if (g_lock) SDL_LockMutex(g_lock); + g_paused = paused; + if (g_lock) SDL_UnlockMutex(g_lock); +} diff --git a/entropie/src/core/audio.h b/entropie/src/core/audio.h new file mode 100644 index 0000000..ab3df7a --- /dev/null +++ b/entropie/src/core/audio.h @@ -0,0 +1,55 @@ +#ifndef CORE_AUDIO_H +#define CORE_AUDIO_H + +/* + * Sound out (SDL3). Everything is made up on the fly - noise, tones, filters, + * envelopes - no sound files. Short voices (a step, a chop, a clink) play + * once; steady beds (wind, rain, a fire, ...) run all the time and are only + * turned up and down. The game sets what plays; the audio thread mixes it. + * Without a sound device everything here quietly does nothing. + */ + +#include <stdbool.h> + +typedef enum { + WAVE_NOISE, /* white noise (shaped by the filters) */ + WAVE_SINE, /* a tone, gliding from freq to freq_end */ + WAVE_SAW, /* a buzzing tone */ + WAVE_CRACKLE /* sparse clicks (gravel, glass, twigs): freq = clicks a second */ +} Wave; + +typedef struct { + Wave wave; + float freq, freq_end; /* Hz (freq_end 0: stays at freq) */ + float low, high; /* filters: lowpass and highpass cutoff (Hz, 0: none) */ + float attack, decay; /* s: up in attack, then dying away (decay: down to a third) */ + float length; /* s: cut off after this (0: when it's died away) */ + float gain; /* 0..1 */ + float pan; /* -1 left .. +1 right */ + float delay; /* s before it starts */ +} Voice; + +typedef enum { + BED_WIND, /* param: 0..1 how strong (brighter, gustier) */ + BED_RAIN, /* param: 0 out in it .. 1 heard through a tent (dull, drumming) */ + BED_FIRE, /* param: 0..1 how big */ + BED_SAW, /* param: 0 idling .. 1 cutting (a chainsaw) */ + BED_ENGINE, /* param: 0..1 how fast (the car) */ + BED_CRICKETS, /* param: unused */ + BED_HOSE, /* param: unused */ + BED_COUNT +} Bed; + +/* Opens the sound device (false: none - the game goes on silent). */ +bool Audio_init(void); +void Audio_shutdown(void); +/* One short sound. */ +void Audio_play(const Voice *voice); +/* A bed this loud (0: off), from this side, its param (eased over a moment). */ +void Audio_bed(Bed bed, float gain, float pan, float param); +/* All quiet (M), or not; and everything paused (menus: the beds go quiet). */ +void Audio_mute(bool mute); +bool Audio_muted(void); +void Audio_pause(bool paused); + +#endif /* CORE_AUDIO_H */ diff --git a/entropie/src/core/input.c b/entropie/src/core/input.c index 36fff03..8fa40c1 100644 --- a/entropie/src/core/input.c +++ b/entropie/src/core/input.c @@ -52,6 +52,7 @@ void Input_poll(Input *input) { case SDL_SCANCODE_F1: input->dev_menu = true; break; case SDL_SCANCODE_F5: input->quick_save = true; break; case SDL_SCANCODE_F9: input->quick_load = true; break; + case SDL_SCANCODE_M: input->mute = true; break; default: break; } } diff --git a/entropie/src/core/input.h b/entropie/src/core/input.h index 4c56220..f77a2d8 100644 --- a/entropie/src/core/input.h +++ b/entropie/src/core/input.h @@ -47,6 +47,7 @@ typedef struct { bool erase; /* Backspace pressed (typing: one letter back) */ bool quick_save; /* F5 pressed */ bool quick_load; /* F9 pressed */ + bool mute; /* M pressed: sound off / on */ } Input; /* Pumps SDL events and fills in the current frame's input. */ diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c index a6dc4b9..69f68cd 100644 --- a/entropie/src/game/game.c +++ b/entropie/src/game/game.c @@ -245,6 +245,16 @@ bool Game_initWorld(Game *game, bool arrive) { return false; } Camera_init(&game->camera, game->camera.position, look * 180.0f / PI_F, 0.0f, CAMERA_FOV); + { /* two umbrellas: by the main building's door and in your garages' kitchen (last: the scene's own come first) */ + Vec3 at[2] = { vec3_add(Site_houseDoor(), vec3(0.9f, 0.0f, 0.4f)), vec3_add(Site_drinksSpot(), vec3(-0.8f, 0.0f, 0.6f)) }; + for (int k = 0; k < 2; k++) { + at[k].y = World_groundHeight(&game->world, at[k].x, at[k].z, at[k].y + 3.0f) + 0.06f; + Object *u = Object_create(OBJ_UMBRELLA, PALETTE_WARM, at[k], vec3_zero(), (float)k); + if (!u) continue; + Object_setRotation(u, vec3(PI_F / 2.0f, 0.4f + 1.3f * (float)k, 0.0f)); /* (lying there) */ + if (!World_add(&game->world, u)) continue; + } + } game->marker = Shapes_marker(1.8f, 0.8f); for (int i = 0; i < game->world.count; i++) { /* (what the scene made: a save finds it again by this) */ Object *o = game->world.objects[i]; @@ -297,6 +307,10 @@ static void rainLight(Game *game) { l->sky_top = vec3_lerp(l->sky_top, vec3(0.9f, 0.92f, 1.0f), 0.7f * f); l->sky_horizon = vec3_lerp(l->sky_horizon, vec3(0.95f, 0.95f, 1.0f), 0.7f * f); l->wet = game->world.wet; + /* clouding over: before the rain comes (and staying a while after), with it fully */ + float coming = clampf(game->world.rain_goal, 0.0f, 1.0f); + game->overcast += (fminf(1.0f, fmaxf(1.6f * r, 0.7f * coming)) - game->overcast) * fminf(1.0f, game->world.game_dt / 600.0f); + l->overcast = fmaxf(game->overcast, fminf(1.0f, 1.6f * r)); } /* Worn out with something heavy in your arms (a crate): down it goes for a moment. */ @@ -470,6 +484,11 @@ int Game_bubbles(const Game *game, Bubble *out, int max, int screen_w, int scree } +void Game_renderAir(Game *game, Renderer *renderer) { + Renderer_beginAirPass(renderer); + World_drawAir(&game->world, renderer); +} + void Game_render(Game *game, Renderer *renderer) { /* A campfire lights its surroundings, the more the darker it is */ Lighting lit = game->daycycle.light; diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h index d8fcabb..5ce9c36 100644 --- a/entropie/src/game/game.h +++ b/entropie/src/game/game.h @@ -73,6 +73,7 @@ typedef struct { Vec3 brace_grip; /* ... here */ Object *putting_up; /* the ladder you are putting up (hands on it, standing still) */ Arrival arrival; /* the start: in by car */ + float overcast; /* how clouded over the sky is (eases: clouds come before the rain) */ struct Object *tent; /* the tent you're working on (left button held) */ float tent_hold; /* ... held this long at a standing one (a moment: you start taking it down) */ struct Object *tent_drag; /* a tent with no pegs in you're moving (F held) ... */ @@ -125,6 +126,8 @@ const char *Game_status(const Game *game); /* Shadow pass and scene pass for all world objects. */ void Game_render(Game *game, Renderer *renderer); +/* Rain, smoke, chips: after the sky (they show against it too). */ +void Game_renderAir(Game *game, Renderer *renderer); #define POCKET_LINES 12 /* Your pockets as lines of text: a heading, what's in them, what Enter does; *picked: the line picked. */ diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c index d7d14cd..cf9c432 100644 --- a/entropie/src/game/game_actions.c +++ b/entropie/src/game/game_actions.c @@ -577,13 +577,14 @@ static bool takeFromSupply(Game *game) { if (o->type == OBJ_CRATE) { /* a bottle in hand that's empty (or no hand free): back in it goes */ for (int h = 0; h < 2; h++) { Object *it = p->hands.item[h]; - if (!it || p->hands.state[h] != HAND_HOLDING || !Object_isBottle(it) || it->broken || (it->sips > 0 && hand >= 0)) continue; + if (!it || p->hands.state[h] != HAND_HOLDING || !Crate_bottleKind(it) || it->broken || (it->sips > 0 && hand >= 0)) continue; if (o->stored >= Crate_bottles(o->variant)) { say(game, "The crate is full"); return true; } Hands_drop(p, h); Hands_touch(p, vec3_add(Object_colliderCenter(o, 0), vec3(0.0f, 0.2f, 0.0f))); - bool empty = it->sips <= 0; + bool empty = it->sips <= 0, own = Crate_belongs(o, Crate_bottleKind(it)); Crate_putBottle(o, it); - say(game, empty ? "The empty back into the crate" : "The bottle back into the crate"); + World_noise(&game->world, NOISE_CLINK, o->position, 0.6f); + say(game, !own ? "In it goes - but it doesn't belong in this crate" : empty ? "The empty back into the crate" : "The bottle back into the crate"); return true; } } @@ -592,7 +593,7 @@ static bool takeFromSupply(Game *game) { if (o->stored <= 0) { say(game, "The crate is empty"); return true; } if (o->stored <= o->crate_empties) { say(game, "Only empties left in this one"); return true; } Object *bottle = Crate_takeBottle(&game->world, o); - if (bottle && Hands_pickUpObject(p, hand, bottle)) tell(game, bottle); + if (bottle && Hands_pickUpObject(p, hand, bottle)) { tell(game, bottle); World_noise(&game->world, NOISE_CLINK, o->position, 0.5f); } return true; } ObjectType what = o->dispenses; @@ -1253,6 +1254,58 @@ static void keepClicking(Game *game) { } /* Right click: put down what is lifted, let go, or drop what is in the hands. */ +/* A crate right in front (or beside you), within reach: NULL if none. */ +#define CRATE_REACH 1.4f +static Object *crateAhead(Game *game) { + Object *p = game->player, *best = NULL; + float best_d = CRATE_REACH; + Vec3 f = Object_facing(p); + for (int i = 0; i < game->world.count; i++) { + Object *c = game->world.objects[i]; + if (c->type != OBJ_CRATE || c->is_held) continue; + Vec3 to = vec3(c->position.x - p->position.x, 0.0f, c->position.z - p->position.z); + float d = vec3_length(to); + if (d > 0.4f && vec3_dot(vec3_scale(to, 1.0f / d), f) < -0.2f) continue; /* (not behind you) */ + if (d < best_d) { best_d = d; best = c; } + } + return best; +} + +/* A bottle that comes in crates, put down next to one: into it (its own crate if one is there). */ +static bool bottleIntoCrate(Game *game, int hand) { + Object *p = game->player, *it = p->hands.item[hand]; + if (!it || p->hands.state[hand] != HAND_HOLDING || !Crate_bottleKind(it) || it->broken) return false; + Object *crate = Crate_forBottle(game->world.objects, game->world.count, it, p->position, CRATE_REACH); + if (!crate) { + if (crateAhead(game)) say(game, "The crate is full"); + return crateAhead(game) != NULL; + } + Hands_drop(p, hand); + Hands_touch(p, vec3_add(Object_colliderCenter(crate, 0), vec3(0.0f, 0.2f, 0.0f))); + bool empty = it->sips <= 0, own = Crate_belongs(crate, Crate_bottleKind(it)); + Crate_putBottle(crate, it); + World_noise(&game->world, NOISE_CLINK, crate->position, 0.6f); + char line[96]; + snprintf(line, sizeof line, "%s into the %s crate%s", empty ? "The empty" : "The bottle", Crate_name(crate->variant), + own ? "" : " - it doesn't belong there (sort it later: right click at the crate)"); + say(game, line); + return true; +} + +/* Hands empty at a crate: out comes a bottle that doesn't belong in it. */ +static bool wrongOutOfCrate(Game *game) { + Object *p = game->player, *crate = crateAhead(game); + int hand = Hands_freeHand(p); + if (!crate || hand < 0) return false; + if (!Crate_wrong(crate)) { say(game, "Everything in this crate belongs there"); return true; } + Object *b = Crate_takeWrong(&game->world, crate); + if (b && Hands_receive(p, hand, b)) { /* (straight out of the crate into the hand) */ + World_noise(&game->world, NOISE_CLINK, crate->position, 0.5f); + tell(game, b); + } + return true; +} + static void putDown(Game *game) { Object *p = game->player; if (Hands_isGrabbing(p)) { @@ -1263,7 +1316,7 @@ static void putDown(Game *game) { } int hand = p->hands.state[HAND_RIGHT] != HAND_FREE ? HAND_RIGHT : p->hands.state[HAND_LEFT] != HAND_FREE ? HAND_LEFT : -1; - if (hand >= 0) Hands_drop(p, hand); + if (hand >= 0 && !bottleIntoCrate(game, hand)) Hands_drop(p, hand); } /* Z: lie down to sleep when tired enough, or get up again. */ @@ -1368,8 +1421,8 @@ void game_handleActions(Game *game, const Input *input) { } else if ((input->use_left || input->use_right) && takeLadder(game)) { /* (taken down) */ } else { - if (input->use_left) Hands_pickUpOrDrop(p, HAND_LEFT, objects, count); - if (input->use_right) Hands_pickUpOrDrop(p, HAND_RIGHT, objects, count); + if (input->use_left && !bottleIntoCrate(game, HAND_LEFT)) Hands_pickUpOrDrop(p, HAND_LEFT, objects, count); + if (input->use_right && !bottleIntoCrate(game, HAND_RIGHT)) Hands_pickUpOrDrop(p, HAND_RIGHT, objects, count); if (input->use_left || input->use_right) tell(game, reachingFor(p)); } @@ -1380,7 +1433,7 @@ void game_handleActions(Game *game, const Input *input) { else game->click_action = CLICK_NONE; if (input->right_click && !Talk_active(&game->talk) && !game->shouting) { /* (empty-handed at a leaning ladder: take it down) */ bool empty = !p->hands.item[HAND_LEFT] && !p->hands.item[HAND_RIGHT]; - if (!empty || !takeLadder(game)) putDown(game); + if (!empty || (!takeLadder(game) && !wrongOutOfCrate(game))) putDown(game); } /* F (held) at a tent with no pegs in: move it */ diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c index 475a993..4848232 100644 --- a/entropie/src/game/save.c +++ b/entropie/src/game/save.c @@ -64,7 +64,7 @@ static const Field PERSON[] = { VE("skin", look.skin), VE("hair", look.hair), VE("shirt", look.shirt), VE("pants", look.pants), VE("shoes", look.shoes), FL("height", look.height), IN("hair_style", look.hair_style), BO("long_sleeves", look.long_sleeves), IN("garment", look.garment), IN("top", look.top), VE("jacket", look.jacket), BO("jacket_tied", look.jacket_tied), FL("belly", look.belly), - BO("keeps_shirt", look.keeps_shirt), FL("wet", look.wet), + BO("keeps_shirt", look.keeps_shirt), FL("wet", look.wet), BO("hoodie", look.hoodie), FL("ladder_skill", ladder_skill), FL("fitness", fitness), FL("wits", wits), FL("worn", worn), FL("height_fear", height_fear), FL("top_speed", top_speed), FL("jump_speed", jump_speed), FL("pace", pace), FL("tempo", tempo), IN("main_hand", main_hand), BO("female", female), BO("urinella", urinella), IN("urinella_tried", urinella_tried), IN("urinellas", urinellas), @@ -212,6 +212,11 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err, } else { writeFields(f, o, THING, COUNT(THING), &ids); if (o->cloth_where) fprintf(f, "where %s\n", o->cloth_where); + if (o->type == OBJ_CRATE) { /* what's in each slot (kind and sips) */ + fprintf(f, "slots"); + for (int q = 0; q < CRATE_SLOTS; q++) fprintf(f, " %d", o->crate_slot[q]); + fprintf(f, "\n"); + } if (o->type == OBJ_HOSE) { /* its rope: where every bit lies, melted, taped */ const HoseRope *rope = Hose_of((World *)w, o); if (rope && rope->ready) { @@ -525,6 +530,10 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t rope->ready = true; } } + else if (!strcmp(key, "slots") && o->type == OBJ_CRATE) { + for (int q = 0; q < CRATE_SLOTS; q++) o->crate_slot[q] = (unsigned char)numI(&s); + Crate_recount(o); + } else if (!strcmp(key, "load") && o->type == OBJ_CAR && o->load) { int n = numI(&s); o->load->n = 0; @@ -555,7 +564,21 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t case OBJ_BAG: Object_fillBag(o, 0.0f); break; case OBJ_TENT: Tent_refresh(o); break; case OBJ_GEAR: Gear_refresh(o); break; - case OBJ_CRATE: o->shown = -1; break; + case OBJ_CRATE: { + bool slots = false; + for (int i = r->first; i < r->first + r->lines; i++) if (!strncmp(p.line[i], "slots ", 6)) slots = true; + if (!slots) { /* (an older save: so many full, so many empties - all its own) */ + int full = o->stored - o->crate_empties, empties = o->crate_empties; + Crate_fill(o); + for (int q = 0; q < CRATE_SLOTS; q++) { + if (q >= full + empties) o->crate_slot[q] = 0; + else if (q >= full) o->crate_slot[q] = CRATE_SLOT(CRATE_KIND(o->crate_slot[q]), 0); + } + Crate_recount(o); + } + o->shown = -1; + break; + } case OBJ_LADDER: Ladder_setExtend(o, o->extend); break; case OBJ_CAR: if (o->boot_open) { o->boot_open = false; Car_openBoot(o, true); } diff --git a/entropie/src/game/sound.c b/entropie/src/game/sound.c new file mode 100644 index 0000000..e583035 --- /dev/null +++ b/entropie/src/game/sound.c @@ -0,0 +1,179 @@ +#include "game/sound.h" +#include "core/audio.h" +#include "world/camp.h" +#include "world/weather.h" + +#include <math.h> + +#define HEAR_REF 4.0f /* m: full loudness this near ... */ +#define HEAR_MOST 70.0f /* ... nothing from further than this */ +#define STEP_GAIN 0.45f /* steps are soft */ +#define BIRD_EVERY 1.6f /* s: a bird now and then, by day */ + +static unsigned g_rng = 0xC0FFEEu; +static float dice(void) { g_rng = g_rng * 1664525u + 1013904223u; return (float)(g_rng >> 8) / 16777216.0f; } + +typedef struct { Vec3 eye, right; } Ear; + +/* How loud from here (0..1) and from which side (-1 left .. 1 right); muffled: the high end goes further off. */ +static float hear(const Ear *ear, Vec3 at, float *pan, float *dull) { + Vec3 to = vec3_sub(at, ear->eye); + float d = vec3_length(to); + *pan = d > 0.3f ? 0.85f * vec3_dot(vec3_scale(to, 1.0f / d), ear->right) : 0.0f; + if (dull) *dull = 1.0f / (1.0f + d / 25.0f); + if (d > HEAR_MOST) return 0.0f; + return HEAR_REF / (HEAR_REF + d) * (1.0f - d / HEAR_MOST); +} + +static void play(Wave wave, float f0, float f1, float low, float high, float attack, float decay, float length, float gain, float pan, float delay) { + Voice v = { wave, f0, f1, low, high, attack, decay, length, gain, pan, delay }; + Audio_play(&v); +} + +static void step(const Noise *n, float g, float pan, float dull) { + g *= STEP_GAIN * (0.85f + 0.3f * dice()); + float lo = dull; + switch (n->surface) { + case STEP_GRAVEL: + play(WAVE_CRACKLE, 900.0f, 0.0f, 7000.0f * lo, 900.0f, 0.004f, 0.05f, 0.14f, 0.9f * g, pan, 0.0f); + play(WAVE_NOISE, 0.0f, 0.0f, 2500.0f * lo, 300.0f, 0.004f, 0.04f, 0.1f, 0.4f * g, pan, 0.0f); + break; + case STEP_HARD: + play(WAVE_NOISE, 0.0f, 0.0f, 3500.0f * lo, 400.0f, 0.001f, 0.018f, 0.06f, 0.7f * g, pan, 0.0f); + play(WAVE_SINE, 160.0f, 110.0f, 0.0f, 0.0f, 0.001f, 0.02f, 0.06f, 0.35f * g, pan, 0.0f); + break; + case STEP_WOOD: + play(WAVE_SINE, 150.0f, 120.0f, 0.0f, 0.0f, 0.002f, 0.05f, 0.15f, 0.6f * g, pan, 0.0f); + play(WAVE_NOISE, 0.0f, 0.0f, 1500.0f * lo, 200.0f, 0.002f, 0.025f, 0.08f, 0.35f * g, pan, 0.0f); + break; + case STEP_EARTH: + play(WAVE_NOISE, 0.0f, 0.0f, 900.0f * lo, 90.0f, 0.004f, 0.045f, 0.12f, 0.7f * g, pan, 0.0f); + break; + case STEP_MUD: /* a squelch */ + play(WAVE_NOISE, 0.0f, 0.0f, 700.0f * lo, 60.0f, 0.01f, 0.08f, 0.2f, 0.7f * g, pan, 0.0f); + play(WAVE_SINE, 220.0f, 90.0f, 0.0f, 0.0f, 0.01f, 0.07f, 0.16f, 0.35f * g, pan, 0.03f); + break; + case STEP_WATER: /* a splash */ + play(WAVE_NOISE, 0.0f, 0.0f, 6000.0f * lo, 900.0f, 0.005f, 0.09f, 0.25f, 0.6f * g, pan, 0.0f); + play(WAVE_CRACKLE, 300.0f, 0.0f, 5000.0f * lo, 1500.0f, 0.005f, 0.1f, 0.25f, 0.4f * g, pan, 0.02f); + break; + default: /* grass: a soft swish */ + play(WAVE_NOISE, 0.0f, 0.0f, 2800.0f * lo, 700.0f, 0.01f, 0.05f, 0.14f, 0.45f * g, pan, 0.0f); + break; + } +} + +static void noiseOut(const Noise *n, const Ear *ear) { + float pan, dull, g = hear(ear, n->at, &pan, &dull) * n->loud; + if (g < 0.003f) return; + switch (n->kind) { + case NOISE_STEP: step(n, g, pan, dull); break; + case NOISE_CHOP: /* the edge into the wood: a knock and a crack */ + play(WAVE_SINE, 110.0f, 70.0f, 0.0f, 0.0f, 0.001f, 0.06f, 0.2f, 0.8f * g, pan, 0.0f); + play(WAVE_NOISE, 0.0f, 0.0f, 3000.0f * dull, 250.0f, 0.001f, 0.035f, 0.12f, 0.7f * g, pan, 0.0f); + play(WAVE_CRACKLE, 700.0f, 0.0f, 6000.0f * dull, 1200.0f, 0.001f, 0.04f, 0.1f, 0.4f * g, pan, 0.01f); + break; + case NOISE_CLINK: { /* glass on glass, twice */ + float f = 2400.0f + 600.0f * dice(); + play(WAVE_SINE, f, 0.0f, 0.0f, 0.0f, 0.001f, 0.12f, 0.5f, 0.35f * g, pan, 0.0f); + play(WAVE_SINE, f * 1.62f, 0.0f, 0.0f, 0.0f, 0.001f, 0.08f, 0.4f, 0.22f * g, pan, 0.0f); + play(WAVE_SINE, f * 1.1f, 0.0f, 0.0f, 0.0f, 0.001f, 0.07f, 0.3f, 0.2f * g, pan, 0.07f + 0.05f * dice()); + break; + } + case NOISE_SMASH: /* glass breaking: a crash and the shards tinkling */ + play(WAVE_CRACKLE, 2500.0f, 0.0f, 9000.0f * dull, 1800.0f, 0.001f, 0.12f, 0.5f, 0.9f * g, pan, 0.0f); + for (int k = 0; k < 6; k++) + play(WAVE_SINE, 3000.0f + 4000.0f * dice(), 0.0f, 0.0f, 0.0f, 0.001f, 0.05f, 0.2f, 0.18f * g, pan, 0.03f + 0.25f * dice()); + break; + case NOISE_THUD: /* somebody down on the ground */ + play(WAVE_SINE, 75.0f, 45.0f, 0.0f, 0.0f, 0.002f, 0.12f, 0.4f, 0.9f * g, pan, 0.0f); + play(WAVE_NOISE, 0.0f, 0.0f, 500.0f * dull, 40.0f, 0.002f, 0.08f, 0.3f, 0.6f * g, pan, 0.0f); + break; + case NOISE_CRASH: /* a tree down: branches cracking, the trunk hitting the ground */ + play(WAVE_CRACKLE, 400.0f, 0.0f, 5000.0f * dull, 600.0f, 0.02f, 0.5f, 2.0f, 0.9f * g, pan, 0.0f); + play(WAVE_NOISE, 0.0f, 0.0f, 260.0f, 25.0f, 0.03f, 0.7f, 2.5f, 1.0f * g, pan, 0.05f); + play(WAVE_SINE, 55.0f, 35.0f, 0.0f, 0.0f, 0.01f, 0.4f, 1.5f, 0.8f * g, pan, 0.05f); + break; + case NOISE_SNAP: /* a dry branch underfoot */ + play(WAVE_CRACKLE, 1500.0f, 0.0f, 7000.0f * dull, 900.0f, 0.001f, 0.03f, 0.08f, 0.8f * g, pan, 0.0f); + play(WAVE_NOISE, 0.0f, 0.0f, 2500.0f * dull, 400.0f, 0.001f, 0.02f, 0.06f, 0.5f * g, pan, 0.0f); + break; + default: break; + } +} + +/* Lightning just now: the thunder rolling, long and low (near: a crack first). */ +static void thunder(void) { + float near = dice(), pan = 1.6f * dice() - 0.8f; + if (near > 0.6f) play(WAVE_CRACKLE, 600.0f, 0.0f, 3500.0f, 300.0f, 0.005f, 0.25f, 0.8f, 0.7f, pan, 0.0f); + play(WAVE_NOISE, 0.0f, 0.0f, 140.0f + 120.0f * near, 20.0f, 0.3f, 1.6f + 1.0f * (1.0f - near), 7.0f, 0.9f, pan, 0.05f); + play(WAVE_NOISE, 0.0f, 0.0f, 90.0f, 15.0f, 0.6f, 2.2f, 7.0f, 0.7f, -pan, 0.9f + 0.6f * dice()); +} + +/* A bird somewhere near: a few quick rising whistles. */ +static void bird(void) { + float f = 2200.0f + 2600.0f * dice(), pan = 1.8f * dice() - 0.9f, g = 0.05f + 0.06f * dice(); + int notes = 2 + (int)(4.0f * dice()); + for (int k = 0; k < notes; k++) + play(WAVE_SINE, f, f * (1.15f + 0.4f * dice()), 0.0f, 0.0f, 0.006f, 0.03f, 0.09f, g, pan, 0.11f * (float)k); +} + +void Sound_update(Game *game, float dt, bool paused) { + World *w = &game->world; + Audio_pause(paused); + if (paused) { w->noise_count = 0; return; } + Ear ear = { game->camera.position, game->camera.right }; + const Object *you = game->player; + bool tent = you && you->in_tent; + + for (int i = 0; i < w->noise_count; i++) noiseOut(&w->noises[i], &ear); + w->noise_count = 0; + if (w->thunder) thunder(); + + /* the air: wind and rain (in a tent: dull and drumming on it) */ + float wind = fminf(1.0f, hypotf(w->wind.x, w->wind.z) / WIND_MAX); + Audio_bed(BED_WIND, (0.06f + 0.55f * wind) * (tent ? 0.5f : 1.0f), 0.0f, wind); + Audio_bed(BED_RAIN, 0.6f * smoothstepf(0.02f, 0.6f, w->rain), 0.0f, tent ? 1.0f : 0.0f); + + /* fires, a chainsaw, the hose: the loudest near you */ + float fire_g = 0.0f, fire_pan = 0.0f, fire_k = 0.0f, saw_g = 0.0f, saw_pan = 0.0f, saw_k = 0.0f, hose_g = 0.0f, hose_pan = 0.0f; + for (int i = 0; i < w->count; i++) { + const Object *o = w->objects[i]; + float pan, g; + if (o->type == OBJ_FIREPLACE && o->burning) { + float size = Camp_flameSize(o); + g = hear(&ear, o->position, &pan, NULL) * (0.4f + 0.6f * size); + if (g > fire_g) { fire_g = g; fire_pan = pan; fire_k = size; } + } else if (o->is_character) { + const Work *ws = &o->work_state; + if (ws->tool == TOOL_CHAINSAW && Tools_held(o, NULL) == TOOL_CHAINSAW && (Tools_isWorking(o) || ws->saw_in > 0.05f)) { + g = hear(&ear, o->position, &pan, NULL); + if (g > saw_g) { saw_g = g; saw_pan = pan; saw_k = ws->saw_in; } + } + if (o->spraying) { + g = hear(&ear, o->position, &pan, NULL); + if (g > hose_g) { hose_g = g; hose_pan = pan; } + } + } + } + Audio_bed(BED_FIRE, 0.9f * fire_g, fire_pan, fire_k); + Audio_bed(BED_SAW, 0.8f * saw_g, saw_pan, saw_k); + Audio_bed(BED_HOSE, 0.5f * hose_g, hose_pan, 0.0f); + + /* the car coming in */ + const Object *car = game->arrival.car; + if (car && game->arrival.stage == ARRIVAL_DRIVING) { + float pan, g = you && you->riding ? 0.6f : hear(&ear, car->position, &pan, NULL); + 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); + + /* 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; + Audio_bed(BED_CRICKETS, night ? 0.12f * calm * (tent ? 0.5f : 1.0f) : 0.0f, 0.0f, 0.0f); + static float bird_t = 0.0f; + if (!night && hour > 5.5f && hour < 20.5f && (bird_t -= dt) <= 0.0f) { + bird_t = BIRD_EVERY * (0.3f + 1.7f * dice()) / fmaxf(0.15f, calm); + if (dice() < calm * (hour < 9.0f ? 1.0f : 0.5f)) bird(); /* (mornings: more of them) */ + } +} diff --git a/entropie/src/game/sound.h b/entropie/src/game/sound.h new file mode 100644 index 0000000..313d14f --- /dev/null +++ b/entropie/src/game/sound.h @@ -0,0 +1,16 @@ +#ifndef GAME_SOUND_H +#define GAME_SOUND_H + +/* + * What you hear: the world's sounds of this frame (steps, chops, clinks, ...: + * World_noise) and the steady ones (wind, rain, the fire, a chainsaw, the car, + * crickets at night, birds by day), from where they are - quieter further + * off, from their side. Played by core/audio.c. + */ + +#include "game/game.h" + +/* Every frame after the game's update; paused (a menu): the beds go quiet. */ +void Sound_update(Game *game, float dt, bool paused); + +#endif /* GAME_SOUND_H */ diff --git a/entropie/src/main.c b/entropie/src/main.c index 34412cc..0838773 100644 --- a/entropie/src/main.c +++ b/entropie/src/main.c @@ -5,6 +5,8 @@ * Layers: core (platform, gl, input) -> render -> game -> main */ +#include "core/audio.h" +#include "game/sound.h" #include <math.h> #include <stdio.h> #include <string.h> @@ -67,7 +69,7 @@ static const char *const KEYS[] = { /* proportional font: no column padding */ "F1: dev menu (change anybody's stats)", "Tab: your stats (arrow keys: next page)", "K: smoke; B: your pockets (Up / Down, Enter: take out or give)", - "Esc: menu (resume, save, load, keys, quit); F5 quick save, F9 quick load", + "Esc: menu (resume, save, load, keys, quit); F5 quick save, F9 quick load; M sound off / on", }; #define KEY_COUNT ((int)(sizeof(KEYS) / sizeof(KEYS[0]))) #define MAX_FRAME_DT 0.1f /* avoids tunneling after stalls (window drag etc.) */ @@ -282,6 +284,7 @@ int main(int argc, char **argv) { if (!Skybox_init(&skybox)) goto cleanup; if (!Ui_init(&ui, UI_FONT, UI_FONT_SIZE)) goto cleanup; if (!(game_ready = Game_init(&game))) goto cleanup; + Audio_init(); /* (no sound device: silent, that's all) */ Platform_frameDelta(&platform); /* reset timer after loading */ float anim_time = 0.0f; /* real seconds, drives the painted animation */ @@ -412,15 +415,22 @@ int main(int argc, char **argv) { look_settle = false; } Game_update(&game, &input, dt, (float)width / (float)height); + if (input.mute) { /* M: sound off / on */ + Audio_mute(!Audio_muted()); + snprintf(note, sizeof note, Audio_muted() ? "Sound off (M)" : "Sound on"); + note_time = 2.0f; + } if ((autosave_t += dt) >= AUTOSAVE_EVERY && !Save_blocked(&game)) { /* (every few minutes, quietly) */ autosave_t = 0.0f; if (Save_write(&game, menu.dir, SAVE_AUTO, err, sizeof err)) { snprintf(note, sizeof note, "Autosaved"); note_time = 2.0f; } } } + Sound_update(&game, dt, menu.screen != SCREEN_NONE); Renderer_beginFrame(&renderer, width, height, anim_time); Game_render(&game, &renderer); Skybox_draw(&skybox, &renderer, &game.camera, &game.daycycle.light); + Game_renderAir(&game, &renderer); /* (rain and smoke against the sky too) */ Renderer_endFrame(&renderer); char clock_text[UI_TEXT_MAX]; @@ -463,6 +473,7 @@ done: exit_code = 0; cleanup: + Audio_shutdown(); if (game_ready) Game_shutdown(&game); if (platform.gl_context) { Ui_shutdown(&ui); diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c index efff106..abd9ad2 100644 --- a/entropie/src/people/ai.c +++ b/entropie/src/people/ai.c @@ -499,7 +499,7 @@ static Object *findConsumable(const Object *self, Object *const *objects, int co bool crate = o->type == OBJ_CRATE; /* (a crate: a bottle out of it) */ bool fits = want == WANT_FOOD ? Object_isFood(o) : Object_isBottle(o); if (want == WANT_BEER && o->alcohol <= 0.0f) fits = false; - if (crate) fits = want != WANT_FOOD && o->stored > 0 && (want != WANT_BEER || o->variant == 0); /* (thirsty: water, soda or mate too) */ + if (crate) fits = want != WANT_FOOD && o->stored - o->crate_empties > 0 && (want != WANT_BEER || o->variant == 0); /* (thirsty: water, soda or mate too) */ if (!fits || o->is_held || (!crate && o->sips <= 0) || o->broken) continue; if (ai_skipped(&self->ai, o)) continue; /* couldn't get at it */ bool taken = false; @@ -753,7 +753,7 @@ static void updateFetch(Object *self, Object *const *objects, int count, float d Object *bottle = ai->target; ai->timer += dt; bool crate = bottle && bottle->type == OBJ_CRATE; - if (!bottle || bottle->is_held || (crate ? bottle->stored <= 0 : bottle->sips <= 0) || ai->timer > FETCH_TIMEOUT || + if (!bottle || bottle->is_held || (crate ? bottle->stored - bottle->crate_empties <= 0 : bottle->sips <= 0) || ai->timer > FETCH_TIMEOUT || ai_skipped(ai, bottle)) { /* gone, or can't get at it */ stand(self); giveUpBottle(ai); @@ -2034,14 +2034,51 @@ static bool goInside(Object *self, Object *const *objects, int count, float dt) but sit out a shower inside). */ #define SHELTER_RAIN 0.3f +#define UMBRELLA_FETCH 25.0f /* m: an umbrella lying this near is fetched when it starts to rain */ + +static bool holdsUmbrella(const Object *self) { + for (int h = 0; h < 2; h++) + if (self->hands.item[h] && self->hands.item[h]->type == OBJ_UMBRELLA && self->hands.state[h] == HAND_HOLDING) return true; + return false; +} + static bool shelter(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; + if (holdsUmbrella(self)) { ai->sheltering = false; ai->umbrella = NULL; return false; } /* (out in it under that) */ + Object *u = ai->umbrella; + if (u && (u->is_held || !g_world || g_world->rain < SHELTER_RAIN * 0.5f || Hands_freeHand(self) < 0)) u = ai->umbrella = NULL; + if (u) { /* over to it and up with it */ + if (self->sitting) self->sitting = false; + if (Hands_gapFrom(self->position, self->look.height, u) < PICKUP_REACH) { + stand(self); + if (Hands_pickUpObject(self, Hands_freeHand(self), u) && random01(ai) < 0.6f) + Ai_say(self, random01(ai) < 0.5f ? "I'll take the umbrella." : "Good thing there's an umbrella."); + ai->umbrella = NULL; + return true; + } + if ((ai->timer += dt) > 40.0f) { skipTarget(ai, u); ai->umbrella = NULL; return true; } /* (can't get at it) */ + goToPoint(self, u->position, 0.3f, objects, count, dt); + return true; + } if (!g_world || g_world->rain < SHELTER_RAIN || self->indoors > 0.0f || self->in_tent || self->body.asleep || self->riding) { if (!g_world || g_world->rain < SHELTER_RAIN * 0.5f) ai->sheltering = false; return false; } if (!ai->sheltering) { if (!self->rain_out) return false; + if (Hands_freeHand(self) >= 0 && ai->mode != AI_WORK && !self->lives_here) { /* an umbrella near: that instead */ + Object *best = NULL; + float best_d = UMBRELLA_FETCH; + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o->type != OBJ_UMBRELLA || o->is_held || ai_skipped(ai, o)) continue; + bool claimed = false; + for (int j = 0; j < count && !claimed; j++) claimed = objects[j] != self && objects[j]->is_character && objects[j]->ai.umbrella == o; + float d = flatDistance(o->position, self->position); + if (!claimed && d < best_d) { best_d = d; best = o; } + } + if (best) { ai->umbrella = best; ai->timer = 0.0f; return true; } + } ai->sheltering = true; if (random01(ai) < 0.6f) Ai_say(self, random01(ai) < 0.5f ? "It's pouring - inside, quick!" : self->lives_here ? "Rain! Into the house." : "Rain! I'm off under a roof."); for (int h = 0; h < 2; h++) { /* (crates and gear down where they are; tools along) */ @@ -2101,19 +2138,80 @@ static bool dryOff(Object *self, Object *const *objects, int count, float dt) { /* Just arrived: over to the fire to say hello to Kurti and his sons, and sit down with them till the work starts (game_arrival.c). */ +#define GREET_BENCH 5.0f /* m: benches this near the fire are sat on */ +#define GREET_STAND 3.7f /* m from the fire: where they stand if there's no room on them */ +#define GREET_ASK 3.0f /* m: a bench where they'd have to ask counts as this much further away */ + +/* No room on the benches: a free spot in the ring round the fire, nearest first. */ +static Vec3 standByFire(const Object *self, const Object *fire, Object *const *objects, int count) { + Vec3 to = flat(vec3_sub(self->position, fire->position)); + float a0 = atan2f(to.z, to.x); + for (int k = 0; k < 24; k++) { + float a = a0 + 0.26f * (float)((k + 1) / 2) * (k % 2 ? 1.0f : -1.0f); + Vec3 at = vec3_add(fire->position, vec3(cosf(a) * GREET_STAND, 0.0f, sinf(a) * GREET_STAND)); + bool free = !ai_blockedBy(self, NULL, NULL, objects, count, at); + for (int i = 0; i < count && free; i++) { + const Object *o = objects[i]; + if (o != self && o->is_character && o->ai.greet_stand && flatDistance(o->ai.greet_at, at) < 0.9f) free = false; + } + if (free) return at; + } + return vec3_add(fire->position, vec3_scale(vec3_length(to) > 0.1f ? vec3_normalize(to) : vec3(1.0f, 0.0f, 0.0f), GREET_STAND)); +} + +/* Just arrived: over to the fire, sitting down on a bench there with Kurti and + his sons (the others move up for them - the unwilling only when asked); no + room anywhere: standing by the fire with them. Then the hello. */ static bool greetHosts(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; - if (!ai->greet) return false; + if (!ai->greet) { ai->greet_stand = ai->greet_asked = false; return false; } if (self->sitting) return false; /* (there: whatever they do sitting) */ Object *fire = Camp_fireNear(objects, count, self->position, 120.0f, false); if (!fire) { ai->greet = false; return false; } - Vec3 to = vec3_sub(self->position, fire->position); - to.y = 0.0f; - float turn = ai->side * 0.8f, len = vec3_length(to); - to = len > 0.1f ? vec3_scale(to, 1.0f / len) : vec3(1.0f, 0.0f, 0.0f); - Vec3 dir = vec3(to.x * cosf(turn) - to.z * sinf(turn), 0.0f, to.x * sinf(turn) + to.z * cosf(turn)); - if (!goToPoint(self, vec3_add(fire->position, vec3_scale(dir, 2.7f)), 0.7f, objects, count, dt)) return true; - Object_sit(self, Object_seatNear(objects, count, self->position, 1.5f), fire->position); + if (ai->greet_stand) { /* standing with the others, facing the fire */ + if (!goToPoint(self, ai->greet_at, 0.35f, objects, count, dt)) return true; + stand(self); + turnTowards(self, flat(vec3_sub(fire->position, self->position)), dt); + return true; + } + Object *best = NULL; + bool best_asked = false; + float best_d = 1e9f; + for (int i = 0; i < count; i++) { + Object *b = objects[i]; + if (b->type != OBJ_FURNITURE || b->variant != FURN_BENCH || flatDistance(b->position, fire->position) > GREET_BENCH) continue; + int room = Object_benchRoom(self, b, self->position, false); + bool asked = false; + if (room == ROOM_NONE) { room = Object_benchRoom(self, b, self->position, true); asked = true; } + if (room == ROOM_NONE) continue; + float d = flatDistance(b->position, self->position) + (asked ? GREET_ASK : 0.0f); + if (d < best_d) { best_d = d; best = b; best_asked = asked; } + } + if (!best) { + ai->greet_stand = true; + ai->greet_at = standByFire(self, fire, objects, count); + return true; + } + /* in front of it, on the fire's side, where along it they come to it */ + Vec3 axis = flat(best->colliders[0].axes[0]), out = flat(vec3_sub(fire->position, best->position)); + axis = vec3_length(axis) > 1e-3f ? vec3_normalize(axis) : vec3(1.0f, 0.0f, 0.0f); + out = vec3_length(out) > 1e-3f ? vec3_normalize(out) : vec3(0.0f, 0.0f, 1.0f); + float half = fmaxf(best->colliders[0].half_extents.x - 0.28f, 0.0f); + float along = clampf(vec3_dot(vec3_sub(self->position, best->position), axis), -half, half); + Vec3 spot = vec3_add(best->position, vec3_add(vec3_scale(axis, along), vec3_scale(out, 0.6f))); + if (!goToPoint(self, spot, 0.5f, objects, count, dt)) return true; + if (best_asked && !ai->greet_asked) { /* "move up a bit?" - and they do, the grumpy grumbling */ + ai->greet_asked = true; + Ai_say(self, random01(ai) < 0.5f ? "Move up a bit?" : "Room for one more?"); + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o == self || !o->is_character || o->seat != best || !o->sitting || o->type == OBJ_PLAYER) continue; + Ai_say(o, o->ai.character.willing > 0.5f ? "Sure, sit down." : "Hm. Alright then."); + break; + } + } + Object_benchMakeRoom(self, best, self->position, best_asked); + if (!Object_sit(self, best, fire->position)) return true; /* (not yet: again next time) */ rest(ai, 300.0f, 400.0f); if (random01(ai) < 0.7f) Ai_say(self, random01(ai) < 0.5f ? "Hello hello! Good to be back." : "Kurti! Still the king of the fire?"); return true; diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h index 6700097..0d20c8c 100644 --- a/entropie/src/people/ai.h +++ b/entropie/src/people/ai.h @@ -172,17 +172,25 @@ typedef struct { bool tool_asked; /* asked the player for their axe (once per order) */ float tidy_pause; /* bending down for trash on the way (seconds left) */ float tray_time; /* on the way to empty the ashtray this long */ + struct Object *sort_from; /* sorting the crates (ai_help.c sortCrates): to this one for a bottle that doesn't belong ... */ + struct Object *sort_bottle; /* ... carrying this one over to its own crate ... */ + float sort_time; /* ... this long already ... */ + float sort_look; /* ... and looking again for one in this long */ bool want_jacket; /* cold, the pullover put down somewhere: off to get it */ bool want_smokes; /* out of cigarettes, nobody gives one: off to the machine */ float smokes_time; /* ... on the way this long */ bool asked_you; /* asked the player for one (nothing came of it: the machine next time) */ struct Object *take_from; /* a bottle from this crate, or the next thing out of this car's boot: please (see car.c Car_serve) */ bool sheltering; /* in out of the rain (ai.c shelter) */ - bool greet; /* just arrived: over to the fire, saying hello, sitting there till the work starts */ + bool greet; /* just arrived: over to the fire, saying hello, sitting there till the work starts ... */ + bool greet_stand; /* ... no room on the benches: standing with the others here instead ... */ + Vec3 greet_at; + bool greet_asked; /* ... asked those on a bench to move up */ bool drying; /* wet through after the rain: sitting by the fire till dry */ bool timber; /* felling: waiting for somebody to get out from where it'll come down */ Vec3 dodge_to; /* warned off (a tree coming down): out to here ... */ float dodge_t; /* ... for this long */ + struct Object *umbrella; /* rain: off to get this umbrella instead of going under a roof (ai.c shelter) */ struct Object *blown; /* the wind took this out of their hand: after it (ai.c fetchBlown) ... */ float blown_t; /* ... this long already */ struct Object *hang_on; /* warm, the pullover round the waist: over this (a railing, a chair) it goes ... */ diff --git a/entropie/src/people/ai_help.c b/entropie/src/people/ai_help.c index 6c25a9e..7564ef9 100644 --- a/entropie/src/people/ai_help.c +++ b/entropie/src/people/ai_help.c @@ -243,6 +243,68 @@ static Object *trashInHands(const Object *self) { return NULL; } +/* Bottles put back into the wrong crate: the tidy sort them - one out, over to + a crate of its own kind with room, in (the crates go back to the shop sorted). */ +#define SORT_LOOK 25.0f /* m: crates this near are looked at ... */ +#define SORT_FAR 60.0f /* ... and a bottle's own crate this near is walked to */ +#define SORT_GIVE 45.0f /* s: can't get there: given up */ +static bool sortCrates(Object *self, Object *const *objects, int count, float dt) { + Ai *ai = &self->ai; + Object *b = ai->sort_bottle; + if (b) { /* over to its own crate with it */ + bool held = self->hands.item[HAND_LEFT] == b || self->hands.item[HAND_RIGHT] == b; + Object *to = held && !b->broken ? Crate_rightFor(objects, count, Crate_bottleKind(b), self->position, SORT_FAR) : NULL; + if (!to || (ai->sort_time += dt) > SORT_GIVE) { + if (held) for (int h = 0; h < 2; h++) if (self->hands.item[h] == b) Hands_drop(self, h); + ai->sort_bottle = NULL; + return false; + } + if (!goTo(self, to, objects, count, 0.8f, dt)) return true; + for (int h = 0; h < 2; h++) if (self->hands.item[h] == b) Hands_drop(self, h); + Hands_touch(self, vec3_add(Object_colliderCenter(to, 0), vec3(0.0f, 0.2f, 0.0f))); + Crate_putBottle(to, b); + ai->sort_bottle = NULL; + ai->tidy_pause = 0.6f; + return true; + } + if (ai->mode != AI_IDLE && ai->mode != AI_WANDER) { ai->sort_from = NULL; return false; } + Object *from = ai->sort_from; + if (from) { /* to the crate, the bottle out */ + int hand = Hands_freeHand(self); + if (hand < 0 || from->is_held || !Crate_nextWrong(from) || (ai->sort_time += dt) > SORT_GIVE) { + if (ai->sort_time > SORT_GIVE) skipTarget(ai, from); + ai->sort_from = NULL; + return false; + } + if (!goTo(self, from, objects, count, 0.8f, dt)) return true; + ai->sort_from = NULL; + Hands_touch(self, vec3_add(Object_colliderCenter(from, 0), vec3(0.0f, 0.2f, 0.0f))); + Object *w = Crate_takeWrong(g_world, from); + if (w && Hands_receive(self, hand, w)) { /* (straight out of the crate into the hand) */ + ai->sort_bottle = w; + ai->sort_time = 0.0f; + if (random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "That doesn't go in there." : "Who puts these in the wrong crate?"); + } + ai->tidy_pause = 0.5f; + return true; + } + if (Hands_freeHand(self) < 0 || trashInHands(self) || (ai->sort_look -= dt) > 0.0f) return false; + ai->sort_look = 3.0f; + for (int i = 0; i < count; i++) { /* a crate with one that doesn't belong, its own crate with room somewhere */ + Object *c = objects[i]; + int kind = c->type == OBJ_CRATE && !c->is_held ? Crate_nextWrong(c) : BOTTLE_KIND_NONE; + if (!kind || ai_skipped(ai, c) || flatDistance(c->position, self->position) > SORT_LOOK) continue; + if (!Crate_rightFor(objects, count, kind, c->position, SORT_FAR)) continue; + bool taken = false; + for (int j = 0; j < count && !taken; j++) taken = objects[j] != self && objects[j]->is_character && objects[j]->ai.sort_from == c; + if (taken) continue; + ai->sort_from = c; + ai->sort_time = 0.0f; + return true; + } + return false; +} + /* Returns true if it took over this frame (bending down, or off to the container). */ bool ai_tidyUp(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; @@ -291,10 +353,12 @@ bool ai_tidyUp(Object *self, Object *const *objects, int count, float dt) { return true; } } + /* bottles in the wrong crate: sorted */ + if (sortCrates(self, objects, count, dt)) return true; /* nothing better to do: an empty bottle back into a crate, other trash to the container */ Object *held = trashInHands(self); if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && held && Object_isBottle(held) && !held->broken) { - Object *crate = Crate_withRoom(objects, count, self->position, TIDY_BIN); + Object *crate = Crate_forBottle(objects, count, held, self->position, TIDY_BIN); /* (its own crate if there's one) */ if (crate && !ai_skipped(ai, crate)) { if ((ai->tray_time += dt) > 45.0f) { skipTarget(ai, crate); ai->tray_time = 0.0f; return false; } /* (can't get there) */ if (goTo(self, crate, objects, count, 0.8f, dt)) { @@ -327,7 +391,8 @@ bool ai_putAway(Object *self, Object *const *objects, int count, float dt) { for (int h = 0; h < 2; h++) { const Object *it = self->hands.item[h]; if (self->hands.state[h] == HAND_HOLDING && it && it->has_home && - (it->type == OBJ_AXE || it->type == OBJ_SHEARS || it->type == OBJ_CHAINSAW)) hand = h; + (it->type == OBJ_AXE || it->type == OBJ_SHEARS || it->type == OBJ_CHAINSAW || + (it->type == OBJ_UMBRELLA && g_world && g_world->rain < 0.02f))) hand = h; /* (an umbrella once the rain's over) */ } if (hand < 0 || ai->talking) return false; Object *tool = self->hands.item[hand]; diff --git a/entropie/src/people/body.c b/entropie/src/people/body.c index 2657a5f..7d97476 100644 --- a/entropie/src/people/body.c +++ b/entropie/src/people/body.c @@ -259,16 +259,18 @@ void Body_dropAndRoll(Body *b, Vec3 dir) { #define LAND_EASY 5.0f /* m/s down (about 1.3 m): the legs take it as they are */ #define LAND_DEEP_ 7.0f /* (2.5 m) deep into the knees; faster: roll or crash */ -#define LAND_BREAK 9.5f /* (4.6 m) nobody lands that on their feet */ +#define LAND_BREAK 9.5f /* (4.6 m) nobody lands that on their feet ... */ +#define LAND_SPORT 1.3f /* ... but the very sporty roll it off up to this much faster (5.8 m) */ -int Body_land(Body *b, Vec3 dir, float speed, float sure) { +int Body_land(Body *b, Vec3 dir, float speed, float sure, float sporty) { if (b->pose != POSE_UP || speed < LAND_EASY) return LAND_SOFT; float hard = (speed - LAND_EASY) / (LAND_DEEP_ - LAND_EASY); /* 0 .. 1 at a deep landing */ b->land_squat = clampf(0.35f + 0.5f * hard, 0.0f, 1.1f); b->crouch_v += 3.0f + 2.0f * hard; /* (down at once, not eased) */ if (speed < LAND_DEEP_) return LAND_DEEP; - float risk = (speed - LAND_DEEP_) / (LAND_BREAK - LAND_DEEP_); /* 0 .. 1 */ - if (speed < LAND_BREAK && random01(b) < sure * (1.0f - 0.7f * risk)) { /* over the shoulder, and up again */ + float limit = LAND_BREAK + LAND_SPORT * sporty; + float risk = (speed - LAND_DEEP_) / (limit - LAND_DEEP_) * (1.0f - 0.8f * sporty); /* 0 .. 1 (the very sporty: hardly any) */ + if (speed < limit && random01(b) < sure * (1.0f - 0.7f * risk)) { /* over the shoulder, and up again */ float lateral = vec3_dot(flat(dir), rightOf(b->last_facing)); /* (the shoulder on the side the way goes, else their own) */ float side = fabsf(lateral) > 0.2f ? (lateral > 0.0f ? 1.0f : -1.0f) : (b->quirk[0] > 0.0f ? 1.0f : -1.0f); Body_shoulderRoll(b, dir, side); diff --git a/entropie/src/people/body.h b/entropie/src/people/body.h index fb79485..3e7ca6e 100644 --- a/entropie/src/people/body.h +++ b/entropie/src/people/body.h @@ -293,10 +293,11 @@ Vec3 Body_takeShift(Body *body); void Body_stagger(Body *body, Vec3 push); /* Falls over towards dir (world), lies a moment and gets up again. */ void Body_fall(Body *body, Vec3 dir); -/* Landed from high up at this speed (m/s down); sure 0..1 (fit, sober, fresh, sure-footed). +/* Landed from high up at this speed (m/s down); sure 0..1 (fit, sober, fresh, sure-footed), + sporty 0..1 (the very sporty: they roll off drops of 5 m and more). Soft: into the knees; harder: deep, a roll (the sure) or a crash. Returns LAND_*. */ enum { LAND_SOFT, LAND_DEEP, LAND_ROLL, LAND_CRASH }; -int Body_land(Body *body, Vec3 dir, float speed, float sure); +int Body_land(Body *body, Vec3 dir, float speed, float sure, float sporty); /* Over one shoulder (side -1 left, 1 right) along dir (world) and up on the feet again: the sporty landing from high up. */ void Body_shoulderRoll(Body *body, Vec3 dir, float side); bool Body_isRolling(const Body *body); diff --git a/entropie/src/people/hands.c b/entropie/src/people/hands.c index 181bc0f..55b6170 100644 --- a/entropie/src/people/hands.c +++ b/entropie/src/people/hands.c @@ -87,7 +87,7 @@ static Vec3 rightOf(Vec3 facing) { return vec3_normalize(vec3_cross(facing, UP)) static float scaleOf(const Object *who) { return who->look.height / 1.75f; } static bool liftableOneHand(const Object *o) { - bool tool = o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_LADDER; /* long, but made for one hand */ + bool tool = o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_LADDER || o->type == OBJ_UMBRELLA; /* long, but made for one hand */ return o->weight <= ONE_HAND_MAX_WEIGHT && (tool || largestSize(o) <= ONE_HAND_MAX_SIZE); } @@ -1020,6 +1020,8 @@ void Hands_update(Object *who, float dt) { b->hand_target[1 - i] = vec3_add(Ladder_raisedGrip(item, 1.0f), vec3_add(rail, vec3_scale(right, -sign * 0.02f))); } } + if (item->type == OBJ_UMBRELLA && item->umbrella_open) /* held up over the head, the hand in front of the shoulder */ + local = vec3(sign * 0.10f * s, 1.18f * s, 0.24f * s); if (item->type == OBJ_CAN) { /* watering: the arm comes up and forward */ Vec3 lifted = vec3(sign * 0.13f * s, 1.02f * s, 0.40f * s); local = vec3_lerp(local, lifted, smoothstepf(0.0f, 1.0f, who->pour)); @@ -1197,7 +1199,11 @@ void Hands_placeHeld(Object *who) { item->rotation.x = item->rotation.z = 0.0f; Object_syncColliders(item); } - if (Object_isBottle(item)) { + if (item->type == OBJ_UMBRELLA && item->umbrella_open) { /* upright, the handle in the fist */ + item->rotation = vec3(0.0f, item->rotation.y, 0.0f); + Object_syncColliders(item); + center = vec3_add(hand, vec3(0.0f, UMBRELLA_LENGTH * 0.5f - 0.07f, 0.0f)); + } else if (Object_isBottle(item)) { /* The hand around the bottle where it grips it */ center = vec3_sub(hand, vec3_scale(item->colliders[0].axes[1], bottleGrip(who, i, item))); } else { @@ -1208,6 +1214,6 @@ void Hands_placeHeld(Object *who) { item->position = vec3_sub(center, item->colliders[0].offset); item->velocity = vec3_zero(); if (!twoHanded(h)) settleInHand(who, i, item); - if (h->drink_hand != i) keepOffBody(who, item); + if (h->drink_hand != i && !(item->type == OBJ_UMBRELLA && item->umbrella_open)) keepOffBody(who, item); } } diff --git a/entropie/src/render/figure.c b/entropie/src/render/figure.c index 3577be0..3d1ae76 100644 --- a/entropie/src/render/figure.c +++ b/entropie/src/render/figure.c @@ -18,7 +18,7 @@ static const FigureLook WARDROBE[] = { .hair_style = HAIR_LONG, .long_sleeves = false }, }; -/* Jackets for the night: denim, olive, brown leather, a grey hoodie, red. */ +/* Jackets for the night: denim, an olive parka, brown leather, a grey hoodie, a red rain jacket. */ static const Vec3 JACKETS[] = { { 0.24f, 0.33f, 0.52f }, { 0.36f, 0.40f, 0.22f }, { 0.42f, 0.26f, 0.16f }, { 0.55f, 0.55f, 0.57f }, { 0.62f, 0.16f, 0.15f } }; @@ -26,6 +26,7 @@ FigureLook Figure_wardrobe(int index) { const int n = (int)(sizeof(WARDROBE) / sizeof(WARDROBE[0])); FigureLook look = WARDROBE[((index % n) + n) % n]; look.jacket = JACKETS[((index % 5) + 5) % 5]; + look.hoodie = index % 5 == 1 || index % 5 == 3 || index % 5 == 4; /* (the parka, the hoodie, the red rain jacket) */ return look; } diff --git a/entropie/src/render/figure.h b/entropie/src/render/figure.h index cb012d3..e5492c5 100644 --- a/entropie/src/render/figure.h +++ b/entropie/src/render/figure.h @@ -33,6 +33,8 @@ typedef struct { float belly; /* 0 .. 1: a belly (Kurti's) */ bool keeps_shirt; /* never takes the shirt off, sun or no sun */ float wet; /* 0 .. 1: clothes and hair wet from the rain (darker) */ + bool hoodie; /* the jacket has a hood ... */ + bool hood_up; /* ... pulled up over the head (rain) */ } FigureLook; enum { TOP_SHIRT, TOP_BARE, TOP_CROP, TOP_JACKET }; diff --git a/entropie/src/render/renderer.c b/entropie/src/render/renderer.c index 2b25bc9..cdae0af 100644 --- a/entropie/src/render/renderer.c +++ b/entropie/src/render/renderer.c @@ -106,6 +106,7 @@ static bool loadShaders(Renderer *r) { f->jacket_tied = glGetUniformLocation(f->program, "jacketTied"); f->belly = glGetUniformLocation(f->program, "belly"); f->wet = glGetUniformLocation(f->program, "soaked"); + f->hood = glGetUniformLocation(f->program, "hood"); f->cig_a = glGetUniformLocation(f->program, "cigA"); f->uri_a = glGetUniformLocation(f->program, "uriA"); f->uri_b = glGetUniformLocation(f->program, "uriB"); @@ -384,6 +385,13 @@ void Renderer_beginScenePass(Renderer *r, const Camera *camera, const Lighting * r->pass_program = s->program; } +void Renderer_beginAirPass(Renderer *r) { + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glDepthFunc(GL_LESS); + glUseProgram(r->scene.program); +} + void Renderer_drawMeshTinted(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float glow) { if (!meshInView(r, mesh, model)) return; glUniform4f(r->scene.tint, rgb.x, rgb.y, rgb.z, glow); @@ -550,6 +558,7 @@ void Renderer_drawFigure(Renderer *r, const FigurePose *pose) { glUniform1i(f->jacket_tied, pose->look.jacket_tied && pose->look.top != TOP_JACKET ? 1 : 0); glUniform1f(f->belly, pose->look.belly); glUniform1f(f->wet, pose->look.wet); + glUniform1i(f->hood, pose->look.hoodie && pose->look.hood_up && pose->look.top == TOP_JACKET ? 1 : 0); setVec3(f->cig_a, sk->cig_a); setVec3(f->uri_a, sk->uri_a); setVec3(f->uri_b, sk->uri_b); diff --git a/entropie/src/render/renderer.h b/entropie/src/render/renderer.h index fb7cdb5..ac9b9eb 100644 --- a/entropie/src/render/renderer.h +++ b/entropie/src/render/renderer.h @@ -38,6 +38,7 @@ typedef struct { Vec3 fire_position; /* a campfire's light ... */ float fire_strength; /* ... and how bright it is (0 = none) */ float wet; /* 0..1 everything wet from rain (darker, the ground damp) */ + float overcast; /* 0..1 clouded over (the sky: more and darker clouds, no sun, moon or stars) */ } Lighting; typedef struct { @@ -70,7 +71,7 @@ typedef struct { GLint skin, hair, shirt, pants, shoes, hair_style, long_sleeves; GLint head_forward, head_up, chest_forward, waist_bend, face, expression; GLint stream, stream_kind, stream_width; - GLint garment, undress, top, jacket_tied, belly, wet; + GLint garment, undress, top, jacket_tied, belly, wet, hood; GLint cig_a, cig_tip, cig_glow, jacket, uri_a, uri_b, uri; GLint species, tail_a, tail_b; GLint view_pos, light_dir, light_color, shadow_color, sky_top, sky_horizon; @@ -132,6 +133,8 @@ void Renderer_endShadowPass(Renderer *r); void Renderer_drawFigure(Renderer *r, const FigurePose *pose); void Renderer_beginScenePass(Renderer *r, const Camera *camera, const Lighting *light); +/* Back to the scene pass after the sky (its settings still hold): what floats in the air. */ +void Renderer_beginAirPass(Renderer *r); /* Like Renderer_drawMesh, darkened by rgb and glowing like embers by glow (0..1). */ void Renderer_drawMeshTinted(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float glow); /* Like Renderer_drawMeshTinted, but no ink outline round it and nothing hides behind it (smoke). */ diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h index e5c1e47..e355756 100644 --- a/entropie/src/render/shapes.h +++ b/entropie/src/render/shapes.h @@ -140,7 +140,17 @@ Mesh Shapes_rain(const Vec3 *drops, int n, float len, Vec3 lean); #define CRATE_SIZE_X 0.40f #define CRATE_SIZE_Y 0.30f #define CRATE_SIZE_Z 0.30f -Mesh Shapes_crate(int variant, int bottles); +/* What's in each slot of a crate: 0 nothing, else a bottle - its kind + (CRATE_KIND) and how many sips are left in it (CRATE_SIPS, 0: an empty). */ +#define CRATE_SLOTS 20 +enum { BOTTLE_KIND_NONE, BOTTLE_KIND_BEER, BOTTLE_KIND_WATER, BOTTLE_KIND_COLA, BOTTLE_KIND_ORANGE, BOTTLE_KIND_MATE }; +#define CRATE_SLOT(kind, sips) ((unsigned char)(((kind) & 7) | ((sips) << 3))) +#define CRATE_KIND(code) ((code) & 7) +#define CRATE_SIPS(code) ((code) >> 3) +Mesh Shapes_crate(int variant, const unsigned char *slots, int n); +/* An umbrella along Y, centred, length long: folded (a crook at the bottom), or + open (the canopy over the top end, radius across). */ +Mesh Shapes_umbrella(float length, float radius, bool open, Vec3 color); /* Camping things (centred, lying along X): kind 0 a sleeping bag in its sack, 1 a foam mat rolled up, 2 a small tent in its bag, 3 a big one. */ Mesh Shapes_gear(int kind, Vec3 color); diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c index aace874..03bea0a 100644 --- a/entropie/src/render/shapes_props.c +++ b/entropie/src/render/shapes_props.c @@ -1019,15 +1019,17 @@ Mesh Shapes_rain(const Vec3 *drops, int n, float len, Vec3 lean) { return Mesh_fromBuilder(&b); } -/* A crate of bottles, centred: beer (brown bottles, gold caps, a red crate) - or water (green glass, blue caps, a blue crate); the first `bottles` slots full. */ -Mesh Shapes_crate(int variant, int bottles) { +/* A crate of bottles, centred: beer (brown bottles, gold caps, a red crate), + water (green glass, blue caps, a blue crate), soda (cola and orange, a green + crate) or mate (amber, an orange crate); each slot shows what's in it (a + bottle of another kind in its own colours, an empty one without its cap). */ +Mesh Shapes_crate(int variant, const unsigned char *slots, int n) { MeshBuilder b; MeshBuilder_init(&b); const float x = CRATE_SIZE_X, h = CRATE_SIZE_Y, z = CRATE_SIZE_Z, wall = 0.015f; static const Vec3 CRATE[4] = { { 0.62f, 0.16f, 0.12f }, { 0.22f, 0.42f, 0.72f }, { 0.20f, 0.48f, 0.28f }, { 0.90f, 0.62f, 0.15f } }; - static const Vec3 GLASS[4] = { { 0.38f, 0.22f, 0.08f }, { 0.55f, 0.75f, 0.62f }, { 0.20f, 0.11f, 0.07f }, { 0.55f, 0.33f, 0.10f } }; - static const Vec3 CAP[4] = { { 0.85f, 0.70f, 0.25f }, { 0.20f, 0.35f, 0.75f }, { 0.78f, 0.12f, 0.10f }, { 0.20f, 0.35f, 0.75f } }; + static const Vec3 GLASS[6] = { { 0, 0, 0 }, { 0.38f, 0.22f, 0.08f }, { 0.55f, 0.75f, 0.62f }, { 0.20f, 0.11f, 0.07f }, { 0.96f, 0.56f, 0.14f }, { 0.55f, 0.33f, 0.10f } }; + static const Vec3 CAP[6] = { { 0, 0, 0 }, { 0.85f, 0.70f, 0.25f }, { 0.20f, 0.35f, 0.75f }, { 0.78f, 0.12f, 0.10f }, { 0.95f, 0.50f, 0.15f }, { 0.20f, 0.35f, 0.75f } }; variant = variant < 0 ? 0 : variant > 3 ? 3 : variant; const Vec3 crate = CRATE[variant]; const Vec3 hole = vec3_scale(crate, 0.45f); @@ -1040,18 +1042,54 @@ Mesh Shapes_crate(int variant, int bottles) { addBlock(&b, vec3((float)s * (x * 0.5f + 0.001f), h * 0.32f, 0.0f), vec3(0.004f, 0.035f, 0.11f), hole); const bool big = variant == 1 || variant == 2; /* (water and soda: 12 big bottles; beer and mate: 20 small) */ const int cols = big ? 4 : 5, rows = big ? 3 : 4; - for (int k = 0; k < bottles && k < cols * rows; k++) { - Vec3 glass = GLASS[variant], cap = CAP[variant]; - if (variant == 2 && k % 2) { glass = (Vec3){ 0.96f, 0.56f, 0.14f }; cap = (Vec3){ 0.95f, 0.50f, 0.15f }; } /* (soda: cola and orange) */ + for (int k = 0; k < n && k < cols * rows; k++) { + int kind = CRATE_KIND(slots[k]); + if (kind < BOTTLE_KIND_BEER || kind > BOTTLE_KIND_MATE) continue; float cx = -x * 0.5f + x * ((float)(k % cols) + 0.5f) / (float)cols, cz = -z * 0.5f + z * ((float)(k / cols) + 0.5f) / (float)rows; size_t first = b.vertex_count; - shapes_addTube(&b, h * 0.5f - 0.07f, 0.024f, 0.012f, 0.1f, 6, glass, false); /* the neck out of the top */ - shapes_addTube(&b, h * 0.5f + 0.03f, 0.014f, 0.014f, 0.012f, 6, cap, true); + shapes_addTube(&b, h * 0.5f - 0.07f, 0.024f, 0.012f, 0.1f, 6, GLASS[kind], false); /* the neck out of the top */ + if (CRATE_SIPS(slots[k]) > 0) shapes_addTube(&b, h * 0.5f + 0.03f, 0.014f, 0.014f, 0.012f, 6, CAP[kind], true); + else shapes_addTube(&b, h * 0.5f + 0.03f, 0.013f, 0.012f, 0.004f, 6, vec3_scale(GLASS[kind], 0.6f), true); /* (an empty: the open mouth) */ transformSince(&b, first, mat4_translate(vec3(cx, 0.0f, cz))); } return Mesh_fromBuilder(&b); } +Mesh Shapes_umbrella(float length, float radius, bool open, Vec3 color) { + MeshBuilder b; + MeshBuilder_init(&b); + const float half = length * 0.5f; + const Vec3 shaft = { 0.12f, 0.11f, 0.10f }, handle = { 0.30f, 0.18f, 0.10f }; + shapes_addTube(&b, -half + 0.08f, 0.007f, 0.007f, length - 0.08f, 6, shaft, true); + for (int k = 0; k < 6; k++) { /* the crook: a little hook of a handle */ + float a0 = PI_F * (float)k / 6.0f, a1 = PI_F * (float)(k + 1) / 6.0f; + Vec3 p0 = vec3(0.035f - 0.035f * cosf(a0), -half + 0.08f - 0.035f * sinf(a0), 0.0f); + Vec3 p1 = vec3(0.035f - 0.035f * cosf(a1), -half + 0.08f - 0.035f * sinf(a1), 0.0f); + Vec3 mid = vec3_scale(vec3_add(p0, p1), 0.5f), d = vec3_sub(p1, p0); + addBlock(&b, mid, vec3(fabsf(d.x) + 0.016f, fabsf(d.y) + 0.016f, 0.016f), handle); + } + const int panels = 8; + if (open) { /* the canopy: panels from the tip down to the rim, both sides (seen from under it too) */ + Vec3 tip = vec3(0.0f, half + 0.02f, 0.0f); + for (int k = 0; k < panels; k++) { + float a0 = 2.0f * PI_F * (float)k / (float)panels, a1 = 2.0f * PI_F * (float)(k + 1) / (float)panels; + Vec3 r0 = vec3(cosf(a0) * radius, half - 0.2f * radius, sinf(a0) * radius); + Vec3 r1 = vec3(cosf(a1) * radius, half - 0.2f * radius, sinf(a1) * radius); + Vec3 c = k % 2 ? vec3_scale(color, 0.82f) : color; + Vec3 n = vec3_normalize(vec3_cross(vec3_sub(r1, tip), vec3_sub(r0, tip))); + GLuint t0 = MeshBuilder_vertex(&b, tip, n, c), i0 = MeshBuilder_vertex(&b, r0, n, c), i1 = MeshBuilder_vertex(&b, r1, n, c); + MeshBuilder_triangle(&b, t0, i1, i0); + Vec3 under = vec3_scale(c, 0.7f), m = vec3_scale(n, -1.0f); + GLuint u0 = MeshBuilder_vertex(&b, tip, m, under), u1 = MeshBuilder_vertex(&b, r0, m, under), u2 = MeshBuilder_vertex(&b, r1, m, under); + MeshBuilder_triangle(&b, u0, u1, u2); + } + } else { /* folded round the shaft */ + shapes_addTube(&b, -half + 0.25f * length, 0.012f, 0.038f, 0.45f * length, panels, color, false); + shapes_addTube(&b, -half + 0.70f * length, 0.038f, 0.008f, 0.27f * length, panels, color, false); + } + return Mesh_fromBuilder(&b); +} + /* Camping things, centred, lying along X (see shapes.h). */ Mesh Shapes_gear(int kind, Vec3 color) { MeshBuilder b; diff --git a/entropie/src/render/skybox.c b/entropie/src/render/skybox.c index 669e013..a56c750 100644 --- a/entropie/src/render/skybox.c +++ b/entropie/src/render/skybox.c @@ -25,6 +25,7 @@ bool Skybox_init(Skybox *sky) { sky->u_sky_top = glGetUniformLocation(sky->program, "skyTop"); sky->u_sky_horizon = glGetUniformLocation(sky->program, "skyHorizon"); sky->u_day_factor = glGetUniformLocation(sky->program, "dayFactor"); + sky->u_overcast = glGetUniformLocation(sky->program, "overcast"); glGenVertexArrays(1, &sky->vao); glGenBuffers(1, &sky->vbo); @@ -51,6 +52,7 @@ void Skybox_draw(const Skybox *sky, const Renderer *renderer, const Camera *came glUniformMatrix4fv(sky->u_projection, 1, GL_FALSE, camera->projection.m); glUniform1f(sky->u_sky_time, renderer->time); glUniform1f(sky->u_day_factor, light->day_factor); + glUniform1f(sky->u_overcast, light->overcast); setVec3(sky->u_sun_dir, light->sun_direction); setVec3(sky->u_light_color, light->light_color); setVec3(sky->u_sky_top, light->sky_top); diff --git a/entropie/src/render/skybox.h b/entropie/src/render/skybox.h index f771c49..057afff 100644 --- a/entropie/src/render/skybox.h +++ b/entropie/src/render/skybox.h @@ -1,7 +1,8 @@ #ifndef RENDER_SKYBOX_H #define RENDER_SKYBOX_H -/* Painted day/night sky with clouds, sun and moon. Draw after opaque geometry. */ +/* Painted day/night sky with clouds, sun and moon (clouding over with the weather). + Drawn after the solid things, before what floats in the air (rain, smoke). */ #include <stdbool.h> @@ -14,7 +15,7 @@ typedef struct { GLuint vbo; GLuint program; GLint u_view, u_projection, u_sky_time; - GLint u_sun_dir, u_light_color, u_sky_top, u_sky_horizon, u_day_factor; + GLint u_sun_dir, u_light_color, u_sky_top, u_sky_horizon, u_day_factor, u_overcast; } Skybox; bool Skybox_init(Skybox *sky); diff --git a/entropie/src/world/car.c b/entropie/src/world/car.c index 4c65cbf..6bd1d90 100644 --- a/entropie/src/world/car.c +++ b/entropie/src/world/car.c @@ -224,36 +224,150 @@ Vec3 Car_dropSpot(const World *world, const Object *item) { Vec3 Car_campDoor(void) { return Site_campDoor(); } -Object *Crate_takeBottle(World *world, Object *crate) { - if (crate->type != OBJ_CRATE || crate->stored - crate->crate_empties <= 0) return NULL; +#define EMPTY_KG 0.35f /* an empty bottle */ + +static int crateSlots(const Object *crate) { return Crate_bottles(crate->variant) < CRATE_SLOTS ? Crate_bottles(crate->variant) : CRATE_SLOTS; } +static bool bigKind(int kind) { return kind == BOTTLE_KIND_WATER || kind == BOTTLE_KIND_COLA || kind == BOTTLE_KIND_ORANGE; } + +void Crate_fill(Object *crate) { + static const int OWN[4] = { BOTTLE_KIND_BEER, BOTTLE_KIND_WATER, BOTTLE_KIND_COLA, BOTTLE_KIND_MATE }; + int v = crate->variant & 3; + memset(crate->crate_slot, 0, sizeof crate->crate_slot); + for (int k = 0; k < crateSlots(crate); k++) { + int kind = v == 2 ? (k % 2 ? BOTTLE_KIND_ORANGE : BOTTLE_KIND_COLA) : OWN[v]; /* (soda: cola and orange) */ + crate->crate_slot[k] = CRATE_SLOT(kind, BOTTLE_SIPS); + } + Crate_recount(crate); +} + +void Crate_recount(Object *crate) { + int n = 0, empties = 0; + float kg = 2.0f; + for (int k = 0; k < crateSlots(crate); k++) { + int code = crate->crate_slot[k], kind = CRATE_KIND(code); + if (!kind) continue; + n++; + if (CRATE_SIPS(code) == 0) { empties++; kg += EMPTY_KG; } + else kg += bigKind(kind) ? 1.2f : 0.85f; + } + crate->stored = n; + crate->crate_empties = empties; + crate->weight = kg; +} + +int Crate_look(const Object *crate) { + int h = 17; + for (int k = 0; k < CRATE_SLOTS; k++) h = h * 31 + crate->crate_slot[k]; + return h; +} + +int Crate_bottleKind(const Object *b) { + if (b->type == OBJ_BOTTLE) return BOTTLE_KIND_BEER; + if (b->type != OBJ_WATER) return BOTTLE_KIND_NONE; + static const int DRINK[4] = { BOTTLE_KIND_WATER, BOTTLE_KIND_COLA, BOTTLE_KIND_ORANGE, BOTTLE_KIND_MATE }; + return DRINK[b->variant & 3]; +} + +bool Crate_belongs(const Object *crate, int kind) { + switch (crate->variant & 3) { + case 0: return kind == BOTTLE_KIND_BEER; + case 1: return kind == BOTTLE_KIND_WATER; + case 2: return kind == BOTTLE_KIND_COLA || kind == BOTTLE_KIND_ORANGE; + default: return kind == BOTTLE_KIND_MATE; + } +} + +int Crate_wrong(const Object *crate) { + int n = 0; + for (int k = 0; k < crateSlots(crate); k++) { + int kind = CRATE_KIND(crate->crate_slot[k]); + if (kind && !Crate_belongs(crate, kind)) n++; + } + return n; +} + +/* The bottle in slot k, lying on top of the crate (the slot empty now). */ +static Object *outOfSlot(World *world, Object *crate, int k) { + int code = crate->crate_slot[k], kind = CRATE_KIND(code); Vec3 top = vec3_add(Object_colliderCenter(crate, 0), vec3(0.0f, CRATE_SIZE_Y * 0.5f + 0.16f, 0.0f)); - Object *b = Object_create(crate->variant ? OBJ_WATER : OBJ_BOTTLE, PALETTE_WARM, top, CRATE_BOTTLE, BOTTLE_KG); + Object *b = Object_create(kind == BOTTLE_KIND_BEER ? OBJ_BOTTLE : OBJ_WATER, PALETTE_WARM, top, CRATE_BOTTLE, BOTTLE_KG); if (!b) return NULL; - if (crate->variant >= 2) Object_setDrink(b, crate->variant == 3 ? 3 : 1 + ((crate->stored + 1) % 2)); /* (soda: cola and orange in turn) */ - if (!World_add(world, b)) return NULL; - crate->stored--; - crate->weight = fmaxf(2.0f, crate->weight - Crate_bottleKg(crate->variant)); + if (kind != BOTTLE_KIND_BEER) Object_setDrink(b, kind == BOTTLE_KIND_WATER ? 0 : kind == BOTTLE_KIND_COLA ? 1 : kind == BOTTLE_KIND_ORANGE ? 2 : 3); + b->sips = CRATE_SIPS(code); + if (!World_add(world, b)) { Object_destroy(b); return NULL; } + crate->crate_slot[k] = 0; + Crate_recount(crate); return b; } -#define EMPTY_KG 0.35f /* an empty bottle */ +Object *Crate_takeBottle(World *world, Object *crate) { + if (crate->type != OBJ_CRATE) return NULL; + int best = -1, best_score = -1; + for (int k = 0; k < crateSlots(crate); k++) { /* (its own, the fullest; else a full one of another kind) */ + int code = crate->crate_slot[k]; + if (!CRATE_KIND(code) || CRATE_SIPS(code) == 0) continue; + int score = CRATE_SIPS(code) + (Crate_belongs(crate, CRATE_KIND(code)) ? 100 : 0); + if (score > best_score) { best_score = score; best = k; } + } + return best >= 0 ? outOfSlot(world, crate, best) : NULL; +} + +int Crate_nextWrong(const Object *crate) { + for (int k = crateSlots(crate) - 1; k >= 0; k--) { + int kind = CRATE_KIND(crate->crate_slot[k]); + if (kind && !Crate_belongs(crate, kind)) return kind; + } + return BOTTLE_KIND_NONE; +} + +Object *Crate_takeWrong(World *world, Object *crate) { + if (crate->type != OBJ_CRATE) return NULL; + for (int k = crateSlots(crate) - 1; k >= 0; k--) { + int kind = CRATE_KIND(crate->crate_slot[k]); + if (kind && !Crate_belongs(crate, kind)) return outOfSlot(world, crate, k); + } + return NULL; +} bool Crate_putBottle(Object *crate, Object *bottle) { - if (!crate || crate->type != OBJ_CRATE || !bottle || !Object_isBottle(bottle) || bottle->broken) return false; - if (crate->stored >= Crate_bottles(crate->variant)) return false; - bool empty = bottle->sips <= 0; - crate->stored++; - if (empty) crate->crate_empties++; - crate->weight += empty ? EMPTY_KG : Crate_bottleKg(crate->variant); - Object_useUp(bottle); - return true; + if (!crate || crate->type != OBJ_CRATE || !bottle || bottle->broken) return false; + int kind = Crate_bottleKind(bottle); + if (!kind) return false; + for (int k = 0; k < crateSlots(crate); k++) { + if (crate->crate_slot[k]) continue; + crate->crate_slot[k] = CRATE_SLOT(kind, bottle->sips < 0 ? 0 : bottle->sips > 31 ? 31 : bottle->sips); + Crate_recount(crate); + Object_useUp(bottle); + return true; + } + return false; +} + +static bool hasRoom(const Object *c) { return c->type == OBJ_CRATE && !c->is_held && !c->broken && c->stored < crateSlots(c); } + +Object *Crate_rightFor(Object *const *objects, int count, int kind, Vec3 at, float range) { + Object *best = NULL; + for (int i = 0; i < count; i++) { + Object *c = objects[i]; + if (!hasRoom(c) || !Crate_belongs(c, kind)) continue; + float d = hypotf(c->position.x - at.x, c->position.z - at.z); + if (d < range) { range = d; best = c; } + } + return best; +} + +Object *Crate_forBottle(Object *const *objects, int count, const Object *bottle, Vec3 at, float range) { + int kind = Crate_bottleKind(bottle); + if (!kind) return NULL; + Object *own = Crate_rightFor(objects, count, kind, at, range); + return own ? own : Crate_withRoom(objects, count, at, range); } Object *Crate_withRoom(Object *const *objects, int count, Vec3 at, float range) { Object *best = NULL; for (int i = 0; i < count; i++) { Object *c = objects[i]; - if (c->type != OBJ_CRATE || c->is_held || c->broken || c->stored >= Crate_bottles(c->variant)) continue; + if (!hasRoom(c)) continue; float d = hypotf(c->position.x - at.x, c->position.z - at.z); if (d < range) { range = d; best = c; } } diff --git a/entropie/src/world/car.h b/entropie/src/world/car.h index 893f733..523c879 100644 --- a/entropie/src/world/car.h +++ b/entropie/src/world/car.h @@ -54,10 +54,31 @@ void Car_openBoot(Object *car, bool open); Vec3 Car_campDoor(void); Vec3 Car_dropSpot(const World *world, const Object *item); -/* A bottle out of a crate (on top of it, for the taking): NULL if it's empty. */ -Object *Crate_takeBottle(World *world, Object *crate); /* a full one (NULL: none, or only empties) */ -/* A bottle (empty or not) back into a crate with room: true if it went in. */ +/* Crates know what's in each slot (Object.crate_slot): their own bottles, + full or empty, and bottles of other kinds put back into the wrong crate + (they have to be sorted before the crates go back to the shop). */ +/* Full of its own bottles (a new crate); and stored, crate_empties, weight from the slots. */ +void Crate_fill(Object *crate); +void Crate_recount(Object *crate); +/* A slot's worth for the look (its mesh is built again when this changes). */ +int Crate_look(const Object *crate); +/* Which kind of bottle (BOTTLE_KIND_*: one that comes in crates), else BOTTLE_KIND_NONE. */ +int Crate_bottleKind(const Object *bottle); +/* Does a bottle of that kind belong in this crate? */ +bool Crate_belongs(const Object *crate, int kind); +/* How many bottles of other kinds are in it; the kind Crate_takeWrong takes out next (or none). */ +int Crate_wrong(const Object *crate); +int Crate_nextWrong(const Object *crate); +/* A bottle to drink out of a crate (its own first, else any full one), lying on top of it: NULL if none. */ +Object *Crate_takeBottle(World *world, Object *crate); +/* A bottle that doesn't belong in it, out (full or empty): NULL if none. */ +Object *Crate_takeWrong(World *world, Object *crate); +/* A bottle (empty or not, as it is) into a crate with room: true if it went in. */ bool Crate_putBottle(Object *crate, Object *bottle); +/* The crate a bottle goes into within range (not carried, room in it): its own kind first, else any. */ +Object *Crate_forBottle(Object *const *objects, int count, const Object *bottle, Vec3 at, float range); +/* A crate where a bottle of that kind belongs, with room, within range (nearest), or NULL. */ +Object *Crate_rightFor(Object *const *objects, int count, int kind, Vec3 at, float range); /* The nearest crate within range with room for one more (not carried), or NULL. */ Object *Crate_withRoom(Object *const *objects, int count, Vec3 at, float range); /* Every frame: whoever asked for a bottle from a crate or the next thing from diff --git a/entropie/src/world/forestry.c b/entropie/src/world/forestry.c index a011d0e..2014506 100644 --- a/entropie/src/world/forestry.c +++ b/entropie/src/world/forestry.c @@ -296,6 +296,7 @@ void Forestry_update(World *world, float dt) { Vec3 rotation = tree->rotation, position = tree->position; Object_advanceTopple(tree, dt); /* (cut above the ground: it slides off the stump and comes down, see fell) */ + if (tree->topple >= 1.0f) World_noise(world, NOISE_CRASH, Object_colliderCenter(tree, 0), 1.0f); /* (down: the crash) */ Object *stopped = NULL; for (int k = 0; k < world->count; k++) { diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c index 6b8eaae..f4821b4 100644 --- a/entropie/src/world/object.c +++ b/entropie/src/world/object.c @@ -10,7 +10,6 @@ #define ROOF_PITCH (5.0f / 12.0f) /* roof height per building width */ #define ROOF_THICKNESS 0.4f /* roof collider slab, below the surface */ #define HALL_PITCH 0.14f /* farm halls have shallow roofs */ -#define BOTTLE_SIPS 5 #define BEER_PER_SIP 0.07f #define VODKA_SIPS 8 #define VODKA_PER_SIP 0.16f @@ -366,11 +365,10 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size case OBJ_CRATE: obj->variant = (int)(weight + 0.5f) < 0 ? 0 : (int)(weight + 0.5f) > 3 ? 3 : (int)(weight + 0.5f); - obj->stored = Crate_bottles(obj->variant); obj->size = vec3(CRATE_SIZE_X, CRATE_SIZE_Y, CRATE_SIZE_Z); - obj->weight = 2.0f + (float)obj->stored * Crate_bottleKg(obj->variant); - obj->mesh = Shapes_crate(obj->variant, obj->stored); - obj->shown = obj->stored; + Crate_fill(obj); /* (full of its own bottles: stored, weight) */ + obj->mesh = Shapes_crate(obj->variant, obj->crate_slot, Crate_bottles(obj->variant)); + obj->shown = Crate_look(obj); obj->pivot = vec3(0.0f, CRATE_SIZE_Y * 0.5f, 0.0f); addCollider(obj, vec3_zero(), obj->size, vec3_zero()); break; @@ -387,6 +385,17 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size break; } + case OBJ_UMBRELLA: { + static const Vec3 COLORS[] = { { 0.15f, 0.20f, 0.42f }, { 0.70f, 0.14f, 0.16f }, { 0.20f, 0.32f, 0.22f } }; + obj->size = vec3(0.08f, UMBRELLA_LENGTH, 0.08f); + obj->weight = 0.5f; + obj->variant = (int)weight % 3; + obj->mesh = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, false, COLORS[obj->variant]); + obj->pivot = vec3(0.0f, UMBRELLA_LENGTH * 0.5f, 0.0f); + addCollider(obj, vec3_zero(), obj->size, vec3_zero()); + break; + } + case OBJ_TENT: { /* (see tent.c: its mesh by how far it's up) */ obj->variant = weight > 0.5f ? 1 : 0; obj->weight = 50.0f; @@ -797,13 +806,122 @@ static float freeAlong(const Object *who, const Object *seat, Vec3 c, Vec3 axis, for (int i = 0; i < g_people_n && clear; i++) { const Object *o = g_people[i]; if (o == who || !o->is_character || o->seat != seat || !o->sitting) continue; - clear = fabsf(vec3_dot(vec3_sub(o->sit_at, c), axis) - cand) >= SEAT_SPACE; + clear = fabsf(vec3_dot(vec3_sub(o->sit_at, c), axis) - cand) >= SEAT_SPACE - 0.06f; /* (a little give: moved up exactly that far) */ } if (clear) return cand; } return NAN; } +/* A bench or a log as a line to sit along: its middle, which way it runs, how far either way. */ +static bool seatLine(const Object *seat, Vec3 *c, Vec3 *axis, float *half) { + if (!seat || seat->collider_count == 0) return false; + if (seat->type == OBJ_LOG) { + *c = Object_colliderCenter(seat, 0); + *axis = seat->colliders[0].axes[1]; + *half = seat->size.y * 0.35f; + } else if (seat->type == OBJ_FURNITURE && seat->variant == FURN_BENCH) { + *c = seat->position; + Vec3 a = seat->colliders[0].axes[0]; + *axis = vec3_normalize(vec3(a.x, 0.0f, a.z)); + *half = fmaxf(seat->colliders[0].half_extents.x - 0.28f, 0.0f); + } else return false; + return true; +} + +/* Moving up on a bench: everybody on it in order along it, the newcomer at + `want`; spread out till each has SEAT_SPACE, as little moved as can be. + Who won't move (you, the unwilling unless asked, sleepers) stays put. + Fills pos (the newcomer's at index *mine); false: no room even so. */ +#define ROOM_MOST 6 +static bool spreadOut(const Object *who, const Object *seat, Vec3 c, Vec3 axis, float half, float want, bool asked, + const Object **people, float *pos, float *from, int *n_out, int *mine) { + int n = 0; + for (int i = 0; i < g_people_n && n < ROOM_MOST - 1; i++) { + const Object *o = g_people[i]; + if (o == who || !o->is_character || o->seat != seat || !o->sitting) continue; + people[n] = o; + from[n] = vec3_dot(vec3_sub(o->sit_at, c), axis); + n++; + } + people[n] = who; + from[n] = clampf(want, -half, half); + n++; + for (int a = 1; a < n; a++) /* (in order along it) */ + for (int b = a; b > 0 && from[b] < from[b - 1]; b--) { + float t = from[b]; from[b] = from[b - 1]; from[b - 1] = t; + const Object *q = people[b]; people[b] = people[b - 1]; people[b - 1] = q; + } + bool fixed[ROOM_MOST]; + for (int k = 0; k < n; k++) { + const Object *o = people[k]; + pos[k] = from[k]; + if (o == who) *mine = k; + fixed[k] = o != who && (o->type == OBJ_PLAYER || o->body.asleep || o->sit_moving || + (!asked && (o->ai.character.willing < 0.35f || o->ai.annoyed > 0.3f))); + } + for (int pass = 0; pass < 40; pass++) { + for (int k = 1; k < n; k++) { + float gap = pos[k] - pos[k - 1]; + if (gap >= SEAT_SPACE) continue; + float d = SEAT_SPACE - gap; + if (fixed[k] && fixed[k - 1]) continue; + if (fixed[k]) pos[k - 1] -= d; + else if (fixed[k - 1]) pos[k] += d; + else { pos[k - 1] -= 0.5f * d; pos[k] += 0.5f * d; } + } + for (int k = 0; k < n; k++) if (!fixed[k]) pos[k] = clampf(pos[k], -half, half); + } + for (int k = 0; k < n; k++) { + if (pos[k] < -half - 0.01f || pos[k] > half + 0.01f) return false; + if (k > 0 && pos[k] - pos[k - 1] < SEAT_SPACE - 0.01f) return false; + } + *n_out = n; + return true; +} + +int Object_benchRoom(const Object *who, const Object *seat, Vec3 near, bool asked) { + Vec3 c, axis; + float half; + if (!seatLine(seat, &c, &axis, &half)) return ROOM_NONE; + float want = clampf(vec3_dot(vec3_sub(near, c), axis), -half, half); + if (!isnan(freeAlong(who, seat, c, axis, want, half))) return ROOM_FREE; + const Object *people[ROOM_MOST]; + float pos[ROOM_MOST], from[ROOM_MOST]; + int n = 0, mine = 0; + return spreadOut(who, seat, c, axis, half, want, asked, people, pos, from, &n, &mine) ? ROOM_SCOOT : ROOM_NONE; +} + +bool Object_benchMakeRoom(const Object *who, Object *seat, Vec3 near, bool asked) { + Vec3 c, axis; + float half; + if (!seatLine(seat, &c, &axis, &half)) return false; + float want = clampf(vec3_dot(vec3_sub(near, c), axis), -half, half); + if (!isnan(freeAlong(who, seat, c, axis, want, half))) return true; + const Object *people[ROOM_MOST]; + float pos[ROOM_MOST], from[ROOM_MOST]; + int n = 0, mine = 0; + if (!spreadOut(who, seat, c, axis, half, want, asked, people, pos, from, &n, &mine)) return false; + for (int k = 0; k < n; k++) { + if (k == mine || fabsf(pos[k] - from[k]) < 0.01f) continue; + Object *o = (Object *)people[k]; /* (one of g_people: the world's own) */ + o->sit_at = vec3_add(o->sit_at, vec3_scale(axis, pos[k] - from[k])); + o->sit_scoot = true; + } + return true; +} + +#define SCOOT_SPEED 0.35f /* m/s sliding along a bench, sitting */ +void Object_scootOnSeat(Object *who, float dt) { + if (!who->sit_scoot) return; + if (!who->sitting || who->sit_moving) { who->sit_scoot = false; return; } + Vec3 to = vec3(who->sit_at.x - who->position.x, 0.0f, who->sit_at.z - who->position.z); + float d = vec3_length(to), step = SCOOT_SPEED * dt; + if (d <= step) { who->position.x = who->sit_at.x; who->position.z = who->sit_at.z; who->sit_scoot = false; } + else { who->position.x += to.x / d * step; who->position.z += to.z / d * step; } + Object_syncColliders(who); +} + static bool seatTaken(const Object *who, const Object *seat) { for (int i = 0; i < g_people_n; i++) if (g_people[i] != who && g_people[i]->is_character && g_people[i]->seat == seat && g_people[i]->sitting) return true; @@ -825,7 +943,9 @@ bool Object_sit(Object *who, Object *seat, Vec3 toward) { Vec3 rel = vec3_sub(who->position, c); Vec3 across = vec3_normalize(vec3(-axis.z, 0.0f, axis.x)); if (vec3_dot(across, rel) < 0.0f) across = vec3_scale(across, -1.0f); - float along = freeAlong(who, seat, c, axis, clampf(vec3_dot(rel, axis), -seat->size.y * 0.35f, seat->size.y * 0.35f), seat->size.y * 0.35f); + float want = clampf(vec3_dot(rel, axis), -seat->size.y * 0.35f, seat->size.y * 0.35f); + float along = freeAlong(who, seat, c, axis, want, seat->size.y * 0.35f); + if (isnan(along) && Object_benchMakeRoom(who, seat, who->position, false)) along = freeAlong(who, seat, c, axis, want, seat->size.y * 0.35f); /* (the others move up) */ if (isnan(along)) return noRoom(who); /* (somebody's there already: no sitting on top of them) */ Vec3 at = vec3_add(vec3_add(c, vec3_scale(axis, along)), vec3_scale(across, SEAT_BACK - 0.02f)); /* the bottom on top of it */ who->sit_at = vec3(at.x, who->position.y, at.z); @@ -855,8 +975,9 @@ bool Object_sit(Object *who, Object *seat, Vec3 toward) { Vec3 along_axis = seat->colliders[0].axes[0]; along_axis = vec3_normalize(vec3(along_axis.x, 0.0f, along_axis.z)); float half = fmaxf(seat->colliders[0].half_extents.x - 0.28f, 0.0f); - float along = freeAlong(who, seat, seat->position, along_axis, - clampf(vec3_dot(vec3_sub(who->position, seat->position), along_axis), -half, half), half); + float want = clampf(vec3_dot(vec3_sub(who->position, seat->position), along_axis), -half, half); + float along = freeAlong(who, seat, seat->position, along_axis, want, half); + if (isnan(along) && Object_benchMakeRoom(who, seat, who->position, false)) along = freeAlong(who, seat, seat->position, along_axis, want, half); /* (the others move up) */ if (isnan(along)) return noRoom(who); base = vec3_add(base, vec3_scale(along_axis, along)); } @@ -1246,6 +1367,14 @@ static const char *bottleName(const Object *b) { return b->type == OBJ_VODKA ? "Vodka" : b->type == OBJ_WATER ? DRINK[b->variant & 3] : "Beer"; } +void Object_showUmbrella(Object *u) { + static const Vec3 COLORS[] = { { 0.15f, 0.20f, 0.42f }, { 0.70f, 0.14f, 0.16f }, { 0.20f, 0.32f, 0.22f } }; + if (u->type != OBJ_UMBRELLA || u->shown == (u->umbrella_open ? 1 : 0)) return; + Mesh_destroy(&u->mesh); + u->mesh = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, u->umbrella_open, COLORS[u->variant % 3]); + u->shown = u->umbrella_open ? 1 : 0; +} + void Object_setDrink(Object *b, int drink) { static const BottleStyle STYLE[] = { BOTTLE_WATER, BOTTLE_COLA, BOTTLE_ORANGE, BOTTLE_MATE }; if (b->type != OBJ_WATER) return; @@ -1341,11 +1470,22 @@ void Object_describe(const Object *obj, char *out, size_t size) { else snprintf(out, size, "Volkram's car - left click at the boot: take out %s", Car_nextName(obj)); break; case OBJ_CRATE: - if (obj->crate_empties > 0) - snprintf(out, size, "Crate of %s - %d full, %d empt%s%s", Crate_name(obj->variant), obj->stored - obj->crate_empties, obj->crate_empties, - obj->crate_empties == 1 ? "y" : "ies", obj->stored > obj->crate_empties && !obj->is_held ? " (left click: take one)" : ""); - else snprintf(out, size, "Crate of %s - %d bottle%s left%s", Crate_name(obj->variant), obj->stored, obj->stored == 1 ? "" : "s", - obj->stored > 0 && !obj->is_held ? " (left click: take one)" : ""); + { /* its own bottles, full and empty; and what doesn't belong in it (right click takes that out) */ + int full = 0, empty = 0, wrong = 0; + for (int k = 0; k < CRATE_SLOTS; k++) { + int kind = CRATE_KIND(obj->crate_slot[k]); + if (!kind) continue; + if (!Crate_belongs(obj, kind)) wrong++; + else if (CRATE_SIPS(obj->crate_slot[k]) > 0) full++; + else empty++; + } + char extra[64] = ""; + if (wrong > 0) snprintf(extra, sizeof extra, ", %d not %s%s", wrong, Crate_name(obj->variant), obj->is_held ? "" : " (right click: out)"); + if (empty > 0) snprintf(out, size, "Crate of %s - %d full, %d empt%s%s%s", Crate_name(obj->variant), full, empty, empty == 1 ? "y" : "ies", extra, + obj->stored > obj->crate_empties && !obj->is_held ? " (left click: take one)" : ""); + else snprintf(out, size, "Crate of %s - %d bottle%s left%s%s", Crate_name(obj->variant), full, full == 1 ? "" : "s", extra, + obj->stored > 0 && !obj->is_held ? " (left click: take one)" : ""); + } break; case OBJ_TENT: { static const char *const STAGE[] = { "spread out - left click: put the poles in", "the poles going in - keep clicking", @@ -1355,6 +1495,9 @@ void Object_describe(const Object *obj, char *out, size_t size) { else snprintf(out, size, "%s's tent - %s", obj->owner ? obj->owner->name : "Somebody", STAGE[obj->tent_stage % 5]); break; } + case OBJ_UMBRELLA: + snprintf(out, size, obj->umbrella_open ? "Umbrella - open" : "Umbrella (carried, it opens in the rain)"); + break; case OBJ_GEAR: { static const char *const WHAT[] = { "sleeping bag", "sleeping mat", "tent", "tent (the big one)" }; if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s", WHAT[obj->variant & 3]); diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h index ef97c8f..5181084 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -87,6 +87,7 @@ typedef enum { OBJ_CRATE, /* a crate of bottles: weight 0 = beer (CRATE_BEER bottles), 1 = water (CRATE_WATER); stored = bottles in it */ OBJ_GEAR, /* camping things: variant = GearKind (see car.h), owner = whose */ OBJ_TENT, /* static: a dome tent, weight 0 small / 1 big (see tent.h); stored = pegs in */ + OBJ_UMBRELLA, /* an umbrella: size.y long (folded); opens held in the rain (umbrella_open); variant = colour */ } ObjectType; /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */ @@ -109,6 +110,9 @@ typedef struct { #define OBJECT_MAX_COLLIDERS 3 +#define BOTTLE_SIPS 5 /* a full bottle of beer, water, soda or mate */ +#define UMBRELLA_LENGTH 0.9f /* m, folded */ +#define UMBRELLA_RADIUS 0.55f /* m across the open canopy */ #define POCKET_URINELLAS 4 /* disposable Urinellas a pocket holds (one-time use) */ #define LIGHTER_USES 60 /* a full lighter lights this many times */ #define POUCH_ROLLUPS 75 /* a pouch of tobacco rolls about this many */ @@ -180,7 +184,9 @@ typedef struct Object { float indoors; /* characters: inside the house this many more seconds (hidden: see ai.c goInside) */ struct Object *owner; /* camping gear, tents: whose ... */ struct Object *owner2; /* ... tents: and who shares it */ - int crate_empties; /* crates: of the bottles in it (stored), this many are empties put back */ + int crate_empties; /* crates: of the bottles in it (stored), this many are empties put back ... */ + unsigned char crate_slot[CRATE_SLOTS]; + bool umbrella_open; /* umbrellas: open (held up in the rain) */ /* ... and what's in each slot (CRATE_SLOT: kind and sips; see car.c Crate_*) */ int tent_stage; /* tents: TentStage ... */ float tent_work; /* ... how far the stage is (0..1) ... */ float tent_idle; /* ... seconds since anybody worked on it */ @@ -271,6 +277,8 @@ typedef struct Object { float sit_yaw; /* ... (as rotation.y) */ bool sit_moving; bool sit_backing; /* ... turned round, stepping back to the seat */ + bool sit_scoot; + int heard_pose; /* characters: the body's pose last frame (going down: a thud) */ /* ... sitting, sliding along the bench to sit_at (moving up for somebody) */ float rest_time; /* seconds lying still (physics looks less often) */ bool settled; /* ... and turned to lie on the ground under it (see physics: settleOnGround) */ bool climbing; /* characters: pulling themselves up a ledge (no physics meanwhile) */ @@ -464,6 +472,15 @@ Object *Object_createFurniture(FurnitureKind kind, Vec3 position, Vec3 size, flo /* Sits down (on seat, or the ground with NULL) facing toward. False: no room there (somebody sits on that chair, or that spot of the bench or log). */ bool Object_sit(Object *who, Object *seat, Vec3 toward); +/* Room on a bench or a log for who, as near `near` as can be: ROOM_FREE (there is), + ROOM_SCOOT (the others there would move up for it), ROOM_NONE. asked: the + unwilling move up too (they were asked); you never move for anybody. */ +enum { ROOM_NONE, ROOM_FREE, ROOM_SCOOT }; +int Object_benchRoom(const Object *who, const Object *seat, Vec3 near, bool asked); +/* ... and they do move up (sliding along it): true if there's room now. */ +bool Object_benchMakeRoom(const Object *who, Object *seat, Vec3 near, bool asked); +/* Sitting, sliding along the bench (moving up): every frame. */ +void Object_scootOnSeat(Object *who, float dt); /* Who's about (the world each frame): Object_sit keeps people apart. */ void Object_seePeople(Object *const *objects, int count); /* Sitting down on a seat: steps over and turns round first (true meanwhile). */ @@ -477,6 +494,8 @@ void Object_slideChair(Object *chair, float dt); Object *Object_seatNear(Object *const *objects, int count, Vec3 pos, float reach); /* Houses: the front door at x (meters along the ridge from the middle). */ void Object_setDoor(Object *obj, float x); +/* Umbrellas: the mesh open or folded, as umbrella_open says (when that changed). */ +void Object_showUmbrella(Object *umbrella); /* Garbage bags: a new mesh for how full it is. */ void Object_fillBag(Object *bag, float added_weight); diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c index 4d40091..de04371 100644 --- a/entropie/src/world/scene.c +++ b/entropie/src/world/scene.c @@ -242,8 +242,10 @@ Object *Scene_load(World *world) { obj->look = Figure_wardrobe(characters); if (characters < (int)(sizeof(LOOKS) / sizeof(LOOKS[0]))) { /* (their own clothes; extra people: the wardrobe) */ Vec3 jacket = obj->look.jacket; + bool hoodie = obj->look.hoodie; obj->look = LOOKS[characters]; obj->look.jacket = jacket; + obj->look.hoodie = hoodie; } obj->look.garment = GARMENT[characters % TEMPERAMENT_COUNT]; Object_setPerson(obj, NAMES[characters % TEMPERAMENT_COUNT], MAIN_HAND[characters % TEMPERAMENT_COUNT]); diff --git a/entropie/src/world/weather.c b/entropie/src/world/weather.c index 5114316..75c4cb7 100644 --- a/entropie/src/world/weather.c +++ b/entropie/src/world/weather.c @@ -12,9 +12,11 @@ #define RAIN_EASE 150.0f /* game s: it sets in / eases off over about this */ #define SOAK_TIME (25.0f * 60.0f) /* game s of hard rain: everything soaked */ #define DRY_TIME (3.0f * 3600.0f) /* ... and dry again (in the day; slower at night) */ -#define RAIN_MM_HOUR 7.0f /* mm an hour in hard rain */ +#define RAIN_MM_HOUR 9.0f /* mm an hour in hard rain */ #define POUR_RADIUS 16.0f /* m round you the ground gets the water (puddles, mud) */ #define POURS 8 /* ... in this many splashes a frame */ +#define PEOPLE_RADIUS 6.0f /* m round anybody else out in it ... */ +#define PEOPLE_POURS 2 /* ... in this many */ #define DROP_RADIUS 9.0f /* m round the camera the drops fall */ #define DROP_SPEED 8.5f #define DROP_LEN 0.6f @@ -139,6 +141,20 @@ void Weather_update(World *world, float dt, float game_dt) { p.y = Terrain_height(&world->terrain, p.x, p.z); if (!Weather_covered(world, p)) Wetness_pour(&world->terrain, p.x, p.z, liters, 2.2f, 0.0f); } + /* ... and round everybody else out in it: where they walk (the paths) gets soaked too, and turns to mud */ + float near_liters = RAIN_MM_HOUR * world->rain / 3600.0f * game_dt * PI_F * PEOPLE_RADIUS * PEOPLE_RADIUS / (float)PEOPLE_POURS; + for (int i = 0; i < world->count && world->has_terrain; i++) { + const Object *o = world->objects[i]; + if (!o->is_character || o->indoors > 0.0f || o->in_tent || o->riding) continue; + float ex = o->position.x - world->eye.x, ez = o->position.z - world->eye.z; + if (ex * ex + ez * ez < (POUR_RADIUS - PEOPLE_RADIUS) * (POUR_RADIUS - PEOPLE_RADIUS)) continue; /* (in the camera's patch already) */ + for (int k = 0; k < PEOPLE_POURS; k++) { + float a = dice(&d->rng) * 2.0f * PI_F, r = PEOPLE_RADIUS * sqrtf(dice(&d->rng)); + Vec3 p = vec3_add(o->position, vec3(cosf(a) * r, 0.0f, sinf(a) * r)); + p.y = Terrain_height(&world->terrain, p.x, p.z); + if (!Weather_covered(world, p)) Wetness_pour(&world->terrain, p.x, p.z, near_liters, 2.2f, 0.0f); + } + } /* a fire burns down faster in it */ for (int i = 0; i < world->count; i++) { Object *f = world->objects[i]; diff --git a/entropie/src/world/wetness.c b/entropie/src/world/wetness.c index cbc4a65..735a265 100644 --- a/entropie/src/world/wetness.c +++ b/entropie/src/world/wetness.c @@ -395,6 +395,12 @@ void Wetness_sync(Wetness *w) { /* ---------- Queries ---------- */ +float Wetness_water(const Wetness *w, float x, float z) { + int i; + const WetChunk *c = cellAtConst(w, x, z, &i); + return c ? c->water[i] : 0.0f; +} + float Wetness_mud(const Wetness *w, float x, float z) { int i; const WetChunk *c = cellAtConst(w, x, z, &i); diff --git a/entropie/src/world/wetness.h b/entropie/src/world/wetness.h index 4d6e073..cb40d00 100644 --- a/entropie/src/world/wetness.h +++ b/entropie/src/world/wetness.h @@ -63,6 +63,8 @@ void Wetness_sync(Wetness *w); /* How muddy (0..1). */ float Wetness_mud(const Wetness *w, float x, float z); +/* Standing water here (mm). */ +float Wetness_water(const Wetness *w, float x, float z); /* How soft and wet for trampling (0..1): wet soil that can turn to mud. */ float Wetness_softness(const Wetness *w, float x, float z); /* The strongest pee smell within radius (0..1). */ diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c index 281bef5..437e0e6 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -522,6 +522,7 @@ static Vec3 closestPoint(const Object *o, Vec3 p) { static void snapBranch(World *world, Object *branch) { float half = branch->size.y * 0.5f; if (half < 0.25f) return; + World_noise(world, NOISE_SNAP, branch->position, 0.4f + half); Vec3 center = Object_colliderCenter(branch, 0), along = branch->colliders[0].axes[1]; for (int k = -1; k <= 1; k += 2) { Vec3 at = vec3_add(center, vec3_scale(along, (float)k * half * 0.55f)); @@ -557,12 +558,30 @@ static void scraped(World *world, Object *who, Vec3 facing, float dt) { if (dice(world) < 0.4f * run * unsure(who)) Body_trip(&who->body, 0, facing, clampf((0.25f + 0.3f * run) * (0.7f + 0.3f * unsure(who)), 0.0f, 1.0f)); } +/* What a foot comes down on (for its sound). */ +static int stepSurface(const World *world, const Object *who, Vec3 sole) { + const Object *on = who->standing_on; + if (on && sole.y > Terrain_height(&world->terrain, sole.x, sole.z) + 0.05f) { + if (on->type == OBJ_LOG || on->type == OBJ_FIREWOOD || on->type == OBJ_LADDER || on->type == OBJ_FURNITURE || on->type == OBJ_TREE) return STEP_WOOD; + return STEP_HARD; /* (floors, steps, walls, crates, roofs) */ + } + if (Wetness_mud(&world->terrain.wet, sole.x, sole.z) > 0.35f) return STEP_MUD; + if (Wetness_water(&world->terrain.wet, sole.x, sole.z) > 3.0f) return STEP_WATER; + SiteSurface s = Site_surface(sole.x, sole.z); + if (s.asphalt + s.concrete > 0.5f) return STEP_HARD; + if (s.gravel > 0.5f) return STEP_GRAVEL; + if (s.earth > 0.5f || Site_grass(sole.x, sole.z) < 0.3f) return STEP_EARTH; + return STEP_GRASS; +} + static void stepOn(World *world, Object *who, int foot, Vec3 ankle) { const float s = who->look.height / 1.75f; Vec3 sole = vec3_sub(ankle, vec3(0.0f, 0.075f * s, 0.0f)); Vec3 facing = Object_facing(who); float speed = vec3_length(vec3(who->velocity.x + who->movement.x, 0.0f, who->velocity.z + who->movement.z)); speed = fmaxf(speed, who->ground_speed); + World_noiseOn(world, NOISE_STEP, sole, clampf(0.25f + 0.12f * speed, 0.2f, 1.0f) * clampf(who->weight / 75.0f, 0.4f, 1.3f), + who->on_ladder ? STEP_WOOD : stepSurface(world, who, sole)); if (!who->on_ladder && sole.y < Terrain_height(&world->terrain, sole.x, sole.z) + 0.1f) { float mud = Wetness_step(&world->terrain.wet, sole.x, sole.z); who->dirt = fminf(1.0f, who->dirt + 0.015f * mud); @@ -601,6 +620,7 @@ static void stepOn(World *world, Object *who, int foot, Vec3 ankle) { /* on grass it just rolls away; on hard ground a heavy step may crack it */ if (Site_hard(o->position.x, o->position.z) && dice(world) < 0.08f + 0.04f * speed) { Object_shatter(o, Object_bottom(o)); + World_noise(world, NOISE_SMASH, o->position, 0.6f); trip = 0.5f; } else { Object_topple(o, facing); o->velocity = vec3_add(o->velocity, kick); o->thrown_by = who; o->thrown_time = 0.0f; } @@ -746,14 +766,29 @@ static void clothesWet(World *world, Object *o, bool by_fire, bool night) { on; by the fire, in the day (or warm with drink) it comes off again (round the waist). Put down somewhere: people go and get it, you are told where it is. You: put on for you once a cold spell (then it's up to you, B). */ +/* An umbrella in hand: open while it rains (out in it), folded otherwise. Is one open over them? */ +static bool underUmbrella(World *world, Object *o) { + bool over = false; + for (int h = 0; h < 2; h++) { + Object *u = o->hands.item[h]; + if (!u || u->type != OBJ_UMBRELLA || o->hands.state[h] != HAND_HOLDING) continue; + u->umbrella_open = world->rain > 0.05f && !o->in_tent && o->indoors <= 0.0f && !o->riding && + !Weather_covered(world, vec3_add(o->position, vec3(0.0f, o->look.height + 0.3f, 0.0f))); + over = over || u->umbrella_open; + } + return over; +} + static void dressForNight(World *world, Object *o, float dt) { if (o->type == OBJ_DOG) return; + bool umbrella = underUmbrella(world, o); bool night = world->hour >= 21.0f || world->hour < 7.0f; bool by_fire = Camp_fireNear(world->objects, world->count, o->position, 4.0f, true) != NULL; float cold = !night ? -2.0f : by_fire || (o->in_tent && Tent_hasBed(o->in_tent, o)) ? -1.0f : 1.0f - 0.8f * fminf(o->drunk, 1.0f); if (world->rain > 0.2f && !o->in_tent && o->indoors <= 0.0f && !o->riding && ((int)(world->hour * 3600.0f) + (int)(o->position.x * 7.0f)) % 30 == 0) o->rain_out = !Weather_covered(world, vec3_add(o->position, vec3(0.0f, o->look.height, 0.0f))); /* (out in it: now and then looked up) */ if (world->rain <= 0.2f || o->in_tent || o->indoors > 0.0f) o->rain_out = false; + if (umbrella) o->rain_out = false; /* (dry under it) */ clothesWet(world, o, by_fire, night); if (o->rain_out) cold = fmaxf(cold, 0.0f) + 1.2f * world->rain; /* (out in it: cold, even in the day) */ else if (o->look.wet > 0.2f && !by_fire) cold = fmaxf(cold, 0.0f) + 0.8f * o->look.wet; /* (in wet clothes: cold too) */ @@ -773,6 +808,14 @@ static void dressForNight(World *world, Object *o, float dt) { o->look.top = TOP_SHIRT; /* (off, and along with them: round the waist) */ } if (o->chill < 0.3f) o->jacket_told = false; + /* out in the rain with a hood to it: the jacket on, the hood up (none under an umbrella) */ + if (o->rain_out && o->look.hoodie && !umbrella) { + if (o->look.top != TOP_JACKET && !o->pullover_down && o->look.top != TOP_BARE) { + Clothes_takeBack(o, true); + if (o->type != OBJ_PLAYER && o->ai.speech_time <= 0.0f && worldDice(world) < 0.3f) Ai_say(o, "Hood up."); + } + o->look.hood_up = o->look.top == TOP_JACKET; + } else if (!o->rain_out) o->look.hood_up = false; o->look.jacket_tied = !o->pullover_down && o->look.top != TOP_JACKET; /* (off and with them) */ } @@ -824,10 +867,15 @@ static float scootTo(const World *world, const Object *chair) { static void showProps(World *world) { for (int i = 0; i < world->count; i++) { Object *f = world->objects[i]; - if (f->type == OBJ_CRATE && f->shown != f->stored) { /* (a bottle taken: one fewer shows) */ + if (f->type == OBJ_UMBRELLA) { /* (folded once it's put down) */ + if (!f->is_held) f->umbrella_open = false; + Object_showUmbrella(f); + continue; + } + if (f->type == OBJ_CRATE && f->shown != Crate_look(f)) { /* (a bottle taken or put back: it shows) */ Mesh_destroy(&f->mesh); - f->mesh = Shapes_crate(f->variant, f->stored); - f->shown = f->stored; + f->mesh = Shapes_crate(f->variant, f->crate_slot, Crate_bottles(f->variant)); + f->shown = Crate_look(f); } if (f->type != OBJ_FURNITURE) continue; if (f->variant == FURN_GATE) { if (f->gate_t != (f->gate_open ? 1.0f : 0.0f)) Object_swingGate(f, world->dt_props); continue; } @@ -849,16 +897,17 @@ static void landFromHigh(World *world, Object *o) { float speed = o->air_fall; o->air_fall = 0.0f; if (o->type == OBJ_DOG || speed < 5.0f) return; + float sporty = smoothstepf(0.6f, 0.95f, o->fitness) * (1.0f - fminf(o->drunk, 1.0f)) * smoothstepf(0.15f, 0.4f, o->needs.stamina); float sure = clampf(o->fitness * 1.3f, 0.0f, 1.0f) * sqrtf(o->needs.stamina) * (1.0f - fminf(o->drunk, 1.0f)) - * (1.0f - 0.5f * Body_clumsiness(&o->body)); + * (1.0f - 0.5f * Body_clumsiness(&o->body) * (1.0f - sporty)) + 0.15f * sporty; /* (the very sporty: next to sure) */ Vec3 dir = vec3(o->velocity.x, 0.0f, o->velocity.z); if (vec3_length(dir) < 0.3f) dir = Object_facing(o); - int how = Body_land(&o->body, vec3_normalize(dir), speed, sure); + int how = Body_land(&o->body, vec3_normalize(dir), speed, sure, sporty); float jar = smoothstepf(5.0f, 10.0f, speed); o->needs.stamina = fmaxf(0.0f, o->needs.stamina - (how == LAND_CRASH ? 0.25f : 0.08f) * jar); if (how == LAND_CRASH) { o->dirt = fminf(1.0f, o->dirt + 0.1f); - Ai_say(o, speed > 9.5f ? "Aaargh... my ankle!" : "Ouch!"); + Ai_say(o, speed > 9.5f + 1.3f * sporty ? "Aaargh... my ankle!" : "Ouch!"); } else if (how == LAND_ROLL) { o->dirt = fminf(1.0f, o->dirt + 0.05f); } @@ -895,6 +944,8 @@ static void scrambleHand(World *world, Object *o) { /* Hands, feet and skeleton, then carried items follow the hands. */ static void animateCharacter(World *world, Object *o, float dt) { scrambleHand(world, o); + if (o->body.pose == POSE_DOWN && o->heard_pose == POSE_FALLING) World_noise(world, NOISE_THUD, o->position, clampf(o->weight / 80.0f, 0.4f, 1.0f)); /* (down on the ground) */ + o->heard_pose = o->body.pose; if (o->climbing || o->on_ladder || o->is_held) o->air_fall = 0.0f; else if (!o->is_grounded) o->air_fall = fmaxf(o->air_fall, -o->velocity.y); else if (o->air_fall > 0.0f) landFromHigh(world, o); @@ -912,6 +963,7 @@ static void animateCharacter(World *world, Object *o, float dt) { if (o->seat->type == OBJ_FURNITURE && o->seat->variant == FURN_CHAIR) o->seat->chair_goal = CHAIR_PULL; } Object_leaveSeat(o, dt); + Object_scootOnSeat(o, dt); o->body.stand_ahead = o->sitting && o->seat ? 0.12f : 0.0f; /* (feet clear of it) */ const bool sit_climb = o->climbing && o->climb_move.sit_way; /* (the careful way up: sitting on it, not pulling up) */ o->body.on_ladder = o->on_ladder != NULL || (o->climbing && !sit_climb); /* (or pulling up over a ledge) */ @@ -1185,6 +1237,13 @@ static void pushOffCarried(World *world, Object *who) { } } +void World_noiseOn(World *world, NoiseKind kind, Vec3 at, float loud, int surface) { + if (world->noise_count >= WORLD_NOISES) return; + world->noises[world->noise_count++] = (Noise){ kind, at, clampf(loud, 0.0f, 1.0f), surface }; +} + +void World_noise(World *world, NoiseKind kind, Vec3 at, float loud) { World_noiseOn(world, kind, at, loud, 0); } + void World_init(World *world) { memset(world, 0, sizeof(*world)); Object_seePeople(NULL, 0); /* (not the people of a world gone: loading a save builds a new one) */ @@ -1221,7 +1280,7 @@ bool World_add(World *world, Object *obj) { world->objects = grown; world->capacity = cap; } - if (!obj->has_home && (obj->type == OBJ_AXE || obj->type == OBJ_SHEARS || obj->type == OBJ_CHAINSAW)) { + if (!obj->has_home && (obj->type == OBJ_AXE || obj->type == OBJ_SHEARS || obj->type == OBJ_CHAINSAW || obj->type == OBJ_UMBRELLA)) { obj->has_home = true; /* (where it is at first is where it belongs) */ obj->home_spot = obj->position; obj->home_rot = obj->rotation; @@ -1254,11 +1313,11 @@ static bool hitHard(const Object *o) { } /* Thrown things: bottles break when they hit something hard; anything lands eventually. */ -static void landThrown(Object *o) { +static void landThrown(World *world, Object *o) { if (!o->thrown_by) return; float speed = vec3_length(o->velocity); bool hit = o->impact > BREAK_IMPACT || (o->is_grounded && speed > BREAK_SPEED); - if (Object_isBottle(o) && hit && hitHard(o)) Object_shatter(o, bottomOf(o)); + if (Object_isBottle(o) && hit && hitHard(o)) { Object_shatter(o, bottomOf(o)); World_noise(world, NOISE_SMASH, o->position, 0.9f); } else if (o->is_grounded) o->thrown_by = NULL; } @@ -1393,7 +1452,7 @@ void World_update(World *world, float dt, float game_dt) { windBlows(world, o, dt); windSnatches(world, o, dt); Physics_step(o, world->objects, world->count, &world->terrain, dt); - landThrown(o); + landThrown(world, o); } Grid_build(&world->grid, world->objects, world->count); /* (where things are now: for the questions below) */ Forestry_update(world, dt); @@ -1407,6 +1466,7 @@ void World_update(World *world, float dt, float game_dt) { out = vec3(out.x, 0.0f, out.z); out = vec3_length(out) > 1e-3f ? vec3_scale(vec3_normalize(out), -1.0f) : vec3(0.0f, 0.0f, 1.0f); /* (back towards the worker) */ if (w->struck > 0 && w->tool == TOOL_AXE) { + World_noise(world, NOISE_CHOP, w->contact > 0.5f ? w->edge : w->point, 0.9f); Vec3 side = vec3(-out.z, 0.0f, out.x); float k = (world->rng & 1u) ? 1.0f : -1.0f; world->rng = world->rng * 1103515245u + 12345u; @@ -1646,6 +1706,9 @@ void World_draw(World *world, Renderer *renderer, bool draw_player) { } } } +} + +void World_drawAir(World *world, Renderer *renderer) { Chips_draw(&world->chips, renderer); Weather_draw(world, renderer); } diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h index 276a059..5f0cbef 100644 --- a/entropie/src/world/world.h +++ b/entropie/src/world/world.h @@ -13,6 +13,22 @@ #include "world/chips.h" #include "world/grid.h" +/* Sounds things make this frame (game/sound.c plays them, then empties the list): + where, how loud (0..1), and for steps what's underfoot. */ +typedef enum { + NOISE_STEP, /* a foot set down (surface: STEP_*) */ + NOISE_CHOP, /* an axe into wood */ + NOISE_CLINK, /* a bottle against a bottle (a crate) */ + NOISE_SMASH, /* glass breaking */ + NOISE_THUD, /* somebody (or something heavy) hitting the ground */ + NOISE_CRASH, /* a tree coming down */ + NOISE_SNAP, /* a dry branch breaking underfoot */ + NOISE_COUNT +} NoiseKind; +enum { STEP_GRASS, STEP_GRAVEL, STEP_HARD, STEP_EARTH, STEP_MUD, STEP_WOOD, STEP_WATER }; +typedef struct { NoiseKind kind; Vec3 at; float loud; int surface; } Noise; +#define WORLD_NOISES 96 + 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) */ @@ -45,9 +61,15 @@ typedef struct World { int wind_kind; /* WindKind: on its own, or set (dev menu) */ struct Object *tent_blowing; /* a tent the wind is pushing about just now (or NULL) */ Vec3 eye; /* where the camera is (set by the game: the rain falls round it) */ + Noise noises[WORLD_NOISES]; /* sounds made this frame (see Noise) */ + int noise_count; struct RainDrops *drops; } World; +/* A sound made here (dropped when the list is full: nobody's listening). */ +void World_noise(World *world, NoiseKind kind, Vec3 at, float loud); +void World_noiseOn(World *world, NoiseKind kind, Vec3 at, float loud, int surface); + void World_init(World *world); void World_destroy(World *world); @@ -78,5 +100,7 @@ void World_update(World *world, float dt, float game_dt); void World_drawShadows(const World *world, Renderer *renderer); /* draw_player = false in first person (the camera is inside the player). */ void World_draw(World *world, Renderer *renderer, bool draw_player); +/* What floats in the air (rain, smoke, chips): after the sky, so it shows against it too. */ +void World_drawAir(World *world, Renderer *renderer); #endif /* WORLD_WORLD_H */