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 restructore and co
benchmark/bench_physics.cpp | 3 +- src/states/game_state.cpp | 630 +++++++++------------------------------- src/states/game_state.hpp | 48 +-- src/systems/freelook_system.cpp | 34 +-- src/systems/freelook_system.hpp | 33 +-- src/systems/wall_connection.cpp | 342 ++++++++++++++++++++++ src/systems/wall_connection.hpp | 63 ++++ src/wall/text_wall_scroll.hpp | 18 +- 8 files changed, 568 insertions(+), 603 deletions(-) diff --git a/benchmark/bench_physics.cpp b/benchmark/bench_physics.cpp index 77b935f..c9ec5a5 100644 --- a/benchmark/bench_physics.cpp +++ b/benchmark/bench_physics.cpp @@ -27,11 +27,10 @@ static void BM_PhysicsUpdateObjects(benchmark::State& state) } PhysicsSystem sys; - constexpr float ws = 16.0f; constexpr double dt = 1.0 / 60.0; for (auto _ : state) { - sys.update_Objects(objects, dt, 25.0, ws); + sys.update_Objects(objects, dt, 25.0); } state.SetLabel("N=" + std::to_string(N)); diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 0bb6d94..93dab8d 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -98,68 +98,45 @@ GameState::debug_SpawnWords([[maybe_unused]]size_t n) -std::tuple<Vec3, Vec3, double> -GameState::anchorframe_Current() const -{ - const FreelookState& fs = _freelook_sys->state; - - // Frozen in place while freelook is actively held. - if (fs.active) - return { fs.frozen_anchor, fs.frozen_right, fs.frozen_glyph_yaw }; - - // Live carry frame, in front of wherever the camera is looking now. - const Vec3 live_anchor = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST); - const Vec3 live_right = _ctx.camera->right; - const double live_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); - - // Returning: ease the whole carry frame from the frozen spot back to the - // live one. The line flies to the player; the camera is never moved. - if (fs.lerping) { - double t = fs.lerp_t; - t = (t < 0.0) ? 0.0 : (t > 1.0 ? 1.0 : t); - const double s = t * t * (3.0 - 2.0 * t); // smoothstep - - auto lerp3 = [s](const Vec3& a, const Vec3& b) { - return Vec3{ a.x + (b.x - a.x) * s, - a.y + (b.y - a.y) * s, - a.z + (b.z - a.z) * s }; - }; - - Vec3 anchor = lerp3(fs.frozen_anchor, live_anchor); - Vec3 right = lerp3(fs.frozen_right, live_right); - const double rl = SDL_sqrt(right.x*right.x + right.y*right.y + right.z*right.z); - if (rl > 1e-9) { right.x /= rl; right.y /= rl; right.z /= rl; } - - // Shortest-path angular interpolation for the glyph yaw. - double dyaw = live_yaw - fs.frozen_glyph_yaw; - while (dyaw > SDL_PI_D) dyaw -= 2.0 * SDL_PI_D; - while (dyaw < -SDL_PI_D) dyaw += 2.0 * SDL_PI_D; - const double yaw = fs.frozen_glyph_yaw + dyaw * s; - - return { anchor, right, yaw }; - } - - return { live_anchor, live_right, live_yaw }; -} - - void GameState::carriedline_Update() { if (_carried.empty()) return; - auto [anchor, right, glyph_yaw] = anchorframe_Current(); - - DynArray<Vec3> - positions = CarryMath::slot_positions(_carried, anchor, right, CarryMath::CARRY_GAP); - DynArray<CarriedWordNode*> - nodes = _carried.reverse_nodes(); + const FreelookState& fs = _freelook_sys->state; - for (size_t i = 0; i < nodes.size() && i < positions.size(); ++i) { - if (nodes[i]->word) { - nodes[i]->word->pos = positions[i]; - nodes[i]->word->yaw = static_cast<float>(glyph_yaw); + // 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<CarriedWordNode*> nodes = _carried.reverse_nodes(); + + // Easing back from the frozen spots: lerp each word from its release + // position toward its live slot. Yaw snaps to live (cheap, reads fine). + const bool easing = fs.lerping && !_freelook_return_from.empty(); + double s = 1.0; + if (easing) { + double t = fs.lerp_t; t = (t < 0.0) ? 0.0 : (t > 1.0 ? 1.0 : t); + s = t * t * (3.0 - 2.0 * t); // smoothstep + } + + for (size_t i = 0; i < nodes.size() && i < slots.size(); ++i) { + if (!nodes[i]->word) continue; + Vec3 target = slots[i]; + if (easing && i < _freelook_return_from.size()) { + const Vec3& from = _freelook_return_from[i]; + 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 { + nodes[i]->word->pos = target; } + nodes[i]->word->yaw = static_cast<float>(glyph_yaw); } } @@ -169,29 +146,20 @@ GameState::pult_DispatchAction(PultAction act) { _step_ledger.buttons_pressed.push_back(act); - const double floor_y = World::get_FloorY<double>(); - switch (act) { - case PultAction::Compile: - on_Compile(); - break; - case PultAction::RunProgram: - on_Run(); - break; - case PultAction::ScrollEditorUp: - TextWallScroll::scroll_Up(_ctx.wall_texts, Wall::Role::Editor); - _editor_wall.scroll(ConfigWall::scroll_step); - break; - case PultAction::ScrollEditorDown: - TextWallScroll::scroll_Down(_ctx.wall_texts, Wall::Role::Editor); - _editor_wall.scroll(-ConfigWall::scroll_step); - break; - case PultAction::ClearEditor: - TextWallScroll::clear_ToFloor(_ctx.wall_texts, _ctx.words, Wall::Role::Editor, floor_y); - _editor_wall.clear(); - break; - case PultAction::ConnectEditor: connect_BeginEditor(); break; - case PultAction::ConnectProgram: connect_Begin(Conn::Program); break; + case PultAction::Compile: on_Compile(); break; + case PultAction::RunProgram: on_Run(); break; + case PultAction::ScrollEditorUp: TextWallScroll::scroll_Up(_ctx.wall_texts, Wall::Role::Editor); + _editor_wall.scroll(ConfigWall::scroll_step); + break; + case PultAction::ScrollEditorDown: TextWallScroll::scroll_Down(_ctx.wall_texts, Wall::Role::Editor); + _editor_wall.scroll(-ConfigWall::scroll_step); + break; + case PultAction::ClearEditor: TextWallScroll::clear_ToFloor(_ctx.wall_texts, _ctx.words, Wall::Role::Editor); + _editor_wall.clear(); + break; + case PultAction::ConnectEditor: wallconnect_BeginEditor(); break; + case PultAction::ConnectProgram: wallconnect_BeginProgram(); break; } } @@ -380,17 +348,19 @@ GameState::on_Jump() void GameState::on_FreelookStart() { - Vec3 anchor = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST); - double yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); - _freelook_sys->begin(anchor, _ctx.camera->right, yaw); + _freelook_sys->begin(); } - void GameState::on_FreelookEnd() { - if (_freelook_sys->state.active) - _freelook_sys->end(); + if (!_freelook_sys->state.active) return; + + _freelook_return_from.clear(); + for (CarriedWordNode* node : _carried.reverse_nodes()) + if (node->word) _freelook_return_from.push_back(node->word->pos); + + _freelook_sys->end(); } void @@ -613,19 +583,28 @@ GameState::handle_Input(FrameInput& input, double dt) { case InputCommand::TypingCommit: on_TypingCommit(input); break; case InputCommand::TypingCancel: on_TypingCancel(input); break; case InputCommand::SandboxRestore: sandbox_Restore(); break; - case InputCommand::ConnectPultSubmit: conn_Submit(); break; - case InputCommand::ConnectPultExit: connect_End(); break; + case InputCommand::ConnectPultSubmit: on_WallConnectSubmit(); break; + case InputCommand::ConnectPultExit: on_WallConnectExit(); break; } } - if (input.capture_vim) { - editor_FeedVim(input.vim_keys); + if (_wall_connection.target() == WallConnection::Target::Editor) { + WallConnection::Request req = _wall_connection.feed_Vim(input.vim_keys, _ctx.camera->pos.y); input.vim_keys.clear(); - } else if (input.capture_typing) { - if (input.typing_buffer != _conn_last) { conn_RebuildPreview(); _conn_last = input.typing_buffer; } + if (req == WallConnection::Request::Exit) { + editor_CommitToWall(_wall_connection.lines()); + _wall_connection.end(); + _frame_input.capture_vim = false; + } 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) { + _wall_connection.set_ProgramBuffer(input.typing_buffer); } 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; } } + if (_pick_cooldown > 0.0) _pick_cooldown -= dt; if (input.pick && _pick_cooldown <= 0.0) { if (on_Pick()) _pick_cooldown = 0.15; } @@ -683,7 +662,7 @@ GameState::update(double dt) _plate_sys-> update(dt, *_ctx.player, _step_plates); _editor_wall.update(dt, _ctx.camera->pos); - if (_conn != Conn::None) _conn_blink += dt; + _wall_connection.update(dt); } @@ -767,17 +746,7 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) } { FP_ZONE("render.wall_edit"); - if (_conn == Conn::Program) { - if (_conn_input) _conn_input->draw(renderer, *_ctx.camera, *_ctx.light); - conn_DrawCursor(renderer); - conn_DrawElectric(renderer); - } else if (_conn == Conn::Editor) { - for (const Text3dPtr& t : _edit_render) if (t) t->draw(renderer, *_ctx.camera, *_ctx.light); - for (const Text3dPtr& n : _edit_linenums) if (n) n->draw(renderer, *_ctx.camera, *_ctx.light); - if (_edit_cmdline) _edit_cmdline->draw(renderer, *_ctx.camera, *_ctx.light); - edit_DrawCursor(renderer); - conn_DrawElectric(renderer); - } + _wall_connection.render(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render._carried"); @@ -961,7 +930,7 @@ GameState::update_WorldSimulation(double dt) { _ctx.player->prev_pos = _ctx.player->pos; - { FP_ZONE("subst.phys_player"); + { FP_ZONE("updateworldsim.phys_player"); _physics_sys->update_Player(*_ctx.player, _ctx.player->get_AccX(), _ctx.player->get_AccZ(), @@ -973,11 +942,11 @@ GameState::update_WorldSimulation(double dt) for (const GameObjectPtr& p : _ctx.scenery) if (p) colliders.push_back(p.get()); for (const Text3dPtr& p : _ctx.words) if (p) colliders.push_back(p.get()); - { FP_ZONE("subst.phys_objects"); + { FP_ZONE("updateworldsim.phys_objects"); _physics_sys->update_Objects(colliders, dt, 25.0); } - { FP_ZONE("subst.word_ai"); + { FP_ZONE("updateworldsim.word_ai"); DynArray<Text3dPtr> ai_dropped; _word_ai_sys->update(_ctx.words, *_ctx.player, _carried, ai_dropped, dt); for (Text3dPtr& d : ai_dropped) { @@ -985,14 +954,14 @@ GameState::update_WorldSimulation(double dt) } } - { FP_ZONE("subst.phys_projectiles"); + { FP_ZONE("updateworldsim.phys_projectiles"); _physics_sys->update_Projectiles(_projectiles, dt, _ctx.words); } DynArray<ProjectileContactEvent> projectile_contacts; DynArray<TextLabel*> all_labels = _pult_sys->get_Labels(); - { FP_ZONE("subst.hint_and_labels"); + { FP_ZONE("updateworldsim.hint_and_labels"); _hint_sys->update(dt, *_ctx.camera); for (TextLabel* lbl : all_labels) lbl->update(dt, *_ctx.camera); @@ -1002,12 +971,12 @@ GameState::update_WorldSimulation(double dt) all_labels.push_back(&wl.label); } - { FP_ZONE("subst.resolve_projectiles"); + { FP_ZONE("updateworldsim.resolve_projectiles"); _physics_sys->resolve_Projectiles(_projectiles, &colliders, &all_labels, &projectile_contacts); } DynArray<WallTextHitTest> wall_text_hits; - { FP_ZONE("subst.build_wall_hits"); + { FP_ZONE("updateworldsim.build_wall_hits"); wall_text_hits.reserve(_ctx.wall_texts.size()); for (size_t i = 0; i < _ctx.wall_texts.size(); ++i) { const TextWallPtr& wt = _ctx.wall_texts[i]; @@ -1040,7 +1009,7 @@ GameState::update_WorldSimulation(double dt) } DynArray<ProjectileResult> proj_results; - { FP_ZONE("subst.projectile_sys"); + { FP_ZONE("updateworldsim.projectile_sys"); _projectiles_sys->update(_projectiles, proj_results, wall_text_hits, dt, _program_sys->is_Running()); @@ -1048,7 +1017,7 @@ GameState::update_WorldSimulation(double dt) DynArray<size_t> wall_text_to_erase; - { FP_ZONE("subst.projectile_land"); + { FP_ZONE("updateworldsim.projectile_land"); for (ProjectileResult& result : proj_results) { switch (result.landing) { case ProjectileLanding::Floor: projectile_LandOnFloor (result); break; @@ -1087,7 +1056,7 @@ GameState::update_WorldSimulation(double dt) } } - { FP_ZONE("subst.player_collision"); + { FP_ZONE("updateworldsim.player_collision"); PlayerCollision col = _ctx.world->resolve_Player(*_ctx.player); _ctx.player->apply_Collision(col); @@ -1146,7 +1115,7 @@ GameState::level_TryAdvance() if (!_level_runner_sys || _level_runner_sys->complete()) return false; - auto [anchor, right, glyph_yaw] = anchorframe_Current(); + double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); _celebration_sys->start(*_ctx.camera, glyph_yaw); return false; } @@ -1241,49 +1210,19 @@ GameState::sandbox_LoadFileToWall() std::string src; if (!SourceTokens::read_File(path, src)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Sandbox: cannot read %s", path.c_str()); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Sandbox: cannot read %s", path.c_str()); return; } - // Clear any existing editor-wall words first. - _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; }), - _ctx.wall_texts.end()); - - const float ws = World::get_BoxScale(); - const double x_wall = -static_cast<double>(ws) + 0.05; // NegX wall surface - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); - - auto lines = SourceTokens::tokenize_Lines(src); - - // Top line highest; step down by the editor's line height. - double y = static_cast<double>(ws) - 2.0; // start near top, tweak to taste - const double z_start = static_cast<double>(ws) - 1.0; // leftmost (large z) - - for (const auto& toks : lines) { - double z = z_start; - for (const std::string& tk : toks) { - Text3dPtr t = make_Text3d(tk, Text3d::depth_wall, 0.003f, - Vec3{ x_wall, y, z }, ConfigColor::wall); - t->yaw = tf.yaw; + editor_Clear(); - // Snap into the editor wall's line tracking, like a thrown word. - Vec3 snapped = _editor_wall.on_WordPlaced(t.get(), Vec3{ x_wall, y, z }); - t->pos = snapped; - - _ctx.wall_texts.push_back(cpputils::make_unique<TextWall>(TextWall{ - Wall::Position::NegX, Wall::Role::Editor, std::move(t) - })); - - // Advance left for the next token. on_WordPlaced already spaced - // it; nudge our cursor past where it landed. - z = snapped.z - 0.5; // coarse step; place_OnLine handles exact gaps - } - y -= ConfigWall::line_height; + DynArray<std::string> lines; + for (const auto& toks : SourceTokens::tokenize_Lines(src)) { + std::string joined; + for (const std::string& tk : toks) { if (!joined.empty()) joined += ' '; joined += tk; } + lines.push_back(joined); } + editor_PlaceLines(lines); } @@ -1297,10 +1236,10 @@ void GameState::sandbox_Restore() { void GameState::typing_RebuildPreview(const std::string& s) { - // Remove the previous preview words from the carry line. typing_ClearPreview(); - auto [anchor, right, glyph_yaw] = anchorframe_Current(); + 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; while (in >> tok) { @@ -1308,7 +1247,7 @@ GameState::typing_RebuildPreview(const std::string& s) anchor, ConfigColor::floor); t->yaw = static_cast<float>(glyph_yaw); t->body.mode = PhysicsMode::Carried; - _typing_words.push_back(t.get()); // track raw ptr for later removal + _typing_words.push_back(t.get()); _carried.push(std::move(t)); } } @@ -1317,11 +1256,9 @@ GameState::typing_RebuildPreview(const std::string& s) void GameState::typing_ClearPreview() { - // Preview words are the most recently pushed → at the tail. Pop exactly - // as many as we added; let them be destroyed (don't re-add to scene). for (size_t i = 0; i < _typing_words.size(); ++i) { if (_carried.empty()) break; - _carried.pop_back(); // returns Text3dPtr; dropping it frees the word + _carried.pop_back(); } _typing_words.clear(); } @@ -1341,35 +1278,42 @@ GameState::carry_BeginAnim(Text3dPtr word, bool with_pitch) anim.t = 0.0; anim.complete = false; - auto [anchor, right, glyph_yaw] = anchorframe_Current(); - anim.target_pos = anchor; - anim.yaw_target = static_cast<float>(glyph_yaw); + 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); + anim.target_pos = anchor; + anim.yaw_target = static_cast<float>(glyph_yaw); _anims_add.push_back(std::move(anim)); } void -GameState::connect_Begin(Conn c) +GameState::wallconnect_BeginProgram() { - if (c != Conn::Program) return; // Editor connect is Step 2 - _conn = c; - _conn_pult_pos = _pult_sys->get_ConnectProgramPos(); _frame_input.capture_typing = true; _frame_input.typing_buffer.clear(); - _conn_last.clear(); - _conn_input.reset(); + _wall_connection.begin_Program(_pult_sys->get_ConnectProgramPos()); } + void -GameState::conn_Submit() +GameState::wallconnect_BeginEditor() { - const std::string line = _frame_input.typing_buffer; + 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); +} + + +void +GameState::on_WallConnectSubmit() +{ + std::string line = _frame_input.typing_buffer; _frame_input.typing_buffer.clear(); - _conn_last.clear(); - _conn_input.reset(); + _wall_connection.clear_ProgramPreview(); - if (line == ":exit") { connect_End(); return; } + if (line == ":exit") { on_WallConnectExit(); return; } if (line == ":help") { for (const char* h : { ":exit leave the connection", "ctrl+WASD move while connected", @@ -1377,107 +1321,35 @@ GameState::conn_Submit() _program_sys->echo_Input(_ctx.wall_texts, h); return; } - _program_sys->feed_Input(line); _program_sys->echo_Input(_ctx.wall_texts, line); } -void -GameState::conn_RebuildPreview() -{ - Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, World::get_BoxScale()); - Vec3 pos = tf.position; - pos.y = ProgramSystem::start_y; - - _conn_input = make_Text3d("> " + _frame_input.typing_buffer, - Text3d::depth_wall, 0.003f, pos, ConfigColor::white); - _conn_input->yaw = tf.yaw; -} void -GameState::conn_DrawCursor(GlRenderer& gl) +GameState::on_WallConnectExit() { - if (!_conn_input) return; - if (SDL_fmod(_conn_blink, 0.8) > 0.4) return; - - Vec3 c = _conn_input->pos; - c.z = _conn_input->pos.z - (static_cast<double>(_conn_input->half_extent) + 0.15); - - Mat4 view = buildViewMat4(*_ctx.camera); - Mat4 model = buildModelMat4(c, _conn_input->yaw); - Mat4 mvp = Mat4::mul(gl.projection(), Mat4::mul(view, model)); - - gl.set_Wireframe(false); - gl.set_MVP(model, mvp); - gl.set_NormalMat(model); - gl.set_Light(*_ctx.light); - gl.set_Color(0.6f, 1.0f, 1.0f); - gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f); - gl.draw_Box(0.04f, 0.22f, 0.02f); - gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f); -} - -void -GameState::conn_DrawElectric(GlRenderer& gl) -{ - const Vec3 a = _ctx.camera->pos; - const Vec3 b = _conn_pult_pos; - - constexpr int SEG = 16; - DynArray<float> pts; - pts.reserve(SEG * 6); - Vec3 prev = a; - for (int i = 1; i <= SEG; ++i) { - const double t = static_cast<double>(i) / SEG; - Vec3 p { a.x + (b.x - a.x) * t, - a.y + (b.y - a.y) * t, - a.z + (b.z - a.z) * t }; - if (i < SEG) { // jitter the interior for the spark look - const double j = 0.18; - p.x += ((rand() % 1000) / 1000.0 - 0.5) * j; - p.y += ((rand() % 1000) / 1000.0 - 0.5) * j; - p.z += ((rand() % 1000) / 1000.0 - 0.5) * j; - } - pts.push_back((float)prev.x); pts.push_back((float)prev.y); pts.push_back((float)prev.z); - pts.push_back((float)p.x); pts.push_back((float)p.y); pts.push_back((float)p.z); - prev = p; - } - - Mat4 ident = Mat4::identity(); - Mat4 mvp = Mat4::mul(gl.projection(), buildViewMat4(*_ctx.camera)); - gl.set_MVP(ident, mvp); - gl.set_NormalMat(ident); - gl.set_Wireframe(true); // wireframe path emits u_Color directly - gl.set_Color(0.5f, 0.9f, 1.0f); - gl.draw_Lines(pts.data(), static_cast<int>(pts.size()) / 3); - gl.set_Wireframe(false); + _wall_connection.end(); + _frame_input.capture_typing = false; + _frame_input.typing_buffer.clear(); } void -GameState::connect_BeginEditor() +GameState::editor_Clear() { - _conn = Conn::Editor; - _conn_pult_pos = _pult_sys->get_ConnectEditorPos(); - _frame_input.capture_vim = true; - _edit_status.clear(); - - editor_Absorb(); // loads _vim, removes real wall words - - if (_vim.is_Empty()) - _vim.start_Insert(); // empty wall → fresh line, ready to type - // else: load() left cursor at row 0 / col 0 in Normal mode - - _vim_seen_version = _vim.version(); - editor_RebuildRender(); + _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; }), + _ctx.wall_texts.end()); } -void -GameState::editor_Absorb() + +DynArray<std::string> +GameState::editor_AbsorbLines() { - // Pull existing editor source (lines joined by spaces, '\n' separated). std::string src = TextWall::extract_Text(_ctx.wall_texts, Wall::Role::Editor); - DynArray<std::string> lines; std::string cur; for (char ch : src) { @@ -1486,246 +1358,8 @@ GameState::editor_Absorb() } if (!cur.empty()) lines.push_back(cur); - // VimBuffer wants std::vector; convert (DynArray ↔ vector). - std::vector<std::string> v(lines.begin(), lines.end()); - _vim.load(v); - - // Remove the real editor words — the buffer is the source of truth now. - _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; }), - _ctx.wall_texts.end()); -} - - -void -GameState::editor_RebuildRender() -{ - _edit_render.clear(); - _edit_linenums.clear(); - - const float ws = World::get_BoxScale(); - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); - const double lh = ConfigWall::line_height; - const double eye_y = _ctx.camera->pos.y; - const int R = _vim.row(); - const int N = (int)_vim.lines().size(); - const double bottom_y = -static_cast<double>(ws) + 1.2; - - const double anchor_cursor = eye_y + static_cast<double>(R) * lh; - const double anchor_bottom = bottom_y + static_cast<double>(N - 1) * lh; - _edit_row0_y = SDL_min(anchor_cursor, anchor_bottom); - - const auto& lines = _vim.lines(); - - // ── Pass 1: build the renderables in a LOCAL left-aligned frame ────────── - // u = 0 at block's left edge, increasing rightward. We track each line's - // text width to find the block width (gutter + widest line). - struct Row { Text3dPtr num; Text3dPtr txt; double txt_w; }; - DynArray<Row> rows; - rows.reserve(lines.size()); - - // Gutter width: built from the widest line-number ("N"), plus a gap. - // Build numbers first to measure. - double gutter_w = 0.0; - for (size_t i = 0; i < lines.size(); ++i) { - Text3dPtr num = make_Text3d(std::to_string(i + 1), Text3d::depth_wall, 0.0022f, - tf.position, ConfigColor::editor); - num->yaw = tf.yaw; - gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(num->half_extent)); - Row r; r.num = std::move(num); rows.push_back(std::move(r)); - } - const double gutter_gap = 0.6; // space between number and code - const double code_u0 = gutter_w + gutter_gap; // code's left edge in u - - double block_w = code_u0; // at least the gutter; grows with widest line - for (size_t i = 0; i < lines.size(); ++i) { - const bool drawable = lines[i].find_first_not_of(' ') != std::string::npos; - if (drawable) { - Text3dPtr t = make_Text3d(lines[i], Text3d::depth_wall, 0.003f, - tf.position, ConfigColor::wall); - t->yaw = tf.yaw; - rows[i].txt_w = 2.0 * static_cast<double>(t->half_extent); - rows[i].txt = std::move(t); - } else { - rows[i].txt_w = 0.0; - } - block_w = SDL_max(block_w, code_u0 + rows[i].txt_w); - } - - // ── Pass 2: center the block on the wall (z = 0) and emit ──────────────── - // Block spans u ∈ [0, block_w]; we want it centered, so its world left edge - // (largest z) is at z = block_w/2, and z = z_left - u. - const double z_block_left = block_w * 0.5; - - for (size_t i = 0; i < rows.size(); ++i) { - const double row_y = _edit_row0_y - static_cast<double>(i) * lh; - - // number: left-aligned at u = 0 → its left edge sits at z_block_left - if (rows[i].num) { - const double he = static_cast<double>(rows[i].num->half_extent); - rows[i].num->pos = tf.position; - rows[i].num->pos.y = row_y; - rows[i].num->pos.z = z_block_left - he; // left edge at u=0, center inward - _edit_linenums.push_back(std::move(rows[i].num)); - } - - // code: left edge at u = code_u0 - if (rows[i].txt) { - const double he = static_cast<double>(rows[i].txt->half_extent); - rows[i].txt->pos = tf.position; - rows[i].txt->pos.y = row_y; - rows[i].txt->pos.z = z_block_left - code_u0 - he; // left-aligned within block - _edit_render.push_back(std::move(rows[i].txt)); - } else { - _edit_render.push_back(Text3dPtr{}); // keep row index alignment - } - } - - // stash for the cursor (which needs the code column's world left edge) - _edit_code_left_z = z_block_left - code_u0; - - editor_RebuildCmdline(); -} - - -void -GameState::editor_RebuildCmdline() -{ - _edit_cmdline.reset(); - - std::string text; - if (_vim.mode() == VimBuffer::Mode::Command) text = _vim.cmdline(); // includes ':' - else if (!_edit_status.empty()) text = _edit_status[0]; // help/first status line - else if (!_vim.status().empty()) text = _vim.status(); - else { - switch (_vim.mode()) { - case VimBuffer::Mode::Insert: text = "-- INSERT --"; break; - default: text = "-- NORMAL --"; break; - } - } - if (text.empty()) return; - - const float ws = World::get_BoxScale(); - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); - - Vec3 pos = tf.position; - pos.x += 0.8; // pull toward room center (off the wall, in front) - pos.y = -static_cast<double>(ws) + 0.8; // fixed near the floor — always visible - pos.z = static_cast<double>(ws) - 1.0; // left-aligned with the gutter - - _edit_cmdline = make_Text3d(text, Text3d::depth_wall, 0.0028f, pos, ConfigColor::white); - _edit_cmdline->yaw = tf.yaw; - _edit_cmdline->pos.z = 0.0; -} - - -void -GameState::editor_FeedVim(DynArray<int>& keys) -{ - for (int k : keys) { - if (k == -1) _vim.key_enter(); - else if (k == -2) _vim.key_escape(); - else if (k == -3) _vim.key_backspace(); - else if (k > 0) _vim.key_char(static_cast<char>(k)); - } - - switch (_vim.take_Signal()) { - case VimBuffer::Signal::Exit: editor_CommitToWall(); connect_End(); return; - case VimBuffer::Signal::Write: editor_CommitToWall(); editor_Absorb(); - _vim_seen_version = _vim.version() - 1; break; - case VimBuffer::Signal::Help: - _edit_status.clear(); - _edit_status.push_back("-- VIM HELP --"); - _edit_status.push_back("i a A o O insert Esc normal h j k l move"); - _edit_status.push_back("0 $ line ends gg G :N jump line x dd delete"); - _edit_status.push_back(":w write to wall :exit leave ctrl+WASD move"); - break; - default: break; - } - - if (_vim.version() != _vim_seen_version) { - _vim_seen_version = _vim.version(); - editor_RebuildRender(); - } -} - -void -GameState::editor_CommitToWall() -{ - // buffer lines → real editor-wall words (throwing/compile/scroll work again) - DynArray<std::string> lines(_vim.lines().begin(), _vim.lines().end()); - editor_PlaceLines(lines); - _edit_render.clear(); - _edit_status.clear(); -} - -void -GameState::connect_End() // editor-aware version (replaces the Step-1 one) -{ - if (_conn == Conn::Editor) { - _frame_input.capture_vim = false; - _frame_input.vim_keys.clear(); - _edit_render.clear(); - _edit_linenums.clear(); - _edit_cmdline.reset(); - _edit_status.clear(); - } else { - _frame_input.capture_typing = false; - _frame_input.typing_buffer.clear(); - _conn_last.clear(); - _conn_input.reset(); - } - _conn = Conn::None; -} - -void -GameState::edit_DrawCursor(GlRenderer& gl) -{ - if (_conn != Conn::Editor) return; - if (SDL_fmod(_conn_blink, 0.8) > 0.4) return; - - const int row = _vim.row(); - if (row < 0 || row >= (int)_vim.lines().size()) return; - - const float ws = World::get_BoxScale(); - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); - const double lh = ConfigWall::line_height; - - const std::string& s = _vim.lines()[(size_t)row]; - const double row_y = _edit_row0_y - static_cast<double>(row) * lh; // same formula - - double cw, line_x = tf.position.x, he_y = 0.2; - if (row < (int)_edit_render.size() && _edit_render[(size_t)row]) { - const Text3dPtr& line = _edit_render[(size_t)row]; - const int N = s.empty() ? 1 : (int)s.size(); - 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 { - cw = 0.18; // empty line: nominal advance - } - - const int col = _vim.col(); - // left edge of the code column (set in editor_RebuildRender after centering) - const double zL = _edit_code_left_z; - const double cz = zL - (static_cast<double>(col) + 0.5) * cw; - - Vec3 c { line_x, row_y - (he_y + 0.06), cz }; // underline: just below the glyphs - - Mat4 view = buildViewMat4(*_ctx.camera); - Mat4 model = buildModelMat4(c, tf.yaw); - Mat4 mvp = Mat4::mul(gl.projection(), Mat4::mul(view, model)); - - gl.set_Wireframe(false); - gl.set_MVP(model, mvp); - gl.set_NormalMat(model); - gl.set_Light(*_ctx.light); - gl.set_Color(0.6f, 1.0f, 1.0f); - gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f); - gl.draw_Box(static_cast<float>(cw * 0.45), 0.015f, 0.03f); // wide+thin = underline - gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f); + editor_Clear(); + return lines; } @@ -1756,3 +1390,9 @@ GameState::editor_PlaceLines(const DynArray<std::string>& lines) y -= ConfigWall::line_height; } } + +void +GameState::editor_CommitToWall(const DynArray<std::string>& lines) +{ + editor_PlaceLines(lines); +} diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index 9c7e67e..93961c6 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -23,6 +23,7 @@ #include "systems/program_system.hpp" #include "systems/projectile_system.hpp" #include "systems/pult_system.hpp" +#include "systems/wall_connection.hpp" #include "systems/word_ai_system.hpp" #include "systems/word_anim.hpp" #include "text_3d/vim_buffer.hpp" @@ -71,38 +72,20 @@ class GameState : public IState double _shoot_cooldown { 0.0 }; double _step_age { 0.0 }; - // TODO START: OBJECT ORIENT ME - VimBuffer _vim; - unsigned _vim_seen_version { 0 }; - DynArray<Text3dPtr> _edit_render; - DynArray<std::string> _edit_status; - double _edit_code_left_z { 0.0 }; // world z of the code column's left edge (for cursor) - double _edit_row0_y { 0.0 }; // world-y of buffer row 0 (scroll anchor) - - enum class Conn : uint8_t { None, Program, Editor }; - Conn _conn { Conn::None }; - std::string _conn_last; // last previewed buffer - Text3dPtr _conn_input; // live typed-line preview on the wall - double _conn_blink { 0.0 }; // cursor blink accumulator - Vec3 _conn_pult_pos {}; // anchor for the electric line - void connect_Begin (Conn c); - void connect_End (); - void conn_Submit (); - void conn_RebuildPreview (); - void conn_DrawCursor (GlRenderer& gl); - void conn_DrawElectric (GlRenderer& gl); - void connect_BeginEditor (); - void editor_Absorb (); - void editor_RebuildRender(); - void editor_CommitToWall (); - void editor_FeedVim (DynArray<int>& keys); - void edit_DrawCursor (GlRenderer& gl); + + 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); - void editor_RebuildCmdline(); - DynArray<Text3dPtr> _edit_linenums; // per-row "1,2,3…" labels (left gutter) - Text3dPtr _edit_cmdline; // pinned command/status bar (in front of wall) - // TODO END: OBJECT ORIENT ME + DynArray<Vec3> _freelook_return_from; DynArray<WallLabel> _wall_labels; @@ -131,7 +114,7 @@ class GameState : public IState cpputils::unique_ptr<ParticleSystem> _particle_sys; cpputils::unique_ptr<FloorMandelbrot> _floor_sys; cpputils::unique_ptr<LevelRunner> _level_runner_sys; - cpputils::unique_ptr<DynLoaderSystem> _dyn_loader_sys; + cpputils::unique_ptr<DynLoaderSystem> _dyn_loader_sys; cpputils::unique_ptr<CelebrationSystem>_celebration_sys; @@ -181,9 +164,6 @@ class GameState : public IState void debug_SpawnWords (size_t n); - std::tuple<Vec3, Vec3, double> - anchorframe_Current () const; - void handle_Input (FrameInput&, double dt); void update_PlayerCamera(FrameInput& input, double dt); diff --git a/src/systems/freelook_system.cpp b/src/systems/freelook_system.cpp index d28add3..42e0de5 100644 --- a/src/systems/freelook_system.cpp +++ b/src/systems/freelook_system.cpp @@ -7,21 +7,16 @@ FREELOOK_LERP_DUR = 0.20; void -FreelookSystem::begin(const Vec3& anchor, const Vec3& right, double glyph_yaw) +FreelookSystem::begin() { - state.active = true; - state.lerping = false; - state.frozen_anchor = anchor; - state.frozen_right = right; - state.frozen_glyph_yaw = glyph_yaw; + state.active = true; + state.lerping = false; } void FreelookSystem::end() { - // Hand control of the carry anchor back to the live camera frame and let - // the line ease there. The camera angle is NOT changed. state.active = false; state.lerping = true; state.lerp_t = 0.0; @@ -39,26 +34,3 @@ FreelookSystem::update(double dt) state.lerping = false; } } - - -void -FreelookSystem::look_At(const Vec3& eye, const Vec3& target, - uint32_t& out_yaw, int32_t& out_pitch) -{ - double dx = target.x - eye.x; - double dy = target.y - eye.y; - double dz = target.z - eye.z; - - double yaw_r = SDL_atan2(-dx, -dz); - double xz_dist = SDL_sqrt(dx*dx + dz*dz); - double pitch_r = SDL_atan2(dy, xz_dist); - - const double bin = SDL_PI_D / 2147483648.0; - - out_yaw = static_cast<uint32_t>(static_cast<int32_t>(yaw_r / bin)); - out_pitch = std::clamp( - static_cast<int32_t>(pitch_r / bin), - INT32_MIN / 2, - INT32_MAX / 2 - ); -} diff --git a/src/systems/freelook_system.hpp b/src/systems/freelook_system.hpp index 1d138e4..facedb5 100644 --- a/src/systems/freelook_system.hpp +++ b/src/systems/freelook_system.hpp @@ -5,37 +5,16 @@ struct FreelookState { bool active{}; - - Vec3 frozen_anchor{}; - Vec3 frozen_right{}; - double frozen_glyph_yaw{}; - - bool lerping{}; - double lerp_t{}; - uint32_t lerp_yaw_start{}; - int32_t lerp_pitch_start{}; - uint32_t lerp_yaw_target{}; - int32_t lerp_pitch_target{}; + bool lerping{}; + double lerp_t{}; }; + class FreelookSystem { public: FreelookState state; - // Call when shift pressed — freezes anchor from current camera+carried state - void - begin(const Vec3& anchor, const Vec3& right, double glyph_yaw); - - // Call when shift released — starts the carried-line fly-back. The camera - // is left exactly where the player is looking. - void - end(); - - // Call every frame — advances the fly-back timer only. - void - update(double dt); - - static void - look_At(const Vec3& eye, const Vec3& target, - uint32_t& out_yaw, int32_t& out_pitch); + void begin (); + void end (); + void update(double dt); }; diff --git a/src/systems/wall_connection.cpp b/src/systems/wall_connection.cpp new file mode 100644 index 0000000..a906076 --- /dev/null +++ b/src/systems/wall_connection.cpp @@ -0,0 +1,342 @@ +#include <SDL3/SDL.h> +#include <cstdlib> + +#include "entities/camera.hpp" +#include "entities/world.hpp" +#include "rendering/gl_renderer.hpp" +#include "systems/program_system.hpp" +#include "systems/wall_connection.hpp" +#include "utils/config.hpp" +#include "utils/light.hpp" +#include "wall/wall.hpp" + + +void +WallConnection::begin_Program(const Vec3& pult_pos) +{ + _target = Target::Program; + _pult_pos = pult_pos; + _prog_input.reset(); + _prog_last.clear(); +} + +void +WallConnection::begin_Editor(const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y) +{ + _target = Target::Editor; + _pult_pos = pult_pos; + load_Editor(lines, eye_y); +} + +void +WallConnection::load_Editor(const DynArray<std::string>& lines, double eye_y) +{ + std::vector<std::string> v(lines.begin(), lines.end()); + _vim.load(v); + if (_vim.is_Empty()) _vim.start_Insert(); + _vim_seen_version = _vim.version(); + _eye_y = eye_y; + _status.clear(); + rebuild_Editor(); +} + +void +WallConnection::end() +{ + _target = Target::None; + _prog_input.reset(); _prog_last.clear(); + _render. clear(); _linenums. clear(); + _cmdline. reset(); _status. clear(); + _blink = 0.0; +} + + +void +WallConnection::set_ProgramBuffer(const std::string& buffer) +{ + if (buffer == _prog_last) return; + rebuild_ProgramPreview(buffer); + _prog_last = buffer; +} + +void +WallConnection::clear_ProgramPreview() +{ + _prog_input.reset(); + _prog_last. clear(); +} + +void +WallConnection::rebuild_ProgramPreview(const std::string& buffer) +{ + Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, World::get_BoxScale()); + Vec3 pos = tf.position; + pos.y = ProgramSystem::start_y; + + _prog_input = make_Text3d("> " + buffer, Text3d::depth_wall, 0.003f, pos, ConfigColor::white); + _prog_input->yaw = tf.yaw; +} + + +WallConnection::Request +WallConnection::feed_Vim(DynArray<int>& keys, double eye_y) +{ + _eye_y = eye_y; + + for (int k : keys) { + if (k == -1) _vim.key_enter(); + else if (k == -2) _vim.key_escape(); + else if (k == -3) _vim.key_backspace(); + else if (k > 0) _vim.key_char(static_cast<char>(k)); + } + + Request req = Request::None; + switch (_vim.take_Signal()) { + case VimBuffer::Signal::Exit: return Request::Exit; + case VimBuffer::Signal::Write: req = Request::EditorWrite; break; + case VimBuffer::Signal::Help: + _status.clear(); + _status.push_back("-- VIM HELP --"); + _status.push_back("i a A o O insert Esc normal h j k l move"); + _status.push_back("0 $ line ends gg G :N jump line x dd delete"); + _status.push_back(":w write to wall :exit leave ctrl+WASD move"); + break; + default: break; + } + + if (_vim.version() != _vim_seen_version) { + _vim_seen_version = _vim.version(); + rebuild_Editor(); + } + return req; +} + +DynArray<std::string> +WallConnection::lines() const +{ + DynArray<std::string> out; + for (const std::string& l : _vim.lines()) out.push_back(l); + return out; +} + + +void +WallConnection::rebuild_Editor() +{ + _render.clear(); + _linenums.clear(); + + const float ws = World::get_BoxScale(); + Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); + const double lh = ConfigWall::line_height; + const int R = _vim.row(); + const int N = (int)_vim.lines().size(); + const double bottom_y = -static_cast<double>(ws) + 1.2; + + const double anchor_cursor = _eye_y + static_cast<double>(R) * lh; + const double anchor_bottom = bottom_y + static_cast<double>(N - 1) * lh; + _row0_y = SDL_min(anchor_cursor, anchor_bottom); + + const auto& lines = _vim.lines(); + + // Pass 1: build renderables in a local left-aligned frame, find block width. + struct Row { Text3dPtr num; Text3dPtr txt; double txt_w; }; + DynArray<Row> rows; + rows.reserve(lines.size()); + + double gutter_w = 0.0; + for (size_t i = 0; i < lines.size(); ++i) { + Text3dPtr num = make_Text3d(std::to_string(i + 1), Text3d::depth_wall, 0.0022f, + tf.position, ConfigColor::editor); + num->yaw = tf.yaw; + gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(num->half_extent)); + Row r; r.num = std::move(num); r.txt_w = 0.0; rows.push_back(std::move(r)); + } + const double gutter_gap = 0.6; + const double code_u0 = gutter_w + gutter_gap; + + double block_w = code_u0; + for (size_t i = 0; i < lines.size(); ++i) { + const bool drawable = lines[i].find_first_not_of(' ') != std::string::npos; + if (drawable) { + Text3dPtr t = make_Text3d(lines[i], Text3d::depth_wall, 0.003f, tf.position, ConfigColor::wall); + t->yaw = tf.yaw; + rows[i].txt_w = 2.0 * static_cast<double>(t->half_extent); + rows[i].txt = std::move(t); + } + block_w = SDL_max(block_w, code_u0 + rows[i].txt_w); + } + + // Pass 2: center the whole block on the wall (z = block_w/2 → -block_w/2). + const double z_block_left = block_w * 0.5; + + for (size_t i = 0; i < rows.size(); ++i) { + const double row_y = _row0_y - static_cast<double>(i) * lh; + + if (rows[i].num) { + const double he = static_cast<double>(rows[i].num->half_extent); + rows[i].num->pos = tf.position; + rows[i].num->pos.y = row_y; + rows[i].num->pos.z = z_block_left - he; + _linenums.push_back(std::move(rows[i].num)); + } + if (rows[i].txt) { + const double he = static_cast<double>(rows[i].txt->half_extent); + rows[i].txt->pos = tf.position; + 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 { + _render.push_back(Text3dPtr{}); + } + } + + _code_left_z = z_block_left - code_u0; + rebuild_Cmdline(); +} + +void +WallConnection::rebuild_Cmdline() +{ + _cmdline.reset(); + + std::string text; + if (_vim.mode() == VimBuffer::Mode::Command) text = _vim.cmdline(); + else if (!_status.empty()) text = _status[0]; + else if (!_vim.status().empty()) text = _vim.status(); + else text = (_vim.mode() == VimBuffer::Mode::Insert) ? "-- INSERT --" : "-- NORMAL --"; + if (text.empty()) return; + + const float ws = World::get_BoxScale(); + Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); + + Vec3 pos = tf.position; + pos.x += 0.8; + pos.y = -static_cast<double>(ws) + 0.8; + pos.z = 0.0; + + _cmdline = make_Text3d(text, Text3d::depth_wall, 0.0028f, pos, ConfigColor::white); + _cmdline->yaw = tf.yaw; +} + + +void +WallConnection::render(GlRenderer& gl, const Camera& cam, const Light& light) const +{ + if (_target == Target::Program) { + if (_prog_input) _prog_input->draw(gl, cam, light); + + draw_ProgramCursor(gl, cam, light); + draw_Electric (gl, cam); + } 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); + + if (_cmdline) _cmdline->draw(gl, cam, light); + + draw_EditorCursor(gl, cam, light); + draw_Electric (gl, cam); + } +} + +void +WallConnection::draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const +{ + if (!_prog_input) return; + if (SDL_fmod(_blink, 0.8) > 0.4) return; + + Vec3 c = _prog_input->pos; + c.z = _prog_input->pos.z - (static_cast<double>(_prog_input->half_extent) + 0.15); + + Mat4 view = buildViewMat4(cam); + Mat4 model = buildModelMat4(c, _prog_input->yaw); + Mat4 mvp = Mat4::mul(gl.projection(), Mat4::mul(view, model)); + + gl.set_Wireframe(false); + gl.set_MVP(model, mvp); + gl.set_NormalMat(model); + gl.set_Light(light); + gl.set_Color(0.6f, 1.0f, 1.0f); + gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f); + gl.draw_Box(0.04f, 0.22f, 0.02f); + gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f); +} + +void +WallConnection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& light) const +{ + if (SDL_fmod(_blink, 0.8) > 0.4) return; + + const int row = _vim.row(); + if (row < 0 || row >= (int)_vim.lines().size()) return; + + const float ws = World::get_BoxScale(); + Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); + const double lh = ConfigWall::line_height; + + const std::string& s = _vim.lines()[(size_t)row]; + const double row_y = _row0_y - static_cast<double>(row) * lh; + + double cw, line_x = tf.position.x, he_y = 0.2; + if (row < (int)_render.size() && _render[(size_t)row]) { + const Text3dPtr& line = _render[(size_t)row]; + const int N = s.empty() ? 1 : (int)s.size(); + 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 { + cw = 0.18; + } + + const int col = _vim.col(); + const double cz = _code_left_z - (static_cast<double>(col) + 0.5) * cw; + + Vec3 c { line_x, row_y - (he_y + 0.06), cz }; + + Mat4 view = buildViewMat4(cam); + Mat4 model = buildModelMat4(c, tf.yaw); + Mat4 mvp = Mat4::mul(gl.projection(), Mat4::mul(view, model)); + + gl.set_Wireframe(false); + gl.set_MVP(model, mvp); + gl.set_NormalMat(model); + gl.set_Light(light); + gl.set_Color(0.6f, 1.0f, 1.0f); + gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f); + gl.draw_Box(static_cast<float>(cw * 0.45), 0.015f, 0.03f); + gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f); +} + +void +WallConnection::draw_Electric(GlRenderer& gl, const Camera& cam) const +{ + const Vec3 a = cam.pos; + const Vec3 b = _pult_pos; + + constexpr int SEG = 16; + DynArray<float> pts; + pts.reserve(SEG * 6); + Vec3 prev = a; + for (int i = 1; i <= SEG; ++i) { + const double t = static_cast<double>(i) / SEG; + Vec3 p { a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t }; + if (i < SEG) { + const double j = 0.18; + p.x += ((rand() % 1000) / 1000.0 - 0.5) * j; + p.y += ((rand() % 1000) / 1000.0 - 0.5) * j; + p.z += ((rand() % 1000) / 1000.0 - 0.5) * j; + } + pts.push_back((float)prev.x); pts.push_back((float)prev.y); pts.push_back((float)prev.z); + pts.push_back((float)p.x); pts.push_back((float)p.y); pts.push_back((float)p.z); + prev = p; + } + + Mat4 ident = Mat4::identity(); + Mat4 mvp = Mat4::mul(gl.projection(), buildViewMat4(cam)); + gl.set_MVP(ident, mvp); + gl.set_NormalMat(ident); + gl.set_Wireframe(true); + gl.set_Color(0.5f, 0.9f, 1.0f); + gl.draw_Lines(pts.data(), static_cast<int>(pts.size()) / 3); + gl.set_Wireframe(false); +} diff --git a/src/systems/wall_connection.hpp b/src/systems/wall_connection.hpp new file mode 100644 index 0000000..5735810 --- /dev/null +++ b/src/systems/wall_connection.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include <string> +#include <vector> + +#include "text_3d/text_3d.hpp" +#include "text_3d/vim_buffer.hpp" +#include "utils/globals.hpp" +#include "utils/math.hpp" + +class Camera; +class GlRenderer; +struct Light; + +class WallConnection +{ +public: + enum class Target : uint8_t { None, Program, Editor }; + enum class Request : uint8_t { None, Exit, EditorWrite }; + + bool active() const { return _target != Target::None; } + Target target() const { return _target; } + + void begin_Program(const Vec3& pult_pos); + void begin_Editor (const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y); + void load_Editor (const DynArray<std::string>& lines, double eye_y); + void end(); + + void set_ProgramBuffer (const std::string& buffer); + void clear_ProgramPreview(); + + Request feed_Vim(DynArray<int>& keys, double eye_y); + DynArray<std::string> + lines () const; + + void update(double dt) { if (active()) _blink += dt; } + void render(GlRenderer& gl, const Camera& cam, const Light& light) const; + +private: + Target _target { Target::None }; + Vec3 _pult_pos {}; + double _blink { 0.0 }; + + Text3dPtr _prog_input; + std::string _prog_last; + + VimBuffer _vim; + unsigned _vim_seen_version { 0 }; + 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(); + void rebuild_ProgramPreview(const std::string& buffer); + void draw_EditorCursor (GlRenderer& gl, const Camera& cam, const Light& light) const; + void draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const; + void draw_Electric (GlRenderer& gl, const Camera& cam) const; +}; diff --git a/src/wall/text_wall_scroll.hpp b/src/wall/text_wall_scroll.hpp index a71ee9e..5fa89c9 100644 --- a/src/wall/text_wall_scroll.hpp +++ b/src/wall/text_wall_scroll.hpp @@ -1,18 +1,12 @@ #pragma once -// ───────────────────────────────────────────────────────────────────────────── -// TextWall scroll utilities -// -// These free functions live alongside TextWall and operate on TextWallList. -// Include this header wherever you need to scroll or clear wall content. -// ───────────────────────────────────────────────────────────────────────────── +#include "entities/world.hpp" #include "text_wall.hpp" #include "utils/config.hpp" #include "wall/wall.hpp" namespace TextWallScroll { - // Scroll all words on the given wall role up (+) or down (-) inline void scroll(TextWallList& texts, Wall::Role role, float delta) { @@ -31,27 +25,23 @@ namespace TextWallScroll scroll_Down(TextWallList& texts, Wall::Role role) { scroll(texts, role, -ConfigWall::scroll_step); } - // Drop all words of the given wall role back to the floor as dynamic objects. - // They are moved out of wall_texts and into floor_objects with Dynamic physics. inline void clear_ToFloor(TextWallList& texts, DynArray<Text3dPtr>& words, - Wall::Role role, - double floor_y) + Wall::Role role) { for (TextWallPtr& wt : texts) { if (wt->role != role || !wt->text3d) continue; Text3dPtr word = std::move(wt->text3d); word->body.mode = PhysicsMode::Dynamic; - word->body.vel = { 0.0, 1.5, 0.0 }; // small upward nudge - word->body.hover_height = floor_y + 1.5; + word->body.vel = { 0.0, 1.5, 0.0 }; + word->body.hover_height = World::get_FloorY<double>() + 1.5; word->set_Look(ConfigColor::floor, Text3d::depth_floor); words.push_back(std::move(word)); } - // Remove cleared entries from wall_texts texts.erase( std::remove_if(texts.begin(), texts.end(), [role](const TextWallPtr& wt){