mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
71 commits
34bab8edd6
...
2f0809b570
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f0809b570 | ||
|
|
144c87a72f | ||
|
|
1446d4fddd | ||
|
|
84a6ee4801 | ||
|
|
00db93b7f2 | ||
|
|
fb01361837 | ||
|
|
d1c5e09127 | ||
|
|
f50cb04887 | ||
|
|
031448ec6d | ||
|
|
dfc78572f5 | ||
|
|
d2ae4d8215 | ||
|
|
e902375763 | ||
|
|
d5196abaec | ||
|
|
35683920ff | ||
|
|
1646bef1c2 | ||
|
|
ae6c2aa056 | ||
|
|
603e52e5b0 | ||
|
|
0a17683545 | ||
|
|
9cd7e7978d | ||
|
|
1f4880985b | ||
|
|
d696da9227 | ||
|
|
21c55ad6b4 | ||
|
|
643d48ee89 | ||
|
|
3082df2ac0 | ||
|
|
c4b2089d31 | ||
|
|
a67feb6d93 | ||
|
|
7c77c4a81e | ||
|
|
570465f51a | ||
|
|
f043077746 | ||
|
|
4393798409 | ||
|
|
ce36171000 | ||
|
|
f462db6bfa | ||
|
|
568c566e1a | ||
|
|
170ff1597c | ||
|
|
06facc0060 | ||
|
|
7c5d688c00 | ||
|
|
eaf827668a | ||
|
|
77ce54833a | ||
|
|
72a16a2427 | ||
|
|
2ee0934653 | ||
|
|
ef0e171da5 | ||
|
|
6f5bdb2e91 | ||
|
|
12aa5e01b6 | ||
|
|
e64b566d72 | ||
|
|
73439a4457 | ||
|
|
7e55d21cdd | ||
|
|
3a7ff71262 | ||
|
|
1b55ebb387 | ||
|
|
99de1fa3e5 | ||
|
|
d95abfb607 | ||
|
|
b658743e94 | ||
|
|
00a939a733 | ||
|
|
6928b8ddf6 | ||
|
|
19eb7a1fb7 | ||
|
|
8f2974b17c | ||
|
|
7c8bda0907 | ||
|
|
b4469b1577 | ||
|
|
a7474b96cf | ||
|
|
5fbeb7938c | ||
|
|
bc18fb7c3e | ||
|
|
4a445081d8 | ||
|
|
8ab83987f1 | ||
|
|
a7a559cdcc | ||
|
|
4986308581 | ||
|
|
6275a45ec0 | ||
|
|
984decd664 | ||
|
|
b87b6cee0f | ||
|
|
2b9f216dae | ||
|
|
28550dbc99 | ||
|
|
564a286282 | ||
|
|
48d15fc653 |
43 changed files with 2589 additions and 401 deletions
|
|
@ -30,7 +30,7 @@
|
|||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||
"AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"OBJECT_FIELD_SCALE_X": 4,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_BYTES_0": 36,
|
||||
|
|
@ -16,13 +17,18 @@
|
|||
"UNIT_NPC_FLAGS": 147,
|
||||
"UNIT_DYNAMIC_FLAGS": 143,
|
||||
"UNIT_FIELD_RESISTANCES": 154,
|
||||
"UNIT_FIELD_STAT0": 138,
|
||||
"UNIT_FIELD_STAT1": 139,
|
||||
"UNIT_FIELD_STAT2": 140,
|
||||
"UNIT_FIELD_STAT3": 141,
|
||||
"UNIT_FIELD_STAT4": 142,
|
||||
"UNIT_END": 188,
|
||||
"PLAYER_FLAGS": 190,
|
||||
"PLAYER_BYTES": 191,
|
||||
"PLAYER_BYTES_2": 192,
|
||||
"PLAYER_XP": 716,
|
||||
"PLAYER_NEXT_LEVEL_XP": 717,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 718,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 1175,
|
||||
"PLAYER_FIELD_COINAGE": 1176,
|
||||
"PLAYER_QUEST_LOG_START": 198,
|
||||
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||
"AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"OBJECT_FIELD_SCALE_X": 4,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_BYTES_0": 36,
|
||||
|
|
@ -16,13 +17,18 @@
|
|||
"UNIT_NPC_FLAGS": 168,
|
||||
"UNIT_DYNAMIC_FLAGS": 164,
|
||||
"UNIT_FIELD_RESISTANCES": 185,
|
||||
"UNIT_FIELD_STAT0": 159,
|
||||
"UNIT_FIELD_STAT1": 160,
|
||||
"UNIT_FIELD_STAT2": 161,
|
||||
"UNIT_FIELD_STAT3": 162,
|
||||
"UNIT_FIELD_STAT4": 163,
|
||||
"UNIT_END": 234,
|
||||
"PLAYER_FLAGS": 236,
|
||||
"PLAYER_BYTES": 237,
|
||||
"PLAYER_BYTES_2": 238,
|
||||
"PLAYER_XP": 926,
|
||||
"PLAYER_NEXT_LEVEL_XP": 927,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 928,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 1440,
|
||||
"PLAYER_FIELD_COINAGE": 1441,
|
||||
"PLAYER_QUEST_LOG_START": 244,
|
||||
"PLAYER_FIELD_INV_SLOT_HEAD": 650,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||
"AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"OBJECT_FIELD_SCALE_X": 4,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_BYTES_0": 36,
|
||||
|
|
@ -16,13 +17,18 @@
|
|||
"UNIT_NPC_FLAGS": 147,
|
||||
"UNIT_DYNAMIC_FLAGS": 143,
|
||||
"UNIT_FIELD_RESISTANCES": 154,
|
||||
"UNIT_FIELD_STAT0": 138,
|
||||
"UNIT_FIELD_STAT1": 139,
|
||||
"UNIT_FIELD_STAT2": 140,
|
||||
"UNIT_FIELD_STAT3": 141,
|
||||
"UNIT_FIELD_STAT4": 142,
|
||||
"UNIT_END": 188,
|
||||
"PLAYER_FLAGS": 190,
|
||||
"PLAYER_BYTES": 191,
|
||||
"PLAYER_BYTES_2": 192,
|
||||
"PLAYER_XP": 716,
|
||||
"PLAYER_NEXT_LEVEL_XP": 717,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 718,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 1175,
|
||||
"PLAYER_FIELD_COINAGE": 1176,
|
||||
"PLAYER_QUEST_LOG_START": 198,
|
||||
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"Achievement": { "ID": 0, "Title": 4, "Description": 21 },
|
||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||
"AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"OBJECT_FIELD_SCALE_X": 4,
|
||||
"UNIT_FIELD_TARGET_LO": 6,
|
||||
"UNIT_FIELD_TARGET_HI": 7,
|
||||
"UNIT_FIELD_BYTES_0": 23,
|
||||
|
|
@ -16,13 +17,18 @@
|
|||
"UNIT_NPC_FLAGS": 82,
|
||||
"UNIT_DYNAMIC_FLAGS": 147,
|
||||
"UNIT_FIELD_RESISTANCES": 99,
|
||||
"UNIT_FIELD_STAT0": 84,
|
||||
"UNIT_FIELD_STAT1": 85,
|
||||
"UNIT_FIELD_STAT2": 86,
|
||||
"UNIT_FIELD_STAT3": 87,
|
||||
"UNIT_FIELD_STAT4": 88,
|
||||
"UNIT_END": 148,
|
||||
"PLAYER_FLAGS": 150,
|
||||
"PLAYER_BYTES": 153,
|
||||
"PLAYER_BYTES_2": 154,
|
||||
"PLAYER_XP": 634,
|
||||
"PLAYER_NEXT_LEVEL_XP": 635,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 636,
|
||||
"PLAYER_REST_STATE_EXPERIENCE": 1169,
|
||||
"PLAYER_FIELD_COINAGE": 1170,
|
||||
"PLAYER_QUEST_LOG_START": 158,
|
||||
"PLAYER_FIELD_INV_SLOT_HEAD": 324,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ layout(set = 1, binding = 0) uniform sampler2D markerTexture;
|
|||
layout(push_constant) uniform Push {
|
||||
mat4 model;
|
||||
float alpha;
|
||||
float grayscale; // 0 = full colour, 1 = fully desaturated (trivial quests)
|
||||
} push;
|
||||
|
||||
layout(location = 0) in vec2 TexCoord;
|
||||
|
|
@ -14,5 +15,7 @@ layout(location = 0) out vec4 outColor;
|
|||
void main() {
|
||||
vec4 texColor = texture(markerTexture, TexCoord);
|
||||
if (texColor.a < 0.1) discard;
|
||||
outColor = vec4(texColor.rgb, texColor.a * push.alpha);
|
||||
float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
|
||||
vec3 rgb = mix(texColor.rgb, vec3(lum), push.grayscale);
|
||||
outColor = vec4(rgb, texColor.a * push.alpha);
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -98,7 +98,7 @@ private:
|
|||
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
|
||||
void buildFactionHostilityMap(uint8_t playerRace);
|
||||
pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path);
|
||||
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale = 1.0f);
|
||||
void despawnOnlineCreature(uint64_t guid);
|
||||
bool tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instanceId);
|
||||
void spawnOnlinePlayer(uint64_t guid,
|
||||
|
|
@ -113,7 +113,7 @@ private:
|
|||
void despawnOnlinePlayer(uint64_t guid);
|
||||
void buildCreatureDisplayLookups();
|
||||
std::string getModelPathForDisplayId(uint32_t displayId) const;
|
||||
void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale = 1.0f);
|
||||
void despawnOnlineGameObject(uint64_t guid);
|
||||
void buildGameObjectDisplayLookups();
|
||||
std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const;
|
||||
|
|
@ -214,6 +214,7 @@ private:
|
|||
uint32_t displayId;
|
||||
uint32_t modelId;
|
||||
float x, y, z, orientation;
|
||||
float scale = 1.0f;
|
||||
std::shared_ptr<pipeline::M2Model> model; // parsed on background thread
|
||||
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures; // decoded on bg thread
|
||||
bool valid = false;
|
||||
|
|
@ -300,6 +301,7 @@ private:
|
|||
uint64_t guid;
|
||||
uint32_t displayId;
|
||||
float x, y, z, orientation;
|
||||
float scale = 1.0f;
|
||||
};
|
||||
std::deque<PendingCreatureSpawn> pendingCreatureSpawns_;
|
||||
static constexpr int MAX_SPAWNS_PER_FRAME = 3;
|
||||
|
|
@ -393,6 +395,7 @@ private:
|
|||
uint32_t entry;
|
||||
uint32_t displayId;
|
||||
float x, y, z, orientation;
|
||||
float scale = 1.0f;
|
||||
};
|
||||
std::vector<PendingGameObjectSpawn> pendingGameObjectSpawns_;
|
||||
void processGameObjectSpawnQueue();
|
||||
|
|
@ -403,6 +406,7 @@ private:
|
|||
uint32_t entry;
|
||||
uint32_t displayId;
|
||||
float x, y, z, orientation;
|
||||
float scale = 1.0f;
|
||||
std::shared_ptr<pipeline::WMOModel> wmoModel;
|
||||
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures; // decoded on bg thread
|
||||
bool valid = false;
|
||||
|
|
|
|||
|
|
@ -295,6 +295,13 @@ public:
|
|||
// Server-authoritative armor (UNIT_FIELD_RESISTANCES[0])
|
||||
int32_t getArmorRating() const { return playerArmorRating_; }
|
||||
|
||||
// Server-authoritative primary stats (UNIT_FIELD_STAT0-4: STR, AGI, STA, INT, SPI).
|
||||
// Returns -1 if the server hasn't sent the value yet.
|
||||
int32_t getPlayerStat(int idx) const {
|
||||
if (idx < 0 || idx > 4) return -1;
|
||||
return playerStats_[idx];
|
||||
}
|
||||
|
||||
// Inventory
|
||||
Inventory& getInventory() { return inventory; }
|
||||
const Inventory& getInventory() const { return inventory; }
|
||||
|
|
@ -340,9 +347,24 @@ public:
|
|||
// Random roll
|
||||
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
|
||||
|
||||
// Battleground queue slot (public so UI can read invite details)
|
||||
struct BgQueueSlot {
|
||||
uint32_t queueSlot = 0;
|
||||
uint32_t bgTypeId = 0;
|
||||
uint8_t arenaType = 0;
|
||||
uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress
|
||||
uint32_t inviteTimeout = 80;
|
||||
std::chrono::steady_clock::time_point inviteReceivedTime{};
|
||||
};
|
||||
|
||||
// Battleground
|
||||
bool hasPendingBgInvite() const;
|
||||
void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||
void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
|
||||
|
||||
// Network latency (milliseconds, updated each PONG response)
|
||||
uint32_t getLatencyMs() const { return lastLatency; }
|
||||
|
||||
// Logout commands
|
||||
void requestLogout();
|
||||
|
|
@ -720,8 +742,8 @@ public:
|
|||
void setHearthstonePreloadCallback(HearthstonePreloadCallback cb) { hearthstonePreloadCallback_ = std::move(cb); }
|
||||
|
||||
// Creature spawn callback (online mode - triggered when creature enters view)
|
||||
// Parameters: guid, displayId, x, y, z (canonical), orientation
|
||||
using CreatureSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
// Parameters: guid, displayId, x, y, z (canonical), orientation, scale (OBJECT_FIELD_SCALE_X)
|
||||
using CreatureSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale)>;
|
||||
void setCreatureSpawnCallback(CreatureSpawnCallback cb) { creatureSpawnCallback_ = std::move(cb); }
|
||||
|
||||
// Creature despawn callback (online mode - triggered when creature leaves view)
|
||||
|
|
@ -751,8 +773,8 @@ public:
|
|||
void setPlayerEquipmentCallback(PlayerEquipmentCallback cb) { playerEquipmentCallback_ = std::move(cb); }
|
||||
|
||||
// GameObject spawn callback (online mode - triggered when gameobject enters view)
|
||||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation
|
||||
using GameObjectSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation, scale (OBJECT_FIELD_SCALE_X)
|
||||
using GameObjectSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale)>;
|
||||
void setGameObjectSpawnCallback(GameObjectSpawnCallback cb) { gameObjectSpawnCallback_ = std::move(cb); }
|
||||
|
||||
// GameObject move callback (online mode - triggered when gameobject position updates)
|
||||
|
|
@ -911,13 +933,38 @@ public:
|
|||
enum class TradeStatus : uint8_t {
|
||||
None = 0, PendingIncoming, Open, Accepted, Complete
|
||||
};
|
||||
|
||||
static constexpr int TRADE_SLOT_COUNT = 6; // WoW has 6 normal trade slots + slot 6 for non-trade item
|
||||
|
||||
struct TradeSlot {
|
||||
uint32_t itemId = 0;
|
||||
uint32_t displayId = 0;
|
||||
uint32_t stackCount = 0;
|
||||
uint64_t itemGuid = 0;
|
||||
uint8_t bag = 0xFF; // 0xFF = not set
|
||||
uint8_t bagSlot = 0xFF;
|
||||
bool occupied = false;
|
||||
};
|
||||
|
||||
TradeStatus getTradeStatus() const { return tradeStatus_; }
|
||||
bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; }
|
||||
bool isTradeOpen() const { return tradeStatus_ == TradeStatus::Open || tradeStatus_ == TradeStatus::Accepted; }
|
||||
const std::string& getTradePeerName() const { return tradePeerName_; }
|
||||
|
||||
// My trade slots (what I'm offering)
|
||||
const std::array<TradeSlot, TRADE_SLOT_COUNT>& getMyTradeSlots() const { return myTradeSlots_; }
|
||||
// Peer's trade slots (what they're offering)
|
||||
const std::array<TradeSlot, TRADE_SLOT_COUNT>& getPeerTradeSlots() const { return peerTradeSlots_; }
|
||||
uint64_t getMyTradeGold() const { return myTradeGold_; }
|
||||
uint64_t getPeerTradeGold() const { return peerTradeGold_; }
|
||||
|
||||
void acceptTradeRequest(); // respond to incoming SMSG_TRADE_STATUS(1) with CMSG_BEGIN_TRADE
|
||||
void declineTradeRequest(); // respond with CMSG_CANCEL_TRADE
|
||||
void acceptTrade(); // lock in offer: CMSG_ACCEPT_TRADE
|
||||
void cancelTrade(); // CMSG_CANCEL_TRADE
|
||||
void setTradeItem(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot);
|
||||
void clearTradeItem(uint8_t tradeSlot);
|
||||
void setTradeGold(uint64_t copper);
|
||||
|
||||
// ---- Duel ----
|
||||
bool hasPendingDuelRequest() const { return pendingDuelRequest_; }
|
||||
|
|
@ -1047,12 +1094,27 @@ public:
|
|||
std::string title;
|
||||
std::string objectives;
|
||||
bool complete = false;
|
||||
// Objective kill counts: objectiveIndex -> (current, required)
|
||||
// Objective kill counts: npcOrGoEntry -> (current, required)
|
||||
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> killCounts;
|
||||
// Quest item progress: itemId -> current count
|
||||
std::unordered_map<uint32_t, uint32_t> itemCounts;
|
||||
// Server-authoritative quest item requirements from REQUEST_ITEMS
|
||||
std::unordered_map<uint32_t, uint32_t> requiredItemCounts;
|
||||
// Structured kill objectives parsed from SMSG_QUEST_QUERY_RESPONSE.
|
||||
// Index 0-3 map to the server's objective slot order (packed into update fields).
|
||||
// npcOrGoId != 0 => entity objective (kill NPC or interact with GO).
|
||||
struct KillObjective {
|
||||
int32_t npcOrGoId = 0; // negative = game-object entry
|
||||
uint32_t required = 0;
|
||||
};
|
||||
std::array<KillObjective, 4> killObjectives{}; // zeroed by default
|
||||
// Required item objectives parsed from SMSG_QUEST_QUERY_RESPONSE.
|
||||
// itemId != 0 => collect items of that type.
|
||||
struct ItemObjective {
|
||||
uint32_t itemId = 0;
|
||||
uint32_t required = 0;
|
||||
};
|
||||
std::array<ItemObjective, 6> itemObjectives{}; // zeroed by default
|
||||
};
|
||||
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||
void abandonQuest(uint32_t questId);
|
||||
|
|
@ -1134,8 +1196,9 @@ public:
|
|||
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
|
||||
|
||||
// Achievement earned callback — fires when SMSG_ACHIEVEMENT_EARNED is received
|
||||
using AchievementEarnedCallback = std::function<void(uint32_t achievementId)>;
|
||||
using AchievementEarnedCallback = std::function<void(uint32_t achievementId, const std::string& name)>;
|
||||
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
|
||||
const std::unordered_set<uint32_t>& getEarnedAchievements() const { return earnedAchievements_; }
|
||||
|
||||
// Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received.
|
||||
// The soundId corresponds to a SoundEntries.dbc record. The receiver is
|
||||
|
|
@ -1580,6 +1643,7 @@ private:
|
|||
void handleGossipMessage(network::Packet& packet);
|
||||
void handleQuestgiverQuestList(network::Packet& packet);
|
||||
void handleGossipComplete(network::Packet& packet);
|
||||
void handleQuestPoiQueryResponse(network::Packet& packet);
|
||||
void handleQuestDetails(network::Packet& packet);
|
||||
void handleQuestRequestItems(network::Packet& packet);
|
||||
void handleQuestOfferReward(network::Packet& packet);
|
||||
|
|
@ -1614,6 +1678,8 @@ private:
|
|||
void handleQuestConfirmAccept(network::Packet& packet);
|
||||
void handleSummonRequest(network::Packet& packet);
|
||||
void handleTradeStatus(network::Packet& packet);
|
||||
void handleTradeStatusExtended(network::Packet& packet);
|
||||
void resetTradeState();
|
||||
void handleDuelRequested(network::Packet& packet);
|
||||
void handleDuelComplete(network::Packet& packet);
|
||||
void handleDuelWinner(network::Packet& packet);
|
||||
|
|
@ -1969,12 +2035,6 @@ private:
|
|||
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
||||
|
||||
// ---- Battleground queue state ----
|
||||
struct BgQueueSlot {
|
||||
uint32_t queueSlot = 0;
|
||||
uint32_t bgTypeId = 0;
|
||||
uint8_t arenaType = 0;
|
||||
uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress
|
||||
};
|
||||
std::array<BgQueueSlot, 3> bgQueues_{};
|
||||
|
||||
// Instance difficulty
|
||||
|
|
@ -2044,6 +2104,10 @@ private:
|
|||
TradeStatus tradeStatus_ = TradeStatus::None;
|
||||
uint64_t tradePeerGuid_= 0;
|
||||
std::string tradePeerName_;
|
||||
std::array<TradeSlot, TRADE_SLOT_COUNT> myTradeSlots_{};
|
||||
std::array<TradeSlot, TRADE_SLOT_COUNT> peerTradeSlots_{};
|
||||
uint64_t myTradeGold_ = 0;
|
||||
uint64_t peerTradeGold_ = 0;
|
||||
|
||||
// Duel state
|
||||
bool pendingDuelRequest_ = false;
|
||||
|
|
@ -2099,6 +2163,8 @@ private:
|
|||
std::unordered_map<uint64_t, float> recentLootMoneyAnnounceCooldowns_;
|
||||
uint64_t playerMoneyCopper_ = 0;
|
||||
int32_t playerArmorRating_ = 0;
|
||||
// Server-authoritative primary stats: [0]=STR [1]=AGI [2]=STA [3]=INT [4]=SPI; -1 = not received yet
|
||||
int32_t playerStats_[5] = {-1, -1, -1, -1, -1};
|
||||
// Some servers/custom clients shift update field indices. We can auto-detect coinage by correlating
|
||||
// money-notify deltas with update-field diffs and then overriding UF::PLAYER_FIELD_COINAGE at runtime.
|
||||
uint32_t pendingMoneyDelta_ = 0;
|
||||
|
|
@ -2246,6 +2312,9 @@ private:
|
|||
std::unordered_map<uint32_t, std::string> achievementNameCache_;
|
||||
bool achievementNameCacheLoaded_ = false;
|
||||
void loadAchievementNameCache();
|
||||
// Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA)
|
||||
std::unordered_set<uint32_t> earnedAchievements_;
|
||||
void handleAllAchievementData(network::Packet& packet);
|
||||
|
||||
// Area name cache (lazy-loaded from WorldMapArea.dbc; maps AreaTable ID → display name)
|
||||
std::unordered_map<uint32_t, std::string> areaNameCache_;
|
||||
|
|
@ -2340,6 +2409,10 @@ private:
|
|||
void loadSkillLineAbilityDbc();
|
||||
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void extractExploredZoneFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void applyQuestStateFromFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
// Apply packed kill counts from player update fields to a quest entry that has
|
||||
// already had its killObjectives populated from SMSG_QUEST_QUERY_RESPONSE.
|
||||
void applyPackedKillCountsFromFields(QuestLogEntry& quest);
|
||||
|
||||
NpcDeathCallback npcDeathCallback_;
|
||||
NpcAggroCallback npcAggroCallback_;
|
||||
|
|
|
|||
|
|
@ -207,6 +207,11 @@ public:
|
|||
* WotLK: 5 fields per slot, Classic/Vanilla: 3. */
|
||||
virtual uint8_t questLogStride() const { return 5; }
|
||||
|
||||
/** Number of PLAYER_EXPLORED_ZONES uint32 fields in update-object blocks.
|
||||
* Classic/Vanilla/Turtle: 64 (bit-packs up to zone ID 2047).
|
||||
* TBC/WotLK: 128 (covers Outland/Northrend zone IDs up to 4095). */
|
||||
virtual uint8_t exploredZonesCount() const { return 128; }
|
||||
|
||||
// --- Quest Giver Status ---
|
||||
|
||||
/** Read quest giver status from packet.
|
||||
|
|
@ -407,6 +412,9 @@ public:
|
|||
network::Packet buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) override;
|
||||
// parseQuestDetails inherited from TbcPacketParsers (same format as TBC 2.4.3)
|
||||
uint8_t questLogStride() const override { return 3; }
|
||||
// Classic 1.12 has 64 explored-zone uint32 fields (zone IDs fit in 2048 bits).
|
||||
// TBC/WotLK use 128 (needed for Outland/Northrend zone IDs up to 4095).
|
||||
uint8_t exploredZonesCount() const override { return 64; }
|
||||
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override {
|
||||
return MonsterMoveParser::parseVanilla(packet, data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,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
|
||||
ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST
|
||||
};
|
||||
Type type;
|
||||
int32_t amount = 0;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace game {
|
|||
enum class UF : uint16_t {
|
||||
// Object fields
|
||||
OBJECT_FIELD_ENTRY,
|
||||
OBJECT_FIELD_SCALE_X,
|
||||
|
||||
// Unit fields
|
||||
UNIT_FIELD_TARGET_LO,
|
||||
|
|
@ -33,6 +34,11 @@ enum class UF : uint16_t {
|
|||
UNIT_NPC_FLAGS,
|
||||
UNIT_DYNAMIC_FLAGS,
|
||||
UNIT_FIELD_RESISTANCES, // Physical armor (index 0 of the resistance array)
|
||||
UNIT_FIELD_STAT0, // Strength (effective base, includes items)
|
||||
UNIT_FIELD_STAT1, // Agility
|
||||
UNIT_FIELD_STAT2, // Stamina
|
||||
UNIT_FIELD_STAT3, // Intellect
|
||||
UNIT_FIELD_STAT4, // Spirit
|
||||
UNIT_END,
|
||||
|
||||
// Player fields
|
||||
|
|
|
|||
|
|
@ -1356,6 +1356,33 @@ public:
|
|||
static network::Packet build();
|
||||
};
|
||||
|
||||
/** CMSG_SET_TRADE_ITEM packet builder (tradeSlot, bag, bagSlot) */
|
||||
class SetTradeItemPacket {
|
||||
public:
|
||||
// tradeSlot: 0-5 (normal) or 6 (backpack money-only slot)
|
||||
// bag: 255 = main backpack, 19-22 = bag slots
|
||||
// bagSlot: slot within bag
|
||||
static network::Packet build(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot);
|
||||
};
|
||||
|
||||
/** CMSG_CLEAR_TRADE_ITEM packet builder (remove item from trade slot) */
|
||||
class ClearTradeItemPacket {
|
||||
public:
|
||||
static network::Packet build(uint8_t tradeSlot);
|
||||
};
|
||||
|
||||
/** CMSG_SET_TRADE_GOLD packet builder (gold offered, in copper) */
|
||||
class SetTradeGoldPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t copper);
|
||||
};
|
||||
|
||||
/** CMSG_UNACCEPT_TRADE packet builder (unaccept without cancelling) */
|
||||
class UnacceptTradePacket {
|
||||
public:
|
||||
static network::Packet build();
|
||||
};
|
||||
|
||||
/** CMSG_ATTACKSWING packet builder */
|
||||
class AttackSwingPacket {
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@ public:
|
|||
// Movement callback for sending opcodes to server
|
||||
using MovementCallback = std::function<void(uint32_t opcode)>;
|
||||
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
||||
|
||||
// Callback invoked when the player stands up via local input (space/X/movement key
|
||||
// while server-sitting), so the caller can send CMSG_STAND_STATE_CHANGE(0).
|
||||
using StandUpCallback = std::function<void()>;
|
||||
void setStandUpCallback(StandUpCallback cb) { standUpCallback_ = std::move(cb); }
|
||||
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
||||
void setRunSpeedOverride(float speed) { runSpeedOverride_ = speed; }
|
||||
void setWalkSpeedOverride(float speed) { walkSpeedOverride_ = speed; }
|
||||
|
|
@ -265,6 +270,7 @@ private:
|
|||
|
||||
// Movement callback
|
||||
MovementCallback movementCallback;
|
||||
StandUpCallback standUpCallback_;
|
||||
|
||||
// Movement speeds
|
||||
bool useWoWSpeed = false;
|
||||
|
|
|
|||
|
|
@ -127,7 +127,8 @@ struct M2ModelGPU {
|
|||
|
||||
// Particle emitter data (kept from M2Model)
|
||||
std::vector<pipeline::M2ParticleEmitter> particleEmitters;
|
||||
std::vector<VkTexture*> particleTextures; // Resolved Vulkan textures per emitter
|
||||
std::vector<VkTexture*> particleTextures; // Resolved Vulkan textures per emitter
|
||||
std::vector<VkDescriptorSet> particleTexSets; // Pre-allocated descriptor sets per emitter (stable, avoids per-frame alloc)
|
||||
|
||||
// Texture transform data for UV animation
|
||||
std::vector<pipeline::M2TextureTransform> textureTransforms;
|
||||
|
|
|
|||
|
|
@ -35,8 +35,10 @@ public:
|
|||
* @param position World position (NPC base position)
|
||||
* @param markerType 0=available(!), 1=turnin(?), 2=incomplete(?)
|
||||
* @param boundingHeight NPC bounding height (optional, default 2.0f)
|
||||
* @param grayscale 0 = full colour, 1 = desaturated grey (trivial/low-level quests)
|
||||
*/
|
||||
void setMarker(uint64_t guid, const glm::vec3& position, int markerType, float boundingHeight = 2.0f);
|
||||
void setMarker(uint64_t guid, const glm::vec3& position, int markerType,
|
||||
float boundingHeight = 2.0f, float grayscale = 0.0f);
|
||||
|
||||
/**
|
||||
* Remove a quest marker
|
||||
|
|
@ -61,6 +63,7 @@ private:
|
|||
glm::vec3 position;
|
||||
int type; // 0=available, 1=turnin, 2=incomplete
|
||||
float boundingHeight = 2.0f;
|
||||
float grayscale = 0.0f; // 0 = colour, 1 = desaturated (trivial quests)
|
||||
};
|
||||
|
||||
std::unordered_map<uint64_t, Marker> markers_;
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ private:
|
|||
std::vector<uint32_t> serverExplorationMask;
|
||||
bool hasServerExplorationMask = false;
|
||||
std::unordered_set<int> exploredZones;
|
||||
// Locally accumulated exploration (used as fallback when server mask is unavailable)
|
||||
std::unordered_set<int> locallyExploredZones_;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ private:
|
|||
* Render target frame
|
||||
*/
|
||||
void renderTargetFrame(game::GameHandler& gameHandler);
|
||||
void renderFocusFrame(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Render pet frame (below player frame when player has an active pet)
|
||||
|
|
@ -223,6 +224,7 @@ private:
|
|||
void renderDuelRequestPopup(game::GameHandler& gameHandler);
|
||||
void renderLootRollPopup(game::GameHandler& gameHandler);
|
||||
void renderTradeRequestPopup(game::GameHandler& gameHandler);
|
||||
void renderTradeWindow(game::GameHandler& gameHandler);
|
||||
void renderSummonRequestPopup(game::GameHandler& gameHandler);
|
||||
void renderSharedQuestPopup(game::GameHandler& gameHandler);
|
||||
void renderItemTextWindow(game::GameHandler& gameHandler);
|
||||
|
|
@ -248,6 +250,8 @@ private:
|
|||
void renderGuildRoster(game::GameHandler& gameHandler);
|
||||
void renderGuildInvitePopup(game::GameHandler& gameHandler);
|
||||
void renderReadyCheckPopup(game::GameHandler& gameHandler);
|
||||
void renderBgInvitePopup(game::GameHandler& gameHandler);
|
||||
void renderLfgProposalPopup(game::GameHandler& gameHandler);
|
||||
void renderChatBubbles(game::GameHandler& gameHandler);
|
||||
void renderMailWindow(game::GameHandler& gameHandler);
|
||||
void renderMailComposeWindow(game::GameHandler& gameHandler);
|
||||
|
|
@ -366,6 +370,7 @@ private:
|
|||
static constexpr float ACHIEVEMENT_TOAST_DURATION = 5.0f;
|
||||
float achievementToastTimer_ = 0.0f;
|
||||
uint32_t achievementToastId_ = 0;
|
||||
std::string achievementToastName_;
|
||||
void renderAchievementToast();
|
||||
|
||||
// Zone discovery text ("Entering: <ZoneName>")
|
||||
|
|
@ -377,7 +382,7 @@ private:
|
|||
|
||||
public:
|
||||
void triggerDing(uint32_t newLevel);
|
||||
void triggerAchievementToast(uint32_t achievementId);
|
||||
void triggerAchievementToast(uint32_t achievementId, std::string name = {});
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ private:
|
|||
std::unordered_map<uint32_t, VkDescriptorSet> iconCache_;
|
||||
public:
|
||||
VkDescriptorSet getItemIcon(uint32_t displayInfoId);
|
||||
void renderItemTooltip(const game::ItemQueryResponseData& info);
|
||||
private:
|
||||
|
||||
// Character model preview
|
||||
|
|
@ -147,7 +148,8 @@ private:
|
|||
int bagIndex, float defaultX, float defaultY, uint64_t moneyCopper);
|
||||
void renderEquipmentPanel(game::Inventory& inventory);
|
||||
void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false);
|
||||
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0);
|
||||
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0,
|
||||
const int32_t* serverStats = nullptr);
|
||||
void renderReputationPanel(game::GameHandler& gameHandler);
|
||||
|
||||
void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ private:
|
|||
VkDescriptorSet getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager);
|
||||
const SpellInfo* getSpellInfo(uint32_t spellId) const;
|
||||
|
||||
// Tooltip rendering helper
|
||||
void renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler);
|
||||
// Tooltip rendering helper (showUsageHints=false when called from action bar)
|
||||
void renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler, bool showUsageHints = true);
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -628,6 +628,11 @@ void Application::setState(AppState newState) {
|
|||
gameHandler->sendMovement(static_cast<game::Opcode>(opcode));
|
||||
}
|
||||
});
|
||||
cc->setStandUpCallback([this]() {
|
||||
if (gameHandler) {
|
||||
gameHandler->setStandState(0); // CMSG_STAND_STATE_CHANGE(STAND)
|
||||
}
|
||||
});
|
||||
cc->setUseWoWSpeed(true);
|
||||
}
|
||||
if (gameHandler) {
|
||||
|
|
@ -980,6 +985,18 @@ void Application::update(float deltaTime) {
|
|||
retrySpawn.y = unit->getY();
|
||||
retrySpawn.z = unit->getZ();
|
||||
retrySpawn.orientation = unit->getOrientation();
|
||||
{
|
||||
using game::fieldIndex; using game::UF;
|
||||
uint16_t si = fieldIndex(UF::OBJECT_FIELD_SCALE_X);
|
||||
if (si != 0xFFFF) {
|
||||
uint32_t raw = unit->getField(si);
|
||||
if (raw != 0) {
|
||||
float s2 = 1.0f;
|
||||
std::memcpy(&s2, &raw, sizeof(float));
|
||||
if (s2 > 0.01f && s2 < 100.0f) retrySpawn.scale = s2;
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingCreatureSpawns_.push_back(retrySpawn);
|
||||
pendingCreatureSpawnGuids_.insert(guid);
|
||||
}
|
||||
|
|
@ -2193,12 +2210,12 @@ void Application::setupUICallbacks() {
|
|||
// Faction hostility map is built in buildFactionHostilityMap() when character enters world
|
||||
|
||||
// Creature spawn callback (online mode) - spawn creature models
|
||||
gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale) {
|
||||
// Queue spawns to avoid hanging when many creatures appear at once.
|
||||
// Deduplicate so repeated updates don't flood pending queue.
|
||||
if (creatureInstances_.count(guid)) return;
|
||||
if (pendingCreatureSpawnGuids_.count(guid)) return;
|
||||
pendingCreatureSpawns_.push_back({guid, displayId, x, y, z, orientation});
|
||||
pendingCreatureSpawns_.push_back({guid, displayId, x, y, z, orientation, scale});
|
||||
pendingCreatureSpawnGuids_.insert(guid);
|
||||
});
|
||||
|
||||
|
|
@ -2244,8 +2261,8 @@ void Application::setupUICallbacks() {
|
|||
});
|
||||
|
||||
// GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.)
|
||||
gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
pendingGameObjectSpawns_.push_back({guid, entry, displayId, x, y, z, orientation});
|
||||
gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale) {
|
||||
pendingGameObjectSpawns_.push_back({guid, entry, displayId, x, y, z, orientation, scale});
|
||||
});
|
||||
|
||||
// GameObject despawn callback (online mode) - remove static models
|
||||
|
|
@ -2330,9 +2347,9 @@ void Application::setupUICallbacks() {
|
|||
});
|
||||
|
||||
// Achievement earned callback — show toast banner
|
||||
gameHandler->setAchievementEarnedCallback([this](uint32_t achievementId) {
|
||||
gameHandler->setAchievementEarnedCallback([this](uint32_t achievementId, const std::string& name) {
|
||||
if (uiManager) {
|
||||
uiManager->getGameScreen().triggerAchievementToast(achievementId);
|
||||
uiManager->getGameScreen().triggerAchievementToast(achievementId, name);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2548,13 +2565,19 @@ void Application::setupUICallbacks() {
|
|||
// Don't override Death animation (1). The per-frame sync loop will return to
|
||||
// Stand when movement stops.
|
||||
if (durationMs > 0) {
|
||||
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur);
|
||||
if (!gotState || curAnimId != 1 /*Death*/) {
|
||||
cr->playAnimation(instanceId, 5u, /*loop=*/true);
|
||||
// Player animation is managed by the local renderer state machine —
|
||||
// don't reset it here or every server movement packet restarts the
|
||||
// run cycle from frame 0, causing visible stutter.
|
||||
if (!isPlayer) {
|
||||
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur);
|
||||
// Only start Run if not already running and not in Death animation.
|
||||
if (!gotState || (curAnimId != 1 /*Death*/ && curAnimId != 5u /*Run*/)) {
|
||||
cr->playAnimation(instanceId, 5u, /*loop=*/true);
|
||||
}
|
||||
creatureWasMoving_[guid] = true;
|
||||
}
|
||||
if (!isPlayer) creatureWasMoving_[guid] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -4743,7 +4766,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
// Process ALL pending game object spawns.
|
||||
while (!pendingGameObjectSpawns_.empty()) {
|
||||
auto& s = pendingGameObjectSpawns_.front();
|
||||
spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||
spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation, s.scale);
|
||||
pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin());
|
||||
}
|
||||
|
||||
|
|
@ -5274,7 +5297,7 @@ pipeline::M2Model Application::loadCreatureM2Sync(const std::string& m2Path) {
|
|||
return model;
|
||||
}
|
||||
|
||||
void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale) {
|
||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return;
|
||||
|
||||
// Skip if lookups not yet built (asset manager not ready)
|
||||
|
|
@ -5711,9 +5734,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Convert canonical WoW orientation (0=north) -> render yaw (0=west)
|
||||
float renderYaw = orientation + glm::radians(90.0f);
|
||||
|
||||
// Create instance
|
||||
// Create instance (apply server-provided scale from OBJECT_FIELD_SCALE_X)
|
||||
uint32_t instanceId = charRenderer->createInstance(modelId, renderPos,
|
||||
glm::vec3(0.0f, 0.0f, renderYaw), 1.0f);
|
||||
glm::vec3(0.0f, 0.0f, renderYaw), scale);
|
||||
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create creature instance for guid 0x", std::hex, guid, std::dec);
|
||||
|
|
@ -7024,7 +7047,7 @@ void Application::despawnOnlinePlayer(uint64_t guid) {
|
|||
creatureWasWalking_.erase(guid);
|
||||
}
|
||||
|
||||
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale) {
|
||||
if (!renderer || !assetManager) return;
|
||||
|
||||
if (!gameObjectLookupsBuilt_) {
|
||||
|
|
@ -7181,7 +7204,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
|
||||
if (loadedAsWmo) {
|
||||
uint32_t instanceId = wmoRenderer->createInstance(modelId, renderPos,
|
||||
glm::vec3(0.0f, 0.0f, renderYawWmo), 1.0f);
|
||||
glm::vec3(0.0f, 0.0f, renderYawWmo), scale);
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create gameobject WMO instance for guid 0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
|
|
@ -7289,7 +7312,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
}
|
||||
|
||||
uint32_t instanceId = m2Renderer->createInstance(modelId, renderPos,
|
||||
glm::vec3(0.0f, 0.0f, renderYawM2go), 1.0f);
|
||||
glm::vec3(0.0f, 0.0f, renderYawM2go), scale);
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create gameobject instance for guid 0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
|
|
@ -7407,6 +7430,7 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
|||
s.y = result.y;
|
||||
s.z = result.z;
|
||||
s.orientation = result.orientation;
|
||||
s.scale = result.scale;
|
||||
pendingCreatureSpawns_.push_back(s);
|
||||
pendingCreatureSpawnGuids_.insert(result.guid);
|
||||
}
|
||||
|
|
@ -7721,6 +7745,7 @@ void Application::processCreatureSpawnQueue(bool unlimited) {
|
|||
result.y = s.y;
|
||||
result.z = s.z;
|
||||
result.orientation = s.orientation;
|
||||
result.scale = s.scale;
|
||||
|
||||
auto m2Data = am->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
|
|
@ -7799,7 +7824,7 @@ void Application::processCreatureSpawnQueue(bool unlimited) {
|
|||
// Cached model — spawn is fast (no file I/O, just instance creation + texture setup)
|
||||
{
|
||||
auto spawnStart = std::chrono::steady_clock::now();
|
||||
spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||
spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation, s.scale);
|
||||
auto spawnEnd = std::chrono::steady_clock::now();
|
||||
float spawnMs = std::chrono::duration<float, std::milli>(spawnEnd - spawnStart).count();
|
||||
if (spawnMs > 100.0f) {
|
||||
|
|
@ -8004,7 +8029,7 @@ void Application::processAsyncGameObjectResults() {
|
|||
if (!result.valid || !result.isWmo || !result.wmoModel) {
|
||||
// Fallback: spawn via sync path (likely an M2 or failed WMO)
|
||||
spawnOnlineGameObject(result.guid, result.entry, result.displayId,
|
||||
result.x, result.y, result.z, result.orientation);
|
||||
result.x, result.y, result.z, result.orientation, result.scale);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -8031,7 +8056,7 @@ void Application::processAsyncGameObjectResults() {
|
|||
glm::vec3 renderPos = core::coords::canonicalToRender(
|
||||
glm::vec3(result.x, result.y, result.z));
|
||||
uint32_t instanceId = wmoRenderer->createInstance(
|
||||
modelId, renderPos, glm::vec3(0.0f, 0.0f, result.orientation), 1.0f);
|
||||
modelId, renderPos, glm::vec3(0.0f, 0.0f, result.orientation), result.scale);
|
||||
if (instanceId == 0) continue;
|
||||
|
||||
gameObjectInstances_[result.guid] = {modelId, instanceId, true};
|
||||
|
|
@ -8118,6 +8143,7 @@ void Application::processGameObjectSpawnQueue() {
|
|||
result.y = capture.y;
|
||||
result.z = capture.z;
|
||||
result.orientation = capture.orientation;
|
||||
result.scale = capture.scale;
|
||||
result.modelPath = capturePath;
|
||||
result.isWmo = true;
|
||||
|
||||
|
|
@ -8183,7 +8209,7 @@ void Application::processGameObjectSpawnQueue() {
|
|||
}
|
||||
|
||||
// Cached WMO or M2 — spawn synchronously (cheap)
|
||||
spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||
spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation, s.scale);
|
||||
pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin());
|
||||
}
|
||||
}
|
||||
|
|
@ -8696,17 +8722,21 @@ void Application::updateQuestMarkers() {
|
|||
int markerType = -1; // -1 = no marker
|
||||
|
||||
using game::QuestGiverStatus;
|
||||
float markerGrayscale = 0.0f; // 0 = colour, 1 = grey (trivial quests)
|
||||
switch (status) {
|
||||
case QuestGiverStatus::AVAILABLE:
|
||||
markerType = 0; // Yellow !
|
||||
break;
|
||||
case QuestGiverStatus::AVAILABLE_LOW:
|
||||
markerType = 0; // Available (yellow !)
|
||||
markerType = 0; // Grey ! (same texture, desaturated in shader)
|
||||
markerGrayscale = 1.0f;
|
||||
break;
|
||||
case QuestGiverStatus::REWARD:
|
||||
case QuestGiverStatus::REWARD_REP:
|
||||
markerType = 1; // Turn-in (yellow ?)
|
||||
markerType = 1; // Yellow ?
|
||||
break;
|
||||
case QuestGiverStatus::INCOMPLETE:
|
||||
markerType = 2; // Incomplete (grey ?)
|
||||
markerType = 2; // Grey ?
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -8740,7 +8770,7 @@ void Application::updateQuestMarkers() {
|
|||
}
|
||||
|
||||
// Set the marker (renderer will handle positioning, bob, glow, etc.)
|
||||
questMarkerRenderer->setMarker(guid, renderPos, markerType, boundingHeight);
|
||||
questMarkerRenderer->setMarker(guid, renderPos, markerType, boundingHeight, markerGrayscale);
|
||||
markersAdded++;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,16 +109,16 @@ size_t MemoryMonitor::getAvailableRAM() const {
|
|||
|
||||
size_t MemoryMonitor::getRecommendedCacheBudget() const {
|
||||
size_t available = getAvailableRAM();
|
||||
// Use 80% of available RAM for caches (very aggressive), but cap at 90% of total
|
||||
size_t budget = available * 80 / 100;
|
||||
size_t maxBudget = totalRAM_ * 90 / 100;
|
||||
return budget < maxBudget ? budget : maxBudget;
|
||||
// Use 50% of available RAM for caches, hard-capped at 16 GB.
|
||||
static constexpr size_t kHardCapBytes = 16ull * 1024 * 1024 * 1024; // 16 GB
|
||||
size_t budget = available * 50 / 100;
|
||||
return budget < kHardCapBytes ? budget : kHardCapBytes;
|
||||
}
|
||||
|
||||
bool MemoryMonitor::isMemoryPressure() const {
|
||||
size_t available = getAvailableRAM();
|
||||
// Memory pressure if < 20% RAM available
|
||||
return available < (totalRAM_ * 20 / 100);
|
||||
// Memory pressure if < 10% RAM available
|
||||
return available < (totalRAM_ * 10 / 100);
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -19,6 +19,7 @@ struct UFNameEntry {
|
|||
|
||||
static const UFNameEntry kUFNames[] = {
|
||||
{"OBJECT_FIELD_ENTRY", UF::OBJECT_FIELD_ENTRY},
|
||||
{"OBJECT_FIELD_SCALE_X", UF::OBJECT_FIELD_SCALE_X},
|
||||
{"UNIT_FIELD_TARGET_LO", UF::UNIT_FIELD_TARGET_LO},
|
||||
{"UNIT_FIELD_TARGET_HI", UF::UNIT_FIELD_TARGET_HI},
|
||||
{"UNIT_FIELD_BYTES_0", UF::UNIT_FIELD_BYTES_0},
|
||||
|
|
@ -36,6 +37,11 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS},
|
||||
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
||||
{"UNIT_FIELD_RESISTANCES", UF::UNIT_FIELD_RESISTANCES},
|
||||
{"UNIT_FIELD_STAT0", UF::UNIT_FIELD_STAT0},
|
||||
{"UNIT_FIELD_STAT1", UF::UNIT_FIELD_STAT1},
|
||||
{"UNIT_FIELD_STAT2", UF::UNIT_FIELD_STAT2},
|
||||
{"UNIT_FIELD_STAT3", UF::UNIT_FIELD_STAT3},
|
||||
{"UNIT_FIELD_STAT4", UF::UNIT_FIELD_STAT4},
|
||||
{"UNIT_END", UF::UNIT_END},
|
||||
{"PLAYER_FLAGS", UF::PLAYER_FLAGS},
|
||||
{"PLAYER_BYTES", UF::PLAYER_BYTES},
|
||||
|
|
@ -52,6 +58,9 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START},
|
||||
{"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID},
|
||||
{"ITEM_FIELD_STACK_COUNT", UF::ITEM_FIELD_STACK_COUNT},
|
||||
{"ITEM_FIELD_DURABILITY", UF::ITEM_FIELD_DURABILITY},
|
||||
{"ITEM_FIELD_MAXDURABILITY", UF::ITEM_FIELD_MAXDURABILITY},
|
||||
{"PLAYER_REST_STATE_EXPERIENCE", UF::PLAYER_REST_STATE_EXPERIENCE},
|
||||
{"CONTAINER_FIELD_NUM_SLOTS", UF::CONTAINER_FIELD_NUM_SLOTS},
|
||||
{"CONTAINER_FIELD_SLOT_1", UF::CONTAINER_FIELD_SLOT_1},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2177,6 +2177,35 @@ network::Packet AcceptTradePacket::build() {
|
|||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SetTradeItemPacket::build(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_ITEM));
|
||||
packet.writeUInt8(tradeSlot);
|
||||
packet.writeUInt8(bag);
|
||||
packet.writeUInt8(bagSlot);
|
||||
LOG_DEBUG("Built CMSG_SET_TRADE_ITEM slot=", (int)tradeSlot, " bag=", (int)bag, " bagSlot=", (int)bagSlot);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet ClearTradeItemPacket::build(uint8_t tradeSlot) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CLEAR_TRADE_ITEM));
|
||||
packet.writeUInt8(tradeSlot);
|
||||
LOG_DEBUG("Built CMSG_CLEAR_TRADE_ITEM slot=", (int)tradeSlot);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SetTradeGoldPacket::build(uint64_t copper) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_GOLD));
|
||||
packet.writeUInt64(copper);
|
||||
LOG_DEBUG("Built CMSG_SET_TRADE_GOLD copper=", copper);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet UnacceptTradePacket::build() {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_UNACCEPT_TRADE));
|
||||
LOG_DEBUG("Built CMSG_UNACCEPT_TRADE");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE));
|
||||
packet.writeUInt64(targetGuid);
|
||||
|
|
@ -3637,11 +3666,19 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
|
|||
data.title = normalizeWowTextTokens(packet.readString());
|
||||
data.rewardText = normalizeWowTextTokens(packet.readString());
|
||||
|
||||
if (packet.getReadPos() + 10 > packet.getSize()) {
|
||||
if (packet.getReadPos() + 8 > packet.getSize()) {
|
||||
LOG_DEBUG("Quest offer reward (short): id=", data.questId, " title='", data.title, "'");
|
||||
return true;
|
||||
}
|
||||
|
||||
// After the two strings the packet contains a variable prefix (autoFinish + optional fields)
|
||||
// before the emoteCount. Different expansions and server emulator versions differ:
|
||||
// Classic 1.12 : uint8 autoFinish + uint32 suggestedPlayers = 5 bytes
|
||||
// TBC 2.4.3 : uint32 autoFinish + uint32 suggestedPlayers = 8 bytes (variable arrays)
|
||||
// WotLK 3.3.5a : uint32 autoFinish + uint32 suggestedPlayers = 8 bytes (fixed 6/4 arrays)
|
||||
// Some vanilla-family servers omit autoFinish entirely (0 bytes of prefix).
|
||||
// We scan prefix sizes 0..16 bytes with both fixed and variable array layouts, scoring each.
|
||||
|
||||
struct ParsedTail {
|
||||
uint32_t rewardMoney = 0;
|
||||
uint32_t rewardXp = 0;
|
||||
|
|
@ -3649,28 +3686,27 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
|
|||
std::vector<QuestRewardItem> fixedRewards;
|
||||
bool ok = false;
|
||||
int score = -1000;
|
||||
size_t prefixSkip = 0;
|
||||
bool fixedArrays = false;
|
||||
};
|
||||
|
||||
auto parseTail = [&](size_t startPos, bool hasFlags, bool fixedArrays) -> ParsedTail {
|
||||
auto parseTail = [&](size_t startPos, size_t prefixSkip, bool fixedArrays) -> ParsedTail {
|
||||
ParsedTail out;
|
||||
out.prefixSkip = prefixSkip;
|
||||
out.fixedArrays = fixedArrays;
|
||||
packet.setReadPos(startPos);
|
||||
|
||||
if (packet.getReadPos() + 1 > packet.getSize()) return out;
|
||||
/*autoFinish*/ packet.readUInt8();
|
||||
if (hasFlags) {
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return out;
|
||||
/*flags*/ packet.readUInt32();
|
||||
}
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return out;
|
||||
/*suggestedPlayers*/ packet.readUInt32();
|
||||
// Skip the prefix bytes (autoFinish + optional suggestedPlayers before emoteCount)
|
||||
if (packet.getReadPos() + prefixSkip > packet.getSize()) return out;
|
||||
packet.setReadPos(packet.getReadPos() + prefixSkip);
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return out;
|
||||
uint32_t emoteCount = packet.readUInt32();
|
||||
if (emoteCount > 64) return out; // guard against misalignment
|
||||
if (emoteCount > 32) return out; // guard against misalignment
|
||||
for (uint32_t i = 0; i < emoteCount; ++i) {
|
||||
if (packet.getReadPos() + 8 > packet.getSize()) return out;
|
||||
packet.readUInt32(); // delay
|
||||
packet.readUInt32(); // emote
|
||||
packet.readUInt32(); // emote type
|
||||
}
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return out;
|
||||
|
|
@ -3688,7 +3724,7 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
|
|||
item.choiceSlot = i;
|
||||
if (item.itemId > 0) {
|
||||
out.choiceRewards.push_back(item);
|
||||
nonZeroChoice++;
|
||||
++nonZeroChoice;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3706,7 +3742,7 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
|
|||
item.displayInfoId = packet.readUInt32();
|
||||
if (item.itemId > 0) {
|
||||
out.fixedRewards.push_back(item);
|
||||
nonZeroFixed++;
|
||||
++nonZeroFixed;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3717,43 +3753,56 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
|
|||
|
||||
out.ok = true;
|
||||
out.score = 0;
|
||||
if (hasFlags) out.score += 1;
|
||||
if (fixedArrays) out.score += 1;
|
||||
// Prefer the standard WotLK/TBC 8-byte prefix (uint32 autoFinish + uint32 suggestedPlayers)
|
||||
if (prefixSkip == 8) out.score += 3;
|
||||
else if (prefixSkip == 5) out.score += 1; // Classic uint8 autoFinish + uint32 suggestedPlayers
|
||||
// Prefer fixed arrays (WotLK/TBC servers always send 6+4 slots)
|
||||
if (fixedArrays) out.score += 2;
|
||||
// Valid counts
|
||||
if (choiceCount <= 6) out.score += 3;
|
||||
if (rewardCount <= 4) out.score += 3;
|
||||
if (fixedArrays) {
|
||||
if (nonZeroChoice <= choiceCount) out.score += 3;
|
||||
if (nonZeroFixed <= rewardCount) out.score += 3;
|
||||
} else {
|
||||
out.score += 3; // variable arrays align naturally with count
|
||||
}
|
||||
if (packet.getReadPos() <= packet.getSize()) out.score += 2;
|
||||
// All non-zero items are within declared counts
|
||||
if (nonZeroChoice <= choiceCount) out.score += 2;
|
||||
if (nonZeroFixed <= rewardCount) out.score += 2;
|
||||
// No bytes left over (or only a few)
|
||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining <= 32) out.score += 2;
|
||||
if (remaining == 0) out.score += 5;
|
||||
else if (remaining <= 4) out.score += 3;
|
||||
else if (remaining <= 8) out.score += 2;
|
||||
else if (remaining <= 16) out.score += 1;
|
||||
else out.score -= static_cast<int>(remaining / 4);
|
||||
// Plausible money/XP values
|
||||
if (out.rewardMoney < 5000000u) out.score += 1; // < 500g
|
||||
if (out.rewardXp < 200000u) out.score += 1; // < 200k XP
|
||||
return out;
|
||||
};
|
||||
|
||||
size_t tailStart = packet.getReadPos();
|
||||
ParsedTail a = parseTail(tailStart, true, true); // WotLK-like (flags + fixed 6/4 arrays)
|
||||
ParsedTail b = parseTail(tailStart, false, true); // no flags + fixed 6/4 arrays
|
||||
ParsedTail c = parseTail(tailStart, true, false); // flags + variable arrays
|
||||
ParsedTail d = parseTail(tailStart, false, false); // classic-like variable arrays
|
||||
// Try prefix sizes 0..16 bytes with both fixed and variable array layouts
|
||||
std::vector<ParsedTail> candidates;
|
||||
candidates.reserve(34);
|
||||
for (size_t skip = 0; skip <= 16; ++skip) {
|
||||
candidates.push_back(parseTail(tailStart, skip, true)); // fixed arrays
|
||||
candidates.push_back(parseTail(tailStart, skip, false)); // variable arrays
|
||||
}
|
||||
|
||||
const ParsedTail* best = nullptr;
|
||||
for (const ParsedTail* cand : {&a, &b, &c, &d}) {
|
||||
if (!cand->ok) continue;
|
||||
if (!best || cand->score > best->score) best = cand;
|
||||
for (const auto& cand : candidates) {
|
||||
if (!cand.ok) continue;
|
||||
if (!best || cand.score > best->score) best = &cand;
|
||||
}
|
||||
|
||||
if (best) {
|
||||
data.choiceRewards = best->choiceRewards;
|
||||
data.fixedRewards = best->fixedRewards;
|
||||
data.rewardMoney = best->rewardMoney;
|
||||
data.rewardXp = best->rewardXp;
|
||||
data.fixedRewards = best->fixedRewards;
|
||||
data.rewardMoney = best->rewardMoney;
|
||||
data.rewardXp = best->rewardXp;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Quest offer reward: id=", data.questId, " title='", data.title,
|
||||
"' choices=", data.choiceRewards.size(), " fixed=", data.fixedRewards.size());
|
||||
"' choices=", data.choiceRewards.size(), " fixed=", data.fixedRewards.size(),
|
||||
" prefix=", (best ? best->prefixSkip : size_t(0)),
|
||||
(best && best->fixedArrays ? " fixed" : " var"));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ void AssetManager::setupFileCacheBudget() {
|
|||
const size_t envMaxMB = parseEnvSizeMB("WOWEE_FILE_CACHE_MAX_MB");
|
||||
|
||||
const size_t minBudgetBytes = 256ull * 1024ull * 1024ull;
|
||||
const size_t defaultMaxBudgetBytes = 32768ull * 1024ull * 1024ull;
|
||||
const size_t defaultMaxBudgetBytes = 12288ull * 1024ull * 1024ull; // 12 GB max for file cache
|
||||
const size_t maxBudgetBytes = (envMaxMB > 0)
|
||||
? (envMaxMB * 1024ull * 1024ull)
|
||||
: defaultMaxBudgetBytes;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ std::string narrowWString(const wchar_t* msg) {
|
|||
std::string out;
|
||||
for (const wchar_t* p = msg; *p; ++p) {
|
||||
const wchar_t wc = *p;
|
||||
if (wc >= 0 && wc <= 0x7f) {
|
||||
if (wc <= 0x7f) {
|
||||
out.push_back(static_cast<char>(wc));
|
||||
} else {
|
||||
out.push_back('?');
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// Toggle sit/crouch with X key (edge-triggered) — only when UI doesn't want keyboard
|
||||
// Blocked while mounted
|
||||
bool prevSitting = sitting;
|
||||
bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X);
|
||||
if (xDown && !xKeyWasDown && !mounted_) {
|
||||
sitting = !sitting;
|
||||
|
|
@ -376,6 +377,21 @@ void CameraController::update(float deltaTime) {
|
|||
if (mounted_) sitting = false;
|
||||
xKeyWasDown = xDown;
|
||||
|
||||
// Stand up on any movement key or jump while sitting (WoW behaviour)
|
||||
if (!uiWantsKeyboard && sitting && !movementSuppressed) {
|
||||
bool anyMoveKey =
|
||||
input.isKeyPressed(SDL_SCANCODE_W) || input.isKeyPressed(SDL_SCANCODE_S) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_A) || input.isKeyPressed(SDL_SCANCODE_D) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_Q) || input.isKeyPressed(SDL_SCANCODE_E) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_SPACE);
|
||||
if (anyMoveKey) sitting = false;
|
||||
}
|
||||
|
||||
// Notify server when the player stands up via local input
|
||||
if (prevSitting && !sitting && standUpCallback_) {
|
||||
standUpCallback_();
|
||||
}
|
||||
|
||||
// Update eye height based on crouch state (smooth transition)
|
||||
float targetEyeHeight = sitting ? CROUCH_EYE_HEIGHT : STAND_EYE_HEIGHT;
|
||||
float heightLerpSpeed = 10.0f * deltaTime;
|
||||
|
|
@ -389,11 +405,6 @@ void CameraController::update(float deltaTime) {
|
|||
if (nowStrafeLeft) movement += right;
|
||||
if (nowStrafeRight) movement -= right;
|
||||
|
||||
// Stand up if jumping while crouched
|
||||
if (!uiWantsKeyboard && sitting && input.isKeyPressed(SDL_SCANCODE_SPACE)) {
|
||||
sitting = false;
|
||||
}
|
||||
|
||||
// Third-person orbit camera mode
|
||||
if (thirdPerson && followTarget) {
|
||||
// Move the follow target (character position) instead of the camera
|
||||
|
|
|
|||
|
|
@ -1327,12 +1327,12 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
|||
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
||||
}
|
||||
} else {
|
||||
// Size mismatch — blit at natural size (may clip or leave gap)
|
||||
core::Logger::getInstance().warning("compositeWithRegions: region ", regionIdx,
|
||||
" at (", dstX, ",", dstY, ") overlay=", overlay.width, "x", overlay.height,
|
||||
" expected=", expectedW, "x", expectedH, " from ", rl.second);
|
||||
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
||||
}
|
||||
|
||||
core::Logger::getInstance().warning("compositeWithRegions: region ", regionIdx,
|
||||
" at (", dstX, ",", dstY, ") overlay=", overlay.width, "x", overlay.height,
|
||||
" expected=", expectedW, "x", expectedH, " from ", rl.second);
|
||||
}
|
||||
|
||||
// Upload to GPU via VkTexture
|
||||
|
|
@ -1580,12 +1580,20 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId,
|
|||
instance.animationTime = 0.0f;
|
||||
instance.animationLoop = loop;
|
||||
|
||||
// Prefer variationIndex==0 (primary animation); fall back to first match
|
||||
int firstMatch = -1;
|
||||
for (size_t i = 0; i < model.sequences.size(); i++) {
|
||||
if (model.sequences[i].id == animationId) {
|
||||
instance.currentSequenceIndex = static_cast<int>(i);
|
||||
break;
|
||||
if (firstMatch < 0) firstMatch = static_cast<int>(i);
|
||||
if (model.sequences[i].variationIndex == 0) {
|
||||
instance.currentSequenceIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (instance.currentSequenceIndex < 0 && firstMatch >= 0) {
|
||||
instance.currentSequenceIndex = firstMatch;
|
||||
}
|
||||
|
||||
if (instance.currentSequenceIndex < 0) {
|
||||
// Fall back to first sequence
|
||||
|
|
|
|||
|
|
@ -743,10 +743,16 @@ void M2Renderer::destroyModelGPU(M2ModelGPU& model) {
|
|||
VmaAllocator alloc = vkCtx_->getAllocator();
|
||||
if (model.vertexBuffer) { vmaDestroyBuffer(alloc, model.vertexBuffer, model.vertexAlloc); model.vertexBuffer = VK_NULL_HANDLE; }
|
||||
if (model.indexBuffer) { vmaDestroyBuffer(alloc, model.indexBuffer, model.indexAlloc); model.indexBuffer = VK_NULL_HANDLE; }
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
for (auto& batch : model.batches) {
|
||||
if (batch.materialSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &batch.materialSet); batch.materialSet = VK_NULL_HANDLE; }
|
||||
if (batch.materialUBO) { vmaDestroyBuffer(alloc, batch.materialUBO, batch.materialUBOAlloc); batch.materialUBO = VK_NULL_HANDLE; }
|
||||
// materialSet freed when pool is reset/destroyed
|
||||
}
|
||||
// Free pre-allocated particle texture descriptor sets
|
||||
for (auto& pSet : model.particleTexSets) {
|
||||
if (pSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &pSet); pSet = VK_NULL_HANDLE; }
|
||||
}
|
||||
model.particleTexSets.clear();
|
||||
}
|
||||
|
||||
void M2Renderer::destroyInstanceBones(M2Instance& inst) {
|
||||
|
|
@ -979,8 +985,16 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
(lowerName.find("monument") != std::string::npos) ||
|
||||
(lowerName.find("sculpture") != std::string::npos);
|
||||
gpuModel.collisionStatue = statueName;
|
||||
// Sittable furniture: chairs/benches/stools cause players to get stuck against
|
||||
// invisible bounding boxes; WMOs already handle room collision.
|
||||
bool sittableFurnitureName =
|
||||
(lowerName.find("chair") != std::string::npos) ||
|
||||
(lowerName.find("bench") != std::string::npos) ||
|
||||
(lowerName.find("stool") != std::string::npos) ||
|
||||
(lowerName.find("seat") != std::string::npos) ||
|
||||
(lowerName.find("throne") != std::string::npos);
|
||||
bool smallSolidPropName =
|
||||
statueName ||
|
||||
(statueName && !sittableFurnitureName) ||
|
||||
(lowerName.find("crate") != std::string::npos) ||
|
||||
(lowerName.find("box") != std::string::npos) ||
|
||||
(lowerName.find("chest") != std::string::npos) ||
|
||||
|
|
@ -1023,6 +1037,10 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
(lowerName.find("bamboo") != std::string::npos) ||
|
||||
(lowerName.find("banana") != std::string::npos) ||
|
||||
(lowerName.find("coconut") != std::string::npos) ||
|
||||
(lowerName.find("watermelon") != std::string::npos) ||
|
||||
(lowerName.find("melon") != std::string::npos) ||
|
||||
(lowerName.find("squash") != std::string::npos) ||
|
||||
(lowerName.find("gourd") != std::string::npos) ||
|
||||
(lowerName.find("canopy") != std::string::npos) ||
|
||||
(lowerName.find("hedge") != std::string::npos) ||
|
||||
(lowerName.find("cactus") != std::string::npos) ||
|
||||
|
|
@ -1148,7 +1166,8 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
(lowerName.find("lavasplash") != std::string::npos) ||
|
||||
(lowerName.find("lavabubble") != std::string::npos) ||
|
||||
(lowerName.find("lavasteam") != std::string::npos) ||
|
||||
(lowerName.find("wisps") != std::string::npos);
|
||||
(lowerName.find("wisps") != std::string::npos) ||
|
||||
(lowerName.find("levelup") != std::string::npos);
|
||||
gpuModel.isSpellEffect = effectByName ||
|
||||
(hasParticles && model.vertices.size() <= 200 &&
|
||||
model.particleEmitters.size() >= 3);
|
||||
|
|
@ -1335,6 +1354,31 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Pre-allocate one stable descriptor set per particle emitter to avoid per-frame allocation.
|
||||
// This prevents materialDescPool_ exhaustion when many emitters are active each frame.
|
||||
if (particleTexLayout_ && materialDescPool_ && !model.particleEmitters.empty()) {
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
gpuModel.particleTexSets.resize(model.particleEmitters.size(), VK_NULL_HANDLE);
|
||||
for (size_t ei = 0; ei < model.particleEmitters.size(); ei++) {
|
||||
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
ai.descriptorPool = materialDescPool_;
|
||||
ai.descriptorSetCount = 1;
|
||||
ai.pSetLayouts = &particleTexLayout_;
|
||||
if (vkAllocateDescriptorSets(device, &ai, &gpuModel.particleTexSets[ei]) == VK_SUCCESS) {
|
||||
VkTexture* tex = gpuModel.particleTextures[ei];
|
||||
VkDescriptorImageInfo imgInfo = tex->descriptorInfo();
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.dstSet = gpuModel.particleTexSets[ei];
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy texture transform data for UV animation
|
||||
gpuModel.textureTransforms = model.textureTransforms;
|
||||
gpuModel.textureTransformLookup = model.textureTransformLookup;
|
||||
|
|
@ -3401,6 +3445,7 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame
|
|||
uint8_t blendType;
|
||||
uint16_t tilesX;
|
||||
uint16_t tilesY;
|
||||
VkDescriptorSet preAllocSet = VK_NULL_HANDLE; // Pre-allocated stable set, avoids per-frame alloc
|
||||
std::vector<float> vertexData; // 9 floats per particle
|
||||
};
|
||||
std::unordered_map<ParticleGroupKey, ParticleGroup, ParticleGroupKeyHash> groups;
|
||||
|
|
@ -3442,6 +3487,11 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame
|
|||
group.blendType = em.blendingType;
|
||||
group.tilesX = tilesX;
|
||||
group.tilesY = tilesY;
|
||||
// Capture pre-allocated descriptor set on first insertion for this key
|
||||
if (group.preAllocSet == VK_NULL_HANDLE &&
|
||||
p.emitterIndex < static_cast<int>(gpu.particleTexSets.size())) {
|
||||
group.preAllocSet = gpu.particleTexSets[p.emitterIndex];
|
||||
}
|
||||
|
||||
group.vertexData.push_back(p.position.x);
|
||||
group.vertexData.push_back(p.position.y);
|
||||
|
|
@ -3485,23 +3535,27 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame
|
|||
currentPipeline = desiredPipeline;
|
||||
}
|
||||
|
||||
// Allocate descriptor set for this group's texture
|
||||
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
ai.descriptorPool = materialDescPool_;
|
||||
ai.descriptorSetCount = 1;
|
||||
ai.pSetLayouts = &particleTexLayout_;
|
||||
VkDescriptorSet texSet = VK_NULL_HANDLE;
|
||||
if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &texSet) == VK_SUCCESS) {
|
||||
VkTexture* tex = group.texture ? group.texture : whiteTexture_.get();
|
||||
VkDescriptorImageInfo imgInfo = tex->descriptorInfo();
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = texSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr);
|
||||
|
||||
// Use pre-allocated stable descriptor set; fall back to per-frame alloc only if unavailable
|
||||
VkDescriptorSet texSet = group.preAllocSet;
|
||||
if (texSet == VK_NULL_HANDLE) {
|
||||
// Fallback: allocate per-frame (pool exhaustion risk — should not happen in practice)
|
||||
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
ai.descriptorPool = materialDescPool_;
|
||||
ai.descriptorSetCount = 1;
|
||||
ai.pSetLayouts = &particleTexLayout_;
|
||||
if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &texSet) == VK_SUCCESS) {
|
||||
VkTexture* tex = group.texture ? group.texture : whiteTexture_.get();
|
||||
VkDescriptorImageInfo imgInfo = tex->descriptorInfo();
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = texSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr);
|
||||
}
|
||||
}
|
||||
if (texSet != VK_NULL_HANDLE) {
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
particlePipelineLayout_, 1, 1, &texSet, 0, nullptr);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ namespace wowee { namespace rendering {
|
|||
|
||||
// Push constant layout matching quest_marker.vert.glsl / quest_marker.frag.glsl
|
||||
struct QuestMarkerPushConstants {
|
||||
glm::mat4 model; // 64 bytes, used by vertex shader
|
||||
float alpha; // 4 bytes, used by fragment shader
|
||||
glm::mat4 model; // 64 bytes, used by vertex shader
|
||||
float alpha; // 4 bytes, used by fragment shader
|
||||
float grayscale; // 4 bytes: 0=colour, 1=desaturated (trivial quests)
|
||||
};
|
||||
|
||||
QuestMarkerRenderer::QuestMarkerRenderer() {
|
||||
|
|
@ -340,8 +341,9 @@ void QuestMarkerRenderer::loadTextures(pipeline::AssetManager* assetManager) {
|
|||
}
|
||||
}
|
||||
|
||||
void QuestMarkerRenderer::setMarker(uint64_t guid, const glm::vec3& position, int markerType, float boundingHeight) {
|
||||
markers_[guid] = {position, markerType, boundingHeight};
|
||||
void QuestMarkerRenderer::setMarker(uint64_t guid, const glm::vec3& position, int markerType,
|
||||
float boundingHeight, float grayscale) {
|
||||
markers_[guid] = {position, markerType, boundingHeight, grayscale};
|
||||
}
|
||||
|
||||
void QuestMarkerRenderer::removeMarker(uint64_t guid) {
|
||||
|
|
@ -436,10 +438,11 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe
|
|||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_,
|
||||
1, 1, &texDescSets_[marker.type], 0, nullptr);
|
||||
|
||||
// Push constants: model matrix + alpha
|
||||
// Push constants: model matrix + alpha + grayscale tint
|
||||
QuestMarkerPushConstants push{};
|
||||
push.model = model;
|
||||
push.alpha = fadeAlpha;
|
||||
push.grayscale = marker.grayscale;
|
||||
|
||||
vkCmdPushConstants(cmd, pipelineLayout_,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
|
|
|
|||
|
|
@ -710,6 +710,8 @@ bool Renderer::initialize(core::Window* win) {
|
|||
|
||||
levelUpEffect = std::make_unique<LevelUpEffect>();
|
||||
|
||||
questMarkerRenderer = std::make_unique<QuestMarkerRenderer>();
|
||||
|
||||
LOG_INFO("Vulkan sub-renderers initialized (Phase 3)");
|
||||
|
||||
// LightingManager doesn't use GL — initialize for data-only use
|
||||
|
|
@ -2222,6 +2224,14 @@ void Renderer::updateCharacterAnimation() {
|
|||
} else if (sitting) {
|
||||
cancelEmote();
|
||||
newState = CharAnimState::SIT_DOWN;
|
||||
} else if (!emoteLoop && characterRenderer && characterInstanceId > 0) {
|
||||
// Auto-cancel non-looping emotes once animation completes
|
||||
uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)
|
||||
&& curDur > 0.1f && curT >= curDur - 0.05f) {
|
||||
cancelEmote();
|
||||
newState = CharAnimState::IDLE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -4845,7 +4855,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
minimapPlayerOrientation = std::atan2(-facingFwd.x, facingFwd.y);
|
||||
hasMinimapPlayerOrientation = true;
|
||||
} else if (gameHandler) {
|
||||
minimapPlayerOrientation = gameHandler->getMovementInfo().orientation;
|
||||
// Server orientation is in WoW space: π/2 = North, 0 = East.
|
||||
// Minimap arrow expects render space: 0 = North, π/2 = East.
|
||||
// Convert: minimap_angle = server_orientation - π/2
|
||||
minimapPlayerOrientation = gameHandler->getMovementInfo().orientation
|
||||
- static_cast<float>(M_PI_2);
|
||||
hasMinimapPlayerOrientation = true;
|
||||
}
|
||||
minimap->render(cmd, *camera, minimapCenter,
|
||||
|
|
@ -4973,7 +4987,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
minimapPlayerOrientation = std::atan2(-facingFwd.x, facingFwd.y);
|
||||
hasMinimapPlayerOrientation = true;
|
||||
} else if (gameHandler) {
|
||||
minimapPlayerOrientation = gameHandler->getMovementInfo().orientation;
|
||||
// Server orientation is in WoW space: π/2 = North, 0 = East.
|
||||
// Minimap arrow expects render space: 0 = North, π/2 = East.
|
||||
// Convert: minimap_angle = server_orientation - π/2
|
||||
minimapPlayerOrientation = gameHandler->getMovementInfo().orientation
|
||||
- static_cast<float>(M_PI_2);
|
||||
hasMinimapPlayerOrientation = true;
|
||||
}
|
||||
minimap->render(currentCmd, *camera, minimapCenter,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL
|
|||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.maxSets = MAX_MATERIAL_SETS;
|
||||
poolInfo.poolSizeCount = 2;
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
|
|
@ -1034,6 +1035,10 @@ void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) {
|
|||
destroyBuffer(allocator, ab);
|
||||
chunk.paramsUBO = VK_NULL_HANDLE;
|
||||
}
|
||||
// Return material descriptor set to the pool so it can be reused by new chunks
|
||||
if (chunk.materialSet && materialDescPool) {
|
||||
vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &chunk.materialSet);
|
||||
}
|
||||
chunk.materialSet = VK_NULL_HANDLE;
|
||||
|
||||
// Destroy owned alpha textures (VkTexture::~VkTexture is a no-op, must call destroy() explicitly)
|
||||
|
|
|
|||
|
|
@ -1051,14 +1051,21 @@ bool VkContext::recreateSwapchain(int width, int height) {
|
|||
|
||||
auto swapRet = builder.build();
|
||||
|
||||
if (oldSwapchain) {
|
||||
vkDestroySwapchainKHR(device, oldSwapchain, nullptr);
|
||||
if (!swapRet) {
|
||||
// Destroy old swapchain now that we failed (it can't be used either)
|
||||
if (oldSwapchain) {
|
||||
vkDestroySwapchainKHR(device, oldSwapchain, nullptr);
|
||||
swapchain = VK_NULL_HANDLE;
|
||||
}
|
||||
LOG_ERROR("Failed to recreate swapchain: ", swapRet.error().message());
|
||||
// Keep swapchainDirty=true so the next frame retries
|
||||
swapchainDirty = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!swapRet) {
|
||||
LOG_ERROR("Failed to recreate swapchain: ", swapRet.error().message());
|
||||
swapchain = VK_NULL_HANDLE;
|
||||
return false;
|
||||
// Success — safe to retire the old swapchain
|
||||
if (oldSwapchain) {
|
||||
vkDestroySwapchainKHR(device, oldSwapchain, nullptr);
|
||||
}
|
||||
|
||||
auto vkbSwap = swapRet.value();
|
||||
|
|
@ -1322,6 +1329,7 @@ bool VkContext::recreateSwapchain(int width, int height) {
|
|||
|
||||
VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) {
|
||||
if (deviceLost_) return VK_NULL_HANDLE;
|
||||
if (swapchain == VK_NULL_HANDLE) return VK_NULL_HANDLE; // Swapchain lost; recreate pending
|
||||
|
||||
auto& frame = frames[currentFrame];
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
|||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.maxSets = MAX_MATERIAL_SETS;
|
||||
poolInfo.poolSizeCount = 2;
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
|
|
@ -1946,8 +1947,13 @@ void WMORenderer::destroyGroupGPU(GroupResources& group) {
|
|||
group.indexAlloc = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// Destroy material UBOs (descriptor sets are freed when pool is reset/destroyed)
|
||||
// Destroy material UBOs and free descriptor sets back to pool
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
for (auto& mb : group.mergedBatches) {
|
||||
if (mb.materialSet) {
|
||||
vkFreeDescriptorSets(device, materialDescPool_, 1, &mb.materialSet);
|
||||
mb.materialSet = VK_NULL_HANDLE;
|
||||
}
|
||||
if (mb.materialUBO) {
|
||||
vmaDestroyBuffer(allocator, mb.materialUBO, mb.materialUBOAlloc);
|
||||
mb.materialUBO = VK_NULL_HANDLE;
|
||||
|
|
|
|||
|
|
@ -233,6 +233,10 @@ void WorldMap::setMapName(const std::string& name) {
|
|||
|
||||
void WorldMap::setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData) {
|
||||
if (!hasData || masks.empty()) {
|
||||
// New session or no data yet — reset both server mask and local accumulation
|
||||
if (hasServerExplorationMask) {
|
||||
locallyExploredZones_.clear();
|
||||
}
|
||||
hasServerExplorationMask = false;
|
||||
serverExplorationMask.clear();
|
||||
return;
|
||||
|
|
@ -765,9 +769,12 @@ void WorldMap::updateExploration(const glm::vec3& playerRenderPos) {
|
|||
}
|
||||
if (markedAny) return;
|
||||
|
||||
// Server mask unavailable or empty — fall back to locally-accumulated position tracking.
|
||||
// Add the zone the player is currently in to the local set and display that.
|
||||
float wowX = playerRenderPos.y;
|
||||
float wowY = playerRenderPos.x;
|
||||
|
||||
bool foundPos = false;
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& z = zones[i];
|
||||
if (z.areaID == 0) continue;
|
||||
|
|
@ -775,15 +782,18 @@ void WorldMap::updateExploration(const glm::vec3& playerRenderPos) {
|
|||
float minY = std::min(z.locTop, z.locBottom), maxY = std::max(z.locTop, z.locBottom);
|
||||
if (maxX - minX < 0.001f || maxY - minY < 0.001f) continue;
|
||||
if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) {
|
||||
exploredZones.insert(i);
|
||||
markedAny = true;
|
||||
locallyExploredZones_.insert(i);
|
||||
foundPos = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!markedAny) {
|
||||
if (!foundPos) {
|
||||
int zoneIdx = findZoneForPlayer(playerRenderPos);
|
||||
if (zoneIdx >= 0) exploredZones.insert(zoneIdx);
|
||||
if (zoneIdx >= 0) locallyExploredZones_.insert(zoneIdx);
|
||||
}
|
||||
|
||||
// Display the accumulated local set
|
||||
exploredZones = locallyExploredZones_;
|
||||
}
|
||||
|
||||
void WorldMap::zoomIn(const glm::vec3& playerRenderPos) {
|
||||
|
|
|
|||
|
|
@ -380,6 +380,11 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderTargetFrame(gameHandler);
|
||||
}
|
||||
|
||||
// Focus target frame (only when we have a focus)
|
||||
if (gameHandler.hasFocus()) {
|
||||
renderFocusFrame(gameHandler);
|
||||
}
|
||||
|
||||
// Render windows
|
||||
if (showPlayerInfo) {
|
||||
renderPlayerInfo(gameHandler);
|
||||
|
|
@ -409,11 +414,14 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderDuelRequestPopup(gameHandler);
|
||||
renderLootRollPopup(gameHandler);
|
||||
renderTradeRequestPopup(gameHandler);
|
||||
renderTradeWindow(gameHandler);
|
||||
renderSummonRequestPopup(gameHandler);
|
||||
renderSharedQuestPopup(gameHandler);
|
||||
renderItemTextWindow(gameHandler);
|
||||
renderGuildInvitePopup(gameHandler);
|
||||
renderReadyCheckPopup(gameHandler);
|
||||
renderBgInvitePopup(gameHandler);
|
||||
renderLfgProposalPopup(gameHandler);
|
||||
renderGuildRoster(gameHandler);
|
||||
renderBuffBar(gameHandler);
|
||||
renderLootWindow(gameHandler);
|
||||
|
|
@ -1683,11 +1691,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
heightOffset = 0.3f;
|
||||
}
|
||||
} else if (t == game::ObjectType::GAMEOBJECT) {
|
||||
// Do not hard-filter by GO type here. Some realms/content
|
||||
// classify usable objects (including some chests) with types
|
||||
// that look decorative in cache data.
|
||||
hitRadius = 2.5f;
|
||||
heightOffset = 1.2f;
|
||||
// For GOs with no renderer instance yet, use a tight fallback
|
||||
// sphere (not 2.5f) so invisible/unloaded GOs (chairs, doodads)
|
||||
// are not accidentally clicked during camera right-drag.
|
||||
hitRadius = 1.2f;
|
||||
heightOffset = 1.0f;
|
||||
}
|
||||
hitCenter = core::coords::canonicalToRender(
|
||||
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
|
|
@ -2458,6 +2466,10 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
if (totEntity->getType() == game::ObjectType::UNIT ||
|
||||
totEntity->getType() == game::ObjectType::PLAYER) {
|
||||
auto totUnit = std::static_pointer_cast<game::Unit>(totEntity);
|
||||
if (totUnit->getLevel() > 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Lv%u", totUnit->getLevel());
|
||||
}
|
||||
uint32_t hp = totUnit->getHealth();
|
||||
uint32_t maxHp = totUnit->getMaxHealth();
|
||||
if (maxHp > 0) {
|
||||
|
|
@ -2470,6 +2482,10 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
// Click to target the target-of-target
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0)) {
|
||||
gameHandler.setTarget(totGuid);
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor(2);
|
||||
|
|
@ -2479,6 +2495,134 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) {
|
||||
auto focus = gameHandler.getFocus();
|
||||
if (!focus) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
// Position: right side of screen, mirroring the target frame on the opposite side
|
||||
float frameW = 200.0f;
|
||||
float frameX = screenW - frameW - 10.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
// Determine color based on relation (same logic as target frame)
|
||||
ImVec4 focusColor(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
if (focus->getType() == game::ObjectType::PLAYER) {
|
||||
focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
} else if (focus->getType() == game::ObjectType::UNIT) {
|
||||
auto u = std::static_pointer_cast<game::Unit>(focus);
|
||||
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
||||
focusColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
} else if (u->isHostile()) {
|
||||
uint32_t playerLv = gameHandler.getPlayerLevel();
|
||||
uint32_t mobLv = u->getLevel();
|
||||
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
||||
if (game::GameHandler::killXp(playerLv, mobLv) == 0)
|
||||
focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f);
|
||||
else if (diff >= 10)
|
||||
focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
|
||||
else if (diff >= 5)
|
||||
focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f);
|
||||
else if (diff >= -2)
|
||||
focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f);
|
||||
else
|
||||
focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
} else {
|
||||
focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.15f, 0.85f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.9f, 0.8f)); // Blue tint = focus
|
||||
|
||||
if (ImGui::Begin("##FocusFrame", nullptr, flags)) {
|
||||
// "Focus" label
|
||||
ImGui::TextDisabled("[Focus]");
|
||||
ImGui::SameLine();
|
||||
|
||||
std::string focusName = getEntityName(focus);
|
||||
ImGui::TextColored(focusColor, "%s", focusName.c_str());
|
||||
|
||||
if (focus->getType() == game::ObjectType::UNIT ||
|
||||
focus->getType() == game::ObjectType::PLAYER) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(focus);
|
||||
|
||||
// Level + health on same row
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Lv %u", unit->getLevel());
|
||||
|
||||
uint32_t hp = unit->getHealth();
|
||||
uint32_t maxHp = unit->getMaxHealth();
|
||||
if (maxHp > 0) {
|
||||
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
||||
pct > 0.5f ? ImVec4(0.2f, 0.7f, 0.2f, 1.0f) :
|
||||
pct > 0.2f ? ImVec4(0.7f, 0.7f, 0.2f, 1.0f) :
|
||||
ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
||||
char overlay[32];
|
||||
snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp);
|
||||
ImGui::ProgressBar(pct, ImVec2(-1, 14), overlay);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Power bar
|
||||
uint8_t pType = unit->getPowerType();
|
||||
uint32_t pwr = unit->getPower();
|
||||
uint32_t maxPwr = unit->getMaxPower();
|
||||
if (maxPwr == 0 && (pType == 1 || pType == 3)) maxPwr = 100;
|
||||
if (maxPwr > 0) {
|
||||
float mpPct = static_cast<float>(pwr) / static_cast<float>(maxPwr);
|
||||
ImVec4 pwrColor;
|
||||
switch (pType) {
|
||||
case 0: pwrColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
||||
case 1: pwrColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break;
|
||||
case 3: pwrColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break;
|
||||
case 6: pwrColor = ImVec4(0.8f, 0.1f, 0.2f, 1.0f); break;
|
||||
default: pwrColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, pwrColor);
|
||||
ImGui::ProgressBar(mpPct, ImVec2(-1, 10), "");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
// Focus cast bar
|
||||
const auto* focusCast = gameHandler.getUnitCastState(focus->getGuid());
|
||||
if (focusCast) {
|
||||
float total = focusCast->timeTotal > 0.f ? focusCast->timeTotal : 1.f;
|
||||
float rem = focusCast->timeRemaining;
|
||||
float prog = std::clamp(1.0f - rem / total, 0.f, 1.f);
|
||||
const std::string& spName = gameHandler.getSpellName(focusCast->spellId);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f));
|
||||
char castBuf[64];
|
||||
if (!spName.empty())
|
||||
snprintf(castBuf, sizeof(castBuf), "%s (%.1fs)", spName.c_str(), rem);
|
||||
else
|
||||
snprintf(castBuf, sizeof(castBuf), "Casting... (%.1fs)", rem);
|
||||
ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking the focus frame targets it
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0)) {
|
||||
gameHandler.setTarget(focus->getGuid());
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
||||
if (strlen(chatInputBuffer) > 0) {
|
||||
std::string input(chatInputBuffer);
|
||||
|
|
@ -4867,7 +5011,7 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
|
|||
if (toShow.empty()) return;
|
||||
|
||||
float x = screenW - TRACKER_W - RIGHT_MARGIN;
|
||||
float y = 200.0f; // below minimap area
|
||||
float y = 320.0f; // below minimap (210) + buff bar space (up to 3 rows ≈ 114px)
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(TRACKER_W, 0), ImGuiCond_Always);
|
||||
|
|
@ -4905,10 +5049,15 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
|
|||
} else {
|
||||
// Kill counts
|
||||
for (const auto& [entry, progress] : q.killCounts) {
|
||||
std::string creatureName = gameHandler.getCachedCreatureName(entry);
|
||||
if (!creatureName.empty()) {
|
||||
std::string name = gameHandler.getCachedCreatureName(entry);
|
||||
if (name.empty()) {
|
||||
// May be a game object objective; fall back to GO name cache.
|
||||
const auto* goInfo = gameHandler.getCachedGameObjectInfo(entry);
|
||||
if (goInfo && !goInfo->name.empty()) name = goInfo->name;
|
||||
}
|
||||
if (!name.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
|
||||
" %s: %u/%u", creatureName.c_str(),
|
||||
" %s: %u/%u", name.c_str(),
|
||||
progress.first, progress.second);
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
|
||||
|
|
@ -5024,7 +5173,10 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
|||
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
||||
break;
|
||||
case game::CombatTextEntry::BLOCK:
|
||||
snprintf(text, sizeof(text), outgoing ? "Block" : "You Block");
|
||||
if (entry.amount > 0)
|
||||
snprintf(text, sizeof(text), outgoing ? "Block %d" : "You Block %d", entry.amount);
|
||||
else
|
||||
snprintf(text, sizeof(text), outgoing ? "Block" : "You Block");
|
||||
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
|
||||
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
||||
break;
|
||||
|
|
@ -5054,6 +5206,20 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
|||
snprintf(text, sizeof(text), "Immune!");
|
||||
color = ImVec4(0.9f, 0.9f, 0.9f, alpha); // White for immune
|
||||
break;
|
||||
case game::CombatTextEntry::ABSORB:
|
||||
if (entry.amount > 0)
|
||||
snprintf(text, sizeof(text), "Absorbed %d", entry.amount);
|
||||
else
|
||||
snprintf(text, sizeof(text), "Absorbed");
|
||||
color = ImVec4(0.5f, 0.8f, 1.0f, alpha); // Light blue for absorb
|
||||
break;
|
||||
case game::CombatTextEntry::RESIST:
|
||||
if (entry.amount > 0)
|
||||
snprintf(text, sizeof(text), "Resisted %d", entry.amount);
|
||||
else
|
||||
snprintf(text, sizeof(text), "Resisted");
|
||||
color = ImVec4(0.7f, 0.7f, 0.7f, alpha); // Grey for resist
|
||||
break;
|
||||
default:
|
||||
snprintf(text, sizeof(text), "%d", entry.amount);
|
||||
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
||||
|
|
@ -5094,6 +5260,27 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
|
|||
const uint64_t playerGuid = gameHandler.getPlayerGuid();
|
||||
const uint64_t targetGuid = gameHandler.getTargetGuid();
|
||||
|
||||
// Build set of creature entries that are kill objectives in active (incomplete) quests.
|
||||
std::unordered_set<uint32_t> questKillEntries;
|
||||
{
|
||||
const auto& questLog = gameHandler.getQuestLog();
|
||||
const auto& trackedIds = gameHandler.getTrackedQuestIds();
|
||||
for (const auto& q : questLog) {
|
||||
if (q.complete || q.questId == 0) continue;
|
||||
// Only highlight for tracked quests (or all if nothing tracked).
|
||||
if (!trackedIds.empty() && !trackedIds.count(q.questId)) continue;
|
||||
for (const auto& obj : q.killObjectives) {
|
||||
if (obj.npcOrGoId > 0 && obj.required > 0) {
|
||||
// Check if not already completed.
|
||||
auto it = q.killCounts.find(static_cast<uint32_t>(obj.npcOrGoId));
|
||||
if (it == q.killCounts.end() || it->second.first < it->second.second) {
|
||||
questKillEntries.insert(static_cast<uint32_t>(obj.npcOrGoId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImDrawList* drawList = ImGui::GetBackgroundDrawList();
|
||||
|
||||
for (const auto& [guid, entityPtr] : gameHandler.getEntityManager().getEntities()) {
|
||||
|
|
@ -5108,9 +5295,13 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
|
|||
// Player nameplates are always shown; NPC nameplates respect the V-key toggle
|
||||
if (!isPlayer && !showNameplates_) continue;
|
||||
|
||||
// Convert canonical WoW position → render space, raise to head height
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(
|
||||
glm::vec3(unit->getX(), unit->getY(), unit->getZ()));
|
||||
// Prefer the renderer's actual instance position so the nameplate tracks the
|
||||
// rendered model exactly (avoids drift from the parallel entity interpolator).
|
||||
glm::vec3 renderPos;
|
||||
if (!core::Application::getInstance().getRenderPositionForGuid(guid, renderPos)) {
|
||||
renderPos = core::coords::canonicalToRender(
|
||||
glm::vec3(unit->getX(), unit->getY(), unit->getZ()));
|
||||
}
|
||||
renderPos.z += 2.3f;
|
||||
|
||||
// Cull distance: target or other players up to 40 units; NPC others up to 20 units
|
||||
|
|
@ -5215,6 +5406,14 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
|
|||
drawList->AddText(ImVec2(markX + 1.0f, nameY + 1.0f), IM_COL32(0,0,0,120), kNPMarks[raidMark].sym);
|
||||
drawList->AddText(ImVec2(markX, nameY), kNPMarks[raidMark].col, kNPMarks[raidMark].sym);
|
||||
}
|
||||
|
||||
// Quest kill objective indicator: small yellow sword icon to the right of the name
|
||||
if (!isPlayer && questKillEntries.count(unit->getEntry())) {
|
||||
const char* objSym = "\xe2\x9a\x94"; // ⚔ crossed swords (UTF-8)
|
||||
float objX = nameX + textSize.x + 4.0f;
|
||||
drawList->AddText(ImVec2(objX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), objSym);
|
||||
drawList->AddText(ImVec2(objX, nameY), IM_COL32(255, 220, 0, A(230)), objSym);
|
||||
}
|
||||
}
|
||||
|
||||
// Click to target: detect left-click inside the combined nameplate region
|
||||
|
|
@ -5313,13 +5512,27 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
|||
bool isDead = (m.onlineStatus & 0x0020) != 0;
|
||||
bool isGhost = (m.onlineStatus & 0x0010) != 0;
|
||||
|
||||
// Name text (truncated)
|
||||
// Name text (truncated); leader name is gold
|
||||
char truncName[16];
|
||||
snprintf(truncName, sizeof(truncName), "%.12s", m.name.c_str());
|
||||
ImU32 nameCol = (!isOnline || isDead || isGhost)
|
||||
? IM_COL32(140, 140, 140, 200) : IM_COL32(220, 220, 220, 255);
|
||||
bool isMemberLeader = (m.guid == partyData.leaderGuid);
|
||||
ImU32 nameCol = isMemberLeader ? IM_COL32(255, 215, 0, 255) :
|
||||
(!isOnline || isDead || isGhost)
|
||||
? IM_COL32(140, 140, 140, 200) : IM_COL32(220, 220, 220, 255);
|
||||
draw->AddText(ImVec2(cellMin.x + 4.0f, cellMin.y + 3.0f), nameCol, truncName);
|
||||
|
||||
// Leader crown star in top-right of cell
|
||||
if (isMemberLeader)
|
||||
draw->AddText(ImVec2(cellMax.x - 10.0f, cellMin.y + 2.0f), IM_COL32(255, 215, 0, 255), "*");
|
||||
|
||||
// LFG role badge in bottom-right corner of cell
|
||||
if (m.roles & 0x02)
|
||||
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(80, 130, 255, 230), "T");
|
||||
else if (m.roles & 0x04)
|
||||
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(60, 220, 80, 230), "H");
|
||||
else if (m.roles & 0x08)
|
||||
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(220, 80, 80, 230), "D");
|
||||
|
||||
// Health bar
|
||||
uint32_t hp = m.hasPartyStats ? m.curHealth : 0;
|
||||
uint32_t maxHp = m.hasPartyStats ? m.maxHealth : 0;
|
||||
|
|
@ -5397,11 +5610,14 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
|||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.8f));
|
||||
|
||||
if (ImGui::Begin("##PartyFrames", nullptr, flags)) {
|
||||
const uint64_t leaderGuid = partyData.leaderGuid;
|
||||
for (const auto& member : partyData.members) {
|
||||
ImGui::PushID(static_cast<int>(member.guid));
|
||||
|
||||
// Name with level and status info
|
||||
std::string label = member.name;
|
||||
bool isLeader = (member.guid == leaderGuid);
|
||||
|
||||
// Name with level and status info — leader gets a gold star prefix
|
||||
std::string label = (isLeader ? "* " : " ") + member.name;
|
||||
if (member.hasPartyStats && member.level > 0) {
|
||||
label += " [" + std::to_string(member.level) + "]";
|
||||
}
|
||||
|
|
@ -5413,10 +5629,20 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
|||
else if (isDead || isGhost) label += " (dead)";
|
||||
}
|
||||
|
||||
// Clickable name to target
|
||||
// Clickable name to target; leader name is gold
|
||||
if (isLeader) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
||||
if (ImGui::Selectable(label.c_str(), gameHandler.getTargetGuid() == member.guid)) {
|
||||
gameHandler.setTarget(member.guid);
|
||||
}
|
||||
if (isLeader) ImGui::PopStyleColor();
|
||||
|
||||
// LFG role badge (Tank/Healer/DPS) — shown on same line as name when set
|
||||
if (member.roles != 0) {
|
||||
ImGui::SameLine();
|
||||
if (member.roles & 0x02) ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "[T]");
|
||||
if (member.roles & 0x04) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.3f, 1.0f), "[H]"); }
|
||||
if (member.roles & 0x08) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[D]"); }
|
||||
}
|
||||
|
||||
// Health bar: prefer party stats, fall back to entity
|
||||
uint32_t hp = 0, maxHp = 0;
|
||||
|
|
@ -5475,6 +5701,32 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
|||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Right-click context menu for party member actions
|
||||
if (ImGui::BeginPopupContextItem("PartyMemberCtx")) {
|
||||
ImGui::TextDisabled("%s", member.name.c_str());
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Target")) {
|
||||
gameHandler.setTarget(member.guid);
|
||||
}
|
||||
if (ImGui::MenuItem("Set Focus")) {
|
||||
gameHandler.setFocus(member.guid);
|
||||
}
|
||||
if (ImGui::MenuItem("Whisper")) {
|
||||
selectedChatType = 4; // WHISPER
|
||||
strncpy(whisperTargetBuffer, member.name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
||||
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
||||
refocusChatInput = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Trade")) {
|
||||
gameHandler.initiateTrade(member.guid);
|
||||
}
|
||||
if (ImGui::MenuItem("Inspect")) {
|
||||
gameHandler.setTarget(member.guid);
|
||||
gameHandler.inspectTarget();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
|
@ -5746,6 +5998,150 @@ void GameScreen::renderTradeRequestPopup(game::GameHandler& gameHandler) {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
void GameScreen::renderTradeWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isTradeOpen()) return;
|
||||
|
||||
const auto& mySlots = gameHandler.getMyTradeSlots();
|
||||
const auto& peerSlots = gameHandler.getPeerTradeSlots();
|
||||
const uint64_t myGold = gameHandler.getMyTradeGold();
|
||||
const uint64_t peerGold = gameHandler.getPeerTradeGold();
|
||||
const auto& peerName = gameHandler.getTradePeerName();
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 240.0f, screenH / 2.0f - 180.0f), ImGuiCond_Once);
|
||||
ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once);
|
||||
|
||||
bool open = true;
|
||||
if (ImGui::Begin(("Trade with " + peerName).c_str(), &open,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
||||
|
||||
auto formatGold = [](uint64_t copper, char* buf, size_t bufsz) {
|
||||
uint64_t g = copper / 10000;
|
||||
uint64_t s = (copper % 10000) / 100;
|
||||
uint64_t c = copper % 100;
|
||||
if (g > 0) std::snprintf(buf, bufsz, "%llug %llus %lluc",
|
||||
(unsigned long long)g, (unsigned long long)s, (unsigned long long)c);
|
||||
else if (s > 0) std::snprintf(buf, bufsz, "%llus %lluc",
|
||||
(unsigned long long)s, (unsigned long long)c);
|
||||
else std::snprintf(buf, bufsz, "%lluc", (unsigned long long)c);
|
||||
};
|
||||
|
||||
auto renderSlotColumn = [&](const char* label,
|
||||
const std::array<game::GameHandler::TradeSlot,
|
||||
game::GameHandler::TRADE_SLOT_COUNT>& slots,
|
||||
uint64_t gold, bool isMine) {
|
||||
ImGui::Text("%s", label);
|
||||
ImGui::Separator();
|
||||
|
||||
for (int i = 0; i < game::GameHandler::TRADE_SLOT_COUNT; ++i) {
|
||||
const auto& slot = slots[i];
|
||||
ImGui::PushID(i * (isMine ? 1 : -1) - (isMine ? 0 : 100));
|
||||
|
||||
if (slot.occupied && slot.itemId != 0) {
|
||||
const auto* info = gameHandler.getItemInfo(slot.itemId);
|
||||
std::string name = (info && info->valid && !info->name.empty())
|
||||
? info->name
|
||||
: ("Item " + std::to_string(slot.itemId));
|
||||
if (slot.stackCount > 1)
|
||||
name += " x" + std::to_string(slot.stackCount);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 1.0f), " %d. %s", i + 1, name.c_str());
|
||||
|
||||
if (isMine && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
gameHandler.clearTradeItem(static_cast<uint8_t>(i));
|
||||
}
|
||||
if (isMine && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Double-click to remove");
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled(" %d. (empty)", i + 1);
|
||||
|
||||
// Allow dragging inventory items into trade slots via right-click context menu
|
||||
if (isMine && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
ImGui::OpenPopup(("##additem" + std::to_string(i)).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (isMine) {
|
||||
// Drag-from-inventory: show small popup listing bag items
|
||||
if (ImGui::BeginPopup(("##additem" + std::to_string(i)).c_str())) {
|
||||
ImGui::TextDisabled("Add from inventory:");
|
||||
const auto& inv = gameHandler.getInventory();
|
||||
// Backpack slots 0-15 (bag=255)
|
||||
for (int si = 0; si < game::Inventory::BACKPACK_SLOTS; ++si) {
|
||||
const auto& slot = inv.getBackpackSlot(si);
|
||||
if (slot.empty()) continue;
|
||||
const auto* ii = gameHandler.getItemInfo(slot.item.itemId);
|
||||
std::string iname = (ii && ii->valid && !ii->name.empty())
|
||||
? ii->name
|
||||
: (!slot.item.name.empty() ? slot.item.name
|
||||
: ("Item " + std::to_string(slot.item.itemId)));
|
||||
if (ImGui::Selectable(iname.c_str())) {
|
||||
// bag=255 = main backpack
|
||||
gameHandler.setTradeItem(static_cast<uint8_t>(i), 255u,
|
||||
static_cast<uint8_t>(si));
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Gold row
|
||||
char gbuf[48];
|
||||
formatGold(gold, gbuf, sizeof(gbuf));
|
||||
ImGui::Spacing();
|
||||
if (isMine) {
|
||||
ImGui::Text("Gold offered: %s", gbuf);
|
||||
static char goldInput[32] = "0";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
if (ImGui::InputText("##goldset", goldInput, sizeof(goldInput),
|
||||
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
uint64_t copper = std::strtoull(goldInput, nullptr, 10);
|
||||
gameHandler.setTradeGold(copper);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(copper, Enter to set)");
|
||||
} else {
|
||||
ImGui::Text("Gold offered: %s", gbuf);
|
||||
}
|
||||
};
|
||||
|
||||
// Two-column layout: my offer | peer offer
|
||||
float colW = ImGui::GetContentRegionAvail().x * 0.5f - 4.0f;
|
||||
ImGui::BeginChild("##myoffer", ImVec2(colW, 240.0f), true);
|
||||
renderSlotColumn("Your offer", mySlots, myGold, true);
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginChild("##peroffer", ImVec2(colW, 240.0f), true);
|
||||
renderSlotColumn((peerName + "'s offer").c_str(), peerSlots, peerGold, false);
|
||||
ImGui::EndChild();
|
||||
|
||||
// Buttons
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ImGui::Button("Accept Trade", ImVec2(bw, 0))) {
|
||||
gameHandler.acceptTrade();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(bw, 0))) {
|
||||
gameHandler.cancelTrade();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
gameHandler.cancelTrade();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.hasPendingLootRoll()) return;
|
||||
|
||||
|
|
@ -5771,7 +6167,19 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) {
|
|||
ImVec4 col = (q < 6) ? kQualityColors[q] : kQualityColors[1];
|
||||
|
||||
ImGui::Text("An item is up for rolls:");
|
||||
|
||||
// Show item icon if available
|
||||
const auto* rollInfo = gameHandler.getItemInfo(roll.itemId);
|
||||
uint32_t rollDisplayId = rollInfo ? rollInfo->displayInfoId : 0;
|
||||
VkDescriptorSet rollIcon = rollDisplayId ? inventoryScreen.getItemIcon(rollDisplayId) : VK_NULL_HANDLE;
|
||||
if (rollIcon) {
|
||||
ImGui::Image((ImTextureID)(uintptr_t)rollIcon, ImVec2(24, 24));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::TextColored(col, "[%s]", roll.itemName.c_str());
|
||||
if (ImGui::IsItemHovered() && rollInfo && rollInfo->valid) {
|
||||
inventoryScreen.renderItemTooltip(*rollInfo);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Need", ImVec2(80, 30))) {
|
||||
|
|
@ -5851,6 +6259,148 @@ void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
void GameScreen::renderBgInvitePopup(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.hasPendingBgInvite()) return;
|
||||
|
||||
const auto& queues = gameHandler.getBgQueues();
|
||||
// Find the first WAIT_JOIN slot
|
||||
const game::GameHandler::BgQueueSlot* slot = nullptr;
|
||||
for (const auto& s : queues) {
|
||||
if (s.statusId == 2) { slot = &s; break; }
|
||||
}
|
||||
if (!slot) return;
|
||||
|
||||
// Compute time remaining
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(now - slot->inviteReceivedTime).count();
|
||||
double remaining = static_cast<double>(slot->inviteTimeout) - elapsed;
|
||||
|
||||
// If invite has expired, clear it silently (server will handle the queue)
|
||||
if (remaining <= 0.0) {
|
||||
gameHandler.declineBattlefield(slot->queueSlot);
|
||||
return;
|
||||
}
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 190, screenH / 2 - 70), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(380, 0), ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.95f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.15f, 0.4f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
||||
|
||||
const ImGuiWindowFlags popupFlags =
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse;
|
||||
|
||||
if (ImGui::Begin("Battleground Ready!", nullptr, popupFlags)) {
|
||||
// BG name
|
||||
std::string bgName;
|
||||
if (slot->arenaType > 0) {
|
||||
bgName = std::to_string(slot->arenaType) + "v" + std::to_string(slot->arenaType) + " Arena";
|
||||
} else {
|
||||
switch (slot->bgTypeId) {
|
||||
case 1: bgName = "Alterac Valley"; break;
|
||||
case 2: bgName = "Warsong Gulch"; break;
|
||||
case 3: bgName = "Arathi Basin"; break;
|
||||
case 7: bgName = "Eye of the Storm"; break;
|
||||
case 9: bgName = "Strand of the Ancients"; break;
|
||||
case 11: bgName = "Isle of Conquest"; break;
|
||||
default: bgName = "Battleground"; break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", bgName.c_str());
|
||||
ImGui::TextWrapped("A spot has opened! You have %d seconds to enter.", static_cast<int>(remaining));
|
||||
ImGui::Spacing();
|
||||
|
||||
// Countdown progress bar
|
||||
float frac = static_cast<float>(remaining / static_cast<double>(slot->inviteTimeout));
|
||||
frac = std::clamp(frac, 0.0f, 1.0f);
|
||||
ImVec4 barColor = frac > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f)
|
||||
: frac > 0.25f ? ImVec4(0.9f, 0.7f, 0.1f, 1.0f)
|
||||
: ImVec4(0.9f, 0.2f, 0.2f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor);
|
||||
char countdownLabel[32];
|
||||
snprintf(countdownLabel, sizeof(countdownLabel), "%ds", static_cast<int>(remaining));
|
||||
ImGui::ProgressBar(frac, ImVec2(-1, 16), countdownLabel);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.15f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f));
|
||||
if (ImGui::Button("Enter Battleground", ImVec2(180, 30))) {
|
||||
gameHandler.acceptBattlefield(slot->queueSlot);
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
||||
if (ImGui::Button("Leave Queue", ImVec2(175, 30))) {
|
||||
gameHandler.declineBattlefield(slot->queueSlot);
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) {
|
||||
using LfgState = game::GameHandler::LfgState;
|
||||
if (gameHandler.getLfgState() != LfgState::Proposal) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 175.0f, screenH / 2.0f - 65.0f), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(350.0f, 0.0f), ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.14f, 0.08f, 0.96f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.8f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.1f, 0.3f, 0.1f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
||||
|
||||
const ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse;
|
||||
|
||||
if (ImGui::Begin("Dungeon Finder", nullptr, flags)) {
|
||||
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "A group has been found!");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("Please accept or decline to join the dungeon.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.15f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f));
|
||||
if (ImGui::Button("Accept", ImVec2(155.0f, 30.0f))) {
|
||||
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true);
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
||||
if (ImGui::Button("Decline", ImVec2(155.0f, 30.0f))) {
|
||||
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), false);
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
||||
// O key toggle (WoW default Social/Guild keybind)
|
||||
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
||||
|
|
@ -6298,12 +6848,15 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
|||
|
||||
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
||||
|
||||
// Position below the player frame in top-left
|
||||
// Position below the minimap (minimap: 200x200 at top-right, bottom edge at Y≈210)
|
||||
// Anchored to the right side to stay away from party frames on the left
|
||||
constexpr float ICON_SIZE = 32.0f;
|
||||
constexpr int ICONS_PER_ROW = 8;
|
||||
float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f;
|
||||
// Dock under player frame in top-left (player frame is at 10, 30 with ~110px height)
|
||||
ImGui::SetNextWindowPos(ImVec2(10.0f, 145.0f), ImGuiCond_Always);
|
||||
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
||||
float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f;
|
||||
// Y=215 puts us just below the minimap's bottom edge (minimap bottom ≈ 210)
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW - barW - 10.0f, 215.0f), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(barW, 0), ImGuiCond_Always);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
|
|
@ -6314,16 +6867,22 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
|||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f));
|
||||
|
||||
if (ImGui::Begin("##BuffBar", nullptr, flags)) {
|
||||
int shown = 0;
|
||||
for (size_t i = 0; i < auras.size() && shown < 16; ++i) {
|
||||
// Separate buffs and debuffs; show buffs first, then debuffs with a visual gap
|
||||
// Render one pass for buffs, one for debuffs
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
bool wantBuff = (pass == 0);
|
||||
int shown = 0;
|
||||
for (size_t i = 0; i < auras.size() && shown < 40; ++i) {
|
||||
const auto& aura = auras[i];
|
||||
if (aura.isEmpty()) continue;
|
||||
|
||||
bool isBuff = (aura.flags & 0x80) == 0; // 0x80 = negative/debuff flag
|
||||
if (isBuff != wantBuff) continue; // only render matching pass
|
||||
|
||||
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
||||
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::PushID(static_cast<int>(i) + (pass * 256));
|
||||
|
||||
bool isBuff = (aura.flags & 0x80) == 0; // 0x80 = negative/debuff flag
|
||||
ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f);
|
||||
|
||||
// Try to get spell icon
|
||||
|
|
@ -6418,10 +6977,14 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
|||
|
||||
ImGui::PopID();
|
||||
shown++;
|
||||
}
|
||||
} // end aura loop
|
||||
// Add visual gap between buffs and debuffs
|
||||
if (pass == 0 && shown > 0) ImGui::Spacing();
|
||||
} // end pass loop
|
||||
|
||||
// Dismiss Pet button
|
||||
if (gameHandler.hasPet()) {
|
||||
if (shown > 0) ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 0.9f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.3f, 0.3f, 1.0f));
|
||||
if (ImGui::Button("Dismiss Pet", ImVec2(-1, 0))) {
|
||||
|
|
@ -6495,6 +7058,13 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
// Show item tooltip on hover
|
||||
if (hovered && info && info->valid) {
|
||||
inventoryScreen.renderItemTooltip(*info);
|
||||
} else if (hovered && !itemName.empty() && itemName[0] != 'I') {
|
||||
ImGui::SetTooltip("%s", itemName.c_str());
|
||||
}
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
// Draw hover highlight
|
||||
|
|
@ -6977,15 +7547,16 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
|
|||
return {iconTex, col};
|
||||
};
|
||||
|
||||
// Helper: show item tooltip
|
||||
auto rewardItemTooltip = [&](const game::QuestRewardItem& ri, ImVec4 nameCol) {
|
||||
// Helper: show full item tooltip (reuses InventoryScreen's rich tooltip)
|
||||
auto rewardItemTooltip = [&](const game::QuestRewardItem& ri, ImVec4 /*nameCol*/) {
|
||||
auto* info = gameHandler.getItemInfo(ri.itemId);
|
||||
if (!info || !info->valid) return;
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextColored(nameCol, "%s", info->name.c_str());
|
||||
if (!info->description.empty())
|
||||
ImGui::TextWrapped("%s", info->description.c_str());
|
||||
ImGui::EndTooltip();
|
||||
if (!info || !info->valid) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextDisabled("Loading item data...");
|
||||
ImGui::EndTooltip();
|
||||
return;
|
||||
}
|
||||
inventoryScreen.renderItemTooltip(*info);
|
||||
};
|
||||
|
||||
if (!quest.choiceRewards.empty()) {
|
||||
|
|
@ -7218,24 +7789,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
||||
// Tooltip with stats on hover
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
||||
if (info->damageMax > 0.0f) {
|
||||
ImGui::Text("%.0f - %.0f Damage", info->damageMin, info->damageMax);
|
||||
if (info->delayMs > 0) {
|
||||
float speed = static_cast<float>(info->delayMs) / 1000.0f;
|
||||
float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed;
|
||||
ImGui::Text("Speed %.2f", speed);
|
||||
ImGui::Text("%.1f damage per second", dps);
|
||||
}
|
||||
}
|
||||
if (info->armor > 0) ImGui::Text("Armor: %d", info->armor);
|
||||
if (info->stamina > 0) ImGui::Text("+%d Stamina", info->stamina);
|
||||
if (info->strength > 0) ImGui::Text("+%d Strength", info->strength);
|
||||
if (info->agility > 0) ImGui::Text("+%d Agility", info->agility);
|
||||
if (info->intellect > 0) ImGui::Text("+%d Intellect", info->intellect);
|
||||
if (info->spirit > 0) ImGui::Text("+%d Spirit", info->spirit);
|
||||
ImGui::EndTooltip();
|
||||
inventoryScreen.renderItemTooltip(*info);
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("Item %u", item.itemId);
|
||||
|
|
@ -8869,10 +9423,13 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|||
float dx = worldRenderPos.x - playerRender.x;
|
||||
float dy = worldRenderPos.y - playerRender.y;
|
||||
|
||||
// Match minimap shader transform exactly.
|
||||
// Render axes: +X=west, +Y=north. Minimap screen axes: +X=right(east), +Y=down(south).
|
||||
float rx = -dx * cosB + dy * sinB;
|
||||
float ry = -dx * sinB - dy * cosB;
|
||||
// Exact inverse of minimap display shader:
|
||||
// shader: mapUV = playerUV + vec2(-rotated.x, rotated.y) * zoom * 2
|
||||
// where rotated = R(bearing) * center, center in [-0.5, 0.5]
|
||||
// Inverse: center = R^-1(bearing) * (-deltaUV.x, deltaUV.y) / (zoom*2)
|
||||
// With deltaUV.x ∝ +dx (render +X=west=larger U) and deltaUV.y ∝ -dy (V increases south):
|
||||
float rx = -(dx * cosB + dy * sinB);
|
||||
float ry = dx * sinB - dy * cosB;
|
||||
|
||||
// Scale to minimap pixels
|
||||
float px = rx / viewRadius * mapRadius;
|
||||
|
|
@ -9135,21 +9692,73 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|||
}
|
||||
ImGui::End();
|
||||
|
||||
// "New Mail" indicator below the minimap
|
||||
// Indicators below the minimap (stacked: new mail, then BG queue, then latency)
|
||||
float indicatorX = centerX - mapRadius;
|
||||
float nextIndicatorY = centerY + mapRadius + 4.0f;
|
||||
const float indicatorW = mapRadius * 2.0f;
|
||||
constexpr float kIndicatorH = 22.0f;
|
||||
ImGuiWindowFlags indicatorFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs;
|
||||
|
||||
// "New Mail" indicator
|
||||
if (gameHandler.hasNewMail()) {
|
||||
float indicatorX = centerX - mapRadius;
|
||||
float indicatorY = centerY + mapRadius + 4.0f;
|
||||
ImGui::SetNextWindowPos(ImVec2(indicatorX, indicatorY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(mapRadius * 2.0f, 22), ImGuiCond_Always);
|
||||
ImGuiWindowFlags mailFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs;
|
||||
if (ImGui::Begin("##NewMailIndicator", nullptr, mailFlags)) {
|
||||
// Pulsing effect
|
||||
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
||||
if (ImGui::Begin("##NewMailIndicator", nullptr, indicatorFlags)) {
|
||||
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 3.0f);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, pulse), "New Mail!");
|
||||
}
|
||||
ImGui::End();
|
||||
nextIndicatorY += kIndicatorH;
|
||||
}
|
||||
|
||||
// BG queue status indicator (when in queue but not yet invited)
|
||||
for (const auto& slot : gameHandler.getBgQueues()) {
|
||||
if (slot.statusId != 1) continue; // STATUS_WAIT_QUEUE only
|
||||
|
||||
std::string bgName;
|
||||
if (slot.arenaType > 0) {
|
||||
bgName = std::to_string(slot.arenaType) + "v" + std::to_string(slot.arenaType) + " Arena";
|
||||
} else {
|
||||
switch (slot.bgTypeId) {
|
||||
case 1: bgName = "AV"; break;
|
||||
case 2: bgName = "WSG"; break;
|
||||
case 3: bgName = "AB"; break;
|
||||
case 7: bgName = "EotS"; break;
|
||||
case 9: bgName = "SotA"; break;
|
||||
case 11: bgName = "IoC"; break;
|
||||
default: bgName = "BG"; break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
||||
if (ImGui::Begin("##BgQueueIndicator", nullptr, indicatorFlags)) {
|
||||
float pulse = 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 1.5f);
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse),
|
||||
"In Queue: %s", bgName.c_str());
|
||||
}
|
||||
ImGui::End();
|
||||
nextIndicatorY += kIndicatorH;
|
||||
break; // Show at most one queue slot indicator
|
||||
}
|
||||
|
||||
// Latency indicator (shown when in world and last latency is known)
|
||||
uint32_t latMs = gameHandler.getLatencyMs();
|
||||
if (latMs > 0 && gameHandler.getState() == game::WorldState::IN_WORLD) {
|
||||
ImVec4 latColor;
|
||||
if (latMs < 100) latColor = ImVec4(0.3f, 1.0f, 0.3f, 0.8f); // Green < 100ms
|
||||
else if (latMs < 250) latColor = ImVec4(1.0f, 1.0f, 0.3f, 0.8f); // Yellow < 250ms
|
||||
else if (latMs < 500) latColor = ImVec4(1.0f, 0.6f, 0.1f, 0.8f); // Orange < 500ms
|
||||
else latColor = ImVec4(1.0f, 0.2f, 0.2f, 0.8f); // Red >= 500ms
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
||||
if (ImGui::Begin("##LatencyIndicator", nullptr, indicatorFlags)) {
|
||||
ImGui::TextColored(latColor, "%u ms", latMs);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10795,8 +11404,9 @@ void GameScreen::renderDingEffect() {
|
|||
IM_COL32(255, 210, 0, (int)(alpha * 255)), buf);
|
||||
}
|
||||
|
||||
void GameScreen::triggerAchievementToast(uint32_t achievementId) {
|
||||
void GameScreen::triggerAchievementToast(uint32_t achievementId, std::string name) {
|
||||
achievementToastId_ = achievementId;
|
||||
achievementToastName_ = std::move(name);
|
||||
achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION;
|
||||
|
||||
// Play a UI sound if available
|
||||
|
|
@ -10855,9 +11465,15 @@ void GameScreen::renderAchievementToast() {
|
|||
draw->AddText(font, titleSize, ImVec2(titleX, toastY + 8),
|
||||
IM_COL32(255, 215, 0, (int)(alpha * 255)), title);
|
||||
|
||||
// Achievement ID line (until we have Achievement.dbc name lookup)
|
||||
char idBuf[64];
|
||||
std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_);
|
||||
// Achievement name (falls back to ID if name not available)
|
||||
char idBuf[256];
|
||||
const char* achText = achievementToastName_.empty()
|
||||
? nullptr : achievementToastName_.c_str();
|
||||
if (achText) {
|
||||
std::snprintf(idBuf, sizeof(idBuf), "%s", achText);
|
||||
} else {
|
||||
std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_);
|
||||
}
|
||||
float idW = font->CalcTextSizeA(bodySize, FLT_MAX, 0.0f, idBuf).x;
|
||||
float idX = toastX + (TOAST_W - idW) * 0.5f;
|
||||
draw->AddText(font, bodySize, ImVec2(idX, toastY + 28),
|
||||
|
|
|
|||
|
|
@ -1086,7 +1086,10 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
|
||||
if (ImGui::BeginTabItem("Stats")) {
|
||||
ImGui::Spacing();
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating());
|
||||
int32_t stats[5];
|
||||
for (int i = 0; i < 5; ++i) stats[i] = gameHandler.getPlayerStat(i);
|
||||
const int32_t* serverStats = (stats[0] >= 0) ? stats : nullptr;
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating(), serverStats);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
|
|
@ -1376,18 +1379,18 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
// Stats Panel
|
||||
// ============================================================
|
||||
|
||||
void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor) {
|
||||
// Sum equipment stats
|
||||
int32_t totalStr = 0, totalAgi = 0, totalSta = 0, totalInt = 0, totalSpi = 0;
|
||||
|
||||
void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel,
|
||||
int32_t serverArmor, const int32_t* serverStats) {
|
||||
// Sum equipment stats for item-query bonus display
|
||||
int32_t itemStr = 0, itemAgi = 0, itemSta = 0, itemInt = 0, itemSpi = 0;
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (slot.empty()) continue;
|
||||
totalStr += slot.item.strength;
|
||||
totalAgi += slot.item.agility;
|
||||
totalSta += slot.item.stamina;
|
||||
totalInt += slot.item.intellect;
|
||||
totalSpi += slot.item.spirit;
|
||||
itemStr += slot.item.strength;
|
||||
itemAgi += slot.item.agility;
|
||||
itemSta += slot.item.stamina;
|
||||
itemInt += slot.item.intellect;
|
||||
itemSpi += slot.item.spirit;
|
||||
}
|
||||
|
||||
// Use server-authoritative armor from UNIT_FIELD_RESISTANCES when available.
|
||||
|
|
@ -1399,9 +1402,6 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
}
|
||||
int32_t totalArmor = (serverArmor > 0) ? serverArmor : itemQueryArmor;
|
||||
|
||||
// Base stats: 20 + level
|
||||
int32_t baseStat = 20 + static_cast<int32_t>(playerLevel);
|
||||
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
ImVec4 white(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
ImVec4 gold(1.0f, 0.84f, 0.0f, 1.0f);
|
||||
|
|
@ -1414,23 +1414,41 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
ImGui::TextColored(gray, "Armor: 0");
|
||||
}
|
||||
|
||||
// Helper to render a stat line
|
||||
auto renderStat = [&](const char* name, int32_t equipBonus) {
|
||||
int32_t total = baseStat + equipBonus;
|
||||
if (equipBonus > 0) {
|
||||
ImGui::TextColored(white, "%s: %d", name, total);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(green, "(+%d)", equipBonus);
|
||||
} else {
|
||||
ImGui::TextColored(gray, "%s: %d", name, total);
|
||||
if (serverStats) {
|
||||
// Server-authoritative stats from UNIT_FIELD_STAT0-4: show total and item bonus.
|
||||
// serverStats[i] is the server's effective base stat (items included, buffs excluded).
|
||||
const char* statNames[5] = {"Strength", "Agility", "Stamina", "Intellect", "Spirit"};
|
||||
const int32_t itemBonuses[5] = {itemStr, itemAgi, itemSta, itemInt, itemSpi};
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
int32_t total = serverStats[i];
|
||||
int32_t bonus = itemBonuses[i];
|
||||
if (bonus > 0) {
|
||||
ImGui::TextColored(white, "%s: %d", statNames[i], total);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(green, "(+%d)", bonus);
|
||||
} else {
|
||||
ImGui::TextColored(gray, "%s: %d", statNames[i], total);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderStat("Strength", totalStr);
|
||||
renderStat("Agility", totalAgi);
|
||||
renderStat("Stamina", totalSta);
|
||||
renderStat("Intellect", totalInt);
|
||||
renderStat("Spirit", totalSpi);
|
||||
} else {
|
||||
// Fallback: estimated base (20 + level) plus item query bonuses.
|
||||
int32_t baseStat = 20 + static_cast<int32_t>(playerLevel);
|
||||
auto renderStat = [&](const char* name, int32_t equipBonus) {
|
||||
int32_t total = baseStat + equipBonus;
|
||||
if (equipBonus > 0) {
|
||||
ImGui::TextColored(white, "%s: %d", name, total);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(green, "(+%d)", equipBonus);
|
||||
} else {
|
||||
ImGui::TextColored(gray, "%s: %d", name, total);
|
||||
}
|
||||
};
|
||||
renderStat("Strength", itemStr);
|
||||
renderStat("Agility", itemAgi);
|
||||
renderStat("Stamina", itemSta);
|
||||
renderStat("Intellect", itemInt);
|
||||
renderStat("Spirit", itemSpi);
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections) {
|
||||
|
|
@ -1704,7 +1722,9 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
}
|
||||
|
||||
if (ImGui::IsItemHovered() && !holdingItem) {
|
||||
renderItemTooltip(item, &inventory);
|
||||
// Pass inventory for backpack/bag items only; equipped items compare against themselves otherwise
|
||||
const game::Inventory* tooltipInv = (kind == SlotKind::EQUIPMENT) ? nullptr : &inventory;
|
||||
renderItemTooltip(item, tooltipInv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1880,7 +1900,10 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
}
|
||||
|
||||
if (item.requiredLevel > 1) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Requires Level %u", item.requiredLevel);
|
||||
uint32_t playerLvl = gameHandler_ ? gameHandler_->getPlayerLevel() : 0;
|
||||
bool meetsReq = (playerLvl >= item.requiredLevel);
|
||||
ImVec4 reqColor = meetsReq ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
||||
ImGui::TextColored(reqColor, "Requires Level %u", item.requiredLevel);
|
||||
}
|
||||
if (item.maxDurability > 0) {
|
||||
float durPct = static_cast<float>(item.curDurability) / static_cast<float>(item.maxDurability);
|
||||
|
|
@ -1947,6 +1970,22 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
}
|
||||
ImGui::TextColored(getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
|
||||
|
||||
// Item level comparison (always shown when different)
|
||||
if (eq->item.itemLevel > 0 || item.itemLevel > 0) {
|
||||
char ilvlBuf[64];
|
||||
float diff = static_cast<float>(item.itemLevel) - static_cast<float>(eq->item.itemLevel);
|
||||
if (diff > 0.0f)
|
||||
std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▲%.0f)", item.itemLevel, diff);
|
||||
else if (diff < 0.0f)
|
||||
std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▼%.0f)", item.itemLevel, -diff);
|
||||
else
|
||||
std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (=)", item.itemLevel);
|
||||
ImVec4 ilvlColor = (diff > 0.0f) ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f)
|
||||
: (diff < 0.0f) ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f)
|
||||
: ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
ImGui::TextColored(ilvlColor, "%s", ilvlBuf);
|
||||
}
|
||||
|
||||
// Helper: render a numeric stat diff line
|
||||
auto showDiff = [](const char* label, float newVal, float eqVal) {
|
||||
if (newVal == 0.0f && eqVal == 0.0f) return;
|
||||
|
|
@ -1959,7 +1998,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
std::snprintf(buf, sizeof(buf), "%s: %.0f (▼%.0f)", label, newVal, -diff);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", buf);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "%s: %.0f", label, newVal);
|
||||
std::snprintf(buf, sizeof(buf), "%s: %.0f (=)", label, newVal);
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", buf);
|
||||
}
|
||||
};
|
||||
|
|
@ -2027,5 +2066,170 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tooltip overload for ItemQueryResponseData (used by loot window, etc.)
|
||||
// ---------------------------------------------------------------------------
|
||||
void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) {
|
||||
ImGui::BeginTooltip();
|
||||
|
||||
ImVec4 qColor = getQualityColor(static_cast<game::ItemQuality>(info.quality));
|
||||
ImGui::TextColored(qColor, "%s", info.name.c_str());
|
||||
if (info.itemLevel > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", info.itemLevel);
|
||||
}
|
||||
|
||||
// Binding type
|
||||
switch (info.bindType) {
|
||||
case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break;
|
||||
case 2: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when equipped"); break;
|
||||
case 3: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when used"); break;
|
||||
case 4: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Quest Item"); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Slot / subclass
|
||||
if (info.inventoryType > 0) {
|
||||
const char* slotName = "";
|
||||
switch (info.inventoryType) {
|
||||
case 1: slotName = "Head"; break;
|
||||
case 2: slotName = "Neck"; break;
|
||||
case 3: slotName = "Shoulder"; break;
|
||||
case 4: slotName = "Shirt"; break;
|
||||
case 5: slotName = "Chest"; break;
|
||||
case 6: slotName = "Waist"; break;
|
||||
case 7: slotName = "Legs"; break;
|
||||
case 8: slotName = "Feet"; break;
|
||||
case 9: slotName = "Wrist"; break;
|
||||
case 10: slotName = "Hands"; break;
|
||||
case 11: slotName = "Finger"; break;
|
||||
case 12: slotName = "Trinket"; break;
|
||||
case 13: slotName = "One-Hand"; break;
|
||||
case 14: slotName = "Shield"; break;
|
||||
case 15: slotName = "Ranged"; break;
|
||||
case 16: slotName = "Back"; break;
|
||||
case 17: slotName = "Two-Hand"; break;
|
||||
case 18: slotName = "Bag"; break;
|
||||
case 19: slotName = "Tabard"; break;
|
||||
case 20: slotName = "Robe"; break;
|
||||
case 21: slotName = "Main Hand"; break;
|
||||
case 22: slotName = "Off Hand"; break;
|
||||
case 23: slotName = "Held In Off-hand"; break;
|
||||
case 25: slotName = "Thrown"; break;
|
||||
case 26: slotName = "Ranged"; break;
|
||||
default: break;
|
||||
}
|
||||
if (slotName[0]) {
|
||||
if (!info.subclassName.empty())
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, info.subclassName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon stats
|
||||
auto isWeaponInvType = [](uint32_t t) {
|
||||
return t == 13 || t == 15 || t == 17 || t == 21 || t == 25 || t == 26;
|
||||
};
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
if (isWeaponInvType(info.inventoryType) && info.damageMax > 0.0f && info.delayMs > 0) {
|
||||
float speed = static_cast<float>(info.delayMs) / 1000.0f;
|
||||
float dps = ((info.damageMin + info.damageMax) * 0.5f) / speed;
|
||||
ImGui::Text("%.0f - %.0f Damage", info.damageMin, info.damageMax);
|
||||
ImGui::SameLine(160.0f);
|
||||
ImGui::TextDisabled("Speed %.2f", speed);
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(%.1f damage per second)", dps);
|
||||
}
|
||||
|
||||
if (info.armor > 0) ImGui::Text("%d Armor", info.armor);
|
||||
|
||||
auto appendBonus = [](std::string& out, int32_t val, const char* name) {
|
||||
if (val <= 0) return;
|
||||
if (!out.empty()) out += " ";
|
||||
out += "+" + std::to_string(val) + " " + name;
|
||||
};
|
||||
std::string bonusLine;
|
||||
appendBonus(bonusLine, info.strength, "Str");
|
||||
appendBonus(bonusLine, info.agility, "Agi");
|
||||
appendBonus(bonusLine, info.stamina, "Sta");
|
||||
appendBonus(bonusLine, info.intellect, "Int");
|
||||
appendBonus(bonusLine, info.spirit, "Spi");
|
||||
if (!bonusLine.empty()) ImGui::TextColored(green, "%s", bonusLine.c_str());
|
||||
|
||||
// Extra stats
|
||||
for (const auto& es : info.extraStats) {
|
||||
const char* statName = nullptr;
|
||||
switch (es.statType) {
|
||||
case 12: statName = "Defense Rating"; break;
|
||||
case 13: statName = "Dodge Rating"; break;
|
||||
case 14: statName = "Parry Rating"; break;
|
||||
case 16: case 17: case 18: case 31: statName = "Hit Rating"; break;
|
||||
case 19: case 20: case 21: case 32: statName = "Crit Rating"; break;
|
||||
case 28: case 29: case 30: case 36: statName = "Haste Rating"; break;
|
||||
case 35: statName = "Resilience"; break;
|
||||
case 37: statName = "Expertise Rating"; break;
|
||||
case 38: statName = "Attack Power"; break;
|
||||
case 39: statName = "Ranged Attack Power"; break;
|
||||
case 41: statName = "Healing Power"; break;
|
||||
case 42: statName = "Spell Damage"; break;
|
||||
case 43: statName = "Mana per 5 sec"; break;
|
||||
case 44: statName = "Armor Penetration"; break;
|
||||
case 45: statName = "Spell Power"; break;
|
||||
case 46: statName = "Health per 5 sec"; break;
|
||||
case 47: statName = "Spell Penetration"; break;
|
||||
case 48: statName = "Block Value"; break;
|
||||
default: statName = nullptr; break;
|
||||
}
|
||||
char buf[64];
|
||||
if (statName)
|
||||
std::snprintf(buf, sizeof(buf), "%+d %s", es.statValue, statName);
|
||||
else
|
||||
std::snprintf(buf, sizeof(buf), "%+d (stat %u)", es.statValue, es.statType);
|
||||
ImGui::TextColored(green, "%s", buf);
|
||||
}
|
||||
|
||||
if (info.requiredLevel > 1) {
|
||||
uint32_t playerLvl = gameHandler_ ? gameHandler_->getPlayerLevel() : 0;
|
||||
bool meetsReq = (playerLvl >= info.requiredLevel);
|
||||
ImVec4 reqColor = meetsReq ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
||||
ImGui::TextColored(reqColor, "Requires Level %u", info.requiredLevel);
|
||||
}
|
||||
|
||||
// Spell effects
|
||||
for (const auto& sp : info.spells) {
|
||||
if (sp.spellId == 0) continue;
|
||||
const char* trigger = nullptr;
|
||||
switch (sp.spellTrigger) {
|
||||
case 0: trigger = "Use"; break;
|
||||
case 1: trigger = "Equip"; break;
|
||||
case 2: trigger = "Chance on Hit"; break;
|
||||
default: break;
|
||||
}
|
||||
if (!trigger) continue;
|
||||
if (gameHandler_) {
|
||||
const std::string& spName = gameHandler_->getSpellName(sp.spellId);
|
||||
if (!spName.empty())
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "%s: %s", trigger, spName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "%s: Spell #%u", trigger, sp.spellId);
|
||||
}
|
||||
}
|
||||
|
||||
if (info.startQuestId != 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Begins a Quest");
|
||||
}
|
||||
if (!info.description.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 0.9f), "\"%s\"", info.description.c_str());
|
||||
}
|
||||
|
||||
if (info.sellPrice > 0) {
|
||||
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::EndTooltip();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -379,14 +379,24 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
|||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tracked Progress");
|
||||
for (const auto& [entry, progress] : sel.killCounts) {
|
||||
ImGui::BulletText("Kill %u: %u/%u", entry, progress.first, progress.second);
|
||||
std::string name = gameHandler.getCachedCreatureName(entry);
|
||||
if (name.empty()) {
|
||||
// Game object objective: fall back to GO name cache.
|
||||
const auto* goInfo = gameHandler.getCachedGameObjectInfo(entry);
|
||||
if (goInfo && !goInfo->name.empty()) name = goInfo->name;
|
||||
}
|
||||
if (name.empty()) name = "Unknown (" + std::to_string(entry) + ")";
|
||||
ImGui::BulletText("%s: %u/%u", name.c_str(), progress.first, progress.second);
|
||||
}
|
||||
for (const auto& [itemId, count] : sel.itemCounts) {
|
||||
std::string itemLabel = "Item " + std::to_string(itemId);
|
||||
if (const auto* info = gameHandler.getItemInfo(itemId)) {
|
||||
if (!info->name.empty()) itemLabel = info->name;
|
||||
}
|
||||
ImGui::BulletText("%s: %u", itemLabel.c_str(), count);
|
||||
uint32_t required = 1;
|
||||
auto reqIt = sel.requiredItemCounts.find(itemId);
|
||||
if (reqIt != sel.requiredItemCounts.end()) required = reqIt->second;
|
||||
ImGui::BulletText("%s: %u/%u", itemLabel.c_str(), count, required);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ bool SpellbookScreen::renderSpellInfoTooltip(uint32_t spellId, game::GameHandler
|
|||
if (!dbcLoadAttempted) loadSpellDBC(assetManager);
|
||||
const SpellInfo* info = getSpellInfo(spellId);
|
||||
if (!info) return false;
|
||||
renderSpellTooltip(info, gameHandler);
|
||||
renderSpellTooltip(info, gameHandler, /*showUsageHints=*/false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +446,7 @@ const SpellInfo* SpellbookScreen::getSpellInfo(uint32_t spellId) const {
|
|||
return (it != spellData.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler) {
|
||||
void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler, bool showUsageHints) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(320.0f);
|
||||
|
||||
|
|
@ -551,8 +551,8 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle
|
|||
ImGui::TextWrapped("%s", info->description.c_str());
|
||||
}
|
||||
|
||||
// Usage hints
|
||||
if (!info->isPassive()) {
|
||||
// Usage hints — only shown when browsing the spellbook, not on action bar hover
|
||||
if (!info->isPassive() && showUsageHints) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Drag to action bar");
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Double-click to cast");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue