diablo2-web/src/common/skills/missiles-data.ts

5137 lines
122 KiB
TypeScript

import type { CharacterClassCode } from '../units/classes.ts'
import type { CombatPlayer } from '../units/unit-types.ts'
import {
calculateMagicArrowStats,
calculateFireArrowStats,
calculateColdArrowStats,
calculateMultipleShotStats,
calculateExplodingArrowStats,
calculateIceArrowStats,
calculateGuidedArrowStats,
calculateStrafeStats,
calculateImmolationArrowStats,
calculateFreezingArrowStats,
} from './amazon-bow.ts'
import {
calculateInnerSightStats,
calculateCriticalStrikeStats,
calculateDodgeStats,
calculateSlowMissilesStats,
calculateAvoidStats,
calculatePenetrateStats,
calculateDecoyStats,
calculateEvadeStats,
calculateValkyrieStats,
calculatePierceStats,
} from './amazon-passive.ts'
import {
calculateJabStats,
calculatePowerStrikeStats,
calculatePoisonJavelinStats,
calculateImpaleStats,
calculateLightningBoltStats,
calculateChargedStrikeStats,
calculatePlagueJavelinStats,
calculateFendStats,
calculateLightningStrikeStats,
calculateLightningFuryStats,
} from './amazon-javelin-spear.ts'
/** One castable skill, read from a table row. */
export interface SkillDef {
/** Table id. */
readonly id: string
/** Display name, resolved through the text table when the row names an index. */
readonly name: string
/** Mana spent per cast. */
readonly manaCost: number
/** Ticks between casts. */
readonly cooldownTicks: number
/** Maximum cast distance; also the projectile's lifetime in pixels. */
readonly range: number
/** Whether the skill spawns a projectile (as opposed to striking instantly). */
readonly projectile: boolean
/** Projectile speed in pixels per second. */
readonly speed: number
/** Damage at skill level 1. */
readonly baseMinDamage: number
/** Damage at skill level 1. */
readonly baseMaxDamage: number
/** Damage added per additional skill level. */
readonly damagePerLevel: number
/** Radius of the instant effect, for non-projectile skills. */
readonly radius: number
/** Cast overlay identifier from Skills.txt castoverlay column (e.g. 'fire_cast_1'). */
readonly castOverlay?: string | undefined
}
/** A projectile in flight. */
export interface Projectile {
/** Skill it came from. */
readonly skillId: string
/** World x. */
readonly x: number
/** World y. */
readonly y: number
/** Velocity x, pixels per tick. */
readonly vx: number
/** Velocity y, pixels per tick. */
readonly vy: number
/** Damage on hit. */
readonly damage: number
/** Ticks left before it expires. */
readonly ttl: number
/** Whether the player fired it (projectiles hit the other side). */
readonly fromPlayer: boolean
/** Optional missile type / token (e.g. 'firebolt'). */
readonly missileType?: string | undefined
/** Pierces through enemies without expiring until range or wall hit. */
readonly pierce?: boolean | undefined
/** Set of monster indices already hit by this projectile (for pierce/nextHit). */
readonly hitTargets?: Set<number> | undefined
/** Area of effect splash radius in pixels upon impact. */
readonly aoeRadius?: number | undefined
/** Elemental status effect applied on hit: chill, freeze, poison. */
readonly statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
/** Duration in ticks of the status effect. */
readonly statusDuration?: number | undefined
/** Homing missile: steers towards nearest alive monster. */
readonly homing?: boolean | undefined
/** Archimedean spiral trajectory (e.g. Blessed Hammer). */
readonly spiral?: {
readonly originX: number
readonly originY: number
readonly angle: number
readonly radius: number
} | undefined
/** Random perpendicular trajectory wobble (e.g. Charged Bolt). */
readonly jitter?: boolean | undefined
/** PRNG seed for projectile variance (e.g. Charged Bolt). */
readonly seed?: number | undefined
/** Current index in the discrete orthogonal jitter table (0-31). */
readonly jitterIndex?: number | undefined
/** Target restriction: only hits undead monsters. */
readonly isUndeadOnly?: boolean | undefined
/** Sub-missile emission index along the 64-point discrete circle table (e.g. Frozen Orb). */
readonly subMissileIdx?: number | undefined
/** Affine curl damping trajectory in last 6 frames (e.g. Frozen Orb Nova). */
readonly isNovaCurl?: boolean | undefined
/** Stationary turret emitter (e.g. Hydra, Skill 62). */
readonly isTurret?: boolean | undefined
/** Fire cadence interval in ticks for turret emitters (e.g. 25). */
readonly fireCadenceTicks?: number | undefined
/** Head index within multi-head cluster (e.g. 0, 1, 2). */
readonly headIndex?: number | undefined
/** Pre-configured target world coordinate when targets array is empty. */
readonly targetX?: number | undefined
readonly targetY?: number | undefined
/** Missile type spawned by turret (defaults to 'hydra'). */
readonly spawnMissileType?: string | undefined
/** Number of elapsed ticks this projectile has lived. */
readonly ageTicks?: number | undefined
/** Altitude above ground in pixels for falling missiles (e.g. Blizzard shards). */
readonly altitude?: number | undefined
/** Skill level of the source skill (for dynamic duration/damage scaling). */
readonly slvl?: number | undefined
/** Residual ground fire duration in ticks. */
readonly fireDuration?: number | undefined
/** Residual ground fire damage per tick. */
readonly residualDamage?: number | undefined
/** Authentic 1.13c NextHit flag and NextDelay immunity window in frames. */
readonly nextHit?: boolean | number | undefined
readonly nextDelay?: number | undefined
/** Number of jumps remaining for chaining projectiles (e.g. Chain Lightning). */
readonly chainRemaining?: number | undefined
/** Previous target index hit by this chain projectile to avoid immediately retargeting it. */
readonly lastHitTargetIndex?: number | undefined
/** Previous position before last tick step (for client-side trail generation). */
readonly prevX?: number | undefined
readonly prevY?: number | undefined
}
/**
* In Diablo II's 2:1 isometric projection (orthogonal diamond grid: 80x40 cells, 16x8 sub-tiles),
* any horizontal circular curve on the map ground plane projects into scene-pixel coordinates
* as an ellipse with a 2:1 aspect ratio (vertical scale / horizontal scale = 8 / 16 = 0.5).
*
* NOTE: The calculated coordinates (x, y) are physical MAP coordinates on the level plane,
* NOT the rendered sprite icon offsets. Rendered icons are pre-rendered 2.5D DCC billboards
* positioned via sprite frame anchors, while map coordinates govern physical trajectory and collision.
*/
export const ISO_GROUND_ASPECT_RATIO = 0.5
/** What a cast produced. */
export type CastResult =
| { readonly kind: 'mana'; readonly cost: number }
| { readonly kind: 'cooldown'; readonly ticksLeft: number }
| { readonly kind: 'projectile'; readonly projectile: Projectile }
| { readonly kind: 'instant'; readonly radius: number; readonly damage: number }
/** A caster's position and facing. */
export interface Caster {
/** World x. */
readonly x: number
/** World y. */
readonly y: number
/** Facing index (0 = south, turning west). */
readonly facing: number
}
/** Direction vectors, in the same order as sprite groups (0 = south, turning west). */
export const FACING_VECTORS: readonly (readonly [number, number])[] = [
[0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1], [1, 0], [1, 1],
]
/**
* Data extracted from Diablo II 1.13c Missiles.txt for projectile physics.
*/
export interface MissileTxtData {
readonly name: string
readonly id: number
/**
* `true` for client-side visual aliases that are NOT rows of 1.13c Missiles.txt (their `id` points at the
* real row they render for). Aliases are excluded from the wire id -> missile key lookup.
*/
readonly clientAlias?: boolean | undefined
readonly vel: number
readonly maxVel: number
readonly range: number
readonly levRange: number
readonly celFile: string
readonly animLen: number
readonly animSpeed: number
readonly loopAnim: number
readonly numDirections: number
readonly explosionMissile: string
readonly light: number
readonly red: number
readonly green: number
readonly blue: number
/** Calculated speed in pixels per second: vel * 25 */
readonly speedPxPerSec: number
/** Calculated flight distance in pixels: vel * range */
readonly distancePx: number
readonly nextHit?: number | boolean | undefined
readonly nextDelay?: number | undefined
readonly trans?: number | undefined
readonly subLoop?: number | undefined
readonly subStop?: number | undefined
readonly alwaysExplode?: number | boolean | undefined
readonly pierce?: number | boolean | undefined
}
/** Canonical 1.13c Ground Truth Missiles.txt defaults. */
export const CANONICAL_113C_MISSILES: Readonly<Record<string, MissileTxtData>> = Object.freeze({
lightninghit: {
name: 'lightninghit',
id: 99,
vel: 0,
maxVel: 0,
range: 12,
levRange: 0,
celFile: 'LightningStrike',
animLen: 8,
animSpeed: 16,
loopAnim: 1,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
trans: 1,
speedPxPerSec: 0,
distancePx: 0,
subLoop: 1,
subStop: 7,
},
lightningbolt: {
name: 'lightningbolt',
id: 98,
vel: 30,
maxVel: 30,
range: 25,
levRange: 0,
celFile: 'LightningStrike',
animLen: 16,
animSpeed: 16,
loopAnim: 2,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
trans: 1,
speedPxPerSec: 750,
distancePx: 750,
},
lightning: {
name: 'lightning',
clientAlias: true,
id: 98,
vel: 30,
maxVel: 30,
range: 25,
levRange: 0,
celFile: 'LightningStrike',
animLen: 16,
animSpeed: 16,
loopAnim: 2,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
trans: 1,
speedPxPerSec: 750,
distancePx: 750,
},
chainlightning: {
name: 'chainlightning',
id: 93,
vel: 30,
maxVel: 30,
range: 25,
levRange: 0,
celFile: 'LightningStrike',
animLen: 16,
animSpeed: 16,
loopAnim: 1,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
nextHit: 1,
nextDelay: 4,
trans: 1,
speedPxPerSec: 750,
distancePx: 750,
},
thunderstorm1: {
name: 'thunderstorm1',
id: 166,
vel: 2,
maxVel: 2,
range: 9,
levRange: 0,
celFile: 'LightningboltBig',
animLen: 8,
animSpeed: 8,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
trans: 1,
speedPxPerSec: 50,
distancePx: 18,
},
thunderstorm: {
name: 'thunderstorm',
clientAlias: true,
id: 166,
vel: 2,
maxVel: 2,
range: 9,
levRange: 0,
celFile: 'LightningboltBig',
animLen: 8,
animSpeed: 8,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
trans: 1,
speedPxPerSec: 50,
distancePx: 18,
},
nova: {
name: 'nova',
id: 90,
vel: 24,
maxVel: 24,
range: 13,
levRange: 0,
celFile: 'ElectricNova',
animLen: 13,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
nextHit: 1,
nextDelay: 4,
trans: 1,
speedPxPerSec: 600,
distancePx: 312,
},
electricnova: {
name: 'electricnova',
clientAlias: true,
id: 90,
vel: 24,
maxVel: 24,
range: 13,
levRange: 0,
celFile: 'ElectricNova',
animLen: 13,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
nextHit: 1,
nextDelay: 4,
trans: 1,
speedPxPerSec: 600,
distancePx: 312,
},
frostnova: {
name: 'frostnova',
id: 119,
vel: 24,
maxVel: 24,
range: 14,
levRange: 0,
celFile: 'FrostNova',
animLen: 14,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 0,
red: 81,
green: 81,
blue: 255,
nextHit: 1,
nextDelay: 4,
trans: 1,
speedPxPerSec: 600,
distancePx: 336,
},
firebolt: {
name: 'firebolt',
id: 58,
vel: 20,
maxVel: 20,
range: 50,
levRange: 0,
celFile: 'Firebolt',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'fireexplode',
light: 7,
red: 255,
green: 178,
blue: 64,
speedPxPerSec: 500,
trans: 1,
distancePx: 1000,
},
fireball: {
name: 'fireball',
id: 62,
vel: 20,
maxVel: 20,
range: 50,
levRange: 0,
celFile: 'Fireball',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'explodingarrowexp',
light: 7,
red: 255,
green: 178,
blue: 64,
speedPxPerSec: 500,
trans: 1,
distancePx: 1000,
},
fireexplode: {
name: 'fireexplode',
id: 29,
vel: 0,
maxVel: 0,
range: 12,
levRange: 0,
celFile: 'FireArrowExplode2',
animLen: 12,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 13,
red: 255,
green: 178,
blue: 64,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
explodingarrowexp: {
name: 'explodingarrowexp',
id: 42,
vel: 0,
maxVel: 0,
range: 16,
levRange: 0,
celFile: 'ExpArrowExplode',
animLen: 16,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 178,
blue: 64,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
icebolt: {
name: 'icebolt',
id: 59,
vel: 12,
maxVel: 12,
range: 50,
levRange: 0,
celFile: 'Icebolt',
animLen: 6,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'iceexplode',
light: 7,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 300,
trans: 1,
distancePx: 600,
},
iceblast: {
name: 'iceblast',
id: 91,
vel: 12,
maxVel: 12,
range: 50,
levRange: 0,
celFile: 'IceBlast',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'freezeexplode',
light: 7,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 300,
trans: 1,
distancePx: 600,
},
glacialspike: {
name: 'glacialspike',
id: 96,
vel: 16,
maxVel: 16,
range: 40,
levRange: 0,
celFile: 'GlacialSpike',
animLen: 6,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'freezeexplode',
light: 5,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 400,
trans: 1,
distancePx: 640,
},
iceexplode: {
name: 'iceexplode',
id: 30,
vel: 0,
maxVel: 0,
range: 16,
levRange: 0,
celFile: 'IceArrowExplode',
animLen: 16,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 11,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
freezeexplode: {
name: 'freezeexplode',
clientAlias: true,
id: 88,
vel: 0,
maxVel: 0,
range: 15,
levRange: 0,
celFile: 'FreezeExplodeCenter',
animLen: 15,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 11,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
freezingarrowexp1: {
name: 'freezingarrowexp1',
id: 88,
vel: 0,
maxVel: 0,
range: 15,
levRange: 0,
celFile: 'FreezeExplodeCenter',
animLen: 15,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 11,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
glacialspikeejecta: {
name: 'glacialspikeejecta',
clientAlias: true,
id: 89,
vel: 0,
maxVel: 0,
range: 15,
levRange: 0,
celFile: 'FreezeExplodeEjecta',
animLen: 15,
animSpeed: 16,
loopAnim: 0,
numDirections: 8,
explosionMissile: '',
light: 11,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
frozenorb: {
name: 'frozenorb',
id: 260,
vel: 10,
maxVel: 10,
range: 30,
levRange: 0,
celFile: 'IceOrb',
animLen: 16,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'frozenorbexplode',
light: 6,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 250,
trans: 1,
distancePx: 300,
},
frozenorbbolt: {
name: 'frozenorbbolt',
id: 261,
vel: 18,
maxVel: 18,
range: 25,
levRange: 0,
celFile: 'IceBolt',
animLen: 6,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'iceexplode',
light: 4,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 450,
trans: 1,
distancePx: 450,
},
frozenorbnova: {
name: 'frozenorbnova',
id: 262,
vel: 24,
maxVel: 24,
range: 25,
levRange: 0,
celFile: 'IceBolt',
animLen: 6,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'iceexplode',
light: 4,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 600,
trans: 1,
distancePx: 600,
},
frozenorbexplode: {
name: 'frozenorbexplode',
id: 263,
vel: 0,
maxVel: 0,
range: 16,
levRange: 0,
celFile: 'IceOrbExplode',
animLen: 16,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 6,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzardcenter: {
name: 'blizzardcenter',
id: 158,
vel: 0,
maxVel: 0,
range: 100,
levRange: 0,
celFile: '',
animLen: 2,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzard1: {
name: 'blizzard1',
id: 159,
vel: 0,
maxVel: 0,
range: 9,
levRange: 0,
celFile: 'icestormfallvar01',
animLen: 6,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: 'blizzardexplode1',
light: 4,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzard2: {
name: 'blizzard2',
id: 160,
vel: 0,
maxVel: 0,
range: 9,
levRange: 0,
celFile: 'icestormfallvar02',
animLen: 6,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: 'blizzardexplode2',
light: 4,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzard3: {
name: 'blizzard3',
id: 161,
vel: 0,
maxVel: 0,
range: 9,
levRange: 0,
celFile: 'Blizzard',
animLen: 8,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: 'blizzardexplode3',
light: 4,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzard4: {
name: 'blizzard4',
id: 162,
vel: 0,
maxVel: 0,
range: 9,
levRange: 0,
celFile: 'Blizzard',
animLen: 8,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: 'blizzardexplode1',
light: 4,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzardexplode1: {
name: 'blizzardexplode1',
id: 163,
vel: 0,
maxVel: 0,
range: 6,
levRange: 0,
celFile: 'icestormimpactvar01',
animLen: 6,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 6,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzardexplode2: {
name: 'blizzardexplode2',
id: 164,
vel: 0,
maxVel: 0,
range: 6,
levRange: 0,
celFile: 'icestormimpactvar02',
animLen: 6,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 6,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
blizzardexplode3: {
name: 'blizzardexplode3',
id: 165,
vel: 0,
maxVel: 0,
range: 6,
levRange: 0,
celFile: 'icestormimpactvar02',
animLen: 6,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 6,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
hydra: {
name: 'hydra',
id: 247,
vel: 16,
maxVel: 16,
range: 30,
levRange: 0,
celFile: 'Firebolt',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'fireexplode',
light: 5,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 400,
trans: 1,
distancePx: 480,
},
hydrafire: {
name: 'hydrafire',
clientAlias: true,
id: 247,
vel: 16,
maxVel: 16,
range: 30,
levRange: 0,
celFile: 'Firebolt',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'fireexplode',
light: 5,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 400,
trans: 1,
distancePx: 480,
},
firewallmaker: {
name: 'firewallmaker',
id: 68,
vel: 0,
maxVel: 0,
range: 1,
levRange: 0,
celFile: '',
animLen: 1,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 0,
green: 0,
blue: 0,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
firewall: {
name: 'firewall',
id: 69,
vel: 0,
maxVel: 0,
range: 90,
levRange: 0,
celFile: 'groundFireBig',
animLen: 37,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 5,
red: 255,
green: 178,
blue: 64,
trans: 1,
speedPxPerSec: 0,
distancePx: 0,
},
groundfirebig: {
name: 'groundfirebig',
clientAlias: true,
id: 83,
vel: 0,
maxVel: 0,
range: 90,
levRange: 0,
celFile: 'groundFireBig',
animLen: 15,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 10,
red: 255,
green: 178,
blue: 64,
trans: 1,
speedPxPerSec: 0,
distancePx: 0,
},
blaze: {
name: 'blaze',
id: 67,
vel: 0,
maxVel: 0,
range: 90,
levRange: 25,
celFile: 'Blaze',
animLen: 14,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 8,
red: 255,
green: 178,
blue: 64,
trans: 1,
speedPxPerSec: 0,
distancePx: 0,
},
infernoflame1: {
name: 'infernoflame1',
id: 60,
vel: 12,
maxVel: 12,
range: 16,
levRange: 0,
celFile: 'InfernoFlame1',
animLen: 8,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 6,
red: 255,
green: 178,
blue: 64,
trans: 1,
speedPxPerSec: 300,
distancePx: 192,
},
infernoflame2: {
name: 'infernoflame2',
id: 61,
vel: 12,
maxVel: 12,
range: 16,
levRange: 0,
celFile: 'InfernoFlame2',
animLen: 8,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 6,
red: 255,
green: 178,
blue: 64,
trans: 1,
speedPxPerSec: 300,
distancePx: 192,
},
meteor: {
name: 'meteor',
id: 100,
vel: 0,
maxVel: 0,
range: 60,
levRange: 0,
celFile: 'Meteor',
animLen: 12,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: 'meteorexplode',
light: 0,
red: 0,
green: 0,
blue: 0,
speedPxPerSec: 0,
trans: 0,
distancePx: 0,
},
meteorcenter: {
name: 'meteorcenter',
id: 101,
vel: 0,
maxVel: 0,
range: 60,
levRange: 0,
celFile: 'MeteorTargetIndicator',
animLen: 17,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: 'meteorexplode',
light: 4,
red: 255,
green: 64,
blue: 64,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
meteortail: {
name: 'meteortail',
id: 102,
vel: 0,
maxVel: 0,
range: 60,
levRange: 0,
celFile: 'MeteorTail',
animLen: 12,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 0,
green: 0,
blue: 0,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
meteorexplode: {
name: 'meteorexplode',
id: 103,
vel: 0,
maxVel: 0,
range: 16,
levRange: 0,
celFile: 'ExpArrowExplode',
animLen: 16,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 13,
red: 255,
green: 178,
blue: 64,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
meteorfire: {
name: 'meteorfire',
id: 240,
vel: 0,
maxVel: 0,
range: 30,
levRange: 15,
celFile: 'groundFireMedium',
animLen: 37,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 3,
red: 255,
green: 178,
blue: 64,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
chargedbolt: {
name: 'chargedbolt',
id: 56,
vel: 12,
maxVel: 12,
range: 98,
levRange: 0,
celFile: 'ChargedBolt',
animLen: 10,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 3,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 300,
trans: 1,
distancePx: 1176,
},
teeth: {
name: 'teeth',
id: 114,
vel: 16,
maxVel: 16,
range: 50,
levRange: 0,
celFile: 'teethMissile',
animLen: 30,
animSpeed: 16,
loopAnim: 0,
numDirections: 32,
explosionMissile: 'teethexplode',
light: 3,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 400,
trans: 1,
distancePx: 800,
},
teethexplode: {
name: 'teethexplode',
id: 204,
vel: 0,
maxVel: 0,
range: 13,
levRange: 0,
celFile: 'teethexplode',
animLen: 13,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
bonespear: {
name: 'bonespear',
id: 192,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'BoneSpear',
animLen: 6,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'teethexplode',
light: 3,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 600,
trans: 1,
distancePx: 960,
},
bonespearexplode: {
name: 'bonespearexplode',
id: 216,
vel: 0,
maxVel: 0,
range: 13,
levRange: 0,
celFile: 'teethexplode',
animLen: 13,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
bonespirit: {
name: 'bonespirit',
id: 193,
vel: 12,
maxVel: 12,
range: 128,
levRange: 0,
celFile: 'BoneSpirit',
animLen: 8,
animSpeed: 16,
loopAnim: 1,
numDirections: 8,
explosionMissile: 'bonespiritexplode',
light: 3,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 300,
trans: 1,
distancePx: 1536,
},
bonespiritexplode: {
name: 'bonespiritexplode',
id: 327,
vel: 0,
maxVel: 0,
range: 13,
levRange: 0,
celFile: 'BoneSpiritExplode',
animLen: 13,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
},
poisonnova: {
name: 'poisonnova',
id: 118,
vel: 12,
maxVel: 12,
range: 30,
levRange: 0,
celFile: 'poisonNova',
animLen: 30,
animSpeed: 16,
loopAnim: 0,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 300,
trans: 1,
distancePx: 360,
},
holybolt: {
name: 'holybolt',
id: 55,
vel: 20,
maxVel: 20,
range: 50,
levRange: 0,
celFile: 'HolyBoltMissile',
animLen: 16,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 7,
red: 222,
green: 222,
blue: 255,
speedPxPerSec: 500,
trans: 1,
distancePx: 1000,
},
blessedhammer: {
name: 'blessedhammer',
id: 92,
vel: 18,
maxVel: 30,
range: 120,
levRange: 0,
celFile: 'blessedhammer',
animLen: 6,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 5,
red: 222,
green: 222,
blue: 255,
speedPxPerSec: 450,
trans: 1,
distancePx: 2160,
},
magicarrow: {
name: 'magicarrow',
id: 27,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'SafeArrow',
animLen: 1,
animSpeed: 16,
loopAnim: 0,
numDirections: 32,
explosionMissile: 'teethexplode',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 600,
trans: 1,
distancePx: 960,
},
firearrow: {
name: 'firearrow',
id: 12,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'FireArrow',
animLen: 8,
animSpeed: 16,
loopAnim: 0,
numDirections: 32,
explosionMissile: 'fireexplode',
light: 6,
red: 255,
green: 178,
blue: 64,
speedPxPerSec: 600,
trans: 1,
distancePx: 960,
},
icearrow: {
name: 'icearrow',
id: 28,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'IceArrow',
animLen: 8,
animSpeed: 16,
loopAnim: 0,
numDirections: 32,
explosionMissile: 'iceexplode',
light: 6,
red: 81,
green: 81,
blue: 255,
speedPxPerSec: 600,
trans: 1,
distancePx: 960,
},
arrow: {
name: 'arrow',
id: 0,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'Arrow',
animLen: 1,
animSpeed: 16,
loopAnim: 0,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
},
multipleshotarrow: {
name: 'multipleshotarrow',
id: 213,
vel: 24,
maxVel: 24,
range: 50,
levRange: 0,
celFile: 'Arrow',
animLen: 1,
animSpeed: 16,
loopAnim: 0,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 600,
trans: 0,
distancePx: 1200,
},
lightningjavelin: {
name: 'lightningjavelin',
id: 205,
vel: 30,
maxVel: 30,
range: 25,
levRange: 0,
celFile: 'LightningJavelin',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 6,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 750,
trans: 1,
distancePx: 750,
},
bolt: {
name: 'bolt',
id: 1,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'Spike',
animLen: 1,
animSpeed: 16,
loopAnim: 1,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 0,
green: 0,
blue: 0,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
clientAlias: true,
},
coldarrow: {
name: 'coldarrow',
id: 13,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'ColdArrow',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 3,
red: 112,
green: 160,
blue: 255,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
clientAlias: true,
},
guidedarrow: {
name: 'guidedarrow',
id: 31,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'Arrow',
animLen: 1,
animSpeed: 16,
loopAnim: 1,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
clientAlias: true,
},
explodingarrow: {
name: 'explodingarrow',
id: 41,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'FireArrow',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'explodingarrowexp',
alwaysExplode: true,
light: 3,
red: 255,
green: 175,
blue: 55,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
clientAlias: true,
},
immolationarrow: {
name: 'immolationarrow',
id: 63,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'FireArrow',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'explodingarrowexp',
alwaysExplode: true,
light: 5,
red: 255,
green: 175,
blue: 55,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
clientAlias: true,
},
immolationfire: {
name: 'immolationfire',
id: 64,
vel: 0,
maxVel: 0,
range: 75,
levRange: 0,
celFile: 'FireSmall',
animLen: 21,
animSpeed: 16,
loopAnim: 1,
numDirections: 1,
explosionMissile: '',
light: 3,
red: 255,
green: 185,
blue: 75,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
clientAlias: true,
},
freezingarrow: {
name: 'freezingarrow',
id: 87,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'ColdArrow',
animLen: 5,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: 'freezingarrowexp1',
alwaysExplode: true,
light: 3,
red: 112,
green: 160,
blue: 255,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
clientAlias: true,
},
foa: {
name: 'foa',
id: 211,
vel: 0,
maxVel: 0,
range: 25,
levRange: 0,
celFile: 'FistOfTheHeavensDelay',
animLen: 5,
animSpeed: 16,
loopAnim: 0,
numDirections: 1,
explosionMissile: 'fistoftheheavensbolt',
alwaysExplode: true,
light: 8,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 0,
trans: 1,
distancePx: 0,
clientAlias: true,
},
fistoftheheavensbolt: {
name: 'fistoftheheavensbolt',
id: 212,
vel: 24,
maxVel: 24,
range: 25,
levRange: 0,
celFile: 'HolyBolt',
animLen: 6,
animSpeed: 16,
loopAnim: 1,
numDirections: 16,
explosionMissile: '',
light: 3,
red: 255,
green: 255,
blue: 255,
speedPxPerSec: 600,
trans: 1,
distancePx: 600,
clientAlias: true,
},
strafearrow: {
name: 'strafearrow',
id: 215,
vel: 24,
maxVel: 24,
range: 40,
levRange: 0,
celFile: 'Arrow',
animLen: 1,
animSpeed: 16,
loopAnim: 1,
numDirections: 32,
explosionMissile: '',
light: 0,
red: 0,
green: 0,
blue: 0,
speedPxPerSec: 600,
trans: 0,
distancePx: 960,
clientAlias: true,
},
})
/**
* Lookup from 1.13c Missiles.txt numeric `Id` to canonical missile key.
*/
export const MISSILE_CLASS_ID_TO_KEY: Readonly<Record<number, string>> = Object.freeze(
(() => {
const map: Record<number, string> = {}
for (const [key, data] of Object.entries(CANONICAL_113C_MISSILES)) {
if (!data.clientAlias || map[data.id] === undefined) {
map[data.id] = key
}
}
return map
})(),
)
/**
* Safely parse an RGB color channel (0..255) with fallback (default 255).
* Preserves authentic 0 values while mapping empty/unspecified cells to fallback.
*/
export function parseColorChannel(raw: string | undefined, fallback = 255): number {
if (raw === undefined) return fallback
const trimmed = raw.trim()
if (trimmed === '') return fallback
const n = Number(trimmed)
return Number.isFinite(n) ? n : fallback
}
/**
* Parses raw Missiles.txt text into a Map of missile records.
*/
export function parseMissilesTxt(content: string): Map<string, MissileTxtData> {
const map = new Map<string, MissileTxtData>()
const lines = content.split(/\r?\n/).filter(line => line.trim().length > 0)
if (lines.length < 2) return map
const headers = lines[0]!.split('\t').map(h => h.trim())
const col = (name: string) => headers.indexOf(name)
const missileIdx = col('Missile')
const idIdx = col('Id')
const velIdx = col('Vel')
const maxVelIdx = col('MaxVel')
const rangeIdx = col('Range')
const levRangeIdx = col('LevRange')
const celFileIdx = col('CelFile')
const animLenIdx = col('AnimLen')
const animSpeedIdx = col('AnimSpeed')
const loopAnimIdx = col('LoopAnim')
const numDirIdx = col('NumDirections')
const explIdx = col('ExplosionMissile')
const lightIdx = col('Light')
const redIdx = col('Red')
const greenIdx = col('Green')
const blueIdx = col('Blue')
const nextHitIdx = col('NextHit')
const nextDelayIdx = col('NextDelay')
const transIdx = col('Trans')
for (let i = 1; i < lines.length; i++) {
const cells = lines[i]!.split('\t')
const name = (cells[missileIdx] ?? '').trim().toLowerCase()
if (!name || name === 'expansion') continue
const vel = Number(cells[velIdx] ?? 0) || 0
const maxVel = Number(cells[maxVelIdx] ?? 0) || vel
const range = Number(cells[rangeIdx] ?? 0) || 0
const levRange = Number(cells[levRangeIdx] ?? 0) || 0
const celFile = (cells[celFileIdx] ?? '').trim()
const animLen = Number(cells[animLenIdx] ?? 0) || 0
const animSpeed = Number(cells[animSpeedIdx] ?? 0) || 0
const loopAnim = Number(cells[loopAnimIdx] ?? 0) || 0
const numDirections = Number(cells[numDirIdx] ?? 0) || 1
const explosionMissile = (cells[explIdx] ?? '').trim()
const light = Number(cells[lightIdx] ?? 0) || 0
const red = parseColorChannel(cells[redIdx])
const green = parseColorChannel(cells[greenIdx])
const blue = parseColorChannel(cells[blueIdx])
const nextHit = Number(cells[nextHitIdx] ?? 0) || 0
const nextDelay = Number(cells[nextDelayIdx] ?? 0) || 0
const trans = Number(cells[transIdx] ?? 0) || 0
const rec: MissileTxtData = {
name,
id: Number(cells[idIdx] ?? 0) || 0,
vel,
maxVel,
range,
levRange,
celFile,
animLen,
animSpeed,
loopAnim,
numDirections,
explosionMissile,
light,
red,
green,
blue,
nextHit,
nextDelay,
trans,
speedPxPerSec: vel * 25,
distancePx: vel * range,
}
map.set(name, rec)
if (name === 'nova') {
map.set('electricnova', { ...rec, name: 'electricnova' })
}
}
return map
}
/**
* Extracts missile speed and flight distance from a Missiles.txt table or parsed records.
*/
export function getMissileTxtData(
missileName: string,
missilesSource?:
| Map<string, MissileTxtData>
| { rows: readonly Readonly<Record<string, string>>[] }
| { header: readonly string[]; rows: readonly (readonly string[])[] }
| string
| undefined,
): MissileTxtData {
const key = missileName.trim().toLowerCase()
if (!missilesSource) {
const canonical = CANONICAL_113C_MISSILES[key]
if (!canonical) {
throw new Error(`Unknown missile "${missileName}" in Missiles.txt`)
}
return canonical
}
if (typeof missilesSource === 'string') {
const parsed = parseMissilesTxt(missilesSource)
return parsed.get(key) ?? getMissileTxtData(missileName, undefined)
}
if (missilesSource instanceof Map) {
return missilesSource.get(key) ?? getMissileTxtData(missileName, undefined)
}
// D2Table ({ header, rows })
if ('header' in missilesSource && Array.isArray((missilesSource as any).header)) {
const t = missilesSource as { header: readonly string[]; rows: readonly (readonly string[])[] }
const mIdx = t.header.indexOf('Missile')
if (mIdx !== -1) {
const row = t.rows.find(r => (r[mIdx] ?? '').trim().toLowerCase() === key)
if (row) {
const vel = Number(row[t.header.indexOf('Vel')] ?? 0) || 0
const maxVel = Number(row[t.header.indexOf('MaxVel')] ?? 0) || vel
const range = Number(row[t.header.indexOf('Range')] ?? 0) || 0
const levRange = Number(row[t.header.indexOf('LevRange')] ?? 0) || 0
const celFile = (row[t.header.indexOf('CelFile')] ?? '').trim()
const animLen = Number(row[t.header.indexOf('AnimLen')] ?? 0) || 0
const animSpeed = Number(row[t.header.indexOf('AnimSpeed')] ?? 0) || 0
const loopAnim = Number(row[t.header.indexOf('LoopAnim')] ?? 0) || 0
const numDirections = Number(row[t.header.indexOf('NumDirections')] ?? 0) || 1
const explosionMissile = (row[t.header.indexOf('ExplosionMissile')] ?? '').trim()
const light = Number(row[t.header.indexOf('Light')] ?? 0) || 0
const red = parseColorChannel(row[t.header.indexOf('Red')])
const green = parseColorChannel(row[t.header.indexOf('Green')])
const blue = parseColorChannel(row[t.header.indexOf('Blue')])
const transColIdx = t.header.indexOf('Trans')
const trans = transColIdx !== -1 ? Number(row[transColIdx] ?? 0) || 0 : 0
return {
name: key,
id: Number(row[t.header.indexOf('Id')] ?? 0) || 0,
vel,
maxVel,
range,
levRange,
celFile,
animLen,
animSpeed,
loopAnim,
numDirections,
explosionMissile,
light,
red,
green,
blue,
trans,
speedPxPerSec: vel * 25,
distancePx: vel * range,
}
}
}
}
// DataTable ({ rows: Record<string, string>[] })
if ('rows' in missilesSource && !('header' in missilesSource)) {
const tableRows = missilesSource.rows as readonly Readonly<Record<string, string>>[]
const row = tableRows.find(
r => (r['Missile'] ?? r['missile'] ?? '').trim().toLowerCase() === key,
)
if (row) {
const vel = Number(row['Vel'] ?? row['vel'] ?? 0) || 0
const maxVel = Number(row['MaxVel'] ?? row['maxvel'] ?? 0) || vel
const range = Number(row['Range'] ?? row['range'] ?? 0) || 0
const levRange = Number(row['LevRange'] ?? row['levrange'] ?? 0) || 0
const celFile = (row['CelFile'] ?? row['celfile'] ?? '').trim()
const animLen = Number(row['AnimLen'] ?? row['animlen'] ?? 0) || 0
const animSpeed = Number(row['AnimSpeed'] ?? row['animspeed'] ?? 0) || 0
const loopAnim = Number(row['LoopAnim'] ?? row['loopanim'] ?? 0) || 0
const numDirections = Number(row['NumDirections'] ?? row['numdirections'] ?? 0) || 1
const explosionMissile = (row['ExplosionMissile'] ?? row['explosionmissile'] ?? '').trim()
const light = Number(row['Light'] ?? row['light'] ?? 0) || 0
const red = parseColorChannel(row['Red'] ?? row['red'])
const green = parseColorChannel(row['Green'] ?? row['green'])
const blue = parseColorChannel(row['Blue'] ?? row['blue'])
const trans = Number(row['Trans'] ?? row['trans'] ?? 0) || 0
return {
name: key,
id: Number(row['Id'] ?? row['id'] ?? 0) || 0,
vel,
maxVel,
range,
levRange,
celFile,
animLen,
animSpeed,
loopAnim,
numDirections,
explosionMissile,
light,
red,
green,
blue,
trans,
speedPxPerSec: vel * 25,
distancePx: vel * range,
}
}
}
return getMissileTxtData(missileName, undefined)
}
/**
* Data extracted from Diablo II 1.13c Overlay.txt for visual overlay effects.
*/
export interface OverlayTxtData {
readonly overlay: string
readonly filename: string
readonly frames: number
readonly animRate: number
readonly trans: number
readonly preDraw: boolean
readonly initRadius: number
readonly radius: number
readonly red: number
readonly green: number
readonly blue: number
readonly numDirections?: number
readonly xOffset?: number
readonly yOffset?: number
}
/** Canonical 1.13c Ground Truth Overlay.txt defaults for elemental sorceress spell overlays. */
export const CANONICAL_113C_OVERLAYS: Readonly<Record<string, OverlayTxtData>> = Object.freeze({
fire_cast_1: {
overlay: 'fire_cast_1',
filename: 'FireCast_for_Sorceress',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 178,
blue: 64,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
firecast_for_sorceress: {
overlay: 'fire_cast_1',
filename: 'FireCast_for_Sorceress',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 178,
blue: 64,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
fire_cast_2: {
overlay: 'fire_cast_2',
filename: 'FireCast2',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 178,
blue: 64,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
firecast2: {
overlay: 'fire_cast_2',
filename: 'FireCast2',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 178,
blue: 64,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
ice_cast_1: {
overlay: 'ice_cast_1',
filename: 'IceCastNew01',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 81,
green: 81,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
icecastnew01: {
overlay: 'ice_cast_1',
filename: 'IceCastNew01',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 81,
green: 81,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
ice_cast_2: {
overlay: 'ice_cast_2',
filename: 'IceCastNew02',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 81,
green: 81,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
icecastnew02: {
overlay: 'ice_cast_2',
filename: 'IceCastNew02',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 81,
green: 81,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
ice_cast_3: {
overlay: 'ice_cast_3',
filename: 'IceCastNew03',
// Note: Overlay.txt lists 15 frames, but IceCastNew03.dcc contains 16 full frames.
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 81,
green: 81,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
icecastnew03: {
overlay: 'ice_cast_3',
filename: 'IceCastNew03',
// Note: Overlay.txt lists 15 frames, but IceCastNew03.dcc contains 16 full frames.
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 81,
green: 81,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
light_cast_1: {
overlay: 'light_cast_1',
filename: 'LightningCast',
frames: 10,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
lightningcast: {
overlay: 'light_cast_1',
filename: 'LightningCast',
frames: 10,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
light_cast_2: {
overlay: 'light_cast_2',
filename: 'LightningCastRunesFront',
frames: 10,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
lightningcastrunesfront: {
overlay: 'light_cast_2',
filename: 'LightningCastRunesFront',
frames: 10,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
teleport: {
overlay: 'teleport',
filename: 'Teleport',
frames: 18,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 5,
red: 255,
green: 255,
blue: 200,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// Authentic 1.13c Sorceress State / Buff Overlays
frozenarmor: {
overlay: 'frozenarmor',
filename: 'FrozenArmor',
frames: 24,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 13,
red: 200,
green: 200,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
shiverarmor: {
overlay: 'shiverarmor',
filename: 'FrozenArmor',
frames: 24,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 13,
red: 200,
green: 200,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
chillingarmor: {
overlay: 'chillingarmor',
filename: 'FrozenArmor',
frames: 24,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 13,
red: 200,
green: 200,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
chillarmor: {
overlay: 'chillingarmor',
filename: 'FrozenArmor',
frames: 24,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 13,
red: 200,
green: 200,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
energyshield: {
overlay: 'energyshield',
filename: 'ManaShieldBall',
frames: 8,
animRate: 8,
trans: 5,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
enchant: {
overlay: 'enchant',
filename: 'FireEnchant',
frames: 17,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 178,
blue: 64,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
thunderstormback: {
overlay: 'thunderstormback',
filename: 'ThunderstormCast_operate',
frames: 19,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 8,
radius: 8,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// Authentic 1.13c Paladin Aura Overlays (All 33 targets: 13 pairs front/back + 7 full rings)
// 1. Might (#98)
might_front: {
overlay: 'might_front',
filename: 'AuraMightFront',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_might_front: {
overlay: 'might_front',
filename: 'AuraMightFront',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
might: {
overlay: 'might_front',
filename: 'AuraMightFront',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
might_back: {
overlay: 'might_back',
filename: 'AuraMightBack',
frames: 15,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_might_back: {
overlay: 'might_back',
filename: 'AuraMightBack',
frames: 15,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 2. Prayer (#99)
prayer_front: {
overlay: 'prayer_front',
filename: 'AuraPrayerFront',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_prayer_front: {
overlay: 'prayer_front',
filename: 'AuraPrayerFront',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
prayer: {
overlay: 'prayer_front',
filename: 'AuraPrayerFront',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
prayer_back: {
overlay: 'prayer_back',
filename: 'AuraPrayerBack',
frames: 16,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_prayer_back: {
overlay: 'prayer_back',
filename: 'AuraPrayerBack',
frames: 16,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 3. Resist Fire (#100) - Full Ring
resist_fire: {
overlay: 'resist_fire',
filename: 'AuraResistFireFront',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 255,
green: 193,
blue: 103,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_resistfire: {
overlay: 'resist_fire',
filename: 'AuraResistFireFront',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 255,
green: 193,
blue: 103,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 4. Holy Fire (#102)
holy_fire_front: {
overlay: 'holy_fire_front',
filename: 'AuraHolyFireFront',
frames: 19,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_holyfire_front: {
overlay: 'holy_fire_front',
filename: 'AuraHolyFireFront',
frames: 19,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holy_fire: {
overlay: 'holy_fire_front',
filename: 'AuraHolyFireFront',
frames: 19,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holyfire: {
overlay: 'holy_fire_front',
filename: 'AuraHolyFireFront',
frames: 19,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holy_fire_back: {
overlay: 'holy_fire_back',
filename: 'AuraHolyFireBack',
frames: 19,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_holyfire_back: {
overlay: 'holy_fire_back',
filename: 'AuraHolyFireBack',
frames: 19,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 5. Thorns (#103)
thorns_front: {
overlay: 'thorns_front',
filename: 'AuraThornsFront',
frames: 18,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_thorns_front: {
overlay: 'thorns_front',
filename: 'AuraThornsFront',
frames: 18,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
thorns: {
overlay: 'thorns_front',
filename: 'AuraThornsFront',
frames: 18,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
thorns_back: {
overlay: 'thorns_back',
filename: 'AuraThornsBack',
frames: 18,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_thorns_back: {
overlay: 'thorns_back',
filename: 'AuraThornsBack',
frames: 18,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 6. Defiance (#104)
defiance_front: {
overlay: 'defiance_front',
filename: 'AuraDefianceFront',
frames: 15,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_defiance_front: {
overlay: 'defiance_front',
filename: 'AuraDefianceFront',
frames: 15,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
defiance: {
overlay: 'defiance_front',
filename: 'AuraDefianceFront',
frames: 15,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
defiance_back: {
overlay: 'defiance_back',
filename: 'AuraDefianceBack',
frames: 15,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_defiance_back: {
overlay: 'defiance_back',
filename: 'AuraDefianceBack',
frames: 15,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 7. Resist Cold (#105) - Full Ring
resist_cold: {
overlay: 'resist_cold',
filename: 'AuraResistColdBack',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 210,
green: 210,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_resistcold: {
overlay: 'resist_cold',
filename: 'AuraResistColdBack',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 210,
green: 210,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 8. Blessed Aim (#108)
blessed_aim_front: {
overlay: 'blessed_aim_front',
filename: 'BlessedAim_front',
frames: 29,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
blessedaimfront: {
overlay: 'blessed_aim_front',
filename: 'BlessedAim_front',
frames: 29,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
blessed_aim: {
overlay: 'blessed_aim_front',
filename: 'BlessedAim_front',
frames: 29,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
blessedaim: {
overlay: 'blessed_aim_front',
filename: 'BlessedAim_front',
frames: 29,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
blessed_aim_back: {
overlay: 'blessed_aim_back',
filename: 'BlessedAim_back',
frames: 29,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
blessedaimback: {
overlay: 'blessed_aim_back',
filename: 'BlessedAim_back',
frames: 29,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 9. Cleansing (#109)
cleansing_front: {
overlay: 'cleansing_front',
filename: 'Cleansing_front',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cleansingfront: {
overlay: 'cleansing_front',
filename: 'Cleansing_front',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cleansing: {
overlay: 'cleansing_front',
filename: 'Cleansing_front',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cleansing_back: {
overlay: 'cleansing_back',
filename: 'Cleansing_back',
frames: 16,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cleansingback: {
overlay: 'cleansing_back',
filename: 'Cleansing_back',
frames: 16,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 10. Resist Lightning (#110) - Full Ring
resist_lightning: {
overlay: 'resist_lightning',
filename: 'AuraResistLightningBack',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 255,
green: 255,
blue: 200,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_resistlight: {
overlay: 'resist_lightning',
filename: 'AuraResistLightningBack',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 255,
green: 255,
blue: 200,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 11. Concentration (#113)
concentration_front: {
overlay: 'concentration_front',
filename: 'Concentration_front',
frames: 30,
animRate: 3,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
concentrationfront: {
overlay: 'concentration_front',
filename: 'Concentration_front',
frames: 30,
animRate: 3,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
concentration: {
overlay: 'concentration_front',
filename: 'Concentration_front',
frames: 30,
animRate: 3,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
concentration_back: {
overlay: 'concentration_back',
filename: 'Concentration_back',
frames: 30,
animRate: 3,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
concentrationback: {
overlay: 'concentration_back',
filename: 'Concentration_back',
frames: 30,
animRate: 3,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 12. Holy Freeze (#114) - Full Ring
holy_freeze: {
overlay: 'holyfreeze',
filename: 'HolyFreeze',
frames: 15,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holyfreeze: {
overlay: 'holyfreeze',
filename: 'HolyFreeze',
frames: 15,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 13. Vigor (#115)
vigor_front: {
overlay: 'vigor_front',
filename: 'Stamina_front',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
staminafront: {
overlay: 'vigor_front',
filename: 'Stamina_front',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
vigor: {
overlay: 'vigor_front',
filename: 'Stamina_front',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
stamina: {
overlay: 'vigor_front',
filename: 'Stamina_front',
frames: 16,
animRate: 8,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
vigor_back: {
overlay: 'vigor_back',
filename: 'Stamina_back',
frames: 16,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
staminaback: {
overlay: 'vigor_back',
filename: 'Stamina_back',
frames: 16,
animRate: 8,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 14. Holy Shock (#118)
holy_shock_front: {
overlay: 'holy_shock_front',
filename: 'HolyShock_front',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holyshockfront: {
overlay: 'holy_shock_front',
filename: 'HolyShock_front',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holy_shock: {
overlay: 'holy_shock_front',
filename: 'HolyShock_front',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holyshock: {
overlay: 'holy_shock_front',
filename: 'HolyShock_front',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holy_shock_back: {
overlay: 'holy_shock_back',
filename: 'HolyShock_back',
frames: 14,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holyshockback: {
overlay: 'holy_shock_back',
filename: 'HolyShock_back',
frames: 14,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 15. Sanctuary (#119)
sanctuary_front: {
overlay: 'sanctuary_front',
filename: 'SanctuaryAura_Front',
frames: 12,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
sanctuaryfront: {
overlay: 'sanctuary_front',
filename: 'SanctuaryAura_Front',
frames: 12,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
sanctuary: {
overlay: 'sanctuary_front',
filename: 'SanctuaryAura_Front',
frames: 12,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
sanctuary_back: {
overlay: 'sanctuary_back',
filename: 'SanctuaryAura_Back',
frames: 12,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
sanctuaryback: {
overlay: 'sanctuary_back',
filename: 'SanctuaryAura_Back',
frames: 12,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 16. Meditation (#120) - Full Ring
meditation: {
overlay: 'meditation',
filename: 'Meditation',
frames: 20,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
meditationfront: {
overlay: 'meditation',
filename: 'Meditation',
frames: 20,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 17. Fanaticism (#122)
fanaticism_front: {
overlay: 'fanaticism_front',
filename: 'Fanatacism_Front',
frames: 30,
animRate: 6,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
fanaticismfront: {
overlay: 'fanaticism_front',
filename: 'Fanatacism_Front',
frames: 30,
animRate: 6,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
fanaticism_back: {
overlay: 'fanaticism_back',
filename: 'Fanatacism_Back',
frames: 30,
animRate: 6,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
fanaticismback: {
overlay: 'fanaticism_back',
filename: 'Fanatacism_Back',
frames: 30,
animRate: 6,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
fanaticism: {
overlay: 'fanaticism_back',
filename: 'Fanatacism_Back',
frames: 30,
animRate: 6,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_fanatic: {
overlay: 'fanaticism_back',
filename: 'Fanatacism_Back',
frames: 30,
animRate: 6,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 18. Conviction (#123) - Full Ring
conviction: {
overlay: 'convictionback',
filename: 'Conviction',
frames: 21,
animRate: 6,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
convictionback: {
overlay: 'convictionback',
filename: 'Conviction',
frames: 21,
animRate: 6,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 19. Redemption (#124) - Full Ring
redemption: {
overlay: 'redemptionback',
filename: 'Redemption',
frames: 12,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
redemptionback: {
overlay: 'redemptionback',
filename: 'Redemption',
frames: 12,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
// 20. Salvation (#125)
salvation_front: {
overlay: 'salvation_front',
filename: 'AuraResistAllFront',
frames: 20,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 6,
radius: 6,
red: 211,
green: 148,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_resistall_front: {
overlay: 'salvation_front',
filename: 'AuraResistAllFront',
frames: 20,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 6,
radius: 6,
red: 211,
green: 148,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
salvation: {
overlay: 'salvation_front',
filename: 'AuraResistAllFront',
frames: 20,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 6,
radius: 6,
red: 211,
green: 148,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
salvation_back: {
overlay: 'salvation_back',
filename: 'AuraResistAllBack',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 211,
green: 148,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
aura_resistall_back: {
overlay: 'salvation_back',
filename: 'AuraResistAllBack',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 6,
radius: 6,
red: 211,
green: 148,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cursecast: {
overlay: 'cursecast',
filename: 'CurseCast',
frames: 25,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 9,
red: 255,
green: 128,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
curseamplifydamage: {
overlay: 'curseamplifydamage',
filename: 'CurseAmplifyDamageEffect',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cursedimvision: {
overlay: 'cursedimvision',
filename: 'CurseDimVisionEffect',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
curseweaken: {
overlay: 'curseweaken',
filename: 'CurseWeakenEffect',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
curseironmaiden: {
overlay: 'curseironmaiden',
filename: 'CurseIronMaidenEffect',
frames: 19,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
curseterror: {
overlay: 'curseterror',
filename: 'CurseTerrorEffect',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
curseconfuse: {
overlay: 'curseconfuse',
filename: 'CurseConfuseEffect',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cursereversevampire: {
overlay: 'cursereversevampire',
filename: 'CurseReverseVampireEffect',
frames: 19,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
steallife: {
overlay: 'cursereversevampire',
filename: 'CurseReverseVampireEffect',
frames: 19,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
curseattract: {
overlay: 'curseattract',
filename: 'CurseAttractHarmEffect',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cursedecrepify: {
overlay: 'cursedecrepify',
filename: 'CurseDecrepifyEffect',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
curselowerresist: {
overlay: 'curselowerresist',
filename: 'CurseLowerResistEffect',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
bonearmor_cast: {
overlay: 'bonearmor_cast',
filename: 'NecromancerSpellCast',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 8,
red: 215,
green: 215,
blue: 235,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
bonecast: {
overlay: 'bonecast',
filename: 'NecromancerSpellCast',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 8,
red: 215,
green: 215,
blue: 235,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
holy_bolt_cast: {
overlay: 'holy_bolt_cast',
filename: 'HolyBoltCast',
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
fist_of_the_heavens_cast: {
overlay: 'fist_of_the_heavens_cast',
filename: 'FistOfTheHeavensCast',
frames: 12,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
conversioncast: {
overlay: 'conversioncast',
filename: 'ConversionCast',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 7,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cast_innersight: {
overlay: 'cast_innersight',
filename: 'CastInnerSight',
frames: 16,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 7,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
cast_slowmissiles: {
overlay: 'cast_slowmissiles',
filename: 'CastSlowMissiles',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 7,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
valkyriestart: {
overlay: 'valkyriestart',
filename: 'ValkyrieStart',
frames: 20,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 235,
blue: 155,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
shoutstart: {
overlay: 'shoutstart',
filename: 'ShoutCast',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 1,
radius: 8,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
battleorderscast: {
overlay: 'battleorderscast',
filename: 'BattleOrdersCast',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 1,
radius: 8,
red: 155,
green: 205,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
battlecommandcast: {
overlay: 'battlecommandcast',
filename: 'BattleCommandCast',
frames: 20,
animRate: 16,
trans: 3,
preDraw: true,
initRadius: 1,
radius: 8,
red: 255,
green: 185,
blue: 115,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
druid_fire_cast_1: {
overlay: 'druid_fire_cast_1',
filename: 'DruidFireCast1',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 8,
red: 255,
green: 175,
blue: 55,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
druid_fire_cast_2: {
overlay: 'druid_fire_cast_2',
filename: 'DruidFireCast2',
frames: 17,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 175,
blue: 55,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
wolf_into: {
overlay: 'wolf_into',
filename: 'WolfTransform',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
bear_into: {
overlay: 'bear_into',
filename: 'BearTransform',
frames: 15,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 0,
radius: 0,
red: 255,
green: 255,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
lightning: {
overlay: 'lightning',
filename: 'LightHit',
frames: 12,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 8,
red: 192,
green: 215,
blue: 255,
numDirections: 1,
xOffset: 0,
yOffset: 0,
},
})
/**
* Parses raw Overlay.txt text into a Map of overlay records.
*/
export function parseOverlayTxt(content: string): Map<string, OverlayTxtData> {
const map = new Map<string, OverlayTxtData>()
const lines = content.split(/\r?\n/).filter(line => line.trim().length > 0)
if (lines.length < 2) return map
const headers = lines[0]!.split('\t').map(h => h.trim())
const col = (name: string) => headers.findIndex(h => h.toLowerCase() === name.toLowerCase())
const overlayIdx = col('overlay')
const filenameIdx = col('Filename')
const framesIdx = col('Frames')
const animRateIdx = col('AnimRate')
const transIdx = col('Trans')
const preDrawIdx = col('PreDraw')
const initRadiusIdx = col('InitRadius')
const radiusIdx = col('Radius')
const redIdx = col('Red')
const greenIdx = col('Green')
const blueIdx = col('Blue')
const numDirIdx = col('NumDirections')
const xOffsetIdx = col('Xoffset')
const yOffsetIdx = col('Yoffset')
for (let i = 1; i < lines.length; i++) {
const cells = lines[i]!.split('\t')
const rawName = (cells[overlayIdx] ?? '').trim()
const name = rawName.toLowerCase()
if (!name || name === 'expansion') continue
const filename = (cells[filenameIdx] ?? '').trim()
const frames = Number(cells[framesIdx] ?? 1) || 1
const animRate = Number(cells[animRateIdx] ?? 16) || 16
const trans = Number(cells[transIdx] ?? 0) || 0
const preDraw = Number(cells[preDrawIdx] ?? 0) !== 0
const initRadius = Number(cells[initRadiusIdx] ?? 0) || 0
const radius = Number(cells[radiusIdx] ?? 0) || 0
const red = parseColorChannel(cells[redIdx])
const green = parseColorChannel(cells[greenIdx])
const blue = parseColorChannel(cells[blueIdx])
const numDirections = Number(cells[numDirIdx] ?? 1) || 1
const xOffset = Number(cells[xOffsetIdx] ?? 0) || 0
const yOffset = Number(cells[yOffsetIdx] ?? 0) || 0
const rec: OverlayTxtData = {
overlay: rawName,
filename,
frames,
animRate,
trans,
preDraw,
initRadius,
radius,
red,
green,
blue,
numDirections,
xOffset,
yOffset,
}
map.set(name, rec)
if (filename) {
map.set(filename.toLowerCase(), rec)
}
}
return map
}
/**
* Extracts overlay data from an Overlay.txt table or parsed records.
*/
export function getOverlayTxtData(
overlayName: string,
overlaySource?:
| Map<string, OverlayTxtData>
| { rows: readonly Readonly<Record<string, string>>[] }
| { header: readonly string[]; rows: readonly (readonly string[])[] }
| string
| undefined,
): OverlayTxtData {
const key = overlayName.trim().toLowerCase()
if (!overlaySource) {
return CANONICAL_113C_OVERLAYS[key] ?? {
overlay: overlayName,
filename: overlayName,
frames: 14,
animRate: 16,
trans: 3,
preDraw: false,
initRadius: 1,
radius: 9,
red: 255,
green: 178,
blue: 64,
}
}
if (typeof overlaySource === 'string') {
const parsed = parseOverlayTxt(overlaySource)
return parsed.get(key) ?? getOverlayTxtData(overlayName, undefined)
}
if (overlaySource instanceof Map) {
return overlaySource.get(key) ?? getOverlayTxtData(overlayName, undefined)
}
// D2Table ({ header, rows })
if ('header' in overlaySource && Array.isArray((overlaySource as any).header)) {
const t = overlaySource as { header: readonly string[]; rows: readonly (readonly string[])[] }
const oIdx = t.header.indexOf('Overlay')
const fIdx = t.header.indexOf('Filename')
const row = t.rows.find(
r => (oIdx !== -1 && (r[oIdx] ?? '').trim().toLowerCase() === key) ||
(fIdx !== -1 && (r[fIdx] ?? '').trim().toLowerCase() === key),
)
if (row) {
return {
overlay: oIdx !== -1 ? (row[oIdx] ?? key).trim() : key,
filename: fIdx !== -1 ? (row[fIdx] ?? '').trim() : '',
frames: Number(row[t.header.indexOf('Frames')] ?? 1) || 1,
animRate: Number(row[t.header.indexOf('AnimRate')] ?? 16) || 16,
trans: Number(row[t.header.indexOf('Trans')] ?? 0) || 0,
preDraw: Number(row[t.header.indexOf('PreDraw')] ?? 0) !== 0,
initRadius: Number(row[t.header.indexOf('InitRadius')] ?? 0) || 0,
radius: Number(row[t.header.indexOf('Radius')] ?? 0) || 0,
red: parseColorChannel(row[t.header.indexOf('Red')]),
green: parseColorChannel(row[t.header.indexOf('Green')]),
blue: parseColorChannel(row[t.header.indexOf('Blue')]),
numDirections: Number(row[t.header.indexOf('NumDirections')] ?? 1) || 1,
xOffset: Number(row[t.header.indexOf('Xoffset')] ?? 0) || 0,
yOffset: Number(row[t.header.indexOf('Yoffset')] ?? 0) || 0,
}
}
}
// DataTable ({ rows: Record<string, string>[] })
if ('rows' in overlaySource && !('header' in overlaySource)) {
const tableRows = overlaySource.rows as readonly Readonly<Record<string, string>>[]
const row = tableRows.find(
r => (r['overlay'] ?? r['Overlay'] ?? '').trim().toLowerCase() === key ||
(r['Filename'] ?? r['filename'] ?? '').trim().toLowerCase() === key,
)
if (row) {
return {
overlay: (row['overlay'] ?? row['Overlay'] ?? key).trim(),
filename: (row['Filename'] ?? row['filename'] ?? '').trim(),
frames: Number(row['Frames'] ?? row['frames'] ?? 1) || 1,
animRate: Number(row['AnimRate'] ?? row['animrate'] ?? 16) || 16,
trans: Number(row['Trans'] ?? row['trans'] ?? 0) || 0,
preDraw: Number(row['PreDraw'] ?? row['predraw'] ?? 0) !== 0,
initRadius: Number(row['InitRadius'] ?? row['initradius'] ?? 0) || 0,
radius: Number(row['Radius'] ?? row['radius'] ?? 0) || 0,
red: parseColorChannel(row['Red'] ?? row['red']),
green: parseColorChannel(row['Green'] ?? row['green']),
blue: parseColorChannel(row['Blue'] ?? row['blue']),
numDirections: Number(row['NumDirections'] ?? row['numdirections'] ?? 1) || 1,
xOffset: Number(row['Xoffset'] ?? row['xoffset'] ?? 0) || 0,
yOffset: Number(row['Yoffset'] ?? row['yoffset'] ?? 0) || 0,
}
}
}
return getOverlayTxtData(overlayName, undefined)
}
/**
* An active unit overlay in the world (e.g. cast overlay, aura, buff visual).
*/
export interface ActiveOverlay {
readonly id: string
readonly overlayName: string
readonly casterId: string
x: number
y: number
frame: number
readonly maxFrames: number
readonly animRate: number
readonly preDraw: boolean
readonly trans: number
readonly lightRadius: number
readonly lightColor: readonly [number, number, number]
expired: boolean
readonly loop?: boolean | undefined
readonly targetMonsterIndex?: number | undefined
readonly isAura?: boolean | undefined
readonly auraSkillId?: number | undefined
readonly isCastOverlay?: boolean | undefined
readonly isStateOverlay?: boolean | undefined
readonly stateGroup?: number | undefined
}
export interface CreateActiveOverlayOptions {
readonly id?: string | undefined
readonly loop?: boolean | undefined
readonly targetMonsterIndex?: number | undefined
readonly isAura?: boolean | undefined
readonly auraSkillId?: number | undefined
readonly isCastOverlay?: boolean | undefined
readonly isStateOverlay?: boolean | undefined
readonly stateGroup?: number | undefined
}
let overlaySeqCounter = 0
export function resetOverlaySeqCounter(): void {
overlaySeqCounter = 0
}
/**
* Creates an ActiveOverlay entity initialized at the caster's coordinates.
*/
export function createActiveOverlay(
overlayName: string,
caster: { x: number; y: number },
casterId = 'player',
overlaySource?: Map<string, OverlayTxtData> | string,
options?: CreateActiveOverlayOptions,
): ActiveOverlay {
const data = getOverlayTxtData(overlayName, overlaySource)
const cleanName = overlayName.trim().toLowerCase()
const isColdArmor = ['frozenarmor', 'shiverarmor', 'chillingarmor', 'chillarmor'].includes(cleanName)
const isKnownState = [
'frozenarmor',
'shiverarmor',
'chillingarmor',
'chillarmor',
'energyshield',
'enchant',
'thunderstormback',
].includes(cleanName)
const isStateOverlay = options?.isStateOverlay ?? isKnownState
const isCastOverlay = options?.isCastOverlay ?? (!isStateOverlay && !options?.isAura)
const stateGroup = options?.stateGroup ?? (isColdArmor ? 1 : 0)
return {
id: options?.id ?? `overlay_${overlayName}_${++overlaySeqCounter}`,
overlayName: data.overlay,
casterId,
x: caster.x,
y: caster.y,
frame: 0,
maxFrames: data.frames,
animRate: data.animRate,
preDraw: data.preDraw,
trans: data.trans,
lightRadius: data.radius,
lightColor: [data.red, data.green, data.blue] as const,
expired: false,
loop: options?.loop,
targetMonsterIndex: options?.targetMonsterIndex,
isAura: options?.isAura,
auraSkillId: options?.auraSkillId,
isCastOverlay,
isStateOverlay,
stateGroup,
}
}
/** Complete canonical mapping for all Paladin aura skills (IDs 98..125) to their 1.13c overlay name. */
export const PALADIN_AURA_OVERLAYS: Readonly<Record<number, string>> = Object.freeze({
98: 'might',
99: 'prayer',
100: 'resist_fire',
102: 'holy_fire',
103: 'thorns',
104: 'defiance',
105: 'resist_cold',
108: 'blessed_aim',
109: 'cleansing',
110: 'resist_lightning',
113: 'concentration',
114: 'holy_freeze',
115: 'vigor',
118: 'holy_shock',
119: 'sanctuary',
120: 'meditation',
122: 'fanaticism',
123: 'conviction',
124: 'redemption',
125: 'salvation',
})
/**
* Authentic Diablo II v1.13c Paladin Aura Overlay Parts (States.txt + Overlay.txt).
* - 13 auras have TWO separate parts: front (preDraw=false) + back (preDraw=true).
* - 7 auras are single full-circle overlays (preDraw=true or false).
*/
export const PALADIN_AURA_OVERLAY_PARTS: Readonly<Record<number, readonly string[]>> = Object.freeze({
98: ['might_front', 'might_back'],
99: ['prayer_front', 'prayer_back'],
100: ['resist_fire'],
102: ['holy_fire_front', 'holy_fire_back'],
103: ['thorns_front', 'thorns_back'],
104: ['defiance_front', 'defiance_back'],
105: ['resist_cold'],
108: ['blessed_aim_front', 'blessed_aim_back'],
109: ['cleansing_front', 'cleansing_back'],
110: ['resist_lightning'],
113: ['concentration_front', 'concentration_back'],
114: ['holy_freeze'],
115: ['vigor_front', 'vigor_back'],
118: ['holy_shock_front', 'holy_shock_back'],
119: ['sanctuary_front', 'sanctuary_back'],
120: ['meditation'],
122: ['fanaticism_front', 'fanaticism_back'],
123: ['conviction'],
124: ['redemption'],
125: ['salvation_front', 'salvation_back'],
})
export const PALADIN_AURA_SKILL_IDS: ReadonlySet<number> = new Set(
Object.keys(PALADIN_AURA_OVERLAYS).map(Number),
)
export function isAuraSkill(skillId: number): boolean {
return PALADIN_AURA_SKILL_IDS.has(skillId)
}
export function getAuraOverlayName(skillId: number): string | null {
return PALADIN_AURA_OVERLAYS[skillId] ?? null
}
export function getAuraOverlayNames(skillId: number): readonly string[] {
return PALADIN_AURA_OVERLAY_PARTS[skillId] ?? (PALADIN_AURA_OVERLAYS[skillId] ? [PALADIN_AURA_OVERLAYS[skillId]] : [])
}
/** Complete canonical mapping for all Sorceress skills to their 1.13c castoverlay. */
export const SORCERESS_SKILL_CAST_OVERLAYS: Readonly<Record<number, string>> = Object.freeze({
36: 'fire_cast_1', // Fire Bolt
38: 'light_cast_1', // Charged Bolt
39: 'ice_cast_1', // Ice Bolt
40: 'ice_cast_1', // Frozen Armor
41: 'fire_cast_2', // Inferno
42: 'light_cast_1', // Static Field
43: 'light_cast_2', // Telekinesis
44: 'ice_cast_2', // Frost Nova
45: 'ice_cast_1', // Ice Blast
46: 'fire_cast_2', // Blaze
47: 'fire_cast_2', // Fire Ball
48: 'light_cast_1', // Nova
49: 'light_cast_1', // Lightning
50: 'ice_cast_2', // Shiver Armor
51: 'fire_cast_2', // Fire Wall
52: 'fire_cast_2', // Enchant
53: 'light_cast_1', // Chain Lightning
54: 'teleport', // Teleport
55: 'ice_cast_2', // Glacial Spike
56: 'fire_cast_2', // Meteor
57: 'light_cast_2', // Thunder Storm
58: 'light_cast_2', // Energy Shield
59: 'ice_cast_3', // Blizzard
60: 'ice_cast_3', // Chilling Armor
62: 'fire_cast_2', // Hydra
64: 'ice_cast_3', // Frozen Orb
})
const SORCERESS_SKILL_NAME_TO_OVERLAY: Readonly<Record<string, string>> = Object.freeze({
firebolt: 'fire_cast_1',
'fire bolt': 'fire_cast_1',
chargedbolt: 'light_cast_1',
'charged bolt': 'light_cast_1',
icebolt: 'ice_cast_1',
'ice bolt': 'ice_cast_1',
frozenarmor: 'ice_cast_1',
'frozen armor': 'ice_cast_1',
inferno: 'fire_cast_2',
staticfield: 'light_cast_1',
'static field': 'light_cast_1',
telekinesis: 'light_cast_2',
frostnova: 'ice_cast_2',
'frost nova': 'ice_cast_2',
iceblast: 'ice_cast_1',
'ice blast': 'ice_cast_1',
blaze: 'fire_cast_2',
fireball: 'fire_cast_2',
'fire ball': 'fire_cast_2',
nova: 'light_cast_1',
lightning: 'light_cast_1',
shiverarmor: 'ice_cast_2',
'shiver armor': 'ice_cast_2',
firewall: 'fire_cast_2',
'fire wall': 'fire_cast_2',
enchant: 'fire_cast_2',
chainlightning: 'light_cast_1',
'chain lightning': 'light_cast_1',
teleport: 'teleport',
glacialspike: 'ice_cast_2',
'glacial spike': 'ice_cast_2',
meteor: 'fire_cast_2',
thunderstorm: 'light_cast_2',
'thunder storm': 'light_cast_2',
energyshield: 'light_cast_2',
'energy shield': 'light_cast_2',
blizzard: 'ice_cast_3',
chillingarmor: 'ice_cast_3',
'chilling armor': 'ice_cast_3',
hydra: 'fire_cast_2',
frozenorb: 'ice_cast_3',
'frozen orb': 'ice_cast_3',
bonearmor: 'bonearmor_cast',
'bone armor': 'bonearmor_cast',
bonewall: 'bonecast',
'bone wall': 'bonecast',
boneprison: 'bonecast',
'bone prison': 'bonecast',
teeth: 'bonecast',
bonespear: 'bonecast',
'bone spear': 'bonecast',
bonespirit: 'bonecast',
'bone spirit': 'bonecast',
corpseexplosion: 'bonecast',
'corpse explosion': 'bonecast',
poisonexplosion: 'bonecast',
'poison explosion': 'bonecast',
poisonnova: 'bonecast',
'poison nova': 'bonecast',
amplifydamage: 'cursecast',
'amplify damage': 'cursecast',
dimvision: 'cursecast',
'dim vision': 'cursecast',
weaken: 'cursecast',
ironmaiden: 'cursecast',
'iron maiden': 'cursecast',
terror: 'cursecast',
confuse: 'cursecast',
lifetap: 'cursecast',
'life tap': 'cursecast',
attract: 'cursecast',
decrepify: 'cursecast',
lowerresist: 'cursecast',
'lower resist': 'cursecast',
holybolt: 'holy_bolt_cast',
'holy bolt': 'holy_bolt_cast',
blessedhammer: 'holy_bolt_cast',
'blessed hammer': 'holy_bolt_cast',
fistoftheheavens: 'fist_of_the_heavens_cast',
'fist of the heavens': 'fist_of_the_heavens_cast',
})
const NECROMANCER_SKILL_CAST_OVERLAYS: Readonly<Record<number, string>> = Object.freeze({
66: 'cursecast', // Amplify Damage
67: 'bonecast', // Teeth
68: 'bonearmor_cast', // Bone Armor
70: 'bonecast', // Raise Skeleton
71: 'cursecast', // Dim Vision
72: 'cursecast', // Weaken
74: 'bonecast', // Corpse Explosion
76: 'cursecast', // Iron Maiden
77: 'cursecast', // Terror
78: 'bonecast', // Bone Wall
80: 'bonecast', //Raise Skeletal Mage
81: 'cursecast', // Confuse
82: 'cursecast', // Life Tap
83: 'bonecast', // Poison Explosion
84: 'bonecast', // Bone Spear
86: 'cursecast', // Attract
87: 'cursecast', // Decrepify
88: 'bonecast', // Bone Prison
91: 'cursecast', // Lower Resist
92: 'bonecast', // Poison Nova
93: 'bonecast', // Bone Spirit
95: 'bonecast', // Revive
})
const OTHER_CLASS_SKILL_CAST_OVERLAYS: Readonly<Record<number, string>> = Object.freeze({
// Amazon
8: 'cast_innersight', // Inner Sight
17: 'cast_slowmissiles', // Slow Missiles
32: 'valkyriestart', // Valkyrie
// Paladin
101: 'holy_bolt_cast', // Holy Bolt
112: 'holy_bolt_cast', // Blessed Hammer
116: 'conversioncast', // Conversion
121: 'fist_of_the_heavens_cast', // Fist of the Heavens
// Barbarian
138: 'shoutstart', // Shout
149: 'battleorderscast', // Battle Orders
155: 'battlecommandcast', // Battle Command
// Druid
223: 'wolf_into', // Werewolf
225: 'druid_fire_cast_1', // Firestorm
228: 'bear_into', // Werebear
229: 'druid_fire_cast_1', // Molten Boulder
234: 'druid_fire_cast_1', // Fissure
244: 'druid_fire_cast_2', // Volcano
249: 'druid_fire_cast_2', // Armageddon
})
/**
* Returns canonical castoverlay name for a skill, if any.
*/
export function getSkillCastOverlay(skillId: number | string): string | undefined {
if (typeof skillId === 'number') {
return (
SORCERESS_SKILL_CAST_OVERLAYS[skillId] ??
NECROMANCER_SKILL_CAST_OVERLAYS[skillId] ??
OTHER_CLASS_SKILL_CAST_OVERLAYS[skillId] ??
BATCH1_SKILLS[skillId]?.castOverlay
)
}
const num = parseInt(skillId, 10)
if (!Number.isNaN(num)) {
return (
SORCERESS_SKILL_CAST_OVERLAYS[num] ??
NECROMANCER_SKILL_CAST_OVERLAYS[num] ??
OTHER_CLASS_SKILL_CAST_OVERLAYS[num] ??
BATCH1_SKILLS[num]?.castOverlay
)
}
const normalized = skillId.toLowerCase().trim()
const overlay =
SORCERESS_SKILL_NAME_TO_OVERLAY[normalized] ??
SORCERESS_SKILL_NAME_TO_OVERLAY[normalized.replace(/[\s_-]/g, '')]
if (overlay) return overlay
for (const skill of Object.values(BATCH1_SKILLS)) {
if (skill.name.toLowerCase() === normalized && skill.castOverlay) {
return skill.castOverlay
}
}
return undefined
}
export interface SkillExecutionContext {
readonly caster: CombatPlayer
readonly target?: { x: number; y: number; id?: string } | undefined
readonly skillLevel: number // effective level: hardPoints + softPoints
readonly hardPoints?: number | undefined // base points blvl: 0..20 (for synergy evaluation)
readonly synergyMultiplier?: number | undefined // computed from skill-tree
}
export type SkillType =
| 'projectile'
| 'melee'
| 'area'
| 'buff'
| 'curse'
| 'movement'
| 'summon'
| 'aura'
| 'passive'
export interface Batch1SkillDef {
readonly id: number
readonly name: string
readonly classCode: CharacterClassCode
readonly charClass: string
readonly type: SkillType
readonly mana: number
readonly lvlmana: number
readonly minmana: number
readonly manashift: number
readonly delay: number
readonly hitshift: number
readonly emin: number
readonly emax: number
readonly elev1: number
readonly elev2: number
readonly elev3: number
readonly elev4: number
readonly elev5: number
readonly emaxlev1?: number
readonly emaxlev2?: number
readonly emaxlev3?: number
readonly emaxlev4?: number
readonly emaxlev5?: number
readonly srcDamage: number
readonly range?: number
readonly speed?: number
readonly castOverlay?: string
readonly srvdofunc?: number | undefined
readonly castDelay?: number | undefined
}
export const BATCH1_SKILLS: Readonly<Record<number, Batch1SkillDef>> = Object.freeze({
// Amazon (5 skills)
6: { id: 6, name: 'Magic Arrow', classCode: 'ama', charClass: 'amazon', type: 'projectile', mana: 12, lvlmana: -1, minmana: 0, manashift: 5, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128, range: 480, speed: 600 },
7: { id: 7, name: 'Fire Arrow', classCode: 'ama', charClass: 'amazon', type: 'projectile', mana: 24, lvlmana: 1, minmana: 1, manashift: 5, delay: 0, hitshift: 8, emin: 1, emax: 4, elev1: 2, elev2: 3, elev3: 6, elev4: 12, elev5: 24, emaxlev1: 2, emaxlev2: 3, emaxlev3: 7, emaxlev4: 14, emaxlev5: 27, srcDamage: 128, range: 480, speed: 600 },
10: { id: 10, name: 'Jab', classCode: 'ama', charClass: 'amazon', type: 'melee', mana: 8, lvlmana: 1, minmana: 1, manashift: 6, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128 },
11: { id: 11, name: 'Cold Arrow', classCode: 'ama', charClass: 'amazon', type: 'projectile', mana: 28, lvlmana: 1, minmana: 1, manashift: 5, delay: 0, hitshift: 7, emin: 6, emax: 8, elev1: 4, elev2: 5, elev3: 8, elev4: 16, elev5: 42, emaxlev1: 4, emaxlev2: 5, emaxlev3: 9, emaxlev4: 17, emaxlev5: 44, srcDamage: 128, range: 480, speed: 600 },
14: { id: 14, name: 'Power Strike', classCode: 'ama', charClass: 'amazon', type: 'melee', mana: 8, lvlmana: 1, minmana: 1, manashift: 6, delay: 0, hitshift: 8, emin: 1, emax: 16, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, emaxlev1: 18, emaxlev2: 36, emaxlev3: 54, emaxlev4: 72, emaxlev5: 90, srcDamage: 128 },
// Sorceress (6 skills)
36: { id: 36, name: 'Fire Bolt', classCode: 'sor', charClass: 'sorceress', type: 'projectile', mana: 5, lvlmana: 0, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 6, emax: 12, elev1: 3, elev2: 4, elev3: 8, elev4: 18, elev5: 54, emaxlev1: 3, emaxlev2: 6, emaxlev3: 10, emaxlev4: 20, emaxlev5: 56, srcDamage: 0, range: 1000, speed: 500, castOverlay: 'fire_cast_1' },
37: { id: 37, name: 'Warmth', classCode: 'sor', charClass: 'sorceress', type: 'passive', mana: 0, lvlmana: 0, minmana: 0, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
39: { id: 39, name: 'Ice Bolt', classCode: 'sor', charClass: 'sorceress', type: 'projectile', mana: 3, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 7, emin: 6, emax: 10, elev1: 2, elev2: 4, elev3: 6, elev4: 8, elev5: 10, emaxlev1: 3, emaxlev2: 5, emaxlev3: 7, emaxlev4: 9, emaxlev5: 11, srcDamage: 0, range: 400, speed: 450, castOverlay: 'ice_cast_1' },
44: { id: 44, name: 'Frost Nova', classCode: 'sor', charClass: 'sorceress', type: 'area', mana: 9, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 7, emin: 4, emax: 8, elev1: 4, elev2: 6, elev3: 8, elev4: 10, elev5: 12, emaxlev1: 5, emaxlev2: 7, emaxlev3: 9, emaxlev4: 11, emaxlev5: 13, srcDamage: 0, castOverlay: 'ice_cast_2' },
47: { id: 47, name: 'Fire Ball', classCode: 'sor', charClass: 'sorceress', type: 'projectile', mana: 10, lvlmana: 1, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 12, emax: 28, elev1: 13, elev2: 23, elev3: 28, elev4: 33, elev5: 38, emaxlev1: 15, emaxlev2: 25, emaxlev3: 30, emaxlev4: 35, emaxlev5: 40, srcDamage: 0, range: 450, speed: 450, castOverlay: 'fire_cast_2' },
54: { id: 54, name: 'Teleport', classCode: 'sor', charClass: 'sorceress', type: 'movement', mana: 24, lvlmana: -1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0, castOverlay: 'teleport' },
// Necromancer (5 skills)
66: { id: 66, name: 'Amplify Damage', classCode: 'nec', charClass: 'necromancer', type: 'curse', mana: 4, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
67: { id: 67, name: 'Teeth', classCode: 'nec', charClass: 'necromancer', type: 'area', mana: 6, lvlmana: 1, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 4, emax: 8, elev1: 2, elev2: 2, elev3: 3, elev4: 4, elev5: 5, emaxlev1: 2, emaxlev2: 3, emaxlev3: 4, emaxlev4: 5, emaxlev5: 6, srcDamage: 0 },
68: { id: 68, name: 'Bone Armor', classCode: 'nec', charClass: 'necromancer', type: 'buff', mana: 11, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
70: { id: 70, name: 'Raise Skeleton', classCode: 'nec', charClass: 'necromancer', type: 'summon', mana: 6, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 1, elev3: 2, elev4: 3, elev5: 4, srcDamage: 0 },
74: { id: 74, name: 'Corpse Explosion', classCode: 'nec', charClass: 'necromancer', type: 'area', mana: 15, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
// Paladin (6 skills)
96: { id: 96, name: 'Sacrifice', classCode: 'pal', charClass: 'paladin', type: 'melee', mana: 0, lvlmana: 0, minmana: 0, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128, castOverlay: 'blood1' },
97: { id: 97, name: 'Smite', classCode: 'pal', charClass: 'paladin', type: 'melee', mana: 2, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
98: { id: 98, name: 'Might', classCode: 'pal', charClass: 'paladin', type: 'aura', mana: 0, lvlmana: 0, minmana: 0, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
101: { id: 101, name: 'Holy Bolt', classCode: 'pal', charClass: 'paladin', type: 'projectile', mana: 32, lvlmana: 1, minmana: 1, manashift: 4, delay: 0, hitshift: 8, emin: 8, emax: 16, elev1: 8, elev2: 10, elev3: 13, elev4: 16, elev5: 20, emaxlev1: 8, emaxlev2: 11, emaxlev3: 15, emaxlev4: 18, emaxlev5: 23, srcDamage: 0, range: 450, speed: 450 },
106: { id: 106, name: 'Zeal', classCode: 'pal', charClass: 'paladin', type: 'melee', mana: 2, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128 },
112: { id: 112, name: 'Blessed Hammer', classCode: 'pal', charClass: 'paladin', type: 'area', mana: 20, lvlmana: 1, minmana: 1, manashift: 6, delay: 0, hitshift: 8, emin: 12, emax: 16, elev1: 8, elev2: 10, elev3: 12, elev4: 13, elev5: 14, srcDamage: 0 },
// Barbarian (6 skills)
126: { id: 126, name: 'Bash', classCode: 'bar', charClass: 'barbarian', type: 'melee', mana: 2, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128 },
130: { id: 130, name: 'Howl', classCode: 'bar', charClass: 'barbarian', type: 'curse', mana: 4, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
132: { id: 132, name: 'Leap', classCode: 'bar', charClass: 'barbarian', type: 'movement', mana: 2, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
133: { id: 133, name: 'Double Swing', classCode: 'bar', charClass: 'barbarian', type: 'melee', mana: 8, lvlmana: -1, minmana: 0, manashift: 5, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128 },
138: { id: 138, name: 'Shout', classCode: 'bar', charClass: 'barbarian', type: 'buff', mana: 6, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
143: { id: 143, name: 'Leap Attack', classCode: 'bar', charClass: 'barbarian', type: 'melee', mana: 9, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128 },
// Druid (5 skills)
223: { id: 223, name: 'Werewolf', classCode: 'dru', charClass: 'druid', type: 'buff', mana: 15, lvlmana: 0, minmana: 1, manashift: 8, delay: 25, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128 },
224: { id: 224, name: 'Lycanthropy', classCode: 'dru', charClass: 'druid', type: 'passive', mana: 0, lvlmana: 0, minmana: 0, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
225: { id: 225, name: 'Firestorm', classCode: 'dru', charClass: 'druid', type: 'area', mana: 4, lvlmana: 0, minmana: 1, manashift: 8, delay: 15, hitshift: 2, emin: 3, emax: 6, elev1: 3, elev2: 5, elev3: 7, elev4: 14, elev5: 21, emaxlev1: 3, emaxlev2: 6, emaxlev3: 8, emaxlev4: 15, emaxlev5: 23, srcDamage: 0 },
226: { id: 226, name: 'Oak Sage', classCode: 'dru', charClass: 'druid', type: 'summon', mana: 15, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
229: { id: 229, name: 'Molten Boulder', classCode: 'dru', charClass: 'druid', type: 'projectile', mana: 20, lvlmana: 1, minmana: 1, manashift: 7, delay: 50, hitshift: 8, emin: 6, emax: 12, elev1: 4, elev2: 7, elev3: 10, elev4: 13, elev5: 16, emaxlev1: 5, emaxlev2: 8, emaxlev3: 11, emaxlev4: 14, emaxlev5: 17, srcDamage: 0, range: 450, speed: 300 },
// Assassin (5 skills)
251: { id: 251, name: 'Fire Blast', classCode: 'ass', charClass: 'assassin', type: 'projectile', mana: 24, lvlmana: 1, minmana: 1, manashift: 5, delay: 0, hitshift: 7, emin: 6, emax: 8, elev1: 3, elev2: 8, elev3: 20, elev4: 38, elev5: 58, emaxlev1: 5, emaxlev2: 11, emaxlev3: 24, emaxlev4: 44, emaxlev5: 66, srcDamage: 0, range: 350, speed: 400 },
252: { id: 252, name: 'Claw Mastery', classCode: 'ass', charClass: 'assassin', type: 'passive', mana: 0, lvlmana: 0, minmana: 0, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
254: { id: 254, name: 'Tiger Strike', classCode: 'ass', charClass: 'assassin', type: 'melee', mana: 1, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 128 },
258: { id: 258, name: 'Burst of Speed', classCode: 'ass', charClass: 'assassin', type: 'buff', mana: 10, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
261: { id: 261, name: 'Charged Bolt Sentry', classCode: 'ass', charClass: 'assassin', type: 'summon', mana: 13, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 7, emin: 2, emax: 14, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, emaxlev1: 6, emaxlev2: 8, emaxlev3: 12, emaxlev4: 14, emaxlev5: 16, srcDamage: 0 },
})
/**
* Returns the metadata definition for a Batch 1 combat skill, or undefined if not in Batch 1.
*/
export function getBatch1SkillDef(skillId: number): Batch1SkillDef | undefined {
return BATCH1_SKILLS[skillId]
}
/**
* Checks whether a skill ID belongs to the canonical Batch 1 essential combat skills.
*/
export function isBatch1Skill(skillId: number): boolean {
return skillId in BATCH1_SKILLS
}
/**
* Computes 1.13c mana cost with 24.8 fixed-point precision scaling and minmana clamping.
*
* Formula: Math.max(minmana, (mana + lvlmana * (effectiveLevel - 1)) * 2^(manashift - 8))
* - Decreasing mana skills reach 0.0 at high levels (Magic Arrow, Double Swing).
* - Minimum mana clamping prevents dropping below minmana (Teleport clamps at 1).
* - Resilience: Invalid / NaN / non-positive levels default to level 1.
*/
export function getSkillManaCost(skillId: number, effectiveLevel: number): number {
if (skillId === 6) {
return calculateMagicArrowStats(effectiveLevel).manaCost
}
if (skillId === 7) {
return calculateFireArrowStats(effectiveLevel).manaCost
}
if (skillId === 8) {
return calculateInnerSightStats(effectiveLevel).manaCost
}
if (skillId === 9) {
return calculateCriticalStrikeStats(effectiveLevel).manaCost
}
if (skillId === 10) {
return calculateJabStats(effectiveLevel).manaCost
}
if (skillId === 11) {
return calculateColdArrowStats(effectiveLevel).manaCost
}
if (skillId === 12) {
return calculateMultipleShotStats(effectiveLevel).manaCost
}
if (skillId === 13) {
return calculateDodgeStats(effectiveLevel).manaCost
}
if (skillId === 14) {
return calculatePowerStrikeStats(effectiveLevel).manaCost
}
if (skillId === 15) {
return calculatePoisonJavelinStats(effectiveLevel).manaCost
}
if (skillId === 16) {
return calculateExplodingArrowStats(effectiveLevel).manaCost
}
if (skillId === 17) {
return calculateSlowMissilesStats(effectiveLevel).manaCost
}
if (skillId === 18) {
return calculateAvoidStats(effectiveLevel).manaCost
}
if (skillId === 19) {
return calculateImpaleStats(effectiveLevel).manaCost
}
if (skillId === 20) {
return calculateLightningBoltStats(effectiveLevel).manaCost
}
if (skillId === 21) {
return calculateIceArrowStats(effectiveLevel).manaCost
}
if (skillId === 22) {
return calculateGuidedArrowStats(effectiveLevel).manaCost
}
if (skillId === 23) {
return calculatePenetrateStats(effectiveLevel).manaCost
}
if (skillId === 24) {
return calculateChargedStrikeStats(effectiveLevel).manaCost
}
if (skillId === 25) {
return calculatePlagueJavelinStats(effectiveLevel).manaCost
}
if (skillId === 26) {
return calculateStrafeStats(effectiveLevel).manaCost
}
if (skillId === 27) {
return calculateImmolationArrowStats(effectiveLevel).manaCost
}
if (skillId === 28) {
return calculateDecoyStats(effectiveLevel).manaCost
}
if (skillId === 29) {
return calculateEvadeStats(effectiveLevel).manaCost
}
if (skillId === 30) {
return calculateFendStats(effectiveLevel).manaCost
}
if (skillId === 31) {
return calculateFreezingArrowStats(effectiveLevel).manaCost
}
if (skillId === 32) {
return calculateValkyrieStats(effectiveLevel).manaCost
}
if (skillId === 33) {
return calculatePierceStats(effectiveLevel).manaCost
}
if (skillId === 34) {
return calculateLightningStrikeStats(effectiveLevel).manaCost
}
if (skillId === 35) {
return calculateLightningFuryStats(effectiveLevel).manaCost
}
if (skillId === 38) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 24, lvlmana = 4, manashift = 5 (factor = 0.125)
// Formula: (24 + 4 * (lvl - 1)) * 0.125 = 3 + 0.5 * (lvl - 1)
return (24 + 4 * (lvl - 1)) * 0.125
}
if (skillId === 40) {
// 1.13c Skills.txt: mana = 7, lvlmana = 0, manashift = 8
return 7
}
if (skillId === 41) {
// 1.13c Skills.txt: mana = 6, lvlmana = 0, manashift = 8
return 6
}
if (skillId === 46) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 22, lvlmana = 1, manashift = 7 (factor = 0.5)
return Math.floor((22 + (lvl - 1)) * 0.5)
}
if (skillId === 51) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 22, lvlmana = 1, manashift = 8 (factor = 1.0)
return 22 + (lvl - 1)
}
if (skillId === 52) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 25, lvlmana = 1, manashift = 8 (factor = 1.0)
return 25 + (lvl - 1)
}
if (skillId === 43) {
// 1.13c Skills.txt: mana = 7, lvlmana = 0, manashift = 8
return 7
}
if (skillId === 48) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 15, lvlmana = 1, manashift = 8
return 15 + (lvl - 1)
}
if (skillId === 42) {
// 1.13c Skills.txt: mana = 9, lvlmana = 0, manashift = 8
return 9
}
if (skillId === 45) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 12, lvlmana = 1, manashift = 7 (factor = 0.5)
return (12 + (lvl - 1)) * 0.5
}
if (skillId === 49) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 16, lvlmana = 1, manashift = 7 (factor = 0.5)
return (16 + (lvl - 1)) * 0.5
}
if (skillId === 50) {
// 1.13c Skills.txt: mana = 11, lvlmana = 0, manashift = 8
return 11
}
if (skillId === 53) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 9, lvlmana = 1, manashift = 8 (factor = 1.0)
return 9 + (lvl - 1)
}
if (skillId === 55) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 20, lvlmana = 1, manashift = 7 (factor = 0.5)
return (20 + (lvl - 1)) * 0.5
}
if (skillId === 56) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 34, lvlmana = 1, manashift = 7 (factor = 0.5)
return (34 + (lvl - 1)) * 0.5
}
if (skillId === 57) {
// 1.13c Skills.txt: mana = 19, lvlmana = 0, manashift = 8
return 19
}
if (skillId === 58) {
// 1.13c Skills.txt: mana = 5, lvlmana = 0, manashift = 8
return 5
}
if (skillId === 59) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 23, lvlmana = 1, manashift = 8 (factor = 1.0)
return 23 + (lvl - 1)
}
if (skillId === 60) {
// 1.13c Skills.txt: mana = 17, lvlmana = 0, manashift = 8
return 17
}
if (skillId === 61) {
// Fire Mastery: passive
return 0
}
if (skillId === 62) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 40, lvlmana = 1, manashift = 7 (factor = 0.5)
return (40 + (lvl - 1)) * 0.5
}
if (skillId === 63) {
// Lightning Mastery: passive
return 0
}
if (skillId === 64) {
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
// 1.13c Skills.txt: mana = 30, lvlmana = 1, manashift = 8 (factor = 1.0)
return 30 + (lvl - 1)
}
if (skillId === 65) {
// Cold Mastery: passive
return 0
}
const def = getBatch1SkillDef(skillId)
if (!def) return 0
if (def.mana === 0 && def.lvlmana === 0) return 0
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
const shiftFactor = 2 ** ((def.manashift ?? 8) - 8)
const rawCost = (def.mana + def.lvlmana * (lvl - 1)) * shiftFactor
return Math.max(def.minmana ?? 0, rawCost)
}
/**
* Returns casting delay cooldown in frames at 25 fps.
* - Werewolf: 25 frames (1.0s)
* - Firestorm: 15 frames (0.6s)
* - Molten Boulder: 50 frames (2.0s)
* - Meteor: 30 frames (1.2s)
* - Blizzard: 45 frames (1.8s)
* - Hydra: 40 frames (1.6s)
* - Frozen Orb: 25 frames (1.0s)
* - All other Batch 1 skills: 0 frames
*/
export function getSkillCooldownTicks(skillId: number): number {
if (
skillId === 6 ||
skillId === 7 ||
skillId === 8 ||
skillId === 9 ||
skillId === 10 ||
skillId === 11 ||
skillId === 12 ||
skillId === 13 ||
skillId === 14 ||
skillId === 16 ||
skillId === 17 ||
skillId === 18 ||
skillId === 19 ||
skillId === 20 ||
skillId === 21 ||
skillId === 22 ||
skillId === 23 ||
skillId === 24 ||
skillId === 26 ||
skillId === 28 ||
skillId === 29 ||
skillId === 30 ||
skillId === 31 ||
skillId === 33 ||
skillId === 34 ||
skillId === 35 ||
skillId === 38 ||
skillId === 40 ||
skillId === 41 ||
skillId === 42 ||
skillId === 43 ||
skillId === 44 ||
skillId === 45 ||
skillId === 46 ||
skillId === 48 ||
skillId === 49 ||
skillId === 50 ||
skillId === 52 ||
skillId === 53 ||
skillId === 55 ||
skillId === 57 ||
skillId === 58 ||
skillId === 60 ||
skillId === 61 ||
skillId === 63 ||
skillId === 65
) {
return 0
}
if (skillId === 15) {
return 12 // Poison Javelin: 12 frames (0.5s) casting delay
}
if (skillId === 25) {
return 100 // Plague Javelin: 100 frames (4.0s) casting delay
}
if (skillId === 27) {
return 25 // Immolation Arrow: 25 frames (1.0s)
}
if (skillId === 32) {
return 150 // Valkyrie: 150 frames (6.0s) casting delay
}
if (skillId === 51) {
return 35
}
if (skillId === 56) {
return 30
}
if (skillId === 59) {
return 45
}
if (skillId === 62) {
return 40
}
if (skillId === 64) {
return 25
}
if (skillId === 121) {
return 25
}
const def = getBatch1SkillDef(skillId)
return def?.delay ?? 0
}