From 88effe39cdcbf998c3cc55e4bd40bde822b43547 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 22:00:55 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20add=20WSPC=20(Spell=20Power=20C?= =?UTF-8?q?ost)=20=E2=80=94=20completes=20spell-bucket=20five-pack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open replacement for the per-spell power-cost fields in Spell.dbc plus SpellPowerCost-related side tables. Defines categorical power-cost buckets that spells reference (LowMana 5% / MediumMana 15% / HighMana 30% of caster max mana; fixed Rage-30 / Energy-40 / Runic-30 / etc), so spells share cost metadata across ranks instead of embedding per-rank cost numbers. Completes the small lookup-bucket five-pack: WSRG — range bucket WSCT — cast time bucket WSDR — duration bucket WSCD — cooldown bucket WSPC — power cost bucket (this catalog) Five small integer ids per spell (range / cast / dur / cd / cost) replace the dozens of duplicate per-rank fields that Blizzard's Spell.dbc carries. Editing one bucket here retunes every spell that references it — change LowMana from 5% to 4% and every rank-1 bolt across every caster class becomes cheaper. Cost can be flat (baseCost), per-level scaled (perLevelCost), or percentage-of-max-power (percentOfBase) — the engine sums whichever fields are non-zero. resolveCost(id, level, maxPower) does the math. Twelve power types covering every WoW resource (Mana / Rage / Focus / Energy / Happiness / Runic Power / Runes / Soul Shards / Holy Power / Eclipse / Health / NoCost). Three preset emitters: --gen-spc (4 baseline mana tiers), --gen-spc-rage (4 fixed warrior rage costs including stance-locked Whirlwind), --gen-spc-mixed (5 cross-class costs covering every non-mana power type with refund-on-miss flag for energy). Validation enforces id+name presence, powerType 0..11, no duplicate ids; warns on percentOfBase outside [0,1] (would overflow), NoCost type with non-zero cost fields, and non-NoCost types with no cost set (would cast for free — easy bug to ship). Wired through the cross-format table; WSPC appears automatically in all 11 cross-format utilities. Format count 72 -> 73; CLI flag count 922 -> 927. --- CMakeLists.txt | 3 + include/pipeline/wowee_spell_power_costs.hpp | 136 +++++++++ src/pipeline/wowee_spell_power_costs.cpp | 258 +++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + .../editor/cli_spell_power_costs_catalog.cpp | 270 ++++++++++++++++++ .../editor/cli_spell_power_costs_catalog.hpp | 12 + 10 files changed, 695 insertions(+) create mode 100644 include/pipeline/wowee_spell_power_costs.hpp create mode 100644 src/pipeline/wowee_spell_power_costs.cpp create mode 100644 tools/editor/cli_spell_power_costs_catalog.cpp create mode 100644 tools/editor/cli_spell_power_costs_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b444421b..c634f35e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -661,6 +661,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_spell_durations.cpp src/pipeline/wowee_spell_cooldowns.cpp src/pipeline/wowee_creature_families.cpp + src/pipeline/wowee_spell_power_costs.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1479,6 +1480,7 @@ add_executable(wowee_editor tools/editor/cli_spell_durations_catalog.cpp tools/editor/cli_spell_cooldowns_catalog.cpp tools/editor/cli_creature_families_catalog.cpp + tools/editor/cli_spell_power_costs_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1618,6 +1620,7 @@ add_executable(wowee_editor src/pipeline/wowee_spell_durations.cpp src/pipeline/wowee_spell_cooldowns.cpp src/pipeline/wowee_creature_families.cpp + src/pipeline/wowee_spell_power_costs.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spell_power_costs.hpp b/include/pipeline/wowee_spell_power_costs.hpp new file mode 100644 index 00000000..1d408ce4 --- /dev/null +++ b/include/pipeline/wowee_spell_power_costs.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spell Power Cost catalog (.wspc) — novel +// replacement for the per-spell power-cost fields in +// Spell.dbc plus SpellPowerCost-related side tables. +// Defines categorical power-cost buckets that spells +// reference (LowMana 5% / MediumMana 15% / HighMana 30% / +// fixed Rage-30 / Energy-40 / Runic-30 / etc), so spells +// share cost metadata across ranks instead of embedding +// per-rank cost numbers. +// +// Completes the small lookup-bucket set: +// WSRG — range bucket +// WSCT — cast time bucket +// WSDR — duration bucket +// WSCD — cooldown bucket +// WSPC — power cost bucket (this catalog) +// +// Five small integer ids per spell (range / cast / dur / +// cd / cost) replace the dozens of duplicate per-rank +// fields that Blizzard's Spell.dbc carries. The engine +// retunes thousands of spells at once by editing one +// bucket here. +// +// Cost can be flat (baseCost), per-level scaled +// (perLevelCost), or percentage-of-max-power +// (percentOfBase). The engine uses whichever fields are +// non-zero, summing them. percentOfBase=0.05 with +// baseCost=10 means "5% of max mana + 10 flat". +// +// Cross-references with previously-added formats: +// None — this catalog is consumed directly by the spell +// engine. WSPL spell entries reference powerCostId. +// +// Binary layout (little-endian): +// magic[4] = "WSPC" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// powerCostId (uint32) +// nameLen + name +// descLen + description +// powerType (uint8) / pad[3] +// baseCost (int32) +// perLevelCost (int32) +// percentOfBase (float) +// costFlags (uint32) +// iconColorRGBA (uint32) +struct WoweeSpellPowerCost { + enum PowerType : uint8_t { + Mana = 0, + Rage = 1, + Focus = 2, // hunter pet only in Classic; hunter in Cata+ + Energy = 3, // rogue / cat druid + Happiness = 4, // hunter pet (Classic-Wrath) + RunicPower = 5, // death knight + Runes = 6, // death knight rune count (separate bucket) + SoulShards = 7, // warlock + HolyPower = 8, // paladin (Cata+) + Eclipse = 9, // balance druid (Cata+) + Health = 10, // sacrificial / blood-tap costs + NoCost = 11, // free spell + }; + + enum CostFlag : uint32_t { + RequiresCombatStance = 1u << 0, // berserker stance / battle stance + RefundOnMiss = 1u << 1, // refunds 80% if spell misses + DoublesInForm = 1u << 2, // doubles in cat/bear form + ScalesWithMastery = 1u << 3, // mastery reduces this cost + }; + + struct Entry { + uint32_t powerCostId = 0; + std::string name; + std::string description; + uint8_t powerType = Mana; + int32_t baseCost = 0; + int32_t perLevelCost = 0; + float percentOfBase = 0.0f; + uint32_t costFlags = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t powerCostId) const; + + // Resolve the actual cost amount at the given caster + // level + max power pool. Sums baseCost + perLevelCost + // *level + percentOfBase*maxPower (whichever fields are + // non-zero). Returns 0 for NoCost type. + int32_t resolveCost(uint32_t powerCostId, + uint32_t casterLevel, + int32_t maxPower) const; + + static const char* powerTypeName(uint8_t k); +}; + +class WoweeSpellPowerCostLoader { +public: + static bool save(const WoweeSpellPowerCost& cat, + const std::string& basePath); + static WoweeSpellPowerCost load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-spc* variants. + // + // makeStarter — 4 baseline mana cost tiers (NoCost, + // LowMana 5%, MediumMana 15%, HighMana + // 30%) — % of max mana, scales with + // caster level naturally. + // makeRage — 4 fixed rage costs for warrior abilities + // (HeroicStrike 15, Slam 20, Whirlwind 25, + // MortalStrike 30). + // makeMixed — 5 cross-class cost buckets covering + // every non-mana power type (Hunter + // Focus 30, Rogue Energy 40, DK Runic 30, + // Paladin Holy 1, Warlock SoulShard 1). + static WoweeSpellPowerCost makeStarter(const std::string& catalogName); + static WoweeSpellPowerCost makeRage(const std::string& catalogName); + static WoweeSpellPowerCost makeMixed(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spell_power_costs.cpp b/src/pipeline/wowee_spell_power_costs.cpp new file mode 100644 index 00000000..f80aeec1 --- /dev/null +++ b/src/pipeline/wowee_spell_power_costs.cpp @@ -0,0 +1,258 @@ +#include "pipeline/wowee_spell_power_costs.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'P', 'C'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(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(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wspc") { + base += ".wspc"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeSpellPowerCost::Entry* +WoweeSpellPowerCost::findById(uint32_t powerCostId) const { + for (const auto& e : entries) + if (e.powerCostId == powerCostId) return &e; + return nullptr; +} + +int32_t WoweeSpellPowerCost::resolveCost(uint32_t powerCostId, + uint32_t casterLevel, + int32_t maxPower) const { + const Entry* e = findById(powerCostId); + if (!e) return 0; + if (e->powerType == NoCost) return 0; + int64_t cost = static_cast(e->baseCost) + + static_cast(e->perLevelCost) * + static_cast(casterLevel); + if (e->percentOfBase != 0.0f) { + cost += static_cast( + static_cast(maxPower) * e->percentOfBase); + } + if (cost < 0) cost = 0; + return static_cast(cost); +} + +const char* WoweeSpellPowerCost::powerTypeName(uint8_t k) { + switch (k) { + case Mana: return "mana"; + case Rage: return "rage"; + case Focus: return "focus"; + case Energy: return "energy"; + case Happiness: return "happiness"; + case RunicPower: return "runic-power"; + case Runes: return "runes"; + case SoulShards: return "soul-shards"; + case HolyPower: return "holy-power"; + case Eclipse: return "eclipse"; + case Health: return "health"; + case NoCost: return "no-cost"; + default: return "unknown"; + } +} + +bool WoweeSpellPowerCostLoader::save(const WoweeSpellPowerCost& 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(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.powerCostId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.powerType); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, e.baseCost); + writePOD(os, e.perLevelCost); + writePOD(os, e.percentOfBase); + writePOD(os, e.costFlags); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeSpellPowerCost WoweeSpellPowerCostLoader::load( + const std::string& basePath) { + WoweeSpellPowerCost 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.powerCostId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.powerType)) { + out.entries.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + if (!readPOD(is, e.baseCost) || + !readPOD(is, e.perLevelCost) || + !readPOD(is, e.percentOfBase) || + !readPOD(is, e.costFlags) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSpellPowerCostLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSpellPowerCost WoweeSpellPowerCostLoader::makeStarter( + const std::string& catalogName) { + using P = WoweeSpellPowerCost; + WoweeSpellPowerCost c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t type, + float pct, int32_t base, const char* desc) { + P::Entry e; + e.powerCostId = id; e.name = name; e.description = desc; + e.powerType = type; + e.baseCost = base; + e.percentOfBase = pct; + e.iconColorRGBA = packRgba(80, 140, 240); // mana blue + c.entries.push_back(e); + }; + add(1, "NoCost", P::NoCost, 0.0f, 0, + "Free spell — no resource cost (Auto Attack)."); + add(2, "LowMana", P::Mana, 0.05f, 0, + "Low-cost spell — 5% of max mana (Frostbolt)."); + add(3, "MediumMana", P::Mana, 0.15f, 0, + "Medium-cost spell — 15% of max mana (Fireball)."); + add(4, "HighMana", P::Mana, 0.30f, 0, + "High-cost spell — 30% of max mana (Pyroblast)."); + return c; +} + +WoweeSpellPowerCost WoweeSpellPowerCostLoader::makeRage( + const std::string& catalogName) { + using P = WoweeSpellPowerCost; + WoweeSpellPowerCost c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, int32_t rage, + uint32_t flags, const char* desc) { + P::Entry e; + e.powerCostId = id; e.name = name; e.description = desc; + e.powerType = P::Rage; + e.baseCost = rage; + e.costFlags = flags; + e.iconColorRGBA = packRgba(220, 60, 60); // rage red + c.entries.push_back(e); + }; + // Warrior abilities, fixed rage cost (no level scaling). + add(100, "HeroicStrikeRage", 15, 0, + "Heroic Strike — 15 rage on next melee."); + add(101, "SlamRage", 20, 0, + "Slam — 20 rage, channeled."); + add(102, "WhirlwindRage", 25, P::RequiresCombatStance, + "Whirlwind — 25 rage, requires Berserker stance."); + add(103, "MortalStrikeRage", 30, 0, + "Mortal Strike — 30 rage instant strike."); + return c; +} + +WoweeSpellPowerCost WoweeSpellPowerCostLoader::makeMixed( + const std::string& catalogName) { + using P = WoweeSpellPowerCost; + WoweeSpellPowerCost c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t type, + int32_t cost, uint32_t flags, const char* desc) { + P::Entry e; + e.powerCostId = id; e.name = name; e.description = desc; + e.powerType = type; + e.baseCost = cost; + e.costFlags = flags; + e.iconColorRGBA = packRgba(180, 180, 180); // neutral + c.entries.push_back(e); + }; + add(200, "HunterFocus30", P::Focus, 30, 0, + "Hunter Focus — 30 focus (Cobra Shot)."); + add(201, "RogueEnergy40", P::Energy, 40, P::RefundOnMiss, + "Rogue Energy — 40 energy (Sinister Strike), refunds " + "on miss/dodge/parry."); + add(202, "DKRunic30", P::RunicPower, 30, 0, + "Death Knight Runic Power — 30 RP (Death Coil)."); + add(203, "PaladinHoly1", P::HolyPower, 1, 0, + "Paladin Holy Power — 1 HP per finisher (Templar's " + "Verdict, Word of Glory)."); + add(204, "WarlockShard1", P::SoulShards, 1, 0, + "Warlock Soul Shard — 1 shard (Soulburn finishers)."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 565e86c2..4a2caeb5 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -222,6 +222,8 @@ const char* const kArgRequired[] = { "--gen-cef", "--gen-cef-ferocity", "--gen-cef-exotic", "--info-wcef", "--validate-wcef", "--export-wcef-json", "--import-wcef-json", + "--gen-spc", "--gen-spc-rage", "--gen-spc-mixed", + "--info-wspc", "--validate-wspc", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 448d52e6..a5831d66 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -111,6 +111,7 @@ #include "cli_spell_durations_catalog.hpp" #include "cli_spell_cooldowns_catalog.hpp" #include "cli_creature_families_catalog.hpp" +#include "cli_spell_power_costs_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -263,6 +264,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSpellDurationsCatalog, handleSpellCooldownsCatalog, handleCreatureFamiliesCatalog, + handleSpellPowerCostsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index f9f1b3ad..a0d4c204 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -75,6 +75,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','D','R'}, ".wsdr", "spells", "--info-wsdr", "Spell duration bucket catalog"}, {{'W','S','C','D'}, ".wscd", "spells", "--info-wscd", "Spell cooldown category catalog"}, {{'W','C','E','F'}, ".wcef", "creatures", "--info-wcef", "Creature / pet family catalog"}, + {{'W','S','P','C'}, ".wspc", "spells", "--info-wspc", "Spell power cost bucket 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"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index f84c9b77..6169b569 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1761,6 +1761,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wcef to a human-editable JSON sidecar (defaults to .wcef.json)\n"); std::printf(" --import-wcef-json [out-base]\n"); std::printf(" Import a .wcef.json sidecar back into binary .wcef (accepts familyKind/petTalentTree int OR name; petFoodTypes int OR pipe-separated label string)\n"); + std::printf(" --gen-spc [name]\n"); + std::printf(" Emit .wspc starter: 4 baseline mana cost tiers (NoCost / LowMana 5%% / MediumMana 15%% / HighMana 30%%) — %% of caster max mana\n"); + std::printf(" --gen-spc-rage [name]\n"); + std::printf(" Emit .wspc 4 fixed warrior rage costs (HeroicStrike 15 / Slam 20 / Whirlwind 25 stance-locked / MortalStrike 30)\n"); + std::printf(" --gen-spc-mixed [name]\n"); + std::printf(" Emit .wspc 5 cross-class costs covering every non-mana power type (Hunter Focus 30 / Rogue Energy 40 refund / DK Runic 30 / Paladin Holy 1 / Warlock Shard 1)\n"); + std::printf(" --info-wspc [--json]\n"); + std::printf(" Print WSPC entries (id / powerType / baseCost / perLevelCost / percentOfBase / cost flags / name) — flags decoded as label list\n"); + std::printf(" --validate-wspc [--json]\n"); + std::printf(" Static checks: id+name required, powerType 0..11, no duplicate ids; warns on percent outside [0,1], NoCost+nonzero, and non-NoCost types with no cost set (would cast for free)\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index baf52c38..d655ea0c 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -97,6 +97,7 @@ constexpr FormatRow kFormats[] = { {"WSDR", ".wsdr", "spells", "SpellDuration.dbc + per-spell dur","Spell duration bucket catalog"}, {"WSCD", ".wscd", "spells", "SpellCooldown.dbc + shared cd grp","Spell cooldown category catalog"}, {"WCEF", ".wcef", "creatures", "CreatureFamily.dbc + pet trees", "Creature / pet family catalog"}, + {"WSPC", ".wspc", "spells", "Spell.dbc power-cost fields", "Spell power cost bucket catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_spell_power_costs_catalog.cpp b/tools/editor/cli_spell_power_costs_catalog.cpp new file mode 100644 index 00000000..6dbeab70 --- /dev/null +++ b/tools/editor/cli_spell_power_costs_catalog.cpp @@ -0,0 +1,270 @@ +#include "cli_spell_power_costs_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spell_power_costs.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWspcExt(std::string base) { + stripExt(base, ".wspc"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSpellPowerCost& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpellPowerCostLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wspc\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpellPowerCost& c, + const std::string& base) { + std::printf("Wrote %s.wspc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" buckets : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterPowerCosts"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspcExt(base); + auto c = wowee::pipeline::WoweeSpellPowerCostLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-spc")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRage(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WarriorRageCosts"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspcExt(base); + auto c = wowee::pipeline::WoweeSpellPowerCostLoader::makeRage(name); + if (!saveOrError(c, base, "gen-spc-rage")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMixed(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MixedClassPowerCosts"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspcExt(base); + auto c = wowee::pipeline::WoweeSpellPowerCostLoader::makeMixed(name); + if (!saveOrError(c, base, "gen-spc-mixed")) return 1; + printGenSummary(c, base); + return 0; +} + +void appendCostFlagNames(uint32_t flags, std::string& out) { + using F = wowee::pipeline::WoweeSpellPowerCost; + auto add = [&](const char* n) { + if (!out.empty()) out += "|"; + out += n; + }; + if (flags & F::RequiresCombatStance) add("RequiresCombatStance"); + if (flags & F::RefundOnMiss) add("RefundOnMiss"); + if (flags & F::DoublesInForm) add("DoublesInForm"); + if (flags & F::ScalesWithMastery) add("ScalesWithMastery"); + if (out.empty()) out = "-"; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWspcExt(base); + if (!wowee::pipeline::WoweeSpellPowerCostLoader::exists(base)) { + std::fprintf(stderr, "WSPC not found: %s.wspc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellPowerCostLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wspc"] = base + ".wspc"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string flagNames; + appendCostFlagNames(e.costFlags, flagNames); + arr.push_back({ + {"powerCostId", e.powerCostId}, + {"name", e.name}, + {"description", e.description}, + {"powerType", e.powerType}, + {"powerTypeName", wowee::pipeline::WoweeSpellPowerCost::powerTypeName(e.powerType)}, + {"baseCost", e.baseCost}, + {"perLevelCost", e.perLevelCost}, + {"percentOfBase", e.percentOfBase}, + {"costFlags", e.costFlags}, + {"costFlagsLabels", flagNames}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSPC: %s.wspc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" buckets : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id powerType base /lvl %%max flags name\n"); + for (const auto& e : c.entries) { + std::string flagNames; + appendCostFlagNames(e.costFlags, flagNames); + std::printf(" %4u %-11s %5d %5d %5.2f %-36s %s\n", + e.powerCostId, + wowee::pipeline::WoweeSpellPowerCost::powerTypeName(e.powerType), + e.baseCost, e.perLevelCost, + e.percentOfBase, + flagNames.c_str(), + e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWspcExt(base); + if (!wowee::pipeline::WoweeSpellPowerCostLoader::exists(base)) { + std::fprintf(stderr, + "validate-wspc: WSPC not found: %s.wspc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellPowerCostLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + constexpr uint32_t kKnownFlagMask = + wowee::pipeline::WoweeSpellPowerCost::RequiresCombatStance | + wowee::pipeline::WoweeSpellPowerCost::RefundOnMiss | + wowee::pipeline::WoweeSpellPowerCost::DoublesInForm | + wowee::pipeline::WoweeSpellPowerCost::ScalesWithMastery; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.powerCostId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.powerCostId == 0) + errors.push_back(ctx + ": powerCostId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.powerType > wowee::pipeline::WoweeSpellPowerCost::NoCost) { + errors.push_back(ctx + ": powerType " + + std::to_string(e.powerType) + " not in 0..11"); + } + if (e.percentOfBase < 0.0f || e.percentOfBase > 1.0f) { + warnings.push_back(ctx + + ": percentOfBase " + std::to_string(e.percentOfBase) + + " is outside [0..1] — may overflow caster's max power"); + } + if (e.costFlags & ~kKnownFlagMask) { + warnings.push_back(ctx + + ": costFlags has bits outside known mask " + + "(0x" + std::to_string(e.costFlags & ~kKnownFlagMask) + + ") — engine will ignore unknown flags"); + } + // NoCost type with non-zero cost values is + // contradictory. + if (e.powerType == wowee::pipeline::WoweeSpellPowerCost::NoCost && + (e.baseCost != 0 || e.perLevelCost != 0 || + e.percentOfBase != 0.0f)) { + warnings.push_back(ctx + + ": NoCost type with non-zero cost fields — " + "engine treats this as free regardless"); + } + // Spell with no cost fields set when not NoCost — is + // probably misconfigured (would be free). + if (e.powerType != wowee::pipeline::WoweeSpellPowerCost::NoCost && + e.baseCost == 0 && e.perLevelCost == 0 && + e.percentOfBase == 0.0f) { + warnings.push_back(ctx + + ": no cost fields set but powerType is " + + wowee::pipeline::WoweeSpellPowerCost::powerTypeName(e.powerType) + + " — spell will cast for free, switch to NoCost type if intended"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.powerCostId) { + errors.push_back(ctx + ": duplicate powerCostId"); + break; + } + } + idsSeen.push_back(e.powerCostId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wspc"] = base + ".wspc"; + 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-wspc: %s.wspc\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu buckets, all powerCostIds unique\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 handleSpellPowerCostsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-spc") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spc-rage") == 0 && i + 1 < argc) { + outRc = handleGenRage(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spc-mixed") == 0 && i + 1 < argc) { + outRc = handleGenMixed(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wspc") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wspc") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_spell_power_costs_catalog.hpp b/tools/editor/cli_spell_power_costs_catalog.hpp new file mode 100644 index 00000000..92c3e449 --- /dev/null +++ b/tools/editor/cli_spell_power_costs_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpellPowerCostsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee