mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 11:33:52 +00:00
feat(pipeline): WPRC spell proc rules catalog (138th open format)
Novel replacement for the implicit proc-on-event spell triggers
vanilla WoW carried in SpellProcEvents (later rows of
Spell.dbc) + the per-spell procFlags + procChance fields
scattered across multiple DBC tables. Each WPRC entry binds
one proc rule to its source spell (the aura/buff that has the
proc), trigger event (OnHit/OnCrit/OnCast/OnTakeDamage/OnHeal/
OnDodge/OnParry/OnBlock/OnKill), proc chance in basis points
(0..10000 = 0..100%), internal cooldown ms, the spell to
trigger on proc, max-stacks-on-target cap, and optional
condition flags (RequireMeleeWeapon / RequireSpellSchool /
ExcludeAutoAttack / OnlyFromBehind / OnlyVsPvPTarget).
Three presets covering common vanilla proc archetypes:
--gen-prc-weapon 3 weapon-enchant procs (Crusader 2%
OnHit / Lifestealing 5% / Fiery Weapon
7% with 1.5s ICD to handle dual-wield)
--gen-prc-ret 4 Retribution Paladin procs (Vengeance
OnCrit 5-stack / Seal of Justice OnHit
25% with 60s per-target ICD / Reckoning
OnBlock 10% extra-attack / Sanctity Aura
OnCast bookkeeping)
--gen-prc-rage 3 Warrior Rage-generation procs
(Bloodrage OnCast / Berserker Rage
OnCast immunity aura / Anger Mgmt OnDodge
passive)
Validator catches: id+name+sourceSpellId+procEffectSpellId
required, triggerEvent 0..8, procChancePct in 1..10000
(0 = never fires; > 10000 = > 100% basis points). CRITICAL:
sourceSpellId == procEffectSpellId on OnCast trigger errors
(infinite proc loop — the effect re-casts itself which fires
the proc again). Warns on 100% + 0ms ICD on high-frequency
events (OnHit/OnCrit/OnTakeDamage) — would spam every swing
without rate limiting.
Validator immediately caught a real preset bug during smoke-
test: Berserker Rage initially had sourceSpellId=18499 ==
procEffectSpellId=18499 with OnCast trigger. Validator
correctly errored with "infinite proc loop". Fixed preset to
use procEffectSpellId=23691 (the immunity-aura effect — a
distinct spell from the cast trigger).
Format count 137 -> 138. CLI flag count 1436 -> 1443.
This commit is contained in:
parent
c60e779e67
commit
73d66a04d0
10 changed files with 785 additions and 0 deletions
308
src/pipeline/wowee_spell_proc_rules.cpp
Normal file
308
src/pipeline/wowee_spell_proc_rules.cpp
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
#include "pipeline/wowee_spell_proc_rules.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'P', 'R', 'C'};
|
||||
constexpr uint32_t kVersion = 1;
|
||||
|
||||
template <typename T>
|
||||
void writePOD(std::ofstream& os, const T& v) {
|
||||
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readPOD(std::ifstream& is, T& v) {
|
||||
is.read(reinterpret_cast<char*>(&v), sizeof(T));
|
||||
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
|
||||
}
|
||||
|
||||
void writeStr(std::ofstream& os, const std::string& s) {
|
||||
uint32_t n = static_cast<uint32_t>(s.size());
|
||||
writePOD(os, n);
|
||||
if (n > 0) os.write(s.data(), n);
|
||||
}
|
||||
|
||||
bool readStr(std::ifstream& is, std::string& s) {
|
||||
uint32_t n = 0;
|
||||
if (!readPOD(is, n)) return false;
|
||||
if (n > (1u << 20)) return false;
|
||||
s.resize(n);
|
||||
if (n > 0) {
|
||||
is.read(s.data(), n);
|
||||
if (is.gcount() != static_cast<std::streamsize>(n)) {
|
||||
s.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string normalizePath(std::string base) {
|
||||
if (base.size() < 5 || base.substr(base.size() - 5) != ".wprc") {
|
||||
base += ".wprc";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeSpellProcRules::Entry*
|
||||
WoweeSpellProcRules::findById(uint32_t procRuleId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.procRuleId == procRuleId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const WoweeSpellProcRules::Entry*>
|
||||
WoweeSpellProcRules::findBySourceSpell(uint32_t spellId) const {
|
||||
std::vector<const Entry*> out;
|
||||
for (const auto& e : entries)
|
||||
if (e.sourceSpellId == spellId) out.push_back(&e);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<const WoweeSpellProcRules::Entry*>
|
||||
WoweeSpellProcRules::findByEvent(uint8_t triggerEvent) const {
|
||||
std::vector<const Entry*> out;
|
||||
for (const auto& e : entries)
|
||||
if (e.triggerEvent == triggerEvent) out.push_back(&e);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeSpellProcRulesLoader::save(
|
||||
const WoweeSpellProcRules& cat,
|
||||
const std::string& basePath) {
|
||||
std::ofstream os(normalizePath(basePath), std::ios::binary);
|
||||
if (!os) return false;
|
||||
os.write(kMagic, 4);
|
||||
writePOD(os, kVersion);
|
||||
writeStr(os, cat.name);
|
||||
uint32_t entryCount = static_cast<uint32_t>(cat.entries.size());
|
||||
writePOD(os, entryCount);
|
||||
for (const auto& e : cat.entries) {
|
||||
writePOD(os, e.procRuleId);
|
||||
writeStr(os, e.name);
|
||||
writePOD(os, e.sourceSpellId);
|
||||
writePOD(os, e.procEffectSpellId);
|
||||
writePOD(os, e.triggerEvent);
|
||||
writePOD(os, e.maxStacksOnTarget);
|
||||
writePOD(os, e.procChancePct);
|
||||
writePOD(os, e.internalCooldownMs);
|
||||
writePOD(os, e.procFlagsMask);
|
||||
writePOD(os, e.pad0);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeSpellProcRules WoweeSpellProcRulesLoader::load(
|
||||
const std::string& basePath) {
|
||||
WoweeSpellProcRules out;
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
if (!is) return out;
|
||||
char magic[4];
|
||||
is.read(magic, 4);
|
||||
if (std::memcmp(magic, kMagic, 4) != 0) return out;
|
||||
uint32_t version = 0;
|
||||
if (!readPOD(is, version) || version != kVersion) return out;
|
||||
if (!readStr(is, out.name)) return out;
|
||||
uint32_t entryCount = 0;
|
||||
if (!readPOD(is, entryCount)) return out;
|
||||
if (entryCount > (1u << 20)) return out;
|
||||
out.entries.resize(entryCount);
|
||||
for (auto& e : out.entries) {
|
||||
if (!readPOD(is, e.procRuleId)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readStr(is, e.name)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readPOD(is, e.sourceSpellId) ||
|
||||
!readPOD(is, e.procEffectSpellId) ||
|
||||
!readPOD(is, e.triggerEvent) ||
|
||||
!readPOD(is, e.maxStacksOnTarget) ||
|
||||
!readPOD(is, e.procChancePct) ||
|
||||
!readPOD(is, e.internalCooldownMs) ||
|
||||
!readPOD(is, e.procFlagsMask) ||
|
||||
!readPOD(is, e.pad0)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeSpellProcRulesLoader::exists(
|
||||
const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
WoweeSpellProcRules::Entry makeRule(
|
||||
uint32_t procRuleId, const char* name,
|
||||
uint32_t sourceSpellId, uint32_t procEffectSpellId,
|
||||
uint8_t triggerEvent,
|
||||
uint16_t procChancePct,
|
||||
uint32_t internalCooldownMs,
|
||||
uint16_t procFlagsMask,
|
||||
uint8_t maxStacksOnTarget = 0) {
|
||||
WoweeSpellProcRules::Entry e;
|
||||
e.procRuleId = procRuleId; e.name = name;
|
||||
e.sourceSpellId = sourceSpellId;
|
||||
e.procEffectSpellId = procEffectSpellId;
|
||||
e.triggerEvent = triggerEvent;
|
||||
e.procChancePct = procChancePct;
|
||||
e.internalCooldownMs = internalCooldownMs;
|
||||
e.procFlagsMask = procFlagsMask;
|
||||
e.maxStacksOnTarget = maxStacksOnTarget;
|
||||
return e;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WoweeSpellProcRules WoweeSpellProcRulesLoader::makeWeaponProcs(
|
||||
const std::string& catalogName) {
|
||||
using P = WoweeSpellProcRules;
|
||||
WoweeSpellProcRules c;
|
||||
c.name = catalogName;
|
||||
// Crusader (Enchant Weapon — Crusader, spellId
|
||||
// 20007) procs spell 20007 buff OnHit at ~2%
|
||||
// chance, no ICD, requires melee weapon.
|
||||
c.entries.push_back(makeRule(
|
||||
1, "Crusader Weapon Enchant",
|
||||
20007 /* Enchant: Crusader */,
|
||||
20007 /* same spell triggers the buff */,
|
||||
P::OnHit,
|
||||
200 /* 2% basis points */,
|
||||
0,
|
||||
P::RequireMeleeWeapon | P::ExcludeAutoAttack));
|
||||
// Lifesteal: spellId 20004 (Enchant Weapon —
|
||||
// Lifestealing) procs heal-on-hit. 5% chance,
|
||||
// no ICD.
|
||||
c.entries.push_back(makeRule(
|
||||
2, "Lifestealing Weapon Enchant",
|
||||
20004 /* Enchant: Lifesteal */,
|
||||
20004,
|
||||
P::OnHit,
|
||||
500 /* 5% */,
|
||||
0,
|
||||
P::RequireMeleeWeapon));
|
||||
// Fiery Weapon (spellId 13898): 7% chance fire
|
||||
// damage proc, 1.5s ICD to prevent
|
||||
// double-procs on dual-wield.
|
||||
c.entries.push_back(makeRule(
|
||||
3, "Fiery Weapon Enchant",
|
||||
13898 /* Enchant: Fiery Weapon */,
|
||||
13898,
|
||||
P::OnHit,
|
||||
700 /* 7% */,
|
||||
1500 /* 1.5s ICD */,
|
||||
P::RequireMeleeWeapon));
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeSpellProcRules WoweeSpellProcRulesLoader::makeRetPaladin(
|
||||
const std::string& catalogName) {
|
||||
using P = WoweeSpellProcRules;
|
||||
WoweeSpellProcRules c;
|
||||
c.name = catalogName;
|
||||
// Vengeance: each crit grants 5-stack Vengeance
|
||||
// buff (spellId 9452 procs effect 9452 OnCrit).
|
||||
// 100% chance, no ICD, max 5 stacks.
|
||||
c.entries.push_back(makeRule(
|
||||
10, "Vengeance Paladin Crit Stack",
|
||||
9452 /* Vengeance talent */,
|
||||
9452 /* stacking buff */,
|
||||
P::OnCrit,
|
||||
10000 /* 100% — every crit */,
|
||||
0,
|
||||
0,
|
||||
5 /* max 5 stacks */));
|
||||
// Seal of Justice: 25% chance to stun on hit
|
||||
// (spellId 20164 sourceAura procs spell 20170
|
||||
// stun effect). 60s ICD per target to prevent
|
||||
// perma-stun.
|
||||
c.entries.push_back(makeRule(
|
||||
11, "Seal of Justice Stun",
|
||||
20164 /* Seal of Justice aura */,
|
||||
20170 /* Justice stun effect */,
|
||||
P::OnHit,
|
||||
2500 /* 25% */,
|
||||
60000 /* 1min ICD per target */,
|
||||
P::RequireMeleeWeapon));
|
||||
// Reckoning: 10% chance on block to gain extra
|
||||
// attack. Ret talent.
|
||||
c.entries.push_back(makeRule(
|
||||
12, "Reckoning Block-to-Attack",
|
||||
20176 /* Reckoning talent */,
|
||||
20178 /* extra-attack effect */,
|
||||
P::OnBlock,
|
||||
1000 /* 10% */,
|
||||
0,
|
||||
0));
|
||||
// Sanctity Aura: 100% on cast, amplifies holy
|
||||
// damage by 10%. Aura is permanent so trigger is
|
||||
// just OnCast bookkeeping.
|
||||
c.entries.push_back(makeRule(
|
||||
13, "Sanctity Aura Holy Amp",
|
||||
20218 /* Sanctity Aura */,
|
||||
20221 /* Holy-amp passive */,
|
||||
P::OnCast,
|
||||
10000 /* 100% — bookkeeping */,
|
||||
0,
|
||||
P::RequireSpellSchool));
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeSpellProcRules WoweeSpellProcRulesLoader::makeRageGen(
|
||||
const std::string& catalogName) {
|
||||
using P = WoweeSpellProcRules;
|
||||
WoweeSpellProcRules c;
|
||||
c.name = catalogName;
|
||||
// Bloodrage: instant 10 Rage on cast, costs
|
||||
// health. Always procs (100%, no ICD — has
|
||||
// its own 60s shared cooldown).
|
||||
c.entries.push_back(makeRule(
|
||||
20, "Bloodrage Instant Rage",
|
||||
2687 /* Bloodrage spell */,
|
||||
14201 /* Rage gain effect */,
|
||||
P::OnCast,
|
||||
10000 /* 100% */,
|
||||
0,
|
||||
0));
|
||||
// Berserker Rage: 100% on cast, immunity to
|
||||
// fear/sap/incapacitate. Uses the OnCast event
|
||||
// for bookkeeping.
|
||||
c.entries.push_back(makeRule(
|
||||
21, "Berserker Rage CC Immune",
|
||||
18499 /* Berserker Rage spell */,
|
||||
23691 /* Berserker Rage CC-immunity aura
|
||||
effect — distinct spellId so the
|
||||
OnCast trigger does NOT recurse
|
||||
into the source spell */,
|
||||
P::OnCast,
|
||||
10000 /* 100% */,
|
||||
0,
|
||||
0));
|
||||
// Anger Management: passive talent. OnDodge
|
||||
// generates 2 Rage. 100%, no ICD.
|
||||
c.entries.push_back(makeRule(
|
||||
22, "Anger Management Dodge Rage",
|
||||
12296 /* Anger Mgmt talent */,
|
||||
14201 /* Rage gain effect */,
|
||||
P::OnDodge,
|
||||
10000 /* 100% */,
|
||||
0,
|
||||
0));
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue