#include #include #include #include "core/game.hpp" #include "entities/solid_box.hpp" #include "entities/world.hpp" #include "game_state.hpp" #include "rendering/gl_renderer.hpp" #include "systems/aim.hpp" #include "systems/carry_lex.hpp" #include "systems/celebration_system.hpp" #include "systems/cpp_highlight_system.hpp" #include "systems/game_mod_abi.h" #include "systems/input_system.hpp" #include "systems/physics_system.hpp" #include "systems/pick_system.hpp" #include "systems/source_tokens.hpp" #include "text/token.hpp" #include "utils/config.hpp" #include "utils/frame_profiler.hpp" #include "wall/label.hpp" #include "wall/wall.hpp" namespace Nimbin { GameState::GameState(size_t starting_level) { _ctx.player = cpputils::make_unique(); _ctx.world = cpputils::make_unique (); _ctx.camera = cpputils::make_unique(); _ctx.light = cpputils::make_unique (); _input_sys = cpputils::make_unique (); _freelook_sys = cpputils::make_unique (); _compiler_sys = cpputils::make_unique (); _program_sys = cpputils::make_unique (); _dyn_loader_sys = cpputils::make_unique (); _physics_sys = cpputils::make_unique (); _celebration_sys = cpputils::make_unique (); _projectiles_sys = cpputils::make_unique (); _particle_sys = cpputils::make_unique (); _word_anim_sys = cpputils::make_unique (); _highlight_sys = cpputils::make_unique(); _wall_labels = Wall::Label::generate_All(World::get_BoxScale()); _wall_editor = Text::Handler(Wall::Role::Editor, World::get_BoxScale(), Text::LayoutMode::Editing); _wall_editor.set_ShowNumbers(true, true); _text_walls = { &_wall_editor }; _wall_program = Text::Handler (Wall::Role::Program, World::get_BoxScale(), Text::LayoutMode::Display); _wall_program.set_Lexing (false); _wall_program.set_ShowNumbers (false, false); _wall_program.set_DefaultStyle (Text::Token::depth_wall, 0.003f, static_cast(ProgramSystem::line_height), Config::Wall::free_space_dist); _wall_program.set_AppendBaseline (static_cast(ProgramSystem::start_y)); _wall_program.set_FaceMode (Text::FaceMode::Pitch); _wall_compiler = Text::Handler (Wall::Role::Compiler, World::get_BoxScale(), Text::LayoutMode::Display); _wall_compiler.set_Lexing (false); _wall_compiler.set_ShowNumbers (false, false); _wall_compiler.set_DefaultStyle (Text::Token::depth_compiler, 0.003f, static_cast(CompilerSystem::line_height), Config::Wall::free_space_dist); _wall_compiler.set_AppendBaseline(static_cast(CompilerSystem::start_y)); _wall_compiler.set_AppendColor (Config::Color::white); _wall_compiler.set_FaceMode (Text::FaceMode::Pitch); _wall_info = Text::Handler (Wall::Role::Info, World::get_BoxScale(), Text::LayoutMode::Display); _wall_info.set_Lexing (false); _wall_info.set_ShowNumbers (false, false); _wall_info.set_DefaultStyle (Text::Token::depth_wall, 0.003f, static_cast(Config::Wall::text_line_spacing), Config::Wall::free_space_dist); _wall_info.set_AppendBaseline(World::get_FloorY() + 1.0); _wall_info.set_AppendColor (Config::Color::white); _wall_info.set_FaceMode (Text::FaceMode::Pitch); _text_walls = { &_wall_editor, &_wall_program, &_wall_compiler, &_wall_info }; _ctx.editor_wall = &_wall_editor; _ctx.info_wall = &_wall_info; _pult_sys = cpputils::make_unique (); _pult_sys->init(_ctx.scenery, _ctx.buttons, World::get_BoxScale()); if (starting_level >= Level::get_Count()) starting_level = 0; _level_director = cpputils::make_unique(Level::get_Data(starting_level)); _level_director->enter_Step(_ctx); _word_ai_sys = cpputils::make_unique(); _word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep()); _hint_sys = cpputils::make_unique(); if (!_level_director->is_LevelComplete()) _hint_sys->set_Hint(_level_director->get_CurrentHint()); if (_level_director->get_Runner().is_DynamicLib()) sandbox_LoadFileToWall(); } void GameState::debug_SpawnWords([[maybe_unused]] size_t n) { DynArray tokens = Text::Token::get_TestArrayAllKeywords(); static constexpr float spacing = 3.0f; static constexpr int per_row = 8; static constexpr float row_gap = 3.0f; const double hover_y = World::get_FloorY() + 1.5; for (size_t i = 0; i < tokens.size(); i++) { Vec3 pos { -static_cast(per_row) * spacing * 0.5f + static_cast(i % per_row) * spacing, hover_y, static_cast(i / per_row) * row_gap }; Text::TokenPtr obj = Text::make_Token(tokens[i], Text::Token::depth_floor, 0.003f, pos); obj->body.hover_height = hover_y; _ctx.words.push_back(std::move(obj)); } } void GameState::carriedline_Update() { if (_carried.empty()) return; const FreelookState& fs = _freelook_sys->state; if (fs.active) return; const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, Config::Carry::dist); const Vec3 right = _ctx.camera->right; const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); DynArray slots = _carried.get_SlotPosition(anchor, right, Config::Carry::gap); DynArray nodes = _carried.reverse_nodes(); const bool easing = fs.lerping && !_freelook_return_from.empty(); double s = 1.0; if (easing) { double t = fs.lerp_t; t = (t < 0.0) ? 0.0 : (t > 1.0 ? 1.0 : t); s = t * t * (3.0 - 2.0 * t); } for (size_t i = 0; i < nodes.size() && i < slots.size(); i++) { if (!nodes[i]->word) continue; Vec3 target = slots[i]; if (easing && i < _freelook_return_from.size()) { const Vec3& from = _freelook_return_from[i]; nodes[i]->word->pos = { from.x + (target.x - from.x) * s, from.y + (target.y - from.y) * s, from.z + (target.z - from.z) * s }; } else { nodes[i]->word->pos = target; } nodes[i]->word->yaw = static_cast(glyph_yaw); } } void GameState::pult_RunAction(PultAction act) { _level_director->record_Button(act); switch (act) { 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; } } void GameState::on_Click() { if (_focus_active) { const float ws = World::get_BoxScale(); const Wall::Transform transform = Wall::get_Transform(_focus_role, ws); const Vec3 normal = Wall::inward_Normal(transform.wall_position); const Vec3& cam_pos = _ctx.camera->pos; const Vec3& cam_fwd = _ctx.camera->fwd; const double denom = cam_fwd.x*normal.x + cam_fwd.y*normal.y + cam_fwd.z*normal.z; if (denom < -1e-6) { const double t = ((transform.position.x-cam_pos.x)*normal.x + (transform.position.y-cam_pos.y)*normal.y + (transform.position.z-cam_pos.z)*normal.z) / denom; if (t > 0.0) { // crosshair points on wall const Vec3 impact { cam_pos.x + cam_fwd.x*t, cam_pos.y + cam_fwd.y*t, cam_pos.z + cam_fwd.z*t }; if (_focus_role == Wall::Role::Editor) { const Text::Handler::CursorHit hit = _wall_editor.resolve_Impact(impact); wallconnect_BeginEditor(); _wall_editor.place_CursorInsert(hit); } else if (_focus_role == Wall::Role::Program) { wallconnect_BeginProgram(); } } } return; } if (_wall_editor.edit_Active()) { const Wall::Transform wall_transform = Wall::get_Transform(Wall::Role::Editor, World::get_BoxScale()); const double wall_tans_x = wall_transform.position.x; const Vec3& cam_pos = _ctx.camera->pos; const Vec3& cam_fwd = _ctx.camera->fwd; if (SDL_fabs(cam_fwd.x) > 1e-6) { const double t = (wall_tans_x - cam_pos.x) / cam_fwd.x; if (t > 0.0) { Vec3 impact { wall_tans_x, cam_pos.y + cam_fwd.y * t, cam_pos.z + cam_fwd.z * t }; if (_wall_editor.cursor_AtImpact(impact)) return; } } if (Text::Token::Data* tk = PickSystem::pick_WallToken(*_ctx.camera, _wall_editor)) _wall_editor.cursor_ToToken(tk); return; } PickResult result = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, _text_walls); if (!result.hit()) return; if (result.kind == PickKind::Scenery && result.scenery_obj) { if (std::optional act = _pult_sys->try_Activate(result.scenery_obj, _ctx.buttons); act) pult_RunAction(*act); } } bool GameState::on_Pick() { PickResult result = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, _text_walls); if (!result.hit()) return false; if (result.kind == PickKind::WallWord && result.wall_handler && result.wall_token) { Text::TokenPtr picked = result.wall_handler->detach(result.wall_token); if (!picked) return false; _level_director->record_PickedUp(picked->text); picked->set_Look (Config::Color::keep, Text::Token::depth_floor); Wall::show_TokenInfo (_wall_info, picked->text); carry_BeginAnim (std::move(picked), true); return true; } if (result.kind == PickKind::Word) { Text::TokenPtr picked = std::move(_ctx.words[result.word_index]); _ctx.words.erase(_ctx.words.begin() + static_cast(result.word_index)); if (!picked) return false; _level_director->record_PickedUp(picked->text); picked->set_Look (Config::Color::keep, Text::Token::depth_floor); Wall::show_TokenInfo(_wall_info, picked->text); carry_BeginAnim (std::move(picked), false); return true; } return false; } void GameState::on_DropLast() { if (_carried.empty()) return; Text::TokenPtr dropped = _carried.pop_back(); if (!dropped) return; CarryLex::revert_ToSingle(dropped.get()); dropped->body.mode = PhysicsMode::Dynamic; dropped->body.vel = { _ctx.camera->fwd.x * 1.5, 0.8, _ctx.camera->fwd.z * 1.5 }; WordAnim_Drop drop; drop.word = std::move(dropped); _anims_drop.push_back(std::move(drop)); if (!_carried.empty()) { Wall::show_TokenInfo(_wall_info, _carried.last->word->text); } else _level_director->show_Details(_ctx); _level_director->record_Dropped(); } bool GameState::on_ShootOne() { if (_carried.empty()) return false; Text::TokenPtr word = _carried.pop_back(); if (!word) return false; CarryLex::revert_ToSingle(word.get()); _freelook_sys->state.active = false; _freelook_sys->state.lerping = false; DynArray single; single.push_back(std::move(word)); Aim::Result aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery, _text_walls, World::get_BoxScale()); _projectiles_sys->launch(_projectiles, std::move(single), _ctx.camera->fwd, aim); if (!_carried.empty() && _carried.last && _carried.last->word) { Wall::show_TokenInfo(_wall_info, _carried.last->word->text); } else _level_director->show_Details(_ctx); _level_director->record_Shot(); return true; } void GameState::on_ShootAll() { if (_carried.empty()) return; _freelook_sys->state.active = false; _freelook_sys->state.lerping = false; DynArray all_words; for (CarriedWordNode* node : _carried.nodes()) if (node->word) { CarryLex::revert_ToSingle (node->word.get()); all_words.push_back(std::move(node->word)); } _carried = CarriedLine(); Aim::Result aim; if (all_words.empty()) return; aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery, _text_walls, World::get_BoxScale()); _projectiles_sys->launch(_projectiles, std::move(all_words), _ctx.camera->fwd, aim); _level_director->show_Details(_ctx); _level_director->record_Shot (); } std::string GameState::get_EditorSource() const { std::string src; for (const std::string& l : _wall_editor.as_Lines()) { src += l; src += '\n'; } return src; } void GameState::on_Compile() { std::string src = get_EditorSource(); if ( !_level_director->is_LevelComplete() && _level_director->get_Runner().is_DynamicLib()) { const std::string path = sandbox_GetSourcePath(); std::string stripped; for (char c : src) if (!std::isspace((unsigned char)c)) { stripped = src; break; } if (!path.empty() && !stripped.empty()) SourceTokens::write_File(path, src); _dyn_loader_sys->build(_wall_compiler, src, true); } else { _compiler_sys->compile(_wall_compiler, src); } } void GameState::on_Run() { _program_sys->run(_wall_program); } void GameState::on_DebugPrint() { std::print("TEXT::: {0}\n", get_EditorSource()); } void GameState::on_FocusToggle() { _focus_on = !_focus_on; } void GameState::on_Jump() { if (_focus_active) { _ctx.player->pos = _ctx.camera->pos; _ctx.player->prev_pos = _ctx.camera->pos; _ctx.player->vel = Vec3{0, 0, 0}; _ctx.player->on_ground = false; return; } if (_ctx.player->on_ground) { _ctx.player->vel.y = Config::Physics::player_jump_velocity; _ctx.player->on_ground = false; } } void GameState::on_FreelookStart() { _freelook_sys->begin(); } void GameState::on_FreelookEnd() { if (!_freelook_sys->state.active) return; _freelook_return_from.clear(); for (CarriedWordNode* node : _carried.reverse_nodes()) if (node->word) _freelook_return_from.push_back(node->word->pos); _freelook_sys->end(); } void GameState::on_NumberAdjust(int delta) { if (_carried.empty() || !_carried.last || !_carried.last->word) return; Text::Token::Data* word = _carried.last->word.get(); const std::string& text = word->text; if (text.empty()) return; size_t start = (text[0] == '-') ? 1 : 0; if (start == text.size()) return; for (size_t i = start; i < text.size(); i++) if (!std::isdigit(static_cast(text[i]))) return; long val = std::strtol(text.c_str(), nullptr, 10); val += delta; word->text = std::to_string(val); word->rebuild_Mesh(); } void GameState::on_TypingStart(FrameInput& input) { input.typing_mode = true; input.typing_buffer.clear(); _typing_words.clear(); _typing_last .clear(); } void GameState::on_TypingCommit(FrameInput& input) { input.typing_mode = false; input.typing_buffer.clear(); _typing_words.clear(); _typing_last .clear(); } void GameState::on_TypingCancel(FrameInput& input) { typing_ClearPreview(); input.typing_mode = false; input.typing_buffer.clear(); _typing_last.clear(); } void GameState::projectile_LandOnProgramInput(ProjectileResult& result) { std::string line; for (const Text::TokenPtr& word : result.words) { if (!word || word->type != ObjectType::Text) continue; if (!line.empty()) line += ' '; line += word->text; } if (!line.empty()) _program_sys->feed_Input(line); _program_sys->echo_Words(_wall_program, result.words); } void GameState::projectile_LandOnFloor(ProjectileResult& result) { for (Text::TokenPtr& word : result.words) { if (!word || word->type != ObjectType::Text) continue; word->set_Look(Config::Color::keep, Text::Token::depth_floor); word->body.mode = PhysicsMode::Dynamic; word->body.vel = result.vel; _ctx.words.push_back(std::move(word)); } } void GameState::projectile_LandOnWall(ProjectileResult& result) { DynArray edit_block, prog_block, comp_block, info_block; for (Text::TokenPtr& word : result.words) { if (!word) continue; word->body.mode = PhysicsMode::Static; word->body.vel = Vec3{0, 0, 0}; Wall::Drop drop = Wall::attach_ToNearestWall(std::move(word)); if (!drop.token) continue; switch (drop.role) { case Wall::Role::Editor: edit_block.push_back(std::move(drop.token)); break; case Wall::Role::Program: prog_block.push_back(std::move(drop.token)); break; case Wall::Role::Compiler: comp_block.push_back(std::move(drop.token)); break; case Wall::Role::Info: info_block.push_back(std::move(drop.token)); break; } } if (!edit_block.empty()) { Vec3 impact{0, 0, 0}; for (const Text::TokenPtr& t : edit_block) { impact.x += t->pos.x; impact.y += t->pos.y; impact.z += t->pos.z; } const double inv = 1.0 / static_cast(edit_block.size()); impact.x *= inv; impact.y *= inv; impact.z *= inv; _wall_editor.attach(std::move(edit_block), impact); } { FP_ZONE("flush.append"); if (!prog_block.empty()) _wall_program .append_Tokens(std::move(prog_block)); if (!comp_block.empty()) _wall_compiler.append_Tokens(std::move(comp_block)); if (!info_block.empty()) _wall_info .append_Tokens(std::move(info_block)); } } void GameState::projectile_LandOnWallText(ProjectileResult& result) { const size_t idx = result.wall_text_index; if (idx >= LABEL_HIT_BASE) { for (Text::TokenPtr& word : result.words) if (word) _ctx.drop_WordToFloor(std::move(word), result.vel); return; } if (idx >= WALL_TOK_BASE) { const size_t rel = idx - WALL_TOK_BASE; const size_t handler_id = rel >> 28; const size_t line_idx = (rel >> 14) & 0x3FFF; const size_t token_idx = rel & 0x3FFF; Text::Handler* handler = (handler_id < _text_walls.size()) ? _text_walls[handler_id] : nullptr; if (handler) { const auto& lines = handler->get_Lines(); if ( line_idx < lines.size() && token_idx < lines[line_idx].tokens.size() && lines[line_idx].tokens[token_idx]) { if (handler->is_Editing()) { Vec3 impact = lines[line_idx].tokens[token_idx]->pos; for (const Text::TokenPtr& word : result.words) if (word) { impact = word->pos; break; } if (result.use_aim) impact = result.aim_point; const Text::Handler::EditorHit hit = handler->classify_Impact(impact); if (hit.kind == Text::Handler::HitKind::DropToken) { const auto& ls = handler->get_Lines(); if ( hit.line_idx < ls.size() && hit.token_idx < ls[hit.line_idx].tokens.size() && ls[hit.line_idx].tokens[hit.token_idx]) { if (Text::TokenPtr orphan = handler->detach(ls[hit.line_idx].tokens[hit.token_idx].get())) { orphan->body.hover_height = World::get_FloorY() + 1.5; orphan->yaw = 0.0f; _ctx.drop_WordToFloor(std::move(orphan), Vec3{0.0, 1.5, 0.0}); } } for (Text::TokenPtr& word : result.words) if (word) _ctx.drop_WordToFloor(std::move(word), result.vel); } else { DynArray block; for (Text::TokenPtr& word : result.words) if (word) block.push_back(std::move(word)); handler->attach(std::move(block), impact); } return; } if (Text::TokenPtr orphan = handler->detach(lines[line_idx].tokens[token_idx].get())) { orphan->body.hover_height = World::get_FloorY() + 1.5; orphan->yaw = 0.0f; _ctx.drop_WordToFloor(std::move(orphan), Vec3{0.0, 1.5, 0.0}); } } } for (Text::TokenPtr& word : result.words) if (word) _ctx.drop_WordToFloor(std::move(word), result.vel); return; } if (idx >= Text::Handler::HIT_BASE) { const size_t lid = Text::Handler::line_Of(idx); const double delta = _wall_editor.scroll_DeltaToEye(lid, _ctx.camera->pos.y); _wall_editor.scroll(delta); _wall_editor.bounce(lid); projectile_LandOnFloor(result); return; } for (Text::TokenPtr& word : result.words) if (word) _ctx.drop_WordToFloor(std::move(word), result.vel); } void GameState::handle_Event(Game& game, const SDL_Event& event) { _input_sys->handle_Event(game, event, _frame_input); } void GameState::update(double dt, const GlRenderer* gl) { if (gl) { _fov_y = gl->get_Fov_y(); _aspect = gl->get_Aspect(); } FrameInput& input = _frame_input; { FP_ZONE("update.input" ); update_Input (input, dt); } { FP_ZONE("update.world_sim" ); update_Physics (dt); } { FP_ZONE("update.present" ); update_Objects (dt, gl); } { FP_ZONE("update.subsystems" ); update_Compiler (); } { FP_ZONE("update.progression"); update_Progression (dt); } { FP_ZONE("update.highlight" ); update_Highlighting(); } input.look_x = 0; input.look_y = 0; input.scroll_x = 0; input.scroll_y = 0; input.commands.clear(); } void GameState::update_Input(FrameInput& input, double dt) { for (InputCommand cmd : input.commands) { switch (cmd) { case InputCommand::Jump: on_Jump(); break; case InputCommand::Click: on_Click(); break; case InputCommand::Drop: on_DropLast(); break; case InputCommand::ShootAll: on_ShootAll(); break; case InputCommand::Compile: on_Compile(); break; case InputCommand::Run: on_Run(); break; case InputCommand::DebugPrint: on_DebugPrint(); break; case InputCommand::FocusToggle: on_FocusToggle(); break; case InputCommand::FreelookStart: on_FreelookStart(); break; case InputCommand::FreelookEnd: on_FreelookEnd(); break; case InputCommand::NumberInc: on_NumberAdjust(+1); break; case InputCommand::NumberDec: on_NumberAdjust(-1); break; case InputCommand::TypingStart: on_TypingStart (input); break; case InputCommand::TypingCommit: on_TypingCommit(input); break; case InputCommand::TypingCancel: on_TypingCancel(input); break; case InputCommand::SandboxRestore: on_SandboxRestore(); break; case InputCommand::ConnectPultSubmit: on_WallConnectSubmit(); break; case InputCommand::ConnectPultExit: on_WallConnectExit(); break; } } Wall::Connection::Target target = _wall_connection.get_Target(); if (target == Wall::Connection::Target::Editor) { { FP_ZONE("handle_input.Editor"); Text::Handler::EditReq req = _wall_editor.feed_Keys(input.vim_keys, _ctx.camera->pos.y); input.vim_keys.clear(); switch (req) { case Text::Handler::EditReq::Write: _wall_editor.mark_Saved(); break; case Text::Handler::EditReq::WriteQuit: case Text::Handler::EditReq::Exit: case Text::Handler::EditReq::Discard: _wall_editor.end_Edit(); _wall_connection.end(); _frame_input.capture_vim = false; _focus_locked = false; break; case Text::Handler::EditReq::None: break; } } } else if (target == Wall::Connection::Target::Program) { { FP_ZONE("handle_input.Program"); _wall_connection.set_ProgramBuffer(input.typing_buffer); } } else if (input.typing_mode) { { FP_ZONE("handle_input.typing_mode"); input.forward = input.backward = input.left = input.right = false; if (input.typing_buffer != _typing_last) { typing_RebuildPreview(input.typing_buffer); _typing_last = input.typing_buffer; } } } if (_pick_cooldown > 0.0) _pick_cooldown -= dt; if (input.pick && _pick_cooldown <= 0.0) if (on_Pick()) _pick_cooldown = 0.15; if (_shoot_cooldown > 0.0) _shoot_cooldown -= dt; if (input.shoot && _shoot_cooldown <= 0.0) { FP_ZONE("handle_input.onShootO"); if (on_ShootOne()) _shoot_cooldown = 0.15; } const bool want_focus = _focus_on || _focus_locked; if (!_freelook_sys->state.active && !want_focus) _ctx.player->add_Look(input.look_x, input.look_y); _freelook_sys->update(dt); if (want_focus) { const bool f = input.forward, b = input.backward, l = input.left, r = input.right; input.forward = input.backward = input.left = input.right = false; _ctx.player->compute_Movement(input); input.forward = f; input.backward = b; input.left = l; input.right = r; } else { _ctx.player->compute_Movement(input); } _ctx.camera->sync_FromPlayer(*_ctx.player); focus_UpdateWall(input, dt); } void GameState::update_Objects(double dt, const GlRenderer* gl) { for (Wall::Label& label : _wall_labels) label.update(dt, *_ctx.camera); carriedline_Update(); _word_anim_sys->update(_anims_add, _anims_drop, _carried, _ctx.words, dt); if (_carried.size != _carried_last_size) { if (_carried.size > 0) CarryLex::lex_CarriedLine(_carried); _carried_last_size = _carried.size; } _pult_sys ->update(dt, *_ctx.camera); _plate_sys->update(dt, *_ctx.player, _level_director->get_StepPlates()); for (Text::Handler* handler : _text_walls) if (handler) handler->update(dt, *_ctx.camera); _wall_connection.update(dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y()); focus_UpdateLabel (dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y()); DynArray particle_colliders; particle_colliders.reserve(_ctx.scenery.size()); for (const GameObjectPtr& o : _ctx.scenery) if (o) particle_colliders.push_back(o.get()); _particle_sys->update(dt, particle_colliders, World::get_BoxScale()); } void GameState::update_Compiler() { _compiler_sys->update(_wall_compiler); if (_compiler_sys->last_compile_ok) { _compiler_sys->last_compile_ok = false; _level_director->record_CompileOk(); } // TODO: the dyn path may still blocks the frame in dynlib_Apply _dyn_loader_sys->update(_wall_compiler); if (_dyn_loader_sys->last_build_ok) { _dyn_loader_sys->last_build_ok = false; dynlib_Apply(_dyn_loader_sys->load_Objects()); SourceTokens::copy_File(sandbox_GetSourcePath(), sandbox_BackupPath()); } _program_sys->update(_wall_program); } void GameState::update_Highlighting() { if (_wall_editor.is_Dirty()) _highlight_sys->request(_wall_editor.render_Source(), _wall_editor.generation()); DynArray spans; uint64_t gen; if (_highlight_sys->poll(spans, gen)) _wall_editor.apply_Highlight(spans, gen); } void GameState::update_Progression(double dt) { _celebration_sys->tick(dt, *_particle_sys); level_TryAdvance(dt); } void GameState::render(SDL_Window& window, GlRenderer& renderer) { int w, h; SDL_GetWindowSizeInPixels(&window, &w, &h); { FP_ZONE("render.light_setup"); const double yaw_r = Config::Physics::binrad * static_cast(_ctx.camera->yaw); const double pitch_r = Config::Physics::binrad * static_cast(_ctx.camera->pitch); const double cy = SDL_cos(yaw_r), sy = SDL_sin(yaw_r); const double cp = SDL_cos(pitch_r), sp = SDL_sin(pitch_r); _ctx.light->flashlight_pos = _ctx.camera->pos; _ctx.light->flashlight_dir = Vec3{ -sy*cp, sp, -cy*cp }; } { FP_ZONE("render.begin_Frame"); renderer.begin_Frame(w, h); } { FP_ZONE("render.set_Light" ); renderer.set_Light(*_ctx.light); } { FP_ZONE("render.world" ); _ctx.world ->draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.text_walls" ); for (Text::Handler* handler : _text_walls) if (handler) handler->render(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.scenery" ); for (const GameObjectPtr& scenery : _ctx.scenery) if (scenery) scenery->draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.words" ); for (const Text::TokenPtr& word : _ctx.words) if (word) word->draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.wall_labels"); for (const Wall::Label& label : _wall_labels) label.draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render._carried" ); DynArray nodes = _carried.nodes(); for (const CarriedWordNode* node : nodes) if (node && node->word) node->word->draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.anims" ); for (const WordAnim_Add& anim : _anims_add) if (anim.word) anim.word->draw(renderer, *_ctx.camera, *_ctx.light); for (const WordAnim_Drop& drop : _anims_drop) if (drop.word) drop.word->draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.ai.stolen" ); for (const Text::TokenPtr& word : _ctx.words) { if (!word) continue; for (const Text::TokenPtr& s : word->ai.stolen) if (s) s->draw(renderer, *_ctx.camera, *_ctx.light); } } { FP_ZONE("render._projectiles"); for (const Projectile& proj : _projectiles) for (const Text::TokenPtr& word : proj.words) if (word) word->draw(renderer, *_ctx.camera, *_ctx.light); } { 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.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.focus_label"); if (_focus_active && _focus_label) { const double hw = static_cast(_focus_label->half_extent); _focus_label->pos = Vec3{ _focus_label_pos.x + _ctx.camera->right.x * hw, _focus_label_pos.y + _ctx.camera->right.y * hw, _focus_label_pos.z + _ctx.camera->right.z * hw, }; _focus_label->yaw = _focus_label_yaw; _focus_label->pitch = 0.0f; renderer.clear_Depth(); _focus_label->draw(renderer, *_ctx.camera, *_ctx.light); } } { FP_ZONE("render.end_Frame" ); renderer.end_Frame(&window); } } void GameState::update_Physics(double dt) { _ctx.player->prev_pos = _ctx.player->pos; { FP_ZONE("updateworldsim.phys_player" ); _physics_sys->update_Player(*_ctx.player, _ctx.player->get_AccX(), _ctx.player->get_AccZ(), dt); } DynArray colliders; colliders.reserve(_ctx.scenery.size() + _ctx.words.size()); for (const GameObjectPtr& o : _ctx.scenery) if (o) colliders.push_back(o.get()); for (const Text::TokenPtr& w : _ctx.words) if (w) colliders.push_back(w.get()); { FP_ZONE("updateworldsim.phys_objects"); _physics_sys->update_Objects(colliders, dt, 25.0); } { FP_ZONE("updateworldsim.word_ai" ); DynArray ai_dropped; _word_ai_sys->update(_ctx.words, *_ctx.player, _carried, ai_dropped, dt); for (Text::TokenPtr& drop : ai_dropped) if (drop) _ctx.words.push_back(std::move(drop)); } { FP_ZONE("updateworldsim.phys_projectiles"); _physics_sys->update_Projectiles(_projectiles, dt, _ctx.words); } DynArray all_labels = _pult_sys->get_Labels(); { FP_ZONE("updateworldsim.hint_and_labels" ); _hint_sys->update(dt, *_ctx.camera); for (Text::Handler* label : all_labels) label->update(dt, *_ctx.camera); all_labels.push_back(&_hint_sys->get_Label()); for (Wall::Label& label : _wall_labels) all_labels.push_back(&label.label); } DynArray projectile_contacts; { FP_ZONE("updateworldsim.resolve_projectiles"); _physics_sys->resolve_Projectiles(_projectiles, &colliders, &all_labels, &projectile_contacts); } DynArray wall_text_hits; { FP_ZONE("updateworldsim.build_wall_hits" ); for (size_t i = 0; i < _text_walls.size(); ++i) { Text::Handler* handler = _text_walls[i]; if (!handler) continue; const auto& elines = handler->get_Lines(); for (size_t line_idx = 0; line_idx < elines.size(); ++line_idx) { const DynArray& toks = elines[line_idx].tokens; for (size_t token_idx = 0; token_idx < toks.size(); ++token_idx) { if (!toks[token_idx]) continue; wall_text_hits.push_back({ toks[token_idx]->aabb_Min(), toks[token_idx]->aabb_Max(), WALL_TOK_BASE | (i << 28) | (line_idx << 14) | token_idx, Wall::Position::NegX, }); } } } for (const Text::Handler::NumberHit& nh : _wall_editor.number_Hitboxes()) wall_text_hits.push_back({ nh.aabb_Min, nh.aabb_Max, nh.index, Wall::Position::NegX, }); for (size_t line_idx = 0; line_idx < _wall_labels.size(); ++line_idx) { const DynArray& words = _wall_labels[line_idx].label.get_Words(); for (size_t wi = 0; wi < words.size(); ++wi) { if (!words[wi]) continue; wall_text_hits.push_back({ words[wi]->aabb_Min(), words[wi]->aabb_Max(), LABEL_HIT_BASE | (line_idx << 16) | wi, Wall::position_Of(_wall_labels[line_idx].role), }); } } } DynArray proj_results; { FP_ZONE("updateworldsim.projectile_sys" ); _projectiles_sys->update(_projectiles, proj_results, wall_text_hits, dt, _program_sys->is_Running()); } { FP_ZONE("updateworldsim.projectile_land"); for (ProjectileResult& result : proj_results) { switch (result.landing) { case ProjectileLanding::Floor: projectile_LandOnFloor (result); break; case ProjectileLanding::ProgramInput: projectile_LandOnProgramInput(result); break; case ProjectileLanding::Wall: projectile_LandOnWall (result); break; case ProjectileLanding::WallText: projectile_LandOnWallText (result); break; case ProjectileLanding::None: break; } } for (const ProjectileContactEvent& contact : projectile_contacts) { if (std::optional action = _pult_sys->handle_ProjectileContact(contact, _ctx.buttons); action) { pult_RunAction(*action); continue; } if (contact.other && contact.other->type == ObjectType::Text) { Text::Token::Data* word = static_cast(contact.other); _level_director->record_AiDefeated(word->text); word->ai.state = Text::Token::Ai::State::Stunned; word->ai.state_timer = 0.0; word->body.vel = Vec3{0, 0, 0}; word->set_Color (Config::Color::stunned); word->set_ColorSource(Text::ColorSource::Static); } } } { FP_ZONE("updateworldsim.player_collision"); PlayerCollision col = _ctx.world->resolve_Player(*_ctx.player); _ctx.player->apply_Collision(col); _physics_sys->resolve_PlayerVsObjects(*_ctx.player, _carried, colliders); if (std::optional act = _pult_sys->handle_PlayerContact(*_ctx.player, _ctx.buttons); act) pult_RunAction(*act); } } void GameState::level_TryAdvance(double dt) { Level::Director::AdvanceResult res = _level_director->try_Advance(_ctx, dt, *_celebration_sys, *_particle_sys); if (!res.stepped) return; dynlib_Clear(); if (res.reset_carry) { _ctx.words .clear(); _projectiles.clear(); _anims_add .clear(); _anims_drop .clear(); _carried = CarriedLine(); } if (res.advanced) { _level_director->enter_Step(_ctx); _word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep()); _hint_sys ->set_Hint (_level_director->get_CurrentHint()); } else { _hint_sys ->clear(); _level_director->show_Details(_ctx); } } void GameState::dynlib_Clear() { if (_dyn_scenery.empty()) { return; } std::unordered_set set(_dyn_scenery.begin(), _dyn_scenery.end()); _ctx.scenery.erase( std::remove_if(_ctx.scenery.begin(), _ctx.scenery.end(), [&set](const GameObjectPtr& sp) { return set.count(sp.get()) > 0; }), _ctx.scenery.end() ); _ctx.words .erase( std::remove_if(_ctx.words.begin(), _ctx.words.end(), [&set](const Text::TokenPtr& wp) { return set.count(wp.get()) > 0; }), _ctx.words.end() ); _dyn_scenery.clear(); } void GameState::dynlib_Apply(const DynArray& objs) { dynlib_Clear(); for (const DynObject& o : objs) { if (o.kind == DYN_BOX) { auto box = cpputils::make_unique( Vec3{ o.x , o.y , o.z }, Vec3{ o.hx, o.hy, o.hz }, SDL_FColor{ o.r, o.g, o.b, 1.0f }); box->body.mode = o.dynamic ? PhysicsMode::Dynamic : PhysicsMode::Static; _dyn_scenery.push_back(box.get()); _ctx.scenery.push_back(std::move(box)); } else { Text::TokenPtr t = Text::make_Token(o.text, Text::Token::depth_floor, 0.003f, Vec3{ o.x, o.y, o.z }, SDL_FColor{ o.r, o.g, o.b, 1.0f }); t->body.mode = PhysicsMode::Static; _dyn_scenery.push_back(t.get()); _ctx.words .push_back(std::move(t)); } } } std::string GameState::sandbox_GetSourcePath() const { return _level_director ? _level_director->get_Runner().get_SourceFile() : std::string{}; } std::string GameState::sandbox_BackupPath() const { std::string c = sandbox_GetSourcePath(); return c.empty() ? c : (c + ".last_ok"); } void GameState::sandbox_LoadFileToWall() { const std::string path = sandbox_GetSourcePath(); if (path.empty()) return; std::string src; if (!SourceTokens::read_File(path, src)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Sandbox: cannot read %s", path.c_str()); return; } DynArray lines; std::string cur; for (char c : src) { if (c == '\n') { lines.push_back(cur); cur.clear(); } else if (c != '\r') { cur += c; } } if (!cur .empty()) lines.push_back(cur); if (lines.empty()) lines.push_back(""); _wall_editor.set_Lines(lines); } void GameState::on_SandboxRestore() { if (!_level_director || !_level_director->get_Runner().is_DynamicLib()) return; if (SourceTokens::copy_File(sandbox_BackupPath(), sandbox_GetSourcePath())) sandbox_LoadFileToWall(); } void GameState::typing_RebuildPreview(const std::string& typed) { typing_ClearPreview(); const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, Config::Carry::dist); const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); std::istringstream in(typed); std::string tok_str; while (in >> tok_str) { Text::TokenPtr token = Text::make_Token(tok_str, Text::Token::depth_floor, 0.003f, anchor, Config::Color::floor); token->yaw = static_cast(glyph_yaw); token->body.mode = PhysicsMode::Carried; _typing_words.push_back(token.get()); _carried.push(std::move(token)); } CarryLex::lex_CarriedLine(_carried); } void GameState::typing_ClearPreview() { for (size_t i = 0; i < _typing_words.size(); i++) { if (_carried.empty()) break; _carried.pop_back(); } _typing_words.clear(); CarryLex::lex_CarriedLine(_carried); } void GameState::carry_BeginAnim(Text::TokenPtr word, bool with_pitch) { WordAnim_Add anim; anim.start_pos = word->pos; anim.yaw_start = word->yaw; if (with_pitch) { anim.pitch_start = word->pitch; anim.pitch_target = 0.0; } anim.word = std::move(word); anim.t = 0.0; anim.complete = false; const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, Config::Carry::dist); const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); anim.target_pos = anchor; anim.yaw_target = static_cast(glyph_yaw); _anims_add.push_back(std::move(anim)); } void GameState::wallconnect_BeginProgram() { _frame_input.capture_typing = true; _frame_input.typing_buffer.clear(); _wall_connection.begin_Program(_pult_sys->get_ConnectProgramPos()); } void GameState::wallconnect_BeginEditor() { _wall_editor.begin_Edit(_ctx.camera->pos.y); _frame_input.capture_vim = true; _frame_input.vim_keys.clear(); _wall_connection.begin_Editor(_pult_sys->get_ConnectEditorPos(), _wall_editor); } void GameState::on_WallConnectSubmit() { std::string line = _frame_input.typing_buffer; _frame_input.typing_buffer.clear(); _wall_connection.clear_ProgramPreview(); if (line == ":exit") { on_WallConnectExit(); return; } if (line == ":help") { for (const char* h : { ":exit leave the connection", "ctrl+WASD move while connected", " + Enter send a line to the program (stdin)" }) _program_sys->echo_Input(_wall_program, h); return; } _program_sys->feed_Input(line); _program_sys->echo_Input(_wall_program, line); } void GameState::on_WallConnectExit() { _wall_connection.end(); _frame_input.capture_typing = false; _frame_input.typing_buffer.clear(); _focus_locked = false; } void GameState::on_EditorClear() { _wall_editor.clear_ToFloor(_ctx.words); } void GameState::on_EditorScroll(int8_t dt) { if (dt > 0) _wall_editor.scroll( Config::Wall::scroll_step); else _wall_editor.scroll(-Config::Wall::scroll_step); } Text::Handler* GameState::get_Handler(Wall::Role role) { switch (role) { case Wall::Role::Editor: return &_wall_editor; case Wall::Role::Program: return &_wall_program; case Wall::Role::Compiler: return &_wall_compiler; case Wall::Role::Info: return &_wall_info; } return nullptr; } bool GameState::focus_ComputeTarget() { const Camera& cam = *_ctx.camera; const float ws = World::get_BoxScale(); const Wall::Role roles[4] = { Wall::Role::Editor, Wall::Role::Program, Wall::Role::Info, Wall::Role::Compiler }; Wall::Role best_role = Wall::Role::Editor; bool found = false; double best_t = 1e30; for (Wall::Role role : roles) { const Vec3 n = Wall::inward_Normal(Wall::position_Of(role)); const Vec3 p = Wall::get_Position(role, ws); const double denom = cam.fwd.x*n.x + cam.fwd.y*n.y + cam.fwd.z*n.z; if (denom < -1e-4) { const double t = ((p.x-cam.pos.x)*n.x + (p.y-cam.pos.y)*n.y + (p.z-cam.pos.z)*n.z) / denom; if (t > 0.1 && t < best_t) { best_t = t; best_role = role; found = true; } } } if (!found) return false; const Wall::Transform tf = Wall::get_Transform(best_role, ws); const Vec3 n = Wall::inward_Normal(tf.wall_position); const bool horiz_is_z = (tf.wall_position == Wall::Position::PosX || tf.wall_position == Wall::Position::NegX); double y_min=1e30, y_max=-1e30, h_min=1e30, h_max=-1e30; bool any=false; if (Text::Handler* hw = get_Handler(best_role)) for (const Text::Line& line : hw->get_Lines()) for (const Text::TokenPtr& tp : line.tokens) { if (!tp || tp->is_Whitespace()) continue; const double hc = horiz_is_z ? tp->pos.z : tp->pos.x; y_min = SDL_min(y_min, tp->pos.y - static_cast(tp->half_extent_y)); y_max = SDL_max(y_max, tp->pos.y + static_cast(tp->half_extent_y)); h_min = SDL_min(h_min, hc - static_cast(tp->half_extent)); h_max = SDL_max(h_max, hc + static_cast(tp->half_extent)); any = true; } double cy, ch, text_w, text_h; if (any) { cy=(y_min+y_max)*0.5; ch=(h_min+h_max)*0.5; text_w=h_max-h_min; text_h=y_max-y_min; } else { cy=tf.position.y; ch=0.0; text_w=2.0; text_h=2.0; } const double fov = (_fov_y > 0.01f) ? (double)_fov_y : (SDL_PI_D / 3.0); const double asp = (_aspect > 0.01f) ? (double)_aspect : 1.7; const double half_tan = SDL_tan(fov * 0.5); constexpr double FIT = 0.85; const double d_v = (text_h * 0.5) / (FIT * half_tan); const double d_h = (text_w * 0.5) / (FIT * half_tan * asp); Vec3 center = tf.position; center.y = cy; if (horiz_is_z) center.z = ch; else center.x = ch; _focus_center = center; _focus_normal = n; _focus_dist = SDL_max(SDL_max(d_v, d_h), 2.0); _focus_yaw_r = tf.yaw; _focus_role = best_role; return true; } void GameState::focus_UpdateWall(FrameInput& input, double dt) { const bool want = _focus_on || _focus_locked; constexpr double DUR = 0.25; if (want) { if (!_focus_active) { if (!focus_ComputeTarget()) return; _focus_active = true; } _focus_t = SDL_min(1.0, _focus_t + dt / DUR); const double ws = World::get_BoxScale(); // dolly (scroll wheel): wheel up = toward the wall, down = away. Clamped // inside the room. Scale by distance so each notch feels proportional. constexpr double DOLLY = 1.2, MIN_D = 0.6; const double MAX_D = 2.0 * ws - 0.6; _focus_dist -= input.scroll_y * DOLLY * (_focus_dist * 0.15 + 1.0); _focus_dist = SDL_clamp(_focus_dist, MIN_D, MAX_D); // pan via mouse motion — smooth, any direction (look is suppressed while // focused, so the mouse deltas are ours). Scale by distance so the pan // feels the same in screen space whether zoomed in or out. constexpr double PAN_SENS = 0.0016; const double k = PAN_SENS * _focus_dist; const double rx = SDL_cos(_focus_yaw_r); // focus camera right axis (y = 0) const double rz = -SDL_sin(_focus_yaw_r); const double pan_h = (double)input.look_x * k; const double pan_v = (double)input.look_y * k; const double lim = ws - 0.5; _focus_center.x = SDL_clamp(_focus_center.x + rx * pan_h, -lim, lim); _focus_center.z = SDL_clamp(_focus_center.z + rz * pan_h, -lim, lim); _focus_center.y = SDL_clamp(_focus_center.y - pan_v, -lim, lim); } else { if (!_focus_active) return; _focus_t -= dt / DUR; if (_focus_t <= 0.0) { _focus_t = 0.0; _focus_active = false; return; } } const Vec3 focus_pos = { _focus_center.x + _focus_normal.x * _focus_dist, _focus_center.y + _focus_normal.y * _focus_dist, _focus_center.z + _focus_normal.z * _focus_dist }; Camera& cam = *_ctx.camera; const double s = _focus_t * _focus_t * (3.0 - 2.0 * _focus_t); double dyaw = _focus_yaw_r - cam.yaw_r; while (dyaw > SDL_PI_D) dyaw -= 2.0 * SDL_PI_D; while (dyaw < -SDL_PI_D) dyaw += 2.0 * SDL_PI_D; const double yaw_r = cam.yaw_r + dyaw * s; const double pitch_r = cam.pitch_r + (0.0 - cam.pitch_r) * s; const Vec3 pos = { cam.pos.x + (focus_pos.x - cam.pos.x) * s, cam.pos.y + (focus_pos.y - cam.pos.y) * s, cam.pos.z + (focus_pos.z - cam.pos.z) * s }; const double binrad = Config::Physics::binrad; cam.pos = pos; cam.yaw_r = yaw_r; cam.pitch_r = pitch_r; cam.yaw = (uint32_t)SDL_lround(yaw_r / binrad); cam.pitch = (int32_t) SDL_lround(pitch_r / binrad); cam.fwd = { -SDL_sin(yaw_r)*SDL_cos(pitch_r), SDL_sin(pitch_r), -SDL_cos(yaw_r)*SDL_cos(pitch_r) }; cam.right = { SDL_cos(yaw_r), 0.0, -SDL_sin(yaw_r) }; } void GameState::focus_UpdateLabel(double dt, const Camera& cam, float aspect, float fov_y) { if (!_focus_active) { _focus_label_init = false; return; } if (!_focus_label) _focus_label = Text::make_Token("focus", Text::Token::depth_wall, 0.0016f, Vec3{0, 0, 0}, SDL_FColor{0.35f, 0.55f, 1.0f, 1.0f}); const Vec3 up = Math::cross(cam.right, cam.fwd); const double dist = 2.2; const double half_h = dist * static_cast(SDL_tanf(fov_y * 0.5f)); const double half_w = half_h * static_cast(aspect); const double fx = 0.82; const double fy = 0.80; const double left = half_w * fx; const double upamt= half_h * fy; const Vec3 target { cam.pos.x + cam.fwd.x*dist - cam.right.x*left + up.x*upamt, cam.pos.y + cam.fwd.y*dist - cam.right.y*left + up.y*upamt, cam.pos.z + cam.fwd.z*dist - cam.right.z*left + up.z*upamt, }; if (!_focus_label_init) { _focus_label_pos = target; _focus_label_init = true; } const double k = 1.0 - SDL_exp(-6.0 * dt); // exponential trail = "delay in movement" _focus_label_pos.x += (target.x - _focus_label_pos.x) * k; _focus_label_pos.y += (target.y - _focus_label_pos.y) * k; _focus_label_pos.z += (target.z - _focus_label_pos.z) * k; _focus_label_yaw = static_cast(SDL_atan2(-cam.fwd.x, -cam.fwd.z)); } } // namespace Nimbin