mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Restructure inventory UI, add vendor selling, camera intro on all spawns, and quest log
Split inventory into bags-only (B key) and character screen (C key). Vendor window auto-opens bags with sell prices on hover and right-click to sell. Add camera intro pan on all login/spawn/teleport/hearthstone events and idle orbit after 2 minutes. Add quest log UI, SMSG_MONSTER_MOVE handling, deferred creature spawn queue, and creature fade-in/movement interpolation for online mode.
This commit is contained in:
parent
bb4c2c25f7
commit
71c3d2ea77
21 changed files with 1092 additions and 149 deletions
|
|
@ -152,6 +152,7 @@ set(WOWEE_SOURCES
|
||||||
src/ui/character_screen.cpp
|
src/ui/character_screen.cpp
|
||||||
src/ui/game_screen.cpp
|
src/ui/game_screen.cpp
|
||||||
src/ui/inventory_screen.cpp
|
src/ui/inventory_screen.cpp
|
||||||
|
src/ui/quest_log_screen.cpp
|
||||||
src/ui/spellbook_screen.cpp
|
src/ui/spellbook_screen.cpp
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
|
|
|
||||||
|
|
@ -153,8 +153,19 @@ private:
|
||||||
std::unordered_map<uint32_t, FacialHairGeosets> facialHairGeosetMap_;
|
std::unordered_map<uint32_t, FacialHairGeosets> facialHairGeosetMap_;
|
||||||
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
|
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
|
||||||
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
||||||
|
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
|
||||||
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
||||||
bool creatureLookupsBuilt_ = false;
|
bool creatureLookupsBuilt_ = false;
|
||||||
|
|
||||||
|
// Deferred creature spawn queue (throttles spawning to avoid hangs)
|
||||||
|
struct PendingCreatureSpawn {
|
||||||
|
uint64_t guid;
|
||||||
|
uint32_t displayId;
|
||||||
|
float x, y, z, orientation;
|
||||||
|
};
|
||||||
|
std::vector<PendingCreatureSpawn> pendingCreatureSpawns_;
|
||||||
|
static constexpr int MAX_SPAWNS_PER_FRAME = 2;
|
||||||
|
void processCreatureSpawnQueue();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace core
|
} // namespace core
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,11 @@ public:
|
||||||
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
||||||
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
// Creature move callback (online mode - triggered by SMSG_MONSTER_MOVE)
|
||||||
|
// Parameters: guid, x, y, z (canonical), duration_ms (0 = instant)
|
||||||
|
using CreatureMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, uint32_t durationMs)>;
|
||||||
|
void setCreatureMoveCallback(CreatureMoveCallback cb) { creatureMoveCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// Cooldowns
|
// Cooldowns
|
||||||
float getSpellCooldown(uint32_t spellId) const;
|
float getSpellCooldown(uint32_t spellId) const;
|
||||||
|
|
||||||
|
|
@ -330,17 +335,33 @@ public:
|
||||||
bool isQuestDetailsOpen() const { return questDetailsOpen; }
|
bool isQuestDetailsOpen() const { return questDetailsOpen; }
|
||||||
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
|
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
|
||||||
|
|
||||||
|
// Quest log
|
||||||
|
struct QuestLogEntry {
|
||||||
|
uint32_t questId = 0;
|
||||||
|
std::string title;
|
||||||
|
std::string objectives;
|
||||||
|
bool complete = false;
|
||||||
|
};
|
||||||
|
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||||
|
void abandonQuest(uint32_t questId);
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
void openVendor(uint64_t npcGuid);
|
void openVendor(uint64_t npcGuid);
|
||||||
void closeVendor();
|
void closeVendor();
|
||||||
void buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count);
|
void buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count);
|
||||||
void sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count);
|
void sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count);
|
||||||
|
void sellItemBySlot(int backpackIndex);
|
||||||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||||||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||||||
const ItemQueryResponseData* getItemInfo(uint32_t itemId) const {
|
const ItemQueryResponseData* getItemInfo(uint32_t itemId) const {
|
||||||
auto it = itemInfoCache_.find(itemId);
|
auto it = itemInfoCache_.find(itemId);
|
||||||
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
||||||
}
|
}
|
||||||
|
uint64_t getBackpackItemGuid(int index) const {
|
||||||
|
if (index < 0 || index >= static_cast<int>(backpackSlotGuids_.size())) return 0;
|
||||||
|
return backpackSlotGuids_[index];
|
||||||
|
}
|
||||||
|
uint64_t getVendorGuid() const { return currentVendorItems.vendorGuid; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set callbacks
|
* Set callbacks
|
||||||
|
|
@ -464,6 +485,9 @@ private:
|
||||||
// ---- XP handler ----
|
// ---- XP handler ----
|
||||||
void handleXpGain(network::Packet& packet);
|
void handleXpGain(network::Packet& packet);
|
||||||
|
|
||||||
|
// ---- Creature movement handler ----
|
||||||
|
void handleMonsterMove(network::Packet& packet);
|
||||||
|
|
||||||
// ---- Phase 5 handlers ----
|
// ---- Phase 5 handlers ----
|
||||||
void handleLootResponse(network::Packet& packet);
|
void handleLootResponse(network::Packet& packet);
|
||||||
void handleLootReleaseResponse(network::Packet& packet);
|
void handleLootReleaseResponse(network::Packet& packet);
|
||||||
|
|
@ -580,6 +604,7 @@ private:
|
||||||
WorldEntryCallback worldEntryCallback_;
|
WorldEntryCallback worldEntryCallback_;
|
||||||
CreatureSpawnCallback creatureSpawnCallback_;
|
CreatureSpawnCallback creatureSpawnCallback_;
|
||||||
CreatureDespawnCallback creatureDespawnCallback_;
|
CreatureDespawnCallback creatureDespawnCallback_;
|
||||||
|
CreatureMoveCallback creatureMoveCallback_;
|
||||||
std::vector<uint32_t> knownSpells;
|
std::vector<uint32_t> knownSpells;
|
||||||
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
|
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
|
||||||
uint8_t castCount = 0;
|
uint8_t castCount = 0;
|
||||||
|
|
@ -616,6 +641,9 @@ private:
|
||||||
bool questDetailsOpen = false;
|
bool questDetailsOpen = false;
|
||||||
QuestDetailsData currentQuestDetails;
|
QuestDetailsData currentQuestDetails;
|
||||||
|
|
||||||
|
// Quest log
|
||||||
|
std::vector<QuestLogEntry> questLog_;
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
bool vendorWindowOpen = false;
|
bool vendorWindowOpen = false;
|
||||||
ListInventoryData currentVendorItems;
|
ListInventoryData currentVendorItems;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ enum class Opcode : uint16_t {
|
||||||
// ---- XP ----
|
// ---- XP ----
|
||||||
SMSG_LOG_XPGAIN = 0x1D0,
|
SMSG_LOG_XPGAIN = 0x1D0,
|
||||||
|
|
||||||
|
// ---- Creature Movement ----
|
||||||
|
SMSG_MONSTER_MOVE = 0x0DD,
|
||||||
|
|
||||||
// ---- Phase 2: Combat Core ----
|
// ---- Phase 2: Combat Core ----
|
||||||
CMSG_ATTACKSWING = 0x141,
|
CMSG_ATTACKSWING = 0x141,
|
||||||
CMSG_ATTACKSTOP = 0x142,
|
CMSG_ATTACKSTOP = 0x142,
|
||||||
|
|
@ -146,6 +149,7 @@ enum class Opcode : uint16_t {
|
||||||
SMSG_QUESTGIVER_OFFER_REWARD = 0x18D,
|
SMSG_QUESTGIVER_OFFER_REWARD = 0x18D,
|
||||||
CMSG_QUESTGIVER_CHOOSE_REWARD = 0x18E,
|
CMSG_QUESTGIVER_CHOOSE_REWARD = 0x18E,
|
||||||
SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191,
|
SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191,
|
||||||
|
CMSG_QUESTLOG_REMOVE_QUEST = 0x194,
|
||||||
|
|
||||||
// ---- Phase 5: Vendor ----
|
// ---- Phase 5: Vendor ----
|
||||||
CMSG_LIST_INVENTORY = 0x19E,
|
CMSG_LIST_INVENTORY = 0x19E,
|
||||||
|
|
|
||||||
|
|
@ -740,6 +740,7 @@ struct ItemQueryResponseData {
|
||||||
int32_t agility = 0;
|
int32_t agility = 0;
|
||||||
int32_t intellect = 0;
|
int32_t intellect = 0;
|
||||||
int32_t spirit = 0;
|
int32_t spirit = 0;
|
||||||
|
uint32_t sellPrice = 0;
|
||||||
std::string subclassName;
|
std::string subclassName;
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
};
|
};
|
||||||
|
|
@ -754,6 +755,25 @@ public:
|
||||||
// Phase 2: Combat Core
|
// Phase 2: Combat Core
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
/** SMSG_MONSTER_MOVE data */
|
||||||
|
struct MonsterMoveData {
|
||||||
|
uint64_t guid = 0;
|
||||||
|
float x = 0, y = 0, z = 0; // Current position (server coords)
|
||||||
|
uint8_t moveType = 0; // 0=Normal, 1=Stop, 2=FacingSpot, 3=FacingTarget, 4=FacingAngle
|
||||||
|
float facingAngle = 0;
|
||||||
|
uint64_t facingTarget = 0;
|
||||||
|
uint32_t splineFlags = 0;
|
||||||
|
uint32_t duration = 0;
|
||||||
|
// Destination (final point of the spline, server coords)
|
||||||
|
float destX = 0, destY = 0, destZ = 0;
|
||||||
|
bool hasDest = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MonsterMoveParser {
|
||||||
|
public:
|
||||||
|
static bool parse(network::Packet& packet, MonsterMoveData& data);
|
||||||
|
};
|
||||||
|
|
||||||
/** CMSG_ATTACKSWING packet builder */
|
/** CMSG_ATTACKSWING packet builder */
|
||||||
class AttackSwingPacket {
|
class AttackSwingPacket {
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,11 @@ private:
|
||||||
float introEndPitch = -5.0f;
|
float introEndPitch = -5.0f;
|
||||||
float introStartDistance = 12.0f;
|
float introStartDistance = 12.0f;
|
||||||
float introEndDistance = 10.0f;
|
float introEndDistance = 10.0f;
|
||||||
|
|
||||||
|
// Idle camera: triggers intro pan after IDLE_TIMEOUT seconds of no input
|
||||||
|
float idleTimer_ = 0.0f;
|
||||||
|
bool idleOrbit_ = false; // true when current intro pan is an idle orbit (loops)
|
||||||
|
static constexpr float IDLE_TIMEOUT = 120.0f; // 2 minutes
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ public:
|
||||||
|
|
||||||
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
||||||
void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation);
|
void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation);
|
||||||
|
void moveInstanceTo(uint32_t instanceId, const glm::vec3& destination, float durationSeconds);
|
||||||
|
void startFadeIn(uint32_t instanceId, float durationSeconds);
|
||||||
|
const pipeline::M2Model* getModelData(uint32_t modelId) const;
|
||||||
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
||||||
void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId);
|
void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId);
|
||||||
void setInstanceVisible(uint32_t instanceId, bool visible);
|
void setInstanceVisible(uint32_t instanceId, bool visible);
|
||||||
|
|
@ -130,6 +133,18 @@ private:
|
||||||
// Weapon attachments (weapons parented to this instance's bones)
|
// Weapon attachments (weapons parented to this instance's bones)
|
||||||
std::vector<WeaponAttachment> weaponAttachments;
|
std::vector<WeaponAttachment> weaponAttachments;
|
||||||
|
|
||||||
|
// Opacity (for fade-in)
|
||||||
|
float opacity = 1.0f;
|
||||||
|
float fadeInTime = 0.0f; // elapsed fade time (seconds)
|
||||||
|
float fadeInDuration = 0.0f; // total fade duration (0 = no fade)
|
||||||
|
|
||||||
|
// Movement interpolation
|
||||||
|
bool isMoving = false;
|
||||||
|
glm::vec3 moveStart{0.0f};
|
||||||
|
glm::vec3 moveEnd{0.0f};
|
||||||
|
float moveDuration = 0.0f; // seconds
|
||||||
|
float moveElapsed = 0.0f;
|
||||||
|
|
||||||
// Override model matrix (used for weapon instances positioned by parent bone)
|
// Override model matrix (used for weapon instances positioned by parent bone)
|
||||||
bool hasOverrideModelMatrix = false;
|
bool hasOverrideModelMatrix = false;
|
||||||
glm::mat4 overrideModelMatrix{1.0f};
|
glm::mat4 overrideModelMatrix{1.0f};
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,10 @@ public:
|
||||||
bool isMoving() const;
|
bool isMoving() const;
|
||||||
void triggerMeleeSwing();
|
void triggerMeleeSwing();
|
||||||
|
|
||||||
|
// Selection circle for targeted entity
|
||||||
|
void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color);
|
||||||
|
void clearSelectionCircle();
|
||||||
|
|
||||||
// CPU timing stats (milliseconds, last frame).
|
// CPU timing stats (milliseconds, last frame).
|
||||||
double getLastUpdateMs() const { return lastUpdateMs; }
|
double getLastUpdateMs() const { return lastUpdateMs; }
|
||||||
double getLastRenderMs() const { return lastRenderMs; }
|
double getLastRenderMs() const { return lastRenderMs; }
|
||||||
|
|
@ -224,6 +228,18 @@ private:
|
||||||
// Target facing
|
// Target facing
|
||||||
const glm::vec3* targetPosition = nullptr;
|
const glm::vec3* targetPosition = nullptr;
|
||||||
|
|
||||||
|
// Selection circle rendering
|
||||||
|
uint32_t selCircleVAO = 0;
|
||||||
|
uint32_t selCircleVBO = 0;
|
||||||
|
uint32_t selCircleShader = 0;
|
||||||
|
int selCircleVertCount = 0;
|
||||||
|
void initSelectionCircle();
|
||||||
|
void renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection);
|
||||||
|
glm::vec3 selCirclePos{0.0f};
|
||||||
|
glm::vec3 selCircleColor{1.0f, 0.0f, 0.0f};
|
||||||
|
float selCircleRadius = 1.5f;
|
||||||
|
bool selCircleVisible = false;
|
||||||
|
|
||||||
// Footstep event tracking (animation-driven)
|
// Footstep event tracking (animation-driven)
|
||||||
uint32_t footstepLastAnimationId = 0;
|
uint32_t footstepLastAnimationId = 0;
|
||||||
float footstepLastNormTime = 0.0f;
|
float footstepLastNormTime = 0.0f;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
#include "rendering/world_map.hpp"
|
#include "rendering/world_map.hpp"
|
||||||
#include "ui/inventory_screen.hpp"
|
#include "ui/inventory_screen.hpp"
|
||||||
|
#include "ui/quest_log_screen.hpp"
|
||||||
#include "ui/spellbook_screen.hpp"
|
#include "ui/spellbook_screen.hpp"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -143,6 +144,7 @@ private:
|
||||||
void renderWorldMap(game::GameHandler& gameHandler);
|
void renderWorldMap(game::GameHandler& gameHandler);
|
||||||
|
|
||||||
InventoryScreen inventoryScreen;
|
InventoryScreen inventoryScreen;
|
||||||
|
QuestLogScreen questLogScreen;
|
||||||
SpellbookScreen spellbookScreen;
|
SpellbookScreen spellbookScreen;
|
||||||
rendering::WorldMap worldMap;
|
rendering::WorldMap worldMap;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,36 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
|
#include "game/world_packets.hpp"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
namespace game { class GameHandler; }
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class InventoryScreen {
|
class InventoryScreen {
|
||||||
public:
|
public:
|
||||||
|
/// Render bags window (B key). Positioned at bottom of screen.
|
||||||
void render(game::Inventory& inventory, uint64_t moneyCopper);
|
void render(game::Inventory& inventory, uint64_t moneyCopper);
|
||||||
|
|
||||||
|
/// Render character screen (C key). Standalone equipment window.
|
||||||
|
void renderCharacterScreen(game::Inventory& inventory);
|
||||||
|
|
||||||
bool isOpen() const { return open; }
|
bool isOpen() const { return open; }
|
||||||
void toggle() { open = !open; }
|
void toggle() { open = !open; }
|
||||||
void setOpen(bool o) { open = o; }
|
void setOpen(bool o) { open = o; }
|
||||||
|
|
||||||
|
bool isCharacterOpen() const { return characterOpen; }
|
||||||
|
void toggleCharacter() { characterOpen = !characterOpen; }
|
||||||
|
void setCharacterOpen(bool o) { characterOpen = o; }
|
||||||
|
|
||||||
|
/// Enable vendor mode: right-clicking bag items sells them.
|
||||||
|
void setVendorMode(bool enabled, game::GameHandler* handler) {
|
||||||
|
vendorMode_ = enabled;
|
||||||
|
gameHandler_ = handler;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if equipment changed since last call, and clears the flag.
|
/// Returns true if equipment changed since last call, and clears the flag.
|
||||||
bool consumeEquipmentDirty() { bool d = equipmentDirty; equipmentDirty = false; return d; }
|
bool consumeEquipmentDirty() { bool d = equipmentDirty; equipmentDirty = false; return d; }
|
||||||
/// Returns true if any inventory slot changed since last call, and clears the flag.
|
/// Returns true if any inventory slot changed since last call, and clears the flag.
|
||||||
|
|
@ -20,10 +38,16 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool open = false;
|
bool open = false;
|
||||||
|
bool characterOpen = false;
|
||||||
bool bKeyWasDown = false;
|
bool bKeyWasDown = false;
|
||||||
|
bool cKeyWasDown = false;
|
||||||
bool equipmentDirty = false;
|
bool equipmentDirty = false;
|
||||||
bool inventoryDirty = false;
|
bool inventoryDirty = false;
|
||||||
|
|
||||||
|
// Vendor sell mode
|
||||||
|
bool vendorMode_ = false;
|
||||||
|
game::GameHandler* gameHandler_ = nullptr;
|
||||||
|
|
||||||
// Drag-and-drop held item state
|
// Drag-and-drop held item state
|
||||||
bool holdingItem = false;
|
bool holdingItem = false;
|
||||||
game::ItemDef heldItem;
|
game::ItemDef heldItem;
|
||||||
|
|
|
||||||
21
include/ui/quest_log_screen.hpp
Normal file
21
include/ui/quest_log_screen.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "game/game_handler.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace wowee { namespace ui {
|
||||||
|
|
||||||
|
class QuestLogScreen {
|
||||||
|
public:
|
||||||
|
void render(game::GameHandler& gameHandler);
|
||||||
|
bool isOpen() const { return open; }
|
||||||
|
void toggle() { open = !open; }
|
||||||
|
void setOpen(bool o) { open = o; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool open = false;
|
||||||
|
bool lKeyWasDown = false;
|
||||||
|
int selectedIndex = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace wowee::ui
|
||||||
|
|
@ -444,6 +444,8 @@ void Application::update(float deltaTime) {
|
||||||
if (!npcsSpawned && singlePlayerMode) {
|
if (!npcsSpawned && singlePlayerMode) {
|
||||||
spawnNpcs();
|
spawnNpcs();
|
||||||
}
|
}
|
||||||
|
// Process deferred online creature spawns (throttled)
|
||||||
|
processCreatureSpawnQueue();
|
||||||
if (npcManager && renderer && renderer->getCharacterRenderer()) {
|
if (npcManager && renderer && renderer->getCharacterRenderer()) {
|
||||||
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
||||||
}
|
}
|
||||||
|
|
@ -626,7 +628,8 @@ void Application::setupUICallbacks() {
|
||||||
|
|
||||||
// Creature spawn callback (online mode) - spawn creature models
|
// Creature spawn callback (online mode) - spawn creature models
|
||||||
gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||||
spawnOnlineCreature(guid, displayId, x, y, z, orientation);
|
// Queue spawns to avoid hanging when many creatures appear at once
|
||||||
|
pendingCreatureSpawns_.push_back({guid, displayId, x, y, z, orientation});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Creature despawn callback (online mode) - remove creature models
|
// Creature despawn callback (online mode) - remove creature models
|
||||||
|
|
@ -634,6 +637,32 @@ void Application::setupUICallbacks() {
|
||||||
despawnOnlineCreature(guid);
|
despawnOnlineCreature(guid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Creature move callback (online mode) - update creature positions
|
||||||
|
gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
||||||
|
auto it = creatureInstances_.find(guid);
|
||||||
|
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
||||||
|
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||||
|
float durationSec = static_cast<float>(durationMs) / 1000.0f;
|
||||||
|
renderer->getCharacterRenderer()->moveInstanceTo(it->second, renderPos, durationSec);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NPC death callback (online mode) - play death animation
|
||||||
|
gameHandler->setNpcDeathCallback([this](uint64_t guid) {
|
||||||
|
auto it = creatureInstances_.find(guid);
|
||||||
|
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
||||||
|
renderer->getCharacterRenderer()->playAnimation(it->second, 1, false); // Death
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NPC swing callback (online mode) - play attack animation
|
||||||
|
gameHandler->setNpcSwingCallback([this](uint64_t guid) {
|
||||||
|
auto it = creatureInstances_.find(guid);
|
||||||
|
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
||||||
|
renderer->getCharacterRenderer()->playAnimation(it->second, 16, false); // Attack
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// "Create Character" button on character screen
|
// "Create Character" button on character screen
|
||||||
uiManager->getCharacterScreen().setOnCreateCharacter([this]() {
|
uiManager->getCharacterScreen().setOnCreateCharacter([this]() {
|
||||||
uiManager->getCharacterCreateScreen().reset();
|
uiManager->getCharacterCreateScreen().reset();
|
||||||
|
|
@ -1415,6 +1444,7 @@ void Application::startSinglePlayer() {
|
||||||
// snap the third-person camera into the correct orbit position.
|
// snap the third-person camera into the correct orbit position.
|
||||||
if (spawnSnapToGround && renderer && renderer->getCameraController()) {
|
if (spawnSnapToGround && renderer && renderer->getCameraController()) {
|
||||||
renderer->getCameraController()->reset();
|
renderer->getCameraController()->reset();
|
||||||
|
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingScreenOk) {
|
if (loadingScreenOk) {
|
||||||
|
|
@ -1426,6 +1456,7 @@ void Application::startSinglePlayer() {
|
||||||
auto* camCtrl = renderer->getCameraController();
|
auto* camCtrl = renderer->getCameraController();
|
||||||
gameHandler->setHearthstoneCallback([camCtrl]() {
|
gameHandler->setHearthstoneCallback([camCtrl]() {
|
||||||
camCtrl->reset();
|
camCtrl->reset();
|
||||||
|
camCtrl->startIntroPan(2.8f, 140.0f);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1549,6 +1580,7 @@ void Application::teleportTo(int presetIndex) {
|
||||||
// Floor-snapping presets use camera reset. WMO-floor presets keep explicit Z.
|
// Floor-snapping presets use camera reset. WMO-floor presets keep explicit Z.
|
||||||
if (spawnSnapToGround && renderer && renderer->getCameraController()) {
|
if (spawnSnapToGround && renderer && renderer->getCameraController()) {
|
||||||
renderer->getCameraController()->reset();
|
renderer->getCameraController()->reset();
|
||||||
|
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!spawnSnapToGround && renderer) {
|
if (!spawnSnapToGround && renderer) {
|
||||||
|
|
@ -1592,6 +1624,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
if (renderer->getCameraController()) {
|
if (renderer->getCameraController()) {
|
||||||
renderer->getCameraController()->setDefaultSpawn(spawnRender, 0.0f, 15.0f);
|
renderer->getCameraController()->setDefaultSpawn(spawnRender, 0.0f, 15.0f);
|
||||||
renderer->getCameraController()->reset();
|
renderer->getCameraController()->reset();
|
||||||
|
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set map name for WMO renderer
|
// Set map name for WMO renderer
|
||||||
|
|
@ -1802,50 +1835,61 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
|
|
||||||
auto* charRenderer = renderer->getCharacterRenderer();
|
auto* charRenderer = renderer->getCharacterRenderer();
|
||||||
|
|
||||||
// Load model if not already loaded for this displayId
|
// Check model cache - reuse if same displayId was already loaded
|
||||||
uint32_t modelId = nextCreatureModelId_++;
|
uint32_t modelId = 0;
|
||||||
|
bool modelCached = false;
|
||||||
|
auto cacheIt = displayIdModelCache_.find(displayId);
|
||||||
|
if (cacheIt != displayIdModelCache_.end()) {
|
||||||
|
modelId = cacheIt->second;
|
||||||
|
modelCached = true;
|
||||||
|
} else {
|
||||||
|
// Load model from disk (only once per displayId)
|
||||||
|
modelId = nextCreatureModelId_++;
|
||||||
|
|
||||||
auto m2Data = assetManager->readFile(m2Path);
|
auto m2Data = assetManager->readFile(m2Path);
|
||||||
if (m2Data.empty()) {
|
if (m2Data.empty()) {
|
||||||
LOG_WARNING("Failed to read creature M2: ", m2Path);
|
LOG_WARNING("Failed to read creature M2: ", m2Path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||||
if (model.vertices.empty()) {
|
if (model.vertices.empty()) {
|
||||||
LOG_WARNING("Failed to parse creature M2: ", m2Path);
|
LOG_WARNING("Failed to parse creature M2: ", m2Path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load skin file
|
// Load skin file
|
||||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||||
auto skinData = assetManager->readFile(skinPath);
|
auto skinData = assetManager->readFile(skinPath);
|
||||||
if (!skinData.empty()) {
|
if (!skinData.empty()) {
|
||||||
pipeline::M2Loader::loadSkin(skinData, model);
|
pipeline::M2Loader::loadSkin(skinData, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load external .anim files for sequences without flag 0x20
|
// Load external .anim files for sequences without flag 0x20
|
||||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||||
if (!(model.sequences[si].flags & 0x20)) {
|
if (!(model.sequences[si].flags & 0x20)) {
|
||||||
char animFileName[256];
|
char animFileName[256];
|
||||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||||
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
||||||
auto animData = assetManager->readFile(animFileName);
|
auto animData = assetManager->readFile(animFileName);
|
||||||
if (!animData.empty()) {
|
if (!animData.empty()) {
|
||||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!charRenderer->loadModel(model, modelId)) {
|
||||||
|
LOG_WARNING("Failed to load creature model: ", m2Path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayIdModelCache_[displayId] = modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!charRenderer->loadModel(model, modelId)) {
|
// Apply skin textures from CreatureDisplayInfo.dbc (only for newly loaded models)
|
||||||
LOG_WARNING("Failed to load creature model: ", m2Path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply skin textures from CreatureDisplayInfo.dbc
|
|
||||||
auto itDisplayData = displayDataMap_.find(displayId);
|
auto itDisplayData = displayDataMap_.find(displayId);
|
||||||
if (itDisplayData != displayDataMap_.end()) {
|
if (!modelCached && itDisplayData != displayDataMap_.end()) {
|
||||||
const auto& dispData = itDisplayData->second;
|
const auto& dispData = itDisplayData->second;
|
||||||
|
|
||||||
// Get model directory for texture path construction
|
// Get model directory for texture path construction
|
||||||
|
|
@ -1858,9 +1902,17 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
LOG_DEBUG("DisplayId ", displayId, " skins: '", dispData.skin1, "', '", dispData.skin2, "', '", dispData.skin3,
|
LOG_DEBUG("DisplayId ", displayId, " skins: '", dispData.skin1, "', '", dispData.skin2, "', '", dispData.skin3,
|
||||||
"' extraDisplayId=", dispData.extraDisplayId);
|
"' extraDisplayId=", dispData.extraDisplayId);
|
||||||
|
|
||||||
|
// Get model data from CharacterRenderer for texture iteration
|
||||||
|
const auto* modelData = charRenderer->getModelData(modelId);
|
||||||
|
if (!modelData) {
|
||||||
|
LOG_WARNING("Model data not found for modelId ", modelId);
|
||||||
|
}
|
||||||
|
|
||||||
// Log texture types in the model
|
// Log texture types in the model
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
if (modelData) {
|
||||||
LOG_DEBUG(" Model texture ", ti, ": type=", model.textures[ti].type, " filename='", model.textures[ti].filename, "'");
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
|
LOG_DEBUG(" Model texture ", ti, ": type=", modelData->textures[ti].type, " filename='", modelData->textures[ti].filename, "'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a humanoid NPC with extra display info
|
// Check if this is a humanoid NPC with extra display info
|
||||||
|
|
@ -1885,9 +1937,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
|
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
|
||||||
GLuint finalTex = charRenderer->loadTexture(bakePath);
|
GLuint finalTex = charRenderer->loadTexture(bakePath);
|
||||||
|
|
||||||
if (finalTex != 0) {
|
if (finalTex != 0 && modelData) {
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
uint32_t texType = model.textures[ti].type;
|
uint32_t texType = modelData->textures[ti].type;
|
||||||
if (texType == 1 || texType == 2) {
|
if (texType == 1 || texType == 2) {
|
||||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), finalTex);
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), finalTex);
|
||||||
LOG_DEBUG("Applied baked NPC texture to slot ", ti, " (type ", texType, "): ", bakePath);
|
LOG_DEBUG("Applied baked NPC texture to slot ", ti, " (type ", texType, "): ", bakePath);
|
||||||
|
|
@ -1926,9 +1978,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
|
|
||||||
if (!hairTexPath.empty()) {
|
if (!hairTexPath.empty()) {
|
||||||
GLuint hairTex = charRenderer->loadTexture(hairTexPath);
|
GLuint hairTex = charRenderer->loadTexture(hairTexPath);
|
||||||
if (hairTex != 0) {
|
if (hairTex != 0 && modelData) {
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
if (model.textures[ti].type == 6) {
|
if (modelData->textures[ti].type == 6) {
|
||||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), hairTex);
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), hairTex);
|
||||||
LOG_DEBUG("Applied hair texture to slot ", ti, ": ", hairTexPath);
|
LOG_DEBUG("Applied hair texture to slot ", ti, ": ", hairTexPath);
|
||||||
}
|
}
|
||||||
|
|
@ -1942,9 +1994,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply creature skin textures (for non-humanoid creatures)
|
// Apply creature skin textures (for non-humanoid creatures)
|
||||||
if (!hasHumanoidTexture) {
|
if (!hasHumanoidTexture && modelData) {
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
const auto& tex = model.textures[ti];
|
const auto& tex = modelData->textures[ti];
|
||||||
std::string skinPath;
|
std::string skinPath;
|
||||||
|
|
||||||
// Creature skin types: 11 = skin1, 12 = skin2, 13 = skin3
|
// Creature skin types: 11 = skin1, 12 = skin2, 13 = skin3
|
||||||
|
|
@ -2113,9 +2165,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log model's actual submesh IDs for debugging geoset mismatches
|
// Log model's actual submesh IDs for debugging geoset mismatches
|
||||||
{
|
if (auto* md = charRenderer->getModelData(modelId)) {
|
||||||
std::string batchIds;
|
std::string batchIds;
|
||||||
for (const auto& b : model.batches) {
|
for (const auto& b : md->batches) {
|
||||||
if (!batchIds.empty()) batchIds += ",";
|
if (!batchIds.empty()) batchIds += ",";
|
||||||
batchIds += std::to_string(b.submeshId);
|
batchIds += std::to_string(b.submeshId);
|
||||||
}
|
}
|
||||||
|
|
@ -2210,8 +2262,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play idle animation
|
// Play idle animation and fade in
|
||||||
charRenderer->playAnimation(instanceId, 0, true);
|
charRenderer->playAnimation(instanceId, 0, true);
|
||||||
|
charRenderer->startFadeIn(instanceId, 0.5f);
|
||||||
|
|
||||||
// Track instance
|
// Track instance
|
||||||
creatureInstances_[guid] = instanceId;
|
creatureInstances_[guid] = instanceId;
|
||||||
|
|
@ -2221,6 +2274,18 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::processCreatureSpawnQueue() {
|
||||||
|
if (pendingCreatureSpawns_.empty()) return;
|
||||||
|
|
||||||
|
int spawned = 0;
|
||||||
|
while (!pendingCreatureSpawns_.empty() && spawned < MAX_SPAWNS_PER_FRAME) {
|
||||||
|
auto& s = pendingCreatureSpawns_.front();
|
||||||
|
spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||||
|
pendingCreatureSpawns_.erase(pendingCreatureSpawns_.begin());
|
||||||
|
spawned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Application::despawnOnlineCreature(uint64_t guid) {
|
void Application::despawnOnlineCreature(uint64_t guid) {
|
||||||
auto it = creatureInstances_.find(guid);
|
auto it = creatureInstances_.find(guid);
|
||||||
if (it == creatureInstances_.end()) return;
|
if (it == creatureInstances_.end()) return;
|
||||||
|
|
|
||||||
|
|
@ -840,6 +840,28 @@ void GameHandler::update(float deltaTime) {
|
||||||
updateLocalCombat(deltaTime);
|
updateLocalCombat(deltaTime);
|
||||||
updateNpcAggro(deltaTime);
|
updateNpcAggro(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Online mode: maintain auto-attack by periodically re-sending CMSG_ATTACKSWING
|
||||||
|
if (!singlePlayerMode_ && autoAttacking && autoAttackTarget != 0 && socket) {
|
||||||
|
auto target = entityManager.getEntity(autoAttackTarget);
|
||||||
|
if (!target) {
|
||||||
|
// Target gone
|
||||||
|
stopAutoAttack();
|
||||||
|
} else if (target->getType() == ObjectType::UNIT) {
|
||||||
|
auto unit = std::static_pointer_cast<Unit>(target);
|
||||||
|
if (unit->getHealth() == 0) {
|
||||||
|
stopAutoAttack();
|
||||||
|
} else {
|
||||||
|
// Re-send attack swing every 2 seconds to keep server combat alive
|
||||||
|
swingTimer_ += deltaTime;
|
||||||
|
if (swingTimer_ >= 2.0f) {
|
||||||
|
auto pkt = AttackSwingPacket::build(autoAttackTarget);
|
||||||
|
socket->send(pkt);
|
||||||
|
swingTimer_ = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (singlePlayerMode_) {
|
if (singlePlayerMode_) {
|
||||||
|
|
@ -982,6 +1004,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
handleXpGain(packet);
|
handleXpGain(packet);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ---- Creature Movement ----
|
||||||
|
case Opcode::SMSG_MONSTER_MOVE:
|
||||||
|
handleMonsterMove(packet);
|
||||||
|
break;
|
||||||
|
|
||||||
// ---- Phase 2: Combat ----
|
// ---- Phase 2: Combat ----
|
||||||
case Opcode::SMSG_ATTACKSTART:
|
case Opcode::SMSG_ATTACKSTART:
|
||||||
handleAttackStart(packet);
|
handleAttackStart(packet);
|
||||||
|
|
@ -1082,7 +1109,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_PERIODICAURALOG:
|
case Opcode::SMSG_PERIODICAURALOG:
|
||||||
case Opcode::SMSG_SPELLENERGIZELOG:
|
case Opcode::SMSG_SPELLENERGIZELOG:
|
||||||
case Opcode::SMSG_ENVIRONMENTALDAMAGELOG:
|
case Opcode::SMSG_ENVIRONMENTALDAMAGELOG:
|
||||||
case Opcode::SMSG_LOOT_MONEY_NOTIFY:
|
case Opcode::SMSG_LOOT_MONEY_NOTIFY: {
|
||||||
|
// uint32 money + uint8 soleLooter
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||||
|
uint32_t amount = packet.readUInt32();
|
||||||
|
playerMoneyCopper_ += amount;
|
||||||
|
LOG_INFO("Looted ", amount, " copper (total: ", playerMoneyCopper_, ")");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Opcode::SMSG_LOOT_CLEAR_MONEY:
|
case Opcode::SMSG_LOOT_CLEAR_MONEY:
|
||||||
case Opcode::SMSG_NPC_TEXT_UPDATE:
|
case Opcode::SMSG_NPC_TEXT_UPDATE:
|
||||||
case Opcode::SMSG_SELL_ITEM:
|
case Opcode::SMSG_SELL_ITEM:
|
||||||
|
|
@ -1091,12 +1126,26 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
||||||
case Opcode::MSG_RAID_TARGET_UPDATE:
|
case Opcode::MSG_RAID_TARGET_UPDATE:
|
||||||
case Opcode::SMSG_QUESTGIVER_STATUS:
|
case Opcode::SMSG_QUESTGIVER_STATUS:
|
||||||
|
LOG_DEBUG("Ignoring SMSG_QUESTGIVER_STATUS");
|
||||||
|
break;
|
||||||
case Opcode::SMSG_QUESTGIVER_QUEST_DETAILS:
|
case Opcode::SMSG_QUESTGIVER_QUEST_DETAILS:
|
||||||
handleQuestDetails(packet);
|
handleQuestDetails(packet);
|
||||||
break;
|
break;
|
||||||
|
case Opcode::SMSG_QUESTGIVER_QUEST_COMPLETE: {
|
||||||
|
// Mark quest as complete in local log
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||||
|
uint32_t questId = packet.readUInt32();
|
||||||
|
for (auto& q : questLog_) {
|
||||||
|
if (q.questId == questId) {
|
||||||
|
q.complete = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS:
|
case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS:
|
||||||
case Opcode::SMSG_QUESTGIVER_OFFER_REWARD:
|
case Opcode::SMSG_QUESTGIVER_OFFER_REWARD:
|
||||||
case Opcode::SMSG_QUESTGIVER_QUEST_COMPLETE:
|
|
||||||
case Opcode::SMSG_GROUP_SET_LEADER:
|
case Opcode::SMSG_GROUP_SET_LEADER:
|
||||||
LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec);
|
LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec);
|
||||||
break;
|
break;
|
||||||
|
|
@ -2478,7 +2527,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
for (const auto& [key, val] : block.fields) {
|
for (const auto& [key, val] : block.fields) {
|
||||||
if (key == 634) { playerXp_ = val; } // PLAYER_XP
|
if (key == 634) { playerXp_ = val; } // PLAYER_XP
|
||||||
else if (key == 635) { playerNextLevelXp_ = val; } // PLAYER_NEXT_LEVEL_XP
|
else if (key == 635) { playerNextLevelXp_ = val; } // PLAYER_NEXT_LEVEL_XP
|
||||||
else if (key == 54) { serverPlayerLevel_ = val; } // UNIT_FIELD_LEVEL
|
else if (key == 54) {
|
||||||
|
serverPlayerLevel_ = val; // UNIT_FIELD_LEVEL
|
||||||
|
for (auto& ch : characters) {
|
||||||
|
if (ch.guid == playerGuid) { ch.level = val; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (key == 632) { playerMoneyCopper_ = val; } // PLAYER_FIELD_COINAGE
|
else if (key == 632) { playerMoneyCopper_ = val; } // PLAYER_FIELD_COINAGE
|
||||||
else if (key >= 322 && key <= 367) {
|
else if (key >= 322 && key <= 367) {
|
||||||
// PLAYER_FIELD_INV_SLOT_HEAD: equipment slots (23 slots × 2 fields)
|
// PLAYER_FIELD_INV_SLOT_HEAD: equipment slots (23 slots × 2 fields)
|
||||||
|
|
@ -2522,8 +2576,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 24:
|
case 24:
|
||||||
unit->setHealth(val);
|
unit->setHealth(val);
|
||||||
if (val == 0 && block.guid == autoAttackTarget) {
|
if (val == 0) {
|
||||||
stopAutoAttack();
|
if (block.guid == autoAttackTarget) {
|
||||||
|
stopAutoAttack();
|
||||||
|
}
|
||||||
|
// Trigger death animation for NPC units
|
||||||
|
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
|
||||||
|
npcDeathCallback_(block.guid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 25: unit->setPower(val); break;
|
case 25: unit->setPower(val); break;
|
||||||
|
|
@ -2540,10 +2600,29 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (block.guid == playerGuid) {
|
if (block.guid == playerGuid) {
|
||||||
bool slotsChanged = false;
|
bool slotsChanged = false;
|
||||||
for (const auto& [key, val] : block.fields) {
|
for (const auto& [key, val] : block.fields) {
|
||||||
if (key == 634) { playerXp_ = val; }
|
if (key == 634) {
|
||||||
else if (key == 635) { playerNextLevelXp_ = val; }
|
playerXp_ = val;
|
||||||
else if (key == 54) { serverPlayerLevel_ = val; }
|
LOG_INFO("XP updated: ", val);
|
||||||
else if (key == 632) { playerMoneyCopper_ = val; }
|
}
|
||||||
|
else if (key == 635) {
|
||||||
|
playerNextLevelXp_ = val;
|
||||||
|
LOG_INFO("Next level XP updated: ", val);
|
||||||
|
}
|
||||||
|
else if (key == 54) {
|
||||||
|
serverPlayerLevel_ = val;
|
||||||
|
LOG_INFO("Level updated: ", val);
|
||||||
|
// Update Character struct for character selection screen
|
||||||
|
for (auto& ch : characters) {
|
||||||
|
if (ch.guid == playerGuid) {
|
||||||
|
ch.level = val;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (key == 632) {
|
||||||
|
playerMoneyCopper_ = val;
|
||||||
|
LOG_INFO("Money updated via VALUES: ", val, " copper");
|
||||||
|
}
|
||||||
else if (key >= 322 && key <= 367) {
|
else if (key >= 322 && key <= 367) {
|
||||||
int slotIndex = (key - 322) / 2;
|
int slotIndex = (key - 322) / 2;
|
||||||
bool isLow = ((key - 322) % 2 == 0);
|
bool isLow = ((key - 322) % 2 == 0);
|
||||||
|
|
@ -2795,6 +2874,7 @@ void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
||||||
for (const auto& [guid, entity] : entityManager.getEntities()) {
|
for (const auto& [guid, entity] : entityManager.getEntities()) {
|
||||||
auto t = entity->getType();
|
auto t = entity->getType();
|
||||||
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
|
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
|
||||||
|
if (guid == playerGuid) continue; // Don't tab-target self
|
||||||
float dx = entity->getX() - playerX;
|
float dx = entity->getX() - playerX;
|
||||||
float dy = entity->getY() - playerY;
|
float dy = entity->getY() - playerY;
|
||||||
float dz = entity->getZ() - playerZ;
|
float dz = entity->getZ() - playerZ;
|
||||||
|
|
@ -3063,9 +3143,75 @@ void GameHandler::handleAttackStop(network::Packet& packet) {
|
||||||
AttackStopData data;
|
AttackStopData data;
|
||||||
if (!AttackStopParser::parse(packet, data)) return;
|
if (!AttackStopParser::parse(packet, data)) return;
|
||||||
|
|
||||||
|
// Don't clear autoAttacking on SMSG_ATTACKSTOP - the server sends this
|
||||||
|
// when the attack loop pauses (out of range, etc). The player's intent
|
||||||
|
// to attack persists until target dies or player explicitly cancels.
|
||||||
|
// We'll re-send CMSG_ATTACKSWING periodically in the update loop.
|
||||||
if (data.attackerGuid == playerGuid) {
|
if (data.attackerGuid == playerGuid) {
|
||||||
autoAttacking = false;
|
LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)");
|
||||||
autoAttackTarget = 0;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||||
|
MonsterMoveData data;
|
||||||
|
if (!MonsterMoveParser::parse(packet, data)) {
|
||||||
|
LOG_WARNING("Failed to parse SMSG_MONSTER_MOVE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update entity position in entity manager
|
||||||
|
auto entity = entityManager.getEntity(data.guid);
|
||||||
|
if (entity) {
|
||||||
|
if (data.hasDest) {
|
||||||
|
// Convert destination from server to canonical coords
|
||||||
|
glm::vec3 destCanonical = core::coords::serverToCanonical(
|
||||||
|
glm::vec3(data.destX, data.destY, data.destZ));
|
||||||
|
|
||||||
|
// Calculate facing angle
|
||||||
|
float orientation = entity->getOrientation();
|
||||||
|
if (data.moveType == 4) {
|
||||||
|
// FacingAngle - server specifies exact angle
|
||||||
|
orientation = data.facingAngle;
|
||||||
|
} else if (data.moveType == 3) {
|
||||||
|
// FacingTarget - face toward the target entity
|
||||||
|
auto target = entityManager.getEntity(data.facingTarget);
|
||||||
|
if (target) {
|
||||||
|
float dx = target->getX() - entity->getX();
|
||||||
|
float dy = target->getY() - entity->getY();
|
||||||
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||||
|
orientation = std::atan2(dy, dx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal move - face toward destination
|
||||||
|
float dx = destCanonical.x - entity->getX();
|
||||||
|
float dy = destCanonical.y - entity->getY();
|
||||||
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||||
|
orientation = std::atan2(dy, dx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set entity to destination for targeting/logic; renderer interpolates visually
|
||||||
|
entity->setPosition(destCanonical.x, destCanonical.y, destCanonical.z, orientation);
|
||||||
|
|
||||||
|
// Notify renderer to smoothly move the creature
|
||||||
|
if (creatureMoveCallback_) {
|
||||||
|
creatureMoveCallback_(data.guid,
|
||||||
|
destCanonical.x, destCanonical.y, destCanonical.z,
|
||||||
|
data.duration);
|
||||||
|
}
|
||||||
|
} else if (data.moveType == 1) {
|
||||||
|
// Stop at current position
|
||||||
|
glm::vec3 posCanonical = core::coords::serverToCanonical(
|
||||||
|
glm::vec3(data.x, data.y, data.z));
|
||||||
|
entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z,
|
||||||
|
entity->getOrientation());
|
||||||
|
|
||||||
|
if (creatureMoveCallback_) {
|
||||||
|
creatureMoveCallback_(data.guid,
|
||||||
|
posCanonical.x, posCanonical.y, posCanonical.z, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3556,6 +3702,20 @@ void GameHandler::acceptQuest() {
|
||||||
auto packet = QuestgiverAcceptQuestPacket::build(
|
auto packet = QuestgiverAcceptQuestPacket::build(
|
||||||
currentQuestDetails.npcGuid, currentQuestDetails.questId);
|
currentQuestDetails.npcGuid, currentQuestDetails.questId);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
|
|
||||||
|
// Add to quest log
|
||||||
|
bool alreadyInLog = false;
|
||||||
|
for (const auto& q : questLog_) {
|
||||||
|
if (q.questId == currentQuestDetails.questId) { alreadyInLog = true; break; }
|
||||||
|
}
|
||||||
|
if (!alreadyInLog) {
|
||||||
|
QuestLogEntry entry;
|
||||||
|
entry.questId = currentQuestDetails.questId;
|
||||||
|
entry.title = currentQuestDetails.title;
|
||||||
|
entry.objectives = currentQuestDetails.objectives;
|
||||||
|
questLog_.push_back(entry);
|
||||||
|
}
|
||||||
|
|
||||||
questDetailsOpen = false;
|
questDetailsOpen = false;
|
||||||
currentQuestDetails = QuestDetailsData{};
|
currentQuestDetails = QuestDetailsData{};
|
||||||
}
|
}
|
||||||
|
|
@ -3565,6 +3725,23 @@ void GameHandler::declineQuest() {
|
||||||
currentQuestDetails = QuestDetailsData{};
|
currentQuestDetails = QuestDetailsData{};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::abandonQuest(uint32_t questId) {
|
||||||
|
// Find the quest's index in our local log
|
||||||
|
for (size_t i = 0; i < questLog_.size(); i++) {
|
||||||
|
if (questLog_[i].questId == questId) {
|
||||||
|
// Tell server to remove it (slot index in server quest log)
|
||||||
|
// We send the local index; server maps it via PLAYER_QUEST_LOG fields
|
||||||
|
if (state == WorldState::IN_WORLD && socket) {
|
||||||
|
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_QUESTLOG_REMOVE_QUEST));
|
||||||
|
pkt.writeUInt8(static_cast<uint8_t>(i));
|
||||||
|
socket->send(pkt);
|
||||||
|
}
|
||||||
|
questLog_.erase(questLog_.begin() + static_cast<ptrdiff_t>(i));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::closeGossip() {
|
void GameHandler::closeGossip() {
|
||||||
gossipWindowOpen = false;
|
gossipWindowOpen = false;
|
||||||
currentGossip = GossipMessageData{};
|
currentGossip = GossipMessageData{};
|
||||||
|
|
@ -3593,6 +3770,28 @@ void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::sellItemBySlot(int backpackIndex) {
|
||||||
|
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return;
|
||||||
|
const auto& slot = inventory.getBackpackSlot(backpackIndex);
|
||||||
|
if (slot.empty()) return;
|
||||||
|
|
||||||
|
if (singlePlayerMode_) {
|
||||||
|
auto it = itemInfoCache_.find(slot.item.itemId);
|
||||||
|
if (it != itemInfoCache_.end() && it->second.sellPrice > 0) {
|
||||||
|
addMoneyCopper(it->second.sellPrice);
|
||||||
|
std::string msg = "You sold " + slot.item.name + ".";
|
||||||
|
addSystemChatMessage(msg);
|
||||||
|
}
|
||||||
|
inventory.clearBackpackSlot(backpackIndex);
|
||||||
|
notifyInventoryChanged();
|
||||||
|
} else {
|
||||||
|
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
|
||||||
|
if (itemGuid != 0 && currentVendorItems.vendorGuid != 0) {
|
||||||
|
sellItem(currentVendorItems.vendorGuid, itemGuid, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::handleLootResponse(network::Packet& packet) {
|
void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||||
if (!LootResponseParser::parse(packet, currentLoot)) return;
|
if (!LootResponseParser::parse(packet, currentLoot)) return;
|
||||||
lootWindowOpen = true;
|
lootWindowOpen = true;
|
||||||
|
|
|
||||||
|
|
@ -575,11 +575,23 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
|
||||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
||||||
}
|
}
|
||||||
|
|
||||||
static int mvLog = 10;
|
// Detailed hex dump for debugging
|
||||||
|
static int mvLog = 5;
|
||||||
if (mvLog-- > 0) {
|
if (mvLog-- > 0) {
|
||||||
LOG_INFO("Movement pkt: opcode=0x", std::hex, static_cast<uint16_t>(opcode), std::dec,
|
const auto& raw = packet.getData();
|
||||||
" size=", packet.getSize(), " flags=0x", std::hex, info.flags, std::dec,
|
std::string hex;
|
||||||
" pos=(", info.x, ",", info.y, ",", info.z, ")");
|
for (size_t i = 0; i < raw.size(); i++) {
|
||||||
|
char b[4]; snprintf(b, sizeof(b), "%02x ", raw[i]);
|
||||||
|
hex += b;
|
||||||
|
}
|
||||||
|
LOG_INFO("MOVEPKT opcode=0x", std::hex, static_cast<uint16_t>(opcode), std::dec,
|
||||||
|
" guid=0x", std::hex, playerGuid, std::dec,
|
||||||
|
" payload=", raw.size(), " bytes",
|
||||||
|
" flags=0x", std::hex, info.flags, std::dec,
|
||||||
|
" flags2=0x", std::hex, info.flags2, std::dec,
|
||||||
|
" pos=(", info.x, ",", info.y, ",", info.z, ",", info.orientation, ")",
|
||||||
|
" fallTime=", info.fallTime);
|
||||||
|
LOG_INFO("MOVEPKT hex: ", hex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return packet;
|
return packet;
|
||||||
|
|
@ -1288,7 +1300,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
||||||
packet.readUInt32(); // Flags
|
packet.readUInt32(); // Flags
|
||||||
packet.readUInt32(); // Flags2
|
packet.readUInt32(); // Flags2
|
||||||
packet.readUInt32(); // BuyPrice
|
packet.readUInt32(); // BuyPrice
|
||||||
packet.readUInt32(); // SellPrice
|
data.sellPrice = packet.readUInt32(); // SellPrice
|
||||||
|
|
||||||
data.inventoryType = packet.readUInt32();
|
data.inventoryType = packet.readUInt32();
|
||||||
|
|
||||||
|
|
@ -1339,6 +1351,118 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Creature Movement
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
|
||||||
|
// PackedGuid
|
||||||
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (data.guid == 0) return false;
|
||||||
|
|
||||||
|
// uint8 unk (toggle for MOVEMENTFLAG2_UNK7)
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||||
|
packet.readUInt8();
|
||||||
|
|
||||||
|
// Current position (server coords: float x, y, z)
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
||||||
|
data.x = packet.readFloat();
|
||||||
|
data.y = packet.readFloat();
|
||||||
|
data.z = packet.readFloat();
|
||||||
|
|
||||||
|
// uint32 splineId
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
packet.readUInt32();
|
||||||
|
|
||||||
|
// uint8 moveType
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||||
|
data.moveType = packet.readUInt8();
|
||||||
|
|
||||||
|
if (data.moveType == 1) {
|
||||||
|
// Stop - no more required data
|
||||||
|
data.destX = data.x;
|
||||||
|
data.destY = data.y;
|
||||||
|
data.destZ = data.z;
|
||||||
|
data.hasDest = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read facing data based on move type
|
||||||
|
if (data.moveType == 2) {
|
||||||
|
// FacingSpot: float x, y, z
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
||||||
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||||
|
} else if (data.moveType == 3) {
|
||||||
|
// FacingTarget: uint64 guid
|
||||||
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||||
|
data.facingTarget = packet.readUInt64();
|
||||||
|
} else if (data.moveType == 4) {
|
||||||
|
// FacingAngle: float angle
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.facingAngle = packet.readFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
// uint32 splineFlags
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.splineFlags = packet.readUInt32();
|
||||||
|
|
||||||
|
// Check for animation flag (0x00000100)
|
||||||
|
if (data.splineFlags & 0x00000100) {
|
||||||
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||||
|
packet.readUInt32(); // animId
|
||||||
|
packet.readUInt32(); // effectStartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// uint32 duration
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.duration = packet.readUInt32();
|
||||||
|
|
||||||
|
// Check for parabolic flag (0x00000200)
|
||||||
|
if (data.splineFlags & 0x00000200) {
|
||||||
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||||
|
packet.readFloat(); // vertAccel
|
||||||
|
packet.readUInt32(); // effectStartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// uint32 pointCount
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
uint32_t pointCount = packet.readUInt32();
|
||||||
|
|
||||||
|
if (pointCount == 0) return true;
|
||||||
|
|
||||||
|
// Read destination point(s)
|
||||||
|
// If UncompressedPath flag (0x00040000): all points are full float x,y,z
|
||||||
|
// Otherwise: first is packed destination, rest are packed deltas
|
||||||
|
bool uncompressed = (data.splineFlags & 0x00040000) != 0;
|
||||||
|
|
||||||
|
if (uncompressed) {
|
||||||
|
// Read last point as destination
|
||||||
|
// Skip to last point: each point is 12 bytes
|
||||||
|
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
||||||
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||||
|
}
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
||||||
|
data.destX = packet.readFloat();
|
||||||
|
data.destY = packet.readFloat();
|
||||||
|
data.destZ = packet.readFloat();
|
||||||
|
data.hasDest = true;
|
||||||
|
} else {
|
||||||
|
// Compressed: first 3 floats are the destination (final point)
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
||||||
|
data.destX = packet.readFloat();
|
||||||
|
data.destY = packet.readFloat();
|
||||||
|
data.destZ = packet.readFloat();
|
||||||
|
data.hasDest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("MonsterMove: guid=0x", std::hex, data.guid, std::dec,
|
||||||
|
" type=", (int)data.moveType, " dur=", data.duration, "ms",
|
||||||
|
" dest=(", data.destX, ",", data.destY, ",", data.destZ, ")");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Phase 2: Combat Core
|
// Phase 2: Combat Core
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,22 @@ void WorldSocket::send(const Packet& packet) {
|
||||||
LOG_DEBUG("Sending world packet: opcode=0x", std::hex, opcode, std::dec,
|
LOG_DEBUG("Sending world packet: opcode=0x", std::hex, opcode, std::dec,
|
||||||
" payload=", payloadLen, " bytes (", sendData.size(), " total)");
|
" payload=", payloadLen, " bytes (", sendData.size(), " total)");
|
||||||
|
|
||||||
|
// Debug: dump first few movement packets
|
||||||
|
{
|
||||||
|
static int moveDump = 3;
|
||||||
|
bool isMove = (opcode >= 0xB5 && opcode <= 0xBE) || opcode == 0xC9 || opcode == 0xDA || opcode == 0xEE;
|
||||||
|
if (isMove && moveDump-- > 0) {
|
||||||
|
std::string hex = "MOVE PKT dump opcode=0x";
|
||||||
|
char buf[8]; snprintf(buf, sizeof(buf), "%03x", opcode); hex += buf;
|
||||||
|
hex += " payload=" + std::to_string(payloadLen) + " bytes: ";
|
||||||
|
for (size_t i = 6; i < sendData.size() && i < 6 + 48; i++) {
|
||||||
|
char b[4]; snprintf(b, sizeof(b), "%02x ", sendData[i]);
|
||||||
|
hex += b;
|
||||||
|
}
|
||||||
|
LOG_INFO(hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: dump packet bytes for AUTH_SESSION
|
// Debug: dump packet bytes for AUTH_SESSION
|
||||||
if (opcode == 0x1ED) {
|
if (opcode == 0x1ED) {
|
||||||
std::string hexDump = "AUTH_SESSION raw bytes: ";
|
std::string hexDump = "AUTH_SESSION raw bytes: ";
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ void CameraController::startIntroPan(float durationSec, float orbitDegrees) {
|
||||||
if (!camera) return;
|
if (!camera) return;
|
||||||
introActive = true;
|
introActive = true;
|
||||||
introTimer = 0.0f;
|
introTimer = 0.0f;
|
||||||
|
idleTimer_ = 0.0f;
|
||||||
introDuration = std::max(0.5f, durationSec);
|
introDuration = std::max(0.5f, durationSec);
|
||||||
introStartYaw = facingYaw + orbitDegrees;
|
introStartYaw = facingYaw + orbitDegrees;
|
||||||
introEndYaw = facingYaw;
|
introEndYaw = facingYaw;
|
||||||
|
|
@ -96,9 +97,24 @@ void CameraController::update(float deltaTime) {
|
||||||
bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL));
|
bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL));
|
||||||
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
|
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
|
||||||
|
|
||||||
|
// Idle camera: any input resets the timer; timeout triggers a slow orbit pan
|
||||||
|
bool anyInput = leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump;
|
||||||
|
if (anyInput) {
|
||||||
|
idleTimer_ = 0.0f;
|
||||||
|
} else if (!introActive) {
|
||||||
|
idleTimer_ += deltaTime;
|
||||||
|
if (idleTimer_ >= IDLE_TIMEOUT) {
|
||||||
|
idleTimer_ = 0.0f;
|
||||||
|
startIntroPan(6.0f, 360.0f); // Slow full orbit
|
||||||
|
idleOrbit_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (introActive) {
|
if (introActive) {
|
||||||
if (leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump) {
|
if (anyInput) {
|
||||||
introActive = false;
|
introActive = false;
|
||||||
|
idleOrbit_ = false;
|
||||||
|
idleTimer_ = 0.0f;
|
||||||
} else {
|
} else {
|
||||||
introTimer += deltaTime;
|
introTimer += deltaTime;
|
||||||
float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f;
|
float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f;
|
||||||
|
|
@ -109,7 +125,13 @@ void CameraController::update(float deltaTime) {
|
||||||
camera->setRotation(yaw, pitch);
|
camera->setRotation(yaw, pitch);
|
||||||
facingYaw = yaw;
|
facingYaw = yaw;
|
||||||
if (t >= 1.0f) {
|
if (t >= 1.0f) {
|
||||||
introActive = false;
|
if (idleOrbit_) {
|
||||||
|
// Loop: restart the slow orbit continuously
|
||||||
|
startIntroPan(6.0f, 360.0f);
|
||||||
|
idleOrbit_ = true;
|
||||||
|
} else {
|
||||||
|
introActive = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Suppress player movement/input during intro.
|
// Suppress player movement/input during intro.
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ bool CharacterRenderer::initialize() {
|
||||||
uniform mat4 uLightSpaceMatrix;
|
uniform mat4 uLightSpaceMatrix;
|
||||||
uniform int uShadowEnabled;
|
uniform int uShadowEnabled;
|
||||||
uniform float uShadowStrength;
|
uniform float uShadowStrength;
|
||||||
|
uniform float uOpacity;
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
|
@ -154,8 +155,8 @@ bool CharacterRenderer::initialize() {
|
||||||
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
|
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
|
||||||
result = mix(uFogColor, result, fogFactor);
|
result = mix(uFogColor, result, fogFactor);
|
||||||
|
|
||||||
// Force alpha=1 for opaque character rendering (baked NPC textures may have alpha=0)
|
// Apply opacity (for fade-in effects)
|
||||||
FragColor = vec4(result, 1.0);
|
FragColor = vec4(result, uOpacity);
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
|
@ -906,6 +907,35 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId,
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterRenderer::update(float deltaTime) {
|
void CharacterRenderer::update(float deltaTime) {
|
||||||
|
// Update fade-in opacity
|
||||||
|
for (auto& [id, inst] : instances) {
|
||||||
|
if (inst.fadeInDuration > 0.0f && inst.opacity < 1.0f) {
|
||||||
|
inst.fadeInTime += deltaTime;
|
||||||
|
inst.opacity = std::min(1.0f, inst.fadeInTime / inst.fadeInDuration);
|
||||||
|
if (inst.opacity >= 1.0f) {
|
||||||
|
inst.fadeInDuration = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate creature movement
|
||||||
|
for (auto& [id, inst] : instances) {
|
||||||
|
if (inst.isMoving) {
|
||||||
|
inst.moveElapsed += deltaTime;
|
||||||
|
float t = inst.moveElapsed / inst.moveDuration;
|
||||||
|
if (t >= 1.0f) {
|
||||||
|
inst.position = inst.moveEnd;
|
||||||
|
inst.isMoving = false;
|
||||||
|
// Return to idle when movement completes
|
||||||
|
if (inst.currentAnimationId == 4) {
|
||||||
|
playAnimation(id, 0, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inst.position = glm::mix(inst.moveStart, inst.moveEnd, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (auto& pair : instances) {
|
for (auto& pair : instances) {
|
||||||
updateAnimation(pair.second, deltaTime);
|
updateAnimation(pair.second, deltaTime);
|
||||||
}
|
}
|
||||||
|
|
@ -1123,6 +1153,8 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
|
|
||||||
glEnable(GL_DEPTH_TEST);
|
glEnable(GL_DEPTH_TEST);
|
||||||
glDisable(GL_CULL_FACE); // M2 models have mixed winding; render both sides
|
glDisable(GL_CULL_FACE); // M2 models have mixed winding; render both sides
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
characterShader->use();
|
characterShader->use();
|
||||||
characterShader->setUniform("uView", view);
|
characterShader->setUniform("uView", view);
|
||||||
|
|
@ -1155,11 +1187,15 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
|
|
||||||
const auto& gpuModel = models[instance.modelId];
|
const auto& gpuModel = models[instance.modelId];
|
||||||
|
|
||||||
|
// Skip fully transparent instances
|
||||||
|
if (instance.opacity <= 0.0f) continue;
|
||||||
|
|
||||||
// Set model matrix (use override for weapon instances)
|
// Set model matrix (use override for weapon instances)
|
||||||
glm::mat4 modelMat = instance.hasOverrideModelMatrix
|
glm::mat4 modelMat = instance.hasOverrideModelMatrix
|
||||||
? instance.overrideModelMatrix
|
? instance.overrideModelMatrix
|
||||||
: getModelMatrix(instance);
|
: getModelMatrix(instance);
|
||||||
characterShader->setUniform("uModel", modelMat);
|
characterShader->setUniform("uModel", modelMat);
|
||||||
|
characterShader->setUniform("uOpacity", instance.opacity);
|
||||||
|
|
||||||
// Set bone matrices (upload all at once for performance)
|
// Set bone matrices (upload all at once for performance)
|
||||||
int numBones = std::min(static_cast<int>(instance.boneMatrices.size()), MAX_BONES);
|
int numBones = std::min(static_cast<int>(instance.boneMatrices.size()), MAX_BONES);
|
||||||
|
|
@ -1273,6 +1309,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
}
|
}
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
glEnable(GL_CULL_FACE); // Restore culling for other renderers
|
glEnable(GL_CULL_FACE); // Restore culling for other renderers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1379,6 +1416,55 @@ void CharacterRenderer::setInstanceRotation(uint32_t instanceId, const glm::vec3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& destination, float durationSeconds) {
|
||||||
|
auto it = instances.find(instanceId);
|
||||||
|
if (it == instances.end()) return;
|
||||||
|
|
||||||
|
auto& inst = it->second;
|
||||||
|
if (durationSeconds <= 0.0f) {
|
||||||
|
// Instant move (stop)
|
||||||
|
inst.position = destination;
|
||||||
|
inst.isMoving = false;
|
||||||
|
// Return to idle animation if currently walking
|
||||||
|
if (inst.currentAnimationId == 4) {
|
||||||
|
playAnimation(instanceId, 0, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.moveStart = inst.position;
|
||||||
|
inst.moveEnd = destination;
|
||||||
|
inst.moveDuration = durationSeconds;
|
||||||
|
inst.moveElapsed = 0.0f;
|
||||||
|
inst.isMoving = true;
|
||||||
|
|
||||||
|
// Face toward destination (yaw around Z axis since Z is up)
|
||||||
|
glm::vec3 dir = destination - inst.position;
|
||||||
|
if (glm::length(glm::vec2(dir.x, dir.y)) > 0.001f) {
|
||||||
|
float angle = std::atan2(dir.y, dir.x);
|
||||||
|
inst.rotation.z = angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play walk animation (ID 4) while moving
|
||||||
|
if (inst.currentAnimationId == 0) {
|
||||||
|
playAnimation(instanceId, 4, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeline::M2Model* CharacterRenderer::getModelData(uint32_t modelId) const {
|
||||||
|
auto it = models.find(modelId);
|
||||||
|
if (it == models.end()) return nullptr;
|
||||||
|
return &it->second.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterRenderer::startFadeIn(uint32_t instanceId, float durationSeconds) {
|
||||||
|
auto it = instances.find(instanceId);
|
||||||
|
if (it == instances.end()) return;
|
||||||
|
it->second.opacity = 0.0f;
|
||||||
|
it->second.fadeInTime = 0.0f;
|
||||||
|
it->second.fadeInDuration = durationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
void CharacterRenderer::setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets) {
|
void CharacterRenderer::setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets) {
|
||||||
auto it = instances.find(instanceId);
|
auto it = instances.find(instanceId);
|
||||||
if (it != instances.end()) {
|
if (it != instances.end()) {
|
||||||
|
|
|
||||||
|
|
@ -391,7 +391,8 @@ uint32_t Renderer::resolveMeleeAnimId() {
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
const uint32_t attackCandidates[] = {16, 17, 18, 19, 20, 21};
|
// Prefer weapon attacks (1H=17, 2H=18) over unarmed (16); 19-21 are other variants
|
||||||
|
const uint32_t attackCandidates[] = {17, 18, 16, 19, 20, 21};
|
||||||
for (uint32_t id : attackCandidates) {
|
for (uint32_t id : attackCandidates) {
|
||||||
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
|
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
|
||||||
meleeAnimId = id;
|
meleeAnimId = id;
|
||||||
|
|
@ -1032,6 +1033,113 @@ void Renderer::update(float deltaTime) {
|
||||||
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Selection Circle
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void Renderer::initSelectionCircle() {
|
||||||
|
if (selCircleVAO) return;
|
||||||
|
|
||||||
|
// Minimal shader: position + uniform MVP + color
|
||||||
|
const char* vsSrc = R"(
|
||||||
|
#version 330 core
|
||||||
|
layout(location = 0) in vec3 aPos;
|
||||||
|
uniform mat4 uMVP;
|
||||||
|
void main() {
|
||||||
|
gl_Position = uMVP * vec4(aPos, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* fsSrc = R"(
|
||||||
|
#version 330 core
|
||||||
|
uniform vec3 uColor;
|
||||||
|
out vec4 FragColor;
|
||||||
|
void main() {
|
||||||
|
FragColor = vec4(uColor, 0.6);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
auto compile = [](GLenum type, const char* src) -> GLuint {
|
||||||
|
GLuint s = glCreateShader(type);
|
||||||
|
glShaderSource(s, 1, &src, nullptr);
|
||||||
|
glCompileShader(s);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
GLuint vs = compile(GL_VERTEX_SHADER, vsSrc);
|
||||||
|
GLuint fs = compile(GL_FRAGMENT_SHADER, fsSrc);
|
||||||
|
selCircleShader = glCreateProgram();
|
||||||
|
glAttachShader(selCircleShader, vs);
|
||||||
|
glAttachShader(selCircleShader, fs);
|
||||||
|
glLinkProgram(selCircleShader);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
|
||||||
|
// Build ring vertices (two concentric circles forming a strip)
|
||||||
|
constexpr int SEGMENTS = 48;
|
||||||
|
constexpr float INNER = 0.85f;
|
||||||
|
constexpr float OUTER = 1.0f;
|
||||||
|
std::vector<float> verts;
|
||||||
|
for (int i = 0; i <= SEGMENTS; i++) {
|
||||||
|
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(SEGMENTS);
|
||||||
|
float c = std::cos(angle), s = std::sin(angle);
|
||||||
|
// Outer vertex
|
||||||
|
verts.push_back(c * OUTER);
|
||||||
|
verts.push_back(s * OUTER);
|
||||||
|
verts.push_back(0.0f);
|
||||||
|
// Inner vertex
|
||||||
|
verts.push_back(c * INNER);
|
||||||
|
verts.push_back(s * INNER);
|
||||||
|
verts.push_back(0.0f);
|
||||||
|
}
|
||||||
|
selCircleVertCount = static_cast<int>((SEGMENTS + 1) * 2);
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &selCircleVAO);
|
||||||
|
glGenBuffers(1, &selCircleVBO);
|
||||||
|
glBindVertexArray(selCircleVAO);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, selCircleVBO);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW);
|
||||||
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) {
|
||||||
|
selCirclePos = pos;
|
||||||
|
selCircleRadius = radius;
|
||||||
|
selCircleColor = color;
|
||||||
|
selCircleVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::clearSelectionCircle() {
|
||||||
|
selCircleVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection) {
|
||||||
|
if (!selCircleVisible) return;
|
||||||
|
initSelectionCircle();
|
||||||
|
|
||||||
|
glm::mat4 model = glm::translate(glm::mat4(1.0f), selCirclePos);
|
||||||
|
model = glm::scale(model, glm::vec3(selCircleRadius));
|
||||||
|
|
||||||
|
glm::mat4 mvp = projection * view * model;
|
||||||
|
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
glDepthMask(GL_FALSE);
|
||||||
|
|
||||||
|
glUseProgram(selCircleShader);
|
||||||
|
glUniformMatrix4fv(glGetUniformLocation(selCircleShader, "uMVP"), 1, GL_FALSE, &mvp[0][0]);
|
||||||
|
glUniform3fv(glGetUniformLocation(selCircleShader, "uColor"), 1, &selCircleColor[0]);
|
||||||
|
|
||||||
|
glBindVertexArray(selCircleVAO);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, selCircleVertCount);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glEnable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
|
|
||||||
void Renderer::renderWorld(game::World* world) {
|
void Renderer::renderWorld(game::World* world) {
|
||||||
auto renderStart = std::chrono::steady_clock::now();
|
auto renderStart = std::chrono::steady_clock::now();
|
||||||
lastTerrainRenderMs = 0.0;
|
lastTerrainRenderMs = 0.0;
|
||||||
|
|
@ -1157,6 +1265,9 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
characterRenderer->render(*camera, view, projection);
|
characterRenderer->render(*camera, view, projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render selection circle under targeted creature
|
||||||
|
renderSelectionCircle(view, projection);
|
||||||
|
|
||||||
// Render WMO buildings (after characters, before UI)
|
// Render WMO buildings (after characters, before UI)
|
||||||
if (wmoRenderer && camera) {
|
if (wmoRenderer && camera) {
|
||||||
auto wmoStart = std::chrono::steady_clock::now();
|
auto wmoStart = std::chrono::steady_clock::now();
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,26 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
// Teleporter panel (T key toggle handled in Application event loop)
|
// Teleporter panel (T key toggle handled in Application event loop)
|
||||||
renderTeleporterPanel();
|
renderTeleporterPanel();
|
||||||
|
|
||||||
|
// Quest Log (L key toggle handled inside)
|
||||||
|
questLogScreen.render(gameHandler);
|
||||||
|
|
||||||
// Spellbook (P key toggle handled inside)
|
// Spellbook (P key toggle handled inside)
|
||||||
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
||||||
|
|
||||||
// Inventory (B key toggle handled inside)
|
// Set vendor mode before rendering inventory
|
||||||
|
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
||||||
|
|
||||||
|
// Auto-open bags when vendor window opens
|
||||||
|
if (gameHandler.isVendorWindowOpen() && !inventoryScreen.isOpen()) {
|
||||||
|
inventoryScreen.setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bags (B key toggle handled inside)
|
||||||
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
||||||
|
|
||||||
|
// Character screen (C key toggle handled inside render())
|
||||||
|
inventoryScreen.renderCharacterScreen(gameHandler.getInventory());
|
||||||
|
|
||||||
if (inventoryScreen.consumeInventoryDirty()) {
|
if (inventoryScreen.consumeInventoryDirty()) {
|
||||||
gameHandler.notifyInventoryChanged();
|
gameHandler.notifyInventoryChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +126,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
gameHandler.notifyEquipmentChanged();
|
gameHandler.notifyEquipmentChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update renderer face-target position
|
// Update renderer face-target position and selection circle
|
||||||
auto* renderer = core::Application::getInstance().getRenderer();
|
auto* renderer = core::Application::getInstance().getRenderer();
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
static glm::vec3 targetGLPos;
|
static glm::vec3 targetGLPos;
|
||||||
|
|
@ -121,11 +135,30 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
if (target) {
|
if (target) {
|
||||||
targetGLPos = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
|
targetGLPos = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
|
||||||
renderer->setTargetPosition(&targetGLPos);
|
renderer->setTargetPosition(&targetGLPos);
|
||||||
|
|
||||||
|
// Selection circle color: red=hostile, green=friendly, gray=dead
|
||||||
|
glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow
|
||||||
|
float circleRadius = 1.5f;
|
||||||
|
if (target->getType() == game::ObjectType::UNIT) {
|
||||||
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||||
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||||
|
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
||||||
|
} else if (unit->isInteractable()) {
|
||||||
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
||||||
|
} else {
|
||||||
|
circleColor = glm::vec3(1.0f, 0.2f, 0.2f); // red (hostile)
|
||||||
|
}
|
||||||
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
||||||
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (player)
|
||||||
|
}
|
||||||
|
renderer->setSelectionCircle(targetGLPos, circleRadius, circleColor);
|
||||||
} else {
|
} else {
|
||||||
renderer->setTargetPosition(nullptr);
|
renderer->setTargetPosition(nullptr);
|
||||||
|
renderer->clearSelectionCircle();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
renderer->setTargetPosition(nullptr);
|
renderer->setTargetPosition(nullptr);
|
||||||
|
renderer->clearSelectionCircle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -422,16 +455,29 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
float closestT = 1e30f;
|
float closestT = 1e30f;
|
||||||
uint64_t closestGuid = 0;
|
uint64_t closestGuid = 0;
|
||||||
|
|
||||||
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
||||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||||
auto t = entity->getType();
|
auto t = entity->getType();
|
||||||
if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue;
|
if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue;
|
||||||
|
if (guid == myGuid) continue; // Don't target self
|
||||||
|
|
||||||
|
// Scale hitbox based on entity type
|
||||||
|
float hitRadius = 1.5f;
|
||||||
|
float heightOffset = 1.5f;
|
||||||
|
if (t == game::ObjectType::UNIT) {
|
||||||
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||||
|
// Critters have very low max health (< 100)
|
||||||
|
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
||||||
|
hitRadius = 0.5f;
|
||||||
|
heightOffset = 0.3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
glm::vec3 entityGL = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
glm::vec3 entityGL = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||||
// Add half-height offset so we target the body center, not feet
|
entityGL.z += heightOffset;
|
||||||
entityGL.z += 3.0f;
|
|
||||||
|
|
||||||
float hitT;
|
float hitT;
|
||||||
if (raySphereIntersect(ray, entityGL, 3.0f, hitT)) {
|
if (raySphereIntersect(ray, entityGL, hitRadius, hitT)) {
|
||||||
if (hitT < closestT) {
|
if (hitT < closestT) {
|
||||||
closestT = hitT;
|
closestT = hitT;
|
||||||
closestGuid = guid;
|
closestGuid = guid;
|
||||||
|
|
@ -505,11 +551,11 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
const auto& characters = gameHandler.getCharacters();
|
const auto& characters = gameHandler.getCharacters();
|
||||||
if (!characters.empty()) {
|
if (!characters.empty()) {
|
||||||
// Use the first (or most recently selected) character
|
|
||||||
const auto& ch = characters[0];
|
const auto& ch = characters[0];
|
||||||
playerName = ch.name;
|
playerName = ch.name;
|
||||||
playerLevel = ch.level;
|
// Use live server level if available, otherwise character struct
|
||||||
// Characters don't store HP; use level-scaled estimate
|
playerLevel = gameHandler.getPlayerLevel();
|
||||||
|
if (playerLevel == 0) playerLevel = ch.level;
|
||||||
playerMaxHp = 20 + playerLevel * 10;
|
playerMaxHp = 20 + playerLevel * 10;
|
||||||
playerHp = playerMaxHp;
|
playerHp = playerMaxHp;
|
||||||
}
|
}
|
||||||
|
|
@ -589,26 +635,30 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
||||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
||||||
|
|
||||||
|
// Determine hostility color for border and name
|
||||||
|
ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f);
|
||||||
|
if (target->getType() == game::ObjectType::PLAYER) {
|
||||||
|
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||||
|
} else if (target->getType() == game::ObjectType::UNIT) {
|
||||||
|
auto u = std::static_pointer_cast<game::Unit>(target);
|
||||||
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
||||||
|
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||||
|
} else if (u->isInteractable()) {
|
||||||
|
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||||
|
} else {
|
||||||
|
hostileColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f));
|
||||||
|
|
||||||
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
|
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
|
||||||
// Entity name and type
|
// Entity name and type
|
||||||
std::string name = getEntityName(target);
|
std::string name = getEntityName(target);
|
||||||
|
|
||||||
ImVec4 nameColor;
|
ImVec4 nameColor = hostileColor;
|
||||||
switch (target->getType()) {
|
|
||||||
case game::ObjectType::PLAYER:
|
|
||||||
nameColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green
|
|
||||||
break;
|
|
||||||
case game::ObjectType::UNIT:
|
|
||||||
nameColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
nameColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::TextColored(nameColor, "%s", name.c_str());
|
ImGui::TextColored(nameColor, "%s", name.c_str());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "ui/inventory_screen.hpp"
|
#include "ui/inventory_screen.hpp"
|
||||||
|
#include "game/game_handler.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
@ -32,13 +33,11 @@ game::EquipSlot InventoryScreen::getEquipSlotForType(uint8_t inventoryType, game
|
||||||
case 9: return game::EquipSlot::WRISTS;
|
case 9: return game::EquipSlot::WRISTS;
|
||||||
case 10: return game::EquipSlot::HANDS;
|
case 10: return game::EquipSlot::HANDS;
|
||||||
case 11: {
|
case 11: {
|
||||||
// Ring: prefer empty slot, else RING1
|
|
||||||
if (inv.getEquipSlot(game::EquipSlot::RING1).empty())
|
if (inv.getEquipSlot(game::EquipSlot::RING1).empty())
|
||||||
return game::EquipSlot::RING1;
|
return game::EquipSlot::RING1;
|
||||||
return game::EquipSlot::RING2;
|
return game::EquipSlot::RING2;
|
||||||
}
|
}
|
||||||
case 12: {
|
case 12: {
|
||||||
// Trinket: prefer empty slot, else TRINKET1
|
|
||||||
if (inv.getEquipSlot(game::EquipSlot::TRINKET1).empty())
|
if (inv.getEquipSlot(game::EquipSlot::TRINKET1).empty())
|
||||||
return game::EquipSlot::TRINKET1;
|
return game::EquipSlot::TRINKET1;
|
||||||
return game::EquipSlot::TRINKET2;
|
return game::EquipSlot::TRINKET2;
|
||||||
|
|
@ -99,7 +98,6 @@ void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
||||||
game::ItemDef targetItem = target.item;
|
game::ItemDef targetItem = target.item;
|
||||||
inv.setBackpackSlot(index, heldItem);
|
inv.setBackpackSlot(index, heldItem);
|
||||||
heldItem = targetItem;
|
heldItem = targetItem;
|
||||||
// Keep holding the swapped item - update source to this backpack slot
|
|
||||||
heldSource = HeldSource::BACKPACK;
|
heldSource = HeldSource::BACKPACK;
|
||||||
heldBackpackIndex = index;
|
heldBackpackIndex = index;
|
||||||
}
|
}
|
||||||
|
|
@ -112,19 +110,18 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
||||||
// Validate: check if the held item can go in this slot
|
// Validate: check if the held item can go in this slot
|
||||||
if (heldItem.inventoryType > 0) {
|
if (heldItem.inventoryType > 0) {
|
||||||
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inv);
|
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inv);
|
||||||
if (validSlot == game::EquipSlot::NUM_SLOTS) return; // Not equippable
|
if (validSlot == game::EquipSlot::NUM_SLOTS) return;
|
||||||
|
|
||||||
// For rings/trinkets, allow either slot
|
|
||||||
bool valid = (slot == validSlot);
|
bool valid = (slot == validSlot);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
if (heldItem.inventoryType == 11) // Ring
|
if (heldItem.inventoryType == 11)
|
||||||
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
|
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
|
||||||
else if (heldItem.inventoryType == 12) // Trinket
|
else if (heldItem.inventoryType == 12)
|
||||||
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
|
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
|
||||||
}
|
}
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
} else {
|
} else {
|
||||||
return; // No inventoryType means not equippable
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& target = inv.getEquipSlot(slot);
|
const auto& target = inv.getEquipSlot(slot);
|
||||||
|
|
@ -132,7 +129,6 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
||||||
inv.setEquipSlot(slot, heldItem);
|
inv.setEquipSlot(slot, heldItem);
|
||||||
holdingItem = false;
|
holdingItem = false;
|
||||||
} else {
|
} else {
|
||||||
// Swap
|
|
||||||
game::ItemDef targetItem = target.item;
|
game::ItemDef targetItem = target.item;
|
||||||
inv.setEquipSlot(slot, heldItem);
|
inv.setEquipSlot(slot, heldItem);
|
||||||
heldItem = targetItem;
|
heldItem = targetItem;
|
||||||
|
|
@ -163,13 +159,10 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
||||||
|
|
||||||
void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
||||||
if (!holdingItem) return;
|
if (!holdingItem) return;
|
||||||
// Return item to source
|
|
||||||
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
||||||
// If source slot is still empty, put it back
|
|
||||||
if (inv.getBackpackSlot(heldBackpackIndex).empty()) {
|
if (inv.getBackpackSlot(heldBackpackIndex).empty()) {
|
||||||
inv.setBackpackSlot(heldBackpackIndex, heldItem);
|
inv.setBackpackSlot(heldBackpackIndex, heldItem);
|
||||||
} else {
|
} else {
|
||||||
// Source was swapped into; find free slot
|
|
||||||
inv.addItem(heldItem);
|
inv.addItem(heldItem);
|
||||||
}
|
}
|
||||||
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
||||||
|
|
@ -180,7 +173,6 @@ void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
||||||
inv.addItem(heldItem);
|
inv.addItem(heldItem);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: just add to inventory
|
|
||||||
inv.addItem(heldItem);
|
inv.addItem(heldItem);
|
||||||
}
|
}
|
||||||
holdingItem = false;
|
holdingItem = false;
|
||||||
|
|
@ -199,13 +191,11 @@ void InventoryScreen::renderHeldItem() {
|
||||||
ImVec4 qColor = getQualityColor(heldItem.quality);
|
ImVec4 qColor = getQualityColor(heldItem.quality);
|
||||||
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
||||||
|
|
||||||
// Background
|
|
||||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
|
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
|
||||||
IM_COL32(40, 35, 30, 200));
|
IM_COL32(40, 35, 30, 200));
|
||||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||||
borderCol, 0.0f, 0, 2.0f);
|
borderCol, 0.0f, 0, 2.0f);
|
||||||
|
|
||||||
// Item abbreviation
|
|
||||||
char abbr[4] = {};
|
char abbr[4] = {};
|
||||||
if (!heldItem.name.empty()) {
|
if (!heldItem.name.empty()) {
|
||||||
abbr[0] = heldItem.name[0];
|
abbr[0] = heldItem.name[0];
|
||||||
|
|
@ -215,7 +205,6 @@ void InventoryScreen::renderHeldItem() {
|
||||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||||
|
|
||||||
// Stack count
|
|
||||||
if (heldItem.stackCount > 1) {
|
if (heldItem.stackCount > 1) {
|
||||||
char countStr[16];
|
char countStr[16];
|
||||||
snprintf(countStr, sizeof(countStr), "%u", heldItem.stackCount);
|
snprintf(countStr, sizeof(countStr), "%u", heldItem.stackCount);
|
||||||
|
|
@ -225,6 +214,10 @@ void InventoryScreen::renderHeldItem() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Bags window (B key) — bottom of screen, no equipment panel
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||||
// B key toggle (edge-triggered)
|
// B key toggle (edge-triggered)
|
||||||
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||||
|
|
@ -234,8 +227,14 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||||
}
|
}
|
||||||
bKeyWasDown = bDown;
|
bKeyWasDown = bDown;
|
||||||
|
|
||||||
|
// C key toggle for character screen (edge-triggered)
|
||||||
|
bool cDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_C);
|
||||||
|
if (cDown && !cKeyWasDown) {
|
||||||
|
characterOpen = !characterOpen;
|
||||||
|
}
|
||||||
|
cKeyWasDown = cDown;
|
||||||
|
|
||||||
if (!open) {
|
if (!open) {
|
||||||
// Cancel held item if inventory closes
|
|
||||||
if (holdingItem) cancelPickup(inventory);
|
if (holdingItem) cancelPickup(inventory);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -252,33 +251,42 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
float screenW = io.DisplaySize.x;
|
float screenW = io.DisplaySize.x;
|
||||||
|
float screenH = io.DisplaySize.y;
|
||||||
|
|
||||||
// Position inventory window on the right side of the screen
|
// Calculate bag window size
|
||||||
ImGui::SetNextWindowPos(ImVec2(screenW - 520.0f, 80.0f), ImGuiCond_FirstUseEver);
|
constexpr float slotSize = 40.0f;
|
||||||
ImGui::SetNextWindowSize(ImVec2(500.0f, 560.0f), ImGuiCond_FirstUseEver);
|
constexpr int columns = 4;
|
||||||
|
int rows = (inventory.getBackpackSize() + columns - 1) / columns;
|
||||||
|
float bagContentH = rows * (slotSize + 4.0f) + 40.0f; // slots + header + money
|
||||||
|
|
||||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
// Check for extra bags and add space
|
||||||
if (!ImGui::Begin("Inventory", &open, flags)) {
|
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) {
|
||||||
|
int bagSize = inventory.getBagSize(bag);
|
||||||
|
if (bagSize <= 0) continue;
|
||||||
|
int bagRows = (bagSize + columns - 1) / columns;
|
||||||
|
bagContentH += bagRows * (slotSize + 4.0f) + 30.0f; // slots + header
|
||||||
|
}
|
||||||
|
|
||||||
|
float windowW = columns * (slotSize + 4.0f) + 30.0f;
|
||||||
|
float windowH = bagContentH + 50.0f; // padding
|
||||||
|
|
||||||
|
// Position at bottom-right of screen
|
||||||
|
float posX = screenW - windowW - 10.0f;
|
||||||
|
float posY = screenH - windowH - 60.0f; // above action bar area
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always);
|
||||||
|
|
||||||
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
|
||||||
|
if (!ImGui::Begin("Bags", &open, flags)) {
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve space for money display at bottom
|
|
||||||
float moneyHeight = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
|
||||||
float panelHeight = ImGui::GetContentRegionAvail().y - moneyHeight;
|
|
||||||
|
|
||||||
// Two-column layout: Equipment (left) | Backpack (right)
|
|
||||||
ImGui::BeginChild("EquipPanel", ImVec2(200.0f, panelHeight), true);
|
|
||||||
renderEquipmentPanel(inventory);
|
|
||||||
ImGui::EndChild();
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, panelHeight), true);
|
|
||||||
renderBackpackPanel(inventory);
|
renderBackpackPanel(inventory);
|
||||||
ImGui::EndChild();
|
|
||||||
|
|
||||||
// Money display
|
// Money display
|
||||||
|
ImGui::Spacing();
|
||||||
uint64_t gold = moneyCopper / 10000;
|
uint64_t gold = moneyCopper / 10000;
|
||||||
uint64_t silver = (moneyCopper / 100) % 100;
|
uint64_t silver = (moneyCopper / 100) % 100;
|
||||||
uint64_t copper = moneyCopper % 100;
|
uint64_t copper = moneyCopper % 100;
|
||||||
|
|
@ -288,10 +296,37 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||||
static_cast<unsigned long long>(copper));
|
static_cast<unsigned long long>(copper));
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
// Draw held item at cursor (on top of everything)
|
// Draw held item at cursor
|
||||||
renderHeldItem();
|
renderHeldItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Character screen (C key) — standalone equipment window
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void InventoryScreen::renderCharacterScreen(game::Inventory& inventory) {
|
||||||
|
if (!characterOpen) return;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(220.0f, 520.0f), ImGuiCond_FirstUseEver);
|
||||||
|
|
||||||
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||||
|
if (!ImGui::Begin("Character", &characterOpen, flags)) {
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEquipmentPanel(inventory);
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
// If both bags and character are open, allow drag-and-drop between them
|
||||||
|
// (held item rendering is handled in render())
|
||||||
|
if (open) {
|
||||||
|
renderHeldItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment");
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment");
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
@ -312,10 +347,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
||||||
constexpr float slotSize = 36.0f;
|
constexpr float slotSize = 36.0f;
|
||||||
constexpr float spacing = 4.0f;
|
constexpr float spacing = 4.0f;
|
||||||
|
|
||||||
// Two columns of equipment
|
|
||||||
int rows = 8;
|
int rows = 8;
|
||||||
for (int r = 0; r < rows; r++) {
|
for (int r = 0; r < rows; r++) {
|
||||||
// Left slot
|
|
||||||
{
|
{
|
||||||
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
|
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
|
||||||
const char* label = game::getEquipSlotName(leftSlots[r]);
|
const char* label = game::getEquipSlotName(leftSlots[r]);
|
||||||
|
|
@ -329,7 +362,6 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
||||||
|
|
||||||
ImGui::SameLine(slotSize + spacing + 60.0f);
|
ImGui::SameLine(slotSize + spacing + 60.0f);
|
||||||
|
|
||||||
// Right slot
|
|
||||||
{
|
{
|
||||||
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
||||||
const char* label = game::getEquipSlotName(rightSlots[r]);
|
const char* label = game::getEquipSlotName(rightSlots[r]);
|
||||||
|
|
@ -420,7 +452,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
bool validDrop = false;
|
bool validDrop = false;
|
||||||
if (holdingItem) {
|
if (holdingItem) {
|
||||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||||
validDrop = true; // Can always drop in backpack
|
validDrop = true;
|
||||||
} else if (kind == SlotKind::EQUIPMENT && heldItem.inventoryType > 0) {
|
} else if (kind == SlotKind::EQUIPMENT && heldItem.inventoryType > 0) {
|
||||||
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inventory);
|
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inventory);
|
||||||
validDrop = (equipSlot == validSlot);
|
validDrop = (equipSlot == validSlot);
|
||||||
|
|
@ -432,11 +464,9 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
// Empty slot: dark grey background
|
|
||||||
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
|
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
|
||||||
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
|
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
|
||||||
|
|
||||||
// Highlight valid drop targets
|
|
||||||
if (validDrop) {
|
if (validDrop) {
|
||||||
bgCol = IM_COL32(20, 50, 20, 200);
|
bgCol = IM_COL32(20, 50, 20, 200);
|
||||||
borderCol = IM_COL32(0, 180, 0, 200);
|
borderCol = IM_COL32(0, 180, 0, 200);
|
||||||
|
|
@ -445,7 +475,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
||||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size), borderCol);
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size), borderCol);
|
||||||
|
|
||||||
// Slot label for equipment slots
|
|
||||||
if (label) {
|
if (label) {
|
||||||
char abbr[4] = {};
|
char abbr[4] = {};
|
||||||
abbr[0] = label[0];
|
abbr[0] = label[0];
|
||||||
|
|
@ -457,7 +486,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
|
|
||||||
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
||||||
|
|
||||||
// Click interactions
|
|
||||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
||||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||||
placeInBackpack(inventory, backpackIndex);
|
placeInBackpack(inventory, backpackIndex);
|
||||||
|
|
@ -466,7 +494,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tooltip for empty equip slots
|
|
||||||
if (label && ImGui::IsItemHovered()) {
|
if (label && ImGui::IsItemHovered()) {
|
||||||
ImGui::BeginTooltip();
|
ImGui::BeginTooltip();
|
||||||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", label);
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", label);
|
||||||
|
|
@ -478,7 +505,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
ImVec4 qColor = getQualityColor(item.quality);
|
ImVec4 qColor = getQualityColor(item.quality);
|
||||||
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
||||||
|
|
||||||
// Highlight valid drop targets with green tint
|
|
||||||
ImU32 bgCol = IM_COL32(40, 35, 30, 220);
|
ImU32 bgCol = IM_COL32(40, 35, 30, 220);
|
||||||
if (holdingItem && validDrop) {
|
if (holdingItem && validDrop) {
|
||||||
bgCol = IM_COL32(30, 55, 30, 220);
|
bgCol = IM_COL32(30, 55, 30, 220);
|
||||||
|
|
@ -489,7 +515,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||||
borderCol, 0.0f, 0, 2.0f);
|
borderCol, 0.0f, 0, 2.0f);
|
||||||
|
|
||||||
// Item abbreviation (first 2 letters)
|
|
||||||
char abbr[4] = {};
|
char abbr[4] = {};
|
||||||
if (!item.name.empty()) {
|
if (!item.name.empty()) {
|
||||||
abbr[0] = item.name[0];
|
abbr[0] = item.name[0];
|
||||||
|
|
@ -499,7 +524,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||||
|
|
||||||
// Stack count (bottom-right)
|
|
||||||
if (item.stackCount > 1) {
|
if (item.stackCount > 1) {
|
||||||
char countStr[16];
|
char countStr[16];
|
||||||
snprintf(countStr, sizeof(countStr), "%u", item.stackCount);
|
snprintf(countStr, sizeof(countStr), "%u", item.stackCount);
|
||||||
|
|
@ -513,14 +537,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
// Left-click: pickup or place/swap
|
// Left-click: pickup or place/swap
|
||||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||||
if (!holdingItem) {
|
if (!holdingItem) {
|
||||||
// Pick up this item
|
|
||||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||||
pickupFromBackpack(inventory, backpackIndex);
|
pickupFromBackpack(inventory, backpackIndex);
|
||||||
} else if (kind == SlotKind::EQUIPMENT) {
|
} else if (kind == SlotKind::EQUIPMENT) {
|
||||||
pickupFromEquipment(inventory, equipSlot);
|
pickupFromEquipment(inventory, equipSlot);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Holding an item - place or swap
|
|
||||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||||
placeInBackpack(inventory, backpackIndex);
|
placeInBackpack(inventory, backpackIndex);
|
||||||
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
|
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
|
||||||
|
|
@ -529,9 +551,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right-click: auto-equip from backpack, or unequip from equipment
|
// Right-click: vendor sell (if vendor mode) or auto-equip/unequip
|
||||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
|
||||||
if (kind == SlotKind::EQUIPMENT) {
|
if (vendorMode_ && gameHandler_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||||
|
// Sell to vendor
|
||||||
|
gameHandler_->sellItemBySlot(backpackIndex);
|
||||||
|
} else if (kind == SlotKind::EQUIPMENT) {
|
||||||
// Unequip: move to free backpack slot
|
// Unequip: move to free backpack slot
|
||||||
int freeSlot = inventory.findFreeBackpackSlot();
|
int freeSlot = inventory.findFreeBackpackSlot();
|
||||||
if (freeSlot >= 0) {
|
if (freeSlot >= 0) {
|
||||||
|
|
@ -541,8 +566,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
inventoryDirty = true;
|
inventoryDirty = true;
|
||||||
}
|
}
|
||||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0 && item.inventoryType > 0) {
|
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0 && item.inventoryType > 0) {
|
||||||
// Auto-equip: find the right slot
|
// Auto-equip
|
||||||
// Capture type before swap (item ref may become stale)
|
|
||||||
uint8_t equippingType = item.inventoryType;
|
uint8_t equippingType = item.inventoryType;
|
||||||
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);
|
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);
|
||||||
if (targetSlot != game::EquipSlot::NUM_SLOTS) {
|
if (targetSlot != game::EquipSlot::NUM_SLOTS) {
|
||||||
|
|
@ -551,12 +575,10 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
inventory.setEquipSlot(targetSlot, item);
|
inventory.setEquipSlot(targetSlot, item);
|
||||||
inventory.clearBackpackSlot(backpackIndex);
|
inventory.clearBackpackSlot(backpackIndex);
|
||||||
} else {
|
} else {
|
||||||
// Swap with equipped item
|
|
||||||
game::ItemDef equippedItem = eqSlot.item;
|
game::ItemDef equippedItem = eqSlot.item;
|
||||||
inventory.setEquipSlot(targetSlot, item);
|
inventory.setEquipSlot(targetSlot, item);
|
||||||
inventory.setBackpackSlot(backpackIndex, equippedItem);
|
inventory.setBackpackSlot(backpackIndex, equippedItem);
|
||||||
}
|
}
|
||||||
// Two-handed weapon in main hand clears the off-hand
|
|
||||||
if (targetSlot == game::EquipSlot::MAIN_HAND && equippingType == 17) {
|
if (targetSlot == game::EquipSlot::MAIN_HAND && equippingType == 17) {
|
||||||
const auto& offHand = inventory.getEquipSlot(game::EquipSlot::OFF_HAND);
|
const auto& offHand = inventory.getEquipSlot(game::EquipSlot::OFF_HAND);
|
||||||
if (!offHand.empty()) {
|
if (!offHand.empty()) {
|
||||||
|
|
@ -564,7 +586,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
inventory.clearEquipSlot(game::EquipSlot::OFF_HAND);
|
inventory.clearEquipSlot(game::EquipSlot::OFF_HAND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Equipping off-hand unequips a 2H weapon from main hand
|
|
||||||
if (targetSlot == game::EquipSlot::OFF_HAND &&
|
if (targetSlot == game::EquipSlot::OFF_HAND &&
|
||||||
inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
||||||
inventory.addItem(inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
|
inventory.addItem(inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
|
||||||
|
|
@ -645,6 +666,18 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
|
||||||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sell price (when vendor is open)
|
||||||
|
if (vendorMode_ && gameHandler_) {
|
||||||
|
const auto* info = gameHandler_->getItemInfo(item.itemId);
|
||||||
|
if (info && info->sellPrice > 0) {
|
||||||
|
uint32_t g = info->sellPrice / 10000;
|
||||||
|
uint32_t s = (info->sellPrice / 100) % 100;
|
||||||
|
uint32_t c = info->sellPrice % 100;
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell Price: %ug %us %uc", g, s, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::EndTooltip();
|
ImGui::EndTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
90
src/ui/quest_log_screen.cpp
Normal file
90
src/ui/quest_log_screen.cpp
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#include "ui/quest_log_screen.hpp"
|
||||||
|
#include "core/application.hpp"
|
||||||
|
#include "core/input.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace wowee { namespace ui {
|
||||||
|
|
||||||
|
void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
||||||
|
// L key toggle (edge-triggered)
|
||||||
|
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||||
|
bool lDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_L);
|
||||||
|
if (lDown && !lKeyWasDown) {
|
||||||
|
open = !open;
|
||||||
|
}
|
||||||
|
lKeyWasDown = lDown;
|
||||||
|
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
auto* window = core::Application::getInstance().getWindow();
|
||||||
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||||
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||||
|
|
||||||
|
float logW = 380.0f;
|
||||||
|
float logH = std::min(450.0f, screenH - 120.0f);
|
||||||
|
float logX = (screenW - logW) * 0.5f;
|
||||||
|
float logY = 80.0f;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(logX, logY), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(logW, logH), ImGuiCond_FirstUseEver);
|
||||||
|
|
||||||
|
bool stillOpen = true;
|
||||||
|
if (ImGui::Begin("Quest Log", &stillOpen)) {
|
||||||
|
const auto& quests = gameHandler.getQuestLog();
|
||||||
|
|
||||||
|
if (quests.empty()) {
|
||||||
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No active quests.");
|
||||||
|
} else {
|
||||||
|
// Left panel: quest list
|
||||||
|
ImGui::BeginChild("QuestList", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 4), true);
|
||||||
|
for (size_t i = 0; i < quests.size(); i++) {
|
||||||
|
const auto& q = quests[i];
|
||||||
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
|
||||||
|
ImVec4 color = q.complete
|
||||||
|
? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) // Green for complete
|
||||||
|
: ImVec4(1.0f, 0.82f, 0.0f, 1.0f); // Gold for active
|
||||||
|
|
||||||
|
bool selected = (selectedIndex == static_cast<int>(i));
|
||||||
|
if (ImGui::Selectable("##quest", selected, 0, ImVec2(0, 20))) {
|
||||||
|
selectedIndex = static_cast<int>(i);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(color, "%s%s",
|
||||||
|
q.title.c_str(),
|
||||||
|
q.complete ? " (Complete)" : "");
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// Details panel for selected quest
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(quests.size())) {
|
||||||
|
const auto& sel = quests[static_cast<size_t>(selectedIndex)];
|
||||||
|
|
||||||
|
if (!sel.objectives.empty()) {
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextWrapped("%s", sel.objectives.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abandon button
|
||||||
|
if (!sel.complete) {
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::Button("Abandon Quest")) {
|
||||||
|
gameHandler.abandonQuest(sel.questId);
|
||||||
|
if (selectedIndex >= static_cast<int>(quests.size())) {
|
||||||
|
selectedIndex = static_cast<int>(quests.size()) - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
if (!stillOpen) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace wowee::ui
|
||||||
Loading…
Add table
Add a link
Reference in a new issue