# mod-dragon-legacy — Plan

Fun/hobby project for a friend who wants to play as a dragon. No deadline — meant to grow over
multiple sessions. See `README.md` for what's actually built; this file is where the roadmap and
open ideas live.

**Tone/design pillar:** this module leans fully into dragon fantasy/RP over mechanical seriousness.
Quests should be flavor-text-heavy and narrative-forward rather than terse kill-X-collect-Y filler.
Light romance RP with select dragon NPCs is on the table as a future quest/interaction thread —
keep it in mind when designing dragon NPCs and dialogue so the hooks are there later, even before
it's actually written.

**Candidate dragon NPCs for romance-flavored interactions:**

- *Already exist in this server's world DB, humanoid-disguised, could cameo/mentor:* Krasus (entry
  27990, red, "Consort of the Queen" — i.e. already canonically Alexstrasza's partner), Kalecgos
  (24844/24848/24850/24891/25319/38017, blue), Chromie (10667/26527/27856/27915/30997, bronze),
  Eranikus (5709/8506/15491/15628/15660, green, redemption-arc lore). Since these are all
  established, lore-committed characters (Krasus especially — he's *taken*), best used as flavor
  cameos/mentors/chain narrators rather than the actual romance target, to avoid stepping on
  existing canon relationships.
- *Best fit for the actual romance thread:* original, module-original named dragon NPCs (own
  reserved creature ID range, same as Aerion) invented per flight — sidesteps canon conflicts
  entirely and can be written however fits the friend's taste. Aerion Dragonspeaker himself is also
  an option if there's appetite to make the chain's narrator NPC double as a personal thread.

## Status

- **v1 (done, bug found):** Aerion Dragonspeaker NPC taught Dragon Form (transform) + Whelp Breath
  (gated dragon-only ability) via `player->learnSpell()`. In-game testing (character guid 1515)
  showed the learn *did* persist to `character_spell` correctly server-side — the actual failure
  mode matches `mod-waygate-network`'s v1 exactly (not its "no DB row at all" story, which was a
  different, earlier Waygate symptom): a from-scratch spell ID has no entry in the client's own
  `Spell.dbc`, so the client has nothing to render/cast even though the server-side grant is
  correct. No spellbook entry, no way to cast it. (Earlier notes in this file/memory mis-described
  this as "never persisted" — corrected here against the actual DB check.)
- **v1.1 (built 2026-08-14, not yet in-game tested): spell → command pivot.** Same fix shape as
  Waygate's v1 → v1.1 pivot — see `modules/mod-waygate-network/PLAN.md`'s write-up. Implemented:
  - `mod_dragon_legacy_unlocked` (characters DB, one row per guid) replaces `learnSpell`/`HasSpell()`
    for unlock tracking, mirroring `mod_waygate_network_discovered`. Aerion's gossip now inserts a
    row instead of calling `learnSpell`. The migration that carries forward "already unlocked" for
    old-approach characters and cleans up their 900000/900001 `character_spell` rows lives *inside*
    `data/sql/db-characters/base/dragon_legacy_unlocked.sql` itself (idempotent, reruns harmlessly
    every startup), not a separate `updates/` file.
    - **Why not a separate `updates/` file (found the hard way, 2026-08-14):** the module SQL
      updater orders *all* files across a module's `base/` + `updates/` tree in one global set
      sorted by filename alone (`UpdateFetcher::FillFileListRecursively`/`GetFileList` in
      `src/server/database/Updater/UpdateFetcher.cpp`) — it does not run `base/` before `updates/`.
      A first attempt used a dated `updates/2026_08_14_01.sql` for this migration; `"2026_08_14_01.sql"`
      sorts before `"dragon_legacy_unlocked.sql"` alphabetically (`2` < `d`), so it tried to
      `INSERT`/`DELETE` against the table before the same batch's base file had created it, and the
      whole update batch aborted with `Table 'acore_characters.mod_dragon_legacy_unlocked' doesn't
      exist`. Waygate's own `updates/2026_08_13_01.sql` never hit this because it only touches
      `character_spell`, a core table that already exists — the risk is specific to an `updates/`
      file that depends on a table its own module's `base/` just created. Worth checking filename
      sort order (not just directory) any time a module ships a `base/` table and an `updates/`
      migration against it in the same pass.
  - `.dragon-rawr` (`SEC_PLAYER`) toggles Dragon Form via a **triggered** `CastSpell`/
    `RemoveAurasDueToSpell` — never something the player casts by name, so the client never needs to
    resolve the spell to trigger it. Gated on the unlock table.
  - `.dragon-rawr breath` casts Whelp Breath at the player's current target (`GetSelectedUnit()`),
    gated both in the command handler and by the existing `OnCheckCast`/`HasAura` check in
    `spell_dragon_legacy_whelp_breath`.
  - Known open risk: Dragon Form's buff icon may render blank/generic even if the transform works,
    since icon art is tied to client-side `SpellIcon.dbc`/`Spell.dbc` data. Cosmetic, not
    functional.
- **v1.1.1 (fixed 2026-08-14): `EquippedItemClass` blocked every cast.** First in-game test of
  `.dragon-rawr` found the triggered `CastSpell` was being silently rejected — server log showed
  `HasItemFitToSpellRequirements: Not handled spell requirement for item class 0` on every use.
  Root cause: neither spell's `spell_dbc` INSERT set `EquippedItemClass`, so it took the column's
  schema default of `0` (`ITEM_CLASS_CONSUMABLE`) instead of `-1` (no item requirement).
  `Player::HasItemFitToSpellRequirements` (`src/server/game/Entities/Player/Player.cpp:12781`) only
  handles `ITEM_CLASS_WEAPON`/`ITEM_CLASS_ARMOR`; anything else falls into an unhandled `default:`
  branch, logs that warning, and returns `false` — which makes `Spell::CheckCast`
  (`src/server/game/Spells/Spell.cpp:7308`) reject the cast with `SPELL_FAILED_EQUIPPED_ITEM_CLASS`.
  This would have equally blocked v1's player-initiated `learnSpell`+cast path too, so it was a
  latent bug, not something the v1.1 pivot introduced. Fixed by explicitly setting
  `EquippedItemClass = -1` on both 900000 and 900001 in `dragon_legacy_spells.sql`.
- **v1.1 confirmed working end-to-end (in-game tested 2026-08-14).** `.dragon-rawr` transforms into
  the Black Dragon Whelp and toggles back off correctly. This is the actual confirmation that a
  **triggered** `CastSpell` on a from-scratch spell ID (900000) renders the transform correctly on
  the client, once the unrelated `EquippedItemClass` cast-rejection bug above was out of the way —
  the open question from the whole v1.1 pivot is resolved. Dragon Form's buff-icon-rendering risk
  (noted above) not yet specifically checked.
- **Stance bar experiment (tried and rejected, 2026-08-14).** See "Deferred / rejected" below for
  the full writeup and reasoning.
- **v2 (agreed direction 2026-08-14, not yet built): vehicle-based full transform.** Following the
  stance-bar rejection, investigated whether WotLK's vehicle system could give a real dedicated
  ability bar — it can, and much more cleanly than shapeshift forms (see prior chat, not repeated
  here). Agreed direction: `.dragon-rawr` stops being a self-transform aura and instead summons a
  real dragon-vehicle creature, seats the player in it (`SPELL_AURA_CONTROL_VEHICLE`), and **locks
  out normal play while active** — no spellbook, no bags, just the vehicle's own ability bar. This
  is a deliberate design choice, not a limitation to work around: "you are a dragon, not a human"
  while transformed, matching the tone pillar above. Exiting the vehicle returns the player to
  normal play.
  - **Existing content to build on, found in this world DB:** the Wyrmrest Temple rentable drakes
    from the Dragonblight dailies — Ruby Drake (`27756`, display `25854`), Emerald Drake (`27692`,
    display `25853`), Amber Drake (`27755`, display `25852`) — all share `VehicleId 70` (a reusable
    single-seat vehicle container, already proven shared across other creatures too), each with a
    small 2-ability kit already in `creature_template_spell` (real, already-client-known spells, no
    new content needed). Ruby Drake is the natural pick for the friend's red-flight priority.
  - **Richer reference kit:** Wyrmrest Skytalon (`30161`, display `25835`, `VehicleId 220`) is the
    actual Malygos/Eye of Eternity fight vehicle — a full 7-ability combat kit
    (`56091, 56092, 57090, 57143, 57108, 57092, 57403`), real raid-tested content, worth using as
    inspiration or even reusing outright for a fuller ability spread than the daily-quest drakes.
  - **Mechanism confirmed from source, not yet in-game tested:** `CharmInfo::InitCharmCreateSpells`
    (`src/server/game/Entities/Unit/CharmInfo.cpp:98`) builds the vehicle's ability bar straight from
    the possessed creature's `m_spells[]` and sends it via `SMSG_PET_SPELLS` — same generic,
    client-agnostic packet hunter pet bars use, no client patch needed for *content*. Still subject
    to the general custom-spell-ID rendering limit (see "Deferred / rejected"): any ability on the
    bar needs to be a real, already-client-known spell ID — a from-scratch one would hit the same
    wall in a vehicle bar as it did in the regular spellbook. Not a problem here since the plan is
    to reuse existing drake abilities.
  - **Open design questions for implementation time:** whether to reuse the drake creature entries
    directly or clone them into the module's own reserved ID range (cloning is safer against
    upstream DB changes, matches the module's existing custom-ID discipline); how re-entering combat
    normally after exiting works (does `ExitVehicle` cleanly restore the player, per core behavior,
    or does anything module-side need to force it, similar to `DragonLegacy_Player::OnPlayerBeforeLogout`'s
    existing transform cleanup); whether `mod_dragon_legacy_unlocked`'s existing unlock-gate and
    Aerion's gossip flow carry over unchanged (likely yes — only *what `.dragon-rawr` does* changes,
    not how it's unlocked).
  - **First in-game test (2026-08-14): confirmed working, including a false alarm.** `testvehicle`
    boarded the Ruby Drake successfully. Appeared seethrough at first — investigated multiple data
    -driven theories (unit flags, the drake's addon auras, `Vehicle.dbc`'s camera-fade scalars,
    `CreatureDisplayInfo.dbc`'s model alpha) and ruled all of them out by parsing the actual DBCs;
    turned out to just be the WoW client's normal camera-too-close fade behavior for large vehicle
    models, not a bug. `testvehicle2` (Wintergrasp Demolisher, a real ground vehicle) was built as a
    control group for this but the false alarm was resolved before it mattered.
  - **Hiding the rider ("transform into" vs. "ride"), tested 2026-08-14.** User asked whether the
    player's own model could be hidden while on the vehicle, for a fuller "you become the dragon"
    feel rather than visibly sitting on it. Real mechanism found: `VEHICLE_SEAT_FLAG_HIDE_PASSENGER`
    (`VehicleSeatFlags` enum, `src/server/shared/DataStores/DBCEnums.h:453`) — confirmed by parsing
    `VehicleSeat.dbc` directly that 81 real seats already use it (mostly turrets/siege vehicles, but
    some flying ones too). None of the dragon vehicles (`VehicleId` 70 or 220) use it by default,
    but the vehicle *container* is independent of the creature's model — found `VehicleId 81`'s sole
    seat (`1435`) has hide-passenger + controllable + can-cast all set, and built `testvehicle3` to
    test riding the Ruby Drake's model with that container swapped in at runtime via
    `Unit::CreateVehicleKit()` (no DB changes needed for the test).
    - **First attempt (2026-08-14) used the wrong ID and gave a clean negative signal, not a real
      result.** Initially set to `VehicleId 129` ("Vic's Flying Machine") by mistake — seat 1435
      actually belongs to `81`, a different, otherwise-unused vehicle ID; 129's own seat 0 (`1808`)
      has none of the needed flags. In-game test showed exactly that: empty ability bar, no
      movement — not a flaw in the hide-passenger mechanism, just the wrong container. Corrected to
      `81` (see `mod_dragon_legacy.h`).
    - **Second attempt (2026-08-14, correct `VehicleId 81`): mechanically fine, didn't hide the
      rider.** Ability bar and movement worked correctly this time, confirming `81` was the right
      container — but the player's own model still rendered. `VEHICLE_SEAT_FLAG_HIDE_PASSENGER` is
      never referenced anywhere in this codebase's server logic (confirmed via grep — zero hits), so
      it's pure client-rendering behavior outside what the server controls or this investigation can
      verify statically. Cross-checked against the classic "you feel invisible" vehicles players
      remember (Wintergrasp cannons, demolishers) and **none of them use this flag either** — a
      strong hint it governs how *other* players see you on a vehicle, not your own camera; the
      WotLK client appears to always render the pilot's own model locally for themselves regardless
      of seat flags, the same way stealth/invisibility auras hide you from others but never from
      your own view.
    - **Fallback approach, confirmed working in-game 2026-08-14: shrink instead of hide.**
      `HandleDragonTestVehicle3Command` calls `player->SetObjectScale(0.01f)` after boarding — a
      fully server-controlled unit property, not a client-rendering guess. Confirmed working
      end-to-end: rider becomes effectively invisible. One cosmetic note — the shrink visibly
      animates (smoothly scales down over ~1s) rather than snapping instantly; almost certainly the
      WotLK client's normal interpolation for any scale change (same as Gnomish Shrink Ray, size
      trinkets, etc.), not something server-side code controls. User's call: acceptable tradeoff.
      Restored via `player->SetObjectScale(player->GetNativeObjectScale())` in the shared
      `ExitTestVehicleIfMounted` exit path.
- **Cloning into the module's own range, tested 2026-08-14.** Before settling on final v2 specifics,
  tested whether cloning a real vehicle creature into this module's reserved range works, and
  whether the display model can be freely swapped independent of what it was cloned from. Cloned
  Ruby Drake into entry `901001` (`dragon_legacy_vehicle_test.sql`, same abilities/vehicle config,
  custom name "Dragon Legacy Test Clone") — confirmed working via `.dragon-rawr testvehicle4`: the
  clone spawned correctly with its own name and behaved identically to riding the original.
  - **First model swap attempt showed no visible change — a real DBC finding, not a bug.** Set the
    clone's display to the Wyrmrest Skytalon's (`25835`) to prove independent model selection.
    In-game it looked unchanged. Checked `CreatureDisplayInfo.dbc` directly: Skytalon and all three
    Wyrmrest drakes (Ruby/Emerald/Amber) share the exact same `ModelId 2858` — same 3D mesh, just
    different `CreatureDisplayID`/texture variant. The swap mechanism worked correctly; the chosen
    display just happened to reuse the same underlying model as the source.
  - **Corrected to a genuinely different mesh — confirmed working in-game 2026-08-14.** Switched to
    the Enslaved Proto-Drake's display (`24874`, `ModelId 2650` — the skeletal/armored Storm Peaks
    proto-drake style, confirmed different from `2858` by parsing the DBC). Rendered as a visibly
    distinct model. Cloning + custom name/abilities/vehicle config + independent display model are
    all now confirmed working end-to-end.

## v2 prototyping: done

All open technical questions for the vehicle-based full transform are now resolved and in-game
confirmed (2026-08-14): possess + real ability bar, the mechanism generalizes beyond dragons (control
group), hiding the rider (scale shrink), and cloning into the module's own range with a fully
independent display model. Nothing left to prototype — next session should move from throwaway
`testvehicle`/`testvehicle2`/`testvehicle3`/`testvehicle4` GM commands to building the real thing:
rework `.dragon-rawr` itself around this (summon the module's own cloned dragon, hide + lock out
normal play, clean exit/logout/death handling), decide the final display model per flight color
(red/Alexstrasza first, per the friend's priority), and retire the test commands/entry once the real
build replaces them.

## Roadmap (agreed direction, not just ideas)

- **Neutral dragons.** Certain dragon NPCs should turn neutral toward a player currently in Dragon
  Form, instead of hostile. Likely a SmartAI condition/faction change keyed off the player having
  `SPELL_DRAGON_FORM`'s aura, or a `condition` entry on those NPCs' aggro/faction checks.
- **Custom quests/interactions.** A quest chain tied to the dragon theme — see notes below on how
  quest chains work in AzerothCore. Could tie into unlocking Dragon Form narratively instead of a
  flat gossip-teach, and/or lead into the neutral-dragons mechanic (e.g. a quest that's the
  in-fiction reason certain dragons stop attacking you).
  - **Gate Dragon Form behind a lengthy quest chain.** Replace Aerion's current "yes, gimme"
    single-gossip-option teach with a proper multi-step chain the player has to earn their way
    through to unlock their "dragon potential/blood," rather than instant-granting spell 900000 on
    a gossip click. Aerion would become the chain's start (and likely questgiver/narrator
    throughout, possibly walking/talking via the SmartAI waypoint pattern noted below) instead of a
    one-shot trainer.
    - The chain would likely involve meeting the dragon aspects — realistically just the one
      matching whichever dragonflight/color the player is aiming to become (e.g. Alexstrasza for
      red, Ysera for green, Malygos for blue, Nozdormu for bronze), rather than all of them, since
      the chain is about earning *that* flight's blood specifically. Existing aspect NPCs already
      exist in the world (Wyrmrest Temple etc.) and could be used as later-chain questgivers/story
      beats instead of inventing new ones.
    - Multiple dragonflight colors (currently only Black Whelp exists) is a "might as well" nice-to
      -have, not a priority — the friend this module is for is specifically a red dragon fan, so
      **red/Alexstrasza's flight should be the first (and possibly only, for a while) color
      actually built**, with the chain and Aerion's story written red-first. Generalizing to other
      colors can come later if there's appetite for it.
    - **Per-flight abilities, lore-driven, with some overlap.** This is where to go full lore
      immersion/fun — each color's kit should feel like *that* flight, not a reskinned Whelp
      Breath. Rough direction (flavor to nail down later, not locked in): red (Alexstrasza) leans
      life/fire-as-vitality, could include something healing-adjacent; green (Ysera) leans
      dream/nature, sleep or corruption-cleanse themed; blue (Malygos) leans arcane/spellpower;
      bronze (Nozdormu) leans time (haste/slow effects); black keeps the existing Whelp Breath as
      its signature. A shared baseline (e.g. some form of breath attack, maybe a glide/flight
      utility) makes sense across all flights so they don't feel disconnected from each other, with
      1-2 flight-specific signature abilities layered on top per color — same gating pattern as
      Whelp Breath (`OnCheckCast` + `HasAura`) extends cleanly to "has *this* flight's form active."

## Deferred / rejected (investigated, not module-shaped)

- A genuine 4th talent tab — client's talent UI is hardcoded to 3 tabs, needs a client-side DBC
  patch, not just server-side module code.
- Overwriting a player's class entirely — `getClass()` is checked at hundreds of call sites across
  the core; core-surgery-shaped, not a module.
- **A custom title** ("%s the Dragonspeaker" or similar) — deferred on purpose, not rejected. New
  title *text* needs a client-side `CharTitles.dbc` patch (server can only grant existing DBC
  entries by flipping a bit — see `Player::SetTitle`). Revisit once client modification work
  actually starts. In the meantime, title ID 87 (`%s of the Emerald Dream`) is unused by any
  default AzerothCore content and is a strong thematic fit if a quick, no-client-changes title
  reward is wanted before then.
- **Player-initiated casting of brand-new custom spell IDs** (spellbook/cast bar/macro) — confirmed
  broken in this module (v1, see Status) and independently in `mod-waygate-network`'s v1. `spell_dbc`
  is server-only; the client needs its own local `Spell.dbc` entry to let a player learn/cast
  something by name, and no client patch is distributed here. Same two ways around it as documented
  in Waygate's PLAN.md: reuse an already-client-known spell ID, or ship a client patch. Filed here so
  it's not re-discovered a third time in some future module.
- **A dedicated action bar (like Druid stance bars) that auto-shows on transform, filled with
  real/existing spells.** Investigated and tried in-game 2026-08-14, rejected as not achievable
  cleanly. What's true: action bar *contents* need nothing new (`character_action` is
  server-authoritative regardless of whether the spells in it are custom or already-existing/
  client-known — that half was never the problem). The auto-*switch*-to-a-dedicated-bar behavior
  needs a real `SPELL_AURA_MOD_SHAPESHIFT` (aura 36) into a `SpellShapeshiftForm.dbc` entry, and two
  genuinely blank/unused slots exist in that DBC (IDs 23, 24 — no name, no model, no flags, same
  trick as the unused `CharTitles.dbc` title 87 found earlier). Built a throwaway test (spell
  900002, GM-only `.dragon-rawr teststance`, applying form 23 alongside the existing transform aura
  — safe to layer since slot 23's model fields are both 0) and tried it in-game: **no bonus action
  bar appeared.** Likely explanation: every real player form that *does* get a bonus bar (Cat,
  Bear, Battle/Defensive/Berserker Stance, Shadowform, Stealth, Moonkin, Tree of Life) has a nonzero
  `bonusActionBar` DBC field (1-4, presumably mapping to one of four hardcoded client bonus-bar
  frames); the two blank slots have `bonusActionBar = 0`, matching non-bar forms like Travel/Ghost
  Wolf/Flight Form. No blank slot exists with a nonzero `bonusActionBar` to test instead. Reusing a
  *real* class form's ID (e.g. Bear Form) was considered and rejected without testing — it would
  inherit that form's actual name/icon in the UI (thematically wrong), and the relevant
  MultiActionBar frame may only even get created client-side for characters of the matching class.
  Cleanup: spell 900002 and `.dragon-rawr teststance` were removed after the negative result (see
  `dragon_legacy_spells.sql`'s cleanup `DELETE`) — retired, not reused. Revisit only if a client
  patch ever becomes in scope (same condition as the custom title above).

## How quests would work here

Quests are data-driven (no C++). Core tables in `acore_world`:

- `quest_template` (+ `quest_template_addon`) — the quest: title, level, rewards, and critically
  `PrevQuestID` / `NextQuestID` / `ExclusiveGroup` to link quests into a chain.
- `quest_objectives` — kill/collect/talk-to/area-trigger/etc. objectives.
- `quest_offer_reward` / `quest_request_items` — the narrative text (this is where lore writing
  lives).
- `creature_queststarter` / `creature_questender` — which NPC hands out / takes the quest.
- `npc_text` / `quest_greeting` — pre-quest gossip text.

Workflow: outline the story beats → insert `quest_template` rows with `PrevQuestID`/`NextQuestID`
chaining them → add objectives → wire up questgiver NPCs → write the offer/reward/request text →
reach for SmartAI for anything scripted (see below) → SQL goes under this module's
`data/sql/db-world/`, following the same custom-ID-range discipline as the rest of the module →
test with `.quest add ` / `.reload quest_template` (no rebuild needed, it's pure SQL).

## SmartAI notes (for scripted moments — Aerion walking/talking, escorts, etc.)

SmartAI (`smart_scripts` table) is event → action → target rows per NPC, no C++. Relevant patterns
for this module:

- **NPC walks somewhere while talking:** waypoint path (`waypoint_data`) + `SMART_EVENT_WAYPOINT_REACHED`
  → `SMART_ACTION_TALK` at specific waypoints. Good for "follow me while I narrate the dragon
  legacy."
- **One-off move-then-talk:** event → `SMART_ACTION_MOVE_TO_POS`, linked to a `SMART_ACTION_TALK`
  fired on arrival (`SMART_EVENT_MOVEMENTINFORM` or a delayed link).
- **Escort-flavor quests:** dedicated waypoint/escort action flags handle "player falls behind,"
  "escort complete/failed" automatically — worth using if a quest has the player walking Aerion (or
  a dragon) somewhere.
- Store actual dialogue lines in `creature_text` (referenced by ID from `TALK` actions), not
  inlined, so sound/emote can ride along.

Reserve any new SmartAI-referenced text/creature IDs within this module's existing custom ranges
(900000–900099 spells, 901000–901099 creature/npc_text) — check README before adding more.