Files
healer-man/src/game/enemyEquipment.ts
2026-10-06 22:25:34 -04:00

137 lines
5.6 KiB
TypeScript

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<Record<string, EnemyEquipmentSpawnAssignment>>;
readonly equipmentTemplatesByEntry: Readonly<
Record<string, Readonly<Record<string, EnemyEquipmentTemplateReference>>>
>;
readonly equipmentItemsById: Readonly<
Record<string, EnemyEquipmentItemReference | null>
>;
}
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<typeof enemyAutoEquippedOffhandContract>;
}
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 } : {}),
});
}