sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzCleanup game_state sort out level direction With Claude
src/states/game_context.cpp | 42 +++++ src/states/game_context.hpp | 14 +- src/states/game_state.cpp | 371 +++++++++-------------------------------- src/states/game_state.hpp | 49 +----- src/systems/level_director.cpp | 227 +++++++++++++++++++++++++ src/systems/level_director.hpp | 89 ++++++++++ 6 files changed, 451 insertions(+), 341 deletions(-) diff --git a/src/states/game_context.cpp b/src/states/game_context.cpp new file mode 100644 index 0000000..0d064ec --- /dev/null +++ b/src/states/game_context.cpp @@ -0,0 +1,42 @@ +#include "states/game_context.hpp" + +#include <algorithm> + +#include "text_3d/text_3d.hpp" + + +void +GameContext::drop_WordToFloor(Text3dPtr word, const Vec3& vel) +{ + if (!word) return; + word->set_Look(ConfigColor::keep, Text3d::depth_floor); + word->body.mode = PhysicsMode::Dynamic; + word->body.vel = vel; + words.push_back(std::move(word)); +} + + +void +GameContext::clear_WallRole(Wall::Role role) +{ + wall_texts.erase( + std::remove_if(wall_texts.begin(), wall_texts.end(), + [role](const TextWallPtr& wt){ return wt->role == role; }), + wall_texts.end()); +} + + +void +GameContext::add_WallText(Wall::Position pos, Wall::Role role, Text3dPtr t) +{ + wall_texts.push_back(cpputils::make_unique<TextWall>( + TextWall{ pos, role, std::move(t) })); +} + + +void +GameContext::remove_WallText(size_t index) +{ + if (index < wall_texts.size()) + wall_texts.erase(wall_texts.begin() + static_cast<std::ptrdiff_t>(index)); +} diff --git a/src/states/game_context.hpp b/src/states/game_context.hpp index c9402d3..d42b88b 100644 --- a/src/states/game_context.hpp +++ b/src/states/game_context.hpp @@ -1,11 +1,13 @@ #pragma once #include <memory> +#include <string> #include "entities/game_object.hpp" #include "text_3d/text_3d.hpp" #include "utils/globals.hpp" #include "wall/text_wall.hpp" +#include "wall/wall.hpp" class Player; class Camera; @@ -20,9 +22,13 @@ struct GameContext cpputils::unique_ptr<World> world; cpputils::unique_ptr<Light> light; - DynArray<Text3dPtr> words; - DynArray<GameObjectPtr> scenery; - DynArray<PultButton*> buttons; + DynArray<Text3dPtr> words; + DynArray<GameObjectPtr> scenery; + DynArray<PultButton*> buttons; + TextWallList wall_texts; - TextWallList wall_texts; + void drop_WordToFloor(Text3dPtr word, const Vec3& vel); + void clear_WallRole (Wall::Role role); + void add_WallText (Wall::Position pos, Wall::Role role, Text3dPtr t); + void remove_WallText (size_t index); }; diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 0a88673..936a145 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -43,30 +43,28 @@ GameState::GameState(size_t starting_level) _particle_sys = cpputils::make_unique<ParticleSystem> (); _dyn_loader_sys = cpputils::make_unique<DynLoaderSystem> (); _celebration_sys = cpputils::make_unique<CelebrationSystem>(); - _wall_labels = WallLabel::generate_All(World::get_BoxScale()); - _word_ai_sys = cpputils::make_unique<WordAiSystem>(); _floor_sys = cpputils::make_unique<FloorMandelbrot> (); _floor_sys->generate(World::get_BoxScale(), World::get_FloorY()); + _wall_labels = WallLabel::generate_All(World::get_BoxScale()); + + _word_ai_sys = cpputils::make_unique<WordAiSystem>(); + _pult_sys = cpputils::make_unique<PultSystem> (); _pult_sys->init(_ctx.scenery, _ctx.buttons, World::get_BoxScale()); if (starting_level >= Level::level_Count()) starting_level = 0; - _level_runner_sys = cpputils::make_unique<LevelRunner>(Level::get_Level(starting_level)); + _level_director = cpputils::make_unique<LevelDirector>(Level::get_Level(starting_level)); - DynArray<std::string> tokens = _level_runner_sys->grants_for_step(); - step_Spawn(tokens); + _level_director->enter_Step(_ctx); + _word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep()); - scene_Setup(_level_runner_sys->current().scene); - step_SettingsApply(); - infowall_StepdetailsShow(); + _hint_sys = cpputils::make_unique<HintSystem>(); + if (!_level_director->complete()) + _hint_sys->set_Hint(_level_director->current_hint()); - _hint_sys = cpputils::make_unique<HintSystem>(); - if (_level_runner_sys && !_level_runner_sys->complete()) - _hint_sys->set_Hint(_level_runner_sys->current().hint); - - if (_level_runner_sys && _level_runner_sys->dynamic_lib()) + if (_level_director->runner().dynamic_lib()) sandbox_LoadFileToWall(); /* @@ -87,12 +85,27 @@ GameState::GameState(size_t starting_level) void -GameState::debug_SpawnWords([[maybe_unused]]size_t n) +GameState::debug_SpawnWords([[maybe_unused]] size_t n) { DynArray<std::string> tokens = TokenDB::get_TestArrayAllKeywords(); - step_Spawn(tokens); -} + 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<double>() + 1.5; + + for (size_t i = 0; i < tokens.size(); ++i) { + Vec3 pos { + -static_cast<float>(per_row) * spacing * 0.5f + + static_cast<float>(i % per_row) * spacing, + hover_y, + static_cast<float>(i / per_row) * row_gap + }; + Text3dPtr obj = make_Text3d(tokens[i], Text3d::depth_floor, 0.003f, pos); + obj->body.hover_height = hover_y; + _ctx.words.push_back(std::move(obj)); + } +} void @@ -140,7 +153,7 @@ GameState::carriedline_Update() void GameState::pult_DispatchAction(PultAction act) { - _step_ledger.buttons_pressed.push_back(act); + _level_director->record_Button(act); switch (act) { case PultAction::Compile: on_Compile(); break; @@ -183,11 +196,11 @@ GameState::on_Pick() _editor_wall.on_WordRemoved(_ctx.wall_texts[r.wall_index]->text3d.get()); Text3dPtr base = std::move(_ctx.wall_texts[r.wall_index]->text3d); - _ctx.wall_texts.erase(_ctx.wall_texts.begin() + static_cast<std::ptrdiff_t>(r.wall_index)); + _ctx.remove_WallText(r.wall_index); if (!base) return false; if (base->type == ObjectType::Text) { - _step_ledger.picked_up.push_back(base->text); + _level_director->record_PickedUp(base->text); base->set_Look(ConfigColor::keep, Text3d::depth_floor); TextWall::show_TokenInfo(_ctx.wall_texts, base->text); } @@ -200,7 +213,7 @@ GameState::on_Pick() _ctx.words.erase(_ctx.words.begin() + static_cast<std::ptrdiff_t>(r.word_index)); if (!word) return false; - _step_ledger.picked_up.push_back(word->text); + _level_director->record_PickedUp(word->text); word->set_Look(ConfigColor::keep, Text3d::depth_floor); TextWall::show_TokenInfo(_ctx.wall_texts, word->text); @@ -231,9 +244,9 @@ GameState::on_DropLast() if (!_carried.empty()) TextWall::show_TokenInfo(_ctx.wall_texts, _carried.last->word->text); else - infowall_StepdetailsShow(); + _level_director->show_Details(_ctx); - _step_ledger.dropped = true; + _level_director->record_Dropped(); } @@ -260,9 +273,9 @@ GameState::on_ShootOne() if (!_carried.empty() && _carried.last && _carried.last->word) TextWall::show_TokenInfo(_ctx.wall_texts, _carried.last->word->text); - else infowall_StepdetailsShow(); + else _level_director->show_Details(_ctx); - _step_ledger.shot = true; + _level_director->record_Shot(); return true; } @@ -290,17 +303,16 @@ GameState::on_ShootAll() _ctx.camera->fwd, _ctx.camera->pos, aim); } - infowall_StepdetailsShow(); - - _step_ledger.shot = true; + _level_director->show_Details(_ctx); + _level_director->record_Shot(); } void GameState::on_Compile() { std::string src = TextWall::extract_Text(_ctx.wall_texts, Wall::Role::Editor); - if ( !_level_runner_sys->complete() && - _level_runner_sys->dynamic_lib()) { + if ( !_level_director->complete() && + _level_director->runner().dynamic_lib()) { const std::string path = sandbox_CanonicalPath(); std::string stripped; for (char c : src) if (!std::isspace((unsigned char)c)) @@ -425,34 +437,6 @@ GameState::projectile_FeedProgramInput(ProjectileResult& result) } -void -GameState::step_Spawn(const DynArray<std::string>& tokens) -{ - static constexpr float spacing = 3.0f; - static constexpr int per_row = 8; - static constexpr float row_gap = 3.0f; - - const double floor_y = World::get_FloorY<double>(); - const double hover_y = floor_y + 1.5; - - for (size_t i = 0; i < tokens.size(); ++i) { - size_t col = i % per_row; - size_t row = i / per_row; - - Vec3 pos { - -static_cast<float>(per_row) * spacing * 0.5f - + static_cast<float>(col) * spacing, - hover_y, - static_cast<float>(row) * row_gap - }; - - Text3dPtr obj = make_Text3d(tokens[i], Text3d::depth_floor, 0.003f, pos); - obj->body.hover_height = hover_y; - _ctx.words.push_back(std::move(obj)); - } -} - - void GameState::projectile_LandOnFloor(ProjectileResult& result) { @@ -534,24 +518,16 @@ GameState::walltext_HandleShootDown(ProjectileResult& result, _editor_wall.on_WordRemoved(wt->text3d.get()); Text3dPtr orphan = std::move(wt->text3d); - orphan->body.mode = PhysicsMode::Dynamic; - orphan->body.vel = { 0.0, 1.5, 0.0 }; orphan->body.hover_height = World::get_FloorY<double>() + 1.5; orphan->yaw = 0.0f; - orphan->set_Look(ConfigColor::keep, Text3d::depth_floor); - _ctx.words.push_back(std::move(orphan)); + _ctx.drop_WordToFloor(std::move(orphan), Vec3{0.0, 1.5, 0.0}); to_erase.push_back(result.wall_text_index); } } for (Text3dPtr& w : result.words) { if (!w) continue; - if (w->type == ObjectType::Text) { - w->set_Look(ConfigColor::keep, Text3d::depth_floor); - } - w->body.mode = PhysicsMode::Dynamic; - w->body.vel = result.vel; - _ctx.words.push_back(std::move(w)); + _ctx.drop_WordToFloor(std::move(w), result.vel); } } @@ -631,8 +607,6 @@ GameState::update(double dt) { FrameInput& input = _frame_input; - _step_age += dt; - { FP_ZONE("update.input_dispatch"); handle_Input(input, dt); } @@ -660,7 +634,7 @@ GameState::update(double dt) _word_anim_sys->update(_anims_add, _anims_drop, _carried, _ctx.words, dt); _pult_sys-> update(dt, *_ctx.camera); - _plate_sys-> update(dt, *_ctx.player, _step_plates); + _plate_sys-> update(dt, *_ctx.player, _level_director->step_Plates()); _editor_wall.update(dt, _ctx.camera->pos); _wall_connection.update(dt); @@ -679,7 +653,7 @@ GameState::update(double dt) _compiler_sys->update(_ctx.wall_texts); if (_compiler_sys->last_compile_ok) { _compiler_sys->last_compile_ok = false; - _step_ledger.compile_ok = true; + _level_director->record_CompileOk(); } _dyn_loader_sys->update(_ctx.wall_texts); @@ -696,7 +670,7 @@ GameState::update(double dt) { FP_ZONE("update.goals"); _celebration_sys->tick(dt, *_particle_sys); - level_TryAdvance(); + level_TryAdvance(dt); } input.look_x = 0; @@ -794,134 +768,7 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) void GameState::infowall_Clear() { - _ctx.wall_texts.erase( - std::remove_if(_ctx.wall_texts.begin(), _ctx.wall_texts.end(), - [](const TextWallPtr& wt) { - return wt->role == Wall::Role::Info; - }), - _ctx.wall_texts.end()); -} - - -void -GameState::step_SettingsApply() -{ - if (!_level_runner_sys || _level_runner_sys->complete()) { - _word_ai_sys->set_MaxActiveAi(0); - return; - } - - const Level::Step& step = _level_runner_sys->current(); - _word_ai_sys->set_MaxActiveAi(step.max_ai_words); -} - -void -GameState::infowall_StepdetailsShow() -{ - infowall_Clear(); - - if (!_level_runner_sys || _level_runner_sys->complete()) return; - - const std::string& details = _level_runner_sys->current().details; - if (details.empty()) return; - - Wall::Transform wall_transform = Wall::get_Transform(Wall::Role::Info, World::get_BoxScale()); - - constexpr size_t MAX_INFO_LINE_CHARS = 92; - DynArray<std::string> lines = wrap_Text(details, MAX_INFO_LINE_CHARS); - - const float bottom_y = World::get_FloorY() + 1.0f; - float y = bottom_y + static_cast<float>(lines.size() - 1) * ConfigWall::text_line_spacing; - - for (const std::string& line : lines) { - Vec3 pos = wall_transform.position; - pos.y = y; - y -= ConfigWall::text_line_spacing; - if (line.empty()) continue; - - Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.0025f, - pos, ConfigColor::white); - t->yaw = wall_transform.yaw; - - _ctx.wall_texts.push_back(cpputils::make_unique<TextWall>( - TextWall{ wall_transform.wall_position, Wall::Role::Info, std::move(t) })); - } -} - - -void -GameState::scene_Setup(const Scene& scene) -{ - for (const PlacedWord& pw : scene.words) { - Text3dPtr t = make_Text3d(pw.text, Text3d::depth_floor, 0.003f, - pw.pos, pw.color); - t->body.hover_height = pw.pos.y; - _ctx.words.push_back(std::move(t)); - } - - for (const PlacedPlate& pp : scene.plates) { - auto plate = cpputils::make_unique<Plate>(pp.pos); - Plate* raw = plate.get(); - _step_plates .push_back({ raw, pp.tag }); - _step_scenery.push_back(raw); - _ctx.scenery .push_back(std::move(plate)); - } - - for (const PlacedPlatform& pl : scene.platforms) { - auto box = cpputils::make_unique<SolidBox>(pl.pos, pl.half_extents, - pl.fill_color, pl.wire_color); - GameObject* raw = box.get(); - _step_scenery.push_back(raw); - _ctx.scenery .push_back(std::move(box)); - } - - for (const PlacedAi& a : scene.ai_seeds) { - Text3dPtr t = make_Text3d(a.text, Text3d::depth_floor, 0.003f, - a.pos, ConfigColor::floor); - t->body.hover_height = a.pos.y; - t->ai.aggressiveness = a.aggressiveness; - - switch (a.initial_state) { - case Text3dAI::State::Attack: - t->ai.state = Text3dAI::State::Attack; - t->body.mode = PhysicsMode::AiHover; - t->set_Color(ConfigColor::attack); - t->ai.state_timer = 0.0; - t->ai.phase_timer = 0.5; - break; - case Text3dAI::State::Flee: - t->ai.state = Text3dAI::State::Flee; - t->body.mode = PhysicsMode::AiHover; - t->set_Color(ConfigColor::flee); - break; - case Text3dAI::State::Idle: - case Text3dAI::State::Stunned: - break; - } - - _ctx.words.push_back(std::move(t)); - } -} - - -void -GameState::scene_Teardown() -{ - if (!_step_scenery.empty()) { - std::unordered_set<GameObject*> scenery_set(_step_scenery.begin(), _step_scenery.end()); - _ctx.scenery.erase( - std::remove_if(_ctx.scenery.begin(), _ctx.scenery.end(), - [&scenery_set](const GameObjectPtr& sp) { - return scenery_set.count(sp.get()) > 0; - }), - _ctx.scenery.end() - ); - } - - dyn_Clear(); - - _step_scenery.clear(); - _step_plates .clear(); + _ctx.clear_WallRole(Wall::Role::Info); } @@ -1032,10 +879,8 @@ GameState::update_WorldSimulation(double dt) } std::sort(wall_text_to_erase.begin(), wall_text_to_erase.end(), std::greater<size_t>{}); - for (size_t idx : wall_text_to_erase) { - if (idx < _ctx.wall_texts.size()) - _ctx.wall_texts.erase(_ctx.wall_texts.begin() + static_cast<std::ptrdiff_t>(idx)); - } + for (size_t idx : wall_text_to_erase) + _ctx.remove_WallText(idx); for (const ProjectileContactEvent& ev : projectile_contacts) { if (std::optional<PultAction> act = @@ -1046,8 +891,7 @@ GameState::update_WorldSimulation(double dt) if (ev.other && ev.other->type == ObjectType::Text) { Text3d* word = static_cast<Text3d*>(ev.other); - _step_ledger.ai_defeated.push_back(word->text); - + _level_director->record_AiDefeated(word->text); word->ai.state = Text3dAI::State::Stunned; word->ai.state_timer = 0.0; word->body.vel = Vec3{0, 0, 0}; @@ -1069,87 +913,32 @@ GameState::update_WorldSimulation(double dt) } -GoalContext -GameState::goal_ContextBuild() const +void +GameState::level_TryAdvance(double dt) { - GoalContext gctx; - - for (const StepPlate& sp : _step_plates) - if (sp.plate && sp.plate->active) - gctx.active_plate_tags.push_back(sp.tag); - - gctx.buttons_pressed_this_step = _step_ledger.buttons_pressed; - gctx.dropped_this_step = _step_ledger.dropped; - gctx.shot_this_step = _step_ledger.shot; - gctx.compile_ok_this_step = _step_ledger.compile_ok; - gctx.ai_words_defeated_this_step = _step_ledger.ai_defeated; - gctx.picked_up_this_step = _step_ledger.picked_up; - - if (gctx.compile_ok_this_step) { - std::string src = TextWall::extract_Text(_ctx.wall_texts, - Wall::Role::Editor); - gctx.editor_normalized = LevelCheck::normalize(src); - } + LevelDirector::AdvanceResult res = + _level_director->try_Advance(_ctx, dt, *_celebration_sys, *_particle_sys); - return gctx; -} + if (!res.stepped) return; + dyn_Clear(); -bool -GameState::level_TryAdvance() -{ - if (_celebration_sys->active()) { - if (_celebration_sys->progress() >= 1.0) { - _celebration_sys->finish(*_particle_sys); - level_Advance(); - return true; - } - return false; + if (res.reset_carry) { + _ctx.words .clear(); + _projectiles.clear(); + _anims_add .clear(); + _anims_drop .clear(); + _carried = CarriedLine(); } - GoalContext gctx = goal_ContextBuild(); - if (!goal_Satisfied(_level_runner_sys->current().goal, gctx)) - return false; - if (_step_age < ConfigLevel::settle_time_for_step) - return false; - if (!_level_runner_sys || _level_runner_sys->complete()) - return false; - - double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z); - _celebration_sys->start(*_ctx.camera, glyph_yaw); - return false; -} - - -void -GameState::level_Advance() -{ - scene_Teardown(); - _step_ledger.clear(); - - if (_level_runner_sys->advance()) { - if (_level_runner_sys->current().reset_carry) { - _ctx.words .clear(); - _projectiles.clear(); - _anims_add .clear(); - _anims_drop .clear(); - _carried = CarriedLine(); - } - - DynArray<std::string> - tokens = _level_runner_sys->grants_for_step(); - - step_Spawn(tokens); - scene_Setup(_level_runner_sys->current().scene); - step_SettingsApply(); - infowall_StepdetailsShow(); - _hint_sys->set_Hint(_level_runner_sys->current().hint); - } - else { + if (res.advanced) { + _level_director->enter_Step(_ctx); + _word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep()); + _hint_sys->set_Hint(_level_director->current_hint()); + } else { _hint_sys->clear(); - infowall_StepdetailsShow(); + _level_director->show_Details(_ctx); } - _step_age = 0.0; } @@ -1202,10 +991,13 @@ GameState::dyn_Apply(const DynArray<DynObject>& objs) } } -std::string GameState::sandbox_CanonicalPath() const { - return _level_runner_sys ? _level_runner_sys->source_file() : std::string{}; +std::string +GameState::sandbox_CanonicalPath() const { + return _level_director ? _level_director->runner().source_file() : std::string{}; } -std::string GameState::sandbox_BackupPath() const { + +std::string +GameState::sandbox_BackupPath() const { std::string c = sandbox_CanonicalPath(); return c.empty() ? c : (c + ".last_ok"); } @@ -1235,8 +1027,9 @@ GameState::sandbox_LoadFileToWall() } -void GameState::sandbox_Restore() { - if (!_level_runner_sys || !_level_runner_sys->dynamic_lib()) return; +void +GameState::sandbox_Restore() { + if (!_level_director || !_level_director->runner().dynamic_lib()) return; if (SourceTokens::copy_File(sandbox_BackupPath(), sandbox_CanonicalPath())) sandbox_LoadFileToWall(); } @@ -1355,11 +1148,7 @@ void GameState::editor_Clear() { _editor_wall.clear(); - - _ctx.wall_texts.erase( - std::remove_if(_ctx.wall_texts.begin(), _ctx.wall_texts.end(), - [](const TextWallPtr& wt){ return wt->role == Wall::Role::Editor; }), - _ctx.wall_texts.end()); + _ctx.clear_WallRole(Wall::Role::Editor); } @@ -1403,9 +1192,7 @@ GameState::editor_PlaceLines(const DynArray<std::string>& lines) t->yaw = tf.yaw; Vec3 snapped = _editor_wall.on_WordPlaced(t.get(), Vec3{ x_wall, y, z }); t->pos = snapped; - - _ctx.wall_texts.push_back(cpputils::make_unique<TextWall>(TextWall{ - Wall::Position::NegX, Wall::Role::Editor, std::move(t) })); + _ctx.add_WallText(Wall::Position::NegX, Wall::Role::Editor, std::move(t)); z = snapped.z - 0.5; } y -= ConfigWall::line_height; diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index 93961c6..4553153 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -16,7 +16,7 @@ #include "systems/freelook_system.hpp" #include "systems/hint_system.hpp" #include "systems/input_system.hpp" -#include "systems/level_runner_system.hpp" +#include "systems/level_director.hpp" #include "systems/particle_system.hpp" #include "systems/physics_system.hpp" #include "systems/plate_system.hpp" @@ -34,43 +34,15 @@ #include "wall/wall_label.hpp" -struct StepLedger { - DynArray<PultAction> buttons_pressed; - DynArray<std::string> ai_defeated; - DynArray<std::string> picked_up; - bool compile_ok {false}; - bool dropped {false}; - bool shot {false}; - - void clear() { - SDL_Log("LEDGER cleared (had %zu picked)", picked_up.size()); - buttons_pressed.clear(); - ai_defeated.clear(); - picked_up.clear(); - compile_ok = false; - dropped = false; - shot = false; - } -}; - - -struct StepPlate { - Plate* plate; - std::string tag; -}; - - class GameState : public IState { GameContext _ctx; FrameInput _frame_input; CarriedLine _carried; EditorWall _editor_wall; - StepLedger _step_ledger; double _pick_cooldown { 0.0 }; double _shoot_cooldown { 0.0 }; - double _step_age { 0.0 }; WallConnection _wall_connection; @@ -97,8 +69,6 @@ class GameState : public IState DynArray<WordAnim_Drop> _anims_drop; DynArray<GameObject*> _dyn_scenery; - DynArray<GameObject*> _step_scenery; - DynArray<StepPlate> _step_plates; cpputils::unique_ptr<InputSystem> _input_sys; cpputils::unique_ptr<PhysicsSystem> _physics_sys; @@ -113,9 +83,9 @@ class GameState : public IState cpputils::unique_ptr<PlateSystem> _plate_sys; cpputils::unique_ptr<ParticleSystem> _particle_sys; cpputils::unique_ptr<FloorMandelbrot> _floor_sys; - cpputils::unique_ptr<LevelRunner> _level_runner_sys; cpputils::unique_ptr<DynLoaderSystem> _dyn_loader_sys; cpputils::unique_ptr<CelebrationSystem>_celebration_sys; + cpputils::unique_ptr<LevelDirector> _level_director; bool on_Pick (); @@ -134,25 +104,14 @@ class GameState : public IState void on_TypingCommit (FrameInput&); void on_TypingCancel (FrameInput&); - void scene_Setup (const Scene& scene); - void scene_Teardown (); - void update_WorldSimulation - (double dt); - void step_Spawn (const DynArray<std::string>& tokens); - void step_SettingsApply(); - - bool level_TryAdvance (); - void level_Advance (); - GoalContext - goal_ContextBuild () const; - + void update_WorldSimulation(double dt); + void level_TryAdvance (double dt); void carriedline_Update (); void carry_BeginAnim (Text3dPtr word, bool with_pitch); void typing_RebuildPreview (const std::string& s); void typing_ClearPreview (); void infowall_Clear (); - void infowall_StepdetailsShow (); void walltext_HandleShootDown (ProjectileResult& result, DynArray<size_t>& to_erase); void projectile_FeedProgramInput(ProjectileResult&); diff --git a/src/systems/level_director.cpp b/src/systems/level_director.cpp new file mode 100644 index 0000000..89b32b9 --- /dev/null +++ b/src/systems/level_director.cpp @@ -0,0 +1,227 @@ +#include "systems/level_director.hpp" + +#include <algorithm> +#include <unordered_set> + +#include "entities/camera.hpp" +#include "entities/plate.hpp" +#include "entities/solid_box.hpp" +#include "entities/world.hpp" +#include "levels/level_check.hpp" +#include "states/game_context.hpp" +#include "systems/celebration_system.hpp" +#include "systems/particle_system.hpp" +#include "utils/config.hpp" +#include "utils/text_wrap.hpp" +#include "wall/text_wall.hpp" + + +LevelDirector::LevelDirector(const Level::Data& level) + : _runner(cpputils::make_unique<LevelRunner>(level)) +{} + + +int +LevelDirector::get_MaxAiForStep() const +{ + if (_runner->complete()) return 0; + return _runner->current().max_ai_words; +} + + +void +LevelDirector::enter_Step(GameContext& ctx) +{ + spawn_Grants(ctx); + setup_Scene(ctx, _runner->current().scene); + show_Details(ctx); + _step_age = 0.0; +} + + +void +LevelDirector::spawn_Grants(GameContext& ctx) +{ + DynArray<std::string> tokens = _runner->grants_for_step(); + + 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<double>() + 1.5; + + for (size_t i = 0; i < tokens.size(); ++i) { + size_t col = i % per_row; + size_t row = i / per_row; + + Vec3 pos { + -static_cast<float>(per_row) * spacing * 0.5f + + static_cast<float>(col) * spacing, + hover_y, + static_cast<float>(row) * row_gap + }; + + Text3dPtr obj = make_Text3d(tokens[i], Text3d::depth_floor, 0.003f, pos); + obj->body.hover_height = hover_y; + ctx.words.push_back(std::move(obj)); + } +} + + +void +LevelDirector::setup_Scene(GameContext& ctx, const Scene& scene) +{ + for (const PlacedWord& pw : scene.words) { + Text3dPtr t = make_Text3d(pw.text, Text3d::depth_floor, 0.003f, pw.pos, pw.color); + t->body.hover_height = pw.pos.y; + ctx.words.push_back(std::move(t)); + } + + for (const PlacedPlate& pp : scene.plates) { + auto plate = cpputils::make_unique<Plate>(pp.pos); + Plate* raw = plate.get(); + _step_plates .push_back({ raw, pp.tag }); + _step_scenery.push_back(raw); + ctx.scenery .push_back(std::move(plate)); + } + + for (const PlacedPlatform& pl : scene.platforms) { + auto box = cpputils::make_unique<SolidBox>(pl.pos, pl.half_extents, + pl.fill_color, pl.wire_color); + GameObject* raw = box.get(); + _step_scenery.push_back(raw); + ctx.scenery .push_back(std::move(box)); + } + + for (const PlacedAi& a : scene.ai_seeds) { + Text3dPtr t = make_Text3d(a.text, Text3d::depth_floor, 0.003f, a.pos, ConfigColor::floor); + t->body.hover_height = a.pos.y; + t->ai.aggressiveness = a.aggressiveness; + + switch (a.initial_state) { + case Text3dAI::State::Attack: + t->ai.state = Text3dAI::State::Attack; + t->body.mode = PhysicsMode::AiHover; + t->set_Color(ConfigColor::attack); + t->ai.state_timer = 0.0; + t->ai.phase_timer = 0.5; + break; + case Text3dAI::State::Flee: + t->ai.state = Text3dAI::State::Flee; + t->body.mode = PhysicsMode::AiHover; + t->set_Color(ConfigColor::flee); + break; + case Text3dAI::State::Idle: + case Text3dAI::State::Stunned: + break; + } + + ctx.words.push_back(std::move(t)); + } +} + + +void +LevelDirector::teardown_Scene(GameContext& ctx) +{ + if (!_step_scenery.empty()) { + std::unordered_set<GameObject*> scenery_set(_step_scenery.begin(), _step_scenery.end()); + ctx.scenery.erase( + std::remove_if(ctx.scenery.begin(), ctx.scenery.end(), + [&scenery_set](const GameObjectPtr& sp) { + return scenery_set.count(sp.get()) > 0; + }), + ctx.scenery.end()); + } + _step_scenery.clear(); + _step_plates .clear(); +} + + +void +LevelDirector::show_Details(GameContext& ctx) const +{ + ctx.clear_WallRole(Wall::Role::Info); + + if (_runner->complete()) return; + + const std::string& details = _runner->current().details; + if (details.empty()) return; + + Wall::Transform wt = Wall::get_Transform(Wall::Role::Info, World::get_BoxScale()); + + constexpr size_t MAX_INFO_LINE_CHARS = 92; + DynArray<std::string> lines = wrap_Text(details, MAX_INFO_LINE_CHARS); + + const float bottom_y = World::get_FloorY() + 1.0f; + float y = bottom_y + static_cast<float>(lines.size() - 1) * ConfigWall::text_line_spacing; + + for (const std::string& line : lines) { + Vec3 pos = wt.position; + pos.y = y; + y -= ConfigWall::text_line_spacing; + if (line.empty()) continue; + + Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.0025f, pos, ConfigColor::white); + t->yaw = wt.yaw; + ctx.add_WallText(wt.wall_position, Wall::Role::Info, std::move(t)); + } +} + + +GoalContext +LevelDirector::build_GoalCtx(const GameContext& ctx) const +{ + GoalContext gctx; + + for (const StepPlate& sp : _step_plates) + if (sp.plate && sp.plate->active) + gctx.active_plate_tags.push_back(sp.tag); + + gctx.buttons_pressed_this_step = _ledger.buttons_pressed; + gctx.dropped_this_step = _ledger.dropped; + gctx.shot_this_step = _ledger.shot; + gctx.compile_ok_this_step = _ledger.compile_ok; + gctx.ai_words_defeated_this_step = _ledger.ai_defeated; + gctx.picked_up_this_step = _ledger.picked_up; + + if (gctx.compile_ok_this_step) { + std::string src = TextWall::extract_Text(ctx.wall_texts, Wall::Role::Editor); + gctx.editor_normalized = LevelCheck::normalize(src); + } + return gctx; +} + + +LevelDirector::AdvanceResult +LevelDirector::try_Advance(GameContext& ctx, double dt, + CelebrationSystem& celebration, ParticleSystem& particles) +{ + _step_age += dt; + AdvanceResult res; + + if (celebration.active()) { + if (celebration.progress() >= 1.0) { + celebration.finish(particles); + + teardown_Scene(ctx); + _ledger.clear(); + + res.stepped = true; + res.advanced = _runner->advance(); + res.reset_carry = res.advanced && _runner->current().reset_carry; + _step_age = 0.0; + } + return res; + } + + if (_runner->complete()) return res; + + GoalContext gctx = build_GoalCtx(ctx); + if (!goal_Satisfied(_runner->current().goal, gctx)) return res; + if (_step_age < ConfigLevel::settle_time_for_step) return res; + + double glyph_yaw = SDL_atan2(-ctx.camera->fwd.x, -ctx.camera->fwd.z); + celebration.start(*ctx.camera, glyph_yaw); + return res; +} diff --git a/src/systems/level_director.hpp b/src/systems/level_director.hpp new file mode 100644 index 0000000..289d9c4 --- /dev/null +++ b/src/systems/level_director.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include <string> + +#include "cpputils/unique_ptr.hpp" +#include "entities/game_object.hpp" +#include "levels/goal.hpp" +#include "levels/level.hpp" +#include "levels/scene.hpp" +#include "systems/level_runner_system.hpp" +#include "utils/globals.hpp" + +struct GameContext; +class Plate; +class WordAiSystem; +class HintSystem; +class CelebrationSystem; +class ParticleSystem; + + + +struct StepLedger { + DynArray<PultAction> buttons_pressed; + DynArray<std::string> ai_defeated; + DynArray<std::string> picked_up; + bool compile_ok {false}; + bool dropped {false}; + bool shot {false}; + + void clear() { + buttons_pressed.clear(); + ai_defeated.clear(); + picked_up.clear(); + compile_ok = false; + dropped = false; + shot = false; + } +}; + + +struct StepPlate { + Plate* plate; + std::string tag; +}; + + +class LevelDirector +{ + cpputils::unique_ptr<LevelRunner> _runner; + StepLedger _ledger; + DynArray<GameObject*> _step_scenery; + DynArray<StepPlate> _step_plates; + double _step_age { 0.0 }; + + void spawn_Grants (GameContext& ctx); + void setup_Scene (GameContext& ctx, const Scene& scene); + void teardown_Scene(GameContext& ctx); + GoalContext build_GoalCtx (const GameContext& ctx) const; +public: + struct AdvanceResult { + bool stepped {false}; // a step boundary was crossed this frame + bool advanced {false}; // ...and there is a real next step + bool reset_carry {false}; // next step requests carry/projectile wipe + }; + + explicit LevelDirector(const Level::Data& level); + + void enter_Step(GameContext& ctx); + + AdvanceResult try_Advance(GameContext& ctx, double dt, + CelebrationSystem& celebration, + ParticleSystem& particles); + + void show_Details(GameContext& ctx) const; + + void record_PickedUp (const std::string& t) { _ledger.picked_up.push_back(t); } + void record_Shot () { _ledger.shot = true; } + void record_Dropped () { _ledger.dropped = true; } + void record_Button (PultAction a) { _ledger.buttons_pressed.push_back(a); } + void record_AiDefeated(const std::string& t) { _ledger.ai_defeated.push_back(t); } + void record_CompileOk () { _ledger.compile_ok = true; } + + LevelRunner& runner() { return *_runner; } + const LevelRunner& runner() const { return *_runner; } + bool complete() const { return _runner->complete(); } + int get_MaxAiForStep() const; + const std::string& current_hint() const { return _runner->current().hint; } + DynArray<StepPlate>& step_Plates() { return _step_plates; } +};