mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: profession crafting improvements and combat sound fixes
- Suppress spell sounds for profession/tradeskill spells (crafting is silent) - Add craft quantity UI to profession trainer: recipe selector, quantity input, Create button, and Stop button for active queue - Known recipes show Create button to cast directly from trainer window - Craft queue auto-recasts on CREATE_ITEM completion, cancels on failure - Fix missing combat sounds: player spell impacts on enemies, enemy spell cast sounds targeting player, instant melee ability weapon sounds
This commit is contained in:
parent
502d506a44
commit
7b03d5363b
3 changed files with 278 additions and 59 deletions
|
|
@ -2217,6 +2217,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
lastInteractedGoGuid_ = 0;
|
||||
// Cancel craft queue on cast failure
|
||||
craftQueueSpellId_ = 0;
|
||||
craftQueueRemaining_ = 0;
|
||||
// Pass player's power type so result 85 says "Not enough rage/energy/etc."
|
||||
int playerPowerType = -1;
|
||||
if (auto pe = entityManager.getEntity(playerGuid)) {
|
||||
|
|
@ -6774,6 +6777,16 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
addSystemChatMessage(msg);
|
||||
LOG_DEBUG("SMSG_SPELLLOGEXECUTE CREATE_ITEM: spell=", exeSpellId,
|
||||
" item=", itemEntry, " name=", itemName);
|
||||
|
||||
// Repeat-craft queue: re-cast if more crafts remaining
|
||||
if (craftQueueRemaining_ > 0 && craftQueueSpellId_ == exeSpellId) {
|
||||
--craftQueueRemaining_;
|
||||
if (craftQueueRemaining_ > 0) {
|
||||
castSpell(craftQueueSpellId_, 0);
|
||||
} else {
|
||||
craftQueueSpellId_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (effectType == 26) {
|
||||
|
|
@ -17696,6 +17709,21 @@ void GameHandler::cancelCast() {
|
|||
castIsChannel = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
// Cancel craft queue when player manually cancels cast
|
||||
craftQueueSpellId_ = 0;
|
||||
craftQueueRemaining_ = 0;
|
||||
}
|
||||
|
||||
void GameHandler::startCraftQueue(uint32_t spellId, int count) {
|
||||
craftQueueSpellId_ = spellId;
|
||||
craftQueueRemaining_ = count;
|
||||
// Cast the first one immediately
|
||||
castSpell(spellId, 0);
|
||||
}
|
||||
|
||||
void GameHandler::cancelCraftQueue() {
|
||||
craftQueueSpellId_ = 0;
|
||||
craftQueueRemaining_ = 0;
|
||||
}
|
||||
|
||||
void GameHandler::cancelAura(uint32_t spellId) {
|
||||
|
|
@ -18022,14 +18050,17 @@ void GameHandler::handleSpellStart(network::Packet& packet) {
|
|||
castTimeRemaining = castTimeTotal;
|
||||
|
||||
// Play precast (channeling) sound with correct magic school
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||
loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(data.spellId);
|
||||
auto school = (it != spellNameCache_.end() && it->second.schoolMask)
|
||||
? schoolMaskToMagicSchool(it->second.schoolMask)
|
||||
: audio::SpellSoundManager::MagicSchool::ARCANE;
|
||||
ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM);
|
||||
// Skip sound for profession/tradeskill spells (crafting should be silent)
|
||||
if (!isProfessionSpell(data.spellId)) {
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||
loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(data.spellId);
|
||||
auto school = (it != spellNameCache_.end() && it->second.schoolMask)
|
||||
? schoolMaskToMagicSchool(it->second.schoolMask)
|
||||
: audio::SpellSoundManager::MagicSchool::ARCANE;
|
||||
ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -18055,14 +18086,17 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
|||
// Cast completed
|
||||
if (data.casterUnit == playerGuid) {
|
||||
// Play cast-complete sound with correct magic school
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||
loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(data.spellId);
|
||||
auto school = (it != spellNameCache_.end() && it->second.schoolMask)
|
||||
? schoolMaskToMagicSchool(it->second.schoolMask)
|
||||
: audio::SpellSoundManager::MagicSchool::ARCANE;
|
||||
ssm->playCast(school);
|
||||
// Skip sound for profession/tradeskill spells (crafting should be silent)
|
||||
if (!isProfessionSpell(data.spellId)) {
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||
loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(data.spellId);
|
||||
auto school = (it != spellNameCache_.end() && it->second.schoolMask)
|
||||
? schoolMaskToMagicSchool(it->second.schoolMask)
|
||||
: audio::SpellSoundManager::MagicSchool::ARCANE;
|
||||
ssm->playCast(school);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -18082,8 +18116,16 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
|||
isMeleeAbility = (currentCastSpellId != sid);
|
||||
}
|
||||
}
|
||||
if (isMeleeAbility && meleeSwingCallback_) {
|
||||
meleeSwingCallback_();
|
||||
if (isMeleeAbility) {
|
||||
if (meleeSwingCallback_) meleeSwingCallback_();
|
||||
// Play weapon swing + impact sound for instant melee abilities (Sinister Strike, etc.)
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* csm = renderer->getCombatSoundManager()) {
|
||||
csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false);
|
||||
csm->playImpact(audio::CombatSoundManager::WeaponSize::MEDIUM,
|
||||
audio::CombatSoundManager::ImpactType::FLESH, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture cast state before clearing. Guard with spellId match so that
|
||||
|
|
@ -18110,9 +18152,28 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
|||
if (spellCastAnimCallback_) {
|
||||
spellCastAnimCallback_(playerGuid, false, false);
|
||||
}
|
||||
} else if (spellCastAnimCallback_) {
|
||||
// End cast animation on other unit
|
||||
spellCastAnimCallback_(data.casterUnit, false, false);
|
||||
} else {
|
||||
if (spellCastAnimCallback_) {
|
||||
// End cast animation on other unit
|
||||
spellCastAnimCallback_(data.casterUnit, false, false);
|
||||
}
|
||||
// Play cast-complete sound for enemy spells targeting the player
|
||||
bool targetsPlayer = false;
|
||||
for (const auto& tgt : data.hitTargets) {
|
||||
if (tgt == playerGuid) { targetsPlayer = true; break; }
|
||||
}
|
||||
if (targetsPlayer) {
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||
loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(data.spellId);
|
||||
auto school = (it != spellNameCache_.end() && it->second.schoolMask)
|
||||
? schoolMaskToMagicSchool(it->second.schoolMask)
|
||||
: audio::SpellSoundManager::MagicSchool::ARCANE;
|
||||
ssm->playCast(school);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear unit cast bar when the spell lands (for any tracked unit)
|
||||
|
|
@ -18133,12 +18194,16 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
// Play impact sound when player is hit by any spell (from self or others)
|
||||
// Play impact sound for spell hits involving the player
|
||||
// - When player is hit by an enemy spell
|
||||
// - When player's spell hits an enemy target
|
||||
bool playerIsHit = false;
|
||||
bool playerHitEnemy = false;
|
||||
for (const auto& tgt : data.hitTargets) {
|
||||
if (tgt == playerGuid) { playerIsHit = true; break; }
|
||||
if (tgt == playerGuid) { playerIsHit = true; }
|
||||
if (data.casterUnit == playerGuid && tgt != playerGuid && tgt != 0) { playerHitEnemy = true; }
|
||||
}
|
||||
if (playerIsHit && data.casterUnit != playerGuid) {
|
||||
if (playerIsHit || playerHitEnemy) {
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||
loadSpellNameCache();
|
||||
|
|
@ -19327,7 +19392,7 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
float dy = entity->getY() - movementInfo.y;
|
||||
float dz = entity->getZ() - movementInfo.z;
|
||||
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (dist3d > 6.0f) {
|
||||
if (dist3d > 10.0f) {
|
||||
addSystemChatMessage("Too far away.");
|
||||
return;
|
||||
}
|
||||
|
|
@ -19391,13 +19456,17 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
}
|
||||
}
|
||||
if (shouldSendLoot) {
|
||||
lootTarget(guid);
|
||||
// Some servers/scripts only make certain quest/chest GOs lootable after a short delay
|
||||
// (use animation, state change). Queue one delayed loot attempt to catch that case.
|
||||
// Don't send CMSG_LOOT immediately — give the server time to process
|
||||
// CMSG_GAMEOBJ_USE first (chests need to transition to lootable state,
|
||||
// gathering nodes start a spell cast). A premature CMSG_LOOT can cause
|
||||
// an empty SMSG_LOOT_RESPONSE that clears our gather-cast loot state.
|
||||
pendingGameObjectLootOpens_.erase(
|
||||
std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(),
|
||||
[&](const PendingLootOpen& p) { return p.guid == guid; }),
|
||||
pendingGameObjectLootOpens_.end());
|
||||
// Short delay for chests (server makes them lootable quickly after USE),
|
||||
// plus a longer retry to catch slow state transitions.
|
||||
pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.20f});
|
||||
pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.75f});
|
||||
} else {
|
||||
// Non-lootable interaction (mailbox, door, button, etc.) — no CMSG_LOOT will be
|
||||
|
|
@ -19405,12 +19474,9 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
// guid now so a subsequent timed cast completion can't fire a spurious CMSG_LOOT.
|
||||
lastInteractedGoGuid_ = 0;
|
||||
}
|
||||
// Retry use briefly to survive packet loss/order races.
|
||||
const bool retryLoot = shouldSendLoot;
|
||||
const bool retryUse = turtleMode || isActiveExpansion("classic");
|
||||
if (retryUse || retryLoot) {
|
||||
pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot});
|
||||
}
|
||||
// Don't retry CMSG_GAMEOBJ_USE — resending can toggle chest state on some
|
||||
// servers (opening→closing the chest). The delayed CMSG_LOOT retries above
|
||||
// handle the case where the first loot attempt arrives too early.
|
||||
}
|
||||
|
||||
void GameHandler::selectGossipOption(uint32_t optionId) {
|
||||
|
|
@ -20584,6 +20650,13 @@ void GameHandler::handleLootResponse(network::Packet& packet) {
|
|||
// WotLK 3.3.5a uses 22 bytes/item.
|
||||
const bool wotlkLoot = isActiveExpansion("wotlk");
|
||||
if (!LootResponseParser::parse(packet, currentLoot, wotlkLoot)) return;
|
||||
const bool hasLoot = !currentLoot.items.empty() || currentLoot.gold > 0;
|
||||
// If we're mid-gather-cast and got an empty loot response (premature CMSG_LOOT
|
||||
// before the node became lootable), ignore it — don't clear our gather state.
|
||||
if (!hasLoot && casting && currentCastSpellId != 0 && lastInteractedGoGuid_ != 0) {
|
||||
LOG_DEBUG("Ignoring empty SMSG_LOOT_RESPONSE during gather cast");
|
||||
return;
|
||||
}
|
||||
lootWindowOpen = true;
|
||||
lastInteractedGoGuid_ = 0; // loot opened — no need to re-send in handleSpellGo
|
||||
pendingGameObjectLootOpens_.erase(
|
||||
|
|
@ -22667,6 +22740,15 @@ uint32_t GameHandler::getSkillCategory(uint32_t skillId) const {
|
|||
return (it != skillLineCategories_.end()) ? it->second : 0;
|
||||
}
|
||||
|
||||
bool GameHandler::isProfessionSpell(uint32_t spellId) const {
|
||||
auto slIt = spellToSkillLine_.find(spellId);
|
||||
if (slIt == spellToSkillLine_.end()) return false;
|
||||
auto catIt = skillLineCategories_.find(slIt->second);
|
||||
if (catIt == skillLineCategories_.end()) return false;
|
||||
// Category 11 = profession (Blacksmithing, etc.), 9 = secondary (Cooking, First Aid, Fishing)
|
||||
return catIt->second == 11 || catIt->second == 9;
|
||||
}
|
||||
|
||||
void GameHandler::loadSkillLineDbc() {
|
||||
if (skillLineDbcLoaded_) return;
|
||||
skillLineDbcLoaded_ = true;
|
||||
|
|
|
|||
|
|
@ -2091,18 +2091,20 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
std::string cmd = buf.substr(1, sp - 1);
|
||||
for (char& c : cmd) c = std::tolower(c);
|
||||
int detected = -1;
|
||||
bool isReply = false;
|
||||
if (cmd == "s" || cmd == "say") detected = 0;
|
||||
else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1;
|
||||
else if (cmd == "p" || cmd == "party") detected = 2;
|
||||
else if (cmd == "g" || cmd == "guild") detected = 3;
|
||||
else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4;
|
||||
else if (cmd == "r" || cmd == "reply") { detected = 4; isReply = true; }
|
||||
else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5;
|
||||
else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6;
|
||||
else if (cmd == "bg" || cmd == "battleground") detected = 7;
|
||||
else if (cmd == "rw" || cmd == "raidwarning") detected = 8;
|
||||
else if (cmd == "i" || cmd == "instance") detected = 9;
|
||||
else if (cmd.size() == 1 && cmd[0] >= '1' && cmd[0] <= '9') detected = 10; // /1, /2 etc.
|
||||
if (detected >= 0 && (selectedChatType != detected || detected == 10)) {
|
||||
if (detected >= 0 && (selectedChatType != detected || detected == 10 || isReply)) {
|
||||
// For channel shortcuts, also update selectedChannelIdx
|
||||
if (detected == 10) {
|
||||
int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc.
|
||||
|
|
@ -2114,8 +2116,16 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
selectedChatType = detected;
|
||||
// Strip the prefix, keep only the message part
|
||||
std::string remaining = buf.substr(sp + 1);
|
||||
// For whisper, first word after /w is the target
|
||||
if (detected == 4) {
|
||||
// /r reply: pre-fill whisper target from last whisper sender
|
||||
if (detected == 4 && isReply) {
|
||||
std::string lastSender = gameHandler.getLastWhisperSender();
|
||||
if (!lastSender.empty()) {
|
||||
strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1);
|
||||
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
||||
}
|
||||
// remaining is the message — don't extract a target from it
|
||||
} else if (detected == 4) {
|
||||
// For whisper, first word after /w is the target
|
||||
size_t msgStart = remaining.find(' ');
|
||||
if (msgStart != std::string::npos) {
|
||||
std::string wTarget = remaining.substr(0, msgStart);
|
||||
|
|
@ -2576,6 +2586,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
uint64_t closestHostileUnitGuid = 0;
|
||||
float closestQuestGoT = 1e30f;
|
||||
uint64_t closestQuestGoGuid = 0;
|
||||
float closestGoT = 1e30f;
|
||||
uint64_t closestGoGuid = 0;
|
||||
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
|
|
@ -2598,16 +2610,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
heightOffset = 0.3f;
|
||||
}
|
||||
} else if (t == game::ObjectType::GAMEOBJECT) {
|
||||
// For GOs with no renderer instance yet, use a tight fallback
|
||||
// sphere so invisible/unloaded doodads aren't accidentally clicked.
|
||||
hitRadius = 1.2f;
|
||||
heightOffset = 1.0f;
|
||||
// Quest objective GOs should be easier to click.
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (questObjectiveGoEntries.count(go->getEntry())) {
|
||||
hitRadius = 2.2f;
|
||||
heightOffset = 1.2f;
|
||||
}
|
||||
hitRadius = 2.5f;
|
||||
heightOffset = 1.2f;
|
||||
}
|
||||
hitCenter = core::coords::canonicalToRender(
|
||||
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
|
|
@ -2626,12 +2630,18 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
closestHostileUnitGuid = guid;
|
||||
}
|
||||
}
|
||||
if (t == game::ObjectType::GAMEOBJECT && !questObjectiveGoEntries.empty()) {
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (questObjectiveGoEntries.count(go->getEntry())) {
|
||||
if (hitT < closestQuestGoT) {
|
||||
closestQuestGoT = hitT;
|
||||
closestQuestGoGuid = guid;
|
||||
if (t == game::ObjectType::GAMEOBJECT) {
|
||||
if (hitT < closestGoT) {
|
||||
closestGoT = hitT;
|
||||
closestGoGuid = guid;
|
||||
}
|
||||
if (!questObjectiveGoEntries.empty()) {
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (questObjectiveGoEntries.count(go->getEntry())) {
|
||||
if (hitT < closestQuestGoT) {
|
||||
closestQuestGoT = hitT;
|
||||
closestQuestGoGuid = guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2643,12 +2653,23 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// Prefer quest objective GOs over hostile monsters when both are hittable.
|
||||
// Priority: quest GO > closer of (GO, hostile unit) > closest anything.
|
||||
if (closestQuestGoGuid != 0) {
|
||||
closestGuid = closestQuestGoGuid;
|
||||
closestType = game::ObjectType::GAMEOBJECT;
|
||||
} else if (closestGoGuid != 0 && closestHostileUnitGuid != 0) {
|
||||
// Both a GO and hostile unit were hit — prefer whichever is closer.
|
||||
if (closestGoT <= closestHostileUnitT) {
|
||||
closestGuid = closestGoGuid;
|
||||
closestType = game::ObjectType::GAMEOBJECT;
|
||||
} else {
|
||||
closestGuid = closestHostileUnitGuid;
|
||||
closestType = game::ObjectType::UNIT;
|
||||
}
|
||||
} else if (closestGoGuid != 0) {
|
||||
closestGuid = closestGoGuid;
|
||||
closestType = game::ObjectType::GAMEOBJECT;
|
||||
} else if (closestHostileUnitGuid != 0) {
|
||||
// Prefer hostile monsters over nearby gameobjects/others when right-click picking.
|
||||
closestGuid = closestHostileUnitGuid;
|
||||
closestType = game::ObjectType::UNIT;
|
||||
}
|
||||
|
|
@ -5951,6 +5972,28 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
message = "";
|
||||
isChannelCommand = true;
|
||||
}
|
||||
} else if (cmdLower == "r" || cmdLower == "reply") {
|
||||
switchChatType = 4;
|
||||
std::string lastSender = gameHandler.getLastWhisperSender();
|
||||
if (lastSender.empty()) {
|
||||
game::MessageChatData sysMsg;
|
||||
sysMsg.type = game::ChatType::SYSTEM;
|
||||
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
sysMsg.message = "No one has whispered you yet.";
|
||||
gameHandler.addLocalChatMessage(sysMsg);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
target = lastSender;
|
||||
strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1);
|
||||
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
||||
if (spacePos != std::string::npos) {
|
||||
message = command.substr(spacePos + 1);
|
||||
type = game::ChatType::WHISPER;
|
||||
} else {
|
||||
message = "";
|
||||
}
|
||||
isChannelCommand = true;
|
||||
}
|
||||
|
||||
// Check for emote commands
|
||||
|
|
@ -13624,6 +13667,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
const auto& trainer = gameHandler.getTrainerSpells();
|
||||
const bool isProfessionTrainer = (trainer.trainerType == 2);
|
||||
|
||||
// NPC name
|
||||
auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid);
|
||||
|
|
@ -13844,11 +13888,21 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
logCount++;
|
||||
}
|
||||
|
||||
if (!canTrain) ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton("Train")) {
|
||||
gameHandler.trainSpell(spell->spellId);
|
||||
if (isProfessionTrainer && alreadyKnown) {
|
||||
// Profession trainer: known recipes show "Create" button to craft
|
||||
bool isCasting = gameHandler.isCasting();
|
||||
if (isCasting) ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton("Create")) {
|
||||
gameHandler.castSpell(spell->spellId, 0);
|
||||
}
|
||||
if (isCasting) ImGui::EndDisabled();
|
||||
} else {
|
||||
if (!canTrain) ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton("Train")) {
|
||||
gameHandler.trainSpell(spell->spellId);
|
||||
}
|
||||
if (!canTrain) ImGui::EndDisabled();
|
||||
}
|
||||
if (!canTrain) ImGui::EndDisabled();
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
|
@ -13952,6 +14006,79 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
if (!hasTrainable) ImGui::EndDisabled();
|
||||
|
||||
// Profession trainer: craft quantity controls
|
||||
if (isProfessionTrainer) {
|
||||
ImGui::Separator();
|
||||
static int craftQuantity = 1;
|
||||
static uint32_t selectedCraftSpell = 0;
|
||||
|
||||
// Show craft queue status if active
|
||||
int queueRemaining = gameHandler.getCraftQueueRemaining();
|
||||
if (queueRemaining > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f),
|
||||
"Crafting... %d remaining", queueRemaining);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Stop")) {
|
||||
gameHandler.cancelCraftQueue();
|
||||
gameHandler.cancelCast();
|
||||
}
|
||||
} else {
|
||||
// Spell selector + quantity input
|
||||
// Build list of known (craftable) spells
|
||||
std::vector<const game::TrainerSpell*> craftable;
|
||||
for (const auto& spell : trainer.spells) {
|
||||
if (isKnown(spell.spellId)) {
|
||||
craftable.push_back(&spell);
|
||||
}
|
||||
}
|
||||
if (!craftable.empty()) {
|
||||
// Combo box for recipe selection
|
||||
const char* previewName = "Select recipe...";
|
||||
for (const auto* sp : craftable) {
|
||||
if (sp->spellId == selectedCraftSpell) {
|
||||
const std::string& n = gameHandler.getSpellName(sp->spellId);
|
||||
if (!n.empty()) previewName = n.c_str();
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.55f);
|
||||
if (ImGui::BeginCombo("##CraftSelect", previewName)) {
|
||||
for (const auto* sp : craftable) {
|
||||
const std::string& n = gameHandler.getSpellName(sp->spellId);
|
||||
const std::string& r = gameHandler.getSpellRank(sp->spellId);
|
||||
char label[128];
|
||||
if (!r.empty())
|
||||
snprintf(label, sizeof(label), "%s (%s)##%u",
|
||||
n.empty() ? "???" : n.c_str(), r.c_str(), sp->spellId);
|
||||
else
|
||||
snprintf(label, sizeof(label), "%s##%u",
|
||||
n.empty() ? "???" : n.c_str(), sp->spellId);
|
||||
if (ImGui::Selectable(label, sp->spellId == selectedCraftSpell)) {
|
||||
selectedCraftSpell = sp->spellId;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(50.0f);
|
||||
ImGui::InputInt("##CraftQty", &craftQuantity, 0, 0);
|
||||
if (craftQuantity < 1) craftQuantity = 1;
|
||||
if (craftQuantity > 99) craftQuantity = 99;
|
||||
ImGui::SameLine();
|
||||
bool canCraft = selectedCraftSpell != 0 && !gameHandler.isCasting();
|
||||
if (!canCraft) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Create")) {
|
||||
if (craftQuantity == 1) {
|
||||
gameHandler.castSpell(selectedCraftSpell, 0);
|
||||
} else {
|
||||
gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity);
|
||||
}
|
||||
}
|
||||
if (!canCraft) ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue