mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
90 commits
8f08d75748
...
e51b215f85
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e51b215f85 | ||
|
|
ffef3dda7e | ||
|
|
38111fe8c0 | ||
|
|
d48ead939b | ||
|
|
0982f557d2 | ||
|
|
3bdd3f1d3f | ||
|
|
8213de1d0f | ||
|
|
d40e8f1618 | ||
|
|
a05abc8881 | ||
|
|
04768f41de | ||
|
|
84d5a1125f | ||
|
|
ef7494700e | ||
|
|
03f8642fad | ||
|
|
0741b4d9e3 | ||
|
|
6e8704c520 | ||
|
|
b0b47c354a | ||
|
|
a6c4f6d2e9 | ||
|
|
c48065473f | ||
|
|
6bca3dd6c5 | ||
|
|
fa9017c6dc | ||
|
|
605d046838 | ||
|
|
8da5e5c029 | ||
|
|
bbbc4efced | ||
|
|
cc24597983 | ||
|
|
b03c326bcd | ||
|
|
792d8e1cf5 | ||
|
|
c31ab8c8b6 | ||
|
|
44ff2dd4ee | ||
|
|
727dfa5c6c | ||
|
|
d58c2f4269 | ||
|
|
f5a834b543 | ||
|
|
01ec830555 | ||
|
|
fdd6ca30c3 | ||
|
|
736d266c7e | ||
|
|
dd412af093 | ||
|
|
850e4e798d | ||
|
|
84f9d2e493 | ||
|
|
a9f21b2820 | ||
|
|
3e85c78790 | ||
|
|
2b79f9d121 | ||
|
|
c0ffca68f2 | ||
|
|
ea7b276125 | ||
|
|
75139aca77 | ||
|
|
ed02f5872a | ||
|
|
59e29e2988 | ||
|
|
9fe2ef381c | ||
|
|
a9835f6873 | ||
|
|
8cac557f86 | ||
|
|
ee3c12b2c0 | ||
|
|
67e4497945 | ||
|
|
90c88d7ecd | ||
|
|
d2f2d6db72 | ||
|
|
0a41ef7285 | ||
|
|
2be793cfba | ||
|
|
2c72d8462d | ||
|
|
28ce441214 | ||
|
|
8e67a41983 | ||
|
|
20b59c9d63 | ||
|
|
cf88a960f4 | ||
|
|
9216a6da28 | ||
|
|
ecc02595de | ||
|
|
e882110e7f | ||
|
|
3a143b9b5b | ||
|
|
8a81ffa29c | ||
|
|
b1015abffe | ||
|
|
2a52aedbf7 | ||
|
|
156ddfad9a | ||
|
|
43b007cdcd | ||
|
|
d79c79e1bc | ||
|
|
38ab1e0aea | ||
|
|
3b499d6871 | ||
|
|
c58fc3073f | ||
|
|
d9b9d1d2f2 | ||
|
|
4507a223cc | ||
|
|
b9c16e9be5 | ||
|
|
dfe091473c | ||
|
|
2b131548aa | ||
|
|
b34df01331 | ||
|
|
7dc12bb35e | ||
|
|
2d587d0d4b | ||
|
|
3c704088af | ||
|
|
103bb5a513 | ||
|
|
48bcee32b4 | ||
|
|
f44defec38 | ||
|
|
01f4ef5e79 | ||
|
|
6878f120e9 | ||
|
|
7a4347dbac | ||
|
|
cc2b413e22 | ||
|
|
2c6902d27d | ||
|
|
4272491d56 |
21 changed files with 2451 additions and 382 deletions
|
|
@ -113,5 +113,8 @@
|
|||
"Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41,
|
||||
"Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45,
|
||||
"Threshold8": 46, "Threshold9": 47
|
||||
},
|
||||
"LFGDungeons": {
|
||||
"ID": 0, "Name": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
"UNIT_FIELD_STAT3": 87,
|
||||
"UNIT_FIELD_STAT4": 88,
|
||||
"UNIT_END": 148,
|
||||
"UNIT_FIELD_ATTACK_POWER": 123,
|
||||
"UNIT_FIELD_RANGED_ATTACK_POWER": 126,
|
||||
"PLAYER_FLAGS": 150,
|
||||
"PLAYER_BYTES": 153,
|
||||
"PLAYER_BYTES_2": 154,
|
||||
|
|
@ -38,6 +40,15 @@
|
|||
"PLAYER_SKILL_INFO_START": 636,
|
||||
"PLAYER_EXPLORED_ZONES_START": 1041,
|
||||
"PLAYER_CHOSEN_TITLE": 1349,
|
||||
"PLAYER_FIELD_MOD_DAMAGE_DONE_POS": 1171,
|
||||
"PLAYER_FIELD_MOD_HEALING_DONE_POS": 1192,
|
||||
"PLAYER_BLOCK_PERCENTAGE": 1024,
|
||||
"PLAYER_DODGE_PERCENTAGE": 1025,
|
||||
"PLAYER_PARRY_PERCENTAGE": 1026,
|
||||
"PLAYER_CRIT_PERCENTAGE": 1029,
|
||||
"PLAYER_RANGED_CRIT_PERCENTAGE": 1030,
|
||||
"PLAYER_SPELL_CRIT_PERCENTAGE1": 1032,
|
||||
"PLAYER_FIELD_COMBAT_RATING_1": 1231,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14,
|
||||
"ITEM_FIELD_DURABILITY": 60,
|
||||
|
|
|
|||
|
|
@ -38,8 +38,11 @@ namespace game {
|
|||
|
||||
struct PlayerSkill {
|
||||
uint32_t skillId = 0;
|
||||
uint16_t value = 0;
|
||||
uint16_t value = 0; // base + permanent item bonuses
|
||||
uint16_t maxValue = 0;
|
||||
uint16_t bonusTemp = 0; // temporary buff bonus (food, potions, etc.)
|
||||
uint16_t bonusPerm = 0; // permanent spec/misc bonus (rarely non-zero)
|
||||
uint16_t effectiveValue() const { return value + bonusTemp + bonusPerm; }
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -218,6 +221,7 @@ public:
|
|||
pos = homeBindPos_;
|
||||
return true;
|
||||
}
|
||||
uint32_t getHomeBindZoneId() const { return homeBindZoneId_; }
|
||||
|
||||
/**
|
||||
* Send a movement packet
|
||||
|
|
@ -283,6 +287,7 @@ public:
|
|||
* @return Vector of chat messages
|
||||
*/
|
||||
const std::deque<MessageChatData>& getChatHistory() const { return chatHistory; }
|
||||
void clearChatHistory() { chatHistory.clear(); }
|
||||
|
||||
/**
|
||||
* Add a locally-generated chat message (e.g., emote feedback)
|
||||
|
|
@ -309,6 +314,43 @@ public:
|
|||
return playerStats_[idx];
|
||||
}
|
||||
|
||||
// Server-authoritative attack power (WotLK: UNIT_FIELD_ATTACK_POWER / RANGED).
|
||||
// Returns -1 if not yet received.
|
||||
int32_t getMeleeAttackPower() const { return playerMeleeAP_; }
|
||||
int32_t getRangedAttackPower() const { return playerRangedAP_; }
|
||||
|
||||
// Server-authoritative spell damage / healing bonus (WotLK: PLAYER_FIELD_MOD_*).
|
||||
// getSpellPower returns the max damage bonus across magic schools 1-6 (Holy/Fire/Nature/Frost/Shadow/Arcane).
|
||||
// Returns -1 if not yet received.
|
||||
int32_t getSpellPower() const {
|
||||
int32_t sp = -1;
|
||||
for (int i = 1; i <= 6; ++i) {
|
||||
if (playerSpellDmgBonus_[i] > sp) sp = playerSpellDmgBonus_[i];
|
||||
}
|
||||
return sp;
|
||||
}
|
||||
int32_t getHealingPower() const { return playerHealBonus_; }
|
||||
|
||||
// Server-authoritative combat chance percentages (WotLK: PLAYER_* float fields).
|
||||
// Returns -1.0f if not yet received.
|
||||
float getDodgePct() const { return playerDodgePct_; }
|
||||
float getParryPct() const { return playerParryPct_; }
|
||||
float getBlockPct() const { return playerBlockPct_; }
|
||||
float getCritPct() const { return playerCritPct_; }
|
||||
float getRangedCritPct() const { return playerRangedCritPct_; }
|
||||
// Spell crit by school (0=Physical,1=Holy,2=Fire,3=Nature,4=Frost,5=Shadow,6=Arcane)
|
||||
float getSpellCritPct(int school = 1) const {
|
||||
if (school < 0 || school > 6) return -1.0f;
|
||||
return playerSpellCritPct_[school];
|
||||
}
|
||||
|
||||
// Server-authoritative combat ratings (WotLK: PLAYER_FIELD_COMBAT_RATING_1+idx).
|
||||
// Returns -1 if not yet received. Indices match AzerothCore CombatRating enum.
|
||||
int32_t getCombatRating(int cr) const {
|
||||
if (cr < 0 || cr > 24) return -1;
|
||||
return playerCombatRatings_[cr];
|
||||
}
|
||||
|
||||
// Inventory
|
||||
Inventory& getInventory() { return inventory; }
|
||||
const Inventory& getInventory() const { return inventory; }
|
||||
|
|
@ -402,6 +444,8 @@ public:
|
|||
uint8_t arenaType = 0;
|
||||
uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress
|
||||
uint32_t inviteTimeout = 80;
|
||||
uint32_t avgWaitTimeSec = 0; // server-estimated average wait (STATUS_WAIT_QUEUE)
|
||||
uint32_t timeInQueueSec = 0; // time already spent in queue (STATUS_WAIT_QUEUE)
|
||||
std::chrono::steady_clock::time_point inviteReceivedTime{};
|
||||
};
|
||||
|
||||
|
|
@ -457,6 +501,8 @@ public:
|
|||
// Logout commands
|
||||
void requestLogout();
|
||||
void cancelLogout();
|
||||
bool isLoggingOut() const { return loggingOut_; }
|
||||
float getLogoutCountdown() const { return logoutCountdown_; }
|
||||
|
||||
// Stand state
|
||||
void setStandState(uint8_t state); // 0=stand, 1=sit, 2=sit_chair, 3=sleep, 4=sit_low_chair, 5=sit_medium_chair, 6=sit_high_chair, 7=dead, 8=kneel, 9=submerged
|
||||
|
|
@ -1089,6 +1135,10 @@ public:
|
|||
const Character* ch = getActiveCharacter();
|
||||
return ch ? static_cast<uint8_t>(ch->characterClass) : 0;
|
||||
}
|
||||
uint8_t getPlayerRace() const {
|
||||
const Character* ch = getActiveCharacter();
|
||||
return ch ? static_cast<uint8_t>(ch->race) : 0;
|
||||
}
|
||||
void setPlayerGuid(uint64_t guid) { playerGuid = guid; }
|
||||
|
||||
// Player death state
|
||||
|
|
@ -1274,6 +1324,8 @@ public:
|
|||
bool isLfgQueued() const { return lfgState_ == LfgState::Queued; }
|
||||
bool isLfgInDungeon() const { return lfgState_ == LfgState::InDungeon; }
|
||||
uint32_t getLfgDungeonId() const { return lfgDungeonId_; }
|
||||
std::string getCurrentLfgDungeonName() const { return getLfgDungeonName(lfgDungeonId_); }
|
||||
std::string getMapName(uint32_t mapId) const;
|
||||
uint32_t getLfgProposalId() const { return lfgProposalId_; }
|
||||
int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; }
|
||||
uint32_t getLfgTimeInQueueMs() const { return lfgTimeInQueueMs_; }
|
||||
|
|
@ -1833,6 +1885,7 @@ public:
|
|||
bool isTaxiMountActive() const { return taxiMountActive_; }
|
||||
bool isTaxiActivationPending() const { return taxiActivatePending_; }
|
||||
void forceClearTaxiAndMovementState();
|
||||
const std::string& getTaxiDestName() const { return taxiDestName_; }
|
||||
const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; }
|
||||
uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; }
|
||||
|
||||
|
|
@ -2314,7 +2367,8 @@ private:
|
|||
void handleLogoutResponse(network::Packet& packet);
|
||||
void handleLogoutComplete(network::Packet& packet);
|
||||
|
||||
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource);
|
||||
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType = 0,
|
||||
uint64_t srcGuid = 0, uint64_t dstGuid = 0);
|
||||
void addSystemChatMessage(const std::string& message);
|
||||
|
||||
/**
|
||||
|
|
@ -2421,6 +2475,7 @@ private:
|
|||
uint32_t currentMapId_ = 0;
|
||||
bool hasHomeBind_ = false;
|
||||
uint32_t homeBindMapId_ = 0;
|
||||
uint32_t homeBindZoneId_ = 0;
|
||||
glm::vec3 homeBindPos_{0.0f};
|
||||
|
||||
// ---- Phase 1: Name caches ----
|
||||
|
|
@ -2448,7 +2503,8 @@ private:
|
|||
std::unordered_map<std::string, uint64_t> ignoreCache; // name -> guid
|
||||
|
||||
// ---- Logout state ----
|
||||
bool loggingOut_ = false;
|
||||
bool loggingOut_ = false;
|
||||
float logoutCountdown_ = 0.0f; // seconds remaining before server logs us out (0 = instant/done)
|
||||
|
||||
// ---- Display state ----
|
||||
bool helmVisible_ = true;
|
||||
|
|
@ -2778,6 +2834,9 @@ private:
|
|||
float timer = 0.0f;
|
||||
};
|
||||
std::vector<PendingLootOpen> pendingGameObjectLootOpens_;
|
||||
// Tracks the last GO we sent CMSG_GAMEOBJ_USE to; used in handleSpellGo
|
||||
// to send CMSG_LOOT after a gather cast (mining/herbalism) completes.
|
||||
uint64_t lastInteractedGoGuid_ = 0;
|
||||
uint64_t pendingLootMoneyGuid_ = 0;
|
||||
uint32_t pendingLootMoneyAmount_ = 0;
|
||||
float pendingLootMoneyNotifyTimer_ = 0.0f;
|
||||
|
|
@ -2787,6 +2846,18 @@ private:
|
|||
int32_t playerResistances_[6] = {}; // [0]=Holy,[1]=Fire,[2]=Nature,[3]=Frost,[4]=Shadow,[5]=Arcane
|
||||
// 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};
|
||||
// WotLK secondary combat stats (-1 = not yet received)
|
||||
int32_t playerMeleeAP_ = -1;
|
||||
int32_t playerRangedAP_ = -1;
|
||||
int32_t playerSpellDmgBonus_[7] = {-1,-1,-1,-1,-1,-1,-1}; // per school 0-6
|
||||
int32_t playerHealBonus_ = -1;
|
||||
float playerDodgePct_ = -1.0f;
|
||||
float playerParryPct_ = -1.0f;
|
||||
float playerBlockPct_ = -1.0f;
|
||||
float playerCritPct_ = -1.0f;
|
||||
float playerRangedCritPct_ = -1.0f;
|
||||
float playerSpellCritPct_[7] = {-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f};
|
||||
int32_t playerCombatRatings_[25] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-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;
|
||||
|
|
@ -2845,6 +2916,7 @@ private:
|
|||
ShowTaxiNodesData currentTaxiData_;
|
||||
uint64_t taxiNpcGuid_ = 0;
|
||||
bool onTaxiFlight_ = false;
|
||||
std::string taxiDestName_;
|
||||
bool taxiMountActive_ = false;
|
||||
uint32_t taxiMountDisplayId_ = 0;
|
||||
bool taxiActivatePending_ = false;
|
||||
|
|
@ -2968,6 +3040,17 @@ private:
|
|||
bool areaNameCacheLoaded_ = false;
|
||||
void loadAreaNameCache();
|
||||
std::string getAreaName(uint32_t areaId) const;
|
||||
|
||||
// Map name cache (lazy-loaded from Map.dbc; maps mapId → localized display name)
|
||||
std::unordered_map<uint32_t, std::string> mapNameCache_;
|
||||
bool mapNameCacheLoaded_ = false;
|
||||
void loadMapNameCache();
|
||||
|
||||
// LFG dungeon name cache (lazy-loaded from LFGDungeons.dbc; WotLK only)
|
||||
std::unordered_map<uint32_t, std::string> lfgDungeonNameCache_;
|
||||
bool lfgDungeonNameCacheLoaded_ = false;
|
||||
void loadLfgDungeonDbc();
|
||||
std::string getLfgDungeonName(uint32_t dungeonId) const;
|
||||
std::vector<TrainerTab> trainerTabs_;
|
||||
void handleTrainerList(network::Packet& packet);
|
||||
void loadSpellNameCache();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ enum class ItemQuality : uint8_t {
|
|||
RARE = 3, // Blue
|
||||
EPIC = 4, // Purple
|
||||
LEGENDARY = 5, // Orange
|
||||
ARTIFACT = 6, // Yellow (unused in 3.3.5a but valid quality value)
|
||||
HEIRLOOM = 7, // Yellow/gold (WotLK bind-on-account heirlooms)
|
||||
};
|
||||
|
||||
enum class EquipSlot : uint8_t {
|
||||
|
|
|
|||
|
|
@ -52,13 +52,15 @@ struct CombatTextEntry {
|
|||
enum Type : uint8_t {
|
||||
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
||||
CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
|
||||
ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER
|
||||
ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER,
|
||||
DISPEL, INTERRUPT
|
||||
};
|
||||
Type type;
|
||||
int32_t amount = 0;
|
||||
uint32_t spellId = 0;
|
||||
float age = 0.0f; // Seconds since creation (for fadeout)
|
||||
bool isPlayerSource = false; // True if player dealt this
|
||||
uint8_t powerType = 0; // For ENERGIZE: 0=mana,1=rage,2=focus,3=energy,6=runicpower
|
||||
|
||||
static constexpr float LIFETIME = 2.5f;
|
||||
bool isExpired() const { return age >= LIFETIME; }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ enum class UF : uint16_t {
|
|||
UNIT_FIELD_STAT4, // Spirit
|
||||
UNIT_END,
|
||||
|
||||
// Unit combat fields (WotLK: PRIVATE+OWNER — only visible for the player character)
|
||||
UNIT_FIELD_ATTACK_POWER, // Melee attack power (int32)
|
||||
UNIT_FIELD_RANGED_ATTACK_POWER, // Ranged attack power (int32)
|
||||
|
||||
// Player fields
|
||||
PLAYER_FLAGS,
|
||||
PLAYER_BYTES,
|
||||
|
|
@ -59,6 +63,19 @@ enum class UF : uint16_t {
|
|||
PLAYER_EXPLORED_ZONES_START,
|
||||
PLAYER_CHOSEN_TITLE, // Active title index (-1 = no title)
|
||||
|
||||
// Player spell power / healing bonus (WotLK: PRIVATE — int32 per school)
|
||||
PLAYER_FIELD_MOD_DAMAGE_DONE_POS, // Spell damage bonus (first of 7 schools)
|
||||
PLAYER_FIELD_MOD_HEALING_DONE_POS, // Healing bonus
|
||||
|
||||
// Player combat stats (WotLK: PRIVATE — float values)
|
||||
PLAYER_BLOCK_PERCENTAGE, // Block chance %
|
||||
PLAYER_DODGE_PERCENTAGE, // Dodge chance %
|
||||
PLAYER_PARRY_PERCENTAGE, // Parry chance %
|
||||
PLAYER_CRIT_PERCENTAGE, // Melee crit chance %
|
||||
PLAYER_RANGED_CRIT_PERCENTAGE, // Ranged crit chance %
|
||||
PLAYER_SPELL_CRIT_PERCENTAGE1, // Spell crit chance % (first school; 7 consecutive float fields)
|
||||
PLAYER_FIELD_COMBAT_RATING_1, // First of 25 int32 combat rating slots (CR_* indices)
|
||||
|
||||
// GameObject fields
|
||||
GAMEOBJECT_DISPLAYID,
|
||||
|
||||
|
|
|
|||
|
|
@ -947,6 +947,21 @@ public:
|
|||
static network::Packet build(uint8_t state);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Action Bar
|
||||
// ============================================================
|
||||
|
||||
/** CMSG_SET_ACTION_BUTTON packet builder */
|
||||
class SetActionButtonPacket {
|
||||
public:
|
||||
// button: 0-based slot index
|
||||
// type: ActionBarSlot::Type (SPELL=0, ITEM=1, MACRO=2, EMPTY=0)
|
||||
// id: spellId, itemId, or macroId (0 to clear)
|
||||
// isClassic: true for Vanilla/Turtle format (5-byte payload),
|
||||
// false for TBC/WotLK (5-byte packed uint32)
|
||||
static network::Packet build(uint8_t button, uint8_t type, uint32_t id, bool isClassic);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Display Toggles
|
||||
// ============================================================
|
||||
|
|
@ -1572,13 +1587,21 @@ struct ItemQueryResponseData {
|
|||
uint32_t subClass = 0;
|
||||
uint32_t displayInfoId = 0;
|
||||
uint32_t quality = 0;
|
||||
uint32_t itemFlags = 0; // Item flag bitmask (Heroic=0x8, Unique-Equipped=0x1000000)
|
||||
uint32_t inventoryType = 0;
|
||||
int32_t maxCount = 0; // Max that can be carried (1 = Unique, 0 = unlimited)
|
||||
int32_t maxStack = 1;
|
||||
uint32_t containerSlots = 0;
|
||||
float damageMin = 0.0f;
|
||||
float damageMax = 0.0f;
|
||||
uint32_t delayMs = 0;
|
||||
int32_t armor = 0;
|
||||
int32_t holyRes = 0;
|
||||
int32_t fireRes = 0;
|
||||
int32_t natureRes = 0;
|
||||
int32_t frostRes = 0;
|
||||
int32_t shadowRes = 0;
|
||||
int32_t arcaneRes = 0;
|
||||
int32_t stamina = 0;
|
||||
int32_t strength = 0;
|
||||
int32_t agility = 0;
|
||||
|
|
@ -1604,6 +1627,13 @@ struct ItemQueryResponseData {
|
|||
std::array<uint32_t, 3> socketColor{};
|
||||
uint32_t socketBonus = 0; // enchantmentId of socket bonus; 0=none
|
||||
uint32_t itemSetId = 0; // ItemSet.dbc entry; 0=not part of a set
|
||||
// Requirement fields
|
||||
uint32_t requiredSkill = 0; // SkillLine.dbc ID (0 = no skill required)
|
||||
uint32_t requiredSkillRank = 0; // Minimum skill value
|
||||
uint32_t allowableClass = 0; // Class bitmask (0 = all classes)
|
||||
uint32_t allowableRace = 0; // Race bitmask (0 = all races)
|
||||
uint32_t requiredReputationFaction = 0; // Faction.dbc ID (0 = none)
|
||||
uint32_t requiredReputationRank = 0; // 0=Hated..8=Exalted
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class StarField;
|
|||
class Clouds;
|
||||
class LensFlare;
|
||||
class Weather;
|
||||
class Lightning;
|
||||
class LightingManager;
|
||||
class SwimEffects;
|
||||
class MountDust;
|
||||
|
|
@ -127,6 +128,7 @@ public:
|
|||
Clouds* getClouds() const { return skySystem ? skySystem->getClouds() : nullptr; }
|
||||
LensFlare* getLensFlare() const { return skySystem ? skySystem->getLensFlare() : nullptr; }
|
||||
Weather* getWeather() const { return weather.get(); }
|
||||
Lightning* getLightning() const { return lightning.get(); }
|
||||
CharacterRenderer* getCharacterRenderer() const { return characterRenderer.get(); }
|
||||
WMORenderer* getWMORenderer() const { return wmoRenderer.get(); }
|
||||
M2Renderer* getM2Renderer() const { return m2Renderer.get(); }
|
||||
|
|
@ -216,6 +218,7 @@ private:
|
|||
std::unique_ptr<Clouds> clouds;
|
||||
std::unique_ptr<LensFlare> lensFlare;
|
||||
std::unique_ptr<Weather> weather;
|
||||
std::unique_ptr<Lightning> lightning;
|
||||
std::unique_ptr<LightingManager> lightingManager;
|
||||
std::unique_ptr<SkySystem> skySystem; // Coordinator for sky rendering
|
||||
std::unique_ptr<SwimEffects> swimEffects;
|
||||
|
|
|
|||
|
|
@ -348,6 +348,7 @@ private:
|
|||
void renderTrainerWindow(game::GameHandler& gameHandler);
|
||||
void renderStableWindow(game::GameHandler& gameHandler);
|
||||
void renderTaxiWindow(game::GameHandler& gameHandler);
|
||||
void renderLogoutCountdown(game::GameHandler& gameHandler);
|
||||
void renderDeathScreen(game::GameHandler& gameHandler);
|
||||
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
||||
void renderResurrectDialog(game::GameHandler& gameHandler);
|
||||
|
|
|
|||
|
|
@ -149,7 +149,8 @@ private:
|
|||
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,
|
||||
const int32_t* serverStats = nullptr, const int32_t* serverResists = nullptr);
|
||||
const int32_t* serverStats = nullptr, const int32_t* serverResists = nullptr,
|
||||
const game::GameHandler* gh = nullptr);
|
||||
void renderReputationPanel(game::GameHandler& gameHandler);
|
||||
|
||||
void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -313,6 +313,8 @@ const char* getQualityName(ItemQuality quality) {
|
|||
case ItemQuality::RARE: return "Rare";
|
||||
case ItemQuality::EPIC: return "Epic";
|
||||
case ItemQuality::LEGENDARY: return "Legendary";
|
||||
case ItemQuality::ARTIFACT: return "Artifact";
|
||||
case ItemQuality::HEIRLOOM: return "Heirloom";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1381,7 +1381,7 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // Flags
|
||||
data.itemFlags = packet.readUInt32(); // Flags
|
||||
// Vanilla: NO Flags2
|
||||
packet.readUInt32(); // BuyPrice
|
||||
data.sellPrice = packet.readUInt32(); // SellPrice
|
||||
|
|
@ -1394,18 +1394,18 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // AllowableClass
|
||||
packet.readUInt32(); // AllowableRace
|
||||
data.allowableClass = packet.readUInt32(); // AllowableClass
|
||||
data.allowableRace = packet.readUInt32(); // AllowableRace
|
||||
data.itemLevel = packet.readUInt32();
|
||||
data.requiredLevel = packet.readUInt32();
|
||||
packet.readUInt32(); // RequiredSkill
|
||||
packet.readUInt32(); // RequiredSkillRank
|
||||
data.requiredSkill = packet.readUInt32(); // RequiredSkill
|
||||
data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank
|
||||
packet.readUInt32(); // RequiredSpell
|
||||
packet.readUInt32(); // RequiredHonorRank
|
||||
packet.readUInt32(); // RequiredCityRank
|
||||
packet.readUInt32(); // RequiredReputationFaction
|
||||
packet.readUInt32(); // RequiredReputationRank
|
||||
packet.readUInt32(); // MaxCount
|
||||
data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction
|
||||
data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank
|
||||
data.maxCount = static_cast<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
|
||||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
|
|
@ -1468,12 +1468,12 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
|
||||
// Remaining tail can vary by core. Read resistances + delay when present.
|
||||
if (packet.getSize() - packet.getReadPos() >= 28) {
|
||||
packet.readUInt32(); // HolyRes
|
||||
packet.readUInt32(); // FireRes
|
||||
packet.readUInt32(); // NatureRes
|
||||
packet.readUInt32(); // FrostRes
|
||||
packet.readUInt32(); // ShadowRes
|
||||
packet.readUInt32(); // ArcaneRes
|
||||
data.holyRes = static_cast<int32_t>(packet.readUInt32()); // HolyRes
|
||||
data.fireRes = static_cast<int32_t>(packet.readUInt32()); // FireRes
|
||||
data.natureRes = static_cast<int32_t>(packet.readUInt32()); // NatureRes
|
||||
data.frostRes = static_cast<int32_t>(packet.readUInt32()); // FrostRes
|
||||
data.shadowRes = static_cast<int32_t>(packet.readUInt32()); // ShadowRes
|
||||
data.arcaneRes = static_cast<int32_t>(packet.readUInt32()); // ArcaneRes
|
||||
data.delayMs = packet.readUInt32();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -998,7 +998,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // Flags (TBC: 1 flags field only — no Flags2)
|
||||
data.itemFlags = packet.readUInt32(); // Flags (TBC: 1 flags field only — no Flags2)
|
||||
// TBC: NO Flags2, NO BuyCount
|
||||
packet.readUInt32(); // BuyPrice
|
||||
data.sellPrice = packet.readUInt32();
|
||||
|
|
@ -1011,19 +1011,19 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // AllowableClass
|
||||
packet.readUInt32(); // AllowableRace
|
||||
data.allowableClass = packet.readUInt32(); // AllowableClass
|
||||
data.allowableRace = packet.readUInt32(); // AllowableRace
|
||||
data.itemLevel = packet.readUInt32();
|
||||
data.requiredLevel = packet.readUInt32();
|
||||
packet.readUInt32(); // RequiredSkill
|
||||
packet.readUInt32(); // RequiredSkillRank
|
||||
data.requiredSkill = packet.readUInt32(); // RequiredSkill
|
||||
data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank
|
||||
packet.readUInt32(); // RequiredSpell
|
||||
packet.readUInt32(); // RequiredHonorRank
|
||||
packet.readUInt32(); // RequiredCityRank
|
||||
packet.readUInt32(); // RequiredReputationFaction
|
||||
packet.readUInt32(); // RequiredReputationRank
|
||||
packet.readUInt32(); // MaxCount
|
||||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||||
data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction
|
||||
data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank
|
||||
data.maxCount = static_cast<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
|
||||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
// TBC: statsCount prefix + exactly statsCount pairs (WotLK always sends 10)
|
||||
|
|
@ -1087,12 +1087,12 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
|
||||
if (packet.getSize() - packet.getReadPos() >= 28) {
|
||||
packet.readUInt32(); // HolyRes
|
||||
packet.readUInt32(); // FireRes
|
||||
packet.readUInt32(); // NatureRes
|
||||
packet.readUInt32(); // FrostRes
|
||||
packet.readUInt32(); // ShadowRes
|
||||
packet.readUInt32(); // ArcaneRes
|
||||
data.holyRes = static_cast<int32_t>(packet.readUInt32()); // HolyRes
|
||||
data.fireRes = static_cast<int32_t>(packet.readUInt32()); // FireRes
|
||||
data.natureRes = static_cast<int32_t>(packet.readUInt32()); // NatureRes
|
||||
data.frostRes = static_cast<int32_t>(packet.readUInt32()); // FrostRes
|
||||
data.shadowRes = static_cast<int32_t>(packet.readUInt32()); // ShadowRes
|
||||
data.arcaneRes = static_cast<int32_t>(packet.readUInt32()); // ArcaneRes
|
||||
data.delayMs = packet.readUInt32();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"UNIT_FIELD_STAT3", UF::UNIT_FIELD_STAT3},
|
||||
{"UNIT_FIELD_STAT4", UF::UNIT_FIELD_STAT4},
|
||||
{"UNIT_END", UF::UNIT_END},
|
||||
{"UNIT_FIELD_ATTACK_POWER", UF::UNIT_FIELD_ATTACK_POWER},
|
||||
{"UNIT_FIELD_RANGED_ATTACK_POWER", UF::UNIT_FIELD_RANGED_ATTACK_POWER},
|
||||
{"PLAYER_FLAGS", UF::PLAYER_FLAGS},
|
||||
{"PLAYER_BYTES", UF::PLAYER_BYTES},
|
||||
{"PLAYER_BYTES_2", UF::PLAYER_BYTES_2},
|
||||
|
|
@ -61,6 +63,16 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"ITEM_FIELD_DURABILITY", UF::ITEM_FIELD_DURABILITY},
|
||||
{"ITEM_FIELD_MAXDURABILITY", UF::ITEM_FIELD_MAXDURABILITY},
|
||||
{"PLAYER_REST_STATE_EXPERIENCE", UF::PLAYER_REST_STATE_EXPERIENCE},
|
||||
{"PLAYER_CHOSEN_TITLE", UF::PLAYER_CHOSEN_TITLE},
|
||||
{"PLAYER_FIELD_MOD_DAMAGE_DONE_POS", UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS},
|
||||
{"PLAYER_FIELD_MOD_HEALING_DONE_POS", UF::PLAYER_FIELD_MOD_HEALING_DONE_POS},
|
||||
{"PLAYER_BLOCK_PERCENTAGE", UF::PLAYER_BLOCK_PERCENTAGE},
|
||||
{"PLAYER_DODGE_PERCENTAGE", UF::PLAYER_DODGE_PERCENTAGE},
|
||||
{"PLAYER_PARRY_PERCENTAGE", UF::PLAYER_PARRY_PERCENTAGE},
|
||||
{"PLAYER_CRIT_PERCENTAGE", UF::PLAYER_CRIT_PERCENTAGE},
|
||||
{"PLAYER_RANGED_CRIT_PERCENTAGE", UF::PLAYER_RANGED_CRIT_PERCENTAGE},
|
||||
{"PLAYER_SPELL_CRIT_PERCENTAGE1", UF::PLAYER_SPELL_CRIT_PERCENTAGE1},
|
||||
{"PLAYER_FIELD_COMBAT_RATING_1", UF::PLAYER_FIELD_COMBAT_RATING_1},
|
||||
{"CONTAINER_FIELD_NUM_SLOTS", UF::CONTAINER_FIELD_NUM_SLOTS},
|
||||
{"CONTAINER_FIELD_SLOT_1", UF::CONTAINER_FIELD_SLOT_1},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1905,6 +1905,42 @@ network::Packet StandStateChangePacket::build(uint8_t state) {
|
|||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Action Bar
|
||||
// ============================================================
|
||||
|
||||
network::Packet SetActionButtonPacket::build(uint8_t button, uint8_t type, uint32_t id, bool isClassic) {
|
||||
// Classic/Turtle (1.12): uint8 button + uint16 id + uint8 type + uint8 misc(0)
|
||||
// type encoding: 0=spell, 1=item, 64=macro
|
||||
// TBC/WotLK: uint8 button + uint32 packed (type<<24 | id)
|
||||
// type encoding: 0x00=spell, 0x80=item, 0x40=macro
|
||||
// packed=0 means clear the slot
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTION_BUTTON));
|
||||
packet.writeUInt8(button);
|
||||
if (isClassic) {
|
||||
// Classic: 16-bit id, 8-bit type code, 8-bit misc
|
||||
// Map ActionBarSlot::Type (0=EMPTY,1=SPELL,2=ITEM,3=MACRO) → classic type byte
|
||||
uint8_t classicType = 0; // 0 = spell
|
||||
if (type == 2 /* ITEM */) classicType = 1;
|
||||
if (type == 3 /* MACRO */) classicType = 64;
|
||||
packet.writeUInt16(static_cast<uint16_t>(id));
|
||||
packet.writeUInt8(classicType);
|
||||
packet.writeUInt8(0); // misc
|
||||
LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (Classic): button=", (int)button,
|
||||
" id=", id, " type=", (int)classicType);
|
||||
} else {
|
||||
// TBC/WotLK: type in bits 24–31, id in bits 0–23; packed=0 clears slot
|
||||
uint8_t packedType = 0x00; // spell
|
||||
if (type == 2 /* ITEM */) packedType = 0x80;
|
||||
if (type == 3 /* MACRO */) packedType = 0x40;
|
||||
uint32_t packed = (id == 0) ? 0 : (static_cast<uint32_t>(packedType) << 24) | (id & 0x00FFFFFF);
|
||||
packet.writeUInt32(packed);
|
||||
LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (TBC/WotLK): button=", (int)button,
|
||||
" packed=0x", std::hex, packed, std::dec);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Display Toggles
|
||||
// ============================================================
|
||||
|
|
@ -2810,7 +2846,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before flags (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
packet.readUInt32(); // Flags
|
||||
data.itemFlags = packet.readUInt32(); // Flags
|
||||
packet.readUInt32(); // Flags2
|
||||
packet.readUInt32(); // BuyCount
|
||||
packet.readUInt32(); // BuyPrice
|
||||
|
|
@ -2820,7 +2856,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
if (data.inventoryType > 28) {
|
||||
// inventoryType out of range — BuyCount probably not present; rewind and try 4 fields
|
||||
packet.setReadPos(postQualityPos);
|
||||
packet.readUInt32(); // Flags
|
||||
data.itemFlags = packet.readUInt32(); // Flags
|
||||
packet.readUInt32(); // Flags2
|
||||
packet.readUInt32(); // BuyPrice
|
||||
data.sellPrice = packet.readUInt32(); // SellPrice
|
||||
|
|
@ -2832,18 +2868,18 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before statsCount (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
packet.readUInt32(); // AllowableClass
|
||||
packet.readUInt32(); // AllowableRace
|
||||
data.allowableClass = packet.readUInt32(); // AllowableClass
|
||||
data.allowableRace = packet.readUInt32(); // AllowableRace
|
||||
data.itemLevel = packet.readUInt32();
|
||||
data.requiredLevel = packet.readUInt32();
|
||||
packet.readUInt32(); // RequiredSkill
|
||||
packet.readUInt32(); // RequiredSkillRank
|
||||
data.requiredSkill = packet.readUInt32(); // RequiredSkill
|
||||
data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank
|
||||
packet.readUInt32(); // RequiredSpell
|
||||
packet.readUInt32(); // RequiredHonorRank
|
||||
packet.readUInt32(); // RequiredCityRank
|
||||
packet.readUInt32(); // RequiredReputationFaction
|
||||
packet.readUInt32(); // RequiredReputationRank
|
||||
packet.readUInt32(); // MaxCount
|
||||
data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction
|
||||
data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank
|
||||
data.maxCount = static_cast<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
|
||||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
|
|
@ -2909,12 +2945,12 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
}
|
||||
|
||||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
packet.readUInt32(); // HolyRes
|
||||
packet.readUInt32(); // FireRes
|
||||
packet.readUInt32(); // NatureRes
|
||||
packet.readUInt32(); // FrostRes
|
||||
packet.readUInt32(); // ShadowRes
|
||||
packet.readUInt32(); // ArcaneRes
|
||||
data.holyRes = static_cast<int32_t>(packet.readUInt32()); // HolyRes
|
||||
data.fireRes = static_cast<int32_t>(packet.readUInt32()); // FireRes
|
||||
data.natureRes = static_cast<int32_t>(packet.readUInt32()); // NatureRes
|
||||
data.frostRes = static_cast<int32_t>(packet.readUInt32()); // FrostRes
|
||||
data.shadowRes = static_cast<int32_t>(packet.readUInt32()); // ShadowRes
|
||||
data.arcaneRes = static_cast<int32_t>(packet.readUInt32()); // ArcaneRes
|
||||
data.delayMs = packet.readUInt32();
|
||||
packet.readUInt32(); // AmmoType
|
||||
packet.readFloat(); // RangedModRange
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "rendering/clouds.hpp"
|
||||
#include "rendering/lens_flare.hpp"
|
||||
#include "rendering/weather.hpp"
|
||||
#include "rendering/lightning.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
|
|
@ -369,6 +370,11 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
|
|||
ImGui::Text("Intensity: %.0f%%", weather->getIntensity() * 100.0f);
|
||||
}
|
||||
|
||||
auto* lightning = renderer->getLightning();
|
||||
if (lightning && lightning->isEnabled()) {
|
||||
ImGui::Text("Lightning: active (%.0f%%)", lightning->getIntensity() * 100.0f);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "rendering/clouds.hpp"
|
||||
#include "rendering/lens_flare.hpp"
|
||||
#include "rendering/weather.hpp"
|
||||
#include "rendering/lightning.hpp"
|
||||
#include "rendering/lighting_manager.hpp"
|
||||
#include "rendering/sky_system.hpp"
|
||||
#include "rendering/swim_effects.hpp"
|
||||
|
|
@ -699,6 +700,9 @@ bool Renderer::initialize(core::Window* win) {
|
|||
weather = std::make_unique<Weather>();
|
||||
weather->initialize(vkCtx, perFrameSetLayout);
|
||||
|
||||
lightning = std::make_unique<Lightning>();
|
||||
lightning->initialize(vkCtx, perFrameSetLayout);
|
||||
|
||||
swimEffects = std::make_unique<SwimEffects>();
|
||||
swimEffects->initialize(vkCtx, perFrameSetLayout);
|
||||
|
||||
|
|
@ -802,6 +806,11 @@ void Renderer::shutdown() {
|
|||
weather.reset();
|
||||
}
|
||||
|
||||
if (lightning) {
|
||||
lightning->shutdown();
|
||||
lightning.reset();
|
||||
}
|
||||
|
||||
if (swimEffects) {
|
||||
swimEffects->shutdown();
|
||||
swimEffects.reset();
|
||||
|
|
@ -942,6 +951,7 @@ void Renderer::applyMsaaChange() {
|
|||
if (characterRenderer) characterRenderer->recreatePipelines();
|
||||
if (questMarkerRenderer) questMarkerRenderer->recreatePipelines();
|
||||
if (weather) weather->recreatePipelines();
|
||||
if (lightning) lightning->recreatePipelines();
|
||||
if (swimEffects) swimEffects->recreatePipelines();
|
||||
if (mountDust) mountDust->recreatePipelines();
|
||||
if (chargeEffect) chargeEffect->recreatePipelines();
|
||||
|
|
@ -2856,6 +2866,7 @@ void Renderer::update(float deltaTime) {
|
|||
// Server-driven weather (SMSG_WEATHER) — authoritative
|
||||
if (wType == 1) weather->setWeatherType(Weather::Type::RAIN);
|
||||
else if (wType == 2) weather->setWeatherType(Weather::Type::SNOW);
|
||||
else if (wType == 3) weather->setWeatherType(Weather::Type::RAIN); // thunderstorm — use rain particles
|
||||
else weather->setWeatherType(Weather::Type::NONE);
|
||||
weather->setIntensity(wInt);
|
||||
} else {
|
||||
|
|
@ -2863,6 +2874,20 @@ void Renderer::update(float deltaTime) {
|
|||
weather->updateZoneWeather(currentZoneId, deltaTime);
|
||||
}
|
||||
weather->setEnabled(true);
|
||||
|
||||
// Enable lightning during storms (wType==3) and heavy rain
|
||||
if (lightning) {
|
||||
uint32_t wType2 = gh->getWeatherType();
|
||||
float wInt2 = gh->getWeatherIntensity();
|
||||
bool stormActive = (wType2 == 3 && wInt2 > 0.1f)
|
||||
|| (wType2 == 1 && wInt2 > 0.7f);
|
||||
lightning->setEnabled(stormActive);
|
||||
if (stormActive) {
|
||||
// Scale intensity: storm at full, heavy rain proportionally
|
||||
float lIntensity = (wType2 == 3) ? wInt2 : (wInt2 - 0.7f) / 0.3f;
|
||||
lightning->setIntensity(lIntensity);
|
||||
}
|
||||
}
|
||||
} else if (weather) {
|
||||
// No game handler (single-player without network) — zone weather only
|
||||
weather->updateZoneWeather(currentZoneId, deltaTime);
|
||||
|
|
@ -2932,6 +2957,11 @@ void Renderer::update(float deltaTime) {
|
|||
weather->update(*camera, deltaTime);
|
||||
}
|
||||
|
||||
// Update lightning (storm / heavy rain)
|
||||
if (lightning && camera && lightning->isEnabled()) {
|
||||
lightning->update(deltaTime, *camera);
|
||||
}
|
||||
|
||||
// Update swim effects
|
||||
if (swimEffects && camera && cameraController && waterRenderer) {
|
||||
swimEffects->update(*camera, *cameraController, *waterRenderer, deltaTime);
|
||||
|
|
@ -5217,6 +5247,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
if (waterRenderer && camera)
|
||||
waterRenderer->render(cmd, perFrameSet, *camera, globalTime, false, frameIdx);
|
||||
if (weather && camera) weather->render(cmd, perFrameSet);
|
||||
if (lightning && camera && lightning->isEnabled()) lightning->render(cmd, perFrameSet);
|
||||
if (swimEffects && camera) swimEffects->render(cmd, perFrameSet);
|
||||
if (mountDust && camera) mountDust->render(cmd, perFrameSet);
|
||||
if (chargeEffect && camera) chargeEffect->render(cmd, perFrameSet);
|
||||
|
|
@ -5353,6 +5384,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
if (waterRenderer && camera)
|
||||
waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime, false, frameIdx);
|
||||
if (weather && camera) weather->render(currentCmd, perFrameSet);
|
||||
if (lightning && camera && lightning->isEnabled()) lightning->render(currentCmd, perFrameSet);
|
||||
if (swimEffects && camera) swimEffects->render(currentCmd, perFrameSet);
|
||||
if (mountDust && camera) mountDust->render(currentCmd, perFrameSet);
|
||||
if (chargeEffect && camera) chargeEffect->render(currentCmd, perFrameSet);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,22 @@ static uint64_t hashEquipment(const std::vector<game::EquipmentItem>& eq) {
|
|||
return h;
|
||||
}
|
||||
|
||||
static ImVec4 classColor(uint8_t classId) {
|
||||
switch (classId) {
|
||||
case 1: return ImVec4(0.78f, 0.61f, 0.43f, 1.0f); // Warrior #C79C6E
|
||||
case 2: return ImVec4(0.96f, 0.55f, 0.73f, 1.0f); // Paladin #F58CBA
|
||||
case 3: return ImVec4(0.67f, 0.83f, 0.45f, 1.0f); // Hunter #ABD473
|
||||
case 4: return ImVec4(1.00f, 0.96f, 0.41f, 1.0f); // Rogue #FFF569
|
||||
case 5: return ImVec4(1.00f, 1.00f, 1.00f, 1.0f); // Priest #FFFFFF
|
||||
case 6: return ImVec4(0.77f, 0.12f, 0.23f, 1.0f); // DeathKnight #C41F3B
|
||||
case 7: return ImVec4(0.00f, 0.44f, 0.87f, 1.0f); // Shaman #0070DE
|
||||
case 8: return ImVec4(0.41f, 0.80f, 0.94f, 1.0f); // Mage #69CCF0
|
||||
case 9: return ImVec4(0.58f, 0.51f, 0.79f, 1.0f); // Warlock #9482C9
|
||||
case 11: return ImVec4(1.00f, 0.49f, 0.04f, 1.0f); // Druid #FF7D0A
|
||||
default: return ImVec4(0.85f, 0.85f, 0.85f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
const ImVec2 pad(24.0f, 24.0f);
|
||||
|
|
@ -184,7 +200,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 45.0f);
|
||||
ImGui::TableSetupColumn("Race", ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
||||
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 1.2f);
|
||||
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 55.0f);
|
||||
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch, 1.5f);
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
|
|
@ -224,10 +240,16 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
ImGui::Text("%s", game::getRaceName(character.race));
|
||||
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::Text("%s", game::getClassName(character.characterClass));
|
||||
ImGui::TextColored(classColor(static_cast<uint8_t>(character.characterClass)), "%s", game::getClassName(character.characterClass));
|
||||
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::Text("%d", character.zoneId);
|
||||
{
|
||||
std::string zoneName = gameHandler.getWhoAreaName(character.zoneId);
|
||||
if (!zoneName.empty())
|
||||
ImGui::TextUnformatted(zoneName.c_str());
|
||||
else
|
||||
ImGui::Text("%u", character.zoneId);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
|
|
@ -325,10 +347,21 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
|
||||
ImGui::Text("Level %d", character.level);
|
||||
ImGui::Text("%s", game::getRaceName(character.race));
|
||||
ImGui::Text("%s", game::getClassName(character.characterClass));
|
||||
ImGui::TextColored(classColor(static_cast<uint8_t>(character.characterClass)), "%s", game::getClassName(character.characterClass));
|
||||
ImGui::Text("%s", game::getGenderName(character.gender));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Map %d, Zone %d", character.mapId, character.zoneId);
|
||||
{
|
||||
std::string mapName = gameHandler.getMapName(character.mapId);
|
||||
std::string zoneName = gameHandler.getWhoAreaName(character.zoneId);
|
||||
if (!mapName.empty() && !zoneName.empty())
|
||||
ImGui::Text("%s — %s", mapName.c_str(), zoneName.c_str());
|
||||
else if (!mapName.empty())
|
||||
ImGui::Text("%s (Zone %u)", mapName.c_str(), character.zoneId);
|
||||
else if (!zoneName.empty())
|
||||
ImGui::Text("Map %u — %s", character.mapId, zoneName.c_str());
|
||||
else
|
||||
ImGui::Text("Map %u, Zone %u", character.mapId, character.zoneId);
|
||||
}
|
||||
|
||||
if (character.hasGuild()) {
|
||||
ImGui::Text("Guild ID: %d", character.guildId);
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ namespace {
|
|||
// Build a WoW-format item link string for chat insertion.
|
||||
// Format: |cff<qualHex>|Hitem:<itemId>:0:0:0:0:0:0:0:0|h[<name>]|h|r
|
||||
std::string buildItemChatLink(uint32_t itemId, uint8_t quality, const std::string& name) {
|
||||
static const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000"};
|
||||
uint8_t qi = quality < 6 ? quality : 1;
|
||||
static const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
|
||||
uint8_t qi = quality < 8 ? quality : 1;
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "|cff%s|Hitem:%u:0:0:0:0:0:0:0:0|h[%s]|h|r",
|
||||
kQualHex[qi], itemId, name.c_str());
|
||||
|
|
@ -395,6 +395,10 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
gameHandler.setUIErrorCallback([this](const std::string& msg) {
|
||||
uiErrors_.push_back({msg, 0.0f});
|
||||
if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin());
|
||||
// Play error sound for each new error (rate-limited by deque cap of 5)
|
||||
if (auto* r = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* sfx = r->getUiSoundManager()) sfx->playError();
|
||||
}
|
||||
});
|
||||
uiErrorCallbackSet_ = true;
|
||||
}
|
||||
|
|
@ -722,6 +726,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
if (showMinimap_) {
|
||||
renderMinimapMarkers(gameHandler);
|
||||
}
|
||||
renderLogoutCountdown(gameHandler);
|
||||
renderDeathScreen(gameHandler);
|
||||
renderReclaimCorpseButton(gameHandler);
|
||||
renderResurrectDialog(gameHandler);
|
||||
|
|
@ -2756,6 +2761,18 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
|||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("You are in combat");
|
||||
}
|
||||
|
||||
// Active title — shown in gold below the name/level line
|
||||
{
|
||||
int32_t titleBit = gameHandler.getChosenTitleBit();
|
||||
if (titleBit >= 0) {
|
||||
const std::string titleText = gameHandler.getFormattedTitle(
|
||||
static_cast<uint32_t>(titleBit));
|
||||
if (!titleText.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 0.9f), "%s", titleText.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get real HP/mana from the player entity
|
||||
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
||||
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) {
|
||||
|
|
@ -3758,6 +3775,44 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Target-of-Target (ToT): show who the current target is targeting
|
||||
{
|
||||
uint64_t totGuid = 0;
|
||||
const auto& tFields = target->getFields();
|
||||
auto itLo = tFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
|
||||
if (itLo != tFields.end()) {
|
||||
totGuid = itLo->second;
|
||||
auto itHi = tFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
|
||||
if (itHi != tFields.end())
|
||||
totGuid |= (static_cast<uint64_t>(itHi->second) << 32);
|
||||
}
|
||||
if (totGuid != 0) {
|
||||
auto totEnt = gameHandler.getEntityManager().getEntity(totGuid);
|
||||
std::string totName;
|
||||
ImVec4 totColor(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
if (totGuid == gameHandler.getPlayerGuid()) {
|
||||
auto playerEnt = gameHandler.getEntityManager().getEntity(totGuid);
|
||||
totName = playerEnt ? getEntityName(playerEnt) : "You";
|
||||
totColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
} else if (totEnt) {
|
||||
totName = getEntityName(totEnt);
|
||||
uint8_t cid = entityClassId(totEnt.get());
|
||||
if (cid != 0) totColor = classColorVec4(cid);
|
||||
}
|
||||
if (!totName.empty()) {
|
||||
ImGui::TextDisabled("▶");
|
||||
ImGui::SameLine(0, 2);
|
||||
ImGui::TextColored(totColor, "%s", totName.c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Target's target: %s\nClick to target", totName.c_str());
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
gameHandler.setTarget(totGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distance
|
||||
const auto& movement = gameHandler.getMovementInfo();
|
||||
float dx = target->getX() - movement.x;
|
||||
|
|
@ -4619,6 +4674,12 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (cmdLower == "clear") {
|
||||
gameHandler.clearChatHistory();
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// /invite command
|
||||
if (cmdLower == "invite" && spacePos != std::string::npos) {
|
||||
std::string targetName = command.substr(spacePos + 1);
|
||||
|
|
@ -6754,14 +6815,24 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
|||
if (slot.id == 8690) {
|
||||
uint32_t mapId = 0; glm::vec3 pos;
|
||||
if (gameHandler.getHomeBind(mapId, pos)) {
|
||||
const char* mapName = "Unknown";
|
||||
switch (mapId) {
|
||||
case 0: mapName = "Eastern Kingdoms"; break;
|
||||
case 1: mapName = "Kalimdor"; break;
|
||||
case 530: mapName = "Outland"; break;
|
||||
case 571: mapName = "Northrend"; break;
|
||||
std::string homeLocation;
|
||||
// Zone name (from zoneId stored in bind point)
|
||||
uint32_t zoneId = gameHandler.getHomeBindZoneId();
|
||||
if (zoneId != 0) {
|
||||
homeLocation = gameHandler.getWhoAreaName(zoneId);
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName);
|
||||
// Fall back to continent name if zone unavailable
|
||||
if (homeLocation.empty()) {
|
||||
switch (mapId) {
|
||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
||||
case 1: homeLocation = "Kalimdor"; break;
|
||||
case 530: homeLocation = "Outland"; break;
|
||||
case 571: homeLocation = "Northrend"; break;
|
||||
default: homeLocation = "Unknown"; break;
|
||||
}
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
||||
"Home: %s", homeLocation.c_str());
|
||||
}
|
||||
}
|
||||
if (outOfRange) {
|
||||
|
|
@ -8287,7 +8358,13 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
|||
break;
|
||||
case game::CombatTextEntry::ENERGIZE:
|
||||
snprintf(text, sizeof(text), "+%d", entry.amount);
|
||||
color = ImVec4(0.3f, 0.6f, 1.0f, alpha); // Blue for mana/energy
|
||||
switch (entry.powerType) {
|
||||
case 1: color = ImVec4(1.0f, 0.2f, 0.2f, alpha); break; // Rage: red
|
||||
case 2: color = ImVec4(1.0f, 0.6f, 0.1f, alpha); break; // Focus: orange
|
||||
case 3: color = ImVec4(1.0f, 0.9f, 0.2f, alpha); break; // Energy: yellow
|
||||
case 6: color = ImVec4(0.3f, 0.9f, 0.8f, alpha); break; // Runic Power: teal
|
||||
default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break; // Mana (0): blue
|
||||
}
|
||||
break;
|
||||
case game::CombatTextEntry::XP_GAIN:
|
||||
snprintf(text, sizeof(text), "+%d XP", entry.amount);
|
||||
|
|
@ -10674,9 +10751,11 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) {
|
|||
ImVec4(0.0f, 0.44f, 0.87f, 1.0f),// 3=rare (blue)
|
||||
ImVec4(0.64f, 0.21f, 0.93f, 1.0f),// 4=epic (purple)
|
||||
ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // 5=legendary (orange)
|
||||
ImVec4(0.90f, 0.80f, 0.50f, 1.0f),// 6=artifact (light gold)
|
||||
ImVec4(0.90f, 0.80f, 0.50f, 1.0f),// 7=heirloom (light gold)
|
||||
};
|
||||
uint8_t q = roll.itemQuality;
|
||||
ImVec4 col = (q < 6) ? kQualityColors[q] : kQualityColors[1];
|
||||
ImVec4 col = (q < 8) ? kQualityColors[q] : kQualityColors[1];
|
||||
|
||||
// Countdown bar
|
||||
{
|
||||
|
|
@ -10716,7 +10795,14 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) {
|
|||
ImGui::Image((ImTextureID)(uintptr_t)rollIcon, ImVec2(24, 24));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::TextColored(col, "[%s]", roll.itemName.c_str());
|
||||
// Prefer live item info (arrives via SMSG_ITEM_QUERY_SINGLE_RESPONSE after the
|
||||
// roll popup opens); fall back to the name cached at SMSG_LOOT_START_ROLL time.
|
||||
const char* displayName = (rollInfo && rollInfo->valid && !rollInfo->name.empty())
|
||||
? rollInfo->name.c_str()
|
||||
: roll.itemName.c_str();
|
||||
if (rollInfo && rollInfo->valid)
|
||||
col = (rollInfo->quality < 8) ? kQualityColors[rollInfo->quality] : kQualityColors[1];
|
||||
ImGui::TextColored(col, "[%s]", displayName);
|
||||
if (ImGui::IsItemHovered() && rollInfo && rollInfo->valid) {
|
||||
inventoryScreen.renderItemTooltip(*rollInfo);
|
||||
}
|
||||
|
|
@ -12376,8 +12462,9 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|||
// 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());
|
||||
} else if (hovered && info && !info->name.empty()) {
|
||||
// Item info received but not yet fully valid — show name at minimum
|
||||
ImGui::SetTooltip("%s", info->name.c_str());
|
||||
}
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
|
@ -13130,7 +13217,16 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
gameHandler.repairAll(vendor.vendorGuid, false);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Repair all equipped items");
|
||||
ImGui::SetTooltip("Repair all equipped items using your gold");
|
||||
}
|
||||
if (gameHandler.isInGuild()) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Repair (Guild)")) {
|
||||
gameHandler.repairAll(vendor.vendorGuid, true);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Repair all equipped items using guild bank funds");
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
|
@ -14016,6 +14112,68 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Logout Countdown
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderLogoutCountdown(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isLoggingOut()) 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;
|
||||
|
||||
constexpr float W = 280.0f;
|
||||
constexpr float H = 80.0f;
|
||||
ImGui::SetNextWindowPos(ImVec2((screenW - W) * 0.5f, screenH * 0.5f - H * 0.5f - 60.0f),
|
||||
ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(W, H), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowBgAlpha(0.88f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.95f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.8f, 1.0f));
|
||||
|
||||
if (ImGui::Begin("##LogoutCountdown", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
||||
|
||||
float cd = gameHandler.getLogoutCountdown();
|
||||
if (cd > 0.0f) {
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 6.0f);
|
||||
ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out in 20s...").x) * 0.5f);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f),
|
||||
"Logging out in %ds...", static_cast<int>(std::ceil(cd)));
|
||||
|
||||
// Progress bar (20 second countdown)
|
||||
float frac = 1.0f - std::min(cd / 20.0f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.5f, 0.5f, 0.9f, 1.0f));
|
||||
ImGui::ProgressBar(frac, ImVec2(-1.0f, 8.0f), "");
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
} else {
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 14.0f);
|
||||
ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out...").x) * 0.5f);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "Logging out...");
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Cancel button — only while countdown is still running
|
||||
if (cd > 0.0f) {
|
||||
float btnW = 100.0f;
|
||||
ImGui::SetCursorPosX((W - btnW) * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f));
|
||||
if (ImGui::Button("Cancel", ImVec2(btnW, 0))) {
|
||||
gameHandler.cancelLogout();
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Death Screen
|
||||
// ============================================================
|
||||
|
|
@ -16557,8 +16715,15 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|||
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());
|
||||
if (slot.avgWaitTimeSec > 0) {
|
||||
int avgMin = static_cast<int>(slot.avgWaitTimeSec) / 60;
|
||||
int avgSec = static_cast<int>(slot.avgWaitTimeSec) % 60;
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse),
|
||||
"Queue: %s (~%d:%02d)", bgName.c_str(), avgMin, avgSec);
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse),
|
||||
"In Queue: %s", bgName.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
nextIndicatorY += kIndicatorH;
|
||||
|
|
@ -16590,6 +16755,47 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// Calendar pending invites indicator (WotLK only)
|
||||
{
|
||||
auto* expReg = core::Application::getInstance().getExpansionRegistry();
|
||||
bool isWotLK = expReg && expReg->getActive() && expReg->getActive()->id == "wotlk";
|
||||
if (isWotLK) {
|
||||
uint32_t calPending = gameHandler.getCalendarPendingInvites();
|
||||
if (calPending > 0) {
|
||||
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
||||
if (ImGui::Begin("##CalendarIndicator", nullptr, indicatorFlags)) {
|
||||
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 2.0f);
|
||||
char calBuf[48];
|
||||
snprintf(calBuf, sizeof(calBuf), "Calendar: %u Invite%s",
|
||||
calPending, calPending == 1 ? "" : "s");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.5f, 1.0f, pulse), "%s", calBuf);
|
||||
}
|
||||
ImGui::End();
|
||||
nextIndicatorY += kIndicatorH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Taxi flight indicator — shown while on a flight path
|
||||
if (gameHandler.isOnTaxiFlight()) {
|
||||
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
||||
if (ImGui::Begin("##TaxiIndicator", nullptr, indicatorFlags)) {
|
||||
const std::string& dest = gameHandler.getTaxiDestName();
|
||||
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 1.0f);
|
||||
if (dest.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, pulse), "\xe2\x9c\x88 In Flight");
|
||||
} else {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "\xe2\x9c\x88 \xe2\x86\x92 %s", dest.c_str());
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, pulse), "%s", buf);
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
nextIndicatorY += kIndicatorH;
|
||||
}
|
||||
|
||||
// Latency indicator — centered at top of screen
|
||||
uint32_t latMs = gameHandler.getLatencyMs();
|
||||
if (showLatencyMeter_ && latMs > 0 && gameHandler.getState() == game::WorldState::IN_WORLD) {
|
||||
|
|
@ -18843,6 +19049,8 @@ void GameScreen::renderItemLootToasts() {
|
|||
IM_COL32( 0, 112, 221, 255), // 3 blue (rare)
|
||||
IM_COL32(163, 53, 238, 255), // 4 purple (epic)
|
||||
IM_COL32(255, 128, 0, 255), // 5 orange (legendary)
|
||||
IM_COL32(230, 204, 128, 255), // 6 light gold (artifact)
|
||||
IM_COL32(230, 204, 128, 255), // 7 light gold (heirloom)
|
||||
};
|
||||
|
||||
// Stack at bottom-left above action bars; each item is 24 px tall
|
||||
|
|
@ -18881,7 +19089,7 @@ void GameScreen::renderItemLootToasts() {
|
|||
IM_COL32(12, 12, 12, bgA), 3.0f);
|
||||
|
||||
// Quality colour accent bar on left edge (3px wide)
|
||||
ImU32 qualCol = kQualityColors[std::min(static_cast<uint32_t>(5u), toast.quality)];
|
||||
ImU32 qualCol = kQualityColors[std::min(static_cast<uint32_t>(7u), toast.quality)];
|
||||
ImU32 qualColA = (qualCol & 0x00FFFFFFu) | (static_cast<uint32_t>(fgA) << 24u);
|
||||
bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + 3.0f, ty + TOAST_H), qualColA, 3.0f);
|
||||
|
||||
|
|
@ -19315,7 +19523,12 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
|||
uint32_t qMs = gameHandler.getLfgTimeInQueueMs();
|
||||
int qMin = static_cast<int>(qMs / 60000);
|
||||
int qSec = static_cast<int>((qMs % 60000) / 1000);
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "Status: In queue (%d:%02d)", qMin, qSec);
|
||||
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
||||
if (!dName.empty())
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f),
|
||||
"Status: In queue for %s (%d:%02d)", dName.c_str(), qMin, qSec);
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "Status: In queue (%d:%02d)", qMin, qSec);
|
||||
if (avgSec >= 0) {
|
||||
int aMin = avgSec / 60;
|
||||
int aSec = avgSec % 60;
|
||||
|
|
@ -19324,18 +19537,33 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case LfgState::Proposal:
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!");
|
||||
case LfgState::Proposal: {
|
||||
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
||||
if (!dName.empty())
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found for %s!", dName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!");
|
||||
break;
|
||||
}
|
||||
case LfgState::Boot:
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Status: Vote kick in progress");
|
||||
break;
|
||||
case LfgState::InDungeon:
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon");
|
||||
case LfgState::InDungeon: {
|
||||
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
||||
if (!dName.empty())
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon (%s)", dName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon");
|
||||
break;
|
||||
case LfgState::FinishedDungeon:
|
||||
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: Dungeon complete");
|
||||
}
|
||||
case LfgState::FinishedDungeon: {
|
||||
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
||||
if (!dName.empty())
|
||||
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: %s complete", dName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: Dungeon complete");
|
||||
break;
|
||||
}
|
||||
case LfgState::RaidBrowser:
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 1.0f, 1.0f), "Status: Raid browser");
|
||||
break;
|
||||
|
|
@ -19345,8 +19573,13 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
// ---- Proposal accept/decline ----
|
||||
if (state == LfgState::Proposal) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
||||
"A group has been found for your dungeon!");
|
||||
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
||||
if (!dName.empty())
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
||||
"A group has been found for %s!", dName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
||||
"A group has been found for your dungeon!");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Accept", ImVec2(120, 0))) {
|
||||
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true);
|
||||
|
|
@ -19517,24 +19750,6 @@ void GameScreen::renderInstanceLockouts(game::GameHandler& gameHandler) {
|
|||
if (lockouts.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No active instance lockouts.");
|
||||
} else {
|
||||
// Build map name lookup from Map.dbc (cached after first call)
|
||||
static std::unordered_map<uint32_t, std::string> sMapNames;
|
||||
static bool sMapNamesLoaded = false;
|
||||
if (!sMapNamesLoaded) {
|
||||
sMapNamesLoaded = true;
|
||||
if (auto* am = core::Application::getInstance().getAssetManager()) {
|
||||
if (auto dbc = am->loadDBC("Map.dbc"); dbc && dbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
// Field 2 = MapName_enUS (first localized), field 1 = InternalName
|
||||
std::string name = dbc->getString(i, 2);
|
||||
if (name.empty()) name = dbc->getString(i, 1);
|
||||
if (!name.empty()) sMapNames[id] = std::move(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto difficultyLabel = [](uint32_t diff) -> const char* {
|
||||
switch (diff) {
|
||||
case 0: return "Normal";
|
||||
|
|
@ -19560,11 +19775,11 @@ void GameScreen::renderInstanceLockouts(game::GameHandler& gameHandler) {
|
|||
for (const auto& lo : lockouts) {
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// Instance name
|
||||
// Instance name — use GameHandler's Map.dbc cache (avoids duplicate DBC load)
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
auto it = sMapNames.find(lo.mapId);
|
||||
if (it != sMapNames.end()) {
|
||||
ImGui::TextUnformatted(it->second.c_str());
|
||||
std::string mapName = gameHandler.getMapName(lo.mapId);
|
||||
if (!mapName.empty()) {
|
||||
ImGui::TextUnformatted(mapName.c_str());
|
||||
} else {
|
||||
ImGui::Text("Map %u", lo.mapId);
|
||||
}
|
||||
|
|
@ -19972,31 +20187,60 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
|
|||
color = ImVec4(0.4f, 0.9f, 0.4f, 1.0f);
|
||||
break;
|
||||
case T::MISS:
|
||||
snprintf(desc, sizeof(desc), "%s misses %s", src, tgt);
|
||||
if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s's %s misses %s", src, spell, tgt);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s misses %s", src, tgt);
|
||||
color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f);
|
||||
break;
|
||||
case T::DODGE:
|
||||
snprintf(desc, sizeof(desc), "%s dodges %s's attack", tgt, src);
|
||||
if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s dodges %s's %s", tgt, src, spell);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s dodges %s's attack", tgt, src);
|
||||
color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f);
|
||||
break;
|
||||
case T::PARRY:
|
||||
snprintf(desc, sizeof(desc), "%s parries %s's attack", tgt, src);
|
||||
if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s parries %s's %s", tgt, src, spell);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s parries %s's attack", tgt, src);
|
||||
color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f);
|
||||
break;
|
||||
case T::BLOCK:
|
||||
snprintf(desc, sizeof(desc), "%s blocks %s's attack (%d blocked)", tgt, src, e.amount);
|
||||
if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s blocks %s's %s (%d blocked)", tgt, src, spell, e.amount);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s blocks %s's attack (%d blocked)", tgt, src, e.amount);
|
||||
color = ImVec4(0.65f, 0.75f, 0.65f, 1.0f);
|
||||
break;
|
||||
case T::IMMUNE:
|
||||
snprintf(desc, sizeof(desc), "%s is immune", tgt);
|
||||
if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s is immune to %s", tgt, spell);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s is immune", tgt);
|
||||
color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
|
||||
break;
|
||||
case T::ABSORB:
|
||||
snprintf(desc, sizeof(desc), "%d absorbed", e.amount);
|
||||
if (spell && e.amount > 0)
|
||||
snprintf(desc, sizeof(desc), "%s's %s absorbs %d", src, spell, e.amount);
|
||||
else if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s absorbs %s", tgt, spell);
|
||||
else if (e.amount > 0)
|
||||
snprintf(desc, sizeof(desc), "%d absorbed", e.amount);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "Absorbed");
|
||||
color = ImVec4(0.5f, 0.8f, 1.0f, 1.0f);
|
||||
break;
|
||||
case T::RESIST:
|
||||
snprintf(desc, sizeof(desc), "%d resisted", e.amount);
|
||||
if (spell && e.amount > 0)
|
||||
snprintf(desc, sizeof(desc), "%s resists %s's %s (%d resisted)", tgt, src, spell, e.amount);
|
||||
else if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s resists %s's %s", tgt, src, spell);
|
||||
else if (e.amount > 0)
|
||||
snprintf(desc, sizeof(desc), "%d resisted", e.amount);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "Resisted");
|
||||
color = ImVec4(0.6f, 0.6f, 0.9f, 1.0f);
|
||||
break;
|
||||
case T::ENVIRONMENTAL:
|
||||
|
|
@ -20021,6 +20265,28 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
|
|||
snprintf(desc, sizeof(desc), "Proc triggered");
|
||||
color = ImVec4(1.0f, 0.85f, 0.3f, 1.0f);
|
||||
break;
|
||||
case T::DISPEL:
|
||||
if (spell && e.isPlayerSource)
|
||||
snprintf(desc, sizeof(desc), "You dispel %s from %s", spell, tgt);
|
||||
else if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s dispels %s from %s", src, spell, tgt);
|
||||
else if (e.isPlayerSource)
|
||||
snprintf(desc, sizeof(desc), "You dispel from %s", tgt);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s dispels from %s", src, tgt);
|
||||
color = ImVec4(0.6f, 0.9f, 1.0f, 1.0f);
|
||||
break;
|
||||
case T::INTERRUPT:
|
||||
if (spell && e.isPlayerSource)
|
||||
snprintf(desc, sizeof(desc), "You interrupt %s's %s", tgt, spell);
|
||||
else if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s interrupts %s's %s", src, tgt, spell);
|
||||
else if (e.isPlayerSource)
|
||||
snprintf(desc, sizeof(desc), "You interrupt %s", tgt);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s interrupted", tgt);
|
||||
color = ImVec4(1.0f, 0.6f, 0.9f, 1.0f);
|
||||
break;
|
||||
default:
|
||||
snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", (int)e.type, e.amount);
|
||||
color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -102,6 +103,8 @@ ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
|
|||
case game::ItemQuality::RARE: return ImVec4(0.0f, 0.44f, 0.87f, 1.0f); // Blue
|
||||
case game::ItemQuality::EPIC: return ImVec4(0.64f, 0.21f, 0.93f, 1.0f); // Purple
|
||||
case game::ItemQuality::LEGENDARY: return ImVec4(1.0f, 0.50f, 0.0f, 1.0f); // Orange
|
||||
case game::ItemQuality::ARTIFACT: return ImVec4(0.90f, 0.80f, 0.50f, 1.0f); // Light gold
|
||||
case game::ItemQuality::HEIRLOOM: return ImVec4(0.90f, 0.80f, 0.50f, 1.0f); // Light gold
|
||||
default: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -1161,7 +1164,7 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
const int32_t* serverStats = (stats[0] >= 0) ? stats : nullptr;
|
||||
int32_t resists[6];
|
||||
for (int i = 0; i < 6; ++i) resists[i] = gameHandler.getResistance(i + 1);
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating(), serverStats, resists);
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating(), serverStats, resists, &gameHandler);
|
||||
|
||||
// Played time (shown if available, fetched on character screen open)
|
||||
uint32_t totalSec = gameHandler.getTotalTimePlayed();
|
||||
|
|
@ -1242,18 +1245,35 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
snprintf(label, sizeof(label), "%s", name.c_str());
|
||||
}
|
||||
|
||||
// Show progress bar with value/max overlay
|
||||
// Effective value includes temporary and permanent bonuses
|
||||
uint16_t effective = skill->effectiveValue();
|
||||
uint16_t bonus = skill->bonusTemp + skill->bonusPerm;
|
||||
|
||||
// Progress bar reflects effective / max; cap visual fill at 1.0
|
||||
float ratio = (skill->maxValue > 0)
|
||||
? static_cast<float>(skill->value) / static_cast<float>(skill->maxValue)
|
||||
? std::min(1.0f, static_cast<float>(effective) / static_cast<float>(skill->maxValue))
|
||||
: 0.0f;
|
||||
|
||||
char overlay[64];
|
||||
snprintf(overlay, sizeof(overlay), "%u / %u", skill->value, skill->maxValue);
|
||||
if (bonus > 0)
|
||||
snprintf(overlay, sizeof(overlay), "%u / %u (+%u)", effective, skill->maxValue, bonus);
|
||||
else
|
||||
snprintf(overlay, sizeof(overlay), "%u / %u", effective, skill->maxValue);
|
||||
|
||||
ImGui::Text("%s", label);
|
||||
// Gold name when maxed out, cyan when buffed above base, default otherwise
|
||||
bool isMaxed = (effective >= skill->maxValue && skill->maxValue > 0);
|
||||
bool isBuffed = (bonus > 0);
|
||||
ImVec4 nameColor = isMaxed ? ImVec4(1.0f, 0.82f, 0.0f, 1.0f)
|
||||
: isBuffed ? ImVec4(0.4f, 0.9f, 1.0f, 1.0f)
|
||||
: ImVec4(0.85f, 0.85f, 0.85f, 1.0f);
|
||||
ImGui::TextColored(nameColor, "%s", label);
|
||||
ImGui::SameLine(180.0f);
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
// Bar color: gold when maxed, green otherwise
|
||||
ImVec4 barColor = isMaxed ? ImVec4(1.0f, 0.82f, 0.0f, 1.0f) : ImVec4(0.2f, 0.7f, 0.2f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor);
|
||||
ImGui::ProgressBar(ratio, ImVec2(0, 14.0f), overlay);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1443,6 +1463,8 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) {
|
|||
bool atWar = (repListId != 0xFFFFFFFFu) && gameHandler.isFactionAtWar(repListId);
|
||||
bool isWatched = (factionId == watchedFactionId);
|
||||
|
||||
ImGui::PushID(static_cast<int>(factionId));
|
||||
|
||||
// Faction name + tier label on same line; mark at-war and watched factions
|
||||
ImGui::TextColored(tier.color, "[%s]", tier.name);
|
||||
ImGui::SameLine(90.0f);
|
||||
|
|
@ -1478,7 +1500,23 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) {
|
|||
ImGui::SetNextItemWidth(-1.0f);
|
||||
ImGui::ProgressBar(ratio, ImVec2(0, 12.0f), overlay);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Right-click context menu on the progress bar
|
||||
if (ImGui::BeginPopupContextItem("##RepCtx")) {
|
||||
ImGui::TextDisabled("%s", displayName);
|
||||
ImGui::Separator();
|
||||
if (isWatched) {
|
||||
if (ImGui::MenuItem("Untrack"))
|
||||
gameHandler.setWatchedFactionId(0);
|
||||
} else {
|
||||
if (ImGui::MenuItem("Track on Rep Bar"))
|
||||
gameHandler.setWatchedFactionId(factionId);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
|
@ -1606,7 +1644,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
|
||||
void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel,
|
||||
int32_t serverArmor, const int32_t* serverStats,
|
||||
const int32_t* serverResists) {
|
||||
const int32_t* serverResists,
|
||||
const game::GameHandler* gh) {
|
||||
// Sum equipment stats for item-query bonus display
|
||||
int32_t itemStr = 0, itemAgi = 0, itemSta = 0, itemInt = 0, itemSpi = 0;
|
||||
// Secondary stat sums from extraStats
|
||||
|
|
@ -1776,6 +1815,174 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Server-authoritative combat stats (WotLK update fields — only shown when received)
|
||||
if (gh) {
|
||||
int32_t meleeAP = gh->getMeleeAttackPower();
|
||||
int32_t rangedAP = gh->getRangedAttackPower();
|
||||
int32_t spellPow = gh->getSpellPower();
|
||||
int32_t healPow = gh->getHealingPower();
|
||||
float dodgePct = gh->getDodgePct();
|
||||
float parryPct = gh->getParryPct();
|
||||
float blockPct = gh->getBlockPct();
|
||||
float critPct = gh->getCritPct();
|
||||
float rCritPct = gh->getRangedCritPct();
|
||||
float sCritPct = gh->getSpellCritPct(1); // Holy school (avg proxy for spell crit)
|
||||
// Hit rating: CR_HIT_MELEE=5, CR_HIT_RANGED=6, CR_HIT_SPELL=7
|
||||
// Haste rating: CR_HASTE_MELEE=17, CR_HASTE_RANGED=18, CR_HASTE_SPELL=19
|
||||
// Other: CR_EXPERTISE=23, CR_ARMOR_PENETRATION=24, CR_CRIT_TAKEN_MELEE=14
|
||||
int32_t hitRating = gh->getCombatRating(5);
|
||||
int32_t hitRangedR = gh->getCombatRating(6);
|
||||
int32_t hitSpellR = gh->getCombatRating(7);
|
||||
int32_t expertiseR = gh->getCombatRating(23);
|
||||
int32_t hasteR = gh->getCombatRating(17);
|
||||
int32_t hasteRangedR = gh->getCombatRating(18);
|
||||
int32_t hasteSpellR = gh->getCombatRating(19);
|
||||
int32_t armorPenR = gh->getCombatRating(24);
|
||||
int32_t resilR = gh->getCombatRating(14); // CR_CRIT_TAKEN_MELEE = Resilience
|
||||
|
||||
bool hasAny = (meleeAP >= 0 || spellPow >= 0 || dodgePct >= 0.0f || parryPct >= 0.0f ||
|
||||
blockPct >= 0.0f || critPct >= 0.0f || hitRating >= 0);
|
||||
if (hasAny) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Combat");
|
||||
ImVec4 cyan(0.5f, 0.9f, 1.0f, 1.0f);
|
||||
if (meleeAP >= 0) ImGui::TextColored(cyan, "Attack Power: %d", meleeAP);
|
||||
if (rangedAP >= 0 && rangedAP != meleeAP)
|
||||
ImGui::TextColored(cyan, "Ranged Attack Power: %d", rangedAP);
|
||||
if (spellPow >= 0) ImGui::TextColored(cyan, "Spell Power: %d", spellPow);
|
||||
if (healPow >= 0 && healPow != spellPow)
|
||||
ImGui::TextColored(cyan, "Healing Power: %d", healPow);
|
||||
if (dodgePct >= 0.0f) ImGui::TextColored(cyan, "Dodge: %.2f%%", dodgePct);
|
||||
if (parryPct >= 0.0f) ImGui::TextColored(cyan, "Parry: %.2f%%", parryPct);
|
||||
if (blockPct >= 0.0f) ImGui::TextColored(cyan, "Block: %.2f%%", blockPct);
|
||||
if (critPct >= 0.0f) ImGui::TextColored(cyan, "Melee Crit: %.2f%%", critPct);
|
||||
if (rCritPct >= 0.0f) ImGui::TextColored(cyan, "Ranged Crit: %.2f%%", rCritPct);
|
||||
if (sCritPct >= 0.0f) ImGui::TextColored(cyan, "Spell Crit: %.2f%%", sCritPct);
|
||||
|
||||
// Combat ratings with percentage conversion (WotLK level-80 divisors scaled by level).
|
||||
// Formula: pct = rating / (divisorAt80 * pow(level/80.0, 0.93))
|
||||
// Level-80 divisors derived from gtCombatRatings.dbc (well-known WotLK constants):
|
||||
// Hit: 26.23, Expertise: 8.19/expertise (0.25% each),
|
||||
// Haste: 32.79, ArmorPen: 13.99, Resilience: 94.27
|
||||
uint32_t level = playerLevel > 0 ? playerLevel : gh->getPlayerLevel();
|
||||
if (level == 0) level = 80;
|
||||
double lvlScale = level <= 80
|
||||
? std::pow(static_cast<double>(level) / 80.0, 0.93)
|
||||
: 1.0;
|
||||
|
||||
auto ratingPct = [&](int32_t rating, double divisorAt80) -> float {
|
||||
if (rating < 0 || divisorAt80 <= 0.0) return -1.0f;
|
||||
double d = divisorAt80 * lvlScale;
|
||||
return static_cast<float>(rating / d);
|
||||
};
|
||||
|
||||
if (hitRating >= 0) {
|
||||
float pct = ratingPct(hitRating, 26.23);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Hit Rating: %d (%.2f%%)", hitRating, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Hit Rating: %d", hitRating);
|
||||
}
|
||||
// Show ranged/spell hit only when they differ from melee hit
|
||||
if (hitRangedR >= 0 && hitRangedR != hitRating) {
|
||||
float pct = ratingPct(hitRangedR, 26.23);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Ranged Hit Rating: %d (%.2f%%)", hitRangedR, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Ranged Hit Rating: %d", hitRangedR);
|
||||
}
|
||||
if (hitSpellR >= 0 && hitSpellR != hitRating) {
|
||||
// Spell hit cap at 17% (446 rating at 80); divisor same as melee hit
|
||||
float pct = ratingPct(hitSpellR, 26.23);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Spell Hit Rating: %d (%.2f%%)", hitSpellR, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Spell Hit Rating: %d", hitSpellR);
|
||||
}
|
||||
if (expertiseR >= 0) {
|
||||
// Each expertise point reduces dodge and parry chance by 0.25%
|
||||
// expertise_points = rating / 8.19
|
||||
float exp_pts = ratingPct(expertiseR, 8.19);
|
||||
if (exp_pts >= 0.0f) {
|
||||
float exp_pct = exp_pts * 0.25f; // % dodge/parry reduction
|
||||
ImGui::TextColored(cyan, "Expertise: %d (%.1f / %.2f%%)",
|
||||
expertiseR, exp_pts, exp_pct);
|
||||
} else {
|
||||
ImGui::TextColored(cyan, "Expertise Rating: %d", expertiseR);
|
||||
}
|
||||
}
|
||||
if (hasteR >= 0) {
|
||||
float pct = ratingPct(hasteR, 32.79);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Haste Rating: %d (%.2f%%)", hasteR, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Haste Rating: %d", hasteR);
|
||||
}
|
||||
if (hasteRangedR >= 0 && hasteRangedR != hasteR) {
|
||||
float pct = ratingPct(hasteRangedR, 32.79);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Ranged Haste Rating: %d (%.2f%%)", hasteRangedR, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Ranged Haste Rating: %d", hasteRangedR);
|
||||
}
|
||||
if (hasteSpellR >= 0 && hasteSpellR != hasteR) {
|
||||
float pct = ratingPct(hasteSpellR, 32.79);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Spell Haste Rating: %d (%.2f%%)", hasteSpellR, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Spell Haste Rating: %d", hasteSpellR);
|
||||
}
|
||||
if (armorPenR >= 0) {
|
||||
float pct = ratingPct(armorPenR, 13.99);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Armor Pen: %d (%.2f%%)", armorPenR, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Armor Penetration: %d", armorPenR);
|
||||
}
|
||||
if (resilR >= 0) {
|
||||
// Resilience: reduces crit chance against you by pct%, and crit damage by 2*pct%
|
||||
float pct = ratingPct(resilR, 94.27);
|
||||
if (pct >= 0.0f)
|
||||
ImGui::TextColored(cyan, "Resilience: %d (%.2f%%)", resilR, pct);
|
||||
else
|
||||
ImGui::TextColored(cyan, "Resilience: %d", resilR);
|
||||
}
|
||||
}
|
||||
|
||||
// Movement speeds (always show when non-default)
|
||||
{
|
||||
constexpr float kBaseRun = 7.0f;
|
||||
constexpr float kBaseFlight = 7.0f;
|
||||
float runSpeed = gh->getServerRunSpeed();
|
||||
float flightSpeed = gh->getServerFlightSpeed();
|
||||
float swimSpeed = gh->getServerSwimSpeed();
|
||||
|
||||
bool showRun = runSpeed > 0.0f && std::fabs(runSpeed - kBaseRun) > 0.05f;
|
||||
bool showFlight = flightSpeed > 0.0f && std::fabs(flightSpeed - kBaseFlight) > 0.05f;
|
||||
bool showSwim = swimSpeed > 0.0f && std::fabs(swimSpeed - 4.722f) > 0.05f;
|
||||
|
||||
if (showRun || showFlight || showSwim) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Movement");
|
||||
ImVec4 speedColor(0.6f, 1.0f, 0.8f, 1.0f);
|
||||
if (showRun) {
|
||||
float pct = (runSpeed / kBaseRun) * 100.0f;
|
||||
ImGui::TextColored(speedColor, "Run Speed: %.1f%%", pct);
|
||||
}
|
||||
if (showFlight) {
|
||||
float pct = (flightSpeed / kBaseFlight) * 100.0f;
|
||||
ImGui::TextColored(speedColor, "Flight Speed: %.1f%%", pct);
|
||||
}
|
||||
if (showSwim) {
|
||||
float pct = (swimSpeed / 4.722f) * 100.0f;
|
||||
ImGui::TextColored(speedColor, "Swim Speed: %.1f%%", pct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections) {
|
||||
|
|
@ -2080,6 +2287,8 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
case game::ItemQuality::RARE: qualHex = "0070dd"; break;
|
||||
case game::ItemQuality::EPIC: qualHex = "a335ee"; break;
|
||||
case game::ItemQuality::LEGENDARY: qualHex = "ff8000"; break;
|
||||
case game::ItemQuality::ARTIFACT: qualHex = "e6cc80"; break;
|
||||
case game::ItemQuality::HEIRLOOM: qualHex = "e6cc80"; break;
|
||||
default: break;
|
||||
}
|
||||
char linkBuf[512];
|
||||
|
|
@ -2106,6 +2315,23 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", item.itemLevel);
|
||||
}
|
||||
|
||||
// Heroic / Unique / Unique-Equipped indicators
|
||||
if (gameHandler_) {
|
||||
const auto* qi = gameHandler_->getItemInfo(item.itemId);
|
||||
if (qi && qi->valid) {
|
||||
constexpr uint32_t kFlagHeroic = 0x8;
|
||||
constexpr uint32_t kFlagUniqueEquipped = 0x1000000;
|
||||
if (qi->itemFlags & kFlagHeroic) {
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic");
|
||||
}
|
||||
if (qi->maxCount == 1) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique");
|
||||
} else if (qi->itemFlags & kFlagUniqueEquipped) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Binding type
|
||||
switch (item.bindType) {
|
||||
case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break;
|
||||
|
|
@ -2119,16 +2345,24 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
uint32_t mapId = 0;
|
||||
glm::vec3 pos;
|
||||
if (gameHandler_->getHomeBind(mapId, pos)) {
|
||||
const char* mapName = "Unknown";
|
||||
switch (mapId) {
|
||||
case 0: mapName = "Eastern Kingdoms"; break;
|
||||
case 1: mapName = "Kalimdor"; break;
|
||||
case 530: mapName = "Outland"; break;
|
||||
case 571: mapName = "Northrend"; break;
|
||||
case 13: mapName = "Test"; break;
|
||||
case 169: mapName = "Emerald Dream"; break;
|
||||
std::string homeLocation;
|
||||
// Prefer the specific zone name from the bind-point zone ID
|
||||
uint32_t zoneId = gameHandler_->getHomeBindZoneId();
|
||||
if (zoneId != 0)
|
||||
homeLocation = gameHandler_->getWhoAreaName(zoneId);
|
||||
// Fall back to continent name if zone unavailable
|
||||
if (homeLocation.empty()) {
|
||||
switch (mapId) {
|
||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
||||
case 1: homeLocation = "Kalimdor"; break;
|
||||
case 530: homeLocation = "Outland"; break;
|
||||
case 571: homeLocation = "Northrend"; break;
|
||||
case 13: homeLocation = "Test"; break;
|
||||
case 169: homeLocation = "Emerald Dream"; break;
|
||||
default: homeLocation = "Unknown"; break;
|
||||
}
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", homeLocation.c_str());
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Home: not set");
|
||||
}
|
||||
|
|
@ -2206,6 +2440,21 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
ImGui::Text("%d Armor", item.armor);
|
||||
}
|
||||
|
||||
// Elemental resistances from item query cache (fire resist gear, nature resist gear, etc.)
|
||||
if (gameHandler_) {
|
||||
const auto* qi = gameHandler_->getItemInfo(item.itemId);
|
||||
if (qi && qi->valid) {
|
||||
const int32_t resValsI[6] = { qi->holyRes, qi->fireRes, qi->natureRes,
|
||||
qi->frostRes, qi->shadowRes, qi->arcaneRes };
|
||||
static const char* resLabelsI[6] = {
|
||||
"Holy Resistance", "Fire Resistance", "Nature Resistance",
|
||||
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
|
||||
};
|
||||
for (int i = 0; i < 6; ++i)
|
||||
if (resValsI[i] > 0) ImGui::Text("+%d %s", resValsI[i], resLabelsI[i]);
|
||||
}
|
||||
}
|
||||
|
||||
auto appendBonus = [](std::string& out, int32_t val, const char* shortName) {
|
||||
if (val <= 0) return;
|
||||
if (!out.empty()) out += " ";
|
||||
|
|
@ -2290,10 +2539,12 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
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;
|
||||
case 6: trigger = "Soulstone"; break;
|
||||
case 0: trigger = "Use"; break; // on use
|
||||
case 1: trigger = "Equip"; break; // on equip
|
||||
case 2: trigger = "Chance on Hit"; break; // proc on melee hit
|
||||
case 4: trigger = "Use"; break; // soulstone (still shows as Use)
|
||||
case 5: trigger = "Use"; break; // on use, no delay
|
||||
case 6: trigger = "Use"; break; // learn spell (recipe/pattern)
|
||||
default: break;
|
||||
}
|
||||
if (!trigger) continue;
|
||||
|
|
@ -2312,6 +2563,258 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
}
|
||||
}
|
||||
|
||||
// Skill / reputation requirements from item query cache
|
||||
if (gameHandler_) {
|
||||
const auto* qInfo = gameHandler_->getItemInfo(item.itemId);
|
||||
if (qInfo && qInfo->valid) {
|
||||
if (qInfo->requiredSkill != 0 && qInfo->requiredSkillRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_skillNamesB;
|
||||
static bool s_skillNamesLoadedB = false;
|
||||
if (!s_skillNamesLoadedB && assetManager_) {
|
||||
s_skillNamesLoadedB = true;
|
||||
auto dbc = assetManager_->loadDBC("SkillLine.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 2;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t sid = dbc->getUInt32(r, idF);
|
||||
if (!sid) continue;
|
||||
std::string sname = dbc->getString(r, nameF);
|
||||
if (!sname.empty()) s_skillNamesB[sid] = std::move(sname);
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t playerSkillVal = 0;
|
||||
const auto& skills = gameHandler_->getPlayerSkills();
|
||||
auto skPit = skills.find(qInfo->requiredSkill);
|
||||
if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue();
|
||||
bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= qInfo->requiredSkillRank);
|
||||
ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
||||
auto skIt = s_skillNamesB.find(qInfo->requiredSkill);
|
||||
if (skIt != s_skillNamesB.end())
|
||||
ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), qInfo->requiredSkillRank);
|
||||
else
|
||||
ImGui::TextColored(skColor, "Requires Skill %u (%u)", qInfo->requiredSkill, qInfo->requiredSkillRank);
|
||||
}
|
||||
if (qInfo->requiredReputationFaction != 0 && qInfo->requiredReputationRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_factionNamesB;
|
||||
static bool s_factionNamesLoadedB = false;
|
||||
if (!s_factionNamesLoadedB && assetManager_) {
|
||||
s_factionNamesLoadedB = true;
|
||||
auto dbc = assetManager_->loadDBC("Faction.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 20;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t fid = dbc->getUInt32(r, idF);
|
||||
if (!fid) continue;
|
||||
std::string fname = dbc->getString(r, nameF);
|
||||
if (!fname.empty()) s_factionNamesB[fid] = std::move(fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
static const char* kRepRankNamesB[] = {
|
||||
"Hated","Hostile","Unfriendly","Neutral","Friendly","Honored","Revered","Exalted"
|
||||
};
|
||||
const char* rankName = (qInfo->requiredReputationRank < 8)
|
||||
? kRepRankNamesB[qInfo->requiredReputationRank] : "Unknown";
|
||||
auto fIt = s_factionNamesB.find(qInfo->requiredReputationFaction);
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s",
|
||||
rankName,
|
||||
fIt != s_factionNamesB.end() ? fIt->second.c_str() : "Unknown Faction");
|
||||
}
|
||||
// Class restriction
|
||||
if (qInfo->allowableClass != 0) {
|
||||
static const struct { uint32_t mask; const char* name; } kClassesB[] = {
|
||||
{ 1,"Warrior" },{ 2,"Paladin" },{ 4,"Hunter" },{ 8,"Rogue" },
|
||||
{ 16,"Priest" },{ 32,"Death Knight" },{ 64,"Shaman" },
|
||||
{ 128,"Mage" },{ 256,"Warlock" },{ 1024,"Druid" },
|
||||
};
|
||||
int mc = 0;
|
||||
for (const auto& kc : kClassesB) if (qInfo->allowableClass & kc.mask) ++mc;
|
||||
if (mc > 0 && mc < 10) {
|
||||
char buf[128] = "Classes: "; bool first = true;
|
||||
for (const auto& kc : kClassesB) {
|
||||
if (!(qInfo->allowableClass & kc.mask)) continue;
|
||||
if (!first) strncat(buf, ", ", sizeof(buf)-strlen(buf)-1);
|
||||
strncat(buf, kc.name, sizeof(buf)-strlen(buf)-1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pc = gameHandler_->getPlayerClass();
|
||||
uint32_t pm = (pc > 0 && pc <= 10) ? (1u << (pc-1)) : 0;
|
||||
bool ok = (pm == 0 || (qInfo->allowableClass & pm));
|
||||
ImGui::TextColored(ok ? ImVec4(1,1,1,0.75f) : ImVec4(1,0.5f,0.5f,1), "%s", buf);
|
||||
}
|
||||
}
|
||||
// Race restriction
|
||||
if (qInfo->allowableRace != 0) {
|
||||
static const struct { uint32_t mask; const char* name; } kRacesB[] = {
|
||||
{ 1,"Human" },{ 2,"Orc" },{ 4,"Dwarf" },{ 8,"Night Elf" },
|
||||
{ 16,"Undead" },{ 32,"Tauren" },{ 64,"Gnome" },{ 128,"Troll" },
|
||||
{ 512,"Blood Elf" },{ 1024,"Draenei" },
|
||||
};
|
||||
constexpr uint32_t kAll = 1|2|4|8|16|32|64|128|512|1024;
|
||||
if ((qInfo->allowableRace & kAll) != kAll) {
|
||||
int mc = 0;
|
||||
for (const auto& kr : kRacesB) if (qInfo->allowableRace & kr.mask) ++mc;
|
||||
if (mc > 0) {
|
||||
char buf[160] = "Races: "; bool first = true;
|
||||
for (const auto& kr : kRacesB) {
|
||||
if (!(qInfo->allowableRace & kr.mask)) continue;
|
||||
if (!first) strncat(buf, ", ", sizeof(buf)-strlen(buf)-1);
|
||||
strncat(buf, kr.name, sizeof(buf)-strlen(buf)-1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pr = gameHandler_->getPlayerRace();
|
||||
uint32_t pm = (pr > 0 && pr <= 11) ? (1u << (pr-1)) : 0;
|
||||
bool ok = (pm == 0 || (qInfo->allowableRace & pm));
|
||||
ImGui::TextColored(ok ? ImVec4(1,1,1,0.75f) : ImVec4(1,0.5f,0.5f,1), "%s", buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gem socket slots and item set — look up from query cache
|
||||
if (gameHandler_) {
|
||||
const auto* qi2 = gameHandler_->getItemInfo(item.itemId);
|
||||
if (qi2 && qi2->valid) {
|
||||
// Gem sockets
|
||||
{
|
||||
static const struct { uint32_t mask; const char* label; ImVec4 col; } kSocketTypes[] = {
|
||||
{ 1, "Meta Socket", { 0.7f, 0.7f, 0.9f, 1.0f } },
|
||||
{ 2, "Red Socket", { 1.0f, 0.3f, 0.3f, 1.0f } },
|
||||
{ 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } },
|
||||
{ 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } },
|
||||
};
|
||||
bool hasSocket = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (qi2->socketColor[i] == 0) continue;
|
||||
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
|
||||
for (const auto& st : kSocketTypes) {
|
||||
if (qi2->socketColor[i] & st.mask) {
|
||||
ImGui::TextColored(st.col, "%s", st.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasSocket && qi2->socketBonus != 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_enchantNamesD;
|
||||
static bool s_enchantNamesLoadedD = false;
|
||||
if (!s_enchantNamesLoadedD && assetManager_) {
|
||||
s_enchantNamesLoadedD = true;
|
||||
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* lay = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
|
||||
uint32_t nameField = lay ? lay->field("Name") : 8u;
|
||||
if (nameField == 0xFFFFFFFF) nameField = 8;
|
||||
uint32_t fc = dbc->getFieldCount();
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t eid = dbc->getUInt32(r, 0);
|
||||
if (eid == 0 || nameField >= fc) continue;
|
||||
std::string ename = dbc->getString(r, nameField);
|
||||
if (!ename.empty()) s_enchantNamesD[eid] = std::move(ename);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto enchIt = s_enchantNamesD.find(qi2->socketBonus);
|
||||
if (enchIt != s_enchantNamesD.end())
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", qi2->socketBonus);
|
||||
}
|
||||
}
|
||||
// Item set membership
|
||||
if (qi2->itemSetId != 0) {
|
||||
struct SetEntryD {
|
||||
std::string name;
|
||||
std::array<uint32_t, 10> itemIds{};
|
||||
std::array<uint32_t, 10> spellIds{};
|
||||
std::array<uint32_t, 10> thresholds{};
|
||||
};
|
||||
static std::unordered_map<uint32_t, SetEntryD> s_setDataD;
|
||||
static bool s_setDataLoadedD = false;
|
||||
if (!s_setDataLoadedD && assetManager_) {
|
||||
s_setDataLoadedD = true;
|
||||
auto dbc = assetManager_->loadDBC("ItemSet.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr;
|
||||
auto lf = [&](const char* k, uint32_t def) -> uint32_t {
|
||||
return layout ? (*layout)[k] : def;
|
||||
};
|
||||
uint32_t idF = lf("ID", 0), nameF = lf("Name", 1);
|
||||
static const char* itemKeys[10] = {
|
||||
"Item0","Item1","Item2","Item3","Item4",
|
||||
"Item5","Item6","Item7","Item8","Item9" };
|
||||
static const char* spellKeys[10] = {
|
||||
"Spell0","Spell1","Spell2","Spell3","Spell4",
|
||||
"Spell5","Spell6","Spell7","Spell8","Spell9" };
|
||||
static const char* thrKeys[10] = {
|
||||
"Threshold0","Threshold1","Threshold2","Threshold3","Threshold4",
|
||||
"Threshold5","Threshold6","Threshold7","Threshold8","Threshold9" };
|
||||
uint32_t itemFB[10], spellFB[10], thrFB[10];
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
itemFB[i] = 18+i; spellFB[i] = 28+i; thrFB[i] = 38+i;
|
||||
}
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t id = dbc->getUInt32(r, idF);
|
||||
if (!id) continue;
|
||||
SetEntryD e;
|
||||
e.name = dbc->getString(r, nameF);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : itemFB[i]);
|
||||
e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : spellFB[i]);
|
||||
e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : thrFB[i]);
|
||||
}
|
||||
s_setDataD[id] = std::move(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto setIt = s_setDataD.find(qi2->itemSetId);
|
||||
ImGui::Spacing();
|
||||
if (setIt != s_setDataD.end()) {
|
||||
const SetEntryD& se = setIt->second;
|
||||
int equipped = 0, total = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.itemIds[i] == 0) continue;
|
||||
++total;
|
||||
if (inventory) {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& eSlot = inventory->getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (!eSlot.empty() && eSlot.item.itemId == se.itemIds[i]) { ++equipped; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (total > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f),
|
||||
"%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total);
|
||||
} else if (!se.name.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "%s", se.name.c_str());
|
||||
}
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue;
|
||||
const std::string& bname = gameHandler_->getSpellName(se.spellIds[i]);
|
||||
bool active = (equipped >= static_cast<int>(se.thresholds[i]));
|
||||
ImVec4 col = active ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f)
|
||||
: ImVec4(0.55f, 0.55f, 0.55f, 1.0f);
|
||||
if (!bname.empty())
|
||||
ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str());
|
||||
else
|
||||
ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Set (id %u)", qi2->itemSetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "Begins a Quest" line (shown in yellow-green like the game)
|
||||
if (item.startQuestId != 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Begins a Quest");
|
||||
|
|
@ -2460,6 +2963,18 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
|
|||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", info.itemLevel);
|
||||
}
|
||||
|
||||
// Unique / Heroic indicators
|
||||
constexpr uint32_t kFlagHeroic = 0x8; // ITEM_FLAG_HEROIC_TOOLTIP
|
||||
constexpr uint32_t kFlagUniqueEquipped = 0x1000000; // ITEM_FLAG_UNIQUE_EQUIPPABLE
|
||||
if (info.itemFlags & kFlagHeroic) {
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic");
|
||||
}
|
||||
if (info.maxCount == 1) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique");
|
||||
} else if (info.itemFlags & kFlagUniqueEquipped) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped");
|
||||
}
|
||||
|
||||
// Binding type
|
||||
switch (info.bindType) {
|
||||
case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break;
|
||||
|
|
@ -2524,6 +3039,18 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
|
|||
|
||||
if (info.armor > 0) ImGui::Text("%d Armor", info.armor);
|
||||
|
||||
// Elemental resistances (fire resist gear, nature resist gear, etc.)
|
||||
{
|
||||
const int32_t resVals[6] = { info.holyRes, info.fireRes, info.natureRes,
|
||||
info.frostRes, info.shadowRes, info.arcaneRes };
|
||||
static const char* resLabels[6] = {
|
||||
"Holy Resistance", "Fire Resistance", "Nature Resistance",
|
||||
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
|
||||
};
|
||||
for (int i = 0; i < 6; ++i)
|
||||
if (resVals[i] > 0) ImGui::Text("+%d %s", resVals[i], resLabels[i]);
|
||||
}
|
||||
|
||||
auto appendBonus = [](std::string& out, int32_t val, const char* name) {
|
||||
if (val <= 0) return;
|
||||
if (!out.empty()) out += " ";
|
||||
|
|
@ -2576,14 +3103,166 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
|
|||
ImGui::TextColored(reqColor, "Requires Level %u", info.requiredLevel);
|
||||
}
|
||||
|
||||
// Required skill (e.g. "Requires Engineering (300)")
|
||||
if (info.requiredSkill != 0 && info.requiredSkillRank > 0) {
|
||||
// Lazy-load SkillLine.dbc names
|
||||
static std::unordered_map<uint32_t, std::string> s_skillNames;
|
||||
static bool s_skillNamesLoaded = false;
|
||||
if (!s_skillNamesLoaded && assetManager_) {
|
||||
s_skillNamesLoaded = true;
|
||||
auto dbc = assetManager_->loadDBC("SkillLine.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 2;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t sid = dbc->getUInt32(r, idF);
|
||||
if (!sid) continue;
|
||||
std::string sname = dbc->getString(r, nameF);
|
||||
if (!sname.empty()) s_skillNames[sid] = std::move(sname);
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t playerSkillVal = 0;
|
||||
if (gameHandler_) {
|
||||
const auto& skills = gameHandler_->getPlayerSkills();
|
||||
auto skPit = skills.find(info.requiredSkill);
|
||||
if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue();
|
||||
}
|
||||
bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info.requiredSkillRank);
|
||||
ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
||||
auto skIt = s_skillNames.find(info.requiredSkill);
|
||||
if (skIt != s_skillNames.end())
|
||||
ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info.requiredSkillRank);
|
||||
else
|
||||
ImGui::TextColored(skColor, "Requires Skill %u (%u)", info.requiredSkill, info.requiredSkillRank);
|
||||
}
|
||||
|
||||
// Required reputation (e.g. "Requires Exalted with Argent Dawn")
|
||||
if (info.requiredReputationFaction != 0 && info.requiredReputationRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_factionNames;
|
||||
static bool s_factionNamesLoaded = false;
|
||||
if (!s_factionNamesLoaded && assetManager_) {
|
||||
s_factionNamesLoaded = true;
|
||||
auto dbc = assetManager_->loadDBC("Faction.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 20;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t fid = dbc->getUInt32(r, idF);
|
||||
if (!fid) continue;
|
||||
std::string fname = dbc->getString(r, nameF);
|
||||
if (!fname.empty()) s_factionNames[fid] = std::move(fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
static const char* kRepRankNames[] = {
|
||||
"Hated", "Hostile", "Unfriendly", "Neutral",
|
||||
"Friendly", "Honored", "Revered", "Exalted"
|
||||
};
|
||||
const char* rankName = (info.requiredReputationRank < 8)
|
||||
? kRepRankNames[info.requiredReputationRank] : "Unknown";
|
||||
auto fIt = s_factionNames.find(info.requiredReputationFaction);
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s",
|
||||
rankName,
|
||||
fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction");
|
||||
}
|
||||
|
||||
// Class restriction (e.g. "Classes: Paladin, Warrior")
|
||||
if (info.allowableClass != 0) {
|
||||
static const struct { uint32_t mask; const char* name; } kClasses[] = {
|
||||
{ 1, "Warrior" },
|
||||
{ 2, "Paladin" },
|
||||
{ 4, "Hunter" },
|
||||
{ 8, "Rogue" },
|
||||
{ 16, "Priest" },
|
||||
{ 32, "Death Knight" },
|
||||
{ 64, "Shaman" },
|
||||
{ 128, "Mage" },
|
||||
{ 256, "Warlock" },
|
||||
{ 1024, "Druid" },
|
||||
};
|
||||
// Count matching classes
|
||||
int matchCount = 0;
|
||||
for (const auto& kc : kClasses)
|
||||
if (info.allowableClass & kc.mask) ++matchCount;
|
||||
// Only show if restricted to a subset (not all classes)
|
||||
if (matchCount > 0 && matchCount < 10) {
|
||||
char classBuf[128] = "Classes: ";
|
||||
bool first = true;
|
||||
for (const auto& kc : kClasses) {
|
||||
if (!(info.allowableClass & kc.mask)) continue;
|
||||
if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
// Check if player's class is allowed
|
||||
bool playerAllowed = true;
|
||||
if (gameHandler_) {
|
||||
uint8_t pc = gameHandler_->getPlayerClass();
|
||||
uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0;
|
||||
playerAllowed = (pmask == 0 || (info.allowableClass & pmask));
|
||||
}
|
||||
ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
||||
ImGui::TextColored(clColor, "%s", classBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// Race restriction (e.g. "Races: Night Elf, Human")
|
||||
if (info.allowableRace != 0) {
|
||||
static const struct { uint32_t mask; const char* name; } kRaces[] = {
|
||||
{ 1, "Human" },
|
||||
{ 2, "Orc" },
|
||||
{ 4, "Dwarf" },
|
||||
{ 8, "Night Elf" },
|
||||
{ 16, "Undead" },
|
||||
{ 32, "Tauren" },
|
||||
{ 64, "Gnome" },
|
||||
{ 128, "Troll" },
|
||||
{ 512, "Blood Elf" },
|
||||
{ 1024, "Draenei" },
|
||||
};
|
||||
constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024;
|
||||
// Only show if not all playable races are allowed
|
||||
if ((info.allowableRace & kAllPlayable) != kAllPlayable) {
|
||||
int matchCount = 0;
|
||||
for (const auto& kr : kRaces)
|
||||
if (info.allowableRace & kr.mask) ++matchCount;
|
||||
if (matchCount > 0) {
|
||||
char raceBuf[160] = "Races: ";
|
||||
bool first = true;
|
||||
for (const auto& kr : kRaces) {
|
||||
if (!(info.allowableRace & kr.mask)) continue;
|
||||
if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
bool playerAllowed = true;
|
||||
if (gameHandler_) {
|
||||
uint8_t pr = gameHandler_->getPlayerRace();
|
||||
uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0;
|
||||
playerAllowed = (pmask == 0 || (info.allowableRace & pmask));
|
||||
}
|
||||
ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
||||
ImGui::TextColored(rColor, "%s", raceBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
case 0: trigger = "Use"; break; // on use
|
||||
case 1: trigger = "Equip"; break; // on equip
|
||||
case 2: trigger = "Chance on Hit"; break; // proc on melee hit
|
||||
case 4: trigger = "Use"; break; // soulstone (still shows as Use)
|
||||
case 5: trigger = "Use"; break; // on use, no delay
|
||||
case 6: trigger = "Use"; break; // learn spell (recipe/pattern)
|
||||
default: break;
|
||||
}
|
||||
if (!trigger) continue;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue