mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
feat(pipeline): add WIT (Wowee Item Template) format
Novel open replacement for Blizzard's Item.dbc +
ItemDisplayInfo.dbc + the SQL item_template tables that
AzerothCore-style servers store item definitions in. The
12th open format added to the editor.
A WIT file holds the catalog of all items in a content
pack: weapons, armor, consumables, quest items, trade
goods. Each entry pairs gameplay metadata (stats, level
reqs, flags, weapon damage / speed) with display metadata
(displayId for icon / model, quality color), so the
runtime can render inventory tooltips and equip slots
from a single load.
Format:
• magic "WITM", version 1, little-endian
• per item: itemId / displayId / quality / itemClass /
itemSubClass / inventoryType / flags / requiredLevel /
itemLevel / sellPrice / buyPrice / maxStack / durability
/ damageMin / damageMax / attackSpeedMs /
statCount + stats[] / name / description
Enums:
• Quality: Poor..Heirloom (8 levels)
• Class: Consumable, Weapon, Armor, Quest, ... (13)
• InventoryType: Head..Cloak..Weapon2H (18 slots)
• Flags: Unique, BoP, BoE, QuestItem, Conjured, ...
• StatType: Stamina, Strength, Intellect, Defense, ...
API: WoweeItemLoader::save / load / exists / findById;
presets makeStarter (4-item demo), makeWeapons (5 items
common -> legendary), makeArmor (6-piece mail set with
BoE flag).
CLI added (5 flags, 480 documented total now):
--gen-items / --gen-items-weapons / --gen-items-armor
--info-wit / --validate-wit
Validator catches: itemId=0, duplicate itemIds, weapons
with 0 damage or attackSpeed, weapons with non-weapon
slot, equippables with durability=0 or maxStack>1, sell
price >= buy price (vendor would lose money), out-of-range
quality.
All 3 presets save / load / re-validate clean. Info-table
output includes a gold/silver/copper price formatter for
hand-readability.
This commit is contained in:
parent
23f262c655
commit
9093975bdd
8 changed files with 870 additions and 0 deletions
|
|
@ -599,6 +599,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_world_map.cpp
|
||||
src/pipeline/wowee_sound.cpp
|
||||
src/pipeline/wowee_spawns.cpp
|
||||
src/pipeline/wowee_items.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1344,6 +1345,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_world_map.cpp
|
||||
tools/editor/cli_sound_catalog.cpp
|
||||
tools/editor/cli_spawns_catalog.cpp
|
||||
tools/editor/cli_items_catalog.cpp
|
||||
tools/editor/cli_quest_objective.cpp
|
||||
tools/editor/cli_quest_reward.cpp
|
||||
tools/editor/cli_clone.cpp
|
||||
|
|
@ -1421,6 +1423,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_world_map.cpp
|
||||
src/pipeline/wowee_sound.cpp
|
||||
src/pipeline/wowee_spawns.cpp
|
||||
src/pipeline/wowee_items.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
185
include/pipeline/wowee_items.hpp
Normal file
185
include/pipeline/wowee_items.hpp
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Item Template (.wit) — novel replacement for
|
||||
// Blizzard's Item.dbc + ItemDisplayInfo.dbc + the SQL
|
||||
// item_template tables that AzerothCore-style servers store
|
||||
// item definitions in. The 12th open format added to the
|
||||
// editor.
|
||||
//
|
||||
// One file holds the catalog of all items in a content pack:
|
||||
// weapons, armor, consumables, quest items, trade goods, etc.
|
||||
// Each entry pairs the gameplay metadata (stats, level reqs,
|
||||
// flags) with the display metadata (icon / model displayId,
|
||||
// quality color) — the runtime needs both together to render
|
||||
// inventory tooltips and equip slots.
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WITM"
|
||||
// version (uint32) = current 1
|
||||
// nameLen (uint32) + name bytes -- catalog label
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// itemId (uint32)
|
||||
// displayId (uint32)
|
||||
// quality (uint8) -- poor..artifact
|
||||
// itemClass (uint8) -- consumable / weapon / armor / ...
|
||||
// itemSubClass (uint8)
|
||||
// inventoryType (uint8) -- equip slot (0 = non-equip)
|
||||
// flags (uint32) -- unique / BoP / BoE / quest / ...
|
||||
// requiredLevel (uint16)
|
||||
// itemLevel (uint16)
|
||||
// sellPriceCopper (uint32)
|
||||
// buyPriceCopper (uint32)
|
||||
// maxStack (uint16)
|
||||
// durability (uint16) -- 0 for non-equippable
|
||||
// damageMin (uint32) -- weapons only; 0 otherwise
|
||||
// damageMax (uint32)
|
||||
// attackSpeedMs (uint32)
|
||||
// statCount (uint8) + pad[3]
|
||||
// stats (statCount × {type: uint8, pad, value: int16})
|
||||
// nameLen (uint32) + name bytes
|
||||
// descLen (uint32) + description bytes
|
||||
struct WoweeItem {
|
||||
enum Quality : uint8_t {
|
||||
Poor = 0,
|
||||
Common = 1,
|
||||
Uncommon = 2,
|
||||
Rare = 3,
|
||||
Epic = 4,
|
||||
Legendary = 5,
|
||||
Artifact = 6,
|
||||
Heirloom = 7,
|
||||
};
|
||||
|
||||
enum Class : uint8_t {
|
||||
Consumable = 0,
|
||||
Container = 1,
|
||||
Weapon = 2,
|
||||
Gem = 3,
|
||||
Armor = 4,
|
||||
Reagent = 5,
|
||||
Projectile = 6,
|
||||
TradeGoods = 7,
|
||||
Recipe = 9,
|
||||
Quiver = 11,
|
||||
Quest = 12,
|
||||
Key = 13,
|
||||
Misc = 15,
|
||||
};
|
||||
|
||||
enum InventoryType : uint8_t {
|
||||
NonEquip = 0,
|
||||
Head = 1,
|
||||
Neck = 2,
|
||||
Shoulders = 3,
|
||||
Body = 4, // shirt
|
||||
Chest = 5,
|
||||
Waist = 6,
|
||||
Legs = 7,
|
||||
Feet = 8,
|
||||
Wrists = 9,
|
||||
Hands = 10,
|
||||
Finger = 11,
|
||||
Trinket = 12,
|
||||
Weapon1H = 13,
|
||||
Shield = 14,
|
||||
Ranged = 15,
|
||||
Cloak = 16,
|
||||
Weapon2H = 17,
|
||||
};
|
||||
|
||||
enum Flags : uint32_t {
|
||||
Unique = 0x01,
|
||||
Conjured = 0x02,
|
||||
Openable = 0x04,
|
||||
Heroic = 0x08,
|
||||
BindOnPickup = 0x10,
|
||||
BindOnEquip = 0x20,
|
||||
QuestItem = 0x40,
|
||||
NoSellable = 0x80,
|
||||
};
|
||||
|
||||
// Common stat types (matches WoW's ItemModType numbering
|
||||
// for the most-used subset; the format permits any uint8).
|
||||
enum StatType : uint8_t {
|
||||
StatNone = 0,
|
||||
StatHealth = 1,
|
||||
StatMana = 2,
|
||||
StatAgility = 3,
|
||||
StatStrength = 4,
|
||||
StatIntellect = 5,
|
||||
StatSpirit = 6,
|
||||
StatStamina = 7,
|
||||
StatDefense = 31,
|
||||
};
|
||||
|
||||
struct Stat {
|
||||
uint8_t type = StatNone;
|
||||
int16_t value = 0;
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
uint32_t itemId = 0;
|
||||
uint32_t displayId = 0;
|
||||
uint8_t quality = Common;
|
||||
uint8_t itemClass = Misc;
|
||||
uint8_t itemSubClass = 0;
|
||||
uint8_t inventoryType = NonEquip;
|
||||
uint32_t flags = 0;
|
||||
uint16_t requiredLevel = 0;
|
||||
uint16_t itemLevel = 1;
|
||||
uint32_t sellPriceCopper = 0;
|
||||
uint32_t buyPriceCopper = 0;
|
||||
uint16_t maxStack = 1;
|
||||
uint16_t durability = 0;
|
||||
uint32_t damageMin = 0;
|
||||
uint32_t damageMax = 0;
|
||||
uint32_t attackSpeedMs = 0;
|
||||
std::vector<Stat> stats; // up to 255 (uint8 count)
|
||||
std::string name;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
// Lookup by itemId — nullptr if not present.
|
||||
const Entry* findById(uint32_t itemId) const;
|
||||
|
||||
static const char* qualityName(uint8_t q);
|
||||
static const char* classNameOf(uint8_t c);
|
||||
static const char* slotName(uint8_t s);
|
||||
static const char* statName(uint8_t t);
|
||||
};
|
||||
|
||||
class WoweeItemLoader {
|
||||
public:
|
||||
static bool save(const WoweeItem& cat,
|
||||
const std::string& basePath);
|
||||
static WoweeItem load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-items* variants.
|
||||
//
|
||||
// makeStarter — a tiny demo catalog: 1 weapon + 1 chest
|
||||
// armor + 1 healing potion + 1 quest item.
|
||||
// makeWeapons — 5 weapon entries spanning common,
|
||||
// uncommon, rare, epic; both 1H and 2H.
|
||||
// makeArmor — full gear set: head + chest + legs +
|
||||
// feet + hands + cloak.
|
||||
static WoweeItem makeStarter(const std::string& catalogName);
|
||||
static WoweeItem makeWeapons(const std::string& catalogName);
|
||||
static WoweeItem makeArmor(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
378
src/pipeline/wowee_items.cpp
Normal file
378
src/pipeline/wowee_items.cpp
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
#include "pipeline/wowee_items.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'I', 'T', 'M'};
|
||||
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; // 1 MiB sanity cap
|
||||
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() < 4 || base.substr(base.size() - 4) != ".wit") {
|
||||
base += ".wit";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeItem::Entry* WoweeItem::findById(uint32_t itemId) const {
|
||||
for (const auto& e : entries) {
|
||||
if (e.itemId == itemId) return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* WoweeItem::qualityName(uint8_t q) {
|
||||
switch (q) {
|
||||
case Poor: return "poor";
|
||||
case Common: return "common";
|
||||
case Uncommon: return "uncommon";
|
||||
case Rare: return "rare";
|
||||
case Epic: return "epic";
|
||||
case Legendary: return "legendary";
|
||||
case Artifact: return "artifact";
|
||||
case Heirloom: return "heirloom";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* WoweeItem::classNameOf(uint8_t c) {
|
||||
switch (c) {
|
||||
case Consumable: return "consumable";
|
||||
case Container: return "container";
|
||||
case Weapon: return "weapon";
|
||||
case Gem: return "gem";
|
||||
case Armor: return "armor";
|
||||
case Reagent: return "reagent";
|
||||
case Projectile: return "projectile";
|
||||
case TradeGoods: return "trade-goods";
|
||||
case Recipe: return "recipe";
|
||||
case Quiver: return "quiver";
|
||||
case Quest: return "quest";
|
||||
case Key: return "key";
|
||||
case Misc: return "misc";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* WoweeItem::slotName(uint8_t s) {
|
||||
switch (s) {
|
||||
case NonEquip: return "-";
|
||||
case Head: return "head";
|
||||
case Neck: return "neck";
|
||||
case Shoulders: return "shoulders";
|
||||
case Body: return "shirt";
|
||||
case Chest: return "chest";
|
||||
case Waist: return "waist";
|
||||
case Legs: return "legs";
|
||||
case Feet: return "feet";
|
||||
case Wrists: return "wrists";
|
||||
case Hands: return "hands";
|
||||
case Finger: return "finger";
|
||||
case Trinket: return "trinket";
|
||||
case Weapon1H: return "weapon-1h";
|
||||
case Shield: return "shield";
|
||||
case Ranged: return "ranged";
|
||||
case Cloak: return "cloak";
|
||||
case Weapon2H: return "weapon-2h";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const char* WoweeItem::statName(uint8_t t) {
|
||||
switch (t) {
|
||||
case StatNone: return "-";
|
||||
case StatHealth: return "health";
|
||||
case StatMana: return "mana";
|
||||
case StatAgility: return "agility";
|
||||
case StatStrength: return "strength";
|
||||
case StatIntellect: return "intellect";
|
||||
case StatSpirit: return "spirit";
|
||||
case StatStamina: return "stamina";
|
||||
case StatDefense: return "defense";
|
||||
default: return "stat?";
|
||||
}
|
||||
}
|
||||
|
||||
bool WoweeItemLoader::save(const WoweeItem& 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.itemId);
|
||||
writePOD(os, e.displayId);
|
||||
writePOD(os, e.quality);
|
||||
writePOD(os, e.itemClass);
|
||||
writePOD(os, e.itemSubClass);
|
||||
writePOD(os, e.inventoryType);
|
||||
writePOD(os, e.flags);
|
||||
writePOD(os, e.requiredLevel);
|
||||
writePOD(os, e.itemLevel);
|
||||
writePOD(os, e.sellPriceCopper);
|
||||
writePOD(os, e.buyPriceCopper);
|
||||
writePOD(os, e.maxStack);
|
||||
writePOD(os, e.durability);
|
||||
writePOD(os, e.damageMin);
|
||||
writePOD(os, e.damageMax);
|
||||
writePOD(os, e.attackSpeedMs);
|
||||
// statCount is uint8; cap to 255 to avoid silent truncation
|
||||
// when a hand-edit JSON has more.
|
||||
uint8_t statCount = static_cast<uint8_t>(
|
||||
e.stats.size() > 255 ? 255 : e.stats.size());
|
||||
writePOD(os, statCount);
|
||||
uint8_t pad[3] = {0, 0, 0};
|
||||
os.write(reinterpret_cast<const char*>(pad), 3);
|
||||
for (uint8_t k = 0; k < statCount; ++k) {
|
||||
const auto& s = e.stats[k];
|
||||
writePOD(os, s.type);
|
||||
uint8_t spad = 0;
|
||||
writePOD(os, spad);
|
||||
writePOD(os, s.value);
|
||||
}
|
||||
writeStr(os, e.name);
|
||||
writeStr(os, e.description);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeItem WoweeItemLoader::load(const std::string& basePath) {
|
||||
WoweeItem 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.itemId) ||
|
||||
!readPOD(is, e.displayId) ||
|
||||
!readPOD(is, e.quality) ||
|
||||
!readPOD(is, e.itemClass) ||
|
||||
!readPOD(is, e.itemSubClass) ||
|
||||
!readPOD(is, e.inventoryType) ||
|
||||
!readPOD(is, e.flags) ||
|
||||
!readPOD(is, e.requiredLevel) ||
|
||||
!readPOD(is, e.itemLevel) ||
|
||||
!readPOD(is, e.sellPriceCopper) ||
|
||||
!readPOD(is, e.buyPriceCopper) ||
|
||||
!readPOD(is, e.maxStack) ||
|
||||
!readPOD(is, e.durability) ||
|
||||
!readPOD(is, e.damageMin) ||
|
||||
!readPOD(is, e.damageMax) ||
|
||||
!readPOD(is, e.attackSpeedMs)) {
|
||||
out.entries.clear();
|
||||
return out;
|
||||
}
|
||||
uint8_t statCount = 0;
|
||||
if (!readPOD(is, statCount)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint8_t pad[3];
|
||||
is.read(reinterpret_cast<char*>(pad), 3);
|
||||
if (is.gcount() != 3) { out.entries.clear(); return out; }
|
||||
e.stats.resize(statCount);
|
||||
for (uint8_t k = 0; k < statCount; ++k) {
|
||||
if (!readPOD(is, e.stats[k].type)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint8_t spad = 0;
|
||||
if (!readPOD(is, spad)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readPOD(is, e.stats[k].value)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
if (!readStr(is, e.name) || !readStr(is, e.description)) {
|
||||
out.entries.clear();
|
||||
return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeItemLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeItem WoweeItemLoader::makeStarter(const std::string& catalogName) {
|
||||
WoweeItem c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
WoweeItem::Entry e;
|
||||
e.itemId = 1; e.displayId = 100;
|
||||
e.quality = WoweeItem::Common; e.itemClass = WoweeItem::Weapon;
|
||||
e.itemSubClass = 0; // sword 1H
|
||||
e.inventoryType = WoweeItem::Weapon1H;
|
||||
e.requiredLevel = 1; e.itemLevel = 5;
|
||||
e.sellPriceCopper = 50; e.buyPriceCopper = 200;
|
||||
e.maxStack = 1; e.durability = 50;
|
||||
e.damageMin = 4; e.damageMax = 9; e.attackSpeedMs = 1800;
|
||||
e.stats.push_back({WoweeItem::StatStrength, 1});
|
||||
e.name = "Worn Shortsword";
|
||||
e.description = "A simple training sword.";
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
WoweeItem::Entry e;
|
||||
e.itemId = 2; e.displayId = 200;
|
||||
e.quality = WoweeItem::Common; e.itemClass = WoweeItem::Armor;
|
||||
e.itemSubClass = 1; // cloth chest
|
||||
e.inventoryType = WoweeItem::Chest;
|
||||
e.requiredLevel = 1; e.itemLevel = 5;
|
||||
e.sellPriceCopper = 30; e.buyPriceCopper = 150;
|
||||
e.maxStack = 1; e.durability = 40;
|
||||
e.stats.push_back({WoweeItem::StatStamina, 1});
|
||||
e.name = "Linen Vest";
|
||||
e.description = "Plain linen.";
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
WoweeItem::Entry e;
|
||||
e.itemId = 3; e.displayId = 300;
|
||||
e.quality = WoweeItem::Common;
|
||||
e.itemClass = WoweeItem::Consumable;
|
||||
e.itemSubClass = 0; // potion
|
||||
e.inventoryType = WoweeItem::NonEquip;
|
||||
e.requiredLevel = 1; e.itemLevel = 1;
|
||||
e.sellPriceCopper = 5; e.buyPriceCopper = 25;
|
||||
e.maxStack = 20; e.durability = 0;
|
||||
e.name = "Minor Healing Potion";
|
||||
e.description = "Restores a small amount of health.";
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
WoweeItem::Entry e;
|
||||
e.itemId = 4; e.displayId = 400;
|
||||
e.quality = WoweeItem::Common;
|
||||
e.itemClass = WoweeItem::Quest;
|
||||
e.itemSubClass = 0;
|
||||
e.inventoryType = WoweeItem::NonEquip;
|
||||
e.flags = WoweeItem::QuestItem | WoweeItem::Unique |
|
||||
WoweeItem::NoSellable;
|
||||
e.maxStack = 1; e.durability = 0;
|
||||
e.name = "Tattered Letter";
|
||||
e.description = "Deliver to the captain in the next town.";
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeItem WoweeItemLoader::makeWeapons(const std::string& catalogName) {
|
||||
WoweeItem c;
|
||||
c.name = catalogName;
|
||||
auto addWeapon = [&](uint32_t id, uint32_t disp, uint8_t qual,
|
||||
uint8_t slot, uint16_t lvlReq, uint16_t ilvl,
|
||||
uint32_t dmgMin, uint32_t dmgMax, uint32_t speedMs,
|
||||
const char* name) {
|
||||
WoweeItem::Entry e;
|
||||
e.itemId = id; e.displayId = disp;
|
||||
e.quality = qual; e.itemClass = WoweeItem::Weapon;
|
||||
e.itemSubClass = (slot == WoweeItem::Weapon2H ? 1 : 0);
|
||||
e.inventoryType = slot;
|
||||
e.requiredLevel = lvlReq; e.itemLevel = ilvl;
|
||||
e.sellPriceCopper = ilvl * 10u;
|
||||
e.buyPriceCopper = ilvl * 100u;
|
||||
e.maxStack = 1;
|
||||
e.durability = static_cast<uint16_t>(40 + ilvl);
|
||||
e.damageMin = dmgMin; e.damageMax = dmgMax;
|
||||
e.attackSpeedMs = speedMs;
|
||||
e.stats.push_back({WoweeItem::StatStrength,
|
||||
static_cast<int16_t>(1 + ilvl / 5)});
|
||||
e.name = name;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
addWeapon(1001, 1100, WoweeItem::Common, WoweeItem::Weapon1H, 1, 5, 4, 9, 1800, "Apprentice Sword");
|
||||
addWeapon(1002, 1101, WoweeItem::Uncommon, WoweeItem::Weapon1H, 10, 15, 12, 22, 1700, "Journeyman Blade");
|
||||
addWeapon(1003, 1102, WoweeItem::Rare, WoweeItem::Weapon1H, 20, 30, 28, 48, 1600, "Steelthorn Edge");
|
||||
addWeapon(1004, 1103, WoweeItem::Epic, WoweeItem::Weapon2H, 35, 50, 70, 110, 3200, "Bloodforged Greatsword");
|
||||
addWeapon(1005, 1104, WoweeItem::Legendary, WoweeItem::Weapon2H, 50, 70, 130, 195, 3000, "Doombringer");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeItem WoweeItemLoader::makeArmor(const std::string& catalogName) {
|
||||
WoweeItem c;
|
||||
c.name = catalogName;
|
||||
auto addArmor = [&](uint32_t id, uint32_t disp, uint8_t slot,
|
||||
uint16_t ilvl, int16_t stam, int16_t str_,
|
||||
const char* name) {
|
||||
WoweeItem::Entry e;
|
||||
e.itemId = id; e.displayId = disp;
|
||||
e.quality = WoweeItem::Uncommon;
|
||||
e.itemClass = WoweeItem::Armor;
|
||||
e.itemSubClass = 3; // mail
|
||||
e.inventoryType = slot;
|
||||
e.requiredLevel = 20; e.itemLevel = ilvl;
|
||||
e.sellPriceCopper = ilvl * 8u;
|
||||
e.buyPriceCopper = ilvl * 80u;
|
||||
e.maxStack = 1;
|
||||
e.durability = static_cast<uint16_t>(60 + ilvl);
|
||||
e.flags = WoweeItem::BindOnEquip;
|
||||
if (stam) e.stats.push_back({WoweeItem::StatStamina, stam});
|
||||
if (str_) e.stats.push_back({WoweeItem::StatStrength, str_});
|
||||
e.stats.push_back({WoweeItem::StatDefense,
|
||||
static_cast<int16_t>(ilvl / 5)});
|
||||
e.name = name;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
addArmor(2001, 2100, WoweeItem::Head, 25, 6, 4, "Iron Helm");
|
||||
addArmor(2002, 2101, WoweeItem::Chest, 25, 9, 6, "Iron Chestguard");
|
||||
addArmor(2003, 2102, WoweeItem::Legs, 25, 7, 5, "Iron Legguards");
|
||||
addArmor(2004, 2103, WoweeItem::Feet, 25, 4, 3, "Iron Boots");
|
||||
addArmor(2005, 2104, WoweeItem::Hands, 25, 4, 3, "Iron Gauntlets");
|
||||
addArmor(2006, 2105, WoweeItem::Cloak, 25, 5, 0, "Traveler's Cloak");
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -32,6 +32,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-spawns", "--gen-spawns-camp", "--gen-spawns-village",
|
||||
"--info-wspn", "--validate-wspn",
|
||||
"--export-wspn-json", "--import-wspn-json",
|
||||
"--gen-items", "--gen-items-weapons", "--gen-items-armor",
|
||||
"--info-wit", "--validate-wit",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
#include "cli_world_map.hpp"
|
||||
#include "cli_sound_catalog.hpp"
|
||||
#include "cli_spawns_catalog.hpp"
|
||||
#include "cli_items_catalog.hpp"
|
||||
#include "cli_quest_objective.hpp"
|
||||
#include "cli_quest_reward.hpp"
|
||||
#include "cli_clone.hpp"
|
||||
|
|
@ -119,6 +120,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleWorldMap,
|
||||
handleSoundCatalog,
|
||||
handleSpawnsCatalog,
|
||||
handleItemsCatalog,
|
||||
handleQuestObjective,
|
||||
handleQuestReward,
|
||||
handleClone,
|
||||
|
|
|
|||
|
|
@ -865,6 +865,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wspn to a human-editable JSON sidecar (defaults to <base>.wspn.json)\n");
|
||||
std::printf(" --import-wspn-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wspn.json sidecar back into binary .wspn (accepts either kind int OR kindName string)\n");
|
||||
std::printf(" --gen-items <wit-base> [name]\n");
|
||||
std::printf(" Emit .wit starter item catalog: 1 weapon + 1 chest + 1 potion + 1 quest item\n");
|
||||
std::printf(" --gen-items-weapons <wit-base> [name]\n");
|
||||
std::printf(" Emit .wit weapon catalog: 5 entries spanning common -> legendary, both 1H and 2H\n");
|
||||
std::printf(" --gen-items-armor <wit-base> [name]\n");
|
||||
std::printf(" Emit .wit full mail-armor set: head + chest + legs + feet + hands + cloak (BoE)\n");
|
||||
std::printf(" --info-wit <wit-base> [--json]\n");
|
||||
std::printf(" Print WIT item entries (id / ilvl / quality / class / slot / buy price / name)\n");
|
||||
std::printf(" --validate-wit <wit-base> [--json]\n");
|
||||
std::printf(" Static checks: itemId>0 + unique, weapon damage>0 + min<=max, equippable durability>0, sell<buy\n");
|
||||
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
|
||||
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
|
||||
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");
|
||||
|
|
|
|||
279
tools/editor/cli_items_catalog.cpp
Normal file
279
tools/editor/cli_items_catalog.cpp
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
#include "cli_items_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_items.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string stripWitExt(std::string base) {
|
||||
stripExt(base, ".wit");
|
||||
return base;
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweeItem& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweeItemLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wit\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweeItem& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wit\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" entries : %zu\n", c.entries.size());
|
||||
}
|
||||
|
||||
int handleGenStarter(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "StarterItems";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWitExt(base);
|
||||
auto c = wowee::pipeline::WoweeItemLoader::makeStarter(name);
|
||||
if (!saveOrError(c, base, "gen-items")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenWeapons(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "WeaponCatalog";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWitExt(base);
|
||||
auto c = wowee::pipeline::WoweeItemLoader::makeWeapons(name);
|
||||
if (!saveOrError(c, base, "gen-items-weapons")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenArmor(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "ArmorCatalog";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWitExt(base);
|
||||
auto c = wowee::pipeline::WoweeItemLoader::makeArmor(name);
|
||||
if (!saveOrError(c, base, "gen-items-armor")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void printPriceCopper(uint32_t copper) {
|
||||
uint32_t gold = copper / 10000;
|
||||
uint32_t silver = (copper / 100) % 100;
|
||||
uint32_t cop = copper % 100;
|
||||
std::printf("%ug %us %uc", gold, silver, cop);
|
||||
}
|
||||
|
||||
int handleInfo(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWitExt(base);
|
||||
if (!wowee::pipeline::WoweeItemLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WIT not found: %s.wit\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeItemLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wit"] = base + ".wit";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
nlohmann::json je;
|
||||
je["itemId"] = e.itemId;
|
||||
je["displayId"] = e.displayId;
|
||||
je["quality"] = e.quality;
|
||||
je["qualityName"] = wowee::pipeline::WoweeItem::qualityName(e.quality);
|
||||
je["itemClass"] = e.itemClass;
|
||||
je["itemClassName"] = wowee::pipeline::WoweeItem::classNameOf(e.itemClass);
|
||||
je["itemSubClass"] = e.itemSubClass;
|
||||
je["inventoryType"] = e.inventoryType;
|
||||
je["slotName"] = wowee::pipeline::WoweeItem::slotName(e.inventoryType);
|
||||
je["flags"] = e.flags;
|
||||
je["requiredLevel"] = e.requiredLevel;
|
||||
je["itemLevel"] = e.itemLevel;
|
||||
je["sellPriceCopper"] = e.sellPriceCopper;
|
||||
je["buyPriceCopper"] = e.buyPriceCopper;
|
||||
je["maxStack"] = e.maxStack;
|
||||
je["durability"] = e.durability;
|
||||
je["damageMin"] = e.damageMin;
|
||||
je["damageMax"] = e.damageMax;
|
||||
je["attackSpeedMs"] = e.attackSpeedMs;
|
||||
nlohmann::json sa = nlohmann::json::array();
|
||||
for (const auto& s : e.stats) {
|
||||
sa.push_back({
|
||||
{"type", s.type},
|
||||
{"typeName", wowee::pipeline::WoweeItem::statName(s.type)},
|
||||
{"value", s.value}
|
||||
});
|
||||
}
|
||||
je["stats"] = sa;
|
||||
je["name"] = e.name;
|
||||
je["description"] = e.description;
|
||||
arr.push_back(je);
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WIT: %s.wit\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 ilvl quality class slot buy name\n");
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf(" %4u %4u %-9s %-11s %-10s ",
|
||||
e.itemId, e.itemLevel,
|
||||
wowee::pipeline::WoweeItem::qualityName(e.quality),
|
||||
wowee::pipeline::WoweeItem::classNameOf(e.itemClass),
|
||||
wowee::pipeline::WoweeItem::slotName(e.inventoryType));
|
||||
printPriceCopper(e.buyPriceCopper);
|
||||
std::printf(" %s\n", 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 = stripWitExt(base);
|
||||
if (!wowee::pipeline::WoweeItemLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wit: WIT not found: %s.wit\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeItemLoader::load(base);
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::vector<uint32_t> idsSeen;
|
||||
idsSeen.reserve(c.entries.size());
|
||||
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.itemId);
|
||||
if (!e.name.empty()) ctx += " " + e.name;
|
||||
ctx += ")";
|
||||
if (e.itemId == 0) {
|
||||
errors.push_back(ctx + ": itemId is 0");
|
||||
}
|
||||
if (e.quality > wowee::pipeline::WoweeItem::Heirloom) {
|
||||
errors.push_back(ctx + ": quality " +
|
||||
std::to_string(e.quality) + " not in 0..7");
|
||||
}
|
||||
// Weapon class implies damage fields > 0 and a 1H/2H slot.
|
||||
if (e.itemClass == wowee::pipeline::WoweeItem::Weapon) {
|
||||
if (e.damageMin == 0 || e.damageMax == 0) {
|
||||
errors.push_back(ctx + ": weapon has zero damage");
|
||||
}
|
||||
if (e.damageMin > e.damageMax) {
|
||||
errors.push_back(ctx + ": damageMin > damageMax");
|
||||
}
|
||||
if (e.attackSpeedMs == 0) {
|
||||
errors.push_back(ctx + ": weapon has zero attackSpeedMs");
|
||||
}
|
||||
if (e.inventoryType != wowee::pipeline::WoweeItem::Weapon1H &&
|
||||
e.inventoryType != wowee::pipeline::WoweeItem::Weapon2H &&
|
||||
e.inventoryType != wowee::pipeline::WoweeItem::Ranged) {
|
||||
warnings.push_back(ctx + ": weapon has non-weapon inventoryType");
|
||||
}
|
||||
}
|
||||
// Equippable items should have non-zero durability (catches
|
||||
// common armor authoring oversight).
|
||||
if (e.inventoryType != wowee::pipeline::WoweeItem::NonEquip &&
|
||||
e.durability == 0) {
|
||||
warnings.push_back(ctx +
|
||||
": equippable item with durability=0");
|
||||
}
|
||||
// Stack-of-one items shouldn't have maxStack > 1
|
||||
// (unique-equip case is already guarded by the Unique flag).
|
||||
if (e.inventoryType != wowee::pipeline::WoweeItem::NonEquip &&
|
||||
e.maxStack > 1) {
|
||||
warnings.push_back(ctx +
|
||||
": equippable item with maxStack > 1");
|
||||
}
|
||||
// Buy price should be greater than sell price (vendor margin).
|
||||
if (e.buyPriceCopper > 0 && e.sellPriceCopper > 0 &&
|
||||
e.sellPriceCopper >= e.buyPriceCopper) {
|
||||
warnings.push_back(ctx +
|
||||
": sellPrice >= buyPrice (vendor would lose money)");
|
||||
}
|
||||
for (uint32_t prev : idsSeen) {
|
||||
if (prev == e.itemId) {
|
||||
errors.push_back(ctx + ": duplicate itemId");
|
||||
break;
|
||||
}
|
||||
}
|
||||
idsSeen.push_back(e.itemId);
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wit"] = base + ".wit";
|
||||
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-wit: %s.wit\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu items, all itemIds 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 handleItemsCatalog(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-items") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenStarter(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-items-weapons") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenWeapons(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-items-armor") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenArmor(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wit") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wit") == 0 && i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
11
tools/editor/cli_items_catalog.hpp
Normal file
11
tools/editor/cli_items_catalog.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleItemsCatalog(int& i, int argc, char** argv, int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue