diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index da7a497a..294c0a82 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -204,6 +204,10 @@ public: const std::vector& getPlayerAuras() const { return playerAuras; } const std::vector& getTargetAuras() const { return targetAuras; } + // Hearthstone callback (single-player teleport) + using HearthstoneCallback = std::function; + void setHearthstoneCallback(HearthstoneCallback cb) { hearthstoneCallback = std::move(cb); } + // Cooldowns float getSpellCooldown(uint32_t spellId) const; @@ -432,6 +436,7 @@ private: std::vector combatText; // ---- Phase 3: Spells ---- + HearthstoneCallback hearthstoneCallback; std::vector knownSpells; std::unordered_map spellCooldowns; // spellId -> remaining seconds uint8_t castCount = 0; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index e9ec0f69..f74debd2 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -35,6 +35,7 @@ public: void reset(); float getMovementSpeed() const { return movementSpeed; } + const glm::vec3& getDefaultPosition() const { return defaultPosition; } bool isMoving() const; float getYaw() const { return yaw; } float getFacingYaw() const { return facingYaw; } @@ -116,6 +117,10 @@ private: float lastGroundZ = 0.0f; // Last known ground height (fallback when no terrain) static constexpr float GRAVITY = -30.0f; static constexpr float JUMP_VELOCITY = 15.0f; + float jumpBufferTimer = 0.0f; // Time since space was pressed + float coyoteTimer = 0.0f; // Time since last grounded + static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer + static constexpr float COYOTE_TIME = 0.10f; // 100ms grace after leaving ground // Swimming bool swimming = false; @@ -160,9 +165,9 @@ private: static constexpr float WOW_GRAVITY = -19.29f; static constexpr float WOW_JUMP_VELOCITY = 7.96f; - // Default spawn position (Stormwind Trade District) - glm::vec3 defaultPosition = glm::vec3(-8830.0f, 640.0f, 200.0f); - float defaultYaw = 0.0f; // Look north toward canals + // Default spawn position (Goldshire Inn) + glm::vec3 defaultPosition = glm::vec3(-9464.0f, 62.0f, 200.0f); + float defaultYaw = 0.0f; float defaultPitch = -5.0f; }; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 2ad91227..0de1c2ec 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -47,6 +47,7 @@ struct M2ModelGPU { bool collisionPlanter = false; bool collisionSmallSolidProp = false; bool collisionNarrowVerticalProp = false; + bool collisionTreeTrunk = false; bool collisionNoBlock = false; bool collisionStatue = false; diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 5b712894..55f55076 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -183,6 +183,7 @@ public: * Get statistics */ int getLoadedTileCount() const { return static_cast(loadedTiles.size()); } + int getPendingTileCount() const { return static_cast(pendingTiles.size()); } TileCoord getCurrentTile() const { return currentTile; } private: @@ -247,8 +248,8 @@ private: // Streaming parameters bool streamingEnabled = true; - int loadRadius = 1; // Load tiles within this radius (3x3 grid for better CPU/GPU perf) - int unloadRadius = 2; // Unload tiles beyond this radius + int loadRadius = 2; // Load tiles within this radius (5x5 grid) + int unloadRadius = 3; // Unload tiles beyond this radius float updateInterval = 0.1f; // Check streaming every 0.1 seconds float timeSinceLastUpdate = 0.0f; diff --git a/src/core/application.cpp b/src/core/application.cpp index be5e64b9..de8df5f5 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -108,43 +108,8 @@ bool Application::initialize() { void Application::run() { LOG_INFO("Starting main loop"); - // Show loading screen while loading initial data - rendering::LoadingScreen loadingScreen; - if (loadingScreen.initialize()) { - // Render loading screen - loadingScreen.setStatus("Initializing..."); - loadingScreen.render(); - window->swapBuffers(); - - // Load terrain data - if (assetManager && assetManager->isInitialized() && renderer) { - loadingScreen.setStatus("Loading terrain..."); - loadingScreen.render(); - window->swapBuffers(); - - renderer->loadTestTerrain(assetManager.get(), "World\\Maps\\Azeroth\\Azeroth_32_49.adt"); - - loadingScreen.setStatus("Spawning character..."); - loadingScreen.render(); - window->swapBuffers(); - - // Spawn player character with third-person camera - spawnPlayerCharacter(); - } - - loadingScreen.setStatus("Ready!"); - loadingScreen.render(); - window->swapBuffers(); - SDL_Delay(500); // Brief pause to show "Ready!" - - loadingScreen.shutdown(); - } else { - // Fallback: load without loading screen - if (assetManager && assetManager->isInitialized() && renderer) { - renderer->loadTestTerrain(assetManager.get(), "World\\Maps\\Azeroth\\Azeroth_32_49.adt"); - spawnPlayerCharacter(); - } - } + // Terrain and character are loaded via startSinglePlayer() when the user + // picks single-player mode, so nothing is preloaded here. auto lastTime = std::chrono::high_resolution_clock::now(); @@ -653,8 +618,17 @@ void Application::spawnPlayerCharacter() { LOG_INFO("Loaded fallback cube model (no MPQ data)"); } - // Spawn character at camera's ground position - glm::vec3 spawnPos = camera->getPosition() - glm::vec3(0.0f, 0.0f, 5.0f); + // Spawn character at the camera controller's default position (matches hearthstone), + // but snap Z to actual terrain height so the character doesn't float. + auto* camCtrl = renderer->getCameraController(); + glm::vec3 spawnPos = camCtrl ? camCtrl->getDefaultPosition() + : (camera->getPosition() - glm::vec3(0.0f, 0.0f, 5.0f)); + if (renderer->getTerrainManager()) { + auto terrainH = renderer->getTerrainManager()->getHeightAt(spawnPos.x, spawnPos.y); + if (terrainH) { + spawnPos.z = *terrainH + 0.1f; + } + } uint32_t instanceId = charRenderer->createInstance(1, spawnPos, glm::vec3(0.0f), 1.0f); // Scale 1.0 = normal WoW character size @@ -861,16 +835,6 @@ void Application::startSinglePlayer() { LOG_INFO("Single-player world created"); } - // Set up camera for single-player mode - if (renderer && renderer->getCamera()) { - auto* camera = renderer->getCamera(); - // Position: high above terrain to see landscape (terrain around origin is ~80-100 units high) - camera->setPosition(glm::vec3(0.0f, 0.0f, 300.0f)); // 300 units up - // Rotation: looking north (yaw 0) with downward tilt to see terrain - camera->setRotation(0.0f, -30.0f); // Look down more to see terrain below - LOG_INFO("Camera positioned for single-player mode"); - } - // Populate test inventory for single-player if (gameHandler) { gameHandler->getInventory().populateTestItems(); @@ -879,134 +843,106 @@ void Application::startSinglePlayer() { // Load weapon models for equipped items (after inventory is populated) loadEquippedWeapons(); + // --- Loading screen: load terrain and wait for streaming before spawning --- + rendering::LoadingScreen loadingScreen; + bool loadingScreenOk = loadingScreen.initialize(); + + auto showStatus = [&](const char* msg) { + if (!loadingScreenOk) return; + loadingScreen.setStatus(msg); + loadingScreen.render(); + window->swapBuffers(); + }; + + showStatus("Loading terrain..."); + // Try to load test terrain if WOW_DATA_PATH is set + bool terrainOk = false; if (renderer && assetManager && assetManager->isInitialized()) { - LOG_INFO("Loading test terrain for single-player mode..."); - - // Try to load Elwynn Forest (most common starting zone) - // ADT coordinates: (32, 49) is near Northshire Abbey std::string adtPath = "World\\Maps\\Azeroth\\Azeroth_32_49.adt"; - - if (renderer->loadTestTerrain(assetManager.get(), adtPath)) { - LOG_INFO("Test terrain loaded successfully"); - } else { - LOG_WARNING("Could not load test terrain - continuing with atmospheric rendering only"); - LOG_INFO("Set WOW_DATA_PATH environment variable to load terrain"); + terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath); + if (!terrainOk) { + LOG_WARNING("Could not load test terrain - atmospheric rendering only"); } - } else { - LOG_INFO("Asset manager not available - atmospheric rendering only"); - LOG_INFO("Set WOW_DATA_PATH environment variable to enable terrain loading"); } - // Spawn test objects for single-player mode - if (renderer) { - LOG_INFO("Spawning test objects for single-player mode..."); + // Wait for surrounding terrain tiles to stream in + if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) { + auto* terrainMgr = renderer->getTerrainManager(); + auto* camera = renderer->getCamera(); - // Spawn test characters in a row - auto* characterRenderer = renderer->getCharacterRenderer(); - if (characterRenderer) { - // Create test character model (same as K key) - pipeline::M2Model testModel; - float size = 2.0f; - std::vector cubePos = { - {-size, -size, -size}, { size, -size, -size}, - { size, size, -size}, {-size, size, -size}, - {-size, -size, size}, { size, -size, size}, - { size, size, size}, {-size, size, size} - }; + // First update with large dt to trigger streamTiles() immediately + terrainMgr->update(*camera, 1.0f); - for (const auto& pos : cubePos) { - pipeline::M2Vertex v; - v.position = pos; - v.normal = glm::normalize(pos); - v.texCoords[0] = glm::vec2(0.0f); - v.boneWeights[0] = 255; - v.boneWeights[1] = v.boneWeights[2] = v.boneWeights[3] = 0; - v.boneIndices[0] = 0; - v.boneIndices[1] = v.boneIndices[2] = v.boneIndices[3] = 0; - testModel.vertices.push_back(v); - } + auto startTime = std::chrono::high_resolution_clock::now(); + const float maxWaitSeconds = 15.0f; - // One bone at origin - pipeline::M2Bone bone; - bone.keyBoneId = -1; - bone.flags = 0; - bone.parentBone = -1; - bone.submeshId = 0; - bone.pivot = glm::vec3(0.0f); - testModel.bones.push_back(bone); - - // Simple animation - pipeline::M2Sequence seq{}; - seq.id = 0; - seq.duration = 1000; - testModel.sequences.push_back(seq); - - // Load model into renderer - if (characterRenderer->loadModel(testModel, 1)) { - // Spawn 5 characters in a row - for (int i = 0; i < 5; i++) { - glm::vec3 pos(i * 15.0f - 30.0f, 80.0f, 0.0f); - characterRenderer->createInstance(1, pos); + while (terrainMgr->getPendingTileCount() > 0) { + // Poll events to keep window responsive + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + window->setShouldClose(true); + loadingScreen.shutdown(); + return; } - LOG_INFO("Spawned 5 test characters"); } + + // Process ready tiles from worker threads + terrainMgr->update(*camera, 0.016f); + + // Update loading screen with progress + if (loadingScreenOk) { + int loaded = terrainMgr->getLoadedTileCount(); + int pending = terrainMgr->getPendingTileCount(); + char buf[128]; + snprintf(buf, sizeof(buf), "Loading terrain... %d tiles loaded, %d remaining", + loaded, pending); + loadingScreen.setStatus(buf); + loadingScreen.render(); + window->swapBuffers(); + } + + // Timeout safety + auto elapsed = std::chrono::high_resolution_clock::now() - startTime; + if (std::chrono::duration(elapsed).count() > maxWaitSeconds) { + LOG_WARNING("Terrain streaming timeout after ", maxWaitSeconds, "s"); + break; + } + + SDL_Delay(16); // ~60fps cap for loading screen } - // Spawn test buildings in a grid - auto* wmoRenderer = renderer->getWMORenderer(); - if (wmoRenderer) { - // Create procedural test WMO if not already loaded - pipeline::WMOModel testWMO; - testWMO.version = 17; + LOG_INFO("Terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded"); - pipeline::WMOGroup group; - group.vertices = { - {{-5, -5, 0}, {0, 0, 1}, {0, 0}, {0.8f, 0.7f, 0.6f, 1.0f}}, - {{5, -5, 0}, {0, 0, 1}, {1, 0}, {0.8f, 0.7f, 0.6f, 1.0f}}, - {{5, 5, 0}, {0, 0, 1}, {1, 1}, {0.8f, 0.7f, 0.6f, 1.0f}}, - {{-5, 5, 0}, {0, 0, 1}, {0, 1}, {0.8f, 0.7f, 0.6f, 1.0f}}, - {{-5, -5, 10}, {0, 0, 1}, {0, 0}, {0.7f, 0.6f, 0.5f, 1.0f}}, - {{5, -5, 10}, {0, 0, 1}, {1, 0}, {0.7f, 0.6f, 0.5f, 1.0f}}, - {{5, 5, 10}, {0, 0, 1}, {1, 1}, {0.7f, 0.6f, 0.5f, 1.0f}}, - {{-5, 5, 10}, {0, 0, 1}, {0, 1}, {0.7f, 0.6f, 0.5f, 1.0f}} - }; - - pipeline::WMOBatch batch; - batch.startIndex = 0; - batch.indexCount = 36; - batch.materialId = 0; - group.batches.push_back(batch); - - group.indices = { - 0,1,2, 0,2,3, 4,6,5, 4,7,6, - 0,4,5, 0,5,1, 1,5,6, 1,6,2, - 2,6,7, 2,7,3, 3,7,4, 3,4,0 - }; - - testWMO.groups.push_back(group); - - pipeline::WMOMaterial material; - material.shader = 0; - material.blendMode = 0; - testWMO.materials.push_back(material); - - // Load the test model - if (wmoRenderer->loadModel(testWMO, 1)) { - // Spawn buildings in a grid pattern - for (int x = -1; x <= 1; x++) { - for (int y = 0; y <= 2; y++) { - glm::vec3 pos(x * 30.0f, y * 30.0f + 120.0f, 0.0f); - wmoRenderer->createInstance(1, pos); - } - } - LOG_INFO("Spawned 9 test buildings"); - } + // Re-snap camera to ground now that all surrounding tiles are loaded + // (the initial reset inside loadTestTerrain only had 1 tile) + if (renderer->getCameraController()) { + renderer->getCameraController()->reset(); } + } - LOG_INFO("Test objects spawned - you should see characters and buildings"); - LOG_INFO("Use WASD to fly around, mouse to look"); - LOG_INFO("Press K for more characters, O for more buildings"); + showStatus("Spawning character..."); + + // Spawn player character on loaded terrain + spawnPlayerCharacter(); + + // Final camera reset: now that follow target exists and terrain is loaded, + // snap the third-person camera into the correct orbit position. + if (renderer && renderer->getCameraController()) { + renderer->getCameraController()->reset(); + } + + if (loadingScreenOk) { + loadingScreen.shutdown(); + } + + // Wire hearthstone to camera reset (teleport home) in single-player + if (gameHandler && renderer && renderer->getCameraController()) { + auto* camCtrl = renderer->getCameraController(); + gameHandler->setHearthstoneCallback([camCtrl]() { + camCtrl->reset(); + }); } // Go directly to game diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 1d8e8ac6..8d7fa8ab 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1101,11 +1101,12 @@ void GameHandler::handleCreatureQueryResponse(network::Packet& packet) { // ============================================================ void GameHandler::startAutoAttack(uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) return; autoAttacking = true; autoAttackTarget = targetGuid; - auto packet = AttackSwingPacket::build(targetGuid); - socket->send(packet); + if (state == WorldState::IN_WORLD && socket) { + auto packet = AttackSwingPacket::build(targetGuid); + socket->send(packet); + } LOG_INFO("Starting auto-attack on 0x", std::hex, targetGuid, std::dec); } @@ -1204,9 +1205,14 @@ void GameHandler::handleSpellHealLog(network::Packet& packet) { // ============================================================ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + // Hearthstone (8690) — handle locally when no server connection (single-player) + if (spellId == 8690 && hearthstoneCallback) { + LOG_INFO("Hearthstone: teleporting home"); + hearthstoneCallback(); + return; + } - // Attack (6603) routes to auto-attack instead of cast + // Attack (6603) routes to auto-attack instead of cast (works without server) if (spellId == 6603) { uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid; if (target != 0) { @@ -1219,6 +1225,8 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { return; } + if (state != WorldState::IN_WORLD || !socket) return; + if (casting) return; // Already casting uint64_t target = targetGuid != 0 ? targetGuid : targetGuid; diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index f72d1bb6..63de3c80 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -154,8 +154,8 @@ void CameraController::update(float deltaTime) { glm::vec3 forward(std::cos(moveYawRad), std::sin(moveYawRad), 0.0f); glm::vec3 right(-std::sin(moveYawRad), std::cos(moveYawRad), 0.0f); - // Toggle sit/crouch with X or C key (edge-triggered) — only when UI doesn't want keyboard - bool xDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_X) || input.isKeyPressed(SDL_SCANCODE_C)); + // Toggle sit/crouch with X key (edge-triggered) — only when UI doesn't want keyboard + bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X); if (xDown && !xKeyWasDown) { sitting = !sitting; } @@ -190,37 +190,16 @@ void CameraController::update(float deltaTime) { m2Renderer->setCollisionFocus(targetPos, COLLISION_FOCUS_RADIUS_THIRD_PERSON); } - // Check for water at current position + // Check for water at current position — simple submersion test. + // If the player's feet are meaningfully below the water surface, swim. std::optional waterH; if (waterRenderer) { waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y); } - constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f; - bool inWater = false; - if (waterH && targetPos.z < *waterH) { - std::optional waterType; - if (waterRenderer) { - waterType = waterRenderer->getWaterTypeAt(targetPos.x, targetPos.y); - } - bool isOcean = false; - if (waterType && *waterType != 0) { - isOcean = (((*waterType - 1) % 4) == 1); - } - bool depthAllowed = isOcean || ((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE); - if (!depthAllowed) { - inWater = false; - } else { - std::optional terrainH; - std::optional wmoH; - std::optional m2H; - if (terrainManager) terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y); - if (wmoRenderer) wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 6.0f); - if (m2Renderer) m2H = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 1.0f); - auto floorH = selectHighestFloor(terrainH, wmoH, m2H); - constexpr float MIN_SWIM_WATER_DEPTH = 1.8f; - // Ocean is valid even when ground isn't currently resolved (deep water or streaming gaps). - inWater = (floorH && ((*waterH - *floorH) >= MIN_SWIM_WATER_DEPTH)) || (isOcean && !floorH); - } + bool inWater = waterH && (targetPos.z < (*waterH - 0.3f)); + // Keep swimming through water-data gaps (chunk boundaries). + if (!inWater && swimming && !waterH) { + inWater = true; } @@ -298,7 +277,7 @@ void CameraController::update(float deltaTime) { if (mh && (!floorH || *mh > *floorH)) floorH = mh; } if (floorH) { - float swimFloor = *floorH + 0.30f; + float swimFloor = *floorH + 0.5f; if (targetPos.z < swimFloor) { targetPos.z = swimFloor; if (verticalVelocity < 0.0f) verticalVelocity = 0.0f; @@ -343,6 +322,7 @@ void CameraController::update(float deltaTime) { grounded = false; } else { + // Exiting water — give a small upward boost to help climb onto shore. swimming = false; if (glm::length(movement) > 0.001f) { @@ -350,12 +330,21 @@ void CameraController::update(float deltaTime) { targetPos += movement * speed * deltaTime; } - // Jump - if (nowJump && grounded) { + // Jump with input buffering and coyote time + if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME; + if (grounded) coyoteTimer = COYOTE_TIME; + + bool canJump = (coyoteTimer > 0.0f) && (jumpBufferTimer > 0.0f); + if (canJump) { verticalVelocity = jumpVel; grounded = false; + jumpBufferTimer = 0.0f; + coyoteTimer = 0.0f; } + jumpBufferTimer -= deltaTime; + coyoteTimer -= deltaTime; + // Apply gravity verticalVelocity += gravity * deltaTime; targetPos.z += verticalVelocity * deltaTime; @@ -501,7 +490,8 @@ void CameraController::update(float deltaTime) { } // Ground the character to terrain or WMO floor - { + // Skip entirely while swimming — the swim floor clamp handles vertical bounds. + if (!swimming) { auto sampleGround = [&](float x, float y) -> std::optional { std::optional terrainH; std::optional wmoH; @@ -549,15 +539,14 @@ void CameraController::update(float deltaTime) { lastGroundZ = *groundH; } - if (targetPos.z <= lastGroundZ + 0.1f) { + if (targetPos.z <= lastGroundZ + 0.1f && verticalVelocity <= 0.0f) { targetPos.z = lastGroundZ; verticalVelocity = 0.0f; grounded = true; - swimming = false; // Touching ground = wading, not swimming - } else if (!swimming) { + } else { grounded = false; } - } else if (!swimming) { + } else { // No terrain found — hold at last known ground targetPos.z = lastGroundZ; verticalVelocity = 0.0f; @@ -762,12 +751,20 @@ void CameraController::update(float deltaTime) { newPos += movement * speed * deltaTime; } - // Jump - if (nowJump && grounded) { + // Jump with input buffering and coyote time + if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME; + if (grounded) coyoteTimer = COYOTE_TIME; + + if (coyoteTimer > 0.0f && jumpBufferTimer > 0.0f) { verticalVelocity = jumpVel; grounded = false; + jumpBufferTimer = 0.0f; + coyoteTimer = 0.0f; } + jumpBufferTimer -= deltaTime; + coyoteTimer -= deltaTime; + // Apply gravity verticalVelocity += gravity * deltaTime; newPos.z += verticalVelocity * deltaTime; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index fc26331b..b1c1795f 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -29,7 +29,18 @@ void getTightCollisionBounds(const M2ModelGPU& model, glm::vec3& outMin, glm::ve // larger than default to prevent walk-through on narrow objects // - default: tighter fit (avoid oversized blockers) // - stepped low platforms (tree curbs/planters): wider XY + lower Z - if (model.collisionNarrowVerticalProp) { + if (model.collisionTreeTrunk) { + // Tree trunk: proportional cylinder at the base of the tree. + float modelHoriz = std::max(model.boundMax.x - model.boundMin.x, + model.boundMax.y - model.boundMin.y); + float trunkHalf = std::clamp(modelHoriz * 0.05f, 0.5f, 5.0f); + half.x = trunkHalf; + half.y = trunkHalf; + // Height proportional to trunk width, capped at 3.5 units. + half.z = std::min(trunkHalf * 2.5f, 3.5f); + // Shift center down so collision is at the base (trunk), not mid-canopy. + center.z = model.boundMin.z + half.z; + } else if (model.collisionNarrowVerticalProp) { // Tall thin props (lamps/posts): keep passable gaps near walls. half.x *= 0.30f; half.y *= 0.30f; @@ -396,19 +407,19 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("flower") != std::string::npos) || (lowerName.find("shrub") != std::string::npos) || (lowerName.find("fern") != std::string::npos) || - (lowerName.find("vine") != std::string::npos); - bool canopyLike = - (lowerName.find("canopy") != std::string::npos) || - (lowerName.find("leaf") != std::string::npos) || - (lowerName.find("leaves") != std::string::npos); + (lowerName.find("vine") != std::string::npos) || + (lowerName.find("lily") != std::string::npos) || + (lowerName.find("weed") != std::string::npos); bool treeLike = (lowerName.find("tree") != std::string::npos); bool hardTreePart = (lowerName.find("trunk") != std::string::npos) || (lowerName.find("stump") != std::string::npos) || (lowerName.find("log") != std::string::npos); - bool softTree = treeLike && !hardTreePart && (canopyLike || vert > horiz * 1.35f); - bool smallSoftShape = (horiz < 2.2f && vert < 2.4f); - bool mediumFoliageShape = (horiz < 4.5f && vert < 4.5f); + // Only large trees (canopy > 20 model units wide) get trunk collision. + // Small/mid trees are walkthrough to avoid getting stuck between them. + // Only large trees get trunk collision; all smaller trees are walkthrough. + bool treeWithTrunk = treeLike && !hardTreePart && !foliageName && horiz > 40.0f; + bool softTree = treeLike && !hardTreePart && !treeWithTrunk; bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName || gpuModel.collisionPlanter; bool narrowVerticalName = (lowerName.find("lamp") != std::string::npos) || @@ -417,6 +428,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("pole") != std::string::npos); bool narrowVerticalShape = (horiz > 0.12f && horiz < 2.0f && vert > 2.2f && vert > horiz * 1.8f); + gpuModel.collisionTreeTrunk = treeWithTrunk; gpuModel.collisionNarrowVerticalProp = !gpuModel.collisionSteppedFountain && !gpuModel.collisionSteppedLowPlatform && @@ -435,10 +447,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { !gpuModel.collisionSteppedFountain && !gpuModel.collisionSteppedLowPlatform && !gpuModel.collisionNarrowVerticalProp && + !gpuModel.collisionTreeTrunk && !curbLikeName && !lowPlatformLikeShape && (smallSolidPropName || (genericSolidPropShape && !foliageName && !softTree)); - gpuModel.collisionNoBlock = ((((foliageName && smallSoftShape) || (foliageName && mediumFoliageShape)) || softTree) && + gpuModel.collisionNoBlock = ((foliageName || softTree) && !forceSolidCurb); } gpuModel.boundMin = tightMin; @@ -883,7 +896,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount = 0; // Adaptive render distance: shorter in dense areas (cities), longer in open terrain - const float maxRenderDistance = (instances.size() > 600) ? 180.0f : 350.0f; + const float maxRenderDistance = (instances.size() > 600) ? 180.0f : 2000.0f; const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const float fadeStartFraction = 0.75f; const glm::vec3 camPos = camera.getPosition(); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index fedd2678..5eaf7dc1 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -310,7 +310,7 @@ std::unique_ptr TerrainManager::prepareTile(int x, int y) { p.rotation = glm::vec3( -placement.rotation[2] * 3.14159f / 180.0f, -placement.rotation[0] * 3.14159f / 180.0f, - placement.rotation[1] * 3.14159f / 180.0f + (placement.rotation[1] + 180.0f) * 3.14159f / 180.0f ); p.scale = placement.scale / 1024.0f; pending->m2Placements.push_back(p);