mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add XP tracking with level-up, kill XP formula, and server-compatible SMSG_LOG_XPGAIN support
This commit is contained in:
parent
ed5d10ec01
commit
78442f8aea
7 changed files with 249 additions and 0 deletions
|
|
@ -219,8 +219,15 @@ public:
|
|||
localPlayerLevel_ = level;
|
||||
localPlayerHealth_ = hp;
|
||||
localPlayerMaxHealth_ = maxHp;
|
||||
playerNextLevelXp_ = xpForLevel(level);
|
||||
playerXp_ = 0;
|
||||
}
|
||||
|
||||
// XP tracking (works in both single-player and server modes)
|
||||
uint32_t getPlayerXp() const { return playerXp_; }
|
||||
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
||||
uint32_t getPlayerLevel() const { return singlePlayerMode_ ? localPlayerLevel_ : serverPlayerLevel_; }
|
||||
|
||||
// Hearthstone callback (single-player teleport)
|
||||
using HearthstoneCallback = std::function<void()>;
|
||||
void setHearthstoneCallback(HearthstoneCallback cb) { hearthstoneCallback = std::move(cb); }
|
||||
|
|
@ -361,6 +368,9 @@ private:
|
|||
void handleGroupUninvite(network::Packet& packet);
|
||||
void handlePartyCommandResult(network::Packet& packet);
|
||||
|
||||
// ---- XP handler ----
|
||||
void handleXpGain(network::Packet& packet);
|
||||
|
||||
// ---- Phase 5 handlers ----
|
||||
void handleLootResponse(network::Packet& packet);
|
||||
void handleLootReleaseResponse(network::Packet& packet);
|
||||
|
|
@ -486,6 +496,15 @@ private:
|
|||
WorldConnectSuccessCallback onSuccess;
|
||||
WorldConnectFailureCallback onFailure;
|
||||
|
||||
// ---- XP tracking ----
|
||||
uint32_t playerXp_ = 0;
|
||||
uint32_t playerNextLevelXp_ = 0;
|
||||
uint32_t serverPlayerLevel_ = 1;
|
||||
void awardLocalXp(uint32_t victimLevel);
|
||||
void levelUp();
|
||||
static uint32_t xpForLevel(uint32_t level);
|
||||
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
||||
|
||||
// ---- Single-player combat ----
|
||||
bool singlePlayerMode_ = false;
|
||||
float swingTimer_ = 0.0f;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ enum class Opcode : uint16_t {
|
|||
SMSG_GAMEOBJECT_QUERY_RESPONSE = 0x05F,
|
||||
CMSG_SET_ACTIVE_MOVER = 0x26A,
|
||||
|
||||
// ---- XP ----
|
||||
SMSG_LOG_XPGAIN = 0x1D0,
|
||||
|
||||
// ---- Phase 2: Combat Core ----
|
||||
CMSG_ATTACKSWING = 0x141,
|
||||
CMSG_ATTACKSTOP = 0x142,
|
||||
|
|
|
|||
|
|
@ -742,6 +742,25 @@ public:
|
|||
static bool parse(network::Packet& packet, SpellHealLogData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// XP Gain
|
||||
// ============================================================
|
||||
|
||||
/** SMSG_LOG_XPGAIN data */
|
||||
struct XpGainData {
|
||||
uint64_t victimGuid = 0; // 0 for non-kill XP (quest, exploration)
|
||||
uint32_t totalXp = 0;
|
||||
uint8_t type = 0; // 0 = kill, 1 = non-kill
|
||||
uint32_t groupBonus = 0;
|
||||
|
||||
bool isValid() const { return totalXp > 0; }
|
||||
};
|
||||
|
||||
class XpGainParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, XpGainData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells, Action Bar, Auras
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ private:
|
|||
|
||||
// ---- New UI renders ----
|
||||
void renderActionBar(game::GameHandler& gameHandler);
|
||||
void renderXpBar(game::GameHandler& gameHandler);
|
||||
void renderCastBar(game::GameHandler& gameHandler);
|
||||
void renderCombatText(game::GameHandler& gameHandler);
|
||||
void renderPartyFrames(game::GameHandler& gameHandler);
|
||||
|
|
|
|||
|
|
@ -253,6 +253,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleCreatureQueryResponse(packet);
|
||||
break;
|
||||
|
||||
// ---- XP ----
|
||||
case Opcode::SMSG_LOG_XPGAIN:
|
||||
handleXpGain(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
case Opcode::SMSG_ATTACKSTART:
|
||||
handleAttackStart(packet);
|
||||
|
|
@ -824,6 +829,17 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Extract XP fields for player entity
|
||||
if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) {
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
switch (key) {
|
||||
case 634: playerXp_ = val; break; // PLAYER_XP
|
||||
case 635: playerNextLevelXp_ = val; break; // PLAYER_NEXT_LEVEL_XP
|
||||
case 54: serverPlayerLevel_ = val; break; // UNIT_FIELD_LEVEL
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -849,6 +865,17 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Update XP fields for player entity
|
||||
if (block.guid == playerGuid) {
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
switch (key) {
|
||||
case 634: playerXp_ = val; break; // PLAYER_XP
|
||||
case 635: playerNextLevelXp_ = val; break; // PLAYER_NEXT_LEVEL_XP
|
||||
case 54: serverPlayerLevel_ = val; break; // UNIT_FIELD_LEVEL
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
|
||||
} else {
|
||||
|
|
@ -1692,6 +1719,13 @@ void GameHandler::performPlayerSwing() {
|
|||
}
|
||||
|
||||
void GameHandler::handleNpcDeath(uint64_t guid) {
|
||||
// Award XP from kill
|
||||
auto entity = entityManager.getEntity(guid);
|
||||
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
awardLocalXp(unit->getLevel());
|
||||
}
|
||||
|
||||
// Remove from aggro list
|
||||
aggroList_.erase(
|
||||
std::remove_if(aggroList_.begin(), aggroList_.end(),
|
||||
|
|
@ -1795,6 +1829,102 @@ void GameHandler::performNpcSwing(uint64_t guid) {
|
|||
damage, 0, false);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// XP tracking
|
||||
// ============================================================
|
||||
|
||||
// WotLK 3.3.5a XP-to-next-level table (from player_xp_for_level)
|
||||
static const uint32_t XP_TABLE[] = {
|
||||
0, // level 0 (unused)
|
||||
400, 900, 1400, 2100, 2800, 3600, 4500, 5400, 6500, 7600, // 1-10
|
||||
8700, 9800, 11000, 12300, 13600, 15000, 16400, 17800, 19300, 20800, // 11-20
|
||||
22400, 24000, 25500, 27200, 28900, 30500, 32200, 33900, 36300, 38800, // 21-30
|
||||
41600, 44600, 48000, 51400, 55000, 58700, 62400, 66200, 70200, 74300, // 31-40
|
||||
78500, 82800, 87100, 91600, 96300, 101000, 105800, 110700, 115700, 120900, // 41-50
|
||||
126100, 131500, 137000, 142500, 148200, 154000, 159900, 165800, 172000, 290000, // 51-60
|
||||
317000, 349000, 386000, 428000, 475000, 527000, 585000, 648000, 717000, 1523800, // 61-70
|
||||
1539600, 1555700, 1571800, 1587900, 1604200, 1620700, 1637400, 1653900, 1670800 // 71-79
|
||||
};
|
||||
static constexpr uint32_t XP_TABLE_SIZE = sizeof(XP_TABLE) / sizeof(XP_TABLE[0]);
|
||||
|
||||
uint32_t GameHandler::xpForLevel(uint32_t level) {
|
||||
if (level == 0 || level >= XP_TABLE_SIZE) return 0;
|
||||
return XP_TABLE[level];
|
||||
}
|
||||
|
||||
uint32_t GameHandler::killXp(uint32_t playerLevel, uint32_t victimLevel) {
|
||||
if (playerLevel == 0 || victimLevel == 0) return 0;
|
||||
|
||||
// Gray level check (too low = 0 XP)
|
||||
int32_t grayLevel;
|
||||
if (playerLevel <= 5) grayLevel = 0;
|
||||
else if (playerLevel <= 39) grayLevel = static_cast<int32_t>(playerLevel) - 5 - static_cast<int32_t>(playerLevel) / 10;
|
||||
else if (playerLevel <= 59) grayLevel = static_cast<int32_t>(playerLevel) - 1 - static_cast<int32_t>(playerLevel) / 5;
|
||||
else grayLevel = static_cast<int32_t>(playerLevel) - 9;
|
||||
|
||||
if (static_cast<int32_t>(victimLevel) <= grayLevel) return 0;
|
||||
|
||||
// Base XP = 45 + 5 * victimLevel (WoW-like ZeroDifference formula)
|
||||
uint32_t baseXp = 45 + 5 * victimLevel;
|
||||
|
||||
// Level difference multiplier
|
||||
int32_t diff = static_cast<int32_t>(victimLevel) - static_cast<int32_t>(playerLevel);
|
||||
float multiplier = 1.0f + diff * 0.05f;
|
||||
if (multiplier < 0.1f) multiplier = 0.1f;
|
||||
if (multiplier > 2.0f) multiplier = 2.0f;
|
||||
|
||||
return static_cast<uint32_t>(baseXp * multiplier);
|
||||
}
|
||||
|
||||
void GameHandler::awardLocalXp(uint32_t victimLevel) {
|
||||
if (localPlayerLevel_ >= 80) return; // Level cap
|
||||
|
||||
uint32_t xp = killXp(localPlayerLevel_, victimLevel);
|
||||
if (xp == 0) return;
|
||||
|
||||
playerXp_ += xp;
|
||||
|
||||
// Show XP gain in combat text as a heal-type (gold text)
|
||||
addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(xp), 0, true);
|
||||
|
||||
LOG_INFO("XP gained: +", xp, " (total: ", playerXp_, "/", playerNextLevelXp_, ")");
|
||||
|
||||
// Check for level-up
|
||||
while (playerXp_ >= playerNextLevelXp_ && localPlayerLevel_ < 80) {
|
||||
playerXp_ -= playerNextLevelXp_;
|
||||
levelUp();
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::levelUp() {
|
||||
localPlayerLevel_++;
|
||||
playerNextLevelXp_ = xpForLevel(localPlayerLevel_);
|
||||
|
||||
// Scale HP with level
|
||||
uint32_t newMaxHp = 20 + localPlayerLevel_ * 10;
|
||||
localPlayerMaxHealth_ = newMaxHp;
|
||||
localPlayerHealth_ = newMaxHp; // Full heal on level-up
|
||||
|
||||
LOG_INFO("LEVEL UP! Now level ", localPlayerLevel_,
|
||||
" (HP: ", newMaxHp, ", next level: ", playerNextLevelXp_, " XP)");
|
||||
|
||||
// Announce in chat
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You have reached level " + std::to_string(localPlayerLevel_) + "!";
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
void GameHandler::handleXpGain(network::Packet& packet) {
|
||||
XpGainData data;
|
||||
if (!XpGainParser::parse(packet, data)) return;
|
||||
|
||||
// Server already updates PLAYER_XP via update fields,
|
||||
// but we can show combat text for XP gains
|
||||
addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(data.totalXp), 0, true);
|
||||
}
|
||||
|
||||
uint32_t GameHandler::generateClientSeed() {
|
||||
// Generate cryptographically random seed
|
||||
std::random_device rd;
|
||||
|
|
|
|||
|
|
@ -1052,6 +1052,23 @@ bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data)
|
|||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// XP Gain
|
||||
// ============================================================
|
||||
|
||||
bool XpGainParser::parse(network::Packet& packet, XpGainData& data) {
|
||||
data.victimGuid = packet.readUInt64();
|
||||
data.totalXp = packet.readUInt32();
|
||||
data.type = packet.readUInt8();
|
||||
if (data.type == 0) {
|
||||
// Kill XP: has group bonus float (unused) + group bonus uint32
|
||||
packet.readFloat();
|
||||
data.groupBonus = packet.readUInt32();
|
||||
}
|
||||
LOG_INFO("XP gain: ", data.totalXp, " xp (type=", static_cast<int>(data.type), ")");
|
||||
return data.totalXp > 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells, Action Bar, Auras
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
|
||||
// ---- New UI elements ----
|
||||
renderActionBar(gameHandler);
|
||||
renderXpBar(gameHandler);
|
||||
renderCastBar(gameHandler);
|
||||
renderCombatText(gameHandler);
|
||||
renderPartyFrames(gameHandler);
|
||||
|
|
@ -1090,6 +1091,65 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
|||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// XP Bar
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
|
||||
uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp();
|
||||
if (nextLevelXp == 0) return; // No XP data yet (level 80 or not initialized)
|
||||
|
||||
uint32_t currentXp = gameHandler.getPlayerXp();
|
||||
uint32_t level = gameHandler.getPlayerLevel();
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
// Position just above the action bar
|
||||
float slotSize = 48.0f;
|
||||
float spacing = 4.0f;
|
||||
float padding = 8.0f;
|
||||
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
||||
float barH = slotSize + 24.0f;
|
||||
float actionBarY = screenH - barH;
|
||||
|
||||
float xpBarH = 14.0f;
|
||||
float xpBarW = barW;
|
||||
float xpBarX = (screenW - xpBarW) / 2.0f;
|
||||
float xpBarY = actionBarY - xpBarH - 2.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(xpBarX, xpBarY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(xpBarW, xpBarH + 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("##XpBar", nullptr, flags)) {
|
||||
float pct = static_cast<float>(currentXp) / static_cast<float>(nextLevelXp);
|
||||
if (pct > 1.0f) pct = 1.0f;
|
||||
|
||||
// Purple XP bar (WoW-style)
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.58f, 0.2f, 0.93f, 1.0f));
|
||||
|
||||
char overlay[96];
|
||||
snprintf(overlay, sizeof(overlay), "Lv %u - %u / %u XP", level, currentXp, nextLevelXp);
|
||||
ImGui::ProgressBar(pct, ImVec2(-1, xpBarH - 4.0f), overlay);
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Cast Bar (Phase 3)
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue