#include "Game.h" #include "AnimatedModel.h" #include "utils/Utils.h" #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; /*s_instance->mainThreadHandler.EnqueueMainThreadTask([&]() { s_instance->setupPart2(); });*/ std::cout << "s_instance setup part 2" << std::endl; s_instance->setupPart2(); } std::cout << "s_instance end setup" << std::endl; } 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 Game::Game() : newTickCount(0) , lastTickCount(0) , menuManager(renderer, globalInts) , audioPlayer(std::make_unique()) { } 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", ""); #ifdef EMSCRIPTEN loadingProgressBarFrameTexture = renderer.textureManager.LoadFromPng( "resources/loadingProgressBarFrame.png", "", true); loadingProgressBarTexture = renderer.textureManager.LoadFromPng( "resources/loadingProgressBar.png", "", true); #endif 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 mainThreadHandler.EnqueueMainThreadTask([this]() { std::cout << "Load resurces step 2" << std::endl; this->setupPart2(); std::cout << "Load resurces step 3" << std::endl; }); #endif } void Game::setupPart2() { std::cout << "Load resurces step 12x1" << std::endl; #ifdef EMSCRIPTEN renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x2" << std::endl; renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/defaultAtmosphere.vertex", "resources/shaders/defaultAtmosphere_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x3" << std::endl; renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/planet_bake.vertex", "resources/shaders/planet_bake_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x4" << std::endl; renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x5" << std::endl; renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x6" << std::endl; renderer.shaderManager.AddShaderFromFiles("spark", "resources/shaders/spark.vertex", "resources/shaders/spark_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x7" << std::endl; renderer.shaderManager.AddShaderFromFiles("skinning", "resources/shaders/skinning.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x8" << std::endl; renderer.shaderManager.AddShaderFromFiles("fog", "resources/shaders/fog.vertex", "resources/shaders/fog_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x9" << std::endl; renderer.shaderManager.AddShaderFromFiles("fog_skinning", "resources/shaders/fog_skinning.vertex", "resources/shaders/fog_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x10" << std::endl; renderer.shaderManager.AddShaderFromFiles("darklands_fog", "resources/shaders/darklands_fog.vertex", "resources/shaders/darklands_fog_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x11" << std::endl; renderer.shaderManager.AddShaderFromFiles("darklands_fog_skinning", "resources/shaders/darklands_fog_skinning.vertex", "resources/shaders/darklands_fog_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x12" << std::endl; renderer.shaderManager.AddShaderFromFiles("darklands_flash", "resources/shaders/default.vertex", "resources/shaders/darklands_flash_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x13" << std::endl; renderer.shaderManager.AddShaderFromFiles("cutsceneFade", "resources/shaders/default.vertex", "resources/shaders/cutscene_fade_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x14" << std::endl; renderer.shaderManager.AddShaderFromFiles("cutsceneBlack", "resources/shaders/default.vertex", "resources/shaders/cutscene_black_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x15" << std::endl; renderer.shaderManager.AddShaderFromFiles("shadow_depth", "resources/shaders/shadow_depth.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x16" << std::endl; renderer.shaderManager.AddShaderFromFiles("shadow_depth_skinning", "resources/shaders/shadow_depth_skinning.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x17" << std::endl; renderer.shaderManager.AddShaderFromFiles("default_shadow", "resources/shaders/default_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x18" << std::endl; renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x19" << std::endl; renderer.shaderManager.AddShaderFromFiles("fog_shadow", "resources/shaders/fog_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x20" << std::endl; renderer.shaderManager.AddShaderFromFiles("fog_skinning_shadow", "resources/shaders/fog_skinning_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x21" << std::endl; renderer.shaderManager.AddShaderFromFiles("night_fog", "resources/shaders/night_fog.vertex", "resources/shaders/night_fog_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x22" << std::endl; renderer.shaderManager.AddShaderFromFiles("night_fog_skinning", "resources/shaders/night_fog_skinning.vertex", "resources/shaders/night_fog_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x23" << std::endl; renderer.shaderManager.AddShaderFromFiles("night_fog_shadow", "resources/shaders/night_fog_shadow.vertex", "resources/shaders/night_fog_shadow_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x24" << std::endl; renderer.shaderManager.AddShaderFromFiles("night_fog_skinning_shadow", "resources/shaders/night_fog_skinning_shadow.vertex", "resources/shaders/night_fog_shadow_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x25" << std::endl; #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 std::cout << "Load resurces step 4" << std::endl; ItemRegistry::instance().loadFromJson("resources/config2/items.json", CONST_ZIP_FILE); globalFloats["player_hp"] = 200; LocationSetup uniInteriorParams; uniInteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects_uni_interior_x.json"; uniInteriorParams.npcsJsonPath = "resources/config2/npcs_uni_interior.json"; uniInteriorParams.dialoguesJsonPath = "resources/dialogue/uni_interior_dialogues_006.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/config2/teleports_uni_interior.json"; uniInteriorParams.triggerZonesJsonPath = "resources/config2/trigger_zones_uni_interior.json"; uniInteriorParams.lightsJsonPath = "resources/config2/lights_uni_interior.json"; uniInteriorParams.scriptPath = "resources/start_uni_interior.lua"; uniInteriorParams.interactiveObjectsJsonPath = "resources/config2/interactive_objects_uni_interior_x.json"; uniInteriorParams.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104); locations["uni_interior"] = std::make_shared(renderer, inventory); locations["uni_interior"]->setup(uniInteriorParams, &menuManager.questJournal); locations["uni_interior"]->scriptEngine.setGlobalStore(&globalInts); locations["uni_interior"]->scriptEngine.setGlobalFloatStore(&globalFloats); locations["uni_interior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { this->menuManager.isNight = isNight; this->menuManager.isDawn = isDawn; }; locations["uni_interior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; locations["uni_interior"]->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); }; locations["uni_interior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); }; locations["uni_interior"]->requestReturnToMainMenu = [this]() { this->currentLocation = nullptr; menuManager.showMainMenu(); }; locations["uni_interior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { menuManager.setChatUnread(idx, unread, msg); }; if (locations["uni_interior"]->player) locations["uni_interior"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); }; for (auto& npc : locations["uni_interior"]->npcs) { if (npc && npc->canAttack) { npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); }; } } LocationSetup uniExteriorParams = uniInteriorParams; uniExteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects_uni_exterior_x.json"; uniExteriorParams.interactiveObjectsJsonPath = "resources/config2/interactive_objects_uni_exterior_x.json"; uniExteriorParams.navigationJsonPaths = {"resources/navigation/uni_exterior2_all.txt"}; //uniExteriorParams.navigationJsonPaths = { "resources/navigation/uni_exterior2_all.json" }; uniExteriorParams.teleportsJsonPath = "resources/config2/teleports_uni_exterior.json"; uniExteriorParams.triggerZonesJsonPath = "resources/config2/trigger_zones_uni_exterior.json"; uniExteriorParams.lightsJsonPath = "resources/config2/lights_uni_exterior.json"; uniExteriorParams.scriptPath = "resources/start_uni_exterior.lua"; uniExteriorParams.playerPosition = Eigen::Vector3f(5, 0, -18.4); uniExteriorParams.npcsJsonPath = "resources/config2/npcs_uni_exterior.json"; uniExteriorParams.dialoguesJsonPath = "resources/dialogue/uni_exterior_dialogues.json"; locations["uni_exterior"] = std::make_shared(renderer, inventory); locations["uni_exterior"]->setup(uniExteriorParams, &menuManager.questJournal); locations["uni_exterior"]->scriptEngine.setGlobalStore(&globalInts); locations["uni_exterior"]->scriptEngine.setGlobalFloatStore(&globalFloats); locations["uni_exterior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { this->menuManager.isNight = isNight; this->menuManager.isDawn = isDawn; /*for (auto& locPair : locations) { if (locPair.second) { locPair.second->isNight = isNight; locPair.second->isDawn = isDawn; } }*/ }; locations["uni_exterior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; locations["uni_exterior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); }; locations["uni_exterior"]->requestReturnToMainMenu = [this]() { menuManager.showMainMenu(); }; locations["uni_exterior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { menuManager.setChatUnread(idx, unread, msg); }; if (locations["uni_exterior"]->player) locations["uni_exterior"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); }; LocationSetup params_dorm; //params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm.json"; //params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm_trees001.json"; params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm_new_x.json"; params_dorm.npcsJsonPath = "resources/config2/npcs_dorm.json"; params_dorm.dialoguesJsonPath = "resources/dialogue/dorm_dialogues.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", "resources/navigation/dorm3_ca.txt", "resources/navigation/dorm3_ba.txt", "resources/navigation/dorm3_a.txt", "resources/navigation/dorm3_b.txt", "resources/navigation/dorm3_all_open.txt", }; params_dorm.teleportsJsonPath = "resources/config2/teleports_dorm.json"; params_dorm.triggerZonesJsonPath = "resources/config2/trigger_zones_dorm.json"; params_dorm.lightsJsonPath = "resources/config2/lights_dorm.json"; params_dorm.scriptPath = "resources/start_dorm.lua"; params_dorm.interactiveObjectsJsonPath = "resources/config2/interactive_objects_dorm_x.json"; params_dorm.playerPosition = Eigen::Vector3f(6.76345, 0, -14.6022); locations["location_dorm"] = std::make_shared(renderer, inventory); locations["location_dorm"]->setup(params_dorm, &menuManager.questJournal); locations["location_dorm"]->scriptEngine.setGlobalStore(&globalInts); locations["location_dorm"]->scriptEngine.setGlobalFloatStore(&globalFloats); locations["location_dorm"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { this->menuManager.isNight = isNight; this->menuManager.isDawn = isDawn; }; locations["location_dorm"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; locations["location_dorm"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); }; locations["location_dorm"]->requestReturnToMainMenu = [this]() { menuManager.showMainMenu(); }; locations["location_dorm"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { menuManager.setChatUnread(idx, unread, msg); }; if (locations["location_dorm"]->player) locations["location_dorm"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); }; locations["location_dorm"]->onPlayerTaxiRequired = [this]() { menuManager.tutorialShowTaxiHint(); }; locations["location_dorm"]->tutorialInteractiveObjectsLocked = true; menuManager.tutorialUnlockInteractiveObjectsFunc = [this]() { if (locations["location_dorm"]) { locations["location_dorm"]->tutorialInteractiveObjectsLocked = false; } }; menuManager.callTaxiFunc = [this]() { if (locations["location_dorm"]) { 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 = locations.find(destName); if (it == locations.end()) { std::cerr << "[TELEPORT] Unknown destination location: " << destName << std::endl; return; } if (currentLocation) currentLocation->scriptEngine.callLocationExitCallback(); currentLocation = it->second; if (currentLocation->player) { currentLocation->player->position = destPos; currentLocation->player->setTarget(destPos); currentLocation->player->stopInPlace(); currentLocation->player->facingAngle = destRotY; currentLocation->player->targetFacingAngle = destRotY; } currentLocation->cameraAzimuth = destRotY; currentLocation->isDarklands = isDarklands; currentLocation->isNight = menuManager.isNight; currentLocation->isDawn = menuManager.isDawn; currentLocation->scriptEngine.callLocationEnterCallback(); menuManager.onLocationChanged(destName); }; locations["uni_exterior"]->onTeleport = teleportCallback; locations["uni_interior"]->onTeleport = teleportCallback; 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] : locations) { loc->dialogueSystem.setGlobalFlagStore(&globalInts); } // Wire tutorial advance callbacks for all locations. // advanceTutorialStep() guards against double-advancing, so sharing is safe. for (auto& [name, loc] : locations) { loc->dialogueSystem.setOnDialogueAdvanced([this]() { menuManager.advanceTutorialStep(); }); loc->onPlayerFloorWalk = [this]() { if (menuManager.tutorialStep == TutorialStep::Step2) { menuManager.advanceTutorialStep(); } menuManager.onPlayerStartedWalking(); }; } renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_received001.png", CONST_ZIP_FILE, true); renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_removed001.png", CONST_ZIP_FILE, true); mobileRotateTexture = renderer.textureManager.LoadFromPng( "resources/rotateYourDevice.png", CONST_ZIP_FILE, true); // Wire inventory callbacks: tutorial tracking + toast notifications. inventory.onItemAdded = [this](const std::string& itemId) { menuManager.onItemPickedUp(itemId); const Item* item = ItemRegistry::instance().findById(itemId); if (item) menuManager.showToast("resources/w/ui/img/toast/item_received001.png", item->name); }; inventory.onItemRemoved = [this](const std::string& itemId) { const Item* item = ItemRegistry::instance().findById(itemId); if (item) menuManager.showToast("resources/w/ui/img/toast/item_removed001.png", item->name); }; // 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); }; // Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown. for (auto& [name, loc] : 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] : locations) { loc->dialogueSystem.setOnCutsceneStarted([this]() { menuManager.onCutsceneStarted(); }); loc->dialogueSystem.setOnCutsceneFinishedExtra([this](const std::string&) { menuManager.onCutsceneFinished(); }); } menuManager.skipCutsceneFunc = [this]() { if (currentLocation) currentLocation->dialogueSystem.skipCutscene(); }; std::cout << "Load resurces step 13" << std::endl; try { menuManager.setup(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]() { currentLocation = locations["location_dorm"]; currentLocation->scriptEngine.callLocationEnterCallback(); }; // Wire HP-change callbacks so all player instances update the health bar HUD. for (auto& [name, loc] : locations) { if (loc->player) { loc->player->onHpChanged = [this](float hp, float maxHp) { menuManager.updateHealthBar(hp, maxHp); }; } } loadingCompleted = true; if (audioPlayer->init()) { audioPlayer->setMusicVolume(100); audioPlayer->setSoundVolume(80); std::cout << "Audio initialized successfully" << std::endl; } else { std::cout << "Audio initialization failed" << std::endl; } } 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); 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 { if (currentLocation) { // Sync global flags so Location's draw functions see them. currentLocation->isDarklands = isDarklands; currentLocation->isNight = menuManager.isNight; currentLocation->isDawn = menuManager.isDawn; if (isDarklands) { currentLocation->drawGameDarklands(); CheckGlError(__FILE__, __LINE__); } else if (menuManager.isNight) { currentLocation->drawGameNight(); CheckGlError(__FILE__, __LINE__); } else if (currentLocation->shadowMap) { CheckGlError(__FILE__, __LINE__); currentLocation->drawShadowDepthPass(); CheckGlError(__FILE__, __LINE__); currentLocation->drawGameWithShadows(); CheckGlError(__FILE__, __LINE__); } else { currentLocation->drawGame(); CheckGlError(__FILE__, __LINE__); } } drawUI(); drawDarklandsFlash(); } CheckGlError(__FILE__, __LINE__); } void Game::drawMobilePortraitOverlay() { if (!mobileRotateTexture) return; const float W = Environment::projectionWidth; const float H = Environment::projectionHeight; if (W != mobileRotateMeshLastW || H != mobileRotateMeshLastH) { mobileRotateMeshLastW = W; mobileRotateMeshLastH = H; const float minDim = min(W, H); mobileRotateMesh.data = CreateRect2D({ 0.f, 0.f }, { minDim * 0.5f, minDim * 0.5f }, 3.f); mobileRotateMesh.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(); glBindTexture(GL_TEXTURE_2D, mobileRotateTexture->getTexID()); renderer.DrawVertexRenderStruct(mobileRotateMesh); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); 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(); #ifdef EMSCRIPTEN if (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(resourcesDownloadProgress, 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(); } #endif 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) { // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях Environment::width = event.window.data1; Environment::height = event.window.data2; Environment::computeProjectionDimensions(); menuManager.uiManager.updateAllLayouts(); std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; //space.clearTextRendererCache(); } // Suppress all input while portrait overlay is shown if (mobileIsPortrait) 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 (menuManager.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 { menuManager.isDawn = !menuManager.isDawn; if (menuManager.isDawn) menuManager.isNight = true; } break; case SDLK_9: if (editorMode == EditorMode::InteractiveObjects && currentLocation) { currentLocation->editor.selectInteractiveObject(9); } else { startNightTransition(); /*if (menuManager.isDawn) { menuManager.isDawn = false; // step back: dawn → plain night } else { menuManager.isNight = !menuManager.isNight; }*/ } 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; //y = y + 1; //std::cout << "current y: " << y << std::endl; //y = y - 0.002; std::cout << "Player pos: " << currentLocation->player->position.transpose() << std::endl; //currentLocation->npcs[0]->walkSpeed -= 0.01f; //std::cout << "Walk speed: " << currentLocation->npcs[0]->walkSpeed << std::endl; break; case SDLK_p: currentLocation = locations["uni_interior"]; currentLocation->player->position = Eigen::Vector3f(-0.0189243, 0, -13.4314); currentLocation->player->setTarget(currentLocation->player->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->cameraAzimuth << std::endl; std::cout << "Inclination: " << currentLocation->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(); } 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) { } } 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->slowMoTimeRemaining = currentLocation->player->slowMoActiveTime; } for (auto& npc : currentLocation->npcs) { if (npc) { npc->slowMoTimeRemaining = npc->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->cameraAzimuth; dragStartInclination = currentLocation->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 (menuManager.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 && menuManager.tutorialStep == TutorialStep::Step1) { float deltaAz = std::abs(currentLocation->cameraAzimuth - dragStartAzimuth); float deltaInc = std::abs(currentLocation->cameraInclination - dragStartInclination); if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) { menuManager.advanceTutorialStep(); } } } } bool Game::startDarklandsTransition() { if (!menuManager.isNight) { currentLocation->dialogueSystem.startDialogue("darklands_day_dialog001"); return false; } if (menuManager.isDawn) { currentLocation->dialogueSystem.startDialogue("darklands_morning_dialog001"); return false; } 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) { menuManager.isNight = !menuManager.isNight; if (currentLocation) { //currentLocation->dialogueSystem.startDialogue("dialog_video001"); currentLocation->isNight = menuManager.isNight; currentLocation->scriptEngine.callTriggerNightEnterCallback(); } } else { isDarklands = !isDarklands; // Change HUD menuManager.setDarklandsMode(isDarklands); if (currentLocation) { currentLocation->isDarklands = isDarklands; if (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); } } // namespace ZL