Nimbin[12]?Games / Entropie / commits / 634202c

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.37.0: umbrellas inside out, hats

Christian Immanuel · 2026-10-27 08:45 · 634202c7c06f10829d7d38cbb63f10eb39cfc93b

- A strong gust can blow an open umbrella inside out for a moment; the
  third time (or in a storm) it's broken for good
- Hats: Toni and Kurti wear caps, Jonas a beanie. The wind can take a cap
  off; it bowls along the ground, they chase it and put it back on
  (pulled down tighter each time)
 commit_message.txt                  |  15 +++---
 entropie/Makefile                   |   2 +-
 entropie/README.md                  |   2 +-
 entropie/STATE.md                   |  16 +++++-
 entropie/assets/shaders/figure.frag |  32 ++++++++++--
 entropie/commit_message.txt         |  15 +++---
 entropie/src/game/save.c            |   4 +-
 entropie/src/people/ai.c            |   4 +-
 entropie/src/render/figure.c        |   6 +++
 entropie/src/render/figure.h        |   5 ++
 entropie/src/render/renderer.c      |   4 ++
 entropie/src/render/renderer.h      |   2 +-
 entropie/src/render/shapes.h        |   6 ++-
 entropie/src/render/shapes_props.c  |  27 ++++++++--
 entropie/src/world/object.c         |  27 ++++++++--
 entropie/src/world/object.h         |   7 ++-
 entropie/src/world/scene.c          |   4 ++
 entropie/src/world/world.c          | 101 ++++++++++++++++++++++++++++++++++--
 18 files changed, 236 insertions(+), 43 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index cbec05c..a418266 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,10 +1,7 @@
-entropie 2.36.0: rain together, steady view on slopes
+entropie 2.37.0: umbrellas inside out, hats
 
-- No more shaking view walking down steep slopes (the bank by your garages)
-- The pullover tied round the waist no longer goes through the legs
-- Umbrellas held properly: open, up in front of the shoulder with the shaft
-  leaning over the head; folded, carried by the middle, tip down
-- People without an umbrella ask to come under someone's (yours too) and
-  walk along side by side; the umbrella leans over both
-- In hard rain the guests sit it out in your garages' kitchen (chairs, a
-  new log bench, or the floor) and get up when it stops
+- A strong gust can blow an open umbrella inside out for a moment; the
+  third time (or in a storm) it's broken for good
+- Hats: Toni and Kurti wear caps, Jonas a beanie. The wind can take a cap
+  off; it bowls along the ground, they chase it and put it back on
+  (pulled down tighter each time)
diff --git a/entropie/Makefile b/entropie/Makefile
index 537b19e..0c05bb2 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.36.0
+VERSION   := 2.37.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 5baee17..3fc8f72 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -124,5 +124,5 @@ Dependencies point downward: `main → game → world/people → render → core
 
 ## Version
 
-`make version` prints e.g. `entropie 2.36.0 (build 25ed4f6)`. The build id is
+`make version` prints e.g. `entropie 2.37.0 (build 1a6ad0e)`. The build id is
 an md5 of all sources and assets.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 125b23a..bffff4d 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -286,6 +286,13 @@ Painted in the rotoscope style of *Waking Life* (2001):
   it starts to rain (if they're not busy working) and stay out under it; the
   careful put it back after. Open, it's held up in front of the shoulder, the
   shaft leaning in over the head; folded, carried by the middle, tip down
+- A strong gust may blow an open umbrella inside out (no use for a moment,
+  then pushed back right); the third time, or in a storm, the ribs bend and
+  it's broken (the others drop theirs and get in out of the rain)
+- Some wear a hat: Toni a red cap, Kurti an old green one, Jonas a beanie.
+  A gust can take a cap off (a beanie hardly ever, none under a hood): it
+  bowls along the ground, they run after it and put it back on - pulled down
+  tighter each time
 - No umbrella of their own: somebody near under one gets asked ("Room under
   there for me?") and they walk along side by side under it (yours too); it
   leans over the two of them
@@ -1218,6 +1225,11 @@ Round 2.36 (rain together, view on the slope):
 - Sharing one (`ai.c` umbrellaToShare / walkUnder): out in the rain without one, somebody within SHARE_SEE 12 m holding one up with nobody under it: asked, then along at their side (SHARE_GAP 0.53 m, the umbrella side), keeping pace (close: stepping straight onto the spot, no circling), for up to SHARE_MAX 4 min; the holder's `umbrella_guest` leans the canopy over both; `world.c` underUmbrella counts a head within 0.9 x its radius under anybody's open umbrella as dry. Checked: under yours, standing and walking: 0.6 m beside you, dry.
 - Sitting out heavy rain (`ai.c` rainRoom, `Site_rainRoom`): the guests go to the middle of your garages' kitchen; from RAIN_SIT 0.45 they sit down on a free chair or the new log bench by its back wall (else the floor) and stay till the rain is nearly over (half of SHELTER_RAIN). Checked: two on the chairs at the table for 100 s of hard rain, up again when it stopped.
 
+Round 2.37 (umbrellas blown inside out, hats):
+- Inside out (`world.c` windUmbrella): an open umbrella held in a gust (FLIP_GUST 0.4) over FLIP_WIND 10 m/s: FLIP_RATE 0.3/s for every 3 m/s over it x gust x carelessness (drunk, careless more) -> `umbrella_flip` FLIP_TIME 2.2 s inside out (`Shapes_umbrella` UMBRELLA_INSIDE_OUT: the rim up above the tip, ragged; no cover meanwhile), a rustle. The FLIP_RUIN 3rd time, or over RUIN_WIND 16 m/s: `umbrella_ruined` (stays inside out, never opens; saved; NPCs drop it, don't fetch it, shelter). Checked: at wind level 0.7 in the rain - flips at 28 and 47 s, broken at 126 s; dry under it otherwise.
+- Hats (`FigureLook.hat` HAT_CAP / HAT_BEANIE, `hat_color`; `figure.frag` hatDistance: a cap's crown and a brim tipped down over the brow, a beanie down to the ears with a knot and ribs; under a hood: none): Toni a red cap, Kurti an old green one, Jonas a blue beanie (`Figure_wardrobe` by index % 7: 3, 4, 6). Saved with the person.
+- Blown off (`world.c` windHat): in a gust (HAT_GUST 0.4) over HAT_WIND 9 m/s, HAT_RATE 0.3/s for every 3 m/s over it x gust (a beanie a tenth), divided by 1 + 3 x `hats_lost` (pulled down tighter each time): an OBJ_HAT (`Shapes_hat`, its colour, `owner`) flies off with the wind and bowls along like paper (`windBlows`); NPCs go after it (`ai.blown`, as for paper); picked up by its owner it goes back on (`hatBackOn`). Checked: Toni at wind level 0.8 - off, chased 7-14 m, back on in 3-14 s; less often each time.
+
 ## Open tasks
 
 Most important first.
@@ -1240,7 +1252,7 @@ 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: hats (nobody wears one yet) blown off; umbrellas while working (now only idle people fetch one); an umbrella blown inside out; more roofed places to sit out the rain together (outdoors: a shelter, a pavilion - `Site_rainRoom` is the one place now)
+- Weather, still to do: umbrellas while working (now only idle people fetch one); more roofed places to sit out the rain together (outdoors: a shelter, a pavilion - `Site_rainRoom` is the one place now)
 - Sound, still to do: the house's doors (they have no doors that open yet)
 
 **Saves, still to do**
@@ -1253,4 +1265,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.36.0 (build 25ed4f6)`
+State of `entropie 2.37.0 (build 1a6ad0e)`
diff --git a/entropie/assets/shaders/figure.frag b/entropie/assets/shaders/figure.frag
index 3debd18..f247e7f 100644
--- a/entropie/assets/shaders/figure.frag
+++ b/entropie/assets/shaders/figure.frag
@@ -43,6 +43,8 @@ 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 int   hat;          // 0 none, 1 a cap (a brim in front), 2 a beanie (knitted, a turned-up cuff)
+uniform vec3  hatColor;
 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;
@@ -66,7 +68,7 @@ uniform float boilTime;
 uniform sampler2D shadowMap;
 
 const int M_SKIN = 0, M_HAIR = 1, M_SHIRT = 2, M_PANTS = 3, M_SHOES = 4, M_PEE = 5, M_SICK = 6, M_WATER = 7, M_CIG = 8, M_JACKET = 9, M_URINELLA = 10;
-const int M_FUR = 11, M_FUR_DARK = 12, M_NOSE = 13;
+const int M_FUR = 11, M_FUR_DARK = 12, M_NOSE = 13, M_HAT = 14;
 
 const float FIGURE_LIGHT_WRAP = 0.6;  // soft, flat faces instead of a hard light split
 const float SELF_SHADOW_SKIP  = 0.10; // meters
@@ -297,6 +299,23 @@ float hairDistance(vec3 world_p) {
     return d;
 }
 
+// A hat on the head (head-local): a cap's crown and its brim out in front, or a beanie down to the ears.
+float hatDistance(vec3 world_p) {
+    vec3 p = toHead(world_p);
+    if (hat == 2) {
+        float crown = length(p - vec3(0.0, 0.028, -0.014)) - 0.112;
+        float rim = dot(p - vec3(0.0, -0.012, 0.0), normalize(vec3(0.0, -1.0, 0.25))); // (the cuff's lower edge, a bit lower behind)
+        float top = length(p - vec3(0.0, 0.128, -0.02)) - 0.018;                       // (a little knot on top)
+        return min(max(crown, rim), top);
+    }
+    float crown = max(length(p - vec3(0.0, 0.026, -0.012)) - 0.108, dot(p - vec3(0.0, 0.020, 0.0), normalize(vec3(0.0, -1.0, 0.15))));
+    vec3 b = p - vec3(0.0, 0.026, 0.115);                                                  // the brim: a thin slab out over the brow
+    b.y += 0.12 * b.z;                                                                     // (tipped down a little)
+    vec3 q = abs(b) - vec3(0.075, 0.004, 0.055);
+    float brim = length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - 0.004;
+    return min(crown, brim);
+}
+
 // 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);
@@ -322,6 +341,11 @@ float body(vec3 p, out int mat) {
         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);
+        if (hat > 0 && !hood) { // a hat over the hair
+            float dhat = hatDistance(p);
+            if (dhat < min(nearest, dh) + 0.003) mat = M_HAT;
+            d = min(d, dhat);
+        }
     }
     return d;
 }
@@ -411,6 +435,7 @@ vec3 clothDetail(int mat, vec3 base, vec3 p) {
 
 vec3 materialColor(int mat) {
     if (mat == M_HAIR)  return hairColor;
+    if (mat == M_HAT)   return hatColor;
     if (mat == M_SHIRT) return shirtColor;
     if (mat == M_PANTS) return pantsColor;
     if (mat == M_SHOES) return shoeColor;
@@ -470,7 +495,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 || hood ? 0.22 : 0.15);
+    hairCandidate = species == 0 && length(cross(head - origin, ray)) < (hairStyle == 1 || hood ? 0.22 : hat == 1 ? 0.2 : 0.15);
     if (candidates == 0 && !hairCandidate) discard;
 
     // March the ray through the body
@@ -505,7 +530,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))
+    if (mat == M_HAT && hat == 2) base *= 0.9 + 0.1 * step(0.5, fract(dot(toHead(p), vec3(60.0, 0.0, 0.0)) + 0.25)); // (knitted ribs)
+    if (species == 0 && (mat == M_SHIRT || mat == M_PANTS || mat == M_JACKET || mat == M_HAIR || mat == M_SHOES || mat == M_HAT))
         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];
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index cbec05c..a418266 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,10 +1,7 @@
-entropie 2.36.0: rain together, steady view on slopes
+entropie 2.37.0: umbrellas inside out, hats
 
-- No more shaking view walking down steep slopes (the bank by your garages)
-- The pullover tied round the waist no longer goes through the legs
-- Umbrellas held properly: open, up in front of the shoulder with the shaft
-  leaning over the head; folded, carried by the middle, tip down
-- People without an umbrella ask to come under someone's (yours too) and
-  walk along side by side; the umbrella leans over both
-- In hard rain the guests sit it out in your garages' kitchen (chairs, a
-  new log bench, or the floor) and get up when it stops
+- A strong gust can blow an open umbrella inside out for a moment; the
+  third time (or in a storm) it's broken for good
+- Hats: Toni and Kurti wear caps, Jonas a beanie. The wind can take a cap
+  off; it bowls along the ground, they chase it and put it back on
+  (pulled down tighter each time)
diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c
index 27cb1c5..76fcde3 100644
--- a/entropie/src/game/save.c
+++ b/entropie/src/game/save.c
@@ -43,7 +43,7 @@ static const Field THING[] = {
     FL("wake", wake), IN("stored", stored), FL("work", work), FL("cut_at", cut_at), FL("mark", mark), VE("mark_at", mark_at),
     VE("mark_out", mark_out), IN("branches", branches), BO("trunk_cut", trunk_cut), BO("toppled", toppled), FL("topple", topple),
     VE("fall_dir", fall_dir), FL("fall_base_y", fall_base_y), VE("fall_origin", fall_origin), FL("fall_ground_y", fall_ground_y),
-    FL("fall_slide", fall_slide), VE("fell_plan", fell_plan), BO("fell_blocked", fell_blocked), BO("keep", keep), FL("charred", charred),
+    FL("fall_slide", fall_slide), VE("fell_plan", fell_plan), BO("fell_blocked", fell_blocked), BO("keep", keep), FL("charred", charred), BO("umbrella_ruined", umbrella_ruined), VE("hat_tint", look.hat_color), IN("umbrella_flips", umbrella_flips),
     FL("glow", glow), FL("fuel", fuel), BO("burning", burning), FL("water", water), FL("grill", grill), BO("grilled", grilled),
     BO("burnt", burnt), FL("extend", extend), FL("raised", raised), BO("leaning", leaning), VE("lean_normal", lean_normal),
     FL("lean_top", lean_top), FL("stability", stability), IN("crate_empties", crate_empties), IN("tent_stage", tent_stage),
@@ -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("hoodie", look.hoodie),
+    BO("keeps_shirt", look.keeps_shirt), FL("wet", look.wet), BO("hoodie", look.hoodie), IN("hat", look.hat), IN("hats_lost", hats_lost), VE("hat_color", look.hat_color),
     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),
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 2b0ca3f..e7e46cb 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -2062,7 +2062,7 @@ static bool goInside(Object *self, Object *const *objects, int count, float dt)
 
 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;
+        if (self->hands.item[h] && self->hands.item[h]->type == OBJ_UMBRELLA && !self->hands.item[h]->umbrella_ruined && self->hands.state[h] == HAND_HOLDING) return true;
     return false;
 }
 
@@ -2230,7 +2230,7 @@ static bool shelter(Object *self, Object *const *objects, int count, float dt) {
             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;
+                if (o->type != OBJ_UMBRELLA || o->is_held || o->umbrella_ruined || 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);
diff --git a/entropie/src/render/figure.c b/entropie/src/render/figure.c
index 3d1ae76..9532c29 100644
--- a/entropie/src/render/figure.c
+++ b/entropie/src/render/figure.c
@@ -27,6 +27,12 @@ FigureLook Figure_wardrobe(int index) {
     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) */
+    switch (((index % 7) + 7) % 7) { /* hats: Toni a red cap, Kurti an old green one, Jonas a beanie (and so on for more people) */
+    case 3: look.hat = HAT_CAP;    look.hat_color = vec3(0.72f, 0.13f, 0.12f); break;
+    case 4: look.hat = HAT_CAP;    look.hat_color = vec3(0.26f, 0.32f, 0.22f); break;
+    case 6: look.hat = HAT_BEANIE; look.hat_color = vec3(0.20f, 0.28f, 0.46f); break;
+    default: break;
+    }
     return look;
 }
 
diff --git a/entropie/src/render/figure.h b/entropie/src/render/figure.h
index e5492c5..9ff8823 100644
--- a/entropie/src/render/figure.h
+++ b/entropie/src/render/figure.h
@@ -35,10 +35,15 @@ typedef struct {
     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) */
+    int       hat;           /* HAT_*: on the head (off under a hood) ... */
+    Vec3      hat_color;     /* ... its colour */
 } FigureLook;
 
 enum { TOP_SHIRT, TOP_BARE, TOP_CROP, TOP_JACKET };
 
+/* A cap (a brim in front: the wind takes it easily) or a knitted beanie (stays on). */
+enum { HAT_NONE, HAT_CAP, HAT_BEANIE };
+
 /* Trousers (pants colour), shorts (knee-length), a skirt (pants colour) or a dress (shirt colour). */
 enum { GARMENT_TROUSERS, GARMENT_SHORTS, GARMENT_SKIRT, GARMENT_DRESS };
 
diff --git a/entropie/src/render/renderer.c b/entropie/src/render/renderer.c
index e8a86e4..0822181 100644
--- a/entropie/src/render/renderer.c
+++ b/entropie/src/render/renderer.c
@@ -108,6 +108,8 @@ static bool loadShaders(Renderer *r) {
     f->belly           = glGetUniformLocation(f->program, "belly");
     f->wet             = glGetUniformLocation(f->program, "soaked");
     f->hood            = glGetUniformLocation(f->program, "hood");
+    f->hat             = glGetUniformLocation(f->program, "hat");
+    f->hat_color       = glGetUniformLocation(f->program, "hatColor");
     f->cig_a           = glGetUniformLocation(f->program, "cigA");
     f->uri_a           = glGetUniformLocation(f->program, "uriA");
     f->uri_b           = glGetUniformLocation(f->program, "uriB");
@@ -575,6 +577,8 @@ void Renderer_drawFigure(Renderer *r, const FigurePose *pose) {
     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);
+    glUniform1i(f->hat, pose->look.hat);
+    glUniform3f(f->hat_color, pose->look.hat_color.x, pose->look.hat_color.y, pose->look.hat_color.z);
     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 5f62c53..9bb0bd5 100644
--- a/entropie/src/render/renderer.h
+++ b/entropie/src/render/renderer.h
@@ -72,7 +72,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, hood;
+    GLint  garment, undress, top, jacket_tied, belly, wet, hood, hat, hat_color;
     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/render/shapes.h b/entropie/src/render/shapes.h
index f2cb342..198f1d4 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -152,7 +152,11 @@ enum { BOTTLE_KIND_NONE, BOTTLE_KIND_BEER, BOTTLE_KIND_WATER, BOTTLE_KIND_COLA,
 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);
+/* shape: UMBRELLA_FOLDED, UMBRELLA_OPEN, UMBRELLA_INSIDE_OUT (blown: the canopy up like a cup, ribs bent) */
+enum { UMBRELLA_FOLDED, UMBRELLA_OPEN, UMBRELLA_INSIDE_OUT };
+Mesh  Shapes_umbrella(float length, float radius, int shape, Vec3 color);
+/* A hat lying about (HAT_CAP, HAT_BEANIE), its crown up, the cap's brim along +Z. */
+Mesh  Shapes_hat(int kind, 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 6ff81e7..7e62f17 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -1073,7 +1073,8 @@ Mesh Shapes_crate(int variant, const unsigned char *slots, int n) {
     return Mesh_fromBuilder(&b);
 }
 
-Mesh Shapes_umbrella(float length, float radius, bool open, Vec3 color) {
+Mesh Shapes_umbrella(float length, float radius, int shape, Vec3 color) {
+    const bool open = shape != UMBRELLA_FOLDED, inside_out = shape == UMBRELLA_INSIDE_OUT;
     MeshBuilder b;
     MeshBuilder_init(&b);
     const float half = length * 0.5f;
@@ -1091,8 +1092,11 @@ Mesh Shapes_umbrella(float length, float radius, bool open, Vec3 color) {
         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);
+            /* (inside out: the rim blown up above the tip, narrower, a bit ragged) */
+            float rr0 = inside_out ? radius * (0.8f + 0.1f * (float)(k % 3)) : radius, rr1 = inside_out ? radius * (0.8f + 0.1f * (float)((k + 1) % 3)) : radius;
+            float lift = inside_out ? 0.55f * radius : -0.2f * radius;
+            Vec3 r0 = vec3(cosf(a0) * rr0, half + lift + (inside_out ? 0.05f * (float)(k % 2) : 0.0f), sinf(a0) * rr0);
+            Vec3 r1 = vec3(cosf(a1) * rr1, half + lift + (inside_out ? 0.05f * (float)((k + 1) % 2) : 0.0f), sinf(a1) * rr1);
             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);
@@ -1108,6 +1112,23 @@ Mesh Shapes_umbrella(float length, float radius, bool open, Vec3 color) {
     return Mesh_fromBuilder(&b);
 }
 
+/* A hat: a dome of rings (a cap: a brim out in front; a beanie: taller, a cuff). */
+Mesh Shapes_hat(int kind, Vec3 color) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const bool beanie = kind == 2;
+    const float r = beanie ? 0.105f : 0.1f, h = beanie ? 0.12f : 0.085f;
+    const int rings = 5;
+    for (int k = 0; k < rings; k++) { /* the crown: a dome */
+        float a0 = 0.5f * PI_F * (float)k / (float)rings, a1 = 0.5f * PI_F * (float)(k + 1) / (float)rings;
+        shapes_addTube(&b, -0.04f + h * sinf(a0), r * cosf(a0), fmaxf(r * cosf(a1), 0.004f), h * (sinf(a1) - sinf(a0)), 12,
+                       vec3_scale(color, 0.92f + 0.08f * (float)k / (float)rings), k == rings - 1);
+    }
+    if (beanie) shapes_addTube(&b, -0.045f, r * 1.04f, r * 1.04f, 0.035f, 12, vec3_scale(color, 0.85f), false); /* the cuff */
+    else addBlock(&b, vec3(0.0f, -0.036f, r + 0.045f), vec3(0.15f, 0.008f, 0.11f), vec3_scale(color, 0.8f)); /* the brim */
+    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/world/object.c b/entropie/src/world/object.c
index f4821b4..ea64f3d 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -390,12 +390,20 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         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->mesh    = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, UMBRELLA_FOLDED, 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_HAT: /* (grey here: its colour once it's known - world.c, showProps) */
+        obj->size    = vec3(0.21f, 0.1f, 0.24f);
+        obj->weight  = 0.1f;
+        obj->variant = (int)weight == 2 ? 2 : 1;
+        obj->mesh    = Shapes_hat(obj->variant, vec3(0.5f, 0.5f, 0.5f));
+        addCollider(obj, vec3(0.0f, 0.0f, 0.02f), 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;
@@ -1369,10 +1377,11 @@ static const char *bottleName(const Object *b) {
 
 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;
+    int want = u->umbrella_flip > 0.0f || u->umbrella_ruined ? UMBRELLA_INSIDE_OUT : u->umbrella_open ? UMBRELLA_OPEN : UMBRELLA_FOLDED;
+    if (u->type != OBJ_UMBRELLA || u->shown == want) 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;
+    u->mesh = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, want, COLORS[u->variant % 3]);
+    u->shown = want;
 }
 
 void Object_setDrink(Object *b, int drink) {
@@ -1495,8 +1504,16 @@ 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_HAT: {
+        const char *what = obj->variant == 2 ? "beanie" : "cap";
+        if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s - pick it up to put it back on", what);
+        else if (obj->owner) snprintf(out, size, "%s's %s (blown off)", obj->owner->name, what);
+        else snprintf(out, size, "A %s", what);
+        break;
+    }
     case OBJ_UMBRELLA:
-        snprintf(out, size, obj->umbrella_open ? "Umbrella - open" : "Umbrella (carried, it opens in the rain)");
+        snprintf(out, size, obj->umbrella_ruined ? "A broken umbrella - blown inside out, the ribs bent" : obj->umbrella_flip > 0.0f ? "Umbrella - blown inside out!" :
+                            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)" };
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index e6e58cd..0f63d48 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -88,6 +88,7 @@ typedef enum {
     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 */
+    OBJ_HAT,             /* a hat off somebody's head (blown off): variant = HAT_*, look.hat_color, owner = whose */
 } ObjectType;
 
 /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */
@@ -189,7 +190,11 @@ typedef struct Object {
     unsigned char crate_slot[CRATE_SLOTS];
     struct Object *umbrella_guest; /* people: somebody under their umbrella with them (the canopy leans their way) ... */
     float      umbrella_guest_t;   /* ... kept up by them this long */
-    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_*) */
+    bool       umbrella_open; /* umbrellas: open (held up in the rain) */
+    float      umbrella_flip;   /* umbrellas: s left blown inside out by a gust (0: as it should be) ... */
+    int        umbrella_flips;  /* ... how often that happened ... */
+    int        hats_lost;       /* people: how often the wind took their cap (they pull it down tighter each time) */
+    bool       umbrella_ruined; /* ... once too often (or in a storm): the ribs bent, it's no use any more */ /* ... 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 */
diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c
index de04371..ac46e75 100644
--- a/entropie/src/world/scene.c
+++ b/entropie/src/world/scene.c
@@ -243,9 +243,13 @@ Object *Scene_load(World *world) {
             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;
+                int hat = obj->look.hat;
+                Vec3 hat_color = obj->look.hat_color;
                 obj->look = LOOKS[characters];
                 obj->look.jacket = jacket;
                 obj->look.hoodie = hoodie;
+                obj->look.hat = hat;
+                obj->look.hat_color = hat_color;
             }
             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/world.c b/entropie/src/world/world.c
index 425f2a6..12e531a 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -776,9 +776,9 @@ static bool underUmbrella(World *world, Object *o, float dt) {
     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 &&
+        u->umbrella_open = world->rain > 0.05f && !o->in_tent && o->indoors <= 0.0f && !o->riding && !u->umbrella_ruined &&
                            !Weather_covered(world, vec3_add(o->position, vec3(0.0f, o->look.height + 0.3f, 0.0f)));
-        over = over || u->umbrella_open;
+        over = over || (u->umbrella_open && u->umbrella_flip <= 0.0f); /* (blown inside out: no use a moment) */
     }
     if (over || o->is_held) return over;
     /* under somebody else's (walking along with them) */
@@ -787,7 +787,7 @@ static bool underUmbrella(World *world, Object *o, float dt) {
         if (p == o || !p->is_character) continue;
         for (int h = 0; h < 2 && !over; h++) {
             const Object *u = p->hands.item[h];
-            if (!u || u->type != OBJ_UMBRELLA || !u->umbrella_open || p->hands.state[h] != HAND_HOLDING) continue;
+            if (!u || u->type != OBJ_UMBRELLA || !u->umbrella_open || u->umbrella_flip > 0.0f || p->hands.state[h] != HAND_HOLDING) continue;
             Vec3 top = vec3_add(Object_colliderCenter(u, 0), vec3_scale(u->colliders[0].axes[1], UMBRELLA_LENGTH * 0.5f));
             float dx = top.x - o->position.x, dz = top.z - o->position.z;
             over = dx * dx + dz * dz < (UMBRELLA_COVERS * UMBRELLA_RADIUS) * (UMBRELLA_COVERS * UMBRELLA_RADIUS) && top.y > o->position.y + o->look.height;
@@ -889,6 +889,12 @@ static void showProps(World *world) {
             if (open != f->heard_open) World_noise(world, f->type == OBJ_CAR ? NOISE_SLAM : NOISE_CREAK, f->position, 0.8f);
             f->heard_open = open;
         }
+        if (f->type == OBJ_HAT && !f->broken && f->shown != 1) { /* (its look: the colour it had on the head) */
+            Mesh_destroy(&f->mesh);
+            f->mesh = Shapes_hat(f->variant, f->look.hat_color);
+            f->shown = 1;
+            continue;
+        }
         if (f->type == OBJ_UMBRELLA) { /* (folded once it's put down) */
             if (!f->is_held) f->umbrella_open = false;
             Object_showUmbrella(f);
@@ -1379,6 +1385,7 @@ static void emptyTray(World *world, Object *o) {
 #define BLOW_SHOVE   0.9f /* m/s^2 for each m/s over it (against the ground's grip) */
 
 static bool paperLike(const Object *o) {
+    if (o->type == OBJ_HAT) return true; /* (light, and a cap catches the wind: it bowls along) */
     return o->type == OBJ_TRASH && o->size.x > 0.0f && ((o->variant & 3) == PALETTE_WARM || (o->variant & 3) == PALETTE_STANDARD);
 }
 
@@ -1422,6 +1429,91 @@ static void windSnatches(World *world, Object *o, float dt) {
     }
 }
 
+/* An open umbrella in a gust: blown inside out now and then (no use a moment, then
+   pushed back right); the third time, or in a storm, the ribs bend and it's done for. */
+#define FLIP_WIND  10.0f /* m/s in a gust: an open umbrella may blow inside out ... */
+#define FLIP_GUST  0.4f  /* ... a gust this strong (0..1) ... */
+#define FLIP_RATE  0.3f  /* ... chance a second for every 3 m/s over it, in a full gust */
+#define FLIP_TIME  2.2f  /* s till it's pushed back the right way */
+#define FLIP_RUIN  3     /* blown inside out this often: broken */
+#define RUIN_WIND  16.0f /* m/s: a gust like that breaks it at once */
+
+static void windUmbrella(World *world, Object *o, float dt) {
+    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 || u->umbrella_ruined) continue;
+        if (u->umbrella_flip > 0.0f) {
+            if ((u->umbrella_flip -= dt) <= 0.0f) {
+                u->umbrella_flip = 0.0f;
+                if (o->type != OBJ_PLAYER && dice(world) < 0.4f) Ai_say(o, "There - right way round again.");
+            }
+            continue;
+        }
+        if (!u->umbrella_open || world->gust < FLIP_GUST) continue;
+        float speed = hypotf(world->wind.x, world->wind.z), over = speed - FLIP_WIND;
+        if (over <= 0.0f) continue;
+        float careless = 0.6f + fminf(o->drunk, 1.0f) + 0.6f * (1.0f - Object_caution(o)); /* (the careful turn it into the wind) */
+        if (dice(world) >= FLIP_RATE * over / 3.0f * world->gust * careless * dt) continue;
+        u->umbrella_flip = FLIP_TIME;
+        u->umbrella_flips++;
+        World_noiseOn(world, NOISE_RUSTLE, u->position, 0.6f, 0); /* (the canopy snapping over) */
+        if (u->umbrella_flips >= FLIP_RUIN || speed > RUIN_WIND) {
+            u->umbrella_ruined = true;
+            u->umbrella_open = false;
+            if (o->type == OBJ_PLAYER) Ai_say(o, "The umbrella's had it - the ribs are bent.");
+            else { /* (no use: down it goes, and in out of the rain) */
+                Ai_say(o, dice(world) < 0.5f ? "Oh no - my umbrella's had it!" : "Great. Broken.");
+                Hands_drop(o, h);
+            }
+        } else {
+            Ai_say(o, dice(world) < 0.5f ? "Argh - inside out!" : "Whoa - the umbrella!");
+        }
+    }
+}
+
+/* A gust takes a cap off a head (a beanie hardly ever; not under a hood): it flies
+   off with the wind and bowls along the ground; whoever's it is goes after it.
+   Picked up by its owner, it goes back on. */
+#define HAT_WIND  9.0f  /* m/s in a gust: a cap may go from this ... */
+#define HAT_GUST  0.4f
+#define HAT_RATE  0.3f  /* ... chance a second for every 3 m/s over it, in a full gust (a beanie: a tenth) */
+
+static void windHat(World *world, Object *o, float dt) {
+    if (!o->look.hat || o->look.hood_up || o->indoors > 0.0f || o->in_tent || o->riding || world->gust < HAT_GUST) return;
+    float speed = hypotf(world->wind.x, world->wind.z), over = speed - HAT_WIND;
+    if (over <= 0.0f) return;
+    float rate = HAT_RATE * over / 3.0f * world->gust * (o->look.hat == HAT_BEANIE ? 0.1f : 1.0f) / (1.0f + 3.0f * (float)o->hats_lost); /* (lost it before: pulled down tight) */
+    if (dice(world) >= rate * dt) return;
+    Vec3 head = vec3_add(o->position, vec3(0.0f, o->look.height + 0.05f, 0.0f));
+    Object *hat = Object_create(OBJ_HAT, PALETTE_STANDARD, head, vec3(0.2f, 0.1f, 0.2f), (float)o->look.hat);
+    if (!hat) return;
+    hat->look.hat_color = o->look.hat_color;
+    hat->owner = o;
+    hat->rotation.y = o->rotation.y;
+    Object_syncColliders(hat);
+    if (!World_add(world, hat)) return;
+    hat->velocity = vec3(world->wind.x * 0.55f, 1.8f, world->wind.z * 0.55f);
+    hat->angular_velocity = vec3(3.0f, 1.0f, 2.0f);
+    hat->is_grounded = false;
+    o->look.hat = HAT_NONE;
+    o->hats_lost++;
+    if (o->type != OBJ_PLAYER) { o->ai.blown = hat; o->ai.blown_t = 0.0f; }
+    Ai_say(o, o->type == OBJ_PLAYER ? "My cap - the wind took it!" : dice(world) < 0.5f ? "Hey - my cap!" : "Oi! Come back here, cap!");
+}
+
+/* Their own hat in hand: back on the head. */
+static void hatBackOn(Object *o) {
+    for (int h = 0; h < 2; h++) {
+        Object *hat = o->hands.item[h];
+        if (!hat || hat->type != OBJ_HAT || hat->owner != o || o->look.hat) continue;
+        Hands_drop(o, h);
+        o->look.hat = hat->variant;
+        o->look.hat_color = hat->look.hat_color;
+        Object_useUp(hat);
+        if (o->type == OBJ_PLAYER) Ai_say(o, "Cap back on.");
+    }
+}
+
 void World_update(World *world, float dt, float game_dt) {
     Object_indexKinds(world->objects, world->count);
     world->dt_props = dt;
@@ -1479,6 +1571,9 @@ void World_update(World *world, float dt, float game_dt) {
         Object *o = world->objects[i];
         windBlows(world, o, dt);
         windSnatches(world, o, dt);
+        windUmbrella(world, o, dt);
+        windHat(world, o, dt);
+        hatBackOn(o);
         Physics_step(o, world->objects, world->count, &world->terrain, dt);
         landThrown(world, o);
     }