#include "mod_bounty_board.h" #include "Chat.h" #include "Config.h" #include "Creature.h" #include "DatabaseEnv.h" #include "Log.h" #include "Map.h" #include "MapMgr.h" #include "Player.h" #include "ScriptMgr.h" #include using namespace Acore::ChatCommands; // v0.2: adds kill_target spawning on top of v0.1's kill-count vertical slice. Progress tracking // (BountyBoard_Player::OnPlayerCreatureKill) was already generic across types -- kill_target is // just count_required == 1 -- so the only new code here is actually summoning the creature. // Rewards auto-grant on completion; the board is browse-only, no turn-in step -- see PLAN.md. // // Spawned kill_target creatures aren't despawned or re-summoned after death yet -- there's no // rotation timer to drive that (PLAN.md step 2), and once a summoned target is killed it stays // gone until the next server restart. Fine for proving the summon mechanism works; revisit // respawn-after-death behavior (so more than one player/week can complete the same slot) when the // rotation timer lands. namespace { std::vector ActiveBounties; void LoadActiveBounties() { ActiveBounties.clear(); QueryResult result = WorldDatabase.Query( "SELECT r.id, t.type, t.name, t.creature_entry, t.count_required, t.item_reward, " "t.item_reward_count, t.money_reward, t.is_champion, t.position_map, t.position_x, " "t.position_y, t.position_z, t.orientation " "FROM mod_bounty_board_rotation r " "JOIN mod_bounty_board_template t ON t.id = r.template_id " "ORDER BY r.id"); if (!result) { LOG_WARN("server.loading", "mod-bounty-board: mod_bounty_board_rotation is empty -- " "no active bounties loaded."); return; } do { Field* row = result->Fetch(); ActiveBounties.push_back(ActiveBounty { row[0].Get(), row[1].Get(), row[2].Get(), row[3].Get(), row[4].Get(), row[5].Get(), row[6].Get(), row[7].Get(), row[8].Get() != 0, row[9].Get(), row[10].Get(), row[11].Get(), row[12].Get(), row[13].Get() }); } while (result->NextRow()); LOG_INFO("server.loading", "mod-bounty-board: loaded {} active bounty rotation slot(s).", ActiveBounties.size()); } // Summons the world-placed creature for every active kill_target bounty. Called once at // startup, after maps are initialized -- see the comment on BountyBoard_World::OnStartup for // why this can't happen from OnLoadCustomDatabaseTable instead. void SpawnBountyTargets() { uint32 spawned = 0; for (ActiveBounty const& bounty : ActiveBounties) { if (bounty.type != "kill_target") continue; Map* map = sMapMgr->CreateBaseMap(bounty.positionMap); Position pos(bounty.positionX, bounty.positionY, bounty.positionZ, bounty.orientation); if (map->SummonCreature(bounty.creatureEntry, pos)) ++spawned; else LOG_ERROR("server.loading", "mod-bounty-board: failed to summon kill_target " "bounty '{}' (creature {}) at map {}.", bounty.name, bounty.creatureEntry, bounty.positionMap); } if (spawned) LOG_INFO("server.loading", "mod-bounty-board: summoned {} kill_target bounty " "target(s).", spawned); } // Grants the reward and marks the row turned-in in the same statement as the completing kill // -- see the file-level comment on why there's no separate turn-in step yet. void CompleteBounty(Player* player, ActiveBounty const& bounty) { if (bounty.moneyReward) player->ModifyMoney(static_cast(bounty.moneyReward)); if (bounty.itemReward && bounty.itemRewardCount) player->AddItem(bounty.itemReward, bounty.itemRewardCount); ChatHandler(player->GetSession()).PSendSysMessage("Bounty complete: {}! Reward delivered.", bounty.name); } } std::vector const& GetActiveBounties() { return ActiveBounties; } class BountyBoardCommandScript : public CommandScript { public: BountyBoardCommandScript() : CommandScript("BountyBoardCommandScript") { } ChatCommandTable GetCommands() const override { static ChatCommandTable commandTable = { { "bounty", HandleBountyCommand, SEC_PLAYER, Console::No }, }; return commandTable; } static bool HandleBountyCommand(ChatHandler* handler) { if (!sConfigMgr->GetOption("BountyBoard.Enable", true)) { handler->PSendSysMessage("The Bounty Board module is currently disabled."); return true; } if (GetActiveBounties().empty()) { handler->PSendSysMessage("A tattered notice board creaks in the wind: no contracts posted this week."); return true; } Player* player = handler->GetPlayer(); for (ActiveBounty const& bounty : GetActiveBounties()) { QueryResult result = CharacterDatabase.Query( "SELECT kill_count, completed FROM mod_bounty_board_progress WHERE guid = {} AND rotation_id = {}", player->GetGUID().GetCounter(), bounty.rotationId); uint32 killCount = 0; bool completed = false; if (result) { Field* row = result->Fetch(); killCount = row[0].Get(); completed = row[1].Get() != 0; } handler->PSendSysMessage("{}: {} ({}/{})", bounty.name, completed ? "completed" : "in progress", killCount, bounty.countRequired); } return true; } }; class BountyBoard_Player : public PlayerScript { public: BountyBoard_Player() : PlayerScript("BountyBoard_Player") { } void OnPlayerCreatureKill(Player* killer, Creature* killed) override { if (!sConfigMgr->GetOption("BountyBoard.Enable", true)) return; for (ActiveBounty const& bounty : GetActiveBounties()) { if (bounty.creatureEntry != killed->GetEntry()) continue; QueryResult result = CharacterDatabase.Query( "SELECT kill_count, completed FROM mod_bounty_board_progress WHERE guid = {} AND rotation_id = {}", killer->GetGUID().GetCounter(), bounty.rotationId); uint32 killCount = 0; bool alreadyCompleted = false; if (result) { Field* row = result->Fetch(); killCount = row[0].Get(); alreadyCompleted = row[1].Get() != 0; } if (alreadyCompleted) continue; // Reward already granted -- don't keep counting past the requirement. killCount = std::min(killCount + 1, bounty.countRequired); bool completedNow = killCount >= bounty.countRequired; CharacterDatabase.Execute( "INSERT INTO mod_bounty_board_progress (guid, rotation_id, kill_count, completed, turned_in) " "VALUES ({}, {}, {}, {}, {}) " "ON DUPLICATE KEY UPDATE kill_count = {}, completed = {}, turned_in = {}", killer->GetGUID().GetCounter(), bounty.rotationId, killCount, completedNow ? 1 : 0, completedNow ? 1 : 0, killCount, completedNow ? 1 : 0, completedNow ? 1 : 0); if (completedNow) CompleteBounty(killer, bounty); else ChatHandler(killer->GetSession()).PSendSysMessage("{}: {}/{}", bounty.name, killCount, bounty.countRequired); return; // A kill can only match one active bounty slot in this vertical slice. } } }; class BountyBoard_World : public WorldScript { public: BountyBoard_World() : WorldScript("BountyBoard_World") { } void OnStartup() override { bool enabled = sConfigMgr->GetOption("BountyBoard.Enable", true); LOG_INFO("server.loading", "mod-bounty-board: loaded (v0.2, enabled={}).", enabled); // Maps aren't initialized yet during OnLoadCustomDatabaseTable (sMapMgr->Initialize() // runs later in World::SetInitialWorldSettings, well before OnStartup fires from // Main.cpp) -- this is the earliest point summoning kill_target creatures is safe. if (enabled) SpawnBountyTargets(); } void OnLoadCustomDatabaseTable() override { LoadActiveBounties(); } }; void AddSC_mod_bounty_board() { new BountyBoardCommandScript(); new BountyBoard_Player(); new BountyBoard_World(); }