fix(affix): 校验词缀classspecific职业限制并支持职业专属装备classlevelreq等级需求 (fixes #132)

This commit is contained in:
troytt 2026-09-19 10:51:55 +00:00
parent 8063c535ac
commit 308e16f955
6 changed files with 304 additions and 18 deletions

View File

@ -36,6 +36,9 @@ export interface RolledAffix {
readonly group: number
readonly level: number
readonly levelreq: number
readonly classspecific?: string | undefined
readonly class?: string | undefined
readonly classlevelreq?: number | undefined
readonly mods: readonly RolledMod[]
readonly id?: number | undefined
}
@ -259,6 +262,9 @@ function createRolledAffix(affix: MagicAffix, rng: D2Rng): RolledAffix {
group: affix.group,
level: affix.level,
levelreq: affix.levelreq,
...(affix.classspecific !== undefined ? { classspecific: affix.classspecific } : {}),
...(affix.class !== undefined ? { class: affix.class } : {}),
...(affix.classlevelreq !== undefined ? { classlevelreq: affix.classlevelreq } : {}),
mods,
id: (affix as any).id,
}
@ -294,6 +300,7 @@ function getEligibleRareNames(
* @param affixes - Tables containing prefix and suffix tables.
* @param isA - Item type inheritance predicate.
* @param rng - Unit PRNG instance (D2Rng).
* @param options - Optional eligibility overrides (e.g. itemClass).
* @returns Generated magic item affixes and metadata.
*/
export function rollMagicAffixes(
@ -302,6 +309,7 @@ export function rollMagicAffixes(
affixes: { prefixes: AffixTable; suffixes: AffixTable },
isA: Function,
rng: D2Rng,
options?: { readonly itemClass?: string | undefined },
): GeneratedAffixes {
const predicate = isA as IsAFunction
const initialSeed = rng.lo
@ -309,6 +317,13 @@ export function rollMagicAffixes(
const magicLvl = getBaseMagicLvl(base)
const alvl = calculateAlvl(ilvl, qlvl, magicLvl)
const itemTypeCode = getItemTypeCode(base)
const rawItemClass = options?.itemClass !== undefined ? options.itemClass : (base as any).itemClass ?? (base as any).class
const itemClass = typeof rawItemClass === 'string' ? rawItemClass.trim().toLowerCase() : undefined
const eligibilityOpts: AffixEligibilityOptions = {
alvl,
isRare: false,
...(itemClass !== undefined ? { itemClass } : {}),
}
const usedGroups = new Set<number>()
const usedAffixes = new Set<MagicAffix>()
@ -327,7 +342,7 @@ export function rollMagicAffixes(
affixes.prefixes,
itemTypeCode,
predicate,
{ alvl, isRare: false },
eligibilityOpts,
usedGroups,
usedAffixes,
)
@ -346,7 +361,7 @@ export function rollMagicAffixes(
affixes.suffixes,
itemTypeCode,
predicate,
{ alvl, isRare: false },
eligibilityOpts,
usedGroups,
usedAffixes,
)
@ -402,6 +417,7 @@ export function rollMagicAffixes(
* @param rareNames - Tables containing rare prefix and suffix name tables.
* @param isA - Item type inheritance predicate.
* @param rng - Unit PRNG instance (D2Rng).
* @param options - Optional eligibility overrides (e.g. itemClass).
* @returns Generated rare item affixes and metadata.
*/
export function rollRareAffixes(
@ -411,6 +427,7 @@ export function rollRareAffixes(
rareNames: { prefixes: RareNameTable; suffixes: RareNameTable },
isA: Function,
rng: D2Rng,
options?: { readonly itemClass?: string | undefined },
): GeneratedRareAffixes {
const predicate = isA as IsAFunction
const initialSeed = rng.lo
@ -418,6 +435,13 @@ export function rollRareAffixes(
const magicLvl = getBaseMagicLvl(base)
const alvl = calculateAlvl(ilvl, qlvl, magicLvl)
const itemTypeCode = getItemTypeCode(base)
const rawItemClass = options?.itemClass !== undefined ? options.itemClass : (base as any).itemClass ?? (base as any).class
const itemClass = typeof rawItemClass === 'string' ? rawItemClass.trim().toLowerCase() : undefined
const eligibilityOpts: AffixEligibilityOptions = {
alvl,
isRare: true,
...(itemClass !== undefined ? { itemClass } : {}),
}
// 1. Pick Rare Name: 1 eligible RarePrefix + 1 eligible RareSuffix
const eligibleRarePrefixes = getEligibleRareNames(rareNames.prefixes, itemTypeCode, predicate)
@ -456,7 +480,7 @@ export function rollRareAffixes(
affixes.prefixes,
itemTypeCode,
predicate,
{ alvl, isRare: true },
eligibilityOpts,
usedGroups,
usedAffixes,
)
@ -474,7 +498,7 @@ export function rollRareAffixes(
affixes.suffixes,
itemTypeCode,
predicate,
{ alvl, isRare: true },
eligibilityOpts,
usedGroups,
usedAffixes,
)

View File

@ -84,6 +84,41 @@ export interface AffixEligibilityOptions {
alvl?: number
/** Whether the target item is rare. */
isRare?: boolean
/** Class-specific 3-letter class code for the target item (e.g. 'ama', 'sor', 'nec', 'pal', 'bar', 'dru', 'ass'; '' for generic). */
readonly itemClass?: string | undefined
}
/**
* Canonical 1.13c mapping from 3-letter class code to ItemTypes.txt class-specific type codes.
*/
export const CLASS_SPECIFIC_ITEM_TYPE_MAP: Readonly<Record<string, readonly string[]>> = {
ama: ['amaz', 'abow', 'aspe', 'ajav'],
sor: ['sorc', 'orb'],
nec: ['necr', 'head'],
pal: ['pala', 'ashd'],
bar: ['barb', 'phlm'],
dru: ['drui', 'pelt'],
ass: ['assn', 'h2h', 'h2h2'],
}
/**
* Infers the 3-letter class code ('ama' | 'sor' | 'nec' | 'pal' | 'bar' | 'dru' | 'ass' | '')
* for an `itemTypeCode` using `isA` hierarchy checks.
*/
export function inferItemClassFromType(
itemTypeCode: string,
isA?: (type: string, target: string) => boolean,
): string {
const code = itemTypeCode.trim().toLowerCase()
if (!code) return ''
for (const [cls, targets] of Object.entries(CLASS_SPECIFIC_ITEM_TYPE_MAP)) {
for (const target of targets) {
if (code === target || (isA && isA(itemTypeCode, target))) {
return cls
}
}
}
return ''
}
/**
@ -121,13 +156,14 @@ export interface AffixTable {
* 1. `affix.spawnable` must be true.
* 2. If `options.isRare` is true, `affix.rare` must be true.
* 3. If `options.alvl` is provided, `affix.level <= options.alvl` and (`affix.maxlevel === 0 || options.alvl <= affix.maxlevel`).
* 4. Item must NOT match any `etype` (`isA(itemTypeCode, etype) === true`).
* 5. Item MUST match at least one `itype` (`isA(itemTypeCode, itype) === true`).
* 4. If `affix.classspecific` is non-empty, the item's effective class (`options.itemClass` or inferred via `isA`) must match `affix.classspecific`.
* 5. Item must NOT match any `etype` (`isA(itemTypeCode, etype) === true`).
* 6. Item MUST match at least one `itype` (`isA(itemTypeCode, itype) === true`).
*
* @param affix - The magic affix to test.
* @param itemTypeCode - Target item type code (e.g. 'swor', 'armo').
* @param isA - Item type hierarchy predicate: `(type, target) => boolean`.
* @param options - Optional filters (alvl and isRare).
* @param options - Optional filters (alvl, isRare, itemClass).
* @returns `true` if the affix is eligible.
*/
export function isAffixEligible(
@ -156,12 +192,24 @@ export function isAffixEligible(
}
}
// 4. Blacklist check: item must NOT match any etype
// 4. Class-specific restriction check (1.13c MagicPrefix.txt / MagicSuffix.txt `classspecific` column)
const requiredClass = affix.classspecific?.trim().toLowerCase()
if (requiredClass) {
const effectiveItemClass =
options?.itemClass !== undefined
? options.itemClass.trim().toLowerCase()
: inferItemClassFromType(itemTypeCode, isA)
if (effectiveItemClass !== requiredClass) {
return false
}
}
// 5. Blacklist check: item must NOT match any etype
if (affix.etypes.length > 0 && affix.etypes.some(etype => isA(itemTypeCode, etype))) {
return false
}
// 5. Whitelist check: item MUST match at least one itype
// 6. Whitelist check: item MUST match at least one itype
if (affix.itypes.length === 0 || !affix.itypes.some(itype => isA(itemTypeCode, itype))) {
return false
}

View File

@ -78,6 +78,7 @@ import {
import {
type AffixTable,
loadMagicAffixes,
inferItemClassFromType,
} from './affixes.ts'
import {
type RareNameTable,
@ -327,6 +328,7 @@ export interface ItemTypeFlags {
readonly isAlwaysMagic: boolean
readonly canBeRare: boolean
readonly isClassSpecific: boolean
readonly itemClass: string
}
const ALWAYS_MAGIC_BASE_IDS = new Set(['rin', 'amu', 'jew', 'cm1', 'cm2', 'cm3'])
@ -363,6 +365,7 @@ const CLASS_SPECIFIC_TYPE_CODES = new Set([
* Minimum quality for these items is always Magic (never Superior/Normal/Low).
* - `canBeRare`: delegates to `canItemBeRare(base, itemTypes, isAPredicate)`.
* - `isClassSpecific`: `def.class !== ''` or any `equiv1`/`equiv2` ancestor has `class !== ''`.
* - `itemClass`: First non-empty `def.class` along the `equiv1`/`equiv2` parent chain (or inferred via `isA`), '' for generic items.
*/
export function resolveItemTypeFlags(
base: ItemBase,
@ -390,8 +393,14 @@ export function resolveItemTypeFlags(
const canBeRare = canItemBeRare(base, itemTypes, isAPredicate)
let isClassSpecific = CLASS_SPECIFIC_TYPE_CODES.has(typeCode)
if (!isClassSpecific && itemTypes) {
let itemClass = ''
const rawBaseClass = (base as any).itemClass ?? (base as any).class
if (typeof rawBaseClass === 'string' && rawBaseClass.trim() !== '') {
itemClass = rawBaseClass.trim().toLowerCase()
}
let isClassSpecific = Boolean(itemClass) || CLASS_SPECIFIC_TYPE_CODES.has(typeCode)
if (itemTypes) {
const visited = new Set<string>()
const queue: string[] = [typeCode]
while (queue.length > 0) {
@ -400,18 +409,26 @@ export function resolveItemTypeFlags(
visited.add(curr)
if (CLASS_SPECIFIC_TYPE_CODES.has(curr)) {
isClassSpecific = true
break
}
const currDef = itemTypes.byCode.get(curr)
if (!currDef) continue
if (currDef.class && currDef.class.trim() !== '') {
isClassSpecific = true
if (!itemClass) {
itemClass = currDef.class.trim().toLowerCase()
}
break
}
if (currDef.equiv1) queue.push(currDef.equiv1)
if (currDef.equiv2) queue.push(currDef.equiv2)
}
}
if (!itemClass) {
itemClass = inferItemClassFromType(typeCode, isAPredicate)
if (itemClass) {
isClassSpecific = true
}
}
if (!isClassSpecific && isAPredicate) {
if (isAPredicate(typeCode, 'clas')) {
isClassSpecific = true
@ -423,6 +440,7 @@ export function resolveItemTypeFlags(
isAlwaysMagic,
canBeRare,
isClassSpecific,
itemClass,
}
}
@ -907,6 +925,7 @@ export function executeDropPipeline(
// Resolve ItemTypes.txt quality & class-specific flags
const typeFlags = resolveItemTypeFlags(effectiveBase, dropTables.itemTypes, dropTables.isA)
;(effectiveBase as any).itemClass = typeFlags.itemClass
// Check if the item can roll magical qualities
const canHaveQuality =
@ -1044,6 +1063,7 @@ export function executeDropPipeline(
dropTables.rareNames,
dropTables.isA,
itemRng,
{ itemClass: typeFlags.itemClass },
)
if (rareAffixes && rareAffixes.affixes && rareAffixes.affixes.length > 0) {
const autoPrefixGroup = resolveAutoPrefixGroup(effectiveBase, dropTables.itemTypes, dropTables.isA)
@ -1092,6 +1112,7 @@ export function executeDropPipeline(
dropTables.magicAffixes,
dropTables.isA,
itemRng,
{ itemClass: typeFlags.itemClass },
)
const autoPrefixGroup = resolveAutoPrefixGroup(effectiveBase, dropTables.itemTypes, dropTables.isA)
const autoMagicAffix =

View File

@ -13,8 +13,10 @@
import type { Item, ItemBase, RolledItemProp } from './items.ts'
import type { UniqueItem, UniqueItemProp } from './unique-items.ts'
import type { SetItem, SetItemProp } from './set-items.ts'
import { inferItemClassFromType } from './affixes.ts'
import {
NON_RANDOM_RANGE_CODES,
getItemTypeCode,
type GeneratedAffixes,
type GeneratedRareAffixes,
type RolledAffix,
@ -1510,6 +1512,38 @@ export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]):
.map(x => x.item)
}
function resolveItemClass(item: Item): string {
const base = item.base
const explicit = (item as any).itemClass ?? (base as any).itemClass ?? (base as any).class
if (typeof explicit === 'string' && explicit.trim() !== '') {
return explicit.trim().toLowerCase()
}
const fromType = inferItemClassFromType(getItemTypeCode(base))
if (fromType) return fromType
if (base.tags && base.tags.length > 0) {
for (const tag of base.tags) {
const fromTag = inferItemClassFromType(tag)
if (fromTag) return fromTag
}
}
return ''
}
function getAffixEffectiveLevelReq(aff: RolledAffix, itemClass: string): number {
const affClass = (aff.class ?? aff.affix?.class ?? '').trim().toLowerCase()
const affClassLevelReq = aff.classlevelreq ?? aff.affix?.classlevelreq
if (
itemClass &&
affClass &&
affClass === itemClass &&
affClassLevelReq !== undefined &&
affClassLevelReq > 0
) {
return affClassLevelReq
}
return aff.levelreq
}
/**
* Generates formatted tooltip structure for any dropped Diablo II Item.
*/
@ -1517,6 +1551,7 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
const base = item.base
const quality = String(item.rarity || item.quality || 'normal').toLowerCase()
const ilvl = item.level ?? item.ilvl ?? 1
const itemClass = resolveItemClass(item)
let title = item.name
let subTitle: string | undefined = undefined
@ -1629,7 +1664,8 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
if (rareDef.name) title = rareDef.name
if (rareDef.affixes) {
for (const aff of rareDef.affixes) {
if (aff.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, aff.levelreq)
const effReq = getAffixEffectiveLevelReq(aff, itemClass)
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
}
}
}
@ -1664,11 +1700,19 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
const magicDef = item.rolledMagicAffixes as GeneratedAffixes | undefined
if (magicDef) {
if (magicDef.name) title = magicDef.name
if (magicDef.prefix && magicDef.prefix.levelreq > 0) {
affixReqLevel = Math.max(affixReqLevel, magicDef.prefix.levelreq)
if (magicDef.prefix) {
const effReq = getAffixEffectiveLevelReq(magicDef.prefix, itemClass)
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
}
if (magicDef.suffix && magicDef.suffix.levelreq > 0) {
affixReqLevel = Math.max(affixReqLevel, magicDef.suffix.levelreq)
if (magicDef.suffix) {
const effReq = getAffixEffectiveLevelReq(magicDef.suffix, itemClass)
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
}
if (magicDef.affixes) {
for (const aff of magicDef.affixes) {
const effReq = getAffixEffectiveLevelReq(aff, itemClass)
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
}
}
}
if (item.rolledProps && item.rolledProps.length > 0) {

View File

@ -169,6 +169,39 @@ describe('isAffixEligible helper with mock isA', () => {
const noItypes = { ...sampleAffix, itypes: [] }
expect(isAffixEligible(noItypes, 'swor', mockIsA, { alvl: 30 })).toBe(false)
})
it('enforces classspecific restriction against explicit options.itemClass and inferred itemTypeCode class', () => {
const classIsA = (type: string, target: string): boolean => {
if (type === target) return true
if ((type === 'swor' || type === 'abow' || type === 'orb') && target === 'weap') return true
if (type === 'abow' && target === 'amaz') return true
if (type === 'orb' && target === 'sorc') return true
return false
}
const amaSpecificAffix: MagicAffix = {
...sampleAffix,
name: 'Maiden\'s',
classspecific: 'ama',
class: 'ama',
levelreq: 36,
classlevelreq: 27,
itypes: ['weap'],
etypes: [],
}
// Generic sword ('swor'): must be false (both inferred and explicit itemClass: '')
expect(isAffixEligible(amaSpecificAffix, 'swor', classIsA, { alvl: 30 })).toBe(false)
expect(isAffixEligible(amaSpecificAffix, 'swor', classIsA, { alvl: 30, itemClass: '' })).toBe(false)
// Sorceress orb ('orb' / itemClass: 'sor'): must be false
expect(isAffixEligible(amaSpecificAffix, 'orb', classIsA, { alvl: 30 })).toBe(false)
expect(isAffixEligible(amaSpecificAffix, 'orb', classIsA, { alvl: 30, itemClass: 'sor' })).toBe(false)
// Amazon bow ('abow' / itemClass: 'ama'): must be true
expect(isAffixEligible(amaSpecificAffix, 'abow', classIsA, { alvl: 30 })).toBe(true)
expect(isAffixEligible(amaSpecificAffix, 'abow', classIsA, { alvl: 30, itemClass: 'ama' })).toBe(true)
})
})
describe('Magic Affixes Integration with 1.13c MPQ Archives', () => {

View File

@ -20,7 +20,7 @@ import {
} from '../src/game/affix-generator.ts'
import { formatItemTooltip, aggregateAndSortProperties } from '../src/game/item-tooltip.ts'
import { D2Rng } from '../src/game/d2-rng.ts'
import type { MagicAffix, AffixTable } from '../src/game/affixes.ts'
import { isAffixEligible, type MagicAffix, type AffixTable } from '../src/game/affixes.ts'
import type { RareNameTable } from '../src/game/rare-names.ts'
import type { UpgradableItemBase } from '../src/game/item-upgrade.ts'
import type { Item, ItemBase } from '../src/game/items.ts'
@ -1010,4 +1010,120 @@ describe('Superior, Low Quality, Ethereal & Sockets, and Rare Slot-by-Slot Alloc
})
})
describe('Affix classspecific Restriction & classlevelreq Tooltip Level Requirement (Issue #132)', () => {
let dropTables: DropTables
beforeAll(() => {
dropTables = getEmbeddedDropTables()
})
it('1. isAffixEligible blocks classspecific="ama" on generic sword (swor) and other class items (orb / sor), allowing only Amazon items (abow / ama)', () => {
const amaPrefix: MagicAffix = {
name: 'Valkyrie\'s',
isPrefix: true,
version: 100,
spawnable: true,
rare: true,
level: 50,
maxlevel: 0,
levelreq: 42,
classspecific: 'ama',
class: 'ama',
classlevelreq: 42,
frequency: 5,
group: 125,
mods: [{ code: 'ama', min: 2, max: 2 }],
transform: false,
itypes: ['weap'],
etypes: [],
}
// Generic weapon ('swor'): blocked both via inferred type and explicit itemClass: ''
expect(isAffixEligible(amaPrefix, 'swor', dropTables.isA, { alvl: 60 })).toBe(false)
expect(isAffixEligible(amaPrefix, 'swor', dropTables.isA, { alvl: 60, itemClass: '' })).toBe(false)
// Sorceress orb ('orb'): blocked both via inferred type and explicit itemClass: 'sor'
expect(isAffixEligible(amaPrefix, 'orb', dropTables.isA, { alvl: 60 })).toBe(false)
expect(isAffixEligible(amaPrefix, 'orb', dropTables.isA, { alvl: 60, itemClass: 'sor' })).toBe(false)
// Amazon bow ('abow'): allowed both via inferred type and explicit itemClass: 'ama'
expect(isAffixEligible(amaPrefix, 'abow', dropTables.isA, { alvl: 60 })).toBe(true)
expect(isAffixEligible(amaPrefix, 'abow', dropTables.isA, { alvl: 60, itemClass: 'ama' })).toBe(true)
// Verify resolveItemTypeFlags returns exact 3-letter itemClass for all 7 classes and '' for generic items
expect(resolveItemTypeFlags(dropTables.getBase('am1')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('ama')
expect(resolveItemTypeFlags(dropTables.getBase('ob1')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('sor')
expect(resolveItemTypeFlags(dropTables.getBase('ne1')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('nec')
expect(resolveItemTypeFlags(dropTables.getBase('pa1')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('pal')
expect(resolveItemTypeFlags(dropTables.getBase('ba1')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('bar')
expect(resolveItemTypeFlags(dropTables.getBase('dr1')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('dru')
expect(resolveItemTypeFlags(dropTables.getBase('ktr')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('ass')
expect(resolveItemTypeFlags(dropTables.getBase('crs')!, dropTables.itemTypes, dropTables.isA).itemClass).toBe('')
})
it('2. formatItemTooltip uses classlevelreq when affix.class matches the equipment itemClass, and levelreq on generic items', () => {
// Skilltab affix with levelreq=40 on generic items (e.g. gloves/circlet) but classlevelreq=30 on Amazon items (e.g. Stag Bow)
const bowSkillPrefix: MagicAffix = {
name: 'Fletcher\'s',
isPrefix: true,
version: 100,
spawnable: true,
rare: true,
level: 20,
maxlevel: 0,
levelreq: 40,
class: 'ama',
classlevelreq: 30,
frequency: 10,
group: 125,
mods: [{ code: 'skilltab', param: '0', min: 1, max: 1 }],
transform: false,
itypes: ['weap', 'miss', 'abow', 'swor'],
etypes: [],
}
const stagBowBase = { ...dropTables.getBase('am1')!, levelreq: 14, itemClass: 'ama' } as ItemBase
const crystalSwordBase = { ...dropTables.getBase('crs')!, levelreq: 11, itemClass: '' } as ItemBase
const prefixesTable = { all: [bowSkillPrefix] } as unknown as AffixTable
const suffixesTable = { all: [] } as unknown as AffixTable
// Find seed that rolls prefix
let rolledOnAmaBow = rollMagicAffixes(stagBowBase, 50, { prefixes: prefixesTable, suffixes: suffixesTable }, dropTables.isA, new D2Rng(1))
let rolledOnGenericSword = rollMagicAffixes(crystalSwordBase, 50, { prefixes: prefixesTable, suffixes: suffixesTable }, dropTables.isA, new D2Rng(1))
for (let s = 1; s <= 20; s++) {
const c1 = rollMagicAffixes(stagBowBase, 50, { prefixes: prefixesTable, suffixes: suffixesTable }, dropTables.isA, new D2Rng(s))
const c2 = rollMagicAffixes(crystalSwordBase, 50, { prefixes: prefixesTable, suffixes: suffixesTable }, dropTables.isA, new D2Rng(s))
if (c1.prefix && c2.prefix) {
rolledOnAmaBow = c1
rolledOnGenericSword = c2
break
}
}
expect(rolledOnAmaBow.prefix?.class).toBe('ama')
expect(rolledOnAmaBow.prefix?.classlevelreq).toBe(30)
expect(rolledOnAmaBow.prefix?.levelreq).toBe(40)
const amaBowItem = createDroppedItem(stagBowBase, 'magic', {
magicAffixes: rolledOnAmaBow,
ilvl: 50,
dwInitSeed: 13201,
})
const genericSwordItem = createDroppedItem(crystalSwordBase, 'magic', {
magicAffixes: rolledOnGenericSword,
ilvl: 50,
dwInitSeed: 13202,
})
// On Amazon Stag Bow (itemClass: 'ama'), affix.class === 'ama' -> uses classlevelreq (30) instead of levelreq (40)
const amaTooltip = formatItemTooltip(amaBowItem)
expect(amaTooltip.reqLevel).toBe(30)
// On generic Crystal Sword (itemClass: ''), uses standard levelreq (40)
const swordTooltip = formatItemTooltip(genericSwordItem)
expect(swordTooltip.reqLevel).toBe(40)
})
})