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

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 1.89.0: the coat rack, a full tin, a real ramp

Christian Immanuel · 2026-10-15 18:39 · ecf414f85a9104d2eb684b7d9df39cda3cec3981

- A coat rack in your kitchen with everybody's jacket: getting cold, people
  walk over and put theirs on; yours: click the rack (on, or back on it)
- The ashtray tin shows its butts, filling up to a little heap
- The tilted test box that hung in the air at one end is now a plank ramp
  resting on a block - you can walk up it
- Fix: walking onto anything tilted (a ramp) was blocked: the step up took
  its highest corner instead of the surface in front of you
 entropie/Makefile                  |  2 +-
 entropie/README.md                 | 12 +++++---
 entropie/STATE.md                  | 10 ++++--
 entropie/commit_message.txt        | 18 +++++------
 entropie/src/game/game.c           |  6 +++-
 entropie/src/game/game_actions.c   | 18 ++++++++++-
 entropie/src/people/ai.c           | 25 +++++++++++++++
 entropie/src/people/ai.h           |  2 ++
 entropie/src/render/shapes.h       |  6 +++-
 entropie/src/render/shapes_props.c | 62 ++++++++++++++++++++++++++++++++------
 entropie/src/world/object.c        |  2 ++
 entropie/src/world/object.h        |  3 ++
 entropie/src/world/physics.c       | 33 +++++++++++++++++++-
 entropie/src/world/scene.c         | 45 ++++++++++++++++++++++++++-
 entropie/src/world/scene.h         |  2 ++
 entropie/src/world/site.c          |  1 +
 entropie/src/world/world.c         | 51 ++++++++++++++++++++++++++++---
 17 files changed, 261 insertions(+), 37 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index d7907f0..b935137 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.88.0
+VERSION   := 1.89.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 7be479d..a5dd0cb 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -27,6 +27,7 @@ Painted in the rotoscope style of *Waking Life* (2001):
 The map is Homburgshöhe at Geflügelhof Oderland, traced from the real map
 and satellite pictures (about 340 x 340 m).
 
+- A plank ramp in the yard, its top end on a block: walk up it
 - An obstacle course in the chicken farm yard: three lanes of logs, crates,
   benches, walls and blocks, spaced and close together. (Testing: set
   `START_AT_COURSE` in `src/game/game.c` to 1 to start there)
@@ -37,7 +38,7 @@ and satellite pictures (about 340 x 340 m).
   Floors are 2.8 m high
 - Your garages have three rooms: tools next to the main building (workbench,
   tool wall, shelf, axe, shears, chainsaw and a ladder), a kitchen (counter, stove,
-  table and chairs; hold food over the stove to cook it) and a car repair
+  table and chairs, the coat rack; hold food over the stove to cook it) and a car repair
   place with an old car behind the wide door
 - Alice the dachshund lives here: she trots after you, sniffs around, begs
   for sausages, comes when called (R), lies by the fire and loves being petted (click)
@@ -161,7 +162,7 @@ and satellite pictures (about 340 x 340 m).
 - The others do it too: a long log across the way they step onto and walk on
   (and wobble), shorter things they walk round
 - Off the edge of something you go once your middle is past it - no standing
-  on air beside it; slopes (a roof, a tilted box) no longer show invisible steps
+  on air beside it; slopes (a roof, the ramp) no longer show invisible steps
 - The sporty fit their steps to what's coming, so a foot lands at a good
   take-off spot
 - Jogging or faster, low things (up to about the knee) are jumped on their
@@ -455,7 +456,9 @@ say no more often and don't come to you for a bit.
 - 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
+  by the fire or in the day it comes off (and stays with them)
+- Everybody's jacket starts on the coat rack in your kitchen: getting cold,
+  they walk over and put it on. Yours too: click the rack (on / back on it)
 - Ask them what they're doing, send them back to helping you, or give them a break
 
 ## Smoking
@@ -468,7 +471,8 @@ say no more often and don't come to you for a bit.
 - A drag now and then (the tip glows), smoke breathed out, the hand up in
   front between drags. Hands needed: it goes into the mouth
 - Done: flicked away or dropped as a butt (into the fire when close; the
-  tidy and you use the old tin on the cool box by the fire as ashtray)
+  tidy and you use the old tin on the cool box by the fire as ashtray; you see
+  it fill up)
 - A full ashtray gets emptied by somebody tidy (into a scrap of paper, off
   to the bin)
 - K: light one (yours, or somebody next to you gives you a light), K again
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 84a85f3..3b2e291 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -77,6 +77,10 @@ Smooth lines (`ink.frag`): edgeAt looked at in 4 spots inside the pixel (rotated
 
 NPCs onto things (1.88): the onto-step (up to STEP_OVER_HEIGHT x 1.15) and walking onto flat things work for OBJ_HUMAN too; `ai.c` longToStepOnto: standable loose things 1.5 m+ long and up to ONTO_HIGH 0.55 m are not in the way for steering (`Physics_standsOn`), so people step onto a log across their way instead of walking round; a step off says "Whoa!" sometimes. Edges (`physics.c` middleOverAny): a character lands on something only with its middle over one of its colliders (OVER_EDGE 0.12 m past an edge at most); else it slides off away from it (the body box's corner used to hold it in the air). `world.c` clutterQuery: a tilted collider (no axis within 0.98 of upright) counts with its surface at the foot (rays down there and towards it), not its highest corner (invisible steps on roofs and tilted boxes).
 
+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.
+
+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).
+
 Tiredness (`object.c` Object_tiredness: sleepiness from 0.25 up x (1 - 0.35 energy) x (1.2 - 0.4 fitness) + 0.4 drunk): the stamina ceiling TIRED_CAP 0.55 lower, effort TIRED_EFFORT 0.6 harder, rest (awake) TIRED_REST 0.5 slower (`condition.c`), Object_vigor 0.25 lower (speed, jumps, work).
 
 Balancing (`world.c` -> `body.balance_goal` from `Physics_footing`: 1 at a top 0.3 m wide or less, 0 from 0.75 m; `body.c` updateBalance): `teeter` is a sprung wobble (3.2 rad/s, damping 0.35) kicked at random (TEETER_KICK 40 x (0.25 + 0.6 clumsy + 1.6 drunk + 0.35 speed + 0.4 nerves) x (1.25 - 0.75 fitness)); past 1: Body_stagger sideways (`stepped_off` -> "Whoa - you lose your balance and step off"). Pose: arms out to the sides (the one on the tipping side lower), the hips with the wobble, the upper body against it; walking BALANCE_SLOW 0.55 slower.
@@ -212,9 +216,9 @@ Most important first.
 **People**
 - The work place, still to do: other jobs than split / cut (felling room, trash); walking a heavy piece round tight corners
 - Ladders, still to do: a holder for a ladder standing free; NPCs taking a ladder somewhere for their own work
-- Smoking, still to do: buying more (a kiosk); the butts showing in the tin (the mesh always has 3)
+- Smoking, still to do: buying more (a kiosk)
 - "No shirt no service" policy (decision open)
-- Clothing, still to do: more outfits for extra people (they all wear trousers); fetching the jacket from somewhere
+- Clothing, still to do: more outfits for extra people (only the performance test has any: they all wear trousers); hanging the jacket back on the rack
 
 **Tools for us**
 - Optional local AI (llama.cpp, small model) for talking and tasks; the menu stays as fallback
@@ -223,4 +227,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 1.88.0 (build d1db253)`
+State of `entropie 1.89.0 (build c1f9348)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index 22763be..f64dcfc 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,11 +1,9 @@
-entropie 1.88.0: the others step onto logs, no invisible steps, tired days
+entropie 1.89.0: the coat rack, a full tin, a real ramp
 
-- The others step onto a long log across their way and walk on (wobbling),
-  shorter things they still walk round
-- Fix: an invisible step next to things - the edge of the body held you up
-  in the air beside it; now you go down once your middle is past the edge
-- Fix: invisible steps on slopes (a roof, a tilted box): the feet took the
-  highest corner of it instead of the surface right there
-- Tired: the later the day, the less energy - a lower ceiling, used up
-  quicker, back slower, all a bit weaker; lively and sporty people hold up
-  better, drink makes it worse
+- A coat rack in your kitchen with everybody's jacket: getting cold, people
+  walk over and put theirs on; yours: click the rack (on, or back on it)
+- The ashtray tin shows its butts, filling up to a little heap
+- The tilted test box that hung in the air at one end is now a plank ramp
+  resting on a block - you can walk up it
+- Fix: walking onto anything tilted (a ramp) was blocked: the step up took
+  its highest corner instead of the surface in front of you
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 520548c..6f15418 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -186,7 +186,7 @@ bool Game_init(Game *game) {
     Camera_init(&game->camera, vec3(0.0f, 1.8f, 5.0f), -90.0f, 0.0f, CAMERA_FOV);
 
     game->player = Scene_load(&game->world);
-    if (!game->player || !World_buildTerrain(&game->world) || !Site_finish(&game->world)) {
+    if (!game->player || !World_buildTerrain(&game->world) || !Site_finish(&game->world) || !Scene_finish(&game->world)) {
         SDL_Log("Scene loading failed");
         Game_shutdown(game);
         return false;
@@ -249,6 +249,10 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     game_climbLadder(game, input, dt);
     game_climb(game, input, dt);
     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");
+    }
     if (game->player->body.stepped_off) {
         game->player->body.stepped_off = false;
         say(game, "Whoa - you lose your balance and step off");
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index e90e0cd..4f7864c 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -427,6 +427,22 @@ static Object *furnitureNear(const Game *game, FurnitureKind kind, float reach)
     return NULL;
 }
 
+/* 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++)
@@ -677,7 +693,7 @@ static int leftClick(Game *game) {
         Talk_showHere(&game->world, p, shown);
         return CLICK_NONE;
     }
-    if (mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game)) return CLICK_NONE;
+    if (mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || coatRack(game)) return CLICK_NONE;
     int watering = useWater(game);
     if (watering >= 0) return watering;
     if (bagTrash(game)) return CLICK_BAG;
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 344412c..7ca8393 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -1302,6 +1302,29 @@ 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 */
+
+static bool fetchJacket(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) {
+        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;
+    ai->want_jacket = false;
+    if (random01(ai) < 0.5f) Ai_say(self, "Ah, better.");
+    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;
@@ -1358,6 +1381,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;
     /* Tidy people pick things up on the way */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || (ai->mode == AI_HELP && !ai->pushing && !ai->copied) ||
          ai->mode == AI_SUPPLY || ai->mode == AI_FETCH) && ai_tidyUp(self, objects, count, dt))
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index 008c50a..48c2780 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -168,6 +168,8 @@ 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 */
+    float          jacket_time; /* ... on the way this long */
     bool           tool_waiting; /* waiting for the player to put the axe down */
     unsigned char  banned;      /* tools (1 << ToolKind) they were told to leave alone ... */
     unsigned char  defies;      /* ... and those they use anyway ("I know what I'm doing") */
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index 1a736d8..db0f8c1 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -105,13 +105,17 @@ 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_COUNT
+    FURN_CHAIR, FURN_BENCH, FURN_CAR, FURN_TIRES, FURN_TOILET, FURN_TAP, FURN_ASHTRAY, FURN_COATRACK, FURN_COUNT
 } FurnitureKind;
 #define SEAT_HEIGHT 0.45f  /* chairs and log benches */
 
 /* Furniture standing on y = 0, size = its box (length along X, height, depth along Z).
    Fronts face +Z. */
 Mesh  Shapes_furniture(FurnitureKind kind, Vec3 size);
+/* The ashtray with this many butts in it (a few show, a heap when full). */
+Mesh  Shapes_ashtray(Vec3 size, int butts);
+/* The coat rack with these jackets hanging on it (n at most 6). */
+Mesh  Shapes_coatRack(Vec3 size, const Vec3 *jackets, int n);
 
 /* A dachshund, its nose towards -Z, standing on y = 0: body (with head and
    ears), one leg (hanging down from its top), the tail (from its root, along +Z). */
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index ecc2b71..513e99c 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -680,6 +680,54 @@ static void addWheel(MeshBuilder *b, Vec3 center, float radius, float width) {
     transformSince(b, first, mat4_mul(mat4_translate(center), mat4_rotate_z(PI_F / 2.0f)));
 }
 
+/* An old tin can as ashtray: the butts stick out of the ash, more and more,
+   a little heap of them when it's full. */
+static void buildAshtray(MeshBuilder *b, Vec3 size, int butts) {
+    const Vec3 tin = { 0.62f, 0.62f, 0.60f }, filter = { 0.86f, 0.6f, 0.34f }, ash = { 0.25f, 0.24f, 0.22f };
+    shapes_addTube(b, 0.0f, size.x * 0.5f, size.x * 0.48f, size.y, 10, tin, true);
+    float fill = fminf((float)butts / 14.0f, 1.0f);
+    addBlock(b, vec3(0.0f, size.y * (0.7f + 0.28f * fill), 0.0f), vec3(size.x * 0.8f, 0.006f, size.x * 0.8f), ash);
+    int shown = butts < 12 ? butts : 12;
+    for (int k = 0; k < shown; k++) {
+        float a = (float)k * 2.39996f, r = 0.012f + 0.022f * sqrtf((float)k / 12.0f);
+        float up = size.y * (0.72f + 0.28f * fill) + 0.006f + 0.008f * (float)(k / 6); /* (full: piled up) */
+        addBlock(b, vec3(r * cosf(a), up, r * sinf(a)), vec3(0.018f, 0.012f, 0.011f), filter);
+    }
+}
+
+/* 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);
+    buildAshtray(&b, size, 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);
@@ -785,16 +833,12 @@ Mesh Shapes_furniture(FurnitureKind kind, Vec3 size) {
         transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, 0.4f, -hz + 0.1f)), mat4_rotate_x(PI_F / 2.0f)));
         break;
     }
-    case FURN_ASHTRAY: { /* an old tin can by the fire, a few butts sticking out */
-        const Vec3 tin = { 0.62f, 0.62f, 0.60f }, filter = { 0.86f, 0.6f, 0.34f };
-        shapes_addTube(&b, 0.0f, size.x * 0.5f, size.x * 0.48f, size.y, 10, tin, true);
-        addBlock(&b, vec3(0.0f, size.y * 0.98f, 0.0f), vec3(size.x * 0.8f, 0.006f, size.x * 0.8f), (Vec3){ 0.25f, 0.24f, 0.22f }); /* ash */
-        for (int k = 0; k < 3; k++) {
-            float a = (float)k * 2.1f;
-            addBlock(&b, vec3(0.02f * cosf(a), size.y + 0.008f, 0.02f * sinf(a)), vec3(0.018f, 0.012f, 0.011f), filter);
-        }
+    case FURN_ASHTRAY:
+        buildAshtray(&b, size, 3);
+        break;
+    case FURN_COATRACK:
+        buildCoatRack(&b, size, NULL, 0);
         break;
-    }
     case FURN_COUNT:
         break;
     }
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index c69406a..416eeb8 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -236,6 +236,7 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
 
     case OBJ_FURNITURE:
         obj->variant   = (int)weight; /* the kind, see Object_createFurniture */
+        obj->shown     = -1;
         obj->weight    = 100.0f;
         obj->mesh      = Shapes_furniture((FurnitureKind)obj->variant, size);
         obj->is_static = true;
@@ -1021,6 +1022,7 @@ void Object_describe(const Object *obj, char *out, size_t size) {
             [FURN_TOILET] = "Toilet - P to use it", [FURN_TAP] = "Tap - the garden hose; fill the watering can here",
         };
         if (obj->variant == FURN_ASHTRAY) { snprintf(out, size, "Ashtray (an old tin) - %d butts in it", obj->stored); 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;
     }
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index fca2c2f..a6e7283 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -249,6 +249,9 @@ typedef struct Object {
     float      raised, raise_goal; /* ladders carried slid out: held near the foot, this steep up in front (radians, 0 = carried at the side) */
     bool       dipped;        /* ... tipped down flat to get under something (a beam, a doorway) */
     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 */
     bool       leaning;       /* ladders: leaning against something (stands still) */
     float      air_fall;      /* characters: in the air, falling this fast at most (m/s; for the landing) */
     float      ladder_skill;  /* characters: practice climbing ladders one-handed (0..1) ... */
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index 4b99f6b..240c9ab 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -217,6 +217,28 @@ static void slideAlong(Object *obj, Vec3 normal, float depth) {
  * Characters walking into a low, flat-topped obstacle step onto it.
  * Returns true if the step was taken.
  */
+/* Straight down from origin into the collider: how far to its surface (-1: misses it). */
+#define STEP_PROBE 0.3f /* m ahead where a step lands */
+
+static float rayDownInto(Vec3 origin, const Object *o, int ic) {
+    const Collider *c = &o->colliders[ic];
+    Vec3 rel = vec3_sub(origin, Object_colliderCenter(o, ic));
+    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
+    float t0 = 0.0f, t1 = 1e9f;
+    for (int k = 0; k < 3; k++) {
+        float p = vec3_dot(rel, c->axes[k]), d = -c->axes[k].y; /* (the ray goes down) */
+        if (fabsf(d) < 1e-6f) {
+            if (fabsf(p) > half[k]) return -1.0f;
+            continue;
+        }
+        float a = (-half[k] - p) / d, b = (half[k] - p) / d;
+        if (a > b) { float x = a; a = b; b = x; }
+        t0 = fmaxf(t0, a);
+        t1 = fminf(t1, b);
+    }
+    return t0 <= t1 ? t0 : -1.0f;
+}
+
 static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 normal, float most) {
     if (!obj->is_character) return false;
     /* Only walking into it, not brushing past along its side */
@@ -227,6 +249,13 @@ static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 nor
     if (c->axes[1].y < WALKABLE_SLOPE) return false; /* top face not walkable */
 
     float top    = Object_colliderCenter(other, ic).y + projectRadius(c, WORLD_UP);
+    if (fmaxf(fabsf(c->axes[0].y), fmaxf(fabsf(c->axes[1].y), fabsf(c->axes[2].y))) < 0.98f) {
+        /* tilted (a ramp): its surface just in front of you, not its highest corner */
+        Vec3 ahead = vec3_add(obj->position, vec3_scale(vec3_normalize(mv), STEP_PROBE));
+        float t = rayDownInto(vec3(ahead.x, top + 0.5f, ahead.z), other, ic);
+        if (t < 0.0f) return false;
+        top = top + 0.5f - t;
+    }
     float bottom = Object_colliderCenter(obj, io).y - projectRadius(&obj->colliders[io], WORLD_UP);
     float rise   = top - bottom;
     if (rise <= 0.0f || rise > most) return false;
@@ -347,8 +376,10 @@ static bool middleOver(const Object *o, int ic, Vec3 p, float margin) {
         Vec3 axis = vec3(c->axes[a].x, 0.0f, c->axes[a].z);
         float len = vec3_length(axis);
         if (len < 0.3f) continue; /* (an upright axis) */
-        if (fabsf(vec3_dot(rel, vec3_scale(axis, 1.0f / len))) > half[a] * len + margin) return false;
+        Vec3 dir = vec3_scale(axis, 1.0f / len);
+        if (fabsf(vec3_dot(rel, dir)) > projectRadius(c, dir) + margin) return false; /* (its whole outline seen from above) */
     }
+    (void)half;
     return true;
 }
 
diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c
index 120ca46..3723a6a 100644
--- a/entropie/src/world/scene.c
+++ b/entropie/src/world/scene.c
@@ -52,7 +52,6 @@ static const Spawn SCENE[] = {
     /* Pushable objects */
     { OBJ_CYLINDER, PALETTE_WARM,   { -3, 0, 1 }, { 1, 3, 1 }, 1200, { 90, 0, 0 } }, /* takes two or more */
     { OBJ_CYLINDER, PALETTE_MODERN, { -3, 0, 3 }, { 1, 3, 1 },  100, NO_ROTATION },
-    { OBJ_BOX,      PALETTE_MODERN, {  2, 2, 8 }, { 1, 1, 3 },  100, { 20, 0, 0 } },
     { OBJ_BOX,      PALETTE_MODERN, {  1, 0, 2 }, { 2, 1, 2 },   20, NO_ROTATION },
 
     /* (the tools are in the tool room of your garages, see site.c) */
@@ -117,6 +116,49 @@ static const Character CHARACTERS[] = {
       0.75f, 2.4f, BREAK_SIT, 50.0f, 40.0f },     /* slow, ponders, sits down a lot */
 };
 
+/* A ramp to walk up: a plank 20 degrees up, its foot on the ground, its top
+   resting on a block (a loose box left tilted would hang in the air: loose
+   things don't turn in the physics). */
+#define RAMP_AT     { 2.0f, 0.0f, 8.0f }
+#define RAMP_SIZE   { 1.2f, 0.15f, 3.2f }
+#define RAMP_SLOPE  20.0f
+
+static bool addRamp(World *world, Vec3 yard) {
+    const Vec3 at = vec3_add(yard, (Vec3)RAMP_AT);
+    Object *ramp = Object_create(OBJ_BOX_STATIC, PALETTE_MODERN, at, (Vec3)RAMP_SIZE, 100.0f);
+    if (!ramp) return false;
+    Object_setRotation(ramp, vec3(deg2rad(RAMP_SLOPE), 0.0f, 0.0f));
+    /* its lowest and highest corners: the low end goes onto the ground */
+    const Collider *c = &ramp->colliders[0];
+    Vec3 center = Object_colliderCenter(ramp, 0), low = center, high = center;
+    for (int k = 0; k < 8; k++) {
+        Vec3 p = center;
+        p = vec3_add(p, vec3_scale(c->axes[0], (k & 1 ? 1.0f : -1.0f) * c->half_extents.x));
+        p = vec3_add(p, vec3_scale(c->axes[1], (k & 2 ? 1.0f : -1.0f) * c->half_extents.y));
+        p = vec3_add(p, vec3_scale(c->axes[2], (k & 4 ? 1.0f : -1.0f) * c->half_extents.z));
+        if (p.y < low.y) low = p;
+        if (p.y > high.y) high = p;
+    }
+    ramp->position.y += Terrain_height(&world->terrain, low.x, low.z) - 0.03f - low.y;
+    Object_syncColliders(ramp);
+    if (!World_add(world, ramp)) return false;
+    /* the block under its top end (up to the ramp's underside there) */
+    Vec3 up_end = vec3_add(center, vec3_scale(c->axes[2], vec3_dot(vec3_sub(high, center), c->axes[2]) > 0.0f ? c->half_extents.z : -c->half_extents.z));
+    up_end.y += Terrain_height(&world->terrain, low.x, low.z) - 0.03f - low.y;
+    Vec3 out = vec3_normalize(vec3(c->axes[2].x, 0.0f, c->axes[2].z));
+    if (vec3_dot(out, vec3_sub(high, low)) < 0.0f) out = vec3_scale(out, -1.0f);
+    Vec3 foot = vec3_sub(up_end, vec3_scale(out, 0.25f)); /* (a bit in from the end: the plank sticks out over it) */
+    float ground = Terrain_height(&world->terrain, foot.x, foot.z);
+    float under = up_end.y - fabsf(c->axes[1].y) * c->half_extents.y - 0.12f - ground; /* (the underside, a bit lower there) */
+    if (under < 0.1f) return true;
+    Object *block = Object_create(OBJ_BOX_STATIC, PALETTE_GROUND, vec3(foot.x, ground, foot.z), vec3(1.3f, under, 0.4f), 100.0f);
+    return block && World_add(world, block);
+}
+
+bool Scene_finish(World *world) {
+    return addRamp(world, Site_yard());
+}
+
 Object *Scene_load(World *world) {
     Object *player = NULL;
     int characters = 0;
@@ -147,6 +189,7 @@ Object *Scene_load(World *world) {
             obj->lighter_uses = obj->lighter ? LIGHTER_USES * (1 + characters % 3) / 3 : 0; /* (some nearly empty) */
             obj->smoker     = SMOKER[characters % TEMPERAMENT_COUNT];
             obj->wits       = WITS[characters % TEMPERAMENT_COUNT];
+            obj->jacket_hung = true; /* (on the rack in your garage) */
             obj->cigarettes = CIGARETTES[characters % TEMPERAMENT_COUNT];
             obj->tobacco    = TOBACCO[characters % TEMPERAMENT_COUNT];
             obj->craving    = 0.3f + 0.15f * (float)(characters % 3);
diff --git a/entropie/src/world/scene.h b/entropie/src/world/scene.h
index 31c7ecf..4d1d556 100644
--- a/entropie/src/world/scene.h
+++ b/entropie/src/world/scene.h
@@ -9,5 +9,7 @@
 
 /* Fills the world. Returns the player object, or NULL on failure. */
 Object *Scene_load(World *world);
+/* Once the ground is there: what stands on it just so (the ramp in the yard). */
+bool    Scene_finish(World *world);
 
 #endif /* WORLD_SCENE_H */
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index eaa1ce7..bb4f432 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -709,6 +709,7 @@ static const RoomPiece ROOMS[] = {
     { FURN_TABLE,     { 23.3f, 0, 4.0f }, { 1.6f, 0.75f, 0.9f }, 0.0f },
     { FURN_CHAIR,     { 22.5f, 0, 4.0f }, { 0.45f, 0.9f, 0.45f }, PI_F },
     { FURN_CHAIR,     { 24.1f, 0, 4.0f }, { 0.45f, 0.9f, 0.45f }, 0.0f },
+    { FURN_COATRACK,  { 20.6f, 0, 2.4f }, { 0.5f, 1.75f, 0.5f }, 0.0f },   /* everybody's jacket */
     /* 3: car repairs (the wide door) */
     { FURN_CAR,       { 23.4f, 0, -11.2f }, { 1.8f, 1.5f, 4.2f }, 0.0f },
     { FURN_WORKBENCH, { 26.6f, 0, -6.6f }, { 2.4f, 0.9f, 0.7f }, 0.0f },
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index ef40ce1..ef89e0a 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -117,7 +117,9 @@ static float clutterQuery(const void *ctx, float x, float z, float radius, float
                 Vec3 axis = vec3(col->axes[a].x, 0.0f, col->axes[a].z);
                 float len = vec3_length(axis);
                 if (len < 0.3f) continue; /* (an upright axis) */
-                float d = fabsf(vec3_dot(rel, vec3_scale(axis, 1.0f / len))) - half[a] * len;
+                Vec3 dir = vec3_scale(axis, 1.0f / len);
+                float extent = fabsf(vec3_dot(dir, col->axes[0])) * half[0] + fabsf(vec3_dot(dir, col->axes[1])) * half[1] + fabsf(vec3_dot(dir, col->axes[2])) * half[2];
+                float d = fabsf(vec3_dot(rel, dir)) - extent; /* (its outline seen from above) */
                 gap = fmaxf(gap, d);
             }
             if (gap > radius) continue;
@@ -667,10 +669,48 @@ static void dressForNight(World *world, Object *o, float dt) {
     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) {
-        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.");
+        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 */
+            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.");
+        }
     } else if (o->look.top == TOP_JACKET && o->chill < 0.15f) {
-        o->look.top = TOP_SHIRT;
+        o->look.top = TOP_SHIRT; /* (off, and along with them: round the waist) */
+    }
+    if (o->chill < 0.3f) o->jacket_told = false;
+}
+
+/* The tin shows its butts, the coat rack the jackets on it: their meshes are
+   built again when that changes. */
+static void showProps(World *world) {
+    for (int i = 0; i < world->count; i++) {
+        Object *f = world->objects[i];
+        if (f->type != OBJ_FURNITURE) continue;
+        if (f->variant == FURN_ASHTRAY) {
+            int butts = f->stored < 12 ? f->stored : 12 + (f->stored >= 14);
+            if (butts == f->shown) continue;
+            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;
+        }
     }
 }
 
@@ -1013,7 +1053,8 @@ static void emptyTray(World *world, Object *o) {
 }
 
 void World_update(World *world, float dt, float game_dt) {
-    Object_indexKinds(world->objects, world->count); /* (who's a person, where the fires are: looked up again and again) */
+    Object_indexKinds(world->objects, world->count);
+    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];
         if (o->type == OBJ_DOG) Dog_update(o, world->objects, world->count, dt);