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.gzsrc/systems/projectile_system.cpp 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