From 7b03d5363b0c842ac0929660697ef9e57ce4041d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Mar 2026 10:12:49 -0700 Subject: [PATCH] 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 --- include/game/game_handler.hpp | 10 ++ src/game/game_handler.cpp | 150 +++++++++++++++++++++------- src/ui/game_screen.cpp | 177 +++++++++++++++++++++++++++++----- 3 files changed, 278 insertions(+), 59 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 11223bbe..e8762167 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -786,6 +786,12 @@ public: float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; } float getCastTimeRemaining() const { return castTimeRemaining; } + // Repeat-craft queue + void startCraftQueue(uint32_t spellId, int count); + void cancelCraftQueue(); + int getCraftQueueRemaining() const { return craftQueueRemaining_; } + uint32_t getCraftQueueSpellId() const { return craftQueueSpellId_; } + // Unit cast state (tracked per GUID for target frame + boss frames) struct UnitCastState { bool casting = false; @@ -970,6 +976,7 @@ public: const std::map& getPlayerSkills() const { return playerSkills_; } const std::string& getSkillName(uint32_t skillId) const; uint32_t getSkillCategory(uint32_t skillId) const; + bool isProfessionSpell(uint32_t spellId) const; // World entry callback (online mode - triggered when entering world) // Parameters: mapId, x, y, z (canonical WoW coords), isInitialEntry=true on first login or reconnect @@ -2669,6 +2676,9 @@ private: bool castIsChannel = false; uint32_t currentCastSpellId = 0; float castTimeRemaining = 0.0f; + // Repeat-craft queue: re-cast the same profession spell N more times after current cast finishes + uint32_t craftQueueSpellId_ = 0; + int craftQueueRemaining_ = 0; // Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START) std::unordered_map unitCastStates_; uint64_t pendingGameObjectInteractGuid_ = 0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 9790d73e..35e34512 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 1f62ac01..0d6ce5c7 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -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(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(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(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 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();