Update class mechanics, stat rulesets, and CoA reference catalogs

This commit is contained in:
phenom committed 2026-10-04 12:56:30 -04:00
1 parent b16821e92e
commit 39458d5de6
221 files changed
+2584962 -568

No files matched your search

+96
View File
@@ -4,10 +4,16 @@ import {
baseClassFor,
classById,
isRomClassId,
isRomRaceId,
type ClassId,
type GenderId,
type RaceId,
} from "../app/characterCatalog";
import { normalizeCharacterStatRuleset, type CharacterStatRuleset } from "./characterStatRuleset";
import { romDualClassBaseStats } from "./romStats";
import { applyRomNativeStatModifiers, deriveRomNativeStats, type RomNativeDerivedStats, type RomNativeStatModifiers } from "./romNativeStats";
import { romNativeGearFromSharedDirectStats } from "./romNativeStatsTypes";
import { ROM_SECONDARY_UNLOCK_LEVEL } from "../app/romDualClass";
import baseStatsData from "./generated/wow335BaseStats.json";
import combatRatingsData from "./generated/wow335CombatRatings.json";
import {
@@ -36,6 +42,10 @@ export interface CharacterStatPercentModifiers {
}
export interface CharacterStats extends PrimaryAttributes {
/** Source-unit RoM channels; values are never interpreted as WoW ratings. */
readonly romNative?: RomNativeDerivedStats;
/** Shared combat only: ATK/MDMG pass-through and DEF uses the WotLK armor curve. */
readonly combatStatModel?: "rom-native-normalized";
readonly level: number;
readonly health: number;
readonly mana: number;
@@ -286,6 +296,87 @@ function spiritManaPer5(classId: ClassId, level: number, intellect: number, spir
return 5 * Math.sqrt(Math.max(1, intellect)) * Math.max(0, spirit) * coefficient;
}
function nativeCharacterStats(
classId: Extract<ClassId, `rom-${string}`>,
level: number,
gear: ItemStats,
raceId: RaceId,
modifiers: CharacterStatPercentModifiers,
secondaryClassId: ClassId | null | undefined,
gender: GenderId,
): CharacterStats {
const nativeBase = deriveRomNativeStats({
classId,
level,
raceId: isRomRaceId(raceId) ? raceId : "rom-human",
gender,
secondaryClassId: safeLevel(level) >= ROM_SECONDARY_UNLOCK_LEVEL
&& secondaryClassId && isRomClassId(secondaryClassId) ? secondaryClassId : null,
// Native payload wins wholesale; direct legacy ItemStats calls receive
// only the verified flat-primary/HP/MP shared-stat fallback.
gear: gear.romNative ?? romNativeGearFromSharedDirectStats(gear),
});
const percentFactor = (value: number | undefined) => Math.max(0, 1 + (Number.isFinite(value) ? value! : 0) / 100);
const rates: NonNullable<RomNativeStatModifiers["rates"]> = {
strength: percentFactor(modifiers.strength),
stamina: percentFactor(modifiers.stamina),
agility: percentFactor(modifiers.agility),
intellect: percentFactor(modifiers.intellect),
health: percentFactor(modifiers.maxHealth),
mana: percentFactor(modifiers.maxMana),
};
const native = applyRomNativeStatModifiers(nativeBase, { rates });
const safe = safeLevel(level);
const physicalDefense = native.physicalDefense;
return Object.freeze({
level: safe,
strength: native.attributes.strength,
agility: native.attributes.agility,
stamina: native.attributes.stamina,
intellect: native.attributes.intellect,
// RoM Wisdom remains in the native sidecar; it is not WoW Spirit.
spirit: 0,
health: native.health.max,
mana: native.usesMana ? native.mana.max : 0,
healthPer5: 0,
manaPer5: 0,
attackPower: native.physicalAttack,
rangedAttackPower: native.physicalAttack,
spellPower: native.magicalDamage,
meleeCritPercent: 0,
rangedCritPercent: 0,
spellCritPercent: 0,
meleeHitPercent: 0,
rangedHitPercent: 0,
spellHitPercent: 0,
meleeHastePercent: 0,
rangedHastePercent: 0,
spellHastePercent: 0,
expertise: 0,
expertiseReductionPercent: 0,
armorPenetrationPercent: 0,
spellPenetration: 0,
armor: physicalDefense,
// Shared dungeon combat deliberately applies the existing WotLK armor curve to native DEF.
armorReductionPercent: armorReductionPercent(physicalDefense, safe),
defenseSkill: 0,
dodgePercent: 0,
parryPercent: 0,
blockPercent: 0,
blockValue: 0,
resiliencePercent: 0,
arcaneResistance: 0,
fireResistance: 0,
frostResistance: 0,
natureResistance: 0,
shadowResistance: 0,
meleeWeapon: gear.meleeWeapon,
rangedWeapon: gear.rangedWeapon,
romNative: native,
combatStatModel: "rom-native-normalized",
});
}
export function deriveCharacterStats(
classId: ClassId,
level: number,
@@ -293,8 +384,13 @@ export function deriveCharacterStats(
raceId: RaceId = "human",
modifiers: CharacterStatPercentModifiers = {},
secondaryClassId?: ClassId | null,
gender: GenderId = "male",
statRuleset: CharacterStatRuleset = "wow",
): CharacterStats {
const safe = safeLevel(level);
if (normalizeCharacterStatRuleset(statRuleset, classId) === "rom" && isRomClassId(classId)) {
return nativeCharacterStats(classId, safe, gear, raceId, modifiers, secondaryClassId, gender);
}
const hybridBase = romHybridBase(classId, secondaryClassId, safe);
const romBase = hybridBase ? null : romDualClassBaseStats(classId, secondaryClassId, safe);
const base = hybridBase?.attributes ?? (romBase ? {