Nimbin[12]?Games / Entropie / commits / 46d773f

Entropie git · main

Entropie festival game; first steps

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

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

entropie 2.19.0: saves keep the hose and sitting; felling hint

Christian Immanuel · 2026-10-20 17:44 · 46d773f6e5e29e18417296411d42269aadbf9e55

- Saves keep the hose (where the rope lies, melted and taped bits) and
  who sits where (they sit down again after loading)
- Felling yourself: the hint line says where the tree comes down and what
  it would hit; nearly through, people in the way step aside
 commit_message.txt               | 14 +++++---------
 entropie/ARCHITECTURE.md         |  2 +-
 entropie/Makefile                |  2 +-
 entropie/README.md               |  2 ++
 entropie/STATE.md                | 10 +++++++---
 entropie/src/game/game_actions.c | 35 +++++++++++++++++++++++++++++++++++
 entropie/src/game/save.c         | 36 ++++++++++++++++++++++++++++++++++--
 entropie/src/people/ai.h         |  4 ++++
 entropie/src/people/ai_work.c    | 29 +++++++++++++++++++++--------
 9 files changed, 110 insertions(+), 24 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index bd1ec9d..ee54708 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,10 +1,6 @@
-entropie 2.18.0: saving and loading, start menu
+entropie 2.19.0: saves keep the hose and sitting; felling hint
 
-- Start menu: continue (the autosave), new game, load a save, quit
-- Esc menu: save as (type a name), quick save, load a save
-- F5 quick save, F9 quick load; autosave every 5 minutes and on quit
-- Saves: a versioned text file plus the ground (worn paths, wet ground);
-  the world is rebuilt and everything put back (people, pockets, clothes,
-  tents, crates, felled trees, the car, the weather, the time)
-- Format version and migration steps so old saves keep working
-- Fix: a freed people list could be used after rebuilding the world
+- Saves keep the hose (where the rope lies, melted and taped bits) and
+  who sits where (they sit down again after loading)
+- Felling yourself: the hint line says where the tree comes down and what
+  it would hit; nearly through, people in the way step aside
diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index ff35b49..03091c7 100644
--- a/entropie/ARCHITECTURE.md
+++ b/entropie/ARCHITECTURE.md
@@ -88,7 +88,7 @@ of view (objects, 8×8 terrain tiles).
   - A key renamed or its meaning changed: raise `SAVE_FORMAT` (save.h) and add a step to `migrate()` that rewrites the old lines.
   - A save from a newer format is refused, never guessed at.
   - A thing the scene no longer makes is dropped; one it newly makes keeps its fresh state.
-- Not kept (comes back fresh): what anybody was in the middle of (AI plans, paths, poses; sitting: they stand), the hose (lies at its tap), sparks and smoke.
+- Not kept (comes back fresh): what anybody was in the middle of (AI plans, paths, poses), sparks and smoke. Kept specially: the hose's rope (`rope` line: every point, melted, taped), who sits where (`sit` line: sat down again, staying a while).
 
 ## Versions
 
diff --git a/entropie/Makefile b/entropie/Makefile
index 49a6ec8..197ffe1 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.18.0
+VERSION   := 2.19.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 3fa2578..2d9f51a 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -460,6 +460,8 @@ Hold the left button to keep working.
 - Felling, they look where the tree will come down first: never onto a house,
   wall, car, tent, table or the fire (no room anywhere: they leave it). Nearly
   through, anybody in the way hears "Timber!" and steps aside; they wait for you
+- You felling: the hint line says where it comes down and what's in the way
+  there; nearly through, the others step out of the way
 - Using or taking something shows a short line about it
 
 Logs weigh what their wood weighs. Click one to hold on, call helpers and
diff --git a/entropie/STATE.md b/entropie/STATE.md
index f940ffe..22cd0f8 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -272,6 +272,10 @@ Round 2.18:
 - Menus (`main.c`): a start menu over the waiting world (Continue: the autosave, New game, Load a save, Quit); Esc: Resume, Save as (`Platform_typing`, `Input.text` / `erase`: letters, digits, - _), Quick save, Load a save, Keys, Quit (autosaves). F5 quick save, F9 quick load. Autosave every AUTOSAVE_EVERY 300 s of play and on quit / closing the window. Folder: `Platform_saveDir` (SDL_GetPrefPath + "saves").
 - Tested in the harness: a save written and read back (the same process, and another one): people, pockets, a tent half taken down, crates, rain, the wet ground, a pullover put down, a felled tree with fewer branches; under AddressSanitizer. The menus themselves aren't run here (no SDL library to link): compiled with all warnings only.
 
+Round 2.19:
+- Saves: the hose's rope (`rope` length + every node: where, melted, taped; was laid out fresh from the tap) and the nozzle where it was (OBJ_HOSE no longer "fixed"); people sitting (`sit` seat + facing) sit down again on loading and stay 20-35 s (they stood up and walked off at once).
+- Felling, you (`game_actions.c` fellingHint, before the ladder hint): an axe or the chainsaw at a standing tree: the hint line says it falls away from you, or "Careful: it would come down on the car - fell it from another side" (`Ai_fallsOnto`: the felling-room rays, the first thing hit). Over 0.6 through and somebody where it'll come down: "Timber! ..." and they step aside (`Ai_warnFellingRoom`, now with the way it falls; NPC fellers use their plan).
+
 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).
@@ -413,7 +417,7 @@ Most important first.
 - More gestures and feelings to pick personalities from: the lists in Body / Fidgets and Gestures can grow
 
 **People**
-- The work place, still to do: trash jobs; walking a heavy piece round tight corners; the felling room when you fell (a hint where it'll come down)
+- The work place, still to do: trash jobs; walking a heavy piece round tight corners
 - Ladders, still to do: a holder for a ladder standing free; NPCs taking a ladder somewhere for their own work
 - Smoking, still to do: paying for a pack (no money in the game yet)
 - "No shirt no service" policy (decision open)
@@ -422,7 +426,7 @@ Most important first.
 - Weather, still to do: a sound for thunder, wind and rain (no sound in the game yet); a hat or anything held blown out of the hands
 
 **Saves, still to do**
-- The hose (its rope) and things being done (sitting, a job half way, a ladder being climbed) come back fresh, not as they were
+- Things being done (a job half way, a ladder being climbed, carrying something together) come back fresh, not as they were
 
 **Tools for us**
 - Optional local AI (llama.cpp, small model) for talking and tasks; the menu stays as fallback
@@ -431,4 +435,4 @@ Most important first.
 - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene)
 
 ---
-State of `entropie 2.18.0 (build a8a2692)`
+State of `entropie 2.19.0 (build 04c05e7)`
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index de3c32d..d7d14cd 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -984,6 +984,40 @@ static const Object *askingYou(const Game *game) {
     return NULL;
 }
 
+/* What a tree would come down on (for the hint line). */
+static const char *thingName(const Object *o) {
+    switch (o->type) {
+    case OBJ_HOUSE:     return "house";
+    case OBJ_BARN:      return "barn";
+    case OBJ_STRUCTURE: case OBJ_WALL: return "building";
+    case OBJ_CAR:       return "car";
+    case OBJ_TENT:      return "tent";
+    case OBJ_FIREPLACE: return "fire";
+    case OBJ_WOODPILE:  return "woodpile";
+    case OBJ_FURNITURE: return o->variant == FURN_BENCH ? "bench" : o->variant == FURN_TABLE ? "table" : o->variant == FURN_CHAIR ? "chair" : "furniture";
+    default:            return "something";
+    }
+}
+
+/* At a tree with an axe or the chainsaw: where it'll come down (away from
+   you), what's in the way there, and nearly through: everybody out of it. */
+static bool fellingHint(Game *game, char *out, size_t size) {
+    Object *p = game->player;
+    ToolKind tool = Tools_held(p, NULL);
+    if (tool != TOOL_AXE && tool != TOOL_CHAINSAW) return false;
+    Object *tree = Tools_target(p, game->world.objects, game->world.count);
+    if (!tree || tree->type != OBJ_TREE || tree->toppled) return false;
+    Vec3 dir = vec3_sub(tree->position, p->position);
+    dir.y = 0.0f;
+    if (vec3_length(dir) < 1e-3f) return false;
+    const Object *onto = Ai_fallsOnto(tree, dir);
+    bool someone = Tools_fellDone(tree) > 0.6f && Ai_warnFellingRoom(p, game->world.objects, game->world.count, tree, dir);
+    if (someone) snprintf(out, size, "Timber! Somebody's where it'll come down - wait till they're out of the way");
+    else if (onto) snprintf(out, size, "Careful: it would come down on the %s - fell it from another side", thingName(onto));
+    else snprintf(out, size, "It falls away from you - clear there");
+    return true;
+}
+
 const char *Game_hint(Game *game) {
     const Object *shown = follower(game, SHOW_PLACE), *watcher = follower(game, SHOW_HOW), *ladder, *food, *asking;
     Object *fire = NULL;
@@ -1010,6 +1044,7 @@ const char *Game_hint(Game *game) {
     } else if (fire && fire->burning && Hands_bottleHand(game->player) >= 0 &&
                game->player->hands.item[Hands_bottleHand(game->player)]->sips > 0)
         snprintf(game->hint, sizeof(game->hint), "The fire  -  C: pour your drink on it");
+    else if (fellingHint(game, game->hint, sizeof(game->hint))) {}
     else if (!game->player->on_ladder && !game->player->climbing && (ladder = Ladder_near(&game->world, game->player, &at_top)))
         snprintf(game->hint, sizeof(game->hint), at_top ? "Ladder: left click to climb down"
                  : "Ladder: left click to climb  -  E / Q or right click: take it down");
diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c
index 3975626..be17474 100644
--- a/entropie/src/game/save.c
+++ b/entropie/src/game/save.c
@@ -9,6 +9,7 @@
 #include "world/weather.h"
 #include "world/wetness.h"
 #include "people/hands.h"
+#include "world/hose.h"
 #include "build_info.h"
 
 #include <dirent.h>
@@ -83,7 +84,7 @@ static const Field PERSON[] = {
 static bool fixedThing(const Object *o) {
     switch (o->type) {
     case OBJ_HOUSE: case OBJ_BARN: case OBJ_WALL: case OBJ_STRUCTURE: case OBJ_DECAL: case OBJ_BUSH: case OBJ_BIN:
-    case OBJ_BOX_STATIC: case OBJ_CYLINDER_STATIC: case OBJ_SUPPLY: case OBJ_HOSE:
+    case OBJ_BOX_STATIC: case OBJ_CYLINDER_STATIC: case OBJ_SUPPLY:
         return o->scene_id > 0;
     default:
         return false;
@@ -202,11 +203,22 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err,
             fprintf(f, "\n");
             if (o->keys) fprintf(f, "keys %s\n", o->keys);
             if (o->body.asleep) fprintf(f, "asleep 1\n");
+            if (o->sitting && !o->riding) /* (where they sit: on what, facing where) */
+                fprintf(f, "sit %d %.9g %.9g %.9g\n", idOf(&ids, o->seat), (double)o->sit_dir.x, (double)o->sit_dir.y, (double)o->sit_dir.z);
             for (int h = 0; h < 2; h++)
                 if (o->hands.item[h] && o->hands.state[h] == HAND_HOLDING) fprintf(f, "hand %d %d\n", h, idOf(&ids, o->hands.item[h]));
         } else {
             writeFields(f, o, THING, COUNT(THING), &ids);
             if (o->cloth_where) fprintf(f, "where %s\n", o->cloth_where);
+            if (o->type == OBJ_HOSE) { /* its rope: where every bit lies, melted, taped */
+                const HoseRope *rope = Hose_of((World *)w, o);
+                if (rope && rope->ready) {
+                    fprintf(f, "rope %.9g %d", (double)rope->length, HOSE_NODES);
+                    for (int k = 0; k < HOSE_NODES; k++)
+                        fprintf(f, " %.6g %.6g %.6g %.4g %d", (double)rope->p[k].x, (double)rope->p[k].y, (double)rope->p[k].z, (double)rope->melt[k], rope->taped[k]);
+                    fprintf(f, "\n");
+                }
+            }
             if (o->type == OBJ_CAR && o->load) {
                 fprintf(f, "load %d", o->load->n);
                 for (int k = 0; k < o->load->n; k++) fprintf(f, " %d %d", o->load->kind[k], idOf(&ids, o->load->owner[k]));
@@ -496,6 +508,21 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
             else if (!strcmp(key, "keys")) o->keys = keysText(s);
             else if (!strcmp(key, "where")) o->cloth_where = Clothes_whereText(s);
             else if (!strcmp(key, "name")) snprintf(o->name, sizeof o->name, "%s", s);
+            else if (!strcmp(key, "rope") && o->type == OBJ_HOSE) {
+                HoseRope *rope = Hose_of(w, o);
+                float len = numF(&s);
+                int n = numI(&s);
+                if (rope && n == HOSE_NODES) {
+                    rope->length = len;
+                    for (int q = 0; q < HOSE_NODES; q++) {
+                        rope->p[q] = rope->prev[q] = numV(&s);
+                        rope->melt[q] = numF(&s);
+                        rope->taped[q] = numI(&s) != 0;
+                        rope->heat[q] = 0.0f;
+                    }
+                    rope->ready = true;
+                }
+            }
             else if (!strcmp(key, "load") && o->type == OBJ_CAR && o->load) {
                 int n = numI(&s);
                 o->load->n = 0;
@@ -550,12 +577,15 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
         for (int q = 0; q < AI_SKIPS; q++) o->ai.skip[q] = NULL;
         if (o->ai.mode != AI_IDLE && o->ai.mode != AI_WANDER && o->ai.mode != AI_WORK) o->ai.mode = AI_IDLE;
         Object_syncColliders(o);
-        bool asleep = false;
+        bool asleep = false, sits = false;
+        Object *seat = NULL;
+        Vec3 sit_dir = vec3(0.0f, 0.0f, 1.0f);
         for (int i = r->first; i < r->first + r->lines; i++) {
             char buf[256], *s = buf;
             snprintf(buf, sizeof buf, "%s", p.line[i]);
             char *key = word(&s);
             if (!strcmp(key, "asleep")) asleep = true;
+            else if (!strcmp(key, "sit")) { sits = true; seat = byId(&p, numI(&s)); sit_dir = numV(&s); }
             else if (!strcmp(key, "hand")) {
                 int h = numI(&s);
                 Object *item = byId(&p, numI(&s));
@@ -575,6 +605,8 @@ bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t
             o->in_tent = NULL;
             Tent_crawlIn(tent, o);
         } else if (asleep) Body_lieDown(&o->body);
+        else if (sits && Object_sit(o, seat, vec3_add(o->position, vec3_scale(sit_dir, 3.0f)))) /* (sat down again where they sat ... */
+            o->ai.timer = 20.0f + 0.37f * (float)(k % 41);                                    /* ... and stay a while) */
     }
     for (int k = 0; k < p.recs; k++) /* (pullovers last: they need their owners' look) */
         if (p.rec[k].obj && p.rec[k].obj->type == OBJ_CLOTHES) Clothes_rebuild(p.rec[k].obj);
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index c506779..50d3789 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -255,6 +255,10 @@ bool Ai_resumePaused(struct Object *self);
 void Ai_say(struct Object *self, const char *text);
 /* Starts working on an order around area (they must know how). */
 void Ai_order(struct Object *self, Order order, Vec3 area);
+/* A tree felled falling that way: the first thing it would come down on (a house, a car, a tent ...), or NULL. */
+const struct Object *Ai_fallsOnto(const struct Object *tree, Vec3 dir);
+/* Somebody where it'll come down (falling that way): told to get out (they step aside). True if anybody is there. */
+bool Ai_warnFellingRoom(struct Object *feller, struct Object *const *objects, int count, struct Object *tree, Vec3 dir);
 void Ai_stopWork(struct Object *self);
 /* Follows the player to be shown how to do the pending order, or where the woodpile goes. */
 void Ai_show(struct Object *self, Showing what, const struct Object *player);
diff --git a/entropie/src/people/ai_work.c b/entropie/src/people/ai_work.c
index e2921a6..9f9a350 100644
--- a/entropie/src/people/ai_work.c
+++ b/entropie/src/people/ai_work.c
@@ -38,8 +38,9 @@ static bool notInTheWay(const Object *o, const void *tree) {
     }
 }
 
-/* What it would come down on, falling that way (counted). */
-static int fallHits(const Object *tree, Vec3 dir) {
+/* What it would come down on, falling that way (counted; the first in *first). */
+static int fallHits(const Object *tree, Vec3 dir, Object **first) {
+    if (first) *first = NULL;
     if (!g_world) return 0;
     Vec3 side = vec3(-dir.z, 0.0f, dir.x);
     float tall = treeTall(tree), r = crownR(tree);
@@ -49,7 +50,11 @@ static int fallHits(const Object *tree, Vec3 dir) {
             float w = (along > tall * 0.45f ? r : 0.4f) * (float)s * 0.8f; /* (the trunk narrow, the crown wide) */
             Vec3 p = vec3_add(tree->position, vec3_add(vec3_scale(dir, along), vec3_scale(side, w)));
             p.y = tree->position.y + 14.0f;
-            if (World_rayCast(g_world, p, vec3(0.0f, -1.0f, 0.0f), 14.5f, notInTheWay, tree, NULL, NULL) >= 0.0f) hits++;
+            Object *hit = NULL;
+            if (World_rayCast(g_world, p, vec3(0.0f, -1.0f, 0.0f), 14.5f, notInTheWay, tree, &hit, NULL) >= 0.0f) {
+                hits++;
+                if (first && !*first) *first = hit;
+            }
         }
     return hits;
 }
@@ -63,7 +68,7 @@ static Vec3 fellPlan(Object *tree, Vec3 from) {
     for (int k = 0; k < FELL_DIRS; k++) { /* (nearest first: 0, +30, -30, +60 ...) */
         float a = (float)((k + 1) / 2) * (2.0f * PI_F / (float)FELL_DIRS) * (k % 2 ? 1.0f : -1.0f);
         Vec3 d = vec3(want.x * cosf(a) - want.z * sinf(a), 0.0f, want.x * sinf(a) + want.z * cosf(a));
-        int hits = fallHits(tree, d);
+        int hits = fallHits(tree, d, NULL);
         if (hits < best_hits) { best_hits = hits; best = d; }
         if (hits == 0) break;
     }
@@ -72,9 +77,17 @@ static Vec3 fellPlan(Object *tree, Vec3 from) {
     return best;
 }
 
+const Object *Ai_fallsOnto(const Object *tree, Vec3 dir) {
+    Object *first = NULL;
+    dir = vec3_normalize(vec3(dir.x, 0.0f, dir.z));
+    fallHits(tree, dir, &first);
+    return first;
+}
+
 /* Anybody standing where it'll come down: warned off (they step out sideways). */
-static bool clearFellingRoom(Object *self, Object *const *objects, int count, Object *tree) {
-    Vec3 dir = tree->fell_plan, side = vec3(-dir.z, 0.0f, dir.x);
+bool Ai_warnFellingRoom(Object *self, Object *const *objects, int count, Object *tree, Vec3 dir) {
+    dir = vec3_normalize(vec3(dir.x, 0.0f, dir.z));
+    Vec3 side = vec3(-dir.z, 0.0f, dir.x);
     float tall = treeTall(tree) + 1.5f, r = crownR(tree) + 0.6f;
     bool told = false, anybody = false;
     for (int i = 0; i < count; i++) {
@@ -84,7 +97,7 @@ static bool clearFellingRoom(Object *self, Object *const *objects, int count, Ob
         float along = vec3_dot(rel, dir), across = vec3_dot(rel, side);
         if (along < -0.5f || along > tall || fabsf(across) > r) continue;
         anybody = true;
-        if (!told && self->ai.speech_time <= 0.0f) {
+        if (!told && self->type != OBJ_PLAYER && self->ai.speech_time <= 0.0f) {
             Ai_say(self, o->type == OBJ_PLAYER ? "Timber! Get out of the way - it's coming down there!" : "Timber! Out of the way!");
             told = true;
         }
@@ -1341,7 +1354,7 @@ void ai_updateWork(Object *self, Object *const *objects, int count, float dt) {
                 skipTarget(ai, t.target);
                 return;
             }
-            ai->timber = Tools_fellDone(t.target) > FELL_WARN && !Tools_isWorking(self) && clearFellingRoom(self, objects, count, t.target);
+            ai->timber = Tools_fellDone(t.target) > FELL_WARN && !Tools_isWorking(self) && Ai_warnFellingRoom(self, objects, count, t.target, t.target->fell_plan);
             if (ai->timber) { stand(self); return; } /* (not the last strokes with somebody under it) */
         }
         if (prepareSpot(self, objects, count, &t, dt)) return;