Fix NPC spawning at initial player position

NPCs were not spawning when the player first entered the world because
spawnNpcs() was defined but never called. Added call to spawnNpcs() in
Application::update() when in IN_GAME state.

The function has a guard (npcsSpawned flag) so it only runs once. NPCs now
appear immediately at spawn instead of requiring the player to walk away first.

Added logging to help debug spawn preconditions.
This commit is contained in:
Kelsi 2026-02-09 21:59:00 -08:00
parent aea8e1ba03
commit 4e3750c00e
3 changed files with 51 additions and 4 deletions

View file

@ -392,6 +392,9 @@ void Application::update(float deltaTime) {
if (world) { if (world) {
world->update(deltaTime); world->update(deltaTime);
} }
// Spawn NPCs once when entering world
spawnNpcs();
// Process deferred online creature spawns (throttled) // Process deferred online creature spawns (throttled)
processCreatureSpawnQueue(); processCreatureSpawnQueue();
processGameObjectSpawnQueue(); processGameObjectSpawnQueue();
@ -1404,16 +1407,29 @@ void Application::loadEquippedWeapons() {
void Application::spawnNpcs() { void Application::spawnNpcs() {
if (npcsSpawned) return; if (npcsSpawned) return;
if (!assetManager || !assetManager->isInitialized()) return; LOG_INFO("spawnNpcs: checking preconditions...");
if (!renderer || !renderer->getCharacterRenderer() || !renderer->getCamera()) return; if (!assetManager || !assetManager->isInitialized()) {
if (!gameHandler) return; LOG_INFO("spawnNpcs: assetManager not ready");
return;
}
if (!renderer || !renderer->getCharacterRenderer() || !renderer->getCamera()) {
LOG_INFO("spawnNpcs: renderer not ready");
return;
}
if (!gameHandler) {
LOG_INFO("spawnNpcs: gameHandler not ready");
return;
}
LOG_INFO("spawnNpcs: spawning NPCs...");
if (npcManager) { if (npcManager) {
npcManager->clear(renderer->getCharacterRenderer(), &gameHandler->getEntityManager()); npcManager->clear(renderer->getCharacterRenderer(), &gameHandler->getEntityManager());
} }
npcManager = std::make_unique<game::NpcManager>(); npcManager = std::make_unique<game::NpcManager>();
glm::vec3 playerSpawnGL = renderer->getCharacterPosition(); glm::vec3 playerSpawnGL = renderer->getCharacterPosition();
glm::vec3 playerCanonical = core::coords::renderToCanonical(playerSpawnGL); glm::vec3 playerCanonical = core::coords::renderToCanonical(playerSpawnGL);
LOG_INFO("spawnNpcs: player position GL=(", playerSpawnGL.x, ",", playerSpawnGL.y, ",", playerSpawnGL.z,
") canonical=(", playerCanonical.x, ",", playerCanonical.y, ",", playerCanonical.z, ")");
std::string mapName = "Azeroth"; std::string mapName = "Azeroth";
if (auto* minimap = renderer->getMinimap()) { if (auto* minimap = renderer->getMinimap()) {
mapName = minimap->getMapName(); mapName = minimap->getMapName();

View file

@ -4627,6 +4627,17 @@ void GameHandler::handleTrainerList(network::Packet& packet) {
trainerWindowOpen_ = true; trainerWindowOpen_ = true;
gossipWindowOpen = false; gossipWindowOpen = false;
// Debug: log known spells
LOG_INFO("Known spells count: ", knownSpells.size());
if (knownSpells.size() <= 20) {
std::string spellList;
for (uint32_t id : knownSpells) {
if (!spellList.empty()) spellList += ", ";
spellList += std::to_string(id);
}
LOG_INFO("Known spells: ", spellList);
}
// Ensure caches are populated // Ensure caches are populated
loadSpellNameCache(); loadSpellNameCache();
loadSkillLineDbc(); loadSkillLineDbc();
@ -4635,12 +4646,19 @@ void GameHandler::handleTrainerList(network::Packet& packet) {
} }
void GameHandler::trainSpell(uint32_t spellId) { void GameHandler::trainSpell(uint32_t spellId) {
if (state != WorldState::IN_WORLD || !socket) return; LOG_INFO("trainSpell called: spellId=", spellId, " state=", (int)state, " socket=", (socket ? "yes" : "no"));
if (state != WorldState::IN_WORLD || !socket) {
LOG_WARNING("trainSpell: Not in world or no socket connection");
return;
}
LOG_INFO("Sending CMSG_TRAINER_BUY_SPELL: guid=", currentTrainerList_.trainerGuid,
" trainerId=", currentTrainerList_.trainerType, " spellId=", spellId);
auto packet = TrainerBuySpellPacket::build( auto packet = TrainerBuySpellPacket::build(
currentTrainerList_.trainerGuid, currentTrainerList_.trainerGuid,
currentTrainerList_.trainerType, currentTrainerList_.trainerType,
spellId); spellId);
socket->send(packet); socket->send(packet);
LOG_INFO("CMSG_TRAINER_BUY_SPELL sent");
} }
void GameHandler::closeTrainer() { void GameHandler::closeTrainer() {

View file

@ -3689,6 +3689,19 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
bool canTrain = !alreadyKnown && spell->state == 1 bool canTrain = !alreadyKnown && spell->state == 1
&& prereqsMet && levelMet && prereqsMet && levelMet
&& (money >= spell->spellCost); && (money >= spell->spellCost);
// Debug logging for first spell to see why buttons are disabled
static bool logged = false;
if (!logged) {
LOG_INFO("Trainer button debug: spellId=", spell->spellId,
" alreadyKnown=", alreadyKnown, " state=", (int)spell->state,
" prereqsMet=", prereqsMet, " levelMet=", levelMet,
" canAfford=", (money >= spell->spellCost),
" money=", money, " cost=", spell->spellCost,
" canTrain=", canTrain);
logged = true;
}
if (!canTrain) ImGui::BeginDisabled(); if (!canTrain) ImGui::BeginDisabled();
if (ImGui::SmallButton("Train")) { if (ImGui::SmallButton("Train")) {
gameHandler.trainSpell(spell->spellId); gameHandler.trainSpell(spell->spellId);