Nimbin[12]?Games / Entropie / commits / 9aec53c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (17 hours ago) · synced 8 hours ago

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.gz

entropie 2.28.0: sound, sorted crates, hoods and umbrellas

Christian Immanuel · 2026-10-26 15:21 · 9aec53c7674293e0f92b481d2f88e0d13a4dc386

- 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 */