mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 11:33:52 +00:00
feat(editor): add WCRE (Creature Resistance & Immunity) — 106th open format
Novel replacement for the per-creature resistance columns
that vanilla WoW buried inside creature_template
(resistance1..6 fields) plus the SpellSchoolMask immunity
and mechanic_immune_mask columns. Each entry is one
creature's full defensive profile: 6 magic-school resist
values (int16, with 32767 as the full-immunity sentinel),
a physical-resistance percentage (0..75 game-engine cap),
plus three immunity bitmasks (CC kinds, spell mechanics,
magic schools).
The CC-immunity mask uses 14 named bits: ImmuneRoot /
Snare / Stun / Fear / Sleep / Silence / Charm / Disarm /
Polymorph / Banish / Knockback / Interrupt / Taunt /
Bleed. The info display renders the mask as a "+"-joined
token list ("root+stun+fear") for readability; "all" for
0xFFFF (typical raid-boss CC profile) and "none" for 0.
Three preset emitters: makeRaidBosses (5 canonical raid
bosses with iconic single-school immunities — Ragnaros
fire / Vael 50%-all / Hakkar arcane / Kel'Thuzad shadow
/ Onyxia fire+frost partial), makeElites (5 mid-tier
elites with single-school resists), makeImmunities (4
selective CC-immunity test cases — root-immune treant,
stun-immune worg, silence-immune acolyte, fear+charm+
poly-immune undead).
Validator's most novel check is creatureEntry uniqueness
— multiple WCRE entries binding the same creature would
make the damage-calc lookup ambiguous (which profile
applies?). Also catches negative resists < -100 (extreme
>2x damage taken), physicalResistPct > 75 (clamped at
runtime to game-engine armor cap), and reserved bits in
schoolImmunityMask (only bits 0-5 are meaningful).
Format count 105 -> 106. CLI flag count 1162 -> 1167.
This commit is contained in:
parent
e4e15b3ffa
commit
f9cad45154
10 changed files with 781 additions and 0 deletions
317
src/pipeline/wowee_creature_resists.cpp
Normal file
317
src/pipeline/wowee_creature_resists.cpp
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
#include "pipeline/wowee_creature_resists.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'C', 'R', 'E'};
|
||||
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) != ".wcre") {
|
||||
base += ".wcre";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) {
|
||||
return (static_cast<uint32_t>(a) << 24) |
|
||||
(static_cast<uint32_t>(b) << 16) |
|
||||
(static_cast<uint32_t>(g) << 8) |
|
||||
static_cast<uint32_t>(r);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeCreatureResists::Entry*
|
||||
WoweeCreatureResists::findById(uint32_t resistId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.resistId == resistId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const WoweeCreatureResists::Entry*
|
||||
WoweeCreatureResists::findByCreature(uint32_t creatureEntry) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.creatureEntry == creatureEntry) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WoweeCreatureResistsLoader::save(const WoweeCreatureResists& 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.resistId);
|
||||
writeStr(os, e.name);
|
||||
writeStr(os, e.description);
|
||||
writePOD(os, e.creatureEntry);
|
||||
writePOD(os, e.holyResist);
|
||||
writePOD(os, e.fireResist);
|
||||
writePOD(os, e.natureResist);
|
||||
writePOD(os, e.frostResist);
|
||||
writePOD(os, e.shadowResist);
|
||||
writePOD(os, e.arcaneResist);
|
||||
writePOD(os, e.physicalResistPct);
|
||||
writePOD(os, e.pad0);
|
||||
writePOD(os, e.ccImmunityMask);
|
||||
writePOD(os, e.mechanicImmunityMask);
|
||||
writePOD(os, e.schoolImmunityMask);
|
||||
writePOD(os, e.pad1);
|
||||
writePOD(os, e.pad2);
|
||||
writePOD(os, e.pad3);
|
||||
writePOD(os, e.iconColorRGBA);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeCreatureResists WoweeCreatureResistsLoader::load(
|
||||
const std::string& basePath) {
|
||||
WoweeCreatureResists 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.resistId)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readStr(is, e.name) || !readStr(is, e.description)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readPOD(is, e.creatureEntry) ||
|
||||
!readPOD(is, e.holyResist) ||
|
||||
!readPOD(is, e.fireResist) ||
|
||||
!readPOD(is, e.natureResist) ||
|
||||
!readPOD(is, e.frostResist) ||
|
||||
!readPOD(is, e.shadowResist) ||
|
||||
!readPOD(is, e.arcaneResist) ||
|
||||
!readPOD(is, e.physicalResistPct) ||
|
||||
!readPOD(is, e.pad0) ||
|
||||
!readPOD(is, e.ccImmunityMask) ||
|
||||
!readPOD(is, e.mechanicImmunityMask) ||
|
||||
!readPOD(is, e.schoolImmunityMask) ||
|
||||
!readPOD(is, e.pad1) ||
|
||||
!readPOD(is, e.pad2) ||
|
||||
!readPOD(is, e.pad3) ||
|
||||
!readPOD(is, e.iconColorRGBA)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeCreatureResistsLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeCreatureResists WoweeCreatureResistsLoader::makeRaidBosses(
|
||||
const std::string& catalogName) {
|
||||
using R = WoweeCreatureResists;
|
||||
WoweeCreatureResists c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint32_t entry,
|
||||
int16_t holy, int16_t fire,
|
||||
int16_t nature, int16_t frost,
|
||||
int16_t shadow, int16_t arcane,
|
||||
uint8_t physPct,
|
||||
uint16_t ccImm, uint8_t schoolImm,
|
||||
const char* desc) {
|
||||
R::Entry e;
|
||||
e.resistId = id; e.name = name; e.description = desc;
|
||||
e.creatureEntry = entry;
|
||||
e.holyResist = holy; e.fireResist = fire;
|
||||
e.natureResist = nature; e.frostResist = frost;
|
||||
e.shadowResist = shadow; e.arcaneResist = arcane;
|
||||
e.physicalResistPct = physPct;
|
||||
e.ccImmunityMask = ccImm;
|
||||
e.schoolImmunityMask = schoolImm;
|
||||
// Bosses immune to most CC by convention (server-
|
||||
// wide behavior — bosses can't be CC'd at all).
|
||||
e.mechanicImmunityMask = 0xFFFFFFFFu;
|
||||
e.iconColorRGBA = packRgba(220, 60, 60); // boss red
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
// Ragnaros — fire-school immune (school bit 0x04 in
|
||||
// typical school-bit numbering: holy=1, fire=2,
|
||||
// nature=4, frost=8, shadow=16, arcane=32). Using
|
||||
// 0x02 here for fire.
|
||||
add(1, "RagnarosFireImmune", 11502,
|
||||
0, 32767, 0, 0, 0, 0, 0,
|
||||
0xFFFF, 0x02,
|
||||
"Ragnaros (Molten Core boss) — 100% fire-school "
|
||||
"immunity (fireResist=32767 = full block) plus "
|
||||
"schoolImmunityMask bit for fire. All CC immune.");
|
||||
add(2, "VaelHalfResist", 13020,
|
||||
100, 100, 100, 100, 100, 100, 0,
|
||||
0xFFFF, 0,
|
||||
"Vaelastrasz (BWL) — 100 resist to all 6 magic "
|
||||
"schools (~50% mitigation against caster spells). "
|
||||
"All CC immune.");
|
||||
add(3, "HakkarArcaneImmune", 14834,
|
||||
0, 0, 0, 0, 0, 32767, 0,
|
||||
0xFFFF, 0x20,
|
||||
"Hakkar (ZG) — full arcane immunity. All CC "
|
||||
"immune. Other schools at default zero resist.");
|
||||
add(4, "KelthuzadShadowImmune", 15990,
|
||||
0, 0, 0, 200, 32767, 0, 0,
|
||||
0xFFFF, 0x10,
|
||||
"Kel'Thuzad (Naxx) — full shadow-school "
|
||||
"immunity, plus 200 frost resist (~50% frost "
|
||||
"mitigation). Iconic anti-warlock fight.");
|
||||
add(5, "OnyxiaPartialImmune", 10184,
|
||||
0, 100, 0, 100, 0, 0, 0,
|
||||
0xFFFF, 0,
|
||||
"Onyxia — 100 fire + 100 frost resist (50% "
|
||||
"mitigation against both schools). All CC "
|
||||
"immune.");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeCreatureResists WoweeCreatureResistsLoader::makeElites(
|
||||
const std::string& catalogName) {
|
||||
using R = WoweeCreatureResists;
|
||||
WoweeCreatureResists c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint32_t entry,
|
||||
int16_t holy, int16_t fire,
|
||||
int16_t nature, int16_t frost,
|
||||
int16_t shadow, int16_t arcane,
|
||||
const char* desc) {
|
||||
R::Entry e;
|
||||
e.resistId = id; e.name = name; e.description = desc;
|
||||
e.creatureEntry = entry;
|
||||
e.holyResist = holy; e.fireResist = fire;
|
||||
e.natureResist = nature; e.frostResist = frost;
|
||||
e.shadowResist = shadow; e.arcaneResist = arcane;
|
||||
e.physicalResistPct = 0;
|
||||
// Elites can be CC'd but not all — 80%
|
||||
// mechanicImmune to common debuffs.
|
||||
e.ccImmunityMask = R::ImmuneFear | R::ImmuneSleep;
|
||||
e.mechanicImmunityMask = 0;
|
||||
e.schoolImmunityMask = 0;
|
||||
e.iconColorRGBA = packRgba(200, 140, 60); // elite gold
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
add(100, "WaterElementalFireResist", 12471,
|
||||
0, 60, 0, 0, 0, 0,
|
||||
"Water Elemental (Bog of Sorrows) — 60 fire "
|
||||
"resist (~30% mitigation). Frost-themed mob "
|
||||
"resists fire by lore.");
|
||||
add(101, "StoneGiantNatureResist", 12476,
|
||||
0, 0, 100, 0, 0, 0,
|
||||
"Stone Giant (Stonetalon) — 100 nature resist "
|
||||
"(50% mitigation). Earth-element mob resists "
|
||||
"druid spells.");
|
||||
add(102, "ScarletPriestHolyResist", 11030,
|
||||
80, 0, 0, 0, 60, 0,
|
||||
"Scarlet Crusade Priest (Scarlet Monastery) — "
|
||||
"80 holy + 60 shadow resist. Light/dark-school "
|
||||
"trained.");
|
||||
add(103, "DustwindStormcasterArcane", 8519,
|
||||
0, 0, 0, 0, 0, 80,
|
||||
"Dustwind Stormcaster (Silithus) — 80 arcane "
|
||||
"resist (40% mitigation). Caster mob favors "
|
||||
"arcane defense.");
|
||||
add(104, "FrostwolfEntinelFrost", 10920,
|
||||
0, 0, 0, 60, 0, 0,
|
||||
"Frostwolf Sentinel (Alterac Valley) — 60 frost "
|
||||
"resist (30% mitigation). Northern enemy "
|
||||
"acclimated to cold.");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeCreatureResists WoweeCreatureResistsLoader::makeImmunities(
|
||||
const std::string& catalogName) {
|
||||
using R = WoweeCreatureResists;
|
||||
WoweeCreatureResists c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint32_t entry, uint16_t ccMask,
|
||||
uint32_t mechMask, const char* desc) {
|
||||
R::Entry e;
|
||||
e.resistId = id; e.name = name; e.description = desc;
|
||||
e.creatureEntry = entry;
|
||||
e.ccImmunityMask = ccMask;
|
||||
e.mechanicImmunityMask = mechMask;
|
||||
e.schoolImmunityMask = 0;
|
||||
e.iconColorRGBA = packRgba(140, 100, 240); // immune purple
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
add(200, "RootImmuneTreant", 12477,
|
||||
R::ImmuneRoot | R::ImmuneSnare, 0,
|
||||
"Treant (Felwood) — root + snare immune. "
|
||||
"Cannot be Entangling Roots or Frost Trap'd.");
|
||||
add(201, "StunImmuneAlphaWolf", 14283,
|
||||
R::ImmuneStun, 0,
|
||||
"Alpha Worg (Silverpine) — stun immune. "
|
||||
"Cannot be Cheap Shot'd or Concussion Blow'd.");
|
||||
add(202, "SilenceImmuneCaster", 11839,
|
||||
R::ImmuneSilence | R::ImmuneInterrupt, 0,
|
||||
"Cult of the Damned Acolyte — silence + "
|
||||
"interrupt immune. Spell pushback works but "
|
||||
"the cast itself can't be interrupted.");
|
||||
add(203, "FearImmuneUndead", 18525,
|
||||
R::ImmuneFear | R::ImmuneCharm | R::ImmunePolymorph,
|
||||
0,
|
||||
"Risen Construct (Naxxramas) — fear + charm + "
|
||||
"polymorph immune. Mind-affecting CC has no "
|
||||
"effect; physical CC like stun/root still works.");
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue