Nimbin[12]?Games / Entropie / commits / 80d00ec

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.4.0: pullovers instead of the coat rack

Christian Immanuel · 2026-10-20 03:03 · 80d00ec76a2d46be62b24e2fd3298c30bfc0f486

- The coat rack is gone (code too); the jacket is now a pullover
- Your pullover (B): put it on, take it off (tied round your hips), or put
  it down where you look: over a chair's back, a railing, a wall or a bench,
  folded on a table, or spread on the ground; click it to take it back
- Sitting on the ground: spread it out and sit on it (C at it sits on it)
- Others hang theirs over their chair or sit on it on the ground, take it
  along when they get up, and go and get it when it gets cold
- Cold: it goes on for you once, after that it's up to you; put down
  somewhere, you're told where it is
 entropie/ARCHITECTURE.md           |   4 +-
 entropie/Makefile                  |   2 +-
 entropie/README.md                 |  10 +-
 entropie/STATE.md                  |  16 ++-
 entropie/commit_message.txt        |  18 +--
 entropie/src/game/game.c           |  13 ++-
 entropie/src/game/game.h           |   2 +-
 entropie/src/game/game_actions.c   |  89 +++++++++++----
 entropie/src/people/ai.c           |  67 +++--------
 entropie/src/people/ai.h           |   3 +-
 entropie/src/render/shapes.h       |   8 +-
 entropie/src/render/shapes_props.c |  70 +++++++-----
 entropie/src/world/clothes.c       | 227 +++++++++++++++++++++++++++++++++++++
 entropie/src/world/clothes.h       |  42 +++++++
 entropie/src/world/object.c        |   9 +-
 entropie/src/world/object.h        |  13 ++-
 entropie/src/world/scene.c         |   1 -
 entropie/src/world/world.c         |  42 +++----
 18 files changed, 481 insertions(+), 155 deletions(-)

diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index cbf1100..e8ae814 100644
--- a/entropie/ARCHITECTURE.md
+++ b/entropie/ARCHITECTURE.md
@@ -10,7 +10,7 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL
 | `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input | core |
 | `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math |
 | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render |
-| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, water, grass, grid, day | everything but game |
+| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, clothes, water, grass, grid, day | everything but game |
 | `src/people/` | bodies (animation), hands, AI, talk, condition, dog, climbing | everything but game |
 | `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI | all |
 
@@ -23,7 +23,7 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL
 `Game_update` → input, actions, talk, player → `World_update`:
 physics for every object (near ones from the `Grid`) → grid rebuilt → felling and
 cutting → hands (pushing, carrying) → hose → AI → bodies animated → props
-(gates, chairs, ashtray, coat rack). Then the camera follows (`game.c`), and
+(gates, chairs, ashtray, pullovers put down). Then the camera follows (`game.c`), and
 `Game_render`: shadow pass, scene pass (terrain, objects, figures, grass, smoke),
 ink pass, UI.
 
diff --git a/entropie/Makefile b/entropie/Makefile
index 3a4a82d..cac588a 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.3.0
+VERSION   := 2.4.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index d8ffccc..89a05b9 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -516,9 +516,13 @@ say no more often and don't come to you for a bit.
   goes back when they think of it; owners ask for theirs
 - Working hard in the midday sun the shirt comes off (women keep a top on;
   the shy keep it all on), yours too; it goes back on when cooled down
-- Cold nights: away from the fire a jacket goes on (each their own colour),
-  by the fire or in the day it comes off and gets tied round the waist;
-  everybody's jacket is with them (the coat rack is gone)
+- Cold nights: away from the fire the pullover goes on (each their own colour),
+  by the fire or in the day it comes off and gets tied round the waist
+- Your pullover (B, your pockets): put it on, take it off (round your hips) or
+  put it down where you look: over a chair's back, a railing or a bench, on
+  a table, or spread on the ground. Sitting on the ground: spread it out and
+  sit on it. Click it to take it back. Others hang theirs over their chair or
+  sit on it, take it along when they get up, and fetch it when it gets cold
 - Ask them what they're doing, send them back to helping you, or give them a break
 
 ## Smoking
diff --git a/entropie/STATE.md b/entropie/STATE.md
index cc2a653..7030def 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -169,9 +169,15 @@ Round 2.3:
 - Sprint: everyone's own stride (`Body.stride`, from their quirks: longer steps and fewer, or shorter and quicker; fitter: longer at a run), step length up to 1.8 m, the foot reaching further ahead at a run (MAX_LEAD x (1 + 0.45 run)). Arms swing against the other foot (they stayed forward when both feet were behind the hips), bent more (1.2 + 0.45 run), the forearm eased less.
 - Size (`object.c` Object_setHeight, called in `scene.c`): the body box, its middle, the step height are from each person's own height (1.66 .. 1.86 m; it was 1.75 for all), top and jump speed x sqrt(height / 1.75). Audit NPCs: joint jumps 4516 -> 2418, knees back 76 -> 8; sprint 338 -> 225.
 
+Round 2.4:
+- The coat rack is gone from the code (FURN_COATRACK, Shapes_coatRack, `jacket_hung`, hangJacket). The jacket is called a pullover in the game.
+- Pullovers as things (`clothes.c`, OBJ_CLOTHES: one each, made the first time it's put down, no mesh and no collision while with them again; `pullover`, `pullover_down`, `pullover_sat`; `cloth_of`, `cloth_host`, `cloth_where`). Clothes_spot: sitting on a chair: over its back; sitting on the ground: under you; a chair within 1 m in front: over its back (CHAIR_GAP 0.05, it goes along when the chair slides); else what you look at within LOOK_REACH 1.9 m: a top 0.35-1.5 m over the feet that drops 0.25 m within EDGE_THICK 0.42 m past the edge: hung over it (a railing up to 0.12 m thick, a wall top, a bench; the drape as wide as the edge), else folded on top (a table, a bench, a chair); else spread on the ground in front (up to 0.6 m). Meshes `Shapes_pullover` (spread with the sleeves out, hung over an edge, folded).
+- You (B, pocket rows "Pullover", "Put your pullover down"): put on / take off (round your hips), put it down where the verb line says; click it (within 1.3 m, `takePullover`) to take it back; somebody else's: "That's Jana's pullover". C at your spread pullover: you sit on it. Cold: put on for you once a cold spell (`jacket_told`), after that it's up to you; put down: told where it is.
+- People (`Clothes_update`): sitting down (settled), warm, theirs with them: over the chair's back (0.8 x tidiness) or under them on the ground (tidiness: 0.35 + 0.35 liking for clearing up + 0.2 pride - 0.3 drunk), "Not sitting in the dirt."; getting up, it comes along. Cold with it put down: `ai.c` fetchPullover (idle, wander, at a table; on their own chair or under them: just take it; else goTo it, on with it; gives up after 90 s, 80 m off or warm).
+
 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); the coat rack (FURN_COATRACK, in your kitchen by the doors, garage coords 20.6 / 2.4) shows the jackets hanging on it (`Shapes_coatRack`, up to 6, a bitmask of whose in `shown`). `jacket_hung` (characters, true at the start): cold (chill 0.6+) and the jacket with them: on it goes (as before); hanging: NPCs `ai.want_jacket` -> `ai.c` fetchJacket (idle / wander: goTo the rack, on with it, "Ah, better."; gives up after 90 s or warm again); you are told once ("your jacket hangs on the rack in your garage") and click the rack (`game_actions.c` coatRack: on, or back onto it). Warm again: it comes off and stays with them.
+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).
 
 The ramp (`scene.c` addRamp via Scene_finish, after the terrain): a static plank 1.2 x 0.15 x 3.2 m, 20 degrees up, its foot on the ground, its top on a block (the loose tilted test box hung in the air at one end: loose things never turn in the physics). Tilted things in the physics (1.89): `tryStepUp` takes the surface just ahead of you (STEP_PROBE 0.3 m, rayDownInto), not the highest corner; the footprints (middleOver, clutterQuery) use the whole box's outline seen from above (a tilted plank's thickness had counted as narrow).
 
@@ -312,7 +318,11 @@ Most important first.
 - Ladders, still to do: a holder for a ladder standing free; NPCs taking a ladder somewhere for their own work
 - 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)
+- 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
+- Butts, next: into a bin or the fire too, a pocket ashtray for some, the tidy ask for an ashtray with no bin near
+- Arrival, next: the first start by car (you, Volkram, Jana, Toni), unloading 2 crates of beer, 2 of water, sleeping bags, mats, tents
+- Tents: Volkram and Jana share one, Toni and you a small one each; pitching together (poles, the cover, pegs); sleeping in them
+- Rain: showers; people go into the house or their tents
 
 **Tools for us**
 - Optional local AI (llama.cpp, small model) for talking and tasks; the menu stays as fallback
@@ -321,4 +331,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.3.0 (build 645f709)`
+State of `entropie 2.4.0 (build 6d78a96)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index 0d5253f..d96e1f1 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,9 +1,11 @@
-entropie 2.3.0: hands, shoulder roll, strides, size
+entropie 2.4.0: pullovers instead of the coat rack
 
-- Hands: wrist kept near straight; held bottles and cigarettes sit in the fist
-- Shoulder roll: dropping from about 2.5 m the sporty roll over the left or
-  right shoulder in the direction they were moving; others go into the knees
-  or crash
-- Sprint: everyone has their own stride (longer and fewer, or shorter and
-  quicker), the foot reaches further ahead, arms swing freely against the legs
-- Size: body box, step height and speed follow each person's own height
+- The coat rack is gone (code too); the jacket is now a pullover
+- Your pullover (B): put it on, take it off (tied round your hips), or put
+  it down where you look: over a chair's back, a railing, a wall or a bench,
+  folded on a table, or spread on the ground; click it to take it back
+- Sitting on the ground: spread it out and sit on it (C at it sits on it)
+- Others hang theirs over their chair or sit on it on the ground, take it
+  along when they get up, and go and get it when it gets cold
+- Cold: it goes on for you once, after that it's up to you; put down
+  somewhere, you're told where it is
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 3a624b6..0f925cb 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -4,6 +4,7 @@
  */
 
 #include "game/game_internal.h"
+#include "world/clothes.h"
 
 #define START_HOUR   13.0f
 #define START_AT_COURSE 0   /* (testing) 1: start at the obstacle course at the chicken farm; 0: in the yard (the course is there either way) */
@@ -286,10 +287,16 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     puttingUp(game);
     game_climbLadder(game, input, dt);
     game_climb(game, input, dt);
+    bool warm = game->player->look.top == TOP_JACKET;
     World_update(&game->world, dt, dt * game->daycycle.time_scale * speed);
-    if (game->player->chill > 0.6f && game->player->jacket_hung && game->player->look.top != TOP_JACKET && !game->player->jacket_told) {
-        game->player->jacket_told = true;
-        say(game, "Brr, it's getting cold - your jacket hangs on the rack in your garage");
+    Object *you = game->player;
+    if (!warm && you->look.top == TOP_JACKET) {
+        say(game, "Brr, it's getting cold - you pull your pullover on (B: off again)");
+    } else if (you->chill > 0.6f && you->pullover_down && you->look.top != TOP_JACKET && !you->jacket_told) {
+        static char line[96];
+        you->jacket_told = true;
+        snprintf(line, sizeof(line), "Brr, it's getting cold - your pullover is %s", Clothes_where(you));
+        say(game, line);
     }
     if (game->player->body.stepped_off) {
         game->player->body.stepped_off = false;
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index 73f28b6..c05f699 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -94,7 +94,7 @@ const char *Game_status(const Game *game);
 /* Shadow pass and scene pass for all world objects. */
 void Game_render(Game *game, Renderer *renderer);
 
-#define POCKET_LINES 10
+#define POCKET_LINES 12
 /* Your pockets as lines of text: a heading, what's in them, what Enter does; *picked: the line picked. */
 int Game_pocketLines(Game *game, char lines[][96], int max, int *picked);
 
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index e87121d..1f21af6 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -5,6 +5,7 @@
 
 #include "game/game_internal.h"
 #include "people/pockets.h"
+#include "world/clothes.h"
 #include "people/climb.h"
 
 #include <stdlib.h>
@@ -163,7 +164,7 @@ static void smokeOrStop(Game *game) {
 
 /* ---------- Your pockets (B): what's in them, taken out, given ---------- */
 
-enum { ROW_CIGS, ROW_TOBACCO, ROW_PAPERS, ROW_FILTERS, ROW_LIGHTER, ROW_KEYS, ROW_URINELLAS, POCKET_ROWS };
+enum { ROW_CIGS, ROW_TOBACCO, ROW_PAPERS, ROW_FILTERS, ROW_LIGHTER, ROW_KEYS, ROW_URINELLAS, ROW_PULLOVER, ROW_PUT_DOWN, POCKET_ROWS };
 
 static bool rowHas(const Object *p, int row) {
     switch (row) {
@@ -174,6 +175,8 @@ static bool rowHas(const Object *p, int row) {
     case ROW_LIGHTER:   return p->lighter;
     case ROW_KEYS:      return p->keys != NULL;
     case ROW_URINELLAS: return p->urinellas > 0;
+    case ROW_PULLOVER:  return true;
+    case ROW_PUT_DOWN:  return !p->pullover_down;
     }
     return false;
 }
@@ -188,10 +191,18 @@ static void rowText(const Object *p, int row, char *out, size_t size) {
                                  p->lighter_uses > 0 ? "works" : "empty"); break;
     case ROW_KEYS:      snprintf(out, size, "%s", p->keys); break;
     case ROW_URINELLAS: snprintf(out, size, "Urinellas - %d", p->urinellas); break;
+    case ROW_PULLOVER:  snprintf(out, size, "Pullover - %s", p->pullover_down ? Clothes_where(p) : p->look.top == TOP_JACKET ? "on"
+                                                         : "tied round your hips"); break;
+    case ROW_PUT_DOWN:  snprintf(out, size, "Put your pullover down"); break;
     default:            out[0] = 0; break;
     }
 }
 
+/* Where your pullover would go if put down now (where you look). */
+static bool pulloverSpot(Game *game, ClothSpot *spot) {
+    return Clothes_spot(&game->world, game->player, game->camera.front, spot);
+}
+
 /* The row picked, kept on something that's there. */
 static int pocketRow(Game *game) {
     const Object *p = game->player;
@@ -214,6 +225,17 @@ static void pocketVerb(Game *game, int row, char *out, size_t size) {
     case ROW_LIGHTER:   snprintf(out, size, name ? "Enter: lend %s your lighter" : "Enter: click - does it work?", name); break;
     case ROW_KEYS:      snprintf(out, size, "Keys stay in your pocket"); break;
     case ROW_URINELLAS: snprintf(out, size, name ? "Enter: give %s one" : "They're for peeing standing (P)", name); break;
+    case ROW_PULLOVER:
+        snprintf(out, size, "%s", game->player->pullover_down ? "Look at it and click to take it"
+                                  : game->player->look.top == TOP_JACKET ? "Enter: take it off (round your hips)" : "Enter: put it on");
+        break;
+    case ROW_PUT_DOWN: {
+        ClothSpot spot;
+        if (!pulloverSpot(game, &spot)) snprintf(out, size, "Nowhere to put it here");
+        else if (game->player->sitting && !game->player->seat) snprintf(out, size, "Enter: spread it out and sit on it");
+        else snprintf(out, size, "Enter: put it %s", spot.where);
+        break;
+    }
     default:            out[0] = 0; break;
     }
 }
@@ -223,6 +245,26 @@ static void smokeOrStop(Game *game);
 static void usePocket(Game *game, int row) {
     Object *p = game->player, *person = game_personInFront(game);
     char line[96];
+    if (row == ROW_PULLOVER) {
+        if (p->pullover_down) { say(game, "Look at it and click to take it"); return; }
+        bool on = p->look.top != TOP_JACKET;
+        if (on) Clothes_takeBack(p, true);
+        else { p->look.top = TOP_SHIRT; p->look.jacket_tied = true; }
+        p->jacket_told = true; /* (your choice now, this cold spell) */
+        say(game, on ? "You pull your pullover on" : "You take your pullover off and tie it round your hips");
+        return;
+    }
+    if (row == ROW_PUT_DOWN) {
+        ClothSpot spot;
+        if (!pulloverSpot(game, &spot)) { say(game, "Nowhere to put it here"); return; }
+        bool under = p->sitting && !p->seat;
+        if (!Clothes_putDown(&game->world, p, &spot)) return;
+        p->jacket_told = true;
+        snprintf(game->info, sizeof(game->info), under ? "You spread your pullover out and sit on it" : "You put your pullover %s", spot.where);
+        game->info_about = NULL;
+        say(game, game->info);
+        return;
+    }
     if (!person || person->type != OBJ_HUMAN) {
         if (row == ROW_CIGS || row == ROW_TOBACCO || row == ROW_PAPERS || row == ROW_FILTERS) {
             if (!Body_isSmoking(&p->body)) smokeOrStop(game);
@@ -586,6 +628,25 @@ static bool swingGate(Game *game) {
     return true;
 }
 
+/* Looking at your pullover (put down) within reach: back round your hips. Somebody else's: whose. */
+static bool takePullover(Game *game) {
+    Object *p = game->player;
+    Object *c = Clothes_alongRay(&game->world, game->camera.position, game->camera.front, 2.3f);
+    if (!c) return false;
+    Vec3 to = vec3_sub(c->position, p->position);
+    to.y = 0.0f;
+    if (vec3_length(to) > 1.3f) return false;
+    if (c->cloth_of != p) {
+        snprintf(game->info, sizeof(game->info), "That's %s's pullover", c->cloth_of ? c->cloth_of->name : "somebody");
+        game->info_about = NULL;
+        say(game, game->info);
+        return true;
+    }
+    Clothes_takeBack(p, false);
+    say(game, "You take your pullover and tie it round your hips");
+    return true;
+}
+
 /* At the cigarette machine: a pack. */
 static bool smokesMachine(Game *game) {
     Object *m = furnitureNear(game, FURN_SMOKES, 0.9f);
@@ -597,22 +658,6 @@ static bool smokesMachine(Game *game) {
     return true;
 }
 
-/* At the coat rack: your jacket off it and on, or off and onto it. */
-static bool coatRack(Game *game) {
-    Object *p = game->player;
-    if (!furnitureNear(game, FURN_COATRACK, 0.9f)) return false;
-    if (p->jacket_hung) {
-        p->jacket_hung = false;
-        p->look.top = TOP_JACKET;
-        say(game, "You put your jacket on");
-    } else {
-        p->jacket_hung = true;
-        if (p->look.top == TOP_JACKET) p->look.top = TOP_SHIRT;
-        say(game, "You hang your jacket on the rack");
-    }
-    return true;
-}
-
 /* The hose or the watering can in a hand, or NULL. */
 static Object *waterInHand(const Object *p) {
     for (int h = 0; h < 2; h++)
@@ -879,7 +924,7 @@ static int leftClick(Game *game) {
         Talk_showHere(&game->world, p, shown);
         return CLICK_NONE;
     }
-    if (mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || coatRack(game) || smokesMachine(game) || swingGate(game)) return CLICK_NONE;
+    if (mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || takePullover(game) || smokesMachine(game) || swingGate(game)) return CLICK_NONE;
     int watering = useWater(game);
     if (watering >= 0) return watering;
     if (bagTrash(game)) return CLICK_BAG;
@@ -996,7 +1041,13 @@ void game_handleActions(Game *game, const Input *input) {
     else if (input->sit && pourOnFire(game)) {} /* (right at a burning fire with a drink: onto it) */
     else if (input->sit && !Body_isBusy(&p->body) && !p->on_ladder) {
         if (p->sitting) p->sitting = false;
-        else Object_sit(p, Object_seatNear(game->world.objects, game->world.count, p->position, 1.0f), p->position);
+        else {
+            Object_sit(p, Object_seatNear(game->world.objects, game->world.count, p->position, 1.0f), p->position);
+            Object *c = p->pullover;
+            if (!p->seat && p->pullover_down && c && c->variant == CLOTH_SPREAD &&
+                vec3_length(vec3(c->position.x - p->position.x, 0.0f, c->position.z - p->position.z)) < 0.6f)
+                say(game, "You sit down on your pullover");
+        }
     }
     if (input->move_forward != 0.0f || input->move_right != 0.0f || input->jump) p->sitting = false;
     if (p->on_ladder) { /* climbing: hands on the rungs */
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 345757e..3ee2260 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -10,6 +10,7 @@
 #include "world/nav.h"
 #include "world/grid.h"
 #include "world/world.h"
+#include "world/clothes.h"
 #include "render/shapes.h"
 
 /* ---------- Small helpers ---------- */
@@ -1638,64 +1639,32 @@ static bool stepAside(Object *self, float dt) {
     return true;
 }
 
-/* Cold, and the jacket hangs on the coat rack: there, and on with it. */
-#define JACKET_GIVE_UP 90.0f /* s on the way: forget it */
+/* Cold, and the pullover is put down somewhere (over a chair, on the ground): there, and on with it. */
+#define JACKET_GIVE_UP 90.0f  /* s on the way: forget it */
+#define PULLOVER_WALK  80.0f  /* m: further off than this it's forgotten (for now) */
 
-static bool fetchJacket(Object *self, Object *const *objects, int count, float dt) {
+static bool fetchPullover(Object *self, Object *const *objects, int count, float dt) {
     Ai *ai = &self->ai;
     if (!ai->want_jacket) return false;
-    if (!self->jacket_hung || self->look.top == TOP_JACKET || (ai->jacket_time += dt) > JACKET_GIVE_UP || self->chill < 0.3f) {
+    Object *pullover = self->pullover;
+    if (!pullover || !self->pullover_down || self->look.top == TOP_JACKET || (ai->jacket_time += dt) > JACKET_GIVE_UP ||
+        self->chill < 0.3f || flatDistance(pullover->position, self->position) > PULLOVER_WALK) {
         ai->want_jacket = false;
         return false;
     }
-    Object *rack = NULL;
-    for (int i = 0; i < count && !rack; i++)
-        if (objects[i]->type == OBJ_FURNITURE && objects[i]->variant == FURN_COATRACK) rack = objects[i];
-    if (!rack) { ai->want_jacket = false; return false; }
-    if (!goTo(self, rack, objects, count, 0.6f, dt)) return true;
-    self->jacket_hung = false;
-    self->look.top = TOP_JACKET;
+    bool at_hand = self->sitting && (pullover->cloth_host == self->seat ||
+                                     (!self->seat && flatDistance(pullover->position, self->position) < 0.5f)); /* (on their chair, under them) */
+    if (!at_hand) {
+        if (self->sitting) self->sitting = false;
+        if (!goTo(self, pullover, objects, count, 0.45f, dt)) return true;
+    }
+    Clothes_takeBack(self, true);
     ai->want_jacket = false;
     if (random01(ai) < 0.5f) Ai_say(self, "Ah, better.");
     rest(ai, 1.0f, 2.0f);
     return true;
 }
 
-#define RACK_WALK   60.0f  /* m: the rack this near ... */
-#define RACK_CHANCE 0.004f /* ... the jacket back onto it this often a second (the tidy more, the lazy less) */
-
-static Object *coatRack(Object *const *objects, int count) {
-    for (int i = 0; i < count; i++)
-        if (objects[i]->type == OBJ_FURNITURE && objects[i]->variant == FURN_COATRACK) return objects[i];
-    return NULL;
-}
-
-/* Warm again, the jacket tied round the waist: now and then back onto the rack with it. */
-static bool hangJacket(Object *self, Object *const *objects, int count, float dt) {
-    Ai *ai = &self->ai;
-    if (!ai->hang_jacket) {
-        if (self->jacket_hung || self->look.top == TOP_JACKET || self->chill > 0.05f || ai->want_jacket) return false;
-        float tidy = clampf(0.5f + ai->character.likes[ORDER_TRASH] - 0.4f * self->drunk, 0.1f, 1.5f);
-        if (random01(ai) > RACK_CHANCE * tidy * dt) return false;
-        Object *rack = coatRack(objects, count);
-        if (!rack || flatDistance(rack->position, self->position) > RACK_WALK) return false;
-        ai->hang_jacket = true;
-        ai->jacket_time = 0.0f;
-        if (random01(ai) < 0.3f) Ai_say(self, "I'll hang my jacket up.");
-    }
-    Object *rack = coatRack(objects, count);
-    if (!rack || self->jacket_hung || self->look.top == TOP_JACKET || self->chill > 0.3f || (ai->jacket_time += dt) > JACKET_GIVE_UP) {
-        ai->hang_jacket = false;
-        return false;
-    }
-    if (!goTo(self, rack, objects, count, 0.6f, dt)) return true;
-    Hands_touch(self, Object_colliderCenter(rack, 0)); /* (onto a hook) */
-    self->jacket_hung = true;
-    ai->hang_jacket = false;
-    rest(ai, 1.0f, 2.0f);
-    return true;
-}
-
 void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     ai_ensureReady(self);
     Ai *ai = &self->ai;
@@ -1752,10 +1721,8 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_HELP) && ai_holdLadder(self, objects, count, dt)) return;
     /* Done with a tool: careful people put it back where it belongs, the others leave it lying */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && ai_putAway(self, objects, count, dt)) return;
-    /* Cold, the jacket on the rack: off to get it */
-    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && fetchJacket(self, objects, count, dt)) return;
-    /* Warm again: the jacket back onto the rack */
-    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && hangJacket(self, objects, count, dt)) return;
+    /* Cold, the pullover put down somewhere: off to get it */
+    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && fetchPullover(self, objects, count, dt)) return;
     /* Out of cigarettes: to the machine */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && buySmokes(self, objects, count, dt)) return;
     /* Tidy people pick things up on the way */
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index c58af24..83ad489 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -172,8 +172,7 @@ typedef struct {
     bool           tool_asked;  /* asked the player for their axe (once per order) */
     float          tidy_pause;  /* bending down for trash on the way (seconds left) */
     float          tray_time;   /* on the way to empty the ashtray this long */
-    bool           want_jacket; /* cold, the jacket on the rack: off to get it */
-    bool           hang_jacket; /* warm again, the jacket tied round the waist: back onto the rack with it */
+    bool           want_jacket; /* cold, the pullover put down somewhere: off to get it */
     bool           want_smokes; /* out of cigarettes, nobody gives one: off to the machine */
     float          smokes_time; /* ... on the way this long */
     bool           asked_you;   /* asked the player for one (nothing came of it: the machine next time) */
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index 7562a58..e1d422f 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -115,7 +115,7 @@ Mesh  Shapes_grill(void);
 
 typedef enum {
     FURN_WORKBENCH, FURN_TOOLWALL, FURN_SHELF, FURN_STOVE, FURN_COUNTER, FURN_TABLE,
-    FURN_CHAIR, FURN_BENCH, FURN_CAR, FURN_TIRES, FURN_TOILET, FURN_TAP, FURN_ASHTRAY, FURN_COATRACK, FURN_GATE, FURN_SMOKES, FURN_COUNT
+    FURN_CHAIR, FURN_BENCH, FURN_CAR, FURN_TIRES, FURN_TOILET, FURN_TAP, FURN_ASHTRAY, FURN_GATE, FURN_SMOKES, FURN_COUNT
 } FurnitureKind;
 #define SEAT_HEIGHT 0.45f  /* chairs and log benches */
 
@@ -126,8 +126,10 @@ Mesh  Shapes_furniture(FurnitureKind kind, Vec3 size);
 Mesh  Shapes_ashtray(Vec3 size, int butts);
 /* Ivy up a wall (at z = 0, facing +Z), this wide and high. */
 Mesh  Shapes_ivy(float width, float height, unsigned seed);
-/* The coat rack with these jackets hanging on it (n at most 6). */
-Mesh  Shapes_coatRack(Vec3 size, const Vec3 *jackets, int n);
+/* A pullover put down (see clothes.h), its colour: lay 0 spread out flat on the ground (y = 0 under it,
+   the neck to +Z, sleeves out), 1 hung over an edge along X (y = 0 its top; gap: how thick that edge is),
+   2 folded up (y = 0 under it). */
+Mesh  Shapes_pullover(int lay, Vec3 color, float gap);
 
 /* A dachshund, its nose towards -Z, standing on y = 0: a plain body for picking it
    out (she's drawn as a painted figure, see people/dog.c); her proportions. */
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index abdb1d9..8548503 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -81,6 +81,47 @@ Mesh Shapes_toy(int variant, Vec3 size) {
     return Mesh_fromBuilder(&b);
 }
 
+/* A sleeve flat on the ground from the shoulder at (x, z), out and down at this angle off straight out. */
+static void addFlatSleeve(MeshBuilder *b, float side, float angle, Vec3 color, Vec3 cuff) {
+    const float len = 0.48f, wide = 0.13f, y = 0.013f;
+    float yaw = side > 0.0f ? -angle : PI_F + angle; /* (lays local +X along (side cos a, 0, -sin a)) */
+    Mat4 at = mat4_mul(mat4_translate(vec3(side * 0.24f, 0.0f, 0.22f)), mat4_rotate_y(yaw));
+    size_t first = b->vertex_count;
+    addBlock(b, vec3(len * 0.5f, y, 0.0f), vec3(len, 0.022f, wide), color);
+    addBlock(b, vec3(len - 0.03f, y + 0.002f, 0.0f), vec3(0.06f, 0.024f, wide * 0.9f), cuff);
+    transformSince(b, first, at);
+}
+
+Mesh Shapes_pullover(int lay, Vec3 color, float gap) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    Vec3 dark = vec3_scale(color, 0.78f); /* (the ribbed band, cuffs and collar) */
+    if (lay == 0) { /* spread out: the body, the band at the bottom, the collar, both sleeves out */
+        addBlock(&b, vec3(0.0f, 0.012f, 0.0f), vec3(0.5f, 0.024f, 0.62f), color);
+        addBlock(&b, vec3(0.0f, 0.014f, -0.28f), vec3(0.5f, 0.026f, 0.06f), dark);
+        addBlock(&b, vec3(0.0f, 0.016f, 0.28f), vec3(0.17f, 0.028f, 0.06f), dark);
+        addFlatSleeve(&b, 1.0f, 0.55f, color, dark);
+        addFlatSleeve(&b, -1.0f, 0.55f, color, dark);
+    } else if (lay == 1) { /* over an edge: rolled over its top, the front (+Z) and the back hanging down, the sleeves in front */
+        float in = gap * 0.5f + 0.015f;
+        addBlock(&b, vec3(0.0f, 0.016f, 0.0f), vec3(0.46f, 0.032f, gap + 0.06f), color);
+        addBlock(&b, vec3(0.0f, -0.15f, in), vec3(0.46f, 0.32f, 0.024f), color);
+        addBlock(&b, vec3(0.0f, -0.30f, in + 0.002f), vec3(0.46f, 0.05f, 0.026f), dark);
+        addBlock(&b, vec3(0.0f, -0.13f, -in), vec3(0.46f, 0.28f, 0.024f), color);
+        for (int s = -1; s <= 1; s += 2) {
+            size_t first = b.vertex_count;
+            addBlock(&b, vec3(0.0f, -0.22f, 0.0f), vec3(0.11f, 0.44f, 0.022f), color);
+            addBlock(&b, vec3(0.0f, -0.42f, 0.0f), vec3(0.1f, 0.05f, 0.024f), dark);
+            transformSince(&b, first, mat4_mul(mat4_translate(vec3((float)s * 0.17f, 0.0f, in + 0.024f)), mat4_rotate_z((float)s * 0.08f)));
+        }
+    } else { /* folded up */
+        addBlock(&b, vec3(0.0f, 0.03f, 0.0f), vec3(0.32f, 0.06f, 0.26f), color);
+        addBlock(&b, vec3(0.0f, 0.03f, 0.125f), vec3(0.322f, 0.04f, 0.014f), dark);
+        addBlock(&b, vec3(0.0f, 0.061f, -0.02f), vec3(0.14f, 0.004f, 0.05f), dark);
+    }
+    return Mesh_fromBuilder(&b);
+}
+
 Mesh Shapes_bread(Vec3 size) {
     MeshBuilder b;
     MeshBuilder_init(&b);
@@ -751,25 +792,6 @@ static void buildAshtray(MeshBuilder *b, Vec3 size, int butts) {
     }
 }
 
-/* A coat rack: a post on a cross foot, hooks at the top; jackets hang on it
-   (boxes in their colours, sleeves down). */
-static void buildCoatRack(MeshBuilder *b, Vec3 size, const Vec3 *jackets, int n) {
-    const Vec3 wood = { 0.40f, 0.27f, 0.16f };
-    addBlock(b, vec3(0.0f, 0.02f, 0.0f), vec3(size.x, 0.04f, 0.06f), wood);
-    addBlock(b, vec3(0.0f, 0.02f, 0.0f), vec3(0.06f, 0.04f, size.z), wood);
-    addBlock(b, vec3(0.0f, size.y * 0.5f, 0.0f), vec3(0.05f, size.y, 0.05f), wood);
-    for (int k = 0; k < 6; k++) {
-        float a = (float)k * PI_F / 3.0f;
-        addBlock(b, vec3(0.06f * cosf(a), size.y - 0.08f, 0.06f * sinf(a)), vec3(0.08f, 0.02f, 0.02f), COLOR_IRON);
-    }
-    for (int k = 0; k < n && k < 6; k++) {
-        float a = (float)k * PI_F / 3.0f + 0.3f;
-        Vec3 at = vec3(0.13f * cosf(a), size.y - 0.42f, 0.13f * sinf(a));
-        addBlock(b, at, vec3(0.16f, 0.62f, 0.12f), jackets[k]);                               /* the body */
-        addBlock(b, vec3_add(at, vec3(0.0f, -0.05f, 0.0f)), vec3(0.2f, 0.4f, 0.06f), vec3_scale(jackets[k], 0.8f)); /* sleeves */
-    }
-}
-
 Mesh Shapes_ashtray(Vec3 size, int butts) {
     MeshBuilder b;
     MeshBuilder_init(&b);
@@ -777,13 +799,6 @@ Mesh Shapes_ashtray(Vec3 size, int butts) {
     return Mesh_fromBuilder(&b);
 }
 
-Mesh Shapes_coatRack(Vec3 size, const Vec3 *jackets, int n) {
-    MeshBuilder b;
-    MeshBuilder_init(&b);
-    buildCoatRack(&b, size, jackets, n);
-    return Mesh_fromBuilder(&b);
-}
-
 Mesh Shapes_furniture(FurnitureKind kind, Vec3 size) {
     MeshBuilder b;
     MeshBuilder_init(&b);
@@ -922,9 +937,6 @@ Mesh Shapes_furniture(FurnitureKind kind, Vec3 size) {
     case FURN_ASHTRAY:
         buildAshtray(&b, size, 3);
         break;
-    case FURN_COATRACK:
-        buildCoatRack(&b, size, NULL, 0);
-        break;
     case FURN_GATE: { /* a wooden gate leaf, painted green: boards, two rails and a brace */
         const Vec3 green = { 0.24f, 0.45f, 0.28f }, dark = { 0.17f, 0.34f, 0.21f };
         int boards = (int)(size.x / 0.14f);
diff --git a/entropie/src/world/clothes.c b/entropie/src/world/clothes.c
new file mode 100644
index 0000000..75cf4ee
--- /dev/null
+++ b/entropie/src/world/clothes.c
@@ -0,0 +1,227 @@
+#include "world/clothes.h"
+#include "people/ai.h"
+#include "people/hands.h"
+#include "render/shapes.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#define CHAIR_REACH 1.0f   /* m: a chair this near in front: over its back */
+#define LOOK_REACH  1.9f   /* m: what you look at this near from the eye */
+#define EDGE_LOW    0.35f  /* m over the feet: a top this high (up to EDGE_HIGH) takes it ... */
+#define EDGE_HIGH   1.5f
+#define EDGE_DROP   0.25f  /* ... hung over it when it drops this much ... */
+#define EDGE_THICK  0.42f  /* ... within this far past the edge (a railing, a wall top, a bench); else laid on it */
+#define CHAIR_GAP   0.05f  /* a chair's back this thick */
+#define AWAY        0.6f   /* m: on the ground this far in front */
+#define SAT_CHANCE  0.8f   /* sitting down on a chair: over its back this often (x tidiness) */
+
+static Vec3 flat(Vec3 v, Vec3 or_else) {
+    Vec3 f = vec3_normalize(vec3(v.x, 0.0f, v.z));
+    return vec3_length(f) > 0.5f ? f : or_else;
+}
+
+static bool isChair(const Object *o) { return o && o->type == OBJ_FURNITURE && o->variant == FURN_CHAIR; }
+
+/* The top of a chair's back (its back at local -Z, see Shapes_furniture). */
+static Vec3 chairBack(const Object *chair) {
+    Vec3 local = vec3(0.0f, chair->size.y, -chair->size.z * 0.5f + 0.02f);
+    return vec3_add(chair->position, mat4_mul_dir(mat4_rotate_y(chair->rotation.y), local));
+}
+
+static void overChair(const Object *chair, ClothSpot *s) {
+    s->lay   = CLOTH_HUNG;
+    s->host  = (Object *)chair;
+    s->at    = chairBack(chair);
+    s->yaw   = chair->rotation.y;
+    s->gap   = CHAIR_GAP;
+    s->where = "over the chair's back";
+}
+
+/* The nearest chair in front within reach. */
+static const Object *chairInFront(const World *world, const Object *who, Vec3 look) {
+    const Object *best = NULL;
+    float best_d = CHAIR_REACH;
+    for (int i = 0; i < world->count; i++) {
+        const Object *o = world->objects[i];
+        if (!isChair(o)) continue;
+        Vec3 to = vec3_sub(chairBack(o), who->position);
+        to.y = 0.0f;
+        float d = vec3_length(to);
+        if (d < best_d && (d < 0.3f || vec3_dot(vec3_scale(to, 1.0f / d), look) > 0.4f)) { best = o; best_d = d; }
+    }
+    return best;
+}
+
+static bool notThere(const Object *o, const void *ctx) {
+    return o == ctx || o->is_character || o->is_held || o->type == OBJ_DOG || o->type == OBJ_CLOTHES;
+}
+
+/* Hung over it: a railing (thin), a wall top, a bench, a chair. */
+static const char *overIt(const Object *o, bool thin) {
+    if (o->type == OBJ_FURNITURE && o->variant == FURN_BENCH) return "over the bench";
+    if (isChair(o)) return "over the chair";
+    if (o->type == OBJ_WALL || o->type == OBJ_HOUSE || o->type == OBJ_BARN) return thin ? "over the railing" : "over the wall";
+    return "over it";
+}
+
+static const char *onTopOf(const Object *o) {
+    if (o->type == OBJ_FURNITURE && o->variant == FURN_TABLE) return "on the table";
+    if (o->type == OBJ_FURNITURE && o->variant == FURN_BENCH) return "on the bench";
+    if (isChair(o)) return "on the chair";
+    return "on top of it";
+}
+
+bool Clothes_spot(const World *world, const Object *who, Vec3 look, ClothSpot *s) {
+    memset(s, 0, sizeof(*s));
+    Vec3 facing = Object_facing(who), ahead = flat(look, facing);
+    float feet = who->position.y;
+    if (who->sitting) {
+        if (isChair(who->seat)) { overChair(who->seat, s); return true; }
+        if (who->seat) return false; /* (a bench or a log: no) */
+        s->lay   = CLOTH_SPREAD;
+        s->at    = vec3_add(who->position, vec3_scale(facing, 0.05f));
+        s->at.y  = World_groundHeight(world, s->at.x, s->at.z, feet + 0.3f);
+        s->yaw   = yaw_facing(facing);
+        s->where = "on the ground";
+        return true;
+    }
+    const Object *chair = chairInFront(world, who, ahead);
+    if (chair) { overChair(chair, s); return true; }
+
+    Vec3 eye = vec3_add(who->position, vec3(0.0f, who->look.height * 0.92f, 0.0f));
+    Object *hit = NULL;
+    Vec3 n = vec3_zero();
+    float t = World_rayCast(world, eye, look, LOOK_REACH, notThere, who, &hit, &n);
+    if (t >= 0.0f && hit) {
+        Vec3 h = vec3_add(eye, vec3_scale(look, t));
+        Vec3 in = flat(vec3_scale(n, -1.0f), ahead); /* (into it) */
+        Vec3 edge = vec3_add(h, vec3_scale(in, 0.02f));
+        float top = World_groundHeight(world, edge.x, edge.z, h.y + 0.6f), up = top - feet;
+        if (up > EDGE_LOW && up < EDGE_HIGH && top < h.y + 0.6f) {
+            float thick = EDGE_THICK; /* (how thick the top is: where it drops away past the edge) */
+            for (float d = 0.04f; d < EDGE_THICK; d += 0.04f) {
+                Vec3 q = vec3_add(h, vec3_scale(in, d));
+                if (top - World_groundHeight(world, q.x, q.z, top + 0.05f) > EDGE_DROP) { thick = d; break; }
+            }
+            s->yaw = yaw_facing(vec3_scale(in, -1.0f)); /* (the front towards you) */
+            if (thick < EDGE_THICK) {
+                Vec3 mid = vec3_add(h, vec3_scale(in, thick * 0.5f));
+                s->lay   = CLOTH_HUNG;
+                s->at    = vec3(mid.x, top, mid.z);
+                s->gap   = thick;
+                s->where = overIt(hit, thick <= 0.12f);
+            } else {
+                s->lay   = CLOTH_FOLDED;
+                s->at    = vec3(h.x + in.x * 0.18f, top, h.z + in.z * 0.18f);
+                s->where = onTopOf(hit);
+            }
+            return true;
+        }
+    }
+    float away = t >= 0.0f ? clampf(t * sqrtf(fmaxf(1.0f - look.y * look.y, 0.0f)) - 0.35f, 0.25f, AWAY) : AWAY;
+    s->lay   = CLOTH_SPREAD;
+    s->at    = vec3_add(who->position, vec3_scale(ahead, away));
+    s->at.y  = World_groundHeight(world, s->at.x, s->at.z, feet + 0.5f);
+    s->yaw   = yaw_facing(ahead);
+    s->where = "on the ground";
+    return fabsf(s->at.y - feet) < 0.5f;
+}
+
+bool Clothes_putDown(World *world, Object *who, const ClothSpot *s) {
+    Mesh mesh = Shapes_pullover((int)s->lay, who->look.jacket, fmaxf(s->gap, 0.04f));
+    Object *c = who->pullover;
+    if (!c) {
+        c = Object_createDecal(mesh, s->at);
+        if (!c || !World_add(world, c)) return false;
+        c->type = OBJ_CLOTHES;
+        snprintf(c->name, sizeof(c->name), "Pullover");
+        who->pullover = c;
+    } else {
+        Mesh_destroy(&c->mesh);
+        c->mesh = mesh;
+        c->position = s->at;
+    }
+    c->variant     = (int)s->lay;
+    c->rotation    = vec3(0.0f, s->yaw, 0.0f);
+    c->cloth_of    = who;
+    c->cloth_host  = s->host;
+    c->cloth_where = s->where;
+    if (who->look.top == TOP_JACKET) who->look.top = TOP_SHIRT;
+    who->pullover_down = true;
+    who->pullover_sat  = false;
+    who->look.jacket_tied = false;
+    Hands_touch(who, vec3_add(s->at, vec3(0.0f, s->lay == CLOTH_HUNG ? 0.0f : 0.05f, 0.0f)));
+    return true;
+}
+
+void Clothes_takeBack(Object *who, bool wear) {
+    Object *c = who->pullover;
+    if (c && who->pullover_down) {
+        Hands_touch(who, c->position);
+        Mesh_destroy(&c->mesh); /* (nothing to see where it was) */
+        c->cloth_host = NULL;
+    }
+    who->pullover_down = false;
+    who->pullover_sat  = false;
+    if (wear) who->look.top = TOP_JACKET;
+    who->look.jacket_tied = who->look.top != TOP_JACKET;
+}
+
+const char *Clothes_where(const Object *who) {
+    return who->pullover_down && who->pullover ? who->pullover->cloth_where : NULL;
+}
+
+Object *Clothes_alongRay(const World *world, Vec3 eye, Vec3 dir, float reach) {
+    Object *best = NULL;
+    float best_off = 0.35f;
+    for (int i = 0; i < world->count; i++) {
+        Object *c = world->objects[i];
+        if (c->type != OBJ_CLOTHES || !c->mesh.vao) continue;
+        Vec3 mid = vec3_add(c->position, vec3(0.0f, c->variant == CLOTH_HUNG ? -0.15f : 0.03f, 0.0f));
+        Vec3 rel = vec3_sub(mid, eye);
+        float along = vec3_dot(rel, dir);
+        if (along < 0.0f || along > reach) continue;
+        float off = vec3_length(vec3_sub(rel, vec3_scale(dir, along)));
+        if (off < best_off) { best = c; best_off = off; }
+    }
+    return best;
+}
+
+static float dice(World *world) {
+    world->rng = world->rng * 1103515245u + 12345u;
+    return (float)((world->rng >> 8) & 0xFFFFu) / 65536.0f;
+}
+
+/* How likely they bother (the tidy and the proud more, the drunk less). */
+static float tidiness(const Object *o) {
+    return clampf(0.35f + 0.35f * o->ai.character.likes[ORDER_TRASH] + 0.2f * o->temperament.pride - 0.3f * fminf(o->drunk, 1.0f), 0.05f, 0.9f);
+}
+
+/* Sat down (settled): theirs over the chair's back, or under them on the ground. */
+static void satDown(World *world, Object *o) {
+    if (o->pullover_down || o->look.top == TOP_JACKET || o->chill > 0.4f) return;
+    bool chair = isChair(o->seat);
+    if (o->seat && !chair) return;
+    if (dice(world) > tidiness(o) * (chair ? SAT_CHANCE : 1.0f)) return;
+    ClothSpot s;
+    if (!Clothes_spot(world, o, Object_facing(o), &s) || !Clothes_putDown(world, o, &s)) return;
+    o->pullover_sat = true;
+    if (!chair && o->ai.speech_time <= 0.0f && dice(world) < 0.3f) Ai_say(o, "Not sitting in the dirt.");
+}
+
+void Clothes_update(World *world) {
+    for (int i = 0; i < world->count; i++) {
+        Object *o = world->objects[i];
+        if (o->type == OBJ_CLOTHES && o->mesh.vao && isChair(o->cloth_host)) {
+            o->position   = chairBack(o->cloth_host);
+            o->rotation.y = o->cloth_host->rotation.y;
+            continue;
+        }
+        if (o->type != OBJ_HUMAN) continue;
+        bool settled = o->sitting && !o->sit_moving;
+        if (settled && !o->was_sitting) satDown(world, o);
+        else if (!o->sitting && o->was_sitting && o->pullover_sat && o->pullover_down) Clothes_takeBack(o, o->chill > 0.6f);
+        o->was_sitting = settled;
+    }
+}
diff --git a/entropie/src/world/clothes.h b/entropie/src/world/clothes.h
new file mode 100644
index 0000000..d5d162f
--- /dev/null
+++ b/entropie/src/world/clothes.h
@@ -0,0 +1,42 @@
+#ifndef WORLD_CLOTHES_H
+#define WORLD_CLOTHES_H
+
+/*
+ * A pullover (the figure's jacket: look.top TOP_JACKET worn, look.jacket_tied
+ * round the waist) put down somewhere: everybody's own OBJ_CLOTHES, made the
+ * first time it's put down, no mesh while it's with them again. Hung over a
+ * chair's back or a railing, spread on the ground to sit on, or folded on a
+ * table or a bench.
+ */
+
+#include "world/world.h"
+
+typedef enum { CLOTH_SPREAD, CLOTH_HUNG, CLOTH_FOLDED } ClothLay;
+
+typedef struct {
+    ClothLay    lay;
+    Vec3        at;     /* spread / folded: under it; hung: the top of the edge */
+    float       yaw;
+    float       gap;    /* hung: how thick the edge is */
+    Object     *host;   /* a chair it goes over (it follows the chair), or NULL */
+    const char *where;  /* said plainly: "over the chair's back" */
+} ClothSpot;
+
+/* Where who's pullover would go now: sitting, over their chair's back or
+   under them; else what they look at (look: unit) - a chair, a railing or a
+   wall top, a table - or the ground in front. False: nowhere. */
+bool        Clothes_spot(const World *world, const Object *who, Vec3 look, ClothSpot *spot);
+/* Put down there (taken off first if worn). */
+bool        Clothes_putDown(World *world, Object *who, const ClothSpot *spot);
+/* With them again: tied round the waist, or on (wear). */
+void        Clothes_takeBack(Object *who, bool wear);
+/* Where theirs is ("over the chair's back"), or NULL: with them. */
+const char *Clothes_where(const Object *who);
+/* Somebody's pullover lying near this ray (from the eye, dir unit) within reach, or NULL. */
+Object     *Clothes_alongRay(const World *world, Vec3 eye, Vec3 dir, float reach);
+/* Every frame: pullovers go along with the chair they hang on; people sitting
+   down (not the player) now and then put theirs over the chair's back or
+   under them, and take it back getting up. */
+void        Clothes_update(World *world);
+
+#endif /* WORLD_CLOTHES_H */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index f458518..1907b88 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -347,6 +347,10 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         break;
     }
 
+    case OBJ_CLOTHES: /* (made in clothes.c, its mesh given) */
+        obj->is_static = true;
+        break;
+
     case OBJ_WALL: /* nothing to see: the building's OBJ_STRUCTURE shows it */
         obj->pivot     = vec3(0.0f, size.y * 0.5f, 0.0f);
         obj->is_static = true;
@@ -1197,7 +1201,6 @@ void Object_describe(const Object *obj, char *out, size_t size) {
         };
         if (obj->variant == FURN_ASHTRAY) { snprintf(out, size, "Ashtray (an old tin) - %d butts in it", obj->stored); break; }
         if (obj->variant == FURN_SMOKES) { snprintf(out, size, "Cigarette machine - click: a pack of %d", SMOKES_PACKS); break; }
-        if (obj->variant == FURN_COATRACK) { snprintf(out, size, "Coat rack - everybody's jacket hangs here (click: yours on or off)"); break; }
         snprintf(out, size, "%s", obj->variant >= 0 && obj->variant < FURN_COUNT && WHAT[obj->variant] ? WHAT[obj->variant] : "Furniture");
         break;
     }
@@ -1216,6 +1219,10 @@ void Object_describe(const Object *obj, char *out, size_t size) {
                               : "Ladder (slid together) - left click: slide it out, then lean it against something");
         break;
     case OBJ_STONE: snprintf(out, size, kg >= 1 ? "A stone - %d kg" : "A pebble", kg); break;
+    case OBJ_CLOTHES:
+        if (obj->cloth_of && obj->cloth_of->type == OBJ_PLAYER) snprintf(out, size, "Your pullover - click: take it");
+        else snprintf(out, size, "%s's pullover", obj->cloth_of ? obj->cloth_of->name : "Somebody");
+        break;
     case OBJ_TOY:   snprintf(out, size, obj->variant == 0 ? "Alice's ball - throw it (T), she brings it back" : "Alice's rope toy - throw it (T), she loves to chew it"); break;
     case OBJ_CAN:
         if (obj->water > 0.05f) snprintf(out, size, "Watering can - %.0f l, left click (hold) to pour", obj->water);
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 635551a..2329f8e 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -81,6 +81,7 @@ typedef enum {
                             size = box, position = its bottom middle; variant 1: a locked door (name says which) */
     OBJ_STRUCTURE,       /* static: how a building made of parts looks (one mesh, no collision: see OBJ_WALL) */
     OBJ_TOY,             /* Alice's toys: weight 0 = a ball (size.x across), 1 = a rope to bite (size.x long, size.y thick) */
+    OBJ_CLOTHES,         /* somebody's pullover put down (see clothes.h): variant = ClothLay; no collision */
 } ObjectType;
 
 /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */
@@ -271,9 +272,15 @@ typedef struct Object {
     bool       steep;         /* ... stood up straight: a wall in front (flat it would go into it) */
     float      grip_shift;    /* ladders carried at the side: slid back (< 0) or on in the hand (m), off a wall */
     bool       just_caught;   /* ladders: fell against something just now and rests there (the game says so) */
-    bool       jacket_hung;   /* characters: the jacket hangs on the coat rack (else it's with them, worn or not) */
-    bool       jacket_told;   /* the player: told where the jacket is this cold spell */
-    int        shown;         /* furniture: what its mesh shows (butts in the tin, jackets on the rack); -1: not built */
+    struct Object *pullover;  /* characters: their pullover as a thing (OBJ_CLOTHES, made the first time it's put down) ... */
+    bool       pullover_down; /* ... lying there (else with them: worn - look.top TOP_JACKET - or tied round the waist) */
+    bool       pullover_sat;  /* ... put down as they sat (over the chair's back, under them): taken back getting up */
+    bool       was_sitting;   /* characters: sitting last frame (see Clothes_update) */
+    struct Object *cloth_of;  /* clothes: whose */
+    struct Object *cloth_host;/* clothes: over this chair's back (and along with it when it slides) */
+    const char *cloth_where;  /* clothes: where, said plainly ("over the chair's back") */
+    bool       jacket_told;   /* the player: put the pullover on for you / told where it is, this cold spell */
+    int        shown;         /* furniture: what its mesh shows (butts in the tin); -1: not built */
     bool       gate_open;     /* gates: open (or opening) ... */
     float      gate_t;        /* ... how far open (0 shut .. 1 open) */
     Vec3       hinge;         /* ... where it turns */
diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c
index 83b5f2f..d0bb9ec 100644
--- a/entropie/src/world/scene.c
+++ b/entropie/src/world/scene.c
@@ -247,7 +247,6 @@ Object *Scene_load(World *world) {
             obj->wits       = WITS[characters % TEMPERAMENT_COUNT];
             obj->height_fear = HEIGHT_FEAR[characters % TEMPERAMENT_COUNT];
             obj->worn       = YESTERDAY[characters % TEMPERAMENT_COUNT];
-            obj->jacket_hung = false; /* (with them: tied round the waist till it gets cold) */
             obj->cigarettes = CIGARETTES[characters % TEMPERAMENT_COUNT];
             obj->tobacco    = TOBACCO[characters % TEMPERAMENT_COUNT];
             obj->papers     = PAPERS[characters % TEMPERAMENT_COUNT];
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 069d214..09d08d1 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -1,5 +1,6 @@
 #include "world/world.h"
 #include "world/camp.h"
+#include "world/clothes.h"
 #include "people/condition.h"
 #include "people/dog.h"
 #include "world/forestry.h"
@@ -700,30 +701,32 @@ static void smoke(World *world, Object *o, Vec3 facing, float dt) {
     }
 }
 
-/* Cold nights: away from a burning fire a chill builds up and the jacket goes
-   on; by the fire, in the day (or warm with drink) it comes off again. */
+/* 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
+   is. You: put on for you once a cold spell (then it's up to you, B). */
 static void dressForNight(World *world, Object *o, float dt) {
     if (o->type == OBJ_DOG) return;
     bool night = world->hour >= 21.0f || world->hour < 7.0f;
     bool by_fire = Camp_fireNear(world->objects, world->count, o->position, 4.0f, true) != NULL;
     float cold = !night ? -2.0f : by_fire ? -1.0f : 1.0f - 0.8f * fminf(o->drunk, 1.0f);
     o->chill = clampf(o->chill + cold * dt / 60.0f, 0.0f, 1.0f); /* (about a minute to feel it) */
-    if (o->look.top != TOP_JACKET && o->chill > 0.6f) {
-        bool rack = false; /* (no rack anywhere: it's just there, like before) */
-        for (int i = 0; i < world->count && !rack; i++) rack = world->objects[i]->type == OBJ_FURNITURE && world->objects[i]->variant == FURN_COATRACK;
-        if (!o->jacket_hung || !rack) { /* with them: on it goes */
-            o->look.top = TOP_JACKET;
-            if (o->type == OBJ_HUMAN && o->ai.speech_time <= 0.0f && worldDice(world) < 0.3f) Ai_say(o, "Brr, getting cold.");
-        } else if (o->type == OBJ_HUMAN && !o->ai.want_jacket) { /* on the rack: they go and get it */
+    bool player = o->type == OBJ_PLAYER;
+    if (o->look.top != TOP_JACKET && o->chill > 0.6f && !(player && o->jacket_told)) {
+        if (!o->pullover_down) { /* with them: on it goes */
+            Clothes_takeBack(o, true);
+            if (player) o->jacket_told = true;
+            else if (o->ai.speech_time <= 0.0f && worldDice(world) < 0.3f) Ai_say(o, "Brr, getting cold.");
+        } else if (!player && !o->ai.want_jacket) { /* put down somewhere: they go and get it */
             o->ai.want_jacket = true;
             o->ai.jacket_time = 0.0f;
-            if (o->ai.speech_time <= 0.0f && worldDice(world) < 0.4f) Ai_say(o, "Brr - I'll get my jacket.");
+            if (o->ai.speech_time <= 0.0f && worldDice(world) < 0.4f) Ai_say(o, "Brr - where did I leave my pullover?");
         }
     } else if (o->look.top == TOP_JACKET && o->chill < 0.15f) {
         o->look.top = TOP_SHIRT; /* (off, and along with them: round the waist) */
     }
     if (o->chill < 0.3f) o->jacket_told = false;
-    o->look.jacket_tied = !o->jacket_hung && o->look.top != TOP_JACKET; /* (off and with them) */
+    o->look.jacket_tied = !o->pullover_down && o->look.top != TOP_JACKET; /* (off and with them) */
 }
 
 /* A chair waits to be pushed in while somebody stands where it goes. */
@@ -770,8 +773,7 @@ static float scootTo(const World *world, const Object *chair) {
     return clampf(chair->chair_out - closer, -0.15f, chair->chair_out);
 }
 
-/* The tin shows its butts, the coat rack the jackets on it: their meshes are
-   built again when that changes. */
+/* The tin shows its butts: its mesh is built again when that changes. */
 static void showProps(World *world) {
     for (int i = 0; i < world->count; i++) {
         Object *f = world->objects[i];
@@ -784,19 +786,6 @@ static void showProps(World *world) {
             Mesh_destroy(&f->mesh);
             f->mesh = Shapes_ashtray(f->size, f->stored);
             f->shown = butts;
-        } else if (f->variant == FURN_COATRACK) {
-            Vec3 colors[6];
-            int n = 0, mask = 0, k = 0;
-            for (int j = 0; j < world->count && n < 6; j++) {
-                const Object *o = world->objects[j];
-                if (o->type != OBJ_HUMAN && o->type != OBJ_PLAYER) continue;
-                if (o->jacket_hung) { colors[n++] = o->look.jacket; mask |= 1 << k; }
-                k++;
-            }
-            if (mask == f->shown) continue;
-            Mesh_destroy(&f->mesh);
-            f->mesh = Shapes_coatRack(f->size, colors, n);
-            f->shown = mask;
         }
     }
 }
@@ -1213,6 +1202,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;
+    Clothes_update(world);
     showProps(world); /* (who's a person, where the fires are: looked up again and again) */
     for (int i = 0; i < world->count; i++) {
         Object *o = world->objects[i];