diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f5c178d0..cc671e1d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -183,6 +183,7 @@ public: int sfxVolume = 100; float mouseSensitivity = 0.2f; bool invertMouse = false; + bool introSeen = false; }; bool getSinglePlayerSettings(SinglePlayerSettings& out) const; void setSinglePlayerSettings(const SinglePlayerSettings& settings); diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index df9af3fa..40281864 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -41,6 +41,8 @@ public: } void reset(); + void startIntroPan(float durationSec = 2.5f, float orbitDegrees = 120.0f); + bool isIntroActive() const { return introActive; } float getMovementSpeed() const { return movementSpeed; } const glm::vec3& getDefaultPosition() const { return defaultPosition; } @@ -178,6 +180,14 @@ private: glm::vec3 defaultPosition = glm::vec3(-9464.0f, 62.0f, 200.0f); float defaultYaw = 0.0f; float defaultPitch = -5.0f; + + // Spawn intro camera pan + bool introActive = false; + float introTimer = 0.0f; + float introDuration = 0.0f; + float introStartYaw = 0.0f; + float introOrbitDegrees = 0.0f; + float introPitch = -10.0f; }; } // namespace rendering diff --git a/src/core/application.cpp b/src/core/application.cpp index ea8b7b46..08cefd3e 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1155,7 +1155,28 @@ void Application::startSinglePlayer() { if (gameHandler && renderer && window) { game::GameHandler::SinglePlayerSettings settings; - if (gameHandler->getSinglePlayerSettings(settings)) { + bool hasSettings = gameHandler->getSinglePlayerSettings(settings); + if (!hasSettings) { + settings.fullscreen = window->isFullscreen(); + settings.vsync = window->isVsyncEnabled(); + settings.shadows = renderer->areShadowsEnabled(); + settings.resWidth = window->getWidth(); + settings.resHeight = window->getHeight(); + if (auto* music = renderer->getMusicManager()) { + settings.musicVolume = music->getVolume(); + } + if (auto* footstep = renderer->getFootstepManager()) { + settings.sfxVolume = static_cast(footstep->getVolumeScale() * 100.0f + 0.5f); + } + if (auto* cameraController = renderer->getCameraController()) { + settings.mouseSensitivity = cameraController->getMouseSensitivity(); + settings.invertMouse = cameraController->isInvertMouse(); + } + settings.introSeen = false; + gameHandler->setSinglePlayerSettings(settings); + hasSettings = true; + } + if (hasSettings) { window->setVsync(settings.vsync); window->setFullscreen(settings.fullscreen); if (settings.resWidth > 0 && settings.resHeight > 0) { @@ -1175,6 +1196,11 @@ void Application::startSinglePlayer() { if (auto* cameraController = renderer->getCameraController()) { cameraController->setMouseSensitivity(settings.mouseSensitivity); cameraController->setInvertMouse(settings.invertMouse); + if (!settings.introSeen) { + cameraController->startIntroPan(2.8f, 140.0f); + settings.introSeen = true; + gameHandler->setSinglePlayerSettings(settings); + } } } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b70bf0db..88766a14 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -204,9 +204,12 @@ struct SinglePlayerSqlite { " music_volume INTEGER," " sfx_volume INTEGER," " mouse_sensitivity REAL," - " invert_mouse INTEGER" + " invert_mouse INTEGER," + " intro_seen INTEGER" ");"; - return exec(kSchema); + bool ok = exec(kSchema); + exec("ALTER TABLE character_settings ADD COLUMN intro_seen INTEGER DEFAULT 0;"); + return ok; } }; @@ -1636,7 +1639,7 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) { spLastDirtyOrientation_ = movementInfo.orientation; const char* sqlSettings = - "SELECT fullscreen, vsync, shadows, res_w, res_h, music_volume, sfx_volume, mouse_sensitivity, invert_mouse " + "SELECT fullscreen, vsync, shadows, res_w, res_h, music_volume, sfx_volume, mouse_sensitivity, invert_mouse, intro_seen " "FROM character_settings WHERE guid=?;"; if (sqlite3_prepare_v2(sp.db, sqlSettings, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_int64(stmt, 1, static_cast(guid)); @@ -1650,6 +1653,7 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) { spSettings_.sfxVolume = sqlite3_column_int(stmt, 6); spSettings_.mouseSensitivity = static_cast(sqlite3_column_double(stmt, 7)); spSettings_.invertMouse = sqlite3_column_int(stmt, 8) != 0; + spSettings_.introSeen = sqlite3_column_int(stmt, 9) != 0; spSettingsLoaded_ = true; } sqlite3_finalize(stmt); @@ -1810,13 +1814,13 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) { if (spSettingsLoaded_ && (force || (spDirtyFlags_ & SP_DIRTY_SETTINGS))) { const char* upsertSettings = "INSERT INTO character_settings " - "(guid, fullscreen, vsync, shadows, res_w, res_h, music_volume, sfx_volume, mouse_sensitivity, invert_mouse) " - "VALUES (?,?,?,?,?,?,?,?,?,?) " + "(guid, fullscreen, vsync, shadows, res_w, res_h, music_volume, sfx_volume, mouse_sensitivity, invert_mouse, intro_seen) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?) " "ON CONFLICT(guid) DO UPDATE SET " "fullscreen=excluded.fullscreen, vsync=excluded.vsync, shadows=excluded.shadows, " "res_w=excluded.res_w, res_h=excluded.res_h, music_volume=excluded.music_volume, " "sfx_volume=excluded.sfx_volume, mouse_sensitivity=excluded.mouse_sensitivity, " - "invert_mouse=excluded.invert_mouse;"; + "invert_mouse=excluded.invert_mouse, intro_seen=excluded.intro_seen;"; if (sqlite3_prepare_v2(sp.db, upsertSettings, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_int64(stmt, 1, static_cast(activeCharacterGuid_)); sqlite3_bind_int(stmt, 2, spSettings_.fullscreen ? 1 : 0); @@ -1828,6 +1832,7 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) { sqlite3_bind_int(stmt, 8, spSettings_.sfxVolume); sqlite3_bind_double(stmt, 9, spSettings_.mouseSensitivity); sqlite3_bind_int(stmt, 10, spSettings_.invertMouse ? 1 : 0); + sqlite3_bind_int(stmt, 11, spSettings_.introSeen ? 1 : 0); sqlite3_step(stmt); sqlite3_finalize(stmt); } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 8c6c3ea5..abe07d62 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -56,6 +56,17 @@ CameraController::CameraController(Camera* cam) : camera(cam) { reset(); } +void CameraController::startIntroPan(float durationSec, float orbitDegrees) { + if (!camera) return; + introActive = true; + introTimer = 0.0f; + introDuration = std::max(0.5f, durationSec); + introStartYaw = yaw; + introOrbitDegrees = orbitDegrees; + introPitch = pitch; + thirdPerson = true; +} + void CameraController::update(float deltaTime) { if (!enabled || !camera) { return; @@ -77,6 +88,24 @@ void CameraController::update(float deltaTime) { bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL)); bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE); + if (introActive) { + if (leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump) { + introActive = false; + } else { + introTimer += deltaTime; + float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f; + yaw = introStartYaw + introOrbitDegrees * t; + pitch = introPitch; + camera->setRotation(yaw, pitch); + facingYaw = yaw; + if (t >= 1.0f) { + introActive = false; + } + } + // Suppress player movement/input during intro. + keyW = keyS = keyA = keyD = keyQ = keyE = nowJump = false; + } + bool mouseAutorun = !uiWantsKeyboard && !sitting && leftMouseDown && rightMouseDown; bool nowForward = keyW || mouseAutorun; bool nowBackward = keyS; @@ -959,6 +988,9 @@ void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) { if (!enabled || !camera) { return; } + if (introActive) { + return; + } if (!mouseButtonDown) { return;