space-game001/src/Game.cpp
2026-04-29 20:02:35 +06:00

805 lines
27 KiB
C++

#include "Game.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "utils/Utils.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
{
void set_Texture(Character& npc, const TextureDataStruct& texture)
{
auto tt = std::make_shared<Texture>(texture);
npc.setTexture(tt);
}
void set_Texture(Character& npc, const std::string& meshName, const TextureDataStruct& texture)
{
auto tt = std::make_shared<Texture>(texture);
npc.setTexture(meshName, tt);
}
#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(const char* /*filename*/) {
std::cout << "Resources.zip loaded successfully" << std::endl;
if (s_instance) {
s_instance->mainThreadHandler.EnqueueMainThreadTask([&]() {
s_instance->setupPart2();
});
}
}
void Game::onResourcesZipError(const char* /*filename*/) {
std::cout << "Failed to download resources.zip" << std::endl;
}
#endif
Game::Game()
: newTickCount(0)
, lastTickCount(0)
, menuManager(renderer)
, audioPlayer(std::make_unique<AudioPlayerAsync>())
{
}
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 = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", ""));
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_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError);
#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()
{
#ifdef EMSCRIPTEN
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/defaultAtmosphere.vertex", "resources/shaders/defaultAtmosphere_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/planet_bake.vertex", "resources/shaders/planet_bake_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("spark", "resources/shaders/spark.vertex", "resources/shaders/spark_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("skinning", "resources/shaders/skinning.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("fog", "resources/shaders/fog.vertex", "resources/shaders/fog_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("fog_skinning", "resources/shaders/fog_skinning.vertex", "resources/shaders/fog_web.fragment", CONST_ZIP_FILE);
#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);
#endif
std::cout << "Load resurces step 4" << std::endl;
LocationSetup params1;
params1.roomMeshPath = "resources/w/room003.txt";
params1.roomTexturePath = "resources/w/room005.png";
params1.gameObjectsJsonPath = "resources/config2/gameobjects.json";
params1.npcsJsonPath = "resources/config2/npcs.json";
params1.dialoguesJsonPath = "resources/dialogue/sample_dialogues.json";
params1.navigationJsonPath = "resources/config2/navigation.json";
params1.scriptPath = "resources/start.lua";
params1.playerPosition = Eigen::Vector3f::Zero();
params1.teleportPosition = Eigen::Vector3f(-2.03001f, 0.f, -4.95618f);
params1.teleportRadius = 1.5f;
location1 = std::make_shared<Location>(renderer, inventory);
location1->setup(params1);
LocationSetup params2 = params1;
params2.roomMeshPath = "resources/w/exterior/Segmented_Plane002.txt";
params2.roomTexturePath = "resources/w/exterior/Segmented_Plane002.png";
params2.gameObjectsJsonPath = "resources/config2/gameobjects2.json";
params2.navigationJsonPath = "resources/config2/navigation2.json";
params2.scriptPath = "resources/start2.lua";
params2.playerPosition = Eigen::Vector3f(7.f, 0.f, 27.f);
params2.npcsJsonPath = "resources/config2/npcs2.json";
params2.teleportPosition = Eigen::Vector3f(-6.45, 0.0, 7.82);
params2.teleportRadius = 1.5f;
location2 = std::make_shared<Location>(renderer, inventory);
location2->setup(params2);
// Teleport callbacks: stepping into a location's zone hands the player
// to the other location at *its* teleport position. We pre-arm the
// destination's playerInTeleportZone so the player must walk out and
// back in before they can teleport again.
location1->onTeleport = [this]() {
std::cout << "[TELEPORT] location 1 -> location 2" << std::endl;
currentLocation = location2;
if (currentLocation->player) {
currentLocation->player->position = currentLocation->teleportPosition;
currentLocation->player->setTarget(currentLocation->teleportPosition);
}
currentLocation->playerInTeleportZone = true;
};
location2->onTeleport = [this]() {
std::cout << "[TELEPORT] location 2 -> location 1" << std::endl;
currentLocation = location1;
if (currentLocation->player) {
currentLocation->player->position = currentLocation->teleportPosition;
currentLocation->player->setTarget(currentLocation->teleportPosition);
}
currentLocation->playerInTeleportZone = true;
};
currentLocation = location2;
std::cout << "Load resurces step 5" << std::endl;
std::cout << "Load resurces step 12" << std::endl;
// Shadow mapping shaders
#ifdef EMSCRIPTEN
renderer.shaderManager.AddShaderFromFiles("shadow_depth", "resources/shaders/shadow_depth.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("shadow_depth_skinning", "resources/shaders/shadow_depth_skinning.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("default_shadow", "resources/shaders/default_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("fog_shadow", "resources/shaders/fog_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("fog_skinning_shadow", "resources/shaders/fog_skinning_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE);
#else
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);
#endif
std::cout << "Load resurces step 13" << std::endl;
// Load UI with inventory button
try {
menuManager.uiManager.loadFromFile("resources/config2/ui_inventory.json", renderer, CONST_ZIP_FILE);
std::cout << "UI loaded successfully" << std::endl;
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
menuManager.uiManager.setNodeVisible("close_inventory_button", false);
menuManager.uiManager.setTextButtonCallback("inventory_button", [this](const std::string& name) {
std::cout << "[UI] Inventory button clicked" << std::endl;
this->menuManager.uiManager.setNodeVisible("inventory_items_panel", true);
this->menuManager.uiManager.setNodeVisible("close_inventory_button", true);
this->inventoryOpen = true;
// Update UI with current items
const auto& items = this->inventory.getItems();
std::string itemText;
if (items.empty()) {
itemText = "Inventory (Empty)";
}
else {
itemText = "Inventory (" + std::to_string(items.size()) + " items)\n\n";
for (size_t i = 0; i < items.size(); ++i) {
itemText += std::to_string(i + 1) + ". " + items[i].name + "\n";
}
}
this->menuManager.uiManager.setText("inventory_items_text", itemText);
});
menuManager.uiManager.setTextButtonCallback("close_inventory_button", [this](const std::string& name) {
std::cout << "[UI] Close button clicked" << std::endl;
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
menuManager.uiManager.setNodeVisible("close_inventory_button", false);
inventoryOpen = false;
});
}
catch (const std::exception& e) {
std::cerr << "Failed to load UI: " << e.what() << std::endl;
}
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);
menuManager.uiManager.draw(renderer);
if (currentLocation)
{
currentLocation->dialogueSystem.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 (currentLocation)
{
if (currentLocation->shadowMap) {
CheckGlError(__FILE__, __LINE__);
currentLocation->drawShadowDepthPass();
CheckGlError(__FILE__, __LINE__);
currentLocation->drawGameWithShadows();
CheckGlError(__FILE__, __LINE__);
}
else {
currentLocation->drawGame();
CheckGlError(__FILE__, __LINE__);
}
}
else
{
// ??? Main menu???
}
drawUI();
}
CheckGlError(__FILE__, __LINE__);
}
void Game::drawLoading()
{
glClear(GL_DEPTH_BUFFER_BIT);
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 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();
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;
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;
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();
}
#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.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 < zoomstep) {
Environment::zoom = zoomstep;
}
}
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
switch (event.key.keysym.sym) {
case SDLK_1:
if (audioPlayer) audioPlayer->playSoundAsync("audio/background.wav");
break;
case SDLK_2:
if (audioPlayer) audioPlayer->playMusicAsync("audio/lullaby-music-vol20-186394--online-audio-convert.com.ogg");
break;
case SDLK_3:
if (audioPlayer) audioPlayer->stopMusicAsync();
break;
case SDLK_f:
currentLocation->dialogueSystem.startDialogue("test_choice_dialogue");
break;
case SDLK_e:
currentLocation->dialogueSystem.startDialogue("test_cutscene_pan_dialogue");
break;
case SDLK_o:
//y = y + 0.002;
currentLocation->player->hp = 200;
break;
case SDLK_k:
//y = y - 0.002;
std::cout << "Player pos: " << currentLocation->player->position.transpose() << std::endl;
break;
case SDLK_p:
currentLocation = (currentLocation == location1) ? location2 : location1;
std::cout << "Switched to location " << ((currentLocation == location1) ? "1" : "2") << std::endl;
break;
case SDLK_l:
x = x - 0.002;
break;
case SDLK_c:
std::cout << "SLOW-MO activated!" << std::endl;
activateSlowMoEffect();
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;
}
}
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.
static const float zoomMin = 2.0f;
if (newZoom < zoomMin) newZoom = zoomMin;
Environment::zoom = newZoom;
}
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.uiManager.onTouchDown(fingerId, uiX, uiY);
st.capturedByUi = 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.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.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);
}
}
} // namespace ZL