diff --git a/Data/expansions/classic/dbc_layouts.json b/Data/expansions/classic/dbc_layouts.json index 5c550c81..ca8c8a50 100644 --- a/Data/expansions/classic/dbc_layouts.json +++ b/Data/expansions/classic/dbc_layouts.json @@ -2,8 +2,7 @@ "Spell": { "ID": 0, "Attributes": 5, "IconID": 117, "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, - "DispelType": 4 + "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33 }, "SpellRange": { "MaxRange": 2 }, "ItemDisplayInfo": { diff --git a/Data/expansions/tbc/dbc_layouts.json b/Data/expansions/tbc/dbc_layouts.json index b99159a6..fdc9e07d 100644 --- a/Data/expansions/tbc/dbc_layouts.json +++ b/Data/expansions/tbc/dbc_layouts.json @@ -2,8 +2,7 @@ "Spell": { "ID": 0, "Attributes": 5, "IconID": 124, "Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215, - "CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40, - "DispelType": 3 + "CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40 }, "SpellRange": { "MaxRange": 4 }, "ItemDisplayInfo": { diff --git a/Data/expansions/turtle/dbc_layouts.json b/Data/expansions/turtle/dbc_layouts.json index 3b06971d..a2482e0d 100644 --- a/Data/expansions/turtle/dbc_layouts.json +++ b/Data/expansions/turtle/dbc_layouts.json @@ -2,8 +2,7 @@ "Spell": { "ID": 0, "Attributes": 5, "IconID": 117, "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, - "DispelType": 4 + "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33 }, "SpellRange": { "MaxRange": 2 }, "ItemDisplayInfo": { diff --git a/Data/expansions/wotlk/dbc_layouts.json b/Data/expansions/wotlk/dbc_layouts.json index 94f7df4d..0d1667a1 100644 --- a/Data/expansions/wotlk/dbc_layouts.json +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -2,8 +2,7 @@ "Spell": { "ID": 0, "Attributes": 4, "IconID": 133, "Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225, - "PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49, - "DispelType": 2 + "PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49 }, "SpellRange": { "MaxRange": 4 }, "ItemDisplayInfo": { diff --git a/include/audio/ui_sound_manager.hpp b/include/audio/ui_sound_manager.hpp index 6423d460..241014ae 100644 --- a/include/audio/ui_sound_manager.hpp +++ b/include/audio/ui_sound_manager.hpp @@ -75,9 +75,6 @@ public: void playTargetSelect(); void playTargetDeselect(); - // Chat notifications - void playWhisperReceived(); - private: struct UISample { std::string path; @@ -125,7 +122,6 @@ private: std::vector errorSounds_; std::vector selectTargetSounds_; std::vector deselectTargetSounds_; - std::vector whisperSounds_; // State tracking float volumeScale_ = 1.0f; diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 57147902..9f4dfde7 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -214,9 +214,6 @@ public: void setMaxPower(uint32_t p) { maxPowers[powerType < 7 ? powerType : 0] = p; } void setMaxPowerByType(uint8_t type, uint32_t p) { if (type < 7) maxPowers[type] = p; } - uint32_t getPowerByType(uint8_t type) const { return type < 7 ? powers[type] : 0; } - uint32_t getMaxPowerByType(uint8_t type) const { return type < 7 ? maxPowers[type] : 0; } - uint8_t getPowerType() const { return powerType; } void setPowerType(uint8_t t) { powerType = t; } diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 7a11d97b..79460a17 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -339,8 +339,7 @@ public: uint32_t unspentTalents = 0; uint8_t talentGroups = 0; uint8_t activeTalentGroup = 0; - std::array itemEntries{}; // 0=head…18=ranged - std::array enchantIds{}; // permanent enchant per slot (0 = none) + std::array itemEntries{}; // 0=head…18=ranged }; const InspectResult* getInspectResult() const { return inspectResult_.guid ? &inspectResult_ : nullptr; @@ -353,19 +352,6 @@ public: uint32_t getTotalTimePlayed() const { return totalTimePlayed_; } uint32_t getLevelTimePlayed() const { return levelTimePlayed_; } - // Who results (structured, from last SMSG_WHO response) - struct WhoEntry { - std::string name; - std::string guildName; - uint32_t level = 0; - uint32_t classId = 0; - uint32_t raceId = 0; - uint32_t zoneId = 0; - }; - const std::vector& getWhoResults() const { return whoResults_; } - uint32_t getWhoOnlineCount() const { return whoOnlineCount_; } - std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); } - // Social commands void addFriend(const std::string& playerName, const std::string& note = ""); void removeFriend(const std::string& playerName); @@ -393,28 +379,6 @@ public: void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF); const std::array& getBgQueues() const { return bgQueues_; } - // BG scoreboard (MSG_PVP_LOG_DATA) - struct BgPlayerScore { - uint64_t guid = 0; - std::string name; - uint8_t team = 0; // 0=Horde, 1=Alliance - uint32_t killingBlows = 0; - uint32_t deaths = 0; - uint32_t honorableKills = 0; - uint32_t bonusHonor = 0; - std::vector> bgStats; // BG-specific fields - }; - struct BgScoreboardData { - std::vector players; - bool hasWinner = false; - uint8_t winner = 0; // 0=Horde, 1=Alliance - bool isArena = false; - }; - void requestPvpLog(); - const BgScoreboardData* getBgScoreboard() const { - return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_; - } - // Network latency (milliseconds, updated each PONG response) uint32_t getLatencyMs() const { return lastLatency; } @@ -437,7 +401,6 @@ public: // Follow/Assist void followTarget(); - void cancelFollow(); // Stop following current target void assistTarget(); // PvP @@ -494,16 +457,11 @@ public: uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; } // Ready check - struct ReadyCheckResult { - std::string name; - bool ready = false; - }; void initiateReadyCheck(); void respondToReadyCheck(bool ready); bool hasPendingReadyCheck() const { return pendingReadyCheck_; } void dismissReadyCheck() { pendingReadyCheck_ = false; } const std::string& getReadyCheckInitiator() const { return readyCheckInitiator_; } - const std::vector& getReadyCheckResults() const { return readyCheckResults_; } // Duel void forfeitDuel(); @@ -558,19 +516,6 @@ public: const std::vector& getCombatText() const { return combatText; } void updateCombatText(float deltaTime); - // Combat log (persistent rolling history, max MAX_COMBAT_LOG entries) - const std::deque& getCombatLog() const { return combatLog_; } - void clearCombatLog() { combatLog_.clear(); } - - // Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame - bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); } - std::string popAreaTriggerMsg() { - if (areaTriggerMsgs_.empty()) return {}; - std::string msg = areaTriggerMsgs_.front(); - areaTriggerMsgs_.pop_front(); - return msg; - } - // Threat struct ThreatEntry { uint64_t victimGuid = 0; @@ -727,11 +672,6 @@ public: // Auras const std::vector& getPlayerAuras() const { return playerAuras; } const std::vector& getTargetAuras() const { return targetAuras; } - // Per-unit aura cache (populated for party members and any unit we receive updates for) - const std::vector* getUnitAuras(uint64_t guid) const { - auto it = unitAurasCache_.find(guid); - return (it != unitAurasCache_.end()) ? &it->second : nullptr; - } // Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE) bool isQuestCompleted(uint32_t questId) const { return completedQuests_.count(questId) > 0; } @@ -803,17 +743,6 @@ public: float getGameTime() const { return gameTime_; } float getTimeSpeed() const { return timeSpeed_; } - // Global Cooldown (GCD) — set when the server sends a spellId=0 cooldown entry - float getGCDRemaining() const { - if (gcdTotal_ <= 0.0f) return 0.0f; - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - gcdStartedAt_).count() / 1000.0f; - float rem = gcdTotal_ - elapsed; - return rem > 0.0f ? rem : 0.0f; - } - float getGCDTotal() const { return gcdTotal_; } - bool isGCDActive() const { return getGCDRemaining() > 0.0f; } - // Weather state (updated by SMSG_WEATHER) // weatherType: 0=clear, 1=rain, 2=snow, 3=storm/fog uint32_t getWeatherType() const { return weatherType_; } @@ -1106,14 +1035,6 @@ public: const std::string& getDuelChallengerName() const { return duelChallengerName_; } void acceptDuel(); // forfeitDuel() already declared at line ~399 - // Returns remaining duel countdown seconds, or 0 if no active countdown - float getDuelCountdownRemaining() const { - if (duelCountdownMs_ == 0) return 0.0f; - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - duelCountdownStartedAt_).count(); - float rem = (static_cast(duelCountdownMs_) - static_cast(elapsed)) / 1000.0f; - return rem > 0.0f ? rem : 0.0f; - } // ---- Instance lockouts ---- struct InstanceLockout { @@ -1174,12 +1095,10 @@ public: uint32_t getLfgProposalId() const { return lfgProposalId_; } int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; } uint32_t getLfgTimeInQueueMs() const { return lfgTimeInQueueMs_; } - uint32_t getLfgBootVotes() const { return lfgBootVotes_; } - uint32_t getLfgBootTotal() const { return lfgBootTotal_; } - uint32_t getLfgBootTimeLeft() const { return lfgBootTimeLeft_; } - uint32_t getLfgBootNeeded() const { return lfgBootNeeded_; } - const std::string& getLfgBootTargetName() const { return lfgBootTargetName_; } - const std::string& getLfgBootReason() const { return lfgBootReason_; } + uint32_t getLfgBootVotes() const { return lfgBootVotes_; } + uint32_t getLfgBootTotal() const { return lfgBootTotal_; } + uint32_t getLfgBootTimeLeft() const { return lfgBootTimeLeft_; } + uint32_t getLfgBootNeeded() const { return lfgBootNeeded_; } // ---- Arena Team Stats ---- struct ArenaTeamStats { @@ -1210,15 +1129,6 @@ public: uint32_t itemId = 0; std::string itemName; uint8_t itemQuality = 0; - uint32_t rollCountdownMs = 60000; // Duration of roll window in ms - std::chrono::steady_clock::time_point rollStartedAt{}; - - struct PlayerRollResult { - std::string playerName; - uint8_t rollNum = 0; - uint8_t rollType = 0; // 0=need,1=greed,2=disenchant,96=pass - }; - std::vector playerRolls; // live roll results from group members }; bool hasPendingLootRoll() const { return pendingLootRollActive_; } const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; } @@ -1305,7 +1215,6 @@ public: }; const std::vector& getQuestLog() const { return questLog_; } void abandonQuest(uint32_t questId); - void shareQuestWithParty(uint32_t questId); // CMSG_PUSHQUESTTOPARTY bool requestQuestQuery(uint32_t questId, bool force = false); bool isQuestTracked(uint32_t questId) const { return trackedQuestIds_.count(questId) > 0; } void setQuestTracked(uint32_t questId, bool tracked) { @@ -1358,29 +1267,7 @@ public: }; const std::vector& getInitialFactions() const { return initialFactions_; } const std::unordered_map& getFactionStandings() const { return factionStandings_; } - // Shaman totems (4 slots: 0=Earth, 1=Fire, 2=Water, 3=Air) - struct TotemSlot { - uint32_t spellId = 0; - uint32_t durationMs = 0; - std::chrono::steady_clock::time_point placedAt{}; - bool active() const { return spellId != 0 && remainingMs() > 0; } - float remainingMs() const { - if (spellId == 0 || durationMs == 0) return 0.0f; - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - placedAt).count(); - float rem = static_cast(durationMs) - static_cast(elapsed); - return rem > 0.0f ? rem : 0.0f; - } - }; - static constexpr int NUM_TOTEM_SLOTS = 4; - const TotemSlot& getTotemSlot(int slot) const { - static TotemSlot empty; - return (slot >= 0 && slot < NUM_TOTEM_SLOTS) ? activeTotemSlots_[slot] : empty; - } - const std::string& getFactionNamePublic(uint32_t factionId) const; - uint32_t getWatchedFactionId() const { return watchedFactionId_; } - void setWatchedFactionId(uint32_t id) { watchedFactionId_ = id; } uint32_t getLastContactListMask() const { return lastContactListMask_; } uint32_t getLastContactListCount() const { return lastContactListCount_; } bool isServerMovementAllowed() const { return serverMovementAllowed_; } @@ -1410,11 +1297,6 @@ public: void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); } const std::unordered_set& getEarnedAchievements() const { return earnedAchievements_; } const std::unordered_map& getCriteriaProgress() const { return criteriaProgress_; } - /// Returns the WoW PackedTime earn date for an achievement, or 0 if unknown. - uint32_t getAchievementDate(uint32_t id) const { - auto it = achievementDates_.find(id); - return (it != achievementDates_.end()) ? it->second : 0u; - } /// Returns the name of an achievement by ID, or empty string if unknown. const std::string& getAchievementName(uint32_t id) const { auto it = achievementNameCache_.find(id); @@ -1448,10 +1330,6 @@ public: using RepChangeCallback = std::function; void setRepChangeCallback(RepChangeCallback cb) { repChangeCallback_ = std::move(cb); } - // Quest turn-in completion callback - using QuestCompleteCallback = std::function; - void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); } - // Mount state using MountCallback = std::function; // 0 = dismount void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } @@ -1661,8 +1539,6 @@ public: const std::string& getSpellName(uint32_t spellId) const; const std::string& getSpellRank(uint32_t spellId) const; const std::string& getSkillLineName(uint32_t spellId) const; - /// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other) - uint8_t getSpellDispelType(uint32_t spellId) const; struct TrainerTab { std::string name; @@ -1943,7 +1819,6 @@ private: void handleArenaTeamEvent(network::Packet& packet); void handleArenaTeamStats(network::Packet& packet); void handleArenaError(network::Packet& packet); - void handlePvpLogData(network::Packet& packet); // ---- Bank handlers ---- void handleShowBank(network::Packet& packet); @@ -2190,9 +2065,6 @@ private: float autoAttackFacingSyncTimer_ = 0.0f; // Periodic facing sync while meleeing std::unordered_set hostileAttackers_; std::vector combatText; - static constexpr size_t MAX_COMBAT_LOG = 500; - std::deque combatLog_; - std::deque areaTriggerMsgs_; // unitGuid → sorted threat list (descending by threat value) std::unordered_map> threatLists_; @@ -2275,7 +2147,6 @@ private: std::array actionBar{}; std::vector playerAuras; std::vector targetAuras; - std::unordered_map> unitAurasCache_; // per-unit aura cache uint64_t petGuid_ = 0; uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots) uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss @@ -2306,9 +2177,6 @@ private: // Arena team stats (indexed by team slot, updated by SMSG_ARENA_TEAM_STATS) std::vector arenaTeamStats_; - // BG scoreboard (MSG_PVP_LOG_DATA) - BgScoreboardData bgScoreboard_; - // Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT) std::array encounterUnitGuids_ = {}; // 0 = empty slot @@ -2322,15 +2190,12 @@ private: uint32_t lfgBootTotal_ = 0; // total votes cast uint32_t lfgBootTimeLeft_ = 0; // seconds remaining uint32_t lfgBootNeeded_ = 0; // votes needed to kick - std::string lfgBootTargetName_; // name of player being voted on - std::string lfgBootReason_; // reason given for kick // Ready check state bool pendingReadyCheck_ = false; uint32_t readyCheckReadyCount_ = 0; uint32_t readyCheckNotReadyCount_ = 0; std::string readyCheckInitiator_; - std::vector readyCheckResults_; // per-player status live during check // Faction standings (factionId → absolute standing value) std::unordered_map factionStandings_; @@ -2364,10 +2229,6 @@ private: uint32_t totalTimePlayed_ = 0; uint32_t levelTimePlayed_ = 0; - // Who results (last SMSG_WHO response) - std::vector whoResults_; - uint32_t whoOnlineCount_ = 0; - // Trade state TradeStatus tradeStatus_ = TradeStatus::None; uint64_t tradePeerGuid_= 0; @@ -2377,16 +2238,11 @@ private: uint64_t myTradeGold_ = 0; uint64_t peerTradeGold_ = 0; - // Shaman totem state - TotemSlot activeTotemSlots_[NUM_TOTEM_SLOTS]; - // Duel state bool pendingDuelRequest_ = false; uint64_t duelChallengerGuid_= 0; uint64_t duelFlagGuid_ = 0; std::string duelChallengerName_; - uint32_t duelCountdownMs_ = 0; // 0 = no active countdown - std::chrono::steady_clock::time_point duelCountdownStartedAt_{}; // ---- Guild state ---- std::string guildName_; @@ -2578,7 +2434,7 @@ private: // Trainer bool trainerWindowOpen_ = false; TrainerListData currentTrainerList_; - struct SpellNameEntry { std::string name; std::string rank; uint32_t schoolMask = 0; uint8_t dispelType = 0; }; + struct SpellNameEntry { std::string name; std::string rank; uint32_t schoolMask = 0; }; std::unordered_map spellNameCache_; bool spellNameCacheLoaded_ = false; @@ -2588,8 +2444,6 @@ private: void loadAchievementNameCache(); // Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA) std::unordered_set earnedAchievements_; - // Earn dates: achievementId → WoW PackedTime (from SMSG_ACHIEVEMENT_EARNED / SMSG_ALL_ACHIEVEMENT_DATA) - std::unordered_map achievementDates_; // Criteria progress: criteriaId → current value (from SMSG_CRITERIA_UPDATE) std::unordered_map criteriaProgress_; void handleAllAchievementData(network::Packet& packet); @@ -2664,10 +2518,6 @@ private: float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour) void handleLoginSetTimeSpeed(network::Packet& packet); - // ---- Global Cooldown (GCD) ---- - float gcdTotal_ = 0.0f; - std::chrono::steady_clock::time_point gcdStartedAt_{}; - // ---- Weather state (SMSG_WEATHER) ---- uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm float weatherIntensity_ = 0.0f; // 0.0 to 1.0 @@ -2780,10 +2630,6 @@ private: // ---- Reputation change callback ---- RepChangeCallback repChangeCallback_; - uint32_t watchedFactionId_ = 0; // auto-set to most recently changed faction - - // ---- Quest completion callback ---- - QuestCompleteCallback questCompleteCallback_; }; } // namespace game diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index dc38f813..c4d70380 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -52,7 +51,7 @@ struct CombatTextEntry { enum Type : uint8_t { MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, - ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER + ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST }; Type type; int32_t amount = 0; @@ -64,19 +63,6 @@ struct CombatTextEntry { bool isExpired() const { return age >= LIFETIME; } }; -/** - * Persistent combat log entry (stored in a rolling deque, survives beyond floating-text lifetime) - */ -struct CombatLogEntry { - CombatTextEntry::Type type = CombatTextEntry::MELEE_DAMAGE; - int32_t amount = 0; - uint32_t spellId = 0; - bool isPlayerSource = false; - time_t timestamp = 0; // Wall-clock time (std::time(nullptr)) - std::string sourceName; // Resolved display name of attacker/caster - std::string targetName; // Resolved display name of victim/target -}; - /** * Spell cooldown entry received from server */ diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index c2320681..709dc502 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -46,32 +46,21 @@ private: char chatInputBuffer[512] = ""; char whisperTargetBuffer[256] = ""; bool chatInputActive = false; - int selectedChatType = 0; // 0=SAY, 1=YELL, 2=PARTY, 3=GUILD, 4=WHISPER, ..., 10=CHANNEL + int selectedChatType = 0; // 0=SAY, 1=YELL, 2=PARTY, 3=GUILD, 4=WHISPER int lastChatType = 0; // Track chat type changes - int selectedChannelIdx = 0; // Index into joinedChannels_ when selectedChatType==10 bool chatInputMoveCursorToEnd = false; // Chat sent-message history (Up/Down arrow recall) std::vector chatSentHistory_; int chatHistoryIdx_ = -1; // -1 = not browsing history - // Tab-completion state for slash commands - std::string chatTabPrefix_; // prefix captured on first Tab press - std::vector chatTabMatches_; // matching command list - int chatTabMatchIdx_ = -1; // active match index (-1 = inactive) - - // Mention notification: plays a sound when the player's name appears in chat - size_t chatMentionSeenCount_ = 0; // how many messages have been scanned for mentions - // Chat tabs int activeChatTab_ = 0; struct ChatTab { std::string name; - uint64_t typeMask; // bitmask of ChatType values to show (64-bit: types go up to 84) + uint32_t typeMask; // bitmask of ChatType values to show }; std::vector chatTabs_; - std::vector chatTabUnread_; // unread message count per tab (0 = none) - size_t chatTabSeenCount_ = 0; // how many history messages have been processed void initChatTabs(); bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const; @@ -86,7 +75,6 @@ private: uint32_t lastPlayerHp_ = 0; // Previous frame HP for damage flash detection float damageFlashAlpha_ = 0.0f; // Screen edge flash intensity (fades to 0) bool damageFlashEnabled_ = true; - bool lowHealthVignetteEnabled_ = true; // Persistent pulsing red vignette below 20% HP float levelUpFlashAlpha_ = 0.0f; // Golden level-up burst effect (fades to 0) uint32_t levelUpDisplayLevel_ = 0; // Level shown in level-up text @@ -112,29 +100,6 @@ private: std::vector repToasts_; bool repChangeCallbackSet_ = false; static constexpr float kRepToastLifetime = 3.5f; - - // Quest completion toast: slide-in when a quest is turned in - struct QuestCompleteToastEntry { uint32_t questId = 0; std::string title; float age = 0.0f; }; - std::vector questCompleteToasts_; - bool questCompleteCallbackSet_ = false; - static constexpr float kQuestCompleteToastLifetime = 4.0f; - - // Zone entry toast: brief banner when entering a new zone - struct ZoneToastEntry { std::string zoneName; float age = 0.0f; }; - std::vector zoneToasts_; - - struct AreaTriggerToast { std::string text; float age = 0.0f; }; - std::vector areaTriggerToasts_; - void renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler); - std::string lastKnownZone_; - static constexpr float kZoneToastLifetime = 3.0f; - - // Death screen: elapsed time since the death dialog first appeared - float deathElapsed_ = 0.0f; - bool deathTimerRunning_ = false; - // WoW forces release after ~6 minutes; show countdown until then - static constexpr float kForcedReleaseSec = 360.0f; - void renderZoneToasts(float deltaTime); bool showPlayerInfo = false; bool showSocialFrame_ = false; // O key toggles social/friends list bool showGuildRoster_ = false; @@ -290,7 +255,6 @@ private: * Render pet frame (below player frame when player has an active pet) */ void renderPetFrame(game::GameHandler& gameHandler); - void renderTotemFrame(game::GameHandler& gameHandler); /** * Process targeting input (Tab, Escape, click) @@ -311,7 +275,6 @@ private: void renderActionBar(game::GameHandler& gameHandler); void renderBagBar(game::GameHandler& gameHandler); void renderXpBar(game::GameHandler& gameHandler); - void renderRepBar(game::GameHandler& gameHandler); void renderCastBar(game::GameHandler& gameHandler); void renderMirrorTimers(game::GameHandler& gameHandler); void renderCombatText(game::GameHandler& gameHandler); @@ -320,10 +283,8 @@ private: void renderBossFrames(game::GameHandler& gameHandler); void renderUIErrors(game::GameHandler& gameHandler, float deltaTime); void renderRepToasts(float deltaTime); - void renderQuestCompleteToasts(float deltaTime); void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); - void renderDuelCountdown(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); void renderTradeRequestPopup(game::GameHandler& gameHandler); void renderTradeWindow(game::GameHandler& gameHandler); @@ -403,14 +364,6 @@ private: int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none) int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none) - // Who Results window - bool showWhoWindow_ = false; - void renderWhoWindow(game::GameHandler& gameHandler); - - // Combat Log window - bool showCombatLog_ = false; - void renderCombatLog(game::GameHandler& gameHandler); - // Instance Lockouts window bool showInstanceLockouts_ = false; @@ -434,10 +387,6 @@ private: // Threat window bool showThreatWindow_ = false; void renderThreatWindow(game::GameHandler& gameHandler); - - // BG scoreboard window - bool showBgScoreboard_ = false; - void renderBgScoreboard(game::GameHandler& gameHandler); uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps) uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK) @@ -528,9 +477,6 @@ private: bool showDPSMeter_ = false; float dpsCombatAge_ = 0.0f; // seconds in current combat (for accurate early-combat DPS) bool dpsWasInCombat_ = false; - float dpsEncounterDamage_ = 0.0f; // total player damage this combat - float dpsEncounterHeal_ = 0.0f; // total player healing this combat - size_t dpsLogSeenCount_ = 0; // log entries already scanned public: void triggerDing(uint32_t newLevel); diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 31cae856..3453e966 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -96,7 +96,7 @@ private: std::unordered_map iconCache_; public: VkDescriptorSet getItemIcon(uint32_t displayInfoId); - void renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory = nullptr); + void renderItemTooltip(const game::ItemQueryResponseData& info); private: // Character model preview diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 2bc0f866..6cc13270 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -54,16 +54,6 @@ public: uint32_t getDragSpellId() const { return dragSpellId_; } void consumeDragSpell() { draggingSpell_ = false; dragSpellId_ = 0; dragSpellIconTex_ = VK_NULL_HANDLE; } - /// Returns the max range in yards for a spell (0 if self-cast, unknown, or melee). - /// Triggers DBC load if needed. Used by the action bar for out-of-range tinting. - uint32_t getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager); - - /// Returns the power cost and type for a spell (cost=0 if unknown/free). - /// powerType: 0=mana, 1=rage, 2=focus, 3=energy, 6=runic power. - /// Triggers DBC load if needed. Used by the action bar for insufficient-power tinting. - void getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager, - uint32_t& outCost, uint32_t& outPowerType); - /// Returns a WoW spell link string if the user shift-clicked a spell, then clears it. std::string getAndClearPendingChatLink() { std::string out = std::move(pendingChatSpellLink_); diff --git a/src/audio/ui_sound_manager.cpp b/src/audio/ui_sound_manager.cpp index 8ef800f0..f32f0d9b 100644 --- a/src/audio/ui_sound_manager.cpp +++ b/src/audio/ui_sound_manager.cpp @@ -122,14 +122,6 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) { deselectTargetSounds_.resize(1); loadSound("Sound\\Interface\\iDeselectTarget.wav", deselectTargetSounds_[0], assets); - // Whisper notification (falls back to iSelectTarget if the dedicated file is absent) - whisperSounds_.resize(1); - if (!loadSound("Sound\\Interface\\Whisper_TellMale.wav", whisperSounds_[0], assets)) { - if (!loadSound("Sound\\Interface\\Whisper_TellFemale.wav", whisperSounds_[0], assets)) { - whisperSounds_ = selectTargetSounds_; - } - } - LOG_INFO("UISoundManager: Window sounds - Bag: ", (bagOpenLoaded && bagCloseLoaded) ? "YES" : "NO", ", QuestLog: ", (questLogOpenLoaded && questLogCloseLoaded) ? "YES" : "NO", ", CharSheet: ", (charSheetOpenLoaded && charSheetCloseLoaded) ? "YES" : "NO"); @@ -233,8 +225,5 @@ void UiSoundManager::playError() { playSound(errorSounds_); } void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); } void UiSoundManager::playTargetDeselect() { playSound(deselectTargetSounds_); } -// Chat notifications -void UiSoundManager::playWhisperReceived() { playSound(whisperSounds_); } - } // namespace audio } // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index df1da0df..b3e57ccc 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2028,7 +2028,7 @@ void GameHandler::handlePacket(network::Packet& packet) { /*uint32_t randSuffix =*/ packet.readUInt32(); /*uint32_t randProp =*/ packet.readUInt32(); } - uint32_t countdown = packet.readUInt32(); + /*uint32_t countdown =*/ packet.readUInt32(); /*uint8_t voteMask =*/ packet.readUInt8(); // Trigger the roll popup for local player pendingLootRollActive_ = true; @@ -2038,8 +2038,6 @@ void GameHandler::handlePacket(network::Packet& packet) { auto* info = getItemInfo(itemId); pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId); pendingLootRoll_.itemQuality = info ? static_cast(info->quality) : 0; - pendingLootRoll_.rollCountdownMs = (countdown > 0 && countdown <= 120000) ? countdown : 60000; - pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now(); LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName, ") slot=", slot); break; @@ -3061,11 +3059,6 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t spellId = packet.readUInt32(); LOG_DEBUG("SMSG_TOTEM_CREATED: slot=", (int)slot, " spellId=", spellId, " duration=", duration, "ms"); - if (slot < NUM_TOTEM_SLOTS) { - activeTotemSlots_[slot].spellId = spellId; - activeTotemSlots_[slot].durationMs = duration; - activeTotemSlots_[slot].placedAt = std::chrono::steady_clock::now(); - } break; } case Opcode::SMSG_AREA_SPIRIT_HEALER_TIME: { @@ -3149,7 +3142,6 @@ void GameHandler::handlePacket(network::Packet& packet) { readyCheckReadyCount_ = 0; readyCheckNotReadyCount_ = 0; readyCheckInitiator_.clear(); - readyCheckResults_.clear(); if (packet.getSize() - packet.getReadPos() >= 8) { uint64_t initiatorGuid = packet.readUInt64(); auto entity = entityManager.getEntity(initiatorGuid); @@ -3183,14 +3175,7 @@ void GameHandler::handlePacket(network::Packet& packet) { auto ent = entityManager.getEntity(respGuid); if (ent) rname = std::static_pointer_cast(ent)->getName(); } - // Track per-player result for live popup display if (!rname.empty()) { - bool found = false; - for (auto& r : readyCheckResults_) { - if (r.name == rname) { r.ready = (isReady != 0); found = true; break; } - } - if (!found) readyCheckResults_.push_back({ rname, isReady != 0 }); - char rbuf[128]; std::snprintf(rbuf, sizeof(rbuf), "%s is %s.", rname.c_str(), isReady ? "Ready" : "Not Ready"); addSystemChatMessage(rbuf); @@ -3206,7 +3191,6 @@ void GameHandler::handlePacket(network::Packet& packet) { pendingReadyCheck_ = false; readyCheckReadyCount_ = 0; readyCheckNotReadyCount_ = 0; - readyCheckResults_.clear(); break; } case Opcode::SMSG_RAID_INSTANCE_INFO: @@ -3228,16 +3212,9 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_DUEL_INBOUNDS: // Re-entered the duel area; no special action needed. break; - case Opcode::SMSG_DUEL_COUNTDOWN: { - // uint32 countdown in milliseconds (typically 3000 ms) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t ms = packet.readUInt32(); - duelCountdownMs_ = (ms > 0 && ms <= 30000) ? ms : 3000; - duelCountdownStartedAt_ = std::chrono::steady_clock::now(); - LOG_INFO("SMSG_DUEL_COUNTDOWN: ", duelCountdownMs_, " ms"); - } + case Opcode::SMSG_DUEL_COUNTDOWN: + // Countdown timer — no action needed; server also sends UNIT_FIELD_FLAGS update. break; - } case Opcode::SMSG_PARTYKILLLOG: { // uint64 killerGuid + uint64 victimGuid if (packet.getSize() - packet.getReadPos() < 16) break; @@ -3567,7 +3544,6 @@ void GameHandler::handlePacket(network::Packet& packet) { delta > 0 ? "increased" : "decreased", std::abs(delta)); addSystemChatMessage(buf); - watchedFactionId_ = factionId; if (repChangeCallback_) repChangeCallback_(name, delta, standing); } LOG_DEBUG("SMSG_SET_FACTION_STANDING: faction=", factionId, " standing=", standing); @@ -3877,10 +3853,7 @@ void GameHandler::handlePacket(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() >= 4) { /*uint32_t len =*/ packet.readUInt32(); std::string msg = packet.readString(); - if (!msg.empty()) { - addSystemChatMessage(msg); - areaTriggerMsgs_.push_back(msg); - } + if (!msg.empty()) addSystemChatMessage(msg); } break; } @@ -4408,10 +4381,6 @@ void GameHandler::handlePacket(network::Packet& packet) { } for (auto it = questLog_.begin(); it != questLog_.end(); ++it) { if (it->questId == questId) { - // Fire toast callback before erasing - if (questCompleteCallback_) { - questCompleteCallback_(questId, it->title); - } questLog_.erase(it); LOG_INFO(" Removed quest ", questId, " from quest log"); break; @@ -4988,7 +4957,7 @@ void GameHandler::handlePacket(network::Packet& packet) { handleArenaError(packet); break; case Opcode::MSG_PVP_LOG_DATA: - handlePvpLogData(packet); + LOG_INFO("Received MSG_PVP_LOG_DATA"); break; case Opcode::MSG_INSPECT_ARENA_TEAMS: LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS"); @@ -5207,7 +5176,6 @@ void GameHandler::handlePacket(network::Packet& packet) { std::vector* auraList = nullptr; if (auraTargetGuid == playerGuid) auraList = &playerAuras; else if (auraTargetGuid == targetGuid) auraList = &targetAuras; - else if (auraTargetGuid != 0) auraList = &unitAurasCache_[auraTargetGuid]; if (auraList && isInit) auraList->clear(); @@ -5591,28 +5559,9 @@ void GameHandler::handlePacket(network::Packet& packet) { packet.setReadPos(packet.getSize()); break; } - case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: { - // Format (all expansions): PackedGuid target + PackedGuid caster + uint32 spellId + ... - if (packet.getSize() - packet.getReadPos() < 3) { - packet.setReadPos(packet.getSize()); break; - } - /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 2) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t procCasterGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) { - packet.setReadPos(packet.getSize()); break; - } - uint32_t procSpellId = packet.readUInt32(); - // Show a "PROC!" floating text when the player triggers the proc - if (procCasterGuid == playerGuid && procSpellId > 0) - addCombatText(CombatTextEntry::PROC_TRIGGER, 0, procSpellId, true); - packet.setReadPos(packet.getSize()); - break; - } case Opcode::SMSG_SPELLINSTAKILLLOG: case Opcode::SMSG_SPELLLOGEXECUTE: + case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: case Opcode::SMSG_SPELL_CHANCE_RESIST_PUSHBACK: case Opcode::SMSG_SPELL_UPDATE_CHAIN_TARGETS: packet.setReadPos(packet.getSize()); @@ -6709,7 +6658,6 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { actionBar = {}; playerAuras.clear(); targetAuras.clear(); - unitAurasCache_.clear(); unitCastStates_.clear(); petGuid_ = 0; playerXp_ = 0; @@ -10261,15 +10209,6 @@ void GameHandler::followTarget() { LOG_INFO("Following target: ", targetName, " (GUID: 0x", std::hex, targetGuid, std::dec, ")"); } -void GameHandler::cancelFollow() { - if (followTargetGuid_ == 0) { - addSystemChatMessage("You are not following anyone."); - return; - } - followTargetGuid_ = 0; - addSystemChatMessage("You stop following."); -} - void GameHandler::assistTarget() { if (state != WorldState::IN_WORLD) { LOG_WARNING("Cannot assist: not in world"); @@ -10526,7 +10465,6 @@ void GameHandler::handleDuelComplete(network::Packet& packet) { uint8_t started = packet.readUInt8(); // started=1: duel began, started=0: duel was cancelled before starting pendingDuelRequest_ = false; - duelCountdownMs_ = 0; // clear countdown once duel is resolved if (!started) { addSystemChatMessage("The duel was cancelled."); } @@ -11362,7 +11300,6 @@ void GameHandler::handleInspectResults(network::Packet& packet) { } // Parse enchantment slot mask + enchant IDs - std::array enchantIds{}; bytesLeft = packet.getSize() - packet.getReadPos(); if (bytesLeft >= 4) { uint32_t slotMask = packet.readUInt32(); @@ -11370,7 +11307,7 @@ void GameHandler::handleInspectResults(network::Packet& packet) { if (slotMask & (1u << slot)) { bytesLeft = packet.getSize() - packet.getReadPos(); if (bytesLeft < 2) break; - enchantIds[slot] = packet.readUInt16(); + packet.readUInt16(); // enchantId } } } @@ -11382,7 +11319,6 @@ void GameHandler::handleInspectResults(network::Packet& packet) { inspectResult_.unspentTalents = unspentTalents; inspectResult_.talentGroups = talentGroupCount; inspectResult_.activeTalentGroup = activeTalentGroup; - inspectResult_.enchantIds = enchantIds; // Merge any gear we already have from a prior inspect request auto gearIt = inspectedPlayerItemEntries_.find(guid); @@ -12165,21 +12101,6 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint entry.age = 0.0f; entry.isPlayerSource = isPlayerSource; combatText.push_back(entry); - - // Persistent combat log - CombatLogEntry log; - log.type = type; - log.amount = amount; - log.spellId = spellId; - log.isPlayerSource = isPlayerSource; - log.timestamp = std::time(nullptr); - std::string pname(lookupName(playerGuid)); - std::string tname((targetGuid != 0) ? lookupName(targetGuid) : std::string()); - log.sourceName = isPlayerSource ? pname : tname; - log.targetName = isPlayerSource ? tname : pname; - if (combatLog_.size() >= MAX_COMBAT_LOG) - combatLog_.pop_front(); - combatLog_.push_back(std::move(log)); } void GameHandler::updateCombatText(float deltaTime) { @@ -13140,19 +13061,11 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { lfgBootTimeLeft_ = timeLeft; lfgBootNeeded_ = votesNeeded; - // Optional: reason string and target name (null-terminated) follow the fixed fields - if (packet.getSize() - packet.getReadPos() > 0) - lfgBootReason_ = packet.readString(); - if (packet.getSize() - packet.getReadPos() > 0) - lfgBootTargetName_ = packet.readString(); - if (inProgress) { lfgState_ = LfgState::Boot; } else { // Boot vote ended — return to InDungeon state regardless of outcome lfgBootVotes_ = lfgBootTotal_ = lfgBootTimeLeft_ = lfgBootNeeded_ = 0; - lfgBootTargetName_.clear(); - lfgBootReason_.clear(); lfgState_ = LfgState::InDungeon; if (myAnswer) { addSystemChatMessage("Dungeon Finder: Vote kick passed — member removed."); @@ -13162,8 +13075,7 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { } LOG_INFO("SMSG_LFG_BOOT_PROPOSAL_UPDATE: inProgress=", inProgress, - " bootVotes=", bootVotes, "/", totalVotes, - " target=", lfgBootTargetName_, " reason=", lfgBootReason_); + " bootVotes=", bootVotes, "/", totalVotes); } void GameHandler::handleLfgTeleportDenied(network::Packet& packet) { @@ -13468,80 +13380,6 @@ void GameHandler::handleArenaError(network::Packet& packet) { LOG_INFO("Arena error: ", error, " - ", msg); } -void GameHandler::requestPvpLog() { - if (state != WorldState::IN_WORLD || !socket) return; - // MSG_PVP_LOG_DATA is bidirectional: client sends an empty packet to request - network::Packet pkt(wireOpcode(Opcode::MSG_PVP_LOG_DATA)); - socket->send(pkt); - LOG_INFO("Requested PvP log data"); -} - -void GameHandler::handlePvpLogData(network::Packet& packet) { - auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (remaining() < 1) return; - - bgScoreboard_ = BgScoreboardData{}; - bgScoreboard_.isArena = (packet.readUInt8() != 0); - - if (bgScoreboard_.isArena) { - // Skip arena-specific header (two teams × (rating change uint32 + name string + 5×uint32)) - // Rather than hardcoding arena parse we skip gracefully up to playerCount - // Each arena team block: uint32 + string + uint32*5 — variable length due to string. - // Skip by scanning for the uint32 playerCount heuristically; simply consume rest. - packet.setReadPos(packet.getSize()); - return; - } - - if (remaining() < 4) return; - uint32_t playerCount = packet.readUInt32(); - bgScoreboard_.players.reserve(playerCount); - - for (uint32_t i = 0; i < playerCount && remaining() >= 13; ++i) { - BgPlayerScore ps; - ps.guid = packet.readUInt64(); - ps.team = packet.readUInt8(); - ps.killingBlows = packet.readUInt32(); - ps.honorableKills = packet.readUInt32(); - ps.deaths = packet.readUInt32(); - ps.bonusHonor = packet.readUInt32(); - - // Resolve player name from entity manager - { - auto ent = entityManager.getEntity(ps.guid); - if (ent && (ent->getType() == game::ObjectType::PLAYER || - ent->getType() == game::ObjectType::UNIT)) { - auto u = std::static_pointer_cast(ent); - if (!u->getName().empty()) ps.name = u->getName(); - } - } - - // BG-specific stat blocks: uint32 count + N × (string fieldName + uint32 value) - if (remaining() < 4) { bgScoreboard_.players.push_back(std::move(ps)); break; } - uint32_t statCount = packet.readUInt32(); - for (uint32_t s = 0; s < statCount && remaining() >= 5; ++s) { - std::string fieldName; - while (remaining() > 0) { - char c = static_cast(packet.readUInt8()); - if (c == '\0') break; - fieldName += c; - } - uint32_t val = (remaining() >= 4) ? packet.readUInt32() : 0; - ps.bgStats.emplace_back(std::move(fieldName), val); - } - - bgScoreboard_.players.push_back(std::move(ps)); - } - - if (remaining() >= 1) { - bgScoreboard_.hasWinner = (packet.readUInt8() != 0); - if (bgScoreboard_.hasWinner && remaining() >= 1) - bgScoreboard_.winner = packet.readUInt8(); - } - - LOG_INFO("PvP log: ", bgScoreboard_.players.size(), " players, hasWinner=", - bgScoreboard_.hasWinner, " winner=", (int)bgScoreboard_.winner); -} - void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { // Server relays MSG_MOVE_* for other players: packed GUID (WotLK) or full uint64 (TBC/Classic) const bool otherMoveTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); @@ -14261,10 +14099,6 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { : CastSpellPacket::build(spellId, target, ++castCount); socket->send(packet); LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec); - - // Optimistically start GCD immediately on cast — server will confirm or override - gcdTotal_ = 1.5f; - gcdStartedAt_ = std::chrono::steady_clock::now(); } void GameHandler::cancelCast() { @@ -14623,14 +14457,6 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) { uint32_t cooldownMs = packet.readUInt32(); float seconds = cooldownMs / 1000.0f; - - // spellId=0 is the Global Cooldown marker (server sends it for GCD triggers) - if (spellId == 0 && cooldownMs > 0 && cooldownMs <= 2000) { - gcdTotal_ = seconds; - gcdStartedAt_ = std::chrono::steady_clock::now(); - continue; - } - spellCooldowns[spellId] = seconds; for (auto& slot : actionBar) { bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId) @@ -14671,10 +14497,6 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) { } else if (data.guid == targetGuid) { auraList = &targetAuras; } - // Also maintain a per-unit cache for any unit (party members, etc.) - if (data.guid != 0 && data.guid != playerGuid && data.guid != targetGuid) { - auraList = &unitAurasCache_[data.guid]; - } if (auraList) { if (isAll) { @@ -15093,34 +14915,20 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { if (updateFlags & 0x0200) { // AURAS if (remaining() >= 8) { uint64_t auraMask = packet.readUInt64(); - // Collect aura updates for this member and store in unitAurasCache_ - // so party frame debuff dots can use them. - std::vector newAuras; for (int i = 0; i < 64; ++i) { if (auraMask & (uint64_t(1) << i)) { - AuraSlot a; - a.level = static_cast(i); // use slot index if (isWotLK) { // WotLK: uint32 spellId + uint8 auraFlags if (remaining() < 5) break; - a.spellId = packet.readUInt32(); - a.flags = packet.readUInt8(); + packet.readUInt32(); + packet.readUInt8(); } else { - // Classic/TBC: uint16 spellId only; negative auras not indicated here + // Classic/TBC: uint16 spellId only if (remaining() < 2) break; - a.spellId = packet.readUInt16(); - // Infer negative/positive from dispel type: non-zero dispel → debuff - uint8_t dt = getSpellDispelType(a.spellId); - if (dt > 0) a.flags = 0x80; // mark as debuff + packet.readUInt16(); } - if (a.spellId != 0) newAuras.push_back(a); } } - // Populate unitAurasCache_ for this member (merge: keep existing per-GUID data - // only if we already have a richer source; otherwise replace with stats data) - if (memberGuid != 0 && memberGuid != playerGuid && memberGuid != targetGuid) { - unitAurasCache_[memberGuid] = std::move(newAuras); - } } } if (updateFlags & 0x0400) { // PET_GUID @@ -16208,28 +16016,6 @@ void GameHandler::abandonQuest(uint32_t questId) { gossipPois_.end()); } -void GameHandler::shareQuestWithParty(uint32_t questId) { - if (state != WorldState::IN_WORLD || !socket) { - addSystemChatMessage("Cannot share quest: not in world."); - return; - } - if (!isInGroup()) { - addSystemChatMessage("You must be in a group to share a quest."); - return; - } - network::Packet pkt(wireOpcode(Opcode::CMSG_PUSHQUESTTOPARTY)); - pkt.writeUInt32(questId); - socket->send(pkt); - // Local feedback: find quest title - for (const auto& q : questLog_) { - if (q.questId == questId && !q.title.empty()) { - addSystemChatMessage("Sharing quest: " + q.title); - return; - } - } - addSystemChatMessage("Quest shared."); -} - void GameHandler::handleQuestRequestItems(network::Packet& packet) { QuestRequestItemsData data; if (!QuestRequestItemsParser::parse(packet, data)) { @@ -17146,14 +16932,6 @@ void GameHandler::loadSpellNameCache() { if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { schoolEnumField = f; hasSchoolEnum = true; } } - // DispelType field (0=none,1=magic,2=curse,3=disease,4=poison,5=stealth,…) - uint32_t dispelField = 0xFFFFFFFF; - bool hasDispelField = false; - if (spellL) { - uint32_t f = spellL->field("DispelType"); - if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; } - } - uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t id = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); @@ -17161,7 +16939,7 @@ void GameHandler::loadSpellNameCache() { std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136); std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153); if (!name.empty()) { - SpellNameEntry entry{std::move(name), std::move(rank), 0, 0}; + SpellNameEntry entry{std::move(name), std::move(rank), 0}; if (hasSchoolMask) { entry.schoolMask = dbc->getUInt32(i, schoolMaskField); } else if (hasSchoolEnum) { @@ -17170,9 +16948,6 @@ void GameHandler::loadSpellNameCache() { uint32_t e = dbc->getUInt32(i, schoolEnumField); entry.schoolMask = (e < 7) ? enumToBitmask[e] : 0; } - if (hasDispelField) { - entry.dispelType = static_cast(dbc->getUInt32(i, dispelField)); - } spellNameCache_[id] = std::move(entry); } } @@ -17366,12 +17141,6 @@ const std::string& GameHandler::getSpellRank(uint32_t spellId) const { return (it != spellNameCache_.end()) ? it->second.rank : EMPTY_STRING; } -uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const { - const_cast(this)->loadSpellNameCache(); - auto it = spellNameCache_.find(spellId); - return (it != spellNameCache_.end()) ? it->second.dispelType : 0; -} - const std::string& GameHandler::getSkillLineName(uint32_t spellId) const { auto slIt = spellToSkillLine_.find(spellId); if (slIt == spellToSkillLine_.end()) return EMPTY_STRING; @@ -18464,15 +18233,13 @@ void GameHandler::handleWho(network::Packet& packet) { LOG_INFO("WHO response: ", displayCount, " players displayed, ", onlineCount, " total online"); - // Store structured results for the who-results window - whoResults_.clear(); - whoOnlineCount_ = onlineCount; - if (displayCount == 0) { addSystemChatMessage("No players found."); return; } + addSystemChatMessage(std::to_string(onlineCount) + " player(s) online:"); + for (uint32_t i = 0; i < displayCount; ++i) { if (packet.getReadPos() >= packet.getSize()) break; std::string playerName = packet.readString(); @@ -18487,16 +18254,17 @@ void GameHandler::handleWho(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() >= 4) zoneId = packet.readUInt32(); - // Store structured entry - WhoEntry entry; - entry.name = playerName; - entry.guildName = guildName; - entry.level = level; - entry.classId = classId; - entry.raceId = raceId; - entry.zoneId = zoneId; - whoResults_.push_back(std::move(entry)); + std::string msg = " " + playerName; + if (!guildName.empty()) + msg += " <" + guildName + ">"; + msg += " - Level " + std::to_string(level); + if (zoneId != 0) { + std::string zoneName = getAreaName(zoneId); + if (!zoneName.empty()) + msg += " [" + zoneName + "]"; + } + addSystemChatMessage(msg); LOG_INFO(" ", playerName, " (", guildName, ") Lv", level, " Class:", classId, " Race:", raceId, " Zone:", zoneId); } @@ -20153,15 +19921,12 @@ void GameHandler::handleLootRoll(network::Packet& packet) { pendingLootRoll_.objectGuid = objectGuid; pendingLootRoll_.slot = slot; pendingLootRoll_.itemId = itemId; - pendingLootRoll_.playerRolls.clear(); // Ensure item info is in cache; query if not queryItemInfo(itemId, 0); // Look up item name from cache auto* info = getItemInfo(itemId); pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId); pendingLootRoll_.itemQuality = info ? static_cast(info->quality) : 0; - pendingLootRoll_.rollCountdownMs = 60000; - pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now(); LOG_INFO("SMSG_LOOT_ROLL: need/greed prompt for item=", itemId, " (", pendingLootRoll_.itemName, ") slot=", slot); return; @@ -20178,28 +19943,6 @@ void GameHandler::handleLootRoll(network::Packet& packet) { } if (rollerName.empty()) rollerName = "Someone"; - // Track in the live roll list while our popup is open for the same item - if (pendingLootRollActive_ && - pendingLootRoll_.objectGuid == objectGuid && - pendingLootRoll_.slot == slot) { - bool found = false; - for (auto& r : pendingLootRoll_.playerRolls) { - if (r.playerName == rollerName) { - r.rollNum = rollNum; - r.rollType = rollType; - found = true; - break; - } - } - if (!found) { - LootRollEntry::PlayerRollResult prr; - prr.playerName = rollerName; - prr.rollNum = rollNum; - prr.rollType = rollType; - pendingLootRoll_.playerRolls.push_back(std::move(prr)); - } - } - auto* info = getItemInfo(itemId); std::string iName = info ? info->name : std::to_string(itemId); @@ -20254,8 +19997,10 @@ void GameHandler::handleLootRollWon(network::Packet& packet) { winnerName.c_str(), iName.c_str(), rollName, static_cast(rollNum)); addSystemChatMessage(buf); - // Dismiss roll popup — roll contest is over regardless of who won - pendingLootRollActive_ = false; + // Clear pending roll if it was ours + if (pendingLootRollActive_ && winnerGuid == playerGuid) { + pendingLootRollActive_ = false; + } LOG_INFO("SMSG_LOOT_ROLL_WON: winner=", winnerName, " item=", itemId, " roll=", rollName, "(", rollNum, ")"); } @@ -20312,7 +20057,7 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { uint64_t guid = packet.readUInt64(); uint32_t achievementId = packet.readUInt32(); - uint32_t earnDate = packet.readUInt32(); // WoW PackedTime bitfield + /*uint32_t date =*/ packet.readUInt32(); // PackedTime — not displayed loadAchievementNameCache(); auto nameIt = achievementNameCache_.find(achievementId); @@ -20331,7 +20076,6 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { addSystemChatMessage(buf); earnedAchievements_.insert(achievementId); - achievementDates_[achievementId] = earnDate; if (achievementEarnedCallback_) { achievementEarnedCallback_(achievementId, achName); } @@ -20372,16 +20116,14 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { void GameHandler::handleAllAchievementData(network::Packet& packet) { loadAchievementNameCache(); earnedAchievements_.clear(); - achievementDates_.clear(); // Parse achievement entries (id + packedDate pairs, sentinel 0xFFFFFFFF) while (packet.getSize() - packet.getReadPos() >= 4) { uint32_t id = packet.readUInt32(); if (id == 0xFFFFFFFF) break; if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t date = packet.readUInt32(); + /*uint32_t date =*/ packet.readUInt32(); earnedAchievements_.insert(id); - achievementDates_[id] = date; } // Parse criteria block: id + uint64 counter + uint32 date + uint32 flags, sentinel 0xFFFFFFFF diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 701c5148..9c30a3b5 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -12,7 +12,6 @@ #include "core/logger.hpp" #include #include -#include #include #include @@ -1017,40 +1016,6 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi } } - // Hover coordinate display — show WoW coordinates under cursor - if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) { - auto& io = ImGui::GetIO(); - ImVec2 mp = io.MousePos; - if (mp.x >= imgMin.x && mp.x <= imgMin.x + displayW && - mp.y >= imgMin.y && mp.y <= imgMin.y + displayH) { - float mu = (mp.x - imgMin.x) / displayW; - float mv = (mp.y - imgMin.y) / displayH; - - const auto& zone = zones[currentIdx]; - float left = zone.locLeft, right = zone.locRight; - float top = zone.locTop, bottom = zone.locBottom; - if (zone.areaID == 0) { - float l, r, t, b; - getContinentProjectionBounds(currentIdx, l, r, t, b); - left = l; right = r; top = t; bottom = b; - // Undo the kVOffset applied during renderPosToMapUV for continent - constexpr float kVOffset = -0.15f; - mv -= kVOffset; - } - - float hWowX = left - mu * (left - right); - float hWowY = top - mv * (top - bottom); - - char coordBuf[32]; - snprintf(coordBuf, sizeof(coordBuf), "%.0f, %.0f", hWowX, hWowY); - ImVec2 coordSz = ImGui::CalcTextSize(coordBuf); - float cx = imgMin.x + displayW - coordSz.x - 8.0f; - float cy = imgMin.y + displayH - coordSz.y - 8.0f; - drawList->AddText(ImVec2(cx + 1.0f, cy + 1.0f), IM_COL32(0, 0, 0, 180), coordBuf); - drawList->AddText(ImVec2(cx, cy), IM_COL32(220, 210, 150, 230), coordBuf); - } - } - // Continent view: clickable zone overlays if (viewLevel == ViewLevel::CONTINENT && continentIdx >= 0) { const auto& cont = zones[continentIdx]; @@ -1115,23 +1080,6 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f); } - - // Zone name label — only if the rect is large enough to fit it - if (!z.areaName.empty()) { - ImVec2 textSz = ImGui::CalcTextSize(z.areaName.c_str()); - float rectW = sx1 - sx0; - float rectH = sy1 - sy0; - if (rectW > textSz.x + 4.0f && rectH > textSz.y + 2.0f) { - float tx = (sx0 + sx1) * 0.5f - textSz.x * 0.5f; - float ty = (sy0 + sy1) * 0.5f - textSz.y * 0.5f; - ImU32 labelCol = explored - ? IM_COL32(255, 230, 150, 210) - : IM_COL32(160, 160, 160, 80); - drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f), - IM_COL32(0, 0, 0, 130), z.areaName.c_str()); - drawList->AddText(ImVec2(tx, ty), labelCol, z.areaName.c_str()); - } - } } } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 33e78f25..2ba78329 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -31,7 +31,6 @@ #include "pipeline/dbc_layout.hpp" #include "game/expansion_profile.hpp" -#include "game/character.hpp" #include "core/logger.hpp" #include #include @@ -72,67 +71,6 @@ namespace { return s; } - // Render gold/silver/copper amounts in WoW-canonical colors on the current ImGui line. - // Skips zero-value denominations (except copper, which is always shown when gold=silver=0). - void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) { - bool any = false; - if (g > 0) { - ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g); - any = true; - } - if (s > 0 || g > 0) { - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s); - any = true; - } - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c); - } - - // Return the canonical Blizzard class color as ImVec4. - // classId is byte 1 of UNIT_FIELD_BYTES_0 (or CharacterData::classId). - // Returns a neutral light-gray for unknown / class 0. - ImVec4 classColorVec4(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); // unknown - } - } - - // ImU32 variant with alpha in [0,255]. - ImU32 classColorU32(uint8_t classId, int alpha = 255) { - ImVec4 c = classColorVec4(classId); - return IM_COL32(static_cast(c.x * 255), static_cast(c.y * 255), - static_cast(c.z * 255), alpha); - } - - // Extract class id from a unit's UNIT_FIELD_BYTES_0 update field. - // Returns 0 if the entity pointer is null or field is unset. - uint8_t entityClassId(const wowee::game::Entity* entity) { - if (!entity) return 0; - using UF = wowee::game::UF; - uint32_t bytes0 = entity->getField(wowee::game::fieldIndex(UF::UNIT_FIELD_BYTES_0)); - return static_cast((bytes0 >> 8) & 0xFF); - } - - // Return the English class name for a class ID (1-11), or "Unknown". - const char* classNameStr(uint8_t classId) { - static const char* kNames[] = { - "Unknown","Warrior","Paladin","Hunter","Rogue","Priest", - "Death Knight","Shaman","Mage","Warlock","","Druid" - }; - return (classId < 12) ? kNames[classId] : "Unknown"; - } - bool isPortBotTarget(const std::string& target) { std::string t = toLower(trim(target)); return t == "portbot" || t == "gmbot" || t == "telebot"; @@ -205,43 +143,32 @@ GameScreen::GameScreen() { void GameScreen::initChatTabs() { chatTabs_.clear(); // General tab: shows everything - chatTabs_.push_back({"General", ~0ULL}); + chatTabs_.push_back({"General", 0xFFFFFFFF}); // Combat tab: system, loot, skills, achievements, and NPC speech/emotes - chatTabs_.push_back({"Combat", (1ULL << static_cast(game::ChatType::SYSTEM)) | - (1ULL << static_cast(game::ChatType::LOOT)) | - (1ULL << static_cast(game::ChatType::SKILL)) | - (1ULL << static_cast(game::ChatType::ACHIEVEMENT)) | - (1ULL << static_cast(game::ChatType::GUILD_ACHIEVEMENT)) | - (1ULL << static_cast(game::ChatType::MONSTER_SAY)) | - (1ULL << static_cast(game::ChatType::MONSTER_YELL)) | - (1ULL << static_cast(game::ChatType::MONSTER_EMOTE)) | - (1ULL << static_cast(game::ChatType::MONSTER_WHISPER)) | - (1ULL << static_cast(game::ChatType::MONSTER_PARTY)) | - (1ULL << static_cast(game::ChatType::RAID_BOSS_WHISPER)) | - (1ULL << static_cast(game::ChatType::RAID_BOSS_EMOTE))}); + chatTabs_.push_back({"Combat", (1u << static_cast(game::ChatType::SYSTEM)) | + (1u << static_cast(game::ChatType::LOOT)) | + (1u << static_cast(game::ChatType::SKILL)) | + (1u << static_cast(game::ChatType::ACHIEVEMENT)) | + (1u << static_cast(game::ChatType::GUILD_ACHIEVEMENT)) | + (1u << static_cast(game::ChatType::MONSTER_SAY)) | + (1u << static_cast(game::ChatType::MONSTER_YELL)) | + (1u << static_cast(game::ChatType::MONSTER_EMOTE))}); // Whispers tab - chatTabs_.push_back({"Whispers", (1ULL << static_cast(game::ChatType::WHISPER)) | - (1ULL << static_cast(game::ChatType::WHISPER_INFORM))}); - // Guild tab: guild and officer chat - chatTabs_.push_back({"Guild", (1ULL << static_cast(game::ChatType::GUILD)) | - (1ULL << static_cast(game::ChatType::OFFICER)) | - (1ULL << static_cast(game::ChatType::GUILD_ACHIEVEMENT))}); + chatTabs_.push_back({"Whispers", (1u << static_cast(game::ChatType::WHISPER)) | + (1u << static_cast(game::ChatType::WHISPER_INFORM))}); // Trade/LFG tab: channel messages - chatTabs_.push_back({"Trade/LFG", (1ULL << static_cast(game::ChatType::CHANNEL))}); - // Reset unread counts to match new tab list - chatTabUnread_.assign(chatTabs_.size(), 0); - chatTabSeenCount_ = 0; + chatTabs_.push_back({"Trade/LFG", (1u << static_cast(game::ChatType::CHANNEL))}); } bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const { if (tabIndex < 0 || tabIndex >= static_cast(chatTabs_.size())) return true; const auto& tab = chatTabs_[tabIndex]; - if (tab.typeMask == ~0ULL) return true; // General tab shows all + if (tab.typeMask == 0xFFFFFFFF) return true; // General tab shows all - uint64_t typeBit = 1ULL << static_cast(msg.type); + uint32_t typeBit = 1u << static_cast(msg.type); - // For Trade/LFG tab (now index 4), also filter by channel name - if (tabIndex == 4 && msg.type == game::ChatType::CHANNEL) { + // For Trade/LFG tab, also filter by channel name + if (tabIndex == 3 && msg.type == game::ChatType::CHANNEL) { const std::string& ch = msg.channelName; if (ch.find("Trade") == std::string::npos && ch.find("General") == std::string::npos && @@ -318,15 +245,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { repChangeCallbackSet_ = true; } - // Set up quest completion toast callback (once) - if (!questCompleteCallbackSet_) { - gameHandler.setQuestCompleteCallback([this](uint32_t id, const std::string& title) { - questCompleteToasts_.push_back({id, title, 0.0f}); - if (questCompleteToasts_.size() > 3) questCompleteToasts_.erase(questCompleteToasts_.begin()); - }); - questCompleteCallbackSet_ = true; - } - // Apply UI transparency setting float prevAlpha = ImGui::GetStyle().Alpha; ImGui::GetStyle().Alpha = uiOpacity_; @@ -492,20 +410,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply auto-loot setting to GameHandler every frame (cheap bool sync) gameHandler.setAutoLoot(pendingAutoLoot); - // Zone entry detection — fire a toast when the renderer's zone name changes - if (auto* rend = core::Application::getInstance().getRenderer()) { - const std::string& curZone = rend->getCurrentZoneName(); - if (!curZone.empty() && curZone != lastKnownZone_) { - if (!lastKnownZone_.empty()) { - // Genuine zone change (not first entry) - zoneToasts_.push_back({curZone, 0.0f}); - if (zoneToasts_.size() > 3) - zoneToasts_.erase(zoneToasts_.begin()); - } - lastKnownZone_ = curZone; - } - } - // Sync chat auto-join settings to GameHandler gameHandler.chatAutoJoin.general = chatAutoJoinGeneral_; gameHandler.chatAutoJoin.trade = chatAutoJoinTrade_; @@ -524,11 +428,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderPetFrame(gameHandler); } - // Totem frame (Shaman only, when any totem is active) - if (gameHandler.getPlayerClass() == 7) { - renderTotemFrame(gameHandler); - } - // Target frame (only when we have a target) if (gameHandler.hasTarget()) { renderTargetFrame(gameHandler); @@ -556,7 +455,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderActionBar(gameHandler); renderBagBar(gameHandler); renderXpBar(gameHandler); - renderRepBar(gameHandler); renderCastBar(gameHandler); renderMirrorTimers(gameHandler); renderQuestObjectiveTracker(gameHandler); @@ -567,16 +465,12 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderDPSMeter(gameHandler); renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime); renderRepToasts(ImGui::GetIO().DeltaTime); - renderQuestCompleteToasts(ImGui::GetIO().DeltaTime); - renderZoneToasts(ImGui::GetIO().DeltaTime); - renderAreaTriggerToasts(ImGui::GetIO().DeltaTime, gameHandler); if (showRaidFrames_) { renderPartyFrames(gameHandler); } renderBossFrames(gameHandler); renderGroupInvitePopup(gameHandler); renderDuelRequestPopup(gameHandler); - renderDuelCountdown(gameHandler); renderLootRollPopup(gameHandler); renderTradeRequestPopup(gameHandler); renderTradeWindow(gameHandler); @@ -605,13 +499,10 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderAuctionHouseWindow(gameHandler); renderDungeonFinderWindow(gameHandler); renderInstanceLockouts(gameHandler); - renderWhoWindow(gameHandler); - renderCombatLog(gameHandler); renderAchievementWindow(gameHandler); renderGmTicketWindow(gameHandler); renderInspectWindow(gameHandler); renderThreatWindow(gameHandler); - renderBgScoreboard(gameHandler); renderObjectiveTracker(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now if (showMinimap_) { @@ -855,45 +746,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { } } - // Persistent low-health vignette — pulsing red edges when HP < 20% - { - auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); - bool isDead = gameHandler.isPlayerDead(); - float hpPct = 1.0f; - if (!isDead && playerEntity && - (playerEntity->getType() == game::ObjectType::PLAYER || - playerEntity->getType() == game::ObjectType::UNIT)) { - auto unit = std::static_pointer_cast(playerEntity); - if (unit->getMaxHealth() > 0) - hpPct = static_cast(unit->getHealth()) / static_cast(unit->getMaxHealth()); - } - - // Only show when alive and below 20% HP; intensity increases as HP drops - if (lowHealthVignetteEnabled_ && !isDead && hpPct < 0.20f && hpPct > 0.0f) { - // Base intensity from HP deficit (0 at 20%, 1 at 0%); pulse at ~1.5 Hz - float danger = (0.20f - hpPct) / 0.20f; - float pulse = 0.55f + 0.45f * std::sin(static_cast(ImGui::GetTime()) * 9.4f); - int alpha = static_cast(danger * pulse * 90.0f); // max ~90 alpha, subtle - if (alpha > 0) { - ImDrawList* fg = ImGui::GetForegroundDrawList(); - ImGuiIO& io = ImGui::GetIO(); - const float W = io.DisplaySize.x; - const float H = io.DisplaySize.y; - const float thickness = std::min(W, H) * 0.15f; - const ImU32 edgeCol = IM_COL32(200, 0, 0, alpha); - const ImU32 fadeCol = IM_COL32(200, 0, 0, 0); - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness), - edgeCol, edgeCol, fadeCol, fadeCol); - fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H), - fadeCol, fadeCol, edgeCol, edgeCol); - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H), - edgeCol, fadeCol, fadeCol, edgeCol); - fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H), - fadeCol, edgeCol, edgeCol, fadeCol); - } - } - } - // Level-up golden burst overlay if (levelUpFlashAlpha_ > 0.0f) { levelUpFlashAlpha_ -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second @@ -1081,7 +933,6 @@ void GameScreen::renderEntityList(game::GameHandler& gameHandler) { void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); - auto* assetMgr = core::Application::getInstance().getAssetManager(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float chatW = std::min(500.0f, screenW * 0.4f); @@ -1111,43 +962,11 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { chatWindowPos_ = ImGui::GetWindowPos(); } - // Update unread counts: scan any new messages since last frame - { - const auto& history = gameHandler.getChatHistory(); - // Ensure unread array is sized correctly (guards against late init) - if (chatTabUnread_.size() != chatTabs_.size()) - chatTabUnread_.assign(chatTabs_.size(), 0); - // If history shrank (e.g. cleared), reset - if (chatTabSeenCount_ > history.size()) chatTabSeenCount_ = 0; - for (size_t mi = chatTabSeenCount_; mi < history.size(); ++mi) { - const auto& msg = history[mi]; - // For each non-General (non-0) tab that isn't currently active, check visibility - for (int ti = 1; ti < static_cast(chatTabs_.size()); ++ti) { - if (ti == activeChatTab_) continue; - if (shouldShowMessage(msg, ti)) { - chatTabUnread_[ti]++; - } - } - } - chatTabSeenCount_ = history.size(); - } - // Chat tabs if (ImGui::BeginTabBar("ChatTabs")) { for (int i = 0; i < static_cast(chatTabs_.size()); ++i) { - // Build label with unread count suffix for non-General tabs - std::string tabLabel = chatTabs_[i].name; - if (i > 0 && i < static_cast(chatTabUnread_.size()) && chatTabUnread_[i] > 0) { - tabLabel += " (" + std::to_string(chatTabUnread_[i]) + ")"; - } - // Use ImGuiTabItemFlags_NoPushId so label changes don't break tab identity - if (ImGui::BeginTabItem(tabLabel.c_str())) { - if (activeChatTab_ != i) { - activeChatTab_ = i; - // Clear unread count when tab becomes active - if (i < static_cast(chatTabUnread_.size())) - chatTabUnread_[i] = 0; - } + if (ImGui::BeginTabItem(chatTabs_[i].name.c_str())) { + activeChatTab_ = i; ImGui::EndTabItem(); } } @@ -1327,8 +1146,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { uint32_t g = info->sellPrice / 10000; uint32_t s = (info->sellPrice / 100) % 100; uint32_t c = info->sellPrice % 100; - ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c); } if (ImGui::GetIO().KeyShift && info->inventoryType > 0) { @@ -1371,13 +1189,10 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { // Find next special element: URL or WoW link size_t urlStart = text.find("https://", pos); - // Find next WoW link (may be colored with |c prefix or bare |H) + // Find next WoW item link: |cXXXXXXXX|Hitem:ENTRY:...|h[Name]|h|r size_t linkStart = text.find("|c", pos); - // Also handle bare |H links without color prefix - size_t bareItem = text.find("|Hitem:", pos); - size_t bareSpell = text.find("|Hspell:", pos); - size_t bareQuest = text.find("|Hquest:", pos); - size_t bareLinkStart = std::min({bareItem, bareSpell, bareQuest}); + // Also handle bare |Hitem: without color prefix + size_t bareLinkStart = text.find("|Hitem:", pos); // Determine which comes first size_t nextSpecial = std::min({urlStart, linkStart, bareLinkStart}); @@ -1410,30 +1225,18 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { if (nextSpecial == linkStart && text.size() > linkStart + 10) { // Parse |cAARRGGBB color linkColor = parseWowColor(text, linkStart); - // Find the nearest |H link of any supported type - size_t hItem = text.find("|Hitem:", linkStart + 10); - size_t hSpell = text.find("|Hspell:", linkStart + 10); - size_t hQuest = text.find("|Hquest:", linkStart + 10); - size_t hAch = text.find("|Hachievement:", linkStart + 10); - hStart = std::min({hItem, hSpell, hQuest, hAch}); + hStart = text.find("|Hitem:", linkStart + 10); } else if (nextSpecial == bareLinkStart) { hStart = bareLinkStart; } if (hStart != std::string::npos) { - // Determine link type - const bool isSpellLink = (text.compare(hStart, 8, "|Hspell:") == 0); - const bool isQuestLink = (text.compare(hStart, 8, "|Hquest:") == 0); - const bool isAchievLink = (text.compare(hStart, 14, "|Hachievement:") == 0); - // Default: item link - - // Parse the first numeric ID after |Htype: - size_t idOffset = isSpellLink ? 8 : (isQuestLink ? 8 : (isAchievLink ? 14 : 7)); - size_t entryStart = hStart + idOffset; + // Parse item entry: |Hitem:ENTRY:... + size_t entryStart = hStart + 7; // skip "|Hitem:" size_t entryEnd = text.find(':', entryStart); - uint32_t linkId = 0; + uint32_t itemEntry = 0; if (entryEnd != std::string::npos) { - linkId = static_cast(strtoul( + itemEntry = static_cast(strtoul( text.substr(entryStart, entryEnd - entryStart).c_str(), nullptr, 10)); } @@ -1442,122 +1245,53 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { size_t nameTagEnd = (nameTagStart != std::string::npos) ? text.find("]|h", nameTagStart + 3) : std::string::npos; - std::string linkName = isSpellLink ? "Unknown Spell" - : isQuestLink ? "Unknown Quest" - : isAchievLink ? "Unknown Achievement" - : "Unknown Item"; + std::string itemName = "Unknown Item"; if (nameTagStart != std::string::npos && nameTagEnd != std::string::npos) { - linkName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3); + itemName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3); } // Find end of entire link sequence (|r or after ]|h) - size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + idOffset; + size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + 7; size_t resetPos = text.find("|r", linkEnd); if (resetPos != std::string::npos && resetPos <= linkEnd + 2) { linkEnd = resetPos + 2; } - if (!isSpellLink && !isQuestLink && !isAchievLink) { - // --- Item link --- - uint32_t itemEntry = linkId; - if (itemEntry > 0) { - gameHandler.ensureItemInfo(itemEntry); - } + // Ensure item info is cached (trigger query if needed) + if (itemEntry > 0) { + gameHandler.ensureItemInfo(itemEntry); + } - // Show small icon before item link if available - if (itemEntry > 0) { - const auto* chatInfo = gameHandler.getItemInfo(itemEntry); - if (chatInfo && chatInfo->valid && chatInfo->displayInfoId != 0) { - VkDescriptorSet chatIcon = inventoryScreen.getItemIcon(chatInfo->displayInfoId); - if (chatIcon) { - ImGui::Image((ImTextureID)(uintptr_t)chatIcon, ImVec2(12, 12)); - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - renderItemLinkTooltip(itemEntry); - } - ImGui::SameLine(0, 2); + // Show small icon before item link if available + if (itemEntry > 0) { + const auto* chatInfo = gameHandler.getItemInfo(itemEntry); + if (chatInfo && chatInfo->valid && chatInfo->displayInfoId != 0) { + VkDescriptorSet chatIcon = inventoryScreen.getItemIcon(chatInfo->displayInfoId); + if (chatIcon) { + ImGui::Image((ImTextureID)(uintptr_t)chatIcon, ImVec2(12, 12)); + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + renderItemLinkTooltip(itemEntry); } + ImGui::SameLine(0, 2); } } - - // Render bracketed item name in quality color - std::string display = "[" + linkName + "]"; - ImGui::PushStyleColor(ImGuiCol_Text, linkColor); - ImGui::TextWrapped("%s", display.c_str()); - ImGui::PopStyleColor(); - - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (itemEntry > 0) { - renderItemLinkTooltip(itemEntry); - } - } - } else if (isSpellLink) { - // --- Spell link: |Hspell:SPELLID:RANK|h[Name]|h --- - // Small icon (use spell icon cache if available) - VkDescriptorSet spellIcon = (linkId > 0) ? getSpellIcon(linkId, assetMgr) : VK_NULL_HANDLE; - if (spellIcon) { - ImGui::Image((ImTextureID)(uintptr_t)spellIcon, ImVec2(12, 12)); - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - spellbookScreen.renderSpellInfoTooltip(linkId, gameHandler, assetMgr); - } - ImGui::SameLine(0, 2); - } - - std::string display = "[" + linkName + "]"; - ImGui::PushStyleColor(ImGuiCol_Text, linkColor); - ImGui::TextWrapped("%s", display.c_str()); - ImGui::PopStyleColor(); - - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (linkId > 0) { - spellbookScreen.renderSpellInfoTooltip(linkId, gameHandler, assetMgr); - } - } - } else if (isQuestLink) { - // --- Quest link: |Hquest:QUESTID:QUESTLEVEL|h[Name]|h --- - std::string display = "[" + linkName + "]"; - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.84f, 0.0f, 1.0f)); // gold - ImGui::TextWrapped("%s", display.c_str()); - ImGui::PopStyleColor(); - - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::BeginTooltip(); - ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%s", linkName.c_str()); - // Parse quest level (second field after questId) - if (entryEnd != std::string::npos) { - size_t lvlEnd = text.find(':', entryEnd + 1); - if (lvlEnd == std::string::npos) lvlEnd = text.find('|', entryEnd + 1); - if (lvlEnd != std::string::npos) { - uint32_t qLvl = static_cast(strtoul( - text.substr(entryEnd + 1, lvlEnd - entryEnd - 1).c_str(), nullptr, 10)); - if (qLvl > 0) ImGui::TextDisabled("Level %u Quest", qLvl); - } - } - ImGui::TextDisabled("Click quest log to view details"); - ImGui::EndTooltip(); - } - // Click: open quest log and select this quest if we have it - if (ImGui::IsItemClicked() && linkId > 0) { - questLogScreen.openAndSelectQuest(linkId); - } - } else { - // --- Achievement link --- - std::string display = "[" + linkName + "]"; - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); // gold - ImGui::TextWrapped("%s", display.c_str()); - ImGui::PopStyleColor(); - - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("Achievement: %s", linkName.c_str()); - } } - // Shift-click: insert entire link back into chat input + // Render bracketed item name in quality color + std::string display = "[" + itemName + "]"; + ImGui::PushStyleColor(ImGuiCol_Text, linkColor); + ImGui::TextWrapped("%s", display.c_str()); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (itemEntry > 0) { + renderItemLinkTooltip(itemEntry); + } + } + + // Shift-click: insert item link into chat input if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) { std::string linkText = text.substr(nextSpecial, linkEnd - nextSpecial); size_t curLen = strlen(chatInputBuffer); @@ -1643,39 +1377,6 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { } }; - // Determine local player name for mention detection (case-insensitive) - std::string selfNameLower; - { - const auto* ch = gameHandler.getActiveCharacter(); - if (ch && !ch->name.empty()) { - selfNameLower = ch->name; - for (auto& c : selfNameLower) c = static_cast(std::tolower(static_cast(c))); - } - } - - // Scan NEW messages (beyond chatMentionSeenCount_) for mentions and play notification sound - if (!selfNameLower.empty() && chatHistory.size() > chatMentionSeenCount_) { - for (size_t mi = chatMentionSeenCount_; mi < chatHistory.size(); ++mi) { - const auto& mMsg = chatHistory[mi]; - // Skip outgoing whispers, system, and monster messages - if (mMsg.type == game::ChatType::WHISPER_INFORM || - mMsg.type == game::ChatType::SYSTEM) continue; - // Case-insensitive search in message body - std::string bodyLower = mMsg.message; - for (auto& c : bodyLower) c = static_cast(std::tolower(static_cast(c))); - if (bodyLower.find(selfNameLower) != std::string::npos) { - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* ui = renderer->getUiSoundManager()) - ui->playWhisperReceived(); - } - break; // play at most once per scan pass - } - } - chatMentionSeenCount_ = chatHistory.size(); - } else if (chatHistory.size() <= chatMentionSeenCount_) { - chatMentionSeenCount_ = chatHistory.size(); // reset if history was cleared - } - int chatMsgIdx = 0; for (const auto& msg : chatHistory) { if (!shouldShowMessage(msg, activeChatTab_)) continue; @@ -1759,30 +1460,10 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { } } - // Detect mention: does this message contain the local player's name? - bool isMention = false; - if (!selfNameLower.empty() && - msg.type != game::ChatType::WHISPER_INFORM && - msg.type != game::ChatType::SYSTEM) { - std::string msgLower = fullMsg; - for (auto& c : msgLower) c = static_cast(std::tolower(static_cast(c))); - isMention = (msgLower.find(selfNameLower) != std::string::npos); - } - // Render message in a group so we can attach a right-click context menu ImGui::PushID(chatMsgIdx++); - if (isMention) { - // Golden highlight strip behind the text - ImVec2 groupMin = ImGui::GetCursorScreenPos(); - float availW = ImGui::GetContentRegionAvail().x; - float lineH = ImGui::GetTextLineHeightWithSpacing(); - ImGui::GetWindowDrawList()->AddRectFilled( - groupMin, - ImVec2(groupMin.x + availW, groupMin.y + lineH), - IM_COL32(255, 200, 50, 45)); // soft golden tint - } ImGui::BeginGroup(); - renderTextWithLinks(fullMsg, isMention ? ImVec4(1.0f, 0.9f, 0.35f, 1.0f) : color); + renderTextWithLinks(fullMsg, color); ImGui::EndGroup(); // Right-click context menu (only for player messages with a sender) @@ -1863,8 +1544,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::Text("Type:"); ImGui::SameLine(); ImGui::SetNextItemWidth(100); - const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE", "CHANNEL" }; - ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 11); + const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE" }; + ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 10); // Auto-fill whisper target when switching to WHISPER mode if (selectedChatType == 4 && lastChatType != 4) { @@ -1891,27 +1572,6 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::InputText("##WhisperTarget", whisperTargetBuffer, sizeof(whisperTargetBuffer)); } - // Show channel picker if CHANNEL is selected - if (selectedChatType == 10) { - const auto& channels = gameHandler.getJoinedChannels(); - if (channels.empty()) { - ImGui::SameLine(); - ImGui::TextDisabled("(no channels joined)"); - } else { - ImGui::SameLine(); - if (selectedChannelIdx >= static_cast(channels.size())) selectedChannelIdx = 0; - ImGui::SetNextItemWidth(140); - if (ImGui::BeginCombo("##ChannelPicker", channels[selectedChannelIdx].c_str())) { - for (int ci = 0; ci < static_cast(channels.size()); ++ci) { - bool selected = (ci == selectedChannelIdx); - if (ImGui::Selectable(channels[ci].c_str(), selected)) selectedChannelIdx = ci; - if (selected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - } - } - ImGui::SameLine(); ImGui::Text("Message:"); ImGui::SameLine(); @@ -1942,16 +1602,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { else if (cmd == "bg" || cmd == "battleground") detected = 7; else if (cmd == "rw" || cmd == "raidwarning") detected = 8; else if (cmd == "i" || cmd == "instance") detected = 9; - else if (cmd.size() == 1 && cmd[0] >= '1' && cmd[0] <= '9') detected = 10; // /1, /2 etc. - if (detected >= 0 && (selectedChatType != detected || detected == 10)) { - // For channel shortcuts, also update selectedChannelIdx - if (detected == 10) { - int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc. - const auto& chans = gameHandler.getJoinedChannels(); - if (chanIdx >= 0 && chanIdx < static_cast(chans.size())) { - selectedChannelIdx = chanIdx; - } - } + if (detected >= 0 && selectedChatType != detected) { selectedChatType = detected; // Strip the prefix, keep only the message part std::string remaining = buf.substr(sp + 1); @@ -1989,8 +1640,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { case 6: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // OFFICER - green case 7: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // BG - orange case 8: inputColor = ImVec4(1.0f, 0.3f, 0.0f, 1.0f); break; // RAID WARNING - red-orange - case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue - case 10: inputColor = ImVec4(0.3f, 0.9f, 0.9f, 1.0f); break; // CHANNEL - cyan + case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue default: inputColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // SAY - white } ImGui::PushStyleColor(ImGuiCol_Text, inputColor); @@ -2008,70 +1658,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { self->chatInputMoveCursorToEnd = false; } - // Tab: slash-command autocomplete - if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) { - if (data->BufTextLen > 0 && data->Buf[0] == '/') { - // Split buffer into command word and trailing args - std::string fullBuf(data->Buf, data->BufTextLen); - size_t spacePos = fullBuf.find(' '); - std::string word = (spacePos != std::string::npos) ? fullBuf.substr(0, spacePos) : fullBuf; - std::string rest = (spacePos != std::string::npos) ? fullBuf.substr(spacePos) : ""; - - // Normalize to lowercase for matching - std::string lowerWord = word; - for (auto& ch : lowerWord) ch = static_cast(std::tolower(static_cast(ch))); - - static const std::vector kCmds = { - "/afk", "/away", "/cast", "/chathelp", "/clear", - "/dance", "/do", "/dnd", "/e", "/emote", - "/cl", "/combatlog", "/equip", "/follow", "/g", "/guild", "/guildinfo", - "/gmticket", "/grouploot", "/i", "/instance", - "/invite", "/j", "/join", "/kick", - "/l", "/leave", "/local", "/me", - "/p", "/party", "/r", "/raid", - "/raidwarning", "/random", "/reply", "/roll", - "/s", "/say", "/setloot", "/shout", - "/stopattack", "/stopfollow", "/t", "/time", - "/trade", "/uninvite", "/use", "/w", "/whisper", - "/who", "/wts", "/wtb", "/y", "/yell", "/zone" - }; - - // New session if prefix changed - if (self->chatTabMatchIdx_ < 0 || self->chatTabPrefix_ != lowerWord) { - self->chatTabPrefix_ = lowerWord; - self->chatTabMatches_.clear(); - for (const auto& cmd : kCmds) { - if (cmd.size() >= lowerWord.size() && - cmd.compare(0, lowerWord.size(), lowerWord) == 0) - self->chatTabMatches_.push_back(cmd); - } - self->chatTabMatchIdx_ = 0; - } else { - // Cycle forward through matches - ++self->chatTabMatchIdx_; - if (self->chatTabMatchIdx_ >= static_cast(self->chatTabMatches_.size())) - self->chatTabMatchIdx_ = 0; - } - - if (!self->chatTabMatches_.empty()) { - std::string match = self->chatTabMatches_[self->chatTabMatchIdx_]; - // Append trailing space when match is unambiguous - if (self->chatTabMatches_.size() == 1 && rest.empty()) - match += ' '; - std::string newBuf = match + rest; - data->DeleteChars(0, data->BufTextLen); - data->InsertChars(0, newBuf.c_str()); - } - } - return 0; - } - // Up/Down arrow: cycle through sent message history if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) { - // Any history navigation resets autocomplete - self->chatTabMatchIdx_ = -1; - self->chatTabMatches_.clear(); - const int histSize = static_cast(self->chatSentHistory_.size()); if (histSize == 0) return 0; @@ -2102,8 +1690,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackAlways | - ImGuiInputTextFlags_CallbackHistory | - ImGuiInputTextFlags_CallbackCompletion; + ImGuiInputTextFlags_CallbackHistory; if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), inputFlags, inputCallback, this)) { sendChatMessage(gameHandler); // Close chat input on send so movement keys work immediately. @@ -2157,23 +1744,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } } - // Toggle character screen (C) and inventory/bags (I) - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) { - inventoryScreen.toggleCharacter(); - } - + // Toggle nameplates (customizable keybinding, default V) if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) { inventoryScreen.toggle(); } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_BAGS)) { - if (inventoryScreen.isSeparateBags()) { - inventoryScreen.openAllBags(); - } else { - inventoryScreen.toggle(); - } - } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) { showNameplates_ = !showNameplates_; } @@ -2546,13 +2121,8 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { playerHp = playerMaxHp; } - // Derive class color via shared helper - ImVec4 classColor = activeChar - ? classColorVec4(static_cast(activeChar->characterClass)) - : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); - - // Name in class color — clickable for self-target, right-click for menu - ImGui::PushStyleColor(ImGuiCol_Text, classColor); + // Name in green (friendly player color) — clickable for self-target, right-click for menu + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) { gameHandler.setTarget(gameHandler.getPlayerGuid()); } @@ -2598,12 +2168,6 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), ""); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Do not disturb — /dnd to cancel"); } - if (inCombatConfirmed && !isDead) { - float combatPulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.2f * combatPulse, 0.2f * combatPulse, 1.0f), "[Combat]"); - if (ImGui::IsItemHovered()) ImGui::SetTooltip("You are in combat"); - } // Try to get real HP/mana from the player entity auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); @@ -2649,16 +2213,7 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { float mpPct = static_cast(power) / static_cast(maxPower); ImVec4 powerColor; switch (powerType) { - case 0: { - // Mana: pulse desaturated blue when critically low (< 20%) - if (mpPct < 0.2f) { - float pulse = 0.6f + 0.4f * std::sin(static_cast(ImGui::GetTime()) * 3.0f); - powerColor = ImVec4(0.1f, 0.1f, 0.8f * pulse, 1.0f); - } else { - powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); - } - break; - } + case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue) case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red) case 2: powerColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus (orange) case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow) @@ -2755,85 +2310,6 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { } } } - - // Shaman totem bar (class 7) — 4 slots: Earth, Fire, Water, Air - if (gameHandler.getPlayerClass() == 7) { - static const ImVec4 kTotemColors[] = { - ImVec4(0.80f, 0.55f, 0.25f, 1.0f), // Earth — brown - ImVec4(1.00f, 0.35f, 0.10f, 1.0f), // Fire — orange-red - ImVec4(0.20f, 0.55f, 0.90f, 1.0f), // Water — blue - ImVec4(0.70f, 0.90f, 1.00f, 1.0f), // Air — pale sky - }; - static const char* kTotemNames[] = { "Earth", "Fire", "Water", "Air" }; - - ImGui::Spacing(); - ImVec2 cursor = ImGui::GetCursorScreenPos(); - float totalW = ImGui::GetContentRegionAvail().x; - float spacing = 3.0f; - float slotW = (totalW - spacing * 3.0f) / 4.0f; - float slotH = 14.0f; - ImDrawList* tdl = ImGui::GetWindowDrawList(); - - for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; i++) { - const auto& ts = gameHandler.getTotemSlot(i); - float x0 = cursor.x + i * (slotW + spacing); - float y0 = cursor.y; - float x1 = x0 + slotW; - float y1 = y0 + slotH; - - // Background - tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1), IM_COL32(20, 20, 20, 200), 2.0f); - - if (ts.active()) { - float rem = ts.remainingMs(); - float frac = rem / static_cast(ts.durationMs); - float fillX = x0 + (x1 - x0) * frac; - tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(fillX, y1), - ImGui::ColorConvertFloat4ToU32(kTotemColors[i]), 2.0f); - // Remaining seconds label - char secBuf[8]; - snprintf(secBuf, sizeof(secBuf), "%.0f", rem / 1000.0f); - ImVec2 tsz = ImGui::CalcTextSize(secBuf); - float lx = x0 + (slotW - tsz.x) * 0.5f; - float ly = y0 + (slotH - tsz.y) * 0.5f; - tdl->AddText(ImVec2(lx + 1, ly + 1), IM_COL32(0, 0, 0, 180), secBuf); - tdl->AddText(ImVec2(lx, ly), IM_COL32(255, 255, 255, 230), secBuf); - } else { - // Inactive — show element letter - const char* letter = kTotemNames[i]; - char single[2] = { letter[0], '\0' }; - ImVec2 tsz = ImGui::CalcTextSize(single); - float lx = x0 + (slotW - tsz.x) * 0.5f; - float ly = y0 + (slotH - tsz.y) * 0.5f; - tdl->AddText(ImVec2(lx, ly), IM_COL32(80, 80, 80, 200), single); - } - - // Border - ImU32 borderCol = ts.active() - ? ImGui::ColorConvertFloat4ToU32(kTotemColors[i]) - : IM_COL32(60, 60, 60, 160); - tdl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f); - - // Tooltip on hover - ImGui::SetCursorScreenPos(ImVec2(x0, y0)); - ImGui::InvisibleButton(("##totem" + std::to_string(i)).c_str(), ImVec2(slotW, slotH)); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - if (ts.active()) { - const std::string& spellNm = gameHandler.getSpellName(ts.spellId); - ImGui::TextColored(ImVec4(kTotemColors[i].x, kTotemColors[i].y, - kTotemColors[i].z, 1.0f), - "%s Totem", kTotemNames[i]); - if (!spellNm.empty()) ImGui::Text("%s", spellNm.c_str()); - ImGui::Text("%.1fs remaining", ts.remainingMs() / 1000.0f); - } else { - ImGui::TextDisabled("%s Totem (empty)", kTotemNames[i]); - } - ImGui::EndTooltip(); - } - } - ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + slotH + 2.0f)); - } } ImGui::End(); @@ -2936,94 +2412,10 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } - // Happiness bar — hunter pets store happiness as power type 4 - { - uint32_t happiness = petUnit->getPowerByType(4); - uint32_t maxHappiness = petUnit->getMaxPowerByType(4); - if (maxHappiness > 0 && happiness > 0) { - float hapPct = static_cast(happiness) / static_cast(maxHappiness); - // Tier: < 33% = Unhappy (red), < 67% = Content (yellow), >= 67% = Happy (green) - ImVec4 hapColor = hapPct >= 0.667f ? ImVec4(0.2f, 0.85f, 0.2f, 1.0f) - : hapPct >= 0.333f ? ImVec4(0.9f, 0.75f, 0.1f, 1.0f) - : ImVec4(0.85f, 0.2f, 0.2f, 1.0f); - const char* hapLabel = hapPct >= 0.667f ? "Happy" : hapPct >= 0.333f ? "Content" : "Unhappy"; - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hapColor); - ImGui::ProgressBar(hapPct, ImVec2(-1, 8), hapLabel); - ImGui::PopStyleColor(); - } - } - - // Pet cast bar - if (auto* pcs = gameHandler.getUnitCastState(petGuid)) { - float castPct = (pcs->timeTotal > 0.0f) - ? (pcs->timeTotal - pcs->timeRemaining) / pcs->timeTotal : 0.0f; - // Orange color to distinguish from health/power bars - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.85f, 0.55f, 0.1f, 1.0f)); - char petCastLabel[48]; - const std::string& spellNm = gameHandler.getSpellName(pcs->spellId); - if (!spellNm.empty()) - snprintf(petCastLabel, sizeof(petCastLabel), "%s (%.1fs)", spellNm.c_str(), pcs->timeRemaining); - else - snprintf(petCastLabel, sizeof(petCastLabel), "Casting... (%.1fs)", pcs->timeRemaining); - ImGui::ProgressBar(castPct, ImVec2(-1, 10), petCastLabel); - ImGui::PopStyleColor(); - } - - // Stance row: Passive / Defensive / Aggressive — with Dismiss right-aligned - { - static const char* kReactLabels[] = { "Psv", "Def", "Agg" }; - static const char* kReactTooltips[] = { "Passive", "Defensive", "Aggressive" }; - static const ImVec4 kReactColors[] = { - ImVec4(0.4f, 0.6f, 1.0f, 1.0f), // passive — blue - ImVec4(0.3f, 0.85f, 0.3f, 1.0f), // defensive — green - ImVec4(1.0f, 0.35f, 0.35f, 1.0f),// aggressive — red - }; - static const ImVec4 kReactDimColors[] = { - ImVec4(0.15f, 0.2f, 0.4f, 0.8f), - ImVec4(0.1f, 0.3f, 0.1f, 0.8f), - ImVec4(0.4f, 0.1f, 0.1f, 0.8f), - }; - uint8_t curReact = gameHandler.getPetReact(); // 0=passive,1=defensive,2=aggressive - - // Find each react-type slot in the action bar by known built-in IDs: - // 1=Passive, 4=Defensive, 6=Aggressive (WoW wire protocol) - static const uint32_t kReactActionIds[] = { 1u, 4u, 6u }; - uint32_t reactSlotVals[3] = { 0, 0, 0 }; - const int slotTotal = game::GameHandler::PET_ACTION_BAR_SLOTS; - for (int i = 0; i < slotTotal; ++i) { - uint32_t sv = gameHandler.getPetActionSlot(i); - uint32_t aid = sv & 0x00FFFFFFu; - for (int r = 0; r < 3; ++r) { - if (aid == kReactActionIds[r]) { reactSlotVals[r] = sv; break; } - } - } - - for (int r = 0; r < 3; ++r) { - if (r > 0) ImGui::SameLine(0.0f, 3.0f); - bool active = (curReact == static_cast(r)); - ImVec4 btnCol = active ? kReactColors[r] : kReactDimColors[r]; - ImGui::PushID(r + 1000); - ImGui::PushStyleColor(ImGuiCol_Button, btnCol); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kReactColors[r]); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, kReactColors[r]); - if (ImGui::Button(kReactLabels[r], ImVec2(34.0f, 16.0f))) { - // Use server-provided slot value if available; fall back to raw ID - uint32_t action = (reactSlotVals[r] != 0) - ? reactSlotVals[r] - : kReactActionIds[r]; - gameHandler.sendPetAction(action, 0); - } - ImGui::PopStyleColor(3); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", kReactTooltips[r]); - ImGui::PopID(); - } - - // Dismiss button right-aligned on the same row - ImGui::SameLine(ImGui::GetContentRegionAvail().x - 58.0f); - if (ImGui::SmallButton("Dismiss")) { - gameHandler.dismissPet(); - } + // Dismiss button (compact, right-aligned) + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60.0f); + if (ImGui::SmallButton("Dismiss")) { + gameHandler.dismissPet(); } // Pet action bar — show up to 10 action slots from SMSG_PET_SPELLS @@ -3053,12 +2445,9 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { // Try to show spell icon; fall back to abbreviated text label. VkDescriptorSet iconTex = VK_NULL_HANDLE; const char* builtinLabel = nullptr; - if (actionId == 1) builtinLabel = "Psv"; - else if (actionId == 2) builtinLabel = "Fol"; + if (actionId == 2) builtinLabel = "Fol"; else if (actionId == 3) builtinLabel = "Sty"; - else if (actionId == 4) builtinLabel = "Def"; else if (actionId == 5) builtinLabel = "Atk"; - else if (actionId == 6) builtinLabel = "Agg"; else if (assetMgr) iconTex = getSpellIcon(actionId, assetMgr); // Tint green when autocast is on. @@ -3096,17 +2485,11 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { // Tooltip: show spell name or built-in command name. if (ImGui::IsItemHovered()) { - const char* tip = nullptr; - if (builtinLabel) { - if (actionId == 1) tip = "Passive"; - else if (actionId == 2) tip = "Follow"; - else if (actionId == 3) tip = "Stay"; - else if (actionId == 4) tip = "Defensive"; - else if (actionId == 5) tip = "Attack"; - else if (actionId == 6) tip = "Aggressive"; - } + const char* tip = builtinLabel + ? (actionId == 5 ? "Attack" : actionId == 2 ? "Follow" : "Stay") + : nullptr; std::string spellNm; - if (!tip && actionId > 6) { + if (!tip && actionId > 5) { spellNm = gameHandler.getSpellName(actionId); if (!spellNm.empty()) tip = spellNm.c_str(); } @@ -3124,87 +2507,6 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } -// ============================================================ -// Totem Frame (Shaman — below pet frame / player frame) -// ============================================================ - -void GameScreen::renderTotemFrame(game::GameHandler& gameHandler) { - // Only show if at least one totem is active - bool anyActive = false; - for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) { - if (gameHandler.getTotemSlot(i).active()) { anyActive = true; break; } - } - if (!anyActive) return; - - static const struct { const char* name; ImU32 color; } kTotemInfo[4] = { - { "Earth", IM_COL32(139, 90, 43, 255) }, // brown - { "Fire", IM_COL32(220, 80, 30, 255) }, // red-orange - { "Water", IM_COL32( 30,120, 220, 255) }, // blue - { "Air", IM_COL32(180,220, 255, 255) }, // light blue - }; - - // Position: below pet frame / player frame, left side - // Pet frame is at ~y=200 if active, player frame is at y=20; totem frame near y=300 - // We anchor relative to screen left edge like pet frame - ImGui::SetNextWindowPos(ImVec2(8.0f, 300.0f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(130.0f, 0.0f), ImGuiCond_Always); - - ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoTitleBar; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.08f, 0.06f, 0.88f)); - - if (ImGui::Begin("##TotemFrame", nullptr, flags)) { - ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.3f, 1.0f), "Totems"); - ImGui::Separator(); - - for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) { - const auto& slot = gameHandler.getTotemSlot(i); - if (!slot.active()) continue; - - ImGui::PushID(i); - - // Colored element dot - ImVec2 dotPos = ImGui::GetCursorScreenPos(); - dotPos.x += 4.0f; dotPos.y += 6.0f; - ImGui::GetWindowDrawList()->AddCircleFilled( - ImVec2(dotPos.x + 4.0f, dotPos.y + 4.0f), 4.0f, kTotemInfo[i].color); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 14.0f); - - // Totem name or spell name - const std::string& spellName = gameHandler.getSpellName(slot.spellId); - const char* displayName = spellName.empty() ? kTotemInfo[i].name : spellName.c_str(); - ImGui::Text("%s", displayName); - - // Duration countdown bar - float remMs = slot.remainingMs(); - float totMs = static_cast(slot.durationMs); - float frac = (totMs > 0.0f) ? std::min(remMs / totMs, 1.0f) : 0.0f; - float remSec = remMs / 1000.0f; - - // Color bar with totem element tint - ImVec4 barCol( - static_cast((kTotemInfo[i].color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f, - static_cast((kTotemInfo[i].color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f, - static_cast((kTotemInfo[i].color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f, - 0.9f); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barCol); - char timeBuf[16]; - snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remSec); - ImGui::ProgressBar(frac, ImVec2(-1, 8), timeBuf); - ImGui::PopStyleColor(); - - ImGui::PopID(); - } - } - ImGui::End(); - - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); -} - void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { auto target = gameHandler.getTarget(); if (!target) return; @@ -3292,12 +2594,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { // Entity name and type — Selectable so we can attach a right-click context menu std::string name = getEntityName(target); - // Player targets: use class color instead of the generic green ImVec4 nameColor = hostileColor; - if (target->getType() == game::ObjectType::PLAYER) { - uint8_t cid = entityClassId(target.get()); - if (cid != 0) nameColor = classColorVec4(cid); - } ImGui::SameLine(0.0f, 0.0f); ImGui::PushStyleColor(ImGuiCol_Text, nameColor); @@ -3382,12 +2679,6 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); } ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); - if (confirmedCombatWithTarget) { - float cPulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.2f * cPulse, 0.2f * cPulse, 1.0f), "[Attacking]"); - if (ImGui::IsItemHovered()) ImGui::SetTooltip("Engaged in combat with this target"); - } // Health bar uint32_t hp = unit->getHealth(); @@ -3438,32 +2729,13 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { float castLeft = gameHandler.getTargetCastTimeRemaining(); uint32_t tspell = gameHandler.getTargetCastSpellId(); const std::string& castName = (tspell != 0) ? gameHandler.getSpellName(tspell) : ""; - // Pulse bright orange when cast is > 80% complete — interrupt window closing - ImVec4 castBarColor; - if (castPct > 0.8f) { - float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); - castBarColor = ImVec4(1.0f * pulse, 0.5f * pulse, 0.0f, 1.0f); - } else { - castBarColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f); - } - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, castBarColor); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f)); char castLabel[72]; if (!castName.empty()) snprintf(castLabel, sizeof(castLabel), "%s (%.1fs)", castName.c_str(), castLeft); else snprintf(castLabel, sizeof(castLabel), "Casting... (%.1fs)", castLeft); - { - auto* tcastAsset = core::Application::getInstance().getAssetManager(); - VkDescriptorSet tIcon = (tspell != 0 && tcastAsset) - ? getSpellIcon(tspell, tcastAsset) : VK_NULL_HANDLE; - if (tIcon) { - ImGui::Image((ImTextureID)(uintptr_t)tIcon, ImVec2(14, 14)); - ImGui::SameLine(0, 2); - ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel); - } else { - ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel); - } - } + ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel); ImGui::PopStyleColor(); } @@ -3497,31 +2769,8 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGui::Separator(); - // Build sorted index list: debuffs before buffs, shorter duration first - uint64_t tNowSort = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - std::vector sortedIdx; - sortedIdx.reserve(targetAuras.size()); - for (size_t i = 0; i < targetAuras.size(); ++i) - if (!targetAuras[i].isEmpty()) sortedIdx.push_back(i); - std::sort(sortedIdx.begin(), sortedIdx.end(), [&](size_t a, size_t b) { - const auto& aa = targetAuras[a]; const auto& ab = targetAuras[b]; - bool aDebuff = (aa.flags & 0x80) != 0; - bool bDebuff = (ab.flags & 0x80) != 0; - if (aDebuff != bDebuff) return aDebuff > bDebuff; // debuffs first - int32_t ra = aa.getRemainingMs(tNowSort); - int32_t rb = ab.getRemainingMs(tNowSort); - // Permanent (-1) goes last; shorter remaining goes first - if (ra < 0 && rb < 0) return false; - if (ra < 0) return false; - if (rb < 0) return true; - return ra < rb; - }); - int shown = 0; - for (size_t si = 0; si < sortedIdx.size() && shown < 16; ++si) { - size_t i = sortedIdx[si]; + for (size_t i = 0; i < targetAuras.size() && shown < 16; ++i) { const auto& aura = targetAuras[i]; if (aura.isEmpty()) continue; @@ -3530,20 +2779,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGui::PushID(static_cast(10000 + i)); bool isBuff = (aura.flags & 0x80) == 0; - ImVec4 auraBorderColor; - if (isBuff) { - auraBorderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f); - } else { - // Debuff: color by dispel type, matching player buff bar convention - uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); - switch (dt) { - case 1: auraBorderColor = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; // magic: blue - case 2: auraBorderColor = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; // curse: purple - case 3: auraBorderColor = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; // disease: brown - case 4: auraBorderColor = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; // poison: green - default: auraBorderColor = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; // other: red - } - } + ImVec4 auraBorderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f); VkDescriptorSet iconTex = VK_NULL_HANDLE; if (assetMgr) { @@ -3587,24 +2823,10 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImVec2 textSize = ImGui::CalcTextSize(timeStr); float cx = iconMin.x + (iconMax.x - iconMin.x - textSize.x) * 0.5f; float cy = iconMax.y - textSize.y - 1.0f; - // Color by urgency (matches player buff bar) - ImU32 tTimerColor; - if (tRemainMs < 10000) { - float pulse = 0.7f + 0.3f * std::sin( - static_cast(ImGui::GetTime()) * 6.0f); - tTimerColor = IM_COL32( - static_cast(255 * pulse), - static_cast(80 * pulse), - static_cast(60 * pulse), 255); - } else if (tRemainMs < 30000) { - tTimerColor = IM_COL32(255, 165, 0, 255); - } else { - tTimerColor = IM_COL32(255, 255, 255, 255); - } ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 200), timeStr); ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), - tTimerColor, timeStr); + IM_COL32(255, 255, 255, 255), timeStr); } // Stack / charge count — upper-left corner @@ -3678,14 +2900,8 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) { std::string totName = getEntityName(totEntity); - // Class color for players; gray for NPCs - ImVec4 totNameColor = ImVec4(0.8f, 0.8f, 0.8f, 1.0f); - if (totEntity->getType() == game::ObjectType::PLAYER) { - uint8_t cid = entityClassId(totEntity.get()); - if (cid != 0) totNameColor = classColorVec4(cid); - } // Selectable so we can attach a right-click context menu - ImGui::PushStyleColor(ImGuiCol_Text, totNameColor); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1,1,1,0.08f)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1,1,1,0.12f)); @@ -3755,9 +2971,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { // Determine color based on relation (same logic as target frame) ImVec4 focusColor(0.7f, 0.7f, 0.7f, 1.0f); if (focus->getType() == game::ObjectType::PLAYER) { - // Use class color for player focus targets - uint8_t cid = entityClassId(focus.get()); - focusColor = (cid != 0) ? classColorVec4(cid) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); } else if (focus->getType() == game::ObjectType::UNIT) { auto u = std::static_pointer_cast(focus); if (u->getHealth() == 0 && u->getMaxHealth() > 0) { @@ -3885,32 +3099,13 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { float rem = focusCast->timeRemaining; float prog = std::clamp(1.0f - rem / total, 0.f, 1.f); const std::string& spName = gameHandler.getSpellName(focusCast->spellId); - // Pulse orange when > 80% complete — interrupt window closing - ImVec4 focusCastColor; - if (prog > 0.8f) { - float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); - focusCastColor = ImVec4(1.0f * pulse, 0.5f * pulse, 0.0f, 1.0f); - } else { - focusCastColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f); - } - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, focusCastColor); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f)); char castBuf[64]; if (!spName.empty()) snprintf(castBuf, sizeof(castBuf), "%s (%.1fs)", spName.c_str(), rem); else snprintf(castBuf, sizeof(castBuf), "Casting... (%.1fs)", rem); - { - auto* fcAsset = core::Application::getInstance().getAssetManager(); - VkDescriptorSet fcIcon = (focusCast->spellId != 0 && fcAsset) - ? getSpellIcon(focusCast->spellId, fcAsset) : VK_NULL_HANDLE; - if (fcIcon) { - ImGui::Image((ImTextureID)(uintptr_t)fcIcon, ImVec2(12, 12)); - ImGui::SameLine(0, 2); - ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf); - } else { - ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf); - } - } + ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf); ImGui::PopStyleColor(); } } @@ -3992,14 +3187,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /score command — BG scoreboard - if (cmdLower == "score") { - gameHandler.requestPvpLog(); - showBgScoreboard_ = true; - chatInputBuffer[0] = '\0'; - return; - } - // /time command if (cmdLower == "time") { gameHandler.queryServerTime(); @@ -4007,20 +3194,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /zone command — print current zone name - if (cmdLower == "zone") { - std::string zoneName; - if (auto* rend = core::Application::getInstance().getRenderer()) - zoneName = rend->getCurrentZoneName(); - game::MessageChatData sysMsg; - sysMsg.type = game::ChatType::SYSTEM; - sysMsg.language = game::ChatLanguage::UNIVERSAL; - sysMsg.message = zoneName.empty() ? "You are not in a known zone." : "You are in: " + zoneName; - gameHandler.addLocalChatMessage(sysMsg); - chatInputBuffer[0] = '\0'; - return; - } - // /played command if (cmdLower == "played") { gameHandler.requestPlayedTime(); @@ -4035,39 +3208,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /chathelp command — list chat-channel slash commands - if (cmdLower == "chathelp") { - static const char* kChatHelp[] = { - "--- Chat Channel Commands ---", - "/s [msg] Say to nearby players", - "/y [msg] Yell to a wider area", - "/w [msg] Whisper to player", - "/r [msg] Reply to last whisper", - "/p [msg] Party chat", - "/g [msg] Guild chat", - "/o [msg] Guild officer chat", - "/raid [msg] Raid chat", - "/rw [msg] Raid warning", - "/bg [msg] Battleground chat", - "/1 [msg] General channel", - "/2 [msg] Trade channel (also /wts /wtb)", - "/ [msg] Channel by number", - "/join Join a channel", - "/leave Leave a channel", - "/afk [msg] Set AFK status", - "/dnd [msg] Set Do Not Disturb", - }; - for (const char* line : kChatHelp) { - game::MessageChatData helpMsg; - helpMsg.type = game::ChatType::SYSTEM; - helpMsg.language = game::ChatLanguage::UNIVERSAL; - helpMsg.message = line; - gameHandler.addLocalChatMessage(helpMsg); - } - chatInputBuffer[0] = '\0'; - return; - } - // /help command — list available slash commands if (cmdLower == "help" || cmdLower == "?") { static const char* kHelpLines[] = { @@ -4079,14 +3219,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { " /maintank /mainassist /roll [min-max]", "Guild: /ginvite /gkick /gquit /gpromote /gdemote /gmotd", " /gleader /groster /ginfo /gcreate /gdisband", - "Combat: /startattack /stopattack /stopcasting /cast /duel /pvp", - " /forfeit /follow /stopfollow /assist", - "Items: /use /equip ", + "Combat: /startattack /stopattack /stopcasting /duel /pvp", + " /forfeit /follow /assist", "Target: /target /cleartarget /focus /clearfocus", "Movement: /sit /stand /kneel /dismount", - "Misc: /played /time /zone /afk [msg] /dnd [msg] /inspect", + "Misc: /played /time /afk [msg] /dnd [msg] /inspect", " /helm /cloak /trade /join /leave ", - " /score /unstuck /logout /ticket /help", + " /unstuck /logout /ticket /help", }; for (const char* line : kHelpLines) { game::MessageChatData helpMsg; @@ -4135,14 +3274,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } gameHandler.queryWho(query); - showWhoWindow_ = true; - chatInputBuffer[0] = '\0'; - return; - } - - // /combatlog command - if (cmdLower == "combatlog" || cmdLower == "cl") { - showCombatLog_ = !showCombatLog_; chatInputBuffer[0] = '\0'; return; } @@ -4337,13 +3468,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /stopfollow command - if (cmdLower == "stopfollow") { - gameHandler.cancelFollow(); - chatInputBuffer[0] = '\0'; - return; - } - // /assist command if (cmdLower == "assist") { gameHandler.assistTarget(); @@ -4712,177 +3836,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - if (cmdLower == "cast" && spacePos != std::string::npos) { - std::string spellArg = command.substr(spacePos + 1); - // Trim leading/trailing whitespace - while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin()); - while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back(); - - // Parse optional "(Rank N)" suffix: "Fireball(Rank 3)" or "Fireball (Rank 3)" - int requestedRank = -1; // -1 = highest rank - std::string spellName = spellArg; - { - auto rankPos = spellArg.find('('); - if (rankPos != std::string::npos) { - std::string rankStr = spellArg.substr(rankPos + 1); - // Strip closing paren and whitespace - auto closePos = rankStr.find(')'); - if (closePos != std::string::npos) rankStr = rankStr.substr(0, closePos); - for (char& c : rankStr) c = static_cast(std::tolower(static_cast(c))); - // Expect "rank N" - if (rankStr.rfind("rank ", 0) == 0) { - try { requestedRank = std::stoi(rankStr.substr(5)); } catch (...) {} - } - spellName = spellArg.substr(0, rankPos); - while (!spellName.empty() && spellName.back() == ' ') spellName.pop_back(); - } - } - - std::string spellNameLower = spellName; - for (char& c : spellNameLower) c = static_cast(std::tolower(static_cast(c))); - - // Search known spells for a name match; pick highest rank (or specific rank) - uint32_t bestSpellId = 0; - int bestRank = -1; - for (uint32_t sid : gameHandler.getKnownSpells()) { - const std::string& sName = gameHandler.getSpellName(sid); - if (sName.empty()) continue; - std::string sNameLower = sName; - for (char& c : sNameLower) c = static_cast(std::tolower(static_cast(c))); - if (sNameLower != spellNameLower) continue; - - // Parse numeric rank from rank string ("Rank 3" → 3, "" → 0) - int sRank = 0; - const std::string& rankStr = gameHandler.getSpellRank(sid); - if (!rankStr.empty()) { - std::string rLow = rankStr; - for (char& c : rLow) c = static_cast(std::tolower(static_cast(c))); - if (rLow.rfind("rank ", 0) == 0) { - try { sRank = std::stoi(rLow.substr(5)); } catch (...) {} - } - } - - if (requestedRank >= 0) { - if (sRank == requestedRank) { bestSpellId = sid; break; } - } else { - if (sRank > bestRank) { bestRank = sRank; bestSpellId = sid; } - } - } - - if (bestSpellId) { - uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; - gameHandler.castSpell(bestSpellId, targetGuid); - } else { - game::MessageChatData sysMsg; - sysMsg.type = game::ChatType::SYSTEM; - sysMsg.language = game::ChatLanguage::UNIVERSAL; - sysMsg.message = requestedRank >= 0 - ? "You don't know '" + spellName + "' (Rank " + std::to_string(requestedRank) + ")." - : "Unknown spell: '" + spellName + "'."; - gameHandler.addLocalChatMessage(sysMsg); - } - chatInputBuffer[0] = '\0'; - return; - } - - // /use — use an item from backpack/bags by name - if (cmdLower == "use" && spacePos != std::string::npos) { - std::string useArg = command.substr(spacePos + 1); - while (!useArg.empty() && useArg.front() == ' ') useArg.erase(useArg.begin()); - while (!useArg.empty() && useArg.back() == ' ') useArg.pop_back(); - std::string useArgLower = useArg; - for (char& c : useArgLower) c = static_cast(std::tolower(static_cast(c))); - - bool found = false; - const auto& inv = gameHandler.getInventory(); - // Search backpack - for (int s = 0; s < inv.getBackpackSize() && !found; ++s) { - const auto& slot = inv.getBackpackSlot(s); - if (slot.empty()) continue; - const auto* info = gameHandler.getItemInfo(slot.item.itemId); - if (!info) continue; - std::string nameLow = info->name; - for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); - if (nameLow == useArgLower) { - gameHandler.useItemBySlot(s); - found = true; - } - } - // Search bags - for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) { - for (int s = 0; s < inv.getBagSize(b) && !found; ++s) { - const auto& slot = inv.getBagSlot(b, s); - if (slot.empty()) continue; - const auto* info = gameHandler.getItemInfo(slot.item.itemId); - if (!info) continue; - std::string nameLow = info->name; - for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); - if (nameLow == useArgLower) { - gameHandler.useItemInBag(b, s); - found = true; - } - } - } - if (!found) { - game::MessageChatData sysMsg; - sysMsg.type = game::ChatType::SYSTEM; - sysMsg.language = game::ChatLanguage::UNIVERSAL; - sysMsg.message = "Item not found: '" + useArg + "'."; - gameHandler.addLocalChatMessage(sysMsg); - } - chatInputBuffer[0] = '\0'; - return; - } - - // /equip — auto-equip an item from backpack/bags by name - if (cmdLower == "equip" && spacePos != std::string::npos) { - std::string equipArg = command.substr(spacePos + 1); - while (!equipArg.empty() && equipArg.front() == ' ') equipArg.erase(equipArg.begin()); - while (!equipArg.empty() && equipArg.back() == ' ') equipArg.pop_back(); - std::string equipArgLower = equipArg; - for (char& c : equipArgLower) c = static_cast(std::tolower(static_cast(c))); - - bool found = false; - const auto& inv = gameHandler.getInventory(); - // Search backpack - for (int s = 0; s < inv.getBackpackSize() && !found; ++s) { - const auto& slot = inv.getBackpackSlot(s); - if (slot.empty()) continue; - const auto* info = gameHandler.getItemInfo(slot.item.itemId); - if (!info) continue; - std::string nameLow = info->name; - for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); - if (nameLow == equipArgLower) { - gameHandler.autoEquipItemBySlot(s); - found = true; - } - } - // Search bags - for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) { - for (int s = 0; s < inv.getBagSize(b) && !found; ++s) { - const auto& slot = inv.getBagSlot(b, s); - if (slot.empty()) continue; - const auto* info = gameHandler.getItemInfo(slot.item.itemId); - if (!info) continue; - std::string nameLow = info->name; - for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); - if (nameLow == equipArgLower) { - gameHandler.autoEquipItemInBag(b, s); - found = true; - } - } - } - if (!found) { - game::MessageChatData sysMsg; - sysMsg.type = game::ChatType::SYSTEM; - sysMsg.language = game::ChatLanguage::UNIVERSAL; - sysMsg.message = "Item not found: '" + equipArg + "'."; - gameHandler.addLocalChatMessage(sysMsg); - } - chatInputBuffer[0] = '\0'; - return; - } - // Targeting commands if (cmdLower == "cleartarget") { gameHandler.clearTarget(); @@ -5113,31 +4066,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } chatInputBuffer[0] = '\0'; return; - } else if ((cmdLower == "wts" || cmdLower == "wtb") && spacePos != std::string::npos) { - // /wts and /wtb — send to Trade channel - // Prefix with [WTS] / [WTB] and route to the Trade channel - const std::string tag = (cmdLower == "wts") ? "[WTS] " : "[WTB] "; - const std::string body = command.substr(spacePos + 1); - // Find the Trade channel among joined channels (case-insensitive prefix match) - std::string tradeChan; - for (const auto& ch : gameHandler.getJoinedChannels()) { - std::string chLow = ch; - for (char& c : chLow) c = static_cast(std::tolower(static_cast(c))); - if (chLow.rfind("trade", 0) == 0) { tradeChan = ch; break; } - } - if (tradeChan.empty()) { - game::MessageChatData errMsg; - errMsg.type = game::ChatType::SYSTEM; - errMsg.language = game::ChatLanguage::UNIVERSAL; - errMsg.message = "You are not in the Trade channel."; - gameHandler.addLocalChatMessage(errMsg); - chatInputBuffer[0] = '\0'; - return; - } - message = tag + body; - type = game::ChatType::CHANNEL; - target = tradeChan; - isChannelCommand = true; } else if (cmdLower.size() == 1 && cmdLower[0] >= '1' && cmdLower[0] <= '9') { // /1 msg, /2 msg — channel shortcuts int channelIdx = cmdLower[0] - '0'; @@ -5244,14 +4172,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { case 7: type = game::ChatType::BATTLEGROUND; break; case 8: type = game::ChatType::RAID_WARNING; break; case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY - case 10: { // CHANNEL - const auto& chans = gameHandler.getJoinedChannels(); - if (!chans.empty() && selectedChannelIdx < static_cast(chans.size())) { - type = game::ChatType::CHANNEL; - target = chans[selectedChannelIdx]; - } else { type = game::ChatType::SAY; } - break; - } default: type = game::ChatType::SAY; break; } } @@ -5268,14 +4188,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { case 7: type = game::ChatType::BATTLEGROUND; break; case 8: type = game::ChatType::RAID_WARNING; break; case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY - case 10: { // CHANNEL - const auto& chans = gameHandler.getJoinedChannels(); - if (!chans.empty() && selectedChannelIdx < static_cast(chans.size())) { - type = game::ChatType::CHANNEL; - target = chans[selectedChannelIdx]; - } else { type = game::ChatType::SAY; } - break; - } default: type = game::ChatType::SAY; break; } } @@ -5865,48 +4777,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { const auto& slot = bar[absSlot]; bool onCooldown = !slot.isReady(); - const bool onGCD = gameHandler.isGCDActive() && !onCooldown && !slot.isEmpty(); - - // Out-of-range check: red tint when a targeted spell cannot reach the current target. - // Only applies to SPELL slots with a known max range (>5 yd) and an active target. - bool outOfRange = false; - if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 - && !onCooldown && gameHandler.hasTarget()) { - uint32_t maxRange = spellbookScreen.getSpellMaxRange(slot.id, assetMgr); - if (maxRange > 5) { // >5 yd = not melee/self - auto& em = gameHandler.getEntityManager(); - auto playerEnt = em.getEntity(gameHandler.getPlayerGuid()); - auto targetEnt = em.getEntity(gameHandler.getTargetGuid()); - if (playerEnt && targetEnt) { - float dx = playerEnt->getX() - targetEnt->getX(); - float dy = playerEnt->getY() - targetEnt->getY(); - float dz = playerEnt->getZ() - targetEnt->getZ(); - float dist = std::sqrt(dx * dx + dy * dy + dz * dz); - if (dist > static_cast(maxRange)) - outOfRange = true; - } - } - } - - // Insufficient-power check: orange tint when player doesn't have enough power to cast. - // Only applies to SPELL slots with a known power cost and when not already on cooldown. - bool insufficientPower = false; - if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 - && !onCooldown) { - uint32_t spellCost = 0, spellPowerType = 0; - spellbookScreen.getSpellPowerInfo(slot.id, assetMgr, spellCost, spellPowerType); - if (spellCost > 0) { - auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); - if (playerEnt && (playerEnt->getType() == game::ObjectType::PLAYER || - playerEnt->getType() == game::ObjectType::UNIT)) { - auto unit = std::static_pointer_cast(playerEnt); - if (unit->getPowerType() == static_cast(spellPowerType)) { - if (unit->getPower() < spellCost) - insufficientPower = true; - } - } - } - } auto getSpellName = [&](uint32_t spellId) -> std::string { std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr); @@ -5954,31 +4824,20 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId); } - // Item-missing check: grey out item slots whose item is not in the player's inventory. - const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0 - && barItemDef == nullptr && !onCooldown); - bool clicked = false; if (iconTex) { ImVec4 tintColor(1, 1, 1, 1); ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f); - if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); } - else if (onGCD) { tintColor = ImVec4(0.6f, 0.6f, 0.6f, 0.85f); } - else if (outOfRange) { tintColor = ImVec4(0.85f, 0.35f, 0.35f, 0.9f); } - else if (insufficientPower) { tintColor = ImVec4(0.6f, 0.5f, 0.9f, 0.85f); } - else if (itemMissing) { tintColor = ImVec4(0.35f, 0.35f, 0.35f, 0.7f); } + if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); } clicked = ImGui::ImageButton("##icon", (ImTextureID)(uintptr_t)iconTex, ImVec2(slotSize, slotSize), ImVec2(0, 0), ImVec2(1, 1), bgColor, tintColor); } else { - if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - else if (outOfRange) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.45f, 0.15f, 0.15f, 0.9f)); - else if (insufficientPower)ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.15f, 0.4f, 0.9f)); - else if (itemMissing) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.12f, 0.12f, 0.7f)); - else if (slot.isEmpty()) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); - else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f)); + if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + else if (slot.isEmpty())ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); + else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f)); char label[32]; if (slot.type == game::ActionBarSlot::SPELL) { @@ -6086,12 +4945,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName); } } - if (outOfRange) { - ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Out of range"); - } - if (insufficientPower) { - ImGui::TextColored(ImVec4(0.75f, 0.55f, 1.0f, 1.0f), "Not enough power"); - } if (onCooldown) { float cd = slot.cooldownRemaining; if (cd >= 60.0f) @@ -6160,99 +5013,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { dl->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 255), cdText); } - // GCD overlay — subtle dark fan sweep (thinner/lighter than regular cooldown) - if (onGCD) { - ImVec2 btnMin = ImGui::GetItemRectMin(); - ImVec2 btnMax = ImGui::GetItemRectMax(); - float cx = (btnMin.x + btnMax.x) * 0.5f; - float cy = (btnMin.y + btnMax.y) * 0.5f; - float r = (btnMax.x - btnMin.x) * 0.5f; - auto* dl = ImGui::GetWindowDrawList(); - float gcdRem = gameHandler.getGCDRemaining(); - float gcdTotal = gameHandler.getGCDTotal(); - if (gcdTotal > 0.0f) { - float elapsed = gcdTotal - gcdRem; - float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / gcdTotal)); - if (elapsedFrac > 0.005f) { - constexpr int N_SEGS = 24; - float startAngle = -IM_PI * 0.5f; - float endAngle = startAngle + elapsedFrac * 2.0f * IM_PI; - float fanR = r * 1.4f; - ImVec2 pts[N_SEGS + 2]; - pts[0] = ImVec2(cx, cy); - for (int s = 0; s <= N_SEGS; ++s) { - float a = startAngle + (endAngle - startAngle) * s / static_cast(N_SEGS); - pts[s + 1] = ImVec2(cx + std::cos(a) * fanR, cy + std::sin(a) * fanR); - } - dl->AddConvexPolyFilled(pts, N_SEGS + 2, IM_COL32(0, 0, 0, 110)); - } - } - } - - // Item stack count overlay — bottom-right corner of icon - if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { - // Count total of this item across all inventory slots - auto& inv = gameHandler.getInventory(); - int totalCount = 0; - for (int bi = 0; bi < inv.getBackpackSize(); bi++) { - const auto& bs = inv.getBackpackSlot(bi); - if (!bs.empty() && bs.item.itemId == slot.id) totalCount += bs.item.stackCount; - } - for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) { - for (int si = 0; si < inv.getBagSize(bag); si++) { - const auto& bs = inv.getBagSlot(bag, si); - if (!bs.empty() && bs.item.itemId == slot.id) totalCount += bs.item.stackCount; - } - } - if (totalCount > 0) { - char countStr[8]; - snprintf(countStr, sizeof(countStr), "%d", totalCount); - ImVec2 btnMax = ImGui::GetItemRectMax(); - ImVec2 tsz = ImGui::CalcTextSize(countStr); - float cx2 = btnMax.x - tsz.x - 2.0f; - float cy2 = btnMax.y - tsz.y - 1.0f; - auto* cdl = ImGui::GetWindowDrawList(); - cdl->AddText(ImVec2(cx2 + 1.0f, cy2 + 1.0f), IM_COL32(0, 0, 0, 200), countStr); - cdl->AddText(ImVec2(cx2, cy2), - totalCount <= 1 ? IM_COL32(220, 100, 100, 255) : IM_COL32(255, 255, 255, 255), - countStr); - } - } - - // Ready glow: animate a gold border for ~1.5s when a cooldown just expires - { - static std::unordered_map slotGlowTimers; // absSlot -> remaining glow seconds - static std::unordered_map slotWasOnCooldown; // absSlot -> last frame state - - float dt = ImGui::GetIO().DeltaTime; - bool wasOnCd = slotWasOnCooldown.count(absSlot) ? slotWasOnCooldown[absSlot] : false; - - // Trigger glow when transitioning from on-cooldown to ready (and slot isn't empty) - if (wasOnCd && !onCooldown && !slot.isEmpty()) { - slotGlowTimers[absSlot] = 1.5f; - } - slotWasOnCooldown[absSlot] = onCooldown; - - auto git = slotGlowTimers.find(absSlot); - if (git != slotGlowTimers.end() && git->second > 0.0f) { - git->second -= dt; - float t = git->second / 1.5f; // 1.0 → 0.0 over lifetime - // Pulse: bright when fresh, fading out - float pulse = std::sin(t * IM_PI * 4.0f) * 0.5f + 0.5f; // 4 pulses - uint8_t alpha = static_cast(200 * t * (0.5f + 0.5f * pulse)); - if (alpha > 0) { - ImVec2 bMin = ImGui::GetItemRectMin(); - ImVec2 bMax = ImGui::GetItemRectMax(); - auto* gdl = ImGui::GetWindowDrawList(); - // Gold glow border (2px inset, 3px thick) - gdl->AddRect(ImVec2(bMin.x - 2, bMin.y - 2), - ImVec2(bMax.x + 2, bMax.y + 2), - IM_COL32(255, 215, 0, alpha), 3.0f, 0, 3.0f); - } - if (git->second <= 0.0f) slotGlowTimers.erase(git); - } - } - // Key label below ImGui::TextDisabled("%s", keyLabel); @@ -6782,26 +5542,6 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { drawList->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), overlay); ImGui::Dummy(barSize); - - // Tooltip with XP-to-level and rested details - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - uint32_t xpToLevel = (currentXp < nextLevelXp) ? (nextLevelXp - currentXp) : 0; - ImGui::TextColored(ImVec4(0.9f, 0.85f, 1.0f, 1.0f), "Experience"); - ImGui::Separator(); - ImGui::Text("Current: %u / %u XP", currentXp, nextLevelXp); - ImGui::Text("To next level: %u XP", xpToLevel); - if (restedXp > 0) { - float restedLevels = static_cast(restedXp) / static_cast(nextLevelXp); - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.78f, 0.60f, 1.0f, 1.0f), - "Rested: +%u XP (%.1f%% of a level)", restedXp, restedLevels * 100.0f); - if (isResting) - ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), - "Resting — accumulating bonus XP"); - } - ImGui::EndTooltip(); - } } ImGui::End(); @@ -6809,126 +5549,6 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { ImGui::PopStyleVar(2); } -// ============================================================ -// Reputation Bar -// ============================================================ - -void GameScreen::renderRepBar(game::GameHandler& gameHandler) { - uint32_t factionId = gameHandler.getWatchedFactionId(); - if (factionId == 0) return; - - const auto& standings = gameHandler.getFactionStandings(); - auto it = standings.find(factionId); - if (it == standings.end()) return; - - int32_t standing = it->second; - - // WoW reputation rank thresholds - struct RepRank { const char* name; int32_t min; int32_t max; ImU32 color; }; - static const RepRank kRanks[] = { - { "Hated", -42000, -6001, IM_COL32(180, 40, 40, 255) }, - { "Hostile", -6000, -3001, IM_COL32(180, 40, 40, 255) }, - { "Unfriendly", -3000, -1, IM_COL32(220, 100, 50, 255) }, - { "Neutral", 0, 2999, IM_COL32(200, 200, 60, 255) }, - { "Friendly", 3000, 8999, IM_COL32( 60, 180, 60, 255) }, - { "Honored", 9000, 20999, IM_COL32( 60, 160, 220, 255) }, - { "Revered", 21000, 41999, IM_COL32(140, 80, 220, 255) }, - { "Exalted", 42000, 42999, IM_COL32(255, 200, 50, 255) }, - }; - constexpr int kNumRanks = static_cast(sizeof(kRanks) / sizeof(kRanks[0])); - - int rankIdx = kNumRanks - 1; // default to Exalted - for (int i = 0; i < kNumRanks; ++i) { - if (standing <= kRanks[i].max) { rankIdx = i; break; } - } - const RepRank& rank = kRanks[rankIdx]; - - float fraction = 1.0f; - if (rankIdx < kNumRanks - 1) { - float range = static_cast(rank.max - rank.min + 1); - fraction = static_cast(standing - rank.min) / range; - fraction = std::max(0.0f, std::min(1.0f, fraction)); - } - - const std::string& factionName = gameHandler.getFactionNamePublic(factionId); - - // Position directly above the XP bar - ImVec2 displaySize = ImGui::GetIO().DisplaySize; - float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; - float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - - float slotSize = 48.0f * pendingActionBarScale; - float spacing = 4.0f; - float padding = 8.0f; - float barW = 12 * slotSize + 11 * spacing + padding * 2; - float barH_ab = slotSize + 24.0f; - float xpBarH = 20.0f; - float repBarH = 12.0f; - float xpBarW = barW; - float xpBarX = (screenW - xpBarW) / 2.0f; - - float bar1TopY = screenH - barH_ab; - float xpBarY; - if (pendingShowActionBar2) { - float bar2TopY = bar1TopY - barH_ab - 2.0f + pendingActionBar2OffsetY; - xpBarY = bar2TopY - xpBarH - 2.0f; - } else { - xpBarY = bar1TopY - xpBarH - 2.0f; - } - float repBarY = xpBarY - repBarH - 2.0f; - - ImGui::SetNextWindowPos(ImVec2(xpBarX, repBarY), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(xpBarW, repBarH + 4.0f), ImGuiCond_Always); - - ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 2.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(2.0f, 2.0f)); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.8f)); - - if (ImGui::Begin("##RepBar", nullptr, flags)) { - ImVec2 barMin = ImGui::GetCursorScreenPos(); - ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, repBarH - 4.0f); - ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y); - auto* dl = ImGui::GetWindowDrawList(); - - dl->AddRectFilled(barMin, barMax, IM_COL32(15, 15, 20, 220), 2.0f); - dl->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f); - - float fillW = barSize.x * fraction; - if (fillW > 0.0f) - dl->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), rank.color, 2.0f); - - // Label: "FactionName - Rank" - char label[96]; - snprintf(label, sizeof(label), "%s - %s", factionName.c_str(), rank.name); - ImVec2 textSize = ImGui::CalcTextSize(label); - float tx = barMin.x + (barSize.x - textSize.x) * 0.5f; - float ty = barMin.y + (barSize.y - textSize.y) * 0.5f; - dl->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), label); - - // Tooltip with exact values on hover - ImGui::Dummy(barSize); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - float cr = ((rank.color ) & 0xFF) / 255.0f; - float cg = ((rank.color >> 8) & 0xFF) / 255.0f; - float cb = ((rank.color >> 16) & 0xFF) / 255.0f; - ImGui::TextColored(ImVec4(cr, cg, cb, 1.0f), "%s", rank.name); - int32_t rankMin = rank.min; - int32_t rankMax = (rankIdx < kNumRanks - 1) ? rank.max : 42000; - ImGui::Text("%s: %d / %d", factionName.c_str(), standing - rankMin, rankMax - rankMin + 1); - ImGui::EndTooltip(); - } - } - ImGui::End(); - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(2); -} - // ============================================================ // Cast Bar (Phase 3) // ============================================================ @@ -6936,16 +5556,10 @@ void GameScreen::renderRepBar(game::GameHandler& gameHandler) { void GameScreen::renderCastBar(game::GameHandler& gameHandler) { if (!gameHandler.isCasting()) return; - auto* assetMgr = core::Application::getInstance().getAssetManager(); - ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - uint32_t currentSpellId = gameHandler.getCurrentCastSpellId(); - VkDescriptorSet iconTex = (currentSpellId != 0 && assetMgr) - ? getSpellIcon(currentSpellId, assetMgr) : VK_NULL_HANDLE; - float barW = 300.0f; float barX = (screenW - barW) / 2.0f; float barY = screenH - 120.0f; @@ -6973,6 +5587,7 @@ void GameScreen::renderCastBar(game::GameHandler& gameHandler) { ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); char overlay[64]; + uint32_t currentSpellId = gameHandler.getCurrentCastSpellId(); if (currentSpellId == 0) { snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining()); } else { @@ -6983,15 +5598,7 @@ void GameScreen::renderCastBar(game::GameHandler& gameHandler) { else snprintf(overlay, sizeof(overlay), "%s... (%.1fs)", verb, gameHandler.getCastTimeRemaining()); } - - if (iconTex) { - // Spell icon to the left of the progress bar - ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(20, 20)); - ImGui::SameLine(0, 4); - ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay); - } else { - ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay); - } + ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay); ImGui::PopStyleColor(); } ImGui::End(); @@ -7137,11 +5744,6 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { gameHandler.setQuestTracked(q.questId, true); } } - if (gameHandler.isInGroup() && !q.complete) { - if (ImGui::MenuItem("Share Quest")) { - gameHandler.shareQuestWithParty(q.questId); - } - } if (!q.complete) { ImGui::Separator(); if (ImGui::MenuItem("Abandon Quest")) { @@ -7157,33 +5759,28 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { if (q.complete) { ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), " (Complete)"); } else { - // Kill counts — green when complete, gray when in progress + // Kill counts for (const auto& [entry, progress] : q.killCounts) { - bool objDone = (progress.first >= progress.second && progress.second > 0); - ImVec4 objColor = objDone ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) - : ImVec4(0.75f, 0.75f, 0.75f, 1.0f); std::string name = gameHandler.getCachedCreatureName(entry); if (name.empty()) { + // May be a game object objective; fall back to GO name cache. const auto* goInfo = gameHandler.getCachedGameObjectInfo(entry); if (goInfo && !goInfo->name.empty()) name = goInfo->name; } if (!name.empty()) { - ImGui::TextColored(objColor, + ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), " %s: %u/%u", name.c_str(), progress.first, progress.second); } else { - ImGui::TextColored(objColor, + ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), " %u/%u", progress.first, progress.second); } } - // Item counts — green when complete, gray when in progress + // Item counts for (const auto& [itemId, count] : q.itemCounts) { uint32_t required = 1; auto reqIt = q.requiredItemCounts.find(itemId); if (reqIt != q.requiredItemCounts.end()) required = reqIt->second; - bool objDone = (count >= required); - ImVec4 objColor = objDone ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) - : ImVec4(0.75f, 0.75f, 0.75f, 1.0f); const auto* info = gameHandler.getItemInfo(itemId); const char* itemName = (info && !info->name.empty()) ? info->name.c_str() : nullptr; @@ -7193,13 +5790,13 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { if (iconTex) { ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(12, 12)); ImGui::SameLine(0, 3); - ImGui::TextColored(objColor, + ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), "%s: %u/%u", itemName ? itemName : "Item", count, required); } else if (itemName) { - ImGui::TextColored(objColor, + ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), " %s: %u/%u", itemName, count, required); } else { - ImGui::TextColored(objColor, + ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), " Item: %u/%u", count, required); } } @@ -7238,7 +5835,6 @@ void GameScreen::renderRaidWarningOverlay(game::GameHandler& gameHandler) { // Walk only the new messages (deque — iterate from back by skipping old ones) size_t toScan = newCount - raidWarnChatSeenCount_; size_t startIdx = newCount > toScan ? newCount - toScan : 0; - auto* renderer = core::Application::getInstance().getRenderer(); for (size_t i = startIdx; i < newCount; ++i) { const auto& msg = chatHistory[i]; if (msg.type == game::ChatType::RAID_WARNING || @@ -7252,11 +5848,6 @@ void GameScreen::renderRaidWarningOverlay(game::GameHandler& gameHandler) { if (raidWarnEntries_.size() > 3) raidWarnEntries_.erase(raidWarnEntries_.begin()); } - // Whisper audio notification - if (msg.type == game::ChatType::WHISPER && renderer) { - if (auto* ui = renderer->getUiSoundManager()) - ui->playWhisperReceived(); - } } raidWarnChatSeenCount_ = newCount; } @@ -7440,15 +6031,6 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { snprintf(text, sizeof(text), "Resisted"); color = ImVec4(0.7f, 0.7f, 0.7f, alpha); // Grey for resist break; - case game::CombatTextEntry::PROC_TRIGGER: { - const std::string& procName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; - if (!procName.empty()) - snprintf(text, sizeof(text), "%s!", procName.c_str()); - else - snprintf(text, sizeof(text), "PROC!"); - color = ImVec4(1.0f, 0.85f, 0.0f, alpha); // Gold for proc - break; - } default: snprintf(text, sizeof(text), "%d", entry.amount); color = ImVec4(1.0f, 1.0f, 1.0f, alpha); @@ -7460,29 +6042,8 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { float baseX = outgoing ? outgoingX : incomingX; float xOffset = baseX + (idx % 3 - 1) * 60.0f; ++idx; - - // Crits render at 1.35× normal font size for visual impact - bool isCrit = (entry.type == game::CombatTextEntry::CRIT_DAMAGE || - entry.type == game::CombatTextEntry::CRIT_HEAL); - ImFont* font = ImGui::GetFont(); - float baseFontSize = ImGui::GetFontSize(); - float renderFontSize = isCrit ? baseFontSize * 1.35f : baseFontSize; - - // Advance cursor so layout accounting is correct, then read screen pos ImGui::SetCursorPos(ImVec2(xOffset, yOffset)); - ImVec2 screenPos = ImGui::GetCursorScreenPos(); - - // Drop shadow for readability over complex backgrounds - ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast(alpha * 180)); - ImU32 textCol = ImGui::ColorConvertFloat4ToU32(color); - ImDrawList* dl = ImGui::GetWindowDrawList(); - dl->AddText(font, renderFontSize, ImVec2(screenPos.x + 1.0f, screenPos.y + 1.0f), - shadowCol, text); - dl->AddText(font, renderFontSize, screenPos, textCol, text); - - // Reserve space so ImGui doesn't clip the window prematurely - ImVec2 ts = font->CalcTextSizeA(renderFontSize, FLT_MAX, 0.0f, text); - ImGui::Dummy(ts); + ImGui::TextColored(color, "%s", text); } } ImGui::End(); @@ -7500,37 +6061,11 @@ void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) { // Track combat duration for accurate DPS denominator in short fights bool inCombat = gameHandler.isInCombat(); - if (inCombat && !dpsWasInCombat_) { - // Just entered combat — reset encounter accumulators - dpsEncounterDamage_ = 0.0f; - dpsEncounterHeal_ = 0.0f; - dpsLogSeenCount_ = gameHandler.getCombatLog().size(); - dpsCombatAge_ = 0.0f; - } if (inCombat) { dpsCombatAge_ += dt; - // Scan any new log entries since last frame - const auto& log = gameHandler.getCombatLog(); - while (dpsLogSeenCount_ < log.size()) { - const auto& e = log[dpsLogSeenCount_++]; - if (!e.isPlayerSource) continue; - switch (e.type) { - case game::CombatTextEntry::MELEE_DAMAGE: - case game::CombatTextEntry::SPELL_DAMAGE: - case game::CombatTextEntry::CRIT_DAMAGE: - case game::CombatTextEntry::PERIODIC_DAMAGE: - dpsEncounterDamage_ += static_cast(e.amount); - break; - case game::CombatTextEntry::HEAL: - case game::CombatTextEntry::CRIT_HEAL: - case game::CombatTextEntry::PERIODIC_HEAL: - dpsEncounterHeal_ += static_cast(e.amount); - break; - default: break; - } - } } else if (dpsWasInCombat_) { - // Just left combat — keep encounter totals but stop accumulating + // Just left combat — let meter show last reading for LIFETIME then reset + dpsCombatAge_ = 0.0f; } dpsWasInCombat_ = inCombat; @@ -7554,9 +6089,8 @@ void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) { } } - // Only show if there's something to report (rolling window or lingering encounter data) - if (totalDamage < 1.0f && totalHeal < 1.0f && !inCombat && - dpsEncounterDamage_ < 1.0f && dpsEncounterHeal_ < 1.0f) return; + // Only show if there's something to report + if (totalDamage < 1.0f && totalHeal < 1.0f && !inCombat) return; // DPS window = min(combat age, combat-text lifetime) to avoid under-counting // at the start of a fight and over-counting when entries expire. @@ -7582,22 +6116,8 @@ void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) { float screenW = appWin ? static_cast(appWin->getWidth()) : 1280.0f; float screenH = appWin ? static_cast(appWin->getHeight()) : 720.0f; - // Show encounter row when fight has been going long enough (> 3s) - bool showEnc = (dpsCombatAge_ > 3.0f || (!inCombat && dpsEncounterDamage_ > 0.0f)); - float encDPS = (dpsCombatAge_ > 0.1f) ? dpsEncounterDamage_ / dpsCombatAge_ : 0.0f; - float encHPS = (dpsCombatAge_ > 0.1f) ? dpsEncounterHeal_ / dpsCombatAge_ : 0.0f; - - char encDpsBuf[16], encHpsBuf[16]; - fmtNum(encDPS, encDpsBuf, sizeof(encDpsBuf)); - fmtNum(encHPS, encHpsBuf, sizeof(encHpsBuf)); - constexpr float WIN_W = 90.0f; - // Extra rows for encounter DPS/HPS if active - int extraRows = 0; - if (showEnc && encDPS > 0.5f) ++extraRows; - if (showEnc && encHPS > 0.5f) ++extraRows; - float WIN_H = 18.0f + extraRows * 14.0f; - if (dps > 0.5f || hps > 0.5f) WIN_H = std::max(WIN_H, 36.0f); + constexpr float WIN_H = 36.0f; float wx = screenW * 0.5f + 160.0f; // right of cast bar float wy = screenH - 130.0f; // above action bar area @@ -7624,17 +6144,6 @@ void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) { ImGui::SameLine(0, 2); ImGui::TextDisabled("hps"); } - // Encounter totals (full-fight average, shown when fight > 3s) - if (showEnc && encDPS > 0.5f) { - ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.25f, 0.80f), "%s", encDpsBuf); - ImGui::SameLine(0, 2); - ImGui::TextDisabled("enc"); - } - if (showEnc && encHPS > 0.5f) { - ImGui::TextColored(ImVec4(0.50f, 1.0f, 0.50f, 0.80f), "%s", encHpsBuf); - ImGui::SameLine(0, 2); - ImGui::TextDisabled("enc"); - } } ImGui::End(); @@ -7782,7 +6291,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { // Cast bar below health bar when unit is casting float castBarBaseY = sy + barH + 2.0f; - float nameplateBottom = castBarBaseY; // tracks lowest drawn element for debuff dots { const auto* cs = gameHandler.getUnitCastState(guid); if (cs && cs->casting && cs->timeTotal > 0.0f) { @@ -7800,15 +6308,9 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { castBarBaseY += snSz.y + 2.0f; } - // Cast bar background + fill (pulse orange when >80% = interrupt window closing) - ImU32 cbBg = IM_COL32(40, 30, 60, A(180)); - ImU32 cbFill; - if (castPct > 0.8f && unit->isHostile()) { - float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); - cbFill = IM_COL32(static_cast(255 * pulse), static_cast(130 * pulse), 0, A(220)); - } else { - cbFill = IM_COL32(140, 80, 220, A(200)); // purple cast bar - } + // Cast bar background + fill + ImU32 cbBg = IM_COL32(40, 30, 60, A(180)); + ImU32 cbFill = IM_COL32(140, 80, 220, A(200)); // purple cast bar drawList->AddRectFilled(ImVec2(barX, castBarBaseY), ImVec2(barX + barW, castBarBaseY + cbH), cbBg, 2.0f); drawList->AddRectFilled(ImVec2(barX, castBarBaseY), @@ -7825,37 +6327,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { float timeY = castBarBaseY + (cbH - timeSz.y) * 0.5f; drawList->AddText(ImVec2(timeX + 1.0f, timeY + 1.0f), IM_COL32(0, 0, 0, A(140)), timeBuf); drawList->AddText(ImVec2(timeX, timeY), IM_COL32(220, 200, 255, A(220)), timeBuf); - nameplateBottom = castBarBaseY + cbH + 2.0f; - } - } - - // Debuff dot indicators: small colored squares below the nameplate showing - // player-applied auras on the current hostile target. - // Colors: Magic=blue, Curse=purple, Disease=yellow, Poison=green, Other=grey - if (isTarget && unit->isHostile() && !isCorpse) { - const auto& auras = gameHandler.getTargetAuras(); - const uint64_t pguid = gameHandler.getPlayerGuid(); - const float dotSize = 6.0f * nameplateScale_; - const float dotGap = 2.0f; - float dotX = barX; - for (const auto& aura : auras) { - if (aura.isEmpty() || aura.casterGuid != pguid) continue; - uint8_t dispelType = gameHandler.getSpellDispelType(aura.spellId); - ImU32 dotCol; - switch (dispelType) { - case 1: dotCol = IM_COL32( 64, 128, 255, A(210)); break; // Magic - blue - case 2: dotCol = IM_COL32(160, 32, 240, A(210)); break; // Curse - purple - case 3: dotCol = IM_COL32(180, 140, 40, A(210)); break; // Disease - yellow-brown - case 4: dotCol = IM_COL32( 50, 200, 50, A(210)); break; // Poison - green - default: dotCol = IM_COL32(170, 170, 170, A(170)); break; // Other - grey - } - drawList->AddRectFilled(ImVec2(dotX, nameplateBottom), - ImVec2(dotX + dotSize, nameplateBottom + dotSize), dotCol, 1.0f); - drawList->AddRect (ImVec2(dotX - 1.0f, nameplateBottom - 1.0f), - ImVec2(dotX + dotSize + 1.0f, nameplateBottom + dotSize + 1.0f), - IM_COL32(0, 0, 0, A(150)), 1.0f); - dotX += dotSize + dotGap; - if (dotX + dotSize > barX + barW) break; } } @@ -7889,19 +6360,12 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { ImVec2 textSize = ImGui::CalcTextSize(labelBuf); float nameX = sx - textSize.x * 0.5f; float nameY = sy - barH - 12.0f; - // Name color: players get WoW class colors; NPCs use hostility (red/yellow) - ImU32 nameColor; - if (isPlayer) { - // Class color with cyan fallback for unknown class - uint8_t cid = entityClassId(unit); - ImVec4 cc = (cid != 0) ? classColorVec4(cid) : ImVec4(0.31f, 0.78f, 1.0f, 1.0f); - nameColor = IM_COL32(static_cast(cc.x*255), static_cast(cc.y*255), - static_cast(cc.z*255), A(230)); - } else { - nameColor = unit->isHostile() + // Name color: other player=cyan, hostile=red, non-hostile=yellow (WoW convention) + ImU32 nameColor = isPlayer + ? IM_COL32( 80, 200, 255, A(230)) // cyan — other players + : unit->isHostile() ? IM_COL32(220, 80, 80, A(230)) // red — hostile NPC : IM_COL32(240, 200, 100, A(230)); // yellow — friendly NPC - } drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf); drawList->AddText(ImVec2(nameX, nameY), nameColor, labelBuf); @@ -7983,16 +6447,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { } if (ImGui::MenuItem("Invite to Group")) gameHandler.inviteToGroup(ctxName); - if (ImGui::MenuItem("Trade")) - gameHandler.initiateTrade(nameplateCtxGuid_); - if (ImGui::MenuItem("Duel")) - gameHandler.proposeDuel(nameplateCtxGuid_); - if (ImGui::MenuItem("Inspect")) { - gameHandler.setTarget(nameplateCtxGuid_); - gameHandler.inspectTarget(); - showInspectWindow_ = true; - } - ImGui::Separator(); if (ImGui::MenuItem("Add Friend")) gameHandler.addFriend(ctxName); if (ImGui::MenuItem("Ignore")) @@ -8014,7 +6468,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { if (!gameHandler.isInGroup()) return; - auto* assetMgr = core::Application::getInstance().getAssetManager(); const auto& partyData = gameHandler.getPartyData(); const bool isRaid = (partyData.groupType == 1); float frameY = 120.0f; @@ -8090,35 +6543,13 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { bool isDead = (m.onlineStatus & 0x0020) != 0; bool isGhost = (m.onlineStatus & 0x0010) != 0; - // Out-of-range check (40 yard threshold) - bool isOOR = false; - if (m.hasPartyStats && isOnline && !isDead && !isGhost && m.zoneId != 0) { - auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); - if (playerEnt) { - float dx = playerEnt->getX() - static_cast(m.posX); - float dy = playerEnt->getY() - static_cast(m.posY); - isOOR = (dx * dx + dy * dy) > (40.0f * 40.0f); - } - } - // Dim cell overlay when out of range - if (isOOR) - draw->AddRectFilled(cellMin, cellMax, IM_COL32(0, 0, 0, 80), 3.0f); - - // Name text (truncated) — class color when alive+online, gray when dead/offline + // Name text (truncated); leader name is gold char truncName[16]; snprintf(truncName, sizeof(truncName), "%.12s", m.name.c_str()); bool isMemberLeader = (m.guid == partyData.leaderGuid); - ImU32 nameCol; - if (!isOnline || isDead || isGhost) { - nameCol = IM_COL32(140, 140, 140, 200); // gray for dead/offline - } else { - // Default: gold for leader, light gray for others - nameCol = isMemberLeader ? IM_COL32(255, 215, 0, 255) : IM_COL32(220, 220, 220, 255); - // Override with WoW class color if entity is loaded - auto mEnt = gameHandler.getEntityManager().getEntity(m.guid); - uint8_t cid = entityClassId(mEnt.get()); - if (cid != 0) nameCol = classColorU32(cid); - } + ImU32 nameCol = isMemberLeader ? IM_COL32(255, 215, 0, 255) : + (!isOnline || isDead || isGhost) + ? IM_COL32(140, 140, 140, 200) : IM_COL32(220, 220, 220, 255); draw->AddText(ImVec2(cellMin.x + 4.0f, cellMin.y + 3.0f), nameCol, truncName); // Leader crown star in top-right of cell @@ -8144,17 +6575,13 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { draw->AddRectFilled(barBg, barBgEnd, IM_COL32(40, 40, 40, 200), 2.0f); ImVec2 barFill(barBg.x, barBg.y); ImVec2 barFillEnd(barBg.x + (barBgEnd.x - barBg.x) * pct, barBgEnd.y); - ImU32 hpCol = isOOR ? IM_COL32(100, 100, 100, 160) : - pct > 0.5f ? IM_COL32(60, 180, 60, 255) : - pct > 0.2f ? IM_COL32(200, 180, 50, 255) : - IM_COL32(200, 60, 60, 255); + ImU32 hpCol = pct > 0.5f ? IM_COL32(60, 180, 60, 255) : + pct > 0.2f ? IM_COL32(200, 180, 50, 255) : + IM_COL32(200, 60, 60, 255); draw->AddRectFilled(barFill, barFillEnd, hpCol, 2.0f); - // HP percentage or OOR text centered on bar + // HP percentage text centered on bar char hpPct[8]; - if (isOOR) - snprintf(hpPct, sizeof(hpPct), "OOR"); - else - snprintf(hpPct, sizeof(hpPct), "%d%%", static_cast(pct * 100.0f + 0.5f)); + snprintf(hpPct, sizeof(hpPct), "%d%%", static_cast(pct * 100.0f + 0.5f)); ImVec2 ts = ImGui::CalcTextSize(hpPct); float tx = (barBg.x + barBgEnd.x - ts.x) * 0.5f; float ty = barBg.y + (BAR_H - ts.y) * 0.5f; @@ -8284,21 +6711,12 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { else if (isDead || isGhost) label += " (dead)"; } - // Clickable name to target — use WoW class colors when entity is loaded, - // fall back to gold for leader / light gray for others - ImVec4 nameColor = isLeader - ? ImVec4(1.0f, 0.85f, 0.0f, 1.0f) - : ImVec4(0.85f, 0.85f, 0.85f, 1.0f); - { - auto memberEntity = gameHandler.getEntityManager().getEntity(member.guid); - uint8_t cid = entityClassId(memberEntity.get()); - if (cid != 0) nameColor = classColorVec4(cid); - } - ImGui::PushStyleColor(ImGuiCol_Text, nameColor); + // Clickable name to target; leader name is gold + if (isLeader) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); if (ImGui::Selectable(label.c_str(), gameHandler.getTargetGuid() == member.guid)) { gameHandler.setTarget(member.guid); } - ImGui::PopStyleColor(); + if (isLeader) ImGui::PopStyleColor(); // LFG role badge (Tank/Healer/DPS) — shown on same line as name when set if (member.roles != 0) { @@ -8321,68 +6739,24 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { maxHp = unit->getMaxHealth(); } } - // Check dead/ghost state for health bar rendering - bool memberDead = false; - bool memberOffline = false; - if (member.hasPartyStats) { - bool isOnline2 = (member.onlineStatus & 0x0001) != 0; - bool isDead2 = (member.onlineStatus & 0x0020) != 0; - bool isGhost2 = (member.onlineStatus & 0x0010) != 0; - memberDead = isDead2 || isGhost2; - memberOffline = !isOnline2; - } - - // Out-of-range check: compare player position to member's reported position - // Range threshold: 40 yards (standard heal/spell range) - bool memberOutOfRange = false; - if (member.hasPartyStats && !memberOffline && !memberDead && - member.zoneId != 0) { - // Same map: use 2D Euclidean distance in WoW coordinates (yards) - auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); - if (playerEntity) { - float dx = playerEntity->getX() - static_cast(member.posX); - float dy = playerEntity->getY() - static_cast(member.posY); - float distSq = dx * dx + dy * dy; - memberOutOfRange = (distSq > 40.0f * 40.0f); - } - } - - if (memberDead) { - // Gray "Dead" bar for fallen party members - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.35f, 0.35f, 0.35f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f)); - ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Dead"); - ImGui::PopStyleColor(2); - } else if (memberOffline) { - // Dim bar for offline members - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.25f, 0.25f, 0.25f, 0.6f)); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.1f, 0.1f, 0.6f)); - ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Offline"); - ImGui::PopStyleColor(2); - } else if (maxHp > 0) { + if (maxHp > 0) { float pct = static_cast(hp) / static_cast(maxHp); - // Out-of-range: desaturate health bar to gray - ImVec4 hpBarColor = memberOutOfRange - ? ImVec4(0.45f, 0.45f, 0.45f, 0.7f) - : (pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : - pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : - ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpBarColor); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, + pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : + pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : + ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); char hpText[32]; - if (memberOutOfRange) { - snprintf(hpText, sizeof(hpText), "OOR"); - } else if (maxHp >= 10000) { + if (maxHp >= 10000) snprintf(hpText, sizeof(hpText), "%dk/%dk", (int)hp / 1000, (int)maxHp / 1000); - } else { + else snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp); - } ImGui::ProgressBar(pct, ImVec2(-1, 14), hpText); ImGui::PopStyleColor(); } - // Power bar (mana/rage/energy) from party stats — hidden for dead/offline/OOR - if (!memberDead && !memberOffline && member.hasPartyStats && member.maxPower > 0) { + // Power bar (mana/rage/energy) from party stats + if (member.hasPartyStats && member.maxPower > 0) { float powerPct = static_cast(member.curPower) / static_cast(member.maxPower); ImVec4 powerColor; switch (member.powerType) { @@ -8400,57 +6774,6 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } - // Dispellable debuff indicators — small colored dots for party member debuffs - // Only show magic/curse/disease/poison (types 1-4); skip non-dispellable - if (!memberDead && !memberOffline) { - const std::vector* unitAuras = nullptr; - if (member.guid == gameHandler.getPlayerGuid()) - unitAuras = &gameHandler.getPlayerAuras(); - else if (member.guid == gameHandler.getTargetGuid()) - unitAuras = &gameHandler.getTargetAuras(); - else - unitAuras = gameHandler.getUnitAuras(member.guid); - - if (unitAuras) { - bool anyDebuff = false; - for (const auto& aura : *unitAuras) { - if (aura.isEmpty()) continue; - if ((aura.flags & 0x80) == 0) continue; // only debuffs - uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); - if (dt == 0) continue; // skip non-dispellable - anyDebuff = true; - break; - } - if (anyDebuff) { - // Render one dot per unique dispel type present - bool shown[5] = {}; - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 1.0f)); - for (const auto& aura : *unitAuras) { - if (aura.isEmpty()) continue; - if ((aura.flags & 0x80) == 0) continue; - uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); - if (dt == 0 || dt > 4 || shown[dt]) continue; - shown[dt] = true; - ImVec4 dotCol; - switch (dt) { - case 1: dotCol = ImVec4(0.25f, 0.50f, 1.00f, 1.0f); break; // Magic: blue - case 2: dotCol = ImVec4(0.70f, 0.15f, 0.90f, 1.0f); break; // Curse: purple - case 3: dotCol = ImVec4(0.65f, 0.45f, 0.10f, 1.0f); break; // Disease: brown - case 4: dotCol = ImVec4(0.10f, 0.75f, 0.10f, 1.0f); break; // Poison: green - default: break; - } - ImGui::PushStyleColor(ImGuiCol_Button, dotCol); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, dotCol); - ImGui::Button("##d", ImVec2(8.0f, 8.0f)); - ImGui::PopStyleColor(2); - ImGui::SameLine(); - } - ImGui::NewLine(); - ImGui::PopStyleVar(); - } - } - } - // Party member cast bar — shows when the party member is casting if (auto* cs = gameHandler.getUnitCastState(member.guid)) { float castPct = (cs->timeTotal > 0.0f) @@ -8462,17 +6785,7 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { snprintf(pcastLabel, sizeof(pcastLabel), "%s (%.1fs)", spellNm.c_str(), cs->timeRemaining); else snprintf(pcastLabel, sizeof(pcastLabel), "Casting... (%.1fs)", cs->timeRemaining); - { - VkDescriptorSet pIcon = (cs->spellId != 0 && assetMgr) - ? getSpellIcon(cs->spellId, assetMgr) : VK_NULL_HANDLE; - if (pIcon) { - ImGui::Image((ImTextureID)(uintptr_t)pIcon, ImVec2(10, 10)); - ImGui::SameLine(0, 2); - ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel); - } else { - ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel); - } - } + ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel); ImGui::PopStyleColor(); } @@ -8695,199 +7008,11 @@ void GameScreen::renderRepToasts(float deltaTime) { } } -void GameScreen::renderQuestCompleteToasts(float deltaTime) { - for (auto& e : questCompleteToasts_) e.age += deltaTime; - questCompleteToasts_.erase( - std::remove_if(questCompleteToasts_.begin(), questCompleteToasts_.end(), - [](const QuestCompleteToastEntry& e) { return e.age >= kQuestCompleteToastLifetime; }), - questCompleteToasts_.end()); - - if (questCompleteToasts_.empty()) return; - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - float screenH = window ? static_cast(window->getHeight()) : 720.0f; - - const float toastW = 260.0f; - const float toastH = 40.0f; - const float padY = 4.0f; - const float baseY = screenH - 220.0f; // above rep toasts - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - float fontSize = ImGui::GetFontSize(); - - for (int i = 0; i < static_cast(questCompleteToasts_.size()); ++i) { - const auto& e = questCompleteToasts_[i]; - constexpr float kSlideDur = 0.3f; - float slideIn = std::min(e.age, kSlideDur) / kSlideDur; - float slideOut = std::min(std::max(0.0f, kQuestCompleteToastLifetime - e.age), kSlideDur) / kSlideDur; - float slide = std::min(slideIn, slideOut); - float alpha = std::clamp(slide, 0.0f, 1.0f); - - float xFull = screenW - 14.0f - toastW; - float xStart = screenW + 10.0f; - float toastX = xStart + (xFull - xStart) * slide; - float toastY = baseY - i * (toastH + padY); - - ImVec2 tl(toastX, toastY); - ImVec2 br(toastX + toastW, toastY + toastH); - - // Background + gold border (quest completion) - draw->AddRectFilled(tl, br, IM_COL32(20, 18, 8, (int)(alpha * 210)), 5.0f); - draw->AddRect(tl, br, IM_COL32(220, 180, 30, (int)(alpha * 230)), 5.0f, 0, 1.5f); - - // Scroll icon placeholder (gold diamond) - float iconCx = tl.x + 18.0f; - float iconCy = tl.y + toastH * 0.5f; - draw->AddCircleFilled(ImVec2(iconCx, iconCy), 7.0f, IM_COL32(210, 170, 20, (int)(alpha * 230))); - draw->AddCircle (ImVec2(iconCx, iconCy), 7.0f, IM_COL32(255, 220, 50, (int)(alpha * 200))); - - // "Quest Complete" header in gold - const char* header = "Quest Complete"; - draw->AddText(font, fontSize * 0.78f, - ImVec2(tl.x + 34.0f, tl.y + 4.0f), - IM_COL32(240, 200, 40, (int)(alpha * 240)), header); - - // Quest title in off-white - const char* titleStr = e.title.empty() ? "Unknown Quest" : e.title.c_str(); - draw->AddText(font, fontSize * 0.82f, - ImVec2(tl.x + 34.0f, tl.y + toastH * 0.5f + 1.0f), - IM_COL32(220, 215, 195, (int)(alpha * 220)), titleStr); - } -} - -// ============================================================ -// Zone Entry Toast -// ============================================================ - -void GameScreen::renderZoneToasts(float deltaTime) { - for (auto& e : zoneToasts_) e.age += deltaTime; - zoneToasts_.erase( - std::remove_if(zoneToasts_.begin(), zoneToasts_.end(), - [](const ZoneToastEntry& e) { return e.age >= kZoneToastLifetime; }), - zoneToasts_.end()); - - if (zoneToasts_.empty()) return; - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - - for (int i = 0; i < static_cast(zoneToasts_.size()); ++i) { - const auto& e = zoneToasts_[i]; - constexpr float kSlideDur = 0.35f; - float slideIn = std::min(e.age, kSlideDur) / kSlideDur; - float slideOut = std::min(std::max(0.0f, kZoneToastLifetime - e.age), kSlideDur) / kSlideDur; - float slide = std::min(slideIn, slideOut); - float alpha = std::clamp(slide, 0.0f, 1.0f); - - // Measure text to size the toast - ImVec2 nameSz = font->CalcTextSizeA(14.0f, FLT_MAX, 0.0f, e.zoneName.c_str()); - const char* header = "Entering:"; - ImVec2 hdrSz = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, header); - - float toastW = std::max(nameSz.x, hdrSz.x) + 28.0f; - float toastH = 42.0f; - - // Center the toast horizontally, appear just below the zone name area (top-center) - float toastX = (screenW - toastW) * 0.5f; - float toastY = 56.0f + i * (toastH + 4.0f); - // Slide down from above - float offY = (1.0f - slide) * (-toastH - 10.0f); - toastY += offY; - - ImVec2 tl(toastX, toastY); - ImVec2 br(toastX + toastW, toastY + toastH); - - draw->AddRectFilled(tl, br, IM_COL32(10, 10, 16, (int)(alpha * 200)), 6.0f); - draw->AddRect(tl, br, IM_COL32(160, 140, 80, (int)(alpha * 220)), 6.0f, 0, 1.2f); - - float cx = tl.x + toastW * 0.5f; - draw->AddText(font, 11.0f, - ImVec2(cx - hdrSz.x * 0.5f, tl.y + 5.0f), - IM_COL32(180, 170, 120, (int)(alpha * 200)), header); - draw->AddText(font, 14.0f, - ImVec2(cx - nameSz.x * 0.5f, tl.y + toastH * 0.5f + 1.0f), - IM_COL32(255, 230, 140, (int)(alpha * 240)), e.zoneName.c_str()); - } -} - -// ─── Area Trigger Message Toasts ───────────────────────────────────────────── -void GameScreen::renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler) { - // Drain any pending messages from GameHandler - while (gameHandler.hasAreaTriggerMsg()) { - AreaTriggerToast t; - t.text = gameHandler.popAreaTriggerMsg(); - t.age = 0.0f; - areaTriggerToasts_.push_back(std::move(t)); - if (areaTriggerToasts_.size() > 4) - areaTriggerToasts_.erase(areaTriggerToasts_.begin()); - } - - // Age and prune - constexpr float kLifetime = 4.5f; - for (auto& t : areaTriggerToasts_) t.age += deltaTime; - areaTriggerToasts_.erase( - std::remove_if(areaTriggerToasts_.begin(), areaTriggerToasts_.end(), - [](const AreaTriggerToast& t) { return t.age >= kLifetime; }), - areaTriggerToasts_.end()); - if (areaTriggerToasts_.empty()) return; - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - float screenH = window ? static_cast(window->getHeight()) : 720.0f; - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - constexpr float kSlideDur = 0.35f; - - for (int i = 0; i < static_cast(areaTriggerToasts_.size()); ++i) { - const auto& t = areaTriggerToasts_[i]; - - float slideIn = std::min(t.age, kSlideDur) / kSlideDur; - float slideOut = std::min(std::max(0.0f, kLifetime - t.age), kSlideDur) / kSlideDur; - float alpha = std::clamp(std::min(slideIn, slideOut), 0.0f, 1.0f); - - // Measure text - ImVec2 txtSz = font->CalcTextSizeA(13.0f, FLT_MAX, 0.0f, t.text.c_str()); - float toastW = txtSz.x + 30.0f; - float toastH = 30.0f; - - // Center horizontally, place below zone text (center of lower-third) - float toastX = (screenW - toastW) * 0.5f; - float toastY = screenH * 0.62f + i * (toastH + 3.0f); - // Slide up from below - float offY = (1.0f - std::min(slideIn, slideOut)) * (toastH + 12.0f); - toastY += offY; - - ImVec2 tl(toastX, toastY); - ImVec2 br(toastX + toastW, toastY + toastH); - - draw->AddRectFilled(tl, br, IM_COL32(8, 12, 22, (int)(alpha * 190)), 5.0f); - draw->AddRect(tl, br, IM_COL32(100, 160, 220, (int)(alpha * 200)), 5.0f, 0, 1.0f); - - float cx = tl.x + toastW * 0.5f; - // Shadow - draw->AddText(font, 13.0f, - ImVec2(cx - txtSz.x * 0.5f + 1, tl.y + (toastH - txtSz.y) * 0.5f + 1), - IM_COL32(0, 0, 0, (int)(alpha * 180)), t.text.c_str()); - // Text in light blue - draw->AddText(font, 13.0f, - ImVec2(cx - txtSz.x * 0.5f, tl.y + (toastH - txtSz.y) * 0.5f), - IM_COL32(180, 220, 255, (int)(alpha * 240)), t.text.c_str()); - } -} - // ============================================================ // Boss Encounter Frames // ============================================================ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { - auto* assetMgr = core::Application::getInstance().getAssetManager(); - // Collect active boss unit slots struct BossSlot { uint32_t slot; uint64_t guid; }; std::vector active; @@ -8953,32 +7078,14 @@ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { uint32_t bspell = cs->spellId; const std::string& bcastName = (bspell != 0) ? gameHandler.getSpellName(bspell) : ""; - // Pulse bright orange when > 80% complete — interrupt window closing - ImVec4 bcastColor; - if (castPct > 0.8f) { - float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); - bcastColor = ImVec4(1.0f * pulse, 0.5f * pulse, 0.0f, 1.0f); - } else { - bcastColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f); - } - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bcastColor); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f)); char bcastLabel[72]; if (!bcastName.empty()) snprintf(bcastLabel, sizeof(bcastLabel), "%s (%.1fs)", bcastName.c_str(), cs->timeRemaining); else snprintf(bcastLabel, sizeof(bcastLabel), "Casting... (%.1fs)", cs->timeRemaining); - { - VkDescriptorSet bIcon = (bspell != 0 && assetMgr) - ? getSpellIcon(bspell, assetMgr) : VK_NULL_HANDLE; - if (bIcon) { - ImGui::Image((ImTextureID)(uintptr_t)bIcon, ImVec2(12, 12)); - ImGui::SameLine(0, 2); - ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel); - } else { - ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel); - } - } + ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel); ImGui::PopStyleColor(); } @@ -9044,47 +7151,6 @@ void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) { ImGui::End(); } -void GameScreen::renderDuelCountdown(game::GameHandler& gameHandler) { - float remaining = gameHandler.getDuelCountdownRemaining(); - if (remaining <= 0.0f) return; - - ImVec2 displaySize = ImGui::GetIO().DisplaySize; - float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; - float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - - auto* dl = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - float fontSize = ImGui::GetFontSize(); - - // Show integer countdown or "Fight!" when under 0.5s - char buf[32]; - if (remaining > 0.5f) { - snprintf(buf, sizeof(buf), "%d", static_cast(std::ceil(remaining))); - } else { - snprintf(buf, sizeof(buf), "Fight!"); - } - - // Large font by scaling — use 4x font size for dramatic effect - float scale = 4.0f; - float scaledSize = fontSize * scale; - ImVec2 textSz = font->CalcTextSizeA(scaledSize, FLT_MAX, 0.0f, buf); - float tx = (screenW - textSz.x) * 0.5f; - float ty = screenH * 0.35f - textSz.y * 0.5f; - - // Pulsing alpha: fades in and out per second - float pulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 6.28f); - uint8_t alpha = static_cast(255 * pulse); - - // Color: golden countdown, red "Fight!" - ImU32 color = (remaining > 0.5f) - ? IM_COL32(255, 200, 50, alpha) - : IM_COL32(255, 60, 60, alpha); - - // Drop shadow - dl->AddText(font, scaledSize, ImVec2(tx + 2.0f, ty + 2.0f), IM_COL32(0, 0, 0, alpha / 2), buf); - dl->AddText(font, scaledSize, ImVec2(tx, ty), color, buf); -} - void GameScreen::renderItemTextWindow(game::GameHandler& gameHandler) { if (!gameHandler.isItemTextOpen()) return; @@ -9391,34 +7457,6 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { uint8_t q = roll.itemQuality; ImVec4 col = (q < 6) ? kQualityColors[q] : kQualityColors[1]; - // Countdown bar - { - auto now = std::chrono::steady_clock::now(); - float elapsedMs = static_cast( - std::chrono::duration_cast(now - roll.rollStartedAt).count()); - float totalMs = static_cast(roll.rollCountdownMs > 0 ? roll.rollCountdownMs : 60000); - float fraction = 1.0f - std::min(elapsedMs / totalMs, 1.0f); - float remainSec = (totalMs - elapsedMs) / 1000.0f; - if (remainSec < 0.0f) remainSec = 0.0f; - - // Color: green → yellow → red - ImVec4 barColor; - if (fraction > 0.5f) - barColor = ImVec4(0.2f + (1.0f - fraction) * 1.4f, 0.85f, 0.2f, 1.0f); - else if (fraction > 0.2f) - barColor = ImVec4(1.0f, fraction * 1.7f, 0.1f, 1.0f); - else { - float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 6.0f); - barColor = ImVec4(pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); - } - - char timeBuf[16]; - std::snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remainSec); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); - ImGui::ProgressBar(fraction, ImVec2(-1, 12), timeBuf); - ImGui::PopStyleColor(); - } - ImGui::Text("An item is up for rolls:"); // Show item icon if available @@ -9460,48 +7498,6 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { if (ImGui::Button("Pass", ImVec2(70, 30))) { gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96); } - - // Live roll results from group members - if (!roll.playerRolls.empty()) { - ImGui::Separator(); - ImGui::TextDisabled("Rolls so far:"); - // Roll-type label + color - static const char* kRollLabels[] = {"Need", "Greed", "Disenchant", "Pass"}; - static const ImVec4 kRollColors[] = { - ImVec4(0.2f, 0.9f, 0.2f, 1.0f), // Need — green - ImVec4(0.3f, 0.6f, 1.0f, 1.0f), // Greed — blue - ImVec4(0.7f, 0.3f, 0.9f, 1.0f), // Disenchant — purple - ImVec4(0.5f, 0.5f, 0.5f, 1.0f), // Pass — gray - }; - auto rollTypeIndex = [](uint8_t t) -> int { - if (t == 0) return 0; - if (t == 1) return 1; - if (t == 2) return 2; - return 3; // pass (96 or unknown) - }; - - if (ImGui::BeginTable("##lootrolls", 3, - ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 72.0f); - ImGui::TableSetupColumn("Roll", ImGuiTableColumnFlags_WidthFixed, 32.0f); - for (const auto& r : roll.playerRolls) { - int ri = rollTypeIndex(r.rollType); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(r.playerName.c_str()); - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(kRollColors[ri], "%s", kRollLabels[ri]); - ImGui::TableSetColumnIndex(2); - if (r.rollType != 96) { - ImGui::TextColored(kRollColors[ri], "%d", static_cast(r.rollNum)); - } else { - ImGui::TextDisabled("—"); - } - } - ImGui::EndTable(); - } - } } ImGui::End(); } @@ -9560,29 +7556,6 @@ void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) { gameHandler.respondToReadyCheck(false); gameHandler.dismissReadyCheck(); } - - // Live player responses - const auto& results = gameHandler.getReadyCheckResults(); - if (!results.empty()) { - ImGui::Separator(); - if (ImGui::BeginTable("##rcresults", 2, - ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 72.0f); - for (const auto& r : results) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(r.name.c_str()); - ImGui::TableSetColumnIndex(1); - if (r.ready) { - ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "Ready"); - } else { - ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "Not Ready"); - } - } - ImGui::EndTable(); - } - } } ImGui::End(); } @@ -9763,8 +7736,7 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { uint32_t gold = cost / 10000; uint32_t silver = (cost % 10000) / 100; uint32_t copper = cost % 100; - ImGui::TextDisabled("Cost:"); ImGui::SameLine(0, 4); - renderCoinsText(gold, silver, copper); + ImGui::Text("Cost: %ug %us %uc", gold, silver, copper); ImGui::Spacing(); ImGui::Text("Guild Name:"); ImGui::InputText("##petitionname", petitionNameBuffer_, sizeof(petitionNameBuffer_)); @@ -9847,14 +7819,19 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { return a.name < b.name; }); + static const char* classNames[] = { + "Unknown", "Warrior", "Paladin", "Hunter", "Rogue", + "Priest", "Death Knight", "Shaman", "Mage", "Warlock", + "", "Druid" + }; + for (const auto& m : sortedMembers) { ImGui::TableNextRow(); ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); - ImVec4 nameColor = m.online ? classColorVec4(m.classId) : textColor; ImGui::TableNextColumn(); - ImGui::TextColored(nameColor, "%s", m.name.c_str()); + ImGui::TextColored(textColor, "%s", m.name.c_str()); // Right-click context menu if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { @@ -9874,9 +7851,8 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::TextColored(textColor, "%u", m.level); ImGui::TableNextColumn(); - const char* className = classNameStr(m.classId); - ImVec4 classCol = m.online ? classColorVec4(m.classId) : textColor; - ImGui::TextColored(classCol, "%s", className); + const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown"; + ImGui::TextColored(textColor, "%s", className); ImGui::TableNextColumn(); // Zone name lookup @@ -10215,37 +8191,17 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::EndTooltip(); } - // Level, class, and status + // Level and status if (c.isOnline()) { - ImGui::SameLine(150.0f); + ImGui::SameLine(160.0f); const char* statusLabel = (c.status == 2) ? " (AFK)" : (c.status == 3) ? " (DND)" : ""; - // Class color for the level/class display - ImVec4 friendClassCol = classColorVec4(static_cast(c.classId)); - const char* friendClassName = classNameStr(static_cast(c.classId)); - if (c.level > 0 && c.classId > 0) { - ImGui::TextColored(friendClassCol, "Lv%u %s%s", c.level, friendClassName, statusLabel); - } else if (c.level > 0) { + if (c.level > 0) { ImGui::TextDisabled("Lv %u%s", c.level, statusLabel); } else if (*statusLabel) { ImGui::TextDisabled("%s", statusLabel + 1); } - - // Tooltip: zone info - if (ImGui::IsItemHovered() && c.areaId != 0) { - ImGui::BeginTooltip(); - if (zoneManager) { - const auto* zi = zoneManager->getZoneInfo(c.areaId); - if (zi && !zi->name.empty()) - ImGui::Text("Zone: %s", zi->name.c_str()); - else - ImGui::TextDisabled("Area ID: %u", c.areaId); - } else { - ImGui::TextDisabled("Area ID: %u", c.areaId); - } - ImGui::EndTooltip(); - } } ImGui::PopID(); @@ -10350,10 +8306,6 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.92f)); - // State for "Set Note" inline editing - static int noteEditContactIdx = -1; - static char noteEditBuf[128] = {}; - bool open = showSocialFrame_; char socialTitle[32]; snprintf(socialTitle, sizeof(socialTitle), "Social (%d online)##SocialFrame", onlineCount); @@ -10361,11 +8313,6 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) { - // Get zone manager for area name lookups - game::ZoneManager* socialZoneMgr = nullptr; - if (auto* rend = core::Application::getInstance().getRenderer()) - socialZoneMgr = rend->getZoneManager(); - if (ImGui::BeginTabBar("##SocialTabs")) { // ---- Friends tab ---- if (ImGui::BeginTabItem("Friends")) { @@ -10397,36 +8344,13 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str(); ImVec4 nameCol = c.isOnline() - ? classColorVec4(static_cast(c.classId)) + ? ImVec4(0.9f, 0.9f, 0.9f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); ImGui::TextColored(nameCol, "%s", displayName); if (c.isOnline() && c.level > 0) { ImGui::SameLine(); - // Show level and class name in class color - ImGui::TextColored(classColorVec4(static_cast(c.classId)), - "Lv%u %s", c.level, classNameStr(static_cast(c.classId))); - } - - // Tooltip: zone info and note - if (ImGui::IsItemHovered() || (c.isOnline() && ImGui::IsItemHovered())) { - if (c.isOnline() && (c.areaId != 0 || !c.note.empty())) { - ImGui::BeginTooltip(); - if (c.areaId != 0) { - const char* zoneName = nullptr; - if (socialZoneMgr) { - const auto* zi = socialZoneMgr->getZoneInfo(c.areaId); - if (zi && !zi->name.empty()) zoneName = zi->name.c_str(); - } - if (zoneName) - ImGui::Text("Zone: %s", zoneName); - else - ImGui::Text("Area ID: %u", c.areaId); - } - if (!c.note.empty()) - ImGui::TextDisabled("Note: %s", c.note.c_str()); - ImGui::EndTooltip(); - } + ImGui::TextDisabled("Lv%u", c.level); } // Right-click context menu @@ -10443,14 +8367,6 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { } if (ImGui::MenuItem("Invite to Group")) gameHandler.inviteToGroup(c.name); - if (c.guid != 0 && ImGui::MenuItem("Trade")) - gameHandler.initiateTrade(c.guid); - } - if (ImGui::MenuItem("Set Note")) { - noteEditContactIdx = static_cast(ci); - strncpy(noteEditBuf, c.note.c_str(), sizeof(noteEditBuf) - 1); - noteEditBuf[sizeof(noteEditBuf) - 1] = '\0'; - ImGui::OpenPopup("##SetFriendNote"); } if (ImGui::MenuItem("Remove Friend")) gameHandler.removeFriend(c.name); @@ -10471,31 +8387,6 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { } ImGui::EndChild(); - - // "Set Note" modal popup - if (ImGui::BeginPopup("##SetFriendNote")) { - const std::string& noteName = (noteEditContactIdx >= 0 && - noteEditContactIdx < static_cast(contacts.size())) - ? contacts[noteEditContactIdx].name : ""; - ImGui::TextDisabled("Note for %s:", noteName.c_str()); - ImGui::SetNextItemWidth(180.0f); - bool confirm = ImGui::InputText("##noteinput", noteEditBuf, sizeof(noteEditBuf), - ImGuiInputTextFlags_EnterReturnsTrue); - ImGui::SameLine(); - if (confirm || ImGui::Button("OK")) { - if (!noteName.empty()) - gameHandler.setFriendNote(noteName, noteEditBuf); - noteEditContactIdx = -1; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - noteEditContactIdx = -1; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - ImGui::Separator(); // Add friend @@ -10632,33 +8523,12 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); if (ImGui::Begin("##BuffBar", nullptr, flags)) { - // Pre-sort auras: buffs first, then debuffs; within each group, shorter remaining first - uint64_t buffNowMs = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - std::vector buffSortedIdx; - buffSortedIdx.reserve(auras.size()); - for (size_t i = 0; i < auras.size(); ++i) - if (!auras[i].isEmpty()) buffSortedIdx.push_back(i); - std::sort(buffSortedIdx.begin(), buffSortedIdx.end(), [&](size_t a, size_t b) { - const auto& aa = auras[a]; const auto& ab = auras[b]; - bool aDebuff = (aa.flags & 0x80) != 0; - bool bDebuff = (ab.flags & 0x80) != 0; - if (aDebuff != bDebuff) return aDebuff < bDebuff; // buffs (0) first - int32_t ra = aa.getRemainingMs(buffNowMs); - int32_t rb = ab.getRemainingMs(buffNowMs); - if (ra < 0 && rb < 0) return false; - if (ra < 0) return false; - if (rb < 0) return true; - return ra < rb; - }); - + // Separate buffs and debuffs; show buffs first, then debuffs with a visual gap // Render one pass for buffs, one for debuffs for (int pass = 0; pass < 2; ++pass) { bool wantBuff = (pass == 0); int shown = 0; - for (size_t si = 0; si < buffSortedIdx.size() && shown < 40; ++si) { - size_t i = buffSortedIdx[si]; + for (size_t i = 0; i < auras.size() && shown < 40; ++i) { const auto& aura = auras[i]; if (aura.isEmpty()) continue; @@ -10669,22 +8539,7 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { ImGui::PushID(static_cast(i) + (pass * 256)); - // Determine border color: buffs = green; debuffs use WoW dispel-type colors - ImVec4 borderColor; - if (isBuff) { - borderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f); // green - } else { - // Debuff: color by dispel type (0=none/red, 1=magic/blue, 2=curse/purple, - // 3=disease/brown, 4=poison/green, other=dark-red) - uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); - switch (dt) { - case 1: borderColor = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; // magic: blue - case 2: borderColor = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; // curse: purple - case 3: borderColor = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; // disease: brown - case 4: borderColor = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; // poison: green - default: borderColor = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; // other: red - } - } + ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f); // Try to get spell icon VkDescriptorSet iconTex = VK_NULL_HANDLE; @@ -10729,26 +8584,11 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { ImVec2 textSize = ImGui::CalcTextSize(timeStr); float cx = iconMin.x + (iconMax.x - iconMin.x - textSize.x) * 0.5f; float cy = iconMax.y - textSize.y - 2.0f; - // Choose timer color based on urgency - ImU32 timerColor; - if (remainMs < 10000) { - // < 10s: pulse red - float pulse = 0.7f + 0.3f * std::sin( - static_cast(ImGui::GetTime()) * 6.0f); - timerColor = IM_COL32( - static_cast(255 * pulse), - static_cast(80 * pulse), - static_cast(60 * pulse), 255); - } else if (remainMs < 30000) { - timerColor = IM_COL32(255, 165, 0, 255); // orange - } else { - timerColor = IM_COL32(255, 255, 255, 255); // white - } // Drop shadow for readability over any icon colour ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 200), timeStr); ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), - timerColor, timeStr); + IM_COL32(255, 255, 255, 255), timeStr); } // Stack / charge count overlay — upper-left corner of the icon @@ -10832,11 +8672,10 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { const auto& loot = gameHandler.getCurrentLoot(); - // Gold (auto-looted on open; shown for feedback) + // Gold if (loot.gold > 0) { - ImGui::TextDisabled("Gold:"); - ImGui::SameLine(0, 4); - renderCoinsText(loot.getGold(), loot.getSilver(), loot.getCopper()); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%ug %us %uc", + loot.getGold(), loot.getSilver(), loot.getCopper()); ImGui::Separator(); } @@ -11242,8 +9081,9 @@ void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) { uint32_t gold = quest.rewardMoney / 10000; uint32_t silver = (quest.rewardMoney % 10000) / 100; uint32_t copper = quest.rewardMoney % 100; - ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4); - renderCoinsText(gold, silver, copper); + if (gold > 0) ImGui::Text(" %ug %us %uc", gold, silver, copper); + else if (silver > 0) ImGui::Text(" %us %uc", silver, copper); + else ImGui::Text(" %uc", copper); } } @@ -11357,8 +9197,7 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) { uint32_t g = quest.requiredMoney / 10000; uint32_t s = (quest.requiredMoney % 10000) / 100; uint32_t c = quest.requiredMoney % 100; - ImGui::TextDisabled("Required money:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + ImGui::Text("Required money: %ug %us %uc", g, s, c); } // Complete / Cancel buttons @@ -11536,8 +9375,9 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) { uint32_t g = quest.rewardMoney / 10000; uint32_t s = (quest.rewardMoney % 10000) / 100; uint32_t c = quest.rewardMoney % 100; - ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + if (g > 0) ImGui::Text(" %ug %us %uc", g, s, c); + else if (s > 0) ImGui::Text(" %us %uc", s, c); + else ImGui::Text(" %uc", c); } } @@ -11597,8 +9437,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { uint32_t mg = static_cast(money / 10000); uint32_t ms = static_cast((money / 100) % 100); uint32_t mc = static_cast(money % 100); - ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4); - renderCoinsText(mg, ms, mc); + ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); if (vendor.canRepair) { ImGui::SameLine(); @@ -11699,11 +9538,9 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered() && bbInfo && bbInfo->valid) inventoryScreen.renderItemTooltip(*bbInfo); ImGui::TableSetColumnIndex(2); - if (canAfford) { - renderCoinsText(g, s, c); - } else { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c); - } + if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::Text("%ug %us %uc", g, s, c); + if (!canAfford) ImGui::PopStyleColor(); ImGui::TableSetColumnIndex(3); if (!canAfford) ImGui::BeginDisabled(); char bbLabel[32]; @@ -11785,7 +9622,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { ImVec4 qc = InventoryScreen::getQualityColor(static_cast(info->quality)); ImGui::TextColored(qc, "%s", info->name.c_str()); if (ImGui::IsItemHovered()) { - inventoryScreen.renderItemTooltip(*info, &gameHandler.getInventory()); + inventoryScreen.renderItemTooltip(*info); } // Shift-click: insert item link into chat if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) { @@ -11810,11 +9647,9 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { uint32_t s = (item.buyPrice / 100) % 100; uint32_t c = item.buyPrice % 100; bool canAfford = money >= item.buyPrice; - if (canAfford) { - renderCoinsText(g, s, c); - } else { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c); - } + if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::Text("%ug %us %uc", g, s, c); + if (!canAfford) ImGui::PopStyleColor(); } ImGui::TableSetColumnIndex(3); @@ -11892,8 +9727,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { uint32_t mg = static_cast(money / 10000); uint32_t ms = static_cast((money / 100) % 100); uint32_t mc = static_cast(money % 100); - ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4); - renderCoinsText(mg, ms, mc); + ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); // Filter controls static bool showUnavailable = false; @@ -12048,11 +9882,8 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { uint32_t s = (spell->spellCost / 100) % 100; uint32_t c = spell->spellCost % 100; bool canAfford = money >= spell->spellCost; - if (canAfford) { - renderCoinsText(g, s, c); - } else { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c); - } + ImVec4 costColor = canAfford ? color : ImVec4(1.0f, 0.3f, 0.3f, 1.0f); + ImGui::TextColored(costColor, "%ug %us %uc", g, s, c); } else { ImGui::TextColored(color, "Free"); } @@ -12332,7 +10163,13 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) { } ImGui::TableSetColumnIndex(1); - renderCoinsText(gold, silver, copper); + if (gold > 0) { + ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper); + } else if (silver > 0) { + ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), "%us %uc", silver, copper); + } else { + ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.2f, 1.0f), "%uc", copper); + } ImGui::TableSetColumnIndex(2); if (ImGui::SmallButton("Fly")) { @@ -12373,18 +10210,7 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) { // ============================================================ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { - if (!gameHandler.showDeathDialog()) { - deathTimerRunning_ = false; - deathElapsed_ = 0.0f; - return; - } - float dt = ImGui::GetIO().DeltaTime; - if (!deathTimerRunning_) { - deathElapsed_ = 0.0f; - deathTimerRunning_ = true; - } else { - deathElapsed_ += dt; - } + if (!gameHandler.showDeathDialog()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; @@ -12402,7 +10228,7 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { // "Release Spirit" dialog centered on screen float dlgW = 280.0f; - float dlgH = 130.0f; + float dlgH = 100.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); @@ -12421,18 +10247,6 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::SetCursorPosX((dlgW - textW) / 2); ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "%s", deathText); - // Respawn timer: show how long until forced release - float timeLeft = kForcedReleaseSec - deathElapsed_; - if (timeLeft > 0.0f) { - int mins = static_cast(timeLeft) / 60; - int secs = static_cast(timeLeft) % 60; - char timerBuf[48]; - snprintf(timerBuf, sizeof(timerBuf), "Release in %d:%02d", mins, secs); - float tw = ImGui::CalcTextSize(timerBuf).x; - ImGui::SetCursorPosX((dlgW - tw) / 2); - ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "%s", timerBuf); - } - ImGui::Spacing(); ImGui::Spacing(); @@ -13092,12 +10906,6 @@ void GameScreen::renderSettingsWindow() { ImGui::SameLine(); ImGui::TextDisabled("(red vignette on taking damage)"); - if (ImGui::Checkbox("Low Health Vignette", &lowHealthVignetteEnabled_)) { - saveSettings(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(pulsing red edges below 20%% HP)"); - ImGui::EndChild(); ImGui::EndTabItem(); } @@ -13976,43 +11784,6 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } - // Lootable corpse dots: small yellow-green diamonds on dead, lootable units. - // Shown whenever NPC dots are enabled (or always, since they're always useful). - { - constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; - for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { - if (!entity || entity->getType() != game::ObjectType::UNIT) continue; - auto unit = std::static_pointer_cast(entity); - if (!unit) continue; - // Must be dead (health == 0) and marked lootable - if (unit->getHealth() != 0) continue; - if (!(unit->getDynamicFlags() & UNIT_DYNFLAG_LOOTABLE)) continue; - - glm::vec3 npcRender = core::coords::canonicalToRender( - glm::vec3(entity->getX(), entity->getY(), entity->getZ())); - float sx = 0.0f, sy = 0.0f; - if (!projectToMinimap(npcRender, sx, sy)) continue; - - // Draw a small diamond (rotated square) in light yellow-green - const float dr = 3.5f; - ImVec2 top (sx, sy - dr); - ImVec2 right(sx + dr, sy ); - ImVec2 bot (sx, sy + dr); - ImVec2 left (sx - dr, sy ); - drawList->AddQuadFilled(top, right, bot, left, IM_COL32(180, 230, 80, 230)); - drawList->AddQuad (top, right, bot, left, IM_COL32(60, 80, 20, 200), 1.0f); - - // Tooltip on hover - if (ImGui::IsMouseHoveringRect(ImVec2(sx - dr, sy - dr), ImVec2(sx + dr, sy + dr))) { - const std::string& nm = unit->getName(); - ImGui::BeginTooltip(); - ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.3f, 1.0f), "%s", - nm.empty() ? "Lootable corpse" : nm.c_str()); - ImGui::EndTooltip(); - } - } - } - for (const auto& [guid, status] : statuses) { ImU32 dotColor; const char* marker = nullptr; @@ -14051,68 +11822,6 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { IM_COL32(0, 0, 0, 255), marker); } - // Quest kill objective markers — highlight live NPCs matching active quest kill objectives - { - // Build map of NPC entry → (quest title, current, required) for tooltips - struct KillInfo { std::string questTitle; uint32_t current = 0; uint32_t required = 0; }; - std::unordered_map killInfoMap; - const auto& trackedIds = gameHandler.getTrackedQuestIds(); - for (const auto& quest : gameHandler.getQuestLog()) { - if (quest.complete) continue; - if (!trackedIds.empty() && !trackedIds.count(quest.questId)) continue; - for (const auto& obj : quest.killObjectives) { - if (obj.npcOrGoId <= 0 || obj.required == 0) continue; - uint32_t npcEntry = static_cast(obj.npcOrGoId); - auto it = quest.killCounts.find(npcEntry); - uint32_t current = (it != quest.killCounts.end()) ? it->second.first : 0; - if (current < obj.required) { - killInfoMap[npcEntry] = { quest.title, current, obj.required }; - } - } - } - - if (!killInfoMap.empty()) { - ImVec2 mouse = ImGui::GetMousePos(); - for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { - if (!entity || entity->getType() != game::ObjectType::UNIT) continue; - auto unit = std::static_pointer_cast(entity); - if (!unit || unit->getHealth() == 0) continue; - auto infoIt = killInfoMap.find(unit->getEntry()); - if (infoIt == killInfoMap.end()) continue; - - glm::vec3 unitRender = core::coords::canonicalToRender( - glm::vec3(entity->getX(), entity->getY(), entity->getZ())); - float sx = 0.0f, sy = 0.0f; - if (!projectToMinimap(unitRender, sx, sy)) continue; - - // Gold circle with a dark "x" mark — indicates a quest kill target - drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, IM_COL32(255, 185, 0, 240)); - drawList->AddCircle(ImVec2(sx, sy), 5.5f, IM_COL32(0, 0, 0, 180), 12, 1.0f); - drawList->AddLine(ImVec2(sx - 2.5f, sy - 2.5f), ImVec2(sx + 2.5f, sy + 2.5f), - IM_COL32(20, 20, 20, 230), 1.2f); - drawList->AddLine(ImVec2(sx + 2.5f, sy - 2.5f), ImVec2(sx - 2.5f, sy + 2.5f), - IM_COL32(20, 20, 20, 230), 1.2f); - - // Tooltip on hover - float mdx = mouse.x - sx, mdy = mouse.y - sy; - if (mdx * mdx + mdy * mdy < 64.0f) { - const auto& ki = infoIt->second; - const std::string& npcName = unit->getName(); - if (!npcName.empty()) { - ImGui::SetTooltip("%s\n%s: %u/%u", - npcName.c_str(), - ki.questTitle.empty() ? "Quest" : ki.questTitle.c_str(), - ki.current, ki.required); - } else { - ImGui::SetTooltip("%s: %u/%u", - ki.questTitle.empty() ? "Quest" : ki.questTitle.c_str(), - ki.current, ki.required); - } - } - } - } - } - // Gossip POI markers (quest / NPC navigation targets) for (const auto& poi : gameHandler.getGossipPois()) { // Convert WoW canonical coords to render coords for minimap projection @@ -14176,16 +11885,9 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { float sx = 0.0f, sy = 0.0f; if (!projectToMinimap(memberRender, sx, sy)) continue; - ImU32 dotColor; - { - auto mEnt = gameHandler.getEntityManager().getEntity(member.guid); - uint8_t cid = entityClassId(mEnt.get()); - dotColor = (cid != 0) - ? classColorU32(cid, 235) - : (member.guid == leaderGuid) - ? IM_COL32(255, 210, 0, 235) - : IM_COL32(100, 180, 255, 235); - } + ImU32 dotColor = (member.guid == leaderGuid) + ? IM_COL32(255, 210, 0, 235) + : IM_COL32(100, 180, 255, 235); drawList->AddCircleFilled(ImVec2(sx, sy), 4.0f, dotColor); drawList->AddCircle(ImVec2(sx, sy), 4.0f, IM_COL32(255, 255, 255, 160), 12, 1.0f); @@ -14303,111 +12005,18 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } - // Persistent coordinate display below the minimap - { - glm::vec3 playerCanon = core::coords::renderToCanonical(playerRender); - char coordBuf[32]; - std::snprintf(coordBuf, sizeof(coordBuf), "%.1f, %.1f", playerCanon.x, playerCanon.y); - - ImFont* font = ImGui::GetFont(); - float fontSize = ImGui::GetFontSize(); - ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, coordBuf); - - float tx = centerX - textSz.x * 0.5f; - float ty = centerY + mapRadius + 3.0f; - - // Semi-transparent dark background pill - float pad = 3.0f; - drawList->AddRectFilled( - ImVec2(tx - pad, ty - pad), - ImVec2(tx + textSz.x + pad, ty + textSz.y + pad), - IM_COL32(0, 0, 0, 140), 4.0f); - // Coordinate text in warm yellow - drawList->AddText(font, fontSize, ImVec2(tx, ty), IM_COL32(230, 220, 140, 255), coordBuf); - } - - // Zone name display — drawn inside the top edge of the minimap circle - { - auto* zmRenderer = renderer ? renderer->getZoneManager() : nullptr; - uint32_t zoneId = gameHandler.getWorldStateZoneId(); - const game::ZoneInfo* zi = (zmRenderer && zoneId != 0) ? zmRenderer->getZoneInfo(zoneId) : nullptr; - if (zi && !zi->name.empty()) { - ImFont* font = ImGui::GetFont(); - float fontSize = ImGui::GetFontSize(); - ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, zi->name.c_str()); - float tx = centerX - ts.x * 0.5f; - float ty = centerY - mapRadius + 4.0f; // just inside top edge of the circle - float pad = 2.0f; - drawList->AddRectFilled( - ImVec2(tx - pad, ty - pad), - ImVec2(tx + ts.x + pad, ty + ts.y + pad), - IM_COL32(0, 0, 0, 160), 2.0f); - drawList->AddText(font, fontSize, ImVec2(tx + 1.0f, ty + 1.0f), - IM_COL32(0, 0, 0, 180), zi->name.c_str()); - drawList->AddText(font, fontSize, ImVec2(tx, ty), - IM_COL32(255, 230, 150, 220), zi->name.c_str()); - } - } - - // Hover tooltip and right-click context menu + // Hover tooltip: show player's WoW coordinates (canonical X=North, Y=West) { ImVec2 mouse = ImGui::GetMousePos(); float mdx = mouse.x - centerX; float mdy = mouse.y - centerY; - bool overMinimap = (mdx * mdx + mdy * mdy <= mapRadius * mapRadius); - - if (overMinimap) { + if (mdx * mdx + mdy * mdy <= mapRadius * mapRadius) { + glm::vec3 playerCanon = core::coords::renderToCanonical(playerRender); ImGui::BeginTooltip(); - // Compute the world coordinate under the mouse cursor - // Inverse of projectToMinimap: pixel offset → world offset in render space → canonical - float rxW = mdx / mapRadius * viewRadius; - float ryW = mdy / mapRadius * viewRadius; - // Un-rotate: [dx, dy] = R^-1 * [rxW, ryW] - // where R applied: rx = -(dx*cosB + dy*sinB), ry = dx*sinB - dy*cosB - float hoverDx = -cosB * rxW + sinB * ryW; - float hoverDy = -sinB * rxW - cosB * ryW; - glm::vec3 hoverRender(playerRender.x + hoverDx, playerRender.y + hoverDy, playerRender.z); - glm::vec3 hoverCanon = core::coords::renderToCanonical(hoverRender); - ImGui::TextColored(ImVec4(0.9f, 0.85f, 0.5f, 1.0f), "%.1f, %.1f", hoverCanon.x, hoverCanon.y); - ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "Ctrl+click to ping"); + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.5f, 1.0f), + "%.1f, %.1f", playerCanon.x, playerCanon.y); + ImGui::TextDisabled("Ctrl+click to ping"); ImGui::EndTooltip(); - - if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - ImGui::OpenPopup("##minimapContextMenu"); - } - } - - if (ImGui::BeginPopup("##minimapContextMenu")) { - ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Minimap"); - ImGui::Separator(); - - // Zoom controls - if (ImGui::MenuItem("Zoom In")) { - minimap->zoomIn(); - } - if (ImGui::MenuItem("Zoom Out")) { - minimap->zoomOut(); - } - - ImGui::Separator(); - - // Toggle options with checkmarks - bool rotWithCam = minimap->isRotateWithCamera(); - if (ImGui::MenuItem("Rotate with Camera", nullptr, rotWithCam)) { - minimap->setRotateWithCamera(!rotWithCam); - } - - bool squareShape = minimap->isSquareShape(); - if (ImGui::MenuItem("Square Shape", nullptr, squareShape)) { - minimap->setSquareShape(!squareShape); - } - - bool npcDots = minimapNpcDots_; - if (ImGui::MenuItem("Show NPC Dots", nullptr, npcDots)) { - minimapNpcDots_ = !minimapNpcDots_; - } - - ImGui::EndPopup(); } } @@ -14455,40 +12064,12 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { auto* fgDl = ImGui::GetForegroundDrawList(); float zoneTextY = centerY - mapRadius - 16.0f; ImFont* font = ImGui::GetFont(); - - // Weather icon appended to zone name when active - uint32_t wType = gameHandler.getWeatherType(); - float wIntensity = gameHandler.getWeatherIntensity(); - const char* weatherIcon = nullptr; - ImU32 weatherColor = IM_COL32(255, 255, 255, 200); - if (wType == 1 && wIntensity > 0.05f) { // Rain - weatherIcon = " \xe2\x9b\x86"; // U+26C6 ⛆ - weatherColor = IM_COL32(140, 180, 240, 220); - } else if (wType == 2 && wIntensity > 0.05f) { // Snow - weatherIcon = " \xe2\x9d\x84"; // U+2744 ❄ - weatherColor = IM_COL32(210, 230, 255, 220); - } else if (wType == 3 && wIntensity > 0.05f) { // Storm/Fog - weatherIcon = " \xe2\x98\x81"; // U+2601 ☁ - weatherColor = IM_COL32(160, 160, 190, 220); - } - - std::string displayName = zoneName; - // Build combined string if weather active - std::string fullLabel = weatherIcon ? (zoneName + weatherIcon) : zoneName; - ImVec2 tsz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, fullLabel.c_str()); + ImVec2 tsz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, zoneName.c_str()); float tzx = centerX - tsz.x * 0.5f; - - // Shadow pass fgDl->AddText(font, 12.0f, ImVec2(tzx + 1.0f, zoneTextY + 1.0f), IM_COL32(0, 0, 0, 180), zoneName.c_str()); - // Zone name in gold fgDl->AddText(font, 12.0f, ImVec2(tzx, zoneTextY), IM_COL32(255, 220, 120, 230), zoneName.c_str()); - // Weather symbol in its own color appended after - if (weatherIcon) { - ImVec2 nameSz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, zoneName.c_str()); - fgDl->AddText(font, 12.0f, ImVec2(tzx + nameSz.x, zoneTextY), weatherColor, weatherIcon); - } } } @@ -14625,24 +12206,6 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { nextIndicatorY += kIndicatorH; } - // Unspent talent points indicator - { - uint8_t unspent = gameHandler.getUnspentTalentPoints(); - if (unspent > 0) { - ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always); - if (ImGui::Begin("##TalentIndicator", nullptr, indicatorFlags)) { - float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 2.5f); - char talentBuf[40]; - snprintf(talentBuf, sizeof(talentBuf), "! %u Talent Point%s Available", - static_cast(unspent), unspent == 1 ? "" : "s"); - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f * pulse, pulse), "%s", talentBuf); - } - ImGui::End(); - nextIndicatorY += kIndicatorH; - } - } - // BG queue status indicator (when in queue but not yet invited) for (const auto& slot : gameHandler.getBgQueues()) { if (slot.statusId != 1) continue; // STATUS_WAIT_QUEUE only @@ -14945,9 +12508,7 @@ void GameScreen::renderChatBubbles(game::GameHandler& gameHandler) { glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w); float screenX = (ndc.x * 0.5f + 0.5f) * screenW; - // Camera bakes the Vulkan Y-flip into the projection matrix: - // NDC y=-1 is top, y=1 is bottom — same convention as nameplate/minimap projection. - float screenY = (ndc.y * 0.5f + 0.5f) * screenH; + float screenY = (1.0f - (ndc.y * 0.5f + 0.5f)) * screenH; // Flip Y // Skip if off-screen if (screenX < -200.0f || screenX > screenW + 200.0f || @@ -15018,7 +12579,6 @@ void GameScreen::saveSettings() { out << "right_bar_offset_y=" << pendingRightBarOffsetY << "\n"; out << "left_bar_offset_y=" << pendingLeftBarOffsetY << "\n"; out << "damage_flash=" << (damageFlashEnabled_ ? 1 : 0) << "\n"; - out << "low_health_vignette=" << (lowHealthVignetteEnabled_ ? 1 : 0) << "\n"; // Audio out << "sound_muted=" << (soundMuted_ ? 1 : 0) << "\n"; @@ -15140,8 +12700,6 @@ void GameScreen::loadSettings() { pendingLeftBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f); } else if (key == "damage_flash") { damageFlashEnabled_ = (std::stoi(val) != 0); - } else if (key == "low_health_vignette") { - lowHealthVignetteEnabled_ = (std::stoi(val) != 0); } // Audio else if (key == "sound_muted") { @@ -15243,8 +12801,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { uint32_t mg = static_cast(money / 10000); uint32_t ms = static_cast((money / 100) % 100); uint32_t mc = static_cast(money % 100); - ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4); - renderCoinsText(mg, ms, mc); + ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); ImGui::SameLine(ImGui::GetWindowWidth() - 100); if (ImGui::Button("Compose")) { mailRecipientBuffer_[0] = '\0'; @@ -15299,21 +12856,6 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), " [A]"); } - // Expiry warning if within 3 days - if (mail.expirationTime > 0.0f) { - auto nowSec = static_cast(std::time(nullptr)); - float secsLeft = mail.expirationTime - nowSec; - if (secsLeft < 3.0f * 86400.0f && secsLeft > 0.0f) { - ImGui::SameLine(); - int daysLeft = static_cast(secsLeft / 86400.0f); - if (daysLeft == 0) { - ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), " [expires today!]"); - } else { - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f), - " [expires in %dd]", daysLeft); - } - } - } ImGui::PopID(); } @@ -15334,35 +12876,6 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { if (mail.messageType == 2) { ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.2f, 1.0f), "[Auction House]"); } - - // Show expiry date in the detail panel - if (mail.expirationTime > 0.0f) { - auto nowSec = static_cast(std::time(nullptr)); - float secsLeft = mail.expirationTime - nowSec; - // Format absolute expiry as a date using struct tm - time_t expT = static_cast(mail.expirationTime); - struct tm* tmExp = std::localtime(&expT); - if (tmExp) { - static const char* kMon[12] = { - "Jan","Feb","Mar","Apr","May","Jun", - "Jul","Aug","Sep","Oct","Nov","Dec" - }; - const char* mname = kMon[tmExp->tm_mon]; - int daysLeft = static_cast(secsLeft / 86400.0f); - if (secsLeft <= 0.0f) { - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), - "Expired: %s %d, %d", mname, tmExp->tm_mday, 1900 + tmExp->tm_year); - } else if (secsLeft < 3.0f * 86400.0f) { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), - "Expires: %s %d, %d (%d day%s!)", - mname, tmExp->tm_mday, 1900 + tmExp->tm_year, - daysLeft, daysLeft == 1 ? "" : "s"); - } else { - ImGui::TextDisabled("Expires: %s %d, %d", - mname, tmExp->tm_mday, 1900 + tmExp->tm_year); - } - } - } ImGui::Separator(); // Body text @@ -15376,8 +12889,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { uint32_t g = mail.money / 10000; uint32_t s = (mail.money / 100) % 100; uint32_t c = mail.money % 100; - ImGui::TextDisabled("Money:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + ImGui::Text("Money: %ug %us %uc", g, s, c); ImGui::SameLine(); if (ImGui::SmallButton("Take Money")) { gameHandler.mailTakeMoney(mail.messageId); @@ -15873,8 +13385,9 @@ void GameScreen::renderGuildBankWindow(game::GameHandler& gameHandler) { uint32_t gold = static_cast(data.money / 10000); uint32_t silver = static_cast((data.money / 100) % 100); uint32_t copper = static_cast(data.money % 100); - ImGui::TextDisabled("Guild Bank Money:"); ImGui::SameLine(0, 4); - renderCoinsText(gold, silver, copper); + ImGui::Text("Guild Bank Money: "); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper); // Tab bar if (!data.tabs.empty()) { @@ -16257,13 +13770,13 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(3); { uint32_t bid = auction.currentBid > 0 ? auction.currentBid : auction.startBid; - renderCoinsText(bid / 10000, (bid / 100) % 100, bid % 100); + ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); } ImGui::TableSetColumnIndex(4); if (auction.buyoutPrice > 0) { - renderCoinsText(auction.buyoutPrice / 10000, - (auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100); + ImGui::Text("%ug%us%uc", auction.buyoutPrice / 10000, + (auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100); } else { ImGui::TextDisabled("--"); } @@ -16437,10 +13950,10 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(1); ImGui::Text("%u", a.stackCount); ImGui::TableSetColumnIndex(2); - renderCoinsText(a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100); + ImGui::Text("%ug%us%uc", a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100); ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) - renderCoinsText(a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); @@ -16513,11 +14026,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(2); { uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid; - renderCoinsText(bid / 10000, (bid / 100) % 100, bid % 100); + ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); } ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) - renderCoinsText(a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); @@ -16827,18 +14340,6 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { // ---- Vote-to-kick buttons ---- if (state == LfgState::Boot) { ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Vote to kick in progress:"); - const std::string& bootTarget = gameHandler.getLfgBootTargetName(); - const std::string& bootReason = gameHandler.getLfgBootReason(); - if (!bootTarget.empty()) { - ImGui::Text("Player: "); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", bootTarget.c_str()); - } - if (!bootReason.empty()) { - ImGui::Text("Reason: "); - ImGui::SameLine(); - ImGui::TextWrapped("%s", bootReason.c_str()); - } uint32_t bootVotes = gameHandler.getLfgBootVotes(); uint32_t bootTotal = gameHandler.getLfgBootTotal(); uint32_t bootNeeded = gameHandler.getLfgBootNeeded(); @@ -17195,321 +14696,6 @@ void GameScreen::renderBattlegroundScore(game::GameHandler& gameHandler) { ImGui::PopStyleVar(2); } -// ─── Who Results Window ─────────────────────────────────────────────────────── -void GameScreen::renderWhoWindow(game::GameHandler& gameHandler) { - if (!showWhoWindow_) return; - - const auto& results = gameHandler.getWhoResults(); - - ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(200, 180), ImGuiCond_FirstUseEver); - - char title[64]; - uint32_t onlineCount = gameHandler.getWhoOnlineCount(); - if (onlineCount > 0) - snprintf(title, sizeof(title), "Players Online: %u###WhoWindow", onlineCount); - else - snprintf(title, sizeof(title), "Who###WhoWindow"); - - if (!ImGui::Begin(title, &showWhoWindow_)) { - ImGui::End(); - return; - } - - // Search bar with Send button - static char whoSearchBuf[64] = {}; - bool doSearch = false; - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f); - if (ImGui::InputTextWithHint("##whosearch", "Search players...", whoSearchBuf, sizeof(whoSearchBuf), - ImGuiInputTextFlags_EnterReturnsTrue)) - doSearch = true; - ImGui::SameLine(); - if (ImGui::Button("Search", ImVec2(-1, 0))) - doSearch = true; - if (doSearch) { - gameHandler.queryWho(std::string(whoSearchBuf)); - } - ImGui::Separator(); - - if (results.empty()) { - ImGui::TextDisabled("No results. Type a filter above or use /who [filter]."); - ImGui::End(); - return; - } - - // Table: Name | Guild | Level | Class | Zone - if (ImGui::BeginTable("##WhoTable", 5, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, - ImVec2(0, 0))) { - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.22f); - ImGui::TableSetupColumn("Guild", ImGuiTableColumnFlags_WidthStretch, 0.20f); - ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f); - ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 0.20f); - ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch, 0.28f); - ImGui::TableHeadersRow(); - - for (size_t i = 0; i < results.size(); ++i) { - const auto& e = results[i]; - ImGui::TableNextRow(); - ImGui::PushID(static_cast(i)); - - // Name (class-colored if class is known) - ImGui::TableSetColumnIndex(0); - uint8_t cid = static_cast(e.classId); - ImVec4 nameCol = classColorVec4(cid); - ImGui::TextColored(nameCol, "%s", e.name.c_str()); - - // Right-click context menu on the name - if (ImGui::BeginPopupContextItem("##WhoCtx")) { - ImGui::TextDisabled("%s", e.name.c_str()); - ImGui::Separator(); - if (ImGui::MenuItem("Whisper")) { - selectedChatType = 4; - strncpy(whisperTargetBuffer, e.name.c_str(), sizeof(whisperTargetBuffer) - 1); - whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; - refocusChatInput = true; - } - if (ImGui::MenuItem("Invite to Group")) - gameHandler.inviteToGroup(e.name); - if (ImGui::MenuItem("Add Friend")) - gameHandler.addFriend(e.name); - if (ImGui::MenuItem("Ignore")) - gameHandler.addIgnore(e.name); - ImGui::EndPopup(); - } - - // Guild - ImGui::TableSetColumnIndex(1); - if (!e.guildName.empty()) - ImGui::TextDisabled("<%s>", e.guildName.c_str()); - - // Level - ImGui::TableSetColumnIndex(2); - ImGui::Text("%u", e.level); - - // Class - ImGui::TableSetColumnIndex(3); - const char* className = game::getClassName(static_cast(e.classId)); - ImGui::TextColored(nameCol, "%s", className); - - // Zone - ImGui::TableSetColumnIndex(4); - if (e.zoneId != 0) { - std::string zoneName = gameHandler.getWhoAreaName(e.zoneId); - ImGui::TextUnformatted(zoneName.empty() ? "Unknown" : zoneName.c_str()); - } - - ImGui::PopID(); - } - - ImGui::EndTable(); - } - - ImGui::End(); -} - -// ─── Combat Log Window ──────────────────────────────────────────────────────── -void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { - if (!showCombatLog_) return; - - const auto& log = gameHandler.getCombatLog(); - - ImGui::SetNextWindowSize(ImVec2(520, 320), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(160, 200), ImGuiCond_FirstUseEver); - - char title[64]; - snprintf(title, sizeof(title), "Combat Log (%zu)###CombatLog", log.size()); - if (!ImGui::Begin(title, &showCombatLog_)) { - ImGui::End(); - return; - } - - // Filter toggles - static bool filterDamage = true; - static bool filterHeal = true; - static bool filterMisc = true; - static bool autoScroll = true; - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 2)); - ImGui::Checkbox("Damage", &filterDamage); ImGui::SameLine(); - ImGui::Checkbox("Healing", &filterHeal); ImGui::SameLine(); - ImGui::Checkbox("Misc", &filterMisc); ImGui::SameLine(); - ImGui::Checkbox("Auto-scroll", &autoScroll); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - 40.0f); - if (ImGui::SmallButton("Clear")) - gameHandler.clearCombatLog(); - ImGui::PopStyleVar(); - ImGui::Separator(); - - // Helper: categorize entry - auto isDamageType = [](game::CombatTextEntry::Type t) { - using T = game::CombatTextEntry; - return t == T::MELEE_DAMAGE || t == T::SPELL_DAMAGE || - t == T::CRIT_DAMAGE || t == T::PERIODIC_DAMAGE || - t == T::ENVIRONMENTAL; - }; - auto isHealType = [](game::CombatTextEntry::Type t) { - using T = game::CombatTextEntry; - return t == T::HEAL || t == T::CRIT_HEAL || t == T::PERIODIC_HEAL; - }; - - // Two-column table: Time | Event description - ImGuiTableFlags tableFlags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | - ImGuiTableFlags_SizingFixedFit; - float availH = ImGui::GetContentRegionAvail().y; - if (ImGui::BeginTable("##CombatLogTable", 2, tableFlags, ImVec2(0.0f, availH))) { - ImGui::TableSetupScrollFreeze(0, 0); - ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 62.0f); - ImGui::TableSetupColumn("Event", ImGuiTableColumnFlags_WidthStretch); - - for (const auto& e : log) { - // Apply filters - bool isDmg = isDamageType(e.type); - bool isHeal = isHealType(e.type); - bool isMisc = !isDmg && !isHeal; - if (isDmg && !filterDamage) continue; - if (isHeal && !filterHeal) continue; - if (isMisc && !filterMisc) continue; - - // Format timestamp as HH:MM:SS - char timeBuf[10]; - { - struct tm* tm_info = std::localtime(&e.timestamp); - if (tm_info) - snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d:%02d", - tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec); - else - snprintf(timeBuf, sizeof(timeBuf), "--:--:--"); - } - - // Build event description and choose color - char desc[256]; - ImVec4 color; - using T = game::CombatTextEntry; - const char* src = e.sourceName.empty() ? (e.isPlayerSource ? "You" : "?") : e.sourceName.c_str(); - const char* tgt = e.targetName.empty() ? "?" : e.targetName.c_str(); - const std::string& spellName = (e.spellId != 0) ? gameHandler.getSpellName(e.spellId) : std::string(); - const char* spell = spellName.empty() ? nullptr : spellName.c_str(); - - switch (e.type) { - case T::MELEE_DAMAGE: - snprintf(desc, sizeof(desc), "%s hits %s for %d", src, tgt, e.amount); - color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.4f, 0.4f, 1.0f); - break; - case T::CRIT_DAMAGE: - snprintf(desc, sizeof(desc), "%s crits %s for %d!", src, tgt, e.amount); - color = e.isPlayerSource ? ImVec4(1.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.2f, 0.2f, 1.0f); - break; - case T::SPELL_DAMAGE: - if (spell) - snprintf(desc, sizeof(desc), "%s's %s hits %s for %d", src, spell, tgt, e.amount); - else - snprintf(desc, sizeof(desc), "%s's spell hits %s for %d", src, tgt, e.amount); - color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.4f, 0.4f, 1.0f); - break; - case T::PERIODIC_DAMAGE: - if (spell) - snprintf(desc, sizeof(desc), "%s's %s ticks %s for %d", src, spell, tgt, e.amount); - else - snprintf(desc, sizeof(desc), "%s's DoT ticks %s for %d", src, tgt, e.amount); - color = e.isPlayerSource ? ImVec4(0.9f, 0.7f, 0.3f, 1.0f) : ImVec4(0.9f, 0.3f, 0.3f, 1.0f); - break; - case T::HEAL: - if (spell) - snprintf(desc, sizeof(desc), "%s heals %s for %d (%s)", src, tgt, e.amount, spell); - else - snprintf(desc, sizeof(desc), "%s heals %s for %d", src, tgt, e.amount); - color = ImVec4(0.4f, 1.0f, 0.4f, 1.0f); - break; - case T::CRIT_HEAL: - if (spell) - snprintf(desc, sizeof(desc), "%s critically heals %s for %d! (%s)", src, tgt, e.amount, spell); - else - snprintf(desc, sizeof(desc), "%s critically heals %s for %d!", src, tgt, e.amount); - color = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); - break; - case T::PERIODIC_HEAL: - if (spell) - snprintf(desc, sizeof(desc), "%s's %s heals %s for %d", src, spell, tgt, e.amount); - else - snprintf(desc, sizeof(desc), "%s's HoT heals %s for %d", src, tgt, e.amount); - color = ImVec4(0.4f, 0.9f, 0.4f, 1.0f); - break; - case T::MISS: - 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); - 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); - 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); - color = ImVec4(0.65f, 0.75f, 0.65f, 1.0f); - break; - case T::IMMUNE: - 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); - color = ImVec4(0.5f, 0.8f, 1.0f, 1.0f); - break; - case T::RESIST: - snprintf(desc, sizeof(desc), "%d resisted", e.amount); - color = ImVec4(0.6f, 0.6f, 0.9f, 1.0f); - break; - case T::ENVIRONMENTAL: - snprintf(desc, sizeof(desc), "Environmental damage: %d", e.amount); - color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f); - break; - case T::ENERGIZE: - if (spell) - snprintf(desc, sizeof(desc), "%s gains %d power (%s)", tgt, e.amount, spell); - else - snprintf(desc, sizeof(desc), "%s gains %d power", tgt, e.amount); - color = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); - break; - case T::XP_GAIN: - snprintf(desc, sizeof(desc), "You gain %d experience", e.amount); - color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f); - break; - case T::PROC_TRIGGER: - if (spell) - snprintf(desc, sizeof(desc), "%s procs!", spell); - else - snprintf(desc, sizeof(desc), "Proc triggered"); - color = ImVec4(1.0f, 0.85f, 0.3f, 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); - break; - } - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextDisabled("%s", timeBuf); - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(color, "%s", desc); - } - - // Auto-scroll to bottom - if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) - ImGui::SetScrollHereY(1.0f); - - ImGui::EndTable(); - } - - ImGui::End(); -} - // ─── Achievement Window ─────────────────────────────────────────────────────── void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { if (!showAchievementWindow_) return; @@ -17559,22 +14745,7 @@ void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { ImGui::TextUnformatted(display.c_str()); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); - ImGui::Text("Achievement ID: %u", id); - uint32_t packed = gameHandler.getAchievementDate(id); - if (packed != 0) { - // WoW PackedTime: year[31:25] month[24:21] day[20:17] weekday[16:14] hour[13:9] minute[8:3] - int minute = (packed >> 3) & 0x3F; - int hour = (packed >> 9) & 0x1F; - int day = (packed >> 17) & 0x1F; - int month = (packed >> 21) & 0x0F; - int year = ((packed >> 25) & 0x7F) + 2000; - static const char* kMonths[12] = { - "Jan","Feb","Mar","Apr","May","Jun", - "Jul","Aug","Sep","Oct","Nov","Dec" - }; - const char* mname = (month >= 1 && month <= 12) ? kMonths[month - 1] : "?"; - ImGui::Text("Earned: %s %d, %d %02d:%02d", mname, day, year, hour, minute); - } + ImGui::Text("ID: %u", id); ImGui::EndTooltip(); } ImGui::PopID(); @@ -17682,38 +14853,10 @@ void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) { uint32_t maxThreat = list->front().threat; - // Pre-scan to find the player's rank and threat percentage - uint64_t playerGuid = gameHandler.getPlayerGuid(); - int playerRank = 0; - float playerPct = 0.0f; - { - int scan = 0; - for (const auto& e : *list) { - ++scan; - if (e.victimGuid == playerGuid) { - playerRank = scan; - playerPct = (maxThreat > 0) ? static_cast(e.threat) / static_cast(maxThreat) : 0.0f; - break; - } - if (scan >= 10) break; - } - } - - // Status bar: aggro alert or rank summary - if (playerRank == 1) { - // Player has aggro — persistent red warning - ImGui::TextColored(ImVec4(1.0f, 0.25f, 0.25f, 1.0f), "!! YOU HAVE AGGRO !!"); - } else if (playerRank > 1 && playerPct >= 0.8f) { - // Close to pulling — pulsing warning - float pulse = 0.55f + 0.45f * sinf(static_cast(ImGui::GetTime()) * 5.0f); - ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.1f, pulse), "! PULLING AGGRO (%.0f%%) !", playerPct * 100.0f); - } else if (playerRank > 0) { - ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "You: #%d %.0f%% threat", playerRank, playerPct * 100.0f); - } - ImGui::TextDisabled("%-19s Threat", "Player"); ImGui::Separator(); + uint64_t playerGuid = gameHandler.getPlayerGuid(); int rank = 0; for (const auto& entry : *list) { ++rank; @@ -17759,139 +14902,6 @@ void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) { ImGui::End(); } -// ─── BG Scoreboard ──────────────────────────────────────────────────────────── -void GameScreen::renderBgScoreboard(game::GameHandler& gameHandler) { - if (!showBgScoreboard_) return; - - const game::GameHandler::BgScoreboardData* data = gameHandler.getBgScoreboard(); - - ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(150, 100), ImGuiCond_FirstUseEver); - - const char* title = "Battleground Score###BgScore"; - if (!ImGui::Begin(title, &showBgScoreboard_, ImGuiWindowFlags_NoCollapse)) { - ImGui::End(); - return; - } - - if (!data) { - ImGui::TextDisabled("No score data yet."); - ImGui::TextDisabled("Use /score to request the scoreboard while in a battleground."); - ImGui::End(); - return; - } - - // Winner banner - if (data->hasWinner) { - const char* winnerStr = (data->winner == 1) ? "Alliance" : "Horde"; - ImVec4 winnerColor = (data->winner == 1) ? ImVec4(0.4f, 0.6f, 1.0f, 1.0f) - : ImVec4(1.0f, 0.35f, 0.35f, 1.0f); - float textW = ImGui::CalcTextSize(winnerStr).x + ImGui::CalcTextSize(" Victory!").x; - ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - textW) * 0.5f); - ImGui::TextColored(winnerColor, "%s", winnerStr); - ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Victory!"); - ImGui::Separator(); - } - - // Refresh button - if (ImGui::SmallButton("Refresh")) { - gameHandler.requestPvpLog(); - } - ImGui::SameLine(); - ImGui::TextDisabled("%zu players", data->players.size()); - - // Score table - constexpr ImGuiTableFlags kTableFlags = - ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | - ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | - ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Sortable; - - // Build dynamic column count based on what BG-specific stats are present - int numBgCols = 0; - std::vector bgColNames; - for (const auto& ps : data->players) { - for (const auto& [fieldName, val] : ps.bgStats) { - // Extract short name after last '.' (e.g. "BattlegroundAB.AbFlagCaptures" → "Caps") - std::string shortName = fieldName; - auto dotPos = fieldName.rfind('.'); - if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1); - bool found = false; - for (const auto& n : bgColNames) { if (n == shortName) { found = true; break; } } - if (!found) bgColNames.push_back(shortName); - } - } - numBgCols = static_cast(bgColNames.size()); - - // Fixed cols: Team | Name | KB | Deaths | HKs | Honor; then BG-specific - int totalCols = 6 + numBgCols; - float tableH = ImGui::GetContentRegionAvail().y; - if (ImGui::BeginTable("##BgScoreTable", totalCols, kTableFlags, ImVec2(0.0f, tableH))) { - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Team", ImGuiTableColumnFlags_WidthFixed, 56.0f); - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("KB", ImGuiTableColumnFlags_WidthFixed, 38.0f); - ImGui::TableSetupColumn("Deaths", ImGuiTableColumnFlags_WidthFixed, 52.0f); - ImGui::TableSetupColumn("HKs", ImGuiTableColumnFlags_WidthFixed, 38.0f); - ImGui::TableSetupColumn("Honor", ImGuiTableColumnFlags_WidthFixed, 52.0f); - for (const auto& col : bgColNames) - ImGui::TableSetupColumn(col.c_str(), ImGuiTableColumnFlags_WidthFixed, 52.0f); - ImGui::TableHeadersRow(); - - // Sort: Alliance first, then Horde; within each team by KB desc - std::vector sorted; - sorted.reserve(data->players.size()); - for (const auto& ps : data->players) sorted.push_back(&ps); - std::stable_sort(sorted.begin(), sorted.end(), - [](const game::GameHandler::BgPlayerScore* a, - const game::GameHandler::BgPlayerScore* b) { - if (a->team != b->team) return a->team > b->team; // Alliance(1) first - return a->killingBlows > b->killingBlows; - }); - - uint64_t playerGuid = gameHandler.getPlayerGuid(); - for (const auto* ps : sorted) { - ImGui::TableNextRow(); - - // Team - ImGui::TableNextColumn(); - if (ps->team == 1) - ImGui::TextColored(ImVec4(0.4f, 0.6f, 1.0f, 1.0f), "Alliance"); - else - ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Horde"); - - // Name (highlight player's own row) - ImGui::TableNextColumn(); - bool isSelf = (ps->guid == playerGuid); - if (isSelf) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); - const char* nameStr = ps->name.empty() ? "Unknown" : ps->name.c_str(); - ImGui::TextUnformatted(nameStr); - if (isSelf) ImGui::PopStyleColor(); - - ImGui::TableNextColumn(); ImGui::Text("%u", ps->killingBlows); - ImGui::TableNextColumn(); ImGui::Text("%u", ps->deaths); - ImGui::TableNextColumn(); ImGui::Text("%u", ps->honorableKills); - ImGui::TableNextColumn(); ImGui::Text("%u", ps->bonusHonor); - - for (const auto& col : bgColNames) { - ImGui::TableNextColumn(); - uint32_t val = 0; - for (const auto& [fieldName, fval] : ps->bgStats) { - std::string shortName = fieldName; - auto dotPos = fieldName.rfind('.'); - if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1); - if (shortName == col) { val = fval; break; } - } - if (val > 0) ImGui::Text("%u", val); - else ImGui::TextDisabled("-"); - } - } - ImGui::EndTable(); - } - - ImGui::End(); -} - // ─── Quest Objective Tracker ────────────────────────────────────────────────── void GameScreen::renderObjectiveTracker(game::GameHandler& gameHandler) { if (gameHandler.getState() != game::WorldState::IN_WORLD) return; @@ -18022,19 +15032,10 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { return; } - // Player name — class-colored if entity is loaded, else gold - { - auto ent = gameHandler.getEntityManager().getEntity(result->guid); - uint8_t cid = entityClassId(ent.get()); - ImVec4 nameColor = (cid != 0) ? classColorVec4(cid) : ImVec4(1.0f, 0.82f, 0.0f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_Text, nameColor); - ImGui::Text("%s", result->playerName.c_str()); - ImGui::PopStyleColor(); - if (cid != 0) { - ImGui::SameLine(); - ImGui::TextColored(classColorVec4(cid), "(%s)", classNameStr(cid)); - } - } + // Talent summary + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.82f, 0.0f, 1.0f)); // gold + ImGui::Text("%s", result->playerName.c_str()); + ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled(" %u talent pts", result->totalTalents); if (result->unspentTalents > 0) { @@ -18058,29 +15059,7 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { ImGui::TextDisabled("Equipment data not yet available."); ImGui::TextDisabled("(Gear loads after the player is inspected in-range)"); } else { - // Average item level (only slots that have loaded info and are not shirt/tabard) - // Shirt=slot3, Tabard=slot18 — excluded from gear score by WoW convention - uint32_t iLevelSum = 0; - int iLevelCount = 0; - for (int s = 0; s < 19; ++s) { - if (s == 3 || s == 18) continue; // shirt, tabard - uint32_t entry = result->itemEntries[s]; - if (entry == 0) continue; - const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry); - if (info && info->valid && info->itemLevel > 0) { - iLevelSum += info->itemLevel; - ++iLevelCount; - } - } - if (iLevelCount > 0) { - float avgIlvl = static_cast(iLevelSum) / static_cast(iLevelCount); - ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Avg iLvl: %.1f", avgIlvl); - ImGui::SameLine(); - ImGui::TextDisabled("(%d/%d slots loaded)", iLevelCount, - [&]{ int c=0; for(int s=0;s<19;++s){if(s==3||s==18)continue;if(result->itemEntries[s])++c;} return c; }()); - } if (ImGui::BeginChild("##inspect_gear", ImVec2(0, 0), false)) { - constexpr float kIconSz = 28.0f; for (int s = 0; s < 19; ++s) { uint32_t entry = result->itemEntries[s]; if (entry == 0) continue; @@ -18088,56 +15067,24 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry); if (!info) { gameHandler.ensureItemInfo(entry); - ImGui::PushID(s); ImGui::TextDisabled("[%s] (loading…)", kSlotNames[s]); - ImGui::PopID(); continue; } - ImGui::PushID(s); + ImGui::TextDisabled("%s", kSlotNames[s]); + ImGui::SameLine(90); auto qColor = InventoryScreen::getQualityColor( static_cast(info->quality)); - uint16_t enchantId = result->enchantIds[s]; - - // Item icon - VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId); - if (iconTex) { - ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(kIconSz, kIconSz), - ImVec2(0,0), ImVec2(1,1), - ImVec4(1,1,1,1), qColor); - } else { - ImGui::GetWindowDrawList()->AddRectFilled( - ImGui::GetCursorScreenPos(), - ImVec2(ImGui::GetCursorScreenPos().x + kIconSz, - ImGui::GetCursorScreenPos().y + kIconSz), - IM_COL32(40, 40, 50, 200)); - ImGui::Dummy(ImVec2(kIconSz, kIconSz)); - } - bool hovered = ImGui::IsItemHovered(); - - ImGui::SameLine(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (kIconSz - ImGui::GetTextLineHeight()) * 0.5f); - ImGui::BeginGroup(); - ImGui::TextDisabled("%s", kSlotNames[s]); ImGui::TextColored(qColor, "%s", info->name.c_str()); - // Enchant indicator on the same row as the name - if (enchantId != 0) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, 1.0f), "\xe2\x9c\xa6"); // UTF-8 ✦ - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Enchanted (ID %u)", static_cast(enchantId)); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextColored(qColor, "%s", info->name.c_str()); + if (info->itemLevel > 0) + ImGui::Text("Item Level %u", info->itemLevel); + if (info->armor > 0) + ImGui::Text("Armor: %d", info->armor); + ImGui::EndTooltip(); } - ImGui::EndGroup(); - hovered = hovered || ImGui::IsItemHovered(); - - if (hovered && info->valid) { - inventoryScreen.renderItemTooltip(*info); - } else if (hovered) { - ImGui::SetTooltip("%s", info->name.c_str()); - } - - ImGui::PopID(); - ImGui::Spacing(); } } ImGui::EndChild(); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index e2075f09..e5735977 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -72,21 +72,6 @@ const game::ItemSlot* findComparableEquipped(const game::Inventory& inventory, u default: return nullptr; } } - -void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) { - bool any = false; - if (g > 0) { - ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g); - any = true; - } - if (s > 0 || g > 0) { - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s); - any = true; - } - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c); -} } // namespace InventoryScreen::~InventoryScreen() { @@ -2212,8 +2197,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I uint32_t g = item.sellPrice / 10000; uint32_t s = (item.sellPrice / 100) % 100; uint32_t c = item.sellPrice % 100; - ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c); } // Shift-hover comparison with currently equipped equivalent. @@ -2337,7 +2321,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I // --------------------------------------------------------------------------- // Tooltip overload for ItemQueryResponseData (used by loot window, etc.) // --------------------------------------------------------------------------- -void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory) { +void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) { ImGui::BeginTooltip(); ImVec4 qColor = getQualityColor(static_cast(info.quality)); @@ -2493,50 +2477,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, uint32_t g = info.sellPrice / 10000; uint32_t s = (info.sellPrice / 100) % 100; uint32_t c = info.sellPrice % 100; - ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); - } - - // Shift-hover: compare with currently equipped item - if (inventory && ImGui::GetIO().KeyShift && info.inventoryType > 0) { - if (const game::ItemSlot* eq = findComparableEquipped(*inventory, static_cast(info.inventoryType))) { - ImGui::Separator(); - ImGui::TextDisabled("Equipped:"); - VkDescriptorSet eqIcon = getItemIcon(eq->item.displayInfoId); - if (eqIcon) { ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f)); ImGui::SameLine(); } - ImGui::TextColored(getQualityColor(eq->item.quality), "%s", eq->item.name.c_str()); - - auto showDiff = [](const char* label, float nv, float ev) { - if (nv == 0.0f && ev == 0.0f) return; - float diff = nv - ev; - char buf[96]; - if (diff > 0.0f) { std::snprintf(buf, sizeof(buf), "%s: %.0f (▲%.0f)", label, nv, diff); ImGui::TextColored(ImVec4(0.0f,1.0f,0.0f,1.0f), "%s", buf); } - else if (diff < 0.0f) { std::snprintf(buf, sizeof(buf), "%s: %.0f (▼%.0f)", label, nv, -diff); ImGui::TextColored(ImVec4(1.0f,0.3f,0.3f,1.0f), "%s", buf); } - else { std::snprintf(buf, sizeof(buf), "%s: %.0f (=)", label, nv); ImGui::TextColored(ImVec4(0.7f,0.7f,0.7f,1.0f), "%s", buf); } - }; - - float ilvlDiff = static_cast(info.itemLevel) - static_cast(eq->item.itemLevel); - if (info.itemLevel > 0 || eq->item.itemLevel > 0) { - char ilvlBuf[64]; - if (ilvlDiff > 0) std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▲%.0f)", info.itemLevel, ilvlDiff); - else if (ilvlDiff < 0) std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▼%.0f)", info.itemLevel, -ilvlDiff); - else std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (=)", info.itemLevel); - ImVec4 ic = ilvlDiff > 0 ? ImVec4(0,1,0,1) : ilvlDiff < 0 ? ImVec4(1,0.3f,0.3f,1) : ImVec4(0.7f,0.7f,0.7f,1); - ImGui::TextColored(ic, "%s", ilvlBuf); - } - - showDiff("Armor", static_cast(info.armor), static_cast(eq->item.armor)); - showDiff("Str", static_cast(info.strength), static_cast(eq->item.strength)); - showDiff("Agi", static_cast(info.agility), static_cast(eq->item.agility)); - showDiff("Sta", static_cast(info.stamina), static_cast(eq->item.stamina)); - showDiff("Int", static_cast(info.intellect), static_cast(eq->item.intellect)); - showDiff("Spi", static_cast(info.spirit), static_cast(eq->item.spirit)); - - // Hint text - ImGui::TextDisabled("Hold Shift to compare"); - } - } else if (info.inventoryType > 0) { - ImGui::TextDisabled("Hold Shift to compare"); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c); } ImGui::EndTooltip(); diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index c343baa5..81f8657d 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -205,21 +205,6 @@ std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) { if (s.size() > 72) s = s.substr(0, 72) + "..."; return s; } - -void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) { - bool any = false; - if (g > 0) { - ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g); - any = true; - } - if (s > 0 || g > 0) { - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s); - any = true; - } - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c); -} } // anonymous namespace void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& invScreen) { @@ -377,11 +362,6 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv if (ImGui::MenuItem(tracked ? "Untrack" : "Track")) { gameHandler.setQuestTracked(q.questId, !tracked); } - if (gameHandler.isInGroup() && !q.complete) { - if (ImGui::MenuItem("Share Quest")) { - gameHandler.shareQuestWithParty(q.questId); - } - } if (!q.complete) { ImGui::Separator(); if (ImGui::MenuItem("Abandon Quest")) { @@ -508,7 +488,12 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv uint32_t rg = static_cast(sel.rewardMoney) / 10000; uint32_t rs = static_cast(sel.rewardMoney % 10000) / 100; uint32_t rc = static_cast(sel.rewardMoney % 100); - renderCoinsText(rg, rs, rc); + if (rg > 0) + ImGui::Text("%ug %us %uc", rg, rs, rc); + else if (rs > 0) + ImGui::Text("%us %uc", rs, rc); + else + ImGui::Text("%uc", rc); } // Guaranteed reward items @@ -564,19 +549,12 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv } } - // Track / Share / Abandon buttons + // Track / Abandon buttons ImGui::Separator(); bool isTracked = gameHandler.isQuestTracked(sel.questId); if (ImGui::Button(isTracked ? "Untrack" : "Track", ImVec2(100.0f, 0.0f))) { gameHandler.setQuestTracked(sel.questId, !isTracked); } - if (gameHandler.isInGroup() && !sel.complete) { - ImGui::SameLine(); - if (ImGui::Button("Share", ImVec2(80.0f, 0.0f))) { - gameHandler.shareQuestWithParty(sel.questId); - } - if (ImGui::IsItemHovered()) ImGui::SetTooltip("Share this quest with your party"); - } if (!sel.complete) { ImGui::SameLine(); if (ImGui::Button("Abandon Quest", ImVec2(150.0f, 0.0f))) { diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index 8c78ab7d..e2c81756 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -203,29 +203,6 @@ std::string SpellbookScreen::lookupSpellName(uint32_t spellId, pipeline::AssetMa return {}; } -uint32_t SpellbookScreen::getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager) { - if (!dbcLoadAttempted) { - loadSpellDBC(assetManager); - } - auto it = spellData.find(spellId); - if (it != spellData.end()) return it->second.rangeIndex; - return 0; -} - -void SpellbookScreen::getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager, - uint32_t& outCost, uint32_t& outPowerType) { - outCost = 0; - outPowerType = 0; - if (!dbcLoadAttempted) { - loadSpellDBC(assetManager); - } - auto it = spellData.find(spellId); - if (it != spellData.end()) { - outCost = it->second.manaCost; - outPowerType = it->second.powerType; - } -} - void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { if (iconDbLoaded) return; iconDbLoaded = true;