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:
Kelsi 2026-02-06 13:47:03 -08:00
parent a4a39c7f0f
commit 7128ea1417
21 changed files with 1092 additions and 149 deletions

View file

@ -153,8 +153,19 @@ private:
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> 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
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

View file

@ -294,6 +294,11 @@ public:
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
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
float getSpellCooldown(uint32_t spellId) const;
@ -330,17 +335,33 @@ public:
bool isQuestDetailsOpen() const { return questDetailsOpen; }
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
void openVendor(uint64_t npcGuid);
void closeVendor();
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 sellItemBySlot(int backpackIndex);
bool isVendorWindowOpen() const { return vendorWindowOpen; }
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
const ItemQueryResponseData* getItemInfo(uint32_t itemId) const {
auto it = itemInfoCache_.find(itemId);
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
@ -464,6 +485,9 @@ private:
// ---- XP handler ----
void handleXpGain(network::Packet& packet);
// ---- Creature movement handler ----
void handleMonsterMove(network::Packet& packet);
// ---- Phase 5 handlers ----
void handleLootResponse(network::Packet& packet);
void handleLootReleaseResponse(network::Packet& packet);
@ -580,6 +604,7 @@ private:
WorldEntryCallback worldEntryCallback_;
CreatureSpawnCallback creatureSpawnCallback_;
CreatureDespawnCallback creatureDespawnCallback_;
CreatureMoveCallback creatureMoveCallback_;
std::vector<uint32_t> knownSpells;
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
uint8_t castCount = 0;
@ -616,6 +641,9 @@ private:
bool questDetailsOpen = false;
QuestDetailsData currentQuestDetails;
// Quest log
std::vector<QuestLogEntry> questLog_;
// Vendor
bool vendorWindowOpen = false;
ListInventoryData currentVendorItems;

View file

@ -68,6 +68,9 @@ enum class Opcode : uint16_t {
// ---- XP ----
SMSG_LOG_XPGAIN = 0x1D0,
// ---- Creature Movement ----
SMSG_MONSTER_MOVE = 0x0DD,
// ---- Phase 2: Combat Core ----
CMSG_ATTACKSWING = 0x141,
CMSG_ATTACKSTOP = 0x142,
@ -146,6 +149,7 @@ enum class Opcode : uint16_t {
SMSG_QUESTGIVER_OFFER_REWARD = 0x18D,
CMSG_QUESTGIVER_CHOOSE_REWARD = 0x18E,
SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191,
CMSG_QUESTLOG_REMOVE_QUEST = 0x194,
// ---- Phase 5: Vendor ----
CMSG_LIST_INVENTORY = 0x19E,

View file

@ -740,6 +740,7 @@ struct ItemQueryResponseData {
int32_t agility = 0;
int32_t intellect = 0;
int32_t spirit = 0;
uint32_t sellPrice = 0;
std::string subclassName;
bool valid = false;
};
@ -754,6 +755,25 @@ public:
// 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 */
class AttackSwingPacket {
public:

View file

@ -192,6 +192,11 @@ private:
float introEndPitch = -5.0f;
float introStartDistance = 12.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

View file

@ -61,6 +61,9 @@ public:
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
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 setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId);
void setInstanceVisible(uint32_t instanceId, bool visible);
@ -130,6 +133,18 @@ private:
// Weapon attachments (weapons parented to this instance's bones)
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)
bool hasOverrideModelMatrix = false;
glm::mat4 overrideModelMatrix{1.0f};

View file

@ -123,6 +123,10 @@ public:
bool isMoving() const;
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).
double getLastUpdateMs() const { return lastUpdateMs; }
double getLastRenderMs() const { return lastRenderMs; }
@ -224,6 +228,18 @@ private:
// Target facing
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)
uint32_t footstepLastAnimationId = 0;
float footstepLastNormTime = 0.0f;

View file

@ -4,6 +4,7 @@
#include "game/inventory.hpp"
#include "rendering/world_map.hpp"
#include "ui/inventory_screen.hpp"
#include "ui/quest_log_screen.hpp"
#include "ui/spellbook_screen.hpp"
#include <imgui.h>
#include <string>
@ -143,6 +144,7 @@ private:
void renderWorldMap(game::GameHandler& gameHandler);
InventoryScreen inventoryScreen;
QuestLogScreen questLogScreen;
SpellbookScreen spellbookScreen;
rendering::WorldMap worldMap;

View file

@ -1,18 +1,36 @@
#pragma once
#include "game/inventory.hpp"
#include "game/world_packets.hpp"
#include <imgui.h>
#include <functional>
namespace wowee {
namespace game { class GameHandler; }
namespace ui {
class InventoryScreen {
public:
/// Render bags window (B key). Positioned at bottom of screen.
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; }
void toggle() { open = !open; }
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.
bool consumeEquipmentDirty() { bool d = equipmentDirty; equipmentDirty = false; return d; }
/// Returns true if any inventory slot changed since last call, and clears the flag.
@ -20,10 +38,16 @@ public:
private:
bool open = false;
bool characterOpen = false;
bool bKeyWasDown = false;
bool cKeyWasDown = false;
bool equipmentDirty = false;
bool inventoryDirty = false;
// Vendor sell mode
bool vendorMode_ = false;
game::GameHandler* gameHandler_ = nullptr;
// Drag-and-drop held item state
bool holdingItem = false;
game::ItemDef heldItem;

View 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