#include #include #include "entities/camera.hpp" #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/particle_system.hpp" #include "utils/config.hpp" #include "text/text.hpp" #include "wall/wall.hpp" namespace Nimbin { namespace Level { Director::Director(const Level::Data& level) : _runner(cpputils::make_unique(level)) {} int Director::get_MaxAiForStep() const { if (_runner->is_Complete()) return 0; return _runner->get_Current().max_ai_words; } void Director::enter_Step(GameContext& ctx) { spawn_Grants(ctx); setup_Scene (ctx, _runner->get_Current().scene); show_Details(ctx); _step_age = 0.0; } void Director::spawn_Grants(GameContext& ctx) { DynArray tokens = _runner->get_GarantsForStep(); 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++) { size_t col = i % per_row; size_t row = i / per_row; Vec3 pos { -static_cast(per_row) * spacing * 0.5f + static_cast(col) * spacing, hover_y, static_cast(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 Director::setup_Scene(GameContext& ctx, const Scene::Data& scene) { for (const Scene::Placed::Word& pw : scene.words) { Text::TokenPtr t = Text::make_Token(pw.text, Text::Token::depth_floor, 0.003f, pw.pos, pw.color); t->body.hover_height = pw.pos.y; ctx.words.push_back(std::move(t)); } for (const Scene::Placed::Plate& pp : scene.plates) { auto plate = cpputils::make_unique(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 Scene::Placed::Platform& pl : scene.platforms) { auto box = cpputils::make_unique(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 Scene::Placed::Ai& a : scene.ai_seeds) { Text::TokenPtr t = Text::make_Token(a.text, Text::Token::depth_floor, 0.003f, a.pos, Config::Color::floor); t->body.hover_height = a.pos.y; t->ai.aggressiveness = a.aggressiveness; switch (a.initial_state) { case Text::Token::Ai::State::Attack: t->ai.state = Text::Token::Ai::State::Attack; t->body.mode = PhysicsMode::AiHover; t->set_Color (Config::Color::attack); t->set_ColorSource(Text::ColorSource::Static); t->ai.state_timer = 0.0; t->ai.phase_timer = 0.5; break; case Text::Token::Ai::State::Flee: t->ai.state = Text::Token::Ai::State::Flee; t->body.mode = PhysicsMode::AiHover; t->set_Color (Config::Color::flee); t->set_ColorSource(Text::ColorSource::Static); break; case Text::Token::Ai::State::Idle: case Text::Token::Ai::State::Stunned: break; } ctx.words.push_back(std::move(t)); } } void Director::teardown_Scene(GameContext& ctx) { if (!_step_scenery.empty()) { std::unordered_set 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 Director::show_Details(GameContext& ctx) const { if (!ctx.info_wall) return; Text::Handler& info = *ctx.info_wall; if (_runner->is_Complete()) { info.clear(); _detail_built_for = nullptr; return; } const Level::Step* step = &_runner->get_Current(); if (_detail_built_for != step) { _detail_tokens.clear(); const std::string& details = step->details; DynArray lines = details.empty() ? DynArray{} : Text::wrap_String(details, 92); float y = World::get_FloorY() + 1.0f; const Wall::Transform tf = Wall::get_Transform(Wall::Role::Info, World::get_BoxScale()); for (const std::string& line : lines) { if (line.empty()) { y += Config::Wall::text_line_spacing; continue; } Text::TokenPtr t = Text::make_Token(line, Text::Token::depth_wall, 0.003f, tf.position, Config::Color::white); t->yaw = tf.yaw; t->pos = tf.position; t->pos.y = y; t->set_ColorSource(Text::ColorSource::Static); t->set_Color(Config::Color::white); _detail_tokens.push_back(std::move(t)); y += Config::Wall::text_line_spacing; } _detail_built_for = step; } info.clear(); info.set_Borrowed(&_detail_tokens); } Goal::Context Director::build_GoalCtx(const GameContext& ctx) const { Goal::Context 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 && ctx.editor_wall) { std::string src; for (const std::string& l : ctx.editor_wall->as_Lines()) { src += l; src += '\n'; } gctx.editor_normalized = Level::Check::normalize(src); } return gctx; } Director::AdvanceResult Director::try_Advance(GameContext& ctx, double dt, CelebrationSystem& celebration, ParticleSystem& particles) { _step_age += dt; AdvanceResult res; if (celebration.is_Active()) { if (celebration.get_Progress() >= 1.0) { celebration.finish(particles); teardown_Scene(ctx); _ledger.clear(); res.stepped = true; res.advanced = _runner->advance(); res.reset_carry = res.advanced && _runner->get_Current().reset_carry; _step_age = 0.0; } return res; } if (_runner->is_Complete()) return res; Goal::Context gctx = build_GoalCtx(ctx); if (!Goal::is_Satisfied(_runner->get_Current().goal, gctx)) return res; if (_step_age < Config::Level::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; } } // namespace Level } // namespace Nimbin