Clean README mentions and finalize current gameplay/UI fixes

This commit is contained in:
Kelsi 2026-02-19 03:31:49 -08:00
parent 16f8b0177e
commit 871da33942
6 changed files with 121 additions and 57 deletions

View file

@ -5084,6 +5084,16 @@ void Application::updateQuestMarkers() {
// Get NPC entity position
auto entity = gameHandler->getEntityManager().getEntity(guid);
if (!entity) continue;
if (entity->getType() == game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<game::Unit>(entity);
std::string name = unit->getName();
std::transform(name.begin(), name.end(), name.begin(),
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
if (name.find("spirit healer") != std::string::npos ||
name.find("spirit guide") != std::string::npos) {
continue; // Spirit healers/guides use their own white visual cue.
}
}
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
glm::vec3 renderPos = coords::canonicalToRender(canonical);

View file

@ -567,9 +567,22 @@ void GameHandler::update(float deltaTime) {
}
// Update cast timer (Phase 3)
if (pendingGameObjectInteractGuid_ != 0 &&
(autoAttacking || !hostileAttackers_.empty())) {
pendingGameObjectInteractGuid_ = 0;
casting = false;
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
addSystemChatMessage("Interrupted.");
}
if (casting && castTimeRemaining > 0.0f) {
castTimeRemaining -= deltaTime;
if (castTimeRemaining <= 0.0f) {
if (pendingGameObjectInteractGuid_ != 0) {
uint64_t interactGuid = pendingGameObjectInteractGuid_;
pendingGameObjectInteractGuid_ = 0;
performGameObjectInteractionNow(interactGuid);
}
casting = false;
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
@ -2636,6 +2649,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
autoAttackTarget = 0;
casting = false;
currentCastSpellId = 0;
pendingGameObjectInteractGuid_ = 0;
castTimeRemaining = 0.0f;
castTimeTotal = 0.0f;
playerDead_ = false;
@ -6029,13 +6043,16 @@ void GameHandler::stopCasting() {
return; // Not casting anything
}
// Send cancel cast packet with current spell ID
auto packet = CancelCastPacket::build(currentCastSpellId);
socket->send(packet);
// Send cancel cast packet only for real spell casts.
if (pendingGameObjectInteractGuid_ == 0 && currentCastSpellId != 0) {
auto packet = CancelCastPacket::build(currentCastSpellId);
socket->send(packet);
}
// Reset casting state
casting = false;
currentCastSpellId = 0;
pendingGameObjectInteractGuid_ = 0;
castTimeRemaining = 0.0f;
castTimeTotal = 0.0f;
@ -7872,10 +7889,14 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
void GameHandler::cancelCast() {
if (!casting) return;
if (state == WorldState::IN_WORLD && socket) {
// GameObject interaction cast is client-side timing only.
if (pendingGameObjectInteractGuid_ == 0 &&
state == WorldState::IN_WORLD && socket &&
currentCastSpellId != 0) {
auto packet = CancelCastPacket::build(currentCastSpellId);
socket->send(packet);
}
pendingGameObjectInteractGuid_ = 0;
casting = false;
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
@ -8545,6 +8566,21 @@ void GameHandler::interactWithNpc(uint64_t guid) {
}
void GameHandler::interactWithGameObject(uint64_t guid) {
if (guid == 0) return;
if (state != WorldState::IN_WORLD || !socket) return;
if (casting && currentCastSpellId != 0) return; // don't overlap spell cast bar
if (autoAttacking) {
stopAutoAttack();
}
pendingGameObjectInteractGuid_ = guid;
casting = true;
currentCastSpellId = 0;
castTimeTotal = 1.5f;
castTimeRemaining = castTimeTotal;
}
void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
if (guid == 0) return;
if (state != WorldState::IN_WORLD || !socket) return;
bool turtleMode = isActiveExpansion("turtle");
@ -8564,10 +8600,6 @@ void GameHandler::interactWithGameObject(uint64_t guid) {
if (autoAttacking) {
stopAutoAttack();
}
if (targetGuid != guid) {
setTarget(guid);
}
auto entity = entityManager.getEntity(guid);
auto packet = GameObjectUsePacket::build(guid);
@ -9258,6 +9290,16 @@ void GameHandler::handleGossipMessage(network::Packet& packet) {
bool hasAvailableQuest = false;
bool hasRewardQuest = false;
bool hasIncompleteQuest = false;
auto questIconIsCompletable = [](uint32_t icon) {
return icon == 5 || icon == 6 || icon == 10;
};
auto questIconIsIncomplete = [](uint32_t icon) {
return icon == 3 || icon == 4;
};
auto questIconIsAvailable = [](uint32_t icon) {
return icon == 2 || icon == 7 || icon == 8;
};
for (const auto& questItem : currentGossip.quests) {
// WotLK gossip questIcon is an integer enum, NOT a bitmask:
// 2 = yellow ! (available, not yet accepted)
@ -9265,9 +9307,9 @@ void GameHandler::handleGossipMessage(network::Packet& packet) {
// 5 = gold ? (complete, ready to turn in)
// Bit-masking these values is wrong: 4 & 0x04 = true, treating incomplete
// quests as completable and causing the server to reject the turn-in request.
bool isCompletable = (questItem.questIcon == 5); // Gold ? = can turn in
bool isIncomplete = (questItem.questIcon == 4); // Gray ? = in progress
bool isAvailable = (questItem.questIcon == 2); // Yellow ! = available
bool isCompletable = questIconIsCompletable(questItem.questIcon);
bool isIncomplete = questIconIsIncomplete(questItem.questIcon);
bool isAvailable = questIconIsAvailable(questItem.questIcon);
hasAvailableQuest |= isAvailable;
hasRewardQuest |= isCompletable;
@ -9290,7 +9332,9 @@ void GameHandler::handleGossipMessage(network::Packet& packet) {
if (hasRewardQuest) derivedStatus = QuestGiverStatus::REWARD;
else if (hasAvailableQuest) derivedStatus = QuestGiverStatus::AVAILABLE;
else if (hasIncompleteQuest) derivedStatus = QuestGiverStatus::INCOMPLETE;
npcQuestStatus_[currentGossip.npcGuid] = derivedStatus;
if (derivedStatus != QuestGiverStatus::NONE) {
npcQuestStatus_[currentGossip.npcGuid] = derivedStatus;
}
}
// Play NPC greeting voice
@ -9371,10 +9415,20 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) {
bool hasAvailableQuest = false;
bool hasRewardQuest = false;
bool hasIncompleteQuest = false;
auto questIconIsCompletable = [](uint32_t icon) {
return icon == 5 || icon == 6 || icon == 10;
};
auto questIconIsIncomplete = [](uint32_t icon) {
return icon == 3 || icon == 4;
};
auto questIconIsAvailable = [](uint32_t icon) {
return icon == 2 || icon == 7 || icon == 8;
};
for (const auto& questItem : currentGossip.quests) {
bool isCompletable = (questItem.questIcon == 5 || questItem.questIcon == 10);
bool isIncomplete = (questItem.questIcon == 3 || questItem.questIcon == 4);
bool isAvailable = (questItem.questIcon == 2 || questItem.questIcon == 7 || questItem.questIcon == 8);
bool isCompletable = questIconIsCompletable(questItem.questIcon);
bool isIncomplete = questIconIsIncomplete(questItem.questIcon);
bool isAvailable = questIconIsAvailable(questItem.questIcon);
hasAvailableQuest |= isAvailable;
hasRewardQuest |= isCompletable;
hasIncompleteQuest |= isIncomplete;
@ -9384,7 +9438,9 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) {
if (hasRewardQuest) derivedStatus = QuestGiverStatus::REWARD;
else if (hasAvailableQuest) derivedStatus = QuestGiverStatus::AVAILABLE;
else if (hasIncompleteQuest) derivedStatus = QuestGiverStatus::INCOMPLETE;
npcQuestStatus_[currentGossip.npcGuid] = derivedStatus;
if (derivedStatus != QuestGiverStatus::NONE) {
npcQuestStatus_[currentGossip.npcGuid] = derivedStatus;
}
}
LOG_INFO("Questgiver quest list: npc=0x", std::hex, currentGossip.npcGuid, std::dec,
@ -9990,6 +10046,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
stopAutoAttack();
casting = false;
currentCastSpellId = 0;
pendingGameObjectInteractGuid_ = 0;
castTimeRemaining = 0.0f;
// Send MSG_MOVE_WORLDPORT_ACK to tell the server we're ready

View file

@ -1649,7 +1649,19 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
characterShader->setUniform("uUnlit", unlit ? 1 : 0);
float emissiveBoost = 1.0f;
glm::vec3 emissiveTint(1.0f, 1.0f, 1.0f);
if (unlit) {
// Keep custom warm/flicker treatment narrowly scoped to kobold candle flames.
bool koboldCandleFlame = false;
if (colorKeyBlack) {
std::string modelKey = gpuModel.data.name;
std::transform(modelKey.begin(), modelKey.end(), modelKey.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
koboldCandleFlame =
(modelKey.find("kobold") != std::string::npos) &&
((modelKey.find("candle") != std::string::npos) ||
(modelKey.find("torch") != std::string::npos) ||
(modelKey.find("mine") != std::string::npos));
}
if (unlit && koboldCandleFlame) {
using clock = std::chrono::steady_clock;
float t = std::chrono::duration<float>(clock::now().time_since_epoch()).count();
float phase = static_cast<float>(batch.submeshId) * 0.31f;

View file

@ -4826,48 +4826,27 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
}
}
// Render item with icon
// Render item with icon + visible selectable label
ImGui::PushID(static_cast<int>(i));
if (ImGui::Selectable("##reward", selected, 0, ImVec2(0, 40))) {
std::string label;
if (info && info->valid && !info->name.empty()) {
label = info->name;
} else {
label = "Item " + std::to_string(item.itemId);
}
if (item.count > 1) {
label += " x" + std::to_string(item.count);
}
if (ImGui::Selectable(label.c_str(), selected, 0, ImVec2(0, 24))) {
selectedChoice = static_cast<int>(i);
}
// Draw icon and text over the selectable
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetItemRectSize().x + 4);
if (ImGui::IsItemHovered() && iconTex) {
ImGui::SetTooltip("Reward option");
}
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(36, 36));
ImGui::SameLine();
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
}
ImGui::BeginGroup();
if (info && info->valid) {
ImGui::TextColored(qualityColor, "%s", info->name.c_str());
if (item.count > 1) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "x%u", item.count);
}
// Show stats
if (info->armor > 0 || info->stamina > 0 || info->strength > 0 ||
info->agility > 0 || info->intellect > 0 || info->spirit > 0) {
std::string stats;
if (info->armor > 0) stats += std::to_string(info->armor) + " Armor ";
if (info->stamina > 0) stats += "+" + std::to_string(info->stamina) + " Sta ";
if (info->strength > 0) stats += "+" + std::to_string(info->strength) + " Str ";
if (info->agility > 0) stats += "+" + std::to_string(info->agility) + " Agi ";
if (info->intellect > 0) stats += "+" + std::to_string(info->intellect) + " Int ";
if (info->spirit > 0) stats += "+" + std::to_string(info->spirit) + " Spi ";
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "%s", stats.c_str());
}
} else {
ImGui::TextColored(qualityColor, "Item %u", item.itemId);
if (item.count > 0) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "x%u", item.count);
}
}
ImGui::EndGroup();
ImGui::PopID();
}
}