mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add mount system and crash mouse-release handler
Render mount M2 model under player with seated animation, apply creature skin textures, server-driven speed via SMSG_FORCE_RUN_SPEED_CHANGE, and /dismount command. X11 XUngrabPointer on crash/hang to always release mouse.
This commit is contained in:
parent
4a932dd8cd
commit
643611ee79
13 changed files with 363 additions and 3 deletions
|
|
@ -272,6 +272,9 @@ if (FFMPEG_LIBRARY_DIRS)
|
|||
endif()
|
||||
|
||||
# Platform-specific libraries
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_link_libraries(wowee PRIVATE X11)
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(wowee PRIVATE ws2_32)
|
||||
# SDL2main provides WinMain entry point on Windows
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ private:
|
|||
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
||||
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
|
||||
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
||||
|
||||
// Mount model tracking
|
||||
uint32_t mountInstanceId_ = 0;
|
||||
uint32_t mountModelId_ = 0;
|
||||
bool creatureLookupsBuilt_ = false;
|
||||
|
||||
// Deferred creature spawn queue (throttles spawning to avoid hangs)
|
||||
|
|
|
|||
|
|
@ -189,6 +189,10 @@ public:
|
|||
uint32_t getDisplayId() const { return displayId; }
|
||||
void setDisplayId(uint32_t id) { displayId = id; }
|
||||
|
||||
// Mount display ID (UNIT_FIELD_MOUNTDISPLAYID, index 69)
|
||||
uint32_t getMountDisplayId() const { return mountDisplayId; }
|
||||
void setMountDisplayId(uint32_t id) { mountDisplayId = id; }
|
||||
|
||||
// Unit flags (UNIT_FIELD_FLAGS, index 59)
|
||||
uint32_t getUnitFlags() const { return unitFlags; }
|
||||
void setUnitFlags(uint32_t f) { unitFlags = f; }
|
||||
|
|
@ -216,6 +220,7 @@ protected:
|
|||
uint32_t level = 1;
|
||||
uint32_t entry = 0;
|
||||
uint32_t displayId = 0;
|
||||
uint32_t mountDisplayId = 0;
|
||||
uint32_t unitFlags = 0;
|
||||
uint32_t npcFlags = 0;
|
||||
uint32_t factionTemplate = 0;
|
||||
|
|
|
|||
|
|
@ -455,6 +455,13 @@ public:
|
|||
}
|
||||
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
|
||||
|
||||
// Mount state
|
||||
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount
|
||||
void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); }
|
||||
bool isMounted() const { return currentMountDisplayId_ != 0; }
|
||||
float getServerRunSpeed() const { return serverRunSpeed_; }
|
||||
void dismount();
|
||||
|
||||
// Taxi / Flight Paths
|
||||
bool isTaxiWindowOpen() const { return taxiWindowOpen_; }
|
||||
void closeTaxi();
|
||||
|
|
@ -630,6 +637,9 @@ private:
|
|||
// ---- Teleport handler ----
|
||||
void handleTeleportAck(network::Packet& packet);
|
||||
|
||||
// ---- Speed change handler ----
|
||||
void handleForceRunSpeedChange(network::Packet& packet);
|
||||
|
||||
// ---- Taxi handlers ----
|
||||
void handleShowTaxiNodes(network::Packet& packet);
|
||||
void handleActivateTaxiReply(network::Packet& packet);
|
||||
|
|
@ -844,6 +854,8 @@ private:
|
|||
ShowTaxiNodesData currentTaxiData_;
|
||||
uint64_t taxiNpcGuid_ = 0;
|
||||
bool onTaxiFlight_ = false;
|
||||
uint32_t knownTaxiMask_[12] = {}; // Track previously known nodes for discovery alerts
|
||||
bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts
|
||||
|
||||
// Vendor
|
||||
bool vendorWindowOpen = false;
|
||||
|
|
@ -877,6 +889,9 @@ private:
|
|||
NpcRespawnCallback npcRespawnCallback_;
|
||||
MeleeSwingCallback meleeSwingCallback_;
|
||||
NpcSwingCallback npcSwingCallback_;
|
||||
MountCallback mountCallback_;
|
||||
uint32_t currentMountDisplayId_ = 0;
|
||||
float serverRunSpeed_ = 7.0f;
|
||||
bool playerDead_ = false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -241,9 +241,16 @@ enum class Opcode : uint16_t {
|
|||
MSG_MOVE_TELEPORT_ACK = 0x0C7,
|
||||
SMSG_TRANSFER_PENDING = 0x003F,
|
||||
|
||||
// ---- Speed Changes ----
|
||||
SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2,
|
||||
|
||||
// ---- Mount ----
|
||||
CMSG_CANCEL_MOUNT_AURA = 0x0375,
|
||||
|
||||
// ---- Taxi / Flight Paths ----
|
||||
SMSG_SHOWTAXINODES = 0x01A9,
|
||||
SMSG_ACTIVATETAXIREPLY = 0x01AE,
|
||||
SMSG_NEW_TAXI_PATH = 0x01AF,
|
||||
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ public:
|
|||
using MovementCallback = std::function<void(uint32_t opcode)>;
|
||||
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
||||
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
||||
void setRunSpeedOverride(float speed) { runSpeedOverride_ = speed; }
|
||||
void setMounted(bool m) { mounted_ = m; }
|
||||
|
||||
// For first-person player hiding
|
||||
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
||||
|
|
@ -188,6 +190,10 @@ private:
|
|||
static constexpr float WOW_GRAVITY = -19.29f;
|
||||
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
|
||||
|
||||
// Server-driven run speed override (0 = use default WOW_RUN_SPEED)
|
||||
float runSpeedOverride_ = 0.0f;
|
||||
bool mounted_ = false;
|
||||
|
||||
// Online mode: trust server position, don't prefer outdoors over WMO floors
|
||||
bool onlineMode = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,11 @@ public:
|
|||
void triggerMeleeSwing();
|
||||
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
|
||||
|
||||
// Mount rendering
|
||||
void setMounted(uint32_t mountInstId, float heightOffset);
|
||||
void clearMount();
|
||||
bool isMounted() const { return mountInstanceId_ != 0; }
|
||||
|
||||
// Selection circle for targeted entity
|
||||
void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color);
|
||||
void clearSelectionCircle();
|
||||
|
|
@ -214,7 +219,7 @@ private:
|
|||
float characterYaw = 0.0f;
|
||||
|
||||
// Character animation state
|
||||
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING };
|
||||
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT };
|
||||
CharAnimState charAnimState = CharAnimState::IDLE;
|
||||
void updateCharacterAnimation();
|
||||
bool isFootstepAnimationState() const;
|
||||
|
|
@ -259,6 +264,10 @@ private:
|
|||
uint32_t meleeAnimId = 0;
|
||||
uint32_t equippedWeaponInvType_ = 0;
|
||||
|
||||
// Mount state
|
||||
uint32_t mountInstanceId_ = 0;
|
||||
float mountHeightOffset_ = 0.0f;
|
||||
|
||||
bool terrainEnabled = true;
|
||||
bool terrainLoaded = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -389,6 +389,11 @@ void Application::update(float deltaTime) {
|
|||
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
||||
}
|
||||
|
||||
// Sync server run speed to camera controller
|
||||
if (renderer && gameHandler && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
||||
}
|
||||
|
||||
// Sync character render position → canonical WoW coords each frame
|
||||
if (renderer && gameHandler) {
|
||||
glm::vec3 renderPos = renderer->getCharacterPosition();
|
||||
|
|
@ -554,6 +559,150 @@ void Application::setupUICallbacks() {
|
|||
despawnOnlineCreature(guid);
|
||||
});
|
||||
|
||||
// Mount callback (online mode) - load/destroy mount model
|
||||
gameHandler->setMountCallback([this](uint32_t mountDisplayId) {
|
||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return;
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
|
||||
if (mountDisplayId == 0) {
|
||||
// Dismount: remove mount instance and model
|
||||
if (mountInstanceId_ != 0) {
|
||||
charRenderer->removeInstance(mountInstanceId_);
|
||||
mountInstanceId_ = 0;
|
||||
}
|
||||
mountModelId_ = 0;
|
||||
renderer->clearMount();
|
||||
LOG_INFO("Dismounted");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mount: load mount model
|
||||
std::string m2Path = getModelPathForDisplayId(mountDisplayId);
|
||||
if (m2Path.empty()) {
|
||||
LOG_WARNING("No model path for mount displayId ", mountDisplayId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check model cache
|
||||
uint32_t modelId = 0;
|
||||
bool modelCached = false;
|
||||
auto cacheIt = displayIdModelCache_.find(mountDisplayId);
|
||||
if (cacheIt != displayIdModelCache_.end()) {
|
||||
modelId = cacheIt->second;
|
||||
modelCached = true;
|
||||
} else {
|
||||
modelId = nextCreatureModelId_++;
|
||||
|
||||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("Failed to read mount M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty()) {
|
||||
LOG_WARNING("Failed to parse mount M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load skin file
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
auto skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, model);
|
||||
}
|
||||
|
||||
// Load external .anim files
|
||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFile(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!charRenderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("Failed to load mount model: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
displayIdModelCache_[mountDisplayId] = modelId;
|
||||
}
|
||||
|
||||
// Apply creature skin textures from CreatureDisplayInfo.dbc
|
||||
if (!modelCached) {
|
||||
auto itDisplayData = displayDataMap_.find(mountDisplayId);
|
||||
if (itDisplayData != displayDataMap_.end()) {
|
||||
const auto& dispData = itDisplayData->second;
|
||||
const auto* modelData = charRenderer->getModelData(modelId);
|
||||
if (modelData) {
|
||||
std::string modelDir;
|
||||
size_t lastSlash = m2Path.find_last_of("\\/");
|
||||
if (lastSlash != std::string::npos) {
|
||||
modelDir = m2Path.substr(0, lastSlash + 1);
|
||||
}
|
||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||
const auto& tex = modelData->textures[ti];
|
||||
std::string texPath;
|
||||
if (tex.type == 11 && !dispData.skin1.empty()) {
|
||||
texPath = modelDir + dispData.skin1 + ".blp";
|
||||
} else if (tex.type == 12 && !dispData.skin2.empty()) {
|
||||
texPath = modelDir + dispData.skin2 + ".blp";
|
||||
} else if (tex.type == 13 && !dispData.skin3.empty()) {
|
||||
texPath = modelDir + dispData.skin3 + ".blp";
|
||||
}
|
||||
if (!texPath.empty()) {
|
||||
GLuint skinTex = charRenderer->loadTexture(texPath);
|
||||
if (skinTex != 0) {
|
||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), skinTex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mountModelId_ = modelId;
|
||||
|
||||
// Create mount instance at player position
|
||||
glm::vec3 mountPos = renderer->getCharacterPosition();
|
||||
float yawRad = glm::radians(renderer->getCharacterYaw());
|
||||
uint32_t instanceId = charRenderer->createInstance(modelId, mountPos,
|
||||
glm::vec3(0.0f, 0.0f, yawRad), 1.0f);
|
||||
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create mount instance");
|
||||
return;
|
||||
}
|
||||
|
||||
mountInstanceId_ = instanceId;
|
||||
|
||||
// Compute height offset — place player above mount's back
|
||||
const auto* modelData = charRenderer->getModelData(modelId);
|
||||
float heightOffset = 1.2f; // Default fallback
|
||||
if (modelData) {
|
||||
// No coord swizzle in character renderer, so Z is up in model space too.
|
||||
// Use the top of the bounding box as the saddle height.
|
||||
float topZ = modelData->boundMax.z;
|
||||
if (topZ > 0.1f) {
|
||||
heightOffset = topZ * 0.85f;
|
||||
}
|
||||
LOG_INFO("Mount bounds: min=(", modelData->boundMin.x, ",", modelData->boundMin.y, ",", modelData->boundMin.z,
|
||||
") max=(", modelData->boundMax.x, ",", modelData->boundMax.y, ",", modelData->boundMax.z,
|
||||
") radius=", modelData->boundRadius, " → heightOffset=", heightOffset);
|
||||
}
|
||||
|
||||
renderer->setMounted(instanceId, heightOffset);
|
||||
charRenderer->playAnimation(instanceId, 0, true); // Idle animation
|
||||
|
||||
LOG_INFO("Mounted: displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset);
|
||||
});
|
||||
|
||||
// Creature move callback (online mode) - update creature positions
|
||||
gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
||||
auto it = creatureInstances_.find(guid);
|
||||
|
|
|
|||
|
|
@ -363,6 +363,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleMonsterMove(packet);
|
||||
break;
|
||||
|
||||
// ---- Speed Changes ----
|
||||
case Opcode::SMSG_FORCE_RUN_SPEED_CHANGE:
|
||||
handleForceRunSpeedChange(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
case Opcode::SMSG_ATTACKSTART:
|
||||
handleAttackStart(packet);
|
||||
|
|
@ -648,6 +653,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_ACTIVATETAXIREPLY:
|
||||
handleActivateTaxiReply(packet);
|
||||
break;
|
||||
case Opcode::SMSG_NEW_TAXI_PATH:
|
||||
// Empty packet - server signals a new flight path was learned
|
||||
// The actual node details come in the next SMSG_SHOWTAXINODES
|
||||
addSystemChatMessage("New flight path discovered!");
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
||||
|
|
@ -1287,6 +1297,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||
case 54: unit->setLevel(val); break;
|
||||
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||
case 69: // UNIT_FIELD_MOUNTDISPLAYID
|
||||
if (block.guid == playerGuid) {
|
||||
uint32_t old = currentMountDisplayId_;
|
||||
currentMountDisplayId_ = val;
|
||||
if (val != old && mountCallback_) mountCallback_(val);
|
||||
}
|
||||
unit->setMountDisplayId(val);
|
||||
break;
|
||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||
default: break;
|
||||
}
|
||||
|
|
@ -1398,6 +1416,15 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
unit->setFactionTemplate(val);
|
||||
unit->setHostile(isHostileFaction(val));
|
||||
break;
|
||||
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||
case 69: // UNIT_FIELD_MOUNTDISPLAYID
|
||||
if (block.guid == playerGuid) {
|
||||
uint32_t old = currentMountDisplayId_;
|
||||
currentMountDisplayId_ = val;
|
||||
if (val != old && mountCallback_) mountCallback_(val);
|
||||
}
|
||||
unit->setMountDisplayId(val);
|
||||
break;
|
||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||
default: break;
|
||||
}
|
||||
|
|
@ -2964,6 +2991,27 @@ void GameHandler::handleAttackStop(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
void GameHandler::dismount() {
|
||||
if (!isMounted() || !socket) return;
|
||||
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_CANCEL_MOUNT_AURA));
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
|
||||
}
|
||||
|
||||
void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
|
||||
// Packed GUID
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
// uint32 counter (ack counter, we ignore)
|
||||
packet.readUInt32();
|
||||
// float newSpeed
|
||||
float newSpeed = packet.readFloat();
|
||||
|
||||
if (guid == playerGuid) {
|
||||
serverRunSpeed_ = newSpeed;
|
||||
LOG_INFO("Server run speed changed to ", newSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||
MonsterMoveData data;
|
||||
if (!MonsterMoveParser::parse(packet, data)) {
|
||||
|
|
@ -3945,6 +3993,29 @@ void GameHandler::handleShowTaxiNodes(network::Packet& packet) {
|
|||
|
||||
loadTaxiDbc();
|
||||
|
||||
// Detect newly discovered flight paths by comparing with stored mask
|
||||
if (taxiMaskInitialized_) {
|
||||
for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) {
|
||||
uint32_t newBits = data.nodeMask[i] & ~knownTaxiMask_[i];
|
||||
if (newBits == 0) continue;
|
||||
for (uint32_t bit = 0; bit < 32; ++bit) {
|
||||
if (newBits & (1u << bit)) {
|
||||
uint32_t nodeId = i * 32 + bit;
|
||||
auto it = taxiNodes_.find(nodeId);
|
||||
if (it != taxiNodes_.end()) {
|
||||
addSystemChatMessage("Discovered flight path: " + it->second.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update stored mask
|
||||
for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) {
|
||||
knownTaxiMask_[i] = data.nodeMask[i];
|
||||
}
|
||||
taxiMaskInitialized_ = true;
|
||||
|
||||
currentTaxiData_ = data;
|
||||
taxiNpcGuid_ = data.npcGuid;
|
||||
taxiWindowOpen_ = true;
|
||||
|
|
|
|||
27
src/main.cpp
27
src/main.cpp
|
|
@ -1,8 +1,33 @@
|
|||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <exception>
|
||||
#include <csignal>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
static void releaseMouseGrab() {
|
||||
// Bypass SDL — talk to X11 directly (signal-safe enough for our purposes)
|
||||
Display* dpy = XOpenDisplay(nullptr);
|
||||
if (dpy) {
|
||||
XUngrabPointer(dpy, CurrentTime);
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
XFlush(dpy);
|
||||
XCloseDisplay(dpy);
|
||||
}
|
||||
}
|
||||
|
||||
static void crashHandler(int sig) {
|
||||
releaseMouseGrab();
|
||||
std::signal(sig, SIG_DFL);
|
||||
std::raise(sig);
|
||||
}
|
||||
|
||||
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
||||
std::signal(SIGSEGV, crashHandler);
|
||||
std::signal(SIGABRT, crashHandler);
|
||||
std::signal(SIGFPE, crashHandler);
|
||||
std::signal(SIGTERM, crashHandler);
|
||||
std::signal(SIGINT, crashHandler);
|
||||
try {
|
||||
wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::DEBUG);
|
||||
LOG_INFO("=== Wowee Native Client ===");
|
||||
|
|
@ -22,10 +47,12 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
|||
return 0;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
releaseMouseGrab();
|
||||
LOG_FATAL("Unhandled exception: ", e.what());
|
||||
return 1;
|
||||
}
|
||||
catch (...) {
|
||||
releaseMouseGrab();
|
||||
LOG_FATAL("Unknown exception occurred");
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ void CameraController::update(float deltaTime) {
|
|||
} else if (ctrlDown) {
|
||||
speed = WOW_WALK_SPEED;
|
||||
} else {
|
||||
speed = WOW_RUN_SPEED;
|
||||
speed = (runSpeedOverride_ > 0.0f) ? runSpeedOverride_ : WOW_RUN_SPEED;
|
||||
}
|
||||
} else {
|
||||
// Exploration mode (original behavior)
|
||||
|
|
@ -225,10 +225,12 @@ void CameraController::update(float deltaTime) {
|
|||
glm::vec3 right(-std::sin(moveYawRad), std::cos(moveYawRad), 0.0f);
|
||||
|
||||
// Toggle sit/crouch with X key (edge-triggered) — only when UI doesn't want keyboard
|
||||
// Blocked while mounted
|
||||
bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X);
|
||||
if (xDown && !xKeyWasDown) {
|
||||
if (xDown && !xKeyWasDown && !mounted_) {
|
||||
sitting = !sitting;
|
||||
}
|
||||
if (mounted_) sitting = false;
|
||||
xKeyWasDown = xDown;
|
||||
|
||||
// Update eye height based on crouch state (smooth transition)
|
||||
|
|
|
|||
|
|
@ -364,6 +364,20 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
|||
}
|
||||
}
|
||||
|
||||
void Renderer::setMounted(uint32_t mountInstId, float heightOffset) {
|
||||
mountInstanceId_ = mountInstId;
|
||||
mountHeightOffset_ = heightOffset;
|
||||
charAnimState = CharAnimState::MOUNT;
|
||||
if (cameraController) cameraController->setMounted(true);
|
||||
}
|
||||
|
||||
void Renderer::clearMount() {
|
||||
mountInstanceId_ = 0;
|
||||
mountHeightOffset_ = 0.0f;
|
||||
charAnimState = CharAnimState::IDLE;
|
||||
if (cameraController) cameraController->setMounted(false);
|
||||
}
|
||||
|
||||
uint32_t Renderer::resolveMeleeAnimId() {
|
||||
if (!characterRenderer || characterInstanceId == 0) {
|
||||
meleeAnimId = 0;
|
||||
|
|
@ -473,6 +487,7 @@ void Renderer::updateCharacterAnimation() {
|
|||
constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle)
|
||||
constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle)
|
||||
constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim)
|
||||
constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount
|
||||
|
||||
CharAnimState newState = charAnimState;
|
||||
|
||||
|
|
@ -489,6 +504,42 @@ void Renderer::updateCharacterAnimation() {
|
|||
bool swim = cameraController->isSwimming();
|
||||
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
||||
|
||||
// When mounted, force MOUNT state and skip normal transitions
|
||||
if (isMounted()) {
|
||||
newState = CharAnimState::MOUNT;
|
||||
charAnimState = newState;
|
||||
|
||||
// Play seated animation on player
|
||||
uint32_t currentAnimId = 0;
|
||||
float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f;
|
||||
bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs);
|
||||
if (!haveState || currentAnimId != ANIM_MOUNT) {
|
||||
characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true);
|
||||
}
|
||||
|
||||
// Sync mount instance position and rotation
|
||||
if (mountInstanceId_ > 0) {
|
||||
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
||||
float yawRad = glm::radians(characterYaw);
|
||||
characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(0.0f, 0.0f, yawRad));
|
||||
|
||||
// Drive mount model animation: idle when still, run when moving
|
||||
uint32_t mountAnimId = moving ? ANIM_RUN : ANIM_STAND;
|
||||
uint32_t curMountAnim = 0;
|
||||
float curMountTime = 0, curMountDur = 0;
|
||||
bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur);
|
||||
if (!haveMountState || curMountAnim != mountAnimId) {
|
||||
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Offset player Z above mount
|
||||
glm::vec3 playerPos = characterPosition;
|
||||
playerPos.z += mountHeightOffset_;
|
||||
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!forceMelee) switch (charAnimState) {
|
||||
case CharAnimState::IDLE:
|
||||
if (swim) {
|
||||
|
|
@ -629,6 +680,9 @@ void Renderer::updateCharacterAnimation() {
|
|||
newState = CharAnimState::IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::MOUNT:
|
||||
break; // Handled by early return above
|
||||
}
|
||||
|
||||
if (forceMelee) {
|
||||
|
|
@ -692,6 +746,7 @@ void Renderer::updateCharacterAnimation() {
|
|||
}
|
||||
loop = false;
|
||||
break;
|
||||
case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break;
|
||||
}
|
||||
|
||||
uint32_t currentAnimId = 0;
|
||||
|
|
|
|||
|
|
@ -1138,6 +1138,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// /dismount command
|
||||
if (cmdLower == "dismount") {
|
||||
gameHandler.dismount();
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// /sit command
|
||||
if (cmdLower == "sit") {
|
||||
gameHandler.setStandState(1); // 1 = sit
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue