shadow-over-bishkek001/src/Game.cpp
2026-06-27 15:14:03 +03:00

1627 lines
62 KiB
C++

#include "Game.h"
#include "AnimatedModel.h"
#include "utils/Utils.h"
#include <fstream>
#include "items/ItemRegistry.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
#include "TextModel.h"
#include <random>
#include <cmath>
#include <algorithm>
#include <functional>
#ifdef __ANDROID__
#include <android/log.h>
#endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#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
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<AudioPlayerAsync>())
, 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", "");
#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
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);
std::cout << "Load resurces step 4" << std::endl;
ItemRegistry::instance().loadFromJson("resources/config2/items.json", CONST_ZIP_FILE);
createLocations();
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.
gameState.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);
};
gameState.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);
};
menuManager.skipCutsceneFunc = [this]() {
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
};
std::cout << "Load resurces step 13" << std::endl;
menuManager.onResetGame = [this]() {
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.items.clear();
gameState.questJournal.loadFromFile("resources/quests/quests.json", CONST_ZIP_FILE);
createLocations();
};
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]() {
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); };
if (audioPlayer->init()) {
audioPlayer->setMusicVolume(100);
audioPlayer->setSoundVolume(80);
std::cout << "Audio initialized successfully" << std::endl;
}
else {
std::cout << "Audio initialization failed" << std::endl;
}
menuManager.loadSettings();
audioPlayer->crossFadeMusicAsync("audio/main menu final.ogg");
loadingCompleted = true;
}
void Game::createLocations()
{
gameState.locations.clear();
gameState.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);
gameState.locations["uni_interior"] = std::make_shared<Location>(renderer, gameState.inventory);
gameState.locations["uni_interior"]->setup(uniInteriorParams, &gameState.questJournal);
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/config2/gameobjects_uni_exterior_x.json";
uniExteriorParams.interactiveObjectsJsonPath = "resources/config2/interactive_objects_uni_exterior_x.json";
//uniExteriorParams.navigationJsonPaths = {"resources/navigation/uni_exterior3_all.json"};
uniExteriorParams.navigationJsonPaths = { "resources/navigation/uni_exterior3_trees_car.txt" };
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";
gameState.locations["uni_exterior"] = std::make_shared<Location>(renderer, gameState.inventory);
gameState.locations["uni_exterior"]->setup(uniExteriorParams, &gameState.questJournal);
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/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", //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/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);
gameState.locations["location_dorm"] = std::make_shared<Location>(renderer, gameState.inventory);
gameState.locations["location_dorm"]->setup(params_dorm, &gameState.questJournal);
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"]->onPlayerTaxiRequired = [this]() {
menuManager.tutorialShowTaxiHint();
};
gameState.locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = true;
menuManager.tutorialUnlockInteractiveObjectsFunc = [this]()
{
if (gameState.locations["location_dorm"])
{
gameState.locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = false;
}
};
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::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::milliseconds>(
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<float>(delta));
menuManager.topUiManager.update(static_cast<float>(delta));
if (!menuManager.isMainMenuOpen())
menuManager.update(static_cast<float>(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;
#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<int>(event.tfinger.x * Environment::width);
int eventY = static_cast<int>(event.tfinger.y * Environment::height);
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
int64_t fingerId = static_cast<int64_t>(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<int>((float)eventX / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((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<int>((float)eventX / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((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;
//y = y + 1;
//std::cout << "current y: " << y << std::endl;
//y = y - 0.002;
std::cout << "Player pos: " << currentLocation()->player->state.position.transpose() << 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) {
}
}
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<float>(a.eventX - b.eventX);
float dy = static_cast<float>(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<float>(itA->second.eventX - itB->second.eventX);
float dy = static_cast<float>(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;
}
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<float>(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();
}
} else {
gameState.isDarklands = !gameState.isDarklands;
// Change HUD
menuManager.setDarklandsMode(gameState.isDarklands);
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<float>(Environment::projectionWidth);
const float H = static_cast<float>(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;
}
}
} // namespace ZL