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.gzSHoot in a Straight line AND invert crosshair With Claude
src/entities/pult.cpp | 2 +- src/levels/level/tutorial.cpp | 24 ++++---- src/rendering/gl_renderer.cpp | 94 +++++++++++++++++++++++----- src/rendering/gl_renderer.hpp | 9 +++ src/states/game_state.cpp | 97 ++++++++++++++++++++--------- src/states/game_state.hpp | 21 ++++--- src/systems/aim.hpp | 98 +++++++++++++++++++++++++++++ src/systems/physics_system.cpp | 58 +++++++++++++----- src/systems/physics_system.hpp | 3 +- src/systems/projectile_system.cpp | 31 ++++++++-- src/systems/projectile_system.hpp | 30 +++++---- src/utils/carry_math.cpp | 12 +++- src/utils/carry_math.hpp | 7 ++- src/utils/config.hpp | 4 ++ src/wall/wall_label.cpp | 126 ++++++++++++++++++++++++++++++++++++++ src/wall/wall_label.hpp | 51 +++++++++++++++ 16 files changed, 570 insertions(+), 97 deletions(-) diff --git a/src/entities/pult.cpp b/src/entities/pult.cpp index 9f6aa96..a785c72 100644 --- a/src/entities/pult.cpp +++ b/src/entities/pult.cpp @@ -67,7 +67,7 @@ Pult::Pult(const std::string& n, Vec3 world_pos, , name(n), half_w(half_width), half_d(half_depth) { label.set_Billboard(true); - label.set_Style(ConfigColors::color_indigo, Text3d::depth_wall, 0.0025f); + label.set_Style(ConfigColors::color_teal, Text3d::depth_wall, 0.0025f); label.set_Collidable(true); label.attach_To(&pos, Vec3{ 0.0, ConfigPult::label_height, 0.0 }); label.set_Text(n); diff --git a/src/levels/level/tutorial.cpp b/src/levels/level/tutorial.cpp index e6972df..42a8c51 100644 --- a/src/levels/level/tutorial.cpp +++ b/src/levels/level/tutorial.cpp @@ -246,24 +246,16 @@ Press BUILD to compile, then walk to the RUN pult and press it. Your program runs and its output appears on the Program wall — and that's what finishes this step.)", .grants = { - Level::TokenGrant::give("int"), - Level::TokenGrant::give("main"), - Level::TokenGrant::give("("), - Level::TokenGrant::give("void"), - Level::TokenGrant::give(")"), - Level::TokenGrant::give("{"), + Level::TokenGrant::give("#include"), + Level::TokenGrant::give("<stdio.h>"), Level::TokenGrant::give("printf"), Level::TokenGrant::give("("), Level::TokenGrant::give("\"hello\""), Level::TokenGrant::give(")"), Level::TokenGrant::give(";"), - Level::TokenGrant::give("return"), - Level::TokenGrant::give("0"), - Level::TokenGrant::give(";"), - Level::TokenGrant::give("}"), }, .scene = {}, - .reset_carry = true, + .reset_carry = false, .goal = AllOf{{ CompilePattern{ "int main ( void ) { printf ( \"hello\" ) ; return 0 ; }" }, PressButton{ PultAction::RunProgram }, @@ -283,11 +275,19 @@ the Program wall — and that's what finishes this step.)", Have a play, then jump onto the plate to continue.)", .grants = { + Level::TokenGrant::give("scanf(\"%d\", &x)"), Level::TokenGrant::give("int"), Level::TokenGrant::give("x"), Level::TokenGrant::give("="), Level::TokenGrant::give("0"), Level::TokenGrant::give(";"), + Level::TokenGrant::give(";"), + Level::TokenGrant::give(";"), + Level::TokenGrant::give("\"Your Number is : %d\""), + Level::TokenGrant::give(","), + Level::TokenGrant::give("x"), + Level::TokenGrant::give("42"), + Level::TokenGrant::give("printf(\"Enter a Number:\\n\\t\")"), }, .scene = Scene{ .words = {}, @@ -299,7 +299,7 @@ Have a play, then jump onto the plate to continue.)", }, .ai_seeds = {}, }, - .reset_carry = true, + .reset_carry = false, .goal = ReachPlate{ "" }, .max_ai_words = 0, }, diff --git a/src/rendering/gl_renderer.cpp b/src/rendering/gl_renderer.cpp index 0b761c9..0e149d3 100644 --- a/src/rendering/gl_renderer.cpp +++ b/src/rendering/gl_renderer.cpp @@ -30,6 +30,12 @@ void main() } )GLSL"; +static const char* kCrosshairFragSrc = R"GLSL( +#version 330 core +out vec4 FragColor; +void main() { FragColor = vec4(1.0, 1.0, 1.0, 1.0); } +)GLSL"; + static const char* kExtractFragSrc = R"GLSL( #version 330 core @@ -505,6 +511,24 @@ bool GlRenderer::init(SDL_Window* window) create_ExtractProgram_(); create_BlurProgram_(); create_CompositeProgram_(); + + { // crosshair: reuse the blit vertex shader (vec2 aPos @ loc 0) + GLuint v = compileShader(GL_VERTEX_SHADER, kBlitVertSrc); + GLuint f = compileShader(GL_FRAGMENT_SHADER, kCrosshairFragSrc); + _crosshair_prog = glCreateProgram(); + glAttachShader(_crosshair_prog, v); + glAttachShader(_crosshair_prog, f); + glLinkProgram (_crosshair_prog); + glDeleteShader(v); glDeleteShader(f); + + glGenVertexArrays(1, &_crosshair_vao); + glGenBuffers (1, &_crosshair_vbo); + glBindVertexArray(_crosshair_vao); + glBindBuffer (GL_ARRAY_BUFFER, _crosshair_vbo); + glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + glBindVertexArray(0); + } // Lookup the new emissive uniforms on the main program. _uEmissiveColor = glGetUniformLocation(_prog, "u_EmissiveColor"); @@ -574,22 +598,21 @@ GlRenderer::end_Frame(SDL_Window* window) // Run bloom passes (extract + blur). { FP_ZONE("end_Frame.bloom"); run_BloomPipeline_(); - glFinish(); // TEMPORARY: force GPU drain so the zone captures real GPU cost - } - // Composite scene + bloom to screen. - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glViewport(0, 0, _view_w, _view_h); - glDisable(GL_DEPTH_TEST); + // Composite scene + bloom to screen. + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, _view_w, _view_h); + glDisable(GL_DEPTH_TEST); - glUseProgram (_composite_prog); - glActiveTexture(GL_TEXTURE0); - glBindTexture (GL_TEXTURE_2D, _scene_color_tex); - glUniform1i (_uComposeScene, 0); - glActiveTexture(GL_TEXTURE1); - glBindTexture (GL_TEXTURE_2D, _blur_tex[1]); // final bloom - glUniform1i (_uComposeBloom, 1); - glUniform1f (_uComposeStrength, _bloom_strength); + glUseProgram (_composite_prog); + glActiveTexture(GL_TEXTURE0); + glBindTexture (GL_TEXTURE_2D, _scene_color_tex); + glUniform1i (_uComposeScene, 0); + glActiveTexture(GL_TEXTURE1); + glBindTexture (GL_TEXTURE_2D, _blur_tex[1]); // final bloom + glUniform1i (_uComposeBloom, 1); + glUniform1f (_uComposeStrength, _bloom_strength); + } { FP_ZONE("end_Frame.composite"); glBindVertexArray(_fullscreen_vao); @@ -601,6 +624,8 @@ GlRenderer::end_Frame(SDL_Window* window) glUseProgram(_prog); } + draw_Crosshair_(); + { FP_ZONE("end_Frame.swap"); SDL_GL_SwapWindow(window); } @@ -695,6 +720,47 @@ GlRenderer::set_FaceShade(float r, float g, float b, float back_dim, bool use_fr glUniform1i(_uUseFront, use_front ? 1 : 0); } + +void +GlRenderer::draw_Crosshair_() +{ + if (!_crosshair_enabled || _view_w <= 0 || _view_h <= 0) return; + + // Pixel dimensions → NDC half-extents (per-axis, so it stays square). + const float gap = 4.0f, len = 9.0f, thick = 1.5f; + const float gx = gap / static_cast<float>(_view_w) * 2.0f, gy = gap / static_cast<float>(_view_h) * 2.0f; + const float lx = len / static_cast<float>(_view_w) * 2.0f, ly = len / static_cast<float>(_view_h) * 2.0f; + const float tx = thick / static_cast<float>(_view_w) * 2.0f, ty = thick / static_cast<float>(_view_h) * 2.0f; + + // Four arms, center gap, NO overlap (overlap would invert twice = no-op). + float v[48]; + int k = 0; + auto quad = [&](float x0, float y0, float x1, float y1) { + v[k++]=x0; v[k++]=y0; v[k++]=x1; v[k++]=y0; v[k++]=x1; v[k++]=y1; + v[k++]=x0; v[k++]=y0; v[k++]=x1; v[k++]=y1; v[k++]=x0; v[k++]=y1; + }; + quad( gx, -ty, gx + lx, ty); // right + quad(-(gx + lx), -ty, -gx, ty); // left + quad( -tx, gy, tx, gy + ly); // up + quad( -tx, -(gy + ly), tx, -gy); // down + + glDisable (GL_DEPTH_TEST); + glEnable (GL_BLEND); + glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO); // out = (1 - dst) → invert + + glUseProgram(_crosshair_prog); + glBindVertexArray(_crosshair_vao); + glBindBuffer (GL_ARRAY_BUFFER, _crosshair_vbo); + glBufferData (GL_ARRAY_BUFFER, sizeof(v), v, GL_STREAM_DRAW); + glDrawArrays (GL_TRIANGLES, 0, 24); + glBindVertexArray(0); + + glDisable (GL_BLEND); + glEnable (GL_DEPTH_TEST); + glUseProgram(_prog); +} + + // ───────────────────────────────────────────────────────────────────────────── // draw_Mesh / draw_Lines // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/rendering/gl_renderer.hpp b/src/rendering/gl_renderer.hpp index 0694851..465dc99 100644 --- a/src/rendering/gl_renderer.hpp +++ b/src/rendering/gl_renderer.hpp @@ -109,6 +109,12 @@ class GlRenderer { int _blur_w {0}; int _blur_h {0}; + // Crosshair: tiny screen-space overlay, inverts the pixels behind it. + GLuint _crosshair_prog {0}; + GLuint _crosshair_vao {0}; + GLuint _crosshair_vbo {0}; + bool _crosshair_enabled {true}; + // Programs GLuint _extract_prog {0}; GLuint _blur_prog {0}; @@ -141,6 +147,8 @@ class GlRenderer { void create_CompositeProgram_(); void run_BloomPipeline_(); + void draw_Crosshair_(); + GLuint compileShader(GLenum type, const char* src); bool linkProgram(GLuint vert, GLuint frag); public: @@ -160,6 +168,7 @@ public: void set_TextLineSpacing(float m); void set_Emissive (float r, float g, float b, float strength); void set_BloomStrength (float s) { _bloom_strength = s; } + void set_Crosshair(bool on) { _crosshair_enabled = on; } void enable_MandelbrotMode(bool on); void enable_WallPaperMode (bool on); diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index b9e66da..6562fb2 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -10,6 +10,7 @@ #include "game_state.hpp" #include "levels/level_check.hpp" #include "rendering/gl_renderer.hpp" +#include "systems/aim.hpp" #include "systems/celebration_system.hpp" #include "systems/game_mod_abi.h" #include "systems/input_system.hpp" @@ -46,6 +47,8 @@ GameState::GameState(size_t starting_level) _floor_sys = cpputils::make_unique<FloorMandelbrot> (); _floor_sys->generate(_ctx.world->scale(), -_ctx.world->scale()); + _wall_labels = WallLabel::generate_All(_ctx.world->scale()); + _word_ai_sys = cpputils::make_unique<WordAiSystem>(); _word_ai_sys->set_WorldScale(_ctx.world->scale()); @@ -294,8 +297,11 @@ GameState::on_ShootOne() DynArray<Text3dPtr> single; single.push_back(std::move(word)); - _projectiles_sys->launch(_projectiles, std::move(single), - _ctx.camera->fwd, _ctx.camera->pos); + + Aim::AimResult aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery, + _ctx.wall_texts, _ctx.world->scale()); + _projectiles_sys->launch(_projectiles, std::move(single /* or all_words */), + _ctx.camera->fwd, _ctx.camera->pos, aim); if (!_carried.empty() && _carried.last && _carried.last->word) TextWall::show_TokenInfo(_ctx.wall_texts, @@ -324,9 +330,12 @@ GameState::on_ShootAll() all_words.push_back(std::move(node->word)); _carried = CarriedLine(); - if (!all_words.empty()) + if (!all_words.empty()) { + Aim::AimResult aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery, + _ctx.wall_texts, _ctx.world->scale()); _projectiles_sys->launch(_projectiles, std::move(all_words), - _ctx.camera->fwd, _ctx.camera->pos); + _ctx.camera->fwd, _ctx.camera->pos, aim); + } infowall_StepdetailsShow(); @@ -443,7 +452,7 @@ GameState::on_TextCancel(FrameInput& input) void -GameState::program_FeedInput(ProjectileResult& result) +GameState::projectile_FeedProgramInput(ProjectileResult& result) { std::string line; for (const Text3dPtr& w : result.words) { @@ -535,6 +544,28 @@ GameState::projectile_LandOnWall(ProjectileResult& result) } +void +GameState::projectile_LandOnWallText(ProjectileResult& result, + DynArray<size_t>& wall_text_to_erase) +{ + if (EditorWall::is_LineNumberHit(result.wall_text_index)) { + const size_t lid = EditorWall::line_Of(result.wall_text_index); + const double delta = _editor_wall.scroll_DeltaToEye(lid, _ctx.camera->pos.y); + + for (TextWallPtr& wt : _ctx.wall_texts) + if (wt && wt->text3d && wt->role == Wall::Role::Editor) + wt->text3d->pos.y += delta; + _editor_wall.scroll(delta); + _editor_wall.bounce(lid); + + projectile_LandOnFloor(result); + } else { + walltext_HandleShootDown(result, wall_text_to_erase); + } + return; +} + + void GameState::walltext_HandleShootDown(ProjectileResult& result, DynArray<size_t>& to_erase) @@ -650,6 +681,9 @@ GameState::update(double dt) wt->text3d->pitch = Wall::tilt_TowardEye(wt->text3d->pos, _ctx.camera->pos); } + for (WallLabel& lbl : _wall_labels) + lbl.update(dt, *_ctx.camera); + carriedline_Update(); const double floor_y = -static_cast<double>(_ctx.world->scale()); @@ -737,6 +771,11 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) wt->draw(renderer, *_ctx.camera, *_ctx.light); } + { FP_ZONE("render.wall_labels"); + for (const WallLabel& lbl : _wall_labels) + lbl.draw(renderer, *_ctx.camera, *_ctx.light); + } + { FP_ZONE("render._carried"); DynArray<CarriedWordNode*> nodes = _carried.nodes(); for (const CarriedWordNode* node : nodes) @@ -945,7 +984,7 @@ GameState::substep(double dt) } { FP_ZONE("subst.phys_projectiles"); - _physics_sys->update_Projectiles(_projectiles, dt); + _physics_sys->update_Projectiles(_projectiles, dt, _ctx.words); } DynArray<ProjectileContactEvent> projectile_contacts; @@ -957,6 +996,8 @@ GameState::substep(double dt) lbl->update(dt, *_ctx.camera); all_labels.push_back(&_hint_sys->get_Label()); + for (WallLabel& wl : _wall_labels) + all_labels.push_back(&wl.label); } { FP_ZONE("subst.resolve_projectiles"); @@ -976,11 +1017,26 @@ GameState::substep(double dt) i }); } - // Line numbers live on the editor wall but outside wall_texts; expose - // their hitboxes here so they can be shot (tagged with an encoded id). + + // Hit Line Number for (const EditorWall::NumberHit& nh : _editor_wall.number_Hitboxes()) wall_text_hits.push_back({ Wall::Position::NegX, nh.aabb_min, nh.aabb_max, nh.index }); + + // Hit Wall Label + for (size_t li = 0; li < _wall_labels.size(); ++li) { + const DynArray<Text3dPtr>& words = _wall_labels[li].label.words(); + for (size_t wi = 0; wi < words.size(); ++wi) { + if (!words[wi]) continue; + constexpr size_t LABEL_HIT_BASE = size_t(1) << 48; + wall_text_hits.push_back({ + Wall::position_of(_wall_labels[li].role), + words[wi]->aabb_min(), + words[wi]->aabb_max(), + LABEL_HIT_BASE | (li << 16) | wi + }); + } + } } DynArray<ProjectileResult> proj_results; @@ -995,28 +1051,13 @@ GameState::substep(double dt) { FP_ZONE("subst.projectile_land"); for (ProjectileResult& result : proj_results) { switch (result.landing) { - case ProjectileLanding::Floor: projectile_LandOnFloor(result); break; + case ProjectileLanding::Floor: projectile_LandOnFloor (result); break; case ProjectileLanding::ProgramInput: - program_FeedInput(result); break; - case ProjectileLanding::Wall: projectile_LandOnWall(result); break; + projectile_FeedProgramInput(result); break; + case ProjectileLanding::Wall: projectile_LandOnWall (result); break; case ProjectileLanding::WallText: - if (EditorWall::is_LineNumberHit(result.wall_text_index)) { - const size_t lid = EditorWall::line_Of(result.wall_text_index); - const double delta = _editor_wall.scroll_DeltaToEye(lid, _ctx.camera->pos.y); - - // Move the editor code AND the wall's own line records - // together so they stay in sync; numbers reflow next update. - for (TextWallPtr& wt : _ctx.wall_texts) - if (wt && wt->text3d && wt->role == Wall::Role::Editor) - wt->text3d->pos.y += delta; - _editor_wall.scroll(delta); - _editor_wall.bounce(lid); - - projectile_LandOnFloor(result); // shot tokens drop; number stays put - } else { - walltext_HandleShootDown(result, wall_text_to_erase); - } - break; + projectile_LandOnWallText (result, wall_text_to_erase); + break; case ProjectileLanding::None: break; } } diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index b9b0e9e..8a0adf0 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -29,6 +29,7 @@ #include "utils/globals.hpp" #include "wall/editor_wall.hpp" #include "wall/text_wall.hpp" +#include "wall/wall_label.hpp" struct StepLedger { @@ -71,16 +72,18 @@ class GameState : public IState double _accumulator { 0.0 }; double _alpha { 0.0 }; - DynArray<Text3d*> _typing_words; - std::string _typing_last; + DynArray<WallLabel> _wall_labels; + + DynArray<Text3d*> _typing_words; + std::string _typing_last; DynArray<Projectile> _projectiles; DynArray<WordAnim_Add> _anims_add; DynArray<WordAnim_Drop> _anims_drop; - DynArray<GameObject*> _mod_scenery; - DynArray<GameObject*> _step_scenery; - DynArray<StepPlate> _step_plates; + DynArray<GameObject*> _mod_scenery; + DynArray<GameObject*> _step_scenery; + DynArray<StepPlate> _step_plates; cpputils::unique_ptr<InputSystem> _input_sys; cpputils::unique_ptr<PhysicsSystem> _physics_sys; @@ -137,10 +140,10 @@ class GameState : public IState void infowall_StepdetailsShow (); void walltext_HandleShootDown (ProjectileResult& result, DynArray<size_t>& to_erase); - void program_FeedInput(ProjectileResult& result); - - void projectile_LandOnFloor (ProjectileResult& result); - void projectile_LandOnWall (ProjectileResult& result); + void projectile_FeedProgramInput(ProjectileResult&); + void projectile_LandOnFloor (ProjectileResult&); + void projectile_LandOnWall (ProjectileResult&); + void projectile_LandOnWallText (ProjectileResult&, DynArray<size_t>& wall_text_to_erase); void pult_DispatchAction (PultAction act); diff --git a/src/systems/aim.hpp b/src/systems/aim.hpp new file mode 100644 index 0000000..deb2b59 --- /dev/null +++ b/src/systems/aim.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include <SDL3/SDL.h> + +#include "entities/camera.hpp" +#include "entities/game_object.hpp" +#include "systems/collision.hpp" +#include "text_3d/text_3d.hpp" +#include "utils/globals.hpp" +#include "utils/math.hpp" +#include "wall/text_wall.hpp" + +// Aim::crosshair_Target — the first surface the crosshair ray meets. +// +// .point world aim point (always valid: wall / floor / object hit) +// .track non-null ONLY when a movable word (from `words`) was hit — that +// object should be hard-tracked so the throw follows it. +// .local the aim point expressed in the target's local frame (yaw-derotated) +// so it rides the word's rotation: a hit on the right edge stays on +// the right edge as the word spins. +// +// Scenery, walls, floor, and stuck wall-text are treated as STATIC points +// (track == nullptr): they don't move, and keeping them out of tracking avoids +// the differing-lifetime list problem (wall_texts live in a separate vector). +namespace Aim +{ + +struct AimResult { + Vec3 point {}; + Text3d* track { nullptr }; + Vec3 local {}; +}; + +inline AimResult +crosshair_Target(const Camera& camera, + const DynArray<Text3dPtr>& words, + const DynArray<GameObjectPtr>& scenery, + const DynArray<TextWallPtr>& wall_texts, + float world_scale) +{ + const Vec3 eye = camera.pos; + const Vec3 dir = camera.fwd; // unit + const double ws = static_cast<double>(world_scale); + + // 1. Distance along the ray to the box surface (nearest bounding plane). + double wall_t = 1e18; + auto plane = [&](double o, double d) { + if (SDL_fabs(d) < 1e-9) return; + const double t = SDL_max((ws - o) / d, (-ws - o) / d); + if (t > 0.0 && t < wall_t) wall_t = t; + }; + plane(eye.x, dir.x); + plane(eye.y, dir.y); + plane(eye.z, dir.z); + if (wall_t > 1e17) wall_t = ws * 2.0; + + const Vec3 seg { dir.x * wall_t, dir.y * wall_t, dir.z * wall_t }; + + AimResult out; + out.point = Vec3{ eye.x + seg.x, eye.y + seg.y, eye.z + seg.z }; // wall point + double best_t = 1.0; // seg-fraction + Text3d* win = nullptr; // winning word, if any + + auto test = [&](const Vec3& mn, const Vec3& mx, Text3d* obj) { + Collision::SweptResult sw = Collision::swept_RayAabb(eye, seg, mn, mx); + if (sw.hit && sw.t < best_t) { + best_t = sw.t; + out.point = { eye.x + seg.x * sw.t, + eye.y + seg.y * sw.t, + eye.z + seg.z * sw.t }; + win = obj; // word → track it; non-word → nullptr + } + }; + + for (const Text3dPtr& w : words) + if (w) test(w->aabb_min(), w->aabb_max(), w.get()); + for (const GameObjectPtr& s : scenery) + if (s) test(s->aabb_min(), s->aabb_max(), nullptr); + for (const TextWallPtr& wt : wall_texts) + if (wt && wt->text3d) test(wt->text3d->aabb_min(), wt->text3d->aabb_max(), nullptr); + + // If a movable word won, record it and de-rotate the offset into its frame. + if (win) { + out.track = win; + const double dx = out.point.x - win->pos.x; + const double dy = out.point.y - win->pos.y; + const double dz = out.point.z - win->pos.z; + const double cy = SDL_cos(win->yaw); + const double sy = SDL_sin(win->yaw); + out.local = { cy * dx - sy * dz, // R(-yaw) * (point - pos) + dy, + sy * dx + cy * dz }; + } + + return out; +} + +} // namespace Aim diff --git a/src/systems/physics_system.cpp b/src/systems/physics_system.cpp index 9817f12..6457086 100644 --- a/src/systems/physics_system.cpp +++ b/src/systems/physics_system.cpp @@ -282,27 +282,53 @@ PhysicsSystem::resolve_WorldBounds(GameObject& obj, float world_scale) /// Projectile void -PhysicsSystem::update_Projectiles(DynArray<Projectile>& projectiles, double dt) +PhysicsSystem::update_Projectiles(DynArray<Projectile>& projectiles, double dt, + const DynArray<Text3dPtr>& words) { for (Projectile& p : projectiles) { if (p.sticking) continue; if (p.words.empty()) continue; - p.step_dt = dt; - + p.step_dt = dt; p.prev_centroid = Text3d::words_centroid(p.words); - // Speed-scaled gravity - double speed = length(p.vel); - double grav_scale = 1.0 + speed * 0.02; - p.vel.y -= ConfigPhysics::proj_grav * grav_scale * dt; - - // Exponential horizontal drag - double drag_factor = SDL_pow(ConfigPhysics::air_drag, dt * 60.0); - p.vel.x *= drag_factor; - p.vel.z *= drag_factor; + // ── Hard-track: steer straight at the target's live point, no gravity. + if (p.track_obj) { + // Validate the target is still alive (could be picked up, shot + // down, consumed, or destroyed on teardown). Never deref a raw + // pointer we haven't re-confirmed this frame. + bool alive = false; + for (const Text3dPtr& w : words) + if (w.get() == p.track_obj) { alive = true; break; } + + if (alive) { + Text3d* o = p.track_obj; + const double cy = SDL_cos(o->yaw), sy = SDL_sin(o->yaw); + const Vec3 live { // pos + R(yaw)*local + o->pos.x + cy * p.track_local.x + sy * p.track_local.z, + o->pos.y + p.track_local.y, + o->pos.z - sy * p.track_local.x + cy * p.track_local.z + }; + p.track_last = live; + + const Vec3 c = p.prev_centroid; + Vec3 d { live.x - c.x, live.y - c.y, live.z - c.z }; + const double L = length(d); + if (L > 1e-6) { + const double s = ConfigPhysics::proj_throw_speed / L; + p.vel = Vec3{ d.x * s, d.y * s, d.z * s }; + } + // No gravity while tracking — fly dead-on to the live point. + } else { + // Target gone: drop tracking, fall toward last known point + // under gravity like a normal thrown word. + p.track_obj = nullptr; + p.vel.y -= ConfigPhysics::proj_grav * dt; + } + } else { + p.vel.y -= ConfigPhysics::proj_grav * dt; + } - // Move all words for (Text3dPtr& w : p.words) { if (!w) continue; w->pos.x += p.vel.x * dt; @@ -400,6 +426,8 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, if (resolve_SingleBoxCollision(proj, obj->aabb_min(), obj->aabb_max(), word_he, RESTITUTION_TEXT, cur, prev, dir, sw)) { + proj.collided = true; + if (obj == proj.track_obj) proj.track_obj = nullptr; obj->body.apply_KickImpulse({ vel_before.x * 0.4, SDL_fabs(vel_before.y) * 0.1 + 0.3, @@ -422,6 +450,7 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, if (resolve_SingleBoxCollision(proj, obj->aabb_min(), obj->aabb_max(), word_he, RESTITUTION_STATIC, cur, prev, dir, sw)) { + proj.collided = true; if (contacts) { contacts->push_back(ProjectileContactEvent{ .projectile = &proj, .other = obj, @@ -434,7 +463,7 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, // 2. Resolve against Text Labels if (labels) { - constexpr double RESTITUTION_LABEL = 0.55; + constexpr double RESTITUTION_LABEL = 0.55; // WTF is that for (TextLabel* label : *labels) { if (!label || label->empty()) continue; @@ -450,6 +479,7 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, if (resolve_SingleBoxCollision(proj, hw->aabb_min(), hw->aabb_max(), word_he, RESTITUTION_LABEL, cur, prev, dir, sw)) { // Handle unique Label side-effect + proj.collided = true; label->kick(i, vel_before, sw.normal); } } diff --git a/src/systems/physics_system.hpp b/src/systems/physics_system.hpp index e919f3b..5ec9b97 100644 --- a/src/systems/physics_system.hpp +++ b/src/systems/physics_system.hpp @@ -45,8 +45,9 @@ public: void update_Player(Player& player, double acc_x, double acc_z, double dt); void update_Objects(const DynArray<GameObject*>& objects, double dt, double gravity, float world_scale); - void update_Projectiles(DynArray<Projectile>& projectiles, double dt); + void update_Projectiles(DynArray<Projectile>& projectiles, double dt, + const DynArray<Text3dPtr>& words); void resolve_Projectiles(DynArray<Projectile>& projectiles, const DynArray<GameObject*>* statics, const DynArray<TextLabel*>* labels, diff --git a/src/systems/projectile_system.cpp b/src/systems/projectile_system.cpp index 24fe12e..44fbc78 100644 --- a/src/systems/projectile_system.cpp +++ b/src/systems/projectile_system.cpp @@ -267,6 +267,7 @@ begin_EditorStick(Projectile& p, Wall::Position hit_pos) c_y + w->ribbon_local.y, 0.0 }; + w->pos.y = w->ribbon_local.y; } } @@ -321,6 +322,7 @@ bounce_OffWall(Projectile& p, Wall::Position hit_pos, double ws, } p.bounce_state |= axis_bit; + p.collided = true; if (x_axis) { p.vel.x = bounce_velocity(p.vel.x, 0.70); @@ -385,9 +387,10 @@ 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& eye) + DynArray<Text3dPtr> words, + const Vec3& fwd, + const Vec3& eye, + const Aim::AimResult& aim) { if (words.empty()) return; @@ -418,8 +421,20 @@ ProjectileSystem::launch(DynArray<Projectile>& out, }; } + // ── Ballistic solve (method b): pick a flight time from horizontal + // distance, then solve the launch velocity so the centroid passes through + // `target` under constant gravity. Always one solution; lands on aim. + const Vec3 target = aim.point; + const double g = ConfigPhysics::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 / ConfigPhysics::proj_throw_speed; + T = SDL_clamp(T, ConfigPhysics::proj_t_min, ConfigPhysics::proj_t_max); + Projectile proj; - proj.vel = fwd * ConfigPhysics::shoot_speed; + 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); @@ -427,6 +442,14 @@ ProjectileSystem::launch(DynArray<Projectile>& out, proj.launch_origin = centroid; proj.cam_origin = eye; proj.prev_centroid = centroid; + + // Hard-track a movable word: steer to its live point until contact. + if (aim.track) { + proj.track_obj = aim.track; + proj.track_local = aim.local; + proj.track_last = aim.point; + } + out.push_back(std::move(proj)); } diff --git a/src/systems/projectile_system.hpp b/src/systems/projectile_system.hpp index a12bce0..01e54c7 100644 --- a/src/systems/projectile_system.hpp +++ b/src/systems/projectile_system.hpp @@ -2,6 +2,7 @@ #include <cstdint> +#include "systems/aim.hpp" #include "text_3d/text_3d.hpp" #include "utils/globals.hpp" #include "utils/math.hpp" @@ -16,14 +17,20 @@ struct Projectile { Vec3 dir{}; Vec3 prev_centroid{}; Vec3 launch_origin{}; - Vec3 cam_origin{}; // camera eye at launch — the crosshair ray origin + Vec3 cam_origin {}; // camera eye at launch — the crosshair ray origin double yaw; - uint8_t bounce_state = 0; - bool sticking = false; + uint8_t bounce_state = 0; + bool sticking = false; + bool collided = false; // hit something → gravity resumes bool program_stick = false; // sticking to the Program wall as input echo - double stick_t = 0.0; - Wall::Position stick_wall { Wall::Position::PosX }; - double step_dt = 0.0; + double stick_t = 0.0; + Wall::Position stick_wall { Wall::Position::PosX }; + double step_dt = 0.0; + + Text3d* track_obj = nullptr; // hard-track target; null = ballistic/free + Vec3 track_local {}; // aim point in target's local frame + Vec3 track_last {}; // last known world point (fallback if obj dies) + Vec3 ribbon_origin {}; Vec3 ribbon_start_origin {}; @@ -48,11 +55,11 @@ enum class ProjectileLanding : uint8_t { }; struct ProjectileResult { - ProjectileLanding landing = ProjectileLanding::None; - Wall::Position wall_pos = Wall::Position::NegZ; - size_t wall_text_index = 0; + ProjectileLanding landing = ProjectileLanding::None; + Wall::Position wall_pos = Wall::Position::NegZ; + size_t wall_text_index = 0; DynArray<Text3dPtr> words; - Vec3 vel{}; + Vec3 vel{}; }; @@ -61,7 +68,8 @@ public: void launch(DynArray<Projectile>& out, DynArray<Text3dPtr> words, const Vec3& fwd, - const Vec3& eye); + const Vec3& eye, + const Aim::AimResult& aim); void update(DynArray<Projectile>& projectiles, DynArray<ProjectileResult>& results, diff --git a/src/utils/carry_math.cpp b/src/utils/carry_math.cpp index ca58122..234eac6 100644 --- a/src/utils/carry_math.cpp +++ b/src/utils/carry_math.cpp @@ -6,10 +6,18 @@ Vec3 CarryMath::line_Anchor(const Camera& camera, double carry_dist) { + // Right-hand carry pose. Camera has no `up`, so derive it: with `right` + // horizontal and `fwd` unit, cross(right, fwd) is the unit up vector. + const Vec3& right = camera.right; + const Vec3 up = cross(camera.right, camera.fwd); + return { - camera.pos.x + camera.fwd.x * carry_dist, - camera.pos.y + camera.fwd.y * carry_dist, + camera.pos.x + camera.fwd.x * carry_dist + + right.x * CARRY_HAND_RIGHT - up.x * CARRY_HAND_DOWN, + camera.pos.y + camera.fwd.y * carry_dist + + right.y * CARRY_HAND_RIGHT - up.y * CARRY_HAND_DOWN, camera.pos.z + camera.fwd.z * carry_dist + + right.z * CARRY_HAND_RIGHT - up.z * CARRY_HAND_DOWN }; } diff --git a/src/utils/carry_math.hpp b/src/utils/carry_math.hpp index 46667ea..2630ec0 100644 --- a/src/utils/carry_math.hpp +++ b/src/utils/carry_math.hpp @@ -13,6 +13,12 @@ namespace CarryMath static constexpr double CARRY_DIST = 1.2; static constexpr double CARRY_GAP = 0.18; + // Hold the carried line in the player's right hand: the anchor (where the + // most-recently-picked word sits) is pushed off the crosshair to the lower + // right. The rest of the line trails left from there, unchanged. + static constexpr double CARRY_HAND_RIGHT = 0.45; // toward the right hand + static constexpr double CARRY_HAND_DOWN = 0.35; // below the crosshair + Vec3 line_Anchor(const Camera& camera, double carry_dist); @@ -21,4 +27,3 @@ namespace CarryMath const Vec3& right, double carry_gap); } - diff --git a/src/utils/config.hpp b/src/utils/config.hpp index 43705b3..0a2658a 100644 --- a/src/utils/config.hpp +++ b/src/utils/config.hpp @@ -20,6 +20,10 @@ static constexpr double stick_dist = 0.2; static constexpr double player_jump_velocity = 8.4375; static constexpr double player_move_accel = 60.0; + +static constexpr double proj_throw_speed = 18.0; +static constexpr double proj_t_min = 0.18; // clamp: nearest throws +static constexpr double proj_t_max = 1.2; // clamp: farthest throws } diff --git a/src/wall/wall_label.cpp b/src/wall/wall_label.cpp new file mode 100644 index 0000000..75a40bb --- /dev/null +++ b/src/wall/wall_label.cpp @@ -0,0 +1,126 @@ +#include "wall_label.hpp" + +#include "entities/camera.hpp" +#include "rendering/gl_renderer.hpp" +#include "utils/config.hpp" +#include "wall/wall.hpp" + +// ── Label appearance ────────────────────────────────────────────────────────── + +namespace { + +static const char* label_Name(Wall::Role r) +{ + switch (r) { + case Wall::Role::Program: return "Program"; + case Wall::Role::Editor: return "Editor"; + case Wall::Role::Info: return "Info"; + case Wall::Role::Compiler: return "Compiler"; + } + return ""; +} + +// Per-role tint — echoes the wall surface colors from wall_surfaces.cpp. +static SDL_FColor label_Color(Wall::Role r) +{ + switch (r) { + case Wall::Role::Program: return { 0.95f, 0.92f, 0.82f, 1.0f }; // warm ivory + case Wall::Role::Editor: return { 0.55f, 0.72f, 0.92f, 1.0f }; // soft blue + case Wall::Role::Info: return { 0.80f, 0.90f, 1.00f, 1.0f }; // pale sky + case Wall::Role::Compiler: return { 0.70f, 0.65f, 1.00f, 1.0f }; // light indigo + } + return ConfigColors::color_white; +} + +} // namespace + + +// ── generate_All ───────────────────────────────────────────────────────────── + +DynArray<WallLabel> +WallLabel::generate_All(float world_scale) +{ + DynArray<WallLabel> labels; + labels.reserve(4); + + const float label_y = -world_scale * 0.68f; + + // Pull slightly further from the wall so it floats in front of wall text. + const double extra_offset = 2.5f; + + // Large scale so the name is readable from across the room. + // depth is Text3d::depth_wall * 4 for chunky, visible letters. + const float label_scale = 0.012f; + const float label_depth = Text3d::depth_wall * 4.0f; + + for (uint8_t ri = 0; ri < 4; ++ri) { + Wall::Role role = static_cast<Wall::Role> (ri); + Wall::Position pos = Wall::position_of (role); + Wall::Transform tf = Wall::get_Transform (role, world_scale); + + Vec3 anch = tf.position; + anch.y = static_cast<double>(label_y); + + switch (pos) { + case Wall::Position::PosX: anch.x -= extra_offset; break; + case Wall::Position::NegX: anch.x += extra_offset; break; + case Wall::Position::PosZ: anch.z -= extra_offset; break; + case Wall::Position::NegZ: anch.z += extra_offset; break; + } + + WallLabel wl; + wl.role = role; + wl.anchor = anch; + wl.face_yaw = tf.yaw; + + wl.label.set_Style (label_Color(role), label_depth, label_scale); + wl.label.set_Billboard (false); + wl.label.set_FaceYaw (tf.yaw); + wl.label.set_Collidable (true); + wl.label.set_Centre (anch); + wl.label.set_Text (label_Name(role)); + + labels.push_back (std::move(wl)); + } + + return labels; +} + + +// ── on_Hit ─────────────────────────────────────────────────────────────────── + +void +WallLabel::on_Hit(size_t word_index, const Vec3& projectile_vel) +{ + const Vec3 normal = wall_InwardNormal(Wall::position_of(role)); + label.kick(word_index, projectile_vel, normal); +} + + +// ── update ─────────────────────────────────────────────────────────────────── + +void +WallLabel::update(double dt, const Camera& camera) +{ + // Positions + jiggle spring first. In non-billboard mode TextLabel::update + // forces every word's pitch to 0, so we must apply our eye-tilt AFTER it, + // not before — otherwise the tilt gets clobbered. Pitch doesn't affect the + // positions update() computes, so ordering it last is safe. + label.update(dt, camera); + + // Tilt up/down toward the player's eye, clamped — identical to the + // editor wall text tilt in wall.hpp::tilt_TowardEye. Shared anchor pitch + // keeps the whole word reading as one rigid plane. + const float pitch = Wall::tilt_TowardEye(anchor, camera.pos); + for (const Text3dPtr& w : label.words()) + if (w) w->pitch = pitch; +} + + +// ── draw ───────────────────────────────────────────────────────────────────── + +void +WallLabel::draw(GlRenderer& gl, const Camera& camera, const Light& light) const +{ + label.draw(gl, camera, light); +} diff --git a/src/wall/wall_label.hpp b/src/wall/wall_label.hpp new file mode 100644 index 0000000..8d5a994 --- /dev/null +++ b/src/wall/wall_label.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "text_3d/text_label.hpp" +#include "utils/globals.hpp" +#include "utils/light.hpp" +#include "wall/wall.hpp" + +class GlRenderer; +class Camera; + +// WallLabel: a large name-plate hovering in front of each wall at a height +// where the player has to look up slightly. Shooting it jiggles the words +// via TextLabel::kick() but they spring back to their anchor position. +// +// Orientation: fixed yaw (faces the room, same as wall text), pitch tilts +// up/down toward the player's eye each frame — identical to the editor +// wall text behaviour. +struct WallLabel +{ + Wall::Role role; + TextLabel label; + Vec3 anchor; // world-space centre (never changes after generate()) + float face_yaw; // fixed yaw matching the wall's orientation + + // Factory: creates one label per wall role, positioned at the given scale. + static DynArray<WallLabel> + generate_All(float world_scale); + + // Call when a projectile hits word at word_index with the given velocity. + // The push direction is the wall's own inward normal (derived from role), + // so the caller doesn't need a per-hit surface normal. + void on_Hit(size_t word_index, const Vec3& projectile_vel); + + // Per-frame: tilt label toward eye and update jiggle spring. + void update(double dt, const Camera& camera); + + void draw(GlRenderer& gl, const Camera& camera, const Light& light) const; +}; + +// Free helper: the inward-facing surface normal for a wall position (points +// from the wall into the room). kick() uses this as the push direction. +inline Vec3 wall_InwardNormal(Wall::Position pos) +{ + switch (pos) { + case Wall::Position::PosX: return { -1.0, 0.0, 0.0 }; + case Wall::Position::NegX: return { 1.0, 0.0, 0.0 }; + case Wall::Position::PosZ: return { 0.0, 0.0, -1.0 }; + case Wall::Position::NegZ: return { 0.0, 0.0, 1.0 }; + } + return { 0.0, 0.0, 1.0 }; +}