mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 11:33:52 +00:00
feat(pipeline): WCST combat stats baseline catalog (130th open format)
Novel replacement for the per-class per-level base-stat scaling
table that vanilla WoW scattered across CharBaseInfo.dbc +
CharStartOutfit.dbc + GtChanceTo*.dbc + the hard-coded HP/mana-
per-level constants in the server's StatSystem. Each WCST entry
binds one (classId, level) pair to base health, mana, armor, and
the five primary stats (Str/Agi/Sta/Int/Spi).
Sparse design: presets emit ~6 sample levels per class with the
runtime stat-interpolator computing intermediate levels.
Three presets:
--gen-cst-warrior Warrior (classId=1) sparse sample at L1/
10/20/30/40/60. baseMana=0 across all
entries (Warrior uses Rage)
--gen-cst-mage Mage (classId=8) same 6 levels with mana
growth tracking Intellect
--gen-cst-starting All 9 vanilla classes at level 1 — shows
per-class flat starting differences
(Warrior/Paladin high Str; Hunter/Rogue
high Agi; Mage/Priest/Warlock high Int;
Shaman/Druid balanced)
Validator catches: id+classId+level required, classId 1..11,
level 1..60, zero baseHealth (player would die instantly),
duplicate statIds, duplicate (classId,level) pairs (runtime
stat-lookup tie). Warns on classId 6/10 (DK/Monk gap unused
in vanilla), Warrior/Rogue baseMana > 0 (these classes use
Rage/Energy not mana), and per-class monotonicity violations
across all 8 stats — sorts by level, walks adjacent pairs,
flags any stat that regresses as level increases (typo guard).
Format count 129 -> 130. CLI flag count 1364 -> 1371.
This commit is contained in:
parent
f41f913a2a
commit
b66e41df87
10 changed files with 722 additions and 0 deletions
|
|
@ -718,6 +718,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_player_movement_anim.cpp
|
||||
src/pipeline/wowee_transit_schedule.cpp
|
||||
src/pipeline/wowee_mage_portals.cpp
|
||||
src/pipeline/wowee_combat_stats.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1599,6 +1600,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_player_movement_anim_catalog.cpp
|
||||
tools/editor/cli_transit_schedule_catalog.cpp
|
||||
tools/editor/cli_mage_portals_catalog.cpp
|
||||
tools/editor/cli_combat_stats_catalog.cpp
|
||||
tools/editor/cli_catalog_pluck.cpp
|
||||
tools/editor/cli_catalog_find.cpp
|
||||
tools/editor/cli_catalog_by_name.cpp
|
||||
|
|
@ -1799,6 +1801,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_player_movement_anim.cpp
|
||||
src/pipeline/wowee_transit_schedule.cpp
|
||||
src/pipeline/wowee_mage_portals.cpp
|
||||
src/pipeline/wowee_combat_stats.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
119
include/pipeline/wowee_combat_stats.hpp
Normal file
119
include/pipeline/wowee_combat_stats.hpp
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Combat Stats Baseline catalog (.wcst)
|
||||
// — novel replacement for the per-class per-level
|
||||
// base-stat scaling table that vanilla WoW
|
||||
// scattered across CharBaseInfo.dbc +
|
||||
// CharStartOutfit.dbc + GtChanceTo*.dbc + the
|
||||
// hard-coded HP/mana-per-level constants in the
|
||||
// server's StatSystem. Each WCST entry binds one
|
||||
// (classId, level) pair to its base health, mana,
|
||||
// armor, and the five primary stats (Str/Agi/Sta/
|
||||
// Int/Spi). Entries are sparse — typical preset
|
||||
// emits ~6 sample levels per class, with the
|
||||
// runtime interpolating between them for
|
||||
// intermediate levels.
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WCDB: classId references the playable-class
|
||||
// catalog (1..11 in vanilla, with 6 + 10
|
||||
// unused).
|
||||
// WSPK: the spell pack catalog gates spellbook
|
||||
// tabs by classId — same id space.
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WCST"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// statId (uint32) — surrogate primary
|
||||
// key
|
||||
// classId (uint8) — 1..11 vanilla class
|
||||
// level (uint8) — 1..60 vanilla cap
|
||||
// pad0 (uint16)
|
||||
// baseHealth (uint32)
|
||||
// baseMana (uint32) — 0 if class doesn't
|
||||
// use mana (Warrior,
|
||||
// Rogue)
|
||||
// baseStrength (uint16)
|
||||
// baseAgility (uint16)
|
||||
// baseStamina (uint16)
|
||||
// baseIntellect (uint16)
|
||||
// baseSpirit (uint16)
|
||||
// pad1 (uint16)
|
||||
// baseArmor (uint32)
|
||||
struct WoweeCombatStats {
|
||||
struct Entry {
|
||||
uint32_t statId = 0;
|
||||
uint8_t classId = 0;
|
||||
uint8_t level = 0;
|
||||
uint16_t pad0 = 0;
|
||||
uint32_t baseHealth = 0;
|
||||
uint32_t baseMana = 0;
|
||||
uint16_t baseStrength = 0;
|
||||
uint16_t baseAgility = 0;
|
||||
uint16_t baseStamina = 0;
|
||||
uint16_t baseIntellect = 0;
|
||||
uint16_t baseSpirit = 0;
|
||||
uint16_t pad1 = 0;
|
||||
uint32_t baseArmor = 0;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
const Entry* findById(uint32_t statId) const;
|
||||
|
||||
// Returns the binding for an exact (classId, level)
|
||||
// pair — used by the level-up handler to commit
|
||||
// base-stat changes when a player dings.
|
||||
const Entry* find(uint8_t classId, uint8_t level) const;
|
||||
|
||||
// Returns all entries for a class, sorted by level.
|
||||
// Used by the runtime stat-interpolator to find the
|
||||
// bracketing two entries for an intermediate level.
|
||||
std::vector<const Entry*> findByClass(uint8_t classId) const;
|
||||
};
|
||||
|
||||
class WoweeCombatStatsLoader {
|
||||
public:
|
||||
static bool save(const WoweeCombatStats& cat,
|
||||
const std::string& basePath);
|
||||
static WoweeCombatStats load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-cst* variants.
|
||||
//
|
||||
// makeWarriorStats — Warrior (classId=1) sparse
|
||||
// sample at levels 1, 10, 20,
|
||||
// 30, 40, 60. baseMana=0 for
|
||||
// all entries (Warrior uses
|
||||
// Rage, not mana).
|
||||
// makeMageStats — Mage (classId=8) sparse
|
||||
// sample at the same 6
|
||||
// levels. baseMana grows
|
||||
// with Intellect.
|
||||
// makeStartingLevels — All 9 vanilla classes
|
||||
// (Warrior/Paladin/Hunter/
|
||||
// Rogue/Priest/Shaman/Mage/
|
||||
// Warlock/Druid) at level 1
|
||||
// only — illustrates per-
|
||||
// class flat starting-stat
|
||||
// differences.
|
||||
static WoweeCombatStats makeWarriorStats(const std::string& catalogName);
|
||||
static WoweeCombatStats makeMageStats(const std::string& catalogName);
|
||||
static WoweeCombatStats makeStartingLevels(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
246
src/pipeline/wowee_combat_stats.cpp
Normal file
246
src/pipeline/wowee_combat_stats.cpp
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
#include "pipeline/wowee_combat_stats.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'C', 'S', 'T'};
|
||||
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) != ".wcst") {
|
||||
base += ".wcst";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeCombatStats::Entry*
|
||||
WoweeCombatStats::findById(uint32_t statId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.statId == statId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const WoweeCombatStats::Entry*
|
||||
WoweeCombatStats::find(uint8_t classId, uint8_t level) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.classId == classId && e.level == level)
|
||||
return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const WoweeCombatStats::Entry*>
|
||||
WoweeCombatStats::findByClass(uint8_t classId) const {
|
||||
std::vector<const Entry*> out;
|
||||
for (const auto& e : entries)
|
||||
if (e.classId == classId) out.push_back(&e);
|
||||
std::sort(out.begin(), out.end(),
|
||||
[](const Entry* a, const Entry* b) {
|
||||
return a->level < b->level;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeCombatStatsLoader::save(const WoweeCombatStats& 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.statId);
|
||||
writePOD(os, e.classId);
|
||||
writePOD(os, e.level);
|
||||
writePOD(os, e.pad0);
|
||||
writePOD(os, e.baseHealth);
|
||||
writePOD(os, e.baseMana);
|
||||
writePOD(os, e.baseStrength);
|
||||
writePOD(os, e.baseAgility);
|
||||
writePOD(os, e.baseStamina);
|
||||
writePOD(os, e.baseIntellect);
|
||||
writePOD(os, e.baseSpirit);
|
||||
writePOD(os, e.pad1);
|
||||
writePOD(os, e.baseArmor);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeCombatStats WoweeCombatStatsLoader::load(
|
||||
const std::string& basePath) {
|
||||
WoweeCombatStats 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.statId) ||
|
||||
!readPOD(is, e.classId) ||
|
||||
!readPOD(is, e.level) ||
|
||||
!readPOD(is, e.pad0) ||
|
||||
!readPOD(is, e.baseHealth) ||
|
||||
!readPOD(is, e.baseMana) ||
|
||||
!readPOD(is, e.baseStrength) ||
|
||||
!readPOD(is, e.baseAgility) ||
|
||||
!readPOD(is, e.baseStamina) ||
|
||||
!readPOD(is, e.baseIntellect) ||
|
||||
!readPOD(is, e.baseSpirit) ||
|
||||
!readPOD(is, e.pad1) ||
|
||||
!readPOD(is, e.baseArmor)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeCombatStatsLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct StatRow {
|
||||
uint32_t statId;
|
||||
uint8_t classId;
|
||||
uint8_t level;
|
||||
uint32_t hp;
|
||||
uint32_t mana;
|
||||
uint16_t str;
|
||||
uint16_t agi;
|
||||
uint16_t sta;
|
||||
uint16_t intel;
|
||||
uint16_t spi;
|
||||
uint32_t armor;
|
||||
};
|
||||
|
||||
WoweeCombatStats fromRows(const std::string& catalogName,
|
||||
const std::vector<StatRow>& rows) {
|
||||
WoweeCombatStats c;
|
||||
c.name = catalogName;
|
||||
for (const auto& r : rows) {
|
||||
WoweeCombatStats::Entry e;
|
||||
e.statId = r.statId;
|
||||
e.classId = r.classId;
|
||||
e.level = r.level;
|
||||
e.baseHealth = r.hp;
|
||||
e.baseMana = r.mana;
|
||||
e.baseStrength = r.str;
|
||||
e.baseAgility = r.agi;
|
||||
e.baseStamina = r.sta;
|
||||
e.baseIntellect = r.intel;
|
||||
e.baseSpirit = r.spi;
|
||||
e.baseArmor = r.armor;
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WoweeCombatStats WoweeCombatStatsLoader::makeWarriorStats(
|
||||
const std::string& catalogName) {
|
||||
// Warrior (classId=1) sparse sample. Numbers
|
||||
// approximate vanilla 1.12 base stats — Warrior
|
||||
// uses Rage so baseMana=0 across all levels.
|
||||
// Stats grow steadily; armor scales with Agility.
|
||||
return fromRows(catalogName, {
|
||||
{101, 1, 1, 60, 0, 23, 20, 23, 20, 20, 60},
|
||||
{102, 1, 10, 180, 0, 30, 26, 31, 22, 22, 130},
|
||||
{103, 1, 20, 400, 0, 40, 34, 42, 24, 24, 220},
|
||||
{104, 1, 30, 720, 0, 52, 44, 56, 26, 26, 330},
|
||||
{105, 1, 40, 1140, 0, 66, 56, 72, 28, 28, 460},
|
||||
{106, 1, 60, 2200, 0, 95, 80,105, 32, 32, 760},
|
||||
});
|
||||
}
|
||||
|
||||
WoweeCombatStats WoweeCombatStatsLoader::makeMageStats(
|
||||
const std::string& catalogName) {
|
||||
// Mage (classId=8) sparse sample. baseMana grows
|
||||
// with Intellect — Mage is the canonical mana-
|
||||
// user. Lower base HP, higher Int/Spi than
|
||||
// warrior at every level.
|
||||
return fromRows(catalogName, {
|
||||
{801, 8, 1, 50, 100, 20, 20, 20, 23, 23, 40},
|
||||
{802, 8, 10, 140, 340, 22, 22, 24, 32, 30, 90},
|
||||
{803, 8, 20, 320, 720, 25, 25, 30, 44, 40, 160},
|
||||
{804, 8, 30, 580, 1180, 28, 28, 38, 58, 52, 240},
|
||||
{805, 8, 40, 920, 1740, 32, 32, 48, 74, 66, 340},
|
||||
{806, 8, 60, 1780, 3120, 40, 40, 70,108, 95, 580},
|
||||
});
|
||||
}
|
||||
|
||||
WoweeCombatStats WoweeCombatStatsLoader::makeStartingLevels(
|
||||
const std::string& catalogName) {
|
||||
// All 9 vanilla classes at level 1. classId 6
|
||||
// (Death Knight) and 10 (Monk) are unused in
|
||||
// vanilla — skipped. Numbers reflect the per-
|
||||
// class racial-base-stat skew (Warrior/Paladin
|
||||
// high Str, Hunter/Rogue high Agi, Mage/Priest/
|
||||
// Warlock high Int, Shaman/Druid balanced).
|
||||
return fromRows(catalogName, {
|
||||
// statId class lvl hp mana str agi sta int spi armor
|
||||
{1001, 1, 1, 60, 0, 23, 20, 23, 20, 20, 60},
|
||||
{1002, 2, 1, 60, 100, 22, 20, 22, 20, 21, 60}, // Paladin
|
||||
{1003, 3, 1, 50, 0, 21, 23, 20, 20, 21, 50}, // Hunter
|
||||
{1004, 4, 1, 55, 0, 20, 23, 21, 20, 20, 55}, // Rogue
|
||||
{1005, 5, 1, 50, 120, 20, 20, 20, 22, 24, 40}, // Priest
|
||||
{1007, 7, 1, 55, 100, 22, 21, 22, 21, 22, 50}, // Shaman
|
||||
{1008, 8, 1, 50, 100, 20, 20, 20, 23, 23, 40}, // Mage
|
||||
{1009, 9, 1, 50, 100, 20, 20, 21, 23, 22, 40}, // Warlock
|
||||
{1011, 11, 1, 55, 100, 21, 21, 22, 22, 22, 50}, // Druid
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -398,6 +398,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-prt-alliance", "--gen-prt-horde", "--gen-prt-teleports",
|
||||
"--info-wprt", "--validate-wprt",
|
||||
"--export-wprt-json", "--import-wprt-json",
|
||||
"--gen-cst-warrior", "--gen-cst-mage", "--gen-cst-starting",
|
||||
"--info-wcst", "--validate-wcst",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
326
tools/editor/cli_combat_stats_catalog.cpp
Normal file
326
tools/editor/cli_combat_stats_catalog.cpp
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
#include "cli_combat_stats_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_combat_stats.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string stripWcstExt(std::string base) {
|
||||
stripExt(base, ".wcst");
|
||||
return base;
|
||||
}
|
||||
|
||||
const char* classIdName(uint8_t c) {
|
||||
switch (c) {
|
||||
case 1: return "Warrior";
|
||||
case 2: return "Paladin";
|
||||
case 3: return "Hunter";
|
||||
case 4: return "Rogue";
|
||||
case 5: return "Priest";
|
||||
case 7: return "Shaman";
|
||||
case 8: return "Mage";
|
||||
case 9: return "Warlock";
|
||||
case 11: return "Druid";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweeCombatStats& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweeCombatStatsLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wcst\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweeCombatStats& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wcst\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" entries : %zu\n", c.entries.size());
|
||||
}
|
||||
|
||||
int handleGenWarrior(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "WarriorBaseStats";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWcstExt(base);
|
||||
auto c = wowee::pipeline::WoweeCombatStatsLoader::
|
||||
makeWarriorStats(name);
|
||||
if (!saveOrError(c, base, "gen-cst-warrior")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenMage(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "MageBaseStats";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWcstExt(base);
|
||||
auto c = wowee::pipeline::WoweeCombatStatsLoader::
|
||||
makeMageStats(name);
|
||||
if (!saveOrError(c, base, "gen-cst-mage")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenStarting(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "StartingLevelStats";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWcstExt(base);
|
||||
auto c = wowee::pipeline::WoweeCombatStatsLoader::
|
||||
makeStartingLevels(name);
|
||||
if (!saveOrError(c, base, "gen-cst-starting")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleInfo(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWcstExt(base);
|
||||
if (!wowee::pipeline::WoweeCombatStatsLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WCST not found: %s.wcst\n",
|
||||
base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeCombatStatsLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wcst"] = base + ".wcst";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
arr.push_back({
|
||||
{"statId", e.statId},
|
||||
{"classId", e.classId},
|
||||
{"className", classIdName(e.classId)},
|
||||
{"level", e.level},
|
||||
{"baseHealth", e.baseHealth},
|
||||
{"baseMana", e.baseMana},
|
||||
{"baseStrength", e.baseStrength},
|
||||
{"baseAgility", e.baseAgility},
|
||||
{"baseStamina", e.baseStamina},
|
||||
{"baseIntellect", e.baseIntellect},
|
||||
{"baseSpirit", e.baseSpirit},
|
||||
{"baseArmor", e.baseArmor},
|
||||
});
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WCST: %s.wcst\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" entries : %zu\n", c.entries.size());
|
||||
if (c.entries.empty()) return 0;
|
||||
std::printf(" id class lvl hp mana str agi sta int spi armor\n");
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf(" %4u %-8s %3u %5u %5u %3u %3u %3u %3u %3u %5u\n",
|
||||
e.statId, classIdName(e.classId),
|
||||
e.level, e.baseHealth, e.baseMana,
|
||||
e.baseStrength, e.baseAgility,
|
||||
e.baseStamina, e.baseIntellect,
|
||||
e.baseSpirit, e.baseArmor);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleValidate(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWcstExt(base);
|
||||
if (!wowee::pipeline::WoweeCombatStatsLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wcst: WCST not found: %s.wcst\n",
|
||||
base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeCombatStatsLoader::load(base);
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::set<uint32_t> idsSeen;
|
||||
using Pair = std::pair<uint8_t, uint8_t>;
|
||||
std::set<Pair> classLevelPairs;
|
||||
for (size_t k = 0; k < c.entries.size(); ++k) {
|
||||
const auto& e = c.entries[k];
|
||||
std::string ctx = "entry " + std::to_string(k) +
|
||||
" (statId=" + std::to_string(e.statId) +
|
||||
" " + classIdName(e.classId) +
|
||||
" L" + std::to_string(e.level) + ")";
|
||||
if (e.statId == 0)
|
||||
errors.push_back(ctx + ": statId is 0");
|
||||
if (e.classId == 0 || e.classId > 11) {
|
||||
errors.push_back(ctx + ": classId " +
|
||||
std::to_string(e.classId) +
|
||||
" out of vanilla range (1..11)");
|
||||
}
|
||||
if (e.classId == 6 || e.classId == 10) {
|
||||
warnings.push_back(ctx + ": classId " +
|
||||
std::to_string(e.classId) +
|
||||
" is unused in vanilla (DK/Monk gap)");
|
||||
}
|
||||
if (e.level == 0 || e.level > 60) {
|
||||
errors.push_back(ctx + ": level " +
|
||||
std::to_string(e.level) +
|
||||
" out of vanilla range (1..60)");
|
||||
}
|
||||
if (e.baseHealth == 0)
|
||||
errors.push_back(ctx +
|
||||
": baseHealth is 0 (player would die "
|
||||
"instantly)");
|
||||
// Warrior(1) and Rogue(4) use Rage/Energy
|
||||
// respectively — baseMana > 0 for these
|
||||
// classes is wrong.
|
||||
if ((e.classId == 1 || e.classId == 4) &&
|
||||
e.baseMana > 0) {
|
||||
warnings.push_back(ctx +
|
||||
": baseMana=" +
|
||||
std::to_string(e.baseMana) +
|
||||
" on Warrior/Rogue — these classes use "
|
||||
"Rage/Energy, not mana. Likely typo");
|
||||
}
|
||||
// (classId, level) MUST be unique — runtime
|
||||
// dispatch by this pair would tie.
|
||||
Pair p{e.classId, e.level};
|
||||
if (!classLevelPairs.insert(p).second) {
|
||||
errors.push_back(ctx +
|
||||
": duplicate (classId=" +
|
||||
std::to_string(e.classId) +
|
||||
", level=" + std::to_string(e.level) +
|
||||
") — runtime stat-lookup tie");
|
||||
}
|
||||
if (!idsSeen.insert(e.statId).second) {
|
||||
errors.push_back(ctx + ": duplicate statId");
|
||||
}
|
||||
}
|
||||
// Monotonicity: per-class, when sorted by level,
|
||||
// no stat (HP/mana/Str/Agi/Sta/Int/Spi/armor)
|
||||
// should regress (decrease) as level increases.
|
||||
// Regression suggests a typo in the data table.
|
||||
std::map<uint8_t, std::vector<const wowee::pipeline::
|
||||
WoweeCombatStats::Entry*>> byClass;
|
||||
for (const auto& e : c.entries) byClass[e.classId].push_back(&e);
|
||||
for (auto& [classId, vec] : byClass) {
|
||||
std::sort(vec.begin(), vec.end(),
|
||||
[](const wowee::pipeline::WoweeCombatStats::
|
||||
Entry* a,
|
||||
const wowee::pipeline::WoweeCombatStats::
|
||||
Entry* b) {
|
||||
return a->level < b->level;
|
||||
});
|
||||
for (size_t k = 1; k < vec.size(); ++k) {
|
||||
const auto* prev = vec[k-1];
|
||||
const auto* cur = vec[k];
|
||||
auto chk = [&](const char* statName,
|
||||
uint64_t prevV, uint64_t curV) {
|
||||
if (curV < prevV) {
|
||||
warnings.push_back(
|
||||
std::string("monotonicity: ") +
|
||||
classIdName(classId) +
|
||||
" " + statName +
|
||||
" regresses from " +
|
||||
std::to_string(prevV) +
|
||||
" (L" + std::to_string(prev->level) +
|
||||
") to " +
|
||||
std::to_string(curV) +
|
||||
" (L" + std::to_string(cur->level) +
|
||||
") — likely typo");
|
||||
}
|
||||
};
|
||||
chk("baseHealth", prev->baseHealth, cur->baseHealth);
|
||||
chk("baseMana", prev->baseMana, cur->baseMana);
|
||||
chk("baseStrength", prev->baseStrength, cur->baseStrength);
|
||||
chk("baseAgility", prev->baseAgility, cur->baseAgility);
|
||||
chk("baseStamina", prev->baseStamina, cur->baseStamina);
|
||||
chk("baseIntellect",prev->baseIntellect,cur->baseIntellect);
|
||||
chk("baseSpirit", prev->baseSpirit, cur->baseSpirit);
|
||||
chk("baseArmor", prev->baseArmor, cur->baseArmor);
|
||||
}
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wcst"] = base + ".wcst";
|
||||
j["ok"] = ok;
|
||||
j["errors"] = errors;
|
||||
j["warnings"] = warnings;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
std::printf("validate-wcst: %s.wcst\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu entries, all statIds + "
|
||||
"(classId,level) unique, classId 1..11, "
|
||||
"level 1..60, no zero baseHealth, no "
|
||||
"Warrior/Rogue mana, all stats "
|
||||
"monotonic over level\n",
|
||||
c.entries.size());
|
||||
return 0;
|
||||
}
|
||||
if (!warnings.empty()) {
|
||||
std::printf(" warnings (%zu):\n", warnings.size());
|
||||
for (const auto& w : warnings)
|
||||
std::printf(" - %s\n", w.c_str());
|
||||
}
|
||||
if (!errors.empty()) {
|
||||
std::printf(" ERRORS (%zu):\n", errors.size());
|
||||
for (const auto& e : errors)
|
||||
std::printf(" - %s\n", e.c_str());
|
||||
}
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleCombatStatsCatalog(int& i, int argc, char** argv,
|
||||
int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-cst-warrior") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenWarrior(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-cst-mage") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenMage(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-cst-starting") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenStarting(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wcst") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wcst") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
12
tools/editor/cli_combat_stats_catalog.hpp
Normal file
12
tools/editor/cli_combat_stats_catalog.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleCombatStatsCatalog(int& i, int argc, char** argv,
|
||||
int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
|
|
@ -174,6 +174,7 @@
|
|||
#include "cli_player_movement_anim_catalog.hpp"
|
||||
#include "cli_transit_schedule_catalog.hpp"
|
||||
#include "cli_mage_portals_catalog.hpp"
|
||||
#include "cli_combat_stats_catalog.hpp"
|
||||
#include "cli_catalog_pluck.hpp"
|
||||
#include "cli_catalog_find.hpp"
|
||||
#include "cli_catalog_by_name.hpp"
|
||||
|
|
@ -393,6 +394,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handlePlayerMovementAnimCatalog,
|
||||
handleTransitScheduleCatalog,
|
||||
handleMagePortalsCatalog,
|
||||
handleCombatStatsCatalog,
|
||||
handleCatalogPluck,
|
||||
handleCatalogFind,
|
||||
handleCatalogByName,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ constexpr FormatMagicEntry kFormats[] = {
|
|||
{{'W','P','H','M'}, ".wphm", "anim", "--info-wphm", "Player movement-to-animation map"},
|
||||
{{'W','T','S','C'}, ".wtsc", "transit", "--info-wtsc", "Transit schedule catalog"},
|
||||
{{'W','P','R','T'}, ".wprt", "portals", "--info-wprt", "Mage portal destinations catalog"},
|
||||
{{'W','C','S','T'}, ".wcst", "stats", "--info-wcst", "Combat stats baseline catalog"},
|
||||
{{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"},
|
||||
{{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"},
|
||||
{{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"},
|
||||
|
|
|
|||
|
|
@ -2573,6 +2573,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wprt to a human-editable JSON sidecar (defaults to <base>.wprt.json; emits factionAccess and portalKind as int + name string; floats preserved bit-for-bit)\n");
|
||||
std::printf(" --import-wprt-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wprt.json sidecar back into binary .wprt (factionAccess int OR \"both\"/\"alliance\"/\"horde\"/\"neutral\"; portalKind int OR \"teleport\"/\"portal\")\n");
|
||||
std::printf(" --gen-cst-warrior <wcst-base> [name]\n");
|
||||
std::printf(" Emit .wcst Warrior (classId=1) sparse base-stats sample at levels 1/10/20/30/40/60. baseMana=0 (Warrior uses Rage)\n");
|
||||
std::printf(" --gen-cst-mage <wcst-base> [name]\n");
|
||||
std::printf(" Emit .wcst Mage (classId=8) sparse base-stats sample at the same 6 levels with mana growth tracking Intellect\n");
|
||||
std::printf(" --gen-cst-starting <wcst-base> [name]\n");
|
||||
std::printf(" Emit .wcst all 9 vanilla classes at level 1 — illustrates per-class flat starting-stat differences (Warrior/Paladin high Str, Hunter/Rogue high Agi, Mage/Priest/Warlock high Int, Shaman/Druid balanced)\n");
|
||||
std::printf(" --info-wcst <wcst-base> [--json]\n");
|
||||
std::printf(" Print WCST entries (statId / class / level / hp / mana / Str/Agi/Sta/Int/Spi / armor)\n");
|
||||
std::printf(" --validate-wcst <wcst-base> [--json]\n");
|
||||
std::printf(" Static checks: statId+classId+level required, classId 1..11, level 1..60, no zero baseHealth (player would die instantly), no duplicate statIds, no duplicate (classId,level) pairs (runtime stat-lookup tie); warns on classId 6/10 (DK/Monk gap unused in vanilla), Warrior/Rogue baseMana > 0 (these classes use Rage/Energy not mana — likely typo), and per-class monotonicity violations across all 8 stats (any stat regressing as level increases)\n");
|
||||
std::printf(" --catalog-pluck <wXXX-file> <id> [--json]\n");
|
||||
std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n");
|
||||
std::printf(" --catalog-find <directory> <id> [--magic <WXXX>] [--json]\n");
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ constexpr FormatRow kFormats[] = {
|
|||
{"WPHM", ".wphm", "anim", "implicit M2 movementState->anim map","Player movement-to-animation map (per race/gender/state)"},
|
||||
{"WTSC", ".wtsc", "transit", "TaxiNodes + zeppelin GO scripts", "Transit schedule catalog (taxi/zeppelin/boat scheduled departures)"},
|
||||
{"WPRT", ".wprt", "portals", "SpellEffect TELEPORT_UNITS + AreaTrigger","Mage portal destinations catalog (spellId -> coords binding)"},
|
||||
{"WCST", ".wcst", "stats", "CharBaseInfo + GtChanceTo*.dbc + StatSystem","Combat stats baseline catalog (per-class per-level base stats)"},
|
||||
|
||||
// Additional pipeline catalogs without the alternating
|
||||
// gen/info/validate CLI surface (loaded by the engine
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue