Floating-point fmod() loses precision with large accumulated time values, causing
subtle jumps/hitches in animation loops. Replace with iterative duration subtraction
to keep animationTime bounded and maintain precision, consistent with the fix
applied to character_renderer.cpp.
Applies to:
- M2 creature/object animation loops (main update)
- M2 particle-only instance wrapping (3333ms limit)
- M2 global sequence timing resolution
- M2 animated particle tile indexing
- Mount bobbing motion (sinusoidal rider motion)
- Character footstep trigger timing
- Mount footstep trigger timing
All timing computations now use the same precision-preserving approach.
Addresses sparseness in lava/magma effects noted in status documentation.
Higher emission rate (48 vs 32 per second) makes lava/slime areas visually
denser and more immersive while staying within GPU budget constraints.
Replace floating-point fmod() with iterative duration subtraction to preserve precision.
When animation time accumulates over many loops, fmod() loses precision with large values,
causing subtle jumps/hitches in looping animations. Subtracting the duration instead keeps
animationTime bounded in [0, duration) and avoids precision loss.
Double the smoke particle emission rate to create visually richer lava and magma
effects. Current implementation emitted only 16 particles/sec per emitter (~88 in
steady state), which appeared sparse especially in multi-emitter lava areas.
Increasing to 32/sec provides denser steam/smoke effects (~176 in steady state)
while remaining well under the 1000 particle cap. This tuning opportunity was
documented in status.md as a known gap in visual completeness.
Add view-frustum intersection testing to QuestMarkerRenderer::render() using
Frustum::intersectsSphere(), bringing quest marker culling in line with the
character instance and WMO group frustum culling improvements. Reduces marker
visibility testing overhead in scenes with many off-screen quest givers.
Replace ad-hoc cone-based backface culling with proper view-frustum intersection
testing using Frustum::intersectsSphere(). Characters are now culled based on
visibility within the view frustum, improving accuracy in complex scenes and
reducing overdraw. Maintains distance-based culling for broad radius filtering.
Replace the basic forward-vector culling (which only culls when all AABB
corners are behind the camera) with proper frustum-AABB intersection testing
for more accurate and aggressive visibility culling. This reduces overdraw
and improves rendering performance in WMO-heavy scenes (dungeons, buildings).
Enable shadows in character preview with 0.5 strength for a subtle
lighting effect that improves visual accuracy. Removes clearShadowMap()
call and enables shadowParams in preview UBO. Enhances character
appearance fidelity when viewing equipment and customization options.
Move ShadowParamsUBO from 5 separate shadow rendering functions (2 in
m2_renderer, 1 in terrain_renderer, 1 in wmo_renderer) into shared
vk_frame_data.hpp header. Eliminates 5 identical local struct definitions
and improves consistency across all shadow pass implementations. Structure
layout matches shader std140 uniform buffer requirements.
Move envSizeMBOrDefault and envSizeOrDefault from 4 separate rendering
modules (character_renderer, m2_renderer, terrain_renderer, wmo_renderer)
into shared vk_utils.hpp header as inline functions. Use the most robust
version which includes overflow checking for MB-to-bytes conversion. This
eliminates 7 identical local function definitions and improves consistency
across all rendering modules.
Move ShadowPush from 4 separate rendering modules (character_renderer,
m2_renderer, terrain_renderer, wmo_renderer) into shared vk_frame_data.hpp
header. This eliminates 4 identical local struct definitions and ensures
consistency across all shadow rendering passes. Add vk_frame_data.hpp include
to character_renderer.cpp.
Consolidated duplicate attachment point resolution code used by both
attachWeapon() and getAttachmentTransform(). New findAttachmentBone()
helper encapsulates the complete lookup chain: attachment by ID, fallback
scan, key-bone fallback, and validation. Eliminates ~55 lines of duplicate
code while improving maintainability and consistency.
Consolidated identical key-bone lookup logic that appeared at lines 3076
and 3099. Both performed the same search for weapon attachment points
(ID 1/2 for right/left hand). Removed duplication while preserving
behavior and improving code clarity with better comments.
Add fallback logic to use bone 0 for head attachment point (ID 11) when models
don't have it explicitly defined. This improves helmet rendering compatibility
on humanoid NPC models that lack explicit attachment 11 definitions. Re-enable
helmet attachments now that the fallback logic is in place.
Allow R key to reset camera position/rotation when chat input is not active.
Previously disabled due to conflict with chat reply command. Now uses the same
safety check as movement keys (ImGui::GetIO().WantTextInput).
Implements edge-triggered reset on R key press, matching X key (sit) pattern.
Pre-allocate one stable VkDescriptorSet per particle emitter at model
upload time (particleTexSets[]) instead of allocating a new set from
materialDescPool_ every frame for each particle group. The per-frame
path exhausted the 8192-set pool in ~14 s at 60 fps with 10 active
particle emitters, causing GPU device-lost crashes. The old path is
kept as an explicit fallback but should never be reached in practice.
- character_renderer: playAnimation now prefers the primary variation
(variationIndex==0) when multiple sequences share the same animation ID;
this fixes hitching on human female run where a variation sequence was
selected first before the base cycle
- character_renderer: move the compositeWithRegions size-mismatch warning
inside the else branch so it only fires when sizes genuinely don't match,
not for every successful 1:1 or scaled blit
- terrain_renderer: add FREE_DESCRIPTOR_SET_BIT flag and vkFreeDescriptorSets
in destroyChunkGPU so material descriptor sets are returned to the pool;
prevents GPU device lost from pool exhaustion near populated areas
- game_screen: fix projectToMinimap to use the exact inverse of the minimap
shader transform so quest objective markers appear at the correct position
and orientation regardless of camera bearing
- inventory_screen: fix item comparison tooltip to not compare equipped items
against themselves (character screen); add item level diff line; show (=)
indicator when stats are equal rather than bare value which looked identical
to the item's own tooltip
When the server has not sent SMSG_INIT_WORLD_STATES or the mask is
empty, fall back to locally-accumulated explored zones tracked by
player position. The local set is cleared when a real server mask
arrives so it doesn't persist stale data.
Trivial/low-level quests now show gray '!' / '?' markers instead of
yellow, matching the in-game distinction between available and trivial
quests. Add grayscale parameter to QuestMarkerRenderer::setMarker and
the push-constant block; application sets grayscale=1.0 for trivial
markers and 0.0 for all others.
- Animation stutter: skip playAnimation(Run) for the local player in the
server movement callback — the player renderer state machine already manages
it; resetting animTime on every movement packet caused visible stutter
- Resolution crash: reorder swapchain recreation so old swapchain is only
destroyed after confirming the new build succeeded; add null-swapchain
guard in beginFrame to survive the retry window
- Memory cap: reduce cache budget from 80% uncapped to 50% hard-capped at
16 GB to prevent excessive RAM use on high-memory systems
- Spell tooltip: suppress "Drag to action bar / Double-click to cast" hints
when the tooltip is shown from the action bar (showUsageHints=false)
- M2 collision: add watermelon/melon/squash/gourd to foliage (no-collision);
exclude chair/bench/stool/seat/throne from smallSolidProp so invisible chair
bounding boxes no longer trap the player
On Windows arm64, wchar_t is unsigned so 'wc >= 0' is always true
and GCC/Clang emit -Wtype-limits. Drop the redundant lower bound
check — only the upper bound 'wc <= 0x7f' is needed.
- WotLK opcode 0x21E is aliased to both SMSG_SET_REST_START and
SMSG_QUEST_FORCE_REMOVE. In WotLK, treat as SET_REST_START (non-zero
= entering rest area, zero = leaving); Classic/TBC treat as quest removal.
- PLAYER_BYTES_2 rest state byte: change from `& 0x01` to `!= 0` to also
detect REST_TYPE_IN_CITY (value 2), not just REST_TYPE_IN_TAVERN (1).
- Minimap arrow: server orientation (π/2=North) needed conversion to
minimap arrow space (0=North). Subtract π/2 in both render paths so
arrow points North when player faces North.
Camera controller / sitting:
- Any movement key (WASD/QE/Space) pressed while sitting now clears the
sitting flag immediately, matching WoW's sit-to-stand-on-move behaviour
- Added StandUpCallback: when the player stands up via local input the
callback fires setStandState(0) → CMSG_STAND_STATE_CHANGE(STAND) so
the server releases the sit lock and restores normal movement
- Fixes character getting stuck in sit state after accidentally
right-clicking a chair GO in Goldshire Inn (or similar)
Nameplates:
- Use getRenderPositionForGuid() (renderer visual position) as primary
source for nameplate anchor, falling back to entity X/Y/Z only when
no render instance exists yet; keeps health bars in sync with the
rendered model instead of the parallel entity interpolator
- renderer: construct QuestMarkerRenderer via make_unique (was never
instantiated, causing getQuestMarkerRenderer() to always return null
and all quest-marker updates to be silently skipped)
- m2_renderer: add "levelup" to effectByName so LevelUp.m2 is treated
as a spell effect (additive blend, no collision, particle-dominated)
- renderer: auto-cancel non-looping emote animations when they reach
end-of-sequence, transitioning player back to IDLE state
- wmo_renderer: pass character position (not camera position) to portal
visibility traversal — the 3rd-person camera can orbit outside a WMO
while the character is inside, causing interior groups to cull; render()
now accepts optional viewerPos that defaults to camPos for compatibility
- renderer: pass &characterPosition to wmoRenderer->render() at both
main and single-threaded call sites; reflection pass keeps camPos
- renderer: apply mount pitch/roll to rider during all flight, not just
taxiFlight_ (fixes zero rider tilt during player-controlled flying)
- game_screen: format SAY/YELL/WHISPER/EMOTE using WoW-style "Name says:"
instead of "[SAY] Name:" bracket prefix
When flyingActive_, detect Space/X key transitions and emit proper flight
vertical movement opcodes so the server (and other players) see the
correct ascending/descending animation state:
- MSG_MOVE_START_ASCEND (Space pressed while flying) → sets ASCENDING flag
- MSG_MOVE_STOP_ASCEND (Space released while flying) → clears ASCENDING flag
- MSG_MOVE_START_DESCEND (X pressed while flying) → clears ASCENDING flag
- MSG_MOVE_STOP_ASCEND (X released while flying) → clears vertical state
Track wasAscending_/wasDescending_ member state to detect transitions.
Also clear lingering vertical state when leaving flight mode.
- Add getServerTurnRate() accessor and turnRateOverride_ field so the
keyboard turn speed respects SMSG_FORCE_TURN_RATE_CHANGE from server
- Convert rad/s → deg/s before applying to camera yaw logic
- Fix SMSG_SPLINE_SET_RUN_BACK/SWIM/FLIGHT/FLIGHT_BACK/SWIM_BACK/WALK/
TURN_RATE handlers: all previously discarded the value; now update the
corresponding serverXxxSpeed_ / serverTurnRate_ field when GUID matches
playerGuid (camera controller syncs these every frame)
SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE was already ACK'd and stored in
serverFlightBackSpeed_, but the value was never accessible or synced
to the CameraController. Backward flight movement always used forward
flight speed (flightSpeedOverride_), making it faster than the server
intended.
- Add getServerFlightBackSpeed() accessor in GameHandler
- Add flightBackSpeedOverride_ field and setter in CameraController
- Apply it in the fly movement block: backward-only flight uses the
back speed; forward or strafing uses the forward speed as WoW does
- Fallback: 50% of forward flight speed when override is unset
- Sync per-frame in application.cpp alongside the other speed overrides
Backward swimming was using 50% of forward swim speed as a hardcoded
fallback. Wire up the server-authoritative swim back speed so Warlock
Dark Pact, buffs, and server-forced speed changes all apply correctly
when swimming backward.
- game_handler.hpp: add getServerSwimBackSpeed() accessor
- camera_controller.hpp: add swimBackSpeedOverride_ field + setter
- camera_controller.cpp: apply swimBackSpeedOverride_ when player
swims backward without forward input; fall back to 50% of swim speed
- application.cpp: sync swim back speed each frame
When the server sets MovementFlags::HOVER (SMSG_MOVE_SET_HOVER), the
player now floats 4 yards above the nearest ground surface instead of
standing on it. Uses the existing floor-snap path with a HOVER_HEIGHT
offset applied to the snap target.
- game_handler.hpp: add isHovering() accessor (reads HOVER flag from
movementInfo.flags, which is already set by handleForceMoveFlagChange)
- camera_controller.hpp: add hoverActive_ field and setHoverActive()
- camera_controller.cpp: apply HOVER_HEIGHT = 4.0f offset at floor snap
- application.cpp: sync hover state each frame alongside other movement
states (gravity, feather fall, water walk, flying)
Previously only run speed was synced. Now all server-driven movement
speeds are forwarded to the camera controller each frame:
- runSpeedOverride_: server run speed (existing)
- walkSpeedOverride_: server walk speed (Ctrl key movement)
- swimSpeedOverride_: swim speed (Swim Form, Engineering fins)
- flightSpeedOverride_: flight speed (epic vs normal flying mounts)
- runBackSpeedOverride_: back-pedal speed
Each uses the server value when non-zero/sane, falling back to the
hardcoded WoW default constant otherwise.
serverFlightSpeed_ (from SMSG_FORCE_FLIGHT_SPEED_CHANGE) was stored but
never synced to CameraController. Add getServerFlightSpeed() accessor,
flightSpeedOverride_ field, and use it in the flying physics path so
normal vs epic flying mounts actually move at their correct speeds.
When CAN_FLY + FLYING movement flags are both set (flying mounts, Druid
Flight Form), the CameraController now uses 3D pitch-following movement
instead of ground physics:
- Forward/back follows the camera's 3D look direction (ascend when
looking up, descend when looking down)
- Space = ascend vertically, X (while mounted) = descend
- No gravity, no grounding, no jump coyote time
- Fall-damage checks suppressed (grounded=true)
Also wire up all remaining server movement state flags to CameraController:
- Feather Fall: cap terminal velocity at -2 m/s
- Water Walk: clamp to water surface, skip swim entry
- Flying: 3D movement with no gravity
All states synced each frame from GameHandler via isPlayerFlying(),
isFeatherFalling(), isWaterWalking(), isGravityDisabled().
SMSG_MOVE_WATER_WALK / SMSG_MOVE_LAND_WALK now correctly set/clear
WATER_WALK (0x00008000) in movementInfo.flags, ensuring the flag is
included in movement ACKs sent to the server.
In CameraController, when waterWalkActive_ is set and the player is
at or above the water surface (within 0.5 units), clamp them to the
water surface and mark as grounded — preventing water entry and allowing
them to walk across the water surface as the spell intends.
Feather Fall (SMSG_MOVE_FEATHER_FALL / SMSG_MOVE_NORMAL_FALL):
- Add FEATHER_FALL = 0x00004000 to MovementFlags enum
- Fix handlers to set/clear the flag instead of passing flag=0
- Cap downward terminal velocity at -2.0 m/s in CameraController when
feather fall is active (Slow Fall, Parachute, etc.)
All three handlers now correctly propagate server movement state flags
that were previously acknowledged without updating any local state.
serverWalkSpeed_ and serverSwimSpeed_ were stored in GameHandler but
never exposed or synced to the camera controller. The controller used
hardcoded WOW_WALK_SPEED and speed*SWIM_SPEED_FACTOR regardless of
server-sent speed changes.
Add getServerWalkSpeed()/getServerSwimSpeed() accessors, walkSpeedOverride_
and swimSpeedOverride_ fields in CameraController, and sync all three
server speeds each frame. Both swim speed sites (main and camera-collision
path) now use the override when set. This makes Slow debuffs (walk speed),
Swim Form, and Engineering fins actually affect movement speed.
SMSG_MOVE_GRAVITY_DISABLE/ENABLE now correctly set/clear the LEVITATING
movement flag instead of passing flag=0. GameHandler::isGravityDisabled()
reads the LEVITATING bit and is synced to CameraController each frame.
When gravity is disabled the physics loop bleeds off downward velocity
and skips gravity accumulation, so Levitate and similar effects actually
float the player rather than letting them fall through the world.
When SMSG_FORCE_MOVE_ROOT sets ROOT in movementInfo.flags, the
camera controller was not aware and continued to accept directional
input. This caused position desync (client moves, server sees player
as rooted).
- Add movementRooted_ flag to CameraController with setter/getter.
- Block nowForward/nowBackward/nowStrafe when movementRooted_ is set.
- Sync isPlayerRooted() from GameHandler to CameraController each
frame alongside the existing run-speed sync in application.cpp.
- Add GameHandler::isPlayerRooted() convenience accessor.
applyKnockBack() sets grounded=false and applies vertical velocity, but
the normal jump detection path (nowJump && !wasJumping && grounded) never
fires during a server-driven knockback because no jump key is pressed.
Without MSG_MOVE_JUMP the game_handler never sets MovementFlags::FALLING
in movementInfo.flags, so all subsequent heartbeat packets carry incorrect
flags — the server sees the player as grounded while airborne.
Fix: fire movementCallback(MSG_MOVE_JUMP) directly from applyKnockBack()
so the FALLING flag is set immediately. MSG_MOVE_FALL_LAND is already sent
when grounded becomes true again (the existing wasFalling && grounded path).
Previously the handler ACKed with current position and ignored the
velocity fields entirely (vcos/vsin/hspeed/vspeed were [[maybe_unused]]).
The server expects the client to fly through the air on knockback — without
simulation the player stays in place while the server models them as airborne,
causing position desync and rubberbanding.
Changes:
- CameraController: add applyKnockBack(vcos, vsin, hspeed, vspeed)
that sets knockbackHorizVel_ and launches verticalVelocity = -vspeed
(server sends vspeed as negative for upward launches, matching TrinityCore)
- Physics loop: each tick adds knockbackHorizVel_ to targetPos then applies
exponential drag (KNOCKBACK_HORIZ_DRAG=4.5/s) until velocity < 0.05 u/s
- GameHandler: parse all four fields, add KnockBackCallback, call it for
the local player so the camera controller receives the impulse
- Application: register the callback — routes server knockback to physics
The existing ACK path is unchanged; the server gets position confirmation
as before while the client now actually simulates the trajectory.
isPortalVisible() was computing the world-space AABB by directly
transforming pMin/pMax with the model matrix. This is incorrect for
rotated WMOs — when the model matrix includes rotations, components can
be swapped or negated, yielding an inverted AABB (worldMin.x >
worldMax.x) that causes frustum.intersectsAABB() to fail.
Fix: transform all 8 corners of the portal bounding box and take the
component-wise min/max, which gives the correct world-space AABB for any
rotation/scale. This was the root cause of portals being incorrectly
culled in rotated WMO instances (e.g. many dungeon and city WMOs).
Also squash the earlier spline-speed no-op fix (parse guid + float
instead of consuming the full packet for SMSG_SPLINE_SET_FLIGHT_SPEED
and friends) into this commit.
The renderer's CharAnimState machine already drives player character
animations (Run=5, Walk=4, Jump, Swim, etc.) — remove the conflicting
camera controller code added in the previous commit.
Fix creature movement animations to use the correct WoW M2 IDs:
4=Walk, 5=Run. Both the per-frame sync loop and the SMSG_MONSTER_MOVE
spline callback now use Run (5) for NPC movement.
CameraController now plays Walk (5) for backpedal/slow pace and Run (4)
for forward running (runPace), matching WoW's animation convention.
Also handles transitions between Walk and Run while already moving.
CameraController now transitions the player character to Run (anim 4)
on movement start and back to Stand (anim 0) on stop, guarded by a
prevPlayerMoving_ flag so animation time is not reset every frame.
Death animation (anim 1) is never overridden.
Application creature sync similarly switches creature models to Run (4)
when they move between server positions and Stand (0) when they stop,
with per-guid creatureWasMoving_ tracking to avoid per-frame resets.
Add GhostStateCallback to GameHandler, fired when PLAYER_FLAGS_GHOST
transitions on or off in UPDATE_OBJECT / login detection. Add
setInstanceOpacity() to CharacterRenderer to directly set opacity
without disturbing fade-in state. Application wires the callback to
set opacity 0.5 on ghost entry and 1.0 on resurrect.