mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to deselect target, auto-target when attacked, fix critter hostility so neutral factions aren't flagged red, add armor/stats to item templates, fix loot iterator invalidation, show item template names as fallback, position drop confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster say/yell, and prevent auto-login on character select screen.
This commit is contained in:
parent
caeb6f56f7
commit
2aa8187562
10 changed files with 280 additions and 81 deletions
|
|
@ -360,6 +360,8 @@ public:
|
|||
auto it = itemInfoCache_.find(itemId);
|
||||
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
||||
}
|
||||
std::string getItemTemplateName(uint32_t itemId) const;
|
||||
ItemQuality getItemTemplateQuality(uint32_t itemId) const;
|
||||
uint64_t getBackpackItemGuid(int index) const {
|
||||
if (index < 0 || index >= static_cast<int>(backpackSlotGuids_.size())) return 0;
|
||||
return backpackSlotGuids_[index];
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@ private:
|
|||
game::EquipSlot getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv);
|
||||
void renderHeldItem();
|
||||
|
||||
// Drop confirmation
|
||||
bool dropConfirmOpen_ = false;
|
||||
int dropBackpackIndex_ = -1;
|
||||
std::string dropItemName_;
|
||||
|
||||
public:
|
||||
static ImVec4 getQualityColor(game::ItemQuality quality);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -642,10 +642,8 @@ void Application::setupUICallbacks() {
|
|||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
uint32_t enemyGroup = dbc->getUInt32(i, 5);
|
||||
uint32_t friendGroup = dbc->getUInt32(i, 4);
|
||||
bool hostile = (enemyGroup & playerFriendGroup) != 0;
|
||||
bool friendly = (friendGroup & playerFriendGroup) != 0;
|
||||
factionMap[id] = hostile ? true : (!friendly && enemyGroup == 0 && friendGroup == 0);
|
||||
factionMap[id] = hostile;
|
||||
}
|
||||
gameHandler->setFactionHostileMap(std::move(factionMap));
|
||||
LOG_INFO("Loaded faction hostility data (playerFriendGroup=0x", std::hex, playerFriendGroup, std::dec, ")");
|
||||
|
|
@ -938,6 +936,19 @@ void Application::spawnPlayerCharacter() {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Override hair texture on GPU (type-6 slot) after model load
|
||||
if (!hairTexturePath.empty()) {
|
||||
GLuint hairTex = charRenderer->loadTexture(hairTexturePath);
|
||||
if (hairTex != 0) {
|
||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||
if (model.textures[ti].type == 6) {
|
||||
charRenderer->setModelTexture(1, static_cast<uint32_t>(ti), hairTex);
|
||||
LOG_INFO("Applied DBC hair texture to slot ", ti, ": ", hairTexturePath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bodySkinPath_.clear();
|
||||
underwearPaths_.clear();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ struct ItemTemplateRow {
|
|||
uint8_t inventoryType = 0;
|
||||
int32_t maxStack = 1;
|
||||
uint32_t sellPrice = 0;
|
||||
int32_t armor = 0;
|
||||
int32_t stamina = 0;
|
||||
int32_t strength = 0;
|
||||
int32_t agility = 0;
|
||||
int32_t intellect = 0;
|
||||
int32_t spirit = 0;
|
||||
};
|
||||
|
||||
struct SinglePlayerLootDb {
|
||||
|
|
@ -163,6 +169,7 @@ struct SinglePlayerSqlite {
|
|||
" spirit INTEGER,"
|
||||
" display_info_id INTEGER,"
|
||||
" subclass_name TEXT,"
|
||||
" sell_price INTEGER DEFAULT 0,"
|
||||
" PRIMARY KEY (guid, location, slot)"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS character_spell ("
|
||||
|
|
@ -208,7 +215,10 @@ struct SinglePlayerSqlite {
|
|||
" mouse_sensitivity REAL,"
|
||||
" invert_mouse INTEGER"
|
||||
");";
|
||||
return exec(kSchema);
|
||||
if (!exec(kSchema)) return false;
|
||||
// Migration: add sell_price column to existing saves
|
||||
exec("ALTER TABLE character_inventory ADD COLUMN sell_price INTEGER DEFAULT 0;");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -502,6 +512,13 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() {
|
|||
int idxInvType = columnIndex(cols, "InventoryType");
|
||||
int idxStack = columnIndex(cols, "stackable");
|
||||
int idxSellPrice = columnIndex(cols, "SellPrice");
|
||||
int idxArmor = columnIndex(cols, "armor");
|
||||
// stat_type/stat_value pairs (up to 10)
|
||||
int idxStatType[10], idxStatVal[10];
|
||||
for (int si = 0; si < 10; si++) {
|
||||
idxStatType[si] = columnIndex(cols, "stat_type" + std::to_string(si + 1));
|
||||
idxStatVal[si] = columnIndex(cols, "stat_value" + std::to_string(si + 1));
|
||||
}
|
||||
if (idxEntry >= 0 && std::filesystem::exists(itemTemplatePath)) {
|
||||
std::ifstream in(itemTemplatePath);
|
||||
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||
|
|
@ -528,6 +545,27 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() {
|
|||
if (idxSellPrice >= 0 && idxSellPrice < static_cast<int>(row.size())) {
|
||||
ir.sellPrice = static_cast<uint32_t>(std::stoul(row[idxSellPrice]));
|
||||
}
|
||||
if (idxArmor >= 0 && idxArmor < static_cast<int>(row.size())) {
|
||||
ir.armor = static_cast<int32_t>(std::stol(row[idxArmor]));
|
||||
}
|
||||
// Parse stat_type/stat_value pairs (protected from parse errors)
|
||||
for (int si = 0; si < 10; si++) {
|
||||
try {
|
||||
if (idxStatType[si] < 0 || idxStatVal[si] < 0) continue;
|
||||
if (idxStatType[si] >= static_cast<int>(row.size())) continue;
|
||||
if (idxStatVal[si] >= static_cast<int>(row.size())) continue;
|
||||
int stype = std::stoi(row[idxStatType[si]]);
|
||||
int sval = std::stoi(row[idxStatVal[si]]);
|
||||
if (sval == 0) continue;
|
||||
switch (stype) {
|
||||
case 3: ir.agility += sval; break;
|
||||
case 4: ir.strength += sval; break;
|
||||
case 5: ir.intellect += sval; break;
|
||||
case 6: ir.spirit += sval; break;
|
||||
case 7: ir.stamina += sval; break;
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
db.itemTemplates[ir.itemId] = std::move(ir);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
|
|
@ -1671,7 +1709,8 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) {
|
|||
inventory = Inventory();
|
||||
const char* sqlInv =
|
||||
"SELECT location, slot, item_id, name, quality, inventory_type, stack_count, max_stack, bag_slots, "
|
||||
"armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name "
|
||||
"armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name, "
|
||||
"COALESCE(sell_price, 0) "
|
||||
"FROM character_inventory WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, sqlInv, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(guid));
|
||||
|
|
@ -1696,6 +1735,23 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) {
|
|||
def.displayInfoId = static_cast<uint32_t>(sqlite3_column_int(stmt, 15));
|
||||
const unsigned char* subclassText = sqlite3_column_text(stmt, 16);
|
||||
def.subclassName = subclassText ? reinterpret_cast<const char*>(subclassText) : "";
|
||||
def.sellPrice = static_cast<uint32_t>(sqlite3_column_int(stmt, 17));
|
||||
|
||||
// Fill missing data from item template DB (for old saves)
|
||||
if (def.itemId != 0) {
|
||||
auto& itemDb = getSinglePlayerLootDb().itemTemplates;
|
||||
auto itTpl = itemDb.find(def.itemId);
|
||||
if (itTpl != itemDb.end()) {
|
||||
if (def.sellPrice == 0) def.sellPrice = itTpl->second.sellPrice;
|
||||
if (def.displayInfoId == 0) def.displayInfoId = itTpl->second.displayId;
|
||||
if (def.armor == 0) def.armor = itTpl->second.armor;
|
||||
if (def.stamina == 0) def.stamina = itTpl->second.stamina;
|
||||
if (def.strength == 0) def.strength = itTpl->second.strength;
|
||||
if (def.agility == 0) def.agility = itTpl->second.agility;
|
||||
if (def.intellect == 0) def.intellect = itTpl->second.intellect;
|
||||
if (def.spirit == 0) def.spirit = itTpl->second.spirit;
|
||||
}
|
||||
}
|
||||
|
||||
if (location == 0) {
|
||||
inventory.setBackpackSlot(slot, def);
|
||||
|
|
@ -1864,6 +1920,13 @@ void GameHandler::applySinglePlayerStartData(Race race, Class cls) {
|
|||
def.inventoryType = itTpl->second.inventoryType;
|
||||
def.maxStack = std::max(def.maxStack, static_cast<uint32_t>(itTpl->second.maxStack));
|
||||
def.sellPrice = itTpl->second.sellPrice;
|
||||
def.displayInfoId = itTpl->second.displayId;
|
||||
def.armor = itTpl->second.armor;
|
||||
def.stamina = itTpl->second.stamina;
|
||||
def.strength = itTpl->second.strength;
|
||||
def.agility = itTpl->second.agility;
|
||||
def.intellect = itTpl->second.intellect;
|
||||
def.spirit = itTpl->second.spirit;
|
||||
} else {
|
||||
def.name = "Item " + std::to_string(row.itemId);
|
||||
}
|
||||
|
|
@ -2026,8 +2089,8 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) {
|
|||
const char* insInv =
|
||||
"INSERT INTO character_inventory "
|
||||
"(guid, location, slot, item_id, name, quality, inventory_type, stack_count, max_stack, bag_slots, "
|
||||
"armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
|
||||
"armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name, sell_price) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
|
||||
if (sqlite3_prepare_v2(sp.db, insInv, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
for (int i = 0; i < Inventory::BACKPACK_SLOTS; i++) {
|
||||
const ItemSlot& slot = inventory.getBackpackSlot(i);
|
||||
|
|
@ -2050,6 +2113,7 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) {
|
|||
sqlite3_bind_int(stmt, 16, static_cast<int>(slot.item.spirit));
|
||||
sqlite3_bind_int(stmt, 17, static_cast<int>(slot.item.displayInfoId));
|
||||
sqlite3_bind_text(stmt, 18, slot.item.subclassName.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 19, static_cast<int>(slot.item.sellPrice));
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
|
@ -2075,6 +2139,7 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) {
|
|||
sqlite3_bind_int(stmt, 16, static_cast<int>(slot.item.spirit));
|
||||
sqlite3_bind_int(stmt, 17, static_cast<int>(slot.item.displayInfoId));
|
||||
sqlite3_bind_text(stmt, 18, slot.item.subclassName.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 19, static_cast<int>(slot.item.sellPrice));
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
|
@ -3636,6 +3701,13 @@ void GameHandler::lootItem(uint8_t slotIndex) {
|
|||
def.inventoryType = itTpl->second.inventoryType;
|
||||
def.maxStack = std::max(def.maxStack, static_cast<uint32_t>(itTpl->second.maxStack));
|
||||
def.sellPrice = itTpl->second.sellPrice;
|
||||
def.displayInfoId = itTpl->second.displayId;
|
||||
def.armor = itTpl->second.armor;
|
||||
def.stamina = itTpl->second.stamina;
|
||||
def.strength = itTpl->second.strength;
|
||||
def.agility = itTpl->second.agility;
|
||||
def.intellect = itTpl->second.intellect;
|
||||
def.spirit = itTpl->second.spirit;
|
||||
} else {
|
||||
def.name = "Item " + std::to_string(it->itemId);
|
||||
}
|
||||
|
|
@ -3819,11 +3891,15 @@ void GameHandler::handleLootResponse(network::Packet& packet) {
|
|||
if (!LootResponseParser::parse(packet, currentLoot)) return;
|
||||
lootWindowOpen = true;
|
||||
if (currentLoot.gold > 0) {
|
||||
if (singlePlayerMode_) {
|
||||
addMoneyCopper(currentLoot.gold);
|
||||
}
|
||||
std::string msg = "You loot ";
|
||||
msg += std::to_string(currentLoot.getGold()) + "g ";
|
||||
msg += std::to_string(currentLoot.getSilver()) + "s ";
|
||||
msg += std::to_string(currentLoot.getCopper()) + "c.";
|
||||
addSystemChatMessage(msg);
|
||||
currentLoot.gold = 0; // Clear gold from loot window after collecting
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4025,6 +4101,11 @@ void GameHandler::performNpcSwing(uint64_t guid) {
|
|||
if (!entity || entity->getType() != ObjectType::UNIT) return;
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
|
||||
// Auto-target the attacker if player has no current target
|
||||
if (targetGuid == 0) {
|
||||
setTarget(guid);
|
||||
}
|
||||
|
||||
if (npcSwingCallback_) {
|
||||
npcSwingCallback_(guid);
|
||||
}
|
||||
|
|
@ -4374,5 +4455,19 @@ void GameHandler::fail(const std::string& reason) {
|
|||
}
|
||||
}
|
||||
|
||||
std::string GameHandler::getItemTemplateName(uint32_t itemId) const {
|
||||
auto& db = getSinglePlayerLootDb();
|
||||
auto it = db.itemTemplates.find(itemId);
|
||||
if (it != db.itemTemplates.end()) return it->second.name;
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemQuality GameHandler::getItemTemplateQuality(uint32_t itemId) const {
|
||||
auto& db = getSinglePlayerLootDb();
|
||||
auto it = db.itemTemplates.find(itemId);
|
||||
if (it != db.itemTemplates.end()) return static_cast<ItemQuality>(it->second.quality);
|
||||
return ItemQuality::COMMON;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -740,13 +740,9 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
|||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
uint32_t enemyGroup = dbc->getUInt32(i, 5);
|
||||
uint32_t friendGroup = dbc->getUInt32(i, 4);
|
||||
// Hostile if creature's enemy groups overlap player's faction/friend groups
|
||||
// Hostile only if creature's enemy groups overlap player's faction/friend groups
|
||||
bool hostile = (enemyGroup & playerFriendGroup) != 0;
|
||||
// Friendly only if creature's friendGroup explicitly includes player's groups
|
||||
bool friendly = (friendGroup & playerFriendGroup) != 0;
|
||||
// Hostile if explicitly hostile, or if no explicit relationship at all
|
||||
factionHostile[id] = hostile ? true : (!friendly && enemyGroup == 0 && friendGroup == 0);
|
||||
factionHostile[id] = hostile;
|
||||
}
|
||||
LOG_INFO("NpcManager: loaded ", dbc->getRecordCount(),
|
||||
" faction templates (playerFriendGroup=0x", std::hex, playerFriendGroup, std::dec, ")");
|
||||
|
|
@ -802,7 +798,7 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
|||
|
||||
// Determine hostility from faction template
|
||||
auto fIt = factionHostile.find(s.faction);
|
||||
unit->setHostile(fIt != factionHostile.end() ? fIt->second : true);
|
||||
unit->setHostile(fIt != factionHostile.end() ? fIt->second : false);
|
||||
|
||||
// Store canonical WoW coordinates for targeting/server compatibility
|
||||
glm::vec3 spawnCanonical = core::coords::renderToCanonical(glPos);
|
||||
|
|
|
|||
|
|
@ -62,17 +62,13 @@ void CameraController::startIntroPan(float durationSec, float orbitDegrees) {
|
|||
introTimer = 0.0f;
|
||||
idleTimer_ = 0.0f;
|
||||
introDuration = std::max(0.5f, durationSec);
|
||||
introStartYaw = facingYaw + orbitDegrees;
|
||||
introEndYaw = facingYaw;
|
||||
introStartYaw = yaw;
|
||||
introEndYaw = yaw - orbitDegrees;
|
||||
introOrbitDegrees = orbitDegrees;
|
||||
introStartPitch = -32.0f;
|
||||
introEndPitch = -10.0f;
|
||||
introStartDistance = 18.0f;
|
||||
introEndDistance = 10.0f;
|
||||
yaw = introStartYaw;
|
||||
pitch = introStartPitch;
|
||||
currentDistance = introStartDistance;
|
||||
userTargetDistance = introEndDistance;
|
||||
introStartPitch = pitch;
|
||||
introEndPitch = pitch;
|
||||
introStartDistance = currentDistance;
|
||||
introEndDistance = currentDistance;
|
||||
thirdPerson = true;
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +101,7 @@ void CameraController::update(float deltaTime) {
|
|||
idleTimer_ += deltaTime;
|
||||
if (idleTimer_ >= IDLE_TIMEOUT) {
|
||||
idleTimer_ = 0.0f;
|
||||
startIntroPan(6.0f, 360.0f); // Slow full orbit
|
||||
startIntroPan(30.0f, 360.0f); // Slow casual orbit over 30 seconds
|
||||
idleOrbit_ = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -117,19 +113,21 @@ void CameraController::update(float deltaTime) {
|
|||
idleTimer_ = 0.0f;
|
||||
} else {
|
||||
introTimer += deltaTime;
|
||||
float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f;
|
||||
yaw = introStartYaw + (introEndYaw - introStartYaw) * t;
|
||||
pitch = introStartPitch + (introEndPitch - introStartPitch) * t;
|
||||
currentDistance = introStartDistance + (introEndDistance - introStartDistance) * t;
|
||||
userTargetDistance = introEndDistance;
|
||||
camera->setRotation(yaw, pitch);
|
||||
facingYaw = yaw;
|
||||
if (t >= 1.0f) {
|
||||
if (idleOrbit_) {
|
||||
// Loop: restart the slow orbit continuously
|
||||
startIntroPan(6.0f, 360.0f);
|
||||
idleOrbit_ = true;
|
||||
} else {
|
||||
if (idleOrbit_) {
|
||||
// Continuous smooth rotation — no lerp endpoint, just constant angular velocity
|
||||
float degreesPerSec = introOrbitDegrees / introDuration;
|
||||
yaw -= degreesPerSec * deltaTime;
|
||||
camera->setRotation(yaw, pitch);
|
||||
facingYaw = yaw;
|
||||
} else {
|
||||
float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f;
|
||||
yaw = introStartYaw + (introEndYaw - introStartYaw) * t;
|
||||
pitch = introStartPitch + (introEndPitch - introStartPitch) * t;
|
||||
currentDistance = introStartDistance + (introEndDistance - introStartDistance) * t;
|
||||
userTargetDistance = introEndDistance;
|
||||
camera->setRotation(yaw, pitch);
|
||||
facingYaw = yaw;
|
||||
if (t >= 1.0f) {
|
||||
introActive = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,21 +33,13 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
gameHandler.requestCharacterList();
|
||||
} else if (characters.empty()) {
|
||||
ImGui::Text("No characters available.");
|
||||
} else if (characters.size() == 1 && !characterSelected) {
|
||||
// Auto-select the only available character
|
||||
selectedCharacterIndex = 0;
|
||||
selectedCharacterGuid = characters[0].guid;
|
||||
characterSelected = true;
|
||||
std::stringstream ss;
|
||||
ss << "Entering world with " << characters[0].name << "...";
|
||||
setStatus(ss.str());
|
||||
if (!gameHandler.isSinglePlayerMode()) {
|
||||
gameHandler.selectCharacter(characters[0].guid);
|
||||
}
|
||||
if (onCharacterSelected) {
|
||||
onCharacterSelected(characters[0].guid);
|
||||
}
|
||||
} else {
|
||||
// Auto-highlight the first character if none selected yet
|
||||
if (selectedCharacterIndex < 0 && !characters.empty()) {
|
||||
selectedCharacterIndex = 0;
|
||||
selectedCharacterGuid = characters[0].guid;
|
||||
}
|
||||
|
||||
// Character table
|
||||
if (ImGui::BeginTable("CharactersTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||
|
|
|
|||
|
|
@ -362,10 +362,17 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
ImVec4 color = getChatTypeColor(msg.type);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
||||
|
||||
if (msg.type == game::ChatType::TEXT_EMOTE) {
|
||||
if (msg.type == game::ChatType::SYSTEM) {
|
||||
// System messages: just yellow text, no header
|
||||
ImGui::TextWrapped("%s", msg.message.c_str());
|
||||
} else if (msg.type == game::ChatType::TEXT_EMOTE) {
|
||||
ImGui::TextWrapped("You %s", msg.message.c_str());
|
||||
} else if (!msg.senderName.empty()) {
|
||||
ImGui::TextWrapped("[%s] %s: %s", getChatTypeName(msg.type), msg.senderName.c_str(), msg.message.c_str());
|
||||
if (msg.type == game::ChatType::MONSTER_SAY || msg.type == game::ChatType::MONSTER_YELL) {
|
||||
ImGui::TextWrapped("%s says: %s", msg.senderName.c_str(), msg.message.c_str());
|
||||
} else {
|
||||
ImGui::TextWrapped("[%s] %s: %s", getChatTypeName(msg.type), msg.senderName.c_str(), msg.message.c_str());
|
||||
}
|
||||
} else {
|
||||
ImGui::TextWrapped("[%s] %s", getChatTypeName(msg.type), msg.message.c_str());
|
||||
}
|
||||
|
|
@ -521,14 +528,58 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
|
||||
if (closestGuid != 0) {
|
||||
gameHandler.setTarget(closestGuid);
|
||||
} else {
|
||||
// Clicked empty space — deselect current target
|
||||
gameHandler.clearTarget();
|
||||
}
|
||||
// Don't clear on miss — left-click is also used for camera orbit
|
||||
}
|
||||
}
|
||||
|
||||
// Right-click on target for NPC interaction / loot / auto-attack
|
||||
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
||||
// Suppress when left button is held (both-button run)
|
||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
||||
// If no target or right-clicking in world, try to pick one under cursor
|
||||
{
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
if (camera && window) {
|
||||
glm::vec2 mousePos = input.getMousePosition();
|
||||
float screenW = static_cast<float>(window->getWidth());
|
||||
float screenH = static_cast<float>(window->getHeight());
|
||||
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
||||
float closestT = 1e30f;
|
||||
uint64_t closestGuid = 0;
|
||||
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue;
|
||||
if (guid == myGuid) continue;
|
||||
float hitRadius = 1.5f;
|
||||
float heightOffset = 1.5f;
|
||||
if (t == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
||||
hitRadius = 0.5f;
|
||||
heightOffset = 0.3f;
|
||||
}
|
||||
}
|
||||
glm::vec3 entityGL = core::coords::canonicalToRender(
|
||||
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
entityGL.z += heightOffset;
|
||||
float hitT;
|
||||
if (raySphereIntersect(ray, entityGL, hitRadius, hitT)) {
|
||||
if (hitT < closestT) {
|
||||
closestT = hitT;
|
||||
closestGuid = guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (closestGuid != 0) {
|
||||
gameHandler.setTarget(closestGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gameHandler.hasTarget()) {
|
||||
auto target = gameHandler.getTarget();
|
||||
if (target) {
|
||||
|
|
@ -1734,17 +1785,27 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
// Items with icons and labels
|
||||
constexpr float iconSize = 32.0f;
|
||||
int lootSlotClicked = -1; // defer loot pickup to avoid iterator invalidation
|
||||
for (const auto& item : loot.items) {
|
||||
ImGui::PushID(item.slotIndex);
|
||||
|
||||
// Get item info for name and quality
|
||||
const auto* info = gameHandler.getItemInfo(item.itemId);
|
||||
std::string itemName = info && !info->name.empty()
|
||||
? info->name
|
||||
: "Item #" + std::to_string(item.itemId);
|
||||
game::ItemQuality quality = info
|
||||
? static_cast<game::ItemQuality>(info->quality)
|
||||
: game::ItemQuality::COMMON;
|
||||
std::string itemName;
|
||||
game::ItemQuality quality = game::ItemQuality::COMMON;
|
||||
if (info && !info->name.empty()) {
|
||||
itemName = info->name;
|
||||
quality = static_cast<game::ItemQuality>(info->quality);
|
||||
} else {
|
||||
// Fallback: look up name from item template DB (single-player)
|
||||
auto tplName = gameHandler.getItemTemplateName(item.itemId);
|
||||
if (!tplName.empty()) {
|
||||
itemName = tplName;
|
||||
quality = gameHandler.getItemTemplateQuality(item.itemId);
|
||||
} else {
|
||||
itemName = "Item #" + std::to_string(item.itemId);
|
||||
}
|
||||
}
|
||||
ImVec4 qColor = InventoryScreen::getQualityColor(quality);
|
||||
|
||||
// Get item icon
|
||||
|
|
@ -1757,7 +1818,7 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
// Invisible selectable for click handling
|
||||
if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) {
|
||||
gameHandler.lootItem(item.slotIndex);
|
||||
lootSlotClicked = item.slotIndex;
|
||||
}
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
|
|
@ -1802,6 +1863,11 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Process deferred loot pickup (after loop to avoid iterator invalidation)
|
||||
if (lootSlotClicked >= 0) {
|
||||
gameHandler.lootItem(static_cast<uint8_t>(lootSlotClicked));
|
||||
}
|
||||
|
||||
if (loot.items.empty() && loot.gold == 0) {
|
||||
ImGui::TextDisabled("Empty");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -587,6 +587,43 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
|||
static_cast<unsigned long long>(copper));
|
||||
ImGui::End();
|
||||
|
||||
// Detect held item dropped outside inventory windows → drop confirmation
|
||||
if (holdingItem && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) {
|
||||
dropConfirmOpen_ = true;
|
||||
dropItemName_ = heldItem.name;
|
||||
}
|
||||
|
||||
// Drop item confirmation popup — positioned near cursor
|
||||
if (dropConfirmOpen_) {
|
||||
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
||||
ImGui::SetNextWindowPos(ImVec2(mousePos.x - 80.0f, mousePos.y - 20.0f), ImGuiCond_Always);
|
||||
ImGui::OpenPopup("##DropItem");
|
||||
dropConfirmOpen_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopup("##DropItem", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
ImGui::Text("Destroy \"%s\"?", dropItemName_.c_str());
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Yes", ImVec2(80, 0))) {
|
||||
holdingItem = false;
|
||||
heldItem = game::ItemDef{};
|
||||
heldSource = HeldSource::NONE;
|
||||
inventoryDirty = true;
|
||||
if (gameHandler_) {
|
||||
gameHandler_->notifyInventoryChanged();
|
||||
}
|
||||
dropItemName_.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("No", ImVec2(80, 0))) {
|
||||
cancelPickup(inventory);
|
||||
dropItemName_.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Draw held item at cursor
|
||||
renderHeldItem();
|
||||
}
|
||||
|
|
@ -617,7 +654,7 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(350.0f, 650.0f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(380.0f, 650.0f), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Character", &characterOpen, flags)) {
|
||||
|
|
@ -640,8 +677,8 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
|
||||
renderEquipmentPanel(inventory);
|
||||
|
||||
// Stats panel
|
||||
ImGui::Spacing();
|
||||
// Stats panel — use full width and separate from equipment layout
|
||||
ImGui::SetCursorPosX(ImGui::GetStyle().WindowPadding.x);
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
|
@ -1114,16 +1151,13 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
|
|||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
|
||||
}
|
||||
|
||||
// Sell price (when vendor is open)
|
||||
if (vendorMode_ && gameHandler_) {
|
||||
const auto* info = gameHandler_->getItemInfo(item.itemId);
|
||||
if (info && info->sellPrice > 0) {
|
||||
uint32_t g = info->sellPrice / 10000;
|
||||
uint32_t s = (info->sellPrice / 100) % 100;
|
||||
uint32_t c = info->sellPrice % 100;
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell Price: %ug %us %uc", g, s, c);
|
||||
}
|
||||
// Sell price
|
||||
if (item.sellPrice > 0) {
|
||||
uint32_t g = item.sellPrice / 10000;
|
||||
uint32_t s = (item.sellPrice / 100) % 100;
|
||||
uint32_t c = item.sellPrice % 100;
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell Price: %ug %us %uc", g, s, c);
|
||||
}
|
||||
|
||||
ImGui::EndTooltip();
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
}
|
||||
|
||||
uint32_t fieldCount = dbc->getFieldCount();
|
||||
if (fieldCount < 142) {
|
||||
if (fieldCount < 154) {
|
||||
LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+");
|
||||
return;
|
||||
}
|
||||
|
||||
// WoW 3.3.5a Spell.dbc fields:
|
||||
// 0 = SpellID, 75 = Attributes, 133 = SpellIconID, 136 = SpellName, 141 = RankText
|
||||
// WoW 3.3.5a Spell.dbc fields (0-based):
|
||||
// 0 = SpellID, 4 = Attributes, 133 = SpellIconID, 136 = SpellName_enUS, 153 = RankText_enUS
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t spellId = dbc->getUInt32(i, 0);
|
||||
|
|
@ -50,10 +50,10 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
|
||||
SpellInfo info;
|
||||
info.spellId = spellId;
|
||||
info.attributes = dbc->getUInt32(i, 75);
|
||||
info.attributes = dbc->getUInt32(i, 4);
|
||||
info.iconId = dbc->getUInt32(i, 133);
|
||||
info.name = dbc->getString(i, 136);
|
||||
info.rank = dbc->getString(i, 141);
|
||||
info.rank = dbc->getString(i, 153);
|
||||
|
||||
if (!info.name.empty()) {
|
||||
spellData[spellId] = std::move(info);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue