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.gzSome Cleanup on my own AND Start New Textstructure with Claude
info.txt | 73 ++
src/core/game.cpp | 44 +-
src/core/game.hpp | 4 +-
src/core/main.cpp | 5 +-
src/entities/player.cpp | 3 +-
src/entities/pult.cpp | 42 +-
src/entities/world.cpp | 34 +-
src/entities/world.hpp | 5 +
.../level_director.cpp => levels/director.cpp} | 31 +-
.../level_director.hpp => levels/director.hpp} | 24 +-
.../progression.cpp} | 15 +-
src/levels/progression.hpp | 33 +
src/rendering/floor_mandelbrot.hpp | 25 -
.../{floor_mandelbrot.cpp => floor_surface.cpp} | 21 +-
src/rendering/floor_surface.hpp | 44 +
src/rendering/gl_renderer.cpp | 75 +-
src/rendering/gl_renderer.hpp | 2 +
.../surfaces.cpp => rendering/wall_surfaces.cpp} | 3 +-
src/states/game_state.cpp | 100 ++-
src/states/game_state.hpp | 37 +-
src/systems/carry_system.cpp | 74 +-
src/systems/carry_system.hpp | 6 +
src/systems/celebration_system.cpp | 14 +-
src/systems/hint_system.cpp | 14 +-
src/systems/input_system.cpp | 14 +-
src/systems/level_runner_system.hpp | 27 -
src/systems/particle_system.cpp | 26 +-
src/systems/physics_system.cpp | 99 +-
src/systems/plate_system.cpp | 10 +-
src/systems/plate_system.hpp | 6 +-
src/systems/pult_system.cpp | 13 +-
src/systems/wall_connection.cpp | 59 +-
src/systems/wall_connection.hpp | 35 +-
src/systems/word_ai_system.cpp | 18 +-
src/systems/word_anim.cpp | 16 +-
src/text/handler.cpp | 999 +++++++++++++++++++++
src/text/handler.hpp | 206 +++++
src/text/line.cpp | 40 +
src/text/line.hpp | 55 ++
src/text/token.cpp | 377 ++++++++
src/text/token.hpp | 179 ++++
src/text/vim_buffer.cpp | 265 ++++++
src/text/vim_buffer.hpp | 82 ++
src/text_3d/vim_buffer.cpp | 48 +-
src/text_3d/vim_buffer.hpp | 2 +-
src/utils/carry_math.cpp | 64 --
src/utils/carry_math.hpp | 25 -
src/utils/config.hpp | 9 +
src/wall/editor.hpp | 4 +-
src/wall/text.cpp | 16 +-
50 files changed, 2875 insertions(+), 547 deletions(-)
diff --git a/info.txt b/info.txt
index 8ce5b36..9d0f111 100644
--- a/info.txt
+++ b/info.txt
@@ -1,3 +1,76 @@
+
+
+Text::Handler
+ - handles tokens and lines as one commplete text structure so it can easily get tokenized from tree-sitter
+ - holds text like in a textedocument. so text has a width, height text-align line-height, word-spacing cursor position, scroll position, position in world space
+ - gets lexed from tree-sitter
+ - if the text changes re lex and set color
+ - lines<Line>
+ - tokens<Token>
+ - render options, underline lines, border text-window , font-size,..
+ - can handle token rotation (so all tokens follow player in a specific rotation (makes it easy to read the text -- is actually implemented))
+ - can render the tokens
+
+Text::Line
+ - position, linenumber, shallRenderNumbers, line height, word spacing
+ - tokens<Token>
+ - can belong to Text
+
+Text::Token
+ - color from Text tokenizer or
+ color from single token linting if not belonging to a text (save that value) or
+ color because the color shall be a static color (can get set)
+ - gets parsed from tree-sitter if not already known
+ - list of tookens for fast lookup if creating new ones
+ - has a position in world space
+ - can get picked up and shot with projectile
+ - can belong to Line
+ - rotate to players eye horizontally
+ - rotate to players eye vertically
+ - rotate to players eye horizontally and vertically
+ - (old Text3d)
+
+Token::Ai
+ - jiggle
+ - hover
+ - attack player (steal token/drop token)
+ - flee from player
+
+A Token can shoot another Token out of a Text with a Projectile.
+
+I can pick up tokens out of a Text (as we currently can from the EditorWall)
+
+Linting is an asyncroun job. before Token get a default color. We take the Linting when it is ready , we dont wait for it. We just need to recreate the text mesh if the token key has changed.
+
+Tokenizer(TreeSitter)
+ - lexes in background and applies color and key when found to token.
+ - Lexes when Text changed
+
+Wall::Editor
+ - can own Text
+ - vim like editing option
+ - fixes Text position on the wall
+ - i can shoot words inbetween other words or lines (as we can)
+
+Text
+ - big namespace with classes and structs
+ - Text in plain
+ - we might want to store that tokenized or in lines or in lines and tokenized depending on whats enabled
+ - can get rendered
+ - has a default color
+ - can jiggle
+ - can get editted with vim like style at the editor wall par example
+ - can be a label/get gicked/ etc
+ - can get tokenized for tokenized-coloring (tree-sitter)
+ - can get scrolled to a specific position
+
+
+struct Text
+{
+
+}
+----------------------------
+
- Dont pick up words through floor
diff --git a/src/core/game.cpp b/src/core/game.cpp
index 871e286..dcea0f0 100644
--- a/src/core/game.cpp
+++ b/src/core/game.cpp
@@ -1,37 +1,32 @@
#include <filesystem>
#include <system_error>
-#include "game.hpp"
-#include "utils/config.hpp"
#include "entities/world.hpp"
-#include "states/istate.hpp"
+#include "game.hpp"
#include "states/game_state.hpp"
+#include "states/istate.hpp"
#include "states/menu_state.hpp"
#include "systems/input_system.hpp"
#include "systems/physics_system.hpp"
+#include "utils/config.hpp"
Game::~Game() = default;
+
bool
Game::init()
{
std::error_code ec;
std::filesystem::create_directories("/tmp/game_mod_include", ec);
- std::filesystem::copy_file("src/systems/game_mod_abi.h",
- "/tmp/game_mod_include/game_mod_abi.h",
- std::filesystem::copy_options::overwrite_existing, ec);
+ std::filesystem::copy_file ("src/systems/game_mod_abi.h",
+ "/tmp/game_mod_include/game_mod_abi.h",
+ std::filesystem::copy_options::overwrite_existing, ec);
if (ec) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Could not stage game_mod_abi.h: %s", ec.message().c_str());
-
- SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
- SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
- SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
- SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
- SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
- SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
- SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4);
+ _gl_renderer = cpputils::make_unique<GlRenderer>();
+ _gl_renderer->pre_Config();
_window = SDL_CreateWindow("game", 640, 480, SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL);
if (!_window) {
@@ -39,12 +34,11 @@ Game::init()
return false;
}
- SDL_SetWindowRelativeMouseMode(_window, true);
-
- _gl_renderer = cpputils::make_unique<GlRenderer>();
- if (!_gl_renderer->init(_window)) return false;
+ if (!_gl_renderer->init(_window))
+ return false;
- SDL_StartTextInput(_window);
+ SDL_SetWindowRelativeMouseMode(_window, true);
+ SDL_StartTextInput (_window);
change_State(cpputils::make_unique<MenuState>());
return true;
@@ -54,32 +48,32 @@ Game::init()
void
Game::change_State(cpputils::unique_ptr<IState> next)
{
- if (m_state) m_state->on_Exit(*this);
+ if (_state) _state->on_Exit(*this);
- m_state = std::move(next);
+ _state = std::move(next);
- if (m_state) m_state->on_Enter(*this);
+ if (_state) _state->on_Enter(*this);
}
void
Game::handle_Event(const SDL_Event& event)
{
- if (m_state) m_state->handle_Event(*this, event);
+ if (_state) _state->handle_Event(*this, event);
}
void
Game::update(double dt_ns)
{
- if (m_state) m_state->update(dt_ns * 1e-9, _gl_renderer.get());
+ if (_state) _state->update(dt_ns * 1e-9, _gl_renderer.get());
}
void
Game::render()
{
- if (m_state) m_state->render(*_window, *_gl_renderer);
+ if (_state) _state->render(*_window, *_gl_renderer);
}
diff --git a/src/core/game.hpp b/src/core/game.hpp
index 7aa3f78..949de2b 100644
--- a/src/core/game.hpp
+++ b/src/core/game.hpp
@@ -13,8 +13,8 @@ class Game
{
SDL_Window* _window {};
GlRendererPtr _gl_renderer{};
-
- cpputils::unique_ptr<IState> m_state;
+ cpputils::unique_ptr<IState>
+ _state;
public:
~Game();
diff --git a/src/core/main.cpp b/src/core/main.cpp
index 172d3db..a2f545e 100644
--- a/src/core/main.cpp
+++ b/src/core/main.cpp
@@ -23,10 +23,7 @@ int main()
return 1;
}
- // IDK About that...
- // Cap any single dt to ~100ms. Beyond that we treat it as a hitch and
- // skip the lost time rather than feeding a huge dt into physics, AI,
- // animations, etc. Without this clamp a single OS pause ruins the next
+ // Without this clamp a single OS pause ruins the next
// physics substep AND poisons profiler readings.
constexpr Uint64 MAX_DT_NS = 100'000'000ull; // 100 ms
diff --git a/src/entities/player.cpp b/src/entities/player.cpp
index 6d4f7c6..e8f70a0 100644
--- a/src/entities/player.cpp
+++ b/src/entities/player.cpp
@@ -8,7 +8,8 @@ Player::Player()
radius(0.5f),
height(1.5f),
pos {8.0, 0.0, 8.0},
- vel {0.0, 0.0, 0.0} {}
+ vel {0.0, 0.0, 0.0}
+{}
void
Player::apply_Collision(const PlayerCollision& col)
diff --git a/src/entities/pult.cpp b/src/entities/pult.cpp
index e0521b5..acd19bb 100644
--- a/src/entities/pult.cpp
+++ b/src/entities/pult.cpp
@@ -30,25 +30,23 @@ PultButton::tick(double dt)
void
PultButton::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
{
- Mat4 view = buildViewMat4(cam);
+ Mat4 view = buildViewMat4 (cam);
+ Mat4 model = buildModelMat4 (pos, yaw);
Mat4 proj = gl.get_Projection();
- Mat4 model = buildModelMat4(pos, yaw);
- Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model));
+ Mat4 mvp = Mat4::mul (proj, Mat4::mul(view, model));
const float hx = static_cast<float>(shape.half_extents.x);
const float hy = static_cast<float>(shape.half_extents.y);
const float hz = static_cast<float>(shape.half_extents.z);
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(pressed ? 1.0f : 0.35f,
- pressed ? 1.0f : 0.75f,
- pressed ? 0.2f : 0.35f);
-
- gl.draw_Box(hx, hy, hz);
+ gl.set_Light (light);
+ gl.set_Color (pressed ? 1.0f : 0.35f,
+ pressed ? 1.0f : 0.75f,
+ pressed ? 0.2f : 0.35f);
+ gl.draw_Box (hx, hy, hz);
}
@@ -66,11 +64,11 @@ Pult::Pult(const std::string& n, Vec3 world_pos,
false)
, name(n), half_w(half_width), half_d(half_depth)
{
- label.set_Billboard(true);
- label.set_Style(ConfigColor::teal, Text3d::depth_wall, 0.0025f);
+ label.set_Billboard (true);
+ label.set_Style (ConfigColor::teal, Text3d::depth_wall, 0.0025f);
label.set_Collidable(true);
- label.attach_To(&pos, Vec3{ 0.0, ConfigPult::label_height, 0.0 });
- label.set_Text(n);
+ label.attach_To (&pos, Vec3{ 0.0, ConfigPult::label_height, 0.0 });
+ label.set_Text (n);
}
@@ -82,17 +80,17 @@ Pult::tick(double dt)
void
Pult::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
{
- Mat4 view = buildViewMat4(cam);
+ Mat4 view = buildViewMat4 (cam);
+ Mat4 model = buildModelMat4 (pos, yaw);
Mat4 proj = gl.get_Projection();
- Mat4 model = buildModelMat4(pos, yaw);
- Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model));
+ Mat4 mvp = Mat4::mul (proj, Mat4::mul(view, model));
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(0.45f, 0.45f, 0.65f);
- gl.draw_Box(half_w, H, half_d);
+ gl.set_Light (light);
+ gl.set_Color (0.45f, 0.45f, 0.65f);
+ gl.draw_Box (half_w, H, half_d);
for (const PultButton* b : buttons)
b->draw(gl, cam, light);
diff --git a/src/entities/world.cpp b/src/entities/world.cpp
index 63818a5..16e6096 100644
--- a/src/entities/world.cpp
+++ b/src/entities/world.cpp
@@ -1,10 +1,13 @@
#include "entities/world.hpp"
+#include "utils/frame_profiler.hpp"
World::World()
{
- // initEdges();
_wall_surfaces.generate(get_BoxScale());
+
+ _floor_surface = cpputils::make_unique<Floor::Mandelbrot>();
+ _floor_surface->generate(World::get_BoxScale(), World::get_FloorY());
}
@@ -53,33 +56,12 @@ World::resolve_Player(const Player& player) const
void
World::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
{
- Mat4 view = buildViewMat4(camera);
- Mat4 proj = gl.get_Projection();
- Mat4 model = Mat4::identity();
- Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model));
-
- // Crosshair: 2D overlay in NDC. Uses identity MVP, then restores the
- // world MVP so subsequent draws (_wall_surfaces) aren't affected.
- {
- Mat4 ident = Mat4::identity();
- gl.set_MVP(model, ident);
- gl.set_Color(1.0f, 1.0f, 1.0f);
-
- float cx_size = 0.02f;
- float pts[] = {
- 0.0f, -cx_size, 0.0f,
- 0.0f, cx_size, 0.0f,
- -cx_size, 0.0f, 0.0f,
- cx_size, 0.0f, 0.0f,
- };
- gl.draw_Lines(pts, 4);
-
- // Restore world MVP for everything drawn after the crosshair.
- gl.set_MVP(model, mvp);
+ { FP_ZONE("world.draw.wall");
+ _wall_surfaces.draw(gl, camera, light); }
+ { FP_ZONE("world.draw.floor");
+ _floor_surface->draw(gl, camera, light);
}
- // ── Solid wall surfaces ─────────────────────────────────────────────
- _wall_surfaces.draw(gl, camera, light);
}
diff --git a/src/entities/world.hpp b/src/entities/world.hpp
index 583a205..40b2181 100644
--- a/src/entities/world.hpp
+++ b/src/entities/world.hpp
@@ -2,7 +2,9 @@
#include <array>
+#include "utils/globals.hpp"
#include "entities/player.hpp"
+#include "rendering/floor_surface.hpp"
#include "rendering/gl_renderer.hpp"
#include "wall/wall.hpp"
@@ -17,9 +19,12 @@ struct SweepResult
};
+namespace Floor { struct Surface; }
+
class World
{
Wall::Surfaces _wall_surfaces;
+ cpputils::unique_ptr<Floor::Surface> _floor_surface;
public:
World();
diff --git a/src/systems/level_director.cpp b/src/levels/director.cpp
similarity index 91%
rename from src/systems/level_director.cpp
rename to src/levels/director.cpp
index ef75de5..99cbf22 100644
--- a/src/systems/level_director.cpp
+++ b/src/levels/director.cpp
@@ -6,23 +6,27 @@
#include "entities/plate.hpp"
#include "entities/solid_box.hpp"
#include "entities/world.hpp"
+#include "levels/director.hpp"
#include "levels/level.hpp"
#include "states/game_context.hpp"
#include "systems/celebration_system.hpp"
-#include "systems/level_director.hpp"
#include "systems/particle_system.hpp"
#include "utils/config.hpp"
#include "utils/text.hpp"
#include "wall/wall.hpp"
-LevelDirector::LevelDirector(const Level::Data& level)
- : _runner(cpputils::make_unique<LevelRunner>(level))
+namespace
+Level
+{
+
+Director::Director(const Level::Data& level)
+ : _runner(cpputils::make_unique<Level::Progression>(level))
{}
int
-LevelDirector::get_MaxAiForStep() const
+Director::get_MaxAiForStep() const
{
if (_runner->is_Complete()) return 0;
return _runner->get_Current().max_ai_words;
@@ -30,7 +34,7 @@ LevelDirector::get_MaxAiForStep() const
void
-LevelDirector::enter_Step(GameContext& ctx)
+Director::enter_Step(GameContext& ctx)
{
spawn_Grants(ctx);
setup_Scene(ctx, _runner->get_Current().scene);
@@ -40,7 +44,7 @@ LevelDirector::enter_Step(GameContext& ctx)
void
-LevelDirector::spawn_Grants(GameContext& ctx)
+Director::spawn_Grants(GameContext& ctx)
{
DynArray<std::string> tokens = _runner->get_GarantsForStep();
@@ -69,7 +73,7 @@ LevelDirector::spawn_Grants(GameContext& ctx)
void
-LevelDirector::setup_Scene(GameContext& ctx, const Scene::Data& scene)
+Director::setup_Scene(GameContext& ctx, const Scene::Data& scene)
{
for (const Scene::Placed::Word& pw : scene.words) {
Text3dPtr t = make_Text3d(pw.text, Text3d::depth_floor, 0.003f, pw.pos, pw.color);
@@ -122,7 +126,7 @@ LevelDirector::setup_Scene(GameContext& ctx, const Scene::Data& scene)
void
-LevelDirector::teardown_Scene(GameContext& ctx)
+Director::teardown_Scene(GameContext& ctx)
{
if (!_step_scenery.empty()) {
std::unordered_set<GameObject*> scenery_set(_step_scenery.begin(), _step_scenery.end());
@@ -139,7 +143,7 @@ LevelDirector::teardown_Scene(GameContext& ctx)
void
-LevelDirector::show_Details(GameContext& ctx) const
+Director::show_Details(GameContext& ctx) const
{
ctx.clear_WallRole(Wall::Role::Info);
@@ -170,7 +174,7 @@ LevelDirector::show_Details(GameContext& ctx) const
Goal::Context
-LevelDirector::build_GoalCtx(const GameContext& ctx) const
+Director::build_GoalCtx(const GameContext& ctx) const
{
Goal::Context gctx;
@@ -193,8 +197,8 @@ LevelDirector::build_GoalCtx(const GameContext& ctx) const
}
-LevelDirector::AdvanceResult
-LevelDirector::try_Advance(GameContext& ctx, double dt,
+Director::AdvanceResult
+Director::try_Advance(GameContext& ctx, double dt,
CelebrationSystem& celebration, ParticleSystem& particles)
{
_step_age += dt;
@@ -225,3 +229,6 @@ LevelDirector::try_Advance(GameContext& ctx, double dt,
celebration.start(*ctx.camera, glyph_yaw);
return res;
}
+
+
+} // namespace Level
diff --git a/src/systems/level_director.hpp b/src/levels/director.hpp
similarity index 80%
rename from src/systems/level_director.hpp
rename to src/levels/director.hpp
index 543ce34..f98a5a6 100644
--- a/src/systems/level_director.hpp
+++ b/src/levels/director.hpp
@@ -7,7 +7,7 @@
#include "levels/goal.hpp"
#include "levels/level.hpp"
#include "levels/scene.hpp"
-#include "systems/level_runner_system.hpp"
+#include "levels/progression.hpp"
#include "utils/globals.hpp"
@@ -19,6 +19,9 @@ class WordAiSystem;
struct GameContext;
+namespace Level
+{
+
struct StepLedger
{
DynArray<PultAction> buttons_pressed;
@@ -46,9 +49,9 @@ struct StepPlate
};
-class LevelDirector
+class Director
{
- cpputils::unique_ptr<LevelRunner>
+ cpputils::unique_ptr<Level::Progression>
_runner;
double _step_age { 0.0 };
@@ -69,7 +72,7 @@ public:
bool reset_carry {false}; // next step requests carry/projectile wipe
};
- explicit LevelDirector(const Level::Data& level);
+ explicit Director(const Level::Data& level);
void enter_Step(GameContext& ctx);
@@ -88,9 +91,12 @@ public:
bool is_LevelComplete() const { return _runner->is_Complete(); }
- LevelRunner& get_Runner() { return *_runner; }
- const LevelRunner& get_Runner() const { return *_runner; }
- const std::string& get_CurrentHint() const { return _runner->get_Current().hint; }
- int get_MaxAiForStep() const;
- DynArray<StepPlate>& get_StepPlates() { return _step_plates; }
+ Level::Progression& get_Runner() { return *_runner; }
+ const Level::Progression& get_Runner() const { return *_runner; }
+ const std::string& get_CurrentHint() const { return _runner->get_Current().hint; }
+ int get_MaxAiForStep() const;
+ DynArray<StepPlate>& get_StepPlates() { return _step_plates; }
};
+
+
+} // namespace Level
diff --git a/src/systems/level_runner_system.cpp b/src/levels/progression.cpp
similarity index 58%
rename from src/systems/level_runner_system.cpp
rename to src/levels/progression.cpp
index 388d186..ecba100 100644
--- a/src/systems/level_runner_system.cpp
+++ b/src/levels/progression.cpp
@@ -1,9 +1,13 @@
-#include "level_runner_system.hpp"
+#include "progression.hpp"
-LevelRunner::LevelRunner(const Level::Data& l) : _level(&l) {}
+
+namespace Level
+{
+
+Progression::Progression(const Level::Data& l) : _level(&l) {}
-bool LevelRunner::advance()
+bool Level::Progression::advance()
{
_current_step++;
return !is_Complete();
@@ -11,7 +15,7 @@ bool LevelRunner::advance()
DynArray<std::string>
-LevelRunner::get_GarantsForStep() const
+Progression::get_GarantsForStep() const
{
DynArray<std::string> result;
for (const Level::TokenGrant& grant : get_Current().grants)
@@ -19,3 +23,6 @@ LevelRunner::get_GarantsForStep() const
result.push_back(c);
return result;
}
+
+
+} // namespace
diff --git a/src/levels/progression.hpp b/src/levels/progression.hpp
new file mode 100644
index 0000000..8bcd1df
--- /dev/null
+++ b/src/levels/progression.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "levels/level.hpp"
+
+
+namespace Level
+{
+
+class Progression
+{
+ const Data*
+ _level;
+
+ size_t _current_step{0};
+public:
+ explicit Progression(const Data& _level);
+
+ bool advance ();
+
+ bool is_Complete () const { return _current_step >= _level->steps.size(); }
+ bool is_DynamicLib () const { return _level->dynamic_lib; }
+
+ size_t get_Step () const { return _current_step; }
+ const Level::Step&
+ get_Current () const { return _level->steps[_current_step]; }
+ const std::string&
+ get_SourceFile() const { return _level->source_file; }
+ DynArray<std::string>
+ get_GarantsForStep() const;
+};
+
+
+} // namespace
diff --git a/src/rendering/floor_mandelbrot.hpp b/src/rendering/floor_mandelbrot.hpp
deleted file mode 100644
index 6f5a269..0000000
--- a/src/rendering/floor_mandelbrot.hpp
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma once
-
-#include <SDL3/SDL.h>
-
-#include "rendering/gl_renderer.hpp"
-#include "utils/globals.hpp"
-#include "utils/light.hpp"
-
-
-class Camera;
-
-
-class FloorMandelbrot
-{
- DynArray<int> _indices;
- DynArray<float> _verts;
-public:
- void generate(float world_scale,
- float floor_y,
- double center_x = -0.5,
- double center_y = 0.0,
- double width = 3.0);
-
- void draw(GlRenderer& gl, const Camera& cam, const Light& light) const;
-};
diff --git a/src/rendering/floor_mandelbrot.cpp b/src/rendering/floor_surface.cpp
similarity index 85%
rename from src/rendering/floor_mandelbrot.cpp
rename to src/rendering/floor_surface.cpp
index 2001635..9ce4685 100644
--- a/src/rendering/floor_mandelbrot.cpp
+++ b/src/rendering/floor_surface.cpp
@@ -1,15 +1,18 @@
#include <SDL3/SDL.h>
#include "entities/camera.hpp"
-#include "floor_mandelbrot.hpp"
+#include "floor_surface.hpp"
+namespace Floor
+{
+
void
-FloorMandelbrot::generate(float world_scale,
- float floor_y,
- double center_x,
- double center_y,
- double width)
+Mandelbrot::generate(float world_scale,
+ float floor_y,
+ double center_x,
+ double center_y,
+ double width)
{
_verts.clear();
_indices.clear();
@@ -18,7 +21,7 @@ FloorMandelbrot::generate(float world_scale,
// layout: [pos(3) | normal(3) | color(3)], but here we *abuse* the color
// slot to carry the complex-plane coordinate (cx, cy, 0). The shader
// reads it from vColor when in Mandelbrot mode.
- const float ws = world_scale;
+ const float ws = world_scale;
const double half_w = width * 0.5;
// Four corners of the floor in world space.
@@ -56,7 +59,7 @@ FloorMandelbrot::generate(float world_scale,
void
-FloorMandelbrot::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
+Mandelbrot::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
{
if (_verts.empty()) return;
@@ -78,3 +81,5 @@ FloorMandelbrot::draw(GlRenderer& gl, const Camera& cam, const Light& light) con
static_cast<int>(_indices.size()));
gl.enable_MandelbrotMode(false);
}
+
+} // namespace Floor
diff --git a/src/rendering/floor_surface.hpp b/src/rendering/floor_surface.hpp
new file mode 100644
index 0000000..1176fbc
--- /dev/null
+++ b/src/rendering/floor_surface.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <SDL3/SDL.h>
+
+#include "rendering/gl_renderer.hpp"
+#include "utils/globals.hpp"
+#include "utils/light.hpp"
+
+
+class Camera;
+
+
+namespace Floor
+{
+
+class Surface
+{
+protected:
+ DynArray<int> _indices;
+ DynArray<float> _verts;
+public:
+ virtual void generate(float world_scale,
+ float floor_y,
+ double center_x = -0.5,
+ double center_y = 0.0,
+ double width = 3.0) = 0;
+
+ virtual void draw(GlRenderer& gl, const Camera& cam, const Light& light) const = 0;
+ virtual ~Surface() = default;
+};
+
+class Mandelbrot final : public Surface
+{
+public:
+ void generate(float world_scale,
+ float floor_y,
+ double center_x = -0.5,
+ double center_y = 0.0,
+ double width = 3.0) override;
+
+ void draw(GlRenderer& gl, const Camera& cam, const Light& light) const override;
+};
+
+} // namespace Floor
diff --git a/src/rendering/gl_renderer.cpp b/src/rendering/gl_renderer.cpp
index 88ce1be..8743c81 100644
--- a/src/rendering/gl_renderer.cpp
+++ b/src/rendering/gl_renderer.cpp
@@ -409,9 +409,20 @@ Mat4 buildModelMat4(const Vec3& pos, float yaw, float pitch)
return m;
}
+
// ─────────────────────────────────────────────────────────────────────────────
// GlRenderer::init
// ─────────────────────────────────────────────────────────────────────────────
+void GlRenderer::pre_Config() {
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
+ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+ SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4);
+}
+
bool GlRenderer::init(SDL_Window* window)
{
_ctx = SDL_GL_CreateContext(window);
@@ -792,11 +803,13 @@ void GlRenderer::draw_Mesh(const float* verts, int vert_count,
const int* indices, int index_count)
{
glBindVertexArray(_vao);
- glBindBuffer(GL_ARRAY_BUFFER, _vbo);
- glBufferData(GL_ARRAY_BUFFER, static_cast<size_t>(vert_count)*6*sizeof(float), verts, GL_STREAM_DRAW);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<size_t>(index_count)*sizeof(int), indices, GL_STREAM_DRAW);
- glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);
+
+ glBindBuffer (GL_ARRAY_BUFFER, _vbo);
+ glBufferData (GL_ARRAY_BUFFER, static_cast<size_t>(vert_count)*6*sizeof(float), verts, GL_STREAM_DRAW);
+ glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, _ebo);
+ glBufferData (GL_ELEMENT_ARRAY_BUFFER, static_cast<size_t>(index_count)*sizeof(int), indices, GL_STREAM_DRAW);
+ glDrawElements (GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);
+
glBindVertexArray(0);
}
@@ -806,10 +819,11 @@ GlRenderer::draw_MeshColored(const float* verts, int vert_count,
const int* indices, int index_count)
{
glBindVertexArray(_vao_c);
- glBindBuffer(GL_ARRAY_BUFFER, _vbo_c);
- glBufferData(GL_ARRAY_BUFFER,
- static_cast<size_t>(vert_count) * 9 * sizeof(float),
- verts, GL_STATIC_DRAW);
+
+ glBindBuffer (GL_ARRAY_BUFFER, _vbo_c);
+ glBufferData (GL_ARRAY_BUFFER,
+ static_cast<size_t> (vert_count) * 9 * sizeof(float),
+ verts, GL_STATIC_DRAW);
// Re-point attributes at _vbo_c — draw_MeshCached may have repointed
// this shared VAO at a per-Text3d VBO.
@@ -821,7 +835,8 @@ GlRenderer::draw_MeshColored(const float* verts, int vert_count,
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
static_cast<size_t>(index_count) * sizeof(int),
indices, GL_STATIC_DRAW);
- glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);
+ glDrawElements (GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);
+
glBindVertexArray(0);
}
@@ -832,7 +847,7 @@ GlRenderer::draw_MeshCached(unsigned int& vbo, unsigned int& ebo, int& index_cou
const float* verts, int vert_count,
const int* indices, int idx_count)
{
- glBindVertexArray(_vao_c); // reuse the 9-float pos|normal|color layout
+ glBindVertexArray(_vao_c);
if (vbo == 0) glGenBuffers(1, &vbo);
if (ebo == 0) glGenBuffers(1, &ebo);
@@ -856,6 +871,7 @@ GlRenderer::draw_MeshCached(unsigned int& vbo, unsigned int& ebo, int& index_cou
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(6*sizeof(float)));
glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);
+
glBindVertexArray(0);
}
@@ -872,9 +888,11 @@ GlRenderer::free_MeshBuffers(unsigned int vbo, unsigned int ebo)
void GlRenderer::draw_Lines(const float* points, int point_count)
{
glBindVertexArray(_line_vao);
+
glBindBuffer(GL_ARRAY_BUFFER, _line_vbo);
glBufferData(GL_ARRAY_BUFFER, static_cast<size_t>(point_count)*3*sizeof(float), points, GL_STREAM_DRAW);
glDrawArrays(GL_LINES, 0, point_count);
+
glBindVertexArray(0);
}
@@ -883,14 +901,16 @@ void GlRenderer::draw_Menu()
glUseProgram(_prog);
Mat4 identity = Mat4::identity();
glUniformMatrix4fv(_uMVP, 1, GL_FALSE, identity.m);
+
float n[9] = {1,0,0, 0,1,0, 0,0,1};
glUniformMatrix3fv(_uNormalMat, 1, GL_FALSE, n);
- glUniform3f(_uColor, 0.2f, 0.4f, 0.8f);
- glUniform3f(_uLightDir, 0.0f, 0.0f, 1.0f);
- glUniform3f(_uAmbient, 1.0f, 1.0f, 1.0f); // full ambient = flat colour
- glUniform1f(_uIntensity, 1.0f);
- glUniform1i(_uWireframe, 1);
- glUniform3f(_uSunColor, 1.0f, 1.0f, 1.0f);
+
+ glUniform3f(_uColor, 0.2f, 0.4f, 0.8f);
+ glUniform3f(_uLightDir, 0.0f, 0.0f, 1.0f);
+ glUniform3f(_uAmbient, 1.0f, 1.0f, 1.0f);
+ glUniform1f(_uIntensity, 1.0f);
+ glUniform1i(_uWireframe, 1);
+ glUniform3f(_uSunColor, 1.0f, 1.0f, 1.0f);
glUniform1i(_uMandelbrot, 0);
glUniform1i(_uWallPaper, 0);
glUniform3f(_uFlashPos, 0.0f, 0.0f, 0.0f);
@@ -900,11 +920,15 @@ void GlRenderer::draw_Menu()
glUniform1f(_uFlashInnerCos, 0.97f);
glUniform1f(_uFlashOuterCos, 0.90f);
glUniform1f(_uFlashIntensity, 0.0f);
+
Mat4 ident_m = Mat4::identity();
glUniformMatrix4fv(_uModel, 1, GL_FALSE, ident_m.m);
+
glBindVertexArray(_menu_vao);
+
glDrawArrays(GL_TRIANGLES, 0, 3);
+
glBindVertexArray(0);
}
@@ -958,13 +982,14 @@ GlRenderer::draw_Emissive(const Camera& cam,
GLuint GlRenderer::compile_Shader(GLenum type, const char* src)
{
GLuint s = glCreateShader(type);
- glShaderSource(s, 1, &src, nullptr);
+ glShaderSource (s, 1, &src, nullptr);
glCompileShader(s);
GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512]; glGetShaderInfoLog(s,512,nullptr,log);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shader compile error: %s", log);
- glDeleteShader(s); return 0;
+ glDeleteShader(s);
+ return 0;
}
return s;
}
@@ -973,21 +998,24 @@ bool GlRenderer::link_Program(GLuint vert, GLuint frag)
{
_prog = glCreateProgram();
glAttachShader(_prog, vert); glAttachShader(_prog, frag);
- glLinkProgram(_prog);
+ glLinkProgram (_prog);
GLint ok; glGetProgramiv(_prog, GL_LINK_STATUS, &ok);
if (!ok) {
char log[512]; glGetProgramInfoLog(_prog,512,nullptr,log);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Program link error: %s", log);
- glDeleteProgram(_prog); _prog=0; return false;
+ glDeleteProgram(_prog); _prog=0;
+ return false;
}
return true;
}
float
-GlRenderer::get_Aspect() const { return _aspect; }
+GlRenderer::get_Aspect() const
+{ return _aspect; }
const Mat4&
-GlRenderer::get_Projection() const { return _proj; }
+GlRenderer::get_Projection() const
+{ return _proj; }
void
GlRenderer::enable_MandelbrotMode(bool on)
@@ -998,7 +1026,6 @@ GlRenderer::enable_WallPaperMode(bool on)
{ glUniform1i(_uWallPaper, on ? 1 : 0); }
-
void
GlRenderer::create_FullscreenQuad_()
{
diff --git a/src/rendering/gl_renderer.hpp b/src/rendering/gl_renderer.hpp
index b8e3e59..4e57bee 100644
--- a/src/rendering/gl_renderer.hpp
+++ b/src/rendering/gl_renderer.hpp
@@ -138,6 +138,8 @@ class GlRenderer
public:
static void free_MeshBuffers(unsigned int vbo, unsigned int ebo);
+
+ void pre_Config ();
bool init (SDL_Window* window);
void begin_Frame (int viewport_w, int viewport_h);
void end_Frame (SDL_Window* window);
diff --git a/src/wall/surfaces.cpp b/src/rendering/wall_surfaces.cpp
similarity index 99%
rename from src/wall/surfaces.cpp
rename to src/rendering/wall_surfaces.cpp
index 6d2fa57..5f23d18 100644
--- a/src/wall/surfaces.cpp
+++ b/src/rendering/wall_surfaces.cpp
@@ -1,12 +1,11 @@
#include <SDL3/SDL.h>
#include "entities/camera.hpp"
-#include "wall.hpp"
-
#include "rendering/gl_renderer.hpp"
#include "utils/globals.hpp"
#include "utils/light.hpp"
#include "utils/math.hpp"
+#include "wall/wall.hpp"
diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index b8b00a4..b3002da 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -48,29 +48,27 @@ GameState::GameState(size_t starting_level)
_ctx.light = cpputils::make_unique<Light> ();
_input_sys = cpputils::make_unique<InputSystem> ();
- _physics_sys = cpputils::make_unique<PhysicsSystem> ();
- _projectiles_sys = cpputils::make_unique<ProjectileSystem> ();
+ _freelook_sys = cpputils::make_unique<FreelookSystem> ();
+
_compiler_sys = cpputils::make_unique<CompilerSystem> ();
_program_sys = cpputils::make_unique<ProgramSystem> ();
- _word_anim_sys = cpputils::make_unique<WordAnimSystem> ();
- _freelook_sys = cpputils::make_unique<FreelookSystem> ();
- _particle_sys = cpputils::make_unique<ParticleSystem> ();
_dyn_loader_sys = cpputils::make_unique<DynLoaderSystem> ();
- _celebration_sys = cpputils::make_unique<CelebrationSystem> ();
- _highlight_sys = cpputils::make_unique<CppHighlightSystem>();
- _floor_sys = cpputils::make_unique<FloorMandelbrot> ();
- _floor_sys->generate(World::get_BoxScale(), World::get_FloorY());
-
- _wall_labels = Wall::Label::generate_All(World::get_BoxScale());
+ _physics_sys = cpputils::make_unique<PhysicsSystem> ();
+ _celebration_sys = cpputils::make_unique<CelebrationSystem> ();
+ _projectiles_sys = cpputils::make_unique<ProjectileSystem> ();
+ _particle_sys = cpputils::make_unique<ParticleSystem> ();
+ _word_anim_sys = cpputils::make_unique<WordAnimSystem> ();
_word_ai_sys = cpputils::make_unique<WordAiSystem>();
+ _highlight_sys = cpputils::make_unique<CppHighlightSystem>();
+ _wall_labels = Wall::Label::generate_All(World::get_BoxScale());
_pult_sys = cpputils::make_unique<PultSystem> ();
_pult_sys->init(_ctx.scenery, _ctx.buttons, World::get_BoxScale());
if (starting_level >= Level::get_Count()) starting_level = 0;
- _level_director = cpputils::make_unique<LevelDirector>(Level::get_Data(starting_level));
+ _level_director = cpputils::make_unique<Level::Director>(Level::get_Data(starting_level));
_level_director->enter_Step(_ctx);
_word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep());
@@ -132,11 +130,11 @@ GameState::carriedline_Update()
if (fs.active) return;
- const Vec3 anchor = CarryMath::get_LineAnchor(*_ctx.camera, CarryMath::CARRY_DIST);
+ const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, ConfigCarry::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::get_SlotPosition(_carried, anchor, right, CarryMath::CARRY_GAP);
+ DynArray<Vec3> slots = _carried.get_SlotPosition(anchor, right, ConfigCarry::gap);
DynArray<CarriedWordNode*> nodes = _carried.reverse_nodes();
// Easing back from the frozen spots: lerp each word from its release
@@ -171,18 +169,12 @@ GameState::pult_DispatchAction(PultAction act)
_level_director->record_Button(act);
switch (act) {
- case PultAction::Compile: on_Compile(); break;
- case PultAction::RunProgram: on_Run(); break;
- case PultAction::ScrollEditorUp: Wall::Text::scroll_Up(_ctx.wall_texts, Wall::Role::Editor);
- _editor_wall.scroll(ConfigWall::scroll_step);
- break;
- case PultAction::ScrollEditorDown: Wall::Text::scroll_Down(_ctx.wall_texts, Wall::Role::Editor);
- _editor_wall.scroll(-ConfigWall::scroll_step);
- break;
- case PultAction::ClearEditor: Wall::Text::clear_ToFloor(_ctx.wall_texts, _ctx.words, Wall::Role::Editor);
- _editor_wall.clear();
- break;
- case PultAction::ConnectEditor: wallconnect_BeginEditor(); break;
+ case PultAction::Compile: on_Compile (); break;
+ case PultAction::RunProgram: on_Run (); break;
+ case PultAction::ScrollEditorUp: on_EditorScroll( 1); break;
+ case PultAction::ScrollEditorDown: on_EditorScroll(-1); break;
+ case PultAction::ClearEditor: on_EditorClear (); break;
+ case PultAction::ConnectEditor: wallconnect_BeginEditor (); break;
case PultAction::ConnectProgram: wallconnect_BeginProgram(); break;
}
}
@@ -585,34 +577,34 @@ GameState::handle_Input(FrameInput& input, double dt) {
}
}
- if (_wall_connection.get_Target() == WallConnection::Target::Editor) {
- WallConnection::Request req = _wall_connection.feed_Vim(input.vim_keys, _ctx.camera->pos.y);
+ if (_wall_connection.get_Target() == Wall::Connection::Target::Editor) {
+ Wall::Connection::Request req = _wall_connection.feed_Vim(input.vim_keys, _ctx.camera->pos.y);
input.vim_keys.clear();
switch (req) {
- case WallConnection::Request::Write:
+ case Wall::Connection::Request::Write:
editor_CommitToWall(_wall_connection.get_Lines(), false);
_wall_connection.mark_Saved();
break;
- case WallConnection::Request::WriteQuit:
+ case Wall::Connection::Request::WriteQuit:
editor_CommitToWall(_wall_connection.get_Lines(), true);
_wall_connection.end();
_frame_input.capture_vim = false;
break;
- case WallConnection::Request::Exit:
+ case Wall::Connection::Request::Exit:
editor_CommitToWall(_wall_connection.get_Lines(), true);
_wall_connection.end();
_frame_input.capture_vim = false;
break;
- case WallConnection::Request::QuitDiscard:
+ case Wall::Connection::Request::QuitDiscard:
editor_CommitToWall(_wall_connection.get_Lines(), true);
_wall_connection.end();
_frame_input.capture_vim = false;
break;
- case WallConnection::Request::None:
+ case Wall::Connection::Request::None:
break;
}
}
- else if (_wall_connection.get_Target() == WallConnection::Target::Program) {
+ else if (_wall_connection.get_Target() == Wall::Connection::Target::Program) {
_wall_connection.set_ProgramBuffer(input.typing_buffer);
}
else if (input.typing_mode) {
@@ -802,17 +794,9 @@ GameState::render(SDL_Window& window, GlRenderer& renderer)
{ FP_ZONE("render.hint"); _hint_sys ->draw(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.pult_labels"); _pult_sys ->draw_Labels(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.particles"); _particle_sys->draw(renderer, *_ctx.camera, *_ctx.light); }
- { FP_ZONE("render._floor_sys"); _floor_sys ->draw(renderer, *_ctx.camera, *_ctx.light); }
-
- { FP_ZONE("render.celebration");
- _celebration_sys->draw(renderer, *_ctx.camera, *_ctx.light);
- }
-
- { FP_ZONE("render.wall_edit");
- _wall_connection.render(renderer, *_ctx.camera, *_ctx.light);
- }
-
- { FP_ZONE("render.end_Frame"); renderer.end_Frame(&window); }
+ { FP_ZONE("render.celebration"); _celebration_sys->draw(renderer, *_ctx.camera, *_ctx.light); }
+ { FP_ZONE("render.wall_edit"); _wall_connection.render(renderer, *_ctx.camera, *_ctx.light); }
+ { FP_ZONE("render.end_Frame"); renderer.end_Frame(&window); }
}
@@ -967,7 +951,7 @@ GameState::update_WorldSimulation(double dt)
void
GameState::level_TryAdvance(double dt)
{
- LevelDirector::AdvanceResult res =
+ Level::Director::AdvanceResult res =
_level_director->try_Advance(_ctx, dt, *_celebration_sys, *_particle_sys);
if (!res.stepped) return;
@@ -1091,7 +1075,7 @@ GameState::typing_RebuildPreview(const std::string& s)
{
typing_ClearPreview();
- const Vec3 anchor = CarryMath::get_LineAnchor(*_ctx.camera, CarryMath::CARRY_DIST);
+ const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, ConfigCarry::dist);
const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
std::istringstream in(s);
@@ -1134,7 +1118,7 @@ GameState::carry_BeginAnim(Text3dPtr word, bool with_pitch)
anim.t = 0.0;
anim.complete = false;
- const Vec3 anchor = CarryMath::get_LineAnchor(*_ctx.camera, CarryMath::CARRY_DIST);
+ const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, ConfigCarry::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);
@@ -1199,6 +1183,26 @@ GameState::on_WallConnectExit()
}
+void
+GameState::on_EditorClear()
+{
+ Wall::Text::clear_ToFloor(_ctx.wall_texts, _ctx.words, Wall::Role::Editor);
+ _editor_wall.clear();
+}
+
+void
+GameState::on_EditorScroll(int8_t dt)
+{
+ if (dt > 0) {
+ Wall::Text::scroll_Up(_ctx.wall_texts, Wall::Role::Editor);
+ _editor_wall.scroll(ConfigWall::scroll_step);
+ }
+ else {
+ Wall::Text::scroll_Down(_ctx.wall_texts, Wall::Role::Editor);
+ _editor_wall.scroll(-ConfigWall::scroll_step);
+ }
+}
+
void
GameState::editor_Clear()
{
diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp
index 096c5e0..573abcf 100644
--- a/src/states/game_state.hpp
+++ b/src/states/game_state.hpp
@@ -3,9 +3,9 @@
#include "cpputils/unique_ptr.hpp"
#include "entities/plate.hpp"
#include "istate.hpp"
+#include "levels/director.hpp"
#include "levels/goal.hpp"
#include "levels/scene.hpp"
-#include "rendering/floor_mandelbrot.hpp"
#include "states/game_context.hpp"
#include "systems/carry_system.hpp"
#include "systems/celebration_system.hpp"
@@ -15,7 +15,6 @@
#include "systems/freelook_system.hpp"
#include "systems/hint_system.hpp"
#include "systems/input_system.hpp"
-#include "systems/level_director.hpp"
#include "systems/particle_system.hpp"
#include "systems/physics_system.hpp"
#include "systems/plate_system.hpp"
@@ -26,7 +25,6 @@
#include "systems/word_ai_system.hpp"
#include "systems/word_anim.hpp"
#include "text_3d/vim_buffer.hpp"
-#include "utils/carry_math.hpp"
#include "utils/globals.hpp"
#include "utils/text.hpp"
#include "wall/editor.hpp"
@@ -35,24 +33,27 @@
class GameState : public IState
{
+ cpputils::unique_ptr<InputSystem> _input_sys;
+ cpputils::unique_ptr<FreelookSystem> _freelook_sys;
- cpputils::unique_ptr<CelebrationSystem> _celebration_sys;
cpputils::unique_ptr<CompilerSystem> _compiler_sys;
- cpputils::unique_ptr<CppHighlightSystem> _highlight_sys;
+ cpputils::unique_ptr<ProgramSystem> _program_sys;
cpputils::unique_ptr<DynLoaderSystem> _dyn_loader_sys;
- cpputils::unique_ptr<FloorMandelbrot> _floor_sys;
- cpputils::unique_ptr<FreelookSystem> _freelook_sys;
+
+
cpputils::unique_ptr<HintSystem> _hint_sys;
- cpputils::unique_ptr<InputSystem> _input_sys;
- cpputils::unique_ptr<LevelDirector> _level_director;
+ cpputils::unique_ptr<Level::Director> _level_director;
+ cpputils::unique_ptr<CelebrationSystem> _celebration_sys;
+ cpputils::unique_ptr<PlateSystem> _plate_sys;
+ cpputils::unique_ptr<PultSystem> _pult_sys;
+
cpputils::unique_ptr<ParticleSystem> _particle_sys;
cpputils::unique_ptr<PhysicsSystem> _physics_sys;
- cpputils::unique_ptr<PlateSystem> _plate_sys;
- cpputils::unique_ptr<ProgramSystem> _program_sys;
cpputils::unique_ptr<ProjectileSystem> _projectiles_sys;
- cpputils::unique_ptr<PultSystem> _pult_sys;
- cpputils::unique_ptr<WordAiSystem> _word_ai_sys;
+ cpputils::unique_ptr<CppHighlightSystem> _highlight_sys;
cpputils::unique_ptr<WordAnimSystem> _word_anim_sys;
+ cpputils::unique_ptr<WordAiSystem> _word_ai_sys;
+
bool _editor_hidden { false };
uint64_t _editor_gen { 0 };
@@ -62,10 +63,10 @@ class GameState : public IState
std::string _typing_last;
CarriedLine _carried;
- Wall::Editor _editor_wall;
+ Wall::Editor _editor_wall;
FrameInput _frame_input;
GameContext _ctx;
- WallConnection _wall_connection;
+ Wall::Connection _wall_connection;
Text::View _editor_file;
@@ -78,8 +79,6 @@ class GameState : public IState
DynArray<WordAnim_Add> _anims_add;
DynArray<WordAnim_Drop> _anims_drop;
- void on_WallConnectExit ();
- void on_WallConnectSubmit ();
void wallconnect_BeginEditor ();
void wallconnect_BeginProgram();
@@ -108,6 +107,10 @@ class GameState : public IState
void on_TypingCancel (FrameInput&);
void on_TypingCommit (FrameInput&);
void on_TypingStart (FrameInput&);
+ void on_EditorScroll (int8_t dt);
+ void on_EditorClear ();
+ void on_WallConnectExit ();
+ void on_WallConnectSubmit();
void carriedline_Update ();
void carry_BeginAnim (Text3dPtr word, bool with_pitch);
diff --git a/src/systems/carry_system.cpp b/src/systems/carry_system.cpp
index ea84023..2f14312 100644
--- a/src/systems/carry_system.cpp
+++ b/src/systems/carry_system.cpp
@@ -1,4 +1,5 @@
#include "carry_system.hpp"
+#include "entities/camera.hpp"
bool
@@ -16,11 +17,11 @@ CarriedLine::push(Text3dPtr w)
if (!first) {
first = std::move(node);
- last = first.get();
+ last = first.get();
}
else {
last->next = std::move(node);
- last = last->next.get();
+ last = last->next.get();
}
size++;
}
@@ -65,7 +66,7 @@ CarriedLine::pop_front()
Text3dPtr word = std::move(first->word);
if (first->next) {
- first = std::move(first->next);
+ first = std::move(first->next);
first->prev = nullptr;
}
else {
@@ -82,9 +83,8 @@ DynArray<CarriedWordNode*>
CarriedLine::nodes() const
{
DynArray<CarriedWordNode*> result;
- for (CarriedWordNode* node = first.get(); node; node = node->next.get()) {
+ for (CarriedWordNode* node = first.get(); node; node = node->next.get())
result.push_back(node);
- }
return result;
}
@@ -93,9 +93,8 @@ DynArray<CarriedWordNode*>
CarriedLine::reverse_nodes() const
{
DynArray<CarriedWordNode*> result;
- for (CarriedWordNode* node = last; node; node = node->prev) {
+ for (CarriedWordNode* node = last; node; node = node->prev)
result.push_back(node);
- }
return result;
}
@@ -111,7 +110,6 @@ CarriedLine::clear()
void
CarriedLine::compact()
{
- // Walk and unlink empties. Easiest: rebuild from non-null words.
DynArray<Text3dPtr> survivors;
for (CarriedWordNode* node = first.get(); node; node = node->next.get())
if (node->word) survivors.push_back(std::move(node->word));
@@ -120,3 +118,63 @@ CarriedLine::compact()
for (Text3dPtr& w : survivors)
push(std::move(w));
}
+
+
+
+Vec3
+CarriedLine::get_LineAnchor(const Camera& camera, double carry_dist)
+{
+ const Vec3& right = camera.right;
+ const Vec3 up = Math::cross(camera.right, camera.fwd);
+
+ return {
+ camera.pos.x + camera.fwd.x * carry_dist
+ + right.x * ConfigCarry::hand_right - up.x * ConfigCarry::hand_down,
+ camera.pos.y + camera.fwd.y * carry_dist
+ + right.y * ConfigCarry::hand_right - up.y * ConfigCarry::hand_down,
+ camera.pos.z + camera.fwd.z * carry_dist
+ + right.z * ConfigCarry::hand_right - up.z * ConfigCarry::hand_down
+ };
+}
+
+
+DynArray<Vec3>
+CarriedLine::get_SlotPosition(const Vec3& anchor,
+ const Vec3& right,
+ double carry_gap)
+{
+ DynArray<Vec3> positions;
+ DynArray<CarriedWordNode*> nodes = reverse_nodes();
+
+ if (nodes.empty()) return positions;
+
+ double current_offset = 0.0;
+
+ for (size_t i = 0; i < nodes.size(); i++) {
+ if (!nodes[i]->word) continue;
+
+ positions.push_back({
+ anchor.x + right.x * current_offset,
+ anchor.y + right.y * current_offset,
+ anchor.z + right.z * current_offset
+ });
+
+ if (i + 1 < nodes.size() && nodes[i + 1]->word) {
+ double a = 0.5, b = 0.5;
+
+ if (nodes[i]->word->type == ObjectType::Text)
+ a = static_cast<const Text3d&>(*nodes[i]->word).half_extent;
+ else
+ a = nodes[i]->word->shape.half_extents.x;
+
+ if (nodes[i+1]->word->type == ObjectType::Text)
+ b = static_cast<const Text3d&>(*nodes[i+1]->word).half_extent;
+ else
+ b = nodes[i+1]->word->shape.half_extents.x;
+
+ current_offset -= (a + b + carry_gap);
+ }
+ }
+
+ return positions;
+}
diff --git a/src/systems/carry_system.hpp b/src/systems/carry_system.hpp
index 06e53eb..5ec9995 100644
--- a/src/systems/carry_system.hpp
+++ b/src/systems/carry_system.hpp
@@ -33,4 +33,10 @@ struct CarriedLine
nodes () const;
DynArray<CarriedWordNode*>
reverse_nodes() const;
+
+ DynArray<Vec3>
+ get_SlotPosition(const Vec3& anchor,
+ const Vec3& right,
+ double carry_gap);
+ static Vec3 get_LineAnchor(const Camera& camera, double carry_dist);
};
diff --git a/src/systems/celebration_system.cpp b/src/systems/celebration_system.cpp
index 2538ae9..ece95f4 100644
--- a/src/systems/celebration_system.cpp
+++ b/src/systems/celebration_system.cpp
@@ -3,6 +3,7 @@
#include "entities/world.hpp"
#include "systems/particle_system.hpp"
+
void
CelebrationSystem::start(const Camera& cam, double& glyph_yaw)
{
@@ -66,17 +67,16 @@ CelebrationSystem::tick(double dt, ParticleSystem& particle_sys)
double f = h - i;
float r=0, g=0, b=0;
switch (i % 6) {
- case 0: r=1; g=(float)f; b=0; break;
- case 1: r=(float)(1-f); g=1; b=0; break;
- case 2: r=0; g=1; b=(float)f; break;
- case 3: r=0; g=(float)(1-f); b=1; break;
- case 4: r=(float)f; g=0; b=1; break;
+ case 0: r=1; g=(float)f; b=0; break;
+ case 1: r=(float)(1-f); g=1; b=0; break;
+ case 2: r=0; g=1; b=(float)f; break;
+ case 3: r=0; g=(float)(1-f); b=1; break;
+ case 4: r=(float)f; g=0; b=1; break;
case 5: r=1; g=0; b=(float)(1-f); break;
}
particle_sys.emit_FireworkBurst(center, SDL_FColor{r, g, b, 1.0f}, 80);
- // Next burst in 0.5..1.0s.
_next_burst = _t + 0.5 + rnd() * 0.5;
}
}
@@ -86,7 +86,7 @@ void
CelebrationSystem::finish(ParticleSystem& particle_sys)
{
_active = false;
- _banner.reset();
+ _banner. reset();
particle_sys.clear();
}
diff --git a/src/systems/hint_system.cpp b/src/systems/hint_system.cpp
index 68eac4e..27e1b9e 100644
--- a/src/systems/hint_system.cpp
+++ b/src/systems/hint_system.cpp
@@ -2,11 +2,12 @@
#include "systems/hint_system.hpp"
#include "utils/config.hpp"
+
HintSystem::HintSystem()
{
- _label.set_Centre(Vec3{ 0.0, World::get_FloorY<double>() + 2.5, 0.0 });
- _label.set_Billboard(true);
- _label.set_Style(ConfigColor::yellow, Text3d::depth_wall, 0.003f);
+ _label.set_Centre (Vec3{ 0.0, World::get_FloorY<double>() + 2.5, 0.0 });
+ _label.set_Billboard (true);
+ _label.set_Style (ConfigColor::yellow, Text3d::depth_wall, 0.003f);
_label.set_Collidable(true);
}
@@ -24,11 +25,14 @@ void
HintSystem::clear()
{
_current_hint.clear();
- _label.clear();
+ _label. clear();
}
+
void
-HintSystem::update(double dt, const Camera& camera) { _label.update(dt, camera); }
+HintSystem::update(double dt, const Camera& camera)
+{ _label.update(dt, camera); }
+
void
HintSystem::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
diff --git a/src/systems/input_system.cpp b/src/systems/input_system.cpp
index c80a8d4..03a50c6 100644
--- a/src/systems/input_system.cpp
+++ b/src/systems/input_system.cpp
@@ -5,6 +5,7 @@
#include "states/menu_state.hpp"
#include "systems/input_system.hpp"
+
void
InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input)
{
@@ -40,8 +41,8 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input)
}
}
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);
+ else if (sym == SDLK_ESCAPE) input.vim_keys.push_back(-2);
+ else if (sym == SDLK_BACKSPACE) input.vim_keys.push_back(-3);
break;
}
if (input.capture_typing) {
@@ -92,19 +93,16 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input)
case SDLK_C: input.commands.push_back(InputCommand::Compile); break;
case SDLK_R: input.commands.push_back(InputCommand::Run); break;
case SDLK_P: input.commands.push_back(InputCommand::DebugPrint); break;
- case SDLK_T: input.commands.push_back(InputCommand::TypingStart); break;
+ case SDLK_T: input.commands.push_back(InputCommand::TypingStart); break;
case SDLK_LSHIFT:
case SDLK_RSHIFT: input.commands.push_back(InputCommand::FreelookStart); break;
case SDLK_ESCAPE: game.change_State(cpputils::make_unique<MenuState>()); break;
case SDLK_PLUS:
if (e.key.mod & SDL_KMOD_CTRL)
- input.commands.push_back(InputCommand::NumberInc);
- break;
+ { input.commands.push_back(InputCommand::NumberInc); } break;
case SDLK_MINUS:
if (e.key.mod & SDL_KMOD_CTRL)
- input.commands.push_back(InputCommand::NumberDec);
- break;
-
+ { input.commands.push_back(InputCommand::NumberDec); } break;
}
break;
}
diff --git a/src/systems/level_runner_system.hpp b/src/systems/level_runner_system.hpp
deleted file mode 100644
index 5f249a5..0000000
--- a/src/systems/level_runner_system.hpp
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma once
-
-#include "levels/level.hpp"
-
-
-class LevelRunner
-{
- const Level::Data*
- _level;
-
- size_t _current_step{0};
-public:
- explicit LevelRunner(const Level::Data& _level);
-
- bool advance ();
-
- bool is_Complete () const { return _current_step >= _level->steps.size(); }
- bool is_DynamicLib () const { return _level->dynamic_lib; }
-
- size_t get_Step () const { return _current_step; }
- const Level::Step&
- get_Current () const { return _level->steps[_current_step]; }
- const std::string&
- get_SourceFile() const { return _level->source_file; }
- DynArray<std::string>
- get_GarantsForStep() const;
-};
diff --git a/src/systems/particle_system.cpp b/src/systems/particle_system.cpp
index dd96552..47fcc45 100644
--- a/src/systems/particle_system.cpp
+++ b/src/systems/particle_system.cpp
@@ -12,7 +12,8 @@ namespace {
// Random point on a unit sphere — used to spread burst velocities evenly.
// Uses the "two uniform angles" method; fine for fireworks.
-Vec3 random_UnitSphere() {
+Vec3 random_UnitSphere()
+{
double u = Random::unit() * 2.0 - 1.0;
double phi = Random::unit() * SDL_PI_D * 2.0;
double s = SDL_sqrt(1.0 - u * u);
@@ -43,12 +44,12 @@ ParticleSystem::emit_FireworkBurst(const Vec3& center,
// Slight upward bias so the burst reads as an explosion, not implosion.
p.vel.y += 1.5;
- p.color = color;
+ p.color = color;
p.age = 0.0;
p.lifetime = 2.5 + Random::unit() * 1.5;
- p.yaw = static_cast<float>(Random::unit() * SDL_PI_D * 2.0);
- p.spin = static_cast<float>((Random::unit() - 0.5) * 12.0); // ± 6 rad/s
- p.alive = true;
+ p.yaw = static_cast<float>(Random::unit() * SDL_PI_D * 2.0);
+ p.spin = static_cast<float>((Random::unit() - 0.5) * 12.0);
+ p.alive = true;
_particles.push_back(p);
}
@@ -76,7 +77,6 @@ ParticleSystem::update(double dt,
p.yaw += p.spin * static_cast<float>(dt);
- // World bounds — vanish on any wall, floor, or ceiling.
if (p.pos.y < -ws + 0.05 || p.pos.y > ws - 0.05 ||
p.pos.x < -ws + 0.05 || p.pos.x > ws - 0.05 ||
p.pos.z < -ws + 0.05 || p.pos.z > ws - 0.05)
@@ -114,17 +114,17 @@ ParticleSystem::draw(GlRenderer& gl, const Camera& cam, const Light& light) cons
{
if (_particles.empty()) return;
- // Glow look: high ambient, low diffuse contribution. Same trick Plate uses.
Light glow = light;
- glow.ambient = { 0.7, 0.7, 0.7 };
+
+ glow.ambient = { 0.7, 0.7, 0.7 };
glow.sun_intensity = 0.3f;
glow.flashlight_intensity = 0.0f;
- Mat4 view = buildViewMat4(cam);
+ Mat4 view = buildViewMat4 (cam);
Mat4 proj = gl.get_Projection();
gl.set_Wireframe(false);
- gl.set_Light(glow);
+ gl.set_Light (glow);
for (const Particle& p : _particles) {
// Fade out over the last 30% of lifetime.
@@ -138,10 +138,10 @@ ParticleSystem::draw(GlRenderer& gl, const Camera& cam, const Light& light) cons
Mat4 model = buildModelMat4(p.pos, p.yaw);
Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model));
- gl.set_MVP(model, mvp);
+ gl.set_MVP (model, mvp);
gl.set_NormalMat(model);
- gl.set_Color(p.color.r * fade, p.color.g * fade, p.color.b * fade);
- gl.draw_Box(PARTICLE_HALF, PARTICLE_HALF, PARTICLE_HALF);
+ gl.set_Color (p.color.r * fade, p.color.g * fade, p.color.b * fade);
+ gl.draw_Box (PARTICLE_HALF, PARTICLE_HALF, PARTICLE_HALF);
}
}
diff --git a/src/systems/physics_system.cpp b/src/systems/physics_system.cpp
index 5d57e11..4ceb678 100644
--- a/src/systems/physics_system.cpp
+++ b/src/systems/physics_system.cpp
@@ -11,9 +11,6 @@
#include "utils/frame_profiler.hpp"
-///////////////////////////////////
-/// Player
-
void
PhysicsSystem::update_Player(Player& p, double acc_x, double acc_z, double dt)
{
@@ -144,47 +141,6 @@ PhysicsSystem::resolve_PlayerVsObjects(Player& player, CarriedLine& carried,
}
-void
-PhysicsSystem::try_Pair(GameObject* a, GameObject* b)
-{
- if (!a || !b) return;
-
- PhysicsMode mi = a->body.mode;
- PhysicsMode mj = b->body.mode;
-
- bool i_dyn = (mi == PhysicsMode::Dynamic);
- bool j_dyn = (mj == PhysicsMode::Dynamic);
- bool i_hov = (mi == PhysicsMode::Hover);
- bool j_hov = (mj == PhysicsMode::Hover);
- bool i_static_solid = (mi == PhysicsMode::Static && a->solid);
- bool j_static_solid = (mj == PhysicsMode::Static && b->solid);
-
- if (!i_dyn && !j_dyn && !i_hov && !j_hov) return;
- if (!Collision::aabb_Overlap(*a, *b)) return;
-
- if (i_dyn && j_hov) {
- b->body.mode = PhysicsMode::Dynamic;
- b->body.vel = a->body.vel * 0.5;
- }
- else if (j_dyn && i_hov) {
- a->body.mode = PhysicsMode::Dynamic;
- a->body.vel = b->body.vel * 0.5;
- }
-
- 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) ) {
- Collision::resolve_Objects(*a, *b);
- }
-}
-
-
-///////////////////////////////////
-/// Objects
-
void
PhysicsSystem::update_Objects(const DynArray<GameObject*>& objects,
double dt, double gravity)
@@ -240,6 +196,44 @@ PhysicsSystem::update_Objects(const DynArray<GameObject*>& objects,
}
+void
+PhysicsSystem::try_Pair(GameObject* a, GameObject* b)
+{
+ if (!a || !b) return;
+
+ PhysicsMode mi = a->body.mode;
+ PhysicsMode mj = b->body.mode;
+
+ bool i_dyn = (mi == PhysicsMode::Dynamic);
+ bool j_dyn = (mj == PhysicsMode::Dynamic);
+ bool i_hov = (mi == PhysicsMode::Hover);
+ bool j_hov = (mj == PhysicsMode::Hover);
+ bool i_static_solid = (mi == PhysicsMode::Static && a->solid);
+ bool j_static_solid = (mj == PhysicsMode::Static && b->solid);
+
+ if (!i_dyn && !j_dyn && !i_hov && !j_hov) return;
+ if (!Collision::aabb_Overlap(*a, *b)) return;
+
+ if (i_dyn && j_hov) {
+ b->body.mode = PhysicsMode::Dynamic;
+ b->body.vel = a->body.vel * 0.5;
+ }
+ else if (j_dyn && i_hov) {
+ a->body.mode = PhysicsMode::Dynamic;
+ a->body.vel = b->body.vel * 0.5;
+ }
+
+ 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) ) {
+ Collision::resolve_Objects(*a, *b);
+ }
+}
+
+
void
PhysicsSystem::resolve_WorldBounds(GameObject& obj)
{
@@ -269,9 +263,6 @@ PhysicsSystem::resolve_WorldBounds(GameObject& obj)
}
-///////////////////////////////////
-/// Projectile
-
void
PhysicsSystem::update_Projectiles(DynArray<Projectile>& projectiles, double dt,
const DynArray<Text3dPtr>& words)
@@ -327,11 +318,12 @@ PhysicsSystem::update_Projectiles(DynArray<Projectile>& projectiles, double dt,
bool
-PhysicsSystem::resolve_SingleBoxCollision(Projectile& proj,
- const Vec3& box_min_raw, const Vec3& box_max_raw,
- const Vec3& word_he, double restitution,
- Vec3& cur, Vec3& prev, Vec3& dir,
- Collision::SweptResult& out_sw)
+PhysicsSystem::resolve_SingleBoxCollision(
+ Projectile& proj,
+ const Vec3& box_min_raw, const Vec3& box_max_raw,
+ const Vec3& word_he, double restitution,
+ Vec3& cur, Vec3& prev, Vec3& dir,
+ Collision::SweptResult& out_sw)
{
constexpr double EPS = 0.001;
@@ -379,7 +371,6 @@ PhysicsSystem::resolve_SingleBoxCollision(Projectile& proj,
}
-
void
PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles,
const DynArray<GameObject*>* statics,
@@ -469,7 +460,6 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles,
}
-
void
PhysicsSystem::SpatialGrid::build(const DynArray<GameObject*>& objs)
{
@@ -493,6 +483,7 @@ PhysicsSystem::SpatialGrid::build(const DynArray<GameObject*>& objs)
}
}
+
void
PhysicsSystem::collide_Grid(const DynArray<GameObject*>& active)
{
diff --git a/src/systems/plate_system.cpp b/src/systems/plate_system.cpp
index 62ee1e1..9bf928a 100644
--- a/src/systems/plate_system.cpp
+++ b/src/systems/plate_system.cpp
@@ -1,11 +1,13 @@
#include "entities/player.hpp"
#include "plate_system.hpp"
#include "states/game_state.hpp"
+#include "levels/director.hpp"
+
void
PlateSystem::update(double dt,
const Player& player,
- const DynArray<StepPlate>& plates)
+ const DynArray<Level::StepPlate>& plates)
{
const double px_min = player.pos.x - static_cast<double>(player.radius);
const double px_max = player.pos.x + static_cast<double>(player.radius);
@@ -14,7 +16,7 @@ PlateSystem::update(double dt,
const double pz_min = player.pos.z - static_cast<double>(player.radius);
const double pz_max = player.pos.z + static_cast<double>(player.radius);
- for (const StepPlate& sp : plates) {
+ for (const Level::StepPlate& sp : plates) {
Plate* plate = sp.plate;
if (!plate) continue;
@@ -24,9 +26,9 @@ PlateSystem::update(double dt,
const Vec3 mx = plate->aabb_Max();
const bool xz_overlap = px_max >= mn.x && px_min <= mx.x &&
- pz_max >= mn.z && pz_min <= mx.z;
+ pz_max >= mn.z && pz_min <= mx.z;
const bool y_close = py_min <= mx.y + 0.2 &&
- py_min >= mn.y - 0.1;
+ py_min >= mn.y - 0.1;
const bool was_active = plate->active;
plate->active = xz_overlap && y_close;
diff --git a/src/systems/plate_system.hpp b/src/systems/plate_system.hpp
index 599e2ac..a6a8a36 100644
--- a/src/systems/plate_system.hpp
+++ b/src/systems/plate_system.hpp
@@ -3,12 +3,14 @@
#include "entities/plate.hpp"
#include "utils/globals.hpp"
+
class Player;
-struct StepPlate;
+namespace Level { struct StepPlate; }
+
class PlateSystem
{
public:
void update(double dt, const Player& player,
- const DynArray<StepPlate>& plates);
+ const DynArray<Level::StepPlate>& plates);
};
diff --git a/src/systems/pult_system.cpp b/src/systems/pult_system.cpp
index 0683ac8..5b957cc 100644
--- a/src/systems/pult_system.cpp
+++ b/src/systems/pult_system.cpp
@@ -1,8 +1,8 @@
#include "entities/camera.hpp"
#include "entities/player.hpp"
#include "pult_system.hpp"
-#include "systems/physics_system.hpp"
#include "rendering/gl_renderer.hpp"
+#include "systems/physics_system.hpp"
#include "text_3d/text_label.hpp"
#include "wall/wall.hpp"
@@ -77,8 +77,8 @@ PultSystem::init(DynArray<GameObjectPtr>& scenery,
_connect_program = connp;
Pult* conne = spawn_pult("Edit", Wall::Role::Editor, lane_3);
- add_btn(conne, "EDIT", PultAction::ConnectEditor, 0.0f, 0.0f);
- _connect_editor = conne;
+ add_btn(conne, "EDIT", PultAction::ConnectEditor, 0.0f, 0.0f);
+ _connect_editor = conne;
}
@@ -112,11 +112,6 @@ PultSystem::handle_ProjectileContact(const ProjectileContactEvent& ev,
}
-// Is the player standing on this button? The button sits on top of the solid
-// pult, so on the ground the player's CENTER is always at least `radius`
-// outside the pult footprint — it can only get over a button by standing on
-// top. So we test the center against the button footprint (small tolerance,
-// NOT expanded by the radius, or brushing the pult's side would trigger it).
static bool
player_OnButton(const Player& p, const PultButton& b)
{
@@ -129,7 +124,6 @@ player_OnButton(const Player& p, const PultButton& b)
if (std::fabs(p.pos.x - b.pos.x) > hx + TOL) return false;
if (std::fabs(p.pos.z - b.pos.z) > hz + TOL) return false;
- // On/above the top face, not sailing far overhead.
const double dy = p.pos.y - (b.pos.y + hy);
return dy >= -0.10 && dy <= 1.8;
}
@@ -143,7 +137,6 @@ PultSystem::handle_PlayerContact(const Player& player,
for (PultButton* btn : buttons) {
const bool over = player_OnButton(player, *btn);
- // Rising edge only: fire once on landing, not every frame stood on it.
if (over && !btn->player_on) {
btn->press();
if (!fired) fired = btn->action;
diff --git a/src/systems/wall_connection.cpp b/src/systems/wall_connection.cpp
index 0484c5a..04f7599 100644
--- a/src/systems/wall_connection.cpp
+++ b/src/systems/wall_connection.cpp
@@ -38,8 +38,12 @@ mono_HalfExtentY()
}
+namespace
+Wall
+{
+
void
-WallConnection::begin_Program(const Vec3& pult_pos)
+Connection::begin_Program(const Vec3& pult_pos)
{
_target = Target::Program;
_pult_pos = pult_pos;
@@ -48,7 +52,7 @@ WallConnection::begin_Program(const Vec3& pult_pos)
}
void
-WallConnection::begin_Editor(const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y)
+Connection::begin_Editor(const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y)
{
_target = Target::Editor;
_pult_pos = pult_pos;
@@ -56,7 +60,7 @@ WallConnection::begin_Editor(const Vec3& pult_pos, const DynArray<std::string>&
}
void
-WallConnection::load_Editor(const DynArray<std::string>& lines, double eye_y)
+Connection::load_Editor(const DynArray<std::string>& lines, double eye_y)
{
std::vector<std::string> v(lines.begin(), lines.end());
_vim.load(v);
@@ -68,7 +72,7 @@ WallConnection::load_Editor(const DynArray<std::string>& lines, double eye_y)
}
void
-WallConnection::end()
+Connection::end()
{
_target = Target::None;
_prog_input.reset(); _prog_last.clear();
@@ -78,7 +82,7 @@ WallConnection::end()
void
-WallConnection::set_ProgramBuffer(const std::string& buffer)
+Connection::set_ProgramBuffer(const std::string& buffer)
{
if (buffer == _prog_last) return;
rebuild_ProgramPreview(buffer);
@@ -86,14 +90,14 @@ WallConnection::set_ProgramBuffer(const std::string& buffer)
}
void
-WallConnection::clear_ProgramPreview()
+Connection::clear_ProgramPreview()
{
_prog_input.reset();
_prog_last. clear();
}
void
-WallConnection::rebuild_ProgramPreview(const std::string& buffer)
+Connection::rebuild_ProgramPreview(const std::string& buffer)
{
Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, World::get_BoxScale());
Vec3 pos = tf.position;
@@ -104,8 +108,8 @@ WallConnection::rebuild_ProgramPreview(const std::string& buffer)
}
-WallConnection::Request
-WallConnection::feed_Vim(DynArray<int>& keys, double eye_y)
+Connection::Request
+Connection::feed_Vim(DynArray<int>& keys, double eye_y)
{
_eye_y = eye_y;
@@ -118,11 +122,11 @@ WallConnection::feed_Vim(DynArray<int>& keys, double eye_y)
Request req = Request::None;
switch (_vim.take_Signal()) {
- case VimBuffer::Signal::Exit: return Request::Exit;
- case VimBuffer::Signal::WriteQuit: return Request::WriteQuit;
- case VimBuffer::Signal::QuitDiscard: return Request::QuitDiscard;
- case VimBuffer::Signal::Write: req = Request::Write; break;
- case VimBuffer::Signal::Help:
+ case VimBufferOLD::Signal::Exit: return Request::Exit;
+ case VimBufferOLD::Signal::WriteQuit: return Request::WriteQuit;
+ case VimBufferOLD::Signal::QuitDiscard: return Request::QuitDiscard;
+ case VimBufferOLD::Signal::Write: req = Request::Write; break;
+ case VimBufferOLD::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");
@@ -139,8 +143,9 @@ WallConnection::feed_Vim(DynArray<int>& keys, double eye_y)
return req;
}
+
DynArray<std::string>
-WallConnection::get_Lines() const
+Connection::get_Lines() const
{
DynArray<std::string> out;
for (const std::string& l : _vim.get_Lines()) out.push_back(l);
@@ -149,7 +154,7 @@ WallConnection::get_Lines() const
void
-WallConnection::rebuild_Editor()
+Connection::rebuild_Editor()
{
const float ws = World::get_BoxScale();
Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
@@ -316,15 +321,15 @@ WallConnection::rebuild_Editor()
void
-WallConnection::rebuild_Cmdline()
+Connection::rebuild_Cmdline()
{
_cmdline.reset();
std::string text;
- if (_vim.get_Mode() == VimBuffer::Mode::Command) text = _vim.get_Cmdline();
+ if (_vim.get_Mode() == VimBufferOLD::Mode::Command) text = _vim.get_Cmdline();
else if (!_status.empty()) text = _status[0];
else if (!_vim.get_Status().empty()) text = _vim.get_Status();
- else text = (_vim.get_Mode() == VimBuffer::Mode::Insert) ? "-- INSERT --" : "-- NORMAL --";
+ else text = (_vim.get_Mode() == VimBufferOLD::Mode::Insert) ? "-- INSERT --" : "-- NORMAL --";
if (text.empty()) return;
// Built at origin; render() places it at the eased _cmd_pos each frame.
@@ -333,7 +338,7 @@ WallConnection::rebuild_Cmdline()
void
-WallConnection::update(double dt, const Camera& cam, float aspect, float fov_y)
+Connection::update(double dt, const Camera& cam, float aspect, float fov_y)
{
if (!is_Active()) return;
_blink += dt;
@@ -370,7 +375,7 @@ WallConnection::update(double dt, const Camera& cam, float aspect, float fov_y)
void
-WallConnection::render(GlRenderer& gl, const Camera& cam, const Light& light) const
+Connection::render(GlRenderer& gl, const Camera& cam, const Light& light) const
{
if (_target == Target::Program) {
if (_prog_input) _prog_input->draw(gl, cam, light);
@@ -403,8 +408,9 @@ WallConnection::render(GlRenderer& gl, const Camera& cam, const Light& light) co
}
}
+
void
-WallConnection::draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const
+Connection::draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const
{
if (!_prog_input) return;
if (SDL_fmod(_blink, 0.8) > 0.4) return;
@@ -426,8 +432,9 @@ WallConnection::draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Ligh
gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f);
}
+
void
-WallConnection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& light) const
+Connection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& light) const
{
if (SDL_fmod(_blink, 0.8) > 0.4) return;
@@ -459,8 +466,9 @@ WallConnection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light
gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f);
}
+
void
-WallConnection::draw_Electric(GlRenderer& gl, const Camera& cam) const
+Connection::draw_Electric(GlRenderer& gl, const Camera& cam) const
{
const Vec3 a = cam.pos;
const Vec3 b = _pult_pos;
@@ -492,3 +500,6 @@ WallConnection::draw_Electric(GlRenderer& gl, const Camera& cam) const
gl.draw_Lines (pts.data(), static_cast<int>(pts.size()) / 3);
gl.set_Wireframe(false);
}
+
+
+} // namespace Wall
diff --git a/src/systems/wall_connection.hpp b/src/systems/wall_connection.hpp
index e1e760b..474fea4 100644
--- a/src/systems/wall_connection.hpp
+++ b/src/systems/wall_connection.hpp
@@ -16,16 +16,18 @@ struct Light;
struct EditorRow
{
DynArray<Text3dPtr>
- toks; // per-token meshes, left→right (decreasing z)
- double left_z { 0.0 }; // z of the row's left edge (start of char 0)
- double char_w { 0.18 }; // mean character width in z (for cursor placement)
- double he_y { 0.2 }; // glyph half-height (for cursor vertical offset)
- double line_x { 0.0 }; // wall-plane x
+ toks; // per-token meshes, left→right (decreasing z)
+ double left_z { 0.0 }; // z of the row's left edge (start of char 0)
+ double char_w { 0.18 }; // mean character width in z (for cursor placement)
+ double he_y { 0.2 }; // glyph half-height (for cursor vertical offset)
+ double line_x { 0.0 }; // wall-plane x
bool has_text { false };
};
+namespace Wall
+{
-class WallConnection
+class Connection
{
public:
enum class Target : uint8_t { None, Program, Editor };
@@ -47,21 +49,21 @@ public:
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 mark_Saved() { _vim.mark_Saved(); }
+ void mark_Saved () { _vim.mark_Saved(); }
void clear_ProgramPreview();
- Request feed_Vim(DynArray<int>& keys, double eye_y);
+ Request feed_Vim (DynArray<int>& keys, double eye_y);
private:
- bool _cmd_pos_init { false };
+ bool _cmd_pos_init { false };
unsigned _vim_seen_version { 0 };
- float _cmd_pitch { 0.0f };
- float _cmd_yaw { 0.0f };
- double _row0_y { 0.0 };
- double _eye_y { 0.0 };
- double _blink { 0.0 };
+ float _cmd_pitch { 0.0f };
+ float _cmd_yaw { 0.0f };
+ double _row0_y { 0.0 };
+ double _eye_y { 0.0 };
+ double _blink { 0.0 };
double _code_left_z { 0.0 };
Target _target{ Target::None };
@@ -74,7 +76,7 @@ private:
std::string _prog_last;
- VimBuffer _vim;
+ VimBufferOLD _vim;
DynArray<std::string> _cached_line_text;
DynArray<EditorRow> _rows;
@@ -88,3 +90,6 @@ private:
void draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const;
void draw_Electric (GlRenderer& gl, const Camera& cam) const;
};
+
+
+} // namespace Wall
diff --git a/src/systems/word_ai_system.cpp b/src/systems/word_ai_system.cpp
index 5cbc296..1ec2a9f 100644
--- a/src/systems/word_ai_system.cpp
+++ b/src/systems/word_ai_system.cpp
@@ -39,10 +39,9 @@ WordAiSystem::update(DynArray<Text3dPtr>& words,
Text3dPtr& w = words[i];
if (!w) continue;
- if (w->ai.state != Text3dAI::State::Idle &&
- w->body.mode != PhysicsMode::AiHover &&
- w->body.mode != PhysicsMode::Hover)
- {
+ if ( w->ai.state != Text3dAI::State::Idle &&
+ w->body.mode != PhysicsMode::AiHover &&
+ w->body.mode != PhysicsMode::Hover) {
for (Text3dPtr& s : w->ai.stolen) {
if (!s) continue;
s->body.mode = PhysicsMode::Dynamic;
@@ -79,7 +78,6 @@ WordAiSystem::update_Idle(Text3d& w, const Player& p, int active_ai_count, doubl
{
if (w.body.mode != PhysicsMode::Hover) return;
-
if (active_ai_count >= _max_active_ai) return;
Vec3 to_player = p.pos - w.pos;
@@ -123,8 +121,8 @@ void WordAiSystem::update_Attack(Text3d& w, const Player& p, CarriedLine& carrie
};
}
- Vec3 to_target = target - w.pos;
- double dist = Math::length(to_target);
+ Vec3 to_target = target - w.pos;
+ double dist = Math::length(to_target);
w.ai.phase_timer -= dt;
if (w.ai.phase_timer <= 0.0) {
@@ -365,8 +363,8 @@ WordAiSystem::update_StolenTrail(Text3d& w, double dt)
w.body.vel.z * w.body.vel.z);
if (speed > 0.5) {
- Vec3 raw_back = { -w.body.vel.x / speed, 0.0, -w.body.vel.z / speed };
- double k = 1.0 - SDL_exp(-3.0 * dt);
+ Vec3 raw_back = { -w.body.vel.x / speed, 0.0, -w.body.vel.z / speed };
+ double k = 1.0 - SDL_exp(-3.0 * dt);
w.ai.trail_dir.x += (raw_back.x - w.ai.trail_dir.x) * k;
w.ai.trail_dir.z += (raw_back.z - w.ai.trail_dir.z) * k;
@@ -382,7 +380,7 @@ WordAiSystem::update_StolenTrail(Text3d& w, double dt)
w.ai.trail_dir = { 1.0, 0.0, 0.0 };
// ── Lerp stolen words toward their slot ─────────────────────────
- double offset = static_cast<double>(w.half_extent);
+ double offset = static_cast<double>(w.half_extent);
double pos_lerp_k = 1.0 - SDL_exp(-8.0 * dt);
Vec3 perp = perpendicular_horizontal(w.ai.trail_dir);
diff --git a/src/systems/word_anim.cpp b/src/systems/word_anim.cpp
index def8a7a..df56a02 100644
--- a/src/systems/word_anim.cpp
+++ b/src/systems/word_anim.cpp
@@ -4,25 +4,28 @@
#include "word_anim.hpp"
#include "utils/config.hpp"
+
static constexpr double ANIM_ADD_DUR = 0.20;
+
static double smoothstep(double t)
{
t = std::clamp(t, 0., 1.);
return t * t * (3. - 2. * t);
}
+
static double lerp_d(double a, double b, double t) { return a + (b - a) * t; }
+
void
-WordAnimSystem::update(DynArray<WordAnim_Add>& anims_add,
- DynArray<WordAnim_Drop>& anims_drop,
- CarriedLine& carried,
- DynArray<Text3dPtr>& words,
- double dt)
+WordAnimSystem::update(DynArray<WordAnim_Add>& anims_add,
+ DynArray<WordAnim_Drop>& anims_drop,
+ CarriedLine& carried,
+ DynArray<Text3dPtr>& words,
+ double dt)
{
double floor_y = World::get_FloorY<double>();
- // Add animations
for (WordAnim_Add& anim : anims_add) {
if (anim.complete) continue;
@@ -52,7 +55,6 @@ WordAnimSystem::update(DynArray<WordAnim_Add>& anims_add,
[](const WordAnim_Add& a) { return a.complete; }),
anims_add.end());
- // Drop animations
for (WordAnim_Drop& drop : anims_drop) {
if (drop.complete) continue;
diff --git a/src/text/handler.cpp b/src/text/handler.cpp
new file mode 100644
index 0000000..f04120d
--- /dev/null
+++ b/src/text/handler.cpp
@@ -0,0 +1,999 @@
+// text/handler.cpp
+//
+// The unified text document. Consolidates four older pieces:
+// GameState::editor_PlaceLines -> relayout_Snapped
+// Wall::Editor (placement/nums) -> resolve_Target / apply_Target / attach_Snapped / maintain_Numbers
+// Text::View (lex mapping) -> render_Source / apply_Highlight
+// Connection::rebuild_Editor -> relayout_Mono (+ shoot-merge attach_Mono)
+//
+// The Handler OWNS its tokens. Snapped is the shoot-and-snap gameplay layout;
+// Mono is the monospace grid vim edits on. Lexing is async: the Handler produces
+// a source string + generation and accepts spans back; GameState owns the thread.
+
+#include <SDL3/SDL.h>
+#include <algorithm>
+#include <sstream>
+#include <string>
+
+#include "cpp_token/cpp_highlight.hpp"
+#include "cpp_token/token_color.hpp"
+#include "entities/camera.hpp"
+#include "entities/world.hpp"
+#include "rendering/gl_renderer.hpp"
+#include "text/handler.hpp"
+#include "utils/config.hpp"
+#include "wall/wall.hpp" // Wall::Text::tilt_TowardEye, get_Transform
+
+namespace Text
+{
+
+// ── monospace metrics (file-local; from wall_connection.cpp) ──────────────────
+namespace {
+
+double mono_CharWidth()
+{
+ static double w = 0.0;
+ if (w == 0.0) {
+ TokenPtr m1 = make_Token("M", Token::depth_wall, 0.003f, Vec3{0,0,0});
+ TokenPtr m2 = make_Token("MM", Token::depth_wall, 0.003f, Vec3{0,0,0});
+ w = 2.0 * (double)m2->half_extent - 2.0 * (double)m1->half_extent;
+ }
+ return w;
+}
+
+double mono_HalfExtentY()
+{
+ static double h = 0.0;
+ if (h == 0.0) {
+ TokenPtr m = make_Token("M", Token::depth_wall, 0.003f, Vec3{0,0,0});
+ h = static_cast<double>(m->half_extent_y);
+ }
+ return h;
+}
+
+// Orange line-number token (digit classifies as a literal, so pin it Static).
+TokenPtr make_Number(const std::string& s, const Vec3& pos, float yaw, float scale)
+{
+ TokenPtr t = make_Token(s, Token::depth_wall, scale, pos, ConfigColor::orange);
+ t->yaw = yaw;
+ t->set_Color(ConfigColor::orange); // -> ColorSource::Static
+ return t;
+}
+
+void set_NumberText(Token* t, const std::string& s)
+{
+ if (t->text == s) return;
+ t->text = s;
+ t->rebuild_Mesh();
+ t->set_Color(ConfigColor::orange);
+}
+
+} // namespace
+
+
+// ── construction / config ─────────────────────────────────────────────────────
+
+Handler::Handler(Wall::Role role, float world_scale, LayoutMode mode)
+ : _role(role), _layout(mode)
+{
+ _tf = Wall::get_Transform(role, world_scale);
+ _line_height = ConfigWall::line_height;
+ _word_spacing = ConfigWall::free_space_dist;
+}
+
+void
+Handler::set_Layout(LayoutMode m)
+{
+ if (m == _layout) return;
+ _layout = m;
+ relayout();
+}
+
+void
+Handler::set_ShowNumbers(bool on, bool both_columns)
+{
+ _show_numbers = on;
+ _both_columns = both_columns;
+}
+
+void
+Handler::set_DefaultStyle(float depth, float scale, double line_height, double word_spacing)
+{
+ _depth = depth;
+ _scale = scale;
+ _line_height = line_height;
+ _word_spacing = word_spacing;
+}
+
+
+// ── content ─────────────────────────────────────────────────────────────────
+
+void
+Handler::clear()
+{
+ _lines.clear();
+ _last_src.clear();
+ mark_Dirty();
+}
+
+void
+Handler::set_Lines(const DynArray<std::string>& lines)
+{
+ _lines.clear();
+ const ColorSource src = _lex_enabled ? ColorSource::Lexed : ColorSource::Static;
+
+ for (const std::string& text : lines) {
+ Line ln;
+ ln.line_height = _line_height;
+ ln.word_spacing = _word_spacing;
+
+ std::istringstream in(text);
+ std::string tk;
+ while (in >> tk) {
+ TokenPtr t = make_Token(tk, _depth, _scale, _tf.position, ConfigColor::wall);
+ t->yaw = _tf.yaw;
+ t->set_ColorSource(src);
+ ln.tokens.push_back(std::move(t));
+ }
+ _lines.push_back(std::move(ln));
+ }
+
+ relayout();
+ mark_Dirty();
+}
+
+void
+Handler::append_Line(const std::string& text)
+{
+ // Scroll existing rows up, drop the new line at the baseline (program wall).
+ for (Line& l : _lines) {
+ l.y += _line_height;
+ for (TokenPtr& t : l.tokens) if (t) t->pos.y += _line_height;
+ }
+
+ Line ln;
+ ln.y = _append_y;
+
+ TokenPtr t = make_Token(text, _depth, _scale, _tf.position, ConfigColor::dark_gray);
+ t->yaw = _tf.yaw;
+ t->pos = _tf.position;
+ t->pos.y = _append_y;
+ t->set_Color(ConfigColor::dark_gray); // Static; program output isn't lexed
+ ln.tokens.push_back(std::move(t));
+
+ _lines.push_back(std::move(ln));
+}
+
+
+// ── ownership transfer ─────────────────────────────────────────────────────────
+
+TokenPtr
+Handler::detach(Token* t)
+{
+ // While editing, the buffer is authoritative — picking a word off the wall
+ // mid-edit would desync it. PickSystem should skip an edit_Active() handler;
+ // refuse here as a backstop.
+ if (edit_Active()) return nullptr;
+
+ for (size_t li = 0; li < _lines.size(); ++li) {
+ Line& l = _lines[li];
+ for (size_t i = 0; i < l.tokens.size(); ++i) {
+ if (l.tokens[i].get() == t) {
+ TokenPtr out = std::move(l.tokens[i]);
+ l.tokens.erase(l.tokens.begin() + static_cast<std::ptrdiff_t>(i));
+ if (l.tokens.empty())
+ _lines.erase(_lines.begin() + static_cast<std::ptrdiff_t>(li));
+ mark_Dirty();
+ return out;
+ }
+ }
+ }
+ return nullptr;
+}
+
+void
+Handler::attach(DynArray<TokenPtr> tokens, const Vec3& impact)
+{
+ if (tokens.empty()) return;
+ if (_layout == LayoutMode::Mono) attach_Mono (tokens, impact);
+ else attach_Snapped(tokens, impact);
+ mark_Dirty();
+}
+
+void
+Handler::clear_ToFloor(DynArray<TokenPtr>& out)
+{
+ const double hover = World::get_FloorY<double>() + 1.5;
+ for (Line& l : _lines) {
+ for (TokenPtr& t : l.tokens) {
+ if (!t) continue;
+ t->body.mode = PhysicsMode::Dynamic;
+ t->body.vel = { 0.0, 1.5, 0.0 };
+ t->body.hover_height = hover;
+ t->set_Color(ConfigColor::floor); // matches old clear_ToFloor (blue)
+ t->set_Depth(Token::depth_floor);
+ t->set_ColorSource(ColorSource::Lint); // free word: reclassifies on AI idle
+ out.push_back(std::move(t));
+ }
+ }
+ _lines.clear();
+ mark_Dirty();
+}
+
+
+// ── Snapped placement (ported from Wall::Editor) ───────────────────────────────
+
+Handler::Target
+Handler::resolve_Target(double y) const
+{
+ DynArray<size_t> order;
+ for (size_t i = 0; i < _lines.size(); ++i)
+ if (!_lines[i].is_Empty()) order.push_back(i);
+
+ if (order.empty())
+ return { Target::Insert, 0, y, 0.0 };
+
+ std::sort(order.begin(), order.end(),
+ [&](size_t a, size_t b){ return _lines[a].y > _lines[b].y; });
+
+ auto glyph_top = [&](size_t li) {
+ return _lines[li].y
+ + (double)_lines[li].tokens.front()->half_extent_y
+ + ConfigWall::merge_top_margin;
+ };
+ auto underline = [&](size_t li) { return _lines[li].y - ConfigWall::underline_drop; };
+
+ // 1) Merge: within a line's glyph band.
+ for (size_t li : order)
+ if (y <= glyph_top(li) && y >= underline(li))
+ return { Target::Merge, li, 0.0, 0.0 };
+
+ const double LH = ConfigWall::line_height;
+
+ // 2) Above the top line.
+ {
+ const size_t top = order.front();
+ if (y > glyph_top(top)) {
+ const double min_y = _lines[top].y + LH;
+ const double new_y = (y > min_y) ? y : min_y;
+ return { Target::Insert, top, new_y, 0.0 };
+ }
+ }
+
+ // 3) Between two lines.
+ for (size_t oi = 0; oi + 1 < order.size(); ++oi) {
+ const size_t hi = order[oi];
+ const size_t lo = order[oi + 1];
+ if (y < underline(hi) && y > glyph_top(lo)) {
+ const double hi_y = _lines[hi].y, lo_y = _lines[lo].y;
+ const double gap = hi_y - lo_y;
+ const double free_top = hi_y - LH;
+ const double free_bot = lo_y + LH;
+
+ if (gap >= 2.0 * LH && y <= free_top && y >= free_bot)
+ return { Target::Insert, lo, y, 0.0 };
+ if (gap >= 2.0 * LH)
+ return { Target::Insert, lo, (y > free_top) ? free_top : free_bot, 0.0 };
+
+ const double deficit = 2.0 * LH - gap;
+ return { Target::Insert, lo, free_top, deficit };
+ }
+ }
+
+ // 4) Below the bottom line.
+ {
+ const size_t bot = order.back();
+ const double max_y = _lines[bot].y - LH;
+ const double new_y = (y < max_y) ? y : max_y;
+ return { Target::Insert, bot, new_y, 0.0 };
+ }
+}
+
+Line&
+Handler::apply_Target(const Target& t)
+{
+ if (t.kind == Target::Merge)
+ return _lines[t.line_index];
+
+ if (t.push_dist > 0.0 && t.line_index < _lines.size()) {
+ const double push_below_y = _lines[t.line_index].y;
+ for (Line& l : _lines) {
+ if (l.y <= push_below_y + 1e-6) {
+ l.y -= t.push_dist;
+ for (TokenPtr& w : l.tokens) if (w) w->pos.y -= t.push_dist;
+ if (l.num_left) l.num_left ->pos.y -= t.push_dist;
+ if (l.num_right) l.num_right->pos.y -= t.push_dist;
+ }
+ }
+ }
+
+ Line fresh;
+ fresh.y = t.new_y;
+ fresh.line_height = _line_height;
+ _lines.push_back(std::move(fresh));
+ return _lines.back();
+}
+
+void
+Handler::attach_Snapped(DynArray<TokenPtr>& block, const Vec3& impact)
+{
+ // Order the run visual left->right (decreasing z on the NegX wall).
+ std::sort(block.begin(), block.end(),
+ [](const TokenPtr& a, const TokenPtr& b){
+ if (!a) return false;
+ if (!b) return true;
+ return a->pos.z > b->pos.z;
+ });
+
+ // Incoming editor tokens are lexed; face the wall.
+ for (TokenPtr& w : block) {
+ if (!w) continue;
+ w->yaw = _tf.yaw;
+ w->set_ColorSource(_lex_enabled ? ColorSource::Lexed : ColorSource::Static);
+ }
+
+ double block_w = 0.0;
+ size_t live = 0;
+ for (const TokenPtr& w : block) {
+ if (!w) continue;
+ if (live) block_w += ConfigWall::free_space_dist;
+ block_w += 2.0 * (double)w->half_extent;
+ ++live;
+ }
+ if (live == 0) return;
+
+ Target t = resolve_Target(impact.y);
+ Line& target = apply_Target(t);
+
+ auto left_edge = [](const Token* w){ return w->pos.z + (double)w->half_extent; };
+ auto right_edge = [](const Token* w){ return w->pos.z - (double)w->half_extent; };
+
+ size_t insert_index = 0;
+ for (const TokenPtr& w : target.tokens)
+ if (w && w->pos.z > impact.z) ++insert_index;
+
+ const size_t n = target.tokens.size();
+ double block_left_z = impact.z + block_w * 0.5;
+
+ if (insert_index > 0) {
+ const double max_left = right_edge(target.tokens[insert_index - 1].get())
+ - ConfigWall::free_space_dist;
+ if (block_left_z > max_left) block_left_z = max_left;
+ }
+ if (insert_index < n) {
+ const double block_right = block_left_z - block_w;
+ const double min_right = left_edge(target.tokens[insert_index].get())
+ + ConfigWall::free_space_dist;
+ if (block_right < min_right) {
+ const double shift = min_right - block_right;
+ for (size_t k = insert_index; k < n; ++k)
+ target.tokens[k]->pos.z -= shift;
+ }
+ }
+
+ // Lay the run out left->right (decreasing z), snap to the row height.
+ double cursor = block_left_z;
+ for (TokenPtr& w : block) {
+ if (!w) continue;
+ const double hw = (double)w->half_extent;
+ const double center_z = cursor - hw;
+ w->pos.y = target.y;
+ w->pos.z = center_z;
+ w->pos.x = _tf.position.x;
+ cursor = center_z - hw - ConfigWall::free_space_dist;
+ }
+
+ // Insert into the line at the chosen slot, in visual order, taking ownership.
+ size_t k = 0;
+ for (TokenPtr& w : block) {
+ if (!w) continue;
+ target.tokens.insert(
+ target.tokens.begin() + static_cast<std::ptrdiff_t>(insert_index + k),
+ std::move(w));
+ ++k;
+ }
+ block.clear();
+}
+
+
+// ── Mono shoot-merge ────────────────────────────────────────────────────────────
+
+bool
+Handler::impact_To_RowCol(const Vec3& impact, int& row, int& col) const
+{
+ if (!_vim || _char_w <= 0.0) return false;
+ const auto& blines = _vim->get_Lines();
+ const int n = static_cast<int>(blines.size());
+ if (n == 0) return false;
+
+ int r = static_cast<int>(SDL_lround((_row0_y - impact.y) / _line_height));
+ r = SDL_clamp(r, 0, n); // n == one past the end == append a new row
+ if (r == n) { row = n; col = 0; return true; }
+
+ const std::string& line = blines[static_cast<size_t>(r)];
+ int c = static_cast<int>(SDL_lround((_code_left_z - impact.z) / _char_w - 0.5));
+ c = SDL_clamp(c, 0, static_cast<int>(line.size()));
+
+ // Snap to the nearest inter-token gap so the shot never bisects a word.
+ auto is_gap = [&](int i) {
+ return i == 0 || i == (int)line.size()
+ || line[(size_t)i] == ' ' || line[(size_t)(i - 1)] == ' ';
+ };
+ if (!is_gap(c)) {
+ int lo = c, hi = c;
+ while (lo > 0 && !is_gap(lo)) --lo;
+ while (hi < (int)line.size() && !is_gap(hi)) ++hi;
+ c = (c - lo <= hi - c) ? lo : hi;
+ }
+
+ row = r; col = c;
+ return true;
+}
+
+void
+Handler::attach_Mono(DynArray<TokenPtr>& block, const Vec3& impact)
+{
+ if (!_vim) { block.clear(); return; }
+
+ int row, col;
+ if (!impact_To_RowCol(impact, row, col)) { block.clear(); return; }
+
+ std::string word;
+ for (const TokenPtr& w : block) {
+ if (!w) continue;
+ if (!word.empty()) word += ' ';
+ word += w->text;
+ }
+ block.clear(); // meshes dropped; re-projection rebuilds them
+ if (word.empty()) return;
+
+ _vim->insert_Word_At(row, col, word);
+
+ _vim_seen_version = _vim->get_Version();
+ relayout_Mono();
+}
+
+
+// ── async lexing (folds in Text::View) ──────────────────────────────────────────
+
+void
+Handler::mark_Dirty()
+{
+ ++_gen;
+ _dirty = true;
+}
+
+std::string
+Handler::render_Source()
+{
+ std::string out;
+ for (Line& line : _lines) {
+ for (size_t i = 0; i < line.tokens.size(); ++i) {
+ Token* t = line.tokens[i].get();
+ if (!t) continue;
+ if (i) out += ' ';
+ t->byte_begin = out.size();
+ out += t->text;
+ t->byte_end = out.size();
+ }
+ out += '\n';
+ }
+ _last_src = out;
+ _dirty = false; // dispatched
+ return out;
+}
+
+void
+Handler::apply_Highlight(const DynArray<HiSpan>& spans, uint64_t gen)
+{
+ if (gen != _gen) return; // stale result; a newer edit is pending
+
+ for (Line& line : _lines) {
+ for (TokenPtr& tp : line.tokens) {
+ Token* t = tp.get();
+ if (!t || t->color_src != ColorSource::Lexed) continue;
+
+ size_t best = 0;
+ TokenType type = TokenType::Identifier;
+ bool found = false;
+ for (const HiSpan& s : spans) {
+ size_t lo = (t->byte_begin > s.byte_begin) ? t->byte_begin : s.byte_begin;
+ size_t hi = (t->byte_end < s.byte_end) ? t->byte_end : s.byte_end;
+ if (lo < hi) {
+ size_t ov = hi - lo;
+ if (ov > best) { best = ov; type = s.type; found = true; }
+ }
+ }
+ if (found)
+ t->color = TokenColor::get(type); // direct: stays ColorSource::Lexed
+ }
+ }
+}
+
+
+// ── queries ─────────────────────────────────────────────────────────────────────
+
+Token*
+Handler::token_At_Byte(size_t off)
+{
+ for (Line& line : _lines)
+ for (TokenPtr& t : line.tokens)
+ if (t && off >= t->byte_begin && off < t->byte_end)
+ return t.get();
+ return nullptr;
+}
+
+DynArray<std::string>
+Handler::as_Lines() const
+{
+ DynArray<std::string> out;
+ out.reserve(_lines.size());
+ for (const Line& line : _lines) {
+ std::string s;
+ for (size_t i = 0; i < line.tokens.size(); ++i) {
+ if (i) s += ' ';
+ if (line.tokens[i]) s += line.tokens[i]->text;
+ }
+ out.push_back(std::move(s));
+ }
+ return out;
+}
+
+
+// ── layout dispatch ─────────────────────────────────────────────────────────────
+
+void
+Handler::relayout()
+{
+ if (_layout == LayoutMode::Mono) relayout_Mono();
+ else relayout_Snapped();
+}
+
+void
+Handler::relayout_Snapped()
+{
+ // Bulk top->bottom placement (was GameState::editor_PlaceLines).
+ const double x_wall = _tf.position.x;
+ const double lh = _line_height;
+ const double gap = 0.5;
+ const double floor_y = World::get_FloorY<double>();
+
+ size_t n_lines = 0;
+ for (const Line& l : _lines) if (!l.is_Empty()) ++n_lines;
+ if (n_lines == 0) return;
+
+ const double bottom_y = floor_y + 1.2;
+ double y = bottom_y + static_cast<double>(n_lines - 1) * lh;
+
+ double max_width = 0.0;
+ for (Line& l : _lines) {
+ if (l.is_Empty()) continue;
+ double total_w = 0.0;
+ for (TokenPtr& t : l.tokens) {
+ t->yaw = _tf.yaw;
+ total_w += 2.0 * (double)t->half_extent;
+ }
+ total_w += gap * static_cast<double>(l.tokens.size() - 1);
+ max_width = SDL_max(max_width, total_w);
+ l.y = y;
+ y -= lh;
+ }
+
+ const double left_z = max_width * 0.5;
+ for (Line& l : _lines) {
+ if (l.is_Empty()) continue;
+ double cursor = left_z;
+ for (TokenPtr& t : l.tokens) {
+ const double half = (double)t->half_extent;
+ const double center_z = cursor - half;
+ t->pos = Vec3{ x_wall, l.y, center_z };
+ cursor = center_z - half - gap;
+ }
+ }
+}
+
+void
+Handler::relayout_Mono()
+{
+ // Project the vim buffer onto a monospace grid (was Connection::rebuild_Editor),
+ // reusing token meshes for unchanged lines. Coloring is deferred to the async
+ // lex (caller marks dirty); this routine is pure geometry + mesh management.
+ if (!_vim) return;
+
+ const auto& blines = _vim->get_Lines();
+ const double lh = _line_height;
+ const int R = _vim->get_Row();
+ const int N = static_cast<int>(blines.size());
+ const double ws = World::get_BoxScale();
+ const double bottom_y = -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);
+
+ _char_w = mono_CharWidth();
+ _he_y = mono_HalfExtentY();
+
+ // Salvage old rows so unchanged lines keep their meshes (reuse only when the
+ // line COUNT is unchanged — matches the original per-keystroke fast path).
+ DynArray<Line> old = std::move(_lines);
+ _lines.clear();
+ const bool can_reuse = (old.size() == blines.size());
+
+ DynArray<std::string> old_text(old.size());
+ for (size_t i = 0; i < old.size(); ++i) {
+ std::string s;
+ for (size_t j = 0; j < old[i].tokens.size(); ++j) {
+ if (j) s += ' ';
+ if (old[i].tokens[j]) s += old[i].tokens[j]->text;
+ }
+ old_text[i] = std::move(s);
+ }
+
+ // Pass 1: build tokens + gutter numbers; measure widest digit + longest line.
+ struct BT { TokenPtr t; size_t col; size_t len; };
+ struct BR { DynArray<BT> toks; TokenPtr num; };
+ DynArray<BR> built;
+ built.reserve(blines.size());
+
+ double gutter_w = 0.0, block_code_w = 0.0;
+ for (size_t i = 0; i < blines.size(); ++i) {
+ const std::string& s = blines[i];
+ block_code_w = SDL_max(block_code_w, _char_w * static_cast<double>(s.size()));
+
+ BR br;
+ if (can_reuse && i < old.size() && old[i].num_left)
+ br.num = std::move(old[i].num_left);
+ else
+ br.num = make_Number(std::to_string(i + 1), _tf.position, _tf.yaw, 0.0022f);
+
+ if (br.num) {
+ const std::string s_no = std::to_string(i + 1);
+ set_NumberText(br.num.get(), s_no);
+ gutter_w = SDL_max(gutter_w, 2.0 * (double)br.num->half_extent);
+ }
+
+ const bool reuse_line = can_reuse && i < old.size() && old_text[i] == s;
+
+ size_t pos = 0, ti = 0;
+ while (pos < s.size()) {
+ while (pos < s.size() && s[pos] == ' ') ++pos;
+ if (pos >= s.size()) break;
+ const size_t start = pos;
+ std::string tk;
+ while (pos < s.size() && s[pos] != ' ') { tk += s[pos]; ++pos; }
+
+ TokenPtr t;
+ if (reuse_line && ti < old[i].tokens.size())
+ t = std::move(old[i].tokens[ti]);
+ else {
+ t = make_Token(tk, Token::depth_wall, 0.003f, _tf.position, ConfigColor::white);
+ t->yaw = _tf.yaw;
+ t->set_ColorSource(ColorSource::Lexed);
+ }
+ br.toks.push_back({ std::move(t), start, tk.size() });
+ ++ti;
+ }
+ built.push_back(std::move(br));
+ }
+
+ const double gutter_gap = 0.6;
+ const double code_u0 = gutter_w + gutter_gap;
+ const double block_w = code_u0 + block_code_w;
+ const double z_block_left = block_w * 0.5;
+ _code_left_z = z_block_left - code_u0;
+
+ // Pass 2: position numbers + tokens on the grid.
+ for (size_t i = 0; i < built.size(); ++i) {
+ const double row_y = _row0_y - static_cast<double>(i) * lh;
+
+ Line ln;
+ ln.y = row_y;
+ ln.line_height = lh;
+ ln.number = static_cast<int>(i) + 1;
+ ln.show_number = true;
+
+ if (built[i].num) {
+ const double he = (double)built[i].num->half_extent;
+ built[i].num->pos = _tf.position;
+ built[i].num->pos.y = row_y;
+ built[i].num->pos.z = z_block_left - he;
+ ln.num_left = std::move(built[i].num);
+ }
+
+ for (BT& bt : built[i].toks) {
+ const double left_z = _code_left_z - static_cast<double>(bt.col) * _char_w;
+ const double tok_w = _char_w * static_cast<double>(bt.len);
+ bt.t->pos = _tf.position;
+ bt.t->pos.y = row_y;
+ bt.t->pos.z = left_z - tok_w * 0.5;
+ ln.tokens.push_back(std::move(bt.t));
+ }
+
+ _lines.push_back(std::move(ln));
+ }
+}
+
+
+// ── line numbers (Snapped) — ported from Wall::Editor::update ──────────────────
+
+void
+Handler::maintain_Numbers(double dt, const Camera& /*cam*/)
+{
+ if (_lines.empty()) return;
+
+ DynArray<size_t> order;
+ order.reserve(_lines.size());
+ for (size_t i = 0; i < _lines.size(); ++i)
+ if (!_lines[i].is_Empty()) order.push_back(i);
+ if (order.empty()) return;
+
+ std::sort(order.begin(), order.end(),
+ [&](size_t a, size_t b){ return _lines[a].y > _lines[b].y; });
+
+ // Global code extents in z, so both columns align with a uniform gap.
+ double code_left = -1e18, code_right = 1e18;
+ for (size_t oi : order)
+ for (TokenPtr& w : _lines[oi].tokens) {
+ if (!w) continue;
+ code_left = SDL_max(code_left, w->pos.z + (double)w->half_extent);
+ code_right = SDL_min(code_right, w->pos.z - (double)w->half_extent);
+ }
+
+ double max_lh = 0.0, max_rh = 0.0;
+ int rank = 1;
+ for (size_t oi : order) {
+ Line& l = _lines[oi];
+ const std::string s = std::to_string(rank);
+
+ if (!l.num_left)
+ l.num_left = make_Number(s, Vec3{_tf.position.x, l.y, 0.0}, _tf.yaw,
+ ConfigWall::line_number_scale);
+ if (_both_columns && !l.num_right)
+ l.num_right = make_Number(s, Vec3{_tf.position.x, l.y, 0.0}, _tf.yaw,
+ ConfigWall::line_number_scale);
+
+ if (l.number != rank) {
+ set_NumberText(l.num_left.get(), s);
+ if (l.num_right) set_NumberText(l.num_right.get(), s);
+ l.number = rank;
+ }
+
+ constexpr double STIFFNESS = 120.0, DAMPING = 12.0;
+ l.bounce_vel += (-STIFFNESS * l.bounce_off - DAMPING * l.bounce_vel) * dt;
+ l.bounce_off += l.bounce_vel * dt;
+
+ max_lh = SDL_max(max_lh, (double)l.num_left->half_extent);
+ if (l.num_right) max_rh = SDL_max(max_rh, (double)l.num_right->half_extent);
+ ++rank;
+ }
+
+ const double ws = World::get_BoxScale();
+ const double EDGE = 0.3;
+ double left_col_z = code_left + ConfigWall::line_number_gap;
+ double right_col_z = code_right - ConfigWall::line_number_gap;
+ left_col_z = SDL_min(left_col_z, ws - EDGE - 2.0 * max_lh);
+ right_col_z = SDL_max(right_col_z, -ws + EDGE + 2.0 * max_rh);
+
+ for (size_t oi : order) {
+ Line& l = _lines[oi];
+ double x = _tf.position.x;
+ for (TokenPtr& w : l.tokens) { if (w) { x = w->pos.x; break; } }
+
+ const double ny = l.y + l.bounce_off;
+ l.num_left->pos = Vec3{ x, ny, left_col_z + (double)l.num_left->half_extent };
+ if (l.num_right)
+ l.num_right->pos = Vec3{ x, ny, right_col_z - (double)l.num_right->half_extent };
+ }
+}
+
+void
+Handler::apply_Tilt(const Camera& cam)
+{
+ if (_face == FaceMode::None) return; // Mono grid stays flat (cursor math)
+
+ // Phase 1 implements the wall case (Pitch toward eye). Yaw/Both (carried,
+ // billboard labels) arrive with the TextLabel fold-in.
+ if (_face != FaceMode::Pitch) return;
+
+ auto tilt = [&](Token* t) {
+ if (t) t->pitch = Wall::Text::tilt_TowardEye(t->pos, cam.pos);
+ };
+ for (Line& l : _lines) {
+ for (TokenPtr& t : l.tokens) tilt(t.get());
+ tilt(l.num_left.get());
+ tilt(l.num_right.get());
+ }
+}
+
+
+// ── per-frame ─────────────────────────────────────────────────────────────────
+
+void
+Handler::update(double dt, const Camera& cam)
+{
+ if (_layout == LayoutMode::Snapped && _show_numbers)
+ maintain_Numbers(dt, cam);
+ apply_Tilt(cam);
+}
+
+void
+Handler::render(GlRenderer& gl, const Camera& cam, const Light& light) const
+{
+ for (const Line& l : _lines) {
+ for (const TokenPtr& t : l.tokens)
+ if (t) t->draw(gl, cam, light);
+ if (_show_numbers) {
+ if (l.num_left) l.num_left ->draw(gl, cam, light);
+ if (l.num_right) l.num_right->draw(gl, cam, light);
+ }
+ }
+}
+
+void
+Handler::scroll(double delta)
+{
+ for (Line& l : _lines) {
+ l.y += delta;
+ for (TokenPtr& t : l.tokens) if (t) t->pos.y += delta;
+ if (l.num_left) l.num_left ->pos.y += delta;
+ if (l.num_right) l.num_right->pos.y += delta;
+ }
+}
+
+
+// ── line-number shooting (Snapped) ──────────────────────────────────────────────
+
+DynArray<Handler::NumberHit>
+Handler::number_Hitboxes() const
+{
+ constexpr double PAD = 0.30;
+ constexpr double DEPTH_X = 0.50;
+
+ DynArray<NumberHit> out;
+ out.reserve(_lines.size() * 2);
+
+ auto push_box = [&](const Token* t, size_t i) {
+ const double hz = (double)t->half_extent + PAD;
+ const double hy = (double)t->half_extent_y + PAD;
+ const Vec3 c = t->pos;
+ out.push_back({ HIT_BASE + i,
+ Vec3{ c.x - DEPTH_X, c.y - hy, c.z - hz },
+ Vec3{ c.x + DEPTH_X, c.y + hy, c.z + hz } });
+ };
+
+ for (size_t i = 0; i < _lines.size(); ++i) {
+ if (_lines[i].num_left) push_box(_lines[i].num_left.get(), i);
+ if (_lines[i].num_right) push_box(_lines[i].num_right.get(), i);
+ }
+ return out;
+}
+
+double
+Handler::scroll_DeltaToEye(size_t line_id, double eye_y) const
+{
+ if (line_id >= _lines.size()) return 0.0;
+ return eye_y - _lines[line_id].y;
+}
+
+void
+Handler::bounce(size_t line_id)
+{
+ if (line_id < _lines.size())
+ _lines[line_id].bounce_vel += ConfigWall::bounce_impulse;
+}
+
+
+// ── Mono cursor support ─────────────────────────────────────────────────────────
+
+Handler::MonoMetrics
+Handler::mono_Metrics() const
+{
+ return { _char_w, _he_y, _code_left_z, _row0_y };
+}
+
+double
+Handler::row_Y(int row) const
+{
+ return _row0_y - static_cast<double>(row) * _line_height;
+}
+
+
+// ── live editing (Mono) ─────────────────────────────────────────────────────────
+
+void
+Handler::begin_Edit(double eye_y)
+{
+ _vim = cpputils::make_unique<VimBuffer>();
+
+ std::vector<std::string> v;
+ for (const std::string& s : as_Lines()) v.push_back(s);
+ _vim->load(v);
+ if (_vim->is_Empty()) _vim->start_Insert();
+ _vim_seen_version = _vim->get_Version();
+
+ _eye_y = eye_y;
+ _face = FaceMode::None; // flat grid so the cursor math holds
+ _status.clear();
+
+ _layout = LayoutMode::Mono;
+ relayout_Mono();
+ mark_Dirty();
+}
+
+void
+Handler::end_Edit()
+{
+ DynArray<std::string> lines;
+ if (_vim) for (const std::string& s : _vim->get_Lines()) lines.push_back(s);
+ _vim.reset();
+
+ _face = FaceMode::Pitch;
+ _layout = LayoutMode::Snapped;
+ set_Lines(lines); // rebuilds + relayout_Snapped + mark_Dirty
+}
+
+Handler::EditReq
+Handler::feed_Keys(DynArray<int>& keys, double eye_y)
+{
+ _eye_y = eye_y;
+ if (!_vim) return EditReq::None;
+
+ 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));
+ }
+
+ EditReq req = EditReq::None;
+ switch (_vim->take_Signal()) {
+ case VimBuffer::Signal::Exit: return EditReq::Exit;
+ case VimBuffer::Signal::WriteQuit: return EditReq::WriteQuit;
+ case VimBuffer::Signal::QuitDiscard: return EditReq::Discard;
+ case VimBuffer::Signal::Write: req = EditReq::Write; break;
+ case VimBuffer::Signal::Help:
+ _status = {
+ "-- VIM HELP --",
+ "i a A o O insert Esc normal h j k l move",
+ "0 $ line ends gg G :N jump line x dd delete",
+ ":w write :q quit :wq write+quit :q! discard",
+ };
+ break;
+ default: break;
+ }
+
+ if (_vim->get_Version() != _vim_seen_version) {
+ _vim_seen_version = _vim->get_Version();
+ relayout_Mono();
+ mark_Dirty();
+ }
+ return req;
+}
+
+void
+Handler::mark_Saved()
+{
+ if (_vim) _vim->mark_Saved();
+}
+
+int
+Handler::cursor_Row() const { return _vim ? _vim->get_Row() : 0; }
+
+int
+Handler::cursor_Col() const { return _vim ? _vim->get_Col() : 0; }
+
+VimBuffer::Mode
+Handler::cursor_Mode() const { return _vim ? _vim->get_Mode() : VimBuffer::Mode::Normal; }
+
+const std::string&
+Handler::cmdline_Text() const
+{
+ static const std::string empty;
+ return _vim ? _vim->get_Cmdline() : empty;
+}
+
+const DynArray<std::string>&
+Handler::status_Lines() const { return _status; }
+
+} // namespace Text
diff --git a/src/text/handler.hpp b/src/text/handler.hpp
new file mode 100644
index 0000000..8736ca2
--- /dev/null
+++ b/src/text/handler.hpp
@@ -0,0 +1,206 @@
+#pragma once
+//
+// text/handler.hpp — Phase 1 contract
+//
+// Text::Handler is the document: the single source of truth for one wall's text.
+// It OWNS its lines/tokens (no more reconstruct-by-sorting in extract_Text, no
+// more parallel Wall::Editor / Text::View / EditorRow). It does layout, async
+// lexing, rotation-to-eye, line numbers, scrolling, render, and the ownership
+// transfer (detach/attach) that the pick/projectile mechanics ride on.
+//
+// One Handler per wall role. Per the agreed split, role-SPECIFIC behavior stays
+// in the systems (program_system feeds lines, compiler writes output,
+// show_TokenInfo writes info rows); the Handler is the layout/lex/render engine
+// those systems push content into.
+//
+// Editor + vim are TWO LAYOUTS of the same lines, never simultaneous:
+// LayoutMode::Snapped — shoot-and-snap placement (resolve_Target/place_OnLine).
+// LayoutMode::Mono — monospace grid; what vim edits on.
+// Switching modes repositions tokens and rebuilds meshes only for changed text,
+// killing the current editor_Clear()+editor_PlaceLines() thrash.
+//
+
+#include <cstdint>
+#include <string>
+
+#include "cpp_token/cpp_highlight.hpp" // HiSpan
+#include "entities/camera.hpp"
+#include "text/line.hpp"
+#include "text/token.hpp"
+#include "utils/globals.hpp"
+#include "utils/light.hpp"
+#include "vim_buffer.hpp" // VimBuffer (owned in Mono mode)
+#include "wall/wall.hpp" // Wall::Role, Wall::Transform, Wall::Position
+
+class GlRenderer;
+
+namespace Text
+{
+
+enum class LayoutMode : uint8_t { Snapped, Mono };
+
+class Handler
+{
+public:
+ // Line-number hitboxes for shooting numbers (was Wall::Editor::NumberHit).
+ // The top bit distinguishes a number hit from a regular token index when a
+ // single id space is shared with the projectile system.
+ static constexpr size_t HIT_BASE = size_t(1) << 32;
+ static bool is_NumberHit(size_t idx) { return idx >= HIT_BASE; }
+ static size_t line_Of (size_t idx) { return idx - HIT_BASE; }
+
+ struct NumberHit { size_t index; Vec3 aabb_Min; Vec3 aabb_Max; };
+
+ struct MonoMetrics { // fed to the vim cursor renderer
+ double char_w;
+ double he_y;
+ double left_z; // z of column 0's left edge
+ double row0_y;
+ };
+
+
+ Handler() = default;
+ Handler(Wall::Role role, float world_scale, LayoutMode mode);
+
+ // ── configuration / render options ────────────────────────────────────
+ void set_Layout (LayoutMode m); // switch + reposition (no full rebuild)
+ void set_FaceMode (FaceMode f) { _face = f; }
+ void set_Lexing (bool on) { _lex_enabled = on; } // info/compiler/program off
+ void set_ShowNumbers (bool on, bool both_columns = false);
+ void set_Bordered (bool on) { _bordered = on; }
+ void set_Underline (bool on) { _underline = on; }
+ void set_DefaultStyle(float depth, float scale, double line_height, double word_spacing);
+
+ Wall::Role role () const { return _role; }
+ LayoutMode layout() const { return _layout; }
+
+ // ── content ────────────────────────────────────────────────────────────
+ void clear();
+ void set_Lines (const DynArray<std::string>& lines); // (re)build from text; relayouts + marks dirty
+ void append_Line(const std::string& text); // program output: scroll up, add at bottom
+ void set_AppendBaseline(double y) { _append_y = y; } // bottom row y for append_Line (program wall)
+
+ // ── ownership transfer (the heart of the rewrite) ─────────────────────
+ // detach: remove a token from its line, hand ownership out (pick / shoot-off).
+ // empties + drops the line if it was the last token. Marks dirty.
+ // attach: take ownership of a token and place it.
+ // Snapped -> resolve_Target + place_OnLine (fuses today's on_LinePlaced +
+ // Wall::Text::from_Drop).
+ // Mono -> SHOOT-MERGE. impact maps to (row,col) via impact_To_RowCol;
+ // col snaps to the nearest inter-token gap (never bisects a
+ // word); the token's text is spliced into the edit buffer
+ // (VimBuffer::insert_Word_At) so it survives the next keystroke.
+ // The incoming mesh is dropped; re-projection rebuilds it.
+ TokenPtr detach(Token* t);
+ void attach(DynArray<TokenPtr> tokens, const Vec3& impact); // a shot run, placed as one block
+ void clear_ToFloor(DynArray<TokenPtr>& out); // was Wall::Text::clear_ToFloor
+
+ // ── async lexing (pull model; GameState owns the CppHighlightSystem) ──
+ // Loop: if (h.is_Dirty()) hl.request(h.render_Source(), h.generation());
+ // if (hl.poll(spans, gen)) h.apply_Highlight(spans, gen);
+ // render_Source assigns byte_begin/byte_end across all tokens (was
+ // Text::View::render_Source); apply_Highlight is the best-overlap span match
+ // (was Text::View::apply_Spans) and ignores stale generations.
+ bool is_Dirty () const { return _lex_enabled && _dirty; }
+ uint64_t generation () const { return _gen; }
+ std::string render_Source();
+ void apply_Highlight(const DynArray<HiSpan>& spans, uint64_t gen);
+ void mark_Dirty ();
+
+ // ── queries (for PickSystem + projectile landing) ─────────────────────
+ // PickSystem walks get_Lines() across all Handlers, tracking the best
+ // {Handler*, Token*}; PickResult carries those instead of a wall index.
+ const DynArray<Line>& get_Lines() const { return _lines; }
+ Token* token_At_Byte (size_t off);
+ DynArray<std::string> as_Lines() const; // for vim load + file sync
+
+ // ── per-frame ──────────────────────────────────────────────────────────
+ void update(double dt, const Camera& cam); // bounce springs, tilt-to-eye, number placement
+ void render(GlRenderer& gl, const Camera& cam, const Light& light) const;
+ void scroll(double delta); // shift every row + its tokens
+
+ // ── line-number shooting (Snapped/editor) ─────────────────────────────
+ DynArray<NumberHit> number_Hitboxes () const;
+ double scroll_DeltaToEye (size_t line_id, double eye_y) const;
+ void bounce (size_t line_id);
+
+ // ── vim cursor support (Mono) ──────────────────────────────────────────
+ MonoMetrics mono_Metrics() const;
+ double row_Y (int row) const;
+
+ // ── live editing (Mono) — Handler owns the VimBuffer ──────────────────
+ // begin_Edit builds the buffer from the current lines and switches to Mono;
+ // Wall::Connection becomes a thin overlay that forwards keys via feed_Keys
+ // and reads cursor_Row/Col/Mode for the cursor box + cmdline. The Write /
+ // WriteQuit / Exit / Discard request that GameState acts on (file sync, end
+ // connection) surfaces from feed_Keys.
+ enum class EditReq : uint8_t { None, Write, WriteQuit, Exit, Discard };
+
+ void begin_Edit (double eye_y);
+ void end_Edit ();
+ bool edit_Active() const { return _layout == LayoutMode::Mono && _vim.get() != nullptr; }
+ EditReq feed_Keys (DynArray<int>& keys, double eye_y); // drives vim, re-projects, re-lexes
+ void mark_Saved ();
+
+ int cursor_Row () const;
+ int cursor_Col () const;
+ VimBuffer::Mode cursor_Mode () const;
+ const std::string& cmdline_Text () const;
+ const DynArray<std::string>& status_Lines () const;
+
+private:
+ DynArray<Line> _lines; // single source of truth
+
+ Wall::Role _role { Wall::Role::Editor };
+ Wall::Transform _tf {}; // cached world placement (pos + yaw)
+ LayoutMode _layout { LayoutMode::Snapped };
+ FaceMode _face { FaceMode::Pitch };
+
+ // style defaults (per-line may override)
+ float _depth { Token::depth_wall };
+ float _scale { 0.003f };
+ double _line_height { 0.0 };
+ double _word_spacing { 0.0 };
+ double _append_y { 0.0 }; // program-wall bottom row
+
+ DynArray<std::string> _status; // multi-line status (vim :help etc.), for the overlay
+
+ bool _lex_enabled { true };
+ bool _show_numbers { false };
+ bool _both_columns { false };
+ bool _bordered { false };
+ bool _underline { false };
+
+ // lexing
+ bool _dirty { false };
+ uint64_t _gen { 0 };
+ std::string _last_src;
+
+ // Mono grid cache (vim layout + cursor)
+ double _char_w { 0.0 };
+ double _he_y { 0.0 };
+ double _code_left_z{ 0.0 };
+ double _row0_y { 0.0 };
+ double _eye_y { 0.0 };
+
+ // Edit model — present only while editing in Mono mode (the editor Handler).
+ cpputils::unique_ptr<VimBuffer> _vim;
+ unsigned _vim_seen_version { 0 };
+
+ // layout
+ void relayout (); // dispatch on _layout
+ void relayout_Snapped();
+ void relayout_Mono ();
+ // Snapped placement internals (lifted from Wall::Editor)
+ struct Target { enum Kind { Merge, Insert } kind; size_t line_index; double new_y; double push_dist; };
+ Target resolve_Target (double y) const;
+ Line& apply_Target (const Target& t);
+ void attach_Snapped (DynArray<TokenPtr>& block, const Vec3& impact);
+ void attach_Mono (DynArray<TokenPtr>& block, const Vec3& impact);
+ void maintain_Numbers(double dt, const Camera& cam); // Snapped: rank + bounce + place
+ void apply_Tilt (const Camera& cam); // FaceMode pitch toward eye
+ // Mono shoot-merge: world impact -> buffer (row,col), col snapped to a gap.
+ bool impact_To_RowCol(const Vec3& impact, int& row, int& col) const;
+};
+
+} // namespace Text
diff --git a/src/text/line.cpp b/src/text/line.cpp
new file mode 100644
index 0000000..b7e8edb
--- /dev/null
+++ b/src/text/line.cpp
@@ -0,0 +1,40 @@
+// text/line.cpp
+
+#include "text/line.hpp"
+
+namespace Text
+{
+
+double
+Line::width(double gap) const
+{
+ if (tokens.empty()) return 0.0;
+ double w = 0.0;
+ for (const TokenPtr& t : tokens)
+ if (t) w += 2.0 * static_cast<double>(t->half_extent);
+ w += gap * static_cast<double>(tokens.size() - 1);
+ return w;
+}
+
+Token*
+Line::token_At_Byte(size_t off) const
+{
+ for (const TokenPtr& t : tokens)
+ if (t && off >= t->byte_begin && off < t->byte_end)
+ return t.get();
+ return nullptr;
+}
+
+void
+Line::clear()
+{
+ tokens.clear();
+ num_left.reset();
+ num_right.reset();
+ number = -1;
+ show_number = false;
+ bounce_off = 0.0;
+ bounce_vel = 0.0;
+}
+
+} // namespace Text
diff --git a/src/text/line.hpp b/src/text/line.hpp
new file mode 100644
index 0000000..8662e90
--- /dev/null
+++ b/src/text/line.hpp
@@ -0,0 +1,55 @@
+#pragma once
+//
+// text/line.hpp — Phase 1 contract
+//
+// Text::Line is the single owning line structure. It replaces three things that
+// each modeled "a row of tokens" for one context:
+// - Wall::Editor::Line (non-owning Text3d*, line numbers, bounce)
+// - Text::Line/Token (logical row for tree-sitter, non-owning view ptr)
+// - EditorRow (vim mode's parallel owned meshes)
+//
+// A Line OWNS its tokens. Layout that needs cross-line context (snapping,
+// pushing neighbors, column alignment) lives on the Handler, not here — Line is
+// mostly data plus a few conveniences. `y` is the row's world height; tokens on
+// the row share it.
+//
+
+#include "text/token.hpp"
+#include "utils/globals.hpp"
+
+namespace Text
+{
+
+struct Line
+{
+ DynArray<TokenPtr> tokens; // OWNS the row's word meshes, visual order
+
+ double y { 0.0 }; // world-space row height (shared by tokens)
+ double line_height { 0.0 }; // spacing to the next row (0 = use Handler default)
+ double word_spacing { 0.0 }; // gap between tokens (0 = use Handler default)
+
+ // Line numbering. The Handler decides numbering policy (one vs two columns,
+ // visibility); the meshes + per-row bounce spring live here because the
+ // bounce is driven per-line when a number is shot (Wall::Editor::bounce).
+ int number { -1 }; // 1-based; -1 = unassigned
+ bool show_number { false };
+ double bounce_off { 0.0 };
+ double bounce_vel { 0.0 };
+ TokenPtr num_left; // left gutter digit (always, when shown)
+ TokenPtr num_right; // right column digit (editor's both_columns only)
+
+ bool is_Empty() const { return tokens.empty(); }
+ size_t count () const { return tokens.size(); }
+
+ // Total visual width = sum of glyph widths + internal gaps. Used by Handler
+ // layout (block centering, snap). gap defaults to the line/Handler spacing.
+ double width(double gap) const;
+
+ // Find the owned token covering a byte offset in the Handler's rendered
+ // source (byte_begin/byte_end are assigned by Handler::render_Source).
+ Token* token_At_Byte(size_t off) const;
+
+ void clear(); // drops tokens + number meshes, resets bounce
+};
+
+} // namespace Text
diff --git a/src/text/token.cpp b/src/text/token.cpp
new file mode 100644
index 0000000..65e383e
--- /dev/null
+++ b/src/text/token.cpp
@@ -0,0 +1,377 @@
+// text/token.cpp
+//
+// Port of text_3d.cpp onto Text::Token. Mesh build, GPU caching, carry-follow
+// update, and the AI sparkle tail are carried over unchanged. The only behavior
+// changes are in color routing (ColorSource instead of the set_Look sentinel)
+// and AI ownership (optional component, constructed by enable_Ai()).
+
+#include <SDL3/SDL.h>
+
+#include "cpp_token/token_color.hpp"
+#include "entities/camera.hpp"
+#include "entities/world.hpp"
+#include "rendering/gl_renderer.hpp"
+#include "systems/cpp_highlight_system.hpp" // CppHighlight::classify_Word
+#include "text/token.hpp"
+#include "utils/globals.hpp"
+
+namespace Text
+{
+
+// ── sparkle-tail helpers (file-local; unchanged from text_3d.cpp) ─────────────
+
+static SDL_FColor
+aiGlintColor(Token::Ai::State s)
+{
+ switch (s) {
+ case Token::Ai::State::Attack: return {1.0f, 0.85f, 0.75f, 1.0f};
+ case Token::Ai::State::Flee: return {1.0f, 1.0f, 0.80f, 1.0f};
+ case Token::Ai::State::Stunned: return {0.85f, 0.85f, 0.95f, 1.0f};
+ case Token::Ai::State::Idle: return {1.0f, 1.0f, 1.0f, 1.0f};
+ }
+ return {1.0f, 1.0f, 1.0f, 1.0f};
+}
+
+static Vec3 v_norm(const Vec3& v)
+{
+ double l = SDL_sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
+ return (l < 1e-9) ? Vec3{0,0,1} : Vec3{v.x/l, v.y/l, v.z/l};
+}
+
+// Camera-facing filled disc (triangle fan). Same vertex layout as the text mesh.
+static void
+append_disc(DynArray<float>& v, DynArray<int>& idx,
+ const Vec3& c, const Vec3& right, const Vec3& up,
+ const Vec3& camN, double radius)
+{
+ constexpr int SEG = 10;
+ const int base = static_cast<int>(v.size()) / 9;
+
+ auto push = [&](const Vec3& p) {
+ v.push_back((float)p.x); v.push_back((float)p.y); v.push_back((float)p.z);
+ v.push_back((float)camN.x); v.push_back((float)camN.y); v.push_back((float)camN.z);
+ v.push_back(1.0f); v.push_back(0.0f); v.push_back(0.0f);
+ };
+
+ push(c);
+ for (int i = 0; i < SEG; ++i) {
+ double ang = static_cast<double>(i) * (2.0 * SDL_PI_D / SEG);
+ double dx = SDL_cos(ang) * radius, dy = SDL_sin(ang) * radius;
+ push(Vec3{ c.x + right.x*dx + up.x*dy,
+ c.y + right.y*dx + up.y*dy,
+ c.z + right.z*dx + up.z*dy });
+ }
+ for (int i = 0; i < SEG; ++i) {
+ idx.push_back(base + 0);
+ idx.push_back(base + 1 + i);
+ idx.push_back(base + 1 + ((i + 1) % SEG));
+ }
+}
+
+// Fast integer hash → float in [0,1). Used for all per-sparkle randomness.
+static double hash01(uint32_t x)
+{
+ x ^= x >> 16; x *= 0x7feb352dU;
+ x ^= x >> 15; x *= 0x846ca68bU;
+ x ^= x >> 16;
+ return (x & 0x00ffffffU) / static_cast<double>(0x01000000);
+}
+
+static SDL_FColor mix_col(SDL_FColor a, SDL_FColor b, float k)
+{
+ return { a.r + (b.r - a.r) * k,
+ a.g + (b.g - a.g) * k,
+ a.b + (b.b - a.b) * k, 1.0f };
+}
+
+
+// ── construction / lifetime ───────────────────────────────────────────────────
+
+Token::Token(const std::string& txt,
+ float dpth, float scle,
+ Vec3 position, SDL_FColor colr)
+ : GameObject(position, ObjectType::Text,
+ CollisionShape{}, RigidBody{PhysicsMode::Hover},
+ true, true, true),
+ text(txt), color(colr), depth(dpth), scale(scle)
+{
+ // Lone-token colour (Lint). Editor tokens get switched to Lexed by the Handler.
+ set_Color(TokenColor::get(CppHighlight::classify_Word(text)));
+ rebuild_Mesh();
+
+ body.hover_phase = Random::unit() * SDL_PI_D * 2.0;
+
+ // AI personality (by value, like the old Text3d ctor).
+ ai.aggressiveness = Random::unit();
+ ai.wiggle_phase = Random::unit() * SDL_PI_D * 2.0;
+ ai.wiggle_freq = 3.0 + Random::unit() * 4.0; // 3–7 rad/s
+ ai.speed = 2.5 + Random::unit() * 2.5;
+}
+
+
+Token::~Token()
+{
+ // Renderer owns the GL context; only delete if buffers were created.
+ if (gpu_vbo || gpu_ebo)
+ GlRenderer::free_MeshBuffers(gpu_vbo, gpu_ebo);
+ if (gpu_vbo_sparkle || gpu_ebo_sparkle)
+ GlRenderer::free_MeshBuffers(gpu_vbo_sparkle, gpu_ebo_sparkle);
+}
+
+
+// ── color ────────────────────────────────────────────────────────────────────
+
+void
+Token::set_Color(SDL_FColor c)
+{
+ color = c; // direct; color_src unchanged (old semantics)
+}
+
+void
+Token::set_Look(SDL_FColor c, float new_depth)
+{
+ // Legacy shim. ConfigColor::keep == {0,-1,0,1} hits the g==-1 "keep" branch.
+ if (c.r == -1.0f) color = TokenColor::get(CppHighlight::classify_Word(text));
+ else if (c.g == -1.0f) { /* keep current colour */ }
+ else color = c;
+ set_Depth(new_depth);
+}
+
+void
+Token::recolor()
+{
+ // Lexed → coloured by Handler::apply_Highlight; Static → pinned; Lint → reclassify.
+ if (color_src == ColorSource::Lint)
+ color = TokenColor::get(CppHighlight::classify_Word(text));
+}
+
+
+void
+Token::set_Depth(float new_depth)
+{
+ if (new_depth == depth) return;
+ depth = new_depth;
+ rebuild_Mesh(); // never touches color
+}
+
+
+// ── misc ───────────────────────────────────────────────────────────────────────
+
+Vec3
+Token::words_centroid(const DynArray<TokenPtr>& words)
+{
+ Vec3 c{};
+ double n = 0.0;
+ for (const TokenPtr& w : words) {
+ if (!w) continue;
+ c += w->pos;
+ n += 1.0;
+ }
+ if (n > 0.0) c = c / n;
+ return c;
+}
+
+
+Vec3
+Token::get_Distance(Vec3 position) const
+{
+ return pos - position;
+}
+
+
+void
+Token::rebuild_Mesh()
+{
+ loaded = mesh.build(font_path, text, 64, depth, scale);
+ if (!loaded) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+ "Text::Token: mesh build failed for \"%s\"", text.c_str());
+ return;
+ }
+
+ float minX = 1e9f, maxX = -1e9f;
+ float minY = 1e9f, maxY = -1e9f;
+ for (const Vec3& v : mesh.verts) {
+ float vx = static_cast<float>(v.x);
+ float vy = static_cast<float>(v.y);
+ if (vx < minX) minX = vx;
+ if (vx > maxX) maxX = vx;
+ if (vy < minY) minY = vy;
+ if (vy > maxY) maxY = vy;
+ }
+ half_extent = (maxX - minX) * 0.5f;
+ half_extent_y = (maxY - minY) * 0.5f;
+
+ shape.half_extents = {
+ static_cast<double>(half_extent),
+ static_cast<double>(half_extent_y),
+ static_cast<double>(depth) * 0.5
+ };
+
+ // Pack the GPU-ready interleaved buffer once. draw() just uploads/binds it.
+ packed_verts.clear();
+ packed_indices.clear();
+ const size_t nv = mesh.verts.size();
+ packed_verts.reserve(nv * 9);
+ for (size_t i = 0; i < nv; i++) {
+ const Vec3& v = mesh.verts[i];
+ const Vec3& n = mesh.vertex_normals[i];
+ const float f = (i < mesh.is_front.size()) ? mesh.is_front[i] : 0.0f;
+ packed_verts.push_back((float)v.x); packed_verts.push_back((float)v.y); packed_verts.push_back((float)v.z);
+ packed_verts.push_back((float)n.x); packed_verts.push_back((float)n.y); packed_verts.push_back((float)n.z);
+ packed_verts.push_back(f); packed_verts.push_back(0.0f); packed_verts.push_back(0.0f);
+ }
+ packed_indices.reserve(mesh.tris.size() * 3);
+ for (const MeshTri& t : mesh.tris) {
+ packed_indices.push_back(t.a);
+ packed_indices.push_back(t.b);
+ packed_indices.push_back(t.c);
+ }
+ gpu_dirty = true; // mesh changed → re-upload on next draw
+}
+
+
+void
+Token::update(const Vec3& camera_pos, const double view_mat[9], float carry_dist)
+{
+ float fx = -static_cast<float>(view_mat[6]);
+ float fy = -static_cast<float>(view_mat[7]);
+ float fz = -static_cast<float>(view_mat[8]);
+ pos.x = camera_pos.x + static_cast<double>(fx * carry_dist);
+ pos.y = camera_pos.y + static_cast<double>(fy * carry_dist);
+ pos.z = camera_pos.z + static_cast<double>(fz * carry_dist);
+ yaw = SDL_atan2f(-fx, -fz);
+}
+
+
+// ── draw (word mesh + AI sparkle tail) ─────────────────────────────────────────
+
+void
+Token::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
+{
+ if (!loaded || packed_indices.empty()) return;
+
+ Mat4 proj = gl.get_Projection();
+ Mat4 view = buildViewMat4 (camera);
+ Mat4 model = buildModelMat4 (pos, yaw, pitch);
+ Mat4 mvp = Mat4::mul (proj, Mat4::mul(view, model));
+
+ gl.set_Wireframe(false);
+ gl.set_Light (light);
+ gl.set_MVP (model, mvp);
+ gl.set_NormalMat(model);
+ gl.set_FaceShade(color.r, color.g, color.b, back_dim, front_tint);
+
+ if (glow_enabled && glow_strength > 0.0f)
+ gl.set_Emissive(color.r, color.g, color.b, glow_strength);
+ else
+ gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
+
+ // Draw the actual word FIRST, while MVP/normal/face-shade are still correct.
+ gl.draw_MeshCached(gpu_vbo, gpu_ebo, gpu_index_count, gpu_dirty,
+ packed_verts.data(), (int)packed_verts.size() / 9,
+ packed_indices.data(), (int)packed_indices.size());
+ gpu_dirty = false;
+
+ gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f);
+ gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false);
+
+ // ── AI sparkle tail (only on active AI state) ─────────────────────────
+ if (ai.state == Ai::State::Idle) return;
+
+ const Uint64 now = SDL_GetTicks();
+
+ // Sample the word's path into a history ring → the trailing tail.
+ if (sparkle_trail_count == 0) {
+ 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
+ for (int i = SPARKLE_TRAIL - 1; i > 0; --i)
+ sparkle_trail[i] = sparkle_trail[i - 1];
+ sparkle_trail[0] = pos;
+ sparkle_last_ms = now;
+ }
+
+ const double t = static_cast<double>(now) * 0.001;
+ const Vec3 fwd = v_norm(camera.fwd);
+ const Vec3 right = v_norm(camera.right);
+ const Vec3 up = v_norm(Vec3{ right.y*fwd.z - right.z*fwd.y,
+ right.z*fwd.x - right.x*fwd.z,
+ right.x*fwd.y - right.y*fwd.x });
+ const Vec3 camN = { -fwd.x, -fwd.y, -fwd.z };
+ const double sz = SDL_max(half_extent, 0.25);
+ const uint32_t wseed = static_cast<uint32_t>(body.hover_phase * 10037.0) | 1u;
+
+ static const SDL_FColor PAL[] = {
+ {1.00f, 0.72f, 0.10f, 1.0f}, // gold
+ {1.00f, 0.25f, 0.65f, 1.0f}, // magenta-pink
+ {0.20f, 0.80f, 1.00f, 1.0f}, // cyan
+ {0.45f, 1.00f, 0.30f, 1.0f}, // lime
+ {1.00f, 0.45f, 0.10f, 1.0f}, // orange
+ {0.65f, 0.40f, 1.00f, 1.0f}, // violet
+ };
+ constexpr int PALN = sizeof(PAL) / sizeof(PAL[0]);
+
+ DynArray<float> sv[PALN];
+ DynArray<int> si[PALN];
+
+ constexpr int PER_NODE = 2;
+ for (int j = 0; j < sparkle_trail_count; ++j) {
+ const double age = (sparkle_trail_count > 1)
+ ? double(j) / double(sparkle_trail_count - 1) : 0.0;
+ const Vec3& node = sparkle_trail[j];
+
+ for (int s = 0; s < PER_NODE; ++s) {
+ uint32_t h = wseed ^ (uint32_t(j) * 2654435761u) ^ (uint32_t(s) * 40503u);
+ if (hash01(h) < 0.6) continue; // sparser → calmer
+
+ double phase = hash01(h ^ 0xA5A5u) * 6.28318;
+ double tw = SDL_pow(SDL_max(0.0, SDL_sin(t * 1.6 + phase)), 3.0); // slower, softer
+ if (tw < 0.04) continue;
+
+ double spread = sz * (0.6 + age * 1.4); // older = wider scatter
+ double jx = (hash01(h ^ 0x1111u) - 0.5) * spread;
+ double jy = (hash01(h ^ 0x2222u) - 0.5) * spread;
+
+ // gentle independent wander so each sparkle drifts in place
+ double wamp = sz * 0.22; // how far it roams
+ double fx = 0.6 + hash01(h ^ 0x7777u) * 0.8; // per-sparkle drift rates
+ double fy = 0.6 + hash01(h ^ 0x8888u) * 0.8;
+ jx += SDL_sin(t * fx + hash01(h ^ 0x5555u) * 6.28318) * wamp;
+ jy += SDL_cos(t * fy + hash01(h ^ 0x6666u) * 6.28318) * wamp;
+
+ Vec3 ctr = { node.x + right.x*jx + up.x*jy + camN.x*0.03,
+ node.y + right.y*jx + up.y*jy + camN.y*0.03,
+ node.z + right.z*jx + up.z*jy + camN.z*0.03 };
+
+ double size_rng = 0.06 + 0.05 * hash01(h ^ 0x3333u); // smaller + varied
+ double outer = size_rng * sz * tw * (1.0 - age * 0.6);
+ if (outer < 1e-4) continue;
+
+ int c = int(hash01(h ^ 0x4444u) * PALN) % PALN;
+ append_disc(sv[c], si[c], ctr, right, up, camN, static_cast<float>(outer));
+ }
+ }
+
+ Mat4 ident = buildModelMat4(Vec3{0,0,0}, 0.0f, 0.0f);
+ gl.set_MVP (ident, Mat4::mul(proj, view));
+ gl.set_NormalMat(ident);
+
+ const SDL_FColor glint = aiGlintColor(ai.state);
+ for (int c = 0; c < PALN; ++c) {
+ if (si[c].empty()) continue;
+ SDL_FColor g = mix_col(PAL[c], glint, 0.15f); // keep only a hint of state
+ gl.set_FaceShade (0.0f, 0.0f, 0.0f, 1.0f, false); // no lit term → no white-out
+ gl.set_Emissive (g.r, g.g, g.b, 0.85f); // pure colour, below clip
+ gl.draw_MeshCached(gpu_vbo_sparkle, gpu_ebo_sparkle,
+ gpu_sparkle_index_count, /*dirty=*/true,
+ sv[c].data(), (int)sv[c].size() / 9,
+ si[c].data(), (int)si[c].size());
+ }
+ gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f);
+ gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false);
+}
+
+} // namespace Text
diff --git a/src/text/token.hpp b/src/text/token.hpp
new file mode 100644
index 0000000..816b9aa
--- /dev/null
+++ b/src/text/token.hpp
@@ -0,0 +1,179 @@
+#pragma once
+//
+// text/token.hpp — Phase 1 contract
+//
+// Text::Token is the renamed, cleaned-up successor to Text3d. It is still the
+// atomic unit: ONE whitespace-delimited word == ONE extruded mesh (TextMesh
+// rasterizes the whole string in one pass, so this granularity is fixed by the
+// mesh layer). It remains a GameObject so physics / pick / projectile code keep
+// working unchanged.
+//
+// Three things change vs. Text3d:
+// 1. Color provenance is explicit (ColorSource) instead of the {0,-1,0}
+// sentinel decoded inside set_Look.
+// 2. byte_begin/byte_end are first-class (were only in Text::View) so a
+// Handler can map tree-sitter spans straight onto the token.
+// 3. The AI state is an OPTIONAL owned component. Free/floor words own one;
+// document tokens (editor/program/info) carry nullptr and stay lean.
+//
+
+#include <SDL3/SDL.h>
+#include <cstdint>
+#include <string>
+
+#include "entities/game_object.hpp" // GameObject, RigidBody, CollisionShape, ObjectType, PhysicsMode
+#include "text_3d/text_mesh.hpp" // TextMesh
+#include "utils/config.hpp" // ConfigColor (defaults)
+#include "utils/globals.hpp" // Vec3, DynArray, cpputils::unique_ptr
+#include "utils/light.hpp"
+
+class Camera;
+class GlRenderer;
+
+namespace Text
+{
+
+// Where a token's color comes from. Replaces the magic-float ConfigColor::keep.
+// Lexed — colored by the owning Handler's tree-sitter spans (apply_Highlight).
+// Lint — self-classified as a lone token: classify_Word(text). Free words on
+// the floor, and AI words returning to Idle, use this.
+// Static — a fixed color set explicitly and never recomputed: line numbers,
+// program output (dark_gray), AI state tints (attack/flee/stunned),
+// info/compiler text.
+enum class ColorSource : uint8_t { Lexed, Lint, Static };
+
+// Rotate-to-eye policy. Stored on the Handler (document-wide) and applied to its
+// tokens each frame; kept here so Token and Handler share the vocabulary.
+// None — fixed yaw/pitch (free floor words).
+// Yaw — follow the eye horizontally (carried words).
+// Pitch — tilt up/down toward the eye, clamped (wall text: tilt_TowardEye).
+// Both — billboard fully (labels).
+enum class FaceMode : uint8_t { None, Yaw, Pitch, Both };
+
+
+struct Token final : public GameObject
+{
+ // ── shared constants (unchanged from Text3d) ──────────────────────────
+ static inline const std::string font_path =
+ "/usr/share/fonts/dejavu/DejaVuSansMono.ttf";
+
+ static inline constexpr float depth_wall = 0.06f;
+ static inline constexpr float depth_floor = 0.14f;
+ static inline constexpr float depth_compiler = 0.02f;
+
+
+ // ── AI component (was Text3dAI; now owned only by free words) ──────────
+ struct Ai
+ {
+ enum class State : uint8_t { Idle, Attack, Flee, Stunned };
+
+ State state = State::Idle;
+ double state_timer = 0.0;
+ double aggressiveness = 0.0;
+ double phase_timer = 0.0;
+ double wiggle_phase = 0.0;
+ double wiggle_freq = 4.0;
+ double speed = 2.0;
+
+ bool in_pause = false;
+ double hover_height_saved = 0.0;
+ double orbit_angle = 0.0;
+ double orbit_dir = 1.0;
+ bool has_stolen = false;
+
+ DynArray<cpputils::unique_ptr<Token>> stolen; // carried-off loot
+ Vec3 trail_dir { 1.0, 0.0, 0.0 };
+ };
+
+
+ // ── text / appearance ─────────────────────────────────────────────────
+ std::string text;
+ SDL_FColor color;
+ TextMesh mesh;
+
+ ColorSource color_src { ColorSource::Lint };
+
+ float depth;
+ float scale;
+ float half_extent { 0.0f }; // width half-extent (along the glyph row)
+ float half_extent_y { 0.2f }; // height half-extent
+ float pitch { 0.0f };
+
+ bool loaded { false };
+ bool front_tint { true };
+ bool glow_enabled { false };
+ float glow_strength { 0.2f };
+ float back_dim { 0.45f };
+ Vec3 ribbon_local {};
+
+
+ // ── byte span (valid only while owned by a Handler; set by render_Source)
+ size_t byte_begin { 0 };
+ size_t byte_end { 0 };
+
+
+ // ── AI state (by-value; idle on document tokens, animated only on free
+ // words by WordAiSystem — matches the old Text3d::ai member exactly) ──
+ Ai ai;
+
+
+ // ── GPU caches (mutable; identical to Text3d) ─────────────────────────
+ mutable bool gpu_dirty { true };
+ mutable int gpu_index_count { 0 };
+ mutable unsigned int gpu_vbo { 0 };
+ mutable unsigned int gpu_ebo { 0 };
+ mutable unsigned int gpu_vbo_sparkle { 0 };
+ mutable unsigned int gpu_ebo_sparkle { 0 };
+ mutable int gpu_sparkle_index_count { 0 };
+
+ static constexpr int SPARKLE_TRAIL = 14;
+ mutable int sparkle_trail_count { 0 };
+ mutable Uint64 sparkle_last_ms { 0 };
+ mutable Vec3 sparkle_trail[SPARKLE_TRAIL];
+
+ DynArray<int> packed_indices;
+ DynArray<float> packed_verts;
+
+
+ // ── lifetime ───────────────────────────────────────────────────────────
+ Token(const std::string& txt,
+ float depth, float scale,
+ Vec3 position,
+ SDL_FColor colr = ConfigColor::blue);
+ ~Token() override;
+
+
+ // ── colour ────────────────────────────────────────────────────────────
+ // set_Color — set the colour directly (does NOT change color_src; matches
+ // old Text3d::set_Color, so existing call sites keep working).
+ // set_Look — legacy shim: ConfigColor::keep keeps colour, an r==-1
+ // sentinel reclassifies, otherwise sets colour; then depth.
+ // recolor — Lint: reclassify from text. Lexed/Static: no-op.
+ // color_src is only consulted by Handler::apply_Highlight (which recolours
+ // its Lexed tokens) and recolor(); free/floor tokens ignore it.
+ void set_Color (SDL_FColor c);
+ void set_Look (SDL_FColor c, float new_depth);
+ void set_ColorSource(ColorSource src) { color_src = src; }
+ void recolor ();
+
+ void set_Depth (float depth); // rebuilds mesh only if depth changed; never touches colour
+ void rebuild_Mesh();
+
+
+ // ── misc (unchanged) ───────────────────────────────────────────────────
+ static Vec3 words_centroid(const DynArray<cpputils::unique_ptr<Token>>& words);
+ Vec3 get_Distance(Vec3 position) const;
+ void update (const Vec3& camera_pos, const double view_mat[9], float carry_dist = 1.5f);
+ void draw (GlRenderer& gl, const Camera& camera, const Light& light) const override;
+};
+
+
+using TokenPtr = cpputils::unique_ptr<Token>;
+
+template<typename... Args>
+TokenPtr make_Token(Args&&... args)
+{
+ return cpputils::make_unique<Token>(std::forward<Args>(args)...);
+}
+
+} // namespace Text
diff --git a/src/text/vim_buffer.cpp b/src/text/vim_buffer.cpp
new file mode 100644
index 0000000..dbef74e
--- /dev/null
+++ b/src/text/vim_buffer.cpp
@@ -0,0 +1,265 @@
+#include "vim_buffer.hpp"
+
+
+void
+VimBuffer::load(const std::vector<std::string>& src)
+{
+ _lines = src;
+ if (_lines.empty()) _lines.push_back("");
+ _row = _col = 0; _mode = Mode::Normal;
+ _count = 0;
+ _sig = Signal::None; ++_version;
+ _saved = _lines;
+ _dirty = false;
+
+ _pending.clear();
+ _cmd .clear();
+ _status .clear();
+}
+
+
+void
+VimBuffer::key_char(char c)
+{
+ if (_mode == Mode::Insert) { insert_Char(c); }
+ else if (_mode == Mode::Command) { _cmd.push_back(c); touch(); }
+ else { normal_Key(c); }
+}
+
+void
+VimBuffer::key_enter()
+{
+ if (_mode == Mode::Insert) { split_Line(); }
+ else if (_mode == Mode::Command) { run_Command(); }
+ else { _row = get_ClampRow(_row + 1); clamp_Col(); touch(); }
+}
+
+void VimBuffer::key_backspace()
+{
+ if (_mode == Mode::Insert) { backspace_Insert(); }
+ else if (_mode == Mode::Command) { if (_cmd.size() > 1) _cmd.pop_back(); else cancel_Command(); touch(); }
+ else { _col = (_col > 0 ? _col - 1 : 0); touch(); }
+}
+
+void VimBuffer::key_escape()
+{
+ if (_mode == Mode::Command) { cancel_Command(); return; }
+ _mode = Mode::Normal;
+ if (_col > 0) --_col;
+ clamp_Col(); touch();
+}
+
+
+VimBuffer::Signal
+VimBuffer::take_Signal()
+{ Signal s = _sig; _sig = Signal::None; return s; }
+
+
+void
+VimBuffer::mark_Saved()
+{ _saved = _lines; _dirty = false; touch(); }
+
+
+// Shoot-merge entry point. Splices a whole word into a line so it tokenizes
+// back out as its own token; the caller (Text::Handler::attach) has already
+// snapped `col` to an inter-token gap. Supports row == size() (append a row).
+void
+VimBuffer::insert_Word_At(int row, int col, const std::string& word)
+{
+ if (word.empty()) return;
+ if (_lines.empty()) _lines.push_back("");
+
+ if (row < 0) row = 0;
+ if (row >= (int)_lines.size()) {
+ _lines.resize((size_t)row + 1); // pad with blank rows (at most one, by caller)
+ col = 0;
+ }
+
+ std::string& L = _lines[(size_t)row];
+ if (col < 0) col = 0;
+ if (col > (int)L.size()) col = (int)L.size();
+
+ // Add separating spaces only where the splice would otherwise fuse with
+ // neighbouring text, so the new word stays a standalone token.
+ std::string ins;
+ if (col > 0 && L[(size_t)(col - 1)] != ' ') ins += ' ';
+ ins += word;
+ if (col < (int)L.size() && L[(size_t)col] != ' ') ins += ' ';
+
+ L.insert((size_t)col, ins);
+
+ // Keep the cursor on the same character if the splice landed at/left of it.
+ if (row == _row && col <= _col) _col += (int)ins.size();
+
+ _dirty = true;
+ clamp_Col();
+ touch();
+}
+
+
+void
+VimBuffer::touch()
+{ ++_version; }
+
+int
+VimBuffer::get_LineLen() const
+{ return (int)_lines[(size_t)_row].size(); }
+
+int
+VimBuffer::get_ClampRow(int r) const
+{ int n=(int)_lines.size(); return r<0?0:(r>=n?n-1:r); }
+
+void
+VimBuffer::clamp_Col()
+{
+ int maxc = (_mode==Mode::Insert) ? get_LineLen() : (get_LineLen()>0 ? get_LineLen()-1 : 0);
+ if (_col < 0) _col = 0;
+ if (_col > maxc) _col = maxc;
+}
+
+int
+VimBuffer::get_Eff() const
+{ return _count > 0 ? _count : 1; }
+
+void
+VimBuffer::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; }
+ _count = _count * 10 + (c - '0'); return;
+ }
+
+ if (_pending == "d") { _pending.clear();
+ if (c == 'd') { delete_Lines(get_Eff()); _count = 0;
+ touch(); return; }
+ _count = 0; return;
+ }
+ if (_pending == "g") { _pending.clear();
+ if (c == 'g') { _row = get_ClampRow(_count > 0 ? _count - 1 : 0);
+ _count = 0; clamp_Col(); touch(); return; }
+ _count = 0; return;
+ }
+
+ switch (c) {
+ case 'h': _col -= get_Eff(); break;
+ case 'l': _col += get_Eff(); break;
+ case 'j': _row = get_ClampRow(_row + get_Eff()); break;
+ case 'k': _row = get_ClampRow(_row - get_Eff()); break;
+ case '0': _col = 0; break;
+ case '$': _col = get_LineLen() > 0 ? get_LineLen() - 1 : 0; break;
+ case 'x': delete_Chars(get_Eff()); break;
+ case 'i': _mode = Mode::Insert; break;
+ case 'a': _col = (get_LineLen() > 0 ? _col + 1 : 0); _mode = Mode::Insert; break;
+ case 'A': _col = get_LineLen(); _mode = Mode::Insert; break;
+ case 'o': open_Below(); _mode = Mode::Insert; break;
+ case 'O': open_Above(); _mode = Mode::Insert; break;
+ case 'G': _row = get_ClampRow(_count > 0 ? _count - 1 : (int)_lines.size() - 1);break;
+ case 'd': _pending = "d"; return;
+ case 'g': _pending = "g"; return;
+ default: break;
+ }
+ _count = 0; clamp_Col(); touch();
+}
+
+void
+VimBuffer::insert_Char(char c)
+{
+ std::string& L = _lines[(size_t)_row];
+ if (_col > (int)L.size()) _col = (int)L.size();
+ L.insert((size_t)_col, 1, c); ++_col; _dirty = true; touch();
+}
+
+void
+VimBuffer::split_Line()
+{
+ std::string& L = _lines[(size_t)_row];
+ std::string tail = L.substr((size_t)_col);
+ L.erase((size_t)_col);
+ _lines.insert(_lines.begin() + _row + 1, tail);
+ ++_row; _col = 0; _dirty = true; touch();
+}
+
+void
+VimBuffer::backspace_Insert()
+{
+ if (_col > 0) { _lines[(size_t)_row].erase((size_t)(_col-1), 1); --_col; _dirty = true; }
+ else if (_row > 0) {
+ int prevlen = (int)_lines[(size_t)(_row-1)].size();
+ _lines[(size_t)(_row-1)] += _lines[(size_t)_row];
+ _lines.erase(_lines.begin() + _row);
+ --_row; _col = prevlen; _dirty = true;
+ }
+ touch();
+}
+
+void
+VimBuffer::delete_Chars(int n)
+{
+ std::string& L = _lines[(size_t)_row];
+ for (int i = 0; i < n && _col < (int)L.size(); ++i) { L.erase((size_t)_col, 1); _dirty = true; }
+ clamp_Col();
+}
+
+void
+VimBuffer::delete_Lines(int n)
+{
+ for (int i = 0; i < n && (int)_lines.size() > 0; ++i) {
+ if (_lines.size() == 1) { _lines[0].clear(); _dirty = true; break; }
+ _lines.erase(_lines.begin() + _row);
+ if (_row >= (int)_lines.size()) _row = (int)_lines.size() - 1;
+ _dirty = true;
+ }
+ clamp_Col();
+}
+void
+VimBuffer::open_Below()
+{ _lines.insert(_lines.begin() + _row + 1, ""); ++_row; _col = 0; _dirty = true; }
+
+void
+VimBuffer::open_Above()
+{ _lines.insert(_lines.begin() + _row, ""); _col = 0; _dirty = true; }
+
+void
+VimBuffer::run_Command()
+{
+ std::string c = _cmd.size() ? _cmd.substr(1) : "";
+ _cmd.clear(); _mode = Mode::Normal;
+
+ if (c == "w" || c == "write") {
+ _sig = Signal::Write;
+ }
+ else if (c == "wq" || c == "x") {
+ _sig = Signal::WriteQuit;
+ }
+ else if (c == "q" || c == "quit") {
+ if (_dirty) {
+ _status = "No write since last change (add ! to override)";
+ } else {
+ _sig = Signal::Exit;
+ }
+ }
+ else if (c == "q!" || c == "quit!") {
+ _lines = _saved;
+ _row = get_ClampRow(_row); clamp_Col();
+ _dirty = false;
+ _sig = Signal::QuitDiscard;
+ }
+ else if (!c.empty() && std::all_of(c.begin(), c.end(),
+ [](char ch){ return std::isdigit((unsigned char)ch); })) {
+ _row = get_ClampRow(std::stoi(c) - 1); clamp_Col();
+ }
+ else _status = "not a command: " + c;
+ touch();
+}
+
+void
+VimBuffer::cancel_Command()
+{ _cmd.clear(); _mode = Mode::Normal; touch(); }
+
+bool
+VimBuffer::is_Dirty() const
+{ return _dirty; }
diff --git a/src/text/vim_buffer.hpp b/src/text/vim_buffer.hpp
new file mode 100644
index 0000000..f8919b0
--- /dev/null
+++ b/src/text/vim_buffer.hpp
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <algorithm>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+class VimBuffer {
+public:
+ enum class Mode : uint8_t { Normal, Insert, Command };
+ enum class Signal : uint8_t {
+ None,
+ Exit,
+ Help,
+ Write,
+ WriteQuit,
+ QuitDiscard
+ };
+
+ void key_char (char c);
+ void key_enter ();
+ void key_backspace();
+ void key_escape ();
+
+ void load (const std::vector<std::string>& src);
+ Signal take_Signal();
+ void mark_Saved ();
+
+ // Shoot-merge: splice a whole word into line `row` at character `col`,
+ // adding surrounding spaces so it tokenizes back out as its own token.
+ // `col` is expected pre-snapped to an inter-token gap by the caller
+ // (Text::Handler::attach). Clamps row/col, bumps version, marks dirty.
+ void insert_Word_At(int row, int col, const std::string& word);
+
+ bool is_Empty() const { return _lines.size() == 1 && _lines[0].empty(); }
+
+ const std::vector<std::string>&
+ get_Lines () const { return _lines; }
+ int get_Row () const { return _row; }
+ int get_Col () const { return _col; }
+ Mode get_Mode () const { return _mode; }
+ unsigned
+ get_Version () const { return _version; }
+ const std::string&
+ get_Cmdline () const { return _cmd; }
+ const std::string&
+ get_Status () const { return _status; }
+ void start_Insert() { _mode = Mode::Insert; _col = 0; touch(); }
+
+private:
+ bool _dirty = false;
+ unsigned _version = 0;
+ int _row = 0, _col = 0;
+ int _count = 0;
+
+ std::string _pending;
+ std::string _cmd;
+ std::string _status;
+
+ Mode _mode = Mode::Normal;
+ Signal _sig = Signal::None;
+ std::vector<std::string> _saved;
+ std::vector<std::string> _lines{ "" };
+
+ void touch ();
+ void clamp_Col ();
+ void normal_Key (char c);
+ void insert_Char (char c);
+ void split_Line ();
+ void backspace_Insert();
+ void delete_Chars (int n);
+ void delete_Lines (int n);
+ void open_Below ();
+ void open_Above ();
+ void run_Command ();
+ void cancel_Command ();
+
+ bool is_Dirty () const;
+ int get_LineLen () const;
+ int get_Eff () const;
+ int get_ClampRow (int r) const;
+};
diff --git a/src/text_3d/vim_buffer.cpp b/src/text_3d/vim_buffer.cpp
index 3c0fa79..b67aa1c 100644
--- a/src/text_3d/vim_buffer.cpp
+++ b/src/text_3d/vim_buffer.cpp
@@ -2,7 +2,7 @@
void
-VimBuffer::load(const std::vector<std::string>& src)
+VimBufferOLD::load(const std::vector<std::string>& src)
{
_lines = src;
if (_lines.empty()) _lines.push_back("");
@@ -19,7 +19,7 @@ VimBuffer::load(const std::vector<std::string>& src)
void
-VimBuffer::key_char(char c)
+VimBufferOLD::key_char(char c)
{
if (_mode == Mode::Insert) { insert_Char(c); }
else if (_mode == Mode::Command) { _cmd.push_back(c); touch(); }
@@ -27,21 +27,21 @@ VimBuffer::key_char(char c)
}
void
-VimBuffer::key_enter()
+VimBufferOLD::key_enter()
{
if (_mode == Mode::Insert) { split_Line(); }
else if (_mode == Mode::Command) { run_Command(); }
else { _row = get_ClampRow(_row + 1); clamp_Col(); touch(); }
}
-void VimBuffer::key_backspace()
+void VimBufferOLD::key_backspace()
{
if (_mode == Mode::Insert) { backspace_Insert(); }
else if (_mode == Mode::Command) { if (_cmd.size() > 1) _cmd.pop_back(); else cancel_Command(); touch(); }
else { _col = (_col > 0 ? _col - 1 : 0); touch(); }
}
-void VimBuffer::key_escape()
+void VimBufferOLD::key_escape()
{
if (_mode == Mode::Command) { cancel_Command(); return; }
_mode = Mode::Normal;
@@ -50,30 +50,30 @@ void VimBuffer::key_escape()
}
-VimBuffer::Signal
-VimBuffer::take_Signal()
+VimBufferOLD::Signal
+VimBufferOLD::take_Signal()
{ Signal s = _sig; _sig = Signal::None; return s; }
void
-VimBuffer::mark_Saved()
+VimBufferOLD::mark_Saved()
{ _saved = _lines; _dirty = false; touch(); }
void
-VimBuffer::touch()
+VimBufferOLD::touch()
{ ++_version; }
int
-VimBuffer::get_LineLen() const
+VimBufferOLD::get_LineLen() const
{ return (int)_lines[(size_t)_row].size(); }
int
-VimBuffer::get_ClampRow(int r) const
+VimBufferOLD::get_ClampRow(int r) const
{ int n=(int)_lines.size(); return r<0?0:(r>=n?n-1:r); }
void
-VimBuffer::clamp_Col()
+VimBufferOLD::clamp_Col()
{
int maxc = (_mode==Mode::Insert) ? get_LineLen() : (get_LineLen()>0 ? get_LineLen()-1 : 0);
if (_col < 0) _col = 0;
@@ -81,11 +81,11 @@ VimBuffer::clamp_Col()
}
int
-VimBuffer::get_Eff() const
+VimBufferOLD::get_Eff() const
{ return _count > 0 ? _count : 1; }
void
-VimBuffer::normal_Key(char c)
+VimBufferOLD::normal_Key(char c)
{
if (c == ':') { _mode = Mode::Command; _cmd = ":";
_status.clear(); touch(); return; }
@@ -129,7 +129,7 @@ VimBuffer::normal_Key(char c)
}
void
-VimBuffer::insert_Char(char c)
+VimBufferOLD::insert_Char(char c)
{
std::string& L = _lines[(size_t)_row];
if (_col > (int)L.size()) _col = (int)L.size();
@@ -137,7 +137,7 @@ VimBuffer::insert_Char(char c)
}
void
-VimBuffer::split_Line()
+VimBufferOLD::split_Line()
{
std::string& L = _lines[(size_t)_row];
std::string tail = L.substr((size_t)_col);
@@ -147,7 +147,7 @@ VimBuffer::split_Line()
}
void
-VimBuffer::backspace_Insert()
+VimBufferOLD::backspace_Insert()
{
if (_col > 0) { _lines[(size_t)_row].erase((size_t)(_col-1), 1); --_col; _dirty = true; }
else if (_row > 0) {
@@ -160,7 +160,7 @@ VimBuffer::backspace_Insert()
}
void
-VimBuffer::delete_Chars(int n)
+VimBufferOLD::delete_Chars(int n)
{
std::string& L = _lines[(size_t)_row];
for (int i = 0; i < n && _col < (int)L.size(); ++i) { L.erase((size_t)_col, 1); _dirty = true; }
@@ -168,7 +168,7 @@ VimBuffer::delete_Chars(int n)
}
void
-VimBuffer::delete_Lines(int n)
+VimBufferOLD::delete_Lines(int n)
{
for (int i = 0; i < n && (int)_lines.size() > 0; ++i) {
if (_lines.size() == 1) { _lines[0].clear(); _dirty = true; break; }
@@ -179,15 +179,15 @@ VimBuffer::delete_Lines(int n)
clamp_Col();
}
void
-VimBuffer::open_Below()
+VimBufferOLD::open_Below()
{ _lines.insert(_lines.begin() + _row + 1, ""); ++_row; _col = 0; _dirty = true; }
void
-VimBuffer::open_Above()
+VimBufferOLD::open_Above()
{ _lines.insert(_lines.begin() + _row, ""); _col = 0; _dirty = true; }
void
-VimBuffer::run_Command()
+VimBufferOLD::run_Command()
{
std::string c = _cmd.size() ? _cmd.substr(1) : "";
_cmd.clear(); _mode = Mode::Normal;
@@ -220,9 +220,9 @@ VimBuffer::run_Command()
}
void
-VimBuffer::cancel_Command()
+VimBufferOLD::cancel_Command()
{ _cmd.clear(); _mode = Mode::Normal; touch(); }
bool
-VimBuffer::is_Dirty() const
+VimBufferOLD::is_Dirty() const
{ return _dirty; }
diff --git a/src/text_3d/vim_buffer.hpp b/src/text_3d/vim_buffer.hpp
index c70fdb3..d470e73 100644
--- a/src/text_3d/vim_buffer.hpp
+++ b/src/text_3d/vim_buffer.hpp
@@ -5,7 +5,7 @@
#include <string>
#include <vector>
-class VimBuffer {
+class VimBufferOLD {
public:
enum class Mode : uint8_t { Normal, Insert, Command };
enum class Signal : uint8_t {
diff --git a/src/utils/carry_math.cpp b/src/utils/carry_math.cpp
deleted file mode 100644
index a247648..0000000
--- a/src/utils/carry_math.cpp
+++ /dev/null
@@ -1,64 +0,0 @@
-#include <SDL3/SDL.h>
-
-#include "carry_math.hpp"
-#include "entities/camera.hpp"
-
-
-Vec3
-CarryMath::get_LineAnchor(const Camera& camera, double carry_dist)
-{
- const Vec3& right = camera.right;
- const Vec3 up = Math::cross(camera.right, camera.fwd);
-
- return {
- camera.pos.x + camera.fwd.x * carry_dist
- + right.x * CARRY_HAND_RIGHT - up.x * CARRY_HAND_DOWN,
- camera.pos.y + camera.fwd.y * carry_dist
- + right.y * CARRY_HAND_RIGHT - up.y * CARRY_HAND_DOWN,
- camera.pos.z + camera.fwd.z * carry_dist
- + right.z * CARRY_HAND_RIGHT - up.z * CARRY_HAND_DOWN
- };
-}
-
-
-DynArray<Vec3>
-CarryMath::get_SlotPosition(const CarriedLine& carried,
- const Vec3& anchor,
- const Vec3& right,
- double carry_gap)
-{
- DynArray<Vec3> positions;
- DynArray<CarriedWordNode*> nodes = carried.reverse_nodes();
-
- if (nodes.empty()) return positions;
-
- double current_offset = 0.0;
-
- for (size_t i = 0; i < nodes.size(); i++) {
- if (!nodes[i]->word) continue;
-
- positions.push_back({
- anchor.x + right.x * current_offset,
- anchor.y + right.y * current_offset,
- anchor.z + right.z * current_offset
- });
-
- if (i + 1 < nodes.size() && nodes[i + 1]->word) {
- double a = 0.5, b = 0.5;
-
- if (nodes[i]->word->type == ObjectType::Text)
- a = static_cast<const Text3d&>(*nodes[i]->word).half_extent;
- else
- a = nodes[i]->word->shape.half_extents.x;
-
- if (nodes[i+1]->word->type == ObjectType::Text)
- b = static_cast<const Text3d&>(*nodes[i+1]->word).half_extent;
- else
- b = nodes[i+1]->word->shape.half_extents.x;
-
- current_offset -= (a + b + carry_gap);
- }
- }
-
- return positions;
-}
diff --git a/src/utils/carry_math.hpp b/src/utils/carry_math.hpp
deleted file mode 100644
index c11f35e..0000000
--- a/src/utils/carry_math.hpp
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma once
-
-#include "systems/carry_system.hpp"
-#include "utils/globals.hpp"
-
-
-class Camera;
-
-
-namespace CarryMath
-{
- static constexpr double CARRY_DIST = 1.2;
- static constexpr double CARRY_GAP = 0.18;
-
- static constexpr double CARRY_HAND_RIGHT = 0.45;
- static constexpr double CARRY_HAND_DOWN = 0.35;
-
-
- Vec3 get_LineAnchor (const Camera& camera, double carry_dist);
- DynArray<Vec3>
- get_SlotPosition(const CarriedLine& carried,
- const Vec3& anchor,
- const Vec3& right,
- double carry_gap);
-}
diff --git a/src/utils/config.hpp b/src/utils/config.hpp
index 448f276..b06462e 100644
--- a/src/utils/config.hpp
+++ b/src/utils/config.hpp
@@ -28,6 +28,15 @@ static constexpr double proj_t_max = 1.2; // clamp: farthest throws
}
+namespace ConfigCarry
+{
+static constexpr double dist = 1.2;
+static constexpr double gap = 0.18;
+
+static constexpr double hand_right = 0.45;
+static constexpr double hand_down = 0.35;
+}
+
namespace ConfigColor
{
static inline constexpr SDL_FColor keep = {0.0f, -1.0f, 0.0f, 1.0f};
diff --git a/src/wall/editor.hpp b/src/wall/editor.hpp
index c1cde5f..915fb42 100644
--- a/src/wall/editor.hpp
+++ b/src/wall/editor.hpp
@@ -75,10 +75,10 @@ public:
double scroll_DeltaToEye(size_t line_id, double eye_y) const;
size_t line_Count () const { return _lines.size(); }
DynArray<DynArray<Text3d*>>
- lines_InOrder() const;
+ lines_InOrder () const;
DynArray<NumberHit>
- number_Hitboxes() const;
+ number_Hitboxes () const;
};
} // namespace Wall
diff --git a/src/wall/text.cpp b/src/wall/text.cpp
index 676edcd..920c0f3 100644
--- a/src/wall/text.cpp
+++ b/src/wall/text.cpp
@@ -40,7 +40,7 @@ Text::from_Drop(Text3dPtr src)
Wall::Role role = Wall::role_Of(wall_pos);
- return cpputils::make_unique<Wall::Text>(Wall::Text{
+ return cpputils::make_unique<Text>(Text{
wall_pos,
role,
std::move(src)
@@ -51,7 +51,7 @@ Text::from_Drop(Text3dPtr src)
std::string
Text::extract_Text(const PtrList& texts, Wall::Role role, double line_threshold)
{
- DynArray<Wall::Text*> items;
+ DynArray<Text*> items;
items.reserve(texts.size());
for (const Ptr& wt : texts) {
@@ -65,19 +65,19 @@ Text::extract_Text(const PtrList& texts, Wall::Role role, double line_threshold)
const Wall::Position wall_pos = Wall::position_Of(role);
std::sort(items.begin(), items.end(),
- [](const Wall::Text* a, const Wall::Text* b) {
+ [](const Text* a, const Text* b) {
return a->text3d->pos.y > b->text3d->pos.y;
});
struct Line {
- DynArray<Wall::Text*> items;
+ DynArray<Text*> items;
double sum_y = 0.0;
};
DynArray<Line> lines;
lines.reserve(items.size());
- for (Wall::Text* wt : items) {
+ for (Text* wt : items) {
const double y = wt->text3d->pos.y;
if (!lines.empty()) {
@@ -96,7 +96,7 @@ Text::extract_Text(const PtrList& texts, Wall::Role role, double line_threshold)
for (Line& line : lines) {
std::sort(line.items.begin(), line.items.end(),
- [&](const Wall::Text* a, const Wall::Text* b) {
+ [&](const Text* a, const Text* b) {
if (wall_pos == Wall::Position::PosX ||
wall_pos == Wall::Position::NegX)
return a->text3d->pos.z > b->text3d->pos.z;
@@ -110,7 +110,7 @@ Text::extract_Text(const PtrList& texts, Wall::Role role, double line_threshold)
for (size_t i = 0; i < line.items.size(); ++i) {
if (i) result += ' ';
- const Wall::Text* wt = line.items[i];
+ const Text* wt = line.items[i];
if (wt->text3d->type == ObjectType::Text)
result += wt->text3d->text;
}
@@ -151,7 +151,7 @@ Text::show_TokenInfo(PtrList& texts, const std::string& token_text)
Vec3 pos = tf.position; pos.y = y;
Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.003f, pos, ConfigColor::white);
t->yaw = tf.yaw;
- texts.push_back(cpputils::make_unique<Wall::Text>(Wall::Text{
+ texts.push_back(cpputils::make_unique<Text>(Text{
tf.wall_position, Wall::Role::Info, std::move(t) }));
y -= ConfigWall::text_line_spacing;
}