Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/states/menu_state.cpp

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

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.gz
src/states/menu_state.cpp 10 KB · 354 lines raw
#include <SDL3/SDL.h>

#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<uint32_t>(rad * (2147483648.0 / SDL_PI_D));
}

double
MenuState::yaw_ToRadians(uint32_t yaw)
{
    return static_cast<double>(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<World>();
    _camera = cpputils::make_unique<Camera>();
    _light  = cpputils::make_unique<Light>();

    _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<double>(e.motion.xrel) * MOUSE_SENSITIVITY;
        const double dy_rad = static_cast<double>(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<int32_t>(
            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<size_t>(-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<double>(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<size_t>(-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<GameState>(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<double>(_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<size_t>(-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<double>(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<size_t>(-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<float>(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<float>(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<double>(_camera->yaw);
    const double pitch_r = Config::Physics::binrad * static_cast<double>(_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