Nimbin[12]?Games / Entropie / commits / 0724cc1

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.90.0: walk into the main building, a brick barn, the shell

Christian Immanuel · 2026-10-15 21:35 · 0724cc1bf8fdbeefd1612cf29c61f9bb4be0524b

- The main building rebuilt of parts you can walk into: two halves side by
  side (the right one a little further out), basement windows, the ground
  floor four steps up. Left door: steps up, the locked basement door; the
  corridor with toilet, kitchen, two locked rooms (the living room); the big
  room of the right half with the meeting table. A porch along the right
  half: in at its far end, up the steps, right through the door
- Your unfinished building: a brick shell under a roof you can walk into,
  the tree frame in front of it as its extension (for the bar)
- The barn shows its bricks, ivy climbs high up its walls; a woodpile against
  it left of the toilet, everybody's woodpile from the start
- The fireplace's tripod: wooden poles, longer, standing well away from the
  fire, solid
- Green wooden gates in your garages' doorways: click to open or shut
 entropie/Makefile                  |   2 +-
 entropie/README.md                 |  22 ++-
 entropie/STATE.md                  |  19 +-
 entropie/commit_message.txt        |  22 ++-
 entropie/src/game/game_actions.c   |  17 +-
 entropie/src/people/ai.c           |   2 +-
 entropie/src/render/shapes.c       | 108 ++++++++++-
 entropie/src/render/shapes.h       |  31 +++-
 entropie/src/render/shapes_props.c |  62 ++++++-
 entropie/src/world/ladder.c        |   2 +-
 entropie/src/world/manor.c         | 363 +++++++++++++++++++++++++++++++++++++
 entropie/src/world/manor.h         |  30 +++
 entropie/src/world/object.c        |  37 +++-
 entropie/src/world/object.h        |  11 ++
 entropie/src/world/physics.c       |   2 +-
 entropie/src/world/scene.c         |   6 +
 entropie/src/world/site.c          | 116 ++++++++++--
 entropie/src/world/world.c         |   5 +-
 entropie/src/world/world.h         |   1 +
 19 files changed, 813 insertions(+), 45 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index b935137..216c863 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.89.0
+VERSION   := 1.90.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index a5dd0cb..2e91db6 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -32,14 +32,28 @@ and satellite pictures (about 340 x 340 m).
   benches, walls and blocks, spaced and close together. (Testing: set
   `START_AT_COURSE` in `src/game/game.c` to 1 to start there)
 - You start in the courtyard, a three-sided farmyard open to a gravel road:
-  the long main building (two wings, two floors, the front door right
-  beside your garages), your garages (you can walk in) joined to it, the
-  long barn opposite and your unfinished building at the barn's right end.
-  Floors are 2.8 m high
+  the main building, your garages (you can walk in) joined to it, the long
+  brick barn opposite (ivy climbing high) and your unfinished building at the
+  barn's right end
+- The main building has two halves side by side, the right one a little
+  further out, with a porch in front. The ground floor sits four steps up
+  (basement windows below). Walk in:
+  - Left door, by your garages: the basement door on the right (locked), four
+    steps up on the left. Along the corridor: the toilet and the kitchen on the
+    right, two locked rooms on the left (the second the living room), and at
+    its end the big room of the right half with the meeting table
+  - Or in at the porch's far end, up its steps and right through the door
+- Your unfinished building: a bare brick shell under a roof (later: drinks),
+  and in front of it the frame of tree trunks where the bar will be built
+- Your garages' doorways have green wooden gates: click to open or shut
 - 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, the coat rack; hold food over the stove to cook it) and a car repair
   place with an old car behind the wide door
+- The woodpile by the barn, left of the toilet: everybody stacks firewood
+  there unless you show them another place
+- The fireplace's tripod is of wooden poles standing well away from the
+  fire; you walk round them
 - 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)
 - A little path leads out between the garages and the barn
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 3b2e291..4435e04 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).
 
+Buildings made of parts (1.90, `world/manor.c`): OBJ_STRUCTURE (one static mesh, no collision: `Shapes_structure` = boxes + windows / panes / doors / door frames + gable roofs, the roof split out of addBuilding as `addRoof`) and OBJ_WALL (static invisible boxes: the solid parts; variant 1 a locked door, clicked says its name; 2 an outer wall under eaves, for the ladder; 3 a roof slab). Not drawn when there's no mesh. The main building (Manor_build at MANOR_A/B, yaw across; local x along it, +z to the yard): left half x -18..0, z -5..5; right half x 0..18, z -4.2..5.8; ground floor FL 0.7 (4 steps of 0.175), ceiling 3.4, eaves 6.2. Left: front door x -7.6; entrance on the ground (x -9.6..-5.75, z 1.4..4.7), steps up on its left (x -9.6..-8.4, from z 2.6 to 1.4), rail along the edge, basement door (locked) on its right; corridor z -0.75..0.75 to the opening into the right half; toilet x -5.6..-3.75 and kitchen -3.6..0 on the yard side (bowl, counter, stove, table); a locked room and the living room on the back side; a closed store room behind your garages. Right: the big room with the 4.2 m table and 8 chairs; its door x 2.3 from the porch landing. Porch z 5.8..7.3, x 1..18: way in at x 18, ground floor to x 5, four steps up going left, landing to the door. Basement stairs going down need a hole in the ground (the terrain is a height field, flat under buildings): for now only the locked door. The old OBJ_HOUSE main building and its wing are gone.
+
+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.
 
 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).
@@ -206,6 +210,19 @@ Most important first.
 **Bugs**
 - Joint jumps left in the audit: jog+stop 171, sprint 297 (worst 26 cm) - running into things: a hurried catch step swings through in a few frames (step 0.07 -> 0.34, the ankle up 20 cm). Capping the swing speed helped sprint (278) but made jumps, drunk walk and the NPCs worse - tried and reverted in 1.83; ladder 45: toes on the sloping roof (a landing step's toes snap onto the slope, 16 cm; blending them in made all walking worse), toes turning onto the rungs getting on from the top (14 cm)
 
+**From the 15th of October (to do)**
+- Tree stump: glitches out of the ground when a tree is felled; trees in general: the trunk looks hollow; push / pull fallen trees; branches break or snag on obstacles (with enough momentum); much easier to carry without branches
+- The pelvis swings far left and right when strafing
+- Walking stiff: the body hardly rises and falls (higher mid-step)
+- The ramp's edge: bad glitches stepping over it; loose things slide down slopes, more the steeper
+- Steps on the course (middle lane) land above the surface: feet must touch what they stand on
+- Things with physics must lie properly on the ground (the course's wood floats a little): rest on their real contact points on uneven ground; feet too (sometimes hovering) - check the drawing against the physics
+- Holding a ladder: don't get in the way of whoever's climbing (wait a moment, or hold it from the side). NPCs on a roof: slow and careful; afraid of heights: don't go up; drunk: more careful, or dumber
+- Breaks: put the tool down, a coffee, a walk, a chat; short and long ones; called away, they go back to the old task afterwards (or you remind them)
+- Tired from the day before, by what they did
+- Woodpiles: drop "show me where" (you can place it yourself), maybe "find a good place"; people think between tasks
+- Alice: doesn't fit the art style, and dumb; she tries to catch things thrown, eats a sausage dropped on the ground, may catch a thrown apple (and not bring it back). NPC pathfinding has problems
+
 **Body and moving**
 - Obstacle approach, still to do: NPCs stepping up onto higher things (now only what they'd stride over anyway); thin round logs (under 0.2 m) are still stepped over; a step in between on top of something unstable (if clumsy); NPCs: pull up on their own for their own reasons (now only following a leader), jumping onto things
 - Climbing rework: hands first (one reaches first, shoulder ahead), a knee up, the other foot next to it; or sit on it and lift the legs; or belly first and roll; by height, surface and person
@@ -227,4 +244,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.89.0 (build c1f9348)`
+State of `entropie 1.90.0 (build 81ba84f)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index f64dcfc..3489211 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,9 +1,15 @@
-entropie 1.89.0: the coat rack, a full tin, a real ramp
+entropie 1.90.0: walk into the main building, a brick barn, the shell
 
-- 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
+- The main building rebuilt of parts you can walk into: two halves side by
+  side (the right one a little further out), basement windows, the ground
+  floor four steps up. Left door: steps up, the locked basement door; the
+  corridor with toilet, kitchen, two locked rooms (the living room); the big
+  room of the right half with the meeting table. A porch along the right
+  half: in at its far end, up the steps, right through the door
+- Your unfinished building: a brick shell under a roof you can walk into,
+  the tree frame in front of it as its extension (for the bar)
+- The barn shows its bricks, ivy climbs high up its walls; a woodpile against
+  it left of the toilet, everybody's woodpile from the start
+- The fireplace's tripod: wooden poles, longer, standing well away from the
+  fire, solid
+- Green wooden gates in your garages' doorways: click to open or shut
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index 4f7864c..9223792 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -427,6 +427,21 @@ static Object *furnitureNear(const Game *game, FurnitureKind kind, float reach)
     return NULL;
 }
 
+/* At a gate: open it, or shut it (both leaves of a double gate go together). */
+static bool swingGate(Game *game) {
+    Object *g = furnitureNear(game, FURN_GATE, 1.2f);
+    if (!g) return false;
+    bool open = !g->gate_open;
+    for (int i = 0; i < game->world.count; i++) { /* (its other leaf: the hinges 6 m apart at most, the same way out) */
+        Object *o = game->world.objects[i];
+        if (o->type == OBJ_FURNITURE && o->variant == FURN_GATE && vec3_length(vec3_sub(o->hinge, g->hinge)) < 5.1f &&
+            vec3_dot(o->gate_out, g->gate_out) > 0.9f && fabsf(vec3_dot(vec3_sub(o->hinge, g->hinge), g->gate_shut)) <= g->size.x * 2.0f + 0.2f)
+            o->gate_open = open;
+    }
+    say(game, open ? "You open the gate" : "You shut the gate");
+    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;
@@ -693,7 +708,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)) return CLICK_NONE;
+    if (mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || coatRack(game) || swingGate(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 7ca8393..433eaae 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -641,7 +641,7 @@ static bool isCover(const Object *o) {
     switch (o->type) {
     case OBJ_BUSH: return o->size.y > 0.6f;
     case OBJ_TREE: return !o->toppled;
-    case OBJ_HOUSE: case OBJ_BARN: return true;
+    case OBJ_HOUSE: case OBJ_BARN: case OBJ_WALL: return true;
     case OBJ_BOX_STATIC: return o->size.y > 1.2f;
     case OBJ_FURNITURE: return o->variant == FURN_CAR;
     default: return false;
diff --git a/entropie/src/render/shapes.c b/entropie/src/render/shapes.c
index dc632ff..6e17855 100644
--- a/entropie/src/render/shapes.c
+++ b/entropie/src/render/shapes.c
@@ -189,20 +189,22 @@ static void addTiledSlope(MeshBuilder *b, float lx, float wz, float wy, float ro
     }
 }
 
-static void addBuilding(MeshBuilder *b, float length, float width, float wall_height,
-                        float roof_height, BuildingLook look) {
+static Vec3 wallColor(BuildingLook look) {
+    const bool plaster = look == BUILDING_PLASTER || look == BUILDING_PLASTER_DARK;
+    return look == BUILDING_BRICK ? COLOR_BRICK : plaster ? COLOR_PLASTER : COLOR_HALL_WALL;
+}
+
+/* The roof on walls of that size (ridge along X): tiled or a plain hall roof, and the gables. */
+static void addRoof(MeshBuilder *b, float length, float width, float wall_height, float roof_height, BuildingLook look) {
     const bool plaster = look == BUILDING_PLASTER || look == BUILDING_PLASTER_DARK;
     const bool tiled = plaster || look == BUILDING_BRICK;
-    const Vec3 wall = look == BUILDING_BRICK ? COLOR_BRICK : plaster ? COLOR_PLASTER : COLOR_HALL_WALL;
+    const Vec3 wall = wallColor(look);
     const Vec3 roof = look == BUILDING_BRICK ? COLOR_ROOF : look == BUILDING_PLASTER ? COLOR_LIGHT_ROOF
                     : look == BUILDING_PLASTER_DARK ? COLOR_DARK_ROOF
                     : look == BUILDING_HALL_RED ? COLOR_RED_ROOF : COLOR_HALL_ROOF;
-    const Vec3 brick[FACE_COUNT] = { wall, wall, wall, wall, wall, wall };
     const float lx = length * 0.5f, wz = width * 0.5f;
     const float wy = wall_height, ry = wall_height + roof_height;
 
-    shapes_addBox(b, vec3(0.0f, wy * 0.5f, 0.0f), vec3(length, wall_height, width), brick);
-
     if (tiled) { /* tiles, overhanging eaves and verges, a ridge cap */
         addTiledSlope(b, lx, wz, wy, roof_height, -1.0f, roof);
         addTiledSlope(b, lx, wz, wy, roof_height, 1.0f, roof);
@@ -231,6 +233,51 @@ static void addBuilding(MeshBuilder *b, float length, float width, float wall_he
                              vec3(1, 0, 0), gable, CELL_SIZE);
 }
 
+/* Brick walls: courses of mortar and the joints between the bricks, staggered,
+   only a shade lighter (the ink doesn't trace every one), on the four faces. */
+#define COURSE      0.42f /* m up the wall per course (a few bricks: from afar it reads as brick) */
+#define BRICK_LEN   0.9f
+#define MORTAR      0.03f
+
+static void addBrickCourses(MeshBuilder *b, float length, float width, float wall_height, Vec3 wall) {
+    const Vec3 mortar = vec3_add(wall, vec3(0.06f, 0.06f, 0.05f));
+    const float lx = length * 0.5f, wz = width * 0.5f, out = 0.006f;
+    for (int face = 0; face < 4; face++) {
+        /* faces: +z, -z (length long), +x, -x (width long) */
+        float span = face < 2 ? length : width, half = span * 0.5f;
+        for (int c = 1; (float)c * COURSE < wall_height - 0.1f; c++) {
+            float y = (float)c * COURSE;
+            float y0 = y - MORTAR * 0.5f, y1 = y + MORTAR * 0.5f;
+            /* the course line, then the joints of the course above it */
+            for (int j = -1; j < (int)(span / BRICK_LEN) + 1; j++) {
+                float a0 = j < 0 ? -half : -half + ((float)j + (c % 2 ? 0.5f : 0.0f)) * BRICK_LEN;
+                float a1 = j < 0 ? half : a0 + MORTAR;
+                if (j >= 0 && (a0 < -half + 0.05f || a1 > half - 0.05f)) continue;
+                float ya = j < 0 ? y0 : y1, yb = j < 0 ? y1 : fminf(y + COURSE - MORTAR * 0.5f, wall_height - 0.02f);
+                Vec3 n, p0, p1, p2, p3;
+                switch (face) {
+                case 0:  n = vec3(0, 0, 1);  p0 = vec3(a0, ya, wz + out);  p1 = vec3(a1, ya, wz + out);  p2 = vec3(a1, yb, wz + out);  p3 = vec3(a0, yb, wz + out); break;
+                case 1:  n = vec3(0, 0, -1); p0 = vec3(a1, ya, -wz - out); p1 = vec3(a0, ya, -wz - out); p2 = vec3(a0, yb, -wz - out); p3 = vec3(a1, yb, -wz - out); break;
+                case 2:  n = vec3(1, 0, 0);  p0 = vec3(lx + out, ya, a1);  p1 = vec3(lx + out, ya, a0);  p2 = vec3(lx + out, yb, a0);  p3 = vec3(lx + out, yb, a1); break;
+                default: n = vec3(-1, 0, 0); p0 = vec3(-lx - out, ya, a0); p1 = vec3(-lx - out, ya, a1); p2 = vec3(-lx - out, yb, a1); p3 = vec3(-lx - out, yb, a0); break;
+                }
+                GLuint i0 = MeshBuilder_vertex(b, p0, n, mortar), i1 = MeshBuilder_vertex(b, p1, n, mortar);
+                GLuint i2 = MeshBuilder_vertex(b, p2, n, mortar), i3 = MeshBuilder_vertex(b, p3, n, mortar);
+                MeshBuilder_quad(b, i0, i1, i2, i3);
+            }
+        }
+    }
+}
+
+static void addBuilding(MeshBuilder *b, float length, float width, float wall_height,
+                        float roof_height, BuildingLook look) {
+    const Vec3 wall = wallColor(look);
+    const Vec3 brick[FACE_COUNT] = { wall, wall, wall, wall, wall, wall };
+    shapes_addBox(b, vec3(0.0f, wall_height * 0.5f, 0.0f), vec3(length, wall_height, width), brick);
+    if (look == BUILDING_BRICK) addBrickCourses(b, length, width, wall_height, wall);
+    addRoof(b, length, width, wall_height, roof_height, look);
+}
+
 static const Vec3 COLOR_FRAME_LIGHT = { 0.93f, 0.91f, 0.86f };
 static const Vec3 COLOR_FRAME_DARK  = { 0.40f, 0.29f, 0.21f };
 
@@ -508,3 +555,52 @@ Mesh Shapes_building(BuildingSize size, bool with_details) {
     }
     return Mesh_fromBuilder(&b);
 }
+
+Vec3 Shapes_wallColor(BuildingLook look) { return wallColor(look); }
+
+Mesh Shapes_structure(const ShapeSlab *slabs, int n_slabs, const ShapeOpening *openings, int n_openings,
+                      const ShapeRoof *roofs, int n_roofs) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    for (int i = 0; i < n_slabs; i++) {
+        const ShapeSlab *s = &slabs[i];
+        Vec3 size = vec3_sub(s->max, s->min);
+        if (size.x <= 0.0f || size.y <= 0.0f || size.z <= 0.0f) continue;
+        addBlock(&b, vec3_scale(vec3_add(s->min, s->max), 0.5f), size, s->color);
+    }
+    for (int i = 0; i < n_openings; i++) {
+        const ShapeOpening *o = &openings[i];
+        size_t first = b.vertex_count;
+        /* made facing +Z at the origin (on the wall face), then turned and moved there */
+        switch (o->kind) {
+        case OPENING_WINDOW:
+            addWindow(&b, 0.0f, 0.0f, o->w, o->h, 0.0f, 1.0f, o->frame);
+            break;
+        case OPENING_PANE: /* seen from inside: the glass and a frame, no sill */
+            addBlock(&b, vec3(0.0f, 0.0f, 0.005f), vec3(o->w, o->h, 0.01f), COLOR_WINDOW);
+            addBlock(&b, vec3(0.0f, o->h * 0.5f, 0.02f), vec3(o->w + 0.12f, 0.06f, 0.04f), o->frame);
+            addBlock(&b, vec3(0.0f, -o->h * 0.5f, 0.02f), vec3(o->w + 0.12f, 0.06f, 0.04f), o->frame);
+            addBlock(&b, vec3(-o->w * 0.5f, 0.0f, 0.02f), vec3(0.06f, o->h, 0.04f), o->frame);
+            addBlock(&b, vec3(o->w * 0.5f, 0.0f, 0.02f), vec3(0.06f, o->h, 0.04f), o->frame);
+            break;
+        case OPENING_DOOR:
+            addDoor(&b, 0.0f, o->w, o->h, 0.0f, 1.0f, o->frame);
+            break;
+        case OPENING_FRAME: { /* an open doorway: just the frame round it */
+            const float fw = 0.09f;
+            addBlock(&b, vec3(0.0f, o->h + fw * 0.5f, 0.02f), vec3(o->w + fw * 2.0f, fw, 0.06f), o->frame);
+            addBlock(&b, vec3(-o->w * 0.5f - fw * 0.5f, o->h * 0.5f, 0.02f), vec3(fw, o->h, 0.06f), o->frame);
+            addBlock(&b, vec3(o->w * 0.5f + fw * 0.5f, o->h * 0.5f, 0.02f), vec3(fw, o->h, 0.06f), o->frame);
+            break;
+        }
+        }
+        transformSince(&b, first, mat4_mul(mat4_translate(o->at), mat4_rotate_y(o->yaw)));
+    }
+    for (int i = 0; i < n_roofs; i++) {
+        const ShapeRoof *r = &roofs[i];
+        size_t first = b.vertex_count;
+        addRoof(&b, r->length, r->width, 0.0f, r->roof_height, r->look);
+        transformSince(&b, first, mat4_translate(vec3(r->center.x, r->wall_top, r->center.z)));
+    }
+    return Mesh_fromBuilder(&b);
+}
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index db0f8c1..1a85d2a 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -81,8 +81,13 @@ Mesh  Shapes_bagCarton(Vec3 size);
 /* A sausage along Y, centered; brown and striped once grilled. */
 Mesh  Shapes_sausage(float length, bool grilled);
 
-/* Fireplace: a ring of stones round a bed of ashes, base at y = 0. */
+/* Fireplace: a ring of stones round a bed of ashes, base at y = 0, and the
+   wooden tripod over it (the grill hangs from it). */
 Mesh  Shapes_fireplace(float diameter);
+/* Where the tripod's pole k (0..2) stands, from the fireplace's middle. */
+Vec3  Shapes_tripodFoot(float diameter, int k);
+#define TRIPOD_OUT  0.9f   /* its poles stand this far outside the stones (they don't catch fire) ... */
+#define TRIPOD_PAST 0.4f   /* ... and reach this far past the top */
 /* Flames over the middle, base at y = 0. Their colors glow in the dark. */
 Mesh  Shapes_flames(void);
 
@@ -105,7 +110,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_COUNT
+    FURN_CHAIR, FURN_BENCH, FURN_CAR, FURN_TIRES, FURN_TOILET, FURN_TAP, FURN_ASHTRAY, FURN_COATRACK, FURN_GATE, FURN_COUNT
 } FurnitureKind;
 #define SEAT_HEIGHT 0.45f  /* chairs and log benches */
 
@@ -114,6 +119,8 @@ typedef enum {
 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);
+/* 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);
 
@@ -229,4 +236,24 @@ Mesh  Shapes_shards(float diameter, unsigned seed);
 /* Walls with a gable roof; base at y = 0. Details adds windows and doors. */
 Mesh  Shapes_building(BuildingSize size, bool with_details);
 
+/* A building made of parts (see world/manor.c): boxes, windows and doors on
+   the walls, gable roofs. */
+typedef struct { Vec3 min, max; Vec3 color; } ShapeSlab;
+typedef enum { OPENING_WINDOW, OPENING_PANE, OPENING_DOOR, OPENING_FRAME } OpeningKind;
+typedef struct {
+    OpeningKind kind;
+    Vec3  at;     /* on the wall face: a window's middle, a door's foot */
+    float yaw;    /* the way it faces (0: +Z, about Y) */
+    float w, h;
+    Vec3  frame;
+} ShapeOpening;
+typedef struct {
+    Vec3  center;     /* x, z of the middle */
+    float length, width, wall_top, roof_height; /* ridge along X */
+    BuildingLook look;
+} ShapeRoof;
+Mesh  Shapes_structure(const ShapeSlab *slabs, int n_slabs, const ShapeOpening *openings, int n_openings,
+                       const ShapeRoof *roofs, int n_roofs);
+Vec3  Shapes_wallColor(BuildingLook look);
+
 #endif /* RENDER_SHAPES_H */
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 513e99c..c661da7 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -411,18 +411,27 @@ static void addRod(MeshBuilder *b, Vec3 a, Vec3 to, float r0, float r1, int segm
 
 static const Vec3 COLOR_IRON = { 0.22f, 0.21f, 0.20f };
 
+Vec3 Shapes_tripodFoot(float diameter, int k) {
+    float a = 2.0f * PI_F * ((float)k + 0.25f) / 3.0f, out = diameter * 0.5f + TRIPOD_OUT;
+    return vec3(cosf(a) * out, 0.0f, sinf(a) * out);
+}
+
 Mesh Shapes_fireplace(float diameter) {
     const float r = diameter * 0.5f;
     const int stones = 11;
     unsigned seed = 91u;
     MeshBuilder b;
     MeshBuilder_init(&b);
-    /* Three long iron rods meeting over the fire */
+    /* Three long wooden poles meeting over the fire, their feet well away from
+       it (they don't burn so easily), crossing at the top and sticking out past
+       the lashing (see Shapes_tripodFoot for where they stand) */
+    const Vec3 pole = { 0.50f, 0.36f, 0.22f };
     for (int k = 0; k < 3; k++) {
-        float a = 2.0f * PI_F * ((float)k + 0.25f) / 3.0f;
-        Vec3 foot = vec3(cosf(a) * (r + 0.25f), 0.0f, sinf(a) * (r + 0.25f));
-        addRod(&b, foot, vec3(cosf(a) * 0.04f, GRILL_TOP + 0.05f, sinf(a) * 0.04f), 0.022f, 0.016f, 6, COLOR_IRON);
+        Vec3 foot = Shapes_tripodFoot(diameter, k), top = vec3(foot.x * 0.03f, GRILL_TOP + 0.05f, foot.z * 0.03f);
+        Vec3 past = vec3_add(top, vec3_scale(vec3_normalize(vec3_sub(top, foot)), TRIPOD_PAST));
+        addRod(&b, foot, past, 0.045f, 0.035f, 7, pole);
     }
+    addRod(&b, vec3(0.0f, GRILL_TOP - 0.02f, 0.0f), vec3(0.0f, GRILL_TOP + 0.14f, 0.0f), 0.075f, 0.075f, 8, (Vec3){ 0.55f, 0.48f, 0.34f }); /* the lashing */
     shapes_addTube(&b, 0.0f, r * 0.8f, r * 0.75f, 0.04f, 14, COLOR_ASH, true);
     for (int i = 0; i < stones; i++) {
         float a = 2.0f * PI_F * ((float)i + 0.3f * scatter(&seed)) / (float)stones;
@@ -839,12 +848,57 @@ Mesh Shapes_furniture(FurnitureKind kind, Vec3 size) {
     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);
+        for (int k = 0; k < boards; k++) {
+            float x = -hx + ((float)k + 0.5f) * size.x / (float)boards;
+            addBlock(&b, vec3(x, size.y * 0.5f, 0.0f), vec3(size.x / (float)boards - 0.012f, size.y, size.z), k % 2 ? green : vec3_scale(green, 0.94f));
+        }
+        for (int r = 0; r < 2; r++)
+            addBlock(&b, vec3(0.0f, size.y * (0.2f + 0.6f * (float)r), hz + 0.02f), vec3(size.x - 0.06f, 0.14f, 0.04f), dark);
+        float rise = size.y * 0.6f, run = size.x - 0.12f, len = sqrtf(rise * rise + run * run);
+        size_t first = b.vertex_count;
+        addBlock(&b, vec3_zero(), vec3(len, 0.12f, 0.04f), dark);
+        transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, size.y * 0.5f, hz + 0.02f)), mat4_rotate_z(atan2f(rise, run))));
+        addBlock(&b, vec3(hx - 0.12f, size.y * 0.5f, hz + 0.05f), vec3(0.05f, 0.18f, 0.05f), COLOR_IRON); /* handle */
+        break;
+    }
     case FURN_COUNT:
         break;
     }
     return Mesh_fromBuilder(&b);
 }
 
+/* Ivy climbing a wall (the wall at z = 0, the plant in front of it, facing
+   +Z): a few stems wandering up from the ground, leaves all along them,
+   thicker low down, spreading out under the eaves. */
+Mesh Shapes_ivy(float width, float height, unsigned seed) {
+    static const Vec3 LEAF[] = { { 0.22f, 0.42f, 0.20f }, { 0.28f, 0.50f, 0.24f }, { 0.18f, 0.36f, 0.18f }, { 0.32f, 0.52f, 0.22f } };
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    if (seed == 0) seed = 1;
+    const int stems = 5 + (int)(scatter(&seed) * 4.0f);
+    for (int s = 0; s < stems; s++) {
+        float x = (scatter(&seed) - 0.5f) * width * 0.6f, top = height * (0.65f + 0.35f * scatter(&seed));
+        for (float y = 0.05f; y < top; y += 0.11f) {
+            x += (scatter(&seed) - 0.5f) * 0.18f;
+            x = clampf(x, -width * 0.5f, width * 0.5f);
+            float spread = 0.4f + 0.6f * smoothstepf(top * 0.55f, top, y) + 0.3f * (1.0f - smoothstepf(0.0f, 1.2f, y));
+            int leaves = 3 + (int)(scatter(&seed) * 3.0f);
+            for (int k = 0; k < leaves; k++) {
+                float lx = clampf(x + (scatter(&seed) - 0.5f) * spread * 2.0f, -width * 0.5f, width * 0.5f);
+                float ly = y + (scatter(&seed) - 0.5f) * 0.1f, d = 0.02f + 0.05f * scatter(&seed);
+                float sz = 0.12f + 0.09f * scatter(&seed);
+                addBlock(&b, vec3(lx, ly, d), vec3(sz, sz * 0.8f, 0.025f), LEAF[(unsigned)(scatter(&seed) * 4.0f) % 4u]);
+            }
+            if ((int)(y / 0.11f) % 3 == 0) /* the stem */
+                addBlock(&b, vec3(x, y, 0.015f), vec3(0.025f, 0.13f, 0.02f), (Vec3){ 0.36f, 0.28f, 0.18f });
+        }
+    }
+    return Mesh_fromBuilder(&b);
+}
+
 Mesh Shapes_hoseNozzle(void) {
     const Vec3 yellow = { 0.90f, 0.72f, 0.18f }, green = { 0.20f, 0.52f, 0.24f };
     MeshBuilder b;
diff --git a/entropie/src/world/ladder.c b/entropie/src/world/ladder.c
index 1d52e0b..3cb7dea 100644
--- a/entropie/src/world/ladder.c
+++ b/entropie/src/world/ladder.c
@@ -501,7 +501,7 @@ int Ladder_lean(World *world, Object *who, Object *l, bool fit, const char **why
     if (top < -1e8f) top = face.y;
     Vec3 out = vec3_add(face, vec3_scale(n, 1.0f));
     float ground = World_groundHeight(world, out.x, out.z, who->position.y + 0.8f);
-    bool eaves = thing->type == OBJ_HOUSE || thing->type == OBJ_BARN;
+    bool eaves = thing->type == OBJ_HOUSE || thing->type == OBJ_BARN || (thing->type == OBJ_WALL && thing->variant == 2);
     float h = top - (eaves ? 0.12f : 0.0f) - ground;
     if (h < MIN_HEIGHT) { *why = "Too low to need a ladder"; return LEAN_NO; }
 
diff --git a/entropie/src/world/manor.c b/entropie/src/world/manor.c
new file mode 100644
index 0000000..fa9ebde
--- /dev/null
+++ b/entropie/src/world/manor.c
@@ -0,0 +1,363 @@
+#include "world/manor.h"
+
+#include <stdio.h>
+#include <string.h>
+
+/* Heights (m from the ground) */
+#define FL        0.70f  /* the ground floor: four steps up */
+#define STEP_H    (FL / 4.0f)
+#define CEIL      3.40f  /* its ceiling ... */
+#define SLAB      0.20f  /* ... this thick */
+#define WALL_TOP  6.20f  /* the eaves (two storeys over the basement) */
+#define PORCH_TOP 3.00f
+#define DOOR_H    2.15f
+
+/* The halves */
+#define LEFT_Z0  -5.0f
+#define LEFT_Z1   5.0f
+#define RIGHT_Z0 -4.2f   /* (the right half stands a little further out) */
+#define RIGHT_Z1  5.8f
+#define HALF_LEN 18.0f
+#define T         0.30f  /* outer walls */
+#define P         0.15f  /* inner walls */
+#define PORCH_Z1  7.3f   /* the porch: 1.5 m in front of the right half */
+#define PORCH_X0  1.0f
+
+#define ROOF_PITCH (5.0f / 12.0f)
+
+static const Vec3 PLASTER   = { 0.88f, 0.82f, 0.68f };
+static const Vec3 PLINTH    = { 0.62f, 0.60f, 0.56f };
+static const Vec3 INSIDE    = { 0.93f, 0.91f, 0.85f };
+static const Vec3 CEILING   = { 0.95f, 0.94f, 0.90f };
+static const Vec3 FLOORING  = { 0.62f, 0.45f, 0.30f };
+static const Vec3 TILES     = { 0.70f, 0.66f, 0.60f };
+static const Vec3 STEPS     = { 0.58f, 0.56f, 0.53f };
+static const Vec3 RAIL      = { 0.45f, 0.32f, 0.22f };
+static const Vec3 FRAME     = { 0.93f, 0.91f, 0.86f };
+static const Vec3 PORCELAIN = { 0.95f, 0.95f, 0.93f };
+static const Vec3 PORCH_ROOF = { 0.38f, 0.38f, 0.43f };
+
+enum { LOOK_ONLY, SOLID, EAVES_WALL };
+
+#define MAX_SLABS    256
+#define MAX_OPENINGS 96
+
+typedef struct {
+    ShapeSlab    slabs[MAX_SLABS];
+    int          n_slabs;
+    ShapeOpening openings[MAX_OPENINGS];
+    int          n_openings;
+    ShapeRoof    roofs[2];
+    int          n_roofs;
+    World       *world;
+    Vec3         origin;
+    float        yaw;
+    bool         ok;
+} Plan;
+
+static Vec3 toWorld(const Plan *p, Vec3 local) {
+    return vec3_add(p->origin, mat4_mul_dir(mat4_rotate_yxz(vec3(0.0f, p->yaw, 0.0f)), local));
+}
+
+/* A solid box of the building (it's walked on, leaned on, run into). */
+static Object *solid(Plan *p, Vec3 min, Vec3 max, int kind, const char *name) {
+    Vec3 size = vec3_sub(max, min);
+    Vec3 foot = vec3((min.x + max.x) * 0.5f, min.y, (min.z + max.z) * 0.5f);
+    Object *o = Object_create(OBJ_WALL, PALETTE_GROUND, toWorld(p, foot), size, 1000.0f);
+    if (!o) { p->ok = false; return NULL; }
+    Object_setRotation(o, vec3(0.0f, p->yaw, 0.0f));
+    o->variant = kind == EAVES_WALL ? 2 : 0;
+    if (name) snprintf(o->name, sizeof(o->name), "%s", name);
+    if (!World_add(p->world, o)) { p->ok = false; return NULL; }
+    return o;
+}
+
+/* A box: seen (colour), and solid unless LOOK_ONLY. */
+static void box(Plan *p, Vec3 min, Vec3 max, Vec3 color, int kind) {
+    if (max.x - min.x <= 1e-3f || max.y - min.y <= 1e-3f || max.z - min.z <= 1e-3f) return;
+    if (p->n_slabs < MAX_SLABS) p->slabs[p->n_slabs++] = (ShapeSlab){ min, max, color };
+    else p->ok = false;
+    if (kind != LOOK_ONLY) solid(p, min, max, kind, NULL);
+}
+
+static void opening(Plan *p, OpeningKind kind, Vec3 at, float yaw, float w, float h) {
+    if (p->n_openings < MAX_OPENINGS) p->openings[p->n_openings++] = (ShapeOpening){ kind, at, yaw, w, h, FRAME };
+    else p->ok = false;
+}
+
+/* A gap in a wall: from a to b along it, from y0 to y1 */
+typedef struct { float a, b, y0, y1; } Gap;
+
+/* A wall along x (from x0 to x1, between z0 and z1, y0 to y1) with gaps. */
+static void wallX(Plan *p, float x0, float x1, float z0, float z1, float y0, float y1,
+                  const Gap *gaps, int n, Vec3 color, int kind) {
+    float at = x0;
+    for (int i = 0; i < n; i++) { /* (gaps given left to right) */
+        box(p, vec3(at, y0, z0), vec3(gaps[i].a, y1, z1), color, kind);
+        box(p, vec3(gaps[i].a, y0, z0), vec3(gaps[i].b, gaps[i].y0, z1), color, kind); /* under it */
+        box(p, vec3(gaps[i].a, gaps[i].y1, z0), vec3(gaps[i].b, y1, z1), color, kind); /* over it */
+        at = gaps[i].b;
+    }
+    box(p, vec3(at, y0, z0), vec3(x1, y1, z1), color, kind);
+}
+
+/* The same along z. */
+static void wallZ(Plan *p, float z0, float z1, float x0, float x1, float y0, float y1,
+                  const Gap *gaps, int n, Vec3 color, int kind) {
+    float at = z0;
+    for (int i = 0; i < n; i++) {
+        box(p, vec3(x0, y0, at), vec3(x1, y1, gaps[i].a), color, kind);
+        box(p, vec3(x0, y0, gaps[i].a), vec3(x1, gaps[i].y0, gaps[i].b), color, kind);
+        box(p, vec3(x0, gaps[i].y1, gaps[i].a), vec3(x1, y1, gaps[i].b), color, kind);
+        at = gaps[i].b;
+    }
+    box(p, vec3(x0, y0, at), vec3(x1, y1, z1), color, kind);
+}
+
+/* A locked door drawn on a wall face, and a thin solid piece in front of it
+   that says what it is (clicked: "locked"). yaw: the way it faces. */
+static void lockedDoor(Plan *p, Vec3 foot, float yaw, float w, const char *name) {
+    opening(p, OPENING_DOOR, foot, yaw, w, DOOR_H - 0.1f);
+    Vec3 out = vec3(sinf(yaw), 0.0f, cosf(yaw)), along = vec3(cosf(yaw), 0.0f, -sinf(yaw));
+    Vec3 a = vec3_add(foot, vec3_add(vec3_scale(along, -w * 0.5f), vec3_scale(out, 0.0f)));
+    Vec3 b = vec3_add(foot, vec3_add(vec3_scale(along, w * 0.5f), vec3_scale(out, 0.06f)));
+    Vec3 min = vec3(fminf(a.x, b.x), foot.y, fminf(a.z, b.z)), max = vec3(fmaxf(a.x, b.x), foot.y + DOOR_H - 0.1f, fmaxf(a.z, b.z));
+    Object *o = solid(p, min, max, SOLID, name);
+    if (o) o->variant = 1;
+}
+
+/* Windows along a wall face at z (facing +z if side > 0), at these x, at height y. */
+static void windowsX(Plan *p, float z, float side, const float *xs, int n, float y, float w, float h, bool inside_too, float inner_z) {
+    for (int i = 0; i < n; i++) {
+        opening(p, OPENING_WINDOW, vec3(xs[i], y, z), side > 0.0f ? 0.0f : PI_F, w, h);
+        if (inside_too) opening(p, OPENING_PANE, vec3(xs[i], y, inner_z), side > 0.0f ? PI_F : 0.0f, w, h);
+    }
+}
+
+/* ---------- The left half: entrance, corridor, toilet, kitchen, locked rooms ---------- */
+
+#define DOOR_X   -7.6f   /* the front door (left half), by your garages */
+#define VEST_X0  -9.6f   /* the entrance: ground level */
+#define VEST_X1  -5.75f
+#define STAIR_X1 -8.4f   /* the four steps up, on the left */
+#define HALL_Z    1.4f   /* the ground floor starts here (towards the back) */
+#define CORR_Z0  -0.75f  /* the corridor */
+#define CORR_Z1   0.75f
+#define TOILET_X0 -5.6f
+#define KITCHEN_X0 -3.6f
+#define LIVING_X0 -5.0f
+#define GARAGE_X1 -9.75f /* your garages stand in front up to here */
+
+static void leftHalf(Plan *p) {
+    const float x0 = -HALF_LEN, z0 = LEFT_Z0, z1 = LEFT_Z1;
+    /* Outer walls: the front with the door; the back; the left end; the wall between the halves */
+    Gap door = { DOOR_X - 0.5f, DOOR_X + 0.5f, 0.0f, DOOR_H };
+    wallX(p, x0, -P, z1 - T, z1, 0.0f, WALL_TOP, &door, 1, PLASTER, EAVES_WALL);
+    wallX(p, x0, -P, z0, z0 + T, 0.0f, WALL_TOP, NULL, 0, PLASTER, EAVES_WALL);
+    wallZ(p, z0 + T, z1 - T, x0, x0 + T, 0.0f, WALL_TOP, NULL, 0, PLASTER, EAVES_WALL);
+    Gap through = { CORR_Z0, CORR_Z1, FL, FL + DOOR_H };
+    wallZ(p, z0, RIGHT_Z1, -P, P, 0.0f, WALL_TOP, &through, 1, PLASTER, SOLID);
+    /* Plinth paint round the outside (the basement), and the front door */
+    box(p, vec3(GARAGE_X1, 0.0f, z1), vec3(DOOR_X - 0.6f, FL - 0.05f, z1 + 0.02f), PLINTH, LOOK_ONLY);
+    box(p, vec3(DOOR_X + 0.6f, 0.0f, z1), vec3(-P, FL - 0.05f, z1 + 0.02f), PLINTH, LOOK_ONLY);
+    box(p, vec3(x0, 0.0f, z0 - 0.02f), vec3(-P, FL - 0.05f, z0), PLINTH, LOOK_ONLY);
+    opening(p, OPENING_FRAME, vec3(DOOR_X, 0.0f, z1), 0.0f, 1.0f, DOOR_H);
+    opening(p, OPENING_FRAME, vec3(DOOR_X, 0.0f, z1 - T), PI_F, 1.0f, DOOR_H);
+
+    /* Floors: the entrance on the ground, everything else four steps up */
+    box(p, vec3(VEST_X0, -0.06f, HALL_Z), vec3(VEST_X1, 0.02f, z1 - T), TILES, SOLID);
+    box(p, vec3(x0 + T, 0.0f, z0 + T), vec3(-P, FL, HALL_Z), FLOORING, SOLID);         /* back: corridor and rooms */
+    box(p, vec3(TOILET_X0, 0.0f, HALL_Z), vec3(-P, FL, z1 - T), FLOORING, SOLID);       /* toilet, kitchen */
+    box(p, vec3(x0 + T, 0.0f, HALL_Z), vec3(GARAGE_X1, FL, z1 - T), FLOORING, SOLID);   /* (a closed store room) */
+    for (int k = 0; k < 4; k++) /* the steps up, on the left */
+        box(p, vec3(VEST_X0, 0.0f, HALL_Z + 1.2f - 0.3f * (float)(k + 1)), vec3(STAIR_X1, STEP_H * (float)(k + 1), HALL_Z + 1.2f - 0.3f * (float)k),
+            STEPS, SOLID);
+    box(p, vec3(STAIR_X1, FL, HALL_Z), vec3(VEST_X1, FL + 0.95f, HALL_Z + 0.08f), RAIL, SOLID); /* the rail along the edge */
+    box(p, vec3(STAIR_X1, 0.0f, HALL_Z), vec3(VEST_X1, FL, HALL_Z + 0.02f), PLINTH, LOOK_ONLY);
+    box(p, vec3(x0 + T, CEIL, z0 + T), vec3(-P, CEIL + SLAB, z1 - T), CEILING, SOLID);
+
+    /* The entrance: its walls; on the right the basement door (locked) */
+    wallZ(p, CORR_Z0, z1 - T, GARAGE_X1, VEST_X0, 0.0f, CEIL, NULL, 0, INSIDE, SOLID);
+    wallZ(p, HALL_Z, z1 - T, VEST_X1, TOILET_X0, 0.0f, CEIL, NULL, 0, INSIDE, SOLID);
+    lockedDoor(p, vec3(VEST_X1, 0.0f, 3.4f), -PI_F * 0.5f, 0.9f, "Basement");
+
+    /* The corridor's walls: on the right toilet and kitchen (open), on the left two locked rooms */
+    Gap rooms[2] = { { -5.0f, -4.2f, FL, FL + DOOR_H }, { -2.5f, -1.5f, FL, FL + DOOR_H } };
+    wallX(p, TOILET_X0, -P, CORR_Z1, CORR_Z1 + P, FL, CEIL, rooms, 2, INSIDE, SOLID);
+    opening(p, OPENING_FRAME, vec3(-4.6f, FL, CORR_Z1), PI_F, 0.8f, DOOR_H);
+    opening(p, OPENING_FRAME, vec3(-2.0f, FL, CORR_Z1), PI_F, 1.0f, DOOR_H);
+    wallZ(p, CORR_Z1 + P, z1 - T, KITCHEN_X0 - P, KITCHEN_X0, FL, CEIL, NULL, 0, INSIDE, SOLID); /* toilet | kitchen */
+    wallX(p, x0 + T, GARAGE_X1, CORR_Z1, CORR_Z1 + P, FL, CEIL, NULL, 0, INSIDE, SOLID);          /* the store room */
+    wallX(p, GARAGE_X1, -P, CORR_Z0 - P, CORR_Z0, FL, CEIL, NULL, 0, INSIDE, SOLID);
+    wallZ(p, z0 + T, CORR_Z0 - P, LIVING_X0 - P, LIVING_X0, FL, CEIL, NULL, 0, INSIDE, SOLID);
+    lockedDoor(p, vec3(-7.2f, FL, CORR_Z0), 0.0f, 0.9f, "A locked room");
+    lockedDoor(p, vec3(-2.6f, FL, CORR_Z0), 0.0f, 0.9f, "Living room");
+
+    /* The toilet: a bowl, a cistern, a little basin */
+    box(p, vec3(-5.25f, FL, 3.6f), vec3(-4.85f, FL + 0.42f, 4.25f), PORCELAIN, SOLID);
+    box(p, vec3(-5.3f, FL + 0.42f, 4.25f), vec3(-4.8f, FL + 0.9f, 4.45f), PORCELAIN, LOOK_ONLY);
+    box(p, vec3(-4.2f, FL + 0.8f, 4.15f), vec3(-3.75f, FL + 0.95f, 4.6f), PORCELAIN, LOOK_ONLY);
+
+    /* Windows: the front (behind your garages there are none), the back; basement windows low down */
+    const float front_low[] = { -4.6f, -1.9f }, front_up[] = { -8.6f, -5.5f, -2.2f };
+    windowsX(p, z1, 1.0f, &front_low[1], 1, FL + 1.45f, 1.0f, 1.2f, true, z1 - T);
+    opening(p, OPENING_WINDOW, vec3(-4.6f, FL + 1.8f, z1), 0.0f, 0.6f, 0.6f);  /* the toilet's */
+    opening(p, OPENING_PANE, vec3(-4.6f, FL + 1.8f, z1 - T), PI_F, 0.6f, 0.6f);
+    windowsX(p, z1, 1.0f, front_up, 3, CEIL + 1.5f, 1.0f, 1.2f, false, 0.0f);
+    windowsX(p, z1, 1.0f, front_low, 2, 0.35f, 0.8f, 0.36f, false, 0.0f);
+    float back[5];
+    for (int i = 0; i < 5; i++) back[i] = x0 + 1.8f + 3.6f * (float)i;
+    windowsX(p, z0, -1.0f, back, 5, FL + 1.45f, 1.0f, 1.2f, false, 0.0f);
+    windowsX(p, z0, -1.0f, back, 5, CEIL + 1.5f, 1.0f, 1.2f, false, 0.0f);
+    windowsX(p, z0, -1.0f, back, 5, 0.35f, 0.8f, 0.36f, false, 0.0f);
+
+    p->roofs[p->n_roofs++] = (ShapeRoof){ vec3(-HALF_LEN * 0.5f, 0.0f, (z0 + z1) * 0.5f), HALF_LEN, z1 - z0, WALL_TOP,
+                                          (z1 - z0) * ROOF_PITCH, BUILDING_PLASTER };
+}
+
+/* ---------- The right half: the big room; the porch in front ---------- */
+
+#define RDOOR_X   2.3f   /* the door from the porch */
+#define PSTEP_X   5.0f   /* the porch steps: up from here, going left */
+
+static void rightHalf(Plan *p) {
+    const float x1 = HALF_LEN, z0 = RIGHT_Z0, z1 = RIGHT_Z1;
+    Gap door = { RDOOR_X - 0.5f, RDOOR_X + 0.5f, FL, FL + DOOR_H };
+    wallX(p, P, x1, z1 - T, z1, 0.0f, WALL_TOP, &door, 1, PLASTER, EAVES_WALL);
+    wallX(p, P, x1, z0, z0 + T, 0.0f, WALL_TOP, NULL, 0, PLASTER, EAVES_WALL);
+    wallZ(p, z0 + T, z1 - T, x1 - T, x1, 0.0f, WALL_TOP, NULL, 0, PLASTER, EAVES_WALL);
+    box(p, vec3(P, 0.0f, z0 - 0.02f), vec3(x1, FL - 0.05f, z0), PLINTH, LOOK_ONLY);
+    opening(p, OPENING_FRAME, vec3(RDOOR_X, FL, z1), 0.0f, 1.0f, DOOR_H);
+    opening(p, OPENING_FRAME, vec3(RDOOR_X, FL, z1 - T), PI_F, 1.0f, DOOR_H);
+
+    box(p, vec3(P, 0.0f, z0 + T), vec3(x1 - T, FL, z1 - T), FLOORING, SOLID);
+    box(p, vec3(P, CEIL, z0 + T), vec3(x1 - T, CEIL + SLAB, z1 - T), CEILING, SOLID);
+    opening(p, OPENING_FRAME, vec3(P, FL, 0.0f), PI_F * 0.5f, CORR_Z1 - CORR_Z0, DOOR_H); /* from the corridor */
+    opening(p, OPENING_FRAME, vec3(-P, FL, 0.0f), -PI_F * 0.5f, CORR_Z1 - CORR_Z0, DOOR_H);
+
+    /* Windows: the front (into the porch), the back, basement windows at the back */
+    const float front[] = { 5.5f, 9.0f, 12.5f, 16.0f }, up[] = { 2.3f, 5.5f, 9.0f, 12.5f, 16.0f };
+    windowsX(p, z1, 1.0f, front, 4, FL + 1.45f, 1.0f, 1.2f, true, z1 - T);
+    windowsX(p, z1, 1.0f, up, 5, CEIL + 1.5f, 1.0f, 1.2f, false, 0.0f);
+    float back[5];
+    for (int i = 0; i < 5; i++) back[i] = 1.8f + 3.6f * (float)i;
+    windowsX(p, z0, -1.0f, back, 5, FL + 1.45f, 1.0f, 1.2f, true, z0 + T);
+    windowsX(p, z0, -1.0f, back, 5, CEIL + 1.5f, 1.0f, 1.2f, false, 0.0f);
+    windowsX(p, z0, -1.0f, back, 5, 0.35f, 0.8f, 0.36f, false, 0.0f);
+
+    /* The porch: its front wall with windows, a closed left end, the way in at the right end */
+    const float pz0 = z1, pz1 = PORCH_Z1;
+    wallX(p, PORCH_X0, x1, pz1 - T, pz1, 0.0f, PORCH_TOP, NULL, 0, PLASTER, SOLID);
+    wallZ(p, pz0, pz1 - T, PORCH_X0, PORCH_X0 + T, 0.0f, PORCH_TOP, NULL, 0, PLASTER, SOLID);
+    Gap in = { pz0 + 0.15f, pz1 - T - 0.1f, 0.0f, DOOR_H };
+    wallZ(p, pz0, pz1 - T, x1 - T, x1, 0.0f, PORCH_TOP, &in, 1, PLASTER, SOLID);
+    opening(p, OPENING_FRAME, vec3(x1, 0.0f, (in.a + in.b) * 0.5f), PI_F * 0.5f, in.b - in.a, DOOR_H);
+    box(p, vec3(PORCH_X0 - 0.1f, PORCH_TOP, pz0), vec3(x1 + 0.1f, PORCH_TOP + 0.18f, pz1 + 0.2f), PORCH_ROOF, SOLID);
+    box(p, vec3(PORCH_X0, 0.0f, pz1), vec3(x1, 0.5f, pz1 + 0.02f), PLINTH, LOOK_ONLY);
+    const float pw[] = { 4.5f, 8.0f, 11.5f, 15.0f };
+    windowsX(p, pz1, 1.0f, pw, 4, 1.65f, 1.0f, 1.0f, true, pz1 - T);
+    /* its floor: on the ground at the way in, four steps up to the landing at the door */
+    box(p, vec3(PSTEP_X, -0.06f, pz0), vec3(x1 - T, 0.02f, pz1 - T), TILES, SOLID);
+    for (int k = 0; k < 4; k++)
+        box(p, vec3(PSTEP_X - 0.3f * (float)(k + 1), 0.0f, pz0), vec3(PSTEP_X - 0.3f * (float)k, STEP_H * (float)(k + 1), pz1 - T),
+            STEPS, SOLID);
+    box(p, vec3(PORCH_X0 + T, 0.0f, pz0), vec3(PSTEP_X - 1.2f, FL, pz1 - T), TILES, SOLID);
+    box(p, vec3(PORCH_X0 + T, PORCH_TOP - 0.05f, pz0), vec3(x1 - T, PORCH_TOP, pz1 - T), CEILING, LOOK_ONLY);
+
+    p->roofs[p->n_roofs++] = (ShapeRoof){ vec3(HALF_LEN * 0.5f, 0.0f, (z0 + z1) * 0.5f), HALF_LEN, z1 - z0, WALL_TOP,
+                                          (z1 - z0) * ROOF_PITCH, BUILDING_PLASTER_DARK };
+}
+
+/* The roofs: two slabs under each (you can get onto them from a ladder). */
+static void roofSlabs(Plan *p, const ShapeRoof *r) {
+    const float half_w = r->width * 0.5f, angle = atanf(r->roof_height / half_w);
+    const float slope = sqrtf(half_w * half_w + r->roof_height * r->roof_height), thick = 0.25f;
+    for (int side = -1; side <= 1; side += 2) {
+        Vec3 normal = vec3(0.0f, cosf(angle), (float)side * sinf(angle));
+        Vec3 surface = vec3(r->center.x, r->wall_top + r->roof_height * 0.5f, r->center.z + (float)side * half_w * 0.5f);
+        Vec3 center = toWorld(p, vec3_sub(surface, vec3_scale(normal, thick * 0.5f)));
+        Vec3 size = vec3(r->length, thick, slope);
+        Object *o = Object_create(OBJ_WALL, PALETTE_GROUND, vec3(center.x, center.y - thick * 0.5f, center.z), size, 1000.0f);
+        if (!o) { p->ok = false; return; }
+        Object_setRotation(o, vec3((float)-side * angle, p->yaw, 0.0f));
+        o->variant = 3; /* (a roof) */
+        if (!World_add(p->world, o)) { p->ok = false; return; }
+    }
+}
+
+/* What stands inside: the meeting table in the big room, the kitchen. */
+static void furnish(Plan *p) {
+    const float fl = FL;
+    struct { FurnitureKind kind; Vec3 at; Vec3 size; float turn; } items[] = {
+        { FURN_TABLE,   { 9.5f, fl, 0.8f }, { 4.2f, 0.78f, 1.6f }, 0.0f },             /* the big table */
+        { FURN_CHAIR,   { 8.0f, fl, 2.15f }, { 0.45f, 0.9f, 0.45f }, PI_F * 0.5f },
+        { FURN_CHAIR,   { 9.5f, fl, 2.15f }, { 0.45f, 0.9f, 0.45f }, PI_F * 0.5f },
+        { FURN_CHAIR,   { 11.0f, fl, 2.15f }, { 0.45f, 0.9f, 0.45f }, PI_F * 0.5f },
+        { FURN_CHAIR,   { 8.0f, fl, -0.55f }, { 0.45f, 0.9f, 0.45f }, -PI_F * 0.5f },
+        { FURN_CHAIR,   { 9.5f, fl, -0.55f }, { 0.45f, 0.9f, 0.45f }, -PI_F * 0.5f },
+        { FURN_CHAIR,   { 11.0f, fl, -0.55f }, { 0.45f, 0.9f, 0.45f }, -PI_F * 0.5f },
+        { FURN_CHAIR,   { 6.85f, fl, 0.8f }, { 0.45f, 0.9f, 0.45f }, 0.0f },
+        { FURN_CHAIR,   { 12.15f, fl, 0.8f }, { 0.45f, 0.9f, 0.45f }, PI_F },
+        { FURN_COUNTER, { -1.9f, fl, 4.3f }, { 3.0f, 0.9f, 0.6f }, PI_F * 0.5f },      /* the kitchen */
+        { FURN_STOVE,   { -0.55f, fl, 2.6f }, { 0.6f, 0.9f, 0.6f }, PI_F },
+        { FURN_TABLE,   { -2.0f, fl, 2.4f }, { 1.2f, 0.75f, 0.8f }, 0.0f },
+        { FURN_CHAIR,   { -2.0f, fl, 1.65f }, { 0.45f, 0.9f, 0.45f }, -PI_F * 0.5f },
+    };
+    for (size_t i = 0; i < sizeof(items) / sizeof(items[0]) && p->ok; i++) {
+        Object *o = Object_createFurniture(items[i].kind, toWorld(p, items[i].at), items[i].size, p->yaw + items[i].turn);
+        if (!o || !World_add(p->world, o)) p->ok = false;
+    }
+}
+
+/* ---------- Your unfinished building: a brick shell under a roof ---------- */
+
+#define SHELL_W     6.4f  /* along x */
+#define SHELL_D     4.7f  /* along z (+z: the back, against the barn) */
+#define SHELL_WALL  2.9f
+#define SHELL_GAP   2.0f  /* the way in, from the extension in front */
+
+static const Vec3 BRICK = { 0.78f, 0.42f, 0.30f };
+
+bool Shell_build(World *world, Vec3 origin, float yaw) {
+    static Plan plan;
+    Plan *p = &plan;
+    memset(p, 0, sizeof(*p));
+    p->world = world;
+    p->origin = origin;
+    p->yaw = yaw;
+    p->ok = true;
+    const float x0 = -SHELL_W * 0.5f, x1 = SHELL_W * 0.5f, z0 = -SHELL_D * 0.5f, z1 = SHELL_D * 0.5f;
+    Gap way_in = { -SHELL_GAP * 0.5f, SHELL_GAP * 0.5f, 0.0f, 2.2f };
+    wallX(p, x0, x1, z0, z0 + T, 0.0f, SHELL_WALL, &way_in, 1, BRICK, EAVES_WALL); /* the front: open to the extension */
+    wallX(p, x0, x1, z1 - T, z1, 0.0f, SHELL_WALL, NULL, 0, BRICK, EAVES_WALL);     /* the back */
+    Gap hole[1] = { { -0.5f, 0.5f, 1.0f, 2.0f } };                                 /* window holes, no glass yet */
+    wallZ(p, z0 + T, z1 - T, x0, x0 + T, 0.0f, SHELL_WALL, hole, 1, BRICK, EAVES_WALL);
+    wallZ(p, z0 + T, z1 - T, x1 - T, x1, 0.0f, SHELL_WALL, hole, 1, BRICK, EAVES_WALL);
+    box(p, vec3(x0 + T, 0.0f, z0 + T), vec3(x1 - T, 0.02f, z1 - T), (Vec3){ 0.55f, 0.50f, 0.44f }, LOOK_ONLY); /* bare concrete */
+    p->roofs[p->n_roofs++] = (ShapeRoof){ vec3(0.0f, 0.0f, 0.0f), SHELL_W, SHELL_D, SHELL_WALL, SHELL_D * ROOF_PITCH, BUILDING_BRICK };
+    roofSlabs(p, &p->roofs[0]);
+    if (!p->ok) return false;
+    Mesh mesh = Shapes_structure(p->slabs, p->n_slabs, p->openings, p->n_openings, p->roofs, p->n_roofs);
+    if (!mesh.vao) return false;
+    Object *look = Object_createStructure(mesh, origin, yaw, vec3(SHELL_W, SHELL_WALL, SHELL_D));
+    return look && World_add(world, look);
+}
+
+bool Manor_build(World *world, Vec3 origin, float yaw) {
+    static Plan plan; /* (big: not on the stack) */
+    Plan *p = &plan;
+    memset(p, 0, sizeof(*p));
+    p->world = world;
+    p->origin = origin;
+    p->yaw = yaw;
+    p->ok = true;
+    leftHalf(p);
+    rightHalf(p);
+    for (int i = 0; i < p->n_roofs; i++) roofSlabs(p, &p->roofs[i]);
+    furnish(p);
+    if (!p->ok) return false;
+    Mesh mesh = Shapes_structure(p->slabs, p->n_slabs, p->openings, p->n_openings, p->roofs, p->n_roofs);
+    if (!mesh.vao) return false;
+    Object *look = Object_createStructure(mesh, origin, yaw, vec3(2.0f * HALF_LEN, WALL_TOP, PORCH_Z1 - LEFT_Z0));
+    return look && World_add(world, look);
+}
diff --git a/entropie/src/world/manor.h b/entropie/src/world/manor.h
new file mode 100644
index 0000000..c609180
--- /dev/null
+++ b/entropie/src/world/manor.h
@@ -0,0 +1,30 @@
+#ifndef WORLD_MANOR_H
+#define WORLD_MANOR_H
+
+/*
+ * The main building, made of parts you can walk into: two halves side by
+ * side (the right one a little further out into the yard), a porch along the
+ * front of the right half. The ground floor sits a few steps above the
+ * ground (basement windows below it).
+ *
+ * Left half: the front door by your garages; inside, on the left four steps
+ * up to the ground floor, on the right the (locked) basement door. A corridor:
+ * on the right the toilet, then the kitchen; on the left two locked rooms (the
+ * second the living room); at its end the big room of the right half (the
+ * meeting table). Right half: in through the porch from its far end, up the
+ * steps, right through the door.
+ *
+ * Local frame: x along the building (left to right seen from the yard),
+ * +z towards the yard, y up from the ground.
+ */
+
+#include "world/world.h"
+
+/* Builds it at origin (the middle of the front... see manor.c), turned by yaw. */
+bool Manor_build(World *world, Vec3 origin, float yaw);
+
+/* Your unfinished building: a brick shell under a roof, open at the front
+   (-z) to its extension (the frame where the bar gets built); +z against the barn. */
+bool Shell_build(World *world, Vec3 origin, float yaw);
+
+#endif /* WORLD_MANOR_H */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 416eeb8..65225dc 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -338,6 +338,14 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         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;
+        obj->weight    = 1000.0f;
+        addCollider(obj, vec3_zero(), size, vec3_zero());
+        break;
+
+    case OBJ_STRUCTURE:
     case OBJ_DECAL: /* made with its mesh by Object_createDecal */
         obj->is_static = true;
         break;
@@ -375,7 +383,7 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
     }
     }
 
-    if (!obj->is_character && !obj->mesh.vao) {
+    if (!obj->is_character && !obj->mesh.vao && type != OBJ_WALL) {
         SDL_Log("Object_create: mesh creation failed (type %d)", (int)type);
         Object_destroy(obj);
         return NULL;
@@ -397,6 +405,15 @@ Object *Object_createOnGround(ObjectType type, Palette palette, Vec3 position, V
     return obj;
 }
 
+Object *Object_createStructure(Mesh mesh, Vec3 position, float yaw, Vec3 size) {
+    Object *obj = Object_createDecal(mesh, position);
+    if (!obj) return NULL;
+    obj->type = OBJ_STRUCTURE;
+    obj->size = size; /* (big: always drawn) */
+    obj->rotation = vec3(0.0f, yaw, 0.0f);
+    return obj;
+}
+
 Object *Object_createDecal(Mesh mesh, Vec3 position) {
     Object *obj = calloc(1, sizeof(*obj));
     if (!obj) {
@@ -571,6 +588,19 @@ Object *Object_createFurniture(FurnitureKind kind, Vec3 position, Vec3 size, flo
     return o;
 }
 
+#define GATE_SWING_TIME 1.2f /* s from shut to open */
+
+void Object_swingGate(Object *g, float dt) {
+    float goal = g->gate_open ? 1.0f : 0.0f;
+    g->gate_t += clampf(goal - g->gate_t, -dt / GATE_SWING_TIME, dt / GATE_SWING_TIME);
+    float a = smoothstepf(0.0f, 1.0f, g->gate_t) * PI_F * 0.5f * 0.97f; /* (open: nearly flat against the wall) */
+    Vec3 dir = vec3_normalize(vec3_add(vec3_scale(g->gate_shut, cosf(a)), vec3_scale(g->gate_out, sinf(a))));
+    Vec3 mid = vec3_add(g->hinge, vec3_scale(dir, g->size.x * 0.5f));
+    g->position = vec3(mid.x, g->hinge.y, mid.z);
+    g->rotation.y = atan2f(dir.z, dir.x); /* (local x along the leaf; same as the site's yawFor) */
+    Object_syncColliders(g);
+}
+
 void Object_useToilet(Object *who, Object *toilet) {
     if (toilet->dog.mode) return; /* occupied */
     who->toilet = toilet;
@@ -967,7 +997,7 @@ static const char *bottleName(ObjectType type) {
 
 bool Object_isHard(const Object *o) {
     switch (o->type) {
-    case OBJ_STONE: case OBJ_BOX_STATIC: case OBJ_CYLINDER_STATIC: case OBJ_HOUSE: case OBJ_BARN: case OBJ_STUMP:
+    case OBJ_STONE: case OBJ_BOX_STATIC: case OBJ_CYLINDER_STATIC: case OBJ_HOUSE: case OBJ_BARN: case OBJ_STUMP: case OBJ_WALL:
     case OBJ_FIREPLACE: case OBJ_FURNITURE: case OBJ_BIN: case OBJ_SUPPLY: case OBJ_LADDER:
     case OBJ_AXE: case OBJ_CHAINSAW: case OBJ_BOTTLE: case OBJ_VODKA: case OBJ_WATER: case OBJ_CAN:
         return true;
@@ -1082,6 +1112,9 @@ void Object_describe(const Object *obj, char *out, size_t size) {
     case OBJ_CYLINDER:
         snprintf(out, size, "%s, %d kg", obj->type == OBJ_BOX ? "Crate" : "Drum", kg);
         break;
+    case OBJ_WALL:
+        snprintf(out, size, "%s", obj->name[0] ? obj->name : "Wall");
+        break;
     default:
         snprintf(out, size, "%d kg", kg);
         break;
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index a6e7283..55f3617 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -77,6 +77,9 @@ typedef enum {
                             work = how far it's been chopped out (see tools.h) */
     OBJ_TAPE,            /* a roll of duct tape: mends a melted hose (see hose.h) */
     OBJ_URINELLA,        /* (only in pockets - a box of them is an OBJ_SUPPLY that dispenses these) */
+    OBJ_WALL,            /* static, invisible: a solid box of a building made of parts (walls, floors, steps);
+                            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) */
 } ObjectType;
 
 /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */
@@ -252,6 +255,10 @@ typedef struct Object {
     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       gate_open;     /* gates: open (or opening) ... */
+    float      gate_t;        /* ... how far open (0 shut .. 1 open) */
+    Vec3       hinge;         /* ... where it turns */
+    Vec3       gate_shut, gate_out; /* ... the way it reaches shut, and open (flat, unit) */
     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) ... */
@@ -376,6 +383,10 @@ Object *Object_createOnGround(ObjectType type, Palette palette, Vec3 position, V
 
 /* Something lying on the ground (a road, water): a static object around a ready mesh. */
 Object *Object_createDecal(Mesh mesh, Vec3 position);
+/* A gate leaf (FURN_GATE) turning about its hinge towards gate_open (dt: 0 puts it right there). */
+void    Object_swingGate(Object *gate, float dt);
+/* A building made of parts: its look (the solid parts are OBJ_WALLs of their own). */
+Object *Object_createStructure(Mesh mesh, Vec3 position, float yaw, Vec3 size);
 
 /* How bad it smells at pos (0 = fine .. 1 = right over something fresh). */
 /* Per-frame index of a few kinds that everybody looks for (the world builds it
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index 240c9ab..0590238 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -99,7 +99,7 @@ static float halfAcross(const Collider *c) {
 static bool neverOver(const Object *o) {
     switch (o->type) {
     case OBJ_HOUSE: case OBJ_BARN: case OBJ_BUSH: case OBJ_FIREPLACE: case OBJ_DOG:
-    case OBJ_HUMAN: case OBJ_PLAYER: case OBJ_DECAL: case OBJ_PUDDLE: case OBJ_STINK:
+    case OBJ_HUMAN: case OBJ_PLAYER: case OBJ_DECAL: case OBJ_PUDDLE: case OBJ_STINK: case OBJ_WALL: case OBJ_STRUCTURE:
         return true;
     case OBJ_TREE:
         return !(o->toppled && o->topple >= 1.0f); /* (one lying on the ground: over it, or onto it) */
diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c
index 3723a6a..1fefce5 100644
--- a/entropie/src/world/scene.c
+++ b/entropie/src/world/scene.c
@@ -156,6 +156,12 @@ static bool addRamp(World *world, Vec3 yard) {
 }
 
 bool Scene_finish(World *world) {
+    /* Everybody knows where the firewood goes: the woodpile by the barn */
+    Object *pile = NULL;
+    for (int i = 0; i < world->count && !pile; i++)
+        if (world->objects[i]->type == OBJ_WOODPILE) pile = world->objects[i];
+    for (int i = 0; i < world->count && pile; i++)
+        if (world->objects[i]->type == OBJ_HUMAN) Ai_setWoodpile(world->objects[i], pile);
     return addRamp(world, Site_yard());
 }
 
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index bb4f432..2acb32b 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -1,4 +1,5 @@
 #include "world/site.h"
+#include "world/manor.h"
 #include "world/ladder.h"
 
 #include <stdio.h>
@@ -245,16 +246,20 @@ static const Piece WALLED[] = {
 };
 static const Vec3 WALLED_CENTER = { 23.25f, 0.0f, 0.0f };
 
-/* The open frame: four pillars and four beams (no roof: it doesn't show from above). */
+/* Your unfinished building: the brick shell at the back (against the barn's
+   end, see manor.c Shell_build, centered at SHELL_AT here), and in front of
+   it the frame of tree trunks for its extension, where the bar gets built:
+   four pillars and four beams. */
+static const Vec3 SHELL_AT = { -13.2f, 0.0f, -6.65f };
 static const Piece FRAME[] = {
     { OBJ_CYLINDER_STATIC, { -10.2f, 0, -13.8f }, { 0.4f, 3.0f, 0.4f }, { 0, 0, 0 } },
-    { OBJ_CYLINDER_STATIC, { -10.2f, 0,  -5.0f }, { 0.4f, 3.0f, 0.4f }, { 0, 0, 0 } },
+    { OBJ_CYLINDER_STATIC, { -10.2f, 0,  -9.2f }, { 0.4f, 3.0f, 0.4f }, { 0, 0, 0 } },
     { OBJ_CYLINDER_STATIC, { -16.2f, 0, -13.8f }, { 0.4f, 3.0f, 0.4f }, { 0, 0, 0 } },
-    { OBJ_CYLINDER_STATIC, { -16.2f, 0,  -5.0f }, { 0.4f, 3.0f, 0.4f }, { 0, 0, 0 } },
-    { OBJ_CYLINDER_STATIC, { -10.2f, -1.4f, -9.4f  }, { 0.4f, 8.8f, 0.4f }, { 90, 0, 0 } },
-    { OBJ_CYLINDER_STATIC, { -16.2f, -1.4f, -9.4f  }, { 0.4f, 8.8f, 0.4f }, { 90, 0, 0 } },
-    { OBJ_CYLINDER_STATIC, { -13.2f,  0.0f, -13.8f }, { 0.4f, 6.0f, 0.4f }, { 90, 0, 90 } },
-    { OBJ_CYLINDER_STATIC, { -13.2f,  0.0f,  -5.0f }, { 0.4f, 6.0f, 0.4f }, { 90, 0, 90 } },
+    { OBJ_CYLINDER_STATIC, { -16.2f, 0,  -9.2f }, { 0.4f, 3.0f, 0.4f }, { 0, 0, 0 } },
+    { OBJ_CYLINDER_STATIC, { -10.2f, 0.7f, -11.5f }, { 0.4f, 4.6f, 0.4f }, { 90, 0, 0 } },
+    { OBJ_CYLINDER_STATIC, { -16.2f, 0.7f, -11.5f }, { 0.4f, 4.6f, 0.4f }, { 90, 0, 0 } },
+    { OBJ_CYLINDER_STATIC, { -13.2f, 0.0f, -13.8f }, { 0.4f, 6.0f, 0.4f }, { 90, 0, 90 } },
+    { OBJ_CYLINDER_STATIC, { -13.2f, 0.0f,  -9.2f }, { 0.4f, 6.0f, 0.4f }, { 90, 0, 90 } },
 };
 static const Vec3 FRAME_CENTER = { -13.2f, 0.0f, -9.4f };
 
@@ -294,13 +299,17 @@ typedef struct {
     float      door;
 } CourtDoorBuilding;
 
+#define BARN_A    45.0f  /* the brick barn */
+#define BARN_B     7.0f
+#define BARN_LEN  28.0f
+#define BARN_WIDTH 10.0f
 static const CourtDoorBuilding COURT[] = {
-    /* main building: the front door right beside your garages */
-    { OBJ_HOUSE, PALETTE_STANDARD,  1.0f,  -1.0f, { 36.0f, 2.0f * FLOOR, 10.0f }, true,  -7.5f },
-    { OBJ_HOUSE, PALETTE_MODERN,    4.0f,  12.0f, { 16.0f, 2.0f * FLOOR, 10.0f }, false,  5.0f }, /* its other wing */
-    { OBJ_BARN,  PALETTE_WARM,     45.0f,   7.0f, { 28.0f, 2.0f * FLOOR, 10.0f }, true,  NO_DOOR }, /* the barn */
+    /* (the main building is made of parts you can walk into: see manor.c, MANOR_A / MANOR_B) */
+    { OBJ_BARN,  PALETTE_WARM,     BARN_A, BARN_B, { BARN_LEN, 2.0f * FLOOR, BARN_WIDTH }, true,  NO_DOOR }, /* the barn (brick) */
     { OBJ_BARN,  PALETTE_GROUND,   46.5f, -16.3f, { 11.0f, 3.0f, 8.4f }, false, NO_DOOR },          /* end of the garage row */
 };
+#define MANOR_A   1.0f   /* the main building: where its two halves meet, in the middle of its depth */
+#define MANOR_B  -1.0f
 #define WALLED_A 20.6f   /* your garages: along the side, right from the main building */
 #define WALLED_B -14.75f
 #define FRAME_A  34.8f   /* your unfinished building: at the right end of the barn */
@@ -838,6 +847,27 @@ static bool furnishYard(World *world) {
         !addStatic(world, OBJ_BIN, PALETTE_STANDARD, court(15.0f, 20.0f), vec3(1.8f, 1.4f, 1.2f), yawFor(mapDir(COURT_ANGLE))) ||
         !addStatic(world, OBJ_SUPPLY, PALETTE_GROUND, court(17.2f, 20.0f), vec3(0.45f, 0.3f, 0.3f), 0.0f))
         return false;
+    for (int k = 0; k < 3; k++) { /* the tripod's poles: solid (you walk round them, not through) */
+        Vec3 foot = vec3_add(fire, Shapes_tripodFoot(FIRE_SIZE, k)), top = vec3_add(fire, vec3(0.0f, GRILL_TOP, 0.0f));
+        foot.y = fire.y;
+        Vec3 d = vec3_sub(top, foot);
+        float len = vec3_length(d);
+        d = vec3_scale(d, 1.0f / len);
+        Vec3 mid = vec3_scale(vec3_add(foot, top), 0.5f);
+        Object *pole = Object_create(OBJ_WALL, PALETTE_WARM, vec3(mid.x, mid.y - len * 0.5f, mid.z), vec3(0.1f, len, 0.1f), 1000.0f);
+        if (!pole) return false;
+        float tilt = acosf(clampf(d.y, -1.0f, 1.0f)), best = -1.0f;
+        Vec3 pick = vec3_zero();
+        for (int s = 0; s < 4; s++) { /* (the turn that lays its length along the pole) */
+            Vec3 r = vec3((s & 1 ? -1.0f : 1.0f) * tilt, atan2f((s & 2 ? -1.0f : 1.0f) * d.x, d.z), 0.0f);
+            Object_setRotation(pole, r);
+            float fit = fabsf(vec3_dot(pole->colliders[0].axes[1], d));
+            if (fit > best) { best = fit; pick = r; }
+        }
+        Object_setRotation(pole, pick);
+        snprintf(pole->name, sizeof(pole->name), "Tripod pole");
+        if (!World_add(world, pole)) return false;
+    }
     for (int k = 0; k < 3; k++) { /* log benches round the fire */
         float a = deg2rad(BENCH_ANGLES[k]);
         Vec3 tangent = vec3_add(vec3_scale(mapDir(COURT_ANGLE), -sinf(a)), vec3_scale(mapDir(COURT_ANGLE - 90.0f), cosf(a)));
@@ -869,6 +899,64 @@ static bool furnishYard(World *world) {
     return true;
 }
 
+/* Your garages' doorways get green wooden gates (open at the start): from the
+   hinge (z, in the front wall at x 19.25) this wide. */
+static const struct { float hinge, end; } GATES[] = {
+    { 12.5f, 11.0f },                     /* the tool room door */
+    { 3.0f, 1.0f }, { -1.0f, 1.0f },      /* the kitchen */
+    { -9.0f, -11.5f }, { -14.0f, -11.5f }, /* the car repairs */
+};
+#define GATE_HEIGHT 2.9f
+
+static bool hangGates(World *world, Vec3 walled, float wyaw, float zsign) {
+    for (int i = 0; i < COUNT(GATES); i++) {
+        Vec3 h = placed(vec3(19.25f, 0.0f, GATES[i].hinge), WALLED_CENTER, walled, wyaw, zsign);
+        Vec3 e = placed(vec3(19.25f, 0.0f, GATES[i].end), WALLED_CENTER, walled, wyaw, zsign);
+        Vec3 o = placed(vec3(18.25f, 0.0f, GATES[i].hinge), WALLED_CENTER, walled, wyaw, zsign);
+        Vec3 along = vec3_sub(e, h), out = vec3_sub(o, h);
+        along.y = out.y = 0.0f;
+        float w = vec3_length(along) - 0.04f;
+        Object *g = Object_createFurniture(FURN_GATE, h, vec3(w, GATE_HEIGHT, 0.06f), 0.0f);
+        if (!g) return false;
+        g->hinge = h;
+        g->gate_shut = vec3_normalize(along);
+        g->gate_out = vec3_normalize(out);
+        g->gate_open = true;
+        g->gate_t = 1.0f;
+        Object_swingGate(g, 0.0f);
+        if (!World_add(world, g)) return false;
+    }
+    return true;
+}
+
+/* The barn: ivy climbing high up its walls; the woodpile against it, left of the toilet. */
+#define WOODPILE_A 39.1f
+#define WOODPILE_B -1.5f
+
+static bool dressBarn(World *world, Vec3 center, float yaw) {
+    const Mat4 r = mat4_rotate_yxz(vec3(0.0f, yaw, 0.0f));
+    Vec3 out = mat4_mul_dir(r, vec3(0.0f, 0.0f, 1.0f)); /* (which long side faces the yard) */
+    float front = vec3_dot(out, vec3_sub(Site_yard(), center)) > 0.0f ? 1.0f : -1.0f;
+    static const struct { float x, width, height; bool yard_side; } IVY[] = {
+        { -11.0f, 2.2f, 5.2f, true }, { -4.5f, 1.6f, 4.4f, true }, { 4.0f, 2.6f, 5.5f, true }, { 10.5f, 1.8f, 4.8f, true },
+        { -8.0f, 2.4f, 5.3f, false }, { 7.0f, 2.0f, 4.6f, false },
+    };
+    for (int i = 0; i < COUNT(IVY); i++) {
+        float side = IVY[i].yard_side ? front : -front;
+        Vec3 at = vec3_add(center, mat4_mul_dir(r, vec3(IVY[i].x, 0.0f, side * (BARN_WIDTH * 0.5f + 0.01f))));
+        Mesh mesh = Shapes_ivy(IVY[i].width, IVY[i].height, 101u + (unsigned)i * 37u);
+        if (!mesh.vao) return false;
+        Object *ivy = Object_createStructure(mesh, at, yaw + (side > 0.0f ? 0.0f : PI_F), vec3(IVY[i].width, IVY[i].height, 0.2f));
+        if (!ivy || !World_add(world, ivy)) return false;
+    }
+    const Vec3 size = { 1.6f, 0.0f, 0.6f };
+    Object *pile = Object_create(OBJ_WOODPILE, PALETTE_WARM, court(WOODPILE_A, WOODPILE_B), size, 0.0f);
+    if (!pile) return false;
+    Object_setRotation(pile, vec3(0.0f, yaw, 0.0f));
+    snprintf(pile->name, sizeof(pile->name), "Woodpile");
+    return World_add(world, pile);
+}
+
 bool Site_build(World *world) {
     /* The courtyard */
     const float along = yawFor(mapDir(COURT_ANGLE)), across = yawFor(mapDir(COURT_ANGLE - 90.0f));
@@ -881,6 +969,9 @@ bool Site_build(World *world) {
         if (!World_add(world, o)) return false;
     }
 
+    if (!Manor_build(world, court(MANOR_A, MANOR_B), across)) return false;
+    if (!dressBarn(world, court(BARN_A, BARN_B), across)) return false;
+
     /* Your garages turn their doors (local -X) to the yard; your unfinished
        building stands square to the barn, its long side along the yard. */
     Vec3 walled = court(WALLED_A, WALLED_B), dir = mapDir(COURT_ANGLE);
@@ -892,7 +983,8 @@ bool Site_build(World *world) {
                   courtA(placed(vec3(23.25f, 0.0f, -12.0f), WALLED_CENTER, walled, wyaw, 1.0f)) ? 1.0f : -1.0f;
     if (!placePieces(world, WALLED, COUNT(WALLED), WALLED_CENTER, walled, wyaw, zsign) ||
         !placePieces(world, FRAME, COUNT(FRAME), FRAME_CENTER, court(FRAME_A, FRAME_B), across, 1.0f) ||
-        !furnishGarages(world, walled, wyaw, zsign))
+        !Shell_build(world, placed(SHELL_AT, FRAME_CENTER, court(FRAME_A, FRAME_B), across, 1.0f), across) ||
+        !furnishGarages(world, walled, wyaw, zsign) || !hangGates(world, walled, wyaw, zsign))
         return false;
     if (!buildGarages(world) || !plantOrchard(world) || !furnishYard(world)) return false;
 
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index ef89e0a..4bdac57 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -691,6 +691,7 @@ 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_GATE) { if (f->gate_t != (f->gate_open ? 1.0f : 0.0f)) Object_swingGate(f, world->dt_props); continue; }
         if (f->variant == FURN_ASHTRAY) {
             int butts = f->stored < 12 ? f->stored : 12 + (f->stored >= 14);
             if (butts == f->shown) continue;
@@ -1054,6 +1055,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;
     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];
@@ -1202,7 +1204,7 @@ static void drawCharred(Renderer *renderer, const Object *o) {
 void World_drawShadows(const World *world, Renderer *renderer) {
     for (int i = 0; i < world->count; i++) {
         const Object *obj = world->objects[i];
-        if (flatOnGround(obj) || (obj->is_character && obj->toilet)) continue; /* flat on the ground, or hidden: no shadow */
+        if (flatOnGround(obj) || (obj->is_character && obj->toilet) || (!obj->is_character && !obj->mesh.vao)) continue; /* flat on the ground, hidden, nothing to see: no shadow */
         bool big = fmaxf(obj->size.x, obj->size.z) >= BIG_SIZE * 3.0f;
         if (!big && flatDistance2(obj->position, renderer->shadow_focus) > SHADOW_REACH * SHADOW_REACH) continue;
         if (obj->is_character) {
@@ -1243,6 +1245,7 @@ void World_draw(World *world, Renderer *renderer, bool draw_player) {
     for (int i = 0; i < world->count; i++) {
         const Object *obj = world->objects[i];
         if (obj->type == OBJ_PLAYER && !draw_player) continue; /* first person */
+        if (!obj->is_character && obj->type != OBJ_DOG && !obj->mesh.vao) continue; /* (invisible: a building's walls) */
         if (outOfSight(obj, renderer->viewer_pos)) continue;
         if (obj->is_character && obj->toilet) continue; /* in the toilet */
         if (obj->is_character) {
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index cf798c9..5380689 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -14,6 +14,7 @@
 #include "world/grid.h"
 
 typedef struct World {
+    float    dt_props;   /* (this frame's step, for the props that move: gates) */
     Object **objects;
     int      count;
     int      capacity;