Add transport system, fix NPC spawning, and improve water rendering

Transport System (Phases 1-7):
- Implement TransportManager with Catmull-Rom spline path interpolation
- Add WMO dynamic transforms for moving transport instances
- Implement player attachment via world position composition
- Add test transport with circular path around Stormwind harbor
- Add /transport board and /transport leave console commands
- Reuse taxi flight spline system and external follow camera mode

NPC Spawn Fixes:
- Add smart ocean spawn filter: blocks land creatures at high altitude over water (Z>50)
- Allow legitimate water creatures at sea level (Z≤50) to spawn correctly
- Fixes Elder Grey Bears, Highland Striders, and Plainscreepers spawning over ocean
- Snap online creatures to terrain height when valid ground exists

NpcManager Removal:
- Remove deprecated NpcManager (offline mode no longer supported)
- Delete npc_manager.hpp and npc_manager.cpp
- Simplify NPC animation callbacks to use only creatureInstances_ map
- Move NPC callbacks to game initialization in application.cpp

Water Rendering:
- Fix tile seam gaps caused by per-vertex wave randomization
- Add distance-based blending: seamless waves up close (<150u), grid effect far away (>400u)
- Smooth transition between seamless and grid modes (150-400 unit range)
- Preserves aesthetic grid pattern at horizon while eliminating gaps when swimming
This commit is contained in:
Kelsi 2026-02-10 21:29:10 -08:00
parent c91e0bb916
commit 2e923311d0
13 changed files with 711 additions and 1079 deletions

View file

@ -35,8 +35,8 @@
#include "ui/ui_manager.hpp"
#include "auth/auth_handler.hpp"
#include "game/game_handler.hpp"
#include "game/transport_manager.hpp"
#include "game/world.hpp"
#include "game/npc_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include <SDL2/SDL.h>
#include <GL/glew.h>
@ -409,12 +409,6 @@ void Application::update(float deltaTime) {
auto w2 = std::chrono::high_resolution_clock::now();
worldTime += std::chrono::duration<float, std::milli>(w2 - w1).count();
auto s1 = std::chrono::high_resolution_clock::now();
// Spawn NPCs once when entering world
spawnNpcs();
auto s2 = std::chrono::high_resolution_clock::now();
spawnTime += std::chrono::duration<float, std::milli>(s2 - s1).count();
auto cq1 = std::chrono::high_resolution_clock::now();
// Process deferred online creature spawns (throttled)
processCreatureSpawnQueue();
@ -432,9 +426,6 @@ void Application::update(float deltaTime) {
mountTime += std::chrono::duration<float, std::milli>(m2 - m1).count();
auto nm1 = std::chrono::high_resolution_clock::now();
if (npcManager && renderer && renderer->getCharacterRenderer()) {
npcManager->update(deltaTime, renderer->getCharacterRenderer());
}
auto nm2 = std::chrono::high_resolution_clock::now();
npcMgrTime += std::chrono::duration<float, std::milli>(nm2 - nm1).count();
@ -493,6 +484,8 @@ void Application::update(float deltaTime) {
// Sync character render position ↔ canonical WoW coords each frame
if (renderer && gameHandler) {
bool onTransport = gameHandler->isOnTransport();
if (onTaxi) {
auto playerEntity = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid());
if (playerEntity) {
@ -503,6 +496,18 @@ void Application::update(float deltaTime) {
float yawDeg = glm::degrees(playerEntity->getOrientation()) + 90.0f;
renderer->setCharacterYaw(yawDeg);
}
} else if (onTransport) {
// Transport mode: compose world position from transport transform + local offset
glm::vec3 canonical = gameHandler->getComposedWorldPosition();
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
renderer->getCharacterPosition() = renderPos;
// Update camera follow target
if (renderer->getCameraController()) {
glm::vec3* followTarget = renderer->getCameraController()->getFollowTargetMutable();
if (followTarget) {
*followTarget = renderPos;
}
}
} else {
glm::vec3 renderPos = renderer->getCharacterPosition();
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
@ -1487,93 +1492,6 @@ void Application::loadEquippedWeapons() {
}
}
void Application::spawnNpcs() {
if (npcsSpawned) return;
LOG_INFO("spawnNpcs: checking preconditions...");
if (!assetManager || !assetManager->isInitialized()) {
LOG_INFO("spawnNpcs: assetManager not ready");
return;
}
if (!renderer || !renderer->getCharacterRenderer() || !renderer->getCamera()) {
LOG_INFO("spawnNpcs: renderer not ready");
return;
}
if (!gameHandler) {
LOG_INFO("spawnNpcs: gameHandler not ready");
return;
}
LOG_INFO("spawnNpcs: spawning NPCs...");
if (npcManager) {
npcManager->clear(renderer->getCharacterRenderer(), &gameHandler->getEntityManager());
}
npcManager = std::make_unique<game::NpcManager>();
glm::vec3 playerSpawnGL = renderer->getCharacterPosition();
glm::vec3 playerCanonical = core::coords::renderToCanonical(playerSpawnGL);
LOG_INFO("spawnNpcs: player position GL=(", playerSpawnGL.x, ",", playerSpawnGL.y, ",", playerSpawnGL.z,
") canonical=(", playerCanonical.x, ",", playerCanonical.y, ",", playerCanonical.z, ")");
std::string mapName = "Azeroth";
if (auto* minimap = renderer->getMinimap()) {
mapName = minimap->getMapName();
}
npcManager->initialize(assetManager.get(),
renderer->getCharacterRenderer(),
gameHandler->getEntityManager(),
mapName,
playerCanonical,
renderer->getTerrainManager());
// If the player WoW position hasn't been set by the server yet (offline mode),
// derive it from the camera so targeting distance calculations work.
const auto& movement = gameHandler->getMovementInfo();
if (movement.x == 0.0f && movement.y == 0.0f && movement.z == 0.0f) {
glm::vec3 canonical = playerCanonical;
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
}
// Set NPC animation callbacks (works for both single-player and online creatures)
if (gameHandler && npcManager) {
auto* npcMgr = npcManager.get();
auto* cr = renderer->getCharacterRenderer();
auto* app = this;
gameHandler->setNpcDeathCallback([npcMgr, cr, app](uint64_t guid) {
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
if (instanceId == 0) {
auto it = app->creatureInstances_.find(guid);
if (it != app->creatureInstances_.end()) instanceId = it->second;
}
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death
}
});
gameHandler->setNpcRespawnCallback([npcMgr, cr, app](uint64_t guid) {
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
if (instanceId == 0) {
auto it = app->creatureInstances_.find(guid);
if (it != app->creatureInstances_.end()) instanceId = it->second;
}
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle
}
});
gameHandler->setNpcSwingCallback([npcMgr, cr, app](uint64_t guid) {
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
if (instanceId == 0) {
auto it = app->creatureInstances_.find(guid);
if (it != app->creatureInstances_.end()) instanceId = it->second;
}
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1
}
});
}
npcsSpawned = true;
LOG_INFO("NPCs spawned for in-game session");
}
void Application::buildFactionHostilityMap(uint8_t playerRace) {
if (!assetManager || !assetManager->isInitialized() || !gameHandler) return;
@ -1901,6 +1819,36 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
loadingScreen.shutdown();
}
// Set up test transport (development feature)
setupTestTransport();
// Set up NPC animation callbacks (for online creatures)
if (gameHandler && renderer && renderer->getCharacterRenderer()) {
auto* cr = renderer->getCharacterRenderer();
auto* app = this;
gameHandler->setNpcDeathCallback([cr, app](uint64_t guid) {
auto it = app->creatureInstances_.find(guid);
if (it != app->creatureInstances_.end() && cr) {
cr->playAnimation(it->second, 1, false); // animation ID 1 = Death
}
});
gameHandler->setNpcRespawnCallback([cr, app](uint64_t guid) {
auto it = app->creatureInstances_.find(guid);
if (it != app->creatureInstances_.end() && cr) {
cr->playAnimation(it->second, 0, true); // animation ID 0 = Idle
}
});
gameHandler->setNpcSwingCallback([cr, app](uint64_t guid) {
auto it = app->creatureInstances_.find(guid);
if (it != app->creatureInstances_.end() && cr) {
cr->playAnimation(it->second, 16, false); // animation ID 16 = Attack1
}
});
}
// Set game state
setState(AppState::IN_GAME);
}
@ -2167,9 +2115,6 @@ bool Application::getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, fl
auto it = creatureInstances_.find(guid);
if (it != creatureInstances_.end()) instanceId = it->second;
}
if (instanceId == 0 && npcManager) {
instanceId = npcManager->findRenderInstanceId(guid);
}
if (instanceId == 0) return false;
return renderer->getCharacterRenderer()->getInstanceBounds(instanceId, outCenter, outRadius);
@ -2380,6 +2325,25 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Convert canonical → render coordinates
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
// Smart filtering for bad spawn data:
// - If over ocean AND at continental height (Z > 50): bad data, skip
// - If over ocean AND near sea level (Z <= 50): water creature, allow
// - If over land: snap to terrain height
if (renderer->getTerrainManager()) {
auto terrainH = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
if (!terrainH) {
// No terrain at this X,Y position (ocean/void)
if (z > 50.0f) {
// High altitude over ocean = bad spawn data (e.g., bears at Z=94 over water)
return;
}
// Low altitude = probably legitimate water creature, allow spawn at original Z
} else {
// Valid terrain found - snap to terrain height
renderPos.z = *terrainH + 0.1f;
}
}
// Convert canonical WoW orientation (0=north) -> render yaw (0=west)
float renderYaw = orientation + glm::radians(90.0f);
@ -3147,5 +3111,130 @@ void Application::updateQuestMarkers() {
}
}
void Application::setupTestTransport() {
if (!gameHandler || !renderer || !assetManager) return;
auto* transportManager = gameHandler->getTransportManager();
auto* wmoRenderer = renderer->getWMORenderer();
if (!transportManager || !wmoRenderer) return;
LOG_INFO("========================================");
LOG_INFO(" SETTING UP TEST TRANSPORT");
LOG_INFO("========================================");
// Connect transport manager to WMO renderer
transportManager->setWMORenderer(wmoRenderer);
// Define a simple circular path around Stormwind harbor (canonical coordinates)
// These coordinates are approximate - adjust based on actual harbor layout
std::vector<glm::vec3> harborPath = {
{-8833.0f, 628.0f, 94.0f}, // Start point (Stormwind harbor)
{-8900.0f, 650.0f, 94.0f}, // Move west
{-8950.0f, 700.0f, 94.0f}, // Northwest
{-8950.0f, 780.0f, 94.0f}, // North
{-8900.0f, 830.0f, 94.0f}, // Northeast
{-8833.0f, 850.0f, 94.0f}, // East
{-8766.0f, 830.0f, 94.0f}, // Southeast
{-8716.0f, 780.0f, 94.0f}, // South
{-8716.0f, 700.0f, 94.0f}, // Southwest
{-8766.0f, 650.0f, 94.0f}, // Back to start direction
};
// Register the path with transport manager
uint32_t pathId = 1;
float speed = 12.0f; // 12 units/sec (slower than taxi for a leisurely boat ride)
transportManager->loadPathFromNodes(pathId, harborPath, true, speed);
LOG_INFO("Registered transport path ", pathId, " with ", harborPath.size(), " waypoints, speed=", speed);
// Try to load a transport WMO model
// Common transport WMOs: Transportship.wmo (generic ship)
std::string transportWmoPath = "Transports\\Transportship\\Transportship.wmo";
auto wmoData = assetManager->readFile(transportWmoPath);
if (wmoData.empty()) {
LOG_WARNING("Could not load transport WMO: ", transportWmoPath);
LOG_INFO("Trying alternative: Boat transport");
transportWmoPath = "Transports\\Boat\\Boat.wmo";
wmoData = assetManager->readFile(transportWmoPath);
}
if (wmoData.empty()) {
LOG_WARNING("No transport WMO found - test transport disabled");
LOG_INFO("Available transport WMOs are typically in Transports\\ directory");
return;
}
// Load WMO model
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
LOG_INFO("Transport WMO root loaded: ", transportWmoPath, " nGroups=", wmoModel.nGroups);
// Load WMO groups
int loadedGroups = 0;
if (wmoModel.nGroups > 0) {
std::string basePath = transportWmoPath.substr(0, transportWmoPath.size() - 4);
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char groupSuffix[16];
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
std::string groupPath = basePath + groupSuffix;
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
if (!groupData.empty()) {
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
loadedGroups++;
} else {
LOG_WARNING(" Failed to load WMO group ", gi, " for: ", basePath);
}
}
}
if (loadedGroups == 0 && wmoModel.nGroups > 0) {
LOG_WARNING("Failed to load any WMO groups for transport");
return;
}
// Load WMO into renderer
uint32_t wmoModelId = 99999; // Use high ID to avoid conflicts
if (!wmoRenderer->loadModel(wmoModel, wmoModelId)) {
LOG_WARNING("Failed to load transport WMO model into renderer");
return;
}
// Create WMO instance at first waypoint (convert canonical to render coords)
glm::vec3 startCanonical = harborPath[0];
glm::vec3 startRender = core::coords::canonicalToRender(startCanonical);
uint32_t wmoInstanceId = wmoRenderer->createInstance(wmoModelId, startRender,
glm::vec3(0.0f, 0.0f, 0.0f), 1.0f);
if (wmoInstanceId == 0) {
LOG_WARNING("Failed to create transport WMO instance");
return;
}
// Register transport with transport manager
uint64_t transportGuid = 0x1000000000000001ULL; // Fake GUID for test
transportManager->registerTransport(transportGuid, wmoInstanceId, pathId);
// Optional: Set deck bounds (rough estimate for a ship deck)
transportManager->setDeckBounds(transportGuid,
glm::vec3(-15.0f, -30.0f, 0.0f),
glm::vec3(15.0f, 30.0f, 10.0f));
LOG_INFO("========================================");
LOG_INFO("Test transport registered:");
LOG_INFO(" GUID: 0x", std::hex, transportGuid, std::dec);
LOG_INFO(" WMO Instance: ", wmoInstanceId);
LOG_INFO(" Path: ", pathId, " (", harborPath.size(), " waypoints)");
LOG_INFO(" Speed: ", speed, " units/sec");
LOG_INFO("========================================");
LOG_INFO("");
LOG_INFO("To board the transport, use console command:");
LOG_INFO(" /transport board");
LOG_INFO("To disembark:");
LOG_INFO(" /transport leave");
LOG_INFO("========================================");
}
} // namespace core
} // namespace wowee