#include "Game.h" #include "AnimatedModel.h" #include "Localization.h" #include "utils/Utils.h" #include #include "items/ItemRegistry.h" #include "render/OpenGlExtensions.h" #include #include "render/TextureManager.h" #include "TextModel.h" #include #include #include #include #ifdef __ANDROID__ #include #endif #ifdef EMSCRIPTEN #include #endif #include "GameConstants.h" namespace ZL { static const float zoomMin = 6.0f; static const float zoomMax = 20.0f; #ifdef EMSCRIPTEN const char* CONST_ZIP_FILE = "resources.zip"; #else const char* CONST_ZIP_FILE = ""; #endif float x = 0; float y = 0; float z = 0; #ifdef EMSCRIPTEN Game* Game::s_instance = nullptr; void Game::onResourcesZipLoaded(unsigned /*handle*/, void* /*userData*/, const char* /*filename*/) { std::cout << "Resources.zip loaded successfully--0" << std::endl; if (s_instance) { s_instance->resourcesDownloadProgress = 1.0f; // Builds the loadSteps queue (cheap, no GL work); the actual loading // happens one step per frame in Game::update() so the browser tab // keeps repainting/responding instead of freezing for the whole load. s_instance->setupPart2(); } } void Game::onResourcesZipError(unsigned /*handle*/, void* /*userData*/, int /*httpStatus*/) { std::cout << "Failed to download resources.zip" << std::endl; } void Game::onResourcesZipProgress(unsigned /*handle*/, void* /*userData*/, int percentComplete) { if (s_instance) s_instance->resourcesDownloadProgress = percentComplete / 100.0f; } #endif Location* Game::currentLocation() const { if (gameState.currentLocationName.empty()) return nullptr; auto it = gameState.locations.find(gameState.currentLocationName); return (it != gameState.locations.end()) ? it->second.get() : nullptr; } Game::Game() : newTickCount(0) , lastTickCount(0) , audioPlayer(std::make_unique()) , menuManager(renderer, gameState, *audioPlayer) { } Game::~Game() { #ifndef EMSCRIPTEN // In Emscripten, SDL must stay alive across context loss/restore cycles // so the window remains valid when the game object is re-created. SDL_Quit(); #endif } void Game::setup() { Environment::width = Environment::CONST_DEFAULT_WIDTH; Environment::height = Environment::CONST_DEFAULT_HEIGHT; Environment::computeProjectionDimensions(); ZL::BindOpenGlFunctions(); ZL::CheckGlError(__FILE__, __LINE__); renderer.InitOpenGL(); #ifdef EMSCRIPTEN // These shaders and loading.png are preloaded separately (not from zip), // so they are available immediately without waiting for resources.zip. renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", ""); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", ""); #else renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE); #endif loadingTexture = renderer.textureManager.LoadFromPng("resources/loading.png", ""); loadingProgressBarFrameTexture = renderer.textureManager.LoadFromPng( "resources/loadingProgressBarFrame.png", "", true); loadingProgressBarTexture = renderer.textureManager.LoadFromPng( "resources/loadingProgressBar.png", "", true); float minDimension; float width = Environment::projectionWidth; float height = Environment::projectionHeight; if (width >= height) { minDimension = height; } else { minDimension = width; } loadingMesh.data = CreateRect2D({ 0.0f, 0.0f }, { minDimension * 0.5f, minDimension * 0.5f }, 3); loadingMesh.RefreshVBO(); #ifdef EMSCRIPTEN // Asynchronously download resources.zip; setupPart2() is called on completion. // The loading screen stays visible until the download finishes. s_instance = this; std::cout << "Load resurces step 1-" << std::endl; emscripten_async_wget2("resources.zip", "resources.zip", "GET", nullptr, nullptr, onResourcesZipLoaded, onResourcesZipError, onResourcesZipProgress); #else // Builds the loadSteps queue (cheap, no GL work); the actual loading // happens one step per frame in Game::update() so the window keeps // pumping events / repainting instead of freezing for the whole load. setupPart2(); #endif } void Game::setupPart2() { // Builds an ordered queue of small closures instead of doing the work here. // Game::update() drains one closure per frame, so the event loop / browser // paint gets a turn between steps instead of the window freezing for the // whole duration of loading. Building the queue itself is cheap (no GL calls). // Must happen before the location step below: some location scripts // (e.g. resources/start_dorm.lua) start a dialogue as soon as they're // loaded, so the language needs to be known before that first load. loadSteps.push([this]() { ZL::loadLanguageSetting(); }); loadSteps.push([this]() { #ifdef EMSCRIPTEN renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/defaultAtmosphere.vertex", "resources/shaders/defaultAtmosphere_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/planet_bake.vertex", "resources/shaders/planet_bake_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("spark", "resources/shaders/spark.vertex", "resources/shaders/spark_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("skinning", "resources/shaders/skinning.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog", "resources/shaders/fog.vertex", "resources/shaders/fog_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_skinning", "resources/shaders/fog_skinning.vertex", "resources/shaders/fog_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("darklands_fog", "resources/shaders/darklands_fog.vertex", "resources/shaders/darklands_fog_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("darklands_fog_skinning", "resources/shaders/darklands_fog_skinning.vertex", "resources/shaders/darklands_fog_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("darklands_flash", "resources/shaders/default.vertex", "resources/shaders/darklands_flash_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("cutsceneFade", "resources/shaders/default.vertex", "resources/shaders/cutscene_fade_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("cutsceneBlack", "resources/shaders/default.vertex", "resources/shaders/cutscene_black_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("shadow_depth", "resources/shaders/shadow_depth.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("shadow_depth_skinning", "resources/shaders/shadow_depth_skinning.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default_shadow", "resources/shaders/default_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_shadow", "resources/shaders/fog_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_skinning_shadow", "resources/shaders/fog_skinning_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog", "resources/shaders/night_fog.vertex", "resources/shaders/night_fog_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog_skinning", "resources/shaders/night_fog_skinning.vertex", "resources/shaders/night_fog_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog_shadow", "resources/shaders/night_fog_shadow.vertex", "resources/shaders/night_fog_shadow_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog_skinning_shadow", "resources/shaders/night_fog_skinning_shadow.vertex", "resources/shaders/night_fog_shadow_web.fragment", CONST_ZIP_FILE); #else renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/defaultAtmosphere.vertex", "resources/shaders/defaultAtmosphere_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/planet_bake.vertex", "resources/shaders/planet_bake_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("spark", "resources/shaders/spark.vertex", "resources/shaders/spark_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("skinning", "resources/shaders/skinning.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog", "resources/shaders/fog.vertex", "resources/shaders/fog_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_skinning", "resources/shaders/fog_skinning.vertex", "resources/shaders/fog_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("darklands_fog", "resources/shaders/darklands_fog.vertex", "resources/shaders/darklands_fog_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("darklands_fog_skinning", "resources/shaders/darklands_fog_skinning.vertex", "resources/shaders/darklands_fog_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("darklands_flash", "resources/shaders/default.vertex", "resources/shaders/darklands_flash_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("cutsceneFade", "resources/shaders/default.vertex", "resources/shaders/cutscene_fade_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("cutsceneBlack", "resources/shaders/default.vertex", "resources/shaders/cutscene_black_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("shadow_depth", "resources/shaders/shadow_depth.vertex", "resources/shaders/shadow_depth_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("shadow_depth_skinning", "resources/shaders/shadow_depth_skinning.vertex", "resources/shaders/shadow_depth_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default_shadow", "resources/shaders/default_shadow.vertex", "resources/shaders/default_shadow_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_shadow", "resources/shaders/fog_shadow.vertex", "resources/shaders/fog_shadow_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_skinning_shadow", "resources/shaders/fog_skinning_shadow.vertex", "resources/shaders/fog_shadow_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog", "resources/shaders/night_fog.vertex", "resources/shaders/night_fog_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog_skinning", "resources/shaders/night_fog_skinning.vertex", "resources/shaders/night_fog_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog_shadow", "resources/shaders/night_fog_shadow.vertex", "resources/shaders/night_fog_shadow_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("night_fog_skinning_shadow", "resources/shaders/night_fog_skinning_shadow.vertex", "resources/shaders/night_fog_shadow_desktop.fragment", CONST_ZIP_FILE); #endif }); loadSteps.push([this]() { renderer.textureManager.LoadFromPng("resources/dialogue/portrait_aiperi.png", CONST_ZIP_FILE); renderer.textureManager.LoadFromPng("resources/dialogue/portrait_ghost.png", CONST_ZIP_FILE); renderer.textureManager.LoadFromPng("resources/dialogue/portrait_hero_neutral.png", CONST_ZIP_FILE); renderer.textureManager.LoadFromPng("resources/dialogue/portrait_hero_think.png", CONST_ZIP_FILE); renderer.textureManager.LoadFromPng("resources/dialogue/portrait_phone.png", CONST_ZIP_FILE); renderer.textureManager.LoadFromPng("resources/dialogue/portrait_student_boy.png", CONST_ZIP_FILE); renderer.textureManager.LoadFromPng("resources/dialogue/portrait_student_girl.png", CONST_ZIP_FILE); renderer.textureManager.LoadFromPng("resources/dialogue/portrait_teacher.png", CONST_ZIP_FILE); }); loadSteps.push([this]() { ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE); Character::nameTranslations.loadFromFile("resources/config/translate_names.json"); }); // createLocations() is also used by the full-reset path (see performFullReset()), // so it stays a single synchronous function rather than being split into // per-location queue steps; it's queued here as one coarse step. loadSteps.push([this]() { createLocations(); }); loadSteps.push([this]() { // Wire inventory callbacks: tutorial tracking + toast notifications. gameState.inventory.onItemAdded = [this](const std::string& itemId) { menuManager.onItemPickedUp(itemId); const Item* item = ItemRegistry::instance().findById(itemId); if (item) menuManager.showToast(menuManager.toastTextureItemAdded, item->name); audioPlayer->playSoundAsync("audio/831642__ienba__generic-game-pick-up.ogg"); }; gameState.inventory.onItemRemoved = [this](const std::string& itemId) { const Item* item = ItemRegistry::instance().findById(itemId); if (item) menuManager.showToast(menuManager.toastTextureItemRemoved, item->name); audioPlayer->playSoundAsync("audio/831642__ienba__generic-game-pick-up.ogg"); }; // Wire phone dialogue start function so MenuManager can trigger dialogues. menuManager.startDialogueFunc = [this](const std::string& id) { if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id); }; menuManager.startDarklandsTransitionFunc = [this]() { startDarklandsTransition(); }; menuManager.startNightTransitionFunc = [this]() { startNightTransition(); }; menuManager.chatOpenCallback = [this](int chatIndex) { if (currentLocation()) currentLocation()->scriptEngine.callChatOpenCallback(chatIndex); }; menuManager.skipCutsceneFunc = [this]() { if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene(); }; }); loadSteps.push([this]() { try { menuManager.setup(gameState.inventory, CONST_ZIP_FILE); std::cout << "UI loaded successfully" << std::endl; } catch (const std::exception& e) { std::cerr << "Failed to load UI: " << e.what() << std::endl; } menuManager.startGameFunc = [this]() { mainThreadHandler.EnqueueMainThreadTask([this]() { performResetToInitialState(); gameState.currentLocationName = "location_dorm"; currentLocation()->scriptEngine.callLocationEnterCallback(); this->audioPlayer->crossFadeMusicAsync("audio/obshaga.ogg"); }); }; menuManager.onSaveGame = [this](int slot) { saveGame(slot); }; menuManager.onLoadGame = [this](int slot) { loadGame(slot); }; menuManager.getSlotInfoFunc = [this](int slot) { return readSlotInfo(slot); }; // Lets MenuManager run a heavy operation (e.g. reloadLocalizedGameContent()) // as a chunked sequence instead of one blocking call: the closures are // drained one per frame by update(), with the loading screen shown for // the duration, exactly like the initial resource load. menuManager.runAsLoadingSequence = [this](std::vector> steps) { for (auto& step : steps) loadSteps.push(std::move(step)); loadSteps.push([this]() { loadingCompleted = true; }); loadStepsTotal = loadSteps.size(); loadStepsDone = 0; showLoadingProgressBar = false; loadingCompleted = false; }; }); loadSteps.push([this]() { if (audioPlayer->init()) { audioPlayer->setMusicVolume(100); audioPlayer->setSoundVolume(80); std::cout << "Audio initialized successfully" << std::endl; } else { std::cout << "Audio initialization failed" << std::endl; } }); loadSteps.push([this]() { menuManager.loadSettings(); audioPlayer->crossFadeMusicAsync("audio/main menu final.ogg"); }); loadSteps.push([this]() { loadingCompleted = true; }); loadStepsTotal = loadSteps.size(); loadStepsDone = 0; } void Game::rearmDormOneShotCallbacks() { auto it = gameState.locations.find("location_dorm"); if (it == gameState.locations.end() || !it->second) return; it->second->onPlayerTaxiRequired = [this]() { menuManager.tutorialShowTaxiHint(); }; menuManager.tutorialUnlockInteractiveObjectsFunc = [this]() { if (gameState.locations["location_dorm"]) { gameState.locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = false; } }; } void Game::performFullReset() { gameState.currentLocationName.clear(); gameState.isDarklands = false; gameState.isNight = false; gameState.isDawn = false; gameState.playerHp = 200.f; gameState.playerMaxHp = 200.f; gameState.money = 5500; gameState.globalInts.clear(); gameState.globalFloats.clear(); gameState.tutorialStep = TutorialStep::Step0; gameState.uniIntTutorialState = UniIntTutorialState::Step10; gameState.tutorialPhonePickedUp = false; gameState.tutorialJournalPickedUp = false; gameState.tutorialPhoneChatScreenOpened = false; gameState.tutorialJournalScreenOpened = false; gameState.tutorialNeedOpenTaxiScreen = false; for (int i = 0; i < 3; ++i) { gameState.chatUnread[i] = true; gameState.chatPreviewMsg[i].clear(); gameState.chatHistory[i].clear(); } gameState.inventory.clear(); gameState.questJournal.loadFromFile(localizedConfigPath("resources/config/quests.json", g_currentLanguage), CONST_ZIP_FILE); createLocations(); // Developer workflow: uncomment to (re)generate resources/config/start_state.json // from this freshly-rebuilt state after changing gameplay/content JSON or Lua. // Re-comment before shipping. //saveInitialState(); } void Game::performResetToInitialState() { const std::string path = "resources/config/start_state.json"; const std::string content = ZL::readTextFile(path); if (!content.empty()) { try { nlohmann::json root = nlohmann::json::parse(content); // GameState::load() merges into existing maps rather than replacing them, // so any Lua globals set during play that aren't in the pristine snapshot // must be cleared explicitly before loading it. gameState.globalInts.clear(); gameState.globalFloats.clear(); gameState.load(root); // updates existing Location objects in place -- no // destruction, no re-parsing JSON/nav files, no new // sol2 state (GameState::load only touches map entries // that already exist). // Locations survive this reset, so any one-shot callbacks they consumed // during the previous playthrough (tutorial unlock, taxi request) must be // re-armed here -- createLocations() normally does this, but doesn't run // on this fast path. rearmDormOneShotCallbacks(); return; } catch (const std::exception& e) { std::cerr << "[reset] Failed to parse " << path << ": " << e.what() << " -- falling back to full rebuild" << std::endl; } } performFullReset(); } void Game::saveInitialState() { nlohmann::json root; gameState.save(root); const std::string path = "resources/config/start_state.json"; std::ofstream file(path); if (file.is_open()) { file << root.dump(2); std::cout << "[reset] Saved initial state snapshot to " << path << std::endl; } else { std::cerr << "[reset] Could not open " << path << " for writing" << std::endl; } } void Game::createLocations() { gameState.locations.clear(); gameState.globalFloats["player_hp"] = 200; LocationSetup uniInteriorParams; uniInteriorParams.gameObjectsJsonPath = "resources/config/gameobjects_uni_interior_x.json"; uniInteriorParams.npcsJsonPath = "resources/config/npcs_uni_interior.json"; uniInteriorParams.dialoguesJsonPath = "resources/dialogue/uni_interior_dialogues_009.json"; uniInteriorParams.navigationJsonPaths = { "resources/navigation/uni_interior4_unlocked_n2.txt", //0 "resources/navigation/uni_interior3_n2_hall.txt", "resources/navigation/uni_interior4_n2_lr_hall.txt", "resources/navigation/uni_interior4_n2_lr_tr_hall.txt", "resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.txt", "resources/navigation/uni_interior3_darklands_all_open.txt", //5 "resources/navigation/uni_interior4_unlocked_hall.txt", "resources/navigation/uni_interior4_unlocked_n3.txt", "resources/navigation/uni_interior4_unlocked_s1.txt", "resources/navigation/uni_interior4_unlocked_s2.txt", "resources/navigation/uni_interior4_unlocked_s3.txt", "resources/navigation/uni_interior4_unlocked_s3_s1.txt", "resources/navigation/uni_interior4_unlocked_s3_hall.txt", "resources/navigation/uni_interior4_unlocked_s3_n3.txt", "resources/navigation/uni_interior4_unlocked_s3_n2.txt", "resources/navigation/uni_interior4_unlocked_s3_s2.txt", "resources/navigation/uni_interior4_locked.txt" //16 }; /* uniInteriorParams.navigationJsonPaths = { "resources/navigation/uni_interior4_unlocked_n2.json", //0 "resources/navigation/uni_interior3_n2_hall.json", "resources/navigation/uni_interior4_n2_lr_hall.json", "resources/navigation/uni_interior4_n2_lr_tr_hall.json", "resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.json", "resources/navigation/uni_interior3_darklands_all_open.json", //5 "resources/navigation/uni_interior4_unlocked_hall.json", "resources/navigation/uni_interior4_unlocked_n3.json", "resources/navigation/uni_interior4_unlocked_s1.json", "resources/navigation/uni_interior4_unlocked_s2.json", "resources/navigation/uni_interior4_unlocked_s3.json", "resources/navigation/uni_interior4_unlocked_s3_s1.json", //11 "resources/navigation/uni_interior4_unlocked_s3_hall.json", "resources/navigation/uni_interior4_unlocked_s3_n3.json", "resources/navigation/uni_interior4_unlocked_s3_n2.json", "resources/navigation/uni_interior4_unlocked_s3_s2.json", "resources/navigation/uni_interior4_locked.json" //16 };*/ /* uniInteriorParams.navigationJsonPaths = { "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", //6 "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json", "resources/navigation/uni_interior3_darklands_all_open.json" };*/ uniInteriorParams.teleportsJsonPath = "resources/config/teleports_uni_interior.json"; uniInteriorParams.triggerZonesJsonPath = "resources/config/trigger_zones_uni_interior.json"; uniInteriorParams.lightsJsonPath = "resources/config/lights_uni_interior.json"; uniInteriorParams.scriptPath = "resources/start_uni_interior.lua"; uniInteriorParams.interactiveObjectsJsonPath = "resources/config/interactive_objects_uni_interior_x.json"; uniInteriorParams.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104); gameState.locations["uni_interior"] = std::make_shared(renderer, gameState.inventory); gameState.locations["uni_interior"]->setup(uniInteriorParams, &gameState.questJournal, audioPlayer.get()); gameState.locations["uni_interior"]->scriptEngine.setGlobalStore(&gameState.globalInts); gameState.locations["uni_interior"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats); gameState.locations["uni_interior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { gameState.isNight = isNight; gameState.isDawn = isDawn; }; gameState.locations["uni_interior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; gameState.locations["uni_interior"]->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); }; gameState.locations["uni_interior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); }; gameState.locations["uni_interior"]->requestReturnToMainMenu = [this]() { gameState.currentLocationName.clear(); menuManager.showMainMenu(); }; gameState.locations["uni_interior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { menuManager.setChatUnread(idx, unread, msg); }; if (gameState.locations["uni_interior"]->player) gameState.locations["uni_interior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); }; for (auto& npc : gameState.locations["uni_interior"]->npcs) { if (npc && npc->state.canAttack) { npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); }; } } gameState.locations["uni_interior"]->requestDarklandsPlayBattleMusic = [this]() { audioPlayer->crossFadeMusicFromPositionAsync("audio/bishkek fight.ogg"); }; gameState.locations["uni_interior"]->requestDarklandsPlayNormalMusic = [this]() { audioPlayer->crossFadeMusicFromPositionAsync("audio/bishkek fight calm.ogg"); }; LocationSetup uniExteriorParams = uniInteriorParams; uniExteriorParams.gameObjectsJsonPath = "resources/config/gameobjects_uni_exterior_x.json"; uniExteriorParams.interactiveObjectsJsonPath = "resources/config/interactive_objects_uni_exterior_x.json"; //uniExteriorParams.navigationJsonPaths = {"resources/navigation/uni_exterior3_all.json"}; /* uniExteriorParams.navigationJsonPaths = { "resources/navigation/uni_exterior3_trees_car.json", "resources/navigation/uni_exterior3_trees.json", };*/ uniExteriorParams.navigationJsonPaths = { "resources/navigation/uni_exterior3_trees_car.txt", "resources/navigation/uni_exterior3_trees.txt", }; uniExteriorParams.teleportsJsonPath = "resources/config/teleports_uni_exterior.json"; uniExteriorParams.triggerZonesJsonPath = "resources/config/trigger_zones_uni_exterior.json"; uniExteriorParams.lightsJsonPath = "resources/config/lights_uni_exterior.json"; uniExteriorParams.scriptPath = "resources/start_uni_exterior.lua"; uniExteriorParams.playerPosition = Eigen::Vector3f(5, 0, -18.4); uniExteriorParams.npcsJsonPath = "resources/config/npcs_uni_exterior.json"; uniExteriorParams.dialoguesJsonPath = "resources/dialogue/uni_exterior_dialogues002.json"; gameState.locations["uni_exterior"] = std::make_shared(renderer, gameState.inventory); gameState.locations["uni_exterior"]->setup(uniExteriorParams, &gameState.questJournal, audioPlayer.get()); gameState.locations["uni_exterior"]->scriptEngine.setGlobalStore(&gameState.globalInts); gameState.locations["uni_exterior"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats); gameState.locations["uni_exterior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { gameState.isNight = isNight; gameState.isDawn = isDawn; }; gameState.locations["uni_exterior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; gameState.locations["uni_exterior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); }; gameState.locations["uni_exterior"]->requestReturnToMainMenu = [this]() { menuManager.showMainMenu(); }; gameState.locations["uni_exterior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { menuManager.setChatUnread(idx, unread, msg); }; if (gameState.locations["uni_exterior"]->player) gameState.locations["uni_exterior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); }; LocationSetup params_dorm; //params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm.json"; //params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm_trees001.json"; params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm_new_x.json"; params_dorm.npcsJsonPath = "resources/config/npcs_dorm.json"; params_dorm.dialoguesJsonPath = "resources/dialogue/dorm_dialogues003.json"; /* params_dorm.navigationJsonPaths = { "resources/navigation/dorm3_bca.json", "resources/navigation/dorm3_ca.json", "resources/navigation/dorm3_ba.json", "resources/navigation/dorm3_a.json", "resources/navigation/dorm3_b.json", "resources/navigation/dorm3_all_open.json", };*/ params_dorm.navigationJsonPaths = { "resources/navigation/dorm3_bca.txt", //0 "resources/navigation/dorm3_ca.txt", //1 "resources/navigation/dorm3_ba.txt", "resources/navigation/dorm3_a.txt", "resources/navigation/dorm3_b.txt", "resources/navigation/dorm3_all_open.txt", //5 }; /* params_dorm.navigationJsonPaths = { "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", };*/ params_dorm.teleportsJsonPath = "resources/config/teleports_dorm.json"; params_dorm.triggerZonesJsonPath = "resources/config/trigger_zones_dorm.json"; params_dorm.lightsJsonPath = "resources/config/lights_dorm.json"; params_dorm.scriptPath = "resources/start_dorm.lua"; params_dorm.interactiveObjectsJsonPath = "resources/config/interactive_objects_dorm_x.json"; params_dorm.playerPosition = Eigen::Vector3f(6.76345, 0, -14.6022); gameState.locations["location_dorm"] = std::make_shared(renderer, gameState.inventory); gameState.locations["location_dorm"]->setup(params_dorm, &gameState.questJournal, audioPlayer.get()); gameState.locations["location_dorm"]->scriptEngine.setGlobalStore(&gameState.globalInts); gameState.locations["location_dorm"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats); gameState.locations["location_dorm"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { gameState.isNight = isNight; gameState.isDawn = isDawn; }; gameState.locations["location_dorm"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; gameState.locations["location_dorm"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); }; gameState.locations["location_dorm"]->requestReturnToMainMenu = [this]() { menuManager.showMainMenu(); }; gameState.locations["location_dorm"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { menuManager.setChatUnread(idx, unread, msg); }; if (gameState.locations["location_dorm"]->player) gameState.locations["location_dorm"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); }; gameState.locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = true; rearmDormOneShotCallbacks(); menuManager.callTaxiFunc = [this]() { if (gameState.locations["location_dorm"]) { gameState.locations["location_dorm"]->onPlayerTaxiRequired = nullptr; } if (currentLocation()) { currentLocation()->scriptEngine.callCallTaxiCallback(); } }; // Teleport callbacks: destination name and position come from the teleport zone data. auto teleportCallback = [this](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) { std::cout << "[TELEPORT] " << " -> " << destName << std::endl; auto it = gameState.locations.find(destName); if (it == gameState.locations.end()) { std::cerr << "[TELEPORT] Unknown destination location: " << destName << std::endl; return; } if (currentLocation()) { currentLocation()->scriptEngine.callLocationExitCallback(); currentLocation()->dialogueSystem.stopDialogue(); } gameState.currentLocationName = destName; Location* loc = currentLocation(); if (loc->player) { loc->player->state.position = destPos; loc->player->setTarget(destPos); loc->player->stopInPlace(); loc->player->state.facingAngle = destRotY; loc->player->state.targetFacingAngle = destRotY; } loc->state.cameraAzimuth = destRotY; loc->state.isDarklands = gameState.isDarklands; loc->state.isNight = gameState.isNight; loc->state.isDawn = gameState.isDawn; loc->scriptEngine.callLocationEnterCallback(); menuManager.onLocationChanged(destName); }; gameState.locations["uni_exterior"]->onTeleport = teleportCallback; gameState.locations["uni_interior"]->onTeleport = teleportCallback; gameState.locations["location_dorm"]->onTeleport = teleportCallback; // Share the global int store with all dialogue runtimes so flags set via // dialogue JSON and via Lua set_dialogue_flag all see the same state. for (auto& [name, loc] : gameState.locations) { loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts); } // Wire tutorial advance callbacks for all locations. // advanceTutorialStep() guards against double-advancing, so sharing is safe. for (auto& [name, loc] : gameState.locations) { loc->dialogueSystem.setOnDialogueAdvanced([this]() { menuManager.advanceTutorialStep(); }); loc->onPlayerFloorWalk = [this]() { if (gameState.tutorialStep == TutorialStep::Step2) { menuManager.advanceTutorialStep(); } menuManager.onPlayerStartedWalking(); }; } // Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown. for (auto& [name, loc] : gameState.locations) { loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) { menuManager.onChatBubbleReady(text, incoming); }); } // Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends. for (auto& [name, loc] : gameState.locations) { loc->dialogueSystem.setOnCutsceneStarted([this]() { menuManager.onCutsceneStarted(); }); loc->dialogueSystem.setOnCutsceneFinishedExtra([this](const std::string&) { menuManager.onCutsceneFinished(); }); } // Wire HP-change callbacks so all player instances update the health bar HUD. for (auto& [name, loc] : gameState.locations) { if (loc->player) { loc->player->state.onHpChanged = [this](float hp, float maxHp) { menuManager.updateHealthBar(hp, maxHp); }; } } } void Game::drawUI() { glClear(GL_DEPTH_BUFFER_BIT); glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); glEnable(GL_BLEND); if (!menuManager.cutsceneHudActive_) menuManager.uiManager.draw(renderer); menuManager.drawChatFbo(renderer); if (currentLocation()) { currentLocation()->dialogueSystem.draw(renderer); } glEnable(GL_BLEND); menuManager.topUiManager.draw(renderer); glDisable(GL_BLEND); renderer.shaderManager.PopShader(); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); CheckGlError(__FILE__, __LINE__); } void Game::drawScene() { glViewport(0, 0, Environment::width, Environment::height); if (!loadingCompleted) { drawLoading(); } /*else if (Environment::width < Environment::height) { drawMobilePortraitOverlay(); }*/ else { Location* loc = currentLocation(); if (loc) { // Sync global flags so Location's draw functions see them. loc->state.isDarklands = gameState.isDarklands; loc->state.isNight = gameState.isNight; loc->state.isDawn = gameState.isDawn; if (gameState.isDarklands) { loc->drawGameDarklands(); CheckGlError(__FILE__, __LINE__); } else if (gameState.isNight) { loc->drawGameNight(); CheckGlError(__FILE__, __LINE__); } else if (loc->shadowMap) { CheckGlError(__FILE__, __LINE__); loc->drawShadowDepthPass(); CheckGlError(__FILE__, __LINE__); loc->drawGameWithShadows(); CheckGlError(__FILE__, __LINE__); } else { loc->drawGame(); CheckGlError(__FILE__, __LINE__); } } drawUI(); drawDarklandsFlash(); } CheckGlError(__FILE__, __LINE__); } void Game::drawLoading() { glClear(GL_DEPTH_BUFFER_BIT); renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1f("uAlpha", 1.0); float width = Environment::projectionWidth; float height = Environment::projectionHeight; renderer.PushProjectionMatrix( -width * 0.5f, width * 0.5f, -height * 0.5f, height * 0.5f, -10, 10); renderer.PushMatrix(); renderer.LoadIdentity(); glBindTexture(GL_TEXTURE_2D, loadingTexture->getTexID()); renderer.DrawVertexRenderStruct(loadingMesh); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); if (showLoadingProgressBar && loadingProgressBarFrameTexture && loadingProgressBarTexture) { const float W = Environment::projectionWidth; const float H = Environment::projectionHeight; static constexpr float kBarW = 400.0f; static constexpr float kBarH = 40.0f; const float barCenterY = -(H * 0.4f - 60.0f); const float barLeft = -kBarW * 0.5f; const float barBottom = barCenterY - kBarH * 0.5f; const float barTop = barCenterY + kBarH * 0.5f; // Rebuild frame mesh when projection dimensions change if (W != loadingBarLastW || H != loadingBarLastH) { loadingBarLastW = W; loadingBarLastH = H; VertexDataStruct frameData; frameData.PositionData = { {barLeft, barBottom, 4.f}, {barLeft, barTop, 4.f}, {barLeft+kBarW, barTop, 4.f}, {barLeft+kBarW, barTop, 4.f}, {barLeft+kBarW, barBottom, 4.f}, {barLeft, barBottom, 4.f} }; frameData.TexCoordData = { {0.f,0.f},{0.f,1.f},{1.f,1.f}, {1.f,1.f},{1.f,0.f},{0.f,0.f} }; loadingProgressBarFrameMesh.data = std::move(frameData); loadingProgressBarFrameMesh.RefreshVBO(); loadingBarLastRenderedProgress = -1.0f; // force fill rebuild too } // Rebuild fill mesh when progress changes const float p = (std::min)(currentLoadingProgress(), 1.0f); if (p != loadingBarLastRenderedProgress) { loadingBarLastRenderedProgress = p; if (p > 0.001f) { const float fillRight = barLeft + kBarW * p; VertexDataStruct fillData; fillData.PositionData = { {barLeft, barBottom, 4.f}, {barLeft, barTop, 4.f}, {fillRight, barTop, 4.f}, {fillRight, barTop, 4.f}, {fillRight, barBottom, 4.f}, {barLeft, barBottom, 4.f} }; fillData.TexCoordData = { {0.f,0.f},{0.f,1.f},{p,1.f}, {p,1.f},{p,0.f},{0.f,0.f} }; loadingProgressBarFillMesh.data = std::move(fillData); loadingProgressBarFillMesh.RefreshVBO(); } } renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1f("uAlpha", 1.0f); renderer.PushProjectionMatrix(-W*0.5f, W*0.5f, -H*0.5f, H*0.5f, -10, 10); renderer.PushMatrix(); renderer.LoadIdentity(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBindTexture(GL_TEXTURE_2D, loadingProgressBarFrameTexture->getTexID()); renderer.DrawVertexRenderStruct(loadingProgressBarFrameMesh); if (p > 0.001f) { glBindTexture(GL_TEXTURE_2D, loadingProgressBarTexture->getTexID()); renderer.DrawVertexRenderStruct(loadingProgressBarFillMesh); } renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } CheckGlError(__FILE__, __LINE__); } int64_t Game::getSyncTimeMs() { int64_t localNow = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); return localNow; } void Game::processTickCount() { if (lastTickCount == 0) { lastTickCount = getSyncTimeMs(); lastTickCount = (lastTickCount / 50) * 50; return; } newTickCount = getSyncTimeMs(); newTickCount = (newTickCount / 50) * 50; if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) { int64_t delta = newTickCount - lastTickCount; lastTickCount = newTickCount; updateDarklandsFlash(delta); menuManager.uiManager.update(static_cast(delta)); menuManager.topUiManager.update(static_cast(delta)); if (!menuManager.isMainMenuOpen()) menuManager.update(static_cast(delta)); if (currentLocation()) { currentLocation()->update(delta); } } } void Game::render() { ZL::CheckGlError(__FILE__, __LINE__); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); drawScene(); processTickCount(); SDL_GL_SwapWindow(ZL::Environment::window); } void Game::update() { SDL_Event event; const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height); while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { Environment::exitGameLoop = true; } if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) { // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях const bool wasPortrait = Environment::width < Environment::height; Environment::width = event.window.data1; Environment::height = event.window.data2; Environment::computeProjectionDimensions(); const bool isPortrait = Environment::width < Environment::height; menuManager.uiManager.updateAllLayouts(); menuManager.topUiManager.updateAllLayouts(); if (wasPortrait != isPortrait) { menuManager.onOrientationChanged(); } std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; //space.clearTextRendererCache(); } // Suppress all input while portrait overlay is shown //if (mobileIsPortrait) continue; // Ignore pointer input until the initial loading screen has finished. // Otherwise a click on the loading screen (e.g. at the spot where a // main-menu button will later be) gets delivered to the menu UI as // soon as it appears underneath, triggering that button unintentionally. if (!loadingCompleted && (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERUP || event.type == SDL_FINGERMOTION || event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP)) { continue; } #ifdef __ANDROID__ if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) { Environment::exitGameLoop = true; } #endif // Touch events (real fingers on Android and on mobile browsers via Emscripten). // SDL_HINT_TOUCH_MOUSE_EVENTS="0" is set in main.cpp so these do not // also fire SDL_MOUSEBUTTONDOWN, preventing double-processing. if (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERUP || event.type == SDL_FINGERMOTION) { int eventX = static_cast(event.tfinger.x * Environment::width); int eventY = static_cast(event.tfinger.y * Environment::height); int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); int64_t fingerId = static_cast(event.tfinger.fingerId); if (event.type == SDL_FINGERDOWN) { onPointerDown(fingerId, eventX, eventY, mx, my); } else if (event.type == SDL_FINGERUP) { onPointerUp(fingerId, eventX, eventY, mx, my); } else { onPointerMotion(fingerId, eventX, eventY, mx, my); } continue; } // Mouse-left maps to a single virtual touch with MOUSE_FINGER_ID. // Right mouse is intentionally unused now — camera rotation is on hold-and-drag. if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { if (event.button.button == SDL_BUTTON_LEFT) { int eventX = event.button.x; int eventY = event.button.y; int mx = static_cast((float)eventX / Environment::width * Environment::projectionWidth); int my = static_cast((float)eventY / Environment::height * Environment::projectionHeight); if (event.type == SDL_MOUSEBUTTONDOWN) { onPointerDown(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my); } else { onPointerUp(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my); } } else if (event.button.button == SDL_BUTTON_RIGHT && event.type == SDL_MOUSEBUTTONUP && editorMode == EditorMode::Navigation && currentLocation()) { currentLocation()->editor.handleRightClick(); } } else if (event.type == SDL_MOUSEMOTION) { int eventX = event.motion.x; int eventY = event.motion.y; int mx = static_cast((float)eventX / Environment::width * Environment::projectionWidth); int my = static_cast((float)eventY / Environment::height * Environment::projectionHeight); onPointerMotion(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my); } if (event.type == SDL_MOUSEWHEEL) { static const float zoomstep = 2.0f; if (event.wheel.y > 0) { Environment::zoom -= zoomstep; } else if (event.wheel.y < 0) { Environment::zoom += zoomstep; } if (Environment::zoom < zoomMin) { Environment::zoom = zoomMin; } if (Environment::zoom > zoomMax) { Environment::zoom = zoomMax; } std::cout << "Current zoom: " << Environment::zoom << std::endl; // Tutorial step3 → step4: any mouse-wheel scroll counts as "zoom gesture". if (gameState.tutorialStep == TutorialStep::Step3) { menuManager.advanceTutorialStep(); } } if (event.type == SDL_KEYDOWN && event.key.repeat == 0) { switch (event.key.keysym.sym) { case SDLK_8: if (editorMode == EditorMode::InteractiveObjects && currentLocation()) { currentLocation()->editor.selectInteractiveObject(8); } else { gameState.isDawn = !gameState.isDawn; if (gameState.isDawn) gameState.isNight = true; } break; case SDLK_9: if (editorMode == EditorMode::InteractiveObjects && currentLocation()) { currentLocation()->editor.selectInteractiveObject(9); } else { startNightTransition(); } break; case SDLK_0: if (editorMode == EditorMode::InteractiveObjects && currentLocation()) { currentLocation()->editor.selectInteractiveObject(event.key.keysym.sym - SDLK_0); } else { currentLocation()->requestDarklandsTransition(); } break; case SDLK_1: case SDLK_2: case SDLK_3: case SDLK_4: case SDLK_5: case SDLK_6: case SDLK_7: if (editorMode == EditorMode::InteractiveObjects && currentLocation()) { currentLocation()->editor.selectInteractiveObject(event.key.keysym.sym - SDLK_0); } else { currentLocation()->switchNavigation(event.key.keysym.sym - SDLK_0); std::cout << "Switched to nav mesh " << (event.key.keysym.sym - SDLK_0) << std::endl; } break; case SDLK_f: currentLocation()->dialogueSystem.startDialogue("phone_night_aiperi001"); break; case SDLK_e: currentLocation()->dialogueSystem.startCutscene("computer_cutscene001"); //.startDialogue("test_cutscene_pan_dialogue"); break; case SDLK_n: if (editorMode == EditorMode::None) editorMode = EditorMode::Navigation; else if (editorMode == EditorMode::Navigation) editorMode = EditorMode::InteractiveObjects; else editorMode = EditorMode::None; if (currentLocation()) { currentLocation()->editorMode = editorMode; if (editorMode == EditorMode::Navigation) currentLocation()->editor.buildNavMeshes(); else if (editorMode == EditorMode::InteractiveObjects) currentLocation()->editor.buildInteractiveObjectBoundsMeshes(); } { const char* modeName = (editorMode == EditorMode::Navigation) ? "Navigation" : (editorMode == EditorMode::InteractiveObjects) ? "InteractiveObjects" : "None"; std::cout << "[EDITOR] Mode: " << modeName << std::endl; } break; case SDLK_o: x = x + 1; std::cout << "current x: " << x << std::endl; //y = y + 0.002; //currentLocation->player->hp = 200; //currentLocation->npcs[0]->walkSpeed += 0.01f; //std::cout << "Walk speed: " << currentLocation->npcs[0]->walkSpeed << std::endl; break; case SDLK_k: x = x - 1; std::cout << "current x: " << x << std::endl; std::cout << "Player pos: " << currentLocation()->player->state.position.transpose() << std::endl; std::cout << "Player rotation: " << currentLocation()->player->state.facingAngle*180.0/M_PI << std::endl; //currentLocation->npcs[0]->walkSpeed -= 0.01f; //std::cout << "Walk speed: " << currentLocation->npcs[0]->walkSpeed << std::endl; break; case SDLK_p: gameState.currentLocationName = "uni_interior"; currentLocation()->player->state.position = Eigen::Vector3f(-0.0189243, 0, -13.4314); currentLocation()->player->setTarget(currentLocation()->player->state.position); //std::cout << "Switched to location " << ((currentLocation == locations["location1"]) ? "1" : "2") << std::endl; break; case SDLK_l: //x = x - 1; //std::cout << "current x: " << x << std::endl; std::cout << "Azimuth: " << currentLocation()->state.cameraAzimuth << std::endl; std::cout << "Inclination: " << currentLocation()->state.cameraInclination << std::endl; break; case SDLK_c: std::cout << "SLOW-MO activated!" << std::endl; activateSlowMoEffect(); break; case SDLK_i: y = y - 1; std::cout << "current y: " << y << std::endl; break; case SDLK_b: if (editorMode != EditorMode::None && currentLocation()) { currentLocation()->editor.saveAll(); } break; case SDLK_j: /* if (editorMode != EditorMode::None && currentLocation()) { currentLocation()->editor.placeTree(); } else { menuManager.toggleQuestJournal(); }*/ currentLocation()->editor.placeTree(); break; case SDLK_v: if (editorMode != EditorMode::None && currentLocation()) { currentLocation()->editor.saveObjects(); } break; case SDLK_RETURN: default: break; } } // Обработка ввода текста if (event.type == SDL_KEYDOWN) { if (event.key.keysym.sym == SDLK_BACKSPACE) { //menuManager.uiManager.onKeyBackspace(); } } if (event.type == SDL_TEXTINPUT) { // Пропускаем ctrl+c и другие команды if ((event.text.text[0] & 0x80) == 0) { // ASCII символы //menuManager.uiManager.onKeyPress((unsigned char)event.text.text[0]); } } if (event.type == SDL_KEYUP) { if (event.key.keysym.sym == SDLK_r) { std::cout << "Camera position: x=" << x << " y=" << y << " z=" << z << std::endl; } } if (event.type == SDL_KEYUP) { } } // Drain one loading step per frame so the window/tab keeps pumping events // and repainting between steps instead of freezing for the whole load. if (!loadingCompleted && !loadSteps.empty()) { loadSteps.front()(); loadSteps.pop(); loadStepsDone++; } render(); mainThreadHandler.processMainThreadTasks(); } int Game::countNonUiPointers() const { int n = 0; for (const auto& kv : activePointers) { if (!kv.second.capturedByUi) ++n; } return n; } void Game::activateSlowMoEffect() { if (!currentLocation()) return; if (currentLocation()->player) { currentLocation()->player->state.slowMoTimeRemaining = currentLocation()->player->state.slowMoActiveTime; } for (auto& npc : currentLocation()->npcs) { if (npc) { npc->state.slowMoTimeRemaining = npc->state.slowMoActiveTime; } } } void Game::enterCameraDragMode(int eventX, int eventY) { cameraDragging = true; if (currentLocation()) { currentLocation()->cameraDragging = true; // Anchor on the *current* position, not the original press, so the // camera doesn't snap by however far the finger drifted before the // movement threshold was crossed. currentLocation()->lastMouseX = eventX; currentLocation()->lastMouseY = eventY; // Snapshot current angles so we can measure how far the user rotates. dragStartAzimuth = currentLocation()->state.cameraAzimuth; dragStartInclination = currentLocation()->state.cameraInclination; } } void Game::exitCameraDragMode() { cameraDragging = false; if (currentLocation()) { currentLocation()->cameraDragging = false; } } void Game::startPinch() { // Pick the first two non-UI pointers as pinch anchors. bool foundA = false, foundB = false; for (auto& kv : activePointers) { if (kv.second.capturedByUi) continue; if (!foundA) { pinchFingerA = kv.first; foundA = true; } else if (!foundB) { pinchFingerB = kv.first; foundB = true; break; } } if (!foundA || !foundB) return; const PointerState& a = activePointers[pinchFingerA]; const PointerState& b = activePointers[pinchFingerB]; float dx = static_cast(a.eventX - b.eventX); float dy = static_cast(a.eventY - b.eventY); pinchStartDistance = std::sqrt(dx * dx + dy * dy); pinchStartZoom = Environment::zoom; pinchActive = true; } void Game::updatePinchZoom() { auto itA = activePointers.find(pinchFingerA); auto itB = activePointers.find(pinchFingerB); if (itA == activePointers.end() || itB == activePointers.end()) return; if (pinchStartDistance <= 1.0f) return; float dx = static_cast(itA->second.eventX - itB->second.eventX); float dy = static_cast(itA->second.eventY - itB->second.eventY); float dist = std::sqrt(dx * dx + dy * dy); if (dist <= 1.0f) return; float newZoom = pinchStartZoom * (pinchStartDistance / dist); // Match the wheel-zoom lower bound so both gestures clamp the same way. if (newZoom < zoomMin) newZoom = zoomMin; if (newZoom > zoomMax) newZoom = zoomMax; Environment::zoom = newZoom; // Tutorial step3 → step4: detect a significant pinch-zoom (≥ 2 zoom units). if (gameState.tutorialStep == TutorialStep::Step3) { if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) { menuManager.advanceTutorialStep(); } } } void Game::endPinch() { pinchActive = false; pinchStartDistance = 0.0f; pinchStartZoom = 0.0f; } void Game::onPointerDown(int64_t fingerId, int eventX, int eventY, int mx, int my) { PointerState st; st.eventX = st.downEventX = eventX; st.eventY = st.downEventY = eventY; st.mx = st.downMx = mx; st.my = st.downMy = my; const int uiX = mx; const int uiY = Environment::projectionHeight - my; menuManager.topUiManager.onTouchDown(fingerId, uiX, uiY); const bool capturedByTopUi = menuManager.topUiManager.isUiInteractionForFinger(fingerId); const bool dialogueActive = currentLocation() && currentLocation()->dialogueSystem.isActive(); if (!dialogueActive && !capturedByTopUi) { menuManager.uiManager.onTouchDown(fingerId, uiX, uiY); } st.capturedByUi = capturedByTopUi || (!dialogueActive && menuManager.uiManager.isUiInteractionForFinger(fingerId)); activePointers[fingerId] = st; if (st.capturedByUi) { // UI button / slider / text-field grabbed this press; don't drive // gameplay or pinch from it. return; } if (countNonUiPointers() >= 2) { // Second finger landed on the gameplay area: switch to pinch-zoom // and abandon any in-flight tap/camera-drag. if (cameraDragging) { exitCameraDragMode(); } hasPrimaryPointer = false; if (!pinchActive) { startPinch(); } } else { hasPrimaryPointer = true; primaryPointerId = fingerId; } } void Game::onPointerUp(int64_t fingerId, int eventX, int eventY, int mx, int my) { const int uiX = mx; const int uiY = Environment::projectionHeight - my; menuManager.topUiManager.onTouchUp(fingerId, uiX, uiY); const bool dialogueActive = currentLocation() && currentLocation()->dialogueSystem.isActive(); if (!dialogueActive) { menuManager.uiManager.onTouchUp(fingerId, uiX, uiY); } auto it = activePointers.find(fingerId); if (it == activePointers.end()) return; PointerState st = it->second; activePointers.erase(it); // Pinch ends as soon as either anchor is lifted. The remaining finger, // if any, is *not* promoted back to a primary tap — the user was mid- // gesture, not starting a fresh press. if (pinchActive && (fingerId == pinchFingerA || fingerId == pinchFingerB)) { endPinch(); return; } if (!hasPrimaryPointer || fingerId != primaryPointerId) { return; } const bool wasDragging = cameraDragging; if (cameraDragging) { exitCameraDragMode(); } hasPrimaryPointer = false; if (st.capturedByUi || wasDragging) { // UI handled the press, or it was a camera-rotation drag — no tap action. return; } // Tap → walk-to / interact, using the original press coords so the target // isn't shifted by tiny finger drift before release. if (currentLocation()) { currentLocation()->handleDown(fingerId, st.downEventX, st.downEventY, st.downMx, st.downMy); } } void Game::onPointerMotion(int64_t fingerId, int eventX, int eventY, int mx, int my) { const int uiX = mx; const int uiY = Environment::projectionHeight - my; menuManager.topUiManager.onTouchMove(fingerId, uiX, uiY); const bool dialogueActive = currentLocation() && currentLocation()->dialogueSystem.isActive(); if (!dialogueActive) { menuManager.uiManager.onTouchMove(fingerId, uiX, uiY); } auto it = activePointers.find(fingerId); if (it != activePointers.end()) { it->second.eventX = eventX; it->second.eventY = eventY; it->second.mx = mx; it->second.my = my; } if (pinchActive) { updatePinchZoom(); return; } // Only the primary, non-UI-captured pointer can promote itself into a // camera-rotation drag once it crosses the movement threshold. if (hasPrimaryPointer && fingerId == primaryPointerId && it != activePointers.end() && !it->second.capturedByUi && !cameraDragging) { int dx = mx - it->second.downMx; int dy = my - it->second.downMy; if (dx * dx + dy * dy >= CAMERA_DRAG_PIXEL_THRESHOLD * CAMERA_DRAG_PIXEL_THRESHOLD) { enterCameraDragMode(eventX, eventY); } } if (currentLocation()) { // Forwarded for dialogue hover and (when cameraDragging) camera rotation. currentLocation()->handleMotion(fingerId, eventX, eventY, mx, my); // Tutorial step1 → step2: detect a significant camera rotation on BOTH axes. // ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D, // not just nudged a single axis by accident. static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f; if (cameraDragging && gameState.tutorialStep == TutorialStep::Step1) { float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth); float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination); if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) { menuManager.advanceTutorialStep(); } } } } bool Game::startDarklandsTransition() { if (!gameState.isNight) { currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001"); return false; } if (gameState.isDawn) { currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001"); return false; } audioPlayer->playSoundAsync("audio/459618__n-lerche__blow.ogg"); if (darklandsFlashActive) return false; darklandsFlashActive = true; darklandsFlashFadingIn = true; darklandsFlashAlpha = 0.0f; isNightTransition = false; return true; } bool Game::startNightTransition() { if (darklandsFlashActive) return false; darklandsFlashActive = true; darklandsFlashFadingIn = true; darklandsFlashAlpha = 0.0f; isNightTransition = true; return true; } void Game::updateDarklandsFlash(int64_t deltaMs) { if (!darklandsFlashActive) return; static constexpr float kFadeDurationMs = 500.0f; const float step = static_cast(deltaMs) / kFadeDurationMs; if (darklandsFlashFadingIn) { darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f); if (darklandsFlashAlpha >= 1.0f) { if (isNightTransition) { gameState.isNight = !gameState.isNight; if (currentLocation()) { currentLocation()->state.isNight = gameState.isNight; currentLocation()->scriptEngine.callTriggerNightEnterCallback(); } audioPlayer->crossFadeMusicAsync("audio/bishkek night.ogg"); } else { gameState.isDarklands = !gameState.isDarklands; // Change HUD menuManager.setDarklandsMode(gameState.isDarklands); if (gameState.isDarklands) { audioPlayer->crossFadeMusicAsync("audio/bishkek fight calm.ogg"); } else { audioPlayer->crossFadeMusicAsync("audio/bishkek night.ogg"); } if (currentLocation()) { currentLocation()->state.isDarklands = gameState.isDarklands; if (gameState.isDarklands) currentLocation()->scriptEngine.callDarklandsEnterCallback(); else currentLocation()->scriptEngine.callDarklandsExitCallback(); } } darklandsFlashFadingIn = false; } } else { darklandsFlashAlpha = max(darklandsFlashAlpha - step, 0.0f); if (darklandsFlashAlpha <= 0.0f) { darklandsFlashAlpha = 0.0f; darklandsFlashActive = false; isNightTransition = false; } } } void Game::drawDarklandsFlash() { if (darklandsFlashAlpha <= 0.001f) return; const float W = static_cast(Environment::projectionWidth); const float H = static_cast(Environment::projectionHeight); if (darklandsFlashQuadW != W || darklandsFlashQuadH != H) { darklandsFlashQuadW = W; darklandsFlashQuadH = H; VertexDataStruct data; data.PositionData = { {0.f, 0.f, 0.f}, {0.f, H, 0.f}, {W, H, 0.f}, {W, H, 0.f}, {W, 0.f, 0.f}, {0.f, 0.f, 0.f} }; data.TexCoordData = { {0.f, 0.f}, {0.f, 1.f}, {1.f, 1.f}, {1.f, 1.f}, {1.f, 0.f}, {0.f, 0.f} }; darklandsFlashQuad.data = std::move(data); darklandsFlashQuad.RefreshVBO(); } glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); renderer.shaderManager.PushShader("darklands_flash"); renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f); renderer.PushMatrix(); renderer.LoadIdentity(); renderer.RenderUniform1f("uAlpha", darklandsFlashAlpha); static const float kWhite[3] = { 1.f, 1.f, 1.f }; static const float kBlack[3] = { 0.f, 0.f, 0.f }; renderer.RenderUniform3fv("uFlashColor", isNightTransition ? kBlack : kWhite); renderer.DrawVertexRenderStruct(darklandsFlashQuad); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); glDisable(GL_BLEND); } SaveSlotInfo Game::readSlotInfo(int slot) const { std::string path = "save_slot" + std::to_string(slot) + ".json"; std::string content = ZL::readTextFile(path); if (content.empty()) return {}; try { nlohmann::json root = nlohmann::json::parse(content); SaveSlotInfo info; info.empty = false; info.locationName = root.value("currentLocationName", ""); info.savedAt = root.value("savedAt", ""); return info; } catch (...) { return {}; } } void Game::saveGame(int slot) { nlohmann::json root; gameState.save(root); std::string path = "save_slot" + std::to_string(slot) + ".json"; std::ofstream file(path); if (file.is_open()) { file << root.dump(2); } else { std::cerr << "[save] Could not open " << path << " for writing" << std::endl; } } void Game::loadGame(int slot) { std::string path = "save_slot" + std::to_string(slot) + ".json"; std::string content = ZL::readTextFile(path); if (content.empty()) { std::cerr << "[save] Save file not found or empty: " << path << std::endl; return; } try { nlohmann::json root = nlohmann::json::parse(content); gameState.load(root); } catch (const std::exception& e) { std::cerr << "[save] Failed to parse save file " << path << ": " << e.what() << std::endl; } if (gameState.isDarklands) { audioPlayer->crossFadeMusicAsync("audio/bishkek fight calm.ogg"); } else if (gameState.isNight) { audioPlayer->crossFadeMusicAsync("audio/bishkek night.ogg"); } else { if (gameState.currentLocationName == "location_dorm") { audioPlayer->crossFadeMusicAsync("audio/obshaga.ogg"); } else { audioPlayer->crossFadeMusicAsync("audio/bishkek univer day.ogg"); } } } } // namespace ZL