Nimbin[12]?Games / Entropie / commits / 9ccac11

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.61.0: step ladder

Christian Immanuel · 2026-10-15 01:30 · 9ccac11addef2166ec5d1993a9dcc660d26ca869

- A step ladder lies in the tool room, in front of the other two
- A click opens it and stands it in front of you; with a wall just ahead
  it stands with its back to it, and from its top you climb onto the wall
- Climb it like the others (feet on the treads, hands on the top);
  E / Q folds it up again

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                  |   7 +-
 entropie/commit_message.txt        |  15 ++-
 entropie/src/game/game_actions.c   |   7 +-
 entropie/src/render/shapes.h       |  10 ++
 entropie/src/render/shapes_props.c |  51 ++++++++++
 entropie/src/world/ladder.c        | 192 ++++++++++++++++++++++++++-----------
 entropie/src/world/ladder.h        |   1 +
 entropie/src/world/object.c        |   5 +-
 entropie/src/world/site.c          |   1 +
 11 files changed, 226 insertions(+), 69 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index 2ed03c0..81a5ea7 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 1.60.0
+VERSION   := 1.61.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 23241d4..298892f 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -320,6 +320,10 @@ A hand that can't reach its grip lets go and takes hold again.
 - Beside it lies a folding ladder (two 2.5 m halves on a hinge, 5 m open).
   A click at a wall unfolds it and leans it there; elsewhere a click unfolds
   or folds it
+- In front of them lies a step ladder. A click stands it up in front of you
+  and opens it; with a wall just ahead it stands with its back to it, and
+  from its top you climb onto the wall if that's not much higher. E / Q
+  folds it up again
 - Ladder in hand: click at a wall and it slides out just as far as it takes
   (about 0.9 m over the edge) and leans there in one go: you stand it up,
   slide it out and tip it onto the wall (about a second); slid out by hand
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 8945bf8..0737850 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -95,6 +95,8 @@ Ladder: one click at a wall slides it out to fit (top on the edge, 0.9 m over it
 
 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.
+
 Raised ladder: slid out (extend goal > 0.5) and in hand, it's held 0.45 m up from its foot, the hand in front, the other hand higher up the rail, tipped up 60 deg in front (43-83 deg with the view pitch, eased 1.6 rad/s; `ladder.c` Ladder_placeHeld). A click leans it where you look (view ray, ladder length + 1.5 m; else a ray along the ladder, length + 0.8 m; a top face: the side towards you under that edge). Aimed within 0.45 m of the top it rests on the edge; lower, against the face at that height, flatter (no flatter than 55 deg: "aim at least X m up"); its foot at most 1.8 m from you. Looking down (view y < -0.6) a click slides it together; nothing aimed at: says so.
 
 Carrying long things (ladder, logs: 0.62 m and longer): they stop at people and the dog like at walls (unless you carry it together), push them out of the way (up to 25 cm a frame), knock light loose things aside; NPCs treat a long thing someone carries as in the way and walk round it (`world.c` pushOffCarried, `ai.c` ai_blockedBy).
@@ -109,7 +111,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 and folding 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, telescopic, folding and step ladder, axe, shears, chainsaw, trees (fruit trees too), bushes, cars, buildings.
 
 ## World
 
@@ -148,7 +150,6 @@ Most important first.
 - Clothing (trousers, skirt, dress, fly) – needed for the above
 
 **Things**
-- More ladder types: a step ladder (stands on its own)
 - 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)
 
 **Tools for us**
@@ -159,4 +160,4 @@ Most important first.
 - The walking map rebuild takes ~9 ms when fixed things change (a felled tree)
 
 ---
-State of `entropie 1.60.0 (build 6e3e5e9)`
+State of `entropie 1.61.0 (build e870d0c)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index bdbb1ab..5ca9a15 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,9 +1,14 @@
-entropie 1.60.0: folding ladder
+entropie 1.61.0: step ladder
 
-- A folding ladder (two 2.5 m halves on a hinge, 5 m open) lies beside
-  the telescopic one in the tool room
-- One click at a wall unfolds it in the put-up animation and leans it
-  there; elsewhere a click unfolds or folds it
+- A step ladder lies in the tool room, in front of the other two
+- A click opens it and stands it in front of you; with a wall just ahead
+  it stands with its back to it, and from its top you climb onto the wall
+- Climb it like the others (feet on the treads, hands on the top);
+  E / Q folds it up again
+
+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
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index e650ffa..96d75a6 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -358,7 +358,10 @@ static bool useLadder(Game *game) {
         if (was_in && lean != LEAN_DONE) { Ladder_setExtend(ladder, extend_was); ladder->extend_goal = 0.0f; }
         if (lean == LEAN_DONE) game->putting_up = ladder;
         const bool fold = ladder->variant == LADDER_FOLDING;
-        if (lean == LEAN_DONE)
+        if (lean == LEAN_DONE && ladder->variant == LADDER_STEP)
+            say(game, ladder->lean_top > 0.0f ? "You stand the step ladder there - climb it to get up there"
+                                              : "You stand the step ladder up - click it to climb");
+        else if (lean == LEAN_DONE)
             say(game, ladder->lean_top > 0.0f ? (was_in ? (fold ? "You unfold the ladder and lean it there - click it to climb"
                                                                : "You slide the ladder out and lean it there - click it to climb")
                                                         : "The ladder leans there - click it to climb")
@@ -390,7 +393,7 @@ static bool takeLadder(Game *game) {
     bool at_top = false;
     Object *ladder = Ladder_near(&game->world, p, &at_top);
     if (!ladder || at_top || Hands_freeHand(p) < 0) return false;
-    if (Ladder_take(p, ladder)) say(game, "You take the ladder down");
+    if (Ladder_take(p, ladder)) say(game, ladder->variant == LADDER_STEP ? "You fold the step ladder up" : "You take the ladder down");
     return true;
 }
 
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index d086276..b191c5a 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -141,6 +141,16 @@ Mesh  Shapes_ladder(float extend);
 float Shapes_foldLadderLength(float unfold);
 float Shapes_foldLadderRung(int k);
 Mesh  Shapes_foldLadder(float unfold);
+/* The step ladder: a front leg with flat treads (+Y up it, +Z its front) and a
+   back leg hinged at the top. open 0 = folded flat .. 1 = standing (legs spread). */
+#define STEP_TREADS   5     /* the top platform comes on top of these */
+#define STEP_HINGE_Z -0.06f /* the back leg's hinge, behind the front leg */
+float Shapes_stepLadderLength(void);
+/* Tread k along the front leg; k = STEP_TREADS: the top platform. */
+float Shapes_stepLadderRung(int k);
+/* Angle between the legs (rad). */
+float Shapes_stepLadderSpread(float open);
+Mesh  Shapes_stepLadder(float open);
 
 /* A round bush, base at y = 0. */
 Mesh  Shapes_bush(float diameter, float height, unsigned seed);
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 68edd06..9292cbb 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -918,6 +918,57 @@ Mesh Shapes_foldLadder(float unfold) {
     return Mesh_fromBuilder(&b);
 }
 
+/* The step ladder: open, the legs stand 30 degrees apart, each 15 off upright;
+   the treads are set at that angle so they're level when it stands. */
+#define STEP_LEN    1.95f
+#define STEP_FIRST  0.27f
+#define STEP_PITCH  0.29f
+#define STEP_SPREAD 0.5236f
+#define STEP_SHUT   0.03f  /* (folded: the back leg lies just behind the front one) */
+
+float Shapes_stepLadderLength(void) { return STEP_LEN; }
+float Shapes_stepLadderRung(int k) {
+    return k >= STEP_TREADS ? STEP_LEN - 0.03f : STEP_FIRST + STEP_PITCH * (float)k;
+}
+float Shapes_stepLadderSpread(float open) { return STEP_SHUT + (STEP_SPREAD - STEP_SHUT) * clampf(open, 0.0f, 1.0f); }
+
+Mesh Shapes_stepLadder(float open) {
+    const Vec3 alu = { 0.74f, 0.76f, 0.78f }, tread = { 0.55f, 0.57f, 0.60f }, plastic = { 0.85f, 0.32f, 0.12f },
+               rubber = { 0.15f, 0.15f, 0.16f };
+    const float half = LADDER_WIDTH * 0.5f, level = STEP_SPREAD * 0.5f;
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    /* the front leg: rails, rubber feet, treads (level when it stands) */
+    for (int s = -1; s <= 1; s += 2) {
+        float x = (float)s * half;
+        addBlock(&b, vec3(x, STEP_LEN * 0.5f, 0.0f), vec3(0.03f, STEP_LEN, 0.07f), alu);
+        addBlock(&b, vec3(x, 0.02f, 0.0f), vec3(0.045f, 0.04f, 0.085f), rubber);
+    }
+    for (int k = 0; k < STEP_TREADS; k++) {
+        size_t first = b.vertex_count;
+        addBlock(&b, vec3_zero(), vec3(2.0f * half - 0.02f, 0.025f, 0.11f), tread);
+        transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, Shapes_stepLadderRung(k), -0.015f)), mat4_rotate_x(-level)));
+    }
+    { /* the top: a plastic platform over the hinge */
+        size_t first = b.vertex_count;
+        addBlock(&b, vec3(0.0f, 0.0f, -0.05f), vec3(2.0f * half + 0.06f, 0.05f, 0.2f), plastic);
+        transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, STEP_LEN - 0.02f, 0.0f)), mat4_rotate_x(-level)));
+    }
+    /* the back leg: rails and two braces, swung out from the hinge */
+    size_t first = b.vertex_count;
+    const float back = STEP_LEN - 0.05f;
+    for (int s = -1; s <= 1; s += 2) {
+        float x = (float)s * (half - 0.02f);
+        addBlock(&b, vec3(x, back * 0.5f, 0.0f), vec3(0.03f, back, 0.05f), alu);
+        addBlock(&b, vec3(x, back - 0.02f, 0.0f), vec3(0.045f, 0.04f, 0.07f), rubber);
+    }
+    for (int k = 1; k <= 2; k++)
+        addBlock(&b, vec3(0.0f, back * (float)k / 3.0f, 0.0f), vec3(2.0f * half - 0.06f, 0.025f, 0.025f), alu);
+    transformSince(&b, first, mat4_mul(mat4_translate(vec3(0.0f, STEP_LEN - 0.04f, STEP_HINGE_Z)),
+                                       mat4_rotate_x(PI_F - Shapes_stepLadderSpread(open))));
+    return Mesh_fromBuilder(&b);
+}
+
 Mesh Shapes_ladder(float extend) {
     const Vec3 alu = { 0.76f, 0.78f, 0.80f }, joint = { 0.20f, 0.20f, 0.22f };
     const float half = LADDER_WIDTH * 0.5f, top = Shapes_ladderLength(extend);
diff --git a/entropie/src/world/ladder.c b/entropie/src/world/ladder.c
index 68148c6..6377464 100644
--- a/entropie/src/world/ladder.c
+++ b/entropie/src/world/ladder.c
@@ -26,34 +26,56 @@
 #define AIM_FOOT      1.8f    /* its foot no further than this from you (else: step closer) */
 #define AIM_PAST      0.8f    /* held up, it leans on what its top points at up to this much past it */
 #define AIM_EDGE      0.45f   /* ... aimed this close under the top of it: the top rests on the edge */
+#define STEP_FOLD_SPEED 2.5f  /* step ladder: folds up this fast (of the way per second) */
+#define STEP_IN_FRONT 0.45f   /* ... stood up: its front feet this far in front of you */
+#define STEP_AT_WALL  0.6f    /* ... a wall this close past its back feet: it stands with its back to it */
+#define STEP_WALL_GAP 0.08f   /* ... its back feet this far from that wall */
+#define STEP_OFF_UP   0.9f    /* ... from its top you get onto that if it's no higher than this over the top */
 
 static const Vec3 UP = { 0.0f, 1.0f, 0.0f };
 
 static Vec3 flatv(Vec3 v) { return vec3(v.x, 0.0f, v.z); }
 static float flatDist(Vec3 a, Vec3 b) { return vec3_length(flatv(vec3_sub(a, b))); }
 
-/* Two kinds: telescopic (variant 0: slides out) and folding (variant 1: two sections on a hinge, unfolds). */
+/* Three kinds: telescopic (variant 0: slides out), folding (two sections on a hinge, unfolds) and the
+   step ladder (stands on its own: extend is how far its legs are open). */
 static bool folding(const Object *l) { return l->variant == LADDER_FOLDING; }
-static float lengthFor(const Object *l, float extend) { return folding(l) ? Shapes_foldLadderLength(extend) : Shapes_ladderLength(extend); }
+static bool stepLadder(const Object *l) { return l->variant == LADDER_STEP; }
+static float lengthFor(const Object *l, float extend) {
+    return stepLadder(l) ? Shapes_stepLadderLength() : folding(l) ? Shapes_foldLadderLength(extend) : Shapes_ladderLength(extend);
+}
 static float lengthOf(const Object *l) { return lengthFor(l, l->extend); }
 static Vec3 upAlong(const Object *l) { return l->colliders[0].axes[1]; }
 static Vec3 pointAt(const Object *l, float along) { return vec3_add(l->position, vec3_scale(upAlong(l), along)); }
 
+/* Colliders for that length (the step ladder: one per leg). */
+static void fitColliders(Object *l, float extend) {
+    float len = lengthFor(l, extend);
+    l->colliders[0].center = vec3(0.0f, len * 0.5f, stepLadder(l) ? -0.015f : 0.0f);
+    l->colliders[0].half_extents = vec3(LADDER_WIDTH * 0.5f + 0.03f, len * 0.5f, stepLadder(l) ? 0.06f : 0.04f);
+    if (stepLadder(l)) {
+        float a = Shapes_stepLadderSpread(extend);
+        Vec3 hinge = vec3(0.0f, len - 0.04f, STEP_HINGE_Z), down = vec3(0.0f, -cosf(a), -sinf(a));
+        l->colliders[1] = (Collider){ .center = vec3_add(hinge, vec3_scale(down, len * 0.5f)),
+                                      .half_extents = vec3(LADDER_WIDTH * 0.5f + 0.01f, len * 0.5f, 0.03f),
+                                      .tilt = vec3(-a, 0.0f, 0.0f) };
+        l->collider_count = 2;
+    }
+    Object_syncColliders(l);
+}
+
 /* New mesh and collider for the current length. */
 static void reshape(Object *l) {
-    Mesh mesh = folding(l) ? Shapes_foldLadder(l->extend) : Shapes_ladder(l->extend);
+    Mesh mesh = stepLadder(l) ? Shapes_stepLadder(l->extend) : folding(l) ? Shapes_foldLadder(l->extend) : Shapes_ladder(l->extend);
     if (mesh.vao) {
         Mesh_destroy(&l->mesh);
         l->mesh = mesh;
     }
-    float len = lengthOf(l);
-    l->colliders[0].center = vec3(0.0f, len * 0.5f, 0.0f);
-    l->colliders[0].half_extents = vec3(LADDER_WIDTH * 0.5f + 0.03f, len * 0.5f, 0.04f);
-    Object_syncColliders(l);
+    fitColliders(l, l->extend);
 }
 
 void Ladder_slide(Object *l) {
-    if (l->leaning || l->placing > 0.0f) return;
+    if (l->leaning || l->placing > 0.0f || stepLadder(l)) return;
     l->extend_goal = l->extend_goal > 0.5f ? 0.0f : 1.0f;
 }
 
@@ -92,7 +114,7 @@ void Ladder_update(Object *l, float dt) {
     if (l->placing > 0.0f) { putUp(l, dt); return; }
     if (l->raised > 0.0f) l->raised += clampf(l->raise_goal - l->raised, -RAISE_SPEED * dt, RAISE_SPEED * dt);
     if (l->extend == l->extend_goal) return;
-    float step = SLIDE_SPEED * dt;
+    float step = (stepLadder(l) ? STEP_FOLD_SPEED : SLIDE_SPEED) * dt;
     l->extend = l->extend < l->extend_goal ? fminf(l->extend + step, l->extend_goal) : fmaxf(l->extend - step, l->extend_goal);
     reshape(l);
 }
@@ -211,9 +233,99 @@ static Object *aimedAt(const World *world, const Object *who, const Object *l, V
     return thing;
 }
 
+/* Lets go of it and puts it up there (stood up, slid out or opened, tipped: see putUp).
+   n: out towards where you stand at it. */
+static void startPutUp(Object *who, Object *l, Vec3 foot, Vec3 up, Vec3 z, Vec3 n, float extend_to, float top) {
+    Vec3 from_foot = l->position, from_axis = upAlong(l), from_face = l->colliders[0].axes[2];
+    for (int h2 = 0; h2 < 2; h2++)
+        if (who->hands.item[h2] == l) Hands_drop(who, h2);
+    l->velocity = l->angular_velocity = vec3_zero();
+    l->is_static = true;
+    l->leaning = false;
+    l->lean_normal = n;
+    l->placing = 1e-4f;
+    l->place_foot[0] = from_foot;
+    l->place_foot[1] = foot;
+    l->place_axis[0] = from_axis;
+    l->place_axis[1] = up;
+    l->place_face[0] = from_face;
+    l->place_face[1] = z;
+    l->place_extend[0] = l->extend;
+    l->place_extend[1] = extend_to;
+    l->place_top = top;
+    l->lean_top = l->place_top; /* (known now: the game says whether it reaches) */
+    /* stepping back out of its way, to stand at its foot */
+    float behind = vec3_dot(flatv(vec3_sub(who->position, foot)), n);
+    if (behind < 0.5f) {
+        who->position = vec3_add(who->position, vec3_scale(n, 0.5f - behind));
+        who->body.replant = true;
+        Object_syncColliders(who);
+    }
+}
+
+/* Would it stand there (opened, at extend_to) without going into anything? */
+static bool roomFor(const World *world, const Object *who, const Object *l, const Object *thing, Vec3 foot, Vec3 up, Vec3 z,
+                    float extend_to) {
+    Object stood = *l;
+    stood.extend = extend_to;
+    stood.rotation = euler_yxz_from_axes(up, z);
+    stood.position = foot;
+    stood.is_held = false;
+    fitColliders(&stood, extend_to);
+    for (int i = 0; i < world->count; i++) {
+        const Object *o = world->objects[i];
+        if (o == l || o == thing || o == who || o->is_held || o->collider_count == 0 || o->type == OBJ_DECAL ||
+            o->type == OBJ_PUDDLE || o->type == OBJ_STINK || flatDist(o->position, foot) > 30.0f) continue;
+        if (Physics_penetration(&stood, o) > LEAN_ROOM) return false;
+    }
+    return true;
+}
+
+/* The step ladder: opened and stood in front of you - or, a wall just past
+   it, with its back to the wall (from its top you get up there if it's not too high). */
+static int standStep(World *world, Object *who, Object *l, const char **why) {
+    const float len = Shapes_stepLadderLength(), lean = 0.5f * Shapes_stepLadderSpread(1.0f);
+    const float out = len * sinf(lean), height = len * cosf(lean); /* (each leg's feet this far out from under the top) */
+    Vec3 f = vec3_normalize(flatv(Object_facing(who)));
+    Vec3 foot = vec3_add(who->position, vec3_scale(f, STEP_IN_FRONT));
+    float top = 0.0f;
+    Vec3 face = vec3_zero(), n = vec3_zero();
+    Object *thing = faceAhead(world, who, l, &face, &n);
+    if (thing && fabsf(n.y) < 0.35f) {
+        n = vec3_normalize(flatv(n));
+        float d = vec3_dot(flatv(vec3_sub(face, who->position)), f);
+        if (d < STEP_IN_FRONT + 2.0f * out + STEP_AT_WALL) { /* at it: its back to it */
+            f = vec3_scale(n, -1.0f);
+            foot = vec3_add(face, vec3_scale(n, STEP_WALL_GAP + 2.0f * out));
+            Vec3 inside = vec3_sub(face, vec3_scale(n, 0.06f));
+            float ground = World_groundHeight(world, foot.x, foot.z, who->position.y + 0.8f);
+            float h = topOf(thing, inside.x, inside.z, face.y + 40.0f) - ground;
+            if (h > height - 0.6f && h < height + STEP_OFF_UP) top = len;
+        } else thing = NULL;
+    } else thing = NULL;
+    foot.y = World_groundHeight(world, foot.x, foot.z, who->position.y + 0.8f);
+    if (fabsf(foot.y - who->position.y) > 0.5f) { *why = "No level ground there for the step ladder"; return LEAN_NO; }
+    Vec3 across = vec3_cross(f, UP);
+    for (int leg = 0; leg < 2; leg++) {
+        Vec3 c = vec3_add(foot, vec3_scale(f, 2.0f * out * (float)leg));
+        for (int s = -1; s <= 1; s += 2) {
+            Vec3 p = vec3_add(c, vec3_scale(across, (float)s * LADDER_WIDTH * 0.5f));
+            float g = World_groundHeight(world, p.x, p.z, foot.y + 1.0f);
+            if (g > foot.y + 0.12f || g < foot.y - 0.15f) { *why = "Not level enough there for the step ladder"; return LEAN_NO; }
+        }
+    }
+    Vec3 back = vec3_scale(f, -1.0f); /* (you climb it from this side) */
+    Vec3 up = vec3_normalize(vec3_add(vec3_scale(UP, cosf(lean)), vec3_scale(f, sinf(lean))));
+    Vec3 z = vec3_normalize(vec3_sub(back, vec3_scale(up, vec3_dot(back, up))));
+    if (!roomFor(world, who, l, NULL, foot, up, z, 1.0f)) { *why = "No room for the step ladder there"; return LEAN_NO; }
+    startPutUp(who, l, foot, up, z, back, 1.0f, top);
+    return LEAN_DONE;
+}
+
 int Ladder_lean(World *world, Object *who, Object *l, bool fit, const char **why) {
     Vec3 face = vec3_zero(), n = vec3_zero();
     float aim_y = -1e9f; /* (held up: where its top should go) */
+    if (stepLadder(l)) return standStep(world, who, l, why);
     const bool raised = Ladder_isRaised(l);
     /* (held up and looking down at the ground: slide it together) */
     if (raised && who->type == OBJ_PLAYER && who->body.look_dir.y < LOOK_DOWN) return LEAN_NOTHING;
@@ -278,49 +390,9 @@ int Ladder_lean(World *world, Object *who, Object *l, bool fit, const char **why
     Vec3 up = vec3_normalize(vec3_sub(rest, foot));
     Vec3 z = vec3_normalize(vec3_sub(n, vec3_scale(up, vec3_dot(n, up)))); /* the ladder's face, out from the wall */
     /* Room for all of it there (not through a wall beside it, or anything else) */
-    {
-        Object stood = *l;
-        stood.extend = extend_to;
-        stood.colliders[0].center = vec3(0.0f, len * 0.5f, 0.0f);
-        stood.colliders[0].half_extents = vec3(LADDER_WIDTH * 0.5f + 0.03f, len * 0.5f, 0.04f);
-        stood.rotation = euler_yxz_from_axes(up, z);
-        stood.position = foot;
-        stood.is_held = false;
-        Object_syncColliders(&stood);
-        for (int i = 0; i < world->count; i++) {
-            const Object *o = world->objects[i];
-            if (o == l || o == thing || o == who || o->is_held || o->collider_count == 0 || o->type == OBJ_DECAL ||
-                o->type == OBJ_PUDDLE || o->type == OBJ_STINK || flatDist(o->position, foot) > 30.0f) continue;
-            if (Physics_penetration(&stood, o) > LEAN_ROOM) { *why = "No room for it there - something's in the way"; return LEAN_NO; }
-        }
-    }
+    if (!roomFor(world, who, l, thing, foot, up, z, extend_to)) { *why = "No room for it there - something's in the way"; return LEAN_NO; }
 
-    /* Let go of it and put it up there (stood up, slid out, tipped onto it: see place) */
-    Vec3 from_foot = l->position, from_axis = upAlong(l), from_face = l->colliders[0].axes[2];
-    for (int h2 = 0; h2 < 2; h2++)
-        if (who->hands.item[h2] == l) Hands_drop(who, h2);
-    l->velocity = l->angular_velocity = vec3_zero();
-    l->is_static = true;
-    l->leaning = false;
-    l->lean_normal = n;
-    l->placing = 1e-4f;
-    l->place_foot[0] = from_foot;
-    l->place_foot[1] = foot;
-    l->place_axis[0] = from_axis;
-    l->place_axis[1] = up;
-    l->place_face[0] = from_face;
-    l->place_face[1] = z;
-    l->place_extend[0] = l->extend;
-    l->place_extend[1] = extend_to;
-    l->place_top = reaches ? vec3_length(vec3_sub(rest, foot)) : 0.0f;
-    l->lean_top = l->place_top; /* (known now: the game says whether it reaches) */
-    /* stepping back out of its way, to stand at its foot */
-    float behind = vec3_dot(flatv(vec3_sub(who->position, foot)), n);
-    if (behind < 0.5f) {
-        who->position = vec3_add(who->position, vec3_scale(n, 0.5f - behind));
-        who->body.replant = true;
-        Object_syncColliders(who);
-    }
+    startPutUp(who, l, foot, up, z, n, extend_to, reaches ? vec3_length(vec3_sub(rest, foot)) : 0.0f);
     return LEAN_DONE;
 }
 
@@ -343,7 +415,7 @@ Object *Ladder_near(const World *world, const Object *who, bool *at_top) {
         /* up there, at its top */
         if (l->lean_top > 0.0f) {
             Vec3 edge = Ladder_edge(l);
-            if (flatDist(edge, who->position) < MOUNT_REACH && fabsf(edge.y - who->position.y) < 0.8f) {
+            if (flatDist(edge, who->position) < MOUNT_REACH && fabsf(edge.y - who->position.y) < (stepLadder(l) ? STEP_OFF_UP + 0.1f : 0.8f)) {
                 *at_top = true;
                 return l;
             }
@@ -359,11 +431,13 @@ bool Ladder_take(Object *who, Object *l) {
     l->is_static = false;
     l->lean_top = 0.0f;
     l->rest_time = 0.0f;
+    if (stepLadder(l)) l->extend_goal = 0.0f; /* (folds up as you take it) */
     return Hands_pickUpObject(who, hand, l);
 }
 
 /* Highest the feet go: the hands still find rungs above them. */
 static float highest(const Object *l) {
+    if (stepLadder(l)) return Shapes_stepLadderRung(STEP_TREADS - 1) - 0.02f; /* (on the top tread, not the platform) */
     float top = l->lean_top > 0.0f ? l->lean_top : lengthOf(l);
     return fmaxf(0.0f, top - HANDS_ABOVE - 0.15f);
 }
@@ -414,8 +488,12 @@ int Ladder_climb(Object *who, float input, float dt) {
 }
 
 /* Rung k's height along the ladder. */
-static float rung(const Object *l, int k) { return folding(l) ? Shapes_foldLadderRung(k) : Shapes_ladderRung(l->extend, k); }
-static int rungCount(const Object *l) { return folding(l) ? (l->extend > 0.99f ? 2 * FOLD_RUNGS : FOLD_RUNGS) : LADDER_RUNGS; }
+static float rung(const Object *l, int k) {
+    return stepLadder(l) ? Shapes_stepLadderRung(k) : folding(l) ? Shapes_foldLadderRung(k) : Shapes_ladderRung(l->extend, k);
+}
+static int rungCount(const Object *l) {
+    return stepLadder(l) ? STEP_TREADS + 1 : folding(l) ? (l->extend > 0.99f ? 2 * FOLD_RUNGS : FOLD_RUNGS) : LADDER_RUNGS;
+}
 
 /* The rung at or just above `along`. */
 static int rungAbove(const Object *l, float along) {
@@ -476,8 +554,8 @@ void Ladder_poseFeet(Object *who, float dt) {
     int k = rungAbove(l, who->ladder_at - 0.02f);
     for (int i = 0; i < 2; i++) {
         /* one foot a rung higher than the other, swapping as you climb */
-        int kf = k + ((k + i) % 2 == 0 ? 0 : 1);
-        if (kf >= rungCount(l)) kf = rungCount(l) - 1;
+        int kf = k + ((k + i) % 2 == 0 ? 0 : 1), last = rungCount(l) - (stepLadder(l) ? 2 : 1); /* (no feet on the platform) */
+        if (kf > last) kf = last;
         float side = i == 0 ? 1.0f : -1.0f;
         Foot *foot = &who->body.feet[i];
         Vec3 goal = vec3_add(pointAt(l, rung(l, kf)), vec3_add(vec3_scale(across, side * 0.11f),
@@ -494,7 +572,7 @@ void Ladder_poseFeet(Object *who, float dt) {
 }
 
 bool Ladder_isRaised(const Object *l) {
-    return l->type == OBJ_LADDER && l->is_held && l->extend_goal > 0.5f && l->placing <= 0.0f;
+    return l->type == OBJ_LADDER && !stepLadder(l) && l->is_held && l->extend_goal > 0.5f && l->placing <= 0.0f;
 }
 
 /* How steep a raised ladder is held: up with the view (more upright looking up). */
diff --git a/entropie/src/world/ladder.h b/entropie/src/world/ladder.h
index 3670244..d0d429e 100644
--- a/entropie/src/world/ladder.h
+++ b/entropie/src/world/ladder.h
@@ -22,6 +22,7 @@ void        Ladder_setExtend(Object *ladder, float extend);
 void        Ladder_update(Object *ladder, float dt);
 
 #define LADDER_FOLDING 1 /* variant: two sections on a hinge (unfolds); 0: telescopic (slides out) */
+#define LADDER_STEP    2 /* variant: a step ladder (opens and stands on its own) */
 
 enum { LEAN_NOTHING, LEAN_DONE, LEAN_NO };
 /* Leans the ladder who carries against what's right in front. LEAN_NO sets why.
diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c
index eb85ccd..3bddc40 100644
--- a/entropie/src/world/object.c
+++ b/entropie/src/world/object.c
@@ -954,7 +954,10 @@ void Object_describe(const Object *obj, char *out, size_t size) {
     }
     case OBJ_HOSE: snprintf(out, size, "Garden hose - left click (hold) to spray: washes away puddles and dirt"); break;
     case OBJ_LADDER:
-        if (obj->variant == 1) /* (folding) */
+        if (obj->variant == 2) /* (step ladder) */
+            snprintf(out, size, obj->leaning ? "Step ladder - left click: climb (W / S),  E / Q: fold it up"
+                              : "Step ladder - left click: stand it up in front of you (at a wall: with its back to it)");
+        else if (obj->variant == 1) /* (folding) */
             snprintf(out, size, obj->leaning ? "Folding ladder - left click: climb (W / S),  E / Q: take it down"
                               : obj->extend_goal > 0.5f ? "Folding ladder (unfolded) - left click where it should lean, look down: fold it"
                               : "Folding ladder (folded) - left click at a wall: unfold it and lean it there");
diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c
index 8ee92df..31d658e 100644
--- a/entropie/src/world/site.c
+++ b/entropie/src/world/site.c
@@ -730,6 +730,7 @@ static bool furnishGarages(World *world, Vec3 origin, float yaw, float zsign) {
         { 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 */
+        { OBJ_LADDER,   { 21.0f, 0.1f, 12.6f }, { 0.0f, 0.0f, 0.0f }, { 90, 90, 0 }, 7.0f, LADDER_STEP }, /* folded, in front of them */
     };
     for (unsigned i = 0; i < sizeof(TOOLS) / sizeof(TOOLS[0]); i++) {
         Vec3 pos = placed(TOOLS[i].at, WALLED_CENTER, origin, yaw, zsign);