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

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.12.0: wet clothes, thunderstorms, bedtime at work

Christian Immanuel · 2026-10-20 15:20 · 9b68ad2310bcee5b9b2d649adc93fc635cc4f2ca

- Clothes get wet in the rain (drawn darker, Tab: Wet), make you cold,
  and dry by a fire quickest; wet NPCs go and sit by a burning fire
- Thunderstorms in hard rain: lightning flashes, thunder a few seconds later
- Working NPCs put the work down and go to bed once really sleepy
- Rain drops right in front of the camera are no longer drawn
 commit_message.txt                  | 12 ++++++------
 entropie/Makefile                   |  2 +-
 entropie/README.md                  |  5 ++++-
 entropie/STATE.md                   | 13 ++++++++++---
 entropie/assets/shaders/figure.frag |  3 +++
 entropie/src/game/game.c            | 19 ++++++++++++++++++
 entropie/src/game/game.h            |  1 +
 entropie/src/people/ai.c            | 39 ++++++++++++++++++++++++++++++++++---
 entropie/src/people/ai.h            |  1 +
 entropie/src/render/figure.h        |  1 +
 entropie/src/render/renderer.c      |  2 ++
 entropie/src/render/renderer.h      |  2 +-
 entropie/src/world/weather.c        | 18 ++++++++++++++++-
 entropie/src/world/world.c          | 21 +++++++++++++++++++-
 entropie/src/world/world.h          |  4 ++++
 15 files changed, 126 insertions(+), 17 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index e487023..07a6e3e 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,7 +1,7 @@
-entropie 2.11.0: taking tents down
+entropie 2.12.0: wet clothes, thunderstorms, bedtime at work
 
-- Hold left click right at a standing tent: after a moment it comes down
-  (pegs out, cover off, poles down, folded into its bag)
-- Sleeping bags and mats that were inside come out at the door
-- The owner and people nearby help; a tent packed on purpose stays packed
-- New Gear_create (car.c) for camping things
+- Clothes get wet in the rain (drawn darker, Tab: Wet), make you cold,
+  and dry by a fire quickest; wet NPCs go and sit by a burning fire
+- Thunderstorms in hard rain: lightning flashes, thunder a few seconds later
+- Working NPCs put the work down and go to bed once really sleepy
+- Rain drops right in front of the camera are no longer drawn
diff --git a/entropie/Makefile b/entropie/Makefile
index 4185ade..2faa756 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.11.0
+VERSION   := 2.12.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 9764bd5..f1896f7 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -52,7 +52,10 @@ Painted in the rotoscope style of *Waking Life* (2001):
 - 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
-- Out in the rain you get cold sooner (pullover)
+- Out in the rain your clothes get wet (darker; Tab: Wet) and you get cold.
+  They dry by a fire quickest, in the day slower, at night slowest. Wet
+  through, the others go and sit by a burning fire
+- Hard rain brings thunderstorms: lightning, then the thunder
 
 ## Tents
 
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 4007350..b4cbc09 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -224,6 +224,13 @@ Round 2.11:
 - Taking a tent down (`tent.c` Tent_takeDown, `tent_down`): anybody in it comes out (woken); then Tent_work runs the stages backwards (workDown: each DOWN_QUICKER 0.6 of the time up, the pegs out one by one from the last, the cover off, the poles down, FOLD_TIME 7 s folding) and returns TENT_DOWN + the stage (TENT_DOWN_PACKED folded). Tent_update packs it (`pack`): its bag (`Gear_create`, from car.c) where it lay, `keep` (its owner doesn't put it up again on their own: `ai.c` pitchTents), the sleeping bags and mats that were in it at the door; the tent used up (no owner: Tent_of skips it). Drawn the other way round (`shownWork`).
 - You: hold left click right at a standing tent (0.6 m, so a click near it still takes things): TAKE_DOWN_HOLD 1 s, then it comes down while you hold (`game_actions.c` workTent). The owner and anybody near help (helpers drop a packed job). Hints: "hold left click: take it down".
 
+Round 2.12:
+- Wet clothes (`FigureLook.wet`, `world.c` clothesWet, game seconds via `world.game_dt`): out in the rain wet through in SOAK_THROUGH 6 min x rain; dry by a burning fire (4 m) in 8 min, in the day outside 40 min, else 2 h. Wet (over 0.2, not by a fire): cold like the rain (0.8 x wet). Drawn darker (`figure.frag` uniform `soaked`: clothes, hair, shoes x (1 - 0.34 wet)). Tab: "Wet"; "You're wet through" once over 0.6.
+- Drying off (`ai.c` dryOff, idle / wander): over 0.45 wet, the rain under 0.1, a burning fire within 60 m: over to it (2.4 m), sitting there till under 0.1.
+- Thunderstorms (`weather.c`, rain over STORM_RAIN 0.7): a flash about every 75 game s (`world.flash`, fading in a quarter second: `game.c` rainLight lights everything white), the thunder 1-6 s after (`world.thunder`: said on the hint line, somebody near says something).
+- Bedtime at work (`ai.c` goToBed): working, they go once sleepiness is over WORK_BEDTIME 0.9 (idle 0.8), putting the work down first.
+- Rain drops within 1.5 m of the eye aren't drawn (fat bars right in front of the camera).
+
 Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves.
 
 Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built).
@@ -370,8 +377,8 @@ Most important first.
 - Smoking, still to do: paying for a pack (no money in the game yet)
 - "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); the pullover over a railing NPCs never do on their own
-- Tents, still to do: wind blowing an unpegged one about; people busy (work) when sleepy go to bed late; NPCs taking theirs down on their own (leaving)
-- Rain, still to do: thunder; wet clothes
+- Tents, still to do: wind blowing an unpegged one about; NPCs taking theirs down on their own (leaving)
+- Rain, still to do: wind; a sound for the thunder (no sound in the game yet)
 - Main building: "the left building's roof peak is too far left" - to be clarified
 
 **Tools for us**
@@ -381,4 +388,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.11.0 (build 7d62268)`
+State of `entropie 2.12.0 (build afa32be)`
diff --git a/entropie/assets/shaders/figure.frag b/entropie/assets/shaders/figure.frag
index e1813f6..e23923d 100644
--- a/entropie/assets/shaders/figure.frag
+++ b/entropie/assets/shaders/figure.frag
@@ -41,6 +41,7 @@ uniform vec3  stream[4];    // pee or sick: an arc from the body to the ground (
 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 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;
@@ -468,6 +469,8 @@ void main() {
     }
     else if (mat == M_HAIR) base = hairDetail(base, p);
     else if (mat < M_PEE) base = clothDetail(mat, base, p);
+    if (species == 0 && (mat == M_SHIRT || mat == M_PANTS || mat == M_JACKET || mat == M_HAIR || mat == M_SHOES))
+        base *= 1.0 - 0.34 * soaked; // (wet cloth: darker)
     if (mat == M_JACKET) { // the zip down the front, a darker collar
         vec3 rel = p - joints[J_CHEST];
         vec3 side = cross(spineDir, chestForward);
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 344a407..6fe77c6 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -182,6 +182,7 @@ int Game_stats(const Game *game, StatBar *bars, int max, const char **title, cha
         { "Worn (days)", p->worn,             true },
         { "Drunk",   fminf(p->drunk, 1.0f),   true },
         { "Dirty",   p->dirt,                 true },
+        { "Wet",     p->look.wet,             true },
         { "Sporty",  p->fitness,              false },
     };
     const StatBar skills[] = { /* grow with practice */
@@ -283,6 +284,10 @@ static void rainLight(Game *game) {
     l->sky_top      = vec3_lerp(l->sky_top, vec3_scale(grey, 0.8f * bright), fminf(1.0f, 1.3f * r));
     l->sky_horizon  = vec3_lerp(l->sky_horizon, vec3_scale(grey, bright), fminf(1.0f, 1.2f * r));
     l->shadow_strength *= 1.0f - 0.85f * r;
+    float f = game->world.flash; /* (lightning: everything lit up white for a moment) */
+    l->light_color = vec3_add(l->light_color, vec3_scale(vec3(1.4f, 1.45f, 1.6f), f));
+    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;
 }
 
@@ -321,6 +326,20 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
         game->rain_told = true;
         say(game, "It's starting to rain - under a roof (the house, your garages) or into your tent");
     } else if (game->world.rain < 0.05f) game->rain_told = false;
+    if (game->world.thunder) { /* (the thunder: said, and somebody near reacts) */
+        static const char *const ROLL[] = { "*KRRRAKK* - thunder, close by", "*rumble* - thunder rolls over the farm", "*BOOM* - that one was close" };
+        say(game, ROLL[(int)(game->daycycle.seconds) % 3]);
+        for (int i = 0; i < game->world.count; i++) {
+            Object *o = game->world.objects[i];
+            if (o->type != OBJ_HUMAN || o->body.asleep || o->indoors > 0.0f || o->ai.speech_time > 0.0f || ((int)(game->daycycle.seconds) + i) % 4) continue;
+            Ai_say(o, (i % 2) ? "Whoa! That was close." : "Did you see that one?");
+            break;
+        }
+    }
+    if (game->player->look.wet > 0.6f && !game->wet_told) {
+        game->wet_told = true;
+        say(game, "You're wet through - by a fire you dry quickest");
+    } else if (game->player->look.wet < 0.2f) game->wet_told = false;
     game_arrival(game, dt);
     Ai_setSettled(game->arrival.task == TASK_NONE);
     tooHeavy(game);
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index ce199bb..004a25c 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -77,6 +77,7 @@ typedef struct {
     float          tent_hold; /* ... held this long at a standing one (a moment: you start taking it down) */
     float    dt;            /* this frame's step */
     bool     rain_told;     /* told it's raining (this shower) */
+    bool     wet_told;      /* "wet through" said (till dry again) */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 4ca5f8a..bda7612 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -1960,6 +1960,33 @@ static bool shelter(Object *self, Object *const *objects, int count, float dt) {
     return true;
 }
 
+/* Wet through after the rain: over to a burning fire, sitting by it till dry. */
+#define DRY_START  0.45f /* wet this much ... */
+#define DRY_DONE   0.1f  /* ... dry enough */
+#define DRY_WALK   60.0f /* m: a burning fire this near */
+
+static bool dryOff(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    if (!g_world || self->body.asleep || self->in_tent || self->indoors > 0.0f || self->riding) return false;
+    Object *fire = Camp_fireNear(objects, count, self->position, DRY_WALK, true);
+    if (!ai->drying) {
+        if (self->look.wet < DRY_START || g_world->rain > 0.1f || !fire || random01(ai) > 0.3f * dt) return false;
+        ai->drying = true;
+        if (random01(ai) < 0.6f) Ai_say(self, random01(ai) < 0.5f ? "Soaked. I'm going to dry off by the fire." : "Wet through - fire, here I come.");
+    }
+    if (self->look.wet < DRY_DONE || !fire || g_world->rain > 0.3f) { ai->drying = false; return false; }
+    float d = flatDistance(fire->position, self->position);
+    if (self->sitting && d < 4.0f) { rest(ai, 5.0f, 10.0f); return true; }
+    Vec3 to = vec3_sub(self->position, fire->position);
+    to.y = 0.0f;
+    to = d > 0.1f ? vec3_scale(to, 1.0f / d) : vec3(1.0f, 0.0f, 0.0f);
+    if (d > 3.4f && !goToPoint(self, vec3_add(fire->position, vec3_scale(to, 2.4f)), 1.0f, objects, count, dt)) return true;
+    stand(self);
+    Object_sit(self, Object_seatNear(objects, count, self->position, 1.5f), fire->position);
+    rest(ai, 5.0f, 10.0f);
+    return true;
+}
+
 /* 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). */
 static bool greetHosts(Object *self, Object *const *objects, int count, float dt) {
@@ -1981,14 +2008,17 @@ static bool greetHosts(Object *self, Object *const *objects, int count, float dt
 }
 
 /* Sleepy, their tent up: over to it and in. */
+#define WORK_BEDTIME 0.9f /* sleepiness: working, they keep going till this (idle: 0.8) */
+
 static bool goToBed(Object *self, Object *const *objects, int count, float dt) {
     if (self->needs.sleepiness < 0.8f || self->body.asleep || self->in_tent || self->body.pose != POSE_UP) return false;
     Object *tent = Tent_of(objects, count, self);
     if (!tent || tent->tent_stage < TENT_UP || flatDistance(tent->position, self->position) > TENT_WALK) return false;
     if (self->sitting) self->sitting = false;
+    if (self->ai.mode == AI_WORK) freeHands(self, NULL); /* (the work put down where it is) */
     if (!goToPoint(self, Tent_door(tent), 0.4f, objects, count, dt)) return true;
     freeHands(self, NULL);
-    if (random01(&self->ai) < 0.5f) Ai_say(self, "Night, everybody.");
+    if (random01(&self->ai) < 0.5f) Ai_say(self, self->ai.mode == AI_WORK ? "That's it, I'm done in. Night." : "Night, everybody.");
     Tent_crawlIn(tent, self);
     rest(&self->ai, 2.0f, 3.0f);
     return true;
@@ -2131,14 +2161,17 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     /* Rain: inside */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || ai->mode == AI_WORK || ai->mode == AI_FETCH) &&
         shelter(self, objects, count, dt)) return;
+    /* Wet through: by the fire to dry */
+    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && dryOff(self, objects, count, dt)) return;
     /* Unloading the car */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && unloadCar(self, objects, count, dt)) return;
     /* Just arrived: hello at the fire */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && greetHosts(self, objects, count, dt)) return;
     /* Kurti and his sons: into the house for a while (not while you're being welcomed) */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && goInside(self, objects, count, dt)) return;
-    /* Sleepy: to bed in their tent */
-    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && goToBed(self, objects, count, dt)) return;
+    /* Sleepy: to bed in their tent (busy working: later, once really done in) */
+    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || (ai->mode == AI_WORK && self->needs.sleepiness > WORK_BEDTIME)) &&
+        goToBed(self, objects, count, dt)) return;
     /* Tents: their own up, a hand with others', the bed things in */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && pitchTents(self, objects, count, dt)) return;
     /* A finished cigarette in the fingers: to the tin, a bin, the fire */
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index e68f421..1735dd8 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -179,6 +179,7 @@ typedef struct {
     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           drying;      /* wet through after the rain: sitting by the fire till dry */
     bool           going_in;    /* a resident on the way into the house (ai.c goInside) */
     struct Object *tent_job;    /* lending a hand with this tent (ai.c pitchTents) ... */
     float          tent_time;   /* ... this long */
diff --git a/entropie/src/render/figure.h b/entropie/src/render/figure.h
index 6e6c098..cb012d3 100644
--- a/entropie/src/render/figure.h
+++ b/entropie/src/render/figure.h
@@ -32,6 +32,7 @@ typedef struct {
     bool      jacket_tied;   /* the jacket off and along with them: tied round the waist by its sleeves */
     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) */
 } 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 5b13af6..2b25bc9 100644
--- a/entropie/src/render/renderer.c
+++ b/entropie/src/render/renderer.c
@@ -105,6 +105,7 @@ static bool loadShaders(Renderer *r) {
     f->top             = glGetUniformLocation(f->program, "top");
     f->jacket_tied     = glGetUniformLocation(f->program, "jacketTied");
     f->belly           = glGetUniformLocation(f->program, "belly");
+    f->wet             = glGetUniformLocation(f->program, "soaked");
     f->cig_a           = glGetUniformLocation(f->program, "cigA");
     f->uri_a           = glGetUniformLocation(f->program, "uriA");
     f->uri_b           = glGetUniformLocation(f->program, "uriB");
@@ -548,6 +549,7 @@ void Renderer_drawFigure(Renderer *r, const FigurePose *pose) {
     glUniform1i(f->top, pose->look.top);
     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);
     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 734bcb9..fb7cdb5 100644
--- a/entropie/src/render/renderer.h
+++ b/entropie/src/render/renderer.h
@@ -70,7 +70,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;
+    GLint  garment, undress, top, jacket_tied, belly, wet;
     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;
diff --git a/entropie/src/world/weather.c b/entropie/src/world/weather.c
index a546f6c..8ec6206 100644
--- a/entropie/src/world/weather.c
+++ b/entropie/src/world/weather.c
@@ -19,6 +19,9 @@
 #define DROP_SPEED    8.5f
 #define DROP_LEN      0.6f
 #define FIRE_DOUSE    0.004f            /* kg of a fire's wood a game second, in hard rain */
+#define STORM_RAIN    0.7f              /* rain this hard: a thunderstorm ... */
+#define FLASH_EVERY   75.0f             /* ... a flash about every this many game seconds */
+#define FLASH_FADE    4.0f              /* a flash gone in a quarter second (real) */
 
 static float dice(unsigned *rng) {
     *rng = *rng * 1103515245u + 12345u;
@@ -30,6 +33,8 @@ void Weather_init(World *world) {
     world->rain_left = FIRST_DRY;
     world->wet = 0.0f;
     world->weather = WEATHER_AUTO;
+    world->flash = world->thunder_in = 0.0f;
+    world->thunder = false;
     world->drops = calloc(1, sizeof(*world->drops));
     if (world->drops) world->drops->rng = 0x5eed1234u;
 }
@@ -72,6 +77,14 @@ void Weather_update(World *world, float dt, float game_dt) {
     if (world->rain < 0.002f && world->rain_goal <= 0.0f) world->rain = 0.0f;
     bool day = world->hour >= 7.0f && world->hour < 20.0f;
     world->wet = clampf(world->wet + world->rain * game_dt / SOAK_TIME - (1.0f - world->rain) * game_dt / (DRY_TIME * (day ? 1.0f : 3.0f)), 0.0f, 1.0f);
+    /* a thunderstorm: lightning, the thunder a few seconds after it */
+    world->thunder = false;
+    world->flash = fmaxf(0.0f, world->flash - FLASH_FADE * dt);
+    if (world->thunder_in > 0.0f && (world->thunder_in -= dt) <= 0.0f) { world->thunder_in = 0.0f; world->thunder = true; }
+    if (world->rain > STORM_RAIN && world->drops && world->thunder_in <= 0.0f && dice(&world->drops->rng) < game_dt / FLASH_EVERY) {
+        world->flash = 1.0f;
+        world->thunder_in = 1.0f + 5.0f * dice(&world->drops->rng); /* (near: right after; further: later) */
+    }
     if (world->rain <= 0.0f || !world->drops) return;
 
     /* the ground near you gets the water: puddles, mud (not under a roof) */
@@ -106,7 +119,10 @@ void Weather_draw(World *world, Renderer *renderer) {
     Vec3 shown[RAIN_DROPS];
     int n = 0, live = (int)(RAIN_DROPS * world->rain);
     for (int k = 0; k < live; k++)
-        if (!world->drops->hidden[k]) shown[n++] = world->drops->p[k];
+        if (!world->drops->hidden[k]) { /* (not right in front of the eye: a fat bar) */
+            Vec3 p = world->drops->p[k], e = vec3_sub(p, world->eye);
+            if (e.x * e.x + e.z * e.z > 1.5f * 1.5f || fabsf(e.y) > 1.5f) shown[n++] = p;
+        }
     if (n == 0) return;
     Mesh mesh = Shapes_rain(shown, n, DROP_LEN);
     Renderer_drawSmoke(renderer, &mesh, mat4_identity(), vec3(1.0f, 1.0f, 1.0f));
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 9e9bee6..6f0b529 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -726,6 +726,22 @@ static void smoke(World *world, Object *o, Vec3 facing, float dt) {
     }
 }
 
+/* Wet clothes: out in the rain they soak through; they dry by a fire quickest,
+   in the day slower, at night or inside slowest (game seconds). */
+#define SOAK_THROUGH  (6.0f * 60.0f)   /* hard rain: wet through */
+#define DRY_BY_FIRE   (8.0f * 60.0f)
+#define DRY_IN_DAY    (40.0f * 60.0f)
+#define DRY_ELSE      (120.0f * 60.0f)
+
+static void clothesWet(World *world, Object *o, bool by_fire, bool night) {
+    float gdt = world->game_dt;
+    if (o->rain_out) o->look.wet = fminf(1.0f, o->look.wet + world->rain * gdt / SOAK_THROUGH);
+    else if (o->look.wet > 0.0f) {
+        float dry = by_fire ? DRY_BY_FIRE : !night && o->indoors <= 0.0f && !o->in_tent ? DRY_IN_DAY : DRY_ELSE;
+        o->look.wet = fmaxf(0.0f, o->look.wet - gdt / dry);
+    }
+}
+
 /* Cold nights: away from a burning fire a chill builds up and the pullover goes
    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
@@ -738,7 +754,9 @@ static void dressForNight(World *world, Object *o, float dt) {
     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 (o->rain_out) cold = fmaxf(cold, 0.0f) + 1.2f * world->rain; /* (wet through: cold, even in the day) */
+    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) */
     o->chill = clampf(o->chill + cold * dt / 60.0f, 0.0f, 1.0f); /* (about a minute to feel it) */
     bool player = o->type == OBJ_PLAYER;
     if (o->look.top != TOP_JACKET && o->chill > 0.6f && !(player && o->jacket_told)) {
@@ -1238,6 +1256,7 @@ static void emptyTray(World *world, Object *o) {
 void World_update(World *world, float dt, float game_dt) {
     Object_indexKinds(world->objects, world->count);
     world->dt_props = dt;
+    world->game_dt = game_dt;
     Clothes_update(world);
     Weather_update(world, dt, game_dt);
     Car_serve(world);
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index deb62ca..aab3899 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -15,6 +15,7 @@
 
 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) */
     Object **objects;
     int      count;
     int      capacity;
@@ -32,6 +33,9 @@ typedef struct World {
     float    rain_goal, rain_left; /* ... where it's going, for how much longer (game s) */
     int      weather;      /* WeatherKind: on its own, or set (dev menu) */
     float    wet;          /* 0..1 everything wet from the rain (dries over hours) */
+    float    flash;        /* 0..1 lightning just now (fades in a moment) ... */
+    float    thunder_in;   /* ... the thunder after it, in this many seconds (0: none coming) ... */
+    bool     thunder;      /* ... rolling just now (this frame: the game says so) */
     Vec3     eye;          /* where the camera is (set by the game: the rain falls round it) */
     struct RainDrops *drops;
 } World;