#include #include "core/game.hpp" #include "levels/level.hpp" #include "menu_state.hpp" #include "rendering/gl_renderer.hpp" #include "states/game_state.hpp" namespace Nimbin { static constexpr float TITLE_SCALE = 0.018f; static constexpr float ITEM_SCALE = 0.012f; static constexpr float TEXT_DEPTH = 0.10f; static constexpr double EYE_Y = 0.0; static constexpr double TITLE_Y = 2.0; static constexpr double COLUMN_RADIUS = 8.0; static constexpr double LEVEL_SPACING = 1.6; static constexpr double COLUMN_LEFT_RAD = -SDL_PI_D * 0.35; static constexpr double COLUMN_MID_RAD = 0.0; static constexpr double COLUMN_RIGHT_RAD = SDL_PI_D * 0.35; static constexpr double MOUSE_SENSITIVITY = 1.0 / 2000.0; static constexpr double SCROLL_LERP_RATE = 8.0; static constexpr SDL_FColor COLOR_TITLE {0.95f, 0.95f, 1.0f, 1.0f}; static constexpr SDL_FColor COLOR_SELECTED {1.0f, 1.0f, 1.0f, 1.0f}; static constexpr SDL_FColor COLOR_NORMAL {0.45f, 0.45f, 0.55f, 1.0f}; uint32_t MenuState::yaw_FromRadians(double rad) { constexpr double TWO_PI = SDL_PI_D * 2.0; while (rad < 0.0) rad += TWO_PI; while (rad >= TWO_PI) rad -= TWO_PI; return static_cast(rad * (2147483648.0 / SDL_PI_D)); } double MenuState::yaw_ToRadians(uint32_t yaw) { return static_cast(yaw) * (Config::Physics::binrad); } static Vec3 slot_PosForColumn(double column_angle_rad, double y) { return Vec3 { COLUMN_RADIUS * SDL_sin(column_angle_rad), y, -COLUMN_RADIUS * SDL_cos(column_angle_rad) }; } MenuState::MenuState() { _world = cpputils::make_unique(); _camera = cpputils::make_unique(); _light = cpputils::make_unique(); _camera->pos = { 0.0, EYE_Y, 0.0 }; _camera->yaw = 0; _camera->pitch = 0; _title = Text::make_Token("Runtime Compiler", TEXT_DEPTH, TITLE_SCALE, slot_PosForColumn(COLUMN_MID_RAD, TITLE_Y), COLOR_TITLE); _title->body.mode = PhysicsMode::Static; build_Items(); } bool MenuState::is_LevelItem(size_t idx) const { return idx < _items.size() && _items[idx].kind == ItemKind::StartLevel; } void MenuState::build_Items() { _items.clear(); { MenuItem mi; mi.label = "Open File"; mi.kind = ItemKind::OpenFile; mi.slot_pos = slot_PosForColumn(COLUMN_LEFT_RAD, EYE_Y); _items.push_back(std::move(mi)); } for (size_t i = 0; i < Level::get_Count(); i++) { MenuItem mi; mi.label = Level::get_Data(i).name; mi.kind = ItemKind::StartLevel; mi.level_index = i; mi.slot_pos = slot_PosForColumn(COLUMN_MID_RAD, EYE_Y); _items.push_back(std::move(mi)); } { MenuItem mi; mi.label = "Quit"; mi.kind = ItemKind::Quit; mi.slot_pos = slot_PosForColumn(COLUMN_RIGHT_RAD, EYE_Y); _items.push_back(std::move(mi)); } for (size_t i = 0; i < _items.size(); i++) { const auto color = (i == _selected) ? COLOR_SELECTED : COLOR_NORMAL; _items[i].text3d = Text::make_Token(_items[i].label, TEXT_DEPTH, ITEM_SCALE, _items[i].slot_pos, color); _items[i].text3d->body.mode = PhysicsMode::Static; _items[i].text3d->set_ColorSource(Text::ColorSource::Static); } } void MenuState::handle_Event(Game& game, const SDL_Event& e) { if (e.type == SDL_EVENT_MOUSE_MOTION) { const double dx_rad = static_cast(e.motion.xrel) * MOUSE_SENSITIVITY; const double dy_rad = static_cast(e.motion.yrel) * MOUSE_SENSITIVITY; const double cur_yaw = yaw_ToRadians(_camera->yaw); _camera->yaw = yaw_FromRadians(cur_yaw - dx_rad); constexpr int32_t PITCH_MAX = 0x40000000; const int32_t dpitch = static_cast( dy_rad * (2147483648.0 / SDL_PI_D)); int32_t p = _camera->pitch - dpitch; if (p > PITCH_MAX) p = PITCH_MAX; if (p < -PITCH_MAX) p = -PITCH_MAX; _camera->pitch = p; return; } if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) { const size_t hit = pick_FromCrosshair(); if (hit != static_cast(-1)) { _selected = hit; activate(game); } return; } if (e.type != SDL_EVENT_KEY_DOWN) return; switch (e.key.key) { case SDLK_W: case SDLK_UP: if (is_LevelItem(pick_FromCrosshair())) { if (_scroll_target > 0.0) _scroll_target -= 1.0; } break; case SDLK_S: case SDLK_DOWN: if (is_LevelItem(pick_FromCrosshair())) { const double max_scroll = static_cast(Level::get_Count() > 0 ? Level::get_Count() - 1 : 0); if (_scroll_target < max_scroll) _scroll_target += 1.0; } break; case SDLK_RETURN: case SDLK_SPACE: { const size_t hit = pick_FromCrosshair(); if (hit != static_cast(-1)) { _selected = hit; activate(game); } break; } /* case SDLK_ESCAPE: { SDL_Event quit_event; quit_event.type = SDL_EVENT_QUIT; SDL_PushEvent(&quit_event); break; } */ } } void MenuState::activate(Game& game) { if (_selected >= _items.size()) return; const MenuItem& mi = _items[_selected]; switch (mi.kind) { case ItemKind::StartLevel: game.change_State(cpputils::make_unique(mi.level_index)); break; case ItemKind::OpenFile: SDL_Log("MenuState: OpenFile not yet implemented"); break; case ItemKind::Quit: { SDL_Event quit_event; quit_event.type = SDL_EVENT_QUIT; SDL_PushEvent(&quit_event); break; } } } size_t MenuState::pick_FromCrosshair() const { const double yaw = yaw_ToRadians(_camera->yaw); const double pitch = static_cast(_camera->pitch) * Config::Physics::binrad; const double cy = SDL_cos(yaw), sy = SDL_sin(yaw); const double cp = SDL_cos(pitch), sp = SDL_sin(pitch); Vec3 fwd { -sy * cp, sp, -cy * cp }; size_t best = static_cast(-1); double best_t = 1e9; for (size_t i = 0; i < _items.size(); i++) { if (!_items[i].text3d || !_items[i].text3d->loaded) continue; const Vec3 mn = _items[i].text3d->aabb_Min(); const Vec3 mx = _items[i].text3d->aabb_Max(); double tmin = 0.0, tmax = 1e9; const double ox = _camera->pos.x; const double oy = _camera->pos.y; const double oz = _camera->pos.z; auto slab = [&](double o, double d, double lo, double hi) -> bool { if (SDL_fabs(d) < 1e-9) return o >= lo && o <= hi; double t1 = (lo - o) / d; double t2 = (hi - o) / d; if (t1 > t2) std::swap(t1, t2); if (t1 > tmin) tmin = t1; if (t2 < tmax) tmax = t2; return tmin <= tmax; }; if (!slab(ox, fwd.x, mn.x, mx.x)) continue; if (!slab(oy, fwd.y, mn.y, mx.y)) continue; if (!slab(oz, fwd.z, mn.z, mx.z)) continue; if (tmin < 0.0) continue; if (tmin < best_t) { best_t = tmin; best = i; } } return best; } void MenuState::update(double dt, const GlRenderer*) { _time += dt; const double lerp_factor = 1.0 - SDL_exp(-SCROLL_LERP_RATE * dt); _scroll_current += (_scroll_target - _scroll_current) * lerp_factor; size_t level_n = 0; for (MenuItem& mi : _items) { if (mi.kind != ItemKind::StartLevel) continue; const double y = EYE_Y - (static_cast(level_n) - _scroll_current) * LEVEL_SPACING; mi.text3d->pos = { mi.slot_pos.x, y, mi.slot_pos.z }; ++level_n; } for (MenuItem& mi : _items) { if (mi.kind == ItemKind::StartLevel) continue; mi.text3d->pos = mi.slot_pos; } const size_t hover = pick_FromCrosshair(); if (hover != static_cast(-1) && hover != _selected) { _items[_selected].text3d->set_Color(COLOR_NORMAL); _items[hover].text3d->set_Color(COLOR_SELECTED); _selected = hover; } for (MenuItem& mi : _items) { if (!mi.text3d) continue; const double dx = _camera->pos.x - mi.text3d->pos.x; const double dz = _camera->pos.z - mi.text3d->pos.z; mi.text3d->yaw = static_cast(SDL_atan2(dx, dz)); } if (_title) { const double dx = _camera->pos.x - _title->pos.x; const double dz = _camera->pos.z - _title->pos.z; _title->yaw = static_cast(SDL_atan2(dx, dz)); } if (_selected < _items.size() && _items[_selected].text3d) { const double hover_phase = SDL_sin(_time * 3.0) * 0.10; _items[_selected].text3d->pos.y += hover_phase; } } void MenuState::render(SDL_Window& window, GlRenderer& renderer) { int w, h; SDL_GetWindowSizeInPixels(&window, &w, &h); const double yaw_r = Config::Physics::binrad * static_cast(_camera->yaw); const double pitch_r = Config::Physics::binrad * static_cast(_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); _light->flashlight_pos = _camera->pos; _light->flashlight_dir = Vec3{ -sy * cp, sp, -cy * cp }; renderer.begin_Frame(w, h); renderer.set_Light(*_light); _world->draw(renderer, *_camera, *_light); if (_title) _title->draw(renderer, *_camera, *_light); for (const MenuItem& mi : _items) if (mi.text3d) mi.text3d->draw(renderer, *_camera, *_light); renderer.end_Frame(&window); } } // namespace Nimbin