Nimbin[12]?Games / Entropie / commits / 3e627a9

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.91.0: a basement, chairs you can sit on, the camera stays out of walls

Christian Immanuel · 2026-10-19 08:45 · 3e627a97e118c914126d66c9b577ea0b3ee3baa6

- Main building: the porch only along the right part of the front; the
  right half's roof turned (ridge front to back); chairs, counter, stove
  and shelf face the right way (one yaw convention: yaw_facing / yaw_along
  / yaw_front in linalg.h); kitchen with a shelf and two chairs
- Basement: stairs down on the right of the entrance, old loose brick
  walls, open rooms, shelves, loose bricks; the ground has a hole there
  (Terrain_addPit, not drawn inside); brick courses on the shell's walls
- Camera: pulled in by walls between your head and it, eases back out;
  your own body is hidden when it's right behind your head
- Chairs: pulled out to sit (a hand on the backrest), pushed back in after
  you leave; sitting down no longer gets stuck on the table
- Ground contact: loose things rest on their real lowest points and lie
  along the slope; logs on their round side; things slide down steeper
  slopes (round ones sooner)
- Walking: the hips rise and fall with the steps; strafing no longer
  swings the pelvis out (springs following a moving goal lagged behind it)
- Edges higher than a stair (the ramp's side, crates): a step up, no jolt
- Trees have a flared foot that becomes the stump: no pop when felled; the
  felled trunk shows its sawn end instead of being hollow
- One shared box ray (Collider_ray, World_rayCast)
- ARCHITECTURE.md and RULES.md: how the code is built, do's and don'ts
 entropie/ARCHITECTURE.md              |  84 ++++++++++++
 entropie/Makefile                     |   2 +-
 entropie/README.md                    |  15 +-
 entropie/RULES.md                     |  38 ++++++
 entropie/STATE.md                     |  27 ++--
 entropie/assets/shaders/scene.frag    |  10 ++
 entropie/commit_message.txt           |  36 +++--
 entropie/src/core/gl.h                |   2 +
 entropie/src/game/game.c              |  53 ++++++--
 entropie/src/game/game.h              |   3 +-
 entropie/src/math/linalg.h            |  12 ++
 entropie/src/people/body_internal.h   |   4 +-
 entropie/src/people/body_pose.c       |  11 +-
 entropie/src/render/renderer.c        |  16 +++
 entropie/src/render/renderer.h        |   5 +
 entropie/src/render/shapes.c          |  28 ++--
 entropie/src/render/shapes.h          |  12 +-
 entropie/src/render/shapes_internal.h |   7 +
 entropie/src/render/shapes_props.c    |  40 ++++--
 entropie/src/world/forestry.c         |   2 +-
 entropie/src/world/ladder.c           |  31 +----
 entropie/src/world/manor.c            | 249 +++++++++++++++++++++++++---------
 entropie/src/world/object.c           |  87 +++++++++++-
 entropie/src/world/object.h           |  15 ++
 entropie/src/world/physics.c          | 192 ++++++++++++++++++++++----
 entropie/src/world/site.c             |  26 ++--
 entropie/src/world/terrain.c          |  26 ++++
 entropie/src/world/terrain.h          |  17 +++
 entropie/src/world/world.c            |  95 ++++++++++---
 entropie/src/world/world.h            |   9 ++
 info.txt                              |   1 +
 31 files changed, 937 insertions(+), 218 deletions(-)

diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
new file mode 100644
index 0000000..cbf1100
--- /dev/null
+++ b/entropie/ARCHITECTURE.md
@@ -0,0 +1,84 @@
+# Entropie – architecture
+
+How the code is put together. Read before adding a feature; update it in the
+same version when the structure changes (rules for working on it: [RULES.md](RULES.md)).
+
+## Modules
+
+| Folder | What it is | May include |
+|---|---|---|
+| `src/core/` | window, GL loader (`gl.h`: every GL call is listed there), input | core |
+| `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math |
+| `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render |
+| `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, water, grass, grid, day | everything but game |
+| `src/people/` | bodies (animation), hands, AI, talk, condition, dog, climbing | everything but game |
+| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI | all |
+
+- `render` knows nothing of the world: it gets meshes, matrices and plain numbers.
+- `world` and `people` are one layer: an `Object` owns its `Body`, `Ai` and `Hands`.
+- `*_internal.h`: shared inside one module only.
+
+## One frame
+
+`Game_update` → input, actions, talk, player → `World_update`:
+physics for every object (near ones from the `Grid`) → grid rebuilt → felling and
+cutting → hands (pushing, carrying) → hose → AI → bodies animated → props
+(gates, chairs, ashtray, coat rack). Then the camera follows (`game.c`), and
+`Game_render`: shadow pass, scene pass (terrain, objects, figures, grass, smoke),
+ink pass, UI.
+
+Budget: the update takes about 1 ms for ~950 objects. Drawing skips what's out
+of view (objects, 8×8 terrain tiles).
+
+## Objects
+
+- One struct for every thing (`world/object.h`): `type`, `variant`, size,
+  position, rotation (yaw `rotation.y`), up to 3 oriented box colliders.
+- Fields only some kinds use are commented with the kind ("characters:",
+  "gates:", "chairs:"). Objects start zeroed: zero must be a safe default.
+- `is_static`: never moved by physics. Only their own code moves them
+  (`Object_swingGate`, `Object_slideChair`), then `Object_syncColliders`.
+- Meshes come from `Shapes_*` and are rebuilt only when what they show changes
+  (`shown` pattern in `world.c` showProps).
+
+## Space and turning
+
+- y up, meters, seconds. Yaw: local +X turns to (cos, 0, sin), +Z to (-sin, 0, cos).
+- `yaw_along(dir)`: lengths along +X (logs, gates, wall pieces).
+  `yaw_facing(dir)`: fronts along +Z (furniture, doors, windows, chairs).
+  `yaw_front(yaw)`: where a front points.
+- The courtyard: `court(a, b)` in `site.c`. The main building: local x along
+  it, +z towards the yard (`manor.c`).
+
+## Building the map
+
+`Scene_load` (start things) → `Site_build` (static things, buildings, roads)
+→ `World_buildTerrain` (levels the ground under static things) →
+`Site_finish` / `Scene_finish` (things that sit on the ground).
+
+- Buildings you walk into (`manor.c`): a `Plan` of boxes (`box`, `wallX`,
+  `wallZ` with gaps, `opening`). The look is one `OBJ_STRUCTURE` mesh, the
+  solid parts invisible `OBJ_WALL`s.
+- Below the ground: `Terrain_addPit` (a turned rectangle). The ground's height
+  there is the pit floor, the ground isn't drawn there (`holes` in `scene.frag`).
+
+## Physics
+
+- Per object, against near objects and the terrain. Characters: walk up stairs
+  (up to `SNAP_UP_MOST`), higher edges and things to step over or onto are a set
+  move (`over_t`, `beginStepOver`); sitting down is a set move too (`sit_moving`).
+- Rays: `Collider_ray` / `Object_rayToCollider` (one box), `World_rayCast`
+  (the first thing along a ray, with a skip filter). Down to the ground:
+  `World_groundHeight`.
+
+## Rendering
+
+- One scene shader for everything not a figure: uniforms switch it to terrain
+  (`terrainSize`), decals, grass, glow. Figures have their own shader. The ink
+  pass draws the outlines over it all.
+- New GL calls: add them to the list in `core/gl.h`.
+
+## Versions
+
+`Makefile` `VERSION`; the build id is an md5 of sources and assets
+(`make version`). `STATE.md` ends with the version it describes.
diff --git a/entropie/Makefile b/entropie/Makefile
index 216c863..720cebd 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.90.0
+VERSION   := 1.91.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 2e91db6..d4b0c8b 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -7,6 +7,7 @@ Walk around the site, work with the people there, watch the sun go by.
 
 What the game can do so far, who is in it and what is still open:
 [STATE.md](STATE.md) (kept up to date with every version).
+How the code is built: [ARCHITECTURE.md](ARCHITECTURE.md); rules for changing it: [RULES.md](RULES.md).
 
 ## Look
 
@@ -36,12 +37,13 @@ and satellite pictures (about 340 x 340 m).
   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
+  further out (its roof ridge runs front to back), with a porch along the
+  right part of its front. The ground floor sits four steps up. Walk in:
+  - Left door, by your garages: four steps up on the left, stairs down to the
+    basement on the right. 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
+  - The basement: old loose brick walls, open rooms, shelves
   - 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
@@ -97,7 +99,8 @@ and satellite pictures (about 340 x 340 m).
   only really good grilled; apples and bread get better too
 - People come over to a burning fire, sit down round it and look into the flames
 - C: sit down yourself (and rest faster): on a bench or chair (right on
-  the seat) if one is next to you, else on a log lying there, else cross-legged on the ground;
+  the seat; a chair is pulled out first and pushed back in after) if one is next
+  to you, else on a log lying there, else cross-legged on the ground;
   walking gets you up
 
 ## Cleaning up
@@ -574,7 +577,7 @@ say no more often and don't come to you for a bit.
 | F1            | Dev menu: pick anyone, change body, needs, temperament, likes and skills (Up / Down, Left / Right); teleport you to the parkour or the yard |
 | Esc           | Menu: resume, keys, quit (the game waits meanwhile) |
 
-The game starts in third person.
+The game starts in third person. Close to a wall the view moves in, never through it.
 With a tool and nothing to work on, a click drinks from the other hand.
 Right click while talking ends the talk instead of putting things down.
 While pushing you can look around; the object stays put.
diff --git a/entropie/RULES.md b/entropie/RULES.md
new file mode 100644
index 0000000..d1503f6
--- /dev/null
+++ b/entropie/RULES.md
@@ -0,0 +1,38 @@
+# Entropie – rules for changing the code
+
+Do's and don'ts that keep the game clean and quick. Short on purpose: add a
+rule when a mistake shouldn't happen twice, drop one when it no longer holds.
+Structure: [ARCHITECTURE.md](ARCHITECTURE.md).
+
+## Do
+
+- Keep the layers: `render` never includes `world`, `people` or `game`.
+- Turn things with the helpers in `math/linalg.h` (`yaw_facing`, `yaw_along`,
+  `yaw_front`). Fronts are +Z, lengths are +X.
+- Tunables as `#define` at the top of the file, with unit and meaning.
+- Ask "what's near" through the `Grid`, rays through `World_rayCast` /
+  `Collider_ray`, the ground through `World_groundHeight`.
+- Static things go in before the terrain; things lying on the ground are
+  placed after it (`Scene_finish`, `Site_finish`).
+- A new building part: `manor.c`'s `Plan` (boxes, walls with gaps, openings).
+- Rebuild a mesh only when what it shows changes.
+- Compile without warnings: `-Wall -Wextra -Wpedantic -Wshadow`.
+- Characters moved by script (stepping over, onto, sitting down) use the set
+  moves, never a jump of the position.
+- Every version: bump `VERSION`, update `STATE.md` (done items out of the to-do
+  list, version line at the end), keep `README.md` short, write
+  `commit_message.txt` with this version's changes only.
+
+## Don't
+
+- No `atan2f` for a yaw in new code, no `mat4_rotate_y` for an object's turn.
+- No allocation per frame, no loop over all objects inside a loop over all objects.
+- No reusing another kind's fields for a new meaning: add a field, say whose it is.
+- No moving static objects behind physics' back (use their own function, then
+  `Object_syncColliders`).
+- No game rules in `render`, no input handling in `world` or `people`.
+- No snapping a character up more than a stair (`SNAP_UP_MOST`).
+- No copying a slab test, ray or spring: there is one of each (`Collider_ray`,
+  `springStep`).
+- README: never name Ubuntu (Debian or "any Linux"), always the
+  "100% Vibecode – but tested" line, no extra files nobody asked for.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 4435e04..8648275 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -77,7 +77,21 @@ 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.
+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, the stairs down to the basement 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 from the porch landing. Porch z 5.8..7.3: way in at x 18, ground floor, four steps up going left, landing to the door (where: see 1.91 below). The old OBJ_HOUSE main building and its wing are gone.
+
+Main building (1.91): the porch covers the right part of the front only (x 9.5..18, door to the big room at x 11, steps at x 12.6..13.8); the right half's roof ridge runs front to back (`ShapeRoof.across`, its slabs tilted about z; eaves wall at its right end). Furniture faces where it should: `math/linalg.h` `yaw_facing` (fronts +Z: furniture, doors, windows), `yaw_along` (lengths +X), `yaw_front`; `manor.c` gives facings as vectors (FACE_PX ...). The bug was two conventions mixed (openings turned the other way round than objects). Kitchen: counter along the front wall, stove against the big room's wall, a shelf, table with two chairs. Basement (BASE_Y -2.3): stairs down on the right of the entrance (13 steps of 0.177 x 0.25, from the landing at z 4.2 going back, a rail along the well), under the corridor and the back rooms (x -9.6..-0.15, z -4.7..1.4) and under the stairs, toilet and kitchen (z 1.4..4.7); old brick walls round it (`ShapeSlab.brick`: mortar courses, finer than outside), loose brick walls inside with openings, no doors, loose bricks on the walls and the floor, 8 shelves. The ground goes down there: `Terrain_addPit` (up to TERRAIN_PITS 4 turned rectangles, kept through Terrain_init): Terrain_height is the pit floor, `scene.frag` doesn't draw the ground inside (`holes`, `Renderer_setHoles`). The unfinished building's walls are brick (courses) too.
+
+Camera (1.91, `game.c` followPlayer): the third-person boom is pulled in by what's between the head and where it would be (5 rays: middle and 0.15 m round it, `World_rayCast` with static things wider than 0.35 m; the ground along it), CAMERA_CLEAR 0.25 m off it; in at once, out again at 2.5 m/s (`camera_reach`). Pulled in closer than 0.6 m your own body isn't drawn.
+
+Chairs (1.91, `object.c`): sitting down on one pulls it out CHAIR_PULL 0.38 m (CHAIR_SPEED 0.9 m/s, a free hand on its backrest within 0.9 m), from where it stands (`tucked`); clear of it again it's pushed back in, once nobody stands within 0.6 m of that spot (`world.c` chairWaits). Sitting down is a set move: no physics meanwhile (the table pushed you off the spot before), and no carrying over (`crossing`) while sitting.
+
+Ground contact (1.91, `physics.c`): loose things lifted to their lowest corner's ground (all 8 corners), and once at rest (SETTLE_AFTER 0.3 s) turned to lie on the ground's slope (`settleOnGround`, up to SETTLE_STEEPEST 0.5); logs and lying round things are stood on at their round top (`world.c` roundLying). On a slope they slide down (`slideDown`: round things grip GRIP_ROUND 0.12, flat ones GRIP_FLAT 0.5). Edges higher than SNAP_UP_MOST 0.22 m (a ramp's side, a crate) are one step up (`beginStepOver`, the onto-move landing STEP_PROBE ahead), not a jolt; stairs are still just walked up.
+
+Walking (1.91, `body_pose.c`): the hips rise over the standing leg mid-step and drop as the other foot lands (WALK_BOB 0.022 m, more for the unfit and the bouncy), until jogging takes over; side steps: the hips stay near the middle (SWAY_MOST 0.045). The springs that follow a moving goal (hips over the character, swinging feet, hands) lagged behind it as far as they were allowed (the hips 8 cm: sideways when strafing): `springStep` now moves the goal on during the step.
+
+Trees and stumps (1.91): a tree's foot flares out (FOOT_FLARE 0.12 up to 0.45 m) into roots, and its stump is that same foot (`shapes_addTrunkFoot`, seed `Shapes_treeSeed` of the spot, as wide as the trunk): nothing pops when it's felled. A felled trunk shows its sawn end (`trunk_cut`), it isn't hollow.
+
+Shared code (1.91): one box ray (`Collider_ray`, `Object_rayToCollider`; the ladder's copy is gone, world.c's down ray uses it), `World_rayCast`, `Object_reach`. ARCHITECTURE.md (modules, the frame, conventions) and RULES.md (do's and don'ts), kept current.
 
 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.
 
@@ -211,17 +225,14 @@ Most important first.
 - 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
+- Trees: push / pull fallen trees; branches break or snag on obstacles (with enough momentum); much easier to carry without branches
+- Steps on the course (middle lane): check the feet land on what they stand on (flat ground checked: planted feet within 1 cm)
 - 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
+- NPCs sitting at the meeting table (chairs work for them, nothing sends them there yet)
 
 **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
@@ -244,4 +255,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.90.0 (build 81ba84f)`
+State of `entropie 1.91.0 (build 286f693)`
diff --git a/entropie/assets/shaders/scene.frag b/entropie/assets/shaders/scene.frag
index 03f740a..7f7eeef 100644
--- a/entropie/assets/shaders/scene.frag
+++ b/entropie/assets/shaders/scene.frag
@@ -24,6 +24,9 @@ uniform float grass;
 uniform sampler2D shadowMap;
 uniform sampler2D waterMap;
 uniform sampler2D surfaceMap; // asphalt, concrete, gravel, bare earth (Site_surface)
+uniform vec4  holes[4];    // holes in the ground (basements): middle xz, x axis (cos, sin) ...
+uniform vec2  holeSize[4]; // ... half sizes
+uniform int   holeCount;
 uniform vec4  tint;   // rgb: darkens (charred wood), a: glows like embers
 
 const vec3  PATH_EARTH  = vec3(0.58, 0.44, 0.28);
@@ -54,6 +57,13 @@ vec3 surfaceColor(vec3 albedo, vec4 s, vec3 p, float patches) {
 const float WATER_SHOWN = 25.0; // mm of standing water at a full texture value (wetness.c)
 
 void main() {
+    if (terrainSize > 0.0) {
+        for (int i = 0; i < holeCount; i++) {
+            vec2 d = WorldPos.xz - holes[i].xy;
+            vec2 uv = vec2(dot(d, holes[i].zw), dot(d, vec2(-holes[i].w, holes[i].z)));
+            if (all(lessThanEqual(abs(uv), holeSize[i]))) discard;
+        }
+    }
     vec3 n = normalize(Normal);
     vec3 l = normalize(lightDirection);
 
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index 3489211..8445eb3 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,15 +1,23 @@
-entropie 1.90.0: walk into the main building, a brick barn, the shell
+entropie 1.91.0: a basement, chairs you can sit on, the camera stays out of walls
 
-- 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
+- Main building: the porch only along the right part of the front; the
+  right half's roof turned (ridge front to back); chairs, counter, stove
+  and shelf face the right way (one yaw convention: yaw_facing / yaw_along
+  / yaw_front in linalg.h); kitchen with a shelf and two chairs
+- Basement: stairs down on the right of the entrance, old loose brick
+  walls, open rooms, shelves, loose bricks; the ground has a hole there
+  (Terrain_addPit, not drawn inside); brick courses on the shell's walls
+- Camera: pulled in by walls between your head and it, eases back out;
+  your own body is hidden when it's right behind your head
+- Chairs: pulled out to sit (a hand on the backrest), pushed back in after
+  you leave; sitting down no longer gets stuck on the table
+- Ground contact: loose things rest on their real lowest points and lie
+  along the slope; logs on their round side; things slide down steeper
+  slopes (round ones sooner)
+- Walking: the hips rise and fall with the steps; strafing no longer
+  swings the pelvis out (springs following a moving goal lagged behind it)
+- Edges higher than a stair (the ramp's side, crates): a step up, no jolt
+- Trees have a flared foot that becomes the stump: no pop when felled; the
+  felled trunk shows its sawn end instead of being hollow
+- One shared box ray (Collider_ray, World_rayCast)
+- ARCHITECTURE.md and RULES.md: how the code is built, do's and don'ts
diff --git a/entropie/src/core/gl.h b/entropie/src/core/gl.h
index a407b91..964905a 100644
--- a/entropie/src/core/gl.h
+++ b/entropie/src/core/gl.h
@@ -32,6 +32,8 @@
     X(PFNGLUNIFORM2FPROC,               glUniform2f)                 \
     X(PFNGLUNIFORM3FPROC,               glUniform3f)                 \
     X(PFNGLUNIFORM3FVPROC,              glUniform3fv)                \
+    X(PFNGLUNIFORM4FVPROC,              glUniform4fv)                \
+    X(PFNGLUNIFORM2FVPROC,              glUniform2fv)                \
     X(PFNGLUNIFORM4FPROC,               glUniform4f)                 \
     X(PFNGLUNIFORMMATRIX4FVPROC,        glUniformMatrix4fv)          \
     /* buffers & vertex arrays */                                    \
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 6f15418..c09147e 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -17,7 +17,37 @@
 #define THIRD_PERSON_SHOULDER 0.45f
 #define SLEEP_FAST_FORWARD 60.0f /* game time runs this much faster while you sleep holding Space */
 
-static void followPlayer(Game *game) {
+/* The camera never goes into a wall: what's between the head and where it
+   would be pulls it in (at once), and it eases back out when there's room. */
+#define CAMERA_CLEAR    0.25f /* m kept off what it would go into */
+#define CAMERA_OUT_RATE 2.5f  /* m/s back out */
+#define CAMERA_FAN      0.15f /* m: rays round the middle one (the view's corners) */
+
+static bool cameraSkips(const Object *o, const void *ctx) {
+    (void)ctx;
+    if (o->is_character || o->is_held || o->type == OBJ_DOG) return true;
+    if (!o->is_static) return true; /* (loose things don't shove the view about) */
+    return Object_reach(o) < 0.35f;  /* (nor poles and posts) */
+}
+
+static float clearDistance(const World *world, Vec3 from, Vec3 dir, float want, Vec3 right, Vec3 up) {
+    float best = want;
+    const Vec3 fan[5] = { vec3_zero(), vec3_scale(right, CAMERA_FAN), vec3_scale(right, -CAMERA_FAN),
+                          vec3_scale(up, CAMERA_FAN), vec3_scale(up, -CAMERA_FAN) };
+    for (int k = 0; k < 5; k++) {
+        float t = World_rayCast(world, vec3_add(from, fan[k]), dir, want + CAMERA_CLEAR, cameraSkips, NULL, NULL, NULL);
+        if (t >= 0.0f) best = fminf(best, t - CAMERA_CLEAR);
+    }
+    /* (the ground along the way) */
+    for (int k = 1; k <= 4; k++) {
+        float d = best * (float)k / 4.0f;
+        Vec3 at = vec3_add(from, vec3_scale(dir, d));
+        if (at.y < World_groundHeight(world, at.x, at.z, from.y) + CAMERA_CLEAR) { best = fmaxf(0.0f, d - CAMERA_CLEAR); break; }
+    }
+    return fmaxf(best, 0.0f);
+}
+
+static void followPlayer(Game *game, float dt) {
     Camera *cam = &game->camera;
     const Object *p = game->player;
     Vec3 eye = vec3_add(p->position, vec3(0, PLAYER_EYE_HEIGHT, 0));
@@ -29,10 +59,16 @@ static void followPlayer(Game *game) {
         eye = vec3_add(head, vec3_add(vec3_scale(face, 0.1f), vec3(0.0f, 0.05f, 0.0f)));
     }
     if (game->third_person) {
-        /* Over the right shoulder, behind the player */
-        eye = vec3_sub(eye, vec3_scale(cam->front, THIRD_PERSON_DISTANCE));
-        eye = vec3_add(eye, vec3_scale(cam->right, THIRD_PERSON_SHOULDER));
-        eye.y += 0.25f;
+        /* Over the right shoulder, behind the player: the boom pulled in by walls */
+        Vec3 want = vec3_add(vec3_add(vec3_scale(cam->front, -THIRD_PERSON_DISTANCE), vec3_scale(cam->right, THIRD_PERSON_SHOULDER)),
+                             vec3(0.0f, 0.25f, 0.0f));
+        float len = vec3_length(want);
+        Vec3 dir = vec3_scale(want, 1.0f / len);
+        Vec3 up = vec3_normalize(vec3_cross(cam->right, dir));
+        float room = clearDistance(&game->world, eye, dir, len, cam->right, up);
+        if (game->camera_reach <= 0.0f || room < game->camera_reach) game->camera_reach = room;
+        else game->camera_reach = fminf(room, game->camera_reach + CAMERA_OUT_RATE * dt);
+        eye = vec3_add(eye, vec3_scale(dir, game->camera_reach));
     }
     cam->position = eye;
 }
@@ -176,6 +212,7 @@ bool Game_init(Game *game) {
     game->message = NULL;
     game->message_time = 0.0f;
     game->third_person = true;
+    game->camera_reach = 0.0f;
     game->holding = false;
     game->click_action = 0;
     game->info[0] = '\0';
@@ -200,7 +237,7 @@ bool Game_init(Game *game) {
     }
     Camera_init(&game->camera, game->camera.position, look * 180.0f / PI_F, 0.0f, CAMERA_FOV);
     game->marker = Shapes_marker(1.8f, 0.8f);
-    followPlayer(game);
+    followPlayer(game, 0.0f);
     return true;
 }
 
@@ -304,7 +341,7 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     float dark_goal = b->asleep && b->pose != POSE_UP ? 1.0f : 0.0f;
     game->dark += (dark_goal - game->dark) * fminf(1.0f, 0.8f * dt);
 
-    followPlayer(game);
+    followPlayer(game, dt);
     swayView(game);
     Camera_updateMatrices(&game->camera, aspect);
 }
@@ -371,7 +408,7 @@ void Game_render(Game *game, Renderer *renderer) {
     renderer->drunk = fminf(game->player->drunk, 1.0f);
     renderer->dark  = game->dark * (game->third_person ? 0.55f : 0.92f); /* eyes shut */
     Renderer_beginScenePass(renderer, &game->camera, light);
-    World_draw(&game->world, renderer, game->third_person);
+    World_draw(&game->world, renderer, game->third_person && game->camera_reach > 0.6f); /* (pulled right in: from the head) */
     Meadow_draw(&game->meadow, &game->world, renderer, game->camera.position);
 
     /* Showing where the firewood goes: the spot, on the ground in front of you */
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index 7f3c0ca..a1641a8 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -18,7 +18,8 @@ typedef struct {
     Object  *player;
     Camera   camera;
     DayCycle daycycle;
-    bool     third_person;  /* camera behind the player (V) */
+    bool     third_person;  /* camera behind the player (V) ... */
+    float    camera_reach;  /* ... this far from the head (pulled in by walls) */
     float    dark;          /* 0..1, fades in while the player sleeps */
     bool     show_keys;     /* key list on screen (I) */
     bool     fast_sleep;    /* time runs fast while asleep (hold Space) */
diff --git a/entropie/src/math/linalg.h b/entropie/src/math/linalg.h
index caabd06..5a61d0f 100644
--- a/entropie/src/math/linalg.h
+++ b/entropie/src/math/linalg.h
@@ -141,6 +141,18 @@ static inline Mat4 mat4_rotate_yxz(Vec3 rad) {
     return mat4_mul(r, mat4_rotate_z(rad.z));
 }
 
+/*
+ * Yaw (rotation.y) conventions, the one place to look them up. A yaw turns
+ * local +X to (cos, 0, sin) and local +Z to (-sin, 0, cos).
+ *   yaw_along(dir):  the yaw that lays local +X along dir (lengths: logs, gates, walls)
+ *   yaw_facing(dir): the yaw that turns local +Z (an object's front: furniture,
+ *                    doors, windows) towards dir
+ *   yaw_front(yaw):  where local +Z points after that yaw
+ */
+static inline float yaw_along(Vec3 dir)  { return atan2f(dir.z, dir.x); }
+static inline float yaw_facing(Vec3 dir) { return atan2f(-dir.x, dir.z); }
+static inline Vec3  yaw_front(float yaw) { return vec3(-sinf(yaw), 0.0f, cosf(yaw)); }
+
 /*
  * Inverse of mat4_rotate_yxz: Euler angles of the rotation that turns the
  * local Y axis into y_axis and the local Z axis into z_axis (both unit and
diff --git a/entropie/src/people/body_internal.h b/entropie/src/people/body_internal.h
index 5700efe..8a0f3f5 100644
--- a/entropie/src/people/body_internal.h
+++ b/entropie/src/people/body_internal.h
@@ -158,6 +158,7 @@ enum {
 #define LEG_RAISE_SPEED   2.5f   /* m/s a foot comes up off the ground when its leg must */
 #define OVER_LEAN         0.22f  /* radians the body leans forward stepping over something high */
 #define BOB_OMEGA         28.0f  /* the running wave follows the strides this quickly */
+#define WALK_BOB          0.022f /* m walking: up mid-step, down as a foot lands (either way) */
 #define BOB_FROM          1.8f   /* m/s: from a brisk walk on ... */
 #define BOB_FULL          3.6f   /* ... to a run: the body rises and falls with each stride */
 #define BOB_FIT           0.035f /* m down onto each step, sporty ... */
@@ -180,6 +181,7 @@ enum {
 #define SWAY_OMEGA    13.0f  /* ... and from foot to foot */
 #define HAND_OMEGA    26.0f  /* ... and the hands to what they reach for */
 #define HAND_WEIGHT_OMEGA 11.0f /* ... and a hand taking up (or giving up) what it does */
+#define SWAY_MOST     0.045f /* m the hips go over the standing foot at most (scaled out: unscaled body) */
 #define ROOT_OMEGA    12.0f  /* the hips follow the character's position on a spring ... */
 #define ROOT_LAG      0.08f  /* ... never further behind than this (meters) */
 #define HAND_FOLLOW   4.0f   /* a hand goal moving faster than this (m/s) jumped: the hand eases there */
@@ -274,7 +276,7 @@ static inline void springStep(float *value, float *velocity, float goal, float g
     if (dt <= 0.0f) return;
     float d = *value - goal, w = *velocity - goal_v;
     float e = expf(-omega * dt), t = (w + omega * d) * dt;
-    *value = goal + (d + t) * e;
+    *value = goal + goal_v * dt + (d + t) * e; /* (the goal moves on meanwhile: no lag behind a steady goal) */
     *velocity = goal_v + (w - omega * t) * e;
 }
 
diff --git a/entropie/src/people/body_pose.c b/entropie/src/people/body_pose.c
index 5f57623..4666475 100644
--- a/entropie/src/people/body_pose.c
+++ b/entropie/src/people/body_pose.c
@@ -456,8 +456,16 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt
             goal_y = fminf(goal_y, pelvisReach(vec3_add(ankle[i], vec3(0.0f, heel[i], 0.0f)),
                                                i == 0 ? -HIP_X : HIP_X));
     }
+    const float reach_y = goal_y; /* (as high as the standing legs reach) */
     goal_y -= b->crouch * 0.42f;
     goal_y -= 0.010f + 0.022f * fminf(speed / 3.0f, 1.0f) + 0.03f * u + 0.012f * tired + 0.12f * bentOver(b);
+    {   /* Walking: up over the standing leg in the middle of each step, down as the other foot lands */
+        float walk = smoothstepf(0.25f, 0.9f, speed) * (1.0f - smoothstepf(BOB_FROM, BOB_FULL, speed)) * (b->airborne ? 0.0f : 1.0f);
+        float swing = 0.0f;
+        for (int i = 0; i < 2; i++) if (b->feet[i].stepping) swing = fmaxf(swing, sinf(PI_F * b->feet[i].step));
+        float bob = WALK_BOB * walk * (2.0f * swing - 1.0f) * (1.2f - 0.4f * b->fitness) * (1.0f + 0.3f * b->style[2]);
+        goal_y = fminf(goal_y + bob, reach_y);
+    }
     /* The hips follow on a spring: they speed up and slow down, never jolt */
     {
         const float was = b->pelvis_y;
@@ -471,7 +479,8 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt
     for (int i = 0; i < 2; i++) {
         const Foot *other = &b->feet[1 - i];
         if (b->feet[i].stepping && !other->stepping) {        /* standing on the other foot */
-            sway = ankle[1 - i].x * 0.3f * (1.0f + 0.35f * b->style[3]) * sinf(PI_F * b->feet[i].step); /* (some sway their hips more) */
+            sway = clampf(ankle[1 - i].x * 0.3f, -SWAY_MOST, SWAY_MOST) /* (side steps: the feet wide apart, the hips stay near the middle) */
+                 * (1.0f + 0.35f * b->style[3]) * sinf(PI_F * b->feet[i].step); /* (some sway their hips more) */
             hip_y[i] = -0.018f * sinf(PI_F * b->feet[i].step); /* the free hip drops a little */
         }
     }
diff --git a/entropie/src/render/renderer.c b/entropie/src/render/renderer.c
index 0c79144..61c25a4 100644
--- a/entropie/src/render/renderer.c
+++ b/entropie/src/render/renderer.c
@@ -44,6 +44,9 @@ static bool loadShaders(Renderer *r) {
     s->wear_map        = glGetUniformLocation(s->program, "wearMap");
     s->water_map       = glGetUniformLocation(s->program, "waterMap");
     s->surface_map     = glGetUniformLocation(s->program, "surfaceMap");
+    s->holes           = glGetUniformLocation(s->program, "holes");
+    s->hole_size       = glGetUniformLocation(s->program, "holeSize");
+    s->hole_count      = glGetUniformLocation(s->program, "holeCount");
 
     ShadowShader *d = &r->shadow;
     d->program = Shader_load("shaders/shadow.vert", "shaders/shadow.frag");
@@ -415,6 +418,19 @@ void Renderer_drawDecal(Renderer *r, const Mesh *mesh, Mat4 model, float terrain
     glUniform1f(r->scene.decal_size, 0.0f);
 }
 
+void Renderer_setHoles(Renderer *r, const float (*rects)[6], int count) {
+    float at[RENDER_HOLES * 4] = { 0 }, size[RENDER_HOLES * 2] = { 0 };
+    if (count > RENDER_HOLES) count = RENDER_HOLES;
+    for (int i = 0; i < count; i++) {
+        for (int k = 0; k < 4; k++) at[i * 4 + k] = rects[i][k];
+        size[i * 2] = rects[i][4];
+        size[i * 2 + 1] = rects[i][5];
+    }
+    glUniform4fv(r->scene.holes, RENDER_HOLES, at);
+    glUniform2fv(r->scene.hole_size, RENDER_HOLES, size);
+    glUniform1i(r->scene.hole_count, count);
+}
+
 void Renderer_drawTerrain(Renderer *r, const Mesh *mesh, GLuint wear_map, GLuint water_map, GLuint surface_map,
                           float size, float grass_depth) {
     const SceneShader *s = &r->scene;
diff --git a/entropie/src/render/renderer.h b/entropie/src/render/renderer.h
index 9500ab0..acb5af3 100644
--- a/entropie/src/render/renderer.h
+++ b/entropie/src/render/renderer.h
@@ -48,6 +48,7 @@ typedef struct {
     GLint  decal_size;                           /* things lying on the ground: sink with it */
     GLint  tint;                                 /* color multiplier and ember glow */
     GLint  grass;                                /* grass blades: terrain size while drawing them */
+    GLint  holes, hole_size, hole_count;         /* holes in the ground (basements): not drawn there */
 } SceneShader;
 
 typedef struct {
@@ -146,6 +147,10 @@ void Renderer_drawDecal(Renderer *r, const Mesh *mesh, Mat4 model, float terrain
  * (see world/wetness.h); surface_map too: asphalt, concrete, gravel, bare
  * earth (see Site_surface).
  */
+/* Holes in the ground drawn next (up to RENDER_HOLES): each a rectangle,
+   middle x, z, its x axis (cos, sin) and half sizes x, z. */
+#define RENDER_HOLES 4
+void Renderer_setHoles(Renderer *r, const float (*rects)[6], int count);
 void Renderer_drawTerrain(Renderer *r, const Mesh *mesh, GLuint wear_map, GLuint water_map, GLuint surface_map,
                           float size, float grass_depth);
 
diff --git a/entropie/src/render/shapes.c b/entropie/src/render/shapes.c
index 6e17855..26e840e 100644
--- a/entropie/src/render/shapes.c
+++ b/entropie/src/render/shapes.c
@@ -239,21 +239,21 @@ static void addRoof(MeshBuilder *b, float length, float width, float wall_height
 #define BRICK_LEN   0.9f
 #define MORTAR      0.03f
 
-static void addBrickCourses(MeshBuilder *b, float length, float width, float wall_height, Vec3 wall) {
+static void addBrickCourses(MeshBuilder *b, float length, float width, float wall_height, Vec3 wall, float course, float brick_len) {
     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;
+        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;
+            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);
+                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;
@@ -274,7 +274,7 @@ static void addBuilding(MeshBuilder *b, float length, float width, float wall_he
     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);
+    if (look == BUILDING_BRICK) addBrickCourses(b, length, width, wall_height, wall, COURSE, BRICK_LEN);
     addRoof(b, length, width, wall_height, roof_height, look);
 }
 
@@ -337,12 +337,18 @@ float Shapes_treeHeight(TreeSize size) {
 static const Vec3 COLOR_FRUIT_LEAVES = { 0.42f, 0.66f, 0.30f };
 static const Vec3 COLOR_APPLE_RED    = { 0.84f, 0.20f, 0.16f };
 
+unsigned Shapes_treeSeed(float x, float z) {
+    return (unsigned)(int)(x * 811.0f) ^ ((unsigned)(int)(z * 677.0f) << 10);
+}
+
 Mesh Shapes_tree(TreeSize size) {
     const int segments = 12;
 
     MeshBuilder b;
     MeshBuilder_init(&b);
     shapes_addTube(&b, 0.0f, size.trunk_radius, size.trunk_radius, size.trunk_height, segments, COLOR_BARK, false);
+    if (size.cut) shapes_addCutEnd(&b, size.trunk_radius); /* (not hollow) */
+    else shapes_addTrunkFoot(&b, size.trunk_radius, 0.0f, 0.45f, size.seed, segments);
 
     /* Leaf cones, stacked: a pine shrinks towards the top, a fruit tree is round */
     const Vec3 leaves = size.fruit ? COLOR_FRUIT_LEAVES : COLOR_LEAVES;
@@ -567,6 +573,11 @@ Mesh Shapes_structure(const ShapeSlab *slabs, int n_slabs, const ShapeOpening *o
         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);
+        if (s->brick) {
+            size_t first = b.vertex_count;
+            addBrickCourses(&b, size.x, size.z, size.y, s->color, COURSE * 0.5f, BRICK_LEN * 0.5f); /* (seen close up: finer) */
+            transformSince(&b, first, mat4_translate(vec3((s->min.x + s->max.x) * 0.5f, s->min.y, (s->min.z + s->max.z) * 0.5f)));
+        }
     }
     for (int i = 0; i < n_openings; i++) {
         const ShapeOpening *o = &openings[i];
@@ -600,7 +611,8 @@ Mesh Shapes_structure(const ShapeSlab *slabs, int n_slabs, const ShapeOpening *o
         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)));
+        Mat4 turn = r->across ? mat4_rotate_y(PI_F * 0.5f) : mat4_identity(); /* (ridge along Z) */
+        transformSince(&b, first, mat4_mul(mat4_translate(vec3(r->center.x, r->wall_top, r->center.z)), turn));
     }
     return Mesh_fromBuilder(&b);
 }
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index 1a85d2a..cf35e31 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -33,6 +33,8 @@ typedef struct {
     int   leaf_layers;   /* stacked cones, 0.6 apart, each 0.8 high */
     float leaf_radius;   /* radius of the lowest cone (fruit trees: the widest) */
     bool  fruit;         /* a round crown with apples instead of a pine */
+    bool  cut;           /* felled: no foot, the cut face at its end (else the foot flares into roots) */
+    unsigned seed;       /* the foot's roots (Shapes_stump makes the same ones from the same seed) */
 } TreeSize;
 
 #define TREE_FRUIT_LAYERS 8  /* layers of a whole fruit tree crown */
@@ -40,7 +42,8 @@ typedef struct {
 /* Total height of a tree (trunk plus cones). */
 float Shapes_treeHeight(TreeSize size);
 
-/* Trunk with stacked cone layers; base at y = 0. */
+/* Trunk with stacked cone layers; base at y = 0. Seed for a spot: Shapes_treeSeed. */
+unsigned Shapes_treeSeed(float x, float z);
 Mesh  Shapes_tree(TreeSize size);
 
 /* Brick walls and a slate roof, or a farm hall: light walls, gray, red or solar roof. */
@@ -238,19 +241,20 @@ 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 struct { Vec3 min, max; Vec3 color; bool brick; } ShapeSlab; /* brick: mortar courses on its sides */
 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 yaw;    /* turns its front (+Z) to yaw_front(yaw) (see linalg.h) */
     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 */
+    float length, width, wall_top, roof_height; /* length along the ridge: X, or Z if across */
     BuildingLook look;
+    bool  across;
 } ShapeRoof;
 Mesh  Shapes_structure(const ShapeSlab *slabs, int n_slabs, const ShapeOpening *openings, int n_openings,
                        const ShapeRoof *roofs, int n_roofs);
diff --git a/entropie/src/render/shapes_internal.h b/entropie/src/render/shapes_internal.h
index 1ff17c0..7cfa164 100644
--- a/entropie/src/render/shapes_internal.h
+++ b/entropie/src/render/shapes_internal.h
@@ -49,6 +49,13 @@ static inline void addBlock(MeshBuilder *b, Vec3 center, Vec3 size, Vec3 color)
     shapes_addBox(b, center, size, colors);
 }
 
+/* The foot of a trunk of radius r standing on the ground at ground_y: flaring
+   out towards it (up to top) and roots running into it. A tree and the stump
+   left of it get the same foot from the same seed. */
+void shapes_addTrunkFoot(MeshBuilder *b, float r, float ground_y, float top, unsigned seed, int segments);
+/* The sawn face at the foot of a felled trunk (y = 0). */
+void shapes_addCutEnd(MeshBuilder *b, float r);
+
 /* Small deterministic random numbers for scattering shards. */
 static inline float scatter(unsigned *seed) {
     *seed = *seed * 1664525u + 1013904223u;
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index c661da7..748505d 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -262,26 +262,44 @@ Mesh Shapes_vomit(float radius, unsigned seed, ShapeGround g) {
 }
 
 /* A tree stump, centered: bark round the sides, roots flaring into the ground, the cut on top. */
-Mesh Shapes_stump(float diameter, float height, unsigned seed) {
-    const float r = diameter * 0.5f, half = height * 0.5f;
-    MeshBuilder b;
-    MeshBuilder_init(&b);
-    shapes_addTube(&b, -half, r * 1.12f, r, height, 14, COLOR_BARK, false);
-    shapes_addTube(&b, half - 0.004f, r * 0.95f, r * 0.95f, 0.008f, 14, COLOR_CUT, true);
-    shapes_addTube(&b, half - 0.002f, r * 0.35f, r * 0.35f, 0.008f, 10, vec3_scale(COLOR_CUT, 0.85f), true); /* the heart */
+#define FOOT_FLARE  0.12f /* a trunk is this much wider at the ground ... */
+#define FOOT_HEIGHT 0.45f /* ... narrowing to its own width this high */
+
+static float footRadius(float r, float h) { return r * (1.0f + FOOT_FLARE * (1.0f - clampf(h / FOOT_HEIGHT, 0.0f, 1.0f))); }
+
+void shapes_addTrunkFoot(MeshBuilder *b, float r, float ground_y, float top, unsigned seed, int segments) {
+    float h = fminf(top - ground_y, FOOT_HEIGHT);
+    shapes_addTube(b, ground_y - 0.05f, footRadius(r, 0.0f) * 1.02f, footRadius(r, h), h + 0.05f, segments, COLOR_BARK, false);
     int roots = 4 + (int)(scatter(&seed) * 3.0f);
     for (int k = 0; k < roots; k++) { /* flaring out, into the ground */
         float a = ((float)k + 0.4f * scatter(&seed)) / (float)roots * 2.0f * PI_F;
         float len = r * (0.9f + 0.6f * scatter(&seed)), thick = r * (0.35f + 0.15f * scatter(&seed));
-        size_t first = b.vertex_count;
-        shapes_addTube(&b, 0.0f, thick, thick * 0.3f, len, 6, COLOR_BARK, false);
-        Mat4 m = mat4_mul(mat4_translate(vec3(cosf(a) * r * 0.75f, -half + thick * 0.5f, sinf(a) * r * 0.75f)),
+        size_t first = b->vertex_count;
+        shapes_addTube(b, 0.0f, thick, thick * 0.3f, len, 6, COLOR_BARK, false);
+        Mat4 m = mat4_mul(mat4_translate(vec3(cosf(a) * r * 0.75f, ground_y + thick * 0.5f, sinf(a) * r * 0.75f)),
                           mat4_mul(mat4_rotate_y(-a), mat4_rotate_z(-PI_F * 0.5f - 0.35f)));
-        transformSince(&b, first, m);
+        transformSince(b, first, m);
     }
+}
+
+Mesh Shapes_stump(float diameter, float height, unsigned seed) {
+    const float r = diameter * 0.5f, half = height * 0.5f;
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    shapes_addTrunkFoot(&b, r, -half, half, seed, 12); /* (the very foot the tree had: no pop when it's cut) */
+    if (height > FOOT_HEIGHT) shapes_addTube(&b, -half + FOOT_HEIGHT, r, r, height - FOOT_HEIGHT, 12, COLOR_BARK, false);
+    const float top = footRadius(r, height);
+    shapes_addTube(&b, half - 0.004f, top * 0.95f, top * 0.95f, 0.008f, 12, COLOR_CUT, true);
+    shapes_addTube(&b, half - 0.002f, top * 0.35f, top * 0.35f, 0.008f, 10, vec3_scale(COLOR_CUT, 0.85f), true); /* the heart */
     return Mesh_fromBuilder(&b);
 }
 
+/* The cut end of a felled trunk (at y = 0, facing down). */
+void shapes_addCutEnd(MeshBuilder *b, float r) {
+    shapes_addTube(b, -0.008f, r * 0.97f, r * 0.97f, 0.008f, 12, COLOR_CUT, true);
+    shapes_addTube(b, -0.010f, r * 0.35f, r * 0.35f, 0.004f, 10, vec3_scale(COLOR_CUT, 0.85f), true);
+}
+
 /* What's left of a stump chopped out: dug-up earth and root ends in it (following the ground). */
 Mesh Shapes_roots(float diameter, unsigned seed, ShapeGround g) {
     const Vec3 earth = { 0.26f, 0.19f, 0.12f };
diff --git a/entropie/src/world/forestry.c b/entropie/src/world/forestry.c
index 14e27e9..36f830a 100644
--- a/entropie/src/world/forestry.c
+++ b/entropie/src/world/forestry.c
@@ -37,7 +37,7 @@ static void fell(World *world, Object *tree, const Object *by, float cut_height)
     float stump_height = tree->position.y - base.y;
     Object_topple(tree, away);
 
-    float diameter = fmaxf(2.0f * tree->colliders[0].half_extents.x * 1.1f, STUMP_MIN);
+    float diameter = fmaxf(2.0f * tree->colliders[0].half_extents.x, STUMP_MIN); /* (as wide as the trunk: it looks the same as before) */
     Object *stump = Object_create(OBJ_STUMP, PALETTE_WARM, vec3_add(base, vec3(0.0f, stump_height * 0.5f, 0.0f)),
                                   vec3(diameter, stump_height, 0.0f), 0.0f);
     if (stump && World_add(world, stump)) tree->stump = stump;
diff --git a/entropie/src/world/ladder.c b/entropie/src/world/ladder.c
index 3cb7dea..2af8047 100644
--- a/entropie/src/world/ladder.c
+++ b/entropie/src/world/ladder.c
@@ -254,37 +254,12 @@ void Ladder_update(World *world, Object *l, float dt) {
     reshape(l);
 }
 
-/* Distance along a ray to an oriented box (and the face normal it enters
-   through), or -1. Slab test in the box's frame. */
-static float rayToBox(Vec3 origin, Vec3 dir, Vec3 center, const Collider *c, Vec3 *normal) {
-    float t_enter = -1e9f, t_exit = 1e9f;
-    int enter_axis = -1;
-    float enter_sign = 1.0f;
-    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
-    Vec3 rel = vec3_sub(origin, center);
-    for (int k = 0; k < 3; k++) {
-        float o = vec3_dot(rel, c->axes[k]), d = vec3_dot(dir, c->axes[k]);
-        if (fabsf(d) < 1e-6f) {
-            if (fabsf(o) > half[k]) return -1.0f;
-            continue;
-        }
-        float t1 = (-half[k] - o) / d, t2 = (half[k] - o) / d;
-        float sign = -1.0f; /* entering through the -half face */
-        if (t1 > t2) { float t = t1; t1 = t2; t2 = t; sign = 1.0f; }
-        if (t1 > t_enter) { t_enter = t1; enter_axis = k; enter_sign = sign; }
-        t_exit = fminf(t_exit, t2);
-    }
-    if (t_enter > t_exit || t_enter < 0.0f || enter_axis < 0) return -1.0f;
-    *normal = vec3_scale(c->axes[enter_axis], enter_sign);
-    return t_enter;
-}
-
 /* Highest top of o's colliders straight below (x, from_y, z), or -1e9. */
 static float topOf(const Object *o, float x, float z, float from_y) {
     float best = -1e9f;
     for (int c = 0; c < o->collider_count; c++) {
         Vec3 n;
-        float t = rayToBox(vec3(x, from_y, z), vec3(0.0f, -1.0f, 0.0f), Object_colliderCenter(o, c), &o->colliders[c], &n);
+        float t = Object_rayToCollider(o, c, vec3(x, from_y, z), vec3(0.0f, -1.0f, 0.0f), &n);
         if (t >= 0.0f) best = fmaxf(best, from_y - t);
     }
     return best;
@@ -304,7 +279,7 @@ static Object *faceAhead(const World *world, const Object *who, const Object *la
             Vec3 origin = vec3_add(who->position, vec3(0.0f, HEIGHTS[h], 0.0f));
             for (int c = 0; c < o->collider_count; c++) {
                 Vec3 n;
-                float t = rayToBox(origin, f, Object_colliderCenter(o, c), &o->colliders[c], &n);
+                float t = Object_rayToCollider(o, c, origin, f, &n);
                 if (t >= 0.0f && t < best) {
                     best = t;
                     hit = o;
@@ -329,7 +304,7 @@ static Object *rayHit(const World *world, const Object *who, const Object *ladde
         if (flatDist(o->position, origin) > 60.0f) continue;
         for (int c = 0; c < o->collider_count; c++) {
             Vec3 nn;
-            float t = rayToBox(origin, dir, Object_colliderCenter(o, c), &o->colliders[c], &nn);
+            float t = Object_rayToCollider(o, c, origin, dir, &nn);
             if (t >= 0.0f && t < best) {
                 best = t;
                 hit = o;
diff --git a/entropie/src/world/manor.c b/entropie/src/world/manor.c
index fa9ebde..1e39d27 100644
--- a/entropie/src/world/manor.c
+++ b/entropie/src/world/manor.c
@@ -1,4 +1,5 @@
 #include "world/manor.h"
+#include "world/terrain.h"
 
 #include <stdio.h>
 #include <string.h>
@@ -21,7 +22,7 @@
 #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 PORCH_X0  9.5f   /* (the right half of the front) */
 
 #define ROOF_PITCH (5.0f / 12.0f)
 
@@ -39,7 +40,7 @@ static const Vec3 PORCH_ROOF = { 0.38f, 0.38f, 0.43f };
 
 enum { LOOK_ONLY, SOLID, EAVES_WALL };
 
-#define MAX_SLABS    256
+#define MAX_SLABS    720
 #define MAX_OPENINGS 96
 
 typedef struct {
@@ -53,8 +54,19 @@ typedef struct {
     Vec3         origin;
     float        yaw;
     bool         ok;
+    bool         brick;  /* boxes made now are brick (mortar lines) */
+    unsigned     rng;
 } Plan;
 
+/* Which way a door, window or piece of furniture faces (local) */
+static const Vec3 FACE_PX = { 1.0f, 0.0f, 0.0f }, FACE_NX = { -1.0f, 0.0f, 0.0f };
+static const Vec3 FACE_PZ = { 0.0f, 0.0f, 1.0f }, FACE_NZ = { 0.0f, 0.0f, -1.0f };
+
+static float rnd(Plan *p) {
+    p->rng = p->rng * 1664525u + 1013904223u;
+    return (float)(p->rng >> 8) / 16777216.0f;
+}
+
 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));
 }
@@ -75,13 +87,14 @@ static Object *solid(Plan *p, Vec3 min, Vec3 max, int kind, const char *name) {
 /* 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 };
+    if (p->n_slabs < MAX_SLABS) p->slabs[p->n_slabs++] = (ShapeSlab){ min, max, color, p->brick };
     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 };
+/* A window, door or frame on a wall face at at, its front facing face (local). */
+static void opening(Plan *p, OpeningKind kind, Vec3 at, Vec3 face, float w, float h) {
+    if (p->n_openings < MAX_OPENINGS) p->openings[p->n_openings++] = (ShapeOpening){ kind, at, yaw_facing(face), w, h, FRAME };
     else p->ok = false;
 }
 
@@ -115,12 +128,12 @@ static void wallZ(Plan *p, float z0, float z1, float x0, float x1, float y0, flo
 }
 
 /* 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)));
+   that says what it is (clicked: "locked"). face: the way it faces. */
+static void lockedDoor(Plan *p, Vec3 foot, Vec3 face, float w, const char *name) {
+    opening(p, OPENING_DOOR, foot, face, w, DOOR_H - 0.1f);
+    Vec3 along = vec3(face.z, 0.0f, -face.x);
+    Vec3 a = vec3_add(foot, vec3_scale(along, -w * 0.5f));
+    Vec3 b = vec3_add(foot, vec3_add(vec3_scale(along, w * 0.5f), vec3_scale(face, 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;
@@ -129,8 +142,8 @@ static void lockedDoor(Plan *p, Vec3 foot, float yaw, float w, const char *name)
 /* 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);
+        opening(p, OPENING_WINDOW, vec3(xs[i], y, z), side > 0.0f ? FACE_PZ : FACE_NZ, w, h);
+        if (inside_too) opening(p, OPENING_PANE, vec3(xs[i], y, inner_z), side > 0.0f ? FACE_NZ : FACE_PZ, w, h);
     }
 }
 
@@ -147,6 +160,8 @@ static void windowsX(Plan *p, float z, float side, const float *xs, int n, float
 #define KITCHEN_X0 -3.6f
 #define LIVING_X0 -5.0f
 #define GARAGE_X1 -9.75f /* your garages stand in front up to here */
+#define WELL_X0   -6.95f /* the stairs down: from the landing at the front (WELL_TOP_Z), going back */
+#define WELL_TOP_Z 4.2f
 
 static void leftHalf(Plan *p) {
     const float x0 = -HALF_LEN, z0 = LEFT_Z0, z1 = LEFT_Z1;
@@ -161,11 +176,12 @@ static void leftHalf(Plan *p) {
     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);
+    opening(p, OPENING_FRAME, vec3(DOOR_X, 0.0f, z1), FACE_PZ, 1.0f, DOOR_H);
+    opening(p, OPENING_FRAME, vec3(DOOR_X, 0.0f, z1 - T), FACE_NZ, 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(VEST_X0, -0.06f, HALL_Z), vec3(WELL_X0, 0.02f, z1 - T), TILES, SOLID);
+    box(p, vec3(WELL_X0, -0.3f, WELL_TOP_Z), vec3(VEST_X1, 0.02f, z1 - T), TILES, SOLID); /* the landing at the top of the stairs down */
     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) */
@@ -176,22 +192,23 @@ static void leftHalf(Plan *p) {
     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) */
+    /* The entrance: its walls; on the right the stairs down to the basement (a rail along them) */
     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");
+    box(p, vec3(WELL_X0 - 0.05f, 0.0f, HALL_Z), vec3(WELL_X0, 0.95f, WELL_TOP_Z), RAIL, SOLID);
+    box(p, vec3(WELL_X0 - 0.06f, 0.0f, WELL_TOP_Z - 0.06f), vec3(WELL_X0 + 0.01f, 0.97f, WELL_TOP_Z), RAIL, LOOK_ONLY); /* (its post) */
 
     /* 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);
+    opening(p, OPENING_FRAME, vec3(-4.6f, FL, CORR_Z1), FACE_NZ, 0.8f, DOOR_H);
+    opening(p, OPENING_FRAME, vec3(-2.0f, FL, CORR_Z1), FACE_NZ, 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");
+    lockedDoor(p, vec3(-7.2f, FL, CORR_Z0), FACE_PZ, 0.9f, "A locked room");
+    lockedDoor(p, vec3(-2.6f, FL, CORR_Z0), FACE_PZ, 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);
@@ -201,8 +218,8 @@ static void leftHalf(Plan *p) {
     /* 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);
+    opening(p, OPENING_WINDOW, vec3(-4.6f, FL + 1.8f, z1), FACE_PZ, 0.6f, 0.6f);  /* the toilet's */
+    opening(p, OPENING_PANE, vec3(-4.6f, FL + 1.8f, z1 - T), FACE_NZ, 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];
@@ -212,31 +229,33 @@ static void leftHalf(Plan *p) {
     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 };
+                                          (z1 - z0) * ROOF_PITCH, BUILDING_PLASTER, false };
 }
 
 /* ---------- 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 */
+#define RDOOR_X   11.0f  /* the door from the porch */
+#define PSTEP_X   13.8f  /* the porch steps: up from here, going left */
+#define RIGHT_ROOF 4.5f  /* its roof: the ridge across (front to back), this high over the eaves */
 
 static void rightHalf(Plan *p) {
     const float x1 = HALF_LEN, z0 = RIGHT_Z0, z1 = RIGHT_Z1;
+    /* (the ridge runs front to back: gables at the front and back, the eaves over the right end) */
     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);
+    wallX(p, P, x1, z1 - T, z1, 0.0f, WALL_TOP, &door, 1, PLASTER, SOLID);
+    wallX(p, P, x1, z0, z0 + T, 0.0f, WALL_TOP, NULL, 0, PLASTER, SOLID);
     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);
+    opening(p, OPENING_FRAME, vec3(RDOOR_X, FL, z1), FACE_PZ, 1.0f, DOOR_H);
+    opening(p, OPENING_FRAME, vec3(RDOOR_X, FL, z1 - T), FACE_NZ, 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);
+    opening(p, OPENING_FRAME, vec3(P, FL, 0.0f), FACE_PX, CORR_Z1 - CORR_Z0, DOOR_H); /* from the corridor */
+    opening(p, OPENING_FRAME, vec3(-P, FL, 0.0f), FACE_NX, 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 };
+    /* Windows: the front (the right ones into the porch), the back, basement windows at the back */
+    const float front[] = { 2.5f, 6.0f, 13.0f, 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];
@@ -244,18 +263,19 @@ static void rightHalf(Plan *p) {
     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);
+    box(p, vec3(P, 0.0f, z1), vec3(PORCH_X0, FL - 0.05f, z1 + 0.02f), PLINTH, LOOK_ONLY); /* (left of the porch: the plinth shows) */
 
-    /* The porch: its front wall with windows, a closed left end, the way in at the right end */
+    /* The porch (the right half of the front): 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);
+    opening(p, OPENING_FRAME, vec3(x1, 0.0f, (in.a + in.b) * 0.5f), FACE_PX, 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);
+    const float pw[] = { 11.2f, 15.2f };
+    windowsX(p, pz1, 1.0f, pw, 2, 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++)
@@ -264,51 +284,147 @@ static void rightHalf(Plan *p) {
     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 };
+    ShapeRoof r = { vec3(HALF_LEN * 0.5f, 0.0f, (z0 + z1) * 0.5f), z1 - z0, HALF_LEN, WALL_TOP, RIGHT_ROOF, BUILDING_PLASTER_DARK, true };
+    p->roofs[p->n_roofs++] = r;
 }
 
-/* The roofs: two slabs under each (you can get onto them from a ladder). */
+/* The roofs: a slab under each slope (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);
+        const float s = (float)side;
+        /* ridge along x: the slopes to -z and +z; along z (across): to -x and +x */
+        Vec3 normal = r->across ? vec3(s * sinf(angle), cosf(angle), 0.0f) : vec3(0.0f, cosf(angle), s * sinf(angle));
+        Vec3 off = r->across ? vec3(s * half_w * 0.5f, 0.0f, 0.0f) : vec3(0.0f, 0.0f, s * half_w * 0.5f);
+        Vec3 surface = vec3_add(vec3(r->center.x, r->wall_top + r->roof_height * 0.5f, r->center.z), off);
         Vec3 center = toWorld(p, vec3_sub(surface, vec3_scale(normal, thick * 0.5f)));
-        Vec3 size = vec3(r->length, thick, slope);
+        Vec3 size = r->across ? vec3(slope, thick, r->length) : 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));
+        Object_setRotation(o, r->across ? vec3(0.0f, p->yaw, s * angle) : vec3(-s * 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. */
+/* ---------- The basement: under the left half, down from the entrance ---------- */
+
+#define BASE_Y    -2.3f  /* its floor */
+#define BASE_X0   -9.6f  /* the big part: under the corridor and the rooms at the back ... */
+#define BASE_X1   -0.15f
+#define BASE_Z0   -4.7f
+#define BASE_Z1    1.4f  /* (= HALL_Z) */
+#define UNDER_Z1   4.7f  /* ... and under the stairs, toilet and kitchen */
+#define STAIR_STEPS 13
+#define STAIR_TREAD 0.25f
+#define BW         0.25f /* its walls */
+
+static const Vec3 OLD_BRICK = { 0.62f, 0.36f, 0.27f };
+static const Vec3 EARTH_FLOOR = { 0.42f, 0.38f, 0.33f };
+static const Vec3 CONCRETE = { 0.60f, 0.58f, 0.54f };
+
+/* A few bricks loose on a wall face (sticking out a bit, darker or paler) and fallen to the floor. */
+static void looseBricks(Plan *p, Vec3 min, Vec3 max, int n) {
+    const bool along_x = max.x - min.x > max.z - min.z;
+    for (int i = 0; i < n; i++) {
+        float t = rnd(p), y = min.y + 0.3f + rnd(p) * (max.y - min.y - 0.5f), shade = 0.8f + 0.35f * rnd(p);
+        Vec3 c = vec3_scale(OLD_BRICK, shade);
+        float side = rnd(p) < 0.5f ? -1.0f : 1.0f;
+        if (along_x) {
+            float x = min.x + 0.2f + t * (max.x - min.x - 0.4f), z = side > 0.0f ? max.z : min.z;
+            box(p, vec3(x - 0.11f, y, z - 0.02f), vec3(x + 0.11f, y + 0.07f, z + 0.02f), c, LOOK_ONLY);
+        } else {
+            float z = min.z + 0.2f + t * (max.z - min.z - 0.4f), x = side > 0.0f ? max.x : min.x;
+            box(p, vec3(x - 0.02f, y, z - 0.11f), vec3(x + 0.02f, y + 0.07f, z + 0.11f), c, LOOK_ONLY);
+        }
+    }
+}
+
+static void basement(Plan *p) {
+    const float rise = -BASE_Y / (float)STAIR_STEPS;
+    /* Floor (packed earth, the ground itself goes down to it: see Terrain_addPit) and the ceiling seen from below */
+    box(p, vec3(BASE_X0, BASE_Y - 0.04f, BASE_Z0), vec3(BASE_X1, BASE_Y, BASE_Z1), EARTH_FLOOR, LOOK_ONLY);
+    box(p, vec3(WELL_X0, BASE_Y - 0.04f, BASE_Z1), vec3(BASE_X1, BASE_Y, UNDER_Z1), EARTH_FLOOR, LOOK_ONLY);
+    box(p, vec3(BASE_X0, -0.03f, BASE_Z0), vec3(BASE_X1, 0.0f, BASE_Z1), CONCRETE, LOOK_ONLY);
+    box(p, vec3(VEST_X1, -0.03f, BASE_Z1), vec3(BASE_X1, 0.0f, UNDER_Z1), CONCRETE, LOOK_ONLY);
+
+    /* The walls round it: old brick, earth behind */
+    p->brick = true;
+    box(p, vec3(BASE_X0 - BW, BASE_Y, BASE_Z0 - BW), vec3(BASE_X1 + BW, 0.0f, BASE_Z0), OLD_BRICK, SOLID);            /* back */
+    box(p, vec3(BASE_X0 - BW, BASE_Y, BASE_Z0), vec3(BASE_X0, 0.0f, BASE_Z1 + BW), OLD_BRICK, SOLID);                 /* left */
+    box(p, vec3(BASE_X1, BASE_Y, BASE_Z0), vec3(BASE_X1 + BW, 0.0f, UNDER_Z1 + BW), OLD_BRICK, SOLID);               /* right */
+    box(p, vec3(BASE_X0, BASE_Y, BASE_Z1), vec3(WELL_X0, 0.0f, BASE_Z1 + BW), OLD_BRICK, SOLID);                      /* under the entrance */
+    box(p, vec3(WELL_X0 - BW, BASE_Y, BASE_Z1 + BW), vec3(WELL_X0, 0.0f, UNDER_Z1 + BW), OLD_BRICK, SOLID);           /* beside the stairs */
+    box(p, vec3(WELL_X0, BASE_Y, UNDER_Z1), vec3(BASE_X1, 0.0f, UNDER_Z1 + BW), OLD_BRICK, SOLID);                    /* front */
+    /* Inside: loose brick walls, open (no doors) */
+    Gap side_way = { 2.5f, 3.4f, BASE_Y, -0.35f };
+    wallZ(p, BASE_Z1 + 0.15f, UNDER_Z1, VEST_X1, TOILET_X0, BASE_Y, 0.0f, &side_way, 1, OLD_BRICK, SOLID);           /* the stairs | under the kitchen */
+    Gap through = { -3.6f, -2.6f, BASE_Y, -0.35f };
+    wallX(p, TOILET_X0, BASE_X1, BASE_Z1, BASE_Z1 + 0.15f, BASE_Y, 0.0f, &through, 1, OLD_BRICK, SOLID);
+    wallZ(p, BASE_Z0, -1.4f, -5.0f, -4.82f, BASE_Y, 0.0f, NULL, 0, OLD_BRICK, SOLID);                                  /* two rooms at the back */
+    wallX(p, BASE_X0, -7.6f, -1.6f, -1.42f, BASE_Y, -0.25f, NULL, 0, OLD_BRICK, SOLID);                               /* a nook on the left */
+    p->brick = false;
+    looseBricks(p, vec3(-5.0f, BASE_Y, BASE_Z0), vec3(-4.82f, 0.0f, -1.4f), 6);
+    looseBricks(p, vec3(BASE_X0, BASE_Y, -1.6f), vec3(-7.6f, -0.25f, -1.42f), 4);
+    looseBricks(p, vec3(TOILET_X0, BASE_Y, BASE_Z1), vec3(BASE_X1, 0.0f, BASE_Z1 + 0.15f), 5);
+    for (int i = 0; i < 9; i++) { /* fallen out */
+        float x = -8.8f + 8.0f * rnd(p), z = BASE_Z0 + 0.3f + 5.2f * rnd(p);
+        box(p, vec3(x, BASE_Y, z), vec3(x + 0.22f, BASE_Y + 0.07f, z + 0.1f), vec3_scale(OLD_BRICK, 0.85f + 0.3f * rnd(p)), LOOK_ONLY);
+    }
+
+    /* The stairs: from the landing in the entrance down towards the back */
+    for (int k = 0; k < STAIR_STEPS - 1; k++)
+        box(p, vec3(WELL_X0, BASE_Y, WELL_TOP_Z - STAIR_TREAD * (float)(k + 1)),
+            vec3(VEST_X1, -rise * (float)(k + 1), WELL_TOP_Z - STAIR_TREAD * (float)k), STEPS, SOLID);
+}
+
+/* What stands inside: the meeting table in the big room, the kitchen, shelves in the basement. */
 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 },
+    const float fl = FL, b = BASE_Y;
+    const Vec3 chair = { 0.45f, 0.9f, 0.45f }, shelf = { 1.2f, 1.8f, 0.4f };
+    struct { FurnitureKind kind; Vec3 at; Vec3 size; Vec3 face; } items[] = {
+        { FURN_TABLE,   { 9.5f, fl, 0.8f }, { 4.2f, 0.78f, 1.6f }, FACE_PZ },  /* the big table */
+        { FURN_CHAIR,   { 8.0f, fl, 2.15f }, chair, FACE_NZ },                  /* (all facing it) */
+        { FURN_CHAIR,   { 9.5f, fl, 2.15f }, chair, FACE_NZ },
+        { FURN_CHAIR,   { 11.0f, fl, 2.15f }, chair, FACE_NZ },
+        { FURN_CHAIR,   { 8.0f, fl, -0.55f }, chair, FACE_PZ },
+        { FURN_CHAIR,   { 9.5f, fl, -0.55f }, chair, FACE_PZ },
+        { FURN_CHAIR,   { 11.0f, fl, -0.55f }, chair, FACE_PZ },
+        { FURN_CHAIR,   { 6.85f, fl, 0.8f }, chair, FACE_PX },
+        { FURN_CHAIR,   { 12.15f, fl, 0.8f }, chair, FACE_NX },
+        { FURN_COUNTER, { -1.9f, fl, 4.4f }, { 3.0f, 0.9f, 0.6f }, FACE_NZ },   /* the kitchen: the counter along the front wall */
+        { FURN_STOVE,   { -0.45f, fl, 2.6f }, { 0.6f, 0.9f, 0.6f }, FACE_NX },  /* ... the stove against the wall to the big room */
+        { FURN_SHELF,   { -3.4f, fl, 2.3f }, shelf, FACE_PX },                  /* ... a shelf against the toilet's wall */
+        { FURN_TABLE,   { -1.9f, fl, 2.4f }, { 1.2f, 0.75f, 0.8f }, FACE_PZ },
+        { FURN_CHAIR,   { -1.9f, fl, 1.65f }, chair, FACE_PZ },
+        { FURN_CHAIR,   { -1.9f, fl, 3.15f }, chair, FACE_NZ },
+        { FURN_SHELF,   { -8.4f, b, BASE_Z0 + 0.2f }, shelf, FACE_PZ },        /* the basement */
+        { FURN_SHELF,   { -6.4f, b, BASE_Z0 + 0.2f }, shelf, FACE_PZ },
+        { FURN_SHELF,   { -3.6f, b, BASE_Z0 + 0.2f }, shelf, FACE_PZ },
+        { FURN_SHELF,   { -1.3f, b, BASE_Z0 + 0.2f }, shelf, FACE_PZ },
+        { FURN_SHELF,   { BASE_X0 + 0.2f, b, -3.2f }, shelf, FACE_PX },
+        { FURN_SHELF,   { -0.35f, b, -1.0f }, shelf, FACE_NX },
+        { FURN_SHELF,   { -2.0f, b, UNDER_Z1 - 0.2f }, shelf, FACE_NZ },
+        { FURN_SHELF,   { -4.0f, b, UNDER_Z1 - 0.2f }, shelf, FACE_NZ },
     };
     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);
+        Object *o = Object_createFurniture(items[i].kind, toWorld(p, items[i].at), items[i].size, p->yaw + yaw_facing(items[i].face));
         if (!o || !World_add(p->world, o)) p->ok = false;
     }
 }
 
+/* The ground goes down to the basement floor under it (rectangles: local min and max x, z). */
+static void digBasement(Plan *p) {
+    const float rects[2][4] = { { BASE_X0, BASE_Z0, BASE_X1, BASE_Z1 }, { WELL_X0, BASE_Z1, BASE_X1, UNDER_Z1 } };
+    for (int i = 0; i < 2; i++) {
+        Vec3 mid = toWorld(p, vec3((rects[i][0] + rects[i][2]) * 0.5f, 0.0f, (rects[i][1] + rects[i][3]) * 0.5f));
+        if (!Terrain_addPit(&p->world->terrain, mid, p->yaw, (rects[i][2] - rects[i][0]) * 0.5f, (rects[i][3] - rects[i][1]) * 0.5f,
+                            p->origin.y + BASE_Y))
+            p->ok = false;
+    }
+}
+
 /* ---------- Your unfinished building: a brick shell under a roof ---------- */
 
 #define SHELL_W     6.4f  /* along x */
@@ -327,14 +443,16 @@ bool Shell_build(World *world, Vec3 origin, float yaw) {
     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;
+    p->brick = true;
     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);
+    p->brick = false;
     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 };
+    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, false };
     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);
@@ -351,8 +469,11 @@ bool Manor_build(World *world, Vec3 origin, float yaw) {
     p->origin = origin;
     p->yaw = yaw;
     p->ok = true;
+    p->rng = 12345u;
     leftHalf(p);
     rightHalf(p);
+    basement(p);
+    digBasement(p);
     for (int i = 0; i < p->n_roofs; i++) roofSlabs(p, &p->roofs[i]);
     furnish(p);
     if (!p->ok) return false;
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 65225dc..75ffbfb 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -55,7 +55,8 @@ static void setupBuilding(Object *obj, BuildingSize b) {
 static TreeSize treeSize(const Object *obj) {
     return (TreeSize){ .trunk_height = obj->size.y, .trunk_radius = obj->variant ? TRUNK_RADIUS * 0.8f : TRUNK_RADIUS,
                        .leaf_layers = obj->branches > 0 ? obj->branches : 1, .leaf_radius = obj->size.x * 0.5f,
-                       .fruit = obj->variant == 1 };
+                       .fruit = obj->variant == 1, .cut = obj->trunk_cut,
+                       .seed = Shapes_treeSeed(obj->position.x, obj->position.z) };
 }
 
 static void setupTree(Object *obj, TreeSize t) {
@@ -131,8 +132,7 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         break;
 
     case OBJ_STUMP: {
-        unsigned seed = (unsigned)(int)(position.x * 811.0f) ^ ((unsigned)(int)(position.z * 677.0f) << 10);
-        obj->mesh      = Shapes_stump(size.x, size.y, seed);
+        obj->mesh      = Shapes_stump(size.x, size.y, Shapes_treeSeed(position.x, position.z)); /* (the tree's own foot) */
         obj->pivot     = centered;
         obj->is_static = true;
         addCollider(obj, vec3_zero(), vec3(size.x, size.y, size.x), vec3_zero());
@@ -517,6 +517,40 @@ Vec3 Object_colliderCenter(const Object *obj, int index) {
     return vec3_add(obj->position, obj->colliders[index].offset);
 }
 
+/* (slab test in the box's frame) */
+float Collider_ray(const Collider *c, Vec3 center, Vec3 origin, Vec3 dir, Vec3 *normal) {
+    float t_enter = -1e9f, t_exit = 1e9f, enter_sign = 1.0f;
+    int enter_axis = -1;
+    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
+    Vec3 rel = vec3_sub(origin, center);
+    for (int k = 0; k < 3; k++) {
+        float o = vec3_dot(rel, c->axes[k]), d = vec3_dot(dir, c->axes[k]);
+        if (fabsf(d) < 1e-6f) {
+            if (fabsf(o) > half[k]) return -1.0f;
+            continue;
+        }
+        float t1 = (-half[k] - o) / d, t2 = (half[k] - o) / d;
+        float sign = -1.0f; /* entering through the -half face */
+        if (t1 > t2) { float t = t1; t1 = t2; t2 = t; sign = 1.0f; }
+        if (t1 > t_enter) { t_enter = t1; enter_axis = k; enter_sign = sign; }
+        t_exit = fminf(t_exit, t2);
+    }
+    if (t_enter > t_exit || t_enter < 0.0f || enter_axis < 0) return -1.0f;
+    if (normal) *normal = vec3_scale(c->axes[enter_axis], enter_sign);
+    return t_enter;
+}
+
+float Object_rayToCollider(const Object *obj, int index, Vec3 origin, Vec3 dir, Vec3 *normal) {
+    return Collider_ray(&obj->colliders[index], Object_colliderCenter(obj, index), origin, dir, normal);
+}
+
+float Object_reach(const Object *obj) {
+    float r = 0.0f;
+    for (int i = 0; i < obj->collider_count; i++)
+        r = fmaxf(r, vec3_length(obj->colliders[i].offset) + vec3_length(obj->colliders[i].half_extents));
+    return r;
+}
+
 /* Worn down by the day: how sleepy, felt more by the sluggish, the unfit and
    the drunk, hardly by the lively and sporty (0 = fresh .. 1 = done in). */
 float Object_tiredness(const Object *obj) {
@@ -597,7 +631,7 @@ void Object_swingGate(Object *g, float dt) {
     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) */
+    g->rotation.y = yaw_along(dir); /* (local x along the leaf) */
     Object_syncColliders(g);
 }
 
@@ -613,10 +647,38 @@ void Object_useToilet(Object *who, Object *toilet) {
 }
 
 #define SEAT_BACK 0.2f /* the hips sit this far behind where the feet are (see body.c) */
+#define CHAIR_PULL  0.38f /* m a chair is pulled out to sit on it */
+#define CHAIR_SPEED 0.9f  /* m/s it slides */
+#define CHAIR_GRAB  0.9f  /* m: within this a free hand is on its backrest while it slides */
+
+void Object_slideChair(Object *chair, float dt) {
+    if (!chair->tucked_set) return;
+    chair->chair_out += clampf(chair->chair_goal - chair->chair_out, -CHAIR_SPEED * dt, CHAIR_SPEED * dt);
+    Vec3 front = chair->colliders[0].axes[2];
+    front = vec3_normalize(vec3(front.x, 0.0f, front.z));
+    Vec3 at = vec3_sub(chair->tucked, vec3_scale(front, chair->chair_out));
+    chair->position = vec3(at.x, chair->position.y, at.z);
+    Object_syncColliders(chair);
+}
+
+/* While a chair slides for who, a free hand holds its backrest. */
+static void handOnChair(Object *who, const Object *chair) {
+    if (chair->type != OBJ_FURNITURE || chair->variant != FURN_CHAIR || fabsf(chair->chair_goal - chair->chair_out) < 1e-3f) return;
+    Vec3 front = chair->colliders[0].axes[2];
+    Vec3 grip = vec3_add(chair->position, vec3_add(vec3_scale(front, -chair->size.z * 0.5f), vec3(0.0f, chair->size.y - 0.04f, 0.0f)));
+    if (vec3_length(vec3_sub(vec3(grip.x, who->position.y, grip.z), who->position)) > CHAIR_GRAB) return;
+    for (int h = 0; h < 2; h++) {
+        if (who->hands.item[h] || who->body.hand_active[h]) continue;
+        who->body.hand_active[h] = true;
+        who->body.hand_target[h] = grip;
+        return; /* (one hand does it) */
+    }
+}
 
 void Object_sit(Object *who, Object *seat, Vec3 toward) {
     who->sitting = true;
     who->seat = seat;
+    who->body.crossing = false; /* (not stepping over it: onto it) */
     who->sit_style = SIT_GROUND;
     who->body.seat_height = SEAT_HEIGHT;
     if (!seat) return;
@@ -641,8 +703,14 @@ void Object_sit(Object *who, Object *seat, Vec3 toward) {
     Vec3 to = vec3_sub(toward, seat->position);
     if (seat->variant == FURN_CHAIR || vec3_dot(n, to) < 0.0f) n = seat->variant == FURN_CHAIR ? n : vec3_scale(n, -1.0f);
     n = vec3_normalize(vec3(n.x, 0.0f, n.z));
+    Vec3 base = seat->position;
+    if (seat->variant == FURN_CHAIR) { /* a chair is pulled out first (it may stand at a table): room in front of it */
+        if (!seat->tucked_set) { seat->tucked = seat->position; seat->tucked_set = true; }
+        seat->chair_goal = CHAIR_PULL;
+        base = vec3_sub(seat->tucked, vec3_scale(n, CHAIR_PULL));
+    }
     /* the bottom in the middle of the seat (a chair: a little back, against the backrest) */
-    Vec3 at = vec3_add(seat->position, vec3_scale(n, SEAT_BACK + (seat->variant == FURN_CHAIR ? -0.04f : 0.0f)));
+    Vec3 at = vec3_add(base, vec3_scale(n, SEAT_BACK + (seat->variant == FURN_CHAIR ? -0.04f : 0.0f)));
     who->sit_at = vec3(at.x, who->position.y, at.z);
     who->sit_yaw = atan2f(n.z, n.x) + PI_F / 2.0f; /* facing n */
     who->sit_dir = n;
@@ -668,7 +736,11 @@ void Object_leaveSeat(Object *who, float dt) {
         clear = fabsf(vec3_dot(rel, col->axes[0])) > col->half_extents.x + 0.3f ||
                 fabsf(vec3_dot(rel, col->axes[2])) > col->half_extents.z + 0.3f;
     }
-    if (clear) { who->seat = NULL; return; }
+    if (clear) {
+        if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR) seat->chair_goal = 0.0f; /* pushed in again (see world.c) */
+        who->seat = NULL;
+        return;
+    }
     /* ... and standing up you step forward off it */
     Vec3 front = vec3_add(who->sit_at, vec3_scale(who->sit_dir, SETTLE_FRONT + 0.1f));
     Vec3 to = vec3(front.x - who->position.x, 0.0f, front.z - who->position.z);
@@ -677,6 +749,7 @@ void Object_leaveSeat(Object *who, float dt) {
         who->position = vec3_add(who->position, vec3_scale(to, fminf(step, dist) / dist));
         Object_syncColliders(who);
     } else if (dist <= 0.02f || dist >= 1.0f) {
+        if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR) seat->chair_goal = 0.0f;
         who->seat = NULL; /* (stood up out of the way) */
     }
 }
@@ -684,6 +757,7 @@ void Object_leaveSeat(Object *who, float dt) {
 bool Object_settleOnSeat(Object *who, float dt) {
     if (!who->sitting || !who->sit_moving) return false;
     if (!who->seat) { who->sit_moving = false; return false; }
+    handOnChair(who, who->seat); /* (the world slides it: Object_slideChair) */
     /* first in front of it (feet clear of it), then turned round, a step back */
     Vec3 goal = who->sit_backing ? who->sit_at : vec3_add(who->sit_at, vec3_scale(who->sit_dir, SETTLE_FRONT));
     Vec3 to = vec3(goal.x - who->position.x, 0.0f, goal.z - who->position.z);
@@ -786,6 +860,7 @@ void Object_cutTrunk(Object *obj, float height) {
     height = clampf(height, 0.1f, obj->size.y - 0.5f);
     obj->size.y -= height;
     obj->position.y += height; /* the tree now starts at the cut */
+    obj->trunk_cut = true;
     rebuildTree(obj);
 }
 
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 55f3617..bd1a75e 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -212,6 +212,7 @@ typedef struct Object {
     bool       sit_moving;
     bool       sit_backing;   /* ... turned round, stepping back to the seat */
     float      rest_time;     /* seconds lying still (physics looks less often) */
+    bool       settled;       /* ... and turned to lie on the ground under it (see physics: settleOnGround) */
     bool       climbing;      /* characters: pulling themselves up a ledge (no physics meanwhile) */
     bool       climb_feet;    /* ... the feet go here meanwhile (world; set by whoever moves the climb) */
     Vec3       climb_ankle[2], climb_toe[2];
@@ -259,6 +260,10 @@ typedef struct Object {
     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) */
+    Vec3       tucked;        /* chairs: where it stands pushed in (set when first pulled out) ... */
+    bool       tucked_set;
+    bool       trunk_cut;     /* trees: felled (cut off its foot: a sawn end, no roots) */
+    float      chair_out, chair_goal; /* ... pulled back from there this far (m), and going to this far */
     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) ... */
@@ -324,6 +329,14 @@ Mat4    Object_modelMatrix(const Object *obj);
 /* Horizontal forward direction (characters face along it). */
 Vec3    Object_facing(const Object *obj);
 Vec3    Object_colliderCenter(const Object *obj, int index);
+/* Distance along a ray (dir: unit) to an oriented box collider centered at
+   center (world), and the face it enters through; -1 if it misses or starts inside. */
+float   Collider_ray(const Collider *c, Vec3 center, Vec3 origin, Vec3 dir, Vec3 *normal);
+/* The same for collider index of obj, and the face it
+   enters through (normal, towards the ray); -1 if it misses or starts inside. */
+float   Object_rayToCollider(const Object *obj, int index, Vec3 origin, Vec3 dir, Vec3 *normal);
+/* How far obj's colliders reach from its position at most (a bounding sphere). */
+float   Object_reach(const Object *obj);
 
 /* Characters: 1 when fresh, down to 0.5 when exhausted (speed, strength, work). */
 float   Object_vigor(const Object *obj);
@@ -356,6 +369,8 @@ void    Object_sit(Object *who, Object *seat, Vec3 toward);
 bool    Object_settleOnSeat(Object *who, float dt);
 /* Up again: forgets the seat once clear of it (no bumping into it meanwhile). */
 void    Object_leaveSeat(Object *who, float dt);
+/* A chair being pulled out or pushed in (towards chair_goal): slides along. */
+void    Object_slideChair(Object *chair, float dt);
 /* A seat (chair, bench) within reach of pos, or NULL. */
 Object *Object_seatNear(Object *const *objects, int count, Vec3 pos, float reach);
 /* Houses: the front door at x (meters along the ridge from the middle). */
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index 0590238..fbe5e19 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -219,6 +219,8 @@ static void slideAlong(Object *obj, Vec3 normal, float depth) {
  */
 /* 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 */
+#define SNAP_UP_MOST 0.22f /* m: a step up to this high is just taken (stairs); higher, the body steps up onto it */
+static void beginStepOver(Object *obj, const Terrain *terrain, Vec3 dir, float near, float far, float top, float land_at, bool up);
 
 static float rayDownInto(Vec3 origin, const Object *o, int ic) {
     const Collider *c = &o->colliders[ic];
@@ -260,6 +262,15 @@ static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 nor
     float rise   = top - bottom;
     if (rise <= 0.0f || rise > most) return false;
 
+    /* Higher than a stair (the side of a ramp, a crate): one step up onto it, not a jolt */
+    const Body *b = &obj->body;
+    const float s = obj->look.height / 1.75f;
+    if (rise > SNAP_UP_MOST * s && (obj->type == OBJ_PLAYER || obj->type == OBJ_HUMAN) && obj->over_t <= 0.0f &&
+        b->pose == POSE_UP && !b->airborne && b->stumble <= 0.0f && !obj->sitting && !obj->climbing && !obj->on_ladder &&
+        !obj->hands.grabbed) {
+        beginStepOver(obj, NULL, vec3_normalize(mv), 0.1f * s, STEP_PROBE + 0.3f, rise, STEP_PROBE, true);
+        return true;
+    }
     land(obj, rise);
     return true;
 }
@@ -390,6 +401,34 @@ static bool middleOverAny(const Object *o, Vec3 p, float margin) {
     return false;
 }
 
+/* Lying on a slope (a ramp, a roof): down it it goes once the slope beats the
+   grip - round things roll off early, the rest only on steeper ones, faster
+   the steeper. */
+#define GRIP_ROUND 0.12f
+#define GRIP_FLAT  0.5f
+static float g_step_dt = 1.0f / 60.0f;
+
+static bool rolls(const Object *o) {
+    switch (o->type) {
+    case OBJ_BOTTLE: case OBJ_VODKA: case OBJ_WATER: case OBJ_LOG: case OBJ_CYLINDER: case OBJ_APPLE: case OBJ_STONE:
+        return true;
+    default:
+        return false;
+    }
+}
+
+static void slideDown(Object *obj, Vec3 normal) {
+    float cos_a = clampf(normal.y, 0.0f, 1.0f), sin_a = sqrtf(fmaxf(0.0f, 1.0f - cos_a * cos_a));
+    float pull = GRAVITY * (sin_a - (rolls(obj) ? GRIP_ROUND : GRIP_FLAT) * cos_a);
+    if (pull <= 0.0f) return;
+    Vec3 down = vec3(normal.x * normal.y, normal.y * normal.y - 1.0f, normal.z * normal.y); /* (down the slope) */
+    if (vec3_length(down) < 1e-4f) return;
+    down = vec3_normalize(down);
+    float along = vec3_dot(obj->velocity, down);
+    float goal = fmaxf(along + pull * g_step_dt, 0.06f); /* (no resting on it: faster than "lying still") */
+    obj->velocity = vec3_add(obj->velocity, vec3_scale(down, goal - along));
+}
+
 static void resolveContact(Object *obj, int io, Object *other, int ic, Vec3 normal, float depth) {
     obj->touched = other;
     if (other->is_character && !obj->is_character && touchPerson(obj, other, normal, depth)) return;
@@ -408,6 +447,7 @@ static void resolveContact(Object *obj, int io, Object *other, int ic, Vec3 norm
     if (normal.y >= WALKABLE_SLOPE && !(obj->is_character && depth / normal.y > LAND_ON_MOST)) {
         land(obj, depth / normal.y);
         if (obj->is_character && !other->is_character) obj->standing_on = other;
+        if (!obj->is_character && !obj->lifted && normal.y < 0.995f) slideDown(obj, normal);
         return;
     }
     if (normal.y >= WALKABLE_SLOPE) {
@@ -513,8 +553,39 @@ static void neverThrough(Object *obj, Vec3 start, Object *const *objects, int co
     }
 }
 
-/* Keeps every collider above the terrain surface. */
+/* The eight corners of a collider. */
+static void corners(const Object *obj, int i, Vec3 out[8]) {
+    const Collider *c = &obj->colliders[i];
+    Vec3 center = Object_colliderCenter(obj, i);
+    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));
+        out[k] = p;
+    }
+}
+
+/* Keeps every collider above the terrain surface. Things: each corner over
+   the ground right under it (they rest on their real contact points, not on
+   the highest ground anywhere under them). People: their feet do the rest. */
 static void resolveTerrain(Object *obj, const Terrain *terrain) {
+    if (!obj->is_character) {
+        float lift = -1e9f;
+        for (int i = 0; i < obj->collider_count; i++) {
+            Vec3 p[8];
+            corners(obj, i, p);
+            for (int k = 0; k < 8; k++) lift = fmaxf(lift, Terrain_height(terrain, p[k].x, p[k].z) - p[k].y);
+        }
+        if (lift > 0.0f) {
+            land(obj, lift);
+            obj->on_terrain = true;
+        } else if (lift > -0.004f && obj->velocity.y <= 0.0f) {
+            obj->on_terrain = true; /* (resting right on it) */
+            obj->is_grounded = true;
+        }
+        return;
+    }
     for (int i = 0; i < obj->collider_count; i++) {
         const Collider *c = &obj->colliders[i];
         Vec3  center = Object_colliderCenter(obj, i);
@@ -536,6 +607,62 @@ static void resolveTerrain(Object *obj, const Terrain *terrain) {
     }
 }
 
+/* Lying on the ground, come to rest: tilted to the ground under it - its
+   bottom face along the slope there, down onto it - instead of lying level on
+   its highest point (one end in the air). */
+#define SETTLE_AFTER    0.3f  /* s at rest before it settles */
+#define SETTLE_STEEPEST 0.5f  /* rad: no steeper (it would slide or roll off) */
+#define SETTLE_SMALL    0.12f /* m: smaller things just lie as they are */
+
+static Vec3 rotateAbout(Vec3 v, Vec3 axis, float angle) {
+    float c = cosf(angle), s = sinf(angle);
+    return vec3_add(vec3_add(vec3_scale(v, c), vec3_scale(vec3_cross(axis, v), s)), vec3_scale(axis, vec3_dot(axis, v) * (1.0f - c)));
+}
+
+static void settleOnGround(Object *obj, const Terrain *terrain) {
+    if (obj->is_character || obj->type == OBJ_TREE || obj->type == OBJ_DOG || obj->collider_count == 0 || obj->lifted ||
+        obj->is_held || obj->thrown_by || (obj->topple > 0.0f && obj->topple < 1.0f) || obj->touched) return;
+    const Collider *c = &obj->colliders[0];
+    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
+    int k = 0;
+    for (int a = 1; a < 3; a++) if (fabsf(c->axes[a].y) > fabsf(c->axes[k].y)) k = a;
+    if (fabsf(c->axes[k].y) < 0.8f) return; /* (on an edge: as it is) */
+    int a1 = (k + 1) % 3, a2 = (k + 2) % 3;
+    if (half[a1] < SETTLE_SMALL && half[a2] < SETTLE_SMALL) return;
+    Vec3 up = vec3_scale(c->axes[k], c->axes[k].y > 0.0f ? 1.0f : -1.0f);
+    Vec3 base = vec3_sub(Object_colliderCenter(obj, 0), vec3_scale(up, half[k]));
+    Vec3 u = vec3_scale(c->axes[a1], half[a1]), v = vec3_scale(c->axes[a2], half[a2]), q[4];
+    q[0] = vec3_sub(vec3_sub(base, u), v);
+    q[1] = vec3_sub(vec3_add(base, u), v);
+    q[2] = vec3_add(vec3_add(base, u), v);
+    q[3] = vec3_add(vec3_sub(base, u), v);
+    for (int i = 0; i < 4; i++) q[i].y = Terrain_height(terrain, q[i].x, q[i].z);
+    Vec3 n = vec3_cross(vec3_sub(q[2], q[0]), vec3_sub(q[3], q[1]));
+    if (vec3_length(n) < 1e-6f) return;
+    n = vec3_normalize(n);
+    if (n.y < 0.0f) n = vec3_scale(n, -1.0f);
+    float angle = acosf(clampf(vec3_dot(up, n), -1.0f, 1.0f));
+    if (angle > 1e-3f) {
+        Vec3 axis = vec3_cross(up, n);
+        if (vec3_length(axis) < 1e-6f) return;
+        axis = vec3_normalize(axis);
+        float tilt = fminf(angle, SETTLE_STEEPEST);
+        Mat4 r = mat4_rotate_yxz(obj->rotation);
+        Vec3 y = rotateAbout(mat4_mul_dir(r, vec3(0, 1, 0)), axis, tilt), z = rotateAbout(mat4_mul_dir(r, vec3(0, 0, 1)), axis, tilt);
+        obj->rotation = euler_yxz_from_axes(vec3_normalize(y), vec3_normalize(z));
+        Object_syncColliders(obj);
+    }
+    /* down (or up) onto it: the lowest gap closed */
+    float lift = -1e9f;
+    for (int i = 0; i < obj->collider_count; i++) {
+        Vec3 p[8];
+        corners(obj, i, p);
+        for (int j = 0; j < 8; j++) lift = fmaxf(lift, Terrain_height(terrain, p[j].x, p[j].z) - p[j].y);
+    }
+    obj->position.y += lift;
+    Object_syncColliders(obj);
+}
+
 static void snapToTerrain(Object *obj, const Terrain *terrain) {
     const Collider *c = &obj->colliders[0];
     Vec3 center = Object_colliderCenter(obj, 0);
@@ -584,7 +711,7 @@ static Vec3 slopeAdjusted(const Object *obj, const Terrain *terrain) {
    over it, after the foot that went over. Walking back takes you back. */
 static Vec3 carryOver(const Object *obj, Vec3 m) {
     const Body *b = &obj->body;
-    if (!b->crossing || Body_isBusy(b)) return m;
+    if (!b->crossing || Body_isBusy(b) || obj->sitting) return m; /* (sitting down: over the seat on purpose) */
     float speed = sqrtf(m.x * m.x + m.z * m.z);
     if (!b->cross_force && speed > 0.3f) return m; /* (walking on, or back: as you like) */
     const float s = obj->look.height / 1.75f;
@@ -729,6 +856,37 @@ static bool pointIn(const Object *o, Vec3 p, float margin) {
 
 /* Walking (not running) up to something knee high to step over, a step away:
    the set move starts. Not if where the feet would land is taken. */
+/* Starts the set move over something (up: onto it) along dir: near and far
+   its edges, top its height (from the feet), land_at where the leading foot comes down. */
+static void beginStepOver(Object *obj, const Terrain *terrain, Vec3 dir, float near, float far, float top, float land_at, bool up) {
+    const Body *b = &obj->body;
+    const float s = obj->look.height / 1.75f;
+    const Vec3 right = vec3(-dir.z, 0.0f, dir.x);
+    obj->over_t = 1e-4f;
+    obj->over_up = up;
+    obj->over_time = (up ? STAND_UP_TIME + STAND_UP_HIGH * top / s /* (onto something low: one step up) */
+                                                        : OVER_MOVE_TIME + OVER_MOVE_HIGH * top / s) * (1.25f - 0.5f * obj->fitness) / fmaxf(Object_vigor(obj), 0.6f);
+    obj->over_from = obj->position;
+    obj->over_dir = dir;
+    obj->over_near = near;
+    obj->over_far = far;
+    obj->over_top = top;
+    /* the foot further back goes first (the other stands) */
+    float a0 = vec3_dot(vec3_sub(b->feet[0].ankle, obj->position), dir), a1 = vec3_dot(vec3_sub(b->feet[1].ankle, obj->position), dir);
+    obj->over_lead = a0 <= a1 ? 0 : 1;
+    for (int k = 0; k < 2; k++) {
+        obj->over_foot0[k] = b->feet[k].ankle;
+        float lateral = clampf(vec3_dot(vec3_sub(b->feet[k].ankle, obj->position), right), -0.14f * s, 0.14f * s);
+        if (fabsf(lateral) < 0.08f * s) lateral = (k == 0 ? -0.1f : 0.1f) * s; /* (feet apart) */
+        float at = k == obj->over_lead ? land_at : land_at + 0.08f * s;
+        Vec3 land = vec3_add(obj->position, vec3_add(vec3_scale(dir, at), vec3_scale(right, lateral)));
+        land.y = up || !terrain ? obj->position.y + top + (b->feet[k].ankle.y - obj->position.y)
+                    : Terrain_height(terrain, land.x, land.z) + (b->feet[k].ankle.y - Terrain_height(terrain, b->feet[k].ankle.x, b->feet[k].ankle.z));
+        obj->over_land[k] = land;
+    }
+    obj->movement = vec3_zero();
+}
+
 static bool startStepOver(Object *obj, Object *const *objects, int count, const Terrain *terrain) {
     float speed = sqrtf(obj->movement.x * obj->movement.x + obj->movement.z * obj->movement.z);
     const Body *b = &obj->body;
@@ -773,28 +931,7 @@ static bool startStepOver(Object *obj, Object *const *objects, int count, const
                     objects[j]->type != OBJ_PUDDLE && objects[j]->type != OBJ_STINK && pointIn(objects[j], p, 0.08f * s)) free = false;
         }
         if (!free) continue;
-        obj->over_t = 1e-4f;
-        obj->over_up = up;
-        obj->over_time = (up ? STAND_UP_TIME + STAND_UP_HIGH * top / s /* (onto something low: one step up) */
-                                                            : OVER_MOVE_TIME + OVER_MOVE_HIGH * top / s) * (1.25f - 0.5f * obj->fitness) / fmaxf(Object_vigor(obj), 0.6f);
-        obj->over_from = obj->position;
-        obj->over_dir = dir;
-        obj->over_near = near;
-        obj->over_far = far;
-        obj->over_top = top;
-        /* the foot further back goes first (the other stands) */
-        float a0 = vec3_dot(vec3_sub(b->feet[0].ankle, obj->position), dir), a1 = vec3_dot(vec3_sub(b->feet[1].ankle, obj->position), dir);
-        obj->over_lead = a0 <= a1 ? 0 : 1;
-        for (int k = 0; k < 2; k++) {
-            obj->over_foot0[k] = b->feet[k].ankle;
-            float lateral = clampf(vec3_dot(vec3_sub(b->feet[k].ankle, obj->position), right), -0.14f * s, 0.14f * s);
-            if (fabsf(lateral) < 0.08f * s) lateral = (k == 0 ? -0.1f : 0.1f) * s; /* (feet apart) */
-            float at = k == obj->over_lead ? land_at : land_at + 0.08f * s;
-            Vec3 land = vec3_add(obj->position, vec3_add(vec3_scale(dir, at), vec3_scale(right, lateral)));
-            land.y = up ? obj->position.y + top + (b->feet[k].ankle.y - obj->position.y)
-                        : Terrain_height(terrain, land.x, land.z) + (b->feet[k].ankle.y - Terrain_height(terrain, b->feet[k].ankle.x, b->feet[k].ankle.z));
-            obj->over_land[k] = land;
-        }
+        beginStepOver(obj, terrain, dir, near, far, top, land_at, up);
         obj->movement = vec3_zero();
         return true;
     }
@@ -907,19 +1044,26 @@ void Physics_useGrid(struct Grid *grid) { g_grid = grid; }
 
 void Physics_step(Object *obj, Object *const *objects, int count, const Terrain *terrain, float dt) {
     if (obj->is_static || obj->is_held) return;
+    g_step_dt = dt;
     if (obj->over_t > 0.0f) { stepOverMove(obj, terrain, dt); return; } /* (a set move) */
 
     /* At rest: skip most steps (a push or a fall wakes it) */
     if (obj->climbing || obj->on_grill || obj->toilet) return; /* moved by hand, or out of sight */
+    if (obj->sitting && obj->sit_moving && obj->seat) return; /* (stepping over to a seat, turning, back onto it: a set move) */
     bool still = !obj->is_character && !obj->lifted && !obj->thrown_by && obj->is_grounded &&
                  vec3_length(obj->movement) < 0.01f &&
                  vec3_length(obj->velocity) < REST_SPEED && fabsf(obj->angular_velocity.y) < 0.05f &&
                  (obj->topple <= 0.0f || obj->topple >= 1.0f);
     if (!still) {
         obj->rest_time = 0.0f;
+        obj->settled = false;
     } else {
         float before = obj->rest_time;
         obj->rest_time += dt;
+        if (!obj->settled && obj->rest_time > SETTLE_AFTER && obj->on_terrain) {
+            obj->settled = true;
+            settleOnGround(obj, terrain);
+        }
         if (before > REST_AFTER && floorf(before / REST_CHECK) == floorf(obj->rest_time / REST_CHECK)) return;
     }
 
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index 2acb32b..bc47b16 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -29,10 +29,6 @@ static Vec3 mapDir(float degrees) {
 }
 
 /* Turn about Y that brings an object's local +X onto dir (objects turn +X towards +Z). */
-static float yawFor(Vec3 dir) {
-    return atan2f(dir.z, dir.x);
-}
-
 static bool insidePolygon(const Px *poly, int n, Px p) {
     bool in = false;
     for (int i = 0, j = n - 1; i < n; j = i++) {
@@ -758,7 +754,7 @@ static bool furnishGarages(World *world, Vec3 origin, float yaw, float zsign) {
 static bool courtBox(World *world, Palette look, float a, float b, float y, Vec3 size) {
     Vec3 p = court(a, b);
     p.y = y;
-    return addStatic(world, OBJ_BOX_STATIC, look, p, size, yawFor(mapDir(COURT_ANGLE)));
+    return addStatic(world, OBJ_BOX_STATIC, look, p, size, yaw_along(mapDir(COURT_ANGLE)));
 }
 
 /* Something loose, dropped a little above the ground (it falls into place). */
@@ -844,7 +840,7 @@ static bool furnishYard(World *world) {
     Vec3 fire = court(FIRE_A, FIRE_B);
     if (!addStatic(world, OBJ_FIREPLACE, PALETTE_WARM, fire, vec3(FIRE_SIZE, 0.3f, FIRE_SIZE), 0.0f) ||
         !addStatic(world, OBJ_SUPPLY, PALETTE_STANDARD, court(FIRE_A - 3.1f, FIRE_B + 2.6f), vec3(0.6f, 0.45f, 0.4f), 0.3f) ||
-        !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_BIN, PALETTE_STANDARD, court(15.0f, 20.0f), vec3(1.8f, 1.4f, 1.2f), yaw_along(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) */
@@ -872,7 +868,7 @@ static bool furnishYard(World *world) {
         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)));
         Object *bench = Object_createFurniture(FURN_BENCH, court(FIRE_A + BENCH_RADIUS * cosf(a), FIRE_B + BENCH_RADIUS * sinf(a)),
-                                               vec3(1.8f, SEAT_HEIGHT, 0.36f), yawFor(tangent));
+                                               vec3(1.8f, SEAT_HEIGHT, 0.36f), yaw_along(tangent));
         if (!bench || !World_add(world, bench)) return false;
     }
     { /* an old tin as ashtray, on the cool box by the fire (on the ground it was in everybody's way) */
@@ -959,7 +955,7 @@ static bool dressBarn(World *world, Vec3 center, float yaw) {
 
 bool Site_build(World *world) {
     /* The courtyard */
-    const float along = yawFor(mapDir(COURT_ANGLE)), across = yawFor(mapDir(COURT_ANGLE - 90.0f));
+    const float along = yaw_along(mapDir(COURT_ANGLE)), across = yaw_along(mapDir(COURT_ANGLE - 90.0f));
     for (int i = 0; i < COUNT(COURT); i++) {
         const CourtDoorBuilding *c = &COURT[i];
         Object *o = Object_create(c->type, c->look, court(c->a, c->b), c->size, 100.0f);
@@ -978,7 +974,7 @@ bool Site_build(World *world) {
     Vec3 front = vec3(dir.z, 0.0f, -dir.x);
     if (vec3_dot(front, vec3_sub(Site_yard(), walled)) < 0.0f) front = vec3_scale(front, -1.0f);
     /* The small tool room goes next to the main building: mirror the walls if needed */
-    const float wyaw = yawFor(vec3_scale(front, -1.0f));
+    const float wyaw = yaw_along(vec3_scale(front, -1.0f));
     float zsign = courtA(placed(vec3(23.25f, 0.0f, 12.0f), WALLED_CENTER, walled, wyaw, 1.0f)) <
                   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) ||
@@ -989,7 +985,7 @@ bool Site_build(World *world) {
     if (!buildGarages(world) || !plantOrchard(world) || !furnishYard(world)) return false;
 
     /* The tap left of the main door, the garden hose and a watering can; a toilet by the barn */
-    const float across_yaw = yawFor(mapDir(COURT_ANGLE - 90.0f));
+    const float across_yaw = yaw_along(mapDir(COURT_ANGLE - 90.0f));
     Object *tap = Object_createFurniture(FURN_TAP, court(6.15f, -9.75f), vec3(0.3f, 1.0f, 0.3f), across_yaw);
     if (!tap || !World_add(world, tap)) return false;
     Object *hose = Object_create(OBJ_HOSE, PALETTE_WARM, vec3_add(court(7.0f, -9.4f), vec3(0.0f, 0.1f, 0.0f)), vec3_zero(), 0.6f);
@@ -1000,12 +996,12 @@ bool Site_build(World *world) {
     Object *can = Object_create(OBJ_CAN, PALETTE_WARM, vec3_add(court(6.6f, -10.1f), vec3(0.0f, 0.2f, 0.0f)), vec3_zero(), 1.5f);
     if (!can || !World_add(world, can)) return false;
     Object *toilet = Object_createFurniture(FURN_TOILET, court(36.8f, -4.3f), vec3(1.1f, 2.3f, 1.1f),
-                                            yawFor(mapDir(COURT_ANGLE + 90.0f)));
+                                            yaw_along(mapDir(COURT_ANGLE + 90.0f)));
     if (!toilet || !World_add(world, toilet)) return false;
     Object *urinellas = Object_create(OBJ_SUPPLY, PALETTE_GROUND, court(37.75f, -4.6f), vec3(0.3f, 0.18f, 0.2f), 100.0f);
     if (!urinellas) return false;
     urinellas->dispenses = OBJ_URINELLA; /* (a little carton beside the toilet) */
-    Object_setRotation(urinellas, vec3(0.0f, yawFor(mapDir(COURT_ANGLE + 90.0f)), 0.0f));
+    Object_setRotation(urinellas, vec3(0.0f, yaw_along(mapDir(COURT_ANGLE + 90.0f)), 0.0f));
     if (!World_add(world, urinellas)) return false;
 
     /* Alice, the dachshund */
@@ -1017,13 +1013,13 @@ bool Site_build(World *world) {
     /* Family houses, and the farm */
     for (int i = 0; i < COUNT(HOUSES); i++) {
         const Building *h = &HOUSES[i];
-        if (!addStatic(world, h->type, h->look, at(h->center), h->size, yawFor(mapDir(h->angle))))
+        if (!addStatic(world, h->type, h->look, at(h->center), h->size, yaw_along(mapDir(h->angle))))
             return false;
     }
     for (int i = 0; i < COUNT(HALLS); i++) {
         const Hall *h = &HALLS[i];
         if (!addStatic(world, OBJ_BARN, h->look, at(h->center), vec3(h->length, HALL_WALL, h->width),
-                       yawFor(mapDir(h->angle))))
+                       yaw_along(mapDir(h->angle))))
             return false;
     }
 
@@ -1045,7 +1041,7 @@ bool Site_build(World *world) {
     Vec3 stop = at(BUS_STOP_AT);
     if (!addStatic(world, OBJ_CYLINDER_STATIC, PALETTE_STANDARD, stop, vec3(0.08f, 2.4f, 0.08f), 0.0f) ||
         !addStatic(world, OBJ_BOX_STATIC, PALETTE_MODERN, vec3_add(stop, vec3(0.0f, 2.0f, 0.0f)),
-                   vec3(0.05f, 0.45f, 0.45f), yawFor(mapDir(-52.0f))))
+                   vec3(0.05f, 0.45f, 0.45f), yaw_along(mapDir(-52.0f))))
         return false;
 
     return plantForests(world);
diff --git a/entropie/src/world/terrain.c b/entropie/src/world/terrain.c
index 9eaade2..922f9b6 100644
--- a/entropie/src/world/terrain.c
+++ b/entropie/src/world/terrain.c
@@ -248,8 +248,32 @@ float Terrain_hardness(const Terrain *t, float x, float z) {
 
 /* ---------- Public ---------- */
 
+bool Terrain_addPit(Terrain *t, Vec3 center, float yaw, float half_x, float half_z, float floor_y) {
+    if (t->pit_count >= TERRAIN_PITS) return false;
+    t->pits[t->pit_count++] = (TerrainPit){ center.x, center.z, cosf(yaw), sinf(yaw), half_x, half_z, floor_y };
+    return true;
+}
+
+bool Terrain_inPit(const Terrain *t, float x, float z, float *floor_y) {
+    for (int i = 0; i < t->pit_count; i++) {
+        const TerrainPit *p = &t->pits[i];
+        float dx = x - p->x, dz = z - p->z;
+        float u = dx * p->cos_yaw + dz * p->sin_yaw, v = -dx * p->sin_yaw + dz * p->cos_yaw;
+        if (fabsf(u) <= p->half_x && fabsf(v) <= p->half_z) {
+            if (floor_y) *floor_y = p->floor;
+            return true;
+        }
+    }
+    return false;
+}
+
 bool Terrain_init(Terrain *t, Object *const *objects, int count) {
+    TerrainPit pits[TERRAIN_PITS]; /* (dug before: kept) */
+    const int pit_count = t->pit_count;
+    memcpy(pits, t->pits, sizeof(pits));
     memset(t, 0, sizeof(*t));
+    memcpy(t->pits, pits, sizeof(pits));
+    t->pit_count = pit_count;
     const size_t grid_n = (size_t)(TERRAIN_GRID + 1) * (TERRAIN_GRID + 1);
     const size_t wear_n = (size_t)TERRAIN_WEAR_RES * TERRAIN_WEAR_RES;
 
@@ -325,6 +349,8 @@ static float wearAt(const Terrain *t, float x, float z) {
 
 float Terrain_height(const Terrain *t, float x, float z) {
     if (x < -HALF || x > HALF || z < -HALF || z > HALF) return OUTSIDE_HEIGHT;
+    float pit;
+    if (t->pit_count && Terrain_inPit(t, x, z, &pit)) return pit;
 
     float gx = fminf((x + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f);
     float gz = fminf((z + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f);
diff --git a/entropie/src/world/terrain.h b/entropie/src/world/terrain.h
index c7c8e66..73c249d 100644
--- a/entropie/src/world/terrain.h
+++ b/entropie/src/world/terrain.h
@@ -27,6 +27,15 @@
 #define TERRAIN_TILES         8       /* the ground mesh is cut into TILES x TILES pieces */
 #define TERRAIN_SURFACE_RES   2048    /* surface map cells per side (1/6 m) */
 
+/* A hole in the ground (a basement under a building): no ground drawn there,
+   and the ground's height in it is its floor. A rectangle turned by yaw. */
+#define TERRAIN_PITS 4
+typedef struct {
+    float x, z, cos_yaw, sin_yaw; /* its middle; local x is (cos, sin) */
+    float half_x, half_z;
+    float floor;
+} TerrainPit;
+
 typedef struct Terrain {
     float         *heights;    /* (TERRAIN_GRID + 1)^2 base heights */
     float         *wear;       /* TERRAIN_WEAR_RES^2, 0 = meadow, 1 = bare path */
@@ -37,8 +46,16 @@ typedef struct Terrain {
     GLuint         surface_texture; /* RGBA8: asphalt, concrete, gravel, bare earth (see Site_surface) */
     Mesh           tiles[TERRAIN_TILES * TERRAIN_TILES]; /* the ground, in tiles (drawn only in view) */
     Wetness        wet;
+    TerrainPit     pits[TERRAIN_PITS];
+    int            pit_count;
 } Terrain;
 
+/* A hole in the ground (see TerrainPit; yaw as objects turn). May be dug
+   before Terrain_init (buildings come first): it keeps them. */
+bool  Terrain_addPit(Terrain *t, Vec3 center, float yaw, float half_x, float half_z, float floor_y);
+/* In a pit here? (its floor, if so) */
+bool  Terrain_inPit(const Terrain *t, float x, float z, float *floor_y);
+
 /* Builds the ground and flattens it around the static objects given. */
 bool  Terrain_init(Terrain *t, Object *const *objects, int count);
 void  Terrain_destroy(Terrain *t);
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 4bdac57..be54359 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -27,25 +27,62 @@
 #define SCRAPE_COOL       2.5f  /* seconds before the next one counts */
 
 /* Distance along a downward ray from origin to an oriented box, or -1 if missed
-   (also when the ray starts inside the box). Slab test in the box's frame. */
+   (also when the ray starts inside the box). */
 static float rayDownToBox(Vec3 origin, Vec3 center, const Collider *c) {
-    float t_enter = -1e9f, t_exit = 1e9f;
-    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
-    Vec3 rel = vec3_sub(origin, center);
-    for (int k = 0; k < 3; k++) {
-        float o = vec3_dot(rel, c->axes[k]);
-        float d = -c->axes[k].y; /* ray direction (0,-1,0) in box space */
-        if (fabsf(d) < 1e-6f) {
-            if (fabsf(o) > half[k]) return -1.0f;
-            continue;
+    return Collider_ray(c, center, origin, vec3(0.0f, -1.0f, 0.0f), NULL);
+}
+
+float World_rayCast(const World *world, Vec3 origin, Vec3 dir, float max, RaySkip skip, const void *ctx,
+                    Object **hit, Vec3 *normal) {
+    float best = -1.0f;
+    for (int i = 0; i < world->count; i++) {
+        Object *o = world->objects[i];
+        if (o->collider_count == 0) continue;
+        if (skip ? skip(o, ctx) : (o->is_character || o->is_held)) continue;
+        /* (far off the ray: not worth the boxes) */
+        Vec3 rel = vec3_sub(o->position, origin);
+        float along = clampf(vec3_dot(rel, dir), 0.0f, max), reach = Object_reach(o);
+        Vec3 off = vec3_sub(rel, vec3_scale(dir, along));
+        if (vec3_dot(off, off) > reach * reach) continue;
+        for (int c = 0; c < o->collider_count; c++) {
+            Vec3 n;
+            float t = Object_rayToCollider(o, c, origin, dir, &n);
+            if (t >= 0.0f && t <= max && (best < 0.0f || t < best)) {
+                best = t;
+                if (hit) *hit = o;
+                if (normal) *normal = n;
+            }
         }
-        float t1 = (-half[k] - o) / d, t2 = (half[k] - o) / d;
-        if (t1 > t2) { float t = t1; t1 = t2; t2 = t; }
-        t_enter = fmaxf(t_enter, t1);
-        t_exit  = fminf(t_exit, t2);
     }
-    if (t_enter > t_exit || t_enter < 0.0f) return -1.0f;
-    return t_enter;
+    return best;
+}
+
+/* The same for a round thing lying (a log): its round top, not the box round it.
+   axis: the collider axis it lies along (k), radius across. */
+static float rayDownToRound(Vec3 origin, Vec3 center, const Collider *c, int k, float radius) {
+    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
+    Vec3 a = c->axes[k], w0 = vec3_sub(origin, center);
+    Vec3 d = vec3_sub(vec3(0.0f, 1.0f, 0.0f), vec3_scale(a, a.y)); /* (how the ray's going down moves it across the axis) */
+    Vec3 p0 = vec3_sub(w0, vec3_scale(a, vec3_dot(w0, a)));
+    float qa = vec3_dot(d, d), qb = -2.0f * vec3_dot(p0, d), qc = vec3_dot(p0, p0) - radius * radius;
+    if (qa < 1e-6f) return rayDownToBox(origin, center, c); /* (standing upright) */
+    float disc = qb * qb - 4.0f * qa * qc;
+    if (disc < 0.0f) return -1.0f;
+    float t = (-qb - sqrtf(disc)) / (2.0f * qa);
+    if (t < 0.0f) return -1.0f;
+    float along = vec3_dot(vec3_sub(w0, vec3(0.0f, t, 0.0f)), a);
+    return fabsf(along) <= half[k] ? t : -1.0f;
+}
+
+static bool roundLying(const Object *o, int *k) {
+    if (o->type != OBJ_LOG && o->type != OBJ_CYLINDER) return false;
+    const Collider *c = &o->colliders[0];
+    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
+    int along = 0;
+    for (int i = 1; i < 3; i++) if (half[i] > half[along]) along = i;
+    if (fabsf(c->axes[along].y) > 0.7f) return false; /* (standing up: flat top) */
+    *k = along;
+    return true;
 }
 
 /* Things whose footprint comes within r of (x, z): from the cell grid, or
@@ -67,7 +104,9 @@ float World_groundHeight(const World *world, float x, float z, float from_y) {
             Vec3 center = Object_colliderCenter(o, c);
             float reach = vec3_length(col->half_extents); /* nowhere near: skip */
             if (fabsf(center.x - x) > reach || fabsf(center.z - z) > reach) continue;
-            float t = rayDownToBox(origin, center, col);
+            int along;
+            float t = c == 0 && roundLying(o, &along) ? rayDownToRound(origin, center, col, along, o->size.x * 0.5f)
+                                                      : rayDownToBox(origin, center, col);
             if (t >= 0.0f) best = fmaxf(best, from_y - t);
         }
     }
@@ -685,6 +724,18 @@ static void dressForNight(World *world, Object *o, float dt) {
     if (o->chill < 0.3f) o->jacket_told = false;
 }
 
+/* A chair waits to be pushed in while somebody stands where it goes. */
+static bool chairWaits(const World *world, const Object *chair) {
+    if (chair->chair_goal >= chair->chair_out) return false;
+    for (int i = 0; i < world->count; i++) {
+        const Object *o = world->objects[i];
+        if (!o->is_character || o->seat == chair) continue;
+        float dx = o->position.x - chair->tucked.x, dz = o->position.z - chair->tucked.z;
+        if (dx * dx + dz * dz < 0.6f * 0.6f) return true;
+    }
+    return 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) {
@@ -692,6 +743,7 @@ static void showProps(World *world) {
         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_CHAIR) { if (f->chair_out != f->chair_goal && !chairWaits(world, f)) Object_slideChair(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;
@@ -1238,6 +1290,15 @@ static void drawMark(Renderer *renderer, const Mesh *block, const Object *o) {
 
 void World_draw(World *world, Renderer *renderer, bool draw_player) {
     Terrain_syncTexture(&world->terrain);
+    {
+        float holes[TERRAIN_PITS][6];
+        for (int i = 0; i < world->terrain.pit_count; i++) {
+            const TerrainPit *p = &world->terrain.pits[i];
+            holes[i][0] = p->x; holes[i][1] = p->z; holes[i][2] = p->cos_yaw; holes[i][3] = p->sin_yaw;
+            holes[i][4] = p->half_x; holes[i][5] = p->half_z;
+        }
+        Renderer_setHoles(renderer, (const float (*)[6])holes, world->terrain.pit_count);
+    }
     for (int i = 0; i < TERRAIN_TILES * TERRAIN_TILES; i++)
         Renderer_drawTerrain(renderer, &world->terrain.tiles[i], world->terrain.wear_texture,
                              world->terrain.wet.texture, world->terrain.surface_texture, TERRAIN_SIZE, TERRAIN_GRASS_DEPTH);
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index 5380689..37ea5ef 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -43,6 +43,15 @@ bool World_add(World *world, Object *obj);
  */
 float World_groundHeight(const World *world, float x, float z, float from_y);
 
+/*
+ * The first thing along a ray (dir: unit) within max that skip doesn't leave
+ * out (skip may be NULL: all but characters and held things): its distance,
+ * or -1. hit and normal (towards the ray) may be NULL.
+ */
+typedef bool (*RaySkip)(const Object *o, const void *ctx);
+float World_rayCast(const World *world, Vec3 origin, Vec3 dir, float max, RaySkip skip, const void *ctx,
+                    Object **hit, Vec3 *normal);
+
 /* Builds the ground once all static objects are placed (it levels around them). */
 bool World_buildTerrain(World *world);
 
diff --git a/info.txt b/info.txt
new file mode 100644
index 0000000..0939323
--- /dev/null
+++ b/info.txt
@@ -0,0 +1 @@
+if we jump down from a height and we are sportive we do a forward roll (not the roll we do when trying to get the fire off) -> a roll is over the left or right shoulder depending on foot placet