sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzCleanup
benchmark/bench_ai.cpp | 1 - src/c_token/token_db.cpp | 6 +- src/entities/camera.hpp | 29 +--------- src/entities/game_object.hpp | 30 ++++++---- src/entities/plate.cpp | 29 ++++------ src/entities/plate.hpp | 17 +++--- src/entities/player.cpp | 7 --- src/entities/player.hpp | 1 - src/levels/level_check.cpp | 3 +- src/states/game_state.cpp | 93 +++++++++++++++++------------ src/states/menu_state.cpp | 86 +++++++-------------------- src/states/menu_state.hpp | 4 +- src/systems/carry_system.cpp | 12 ++-- src/systems/collision.cpp | 22 +++---- src/systems/input_system.cpp | 17 ++++-- src/systems/physics_system.cpp | 116 +++++++++++++++---------------------- src/systems/pick_system.cpp | 44 +++++--------- src/systems/projectile_system.cpp | 104 ++++++++++----------------------- src/systems/source_tokens.hpp | 19 ++---- src/systems/wall_connection.cpp | 9 ++- src/systems/word_ai_system.cpp | 119 ++++++++++++++++---------------------- src/systems/word_ai_system.hpp | 2 - src/systems/word_anim.cpp | 5 +- src/text_3d/text_3d.cpp | 3 +- src/text_3d/text_label.cpp | 49 ++++------------ src/text_3d/text_mesh.cpp | 3 +- src/text_3d/vim_buffer.hpp | 66 ++++++++++----------- src/utils/carry_math.cpp | 14 ++--- src/utils/carry_math.hpp | 2 +- src/utils/math.hpp | 4 ++ src/utils/text_wrap.hpp | 6 +- 31 files changed, 369 insertions(+), 553 deletions(-) diff --git a/benchmark/bench_ai.cpp b/benchmark/bench_ai.cpp index 9aa68a3..8d8c6fc 100644 --- a/benchmark/bench_ai.cpp +++ b/benchmark/bench_ai.cpp @@ -27,7 +27,6 @@ static void BM_WordAiUpdate(benchmark::State& state) } WordAiSystem ai; - ai.set_WorldScale(16.0f); std::vector<Text3dPtr> dropped; constexpr double dt = 1.0 / 60.0; diff --git a/src/c_token/token_db.cpp b/src/c_token/token_db.cpp index 14446c5..1a0b403 100644 --- a/src/c_token/token_db.cpp +++ b/src/c_token/token_db.cpp @@ -307,14 +307,16 @@ synth_number(const std::string& s) t = { s, TokenType::Literal, "Hex literal", "Unsigned hexadecimal constant" }; else t = { s, TokenType::Literal, "Hex literal", "Hexadecimal integer constant" }; - } else if (is_fp) { + } + else if (is_fp) { if (has_f_suffix) t = { s, TokenType::Literal, "Float literal", "Single-precision floating-point (float)" }; else if (is_long) t = { s, TokenType::Literal, "Long double literal", "Extended-precision floating-point (long double)" }; else t = { s, TokenType::Literal, "Double literal", "Double-precision floating-point (double)" }; - } else { + } + else { if (is_unsigned && is_long) t = { s, TokenType::Literal, "Unsigned long literal","Unsigned long integer constant" }; else if (is_unsigned) diff --git a/src/entities/camera.hpp b/src/entities/camera.hpp index e32de85..fae6d2d 100644 --- a/src/entities/camera.hpp +++ b/src/entities/camera.hpp @@ -15,9 +15,9 @@ struct Camera { Vec3 fwd {}; Vec3 right {}; - void syncFromPlayer(const Player& player, double alpha) + void syncFromPlayer(const Player& player) { - pos = player.interpolated_Pos(alpha); + pos = player.prev_pos; yaw = player.yaw; pitch = player.pitch; @@ -35,28 +35,3 @@ struct Camera { }; } }; - -inline void buildViewMatrix(const Camera& cam, double out[9]) -{ - double bin = ConfigPhysics::binrad; - - double yaw = bin * cam.yaw; - double pitch = bin * cam.pitch; - - double cy = SDL_cos(yaw); - double sy = SDL_sin(yaw); - double cp = SDL_cos(pitch); - double sp = SDL_sin(pitch); - - out[0] = cy; - out[1] = 0; - out[2] = -sy; - - out[3] = sy * sp; - out[4] = cp; - out[5] = cy * sp; - - out[6] = sy * cp; - out[7] = -sp; - out[8] = cy * cp; -} diff --git a/src/entities/game_object.hpp b/src/entities/game_object.hpp index cfaa17c..8047f80 100644 --- a/src/entities/game_object.hpp +++ b/src/entities/game_object.hpp @@ -5,14 +5,16 @@ #include "utils/light.hpp" #include "utils/math.hpp" -class GlRenderer; class Camera; +class GlRenderer; + enum class ObjectType : uint8_t { Text, Prop, }; + enum class PhysicsMode : uint8_t { Static, Dynamic, @@ -22,27 +24,29 @@ enum class PhysicsMode : uint8_t { Projectile, }; + struct CollisionShape { Vec3 half_extents{0.5, 0.5, 0.5}; }; + struct RigidBody { - Vec3 vel{}; - Vec3 force{}; - double mass{1.0}; + Vec3 vel {}; + Vec3 force {}; + double mass {1.0}; double restitution{0.3}; - double friction{0.8}; + double friction {0.8}; PhysicsMode mode{PhysicsMode::Hover}; - double hover_phase{0.0}; - double hover_height{0.0}; + 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 + RigidBody(PhysicsMode m = PhysicsMode::Hover, + double mass_ = 1.0, + double restitution_ = 0.3, + double friction_ = 0.8 ) : mass(mass_), restitution(restitution_), friction(friction_), @@ -59,7 +63,8 @@ struct RigidBody { mode == PhysicsMode::AiHover) { mode = PhysicsMode::Dynamic; vel = impulse; - } else if (mode == PhysicsMode::Dynamic) { + } + else if (mode == PhysicsMode::Dynamic) { vel += impulse; } } @@ -73,6 +78,7 @@ struct RigidBody { } }; + struct GameObject { Vec3 pos {}; float yaw {0.0f}; diff --git a/src/entities/plate.cpp b/src/entities/plate.cpp index 5af01d6..091fc04 100644 --- a/src/entities/plate.cpp +++ b/src/entities/plate.cpp @@ -7,11 +7,9 @@ void Plate::tick(double dt) { - // Subtle bob — drifts up and down ~5cm with a slow sine bob_phase += dt * 2.0; pos.y = base_pos.y + SDL_sin(bob_phase) * 0.05; - // Pulse decays over time (set to 1.0 from outside on activation) if (pulse > 0.0) { pulse -= dt * 1.5; if (pulse < 0.0) pulse = 0.0; @@ -32,8 +30,6 @@ Plate::draw(GlRenderer& gl, const Camera& cam, const Light& light) const Mat4 model = buildModelMat4(pos, yaw); Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model)); - // Brightness: active plates glow fully, inactive are dimmer. - // pulse adds a momentary flare on activation. float boost = static_cast<float>(active ? 1.0 : 0.6) + static_cast<float>(pulse) * 0.4f; if (boost > 1.0f) boost = 1.0f; @@ -42,20 +38,17 @@ Plate::draw(GlRenderer& gl, const Camera& cam, const Light& light) const const float eg = fill_color.g * boost; const float eb = fill_color.b * boost; - // Use the emissive path so plates self-illuminate regardless of scene - // lighting and feed into the bloom pass — no light state mutation needed. gl.set_Wireframe(false); - gl.set_MVP(model, mvp); + gl.set_MVP (model, mvp); gl.set_NormalMat(model); - gl.set_Light(light); - gl.set_Color(er, eg, eb); - gl.set_Emissive(er, eg, eb, boost); - gl.draw_Box(hx, hy, hz); - gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f); - - // Wireframe outline - gl.set_Wireframe(true); - gl.set_MVP(model, mvp); - gl.set_Color(wire_color.r, wire_color.g, wire_color.b); - gl.draw_Box(hx, hy, hz); + gl.set_Light (light); + gl.set_Color (er, eg, eb); + gl.set_Emissive (er, eg, eb, boost); + gl.draw_Box (hx, hy, hz); + gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f); + + gl.set_Wireframe(false); + gl.set_MVP (model, mvp); + gl.set_Color (wire_color.r, wire_color.g, wire_color.b); + gl.draw_Box (hx, hy, hz); } diff --git a/src/entities/plate.hpp b/src/entities/plate.hpp index 1b1b043..445e136 100644 --- a/src/entities/plate.hpp +++ b/src/entities/plate.hpp @@ -9,28 +9,25 @@ class Camera; class GlRenderer; -// A floor pad the player stands on. Used by the tutorial / level system as -// a "step here to advance" cue. Glowing bright color, bobs subtly, pulses -// when stepped on. struct Plate final : public SolidBox { - static constexpr float _HALF_W = 0.6f; // plate is wide and short + static constexpr float _HALF_W = 0.6f; static constexpr float _HALF_H = 0.05f; static constexpr float _HALF_D = 0.6f; - bool active { false }; // player currently standing on it - bool was_active{ false }; // for edge detection + bool active { false }; + bool was_active{ false }; double bob_phase { 0.0 }; - double pulse { 0.0 }; // 0..1 visual pulse from recent activation - Vec3 base_pos {}; // anchor for bob + double pulse { 0.0 }; + Vec3 base_pos {}; Plate(Vec3 world_pos) : SolidBox( world_pos, Vec3{ _HALF_W, _HALF_H, _HALF_D }, - SDL_FColor{ 0.3f, 1.0f, 0.6f, 1.0f }, // bright cyan-green + SDL_FColor{ 0.3f, 1.0f, 0.6f, 1.0f }, SDL_FColor{ 0.6f, 1.0f, 0.8f, 1.0f }, - false) // not interactable like a pult + false) , base_pos(world_pos) {} diff --git a/src/entities/player.cpp b/src/entities/player.cpp index 1957af0..1eda670 100644 --- a/src/entities/player.cpp +++ b/src/entities/player.cpp @@ -60,10 +60,3 @@ Player::compute_Movement(const FrameInput& input) _acc_z = ConfigPhysics::player_move_accel * (-s*dirX + c*dirZ) * inv; } } - - -Vec3 -Player::interpolated_Pos(double alpha) const -{ - return lerp(prev_pos, pos, alpha); -} diff --git a/src/entities/player.hpp b/src/entities/player.hpp index 9ef1ff8..fcadfef 100644 --- a/src/entities/player.hpp +++ b/src/entities/player.hpp @@ -37,7 +37,6 @@ public: void compute_Movement(const FrameInput& input); void add_Look (int dx, int dy); void apply_Collision (const PlayerCollision& col); - Vec3 interpolated_Pos(double alpha) const; double get_AccX () const { return _acc_x; } double get_AccZ () const { return _acc_z; } void respawn (const Vec3& spawn_pos); diff --git a/src/levels/level_check.cpp b/src/levels/level_check.cpp index b686036..5ee94a5 100644 --- a/src/levels/level_check.cpp +++ b/src/levels/level_check.cpp @@ -16,7 +16,8 @@ normalize(const std::string& s) out += ' '; last_space = true; } - } else { + } + else { out += c; last_space = false; } diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 93dab8d..0a88673 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -43,15 +43,12 @@ GameState::GameState(size_t starting_level) _particle_sys = cpputils::make_unique<ParticleSystem> (); _dyn_loader_sys = cpputils::make_unique<DynLoaderSystem> (); _celebration_sys = cpputils::make_unique<CelebrationSystem>(); + _wall_labels = WallLabel::generate_All(World::get_BoxScale()); + _word_ai_sys = cpputils::make_unique<WordAiSystem>(); _floor_sys = cpputils::make_unique<FloorMandelbrot> (); _floor_sys->generate(World::get_BoxScale(), World::get_FloorY()); - _wall_labels = WallLabel::generate_All(World::get_BoxScale()); - - _word_ai_sys = cpputils::make_unique<WordAiSystem>(); - _word_ai_sys->set_WorldScale(World::get_BoxScale()); - _pult_sys = cpputils::make_unique<PultSystem> (); _pult_sys->init(_ctx.scenery, _ctx.buttons, World::get_BoxScale()); @@ -105,15 +102,13 @@ GameState::carriedline_Update() const FreelookState& fs = _freelook_sys->state; - // Frozen: leave every word exactly where it is. if (fs.active) return; - // Live carry frame in front of the camera. const Vec3 anchor = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST); const Vec3 right = _ctx.camera->right; const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); - DynArray<Vec3> slots = CarryMath::slot_positions(_carried, anchor, right, CarryMath::CARRY_GAP); + DynArray<Vec3> slots = CarryMath::slot_Positions(_carried, anchor, right, CarryMath::CARRY_GAP); DynArray<CarriedWordNode*> nodes = _carried.reverse_nodes(); // Easing back from the frozen spots: lerp each word from its release @@ -133,7 +128,8 @@ GameState::carriedline_Update() nodes[i]->word->pos = { from.x + (target.x - from.x) * s, from.y + (target.y - from.y) * s, from.z + (target.z - from.z) * s }; - } else { + } + else { nodes[i]->word->pos = target; } nodes[i]->word->yaw = static_cast<float>(glyph_yaw); @@ -289,7 +285,7 @@ GameState::on_ShootAll() _carried = CarriedLine(); if (!all_words.empty()) { Aim::Result aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery, - _ctx.wall_texts, World::get_BoxScale()); + _ctx.wall_texts, World::get_BoxScale()); _projectiles_sys->launch(_projectiles, std::move(all_words), _ctx.camera->fwd, _ctx.camera->pos, aim); } @@ -307,12 +303,14 @@ void GameState::on_Compile() { _level_runner_sys->dynamic_lib()) { const std::string path = sandbox_CanonicalPath(); std::string stripped; - for (char c : src) if (!std::isspace((unsigned char)c)) { stripped = src; break; } + for (char c : src) if (!std::isspace((unsigned char)c)) + { stripped = src; break; } if (!path.empty() && !stripped.empty()) SourceTokens::write_File(path, src); _dyn_loader_sys->build(_ctx.wall_texts, src, true); - } else { + } + else { _compiler_sys->compile(_ctx.wall_texts, src); } } @@ -489,11 +487,10 @@ GameState::projectile_LandOnWall(ProjectileResult& result) placed.push_back(std::move(wt)); } - // Insert the whole shot line as ONE block at its centroid (the aim point), - // so it merges between two existing words instead of scattering. if (!editor_group.empty()) { Vec3 impact{0, 0, 0}; - for (Text3d* t : editor_group) { impact.x += t->pos.x; impact.y += t->pos.y; impact.z += t->pos.z; } + for (Text3d* t : editor_group) + { impact.x += t->pos.x; impact.y += t->pos.y; impact.z += t->pos.z; } const double inv = 1.0 / static_cast<double>(editor_group.size()); impact.x *= inv; impact.y *= inv; impact.z *= inv; _editor_wall.on_LinePlaced(editor_group, impact); @@ -519,7 +516,8 @@ GameState::projectile_LandOnWallText(ProjectileResult& result, _editor_wall.bounce(lid); projectile_LandOnFloor(result); - } else { + } + else { walltext_HandleShootDown(result, wall_text_to_erase); } return; @@ -594,13 +592,16 @@ GameState::handle_Input(FrameInput& input, double dt) { editor_CommitToWall(_wall_connection.lines()); _wall_connection.end(); _frame_input.capture_vim = false; - } else if (req == WallConnection::Request::EditorWrite) { + } + else if (req == WallConnection::Request::EditorWrite) { editor_CommitToWall(_wall_connection.lines()); _wall_connection.load_Editor(editor_AbsorbLines(), _ctx.camera->pos.y); } - } else if (_wall_connection.target() == WallConnection::Target::Program) { + } + else if (_wall_connection.target() == WallConnection::Target::Program) { _wall_connection.set_ProgramBuffer(input.typing_buffer); - } else if (input.typing_mode) { + } + else if (input.typing_mode) { input.forward = input.backward = input.left = input.right = false; if (input.typing_buffer != _typing_last) { typing_RebuildPreview(input.typing_buffer); _typing_last = input.typing_buffer; } } @@ -622,7 +623,7 @@ GameState::update_PlayerCamera(FrameInput& input, double dt) _freelook_sys->update(dt); _ctx.player->compute_Movement(input); - _ctx.camera->syncFromPlayer(*_ctx.player, 0.0); + _ctx.camera->syncFromPlayer(*_ctx.player); } void @@ -824,13 +825,12 @@ GameState::infowall_StepdetailsShow() const std::string& details = _level_runner_sys->current().details; if (details.empty()) return; - const float world_scale = World::get_BoxScale(); - Wall::Transform wall_transform = Wall::get_Transform(Wall::Role::Info, world_scale); + Wall::Transform wall_transform = Wall::get_Transform(Wall::Role::Info, World::get_BoxScale()); constexpr size_t MAX_INFO_LINE_CHARS = 92; DynArray<std::string> lines = wrap_Text(details, MAX_INFO_LINE_CHARS); - const float bottom_y = -world_scale + 1.0f; + const float bottom_y = World::get_FloorY() + 1.0f; float y = bottom_y + static_cast<float>(lines.size() - 1) * ConfigWall::text_line_spacing; for (const std::string& line : lines) { @@ -1136,13 +1136,16 @@ GameState::level_Advance() _carried = CarriedLine(); } - DynArray<std::string> tokens = _level_runner_sys->grants_for_step(); + DynArray<std::string> + tokens = _level_runner_sys->grants_for_step(); + step_Spawn(tokens); scene_Setup(_level_runner_sys->current().scene); step_SettingsApply(); infowall_StepdetailsShow(); _hint_sys->set_Hint(_level_runner_sys->current().hint); - } else { + } + else { _hint_sys->clear(); infowall_StepdetailsShow(); } @@ -1155,14 +1158,16 @@ GameState::dyn_Clear() { if (_dyn_scenery.empty()) { return; } - std::unordered_set<GameObject*> set(_dyn_scenery.begin(), _dyn_scenery.end()); + std::unordered_set<GameObject*> + set(_dyn_scenery.begin(), _dyn_scenery.end()); + _ctx.scenery.erase( std::remove_if(_ctx.scenery.begin(), _ctx.scenery.end(), [&set](const GameObjectPtr& sp) { return set.count(sp.get()) > 0; }), _ctx.scenery.end()); _ctx.words.erase( std::remove_if(_ctx.words.begin(), _ctx.words.end(), - [&set](const Text3dPtr& wp) { return set.count(wp.get()) > 0; }), + [&set](const Text3dPtr& wp) { return set.count(wp.get()) > 0; }), _ctx.words.end()); _dyn_scenery.clear(); } @@ -1181,13 +1186,16 @@ GameState::dyn_Apply(const DynArray<DynObject>& objs) Vec3{ o.hx, o.hy, o.hz }, SDL_FColor{ o.r, o.g, o.b, 1.0f }); box->body.mode = o.dynamic ? PhysicsMode::Dynamic : PhysicsMode::Static; + _dyn_scenery.push_back(box.get()); _ctx.scenery.push_back(std::move(box)); - } else { // DYN_TEXT + } + else { // DYN_TEXT Text3dPtr t = make_Text3d(o.text, Text3d::depth_floor, 0.003f, Vec3{ o.x, o.y, o.z }, SDL_FColor{ o.r, o.g, o.b, 1.0f }); t->body.mode = PhysicsMode::Static; // lib text just sits where placed + _dyn_scenery.push_back(t.get()); _ctx.words.push_back(std::move(t)); } @@ -1222,6 +1230,7 @@ GameState::sandbox_LoadFileToWall() for (const std::string& tk : toks) { if (!joined.empty()) joined += ' '; joined += tk; } lines.push_back(joined); } + editor_PlaceLines(lines); } @@ -1240,8 +1249,10 @@ GameState::typing_RebuildPreview(const std::string& s) const Vec3 anchor = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST); const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); + std::istringstream in(s); - std::string tok; + std::string tok; + while (in >> tok) { Text3dPtr t = make_Text3d(tok, Text3d::depth_floor, 0.003f, anchor, ConfigColor::floor); @@ -1260,6 +1271,7 @@ GameState::typing_ClearPreview() if (_carried.empty()) break; _carried.pop_back(); } + _typing_words.clear(); } @@ -1292,6 +1304,7 @@ GameState::wallconnect_BeginProgram() { _frame_input.capture_typing = true; _frame_input.typing_buffer.clear(); + _wall_connection.begin_Program(_pult_sys->get_ConnectProgramPos()); } @@ -1302,6 +1315,7 @@ GameState::wallconnect_BeginEditor() DynArray<std::string> lines = editor_AbsorbLines(); _frame_input.capture_vim = true; _frame_input.vim_keys.clear(); + _wall_connection.begin_Editor(_pult_sys->get_ConnectEditorPos(), lines, _ctx.camera->pos.y); } @@ -1321,6 +1335,7 @@ GameState::on_WallConnectSubmit() _program_sys->echo_Input(_ctx.wall_texts, h); return; } + _program_sys->feed_Input(line); _program_sys->echo_Input(_ctx.wall_texts, line); } @@ -1330,6 +1345,7 @@ void GameState::on_WallConnectExit() { _wall_connection.end(); + _frame_input.capture_typing = false; _frame_input.typing_buffer.clear(); } @@ -1339,6 +1355,7 @@ void GameState::editor_Clear() { _editor_wall.clear(); + _ctx.wall_texts.erase( std::remove_if(_ctx.wall_texts.begin(), _ctx.wall_texts.end(), [](const TextWallPtr& wt){ return wt->role == Wall::Role::Editor; }), @@ -1351,6 +1368,7 @@ GameState::editor_AbsorbLines() { std::string src = TextWall::extract_Text(_ctx.wall_texts, Wall::Role::Editor); DynArray<std::string> lines; + std::string cur; for (char ch : src) { if (ch == '\n') { lines.push_back(cur); cur.clear(); } @@ -1366,23 +1384,26 @@ GameState::editor_AbsorbLines() void GameState::editor_PlaceLines(const DynArray<std::string>& lines) { - const float ws = World::get_BoxScale(); - const double x_wall = -static_cast<double>(ws) + 0.05; - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); + const float ws = World::get_BoxScale(); + const double x_wall = -static_cast<double>(ws) + 0.05; + Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); - double y = static_cast<double>(ws) - 2.0; + double y = static_cast<double>(ws) - 2.0; const double z_start = static_cast<double>(ws) - 1.0; for (const std::string& line : lines) { double z = z_start; + std::istringstream in(line); - std::string tk; + std::string tk; + while (in >> tk) { Text3dPtr t = make_Text3d(tk, Text3d::depth_wall, 0.003f, Vec3{ x_wall, y, z }, ConfigColor::wall); - t->yaw = tf.yaw; + t->yaw = tf.yaw; Vec3 snapped = _editor_wall.on_WordPlaced(t.get(), Vec3{ x_wall, y, z }); - t->pos = snapped; + t->pos = snapped; + _ctx.wall_texts.push_back(cpputils::make_unique<TextWall>(TextWall{ Wall::Position::NegX, Wall::Role::Editor, std::move(t) })); z = snapped.z - 0.5; diff --git a/src/states/menu_state.cpp b/src/states/menu_state.cpp index e13f4e8..0bba412 100644 --- a/src/states/menu_state.cpp +++ b/src/states/menu_state.cpp @@ -7,16 +7,6 @@ #include "states/game_state.hpp" -// ───────────────────────────────────────────────────────────────────────── -// Layout: three columns at -90° / 0° / +90°. Left holds "Open File", right -// holds "Quit", middle holds the level list — Tutorial at eye level, every -// further level stacked downward. Camera stands at _world origin. -// -// W/S scroll the level list (the list moves; the _camera does not). At -// scroll = 0 the first level sits at eye level. At scroll = N the (N+1)th -// level does. -// ───────────────────────────────────────────────────────────────────────── - static constexpr float TITLE_SCALE = 0.018f; static constexpr float ITEM_SCALE = 0.012f; static constexpr float TEXT_DEPTH = 0.10f; @@ -24,7 +14,7 @@ static constexpr float TEXT_DEPTH = 0.10f; static constexpr double EYE_Y = 0.0; static constexpr double TITLE_Y = 2.0; static constexpr double COLUMN_RADIUS = 8.0; -static constexpr double LEVEL_SPACING = 1.6; // vertical gap between levels +static constexpr double LEVEL_SPACING = 1.6; static constexpr double COLUMN_LEFT_RAD = -SDL_PI_D * 0.35; static constexpr double COLUMN_MID_RAD = 0.0; @@ -54,7 +44,6 @@ MenuState::yaw_ToRadians(uint32_t yaw) } -// Helper: place a single Text3d item at a column angle and a given Y. static Vec3 slot_PosForColumn(double column_angle_rad, double y) { @@ -98,7 +87,6 @@ MenuState::build_Items() { _items.clear(); - // ── Left column: Open File ─────────────────────────────────────────── { MenuItem mi; mi.label = "Open File"; @@ -107,20 +95,15 @@ MenuState::build_Items() _items.push_back(std::move(mi)); } - // ── Middle column: levels, stacked top-to-bottom ───────────────────── - // We DON'T set slot_pos.y here; vertical position is recomputed each - // frame in update() based on _scroll_current. The X/Z components (the - // middle column) are fixed. for (size_t i = 0; i < Level::level_Count(); ++i) { MenuItem mi; mi.label = Level::get_Level(i).name; mi.kind = ItemKind::StartLevel; mi.level_index = i; - mi.slot_pos = slot_PosForColumn(COLUMN_MID_RAD, EYE_Y); // y replaced each frame + mi.slot_pos = slot_PosForColumn(COLUMN_MID_RAD, EYE_Y); _items.push_back(std::move(mi)); } - // ── Right column: Quit ─────────────────────────────────────────────── { MenuItem mi; mi.label = "Quit"; @@ -129,7 +112,6 @@ MenuState::build_Items() _items.push_back(std::move(mi)); } - // Build Text3d objects. for (size_t i = 0; i < _items.size(); ++i) { const auto color = (i == _selected) ? COLOR_SELECTED : COLOR_NORMAL; _items[i].text3d = make_Text3d(_items[i].label, @@ -144,7 +126,6 @@ MenuState::build_Items() void MenuState::handle_Event(Game& game, const SDL_Event& e) { - // ── Mouse-look ─────────────────────────────────────────────────────── if (e.type == SDL_EVENT_MOUSE_MOTION) { const double dx_rad = static_cast<double>(e.motion.xrel) * MOUSE_SENSITIVITY; const double dy_rad = static_cast<double>(e.motion.yrel) * MOUSE_SENSITIVITY; @@ -162,7 +143,6 @@ MenuState::handle_Event(Game& game, const SDL_Event& e) return; } - // ── Left-click: activate hovered item ──────────────────────────────── if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) { @@ -179,8 +159,6 @@ MenuState::handle_Event(Game& game, const SDL_Event& e) switch (e.key.key) { case SDLK_W: case SDLK_UP: - // Only scroll the level list when the player is looking at a - // level. Other columns ignore W/S. if (is_LevelItem(pick_FromCrosshair())) { if (_scroll_target > 0.0) _scroll_target -= 1.0; } @@ -252,7 +230,6 @@ MenuState::pick_FromCrosshair() const const double cy = SDL_cos(yaw), sy = SDL_sin(yaw); const double cp = SDL_cos(pitch), sp = SDL_sin(pitch); - // World-space forward (negation of view-matrix back axis). Vec3 fwd { -sy * cp, sp, -cy * cp }; size_t best = static_cast<size_t>(-1); @@ -299,45 +276,30 @@ MenuState::update(double dt) { _t += dt; - // ── Scroll lerp ────────────────────────────────────────────────────── - { - const double k = 1.0 - SDL_exp(-SCROLL_LERP_RATE * dt); - _scroll_current += (_scroll_target - _scroll_current) * k; - } + const double lerp_factor = 1.0 - SDL_exp(-SCROLL_LERP_RATE * dt); + _scroll_current += (_scroll_target - _scroll_current) * lerp_factor; - // ── Reposition level _items based on scroll ─────────────────────────── - // For each level i: y = EYE_Y - (i - _scroll_current) * SPACING. - // At _scroll_current = 0, i=0 sits at EYE_Y, i=1 below it, etc. - // As _scroll_current grows, the whole list slides upward, so further - // levels reach eye level. - { - size_t level_n = 0; - for (MenuItem& mi : _items) { - if (mi.kind != ItemKind::StartLevel) continue; - const double y = EYE_Y - (static_cast<double>(level_n) - _scroll_current) - * LEVEL_SPACING; - mi.text3d->pos = { mi.slot_pos.x, y, mi.slot_pos.z }; - ++level_n; - } + size_t level_n = 0; + for (MenuItem& mi : _items) { + if (mi.kind != ItemKind::StartLevel) continue; + const double y = EYE_Y - (static_cast<double>(level_n) - _scroll_current) + * LEVEL_SPACING; + mi.text3d->pos = { mi.slot_pos.x, y, mi.slot_pos.z }; + ++level_n; } - // ── Non-level _items stay at their fixed slot positions ─────────────── for (MenuItem& mi : _items) { if (mi.kind == ItemKind::StartLevel) continue; mi.text3d->pos = mi.slot_pos; } - // ── Crosshair-driven selection (mouse hover) ───────────────────────── - { - const size_t hover = pick_FromCrosshair(); - if (hover != static_cast<size_t>(-1) && hover != _selected) { - _items[_selected].text3d->set_Color(COLOR_NORMAL); - _items[hover].text3d->set_Color(COLOR_SELECTED); - _selected = hover; - } + const size_t hover = pick_FromCrosshair(); + if (hover != static_cast<size_t>(-1) && hover != _selected) { + _items[_selected].text3d->set_Color(COLOR_NORMAL); + _items[hover].text3d->set_Color(COLOR_SELECTED); + _selected = hover; } - // ── Billboard: every item faces the _camera ─────────────────────────── for (MenuItem& mi : _items) { if (!mi.text3d) continue; const double dx = _camera->pos.x - mi.text3d->pos.x; @@ -350,7 +312,6 @@ MenuState::update(double dt) _title->yaw = static_cast<float>(SDL_atan2(dx, dz)); } - // ── Bob the _selected item ──────────────────────────────────────────── if (_selected < _items.size() && _items[_selected].text3d) { const double bob = SDL_sin(_t * 3.0) * 0.10; _items[_selected].text3d->pos.y += bob; @@ -364,16 +325,13 @@ MenuState::render(SDL_Window& window, GlRenderer& renderer) int w, h; SDL_GetWindowSizeInPixels(&window, &w, &h); - // Update flashlight: pos = _camera, dir = where the _camera is looking. - { - const double yaw_r = ConfigPhysics::binrad * static_cast<double>(_camera->yaw); - const double pitch_r = ConfigPhysics::binrad * static_cast<double>(_camera->pitch); - const double cy = SDL_cos(yaw_r), sy = SDL_sin(yaw_r); - const double cp = SDL_cos(pitch_r), sp = SDL_sin(pitch_r); + const double yaw_r = ConfigPhysics::binrad * static_cast<double>(_camera->yaw); + const double pitch_r = ConfigPhysics::binrad * static_cast<double>(_camera->pitch); + const double cy = SDL_cos(yaw_r), sy = SDL_sin(yaw_r); + const double cp = SDL_cos(pitch_r), sp = SDL_sin(pitch_r); - _light->flashlight_pos = _camera->pos; - _light->flashlight_dir = Vec3{ -sy * cp, sp, -cy * cp }; - } + _light->flashlight_pos = _camera->pos; + _light->flashlight_dir = Vec3{ -sy * cp, sp, -cy * cp }; renderer.begin_Frame(w, h); renderer.set_Light(*_light); diff --git a/src/states/menu_state.hpp b/src/states/menu_state.hpp index f789845..f0a0fe2 100644 --- a/src/states/menu_state.hpp +++ b/src/states/menu_state.hpp @@ -22,9 +22,9 @@ class MenuState : public IState struct MenuItem { std::string label; ItemKind kind; - size_t level_index = 0; // valid when kind == StartLevel + size_t level_index = 0; Text3dPtr text3d; - Vec3 slot_pos {}; // base position in the column (no scroll/bob) + Vec3 slot_pos {}; }; cpputils::unique_ptr<World> _world; diff --git a/src/systems/carry_system.cpp b/src/systems/carry_system.cpp index 827bce3..a564c37 100644 --- a/src/systems/carry_system.cpp +++ b/src/systems/carry_system.cpp @@ -16,7 +16,8 @@ CarriedLine::push(Text3dPtr w) if (!first) { first = std::move(node); last = first.get(); - } else { + } + else { last->next = std::move(node); last = last->next.get(); } @@ -30,7 +31,8 @@ CarriedLine::pop_back() if (last->prev) { last = last->prev; last->next.reset(); - } else { + } + else { first.reset(); last = nullptr; } @@ -43,7 +45,8 @@ CarriedLine::pop_back() if (last->prev) { last = last->prev; last->next.reset(); - } else { + } + else { first.reset(); last = nullptr; } @@ -61,7 +64,8 @@ CarriedLine::pop_front() if (first->next) { first = std::move(first->next); first->prev = nullptr; - } else { + } + else { first.reset(); last = nullptr; } diff --git a/src/systems/collision.cpp b/src/systems/collision.cpp index 4375829..d2b554a 100644 --- a/src/systems/collision.cpp +++ b/src/systems/collision.cpp @@ -36,17 +36,19 @@ aabb_Manifold(const GameObject& a, const GameObject& b) std::max(a.aabb_min().z, b.aabb_min().z); if (ox <= 0.0 || oy <= 0.0 || oz <= 0.0) - return m; // no overlap + return m; m.hit = true; if (ox < oy && ox < oz) { m.overlap = ox; m.normal.x = (a.pos.x < b.pos.x) ? -1.0 : 1.0; - } else if (oy < oz) { + } + else if (oy < oz) { m.overlap = oy; m.normal.y = (a.pos.y < b.pos.y) ? -1.0 : 1.0; - } else { + } + else { m.overlap = oz; m.normal.z = (a.pos.z < b.pos.z) ? -1.0 : 1.0; } @@ -79,7 +81,7 @@ resolve_Objects(GameObject& a, GameObject& b) } // Impulse resolution - double rel_vel = dot(b.body.vel - a.body.vel, m.normal); + double rel_vel = Math::dot(b.body.vel - a.body.vel, m.normal); if (rel_vel >= 0.0) return; // already separating @@ -93,11 +95,11 @@ resolve_Objects(GameObject& a, GameObject& b) a.body.vel.y += j * inv_mass_a * m.normal.y; a.body.vel.z += j * inv_mass_a * m.normal.z; - if (SDL_fabs(j) < FRICTION_IMPULSE_CUTOFF) { // ← add the same guard - double vn = dot(a.body.vel, m.normal); + if (SDL_fabs(j) < FRICTION_IMPULSE_CUTOFF) { + double vn = Math::dot(a.body.vel, m.normal); Vec3 v_t = a.body.vel - m.normal * vn; v_t = v_t * a.body.friction; - if (length(v_t) < 0.02) v_t = Vec3{}; + if (Math::length(v_t) < 0.02) v_t = Vec3{}; a.body.vel = m.normal * vn + v_t; } } @@ -109,10 +111,10 @@ resolve_Objects(GameObject& a, GameObject& b) // Only damp tangential drift for gentle/resting contacts. A strong // impulse (fast hit) should transfer freely — don't friction it away. if (SDL_fabs(j) < FRICTION_IMPULSE_CUTOFF) { - double vn = dot(b.body.vel, m.normal); + double vn = Math::dot(b.body.vel, m.normal); Vec3 v_t = b.body.vel - m.normal * vn; v_t = v_t * b.body.friction; - if (length(v_t) < 0.02) v_t = Vec3{}; + if (Math::length(v_t) < 0.02) v_t = Vec3{}; b.body.vel = m.normal * vn + v_t; } } @@ -122,7 +124,7 @@ resolve_Objects(GameObject& a, GameObject& b) Vec3 reflect(const Vec3& vel, const Vec3& normal, double restitution) { - double d = dot(vel, normal); + double d = Math::dot(vel, normal); return { vel.x - (1.0 + restitution) * d * normal.x, vel.y - (1.0 + restitution) * d * normal.y, diff --git a/src/systems/input_system.cpp b/src/systems/input_system.cpp index 1c389b4..c80a8d4 100644 --- a/src/systems/input_system.cpp +++ b/src/systems/input_system.cpp @@ -38,7 +38,8 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) case SDLK_S: input.backward = true; break; case SDLK_D: input.right = true; break; } - } else if (sym == SDLK_RETURN) input.vim_keys.push_back(-1); + } + else if (sym == SDLK_RETURN) input.vim_keys.push_back(-1); else if (sym == SDLK_ESCAPE) input.vim_keys.push_back(-2); else if (sym == SDLK_BACKSPACE) input.vim_keys.push_back(-3); break; @@ -52,11 +53,14 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) case SDLK_S: input.backward = true; break; case SDLK_D: input.right = true; break; } - } else if (sym == SDLK_RETURN) { + } + else if (sym == SDLK_RETURN) { input.commands.push_back(InputCommand::ConnectPultSubmit); - } else if (sym == SDLK_ESCAPE) { + } + else if (sym == SDLK_ESCAPE) { input.commands.push_back(InputCommand::ConnectPultExit); - } else if (sym == SDLK_BACKSPACE && !input.typing_buffer.empty()) { + } + else if (sym == SDLK_BACKSPACE && !input.typing_buffer.empty()) { input.typing_buffer.pop_back(); } break; @@ -69,7 +73,7 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) input.commands.push_back(InputCommand::TypingCancel); else if (sym == SDLK_BACKSPACE && !input.typing_buffer.empty()) input.typing_buffer.pop_back(); - break; // swallow everything else — no movement while typing + break; } @@ -128,7 +132,8 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) if (input.capture_vim) { for (const char* p = e.text.text; *p; ++p) input.vim_keys.push_back((unsigned char)*p); - } else if (input.capture_typing || input.typing_mode) { + } + else if (input.capture_typing || input.typing_mode) { input.typing_buffer += e.text.text; } break; diff --git a/src/systems/physics_system.cpp b/src/systems/physics_system.cpp index 5f7d286..394e6e3 100644 --- a/src/systems/physics_system.cpp +++ b/src/systems/physics_system.cpp @@ -74,15 +74,16 @@ PhysicsSystem::resolve_PlayerVsObjects(Player& player, CarriedLine& carried, if (ox < oy && ox < oz) { overlap = ox; normal.x = (player.pos.x < obj->pos.x) ? -1.0 : 1.0; - } else if (oy < oz) { + } + else if (oy < oz) { overlap = oy; normal.y = (player.pos.y < obj->pos.y) ? -1.0 : 1.0; - } else { + } + else { overlap = oz; normal.z = (player.pos.z < obj->pos.z) ? -1.0 : 1.0; } - // Resolve the overlap fully. if (normal.y > 0.0) player.on_ground = true; @@ -90,7 +91,7 @@ PhysicsSystem::resolve_PlayerVsObjects(Player& player, CarriedLine& carried, player.pos.y += normal.y * overlap * 0.1; player.pos.z += normal.z * overlap * 0.1; - double vel_along = dot(player.vel, normal); + double vel_along = Math::dot(player.vel, normal); if (vel_along < 0.0) { player.vel.x -= vel_along * normal.x * 0.1; player.vel.y -= vel_along * normal.y * 0.1; @@ -104,25 +105,23 @@ PhysicsSystem::resolve_PlayerVsObjects(Player& player, CarriedLine& carried, obj->body.apply_KickImpulse({ -normal.x * kick, 0.3, -normal.z * kick }); } - // Grounded check is INDEPENDENT of overlap resolution. - // Feet near the top of this box AND overlap in XZ AND not jumping up. { constexpr double GROUND_EPS = 0.1; const bool xz_overlap = (px_max > omin.x && px_min < omax.x && - pz_max > omin.z && pz_min < omax.z); + pz_max > omin.z && pz_min < omax.z); const bool feet_on_top = (py_min <= omax.y + GROUND_EPS && - py_min >= omax.y - GROUND_EPS); + py_min >= omax.y - GROUND_EPS); if (xz_overlap && feet_on_top && player.vel.y <= 0.01) player.on_ground = true; } for (CarriedWordNode* node : nodes) { - if (!node || !node->word) continue; + if (!node || !node->word) + continue; GameObject& w = *node->word; - if (!Collision::aabb_Overlap(w, *obj)) - continue; + continue; Collision::Manifold m = Collision::aabb_Manifold(w, *obj); if (!m.hit) continue; @@ -135,7 +134,7 @@ PhysicsSystem::resolve_PlayerVsObjects(Player& player, CarriedLine& carried, w.pos.z += normal.z * overlap; double speed = SDL_sqrt(player.vel.x * player.vel.x + - player.vel.z * player.vel.z); + player.vel.z * player.vel.z); double kick = SDL_max(speed * 2.0, 4.0); if (obj->body.mode != PhysicsMode::AiHover) @@ -160,28 +159,24 @@ PhysicsSystem::try_Pair(GameObject* a, GameObject* b) bool i_static_solid = (mi == PhysicsMode::Static && a->solid); bool j_static_solid = (mj == PhysicsMode::Static && b->solid); - // Need at least one moving body if (!i_dyn && !j_dyn && !i_hov && !j_hov) return; + if (!Collision::aabb_Overlap(*a, *b)) return; - if (!Collision::aabb_Overlap(*a, *b)) return; - - // Dynamic body knocks a hovering one off its pedestal if (i_dyn && j_hov) { b->body.mode = PhysicsMode::Dynamic; b->body.vel = a->body.vel * 0.5; - } else if (j_dyn && i_hov) { + } + else if (j_dyn && i_hov) { a->body.mode = PhysicsMode::Dynamic; a->body.vel = b->body.vel * 0.5; } - // Re-read modes after possible mutation above bool i_dyn_now = (a->body.mode == PhysicsMode::Dynamic); bool j_dyn_now = (b->body.mode == PhysicsMode::Dynamic); - if ((i_dyn_now && j_dyn_now) || - (i_dyn_now && j_static_solid) || - (j_dyn_now && i_static_solid)) - { + if ( (i_dyn_now && j_dyn_now) || + (i_dyn_now && j_static_solid) || + (j_dyn_now && i_static_solid) ) { Collision::resolve_Objects(*a, *b); } } @@ -194,7 +189,6 @@ void PhysicsSystem::update_Objects(const DynArray<GameObject*>& objects, double dt, double gravity) { - // Integration pass { FP_ZONE("phys.integrate"); for (GameObject* obj : objects) { if (!obj) continue; @@ -209,7 +203,8 @@ PhysicsSystem::update_Objects(const DynArray<GameObject*>& objects, continue; } - if (obj->body.mode != PhysicsMode::Dynamic) continue; + if (obj->body.mode != PhysicsMode::Dynamic) + continue; obj->body.apply_Force({ 0.0, -gravity * obj->body.mass, 0.0 }); obj->body.integrate(dt); @@ -219,27 +214,22 @@ PhysicsSystem::update_Objects(const DynArray<GameObject*>& objects, } } - // World bounds { FP_ZONE("phys.bounds"); for (GameObject* obj : objects) { - if (!obj || obj->body.mode != PhysicsMode::Dynamic) continue; + if (!obj || obj->body.mode != PhysicsMode::Dynamic) + continue; resolve_WorldBounds(*obj); } } - // ── Object-vs-object collision ──────────────────────────────────────── - // Most objects are Static or AiHover and only ever collide as the passive - // side of a pair. Partition once so we never enumerate passive×passive, - // which is the bulk of the old O(n²) work. { FP_ZONE("phys.pairs"); - // Build grid over ALL objects (passive included — movers must find them). _grid.build(objects); - // Only active bodies initiate collision tests. DynArray<GameObject*> active; active.reserve(objects.size()); for (GameObject* o : objects) { - if (!o) continue; + if (!o) + continue; const PhysicsMode m = o->body.mode; if (m == PhysicsMode::Dynamic || m == PhysicsMode::Hover) active.push_back(o); @@ -293,11 +283,7 @@ PhysicsSystem::update_Projectiles(DynArray<Projectile>& projectiles, double dt, p.step_dt = dt; p.prev_centroid = Text3d::words_centroid(p.words); - // ── 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; } @@ -305,28 +291,28 @@ PhysicsSystem::update_Projectiles(DynArray<Projectile>& projectiles, double dt, 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 + const Vec3 live { 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; + 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); + + const double L = Math::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. + } + else { p.track_obj = nullptr; p.vel.y -= ConfigPhysics::proj_grav * dt; } - } else { + } + else { p.vel.y -= ConfigPhysics::proj_grav * dt; } @@ -412,59 +398,52 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, if (w) { word_he = w->shape.half_extents; break; } } - // 1. Resolve against GameObjects if (statics) { constexpr double RESTITUTION_STATIC = 0.45; for (GameObject* obj : *statics) { if (!obj || !obj->solid) continue; - // Text words: kick and pass through, regardless of Hover/Dynamic mode. if (obj->type == ObjectType::Text) { constexpr double RESTITUTION_TEXT = 0.55; Vec3 vel_before = proj.vel; Collision::SweptResult sw; if (resolve_SingleBoxCollision(proj, obj->aabb_min(), obj->aabb_max(), - word_he, RESTITUTION_TEXT, - cur, prev, dir, sw)) { + 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, vel_before.z * 0.4 - }); + }); - // ── emit contact event so consumers (level goals) can react. if (contacts) { contacts->push_back(ProjectileContactEvent{ - .projectile = &proj, .other = obj, - .point = cur, .normal = sw.normal - }); - } + .projectile = &proj, .other = obj, + .point = cur, .normal = sw.normal }); + } } continue; } - // Props: reflect projectile, emit contact event Collision::SweptResult sw; if (resolve_SingleBoxCollision(proj, obj->aabb_min(), obj->aabb_max(), - word_he, RESTITUTION_STATIC, - cur, prev, dir, sw)) { + word_he, RESTITUTION_STATIC, + cur, prev, dir, sw)) { proj.collided = true; if (contacts) { contacts->push_back(ProjectileContactEvent{ - .projectile = &proj, .other = obj, - .point = cur, .normal = sw.normal - }); + .projectile = &proj, .other = obj, + .point = cur, .normal = sw.normal }); } } } } - // 2. Resolve against Text Labels if (labels) { - constexpr double RESTITUTION_LABEL = 0.55; // WTF is that + constexpr double RESTITUTION_LABEL = 0.55; for (TextLabel* label : *labels) { if (!label || label->empty()) continue; @@ -478,8 +457,8 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, Collision::SweptResult sw; if (resolve_SingleBoxCollision(proj, hw->aabb_min(), hw->aabb_max(), - word_he, RESTITUTION_LABEL, cur, prev, dir, sw)) { - // Handle unique Label side-effect + word_he, RESTITUTION_LABEL, + cur, prev, dir, sw)) { proj.collided = true; label->kick(i, vel_before, sw.normal); } @@ -494,7 +473,6 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, void PhysicsSystem::SpatialGrid::build(const DynArray<GameObject*>& objs) { - // Cell size ~ a couple world units; tune if objects are much larger/smaller. cell = 2.0; inv = 1.0 / cell; min_x = min_z = -World::get_BoxScale<double>(); @@ -503,7 +481,7 @@ PhysicsSystem::SpatialGrid::build(const DynArray<GameObject*>& objs) const int total = nx * nz; if (static_cast<int>(cells.size()) < total) cells.resize(static_cast<size_t>(total)); - for (auto& c : cells) c.clear(); // keep capacity, drop contents + for (auto& c : cells) c.clear(); for (GameObject* o : objs) { if (!o) continue; @@ -518,7 +496,6 @@ PhysicsSystem::SpatialGrid::build(const DynArray<GameObject*>& objs) void PhysicsSystem::collide_Grid(const DynArray<GameObject*>& active) { - // Each active object tests against objects in its own + 8 neighbour cells. for (GameObject* a : active) { if (!a) continue; @@ -539,11 +516,10 @@ PhysicsSystem::collide_Grid(const DynArray<GameObject*>& active) for (GameObject* b : cell) { if (b == a) continue; - // Dedupe only when BOTH are active (both initiate). const PhysicsMode mb = b->body.mode; const bool b_active = (mb == PhysicsMode::Dynamic || mb == PhysicsMode::Hover); - if (b_active && b < a) continue; // active-active: once, from higher ptr + if (b_active && b < a) continue; try_Pair(a, b); } diff --git a/src/systems/pick_system.cpp b/src/systems/pick_system.cpp index 180d50f..7267415 100644 --- a/src/systems/pick_system.cpp +++ b/src/systems/pick_system.cpp @@ -9,10 +9,6 @@ namespace PickSystem static constexpr double MAX_REACH_XZ = 3.8; static constexpr double MAX_ANGLE = 0.25; -// ─── ang_score ────────────────────────────────────────────────────────────── -// -// Distance + angle test from camera to a text-like object's horizontal line. -// Unchanged from the original implementation. static double ang_score(const GameObject& obj, const Vec3& eye, const Vec3& fwd) { @@ -31,11 +27,11 @@ ang_score(const GameObject& obj, const Vec3& eye, const Vec3& fwd) Vec3 seg_dir { seg_b.x - seg_a.x, seg_b.y - seg_a.y, seg_b.z - seg_a.z }; Vec3 w { seg_a.x - eye.x, seg_a.y - eye.y, seg_a.z - eye.z }; - double a = dot(fwd, fwd); - double b = dot(fwd, seg_dir); - double c = dot(seg_dir, seg_dir); - double d = dot(fwd, w); - double e = dot(seg_dir, w); + double a = Math::dot(fwd, fwd); + double b = Math::dot(fwd, seg_dir); + double c = Math::dot(seg_dir, seg_dir); + double d = Math::dot(fwd, w); + double e = Math::dot(seg_dir, w); double denom = a*c - b*b; double seg_t = (denom > 1e-8) ? SDL_clamp((b*d - a*e) / denom, 0.0, 1.0) : 0.0; @@ -51,22 +47,15 @@ ang_score(const GameObject& obj, const Vec3& eye, const Vec3& fwd) double xz = SDL_sqrt(dv.x*dv.x + dv.z*dv.z); if (xz > MAX_REACH_XZ) return -1.0; - double len = length(dv); + double len = Math::length(dv); if (len < 0.001) return -1.0; - double t = SDL_clamp(dot(fwd, dv) / len, -1.0, 1.0); + double t = SDL_clamp(Math::dot(fwd, dv) / len, -1.0, 1.0); double angle = SDL_acos(t); return (angle < MAX_ANGLE) ? angle : -1.0; } -// ─── pick ─────────────────────────────────────────────────────────────────── -// -// Walks each registry separately, finds the single best (smallest angle) hit -// across all of them, returns a PickResult identifying which list it came -// from and where in that list. The picker NEVER mutates a registry — the -// caller is responsible for moving the picked entity into a carry / anim / -// etc. based on the PickResult. PickResult pick(const Camera& camera, const DynArray<Text3dPtr>& words, @@ -75,20 +64,17 @@ pick(const Camera& camera, { const Vec3& eye = camera.pos; - // Floor plane. Words/wall-text scrolled (or fallen) below the floor are - // hidden under it but still angle-pickable, so exclude anything whose - // centre has dropped beneath the floor surface. const double floor_y = World::get_FloorY(); PickResult result; - double best_angle = MAX_ANGLE; // any actual hit must beat this + double best_angle = MAX_ANGLE; - // ── words ─────────────────────────────────────────────────────────── for (size_t i = 0; i < words.size(); ++i) { const Text3dPtr& w = words[i]; if (!w || !w->interactable) continue; - if (w->ai.state != Text3dAI::State::Idle) continue; - if (w->pos.y < floor_y) continue; // below the floor → not pickable + if (w->ai.state != Text3dAI::State::Idle) + continue; + if (w->pos.y < floor_y) continue; double a = ang_score(*w, eye, camera.fwd); if (a >= 0.0 && a < best_angle) { @@ -98,10 +84,9 @@ pick(const Camera& camera, } } - // ── scenery ───────────────────────────────────────────────────────── for (const GameObjectPtr& s : scenery) { if (!s || !s->interactable) continue; - if (s->pos.y < floor_y) continue; // below the floor → not pickable + if (s->pos.y < floor_y) continue; double a = ang_score(*s, eye, camera.fwd); if (a >= 0.0 && a < best_angle) { best_angle = a; @@ -110,11 +95,10 @@ pick(const Camera& camera, } } - // ── walls ─────────────────────────────────────────────────────────── for (size_t i = 0; i < wall_texts.size(); ++i) { const TextWallPtr& wt = wall_texts[i]; - if (!wt || !wt->text3d) continue; - if (wt->text3d->pos.y < floor_y) continue; // scrolled below the floor → not pickable + if (!wt || !wt->text3d) continue; + if (wt->text3d->pos.y < floor_y) continue; double a = ang_score(*wt->text3d, eye, camera.fwd); if (a >= 0.0 && a < best_angle) { best_angle = a; diff --git a/src/systems/projectile_system.cpp b/src/systems/projectile_system.cpp index fb73e57..356a364 100644 --- a/src/systems/projectile_system.cpp +++ b/src/systems/projectile_system.cpp @@ -60,7 +60,6 @@ wall_reconstruct(Wall::Position p, double plane, double tangent, double y) } -// ── Helper: emit a Floor landing result and mark the projectile for removal ── static void emit_FloorLanding(Projectile& p, DynArray<ProjectileResult>& results) { @@ -72,11 +71,6 @@ emit_FloorLanding(Projectile& p, DynArray<ProjectileResult>& results) } -// ── Helper: advance one sticking projectile's smoothing animation ──────────── -// -// Words exponentially smooth toward their final wall positions. When stick_t -// reaches 1 we emit a Wall landing result; GameState transfers the words to -// TextWall. Returns nothing — completion is detected via emptied p.words. static void tick_Sticking(Projectile& p, double dt, double ws, DynArray<ProjectileResult>& results) @@ -93,13 +87,12 @@ tick_Sticking(Projectile& p, double dt, double ws, if (!w || w->type != ObjectType::Text) continue; Vec3 target = wall_reconstruct(p.stick_wall, plane, - w->ribbon_local.x, - w->ribbon_local.y); + w->ribbon_local.x, + w->ribbon_local.y); - w->pos = lerp(w->pos, target, k); - w->yaw = static_cast<float>(lerp( - static_cast<double>(w->yaw), - static_cast<double>(fy), k)); + w->pos = Math::lerp(w->pos, target, k); + w->yaw = static_cast<float>(Math::lerp(static_cast<double>(w->yaw), + static_cast<double>(fy), k)); } if (p.stick_t >= 1.0) { @@ -113,12 +106,6 @@ tick_Sticking(Projectile& p, double dt, double ws, } -// ── Helper: floor / ceiling test for a flying projectile ───────────────────── -// -// Floor landing fires only when the word's lowest point reaches the floor -// plane. Anywhere above the floor — including on top of a pult — the -// projectile keeps flying / bouncing. Ceiling bounce reflects Y velocity. -// Returns true if the projectile should be removed (floor landing emitted). static bool check_FloorCeiling(Projectile& p, const Vec3& centroid, double ws, DynArray<ProjectileResult>& results) @@ -142,8 +129,6 @@ check_FloorCeiling(Projectile& p, const Vec3& centroid, double ws, } -// ── Helper: which wall (if any) does the projectile cross this frame? ──────── -// Sets out_pos when it returns true. static bool detect_WallHit(const Projectile& p, const Vec3& centroid, double dt, double ws, Wall::Position& out_pos) @@ -151,7 +136,7 @@ detect_WallHit(const Projectile& p, const Vec3& centroid, double dt, double ws, const double next_x = centroid.x + p.vel.x * dt; const double next_z = centroid.z + p.vel.z * dt; - if ((centroid.x > -ws + ConfigPhysics::stick_dist && next_x <= -ws + ConfigPhysics::stick_dist) || + if ((centroid.x > -ws + ConfigPhysics::stick_dist && next_x <= -ws + ConfigPhysics::stick_dist) || (centroid.x <= -ws + ConfigPhysics::stick_dist && p.vel.x < 0.0)) { out_pos = Wall::Position::NegX; return true; } @@ -159,7 +144,7 @@ detect_WallHit(const Projectile& p, const Vec3& centroid, double dt, double ws, (centroid.x >= ws - ConfigPhysics::stick_dist && p.vel.x > 0.0)) { out_pos = Wall::Position::PosX; return true; } - if ((centroid.z > -ws + ConfigPhysics::stick_dist && next_z <= -ws + ConfigPhysics::stick_dist) || + if ((centroid.z > -ws + ConfigPhysics::stick_dist && next_z <= -ws + ConfigPhysics::stick_dist) || (centroid.z <= -ws + ConfigPhysics::stick_dist && p.vel.z < 0.0)) { out_pos = Wall::Position::NegZ; return true; } @@ -171,9 +156,6 @@ detect_WallHit(const Projectile& p, const Vec3& centroid, double dt, double ws, } -// ── Helper: does the projectile path cross a wall-text this frame? ─────────── -// Expands each wall-text AABB by the projectile's half-extents (Minkowski) -// and sweeps the centroid ray against it. Sets out_idx on hit. static bool check_WallTextIntercept(const Projectile& p, const Vec3& centroid, double dt, Wall::Position hit_pos, @@ -213,7 +195,6 @@ check_WallTextIntercept(const Projectile& p, const Vec3& centroid, double dt, } -// ── Helper: begin the editor-wall stick animation for a projectile ─────────── static void begin_EditorStick(Projectile& p, Wall::Position hit_pos) { @@ -235,12 +216,6 @@ begin_EditorStick(Projectile& p, Wall::Position hit_pos) const double dot = cr_x * wt_x + cr_z * wt_z; const double sign = (dot >= 0.0) ? 1.0 : -1.0; - // Crosshair aim point on the wall: intersect the CAMERA ray (cam_origin - // along dir) with the wall plane. Using the camera ray rather than the - // line's centroid means the run merges where the crosshair points — the - // carried line is laid out from its anchor, so its centroid sits ~half a - // line-width off the aim, which is what dragged the merge sideways. This - // also gives the aimed-at height with no gravity soak at any distance. const Vec3 c_pos = Text3d::words_centroid(p.words); const double plane = wall_plane_coord(hit_pos, World::get_BoxScale()); const bool x_wall = (hit_pos == Wall::Position::NegX || hit_pos == Wall::Position::PosX); @@ -250,12 +225,13 @@ begin_EditorStick(Projectile& p, Wall::Position hit_pos) Vec3 aim; if (SDL_fabs(dir_coord) > 1e-6) { - const double t = (plane - cam_coord) / dir_coord; // steps along the aim ray + const double t = (plane - cam_coord) / dir_coord; aim = Vec3{ p.cam_origin.x + p.dir.x * t, p.cam_origin.y + p.dir.y * t, p.cam_origin.z + p.dir.z * t }; - } else { - aim = c_pos; // aiming parallel to the wall; fall back to the centroid + } + else { + aim = c_pos; } const double c_tan = wall_tangent(hit_pos, aim.x, aim.z); @@ -273,10 +249,6 @@ begin_EditorStick(Projectile& p, Wall::Position hit_pos) } -// ── Helper: begin the program-wall "input echo" stick ──────────────────────── -// Like the editor stick, but aimed at the program wall's output line (centred, -// at the bottom row where new output appears) regardless of where it was hit — -// so a consumed input word glides into place like text typed at a terminal. static void begin_ProgramStick(Projectile& p, double ws) { @@ -288,12 +260,12 @@ begin_ProgramStick(Projectile& p, double ws) const double cr_x = SDL_cos(p.yaw); const double cr_z = -SDL_sin(p.yaw); - const double wt_z = -1.0; // PosX wall inward tangent + const double wt_z = -1.0; const double dot = cr_x * 0.0 + cr_z * wt_z; const double sign = (dot >= 0.0) ? 1.0 : -1.0; - const double c_tan = 0.0; // centred horizontally on the wall - const double c_y = -ws + 1.0; // matches ProgramSystem::start_y + const double c_tan = 0.0; + const double c_y = -ws + 1.0; for (Text3dPtr& w : p.words) { if (!w || w->type != ObjectType::Text) continue; @@ -306,9 +278,6 @@ begin_ProgramStick(Projectile& p, double ws) } -// ── Helper: bounce a projectile off a non-editor wall ──────────────────────── -// Returns true if the projectile should be removed (second bounce on this -// axis → falls to the floor instead of bouncing again). static bool bounce_OffWall(Projectile& p, Wall::Position hit_pos, double ws, DynArray<ProjectileResult>& results) @@ -323,27 +292,26 @@ bounce_OffWall(Projectile& p, Wall::Position hit_pos, double ws, } p.bounce_state |= axis_bit; - p.collided = true; + p.collided = true; if (x_axis) { p.vel.x = bounce_velocity(p.vel.x, 0.70); for (Text3dPtr& w : p.words) if (w) w->pos.x = (hit_pos == Wall::Position::PosX) - ? ( ws - ConfigPhysics::stick_dist - 0.05) - : (-ws + ConfigPhysics::stick_dist + 0.05); - } else { + ? ( ws - ConfigPhysics::stick_dist - 0.05) + : (-ws + ConfigPhysics::stick_dist + 0.05); + } + else { p.vel.z = bounce_velocity(p.vel.z, 0.70); for (Text3dPtr& w : p.words) if (w) w->pos.z = (hit_pos == Wall::Position::PosZ) - ? ( ws - ConfigPhysics::stick_dist - 0.05) - : (-ws + ConfigPhysics::stick_dist + 0.05); + ? ( ws - ConfigPhysics::stick_dist - 0.05) + : (-ws + ConfigPhysics::stick_dist + 0.05); } return false; } -// ── Helper: handle a wall hit — intercept, stick, or bounce ────────────────── -// Returns true if the projectile should be removed. static bool handle_WallHit(Projectile& p, const Vec3& centroid, Wall::Position hit_pos, double dt,double ws, @@ -351,19 +319,11 @@ handle_WallHit(Projectile& p, const Vec3& centroid, Wall::Position hit_pos, DynArray<ProjectileResult>& results, bool program_running) { - // Program wall (+X): a thrown word is terminal INPUT. - // - program running → it will be read: stick and glide to the output - // line (the "soaked" animation); GameState then feeds stdin + echoes it. - // - not running → nothing to read it: bounce off as normal. if (Wall::role_of(hit_pos) == Wall::Role::Program) { if (program_running) { begin_ProgramStick(p, ws); return false; } return bounce_OffWall(p, hit_pos, ws, results); } - // Wall-text intercept takes priority — but ONLY for a single-word shot, - // which is what knocks an existing word down or hits a line number. A whole - // shot LINE must never knock a word off; it always sticks (and merges) or - // bounces, so skip the intercept when carrying more than one word. size_t hit_idx = 0; if (p.words.size() == 1 && check_WallTextIntercept(p, centroid, dt, hit_pos, wall_text_hits, hit_idx)) { @@ -388,10 +348,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, - const Aim::Result& aim) + DynArray<Text3dPtr> words, + const Vec3& fwd, + const Vec3& eye, + const Aim::Result& aim) { if (words.empty()) return; @@ -422,10 +382,7 @@ 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 Vec3 target= aim.point; const double g = ConfigPhysics::proj_grav; const double dx = target.x - centroid.x; const double dy = target.y - centroid.y; @@ -444,7 +401,6 @@ ProjectileSystem::launch(DynArray<Projectile>& out, 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; @@ -463,15 +419,12 @@ ProjectileSystem::update(DynArray<Projectile>& projectiles, { const double ws = World::get_BoxScale<double>(); - // ── 1. Advance stick animations ────────────────────────────────────────── for (Projectile& p : projectiles) tick_Sticking(p, dt, ws, results); - // ── 2. Flight: floor/ceiling, wall hit, then cleanup ───────────────────── for (auto it = projectiles.begin(); it != projectiles.end(); ) { Projectile& p = *it; - // Finished sticking projectiles: erase once words have been cleared. if (p.sticking) { if (p.words.empty()) it = projectiles.erase(it); else ++it; @@ -492,14 +445,15 @@ ProjectileSystem::update(DynArray<Projectile>& projectiles, wall_text_hits, results, program_running); } - if (!remove && !p.sticking && length(p.vel) < 1.0) { + if (!remove && !p.sticking && Math::length(p.vel) < 1.0) { emit_FloorLanding(p, results); remove = true; } if (remove) { it = projectiles.erase(it); - } else { + } + else { p.prev_centroid = centroid; ++it; } diff --git a/src/systems/source_tokens.hpp b/src/systems/source_tokens.hpp index a3642ea..f84ece3 100644 --- a/src/systems/source_tokens.hpp +++ b/src/systems/source_tokens.hpp @@ -6,19 +6,6 @@ #include "utils/globals.hpp" -// ───────────────────────────────────────────────────────────────────────── -// SourceTokens — Option 1 tokenizer + file IO for the dynamic-lib sandbox. -// -// Tokenizing rule (matches the editor-wall "one word per token" model): -// - identifier/number runs (alnum, '.', '_') stay whole: fuu 1.0 my_var -// - a quoted "..." string is one token (quotes kept, \" respected) -// - every other character is its own single-char token: ( ) , ; # * { } -// -// Round-trips to valid C when rejoined with single spaces (verified: even -// `# include "x.h"` and unary `- 3` compile). The editor wall stores each -// token as a Text3d word; extract_Text rejoins them with spaces, so the -// file written back on compile is space-normalized but compilable. -// ───────────────────────────────────────────────────────────────────────── namespace SourceTokens { @@ -26,7 +13,8 @@ inline DynArray<std::string> tokenize(const std::string& src) { DynArray<std::string> out; - const size_t n = src.size(); + const + size_t n = src.size(); size_t i = 0; while (i < n) { const char c = src[i]; @@ -56,7 +44,8 @@ tokenize(const std::string& src) if (std::isalnum(static_cast<unsigned char>(d)) || d == '.' || d == '_') { s += d; ++i; - } else { + } + else { break; } } diff --git a/src/systems/wall_connection.cpp b/src/systems/wall_connection.cpp index a906076..f719601 100644 --- a/src/systems/wall_connection.cpp +++ b/src/systems/wall_connection.cpp @@ -186,7 +186,8 @@ WallConnection::rebuild_Editor() rows[i].txt->pos.y = row_y; rows[i].txt->pos.z = z_block_left - code_u0 - he; _render.push_back(std::move(rows[i].txt)); - } else { + } + else { _render.push_back(Text3dPtr{}); } } @@ -228,7 +229,8 @@ WallConnection::render(GlRenderer& gl, const Camera& cam, const Light& light) co draw_ProgramCursor(gl, cam, light); draw_Electric (gl, cam); - } else if (_target == Target::Editor) { + } + else if (_target == Target::Editor) { for (const Text3dPtr& t : _render) if (t) t->draw(gl, cam, light); for (const Text3dPtr& n : _linenums) if (n) n->draw(gl, cam, light); @@ -284,7 +286,8 @@ WallConnection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light cw = (2.0 * static_cast<double>(line->half_extent)) / static_cast<double>(N); line_x = line->pos.x; he_y = static_cast<double>(line->half_extent_y); - } else { + } + else { cw = 0.18; } diff --git a/src/systems/word_ai_system.cpp b/src/systems/word_ai_system.cpp index ff2d1ea..6ceec1f 100644 --- a/src/systems/word_ai_system.cpp +++ b/src/systems/word_ai_system.cpp @@ -1,21 +1,19 @@ -#include "word_ai_system.hpp" - #include <SDL3/SDL.h> #include <cstdlib> #include "c_token/token_color.hpp" #include "entities/player.hpp" +#include "entities/world.hpp" #include "systems/carry_system.hpp" #include "systems/collision.hpp" #include "utils/config.hpp" +#include "word_ai_system.hpp" static Vec3 perpendicular_horizontal(const Vec3& v) { - // Horizontal perpendicular (rotated 90° around Y). - // Used for wiggle — vertical wiggle would look weird. return { -v.z, 0.0, v.x }; } @@ -29,21 +27,18 @@ WordAiSystem::update(DynArray<Text3dPtr>& words, { DynArray<Text3dPtr> released; - // Count AI-active words (Attack/Flee/Stunned all count as "active"). int active_ai_count = 0; for (const Text3dPtr& w : words) { if (!w) continue; if (w->ai.state != Text3dAI::State::Idle) ++active_ai_count; } - // Index-based loop so reallocation doesn't invalidate. - // Capture size before the loop so newly-appended items wait until next frame. const size_t n = words.size(); for (size_t i = 0; i < n; ++i) { Text3dPtr& w = words[i]; if (!w) continue; - if (w->ai.state != Text3dAI::State::Idle && + if (w->ai.state != Text3dAI::State::Idle && w->body.mode != PhysicsMode::AiHover && w->body.mode != PhysicsMode::Hover) { @@ -52,7 +47,7 @@ WordAiSystem::update(DynArray<Text3dPtr>& words, s->body.mode = PhysicsMode::Dynamic; s->body.vel = w->body.vel * 0.5; s->set_Color(s->token_info ? TokenColor::get(s->token_info->type) : ConfigColor::floor); - released.push_back(std::move(s)); // deferred — not words.push_back + released.push_back(std::move(s)); } w->ai.stolen.clear(); w->ai.state = Text3dAI::State::Idle; @@ -63,11 +58,11 @@ WordAiSystem::update(DynArray<Text3dPtr>& words, w->ai.state_timer += dt; switch (w->ai.state) { - case Text3dAI::State::Idle: update_Idle(*w, player, active_ai_count, dt); break; - case Text3dAI::State::Attack: update_Attack(*w, player, carried, words, dropped, released, dt); - break; - case Text3dAI::State::Flee: update_Flee(*w, player, released, dt); break; - case Text3dAI::State::Stunned: update_Stunned(*w, released, dt); break; + case Text3dAI::State::Idle: update_Idle (*w, player, active_ai_count, dt); break; + case Text3dAI::State::Attack: update_Attack (*w, player, carried, words, dropped, released, dt); + break; + case Text3dAI::State::Flee: update_Flee (*w, player, released, dt); break; + case Text3dAI::State::Stunned: update_Stunned(*w, released, dt); break; } update_StolenTrail(*w, dt); @@ -84,13 +79,12 @@ WordAiSystem::update_Idle(Text3d& w, const Player& p, int active_ai_count, doubl { if (w.body.mode != PhysicsMode::Hover) return; - // Hard cap: don't start new AI behavior if we're already at the limit. if (active_ai_count >= _max_active_ai) return; Vec3 to_player = p.pos - w.pos; double dist_sq = to_player.x*to_player.x + - to_player.y*to_player.y + - to_player.z*to_player.z; + to_player.y*to_player.y + + to_player.z*to_player.z; if (dist_sq > ConfigAi::trigger_range * ConfigAi::trigger_range) return; @@ -113,7 +107,8 @@ void WordAiSystem::update_Attack(Text3d& w, const Player& p, CarriedLine& carrie Vec3 target; if (has_carry) { target = carried.last->word->pos; - } else { + } + else { constexpr double ORBIT_RADIUS = 4.5; constexpr double ORBIT_SPEED = 0.6; constexpr double ORBIT_HEIGHT = 0.5; @@ -128,12 +123,11 @@ void WordAiSystem::update_Attack(Text3d& w, const Player& p, CarriedLine& carrie } Vec3 to_target = target - w.pos; - double dist = length(to_target); + double dist = Math::length(to_target); - // Pause/dash phase machine (unchanged) w.ai.phase_timer -= dt; if (w.ai.phase_timer <= 0.0) { - w.ai.in_pause = !w.ai.in_pause; + w.ai.in_pause = !w.ai.in_pause; w.ai.phase_timer = w.ai.in_pause ? (0.4 + Random::unit() * 0.8) : (1.0 + Random::unit() * 1.5); @@ -142,24 +136,23 @@ void WordAiSystem::update_Attack(Text3d& w, const Player& p, CarriedLine& carrie Vec3 target_vel{}; if (!w.ai.in_pause && dist > 0.01) { double inv = 1.0 / dist; - Vec3 dir = { to_target.x * inv, to_target.y * inv, to_target.z * inv }; + Vec3 dir = { to_target.x * inv, to_target.y * inv, to_target.z * inv }; target_vel = dir * w.ai.speed; w.ai.wiggle_phase += w.ai.wiggle_freq * dt; - Vec3 perp = perpendicular_horizontal(dir); - double wig = SDL_sin(w.ai.wiggle_phase) * ConfigAi::wiggle_amplitude; - if (!has_carry) wig *= 0.4; // gentler wobble while orbiting + Vec3 perp = perpendicular_horizontal(dir); + double wig = SDL_sin(w.ai.wiggle_phase) * ConfigAi::wiggle_amplitude; + if (!has_carry) wig *= 0.4; target_vel.x += perp.x * wig; target_vel.z += perp.z * wig; } apply_Locomotion(w, target_vel, dt); - // ── Contact: attacker touches carried word? ────────────────────── if (!carried.empty()) { if (w.ai.has_stolen) { - // Already stole this session — touching carry does nothing. - } else { + } + else { DynArray<CarriedWordNode*> nodes = carried.reverse_nodes(); for (CarriedWordNode* node : nodes) { if (!node || !node->word) continue; @@ -170,11 +163,10 @@ void WordAiSystem::update_Attack(Text3d& w, const Player& p, CarriedLine& carrie stolen->body.mode = PhysicsMode::AiHover; stolen->set_Color(ConfigColor::attack); w.ai.stolen.push_back(std::move(stolen)); - enter_Flee(w); // run away with the loot + enter_Flee(w); carried.compact(); break; } else { - // failed steal: knock the word off and flee Text3dPtr knocked = std::move(node->word); knocked->body.mode = PhysicsMode::Dynamic; knocked->body.vel = { 0.0, 2.0, 0.0 }; @@ -187,30 +179,28 @@ void WordAiSystem::update_Attack(Text3d& w, const Player& p, CarriedLine& carrie } } - // ── Bowl over Idle words ───────────────────────────────────────── - // (Runs regardless of whether the player is carrying anything.) for (Text3dPtr& other : all_words) { - if (!other) continue; - if (other.get() == &w) continue; - if (other->ai.state != Text3dAI::State::Idle) continue; - if (other->body.mode != PhysicsMode::Hover) continue; - if (!Collision::aabb_Overlap(w, *other)) continue; + if (!other) continue; + if (other.get() == &w) continue; + if (other->ai.state != Text3dAI::State::Idle)continue; + if (other->body.mode != PhysicsMode::Hover) continue; + if (!Collision::aabb_Overlap(w, *other)) continue; + + Vec3 push_dir = other->pos - w.pos; - Vec3 push_dir = other->pos - w.pos; double d = SDL_sqrt(push_dir.x*push_dir.x + push_dir.z*push_dir.z); if (d < 0.01) { push_dir = { 1.0, 0.0, 0.0 }; d = 1.0; } push_dir.x /= d; push_dir.z /= d; - double kick_speed = SDL_max(length(w.body.vel) * 1.5, 3.0); + double kick_speed = SDL_max(Math::length(w.body.vel) * 1.5, 3.0); other->body.apply_KickImpulse({ - push_dir.x * kick_speed, 1.5, push_dir.z * kick_speed + push_dir.x * kick_speed, 1.5, push_dir.z * kick_speed }); } - // ── Give up if player is too far ───────────────────────────────── Vec3 to_player = p.pos - w.pos; - if (length(to_player) > ConfigAi::trigger_range * 3.0) { + if (Math::length(to_player) > ConfigAi::trigger_range * 3.0) { release_StolenWords(w, released); enter_Stunned(w); } @@ -221,12 +211,11 @@ void WordAiSystem::update_Flee(Text3d& w, const Player& p, DynArray<Text3dPtr>& released, double dt) { Vec3 away = w.pos - p.pos; - double dist = length(away); + double dist = Math::length(away); - // Pause/dash similar to attack w.ai.phase_timer -= dt; if (w.ai.phase_timer <= 0.0) { - w.ai.in_pause = !w.ai.in_pause; + w.ai.in_pause = !w.ai.in_pause; w.ai.phase_timer = w.ai.in_pause ? (0.3 + Random::unit() * 0.5) : (1.5 + Random::unit() * 1.5); @@ -235,7 +224,7 @@ WordAiSystem::update_Flee(Text3d& w, const Player& p, DynArray<Text3dPtr>& relea Vec3 target_vel{}; if (!w.ai.in_pause && dist > 0.01) { double inv = 1.0 / dist; - Vec3 dir = { away.x * inv, 0.0, away.z * inv }; // horizontal flee + Vec3 dir = { away.x * inv, 0.0, away.z * inv }; target_vel = dir * w.ai.speed * 1.2; w.ai.wiggle_phase += w.ai.wiggle_freq * dt; @@ -247,18 +236,15 @@ WordAiSystem::update_Flee(Text3d& w, const Player& p, DynArray<Text3dPtr>& relea apply_Locomotion(w, target_vel, dt); - // Escape condition: far enough, for long enough. if (dist > ConfigAi::flee_escape_dist) { if (w.ai.state_timer > ConfigAi::flee_escape_time) { - // Got away. If carrying loot, roll for drop-vs-keep. if (!w.ai.stolen.empty()) { if (Random::unit() < ConfigAi::drop_loot_probability) { release_StolenWords(w, released); } - // Whether we dropped or kept, come back for another attack. enter_Attack(w); - } else { - // No loot — flee finished naturally. + } + else { enter_Idle(w); } } @@ -294,16 +280,15 @@ WordAiSystem::release_StolenWords(Text3d& w, DynArray<Text3dPtr>& out) void WordAiSystem::enter_Attack(Text3d& w) { - w.ai.state = Text3dAI::State::Attack; - w.ai.state_timer = 0.0; - w.ai.phase_timer = 0.5 + Random::unit() * 1.5; - w.ai.in_pause = false; - w.ai.has_stolen = false; - w.ai.hover_height_saved = w.body.hover_height; - w.body.mode = PhysicsMode::AiHover; w.set_Color(ConfigColor::attack); + w.body.mode = PhysicsMode::AiHover; + w.ai.state = Text3dAI::State::Attack; + w.ai.state_timer = 0.0; + w.ai.phase_timer = 0.5 + Random::unit() * 1.5; + w.ai.in_pause = false; + w.ai.has_stolen = false; + w.ai.hover_height_saved = w.body.hover_height; - // Pick an orbit direction and starting angle for the no-carry wander. w.ai.orbit_angle = Random::unit() * SDL_PI_D * 2.0; w.ai.orbit_dir = (Random::unit() < 0.5) ? -1.0 : 1.0; } @@ -312,13 +297,13 @@ WordAiSystem::enter_Attack(Text3d& w) void WordAiSystem::enter_Flee(Text3d& w) { - w.ai.state = Text3dAI::State::Flee; - w.ai.state_timer = 0.0; - w.ai.phase_timer = 0.3 + Random::unit() * 0.7; - w.ai.in_pause = false; - w.ai.hover_height_saved = w.body.hover_height; - w.body.mode = PhysicsMode::AiHover; w.set_Color(ConfigColor::flee); + w.body.mode = PhysicsMode::AiHover; + w.ai.state = Text3dAI::State::Flee; + w.ai.state_timer = 0.0; + w.ai.phase_timer = 0.3 + Random::unit() * 0.7; + w.ai.in_pause = false; + w.ai.hover_height_saved = w.body.hover_height; } @@ -355,8 +340,7 @@ WordAiSystem::apply_Locomotion(Text3d& w, const Vec3& target_vel, double dt) w.pos.y += w.body.vel.y * dt; w.pos.z += w.body.vel.z * dt; - // Clamp to world bounds - const double ws = static_cast<double>(_world_scale); + const double ws = World::get_BoxScale<double>(); auto clamp_axis = [](double& p, double& v, double he, double lo, double hi) { @@ -430,6 +414,5 @@ WordAiSystem::update_StolenTrail(Text3d& w, double dt) offset += static_cast<double>(s->half_extent); } - // Tick hover_phase so bob/sway animate even when attacker is paused w.body.hover_phase += ConfigText3d::hover_speed * dt; } diff --git a/src/systems/word_ai_system.hpp b/src/systems/word_ai_system.hpp index 0003aa2..3eed820 100644 --- a/src/systems/word_ai_system.hpp +++ b/src/systems/word_ai_system.hpp @@ -8,7 +8,6 @@ struct CarriedLine; class WordAiSystem { - float _world_scale; int _max_active_ai = 5; @@ -33,7 +32,6 @@ class WordAiSystem { void apply_Locomotion(Text3d& w, const Vec3& target_vel, double dt); public: void set_MaxActiveAi(int n) { _max_active_ai = n; } - void set_WorldScale (float ws) { _world_scale = ws; } void update(DynArray<Text3dPtr>& words, const Player& player, CarriedLine& carried, diff --git a/src/systems/word_anim.cpp b/src/systems/word_anim.cpp index 07ddd8f..def8a7a 100644 --- a/src/systems/word_anim.cpp +++ b/src/systems/word_anim.cpp @@ -34,7 +34,8 @@ WordAnimSystem::update(DynArray<WordAnim_Add>& anims_add, anim.word->yaw = static_cast<float>(anim.yaw_target); anim.word->pitch = static_cast<float>(anim.pitch_target); carried.push(std::move(anim.word)); - } else { + } + else { double s = smoothstep(anim.t); anim.word->pos = { lerp_d(anim.start_pos.x, anim.target_pos.x, s), @@ -55,13 +56,11 @@ WordAnimSystem::update(DynArray<WordAnim_Add>& anims_add, for (WordAnim_Drop& drop : anims_drop) { if (drop.complete) continue; - // Integrate: gravity + position update. drop.word->body.vel.y -= ConfigPhysics::proj_grav * dt; drop.word->pos.x += drop.word->body.vel.x * dt; drop.word->pos.y += drop.word->body.vel.y * dt; drop.word->pos.z += drop.word->body.vel.z * dt; - // Account for the word's own thickness — land when the BOTTOM touches floor. const double bottom = drop.word->pos.y - drop.word->shape.half_extents.y; if (bottom <= floor_y) { drop.complete = true; diff --git a/src/text_3d/text_3d.cpp b/src/text_3d/text_3d.cpp index 5d56dbd..2b26986 100644 --- a/src/text_3d/text_3d.cpp +++ b/src/text_3d/text_3d.cpp @@ -261,7 +261,8 @@ Text3d::draw(GlRenderer& gl, const Camera& camera, const Light& light) const for (int i = 0; i < SPARKLE_TRAIL; ++i) sparkle_trail[i] = pos; sparkle_trail_count = SPARKLE_TRAIL; sparkle_last_ms = now; - } else if (now - sparkle_last_ms >= 33) { // ~30 Hz + } + else if (now - sparkle_last_ms >= 33) { // ~30 Hz for (int i = SPARKLE_TRAIL - 1; i > 0; --i) sparkle_trail[i] = sparkle_trail[i - 1]; sparkle_trail[0] = pos; diff --git a/src/text_3d/text_label.cpp b/src/text_3d/text_label.cpp index 46aed0b..69f23ef 100644 --- a/src/text_3d/text_label.cpp +++ b/src/text_3d/text_label.cpp @@ -38,11 +38,11 @@ TextLabel::set_Text(const std::string& text, void TextLabel::clear() { - _words.clear(); - _offsets.clear(); - _anchor_pos.clear(); - _jiggle_off.clear(); - _jiggle_vel.clear(); + _words .clear(); + _offsets .clear(); + _anchor_pos .clear(); + _jiggle_off .clear(); + _jiggle_vel .clear(); _current_text.clear(); } @@ -160,48 +160,26 @@ TextLabel::update(double dt, const Camera& camera) { if (_words.empty()) return; - Vec3 centre = current_Centre(); - - // ── Compute orientation ───────────────────────────────────────────── - // - // Billboard mode: the line of glyphs sits on the horizontal `right` - // axis (no positional tilt), and each glyph is rotated to face the - // camera with both yaw (around world Y) AND pitch (after yaw, around - // the rotated X axis). Because every glyph shares the same yaw+pitch, - // the whole line reads as a single rigid plane facing the player. - // - // Non-billboard: fixed yaw, zero pitch. - - Vec3 right{1, 0, 0}; + Vec3 centre = current_Centre(); + Vec3 right {1, 0, 0}; float yaw_for_glyphs = 0.0f; float pitch_for_glyphs = 0.0f; if (_billboard) { - Vec3 forward = camera.pos - centre; - double flen = length(forward); + Vec3 forward = camera.pos - centre; + double flen = Math::length(forward); if (flen < 0.0001) return; + forward = forward / flen; - // right = normalize(world_up × forward). With world_up = (0,1,0) - // this is always horizontal (right.y == 0), so the line of glyphs - // is a level horizontal sweep. The vertical "tilt" of the label - // comes from each glyph's pitch, not from the line geometry. - right = cross(Vec3{0, 1, 0}, forward); - double rlen = length(right); + right = Math::cross(Vec3{0, 1, 0}, forward); + double rlen = Math::length(right); if (rlen < 0.0001) return; right = right / rlen; - // Yaw: turn around world Y so the glyph's local -Z (its face - // normal) points back toward the camera horizontally. yaw_for_glyphs = SDL_atan2f(static_cast<float>(forward.x), static_cast<float>(forward.z)); - // Pitch: tilt forward/back so the glyph face also points up/down - // toward the camera. forward.y > 0 means the camera is above the - // label, so the label should tilt its top toward the camera. - // Using the 3-arg buildModelMat4 (T · Ry · Rx), positive pitch - // tilts the local +Y toward local -Z (i.e. forward), which is - // what we want when the camera is above. pitch_for_glyphs = -SDL_asinf(static_cast<float>(forward.y)); } else { yaw_for_glyphs = _face_yaw; @@ -209,14 +187,12 @@ TextLabel::update(double dt, const Camera& camera) right = { SDL_cos(_face_yaw), 0.0, -SDL_sin(_face_yaw) }; } - // ── Place each word at anchor + jiggle, apply shared orientation ──── for (size_t i = 0; i < _words.size(); ++i) { Text3d& t = *_words[i]; if (!t.loaded) continue; double off = _offsets[i]; - // Line stays on the horizontal `right` axis through centre. Vec3 anchor{ centre.x + right.x * off, centre.y + right.y * off, @@ -224,7 +200,6 @@ TextLabel::update(double dt, const Camera& camera) }; _anchor_pos[i] = anchor; - // Spring-damp the jiggle Vec3& off3 = _jiggle_off[i]; Vec3& vel3 = _jiggle_vel[i]; diff --git a/src/text_3d/text_mesh.cpp b/src/text_3d/text_mesh.cpp index ec35f5d..a3d8373 100644 --- a/src/text_3d/text_mesh.cpp +++ b/src/text_3d/text_mesh.cpp @@ -196,7 +196,8 @@ void computeVertexNormals(TextMesh& mesh) double len = SDL_sqrt(n.x*n.x + n.y*n.y + n.z*n.z); if (len > 0.000001) { n.x /= len; n.y /= len; n.z /= len; - } else { + } + else { n = {0, 0, -1}; // fallback: face forward } } diff --git a/src/text_3d/vim_buffer.hpp b/src/text_3d/vim_buffer.hpp index 0b292c8..d8f4d39 100644 --- a/src/text_3d/vim_buffer.hpp +++ b/src/text_3d/vim_buffer.hpp @@ -1,13 +1,9 @@ #pragma once -// Dependency-free vim editing model: lines of text, a cursor, modes, and the -// agreed motion/edit subset. No rendering, no game types — GameState drives it -// and reads `lines()`/`row()`/`col()` to render. `version()` bumps on any change -// so the caller knows when to re-render. Signals (Exit/Help/Write) surface -// commands the host must act on. +#include <algorithm> +#include <cstdint> #include <string> #include <vector> -#include <cctype> class VimBuffer { public: @@ -22,7 +18,6 @@ public: _sig = Signal::None; ++_version; } - // ── feed (call in input order) ────────────────────────────────────────── void key_char(char c) { if (_mode == Mode::Insert) { insert_Char(c); } else if (_mode == Mode::Command) { _cmd.push_back(c); touch(); } @@ -45,16 +40,16 @@ public: clampCol(); touch(); } - // ── host accessors ────────────────────────────────────────────────────── - const std::vector<std::string>& lines() const { return _lines; } - int row() const { return _row; } - int col() const { return _col; } - Mode mode() const { return _mode; } - unsigned version() const { return _version; } - bool is_Empty() const { return _lines.size() == 1 && _lines[0].empty(); } - void start_Insert() { _mode = Mode::Insert; _col = 0; touch(); } - const std::string& cmdline() const { return _cmd; } // includes leading ':' - const std::string& status() const { return _status; } + const std::vector<std::string>& + lines() const { return _lines; } + int row() const { return _row; } + int col() const { return _col; } + Mode mode() const { return _mode; } + unsigned version() const { return _version; } + bool is_Empty() const { return _lines.size() == 1 && _lines[0].empty(); } + void start_Insert() { _mode = Mode::Insert; _col = 0; touch(); } + const std::string& cmdline() const { return _cmd; } + const std::string& status() const { return _status; } Signal take_Signal() { Signal s = _sig; _sig = Signal::None; return s; } private: @@ -62,8 +57,8 @@ private: int _row = 0, _col = 0; Mode _mode = Mode::Normal; int _count = 0; - std::string _pending; // "d" or "g" awaiting second key - std::string _cmd; // command-line text (":...") + std::string _pending; // "d" or "g" awaiting second key + std::string _cmd; // command-line text (":...") std::string _status; Signal _sig = Signal::None; unsigned _version = 0; @@ -78,7 +73,6 @@ private: } int eff() const { return _count > 0 ? _count : 1; } - // ── Insert-mode edits ──────────────────────────────────────────────────── void insert_Char(char c) { std::string& L = _lines[(size_t)_row]; if (_col > (int)L.size()) _col = (int)L.size(); @@ -102,12 +96,12 @@ private: touch(); } - // ── Normal-mode interpreter ─────────────────────────────────────────────── void normal_Key(char c) { if (c == ':') { _mode = Mode::Command; _cmd = ":"; _status.clear(); touch(); return; } if (std::isdigit((unsigned char)c)) { - if (c == '0' && _count == 0 && _pending.empty()) { _col = 0; touch(); return; } // 0 = motion + if (c == '0' && _count == 0 && _pending.empty()) + { _col = 0; touch(); return; } _count = _count * 10 + (c - '0'); return; } @@ -121,21 +115,21 @@ private: } switch (c) { - case 'h': _col -= eff(); break; - case 'l': _col += eff(); break; + case 'h': _col -= eff(); break; + case 'l': _col += eff(); break; case 'j': _row = clampRow(_row + eff()); break; case 'k': _row = clampRow(_row - eff()); break; - case '0': _col = 0; break; + case '0': _col = 0; break; case '$': _col = lineLen() > 0 ? lineLen() - 1 : 0; break; - case 'x': delete_Chars(eff()); break; - case 'i': _mode = Mode::Insert; break; - case 'a': _col = (lineLen() > 0 ? _col + 1 : 0); _mode = Mode::Insert; break; - case 'A': _col = lineLen(); _mode = Mode::Insert; break; - case 'o': open_Below(); _mode = Mode::Insert; break; - case 'O': open_Above(); _mode = Mode::Insert; break; - case 'G': _row = clampRow(_count > 0 ? _count - 1 : (int)_lines.size() - 1); break; - case 'd': _pending = "d"; return; - case 'g': _pending = "g"; return; + case 'x': delete_Chars(eff()); break; + case 'i': _mode = Mode::Insert; break; + case 'a': _col = (lineLen() > 0 ? _col + 1 : 0); _mode = Mode::Insert; break; + case 'A': _col = lineLen(); _mode = Mode::Insert; break; + case 'o': open_Below(); _mode = Mode::Insert; break; + case 'O': open_Above(); _mode = Mode::Insert; break; + case 'G': _row = clampRow(_count > 0 ? _count - 1 : (int)_lines.size() - 1);break; + case 'd': _pending = "d"; return; + case 'g': _pending = "g"; return; default: break; } _count = 0; clampCol(); touch(); @@ -157,7 +151,6 @@ private: void open_Below() { _lines.insert(_lines.begin() + _row + 1, ""); ++_row; _col = 0; } void open_Above() { _lines.insert(_lines.begin() + _row, ""); _col = 0; } - // ── command line ────────────────────────────────────────────────────────── void run_Command() { std::string c = _cmd.size() ? _cmd.substr(1) : ""; // strip ':' _cmd.clear(); _mode = Mode::Normal; @@ -167,7 +160,8 @@ private: else if (!c.empty() && std::all_of(c.begin(), c.end(), [](char ch){ return std::isdigit((unsigned char)ch); })) { _row = clampRow(std::stoi(c) - 1); clampCol(); - } else _status = "not a command: " + c; + } + else _status = "not a command: " + c; touch(); } void cancel_Command() { _cmd.clear(); _mode = Mode::Normal; touch(); } diff --git a/src/utils/carry_math.cpp b/src/utils/carry_math.cpp index 234eac6..ddbe43f 100644 --- a/src/utils/carry_math.cpp +++ b/src/utils/carry_math.cpp @@ -6,10 +6,8 @@ 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); + const Vec3 up = Math::cross(camera.right, camera.fwd); return { camera.pos.x + camera.fwd.x * carry_dist @@ -23,10 +21,10 @@ CarryMath::line_Anchor(const Camera& camera, double carry_dist) DynArray<Vec3> -CarryMath::slot_positions(const CarriedLine& carried, - const Vec3& anchor, - const Vec3& right, - double carry_gap) +CarryMath::slot_Positions(const CarriedLine& carried, + const Vec3& anchor, + const Vec3& right, + double carry_gap) { DynArray<Vec3> positions; DynArray<CarriedWordNode*> nodes = carried.reverse_nodes(); @@ -45,7 +43,7 @@ CarryMath::slot_positions(const CarriedLine& carried, }); if (i + 1 < nodes.size() && nodes[i + 1]->word) { - double a = 0.5, b = 0.5; // default fallback + double a = 0.5, b = 0.5; if (nodes[i]->word->type == ObjectType::Text) a = static_cast<const Text3d&>(*nodes[i]->word).half_extent; diff --git a/src/utils/carry_math.hpp b/src/utils/carry_math.hpp index 2630ec0..fbeee20 100644 --- a/src/utils/carry_math.hpp +++ b/src/utils/carry_math.hpp @@ -22,7 +22,7 @@ namespace CarryMath Vec3 line_Anchor(const Camera& camera, double carry_dist); - DynArray<Vec3> slot_positions(const CarriedLine& carried, + DynArray<Vec3> slot_Positions(const CarriedLine& carried, const Vec3& anchor, const Vec3& right, double carry_gap); diff --git a/src/utils/math.hpp b/src/utils/math.hpp index 1ebe461..0534458 100644 --- a/src/utils/math.hpp +++ b/src/utils/math.hpp @@ -37,6 +37,8 @@ inline Vec3 operator-(Vec3 a, const Vec3& b) { return a -= b; } inline Vec3 operator*(Vec3 v, double s) { return v *= s; } inline Vec3 operator/(Vec3 v, double s) { return v /= s; } +namespace Math +{ inline double dot(const Vec3& a, const Vec3& b) @@ -82,3 +84,5 @@ lerp(double a, double b, double t) inline Vec3 lerp(const Vec3& a, const Vec3& b, double t) { return { lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t) }; } + +} // namespace diff --git a/src/utils/text_wrap.hpp b/src/utils/text_wrap.hpp index 93863a1..e75c9d8 100644 --- a/src/utils/text_wrap.hpp +++ b/src/utils/text_wrap.hpp @@ -44,9 +44,11 @@ wrap_Text(const std::string& text, size_t max_chars) if (c == '\n') { flush_word(); flush_line(); - } else if (c == ' ' || c == '\t') { + } + else if (c == ' ' || c == '\t') { flush_word(); - } else { + } + else { word += c; } }