sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
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.gzFix linenumber stuff AND dont pick up through floor With Claude
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); };