Stabilize transports and correct minimap orientation

This commit is contained in:
Kelsi 2026-02-11 17:30:57 -08:00
parent 2bd259c0a8
commit 5dae994830
6 changed files with 235 additions and 203 deletions

View file

@ -55,11 +55,17 @@ struct ActiveTransport {
bool hasServerClock; // Whether we've synced with server time
int32_t serverClockOffsetMs; // Offset: serverClock - localNow
bool useClientAnimation; // Use client-side path animation
bool clientAnimationReverse; // Run client animation in reverse along the selected path
float serverYaw; // Server-authoritative yaw (radians)
bool hasServerYaw; // Whether we've received server yaw
float lastServerUpdate; // Time of last server movement update
int serverUpdateCount; // Number of server updates received
// Dead-reckoning from latest authoritative updates (used only when updates are sparse).
glm::vec3 serverLinearVelocity;
float serverAngularVelocity;
bool hasServerVelocity;
};
class TransportManager {
@ -85,6 +91,8 @@ public:
// Check if a path exists for a given GameObject entry
bool hasPathForEntry(uint32_t entry) const;
// Check if a path has meaningful XY travel (used to reject near-stationary false positives).
bool hasUsableMovingPathForEntry(uint32_t entry, float minXYRange = 1.0f) const;
// Infer a real moving DBC path by spawn position (for servers whose transport entry IDs
// don't map 1:1 to TransportAnimation.dbc entry IDs).

View file

@ -890,17 +890,28 @@ void Application::setupUICallbacks() {
// Check if we have a real path from TransportAnimation.dbc (indexed by entry).
// AzerothCore transport entries are not always 1:1 with DBC path ids.
if (!transportManager->hasPathForEntry(entry)) {
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
if (remappedPath != 0) {
pathId = remappedPath;
LOG_INFO("Using remapped fallback transport path ", pathId,
" for entry ", entry, " displayId=", displayId);
const bool shipOrZeppelinDisplay =
(displayId == 3015 || displayId == 3031 || displayId == 7546 ||
displayId == 7446 || displayId == 1587 || displayId == 2454 ||
displayId == 807 || displayId == 808 || displayId == 455 || displayId == 462);
bool hasUsablePath = transportManager->hasPathForEntry(entry);
if (shipOrZeppelinDisplay) {
// For true transports, reject tiny XY tracks that effectively look stationary.
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
}
if (!hasUsablePath) {
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
if (inferredPath != 0) {
pathId = inferredPath;
LOG_INFO("Using inferred transport path ", pathId, " for entry ", entry);
} else {
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
if (inferredPath != 0) {
pathId = inferredPath;
LOG_INFO("Using inferred transport path ", pathId, " for entry ", entry);
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
if (remappedPath != 0) {
pathId = remappedPath;
LOG_INFO("Using remapped fallback transport path ", pathId,
" for entry ", entry, " displayId=", displayId,
" (usableEntryPath=", transportManager->hasPathForEntry(entry), ")");
} else {
LOG_WARNING("No TransportAnimation.dbc path for entry ", entry,
" - transport will be stationary");
@ -957,20 +968,29 @@ void Application::setupUICallbacks() {
// Coordinates are already canonical (converted in game_handler.cpp)
glm::vec3 canonicalSpawnPos(x, y, z);
// Check if we have a real path, otherwise remap/infer/fall back to stationary.
if (!transportManager->hasPathForEntry(entry)) {
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
if (remappedPath != 0) {
pathId = remappedPath;
LOG_INFO("Auto-spawned transport with remapped fallback path: entry=", entry,
" remappedPath=", pathId, " displayId=", displayId,
// Check if we have a real usable path, otherwise remap/infer/fall back to stationary.
const bool shipOrZeppelinDisplay =
(displayId == 3015 || displayId == 3031 || displayId == 7546 ||
displayId == 7446 || displayId == 1587 || displayId == 2454 ||
displayId == 807 || displayId == 808 || displayId == 455 || displayId == 462);
bool hasUsablePath = transportManager->hasPathForEntry(entry);
if (shipOrZeppelinDisplay) {
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
}
if (!hasUsablePath) {
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
if (inferredPath != 0) {
pathId = inferredPath;
LOG_INFO("Auto-spawned transport with inferred path: entry=", entry,
" inferredPath=", pathId, " displayId=", displayId,
" wmoInstance=", wmoInstanceId);
} else {
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
if (inferredPath != 0) {
pathId = inferredPath;
LOG_INFO("Auto-spawned transport with inferred path: entry=", entry,
" inferredPath=", pathId, " displayId=", displayId,
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
if (remappedPath != 0) {
pathId = remappedPath;
LOG_INFO("Auto-spawned transport with remapped fallback path: entry=", entry,
" remappedPath=", pathId, " displayId=", displayId,
" wmoInstance=", wmoInstanceId);
} else {
std::vector<glm::vec3> path = { canonicalSpawnPos };

View file

@ -1853,6 +1853,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Process out-of-range objects first
for (uint64_t guid : data.outOfRangeGuids) {
if (entityManager.hasEntity(guid)) {
const bool isKnownTransport = transportGuids_.count(guid) > 0;
if (isKnownTransport) {
LOG_INFO("Ignoring out-of-range removal for transport: 0x", std::hex, guid, std::dec);
continue;
}
LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec);
// Trigger despawn callbacks before removing entity
auto entity = entityManager.getEntity(guid);
@ -1934,11 +1940,6 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
glm::vec3 localOffset = core::coords::serverToCanonical(
glm::vec3(block.transportX, block.transportY, block.transportZ));
// Refresh parent transport transform from this packet stream.
if (transportMoveCallback_) {
transportMoveCallback_(block.transportGuid, pos.x, pos.y, pos.z, block.orientation);
}
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
@ -2428,10 +2429,6 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
glm::vec3 localOffset = core::coords::serverToCanonical(
glm::vec3(block.transportX, block.transportY, block.transportZ));
if (transportMoveCallback_) {
transportMoveCallback_(block.transportGuid, pos.x, pos.y, pos.z, block.orientation);
}
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
@ -2538,6 +2535,10 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
// Remove entity
if (entityManager.hasEntity(data.guid)) {
if (transportGuids_.count(data.guid) > 0) {
LOG_INFO("Ignoring destroy for transport entity: 0x", std::hex, data.guid, std::dec);
return;
}
entityManager.removeEntity(data.guid);
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
" (", (data.isDeath ? "death" : "despawn"), ")");

View file

@ -76,10 +76,14 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
transport.serverClockOffsetMs = 0;
// Server-authoritative movement only - no client-side animation
transport.useClientAnimation = false;
transport.clientAnimationReverse = false;
transport.serverYaw = 0.0f;
transport.hasServerYaw = false;
transport.lastServerUpdate = 0.0f;
transport.serverUpdateCount = 0;
transport.serverLinearVelocity = glm::vec3(0.0f);
transport.serverAngularVelocity = 0.0f;
transport.hasServerVelocity = false;
updateTransformMatrices(transport);
@ -235,28 +239,116 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
pathTimeMs = (uint32_t)wrapped;
} else if (transport.useClientAnimation) {
// Pure local clock (no server sync yet, client-driven)
transport.localClockMs += (uint32_t)(deltaTime * 1000.0f);
uint32_t dtMs = static_cast<uint32_t>(deltaTime * 1000.0f);
if (!transport.clientAnimationReverse) {
transport.localClockMs += dtMs;
} else {
if (dtMs > path.durationMs) {
dtMs %= path.durationMs;
}
if (transport.localClockMs >= dtMs) {
transport.localClockMs -= dtMs;
} else {
transport.localClockMs = path.durationMs - (dtMs - transport.localClockMs);
}
}
pathTimeMs = transport.localClockMs % path.durationMs;
} else {
// Server-driven but no clock yet. If updates never arrive, fall back to local animation.
constexpr float kMissingUpdateFallbackSec = 2.5f;
if ((elapsedTime_ - transport.lastServerUpdate) >= kMissingUpdateFallbackSec) {
transport.useClientAnimation = true;
transport.localClockMs = 0;
pathTimeMs = 0;
LOG_WARNING("TransportManager: No server movement updates for transport 0x", std::hex, transport.guid, std::dec,
" after ", kMissingUpdateFallbackSec, "s - enabling client fallback animation");
} else {
updateTransformMatrices(transport);
if (wmoRenderer_) {
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
// Server-driven transport without clock sync.
// Do not auto-fallback to local DBC paths; remapped paths can be wrong and cause
// fast sideways movement, diving below water, or despawn-like behavior.
// Instead, briefly dead-reckon from recent authoritative velocity to avoid visual stutter
// when update bursts are sparse.
constexpr float kMaxExtrapolationSec = 8.0f;
const float ageSec = elapsedTime_ - transport.lastServerUpdate;
if (transport.hasServerVelocity && ageSec > 0.0f && ageSec <= kMaxExtrapolationSec) {
const float blend = glm::clamp(1.0f - (ageSec / kMaxExtrapolationSec), 0.0f, 1.0f);
transport.position += transport.serverLinearVelocity * (deltaTime * blend);
} else if (transport.serverUpdateCount <= 1 &&
ageSec >= 1.0f &&
path.fromDBC && !path.zOnly && path.durationMs > 0 && path.points.size() > 1 &&
((transport.guid & 0xFFF0000000000000ULL) == 0x1FC0000000000000ULL)) {
// Spawn-only fallback: only for world transport GUIDs (0x1fc...), not all transport-like objects.
glm::vec3 localTarget = transport.position - transport.basePosition;
uint32_t bestTimeMs = 0;
float bestScore = FLT_MAX;
float bestD2 = FLT_MAX;
constexpr uint32_t samples = 600;
for (uint32_t i = 0; i < samples; ++i) {
uint32_t t = static_cast<uint32_t>((static_cast<uint64_t>(i) * path.durationMs) / samples);
glm::vec3 off = evalTimedCatmullRom(path, t);
glm::vec3 d = off - localTarget;
float d2 = glm::dot(d, d);
float score = d2;
if (transport.hasServerYaw) {
constexpr uint32_t kHeadingDtMs = 250;
uint32_t tNext = (t + kHeadingDtMs) % path.durationMs;
glm::vec3 offNext = evalTimedCatmullRom(path, tNext);
glm::vec3 tangent = offNext - off;
if (glm::length2(tangent) > 1e-6f) {
float yaw = std::atan2(tangent.y, tangent.x);
float dyaw = yaw - transport.serverYaw;
while (dyaw > glm::pi<float>()) dyaw -= glm::two_pi<float>();
while (dyaw < -glm::pi<float>()) dyaw += glm::two_pi<float>();
constexpr float kHeadingWeight = 60.0f;
score += (kHeadingWeight * std::abs(dyaw)) * (kHeadingWeight * std::abs(dyaw));
}
}
if (score < bestScore) {
bestScore = score;
bestD2 = d2;
bestTimeMs = t;
}
}
constexpr float kMaxPhaseDrift = 120.0f;
if (bestD2 <= (kMaxPhaseDrift * kMaxPhaseDrift)) {
bool reverse = false;
if (transport.hasServerYaw) {
constexpr uint32_t kYawDtMs = 250;
uint32_t tNext = (bestTimeMs + kYawDtMs) % path.durationMs;
glm::vec3 p0 = evalTimedCatmullRom(path, bestTimeMs);
glm::vec3 p1 = evalTimedCatmullRom(path, tNext);
glm::vec3 d = p1 - p0;
if (glm::length2(d) > 1e-6f) {
float yawFwd = std::atan2(d.y, d.x);
float yawRev = yawFwd + glm::pi<float>();
auto angleDiff = [](float a, float b) -> float {
float d = a - b;
while (d > glm::pi<float>()) d -= glm::two_pi<float>();
while (d < -glm::pi<float>()) d += glm::two_pi<float>();
return std::abs(d);
};
reverse = angleDiff(yawRev, transport.serverYaw) < angleDiff(yawFwd, transport.serverYaw);
}
}
transport.useClientAnimation = true;
transport.localClockMs = bestTimeMs;
transport.clientAnimationReverse = reverse;
LOG_WARNING("TransportManager: No follow-up server updates for world transport 0x", std::hex, transport.guid, std::dec,
" (", ageSec, "s since spawn); enabling guarded fallback at t=", bestTimeMs,
"ms (phaseDrift=", std::sqrt(bestD2), ", reverse=", reverse, ")");
}
return;
}
updateTransformMatrices(transport);
if (wmoRenderer_) {
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
}
return;
}
// Evaluate position from time (path is local offsets, add base position)
glm::vec3 pathOffset = evalTimedCatmullRom(path, pathTimeMs);
// Guard against bad fallback Z curves on some remapped transport paths (notably icebreakers),
// where path offsets can sink far below sea level when we only have spawn-time data.
if (transport.useClientAnimation && transport.serverUpdateCount <= 1) {
constexpr float kMinFallbackZOffset = -2.0f;
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
}
transport.position = transport.basePosition + pathOffset;
// Use server yaw if available (authoritative), otherwise compute from tangent
@ -503,10 +595,15 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
return;
}
const bool hadPrevUpdate = (transport->serverUpdateCount > 0);
const float prevUpdateTime = transport->lastServerUpdate;
const glm::vec3 prevPos = transport->position;
// Track server updates
transport->serverUpdateCount++;
transport->lastServerUpdate = elapsedTime_;
transport->useClientAnimation = false; // Server updates take precedence
transport->clientAnimationReverse = false;
auto pathIt = paths_.find(transport->pathId);
if (pathIt == paths_.end() || pathIt->second.durationMs == 0) {
@ -521,162 +618,41 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
return;
}
const auto& path = pathIt->second;
// Z-only paths (elevator/bobbing): server is authoritative, no projection needed
if (path.zOnly) {
transport->position = position;
transport->serverYaw = orientation;
transport->hasServerYaw = true;
transport->rotation = glm::angleAxis(transport->serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
transport->useClientAnimation = false; // Server-driven
updateTransformMatrices(*transport);
if (wmoRenderer_) {
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
}
LOG_INFO("TransportManager: Z-only transport 0x", std::hex, guid, std::dec,
" updated from server: pos=(", position.x, ", ", position.y, ", ", position.z, ")");
return;
// Server-authoritative transport mode:
// Trust explicit server world position/orientation directly for all moving transports.
// This avoids wrong-route and direction errors when local DBC path mapping differs from server route IDs.
transport->hasServerClock = false;
transport->useClientAnimation = false;
if (transport->serverUpdateCount == 1) {
// Seed once from first authoritative update; keep stable base for fallback phase estimation.
transport->basePosition = position;
}
// Seed basePosition from t=0 assumption before first search
// (t=0 corresponds to spawn point / first path point)
if (!transport->hasServerClock) {
glm::vec3 offset0 = evalTimedCatmullRom(path, 0);
transport->basePosition = position - offset0;
}
// Estimate server's path time by projecting position onto path
// Path positions are local offsets, server position is world position
// basePosition = serverWorldPos - pathLocalOffset
uint32_t bestTimeMs = 0;
float bestD2 = FLT_MAX;
glm::vec3 bestPathOffset(0.0f);
// After initial sync, search only in small window around predicted time
bool hasInitialSync = transport->hasServerClock;
uint32_t nowMs = (uint32_t)(elapsedTime_ * 1000.0f);
uint32_t predictedTimeMs = 0;
if (hasInitialSync) {
// Predict where server should be based on last clock offset
int64_t serverTimeMs = (int64_t)nowMs + transport->serverClockOffsetMs;
int64_t mod = (int64_t)path.durationMs;
int64_t wrapped = serverTimeMs % mod;
if (wrapped < 0) wrapped += mod;
predictedTimeMs = (uint32_t)wrapped;
}
uint32_t searchStart = 0;
uint32_t searchEnd = path.durationMs;
uint32_t sampleCount = 1000; // Dense sampling for accuracy
if (hasInitialSync) {
// Search in ±5 second window around predicted time
uint32_t windowMs = 5000;
searchStart = (predictedTimeMs > windowMs) ? (predictedTimeMs - windowMs) : 0;
searchEnd = glm::min(predictedTimeMs + windowMs, path.durationMs);
sampleCount = 200; // Fewer samples needed in small window
}
for (uint32_t i = 0; i < sampleCount; i++) {
// Map i to [searchStart, searchEnd)
uint32_t testTimeMs = searchStart + (uint32_t)((uint64_t)i * (searchEnd - searchStart) / sampleCount);
glm::vec3 testPathOffset = evalTimedCatmullRom(path, testTimeMs);
glm::vec3 testWorldPos = transport->basePosition + testPathOffset; // Convert local → world
glm::vec3 diff = testWorldPos - position;
float d2 = glm::dot(diff, diff); // distance² (cheaper, no sqrt)
if (d2 < bestD2) {
bestD2 = d2;
bestTimeMs = testTimeMs;
bestPathOffset = testPathOffset;
}
}
// Refine with finer sampling around best match
uint32_t refineSampleCount = 50;
uint32_t refineWindow = glm::max(1u, (searchEnd - searchStart) / sampleCount); // Clamp to prevent zero
uint32_t refineStart = (bestTimeMs > refineWindow) ? (bestTimeMs - refineWindow) : 0;
uint32_t refineEnd = glm::min(bestTimeMs + refineWindow, path.durationMs);
uint32_t refineInterval = (refineEnd > refineStart) ? ((refineEnd - refineStart) / refineSampleCount) : 1;
if (refineInterval > 0) {
for (uint32_t i = 0; i < refineSampleCount; i++) {
uint32_t testTimeMs = refineStart + i * refineInterval;
glm::vec3 testPathOffset = evalTimedCatmullRom(path, testTimeMs); // local offset
glm::vec3 testWorldPos = transport->basePosition + testPathOffset; // Convert local → world
glm::vec3 diff = testWorldPos - position; // Compare world to world
float d2 = glm::dot(diff, diff);
if (d2 < bestD2) {
bestD2 = d2;
bestTimeMs = testTimeMs;
bestPathOffset = testPathOffset; // Update best offset when improving match
}
}
}
float bestDistance = std::sqrt(bestD2);
// Infer base position: serverWorldPos = basePos + pathOffset
// So: basePos = serverWorldPos - pathOffset
glm::vec3 inferredBasePos = position - bestPathOffset;
// Compute server clock offset with wrap-aware smoothing
int32_t newOffset = (int32_t)bestTimeMs - (int32_t)nowMs;
if (!transport->hasServerClock) {
// First sync: accept immediately and set base position
transport->basePosition = inferredBasePos;
transport->serverClockOffsetMs = newOffset;
transport->hasServerClock = true;
LOG_INFO("TransportManager: Initial server clock sync for transport 0x", std::hex, guid, std::dec,
" serverTime=", bestTimeMs, "ms / ", path.durationMs, "ms",
" drift=", bestDistance, " units",
" basePos=(", inferredBasePos.x, ", ", inferredBasePos.y, ", ", inferredBasePos.z, ")",
" offset=", newOffset, "ms");
} else {
// Subsequent syncs: wrap-aware smoothing to avoid phase jumps
int32_t oldOffset = transport->serverClockOffsetMs;
int32_t delta = newOffset - oldOffset;
int32_t mod = (int32_t)path.durationMs;
// Wrap delta to shortest path: [-mod/2, mod/2]
if (delta > mod / 2) delta -= mod;
if (delta < -mod / 2) delta += mod;
// Smooth delta, not absolute offset
transport->serverClockOffsetMs = oldOffset + (int32_t)(0.1f * delta);
// Only update basePosition if projection is accurate (< 5 units drift)
// This prevents "swim" from projection noise near ambiguous geometry
if (bestDistance < 5.0f) {
transport->basePosition = glm::mix(transport->basePosition, inferredBasePos, 0.1f);
LOG_INFO("TransportManager: Server clock correction for transport 0x", std::hex, guid, std::dec,
" drift=", bestDistance, " units (updated base)",
" oldOffset=", oldOffset, "ms → newOffset=", transport->serverClockOffsetMs, "ms",
" (delta=", delta, "ms, smoothed by 0.1)");
} else {
LOG_INFO("TransportManager: Server clock correction for transport 0x", std::hex, guid, std::dec,
" drift=", bestDistance, " units (base unchanged, clock only)",
" oldOffset=", oldOffset, "ms → newOffset=", transport->serverClockOffsetMs, "ms",
" (delta=", delta, "ms, smoothed by 0.1)");
}
}
// Update position immediately from synced clock
glm::vec3 pathOffset = evalTimedCatmullRom(path, bestTimeMs);
transport->position = transport->basePosition + pathOffset;
// Store server's authoritative yaw (orientation is in radians around Z axis)
transport->position = position;
transport->serverYaw = orientation;
transport->hasServerYaw = true;
transport->rotation = glm::angleAxis(transport->serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
if (hadPrevUpdate) {
const float dt = elapsedTime_ - prevUpdateTime;
if (dt > 0.001f) {
glm::vec3 v = (position - prevPos) / dt;
const float speed = glm::length(v);
constexpr float kMaxSpeed = 60.0f;
if (speed > kMaxSpeed) {
v *= (kMaxSpeed / speed);
}
transport->serverLinearVelocity = v;
transport->serverAngularVelocity = 0.0f;
transport->hasServerVelocity = true;
}
}
updateTransformMatrices(*transport);
if (wmoRenderer_) {
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
}
return;
}
bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMgr) {
@ -873,6 +849,30 @@ bool TransportManager::hasPathForEntry(uint32_t entry) const {
return it != paths_.end() && it->second.fromDBC;
}
bool TransportManager::hasUsableMovingPathForEntry(uint32_t entry, float minXYRange) const {
auto it = paths_.find(entry);
if (it == paths_.end()) return false;
const auto& path = it->second;
if (!path.fromDBC || path.points.size() < 2 || path.durationMs == 0 || path.zOnly) {
return false;
}
float minX = path.points.front().pos.x;
float maxX = minX;
float minY = path.points.front().pos.y;
float maxY = minY;
for (const auto& p : path.points) {
minX = std::min(minX, p.pos.x);
maxX = std::max(maxX, p.pos.x);
minY = std::min(minY, p.pos.y);
maxY = std::max(maxY, p.pos.y);
}
float rangeXY = std::max(maxX - minX, maxY - minY);
return rangeXY >= minXYRange;
}
uint32_t TransportManager::inferMovingPathForSpawn(const glm::vec3& spawnWorldPos, float maxDistance) const {
float bestD2 = maxDistance * maxDistance;
uint32_t bestPathId = 0;

View file

@ -173,16 +173,15 @@ bool Minimap::initialize(int size) {
if (maxDist > 0.5) discard;
// Rotate screen coords → composite UV offset
// Composite: U increases east, V increases south
// Composite: U increases east, V increases north
// Screen: +X=right, +Y=up
// The -cos(a) term in dV inherently flips V (screen up → composite north)
float c = cos(uRotation);
float s = sin(uRotation);
float scale = uZoomRadius * 2.0;
vec2 offset = vec2(
centered.x * c + centered.y * s,
centered.x * s - centered.y * c
-centered.x * s + centered.y * c
) * scale;
vec2 uv = uPlayerUV + offset;
@ -194,7 +193,7 @@ bool Minimap::initialize(int size) {
}
// Player arrow at center (always points up = forward)
vec2 ap = rot2(centered, -uArrowRotation);
vec2 ap = rot2(centered, -(uArrowRotation + 3.14159265));
vec2 tip = vec2(0.0, 0.035);
vec2 lt = vec2(-0.018, -0.016);
vec2 rt = vec2(0.018, -0.016);

View file

@ -77,7 +77,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
auto* renderer = core::Application::getInstance().getRenderer();
if (renderer) {
if (auto* minimap = renderer->getMinimap()) {
minimap->setRotateWithCamera(minimapRotate_);
minimapRotate_ = false;
pendingMinimapRotate = false;
minimap->setRotateWithCamera(false);
minimap->setSquareShape(minimapSquare_);
minimapSettingsApplied_ = true;
}
@ -4651,10 +4653,12 @@ void GameScreen::renderSettingsWindow() {
saveSettings();
}
if (ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate)) {
minimapRotate_ = pendingMinimapRotate;
// Force north-up minimap.
minimapRotate_ = false;
pendingMinimapRotate = false;
if (renderer) {
if (auto* minimap = renderer->getMinimap()) {
minimap->setRotateWithCamera(minimapRotate_);
minimap->setRotateWithCamera(false);
}
}
saveSettings();
@ -4836,7 +4840,7 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
float bearing = 0.0f;
float cosB = 1.0f;
float sinB = 0.0f;
if (minimapRotate_) {
if (minimap->isRotateWithCamera()) {
glm::vec3 fwd = camera->getForward();
bearing = std::atan2(-fwd.x, fwd.y);
cosB = std::cos(bearing);
@ -5115,9 +5119,9 @@ void GameScreen::loadSettings() {
uiOpacity_ = static_cast<float>(v) / 100.0f;
}
} else if (key == "minimap_rotate") {
int v = std::stoi(val);
minimapRotate_ = (v != 0);
pendingMinimapRotate = minimapRotate_;
// Ignore persisted rotate state; keep north-up.
minimapRotate_ = false;
pendingMinimapRotate = false;
} else if (key == "minimap_square") {
int v = std::stoi(val);
minimapSquare_ = (v != 0);