Compare commits

...

6 Commits

Author SHA1 Message Date
Docker Build a251e56c59 Paragon: Builds QoL -- share codes, unload, remaining AE/TE on hover
- Replace the "favorite" toggle with import-by-share-code: every build
  gets a 6-char realm-unique alphanumeric code on creation; pasting one
  into the BuildsPane share box copies the recipe (name + icon + spells
  + talents) into the importer's catalog as a new build, with a fresh
  share code so the imported copy can be re-shared independently.
- Add C BUILD UNLOAD verb so the client can clear a stale active-build
  pointer without forcing a swap. Wired to a new "Unload (clear active)"
  right-click context menu entry on the active build.
- Per-build tooltip now shows "Remaining if loaded: X AE / Y TE",
  computed server-side as total_earned - recipe_cost. Negative renders
  red so the player sees insufficient-currency cases before clicking
  Load. Suppressed for the active build (HandleBuildLoad short-circuits
  on target == active so the line would be misleading).
- Schema migration 2026_05_10_04.sql: drop is_favorite from
  character_paragon_builds and add share_code CHAR(6) UNIQUE NULL with
  lazy backfill on every PushBuildCatalog (so pre-migration rows pick
  up codes the first time the player opens the panel).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 04:15:11 -04:00
Docker Build 7de018f7eb Paragon: add Builds catalog (saved loadouts with pet park/unpark)
Server-side Character Advancement now stores named, icon-tagged build
recipes (panel-purchased spells + per-spec talent ranks) and atomically
swaps between them by snapshotting the active build, refunding AE/TE
through HandleParagonReset{Talents,Abilities}, and re-spending on the
target recipe. Hunter pets attached to a build are parked to
PET_SAVE_NOT_IN_SLOT (mirroring HandleStableSwapPet) so name, talents,
and exp survive swaps; non-hunter pets (warlock demon, DK ghoul, mage
water elemental) are NOT parked because the engine resummons them from
a fresh template each cast.

New PARAA verbs: Q BUILDS / C BUILD NEW / C BUILD EDIT / C BUILD
DELETE / C BUILD FAVORITE / C BUILD LOAD. The catalog is pushed on
login and after every mutation as a single addon message.

Schema (mod-paragon migration 2026_05_10_03.sql):
- character_paragon_builds (build_id PK, guid, name, icon, is_favorite,
  pet_number, created_at)
- character_paragon_build_spells (build_id, spell_id)
- character_paragon_build_talents (build_id, spec, talent_id, rank)
- character_paragon_active_build (guid PK, build_id)

The talent recipe table is spec-keyed so a build remembers tank/dps
dual-spec layouts independently. Swaps are blocked while in combat.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 02:35:55 -04:00
Docker Build abb25f56d1 Paragon: expand IsClass hooks and addon pet talent reset
Broaden OnPlayerIsClass for CLASS_CONTEXT_ABILITY, pet/charm/equip contexts; add PARAA C RESET PET TALENTS handler. Update CLIENT-PATCHES.md for patch-enUS-5/6 and PARAA.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 01:36:54 -04:00
Docker Build 7a92231614 Add scripts/vps-update-server.sh for native VPS git pull and compile
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 21:42:38 -05:00
Docker Build f2952c905a Fractured: strip class-spell reagents at load; Paragon relic ranged slot
- SpellInfoCorrections: zero Reagent/ReagentCount on spells with non-zero
  SpellFamilyName so class abilities no longer require shards, candles,
  etc., while profession crafts (SpellFamilyName 0) keep mats. Matches
  the client Spell.dbc bake in patch-enUS-4.MPQ.
- Paragon_SC: OnPlayerIsClass returns true for CLASS_CONTEXT_EQUIP_RELIC
  for paladin/druid/shaman/warlock/dk so Paragon can equip all relic types
  in the ranged slot.
- CLIENT-PATCHES: document Spell.dbc reagent pass, rune script order, and
  stock ammo slot behavior in patch-enUS-5.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 22:38:32 -04:00
Docker Build 8abd40f217 Paragon: give class 12 intrinsic AP and SP scaling from stats
Stock 3.3.5 hardcodes per-class stat -> AP/SP formulas in
Player::UpdateAttackPowerAndDamage and Unit::SpellBase{Damage,Healing}BonusDone,
so class 12 fell into the default branches and ended up with 0 AP and 0 SP
regardless of STR / AGI / INT / SPI. The character sheet, combat log, and
ability damage all reflected this, and Mental Quickness-style AP->SP plumbing
silently no-oped on Paragon characters.

Add Paragon-specific branches in core (no PlayerScript hooks - those caused
SIGSEGVs when the new mid-list enum entry shifted later hook ordinals and
broke vtable dispatch):

- StatSystem.cpp: melee and ranged AP = level*2 + STR + AGI - 20, mirroring
  the formula the UI patch already advertises in tooltips.
- Unit.cpp:       intrinsic SP    = level*2 + INT + SPI - 20 (clamped >=0),
  added symmetrically to SpellBaseDamageBonusDone and
  SpellBaseHealingBonusDone so the single advertised Spell Power value the
  character sheet renders matches what spells actually use in combat.

Drop the now-unused UnitDefines.h include in Paragon_SC.cpp - it was only
needed by the AP PlayerScript hook that was rolled back in favor of the
core change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 20:39:23 -04:00
9 changed files with 1746 additions and 20 deletions
+17 -6
View File
@@ -13,16 +13,22 @@ This file is the table of contents and install guide.
| Artifact | Size | Purpose |
|---|---|---|
| `patch-enUS-4.MPQ` | ~5 MB | DBC + GlueXML bake. Adds `CLASS_PARAGON` (id 12), the character-create slot, glue strings, talent-tab DBC entries, and the Paragon resource bar definitions. Required for character creation as Paragon to even show up. |
| `patch-enUS-5.MPQ` | ~50 KB | FrameXML overrides. Replaces stock `PlayerFrame.lua` / `RuneFrame.lua` / `ComboFrame.lua` / `UnitFrame.lua` / `SpellBookFrame.lua` + `SpellBookFrame.xml` with Paragon-aware versions: rune simulator, combo-point simulator, server-authoritative resource sync over the `PARAA` addon channel, action-button usability + click guards, an expanded spellbook (higher `MAX_SPELLS`, 24 skill-line tabs instead of stock 8) so all-class spells render, and a tooltip post-processor that appends ", Paragon" to the "Classes:" line on class-restricted gear / glyphs (the server bypasses `AllowableClass` for class 12, but the engine paints the line red and omits Paragon — the Lua hook recolors it green and adds the name so the player can tell it's wearable). |
| `patch-enUS-6.MPQ` | ~160 KB | The `ParagonAdvancement` addon. Replaces the talent pane (`N` key) for Paragon characters with the Character Advancement panel: per-class spell tabs, talent grid, Overview/Search tabs, AE/TE currency, commit / reset / preview, login-time toast suppression. |
| `patch-enUS-4.MPQ` | ~5 MB | DBC + GlueXML bake. Adds `CLASS_PARAGON` (id 12), the character-create slot, glue strings, game-table DBCs, and a patched `Spell.dbc`: **(1)** `RuneCostID` zeroed on every rune-cost spell so nonDeath Knight clients still send DK casts (rune costs are shown via `RuneFrame.lua`); **(2)** `Reagent[]` / `ReagentCount[]` zeroed on every spell whose `SpellFamilyName` is non-zero (all class abilities), while profession crafts (`SpellFamilyName == 0`) keep their materials. Both edits mirror server load-time corrections so client preflight and server validation stay aligned. Required for character creation as Paragon to even show up. |
| `patch-enUS-5.MPQ` | ~57 KB | FrameXML overrides. Replaces stock `PlayerFrame.lua` / `RuneFrame.lua` / `ComboFrame.lua` / `UnitFrame.lua` / `SpellBookFrame.lua` + `SpellBookFrame.xml` with Paragon-aware versions: rune simulator, combo-point simulator, server-authoritative resource sync over the `PARAA` addon channel, action-button usability + click guards, an expanded spellbook (higher `MAX_SPELLS`, 24 skill-line tabs instead of stock 8) so all-class spells render, Paragon stat tooltips on the character sheet (including filtering duplicate “attack power from strength” lines so the paper doll matches server AP), a tooltip post-processor that appends ", Paragon" to the "Classes:" line on class-restricted gear / glyphs (the server bypasses `AllowableClass` for class 12, but the engine paints the line red and omits Paragon — the Lua hook recolors it green and adds the name so the player can tell it's wearable), and **PetFrame** re-anchored so the **pet unit frame sits below the rune row** for Paragon (stock layout had runes overlapping the pet portrait). The paper-doll **ammo slot** follows stock visibility rules (shown for hunters / ranged weapons; hidden when `UnitHasRelicSlot` applies). |
| `patch-enUS-6.MPQ` | ~134 KB | The `ParagonAdvancement` addon. Replaces the talent pane (`N` key) for Paragon characters with the Character Advancement panel: per-class spell tabs, talent grid, Overview/Search tabs, AE/TE currency, commit / reset / preview, login-time toast suppression, a **PETS** tab with live hunter pet talent trees (preview learn, no TE/AE cost), a dedicated **Reset Pet Talents** control (server `PARAA` `C RESET PET TALENTS` — instant, no gold, no confirmation; requires matching worldserver), bottom-row **Reset all Abilities / Reset Build / Reset all Talents** disabled while on the PETS tab so those paths cannot dismiss the pet or unlearn Tame Beast, and a **Builds** page (full-pane overlay opened from the bottom-row Builds button) for saving named, icon-tagged loadouts: New Build (+) icon picker reuses `MACRO_ICON_FILENAMES`, right-click for edit/delete, shift-left-click to favorite (favorites bubble to the top), left-click pops a Load Build confirm. Build swaps reset + refund AE/TE, re-spend on the saved recipe, and **park hunter pets** to `PET_SAVE_NOT_IN_SLOT` so their name/talents/exp are preserved across swaps. |
| `Wow.exe` | ~7.5 MB | 3.3.5a (build 12340) client byte-patched to skip the MPQ signature check so custom `patch-enUS-N.MPQ` files load. Diff against stock is a few bytes; everything else is unchanged. |
Server and client work as a pair: the addon talks to `mod-paragon` on the
worldserver via `WHISPER` addon-channel messages with the `PARAA` prefix
(currency push, spell/talent snapshot, commit, combo points, rune
cooldowns, learn-toast silence window). Mismatched versions usually
manifest as the panel rendering blank or AE/TE reading 0/0.
cooldowns, learn-toast silence window, **`C RESET PET TALENTS`**
for hunter pet talent resets from the Character Advancement PETS tab,
and the **build catalog** verbs `Q BUILDS` / `C BUILD NEW` / `C BUILD
EDIT` / `C BUILD DELETE` / `C BUILD FAVORITE` / `C BUILD LOAD` for the
saved-loadout system on the Builds page). Build swaps require the
matching worldserver image because the swap path is server-driven
(snapshot → reset → re-spend → pet park/unpark). Mismatched versions
usually manifest as the panel rendering blank or AE/TE reading 0/0.
---
@@ -233,7 +239,12 @@ tools\build_paragon_advancement_patch.ps1 -Deploy # -> patch-enUS-6.MPQ
`patch-enUS-4.MPQ` is the DBC + GlueXML bake; the bake scripts live with
the rest of the dev tooling and are not part of this repo by design
(see the repo-tidy policy in `README.txt` next to this file).
(see the repo-tidy policy in `README.txt` next to this file). Typical
order on a maintainer machine:
1. `fractured-tooling/from-workspace-root/_patch_spell_dbc_runes.py` — stage `Spell.dbc` with `RuneCostID` cleared.
2. `fractured-tooling/from-workspace-root/_patch_spell_dbc_reagents.py` — same staged `Spell.dbc`, clear class-spell reagents for client preflight.
3. `fractured-tooling/from-workspace-root/_make_paragon_dbc_patch.py` — rebuild `ChrClasses` / `CharBaseInfo` / game tables, then pack `patch-enUS-4.MPQ`.
The patched `Wow.exe` is a one-time hex-edit of the stock 3.3.5a
client. The diff is publicly documented in the WoW emulation community
@@ -0,0 +1,62 @@
-- mod-paragon Character Advancement: Build catalog (saved loadouts).
-- ----------------------------------------------------------------------------
-- A "build" is a named, icon-tagged loadout of panel-purchased spells and
-- talent ranks. Each Paragon character can save many builds and swap
-- between them via the Builds page in the Character Advancement panel.
--
-- Swap workflow (see HandleBuildLoad in Paragon_Builds.cpp):
-- 1. If a build is currently active, snapshot the player's current
-- panel-purchased spells + per-spec talent ranks into that build's
-- recipe rows (overwriting the stored recipe).
-- 2. If the active build's hunter pet is currently summoned, unsummon
-- it to PET_SAVE_NOT_IN_SLOT and store its `pet_number` on the
-- active build row so it can be restored on swap-back.
-- 3. Reset all panel-bought abilities and talents (refunding AE/TE).
-- 4. Re-buy each spell + talent in the target build's recipe (charging
-- AE/TE; aborts if insufficient AE/TE -- player keeps refunded
-- currency in that case and active becomes NULL).
-- 5. Move the target build's parked pet (if any) back to current.
-- 6. Update active_build pointer.
--
-- Pet ownership: a parked pet sits in `character_pet` with slot=100
-- (PET_SAVE_NOT_IN_SLOT), exactly like the engine's stable-master
-- offload, but tied to the build via `pet_number` instead of any
-- in-game stable slot. Build deletion drops the parked pet rows
-- entirely (PET_SAVE_AS_DELETED equivalent) -- player is warned.
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `character_paragon_builds` (
`build_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`guid` INT UNSIGNED NOT NULL COMMENT 'characters.guid',
`name` VARCHAR(32) NOT NULL,
`icon` VARCHAR(64) NOT NULL DEFAULT 'INV_Misc_QuestionMark',
`is_favorite` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`pet_number` INT UNSIGNED NULL COMMENT 'character_pet.id of parked hunter pet, NULL when no pet bound to this build',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`build_id`),
KEY `idx_guid` (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='mod-paragon: saved Character Advancement build catalog';
CREATE TABLE IF NOT EXISTS `character_paragon_build_spells` (
`build_id` INT UNSIGNED NOT NULL,
`spell_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`build_id`, `spell_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='mod-paragon: per-build recipe -- panel-purchased spells';
CREATE TABLE IF NOT EXISTS `character_paragon_build_talents` (
`build_id` INT UNSIGNED NOT NULL,
`spec` TINYINT UNSIGNED NOT NULL COMMENT '0 = primary spec, 1 = secondary (dual spec)',
`talent_id` SMALLINT UNSIGNED NOT NULL,
`rank` TINYINT UNSIGNED NOT NULL,
PRIMARY KEY (`build_id`, `spec`, `talent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='mod-paragon: per-build recipe -- panel-purchased talent ranks per spec';
CREATE TABLE IF NOT EXISTS `character_paragon_active_build` (
`guid` INT UNSIGNED NOT NULL COMMENT 'characters.guid',
`build_id` INT UNSIGNED NOT NULL COMMENT 'character_paragon_builds.build_id (per-character active pointer)',
PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='mod-paragon: pointer to whichever build is currently loaded (one row per Paragon character)';
@@ -0,0 +1,30 @@
-- mod-paragon Character Advancement: Builds catalog schema cleanup.
-- ----------------------------------------------------------------------------
-- Two changes:
-- 1. Drop `is_favorite` -- the favorite flag and shift-click-to-favorite
-- flow are removed. Builds are now ordered solely by build_id ASC.
-- 2. Add `share_code` CHAR(6) -- a random alphanumeric token generated
-- server-side at build creation that uniquely identifies a saved
-- build across the realm. Players exchange codes out-of-band and
-- use the BuildsPane "Load Build!" share box to import a copy of
-- the build (name + icon + spell + talent recipe) into their own
-- catalog. The copy gets a fresh share_code so re-sharing is
-- always traceable to the latest owner; the original isn't touched.
--
-- The column is NULL-tolerant so any rows that pre-date this migration
-- (created under 2026_05_10_03's schema) coexist cleanly. The server
-- backfills NULLs lazily in PushBuildCatalog -- the next time a player
-- opens the BuildsPane on a Paragon character, any of their builds that
-- still have a NULL share_code will get one generated and persisted.
--
-- Charset: 31 unambiguous chars (A-Z minus I/O minus 0/1) gives 31^6 ~=
-- 887M codes; collision retry on insert keeps probability of a duplicate
-- vanishing for any realistic catalog size.
-- ----------------------------------------------------------------------------
ALTER TABLE `character_paragon_builds`
DROP COLUMN `is_favorite`,
ADD COLUMN `share_code` CHAR(6) NULL DEFAULT NULL
COMMENT 'random alphanumeric token for import-by-code; lazily generated'
AFTER `icon`,
ADD UNIQUE INDEX `uk_share_code` (`share_code`);
File diff suppressed because it is too large Load Diff
+207 -14
View File
@@ -7,15 +7,17 @@
#include "Chat.h"
#include "Config.h"
#include "Creature.h"
#include "CreatureData.h"
#include "GameTime.h"
#include "Log.h"
#include "ObjectGuid.h"
#include "Pet.h"
#include "Player.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
#include "SpellScript.h"
#include "SpellScriptLoader.h"
#include "UnitDefines.h"
#include "WorldPacket.h"
#include "WorldSession.h"
@@ -45,27 +47,218 @@ public:
if (!player || player->getClass() != CLASS_PARAGON)
return std::nullopt;
// Death Knight rune / runic power ability stack (narrow on purpose).
if (unitClass == CLASS_DEATH_KNIGHT && context == CLASS_CONTEXT_ABILITY)
// ============================================================
// Ability stack -- claim ALL nine vanilla classes.
// ============================================================
// CLASS_CONTEXT_ABILITY is read by every class-specific spell
// gate in core / scripts: DK rune mechanics (Spell.cpp,
// SpellEffects.cpp, spell_dk.cpp, SpellAuraEffects.cpp),
// Warrior Titan's Grip / Bladestorm (Player.cpp 3783, 15432,
// PlayerUpdates.cpp 1547), Paladin Rebuke (Player.cpp 15441),
// Shaman dual-wield bookkeeping (Player.cpp 5028), Hunter pet
// / Hunter's Mark gates (spell_item.cpp 3718), Druid Insect
// Swarm / Wild Growth (SpellAuraEffects.cpp 2153, 2232),
// Priest Spirit of Redemption out-of-bounds check (Unit.cpp
// 14238), Rogue pickpocketing (LootHandler.cpp 86/165/385,
// Vehicle.cpp 80). Paragon learns abilities from every class
// through Character Advancement, so claiming all of them lets
// every gated spell script execute its class-specific branch
// for our players. The only downside is double-pathed scripts
// (e.g. a spell with both warrior and rogue branches) will
// pick whichever the script tests first -- acceptable.
if (context == CLASS_CONTEXT_ABILITY)
return true;
// Warrior ability stack: enables warrior-spec ability gates anywhere
// they're checked. None of the currently-traced sites in core/scripts
// gate on (CLASS_WARRIOR, CLASS_CONTEXT_ABILITY), so this is a safe
// forward-compatible claim. Rage generation itself is gated on
// HasActivePowerType(POWER_RAGE) and is wired below.
if (unitClass == CLASS_WARRIOR && context == CLASS_CONTEXT_ABILITY)
return true;
// Reactive melee states: Overpower-on-dodge (warrior), Counterattack window (hunter).
// We intentionally do NOT claim CLASS_ROGUE here: that context skips the generic
// AURA_STATE_DEFENSE update on dodge (Riposte path) in Unit::ProcDamageAndSpellFor.
// ============================================================
// Reactive melee states.
// ============================================================
// Warrior dodge -> AURA_STATE_DEFENSE (Overpower window).
// Hunter parry -> AURA_STATE_HUNTER_PARRY (Counterattack).
// We intentionally do NOT claim CLASS_ROGUE here:
// Unit::ProcDamageAndSpellFor (Unit.cpp 12824) skips the
// generic AURA_STATE_DEFENSE update on dodge for rogues so
// Riposte can take over. Claiming rogue would silently kill
// Overpower for Paragon, and Riposte already works for us via
// the warrior-style state we already grant.
if (context == CLASS_CONTEXT_ABILITY_REACTIVE)
{
if (unitClass == CLASS_WARRIOR || unitClass == CLASS_HUNTER)
return true;
}
// ============================================================
// Pet ownership contexts.
// ============================================================
// CLASS_CONTEXT_PET is read by Pet::AddToWorld, Pet::CreateBase
// AtCreatureInfo, Pet::InitStatsForLevel (twice -- the
// MAX_PET_TYPE bootstrap branch and the per-class attack-time
// scaling), Pet::IsPermanentPetFor, Player::SummonPet,
// Player::CanResummonPet, Spell::EffectTameCreature,
// SpellEffects.cpp (CreateTamedPet debug effects, Eyes of the
// Beast), spell_generic.cpp 1760 (charm-as-pet conversion),
// and PlayerGossip.cpp's hunter stable check.
//
// The cleanest disambiguation is by the *active pet's* shape:
// HUNTER_PET -> hunter (beast tame)
// SUMMON_PET + DEMON type -> warlock (Imp/VW/Succ/...)
// SUMMON_PET + UNDEAD type -> DK ghoul / Army of Dead
// SUMMON_PET + ELEMENTAL type -> mage water / shaman fire
// For HUNTER specifically the no-pet case is also claimed so
// Tame Beast's EffectTameCreature gate passes during cast.
if (context == CLASS_CONTEXT_PET)
{
Pet const* activePet = const_cast<Player*>(player)->GetPet();
// Hunter beast: claim during taming OR when a HUNTER_PET is
// already active. This is what makes Tame Beast / Call Pet
// / pet stable / Counterattack pet aura feedback work.
if (unitClass == CLASS_HUNTER)
{
if (!activePet || activePet->getPetType() == HUNTER_PET)
return true;
return std::nullopt;
}
// All other classes only claim when an active SUMMON_PET is
// present. We then disambiguate by the creature's type
// because warlock / DK / mage / shaman all use SUMMON_PET.
if (!activePet || activePet->getPetType() != SUMMON_PET)
return std::nullopt;
CreatureTemplate const* tmpl = activePet->GetCreatureTemplate();
if (!tmpl)
return std::nullopt;
switch (unitClass)
{
case CLASS_WARLOCK:
// Drives Master Demonologist / Demonic Knowledge /
// Demonic Pact propagation, last-pet-spell tracking
// (Pet.cpp 112), and IsPermanentPetFor (Pet.cpp
// 2288) so demon pets persist across logins.
if (tmpl->type == CREATURE_TYPE_DEMON)
return true;
break;
case CLASS_DEATH_KNIGHT:
// Risen Ghoul + Army of the Dead. Player.cpp 14354
// and Pet.cpp 243 / 1046 / 2290 read this; without
// it the ghoul is invisible to the owner mid-load
// and ScriptedAI hooks on the ghoul mis-route.
if (tmpl->type == CREATURE_TYPE_UNDEAD)
return true;
break;
case CLASS_MAGE:
// Glyph-of-Eternal-Water permanent Water Elemental
// (entry 510, 37994). Used by Pet.cpp 1047/2292.
if (tmpl->type == CREATURE_TYPE_ELEMENTAL)
return true;
break;
case CLASS_SHAMAN:
// Fire Elemental / Earth Elemental. The base
// engine spawns these as creatures rather than
// proper Pet instances in most code paths, so the
// claim mostly matters for the Pet.cpp 1045 stat
// bootstrap when one is loaded as a SUMMON_PET.
if (tmpl->type == CREATURE_TYPE_ELEMENTAL)
return true;
break;
default:
break;
}
return std::nullopt;
}
// Warlock pet-charm context (Enslave Demon -- Unit.cpp 14828,
// 14894, 15025). Without this claim, charming a demon as a
// Paragon doesn't get the warlock-flavor charm semantics
// (faction-set-on-charm, action-bar layout, charm-break logic).
if (unitClass == CLASS_WARLOCK && context == CLASS_CONTEXT_PET_CHARM)
return true;
// ============================================================
// Equipment contexts.
// ============================================================
// CLASS_CONTEXT_EQUIP_RELIC: PlayerStorage.cpp 224-240 +
// 2475-2493. Routes Librams/Idols/Totems/Misc/Sigils into
// EQUIPMENT_SLOT_RANGED for the matching class. Claim every
// relic-bearing class so a Paragon can drop any of them into
// the ranged slot.
if (context == CLASS_CONTEXT_EQUIP_RELIC)
{
switch (unitClass)
{
case CLASS_PALADIN:
case CLASS_DRUID:
case CLASS_SHAMAN:
case CLASS_WARLOCK:
case CLASS_DEATH_KNIGHT:
return true;
default:
break;
}
}
// CLASS_CONTEXT_EQUIP_ARMOR_CLASS: PlayerStorage.cpp 2326,
// 2330, 2503-2523. At level 40 each class auto-learns its
// top armor proficiency. Paragon should pick up plate (via
// paladin/DK), shields (paladin/warrior/shaman), mail
// (hunter/shaman), and leather (rogue) so the level-40 train
// event grants Paragon full proficiency and we don't have to
// hand-curate it through the Paragon proficiency SQL.
if (context == CLASS_CONTEXT_EQUIP_ARMOR_CLASS)
{
switch (unitClass)
{
case CLASS_PALADIN:
case CLASS_WARRIOR:
case CLASS_DEATH_KNIGHT:
case CLASS_HUNTER:
case CLASS_SHAMAN:
case CLASS_DRUID:
case CLASS_ROGUE:
return true;
default:
break;
}
}
// CLASS_CONTEXT_EQUIP_SHIELDS: PlayerStorage.cpp 2467-2469.
// Lets a Paragon equip shields without a paladin/warrior/
// shaman skill gate.
if (context == CLASS_CONTEXT_EQUIP_SHIELDS)
{
switch (unitClass)
{
case CLASS_PALADIN:
case CLASS_WARRIOR:
case CLASS_SHAMAN:
return true;
default:
break;
}
}
// CLASS_CONTEXT_WEAPON_SWAP: PlayerStorage.cpp 1920, 2838 --
// rogue uses cooldown spell 6123 instead of 6119 on weapon
// swap (Quick Draw / Combat Potency interactions). Claim
// rogue so Paragon picks up the same cooldown spell.
if (context == CLASS_CONTEXT_WEAPON_SWAP && unitClass == CLASS_ROGUE)
return true;
// ============================================================
// Contexts we DELIBERATELY DO NOT claim:
// ============================================================
// CLASS_CONTEXT_STATS -- Paragon has its own STR/AGI->AP and
// INT/SPI->SP curves wired in StatSystem.cpp's CLASS_PARAGON
// branch (level*2 + STR + AGI - 20 etc.). Claiming any
// vanilla class here would override our curves with theirs.
//
// CLASS_CONTEXT_INIT, _TELEPORT, _QUEST, _TAXI, _SKILL,
// _GRAVEYARD, _CLASS_TRAINER, _TALENT_POINT_CALC -- all
// used by DK Ebon Hold / druid Moonglade starting-zone
// scripts. Paragon doesn't go through those zones and we
// don't want our players bound to Acherus or trapped in
// the DK starting quest gates.
return std::nullopt;
}
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# Fractured / AzerothCore — native VPS rolling update (git + compile).
#
# Run from anywhere; resolves the repository root from this script's location.
# Typical production layout: sources in ~/src/Fractured, install prefix in ~/azeroth-server
# (see docs/DEPLOY_LINUX_VPS.md).
#
# What this does:
# 1. git pull on the current branch (optional; can skip)
# 2. ./acore.sh compiler build — or compiler all for a full clean rebuild
#
# Database migrations from data/sql/updates/ run when you next start worldserver/authserver
# (Updates.* / SourceDirectory in *.conf). This script does not start or stop daemons unless
# you pass --run-after or set FRACTURED_POST_UPDATE_CMD.
#
# Usage:
# bash scripts/vps-update-server.sh
# bash scripts/vps-update-server.sh --full
# bash scripts/vps-update-server.sh --no-pull
# bash scripts/vps-update-server.sh --dry-run
# FRACTURED_POST_UPDATE_CMD='sudo systemctl restart fractured-world' bash scripts/vps-update-server.sh --run-after
# bash scripts/vps-update-server.sh --run-after 'sudo systemctl restart fractured-world'
#
# Environment:
# FRACTURED_GIT_REMOTE — remote name (default: origin)
# FRACTURED_POST_UPDATE_CMD — shell command run after a successful compile (if --run-after is passed without an argument, this is used)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
NO_PULL=0
FULL_BUILD=0
COMPILE_ONLY=0
DRY_RUN=0
DO_RUN_AFTER=0
POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}"
GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}"
usage() {
cat <<'EOF'
Fractured VPS update — git pull + compiler (see header in script for full notes).
Usage:
bash scripts/vps-update-server.sh [options]
Options:
--no-pull Skip git pull (only compile current tree).
--full ./acore.sh compiler all (clean + configure + compile).
--compile-only ./acore.sh compiler compile (incremental).
--dry-run Print commands without running them.
--run-after [CMD] Run shell command after successful compile. If CMD is omitted,
uses FRACTURED_POST_UPDATE_CMD from the environment.
Environment:
FRACTURED_GIT_REMOTE Git remote (default: origin).
FRACTURED_POST_UPDATE_CMD Used with bare --run-after.
EOF
}
run() {
if [[ "$DRY_RUN" -eq 1 ]]; then
printf '[dry-run] '
printf '%q ' "$@"
printf '\n'
else
"$@"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
--no-pull)
NO_PULL=1
shift
;;
--full)
FULL_BUILD=1
shift
;;
--compile-only)
COMPILE_ONLY=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--run-after)
DO_RUN_AFTER=1
shift
if [[ $# -gt 0 && "$1" != -* ]]; then
POST_UPDATE_CMD="$1"
shift
fi
;;
*)
echo "error: unknown option: $1" >&2
echo "Try: bash scripts/vps-update-server.sh --help" >&2
exit 2
;;
esac
done
if [[ "$FULL_BUILD" -eq 1 && "$COMPILE_ONLY" -eq 1 ]]; then
echo "error: use only one of --full or --compile-only" >&2
exit 2
fi
if [[ ! -d "$ROOT/.git" ]]; then
echo "error: not a git clone: $ROOT" >&2
exit 1
fi
if [[ ! -f "$ROOT/acore.sh" ]]; then
echo "error: acore.sh not found under $ROOT" >&2
exit 1
fi
if [[ ! -f "$ROOT/conf/config.sh" ]]; then
echo "error: missing $ROOT/conf/config.sh — copy conf/dist/config.sh and edit (see DEPLOY_LINUX_VPS.md)." >&2
exit 1
fi
cd "$ROOT"
if [[ "$DO_RUN_AFTER" -eq 1 && -z "${POST_UPDATE_CMD// }" ]]; then
echo "error: --run-after needs a command or FRACTURED_POST_UPDATE_CMD set in the environment." >&2
exit 2
fi
current_branch() {
git symbolic-ref -q --short HEAD || git rev-parse --short HEAD
}
if [[ "$NO_PULL" -eq 0 ]]; then
ref="$(current_branch)"
if [[ "$ref" == "HEAD" ]]; then
echo "error: detached HEAD; checkout a branch or use --no-pull." >&2
exit 1
fi
echo "==> git pull $GIT_REMOTE $ref"
run git pull "$GIT_REMOTE" "$ref"
else
echo "==> skipping git pull (--no-pull)"
fi
echo "==> ensuring acore.sh and JSONPath are executable"
if [[ "$DRY_RUN" -eq 1 ]]; then
run chmod +x acore.sh deps/jsonpath/JSONPath.sh
else
chmod +x acore.sh deps/jsonpath/JSONPath.sh 2>/dev/null || true
fi
if [[ "$FULL_BUILD" -eq 1 ]]; then
echo "==> ./acore.sh compiler all (clean, configure, compile)"
run ./acore.sh compiler all
elif [[ "$COMPILE_ONLY" -eq 1 ]]; then
echo "==> ./acore.sh compiler compile (incremental; build dir must exist)"
run ./acore.sh compiler compile
else
echo "==> ./acore.sh compiler build (configure + compile)"
run ./acore.sh compiler build
fi
if [[ "$DO_RUN_AFTER" -eq 1 ]]; then
echo "==> post-update: $POST_UPDATE_CMD"
if [[ "$DRY_RUN" -eq 1 ]]; then
printf '[dry-run] eval %q\n' "$POST_UPDATE_CMD"
else
# shellcheck disable=SC2086
eval "$POST_UPDATE_CMD"
fi
fi
echo "Done. Restart authserver/worldserver (or your service manager) when ready so new binaries and SQL updates apply."
@@ -385,6 +385,13 @@ void Player::UpdateAttackPowerAndDamage(bool ranged)
break;
}
}
else if (getClass() == CLASS_PARAGON)
{
// Fractured class 12: same hybrid curve as requested for Paragon UI
// (level*2 + AGI + STR - 20). Implemented in core so we do not rely
// on PlayerScript hooks in this hot path.
val2 = level * 2.0f + GetStat(STAT_AGILITY) + GetStat(STAT_STRENGTH) - 20.0f;
}
else
{
val2 = GetStat(STAT_AGILITY) - 10.0f;
@@ -481,6 +488,10 @@ void Player::UpdateAttackPowerAndDamage(bool ranged)
break;
}
}
else if (getClass() == CLASS_PARAGON)
{
val2 = level * 2.0f + GetStat(STAT_STRENGTH) + GetStat(STAT_AGILITY) - 20.0f;
}
else if (IsClass(CLASS_MAGE, CLASS_CONTEXT_STATS) || IsClass(CLASS_PRIEST, CLASS_CONTEXT_STATS) || IsClass(CLASS_WARLOCK, CLASS_CONTEXT_STATS))
{
val2 = GetStat(STAT_STRENGTH) - 10.0f;
+29
View File
@@ -9046,6 +9046,21 @@ int32 Unit::SpellBaseDamageBonusDone(SpellSchoolMask schoolMask)
DoneAdvertisedBenefit += ToPlayer()->GetBaseSpellPowerBonus();
DoneAdvertisedBenefit += ToPlayer()->GetBaseSpellDamageBonus();
// Fractured class 12 (Paragon) intrinsic spell power:
// SP = level*2 + INT + SPI - 20 (clamped at 0)
// Read live from current stats so character-sheet refreshes (via
// UpdateSpellDamageAndHealingBonus) and live spell casts both see the
// up-to-date value with no script hooks or m_baseSpellPower mutation.
if (ToPlayer()->getClass() == CLASS_PARAGON)
{
int32 paragonSP = int32(GetLevel()) * 2
+ int32(GetStat(STAT_INTELLECT))
+ int32(GetStat(STAT_SPIRIT))
- 20;
if (paragonSP > 0)
DoneAdvertisedBenefit += paragonSP;
}
// Damage bonus from stats
AuraEffectList const& mDamageDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT);
for (AuraEffectList::const_iterator i = mDamageDoneOfStatPercent.begin(); i != mDamageDoneOfStatPercent.end(); ++i)
@@ -9803,6 +9818,20 @@ int32 Unit::SpellBaseHealingBonusDone(SpellSchoolMask schoolMask)
AdvertisedBenefit += ToPlayer()->GetBaseSpellPowerBonus();
AdvertisedBenefit += ToPlayer()->GetBaseSpellHealingBonus();
// Fractured class 12 (Paragon) intrinsic spell power: same level*2 +
// INT + SPI - 20 floor as on the damage side (the character sheet
// shows a single Spell Power value, so both sides must add the same
// bonus).
if (ToPlayer()->getClass() == CLASS_PARAGON)
{
int32 paragonSP = int32(GetLevel()) * 2
+ int32(GetStat(STAT_INTELLECT))
+ int32(GetStat(STAT_SPIRIT))
- 20;
if (paragonSP > 0)
AdvertisedBenefit += paragonSP;
}
// Healing bonus from stats
AuraEffectList const& mHealingDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT);
for (AuraEffectList::const_iterator i = mHealingDoneOfStatPercent.begin(); i != mHealingDoneOfStatPercent.end(); ++i)
@@ -5368,6 +5368,44 @@ void SpellMgr::LoadSpellInfoCorrections()
LockEntry* key = const_cast<LockEntry*>(sLockStore.LookupEntry(36)); // 3366 Opening, allows to open without proper key
key->Type[2] = LOCK_KEY_NONE;
// Fractured: strip reagent requirements from every player-class spell at
// load time. Filtered by SpellFamilyName != 0 so that profession spells
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
// tailoring, engineering, inscription, mining, herbalism, skinning, fishing,
// first aid — all SpellFamilyName == SPELLFAMILY_GENERIC == 0) keep their
// mats and only the class abilities that asked for ankhs / candles / soul
// shards / verdant spheres / etc. cast freely. Done here in core spell
// data rather than as a runtime bypass in Spell::CheckItems / TakeReagents
// so the change is data-driven (the in-memory SpellInfo simply has no
// reagents to require). The client-side preflight is mirrored by the
// matching Spell.dbc patch shipped via patch-enUS-4.MPQ
// (fractured-tooling/_patch_spell_dbc_reagents.py).
{
uint32 fixedClassSpells = 0;
for (uint32 spellId = 1; spellId < sSpellMgr->GetSpellInfoStoreSize(); ++spellId)
{
SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId);
if (!info || info->SpellFamilyName == 0)
continue;
bool hadAny = false;
for (uint32 i = 0; i < MAX_SPELL_REAGENTS; ++i)
if (info->Reagent[i] != 0 || info->ReagentCount[i] != 0)
{ hadAny = true; break; }
if (!hadAny)
continue;
SpellInfo* mut = const_cast<SpellInfo*>(info);
for (uint32 i = 0; i < MAX_SPELL_REAGENTS; ++i)
{
mut->Reagent[i] = 0;
mut->ReagentCount[i] = 0;
}
++fixedClassSpells;
}
LOG_INFO("server.loading", ">> Fractured: cleared reagents on {} class spells", fixedClassSpells);
}
LOG_INFO("server.loading", ">> Loading spell dbc data corrections in {} ms", GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}