mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
Creatures were stuck in Run/Walk animation during the dead-reckoning overrun window (up to 2x movement duration). The animation check used isEntityMoving() which stays true through dead reckoning, causing creatures to "run in place" after reaching their destination. Add isActivelyMoving() which is true only during the active interpolation phase (moveElapsed < moveDuration), and use it for animation state transitions. Dead reckoning still works for position extrapolation — only the animation now correctly stops at arrival.
361 lines
12 KiB
C++
361 lines
12 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <map>
|
|
#include <memory>
|
|
|
|
namespace wowee {
|
|
namespace game {
|
|
|
|
/**
|
|
* Object type IDs for WoW 3.3.5a
|
|
*/
|
|
enum class ObjectType : uint8_t {
|
|
OBJECT = 0,
|
|
ITEM = 1,
|
|
CONTAINER = 2,
|
|
UNIT = 3,
|
|
PLAYER = 4,
|
|
GAMEOBJECT = 5,
|
|
DYNAMICOBJECT = 6,
|
|
CORPSE = 7
|
|
};
|
|
|
|
/**
|
|
* Object type masks for update packets
|
|
*/
|
|
enum class TypeMask : uint16_t {
|
|
OBJECT = 0x0001,
|
|
ITEM = 0x0002,
|
|
CONTAINER = 0x0004,
|
|
UNIT = 0x0008,
|
|
PLAYER = 0x0010,
|
|
GAMEOBJECT = 0x0020,
|
|
DYNAMICOBJECT = 0x0040,
|
|
CORPSE = 0x0080
|
|
};
|
|
|
|
/**
|
|
* Update types for SMSG_UPDATE_OBJECT
|
|
*/
|
|
enum class UpdateType : uint8_t {
|
|
VALUES = 0, // Partial update (changed fields only)
|
|
MOVEMENT = 1, // Movement update
|
|
CREATE_OBJECT = 2, // Create new object (full data)
|
|
CREATE_OBJECT2 = 3, // Create new object (alternate format)
|
|
OUT_OF_RANGE_OBJECTS = 4, // Objects left view range
|
|
NEAR_OBJECTS = 5 // Objects entered view range
|
|
};
|
|
|
|
/**
|
|
* Base entity class for all game objects
|
|
*/
|
|
class Entity {
|
|
public:
|
|
Entity() = default;
|
|
explicit Entity(uint64_t guid) : guid(guid) {}
|
|
virtual ~Entity() = default;
|
|
|
|
// GUID access
|
|
uint64_t getGuid() const { return guid; }
|
|
void setGuid(uint64_t g) { guid = g; }
|
|
|
|
// Position
|
|
float getX() const { return x; }
|
|
float getY() const { return y; }
|
|
float getZ() const { return z; }
|
|
float getOrientation() const { return orientation; }
|
|
// Update orientation only, without disrupting an in-progress movement interpolation.
|
|
void setOrientation(float o) { orientation = o; }
|
|
|
|
void setPosition(float px, float py, float pz, float o) {
|
|
x = px;
|
|
y = py;
|
|
z = pz;
|
|
orientation = o;
|
|
isMoving_ = false; // Instant position set cancels interpolation
|
|
}
|
|
|
|
// Movement interpolation (syncs entity position with renderer during movement)
|
|
void startMoveTo(float destX, float destY, float destZ, float destO, float durationSec) {
|
|
if (durationSec <= 0.0f) {
|
|
setPosition(destX, destY, destZ, destO);
|
|
return;
|
|
}
|
|
// 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.
|
|
float fromX = isMoving_ ? moveEndX_ : x;
|
|
float fromY = isMoving_ ? moveEndY_ : y;
|
|
float fromZ = isMoving_ ? moveEndZ_ : z;
|
|
float impliedVX = (destX - fromX) / durationSec;
|
|
float impliedVY = (destY - fromY) / durationSec;
|
|
float impliedVZ = (destZ - fromZ) / durationSec;
|
|
// Exponentially smooth velocity so jittery packet timing doesn't snap speed.
|
|
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_;
|
|
|
|
moveStartX_ = x; moveStartY_ = y; moveStartZ_ = z;
|
|
moveEndX_ = destX; moveEndY_ = destY; moveEndZ_ = destZ;
|
|
moveDuration_ = durationSec;
|
|
moveElapsed_ = 0.0f;
|
|
orientation = destO;
|
|
isMoving_ = true;
|
|
}
|
|
|
|
void updateMovement(float deltaTime) {
|
|
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;
|
|
} 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
|
|
// drift endlessly if the entity stops sending packets.
|
|
float overrun = moveElapsed_ - moveDuration_;
|
|
if (overrun < moveDuration_) {
|
|
x = moveEndX_ + velX_ * overrun;
|
|
y = moveEndY_ + velY_ * overrun;
|
|
z = moveEndZ_ + velZ_ * overrun;
|
|
} else {
|
|
// Two intervals with no update — entity has probably stopped.
|
|
x = moveEndX_; y = moveEndY_; z = moveEndZ_;
|
|
velX_ = 0.0f; velY_ = 0.0f; velZ_ = 0.0f;
|
|
isMoving_ = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isEntityMoving() const { return isMoving_; }
|
|
|
|
/// True only during the active interpolation phase (before reaching destination).
|
|
/// Unlike isEntityMoving(), this does NOT include the dead-reckoning overrun window,
|
|
/// so animations (Run/Walk) should use this to avoid "running in place" after arrival.
|
|
bool isActivelyMoving() const {
|
|
return isMoving_ && moveElapsed_ < moveDuration_;
|
|
}
|
|
|
|
// Returns the latest server-authoritative position: destination if moving, current if not.
|
|
// Unlike getX/Y/Z (which only update via updateMovement), this always reflects the
|
|
// last known server position regardless of distance culling.
|
|
float getLatestX() const { return isMoving_ ? moveEndX_ : x; }
|
|
float getLatestY() const { return isMoving_ ? moveEndY_ : y; }
|
|
float getLatestZ() const { return isMoving_ ? moveEndZ_ : z; }
|
|
|
|
// Object type
|
|
ObjectType getType() const { return type; }
|
|
void setType(ObjectType t) { type = t; }
|
|
|
|
// Fields (for update values)
|
|
void setField(uint16_t index, uint32_t value) {
|
|
fields[index] = value;
|
|
}
|
|
|
|
uint32_t getField(uint16_t index) const {
|
|
auto it = fields.find(index);
|
|
return (it != fields.end()) ? it->second : 0;
|
|
}
|
|
|
|
bool hasField(uint16_t index) const {
|
|
return fields.find(index) != fields.end();
|
|
}
|
|
|
|
const std::map<uint16_t, uint32_t>& getFields() const {
|
|
return fields;
|
|
}
|
|
|
|
protected:
|
|
uint64_t guid = 0;
|
|
ObjectType type = ObjectType::OBJECT;
|
|
|
|
// Position
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
float z = 0.0f;
|
|
float orientation = 0.0f;
|
|
|
|
// Update fields (dynamic values)
|
|
std::map<uint16_t, uint32_t> fields;
|
|
|
|
// Movement interpolation state
|
|
bool isMoving_ = 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
|
|
};
|
|
|
|
/**
|
|
* Unit entity (NPCs, creatures, players)
|
|
*/
|
|
class Unit : public Entity {
|
|
public:
|
|
Unit() { type = ObjectType::UNIT; }
|
|
explicit Unit(uint64_t guid) : Entity(guid) { type = ObjectType::UNIT; }
|
|
|
|
// Name
|
|
const std::string& getName() const { return name; }
|
|
void setName(const std::string& n) { name = n; }
|
|
|
|
// Health
|
|
uint32_t getHealth() const { return health; }
|
|
void setHealth(uint32_t h) { health = h; }
|
|
|
|
uint32_t getMaxHealth() const { return maxHealth; }
|
|
void setMaxHealth(uint32_t h) { maxHealth = h; }
|
|
|
|
// Power (mana/rage/energy) — indexed by power type (0-6)
|
|
uint32_t getPower() const { return powers[powerType < 7 ? powerType : 0]; }
|
|
void setPower(uint32_t p) { powers[powerType < 7 ? powerType : 0] = p; }
|
|
void setPowerByType(uint8_t type, uint32_t p) { if (type < 7) powers[type] = p; }
|
|
|
|
uint32_t getMaxPower() const { return maxPowers[powerType < 7 ? powerType : 0]; }
|
|
void setMaxPower(uint32_t p) { maxPowers[powerType < 7 ? powerType : 0] = p; }
|
|
void setMaxPowerByType(uint8_t type, uint32_t p) { if (type < 7) maxPowers[type] = p; }
|
|
|
|
uint32_t getPowerByType(uint8_t type) const { return type < 7 ? powers[type] : 0; }
|
|
uint32_t getMaxPowerByType(uint8_t type) const { return type < 7 ? maxPowers[type] : 0; }
|
|
|
|
uint8_t getPowerType() const { return powerType; }
|
|
void setPowerType(uint8_t t) { powerType = t; }
|
|
|
|
// Level
|
|
uint32_t getLevel() const { return level; }
|
|
void setLevel(uint32_t l) { level = l; }
|
|
|
|
// Entry ID (creature template entry)
|
|
uint32_t getEntry() const { return entry; }
|
|
void setEntry(uint32_t e) { entry = e; }
|
|
|
|
// Display ID (model display)
|
|
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; }
|
|
|
|
// Dynamic flags (UNIT_DYNAMIC_FLAGS, index 147)
|
|
uint32_t getDynamicFlags() const { return dynamicFlags; }
|
|
void setDynamicFlags(uint32_t f) { dynamicFlags = f; }
|
|
|
|
// NPC flags (UNIT_NPC_FLAGS, index 82)
|
|
uint32_t getNpcFlags() const { return npcFlags; }
|
|
void setNpcFlags(uint32_t f) { npcFlags = f; }
|
|
|
|
// Returns true if NPC has interaction flags (gossip/vendor/quest/trainer)
|
|
bool isInteractable() const { return npcFlags != 0; }
|
|
|
|
// Faction-based hostility
|
|
uint32_t getFactionTemplate() const { return factionTemplate; }
|
|
void setFactionTemplate(uint32_t f) { factionTemplate = f; }
|
|
bool isHostile() const { return hostile; }
|
|
void setHostile(bool h) { hostile = h; }
|
|
|
|
protected:
|
|
std::string name;
|
|
uint32_t health = 0;
|
|
uint32_t maxHealth = 0;
|
|
uint32_t powers[7] = {}; // Indexed by power type (0=mana,1=rage,2=focus,3=energy,4=happiness,5=runes,6=runic)
|
|
uint32_t maxPowers[7] = {}; // Max values per power type
|
|
uint8_t powerType = 0; // Active power type
|
|
uint32_t level = 1;
|
|
uint32_t entry = 0;
|
|
uint32_t displayId = 0;
|
|
uint32_t mountDisplayId = 0;
|
|
uint32_t unitFlags = 0;
|
|
uint32_t dynamicFlags = 0;
|
|
uint32_t npcFlags = 0;
|
|
uint32_t factionTemplate = 0;
|
|
bool hostile = false;
|
|
};
|
|
|
|
/**
|
|
* Player entity
|
|
*/
|
|
class Player : public Unit {
|
|
public:
|
|
Player() { type = ObjectType::PLAYER; }
|
|
explicit Player(uint64_t guid) : Unit(guid) { type = ObjectType::PLAYER; }
|
|
|
|
// Name
|
|
const std::string& getName() const { return name; }
|
|
void setName(const std::string& n) { name = n; }
|
|
|
|
protected:
|
|
std::string name;
|
|
};
|
|
|
|
/**
|
|
* GameObject entity (doors, chests, etc.)
|
|
*/
|
|
class GameObject : public Entity {
|
|
public:
|
|
GameObject() { type = ObjectType::GAMEOBJECT; }
|
|
explicit GameObject(uint64_t guid) : Entity(guid) { type = ObjectType::GAMEOBJECT; }
|
|
|
|
const std::string& getName() const { return name; }
|
|
void setName(const std::string& n) { name = n; }
|
|
|
|
uint32_t getEntry() const { return entry; }
|
|
void setEntry(uint32_t e) { entry = e; }
|
|
|
|
uint32_t getDisplayId() const { return displayId; }
|
|
void setDisplayId(uint32_t id) { displayId = id; }
|
|
|
|
protected:
|
|
std::string name;
|
|
uint32_t entry = 0;
|
|
uint32_t displayId = 0;
|
|
};
|
|
|
|
/**
|
|
* Entity manager for tracking all entities in view
|
|
*/
|
|
class EntityManager {
|
|
public:
|
|
// Add entity
|
|
void addEntity(uint64_t guid, std::shared_ptr<Entity> entity);
|
|
|
|
// Remove entity
|
|
void removeEntity(uint64_t guid);
|
|
|
|
// Get entity
|
|
std::shared_ptr<Entity> getEntity(uint64_t guid) const;
|
|
|
|
// Check if entity exists
|
|
bool hasEntity(uint64_t guid) const;
|
|
|
|
// Get all entities
|
|
const std::map<uint64_t, std::shared_ptr<Entity>>& getEntities() const {
|
|
return entities;
|
|
}
|
|
|
|
// Clear all entities
|
|
void clear() {
|
|
entities.clear();
|
|
}
|
|
|
|
// Get entity count
|
|
size_t getEntityCount() const {
|
|
return entities.size();
|
|
}
|
|
|
|
private:
|
|
std::map<uint64_t, std::shared_ptr<Entity>> entities;
|
|
};
|
|
|
|
} // namespace game
|
|
} // namespace wowee
|