#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();
}