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 Snap words between words With Claude
src/states/game_state.cpp | 30 +++++++++++---- src/systems/projectile_system.cpp | 56 ++++++++++++++------------- src/systems/projectile_system.hpp | 4 +- src/wall/editor_wall.cpp | 80 +++++++++++++++++++++++++++++++++++++++ src/wall/editor_wall.hpp | 7 ++++ 5 files changed, 141 insertions(+), 36 deletions(-) diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 37d6f92..d9868c7 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -295,7 +295,8 @@ GameState::on_ShootOne() DynArray<Text3dPtr> single; single.push_back(std::move(word)); - _projectiles_sys->launch(_projectiles, std::move(single), _ctx.camera->fwd); + _projectiles_sys->launch(_projectiles, std::move(single), + _ctx.camera->fwd, _ctx.camera->pos); if (!_carried.empty() && _carried.last && _carried.last->word) TextWall::show_TokenInfo(_ctx.wall_texts, @@ -325,7 +326,8 @@ GameState::on_ShootAll() _carried = CarriedLine(); if (!all_words.empty()) - _projectiles_sys->launch(_projectiles, std::move(all_words), _ctx.camera->fwd); + _projectiles_sys->launch(_projectiles, std::move(all_words), + _ctx.camera->fwd, _ctx.camera->pos); infowall_StepdetailsShow(); @@ -485,6 +487,10 @@ GameState::projectile_LandOnFloor(ProjectileResult& result) void GameState::projectile_LandOnWall(ProjectileResult& result) { + DynArray<TextWallPtr> placed; + DynArray<Text3d*> editor_group; + placed.reserve(result.words.size()); + for (Text3dPtr& w : result.words) { if (!w) continue; if (w->type == ObjectType::Text) @@ -494,15 +500,23 @@ GameState::projectile_LandOnWall(ProjectileResult& result) w->body.vel = Vec3{0, 0, 0}; TextWallPtr wt = TextWall::from_Drop(std::move(w), _ctx.world->scale()); + if (wt && wt->text3d && wt->role == Wall::Role::Editor) + editor_group.push_back(wt->text3d.get()); // collect, don't place yet + placed.push_back(std::move(wt)); + } - if (wt && wt->text3d && wt->role == Wall::Role::Editor) { - const Vec3 impact = wt->text3d->pos; - const Vec3 snapped = _editor_wall.on_WordPlaced(wt->text3d.get(), impact); - wt->text3d->pos = snapped; - } + // Insert the whole shot line as ONE block at its centroid (the aim point), + // so it merges between two existing words instead of scattering. + if (!editor_group.empty()) { + Vec3 impact{0, 0, 0}; + for (Text3d* t : editor_group) { impact.x += t->pos.x; impact.y += t->pos.y; impact.z += t->pos.z; } + const double inv = 1.0 / static_cast<double>(editor_group.size()); + impact.x *= inv; impact.y *= inv; impact.z *= inv; + _editor_wall.on_LinePlaced(editor_group, impact); + } + for (TextWallPtr& wt : placed) _ctx.wall_texts.push_back(std::move(wt)); - } } diff --git a/src/systems/projectile_system.cpp b/src/systems/projectile_system.cpp index 56533f5..334d2e3 100644 --- a/src/systems/projectile_system.cpp +++ b/src/systems/projectile_system.cpp @@ -233,35 +233,32 @@ begin_EditorStick(Projectile& p, Wall::Position hit_pos) const double dot = cr_x * wt_x + cr_z * wt_z; const double sign = (dot >= 0.0) ? 1.0 : -1.0; - const Vec3 c_pos = Text3d::words_centroid(p.words); - const double c_tan = wall_tangent(hit_pos, c_pos.x, c_pos.z); - - // Project along the flight direction to the wall plane, so the stick y is - // where the shot is HEADED on the wall, not the centroid's current height. - // Up close the centroid hasn't traveled, so its raw y is the spawn height; - // extrapolating to the plane gives the aimed-at y at any distance. + // Crosshair aim point on the wall: intersect the CAMERA ray (cam_origin + // along dir) with the wall plane. Using the camera ray rather than the + // line's centroid means the run merges where the crosshair points — the + // carried line is laid out from its anchor, so its centroid sits ~half a + // line-width off the aim, which is what dragged the merge sideways. This + // also gives the aimed-at height with no gravity soak at any distance. + const Vec3 c_pos = Text3d::words_centroid(p.words); const double plane = wall_plane_coord(hit_pos, WORLD_BOX_SCALE); const bool x_wall = (hit_pos == Wall::Position::NegX || hit_pos == Wall::Position::PosX); - // y at the exact moment the flight path crosses the wall plane. - // prev_centroid is on the near side, c_pos has just crossed; lerp by how - // far along that segment the plane sits. This respects the gravity arc up - // to the wall (far shots land where they fall) without the overshoot drift. - const Vec3 prev = p.prev_centroid; - const Vec3 cur = c_pos; - const double prev_coord = x_wall ? prev.x : prev.z; - const double cur_coord = x_wall ? cur.x : cur.z; - - double c_y; - const double denom = cur_coord - prev_coord; - if (SDL_fabs(denom) > 1e-6) { - double s = (plane - prev_coord) / denom; // fraction along the segment - s = SDL_clamp(s, 0.0, 1.0); // stay within the crossing step - c_y = prev.y + (cur.y - prev.y) * s; // interpolated y at the plane + const double cam_coord = x_wall ? p.cam_origin.x : p.cam_origin.z; + const double dir_coord = x_wall ? p.dir.x : p.dir.z; + + Vec3 aim; + if (SDL_fabs(dir_coord) > 1e-6) { + const double t = (plane - cam_coord) / dir_coord; // steps along the aim ray + aim = Vec3{ p.cam_origin.x + p.dir.x * t, + p.cam_origin.y + p.dir.y * t, + p.cam_origin.z + p.dir.z * t }; } else { - c_y = cur.y; // moving parallel to wall; fall back + aim = c_pos; // aiming parallel to the wall; fall back to the centroid } + const double c_tan = wall_tangent(hit_pos, aim.x, aim.z); + const double c_y = aim.y; + for (Text3dPtr& w : p.words) { if (!w || w->type != ObjectType::Text) continue; w->ribbon_local = { @@ -316,10 +313,13 @@ handle_WallHit(Projectile& p, const Vec3& centroid, Wall::Position hit_pos, const DynArray<WallTextHitTest>& wall_text_hits, DynArray<ProjectileResult>& results) { - // Wall-text intercept takes priority: if the path crosses a wall-text, - // emit a WallText result and drop the projectile. + // Wall-text intercept takes priority — but ONLY for a single-word shot, + // which is what knocks an existing word down or hits a line number. A whole + // shot LINE must never knock a word off; it always sticks (and merges) or + // bounces, so skip the intercept when carrying more than one word. size_t hit_idx = 0; - if (check_WallTextIntercept(p, centroid, dt, hit_pos, wall_text_hits, hit_idx)) { + if (p.words.size() == 1 && + check_WallTextIntercept(p, centroid, dt, hit_pos, wall_text_hits, hit_idx)) { ProjectileResult r; r.landing = ProjectileLanding::WallText; r.wall_pos = hit_pos; @@ -342,7 +342,8 @@ handle_WallHit(Projectile& p, const Vec3& centroid, Wall::Position hit_pos, void ProjectileSystem::launch(DynArray<Projectile>& out, DynArray<Text3dPtr> words, - const Vec3& fwd) + const Vec3& fwd, + const Vec3& eye) { if (words.empty()) return; @@ -380,6 +381,7 @@ ProjectileSystem::launch(DynArray<Projectile>& out, proj.words = std::move(words); proj.dir = fwd; proj.launch_origin = centroid; + proj.cam_origin = eye; proj.prev_centroid = centroid; out.push_back(std::move(proj)); } diff --git a/src/systems/projectile_system.hpp b/src/systems/projectile_system.hpp index ed4e33a..e8153ae 100644 --- a/src/systems/projectile_system.hpp +++ b/src/systems/projectile_system.hpp @@ -16,6 +16,7 @@ struct Projectile { Vec3 dir{}; Vec3 prev_centroid{}; Vec3 launch_origin{}; + Vec3 cam_origin{}; // camera eye at launch — the crosshair ray origin double yaw; uint8_t bounce_state = 0; bool sticking = false; @@ -57,7 +58,8 @@ class ProjectileSystem { public: void launch(DynArray<Projectile>& out, DynArray<Text3dPtr> words, - const Vec3& fwd); + const Vec3& fwd, + const Vec3& eye); void update(DynArray<Projectile>& projectiles, DynArray<ProjectileResult>& results, diff --git a/src/wall/editor_wall.cpp b/src/wall/editor_wall.cpp index 82492f9..dc5d080 100644 --- a/src/wall/editor_wall.cpp +++ b/src/wall/editor_wall.cpp @@ -105,6 +105,86 @@ EditorWall::on_WordPlaced(Text3d* word, const Vec3& impact_pos) } +void +EditorWall::on_LinePlaced(const DynArray<Text3d*>& words_in, const Vec3& impact) +{ + if (words_in.empty()) return; + + // Visual left→right on the NegX wall is decreasing z (left = larger z). + DynArray<Text3d*> ws; + ws.reserve(words_in.size()); + for (Text3d* w : words_in) if (w) ws.push_back(w); + if (ws.empty()) return; + std::sort(ws.begin(), ws.end(), + [](const Text3d* a, const Text3d* b){ return a->pos.z > b->pos.z; }); + + // Total width of the block: glyph widths + internal gaps. + double block_w = 0.0; + for (size_t i = 0; i < ws.size(); ++i) { + block_w += 2.0 * (double)ws[i]->half_extent; + if (i + 1 < ws.size()) block_w += FREE_SPACE_DIST; + } + + Line* target = find_NearestLine(impact.y); + if (!target) { + Line fresh; + fresh.y = impact.y; + _lines.push_back(std::move(fresh)); + target = &_lines.back(); + } + + auto left_edge = [](const Text3d* w){ return w->pos.z + (double)w->half_extent; }; + auto right_edge = [](const Text3d* w){ return w->pos.z - (double)w->half_extent; }; + + // Insert where the aim points: the slot comes after every word left of the + // aim (the wall's left is +z) and before every word to its right. + size_t insert_index = 0; + for (const Text3d* w : target->words) + if (w && w->pos.z > impact.z) ++insert_index; + + const size_t n = target->words.size(); + + // Centre the block ON the aim, so it merges where the shot actually landed + // rather than flush against whichever word sits left of the gap. + double block_left_z = impact.z + block_w * 0.5; // block's left edge (high-z side) + + // Don't overlap the word to the LEFT of the slot: pull the block right (−z) + // if centring would push its left edge into that word. + if (insert_index > 0) { + const double max_left = right_edge(target->words[insert_index - 1]) - FREE_SPACE_DIST; + if (block_left_z > max_left) block_left_z = max_left; + } + // Make room on the RIGHT: if the block would overlap the word to its right, + // shove that word and everything past it further right (−z). + if (insert_index < n) { + const double block_right = block_left_z - block_w; + const double min_right = left_edge(target->words[insert_index]) + FREE_SPACE_DIST; + if (block_right < min_right) { + const double shift = min_right - block_right; + for (size_t k = insert_index; k < n; ++k) + target->words[k]->pos.z -= shift; + } + } + + // Lay the block out left→right from block_left_z (decreasing z) and snap + // each word to the row height; keep each word's existing x (wall plane). + double cursor = block_left_z; // left edge for the next glyph + for (Text3d* w : ws) { + const double hw = (double)w->half_extent; + const double center_z = cursor - hw; + w->pos.y = target->y; + w->pos.z = center_z; + cursor = center_z - hw - FREE_SPACE_DIST; + } + + // Insert the run into the line in visual order at the chosen slot. + for (size_t k = 0; k < ws.size(); ++k) + target->words.insert( + target->words.begin() + static_cast<std::ptrdiff_t>(insert_index + k), + ws[k]); +} + + void EditorWall::on_WordRemoved(Text3d* word) { diff --git a/src/wall/editor_wall.hpp b/src/wall/editor_wall.hpp index ce62a2c..37339b8 100644 --- a/src/wall/editor_wall.hpp +++ b/src/wall/editor_wall.hpp @@ -98,6 +98,13 @@ public: // Returns the snapped pos so the caller can apply it. Vec3 on_WordPlaced(Text3d* word, const Vec3& impact_pos); + // Place a whole shot line (several words) as ONE contiguous block at the + // aim point, so a multi-word line merges between two existing words + // instead of each word scattering to its own slot. Sets each word's pos + // directly (caller doesn't re-apply). Pass the words in any order and the + // shot line's centroid as the impact. + void on_LinePlaced(const DynArray<Text3d*>& words, const Vec3& impact); + // Deregister a word that's been picked up off the wall. Safe if the // word wasn't tracked (e.g. it was on a non-editor wall). void on_WordRemoved(Text3d* word);