Emulate server loot/xp and combat feedback in single-player

This commit is contained in:
Kelsi 2026-02-05 14:01:26 -08:00
parent 1383e6c159
commit 3ca8944ced
17 changed files with 830 additions and 29 deletions

View file

@ -29,6 +29,7 @@ public:
void setCharacterVoiceProfile(const std::string& modelName);
void playWaterEnter();
void playWaterExit();
void playMeleeSwing();
private:
struct Sample {
@ -48,6 +49,7 @@ private:
std::vector<Sample> splashExitClips;
std::vector<Sample> swimLoopClips;
std::vector<Sample> hardLandClips;
std::vector<Sample> meleeSwingClips;
std::array<SurfaceLandingSet, 7> landingSets;
bool swimmingActive = false;
@ -61,6 +63,8 @@ private:
std::chrono::steady_clock::time_point lastJumpAt{};
std::chrono::steady_clock::time_point lastLandAt{};
std::chrono::steady_clock::time_point lastSplashAt{};
std::chrono::steady_clock::time_point lastMeleeSwingAt{};
bool meleeSwingWarned = false;
std::string voiceProfileKey;
void preloadCandidates(std::vector<Sample>& out, const std::vector<std::string>& candidates);

View file

@ -159,6 +159,9 @@ public:
*/
void addLocalChatMessage(const MessageChatData& msg);
// Money (copper)
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
// Inventory
Inventory& getInventory() { return inventory; }
const Inventory& getInventory() const { return inventory; }
@ -212,6 +215,10 @@ public:
using NpcDeathCallback = std::function<void(uint64_t guid)>;
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
// Melee swing callback (for driving animation/SFX)
using MeleeSwingCallback = std::function<void()>;
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
// Local player stats (single-player)
uint32_t getLocalPlayerHealth() const { return localPlayerHealth_; }
uint32_t getLocalPlayerMaxHealth() const { return localPlayerMaxHealth_; }
@ -378,6 +385,12 @@ private:
void handleGossipMessage(network::Packet& packet);
void handleGossipComplete(network::Packet& packet);
void handleListInventory(network::Packet& packet);
LootResponseData generateLocalLoot(uint64_t guid);
void simulateLootResponse(const LootResponseData& data);
void simulateLootRelease();
void simulateLootRemove(uint8_t slotIndex);
void simulateXpGain(uint64_t victimGuid, uint32_t totalXp);
void addMoneyCopper(uint32_t amount);
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource);
void addSystemChatMessage(const std::string& message);
@ -484,6 +497,12 @@ private:
// ---- Phase 5: Loot ----
bool lootWindowOpen = false;
LootResponseData currentLoot;
struct LocalLootState {
LootResponseData data;
bool moneyTaken = false;
};
std::unordered_map<uint64_t, LocalLootState> localLootState_;
uint64_t playerMoneyCopper_ = 0;
// Gossip
bool gossipWindowOpen = false;
@ -501,7 +520,7 @@ private:
uint32_t playerXp_ = 0;
uint32_t playerNextLevelXp_ = 0;
uint32_t serverPlayerLevel_ = 1;
void awardLocalXp(uint32_t victimLevel);
void awardLocalXp(uint64_t victimGuid, uint32_t victimLevel);
void levelUp();
static uint32_t xpForLevel(uint32_t level);
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
@ -511,6 +530,7 @@ private:
float swingTimer_ = 0.0f;
static constexpr float SWING_SPEED = 2.0f;
NpcDeathCallback npcDeathCallback_;
MeleeSwingCallback meleeSwingCallback_;
uint32_t localPlayerHealth_ = 0;
uint32_t localPlayerMaxHealth_ = 0;
uint32_t localPlayerLevel_ = 1;

View file

@ -16,6 +16,7 @@ class EntityManager;
struct NpcSpawnDef {
std::string mapName;
uint32_t entry = 0;
std::string name;
std::string m2Path;
uint32_t level;

View file

@ -17,6 +17,7 @@ public:
void writeUInt16(uint16_t value);
void writeUInt32(uint32_t value);
void writeUInt64(uint64_t value);
void writeFloat(float value);
void writeString(const std::string& value);
void writeBytes(const uint8_t* data, size_t length);

View file

@ -66,6 +66,7 @@ public:
void removeInstance(uint32_t instanceId);
bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const;
bool hasAnimation(uint32_t instanceId, uint32_t animationId) const;
bool getAnimationSequences(uint32_t instanceId, std::vector<pipeline::M2Sequence>& out) const;
bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const;
/** Attach a weapon model to a character instance at the given attachment point. */

View file

@ -121,6 +121,7 @@ public:
// Targeting support
void setTargetPosition(const glm::vec3* pos);
bool isMoving() const;
void triggerMeleeSwing();
// CPU timing stats (milliseconds, last frame).
double getLastUpdateMs() const { return lastUpdateMs; }
@ -198,12 +199,13 @@ private:
float characterYaw = 0.0f;
// Character animation state
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM };
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING };
CharAnimState charAnimState = CharAnimState::IDLE;
void updateCharacterAnimation();
bool isFootstepAnimationState() const;
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
audio::FootstepSurface resolveFootstepSurface() const;
uint32_t resolveMeleeAnimId();
// Emote state
bool emoteActive = false;
@ -223,6 +225,11 @@ private:
bool sfxPrevFalling = false;
bool sfxPrevSwimming = false;
float meleeSwingTimer = 0.0f;
float meleeSwingCooldown = 0.0f;
float meleeAnimDurationMs = 0.0f;
uint32_t meleeAnimId = 0;
bool terrainEnabled = true;
bool terrainLoaded = false;

View file

@ -8,7 +8,7 @@ namespace ui {
class InventoryScreen {
public:
void render(game::Inventory& inventory);
void render(game::Inventory& inventory, uint64_t moneyCopper);
bool isOpen() const { return open; }
void toggle() { open = !open; }
void setOpen(bool o) { open = o; }