mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add taxi system, fix WMO interior lighting, ramp collision, and /unstuck
- Implement flight path system: SMSG_SHOWTAXINODES parser, CMSG_ACTIVATETAXIEXPRESS builder, BFS multi-hop pathfinding through TaxiNodes/TaxiPath DBC, taxi destination UI, movement blocking during flight - Fix WMO interiors too dark by boosting vertex color lighting multiplier - Dim M2 objects inside WMO interiors (rugs, furniture) via per-instance interior detection - Fix ramp/stair clipping by lowering wall collision normal threshold from 0.85 to 0.55 - Restore 5-sample cardinal footprint for ground detection to fix rug slipping - Fix /unstuck command to reset player Z to WMO/terrain floor height - Handle MSG_MOVE_TELEPORT_ACK and SMSG_TRANSFER_PENDING for hearthstone teleports - Fix spawning under Stormwind with online-mode camera controller reset
This commit is contained in:
parent
c5a1fe927b
commit
3c2a728ec4
15 changed files with 691 additions and 108 deletions
|
|
@ -364,6 +364,11 @@ public:
|
|||
using WorldEntryCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
||||
void setWorldEntryCallback(WorldEntryCallback cb) { worldEntryCallback_ = std::move(cb); }
|
||||
|
||||
// Unstuck callback (resets player Z to floor height)
|
||||
using UnstuckCallback = std::function<void()>;
|
||||
void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); }
|
||||
void unstuck();
|
||||
|
||||
// Creature spawn callback (online mode - triggered when creature enters view)
|
||||
// Parameters: guid, displayId, x, y, z (canonical), orientation
|
||||
using CreatureSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
|
|
@ -450,6 +455,27 @@ public:
|
|||
}
|
||||
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
|
||||
|
||||
// Taxi / Flight Paths
|
||||
bool isTaxiWindowOpen() const { return taxiWindowOpen_; }
|
||||
void closeTaxi();
|
||||
void activateTaxi(uint32_t destNodeId);
|
||||
bool isOnTaxiFlight() const { return onTaxiFlight_; }
|
||||
const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; }
|
||||
uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; }
|
||||
|
||||
struct TaxiNode {
|
||||
uint32_t id = 0;
|
||||
uint32_t mapId = 0;
|
||||
float x = 0, y = 0, z = 0;
|
||||
std::string name;
|
||||
};
|
||||
struct TaxiPathEdge {
|
||||
uint32_t pathId = 0;
|
||||
uint32_t fromNode = 0, toNode = 0;
|
||||
uint32_t cost = 0;
|
||||
};
|
||||
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
|
||||
|
||||
// Vendor
|
||||
void openVendor(uint64_t npcGuid);
|
||||
void closeVendor();
|
||||
|
|
@ -601,6 +627,14 @@ private:
|
|||
void handleListInventory(network::Packet& packet);
|
||||
void addMoneyCopper(uint32_t amount);
|
||||
|
||||
// ---- Teleport handler ----
|
||||
void handleTeleportAck(network::Packet& packet);
|
||||
|
||||
// ---- Taxi handlers ----
|
||||
void handleShowTaxiNodes(network::Packet& packet);
|
||||
void handleActivateTaxiReply(network::Packet& packet);
|
||||
void loadTaxiDbc();
|
||||
|
||||
// ---- Server info handlers ----
|
||||
void handleQueryTimeResponse(network::Packet& packet);
|
||||
void handlePlayedTime(network::Packet& packet);
|
||||
|
|
@ -686,8 +720,9 @@ private:
|
|||
float pingInterval = 30.0f; // Ping interval (30 seconds)
|
||||
uint32_t lastLatency = 0; // Last measured latency (milliseconds)
|
||||
|
||||
// Player GUID
|
||||
// Player GUID and map
|
||||
uint64_t playerGuid = 0;
|
||||
uint32_t currentMapId_ = 0;
|
||||
|
||||
// ---- Phase 1: Name caches ----
|
||||
std::unordered_map<uint64_t, std::string> playerNameCache;
|
||||
|
|
@ -742,6 +777,7 @@ private:
|
|||
|
||||
// ---- Phase 3: Spells ----
|
||||
WorldEntryCallback worldEntryCallback_;
|
||||
UnstuckCallback unstuckCallback_;
|
||||
CreatureSpawnCallback creatureSpawnCallback_;
|
||||
CreatureDespawnCallback creatureDespawnCallback_;
|
||||
CreatureMoveCallback creatureMoveCallback_;
|
||||
|
|
@ -800,6 +836,15 @@ private:
|
|||
return it != factionHostileMap_.end() ? it->second : true; // default hostile if unknown
|
||||
}
|
||||
|
||||
// Taxi / Flight Paths
|
||||
std::unordered_map<uint32_t, TaxiNode> taxiNodes_;
|
||||
std::vector<TaxiPathEdge> taxiPathEdges_;
|
||||
bool taxiDbcLoaded_ = false;
|
||||
bool taxiWindowOpen_ = false;
|
||||
ShowTaxiNodesData currentTaxiData_;
|
||||
uint64_t taxiNpcGuid_ = 0;
|
||||
bool onTaxiFlight_ = false;
|
||||
|
||||
// Vendor
|
||||
bool vendorWindowOpen = false;
|
||||
ListInventoryData currentVendorItems;
|
||||
|
|
|
|||
|
|
@ -236,6 +236,15 @@ enum class Opcode : uint16_t {
|
|||
// ---- Death/Respawn ----
|
||||
CMSG_REPOP_REQUEST = 0x015A,
|
||||
CMSG_SPIRIT_HEALER_ACTIVATE = 0x0176,
|
||||
|
||||
// ---- Teleport / Transfer ----
|
||||
MSG_MOVE_TELEPORT_ACK = 0x0C7,
|
||||
SMSG_TRANSFER_PENDING = 0x003F,
|
||||
|
||||
// ---- Taxi / Flight Paths ----
|
||||
SMSG_SHOWTAXINODES = 0x01A9,
|
||||
SMSG_ACTIVATETAXIREPLY = 0x01AE,
|
||||
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
|||
|
|
@ -1652,6 +1652,48 @@ public:
|
|||
static bool parse(network::Packet& packet, ListInventoryData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Taxi / Flight Paths
|
||||
// ============================================================
|
||||
|
||||
static constexpr uint32_t TLK_TAXI_MASK_SIZE = 12;
|
||||
|
||||
/** SMSG_SHOWTAXINODES data */
|
||||
struct ShowTaxiNodesData {
|
||||
uint32_t windowInfo = 0; // 1 = show window
|
||||
uint64_t npcGuid = 0;
|
||||
uint32_t nearestNode = 0; // Taxi node player is at
|
||||
uint32_t nodeMask[TLK_TAXI_MASK_SIZE] = {};
|
||||
bool isNodeKnown(uint32_t nodeId) const {
|
||||
uint32_t idx = nodeId / 32;
|
||||
uint32_t bit = nodeId % 32;
|
||||
return idx < TLK_TAXI_MASK_SIZE && (nodeMask[idx] & (1u << bit));
|
||||
}
|
||||
};
|
||||
|
||||
/** SMSG_SHOWTAXINODES parser */
|
||||
class ShowTaxiNodesParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, ShowTaxiNodesData& data);
|
||||
};
|
||||
|
||||
/** SMSG_ACTIVATETAXIREPLY data */
|
||||
struct ActivateTaxiReplyData {
|
||||
uint32_t result = 0; // 0 = OK
|
||||
};
|
||||
|
||||
/** SMSG_ACTIVATETAXIREPLY parser */
|
||||
class ActivateTaxiReplyParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, ActivateTaxiReplyData& data);
|
||||
};
|
||||
|
||||
/** CMSG_ACTIVATETAXIEXPRESS packet builder */
|
||||
class ActivateTaxiExpressPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t npcGuid, const std::vector<uint32_t>& pathNodes);
|
||||
};
|
||||
|
||||
/** CMSG_REPOP_REQUEST packet builder */
|
||||
class RepopRequestPacket {
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public:
|
|||
}
|
||||
|
||||
void reset();
|
||||
void setOnlineMode(bool online) { onlineMode = online; }
|
||||
void startIntroPan(float durationSec = 2.8f, float orbitDegrees = 140.0f);
|
||||
bool isIntroActive() const { return introActive; }
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ public:
|
|||
bool isSitting() const { return sitting; }
|
||||
bool isSwimming() const { return swimming; }
|
||||
const glm::vec3* getFollowTarget() const { return followTarget; }
|
||||
glm::vec3* getFollowTargetMutable() { return followTarget; }
|
||||
|
||||
// Movement callback for sending opcodes to server
|
||||
using MovementCallback = std::function<void(uint32_t opcode)>;
|
||||
|
|
@ -134,10 +136,6 @@ private:
|
|||
static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer
|
||||
static constexpr float COYOTE_TIME = 0.10f; // 100ms grace after leaving ground
|
||||
|
||||
// Cached floor height between frames (skip expensive probes when barely moving)
|
||||
std::optional<float> cachedFloorHeight;
|
||||
glm::vec2 cachedFloorPos = glm::vec2(0.0f);
|
||||
|
||||
// Cached isInsideWMO result (throttled to avoid per-frame cost)
|
||||
bool cachedInsideWMO = false;
|
||||
int insideWMOCheckCounter = 0;
|
||||
|
|
@ -188,6 +186,9 @@ private:
|
|||
static constexpr float WOW_GRAVITY = -19.29f;
|
||||
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
|
||||
|
||||
// Online mode: trust server position, don't prefer outdoors over WMO floors
|
||||
bool onlineMode = false;
|
||||
|
||||
// Default spawn position (Goldshire Inn)
|
||||
glm::vec3 defaultPosition = glm::vec3(-9464.0f, 62.0f, 200.0f);
|
||||
float defaultYaw = 0.0f;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ namespace rendering {
|
|||
|
||||
class Shader;
|
||||
class Camera;
|
||||
class WMORenderer;
|
||||
|
||||
/**
|
||||
* GPU representation of an M2 model
|
||||
|
|
@ -276,7 +277,10 @@ public:
|
|||
}
|
||||
void clearShadowMap() { shadowEnabled = false; }
|
||||
|
||||
void setWMORenderer(WMORenderer* wmo) { wmoRenderer = wmo; }
|
||||
|
||||
private:
|
||||
WMORenderer* wmoRenderer = nullptr;
|
||||
pipeline::AssetManager* assetManager = nullptr;
|
||||
std::unique_ptr<Shader> shader;
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,12 @@ public:
|
|||
*/
|
||||
bool isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId = nullptr) const;
|
||||
|
||||
/**
|
||||
* Check if a position is inside an interior WMO group (flag 0x2000).
|
||||
* Used to dim M2 lighting for doodads placed indoors.
|
||||
*/
|
||||
bool isInsideInteriorWMO(float glX, float glY, float glZ) const;
|
||||
|
||||
/**
|
||||
* Raycast against WMO bounding boxes for camera collision
|
||||
* @param origin Ray origin (e.g., character head position)
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ private:
|
|||
void renderQuestRequestItemsWindow(game::GameHandler& gameHandler);
|
||||
void renderQuestOfferRewardWindow(game::GameHandler& gameHandler);
|
||||
void renderVendorWindow(game::GameHandler& gameHandler);
|
||||
void renderTaxiWindow(game::GameHandler& gameHandler);
|
||||
void renderDeathScreen(game::GameHandler& gameHandler);
|
||||
void renderEscapeMenu();
|
||||
void renderSettingsWindow();
|
||||
|
|
|
|||
|
|
@ -530,6 +530,38 @@ void Application::setupUICallbacks() {
|
|||
loadOnlineWorldTerrain(mapId, x, y, z);
|
||||
});
|
||||
|
||||
// Unstuck callback — snap player Z to WMO/terrain floor height
|
||||
gameHandler->setUnstuckCallback([this]() {
|
||||
if (!renderer || !renderer->getCameraController()) return;
|
||||
auto* cc = renderer->getCameraController();
|
||||
auto* ft = cc->getFollowTargetMutable();
|
||||
if (!ft) return;
|
||||
// Probe floor at current XY from high up to find WMO floors above
|
||||
auto* wmo = renderer->getWMORenderer();
|
||||
auto* terrain = renderer->getTerrainManager();
|
||||
float bestZ = ft->z;
|
||||
bool found = false;
|
||||
// Probe from well above to catch WMO floors like Stormwind
|
||||
float probeZ = ft->z + 200.0f;
|
||||
if (wmo) {
|
||||
auto wmoH = wmo->getFloorHeight(ft->x, ft->y, probeZ);
|
||||
if (wmoH) {
|
||||
bestZ = *wmoH;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (terrain) {
|
||||
auto terrH = terrain->getHeightAt(ft->x, ft->y);
|
||||
if (terrH && (!found || *terrH > bestZ)) {
|
||||
bestZ = *terrH;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
ft->z = bestZ;
|
||||
}
|
||||
});
|
||||
|
||||
// Faction hostility map is built in buildFactionHostilityMap() when character enters world
|
||||
|
||||
// Creature spawn callback (online mode) - spawn creature models
|
||||
|
|
@ -1336,6 +1368,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
|
||||
// Set camera position
|
||||
if (renderer->getCameraController()) {
|
||||
renderer->getCameraController()->setOnlineMode(true);
|
||||
renderer->getCameraController()->setDefaultSpawn(spawnRender, 0.0f, 15.0f);
|
||||
renderer->getCameraController()->reset();
|
||||
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
|
||||
|
|
@ -1361,21 +1394,19 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
}
|
||||
}
|
||||
|
||||
// Spawn player model for online mode
|
||||
// Spawn player model for online mode (skip if already spawned, e.g. teleport)
|
||||
if (gameHandler) {
|
||||
const game::Character* activeChar = gameHandler->getActiveCharacter();
|
||||
if (activeChar) {
|
||||
playerRace_ = activeChar->race;
|
||||
playerGender_ = activeChar->gender;
|
||||
playerClass_ = activeChar->characterClass;
|
||||
spawnSnapToGround = false;
|
||||
playerCharacterSpawned = false;
|
||||
spawnPlayerCharacter();
|
||||
if (!playerCharacterSpawned) {
|
||||
playerRace_ = activeChar->race;
|
||||
playerGender_ = activeChar->gender;
|
||||
playerClass_ = activeChar->characterClass;
|
||||
spawnSnapToGround = false;
|
||||
spawnPlayerCharacter();
|
||||
}
|
||||
renderer->getCharacterPosition() = spawnRender;
|
||||
LOG_INFO("Spawned online player model: ", activeChar->name,
|
||||
" (race=", static_cast<int>(playerRace_),
|
||||
", gender=", static_cast<int>(playerGender_),
|
||||
") at render pos (", spawnRender.x, ", ", spawnRender.y, ", ", spawnRender.z, ")");
|
||||
LOG_INFO("Online player at render pos (", spawnRender.x, ", ", spawnRender.y, ", ", spawnRender.z, ")");
|
||||
} else {
|
||||
LOG_WARNING("No active character found for player model spawning");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,18 @@ void GameHandler::update(float deltaTime) {
|
|||
// Update combat text (Phase 2)
|
||||
updateCombatText(deltaTime);
|
||||
|
||||
// Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared
|
||||
if (onTaxiFlight_) {
|
||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
if (playerEntity && playerEntity->getType() == ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<Unit>(playerEntity);
|
||||
if ((unit->getUnitFlags() & 0x00000100) == 0) {
|
||||
onTaxiFlight_ = false;
|
||||
LOG_INFO("Taxi flight landed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update entity movement interpolation (keeps targeting in sync with visuals)
|
||||
for (auto& [guid, entity] : entityManager.getEntities()) {
|
||||
entity->updateMovement(deltaTime);
|
||||
|
|
@ -238,7 +250,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
|
||||
case Opcode::SMSG_LOGIN_VERIFY_WORLD:
|
||||
if (state == WorldState::ENTERING_WORLD) {
|
||||
if (state == WorldState::ENTERING_WORLD || state == WorldState::IN_WORLD) {
|
||||
handleLoginVerifyWorld(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_LOGIN_VERIFY_WORLD in state: ", (int)state);
|
||||
|
|
@ -621,6 +633,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec);
|
||||
break;
|
||||
|
||||
// ---- Teleport / Transfer ----
|
||||
case Opcode::MSG_MOVE_TELEPORT_ACK:
|
||||
handleTeleportAck(packet);
|
||||
break;
|
||||
case Opcode::SMSG_TRANSFER_PENDING:
|
||||
LOG_INFO("SMSG_TRANSFER_PENDING received - map transfer incoming");
|
||||
break;
|
||||
|
||||
// ---- Taxi / Flight Paths ----
|
||||
case Opcode::SMSG_SHOWTAXINODES:
|
||||
handleShowTaxiNodes(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ACTIVATETAXIREPLY:
|
||||
handleActivateTaxiReply(packet);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
||||
break;
|
||||
|
|
@ -964,7 +992,8 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Successfully entered the world!
|
||||
// Successfully entered the world (or teleported)
|
||||
currentMapId_ = data.mapId;
|
||||
setState(WorldState::IN_WORLD);
|
||||
|
||||
LOG_INFO("========================================");
|
||||
|
|
@ -974,7 +1003,6 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
LOG_INFO("Position: (", data.x, ", ", data.y, ", ", data.z, ")");
|
||||
LOG_INFO("Orientation: ", data.orientation, " radians");
|
||||
LOG_INFO("Player is now in the game world");
|
||||
addSystemChatMessage("You have entered the world.");
|
||||
|
||||
// Initialize movement info with world entry position (server → canonical)
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(data.x, data.y, data.z));
|
||||
|
|
@ -1073,6 +1101,9 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Block movement during taxi flight
|
||||
if (onTaxiFlight_) return;
|
||||
|
||||
// Use real millisecond timestamp (server validates for anti-cheat)
|
||||
static auto startTime = std::chrono::steady_clock::now();
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
|
@ -3617,6 +3648,13 @@ void GameHandler::useItemById(uint32_t itemId) {
|
|||
}
|
||||
}
|
||||
|
||||
void GameHandler::unstuck() {
|
||||
if (unstuckCallback_) {
|
||||
unstuckCallback_();
|
||||
addSystemChatMessage("Unstuck: position reset to floor.");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||
if (!LootResponseParser::parse(packet, currentLoot)) return;
|
||||
lootWindowOpen = true;
|
||||
|
|
@ -3774,6 +3812,221 @@ void GameHandler::addSystemChatMessage(const std::string& message) {
|
|||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Teleport Handler
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::handleTeleportAck(network::Packet& packet) {
|
||||
// MSG_MOVE_TELEPORT_ACK (server→client): packedGuid + u32 counter + u32 time
|
||||
// followed by movement info with the new position
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
LOG_WARNING("MSG_MOVE_TELEPORT_ACK too short");
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||
uint32_t counter = packet.readUInt32();
|
||||
|
||||
// Read the movement info embedded in the teleport
|
||||
// Format: u32 flags, u16 flags2, u32 time, float x, float y, float z, float o
|
||||
if (packet.getSize() - packet.getReadPos() < 4 + 2 + 4 + 4 * 4) {
|
||||
LOG_WARNING("MSG_MOVE_TELEPORT_ACK: not enough data for movement info");
|
||||
return;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // moveFlags
|
||||
packet.readUInt16(); // moveFlags2
|
||||
uint32_t moveTime = packet.readUInt32();
|
||||
float serverX = packet.readFloat();
|
||||
float serverY = packet.readFloat();
|
||||
float serverZ = packet.readFloat();
|
||||
float orientation = packet.readFloat();
|
||||
|
||||
LOG_INFO("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec,
|
||||
" counter=", counter,
|
||||
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")");
|
||||
|
||||
// Update our position
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
||||
movementInfo.x = canonical.x;
|
||||
movementInfo.y = canonical.y;
|
||||
movementInfo.z = canonical.z;
|
||||
movementInfo.orientation = orientation;
|
||||
movementInfo.flags = 0;
|
||||
|
||||
// Send the ack back to the server
|
||||
// Client→server MSG_MOVE_TELEPORT_ACK: u64 guid + u32 counter + u32 time
|
||||
if (socket) {
|
||||
network::Packet ack(static_cast<uint16_t>(Opcode::MSG_MOVE_TELEPORT_ACK));
|
||||
// Write packed guid
|
||||
uint8_t mask = 0;
|
||||
uint8_t bytes[8];
|
||||
int byteCount = 0;
|
||||
uint64_t g = playerGuid;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t b = static_cast<uint8_t>(g & 0xFF);
|
||||
g >>= 8;
|
||||
if (b != 0) {
|
||||
mask |= (1 << i);
|
||||
bytes[byteCount++] = b;
|
||||
}
|
||||
}
|
||||
ack.writeUInt8(mask);
|
||||
for (int i = 0; i < byteCount; i++) {
|
||||
ack.writeUInt8(bytes[i]);
|
||||
}
|
||||
ack.writeUInt32(counter);
|
||||
ack.writeUInt32(moveTime);
|
||||
socket->send(ack);
|
||||
LOG_INFO("Sent MSG_MOVE_TELEPORT_ACK response");
|
||||
}
|
||||
|
||||
// Notify application to reload terrain at new position
|
||||
if (worldEntryCallback_) {
|
||||
worldEntryCallback_(currentMapId_, serverX, serverY, serverZ);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Taxi / Flight Path Handlers
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::loadTaxiDbc() {
|
||||
if (taxiDbcLoaded_) return;
|
||||
taxiDbcLoaded_ = true;
|
||||
|
||||
auto* am = core::Application::getInstance().getAssetManager();
|
||||
if (!am || !am->isInitialized()) return;
|
||||
|
||||
// Load TaxiNodes.dbc: 0=ID, 1=mapId, 2=x, 3=y, 4=z, 6=name
|
||||
auto nodesDbc = am->loadDBC("TaxiNodes.dbc");
|
||||
if (nodesDbc && nodesDbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < nodesDbc->getRecordCount(); i++) {
|
||||
TaxiNode node;
|
||||
node.id = nodesDbc->getUInt32(i, 0);
|
||||
node.mapId = nodesDbc->getUInt32(i, 1);
|
||||
node.x = nodesDbc->getFloat(i, 2);
|
||||
node.y = nodesDbc->getFloat(i, 3);
|
||||
node.z = nodesDbc->getFloat(i, 4);
|
||||
node.name = nodesDbc->getString(i, 6);
|
||||
if (node.id > 0) {
|
||||
taxiNodes_[node.id] = std::move(node);
|
||||
}
|
||||
}
|
||||
LOG_INFO("Loaded ", taxiNodes_.size(), " taxi nodes from TaxiNodes.dbc");
|
||||
} else {
|
||||
LOG_WARNING("Could not load TaxiNodes.dbc");
|
||||
}
|
||||
|
||||
// Load TaxiPath.dbc: 0=pathId, 1=fromNode, 2=toNode, 3=cost
|
||||
auto pathDbc = am->loadDBC("TaxiPath.dbc");
|
||||
if (pathDbc && pathDbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < pathDbc->getRecordCount(); i++) {
|
||||
TaxiPathEdge edge;
|
||||
edge.pathId = pathDbc->getUInt32(i, 0);
|
||||
edge.fromNode = pathDbc->getUInt32(i, 1);
|
||||
edge.toNode = pathDbc->getUInt32(i, 2);
|
||||
edge.cost = pathDbc->getUInt32(i, 3);
|
||||
taxiPathEdges_.push_back(edge);
|
||||
}
|
||||
LOG_INFO("Loaded ", taxiPathEdges_.size(), " taxi path edges from TaxiPath.dbc");
|
||||
} else {
|
||||
LOG_WARNING("Could not load TaxiPath.dbc");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleShowTaxiNodes(network::Packet& packet) {
|
||||
ShowTaxiNodesData data;
|
||||
if (!ShowTaxiNodesParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_SHOWTAXINODES");
|
||||
return;
|
||||
}
|
||||
|
||||
loadTaxiDbc();
|
||||
|
||||
currentTaxiData_ = data;
|
||||
taxiNpcGuid_ = data.npcGuid;
|
||||
taxiWindowOpen_ = true;
|
||||
gossipWindowOpen = false;
|
||||
LOG_INFO("Taxi window opened, nearest node=", data.nearestNode);
|
||||
}
|
||||
|
||||
void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
|
||||
ActivateTaxiReplyData data;
|
||||
if (!ActivateTaxiReplyParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_ACTIVATETAXIREPLY");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.result == 0) {
|
||||
onTaxiFlight_ = true;
|
||||
taxiWindowOpen_ = false;
|
||||
LOG_INFO("Taxi flight started!");
|
||||
} else {
|
||||
LOG_WARNING("Taxi activation failed, result=", data.result);
|
||||
addSystemChatMessage("Cannot take that flight path.");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::closeTaxi() {
|
||||
taxiWindowOpen_ = false;
|
||||
}
|
||||
|
||||
void GameHandler::activateTaxi(uint32_t destNodeId) {
|
||||
if (!socket || state != WorldState::IN_WORLD) return;
|
||||
|
||||
uint32_t startNode = currentTaxiData_.nearestNode;
|
||||
if (startNode == 0 || destNodeId == 0 || startNode == destNodeId) return;
|
||||
|
||||
// BFS to find path from startNode to destNodeId through known nodes
|
||||
// Build adjacency list from edges where both nodes are known
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> adj;
|
||||
for (const auto& edge : taxiPathEdges_) {
|
||||
if (currentTaxiData_.isNodeKnown(edge.fromNode) && currentTaxiData_.isNodeKnown(edge.toNode)) {
|
||||
adj[edge.fromNode].push_back(edge.toNode);
|
||||
}
|
||||
}
|
||||
|
||||
// BFS
|
||||
std::unordered_map<uint32_t, uint32_t> parent;
|
||||
std::deque<uint32_t> queue;
|
||||
queue.push_back(startNode);
|
||||
parent[startNode] = startNode;
|
||||
|
||||
bool found = false;
|
||||
while (!queue.empty()) {
|
||||
uint32_t cur = queue.front();
|
||||
queue.pop_front();
|
||||
if (cur == destNodeId) { found = true; break; }
|
||||
for (uint32_t next : adj[cur]) {
|
||||
if (parent.find(next) == parent.end()) {
|
||||
parent[next] = cur;
|
||||
queue.push_back(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
LOG_WARNING("No taxi path found from node ", startNode, " to ", destNodeId);
|
||||
addSystemChatMessage("No flight path available to that destination.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reconstruct path
|
||||
std::vector<uint32_t> path;
|
||||
for (uint32_t n = destNodeId; n != startNode; n = parent[n]) {
|
||||
path.push_back(n);
|
||||
}
|
||||
path.push_back(startNode);
|
||||
std::reverse(path.begin(), path.end());
|
||||
|
||||
LOG_INFO("Taxi path: ", path.size(), " nodes, from ", startNode, " to ", destNodeId);
|
||||
|
||||
auto pkt = ActivateTaxiExpressPacket::build(taxiNpcGuid_, path);
|
||||
socket->send(pkt);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Server Info Command Handlers
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -2591,5 +2591,48 @@ network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) {
|
|||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Taxi / Flight Paths
|
||||
// ============================================================
|
||||
|
||||
bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data) {
|
||||
if (packet.getSize() - packet.getReadPos() < 4 + 8 + 4 + TLK_TAXI_MASK_SIZE * 4) {
|
||||
LOG_ERROR("ShowTaxiNodesParser: packet too short");
|
||||
return false;
|
||||
}
|
||||
data.windowInfo = packet.readUInt32();
|
||||
data.npcGuid = packet.readUInt64();
|
||||
data.nearestNode = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) {
|
||||
data.nodeMask[i] = packet.readUInt32();
|
||||
}
|
||||
LOG_INFO("ShowTaxiNodes: window=", data.windowInfo, " npc=0x", std::hex, data.npcGuid, std::dec,
|
||||
" nearest=", data.nearestNode);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyData& data) {
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
LOG_ERROR("ActivateTaxiReplyParser: packet too short");
|
||||
return false;
|
||||
}
|
||||
data.result = packet.readUInt32();
|
||||
LOG_INFO("ActivateTaxiReply: result=", data.result);
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, const std::vector<uint32_t>& pathNodes) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ACTIVATETAXIEXPRESS));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(0); // totalCost (server recalculates)
|
||||
packet.writeUInt32(static_cast<uint32_t>(pathNodes.size()));
|
||||
for (uint32_t nodeId : pathNodes) {
|
||||
packet.writeUInt32(nodeId);
|
||||
}
|
||||
LOG_INFO("ActivateTaxiExpress: npc=0x", std::hex, npcGuid, std::dec,
|
||||
" nodes=", pathNodes.size());
|
||||
return packet;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -588,15 +588,22 @@ void CameraController::update(float deltaTime) {
|
|||
return base;
|
||||
};
|
||||
|
||||
// Use cached floor height if player hasn't moved much horizontally.
|
||||
float floorPosDist = glm::length(glm::vec2(targetPos.x, targetPos.y) - cachedFloorPos);
|
||||
// Sample center + 4 cardinal offsets so narrow M2 objects (rugs,
|
||||
// planks) are reliably detected. Take the highest result.
|
||||
std::optional<float> groundH;
|
||||
if (cachedFloorHeight && floorPosDist < 0.5f) {
|
||||
groundH = cachedFloorHeight;
|
||||
} else {
|
||||
groundH = sampleGround(targetPos.x, targetPos.y);
|
||||
cachedFloorHeight = groundH;
|
||||
cachedFloorPos = glm::vec2(targetPos.x, targetPos.y);
|
||||
{
|
||||
constexpr float FOOTPRINT = 0.4f;
|
||||
const glm::vec2 offsets[] = {
|
||||
{0.0f, 0.0f},
|
||||
{FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f},
|
||||
{0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT}
|
||||
};
|
||||
for (const auto& o : offsets) {
|
||||
auto h = sampleGround(targetPos.x + o.x, targetPos.y + o.y);
|
||||
if (h && (!groundH || *h > *groundH)) {
|
||||
groundH = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groundH) {
|
||||
|
|
@ -1081,13 +1088,18 @@ void CameraController::reset() {
|
|||
};
|
||||
|
||||
// Search nearby for a stable, non-steep spawn floor to avoid waterfall/ledge spawns.
|
||||
// In online mode, use a tight search radius since the server dictates position.
|
||||
float bestScore = std::numeric_limits<float>::max();
|
||||
glm::vec3 bestPos = spawnPos;
|
||||
bool foundBest = false;
|
||||
constexpr float radii[] = {0.0f, 6.0f, 12.0f, 18.0f, 24.0f, 32.0f};
|
||||
constexpr float radiiOffline[] = {0.0f, 6.0f, 12.0f, 18.0f, 24.0f, 32.0f};
|
||||
constexpr float radiiOnline[] = {0.0f, 2.0f};
|
||||
const float* radii = onlineMode ? radiiOnline : radiiOffline;
|
||||
const int radiiCount = onlineMode ? 2 : 6;
|
||||
constexpr int ANGLES = 16;
|
||||
constexpr float PI = 3.14159265f;
|
||||
for (float r : radii) {
|
||||
for (int ri = 0; ri < radiiCount; ri++) {
|
||||
float r = radii[ri];
|
||||
int steps = (r <= 0.01f) ? 1 : ANGLES;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
float a = (2.0f * PI * static_cast<float>(i)) / static_cast<float>(steps);
|
||||
|
|
@ -1128,8 +1140,9 @@ void CameraController::reset() {
|
|||
const glm::vec3 from(x, y, *h + 0.20f);
|
||||
const bool insideWMO = wmoRenderer->isInsideWMO(x, y, *h + 1.5f, nullptr);
|
||||
|
||||
// Prefer outdoors for default hearth-like spawn points.
|
||||
if (insideWMO) {
|
||||
// Prefer outdoors for default hearth-like spawn points (offline only).
|
||||
// In online mode, trust the server position even if inside a WMO.
|
||||
if (insideWMO && !onlineMode) {
|
||||
score += 120.0f;
|
||||
}
|
||||
|
||||
|
|
@ -1192,10 +1205,6 @@ void CameraController::reset() {
|
|||
lastGroundZ = spawnPos.z - 0.05f;
|
||||
}
|
||||
|
||||
// Invalidate inter-frame floor cache so the first frame probes fresh.
|
||||
cachedFloorHeight.reset();
|
||||
cachedFloorPos = glm::vec2(0.0f);
|
||||
|
||||
camera->setRotation(yaw, pitch);
|
||||
glm::vec3 forward3D = camera->getForward();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/texture.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
|
|
@ -275,6 +276,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
|||
uniform mat4 uLightSpaceMatrix;
|
||||
uniform bool uShadowEnabled;
|
||||
uniform float uShadowStrength;
|
||||
uniform bool uInteriorDarken;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
|
|
@ -306,41 +308,48 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
|||
vec3 normal = normalize(Normal);
|
||||
vec3 lightDir = normalize(uLightDir);
|
||||
|
||||
// Two-sided lighting for foliage
|
||||
float diff = max(abs(dot(normal, lightDir)), 0.3);
|
||||
vec3 result;
|
||||
if (uInteriorDarken) {
|
||||
// Interior: dim ambient, minimal directional light
|
||||
float diff = max(abs(dot(normal, lightDir)), 0.0) * 0.15;
|
||||
result = texColor.rgb * (0.55 + diff);
|
||||
} else {
|
||||
// Two-sided lighting for foliage
|
||||
float diff = max(abs(dot(normal, lightDir)), 0.3);
|
||||
|
||||
// Blinn-Phong specular
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity;
|
||||
// Blinn-Phong specular
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity;
|
||||
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
}
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
|
||||
vec3 ambient = uAmbientColor * texColor.rgb;
|
||||
vec3 diffuse = diff * texColor.rgb;
|
||||
|
||||
result = ambient + (diffuse + specular) * shadow;
|
||||
}
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
|
||||
vec3 ambient = uAmbientColor * texColor.rgb;
|
||||
vec3 diffuse = diff * texColor.rgb;
|
||||
|
||||
vec3 result = ambient + (diffuse + specular) * shadow;
|
||||
|
||||
// Fog
|
||||
float fogDist = length(uViewPos - FragPos);
|
||||
|
|
@ -1487,6 +1496,14 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
shader->setUniform("uModel", instance.modelMatrix);
|
||||
shader->setUniform("uFadeAlpha", fadeAlpha);
|
||||
|
||||
// Dim M2 objects inside WMO interiors
|
||||
bool interior = false;
|
||||
if (wmoRenderer && entry.distSq < 200.0f * 200.0f) {
|
||||
interior = wmoRenderer->isInsideInteriorWMO(
|
||||
instance.position.x, instance.position.y, instance.position.z);
|
||||
}
|
||||
shader->setUniform("uInteriorDarken", interior);
|
||||
|
||||
// Upload bone matrices if model has skeletal animation
|
||||
bool useBones = model.hasAnimation && !model.disableAnimation && !instance.boneMatrices.empty();
|
||||
shader->setUniform("uUseBones", useBones);
|
||||
|
|
|
|||
|
|
@ -185,6 +185,9 @@ bool Renderer::initialize(core::Window* win) {
|
|||
|
||||
// Create M2 renderer (for doodads)
|
||||
m2Renderer = std::make_unique<M2Renderer>();
|
||||
if (wmoRenderer) {
|
||||
m2Renderer->setWMORenderer(wmoRenderer.get());
|
||||
}
|
||||
// Note: M2 renderer needs asset manager, will be initialized when terrain loads
|
||||
|
||||
// Create zone manager
|
||||
|
|
|
|||
|
|
@ -109,9 +109,12 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
|
|||
texColor = texture(uTexture, TexCoord);
|
||||
// Alpha test only for cutout materials (lattice, grating, etc.)
|
||||
if (uAlphaTest && texColor.a < 0.5) discard;
|
||||
// Multiply vertex color (MOCV baked lighting/AO) into texture
|
||||
texColor.rgb *= VertexColor.rgb;
|
||||
alpha = texColor.a;
|
||||
// Exterior: multiply vertex color (MOCV baked AO) into texture
|
||||
// Interior: keep texture clean — vertex color is used as light below
|
||||
if (!uIsInterior) {
|
||||
texColor.rgb *= VertexColor.rgb;
|
||||
}
|
||||
} else {
|
||||
// MOCV vertex color alpha is a lighting blend factor, not transparency
|
||||
texColor = vec4(VertexColor.rgb, 1.0);
|
||||
|
|
@ -130,56 +133,56 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
|
|||
vec3 normal = normalize(Normal);
|
||||
vec3 lightDir = normalize(uLightDir);
|
||||
|
||||
// Interior vs exterior lighting
|
||||
vec3 ambient;
|
||||
float dirScale;
|
||||
vec3 litColor;
|
||||
if (uIsInterior) {
|
||||
ambient = vec3(0.7, 0.7, 0.7);
|
||||
dirScale = 0.3;
|
||||
// Interior: MOCV vertex colors are baked lighting.
|
||||
// Use them directly as the light multiplier on the texture.
|
||||
vec3 vertLight = VertexColor.rgb * 2.2 + 0.3;
|
||||
// Subtle directional fill so geometry reads
|
||||
float diff = max(dot(normal, lightDir), 0.0);
|
||||
vertLight += diff * 0.10;
|
||||
litColor = texColor.rgb * vertLight;
|
||||
} else {
|
||||
ambient = uAmbientColor;
|
||||
dirScale = 1.0;
|
||||
}
|
||||
// Exterior: standard diffuse + specular lighting
|
||||
vec3 ambient = uAmbientColor;
|
||||
|
||||
// Diffuse lighting
|
||||
float diff = max(dot(normal, lightDir), 0.0);
|
||||
vec3 diffuse = diff * vec3(1.0) * dirScale;
|
||||
float diff = max(dot(normal, lightDir), 0.0);
|
||||
vec3 diffuse = diff * vec3(1.0);
|
||||
|
||||
// Blinn-Phong specular
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity * dirScale;
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity;
|
||||
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
}
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
}
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
|
||||
// Combine lighting with texture
|
||||
vec3 result = (ambient + (diffuse + specular) * shadow) * texColor.rgb;
|
||||
litColor = (ambient + (diffuse + specular) * shadow) * texColor.rgb;
|
||||
}
|
||||
|
||||
// Fog
|
||||
float fogDist = length(uViewPos - FragPos);
|
||||
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
|
||||
result = mix(uFogColor, result, fogFactor);
|
||||
vec3 result = mix(uFogColor, litColor, fogFactor);
|
||||
|
||||
FragColor = vec4(result, alpha);
|
||||
}
|
||||
|
|
@ -1833,8 +1836,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
if (normalLen < 0.001f) continue;
|
||||
normal /= normalLen;
|
||||
|
||||
// Skip near-horizontal triangles (floors/ceilings).
|
||||
if (std::abs(normal.z) > 0.85f) continue;
|
||||
// Skip near-horizontal triangles (floors/ceilings/ramps).
|
||||
// Anything more horizontal than ~55° from vertical is walkable.
|
||||
if (std::abs(normal.z) > 0.55f) continue;
|
||||
|
||||
// Get triangle Z range
|
||||
float triMinZ = std::min({v0.z, v1.z, v2.z});
|
||||
|
|
@ -1852,9 +1856,6 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
// Skip low geometry that can be stepped over
|
||||
if (triMaxZ <= localFeetZ + MAX_STEP_HEIGHT) continue;
|
||||
|
||||
// Skip ramp surfaces (facing mostly upward) that are very low
|
||||
if (normal.z > 0.60f && triMaxZ <= localFeetZ + 0.8f) continue;
|
||||
|
||||
// Skip very short vertical surfaces (stair risers)
|
||||
if (triHeight < 0.6f && triMaxZ <= localFeetZ + 0.8f) continue;
|
||||
|
||||
|
|
@ -1960,6 +1961,51 @@ bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outMode
|
|||
return false;
|
||||
}
|
||||
|
||||
bool WMORenderer::isInsideInteriorWMO(float glX, float glY, float glZ) const {
|
||||
glm::vec3 queryMin(glX - 0.5f, glY - 0.5f, glZ - 0.5f);
|
||||
glm::vec3 queryMax(glX + 0.5f, glY + 0.5f, glZ + 0.5f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
continue;
|
||||
}
|
||||
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
||||
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
||||
glZ < instance.worldBoundsMin.z || glZ > instance.worldBoundsMax.z) {
|
||||
continue;
|
||||
}
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it == loadedModels.end()) continue;
|
||||
const ModelData& model = it->second;
|
||||
|
||||
bool anyGroupContains = false;
|
||||
for (size_t gi = 0; gi < model.groups.size() && gi < instance.worldGroupBounds.size(); ++gi) {
|
||||
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
||||
if (glX >= gMin.x && glX <= gMax.x &&
|
||||
glY >= gMin.y && glY <= gMax.y &&
|
||||
glZ >= gMin.z && glZ <= gMax.z) {
|
||||
anyGroupContains = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyGroupContains) continue;
|
||||
|
||||
glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f));
|
||||
for (const auto& group : model.groups) {
|
||||
if (!(group.groupFlags & 0x2000)) continue; // Skip exterior groups
|
||||
if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x &&
|
||||
localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y &&
|
||||
localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const {
|
||||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
float closestHit = maxDistance;
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderQuestRequestItemsWindow(gameHandler);
|
||||
renderQuestOfferRewardWindow(gameHandler);
|
||||
renderVendorWindow(gameHandler);
|
||||
renderTaxiWindow(gameHandler);
|
||||
renderQuestMarkers(gameHandler);
|
||||
renderMinimapMarkers(gameHandler);
|
||||
renderDeathScreen(gameHandler);
|
||||
|
|
@ -1539,6 +1540,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// /unstuck command — resets player position to floor height
|
||||
if (cmdLower == "unstuck") {
|
||||
gameHandler.unstuck();
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat channel slash commands
|
||||
bool isChannelCommand = false;
|
||||
if (cmdLower == "s" || cmdLower == "say") {
|
||||
|
|
@ -3342,6 +3350,71 @@ void GameScreen::renderEscapeMenu() {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Taxi Window
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isTaxiWindowOpen()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
||||
|
||||
bool open = true;
|
||||
if (ImGui::Begin("Flight Master", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
const auto& taxiData = gameHandler.getTaxiData();
|
||||
const auto& nodes = gameHandler.getTaxiNodes();
|
||||
uint32_t currentNode = gameHandler.getTaxiCurrentNode();
|
||||
|
||||
// Get current node's map to filter destinations
|
||||
uint32_t currentMapId = 0;
|
||||
auto curIt = nodes.find(currentNode);
|
||||
if (curIt != nodes.end()) {
|
||||
currentMapId = curIt->second.mapId;
|
||||
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Current: %s", curIt->second.name.c_str());
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImGui::Text("Select a destination:");
|
||||
ImGui::Spacing();
|
||||
|
||||
// List known destinations on same map, excluding current node
|
||||
int destCount = 0;
|
||||
for (const auto& [nodeId, node] : nodes) {
|
||||
if (nodeId == currentNode) continue;
|
||||
if (node.mapId != currentMapId) continue;
|
||||
if (!taxiData.isNodeKnown(nodeId)) continue;
|
||||
|
||||
ImGui::PushID(static_cast<int>(nodeId));
|
||||
ImGui::Text("%s", node.name.c_str());
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - 60);
|
||||
if (ImGui::SmallButton("Fly")) {
|
||||
gameHandler.activateTaxi(nodeId);
|
||||
}
|
||||
ImGui::PopID();
|
||||
destCount++;
|
||||
}
|
||||
|
||||
if (destCount == 0) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No destinations available.");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
||||
gameHandler.closeTaxi();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
gameHandler.closeTaxi();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Death Screen
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue