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

119 lines
5.1 KiB
TypeScript

import { creatureTemplateReference } from "./azerothCoreReference";
import {
ENEMY_DUAL_WIELD_FIRST_SWING_DELAY_FRACTION,
ENEMY_DUAL_WIELD_SIMULTANEOUS_SWING_DELAY_MS,
ENEMY_DUAL_WIELD_WHITE_MISS_PENALTY_PERCENT,
} from "./enemyExtraAttacks";
/** AzerothCore CREATURE_FLAG_EXTRA_USE_OFFHAND_ATTACK. */
export const CREATURE_FLAG_EXTRA_USE_OFFHAND_ATTACK = 0x0000_0800;
/** AzerothCore ITEM_CLASS_WEAPON, also stored in the client Item.dbc Class field. */
export const ENEMY_OFFHAND_ITEM_CLASS_WEAPON = 2;
/** Pinned world row carrying creature_template.flags_extra. */
export const ENEMY_AUTOMATIC_OFFHAND_WORLD_SOURCE = Object.freeze({
repository: "https://github.com/azerothcore/azerothcore-wotlk",
commit: "4d9d1d4b5723e819c5c390f6ca1177283dbfb8e3",
table: "creature_template",
path: "data/sql/base/db_world/creature_template.sql",
sha256: "0066e4fd35b809986dd325487515087f5a95f4ffe50ea03846c8d8bf5196870a",
});
/** Byte-identical local WotLK client/server Item.dbc files used for item Class lookups. */
export const ENEMY_AUTOMATIC_OFFHAND_ITEM_SOURCE = Object.freeze({
path: "../wow335a/Data/patch-A.MPQ/DBFilesClient/Item.dbc",
sha256: "22061a811523790ddd2ee95b36ade2ac6e0c79a8b3121324d32a639acf50567f",
classField: 1,
});
export interface EnemyAutomaticOffhandContract {
readonly entry: number;
readonly name: string;
readonly source: "creature-template-flag" | "auto-offhand-weapon";
/** `enabled` bypasses the equipment check; `auto` requires a valid weapon item. */
readonly dualWieldMode: "enabled" | "auto";
readonly requiresEquippedWeapon: boolean;
readonly attackType: "offhand";
/** Creature::CalculateMinMaxDamage uses the creature-generated hand range and AP formula. */
readonly baseDamageFrom: "same-creature-template-offhand-range-and-ap-formula";
/** UNIT_MOD_DAMAGE_OFFHAND TOTAL_PCT starts at 0.5 in the pinned core source. */
readonly damageMultiplier: 0.5;
/** Ordinary white swings add 19 points when an offhand channel is available. */
readonly whiteSwingMissPenaltyPercent: typeof ENEMY_DUAL_WIELD_WHITE_MISS_PENALTY_PERCENT;
readonly attackTimeFrom: "creature-template-base-attack-time";
readonly independentSwingTimer: true;
readonly firstSwingDelayFractionOnEngage: typeof ENEMY_DUAL_WIELD_FIRST_SWING_DELAY_FRACTION;
readonly simultaneousSwingDelayMs: typeof ENEMY_DUAL_WIELD_SIMULTANEOUS_SWING_DELAY_MS;
}
function makeContract(input: {
entry: number;
name: string;
source: EnemyAutomaticOffhandContract["source"];
dualWieldMode: EnemyAutomaticOffhandContract["dualWieldMode"];
requiresEquippedWeapon: boolean;
}): EnemyAutomaticOffhandContract {
return Object.freeze({
...input,
attackType: "offhand",
baseDamageFrom: "same-creature-template-offhand-range-and-ap-formula",
damageMultiplier: 0.5,
whiteSwingMissPenaltyPercent: ENEMY_DUAL_WIELD_WHITE_MISS_PENALTY_PERCENT,
attackTimeFrom: "creature-template-base-attack-time",
independentSwingTimer: true,
firstSwingDelayFractionOnEngage: ENEMY_DUAL_WIELD_FIRST_SWING_DELAY_FRACTION,
simultaneousSwingDelayMs: ENEMY_DUAL_WIELD_SIMULTANEOUS_SWING_DELAY_MS,
});
}
/**
* Resolves only the pinned creature-template USE_OFFHAND_ATTACK bit. This is
* an intrinsic creature capability, set by Creature::InitEntry/UpdateEntry;
* it survives combat, evade, and death state resets while the creature entry
* remains the same.
*/
export function enemyFlagDrivenOffhandContract(entry: number): EnemyAutomaticOffhandContract | undefined {
if (!Number.isSafeInteger(entry) || entry <= 0) return undefined;
const template = creatureTemplateReference(entry);
const flagsExtra = template?.flagsExtra;
if (!template || !Number.isSafeInteger(flagsExtra) || flagsExtra! < 0 || flagsExtra! > 0xffff_ffff
|| (flagsExtra! & CREATURE_FLAG_EXTRA_USE_OFFHAND_ATTACK) === 0) return undefined;
return makeContract({
entry: template.entry,
name: template.name,
source: "creature-template-flag",
dualWieldMode: "enabled",
requiresEquippedWeapon: false,
});
}
/**
* Resolves the default AUTO-mode offhand gate used by Creature::HasWeapon.
* `itemClass` must come from a successfully resolved client/server item row;
* class WEAPON (2) is the entire core gate after source virtual-item validation.
* Callers must resolve the pinned spawn selector and equipment set; model or
* display IDs and the selector itself are never treated as item IDs.
*/
export function enemyAutoEquippedOffhandContract(input: {
readonly entry: number;
readonly name?: string;
readonly offhandItemId: number;
readonly offhandItemClass: number | null | undefined;
}): EnemyAutomaticOffhandContract | undefined {
if (!Number.isSafeInteger(input.entry) || input.entry <= 0
|| !Number.isSafeInteger(input.offhandItemId) || input.offhandItemId <= 0
|| input.offhandItemClass !== ENEMY_OFFHAND_ITEM_CLASS_WEAPON) return undefined;
const template = creatureTemplateReference(input.entry);
if (!template) return undefined;
return makeContract({
entry: input.entry,
name: input.name?.trim() || template.name,
source: "auto-offhand-weapon",
dualWieldMode: "auto",
requiresEquippedWeapon: true,
});
}