import referenceData from "./generated/azerothCoreReference.json"; import { enemyAutoEquippedOffhandContract } from "./enemyAutomaticOffhand"; export interface EnemyEquipmentItemReference { readonly itemClass: number; readonly inventoryType: number; } export interface EnemyEquipmentTemplateReference { /** Raw SQL item IDs, before ObjectMgr's Item.dbc hand-slot validation. */ readonly sourceItems: readonly [number, number, number]; /** Virtual item IDs after the pinned core loader's Item.dbc validation. */ readonly items: readonly [number, number, number]; } export interface EnemyEquipmentSpawnAssignment { readonly entry: number; readonly alternateEntries: readonly number[]; /** creature.equipment_id selector: 0, a positive equipment ID, or -1. */ readonly selector: number; } export interface EnemyEquipmentSourceReference { readonly equipmentAssignmentsBySpawnGuid: Readonly>; readonly equipmentTemplatesByEntry: Readonly< Record>> >; readonly equipmentItemsById: Readonly< Record >; } export interface EnemySpawnEquipmentContract { readonly entry: number; readonly sourceCreatureGuid: number; /** Original creature.equipment_id selector from the pinned spawn row. */ readonly selector: number; /** Concrete equipment ID looked up by the source loader after -1 selection. */ readonly equipmentId: number; /** Item.dbc-sanitized UNIT_VIRTUAL_ITEM_SLOT_ID..+2 values. */ readonly virtualItemIds: readonly [number, number, number]; readonly offhandItemId: number; readonly offhandItemClass: number | null; readonly autoOffhand?: ReturnType; } interface AzerothCoreEquipmentReferenceShape { readonly creature: EnemyEquipmentSourceReference; } const REFERENCE = referenceData as unknown as AzerothCoreEquipmentReferenceShape; const EMPTY_ITEMS = Object.freeze([0, 0, 0]) as readonly [number, number, number]; /** * Resolves the source spawn's equipment selector. AzerothCore uses an exact * equipment ID for positive selectors, no items for zero, and for -1 chooses * a random ordered map row but rewrites the resulting key to ordinal + 1. * This deliberately returns no contract when that source-style follow-up * lookup fails (including sparse equipment IDs). */ export function enemySpawnEquipmentContract( entry: number, sourceCreatureGuid: number, randomUnit?: number, source: EnemyEquipmentSourceReference = REFERENCE.creature, ): EnemySpawnEquipmentContract | undefined { if (!Number.isSafeInteger(entry) || entry <= 0 || !Number.isSafeInteger(sourceCreatureGuid) || sourceCreatureGuid <= 0 || !source || !source.equipmentAssignmentsBySpawnGuid || !source.equipmentTemplatesByEntry || !source.equipmentItemsById) return undefined; const assignment = source.equipmentAssignmentsBySpawnGuid[String(sourceCreatureGuid)]; if (!assignment || !Array.isArray(assignment.alternateEntries) || ![assignment.entry, ...assignment.alternateEntries].includes(entry)) return undefined; const requestedTemplates = source.equipmentTemplatesByEntry[String(entry)]; const sourceVariantEntries = [...new Set([assignment.entry, ...assignment.alternateEntries])]; let equipmentId = assignment.selector; let equipment: EnemyEquipmentTemplateReference | undefined; if (assignment.selector === 0) { equipmentId = 0; } else if (assignment.selector === -1) { const sourceEntryTemplates = source.equipmentTemplatesByEntry[String(assignment.entry)]; if (!sourceEntryTemplates) return undefined; const availableIds = Object.keys(sourceEntryTemplates) .map(Number) .filter((id) => Number.isSafeInteger(id) && id > 0) .sort((left, right) => left - right); if (!availableIds.length || randomUnit === undefined || !Number.isFinite(randomUnit) || randomUnit < 0 || randomUnit >= 1) return undefined; // ObjectMgr::GetEquipmentInfo sets the mutable ID to distance(begin, row)+1, // not the selected map key. Creature::LoadEquipment then looks up that ID. equipmentId = Math.floor(randomUnit * availableIds.length) + 1; if (assignment.alternateEntries.some((variantEntry) => ( !source.equipmentTemplatesByEntry[String(variantEntry)]?.[String(equipmentId)] ))) return undefined; equipment = requestedTemplates?.[String(equipmentId)]; if (!equipment) return undefined; } else if (Number.isSafeInteger(assignment.selector) && assignment.selector > 0) { equipmentId = assignment.selector; if (sourceVariantEntries.some((variantEntry) => ( !source.equipmentTemplatesByEntry[String(variantEntry)]?.[String(equipmentId)] ))) return undefined; equipment = requestedTemplates?.[String(equipmentId)]; if (!equipment) return undefined; } else { return undefined; } const virtualItemIds = equipment?.items ?? EMPTY_ITEMS; if (virtualItemIds.length !== 3 || virtualItemIds.some((itemId) => !Number.isSafeInteger(itemId) || itemId < 0)) return undefined; const offhandItemId = virtualItemIds[1]; const offhandItemClass = offhandItemId > 0 ? source.equipmentItemsById[String(offhandItemId)]?.itemClass ?? null : null; const autoOffhand = enemyAutoEquippedOffhandContract({ entry, offhandItemId, offhandItemClass, }); return Object.freeze({ entry, sourceCreatureGuid, selector: assignment.selector, equipmentId, virtualItemIds: Object.freeze([...virtualItemIds]) as readonly [number, number, number], offhandItemId, offhandItemClass, ...(autoOffhand ? { autoOffhand } : {}), }); }