Nimbin[12]?Games / Entropie / commits / ec4a5e0

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.13.0: wind

Christian Immanuel · 2026-10-20 15:20 · ec4a5e0173d833aef9db6770ca1562d543d6429b

- Wind with calm and windy spells and gusts (windier with rain and storms)
- Rain slants and drifts with it; fires now smoke, and smoke and flames
  go with the wind
- Tents not pegged down (under half the pegs) blow about in strong wind
  until they snag or someone works on them; the hint line warns you
- Dev menu: wind (on its own, calm, a breeze, a storm)
 commit_message.txt                 | 13 +++++-----
 entropie/Makefile                  |  2 +-
 entropie/README.md                 |  6 ++++-
 entropie/STATE.md                  | 11 ++++++---
 entropie/src/game/game.c           |  8 +++++++
 entropie/src/game/game.h           |  1 +
 entropie/src/game/game_dev.c       |  8 ++++++-
 entropie/src/render/shapes.h       |  2 +-
 entropie/src/render/shapes_props.c |  4 ++--
 entropie/src/world/chips.c         | 35 ++++++++++++++++++++-------
 entropie/src/world/chips.h         |  5 +++-
 entropie/src/world/object.h        |  1 +
 entropie/src/world/tent.c          | 30 +++++++++++++++++++++++
 entropie/src/world/weather.c       | 49 ++++++++++++++++++++++++++++++++++++--
 entropie/src/world/weather.h       |  6 +++++
 entropie/src/world/world.c         | 22 ++++++++++++-----
 entropie/src/world/world.h         |  8 +++++++
 17 files changed, 179 insertions(+), 32 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index 07a6e3e..fdf2961 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,7 +1,8 @@
-entropie 2.12.0: wet clothes, thunderstorms, bedtime at work
+entropie 2.13.0: wind
 
-- 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
+- Wind with calm and windy spells and gusts (windier with rain and storms)
+- Rain slants and drifts with it; fires now smoke, and smoke and flames
+  go with the wind
+- Tents not pegged down (under half the pegs) blow about in strong wind
+  until they snag or someone works on them; the hint line warns you
+- Dev menu: wind (on its own, calm, a breeze, a storm)
diff --git a/entropie/Makefile b/entropie/Makefile
index 2faa756..796f03f 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.12.0
+VERSION   := 2.13.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index f1896f7..f839f4c 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -56,6 +56,10 @@ Painted in the rotoscope style of *Waking Life* (2001):
   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
+- Wind: calm or windy spells, gusts on top (windier with rain). The rain
+  slants, the fire's smoke and flames go with it
+- A tent not pegged down (under half its pegs) blows about in strong wind
+  until it snags on something or somebody works on it
 
 ## Tents
 
@@ -701,7 +705,7 @@ say no more often and don't come to you for a bit.
 | V             | Third / first person |
 | I             | Show all keys in game |
 | Tab           | Your stats (Left / Right: other page) |
-| 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, skip the start |
+| 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, keys, quit (the game waits meanwhile) |
 
 The game starts in third person. Close to a wall the view moves in, never through it.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index b4cbc09..3bd2853 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -231,6 +231,11 @@ Round 2.12:
 - 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).
 
+Round 2.13:
+- Wind (`weather.c` blow): `wind_goal` 0..1 (x WIND_MAX 14 m/s) changes every 20-80 game min (0.04 + 0.35 d^2 + 0.45 x rain coming), a thunderstorm at least 0.55; `wind_level` eases to it (WIND_EASE 240 game s); `wind_angle` turns a bit each change. Gusts: a slow beat of two sines (GUST_RATE 0.45) on top (up to 0.6 x WIND_MAX x level). `world.wind` the vector now; `Weather_windSpeed`. Dev menu: on its own / calm / a breeze / a storm.
+- Rain drops drift with it and slant (`Shapes_rain` lean: the top upwind). Smoke and sawdust slow to the wind's speed (`Chips_update` wind). Fires smoke now (`Chips_smoke`, a big soft grey puff every 0.22 s / flame size, rising 0.9, spreading 0.22 m/s). Flames sheared downwind (`drawFlames`, 0.07 x wind, at most 0.8).
+- Tents in the wind (`tent.c` blowAbout): not up and under half the pegs in, nobody working on it for BLOW_HELD 1.5 s: on its poles from 4 m/s, flat from 6.5 m/s, it slides 0.13 m/s per m/s over that (flat half), turning a little, on the ground; a ray along the wind (0.3 m up) hitting something static: snagged, it stays. `world.tent_blowing` -> the hint line: "Volkram's tent is blowing about in the wind - peg it down!" (not again for 60 s).
+
 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).
@@ -377,8 +382,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; NPCs taking theirs down on their own (leaving)
-- Rain, still to do: wind; a sound for the thunder (no sound in the game yet)
+- Tents, still to do: NPCs taking theirs down on their own (leaving); owners running after theirs when it blows away
+- Weather, still to do: a sound for thunder, wind and rain (no sound in the game yet); trees and bushes swaying; loose light things (paper, a pullover) blown about
 - Main building: "the left building's roof peak is too far left" - to be clarified
 
 **Tools for us**
@@ -388,4 +393,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.12.0 (build afa32be)`
+State of `entropie 2.13.0 (build f0f0b53)`
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 6fe77c6..9882dd9 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -336,6 +336,14 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
             break;
         }
     }
+    game->blow_told = fmaxf(0.0f, game->blow_told - dt);
+    if (game->world.tent_blowing && game->blow_told <= 0.0f) { /* (a tent not pegged down, the wind pushing it about) */
+        const Object *t = game->world.tent_blowing, *o = t->owner;
+        static char line[96];
+        snprintf(line, sizeof(line), "%s%s tent is blowing about in the wind - peg it down!", !o ? "A" : o == game->player ? "Your" : o->name, o && o != game->player ? "'s" : "");
+        say(game, line);
+        game->blow_told = 60.0f;
+    }
     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");
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index 004a25c..d2729ce 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -78,6 +78,7 @@ typedef struct {
     float    dt;            /* this frame's step */
     bool     rain_told;     /* told it's raining (this shower) */
     bool     wet_told;      /* "wet through" said (till dry again) */
+    float    blow_told;     /* "your tent is blowing about" said: not again for a while */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c
index ddcc009..0b8e8b2 100644
--- a/entropie/src/game/game_dev.c
+++ b/entropie/src/game/game_dev.c
@@ -10,7 +10,7 @@
 #include "world/weather.h"
 
 typedef enum {
-    DEV_WHO, DEV_TELEPORT, DEV_TIME, DEV_SPEED, DEV_WEATHER, DEV_START, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_DRUNK, DEV_ENERGY,
+    DEV_WHO, DEV_TELEPORT, DEV_TIME, DEV_SPEED, DEV_WEATHER, DEV_WIND, DEV_START, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_DRUNK, DEV_ENERGY,
     DEV_BLADDER, DEV_HUNGER, DEV_THIRST, DEV_SLEEPY, DEV_STRENGTH, DEV_ANNOYED,
     DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING,
     DEV_LIKES,                       /* likes for each job ... */
@@ -109,6 +109,11 @@ void game_dev(Game *game, const Input *input) {
         if (game->world.weather == WEATHER_AUTO) game->world.rain_left = 0.0f; /* (on its own again: from now) */
         return;
     }
+    if (game->dev_row == DEV_WIND) {
+        game->world.wind_kind = (game->world.wind_kind + dir + WIND_KINDS) % WIND_KINDS;
+        if (game->world.wind_kind == WIND_AUTO) game->world.wind_left = 0.0f; /* (on its own again: from now) */
+        return;
+    }
     if (game->dev_row == DEV_START) {
         if (dir > 0) game_skipArrival(game);
         return;
@@ -174,6 +179,7 @@ static void devLine(const Game *game, const Object *o, int row, char *out) {
     case DEV_SPEED:    snprintf(out, 96, "Time runs:  %.0fx", game->daycycle.time_scale); break;
     case DEV_WEATHER:  snprintf(out, 96, "Weather:  %s  (raining %.0f%%, wet %.0f%%)", Weather_name((WeatherKind)game->world.weather),
                                 game->world.rain * 100.0f, game->world.wet * 100.0f); break;
+    case DEV_WIND:     snprintf(out, 96, "Wind:  %s  (%.0f m/s)", Weather_windName((WindKind)game->world.wind_kind), Weather_windSpeed(&game->world)); break;
     case DEV_START:    snprintf(out, 96, "The start:  %s", game->arrival.task == TASK_NONE && game->arrival.stage != ARRIVAL_DRIVING ? "done" : "skip it ->"); break;
     case DEV_FEMALE:   snprintf(out, 96, "Female:  %s", o->female ? "yes" : "no"); break;
     case DEV_URINELLA: snprintf(out, 96, "Urinella:  %s (%d left)", o->urinella ? "yes" : "no", o->urinellas); break;
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index 8539427..065bb34 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -134,7 +134,7 @@ Mesh  Shapes_car(Vec3 color, bool boot_open);
 /* ... seen from the driver's seat: the bonnet, the dashboard, the wheel, the pillars, the roof. */
 Mesh  Shapes_carInside(Vec3 color);
 /* Rain: a thin light streak at each drop (world space), len long. */
-Mesh  Shapes_rain(const Vec3 *drops, int n, float len);
+Mesh  Shapes_rain(const Vec3 *drops, int n, float len, Vec3 lean);
 /* A crate of bottles (centred): variant 0 beer, 1 water, 2 soda (cola and orange), 3 mate; this many bottles in it. */
 #define CRATE_SIZE_X 0.40f
 #define CRATE_SIZE_Y 0.30f
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 086ea63..405d145 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -954,12 +954,12 @@ Mesh Shapes_carInside(Vec3 color) {
     return Mesh_fromBuilder(&b);
 }
 
-Mesh Shapes_rain(const Vec3 *drops, int n, float len) {
+Mesh Shapes_rain(const Vec3 *drops, int n, float len, Vec3 lean) {
     MeshBuilder b;
     MeshBuilder_init(&b);
     const Vec3 color = { 0.78f, 0.82f, 0.88f };
     for (int k = 0; k < n; k++) {
-        Vec3 p = drops[k], d = vec3(0.07f * len, len, 0.0f); /* (leaning a little with the wind) */
+        Vec3 p = drops[k], d = vec3(lean.x * len, len, lean.z * len); /* (leaning with the wind: the top upwind) */
         Vec3 side = vec3(0.0f, 0.0f, 0.014f), across = vec3(0.014f, 0.0f, 0.0f);
         GLuint i0 = MeshBuilder_vertex(&b, vec3_sub(p, side), vec3(0, 0, 1), color);
         GLuint i1 = MeshBuilder_vertex(&b, vec3_add(p, side), vec3(0, 0, 1), color);
diff --git a/entropie/src/world/chips.c b/entropie/src/world/chips.c
index 6053a54..19b87f4 100644
--- a/entropie/src/world/chips.c
+++ b/entropie/src/world/chips.c
@@ -68,22 +68,41 @@ void Chips_puff(Chips *c, Vec3 at, Vec3 dir, bool small) {
     };
 }
 
-void Chips_update(Chips *c, const Terrain *terrain, float dt) {
+void Chips_smoke(Chips *c, Vec3 at) {
+    Chip *ch = &c->chips[c->next];
+    c->next = (c->next + 1) % CHIP_MAX;
+    Vec3 jitter = vec3(rnd(c) - 0.5f, 0.0f, rnd(c) - 0.5f);
+    float grey = 0.78f + 0.12f * rnd(c);
+    *ch = (Chip){
+        .p = vec3_add(at, vec3_scale(jitter, 0.4f)),
+        .v = vec3_add(vec3_scale(jitter, 0.2f), vec3(0.0f, 0.7f + 0.3f * rnd(c), 0.0f)),
+        .life = 3.5f + 1.5f * rnd(c),
+        .size = 0.25f + 0.12f * rnd(c),
+        .angle = 6.28f * rnd(c),
+        .spin = (rnd(c) - 0.5f) * 0.8f,
+        .color = vec3(grey, grey, grey * 1.04f),
+        .smoke = true,
+    };
+}
+
+void Chips_update(Chips *c, const Terrain *terrain, Vec3 wind, float dt) {
     for (int i = 0; i < CHIP_MAX; i++) {
         Chip *ch = &c->chips[i];
         if (ch->life <= 0.0f) continue;
         ch->life -= dt;
-        if (ch->smoke) { /* slows down, rises, spreads */
-            ch->v = vec3_scale(ch->v, expf(-2.5f * dt));
-            ch->v.y += 0.35f * dt;
+        if (ch->smoke) { /* slows down to the wind's speed, rises, spreads */
+            float k = expf(-2.5f * dt), up = ch->v.y * k;
+            ch->v = vec3_add(vec3_scale(ch->v, k), vec3_scale(wind, 0.8f * (1.0f - k)));
+            ch->v.y = up + (ch->size > 0.05f ? 0.9f : 0.35f) * dt; /* (a fire's smoke: warm, rising more) */
             ch->p = vec3_add(ch->p, vec3_scale(ch->v, dt));
-            ch->size += 0.03f * dt;
+            ch->size += (ch->size > 0.05f ? 0.22f : 0.03f) * dt; /* (a fire's smoke spreads more) */
             ch->angle += ch->spin * dt;
             continue;
         }
         if (ch->down) continue;
-        float drag = ch->dust ? 3.5f : 0.6f;
-        ch->v = vec3_scale(ch->v, expf(-drag * dt));
+        float drag = ch->dust ? 3.5f : 0.6f, k = expf(-drag * dt), fall = ch->v.y * k;
+        ch->v = vec3_add(vec3_scale(ch->v, k), ch->dust ? vec3_scale(wind, 0.7f * (1.0f - k)) : vec3_zero());
+        ch->v.y = fall;
         ch->v.y -= GRAVITY_C * (ch->dust ? 0.25f : 1.0f) * dt;
         ch->p = vec3_add(ch->p, vec3_scale(ch->v, dt));
         ch->angle += ch->spin * dt;
@@ -102,7 +121,7 @@ void Chips_draw(const Chips *c, Renderer *renderer) {
         const Chip *ch = &c->chips[i];
         if (ch->life <= 0.0f) continue;
         float shrink = fminf(1.0f, ch->life / (ch->smoke ? 0.5f : ch->dust ? 0.3f : 1.5f)); /* (fading away at the end) */
-        float s = ch->size * shrink;
+        float s = ch->size * (ch->smoke && ch->size > 0.05f ? 1.0f : shrink); /* (a fire's smoke: thinning out instead) */
         Mat4 m = mat4_mul(mat4_translate(ch->p), mat4_mul(mat4_rotate_yxz(vec3(ch->down ? 0.0f : ch->angle, ch->angle * 0.7f, 0.0f)),
                                                           mat4_scale(vec3(s, s * (ch->dust || ch->smoke ? 1.0f : 0.35f), s * (ch->dust || ch->smoke ? 1.0f : 0.7f)))));
         if (ch->smoke) Renderer_drawSmoke(renderer, &c->mesh, m, ch->color);
diff --git a/entropie/src/world/chips.h b/entropie/src/world/chips.h
index 333e546..1800e7c 100644
--- a/entropie/src/world/chips.h
+++ b/entropie/src/world/chips.h
@@ -40,7 +40,10 @@ void Chips_destroy(Chips *c);
 void Chips_spray(Chips *c, Vec3 at, Vec3 dir, int n, bool dust);
 /* A little puff of smoke at at (breathed out along dir, or off a cigarette's tip: small). */
 void Chips_puff(Chips *c, Vec3 at, Vec3 dir, bool small);
-void Chips_update(Chips *c, const Terrain *terrain, float dt);
+/* Smoke off a fire (a big soft puff, rising). */
+void Chips_smoke(Chips *c, Vec3 at);
+/* wind: m/s the air moves (smoke and dust go with it). */
+void Chips_update(Chips *c, const Terrain *terrain, Vec3 wind, float dt);
 void Chips_draw(const Chips *c, Renderer *renderer);
 
 #endif /* WORLD_CHIPS_H */
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 144e876..493b6d0 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -171,6 +171,7 @@ typedef struct Object {
     int        tent_stage;    /* tents: TentStage ... */
     float      tent_work;     /* ... how far the stage is (0..1) ... */
     float      tent_idle;     /* ... seconds since anybody worked on it */
+    float      smoke_t;       /* fireplaces: till the next puff of smoke */
     int        tent_beds;     /* ... sleeping bags (bit 0 owner, 2 owner2) and mats (1, 3) put in */
     bool       tent_down;     /* ... being taken down: the stages backwards, then packed into its bag */
     struct Object *in_tent;   /* characters: sleeping in this tent (no collisions with it) */
diff --git a/entropie/src/world/tent.c b/entropie/src/world/tent.c
index 513d025..6e13496 100644
--- a/entropie/src/world/tent.c
+++ b/entropie/src/world/tent.c
@@ -2,6 +2,7 @@
 #include "world/car.h"
 #include "people/hands.h"
 #include "render/shapes.h"
+#include "world/weather.h"
 
 #include <stdio.h>
 #include <string.h>
@@ -267,14 +268,43 @@ static void pack(World *world, Object *tent) {
     Object_useUp(tent);
 }
 
+/* The wind and a tent not pegged down (pegs under half in): it slides and
+   turns along the ground, more standing on its poles than lying flat, till
+   it snags on something or somebody gets hold of it (working on it). */
+#define BLOW_STANDING 4.0f  /* m/s: on its poles, it goes from this wind ... */
+#define BLOW_FLAT     6.5f  /* ... lying flat from this */
+#define BLOW_PUSH     0.13f /* m/s it slides for each m/s over that */
+#define BLOW_HELD     1.5f  /* s: worked on this recently, it's held */
+
+static bool notIt(const Object *o, const void *tent) { return o == tent || o->is_character || !o->is_static; }
+
+static void blowAbout(World *world, Object *tent, float dt) {
+    if (tent->tent_stage >= TENT_UP || (tent->tent_stage == TENT_PEGS && tent->stored * 2 >= TENT_PEGS(tent->variant)) ||
+        tent->tent_idle < BLOW_HELD) return;
+    float speed = Weather_windSpeed(world), from = tent->tent_stage == TENT_LAID ? BLOW_FLAT : BLOW_STANDING;
+    if (speed <= from) return;
+    Vec3 dir = vec3_scale(world->wind, 1.0f / speed);
+    float push = (speed - from) * BLOW_PUSH * (tent->tent_stage == TENT_LAID ? 0.5f : 1.0f);
+    float reach = 0.5f * fmaxf(TENT_W(tent->variant), TENT_L(tent->variant)) + 0.15f;
+    Vec3 from_p = vec3_add(tent->position, vec3(0.0f, 0.3f, 0.0f));
+    if (World_rayCast(world, from_p, dir, reach, notIt, tent, NULL, NULL) >= 0.0f) return; /* (snagged on something) */
+    tent->position = vec3_add(tent->position, vec3_scale(dir, push * dt));
+    tent->position.y = World_groundHeight(world, tent->position.x, tent->position.z, tent->position.y + 0.5f);
+    Object_setRotation(tent, vec3(0.0f, tent->rotation.y + 0.06f * push * sinf(tent->position.x * 1.7f + tent->position.z) * dt, 0.0f));
+    Object_syncColliders(tent);
+    world->tent_blowing = tent;
+}
+
 #define RAIN_NAP_END 0.15f /* rain: under this, out of the tent again */
 
 void Tent_update(World *world, float dt) {
+    world->tent_blowing = NULL;
     for (int i = 0; i < world->count; i++) {
         Object *o = world->objects[i];
         if (o->type == OBJ_TENT) {
             o->tent_idle += dt;
             if (o->tent_down && !o->broken && o->tent_stage == TENT_LAID && o->tent_work >= 1.0f) pack(world, o);
+            else if (!o->broken) blowAbout(world, o, dt);
             continue;
         }
         if (o->rain_nap && (!o->body.asleep || world->rain < RAIN_NAP_END)) { /* in out of the rain: up when it stops */
diff --git a/entropie/src/world/weather.c b/entropie/src/world/weather.c
index 8ec6206..5114316 100644
--- a/entropie/src/world/weather.c
+++ b/entropie/src/world/weather.c
@@ -22,6 +22,10 @@
 #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) */
+#define WIND_EASE     240.0f            /* game s: the wind picks up / dies down over about this */
+#define WIND_SPELL    (20.0f * 60.0f)   /* game s: it changes after this ... */
+#define WIND_SPELL_MORE (60.0f * 60.0f) /* ... up to this much later */
+#define GUST_RATE     0.45f             /* gusts come and go (real s, a slow beat of two sines) */
 
 static float dice(unsigned *rng) {
     *rng = *rng * 1103515245u + 12345u;
@@ -33,12 +37,50 @@ void Weather_init(World *world) {
     world->rain_left = FIRST_DRY;
     world->wet = 0.0f;
     world->weather = WEATHER_AUTO;
+    world->wind_kind = WIND_AUTO;
+    world->wind = vec3_zero();
+    world->wind_goal = world->wind_level = 0.08f;
+    world->wind_angle = 0.6f;
+    world->wind_left = WIND_SPELL;
+    world->gust = world->gust_t = 0.0f;
     world->flash = world->thunder_in = 0.0f;
     world->thunder = false;
     world->drops = calloc(1, sizeof(*world->drops));
     if (world->drops) world->drops->rng = 0x5eed1234u;
 }
 
+const char *Weather_windName(WindKind kind) {
+    static const char *const NAME[WIND_KINDS] = { "on its own", "calm", "a breeze", "a storm" };
+    return NAME[(unsigned)kind < WIND_KINDS ? kind : 0];
+}
+
+float Weather_windSpeed(const World *world) {
+    return hypotf(world->wind.x, world->wind.z);
+}
+
+/* The wind: calm spells and windy ones (windier with rain coming), turning now
+   and then; gusts on top of it. */
+static void blow(World *world, float dt, float game_dt) {
+    unsigned *rng = world->drops ? &world->drops->rng : &world->rng;
+    if (world->wind_kind == WIND_CALM) world->wind_goal = 0.03f;
+    else if (world->wind_kind == WIND_BREEZE) world->wind_goal = 0.3f;
+    else if (world->wind_kind == WIND_STORM) world->wind_goal = 0.85f;
+    else if ((world->wind_left -= game_dt) <= 0.0f) {
+        world->wind_left = WIND_SPELL + WIND_SPELL_MORE * dice(rng);
+        float d = dice(rng);
+        world->wind_goal = 0.04f + 0.35f * d * d + 0.45f * world->rain_goal * dice(rng);
+        world->wind_angle += (dice(rng) - 0.5f) * 1.6f; /* (turning a bit) */
+    }
+    if (world->wind_kind == WIND_AUTO && world->rain > STORM_RAIN) world->wind_goal = fmaxf(world->wind_goal, 0.55f); /* (a thunderstorm: windy) */
+    world->wind_level += (world->wind_goal - world->wind_level) * fminf(1.0f, game_dt / WIND_EASE);
+    world->gust_t += dt * GUST_RATE;
+    float beat = 0.5f + 0.5f * sinf(world->gust_t * 2.3f) * sinf(world->gust_t * 0.71f + 1.3f);
+    world->gust = clampf(beat * beat * 1.6f - 0.25f, 0.0f, 1.0f) * world->wind_level;
+    float speed = WIND_MAX * world->wind_level * (0.7f + 0.3f * sinf(world->gust_t * 3.1f)) + 0.6f * WIND_MAX * world->gust;
+    float a = world->wind_angle + 0.15f * sinf(world->gust_t * 1.7f);
+    world->wind = vec3(cosf(a) * speed, 0.0f, sinf(a) * speed);
+}
+
 const char *Weather_name(WeatherKind kind) {
     static const char *const NAME[WEATHER_KINDS] = { "on its own", "dry", "a shower", "pouring" };
     return NAME[(unsigned)kind < WEATHER_KINDS ? kind : 0];
@@ -77,6 +119,7 @@ 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);
+    blow(world, dt, game_dt);
     /* a thunderstorm: lightning, the thunder a few seconds after it */
     world->thunder = false;
     world->flash = fmaxf(0.0f, world->flash - FLASH_FADE * dt);
@@ -108,7 +151,8 @@ void Weather_update(World *world, float dt, float game_dt) {
         float dx = p->x - world->eye.x, dz = p->z - world->eye.z;
         if (p->y == 0.0f && p->x == 0.0f) { respawn(world, k, true); continue; }
         p->y -= DROP_SPEED * dt;
-        p->x += 0.6f * dt; /* (a little wind) */
+        p->x += world->wind.x * dt; /* (blown along) */
+        p->z += world->wind.z * dt;
         float ground = world->has_terrain ? Terrain_height(&world->terrain, p->x, p->z) : 0.0f;
         if (p->y < ground || dx * dx + dz * dz > DROP_RADIUS * DROP_RADIUS * 1.3f) respawn(world, k, p->y >= ground);
     }
@@ -124,7 +168,8 @@ void Weather_draw(World *world, Renderer *renderer) {
             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);
+    Vec3 lean = vec3(-world->wind.x / DROP_SPEED, 0.0f, -world->wind.z / DROP_SPEED); /* (slanting into where it comes from) */
+    Mesh mesh = Shapes_rain(shown, n, DROP_LEN, lean);
     Renderer_drawSmoke(renderer, &mesh, mat4_identity(), vec3(1.0f, 1.0f, 1.0f));
     Mesh_destroy(&mesh);
 }
diff --git a/entropie/src/world/weather.h b/entropie/src/world/weather.h
index a29601c..c01ad9e 100644
--- a/entropie/src/world/weather.h
+++ b/entropie/src/world/weather.h
@@ -12,6 +12,9 @@
 #include "world/world.h"
 
 typedef enum { WEATHER_AUTO, WEATHER_DRY, WEATHER_SHOWER, WEATHER_RAIN, WEATHER_KINDS } WeatherKind;
+typedef enum { WIND_AUTO, WIND_CALM, WIND_BREEZE, WIND_STORM, WIND_KINDS } WindKind;
+
+#define WIND_MAX 14.0f /* m/s: the windiest it gets (gusts on top) */
 
 #define RAIN_DROPS 1400
 
@@ -28,5 +31,8 @@ void        Weather_draw(World *world, Renderer *renderer);
 /* Is there a roof (anything) over this spot? */
 bool        Weather_covered(const World *world, Vec3 p);
 const char *Weather_name(WeatherKind kind);
+const char *Weather_windName(WindKind kind);
+/* m/s the wind blows now (gusts and all). */
+float       Weather_windSpeed(const World *world);
 
 #endif /* WORLD_WEATHER_H */
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 6f0b529..658a95b 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -1334,7 +1334,14 @@ void World_update(World *world, float dt, float game_dt) {
             Chips_spray(&world->chips, w->point, vec3_add(out, vec3(0.0f, -0.4f, 0.0f)), (int)want, true);
         }
     }
-    Chips_update(&world->chips, &world->terrain, dt);
+    Chips_update(&world->chips, &world->terrain, world->wind, dt);
+    for (int i = 0; i < world->count; i++) { /* smoke off the fires (with the wind) */
+        Object *f = world->objects[i];
+        if (f->type != OBJ_FIREPLACE || !f->burning) continue;
+        if ((f->smoke_t -= dt) > 0.0f) continue;
+        f->smoke_t = 0.22f / fmaxf(0.4f, Camp_flameSize(f));
+        Chips_smoke(&world->chips, vec3_add(f->position, vec3(0.0f, 0.6f + 0.4f * Camp_flameSize(f), 0.0f)));
+    }
     {   /* woodpiles count what really lies on them; what fell off isn't on one */
         Object *piles[16];
         int np = 0;
@@ -1407,11 +1414,14 @@ static Mat4 scaled(Vec3 s) {
     return m;
 }
 
-/* Flames over a fire: they flicker, and grow with the wood in it. */
-static void drawFlames(Renderer *renderer, const Mesh *flames, Vec3 at, float width, float height, float spin) {
+/* Flames over a fire: they flicker, and grow with the wood in it; the wind bends them over. */
+static void drawFlames(Renderer *renderer, const Mesh *flames, Vec3 at, float width, float height, float spin, Vec3 wind) {
     float t = renderer->boil_time;
     float flicker = 0.85f + 0.3f * (0.5f + 0.5f * sinf(t * 23.0f + spin) * sinf(t * 7.3f + 1.0f));
-    Mat4 model = mat4_mul(mat4_translate(at), mat4_mul(mat4_rotate_y(t * 2.1f + spin), scaled(vec3(width, height * flicker, width))));
+    Mat4 lean = mat4_identity(); /* (sheared downwind: the tips go furthest) */
+    lean.m[4] = clampf(wind.x * 0.07f, -0.8f, 0.8f);
+    lean.m[6] = clampf(wind.z * 0.07f, -0.8f, 0.8f);
+    Mat4 model = mat4_mul(mat4_translate(at), mat4_mul(lean, mat4_mul(mat4_rotate_y(t * 2.1f + spin), scaled(vec3(width, height * flicker, width)))));
     Renderer_drawMesh(renderer, flames, model);
 }
 
@@ -1495,7 +1505,7 @@ void World_draw(World *world, Renderer *renderer, bool draw_player) {
             if (obj->on_fire > 0.0f) {
                 bool down = Body_isDown(&obj->body);
                 drawFlames(renderer, &world->flames, vec3_add(obj->position, vec3(0.0f, down ? 0.0f : 0.3f, 0.0f)),
-                           down ? 1.3f : 0.8f, down ? 0.9f : 2.0f, (float)i);
+                           down ? 1.3f : 0.8f, down ? 0.9f : 2.0f, (float)i, world->wind);
             }
         } else if (obj->type == OBJ_CAR && !draw_player && playerIn(world, obj)) { /* (from the driver's seat: inside it) */
             Renderer_drawMesh(renderer, &obj->inside, Object_modelMatrix(obj));
@@ -1526,7 +1536,7 @@ void World_draw(World *world, Renderer *renderer, bool draw_player) {
             if (obj->burning) { /* more wood: bigger, and taller still */
                 float size = Camp_flameSize(obj), width = size * obj->size.x / 1.4f;
                 drawFlames(renderer, &obj->flames, vec3_add(obj->position, vec3(0.0f, 0.05f, 0.0f)), width,
-                           width * (0.85f + 0.2f * size), 0.0f);
+                           width * (0.85f + 0.2f * size), 0.0f, world->wind);
             }
         }
     }
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index aab3899..276a059 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -36,6 +36,14 @@ typedef struct World {
     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     wind;         /* m/s the air moves now (gusts and all; flat) ... */
+    float    wind_goal;    /* ... 0..1 how windy it's getting (x WIND_MAX m/s) ... */
+    float    wind_level;   /* ... how windy it is (eases to the goal) ... */
+    float    wind_angle;   /* ... where it blows to (wanders) ... */
+    float    wind_left;    /* ... game s till it changes ... */
+    float    gust, gust_t; /* ... a gust now (0..1), and its clock */
+    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) */
     struct RainDrops *drops;
 } World;