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 shoot down token from wall With Claude
src/states/game_state.cpp | 1 + src/systems/projectile_system.cpp | 44 +++++++++++++++++++++++++++++-------- src/systems/projectile_system.hpp | 3 +++ src/text/handler.cpp | 46 +++++++++++++++++++++++++-------------- 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 45d1b0c..b96ea91 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -622,6 +622,7 @@ GameState::projectile_LandOnWallText(ProjectileResult& result) Vec3 impact = lines[line_idx].tokens[token_idx]->pos; for (const Text::TokenPtr& word : result.words) if (word) { impact = word->pos; break; } + if (result.use_aim) impact = result.aim_point; const Text::Handler::EditorHit hit = handler->classify_Impact(impact); diff --git a/src/systems/projectile_system.cpp b/src/systems/projectile_system.cpp index 58f7c75..1965cbf 100644 --- a/src/systems/projectile_system.cpp +++ b/src/systems/projectile_system.cpp @@ -189,11 +189,14 @@ checkWallTextIntercept(const Projectile& projectile, const Vec3& centroid, doubl if (SDL_fabs(aim_y - center_y) > Config::Wall::line_height * 0.5) continue; - // Horizontal: only shoot the word down if the aim lands in its - // central 2/3. Outer 1/3 each side is "between words" → merge. + // Horizontal: the aim must land within the token's column span at all. + // The fine drop-vs-merge split (central 90% drop, outer 5% each edge + // merge) is applied downstream by classify_Impact, so the gate here is + // the full span — anything tighter would turn edge aims into "stick new + // text" instead of the intended merge. const double center_z = (wt.aabb_Min.z + wt.aabb_Max.z) * 0.5; const double half_z = (wt.aabb_Max.z - wt.aabb_Min.z) * 0.5; - if (SDL_fabs(aim_z - center_z) > half_z * (2.0 / 3.0)) + if (SDL_fabs(aim_z - center_z) > half_z) continue; // In the central band → shoot it down (no projectile-width inflation @@ -334,10 +337,30 @@ handleWallHit(Projectile& projectile, const Vec3& centroid, Wall::Position hit_p } const bool editor = (Wall::role_Of(hit_pos) == Wall::Role::Editor); - // Aim at where the phrase ACTUALLY is (its current centroid), so the gravity - // arc and any prior bounce are respected. The old launch-ray aim ignored both. - const double aim_y = editor ? centroid.y : 0.0; - const double aim_z = editor ? centroid.z : 0.0; + + // Drop/merge is decided from the exact crosshair COLUMN — the launch ray + // intersected with the text plane — not the projectile's physical landing or + // its mesh hit. The arc, the shooting angle, and any object between the camera + // and the wall all corrupt the landing/mesh point; the ray∩plane is pure + // geometry and maps cleanly to a column. After a bounce the launch ray is + // stale, so fall back to the real landing centroid (keeps Bug 1 fixed). + Vec3 decide = centroid; + bool use_aim = false; + if (editor && !projectile.collided && SDL_fabs(projectile.dir.x) > 1e-6) { + // aim_point lies on the crosshair ray, so (aim_point, dir) IS that ray. + // Intersect it with the text plane for the exact aimed column. + const Vec3 n = Wall::inward_Normal(hit_pos); + const double plane_x = wallPlaneCoord(hit_pos, ws) + n.x * static_cast<double>(Config::Wall::text_offset); + const double t = (plane_x - projectile.aim_point.x) / projectile.dir.x; + if (t > 0.0) { + decide = Vec3{ plane_x, + projectile.aim_point.y + projectile.dir.y * t, + projectile.aim_point.z + projectile.dir.z * t }; + use_aim = true; + } + } + const double aim_y = editor ? decide.y : 0.0; + const double aim_z = editor ? decide.z : 0.0; size_t hit_idx = 0; if (projectile.words.size() == 1 && @@ -348,6 +371,8 @@ handleWallHit(Projectile& projectile, const Vec3& centroid, Wall::Position hit_p res.wall_pos = hit_pos; res.wall_text_index = hit_idx; res.vel = projectile.vel; + res.aim_point = decide; + res.use_aim = use_aim; res.words = std::move(projectile.words); results.push_back(std::move(res)); return true; @@ -413,6 +438,7 @@ ProjectileSystem::launch(DynArray<Projectile>& out, proj.words = std::move(words); proj.dir = fwd; proj.launch_origin = centroid; + proj.aim_point = aim.point; // a point on the crosshair ray; rebuilds the ray with dir proj.prev_centroid = centroid; if (aim.track) { @@ -426,8 +452,8 @@ ProjectileSystem::launch(DynArray<Projectile>& out, void -ProjectileSystem::update(DynArray<Projectile>& projectiles, - DynArray<ProjectileResult>& results, +ProjectileSystem::update(DynArray<Projectile>& projectiles, + DynArray<ProjectileResult>& results, const DynArray<WallTextHitTest>& wall_text_hits, double dt, bool program_running) { diff --git a/src/systems/projectile_system.hpp b/src/systems/projectile_system.hpp index c57a3f9..1a6cd58 100644 --- a/src/systems/projectile_system.hpp +++ b/src/systems/projectile_system.hpp @@ -31,6 +31,7 @@ struct Projectile Vec3 dir{}; Vec3 prev_centroid{}; Vec3 launch_origin{}; + Vec3 aim_point {}; Vec3 track_local {}; Vec3 track_last {}; @@ -68,8 +69,10 @@ struct ProjectileResult DynArray<Text::TokenPtr> words; ProjectileLanding landing = ProjectileLanding::None; + Vec3 aim_point {}; Vec3 vel {}; size_t wall_text_index = 0; + bool use_aim = false; Wall::Position wall_pos = Wall::Position::NegZ; diff --git a/src/text/handler.cpp b/src/text/handler.cpp index e519633..8b18312 100644 --- a/src/text/handler.cpp +++ b/src/text/handler.cpp @@ -556,21 +556,28 @@ Handler::classify_Impact(const Vec3& impact) const if (N == 0) { hit.kind = HitKind::NewLine; hit.row = 0; hit.col = col_From_Z(); return hit; } - // ── vertical: 80% band owns the row, the 20% straddling each boundary inserts a line ── - const double rf = (_row0_y - impact.y) / lh; - const int i = static_cast<int>(SDL_lround(rf)); - const double frac = rf - static_cast<double>(i); - - if (i < 0) { hit.kind = HitKind::NewLine; hit.row = i; hit.col = col_From_Z(); return hit; } // prepend - if (i >= N) { hit.kind = HitKind::NewLine; hit.row = i; hit.col = col_From_Z(); return hit; } // append - if (SDL_fabs(frac) > 0.4) { + // ── vertical: a row is "owned" only within the glyph cap height (≈ an 'A'/'M', + // ±_he_y about the row centre) — NOT the whole line height. Aiming in the gap + // between two rows inserts a new line there; aiming above the first row or + // below the last appends/prepends. This mirrors the uniform cell box used for + // the horizontal hit, so "clearly above/below a token" makes a new line + // instead of dropping it, while the inter-row gap can no longer masquerade as + // a hit on the nearest token. ── + const double rf = (_row0_y - impact.y) / lh; + const int i = static_cast<int>(SDL_lround(rf)); + + if (i < 0) { hit.kind = HitKind::NewLine; hit.row = i; hit.col = col_From_Z(); return hit; } // prepend + if (i >= N) { hit.kind = HitKind::NewLine; hit.row = i; hit.col = col_From_Z(); return hit; } // append + + const double row_y = _row0_y - static_cast<double>(i) * lh; + if (SDL_fabs(impact.y - row_y) > _he_y) { // in the inter-row gap → new line hit.kind = HitKind::NewLine; - hit.row = (frac > 0.0) ? i + 1 : i; // gap between rows → new line there + hit.row = (impact.y < row_y) ? i + 1 : i; // below this row → line after it hit.col = col_From_Z(); return hit; } - // ── horizontal on row i: re-chunk the buffer line and apply 80/20 per token span ── + // ── horizontal on row i: re-chunk the buffer line and apply 90/10 per token span ── hit.row = i; hit.line_idx = static_cast<size_t>(i); const std::string& s = blines[static_cast<size_t>(i)]; const double vcf = (_code_left_z - impact.z) / _char_w; // fractional visual column @@ -609,11 +616,11 @@ Handler::classify_Impact(const Vec3& impact) const if (vcf >= vc && vcf < vc + vs) { if (ws) { hit.kind = HitKind::Insert; hit.col = b1; return hit; } // split: word after the space const double center = vc + vs * 0.5; - if (SDL_fabs(vcf - center) <= 0.4 * vs) { // central 80% → drop + if (SDL_fabs(vcf - center) <= 0.45 * vs) { // central 90% → drop hit.kind = HitKind::DropToken; hit.line_idx = static_cast<size_t>(i); hit.token_idx = k; return hit; } - hit.kind = HitKind::Insert; // outer 20% → beside + hit.kind = HitKind::Insert; // outer 5% each side → beside hit.col = (vcf < center) ? b0 : b1; return hit; } @@ -955,10 +962,17 @@ Handler::relayout_Mono() bt.t->pos = _transform.position; bt.t->pos.y = row_y; bt.t->pos.z = left_z - tok_w * 0.5; - if (bt.t->is_Whitespace()) { - bt.t->half_extent = (float)(tok_w * 0.5); - bt.t->shape.half_extents.x = tok_w * 0.5; - } + // Uniform monospace cell for collision/aim: EVERY token (not just + // whitespace) gets a cell-width × cap-height box. The projectile + // intercept tests shape.half_extents (via aabb_Min/Max), so any token + // keeping its ink extents ("," tiny, "M" wide, "g" tall with descender) + // made the aim AABBs drift off the visible monospace columns and rows. + // Width = one cell; height = the cap height of an 'A'/'M' (_he_y), the + // same metric classify_Impact uses for its vertical row ownership. + bt.t->half_extent = (float)(tok_w * 0.5); + bt.t->half_extent_y = (float)_he_y; + bt.t->shape.half_extents.x = tok_w * 0.5; + bt.t->shape.half_extents.y = _he_y; ln.tokens.push_back(std::move(bt.t)); }