Fractured: Paragon core hooks, mod-paragon, mod-ale, Docker build cap

- Track mod-paragon and mod-ale (un-ignore modules in .gitignore).
- Ship docker-compose.override.yml with CMAKE_EXTRA_OPTIONS for LuaJIT (mod-ale).
- Dockerfile: CBUILD_PARALLEL default to limit OOM under Docker/WSL2.
- Core: CLASS_PARAGON sticky combo points (DetachComboTarget), selection rebind,
  Spell::CheckPower rune path for multi-resource Paragon.
- spell_dk_death_rune: IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_ABILITY) for
  Blood of the North / Reaping / DRM on Paragon.
- Remove temporary Paragon CheckPower logging.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-08 00:03:09 -04:00
parent f9f2bc5e0c
commit 8e4c8f57e4
163 changed files with 54817 additions and 10 deletions
+416
View File
@@ -0,0 +1,416 @@
/*
* mod-paragon — Ability Essence (AE) / Talent Essence (TE)
*
* Inspired by Project Ascension's classless currencies (see Fandom:
* Ability Essence / Talent Essence). Server-side only: values persist in
* character_paragon_currency; optional per-spell AE costs live in world
* table paragon_spell_ae_cost. Client UI for bars / advancement panels is
* separate (see README).
*/
#include "CharacterDatabase.h"
#include "Chat.h"
#include "CommandScript.h"
#include "Config.h"
#include "Player.h"
#include "RBAC.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "WorldDatabase.h"
#include "Log.h"
#include <unordered_map>
using namespace Acore::ChatCommands;
namespace
{
struct ParagonCurrencyData
{
uint32 abilityEssence = 0;
uint32 talentEssence = 0;
};
std::unordered_map<uint32, ParagonCurrencyData> gParagonCurrencyCache;
uint32 ComputeStartingAE(uint8 level)
{
uint32 base = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.Start", 9);
uint32 per = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.PerLevel", 1);
uint32 minLv = sConfigMgr->GetOption<uint32>("Paragon.Currency.GrantLevelMin", 10);
if (level >= minLv)
base += uint32(level - minLv + 1) * per;
return base;
}
uint32 ComputeStartingTE(uint8 level)
{
uint32 base = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.Start", 0);
uint32 per = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.PerLevel", 1);
uint32 minLv = sConfigMgr->GetOption<uint32>("Paragon.Currency.GrantLevelMin", 10);
if (level >= minLv)
base += uint32(level - minLv + 1) * per;
return base;
}
ParagonCurrencyData& GetOrCreateCacheEntry(uint32 lowGuid)
{
return gParagonCurrencyCache[lowGuid];
}
void DbUpsertCurrency(uint32 lowGuid, uint32 ae, uint32 te)
{
CharacterDatabase.DirectExecute(
"REPLACE INTO character_paragon_currency (guid, ability_essence, talent_essence) VALUES ({}, {}, {})",
lowGuid, ae, te);
}
void LoadCurrencyFromDb(Player* player)
{
if (!player || player->getClass() != CLASS_PARAGON)
return;
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
return;
uint32 const lowGuid = player->GetGUID().GetCounter();
if (QueryResult r = CharacterDatabase.Query(
"SELECT ability_essence, talent_essence FROM character_paragon_currency WHERE guid = {}", lowGuid))
{
Field const* f = r->Fetch();
ParagonCurrencyData& d = GetOrCreateCacheEntry(lowGuid);
d.abilityEssence = f[0].Get<uint32>();
d.talentEssence = f[1].Get<uint32>();
return;
}
uint32 const ae = ComputeStartingAE(player->GetLevel());
uint32 const te = ComputeStartingTE(player->GetLevel());
DbUpsertCurrency(lowGuid, ae, te);
ParagonCurrencyData& d = GetOrCreateCacheEntry(lowGuid);
d.abilityEssence = ae;
d.talentEssence = te;
}
void SaveCurrencyToDb(Player* player)
{
if (!player || player->getClass() != CLASS_PARAGON)
return;
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
return;
uint32 const lowGuid = player->GetGUID().GetCounter();
auto itr = gParagonCurrencyCache.find(lowGuid);
if (itr == gParagonCurrencyCache.end())
return;
DbUpsertCurrency(lowGuid, itr->second.abilityEssence, itr->second.talentEssence);
}
void RemoveCacheEntry(uint32 lowGuid)
{
gParagonCurrencyCache.erase(lowGuid);
}
uint32 GetAE(Player* player)
{
if (!player)
return 0;
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
if (itr == gParagonCurrencyCache.end())
return 0;
return itr->second.abilityEssence;
}
uint32 GetTE(Player* player)
{
if (!player)
return 0;
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
if (itr == gParagonCurrencyCache.end())
return 0;
return itr->second.talentEssence;
}
bool TrySpendAE(Player* player, uint32 amount)
{
if (!amount)
return true;
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
if (itr == gParagonCurrencyCache.end())
return false;
if (itr->second.abilityEssence < amount)
return false;
itr->second.abilityEssence -= amount;
return true;
}
bool TrySpendTE(Player* player, uint32 amount)
{
if (!amount)
return true;
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
if (itr == gParagonCurrencyCache.end())
return false;
if (itr->second.talentEssence < amount)
return false;
itr->second.talentEssence -= amount;
return true;
}
void GrantLevelUpEssence(Player* player, uint8 oldLevel, uint8 newLevel)
{
if (!player || player->getClass() != CLASS_PARAGON)
return;
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
return;
uint32 const minLv = sConfigMgr->GetOption<uint32>("Paragon.Currency.GrantLevelMin", 10);
uint32 const aePer = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.PerLevel", 1);
uint32 const tePer = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.PerLevel", 1);
ParagonCurrencyData& d = GetOrCreateCacheEntry(player->GetGUID().GetCounter());
for (uint32 lvl = uint32(oldLevel) + 1; lvl <= uint32(newLevel); ++lvl)
{
if (lvl >= minLv)
{
d.abilityEssence += aePer;
d.talentEssence += tePer;
}
}
}
uint32 LookupSpellAECost(uint32 spellId)
{
uint32 const fallback = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.DefaultSpellCost", 2);
QueryResult r = WorldDatabase.Query("SELECT ae_cost FROM paragon_spell_ae_cost WHERE spell_id = {}", spellId);
if (!r)
return fallback;
return std::max<uint32>(r->Fetch()[0].Get<uint32>(), 1u);
}
class Paragon_Essence_PlayerScript : public PlayerScript
{
public:
Paragon_Essence_PlayerScript() : PlayerScript("Paragon_Essence_PlayerScript", {
PLAYERHOOK_ON_LOGIN,
PLAYERHOOK_ON_LOGOUT,
PLAYERHOOK_ON_SAVE,
PLAYERHOOK_ON_CREATE,
PLAYERHOOK_ON_LEVEL_CHANGED,
PLAYERHOOK_CAN_LEARN_TALENT,
PLAYERHOOK_ON_PLAYER_LEARN_TALENTS
}) { }
void OnPlayerLogin(Player* player) override
{
LoadCurrencyFromDb(player);
}
void OnPlayerLogout(Player* player) override
{
SaveCurrencyToDb(player);
RemoveCacheEntry(player->GetGUID().GetCounter());
}
void OnPlayerSave(Player* player) override
{
SaveCurrencyToDb(player);
}
void OnPlayerCreate(Player* player) override
{
if (!player || player->getClass() != CLASS_PARAGON)
return;
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
return;
uint32 const lowGuid = player->GetGUID().GetCounter();
uint32 const ae = ComputeStartingAE(player->GetLevel());
uint32 const te = ComputeStartingTE(player->GetLevel());
DbUpsertCurrency(lowGuid, ae, te);
ParagonCurrencyData& d = GetOrCreateCacheEntry(lowGuid);
d.abilityEssence = ae;
d.talentEssence = te;
}
void OnPlayerLevelChanged(Player* player, uint8 oldLevel) override
{
if (!player || player->getClass() != CLASS_PARAGON)
return;
LoadCurrencyFromDb(player);
GrantLevelUpEssence(player, oldLevel, player->GetLevel());
}
bool OnPlayerCanLearnTalent(Player* player, TalentEntry const* /*talent*/, uint32 /*rank*/) override
{
if (!player || player->getClass() != CLASS_PARAGON)
return true;
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
return true;
uint32 const cost = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.TalentLearnCost", 1);
return GetTE(player) >= cost;
}
void OnPlayerLearnTalents(Player* player, uint32 /*talentId*/, uint32 /*talentRank*/, uint32 /*spellid*/) override
{
if (!player || player->getClass() != CLASS_PARAGON)
return;
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
return;
uint32 const cost = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.TalentLearnCost", 1);
if (!TrySpendTE(player, cost))
{
LOG_ERROR("module", "Paragon TE spend failed post-learn for player {} — currency desync?", player->GetName());
return;
}
}
};
class Paragon_Essence_CommandScript : public CommandScript
{
public:
Paragon_Essence_CommandScript() : CommandScript("Paragon_Essence_CommandScript") { }
ChatCommandTable GetCommands() const override
{
static ChatCommandTable paragonSubTable =
{
{ "currency", HandleCurrency, rbac::RBAC_PERM_COMMAND_LEARN, Console::No },
{ "learn", HandleLearn, rbac::RBAC_PERM_COMMAND_LEARN, Console::No },
{ "runes", HandleRunes, rbac::RBAC_PERM_COMMAND_LEARN, Console::No },
};
static ChatCommandTable commandTable =
{
{ "paragon", paragonSubTable },
};
return commandTable;
}
static bool HandleCurrency(ChatHandler* handler)
{
Player* pl = handler->GetPlayer();
if (!pl || pl->getClass() != CLASS_PARAGON)
{
handler->SendErrorMessage("Paragon currency is only tracked for Paragon characters.", false);
return false;
}
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
{
handler->SendErrorMessage("Paragon.Currency.Enabled is off.", false);
return false;
}
LoadCurrencyFromDb(pl);
handler->PSendSysMessage("Ability Essence: {} | Talent Essence: {}", GetAE(pl), GetTE(pl));
return true;
}
// .paragon runes — diagnostic dump of rune state (server-side truth) and
// forced ResyncRunes packet to the client. Helps diagnose visual-vs-server
// desync where the rune pip animates to ready but the spell cast still
// returns SPELL_FAILED_NO_POWER because GetRuneCooldown(i) > 0 server-side.
static bool HandleRunes(ChatHandler* handler)
{
Player* pl = handler->GetPlayer();
if (!pl || pl->getClass() != CLASS_PARAGON)
{
handler->SendErrorMessage("Only Paragon characters have a rune block to inspect.", false);
return false;
}
for (uint8 i = 0; i < MAX_RUNES; ++i)
{
handler->PSendSysMessage("rune[{}] base={} cur={} cd={}ms grace={}ms",
i,
uint32(pl->GetBaseRune(i)),
uint32(pl->GetCurrentRune(i)),
uint32(pl->GetRuneCooldown(i)),
uint32(pl->GetGracePeriod(i)));
}
for (uint8 t = 0; t < NUM_RUNE_TYPES; ++t)
{
handler->PSendSysMessage("regen[{}] = {} (1/cooldown_ms)",
uint32(t),
pl->GetFloatValue(PLAYER_RUNE_REGEN_1 + t));
}
// Force a fresh client snapshot so we can see if visual catches up.
pl->ResyncRunes(MAX_RUNES);
handler->PSendSysMessage("|cff00ff00ResyncRunes packet sent.|r");
return true;
}
static bool HandleLearn(ChatHandler* handler, SpellInfo const* spell)
{
Player* pl = handler->GetPlayer();
if (!pl || pl->getClass() != CLASS_PARAGON)
{
handler->SendErrorMessage("Only Paragon characters use AE for this command.", false);
return false;
}
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
{
handler->SendErrorMessage("Paragon.Currency.Enabled is off.", false);
return false;
}
if (!spell)
return false;
LoadCurrencyFromDb(pl);
uint32 const cost = LookupSpellAECost(spell->Id);
if (GetAE(pl) < cost)
{
handler->PSendSysMessage("Not enough Ability Essence (need {}, have {}).", cost, GetAE(pl));
return false;
}
if (!SpellMgr::IsSpellValid(spell))
{
handler->SendErrorMessage("Invalid spell.", false);
return false;
}
if (pl->HasSpell(spell->Id))
{
handler->SendErrorMessage("You already know this spell.", false);
return false;
}
if (!TrySpendAE(pl, cost))
return false;
pl->learnSpell(spell->Id, false);
SaveCurrencyToDb(pl);
handler->PSendSysMessage("Learned spell {} ({} AE spent, {} AE remaining).", spell->Id, cost, GetAE(pl));
return true;
}
};
} // namespace (anonymous)
void AddSC_paragon_essence()
{
new Paragon_Essence_PlayerScript();
new Paragon_Essence_CommandScript();
}