Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / db848ca

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

C++ 72.3% C 26.2%
git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gz

Fix linenumber stuff AND dont pick up through floor With Claude

Christian Immanuel · 2026-06-02 14:41 · db848ca868b71032b2bf592a190f3baacb88a049

 src/states/game_state.cpp   |  27 +++++-
 src/systems/pick_system.cpp |   8 ++
 src/wall/editor_wall.cpp    | 202 ++++++++++++++++++++++++++++++++++++++++----
 src/wall/editor_wall.hpp    |  38 +++++++++
 4 files changed, 258 insertions(+), 17 deletions(-)

diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index 52345be..37d6f92 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -629,6 +629,8 @@ GameState::update(double dt)
         _pult_sys->update(dt, *_ctx.camera);
 
         _plate_sys->update(dt, *_ctx.player, _step_plates);
+
+        _editor_wall.update(dt, _ctx.camera->pos);
     }
 
 
@@ -945,6 +947,11 @@ GameState::substep(double dt)
                     i
                 });
         }
+        // Line numbers live on the editor wall but outside wall_texts; expose
+        // their hitboxes here so they can be shot (tagged with an encoded id).
+        for (const EditorWall::NumberHit& nh : _editor_wall.number_Hitboxes())
+            wall_text_hits.push_back({ Wall::Position::NegX,
+                                       nh.aabb_min, nh.aabb_max, nh.index });
     }
 
     DynArray<ProjectileResult> proj_results;
@@ -960,8 +967,24 @@ GameState::substep(double dt)
             switch (result.landing) {
                 case ProjectileLanding::Floor:    projectile_LandOnFloor(result); break;
                 case ProjectileLanding::Wall:     projectile_LandOnWall(result);  break;
-                case ProjectileLanding::WallText: walltext_HandleShootDown(result, wall_text_to_erase);
-                                                  break;
+                case ProjectileLanding::WallText:
+                    if (EditorWall::is_LineNumberHit(result.wall_text_index)) {
+                        const size_t lid   = EditorWall::line_Of(result.wall_text_index);
+                        const double delta = _editor_wall.scroll_DeltaToEye(lid, _ctx.camera->pos.y);
+
+                        // Move the editor code AND the wall's own line records
+                        // together so they stay in sync; numbers reflow next update.
+                        for (TextWallPtr& wt : _ctx.wall_texts)
+                            if (wt && wt->text3d && wt->role == Wall::Role::Editor)
+                                wt->text3d->pos.y += delta;
+                        _editor_wall.scroll(delta);
+                        _editor_wall.bounce(lid);
+
+                        projectile_LandOnFloor(result);  // shot tokens drop; number stays put
+                    } else {
+                        walltext_HandleShootDown(result, wall_text_to_erase);
+                    }
+                    break;
                 case ProjectileLanding::None:     break;
             }
         }
diff --git a/src/systems/pick_system.cpp b/src/systems/pick_system.cpp
index 6f6b43a..885a75e 100644
--- a/src/systems/pick_system.cpp
+++ b/src/systems/pick_system.cpp
@@ -73,6 +73,11 @@ pick(const Camera& camera,
 {
     const Vec3& eye = camera.pos;
 
+    // Floor plane. Words/wall-text scrolled (or fallen) below the floor are
+    // hidden under it but still angle-pickable, so exclude anything whose
+    // centre has dropped beneath the floor surface.
+    const double floor_y = -static_cast<double>(WORLD_BOX_SCALE);
+
     PickResult result;
     double best_angle = MAX_ANGLE;   // any actual hit must beat this
 
@@ -81,6 +86,7 @@ pick(const Camera& camera,
         const Text3dPtr& w = words[i];
         if (!w || !w->interactable) continue;
         if (w->ai.state != Text3dAI::State::Idle) continue;
+        if (w->pos.y < floor_y) continue;   // below the floor → not pickable
 
         double a = ang_score(*w, eye, camera.fwd);
         if (a >= 0.0 && a < best_angle) {
@@ -93,6 +99,7 @@ pick(const Camera& camera,
     // ── scenery ─────────────────────────────────────────────────────────
     for (const GameObjectPtr& s : scenery) {
         if (!s || !s->interactable) continue;
+        if (s->pos.y < floor_y) continue;   // below the floor → not pickable
         double a = ang_score(*s, eye, camera.fwd);
         if (a >= 0.0 && a < best_angle) {
             best_angle         = a;
@@ -105,6 +112,7 @@ pick(const Camera& camera,
     for (size_t i = 0; i < wall_texts.size(); ++i) {
         const TextWallPtr& wt = wall_texts[i];
         if (!wt || !wt->text3d) continue;
+        if (wt->text3d->pos.y < floor_y) continue;   // scrolled below the floor → not pickable
         double a = ang_score(*wt->text3d, eye, camera.fwd);
         if (a >= 0.0 && a < best_angle) {
             best_angle       = a;
diff --git a/src/wall/editor_wall.cpp b/src/wall/editor_wall.cpp
index a5bc98a..82492f9 100644
--- a/src/wall/editor_wall.cpp
+++ b/src/wall/editor_wall.cpp
@@ -2,6 +2,7 @@
 
 #include <SDL3/SDL.h>
 #include <algorithm>
+#include <string>
 
 #include "entities/camera.hpp"
 #include "rendering/gl_renderer.hpp"
@@ -125,6 +126,171 @@ EditorWall::on_WordRemoved(Text3d* word)
 }
 
 
+// ─────────────────────────────────────────────────────────────────────────────
+// Line numbers
+// ─────────────────────────────────────────────────────────────────────────────
+
+namespace {
+
+Text3dPtr
+make_Number(const std::string& s, const Vec3& pos, float yaw)
+{
+    // The Text3d constructor colours by token lookup (e.g. "1" is a Literal →
+    // red), so force orange afterwards.
+    Text3dPtr t = make_Text3d(s, Text3d::depth_wall,
+                              EditorWall::LINE_NUMBER_SCALE, pos,
+                              Text3d::color_orange);
+    t->yaw = yaw;
+    t->set_Color(Text3d::color_orange);
+    return t;
+}
+
+void
+set_NumberText(Text3d* t, const std::string& s)
+{
+    if (t->text == s) return;
+    t->text = s;
+    t->rebuild_Mesh();
+    t->set_Color(Text3d::color_orange);   // rebuild can reset colour; keep orange
+}
+
+} // namespace
+
+
+void
+EditorWall::update(double dt, const Vec3& eye_pos)
+{
+    if (_lines.empty()) return;
+
+    // Order rows top→bottom (largest y = line 1) to assign numbers.
+    DynArray<size_t> order;
+    order.reserve(_lines.size());
+    for (size_t i = 0; i < _lines.size(); ++i)
+        if (!_lines[i].words.empty()) order.push_back(i);
+
+    std::sort(order.begin(), order.end(),
+        [&](size_t a, size_t b){ return _lines[a].y > _lines[b].y; });
+
+    const Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, WORLD_BOX_SCALE);
+
+    if (order.empty()) return;
+
+    // Global code extents (z) across every row, so both number columns land at
+    // a single aligned position with a clear, uniform gap from the code.
+    double code_left  = -1e18;   // furthest-left z code reaches (visual left = +z)
+    double code_right =  1e18;   // furthest-right z code reaches (visual right = -z)
+    for (size_t oi : order)
+        for (Text3d* w : _lines[oi].words) {
+            if (!w) continue;
+            code_left  = std::max(code_left,  w->pos.z + (double)w->half_extent);
+            code_right = std::min(code_right, w->pos.z - (double)w->half_extent);
+        }
+
+    // Pass 1: ensure labels exist, refresh digits, advance the bounce, and find
+    // the widest digit so the clamp below can keep every number on the wall.
+    double max_lh = 0.0, max_rh = 0.0;
+    int rank = 1;
+    for (size_t oi : order) {
+        Line& l = _lines[oi];
+
+        const std::string s = std::to_string(rank);
+        if (!l.num_left)  l.num_left  = make_Number(s, Vec3{tf.position.x, l.y, 0.0}, tf.yaw);
+        if (!l.num_right) l.num_right = make_Number(s, Vec3{tf.position.x, l.y, 0.0}, tf.yaw);
+
+        if (l.shown_number != rank) {
+            set_NumberText(l.num_left.get(),  s);
+            set_NumberText(l.num_right.get(), s);
+            l.shown_number = rank;
+        }
+
+        // Damped spring bounce back to rest (shot numbers jiggle, stay put).
+        constexpr double STIFFNESS = 120.0, DAMPING = 12.0;
+        l.bounce_vel += (-STIFFNESS * l.bounce_off - DAMPING * l.bounce_vel) * dt;
+        l.bounce_off += l.bounce_vel * dt;
+
+        max_lh = std::max(max_lh, (double)l.num_left->half_extent);
+        max_rh = std::max(max_rh, (double)l.num_right->half_extent);
+        ++rank;
+    }
+
+    // Code-relative columns, but CLAMPED so a number can never leave the wall.
+    // The wall spans z in [-ws, +ws]; keep a small margin in from each edge.
+    // A left number occupies [left_col_z, left_col_z + 2*lh] (outer edge = +z);
+    // a right number occupies [right_col_z - 2*rh, right_col_z] (outer = -z).
+    const double ws   = WORLD_BOX_SCALE;
+    const double EDGE = 0.3;
+    double left_col_z  = code_left  + LINE_NUMBER_GAP;
+    double right_col_z = code_right - LINE_NUMBER_GAP;
+    left_col_z  = std::min(left_col_z,   ws - EDGE - 2.0 * max_lh);
+    right_col_z = std::max(right_col_z, -ws + EDGE + 2.0 * max_rh);
+
+    // Pass 2: place each number against its column at its (bounced) row height.
+    for (size_t oi : order) {
+        Line& l = _lines[oi];
+
+        // Use a code word's x so the numbers sit on the wall plane at the
+        // same depth as the text.
+        double x = tf.position.x;
+        for (Text3d* w : l.words) { if (w) { x = w->pos.x; break; } }
+
+        const double ny = l.y + l.bounce_off;
+        const double lh = (double)l.num_left->half_extent;
+        const double rh = (double)l.num_right->half_extent;
+        l.num_left->pos  = Vec3{ x, ny, left_col_z  + lh };   // code-facing edge on the column
+        l.num_right->pos = Vec3{ x, ny, right_col_z - rh };
+
+        l.num_left->pitch  = Wall::tilt_TowardEye(l.num_left->pos,  eye_pos);
+        l.num_right->pitch = Wall::tilt_TowardEye(l.num_right->pos, eye_pos);
+    }
+}
+
+
+DynArray<EditorWall::NumberHit>
+EditorWall::number_Hitboxes() const
+{
+    // Build the box live from the label's current pos + half-extents rather
+    // than the base aabb_*() (which can lag a constantly-repositioned object).
+    // Pad generously: a single digit is a small target.
+    constexpr double PAD     = 0.30;
+    constexpr double DEPTH_X = 0.50;   // thickness into the room so shots cross it
+
+    DynArray<NumberHit> out;
+    out.reserve(_lines.size() * 2);
+
+    auto push_box = [&](const Text3d* t, size_t i) {
+        const double hz = (double)t->half_extent   + PAD;   // along wall (z)
+        const double hy = (double)t->half_extent_y + PAD;   // vertical
+        const Vec3   c  = t->pos;
+        out.push_back({ Vec3{ c.x - DEPTH_X, c.y - hy, c.z - hz },
+                        Vec3{ c.x + DEPTH_X, c.y + hy, c.z + hz },
+                        HIT_BASE + i });
+    };
+
+    for (size_t i = 0; i < _lines.size(); ++i) {
+        const Line& l = _lines[i];
+        if (l.num_left)  push_box(l.num_left.get(),  i);
+        if (l.num_right) push_box(l.num_right.get(), i);
+    }
+    return out;
+}
+
+
+double
+EditorWall::scroll_DeltaToEye(size_t line_id, double eye_y) const
+{
+    if (line_id >= _lines.size()) return 0.0;
+    return eye_y - _lines[line_id].y;
+}
+
+
+void
+EditorWall::bounce(size_t line_id)
+{
+    if (line_id < _lines.size())
+        _lines[line_id].bounce_vel += BOUNCE_IMPULSE;
+}
+
+
 void
 EditorWall::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
 {
@@ -173,23 +339,29 @@ EditorWall::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
         base += 4;
     }
 
-    if (verts.empty()) return;
+    if (!verts.empty()) {
+        Mat4 view  = buildViewMat4(cam);
+        Mat4 proj  = gl.projection();
+        Mat4 model = Mat4::identity();
+        Mat4 mvp   = Mat4::mul(proj, Mat4::mul(view, model));
 
-    Mat4 view  = buildViewMat4(cam);
-    Mat4 proj  = gl.projection();
-    Mat4 model = Mat4::identity();
-    Mat4 mvp   = Mat4::mul(proj, Mat4::mul(view, model));
+        gl.set_Wireframe(false);
+        gl.set_MVP(model, mvp);
+        gl.set_NormalMat(model);
+        gl.set_Light(light);
+        gl.set_ColorUseVertex(true);
 
-    gl.set_Wireframe(false);
-    gl.set_MVP(model, mvp);
-    gl.set_NormalMat(model);
-    gl.set_Light(light);
-    gl.set_ColorUseVertex(true);
+        gl.draw_MeshColored(verts.data(),
+                           static_cast<int>(verts.size() / 9),
+                           indices.data(),
+                           static_cast<int>(indices.size()));
 
-    gl.draw_MeshColored(verts.data(),
-                       static_cast<int>(verts.size() / 9),
-                       indices.data(),
-                       static_cast<int>(indices.size()));
+        gl.set_ColorUseVertex(false);
+    }
 
-    gl.set_ColorUseVertex(false);
+    // Orange line numbers (own meshes; positioned/numbered in update()).
+    for (const Line& l : _lines) {
+        if (l.num_left)  l.num_left->draw(gl, cam, light);
+        if (l.num_right) l.num_right->draw(gl, cam, light);
+    }
 }
diff --git a/src/wall/editor_wall.hpp b/src/wall/editor_wall.hpp
index 028d60c..ce62a2c 100644
--- a/src/wall/editor_wall.hpp
+++ b/src/wall/editor_wall.hpp
@@ -32,6 +32,15 @@ class EditorWall
     struct Line {
         double                 y      {0.0};
         DynArray<Text3d*>   words;            // left-to-right
+
+        // Owned line-number labels, one each end. Deliberately NOT stored in
+        // wall_texts: that keeps them out of source extraction and means the
+        // shoot-down path can never make them fall off the wall.
+        Text3dPtr num_left;
+        Text3dPtr num_right;
+        int       shown_number {-1};          // currently rendered digit(s)
+        double    bounce_off    {0.0};         // transient vertical jiggle
+        double    bounce_vel    {0.0};
     };
 
     // Result of placing a word: the final Z, and the index in the line's
@@ -57,11 +66,31 @@ public:
     static constexpr double UNDERLINE_DROP   = 0.04;  // small gap below letter baseline
     static constexpr double UNDERLINE_THICK  = 0.005; // half-height of the underline strip
 
+    static constexpr double LINE_NUMBER_GAP   = 1.2;    // clear space between code and the numbers
+    static constexpr float  LINE_NUMBER_SCALE  = 0.003f;
+    static constexpr double BOUNCE_IMPULSE     = 2.2;  // upward kick when a number is shot
+
+    // Hit-id encoding: GameState mixes these into its wall-text hit list, and
+    // tells them apart on the way back. (size_t is 64-bit on the target.)
+    static constexpr size_t HIT_BASE = size_t(1) << 32;
+    static bool   is_LineNumberHit(size_t idx) { return idx >= HIT_BASE; }
+    static size_t line_Of          (size_t idx) { return idx - HIT_BASE; }
+
+    struct NumberHit {
+        Vec3   aabb_min;
+        Vec3   aabb_max;
+        size_t index;     // HIT_BASE + line id
+    };
+
     // ── State management ────────────────────────────────────────────────
 
     // Reset everything. Call on scene teardown.
     void clear();
 
+    // Per-frame: renumber rows top→bottom, (re)create/position the orange
+    // number labels, tilt them toward the eye, and advance bounce springs.
+    void update(double dt, const Vec3& eye_pos);
+
     // Register a word that just hit the editor wall. May modify word->pos
     // to snap it (current behavior: snap is computed BEFORE the word is
     // visible; smooth animation is a later step).
@@ -80,4 +109,13 @@ public:
     size_t line_Count() const { return _lines.size(); }
 
     void scroll(double delta) { for (Line& l : _lines) l.y += delta; }
+
+    // ── Line-number shooting ─────────────────────────────────────────────
+    // AABBs for every number label, tagged with an encoded hit id, for
+    // GameState to append to its projectile wall-text hit list.
+    DynArray<NumberHit> number_Hitboxes() const;
+    // World-Y shift needed to bring the given line to eye height.
+    double scroll_DeltaToEye(size_t line_id, double eye_y) const;
+    // Kick a line's numbers so they bounce in place (does not move the line).
+    void   bounce(size_t line_id);
 };