forked from phenom/healer-man
137 lines
5.6 KiB
TypeScript
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 } : {}),
|
|
});
|
|
}
|