Nimbin[12]?Games / Entropie / commits / 4438fa8

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (17 hours ago) · synced 8 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz

entropie 2.7.0: tents

Christian Immanuel · 2026-10-20 06:40 · 4438fa88ed19bf1eba447c486a65e05411be8340

- Tents in the orchard: everybody pitches their own (Volkram and Jana
  share the big one), in stages: spread out, the poles in and up, the
  cover over it, the pegs in (a stone in hand: quicker)
- Left click on a packed tent spreads it out (the door towards you), hold
  left click to put it up; others come over and give you a hand
- Sleeping bags and mats put down at the door go in
- Z at your tent's door: crawl in and sleep (with your sleeping bag in it:
  warmer, rested sooner); the others go to bed in theirs when sleepy
- Unloading lays the sleeping things out in front of where the doors will be
- People find the way round hedges for longer walks
 entropie/Makefile                  |   2 +-
 entropie/README.md                 |  17 ++-
 entropie/STATE.md                  |  12 ++-
 entropie/commit_message.txt        |  24 ++---
 entropie/src/game/game.c           |   1 +
 entropie/src/game/game.h           |   2 +
 entropie/src/game/game_actions.c   |  97 +++++++++++++++++-
 entropie/src/people/ai.c           | 128 +++++++++++++++++++++++
 entropie/src/people/ai.h           |   3 +
 entropie/src/people/condition.c    |   6 +-
 entropie/src/render/shapes.h       |  12 +++
 entropie/src/render/shapes_props.c |  92 +++++++++++++++++
 entropie/src/world/car.c           |  13 ++-
 entropie/src/world/car.h           |   5 +-
 entropie/src/world/object.c        |  19 ++++
 entropie/src/world/object.h        |   9 +-
 entropie/src/world/physics.c       |   3 +-
 entropie/src/world/tent.c          | 205 +++++++++++++++++++++++++++++++++++++
 entropie/src/world/tent.h          |  39 +++++++
 entropie/src/world/world.c         |   6 +-
 20 files changed, 667 insertions(+), 28 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index cb27b9d..9123b08 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.6.0
+VERSION   := 2.7.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 0b674dc..9322bbd 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -36,6 +36,19 @@ Painted in the rotoscope style of *Waking Life* (2001):
 - Crates: left click takes a bottle out (the others help themselves too),
   E / Q carries the whole crate
 
+## Tents
+
+- In the orchard, where the camping things were put down: everybody pitches
+  their own (Volkram and Jana share the big one)
+- Left click on a packed tent spreads it out (the door towards you). Then
+  hold left click: the poles in and up, the cover over it, the pegs in (a
+  stone in the other hand: quicker)
+- Several working on one: quicker. The others come over and give you a hand
+- Put your sleeping bag and mat down at the door: they go in
+- Z at your tent's door: crawl in and sleep (better with your sleeping bag
+  in it: warm, and you're rested sooner). Z again: you crawl out
+- Sleepy, the others go to bed in their tents
+
 ## The site
 
 The map is Homburgshöhe at Geflügelhof Oderland, traced from the real map
@@ -638,7 +651,9 @@ say no more often and don't come to you for a bit.
 | Left click on the fire, hand free | Take food off the grill |
 | P             | Pee (next to the toilet: use it); Shift: further, mouse: aim |
 | K             | Light a cigarette / put it out |
-| Z             | Sleep / get up |
+| Left click at the car's boot | Take out the next thing |
+| Left click on a tent | Spread it out; hold: put it up |
+| Z             | Sleep / get up (at your tent's door: in it) |
 | C             | Sit down / get up; on fire: drop and roll; at a fire with a drink: pour it on |
 | Space (asleep, hold) | Sleep faster |
 | G             | One more person helps you |
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 7cb7c71..bf58c02 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -187,6 +187,14 @@ Round 2.6:
 - Camping gear (OBJ_GEAR: sleeping bag, mat, small and big tent bag; `owner`; colours by owner, `Shapes_gear`).
 - The fireplace's ground slab is 0.6 of the ring wide (its corners at the stones): people beside the fire stepped on and off its corners (Kurti's joint jumps in the NPC audit 1465 -> 421).
 
+Round 2.7:
+- Tents (OBJ_TENT, `tent.c`: small 1.4 x 2.1 x 1.0, big 2.2 x 2.4 x 1.3; `Shapes_tent` by stage: the ground sheet, the inner tent - a dome - rising with the poles, two poles corner to corner, the cover going over from the back, pegs with guy lines, the door; built again in 12 steps a stage). `Tent_unpack`: a tent bag where it lies, the door facing the given way (NPCs: `Car_campDoor`, towards the yard; you: towards you). Stages (`Tent_work`, one average person: x (0.75 + 0.5 wits), drunk slower): spread out 4 s, poles 16 / 24 s, cover 10 / 14 s, pegs 2.5 s each (a stone in hand: half), 6 / 10 pegs. Its box grows with it (nobody stands on a tent: `World_groundHeight` and step-over leave it out).
+- People (`ai.c` pitchTents): after unloading, their own bag spread out and the tent pitched (at `Tent_workSpot`, the hands at `Tent_handsAt`: the edge, the poles, the cover, the next peg - hammering); somebody at work on one within TENT_HELP 25 m (you too): HELP_CHANCE 0.12/s x (0.5 + willing) they come over ("I'll give you a hand"), for up to 150 s; their tent up: their sleeping bag and mat carried to its door. `goToBed`: sleepiness 0.8+, their tent up within 70 m: to its door, `Tent_crawlIn` (lying back into it, the feet by the door, `in_tent`: no collisions with it), up again: out at the door (`Tent_update`). Far walks look for the way round first (`goToPoint`: `findWay` over 6 m - Toni stood behind the orchard's hedge bumping his shins).
+- Beds: a sleeping bag or mat put down within 0.8 m of the door of its owner's tent (cover on) goes in (`tent_beds`). Asleep in a tent: sleep goes 1.15x, with their sleeping bag 1.4x, and they warm up (no chill).
+- Unloading puts each tent bag on its camp spot and the sleeping bags and mats CAMP_BEDS_OUT 2.8 m out in front of where its door will be.
+- You: left click on a packed tent (`tentClick`) - spread out; held at a tent going up (`CLICK_TENT`, 1.2 m): work on it; a hint line says what's next; Z at your tent's door: crawl in.
+- Test (all three tents): up about 2 min after unloading, the beds in; to bed at night: in their tents, hidden by them.
+
 Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves.
 
 Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built).
@@ -334,7 +342,7 @@ Most important first.
 - Smoking, still to do: paying for a pack (no money in the game yet)
 - "No shirt no service" policy (decision open)
 - Clothing, still to do: more outfits for extra people (only the performance test has any: they all wear trousers); the pullover over a railing NPCs never do on their own
-- Tents: Volkram and Jana share one, Toni and you a small one each; pitching together (poles, the cover, pegs); sleeping in them
+- Tents, still to do: taking them down again; wind blowing an unpegged one about; people busy (work) when sleepy go to bed late
 - Rain: showers; people go into the house or their tents
 
 **Tools for us**
@@ -344,4 +352,4 @@ Most important first.
 - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene)
 
 ---
-State of `entropie 2.6.0 (build 963c4fd)`
+State of `entropie 2.7.0 (build ba5c3a3)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index ffdb5fa..7055ce2 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,14 +1,12 @@
-entropie 2.6.0: arriving by car
+entropie 2.7.0: tents
 
-- The game starts in your car: you, Volkram, Jana and Toni drive in along
-  the gravel road and park in the yard (seen from the driver's seat)
-- Everybody gets out, Kurti greets you, the boot opens
-- The boot: 2 crates of beer, 2 of water, everybody's sleeping bag and mat,
-  the big tent (Volkram and Jana) and two small ones (Toni, you)
-- The others unload: crates by the fire, the camping things to the orchard
-  where the tents will go; left click at the boot to help
-- Crates: left click takes a bottle out, E / Q carries the crate; the
-  others take their beer and water from them too
-- Fix: the fire's ground slab stuck out past the stones; people beside the
-  fire stepped on and off its corners and jittered
-- ARRIVE_BY_CAR in src/game/game.c (testing) skips the drive
+- Tents in the orchard: everybody pitches their own (Volkram and Jana
+  share the big one), in stages: spread out, the poles in and up, the
+  cover over it, the pegs in (a stone in hand: quicker)
+- Left click on a packed tent spreads it out (the door towards you), hold
+  left click to put it up; others come over and give you a hand
+- Sleeping bags and mats put down at the door go in
+- Z at your tent's door: crawl in and sleep (with your sleeping bag in it:
+  warmer, rested sooner); the others go to bed in theirs when sleepy
+- Unloading lays the sleeping things out in front of where the doors will be
+- People find the way round hedges for longer walks
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 0ab49bd..166ab32 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -285,6 +285,7 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
         else if (stamina > WORN_OUT_UNTIL) game->worn_out = false;
     }
     Camera_rotate(&game->camera, input->look_yaw, input->look_pitch);
+    game->dt = dt;
     game_handleActions(game, input);
     game_talk(game, input, dt);
     grabLook(game, input, dt);
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index f217356..4366617 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -67,6 +67,8 @@ typedef struct {
     Vec3     brace_grip;    /* ... here */
     Object  *putting_up;    /* the ladder you are putting up (hands on it, standing still) */
     Arrival  arrival;       /* the start: in by car */
+    struct Object *tent;    /* the tent you're working on (left button held) */
+    float    dt;            /* this frame's step */
 } Game;
 
 /* A line of speech above someone's head, in screen pixels (y up from the bottom). */
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index f9be548..4c2ce62 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -7,6 +7,7 @@
 #include "people/pockets.h"
 #include "world/clothes.h"
 #include "world/car.h"
+#include "world/tent.h"
 #include "people/climb.h"
 
 #include <stdlib.h>
@@ -16,7 +17,7 @@
 #define APPLE_REACH  1.6f  /* from the trunk of an apple tree */
 #define BAG_INTERVAL 0.35f /* holding the button: the next piece into the bag */
 
-enum { CLICK_NONE, CLICK_WORK, CLICK_DRINK, CLICK_BAG, CLICK_WATER };
+enum { CLICK_NONE, CLICK_WORK, CLICK_DRINK, CLICK_BAG, CLICK_WATER, CLICK_TENT };
 
 #define BUSH_OVER   1.3f   /* bushes lower than this (scaled): you try to get over, and land in it */
 #define CLIMB_PUSH  0.3f   /* walking against it this long starts the climb */
@@ -698,6 +699,61 @@ static bool takePullover(Game *game) {
     return true;
 }
 
+/* ---------- Tents ---------- */
+
+#define TENT_REACH 1.2f /* m: a tent this near to work on it */
+
+/* A packed tent (its bag lying) or a tent not up yet, near and in front. */
+static Object *tentNear(const Game *game, bool packed) {
+    const Object *p = game->player;
+    Vec3 facing = Object_facing(p);
+    Object *best = NULL;
+    float best_gap = packed ? 0.9f : TENT_REACH;
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        bool fits = packed ? o->type == OBJ_GEAR && o->variant >= GEAR_TENT_SMALL && !o->is_held && !o->broken
+                           : o->type == OBJ_TENT && o->tent_stage < TENT_UP;
+        if (!fits) continue;
+        Vec3 to = vec3_sub(o->position, p->position);
+        to.y = 0.0f;
+        float gap = Hands_gapFrom(p->position, p->look.height, o);
+        if (gap < best_gap && (gap < 0.3f || vec3_dot(vec3_normalize(to), facing) > 0.2f)) { best = o; best_gap = gap; }
+    }
+    return best;
+}
+
+/* Left click: a packed tent in front spread out (the door towards you), or a tent going up: work on it (held). */
+static int tentClick(Game *game) {
+    Object *p = game->player, *bag = tentNear(game, true);
+    if (bag) {
+        Vec3 door = vec3_sub(p->position, bag->position);
+        Object *tent = Tent_unpack(&game->world, bag, door);
+        if (tent) {
+            Hands_touch(p, tent->position);
+            say(game, Tent_isTheirs(tent, p) ? "You spread your tent out - hold left click to put it up"
+                                              : "You spread the tent out - hold left click to put it up");
+        }
+        return CLICK_NONE;
+    }
+    Object *tent = tentNear(game, false);
+    if (!tent) return -1;
+    if (p->hands.state[HAND_LEFT] != HAND_FREE && p->hands.state[HAND_RIGHT] != HAND_FREE) { say(game, "A hand free for that"); return CLICK_NONE; }
+    game->tent = tent;
+    return CLICK_TENT;
+}
+
+/* Left button held at a tent: the next bit of it. */
+static bool workTent(Game *game) {
+    Object *p = game->player, *tent = game->tent;
+    if (!tent || tent->tent_stage >= TENT_UP || Hands_gapFrom(p->position, p->look.height, tent) > TENT_REACH + 0.6f) return false;
+    Hands_touch(p, Tent_handsAt(tent, p, tent->tent_work * 18.0f));
+    int got = Tent_work(tent, p, game->dt);
+    static const char *const SAID[] = { "", "Spread out - now the poles in and up", "The poles are in - now pull the cover over it",
+                                        "The cover's on - now peg it down (a stone helps)", "All pegged - the tent stands" };
+    if (got >= 0) say(game, SAID[got % 5]);
+    return tent->tent_stage < TENT_UP;
+}
+
 /* At the cigarette machine: a pack. */
 static bool smokesMachine(Game *game) {
     Object *m = furnitureNear(game, FURN_SMOKES, 0.9f);
@@ -799,6 +855,13 @@ static bool game_bootHint(Game *game, char *out, size_t size) {
         const Object *it = p->hands.item[h];
         if (!it || p->hands.state[h] != HAND_HOLDING) continue;
         if (it->type == OBJ_CRATE) { snprintf(out, size, "The crates go by the fire, next to each other  -  right click: put it down"); return true; }
+        if (it->type == OBJ_GEAR && it->variant <= GEAR_MAT) { /* (its owner's tent up: in at its door) */
+            const Object *tent = Tent_of(game->world.objects, game->world.count, it->owner);
+            if (tent && tent->tent_stage >= TENT_COVER) {
+                snprintf(out, size, "Put it down at %s tent's door (right click there): it goes in", it->owner == p ? "your" : "their");
+                return true;
+            }
+        }
         if (it->type == OBJ_GEAR) {
             snprintf(out, size, "%s  -  over to the orchard, where the tents go  -  right click: put it down",
                      it->owner == p ? "Yours" : it->owner ? it->owner->name : "Somebody's");
@@ -812,6 +875,23 @@ static bool game_bootHint(Game *game, char *out, size_t size) {
     return true;
 }
 
+/* At a tent: what to do with it now. */
+static bool tentHint(Game *game, char *out, size_t size) {
+    Object *p = game->player, *bag = tentNear(game, true), *tent = tentNear(game, false);
+    if (bag) { snprintf(out, size, "A tent, packed  -  left click: spread it out here (the door towards you)  -  E / Q: carry it"); return true; }
+    if (tent) { snprintf(out, size, "The tent: hold left click - %s%s", Tent_job(tent), tent->tent_stage == TENT_PEGS ? " (a stone in the other hand: quicker)" : ""); return true; }
+    Object *mine = Tent_of(game->world.objects, game->world.count, p);
+    if (!mine || mine->tent_stage < TENT_COVER) return false;
+    Vec3 door = Tent_door(mine);
+    if (hypotf(door.x - p->position.x, door.z - p->position.z) > 1.6f) return false;
+    if (Hands_carries(p, OBJ_GEAR)) { snprintf(out, size, "Your tent's door: right click - put it down here, it goes in"); return true; }
+    if (mine->tent_stage >= TENT_UP) {
+        snprintf(out, size, "Your tent%s  -  Z: crawl in and sleep", Tent_hasBed(mine, p) ? "" : " (your sleeping bag isn't in it: put it down at the door)");
+        return true;
+    }
+    return false;
+}
+
 /* Somebody near who asked you for a paper, a filter or tobacco (and is still waiting). */
 static const Object *askingYou(const Game *game) {
     const Object *p = game->player;
@@ -841,6 +921,7 @@ const char *Game_hint(Game *game) {
     else if (game->player->riding)
         snprintf(game->hint, sizeof(game->hint), "Nearly there - Homburgsh\xC3\xB6he. Look around (mouse)");
     else if (game_bootHint(game, game->hint, sizeof(game->hint))) {}
+    else if (tentHint(game, game->hint, sizeof(game->hint))) {}
     else if (shown)
         snprintf(game->hint, sizeof(game->hint), "%s follows you: walk to where the firewood should go and left click  -  Backspace: never mind", shown->name);
     else if (watcher)
@@ -1002,6 +1083,8 @@ static int leftClick(Game *game) {
         Talk_showHere(&game->world, p, shown);
         return CLICK_NONE;
     }
+    int tent = tentClick(game);
+    if (tent >= 0) return tent;
     if (game_unloadCar(game) || mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game) || takePullover(game) || smokesMachine(game) || swingGate(game)) return CLICK_NONE;
     int watering = useWater(game);
     if (watering >= 0) return watering;
@@ -1050,6 +1133,9 @@ static void keepClicking(Game *game) {
     case CLICK_WATER:
         p->spraying = true; /* see the world: the water arc */
         break;
+    case CLICK_TENT:
+        if (!workTent(game)) game->click_action = CLICK_NONE;
+        break;
     default:
         break;
     }
@@ -1080,6 +1166,15 @@ static void sleepOrWake(Game *game) {
     }
     if (Body_isBusy(b)) return;
     if (p->needs.sleepiness < 0.3f) { say(game, "Not tired"); return; }
+    Object *tent = Tent_of(game->world.objects, game->world.count, p);
+    if (tent && tent->tent_stage >= TENT_UP) {
+        Vec3 door = Tent_door(tent);
+        if (hypotf(door.x - p->position.x, door.z - p->position.z) < 1.5f) {
+            Tent_crawlIn(tent, p);
+            say(game, Tent_hasBed(tent, p) ? "You crawl into your tent and your sleeping bag" : "You crawl into your tent (no sleeping bag in it: a cold night)");
+            return;
+        }
+    }
     Body_lieDown(b);
 }
 
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index fc83009..9391d63 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -12,6 +12,7 @@
 #include "world/world.h"
 #include "world/clothes.h"
 #include "world/car.h"
+#include "world/tent.h"
 #include "render/shapes.h"
 
 /* ---------- Small helpers ---------- */
@@ -1692,6 +1693,7 @@ static Object *carriedCargo(const Object *self) {
 static bool goToPoint(Object *self, Vec3 p, float reach, Object *const *objects, int count, float dt) {
     float d = flatDistance(p, self->position);
     if (d < reach) { stand(self); return true; }
+    if (!self->ai.path_len && d > 6.0f) findWay(self, p); /* (further: the way round from the start - hedges, walls) */
     Vec3 dir = ai_steer(self, NULL, objects, count, p);
     if (vec3_length(dir) < 0.5f) { stand(self); return false; }
     ai_walk(self, dir, fminf(WALK_SPEED, d * 2.0f + 0.3f), dt);
@@ -1754,6 +1756,128 @@ static bool unloadCar(Object *self, Object *const *objects, int count, float dt)
     return true;
 }
 
+/* Tents: their own spread out where its bag lies and pitched; somebody at
+   work on one near (you too): a hand lent, the willing; their own up: the
+   sleeping bag and mat in at its door. Sleepy, their tent up: to bed in it. */
+#define TENT_WALK   70.0f /* m: their tent (or its bag) this near */
+#define TENT_HELP   25.0f /* m: somebody pitching one this near ... */
+#define HELP_CHANCE 0.12f /* ... gets a hand this often a second (x willing) */
+#define HELP_LONG   150.0f/* s: helping, then they've had enough */
+#define BED_WALK    12.0f /* m: their sleeping bag or mat lying this near their tent: brought in */
+
+static Object *ownGear(Object *self, Object *const *objects, int count, bool tent_bag) {
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o->type != OBJ_GEAR || o->owner != self || o->broken) continue;
+        if (tent_bag == (o->variant >= GEAR_TENT_SMALL)) return o;
+    }
+    return NULL;
+}
+
+static Object *bedThing(Object *self, Object *const *objects, int count, const Object *tent) {
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o->type == OBJ_GEAR && o->owner == self && !o->broken && o->variant <= GEAR_MAT &&
+            (!o->is_held || Hands_carries(self, OBJ_GEAR)) && flatDistance(o->position, tent->position) < BED_WALK) return o;
+    }
+    return NULL;
+}
+
+static void workOnTent(Object *self, Object *tent, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    float side = Tent_isTheirs(tent, self) ? (tent->owner2 == self ? -1.0f : 1.0f) : (ai->side != 0.0f ? -ai->side : -1.0f);
+    Vec3 spot = Tent_workSpot(tent, side);
+    if (tent->tent_stage == TENT_PEGS && !Tent_isTheirs(tent, self)) { /* (a helper: the next peg round from the owner's) */
+        Vec3 out = vec3_sub(spot, tent->position);
+        spot = vec3_add(spot, vec3_scale(vec3_normalize(vec3(-out.z, 0.0f, out.x)), 0.6f));
+    }
+    if (!goToPoint(self, spot, 0.35f, objects, count, dt)) return;
+    freeHands(self, NULL);
+    Vec3 hands = Tent_handsAt(tent, self, tent->tent_work * 18.0f);
+    turnTowards(self, vec3_sub(hands, self->position), dt);
+    Hands_touch(self, hands);
+    int got = Tent_work(tent, self, dt);
+    if (got >= 0 && random01(ai) < 0.6f) {
+        static const char *const SAY[] = { "", "Poles in - up it goes.", "Now the cover over it.", "Pegs next - who's got a stone?", "There - it stands!" };
+        Ai_say(self, SAY[got % 5]);
+    }
+}
+
+static bool pitchTents(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    if (ai->unload || self->body.asleep || self->in_tent) return false;
+    Object *tent = Tent_of(objects, count, self);
+    /* their own tent, not up yet */
+    if (tent && tent->tent_stage < TENT_UP && flatDistance(tent->position, self->position) < TENT_WALK) {
+        workOnTent(self, tent, objects, count, dt);
+        return true;
+    }
+    if (!tent) { /* still in its bag: spread it out where it lies */
+        Object *bag = ownGear(self, objects, count, true);
+        if (bag && !bag->is_held && flatDistance(bag->position, self->position) < TENT_WALK) {
+            if (self->sitting) self->sitting = false;
+            if (!goToPoint(self, bag->position, 0.6f, objects, count, dt)) return true;
+            freeHands(self, NULL);
+            Hands_touch(self, bag->position);
+            if (Tent_unpack(g_world, bag, Car_campDoor()) && random01(ai) < 0.7f) Ai_say(self, "Right - let's get the tent up.");
+            return true;
+        }
+    }
+    /* somebody pitching one near: a hand */
+    Object *job = ai->tent_job;
+    if (job && (job->tent_stage >= TENT_UP || (ai->tent_time += dt) > HELP_LONG || job->tent_idle > 20.0f)) job = ai->tent_job = NULL;
+    if (!job && (ai->mode == AI_IDLE || ai->mode == AI_WANDER)) {
+        for (int i = 0; i < count && !job; i++) {
+            Object *t = objects[i];
+            if (t->type != OBJ_TENT || t->tent_stage >= TENT_UP || t->tent_idle > 1.5f || Tent_isTheirs(t, self) ||
+                flatDistance(t->position, self->position) > TENT_HELP) continue;
+            if (random01(ai) < HELP_CHANCE * dt * (0.5f + ai->character.willing)) job = t;
+        }
+        if (job) {
+            ai->tent_job = job;
+            ai->tent_time = 0.0f;
+            if (random01(ai) < 0.8f) Ai_say(self, "Wait - I'll give you a hand with that.");
+        }
+    }
+    if (job) { workOnTent(self, job, objects, count, dt); return true; }
+    /* their own up: the sleeping bag and the mat in at the door */
+    if (tent && tent->tent_stage >= TENT_COVER) {
+        Object *thing = bedThing(self, objects, count, tent);
+        if (!thing) return false;
+        if (thing->is_held) {
+            Vec3 door = Tent_door(tent);
+            if (!goToPoint(self, door, 0.3f, objects, count, dt)) return true;
+            for (int h = 0; h < 2; h++) if (self->hands.item[h] == thing) Hands_drop(self, h);
+            thing->position.x = door.x; /* (in at the door) */
+            thing->position.z = door.z;
+            Object_syncColliders(thing);
+            return true;
+        }
+        if (Hands_gapFrom(self->position, self->look.height, thing) < PICKUP_REACH) {
+            stand(self);
+            if (!Hands_pickUpObject(self, Hands_freeHand(self) >= 0 ? Hands_freeHand(self) : self->main_hand, thing)) freeHands(self, NULL);
+            return true;
+        }
+        goToPoint(self, thing->position, 0.3f, objects, count, dt);
+        return true;
+    }
+    return false;
+}
+
+/* Sleepy, their tent up: over to it and in. */
+static bool goToBed(Object *self, Object *const *objects, int count, float dt) {
+    if (self->needs.sleepiness < 0.8f || self->body.asleep || self->in_tent || self->body.pose != POSE_UP) return false;
+    Object *tent = Tent_of(objects, count, self);
+    if (!tent || tent->tent_stage < TENT_UP || flatDistance(tent->position, self->position) > TENT_WALK) return false;
+    if (self->sitting) self->sitting = false;
+    if (!goToPoint(self, Tent_door(tent), 0.4f, objects, count, dt)) return true;
+    freeHands(self, NULL);
+    if (random01(&self->ai) < 0.5f) Ai_say(self, "Night, everybody.");
+    Tent_crawlIn(tent, self);
+    rest(&self->ai, 2.0f, 3.0f);
+    return true;
+}
+
 /* A finished cigarette in the fingers (the tidy): to the tin, a bin or the
    fire with it; none near: somebody's pocket ashtray (asked for), else wrapped
    up and into the pocket. Butts in the pocket (ashtray) go into the tin, a
@@ -1890,6 +2014,10 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && ai_putAway(self, objects, count, dt)) return;
     /* Unloading the car */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && unloadCar(self, objects, count, dt)) return;
+    /* Sleepy: to bed in their tent */
+    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && goToBed(self, objects, count, dt)) return;
+    /* Tents: their own up, a hand with others', the bed things in */
+    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && pitchTents(self, objects, count, dt)) return;
     /* A finished cigarette in the fingers: to the tin, a bin, the fire */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && binButt(self, objects, count, dt)) return;
     /* Cold, the pullover put down somewhere: off to get it */
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index 2d68ede..c98879e 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -177,6 +177,9 @@ typedef struct {
     float          smokes_time; /* ... on the way this long */
     bool           asked_you;   /* asked the player for one (nothing came of it: the machine next time) */
     struct Object *take_from;   /* a bottle from this crate, or the next thing out of this car's boot: please (see car.c Car_serve) */
+    struct Object *tent_job;    /* lending a hand with this tent (ai.c pitchTents) ... */
+    float          tent_time;   /* ... this long */
+    bool           tent_said;   /* ... said so */
     bool           unload;      /* unloading the car (ai.c unloadCar) ... */
     struct Object *unload_car;
     float          unload_time; /* ... on this trip this long */
diff --git a/entropie/src/people/condition.c b/entropie/src/people/condition.c
index d7d67be..22ef003 100644
--- a/entropie/src/people/condition.c
+++ b/entropie/src/people/condition.c
@@ -1,3 +1,4 @@
+#include "world/tent.h"
 #include "people/condition.h"
 #include "world/object.h"
 #include "world/grid.h"
@@ -21,6 +22,8 @@
 #define BLADDER_PER_HOUR  0.04f
 #define AWAKE_PER_HOUR    0.065f  /* sleepiness while awake: about 16 hours a day */
 #define SLEEP_PER_HOUR    0.16f   /* ... taken off while asleep: a good night's sleep */
+#define TENT_SLEEP        1.15f   /* ... in a tent, quicker */
+#define TENT_BED_SLEEP    1.4f    /* ... and in a sleeping bag in it, quicker still */
 #define WORK_THIRST       2.0f    /* hard work makes you this much thirstier / hungrier */
 /* Stamina, per real second */
 #define TIRE_RUNNING      0.018f
@@ -119,7 +122,8 @@ static void needs(Object *self, float dt, float game_dt) {
     n->hunger     = fminf(1.0f, n->hunger + HUNGER_PER_HOUR * hours * hard * slow);
     n->thirst     = fminf(1.0f, n->thirst + THIRST_PER_HOUR * hours * hard * slow);
     n->bladder    = fminf(1.0f, n->bladder + BLADDER_PER_HOUR * hours / Object_bladder(self));
-    n->sleepiness = clampf(n->sleepiness + (asleep ? -SLEEP_PER_HOUR : AWAKE_PER_HOUR) * hours, 0.0f, 1.0f);
+    float bed = asleep && self->in_tent ? (Tent_hasBed(self->in_tent, self) ? TENT_BED_SLEEP : TENT_SLEEP) : 1.0f; /* (a tent, a sleeping bag: better sleep) */
+    n->sleepiness = clampf(n->sleepiness + (asleep ? -SLEEP_PER_HOUR * bed : AWAKE_PER_HOUR) * hours, 0.0f, 1.0f);
 
     /* Can't keep the eyes open any more: sleep right here */
     bool settled = b->pose == POSE_UP || b->pose == POSE_DOWN;
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index e68c8ed..dadc590 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -141,6 +141,18 @@ Mesh  Shapes_crate(int variant, int bottles);
 /* Camping things (centred, lying along X): kind 0 a sleeping bag in its sack, 1 a foam mat rolled up,
    2 a small tent in its bag, 3 a big one. */
 Mesh  Shapes_gear(int kind, Vec3 color);
+/* A dome tent standing on y = 0, the door to +Z: big 0 small (two people
+   squeezed), 1 the big one. How far it's up: stage 0 laid out flat, 1 the
+   poles going in and up (progress 0..1), 2 the cover going over it (from the
+   back), 3 being pegged (pegs in), 4 standing. */
+#define TENT_STAGES 5
+#define TENT_W(big) ((big) ? 2.2f : 1.4f)
+#define TENT_L(big) ((big) ? 2.4f : 2.1f)
+#define TENT_H(big) ((big) ? 1.3f : 1.0f)
+#define TENT_PEGS(big) ((big) ? 10 : 6)
+Mesh  Shapes_tent(int big, int stage, float progress, int pegs, Vec3 inner, Vec3 fly);
+/* Where peg k goes (local, the tent's middle at 0, 0). */
+void  Shapes_tentPeg(int big, int k, float *x, float *z);
 /* A pullover put down (see clothes.h), its colour: lay 0 spread out flat on the ground (y = 0 under it,
    the neck to +Z, sleeves out), 1 hung over an edge along X (y = 0 its top; gap: how thick that edge is),
    2 folded up (y = 0 under it). */
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index e72d0e5..0866087 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -777,6 +777,98 @@ static void addWheel(MeshBuilder *b, Vec3 center, float radius, float width) {
     transformSince(b, first, mat4_mul(mat4_translate(center), mat4_rotate_z(PI_F / 2.0f)));
 }
 
+/* A thin stick from a to c (a pole, a guy line, a peg). */
+static void addStick(MeshBuilder *b, Vec3 a, Vec3 c, float thick, Vec3 color) {
+    Vec3 d = vec3_sub(c, a);
+    float len = vec3_length(d);
+    if (len < 1e-4f) return;
+    Vec3 y = vec3_scale(d, 1.0f / len);
+    Vec3 x = vec3_normalize(fabsf(y.y) < 0.9f ? vec3_cross(y, vec3(0.0f, 1.0f, 0.0f)) : vec3_cross(y, vec3(1.0f, 0.0f, 0.0f)));
+    Vec3 z = vec3_cross(x, y);
+    Mat4 m = mat4_identity();
+    m.m[0] = x.x; m.m[1] = x.y; m.m[2] = x.z;
+    m.m[4] = y.x; m.m[5] = y.y; m.m[6] = y.z;
+    m.m[8] = z.x; m.m[9] = z.y; m.m[10] = z.z;
+    Vec3 mid = vec3_scale(vec3_add(a, c), 0.5f);
+    m.m[12] = mid.x; m.m[13] = mid.y; m.m[14] = mid.z;
+    size_t first = b->vertex_count;
+    addBlock(b, vec3_zero(), vec3(thick, len, thick), color);
+    transformSince(b, first, m);
+}
+
+/* The dome over a hw x hl footprint, h high (rows along z from the back; only
+   the rows up to `upto` - the cover going over), a little wrinkled when low. */
+#define DOME_N 10
+static void addDome(MeshBuilder *b, float hw, float hl, float h, Vec3 color, float upto) {
+    GLuint idx[DOME_N + 1][DOME_N + 1];
+    for (int j = 0; j <= DOME_N; j++)
+        for (int i = 0; i <= DOME_N; i++) {
+            float u = -1.0f + 2.0f * (float)i / DOME_N, v = -1.0f + 2.0f * (float)j / DOME_N;
+            float fu = powf(fmaxf(0.0f, 1.0f - u * u), 0.55f), fv = powf(fmaxf(0.0f, 1.0f - v * v), 0.55f);
+            float y = h * fu * fv + (h < 0.15f ? 0.02f * sinf(7.0f * u + 3.0f * v) * fu * fv : 0.0f) + 0.012f;
+            Vec3 n = vec3_normalize(vec3(u * h / hw, 1.0f, v * h / hl));
+            Vec3 shade = vec3_scale(color, 0.9f + 0.1f * fu * fv);
+            idx[j][i] = MeshBuilder_vertex(b, vec3(u * hw, y, v * hl), n, shade);
+        }
+    for (int j = 0; j < DOME_N; j++) {
+        if ((float)(j + 1) / DOME_N > upto + 1e-3f) break;
+        for (int i = 0; i < DOME_N; i++) MeshBuilder_quad(b, idx[j][i], idx[j][i + 1], idx[j + 1][i + 1], idx[j + 1][i]);
+    }
+}
+
+void Shapes_tentPeg(int big, int k, float *x, float *z) {
+    const float hw = TENT_W(big) * 0.5f, hl = TENT_L(big) * 0.5f;
+    static const float AT[10][2] = { { -1.0f, -1.0f }, { 1.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, 1.0f }, /* the corners */
+                                     { 0.0f, -1.45f }, { 0.0f, 1.45f },                                    /* back and front (the door) */
+                                     { -1.35f, 0.0f }, { 1.35f, 0.0f }, { -0.55f, 1.4f }, { 0.55f, 1.4f } };
+    k = k < 0 ? 0 : k > 9 ? 9 : k;
+    *x = AT[k][0] * (hw + (k < 4 ? 0.12f : 0.0f));
+    *z = AT[k][1] * (hl + (k < 4 ? 0.12f : 0.0f));
+}
+
+Mesh Shapes_tent(int big, int stage, float progress, int pegs, Vec3 inner, Vec3 fly) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const float hw = TENT_W(big) * 0.5f, hl = TENT_L(big) * 0.5f, h = TENT_H(big);
+    const Vec3 sheet = { 0.22f, 0.24f, 0.25f }, pole = { 0.62f, 0.64f, 0.66f }, peg = { 0.7f, 0.7f, 0.72f }, door = vec3_scale(fly, 0.45f);
+    addBlock(&b, vec3(0.0f, 0.005f, 0.0f), vec3(hw * 2.0f + 0.06f, 0.01f, hl * 2.0f + 0.06f), sheet); /* the ground sheet */
+    float up = stage == 0 ? 0.04f : stage == 1 ? fmaxf(0.04f, h * 0.95f * progress) : h * 0.95f;
+    addDome(&b, hw, hl, up, inner, 1.0f);
+    if (stage >= 1) { /* the poles, corner to corner, crossing at the top */
+        float ph = stage == 1 ? fmaxf(0.06f, h * progress) : h;
+        for (int s = -1; s <= 1; s += 2) {
+            Vec3 last = vec3((float)s * -hw, 0.0f, -hl);
+            for (int k = 1; k <= 10; k++) {
+                float t = (float)k / 10.0f;
+                Vec3 p = vec3((float)s * (-hw + 2.0f * hw * t), ph * sinf(PI_F * t), -hl + 2.0f * hl * t);
+                addStick(&b, last, p, 0.022f, pole);
+                last = p;
+            }
+        }
+    }
+    if (stage >= 2) /* the cover, from the back over the top to the door */
+        addDome(&b, hw * 1.08f, hl * 1.14f, h * 1.05f, fly, stage == 2 ? progress : 1.0f);
+    if (stage >= 3) { /* the pegs in so far, with their lines up to the cover */
+        int n = stage == 3 ? pegs : TENT_PEGS(big);
+        for (int k = 0; k < n && k < TENT_PEGS(big); k++) {
+            float x, z;
+            Shapes_tentPeg(big, k, &x, &z);
+            addStick(&b, vec3(x, -0.08f, z), vec3(x, 0.05f, z), 0.02f, peg);
+            if (k >= 4) { /* a guy line from the cover's edge up there */
+                float ex = x * 0.55f, ez = z * 0.62f, u = ex / (hw * 1.08f), v = ez / (hl * 1.14f);
+                float ey = h * 1.05f * powf(fmaxf(0.0f, 1.0f - u * u), 0.55f) * powf(fmaxf(0.0f, 1.0f - v * v), 0.55f);
+                addStick(&b, vec3(x, 0.05f, z), vec3(ex, ey, ez), 0.008f, (Vec3){ 0.85f, 0.4f, 0.15f });
+            }
+        }
+    }
+    if (stage >= 4) { /* the door: zipped shut, a darker arch in the front */
+        size_t first = b.vertex_count;
+        addBlock(&b, vec3(0.0f, h * 0.32f, 0.0f), vec3(hw * 0.75f, h * 0.62f, 0.01f), door);
+        transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, 0.0f, hl * 1.02f)), mat4_rotate_x(-0.55f)));
+    }
+    return Mesh_fromBuilder(&b);
+}
+
 /* Your estate car (see shapes.h). */
 Mesh Shapes_car(Vec3 color, bool boot_open) {
     MeshBuilder b;
diff --git a/entropie/src/world/car.c b/entropie/src/world/car.c
index 9c7ce3f..df0b1ec 100644
--- a/entropie/src/world/car.c
+++ b/entropie/src/world/car.c
@@ -183,11 +183,16 @@ Vec3 Car_dropSpot(const World *world, const Object *item) {
         }
         return vec3_add(base, vec3_scale(along, 0.5f * (float)n));
     }
-    Vec3 along = Site_campAlong(), across = vec3(-along.z, 0.0f, along.x);
+    Vec3 along = Site_campAlong(), across = Car_campDoor();
     Vec3 at = Site_campSpot(campOf(item->owner));
-    float a = item->variant == GEAR_SLEEPING_BAG ? 0.0f : item->variant == GEAR_MAT ? 0.55f : 1.1f;
-    float b = whose(item->owner) == 2 ? 0.7f : 0.0f; /* (Jana's beside Volkram's) */
-    return vec3_add(at, vec3_add(vec3_scale(along, a), vec3_scale(across, b)));
+    if (item->variant >= GEAR_TENT_SMALL) return at; /* (the tent where it'll stand, the rest in front of its door) */
+    float a = (item->variant == GEAR_MAT ? 0.55f : 0.0f) + (whose(item->owner) == 2 ? 1.2f : 0.0f) - 0.6f; /* (Jana's beside Volkram's) */
+    return vec3_add(at, vec3_add(vec3_scale(along, a), vec3_scale(across, CAMP_BEDS_OUT)));
+}
+
+Vec3 Car_campDoor(void) {
+    Vec3 along = Site_campAlong();
+    return vec3(-along.z, 0.0f, along.x);
 }
 
 Object *Crate_takeBottle(World *world, Object *crate) {
diff --git a/entropie/src/world/car.h b/entropie/src/world/car.h
index a98bfb6..83a6ac6 100644
--- a/entropie/src/world/car.h
+++ b/entropie/src/world/car.h
@@ -45,7 +45,10 @@ void        Car_getIn(Object *car, Object *who, int seat);
 void        Car_getOut(Object *who);
 void        Car_openBoot(Object *car, bool open);
 
-/* Where an unloaded thing goes: crates by the fire, the camping things where its owner's tent will be. */
+/* Where an unloaded thing goes: crates by the fire, the camping things where its owner's tent will be
+   (the tent bag on the spot, sleeping bags and mats CAMP_BEDS_OUT in front of its door). */
+#define CAMP_BEDS_OUT 2.8f
+Vec3        Car_campDoor(void);
 Vec3        Car_dropSpot(const World *world, const Object *item);
 
 /* A bottle out of a crate (on top of it, for the taking): NULL if it's empty. */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 74f3036..77b11c8 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -385,6 +385,17 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         break;
     }
 
+    case OBJ_TENT: { /* (see tent.c: its mesh by how far it's up) */
+        obj->variant   = weight > 0.5f ? 1 : 0;
+        obj->weight    = 50.0f;
+        obj->is_static = true;
+        obj->shown     = -1;
+        obj->size      = vec3(TENT_W(obj->variant), TENT_H(obj->variant), TENT_L(obj->variant));
+        obj->mesh      = Shapes_tent(obj->variant, 0, 0.0f, 0, vec3(0.9f, 0.86f, 0.62f), vec3(0.3f, 0.5f, 0.3f));
+        addCollider(obj, vec3(0.0f, 0.02f, 0.0f), vec3(obj->size.x, 0.04f, obj->size.z), vec3_zero());
+        break;
+    }
+
     case OBJ_CLOTHES: /* (made in clothes.c, its mesh given) */
         obj->is_static = true;
         break;
@@ -1268,6 +1279,14 @@ void Object_describe(const Object *obj, char *out, size_t size) {
         snprintf(out, size, "Crate of %s - %d bottle%s left%s", obj->variant ? "water" : "beer", obj->stored, obj->stored == 1 ? "" : "s",
                  obj->stored > 0 && !obj->is_held ? " (left click: take one)" : "");
         break;
+    case OBJ_TENT: {
+        static const char *const STAGE[] = { "spread out - left click: put the poles in", "the poles going in - keep clicking",
+                                             "the cover going over it - keep clicking", "pegging it down - keep clicking", "up" };
+        const char *whose = obj->owner && obj->owner->type == OBJ_PLAYER ? "Your" : NULL;
+        if (whose) snprintf(out, size, "Your tent - %s", STAGE[obj->tent_stage % 5]);
+        else snprintf(out, size, "%s's tent - %s", obj->owner ? obj->owner->name : "Somebody", STAGE[obj->tent_stage % 5]);
+        break;
+    }
     case OBJ_GEAR: {
         static const char *const WHAT[] = { "sleeping bag", "sleeping mat", "tent", "tent (the big one)" };
         if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s", WHAT[obj->variant & 3]);
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 9f05da7..ae2a6c4 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -86,6 +86,7 @@ typedef enum {
                             what's in its boot: load (see car.h) */
     OBJ_CRATE,           /* a crate of bottles: weight 0 = beer (CRATE_BEER bottles), 1 = water (CRATE_WATER); stored = bottles in it */
     OBJ_GEAR,            /* camping things: variant = GearKind (see car.h), owner = whose */
+    OBJ_TENT,            /* static: a dome tent, weight 0 small / 1 big (see tent.h); stored = pegs in */
 } ObjectType;
 
 /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */
@@ -160,7 +161,13 @@ typedef struct Object {
     struct CarLoad *load;     /* cars: what's in the boot (malloc'd, freed with the car) */
     bool       boot_open;     /* cars: the boot lid up */
     Mesh       inside;        /* cars: what you see of it from the driver's seat */
-    struct Object *owner;     /* camping gear: whose */
+    struct Object *owner;     /* camping gear, tents: whose ... */
+    struct Object *owner2;    /* ... tents: and who shares it */
+    int        tent_stage;    /* tents: TentStage ... */
+    float      tent_work;     /* ... how far the stage is (0..1) ... */
+    float      tent_idle;     /* ... seconds since anybody worked on it */
+    int        tent_beds;     /* ... sleeping bags (bit 0 owner, 2 owner2) and mats (1, 3) put in */
+    struct Object *in_tent;   /* characters: sleeping in this tent (no collisions with it) */
     struct Object *riding;    /* characters: sitting in this car (hidden, no physics, no AI: see car.c) ... */
     int        ride_seat;     /* ... on this seat (0 the driver) */
     int        branches;      /* trees: branches left on the crown */
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index ad9f3cf..29bac22 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -99,7 +99,7 @@ static float halfAcross(const Collider *c) {
 static bool neverOver(const Object *o) {
     switch (o->type) {
     case OBJ_HOUSE: case OBJ_BARN: case OBJ_BUSH: case OBJ_FIREPLACE: case OBJ_DOG:
-    case OBJ_HUMAN: case OBJ_PLAYER: case OBJ_DECAL: case OBJ_PUDDLE: case OBJ_STINK: case OBJ_WALL: case OBJ_STRUCTURE:
+    case OBJ_HUMAN: case OBJ_PLAYER: case OBJ_DECAL: case OBJ_PUDDLE: case OBJ_STINK: case OBJ_WALL: case OBJ_STRUCTURE: case OBJ_TENT:
         return true;
     case OBJ_TREE:
         return !(o->toppled && o->topple >= 1.0f); /* (one lying on the ground: over it, or onto it) */
@@ -501,6 +501,7 @@ static void resolveObjects(Object *obj, Object *const *objects, int count) {
         if (obj->hands.grabbed == other || other->hands.grabbed == obj) continue;
         /* Sitting on it */
         if (obj->seat == other || other->seat == obj) continue; /* (sitting, or just getting up off it) */
+        if (obj->in_tent == other || other->in_tent == obj) continue; /* (lying in it) */
         /* Stepping over it */
         if (Physics_stepsOver(obj, other) || Physics_stepsOver(other, obj)) continue;
 
diff --git a/entropie/src/world/tent.c b/entropie/src/world/tent.c
new file mode 100644
index 0000000..8664d47
--- /dev/null
+++ b/entropie/src/world/tent.c
@@ -0,0 +1,205 @@
+#include "world/tent.h"
+#include "world/car.h"
+#include "people/hands.h"
+#include "render/shapes.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#define LAY_TIME    4.0f   /* s (one person, average): spreading it out, the poles through their sleeves */
+#define POLES_SMALL 16.0f  /* ... the poles in and up */
+#define POLES_BIG   24.0f
+#define COVER_SMALL 10.0f  /* ... the cover over it */
+#define COVER_BIG   14.0f
+#define PEG_TIME    2.5f   /* ... each peg (with a stone to hit it: twice as quick) */
+#define DOOR_OUT    0.45f  /* in front of the door: this far out from the cover */
+#define BED_REACH   0.8f   /* a sleeping bag or mat put down this near the door goes in */
+
+static Vec3 local(const Object *tent, Vec3 p) {
+    return vec3_add(tent->position, mat4_mul_dir(mat4_rotate_y(tent->rotation.y), p));
+}
+
+static Vec3 toLocal(const Object *tent, Vec3 w) {
+    return mat4_mul_dir_transposed(mat4_rotate_y(tent->rotation.y), vec3_sub(w, tent->position));
+}
+
+/* Everybody's own cover colour (by name, like their other things). */
+static Vec3 flyColor(const Object *tent) {
+    const Object *o = tent->owner;
+    if (tent->variant) return vec3(0.40f, 0.44f, 0.24f);                         /* the big one: olive */
+    if (!o || o->type == OBJ_PLAYER) return vec3(0.22f, 0.36f, 0.56f);          /* yours: blue */
+    if (!strcmp(o->name, "Toni")) return vec3(0.78f, 0.42f, 0.16f);             /* Toni's: orange */
+    return vec3(0.30f, 0.50f, 0.30f);
+}
+
+/* Its look and its box for how far it's up (built again only when that shows). */
+static void reshape(Object *tent) {
+    int big = tent->variant, steps = tent->tent_stage == TENT_POLES || tent->tent_stage == TENT_COVER ? (int)(tent->tent_work * 12.0f) : 0;
+    int shown = tent->tent_stage * 1000 + steps * 10 + tent->stored;
+    if (shown == tent->shown) return;
+    tent->shown = shown;
+    Mesh_destroy(&tent->mesh);
+    tent->mesh = Shapes_tent(big, tent->tent_stage, (float)steps / 12.0f, tent->stored, vec3(0.90f, 0.86f, 0.62f), flyColor(tent));
+    float h = tent->tent_stage == TENT_LAID ? 0.04f : tent->tent_stage == TENT_POLES ? fmaxf(0.04f, TENT_H(big) * tent->tent_work) : TENT_H(big);
+    tent->colliders[0].center = vec3(0.0f, h * 0.5f, 0.0f);
+    tent->colliders[0].half_extents = vec3(TENT_W(big) * 0.5f, h * 0.5f, TENT_L(big) * 0.5f);
+    Object_syncColliders(tent);
+}
+
+Object *Tent_unpack(World *world, Object *bag, Vec3 door_dir) {
+    if (!bag || bag->type != OBJ_GEAR || bag->variant < GEAR_TENT_SMALL || bag->is_held || bag->broken) return NULL;
+    int big = bag->variant == GEAR_TENT_BIG;
+    Vec3 at = bag->position;
+    at.y = World_groundHeight(world, at.x, at.z, at.y + 0.3f);
+    Object *tent = Object_create(OBJ_TENT, PALETTE_STANDARD, at, vec3_zero(), big ? 1.0f : 0.0f);
+    if (!tent) return NULL;
+    tent->owner = bag->owner;
+    if (big && bag->owner && !strcmp(bag->owner->name, "Volkram")) /* (the big one is Volkram's and Jana's) */
+        for (int i = 0; i < world->count; i++)
+            if (world->objects[i]->type == OBJ_HUMAN && !strcmp(world->objects[i]->name, "Jana")) tent->owner2 = world->objects[i];
+    snprintf(tent->name, sizeof(tent->name), "Tent");
+    door_dir = vec3_normalize(vec3(door_dir.x, 0.0f, door_dir.z));
+    Object_setRotation(tent, vec3(0.0f, yaw_facing(door_dir), 0.0f));
+    reshape(tent);
+    if (!World_add(world, tent)) return NULL;
+    Object_useUp(bag);
+    return tent;
+}
+
+static bool hasStone(const Object *who) {
+    for (int h = 0; h < 2; h++)
+        if (who->hands.item[h] && who->hands.state[h] == HAND_HOLDING && who->hands.item[h]->type == OBJ_STONE) return true;
+    return false;
+}
+
+int Tent_work(Object *tent, Object *who, float dt) {
+    if (tent->type != OBJ_TENT || tent->tent_stage >= TENT_UP) return -1;
+    int big = tent->variant;
+    float rate = (0.75f + 0.5f * who->wits) * (1.0f - 0.5f * fminf(who->drunk, 1.0f));
+    float time = tent->tent_stage == TENT_LAID ? LAY_TIME
+               : tent->tent_stage == TENT_POLES ? (big ? POLES_BIG : POLES_SMALL)
+               : tent->tent_stage == TENT_COVER ? (big ? COVER_BIG : COVER_SMALL) : PEG_TIME / (hasStone(who) ? 2.0f : 1.0f);
+    tent->tent_work += dt * rate / time;
+    tent->tent_idle = 0.0f;
+    int done = -1;
+    if (tent->tent_work >= 1.0f) {
+        tent->tent_work = 0.0f;
+        if (tent->tent_stage == TENT_PEGS) {
+            if (++tent->stored >= TENT_PEGS(big)) { tent->tent_stage = TENT_UP; done = TENT_UP; }
+        } else {
+            tent->tent_stage++;
+            done = tent->tent_stage;
+        }
+    }
+    reshape(tent);
+    return done;
+}
+
+const char *Tent_job(const Object *tent) {
+    switch (tent->tent_stage) {
+    case TENT_LAID:  return "spreading it out";
+    case TENT_POLES: return "putting the poles in";
+    case TENT_COVER: return "pulling the cover over";
+    case TENT_PEGS:  return "hammering the pegs in";
+    default:         return "done";
+    }
+}
+
+static Vec3 nextPeg(const Object *tent) {
+    float x, z;
+    Shapes_tentPeg(tent->variant, tent->stored, &x, &z);
+    return vec3(x, 0.0f, z);
+}
+
+Vec3 Tent_workSpot(const Object *tent, float side) {
+    const float hw = TENT_W(tent->variant) * 0.5f, hl = TENT_L(tent->variant) * 0.5f;
+    float s = side < 0.0f ? -1.0f : 1.0f;
+    switch (tent->tent_stage) {
+    case TENT_COVER: return local(tent, vec3(s * 0.5f * fabsf(side), 0.0f, -hl * 1.14f - 0.55f)); /* at the back, pulling it over */
+    case TENT_PEGS: {
+        Vec3 p = nextPeg(tent), out = vec3_normalize(vec3(p.x, 0.0f, p.z));
+        return local(tent, vec3_add(p, vec3_scale(out, 0.45f)));
+    }
+    default: return local(tent, vec3(s * (hw + 0.55f), 0.0f, 0.0f)); /* beside it */
+    }
+}
+
+Vec3 Tent_handsAt(const Object *tent, const Object *who, float clock) {
+    const int big = tent->variant;
+    const float hw = TENT_W(big) * 0.5f, hl = TENT_L(big) * 0.5f, h = TENT_H(big);
+    Vec3 me = toLocal(tent, who->position);
+    float s = me.x < 0.0f ? -1.0f : 1.0f;
+    switch (tent->tent_stage) {
+    case TENT_LAID:  return local(tent, vec3(s * hw, 0.08f, clampf(me.z, -hl, hl)));
+    case TENT_POLES: return local(tent, vec3(s * hw * 0.7f, fmaxf(0.1f, 0.55f * h * tent->tent_work), clampf(me.z, -hl * 0.5f, hl * 0.5f)));
+    case TENT_COVER: return local(tent, vec3(s * 0.3f, h * 0.8f, -hl + 2.0f * hl * tent->tent_work));
+    case TENT_PEGS: {
+        Vec3 p = nextPeg(tent);
+        p.y = 0.06f + 0.14f * fabsf(sinf(clock * 7.0f)); /* (hitting it in) */
+        return local(tent, p);
+    }
+    default: return Tent_door(tent);
+    }
+}
+
+Vec3 Tent_door(const Object *tent) {
+    return local(tent, vec3(0.0f, 0.0f, TENT_L(tent->variant) * 0.5f * 1.14f + DOOR_OUT));
+}
+
+bool Tent_isTheirs(const Object *tent, const Object *who) {
+    return tent && tent->type == OBJ_TENT && who && (tent->owner == who || tent->owner2 == who);
+}
+
+Object *Tent_of(Object *const *objects, int count, const Object *who) {
+    for (int i = 0; i < count; i++)
+        if (objects[i]->type == OBJ_TENT && Tent_isTheirs(objects[i], who)) return objects[i];
+    return NULL;
+}
+
+static int slotOf(const Object *tent, const Object *who) { return tent->owner2 == who ? 1 : 0; }
+
+bool Tent_hasBed(const Object *tent, const Object *who) {
+    return tent && (tent->tent_beds & (1 << (2 * slotOf(tent, who)))) != 0;
+}
+
+void Tent_crawlIn(Object *tent, Object *who) {
+    const int big = tent->variant;
+    float x = big ? (slotOf(tent, who) ? 0.45f : -0.45f) : 0.0f;
+    Vec3 at = local(tent, vec3(x, 0.0f, TENT_L(big) * 0.5f - 0.95f)); /* (lying back into it, the feet towards the door) */
+    who->in_tent = tent;
+    who->sitting = false;
+    who->position = vec3(at.x, tent->position.y, at.z);
+    who->rotation.y = tent->rotation.y; /* (facing the door) */
+    who->velocity = vec3_zero();
+    who->movement = vec3_zero();
+    Object_syncColliders(who);
+    who->body.last_facing = Object_facing(who); /* (lies back from where they face now: the head to the back of the tent) */
+    Body_lieDown(&who->body);
+}
+
+void Tent_crawlOut(Object *who) {
+    Object *tent = who->in_tent;
+    if (!tent) return;
+    Vec3 door = Tent_door(tent);
+    who->in_tent = NULL;
+    who->position = vec3(door.x, tent->position.y, door.z);
+    who->rotation.y = tent->rotation.y;
+    who->body.replant = true;
+    Object_syncColliders(who);
+}
+
+void Tent_update(World *world, float dt) {
+    for (int i = 0; i < world->count; i++) {
+        Object *o = world->objects[i];
+        if (o->type == OBJ_TENT) { o->tent_idle += dt; continue; }
+        if (o->is_character && o->in_tent && !o->body.asleep && o->body.pose == POSE_UP) { Tent_crawlOut(o); continue; }
+        /* a sleeping bag or a mat put down at the door of its owner's tent: in it goes */
+        if (o->type != OBJ_GEAR || o->variant > GEAR_MAT || o->is_held || o->broken || !o->owner) continue;
+        Object *tent = Tent_of(world->objects, world->count, o->owner);
+        if (!tent || tent->tent_stage < TENT_COVER) continue;
+        Vec3 door = Tent_door(tent);
+        if (hypotf(o->position.x - door.x, o->position.z - door.z) > BED_REACH) continue;
+        tent->tent_beds |= 1 << (2 * slotOf(tent, o->owner) + (o->variant == GEAR_MAT ? 1 : 0));
+        Object_useUp(o);
+    }
+}
diff --git a/entropie/src/world/tent.h b/entropie/src/world/tent.h
new file mode 100644
index 0000000..34cf5b9
--- /dev/null
+++ b/entropie/src/world/tent.h
@@ -0,0 +1,39 @@
+#ifndef WORLD_TENT_H
+#define WORLD_TENT_H
+
+/*
+ * Tents (OBJ_TENT, variant 0 small, 1 big): spread out from their bag where
+ * it lies, then pitched in stages - the poles in and up, the cover over it,
+ * the pegs hammered in - by whoever works on it (several at once: quicker).
+ * Sleeping bags and mats put down at the door go in; their owners sleep in
+ * it (in_tent: lying inside, the tent round them).
+ */
+
+#include "world/world.h"
+
+typedef enum { TENT_LAID, TENT_POLES, TENT_COVER, TENT_PEGS, TENT_UP } TentStage;
+
+/* A tent bag (OBJ_GEAR) spread out as a tent where it lies, the door facing door_dir: the tent, or NULL. */
+Object     *Tent_unpack(World *world, Object *bag, Vec3 door_dir);
+/* Somebody working on it this frame (both hands free, at it): the next bit
+   done. Returns the stage it got to when one was finished just now, else -1. */
+int         Tent_work(Object *tent, Object *who, float dt);
+/* What working on it means now ("putting the poles in") and where the hands go. */
+const char *Tent_job(const Object *tent);
+Vec3        Tent_workSpot(const Object *tent, float side);  /* where to stand (side -1 / 0 / +1) */
+Vec3        Tent_handsAt(const Object *tent, const Object *who, float clock);
+/* In front of the door, and its tent's sleepers' places inside. */
+Vec3        Tent_door(const Object *tent);
+/* Whose: the owner, or (the big one) who shares it. */
+bool        Tent_isTheirs(const Object *tent, const Object *who);
+/* Somebody's tent (spread out or pitched), or NULL. */
+Object     *Tent_of(Object *const *objects, int count, const Object *who);
+/* Their sleeping bag in it. */
+bool        Tent_hasBed(const Object *tent, const Object *who);
+/* Into it to sleep (lying in their place) / out again in front of the door. */
+void        Tent_crawlIn(Object *tent, Object *who);
+void        Tent_crawlOut(Object *who);
+/* Every frame: sleeping bags and mats put down at the door go in; whoever woke up in a tent comes out. */
+void        Tent_update(World *world, float dt);
+
+#endif /* WORLD_TENT_H */
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index d9e7133..363cf5b 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -2,6 +2,7 @@
 #include "world/camp.h"
 #include "world/clothes.h"
 #include "world/car.h"
+#include "world/tent.h"
 #include "people/pockets.h"
 #include "people/condition.h"
 #include "people/dog.h"
@@ -101,7 +102,7 @@ float World_groundHeight(const World *world, float x, float z, float from_y) {
     int near[NEAR_MAX], nn = nearby(world, x, z, 0.0f, near);
     for (int q = 0; q < (nn < 0 ? world->count : nn); q++) {
         const Object *o = world->objects[nn < 0 ? q : near[q]];
-        if (o->is_character || o->is_held) continue;
+        if (o->is_character || o->is_held || o->type == OBJ_TENT) continue; /* (nobody stands on a tent: it'd give) */
         for (int c = 0; c < o->collider_count; c++) {
             const Collider *col = &o->colliders[c];
             Vec3 center = Object_colliderCenter(o, c);
@@ -732,7 +733,7 @@ static void dressForNight(World *world, Object *o, float dt) {
     if (o->type == OBJ_DOG) return;
     bool night = world->hour >= 21.0f || world->hour < 7.0f;
     bool by_fire = Camp_fireNear(world->objects, world->count, o->position, 4.0f, true) != NULL;
-    float cold = !night ? -2.0f : by_fire ? -1.0f : 1.0f - 0.8f * fminf(o->drunk, 1.0f);
+    float cold = !night ? -2.0f : by_fire || (o->in_tent && Tent_hasBed(o->in_tent, o)) ? -1.0f : 1.0f - 0.8f * fminf(o->drunk, 1.0f);
     o->chill = clampf(o->chill + cold * dt / 60.0f, 0.0f, 1.0f); /* (about a minute to feel it) */
     bool player = o->type == OBJ_PLAYER;
     if (o->look.top != TOP_JACKET && o->chill > 0.6f && !(player && o->jacket_told)) {
@@ -1232,6 +1233,7 @@ void World_update(World *world, float dt, float game_dt) {
     world->dt_props = dt;
     Clothes_update(world);
     Car_serve(world);
+    Tent_update(world, dt);
     showProps(world); /* (who's a person, where the fires are: looked up again and again) */
     for (int i = 0; i < world->count; i++) {
         Object *o = world->objects[i];