mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
105 commits
5adb5e0e9f
...
a90f2acd26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a90f2acd26 | ||
|
|
562fc13a6a | ||
|
|
79c0887db2 | ||
|
|
a4c23b7fa2 | ||
|
|
9c276d1072 | ||
|
|
a336bebbe5 | ||
|
|
f3e399e0ff | ||
|
|
db1f111054 | ||
|
|
afcd6f2db3 | ||
|
|
20fef40b7b | ||
|
|
9fe7bbf826 | ||
|
|
661f7e3e8d | ||
|
|
36d40905e1 | ||
|
|
367390a852 | ||
|
|
2f0fe302bc | ||
|
|
1f7f1076ca | ||
|
|
ef08366efc | ||
|
|
611946bd3e | ||
|
|
71b0a18238 | ||
|
|
e781ede5b2 | ||
|
|
88c3cfe7ab | ||
|
|
84b31d3bbd | ||
|
|
aaae07e477 | ||
|
|
ec93981a9d | ||
|
|
aaa3649975 | ||
|
|
c1a090a17c | ||
|
|
d8d59dcdc8 | ||
|
|
09d4a6ab41 | ||
|
|
fb8377f3ca | ||
|
|
7acaa4d301 | ||
|
|
8a24638ced | ||
|
|
eaf60b4f79 | ||
|
|
ee7de739fc | ||
|
|
641f943268 | ||
|
|
0b14141e97 | ||
|
|
30b821f7ba | ||
|
|
339ee4dbba | ||
|
|
b5131b19a3 | ||
|
|
f4a31fef2a | ||
|
|
c73da1629b | ||
|
|
41d121df1d | ||
|
|
8f68d1efb9 | ||
|
|
d6d70f62c7 | ||
|
|
25e90acf27 | ||
|
|
2fbf3f28e6 | ||
|
|
92ce7459bb | ||
|
|
dedafdf443 | ||
|
|
06c8c26b8a | ||
|
|
6649f1583a | ||
|
|
b5567b362f | ||
|
|
8a9248be79 | ||
|
|
844a0002dc | ||
|
|
0674dc9ec2 | ||
|
|
e41788c63f | ||
|
|
6cd8dc0d9d | ||
|
|
d9ce056e13 | ||
|
|
dd8e09c2d9 | ||
|
|
ddb8f06c3a | ||
|
|
d26bac0221 | ||
|
|
7cb4887011 | ||
|
|
bab66cfa35 | ||
|
|
344556c639 | ||
|
|
c25f7b0e52 | ||
|
|
381fc54c89 | ||
|
|
9a08edae09 | ||
|
|
8cba8033ba | ||
|
|
8858edde05 | ||
|
|
17022b9b40 | ||
|
|
b8e7fee9e7 | ||
|
|
92361c37df | ||
|
|
d817e4144c | ||
|
|
9a21e19486 | ||
|
|
c14b338a92 | ||
|
|
68251b647d | ||
|
|
e8fe53650b | ||
|
|
39bf8fb01e | ||
|
|
bcd984c1c5 | ||
|
|
3e8f03c7b7 | ||
|
|
10ad246e29 | ||
|
|
39634f442b | ||
|
|
8081a43d85 | ||
|
|
bc5a7867a9 | ||
|
|
b6a43d6ce7 | ||
|
|
61c0b91e39 | ||
|
|
8fd9b6afc9 | ||
|
|
162fd790ef | ||
|
|
f5d67c3c7f | ||
|
|
5827a8fcdd | ||
|
|
8efdaed7e4 | ||
|
|
c35bf8d953 | ||
|
|
29a989e1f4 | ||
|
|
0a03bf9028 | ||
|
|
b682e8c686 | ||
|
|
b34bf39746 | ||
|
|
71df1ccf6f | ||
|
|
102b34db2f | ||
|
|
fb6e7c7b57 | ||
|
|
271518ee08 | ||
|
|
f04a5c8f3e | ||
|
|
1b3dc52563 | ||
|
|
7475a4fff3 | ||
|
|
2e504232ec | ||
|
|
10e9e94a73 | ||
|
|
b8141262d2 | ||
|
|
3014c79c1f |
18 changed files with 4004 additions and 283 deletions
|
|
@ -2,7 +2,8 @@
|
|||
"Spell": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33
|
||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
||||
"DispelType": 4
|
||||
},
|
||||
"SpellRange": { "MaxRange": 2 },
|
||||
"ItemDisplayInfo": {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"Spell": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||
"Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215,
|
||||
"CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40
|
||||
"CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40,
|
||||
"DispelType": 3
|
||||
},
|
||||
"SpellRange": { "MaxRange": 4 },
|
||||
"ItemDisplayInfo": {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"Spell": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33
|
||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
||||
"DispelType": 4
|
||||
},
|
||||
"SpellRange": { "MaxRange": 2 },
|
||||
"ItemDisplayInfo": {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"Spell": {
|
||||
"ID": 0, "Attributes": 4, "IconID": 133,
|
||||
"Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225,
|
||||
"PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49
|
||||
"PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49,
|
||||
"DispelType": 2
|
||||
},
|
||||
"SpellRange": { "MaxRange": 4 },
|
||||
"ItemDisplayInfo": {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ public:
|
|||
void playTargetSelect();
|
||||
void playTargetDeselect();
|
||||
|
||||
// Chat notifications
|
||||
void playWhisperReceived();
|
||||
|
||||
private:
|
||||
struct UISample {
|
||||
std::string path;
|
||||
|
|
@ -122,6 +125,7 @@ private:
|
|||
std::vector<UISample> errorSounds_;
|
||||
std::vector<UISample> selectTargetSounds_;
|
||||
std::vector<UISample> deselectTargetSounds_;
|
||||
std::vector<UISample> whisperSounds_;
|
||||
|
||||
// State tracking
|
||||
float volumeScale_ = 1.0f;
|
||||
|
|
|
|||
|
|
@ -214,6 +214,9 @@ public:
|
|||
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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ public:
|
|||
uint8_t talentGroups = 0;
|
||||
uint8_t activeTalentGroup = 0;
|
||||
std::array<uint32_t, 19> itemEntries{}; // 0=head…18=ranged
|
||||
std::array<uint16_t, 19> enchantIds{}; // permanent enchant per slot (0 = none)
|
||||
};
|
||||
const InspectResult* getInspectResult() const {
|
||||
return inspectResult_.guid ? &inspectResult_ : nullptr;
|
||||
|
|
@ -352,6 +353,19 @@ public:
|
|||
uint32_t getTotalTimePlayed() const { return totalTimePlayed_; }
|
||||
uint32_t getLevelTimePlayed() const { return levelTimePlayed_; }
|
||||
|
||||
// Who results (structured, from last SMSG_WHO response)
|
||||
struct WhoEntry {
|
||||
std::string name;
|
||||
std::string guildName;
|
||||
uint32_t level = 0;
|
||||
uint32_t classId = 0;
|
||||
uint32_t raceId = 0;
|
||||
uint32_t zoneId = 0;
|
||||
};
|
||||
const std::vector<WhoEntry>& getWhoResults() const { return whoResults_; }
|
||||
uint32_t getWhoOnlineCount() const { return whoOnlineCount_; }
|
||||
std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); }
|
||||
|
||||
// Social commands
|
||||
void addFriend(const std::string& playerName, const std::string& note = "");
|
||||
void removeFriend(const std::string& playerName);
|
||||
|
|
@ -379,6 +393,28 @@ public:
|
|||
void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
|
||||
|
||||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
||||
struct BgPlayerScore {
|
||||
uint64_t guid = 0;
|
||||
std::string name;
|
||||
uint8_t team = 0; // 0=Horde, 1=Alliance
|
||||
uint32_t killingBlows = 0;
|
||||
uint32_t deaths = 0;
|
||||
uint32_t honorableKills = 0;
|
||||
uint32_t bonusHonor = 0;
|
||||
std::vector<std::pair<std::string, uint32_t>> bgStats; // BG-specific fields
|
||||
};
|
||||
struct BgScoreboardData {
|
||||
std::vector<BgPlayerScore> players;
|
||||
bool hasWinner = false;
|
||||
uint8_t winner = 0; // 0=Horde, 1=Alliance
|
||||
bool isArena = false;
|
||||
};
|
||||
void requestPvpLog();
|
||||
const BgScoreboardData* getBgScoreboard() const {
|
||||
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
|
||||
}
|
||||
|
||||
// Network latency (milliseconds, updated each PONG response)
|
||||
uint32_t getLatencyMs() const { return lastLatency; }
|
||||
|
||||
|
|
@ -401,6 +437,7 @@ public:
|
|||
|
||||
// Follow/Assist
|
||||
void followTarget();
|
||||
void cancelFollow(); // Stop following current target
|
||||
void assistTarget();
|
||||
|
||||
// PvP
|
||||
|
|
@ -457,11 +494,16 @@ public:
|
|||
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
|
||||
|
||||
// Ready check
|
||||
struct ReadyCheckResult {
|
||||
std::string name;
|
||||
bool ready = false;
|
||||
};
|
||||
void initiateReadyCheck();
|
||||
void respondToReadyCheck(bool ready);
|
||||
bool hasPendingReadyCheck() const { return pendingReadyCheck_; }
|
||||
void dismissReadyCheck() { pendingReadyCheck_ = false; }
|
||||
const std::string& getReadyCheckInitiator() const { return readyCheckInitiator_; }
|
||||
const std::vector<ReadyCheckResult>& getReadyCheckResults() const { return readyCheckResults_; }
|
||||
|
||||
// Duel
|
||||
void forfeitDuel();
|
||||
|
|
@ -516,6 +558,19 @@ public:
|
|||
const std::vector<CombatTextEntry>& getCombatText() const { return combatText; }
|
||||
void updateCombatText(float deltaTime);
|
||||
|
||||
// Combat log (persistent rolling history, max MAX_COMBAT_LOG entries)
|
||||
const std::deque<CombatLogEntry>& getCombatLog() const { return combatLog_; }
|
||||
void clearCombatLog() { combatLog_.clear(); }
|
||||
|
||||
// Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame
|
||||
bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); }
|
||||
std::string popAreaTriggerMsg() {
|
||||
if (areaTriggerMsgs_.empty()) return {};
|
||||
std::string msg = areaTriggerMsgs_.front();
|
||||
areaTriggerMsgs_.pop_front();
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Threat
|
||||
struct ThreatEntry {
|
||||
uint64_t victimGuid = 0;
|
||||
|
|
@ -672,6 +727,11 @@ public:
|
|||
// Auras
|
||||
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
|
||||
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
|
||||
// Per-unit aura cache (populated for party members and any unit we receive updates for)
|
||||
const std::vector<AuraSlot>* getUnitAuras(uint64_t guid) const {
|
||||
auto it = unitAurasCache_.find(guid);
|
||||
return (it != unitAurasCache_.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
// Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE)
|
||||
bool isQuestCompleted(uint32_t questId) const { return completedQuests_.count(questId) > 0; }
|
||||
|
|
@ -743,6 +803,17 @@ public:
|
|||
float getGameTime() const { return gameTime_; }
|
||||
float getTimeSpeed() const { return timeSpeed_; }
|
||||
|
||||
// Global Cooldown (GCD) — set when the server sends a spellId=0 cooldown entry
|
||||
float getGCDRemaining() const {
|
||||
if (gcdTotal_ <= 0.0f) return 0.0f;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - gcdStartedAt_).count() / 1000.0f;
|
||||
float rem = gcdTotal_ - elapsed;
|
||||
return rem > 0.0f ? rem : 0.0f;
|
||||
}
|
||||
float getGCDTotal() const { return gcdTotal_; }
|
||||
bool isGCDActive() const { return getGCDRemaining() > 0.0f; }
|
||||
|
||||
// Weather state (updated by SMSG_WEATHER)
|
||||
// weatherType: 0=clear, 1=rain, 2=snow, 3=storm/fog
|
||||
uint32_t getWeatherType() const { return weatherType_; }
|
||||
|
|
@ -1035,6 +1106,14 @@ public:
|
|||
const std::string& getDuelChallengerName() const { return duelChallengerName_; }
|
||||
void acceptDuel();
|
||||
// forfeitDuel() already declared at line ~399
|
||||
// Returns remaining duel countdown seconds, or 0 if no active countdown
|
||||
float getDuelCountdownRemaining() const {
|
||||
if (duelCountdownMs_ == 0) return 0.0f;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - duelCountdownStartedAt_).count();
|
||||
float rem = (static_cast<float>(duelCountdownMs_) - static_cast<float>(elapsed)) / 1000.0f;
|
||||
return rem > 0.0f ? rem : 0.0f;
|
||||
}
|
||||
|
||||
// ---- Instance lockouts ----
|
||||
struct InstanceLockout {
|
||||
|
|
@ -1099,6 +1178,8 @@ public:
|
|||
uint32_t getLfgBootTotal() const { return lfgBootTotal_; }
|
||||
uint32_t getLfgBootTimeLeft() const { return lfgBootTimeLeft_; }
|
||||
uint32_t getLfgBootNeeded() const { return lfgBootNeeded_; }
|
||||
const std::string& getLfgBootTargetName() const { return lfgBootTargetName_; }
|
||||
const std::string& getLfgBootReason() const { return lfgBootReason_; }
|
||||
|
||||
// ---- Arena Team Stats ----
|
||||
struct ArenaTeamStats {
|
||||
|
|
@ -1129,6 +1210,15 @@ public:
|
|||
uint32_t itemId = 0;
|
||||
std::string itemName;
|
||||
uint8_t itemQuality = 0;
|
||||
uint32_t rollCountdownMs = 60000; // Duration of roll window in ms
|
||||
std::chrono::steady_clock::time_point rollStartedAt{};
|
||||
|
||||
struct PlayerRollResult {
|
||||
std::string playerName;
|
||||
uint8_t rollNum = 0;
|
||||
uint8_t rollType = 0; // 0=need,1=greed,2=disenchant,96=pass
|
||||
};
|
||||
std::vector<PlayerRollResult> playerRolls; // live roll results from group members
|
||||
};
|
||||
bool hasPendingLootRoll() const { return pendingLootRollActive_; }
|
||||
const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; }
|
||||
|
|
@ -1215,6 +1305,7 @@ public:
|
|||
};
|
||||
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||
void abandonQuest(uint32_t questId);
|
||||
void shareQuestWithParty(uint32_t questId); // CMSG_PUSHQUESTTOPARTY
|
||||
bool requestQuestQuery(uint32_t questId, bool force = false);
|
||||
bool isQuestTracked(uint32_t questId) const { return trackedQuestIds_.count(questId) > 0; }
|
||||
void setQuestTracked(uint32_t questId, bool tracked) {
|
||||
|
|
@ -1267,7 +1358,29 @@ public:
|
|||
};
|
||||
const std::vector<FactionStandingInit>& getInitialFactions() const { return initialFactions_; }
|
||||
const std::unordered_map<uint32_t, int32_t>& getFactionStandings() const { return factionStandings_; }
|
||||
// Shaman totems (4 slots: 0=Earth, 1=Fire, 2=Water, 3=Air)
|
||||
struct TotemSlot {
|
||||
uint32_t spellId = 0;
|
||||
uint32_t durationMs = 0;
|
||||
std::chrono::steady_clock::time_point placedAt{};
|
||||
bool active() const { return spellId != 0 && remainingMs() > 0; }
|
||||
float remainingMs() const {
|
||||
if (spellId == 0 || durationMs == 0) return 0.0f;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - placedAt).count();
|
||||
float rem = static_cast<float>(durationMs) - static_cast<float>(elapsed);
|
||||
return rem > 0.0f ? rem : 0.0f;
|
||||
}
|
||||
};
|
||||
static constexpr int NUM_TOTEM_SLOTS = 4;
|
||||
const TotemSlot& getTotemSlot(int slot) const {
|
||||
static TotemSlot empty;
|
||||
return (slot >= 0 && slot < NUM_TOTEM_SLOTS) ? activeTotemSlots_[slot] : empty;
|
||||
}
|
||||
|
||||
const std::string& getFactionNamePublic(uint32_t factionId) const;
|
||||
uint32_t getWatchedFactionId() const { return watchedFactionId_; }
|
||||
void setWatchedFactionId(uint32_t id) { watchedFactionId_ = id; }
|
||||
uint32_t getLastContactListMask() const { return lastContactListMask_; }
|
||||
uint32_t getLastContactListCount() const { return lastContactListCount_; }
|
||||
bool isServerMovementAllowed() const { return serverMovementAllowed_; }
|
||||
|
|
@ -1297,6 +1410,11 @@ public:
|
|||
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
|
||||
const std::unordered_set<uint32_t>& getEarnedAchievements() const { return earnedAchievements_; }
|
||||
const std::unordered_map<uint32_t, uint64_t>& getCriteriaProgress() const { return criteriaProgress_; }
|
||||
/// Returns the WoW PackedTime earn date for an achievement, or 0 if unknown.
|
||||
uint32_t getAchievementDate(uint32_t id) const {
|
||||
auto it = achievementDates_.find(id);
|
||||
return (it != achievementDates_.end()) ? it->second : 0u;
|
||||
}
|
||||
/// Returns the name of an achievement by ID, or empty string if unknown.
|
||||
const std::string& getAchievementName(uint32_t id) const {
|
||||
auto it = achievementNameCache_.find(id);
|
||||
|
|
@ -1330,6 +1448,10 @@ public:
|
|||
using RepChangeCallback = std::function<void(const std::string& factionName, int32_t delta, int32_t standing)>;
|
||||
void setRepChangeCallback(RepChangeCallback cb) { repChangeCallback_ = std::move(cb); }
|
||||
|
||||
// Quest turn-in completion callback
|
||||
using QuestCompleteCallback = std::function<void(uint32_t questId, const std::string& questTitle)>;
|
||||
void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); }
|
||||
|
||||
// Mount state
|
||||
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount
|
||||
void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); }
|
||||
|
|
@ -1539,6 +1661,8 @@ public:
|
|||
const std::string& getSpellName(uint32_t spellId) const;
|
||||
const std::string& getSpellRank(uint32_t spellId) const;
|
||||
const std::string& getSkillLineName(uint32_t spellId) const;
|
||||
/// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other)
|
||||
uint8_t getSpellDispelType(uint32_t spellId) const;
|
||||
|
||||
struct TrainerTab {
|
||||
std::string name;
|
||||
|
|
@ -1819,6 +1943,7 @@ private:
|
|||
void handleArenaTeamEvent(network::Packet& packet);
|
||||
void handleArenaTeamStats(network::Packet& packet);
|
||||
void handleArenaError(network::Packet& packet);
|
||||
void handlePvpLogData(network::Packet& packet);
|
||||
|
||||
// ---- Bank handlers ----
|
||||
void handleShowBank(network::Packet& packet);
|
||||
|
|
@ -2065,6 +2190,9 @@ private:
|
|||
float autoAttackFacingSyncTimer_ = 0.0f; // Periodic facing sync while meleeing
|
||||
std::unordered_set<uint64_t> hostileAttackers_;
|
||||
std::vector<CombatTextEntry> combatText;
|
||||
static constexpr size_t MAX_COMBAT_LOG = 500;
|
||||
std::deque<CombatLogEntry> combatLog_;
|
||||
std::deque<std::string> areaTriggerMsgs_;
|
||||
// unitGuid → sorted threat list (descending by threat value)
|
||||
std::unordered_map<uint64_t, std::vector<ThreatEntry>> threatLists_;
|
||||
|
||||
|
|
@ -2147,6 +2275,7 @@ private:
|
|||
std::array<ActionBarSlot, ACTION_BAR_SLOTS> actionBar{};
|
||||
std::vector<AuraSlot> playerAuras;
|
||||
std::vector<AuraSlot> targetAuras;
|
||||
std::unordered_map<uint64_t, std::vector<AuraSlot>> unitAurasCache_; // per-unit aura cache
|
||||
uint64_t petGuid_ = 0;
|
||||
uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots)
|
||||
uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss
|
||||
|
|
@ -2177,6 +2306,9 @@ private:
|
|||
// Arena team stats (indexed by team slot, updated by SMSG_ARENA_TEAM_STATS)
|
||||
std::vector<ArenaTeamStats> arenaTeamStats_;
|
||||
|
||||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
||||
BgScoreboardData bgScoreboard_;
|
||||
|
||||
// Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||||
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {}; // 0 = empty slot
|
||||
|
||||
|
|
@ -2190,12 +2322,15 @@ private:
|
|||
uint32_t lfgBootTotal_ = 0; // total votes cast
|
||||
uint32_t lfgBootTimeLeft_ = 0; // seconds remaining
|
||||
uint32_t lfgBootNeeded_ = 0; // votes needed to kick
|
||||
std::string lfgBootTargetName_; // name of player being voted on
|
||||
std::string lfgBootReason_; // reason given for kick
|
||||
|
||||
// Ready check state
|
||||
bool pendingReadyCheck_ = false;
|
||||
uint32_t readyCheckReadyCount_ = 0;
|
||||
uint32_t readyCheckNotReadyCount_ = 0;
|
||||
std::string readyCheckInitiator_;
|
||||
std::vector<ReadyCheckResult> readyCheckResults_; // per-player status live during check
|
||||
|
||||
// Faction standings (factionId → absolute standing value)
|
||||
std::unordered_map<uint32_t, int32_t> factionStandings_;
|
||||
|
|
@ -2229,6 +2364,10 @@ private:
|
|||
uint32_t totalTimePlayed_ = 0;
|
||||
uint32_t levelTimePlayed_ = 0;
|
||||
|
||||
// Who results (last SMSG_WHO response)
|
||||
std::vector<WhoEntry> whoResults_;
|
||||
uint32_t whoOnlineCount_ = 0;
|
||||
|
||||
// Trade state
|
||||
TradeStatus tradeStatus_ = TradeStatus::None;
|
||||
uint64_t tradePeerGuid_= 0;
|
||||
|
|
@ -2238,11 +2377,16 @@ private:
|
|||
uint64_t myTradeGold_ = 0;
|
||||
uint64_t peerTradeGold_ = 0;
|
||||
|
||||
// Shaman totem state
|
||||
TotemSlot activeTotemSlots_[NUM_TOTEM_SLOTS];
|
||||
|
||||
// Duel state
|
||||
bool pendingDuelRequest_ = false;
|
||||
uint64_t duelChallengerGuid_= 0;
|
||||
uint64_t duelFlagGuid_ = 0;
|
||||
std::string duelChallengerName_;
|
||||
uint32_t duelCountdownMs_ = 0; // 0 = no active countdown
|
||||
std::chrono::steady_clock::time_point duelCountdownStartedAt_{};
|
||||
|
||||
// ---- Guild state ----
|
||||
std::string guildName_;
|
||||
|
|
@ -2434,7 +2578,7 @@ private:
|
|||
// Trainer
|
||||
bool trainerWindowOpen_ = false;
|
||||
TrainerListData currentTrainerList_;
|
||||
struct SpellNameEntry { std::string name; std::string rank; uint32_t schoolMask = 0; };
|
||||
struct SpellNameEntry { std::string name; std::string rank; uint32_t schoolMask = 0; uint8_t dispelType = 0; };
|
||||
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
||||
bool spellNameCacheLoaded_ = false;
|
||||
|
||||
|
|
@ -2444,6 +2588,8 @@ private:
|
|||
void loadAchievementNameCache();
|
||||
// Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA)
|
||||
std::unordered_set<uint32_t> earnedAchievements_;
|
||||
// Earn dates: achievementId → WoW PackedTime (from SMSG_ACHIEVEMENT_EARNED / SMSG_ALL_ACHIEVEMENT_DATA)
|
||||
std::unordered_map<uint32_t, uint32_t> achievementDates_;
|
||||
// Criteria progress: criteriaId → current value (from SMSG_CRITERIA_UPDATE)
|
||||
std::unordered_map<uint32_t, uint64_t> criteriaProgress_;
|
||||
void handleAllAchievementData(network::Packet& packet);
|
||||
|
|
@ -2518,6 +2664,10 @@ private:
|
|||
float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour)
|
||||
void handleLoginSetTimeSpeed(network::Packet& packet);
|
||||
|
||||
// ---- Global Cooldown (GCD) ----
|
||||
float gcdTotal_ = 0.0f;
|
||||
std::chrono::steady_clock::time_point gcdStartedAt_{};
|
||||
|
||||
// ---- Weather state (SMSG_WEATHER) ----
|
||||
uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm
|
||||
float weatherIntensity_ = 0.0f; // 0.0 to 1.0
|
||||
|
|
@ -2630,6 +2780,10 @@ private:
|
|||
|
||||
// ---- Reputation change callback ----
|
||||
RepChangeCallback repChangeCallback_;
|
||||
uint32_t watchedFactionId_ = 0; // auto-set to most recently changed faction
|
||||
|
||||
// ---- Quest completion callback ----
|
||||
QuestCompleteCallback questCompleteCallback_;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ struct CombatTextEntry {
|
|||
enum Type : uint8_t {
|
||||
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
||||
CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
|
||||
ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST
|
||||
ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER
|
||||
};
|
||||
Type type;
|
||||
int32_t amount = 0;
|
||||
|
|
@ -63,6 +64,19 @@ struct CombatTextEntry {
|
|||
bool isExpired() const { return age >= LIFETIME; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent combat log entry (stored in a rolling deque, survives beyond floating-text lifetime)
|
||||
*/
|
||||
struct CombatLogEntry {
|
||||
CombatTextEntry::Type type = CombatTextEntry::MELEE_DAMAGE;
|
||||
int32_t amount = 0;
|
||||
uint32_t spellId = 0;
|
||||
bool isPlayerSource = false;
|
||||
time_t timestamp = 0; // Wall-clock time (std::time(nullptr))
|
||||
std::string sourceName; // Resolved display name of attacker/caster
|
||||
std::string targetName; // Resolved display name of victim/target
|
||||
};
|
||||
|
||||
/**
|
||||
* Spell cooldown entry received from server
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -46,21 +46,32 @@ private:
|
|||
char chatInputBuffer[512] = "";
|
||||
char whisperTargetBuffer[256] = "";
|
||||
bool chatInputActive = false;
|
||||
int selectedChatType = 0; // 0=SAY, 1=YELL, 2=PARTY, 3=GUILD, 4=WHISPER
|
||||
int selectedChatType = 0; // 0=SAY, 1=YELL, 2=PARTY, 3=GUILD, 4=WHISPER, ..., 10=CHANNEL
|
||||
int lastChatType = 0; // Track chat type changes
|
||||
int selectedChannelIdx = 0; // Index into joinedChannels_ when selectedChatType==10
|
||||
bool chatInputMoveCursorToEnd = false;
|
||||
|
||||
// Chat sent-message history (Up/Down arrow recall)
|
||||
std::vector<std::string> chatSentHistory_;
|
||||
int chatHistoryIdx_ = -1; // -1 = not browsing history
|
||||
|
||||
// Tab-completion state for slash commands
|
||||
std::string chatTabPrefix_; // prefix captured on first Tab press
|
||||
std::vector<std::string> chatTabMatches_; // matching command list
|
||||
int chatTabMatchIdx_ = -1; // active match index (-1 = inactive)
|
||||
|
||||
// Mention notification: plays a sound when the player's name appears in chat
|
||||
size_t chatMentionSeenCount_ = 0; // how many messages have been scanned for mentions
|
||||
|
||||
// Chat tabs
|
||||
int activeChatTab_ = 0;
|
||||
struct ChatTab {
|
||||
std::string name;
|
||||
uint32_t typeMask; // bitmask of ChatType values to show
|
||||
uint64_t typeMask; // bitmask of ChatType values to show (64-bit: types go up to 84)
|
||||
};
|
||||
std::vector<ChatTab> chatTabs_;
|
||||
std::vector<int> chatTabUnread_; // unread message count per tab (0 = none)
|
||||
size_t chatTabSeenCount_ = 0; // how many history messages have been processed
|
||||
void initChatTabs();
|
||||
bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const;
|
||||
|
||||
|
|
@ -75,6 +86,7 @@ private:
|
|||
uint32_t lastPlayerHp_ = 0; // Previous frame HP for damage flash detection
|
||||
float damageFlashAlpha_ = 0.0f; // Screen edge flash intensity (fades to 0)
|
||||
bool damageFlashEnabled_ = true;
|
||||
bool lowHealthVignetteEnabled_ = true; // Persistent pulsing red vignette below 20% HP
|
||||
float levelUpFlashAlpha_ = 0.0f; // Golden level-up burst effect (fades to 0)
|
||||
uint32_t levelUpDisplayLevel_ = 0; // Level shown in level-up text
|
||||
|
||||
|
|
@ -100,6 +112,29 @@ private:
|
|||
std::vector<RepToastEntry> repToasts_;
|
||||
bool repChangeCallbackSet_ = false;
|
||||
static constexpr float kRepToastLifetime = 3.5f;
|
||||
|
||||
// Quest completion toast: slide-in when a quest is turned in
|
||||
struct QuestCompleteToastEntry { uint32_t questId = 0; std::string title; float age = 0.0f; };
|
||||
std::vector<QuestCompleteToastEntry> questCompleteToasts_;
|
||||
bool questCompleteCallbackSet_ = false;
|
||||
static constexpr float kQuestCompleteToastLifetime = 4.0f;
|
||||
|
||||
// Zone entry toast: brief banner when entering a new zone
|
||||
struct ZoneToastEntry { std::string zoneName; float age = 0.0f; };
|
||||
std::vector<ZoneToastEntry> zoneToasts_;
|
||||
|
||||
struct AreaTriggerToast { std::string text; float age = 0.0f; };
|
||||
std::vector<AreaTriggerToast> areaTriggerToasts_;
|
||||
void renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler);
|
||||
std::string lastKnownZone_;
|
||||
static constexpr float kZoneToastLifetime = 3.0f;
|
||||
|
||||
// Death screen: elapsed time since the death dialog first appeared
|
||||
float deathElapsed_ = 0.0f;
|
||||
bool deathTimerRunning_ = false;
|
||||
// WoW forces release after ~6 minutes; show countdown until then
|
||||
static constexpr float kForcedReleaseSec = 360.0f;
|
||||
void renderZoneToasts(float deltaTime);
|
||||
bool showPlayerInfo = false;
|
||||
bool showSocialFrame_ = false; // O key toggles social/friends list
|
||||
bool showGuildRoster_ = false;
|
||||
|
|
@ -255,6 +290,7 @@ private:
|
|||
* Render pet frame (below player frame when player has an active pet)
|
||||
*/
|
||||
void renderPetFrame(game::GameHandler& gameHandler);
|
||||
void renderTotemFrame(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Process targeting input (Tab, Escape, click)
|
||||
|
|
@ -275,6 +311,7 @@ private:
|
|||
void renderActionBar(game::GameHandler& gameHandler);
|
||||
void renderBagBar(game::GameHandler& gameHandler);
|
||||
void renderXpBar(game::GameHandler& gameHandler);
|
||||
void renderRepBar(game::GameHandler& gameHandler);
|
||||
void renderCastBar(game::GameHandler& gameHandler);
|
||||
void renderMirrorTimers(game::GameHandler& gameHandler);
|
||||
void renderCombatText(game::GameHandler& gameHandler);
|
||||
|
|
@ -283,8 +320,10 @@ private:
|
|||
void renderBossFrames(game::GameHandler& gameHandler);
|
||||
void renderUIErrors(game::GameHandler& gameHandler, float deltaTime);
|
||||
void renderRepToasts(float deltaTime);
|
||||
void renderQuestCompleteToasts(float deltaTime);
|
||||
void renderGroupInvitePopup(game::GameHandler& gameHandler);
|
||||
void renderDuelRequestPopup(game::GameHandler& gameHandler);
|
||||
void renderDuelCountdown(game::GameHandler& gameHandler);
|
||||
void renderLootRollPopup(game::GameHandler& gameHandler);
|
||||
void renderTradeRequestPopup(game::GameHandler& gameHandler);
|
||||
void renderTradeWindow(game::GameHandler& gameHandler);
|
||||
|
|
@ -364,6 +403,14 @@ private:
|
|||
int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none)
|
||||
int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none)
|
||||
|
||||
// Who Results window
|
||||
bool showWhoWindow_ = false;
|
||||
void renderWhoWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// Combat Log window
|
||||
bool showCombatLog_ = false;
|
||||
void renderCombatLog(game::GameHandler& gameHandler);
|
||||
|
||||
// Instance Lockouts window
|
||||
bool showInstanceLockouts_ = false;
|
||||
|
||||
|
|
@ -387,6 +434,10 @@ private:
|
|||
// Threat window
|
||||
bool showThreatWindow_ = false;
|
||||
void renderThreatWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// BG scoreboard window
|
||||
bool showBgScoreboard_ = false;
|
||||
void renderBgScoreboard(game::GameHandler& gameHandler);
|
||||
uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps)
|
||||
uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK)
|
||||
|
||||
|
|
@ -477,6 +528,9 @@ private:
|
|||
bool showDPSMeter_ = false;
|
||||
float dpsCombatAge_ = 0.0f; // seconds in current combat (for accurate early-combat DPS)
|
||||
bool dpsWasInCombat_ = false;
|
||||
float dpsEncounterDamage_ = 0.0f; // total player damage this combat
|
||||
float dpsEncounterHeal_ = 0.0f; // total player healing this combat
|
||||
size_t dpsLogSeenCount_ = 0; // log entries already scanned
|
||||
|
||||
public:
|
||||
void triggerDing(uint32_t newLevel);
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ private:
|
|||
std::unordered_map<uint32_t, VkDescriptorSet> iconCache_;
|
||||
public:
|
||||
VkDescriptorSet getItemIcon(uint32_t displayInfoId);
|
||||
void renderItemTooltip(const game::ItemQueryResponseData& info);
|
||||
void renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory = nullptr);
|
||||
private:
|
||||
|
||||
// Character model preview
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@ public:
|
|||
uint32_t getDragSpellId() const { return dragSpellId_; }
|
||||
void consumeDragSpell() { draggingSpell_ = false; dragSpellId_ = 0; dragSpellIconTex_ = VK_NULL_HANDLE; }
|
||||
|
||||
/// Returns the max range in yards for a spell (0 if self-cast, unknown, or melee).
|
||||
/// Triggers DBC load if needed. Used by the action bar for out-of-range tinting.
|
||||
uint32_t getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager);
|
||||
|
||||
/// Returns the power cost and type for a spell (cost=0 if unknown/free).
|
||||
/// powerType: 0=mana, 1=rage, 2=focus, 3=energy, 6=runic power.
|
||||
/// Triggers DBC load if needed. Used by the action bar for insufficient-power tinting.
|
||||
void getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager,
|
||||
uint32_t& outCost, uint32_t& outPowerType);
|
||||
|
||||
/// Returns a WoW spell link string if the user shift-clicked a spell, then clears it.
|
||||
std::string getAndClearPendingChatLink() {
|
||||
std::string out = std::move(pendingChatSpellLink_);
|
||||
|
|
|
|||
|
|
@ -122,6 +122,14 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
|
|||
deselectTargetSounds_.resize(1);
|
||||
loadSound("Sound\\Interface\\iDeselectTarget.wav", deselectTargetSounds_[0], assets);
|
||||
|
||||
// Whisper notification (falls back to iSelectTarget if the dedicated file is absent)
|
||||
whisperSounds_.resize(1);
|
||||
if (!loadSound("Sound\\Interface\\Whisper_TellMale.wav", whisperSounds_[0], assets)) {
|
||||
if (!loadSound("Sound\\Interface\\Whisper_TellFemale.wav", whisperSounds_[0], assets)) {
|
||||
whisperSounds_ = selectTargetSounds_;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("UISoundManager: Window sounds - Bag: ", (bagOpenLoaded && bagCloseLoaded) ? "YES" : "NO",
|
||||
", QuestLog: ", (questLogOpenLoaded && questLogCloseLoaded) ? "YES" : "NO",
|
||||
", CharSheet: ", (charSheetOpenLoaded && charSheetCloseLoaded) ? "YES" : "NO");
|
||||
|
|
@ -225,5 +233,8 @@ void UiSoundManager::playError() { playSound(errorSounds_); }
|
|||
void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); }
|
||||
void UiSoundManager::playTargetDeselect() { playSound(deselectTargetSounds_); }
|
||||
|
||||
// Chat notifications
|
||||
void UiSoundManager::playWhisperReceived() { playSound(whisperSounds_); }
|
||||
|
||||
} // namespace audio
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -2028,7 +2028,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||
/*uint32_t randProp =*/ packet.readUInt32();
|
||||
}
|
||||
/*uint32_t countdown =*/ packet.readUInt32();
|
||||
uint32_t countdown = packet.readUInt32();
|
||||
/*uint8_t voteMask =*/ packet.readUInt8();
|
||||
// Trigger the roll popup for local player
|
||||
pendingLootRollActive_ = true;
|
||||
|
|
@ -2038,6 +2038,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
auto* info = getItemInfo(itemId);
|
||||
pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId);
|
||||
pendingLootRoll_.itemQuality = info ? static_cast<uint8_t>(info->quality) : 0;
|
||||
pendingLootRoll_.rollCountdownMs = (countdown > 0 && countdown <= 120000) ? countdown : 60000;
|
||||
pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now();
|
||||
LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName,
|
||||
") slot=", slot);
|
||||
break;
|
||||
|
|
@ -3059,6 +3061,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
uint32_t spellId = packet.readUInt32();
|
||||
LOG_DEBUG("SMSG_TOTEM_CREATED: slot=", (int)slot,
|
||||
" spellId=", spellId, " duration=", duration, "ms");
|
||||
if (slot < NUM_TOTEM_SLOTS) {
|
||||
activeTotemSlots_[slot].spellId = spellId;
|
||||
activeTotemSlots_[slot].durationMs = duration;
|
||||
activeTotemSlots_[slot].placedAt = std::chrono::steady_clock::now();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_AREA_SPIRIT_HEALER_TIME: {
|
||||
|
|
@ -3142,6 +3149,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
readyCheckReadyCount_ = 0;
|
||||
readyCheckNotReadyCount_ = 0;
|
||||
readyCheckInitiator_.clear();
|
||||
readyCheckResults_.clear();
|
||||
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||||
uint64_t initiatorGuid = packet.readUInt64();
|
||||
auto entity = entityManager.getEntity(initiatorGuid);
|
||||
|
|
@ -3175,7 +3183,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
auto ent = entityManager.getEntity(respGuid);
|
||||
if (ent) rname = std::static_pointer_cast<game::Unit>(ent)->getName();
|
||||
}
|
||||
// Track per-player result for live popup display
|
||||
if (!rname.empty()) {
|
||||
bool found = false;
|
||||
for (auto& r : readyCheckResults_) {
|
||||
if (r.name == rname) { r.ready = (isReady != 0); found = true; break; }
|
||||
}
|
||||
if (!found) readyCheckResults_.push_back({ rname, isReady != 0 });
|
||||
|
||||
char rbuf[128];
|
||||
std::snprintf(rbuf, sizeof(rbuf), "%s is %s.", rname.c_str(), isReady ? "Ready" : "Not Ready");
|
||||
addSystemChatMessage(rbuf);
|
||||
|
|
@ -3191,6 +3206,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
pendingReadyCheck_ = false;
|
||||
readyCheckReadyCount_ = 0;
|
||||
readyCheckNotReadyCount_ = 0;
|
||||
readyCheckResults_.clear();
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_RAID_INSTANCE_INFO:
|
||||
|
|
@ -3212,9 +3228,16 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_DUEL_INBOUNDS:
|
||||
// Re-entered the duel area; no special action needed.
|
||||
break;
|
||||
case Opcode::SMSG_DUEL_COUNTDOWN:
|
||||
// Countdown timer — no action needed; server also sends UNIT_FIELD_FLAGS update.
|
||||
case Opcode::SMSG_DUEL_COUNTDOWN: {
|
||||
// uint32 countdown in milliseconds (typically 3000 ms)
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t ms = packet.readUInt32();
|
||||
duelCountdownMs_ = (ms > 0 && ms <= 30000) ? ms : 3000;
|
||||
duelCountdownStartedAt_ = std::chrono::steady_clock::now();
|
||||
LOG_INFO("SMSG_DUEL_COUNTDOWN: ", duelCountdownMs_, " ms");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_PARTYKILLLOG: {
|
||||
// uint64 killerGuid + uint64 victimGuid
|
||||
if (packet.getSize() - packet.getReadPos() < 16) break;
|
||||
|
|
@ -3544,6 +3567,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
delta > 0 ? "increased" : "decreased",
|
||||
std::abs(delta));
|
||||
addSystemChatMessage(buf);
|
||||
watchedFactionId_ = factionId;
|
||||
if (repChangeCallback_) repChangeCallback_(name, delta, standing);
|
||||
}
|
||||
LOG_DEBUG("SMSG_SET_FACTION_STANDING: faction=", factionId, " standing=", standing);
|
||||
|
|
@ -3853,7 +3877,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
/*uint32_t len =*/ packet.readUInt32();
|
||||
std::string msg = packet.readString();
|
||||
if (!msg.empty()) addSystemChatMessage(msg);
|
||||
if (!msg.empty()) {
|
||||
addSystemChatMessage(msg);
|
||||
areaTriggerMsgs_.push_back(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -4381,6 +4408,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
for (auto it = questLog_.begin(); it != questLog_.end(); ++it) {
|
||||
if (it->questId == questId) {
|
||||
// Fire toast callback before erasing
|
||||
if (questCompleteCallback_) {
|
||||
questCompleteCallback_(questId, it->title);
|
||||
}
|
||||
questLog_.erase(it);
|
||||
LOG_INFO(" Removed quest ", questId, " from quest log");
|
||||
break;
|
||||
|
|
@ -4957,7 +4988,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleArenaError(packet);
|
||||
break;
|
||||
case Opcode::MSG_PVP_LOG_DATA:
|
||||
LOG_INFO("Received MSG_PVP_LOG_DATA");
|
||||
handlePvpLogData(packet);
|
||||
break;
|
||||
case Opcode::MSG_INSPECT_ARENA_TEAMS:
|
||||
LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS");
|
||||
|
|
@ -5176,6 +5207,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
std::vector<AuraSlot>* auraList = nullptr;
|
||||
if (auraTargetGuid == playerGuid) auraList = &playerAuras;
|
||||
else if (auraTargetGuid == targetGuid) auraList = &targetAuras;
|
||||
else if (auraTargetGuid != 0) auraList = &unitAurasCache_[auraTargetGuid];
|
||||
|
||||
if (auraList && isInit) auraList->clear();
|
||||
|
||||
|
|
@ -5559,9 +5591,28 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: {
|
||||
// Format (all expansions): PackedGuid target + PackedGuid caster + uint32 spellId + ...
|
||||
if (packet.getSize() - packet.getReadPos() < 3) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
/*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||||
if (packet.getSize() - packet.getReadPos() < 2) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
uint64_t procCasterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
uint32_t procSpellId = packet.readUInt32();
|
||||
// Show a "PROC!" floating text when the player triggers the proc
|
||||
if (procCasterGuid == playerGuid && procSpellId > 0)
|
||||
addCombatText(CombatTextEntry::PROC_TRIGGER, 0, procSpellId, true);
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_SPELLINSTAKILLLOG:
|
||||
case Opcode::SMSG_SPELLLOGEXECUTE:
|
||||
case Opcode::SMSG_SPELL_CHANCE_PROC_LOG:
|
||||
case Opcode::SMSG_SPELL_CHANCE_RESIST_PUSHBACK:
|
||||
case Opcode::SMSG_SPELL_UPDATE_CHAIN_TARGETS:
|
||||
packet.setReadPos(packet.getSize());
|
||||
|
|
@ -6658,6 +6709,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
|||
actionBar = {};
|
||||
playerAuras.clear();
|
||||
targetAuras.clear();
|
||||
unitAurasCache_.clear();
|
||||
unitCastStates_.clear();
|
||||
petGuid_ = 0;
|
||||
playerXp_ = 0;
|
||||
|
|
@ -10209,6 +10261,15 @@ void GameHandler::followTarget() {
|
|||
LOG_INFO("Following target: ", targetName, " (GUID: 0x", std::hex, targetGuid, std::dec, ")");
|
||||
}
|
||||
|
||||
void GameHandler::cancelFollow() {
|
||||
if (followTargetGuid_ == 0) {
|
||||
addSystemChatMessage("You are not following anyone.");
|
||||
return;
|
||||
}
|
||||
followTargetGuid_ = 0;
|
||||
addSystemChatMessage("You stop following.");
|
||||
}
|
||||
|
||||
void GameHandler::assistTarget() {
|
||||
if (state != WorldState::IN_WORLD) {
|
||||
LOG_WARNING("Cannot assist: not in world");
|
||||
|
|
@ -10465,6 +10526,7 @@ void GameHandler::handleDuelComplete(network::Packet& packet) {
|
|||
uint8_t started = packet.readUInt8();
|
||||
// started=1: duel began, started=0: duel was cancelled before starting
|
||||
pendingDuelRequest_ = false;
|
||||
duelCountdownMs_ = 0; // clear countdown once duel is resolved
|
||||
if (!started) {
|
||||
addSystemChatMessage("The duel was cancelled.");
|
||||
}
|
||||
|
|
@ -11300,6 +11362,7 @@ void GameHandler::handleInspectResults(network::Packet& packet) {
|
|||
}
|
||||
|
||||
// Parse enchantment slot mask + enchant IDs
|
||||
std::array<uint16_t, 19> enchantIds{};
|
||||
bytesLeft = packet.getSize() - packet.getReadPos();
|
||||
if (bytesLeft >= 4) {
|
||||
uint32_t slotMask = packet.readUInt32();
|
||||
|
|
@ -11307,7 +11370,7 @@ void GameHandler::handleInspectResults(network::Packet& packet) {
|
|||
if (slotMask & (1u << slot)) {
|
||||
bytesLeft = packet.getSize() - packet.getReadPos();
|
||||
if (bytesLeft < 2) break;
|
||||
packet.readUInt16(); // enchantId
|
||||
enchantIds[slot] = packet.readUInt16();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11319,6 +11382,7 @@ void GameHandler::handleInspectResults(network::Packet& packet) {
|
|||
inspectResult_.unspentTalents = unspentTalents;
|
||||
inspectResult_.talentGroups = talentGroupCount;
|
||||
inspectResult_.activeTalentGroup = activeTalentGroup;
|
||||
inspectResult_.enchantIds = enchantIds;
|
||||
|
||||
// Merge any gear we already have from a prior inspect request
|
||||
auto gearIt = inspectedPlayerItemEntries_.find(guid);
|
||||
|
|
@ -12101,6 +12165,21 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint
|
|||
entry.age = 0.0f;
|
||||
entry.isPlayerSource = isPlayerSource;
|
||||
combatText.push_back(entry);
|
||||
|
||||
// Persistent combat log
|
||||
CombatLogEntry log;
|
||||
log.type = type;
|
||||
log.amount = amount;
|
||||
log.spellId = spellId;
|
||||
log.isPlayerSource = isPlayerSource;
|
||||
log.timestamp = std::time(nullptr);
|
||||
std::string pname(lookupName(playerGuid));
|
||||
std::string tname((targetGuid != 0) ? lookupName(targetGuid) : std::string());
|
||||
log.sourceName = isPlayerSource ? pname : tname;
|
||||
log.targetName = isPlayerSource ? tname : pname;
|
||||
if (combatLog_.size() >= MAX_COMBAT_LOG)
|
||||
combatLog_.pop_front();
|
||||
combatLog_.push_back(std::move(log));
|
||||
}
|
||||
|
||||
void GameHandler::updateCombatText(float deltaTime) {
|
||||
|
|
@ -13061,11 +13140,19 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) {
|
|||
lfgBootTimeLeft_ = timeLeft;
|
||||
lfgBootNeeded_ = votesNeeded;
|
||||
|
||||
// Optional: reason string and target name (null-terminated) follow the fixed fields
|
||||
if (packet.getSize() - packet.getReadPos() > 0)
|
||||
lfgBootReason_ = packet.readString();
|
||||
if (packet.getSize() - packet.getReadPos() > 0)
|
||||
lfgBootTargetName_ = packet.readString();
|
||||
|
||||
if (inProgress) {
|
||||
lfgState_ = LfgState::Boot;
|
||||
} else {
|
||||
// Boot vote ended — return to InDungeon state regardless of outcome
|
||||
lfgBootVotes_ = lfgBootTotal_ = lfgBootTimeLeft_ = lfgBootNeeded_ = 0;
|
||||
lfgBootTargetName_.clear();
|
||||
lfgBootReason_.clear();
|
||||
lfgState_ = LfgState::InDungeon;
|
||||
if (myAnswer) {
|
||||
addSystemChatMessage("Dungeon Finder: Vote kick passed — member removed.");
|
||||
|
|
@ -13075,7 +13162,8 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) {
|
|||
}
|
||||
|
||||
LOG_INFO("SMSG_LFG_BOOT_PROPOSAL_UPDATE: inProgress=", inProgress,
|
||||
" bootVotes=", bootVotes, "/", totalVotes);
|
||||
" bootVotes=", bootVotes, "/", totalVotes,
|
||||
" target=", lfgBootTargetName_, " reason=", lfgBootReason_);
|
||||
}
|
||||
|
||||
void GameHandler::handleLfgTeleportDenied(network::Packet& packet) {
|
||||
|
|
@ -13380,6 +13468,80 @@ void GameHandler::handleArenaError(network::Packet& packet) {
|
|||
LOG_INFO("Arena error: ", error, " - ", msg);
|
||||
}
|
||||
|
||||
void GameHandler::requestPvpLog() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
// MSG_PVP_LOG_DATA is bidirectional: client sends an empty packet to request
|
||||
network::Packet pkt(wireOpcode(Opcode::MSG_PVP_LOG_DATA));
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Requested PvP log data");
|
||||
}
|
||||
|
||||
void GameHandler::handlePvpLogData(network::Packet& packet) {
|
||||
auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||
if (remaining() < 1) return;
|
||||
|
||||
bgScoreboard_ = BgScoreboardData{};
|
||||
bgScoreboard_.isArena = (packet.readUInt8() != 0);
|
||||
|
||||
if (bgScoreboard_.isArena) {
|
||||
// Skip arena-specific header (two teams × (rating change uint32 + name string + 5×uint32))
|
||||
// Rather than hardcoding arena parse we skip gracefully up to playerCount
|
||||
// Each arena team block: uint32 + string + uint32*5 — variable length due to string.
|
||||
// Skip by scanning for the uint32 playerCount heuristically; simply consume rest.
|
||||
packet.setReadPos(packet.getSize());
|
||||
return;
|
||||
}
|
||||
|
||||
if (remaining() < 4) return;
|
||||
uint32_t playerCount = packet.readUInt32();
|
||||
bgScoreboard_.players.reserve(playerCount);
|
||||
|
||||
for (uint32_t i = 0; i < playerCount && remaining() >= 13; ++i) {
|
||||
BgPlayerScore ps;
|
||||
ps.guid = packet.readUInt64();
|
||||
ps.team = packet.readUInt8();
|
||||
ps.killingBlows = packet.readUInt32();
|
||||
ps.honorableKills = packet.readUInt32();
|
||||
ps.deaths = packet.readUInt32();
|
||||
ps.bonusHonor = packet.readUInt32();
|
||||
|
||||
// Resolve player name from entity manager
|
||||
{
|
||||
auto ent = entityManager.getEntity(ps.guid);
|
||||
if (ent && (ent->getType() == game::ObjectType::PLAYER ||
|
||||
ent->getType() == game::ObjectType::UNIT)) {
|
||||
auto u = std::static_pointer_cast<game::Unit>(ent);
|
||||
if (!u->getName().empty()) ps.name = u->getName();
|
||||
}
|
||||
}
|
||||
|
||||
// BG-specific stat blocks: uint32 count + N × (string fieldName + uint32 value)
|
||||
if (remaining() < 4) { bgScoreboard_.players.push_back(std::move(ps)); break; }
|
||||
uint32_t statCount = packet.readUInt32();
|
||||
for (uint32_t s = 0; s < statCount && remaining() >= 5; ++s) {
|
||||
std::string fieldName;
|
||||
while (remaining() > 0) {
|
||||
char c = static_cast<char>(packet.readUInt8());
|
||||
if (c == '\0') break;
|
||||
fieldName += c;
|
||||
}
|
||||
uint32_t val = (remaining() >= 4) ? packet.readUInt32() : 0;
|
||||
ps.bgStats.emplace_back(std::move(fieldName), val);
|
||||
}
|
||||
|
||||
bgScoreboard_.players.push_back(std::move(ps));
|
||||
}
|
||||
|
||||
if (remaining() >= 1) {
|
||||
bgScoreboard_.hasWinner = (packet.readUInt8() != 0);
|
||||
if (bgScoreboard_.hasWinner && remaining() >= 1)
|
||||
bgScoreboard_.winner = packet.readUInt8();
|
||||
}
|
||||
|
||||
LOG_INFO("PvP log: ", bgScoreboard_.players.size(), " players, hasWinner=",
|
||||
bgScoreboard_.hasWinner, " winner=", (int)bgScoreboard_.winner);
|
||||
}
|
||||
|
||||
void GameHandler::handleOtherPlayerMovement(network::Packet& packet) {
|
||||
// Server relays MSG_MOVE_* for other players: packed GUID (WotLK) or full uint64 (TBC/Classic)
|
||||
const bool otherMoveTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||
|
|
@ -14099,6 +14261,10 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
|||
: CastSpellPacket::build(spellId, target, ++castCount);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
|
||||
|
||||
// Optimistically start GCD immediately on cast — server will confirm or override
|
||||
gcdTotal_ = 1.5f;
|
||||
gcdStartedAt_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void GameHandler::cancelCast() {
|
||||
|
|
@ -14457,6 +14623,14 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) {
|
|||
uint32_t cooldownMs = packet.readUInt32();
|
||||
|
||||
float seconds = cooldownMs / 1000.0f;
|
||||
|
||||
// spellId=0 is the Global Cooldown marker (server sends it for GCD triggers)
|
||||
if (spellId == 0 && cooldownMs > 0 && cooldownMs <= 2000) {
|
||||
gcdTotal_ = seconds;
|
||||
gcdStartedAt_ = std::chrono::steady_clock::now();
|
||||
continue;
|
||||
}
|
||||
|
||||
spellCooldowns[spellId] = seconds;
|
||||
for (auto& slot : actionBar) {
|
||||
bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|
||||
|
|
@ -14497,6 +14671,10 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
|||
} else if (data.guid == targetGuid) {
|
||||
auraList = &targetAuras;
|
||||
}
|
||||
// Also maintain a per-unit cache for any unit (party members, etc.)
|
||||
if (data.guid != 0 && data.guid != playerGuid && data.guid != targetGuid) {
|
||||
auraList = &unitAurasCache_[data.guid];
|
||||
}
|
||||
|
||||
if (auraList) {
|
||||
if (isAll) {
|
||||
|
|
@ -14915,19 +15093,33 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) {
|
|||
if (updateFlags & 0x0200) { // AURAS
|
||||
if (remaining() >= 8) {
|
||||
uint64_t auraMask = packet.readUInt64();
|
||||
// Collect aura updates for this member and store in unitAurasCache_
|
||||
// so party frame debuff dots can use them.
|
||||
std::vector<AuraSlot> newAuras;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
if (auraMask & (uint64_t(1) << i)) {
|
||||
AuraSlot a;
|
||||
a.level = static_cast<uint8_t>(i); // use slot index
|
||||
if (isWotLK) {
|
||||
// WotLK: uint32 spellId + uint8 auraFlags
|
||||
if (remaining() < 5) break;
|
||||
packet.readUInt32();
|
||||
packet.readUInt8();
|
||||
a.spellId = packet.readUInt32();
|
||||
a.flags = packet.readUInt8();
|
||||
} else {
|
||||
// Classic/TBC: uint16 spellId only
|
||||
// Classic/TBC: uint16 spellId only; negative auras not indicated here
|
||||
if (remaining() < 2) break;
|
||||
packet.readUInt16();
|
||||
a.spellId = packet.readUInt16();
|
||||
// Infer negative/positive from dispel type: non-zero dispel → debuff
|
||||
uint8_t dt = getSpellDispelType(a.spellId);
|
||||
if (dt > 0) a.flags = 0x80; // mark as debuff
|
||||
}
|
||||
if (a.spellId != 0) newAuras.push_back(a);
|
||||
}
|
||||
}
|
||||
// Populate unitAurasCache_ for this member (merge: keep existing per-GUID data
|
||||
// only if we already have a richer source; otherwise replace with stats data)
|
||||
if (memberGuid != 0 && memberGuid != playerGuid && memberGuid != targetGuid) {
|
||||
unitAurasCache_[memberGuid] = std::move(newAuras);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16016,6 +16208,28 @@ void GameHandler::abandonQuest(uint32_t questId) {
|
|||
gossipPois_.end());
|
||||
}
|
||||
|
||||
void GameHandler::shareQuestWithParty(uint32_t questId) {
|
||||
if (state != WorldState::IN_WORLD || !socket) {
|
||||
addSystemChatMessage("Cannot share quest: not in world.");
|
||||
return;
|
||||
}
|
||||
if (!isInGroup()) {
|
||||
addSystemChatMessage("You must be in a group to share a quest.");
|
||||
return;
|
||||
}
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_PUSHQUESTTOPARTY));
|
||||
pkt.writeUInt32(questId);
|
||||
socket->send(pkt);
|
||||
// Local feedback: find quest title
|
||||
for (const auto& q : questLog_) {
|
||||
if (q.questId == questId && !q.title.empty()) {
|
||||
addSystemChatMessage("Sharing quest: " + q.title);
|
||||
return;
|
||||
}
|
||||
}
|
||||
addSystemChatMessage("Quest shared.");
|
||||
}
|
||||
|
||||
void GameHandler::handleQuestRequestItems(network::Packet& packet) {
|
||||
QuestRequestItemsData data;
|
||||
if (!QuestRequestItemsParser::parse(packet, data)) {
|
||||
|
|
@ -16932,6 +17146,14 @@ void GameHandler::loadSpellNameCache() {
|
|||
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { schoolEnumField = f; hasSchoolEnum = true; }
|
||||
}
|
||||
|
||||
// DispelType field (0=none,1=magic,2=curse,3=disease,4=poison,5=stealth,…)
|
||||
uint32_t dispelField = 0xFFFFFFFF;
|
||||
bool hasDispelField = false;
|
||||
if (spellL) {
|
||||
uint32_t f = spellL->field("DispelType");
|
||||
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; }
|
||||
}
|
||||
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t id = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
|
||||
|
|
@ -16939,7 +17161,7 @@ void GameHandler::loadSpellNameCache() {
|
|||
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
|
||||
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
|
||||
if (!name.empty()) {
|
||||
SpellNameEntry entry{std::move(name), std::move(rank), 0};
|
||||
SpellNameEntry entry{std::move(name), std::move(rank), 0, 0};
|
||||
if (hasSchoolMask) {
|
||||
entry.schoolMask = dbc->getUInt32(i, schoolMaskField);
|
||||
} else if (hasSchoolEnum) {
|
||||
|
|
@ -16948,6 +17170,9 @@ void GameHandler::loadSpellNameCache() {
|
|||
uint32_t e = dbc->getUInt32(i, schoolEnumField);
|
||||
entry.schoolMask = (e < 7) ? enumToBitmask[e] : 0;
|
||||
}
|
||||
if (hasDispelField) {
|
||||
entry.dispelType = static_cast<uint8_t>(dbc->getUInt32(i, dispelField));
|
||||
}
|
||||
spellNameCache_[id] = std::move(entry);
|
||||
}
|
||||
}
|
||||
|
|
@ -17141,6 +17366,12 @@ const std::string& GameHandler::getSpellRank(uint32_t spellId) const {
|
|||
return (it != spellNameCache_.end()) ? it->second.rank : EMPTY_STRING;
|
||||
}
|
||||
|
||||
uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const {
|
||||
const_cast<GameHandler*>(this)->loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(spellId);
|
||||
return (it != spellNameCache_.end()) ? it->second.dispelType : 0;
|
||||
}
|
||||
|
||||
const std::string& GameHandler::getSkillLineName(uint32_t spellId) const {
|
||||
auto slIt = spellToSkillLine_.find(spellId);
|
||||
if (slIt == spellToSkillLine_.end()) return EMPTY_STRING;
|
||||
|
|
@ -18233,13 +18464,15 @@ void GameHandler::handleWho(network::Packet& packet) {
|
|||
|
||||
LOG_INFO("WHO response: ", displayCount, " players displayed, ", onlineCount, " total online");
|
||||
|
||||
// Store structured results for the who-results window
|
||||
whoResults_.clear();
|
||||
whoOnlineCount_ = onlineCount;
|
||||
|
||||
if (displayCount == 0) {
|
||||
addSystemChatMessage("No players found.");
|
||||
return;
|
||||
}
|
||||
|
||||
addSystemChatMessage(std::to_string(onlineCount) + " player(s) online:");
|
||||
|
||||
for (uint32_t i = 0; i < displayCount; ++i) {
|
||||
if (packet.getReadPos() >= packet.getSize()) break;
|
||||
std::string playerName = packet.readString();
|
||||
|
|
@ -18254,17 +18487,16 @@ void GameHandler::handleWho(network::Packet& packet) {
|
|||
if (packet.getSize() - packet.getReadPos() >= 4)
|
||||
zoneId = packet.readUInt32();
|
||||
|
||||
std::string msg = " " + playerName;
|
||||
if (!guildName.empty())
|
||||
msg += " <" + guildName + ">";
|
||||
msg += " - Level " + std::to_string(level);
|
||||
if (zoneId != 0) {
|
||||
std::string zoneName = getAreaName(zoneId);
|
||||
if (!zoneName.empty())
|
||||
msg += " [" + zoneName + "]";
|
||||
}
|
||||
// Store structured entry
|
||||
WhoEntry entry;
|
||||
entry.name = playerName;
|
||||
entry.guildName = guildName;
|
||||
entry.level = level;
|
||||
entry.classId = classId;
|
||||
entry.raceId = raceId;
|
||||
entry.zoneId = zoneId;
|
||||
whoResults_.push_back(std::move(entry));
|
||||
|
||||
addSystemChatMessage(msg);
|
||||
LOG_INFO(" ", playerName, " (", guildName, ") Lv", level, " Class:", classId,
|
||||
" Race:", raceId, " Zone:", zoneId);
|
||||
}
|
||||
|
|
@ -19921,12 +20153,15 @@ void GameHandler::handleLootRoll(network::Packet& packet) {
|
|||
pendingLootRoll_.objectGuid = objectGuid;
|
||||
pendingLootRoll_.slot = slot;
|
||||
pendingLootRoll_.itemId = itemId;
|
||||
pendingLootRoll_.playerRolls.clear();
|
||||
// Ensure item info is in cache; query if not
|
||||
queryItemInfo(itemId, 0);
|
||||
// Look up item name from cache
|
||||
auto* info = getItemInfo(itemId);
|
||||
pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId);
|
||||
pendingLootRoll_.itemQuality = info ? static_cast<uint8_t>(info->quality) : 0;
|
||||
pendingLootRoll_.rollCountdownMs = 60000;
|
||||
pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now();
|
||||
LOG_INFO("SMSG_LOOT_ROLL: need/greed prompt for item=", itemId,
|
||||
" (", pendingLootRoll_.itemName, ") slot=", slot);
|
||||
return;
|
||||
|
|
@ -19943,6 +20178,28 @@ void GameHandler::handleLootRoll(network::Packet& packet) {
|
|||
}
|
||||
if (rollerName.empty()) rollerName = "Someone";
|
||||
|
||||
// Track in the live roll list while our popup is open for the same item
|
||||
if (pendingLootRollActive_ &&
|
||||
pendingLootRoll_.objectGuid == objectGuid &&
|
||||
pendingLootRoll_.slot == slot) {
|
||||
bool found = false;
|
||||
for (auto& r : pendingLootRoll_.playerRolls) {
|
||||
if (r.playerName == rollerName) {
|
||||
r.rollNum = rollNum;
|
||||
r.rollType = rollType;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LootRollEntry::PlayerRollResult prr;
|
||||
prr.playerName = rollerName;
|
||||
prr.rollNum = rollNum;
|
||||
prr.rollType = rollType;
|
||||
pendingLootRoll_.playerRolls.push_back(std::move(prr));
|
||||
}
|
||||
}
|
||||
|
||||
auto* info = getItemInfo(itemId);
|
||||
std::string iName = info ? info->name : std::to_string(itemId);
|
||||
|
||||
|
|
@ -19997,10 +20254,8 @@ void GameHandler::handleLootRollWon(network::Packet& packet) {
|
|||
winnerName.c_str(), iName.c_str(), rollName, static_cast<int>(rollNum));
|
||||
addSystemChatMessage(buf);
|
||||
|
||||
// Clear pending roll if it was ours
|
||||
if (pendingLootRollActive_ && winnerGuid == playerGuid) {
|
||||
// Dismiss roll popup — roll contest is over regardless of who won
|
||||
pendingLootRollActive_ = false;
|
||||
}
|
||||
LOG_INFO("SMSG_LOOT_ROLL_WON: winner=", winnerName, " item=", itemId,
|
||||
" roll=", rollName, "(", rollNum, ")");
|
||||
}
|
||||
|
|
@ -20057,7 +20312,7 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) {
|
|||
|
||||
uint64_t guid = packet.readUInt64();
|
||||
uint32_t achievementId = packet.readUInt32();
|
||||
/*uint32_t date =*/ packet.readUInt32(); // PackedTime — not displayed
|
||||
uint32_t earnDate = packet.readUInt32(); // WoW PackedTime bitfield
|
||||
|
||||
loadAchievementNameCache();
|
||||
auto nameIt = achievementNameCache_.find(achievementId);
|
||||
|
|
@ -20076,6 +20331,7 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) {
|
|||
addSystemChatMessage(buf);
|
||||
|
||||
earnedAchievements_.insert(achievementId);
|
||||
achievementDates_[achievementId] = earnDate;
|
||||
if (achievementEarnedCallback_) {
|
||||
achievementEarnedCallback_(achievementId, achName);
|
||||
}
|
||||
|
|
@ -20116,14 +20372,16 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) {
|
|||
void GameHandler::handleAllAchievementData(network::Packet& packet) {
|
||||
loadAchievementNameCache();
|
||||
earnedAchievements_.clear();
|
||||
achievementDates_.clear();
|
||||
|
||||
// Parse achievement entries (id + packedDate pairs, sentinel 0xFFFFFFFF)
|
||||
while (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t id = packet.readUInt32();
|
||||
if (id == 0xFFFFFFFF) break;
|
||||
if (packet.getSize() - packet.getReadPos() < 4) break;
|
||||
/*uint32_t date =*/ packet.readUInt32();
|
||||
uint32_t date = packet.readUInt32();
|
||||
earnedAchievements_.insert(id);
|
||||
achievementDates_[id] = date;
|
||||
}
|
||||
|
||||
// Parse criteria block: id + uint64 counter + uint32 date + uint32 flags, sentinel 0xFFFFFFFF
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
|
|
@ -1016,6 +1017,40 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi
|
|||
}
|
||||
}
|
||||
|
||||
// Hover coordinate display — show WoW coordinates under cursor
|
||||
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
||||
auto& io = ImGui::GetIO();
|
||||
ImVec2 mp = io.MousePos;
|
||||
if (mp.x >= imgMin.x && mp.x <= imgMin.x + displayW &&
|
||||
mp.y >= imgMin.y && mp.y <= imgMin.y + displayH) {
|
||||
float mu = (mp.x - imgMin.x) / displayW;
|
||||
float mv = (mp.y - imgMin.y) / displayH;
|
||||
|
||||
const auto& zone = zones[currentIdx];
|
||||
float left = zone.locLeft, right = zone.locRight;
|
||||
float top = zone.locTop, bottom = zone.locBottom;
|
||||
if (zone.areaID == 0) {
|
||||
float l, r, t, b;
|
||||
getContinentProjectionBounds(currentIdx, l, r, t, b);
|
||||
left = l; right = r; top = t; bottom = b;
|
||||
// Undo the kVOffset applied during renderPosToMapUV for continent
|
||||
constexpr float kVOffset = -0.15f;
|
||||
mv -= kVOffset;
|
||||
}
|
||||
|
||||
float hWowX = left - mu * (left - right);
|
||||
float hWowY = top - mv * (top - bottom);
|
||||
|
||||
char coordBuf[32];
|
||||
snprintf(coordBuf, sizeof(coordBuf), "%.0f, %.0f", hWowX, hWowY);
|
||||
ImVec2 coordSz = ImGui::CalcTextSize(coordBuf);
|
||||
float cx = imgMin.x + displayW - coordSz.x - 8.0f;
|
||||
float cy = imgMin.y + displayH - coordSz.y - 8.0f;
|
||||
drawList->AddText(ImVec2(cx + 1.0f, cy + 1.0f), IM_COL32(0, 0, 0, 180), coordBuf);
|
||||
drawList->AddText(ImVec2(cx, cy), IM_COL32(220, 210, 150, 230), coordBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// Continent view: clickable zone overlays
|
||||
if (viewLevel == ViewLevel::CONTINENT && continentIdx >= 0) {
|
||||
const auto& cont = zones[continentIdx];
|
||||
|
|
@ -1080,6 +1115,23 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi
|
|||
drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||
IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f);
|
||||
}
|
||||
|
||||
// Zone name label — only if the rect is large enough to fit it
|
||||
if (!z.areaName.empty()) {
|
||||
ImVec2 textSz = ImGui::CalcTextSize(z.areaName.c_str());
|
||||
float rectW = sx1 - sx0;
|
||||
float rectH = sy1 - sy0;
|
||||
if (rectW > textSz.x + 4.0f && rectH > textSz.y + 2.0f) {
|
||||
float tx = (sx0 + sx1) * 0.5f - textSz.x * 0.5f;
|
||||
float ty = (sy0 + sy1) * 0.5f - textSz.y * 0.5f;
|
||||
ImU32 labelCol = explored
|
||||
? IM_COL32(255, 230, 150, 210)
|
||||
: IM_COL32(160, 160, 160, 80);
|
||||
drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f),
|
||||
IM_COL32(0, 0, 0, 130), z.areaName.c_str());
|
||||
drawList->AddText(ImVec2(tx, ty), labelCol, z.areaName.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -72,6 +72,21 @@ const game::ItemSlot* findComparableEquipped(const game::Inventory& inventory, u
|
|||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) {
|
||||
bool any = false;
|
||||
if (g > 0) {
|
||||
ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g);
|
||||
any = true;
|
||||
}
|
||||
if (s > 0 || g > 0) {
|
||||
if (any) ImGui::SameLine(0, 3);
|
||||
ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s);
|
||||
any = true;
|
||||
}
|
||||
if (any) ImGui::SameLine(0, 3);
|
||||
ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InventoryScreen::~InventoryScreen() {
|
||||
|
|
@ -2197,7 +2212,8 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
uint32_t g = item.sellPrice / 10000;
|
||||
uint32_t s = (item.sellPrice / 100) % 100;
|
||||
uint32_t c = item.sellPrice % 100;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c);
|
||||
ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4);
|
||||
renderCoinsText(g, s, c);
|
||||
}
|
||||
|
||||
// Shift-hover comparison with currently equipped equivalent.
|
||||
|
|
@ -2321,7 +2337,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
// ---------------------------------------------------------------------------
|
||||
// Tooltip overload for ItemQueryResponseData (used by loot window, etc.)
|
||||
// ---------------------------------------------------------------------------
|
||||
void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) {
|
||||
void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory) {
|
||||
ImGui::BeginTooltip();
|
||||
|
||||
ImVec4 qColor = getQualityColor(static_cast<game::ItemQuality>(info.quality));
|
||||
|
|
@ -2477,7 +2493,50 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info)
|
|||
uint32_t g = info.sellPrice / 10000;
|
||||
uint32_t s = (info.sellPrice / 100) % 100;
|
||||
uint32_t c = info.sellPrice % 100;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c);
|
||||
ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4);
|
||||
renderCoinsText(g, s, c);
|
||||
}
|
||||
|
||||
// Shift-hover: compare with currently equipped item
|
||||
if (inventory && ImGui::GetIO().KeyShift && info.inventoryType > 0) {
|
||||
if (const game::ItemSlot* eq = findComparableEquipped(*inventory, static_cast<uint8_t>(info.inventoryType))) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Equipped:");
|
||||
VkDescriptorSet eqIcon = getItemIcon(eq->item.displayInfoId);
|
||||
if (eqIcon) { ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f)); ImGui::SameLine(); }
|
||||
ImGui::TextColored(getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
|
||||
|
||||
auto showDiff = [](const char* label, float nv, float ev) {
|
||||
if (nv == 0.0f && ev == 0.0f) return;
|
||||
float diff = nv - ev;
|
||||
char buf[96];
|
||||
if (diff > 0.0f) { std::snprintf(buf, sizeof(buf), "%s: %.0f (▲%.0f)", label, nv, diff); ImGui::TextColored(ImVec4(0.0f,1.0f,0.0f,1.0f), "%s", buf); }
|
||||
else if (diff < 0.0f) { std::snprintf(buf, sizeof(buf), "%s: %.0f (▼%.0f)", label, nv, -diff); ImGui::TextColored(ImVec4(1.0f,0.3f,0.3f,1.0f), "%s", buf); }
|
||||
else { std::snprintf(buf, sizeof(buf), "%s: %.0f (=)", label, nv); ImGui::TextColored(ImVec4(0.7f,0.7f,0.7f,1.0f), "%s", buf); }
|
||||
};
|
||||
|
||||
float ilvlDiff = static_cast<float>(info.itemLevel) - static_cast<float>(eq->item.itemLevel);
|
||||
if (info.itemLevel > 0 || eq->item.itemLevel > 0) {
|
||||
char ilvlBuf[64];
|
||||
if (ilvlDiff > 0) std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▲%.0f)", info.itemLevel, ilvlDiff);
|
||||
else if (ilvlDiff < 0) std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▼%.0f)", info.itemLevel, -ilvlDiff);
|
||||
else std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (=)", info.itemLevel);
|
||||
ImVec4 ic = ilvlDiff > 0 ? ImVec4(0,1,0,1) : ilvlDiff < 0 ? ImVec4(1,0.3f,0.3f,1) : ImVec4(0.7f,0.7f,0.7f,1);
|
||||
ImGui::TextColored(ic, "%s", ilvlBuf);
|
||||
}
|
||||
|
||||
showDiff("Armor", static_cast<float>(info.armor), static_cast<float>(eq->item.armor));
|
||||
showDiff("Str", static_cast<float>(info.strength), static_cast<float>(eq->item.strength));
|
||||
showDiff("Agi", static_cast<float>(info.agility), static_cast<float>(eq->item.agility));
|
||||
showDiff("Sta", static_cast<float>(info.stamina), static_cast<float>(eq->item.stamina));
|
||||
showDiff("Int", static_cast<float>(info.intellect), static_cast<float>(eq->item.intellect));
|
||||
showDiff("Spi", static_cast<float>(info.spirit), static_cast<float>(eq->item.spirit));
|
||||
|
||||
// Hint text
|
||||
ImGui::TextDisabled("Hold Shift to compare");
|
||||
}
|
||||
} else if (info.inventoryType > 0) {
|
||||
ImGui::TextDisabled("Hold Shift to compare");
|
||||
}
|
||||
|
||||
ImGui::EndTooltip();
|
||||
|
|
|
|||
|
|
@ -205,6 +205,21 @@ std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) {
|
|||
if (s.size() > 72) s = s.substr(0, 72) + "...";
|
||||
return s;
|
||||
}
|
||||
|
||||
void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) {
|
||||
bool any = false;
|
||||
if (g > 0) {
|
||||
ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g);
|
||||
any = true;
|
||||
}
|
||||
if (s > 0 || g > 0) {
|
||||
if (any) ImGui::SameLine(0, 3);
|
||||
ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s);
|
||||
any = true;
|
||||
}
|
||||
if (any) ImGui::SameLine(0, 3);
|
||||
ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c);
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& invScreen) {
|
||||
|
|
@ -362,6 +377,11 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
if (ImGui::MenuItem(tracked ? "Untrack" : "Track")) {
|
||||
gameHandler.setQuestTracked(q.questId, !tracked);
|
||||
}
|
||||
if (gameHandler.isInGroup() && !q.complete) {
|
||||
if (ImGui::MenuItem("Share Quest")) {
|
||||
gameHandler.shareQuestWithParty(q.questId);
|
||||
}
|
||||
}
|
||||
if (!q.complete) {
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Abandon Quest")) {
|
||||
|
|
@ -488,12 +508,7 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
uint32_t rg = static_cast<uint32_t>(sel.rewardMoney) / 10000;
|
||||
uint32_t rs = static_cast<uint32_t>(sel.rewardMoney % 10000) / 100;
|
||||
uint32_t rc = static_cast<uint32_t>(sel.rewardMoney % 100);
|
||||
if (rg > 0)
|
||||
ImGui::Text("%ug %us %uc", rg, rs, rc);
|
||||
else if (rs > 0)
|
||||
ImGui::Text("%us %uc", rs, rc);
|
||||
else
|
||||
ImGui::Text("%uc", rc);
|
||||
renderCoinsText(rg, rs, rc);
|
||||
}
|
||||
|
||||
// Guaranteed reward items
|
||||
|
|
@ -549,12 +564,19 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
}
|
||||
}
|
||||
|
||||
// Track / Abandon buttons
|
||||
// Track / Share / Abandon buttons
|
||||
ImGui::Separator();
|
||||
bool isTracked = gameHandler.isQuestTracked(sel.questId);
|
||||
if (ImGui::Button(isTracked ? "Untrack" : "Track", ImVec2(100.0f, 0.0f))) {
|
||||
gameHandler.setQuestTracked(sel.questId, !isTracked);
|
||||
}
|
||||
if (gameHandler.isInGroup() && !sel.complete) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Share", ImVec2(80.0f, 0.0f))) {
|
||||
gameHandler.shareQuestWithParty(sel.questId);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Share this quest with your party");
|
||||
}
|
||||
if (!sel.complete) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Abandon Quest", ImVec2(150.0f, 0.0f))) {
|
||||
|
|
|
|||
|
|
@ -203,6 +203,29 @@ std::string SpellbookScreen::lookupSpellName(uint32_t spellId, pipeline::AssetMa
|
|||
return {};
|
||||
}
|
||||
|
||||
uint32_t SpellbookScreen::getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager) {
|
||||
if (!dbcLoadAttempted) {
|
||||
loadSpellDBC(assetManager);
|
||||
}
|
||||
auto it = spellData.find(spellId);
|
||||
if (it != spellData.end()) return it->second.rangeIndex;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SpellbookScreen::getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager,
|
||||
uint32_t& outCost, uint32_t& outPowerType) {
|
||||
outCost = 0;
|
||||
outPowerType = 0;
|
||||
if (!dbcLoadAttempted) {
|
||||
loadSpellDBC(assetManager);
|
||||
}
|
||||
auto it = spellData.find(spellId);
|
||||
if (it != spellData.end()) {
|
||||
outCost = it->second.manaCost;
|
||||
outPowerType = it->second.powerType;
|
||||
}
|
||||
}
|
||||
|
||||
void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
||||
if (iconDbLoaded) return;
|
||||
iconDbLoaded = true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue