Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/systems/projectile_system.cpp

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
src/systems/projectile_system.cpp 17.4 KB · 504 lines raw
#include <algorithm>

#include "entities/world.hpp"
#include "projectile_system.hpp"
#include "systems/collision.hpp"
#include "utils/config.hpp"

namespace
Nimbin
{

static inline double
bounceVelocity(double v, double restitution)
{ return -v * restitution; }


static double
wallPlaneCoord(Wall::Position projectile, double ws)
{
    switch (projectile) {
        case Wall::Position::PosX: return  ws;
        case Wall::Position::NegX: return -ws;
        case Wall::Position::PosZ: return  ws;
        case Wall::Position::NegZ: return -ws;
    }
    return ws;
}

static float
wallTargetYaw(Wall::Position projectile)
{
    switch (projectile) {
        case Wall::Position::PosX: return -SDL_PI_F * 0.5f;
        case Wall::Position::NegX: return  SDL_PI_F * 0.5f;
        case Wall::Position::PosZ: return  SDL_PI_F;
        case Wall::Position::NegZ: return  0.0f;
    }
    return 0.0f;
}

static double
wallTangent(Wall::Position projectile, double wx, double wz)
{
    switch (projectile) {
        case Wall::Position::PosX: return -wz;
        case Wall::Position::NegX: return  wz;
        case Wall::Position::PosZ: return -wx;
        case Wall::Position::NegZ: return  wx;
    }
    return 0.0;
}

static Vec3
wallReconstruct(Wall::Position projectile, double plane, double tangent, double y)
{
    switch (projectile) {
        case Wall::Position::PosX: return { plane   , y, -tangent };
        case Wall::Position::NegX: return { plane   , y,  tangent };
        case Wall::Position::PosZ: return { -tangent, y, plane    };
        case Wall::Position::NegZ: return {  tangent, y, plane    };
    }
    return {};
}


static void
emitFloorLanding(Projectile& projectile, DynArray<ProjectileResult>& results)
{
    ProjectileResult res;
    res.landing = ProjectileLanding::Floor;
    res.vel     = projectile.vel;
    res.words   = std::move(projectile.words);
    results.push_back(std::move(res));
}


static void
tickSticking(Projectile& projectile, double dt, double ws,
             DynArray<ProjectileResult>& results)
{
    if (!projectile.sticking || projectile.words.empty()) return;

    projectile.stick_t += dt * 5.0;

    const double smoothing_factor = 1.0 - SDL_exp(-12.0 * dt);
    const float  target_yaw       = wallTargetYaw (projectile.stick_wall);
    const double plane            = wallPlaneCoord(projectile.stick_wall, ws);

    for (Text::TokenPtr& word : projectile.words) {
        if (!word || word->type != ObjectType::Text) continue;

        Vec3 target = wallReconstruct(projectile.stick_wall, plane,
                                      word->phrase_offset.x,
                                      word->phrase_offset.y);

        word->pos = Math::lerp(word->pos, target, smoothing_factor);
        word->yaw = static_cast<float>(Math::lerp(static_cast<double>(word->yaw),
                                                  static_cast<double>(target_yaw),
                                                  smoothing_factor));
    }

    if (projectile.stick_t >= 1.0) {
        ProjectileResult res;
        res.landing  = projectile.program_stick
                     ? ProjectileLanding::ProgramInput
                     : ProjectileLanding::Wall;
        res.wall_pos = projectile.stick_wall;
        res.words    = std::move(projectile.words);
        results.push_back(std::move(res));
    }
}


static bool
checkFloorCeiling(Projectile& projectile, const Vec3& centroid, double ws,
                  DynArray<ProjectileResult>& results)
{
    double word_he_y = 0.07;
    for (const Text::TokenPtr& w : projectile.words) {
        if (w) { word_he_y = w->shape.half_extents.y; break; }
    }

    if ((centroid.y - word_he_y) <= -ws + 0.05) {
        emitFloorLanding(projectile, results);
        return true;
    }

    if (centroid.y > ws) {
        projectile.vel.y = bounceVelocity(projectile.vel.y, 0.75);
        for (Text::TokenPtr& w : projectile.words)
            if (w) w->pos.y = ws - 0.01;
    }
    return false;
}


static bool
detectWallHit(const Projectile& projectile, const Vec3& centroid, double dt, double ws,
              Wall::Position& out_pos)
{
    const double next_x = centroid.x + projectile.vel.x * dt;
    const double next_z = centroid.z + projectile.vel.z * dt;

    if ((centroid.x >  -ws + Config::Physics::stick_dist && next_x <= -ws + Config::Physics::stick_dist) ||
        (centroid.x <= -ws + Config::Physics::stick_dist && projectile.vel.x < 0.0))
        { out_pos = Wall::Position::NegX; return true; }

    if ((centroid.x <  ws - Config::Physics::stick_dist && next_x >=  ws - Config::Physics::stick_dist) ||
        (centroid.x >= ws - Config::Physics::stick_dist && projectile.vel.x > 0.0))
        { out_pos = Wall::Position::PosX; return true; }

    if ((centroid.z >  -ws + Config::Physics::stick_dist && next_z <= -ws + Config::Physics::stick_dist) ||
        (centroid.z <= -ws + Config::Physics::stick_dist && projectile.vel.z < 0.0))
        { out_pos = Wall::Position::NegZ; return true; }

    if ((centroid.z <  ws - Config::Physics::stick_dist && next_z >=  ws - Config::Physics::stick_dist) ||
        (centroid.z >= ws - Config::Physics::stick_dist && projectile.vel.z > 0.0))
        { out_pos = Wall::Position::PosZ; return true; }

    return false;
}


static bool
checkWallTextIntercept(const Projectile& projectile, const Vec3& centroid, double dt,
                       Wall::Position hit_pos,
                       const DynArray<WallTextHitTest>& wall_text_hits,
                       double aim_y, double aim_z, bool editor_wall,
                       size_t& out_idx)
{
    const Vec3 next_centroid {
        centroid.x + projectile.vel.x * dt,
        centroid.y + projectile.vel.y * dt,
        centroid.z + projectile.vel.z * dt
    };
    const Vec3 ray_dir = next_centroid - projectile.prev_centroid;

    Vec3 word_he { 0.3, 0.07, 0.07 };
    for (const Text::TokenPtr& w : projectile.words) {
        if (w) { word_he = w->shape.half_extents; break; }
    }

    for (const WallTextHitTest& wt : wall_text_hits) {
        if (wt.wall_position != hit_pos) continue;

        if (editor_wall) {
            // Vertical row gate (as before): only the aimed row's words.
            const double center_y = (wt.aabb_Min.y + wt.aabb_Max.y) * 0.5;
            if (SDL_fabs(aim_y - center_y) > Config::Wall::line_height * 0.5)
                continue;

            // 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)
                continue;

            // In the central band → shoot it down (no projectile-width inflation
            // on z, so the gap between words stays a merge zone).
            out_idx = wt.index;
            return true;
        }

        Vec3 box_min { wt.aabb_Min.x - word_he.x,
                       wt.aabb_Min.y - word_he.y,
                       wt.aabb_Min.z - word_he.z };
        Vec3 box_max { wt.aabb_Max.x + word_he.x,
                       wt.aabb_Max.y + word_he.y,
                       wt.aabb_Max.z + word_he.z };

        Collision::SweptResult sw =
            Collision::swept_RayAabb(projectile.prev_centroid, ray_dir, box_min, box_max);
        if (sw.hit) { out_idx = wt.index; return true; }
    }
    return false;
}


static void
beginEditorStick(Projectile& projectile, Wall::Position hit_pos)
{
    projectile.sticking   = true;
    projectile.stick_wall = hit_pos;
    projectile.vel        = Vec3{0, 0, 0};
    projectile.stick_t    = 0.0;

    const double cr_x =  SDL_cos(projectile.yaw);
    const double cr_z = -SDL_sin(projectile.yaw);

    double wt_x = 0.0, wt_z = 0.0;
    switch (hit_pos) {
        case Wall::Position::NegZ: wt_x =  1.0; break;
        case Wall::Position::PosZ: wt_x = -1.0; break;
        case Wall::Position::NegX: wt_z =  1.0; break;
        case Wall::Position::PosX: wt_z = -1.0; break;
    }
    const double dot  = cr_x * wt_x + cr_z * wt_z;
    const double sign = (dot >= 0.0) ? 1.0 : -1.0;

    // Stick where the phrase actually is — its current centroid — NOT where the
    // original launch ray would cross the wall. The launch ray ignores the gravity
    // arc and is meaningless after a bounce (it would teleport the word, which is
    // why bounced shots used to respawn near the world centre).
    const Vec3   c_pos = Text::Token::Data::words_centroid(projectile.words);
    const double c_tan = wallTangent(hit_pos, c_pos.x, c_pos.z);
    const double c_y   = c_pos.y;

    for (Text::TokenPtr& w : projectile.words) {
        if (!w || w->type != ObjectType::Text) continue;
        w->phrase_offset = {
            c_tan + w->phrase_offset.x * sign,
            c_y   + w->phrase_offset.y,
            0.0
        };
        w->pos.y = w->phrase_offset.y;
    }
}


static void
beginProgramStick(Projectile& projectile, double ws)
{
    projectile.sticking      = true;
    projectile.program_stick = true;
    projectile.stick_wall    = Wall::Position::PosX;
    projectile.vel           = Vec3{0, 0, 0};
    projectile.stick_t       = 0.0;

    const double cr_x =  SDL_cos(projectile.yaw);
    const double cr_z = -SDL_sin(projectile.yaw);
    const double wt_z = -1.0;
    const double dot  = cr_x * 0.0 + cr_z * wt_z;
    const double sign = (dot >= 0.0) ? 1.0 : -1.0;

    const double c_tan = 0.0;
    const double c_y   = -ws + 1.0;

    for (Text::TokenPtr& w : projectile.words) {
        if (!w || w->type != ObjectType::Text) continue;
        w->phrase_offset = {
            c_tan + w->phrase_offset.x * sign,
            c_y   + w->phrase_offset.y,
            0.0
        };
    }
}


static bool
bounceOffWall(Projectile& projectile, Wall::Position hit_pos, double ws,
              DynArray<ProjectileResult>& results)
{
    const bool    x_axis   = (hit_pos == Wall::Position::PosX ||
                              hit_pos == Wall::Position::NegX);
    const uint8_t axis_bit = x_axis ? BOUNCED_X : BOUNCED_Z;

    if (projectile.bounce_state & axis_bit) {
        emitFloorLanding(projectile, results);
        return true;
    }

    projectile.bounce_state |= axis_bit;
    projectile.collided      = true;

    if (x_axis) {
        projectile.vel.x = bounceVelocity(projectile.vel.x, 0.70);
        for (Text::TokenPtr& w : projectile.words) if (w)
            w->pos.x = (hit_pos == Wall::Position::PosX)
                     ? ( ws - Config::Physics::stick_dist - 0.05)
                     : (-ws + Config::Physics::stick_dist + 0.05);
    }
    else {
        projectile.vel.z = bounceVelocity(projectile.vel.z, 0.70);
        for (Text::TokenPtr& w : projectile.words) if (w)
            w->pos.z = (hit_pos == Wall::Position::PosZ)
                     ? ( ws - Config::Physics::stick_dist - 0.05)
                     : (-ws + Config::Physics::stick_dist + 0.05);
    }
    return false;
}


static bool
handleWallHit(Projectile& projectile, const Vec3& centroid, Wall::Position hit_pos,
               double dt,double ws,
               const DynArray<WallTextHitTest>& wall_text_hits,
               DynArray<ProjectileResult>& results,
               bool program_running)
{
    if (Wall::role_Of(hit_pos) == Wall::Role::Program) {
        if (program_running) { beginProgramStick(projectile, ws); return false; }
        return bounceOffWall(projectile, hit_pos, ws, results);
    }

    const bool   editor = (Wall::role_Of(hit_pos) == Wall::Role::Editor);

    // 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 &&
            checkWallTextIntercept(projectile, centroid, dt, hit_pos, wall_text_hits,
                                   aim_y, aim_z, editor, hit_idx)) {
        ProjectileResult res;
        res.landing         = ProjectileLanding::WallText;
        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;
    }

    if (Wall::role_Of(hit_pos) == Wall::Role::Editor) {
        beginEditorStick(projectile, hit_pos);
        return false;
    }

    return bounceOffWall(projectile, hit_pos, ws, results);
}


void
ProjectileSystem::launch(DynArray<Projectile>& out,
                         DynArray<Text::TokenPtr> words,
                         const Vec3& fwd,
                         const Aim::Result& aim)
{
    if (words.empty()) return;

    double yaw = SDL_atan2(-fwd.x, -fwd.z);

    for (Text::TokenPtr& w : words) {
        if (!w) continue;
        if (w->type == ObjectType::Text)
            w->set_Look(Config::Color::keep, Text::Token::depth_floor);

        w->yaw = static_cast<float>(yaw);
    }

    double right_x =  SDL_cos(yaw);
    double right_z = -SDL_sin(yaw);

    Vec3 centroid = Text::Token::Data::words_centroid(words);

    for (Text::TokenPtr& w : words) {
        if (!w || w->type != ObjectType::Text) continue;
        Text::Token::Data& tx = static_cast<Text::Token::Data&>(*w);
        double dx = w->pos.x - centroid.x;
        double dz = w->pos.z - centroid.z;
        tx.phrase_offset = {
            dx * right_x + dz * right_z,
            w->pos.y - centroid.y,
            0.0
        };
    }

    const Vec3   target= aim.point;
    const double g     = Config::Physics::proj_grav;
    const double dx    = target.x - centroid.x;
    const double dy    = target.y - centroid.y;
    const double dz    = target.z - centroid.z;
    const double horiz = SDL_sqrt(dx*dx + dz*dz);
    double T = horiz / Config::Physics::proj_throw_speed;
    T = SDL_clamp(T, Config::Physics::proj_t_min, Config::Physics::proj_t_max);

    Projectile proj;
    proj.vel           = Vec3{ dx / T, dy / T + 0.5 * g * T, dz / T };
    proj.yaw           = yaw;
    proj.bounce_state  = 0;
    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) {
        proj.track_obj   = aim.track;
        proj.track_local = aim.local;
        proj.track_last  = aim.point;
    }

    out.push_back(std::move(proj));
}


void
ProjectileSystem::update(DynArray<Projectile>&        projectiles,
                         DynArray<ProjectileResult>&  results,
                         const DynArray<WallTextHitTest>& wall_text_hits,
                         double dt, bool program_running)
{
    const double ws = World::get_BoxScale<double>();

    for (Projectile& projectile : projectiles)
        tickSticking(projectile, dt, ws, results);

    for (auto it = projectiles.begin(); it != projectiles.end(); ) {
        Projectile& projectile = *it;

        if (projectile.sticking) {
            if (projectile.words.empty())
                 it = projectiles.erase(it);
            else ++it;
            continue;
        }

        if (projectile.words.empty()) { it = projectiles.erase(it); continue; }

                const Vec3 centroid = Text::Token::Data::words_centroid(projectile.words);
        bool remove = false;

        if (checkFloorCeiling(projectile, centroid, ws, results)) remove = true;

        if (!remove) {
            Wall::Position hit_pos{};
            if (detectWallHit(projectile, centroid, dt, ws, hit_pos))
                remove = handleWallHit(projectile, centroid, hit_pos, dt, ws,
                                        wall_text_hits, results, program_running);
        }

        if (!remove && !projectile.sticking && Math::length(projectile.vel) < 1.0) {
            emitFloorLanding(projectile, results);
            remove = true;
        }

        if (remove) {
            it = projectiles.erase(it);
        }
        else {
            projectile.prev_centroid = centroid;
            ++it;
        }
    }
}

} // namespace Nimbin