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.gzBetter shoot between lines With Claude
src/c_token/token.hpp | 10 +- src/c_token/token_db.hpp | 11 +- src/core/game.hpp | 15 ++- src/entities/camera.hpp | 9 +- src/entities/game_object.hpp | 45 +++---- src/entities/player.cpp | 13 +- src/entities/player.hpp | 17 +-- src/entities/pult.hpp | 2 +- src/entities/world.hpp | 12 +- src/levels/goal.hpp | 69 ++-------- src/levels/level.hpp | 27 ++-- src/levels/level/char_copy.cpp | 49 +++---- src/levels/level/sandbox.cpp | 10 +- src/levels/level/tutorial.cpp | 256 ++++++++++++++++++------------------ src/levels/level_check.hpp | 8 +- src/levels/scene.hpp | 44 +++---- src/rendering/floor_mandelbrot.hpp | 5 - src/states/game_state.cpp | 8 +- src/states/game_state.hpp | 97 +++++++------- src/states/menu_state.hpp | 12 +- src/systems/aim.hpp | 24 +--- src/systems/carry_system.hpp | 24 ++-- src/systems/celebration_system.hpp | 10 +- src/systems/collision.hpp | 2 +- src/systems/compiler_system.hpp | 6 +- src/systems/dyn_loader_system.hpp | 20 +-- src/systems/hint_system.hpp | 2 +- src/systems/input_system.hpp | 10 +- src/systems/level_director.cpp | 2 +- src/systems/level_director.hpp | 16 ++- src/systems/level_runner_system.cpp | 2 +- src/systems/level_runner_system.hpp | 28 ++-- src/systems/particle_system.hpp | 26 ++-- src/systems/physics_system.hpp | 7 +- src/systems/pick_system.hpp | 11 +- src/systems/plate_system.cpp | 8 -- src/systems/plate_system.hpp | 1 - src/systems/program_system.cpp | 2 +- src/systems/program_system.hpp | 21 ++- src/systems/projectile_system.cpp | 29 +++- src/systems/projectile_system.hpp | 75 ++++++----- src/systems/wall_connection.hpp | 23 ++-- src/systems/word_ai_system.hpp | 28 ++-- src/systems/word_anim.hpp | 21 +-- src/text_3d/text_label.hpp | 62 ++++----- src/utils/carry_math.hpp | 19 ++- src/utils/config.hpp | 1 + src/utils/frame_profiler.hpp | 8 +- src/utils/math.hpp | 12 +- src/wall/editor_wall.cpp | 186 ++++++++++++++++++++++---- src/wall/editor_wall.hpp | 114 +++++----------- src/wall/text_wall.hpp | 13 +- src/wall/wall_label.hpp | 31 ++--- src/wall/wall_surfaces.hpp | 14 +- 54 files changed, 803 insertions(+), 774 deletions(-) diff --git a/src/c_token/token.hpp b/src/c_token/token.hpp index 924621f..88ea12b 100644 --- a/src/c_token/token.hpp +++ b/src/c_token/token.hpp @@ -4,18 +4,18 @@ #include <string> enum class TokenType : uint8_t { - Keyword, - Type, + Delimiter, Identifier, + Keyword, Literal, Operator, - Delimiter, Preprocessor, + Type, }; struct Token { std::string text; TokenType type; - std::string short_desc; // shown on info wall: "Integer type" - std::string long_desc; // shown on info wall: "Used to declare integer variables" + std::string short_desc; + std::string long_desc; }; diff --git a/src/c_token/token_db.hpp b/src/c_token/token_db.hpp index 12b8abd..e0a0355 100644 --- a/src/c_token/token_db.hpp +++ b/src/c_token/token_db.hpp @@ -5,12 +5,11 @@ namespace TokenDB { - const Token* find(const std::string& text); - const Token* find_OrSynthesize(const std::string& text); - DynArray<const Token*> get_AllOfType(TokenType type); - const char* get_TypeName(TokenType type); + bool known(const std::string& text); + const Token* find (const std::string& text); + const Token* find_OrSynthesize(const std::string& text); + const char* get_TypeName (TokenType type); + DynArray<const Token*> get_AllOfType(TokenType type); DynArray<std::string> get_TestArrayAllKeywords(); - - bool known(const std::string& text); } diff --git a/src/core/game.hpp b/src/core/game.hpp index 0845c02..7f5bb17 100644 --- a/src/core/game.hpp +++ b/src/core/game.hpp @@ -11,17 +11,18 @@ class Game { - GlRendererPtr _gl_renderer{}; SDL_Window* _window {}; + GlRendererPtr _gl_renderer{}; cpputils::unique_ptr<IState> m_state; public: ~Game(); - bool init(); - void change_State(cpputils::unique_ptr<IState> next); + void change_State (cpputils::unique_ptr<IState> next); + + void handle_Event (const SDL_Event& event); + bool init (); + void update (double dt_ns); + void render (); - void handle_Event(const SDL_Event& event); - void update(double dt_ns); - void render(); - void shutdown(); + void shutdown (); }; diff --git a/src/entities/camera.hpp b/src/entities/camera.hpp index fae6d2d..aa8f82b 100644 --- a/src/entities/camera.hpp +++ b/src/entities/camera.hpp @@ -7,13 +7,14 @@ #include "entities/player.hpp" struct Camera { - Vec3 pos{}; + int32_t pitch {}; uint32_t yaw {}; double yaw_r {}; - int32_t pitch {}; double pitch_r{}; - Vec3 fwd {}; - Vec3 right {}; + + Vec3 pos {}; + Vec3 fwd {}; + Vec3 right{}; void syncFromPlayer(const Player& player) { diff --git a/src/entities/game_object.hpp b/src/entities/game_object.hpp index 8047f80..33bc68c 100644 --- a/src/entities/game_object.hpp +++ b/src/entities/game_object.hpp @@ -16,12 +16,12 @@ enum class ObjectType : uint8_t { enum class PhysicsMode : uint8_t { - Static, - Dynamic, - Hover, AiHover, Carried, + Dynamic, + Hover, Projectile, + Static, }; @@ -31,25 +31,25 @@ struct CollisionShape { struct RigidBody { - Vec3 vel {}; - Vec3 force {}; - double mass {1.0}; - double restitution{0.3}; - double friction {0.8}; + bool hover_disturbed{false}; + double friction {0.8}; + double hover_height{0.0}; + double hover_phase {0.0}; + double mass {1.0}; + double restitution {0.3}; + Vec3 vel {}; + Vec3 force{}; PhysicsMode mode{PhysicsMode::Hover}; - double hover_phase {0.0}; - double hover_height {0.0}; - bool hover_disturbed{false}; RigidBody(PhysicsMode m = PhysicsMode::Hover, double mass_ = 1.0, double restitution_ = 0.3, double friction_ = 0.8 - ) : mass(mass_), + ) : friction(friction_), + mass(mass_), restitution(restitution_), - friction(friction_), mode(m) {} void apply_Force(const Vec3& f) { @@ -80,23 +80,24 @@ struct RigidBody { struct GameObject { - Vec3 pos {}; - float yaw {0.0f}; - ObjectType type {ObjectType::Text}; + bool carriable {true}; + bool interactable {true}; + bool solid {true}; + + float yaw {0.0f}; + Vec3 pos {}; + + ObjectType type{ObjectType::Text}; CollisionShape shape; RigidBody body; - bool carriable {true}; - bool interactable {true}; - bool solid {true}; - GameObject() {} // c = carriable, a = active, i = interactable, s = solid GameObject(Vec3 p, ObjectType t, CollisionShape sh, RigidBody b, bool c = true, bool i = true, bool s = true) - : pos(p), type(t), shape(sh), body(b), - carriable(c), interactable(i), solid(s) {} + : carriable(c), interactable(i), solid(s), + pos(p), type(t), shape(sh), body(b) {} virtual ~GameObject() = default; virtual void draw(GlRenderer&, const Camera&, const Light&) const = 0; diff --git a/src/entities/player.cpp b/src/entities/player.cpp index 1eda670..6d4f7c6 100644 --- a/src/entities/player.cpp +++ b/src/entities/player.cpp @@ -3,13 +3,12 @@ Player::Player() - : pos{8.0, 0.0, 8.0}, - vel{0.0, 0.0, 0.0}, - yaw(0x20000000), - pitch(-0x08000000), - radius(0.5f), - height(1.5f) -{} + : pitch (-0x08000000), + yaw (0x20000000), + radius(0.5f), + height(1.5f), + pos {8.0, 0.0, 8.0}, + vel {0.0, 0.0, 0.0} {} void Player::apply_Collision(const PlayerCollision& col) diff --git a/src/entities/player.hpp b/src/entities/player.hpp index fcadfef..0411920 100644 --- a/src/entities/player.hpp +++ b/src/entities/player.hpp @@ -9,11 +9,11 @@ struct PlayerCollision { - Vec3 pos {}; - Vec3 vel {}; bool hit_x {}; bool hit_y {}; bool hit_z {}; + Vec3 pos {}; + Vec3 vel {}; }; @@ -24,15 +24,15 @@ struct Player public: Player(); - Vec3 pos {}; - Vec3 prev_pos {}; - Vec3 vel {}; - - uint32_t yaw {}; + bool on_ground{false}; int32_t pitch {}; + uint32_t yaw {}; float radius {0.5f}; float height {1.5f}; - bool on_ground{false}; + + Vec3 pos {}; + Vec3 prev_pos {}; + Vec3 vel {}; void compute_Movement(const FrameInput& input); void add_Look (int dx, int dy); @@ -41,3 +41,4 @@ public: double get_AccZ () const { return _acc_z; } void respawn (const Vec3& spawn_pos); }; + diff --git a/src/entities/pult.hpp b/src/entities/pult.hpp index 042947a..5983119 100644 --- a/src/entities/pult.hpp +++ b/src/entities/pult.hpp @@ -35,8 +35,8 @@ struct PultButton final : public SolidBox PultButton(const std::string& lbl, PultAction act, Vec3 world_pos); void press(); - void tick(double dt); + void tick(double dt); void draw(GlRenderer& gl, const Camera& cam, const Light& light) const override; }; diff --git a/src/entities/world.hpp b/src/entities/world.hpp index f62aca0..f244aef 100644 --- a/src/entities/world.hpp +++ b/src/entities/world.hpp @@ -21,19 +21,17 @@ public: World(); PlayerCollision - resolve_Player(const Player& player) const; + resolve_Player(const Player& player) const; - void - draw(GlRenderer& gl, const Camera& camera, const Light& light) const; + void draw(GlRenderer& gl, const Camera& camera, const Light& light) const; - Vec3 - get_SpawnPoint() const; + Vec3 get_SpawnPoint() const; template <typename T = float> inline static T - get_FloorY() { return static_cast<T>(-ConfigGame::world_box_scale); } + get_FloorY() { return static_cast<T>(-ConfigGame::world_box_scale); } template <typename T = float> inline static T - get_BoxScale() { return static_cast<T>(ConfigGame::world_box_scale); } + get_BoxScale() { return static_cast<T>(ConfigGame::world_box_scale); } }; diff --git a/src/levels/goal.hpp b/src/levels/goal.hpp index 1ff6b80..46e458e 100644 --- a/src/levels/goal.hpp +++ b/src/levels/goal.hpp @@ -3,53 +3,19 @@ #include <string> #include <variant> -#include "entities/pult.hpp" // for PultAction +#include "entities/pult.hpp" #include "utils/globals.hpp" -// ───────────────────────────────────────────────────────────────────────── -// Goal: declarative description of "what makes the current step complete". -// -// Each Level::Step owns one Goal. Each frame, the level runner builds a -// GoalContext from the live game state and asks goal_Satisfied(goal, ctx). -// When that returns true (and the settle timer has elapsed), the step -// advances. -// -// Action handlers (on_Pick, on_Compile, ...) DO NOT call into the level -// system. They do their own job. The level system pulls. -// ───────────────────────────────────────────────────────────────────────── +struct ReachPlate { std::string tag; }; +struct PressButton { PultAction action; }; +struct CollectWord { std::string text; }; +struct DroppedWord {}; +struct ShotWord {}; +struct CompilePattern{ std::string pattern; }; +struct AiWordDefeated{ std::string text; }; -// Player is standing on a plate. Empty tag = any plate this step. -// Non-empty tag matches PlacedPlate::tag in the scene definition. -struct ReachPlate { std::string tag; }; - - -// A specific pult button has been pressed this step. -struct PressButton { PultAction action; }; - - -// Player is carrying (or is in the process of picking up) a word -// with this exact text. -struct CollectWord { std::string text; }; - - -// Player dropped at least one word this step (Q key). -struct DroppedWord {}; - - -// Player shot at least one word this step (left click / shoot-all). -struct ShotWord {}; - - -// Compiler reported success AND the editor wall matches the pattern. -// Replaces the old AdvanceTrigger::Compile + expected_pattern combo. -struct CompilePattern { std::string pattern; }; - -struct AiWordDefeated { std::string text; }; - - -// Forward declare for recursive variant. struct AnyOf; struct AllOf; @@ -75,29 +41,18 @@ struct AnyOf { DynArray<Goal> options; }; struct AllOf { DynArray<Goal> options; }; -// ───────────────────────────────────────────────────────────────────────── -// GoalContext: read-only snapshot of game facts a goal might query. -// -// Built fresh each frame in GameState::update() right before the goal -// check. Anything a goal needs to look at MUST appear here — goals never -// reach into GameState directly. -// ───────────────────────────────────────────────────────────────────────── struct GoalContext { DynArray<std::string> active_plate_tags; DynArray<PultAction> buttons_pressed_this_step; DynArray<std::string> picked_up_this_step; DynArray<std::string> ai_words_defeated_this_step; - // Action flags — cleared on step advance. - bool dropped_this_step {false}; - bool shot_this_step {false}; - bool compile_ok_this_step {false}; + bool dropped_this_step {false}; + bool shot_this_step {false}; + bool compile_ok_this_step {false}; - // Normalized editor text — only populated when compile_ok_this_step is true - // (CompilePattern is the only goal that needs it). - std::string editor_normalized; + std::string editor_normalized; }; -// Evaluate a goal against a context. Pure function, no side effects. bool goal_Satisfied(const Goal& g, const GoalContext& ctx); diff --git a/src/levels/level.hpp b/src/levels/level.hpp index 864a5d6..eb9dcc7 100644 --- a/src/levels/level.hpp +++ b/src/levels/level.hpp @@ -9,8 +9,6 @@ namespace Level { - - struct TokenGrant { DynArray<std::string> choices; @@ -22,33 +20,38 @@ namespace Level struct Step { - std::string hint; - std::string details; + bool reset_carry = false; + int max_ai_words = 5; + + Scene scene; + Goal goal = AllOf{}; + + std::string hint; + std::string details; + DynArray<TokenGrant> grants; - Scene scene; - bool reset_carry = false; - Goal goal = AllOf{}; - int max_ai_words = 5; }; struct Data { + bool dynamic_lib = false; + std::string name; std::string desc; - bool dynamic_lib = false; - std::string source_file = {}; + std::string source_file{}; + DynArray<Step> steps; }; - extern Data build_Tutorial(); extern Data build_CharCopy(); extern Data build_Sandbox (); + extern Data build_Tutorial(); inline const DynArray<Data> levels = { - build_Tutorial(), build_CharCopy(), build_Sandbox (), + build_Tutorial(), }; inline const Data& get_Level(size_t i) diff --git a/src/levels/level/char_copy.cpp b/src/levels/level/char_copy.cpp index e164e33..db2ff62 100644 --- a/src/levels/level/char_copy.cpp +++ b/src/levels/level/char_copy.cpp @@ -6,11 +6,16 @@ Level::Data Level::build_CharCopy() { return { + .dynamic_lib = false, .name = "Char Copy", .desc = "Implement a function that copies a char array and prints the result.", .steps = { // ── Step 0: main scaffold ───────────────────────────────────── { + .reset_carry = false, + .max_ai_words = 3, + .scene = {}, + .goal = CompilePattern{ "int main ( void ) { return 0 ; }" }, .hint = "Build a working main()", .details = R"(C programs start at main(). @@ -35,13 +40,13 @@ Grab tokens and arrange them on the editor wall.)", Level::TokenGrant::give(";"), Level::TokenGrant::give("}"), }, - .scene = {}, - .reset_carry = false, - .goal = CompilePattern{ "int main ( void ) { return 0 ; }" }, - .max_ai_words = 3, }, // ── Step 1: copy_str signature ──────────────────────────────── { + .reset_carry = false, + .max_ai_words = 3, + .scene = {}, + .goal = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { }" }, .hint = "Declare the copy_str function", .details = R"(Now add a function above main(). @@ -74,13 +79,13 @@ Place this BEFORE main.)", Level::TokenGrant::give("{"), Level::TokenGrant::give("}"), }, - .scene = {}, - .reset_carry = false, - .goal = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { }" }, - .max_ai_words = 3, }, // ── Step 2: loop counter ────────────────────────────────────── { + .reset_carry = false, + .max_ai_words = 3, + .scene = {}, + .goal = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { int i ; }" }, .hint = "Add a loop counter inside copy_str", .details = R"(Inside copy_str's body, declare an integer to track our position in the array. @@ -95,13 +100,13 @@ of copy_str.)", Level::TokenGrant::give("i"), Level::TokenGrant::give(";"), }, - .scene = {}, - .reset_carry = false, - .goal = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { int i ; }" }, - .max_ai_words = 3, }, // ── Step 3: for loop scaffold ───────────────────────────────── { + .reset_carry = false, + .max_ai_words = 3, + .scene = {}, + .goal = CompilePattern{ "for ( i = 0 ; i < strlen ( src ) ; i ++ ) { }" }, .hint = "Loop through src", .details = R"(Add a for loop after 'int i;'. @@ -121,13 +126,13 @@ This is the loop scaffold only — the body comes next.)", Level::TokenGrant::give("i"), Level::TokenGrant::give("++"), Level::TokenGrant::give(")"), Level::TokenGrant::give("{"), Level::TokenGrant::give("}"), }, - .scene = {}, - .reset_carry = false, - .goal = CompilePattern{ "for ( i = 0 ; i < strlen ( src ) ; i ++ ) { }" }, - .max_ai_words = 3, }, // ── Step 4: copy the character ──────────────────────────────── { + .reset_carry = false, + .max_ai_words = 3, + .scene = {}, + .goal = CompilePattern{ "dst [ i ] = src [ i ] ;" }, .hint = "Copy one character per iteration", .details = R"(Inside the for loop body, copy src[i] into dst[i]. @@ -142,13 +147,13 @@ Goal (inside the for loop): Level::TokenGrant::give("src"), Level::TokenGrant::give("["), Level::TokenGrant::give("i"), Level::TokenGrant::give("]"), Level::TokenGrant::give(";"), }, - .scene = {}, - .reset_carry = false, - .goal = CompilePattern{ "dst [ i ] = src [ i ] ;" }, - .max_ai_words = 3, }, // ── Step 5: call copy_str and print ────────────────────────── { + .reset_carry = false, + .max_ai_words = 3, + .scene = {}, + .goal = CompilePattern{ "char dst [ 100 ] ; copy_str ( \"hello\" , dst ) ; printf ( \"%s\" , dst ) ;" }, .hint = "Call copy_str and print the result", .details = R"(In main(), do three things before 'return 0;': @@ -174,10 +179,6 @@ Place all three lines BEFORE the existing return.)", Level::TokenGrant::give(","), Level::TokenGrant::give("dst"), Level::TokenGrant::give(")"), Level::TokenGrant::give(";"), }, - .scene = {}, - .reset_carry = false, - .goal = CompilePattern{ "char dst [ 100 ] ; copy_str ( \"hello\" , dst ) ; printf ( \"%s\" , dst ) ;" }, - .max_ai_words = 3, }, }, }; diff --git a/src/levels/level/sandbox.cpp b/src/levels/level/sandbox.cpp index 15a65be..32558b4 100644 --- a/src/levels/level/sandbox.cpp +++ b/src/levels/level/sandbox.cpp @@ -6,12 +6,16 @@ Level::Data Level::build_Sandbox() { return { + .dynamic_lib = true, .name = "Sandbox", .desc = "Build the world yourself: compile a library that spawns objects.", - .dynamic_lib = true, .source_file = "assets/sandbox.c", .steps = { { + .reset_carry = true, + .max_ai_words = 0, + .scene = {}, + .goal = AllOf{}, .hint = "Spawn a box with box(x,y,z, hx,hy,hz);", .details = R"(This level IS the code you write. @@ -27,10 +31,6 @@ Build something like: Then press the Compile pult. Edit and recompile freely.)", .grants = { }, - .scene = {}, - .reset_carry = true, - .goal = AllOf{}, - .max_ai_words = 0, }, }, }; diff --git a/src/levels/level/tutorial.cpp b/src/levels/level/tutorial.cpp index 973b690..4603f63 100644 --- a/src/levels/level/tutorial.cpp +++ b/src/levels/level/tutorial.cpp @@ -12,6 +12,21 @@ Level::build_Tutorial() .steps = { { + .reset_carry = true, + .max_ai_words = 0, + .scene = Scene{ + .ai_seeds = {}, + .plates = { + { .pos = { 8.6, World::get_FloorY<double>() + 2.2, 0.0 }, .tag = "" }, + }, + .platforms = { + { .pos = { 5.0, World::get_FloorY<double>() + 0.4, 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } }, + { .pos = { 6.8, World::get_FloorY<double>() + 1.1, 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } }, + { .pos = { 8.6, World::get_FloorY<double>() + 1.8, 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } }, + }, + .words = {}, + }, + .goal = ReachPlate{ "" }, .hint = "Jump up the steps to the plate", .details = R"(Press SPACE to jump. @@ -22,24 +37,22 @@ Jump with SPACE. Jump on the plate on top of the platform. )", .grants = {}, + }, + + { + .reset_carry = true, + .max_ai_words = 0, .scene = Scene{ - .words = {}, + .ai_seeds = {}, .plates = { - { .pos = { 8.6, World::get_FloorY<double>() + 2.2, 0.0 }, .tag = "" }, + { .pos = { 0.0, World::get_FloorY<double>() + 1.2, 6.0 }, .tag = "" }, }, .platforms = { - { .pos = { 5.0, World::get_FloorY<double>() + 0.4, 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } }, - { .pos = { 6.8, World::get_FloorY<double>() + 1.1, 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } }, - { .pos = { 8.6, World::get_FloorY<double>() + 1.8, 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } }, + { .pos = { 0.0, World::get_FloorY<double>() + 0.6, 6.0 }, .half_extents = { 1.0, 0.4, 1.0 } }, }, - .ai_seeds = {}, + .words = {}, }, - .reset_carry = true, .goal = ReachPlate{ "" }, - .max_ai_words = 0, - }, - - { .hint = "Look at each wall, then jump onto the plate", .details = R"(Move the mouse to look around. @@ -50,22 +63,20 @@ Four walls surround you: - Info: this wall, with hints )", .grants = {}, - .scene = Scene{ - .words = {}, - .plates = { - { .pos = { 0.0, World::get_FloorY<double>() + 1.2, 6.0 }, .tag = "" }, - }, - .platforms = { - { .pos = { 0.0, World::get_FloorY<double>() + 0.6, 6.0 }, .half_extents = { 1.0, 0.4, 1.0 } }, - }, - .ai_seeds = {}, - }, - .reset_carry = true, - .goal = ReachPlate{ "" }, - .max_ai_words = 0, }, { + .reset_carry = true, + .max_ai_words = 0, + .scene = Scene{ + .ai_seeds = {}, + .plates = {}, + .platforms = {}, + .words = { + { .pos = { 5.0, World::get_FloorY<double>() + 1.5, 0.0 }, .text = "hello" }, + }, + }, + .goal = CollectWord{ "hello" }, .hint = "Walk into the floating word and press 'e'", .details = R"(Words float in the air. @@ -75,59 +86,59 @@ Picked words trail in front of you as you move. > hold SHIFT to free-look without turning your body.") )", .grants = {}, - .scene = Scene{ - .words = { - { .text = "hello", .pos = { 5.0, World::get_FloorY<double>() + 1.5, 0.0 } }, - }, - .plates = {}, - .platforms = {}, - .ai_seeds = {}, - }, - .reset_carry = true, - .goal = CollectWord{ "hello" }, - .max_ai_words = 0, }, { - .hint = "Keep 'e' pressed and pick up all words.", - .details = R"(You can carry several words at once. - -Collect "press", "any" and "key" with 'e'. -They queue up in the order you grab them.)", - .grants = {}, + .reset_carry = false, + .max_ai_words = 0, .scene = Scene{ - .words = { - { .text = "keep", .pos = { 5.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - { .text = "e", .pos = { 0.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - { .text = "pressed", .pos = { -5.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - }, + .ai_seeds = {}, .plates = {}, .platforms = {}, - .ai_seeds = {}, + .words = { + { .pos = { 5.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "keep", }, + { .pos = { 0.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "e", }, + { .pos = { -5.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "pressed", }, + }, }, - .reset_carry = false, .goal = AllOf{{ CollectWord{ "keep" }, CollectWord{ "e" }, CollectWord{ "pressed" }, }}, - .max_ai_words = 0, + .hint = "Keep 'e' pressed and pick up all words.", + .details = R"(You can carry several words at once. + +Collect "press", "any" and "key" with 'e'. +They queue up in the order you grab them.)", + .grants = {}, }, { + .reset_carry = false, + .max_ai_words = 0, + .scene = {}, + .goal = DroppedWord{}, .hint = "Press 'q' to drop a word", .details = R"('q' drops the most recently picked word. The dropped word falls to the floor — handy for putting back something you grabbed by mistake.)", .grants = {}, - .scene = {}, - .reset_carry = false, - .goal = DroppedWord{}, - .max_ai_words = 0, }, { + .reset_carry = false, + .max_ai_words = 0, + .scene = { + .words = { + { .pos = { 5.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "keep" }, + { .pos = { 3.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "left" }, + { .pos = { 0.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "click" }, + { .pos = { 2.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "pressed" }, + }, + }, + .goal = ShotWord{}, .hint = "LEFT CLICK to throw a word (you can keep it pressed)", .details = R"(Left click throws the word at the front of your line. @@ -135,69 +146,62 @@ Aim with the crosshair and throw — any direction is fine. Carrying several? Tap F to throw the WHOLE line at once.)", .grants = {}, + }, + { + .reset_carry = false, + .max_ai_words = 0, .scene = { .words = { - { .text = "keep", .pos = { 5.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - { .text = "left", .pos = { 3.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - { .text = "click", .pos = { 0.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - { .text = "pressed", .pos = { 2.0, World::get_FloorY<double>() + 1.5, -3.0 } }, + { .pos = { 1.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "f", }, + { .pos = { 2.0, World::get_FloorY<double>() + 1.5, -2.0 }, .text = "for", }, + { .pos = { 0.0, World::get_FloorY<double>() + 1.5, -3.0 }, .text = "flush",}, }, }, - .reset_carry = false, - .goal = ShotWord{}, - .max_ai_words = 0, - }, - { + .goal = ShotWord{}, .hint = "Press 'f' to flush all carried words", .details = R"(Bla blub ...)", .grants = {}, - .scene = { - .words = { - { .text = "f", .pos = { 1.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - { .text = "for", .pos = { 2.0, World::get_FloorY<double>() + 1.5, -2.0 } }, - { .text = "flush",.pos = { 0.0, World::get_FloorY<double>() + 1.5, -3.0 } }, - }, - }, - .reset_carry = false, - .goal = ShotWord{}, - .max_ai_words = 0, }, { - .hint = "Grab the ammo, then shoot the red word", - .details = R"(Some words turn red and chase you to steal what -you carry. - -Pick up "ammo", then knock out the red "danger" -word by throwing at it with LEFT CLICK before it -reaches you.)", - .grants = {}, + .reset_carry = true, + .max_ai_words = 0, .scene = Scene{ - .words = { - { .text = "ammo", .pos = { 6.0, World::get_FloorY<double>() + 1.5, 6.0 } }, - { .text = "ammo", .pos = { 3.0, World::get_FloorY<double>() + 1.5, 2.0 } }, - { .text = "ammo", .pos = { 6.0, World::get_FloorY<double>() + 1.5, 3.0 } }, - { .text = "ammo", .pos = { 4.0, World::get_FloorY<double>() + 1.5, 6.0 } }, - { .text = "ammo", .pos = { 6.0, World::get_FloorY<double>() + 1.5, 1.0 } }, - }, - .plates = {}, - .platforms = {}, .ai_seeds = { { - .text = "danger", + .aggressiveness = 1.0, .pos = { -6.0, World::get_FloorY<double>() + 2.0, -6.0 }, + .text = "danger", .initial_state = Text3dAI::State::Attack, - .aggressiveness = 1.0, }, }, + .plates = {}, + .platforms = {}, + .words = { + { .pos = { 6.0, World::get_FloorY<double>() + 1.5, 6.0 }, .text = "ammo", }, + { .pos = { 3.0, World::get_FloorY<double>() + 1.5, 2.0 }, .text = "ammo", }, + { .pos = { 6.0, World::get_FloorY<double>() + 1.5, 3.0 }, .text = "ammo", }, + { .pos = { 4.0, World::get_FloorY<double>() + 1.5, 6.0 }, .text = "ammo", }, + { .pos = { 6.0, World::get_FloorY<double>() + 1.5, 1.0 }, .text = "ammo", }, + }, }, - .reset_carry = true, - .goal = AllOf{{ CollectWord{ "ammo" }, AiWordDefeated{ "danger" } }}, - .max_ai_words = 0, + .goal = AllOf{{ CollectWord{ "ammo" }, AiWordDefeated{ "danger" } }}, + .hint = "Grab the ammo, then shoot the red word", + .details = R"(Some words turn red and chase you to steal what +you carry. + +Pick up "ammo", then knock out the red "danger" +word by throwing at it with LEFT CLICK before it +reaches you.)", + .grants = {}, }, { + .reset_carry = true, + .max_ai_words = 0, + .scene = {}, + .goal = CompilePattern{ "int main ( void ) { return 0 ; }" }, .hint = "Build int main(void) { return 0; } and press BUILD", .details = R"(Now make code. @@ -227,13 +231,16 @@ press it (or press 'c' to compile).)", Level::TokenGrant::give(";"), Level::TokenGrant::give("}"), }, - .scene = {}, - .reset_carry = true, - .goal = CompilePattern{ "int main ( void ) { return 0 ; }" }, - .max_ai_words = 0, }, { + .reset_carry = false, + .max_ai_words = 0, + .scene = {}, + .goal = AllOf{{ + CompilePattern{ "int main ( void ) { printf ( \"hello\" ) ; return 0 ; }" }, + PressButton{ PultAction::RunProgram }, + }}, .hint = "Add printf, BUILD, then press RUN", .details = R"(A program that does nothing is dull. Make it print. @@ -255,16 +262,22 @@ the Program wall — and that's what finishes this step.)", Level::TokenGrant::give(")"), Level::TokenGrant::give(";"), }, - .scene = {}, - .reset_carry = false, - .goal = AllOf{{ - CompilePattern{ "int main ( void ) { printf ( \"hello\" ) ; return 0 ; }" }, - PressButton{ PultAction::RunProgram }, - }}, - .max_ai_words = 0, }, { + .reset_carry = false, + .max_ai_words = 0, + .scene = Scene{ + .ai_seeds = {}, + .plates = { + { .pos = { 0.0, World::get_FloorY<double>() + 1.2, -6.0 }, .tag = "" }, + }, + .platforms = { + { .pos = { 0.0, World::get_FloorY<double>() + 0.6, -6.0 }, .half_extents = { 1.0, 0.4, 1.0 } }, + }, + .words = {}, + }, + .goal = ReachPlate{ "" }, .hint = "Try the editor pults, then jump onto the plate", .details = R"(A few tools for managing the Editor wall: @@ -290,41 +303,28 @@ Have a play, then jump onto the plate to continue.)", Level::TokenGrant::give("42"), Level::TokenGrant::give("printf(\"Enter a Number:\\n\\t\")"), }, + }, + + { + .reset_carry = true, + .max_ai_words = 0, .scene = Scene{ - .words = {}, + .ai_seeds = {}, .plates = { - { .pos = { 0.0, World::get_FloorY<double>() + 1.2, -6.0 }, .tag = "" }, + { .pos = { 0.0, World::get_FloorY<double>() + 1.2, 0.0 }, .tag = "" }, }, .platforms = { - { .pos = { 0.0, World::get_FloorY<double>() + 0.6, -6.0 }, .half_extents = { 1.0, 0.4, 1.0 } }, + { .pos = { 0.0, World::get_FloorY<double>() + 0.6, 0.0 }, .half_extents = { 1.0, 0.4, 1.0 } }, }, - .ai_seeds = {}, + .words = {}, }, - .reset_carry = false, - .goal = ReachPlate{ "" }, - .max_ai_words = 0, - }, - - { + .goal = ReachPlate{ "" }, .hint = "Jump onto the plate to begin \"Char Copy\"", .details = R"(You're ready. Jump onto the plate to start the first real level: "Char Copy".)", .grants = {}, - .scene = Scene{ - .words = {}, - .plates = { - { .pos = { 0.0, World::get_FloorY<double>() + 1.2, 0.0 }, .tag = "" }, - }, - .platforms = { - { .pos = { 0.0, World::get_FloorY<double>() + 0.6, 0.0 }, .half_extents = { 1.0, 0.4, 1.0 } }, - }, - .ai_seeds = {}, - }, - .reset_carry = true, - .goal = ReachPlate{ "" }, - .max_ai_words = 0, }, }, }; diff --git a/src/levels/level_check.hpp b/src/levels/level_check.hpp index 97b6928..21d13b6 100644 --- a/src/levels/level_check.hpp +++ b/src/levels/level_check.hpp @@ -1,10 +1,10 @@ #pragma once #include <string> -namespace LevelCheck { - // Normalize: trim, collapse all whitespace (tab/newline/space) to single spaces. - std::string normalize(const std::string& s); +namespace LevelCheck +{ + std::string + normalize(const std::string& s); - // Does `haystack` (already normalized) contain `needle` (will be normalized)? bool contains_pattern(const std::string& haystack, const std::string& needle); } diff --git a/src/levels/scene.hpp b/src/levels/scene.hpp index 531da62..8f74766 100644 --- a/src/levels/scene.hpp +++ b/src/levels/scene.hpp @@ -8,50 +8,44 @@ #include "utils/math.hpp" -// A word placed at a specific world position. Becomes a Hover-mode Text3d -// in ctx.words. If `required` is set, the PickAllWords goal will check -// whether this word is currently in the carry line. struct PlacedWord { - std::string text; - Vec3 pos; + bool required = false; + Vec3 pos {}; + SDL_FColor color = ConfigColor::floor; - bool required = false; + std::string text; }; -// A glowing pad. The step's goal can require the player to stand on it. -// `tag` is optional — used by goals that target a specific plate. struct PlacedPlate { - Vec3 pos; + Vec3 pos; + std::string tag; }; -// A solid platform. Inert scenery the player can walk and jump on. -// Color defaults to a neutral grey-blue; override for visual variety. struct PlacedPlatform { - Vec3 pos; - Vec3 half_extents { 1.0, 0.2, 1.0 }; - SDL_FColor fill_color { 0.45f, 0.45f, 0.65f, 1.0f }; - SDL_FColor wire_color { 0.70f, 0.70f, 0.90f, 1.0f }; + Vec3 pos; + Vec3 half_extents { 1.0, 0.2, 1.0 }; + + SDL_FColor fill_color { 0.45f, 0.45f, 0.65f, 1.0f }; + SDL_FColor wire_color { 0.70f, 0.70f, 0.90f, 1.0f }; }; -// A pre-aggressive AI word. Spawned in the requested state (typically Attack) -// at the requested position. struct PlacedAi { - std::string text; - Vec3 pos; - Text3dAI::State initial_state = Text3dAI::State::Attack; - double aggressiveness = 1.0; + double aggressiveness = 1.0; + Vec3 pos; + + std::string text; + + Text3dAI::State initial_state = Text3dAI::State::Attack; }; -// All spatial content for a single step. Spawned on step entry, removed -// on step advance (or reset). struct Scene { - DynArray<PlacedWord> words {}; + DynArray<PlacedAi> ai_seeds {}; DynArray<PlacedPlate> plates {}; DynArray<PlacedPlatform> platforms{}; - DynArray<PlacedAi> ai_seeds {}; + DynArray<PlacedWord> words {}; }; diff --git a/src/rendering/floor_mandelbrot.hpp b/src/rendering/floor_mandelbrot.hpp index b8424c1..7c68419 100644 --- a/src/rendering/floor_mandelbrot.hpp +++ b/src/rendering/floor_mandelbrot.hpp @@ -10,11 +10,6 @@ class Camera; -// FloorMandelbrot: bakes a triangle-mesh floor with the classic full -// Mandelbrot set, colored in discrete iteration bands. Each iteration -// count maps to one of 8 cycling pastel-rainbow colors, giving sharp -// transitions and clear "ring" regions around the set boundary. Inside -// the set is a near-black indigo. class FloorMandelbrot { DynArray<float> _verts; DynArray<int> _indices; diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 936a145..51308fd 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -830,15 +830,15 @@ GameState::update_WorldSimulation(double dt) if (!wt || !wt->text3d) continue; wall_text_hits.push_back({ wt->wall_position, + i, wt->text3d->aabb_min(), wt->text3d->aabb_max(), - i }); } 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 }); + wall_text_hits.push_back({ Wall::Position::NegX, nh.index, + nh.aabb_min, nh.aabb_max }); for (size_t li = 0; li < _wall_labels.size(); ++li) { const DynArray<Text3dPtr>& words = _wall_labels[li].label.words(); @@ -847,9 +847,9 @@ GameState::update_WorldSimulation(double dt) constexpr size_t LABEL_HIT_BASE = size_t(1) << 48; wall_text_hits.push_back({ Wall::position_of(_wall_labels[li].role), + LABEL_HIT_BASE | (li << 16) | wi, words[wi]->aabb_min(), words[wi]->aabb_max(), - LABEL_HIT_BASE | (li << 16) | wi }); } } diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index 4553153..16e8b1a 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -36,80 +36,79 @@ class GameState : public IState { - GameContext _ctx; - FrameInput _frame_input; - CarriedLine _carried; - EditorWall _editor_wall; + + cpputils::unique_ptr<CelebrationSystem>_celebration_sys; + cpputils::unique_ptr<CompilerSystem> _compiler_sys; + cpputils::unique_ptr<DynLoaderSystem> _dyn_loader_sys; + cpputils::unique_ptr<FloorMandelbrot> _floor_sys; + cpputils::unique_ptr<FreelookSystem> _freelook_sys; + cpputils::unique_ptr<HintSystem> _hint_sys; + cpputils::unique_ptr<InputSystem> _input_sys; + cpputils::unique_ptr<LevelDirector> _level_director; + cpputils::unique_ptr<ParticleSystem> _particle_sys; + cpputils::unique_ptr<PhysicsSystem> _physics_sys; + cpputils::unique_ptr<PlateSystem> _plate_sys; + cpputils::unique_ptr<ProgramSystem> _program_sys; + cpputils::unique_ptr<ProjectileSystem> _projectiles_sys; + cpputils::unique_ptr<PultSystem> _pult_sys; + cpputils::unique_ptr<WordAiSystem> _word_ai_sys; + cpputils::unique_ptr<WordAnimSystem> _word_anim_sys; double _pick_cooldown { 0.0 }; double _shoot_cooldown { 0.0 }; + std::string _typing_last; + CarriedLine _carried; + EditorWall _editor_wall; + FrameInput _frame_input; + GameContext _ctx; WallConnection _wall_connection; - void wallconnect_BeginProgram(); - void wallconnect_BeginEditor (); - void on_WallConnectSubmit (); - void on_WallConnectExit (); - - DynArray<std::string> editor_AbsorbLines(); - void editor_Clear (); - void editor_CommitToWall (const DynArray<std::string>& lines); - void editor_PlaceLines (const DynArray<std::string>& lines); - - DynArray<Vec3> _freelook_return_from; - - DynArray<WallLabel> _wall_labels; - - DynArray<Text3d*> _typing_words; - std::string _typing_last; + DynArray<GameObject*> _dyn_scenery; + DynArray<Text3d*> _typing_words; + DynArray<Vec3> _freelook_return_from; DynArray<Projectile> _projectiles; + DynArray<WallLabel> _wall_labels; DynArray<WordAnim_Add> _anims_add; DynArray<WordAnim_Drop> _anims_drop; - DynArray<GameObject*> _dyn_scenery; + void on_WallConnectExit (); + void on_WallConnectSubmit (); + void wallconnect_BeginEditor (); + void wallconnect_BeginProgram(); - cpputils::unique_ptr<InputSystem> _input_sys; - cpputils::unique_ptr<PhysicsSystem> _physics_sys; - cpputils::unique_ptr<ProjectileSystem> _projectiles_sys; - cpputils::unique_ptr<WordAnimSystem> _word_anim_sys; - cpputils::unique_ptr<CompilerSystem> _compiler_sys; - cpputils::unique_ptr<ProgramSystem> _program_sys; - cpputils::unique_ptr<FreelookSystem> _freelook_sys; - cpputils::unique_ptr<PultSystem> _pult_sys; - cpputils::unique_ptr<HintSystem> _hint_sys; - cpputils::unique_ptr<WordAiSystem> _word_ai_sys; - cpputils::unique_ptr<PlateSystem> _plate_sys; - cpputils::unique_ptr<ParticleSystem> _particle_sys; - cpputils::unique_ptr<FloorMandelbrot> _floor_sys; - cpputils::unique_ptr<DynLoaderSystem> _dyn_loader_sys; - cpputils::unique_ptr<CelebrationSystem>_celebration_sys; - cpputils::unique_ptr<LevelDirector> _level_director; + DynArray<std::string> + editor_AbsorbLines (); + void editor_Clear (); + void editor_CommitToWall (const DynArray<std::string>& lines); + void editor_PlaceLines (const DynArray<std::string>& lines); bool on_Pick (); - void on_Click (); - void on_DropLast (); bool on_ShootOne (); - void on_ShootAll (); + void on_Click (); + void on_Compile (); void on_DebugPrint (); - void on_Jump (); - void on_FreelookStart(); + void on_DropLast (); void on_FreelookEnd (); - void on_Compile (); - void on_Run (); + void on_FreelookStart(); + void on_Jump (); void on_NumberAdjust (int dt); - void on_TypingStart (FrameInput&); - void on_TypingCommit (FrameInput&); + void on_Run (); + void on_ShootAll (); void on_TypingCancel (FrameInput&); + void on_TypingCommit (FrameInput&); + void on_TypingStart (FrameInput&); - void update_WorldSimulation(double dt); - void level_TryAdvance (double dt); void carriedline_Update (); void carry_BeginAnim (Text3dPtr word, bool with_pitch); - void typing_RebuildPreview (const std::string& s); + void level_TryAdvance (double dt); void typing_ClearPreview (); + void typing_RebuildPreview (const std::string& s); + + void update_WorldSimulation(double dt); void infowall_Clear (); void walltext_HandleShootDown (ProjectileResult& result, DynArray<size_t>& to_erase); diff --git a/src/states/menu_state.hpp b/src/states/menu_state.hpp index f0a0fe2..a82866b 100644 --- a/src/states/menu_state.hpp +++ b/src/states/menu_state.hpp @@ -40,17 +40,17 @@ class MenuState : public IState double _t{0.0}; - void build_Items (); - void activate (Game& game); + void build_Items (); + void activate (Game& game); + bool is_LevelItem(size_t idx) const; size_t pick_FromCrosshair() const; - bool is_LevelItem (size_t idx) const; static uint32_t yaw_FromRadians(double rad); static double yaw_ToRadians (uint32_t yaw); public: MenuState(); - void handle_Event(Game&, const SDL_Event&) override; - void update(double dt) override; - void render(SDL_Window&, GlRenderer&) override; + void handle_Event(Game&, const SDL_Event&) override; + void update (double dt) override; + void render (SDL_Window&, GlRenderer&) override; }; diff --git a/src/systems/aim.hpp b/src/systems/aim.hpp index 0543889..bf54bd1 100644 --- a/src/systems/aim.hpp +++ b/src/systems/aim.hpp @@ -10,25 +10,14 @@ #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 Result { - Vec3 point {}; Text3d* track { nullptr }; - Vec3 local {}; + + Vec3 point {}; + Vec3 local {}; }; inline Result @@ -39,7 +28,7 @@ crosshair_Target(const Camera& camera, float world_scale) { const Vec3 eye = camera.pos; - const Vec3 dir = camera.fwd; // unit + const Vec3 dir = camera.fwd; const double ws = static_cast<double>(world_scale); // 1. Distance along the ray to the box surface (nearest bounding plane). @@ -68,7 +57,7 @@ crosshair_Target(const Camera& camera, 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 + win = obj; } }; @@ -79,7 +68,6 @@ crosshair_Target(const Camera& camera, 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; @@ -87,7 +75,7 @@ crosshair_Target(const Camera& camera, 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) + out.local = { cy * dx - sy * dz, dy, sy * dx + cy * dz }; } diff --git a/src/systems/carry_system.hpp b/src/systems/carry_system.hpp index 2bfd76d..7e81a21 100644 --- a/src/systems/carry_system.hpp +++ b/src/systems/carry_system.hpp @@ -14,21 +14,21 @@ struct CarriedWordNode { }; struct CarriedLine { - cpputils::unique_ptr<CarriedWordNode> first; // Owns the first node - CarriedWordNode* last{nullptr}; // Raw pointer to last node (non-owning) + cpputils::unique_ptr<CarriedWordNode> first; + CarriedWordNode* last{nullptr}; size_t size{0}; - bool empty() const; - - void push(Text3dPtr w); - Text3dPtr pop_back(); - Text3dPtr pop_front(); - - DynArray<CarriedWordNode*> nodes() const; + bool empty () const; + void clear (); + void compact(); - DynArray<CarriedWordNode*> reverse_nodes() const; - void clear(); + void push (Text3dPtr w); + Text3dPtr pop_back (); + Text3dPtr pop_front(); - void compact(); + DynArray<CarriedWordNode*> + nodes () const; + DynArray<CarriedWordNode*> + reverse_nodes() const; }; diff --git a/src/systems/celebration_system.hpp b/src/systems/celebration_system.hpp index 244de1c..dca732a 100644 --- a/src/systems/celebration_system.hpp +++ b/src/systems/celebration_system.hpp @@ -15,10 +15,10 @@ class CelebrationSystem { _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; + 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; + void draw (GlRenderer&, const Camera&, const Light&) const; }; diff --git a/src/systems/collision.hpp b/src/systems/collision.hpp index f310e03..ae8e8a0 100644 --- a/src/systems/collision.hpp +++ b/src/systems/collision.hpp @@ -8,8 +8,8 @@ namespace Collision { struct Manifold { bool hit = false; - Vec3 normal {}; double overlap {}; + Vec3 normal {}; }; struct SweptResult { diff --git a/src/systems/compiler_system.hpp b/src/systems/compiler_system.hpp index c96db89..af125c6 100644 --- a/src/systems/compiler_system.hpp +++ b/src/systems/compiler_system.hpp @@ -12,11 +12,11 @@ struct CompilerSystem { private: - std::thread _worker; + int _count {0}; + std::atomic<bool> _running{false}; std::mutex _mtx; + std::thread _worker; DynArray<std::string> _pending; - std::atomic<bool> _running{false}; - int _count {0}; void push_Line (TextWallList&, const std::string& line); static void run_Compile(CompilerSystem* self, std::string src); diff --git a/src/systems/dyn_loader_system.hpp b/src/systems/dyn_loader_system.hpp index 47ddd95..66ab185 100644 --- a/src/systems/dyn_loader_system.hpp +++ b/src/systems/dyn_loader_system.hpp @@ -9,16 +9,18 @@ class DynLoaderSystem { - FILE* _pipe { nullptr }; - int _count { 0 }; - int _build_serial { 0 }; + static constexpr float line_height = 0.25f; + static constexpr float start_y = -15.0f; + + FILE* _pipe { nullptr }; + void* _handle { nullptr }; + + int _count { 0 }; + int _build_serial { 0 }; + std::string _pending_so; std::string _last_so_path; - void* _handle { nullptr }; // current dlopen handle - - static constexpr float line_height = 0.25f; - static constexpr float start_y = -15.0f; static std::string wrap_Source(const std::string& user_body); @@ -33,10 +35,8 @@ public: const std::string& last_so_path() const { return _last_so_path; } - // Checkpoint 2: dlopen the last successful .so, call game_build, - // return the spawned descriptions. Empty vector on any failure. DynArray<DynObject> load_Objects(); - ~DynLoaderSystem(); // dlclose on teardown + ~DynLoaderSystem(); }; diff --git a/src/systems/hint_system.hpp b/src/systems/hint_system.hpp index 740a7d2..142345a 100644 --- a/src/systems/hint_system.hpp +++ b/src/systems/hint_system.hpp @@ -8,8 +8,8 @@ class GlRenderer; class HintSystem { - TextLabel _label; std::string _current_hint; + TextLabel _label; public: HintSystem (); diff --git a/src/systems/input_system.hpp b/src/systems/input_system.hpp index ba551cd..150e561 100644 --- a/src/systems/input_system.hpp +++ b/src/systems/input_system.hpp @@ -34,12 +34,12 @@ struct FrameInput { bool capture_typing = false; bool capture_vim = false; - bool typing_mode = false; - bool typing_commit = false; - bool typing_cancel = false; + bool typing_mode = false; + bool typing_commit = false; + bool typing_cancel = false; - int look_x = 0; - int look_y = 0; + int look_x = 0; + int look_y = 0; std::string typing_buffer; DynArray<int> vim_keys; diff --git a/src/systems/level_director.cpp b/src/systems/level_director.cpp index 89b32b9..d2d1766 100644 --- a/src/systems/level_director.cpp +++ b/src/systems/level_director.cpp @@ -42,7 +42,7 @@ LevelDirector::enter_Step(GameContext& ctx) void LevelDirector::spawn_Grants(GameContext& ctx) { - DynArray<std::string> tokens = _runner->grants_for_step(); + DynArray<std::string> tokens = _runner->garants_ForStep(); static constexpr float spacing = 3.0f; static constexpr int per_row = 8; diff --git a/src/systems/level_director.hpp b/src/systems/level_director.hpp index 289d9c4..76f6ecd 100644 --- a/src/systems/level_director.hpp +++ b/src/systems/level_director.hpp @@ -46,16 +46,20 @@ struct StepPlate { class LevelDirector { - cpputils::unique_ptr<LevelRunner> _runner; + cpputils::unique_ptr<LevelRunner> + _runner; + double _step_age { 0.0 }; + StepLedger _ledger; - DynArray<GameObject*> _step_scenery; DynArray<StepPlate> _step_plates; - double _step_age { 0.0 }; + DynArray<GameObject*> _step_scenery; + + void spawn_Grants (GameContext& ctx); + void setup_Scene (GameContext& ctx, const Scene& scene); + void teardown_Scene(GameContext& ctx); - void spawn_Grants (GameContext& ctx); - void setup_Scene (GameContext& ctx, const Scene& scene); - void teardown_Scene(GameContext& ctx); GoalContext build_GoalCtx (const GameContext& ctx) const; + public: struct AdvanceResult { bool stepped {false}; // a step boundary was crossed this frame diff --git a/src/systems/level_runner_system.cpp b/src/systems/level_runner_system.cpp index 2167b83..f66dffe 100644 --- a/src/systems/level_runner_system.cpp +++ b/src/systems/level_runner_system.cpp @@ -8,7 +8,7 @@ bool LevelRunner::advance() return !complete(); } -DynArray<std::string> LevelRunner::grants_for_step() const +DynArray<std::string> LevelRunner::garants_ForStep() const { DynArray<std::string> result; for (const Level::TokenGrant& grant : current().grants) diff --git a/src/systems/level_runner_system.hpp b/src/systems/level_runner_system.hpp index 15aea80..c1d6131 100644 --- a/src/systems/level_runner_system.hpp +++ b/src/systems/level_runner_system.hpp @@ -2,22 +2,24 @@ #include "levels/level.hpp" class LevelRunner { - const Level::Data* _level; - size_t _current_step{0}; + const Level::Data* + _level; + + size_t _current_step{0}; public: explicit LevelRunner(const Level::Data& _level); - // Call after a successful compile - // Returns true if there are more steps, false if _level complete - bool advance(); + bool advance(); + bool complete() const { return _current_step >= _level->steps.size(); } + + size_t step() const { return _current_step; } + const Level::Step& + current() const { return _level->steps[_current_step]; } - bool complete() const { return _current_step >= _level->steps.size(); } - size_t step() const { return _current_step; } - const Level::Step& current() const { return _level->steps[_current_step]; } - bool dynamic_lib() const { return _level->dynamic_lib; } - const std::string& source_file() const { return _level->source_file; } + bool dynamic_lib() const { return _level->dynamic_lib; } + const std::string& + source_file() const { return _level->source_file; } - // All tokens to spawn for the current step (resolved — no choices yet) - // Choices are returned as multiple Text3d objects placed near each other - DynArray<std::string> grants_for_step() const; + DynArray<std::string> + garants_ForStep() const; }; diff --git a/src/systems/particle_system.hpp b/src/systems/particle_system.hpp index 8aa7633..a7b479f 100644 --- a/src/systems/particle_system.hpp +++ b/src/systems/particle_system.hpp @@ -14,14 +14,15 @@ class GameObject; // One particle. Lives ~1-2 seconds, falls under gravity, vanishes on // collision or when its lifetime expires. struct Particle { - Vec3 pos{}; - Vec3 vel{}; + bool alive { true }; + float yaw { 0.0f }; + float spin { 0.0f }; // radians/sec + double age { 0.0 }; + double lifetime { 1.5 }; + + Vec3 pos{}; + Vec3 vel{}; SDL_FColor color { 1.0f, 1.0f, 1.0f, 1.0f }; - double age { 0.0 }; - double lifetime { 1.5 }; - float yaw { 0.0f }; - float spin { 0.0f }; // radians/sec - bool alive { true }; }; @@ -43,15 +44,10 @@ public: void update(double dt, const DynArray<GameObject*>& solid_colliders, float world_scale); - - void draw(GlRenderer& gl, const Camera& cam, const Light& light) const; - + void clear (); + bool empty () const { return _particles.empty(); } + void draw (GlRenderer& gl, const Camera& cam, const Light& light) const; void emit_FireworkBurst(const Vec3& center, SDL_FColor color, int count = 80); - - // Clear all particles — called when celebration ends. - void clear(); - - bool empty() const { return _particles.empty(); } }; diff --git a/src/systems/physics_system.hpp b/src/systems/physics_system.hpp index 8bd2c07..f5fa74b 100644 --- a/src/systems/physics_system.hpp +++ b/src/systems/physics_system.hpp @@ -19,10 +19,11 @@ struct ProjectileContactEvent { class PhysicsSystem { // Uniform spatial grid scratch, reused each frame to avoid reallocation. struct SpatialGrid { - double cell = 2.0; - double inv = 0.5; - int nx = 0, nz = 0; + int nx = 0, nz = 0; + double cell = 2.0; + double inv = 0.5; double min_x = 0, min_z = 0; + DynArray<DynArray<GameObject*>> cells; void build(const DynArray<GameObject*>& objs); diff --git a/src/systems/pick_system.hpp b/src/systems/pick_system.hpp index e086fd5..81a4ed0 100644 --- a/src/systems/pick_system.hpp +++ b/src/systems/pick_system.hpp @@ -10,16 +10,17 @@ enum class PickKind : uint8_t { None, - Word, Scenery, Wall, + Word, }; struct PickResult { - PickKind kind { PickKind::None }; - size_t word_index { 0 }; - size_t wall_index { 0 }; - GameObject* scenery_obj { nullptr }; + size_t word_index { 0 }; + size_t wall_index { 0 }; + + GameObject* scenery_obj { nullptr }; + PickKind kind { PickKind::None }; bool hit() const { return kind != PickKind::None; } }; diff --git a/src/systems/plate_system.cpp b/src/systems/plate_system.cpp index c2eb291..c60f7de 100644 --- a/src/systems/plate_system.cpp +++ b/src/systems/plate_system.cpp @@ -34,11 +34,3 @@ PlateSystem::update(double dt, plate->pulse = 1.0; } } - -bool -PlateSystem::any_Active(const DynArray<StepPlate>& plates) const -{ - for (const StepPlate& sp : plates) - if (sp.plate && sp.plate->active) return true; - return false; -} diff --git a/src/systems/plate_system.hpp b/src/systems/plate_system.hpp index bfd408a..599e2ac 100644 --- a/src/systems/plate_system.hpp +++ b/src/systems/plate_system.hpp @@ -11,5 +11,4 @@ class PlateSystem public: void update(double dt, const Player& player, const DynArray<StepPlate>& plates); - bool any_Active(const DynArray<StepPlate>& plates) const; }; diff --git a/src/systems/program_system.cpp b/src/systems/program_system.cpp index ca58d75..22095be 100644 --- a/src/systems/program_system.cpp +++ b/src/systems/program_system.cpp @@ -27,7 +27,7 @@ ProgramSystem::close_Child() if (_out_fd >= 0) { ::close(_out_fd); _out_fd = -1; } if (_child > 0) { int status; - if (waitpid(_child, &status, WNOHANG) == 0) { // still alive → kill it + if (waitpid(_child, &status, WNOHANG) == 0) { kill(_child, SIGKILL); waitpid(_child, &status, 0); } diff --git a/src/systems/program_system.hpp b/src/systems/program_system.hpp index 7fa4a1a..19e419a 100644 --- a/src/systems/program_system.hpp +++ b/src/systems/program_system.hpp @@ -1,34 +1,31 @@ #pragma once #include <string> -#include <sys/types.h> // pid_t +#include <sys/types.h> #include "entities/world.hpp" #include "utils/globals.hpp" #include "wall/text_wall.hpp" -// ProgramSystem: runs the executable built by CompilerSystem (/tmp/game_prog) -// and streams its stdout/stderr onto the Program wall (+X). The child is -// spawned with BOTH a readable stdout pipe and a writable stdin pipe, so the -// player can feed input to scanf/cin/getline by throwing words at the wall -// (see feed_Input). Polls non-blocking so it never stalls the frame. struct ProgramSystem { private: - int _out_fd {-1}; // read end of child's stdout+stderr - int _in_fd {-1}; // write end of child's stdin - pid_t _child {-1}; - int _count {0}; + int _out_fd {-1}; + int _in_fd {-1}; + int _count {0}; + + pid_t _child {-1}; + std::string _pending; // partial-line buffer for stdout void push_Line(TextWallList&, const std::string& line, float world_scale); - void close_Child(); // close pipes, reap (or kill) the child + void close_Child(); public: static inline float line_height = 0.4f; static inline float start_y = World::get_FloorY() + 1; void run (TextWallList&, float world_scale); void update (TextWallList&, float world_scale); - void feed_Input (const std::string& text); // thrown word(s) → child stdin + void feed_Input (const std::string& text); void echo_Input (TextWallList&, const std::string& line); // Park already-thrown (animated) words on the Program wall as one echoed // input line, scrolling existing output up — terminal style. diff --git a/src/systems/projectile_system.cpp b/src/systems/projectile_system.cpp index 356a364..d59f0a2 100644 --- a/src/systems/projectile_system.cpp +++ b/src/systems/projectile_system.cpp @@ -160,6 +160,7 @@ static bool check_WallTextIntercept(const Projectile& p, const Vec3& centroid, double dt, Wall::Position hit_pos, const DynArray<WallTextHitTest>& wall_text_hits, + double aim_y, bool editor_wall, size_t& out_idx) { const Vec3 next_centroid { @@ -177,6 +178,12 @@ check_WallTextIntercept(const Projectile& p, const Vec3& centroid, double dt, for (const WallTextHitTest& wt : wall_text_hits) { if (wt.wall_position != hit_pos) continue; + if (editor_wall) { + const double center_y = (wt.aabb_min.y + wt.aabb_max.y) * 0.5; + if (SDL_fabs(aim_y - center_y) > ConfigWall::line_height * 0.5) + continue; + } + Vec3 box_min { wt.aabb_min.x - word_he.x, wt.aabb_min.y - word_he.y, wt.aabb_min.z - word_he.z }; @@ -186,10 +193,7 @@ check_WallTextIntercept(const Projectile& p, const Vec3& centroid, double dt, Collision::SweptResult sw = Collision::swept_RayAabb(p.prev_centroid, ray_dir, box_min, box_max); - if (sw.hit) { - out_idx = wt.index; - return true; - } + if (sw.hit) { out_idx = wt.index; return true; } } return false; } @@ -312,6 +316,15 @@ bounce_OffWall(Projectile& p, Wall::Position hit_pos, double ws, } +static double +editor_AimY(const Projectile& p, double plane_x) +{ + if (SDL_fabs(p.dir.x) <= 1e-6) return Text3d::words_centroid(p.words).y; + const double t = (plane_x - p.cam_origin.x) / p.dir.x; + return p.cam_origin.y + p.dir.y * t; +} + + static bool handle_WallHit(Projectile& p, const Vec3& centroid, Wall::Position hit_pos, double dt,double ws, @@ -324,9 +337,15 @@ handle_WallHit(Projectile& p, const Vec3& centroid, Wall::Position hit_pos, return bounce_OffWall(p, hit_pos, ws, results); } + const bool editor = (Wall::role_of(hit_pos) == Wall::Role::Editor); + const double aim_y = editor + ? editor_AimY(p, wall_plane_coord(hit_pos, ws)) + : 0.0; + size_t hit_idx = 0; if (p.words.size() == 1 && - check_WallTextIntercept(p, centroid, dt, hit_pos, wall_text_hits, hit_idx)) { + check_WallTextIntercept(p, centroid, dt, hit_pos, wall_text_hits, + aim_y, editor, hit_idx)) { ProjectileResult r; r.landing = ProjectileLanding::WallText; r.wall_pos = hit_pos; diff --git a/src/systems/projectile_system.hpp b/src/systems/projectile_system.hpp index 45963f1..cee9fc1 100644 --- a/src/systems/projectile_system.hpp +++ b/src/systems/projectile_system.hpp @@ -12,54 +12,61 @@ static constexpr uint8_t BOUNCED_X = 1 << 0; static constexpr uint8_t BOUNCED_Z = 1 << 1; struct Projectile { + Wall::Position + stick_wall { Wall::Position::PosX }; + bool sticking = false; + bool collided = false; + bool program_stick = false; + uint8_t bounce_state = 0; + float ribbon_start_yaw {}; + float ribbon_target_yaw {}; + double yaw; + double stick_t = 0.0; + double step_dt = 0.0; + Text3d* track_obj = nullptr; + + Vec3 vel{}; + Vec3 dir{}; + Vec3 prev_centroid{}; + Vec3 launch_origin{}; + Vec3 cam_origin {}; + + Vec3 track_local{}; + Vec3 track_last {}; + + + Vec3 ribbon_origin {}; + Vec3 ribbon_start_origin {}; + Vec3 ribbon_target_origin{}; + DynArray<Text3dPtr> words; - Vec3 vel{}; - 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; - 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; - - 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 {}; - Vec3 ribbon_target_origin {}; - float ribbon_start_yaw {}; - float ribbon_target_yaw {}; }; struct WallTextHitTest { - Wall::Position wall_position; - Vec3 aabb_min; - Vec3 aabb_max; - size_t index; + Wall::Position + wall_position; + size_t index; + + Vec3 aabb_min; + Vec3 aabb_max; }; enum class ProjectileLanding : uint8_t { - None, Floor, + None, + ProgramInput, Wall, WallText, - ProgramInput, }; struct ProjectileResult { - ProjectileLanding landing = ProjectileLanding::None; - Wall::Position wall_pos = Wall::Position::NegZ; - size_t wall_text_index = 0; + Wall::Position + wall_pos = Wall::Position::NegZ; + size_t wall_text_index = 0; + Vec3 vel{}; + + ProjectileLanding landing = ProjectileLanding::None; DynArray<Text3dPtr> words; - Vec3 vel{}; }; diff --git a/src/systems/wall_connection.hpp b/src/systems/wall_connection.hpp index 5735810..7a1b6d4 100644 --- a/src/systems/wall_connection.hpp +++ b/src/systems/wall_connection.hpp @@ -37,22 +37,27 @@ public: void render(GlRenderer& gl, const Camera& cam, const Light& light) const; private: - Target _target { Target::None }; - Vec3 _pult_pos {}; + unsigned _vim_seen_version { 0 }; + + double _row0_y{ 0.0 }; + double _eye_y { 0.0 }; double _blink { 0.0 }; + double _code_left_z { 0.0 }; + + Target _target{ Target::None }; + + Text3dPtr _cmdline; + Text3dPtr _prog_input; + + Vec3 _pult_pos{}; - Text3dPtr _prog_input; std::string _prog_last; - VimBuffer _vim; - unsigned _vim_seen_version { 0 }; + VimBuffer _vim; + DynArray<Text3dPtr> _render; DynArray<Text3dPtr> _linenums; - Text3dPtr _cmdline; DynArray<std::string> _status; - double _row0_y { 0.0 }; - double _code_left_z { 0.0 }; - double _eye_y { 0.0 }; void rebuild_Editor (); void rebuild_Cmdline(); diff --git a/src/systems/word_ai_system.hpp b/src/systems/word_ai_system.hpp index 3eed820..a6de80f 100644 --- a/src/systems/word_ai_system.hpp +++ b/src/systems/word_ai_system.hpp @@ -11,27 +11,27 @@ class WordAiSystem { int _max_active_ai = 5; - void update_Idle (Text3d& w, const Player& p, int active_ai_count, double dt); - void update_Attack (Text3d& w, const Player& p, CarriedLine& carried, - DynArray<Text3dPtr>& all_words, - DynArray<Text3dPtr>& dropped, - DynArray<Text3dPtr>& released, - double dt); - void update_Flee (Text3d& w, const Player& p, - DynArray<Text3dPtr>& released, double dt); - void update_Stunned (Text3d& w, DynArray<Text3dPtr>& released, double dt); + void update_Idle (Text3d& w, const Player& p, int active_ai_count, double dt); + void update_Attack (Text3d& w, const Player& p, CarriedLine& carried, + DynArray<Text3dPtr>& all_words, + DynArray<Text3dPtr>& dropped, + DynArray<Text3dPtr>& released, + double dt); + void update_Flee (Text3d& w, const Player& p, + DynArray<Text3dPtr>& released, double dt); + void update_Stunned(Text3d& w, DynArray<Text3dPtr>& released, double dt); void update_StolenTrail(Text3d& w, double dt); void release_StolenWords(Text3d& w, DynArray<Text3dPtr>& out); - void enter_Attack (Text3d& w); - void enter_Flee (Text3d& w); - void enter_Stunned (Text3d& w); - void enter_Idle (Text3d& w); + void enter_Attack (Text3d& w); + void enter_Flee (Text3d& w); + void enter_Stunned (Text3d& w); + void enter_Idle (Text3d& w); void apply_Locomotion(Text3d& w, const Vec3& target_vel, double dt); public: - void set_MaxActiveAi(int n) { _max_active_ai = n; } + void set_MaxActiveAi (int n) { _max_active_ai = n; } void update(DynArray<Text3dPtr>& words, const Player& player, CarriedLine& carried, diff --git a/src/systems/word_anim.hpp b/src/systems/word_anim.hpp index c3242b1..fa10fde 100644 --- a/src/systems/word_anim.hpp +++ b/src/systems/word_anim.hpp @@ -5,20 +5,23 @@ #include "utils/globals.hpp" struct WordAnim_Add { + bool complete{false}; + double yaw_start; + double yaw_target; + double pitch_start {0.0}; + double pitch_target {0.0}; + double t{0.0}; + Text3dPtr word; - Vec3 start_pos; - Vec3 target_pos; - double yaw_start; - double yaw_target; - double pitch_start {0.0}; - double pitch_target {0.0}; - double t{0.0}; - bool complete{false}; + + Vec3 start_pos; + Vec3 target_pos; }; struct WordAnim_Drop { + bool complete{false}; + Text3dPtr word; - bool complete{false}; }; class WordAnimSystem { diff --git a/src/text_3d/text_label.hpp b/src/text_3d/text_label.hpp index ffddd34..1f14ef1 100644 --- a/src/text_3d/text_label.hpp +++ b/src/text_3d/text_label.hpp @@ -15,26 +15,28 @@ class TextLabel { Anchor, }; + bool _collidable { true }; + bool _billboard { true }; + + float _style_depth{ 0.06f }; + float _style_scale{ 0.003f }; + float _face_yaw { 0.0f }; + SDL_FColor _style_color{ 1.0f, 1.0f, 0.0f, 1.0f }; + + const Vec3* _anchor_ptr { nullptr }; + Vec3 _centre {}; + Vec3 _attach_offset{}; + + std::string _current_text; + + PositionMode _mode { PositionMode::Absolute }; + DynArray<Text3dPtr> _words; DynArray<double> _offsets; DynArray<Vec3> _anchor_pos; DynArray<Vec3> _jiggle_off; DynArray<Vec3> _jiggle_vel; - PositionMode _mode { PositionMode::Absolute }; - Vec3 _centre {}; - const Vec3* _anchor_ptr { nullptr }; - Vec3 _attach_offset {}; - - bool _billboard { true }; - float _face_yaw { 0.0f }; - - SDL_FColor _style_color { 1.0f, 1.0f, 0.0f, 1.0f }; - float _style_depth { 0.06f }; - float _style_scale { 0.003f }; - - bool _collidable { true }; - std::string _current_text; void rebuild(const std::string& text); Vec3 current_Centre() const; @@ -42,22 +44,22 @@ class TextLabel { public: TextLabel() = default; - void set_Text(const std::string& text); - void set_Text(const std::string& text, - SDL_FColor color, float depth, float scale); - void clear(); - void set_Centre(const Vec3& p); - void attach_To(const Vec3* anchor, const Vec3& offset = {}); - void detach(); - void set_Billboard(bool on) { _billboard = on; } - void set_FaceYaw(float y) { _face_yaw = y; } - void set_Style(SDL_FColor color, float depth, float scale); + void attach_To (const Vec3* anchor, const Vec3& offset = {}); + void detach (); + + void set_Centre (const Vec3& p); + void set_Billboard (bool on) { _billboard = on; } + void set_FaceYaw (float y) { _face_yaw = y; } + void set_Style (SDL_FColor color, float depth, float scale); void set_Collidable(bool on); - void update(double dt, const Camera& camera); - void draw(GlRenderer& gl, const Camera& camera, const Light& light) const; + void set_Text (const std::string& text); + void set_Text (const std::string& text, + SDL_FColor color, float depth, float scale); + void clear (); + void update (double dt, const Camera& camera); + void draw (GlRenderer& gl, const Camera& camera, const Light& light) const; + void kick (size_t word_index, const Vec3& projectile_vel, const Vec3& normal); + bool empty () const { return _words.empty(); } const DynArray<Text3dPtr>& - words() const { return _words; } - void kick(size_t word_index, const Vec3& projectile_vel, const Vec3& normal); - - bool empty() const { return _words.empty(); } + words () const { return _words; } }; diff --git a/src/utils/carry_math.hpp b/src/utils/carry_math.hpp index fbeee20..2fb88c9 100644 --- a/src/utils/carry_math.hpp +++ b/src/utils/carry_math.hpp @@ -13,17 +13,14 @@ 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 + static constexpr double CARRY_HAND_RIGHT = 0.45; + static constexpr double CARRY_HAND_DOWN = 0.35; - Vec3 line_Anchor(const Camera& camera, double carry_dist); - - DynArray<Vec3> slot_Positions(const CarriedLine& carried, - const Vec3& anchor, - const Vec3& right, - double carry_gap); + Vec3 line_Anchor(const Camera& camera, double carry_dist); + DynArray<Vec3> + slot_Positions(const CarriedLine& carried, + const Vec3& anchor, + const Vec3& right, + double carry_gap); } diff --git a/src/utils/config.hpp b/src/utils/config.hpp index 181ef0b..07604b0 100644 --- a/src/utils/config.hpp +++ b/src/utils/config.hpp @@ -129,6 +129,7 @@ static constexpr double underline_thick = 0.005; // half-height of the underlin static constexpr double line_number_gap = 1.2; // clear space between code and the numbers static constexpr float line_number_scale = 0.003f; static constexpr double bounce_impulse = 2.2; // upward kick when a number is shot +static constexpr double merge_top_margin = 0.20; // how much space to the top to merge with current line } diff --git a/src/utils/frame_profiler.hpp b/src/utils/frame_profiler.hpp index 9e2c598..8ef699e 100644 --- a/src/utils/frame_profiler.hpp +++ b/src/utils/frame_profiler.hpp @@ -46,20 +46,22 @@ inline double perf_to_ms(Uint64 ticks) { struct Zone { const char* name {nullptr}; + uint32_t calls {0}; // calls across the report window Uint64 total {0}; // accumulated ticks across the report window Uint64 frame_sum {0}; // ticks this frame (so we can also track peak) Uint64 peak {0}; // peak per-frame ticks in window - uint32_t calls {0}; // calls across the report window }; struct Frame { static constexpr size_t MAX_ZONES = 32; - std::array<Zone, MAX_ZONES> zones{}; + bool started {false}; size_t num_zones {0}; uint32_t frames_in_win {0}; + Uint64 window_start {0}; // perf-counter ticks Uint64 report_every_ns{1'000'000'000ull}; // 1 second - bool started {false}; + + std::array<Zone, MAX_ZONES> zones{}; Zone* find_or_add(const char* name) { // Pointer-equality lookup — string literals dedupe naturally. diff --git a/src/utils/math.hpp b/src/utils/math.hpp index 0534458..4171cb9 100644 --- a/src/utils/math.hpp +++ b/src/utils/math.hpp @@ -26,10 +26,14 @@ struct Vec3 { Vec3() = default; Vec3(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {} - Vec3& operator+=(const Vec3& other) { x += other.x; y += other.y; z += other.z; return *this; } - Vec3& operator-=(const Vec3& other) { x -= other.x; y -= other.y; z -= other.z; return *this; } - Vec3& operator*=(double s) { x *= s; y *= s; z *= s; return *this; } - Vec3& operator/=(double s) { x /= s; y /= s; z /= s; return *this; } + Vec3& operator+=(const Vec3& other) + { x += other.x; y += other.y; z += other.z; return *this; } + Vec3& operator-=(const Vec3& other) + { x -= other.x; y -= other.y; z -= other.z; return *this; } + Vec3& operator*=(double s) + { x *= s; y *= s; z *= s; return *this; } + Vec3& operator/=(double s) + { x /= s; y /= s; z /= s; return *this; } }; inline Vec3 operator+(Vec3 a, const Vec3& b) { return a += b; } diff --git a/src/wall/editor_wall.cpp b/src/wall/editor_wall.cpp index 7bc8d03..088c156 100644 --- a/src/wall/editor_wall.cpp +++ b/src/wall/editor_wall.cpp @@ -30,11 +30,118 @@ EditorWall::find_NearestLine(double y) } +EditorWall::Target +EditorWall::resolve_Target(double y) const +{ + DynArray<size_t> order; + for (size_t i = 0; i < _lines.size(); ++i) + if (!_lines[i].words.empty()) order.push_back(i); + + if (order.empty()) + return { Target::Insert, 0, y }; + + std::sort(order.begin(), order.end(), + [&](size_t a, size_t b){ return _lines[a].y > _lines[b].y; }); + + auto glyph_top = [&](size_t li) { + return _lines[li].y + + (double)_lines[li].words.front()->half_extent_y + + ConfigWall::merge_top_margin; + }; + auto underline = [&](size_t li) { + return _lines[li].y - ConfigWall::underline_drop; + }; + + // 1) Merge: aim within any line's glyph band [underline .. glyph top]. + for (size_t li : order) + if (y <= glyph_top(li) && y >= underline(li)) + return { Target::Merge, li, 0.0 }; + + const double LH = ConfigWall::line_height; + + // 2) Above the top line's glyph → new row at the aim height, but never + // closer than one LH to the current top (keep whatever wider gap the + // shot asked for). + { + const size_t top = order.front(); + if (y > glyph_top(top)) { + const double min_y = _lines[top].y + LH; // closest allowed + const double new_y = (y > min_y) ? y : min_y; // keep the wide gap if aimed higher + return { Target::Insert, top, new_y, 0.0 }; + } + } + + // 3) Between two lines. Decide by where in the gap the aim falls: + // - enough room and aimed in the middle → place AT the aim (no push) + // - too close to the upper line → snap one row under it + // - too close to the lower line → snap one row above it (push lo if needed) + for (size_t oi = 0; oi + 1 < order.size(); ++oi) { + const size_t hi = order[oi]; + const size_t lo = order[oi + 1]; + if (y < underline(hi) && y > glyph_top(lo)) { + const double hi_y = _lines[hi].y; + const double lo_y = _lines[lo].y; + const double gap = hi_y - lo_y; + + const double free_top = hi_y - LH; + const double free_bot = lo_y + LH; + + // Wide gap, aimed in the open middle → place exactly at the aim, + // push nothing (there's room on both sides). + if (gap >= 2.0 * LH && y <= free_top && y >= free_bot) + return { Target::Insert, lo, y, 0.0 }; + + // Wide gap but aimed near an edge → snap to that edge, no push + // (the gap already has room, we just don't free-float there). + if (gap >= 2.0 * LH) + return { Target::Insert, lo, (y > free_top) ? free_top : free_bot, 0.0 }; + + // Tight gap (< 2*LH): only one placement fits. Sit one row under hi + // and push lo+below down by the deficit to restore LH spacing. + const double deficit = 2.0 * LH - gap; + return { Target::Insert, lo, free_top, deficit }; + } + } + + // 4) Below the bottom line → new row at the aim height, but never closer + // than one LH below the current bottom. + { + const size_t bot = order.back(); + const double max_y = _lines[bot].y - LH; // closest allowed (further down = smaller y) + const double new_y = (y < max_y) ? y : max_y; // keep the wide gap if aimed lower + return { Target::Insert, bot, new_y, 0.0 }; + } +} + + +EditorWall::Line& +EditorWall::apply_Target(const Target& t) +{ + if (t.kind == Target::Merge) + return _lines[t.line_index]; + + if (t.push_dist > 0.0 && t.line_index < _lines.size()) { + const double push_below_y = _lines[t.line_index].y; + for (Line& l : _lines) { + if (l.y <= push_below_y + 1e-6) { + l.y -= t.push_dist; + for (Text3d* w : l.words) if (w) w->pos.y -= t.push_dist; + } + } + } + + Line fresh; + fresh.y = t.new_y; + _lines.push_back(std::move(fresh)); + return _lines.back(); +} + + EditorWall::Placement EditorWall::place_OnLine(Line& line, double new_half_z, double impact_z) { if (line.words.empty()) - return { impact_z, 0 }; + return { 0, impact_z }; const size_t n = line.words.size(); auto left_edge = [](const Text3d* w){ return w->pos.z + (double)w->half_extent; }; @@ -48,7 +155,7 @@ EditorWall::place_OnLine(Line& line, double new_half_z, double impact_z) const double flush_z = left_edge(leftmost) + ConfigWall::free_space_dist + new_half_z; // Aimed further LEFT than flush (larger z)? keep it. Else snap flush. const double z = (impact_z > flush_z) ? impact_z : flush_z; - return { z, 0 }; // front + return { 0, z }; // front } // Case B: right of the whole line — always flush. @@ -57,7 +164,7 @@ EditorWall::place_OnLine(Line& line, double new_half_z, double impact_z) // Aimed further RIGHT than flush (smaller z)? keep it. Else snap flush. // Never allow larger-than-flush z (that would overlap the neighbour). const double z = (impact_z < flush_z) ? impact_z : flush_z; - return { z, n }; + return { n, z }; } // Case C: between two words. @@ -68,14 +175,14 @@ EditorWall::place_OnLine(Line& line, double new_half_z, double impact_z) const double needed = 2.0 * new_half_z + 2.0 * ConfigWall::free_space_dist; const double avail = gap_left - gap_right; if (avail >= needed) - return { (gap_left + gap_right) * 0.5, i + 1 }; + return { i + 1, (gap_left + gap_right) * 0.5 }; const double shift = needed - avail; for (size_t k = i + 1; k < n; ++k) line.words[k]->pos.z -= shift; - return { gap_left - ConfigWall::free_space_dist - new_half_z, i + 1 }; + return { i + 1, gap_left - ConfigWall::free_space_dist - new_half_z }; } } - return { right_edge(rightmost) - ConfigWall::free_space_dist - new_half_z, n }; + return { n, right_edge(rightmost) - ConfigWall::free_space_dist - new_half_z }; } @@ -84,21 +191,16 @@ EditorWall::on_WordPlaced(Text3d* word, const Vec3& impact_pos) { if (!word) return impact_pos; - Line* target = find_NearestLine(impact_pos.y); - if (!target) { - Line fresh; - fresh.y = impact_pos.y; - _lines.push_back(std::move(fresh)); - target = &_lines.back(); - } + Target t = resolve_Target(impact_pos.y); + Line& target = apply_Target(t); const double new_half_z = word->half_extent; - const Placement p = place_OnLine(*target, new_half_z, impact_pos.z); + const Placement p = place_OnLine(target, new_half_z, impact_pos.z); - Vec3 snapped { impact_pos.x, target->y, p.z }; + Vec3 snapped { impact_pos.x, target.y, p.z }; - target->words.insert( - target->words.begin() + static_cast<std::ptrdiff_t>(p.insert_index), + target.words.insert( + target.words.begin() + static_cast<std::ptrdiff_t>(p.insert_index), word); return snapped; @@ -125,13 +227,9 @@ EditorWall::on_LinePlaced(const DynArray<Text3d*>& words_in, const Vec3& impact) if (i + 1 < ws.size()) block_w += ConfigWall::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(); - } + Target t = resolve_Target(impact.y); + Line& targetR = apply_Target(t); + Line* target = &targetR; 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; }; @@ -341,9 +439,9 @@ EditorWall::number_Hitboxes() const const double hz = (double)t->half_extent + PAD; // along wall (z) const double hy = (double)t->half_extent_y + PAD; // vertical const Vec3 c = t->pos; - out.push_back({ Vec3{ c.x - DEPTH_X, c.y - hy, c.z - hz }, - Vec3{ c.x + DEPTH_X, c.y + hy, c.z + hz }, - HIT_BASE + i }); + out.push_back({ HIT_BASE + i, + Vec3{ c.x - DEPTH_X, c.y - hy, c.z - hz }, + Vec3{ c.x + DEPTH_X, c.y + hy, c.z + hz } }); }; for (size_t i = 0; i < _lines.size(); ++i) { @@ -445,3 +543,37 @@ EditorWall::draw(GlRenderer& gl, const Camera& cam, const Light& light) const if (l.num_right) l.num_right->draw(gl, cam, light); } } + + +double +EditorWall::get_GridOrigin() const +{ + double origin = 0.0; + bool found = false; + for (const Line& l : _lines) { + if (l.words.empty()) continue; + if (!found || l.y > origin) { origin = l.y; found = true; } + } + return found ? origin : 0.0; +} + +double +EditorWall::snap_RowY(double y) const +{ + const double LH = ConfigWall::line_height; + const double origin = get_GridOrigin(); + const double row = std::round((y - origin) / LH); + return origin + row * LH; +} + +bool +EditorWall::row_IsEmpty(double y) const +{ + const double LH = ConfigWall::line_height; + const double slot_y = snap_RowY(y); + for (const Line& l : _lines) { + if (l.words.empty()) continue; + if (SDL_fabs(l.y - slot_y) < LH * 0.5) return false; + } + return true; +} diff --git a/src/wall/editor_wall.hpp b/src/wall/editor_wall.hpp index 820b056..bb88ab1 100644 --- a/src/wall/editor_wall.hpp +++ b/src/wall/editor_wall.hpp @@ -10,108 +10,66 @@ class Camera; class GlRenderer; -// EditorWall: tracks words placed on the editor (NegX) wall, organizes -// them into horizontal lines with snap-to-row positioning, and renders -// thin underline guides below each populated row. -// -// Layout convention: editor wall is at -X. Wall surface is the YZ plane, -// so: -// pos.y → vertical position (which row) -// pos.z → horizontal position along that row -// pos.x → fixed at -world_scale + small offset -// -// Snap rules: -// - Throws within ConfigWall::snap_threshold_y of an existing line's y → snap to it -// - Throws outside threshold → start a new line at the throw's y -// - Words on a line append to the right of the rightmost word with -// a fixed horizontal gap (ConfigWall::word_gap). -// -// All thresholds expressed in world units. Tunable below. class EditorWall { struct Line { - double y {0.0}; - DynArray<Text3d*> words; // left-to-right + int shown_number {-1}; + double y {0.0}; + double bounce_off {0.0}; + double bounce_vel {0.0}; - // Owned line-number labels, one each end. Deliberately NOT stored in - // wall_texts: that keeps them out of source extraction and means the - // shoot-down path can never make them fall off the wall. Text3dPtr num_left; Text3dPtr num_right; - int shown_number {-1}; // currently rendered digit(s) - double bounce_off {0.0}; // transient vertical jiggle - double bounce_vel {0.0}; + + DynArray<Text3d*> words; // left-to-right }; - // Result of placing a word: the final Z, and the index in the line's - // word vector where it should be inserted (visual left→right order). struct Placement { - double z; size_t insert_index; + double z; + }; + + struct Target { + enum Kind { Merge, Insert } kind; + size_t line_index; + double new_y; + double push_dist {0.0}; }; DynArray<Line> _lines; - Line* find_NearestLine(double y); - // Decide where the new word lands on the line and shift neighbours if a - // between-words insert needs room. Mutates the line's existing word - // positions when a shift is required. - Placement place_OnLine(Line& line, double new_half_z, double impact_z); + Line* find_NearestLine (double y); + Placement place_OnLine (Line& line, double new_half_z, double impact_z); + double get_GridOrigin () const; + Target resolve_Target (double y) const; + Line& apply_Target (const Target& t); public: - // ── World-tuning constants (public, edit to taste) ─────────────────── - // Hit-id encoding: GameState mixes these into its wall-text hit list, and - // tells them apart on the way back. (size_t is 64-bit on the target.) - static constexpr size_t HIT_BASE = size_t(1) << 32; + static constexpr size_t + HIT_BASE = size_t(1) << 32; static bool is_LineNumberHit(size_t idx) { return idx >= HIT_BASE; } - static size_t line_Of (size_t idx) { return idx - HIT_BASE; } + static size_t line_Of (size_t idx) { return idx - HIT_BASE; } struct NumberHit { + size_t index; Vec3 aabb_min; Vec3 aabb_max; - size_t index; // HIT_BASE + line id }; - // ── State management ──────────────────────────────────────────────── - - // Reset everything. Call on scene teardown. - void clear(); - - // Per-frame: renumber rows top→bottom, (re)create/position the orange - // number labels, tilt them toward the eye, and advance bounce springs. - void update(double dt, const Vec3& eye_pos); - - // Register a word that just hit the editor wall. May modify word->pos - // to snap it (current behavior: snap is computed BEFORE the word is - // visible; smooth animation is a later step). - // - // 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); - - // Render underlines for every populated row. - void draw(GlRenderer& gl, const Camera& cam, const Light& light) const; - - // Diagnostics. - size_t line_Count() const { return _lines.size(); } + void clear (); + void update (double dt, const Vec3& eye_pos); + void draw (GlRenderer& gl, const Camera& cam, const Light& light) const; void scroll(double delta) { for (Line& l : _lines) l.y += delta; } + void on_LinePlaced (const DynArray<Text3d*>& words, const Vec3& impact); + void on_WordRemoved(Text3d* word); + Vec3 on_WordPlaced (Text3d* word, const Vec3& impact_pos); - // ── Line-number shooting ───────────────────────────────────────────── - // AABBs for every number label, tagged with an encoded hit id, for - // GameState to append to its projectile wall-text hit list. - DynArray<NumberHit> number_Hitboxes() const; - // World-Y shift needed to bring the given line to eye height. + size_t line_Count () const { return _lines.size(); } + bool row_IsEmpty(double y) const; + double snap_RowY (double y) const; double scroll_DeltaToEye(size_t line_id, double eye_y) const; - // Kick a line's numbers so they bounce in place (does not move the line). - void bounce(size_t line_id); + void bounce (size_t line_id); + + DynArray<NumberHit> + number_Hitboxes() const; }; diff --git a/src/wall/text_wall.hpp b/src/wall/text_wall.hpp index 2f84c38..a359ca6 100644 --- a/src/wall/text_wall.hpp +++ b/src/wall/text_wall.hpp @@ -13,16 +13,11 @@ struct TextWall Wall::Role role; Text3dPtr text3d; + void draw (GlRenderer& gl, const Camera& camera, const Light& light) const; static TextWallPtr - from_Drop(Text3dPtr src); - + from_Drop (Text3dPtr src); static std::string - extract_Text(const TextWallList&, Wall::Role, double line_threshold = 0.3); - - void - draw(GlRenderer& gl, const Camera& camera, const Light& light) const; - + extract_Text (const TextWallList&, Wall::Role, double line_threshold = 0.3); static void - show_TokenInfo(TextWallList& texts, - const std::string& token_text); + show_TokenInfo(TextWallList& texts, const std::string& token_text); }; diff --git a/src/wall/wall_label.hpp b/src/wall/wall_label.hpp index 8d5a994..96af1bc 100644 --- a/src/wall/wall_label.hpp +++ b/src/wall/wall_label.hpp @@ -8,37 +8,22 @@ 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 + Wall::Role role; + float face_yaw; + Vec3 anchor; + TextLabel label; - // 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; + void on_Hit (size_t word_index, const Vec3& projectile_vel); + 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) { diff --git a/src/wall/wall_surfaces.hpp b/src/wall/wall_surfaces.hpp index 6150a17..100a25d 100644 --- a/src/wall/wall_surfaces.hpp +++ b/src/wall/wall_surfaces.hpp @@ -11,24 +11,16 @@ class Camera; -// WallSurfaces: four solid colored quads, one per wall, each tinted to its -// role's purpose. The editor wall additionally renders horizontal "paper -// rule" lines via the shader's wall-pattern path. -// -// Layout per wall: -// Editor (-X) cream + horizontal blue rules "writing surface" -// Program (+X) warm ivory "spec / document" -// Info (+Z) soft slate blue "info card" -// Compiler (-Z) near-black indigo "terminal" class WallSurfaces { public: struct WallQuad { + bool is_editor; Wall::Role role; + SDL_FColor color; + DynArray<float> verts; DynArray<int> indices; - SDL_FColor color; - bool is_editor; }; void generate(float world_scale);