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

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.95.0: people sit at the tables, find their way into buildings

Christian Immanuel · 2026-10-19 16:23 · ff527bffd0396566a15506faeb958957db425b82

- People now and then sit down at a table (the meeting table, the kitchen
  tables) for a while and have a word with whoever sits there; food they
  pick up they mostly eat there, a beer sometimes too
- They find their way into the buildings: the walking map knows floors and
  steps (before, every floor counted as a wall), and they walk up steps
  instead of round them
- Nobody stands stuck under a small tree's crown or squeezed against a box
  any more: they step out the way that's free
- Sitting at a table no longer ends in a fall (the feet under the table
  counted as stepping over something)
- Felling: look down at the trunk to cut right there, look up the trunk
  above your head to cut higher (up to the chest); straight ahead stays hip high
 entropie/Makefile            |   2 +-
 entropie/README.md           |   6 +-
 entropie/STATE.md            |  13 ++-
 entropie/commit_message.txt  |  24 ++---
 entropie/src/people/ai.c     | 206 ++++++++++++++++++++++++++++++++++++++++++-
 entropie/src/people/ai.h     |   4 +
 entropie/src/world/nav.c     |  67 ++++++++++++--
 entropie/src/world/terrain.c |   7 +-
 entropie/src/world/terrain.h |   2 +
 entropie/src/world/tools.c   |   8 +-
 entropie/src/world/world.c   |   1 +
 11 files changed, 308 insertions(+), 32 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index c68f953..2633686 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.94.0
+VERSION   := 1.95.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 47913ad..5d18828 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -102,6 +102,9 @@ and satellite pictures (about 340 x 340 m).
 - Hold food close to the fire to grill it: sausages (from the cool box) are
   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
+- Now and then they sit down at a table (the meeting table, the kitchens) for
+  a while and have a word; food they pick up they mostly eat there, a beer
+  sometimes too. They find their way in: up the steps, through the doors
 - C: sit down yourself (and rest faster): on a bench or chair (right on
   the seat; a chair is pulled out first, shuffled in to the table once you sit,
   and pushed back in after you leave) if one is next
@@ -334,7 +337,8 @@ An axe, pruning shears and a chainsaw are in the tool room of your garages; the
 | Split a short log or half | 3 | – | – | – |
 | Chop out a stump | 14 | 7 | – | – |
 
-A tree is cut at about hip height (lower if you look lower), where you started
+A tree is cut at about hip height: look down at the trunk to cut right there,
+look up the trunk (above your head) to cut higher, up to the chest; where you started
 its notch; strokes elsewhere start a new notch from scratch. It falls beside its
 stump and lies on the ground.
 
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 313a383..d29d011 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -122,6 +122,13 @@ Round 1.94:
 - Fallen trees (`forestry.c`): once down (not stopped by something) loose (`is_static` false), weight `Object_treeWeight` (wood + TREE_BRANCH_KG 18 per branch). Grabbing uses `Object_dragWeight`: lying on the ground TREE_DRAG 0.5 x weight + TREE_SNAG_KG 25 per branch. Only the trunk collider meets the ground. Dragged with the crown against something static: `snag` adds up (fades 0.5/s when free), the tree is held; at SNAG_HOLD 1 s (or SNAG_SPEED 0.9 m/s) a branch snaps (`dropBranch`, chips).
 - Carried ladders and walls (`world.c`): a raised ladder running into something tips down flat (`dipped`) only if that gets it out (a beam, a doorway); else it is stood up straight (`steep`, RAISE_HIGH) until it would stand at 1.36 clear again. Carried at the side, a ladder in a wall slides along in the hand (`grip_shift`, SLIDE_STEP 0.1 m, at least SLIDE_KEEP 0.35 m from its end; back to the middle once clear) before the walk is stopped: taken off a wall, walked into one.
 
+Round 1.95:
+- At a table (`ai.c` AI_TABLE): a free chair standing at a table (`chairAtTable`: within 0.8 m of its edge, facing it, same floor; not anyone's `seat` or target) within TABLE_WALK 35 m. Food picked up goes there TABLE_EAT 0.75 of the time, a beer TABLE_BEER 0.3; idle with no fire TABLE_SIT 0.06 a decision. Walk there (`ai_steer`), within TABLE_SEAT 0.5 m `Object_sit`; sitting TABLE_STAY 25-70 s: a bite or sip every 3-5 s (`table_pause`), done: on the table (`Camp_doneWithBottle`); a word with someone sitting within 2.6 m every ~14 s, they answer; looks at them. Up when done and the time is over, or bladder 0.8 / sleepy 0.85 / hungry or thirsty 0.6 with nothing in hand. Chair taken on the way: another one, else eat standing.
+- Walking map (`nav.c`): per cell `g_floor`, where one stands: the ground, or the top of a building's level box (OBJ_WALL, not a roof, at least 0.2 m across, up to WALK_ON_MOST 1.2 m over `Terrain_naturalHeight`, the ground without pits). Blocking is measured from there (floors over pits were blocked: no way into any building). A* and the straightening don't go up or down more than NAV_RISE 0.38 m a cell (x1.42 diagonal): stairs yes, floor edges no.
+- Steering (`ai.c` ai_blockedBy): a building's floor or step up to STAIR_FIRST 0.3 + STAIR_SLOPE 0.65 x distance over the feet is walked up, not round; a standing tree blocks with its trunk only (the crown boxed people in under small trees). Squeezed (their own spot within clearance of something): out the way that's free 2/3 and 1 x the look ahead (`ai_steerPast`).
+- Sitting: no half-over-something falls (`world.c` halfOver clears `crossing` while sitting; feet under the table started a step over).
+- Felling cut (`tools.c`): looking lower than hip height, right where you look; between that and AIM_UP_FROM 0.3 m over the eyes, hip height; higher, the cut goes up as much (clamped to the tool's range: axe 0.45-1.35 m).
+
 Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves.
 
 Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built); the coat rack (FURN_COATRACK, in your kitchen by the doors, garage coords 20.6 / 2.4) shows the jackets hanging on it (`Shapes_coatRack`, up to 6, a bitmask of whose in `shown`). `jacket_hung` (characters, true at the start): cold (chill 0.6+) and the jacket with them: on it goes (as before); hanging: NPCs `ai.want_jacket` -> `ai.c` fetchJacket (idle / wander: goTo the rack, on with it, "Ah, better."; gives up after 90 s or warm again); you are told once ("your jacket hangs on the rack in your garage") and click the rack (`game_actions.c` coatRack: on, or back onto it). Warm again: it comes off and stays with them.
@@ -254,9 +261,7 @@ 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)**
-- NPC pathfinding has problems (which ones: to look at with examples)
-- NPCs sitting at the meeting table (chairs work for them, nothing sends them there yet); eating at a table
-- Felling: aiming could set the cut height more freely
+- NPC pathfinding: buildings, steps and squeezes fixed in 1.95; the basement (under a floor) isn't on the walking map 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
@@ -279,4 +284,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.94.0 (build 83ab53f)`
+State of `entropie 1.95.0 (build f8a9b47)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index 6a09c1c..76f77ad 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,12 +1,14 @@
-entropie 1.94.0: Alice fetches, fallen trees drag, ladders stay out of walls
+entropie 1.95.0: people sit at the tables, find their way into buildings
 
-- Alice runs after what you throw and brings it back to your feet
-  (sometimes catching it in the air); a dropped sausage she eats, an apple
-  she keeps and chews lying somewhere
-- Felled trees lying on the ground can be pushed or pulled; their branches
-  make them heavy and snag on things in the way, and snap off when you
-  keep pulling. Without branches they move much more easily
-- A ladder taken off a wall no longer pokes into it: carried at the side it
-  slides back in your hand; walking into a wall stops you
-- A slid-out ladder walked into a wall is stood up straight instead of
-  tipping flat through it (it still tips down under a beam or doorway)
+- People now and then sit down at a table (the meeting table, the kitchen
+  tables) for a while and have a word with whoever sits there; food they
+  pick up they mostly eat there, a beer sometimes too
+- They find their way into the buildings: the walking map knows floors and
+  steps (before, every floor counted as a wall), and they walk up steps
+  instead of round them
+- Nobody stands stuck under a small tree's crown or squeezed against a box
+  any more: they step out the way that's free
+- Sitting at a table no longer ends in a fall (the feet under the table
+  counted as stepping over something)
+- Felling: look down at the trunk to cut right there, look up the trunk
+  above your head to cut higher (up to the chest); straight ahead stays hip high
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 3c92f0b..481ede4 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -110,6 +110,9 @@ static bool longToStepOnto(const Object *self, const Object *o) {
     return longest >= ONTO_LONG && top - self->position.y <= ONTO_HIGH;
 }
 
+#define STAIR_FIRST 0.3f  /* m: a step up right here ... */
+#define STAIR_SLOPE 0.65f /* ... and this much more per m further on: stairs, not a wall */
+
 bool ai_blockedBy(const Object *self, const Object *ignore, const Object *ignore2,
                       Object *const *objects, int count, Vec3 p) {
     Object *near[512]; /* (only what's near p, from the cell grid) */
@@ -140,13 +143,18 @@ bool ai_blockedBy(const Object *self, const Object *ignore, const Object *ignore
             if (flatDistance(o->position, p) < PERSON_RADIUS) return true;
             continue;
         }
-        for (int c = 0; c < o->collider_count; c++) {
+        int parts = o->type == OBJ_TREE && !o->toppled ? 1 : o->collider_count; /* (a standing tree: its trunk; under the crown you just duck) */
+        for (int c = 0; c < parts; c++) {
             /* Low things (a woodpile's sleepers, a stump) are stepped over */
             const Collider *box = &o->colliders[c];
             if (flatDistance(Object_colliderCenter(o, c), p) > vec3_length(box->half_extents) + BODY_RADIUS) continue;
             float top = Object_colliderCenter(o, c).y + fabsf(box->axes[0].y) * box->half_extents.x +
                         fabsf(box->axes[1].y) * box->half_extents.y + fabsf(box->axes[2].y) * box->half_extents.z;
             if (top < self->position.y + 0.25f) continue;
+            /* A building's floor or steps: walked up (a stair rises about STAIR_SLOPE) */
+            if (o->type == OBJ_WALL && o->variant != 3 && box->axes[1].y > 0.99f && box->half_extents.x >= 0.1f && box->half_extents.z >= 0.1f &&
+                top <= self->position.y + STAIR_FIRST + STAIR_SLOPE * flatDistance(self->position, p))
+                continue;
             /* Knee, waist and head height above the feet */
             for (int k = 0; k < 3; k++) {
                 Vec3 q = vec3(p.x, self->position.y + 0.4f + 0.6f * (float)k, p.z);
@@ -202,6 +210,16 @@ Vec3 ai_steerPast(Object *self, const Object *ignore, const Object *ignore2,
             }
         }
     }
+    /* Already too close to something (squeezed in by a box, a trunk): out the way that's free further on */
+    if (ai_blockedBy(self, ignore, ignore2, objects, count, self->position)) {
+        for (unsigned i = 0; i < sizeof(DETOURS) / sizeof(DETOURS[0]); i++)
+            for (int s = 0; s < (i ? 2 : 1); s++) {
+                Vec3 d = rotateY(dir, deg2rad(DETOURS[i]) * (s ? -ai->detour : ai->detour));
+                if (!ai_blockedBy(self, ignore, ignore2, objects, count, vec3_add(self->position, vec3_scale(d, ahead * 2.0f / 3.0f))) &&
+                    !ai_blockedBy(self, ignore, ignore2, objects, count, vec3_add(self->position, vec3_scale(d, ahead))))
+                    return d;
+            }
+    }
     /* Boxed in: next time try the other side first */
     ai->detour = -ai->detour;
     ai->steer_hold = 0.0f;
@@ -289,6 +307,168 @@ static void rest(Ai *ai, float min, float max) {
     ai->timer = min + (max - min) * random01(ai);
 }
 
+/* ---------- At a table ---------- */
+
+#define TABLE_WALK     35.0f /* m: a free chair at a table this near is worth the walk */
+#define TABLE_EAT      0.75f /* chance food they pick up is eaten sitting at a table (a free chair there) ... */
+#define TABLE_BEER     0.3f  /* ... a beer drunk there */
+#define TABLE_SIT      0.06f /* idle (no fire): now and then over to a table to sit a while */
+#define TABLE_STAY_MIN 25.0f
+#define TABLE_STAY_MAX 70.0f
+#define TABLE_CHAT     14.0f /* s between words with others sitting there */
+#define TABLE_NEAR     2.6f  /* m: sitting this close counts as at the same table */
+#define TABLE_SEAT     0.5f  /* m: within this of the chair they sit down */
+
+#define PICK_ONE(list, ai) (list)[(int)(random01(ai) * (float)(sizeof(list) / sizeof((list)[0]))) % (int)(sizeof(list) / sizeof((list)[0]))]
+static const char *const TABLE_OPEN[]  = { "Ah, good to sit for a bit.", "Mind if I join you?", "That's better.", "Nice table, this." };
+static const char *const TABLE_EATING[] = { "Mmh. Not bad at all.", "I was starving.", "Good stuff.", "Want a bite?" };
+static const char *const TABLE_REPLY[] = { "Mhm.", "Sure, sit down.", "Ha, yes.", "Enjoy.", "Right?" };
+
+/* A chair standing at a table (facing it, on the same floor). */
+static bool chairAtTable(const Object *chair, Object *const *objects, int count) {
+    Vec3 front = chair->colliders[0].axes[2];
+    for (int i = 0; i < count; i++) {
+        const Object *t = objects[i];
+        if (t->type != OBJ_FURNITURE || t->variant != FURN_TABLE || t->collider_count == 0) continue;
+        if (fabsf(t->position.y - chair->position.y) > 0.4f) continue;
+        const Collider *c = &t->colliders[0];
+        Vec3 rel = vec3_sub(chair->position, Object_colliderCenter(t, 0));
+        if (fabsf(vec3_dot(rel, c->axes[0])) < c->half_extents.x + 0.8f && fabsf(vec3_dot(rel, c->axes[2])) < c->half_extents.z + 0.8f &&
+            vec3_dot(front, rel) < 0.0f)
+            return true;
+    }
+    return false;
+}
+
+static bool chairTaken(const Object *self, const Object *chair, Object *const *objects, int count) {
+    for (int i = 0; i < count; i++) {
+        const Object *c = objects[i];
+        if (c == self || !c->is_character) continue;
+        if (c->seat == chair || (c->type == OBJ_HUMAN && c->ai.mode == AI_TABLE && c->ai.target == chair)) return true;
+    }
+    return false;
+}
+
+/* The nearest free chair at a table (another floor counts as further). */
+static Object *freeTableChair(const Object *self, Object *const *objects, int count, float range) {
+    Object *best = NULL;
+    float best_d = range;
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o->type != OBJ_FURNITURE || o->variant != FURN_CHAIR || ai_skipped(&self->ai, o)) continue;
+        float d = flatDistance(o->position, self->position) + 3.0f * fabsf(o->position.y - self->position.y);
+        if (d >= best_d || chairTaken(self, o, objects, count) || !chairAtTable(o, objects, count)) continue;
+        best_d = d;
+        best = o;
+    }
+    return best;
+}
+
+static void goToTable(Object *self, Object *chair) {
+    Ai *ai = &self->ai;
+    ai->mode = AI_TABLE;
+    ai->target = chair;
+    ai->timer = 0.0f;
+    ai->table_stay = TABLE_STAY_MIN + (TABLE_STAY_MAX - TABLE_STAY_MIN) * random01(ai);
+    ai->table_pause = DRINK_PAUSE;
+    ai->table_chat = 2.0f + 4.0f * random01(ai);
+    setGoal(ai, chair->position);
+}
+
+/* Food or a bottle in the main hand (what they eat or drink at the table). */
+static Object *mealInHand(const Object *self) {
+    const Hands *h = &self->hands;
+    Object *m = h->state[self->main_hand] == HAND_HOLDING ? h->item[self->main_hand] : NULL;
+    return m && (Object_isFood(m) || Object_isBottle(m)) ? m : NULL;
+}
+
+static void leaveTable(Object *self) {
+    self->sitting = false; /* (gets up: Object_leaveSeat) */
+    self->ai.target = NULL;
+    rest(&self->ai, 1.0f, 3.0f);
+}
+
+/* Somebody else sitting at the same table. */
+static Object *tableMate(const Object *self, Object *const *objects, int count) {
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o != self && o->is_character && o->sitting && o->seat && o->seat->type == OBJ_FURNITURE &&
+            flatDistance(o->position, self->position) < TABLE_NEAR && fabsf(o->position.y - self->position.y) < 0.5f)
+            return o;
+    }
+    return NULL;
+}
+
+static void updateTable(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    Object *chair = ai->target;
+    Object *meal = mealInHand(self);
+    const int hand = self->main_hand;
+    if (self->hands.state[hand] == HAND_REACHING) { stand(self); return; } /* (still picking it up) */
+    if (!chair) { leaveTable(self); return; }
+
+    /* Over to the chair, then sit down on it */
+    if (!self->sitting || self->seat != chair) {
+        if (chairTaken(self, chair, objects, count)) { /* somebody was quicker: another one, or eat standing */
+            Object *other = freeTableChair(self, objects, count, TABLE_WALK * 0.5f);
+            if (other) { ai->target = other; setGoal(ai, other->position); return; }
+            ai->target = NULL;
+            if (meal) { ai->mode = AI_DRINK; ai->timer = 0.0f; ai->target = meal; } else rest(ai, 1.0f, 2.0f);
+            return;
+        }
+        ai->timer += dt;
+        if (ai->timer > FETCH_TIMEOUT * 2.0f) { /* can't get there */
+            skipTarget(ai, chair);
+            ai->target = NULL;
+            if (meal) { ai->mode = AI_DRINK; ai->timer = 0.0f; ai->target = meal; } else rest(ai, 1.0f, 2.0f);
+            return;
+        }
+        float gap = Hands_gapFrom(self->position, self->look.height, chair);
+        if (gap < TABLE_SEAT && fabsf(chair->position.y - self->position.y) < 0.4f) {
+            stand(self);
+            Object_sit(self, chair, self->position);
+            ai->timer = 0.0f;
+            return;
+        }
+        Vec3 dir = ai_steer(self, chair, objects, count, chair->position);
+        if (vec3_length(dir) < 0.5f) { stand(self); return; }
+        ai_walk(self, dir, fminf(WALK_SPEED * 0.7f, gap * 1.5f + 0.3f), dt);
+        return;
+    }
+
+    stand(self);
+    if (self->sit_moving) return; /* (sitting down) */
+    ai->timer += dt;
+    Object *mate = tableMate(self, objects, count);
+    if (mate) { /* look at them now and then */
+        Vec3 to = vec3_sub(vec3_add(mate->position, vec3(0.0f, mate->look.height * 0.9f, 0.0f)),
+                           vec3_add(self->position, vec3(0.0f, self->look.height * 0.6f, 0.0f)));
+        self->body.look_dir = vec3_normalize(to);
+        self->body.look_weight = fmodf(ai->timer, 9.0f) < 4.0f ? 0.7f : 0.0f;
+    }
+
+    /* A bite or a sip now and then */
+    if (meal && !Hands_isDrinking(self) && (ai->table_pause -= dt) <= 0.0f) {
+        ai->table_pause = DRINK_PAUSE * (Object_isFood(meal) ? 2.5f : 4.0f) * (0.7f + 0.6f * random01(ai));
+        if (ai->sips_left > 0 && meal->sips > 0 && Hands_drink(self)) ai->sips_left--;
+        else if (Camp_doneWithBottle(self, hand, objects, count, random01(ai)) == DONE_NONE) Hands_drop(self, hand); /* (on the table, mostly) */
+    }
+
+    /* A word with the others */
+    if ((ai->table_chat -= dt) <= 0.0f) {
+        ai->table_chat = TABLE_CHAT * (0.7f + 0.6f * random01(ai));
+        if (mate && ai->speech_time <= 0.0f && !mate->ai.talking) {
+            Ai_say(self, meal && Object_isFood(meal) ? PICK_ONE(TABLE_EATING, ai) : PICK_ONE(TABLE_OPEN, ai));
+            if (mate->type == OBJ_HUMAN && mate->ai.speech_time <= 0.0f && random01(ai) < 0.6f) Ai_say(mate, PICK_ONE(TABLE_REPLY, ai));
+        }
+    }
+
+    /* Up again: sat long enough (and finished), or something more pressing */
+    const Needs *n = &self->needs;
+    bool pressing = n->bladder > 0.8f || n->sleepiness > 0.85f || (!meal && (n->hunger > 0.6f || n->thirst > 0.6f));
+    if (pressing || (ai->timer > ai->table_stay && !meal)) leaveTable(self);
+}
+
 /* Nearest loose bottle (or food) with something left that nobody else is going for.
    Thirsty people prefer water. */
 static Object *findConsumable(const Object *self, Object *const *objects, int count, Want want) {
@@ -499,6 +679,16 @@ static void updateIdle(Object *self, Object *const *objects, int count, float dt
         return;
     }
 
+    /* Over to a table to sit a while */
+    if (!fire && random01(ai) < TABLE_SIT) {
+        Object *chair = freeTableChair(self, objects, count, TABLE_WALK);
+        if (chair) {
+            goToTable(self, chair);
+            ai->sips_left = 0;
+            return;
+        }
+    }
+
     Object *bottle = NULL;
     if (hands_free && self->drunk < DRINK_MAX_DRUNK && random01(ai) < DRINK_CHANCE)
         bottle = findConsumable(self, objects, count, WANT_BEER);
@@ -554,6 +744,15 @@ static void updateFetch(Object *self, Object *const *objects, int count, float d
             /* Beer: a sip or two. Thirsty or hungry: until it's better. */
             bool need = self->needs.thirst > 0.3f || Object_isFood(bottle);
             ai->sips_left = need ? bottle->sips : 1 + (random01(ai) < 0.5f ? 1 : 0);
+            /* food (and now and then a beer) at a table if there's a chair free */
+            if (random01(ai) < (Object_isFood(bottle) ? TABLE_EAT : TABLE_BEER)) {
+                Object *chair = freeTableChair(self, objects, count, TABLE_WALK);
+                if (chair) {
+                    int sips = ai->sips_left;
+                    goToTable(self, chair);
+                    ai->sips_left = sips;
+                }
+            }
         }
         return;
     }
@@ -1144,7 +1343,7 @@ static void listen(Object *self, Object *const *objects, int count, float dt) {
 /* Waiting is fine here: for the leader, by the fire, on a break, for an answer. */
 static bool waitingIsFine(const Object *self) {
     const Ai *ai = &self->ai;
-    return ai->mode == AI_IDLE || ai->mode == AI_HELP || ai->mode == AI_FOLLOW || ai->mode == AI_DRINK ||
+    return ai->mode == AI_IDLE || ai->mode == AI_HELP || ai->mode == AI_FOLLOW || ai->mode == AI_DRINK || (ai->mode == AI_TABLE && self->sitting) ||
            (ai->mode == AI_APPROACH && ai->ready_to_talk) || ai->talking || self->sitting || self->body.asleep ||
            ai->on_break > 0.0f || ai->ponder > 0.0f || ai->tool_waiting;
 }
@@ -1409,7 +1608,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     smokeBreak(self, objects, count, dt);
     if (ai_climbing(self, dt)) return; /* (up or down a ladder, over an edge: that first) */
     if (stepAside(self, dt)) return;   /* (a pushed load coming at us) */
-    if (ai->mode != AI_IDLE && !(ai->mode == AI_WORK && ai->order == ORDER_FIRE)) self->sitting = false; /* up again to do anything */
+    if (ai->mode != AI_IDLE && ai->mode != AI_TABLE && !(ai->mode == AI_WORK && ai->order == ORDER_FIRE)) self->sitting = false; /* up again to do anything */
     smell(self, objects, count, dt);
     if (ai->talking) {
         listen(self, objects, count, dt);
@@ -1447,6 +1646,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     case AI_WORK:     ai_updateWork(self, objects, count, dt);     break;
     case AI_FOLLOW:   ai_updateFollow(self, objects, count, dt);   break;
     case AI_APPROACH: updateApproach(self, objects, count, dt); break;
+    case AI_TABLE:    updateTable(self, objects, count, dt);    break;
     }
     watchdog(self);
 }
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index da6e775..413f01f 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -62,6 +62,7 @@ typedef enum {
     AI_WORK,     /* on an order: firewood (and more trees) on their own */
     AI_FOLLOW,   /* being shown something by the player */
     AI_APPROACH, /* walks up to the player to say something */
+    AI_TABLE,    /* over to a free chair at a table: sits a while, eats or drinks what they have there */
 } AiMode;
 
 typedef enum { ORDER_NONE, ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE, ORDER_COUNT } Order;
@@ -122,6 +123,9 @@ typedef struct {
     float          stuck;     /* seconds without getting closer to the goal */
     float          closest;
     int            sips_left; /* AI_DRINK: sips still wanted */
+    float          table_stay; /* AI_TABLE: seconds they sit there ... */
+    float          table_pause; /* ... till the next bite or sip ... */
+    float          table_chat;  /* ... and the next word with the others there */
     bool           resting;   /* AI_HELP: worn out, catching breath */
     struct Object *tool;      /* AI_HELP: tool we are going to fetch for the leader's job */
     struct Object *copied;    /* AI_HELP: the leader's item we copy (got one like it, do the same with it) */
diff --git a/entropie/src/world/nav.c b/entropie/src/world/nav.c
index 2ec2654..85ba462 100644
--- a/entropie/src/world/nav.c
+++ b/entropie/src/world/nav.c
@@ -1,7 +1,9 @@
 /*
  * Walking map: a grid over the site, a cell blocked where something fixed
- * stands between knee and head height. Ways are found with A* (8 directions,
- * no cutting corners) and then straightened: only the turns are kept.
+ * stands between knee and head height over where one stands there (the
+ * ground, or a building's floor or step). Ways are found with A* (8
+ * directions, no cutting corners, no more up or down from cell to cell than a
+ * step or two) and then straightened: only the turns are kept.
  */
 #include "world/nav.h"
 
@@ -15,8 +17,11 @@
 #define HEADROOM      1.8f   /* higher than this: walked under */
 #define MOST_STEPS    90000  /* cells looked at for one way, at most */
 #define FREE_SEARCH   6      /* cells round a blocked start or end to find a free one */
+#define WALK_ON_MOST  1.2f   /* building floors and steps (OBJ_WALL) up to this over the natural ground are walked on ... */
+#define NAV_RISE      0.38f  /* ... from one cell to the next at most this much up or down (a stair: 1-2 steps) */
 
 static unsigned char *g_blocked;
+static float         *g_floor; /* where one stands in each cell: the ground, or a floor / step of a building */
 static bool           g_ready;
 static double         g_signature = -1.0;
 
@@ -33,6 +38,8 @@ static int cellOf(float v) { return (int)floorf((v + TERRAIN_SIZE * 0.5f) / NAV_
 static float centerOf(int i) { return ((float)i + 0.5f) * NAV_CELL - TERRAIN_SIZE * 0.5f; }
 static bool inside(int x, int z) { return x >= 0 && z >= 0 && x < NAV_N && z < NAV_N; }
 static bool open(int x, int z) { return inside(x, z) && !g_blocked[z * NAV_N + x]; }
+/* From one cell to the next: no more up or down than a step or two. */
+static bool canStep(int a, int b, bool diagonal) { return fabsf(g_floor[a] - g_floor[b]) <= (diagonal ? NAV_RISE * 1.42f : NAV_RISE); }
 
 /* Point in the collider, with clearance sideways only (not up and down). */
 static bool inCollider(const Object *o, int i, Vec3 p) {
@@ -76,7 +83,38 @@ static CellRect boundsOf(const Object *o) {
 
 static bool overlaps(CellRect a, CellRect b) { return a.x0 <= b.x1 && b.x0 <= a.x1 && a.z0 <= b.z1 && b.z0 <= a.z1; }
 
-static void blockCollider(const Object *o, int i, const Terrain *terrain, CellRect clip) {
+/* A building's floor slab or step (level, its top within reach of the natural ground): stood on. */
+static bool walkOn(const Object *o, int i, const Terrain *terrain, float *top) {
+    if (o->type != OBJ_WALL || o->variant == 3) return false; /* (building parts; not roofs) */
+    const Collider *c = &o->colliders[i];
+    if (fabsf(c->axes[1].y) < 0.99f || c->half_extents.x < 0.1f || c->half_extents.z < 0.1f) return false;
+    Vec3 center = Object_colliderCenter(o, i);
+    *top = center.y + c->half_extents.y;
+    return *top <= Terrain_naturalHeight(terrain, center.x, center.z) + WALK_ON_MOST;
+}
+
+static void raiseFloor(const Object *o, int i, const Terrain *terrain, CellRect clip) {
+    float top;
+    if (!walkOn(o, i, terrain, &top)) return;
+    const Collider *c = &o->colliders[i];
+    Vec3 center = Object_colliderCenter(o, i);
+    float ex = fabsf(c->axes[0].x) * c->half_extents.x + fabsf(c->axes[2].x) * c->half_extents.z;
+    float ez = fabsf(c->axes[0].z) * c->half_extents.x + fabsf(c->axes[2].z) * c->half_extents.z;
+    int x0 = cellOf(center.x - ex), x1 = cellOf(center.x + ex), z0 = cellOf(center.z - ez), z1 = cellOf(center.z + ez);
+    if (x0 < clip.x0) x0 = clip.x0;
+    if (z0 < clip.z0) z0 = clip.z0;
+    if (x1 > clip.x1) x1 = clip.x1;
+    if (z1 > clip.z1) z1 = clip.z1;
+    for (int z = z0; z <= z1; z++)
+        for (int x = x0; x <= x1; x++) {
+            Vec3 rel = vec3_sub(vec3(centerOf(x), center.y, centerOf(z)), center);
+            if (fabsf(vec3_dot(rel, c->axes[0])) > c->half_extents.x || fabsf(vec3_dot(rel, c->axes[2])) > c->half_extents.z) continue;
+            float *f = &g_floor[z * NAV_N + x];
+            if (top > *f) *f = top;
+        }
+}
+
+static void blockCollider(const Object *o, int i, CellRect clip) {
     const Collider *c = &o->colliders[i];
     Vec3 center = Object_colliderCenter(o, i);
     float ex = 0.0f, ey = 0.0f, ez = 0.0f;
@@ -96,7 +134,7 @@ static void blockCollider(const Object *o, int i, const Terrain *terrain, CellRe
         for (int x = x0; x <= x1; x++) {
             if (g_blocked[z * NAV_N + x]) continue;
             float px = centerOf(x), pz = centerOf(z);
-            float ground = Terrain_height(terrain, px, pz);
+            float ground = g_floor[z * NAV_N + x];
             if (center.y + ey < ground + STEP_OVER || center.y - ey > ground + HEADROOM) continue;
             for (int k = 0; k < 3; k++)
                 if (inCollider(o, i, vec3(px, ground + 0.4f + 0.6f * (float)k, pz))) {
@@ -126,11 +164,19 @@ static NavEntry *slotFor(NavEntry *table, const Object *o, bool *found) {
 
 /* Clears the cells in r and blocks them again from every fixed thing over them. */
 static void redo(Object *const *objects, int count, const Terrain *terrain, CellRect r) {
-    for (int z = r.z0; z <= r.z1; z++) memset(&g_blocked[z * NAV_N + r.x0], 0, (size_t)(r.x1 - r.x0 + 1));
+    for (int z = r.z0; z <= r.z1; z++) {
+        memset(&g_blocked[z * NAV_N + r.x0], 0, (size_t)(r.x1 - r.x0 + 1));
+        for (int x = r.x0; x <= r.x1; x++) g_floor[z * NAV_N + x] = Terrain_height(terrain, centerOf(x), centerOf(z));
+    }
+    for (int i = 0; i < count; i++) { /* floors and steps first: what's in the way is from there up */
+        const Object *o = objects[i];
+        if (!o->is_static || o->type != OBJ_WALL || !overlaps(boundsOf(o), r)) continue;
+        for (int c = 0; c < o->collider_count; c++) raiseFloor(o, c, terrain, r);
+    }
     for (int i = 0; i < count; i++) {
         const Object *o = objects[i];
         if (!o->is_static || o->collider_count == 0 || !overlaps(boundsOf(o), r)) continue;
-        for (int c = 0; c < o->collider_count; c++) blockCollider(o, c, terrain, r);
+        for (int c = 0; c < o->collider_count; c++) blockCollider(o, c, r);
     }
 }
 
@@ -147,13 +193,14 @@ void Nav_update(Object *const *objects, int count, const Terrain *terrain) {
     if (!g_blocked) {
         size_t n = (size_t)NAV_N * NAV_N;
         g_blocked = calloc(n, 1);
+        g_floor = calloc(n, sizeof *g_floor);
         g_cost = malloc(n * sizeof *g_cost);
         g_from = malloc(n * sizeof *g_from);
         g_seen = calloc(n, sizeof *g_seen);
         g_done = calloc(n, sizeof *g_done);
         g_heap_cap = MOST_STEPS * 8;
         g_heap = malloc((size_t)g_heap_cap * sizeof *g_heap);
-        if (!g_blocked || !g_cost || !g_from || !g_seen || !g_done || !g_heap) return;
+        if (!g_blocked || !g_floor || !g_cost || !g_from || !g_seen || !g_done || !g_heap) return;
     }
     if (!g_table) {
         g_table = calloc(NAV_TABLE, sizeof *g_table);
@@ -263,11 +310,14 @@ static float guess(int x, int z, int gx, int gz) {
 /* Straight line between two cells without touching a blocked one. */
 static bool clearLine(int ax, int az, int bx, int bz) {
     float dx = (float)(bx - ax), dz = (float)(bz - az);
-    int steps = (int)(fmaxf(fabsf(dx), fabsf(dz)) * 3.0f) + 1;
+    int steps = (int)(fmaxf(fabsf(dx), fabsf(dz)) * 3.0f) + 1, last = az * NAV_N + ax;
     for (int s = 1; s <= steps; s++) {
         float t = (float)s / (float)steps;
         float fx = (float)ax + 0.5f + dx * t, fz = (float)az + 0.5f + dz * t;
         if (!open((int)floorf(fx), (int)floorf(fz))) return false;
+        int here = (int)floorf(fz) * NAV_N + (int)floorf(fx);
+        if (here != last && !canStep(last, here, true)) return false; /* (not off the edge of a floor) */
+        last = here;
         /* (a little to each side too: no grazing corners) */
         if (!open((int)floorf(fx + 0.3f), (int)floorf(fz + 0.3f)) || !open((int)floorf(fx - 0.3f), (int)floorf(fz - 0.3f)))
             return false;
@@ -306,6 +356,7 @@ int Nav_findPath(Vec3 from, Vec3 to, Vec3 *out, int max) {
             if (!open(nx, nz)) continue;
             if (k >= 4 && (!open(x + DX[k], z) || !open(x, z + DZ[k]))) continue; /* no cutting corners */
             int n = nz * NAV_N + nx;
+            if (!canStep(cell, n, k >= 4)) continue; /* (up or down a floor's edge: no) */
             if (g_done[n] == g_stamp) continue;
             float cost = g_cost[cell] + (k < 4 ? 1.0f : 1.41421356f);
             if (g_seen[n] == g_stamp && cost >= g_cost[n]) continue;
diff --git a/entropie/src/world/terrain.c b/entropie/src/world/terrain.c
index 922f9b6..523c6bc 100644
--- a/entropie/src/world/terrain.c
+++ b/entropie/src/world/terrain.c
@@ -348,10 +348,13 @@ 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;
+    if (t->pit_count && x >= -HALF && x <= HALF && z >= -HALF && z <= HALF && Terrain_inPit(t, x, z, &pit)) return pit;
+    return Terrain_naturalHeight(t, x, z);
+}
 
+float Terrain_naturalHeight(const Terrain *t, float x, float z) {
+    if (x < -HALF || x > HALF || z < -HALF || z > HALF) return OUTSIDE_HEIGHT;
     float gx = fminf((x + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f);
     float gz = fminf((z + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f);
     int ix = (int)gx, iz = (int)gz;
diff --git a/entropie/src/world/terrain.h b/entropie/src/world/terrain.h
index 21127c3..6421b53 100644
--- a/entropie/src/world/terrain.h
+++ b/entropie/src/world/terrain.h
@@ -64,6 +64,8 @@ void  Terrain_destroy(Terrain *t);
    Returns a very low value outside the terrain (objects fall off). */
 float Terrain_height(const Terrain *t, float x, float z);
 
+/* The ground as if no pit were dug there (what a building stands on). */
+float Terrain_naturalHeight(const Terrain *t, float x, float z);
 /* Ground height without the trampled paths (as built). */
 float Terrain_baseHeight(const Terrain *t, float x, float z);
 /* The meadow's own color here (before trampling). */
diff --git a/entropie/src/world/tools.c b/entropie/src/world/tools.c
index cfff10f..424e1ad 100644
--- a/entropie/src/world/tools.c
+++ b/entropie/src/world/tools.c
@@ -48,6 +48,8 @@ static const ToolSpec SPECS[] = {
 
 static const Vec3 UP = { 0.0f, 1.0f, 0.0f };
 
+#define AIM_UP_FROM 0.3f /* m over the eyes on the trunk: looking higher than that raises the felling cut */
+
 static float workFor(ToolKind tool, Job job) {
     switch (job) {
     case JOB_FELL:   return SPECS[tool].fell;
@@ -214,8 +216,10 @@ static Plan planFor(const Object *who, ToolKind tool, Object *const *objects, in
             /* The cut goes in where the worker looks, within the tool's range */
             Vec3 c = Object_colliderCenter(o, 0);
             float distance = vec3_length(flat(vec3_sub(c, who->position)));
-            /* (about hip high; lower only if they look lower: looking straight at the tree isn't aiming up the trunk) */
-            float h = clampf(fminf(lookHeight(who, distance, spec->height * s), spec->height * s), spec->low * s, spec->high * s);
+            /* About hip high. Looking lower: right where you look. Looking straight ahead isn't aiming
+               up the trunk; looking up past AIM_UP_FROM over the eyes, the cut goes up that much */
+            float look = lookHeight(who, distance, spec->height * s), hip = spec->height * s, from = (EYE_HEIGHT + AIM_UP_FROM) * s;
+            float h = clampf(look <= hip ? look : look >= from ? hip + (look - from) : hip, spec->low * s, spec->high * s);
             float top = o->position.y + 2.0f * o->colliders[0].half_extents.y - 0.3f;
             float y = who->position.y + h;
             /* A cut already started: keep hitting the same notch, whoever swings, as long as it's in reach */
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 0031d4a..f7fd015 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -455,6 +455,7 @@ static void runInto(World *world, Object *o, float into) {
    apart: over you go (the body carries on); can't get over: down you go. */
 static void halfOver(Object *o, float dt) {
     Body *b = &o->body;
+    if (b->crossing && o->sitting) { b->crossing = b->cross_force = false; b->cross_held = 0.0f; } /* (feet shuffled under a table: no getting over anything) */
     if (!b->crossing || o->type == OBJ_DOG) return;
     const float s = o->look.height / 1.75f;
     const Foot *lead = &b->feet[b->cross_foot], *other = &b->feet[1 - b->cross_foot];