mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-02 15:53:51 +00:00
Use created character data and DB start items/spells
This commit is contained in:
parent
2db690a866
commit
c89fccdfeb
4 changed files with 509 additions and 102 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "core/window.hpp"
|
#include "core/window.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
|
#include "game/character.hpp"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
@ -76,6 +77,8 @@ private:
|
||||||
void setupUICallbacks();
|
void setupUICallbacks();
|
||||||
void spawnPlayerCharacter();
|
void spawnPlayerCharacter();
|
||||||
void spawnNpcs();
|
void spawnNpcs();
|
||||||
|
std::string getPlayerModelPath() const;
|
||||||
|
static const char* mapIdToName(uint32_t mapId);
|
||||||
|
|
||||||
static Application* instance;
|
static Application* instance;
|
||||||
|
|
||||||
|
|
@ -96,6 +99,14 @@ private:
|
||||||
bool spawnSnapToGround = true;
|
bool spawnSnapToGround = true;
|
||||||
float lastFrameTime = 0.0f;
|
float lastFrameTime = 0.0f;
|
||||||
float movementHeartbeatTimer = 0.0f;
|
float movementHeartbeatTimer = 0.0f;
|
||||||
|
game::Race spRace_ = game::Race::HUMAN;
|
||||||
|
game::Gender spGender_ = game::Gender::MALE;
|
||||||
|
game::Class spClass_ = game::Class::WARRIOR;
|
||||||
|
uint32_t spMapId_ = 0;
|
||||||
|
uint32_t spZoneId_ = 0;
|
||||||
|
glm::vec3 spSpawnCanonical_ = glm::vec3(62.0f, -9464.0f, 200.0f);
|
||||||
|
float spYawDeg_ = 0.0f;
|
||||||
|
float spPitchDeg_ = -5.0f;
|
||||||
|
|
||||||
// Weapon model ID counter (starting high to avoid collision with character model IDs)
|
// Weapon model ID counter (starting high to avoid collision with character model IDs)
|
||||||
uint32_t nextWeaponModelId_ = 1000;
|
uint32_t nextWeaponModelId_ = 1000;
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,10 @@ public:
|
||||||
* @param characterGuid GUID of character to log in with
|
* @param characterGuid GUID of character to log in with
|
||||||
*/
|
*/
|
||||||
void selectCharacter(uint64_t characterGuid);
|
void selectCharacter(uint64_t characterGuid);
|
||||||
|
void setActiveCharacterGuid(uint64_t guid) { activeCharacterGuid_ = guid; }
|
||||||
|
uint64_t getActiveCharacterGuid() const { return activeCharacterGuid_; }
|
||||||
|
const Character* getActiveCharacter() const;
|
||||||
|
const Character* getFirstCharacter() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current player movement info
|
* Get current player movement info
|
||||||
|
|
@ -167,6 +171,9 @@ public:
|
||||||
// Money (copper)
|
// Money (copper)
|
||||||
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
|
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
|
||||||
|
|
||||||
|
// Single-player: mark character list ready for selection UI
|
||||||
|
void setSinglePlayerCharListReady();
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
Inventory& getInventory() { return inventory; }
|
Inventory& getInventory() { return inventory; }
|
||||||
const Inventory& getInventory() const { return inventory; }
|
const Inventory& getInventory() const { return inventory; }
|
||||||
|
|
@ -216,6 +223,7 @@ public:
|
||||||
void setSinglePlayerMode(bool sp) { singlePlayerMode_ = sp; }
|
void setSinglePlayerMode(bool sp) { singlePlayerMode_ = sp; }
|
||||||
bool isSinglePlayerMode() const { return singlePlayerMode_; }
|
bool isSinglePlayerMode() const { return singlePlayerMode_; }
|
||||||
void simulateMotd(const std::vector<std::string>& lines);
|
void simulateMotd(const std::vector<std::string>& lines);
|
||||||
|
void applySinglePlayerStartData(Race race, Class cls);
|
||||||
|
|
||||||
// NPC death callback (single-player)
|
// NPC death callback (single-player)
|
||||||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
||||||
|
|
@ -296,6 +304,16 @@ public:
|
||||||
*/
|
*/
|
||||||
void update(float deltaTime);
|
void update(float deltaTime);
|
||||||
|
|
||||||
|
struct SinglePlayerCreateInfo {
|
||||||
|
uint32_t mapId = 0;
|
||||||
|
uint32_t zoneId = 0;
|
||||||
|
float x = 0.0f;
|
||||||
|
float y = 0.0f;
|
||||||
|
float z = 0.0f;
|
||||||
|
float orientation = 0.0f;
|
||||||
|
};
|
||||||
|
bool getSinglePlayerCreateInfo(Race race, Class cls, SinglePlayerCreateInfo& out) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* Handle incoming packet from world server
|
* Handle incoming packet from world server
|
||||||
|
|
@ -503,6 +521,8 @@ private:
|
||||||
bool pendingGroupInvite = false;
|
bool pendingGroupInvite = false;
|
||||||
std::string pendingInviterName;
|
std::string pendingInviterName;
|
||||||
|
|
||||||
|
uint64_t activeCharacterGuid_ = 0;
|
||||||
|
|
||||||
// ---- Phase 5: Loot ----
|
// ---- Phase 5: Loot ----
|
||||||
bool lootWindowOpen = false;
|
bool lootWindowOpen = false;
|
||||||
LootResponseData currentLoot;
|
LootResponseData currentLoot;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,71 @@ const SpawnPreset* selectSpawnPreset(const char* envValue) {
|
||||||
return &SPAWN_PRESETS[0];
|
return &SPAWN_PRESETS[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const char* Application::mapIdToName(uint32_t mapId) {
|
||||||
|
switch (mapId) {
|
||||||
|
case 0: return "Azeroth";
|
||||||
|
case 1: return "Kalimdor";
|
||||||
|
case 530: return "Outland";
|
||||||
|
case 571: return "Northrend";
|
||||||
|
default: return "Azeroth";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Application::getPlayerModelPath() const {
|
||||||
|
auto pick = [&](game::Race race, game::Gender gender) -> std::string {
|
||||||
|
switch (race) {
|
||||||
|
case game::Race::HUMAN:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Human\\Female\\HumanFemale.m2"
|
||||||
|
: "Character\\Human\\Male\\HumanMale.m2";
|
||||||
|
case game::Race::ORC:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Orc\\Female\\OrcFemale.m2"
|
||||||
|
: "Character\\Orc\\Male\\OrcMale.m2";
|
||||||
|
case game::Race::DWARF:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Dwarf\\Female\\DwarfFemale.m2"
|
||||||
|
: "Character\\Dwarf\\Male\\DwarfMale.m2";
|
||||||
|
case game::Race::NIGHT_ELF:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\NightElf\\Female\\NightElfFemale.m2"
|
||||||
|
: "Character\\NightElf\\Male\\NightElfMale.m2";
|
||||||
|
case game::Race::UNDEAD:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Scourge\\Female\\ScourgeFemale.m2"
|
||||||
|
: "Character\\Scourge\\Male\\ScourgeMale.m2";
|
||||||
|
case game::Race::TAUREN:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Tauren\\Female\\TaurenFemale.m2"
|
||||||
|
: "Character\\Tauren\\Male\\TaurenMale.m2";
|
||||||
|
case game::Race::GNOME:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Gnome\\Female\\GnomeFemale.m2"
|
||||||
|
: "Character\\Gnome\\Male\\GnomeMale.m2";
|
||||||
|
case game::Race::TROLL:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Troll\\Female\\TrollFemale.m2"
|
||||||
|
: "Character\\Troll\\Male\\TrollMale.m2";
|
||||||
|
case game::Race::BLOOD_ELF:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\BloodElf\\Female\\BloodElfFemale.m2"
|
||||||
|
: "Character\\BloodElf\\Male\\BloodElfMale.m2";
|
||||||
|
case game::Race::DRAENEI:
|
||||||
|
return gender == game::Gender::FEMALE
|
||||||
|
? "Character\\Draenei\\Female\\DraeneiFemale.m2"
|
||||||
|
: "Character\\Draenei\\Male\\DraeneiMale.m2";
|
||||||
|
default:
|
||||||
|
return "Character\\Human\\Male\\HumanMale.m2";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return pick(spRace_, spGender_);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
std::optional<glm::vec3> parseVec3Csv(const char* raw) {
|
std::optional<glm::vec3> parseVec3Csv(const char* raw) {
|
||||||
if (!raw || !*raw) return std::nullopt;
|
if (!raw || !*raw) return std::nullopt;
|
||||||
std::stringstream ss(raw);
|
std::stringstream ss(raw);
|
||||||
|
|
@ -452,7 +517,10 @@ void Application::setupUICallbacks() {
|
||||||
uiManager->getAuthScreen().setOnSinglePlayer([this]() {
|
uiManager->getAuthScreen().setOnSinglePlayer([this]() {
|
||||||
LOG_INFO("Single-player mode selected, opening character creation");
|
LOG_INFO("Single-player mode selected, opening character creation");
|
||||||
singlePlayerMode = true;
|
singlePlayerMode = true;
|
||||||
gameHandler->setSinglePlayerMode(true);
|
if (gameHandler) {
|
||||||
|
gameHandler->setSinglePlayerMode(true);
|
||||||
|
gameHandler->setSinglePlayerCharListReady();
|
||||||
|
}
|
||||||
uiManager->getCharacterCreateScreen().reset();
|
uiManager->getCharacterCreateScreen().reset();
|
||||||
setState(AppState::CHARACTER_CREATION);
|
setState(AppState::CHARACTER_CREATION);
|
||||||
});
|
});
|
||||||
|
|
@ -487,13 +555,8 @@ void Application::setupUICallbacks() {
|
||||||
uiManager->getCharacterScreen().setOnCharacterSelected([this](uint64_t characterGuid) {
|
uiManager->getCharacterScreen().setOnCharacterSelected([this](uint64_t characterGuid) {
|
||||||
LOG_INFO("Character selected: GUID=0x", std::hex, characterGuid, std::dec);
|
LOG_INFO("Character selected: GUID=0x", std::hex, characterGuid, std::dec);
|
||||||
if (singlePlayerMode) {
|
if (singlePlayerMode) {
|
||||||
// Use created character's data for level/HP
|
if (gameHandler) {
|
||||||
for (const auto& ch : gameHandler->getCharacters()) {
|
gameHandler->setActiveCharacterGuid(characterGuid);
|
||||||
if (ch.guid == characterGuid) {
|
|
||||||
uint32_t maxHp = 20 + static_cast<uint32_t>(ch.level) * 10;
|
|
||||||
gameHandler->initLocalPlayerStats(ch.level, maxHp, maxHp);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
startSinglePlayer();
|
startSinglePlayer();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -544,16 +607,31 @@ void Application::spawnPlayerCharacter() {
|
||||||
auto* charRenderer = renderer->getCharacterRenderer();
|
auto* charRenderer = renderer->getCharacterRenderer();
|
||||||
auto* camera = renderer->getCamera();
|
auto* camera = renderer->getCamera();
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
|
std::string m2Path = getPlayerModelPath();
|
||||||
|
std::string modelDir;
|
||||||
|
std::string baseName;
|
||||||
|
{
|
||||||
|
size_t slash = m2Path.rfind('\\');
|
||||||
|
if (slash != std::string::npos) {
|
||||||
|
modelDir = m2Path.substr(0, slash + 1);
|
||||||
|
baseName = m2Path.substr(slash + 1);
|
||||||
|
} else {
|
||||||
|
baseName = m2Path;
|
||||||
|
}
|
||||||
|
size_t dot = baseName.rfind('.');
|
||||||
|
if (dot != std::string::npos) {
|
||||||
|
baseName = baseName.substr(0, dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try loading real HumanMale M2 from MPQ
|
// Try loading selected character model from MPQ
|
||||||
if (assetManager && assetManager->isInitialized()) {
|
if (assetManager && assetManager->isInitialized()) {
|
||||||
std::string m2Path = "Character\\Human\\Male\\HumanMale.m2";
|
|
||||||
auto m2Data = assetManager->readFile(m2Path);
|
auto m2Data = assetManager->readFile(m2Path);
|
||||||
if (!m2Data.empty()) {
|
if (!m2Data.empty()) {
|
||||||
auto model = pipeline::M2Loader::load(m2Data);
|
auto model = pipeline::M2Loader::load(m2Data);
|
||||||
|
|
||||||
// Load skin file for submesh/batch data
|
// Load skin file for submesh/batch data
|
||||||
std::string skinPath = "Character\\Human\\Male\\HumanMale00.skin";
|
std::string skinPath = modelDir + baseName + "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);
|
||||||
|
|
@ -566,67 +644,77 @@ void Application::spawnPlayerCharacter() {
|
||||||
LOG_INFO(" Texture ", ti, ": type=", tex.type, " name='", tex.filename, "'");
|
LOG_INFO(" Texture ", ti, ": type=", tex.type, " name='", tex.filename, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up underwear textures from CharSections.dbc
|
// Look up underwear textures from CharSections.dbc (humans only for now)
|
||||||
std::string bodySkinPath = "Character\\Human\\Male\\HumanMaleSkin00_00.blp";
|
bool useCharSections = (spRace_ == game::Race::HUMAN);
|
||||||
|
uint32_t targetRaceId = static_cast<uint32_t>(spRace_);
|
||||||
|
uint32_t targetSexId = (spGender_ == game::Gender::FEMALE) ? 1u : 0u;
|
||||||
|
std::string bodySkinPath = (spGender_ == game::Gender::FEMALE)
|
||||||
|
? "Character\\Human\\Female\\HumanFemaleSkin00_00.blp"
|
||||||
|
: "Character\\Human\\Male\\HumanMaleSkin00_00.blp";
|
||||||
|
std::string pelvisPath = (spGender_ == game::Gender::FEMALE)
|
||||||
|
? "Character\\Human\\Female\\HumanFemaleNakedPelvisSkin00_00.blp"
|
||||||
|
: "Character\\Human\\Male\\HumanMaleNakedPelvisSkin00_00.blp";
|
||||||
std::string faceLowerTexturePath;
|
std::string faceLowerTexturePath;
|
||||||
std::vector<std::string> underwearPaths;
|
std::vector<std::string> underwearPaths;
|
||||||
|
|
||||||
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
|
if (useCharSections) {
|
||||||
if (charSectionsDbc) {
|
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
|
||||||
LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records");
|
if (charSectionsDbc) {
|
||||||
bool foundSkin = false;
|
LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records");
|
||||||
bool foundUnderwear = false;
|
bool foundSkin = false;
|
||||||
bool foundFaceLower = false;
|
bool foundUnderwear = false;
|
||||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
bool foundFaceLower = false;
|
||||||
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
|
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||||
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
|
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
|
||||||
uint32_t baseSection = charSectionsDbc->getUInt32(r, 3);
|
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
|
||||||
uint32_t variationIndex = charSectionsDbc->getUInt32(r, 8);
|
uint32_t baseSection = charSectionsDbc->getUInt32(r, 3);
|
||||||
uint32_t colorIndex = charSectionsDbc->getUInt32(r, 9);
|
uint32_t variationIndex = charSectionsDbc->getUInt32(r, 8);
|
||||||
|
uint32_t colorIndex = charSectionsDbc->getUInt32(r, 9);
|
||||||
|
|
||||||
if (raceId != 1 || sexId != 0) continue;
|
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||||
|
|
||||||
if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == 0) {
|
if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == 0) {
|
||||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||||
if (!tex1.empty()) {
|
if (!tex1.empty()) {
|
||||||
bodySkinPath = tex1;
|
bodySkinPath = tex1;
|
||||||
foundSkin = true;
|
foundSkin = true;
|
||||||
LOG_INFO(" DBC body skin: ", bodySkinPath);
|
LOG_INFO(" DBC body skin: ", bodySkinPath);
|
||||||
}
|
|
||||||
} else if (baseSection == 3 && colorIndex == 0) {
|
|
||||||
(void)variationIndex;
|
|
||||||
} else if (baseSection == 1 && !foundFaceLower && variationIndex == 0 && colorIndex == 0) {
|
|
||||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
|
||||||
if (!tex1.empty()) {
|
|
||||||
faceLowerTexturePath = tex1;
|
|
||||||
foundFaceLower = true;
|
|
||||||
LOG_INFO(" DBC face texture: ", faceLowerTexturePath);
|
|
||||||
}
|
|
||||||
} else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == 0) {
|
|
||||||
for (int f = 4; f <= 6; f++) {
|
|
||||||
std::string tex = charSectionsDbc->getString(r, f);
|
|
||||||
if (!tex.empty()) {
|
|
||||||
underwearPaths.push_back(tex);
|
|
||||||
LOG_INFO(" DBC underwear texture: ", tex);
|
|
||||||
}
|
}
|
||||||
|
} else if (baseSection == 3 && colorIndex == 0) {
|
||||||
|
(void)variationIndex;
|
||||||
|
} else if (baseSection == 1 && !foundFaceLower && variationIndex == 0 && colorIndex == 0) {
|
||||||
|
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||||
|
if (!tex1.empty()) {
|
||||||
|
faceLowerTexturePath = tex1;
|
||||||
|
foundFaceLower = true;
|
||||||
|
LOG_INFO(" DBC face texture: ", faceLowerTexturePath);
|
||||||
|
}
|
||||||
|
} else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == 0) {
|
||||||
|
for (int f = 4; f <= 6; f++) {
|
||||||
|
std::string tex = charSectionsDbc->getString(r, f);
|
||||||
|
if (!tex.empty()) {
|
||||||
|
underwearPaths.push_back(tex);
|
||||||
|
LOG_INFO(" DBC underwear texture: ", tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundUnderwear = true;
|
||||||
}
|
}
|
||||||
foundUnderwear = true;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& tex : model.textures) {
|
for (auto& tex : model.textures) {
|
||||||
if (tex.type == 1 && tex.filename.empty()) {
|
if (tex.type == 1 && tex.filename.empty()) {
|
||||||
tex.filename = bodySkinPath;
|
tex.filename = bodySkinPath;
|
||||||
} else if (tex.type == 6 && tex.filename.empty()) {
|
} else if (tex.type == 6 && tex.filename.empty()) {
|
||||||
tex.filename = "Character\\Human\\Hair00_00.blp";
|
tex.filename = "Character\\Human\\Hair00_00.blp";
|
||||||
} else if (tex.type == 8 && tex.filename.empty()) {
|
} else if (tex.type == 8 && tex.filename.empty()) {
|
||||||
if (!underwearPaths.empty()) {
|
if (!underwearPaths.empty()) {
|
||||||
tex.filename = underwearPaths[0];
|
tex.filename = underwearPaths[0];
|
||||||
} else {
|
} else {
|
||||||
tex.filename = "Character\\Human\\Male\\HumanMaleNakedPelvisSkin00_00.blp";
|
tex.filename = pelvisPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -640,8 +728,11 @@ void Application::spawnPlayerCharacter() {
|
||||||
// e.g. Character\Human\Male\HumanMale0097-00.anim
|
// e.g. Character\Human\Male\HumanMale0097-00.anim
|
||||||
char animFileName[256];
|
char animFileName[256];
|
||||||
snprintf(animFileName, sizeof(animFileName),
|
snprintf(animFileName, sizeof(animFileName),
|
||||||
"Character\\Human\\Male\\HumanMale%04u-%02u.anim",
|
"%s%s%04u-%02u.anim",
|
||||||
model.sequences[si].id, model.sequences[si].variationIndex);
|
modelDir.c_str(),
|
||||||
|
baseName.c_str(),
|
||||||
|
model.sequences[si].id,
|
||||||
|
model.sequences[si].variationIndex);
|
||||||
auto animFileData = assetManager->readFile(animFileName);
|
auto animFileData = assetManager->readFile(animFileName);
|
||||||
if (!animFileData.empty()) {
|
if (!animFileData.empty()) {
|
||||||
pipeline::M2Loader::loadAnimFile(m2Data, animFileData, si, model);
|
pipeline::M2Loader::loadAnimFile(m2Data, animFileData, si, model);
|
||||||
|
|
@ -651,29 +742,33 @@ void Application::spawnPlayerCharacter() {
|
||||||
|
|
||||||
charRenderer->loadModel(model, 1);
|
charRenderer->loadModel(model, 1);
|
||||||
|
|
||||||
// Save skin composite state for re-compositing on equipment changes
|
if (useCharSections) {
|
||||||
bodySkinPath_ = bodySkinPath;
|
// Save skin composite state for re-compositing on equipment changes
|
||||||
underwearPaths_ = underwearPaths;
|
bodySkinPath_ = bodySkinPath;
|
||||||
|
underwearPaths_ = underwearPaths;
|
||||||
|
|
||||||
|
// Composite body skin + underwear overlays
|
||||||
// Composite body skin + underwear overlays
|
if (!underwearPaths.empty()) {
|
||||||
if (!underwearPaths.empty()) {
|
std::vector<std::string> layers;
|
||||||
std::vector<std::string> layers;
|
layers.push_back(bodySkinPath);
|
||||||
layers.push_back(bodySkinPath);
|
for (const auto& up : underwearPaths) {
|
||||||
for (const auto& up : underwearPaths) {
|
layers.push_back(up);
|
||||||
layers.push_back(up);
|
}
|
||||||
}
|
GLuint compositeTex = charRenderer->compositeTextures(layers);
|
||||||
GLuint compositeTex = charRenderer->compositeTextures(layers);
|
if (compositeTex != 0) {
|
||||||
if (compositeTex != 0) {
|
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
if (model.textures[ti].type == 1) {
|
||||||
if (model.textures[ti].type == 1) {
|
charRenderer->setModelTexture(1, static_cast<uint32_t>(ti), compositeTex);
|
||||||
charRenderer->setModelTexture(1, static_cast<uint32_t>(ti), compositeTex);
|
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
||||||
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+underwear");
|
||||||
LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+underwear");
|
break;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
bodySkinPath_.clear();
|
||||||
|
underwearPaths_.clear();
|
||||||
}
|
}
|
||||||
// Find cloak (type-2, Object Skin) texture slot index
|
// Find cloak (type-2, Object Skin) texture slot index
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||||
|
|
@ -685,7 +780,7 @@ void Application::spawnPlayerCharacter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
LOG_INFO("Loaded HumanMale M2: ", model.vertices.size(), " verts, ",
|
LOG_INFO("Loaded character model: ", m2Path, " (", model.vertices.size(), " verts, ",
|
||||||
model.bones.size(), " bones, ", model.sequences.size(), " anims, ",
|
model.bones.size(), " bones, ", model.sequences.size(), " anims, ",
|
||||||
model.indices.size(), " indices, ", model.batches.size(), " batches");
|
model.indices.size(), " indices, ", model.batches.size(), " batches");
|
||||||
// Log all animation sequence IDs
|
// Log all animation sequence IDs
|
||||||
|
|
@ -980,12 +1075,6 @@ void Application::startSinglePlayer() {
|
||||||
// Enable single-player combat mode on game handler
|
// Enable single-player combat mode on game handler
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
gameHandler->setSinglePlayerMode(true);
|
gameHandler->setSinglePlayerMode(true);
|
||||||
// Only init stats with defaults if not already set (e.g. via character creation)
|
|
||||||
if (gameHandler->getLocalPlayerMaxHealth() == 0) {
|
|
||||||
uint32_t level = 10;
|
|
||||||
uint32_t maxHealth = 20 + level * 10;
|
|
||||||
gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create world object for single-player
|
// Create world object for single-player
|
||||||
|
|
@ -994,9 +1083,44 @@ void Application::startSinglePlayer() {
|
||||||
LOG_INFO("Single-player world created");
|
LOG_INFO("Single-player world created");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate test inventory for single-player
|
const game::Character* activeChar = gameHandler ? gameHandler->getActiveCharacter() : nullptr;
|
||||||
|
if (!activeChar && gameHandler) {
|
||||||
|
activeChar = gameHandler->getFirstCharacter();
|
||||||
|
if (activeChar) {
|
||||||
|
gameHandler->setActiveCharacterGuid(activeChar->guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!activeChar) {
|
||||||
|
LOG_ERROR("Single-player start: no character selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spRace_ = activeChar->race;
|
||||||
|
spGender_ = activeChar->gender;
|
||||||
|
spClass_ = activeChar->characterClass;
|
||||||
|
spMapId_ = activeChar->mapId;
|
||||||
|
spZoneId_ = activeChar->zoneId;
|
||||||
|
spSpawnCanonical_ = core::coords::serverToCanonical(glm::vec3(activeChar->x, activeChar->y, activeChar->z));
|
||||||
|
spYawDeg_ = 0.0f;
|
||||||
|
spPitchDeg_ = -5.0f;
|
||||||
|
|
||||||
|
game::GameHandler::SinglePlayerCreateInfo createInfo;
|
||||||
|
bool hasCreate = gameHandler && gameHandler->getSinglePlayerCreateInfo(activeChar->race, activeChar->characterClass, createInfo);
|
||||||
|
if (hasCreate) {
|
||||||
|
spMapId_ = createInfo.mapId;
|
||||||
|
spZoneId_ = createInfo.zoneId;
|
||||||
|
spSpawnCanonical_ = core::coords::serverToCanonical(glm::vec3(createInfo.x, createInfo.y, createInfo.z));
|
||||||
|
spYawDeg_ = glm::degrees(createInfo.orientation);
|
||||||
|
spPitchDeg_ = -5.0f;
|
||||||
|
spawnSnapToGround = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
gameHandler->getInventory().populateTestItems();
|
gameHandler->setPlayerGuid(activeChar->guid);
|
||||||
|
uint32_t level = std::max<uint32_t>(1, activeChar->level);
|
||||||
|
uint32_t maxHealth = 20 + level * 10;
|
||||||
|
gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth);
|
||||||
|
gameHandler->applySinglePlayerStartData(activeChar->race, activeChar->characterClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load weapon models for equipped items (after inventory is populated)
|
// Load weapon models for equipped items (after inventory is populated)
|
||||||
|
|
@ -1005,11 +1129,11 @@ void Application::startSinglePlayer() {
|
||||||
// --- Loading screen: load terrain and wait for streaming before spawning ---
|
// --- Loading screen: load terrain and wait for streaming before spawning ---
|
||||||
const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN"));
|
const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN"));
|
||||||
// Canonical WoW coords: +X=North, +Y=West, +Z=Up
|
// Canonical WoW coords: +X=North, +Y=West, +Z=Up
|
||||||
glm::vec3 spawnCanonical = spawnPreset ? spawnPreset->spawnCanonical : glm::vec3(62.0f, -9464.0f, 200.0f);
|
glm::vec3 spawnCanonical = spawnPreset ? spawnPreset->spawnCanonical : spSpawnCanonical_;
|
||||||
std::string mapName = spawnPreset ? spawnPreset->mapName : "Azeroth";
|
std::string mapName = spawnPreset ? spawnPreset->mapName : mapIdToName(spMapId_);
|
||||||
float spawnYaw = spawnPreset ? spawnPreset->yawDeg : 0.0f;
|
float spawnYaw = spawnPreset ? spawnPreset->yawDeg : spYawDeg_;
|
||||||
float spawnPitch = spawnPreset ? spawnPreset->pitchDeg : -5.0f;
|
float spawnPitch = spawnPreset ? spawnPreset->pitchDeg : spPitchDeg_;
|
||||||
spawnSnapToGround = spawnPreset ? spawnPreset->snapToGround : true;
|
spawnSnapToGround = spawnPreset ? spawnPreset->snapToGround : spawnSnapToGround;
|
||||||
|
|
||||||
if (auto envSpawnPos = parseVec3Csv(std::getenv("WOW_SPAWN_POS"))) {
|
if (auto envSpawnPos = parseVec3Csv(std::getenv("WOW_SPAWN_POS"))) {
|
||||||
spawnCanonical = *envSpawnPos;
|
spawnCanonical = *envSpawnPos;
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,28 @@ struct SinglePlayerLootDb {
|
||||||
std::unordered_map<uint32_t, ItemTemplateRow> itemTemplates;
|
std::unordered_map<uint32_t, ItemTemplateRow> itemTemplates;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SinglePlayerCreateDb {
|
||||||
|
bool loaded = false;
|
||||||
|
std::unordered_map<uint16_t, GameHandler::SinglePlayerCreateInfo> rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SinglePlayerStartDb {
|
||||||
|
bool loaded = false;
|
||||||
|
struct StartItemRow {
|
||||||
|
uint8_t race = 0;
|
||||||
|
uint8_t cls = 0;
|
||||||
|
uint32_t itemId = 0;
|
||||||
|
int32_t amount = 1;
|
||||||
|
};
|
||||||
|
struct StartSpellRow {
|
||||||
|
uint32_t raceMask = 0;
|
||||||
|
uint32_t classMask = 0;
|
||||||
|
uint32_t spellId = 0;
|
||||||
|
};
|
||||||
|
std::vector<StartItemRow> items;
|
||||||
|
std::vector<StartSpellRow> spells;
|
||||||
|
};
|
||||||
|
|
||||||
static std::string trimSql(const std::string& s) {
|
static std::string trimSql(const std::string& s) {
|
||||||
size_t b = 0;
|
size_t b = 0;
|
||||||
while (b < s.size() && std::isspace(static_cast<unsigned char>(s[b]))) b++;
|
while (b < s.size() && std::isspace(static_cast<unsigned char>(s[b]))) b++;
|
||||||
|
|
@ -319,6 +341,129 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SinglePlayerCreateDb& getSinglePlayerCreateDb() {
|
||||||
|
static SinglePlayerCreateDb 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 createInfoPath = basePath / "playercreateinfo.sql";
|
||||||
|
if (!std::filesystem::exists(createInfoPath)) {
|
||||||
|
auto alt = basePath / "base";
|
||||||
|
if (std::filesystem::exists(alt / "playercreateinfo.sql")) {
|
||||||
|
basePath = alt;
|
||||||
|
createInfoPath = basePath / "playercreateinfo.sql";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(createInfoPath)) {
|
||||||
|
db.loaded = true;
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cols = loadCreateTableColumns(createInfoPath);
|
||||||
|
int idxRace = columnIndex(cols, "race");
|
||||||
|
int idxClass = columnIndex(cols, "class");
|
||||||
|
int idxMap = columnIndex(cols, "map");
|
||||||
|
int idxZone = columnIndex(cols, "zone");
|
||||||
|
int idxX = columnIndex(cols, "position_x");
|
||||||
|
int idxY = columnIndex(cols, "position_y");
|
||||||
|
int idxZ = columnIndex(cols, "position_z");
|
||||||
|
int idxO = columnIndex(cols, "orientation");
|
||||||
|
|
||||||
|
std::ifstream in(createInfoPath);
|
||||||
|
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||||
|
if (idxRace < 0 || idxClass < 0 || idxMap < 0 || idxZone < 0 ||
|
||||||
|
idxX < 0 || idxY < 0 || idxZ < 0 || idxO < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (idxRace >= static_cast<int>(row.size()) || idxClass >= static_cast<int>(row.size())) return;
|
||||||
|
try {
|
||||||
|
uint32_t race = static_cast<uint32_t>(std::stoul(row[idxRace]));
|
||||||
|
uint32_t cls = static_cast<uint32_t>(std::stoul(row[idxClass]));
|
||||||
|
GameHandler::SinglePlayerCreateInfo info;
|
||||||
|
info.mapId = static_cast<uint32_t>(std::stoul(row[idxMap]));
|
||||||
|
info.zoneId = static_cast<uint32_t>(std::stoul(row[idxZone]));
|
||||||
|
info.x = std::stof(row[idxX]);
|
||||||
|
info.y = std::stof(row[idxY]);
|
||||||
|
info.z = std::stof(row[idxZ]);
|
||||||
|
info.orientation = std::stof(row[idxO]);
|
||||||
|
uint16_t key = static_cast<uint16_t>((race << 8) | cls);
|
||||||
|
db.rows[key] = info;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.loaded = true;
|
||||||
|
LOG_INFO("Single-player create DB loaded from ", createInfoPath.string(),
|
||||||
|
" (rows=", db.rows.size(), ")");
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SinglePlayerStartDb& getSinglePlayerStartDb() {
|
||||||
|
static SinglePlayerStartDb 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 itemPath = basePath / "playercreateinfo_item.sql";
|
||||||
|
std::filesystem::path spellPath = basePath / "playercreateinfo_spell.sql";
|
||||||
|
if (!std::filesystem::exists(itemPath) || !std::filesystem::exists(spellPath)) {
|
||||||
|
auto alt = basePath / "base";
|
||||||
|
if (std::filesystem::exists(alt / "playercreateinfo_item.sql")) {
|
||||||
|
basePath = alt;
|
||||||
|
itemPath = basePath / "playercreateinfo_item.sql";
|
||||||
|
spellPath = basePath / "playercreateinfo_spell.sql";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::filesystem::exists(itemPath)) {
|
||||||
|
std::ifstream in(itemPath);
|
||||||
|
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||||
|
if (row.size() < 4) return;
|
||||||
|
try {
|
||||||
|
SinglePlayerStartDb::StartItemRow r;
|
||||||
|
r.race = static_cast<uint8_t>(std::stoul(row[0]));
|
||||||
|
r.cls = static_cast<uint8_t>(std::stoul(row[1]));
|
||||||
|
r.itemId = static_cast<uint32_t>(std::stoul(row[2]));
|
||||||
|
r.amount = static_cast<int32_t>(std::stol(row[3]));
|
||||||
|
db.items.push_back(r);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::filesystem::exists(spellPath)) {
|
||||||
|
std::ifstream in(spellPath);
|
||||||
|
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||||
|
if (row.size() < 3) return;
|
||||||
|
try {
|
||||||
|
SinglePlayerStartDb::StartSpellRow r;
|
||||||
|
r.raceMask = static_cast<uint32_t>(std::stoul(row[0]));
|
||||||
|
r.classMask = static_cast<uint32_t>(std::stoul(row[1]));
|
||||||
|
r.spellId = static_cast<uint32_t>(std::stoul(row[2]));
|
||||||
|
db.spells.push_back(r);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.loaded = true;
|
||||||
|
LOG_INFO("Single-player start DB loaded (items=", db.items.size(),
|
||||||
|
", spells=", db.spells.size(), ")");
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GameHandler::GameHandler() {
|
GameHandler::GameHandler() {
|
||||||
|
|
@ -849,15 +994,27 @@ void GameHandler::createCharacter(const CharCreateData& data) {
|
||||||
(static_cast<uint32_t>(data.hairStyle) << 16) |
|
(static_cast<uint32_t>(data.hairStyle) << 16) |
|
||||||
(static_cast<uint32_t>(data.hairColor) << 24);
|
(static_cast<uint32_t>(data.hairColor) << 24);
|
||||||
ch.facialFeatures = data.facialHair;
|
ch.facialFeatures = data.facialHair;
|
||||||
ch.zoneId = 12; // Elwynn Forest default
|
SinglePlayerCreateInfo createInfo;
|
||||||
ch.mapId = 0;
|
if (getSinglePlayerCreateInfo(data.race, data.characterClass, createInfo)) {
|
||||||
ch.x = -8949.95f;
|
ch.zoneId = createInfo.zoneId;
|
||||||
ch.y = -132.493f;
|
ch.mapId = createInfo.mapId;
|
||||||
ch.z = 83.5312f;
|
ch.x = createInfo.x;
|
||||||
|
ch.y = createInfo.y;
|
||||||
|
ch.z = createInfo.z;
|
||||||
|
} else {
|
||||||
|
ch.zoneId = 12; // Elwynn Forest default
|
||||||
|
ch.mapId = 0;
|
||||||
|
ch.x = -8949.95f;
|
||||||
|
ch.y = -132.493f;
|
||||||
|
ch.z = 83.5312f;
|
||||||
|
}
|
||||||
ch.guildId = 0;
|
ch.guildId = 0;
|
||||||
ch.flags = 0;
|
ch.flags = 0;
|
||||||
ch.pet = {};
|
ch.pet = {};
|
||||||
characters.push_back(ch);
|
characters.push_back(ch);
|
||||||
|
if (activeCharacterGuid_ == 0) {
|
||||||
|
activeCharacterGuid_ = ch.guid;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO("Single-player character created: ", ch.name);
|
LOG_INFO("Single-player character created: ", ch.name);
|
||||||
// Defer callback to next update() so ImGui frame completes first
|
// Defer callback to next update() so ImGui frame completes first
|
||||||
|
|
@ -910,6 +1067,101 @@ void GameHandler::handleCharCreateResponse(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Character* GameHandler::getActiveCharacter() const {
|
||||||
|
if (activeCharacterGuid_ == 0) return nullptr;
|
||||||
|
for (const auto& ch : characters) {
|
||||||
|
if (ch.guid == activeCharacterGuid_) return &ch;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Character* GameHandler::getFirstCharacter() const {
|
||||||
|
if (characters.empty()) return nullptr;
|
||||||
|
return &characters.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::setSinglePlayerCharListReady() {
|
||||||
|
setState(WorldState::CHAR_LIST_RECEIVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameHandler::getSinglePlayerCreateInfo(Race race, Class cls, SinglePlayerCreateInfo& out) const {
|
||||||
|
auto& db = getSinglePlayerCreateDb();
|
||||||
|
uint16_t key = static_cast<uint16_t>((static_cast<uint32_t>(race) << 8) |
|
||||||
|
static_cast<uint32_t>(cls));
|
||||||
|
auto it = db.rows.find(key);
|
||||||
|
if (it == db.rows.end()) return false;
|
||||||
|
out = it->second;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::applySinglePlayerStartData(Race race, Class cls) {
|
||||||
|
inventory = Inventory();
|
||||||
|
knownSpells.clear();
|
||||||
|
knownSpells.push_back(6603); // Attack
|
||||||
|
knownSpells.push_back(8690); // Hearthstone
|
||||||
|
|
||||||
|
for (auto& slot : actionBar) {
|
||||||
|
slot = ActionBarSlot{};
|
||||||
|
}
|
||||||
|
actionBar[0].type = ActionBarSlot::SPELL;
|
||||||
|
actionBar[0].id = 6603;
|
||||||
|
actionBar[11].type = ActionBarSlot::SPELL;
|
||||||
|
actionBar[11].id = 8690;
|
||||||
|
|
||||||
|
auto& startDb = getSinglePlayerStartDb();
|
||||||
|
auto& itemDb = getSinglePlayerLootDb().itemTemplates;
|
||||||
|
|
||||||
|
uint8_t raceVal = static_cast<uint8_t>(race);
|
||||||
|
uint8_t classVal = static_cast<uint8_t>(cls);
|
||||||
|
|
||||||
|
for (const auto& row : startDb.items) {
|
||||||
|
if (row.itemId == 0 || row.amount == 0) continue;
|
||||||
|
if (row.race != 0 && row.race != raceVal) continue;
|
||||||
|
if (row.cls != 0 && row.cls != classVal) continue;
|
||||||
|
if (row.amount < 0) continue;
|
||||||
|
|
||||||
|
ItemDef def;
|
||||||
|
def.itemId = row.itemId;
|
||||||
|
def.stackCount = static_cast<uint32_t>(row.amount);
|
||||||
|
def.maxStack = def.stackCount;
|
||||||
|
|
||||||
|
auto itTpl = itemDb.find(row.itemId);
|
||||||
|
if (itTpl != itemDb.end()) {
|
||||||
|
def.name = itTpl->second.name.empty()
|
||||||
|
? ("Item " + std::to_string(row.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(row.itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory.addItem(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t raceMask = 1u << (raceVal > 0 ? (raceVal - 1) : 0);
|
||||||
|
uint32_t classMask = 1u << (classVal > 0 ? (classVal - 1) : 0);
|
||||||
|
for (const auto& row : startDb.spells) {
|
||||||
|
if (row.spellId == 0) continue;
|
||||||
|
if (row.raceMask != 0 && (row.raceMask & raceMask) == 0) continue;
|
||||||
|
if (row.classMask != 0 && (row.classMask & classMask) == 0) continue;
|
||||||
|
if (std::find(knownSpells.begin(), knownSpells.end(), row.spellId) == knownSpells.end()) {
|
||||||
|
knownSpells.push_back(row.spellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-populate action bar with known spells
|
||||||
|
int slot = 1;
|
||||||
|
for (uint32_t spellId : knownSpells) {
|
||||||
|
if (spellId == 6603 || spellId == 8690) continue;
|
||||||
|
if (slot >= 11) break;
|
||||||
|
actionBar[slot].type = ActionBarSlot::SPELL;
|
||||||
|
actionBar[slot].id = spellId;
|
||||||
|
slot++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::selectCharacter(uint64_t characterGuid) {
|
void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||||
if (state != WorldState::CHAR_LIST_RECEIVED) {
|
if (state != WorldState::CHAR_LIST_RECEIVED) {
|
||||||
LOG_WARNING("Cannot select character in state: ", (int)state);
|
LOG_WARNING("Cannot select character in state: ", (int)state);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue