Use hybrid WoW-stat gear for five RoM builds
This commit is contained in:
1 parent
f4a7d33f74
commit
8e95480d4f
10 files changed
+134
-29
No files matched your search
@@ -3791,3 +3791,19 @@ Vite's existing large-chunk advisory remains informational.
|
||||
- These references are not equipped by the runtime, so this level-1 visual check
|
||||
confirms the existing character path still renders. The reference suite
|
||||
verifies the five class curves and source IDs at every level 1–79.
|
||||
|
||||
## Five-class RoM hybrid equipment (2026-09-23)
|
||||
|
||||
- Rebuilt and reran `node scripts/playtest-rom-five.cjs` after changing the five
|
||||
active heirloom layouts to legal RoM weapon combinations with WoW combat
|
||||
stats. The Rogue and Dwarf Mage roster previews showed complete bodies and
|
||||
starter blade/staff silhouettes. Wailing Caverns showed the player and
|
||||
companion bodies with visible blade, mace, shield, and staff silhouettes;
|
||||
there were no browser page errors. Evidence: `artifacts/playtest/rom-five/`
|
||||
`roster-preview.png`, `dwarf-mage-roster.png`, and `dungeon-rogue.png`.
|
||||
- At level 1, the screenshot does not confirm each member's full inventory or
|
||||
the level-10 secondary unlock. `src/game/romFiveValidation.test.ts` checks
|
||||
every hybrid weapon slot for legality and stat growth at levels 1, 20, 40,
|
||||
60, and 79. Targeted Shadoj reports at levels 20, 60, and 79 are under
|
||||
`artifacts/balance-lab/rom-hybrid-l*-shadoj/`; they exposed low Mage damage
|
||||
and a level-79 Priest death in each seed, so balance remains Needs Testing.
|
||||
@@ -16,7 +16,7 @@ The five named builds and live NPCs use these race-legal pairings. Their seconda
|
||||
|
||||
`src/game/romFiveIdentities.ts` owns these identities for the live NPC templates and targeted canonical builds. The manual party uses Priest as the active healer and the other four classes as companions. This explicit cohort does not change Ready-only random parties, bracket certification, or the companion readiness registry. Elite skills can appear in a paired kit through the existing RoM catalog, but their mechanics have not yet been validated for this cohort.
|
||||
|
||||
`npm run test:rom-five` checks the named pairings, legal races, level-10 unlock, role-specific heirloom layouts, ordinary ability coverage, and focused runtime mechanics. Knight is a tank seat in RoM generated parties. Rogue uses dual daggers, Knight a weapon and shield, and Scout a bow.
|
||||
`npm run test:rom-five` checks the named pairings, legal races, level-10 unlock, role-specific hybrid heirloom layouts and level growth, ordinary ability coverage, and focused runtime mechanics. Knight is a tank seat in RoM generated parties. The five loadouts use Knight mace/shield, Priest mace/shield, Scout dagger/bow, Rogue dual daggers, and Mage staff. Their equipment uses WoW combat stats while RoM skills and class-pair mechanics remain distinct.
|
||||
|
||||
## Findings and fixes in this pass
|
||||
|
||||
@@ -55,4 +55,4 @@ After the mechanics corrections, the targeted Shadoj run at level 60 with the sa
|
||||
|
||||
The RoM source catalog and generated data remain authoritative for ability descriptions and components. The recorded combat results cover primary-only builds; rerun targeted diagnostics before drawing conclusions about the new pairings. Elite skill validation is deferred.
|
||||
|
||||
RoM stat and equipment fidelity is a separate prerequisite for balance certification. `docs/ROM_STATS_AND_GEARING.md` records the source conversion coefficients, current WoW-shaped gear adapter, native item import gaps, and the players-only RoM online group boundary. Leveling bosses award cards but no items. `docs/ROM_LEVELING_GEAR_PACKAGES.md` records the source-confirmed level-50/55/60/65 four-choice quest packages, a level-5 three-choice quest, and an optional level-30 battlefield family; the actual package contents and native equip stats still need decoding before these become playable gear.
|
||||
The chosen gameplay model uses a WoW-stat hybrid instead of requiring native RoM equipment conversion for Ready promotion. `docs/ROM_STATS_AND_GEARING.md` records the source coefficients, active hybrid rules, remaining balance risks, and the players-only RoM online group boundary. Initial three-seed Shadoj screens at levels 20, 60, and 79 defeated the boss without a wipe, but Mage damage was far below the other two DPS builds and the Priest died once in each level-79 run. Those one-encounter results do not establish balance. Leveling bosses award cards but no items. `docs/ROM_LEVELING_GEAR_PACKAGES.md` records the source-confirmed package families; exact historical contents remain unverified and would need a WoW-stat translation before grant.
|
||||
@@ -1,6 +1,6 @@
|
||||
# RoM level 1–79 heirloom reference
|
||||
|
||||
Status: **source-backed design data, not equipped game items**. `scripts/runewaker-pipeline/export-rom-heirloom-reference.mjs` generates `src/game/generated/romHeirloomReference.json` from the sibling RuneWaker client snapshot, or from `RUNEWAKER_ROOT`. Run `node scripts/runewaker-pipeline/export-rom-heirloom-reference.mjs` after refreshing `src/game/generated/romLevelingGearPackages.json`. `src/game/romHeirloomReference.ts` provides typed level and item lookups. The 5 × 79 curves cover the five selected primary classes; their secondary class always shares the primary level.
|
||||
Status: **source-backed design data, separate from equipped hybrid heirlooms**. `scripts/runewaker-pipeline/export-rom-heirloom-reference.mjs` generates `src/game/generated/romHeirloomReference.json` from the sibling RuneWaker client snapshot, or from `RUNEWAKER_ROOT`. Run `node scripts/runewaker-pipeline/export-rom-heirloom-reference.mjs` after refreshing `src/game/generated/romLevelingGearPackages.json`. `src/game/romHeirloomReference.ts` provides typed level and item lookups. The 5 × 79 curves cover the five selected primary classes; their secondary class always shares the primary level.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
@@ -20,7 +20,7 @@ The parser uses the local `ObjectStruct.h` `GameObjectWeaponENUM`, `GameObjectAr
|
||||
| Rogue/Warrior | Leather | Dagger | Off-hand dagger |
|
||||
| Mage/Warlock | Cloth | Two-hand staff | None |
|
||||
|
||||
These choices follow the [official class equipment guide](https://archive.runesofmagic.gameforge.com/en/classes.html). Scout is **leather**, not chain. Priest can also legally use a wand or mace with a shield, but this client has only one complete blue wand record at level 57. The current Priest curve uses the well-populated staff series; the shield reference belongs to a separate future one-hand build and must not be combined with a two-hand staff. The Mage/Warlock remains dwarf; race has no effect on the equipment reference table itself.
|
||||
These source-reference choices follow the [official class equipment guide](https://archive.runesofmagic.gameforge.com/en/classes.html). Scout is **leather**, not chain. Priest can also legally use a wand or mace with a shield, but this client has only one complete blue wand record at level 57. The native Priest reference curve uses the well-populated staff series; the active WoW-stat hybrid equips a mace and shield to support the Priest/Knight pairing. A shield must not be combined with a two-hand staff. The Mage/Warlock remains dwarf; race has no effect on the equipment reference table itself.
|
||||
|
||||
The generator chooses a median item at each observed required level using role-oriented native-stat scores, then substitutes matching 50/55/60/65 package-suit armor pieces at those milestone levels. For levels between source points it interpolates each stat; below the first point or above the last it scales from the nearest source point. It rounds positive values to at least one and retains the prior level's value when the next candidate would lower a stat. `referenceItemIds` are the points behind that level, while `packageId` identifies a matched package-suit milestone. A curve row can therefore combine retained prior stats with current source values and **is not an exact copy of its listed item**. These scoring coefficients are selection heuristics, not certified combat stat weights.
|
||||
|
||||
@@ -36,6 +36,6 @@ All five profiles cover head, hands, feet, chest, legs, back, waist, shoulder, n
|
||||
|
||||
The [Gale Staff wiki entry](https://runesofmagic.fandom.com/wiki/Gale_Staff) provides an independent level-60 cross-check. More source IDs, including each 65 package chest, are queryable through `romSourceReferenceItem` and each curve's `referenceItemIds`.
|
||||
|
||||
## Before activating heirlooms
|
||||
## Use in the hybrid runtime
|
||||
|
||||
The current RoM runtime still generates WoW-shaped heirloom stats and converts RoM attributes with WoW formulas; see `docs/ROM_STATS_AND_GEARING.md`. Feeding these native RoM values directly into that path would produce meaningless balance. Implement the RoM equipment slots, class permissions, item-instance bonus layer, and native combat conversion first. Then tune one native-stat heirloom set by targeted Knight survival, Priest healing/sustain, and Scout/Rogue/Mage damage checks across levels 1–79. Keep dungeon bosses on the existing cards-only leveling policy; heirlooms and package gear should come from progression, not dungeon equipment drops. Elite skills remain out of scope.
|
||||
The active RoM runtime uses WoW-shaped heirloom stats and WoW combat formulas by design; see `docs/ROM_STATS_AND_GEARING.md`. These native source curves remain references for level progression and class identity. Feeding native values directly into the WoW path would produce meaningless balance. The five active hybrid loadouts use legal weapon layouts, a WoW-stat level budget, and no comparison against WoW boss drops. Tune Knight survival, Priest healing/sustain, and Scout/Rogue/Mage damage across levels 1–79. Keep dungeon bosses on the existing cards-only leveling policy; heirlooms and package gear should come from progression, not dungeon equipment drops. Elite skills remain out of scope.
|
||||
@@ -39,6 +39,6 @@ During leveling, `src/game/lootPolicy.ts` suppresses all boss item drops. Boss c
|
||||
## Gaps before playable package gear
|
||||
|
||||
1. Locate and verify package expansion records or scripts for every parent and subpackage, including weapon selections, accessories, exact armor items, and quantities. Matching a suit by name, category, and required level is not enough to grant its pieces as historical package contents.
|
||||
2. The source-backed 1–79 reference now decodes native equipment slots, weapon/armor type, base stats, and deterministic fixed attributes. Implement the separate item-instance and native-combat adapter before these become equippable heirlooms; the current RoM gear path still uses WoW-shaped synthetic heirlooms.
|
||||
2. The source-backed 1–79 reference now decodes native equipment slots, weapon/armor type, base stats, and deterministic fixed attributes. The active RoM path uses WoW-stat hybrid heirlooms; historical package pieces would need a defined translation into that stat budget and legal slot/weapon checks before they become equippable rewards. A full native item-instance and combat adapter is optional future fidelity work.
|
||||
3. Find a complete, generally available source-backed route from 1–49 and above level 65. Level 5 has only cloth, leather, and chain choices; level 30 has four battlefield choices. Logar/Hope package names exist in the item table, but their acquisition route and contents are not confirmed. Avoid inventing missing plate or weapon contents.
|
||||
4. Make the milestone grant idempotent and persistent, then verify class eligibility, package selection, leveling through each milestone, and native-stat combat outcomes for all five builds.
|
||||
@@ -1,12 +1,12 @@
|
||||
# RoM stat weights and gearing audit
|
||||
|
||||
Status: **source model and level 1–79 gear references identified; runtime conversion and equipment import pending**. This audit covers the Priest/Knight healer, Knight/Scout tank, Scout/Rogue DPS, Rogue/Warrior DPS, and Mage/Warlock DPS. It does not assign balance weights to unimplemented RoM gear. See `docs/ROM_HEIRLOOM_REFERENCE.md` for the client-derived blue-item atlas and proposed per-level native-stat curves.
|
||||
Status: **WoW-stat hybrid chosen for gameplay; native source model retained as reference**. This audit covers the Priest/Knight healer, Knight/Scout tank, Scout/Rogue DPS, Rogue/Warrior DPS, and Mage/Warlock DPS. The game keeps RoM abilities, class growth, resources, and class pairings, while equipment and final combat stats use the existing WoW-shaped model. See `docs/ROM_HEIRLOOM_REFERENCE.md` for the client-derived blue-item atlas and proposed native-stat curves; those numbers are reference points and are not applied directly to combat.
|
||||
|
||||
## Current runtime
|
||||
|
||||
- `src/game/romStats.ts` reads primary-class level growth from `romPlayerCatalog.generated.json` and gives the secondary class 10% of its base stats **at the same level as the primary**. This shared level is the game's progression rule, even though the historical source allowed separate class levels. The runtime also omits source race and sex additions.
|
||||
- `src/game/wotlkStats.ts` maps RoM Wisdom into WoW Spirit, then derives HP, Mana, attack power, spell power, avoidance, and ratings with WoW rules. This is a compatibility adapter, not RoM stat conversion. In particular, the global Mana multiplier, Stamina-to-HP rule, and `0.35 * Intellect + 0.15 * Spirit` spell-power rule are not source RoM formulas.
|
||||
- `src/game/specializationCatalog.ts` selects one of the WoW gear archetype weight tables for RoM. `src/game/heirloomEquipment.ts` uses WoW budgets and compares against WoW boss gear to create RoM heirlooms. The Scout currently receives a two-hand mace alongside a bow, although source Scout equipment allows one-hand swords, daggers, bows, and crossbows. `src/game/equipment.ts` also lacks the source Priest shield permission.
|
||||
- `src/game/wotlkStats.ts` maps RoM Wisdom into WoW Spirit, then derives HP, Mana, attack power, spell power, avoidance, and ratings with WoW rules. This is the selected compatibility adapter, not a historical RoM conversion. In particular, the global Mana multiplier, Stamina-to-HP rule, and `0.35 * Intellect + 0.15 * Spirit` spell-power rule are not source RoM formulas.
|
||||
- `src/game/specializationCatalog.ts` defines distinct hybrid gear profiles for the five builds. `src/game/heirloomEquipment.ts` gives these five WoW-stat heirlooms at levels 1–79 from a level budget without matching WoW boss drops. The legal layouts are Knight mace/shield, Priest mace/shield, Scout dagger/bow, Rogue dual daggers, and Mage staff. The five hybrid loadouts omit the WoW wrist slot; `src/game/equipment.ts` permits Priest shields. These are active gameplay gear, while the native source curves remain references. Other RoM classes retain their existing fallback gear path until separately validated.
|
||||
- `src/game/runewakerLootCatalog.ts` keeps imported RoM drops as non-equippable `misc` items with zero stats. The loot importer preserves names and probabilities but does not decode native equipment slots, base stats, or enhancements. `src/game/lootPolicy.ts` suppresses **all** boss item drops during leveling; boss cards are awarded through a separate path. At level 80, imported RoM boss items still lack usable equipment data.
|
||||
|
||||
## Source stat model
|
||||
@@ -38,18 +38,24 @@ The table below shows **vocation terms only** from the player records of `voctab
|
||||
|
||||
Priest healing cannot be scored by MATK alone: the source healing path also uses magic-power rate and heal-point inputs. Critical chance, weapon damage, resistance, and encounter mix further change the value of a stat. These conversion coefficients are mechanics, not final DPS/HPS weights.
|
||||
|
||||
## Equipment model to implement
|
||||
## Hybrid gameplay boundary
|
||||
|
||||
The five-class equipment path deliberately translates RoM roles into WoW combat inputs: Strength and Agility feed physical attack, Intelligence and Wisdom (as Spirit) feed spell and resource stats, defense/avoidance use armor and WoW ratings, and healing uses spell power and Mana sustain. The gear profile weights and heirloom budgets are design choices, not a claim that RoM native stats have the same numerical scale as WoW stats. The source item atlas can guide level progression and class identity without copying DEF, MDEF, MATK, Heal Point, or weapon damage into WoW fields one-for-one.
|
||||
|
||||
This reduces equipment implementation work but still requires targeted balance validation. A three-seed Shadoj diagnostic with the paired cohort and active Priest defeated the boss at levels 20, 60, and 79; Mage damage remained well below Scout/Rogue, and Priest died in each level-79 run. Reports are under `artifacts/balance-lab/rom-hybrid-l20-shadoj`, `rom-hybrid-l60-shadoj`, and `rom-hybrid-l79-shadoj`. They are screening runs against one encounter, not a balance certification. RoM remains Needs Testing and elite-skill validation remains deferred.
|
||||
|
||||
## Optional native-fidelity path
|
||||
|
||||
RoM equipment has base item properties plus additional attribute sources. [Gameforge's class guide](https://archive.runesofmagic.gameforge.com/en/classes.html) lists native class weapon/armor permissions. Its [rune guide](https://archive.runesofmagic.gameforge.com/us/runes.html) describes socketed runes and up to four rune slots; its [Arcane Transmutor guide](https://archive.runesofmagic.gameforge.com/en/arcanetransmutor.html) describes transferring attributes through Fusion and Mana Stones. These changes must be represented as item-instance bonuses rather than a WoW item-level budget. Source `RoleAttr.cpp` already distinguishes flat equipment contributions, attribute contributions, and multiplicative rates.
|
||||
|
||||
1. Use `scripts/runewaker-pipeline/export-rom-heirloom-reference.mjs` and `src/game/generated/romHeirloomReference.json` as the decoded source references for native slot, armor/weapon type, base effects, deterministic fixed attributes, and required level. Preserve item IDs and investigate the 727 records with unresolved attribute IDs and any special effects before turning a source item into equippable gear. The `src/game/romHeirloomReference.ts` lookup is design data and is not read by combat equipment yet.
|
||||
1. Use `scripts/runewaker-pipeline/export-rom-heirloom-reference.mjs` and `src/game/generated/romHeirloomReference.json` as decoded references for native slot, armor/weapon type, base effects, deterministic fixed attributes, and required level. Preserve item IDs and investigate the 727 records with unresolved attribute IDs and any special effects before importing historical source items. The `src/game/romHeirloomReference.ts` lookup is design data and is not read by combat equipment.
|
||||
2. Add a RoM item-instance bonus layer for native flat stats, transferred attributes, rune sockets, and later enhancement/tier data. Sum those bonuses into a native stat block. Keep WoW `ItemStats` and heirloom scoring unchanged for WoW characters.
|
||||
3. Extend the generated class data with race and sex arguments and the vocation conversion coefficients. Keep secondary growth tied to the primary's level, including after the level-10 secondary unlock. Convert the complete RoM stat block to combat inputs through a RoM-specific adapter, including ATK, MATK, DEF, MDEF, and separate healing inputs.
|
||||
4. Use legal starter equipment as a temporary bridge, then source-backed quest or item-shop equipment packages for leveling. The client snapshot confirms four armor-choice quest reward families at levels 50, 55, 60, and 65, a three-choice quest at level 5, and a battlefield reward at level 30; see `docs/ROM_LEVELING_GEAR_PACKAGES.md`. The native-stat reference curves fill the design gaps at every level 1–79 but are interpolated from source items and must be tuned in native combat. Resolve exact package expansion before granting historical items. Dungeon bosses award cards during leveling, not equipment or miscellaneous items. Do not tune a RoM heirloom to a WoW drop score or apply WoW rating conversions to it.
|
||||
4. If granting historical quest or item-shop packages later, resolve exact package expansion and translate their properties into the chosen WoW-stat budget or add a separate native item path. The client snapshot confirms armor-choice quest families at levels 5, 30, 50, 55, 60, and 65; see `docs/ROM_LEVELING_GEAR_PACKAGES.md`. Dungeon bosses award cards during leveling, not equipment or miscellaneous items. Do not copy native item numbers directly into WoW ratings.
|
||||
5. Derive role-specific *upgrade scores* from the marginal combat effect of each native stat at the current level and gear state. Measure Knight survival/threat, Priest effective HPS and sustain, and each DPS build's damage on the same targeted encounter set. Source coefficients seed that model; targeted combat measurements establish practical weights and catch caps or nonlinear effects.
|
||||
|
||||
## Online and validation boundary
|
||||
|
||||
`server/database.mjs` rejects a RoM/WoW source mixture when a grouped player selects a character and checks again when an activity starts. Until RoM AI companions are separately ready, online RoM groups launch players-only; the WoW-only randomized Bite-Sized Stockades activity is unavailable to them. This does not change local manual RoM party diagnostics. The five RoM profiles remain **Needs Testing** and elite skills remain deferred.
|
||||
|
||||
Validate each import with source-record fixture tests, legal slot/equip tests for all five builds, direct native-stat conversion cases, then `npm run test:rom-five` and targeted level/encounter diagnostics. Compare package upgrades by native stat deltas and combat outcomes before changing dungeon tuning. Keep a common encounter definition with source-specific stat translation and measured balance coefficients; no duplicate WoW/RoM dungeon catalogs are required by this audit.
|
||||
Validate the hybrid with legal slot/equip tests for all five builds, stat growth across 1–79, `npm run test:rom-five`, and targeted level/encounter diagnostics. Check Knight threat and survival, Priest effective healing and sustain, and the three DPS roles separately before promotion. If package rewards are added, compare their translated upgrades and combat outcomes. A common encounter definition can use source-specific tuning coefficients; no duplicate WoW/RoM dungeon catalogs are required by this audit.
|
||||
@@ -57,7 +57,7 @@ const ARMOR_RANK: Readonly<Record<string, number>> = Object.freeze({
|
||||
const SHIELD_CLASSES = new Set<ClassId>([
|
||||
"warrior", "paladin", "shaman",
|
||||
"knight-of-xoroth", "guardian", "cultist", "starcaller", "sun-cleric",
|
||||
"rom-warrior", "rom-knight", "rom-warden", "rom-champion",
|
||||
"rom-warrior", "rom-knight", "rom-priest", "rom-warden", "rom-champion",
|
||||
]);
|
||||
|
||||
const WEAPON_CLASSES: Readonly<Record<string, ReadonlySet<BaseClassId>>> = Object.freeze({
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { InventoryItem } from "./lootTypes";
|
||||
import type { PartyRole } from "./partyRoles";
|
||||
import {
|
||||
gearProfileFor,
|
||||
isRomHybridGearClass,
|
||||
specializationScore,
|
||||
type GearArchetype,
|
||||
type SpecializationDefinition,
|
||||
@@ -119,6 +120,8 @@ function weaponTypeFor(classId: ClassId, slot: EquipmentSlot, profile: Specializ
|
||||
if (slot === "off-hand" && profile.weaponLayout === "dual-wield") return "dagger";
|
||||
if (slot === "ranged") return profile.gearArchetype === "spell-dps" || profile.gearArchetype === "healer" ? "wand" : "bow";
|
||||
if (slot !== "main-hand") return null;
|
||||
if (classId === "rom-scout" && profile.weaponLayout === "ranged") return "dagger";
|
||||
if (classId === "rom-mage") return profile.weaponLayout === "two-hand" ? "staff" : "dagger";
|
||||
if (profile.weaponLayout === "caster") {
|
||||
return ["priest", "shaman", "druid", "witch-doctor", "chronomancer", "primalist", "venomancer"].includes(classId)
|
||||
? "mace"
|
||||
@@ -129,8 +132,8 @@ function weaponTypeFor(classId: ClassId, slot: EquipmentSlot, profile: Specializ
|
||||
return "mace";
|
||||
}
|
||||
|
||||
function inventorySlotFor(slot: EquipmentSlot, layout: WeaponLayout): string {
|
||||
if (slot === "main-hand") return layout === "two-hand" || layout === "ranged" ? "two-hand" : "one-hand";
|
||||
function inventorySlotFor(classId: ClassId, slot: EquipmentSlot, layout: WeaponLayout): string {
|
||||
if (slot === "main-hand") return layout === "two-hand" || (layout === "ranged" && classId !== "rom-scout") ? "two-hand" : "one-hand";
|
||||
return slot;
|
||||
}
|
||||
|
||||
@@ -212,13 +215,16 @@ function heirloomItem(
|
||||
if (cached) return cached;
|
||||
const armorType = armorTypeForSlot(context.classId, slot, layout);
|
||||
const weaponType = weaponTypeFor(context.classId, slot, { ...profile, weaponLayout: layout });
|
||||
const inventorySlot = inventorySlotFor(slot, layout);
|
||||
const inventorySlot = inventorySlotFor(context.classId, slot, layout);
|
||||
const weight = SLOT_WEIGHT[slot];
|
||||
const budget = Math.max(2, level * weight * 1.18);
|
||||
const armor = armorType && armorType !== "shield"
|
||||
? Math.max(1, Math.round(level * weight * ({ cloth: 1, leather: 1.35, mail: 1.7, chain: 1.7, plate: 2.1 }[armorType] ?? 1)))
|
||||
: armorType === "shield" ? Math.max(2, Math.round(level * weight * 3.2)) : 0;
|
||||
const sourceStats = statDistribution(profile.gearArchetype, budget, armor);
|
||||
const distributedStats = statDistribution(profile.gearArchetype, budget, armor);
|
||||
const sourceStats = armorType === "shield" && isRomHybridGearClass(context.classId)
|
||||
? { ...distributedStats, blockValue: Math.max(1, Math.round(level * weight * 0.45)) }
|
||||
: distributedStats;
|
||||
const sourceWeapon = weaponStats(slot, inventorySlot, weaponType, budget);
|
||||
let stats = itemStatsFor({
|
||||
itemId: 9_000_000,
|
||||
@@ -232,11 +238,11 @@ function heirloomItem(
|
||||
sourceWeapon,
|
||||
});
|
||||
const currentScore = specializationScore(stats, profile);
|
||||
const catalogScore = highestCatalogScore(context.classId, slot, level, profile)
|
||||
+ (slot === "main-hand" && ["two-hand", "ranged"].includes(layout)
|
||||
const catalogScore = isRomHybridGearClass(context.classId) ? 0 : highestCatalogScore(context.classId, slot, level, profile)
|
||||
+ (slot === "main-hand" && inventorySlot === "two-hand"
|
||||
? highestCatalogScore(context.classId, "off-hand", level, profile)
|
||||
: 0);
|
||||
const conflictSafety = slot === "main-hand" && ["two-hand", "ranged"].includes(layout) ? 1.05 : 1.02;
|
||||
const conflictSafety = slot === "main-hand" && inventorySlot === "two-hand" ? 1.05 : 1.02;
|
||||
const targetScore = catalogScore > 0 ? catalogScore * conflictSafety : currentScore;
|
||||
if (currentScore > 0 && Math.abs(targetScore - currentScore) > 0.01) stats = scaleStats(stats, targetScore / currentScore);
|
||||
const normalizedScore = specializationScore(stats, profile);
|
||||
@@ -277,7 +283,10 @@ export function resolvedEquipmentLoadout(
|
||||
): Readonly<Partial<Record<EquipmentSlot, InventoryItem>>> {
|
||||
const profile = gearProfileFor(context.classId, context.specializationId, context.role);
|
||||
const result: Partial<Record<EquipmentSlot, InventoryItem>> = {};
|
||||
for (const slot of [...BASE_SLOTS, ...weaponSlots(profile.weaponLayout)]) result[slot] = heirloomItem(context, profile, slot);
|
||||
for (const slot of [...BASE_SLOTS, ...weaponSlots(profile.weaponLayout)]) {
|
||||
if (isRomHybridGearClass(context.classId) && slot === "wrist") continue;
|
||||
result[slot] = heirloomItem(context, profile, slot);
|
||||
}
|
||||
const itemById = new Map(inventory.map((item) => [item.instanceId, item]));
|
||||
const earned = assignments[context.ownerId] ?? {};
|
||||
for (const slot of EQUIPMENT_SLOTS) {
|
||||
|
||||
@@ -3,20 +3,22 @@ import { abilitiesForCharacter, learnedAbilityForBuild } from "./abilityCatalog"
|
||||
import { isValidRomClassPair } from "../app/romDualClass";
|
||||
import { loadCanonicalBenchmarkBuilds } from "./benchmark/benchmarkBuilds";
|
||||
import { readinessStatusFor } from "./companionSpecReadiness";
|
||||
import { resolvedEquipmentLoadout } from "./heirloomEquipment";
|
||||
import { classCanEquipItem } from "./equipment";
|
||||
import { equipmentStatsForItems, resolvedEquipmentLoadout } from "./heirloomEquipment";
|
||||
import { partyRolesForClass } from "./partyRoles";
|
||||
import { createConfiguredPartyMember, createDungeonParty, createSpectatorDungeonParty, usePartyStore } from "./partyStore";
|
||||
import { ROM_ORDINARY_ABILITIES_BY_CLASS } from "./romAbilityCatalog";
|
||||
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
|
||||
import { romDualClassBaseStats, romNativeClassBaseStats } from "./romStats";
|
||||
import { romGearProfile } from "./specializationCatalog";
|
||||
import { deriveCharacterStats } from "./wotlkStats";
|
||||
|
||||
const CASES = [
|
||||
{ classId: "rom-priest", role: "healer", layout: "caster" },
|
||||
{ classId: "rom-priest", role: "healer", layout: "weapon-shield" },
|
||||
{ classId: "rom-knight", role: "tank", layout: "weapon-shield" },
|
||||
{ classId: "rom-scout", role: "damage", layout: "ranged" },
|
||||
{ classId: "rom-rogue", role: "damage", layout: "dual-wield" },
|
||||
{ classId: "rom-mage", role: "damage", layout: "caster" },
|
||||
{ classId: "rom-mage", role: "damage", layout: "two-hand" },
|
||||
] as const;
|
||||
|
||||
describe("RoM five class validation scope", () => {
|
||||
@@ -126,11 +128,55 @@ describe("RoM five class validation scope", () => {
|
||||
level: 60,
|
||||
});
|
||||
if (classId === "rom-knight") expect(loadout["off-hand"]?.armorType).toBe("shield");
|
||||
if (classId === "rom-scout") expect(loadout.ranged?.weaponType).toBe("bow");
|
||||
if (classId === "rom-priest") {
|
||||
expect(loadout["main-hand"]?.weaponType).toBe("mace");
|
||||
expect(loadout["off-hand"]?.armorType).toBe("shield");
|
||||
}
|
||||
if (classId === "rom-scout") {
|
||||
expect(loadout["main-hand"]?.weaponType).toBe("dagger");
|
||||
expect(loadout["main-hand"]?.inventorySlot).toBe("one-hand");
|
||||
expect(loadout.ranged?.weaponType).toBe("bow");
|
||||
}
|
||||
if (classId === "rom-rogue") {
|
||||
expect(loadout["main-hand"]?.weaponType).toBe("dagger");
|
||||
expect(loadout["off-hand"]?.weaponType).toBe("dagger");
|
||||
}
|
||||
if (classId === "rom-mage") {
|
||||
expect(loadout["main-hand"]?.weaponType).toBe("staff");
|
||||
expect(loadout["main-hand"]?.inventorySlot).toBe("two-hand");
|
||||
expect(loadout["off-hand"]).toBeUndefined();
|
||||
}
|
||||
expect(loadout.wrist).toBeUndefined();
|
||||
for (const item of Object.values(loadout)) expect(classCanEquipItem(classId, item!)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("grows the five hybrid combat loadouts through the heirloom levels", () => {
|
||||
for (const { classId, role } of CASES) {
|
||||
const identity = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === classId && entry.role === role)!;
|
||||
let priorHealth = 0;
|
||||
let priorPower = 0;
|
||||
for (const level of [1, 20, 40, 60, 79]) {
|
||||
const loadout = resolvedEquipmentLoadout({}, [], {
|
||||
ownerId: `rom-hybrid:${classId}`,
|
||||
ownerKind: "player",
|
||||
classId,
|
||||
specializationId: null,
|
||||
role,
|
||||
level,
|
||||
});
|
||||
const gear = equipmentStatsForItems(Object.values(loadout).filter((item) => item !== undefined));
|
||||
const combat = deriveCharacterStats(classId, level, gear, identity.raceId, {}, level >= 10 ? identity.secondaryClassId : null);
|
||||
const power = classId === "rom-scout" ? combat.rangedAttackPower
|
||||
: classId === "rom-rogue" || classId === "rom-knight" ? combat.attackPower
|
||||
: combat.spellPower;
|
||||
expect(combat.health, `${classId} health at ${level}`).toBeGreaterThan(priorHealth);
|
||||
expect(power, `${classId} power at ${level}`).toBeGreaterThan(priorPower);
|
||||
expect(Object.values(loadout).every((item) => item?.itemLevel === level)).toBe(true);
|
||||
if (classId === "rom-knight" || classId === "rom-priest") expect(combat.blockValue).toBeGreaterThan(0);
|
||||
priorHealth = combat.health;
|
||||
priorPower = power;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ describe("specialization registry", () => {
|
||||
});
|
||||
|
||||
it("gives the five RoM primary roles their appropriate weapon layouts", () => {
|
||||
expect(romGearProfile("rom-priest", "healer").weaponLayout).toBe("caster");
|
||||
expect(romGearProfile("rom-priest", "healer").weaponLayout).toBe("weapon-shield");
|
||||
expect(romGearProfile("rom-knight", "tank").weaponLayout).toBe("weapon-shield");
|
||||
expect(romGearProfile("rom-scout", "damage").weaponLayout).toBe("ranged");
|
||||
expect(romGearProfile("rom-rogue", "damage").weaponLayout).toBe("dual-wield");
|
||||
expect(romGearProfile("rom-mage", "damage").weaponLayout).toBe("caster");
|
||||
expect(romGearProfile("rom-mage", "damage").weaponLayout).toBe("two-hand");
|
||||
});
|
||||
});
|
||||
@@ -45,6 +45,23 @@ const WEIGHTS: Readonly<Record<GearArchetype, Readonly<Partial<Record<NumericIte
|
||||
"non-block-tank": Object.freeze({ agility: 0.66, strength: 0.28, stamina: 1, armor: 0.14, defenseRating: 0.78, dodgeRating: 0.86, parryRating: 0.46, expertiseRating: 0.4 }),
|
||||
});
|
||||
|
||||
const ROM_FIVE_HYBRID_PROFILES: Readonly<Partial<Record<ClassId, Readonly<{
|
||||
role: PartyRole;
|
||||
gearArchetype: GearArchetype;
|
||||
weaponLayout: WeaponLayout;
|
||||
statWeights: Readonly<Partial<Record<NumericItemStatKey, number>>>;
|
||||
}>>>> = Object.freeze({
|
||||
"rom-knight": Object.freeze({ role: "tank", gearArchetype: "block-tank", weaponLayout: "weapon-shield", statWeights: Object.freeze({ ...WEIGHTS["block-tank"], armor: 0.16, blockValue: 0.76 }) }),
|
||||
"rom-priest": Object.freeze({ role: "healer", gearArchetype: "healer", weaponLayout: "weapon-shield", statWeights: Object.freeze({ ...WEIGHTS.healer, spirit: 0.7, mp5: 0.95 }) }),
|
||||
"rom-scout": Object.freeze({ role: "damage", gearArchetype: "ranged-dps", weaponLayout: "ranged", statWeights: Object.freeze({ ...WEIGHTS["ranged-dps"], rangedAttackPower: 0.6 }) }),
|
||||
"rom-rogue": Object.freeze({ role: "damage", gearArchetype: "agility-dps", weaponLayout: "dual-wield", statWeights: Object.freeze({ ...WEIGHTS["agility-dps"], expertiseRating: 0.78 }) }),
|
||||
"rom-mage": Object.freeze({ role: "damage", gearArchetype: "spell-dps", weaponLayout: "two-hand", statWeights: Object.freeze({ ...WEIGHTS["spell-dps"], spellPower: 1.1 }) }),
|
||||
});
|
||||
|
||||
export function isRomHybridGearClass(classId: ClassId): boolean {
|
||||
return ROM_FIVE_HYBRID_PROFILES[classId] !== undefined;
|
||||
}
|
||||
|
||||
type SpecTuple = readonly [
|
||||
classId: ClassId,
|
||||
displayName: string,
|
||||
@@ -212,6 +229,17 @@ export function specializationScore(stats: ItemStats, definition: Specialization
|
||||
/** RoM has no saved specialization; derive a complete gear profile from primary class and activity role. */
|
||||
export function romGearProfile(classId: ClassId, role: PartyRole): SpecializationDefinition {
|
||||
const definition = classById(classId);
|
||||
const hybrid = ROM_FIVE_HYBRID_PROFILES[classId];
|
||||
if (hybrid?.role === role) return Object.freeze({
|
||||
id: `${classId}:role-${role}`,
|
||||
classId,
|
||||
displayName: `${definition.name} ${role}`,
|
||||
role,
|
||||
talentTree: "Primary class",
|
||||
gearArchetype: hybrid.gearArchetype,
|
||||
weaponLayout: hybrid.weaponLayout,
|
||||
statWeights: hybrid.statWeights,
|
||||
});
|
||||
const base = baseClassFor(classId);
|
||||
const caster = ["priest", "mage", "warlock"].includes(base) || definition.primaryStat === "intellect" || definition.primaryStat === "spirit" || definition.primaryStat === "wisdom";
|
||||
const gearArchetype: GearArchetype = role === "healer"
|
||||
|
||||
Reference in new issue
Block a user