#include "mod_dragon_legacy.h"
#include "Chat.h"
#include "CommandScript.h"
#include "Config.h"
#include "Creature.h"
#include "DatabaseEnv.h"
#include "Player.h"
#include "ScriptedGossip.h"
#include "ScriptMgr.h"
#include "SpellScript.h"
using namespace Acore::ChatCommands;
namespace
{
constexpr uint32 GOSSIP_ACTION_LEARN_DRAGON_FORM = 1;
constexpr uint32 DRAGON_LEGACY_NPC_TEXT = 901000;
// Dragon Form (900000) and Whelp Breath (900001) are from-scratch spell IDs with no entry in
// the client's own Spell.dbc -- confirmed in-game that learning them via Player::learnSpell()
// persists to character_spell, but the client has nothing to render/cast for them (no name, no
// icon, no macro resolution). Unlock is tracked here instead, and Dragon Form is applied via a
// triggered CastSpell from the .dragon-rawr command rather than asking the player to cast a
// "known" spell themselves -- see modules/mod-dragon-legacy/PLAN.md ("v1.1: spell -> command
// pivot") for the full writeup and the same lesson learned independently in mod-waygate-network.
bool IsDragonLegacyUnlocked(Player* player)
{
QueryResult result = CharacterDatabase.Query(
"SELECT 1 FROM mod_dragon_legacy_unlocked WHERE guid = {}",
player->GetGUID().GetCounter());
return result != nullptr;
}
}
class DragonLegacy_NPC : public CreatureScript
{
public:
DragonLegacy_NPC() : CreatureScript("DragonLegacy_NPC") { }
bool OnGossipHello(Player* player, Creature* creature) override
{
ClearGossipMenuFor(player);
if (sConfigMgr->GetOption("DragonLegacy.Enable", true) && !IsDragonLegacyUnlocked(player))
{
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Teach me the way of the dragon!",
GOSSIP_SENDER_MAIN, GOSSIP_ACTION_LEARN_DRAGON_FORM);
}
SendGossipMenuFor(player, DRAGON_LEGACY_NPC_TEXT, creature->GetGUID());
return true;
}
bool OnGossipSelect(Player* player, Creature* /*creature*/, uint32 sender, uint32 action) override
{
if (sender != GOSSIP_SENDER_MAIN)
return false;
CloseGossipMenuFor(player);
if (action == GOSSIP_ACTION_LEARN_DRAGON_FORM && sConfigMgr->GetOption("DragonLegacy.Enable", true)
&& !IsDragonLegacyUnlocked(player))
{
CharacterDatabase.Execute(
"INSERT INTO mod_dragon_legacy_unlocked (guid) VALUES ({})",
player->GetGUID().GetCounter());
ChatHandler(player->GetSession()).PSendSysMessage(
"You have unlocked the way of the dragon. Use .dragon-rawr to transform, and "
".dragon-rawr breath to unleash Whelp Breath on your target.");
}
return true;
}
};
class spell_dragon_legacy_whelp_breath : public SpellScript
{
PrepareSpellScript(spell_dragon_legacy_whelp_breath);
SpellCastResult CheckDragonForm()
{
if (!GetCaster()->HasAura(SPELL_DRAGON_FORM))
return SPELL_FAILED_ONLY_SHAPESHIFT;
return SPELL_CAST_OK;
}
void Register() override
{
OnCheckCast += SpellCheckCastFn(spell_dragon_legacy_whelp_breath::CheckDragonForm);
}
};
class DragonLegacyCommandScript : public CommandScript
{
public:
DragonLegacyCommandScript() : CommandScript("DragonLegacyCommandScript") { }
ChatCommandTable GetCommands() const override
{
static ChatCommandTable dragonRawrCommandTable =
{
{ "breath", HandleDragonBreathCommand, SEC_PLAYER, Console::No },
{ "testvehicle", HandleDragonTestVehicleCommand, SEC_GAMEMASTER, Console::No },
{ "testvehicle2", HandleDragonTestVehicle2Command, SEC_GAMEMASTER, Console::No },
{ "testvehicle3", HandleDragonTestVehicle3Command, SEC_GAMEMASTER, Console::No },
{ "testvehicle4", HandleDragonTestVehicle4Command, SEC_GAMEMASTER, Console::No },
{ "", HandleDragonFormCommand, SEC_PLAYER, Console::No },
};
static ChatCommandTable commandTable =
{
{ "dragon-rawr", dragonRawrCommandTable },
};
return commandTable;
}
static bool HandleDragonFormCommand(ChatHandler* handler)
{
Player* player = handler->GetPlayer();
if (!player)
return true;
if (!sConfigMgr->GetOption("DragonLegacy.Enable", true))
{
handler->PSendSysMessage("The way of the dragon is currently disabled.");
return true;
}
if (player->HasAura(SPELL_DRAGON_FORM))
{
player->RemoveAurasDueToSpell(SPELL_DRAGON_FORM);
return true;
}
if (!IsDragonLegacyUnlocked(player))
{
handler->PSendSysMessage("You have not learned the way of the dragon yet.");
return true;
}
player->CastSpell(player, SPELL_DRAGON_FORM, true);
return true;
}
static bool HandleDragonBreathCommand(ChatHandler* handler)
{
Player* player = handler->GetPlayer();
if (!player)
return true;
if (!player->HasAura(SPELL_DRAGON_FORM))
{
handler->PSendSysMessage("You must take dragon form before you can use Whelp Breath.");
return true;
}
Unit* target = player->GetSelectedUnit();
if (!target)
{
handler->PSendSysMessage("You need a target to breathe fire at.");
return true;
}
player->CastSpell(target, SPELL_WHELP_BREATH, true);
return true;
}
// Shared exit path for all testvehicle* commands below -- returns true if the player was on a
// test vehicle and has now been taken off it (and it despawned), false if they weren't on one.
// Always restores native object scale on exit -- harmless no-op for testvehicle/testvehicle2
// (which never touch it), undoes testvehicle3's shrink-to-hide trick.
static bool ExitTestVehicleIfMounted(Player* player, ChatHandler* handler)
{
if (!player->GetVehicle())
return false;
Unit* base = player->GetVehicleBase();
player->ExitVehicle();
player->SetObjectScale(player->GetNativeObjectScale());
if (base)
if (Creature* baseCreature = base->ToCreature())
baseCreature->DespawnOrUnsummon();
handler->PSendSysMessage("Exited the test vehicle.");
return true;
}
// v2 prototype only, see PLAN.md "v2: vehicle-based full transform" -- summons a vehicle
// creature and seats the player in it via the same public API core scripts use
// (Unit::EnterVehicle), to prove the possess + bar mechanic works before building the real
// thing. No lockout logic yet. GM-only. Shared by testvehicle and testvehicle2.
static bool ToggleTestVehicle(ChatHandler* handler, uint32 creatureEntry, char const* boardedMessage)
{
Player* player = handler->GetPlayer();
if (!player)
return true;
if (ExitTestVehicleIfMounted(player, handler))
return true;
Creature* vehicle = player->SummonCreature(creatureEntry, player->GetPositionX(),
player->GetPositionY(), player->GetPositionZ(), player->GetOrientation(),
TEMPSUMMON_TIMED_DESPAWN, 600000);
if (!vehicle)
{
handler->PSendSysMessage("Could not summon the test vehicle.");
return true;
}
// Mirrors a workaround in the real Oculus drake AI (oculus.cpp, npc_oculus_drakeAI) for a
// known "summoned in air with wrong animation state" issue -- trying it here in case it
// also fixes the seethrough rendering seen on the drake test.
vehicle->SetDisableGravity(false);
vehicle->SetHover(false);
player->EnterVehicle(vehicle, 0);
handler->PSendSysMessage(boardedMessage);
return true;
}
static bool HandleDragonTestVehicleCommand(ChatHandler* handler)
{
return ToggleTestVehicle(handler, NPC_TEST_RUBY_DRAKE,
"Boarded the test drake -- check your action bar and whether it renders solid.");
}
// Control group: a real, well-tested, non-dragon/non-flying vehicle, to check whether the
// seethrough issue is specific to the drake/flying-model or a general artifact of how this
// test summons and boards vehicles.
static bool HandleDragonTestVehicle2Command(ChatHandler* handler)
{
return ToggleTestVehicle(handler, NPC_TEST_DEMOLISHER,
"Boarded the test demolisher (control group, non-dragon) -- check whether it renders solid.");
}
// Same drake model as testvehicle, but the vehicle container is swapped at runtime
// (Unit::CreateVehicleKit) to VEHICLE_TEST_HIDE_PASSENGER, whose seat has
// VEHICLE_SEAT_FLAG_HIDE_PASSENGER set -- tests whether the rider can be hidden on the drake's
// model, i.e. "transform into" rather than "sit on top of."
static bool HandleDragonTestVehicle3Command(ChatHandler* handler)
{
Player* player = handler->GetPlayer();
if (!player)
return true;
if (ExitTestVehicleIfMounted(player, handler))
return true;
Creature* drake = player->SummonCreature(NPC_TEST_RUBY_DRAKE, player->GetPositionX(),
player->GetPositionY(), player->GetPositionZ(), player->GetOrientation(),
TEMPSUMMON_TIMED_DESPAWN, 600000);
if (!drake)
{
handler->PSendSysMessage("Could not summon the test drake.");
return true;
}
drake->SetDisableGravity(false);
drake->SetHover(false);
drake->RemoveVehicleKit();
drake->CreateVehicleKit(VEHICLE_TEST_HIDE_PASSENGER, NPC_TEST_RUBY_DRAKE);
player->EnterVehicle(drake, 0);
// VEHICLE_SEAT_FLAG_HIDE_PASSENGER alone didn't hide the rider in-game (2026-08-14) -- the
// WotLK client appears to always render the pilot's own model locally regardless of seat
// flags. Shrinking the player to near-zero scale is a fully server-controlled fallback that
// achieves the same visual result without depending on client-side seat behavior. Restored
// to native scale in ExitTestVehicleIfMounted.
player->SetObjectScale(0.01f);
handler->PSendSysMessage("Boarded the test drake with the rider shrunk -- check whether you're effectively invisible on it.");
return true;
}
// Boards the module's own cloned test dragon (901001, see dragon_legacy_vehicle_test.sql)
// instead of the real Ruby Drake -- proves cloning into the module's reserved range works, and
// that the display model can be freely swapped independent of the source it was cloned from
// (this clone deliberately uses a different model than Ruby Drake's own).
static bool HandleDragonTestVehicle4Command(ChatHandler* handler)
{
return ToggleTestVehicle(handler, NPC_TEST_DRAGON_CLONE,
"Boarded the cloned test dragon (different model) -- check whether the swapped display renders correctly.");
}
};
class DragonLegacy_Player : public PlayerScript
{
public:
DragonLegacy_Player() : PlayerScript("DragonLegacy_Player") { }
void OnPlayerBeforeLogout(Player* player) override
{
if (player->HasAura(SPELL_DRAGON_FORM))
player->RemoveAurasDueToSpell(SPELL_DRAGON_FORM);
}
void OnPlayerDeleteFromDB(CharacterDatabaseTransaction trans, uint32 guid) override
{
// Otherwise a deleted character's guid can be recycled and the new character on that guid
// would inherit an unlock it never earned.
trans->Append("DELETE FROM mod_dragon_legacy_unlocked WHERE guid = {}", guid);
}
};
void AddSC_mod_dragon_legacy()
{
new DragonLegacy_NPC();
new DragonLegacy_Player();
new DragonLegacyCommandScript();
RegisterSpellScript(spell_dragon_legacy_whelp_breath);
}