mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Emulate server loot/xp and combat feedback in single-player
This commit is contained in:
parent
1383e6c159
commit
3ca8944ced
17 changed files with 830 additions and 29 deletions
|
|
@ -29,6 +29,7 @@ public:
|
||||||
void setCharacterVoiceProfile(const std::string& modelName);
|
void setCharacterVoiceProfile(const std::string& modelName);
|
||||||
void playWaterEnter();
|
void playWaterEnter();
|
||||||
void playWaterExit();
|
void playWaterExit();
|
||||||
|
void playMeleeSwing();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Sample {
|
struct Sample {
|
||||||
|
|
@ -48,6 +49,7 @@ private:
|
||||||
std::vector<Sample> splashExitClips;
|
std::vector<Sample> splashExitClips;
|
||||||
std::vector<Sample> swimLoopClips;
|
std::vector<Sample> swimLoopClips;
|
||||||
std::vector<Sample> hardLandClips;
|
std::vector<Sample> hardLandClips;
|
||||||
|
std::vector<Sample> meleeSwingClips;
|
||||||
std::array<SurfaceLandingSet, 7> landingSets;
|
std::array<SurfaceLandingSet, 7> landingSets;
|
||||||
|
|
||||||
bool swimmingActive = false;
|
bool swimmingActive = false;
|
||||||
|
|
@ -61,6 +63,8 @@ private:
|
||||||
std::chrono::steady_clock::time_point lastJumpAt{};
|
std::chrono::steady_clock::time_point lastJumpAt{};
|
||||||
std::chrono::steady_clock::time_point lastLandAt{};
|
std::chrono::steady_clock::time_point lastLandAt{};
|
||||||
std::chrono::steady_clock::time_point lastSplashAt{};
|
std::chrono::steady_clock::time_point lastSplashAt{};
|
||||||
|
std::chrono::steady_clock::time_point lastMeleeSwingAt{};
|
||||||
|
bool meleeSwingWarned = false;
|
||||||
std::string voiceProfileKey;
|
std::string voiceProfileKey;
|
||||||
|
|
||||||
void preloadCandidates(std::vector<Sample>& out, const std::vector<std::string>& candidates);
|
void preloadCandidates(std::vector<Sample>& out, const std::vector<std::string>& candidates);
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,9 @@ public:
|
||||||
*/
|
*/
|
||||||
void addLocalChatMessage(const MessageChatData& msg);
|
void addLocalChatMessage(const MessageChatData& msg);
|
||||||
|
|
||||||
|
// Money (copper)
|
||||||
|
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
Inventory& getInventory() { return inventory; }
|
Inventory& getInventory() { return inventory; }
|
||||||
const Inventory& getInventory() const { return inventory; }
|
const Inventory& getInventory() const { return inventory; }
|
||||||
|
|
@ -212,6 +215,10 @@ public:
|
||||||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
||||||
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
|
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)
|
// Local player stats (single-player)
|
||||||
uint32_t getLocalPlayerHealth() const { return localPlayerHealth_; }
|
uint32_t getLocalPlayerHealth() const { return localPlayerHealth_; }
|
||||||
uint32_t getLocalPlayerMaxHealth() const { return localPlayerMaxHealth_; }
|
uint32_t getLocalPlayerMaxHealth() const { return localPlayerMaxHealth_; }
|
||||||
|
|
@ -378,6 +385,12 @@ private:
|
||||||
void handleGossipMessage(network::Packet& packet);
|
void handleGossipMessage(network::Packet& packet);
|
||||||
void handleGossipComplete(network::Packet& packet);
|
void handleGossipComplete(network::Packet& packet);
|
||||||
void handleListInventory(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 addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource);
|
||||||
void addSystemChatMessage(const std::string& message);
|
void addSystemChatMessage(const std::string& message);
|
||||||
|
|
@ -484,6 +497,12 @@ private:
|
||||||
// ---- Phase 5: Loot ----
|
// ---- Phase 5: Loot ----
|
||||||
bool lootWindowOpen = false;
|
bool lootWindowOpen = false;
|
||||||
LootResponseData currentLoot;
|
LootResponseData currentLoot;
|
||||||
|
struct LocalLootState {
|
||||||
|
LootResponseData data;
|
||||||
|
bool moneyTaken = false;
|
||||||
|
};
|
||||||
|
std::unordered_map<uint64_t, LocalLootState> localLootState_;
|
||||||
|
uint64_t playerMoneyCopper_ = 0;
|
||||||
|
|
||||||
// Gossip
|
// Gossip
|
||||||
bool gossipWindowOpen = false;
|
bool gossipWindowOpen = false;
|
||||||
|
|
@ -501,7 +520,7 @@ private:
|
||||||
uint32_t playerXp_ = 0;
|
uint32_t playerXp_ = 0;
|
||||||
uint32_t playerNextLevelXp_ = 0;
|
uint32_t playerNextLevelXp_ = 0;
|
||||||
uint32_t serverPlayerLevel_ = 1;
|
uint32_t serverPlayerLevel_ = 1;
|
||||||
void awardLocalXp(uint32_t victimLevel);
|
void awardLocalXp(uint64_t victimGuid, uint32_t victimLevel);
|
||||||
void levelUp();
|
void levelUp();
|
||||||
static uint32_t xpForLevel(uint32_t level);
|
static uint32_t xpForLevel(uint32_t level);
|
||||||
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
||||||
|
|
@ -511,6 +530,7 @@ private:
|
||||||
float swingTimer_ = 0.0f;
|
float swingTimer_ = 0.0f;
|
||||||
static constexpr float SWING_SPEED = 2.0f;
|
static constexpr float SWING_SPEED = 2.0f;
|
||||||
NpcDeathCallback npcDeathCallback_;
|
NpcDeathCallback npcDeathCallback_;
|
||||||
|
MeleeSwingCallback meleeSwingCallback_;
|
||||||
uint32_t localPlayerHealth_ = 0;
|
uint32_t localPlayerHealth_ = 0;
|
||||||
uint32_t localPlayerMaxHealth_ = 0;
|
uint32_t localPlayerMaxHealth_ = 0;
|
||||||
uint32_t localPlayerLevel_ = 1;
|
uint32_t localPlayerLevel_ = 1;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class EntityManager;
|
||||||
|
|
||||||
struct NpcSpawnDef {
|
struct NpcSpawnDef {
|
||||||
std::string mapName;
|
std::string mapName;
|
||||||
|
uint32_t entry = 0;
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string m2Path;
|
std::string m2Path;
|
||||||
uint32_t level;
|
uint32_t level;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ public:
|
||||||
void writeUInt16(uint16_t value);
|
void writeUInt16(uint16_t value);
|
||||||
void writeUInt32(uint32_t value);
|
void writeUInt32(uint32_t value);
|
||||||
void writeUInt64(uint64_t value);
|
void writeUInt64(uint64_t value);
|
||||||
|
void writeFloat(float value);
|
||||||
void writeString(const std::string& value);
|
void writeString(const std::string& value);
|
||||||
void writeBytes(const uint8_t* data, size_t length);
|
void writeBytes(const uint8_t* data, size_t length);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ public:
|
||||||
void removeInstance(uint32_t instanceId);
|
void removeInstance(uint32_t instanceId);
|
||||||
bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const;
|
bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const;
|
||||||
bool hasAnimation(uint32_t instanceId, uint32_t animationId) 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;
|
bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const;
|
||||||
|
|
||||||
/** Attach a weapon model to a character instance at the given attachment point. */
|
/** Attach a weapon model to a character instance at the given attachment point. */
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ public:
|
||||||
// Targeting support
|
// Targeting support
|
||||||
void setTargetPosition(const glm::vec3* pos);
|
void setTargetPosition(const glm::vec3* pos);
|
||||||
bool isMoving() const;
|
bool isMoving() const;
|
||||||
|
void triggerMeleeSwing();
|
||||||
|
|
||||||
// CPU timing stats (milliseconds, last frame).
|
// CPU timing stats (milliseconds, last frame).
|
||||||
double getLastUpdateMs() const { return lastUpdateMs; }
|
double getLastUpdateMs() const { return lastUpdateMs; }
|
||||||
|
|
@ -198,12 +199,13 @@ private:
|
||||||
float characterYaw = 0.0f;
|
float characterYaw = 0.0f;
|
||||||
|
|
||||||
// Character animation state
|
// 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;
|
CharAnimState charAnimState = CharAnimState::IDLE;
|
||||||
void updateCharacterAnimation();
|
void updateCharacterAnimation();
|
||||||
bool isFootstepAnimationState() const;
|
bool isFootstepAnimationState() const;
|
||||||
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
|
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
|
||||||
audio::FootstepSurface resolveFootstepSurface() const;
|
audio::FootstepSurface resolveFootstepSurface() const;
|
||||||
|
uint32_t resolveMeleeAnimId();
|
||||||
|
|
||||||
// Emote state
|
// Emote state
|
||||||
bool emoteActive = false;
|
bool emoteActive = false;
|
||||||
|
|
@ -223,6 +225,11 @@ private:
|
||||||
bool sfxPrevFalling = false;
|
bool sfxPrevFalling = false;
|
||||||
bool sfxPrevSwimming = false;
|
bool sfxPrevSwimming = false;
|
||||||
|
|
||||||
|
float meleeSwingTimer = 0.0f;
|
||||||
|
float meleeSwingCooldown = 0.0f;
|
||||||
|
float meleeAnimDurationMs = 0.0f;
|
||||||
|
uint32_t meleeAnimId = 0;
|
||||||
|
|
||||||
bool terrainEnabled = true;
|
bool terrainEnabled = true;
|
||||||
bool terrainLoaded = false;
|
bool terrainLoaded = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace ui {
|
||||||
|
|
||||||
class InventoryScreen {
|
class InventoryScreen {
|
||||||
public:
|
public:
|
||||||
void render(game::Inventory& inventory);
|
void render(game::Inventory& inventory, uint64_t moneyCopper);
|
||||||
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; }
|
||||||
|
|
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "wowee",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,21 @@ bool ActivitySoundManager::initialize(pipeline::AssetManager* assets) {
|
||||||
preloadLandingSet(FootstepSurface::WATER, "Water");
|
preloadLandingSet(FootstepSurface::WATER, "Water");
|
||||||
preloadLandingSet(FootstepSurface::SNOW, "Snow");
|
preloadLandingSet(FootstepSurface::SNOW, "Snow");
|
||||||
|
|
||||||
|
preloadCandidates(meleeSwingClips, {
|
||||||
|
"Sound\\Item\\Weapons\\Sword\\SwordSwing1.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Sword\\SwordSwing2.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Sword\\SwordSwing3.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Sword\\SwordHit1.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Sword\\SwordHit2.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Sword\\SwordHit3.wav",
|
||||||
|
"Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing1.wav",
|
||||||
|
"Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing2.wav",
|
||||||
|
"Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing3.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Melee\\MeleeSwing1.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Melee\\MeleeSwing2.wav",
|
||||||
|
"Sound\\Item\\Weapons\\Melee\\MeleeSwing3.wav"
|
||||||
|
});
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
core::Logger::getInstance().info("Activity SFX loaded: jump=", jumpClips.size(),
|
core::Logger::getInstance().info("Activity SFX loaded: jump=", jumpClips.size(),
|
||||||
" splash=", splashEnterClips.size(),
|
" splash=", splashEnterClips.size(),
|
||||||
|
|
@ -71,6 +86,7 @@ void ActivitySoundManager::shutdown() {
|
||||||
splashExitClips.clear();
|
splashExitClips.clear();
|
||||||
swimLoopClips.clear();
|
swimLoopClips.clear();
|
||||||
hardLandClips.clear();
|
hardLandClips.clear();
|
||||||
|
meleeSwingClips.clear();
|
||||||
swimmingActive = false;
|
swimmingActive = false;
|
||||||
swimMoving = false;
|
swimMoving = false;
|
||||||
initialized = false;
|
initialized = false;
|
||||||
|
|
@ -275,6 +291,23 @@ void ActivitySoundManager::playLanding(FootstepSurface surface, bool hardLanding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ActivitySoundManager::playMeleeSwing() {
|
||||||
|
if (meleeSwingClips.empty()) {
|
||||||
|
if (!meleeSwingWarned) {
|
||||||
|
core::Logger::getInstance().warning("No melee swing SFX found in assets");
|
||||||
|
meleeSwingWarned = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
if (lastMeleeSwingAt.time_since_epoch().count() != 0) {
|
||||||
|
if (std::chrono::duration<float>(now - lastMeleeSwingAt).count() < 0.12f) return;
|
||||||
|
}
|
||||||
|
if (playOneShot(meleeSwingClips, 0.80f, 0.96f, 1.04f)) {
|
||||||
|
lastMeleeSwingAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ActivitySoundManager::setSwimmingState(bool swimming, bool moving) {
|
void ActivitySoundManager::setSwimmingState(bool swimming, bool moving) {
|
||||||
swimMoving = moving;
|
swimMoving = moving;
|
||||||
if (swimming == swimmingActive) return;
|
if (swimming == swimmingActive) return;
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,13 @@ void Application::setState(AppState newState) {
|
||||||
// Keep player locomotion WoW-like in both single-player and online modes.
|
// Keep player locomotion WoW-like in both single-player and online modes.
|
||||||
cc->setUseWoWSpeed(true);
|
cc->setUseWoWSpeed(true);
|
||||||
}
|
}
|
||||||
|
if (gameHandler) {
|
||||||
|
gameHandler->setMeleeSwingCallback([this]() {
|
||||||
|
if (renderer) {
|
||||||
|
renderer->triggerMeleeSwing();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case AppState::DISCONNECTED:
|
case AppState::DISCONNECTED:
|
||||||
// Back to auth
|
// Back to auth
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,322 @@
|
||||||
#include "core/coordinates.hpp"
|
#include "core/coordinates.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <functional>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace game {
|
namespace game {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct LootEntryRow {
|
||||||
|
uint32_t item = 0;
|
||||||
|
float chance = 0.0f;
|
||||||
|
uint16_t lootmode = 0;
|
||||||
|
uint8_t groupid = 0;
|
||||||
|
int32_t mincountOrRef = 0;
|
||||||
|
uint8_t maxcount = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CreatureTemplateRow {
|
||||||
|
uint32_t lootId = 0;
|
||||||
|
uint32_t minGold = 0;
|
||||||
|
uint32_t maxGold = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ItemTemplateRow {
|
||||||
|
uint32_t itemId = 0;
|
||||||
|
std::string name;
|
||||||
|
uint32_t displayId = 0;
|
||||||
|
uint8_t quality = 0;
|
||||||
|
uint8_t inventoryType = 0;
|
||||||
|
int32_t maxStack = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SinglePlayerLootDb {
|
||||||
|
bool loaded = false;
|
||||||
|
std::string basePath;
|
||||||
|
std::unordered_map<uint32_t, CreatureTemplateRow> creatureTemplates;
|
||||||
|
std::unordered_map<uint32_t, std::vector<LootEntryRow>> creatureLoot;
|
||||||
|
std::unordered_map<uint32_t, std::vector<LootEntryRow>> referenceLoot;
|
||||||
|
std::unordered_map<uint32_t, ItemTemplateRow> itemTemplates;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string trimSql(const std::string& s) {
|
||||||
|
size_t b = 0;
|
||||||
|
while (b < s.size() && std::isspace(static_cast<unsigned char>(s[b]))) b++;
|
||||||
|
size_t e = s.size();
|
||||||
|
while (e > b && std::isspace(static_cast<unsigned char>(s[e - 1]))) e--;
|
||||||
|
return s.substr(b, e - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parseInsertTuples(const std::string& line, std::vector<std::string>& outTuples) {
|
||||||
|
outTuples.clear();
|
||||||
|
size_t valuesPos = line.find("VALUES");
|
||||||
|
if (valuesPos == std::string::npos) valuesPos = line.find("values");
|
||||||
|
if (valuesPos == std::string::npos) return false;
|
||||||
|
|
||||||
|
bool inQuote = false;
|
||||||
|
int depth = 0;
|
||||||
|
size_t tupleStart = std::string::npos;
|
||||||
|
for (size_t i = valuesPos; i < line.size(); i++) {
|
||||||
|
char c = line[i];
|
||||||
|
if (c == '\'' && (i == 0 || line[i - 1] != '\\')) inQuote = !inQuote;
|
||||||
|
if (inQuote) continue;
|
||||||
|
if (c == '(') {
|
||||||
|
if (depth == 0) tupleStart = i + 1;
|
||||||
|
depth++;
|
||||||
|
} else if (c == ')') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0 && tupleStart != std::string::npos && i > tupleStart) {
|
||||||
|
outTuples.push_back(line.substr(tupleStart, i - tupleStart));
|
||||||
|
tupleStart = std::string::npos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !outTuples.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> splitCsvTuple(const std::string& tuple) {
|
||||||
|
std::vector<std::string> cols;
|
||||||
|
std::string cur;
|
||||||
|
bool inQuote = false;
|
||||||
|
for (size_t i = 0; i < tuple.size(); i++) {
|
||||||
|
char c = tuple[i];
|
||||||
|
if (c == '\'' && (i == 0 || tuple[i - 1] != '\\')) {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
cur.push_back(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == ',' && !inQuote) {
|
||||||
|
cols.push_back(trimSql(cur));
|
||||||
|
cur.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur.push_back(c);
|
||||||
|
}
|
||||||
|
if (!cur.empty()) cols.push_back(trimSql(cur));
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string unquoteSqlString(const std::string& s) {
|
||||||
|
if (s.size() >= 2 && s.front() == '\'' && s.back() == '\'') {
|
||||||
|
return s.substr(1, s.size() - 2);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> loadCreateTableColumns(const std::filesystem::path& path) {
|
||||||
|
std::vector<std::string> columns;
|
||||||
|
std::ifstream in(path);
|
||||||
|
if (!in) return columns;
|
||||||
|
std::string line;
|
||||||
|
bool inCreate = false;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
if (!inCreate) {
|
||||||
|
if (line.find("CREATE TABLE") != std::string::npos ||
|
||||||
|
line.find("create table") != std::string::npos) {
|
||||||
|
inCreate = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto trimmed = trimSql(line);
|
||||||
|
if (trimmed.empty()) continue;
|
||||||
|
if (trimmed[0] == ')') break;
|
||||||
|
size_t b = trimmed.find('`');
|
||||||
|
if (b == std::string::npos) continue;
|
||||||
|
size_t e = trimmed.find('`', b + 1);
|
||||||
|
if (e == std::string::npos) continue;
|
||||||
|
columns.push_back(trimmed.substr(b + 1, e - b - 1));
|
||||||
|
}
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int columnIndex(const std::vector<std::string>& cols, const std::string& name) {
|
||||||
|
for (size_t i = 0; i < cols.size(); i++) {
|
||||||
|
if (cols[i] == name) return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::filesystem::path resolveDbBasePath() {
|
||||||
|
if (const char* dbBase = std::getenv("WOW_DB_BASE_PATH")) {
|
||||||
|
std::filesystem::path base(dbBase);
|
||||||
|
if (std::filesystem::exists(base)) return base;
|
||||||
|
}
|
||||||
|
if (std::filesystem::exists("assets/sql")) {
|
||||||
|
return std::filesystem::path("assets/sql");
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void processInsertStatements(
|
||||||
|
std::ifstream& in,
|
||||||
|
const std::function<void(const std::vector<std::string>&)>& onTuple) {
|
||||||
|
std::string line;
|
||||||
|
std::string stmt;
|
||||||
|
std::vector<std::string> tuples;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
if (stmt.empty()) {
|
||||||
|
if (line.find("INSERT INTO") == std::string::npos &&
|
||||||
|
line.find("insert into") == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!stmt.empty()) stmt.push_back('\n');
|
||||||
|
stmt += line;
|
||||||
|
if (line.find(';') == std::string::npos) continue;
|
||||||
|
|
||||||
|
if (parseInsertTuples(stmt, tuples)) {
|
||||||
|
for (const auto& t : tuples) {
|
||||||
|
onTuple(splitCsvTuple(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stmt.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SinglePlayerLootDb& getSinglePlayerLootDb() {
|
||||||
|
static SinglePlayerLootDb db;
|
||||||
|
if (db.loaded) return db;
|
||||||
|
|
||||||
|
auto base = resolveDbBasePath();
|
||||||
|
if (base.empty()) {
|
||||||
|
db.loaded = true;
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path basePath = base;
|
||||||
|
std::filesystem::path creatureTemplatePath = basePath / "creature_template.sql";
|
||||||
|
std::filesystem::path creatureLootPath = basePath / "creature_loot_template.sql";
|
||||||
|
std::filesystem::path referenceLootPath = basePath / "reference_loot_template.sql";
|
||||||
|
std::filesystem::path itemTemplatePath = basePath / "item_template.sql";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(creatureTemplatePath)) {
|
||||||
|
auto alt = basePath / "base";
|
||||||
|
if (std::filesystem::exists(alt / "creature_template.sql")) {
|
||||||
|
basePath = alt;
|
||||||
|
creatureTemplatePath = basePath / "creature_template.sql";
|
||||||
|
creatureLootPath = basePath / "creature_loot_template.sql";
|
||||||
|
referenceLootPath = basePath / "reference_loot_template.sql";
|
||||||
|
itemTemplatePath = basePath / "item_template.sql";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.basePath = basePath.string();
|
||||||
|
|
||||||
|
// creature_template: entry, lootid, mingold, maxgold
|
||||||
|
{
|
||||||
|
auto cols = loadCreateTableColumns(creatureTemplatePath);
|
||||||
|
int idxEntry = columnIndex(cols, "entry");
|
||||||
|
int idxLoot = columnIndex(cols, "lootid");
|
||||||
|
int idxMinGold = columnIndex(cols, "mingold");
|
||||||
|
int idxMaxGold = columnIndex(cols, "maxgold");
|
||||||
|
if (idxEntry >= 0 && std::filesystem::exists(creatureTemplatePath)) {
|
||||||
|
std::ifstream in(creatureTemplatePath);
|
||||||
|
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||||
|
if (idxEntry >= static_cast<int>(row.size())) return;
|
||||||
|
try {
|
||||||
|
uint32_t entry = static_cast<uint32_t>(std::stoul(row[idxEntry]));
|
||||||
|
CreatureTemplateRow tr;
|
||||||
|
if (idxLoot >= 0 && idxLoot < static_cast<int>(row.size())) {
|
||||||
|
tr.lootId = static_cast<uint32_t>(std::stoul(row[idxLoot]));
|
||||||
|
}
|
||||||
|
if (idxMinGold >= 0 && idxMinGold < static_cast<int>(row.size())) {
|
||||||
|
tr.minGold = static_cast<uint32_t>(std::stoul(row[idxMinGold]));
|
||||||
|
}
|
||||||
|
if (idxMaxGold >= 0 && idxMaxGold < static_cast<int>(row.size())) {
|
||||||
|
tr.maxGold = static_cast<uint32_t>(std::stoul(row[idxMaxGold]));
|
||||||
|
}
|
||||||
|
db.creatureTemplates[entry] = tr;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto loadLootTable = [&](const std::filesystem::path& path,
|
||||||
|
std::unordered_map<uint32_t, std::vector<LootEntryRow>>& out) {
|
||||||
|
if (!std::filesystem::exists(path)) return;
|
||||||
|
std::ifstream in(path);
|
||||||
|
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||||
|
if (row.size() < 7) return;
|
||||||
|
try {
|
||||||
|
uint32_t entry = static_cast<uint32_t>(std::stoul(row[0]));
|
||||||
|
LootEntryRow lr;
|
||||||
|
lr.item = static_cast<uint32_t>(std::stoul(row[1]));
|
||||||
|
lr.chance = std::stof(row[2]);
|
||||||
|
lr.lootmode = static_cast<uint16_t>(std::stoul(row[3]));
|
||||||
|
lr.groupid = static_cast<uint8_t>(std::stoul(row[4]));
|
||||||
|
lr.mincountOrRef = static_cast<int32_t>(std::stol(row[5]));
|
||||||
|
lr.maxcount = static_cast<uint8_t>(std::stoul(row[6]));
|
||||||
|
out[entry].push_back(lr);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLootTable(creatureLootPath, db.creatureLoot);
|
||||||
|
loadLootTable(referenceLootPath, db.referenceLoot);
|
||||||
|
|
||||||
|
// item_template
|
||||||
|
{
|
||||||
|
auto cols = loadCreateTableColumns(itemTemplatePath);
|
||||||
|
int idxEntry = columnIndex(cols, "entry");
|
||||||
|
int idxName = columnIndex(cols, "name");
|
||||||
|
int idxDisplay = columnIndex(cols, "displayid");
|
||||||
|
int idxQuality = columnIndex(cols, "Quality");
|
||||||
|
int idxInvType = columnIndex(cols, "InventoryType");
|
||||||
|
int idxStack = columnIndex(cols, "stackable");
|
||||||
|
if (idxEntry >= 0 && std::filesystem::exists(itemTemplatePath)) {
|
||||||
|
std::ifstream in(itemTemplatePath);
|
||||||
|
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||||
|
if (idxEntry >= static_cast<int>(row.size())) return;
|
||||||
|
try {
|
||||||
|
ItemTemplateRow ir;
|
||||||
|
ir.itemId = static_cast<uint32_t>(std::stoul(row[idxEntry]));
|
||||||
|
if (idxName >= 0 && idxName < static_cast<int>(row.size())) {
|
||||||
|
ir.name = unquoteSqlString(row[idxName]);
|
||||||
|
}
|
||||||
|
if (idxDisplay >= 0 && idxDisplay < static_cast<int>(row.size())) {
|
||||||
|
ir.displayId = static_cast<uint32_t>(std::stoul(row[idxDisplay]));
|
||||||
|
}
|
||||||
|
if (idxQuality >= 0 && idxQuality < static_cast<int>(row.size())) {
|
||||||
|
ir.quality = static_cast<uint8_t>(std::stoul(row[idxQuality]));
|
||||||
|
}
|
||||||
|
if (idxInvType >= 0 && idxInvType < static_cast<int>(row.size())) {
|
||||||
|
ir.inventoryType = static_cast<uint8_t>(std::stoul(row[idxInvType]));
|
||||||
|
}
|
||||||
|
if (idxStack >= 0 && idxStack < static_cast<int>(row.size())) {
|
||||||
|
ir.maxStack = static_cast<int32_t>(std::stol(row[idxStack]));
|
||||||
|
if (ir.maxStack <= 0) ir.maxStack = 1;
|
||||||
|
}
|
||||||
|
db.itemTemplates[ir.itemId] = std::move(ir);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.loaded = true;
|
||||||
|
LOG_INFO("Single-player loot DB loaded from ", db.basePath,
|
||||||
|
" (creatures=", db.creatureTemplates.size(),
|
||||||
|
", loot=", db.creatureLoot.size(),
|
||||||
|
", reference=", db.referenceLoot.size(),
|
||||||
|
", items=", db.itemTemplates.size(), ")");
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
GameHandler::GameHandler() {
|
GameHandler::GameHandler() {
|
||||||
LOG_DEBUG("GameHandler created");
|
LOG_DEBUG("GameHandler created");
|
||||||
|
|
||||||
|
|
@ -1218,6 +1527,9 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
||||||
|
|
||||||
bool isPlayerAttacker = (data.attackerGuid == playerGuid);
|
bool isPlayerAttacker = (data.attackerGuid == playerGuid);
|
||||||
bool isPlayerTarget = (data.targetGuid == playerGuid);
|
bool isPlayerTarget = (data.targetGuid == playerGuid);
|
||||||
|
if (isPlayerAttacker && meleeSwingCallback_) {
|
||||||
|
meleeSwingCallback_();
|
||||||
|
}
|
||||||
|
|
||||||
if (data.isMiss()) {
|
if (data.isMiss()) {
|
||||||
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker);
|
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker);
|
||||||
|
|
@ -1558,12 +1870,71 @@ void GameHandler::handlePartyCommandResult(network::Packet& packet) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void GameHandler::lootTarget(uint64_t guid) {
|
void GameHandler::lootTarget(uint64_t guid) {
|
||||||
|
if (singlePlayerMode_) {
|
||||||
|
auto entity = entityManager.getEntity(guid);
|
||||||
|
if (!entity || entity->getType() != ObjectType::UNIT) return;
|
||||||
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
|
if (unit->getHealth() != 0) return;
|
||||||
|
|
||||||
|
auto it = localLootState_.find(guid);
|
||||||
|
if (it == localLootState_.end()) {
|
||||||
|
LocalLootState state;
|
||||||
|
state.data = generateLocalLoot(guid);
|
||||||
|
it = localLootState_.emplace(guid, std::move(state)).first;
|
||||||
|
}
|
||||||
|
simulateLootResponse(it->second.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state != WorldState::IN_WORLD || !socket) return;
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
auto packet = LootPacket::build(guid);
|
auto packet = LootPacket::build(guid);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::lootItem(uint8_t slotIndex) {
|
void GameHandler::lootItem(uint8_t slotIndex) {
|
||||||
|
if (singlePlayerMode_) {
|
||||||
|
if (!lootWindowOpen) return;
|
||||||
|
auto it = std::find_if(currentLoot.items.begin(), currentLoot.items.end(),
|
||||||
|
[slotIndex](const LootItem& item) { return item.slotIndex == slotIndex; });
|
||||||
|
if (it == currentLoot.items.end()) return;
|
||||||
|
|
||||||
|
auto& db = getSinglePlayerLootDb();
|
||||||
|
ItemDef def;
|
||||||
|
def.itemId = it->itemId;
|
||||||
|
def.stackCount = it->count;
|
||||||
|
def.maxStack = it->count;
|
||||||
|
|
||||||
|
auto itTpl = db.itemTemplates.find(it->itemId);
|
||||||
|
if (itTpl != db.itemTemplates.end()) {
|
||||||
|
def.name = itTpl->second.name.empty()
|
||||||
|
? ("Item " + std::to_string(it->itemId))
|
||||||
|
: itTpl->second.name;
|
||||||
|
def.quality = static_cast<ItemQuality>(itTpl->second.quality);
|
||||||
|
def.inventoryType = itTpl->second.inventoryType;
|
||||||
|
def.maxStack = std::max(def.maxStack, static_cast<uint32_t>(itTpl->second.maxStack));
|
||||||
|
} else {
|
||||||
|
def.name = "Item " + std::to_string(it->itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inventory.addItem(def)) {
|
||||||
|
simulateLootRemove(slotIndex);
|
||||||
|
addSystemChatMessage("You receive item: " + def.name + " x" + std::to_string(def.stackCount) + ".");
|
||||||
|
if (currentLoot.lootGuid != 0) {
|
||||||
|
auto st = localLootState_.find(currentLoot.lootGuid);
|
||||||
|
if (st != localLootState_.end()) {
|
||||||
|
auto& items = st->second.data.items;
|
||||||
|
items.erase(std::remove_if(items.begin(), items.end(),
|
||||||
|
[slotIndex](const LootItem& item) {
|
||||||
|
return item.slotIndex == slotIndex;
|
||||||
|
}),
|
||||||
|
items.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addSystemChatMessage("Inventory is full.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state != WorldState::IN_WORLD || !socket) return;
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
auto packet = AutostoreLootItemPacket::build(slotIndex);
|
auto packet = AutostoreLootItemPacket::build(slotIndex);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
|
|
@ -1572,6 +1943,19 @@ void GameHandler::lootItem(uint8_t slotIndex) {
|
||||||
void GameHandler::closeLoot() {
|
void GameHandler::closeLoot() {
|
||||||
if (!lootWindowOpen) return;
|
if (!lootWindowOpen) return;
|
||||||
lootWindowOpen = false;
|
lootWindowOpen = false;
|
||||||
|
if (singlePlayerMode_ && currentLoot.lootGuid != 0) {
|
||||||
|
auto st = localLootState_.find(currentLoot.lootGuid);
|
||||||
|
if (st != localLootState_.end()) {
|
||||||
|
if (!st->second.moneyTaken && st->second.data.gold > 0) {
|
||||||
|
addMoneyCopper(st->second.data.gold);
|
||||||
|
st->second.moneyTaken = true;
|
||||||
|
st->second.data.gold = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLoot.gold = 0;
|
||||||
|
simulateLootRelease();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state == WorldState::IN_WORLD && socket) {
|
if (state == WorldState::IN_WORLD && socket) {
|
||||||
auto packet = LootReleasePacket::build(currentLoot.lootGuid);
|
auto packet = LootReleasePacket::build(currentLoot.lootGuid);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
|
|
@ -1699,6 +2083,10 @@ void GameHandler::performPlayerSwing() {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
if (unit->getHealth() == 0) return;
|
if (unit->getHealth() == 0) return;
|
||||||
|
|
||||||
|
if (meleeSwingCallback_) {
|
||||||
|
meleeSwingCallback_();
|
||||||
|
}
|
||||||
|
|
||||||
// Aggro the target
|
// Aggro the target
|
||||||
aggroNpc(autoAttackTarget);
|
aggroNpc(autoAttackTarget);
|
||||||
|
|
||||||
|
|
@ -1737,7 +2125,7 @@ void GameHandler::handleNpcDeath(uint64_t guid) {
|
||||||
auto entity = entityManager.getEntity(guid);
|
auto entity = entityManager.getEntity(guid);
|
||||||
if (entity && entity->getType() == ObjectType::UNIT) {
|
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
awardLocalXp(unit->getLevel());
|
awardLocalXp(guid, unit->getLevel());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from aggro list
|
// Remove from aggro list
|
||||||
|
|
@ -1890,7 +2278,7 @@ uint32_t GameHandler::killXp(uint32_t playerLevel, uint32_t victimLevel) {
|
||||||
return static_cast<uint32_t>(baseXp * multiplier);
|
return static_cast<uint32_t>(baseXp * multiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::awardLocalXp(uint32_t victimLevel) {
|
void GameHandler::awardLocalXp(uint64_t victimGuid, uint32_t victimLevel) {
|
||||||
if (localPlayerLevel_ >= 80) return; // Level cap
|
if (localPlayerLevel_ >= 80) return; // Level cap
|
||||||
|
|
||||||
uint32_t xp = killXp(localPlayerLevel_, victimLevel);
|
uint32_t xp = killXp(localPlayerLevel_, victimLevel);
|
||||||
|
|
@ -1900,6 +2288,7 @@ void GameHandler::awardLocalXp(uint32_t victimLevel) {
|
||||||
|
|
||||||
// Show XP gain in combat text as a heal-type (gold text)
|
// Show XP gain in combat text as a heal-type (gold text)
|
||||||
addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(xp), 0, true);
|
addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(xp), 0, true);
|
||||||
|
simulateXpGain(victimGuid, xp);
|
||||||
|
|
||||||
LOG_INFO("XP gained: +", xp, " (total: ", playerXp_, "/", playerNextLevelXp_, ")");
|
LOG_INFO("XP gained: +", xp, " (total: ", playerXp_, "/", playerNextLevelXp_, ")");
|
||||||
|
|
||||||
|
|
@ -1945,6 +2334,174 @@ void GameHandler::handleXpGain(network::Packet& packet) {
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LootResponseData GameHandler::generateLocalLoot(uint64_t guid) {
|
||||||
|
LootResponseData data;
|
||||||
|
data.lootGuid = guid;
|
||||||
|
data.lootType = 0;
|
||||||
|
auto entity = entityManager.getEntity(guid);
|
||||||
|
if (!entity || entity->getType() != ObjectType::UNIT) return data;
|
||||||
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
|
uint32_t entry = unit->getEntry();
|
||||||
|
if (entry == 0) return data;
|
||||||
|
|
||||||
|
auto& db = getSinglePlayerLootDb();
|
||||||
|
|
||||||
|
uint32_t lootId = entry;
|
||||||
|
auto itTemplate = db.creatureTemplates.find(entry);
|
||||||
|
if (itTemplate != db.creatureTemplates.end()) {
|
||||||
|
if (itTemplate->second.lootId != 0) lootId = itTemplate->second.lootId;
|
||||||
|
if (itTemplate->second.maxGold > 0) {
|
||||||
|
std::uniform_int_distribution<uint32_t> goldDist(
|
||||||
|
itTemplate->second.minGold, itTemplate->second.maxGold);
|
||||||
|
static std::mt19937 rng(std::random_device{}());
|
||||||
|
data.gold = goldDist(rng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto itLoot = db.creatureLoot.find(lootId);
|
||||||
|
if (itLoot == db.creatureLoot.end() && lootId != entry) {
|
||||||
|
itLoot = db.creatureLoot.find(entry);
|
||||||
|
}
|
||||||
|
if (itLoot == db.creatureLoot.end()) return data;
|
||||||
|
|
||||||
|
std::unordered_map<uint8_t, std::vector<LootEntryRow>> groups;
|
||||||
|
std::vector<LootEntryRow> ungroupped;
|
||||||
|
for (const auto& row : itLoot->second) {
|
||||||
|
if (row.groupid == 0) {
|
||||||
|
ungroupped.push_back(row);
|
||||||
|
} else {
|
||||||
|
groups[row.groupid].push_back(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::mt19937 rng(std::random_device{}());
|
||||||
|
std::uniform_real_distribution<float> roll(0.0f, 100.0f);
|
||||||
|
|
||||||
|
auto addItem = [&](uint32_t itemId, uint32_t count) {
|
||||||
|
LootItem li;
|
||||||
|
li.slotIndex = static_cast<uint8_t>(data.items.size());
|
||||||
|
li.itemId = itemId;
|
||||||
|
li.count = count;
|
||||||
|
auto itItem = db.itemTemplates.find(itemId);
|
||||||
|
if (itItem != db.itemTemplates.end()) {
|
||||||
|
li.displayInfoId = itItem->second.displayId;
|
||||||
|
}
|
||||||
|
data.items.push_back(li);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::function<void(const std::vector<LootEntryRow>&, bool)> processLootTable;
|
||||||
|
processLootTable = [&](const std::vector<LootEntryRow>& rows, bool grouped) {
|
||||||
|
if (rows.empty()) return;
|
||||||
|
if (grouped) {
|
||||||
|
float total = 0.0f;
|
||||||
|
for (const auto& r : rows) total += std::abs(r.chance);
|
||||||
|
if (total <= 0.0f) return;
|
||||||
|
float r = roll(rng);
|
||||||
|
if (total < 100.0f && r > total) return;
|
||||||
|
float pick = (total < 100.0f)
|
||||||
|
? r
|
||||||
|
: std::uniform_real_distribution<float>(0.0f, total)(rng);
|
||||||
|
float acc = 0.0f;
|
||||||
|
for (const auto& row : rows) {
|
||||||
|
acc += std::abs(row.chance);
|
||||||
|
if (pick <= acc) {
|
||||||
|
if (row.mincountOrRef < 0) {
|
||||||
|
auto refIt = db.referenceLoot.find(static_cast<uint32_t>(-row.mincountOrRef));
|
||||||
|
if (refIt != db.referenceLoot.end()) {
|
||||||
|
processLootTable(refIt->second, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uint32_t minc = static_cast<uint32_t>(std::max(1, row.mincountOrRef));
|
||||||
|
uint32_t maxc = std::max(minc, static_cast<uint32_t>(row.maxcount));
|
||||||
|
std::uniform_int_distribution<uint32_t> cnt(minc, maxc);
|
||||||
|
addItem(row.item, cnt(rng));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& row : rows) {
|
||||||
|
float chance = std::abs(row.chance);
|
||||||
|
if (chance <= 0.0f) continue;
|
||||||
|
if (roll(rng) > chance) continue;
|
||||||
|
if (row.mincountOrRef < 0) {
|
||||||
|
auto refIt = db.referenceLoot.find(static_cast<uint32_t>(-row.mincountOrRef));
|
||||||
|
if (refIt != db.referenceLoot.end()) {
|
||||||
|
processLootTable(refIt->second, false);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint32_t minc = static_cast<uint32_t>(std::max(1, row.mincountOrRef));
|
||||||
|
uint32_t maxc = std::max(minc, static_cast<uint32_t>(row.maxcount));
|
||||||
|
std::uniform_int_distribution<uint32_t> cnt(minc, maxc);
|
||||||
|
addItem(row.item, cnt(rng));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processLootTable(ungroupped, false);
|
||||||
|
for (const auto& [gid, rows] : groups) {
|
||||||
|
processLootTable(rows, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::simulateLootResponse(const LootResponseData& data) {
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::SMSG_LOOT_RESPONSE));
|
||||||
|
packet.writeUInt64(data.lootGuid);
|
||||||
|
packet.writeUInt8(data.lootType);
|
||||||
|
packet.writeUInt32(data.gold);
|
||||||
|
packet.writeUInt8(static_cast<uint8_t>(data.items.size()));
|
||||||
|
for (const auto& item : data.items) {
|
||||||
|
packet.writeUInt8(item.slotIndex);
|
||||||
|
packet.writeUInt32(item.itemId);
|
||||||
|
packet.writeUInt32(item.count);
|
||||||
|
packet.writeUInt32(item.displayInfoId);
|
||||||
|
packet.writeUInt32(item.randomSuffix);
|
||||||
|
packet.writeUInt32(item.randomPropertyId);
|
||||||
|
packet.writeUInt8(item.lootSlotType);
|
||||||
|
}
|
||||||
|
handleLootResponse(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::simulateLootRelease() {
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::SMSG_LOOT_RELEASE_RESPONSE));
|
||||||
|
handleLootReleaseResponse(packet);
|
||||||
|
currentLoot = LootResponseData{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::simulateLootRemove(uint8_t slotIndex) {
|
||||||
|
if (!lootWindowOpen) return;
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::SMSG_LOOT_REMOVED));
|
||||||
|
packet.writeUInt8(slotIndex);
|
||||||
|
handleLootRemoved(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::simulateXpGain(uint64_t victimGuid, uint32_t totalXp) {
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::SMSG_LOG_XPGAIN));
|
||||||
|
packet.writeUInt64(victimGuid);
|
||||||
|
packet.writeUInt32(totalXp);
|
||||||
|
packet.writeUInt8(0); // kill XP
|
||||||
|
packet.writeFloat(0.0f);
|
||||||
|
packet.writeUInt32(0); // group bonus
|
||||||
|
handleXpGain(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::addMoneyCopper(uint32_t amount) {
|
||||||
|
if (amount == 0) return;
|
||||||
|
playerMoneyCopper_ += amount;
|
||||||
|
uint32_t gold = amount / 10000;
|
||||||
|
uint32_t silver = (amount / 100) % 100;
|
||||||
|
uint32_t copper = amount % 100;
|
||||||
|
std::string msg = "You receive ";
|
||||||
|
msg += std::to_string(gold) + "g ";
|
||||||
|
msg += std::to_string(silver) + "s ";
|
||||||
|
msg += std::to_string(copper) + "c.";
|
||||||
|
addSystemChatMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::addSystemChatMessage(const std::string& message) {
|
void GameHandler::addSystemChatMessage(const std::string& message) {
|
||||||
if (message.empty()) return;
|
if (message.empty()) return;
|
||||||
MessageChatData msg;
|
MessageChatData msg;
|
||||||
|
|
|
||||||
|
|
@ -594,19 +594,21 @@ std::vector<NpcSpawnDef> NpcManager::loadSpawnDefsFromAzerothCoreDb(
|
||||||
float dy = canonical.y - playerCanonical.y;
|
float dy = canonical.y - playerCanonical.y;
|
||||||
if (dx * dx + dy * dy > kRadius * kRadius) return true;
|
if (dx * dx + dy * dy > kRadius * kRadius) return true;
|
||||||
|
|
||||||
NpcSpawnDef def;
|
NpcSpawnDef def;
|
||||||
def.mapName = mapName;
|
def.mapName = mapName;
|
||||||
auto it = templates.find(entry);
|
auto it = templates.find(entry);
|
||||||
if (it != templates.end()) {
|
if (it != templates.end()) {
|
||||||
def.name = it->second.name;
|
def.entry = entry;
|
||||||
def.level = it->second.level;
|
def.name = it->second.name;
|
||||||
def.health = std::max(it->second.health, curhealth);
|
def.level = it->second.level;
|
||||||
def.m2Path = it->second.m2Path;
|
def.health = std::max(it->second.health, curhealth);
|
||||||
} else {
|
def.m2Path = it->second.m2Path;
|
||||||
def.name = "Creature " + std::to_string(entry);
|
} else {
|
||||||
def.level = 1;
|
def.entry = entry;
|
||||||
def.health = std::max(100u, curhealth);
|
def.name = "Creature " + std::to_string(entry);
|
||||||
}
|
def.level = 1;
|
||||||
|
def.health = std::max(100u, curhealth);
|
||||||
|
}
|
||||||
if (def.m2Path.empty()) {
|
if (def.m2Path.empty()) {
|
||||||
def.m2Path = "Creature\\HumanMalePeasant\\HumanMalePeasant.m2";
|
def.m2Path = "Creature\\HumanMalePeasant\\HumanMalePeasant.m2";
|
||||||
}
|
}
|
||||||
|
|
@ -672,21 +674,21 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
||||||
if (spawnDefs.empty()) {
|
if (spawnDefs.empty()) {
|
||||||
LOG_WARNING("NpcManager: using built-in NPC spawns (assets/npcs/singleplayer_spawns.csv missing)");
|
LOG_WARNING("NpcManager: using built-in NPC spawns (assets/npcs/singleplayer_spawns.csv missing)");
|
||||||
spawnDefs = {
|
spawnDefs = {
|
||||||
{"Azeroth", "Innkeeper Farley", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
{"Azeroth", 0, "Innkeeper Farley", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
||||||
30, 5000, glm::vec3(76.0f, -9468.0f, 205.0f), false, 3.1f, 1.0f, false},
|
30, 5000, glm::vec3(76.0f, -9468.0f, 205.0f), false, 3.1f, 1.0f, false},
|
||||||
{"Azeroth", "Bernard Gump", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
{"Azeroth", 0, "Bernard Gump", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
||||||
25, 4200, glm::vec3(92.0f, -9478.0f, 205.0f), false, 1.2f, 1.0f, false},
|
25, 4200, glm::vec3(92.0f, -9478.0f, 205.0f), false, 1.2f, 1.0f, false},
|
||||||
{"Azeroth", "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
|
{"Azeroth", 0, "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
|
||||||
60, 42000, glm::vec3(86.0f, -9478.0f, 205.0f), false, 0.1f, 1.0f, false},
|
60, 42000, glm::vec3(86.0f, -9478.0f, 205.0f), false, 0.1f, 1.0f, false},
|
||||||
{"Azeroth", "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
|
{"Azeroth", 0, "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
|
||||||
60, 42000, glm::vec3(37.0f, -9440.0f, 205.0f), false, 2.8f, 1.0f, false},
|
60, 42000, glm::vec3(37.0f, -9440.0f, 205.0f), false, 2.8f, 1.0f, false},
|
||||||
{"Azeroth", "Stormwind Citizen", "Creature\\HumanFemalePeasant\\HumanFemalePeasant.m2",
|
{"Azeroth", 0, "Stormwind Citizen", "Creature\\HumanFemalePeasant\\HumanFemalePeasant.m2",
|
||||||
5, 1200, glm::vec3(62.0f, -9468.0f, 205.0f), false, 3.5f, 1.0f, false},
|
5, 1200, glm::vec3(62.0f, -9468.0f, 205.0f), false, 3.5f, 1.0f, false},
|
||||||
{"Azeroth", "Stormwind Citizen", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
{"Azeroth", 0, "Stormwind Citizen", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
||||||
5, 1200, glm::vec3(23.0f, -9518.0f, 205.0f), false, 1.8f, 1.0f, false},
|
5, 1200, glm::vec3(23.0f, -9518.0f, 205.0f), false, 1.8f, 1.0f, false},
|
||||||
{"Azeroth", "Chicken", "Creature\\Chicken\\Chicken.m2",
|
{"Azeroth", 0, "Chicken", "Creature\\Chicken\\Chicken.m2",
|
||||||
1, 10, glm::vec3(58.0f, -9534.0f, 205.0f), false, 2.0f, 1.0f, true},
|
1, 10, glm::vec3(58.0f, -9534.0f, 205.0f), false, 2.0f, 1.0f, true},
|
||||||
{"Azeroth", "Cat", "Creature\\Cat\\Cat.m2",
|
{"Azeroth", 0, "Cat", "Creature\\Cat\\Cat.m2",
|
||||||
1, 42, glm::vec3(90.0f, -9446.0f, 205.0f), false, 4.5f, 1.0f, true}
|
1, 42, glm::vec3(90.0f, -9446.0f, 205.0f), false, 4.5f, 1.0f, true}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -763,6 +765,9 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
||||||
unit->setLevel(s.level);
|
unit->setLevel(s.level);
|
||||||
unit->setHealth(s.health);
|
unit->setHealth(s.health);
|
||||||
unit->setMaxHealth(s.health);
|
unit->setMaxHealth(s.health);
|
||||||
|
if (s.entry != 0) {
|
||||||
|
unit->setEntry(s.entry);
|
||||||
|
}
|
||||||
|
|
||||||
// Store canonical WoW coordinates for targeting/server compatibility
|
// Store canonical WoW coordinates for targeting/server compatibility
|
||||||
glm::vec3 spawnCanonical = core::coords::renderToCanonical(glPos);
|
glm::vec3 spawnCanonical = core::coords::renderToCanonical(glPos);
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ void Packet::writeUInt64(uint64_t value) {
|
||||||
writeUInt32((value >> 32) & 0xFFFFFFFF);
|
writeUInt32((value >> 32) & 0xFFFFFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Packet::writeFloat(float value) {
|
||||||
|
uint32_t bits = 0;
|
||||||
|
std::memcpy(&bits, &value, sizeof(float));
|
||||||
|
writeUInt32(bits);
|
||||||
|
}
|
||||||
|
|
||||||
void Packet::writeString(const std::string& value) {
|
void Packet::writeString(const std::string& value) {
|
||||||
for (char c : value) {
|
for (char c : value) {
|
||||||
data.push_back(static_cast<uint8_t>(c));
|
data.push_back(static_cast<uint8_t>(c));
|
||||||
|
|
|
||||||
|
|
@ -1404,6 +1404,22 @@ bool CharacterRenderer::hasAnimation(uint32_t instanceId, uint32_t animationId)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CharacterRenderer::getAnimationSequences(uint32_t instanceId, std::vector<pipeline::M2Sequence>& out) const {
|
||||||
|
out.clear();
|
||||||
|
auto it = instances.find(instanceId);
|
||||||
|
if (it == instances.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto modelIt = models.find(it->second.modelId);
|
||||||
|
if (modelIt == models.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = modelIt->second.data.sequences;
|
||||||
|
return !out.empty();
|
||||||
|
}
|
||||||
|
|
||||||
bool CharacterRenderer::getInstanceModelName(uint32_t instanceId, std::string& modelName) const {
|
bool CharacterRenderer::getInstanceModelName(uint32_t instanceId, std::string& modelName) const {
|
||||||
auto it = instances.find(instanceId);
|
auto it = instances.find(instanceId);
|
||||||
if (it == instances.end()) {
|
if (it == instances.end()) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
#include "rendering/m2_renderer.hpp"
|
#include "rendering/m2_renderer.hpp"
|
||||||
#include "rendering/minimap.hpp"
|
#include "rendering/minimap.hpp"
|
||||||
#include "rendering/shader.hpp"
|
#include "rendering/shader.hpp"
|
||||||
|
#include "pipeline/m2_loader.hpp"
|
||||||
|
#include <algorithm>
|
||||||
#include "pipeline/asset_manager.hpp"
|
#include "pipeline/asset_manager.hpp"
|
||||||
#include "pipeline/m2_loader.hpp"
|
#include "pipeline/m2_loader.hpp"
|
||||||
#include "pipeline/wmo_loader.hpp"
|
#include "pipeline/wmo_loader.hpp"
|
||||||
|
|
@ -362,6 +364,79 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t Renderer::resolveMeleeAnimId() {
|
||||||
|
if (!characterRenderer || characterInstanceId == 0) {
|
||||||
|
meleeAnimId = 0;
|
||||||
|
meleeAnimDurationMs = 0.0f;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meleeAnimId != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId)) {
|
||||||
|
return meleeAnimId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<pipeline::M2Sequence> sequences;
|
||||||
|
if (!characterRenderer->getAnimationSequences(characterInstanceId, sequences)) {
|
||||||
|
meleeAnimId = 0;
|
||||||
|
meleeAnimDurationMs = 0.0f;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto findDuration = [&](uint32_t id) -> float {
|
||||||
|
for (const auto& seq : sequences) {
|
||||||
|
if (seq.id == id && seq.duration > 0) {
|
||||||
|
return static_cast<float>(seq.duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint32_t attackCandidates[] = {16, 17, 18, 19, 20, 21};
|
||||||
|
for (uint32_t id : attackCandidates) {
|
||||||
|
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
|
||||||
|
meleeAnimId = id;
|
||||||
|
meleeAnimDurationMs = findDuration(id);
|
||||||
|
return meleeAnimId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97};
|
||||||
|
auto isAvoid = [&](uint32_t id) -> bool {
|
||||||
|
for (uint32_t avoid : avoidIds) {
|
||||||
|
if (id == avoid) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t bestId = 0;
|
||||||
|
uint32_t bestDuration = 0;
|
||||||
|
for (const auto& seq : sequences) {
|
||||||
|
if (seq.duration == 0) continue;
|
||||||
|
if (isAvoid(seq.id)) continue;
|
||||||
|
if (seq.movingSpeed > 0.1f) continue;
|
||||||
|
if (seq.duration < 150 || seq.duration > 2000) continue;
|
||||||
|
if (bestId == 0 || seq.duration < bestDuration) {
|
||||||
|
bestId = seq.id;
|
||||||
|
bestDuration = seq.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestId == 0) {
|
||||||
|
for (const auto& seq : sequences) {
|
||||||
|
if (seq.duration == 0) continue;
|
||||||
|
if (isAvoid(seq.id)) continue;
|
||||||
|
if (bestId == 0 || seq.duration < bestDuration) {
|
||||||
|
bestId = seq.id;
|
||||||
|
bestDuration = seq.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meleeAnimId = bestId;
|
||||||
|
meleeAnimDurationMs = static_cast<float>(bestDuration);
|
||||||
|
return meleeAnimId;
|
||||||
|
}
|
||||||
|
|
||||||
void Renderer::updateCharacterAnimation() {
|
void Renderer::updateCharacterAnimation() {
|
||||||
// WoW WotLK AnimationData.dbc IDs
|
// WoW WotLK AnimationData.dbc IDs
|
||||||
constexpr uint32_t ANIM_STAND = 0;
|
constexpr uint32_t ANIM_STAND = 0;
|
||||||
|
|
@ -394,8 +469,9 @@ void Renderer::updateCharacterAnimation() {
|
||||||
bool sprinting = cameraController->isSprinting();
|
bool sprinting = cameraController->isSprinting();
|
||||||
bool sitting = cameraController->isSitting();
|
bool sitting = cameraController->isSitting();
|
||||||
bool swim = cameraController->isSwimming();
|
bool swim = cameraController->isSwimming();
|
||||||
|
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
||||||
|
|
||||||
switch (charAnimState) {
|
if (!forceMelee) switch (charAnimState) {
|
||||||
case CharAnimState::IDLE:
|
case CharAnimState::IDLE:
|
||||||
if (swim) {
|
if (swim) {
|
||||||
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
||||||
|
|
@ -517,6 +593,28 @@ void Renderer::updateCharacterAnimation() {
|
||||||
newState = CharAnimState::SWIM_IDLE;
|
newState = CharAnimState::SWIM_IDLE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CharAnimState::MELEE_SWING:
|
||||||
|
if (swim) {
|
||||||
|
newState = CharAnimState::SWIM_IDLE;
|
||||||
|
} else if (!grounded && jumping) {
|
||||||
|
newState = CharAnimState::JUMP_START;
|
||||||
|
} else if (!grounded) {
|
||||||
|
newState = CharAnimState::JUMP_MID;
|
||||||
|
} else if (moving && sprinting) {
|
||||||
|
newState = CharAnimState::RUN;
|
||||||
|
} else if (moving) {
|
||||||
|
newState = CharAnimState::WALK;
|
||||||
|
} else if (sitting) {
|
||||||
|
newState = CharAnimState::SIT_DOWN;
|
||||||
|
} else {
|
||||||
|
newState = CharAnimState::IDLE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceMelee) {
|
||||||
|
newState = CharAnimState::MELEE_SWING;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState != charAnimState) {
|
if (newState != charAnimState) {
|
||||||
|
|
@ -569,6 +667,13 @@ void Renderer::updateCharacterAnimation() {
|
||||||
case CharAnimState::EMOTE: animId = emoteAnimId; loop = emoteLoop; break;
|
case CharAnimState::EMOTE: animId = emoteAnimId; loop = emoteLoop; break;
|
||||||
case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break;
|
case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break;
|
||||||
case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break;
|
case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break;
|
||||||
|
case CharAnimState::MELEE_SWING:
|
||||||
|
animId = resolveMeleeAnimId();
|
||||||
|
if (animId == 0) {
|
||||||
|
animId = ANIM_STAND;
|
||||||
|
}
|
||||||
|
loop = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t currentAnimId = 0;
|
uint32_t currentAnimId = 0;
|
||||||
|
|
@ -601,6 +706,23 @@ void Renderer::cancelEmote() {
|
||||||
emoteLoop = false;
|
emoteLoop = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Renderer::triggerMeleeSwing() {
|
||||||
|
if (!characterRenderer || characterInstanceId == 0) return;
|
||||||
|
if (meleeSwingCooldown > 0.0f) return;
|
||||||
|
if (emoteActive) {
|
||||||
|
cancelEmote();
|
||||||
|
}
|
||||||
|
resolveMeleeAnimId();
|
||||||
|
meleeSwingCooldown = 0.1f;
|
||||||
|
float durationSec = meleeAnimDurationMs > 0.0f ? meleeAnimDurationMs / 1000.0f : 0.6f;
|
||||||
|
if (durationSec < 0.25f) durationSec = 0.25f;
|
||||||
|
if (durationSec > 1.0f) durationSec = 1.0f;
|
||||||
|
meleeSwingTimer = durationSec;
|
||||||
|
if (activitySoundManager) {
|
||||||
|
activitySoundManager->playMeleeSwing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string Renderer::getEmoteText(const std::string& emoteName) {
|
std::string Renderer::getEmoteText(const std::string& emoteName) {
|
||||||
auto it = EMOTE_TABLE.find(emoteName);
|
auto it = EMOTE_TABLE.find(emoteName);
|
||||||
if (it != EMOTE_TABLE.end()) {
|
if (it != EMOTE_TABLE.end()) {
|
||||||
|
|
@ -714,6 +836,13 @@ void Renderer::update(float deltaTime) {
|
||||||
|
|
||||||
// Sync character model position/rotation and animation with follow target
|
// Sync character model position/rotation and animation with follow target
|
||||||
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
|
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
|
||||||
|
if (meleeSwingCooldown > 0.0f) {
|
||||||
|
meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime);
|
||||||
|
}
|
||||||
|
if (meleeSwingTimer > 0.0f) {
|
||||||
|
meleeSwingTimer = std::max(0.0f, meleeSwingTimer - deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
|
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
|
||||||
if (activitySoundManager) {
|
if (activitySoundManager) {
|
||||||
std::string modelName;
|
std::string modelName;
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
||||||
|
|
||||||
// Inventory (B key toggle handled inside)
|
// Inventory (B key toggle handled inside)
|
||||||
inventoryScreen.render(gameHandler.getInventory());
|
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
||||||
|
|
||||||
if (inventoryScreen.consumeEquipmentDirty()) {
|
if (inventoryScreen.consumeEquipmentDirty()) {
|
||||||
updateCharacterGeosets(gameHandler.getInventory());
|
updateCharacterGeosets(gameHandler.getInventory());
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ void InventoryScreen::renderHeldItem() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InventoryScreen::render(game::Inventory& inventory) {
|
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;
|
||||||
bool bDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
|
bool bDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
|
||||||
|
|
@ -269,6 +269,14 @@ void InventoryScreen::render(game::Inventory& inventory) {
|
||||||
renderBackpackPanel(inventory);
|
renderBackpackPanel(inventory);
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
uint64_t gold = moneyCopper / 10000;
|
||||||
|
uint64_t silver = (moneyCopper / 100) % 100;
|
||||||
|
uint64_t copper = moneyCopper % 100;
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc",
|
||||||
|
static_cast<unsigned long long>(gold),
|
||||||
|
static_cast<unsigned long long>(silver),
|
||||||
|
static_cast<unsigned long long>(copper));
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
// Draw held item at cursor (on top of everything)
|
// Draw held item at cursor (on top of everything)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue