# mod-bounty-board — Plan
Fun/QoL project: a rotating set of weekly bounties, in the spirit of FFXIV's Hunt Board. Pick up
a bounty, kill the target(s), get rewarded automatically. Additive, not a replacement for quests
or the existing rare/elite ecosystem.
Full system design was hashed out 2026-08-15 specifically to minimize decisions left for
implementation sessions — see "Locked decisions" and "System architecture" below. Only genuinely
cheap-to-change flavor details (exact NPC name/model, final non-clipping placement offsets) are
still open.
## Design pillars
- **Three bounty types, two code paths.** Kill-count and elite/champion bounties share almost
nothing mechanically, so treat them as two systems wearing one UI:
1. **Kill-count** — kill N (5-10) of an existing mob species in a zone. No new spawns; just tag
an existing `creature_entry` as this week's target and count kills against it.
2. **Elite target** — a named single creature that only exists in the world while its bounty is
active.
3. **Champion** (the "mid-core" tier) — mechanically identical to an elite target (spawns only
while active, single-kill completion), but backed by a `creature_template` with a bigger
health pool and a couple of extra abilities, tuned for a 5-player pull. **No custom boss AI,
no unique encounter mechanics** — that's explicitly out of scope for now (see "Deferred"
below). Elite and champion share all their plumbing; champion is just `kill_target` tagged
`is_champion`, with a beefier template.
- **Curated, not procedural.** The pool of possible bounties (`mod_bounty_board_template`) is
hand-written SQL, like `mod-waygate-network`'s destination list — the rotation picks *among*
curated entries, it doesn't generate new ones.
- **Kill-count bounties reuse mob species, not spawns.** No spawn/despawn needed for type 1 —
`BountyBoard_Player::OnPlayerCreatureKill` (already built, v0.1) just checks the killed
creature's entry against the active rotation and increments progress if it matches, independent
of quest log/kill credit. The same hook handles `kill_target`/champion kills too — no new
tracking code needed once spawning exists.
## Locked decisions (2026-08-15)
- **Reward auto-grants on the completing kill.** No separate turn-in step. The board is
browse-only — view active bounties and your progress, nothing to click to collect.
- **Fixed weekly slot mix**, not a pure weighted-random pool — every week has the same shape
(e.g. 3 `kill_count` + 1 elite + 1 champion; exact counts are a tunable constant, not an
architectural decision). Guarantees all three flavors show up every week.
- **Anti-repeat**: exclude only the immediately-previous week's picks from the next pick. Simplest
option; revisit only if repetition feels bad in practice.
- **No custom boss encounters, ever** — explicitly out of scope (see "Deferred").
## System architecture
### 1. Weekly rotation timing — reuse the engine's own mechanism
- `Acore::Time::GetNextTimeWithDayAndHour(dayOfWeek, hour)` (`src/common/Utilities/Timer.h:60`) is
the exact primitive core's own weekly quest/raid-lockout reset uses
(`World::InitWeeklyQuestResetTime`, `World.cpp:1686-1694`, via `GetNextTimeWithDayAndHour(4, 6)`
— Wednesday 06:00 server time). Reuse it with the same day/hour so the bounty rotation lines up
with the reset players already know. No new worldstate entry needed.
- Repurpose `mod_bounty_board_rotation.week_id` as the **epoch timestamp of the period a rotation
row belongs to** (not an ISO week number as originally sketched). The rotation table itself is
the source of truth for "what period are we in" via `MAX(week_id)` — no separate persisted
pointer required.
- Check every `WorldScript::OnUpdate(uint32 diff)` tick: compute the current period's start via
`GetNextTimeWithDayAndHour`, compare against `MAX(week_id)` in the rotation table. Mirrors
`World::Update`'s own reset-check block (`World.cpp:1162-1180`) — a plain integer comparison, no
throttling needed.
- On rollover: for each fixed slot (per type/tier), weighted-random pick a template not in the
previous period's rotation, insert new `mod_bounty_board_rotation` rows stamped with the new
period's timestamp, then hand off to spawn/despawn for whatever `kill_target`/champion slots
changed.
### 2. Elite/champion spawning — hand-rolled, not the pool system
Researched `PoolMgr` (`src/server/game/Pools/PoolMgr.{h,cpp}`) in depth: `pool_template`/
`pool_creature` implement N-of-M chance-weighted rotation among candidates sharing a slot, need
2-3 extra DB tables, and `Creature.cpp:2115-2117` auto-couples pooled creatures to their own DB
`spawntimesecs` respawn cycle — that fights a module wanting sole ownership of visibility.
**Verdict: skip it.**
Instead, reuse the exact mechanism `mod-waygate-network`'s portal NPC already uses in this
codebase — `WorldObject::SummonCreature`/`Map::SummonCreature`
(`src/server/game/Entities/Object/Object.h:639-640`, `src/server/game/Maps/Map.h:344`) — just with
`TEMPSUMMON_MANUAL_DESPAWN` instead of a timed despawn. This is the standard "exists until code
says otherwise" pattern used by dozens of boss scripts in this repo (e.g. `boss_kologarn.cpp`,
`boss_the_lich_king.cpp`).
- Template schema gains position columns (see schema below), populated only for `kill_target`
rows.
- Slot goes active: `map->SummonCreature(entry, pos, nullptr, 0 /*duration*/, nullptr
/*summoner*/)`. Track the result in an in-memory `std::unordered_map` — not persisted; rebuilt at `WorldScript::OnStartup` by re-running "ensure this
period's spawns exist" against whatever `mod_bounty_board_rotation` currently holds (handles
server restarts for free).
- Slot goes inactive: resolve the tracked guid via `ObjectAccessor::GetCreature` and call
`DespawnOrUnsummon()`.
- Champion abilities: new `creature_template` row(s) (this module's 931000-931099 range) cloned
from a similar-level base with boosted health/damage, abilities added via **SmartAI**
(`SMART_EVENT_UPDATE_IC` + `SMART_ACTION_CAST` on cooldown) per this project's stated preference
for SmartAI over hand-rolled `CreatureScript` AI (see `AGENTS.md`) — fall back to a thin
`EventMap`-based script only if SmartAI's vocabulary can't express something needed. Still
explicitly no unique mechanics/phases.
### 3. The board itself — an interactive gameobject, not an NPC
Queried the local world DB for an existing bulletin-board-style object: **`gameobject_template`
entry `2713`, "Wanted Board"** (type `2` = `GAMEOBJECT_TYPE_QUESTGIVER`, `displayId 202`) —
already used in-game for Redridge Mountains' wanted-poster content, so the model is proven to read
correctly as a bounty/wanted board. **Don't reuse entry `2713` directly** (it's tied to that
unrelated existing quest content) — clone it into a **new `gameobject_template` row** in this
module's reserved ID range with the same `displayId`.
- `GameObjectScript::OnGossipHello`/`OnGossipSelect`
(`src/server/game/Scripting/ScriptDefines/GameObjectScript.h:33,36`) build the menu fresh on
every click from a live query — active rotation joined with the clicking player's progress —
following the same query-loop-`AddGossipItemFor` shape as the quest-relation loop in
`npcs_special.cpp:130-167`. Since reward is auto-granted on the kill, this menu is purely
informational (per-bounty progress lines) plus a close option — no reward-granting
`OnGossipSelect` branch needed.
### 4. Explainer NPC
A new `creature_template` row (same reserved ID range), standing next to each board, reusing an
existing humanoid display model — no new art needed. Static `OnGossipHello` with a short lore/
explainer blurb; purely flavor, touches no DB tables. Exact name/model is a zero-architectural-
impact detail, left for implementation time.
### 5. Placement — one board+NPC pair per capital, verified real anchor points
Queried the local world DB directly for real nearby landmarks (existing Auctioneer NPC spawns, or
bank gameobjects where no auction house exists) rather than guessing coordinates:
| Capital | Map | Anchor | Coordinates (x, y, z) |
|---|---|---|---|
| Stormwind City | 0 | Auctioneer Fitch | -8821.53, 659.886, 97.4645 |
| Ironforge | 0 | Auctioneer Buckler | -4948.01, -901.528, 505.172 |
| Undercity | 0 | Auctioneer Epitwee | 1542.45, 255.202, -56.7948 |
| Darnassus | 1 | Auctioneer Tolon | 9872.6, 2341.73, 1321.67 |
| Orgrimmar | 1 | Auctioneer Thathung | 1592.8, -4397.05, 7.46388 |
| Thunder Bluff | 1 | Auctioneer Stampi | -1210.21, 94.8587, 134.535 |
| The Exodar | 530 | Bank (no AH — Draenei use Alliance capitals') | -3929.09, -11606.3, -138.606 |
| Silvermoon City | 530 | Bank of Silvermoon (no AH — Blood Elves use Horde capitals') | 9536.14, -7188.54, 33.1805 |
Confirmed via direct query that Exodar and Silvermoon have no auction house at all in this DB
(Draenei/Blood Elves travel to their faction's other capitals for AH access) — using each city's
bank instead is the correct substitute, not a data gap. These are anchor points to place *near*;
final non-clipping spot and orientation at each still needs a quick in-game look, same as any
hand-placed spawn.
## Updated draft DB schema
- **`mod_bounty_board_template`** (world DB): `id, type ENUM('kill_count','kill_target'), name,
creature_entry, count_required, zone_id, min_level, item_reward, item_reward_count,
money_reward, weight`, plus for `kill_target` rows — `is_champion TINYINT UNSIGNED NOT NULL
DEFAULT 0`, `position_map SMALLINT UNSIGNED`, `position_x/y/z FLOAT`, `orientation FLOAT` (all
nullable/unused for `kill_count` rows).
- **`mod_bounty_board_rotation`** (world DB): `id, template_id, week_id, slot_index` — `week_id`
repurposed as period-start epoch timestamp (see timing section above), no column change needed,
just a semantic one.
- **`mod_bounty_board_progress`** (characters DB): `guid, rotation_id, kill_count, completed,
turned_in` — unchanged from v0.1. (`turned_in` is now redundant with `completed` given
auto-grant, but harmless to keep for now rather than a migration just to drop it.)
- No new tables for spawn tracking (in-memory only) or rotation timing (reuses the rotation table
itself as source of truth).
## Status
- **v0 (2026-08-14):** hello-world scaffold. Module loads, config flag reads, `.bounty` command
registered with a placeholder response.
- **v0.1 (2026-08-14, confirmed working in-game 2026-08-15):** kill-count vertical slice — DB
schema, kill tracking, auto-reward, `.bounty` progress display. No rotation timer, no
elite/champion spawning, no board yet.
- **Full design locked (2026-08-15):** every remaining architectural question resolved (rotation
timing, spawn mechanism, board object + placement, explainer NPC) — see sections above.
- **v0.2 (2026-08-21, not yet in-game tested):** `kill_target` spawning. Schema gained
`is_champion`/`position_map`/`position_x/y/z`/`orientation` on `mod_bounty_board_template`
(fresh installs via `base/`, existing installs via
`updates/2026_08_21_01.sql`); seeded a second test bounty (kill 1 Defias Cutpurse, entry 94,
placed a few yards from the wolf test spot). `SpawnBountyTargets()` summons every active
`kill_target` bounty's creature at `WorldScript::OnStartup` via `Map::SummonCreature`/
`TEMPSUMMON_MANUAL_DESPAWN`. Turned out `OnPlayerCreatureKill`'s progress tracking needed **no
changes** to support `kill_target` — it was already generic across types (a `kill_target` row is
just `count_required = 1`), so the type-restriction filters in the kill hook and `.bounty` were
simply removed. **Known gap**: killed `kill_target` creatures don't despawn/respawn yet — once
killed they're gone until the next server restart, so only one player/restart can complete a
given slot right now. Acceptable for proving the summon mechanism; needs solving (probably a
short respawn timer) before this is real content, likely alongside step 3 below.
## Implementation order
1. ~~**Schema migration**~~ — done in v0.2.
2. **Weekly rotation `WorldScript::OnUpdate`**: period-boundary check, weighted-random fixed-slot
pick, anti-repeat exclusion, insert new rotation rows.
3. **Despawn wiring + respawn-after-death**: `DespawnOrUnsummon` off the rotation diff (spawning
already exists from v0.2), plus solving the "killed target should come back for other players"
gap noted above — probably a short respawn timer rather than instant, so the bounty doesn't
feel trivially spammable.
4. **Board gameobject**: clone `gameobject_template` 2713's visual into a new entry,
`GameObjectScript` gossip (browse-only), placed at all 8 verified anchor points.
5. **Explainer NPC**: new `creature_template` + static gossip text, placed next to each board.
6. **Champion content**: dedicated `creature_template`(s) with boosted stats + SmartAI abilities —
real content authoring, can happen incrementally once 1-5 are proven working.
## Deferred / explicitly out of scope for now
- **Full 5-player world boss encounters** (unique mechanics, phases, wipe conditions) — the
user's own framing: "a fun laaaaaaate laaaate challenge." Champion bounties (step 6) cover the
"mid-core, more HP and abilities" need without this.
- **Procedural/random bounty generation** — templates are curated by hand, not generated.
## Verification (for whoever implements)
No build/restart happens without the user doing it themselves — established project convention.
Once implemented, in-game verification should confirm: rotation flips at the scheduled time (or
with a temporarily shortened test interval), `kill_target`/champion spawns appear/disappear
correctly and survive a server restart, board gossip renders correct per-bounty progress at all 8
placements without clipping into geometry, and the explainer NPC's gossip displays.