Nimbin[12]?Games / Entropie / commits / 4f8bfa7

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.63.0: the hose melts in the fire; duct tape and shears mend it

Christian Immanuel · 2026-10-15 02:24 · 4f8bfa76b9f180763501ea53346f7af6f32d1fc7

- Lying in or by a burning fire the hose gets hot (glows), melts and
  leaks (less water, a shorter arc; it squirts onto the ground there),
  then burns through (nothing at the nozzle, the loose end can be taken
  anywhere)
- Duct tape (new, on the workbench) clicked at a melted bit tapes it up
- Shears clicked at a burnt-through bit cut it out and join the ends:
  the hose gets shorter (and reaches less far)

Includes 1.62.0 (if not committed separately):
- Smooth pull-ups: no hop at the end, feet and knees ease over the edge,
  elbows and hands taking hold blend in (audit walk: joint jumps 161 -> 58)

Includes 1.61.0 (if not committed separately):
- A step ladder in the tool room: stands on its own, at a wall with its
  back to it; climb onto the wall from its top

Includes 1.60.0 (if not committed separately):
- A folding ladder (two 2.5 m halves on a hinge, 5 m open) beside the
  telescopic one; one click at a wall unfolds it and leans it there

Includes 1.59.0 (if not committed separately):
- Stepping up onto blocks above knee high; set moves and jumps only for
  things standing on the ground in front of you

Includes 1.58.0 (if not committed separately):
- Walking over knee-high things is one smooth set move that always
  finishes; at a run: jump up onto blocks, catch climbable edges
 entropie/Makefile                  |   2 +-
 entropie/README.md                 |   4 +
 entropie/STATE.md                  |   8 +-
 entropie/commit_message.txt        |  21 +++--
 entropie/src/game/game_actions.c   |  39 +++++++-
 entropie/src/render/shapes.h       |   4 +
 entropie/src/render/shapes_props.c |  22 +++++
 entropie/src/world/hose.c          | 180 +++++++++++++++++++++++++++++++++++--
 entropie/src/world/hose.h          |  30 +++++++
 entropie/src/world/object.c        |   9 +-
 entropie/src/world/object.h        |   1 +
 entropie/src/world/site.c          |   1 +
 entropie/src/world/world.c         |  11 ++-
 entropie/src/world/world.h         |   1 +
 14 files changed, 307 insertions(+), 26 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index 2c5dc21..a9d96b6 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.62.0
+VERSION   := 1.63.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 298892f..6f046f0 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -89,6 +89,10 @@ and satellite pictures (about 340 x 340 m).
   soaks the ground (see below). It reaches 25 m from the tap
 - The hose is a real rope: dropped it falls and lies on the ground, and it
   drapes over and round things instead of going through them
+- Keep it out of the fire: there it gets hot, melts (it leaks, less comes
+  out) and burns through (nothing comes out). Duct tape (on the workbench)
+  mends a melted bit; a burnt-through bit is cut out with the shears and
+  the ends joined, so the hose gets shorter. Click the bit with either
 - Further away: the watering can (next to the tap). Click the tap with it
   to fill it (8 l), hold the left button: the arm comes up and the can tips
   forward to pour
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 275e788..1c378a3 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -93,6 +93,8 @@ Talk topics: offer, ask, suggest, where (new woodpile spot), ask for a hand, ask
 
 Ladder: one click at a wall slides it out to fit (top on the edge, 0.9 m over it; up to its 6 m) and leans it; slid out by hand, it keeps that length. Putting it up is a 1.1 s animation (`ladder.c`, putUp): stood upright, slid out, tipped onto the wall; you stand still with both hands on it (`game.c`, puttingUp); it can only be climbed or taken once it leans.
 
+Hose and fire (`hose.c`): each node of the rope has melt (0..1), heat and taped. Near a burning fireplace (within its flames: (0.25 diameter + 0.1) x flame size + 0.15 m, below about 1 m over it) it heats up and melts (15 s right in the flames to burn through); melted past 0.35 it leaks (12 % + more the worse it is; up to 90 % lost; it squirts onto the ground there); at 1 it's burned through: the rope comes apart there (no constraint, not drawn), nothing comes out at the nozzle, the water runs out at the break, and the end you hold is loose (no reach limit). Drawn darker the more melted, glowing while hot. Duct tape (OBJ_TAPE, on the workbench) clicked at a melted bit tapes it (grey band; no leak); burned through it won't hold. Shears clicked there cut out the run of melted bits and join the ends: the rope is laid out again along what's left (`length` shorter, at least 3 m). Too hot: "pull it out of the fire first". `world.c` water: flow scales the water and the arc.
+
 Folding ladder (`variant` LADDER_FOLDING, 9.8 kg so it carries one-handed): two 2.5 m sections on a hinge (`shapes_props.c`, Shapes_foldLadder: the upper section swings round the hinge by `extend`). It is either folded (2.5 m) or open (5 m, 18 rungs); leaning at a wall it always opens fully, the putUp animation swings the upper half up. `ladder.c` lengthFor / rung / rungCount pick per type.
 
 Step ladder (LADDER_STEP, 7 kg): a 1.95 m front leg with 5 treads plus a platform, the back leg hinged at the top (`Shapes_stepLadder`, `extend` = how far open, 30 degrees between the legs, each 15 off upright, so the treads are set level). Two colliders, one per leg (`ladder.c` fitColliders). standStep: stood up 0.45 m in front of you; a wall within 0.6 m past its back feet: back to the wall, 8 cm off; `lean_top` = its length when the wall top is between 0.6 under and 0.9 over its top, so the usual "over the edge" works. Feet only on the treads (top tread 1.39 m along), hands on the platform. Taking it folds it up again. NOTE: `mat4_rotate_x(a)` turns +Y towards -Z.
@@ -111,7 +113,7 @@ Piling styles: how neatly someone stacks (`ai_stackCare`: firewood skill and lik
 
 Woodpile: firewood is stacked into the lowest free place (gaps left by pieces taken off are filled first); it counts what really lies on it; a piece that fell off is loose again (gets restacked).
 
-Bottles (beer, vodka, water), apples, bread, sausages (grill: done, dark, burnt), crates and boxes, logs, branches, firewood, stumps, woodpiles, fireplace, trash and garbage bags, the waste container, tables and garage furniture, stones, garden hose (a rope: falls, lies, drapes round things), watering can, telescopic, folding and step ladder, axe, shears, chainsaw, trees (fruit trees too), bushes, cars, buildings.
+Bottles (beer, vodka, water), apples, bread, sausages (grill: done, dark, burnt), crates and boxes, logs, branches, firewood, stumps, woodpiles, fireplace, trash and garbage bags, the waste container, tables and garage furniture, stones, garden hose (a rope: falls, lies, drapes round things), watering can, duct tape, telescopic, folding and step ladder, axe, shears, chainsaw, trees (fruit trees too), bushes, cars, buildings.
 
 ## World
 
@@ -150,7 +152,7 @@ Most important first.
 - Clothing (trousers, skirt, dress, fly) – needed for the above
 
 **Things**
-- Hose near fire: melts, may burn; duct tape for small damage; burned through: shorter, or cut the bad part (knife, scissors) and join the ends; put the fire out with your drink (C)
+- Put a fire out with your drink (C)
 
 **Tools for us**
 - Optional local AI (llama.cpp, small model) for talking and tasks; the menu stays as fallback
@@ -160,4 +162,4 @@ Most important first.
 - The walking map rebuild takes ~9 ms when fixed things change (a felled tree)
 
 ---
-State of `entropie 1.62.0 (build 4e94661)`
+State of `entropie 1.63.0 (build 1f95dd5)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index b421ebf..2eb3320 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,13 +1,16 @@
-entropie 1.62.0: smooth pull-ups (no joint jumps climbing up)
+entropie 1.63.0: the hose melts in the fire; duct tape and shears mend it
 
-- The climb ends exactly where you then stand (no hop at the end), each
-  foot lands on what's under it, the feet stay put instead of replanting
-- Feet come over the edge more slowly; the foot kept back from the face
-  lets go gradually; knees ease out of the pull-up pose
-- An elbow swinging up over an edge and a hand taking hold blend in
-  smoothly (they flipped at half way)
-- Audit walk (walks into a wall and climbs it): joint jumps 161 -> 58,
-  the worst 22 -> 10 cm
+- Lying in or by a burning fire the hose gets hot (glows), melts and
+  leaks (less water, a shorter arc; it squirts onto the ground there),
+  then burns through (nothing at the nozzle, the loose end can be taken
+  anywhere)
+- Duct tape (new, on the workbench) clicked at a melted bit tapes it up
+- Shears clicked at a burnt-through bit cut it out and join the ends:
+  the hose gets shorter (and reaches less far)
+
+Includes 1.62.0 (if not committed separately):
+- Smooth pull-ups: no hop at the end, feet and knees ease over the edge,
+  elbows and hands taking hold blend in (audit walk: joint jumps 161 -> 58)
 
 Includes 1.61.0 (if not committed separately):
 - A step ladder in the tool room: stands on its own, at a wall with its
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index 22c398e..a04fb38 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -543,11 +543,12 @@ static Object *waterInHand(const Object *p) {
 void game_holdHose(Game *game) {
     Object *p = game->player, *hose = waterInHand(p);
     if (!hose || hose->type != OBJ_HOSE || !hose->hose_tap) return;
+    if (Hose_burnedThrough(&game->world, hose, NULL)) return; /* (burned through: the end you hold is loose) */
     Vec3 tap = hose->hose_tap->position, to = vec3_sub(p->position, tap);
     to.y = 0.0f;
-    float d = vec3_length(to);
-    if (d <= HOSE_LENGTH) return;
-    Vec3 back = vec3_scale(to, HOSE_LENGTH / d);
+    float d = vec3_length(to), reach = Hose_length(&game->world, hose);
+    if (d <= reach) return;
+    Vec3 back = vec3_scale(to, reach / d);
     p->position.x = tap.x + back.x;
     p->position.z = tap.z + back.z;
     Object_syncColliders(p);
@@ -556,6 +557,32 @@ void game_holdHose(Game *game) {
 
 #define CAN_LITERS 8.0f
 
+/* Duct tape or shears in hand, looking at a melted bit of the hose: tape it up,
+   or cut the burnt bit out and join the ends. */
+#define MEND_REACH 2.2f
+static bool mendHose(Game *game) {
+    Object *p = game->player, *with = NULL;
+    for (int h = 0; h < 2; h++)
+        if (p->hands.item[h] && (p->hands.item[h]->type == OBJ_TAPE || p->hands.item[h]->type == OBJ_SHEARS)) with = p->hands.item[h];
+    if (!with || vec3_length(p->body.look_dir) < 0.5f) return false;
+    Vec3 eye = vec3_add(p->position, vec3(0.0f, 0.93f * p->look.height, 0.0f));
+    int k = -1;
+    HoseRope *rope = Hose_aimed(&game->world, eye, vec3_normalize(p->body.look_dir), MEND_REACH, &k);
+    if (!rope || Hose_damageNear(rope, k) < 0) return false; /* (shears: nothing to mend here, so on to the branches) */
+    if (rope->heat[Hose_damageNear(rope, k)] > 0.15f) { say(game, "Too hot - pull it out of the fire first"); return true; }
+    static char line[96];
+    if (with->type == OBJ_TAPE) {
+        if (Hose_tape(rope, k)) say(game, "You tape up the melted bit - it holds");
+        else say(game, "Burned right through - tape won't hold that. Cut the burnt bit out with the shears");
+        return true;
+    }
+    float cut = Hose_cutOut(rope, k);
+    if (cut <= 0.0f) { say(game, "Not much hose left to cut"); return true; }
+    snprintf(line, sizeof(line), "You cut out the burnt bit and join the ends - the hose is %.1f m now", rope->length);
+    say(game, line);
+    return true;
+}
+
 /* Hose: spray. Watering can: fill it at the tap, else pour. Kitchen sink: wash. */
 static int useWater(Game *game) {
     Object *p = game->player;
@@ -578,6 +605,10 @@ static int useWater(Game *game) {
         say(game, "The watering can is empty - fill it at the tap");
         return CLICK_NONE;
     }
+    if (tool->type == OBJ_HOSE && Hose_burnedThrough(&game->world, tool, NULL))
+        say(game, "Nothing comes out - the hose is burned through. Cut the burnt bit out with the shears");
+    else if (tool->type == OBJ_HOSE && Hose_flow(&game->world, tool, 0.0f) < 0.95f)
+        say(game, "The hose leaks where it's melted - duct tape would mend it");
     p->spraying = true;
     return CLICK_WATER;
 }
@@ -726,7 +757,7 @@ static int leftClick(Game *game) {
         Talk_showHere(&game->world, p, shown);
         return CLICK_NONE;
     }
-    if (useLadder(game) || tendFire(game) || throwAway(game)) return CLICK_NONE;
+    if (mendHose(game) || useLadder(game) || tendFire(game) || throwAway(game)) return CLICK_NONE;
     int watering = useWater(game);
     if (watering >= 0) return watering;
     if (bagTrash(game)) return CLICK_BAG;
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index b191c5a..5e4c89b 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -125,6 +125,10 @@ Mesh  Shapes_dogTail(void);
 /* Garden hose: its nozzle (centered), and one piece of hose 1 m long along +Y from the origin. */
 Mesh  Shapes_hoseNozzle(void);
 Mesh  Shapes_hosePiece(void);
+/* Duct tape round a piece of hose (like Shapes_hosePiece, a bit thicker, grey). */
+Mesh  Shapes_hoseTape(void);
+/* A roll of duct tape, centered, lying flat. */
+Mesh  Shapes_tapeRoll(void);
 /* A watering can, centered, spout towards +Z. */
 Mesh  Shapes_wateringCan(void);
 #define TAP_HEIGHT 0.8f   /* where the hose comes out of the tap */
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 9292cbb..715080a 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -800,6 +800,28 @@ Mesh Shapes_hosePiece(void) {
     return Mesh_fromBuilder(&b);
 }
 
+Mesh Shapes_hoseTape(void) {
+    const Vec3 grey = { 0.62f, 0.63f, 0.64f };
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    shapes_addTube(&b, 0.0f, 0.021f, 0.021f, 1.0f, 6, grey, false);
+    return Mesh_fromBuilder(&b);
+}
+
+Mesh Shapes_tapeRoll(void) {
+    const Vec3 grey = { 0.62f, 0.63f, 0.64f }, card = { 0.72f, 0.60f, 0.42f };
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    shapes_addTube(&b, -0.024f, 0.052f, 0.052f, 0.048f, 16, grey, false); /* the tape ... */
+    shapes_addTube(&b, -0.025f, 0.036f, 0.036f, 0.05f, 16, card, false);  /* ... on its cardboard core */
+    for (int s = -1; s <= 1; s += 2) /* (the sides: rings of tape) */
+        for (int k = 0; k < 16; k++) {
+            float a = 2.0f * PI_F * (float)k / 16.0f;
+            addBlock(&b, vec3(0.044f * cosf(a), (float)s * 0.024f, 0.044f * sinf(a)), vec3(0.022f, 0.002f, 0.022f), grey);
+        }
+    return Mesh_fromBuilder(&b);
+}
+
 Mesh Shapes_wateringCan(void) {
     const Vec3 green = { 0.30f, 0.56f, 0.38f };
     MeshBuilder b;
diff --git a/entropie/src/world/hose.c b/entropie/src/world/hose.c
index 70bffb4..f51844c 100644
--- a/entropie/src/world/hose.c
+++ b/entropie/src/world/hose.c
@@ -1,8 +1,12 @@
 #include "world/hose.h"
 #include "world/world.h"
+#include "world/camp.h"
+#include "world/wetness.h"
 
 #define HOSE_RADIUS   0.018f
-#define HOSE_SEGMENT  (HOSE_LENGTH / (float)(HOSE_NODES - 1))
+#define HOSE_SHORTEST 3.0f   /* cut no shorter than this */
+#define MELT_TIME     15.0f  /* seconds right in the flames to burn through */
+#define LEAK_MOST     0.9f   /* of the water lost through leaks, at most */
 #define HOSE_GRAVITY  9.81f
 #define HOSE_DAMPING  0.985f /* of the speed kept each step (air) */
 #define HOSE_FRICTION 0.55f  /* of the sliding kept each step, lying on something */
@@ -27,9 +31,41 @@ static HoseRope *ropeOf(World *world, const Object *nozzle) {
     if (!free_slot) return NULL;
     free_slot->nozzle = nozzle;
     free_slot->ready = false;
+    free_slot->length = HOSE_LENGTH;
+    for (int k = 0; k < HOSE_NODES; k++) {
+        free_slot->melt[k] = free_slot->heat[k] = 0.0f;
+        free_slot->taped[k] = false;
+    }
     return free_slot;
 }
 
+static float segmentOf(const HoseRope *rope) { return rope->length / (float)(HOSE_NODES - 1); }
+static bool brokenAt(const HoseRope *rope, int k) { return rope->melt[k] >= 1.0f; } /* (between k and k + 1) */
+
+/* Lying in or right by the flames of a burning fire: it gets hot and melts. */
+static void heatUp(World *world, HoseRope *rope, float dt) {
+    float hot[HOSE_NODES] = { 0.0f };
+    for (int i = 0; i < world->count; i++) {
+        const Object *f = world->objects[i];
+        if (f->type != OBJ_FIREPLACE || !f->burning || f->fuel <= 0.0f) continue;
+        float flame = Camp_flameSize(f), reach = (0.25f * f->size.x + 0.1f) * fminf(flame, 1.5f) + 0.15f;
+        for (int k = 1; k < HOSE_NODES - 1; k++) {
+            Vec3 d = vec3_sub(rope->p[k], f->position);
+            float across = sqrtf(d.x * d.x + d.z * d.z);
+            if (across > reach + 0.3f) continue;
+            float h = fminf(flame, 1.5f) * (1.0f - smoothstepf(0.6f * reach, reach + 0.3f, across)) * (1.0f - smoothstepf(0.4f, 1.0f + 0.3f * flame, d.y));
+            hot[k] = fmaxf(hot[k], h);
+        }
+    }
+    for (int k = 1; k < HOSE_NODES - 1; k++) {
+        rope->heat[k] += (hot[k] - rope->heat[k]) * fminf(1.0f, 2.0f * dt); /* (warms up, cools down) */
+        if (hot[k] > 0.0f) {
+            rope->melt[k] = fminf(1.0f, rope->melt[k] + hot[k] * dt / MELT_TIME);
+            if (rope->melt[k] >= HOSE_LEAKS) rope->taped[k] = false; /* (the tape melts too) */
+        }
+    }
+}
+
 /* Laid out on the ground from the tap to the nozzle (the first time). */
 static void layOut(World *world, HoseRope *rope) {
     Vec3 a = tapEnd(rope->nozzle), b = nozzleEnd(rope->nozzle);
@@ -101,12 +137,14 @@ static void simulate(World *world, HoseRope *rope, float dt) {
     rope->p[HOSE_NODES - 1] = nozzleEnd(nozzle);
 
     /* keep its length, keep out of the ground and out of things */
+    const float segment = segmentOf(rope);
     for (int it = 0; it < HOSE_ITERATIONS; it++) {
         for (int k = 0; k + 1 < HOSE_NODES; k++) {
+            if (brokenAt(rope, k)) continue; /* (burned through: two loose ends) */
             Vec3 d = vec3_sub(rope->p[k + 1], rope->p[k]);
             float len = vec3_length(d);
-            if (len <= HOSE_SEGMENT || len < 1e-6f) continue; /* (slack: it may bunch up) */
-            Vec3 fix = vec3_scale(d, (len - HOSE_SEGMENT) / len);
+            if (len <= segment || len < 1e-6f) continue; /* (slack: it may bunch up) */
+            Vec3 fix = vec3_scale(d, (len - segment) / len);
             bool a_fixed = k == 0, b_fixed = k + 1 == HOSE_NODES - 1;
             if (a_fixed && b_fixed) continue;
             if (a_fixed) rope->p[k + 1] = vec3_sub(rope->p[k + 1], fix);
@@ -148,6 +186,7 @@ void Hose_update(World *world, float dt) {
         if (!rope) continue;
         if (!rope->ready) layOut(world, rope);
         simulate(world, rope, dt);
+        heatUp(world, rope, dt);
     }
 }
 
@@ -173,7 +212,138 @@ void Hose_draw(World *world, Renderer *renderer, const Object *nozzle) {
     if (!rope) return;
     if (!rope->ready) layOut(world, rope);
     for (int k = 1; k < HOSE_NODES; k++) {
-        if (vec3_length(vec3_sub(rope->p[k], rope->p[k - 1])) < 1e-3f) continue;
-        Renderer_drawMesh(renderer, &world->hose_piece, pieceFrom(rope->p[k - 1], rope->p[k]));
+        if (brokenAt(rope, k - 1) || vec3_length(vec3_sub(rope->p[k], rope->p[k - 1])) < 1e-3f) continue;
+        /* melted: darker, black where it's burned; hot: glowing */
+        float m = fmaxf(rope->melt[k - 1], rope->melt[k]), heat = fmaxf(rope->heat[k - 1], rope->heat[k]);
+        if (m > 0.02f || heat > 0.02f) {
+            float dark = 1.0f - 0.85f * smoothstepf(0.0f, 0.9f, m);
+            Renderer_drawMeshTinted(renderer, &world->hose_piece, pieceFrom(rope->p[k - 1], rope->p[k]),
+                                    vec3(dark, dark * 0.9f, dark * 0.85f), 0.7f * clampf(heat, 0.0f, 1.0f));
+        } else {
+            Renderer_drawMesh(renderer, &world->hose_piece, pieceFrom(rope->p[k - 1], rope->p[k]));
+        }
+        if (rope->taped[k] && world->hose_tape.vao) { /* a band of tape round it */
+            Vec3 a = vec3_lerp(rope->p[k - 1], rope->p[k], 0.6f), b = k + 1 < HOSE_NODES ? vec3_lerp(rope->p[k], rope->p[k + 1], 0.4f) : rope->p[k];
+            Renderer_drawMesh(renderer, &world->hose_tape, pieceFrom(a, b));
+        }
+    }
+}
+
+HoseRope *Hose_of(World *world, const Object *nozzle) {
+    if (!nozzle || nozzle->type != OBJ_HOSE || !nozzle->hose_tap) return NULL;
+    HoseRope *rope = ropeOf(world, nozzle);
+    if (rope && !rope->ready) layOut(world, rope);
+    return rope;
+}
+
+float Hose_length(World *world, const Object *nozzle) {
+    HoseRope *rope = Hose_of(world, nozzle);
+    return rope ? rope->length : HOSE_LENGTH;
+}
+
+bool Hose_burnedThrough(World *world, const Object *nozzle, Vec3 *at) {
+    HoseRope *rope = Hose_of(world, nozzle);
+    if (!rope) return false;
+    for (int k = 0; k < HOSE_NODES - 1; k++)
+        if (brokenAt(rope, k)) {
+            if (at) *at = rope->p[k];
+            return true;
+        }
+    return false;
+}
+
+float Hose_flow(World *world, const Object *nozzle, float dt) {
+    HoseRope *rope = Hose_of(world, nozzle);
+    if (!rope) return 1.0f;
+    const float liters = 0.3f * dt; /* (what the tap gives) */
+    for (int k = 0; k < HOSE_NODES - 1; k++)
+        if (brokenAt(rope, k)) { /* all of it runs out there */
+            Wetness_pour(&world->terrain, rope->p[k].x, rope->p[k].z, liters, 0.2f, 0.0f);
+            return 0.0f;
+        }
+    float lost = 0.0f;
+    for (int k = 1; k < HOSE_NODES - 1; k++) {
+        if (rope->melt[k] < HOSE_LEAKS || rope->taped[k]) continue;
+        float leak = 0.12f + 0.6f * (rope->melt[k] - HOSE_LEAKS);
+        lost += leak;
+        Wetness_pour(&world->terrain, rope->p[k].x, rope->p[k].z, liters * leak, 0.12f, 0.0f); /* (it squirts out there) */
+    }
+    return 1.0f - fminf(lost, LEAK_MOST);
+}
+
+HoseRope *Hose_aimed(World *world, Vec3 eye, Vec3 dir, float reach, int *node) {
+    HoseRope *best = NULL;
+    float best_d = 0.2f;
+    for (int i = 0; i < HOSE_MAX; i++) {
+        HoseRope *rope = &world->hoses[i];
+        if (!rope->nozzle || !rope->ready) continue;
+        for (int k = 1; k < HOSE_NODES - 1; k++) {
+            Vec3 to = vec3_sub(rope->p[k], eye);
+            float along = vec3_dot(to, dir);
+            if (along < 0.0f || along > reach) continue;
+            float d = vec3_length(vec3_sub(to, vec3_scale(dir, along)));
+            if (d < best_d) { best_d = d; best = rope; *node = k; }
+        }
+    }
+    return best;
+}
+
+int Hose_damageNear(const HoseRope *rope, int k) {
+    int best = -1;
+    for (int j = k - 2; j <= k + 2; j++) {
+        if (j < 1 || j >= HOSE_NODES - 1) continue;
+        bool damaged = rope->melt[j] >= HOSE_LEAKS && !rope->taped[j];
+        if (damaged && (best < 0 || rope->melt[j] > rope->melt[best])) best = j;
+    }
+    return best;
+}
+
+/* The run of melted (untaped) bits round k: [*from, *to]. */
+static void damagedRun(const HoseRope *rope, int k, int *from, int *to) {
+    *from = *to = k;
+    while (*from > 1 && rope->melt[*from - 1] >= HOSE_LEAKS && !rope->taped[*from - 1]) (*from)--;
+    while (*to < HOSE_NODES - 2 && rope->melt[*to + 1] >= HOSE_LEAKS && !rope->taped[*to + 1]) (*to)++;
+}
+
+bool Hose_tape(HoseRope *rope, int k) {
+    int at = Hose_damageNear(rope, k), from, to;
+    if (at < 0) return false;
+    damagedRun(rope, at, &from, &to);
+    for (int j = from; j <= to; j++)
+        if (brokenAt(rope, j)) return false;
+    for (int j = from; j <= to; j++) rope->taped[j] = true;
+    return true;
+}
+
+float Hose_cutOut(HoseRope *rope, int k) {
+    int at = Hose_damageNear(rope, k), from, to;
+    if (at < 0) return 0.0f;
+    damagedRun(rope, at, &from, &to);
+    const float segment = segmentOf(rope);
+    float cut = (float)(to - from + 1) * segment;
+    if (rope->length - cut < HOSE_SHORTEST) return 0.0f;
+    /* what's left, the ends joined (the bits from..to gone), laid out again along it at the new spacing */
+    Vec3 keep[HOSE_NODES];
+    bool keep_tape[HOSE_NODES];
+    float keep_melt[HOSE_NODES];
+    int n = 0;
+    for (int j = 0; j < HOSE_NODES; j++) {
+        if (j >= from && j <= to) continue;
+        keep_tape[n] = rope->taped[j];
+        keep_melt[n] = rope->melt[j];
+        keep[n++] = rope->p[j];
+    }
+    const float length = rope->length - cut, step = length / (float)(HOSE_NODES - 1);
+    for (int j = 0; j < HOSE_NODES; j++) {
+        float u = (float)j * step / segment; /* (in kept bits, each a segment long) */
+        int a = (int)u;
+        if (a >= n - 1) a = n - 2;
+        float t = clampf(u - (float)a, 0.0f, 1.0f);
+        rope->p[j] = rope->prev[j] = vec3_lerp(keep[a], keep[a + 1], t);
+        rope->taped[j] = keep_tape[t < 0.5f ? a : a + 1]; /* (other mended or melted bits stay as they were) */
+        rope->melt[j] = keep_melt[t < 0.5f ? a : a + 1];
+        rope->heat[j] = 0.0f;
     }
+    rope->length = length;
+    return cut;
 }
diff --git a/entropie/src/world/hose.h b/entropie/src/world/hose.h
index 5d97d32..d684b8c 100644
--- a/entropie/src/world/hose.h
+++ b/entropie/src/world/hose.h
@@ -5,6 +5,11 @@
  * Garden hoses as a rope: a chain of points from the tap to the nozzle that
  * falls, lies on the ground, and drapes over and round whatever is in the way
  * (never through it). The length is fixed; pulled straight it goes taut.
+ *
+ * Too near a burning fire it gets hot, melts (it leaks there) and burns
+ * through (nothing comes out at the nozzle; the water runs out at the break).
+ * Duct tape mends a melted spot; a burnt-through bit is cut out with the
+ * shears and the ends joined: the hose is that much shorter.
  */
 
 #include "world/object.h"
@@ -14,9 +19,15 @@
 #define HOSE_NODES  51     /* points along it (half a meter apart) */
 #define HOSE_MAX    4      /* hoses simulated at once */
 
+#define HOSE_LEAKS  0.35f  /* melted this far (0..1) it leaks; 1: burned through */
+
 typedef struct {
     const Object *nozzle;
     Vec3  p[HOSE_NODES], prev[HOSE_NODES];
+    float melt[HOSE_NODES];   /* how far each bit is melted (1: burned through, between it and the next) */
+    float heat[HOSE_NODES];   /* how hot it is right now (0..1): it glows */
+    bool  taped[HOSE_NODES];  /* mended with duct tape */
+    float length;             /* meters (shorter once a bit is cut out) */
     bool  ready;
 } HoseRope;
 
@@ -28,4 +39,23 @@ void Hose_update(struct World *world, float dt);
 /* Draws the hose of this nozzle, piece by piece. */
 void Hose_draw(struct World *world, Renderer *renderer, const Object *nozzle);
 
+/* The hose of this nozzle (NULL if it has none). */
+HoseRope *Hose_of(struct World *world, const Object *nozzle);
+/* How far it reaches from the tap (meters; shorter once mended). */
+float Hose_length(struct World *world, const Object *nozzle);
+/* Burned through somewhere (*at: where). */
+bool  Hose_burnedThrough(struct World *world, const Object *nozzle, Vec3 *at);
+/* Turned on for dt: how much of the water gets to the nozzle (0..1). Where it
+   leaks or is burned through the water runs out onto the ground. */
+float Hose_flow(struct World *world, const Object *nozzle, float dt);
+/* The bit of any hose along a ray (within reach): its rope and node, or NULL. */
+HoseRope *Hose_aimed(struct World *world, Vec3 eye, Vec3 dir, float reach, int *node);
+/* The worst spot near node k: melted (or burned through) bits around it, or -1. */
+int   Hose_damageNear(const HoseRope *rope, int k);
+/* Duct tape round the melted bits at spot k: no more leaking there. False if
+   burned through (tape won't hold that) or nothing to mend. */
+bool  Hose_tape(HoseRope *rope, int k);
+/* Cuts out the melted bits at spot k and joins the ends. Meters it got shorter (0: nothing cut). */
+float Hose_cutOut(HoseRope *rope, int k);
+
 #endif /* WORLD_HOSE_H */
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index 3bddc40..bc4a44e 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -281,6 +281,12 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size
         addCollider(obj, vec3_zero(), size, vec3_zero());
         break;
 
+    case OBJ_TAPE:
+        obj->mesh  = Shapes_tapeRoll();
+        obj->pivot = vec3(0.0f, 0.025f, 0.0f);
+        addCollider(obj, vec3_zero(), vec3(0.1f, 0.05f, 0.1f), vec3_zero());
+        break;
+
     case OBJ_SHEARS:
         obj->mesh  = Shapes_shears(size.y);
         obj->pivot = centered;
@@ -977,7 +983,8 @@ void Object_describe(const Object *obj, char *out, size_t size) {
         else snprintf(out, size, "Fireplace - put firewood or branches in");
         break;
     case OBJ_AXE:      snprintf(out, size, "Axe - fells, cuts and splits wood"); break;
-    case OBJ_SHEARS:   snprintf(out, size, "Pruning shears - cut branches off"); break;
+    case OBJ_SHEARS:   snprintf(out, size, "Pruning shears - cut branches off (or a burnt bit out of the hose)"); break;
+    case OBJ_TAPE:     snprintf(out, size, "Duct tape - left click at a melted bit of the hose: tape it up"); break;
     case OBJ_CHAINSAW: snprintf(out, size, "Chainsaw - fells and cuts fast, %d kg", kg); break;
     case OBJ_LOG:
         snprintf(out, size, "Log %.1f m, %d kg%s", obj->size.y, kg,
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 6543dfb..2fb3d92 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -74,6 +74,7 @@ typedef enum {
     OBJ_STONE,           /* a loose stone, size = bounding box; hard: glass breaks on it */
     OBJ_STUMP,           /* static: what's left of a felled tree, size = (diameter, height, -);
                             work = how far it's been chopped out (see tools.h) */
+    OBJ_TAPE,            /* a roll of duct tape: mends a melted hose (see hose.h) */
 } ObjectType;
 
 /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index 31d658e..9c3297e 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -727,6 +727,7 @@ static bool furnishGarages(World *world, Vec3 origin, float yaw, float zsign) {
     struct { ObjectType type; Vec3 at, size, rot; float kg; int variant; } TOOLS[] = {
         { OBJ_AXE,      { 26.5f, 1.0f, 12.0f }, { 0.0f, 0.8f, 0.0f }, { 90, 0, 0 }, 2.2f, 0 },
         { OBJ_SHEARS,   { 26.5f, 1.0f, 13.2f }, { 0.0f, 0.3f, 0.0f }, { 90, 20, 0 }, 0.4f, 0 },
+        { OBJ_TAPE,     { 26.5f, 1.0f, 12.6f }, { 0.1f, 0.05f, 0.1f }, { 0, 0, 0 }, 0.2f, 0 },
         { OBJ_CHAINSAW, { 25.6f, 0.3f, 12.0f }, { 0.0f, 0.0f, 0.0f }, { 0, 60, 0 }, 6.0f, 0 },
         { OBJ_LADDER,   { 20.9f, 0.1f, 13.55f }, { 0.0f, 0.0f, 0.0f }, { 90, 90, 0 }, 9.5f, 0 }, /* slid together, along the end wall */
         { OBJ_LADDER,   { 23.2f, 0.1f, 13.55f }, { 0.0f, 0.0f, 0.0f }, { 90, 90, 0 }, 9.8f, LADDER_FOLDING }, /* folded, beside it */
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 48f6ea5..33d483f 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -230,7 +230,10 @@ static void water(World *world, Object *o, float dt) {
     Object *tool = o->hands.item[hand];
     bool can = tool->type == OBJ_CAN;
     if (can && tool->water <= 0.0f) { o->spraying = false; return; }
-    float liters = can ? 0.0f : HOSE_FLOW * dt;
+    /* (a melted hose leaks on the way, a burnt-through one gives nothing at the nozzle) */
+    const float flow = can ? 1.0f : Hose_flow(world, tool, dt);
+    if (flow < 0.02f) { o->skeleton.stream_kind = STREAM_NONE; return; }
+    float liters = can ? 0.0f : HOSE_FLOW * flow * dt;
     if (can && o->pour >= 0.6f) {
         liters = fminf(tool->water, CAN_POUR * dt);
         tool->water -= liters;
@@ -248,7 +251,7 @@ static void water(World *world, Object *o, float dt) {
         start = Figure_toLocal(o->position, facing, vec3_add(origin, mat4_mul_dir(r, vec3(0.0f, 0.16f, 0.37f))));
         dir = vec3_normalize(Figure_toLocal(vec3_zero(), facing, mat4_mul_dir(r, vec3(0.0f, 0.22f, 0.26f))));
     }
-    Vec3 v = vec3_scale(dir, can ? CAN_SPEED : HOSE_SPEED);
+    Vec3 v = vec3_scale(dir, can ? CAN_SPEED : HOSE_SPEED * sqrtf(flow)); /* (less pressure: a shorter arc) */
     const float g = 9.81f;
     float land = (v.y + sqrtf(fmaxf(v.y * v.y + 2.0f * g * fmaxf(start.y, 0.05f), 0.0f))) / g;
     for (int k = 0; k < 4; k++) {
@@ -258,7 +261,7 @@ static void water(World *world, Object *o, float dt) {
     sk->stream_kind = STREAM_WATER;
     sk->stream_width = can ? 0.025f : 0.022f;
     Vec3 hit = Figure_toWorld(o->position, facing, sk->stream[3]);
-    Camp_water(world->objects, world->count, o, hit, (can ? CAN_POUR : HOSE_FLOW) * dt, dt);
+    Camp_water(world->objects, world->count, o, hit, (can ? CAN_POUR : HOSE_FLOW * flow) * dt, dt);
     Wetness_pour(&world->terrain, hit.x, hit.z, liters, can ? CAN_SPREAD : HOSE_SPREAD, 0.0f); /* the ground gets wet */
 }
 
@@ -743,6 +746,7 @@ void World_destroy(World *world) {
     Chips_destroy(&world->chips);
     Grid_free(&world->grid);
     Mesh_destroy(&world->hose_piece);
+    Mesh_destroy(&world->hose_tape);
     memset(world, 0, sizeof(*world));
 }
 
@@ -772,6 +776,7 @@ bool World_buildTerrain(World *world) {
     world->flames = Shapes_flames();
     Chips_init(&world->chips);
     world->hose_piece = Shapes_hosePiece();
+    world->hose_tape = Shapes_hoseTape();
     world->has_terrain = Terrain_init(&world->terrain, world->objects, world->count);
     return world->has_terrain;
 }
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index fae848d..9baaedc 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -22,6 +22,7 @@ typedef struct World {
     unsigned rng;          /* dice for small accidents */
     Mesh     flames;       /* on people who walked into a fire */
     Mesh     hose_piece;   /* drawn again and again along a garden hose */
+    Mesh     hose_tape;    /* ... and round it where it's mended */
     HoseRope hoses[HOSE_MAX];
     Chips    chips;        /* wood chips and sawdust flying off work */
     Grid     grid;         /* where things are, by ground cell (built each frame) */