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

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.94.0: Alice fetches, fallen trees drag, ladders stay out of walls

Christian Immanuel · 2026-10-19 15:53 · 50710055adb15dbb277a0b78d7e6dabb902ca4a9

- 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)
 entropie/Makefile             |   2 +-
 entropie/README.md            |  12 +++-
 entropie/STATE.md             |  12 ++--
 entropie/commit_message.txt   |  31 ++++------
 entropie/src/people/dog.c     | 135 +++++++++++++++++++++++++++++++++++++++++-
 entropie/src/people/hands.c   |   8 +--
 entropie/src/world/forestry.c |  46 +++++++++++++-
 entropie/src/world/ladder.c   |   7 ++-
 entropie/src/world/object.c   |  16 +++++
 entropie/src/world/object.h   |  15 ++++-
 entropie/src/world/physics.c  |   3 +-
 entropie/src/world/world.c    |  80 +++++++++++++++++++++----
 12 files changed, 318 insertions(+), 49 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index d612aa4..c68f953 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.93.0
+VERSION   := 1.94.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index e988ab3..47913ad 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -59,7 +59,9 @@ and satellite pictures (about 340 x 340 m).
 - The fireplace's tripod is of wooden poles standing well away from the
   fire; you walk round them
 - Alice the dachshund lives here (painted like the people): she trots after you, sniffs around, begs
-  for sausages, comes when called (R), lies by the fire and loves being petted (click)
+  for sausages, comes when called (R), lies by the fire and loves being petted (click).
+  She runs after what you throw and brings it back (sometimes catching it in the air);
+  a sausage on the ground she eats, an apple she keeps and chews somewhere
 - A little path leads out between the garages and the barn
 - Across the gravel road: the orchard (two apple trees, bushes all round),
   then a small path and a row of other garages, some closed, some full of junk
@@ -378,6 +380,10 @@ Too heavy alone but not for half its weight: hold a log at one end and you
 lift that end, the other drags along on the ground (slower).
 A hand that can't reach its grip lets go and takes hold again.
 
+A felled tree lying on the ground can be pushed or pulled (grab it, call
+helpers). Its branches make it heavy and snag on things in the way: pull on
+and they snap off. Without branches it moves much more easily.
+
 ## Ladder
 
 - A telescopic ladder (6 m out, 1.5 m together) lies in the tool room of
@@ -396,7 +402,9 @@ A hand that can't reach its grip lets go and takes hold again.
   beside it, it says so instead of going through it
 - Slid out, you hold it near its foot, nearly upright (any flatter is too
   heavy to carry). A click leans it where you look: at the top of a wall it
-  rests on the edge, lower down it leans against the face there. Look down
+  rests on the edge, lower down it leans against the face there. Walked into
+  a wall it is stood up straight (under a beam or doorway it tips down flat).
+  Carried at the side it slides back in your hand instead of into a wall Look down
   and click to slide it together. Nothing to lean on: you stand it up on its
   own - and it falls over, or against something (a beam): there it rests
 - A thin beam counts even if you look just past it
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 5b64106..313a383 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -117,6 +117,11 @@ Round 1.93:
 - The shell (`manor.c` Shell_build): 6.4 x 6.7 m (2 m longer towards the frame, which moved out 2 m), the ridge front to back, the door (1.6 m) on its -x side towards the woodpile, windows; the front closed but for a window.
 - Alice is a painted figure (`figure.frag` species 1, buildDog: body, neck, skull, snout, nose, floppy ears, legs with paws, tail; her eyes painted; `dog.c` Dog_draw sets her joints: legs trot in diagonal pairs, lying stretched, nose down sniffing, tail wagging); her shadow the same way. Shapes_dogLeg / dogTail are gone.
 
+Round 1.94:
+- Alice fetches (`dog.c` DOG_FETCH / DOG_BRING / DOG_KEEP): something thrown or dropped up to FETCH_KG 2 kg within FETCH_SEES 20 m (food: FOOD_SMELLS 10 m) -> she runs to it with her mouth; caught in the air below CATCH_HIGH 0.75 m (CATCH_CHANCE 0.55); carried at her mouth (`carry`), brought to whoever threw it (`from`) and dropped at their feet ("Woof! Woof!"). A sausage is eaten on the spot ("*gulp*"), an apple kept: she walks off, lies down and chews it (CHEW_TIME 18 s, "*crunch crunch*").
+- 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.
+
 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.
@@ -129,7 +134,7 @@ Balancing (`world.c` -> `body.balance_goal` from `Physics_footing`: 1 at a top 0
 
 Chainsaw: collider and pivot fit the mesh (the motor's underside on the ground), the bar lower on the motor (it floated).
 
-Ladders and things in the way (`ladder.c`): aiming with the view also tries 0.05 / 0.1 rad above and below (thin beams); hitting a beam from below takes the side towards you. Falling over (`fallOver`, now with the world: `Ladder_update(world, l, dt)`) it stops against anything solid it goes into (`caught`: leaning there, CAUGHT_STEADY 0.45, `lean_top` if its top is over the thing; `just_caught` -> the game says so). Carried held up (`world.c` dipLadder): running into something -> `dipped` (raise goal RAISE_DIP 0.12, nearly flat); down and clear, up again once it would stand up at 1.36 without going into anything.
+Ladders and things in the way (`ladder.c`): aiming with the view also tries 0.05 / 0.1 rad above and below (thin beams); hitting a beam from below takes the side towards you. Falling over (`fallOver`, now with the world: `Ladder_update(world, l, dt)`) it stops against anything solid it goes into (`caught`: leaning there, CAUGHT_STEADY 0.45, `lean_top` if its top is over the thing; `just_caught` -> the game says so). Carried held up (`world.c` dipLadder): running into something -> `dipped` (raise goal RAISE_DIP 0.12, nearly flat) if flat gets it out, else `steep`; down and clear, up again once it would stand up at 1.36 without going into anything.
 
 Climbing for anybody (`people/climb.c`, moved out of the game): `Climb_ledgeAhead`, `Climb_start` (says why not: hands, strength, drunk), `Climb_update` (the pull up, its feet and hands), `Climb_overLadderTop`; the move lives in the person (`climb_move`). Helpers (`ai_help.c` ai_followByLadder): the leader 1.2 m higher (standing, not climbing): a leaning ladder within 25 m from their level to the leader's (top within 1.3 m) - to its foot, up (`ai_climbing`: on a ladder or pulling over its top this runs first, whatever the mode), over the edge; none, and the leader within reach (CLIMB_REACH): walk at it and pull up where the edge ahead is at the leader's level ("Hup!"). The leader 1.2 m lower: to the top of a ladder from up there and down it; none: they just walk off (the landing does the rest). A ladder leading nowhere (no top to get off at): down again after 8 s. `Ai_useWorld` gives the AI the world for this.
 
@@ -249,8 +254,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)**
-- Trees: push / pull fallen trees; branches break or snag on obstacles (with enough momentum); much easier to carry without branches
-- Alice: dumb; she tries to catch things thrown, eats a sausage dropped on the ground, may catch a thrown apple (and not bring it back). NPC pathfinding has problems
+- 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
 
@@ -275,4 +279,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.93.0 (build ac550b2)`
+State of `entropie 1.94.0 (build 83ab53f)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index eebe5bf..6a09c1c 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,21 +1,12 @@
-entropie 1.93.0: Alice painted like the people, one roof, chairs up to the table
+entropie 1.94.0: Alice fetches, fallen trees drag, ladders stay out of walls
 
-- Alice is drawn in the same painted style as the people (smooth body,
-  floppy ears, paws, wagging tail, her shadow too)
-- Chairs: once you sit, the chair is shuffled in to the table; getting up
-  it goes out again first
-- E / Q next to a table, counter or crate sets the thing on top of it
-- Dropped bottles nearly always fall over; things on a slope slide or roll
-  down it when steep enough, and keep going more easily once pushed
-- Felling: the cut at about hip height (lower if you look lower); the trunk
-  slides off the stump and lies on the ground (it hung in the air)
-- Chopping: more hips, back and head in the swing
-- Main building: one merged roof (the left ridge runs into the right
-  roof); the ground no longer shows through the floors; the basement
-  landing is deeper, the stairs room wider, no lintel hanging in the side
-  opening
-- Roofs are closed underneath: no looking through them from below
-- Your unfinished building: 2 m longer towards the frame, the roof turned,
-  the door on the woodpile side
-- Garage gates swing right round and lie flat against the wall
-- No climbing through a wall onto something behind it
+- 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)
diff --git a/entropie/src/people/dog.c b/entropie/src/people/dog.c
index 00e98c5..b4d605e 100644
--- a/entropie/src/people/dog.c
+++ b/entropie/src/people/dog.c
@@ -2,7 +2,7 @@
 
 #include <string.h>
 
-enum { DOG_WANDER, DOG_FOLLOW, DOG_COME, DOG_BEG, DOG_REST };
+enum { DOG_WANDER, DOG_FOLLOW, DOG_COME, DOG_BEG, DOG_REST, DOG_FETCH, DOG_BRING, DOG_KEEP };
 
 #define TROT_SPEED   1.5f
 #define RUN_SPEED    3.4f
@@ -10,6 +10,12 @@ enum { DOG_WANDER, DOG_FOLLOW, DOG_COME, DOG_BEG, DOG_REST };
 #define BEG_RANGE    7.0f   /* smells a sausage this far */
 #define FIRE_RANGE  25.0f
 #define TURN_RATE    6.0f
+#define FETCH_SEES  20.0f   /* m: something thrown this near, she's after it */
+#define FOOD_SMELLS 10.0f   /* m: a sausage lying on the ground this near: hers */
+#define FETCH_KG     2.0f   /* things up to this she carries in her mouth */
+#define CATCH_HIGH   0.75f  /* m over the ground: in the air up to this she may snap it */
+#define CATCH_CHANCE 0.55f
+#define CHEW_TIME   18.0f   /* s an apple keeps her busy */
 
 static float rnd(Object *dog) {
     unsigned *r = &dog->ai.rng;
@@ -39,6 +45,63 @@ static Object *sausageHolder(const Object *dog, Object *const *objects, int coun
     return NULL;
 }
 
+static void bark(Object *dog, const char *what);
+
+/* Something for her: thrown (small enough to carry), or a sausage lying on the ground. */
+static Object *somethingToFetch(const Object *dog, Object *const *objects, int count) {
+    Object *best = NULL;
+    float best_d = 1e9f;
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o->is_held || o->broken || o->weight > FETCH_KG || o->collider_count == 0 || o->on_grill) continue;
+        float d = flatDist(o->position, dog->position);
+        bool thrown = o->thrown_by && o->thrown_by != dog && d < FETCH_SEES;
+        bool food = o->type == OBJ_SAUSAGE && !o->thrown_by && o->is_grounded && d < FOOD_SMELLS;
+        if ((thrown || food) && d < best_d) { best = o; best_d = d; }
+    }
+    return best;
+}
+
+/* In her mouth: in front of her head. */
+static Vec3 mouth(const Object *dog) {
+    return vec3_add(dog->position, vec3_add(vec3_scale(Object_facing(dog), 0.5f), vec3(0.0f, 0.27f - 0.09f * dog->dog.lie, 0.0f)));
+}
+
+static void letGo(Object *dog) {
+    Object *o = dog->dog.carry;
+    if (!o) return;
+    o->is_held = false;
+    o->velocity = vec3_zero();
+    o->position = vec3_add(mouth(dog), vec3(0.0f, -0.12f, 0.0f));
+    Object_syncColliders(o);
+    dog->dog.carry = NULL;
+    dog->dog.from = NULL;
+}
+
+/* Got it: a sausage goes down at once, an apple she keeps, the rest she brings back. */
+static void takeIt(Object *dog, Object *o, bool in_air) {
+    dog->dog.fetch = NULL;
+    dog->dog.wag = 3.0f;
+    if (o->type == OBJ_SAUSAGE) {
+        Object_useUp(o);
+        bark(dog, in_air ? "*snap* *gulp*" : "*gulp*");
+        dog->dog.mode = DOG_WANDER;
+        return;
+    }
+    dog->dog.from = o->thrown_by;
+    dog->dog.carry = o;
+    o->is_held = true;
+    o->thrown_by = NULL;
+    o->velocity = vec3_zero();
+    dog->dog.chew = 0.0f;
+    if (in_air) bark(dog, "*snap!*");
+    dog->dog.mode = o->type == OBJ_APPLE ? DOG_KEEP : DOG_BRING;
+    if (o->type == OBJ_APPLE) { /* (off with it, away from people) */
+        float a = rnd(dog) * 2.0f * PI_F;
+        dog->dog.goal = vec3_add(dog->position, vec3(cosf(a) * 4.0f, 0.0f, sinf(a) * 4.0f));
+    }
+}
+
 /* Turns towards dir and moves that way (smooth turns). */
 static void trot(Object *dog, Vec3 dir, float speed, float dt) {
     dir.y = 0.0f;
@@ -72,9 +135,21 @@ void Dog_update(Object *dog, Object *const *objects, int count, float dt) {
         if (objects[i]->type == OBJ_FIREPLACE && objects[i]->burning && flatDist(objects[i]->position, dog->position) < FIRE_RANGE)
             fire = objects[i];
 
+    /* Something thrown, or food on the ground: after it */
+    Object *carry = dog->dog.carry;
+    if (carry && (carry->broken || !carry->is_held)) { dog->dog.carry = carry = NULL; }
+    if (carry && dog->dog.mode != DOG_KEEP && dog->dog.mode != DOG_BRING && dog->dog.mode != DOG_COME) dog->dog.mode = DOG_BRING;
+    if (!carry) {
+        Object *f = dog->dog.fetch;
+        if (!f || f->is_held || f->broken) f = somethingToFetch(dog, objects, count);
+        dog->dog.fetch = f;
+        if (f && dog->dog.mode != DOG_COME) dog->dog.mode = DOG_FETCH;
+        else if (!f && dog->dog.mode == DOG_FETCH) dog->dog.mode = DOG_WANDER;
+    }
+
     /* What now */
     int mode = dog->dog.mode;
-    if (mode != DOG_COME) {
+    if (mode != DOG_COME && mode != DOG_FETCH && mode != DOG_BRING && mode != DOG_KEEP) {
         if (holder) mode = DOG_BEG;
         else if (to_player > FOLLOW_FAR) mode = DOG_FOLLOW;
         else if (mode == DOG_BEG) mode = DOG_WANDER;
@@ -121,6 +196,56 @@ void Dog_update(Object *dog, Object *const *objects, int count, float dt) {
         goal = dog->dog.goal;
         lie = flatDist(goal, dog->position) < 0.5f;
         break;
+    case DOG_FETCH: { /* after it: where it's going (in the air), or where it lies */
+        Object *f = dog->dog.fetch;
+        Vec3 at = Object_colliderCenter(f, 0);
+        goal = vec3_add(at, f->thrown_by ? vec3_scale(vec3(f->velocity.x, 0.0f, f->velocity.z), 0.35f) : vec3_zero());
+        goal = vec3_sub(goal, vec3_scale(Object_facing(dog), 0.4f)); /* (her mouth to it, not her feet) */
+        speed = RUN_SPEED;
+        float d = flatDist(at, mouth(dog)), over = Object_bottom(f) - dog->position.y;
+        if (d < 0.4f) {
+            if (f->thrown_by && over > 0.08f) { /* still in the air: snap at it */
+                if (over < CATCH_HIGH && dog->dog.timer <= 0.0f) {
+                    if (rnd(dog) < CATCH_CHANCE) takeIt(dog, f, true);
+                    else { dog->dog.timer = 0.5f; bark(dog, "*snap* ...missed"); }
+                }
+            } else if (over < 0.3f) {
+                takeIt(dog, f, false);
+            }
+        }
+        break;
+    }
+    case DOG_BRING: { /* a stick (or so) back to whoever threw it, at their feet */
+        const Object *to = dog->dog.from ? dog->dog.from : player;
+        Vec3 away = vec3_sub(dog->position, to->position);
+        away.y = 0.0f;
+        float d = vec3_length(away);
+        goal = d > 1e-3f ? vec3_add(to->position, vec3_scale(away, 0.8f / d)) : to->position;
+        speed = RUN_SPEED * 0.8f;
+        if (flatDist(goal, dog->position) < 0.4f) {
+            letGo(dog);
+            dog->dog.mode = DOG_WANDER;
+            dog->dog.timer = 3.0f;
+            dog->dog.wag = 4.0f;
+            bark(dog, "Woof! Woof!");
+        }
+        break;
+    }
+    case DOG_KEEP: /* an apple: off with it, down, and eaten bit by bit */
+        goal = dog->dog.goal;
+        lie = flatDist(goal, dog->position) < 0.5f;
+        if (lie) {
+            dog->dog.chew += dt;
+            if (dog->dog.bark > 5.0f && rnd(dog) < 0.05f) bark(dog, "*crunch crunch*");
+            if (dog->dog.chew > CHEW_TIME) {
+                Object *a = dog->dog.carry;
+                dog->dog.carry = NULL;
+                if (a) Object_useUp(a);
+                dog->dog.mode = DOG_WANDER;
+                dog->dog.timer = 2.0f;
+            }
+        }
+        break;
     default: /* sniffing around near you */
         if (dog->dog.timer <= 0.0f) {
             float a = rnd(dog) * 2.0f * PI_F, r = 1.5f + 4.0f * rnd(dog);
@@ -137,6 +262,12 @@ void Dog_update(Object *dog, Object *const *objects, int count, float dt) {
     if (!lie && vec3_length(to) > 0.35f) trot(dog, to, speed, dt);
     else if (mode == DOG_BEG && holder) trot(dog, vec3_scale(vec3_sub(holder->position, dog->position), 0.001f), 0.0f, dt);
     dog->dog.lie = clampf(dog->dog.lie + (lie ? dt : -2.0f * dt), 0.0f, 1.0f);
+    if (dog->dog.carry) { /* in her mouth */
+        Object *c = dog->dog.carry;
+        c->position = mouth(dog);
+        c->rotation = vec3(PI_F * 0.5f, dog->rotation.y + PI_F * 0.5f, 0.0f); /* (crosswise) */
+        Object_syncColliders(c);
+    }
     dog->dog.phase += dog->ground_speed * dt * 11.0f;
 }
 
diff --git a/entropie/src/people/hands.c b/entropie/src/people/hands.c
index f2e9bdd..8500a2a 100644
--- a/entropie/src/people/hands.c
+++ b/entropie/src/people/hands.c
@@ -438,7 +438,7 @@ bool Hands_grabObject(Object *who, Object *target) {
     h->grabbed      = target;
     h->grab_heading = vec3_scale(toward, -1.0f);
     h->grab_vector  = vec3_sub(target->position, who->position);
-    h->grab_share   = teamShare(pushCapacity(who), target->weight);
+    h->grab_share   = teamShare(pushCapacity(who), Object_dragWeight(target));
     who->rotation.y = yawFacing(h->grab_heading);
     Object_syncColliders(who);
     return true;
@@ -480,7 +480,7 @@ float Hands_turnGrabbed(Object *who, Object *const *objects, int count, float wa
             team[n++] = objects[k];
             capacity += pushCapacity(objects[k]);
         }
-    float share = o->lifted ? 1.0f : teamShare(capacity, o->weight);
+    float share = o->lifted ? 1.0f : teamShare(capacity, Object_dragWeight(o));
     float reach = fmaxf(extentAlong(o, vec3(1.0f, 0.0f, 0.0f)), extentAlong(o, vec3(0.0f, 0.0f, 1.0f)));
     float most = TURN_RATE * share / (0.5f + reach) * dt;
     float a = clampf(want, -most, most);
@@ -882,12 +882,12 @@ void Hands_solveGrabs(Object *const *objects, int count, float dt) {
         /* Lifted: it goes where the team goes, carried at hip height.
            On the ground: it follows the team's average wish as far as their
            strength allows; whatever it doesn't follow holds each of them back. */
-        float share = o->end_up ? END_DRAG : o->lifted ? 1.0f : teamShare(capacity, o->weight);
+        float share = o->end_up ? END_DRAG : o->lifted ? 1.0f : teamShare(capacity, Object_dragWeight(o));
         Vec3 move = vec3_scale(sum, share / (float)n);
         if (!o->lifted && !o->end_up && share > 0.0f) { /* somebody in the way: they lean against it (and get told) */
             float in_way = inTheWay(o, team, n, objects, count, move);
             if (in_way > 0.0f) {
-                share = teamShare(capacity, o->weight + BYSTANDER_HOLD * in_way);
+                share = teamShare(capacity, Object_dragWeight(o) + BYSTANDER_HOLD * in_way);
                 move = vec3_scale(sum, share / (float)n);
             }
         }
diff --git a/entropie/src/world/forestry.c b/entropie/src/world/forestry.c
index dd2e98e..a011d0e 100644
--- a/entropie/src/world/forestry.c
+++ b/entropie/src/world/forestry.c
@@ -252,7 +252,44 @@ static bool touches(const Object *tree, const Object *o) {
     return false;
 }
 
+/* A fallen tree dragged along: its branches catch on what stands in the way
+   and hold it back; pulled on (or going fast) one snaps off. */
+#define SNAG_HOLD   1.0f  /* s pulling against a snag before a branch gives */
+#define SNAG_SPEED  0.9f  /* m/s: into it this fast, it snaps at once */
+
+static bool draggedBy(const World *world, const Object *tree) {
+    for (int i = 0; i < world->count; i++)
+        if (world->objects[i]->hands.grabbed == tree) return true;
+    return false;
+}
+
+static void snagBranches(World *world, Object *tree, float dt) {
+    if (tree->is_static || tree->branches <= 1 || tree->collider_count < 2 || !draggedBy(world, tree)) { tree->snag = 0.0f; return; }
+    Object *caught = NULL;
+    for (int i = 0; i < world->count && !caught; i++) {
+        Object *o = world->objects[i];
+        if (o == tree || !o->is_static || o == tree->stump || o->collider_count == 0 || o->type == OBJ_DECAL || o->type == OBJ_STRUCTURE) continue;
+        if (fabsf(o->position.x - tree->position.x) > 15.0f || fabsf(o->position.z - tree->position.z) > 15.0f) continue;
+        for (int c = 0; c < o->collider_count && !caught; c++)
+            if (Physics_overlap(tree, 1, o, c)) caught = o;
+    }
+    if (!caught) { tree->snag = fmaxf(0.0f, tree->snag - 0.5f * dt); return; } /* (caught and freed again by the push back: it adds up) */
+    float speed = sqrtf(tree->velocity.x * tree->velocity.x + tree->velocity.z * tree->velocity.z);
+    tree->snag += dt;
+    if (speed < SNAG_SPEED) tree->velocity = vec3(0.0f, tree->velocity.y, 0.0f); /* (held back) */
+    if (tree->snag >= SNAG_HOLD || speed >= SNAG_SPEED) {
+        tree->snag = 0.0f;
+        Vec3 at = Object_colliderCenter(tree, 1);
+        dropBranch(world, tree, vec3_scale(vec3_add(at, Object_colliderCenter(caught, 0)), 0.5f), 80 + tree->branches);
+        Chips_spray(&world->chips, vec3_add(at, vec3(0.0f, 0.4f, 0.0f)), vec3(0.0f, 1.0f, 0.0f), 8, false);
+    }
+}
+
 void Forestry_update(World *world, float dt) {
+    for (int i = 0; i < world->count; i++) {
+        Object *t = world->objects[i];
+        if (t->type == OBJ_TREE && t->toppled && t->topple >= 1.0f) snagBranches(world, t, dt);
+    }
     for (int i = 0; i < world->count; i++) {
         Object *tree = world->objects[i];
         if (tree->type != OBJ_TREE || !tree->toppled || tree->topple >= 1.0f) continue;
@@ -273,6 +310,13 @@ void Forestry_update(World *world, float dt) {
             tree->topple = 1.0f;
             Object_syncColliders(tree);
         }
-        if (tree->topple >= 1.0f) crash(world, tree, stopped && stopped->type == OBJ_TREE ? stopped : NULL);
+        if (tree->topple >= 1.0f) {
+            crash(world, tree, stopped && stopped->type == OBJ_TREE ? stopped : NULL);
+            if (!stopped) { /* lying on the ground: it can be dragged about now (heavy) */
+                tree->is_static = false;
+                tree->velocity = vec3_zero();
+                tree->weight = Object_treeWeight(tree);
+            }
+        }
     }
 }
diff --git a/entropie/src/world/ladder.c b/entropie/src/world/ladder.c
index eda10c4..7a56f4f 100644
--- a/entropie/src/world/ladder.c
+++ b/entropie/src/world/ladder.c
@@ -588,6 +588,8 @@ bool Ladder_take(Object *who, Object *l) {
     l->is_static = false;
     l->lean_top = 0.0f;
     l->rest_time = 0.0f;
+    l->dipped = l->steep = false;
+    l->grip_shift = 0.0f;
     if (stepLadder(l)) l->extend_goal = 0.0f; /* (folds up as you take it) */
     else l->raised = clampf(asinf(clampf(upAlong(l).y, -1.0f, 1.0f)), 0.15f, RAISE_HIGH); /* (held up as steep as it leaned: no swing down and up again) */
     return Hands_pickUpObject(who, hand, l);
@@ -833,7 +835,7 @@ void Ladder_placeHeld(Object *who, int hand, Object *l) {
     Vec3 right = vec3_normalize(vec3_cross(facing, UP));
     int jh = hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R;
     if (Ladder_isRaised(l)) { /* slid out: held near its foot, reaching up into the air in front */
-        l->raise_goal = l->dipped ? RAISE_DIP : raiseGoal(who); /* (eased in Ladder_update) */
+        l->raise_goal = l->dipped ? RAISE_DIP : l->steep ? RAISE_HIGH : raiseGoal(who); /* (eased in Ladder_update) */
         if (l->raised <= 0.0f) l->raised = 0.15f;
         Vec3 axis = vec3_normalize(vec3_add(vec3_scale(facing, cosf(l->raised)), vec3_scale(UP, sinf(l->raised))));
         Vec3 z = vec3_normalize(vec3_cross(right, axis)); /* (rungs across, the rails side by side) */
@@ -847,13 +849,14 @@ void Ladder_placeHeld(Object *who, int hand, Object *l) {
         return;
     }
     l->raised = 0.0f;
-    l->dipped = false;
+    l->dipped = l->steep = false;
     /* on edge, along the way you walk: local Y forward, the rungs upright */
     l->rotation = euler_yxz_from_axes(facing, vec3_scale(right, -1.0f));
     Object_syncColliders(l);
     Vec3 palm = Figure_toWorld(who->position, facing, who->skeleton.joints[jh]);
     Vec3 center = vec3_sub(palm, vec3(0.0f, LADDER_WIDTH * 0.5f - 0.02f, 0.0f)); /* holding the upper rail */
     center = vec3_add(center, vec3_scale(right, hand == HAND_LEFT ? -0.05f : 0.05f));
+    center = vec3_add(center, vec3_scale(facing, l->grip_shift)); /* (slid along in the hand: off a wall) */
     l->position = vec3_sub(center, l->colliders[0].offset);
     l->velocity = vec3_zero();
 }
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 9c9bf26..40a3b07 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -890,6 +890,22 @@ void Object_setBranches(Object *obj, int branches) {
     if (obj->type != OBJ_TREE || branches == obj->branches) return;
     obj->branches = branches;
     if (branches > 0) rebuildTree(obj);
+    if (!obj->is_static) obj->weight = Object_treeWeight(obj);
+}
+
+#define TREE_BRANCH_KG 18.0f /* a layer of branches */
+#define TREE_DRAG      0.5f  /* dragged along: one end on the ground, about half its weight ... */
+#define TREE_SNAG_KG   25.0f /* ... and each layer of branches catches in the ground */
+
+float Object_treeWeight(const Object *obj) {
+    float r = obj->colliders[0].half_extents.x;
+    return WOOD_DENSITY * PI_F * r * r * obj->size.y + TREE_BRANCH_KG * (float)obj->branches;
+}
+
+float Object_dragWeight(const Object *obj) {
+    if (obj->type == OBJ_TREE && obj->toppled && !obj->lifted)
+        return TREE_DRAG * obj->weight + TREE_SNAG_KG * (float)obj->branches;
+    return obj->weight;
 }
 
 void Object_topple(Object *obj, Vec3 dir) {
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 0d4a6e1..1384d44 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -233,6 +233,10 @@ typedef struct Object {
         int   mode;
         float timer, phase, wag, lie, bark;
         Vec3  goal;
+        struct Object *fetch;   /* after this (thrown, or food lying about) ... */
+        struct Object *carry;   /* ... got it in her mouth ... */
+        struct Object *from;    /* ... thrown by (a stick goes back to them) */
+        float chew;             /* seconds with it (an apple: she keeps it, lies down, eats it) */
     } dog;
     float      charred;       /* wood: 0 = fresh .. 1 = burnt to ashes */
     float      glow;          /* wood: 0..1 glowing embers (in a burning fire) */
@@ -255,6 +259,8 @@ typedef struct Object {
     float      extend, extend_goal; /* ladders: 0 = slid together .. 1 = all the way out */
     float      raised, raise_goal; /* ladders carried slid out: held near the foot, this steep up in front (radians, 0 = carried at the side) */
     bool       dipped;        /* ... tipped down flat to get under something (a beam, a doorway) */
+    bool       steep;         /* ... stood up straight: a wall in front (flat it would go into it) */
+    float      grip_shift;    /* ladders carried at the side: slid back (< 0) or on in the hand (m), off a wall */
     bool       just_caught;   /* ladders: fell against something just now and rests there (the game says so) */
     bool       jacket_hung;   /* characters: the jacket hangs on the coat rack (else it's with them, worn or not) */
     bool       jacket_told;   /* the player: told where the jacket is this cold spell */
@@ -265,7 +271,8 @@ typedef struct Object {
     Vec3       gate_shut, gate_out; /* ... the way it reaches shut, and open (flat, unit) */
     Vec3       tucked;        /* chairs: where it stands pushed in (set when first pulled out) ... */
     bool       tucked_set;
-    bool       trunk_cut;     /* trees: felled (cut off its foot: a sawn end, no roots) */
+    bool       trunk_cut;
+    float      snag;          /* fallen trees: seconds its branches have been caught on something while dragged */     /* trees: felled (cut off its foot: a sawn end, no roots) */
     float      chair_out, chair_goal; /* ... pulled back from there this far (m), and going to this far */
     bool       seat_scooted;
     bool       drop_tip;      /* bottles: dropped (not set down): it falls over when it lands, mostly */  /* characters: sitting, the chair shuffled in to the table */
@@ -347,6 +354,12 @@ float   Object_rayToCollider(const Object *obj, int index, Vec3 origin, Vec3 dir
 /* How far obj's colliders reach from its position at most (a bounding sphere). */
 float   Object_reach(const Object *obj);
 
+/* Trees: the trunk's wood and the branches (kg). */
+float   Object_treeWeight(const Object *obj);
+/* What pushing or pulling it along takes (kg): its weight; a fallen tree is
+   dragged (one end on the ground) and its branches catch: easier without them. */
+float   Object_dragWeight(const Object *obj);
+
 /* Characters: up high (on a roof, or well over the ground)? */
 bool    Object_upHigh(const Object *obj, float ground_y);
 /* Characters up high: how careful (0 not at all .. 1 creeping): fear, drink and wits (drunk and dim: careless). */
diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c
index 7fd3bc8..6c26d60 100644
--- a/entropie/src/world/physics.c
+++ b/entropie/src/world/physics.c
@@ -578,7 +578,8 @@ static void corners(const Object *obj, int i, Vec3 out[8]) {
 static void resolveTerrain(Object *obj, const Terrain *terrain) {
     if (!obj->is_character) {
         float lift = -1e9f;
-        for (int i = 0; i < obj->collider_count; i++) {
+        const int parts = obj->type == OBJ_TREE ? 1 : obj->collider_count; /* (a tree lies on its trunk: the crown gives) */
+        for (int i = 0; i < parts; i++) {
             Vec3 p[8];
             corners(obj, i, p);
             for (int k = 0; k < 8; k++) lift = fmaxf(lift, Terrain_height(terrain, p[k].x, p[k].z) - p[k].y);
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 98ab6e1..0031d4a 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -969,23 +969,81 @@ static void placeAt(Object *who, Vec3 pos, float yaw) {
     Hands_placeHeld(who);
 }
 
-/* A ladder held up that runs into something (a beam, a doorway): tipped down
-   flat, carried under it, and up again once there's room up there. */
+#define RAISE_DIP_TEST 0.12f /* (how flat it goes down) */
+
+static float raisedAt(World *world, Object *who, Object *l, float angle) {
+    float was = l->raised;
+    l->raised = angle;
+    Hands_placeHeld(who);
+    float o = carriedOverlap(world, who);
+    l->raised = was;
+    Hands_placeHeld(who);
+    return o;
+}
+
+/* A ladder held up that runs into something: under a beam or a doorway it is
+   tipped down flat, carried under it, and up again once there's room up there.
+   Against a wall (flat it would only go in deeper) it's stood up straight. */
 static void dipLadder(World *world, Object *who, float overlap) {
     Object *l = NULL;
     for (int h = 0; h < 2; h++)
         if (who->hands.state[h] == HAND_HOLDING && Ladder_isRaised(who->hands.item[h])) l = who->hands.item[h];
     if (!l) return;
-    if (overlap > 0.002f) { l->dipped = true; return; }
+    if (overlap > 0.002f) {
+        if (l->dipped || l->steep) return;
+        float flat = raisedAt(world, who, l, RAISE_DIP_TEST), up = raisedAt(world, who, l, RAISE_UP_TEST);
+        if (flat + 0.002f < overlap && flat <= up) l->dipped = true;
+        else l->steep = true;
+        return;
+    }
+    if (l->steep) { /* clear: back to how it's held once that's clear too */
+        if (raisedAt(world, who, l, RAISE_UP_TEST) <= 0.002f) l->steep = false;
+        return;
+    }
     if (!l->dipped || l->raised > RAISE_DIP_DONE) return; /* (still going down: stays down) */
     /* down and clear: up again if it would stand up there without going into anything */
-    float was = l->raised;
-    l->raised = RAISE_UP_TEST;
-    Hands_placeHeld(who);
-    bool room = carriedOverlap(world, who) <= 0.002f;
-    l->raised = was;
-    Hands_placeHeld(who);
-    if (room) l->dipped = false;
+    if (raisedAt(world, who, l, RAISE_UP_TEST) <= 0.002f) l->dipped = false;
+}
+
+#define SLIDE_STEP 0.1f  /* m: a ladder carried at the side slides back in the hand this much at a time ... */
+#define SLIDE_KEEP 0.35f /* ... held at least this far from its end */
+
+/* A ladder carried at the side that is in a wall (taken off it, walked into it):
+   slid along in the hand till it's out (back to the middle once there's room).
+   Returns how deep it's in now. */
+static float slideLadder(World *world, Object *who, float now) {
+    Object *l = NULL;
+    for (int h = 0; h < 2; h++) {
+        Object *item = who->hands.item[h];
+        if (who->hands.state[h] == HAND_HOLDING && item && item->type == OBJ_LADDER && !Ladder_isRaised(item) && item->leaning == false) l = item;
+    }
+    if (!l) return now;
+    float most = fmaxf(0.0f, l->colliders[0].half_extents.y - SLIDE_KEEP), was = l->grip_shift;
+    if (now > 0.002f) {
+        float best = was, best_o = now;
+        for (int k = 1; k * SLIDE_STEP <= 2.0f * most + SLIDE_STEP && best_o > 0.002f; k++) {
+            for (int side = -1; side <= 1; side += 2) {
+                float shift = clampf(was + side * k * SLIDE_STEP, -most, most);
+                l->grip_shift = shift;
+                Hands_placeHeld(who);
+                float o = carriedOverlap(world, who);
+                if (o < best_o - 0.002f) best = shift, best_o = o;
+                if (best_o <= 0.002f) break;
+            }
+        }
+        l->grip_shift = best;
+        Hands_placeHeld(who);
+        return best_o;
+    }
+    if (was != 0.0f) {
+        l->grip_shift = was > 0.0f ? fmaxf(0.0f, was - 0.5f * SLIDE_STEP) : fminf(0.0f, was + 0.5f * SLIDE_STEP);
+        Hands_placeHeld(who);
+        if (carriedOverlap(world, who) > 0.002f) {
+            l->grip_shift = was;
+            Hands_placeHeld(who);
+        }
+    }
+    return now;
 }
 
 /* A carried ladder, log or axe doesn't go through walls or trees: if turning
@@ -994,7 +1052,7 @@ static void keepCarriedClear(World *world, Object *who) {
     who->carry_blocked = false;
     bool carrying = who->hands.state[0] == HAND_HOLDING || who->hands.state[1] == HAND_HOLDING;
     if (carrying && who->carry_ok && !who->on_ladder && !Tools_isWorking(who)) {
-        float now = carriedOverlap(world, who);
+        float now = slideLadder(world, who, carriedOverlap(world, who));
         dipLadder(world, who, now);
         if (now > 0.002f) {
             Vec3 pos = who->position;