mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-16 09:13:50 +00:00
Merge pull request #59 from ldmonster/fix/minor-bugs
[fix] minor-bugs: UI, animation, quest, rendering & movement fixes
This commit is contained in:
commit
5e82464658
12 changed files with 318 additions and 41 deletions
|
|
@ -1,7 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
|
|
@ -76,14 +79,70 @@ public:
|
|||
z = pz;
|
||||
orientation = o;
|
||||
isMoving_ = false; // Instant position set cancels interpolation
|
||||
usePathMode_ = false;
|
||||
}
|
||||
|
||||
// Multi-segment path movement
|
||||
void startMoveAlongPath(const std::vector<std::array<float, 3>>& path, float destO, float totalDuration) {
|
||||
if (path.empty()) return;
|
||||
if (path.size() == 1 || totalDuration <= 0.0f) {
|
||||
startMoveTo(path.back()[0], path.back()[1], path.back()[2], destO, totalDuration);
|
||||
return;
|
||||
}
|
||||
// Compute cumulative distances for proportional segment timing
|
||||
pathPoints_ = path;
|
||||
pathSegDists_.resize(path.size());
|
||||
pathSegDists_[0] = 0.0f;
|
||||
float totalDist = 0.0f;
|
||||
for (size_t i = 1; i < path.size(); i++) {
|
||||
float dx = path[i][0] - path[i - 1][0];
|
||||
float dy = path[i][1] - path[i - 1][1];
|
||||
float dz = path[i][2] - path[i - 1][2];
|
||||
totalDist += std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
pathSegDists_[i] = totalDist;
|
||||
}
|
||||
if (totalDist < 0.001f) {
|
||||
startMoveTo(path.back()[0], path.back()[1], path.back()[2], destO, totalDuration);
|
||||
return;
|
||||
}
|
||||
// Snap position if in overrun phase
|
||||
if (isMoving_ && moveElapsed_ >= moveDuration_) {
|
||||
x = moveEndX_; y = moveEndY_; z = moveEndZ_;
|
||||
}
|
||||
moveEndX_ = path.back()[0]; moveEndY_ = path.back()[1]; moveEndZ_ = path.back()[2];
|
||||
moveDuration_ = totalDuration;
|
||||
moveElapsed_ = 0.0f;
|
||||
orientation = destO;
|
||||
isMoving_ = true;
|
||||
usePathMode_ = true;
|
||||
// Velocity for dead-reckoning after path completes
|
||||
float fromX = isMoving_ ? moveEndX_ : x;
|
||||
float fromY = isMoving_ ? moveEndY_ : y;
|
||||
float impliedVX = (path.back()[0] - fromX) / totalDuration;
|
||||
float impliedVY = (path.back()[1] - fromY) / totalDuration;
|
||||
float impliedVZ = (path.back()[2] - path[0][2]) / totalDuration;
|
||||
const float alpha = 0.65f;
|
||||
velX_ = alpha * impliedVX + (1.0f - alpha) * velX_;
|
||||
velY_ = alpha * impliedVY + (1.0f - alpha) * velY_;
|
||||
velZ_ = alpha * impliedVZ + (1.0f - alpha) * velZ_;
|
||||
}
|
||||
|
||||
// Movement interpolation (syncs entity position with renderer during movement)
|
||||
void startMoveTo(float destX, float destY, float destZ, float destO, float durationSec) {
|
||||
usePathMode_ = false;
|
||||
if (durationSec <= 0.0f) {
|
||||
setPosition(destX, destY, destZ, destO);
|
||||
return;
|
||||
}
|
||||
// If we're in the dead-reckoning overrun phase, snap x/y/z back to the
|
||||
// destination before using them as the new start. The renderer was showing
|
||||
// the entity at moveEnd (via getLatest) during overrun, so the new
|
||||
// interpolation must start there to avoid a visible teleport.
|
||||
if (isMoving_ && moveElapsed_ >= moveDuration_) {
|
||||
x = moveEndX_;
|
||||
y = moveEndY_;
|
||||
z = moveEndZ_;
|
||||
}
|
||||
// Derive velocity from the displacement this packet implies.
|
||||
// Use the previous destination (not current lerped pos) as the "from" so
|
||||
// variable network timing doesn't inflate/shrink the implied speed.
|
||||
|
|
@ -113,11 +172,31 @@ public:
|
|||
if (!isMoving_) return;
|
||||
moveElapsed_ += deltaTime;
|
||||
if (moveElapsed_ < moveDuration_) {
|
||||
// Linear interpolation within the packet window
|
||||
float t = moveElapsed_ / moveDuration_;
|
||||
x = moveStartX_ + (moveEndX_ - moveStartX_) * t;
|
||||
y = moveStartY_ + (moveEndY_ - moveStartY_) * t;
|
||||
z = moveStartZ_ + (moveEndZ_ - moveStartZ_) * t;
|
||||
if (usePathMode_ && pathPoints_.size() > 1) {
|
||||
// Multi-segment path interpolation
|
||||
float totalDist = pathSegDists_.back();
|
||||
float t = moveElapsed_ / moveDuration_;
|
||||
float targetDist = t * totalDist;
|
||||
// Find the segment containing targetDist
|
||||
size_t seg = 1;
|
||||
while (seg < pathSegDists_.size() - 1 && pathSegDists_[seg] < targetDist)
|
||||
seg++;
|
||||
float segStart = pathSegDists_[seg - 1];
|
||||
float segEnd = pathSegDists_[seg];
|
||||
float segLen = segEnd - segStart;
|
||||
float segT = (segLen > 0.001f) ? (targetDist - segStart) / segLen : 0.0f;
|
||||
const auto& p0 = pathPoints_[seg - 1];
|
||||
const auto& p1 = pathPoints_[seg];
|
||||
x = p0[0] + (p1[0] - p0[0]) * segT;
|
||||
y = p0[1] + (p1[1] - p0[1]) * segT;
|
||||
z = p0[2] + (p1[2] - p0[2]) * segT;
|
||||
} else {
|
||||
// Single-segment linear interpolation
|
||||
float t = moveElapsed_ / moveDuration_;
|
||||
x = moveStartX_ + (moveEndX_ - moveStartX_) * t;
|
||||
y = moveStartY_ + (moveEndY_ - moveStartY_) * t;
|
||||
z = moveStartZ_ + (moveEndZ_ - moveStartZ_) * t;
|
||||
}
|
||||
} else {
|
||||
// Past the interpolation window: dead-reckon at the smoothed velocity
|
||||
// rather than freezing in place. Cap to one extra interval so we don't
|
||||
|
|
@ -192,11 +271,15 @@ protected:
|
|||
|
||||
// Movement interpolation state
|
||||
bool isMoving_ = false;
|
||||
bool usePathMode_ = false;
|
||||
float moveStartX_ = 0, moveStartY_ = 0, moveStartZ_ = 0;
|
||||
float moveEndX_ = 0, moveEndY_ = 0, moveEndZ_ = 0;
|
||||
float moveDuration_ = 0;
|
||||
float moveElapsed_ = 0;
|
||||
float velX_ = 0, velY_ = 0, velZ_ = 0; // Smoothed velocity for dead reckoning
|
||||
// Multi-segment path data
|
||||
std::vector<std::array<float, 3>> pathPoints_;
|
||||
std::vector<float> pathSegDists_; // Cumulative distances for each waypoint
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1699,10 +1699,7 @@ public:
|
|||
bool isServerMovementAllowed() const;
|
||||
|
||||
// Quest giver status (! and ? markers)
|
||||
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const {
|
||||
auto it = npcQuestStatus_.find(guid);
|
||||
return (it != npcQuestStatus_.end()) ? it->second : QuestGiverStatus::NONE;
|
||||
}
|
||||
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const;
|
||||
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const;
|
||||
|
||||
// Charge callback — fires when player casts a charge spell toward target
|
||||
|
|
|
|||
|
|
@ -1678,6 +1678,9 @@ struct MonsterMoveData {
|
|||
// Destination (final point of the spline, server coords)
|
||||
float destX = 0, destY = 0, destZ = 0;
|
||||
bool hasDest = false;
|
||||
// Intermediate waypoints along the path (server coords, excludes start & final dest)
|
||||
struct Point { float x, y, z; };
|
||||
std::vector<Point> waypoints;
|
||||
};
|
||||
|
||||
class MonsterMoveParser {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@ public:
|
|||
statusMessage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset for back-navigation from character screen.
|
||||
* Preserves autoSelectAttempted so single-realm auto-connect doesn't re-fire.
|
||||
*/
|
||||
void resetForBack() {
|
||||
selectedRealmIndex = -1;
|
||||
realmSelected = false;
|
||||
selectedRealmName.clear();
|
||||
selectedRealmAddress.clear();
|
||||
statusMessage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a realm has been selected
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -720,8 +720,12 @@ void Application::run() {
|
|||
int newWidth = event.window.data1;
|
||||
int newHeight = event.window.data2;
|
||||
window->setSize(newWidth, newHeight);
|
||||
// Mark swapchain dirty so it gets recreated at the correct size
|
||||
if (window->getVkContext()) {
|
||||
window->getVkContext()->markSwapchainDirty();
|
||||
}
|
||||
// Vulkan viewport set in command buffer, not globally
|
||||
if (renderer && renderer->getCamera()) {
|
||||
if (renderer && renderer->getCamera() && newHeight > 0) {
|
||||
renderer->getCamera()->setAspectRatio(static_cast<float>(newWidth) / newHeight);
|
||||
}
|
||||
// Notify addons so UI layouts can adapt to the new size
|
||||
|
|
@ -1740,9 +1744,29 @@ void Application::update(float deltaTime) {
|
|||
if (canonDistSq > syncRadiusSq) continue;
|
||||
}
|
||||
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
// Use the destination position once the entity has reached its
|
||||
// target. During the dead-reckoning overrun window getX/Y/Z
|
||||
// drifts past the destination at the last known velocity;
|
||||
// using getLatest (== moveEnd while isMoving_) avoids the
|
||||
// visible forward-drift followed by a backward snap.
|
||||
const bool inOverrun = entity->isEntityMoving() && !entity->isActivelyMoving();
|
||||
glm::vec3 canonical(
|
||||
inOverrun ? entity->getLatestX() : entity->getX(),
|
||||
inOverrun ? entity->getLatestY() : entity->getY(),
|
||||
inOverrun ? entity->getLatestZ() : entity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Clamp creature Z to terrain surface during movement interpolation.
|
||||
// The server sends single-segment moves and expects the client to place
|
||||
// creatures on the ground. Only clamp while actively moving — idle
|
||||
// creatures keep their server-authoritative Z (flight masters, etc.).
|
||||
if (entity->isActivelyMoving() && renderer->getTerrainManager()) {
|
||||
auto terrainZ = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
|
||||
if (terrainZ.has_value()) {
|
||||
renderPos.z = terrainZ.value();
|
||||
}
|
||||
}
|
||||
|
||||
// Visual collision guard: keep hostile melee units from rendering inside the
|
||||
// player's model while attacking. This is client-side only (no server position change).
|
||||
// Only check for creatures within 8 units (melee range) — saves expensive
|
||||
|
|
@ -1822,17 +1846,18 @@ void Application::update(float deltaTime) {
|
|||
auto unitPtr = std::static_pointer_cast<game::Unit>(entity);
|
||||
const bool deadOrCorpse = unitPtr->getHealth() == 0;
|
||||
const bool largeCorrection = (planarDistSq > 36.0f) || (dz > 3.0f);
|
||||
// isEntityMoving() reflects server-authoritative move state set by
|
||||
// startMoveTo() in handleMonsterMove, regardless of distance-cull.
|
||||
// This correctly detects movement for distant creatures (> 150u)
|
||||
// where updateMovement() is not called and getX/Y/Z() stays stale.
|
||||
// Use isActivelyMoving() (not isEntityMoving()) so the
|
||||
// Run/Walk animation stops when the creature reaches its
|
||||
// destination, rather than persisting through the dead-
|
||||
// reckoning overrun window.
|
||||
// Use isActivelyMoving() so Run/Walk animation stops when the
|
||||
// creature reaches its destination. Don't use position-change
|
||||
// (planarDistSq) as a movement indicator when the entity is in
|
||||
// the dead-reckoning overrun window — the residual velocity
|
||||
// drift would keep the walk/run animation playing long after
|
||||
// the creature has actually arrived. Only fall back to position-
|
||||
// change detection for entities with no active movement tracking
|
||||
// (e.g. teleports or position-only updates from the server).
|
||||
const bool entityIsMoving = entity->isActivelyMoving();
|
||||
constexpr float kMoveThreshSq = 0.03f * 0.03f;
|
||||
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDistSq > kMoveThreshSq || dz > 0.08f);
|
||||
const bool posChanging = planarDistSq > kMoveThreshSq || dz > 0.08f;
|
||||
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || (posChanging && !entity->isEntityMoving()));
|
||||
if (deadOrCorpse || largeCorrection) {
|
||||
charRenderer->setInstancePosition(instanceId, renderPos);
|
||||
} else if (planarDistSq > kMoveThreshSq || dz > 0.08f) {
|
||||
|
|
@ -1936,10 +1961,23 @@ void Application::update(float deltaTime) {
|
|||
if (glm::dot(d, d) > pSyncRadiusSq) continue;
|
||||
}
|
||||
|
||||
// Position sync
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
// Position sync — clamp to destination during dead-reckoning
|
||||
// overrun to avoid drift + backward snap (same as creature loop).
|
||||
const bool inOverrun = entity->isEntityMoving() && !entity->isActivelyMoving();
|
||||
glm::vec3 canonical(
|
||||
inOverrun ? entity->getLatestX() : entity->getX(),
|
||||
inOverrun ? entity->getLatestY() : entity->getY(),
|
||||
inOverrun ? entity->getLatestZ() : entity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Clamp other players' Z to terrain surface during movement
|
||||
if (entity->isActivelyMoving() && renderer->getTerrainManager()) {
|
||||
auto terrainZ = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
|
||||
if (terrainZ.has_value()) {
|
||||
renderPos.z = terrainZ.value();
|
||||
}
|
||||
}
|
||||
|
||||
auto posIt = _pCreatureRenderPosCache.find(guid);
|
||||
if (posIt == _pCreatureRenderPosCache.end()) {
|
||||
charRenderer->setInstancePosition(instanceId, renderPos);
|
||||
|
|
@ -1956,7 +1994,8 @@ void Application::update(float deltaTime) {
|
|||
const bool largeCorrection = (planarDistSq > 36.0f) || (dz > 3.0f);
|
||||
const bool entityIsMoving = entity->isActivelyMoving();
|
||||
constexpr float kMoveThreshSq2 = 0.03f * 0.03f;
|
||||
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDistSq > kMoveThreshSq2 || dz > 0.08f);
|
||||
const bool posChanging2 = planarDistSq > kMoveThreshSq2 || dz > 0.08f;
|
||||
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || (posChanging2 && !entity->isEntityMoving()));
|
||||
|
||||
if (deadOrCorpse || largeCorrection) {
|
||||
charRenderer->setInstancePosition(instanceId, renderPos);
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ void UIScreenCallbackHandler::setupCallbacks() {
|
|||
uiManager_.getCharacterScreen().setOnBack([this]() {
|
||||
// Disconnect from world server and reset UI state for fresh realm selection
|
||||
gameHandler_.disconnect();
|
||||
uiManager_.getRealmScreen().reset();
|
||||
uiManager_.getRealmScreen().resetForBack();
|
||||
uiManager_.getCharacterScreen().reset();
|
||||
setState_(AppState::REALM_SELECTION);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2261,6 +2261,10 @@ const std::unordered_map<uint64_t, QuestGiverStatus>& GameHandler::getNpcQuestSt
|
|||
static const std::unordered_map<uint64_t, QuestGiverStatus> empty;
|
||||
return empty;
|
||||
}
|
||||
QuestGiverStatus GameHandler::getQuestGiverStatus(uint64_t guid) const {
|
||||
if (questHandler_) return questHandler_->getQuestGiverStatus(guid);
|
||||
return QuestGiverStatus::NONE;
|
||||
}
|
||||
const std::vector<GameHandler::QuestLogEntry>& GameHandler::getQuestLog() const {
|
||||
if (questHandler_) return questHandler_->getQuestLog();
|
||||
static const std::vector<QuestLogEntry> empty;
|
||||
|
|
|
|||
|
|
@ -1454,8 +1454,23 @@ void MovementHandler::handleMonsterMove(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
|
||||
orientation, data.duration / 1000.0f);
|
||||
// Build full path: start → waypoints → destination (all in canonical coords)
|
||||
if (!data.waypoints.empty()) {
|
||||
glm::vec3 startCanonical = core::coords::serverToCanonical(
|
||||
glm::vec3(data.x, data.y, data.z));
|
||||
std::vector<std::array<float, 3>> path;
|
||||
path.push_back({startCanonical.x, startCanonical.y, startCanonical.z});
|
||||
for (const auto& wp : data.waypoints) {
|
||||
glm::vec3 wpCanonical = core::coords::serverToCanonical(
|
||||
glm::vec3(wp.x, wp.y, wp.z));
|
||||
path.push_back({wpCanonical.x, wpCanonical.y, wpCanonical.z});
|
||||
}
|
||||
path.push_back({destCanonical.x, destCanonical.y, destCanonical.z});
|
||||
entity->startMoveAlongPath(path, orientation, data.duration / 1000.0f);
|
||||
} else {
|
||||
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
|
||||
orientation, data.duration / 1000.0f);
|
||||
}
|
||||
|
||||
if (owner_.creatureMoveCallbackRef()) {
|
||||
owner_.creatureMoveCallbackRef()(data.guid,
|
||||
|
|
|
|||
|
|
@ -339,6 +339,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
if (packet.hasRemaining(9)) {
|
||||
uint64_t npcGuid = packet.readUInt64();
|
||||
uint8_t status = owner_.getPacketParsers()->readQuestGiverStatus(packet);
|
||||
LOG_INFO("SMSG_QUESTGIVER_STATUS: npcGuid=0x", std::hex, npcGuid, std::dec,
|
||||
" status=", static_cast<int>(status));
|
||||
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
|
||||
}
|
||||
};
|
||||
|
|
@ -1075,8 +1077,17 @@ void QuestHandler::acceptQuest() {
|
|||
const bool inLocalLog = hasQuestInLog(questId);
|
||||
const int serverSlot = findQuestLogSlotIndexFromServer(questId);
|
||||
if (serverSlot >= 0) {
|
||||
LOG_INFO("Ignoring duplicate quest accept already in server quest log: questId=", questId,
|
||||
" slot=", serverSlot);
|
||||
LOG_INFO("Quest already in server quest log: questId=", questId,
|
||||
" slot=", serverSlot, " inLocalLog=", inLocalLog);
|
||||
// Ensure it's in our local log even if server already has it
|
||||
addQuestToLocalLogIfMissing(questId, currentQuestDetails_.title, currentQuestDetails_.objectives);
|
||||
requestQuestQuery(questId, false);
|
||||
// Re-query NPC status from server
|
||||
if (npcGuid && owner_.getSocket()) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(npcGuid);
|
||||
owner_.getSocket()->send(qsPkt);
|
||||
}
|
||||
questDetailsOpen_ = false;
|
||||
questDetailsOpenTime_ = std::chrono::steady_clock::time_point{};
|
||||
currentQuestDetails_ = QuestDetailsData{};
|
||||
|
|
@ -1094,6 +1105,9 @@ void QuestHandler::acceptQuest() {
|
|||
pendingQuestAcceptTimeouts_[questId] = 5.0f;
|
||||
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
|
||||
|
||||
// Immediately add to local quest log using available details
|
||||
addQuestToLocalLogIfMissing(questId, currentQuestDetails_.title, currentQuestDetails_.objectives);
|
||||
|
||||
// Play quest-accept sound
|
||||
if (auto* ac = owner_.services().audioCoordinator) {
|
||||
if (auto* sfx = ac->getUiSoundManager())
|
||||
|
|
@ -1223,6 +1237,19 @@ void QuestHandler::abandonQuest(uint32_t questId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Re-query nearby quest giver NPCs so markers refresh (e.g. "?" → "!")
|
||||
if (owner_.getSocket()) {
|
||||
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
|
||||
if (entity->getType() != ObjectType::UNIT) continue;
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
if (unit->getNpcFlags() & 0x02) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(guid);
|
||||
owner_.getSocket()->send(qsPkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any quest POI minimap markers for this quest.
|
||||
gossipPois_.erase(
|
||||
std::remove_if(gossipPois_.begin(), gossipPois_.end(),
|
||||
|
|
@ -1376,7 +1403,7 @@ bool QuestHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
|
|||
|
||||
void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>& fields) {
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
if (ufQuestStart == 0xFFFF || questLog_.empty()) return;
|
||||
if (ufQuestStart == 0xFFFF) return;
|
||||
|
||||
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
|
||||
if (qStride < 2) return;
|
||||
|
|
@ -1391,6 +1418,20 @@ void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>&
|
|||
uint32_t questId = idIt->second;
|
||||
if (questId == 0) continue;
|
||||
|
||||
// Add quest to local log only if we have a pending accept for it
|
||||
if (!hasQuestInLog(questId) && pendingQuestAcceptTimeouts_.count(questId) != 0) {
|
||||
addQuestToLocalLogIfMissing(questId, "Quest #" + std::to_string(questId), "");
|
||||
requestQuestQuery(questId, false);
|
||||
// Re-query quest giver status for the NPC that gave us this quest
|
||||
auto pendingIt = pendingQuestAcceptNpcGuids_.find(questId);
|
||||
if (pendingIt != pendingQuestAcceptNpcGuids_.end() && pendingIt->second != 0 && owner_.getSocket()) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(pendingIt->second);
|
||||
owner_.getSocket()->send(qsPkt);
|
||||
}
|
||||
clearPendingQuestAccept(questId);
|
||||
}
|
||||
|
||||
auto stateIt = fields.find(stateField);
|
||||
if (stateIt == fields.end()) continue;
|
||||
bool serverComplete = ((stateIt->second & 0xFF) == kQuestStatusComplete);
|
||||
|
|
|
|||
|
|
@ -637,13 +637,15 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
|
|||
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
|
||||
|
||||
if (uncompressed) {
|
||||
// Read last point as destination
|
||||
// Skip to last point: each point is 12 bytes
|
||||
if (pointCount > 1) {
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
}
|
||||
// All waypoints stored as absolute float3 (Catmullrom/Flying paths)
|
||||
// Read all intermediate points, then the final destination
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
MonsterMoveData::Point wp;
|
||||
wp.x = packet.readFloat();
|
||||
wp.y = packet.readFloat();
|
||||
wp.z = packet.readFloat();
|
||||
data.waypoints.push_back(wp);
|
||||
}
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
data.destX = packet.readFloat();
|
||||
|
|
@ -657,6 +659,33 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
|
|||
data.destY = packet.readFloat();
|
||||
data.destZ = packet.readFloat();
|
||||
data.hasDest = true;
|
||||
|
||||
// Remaining waypoints are packed as uint32 deltas from the midpoint
|
||||
// between the creature's start position and the destination.
|
||||
// Encoding matches TrinityCore MoveSpline::PackXYZ:
|
||||
// x = 11-bit signed (bits 0-10), y = 11-bit signed (bits 11-21),
|
||||
// z = 10-bit signed (bits 22-31), each scaled by 0.25 units.
|
||||
if (pointCount > 1) {
|
||||
float midX = (data.x + data.destX) * 0.5f;
|
||||
float midY = (data.y + data.destY) * 0.5f;
|
||||
float midZ = (data.z + data.destZ) * 0.5f;
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
uint32_t packed = packet.readUInt32();
|
||||
// Sign-extend 11-bit x and y, 10-bit z (2's complement)
|
||||
int32_t sx = static_cast<int32_t>(packed & 0x7FF);
|
||||
if (sx & 0x400) sx |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sy = static_cast<int32_t>((packed >> 11) & 0x7FF);
|
||||
if (sy & 0x400) sy |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sz = static_cast<int32_t>((packed >> 22) & 0x3FF);
|
||||
if (sz & 0x200) sz |= static_cast<int32_t>(0xFFFFFC00);
|
||||
MonsterMoveData::Point wp;
|
||||
wp.x = midX - static_cast<float>(sx) * 0.25f;
|
||||
wp.y = midY - static_cast<float>(sy) * 0.25f;
|
||||
wp.z = midZ - static_cast<float>(sz) * 0.25f;
|
||||
data.waypoints.push_back(wp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("MonsterMove: guid=0x", std::hex, data.guid, std::dec,
|
||||
|
|
@ -754,12 +783,26 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d
|
|||
data.destZ = packet.readFloat();
|
||||
data.hasDest = true;
|
||||
|
||||
// Remaining waypoints are packed as uint32 deltas.
|
||||
// Remaining waypoints are packed as uint32 deltas from midpoint.
|
||||
if (pointCount > 1) {
|
||||
size_t skipBytes = static_cast<size_t>(pointCount - 1) * 4;
|
||||
size_t newPos = packet.getReadPos() + skipBytes;
|
||||
if (newPos > packet.getSize()) return false;
|
||||
packet.setReadPos(newPos);
|
||||
float midX = (data.x + data.destX) * 0.5f;
|
||||
float midY = (data.y + data.destY) * 0.5f;
|
||||
float midZ = (data.z + data.destZ) * 0.5f;
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
uint32_t packed = packet.readUInt32();
|
||||
int32_t sx = static_cast<int32_t>(packed & 0x7FF);
|
||||
if (sx & 0x400) sx |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sy = static_cast<int32_t>((packed >> 11) & 0x7FF);
|
||||
if (sy & 0x400) sy |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sz = static_cast<int32_t>((packed >> 22) & 0x3FF);
|
||||
if (sz & 0x200) sz |= static_cast<int32_t>(0xFFFFFC00);
|
||||
MonsterMoveData::Point wp;
|
||||
wp.x = midX - static_cast<float>(sx) * 0.25f;
|
||||
wp.y = midY - static_cast<float>(sy) * 0.25f;
|
||||
wp.z = midZ - static_cast<float>(sz) * 0.25f;
|
||||
data.waypoints.push_back(wp);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec,
|
||||
|
|
|
|||
|
|
@ -859,6 +859,8 @@ void Renderer::beginFrame() {
|
|||
|
||||
// Handle swapchain recreation if needed
|
||||
if (vkCtx->isSwapchainDirty()) {
|
||||
// Skip recreation while window is minimized (0×0 extent is a Vulkan spec violation)
|
||||
if (window->getWidth() == 0 || window->getHeight() == 0) return;
|
||||
(void)vkCtx->recreateSwapchain(window->getWidth(), window->getHeight());
|
||||
// Rebuild water resources that reference swapchain extent/views
|
||||
if (waterRenderer) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
|
@ -241,6 +242,43 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
// Keyboard navigation: Up/Down to change selection, Enter to enter world
|
||||
// Claim ownership of arrow keys so ImGui nav doesn't move focus between buttons
|
||||
ImGuiID charListOwner = ImGui::GetID("CharListNav");
|
||||
ImGui::SetKeyOwner(ImGuiKey_UpArrow, charListOwner);
|
||||
ImGui::SetKeyOwner(ImGuiKey_DownArrow, charListOwner);
|
||||
|
||||
if (!characters.empty() && deleteConfirmStage == 0) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
|
||||
if (selectedCharacterIndex > 0) {
|
||||
selectedCharacterIndex--;
|
||||
selectedCharacterGuid = characters[selectedCharacterIndex].guid;
|
||||
saveLastCharacter(selectedCharacterGuid);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
||||
if (selectedCharacterIndex < static_cast<int>(characters.size()) - 1) {
|
||||
selectedCharacterIndex++;
|
||||
selectedCharacterGuid = characters[selectedCharacterIndex].guid;
|
||||
saveLastCharacter(selectedCharacterGuid);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
|
||||
if (selectedCharacterIndex >= 0 &&
|
||||
selectedCharacterIndex < static_cast<int>(characters.size())) {
|
||||
const auto& ch = characters[selectedCharacterIndex];
|
||||
characterSelected = true;
|
||||
saveLastCharacter(ch.guid);
|
||||
// Claim Enter so the game screen doesn't activate chat on the same press
|
||||
ImGui::SetKeyOwner(ImGuiKey_Enter, charListOwner, ImGuiInputFlags_LockUntilRelease);
|
||||
ImGui::SetKeyOwner(ImGuiKey_KeypadEnter, charListOwner, ImGuiInputFlags_LockUntilRelease);
|
||||
gameHandler.selectCharacter(ch.guid);
|
||||
if (onCharacterSelected) onCharacterSelected(ch.guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
// ── Right: Details panel ──
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue