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.gzCleanup With Claude
src/states/game_state.cpp | 155 ++++--------------------------------- src/states/game_state.hpp | 17 +--- src/systems/celebration_system.cpp | 110 ++++++++++++++++++++++++++ src/systems/celebration_system.hpp | 24 ++++++ src/systems/word_ai_system.cpp | 22 +++--- src/utils/text_wrap.hpp | 55 +++++++++++++ 6 files changed, 216 insertions(+), 167 deletions(-) diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 906f29b..971e639 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -10,6 +10,7 @@ #include "levels/level_check.hpp" #include "levels/level_list.hpp" #include "rendering/gl_renderer.hpp" +#include "systems/celebration_system.hpp" #include "systems/game_mod_abi.h" #include "systems/input_system.hpp" #include "systems/physics_system.hpp" @@ -18,6 +19,7 @@ #include "text_3d/text_label.hpp" #include "utils/config.hpp" #include "utils/frame_profiler.hpp" +#include "utils/text_wrap.hpp" #include "wall/text_wall.hpp" #include "wall/text_wall_scroll.hpp" @@ -38,6 +40,7 @@ GameState::GameState(size_t starting_level) freelook_sys = cpputils::make_unique<FreelookSystem>(); particle_sys = cpputils::make_unique<ParticleSystem>(); mod_loader = cpputils::make_unique<ModLoader>(); + celebration_sys = cpputils::make_unique<CelebrationSystem>(); floor_sys = cpputils::make_unique<FloorMandelbrot>(); floor_sys->generate(ctx.world->scale(), -ctx.world->scale()); @@ -623,7 +626,7 @@ GameState::update(double dt) } { FP_ZONE("update.goals"); - celebration_Tick(dt); + celebration_sys->tick(dt, *particle_sys); level_TryAdvance(); } @@ -702,9 +705,8 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) { FP_ZONE("render.particles"); particle_sys->draw(renderer, *ctx.camera, *ctx.light); } { FP_ZONE("render.floor_sys"); floor_sys ->draw(renderer, *ctx.camera, *ctx.light); } - if (celebration.banner) { - FP_ZONE("render.banner"); - celebration.banner->draw(renderer, *ctx.camera, *ctx.light); + { FP_ZONE("render.celebration"); + celebration_sys->draw(renderer, *ctx.camera, *ctx.light); } { FP_ZONE("render.end_Frame"); renderer.end_Frame(&window); } @@ -749,45 +751,7 @@ GameState::infowall_StepdetailsShow() Wall::Transform wall_transform = Wall::get_Transform(Wall::Role::Info, world_scale); constexpr size_t MAX_INFO_LINE_CHARS = 92; - - std::vector<std::string> lines; - std::string current_line; - - auto flush_line = [&]() { - while (!current_line.empty() && current_line.back() == ' ') - current_line.pop_back(); - if (!current_line.empty()) { - lines.push_back(current_line); - current_line.clear(); - } else { - lines.push_back(""); - } - }; - - std::string word; - auto flush_word = [&]() { - if (word.empty()) return; - if (current_line.size() + word.size() + (current_line.empty() ? 0 : 1) - > MAX_INFO_LINE_CHARS) { - flush_line(); - } - if (!current_line.empty()) current_line += ' '; - current_line += word; - word.clear(); - }; - - for (char c : details) { - if (c == '\n') { - flush_word(); - flush_line(); - } else if (c == ' ' || c == '\t') { - flush_word(); - } else { - word += c; - } - } - flush_word(); - flush_line(); + std::vector<std::string> lines = wrap_Text(details, MAX_INFO_LINE_CHARS); const float bottom_y = -world_scale + 1.0f; float y = bottom_y + static_cast<float>(lines.size() - 1) * Wall::TEXT_LINE_SPACING; @@ -799,15 +763,11 @@ GameState::infowall_StepdetailsShow() if (line.empty()) continue; Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.0025f, - pos, Text3d::color_white); + pos, Text3d::color_white); t->yaw = wall_transform.yaw; ctx.wall_texts.push_back(cpputils::make_unique<TextWall>( - TextWall{ - wall_transform.wall_position, - Wall::Role::Info, - std::move(t) - })); + TextWall{ wall_transform.wall_position, Wall::Role::Info, std::move(t) })); } } @@ -1046,9 +1006,9 @@ GameState::goal_ContextBuild() const bool GameState::level_TryAdvance() { - if (celebration.active) { - if (celebration.t >= celebration.duration) { - celebration_Finish(); + if (celebration_sys->active()) { + if (celebration_sys->progress() >= 1.0) { + celebration_sys->finish(*particle_sys); level_Advance(); return true; } @@ -1063,7 +1023,8 @@ GameState::level_TryAdvance() if (!level_runner || level_runner->complete()) return false; - celebration_Start(); + auto [anchor, right, glyph_yaw] = anchorframe_Current(); + celebration_sys->start(*ctx.camera, glyph_yaw); return false; } @@ -1097,94 +1058,6 @@ GameState::level_Advance() } -void -GameState::celebration_Start() -{ - celebration.active = true; - celebration.t = 0.0; - celebration.next_burst_t = 0.0; - - auto [anchor, right, glyph_yaw] = anchorframe_Current(); - - Vec3 banner_pos { - ctx.camera->pos.x + ctx.camera->fwd.x * 10.0, - ctx.camera->pos.y + ctx.camera->fwd.y * 10.0, - ctx.camera->pos.z + ctx.camera->fwd.z * 10.0, - }; - - if (banner_pos.y < static_cast<double>(-WORLD_BOX_SCALE+1.0f) ) - banner_pos.y = -WORLD_BOX_SCALE+1.0f; - - celebration.banner = make_Text3d("Success", - Text3d::depth_floor * 2.0f, - 0.012f, banner_pos, - Text3d::color_white); - celebration.banner->yaw = static_cast<float>(glyph_yaw); - celebration.banner->body.mode = PhysicsMode::Static; -} - - -void -GameState::celebration_Tick(double dt) -{ - if (!celebration.active) return; - - celebration.t += dt; - - // Slow banner spin - if (celebration.banner) - celebration.banner->yaw += static_cast<float>(dt * 0.6); - - // Fireworks bursts on a schedule. We aim for ~7 bursts across the - // duration, with slight jitter to avoid mechanical pacing. - if (celebration.t >= celebration.next_burst_t && - celebration.t < celebration.duration - 0.3) - { - // Random position across the play area, biased to upper half so - // bursts read as airborne fireworks not floor_sys explosions. - const double ws = ctx.world->scale(); - const double margin = 3.0; - auto rnd = []() { - return static_cast<double>(rand()) / static_cast<double>(RAND_MAX); - }; - - Vec3 center { - (rnd() * 2.0 - 1.0) * (ws - margin), - rnd() * (ws - margin), // 0 .. ws-margin (upper half) - (rnd() * 2.0 - 1.0) * (ws - margin), - }; - - // Random saturated hue. Convert HSV(h, 1, 1) to RGB cheaply. - double h = rnd() * 6.0; - int i = static_cast<int>(h); - double f = h - i; - float r=0, g=0, b=0; - switch (i % 6) { - case 0: r=1; g=(float)f; b=0; break; - case 1: r=(float)(1-f); g=1; b=0; break; - case 2: r=0; g=1; b=(float)f; break; - case 3: r=0; g=(float)(1-f); b=1; break; - case 4: r=(float)f; g=0; b=1; break; - case 5: r=1; g=0; b=(float)(1-f); break; - } - - particle_sys->emit_FireworkBurst(center, SDL_FColor{r, g, b, 1.0f}, 80); - - // Next burst in 0.5..1.0s. - celebration.next_burst_t = celebration.t + 0.5 + rnd() * 0.5; - } -} - - -void -GameState::celebration_Finish() -{ - celebration.active = false; - celebration.banner.reset(); - particle_sys->clear(); -} - - void GameState::mod_Apply(const std::vector<ModObject>& objs) { diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index a88f9cf..f7d9948 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -13,6 +13,7 @@ #include "rendering/floor_mandelbrot.hpp" #include "states/game_context.hpp" #include "systems/carry_system.hpp" +#include "systems/celebration_system.hpp" #include "systems/compiler_system.hpp" #include "systems/freelook_system.hpp" #include "systems/hint_system.hpp" @@ -50,15 +51,6 @@ struct StepLedger { }; -struct Celebration { - bool active { false }; - double t { 0.0 }; - double duration { 3.5 }; - double next_burst_t { 0.0 }; - Text3dPtr banner; -}; - - struct StepPlate { Plate* plate; std::string tag; @@ -70,7 +62,6 @@ class GameState : public IState GameContext ctx; FrameInput frame_input; CarriedLine carried; - Celebration celebration; EditorWall editor_wall; StepLedger step_ledger; @@ -106,6 +97,7 @@ class GameState : public IState cpputils::unique_ptr<FloorMandelbrot> floor_sys; cpputils::unique_ptr<LevelRunner> level_runner; cpputils::unique_ptr<ModLoader> mod_loader; + cpputils::unique_ptr<CelebrationSystem>celebration_sys; bool on_Pick(); @@ -150,11 +142,6 @@ class GameState : public IState void level_Advance(); GoalContext goal_ContextBuild() const; - void celebration_Start(); - void celebration_Tick(double dt); - void celebration_Finish(); - - void projectile_LandOnFloor(ProjectileResult& result); void projectile_LandOnWall (ProjectileResult& result); void walltext_HandleShootDown(ProjectileResult& result, std::vector<size_t>& to_erase); diff --git a/src/systems/celebration_system.cpp b/src/systems/celebration_system.cpp new file mode 100644 index 0000000..080f93e --- /dev/null +++ b/src/systems/celebration_system.cpp @@ -0,0 +1,110 @@ +#include "celebration_system.hpp" +#include "entities/camera.hpp" +#include "systems/particle_system.hpp" + +void +CelebrationSystem::start(const Camera& cam, double& glyph_yaw) +{ + active_ = true; + t_ = 0.0; + next_burst_ = 0.0; + + Vec3 banner_pos { + cam.pos.x + cam.fwd.x * 10.0, + cam.pos.y + cam.fwd.y * 10.0, + cam.pos.z + cam.fwd.z * 10.0, + }; + + if (banner_pos.y < static_cast<double>(-WORLD_BOX_SCALE+1.0f) ) + banner_pos.y = -WORLD_BOX_SCALE+1.0f; + + banner_ = make_Text3d("Success", + Text3d::depth_floor * 2.0f, + 0.012f, banner_pos, + Text3d::color_white); + banner_->yaw = static_cast<float>(glyph_yaw); + banner_->body.mode = PhysicsMode::Static; +} + + +void +CelebrationSystem::tick(double dt, ParticleSystem& particle_sys) +{ + if (!active_) return; + + t_ += dt; + + // Slow banner spin + if (banner_) + banner_->yaw += static_cast<float>(dt * 0.6); + + // Fireworks bursts on a schedule. We aim for ~7 bursts across the + // duration, with slight jitter to avoid mechanical pacing. + if (t_ >= next_burst_ && + t_ < duration_ - 0.3) + { + // Random position across the play area, biased to upper half so + // bursts read as airborne fireworks not floor_sys explosions. + const double ws = WORLD_BOX_SCALE; + const double margin = 3.0; + auto rnd = []() { + return static_cast<double>(rand()) / static_cast<double>(RAND_MAX); + }; + + Vec3 center { + (rnd() * 2.0 - 1.0) * (ws - margin), + rnd() * (ws - margin), + (rnd() * 2.0 - 1.0) * (ws - margin), + }; + + // Random saturated hue. Convert HSV(h, 1, 1) to RGB cheaply. + double h = rnd() * 6.0; + int i = static_cast<int>(h); + double f = h - i; + float r=0, g=0, b=0; + switch (i % 6) { + case 0: r=1; g=(float)f; b=0; break; + case 1: r=(float)(1-f); g=1; b=0; break; + case 2: r=0; g=1; b=(float)f; break; + case 3: r=0; g=(float)(1-f); b=1; break; + case 4: r=(float)f; g=0; b=1; break; + case 5: r=1; g=0; b=(float)(1-f); break; + } + + particle_sys.emit_FireworkBurst(center, SDL_FColor{r, g, b, 1.0f}, 80); + + // Next burst in 0.5..1.0s. + next_burst_ = t_ + 0.5 + rnd() * 0.5; + } +} + + +void +CelebrationSystem::finish(ParticleSystem& particle_sys) +{ + active_ = false; + banner_.reset(); + particle_sys.clear(); +} + + +bool +CelebrationSystem::active() const +{ + return active_; +} + + +double +CelebrationSystem::progress() const +{ + return (duration_ > 0.0) ? (t_ / duration_) : 1.0; +} + + +void +CelebrationSystem::draw(GlRenderer& gl, const Camera& cam, const Light& light) const +{ + if (!active_ || !banner_) return; + banner_->draw(gl, cam, light); +} diff --git a/src/systems/celebration_system.hpp b/src/systems/celebration_system.hpp new file mode 100644 index 0000000..36dbe45 --- /dev/null +++ b/src/systems/celebration_system.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "utils/globals.hpp" + +struct Light; +struct Camera; +class ParticleSystem; +class GlRenderer; + + +class CelebrationSystem { + bool active_ = false; + double t_ = 0, + duration_ = 3.5, + next_burst_ = 0; + Text3dPtr banner_; +public: + void start(const Camera& cam, double& glyph_yaw); + void tick(double dt, ParticleSystem& particle_sys); + void finish(ParticleSystem& particle_sys); + bool active() const; + double progress() const; + void draw(GlRenderer&, const Camera&, const Light&) const; +}; diff --git a/src/systems/word_ai_system.cpp b/src/systems/word_ai_system.cpp index 5f8c22a..1b20f43 100644 --- a/src/systems/word_ai_system.cpp +++ b/src/systems/word_ai_system.cpp @@ -361,17 +361,17 @@ WordAiSystem::apply_Locomotion(Text3d& w, const Vec3& target_vel, double dt) w.pos.z += w.body.vel.z * dt; // Clamp to world bounds - const double ws = static_cast<double>(world_scale); - const double hex = w.shape.half_extents.x; - const double hey = w.shape.half_extents.y; - const double hez = w.shape.half_extents.z; - - if (w.pos.x - hex < -ws) { w.pos.x = -ws + hex; w.body.vel.x = 0; } - if (w.pos.x + hex > ws) { w.pos.x = ws - hex; w.body.vel.x = 0; } - if (w.pos.y - hey < -ws + 0.5) { w.pos.y = -ws + 0.5 + hey; w.body.vel.y = 0; } - if (w.pos.y + hey > ws) { w.pos.y = ws - hey; w.body.vel.y = 0; } - if (w.pos.z - hez < -ws) { w.pos.z = -ws + hez; w.body.vel.z = 0; } - if (w.pos.z + hez > ws) { w.pos.z = ws - hez; w.body.vel.z = 0; } + const double ws = static_cast<double>(world_scale); + + auto clamp_axis = [](double& p, double& v, double he, + double lo, double hi) { + if (p - he < lo) { p = lo + he; v = 0.0; } + if (p + he > hi) { p = hi - he; v = 0.0; } + }; + + clamp_axis(w.pos.x, w.body.vel.x, w.shape.half_extents.x, -ws, ws); + clamp_axis(w.pos.y, w.body.vel.y, w.shape.half_extents.y, -ws + 0.5, ws); + clamp_axis(w.pos.z, w.body.vel.z, w.shape.half_extents.z, -ws, ws); } diff --git a/src/utils/text_wrap.hpp b/src/utils/text_wrap.hpp new file mode 100644 index 0000000..53b15af --- /dev/null +++ b/src/utils/text_wrap.hpp @@ -0,0 +1,55 @@ +#pragma once +#include <string> +#include <vector> + +// ───────────────────────────────────────────────────────────────────────── +// wrap_Text — break `text` into lines no longer than `max_chars`. +// +// Rules (matching the infowall behavior it was extracted from): +// - explicit '\n' forces a line break (and can produce an empty line) +// - spaces/tabs separate words; a word that would overflow the current +// line starts a new line instead of being split +// - trailing spaces are trimmed from each line +// - a single word longer than max_chars is NOT split — it occupies its +// own (over-length) line, same as the original +// ───────────────────────────────────────────────────────────────────────── +inline std::vector<std::string> +wrap_Text(const std::string& text, size_t max_chars) +{ + std::vector<std::string> lines; + std::string current_line; + std::string word; + + auto flush_line = [&]() { + while (!current_line.empty() && current_line.back() == ' ') + current_line.pop_back(); + lines.push_back(current_line); // may be empty (blank line) + current_line.clear(); + }; + + auto flush_word = [&]() { + if (word.empty()) return; + if (current_line.size() + word.size() + (current_line.empty() ? 0 : 1) + > max_chars) { + flush_line(); + } + if (!current_line.empty()) current_line += ' '; + current_line += word; + word.clear(); + }; + + for (char c : text) { + if (c == '\n') { + flush_word(); + flush_line(); + } else if (c == ' ' || c == '\t') { + flush_word(); + } else { + word += c; + } + } + flush_word(); + flush_line(); + + return lines; +}