mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
feat(editor): add WIQR (Item Quality) open catalog format
Open replacement for the hardcoded item quality tiers in the
WoW client (Poor / Common / Uncommon / Rare / Epic / Legendary
/ Artifact / Heirloom). Defines each tier's tooltip text color,
inventory slot border color, vendor price multiplier, drop-level
gating, and disenchant eligibility.
The hardcoded client uses a fixed color table (gray/white/green/
blue/purple/orange/red/gold). This catalog lets server admins:
- retune the colors (rename "Epic" to "Tier 1" with custom hex)
- add server-custom tiers above Heirloom
- change vendor markup per tier (legendary 50x base price)
- gate quality drops by character level (Heirlooms unlock 80)
The standard preset reproduces the canonical 8-tier scale with
exact hex values from the live client (#9d9d9d through #00ccff)
and standard disenchant rules (Common+ disenchantable, Legendary
and Artifact aren't). The server-custom preset shows 4 tiers
above the standard range with non-standard pricing (Junk 0.1x,
QuestLocked 0.0x unsellable). The raid preset gates 4
progression tiers behind minLevelToDrop=60 with escalating
vendor multipliers up to 50x for Legendary.
Cross-references back to WIT — item entries reference qualityId
here for tooltip color and sort order. canDropAtLevel(id, lvl)
is the engine helper used by loot generation.
Validation enforces name presence, no duplicate ids,
vendorPriceMultiplier >= 0, minLevelToDrop <= maxLevelToDrop;
warns on:
- minLevelToDrop > 80 (unreachable at WotLK cap)
- vendorPriceMultiplier > 100x (sanity check the economy)
- nameColorRGBA with alpha=0 (text would be invisible in
tooltips — common bug when copy-pasting RGB hex without
alpha byte)
Wired through the cross-format table; WIQR appears automatically
in all 15 cross-format utilities. Format count 83 -> 84; CLI
flag count 1003 -> 1008.
This commit is contained in:
parent
18f07f1b8a
commit
efb88be366
10 changed files with 652 additions and 0 deletions
263
src/pipeline/wowee_item_qualities.cpp
Normal file
263
src/pipeline/wowee_item_qualities.cpp
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
#include "pipeline/wowee_item_qualities.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'I', 'Q', 'R'};
|
||||
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) != ".wiqr") {
|
||||
base += ".wiqr";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) {
|
||||
return (static_cast<uint32_t>(a) << 24) |
|
||||
(static_cast<uint32_t>(b) << 16) |
|
||||
(static_cast<uint32_t>(g) << 8) |
|
||||
static_cast<uint32_t>(r);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeItemQuality::Entry*
|
||||
WoweeItemQuality::findById(uint32_t qualityId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.qualityId == qualityId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WoweeItemQuality::canDropAtLevel(uint32_t qualityId,
|
||||
uint8_t characterLevel) const {
|
||||
const Entry* e = findById(qualityId);
|
||||
if (!e) return false;
|
||||
if (characterLevel < e->minLevelToDrop) return false;
|
||||
if (e->maxLevelToDrop != 0 && characterLevel > e->maxLevelToDrop)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WoweeItemQualityLoader::save(const WoweeItemQuality& 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.qualityId);
|
||||
writeStr(os, e.name);
|
||||
writeStr(os, e.description);
|
||||
writePOD(os, e.nameColorRGBA);
|
||||
writePOD(os, e.borderColorRGBA);
|
||||
writePOD(os, e.vendorPriceMultiplier);
|
||||
writePOD(os, e.minLevelToDrop);
|
||||
writePOD(os, e.maxLevelToDrop);
|
||||
writePOD(os, e.canBeDisenchanted);
|
||||
writePOD(os, e.pad0);
|
||||
writeStr(os, e.inventoryBorderTexture);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeItemQuality WoweeItemQualityLoader::load(
|
||||
const std::string& basePath) {
|
||||
WoweeItemQuality 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.qualityId)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readStr(is, e.name) || !readStr(is, e.description)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readPOD(is, e.nameColorRGBA) ||
|
||||
!readPOD(is, e.borderColorRGBA) ||
|
||||
!readPOD(is, e.vendorPriceMultiplier) ||
|
||||
!readPOD(is, e.minLevelToDrop) ||
|
||||
!readPOD(is, e.maxLevelToDrop) ||
|
||||
!readPOD(is, e.canBeDisenchanted) ||
|
||||
!readPOD(is, e.pad0)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readStr(is, e.inventoryBorderTexture)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeItemQualityLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeItemQuality WoweeItemQualityLoader::makeStandard(
|
||||
const std::string& catalogName) {
|
||||
using Q = WoweeItemQuality;
|
||||
WoweeItemQuality c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint8_t r, uint8_t g, uint8_t b,
|
||||
float vendorMul, uint8_t minLvl, uint8_t maxLvl,
|
||||
uint8_t disenchant, const char* texture,
|
||||
const char* desc) {
|
||||
Q::Entry e;
|
||||
e.qualityId = id; e.name = name; e.description = desc;
|
||||
e.nameColorRGBA = packRgba(r, g, b);
|
||||
e.borderColorRGBA = packRgba(r, g, b);
|
||||
e.vendorPriceMultiplier = vendorMul;
|
||||
e.minLevelToDrop = minLvl;
|
||||
e.maxLevelToDrop = maxLvl;
|
||||
e.canBeDisenchanted = disenchant;
|
||||
e.inventoryBorderTexture = texture;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
// The canonical WoW item quality scale, with hex colors
|
||||
// matching the live client. Heirlooms (id 7) are gated
|
||||
// to character level 80 in WotLK.
|
||||
add(0, "Poor", 0x9d, 0x9d, 0x9d, 0.5f, 1, 0, 0,
|
||||
"", "Poor (gray) — junk loot; vendor sells at half price.");
|
||||
add(1, "Common", 0xff, 0xff, 0xff, 1.0f, 1, 0, 1,
|
||||
"", "Common (white) — basic gear; standard vendor pricing.");
|
||||
add(2, "Uncommon", 0x1e, 0xff, 0x00, 1.5f, 1, 0, 1,
|
||||
"Border-Uncommon",
|
||||
"Uncommon (green) — early-tier quest reward; 50% markup.");
|
||||
add(3, "Rare", 0x00, 0x70, 0xdd, 2.0f, 1, 0, 1,
|
||||
"Border-Rare",
|
||||
"Rare (blue) — dungeon-tier; 2x markup, can be disenchanted.");
|
||||
add(4, "Epic", 0xa3, 0x35, 0xee, 4.0f, 60, 0, 1,
|
||||
"Border-Epic",
|
||||
"Epic (purple) — raid-tier; 4x markup, disenchants to high-tier dust.");
|
||||
add(5, "Legendary", 0xff, 0x80, 0x00, 8.0f, 60, 0, 0,
|
||||
"Border-Legendary",
|
||||
"Legendary (orange) — extremely rare; cannot be disenchanted.");
|
||||
add(6, "Artifact", 0xe6, 0xcc, 0x80, 16.0f, 80, 0, 0,
|
||||
"Border-Artifact",
|
||||
"Artifact (red-gold) — unique, account-bound.");
|
||||
add(7, "Heirloom", 0x00, 0xcc, 0xff, 1.0f, 80, 0, 0,
|
||||
"Border-Heirloom",
|
||||
"Heirloom (cyan) — scales to character level, lvl 80+ unlock.");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeItemQuality WoweeItemQualityLoader::makeServerCustom(
|
||||
const std::string& catalogName) {
|
||||
using Q = WoweeItemQuality;
|
||||
WoweeItemQuality c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint8_t r, uint8_t g, uint8_t b,
|
||||
float vendorMul, uint8_t disenchant,
|
||||
const char* desc) {
|
||||
Q::Entry e;
|
||||
e.qualityId = id; e.name = name; e.description = desc;
|
||||
e.nameColorRGBA = packRgba(r, g, b);
|
||||
e.borderColorRGBA = packRgba(r, g, b);
|
||||
e.vendorPriceMultiplier = vendorMul;
|
||||
e.canBeDisenchanted = disenchant;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
// 4 server-custom tiers above the standard 0..7 range.
|
||||
add(100, "Junk", 0x33, 0x33, 0x33, 0.1f, 0,
|
||||
"Server-custom: cosmetic junk, near-zero vendor price.");
|
||||
add(101, "Weekly", 0x80, 0xff, 0x80, 5.0f, 0,
|
||||
"Server-custom: drops only from weekly raids, "
|
||||
"premium pricing.");
|
||||
add(102, "QuestLocked",0xff, 0xff, 0x40, 0.0f, 0,
|
||||
"Server-custom: quest-bound, cannot be sold.");
|
||||
add(103, "Donator", 0xff, 0x40, 0xff, 0.0f, 0,
|
||||
"Server-custom: donor reward, soulbound, unsellable.");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeItemQuality WoweeItemQualityLoader::makeRaidTiers(
|
||||
const std::string& catalogName) {
|
||||
using Q = WoweeItemQuality;
|
||||
WoweeItemQuality c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint8_t r, uint8_t g, uint8_t b,
|
||||
float vendorMul, uint8_t minLvl,
|
||||
const char* desc) {
|
||||
Q::Entry e;
|
||||
e.qualityId = id; e.name = name; e.description = desc;
|
||||
e.nameColorRGBA = packRgba(r, g, b);
|
||||
e.borderColorRGBA = packRgba(r, g, b);
|
||||
e.vendorPriceMultiplier = vendorMul;
|
||||
e.minLevelToDrop = minLvl;
|
||||
e.canBeDisenchanted = 1;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
// Vanilla raid progression tiers as alternative quality
|
||||
// markers — each tier gates at a higher minLevelToDrop
|
||||
// and commands a higher vendor multiplier.
|
||||
add(200, "Tier1Raid", 0xa3, 0x35, 0xee, 4.0f, 60,
|
||||
"Tier 1 raid set (MC / Onyxia) — Epic-color, lvl 60.");
|
||||
add(201, "Tier2Raid", 0xc8, 0x4c, 0xff, 6.0f, 60,
|
||||
"Tier 2 raid set (BWL) — slightly brighter purple, "
|
||||
"premium pricing.");
|
||||
add(202, "Tier3Raid", 0xff, 0x80, 0xc8, 10.0f, 60,
|
||||
"Tier 3 raid set (Naxx pre-WotLK) — pink-orange, "
|
||||
"rarest pre-TBC tier.");
|
||||
add(203, "Legendary", 0xff, 0x80, 0x00, 50.0f, 60,
|
||||
"Tier-equivalent legendary (Thunderfury / Sulfuras / "
|
||||
"Atiesh) — premium economy pricing.");
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue