/** * Offline Cast Overlay Asset & Data Baking Script. * * Extracts authentic Diablo II v1.13c cast overlay DCC sprites from d2data.mpq: * - FireCast_for_Sorceress.dcc (fire_cast_1, 14 frames, box: [-55, -127, 117, 164]) * - FireCast2.dcc (fire_cast_2, 16 frames, box: [-74, -89, 145, 133]) * - IceCastNew01.dcc (ice_cast_1, 15 frames, box: [-49, -90, 97, 55]) * - IceCastNew02.dcc (ice_cast_2, 15 frames, box: [-58, -100, 115, 123]) * - IceCastNew03.dcc (ice_cast_3, 16 frames, box: [-63, -113, 127, 148]) * - LightningCast.dcc (light_cast_1, 10 frames, box: [-79, -107, 164, 144]) * - LightningCastRunesFront.dcc (light_cast_2, 10 frames, box: [-73, -154, 147, 190]) * - Teleport.dcc (teleport, 18 frames, box: [-61, -108, 136, 154]) * * Bakes them into indexed-color PNG atlases and JSON metadata under: * - public/overlays/ * - samples/d2-packs/overlays/ * * And generates typed metadata in src/render/overlays-meta.ts. * Enforces Zero Runtime MPQ/DLL Architectural Invariant. */ import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { MpqArchive } from '../src/mpq/archive.ts' import { fileSource } from '../src/mpq/file-source.ts' import { decodeDcc } from '../src/formats/dcc.ts' import { decodePl2 } from '../src/formats/pl2.ts' import { packMissileSheetToRgbaPng } from './pack-missiles.ts' import type { SpriteFrame, SpriteSheet } from '../src/formats/sprite.ts' export interface OverlayFrameMeta { readonly x: number readonly y: number readonly w: number readonly h: number readonly anchorX: number readonly anchorY: number } export type OverlayFrameTuple = readonly [ x: number, y: number, width: number, height: number, anchorX: number, anchorY: number, ] export interface OverlayBox { readonly left: number readonly top: number readonly width: number readonly height: number } export interface OverlayMeta { readonly name: string readonly celFile: string readonly width: number readonly height: number readonly directions: number readonly frames: number readonly framesPerDirection: number readonly animRate: number readonly trans: number readonly preDraw: boolean readonly lightRadius: number readonly lightColor: readonly [number, number, number] readonly box: OverlayBox readonly groups: readonly (readonly OverlayFrameTuple[])[] } export interface OverlayTarget { readonly name: string readonly celFile: string readonly aliases?: readonly string[] readonly animRate: number readonly trans: number readonly preDraw: boolean readonly lightRadius: number readonly lightColor: readonly [number, number, number] } export const OVERLAY_TARGETS: readonly OverlayTarget[] = [ { name: 'fire_cast_1', celFile: 'FireCast_for_Sorceress', animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [255, 178, 64] as const, }, { name: 'fire_cast_2', celFile: 'FireCast2', animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [255, 178, 64] as const, }, { name: 'ice_cast_1', celFile: 'IceCastNew01', animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [81, 81, 255] as const, }, { name: 'ice_cast_2', celFile: 'IceCastNew02', animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [81, 81, 255] as const, }, { name: 'ice_cast_3', celFile: 'IceCastNew03', animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [81, 81, 255] as const, }, { name: 'light_cast_1', celFile: 'LightningCast', animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [255, 255, 255] as const, }, { name: 'light_cast_2', celFile: 'LightningCastRunesFront', animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [255, 255, 255] as const, }, { name: 'teleport', celFile: 'Teleport', animRate: 16, trans: 3, preDraw: false, lightRadius: 5, lightColor: [255, 255, 200] as const, }, // 33 Authentic Diablo II v1.13c Paladin Aura Overlays (13 Pairs with Front & Back + 7 Full Rings) // 1. Might (#98) { name: 'might_front', celFile: 'AuraMightFront', aliases: ['might', 'aura_might_front', 'auramightfront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'might_back', celFile: 'AuraMightBack', aliases: ['might_back', 'aura_might_back', 'auramightback'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 2. Prayer (#99) { name: 'prayer_front', celFile: 'AuraPrayerFront', aliases: ['prayer', 'aura_prayer_front', 'auraprayerfront'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'prayer_back', celFile: 'AuraPrayerBack', aliases: ['prayer_back', 'aura_prayer_back', 'auraprayerback'], animRate: 8, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 3. Resist Fire (#100) - Full Ring { name: 'resist_fire', celFile: 'AuraResistFireFront', aliases: ['aura_resistfire', 'AuraResistFireFront'], animRate: 16, trans: 3, preDraw: true, lightRadius: 6, lightColor: [255, 193, 103] as const, }, // 4. Holy Fire (#102) { name: 'holy_fire_front', celFile: 'AuraHolyFireFront', aliases: ['holy_fire', 'holyfire', 'aura_holyfire_front', 'auraholyfirefront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'holy_fire_back', celFile: 'AuraHolyFireBack', aliases: ['holy_fire_back', 'holyfire_back', 'aura_holyfire_back', 'auraholyfireback'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 5. Thorns (#103) { name: 'thorns_front', celFile: 'AuraThornsFront', aliases: ['thorns', 'aura_thorns_front', 'aurathornsfront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'thorns_back', celFile: 'AuraThornsBack', aliases: ['thorns_back', 'aura_thorns_back', 'aurathornsback'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 6. Defiance (#104) { name: 'defiance_front', celFile: 'AuraDefianceFront', aliases: ['defiance', 'aura_defiance_front', 'auradefiancefront'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'defiance_back', celFile: 'AuraDefianceBack', aliases: ['defiance_back', 'aura_defiance_back', 'auradefianceback'], animRate: 8, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 7. Resist Cold (#105) - Full Ring { name: 'resist_cold', celFile: 'AuraResistColdBack', aliases: ['aura_resistcold', 'AuraResistColdBack'], animRate: 16, trans: 3, preDraw: true, lightRadius: 6, lightColor: [210, 210, 255] as const, }, // 8. Blessed Aim (#108) { name: 'blessed_aim_front', celFile: 'BlessedAim_front', aliases: ['blessed_aim', 'blessedaim', 'blessedaimfront', 'BlessedAim_front'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'blessed_aim_back', celFile: 'BlessedAim_back', aliases: ['blessed_aim_back', 'blessedaimback', 'BlessedAim_back'], animRate: 8, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 9. Cleansing (#109) { name: 'cleansing_front', celFile: 'Cleansing_front', aliases: ['cleansing', 'cleansingfront', 'Cleansing_front'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'cleansing_back', celFile: 'Cleansing_back', aliases: ['cleansing_back', 'cleansingback', 'Cleansing_back'], animRate: 8, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 10. Resist Lightning (#110) - Full Ring { name: 'resist_lightning', celFile: 'AuraResistLightningBack', aliases: ['aura_resistlight', 'AuraResistLightningBack'], animRate: 16, trans: 3, preDraw: true, lightRadius: 6, lightColor: [255, 255, 200] as const, }, // 11. Concentration (#113) { name: 'concentration_front', celFile: 'Concentration_front', aliases: ['concentration', 'concentrationfront', 'Concentration_front'], animRate: 3, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'concentration_back', celFile: 'Concentration_back', aliases: ['concentration_back', 'concentrationback', 'Concentration_back'], animRate: 3, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 12. Holy Freeze (#114) - Full Ring { name: 'holy_freeze', celFile: 'HolyFreeze', aliases: ['holyfreeze', 'holywind', 'HolyFreeze'], animRate: 8, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 13. Vigor (#115) { name: 'vigor_front', celFile: 'Stamina_front', aliases: ['vigor', 'stamina', 'staminafront', 'Stamina_front'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'vigor_back', celFile: 'Stamina_back', aliases: ['vigor_back', 'staminaback', 'Stamina_back'], animRate: 8, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 14. Holy Shock (#118) { name: 'holy_shock_front', celFile: 'HolyShock_front', aliases: ['holy_shock', 'holyshock', 'holyshockfront', 'HolyShock_front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'holy_shock_back', celFile: 'HolyShock_back', aliases: ['holy_shock_back', 'holyshockback', 'HolyShock_back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 15. Sanctuary (#119) { name: 'sanctuary_front', celFile: 'SanctuaryAura_Front', aliases: ['sanctuary', 'sanctuaryfront', 'SanctuaryAura_Front'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'sanctuary_back', celFile: 'SanctuaryAura_Back', aliases: ['sanctuary_back', 'sanctuaryback', 'SanctuaryAura_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 16. Meditation (#120) - Full Ring { name: 'meditation', celFile: 'Meditation', aliases: ['meditationfront', 'Meditation'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 17. Fanaticism (#122) { name: 'fanaticism_front', celFile: 'Fanatacism_Front', aliases: ['fanaticism_front', 'fanaticismfront', 'Fanatacism_Front'], animRate: 6, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'fanaticism_back', celFile: 'Fanatacism_Back', aliases: ['fanaticism', 'fanaticismback', 'aura_fanatic', 'Fanatacism_Back'], animRate: 6, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 18. Conviction (#123) - Full Ring { name: 'conviction', celFile: 'Conviction', aliases: ['convictionback', 'Conviction'], animRate: 6, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 19. Redemption (#124) - Full Ring { name: 'redemption', celFile: 'Redemption', aliases: ['redemptionback', 'Redemption'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 20. Salvation (#125) { name: 'salvation_front', celFile: 'AuraResistAllFront', aliases: ['salvation', 'aura_resistall_front', 'AuraResistAllFront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [211, 148, 255] as const, }, { name: 'salvation_back', celFile: 'AuraResistAllBack', aliases: ['salvation_back', 'aura_resistall_back', 'AuraResistAllBack'], animRate: 16, trans: 3, preDraw: true, lightRadius: 6, lightColor: [211, 148, 255] as const, }, // --------------------------------------------------------------------------- // 9 Sorceress Skill & Hit Overlays (v1.13c Ground Truth) // --------------------------------------------------------------------------- // 1. ShockHitSm (lightning hit overlay for skills 38, 48, 49, 53, 57) { name: 'lightning', celFile: 'ShockHitSm', aliases: ['ShockHitSm', 'shockhitsm', 'holyshockhit', 'lightning_hit'], animRate: 16, trans: 3, preDraw: false, lightRadius: 11, lightColor: [255, 255, 255] as const, }, // 2. FrozenArmor (frozenarmor buff overlay for Frozen/Shiver/Chilling Armor 40, 50, 60) { name: 'frozenarmor', celFile: 'FrozenArmor', aliases: ['FrozenArmor', 'shiverarmor', 'chillarmor', 'chillingarmor', 'frozen_armor', 'shiver_armor', 'chill_armor', 'chilling_armor'], animRate: 16, trans: 3, preDraw: false, lightRadius: 13, lightColor: [200, 200, 255] as const, }, // 3. AuraResistColdCast (frozenarmor_hit hit overlay for 40, 50, 60) { name: 'frozenarmor_hit', celFile: 'AuraResistColdCast', aliases: ['AuraResistColdCast', 'cast_shivers', 'shiverarmor_hit', 'chillarmor_hit', 'chillingarmor_hit', 'chilling_armor_hit', 'frozen_armor_hit', 'shiver_armor_hit', 'chill_armor_hit'], animRate: 24, trans: 3, preDraw: true, lightRadius: 6, lightColor: [81, 81, 255] as const, }, // 4. ManaShieldBall (energyshield buff overlay for Energy Shield 58) { name: 'energyshield', celFile: 'ManaShieldBall', aliases: ['ManaShieldBall', 'manashield', 'energy_shield'], animRate: 8, trans: 5, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 5. ManaShieldDir00 (energyshieldhit0 absorb hit overlay for Energy Shield 58) { name: 'energyshieldhit0', celFile: 'ManaShieldDir00', aliases: ['ManaShieldDir00', 'energyshield_hit', 'energyshieldhit', 'energy_shield_hit'], animRate: 16, trans: 3, preDraw: false, lightRadius: 3, lightColor: [255, 255, 255] as const, }, // 6. ThunderstormCast (thunderstormcast cloud overlay for Thunder Storm 57) { name: 'thunderstormcast', celFile: 'ThunderstormCast', aliases: ['ThunderstormCast', 'thunderstorm_front', 'thunderstorm_cast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 8, lightColor: [255, 255, 255] as const, }, // 7. ThunderstormCast_operate (thunderstormback rear cloud overlay for Thunder Storm 57) { name: 'thunderstormback', celFile: 'ThunderstormCast_operate', aliases: ['ThunderstormCast_operate', 'thunderstorm_back', 'thunderstormoperate'], animRate: 16, trans: 3, preDraw: true, lightRadius: 8, lightColor: [255, 255, 255] as const, }, // 8. FireEnchant (enchant buff overlay for Enchant 52) { name: 'enchant', celFile: 'FireEnchant', aliases: ['FireEnchant', 'fireenchant', 'fire_enchant'], animRate: 16, trans: 3, preDraw: false, lightRadius: 9, lightColor: [255, 178, 64] as const, }, // 9. IceExplode (ice_explode overlay) { name: 'ice_explode', celFile: 'IceExplode', aliases: ['IceExplode', 'iceexplode'], animRate: 16, trans: 3, preDraw: false, lightRadius: 14, lightColor: [210, 210, 255] as const, }, // --------------------------------------------------------------------------- // 15 Necromancer Overlays (10 Curses, 2 Hits, 3 Bone Armor) // --------------------------------------------------------------------------- // 1. Amplify Damage (#66) { name: 'curseamplifydamage', celFile: 'CurseAmplifyDamageEffect', aliases: ['amplify_damage', 'amplifydamage', 'curse_amplify_damage'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [255, 64, 64] as const, }, // 2. Dim Vision (#71) { name: 'cursedimvision', celFile: 'CurseDimVisionEffect', aliases: ['dim_vision', 'dimvision', 'curse_dim_vision'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [255, 210, 210] as const, }, // 3. Weaken (#72) { name: 'curseweaken', celFile: 'CurseWeakenEffect', aliases: ['weaken', 'curse_weaken'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [255, 210, 210] as const, }, // 4. Iron Maiden (#76) { name: 'curseironmaiden', celFile: 'CurseIronMaidenEffect', aliases: ['iron_maiden', 'ironmaiden', 'curse_iron_maiden'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [255, 0, 0] as const, }, // 5. Terror (#77) { name: 'curseterror', celFile: 'CurseTerrorEffect', aliases: ['terror', 'curse_terror'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 6. Confuse (#81) & Barbarian Taunt (#137) { name: 'curseconfuse', celFile: 'CurseConfuseEffect', aliases: ['confuse', 'curse_confuse', 'taunt'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [0, 255, 0] as const, }, // 7. Life Tap (#82) { name: 'cursereversevampire', celFile: 'CurseReverseVampireEffect', aliases: ['lifetap', 'life_tap', 'curse_lifetap', 'curselifetap', 'reversevampire'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 8. Attract (#86) { name: 'curseattract', celFile: 'CurseAttractHarmEffect', aliases: ['attract', 'curse_attract'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [255, 0, 0] as const, }, // 9. Decrepify (#87) { name: 'cursedecrepify', celFile: 'CurseDecrepifyEffect', aliases: ['decrepify', 'curse_decrepify'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [0, 0, 255] as const, }, // 10. Lower Resist (#91) { name: 'curselowerresist', celFile: 'CurseLowerResistEffect', aliases: ['lower_resist', 'lowerresist', 'curse_lower_resist'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [0, 0, 255] as const, }, // 11. Iron Maiden Hit Overlay (#139) { name: 'hit_ironmaiden', celFile: 'AuraThornsHit', aliases: ['ironmaiden_hit', 'iron_maiden_hit'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 12. Life Tap Hit Overlay (#140) { name: 'hit_reversevampire', celFile: 'CurseRevVampireHit', aliases: ['lifetap_hit', 'life_tap_hit', 'reversevampire_hit'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 13. Bone Armor Cast Emerge (#141) { name: 'bonearmor_cast', celFile: 'BoneShieldEmerge', aliases: ['bonearmorcast', 'bone_armor_cast', 'boneshieldemerge'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 14. Bone Armor Front (#142) { name: 'bonearmor_front', celFile: 'BoneShieldFront', aliases: ['bonearmor', 'bone_armor', 'bonearmorfront', 'bone_armor_front', 'boneshieldfront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 15. Bone Armor Back (#143) { name: 'bonearmor_back', celFile: 'BoneShieldBack', aliases: ['bonearmorback', 'bone_armor_back', 'boneshieldback'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // --------------------------------------------------------------------------- // 16 Authentic Diablo II v1.13c Paladin Combat, Cast, Hit & State Overlays // --------------------------------------------------------------------------- // 1. Charge (#107, Overlay ID 125) { name: 'charge', celFile: 'Charge', aliases: ['Charge'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 2. Conversion Aura (#116, Overlay ID 124) { name: 'conversion', celFile: 'Conversion', aliases: ['Conversion', 'conversionaura', 'conversion_aura'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 3. Conversion Cast (#116, Overlay ID 123) { name: 'conversioncast', celFile: 'AuraResistColdBackCast', aliases: ['conversion_cast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 4. Holy Bolt Cast (#101, cast_undead, Overlay ID 21) { name: 'holy_bolt_cast', celFile: 'HolyBoltCloud', aliases: ['HolyBoltCloud', 'cast_undead', 'holyboltcloud', 'holybolt_cast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 5. Fist of the Heavens Cast (#121, cast_fistofares, Overlay ID 32) { name: 'fist_of_the_heavens_cast', celFile: 'FOACast', aliases: ['FOACast', 'cast_fistofares', 'foacast', 'fistoftheheavenscast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 6. Fist of the Heavens Hit (#121, handofgod, Overlay ID 158) { name: 'handofgod', celFile: 'HolyShockHit', aliases: ['fist_of_the_heavens_hit', 'fistoftheheavenshit'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 7. Sanctuary Knockback (#119, sanctuaryknockback, Overlay ID 113) { name: 'sanctuary_knockback', celFile: 'SanctuaryKnockback', aliases: ['SanctuaryKnockback', 'sanctuaryknockback'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 8. Holy Fire Hit (#102, hit_holyfire, Overlay ID 21) { name: 'holy_fire_hit', celFile: 'AuraHolyFireHit', aliases: ['AuraHolyFireHit', 'hit_holyfire', 'auraholyfirehit'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 9. Holy Fire Indicate (#102, target_holyfire, Overlay ID 22) { name: 'holy_fire_indicate', celFile: 'AuraHfireIndicate', aliases: ['AuraHfireIndicate', 'target_holyfire', 'aurahfireindicate'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 10. Thorns Hit (#103, hit_thorns, Overlay ID 25) { name: 'thorns_hit', celFile: 'AuraThornsHit', aliases: ['hit_thorns'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 11. Thorns Indicate (#103, target_thorns, Overlay ID 26) { name: 'thorns_indicate', celFile: 'AuraHfireIndicate', aliases: ['target_thorns'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 12. Cast Resist Fire (#100, cast_resistfire, Overlay ID 6) { name: 'cast_resistfire', celFile: 'AuraResistFireFrontCast', aliases: ['AuraResistFireFrontCast', 'cast_resist_fire', 'auraresistfirefrontcast'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 13. Cast Resist Cold (#105, cast_resistcold, Overlay ID 8) { name: 'cast_resistcold', celFile: 'AuraResistColdBackCast', aliases: ['cast_resist_cold'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 14. Cast Resist Lightning (#110, cast_resistlight, Overlay ID 10) { name: 'cast_resistlight', celFile: 'AuraResistLightningBackCast', aliases: ['AuraResistLightningBackCast', 'cast_resist_light', 'auraresistlightningbackcast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 15. Cast Resist All (#125, cast_resistall, Overlay ID 12) { name: 'cast_resistall', celFile: 'AuraResistAllCast', aliases: ['AuraResistAllCast', 'cast_resist_all', 'auraresistallcast'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // 16. Stun (#97 Smite, Overlay ID 148) { name: 'stun', celFile: 'Stun', aliases: ['Stun'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // --------------------------------------------------------------------------- // 10 Authentic Diablo II v1.13c Amazon Cast, Skill & Minion Overlays // --------------------------------------------------------------------------- { name: 'cast_innersight', celFile: 'EOZCast', aliases: ['castinnersight', 'eyeofzeus_cast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 5, lightColor: [255, 255, 200] as const, }, { name: 'innersight', celFile: 'HardenArmor', aliases: ['innersight_target', 'hardenarmor'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [255, 255, 128] as const, }, { name: 'cast_slowmissiles', celFile: 'HOAcast', aliases: ['castslowmissiles', 'handofathena_cast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 5, lightColor: [128, 200, 255] as const, }, { name: 'handofathena', celFile: 'HOAmissileExplode', aliases: ['slowmissiles_aura', 'hoa_explode'], animRate: 16, trans: 3, preDraw: false, lightRadius: 5, lightColor: [128, 200, 255] as const, }, { name: 'valkyriestart', celFile: 'ValkarieEmerge', aliases: ['valkyrie_emerge', 'valkyrie_cast'], animRate: 16, trans: 3, preDraw: false, lightRadius: 8, lightColor: [255, 220, 100] as const, }, { name: 'valkyrie', celFile: 'ValkarieGlow', aliases: ['valkyrie_glow', 'valkyrie_aura'], animRate: 16, trans: 3, preDraw: true, lightRadius: 7, lightColor: [255, 220, 100] as const, }, { name: 'dopllezon_appear', celFile: 'DopplezonAppear', aliases: ['decoy_appear', 'dopplezon'], animRate: 16, trans: 3, preDraw: false, lightRadius: 6, lightColor: [200, 200, 255] as const, }, { name: 'doubledamage1', celFile: 'impacting01', aliases: ['critical_strike_1', 'impacting01'], animRate: 16, trans: 3, preDraw: false, lightRadius: 4, lightColor: [255, 64, 64] as const, }, { name: 'doubledamage2', celFile: 'impacting02', aliases: ['critical_strike_2', 'impacting02'], animRate: 16, trans: 3, preDraw: false, lightRadius: 4, lightColor: [255, 64, 64] as const, }, { name: 'doubledamage3', celFile: 'impacting03', aliases: ['critical_strike_3', 'impacting03'], animRate: 16, trans: 3, preDraw: false, lightRadius: 4, lightColor: [255, 64, 64] as const, }, // --------------------------------------------------------------------------- // 10 Authentic Diablo II v1.13c Barbarian Warcry & Combat Overlays // --------------------------------------------------------------------------- { name: 'shoutstart', celFile: 'ShoutStart', aliases: ['shout_start', 'shout_cast', 'shoutcast'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'shout', celFile: 'Shout', aliases: ['shout_overlay', 'shout_buff'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'battlecry', celFile: 'BattleCry', aliases: ['battle_cry', 'battlecry_overlay'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'battleorderscast', celFile: 'BattleOrdersStart', aliases: ['battle_orders_start', 'battleorders_start', 'battleorders_cast'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'battleorders', celFile: 'BattleOrders', aliases: ['battle_orders', 'battleorders_overlay', 'battleorders_buff'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'battlecommandcast', celFile: 'BattleCommandStart', aliases: ['battle_command_start', 'battlecommand_start', 'battlecommand_cast'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'battlecommand', celFile: 'BattleCommand', aliases: ['battle_command', 'battlecommand_overlay', 'battlecommand_buff'], animRate: 8, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'berserkfront', celFile: 'BerserkOverlay', aliases: ['berserk', 'berserk_overlay', 'berserk_front', 'berserkfront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'frenzy', celFile: 'Frenzy', aliases: ['frenzy_overlay', 'frenzy_buff'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'warcry', celFile: 'WarcryHit', aliases: ['warcry_hit', 'warcryhit', 'warcry_overlay'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // --------------------------------------------------------------------------- // 31 Authentic Diablo II v1.13c Druid Spells, Transformations & Combat Overlays // --------------------------------------------------------------------------- { name: 'druid_fire_cast_1', celFile: 'Expansion/Fire_Overlay_A', aliases: ['Expansion/Fire_Overlay_A', 'expansion/fire_overlay_a', 'Fire_Overlay_A', 'fire_overlay_a'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'druid_fire_cast_2', celFile: 'Expansion/Fire_Overlay_B', aliases: ['Expansion/Fire_Overlay_B', 'expansion/fire_overlay_b', 'Fire_Overlay_B', 'fire_overlay_b'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'druidmorph', celFile: 'expansion/druidmorph', aliases: ['wolf_into', 'wolf_undo', 'bear_into', 'bear_undo', 'expansion/druidmorph'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'cyclonearmor1front', celFile: 'Expansion/CycloneArmorAFront', aliases: ['cyclonearmorafront', 'cyclone_armor_1_front', 'Expansion/CycloneArmorAFront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'cyclonearmor1back', celFile: 'Expansion/CycloneArmorABack', aliases: ['cyclonearmoraback', 'cyclone_armor_1_back', 'Expansion/CycloneArmorABack'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'cyclonearmor2front', celFile: 'Expansion/CycloneArmorBFront', aliases: ['cyclonearmorbfront', 'cyclone_armor_2_front', 'Expansion/CycloneArmorBFront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'cyclonearmor2back', celFile: 'Expansion/CycloneArmorBBack', aliases: ['cyclonearmorbback', 'cyclone_armor_2_back', 'Expansion/CycloneArmorBBack'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'cyclonearmor3front', celFile: 'Expansion/CycloneArmorCFront', aliases: ['cyclonearmorcfront', 'cyclone_armor_3_front', 'Expansion/CycloneArmorCFront'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'cyclonearmor3back', celFile: 'Expansion/CycloneArmorCBack', aliases: ['cyclonearmorcback', 'cyclone_armor_3_back', 'Expansion/CycloneArmorCBack'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'rabiesplague', celFile: 'Expansion/rabies', aliases: ['rabies', 'rabies_overlay', 'Expansion/rabies'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul1front', celFile: 'expansion/Maul_A_Front', aliases: ['maul1', 'maul_1_front', 'expansion/Maul_A_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul2front', celFile: 'expansion/Maul_B_Front', aliases: ['maul2', 'maul_2_front', 'expansion/Maul_B_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul3front', celFile: 'expansion/Maul_C_Front', aliases: ['maul3', 'maul_3_front', 'expansion/Maul_C_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul4front', celFile: 'expansion/Maul_D_Front', aliases: ['maul4', 'maul_4_front', 'expansion/Maul_D_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul5front', celFile: 'expansion/Maul_E_Front', aliases: ['maul5', 'maul_5_front', 'expansion/Maul_E_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul1back', celFile: 'expansion/Maul_A_Back', aliases: ['maul_1_back', 'expansion/Maul_A_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul2back', celFile: 'expansion/Maul_B_Back', aliases: ['maul_2_back', 'expansion/Maul_B_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul3back', celFile: 'expansion/Maul_C_Back', aliases: ['maul_3_back', 'expansion/Maul_C_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul4back', celFile: 'expansion/Maul_D_Back', aliases: ['maul_4_back', 'expansion/Maul_D_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'maul5back', celFile: 'expansion/Maul_E_Back', aliases: ['maul_5_back', 'expansion/Maul_E_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage1front', celFile: 'expansion/feral_rage_A_Front', aliases: ['feralrage1', 'feral_rage_1_front', 'expansion/feral_rage_A_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage2front', celFile: 'expansion/feral_rage_B_Front', aliases: ['feralrage2', 'feral_rage_2_front', 'expansion/feral_rage_B_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage3front', celFile: 'expansion/feral_rage_C_Front', aliases: ['feralrage3', 'feral_rage_3_front', 'expansion/feral_rage_C_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage4front', celFile: 'expansion/feral_rage_D_Front', aliases: ['feralrage4', 'feral_rage_4_front', 'expansion/feral_rage_D_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage5front', celFile: 'expansion/feral_rage_E_Front', aliases: ['feralrage5', 'feral_rage_5_front', 'expansion/feral_rage_E_Front'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage1back', celFile: 'expansion/feral_rage_A_Back', aliases: ['feral_rage_1_back', 'expansion/feral_rage_A_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage2back', celFile: 'expansion/feral_rage_B_Back', aliases: ['feral_rage_2_back', 'expansion/feral_rage_B_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage3back', celFile: 'expansion/feral_rage_C_Back', aliases: ['feral_rage_3_back', 'expansion/feral_rage_C_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage4back', celFile: 'expansion/feral_rage_D_Back', aliases: ['feral_rage_4_back', 'expansion/feral_rage_D_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'feralrage5back', celFile: 'expansion/feral_rage_E_Back', aliases: ['feral_rage_5_back', 'expansion/feral_rage_E_Back'], animRate: 16, trans: 3, preDraw: true, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'direwolfcharged', celFile: 'expansion/DireWolfCharged', aliases: ['dire_wolf_charged', 'direwolf_charged', 'expansion/DireWolfCharged'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // --------------------------------------------------------------------------- // Authentic Diablo II v1.13c Assassin Trap Overlays // --------------------------------------------------------------------------- { name: 'blade_shield', celFile: 'Expansion/blade_shield', aliases: ['bladeshield', 'Expansion/blade_shield'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, { name: 'deathsentry_explode', celFile: 'expansion/dethsentry01', aliases: ['deathsentry', 'dethsentry01', 'expansion/dethsentry01'], animRate: 16, trans: 3, preDraw: false, lightRadius: 0, lightColor: [255, 255, 255] as const, }, // Authentic Diablo II v1.13c Town Portal Object Art (Objects.txt ID 59, Token TP: tpophth.cof & tponhth.cof) { name: 'town_portal_op_tr', celFile: 'tptrlitophth', aliases: ['tptrlitophth'], animRate: 16, trans: 0, preDraw: false, lightRadius: 19, lightColor: [64, 64, 255] as const, }, { name: 'town_portal_op_hd', celFile: 'tphdlitophth', aliases: ['tphdlitophth'], animRate: 16, trans: 3, preDraw: false, lightRadius: 19, lightColor: [64, 64, 255] as const, }, { name: 'town_portal_on_tr', celFile: 'tptrlitonhth', aliases: ['tptrlitonhth'], animRate: 16, trans: 0, preDraw: false, lightRadius: 19, lightColor: [64, 64, 255] as const, }, { name: 'town_portal_on_hd', celFile: 'tphdlitonhth', aliases: ['tphdlitonhth'], animRate: 16, trans: 3, preDraw: false, lightRadius: 19, lightColor: [64, 64, 255] as const, }, ] as const export async function bakeOverlays( projectRoot: string = process.cwd(), options?: { outDir?: string; skipMetaTs?: boolean } ): Promise> { const targetRoot = options?.outDir ?? process.env.D2_PACK_OUT_DIR ?? projectRoot const archivePaths = [ join(projectRoot, 'samples', 'd2', 'Patch_D2.mpq'), join(projectRoot, 'samples', 'd2', 'd2exp.mpq'), join(projectRoot, 'samples', 'd2', 'd2data.mpq'), '/usr/local/google/home/taodao/d2-data/Patch_D2.mpq', '/usr/local/google/home/taodao/d2-data/d2exp.mpq', '/usr/local/google/home/taodao/d2-data/d2data.mpq', ] const archives: MpqArchive[] = [] for (const p of archivePaths) { try { archives.push(await MpqArchive.open(await fileSource(p))) } catch { // ignore } } async function readMpqFile(path: string): Promise { for (const a of archives) { const f = a.find(path) if (f) return a.read(f) } return null } const palBytes = await readMpqFile('data/global/palette/ACT1/pal.pl2') if (!palBytes) throw new Error('data/global/palette/ACT1/pal.pl2 not found in MPQ archives') const pl2 = decodePl2(palBytes) const publicOutDir = join(targetRoot, 'public', 'overlays') const packOutDir = join(targetRoot, 'samples', 'd2-packs', 'overlays') mkdirSync(publicOutDir, { recursive: true }) mkdirSync(packOutDir, { recursive: true }) const metaRecord: Record = {} for (const target of OVERLAY_TARGETS) { const raw = (await readMpqFile(`data/global/overlays/${target.celFile}.dcc`)) ?? (await readMpqFile(`data/global/objects/tp/tr/${target.celFile}.dcc`)) ?? (await readMpqFile(`data/global/objects/tp/hd/${target.celFile}.dcc`)) if (!raw) throw new Error(`Overlay DCC not found: data/global/overlays/${target.celFile}.dcc`) const dcc = decodeDcc(raw) const firstDir = dcc.directions[0] if (!firstDir) throw new Error(`Overlay DCC has no directions: ${target.celFile}`) const box: OverlayBox = { left: firstDir.box.left, top: firstDir.box.top, width: firstDir.box.width, height: firstDir.box.height, } const groups = dcc.directions.map(dir => ({ frames: dir.frames.map( (frame): SpriteFrame => ({ width: frame.frame.width, height: frame.frame.height, indices: frame.frame.indices, mask: frame.frame.mask, anchorX: dir.box.left, anchorY: dir.box.top, }), ), })) const sheet: SpriteSheet = { groups, width: null } const packed = packMissileSheetToRgbaPng(sheet, pl2.rgb, target.trans) const meta: OverlayMeta = { name: target.name, celFile: target.celFile, width: packed.width, height: packed.height, directions: dcc.directions.length, frames: firstDir.frames.length, framesPerDirection: firstDir.frames.length, animRate: target.animRate, trans: target.trans, preDraw: target.preDraw, lightRadius: target.lightRadius, lightColor: target.lightColor, box, groups: packed.groups, } metaRecord[target.name] = meta metaRecord[target.celFile] = meta if (target.celFile.toLowerCase() !== target.celFile) { metaRecord[target.celFile.toLowerCase()] = meta } const allKeys = new Set([target.name, target.celFile, ...(target.aliases ?? [])]) for (const alias of target.aliases ?? []) { metaRecord[alias] = meta } const jsonStr = JSON.stringify(meta, null, 2) // Write primary name and aliases to both public/overlays/ and samples/d2-packs/overlays/ for (const fileKey of allKeys) { const pubPng = join(publicOutDir, `${fileKey}.png`) const pubJson = join(publicOutDir, `${fileKey}.json`) const packPng = join(packOutDir, `${fileKey}.png`) const packJson = join(packOutDir, `${fileKey}.json`) mkdirSync(join(pubPng, '..'), { recursive: true }) mkdirSync(join(packPng, '..'), { recursive: true }) writeFileSync(pubPng, packed.png) writeFileSync(pubJson, jsonStr, 'utf-8') writeFileSync(packPng, packed.png) writeFileSync(packJson, jsonStr, 'utf-8') if (fileKey.includes('/') || fileKey.includes('\\')) { const baseKey = fileKey.split(/[/\\]/).pop()! writeFileSync(join(publicOutDir, `${baseKey}.png`), packed.png) writeFileSync(join(publicOutDir, `${baseKey}.json`), jsonStr, 'utf-8') writeFileSync(join(packOutDir, `${baseKey}.png`), packed.png) writeFileSync(join(packOutDir, `${baseKey}.json`), jsonStr, 'utf-8') writeFileSync(join(publicOutDir, `${baseKey.toLowerCase()}.png`), packed.png) writeFileSync(join(publicOutDir, `${baseKey.toLowerCase()}.json`), jsonStr, 'utf-8') writeFileSync(join(packOutDir, `${baseKey.toLowerCase()}.png`), packed.png) writeFileSync(join(packOutDir, `${baseKey.toLowerCase()}.json`), jsonStr, 'utf-8') } } console.log( `[Overlay Packer] Baked ${target.name} (${target.celFile}.dcc): ` + `${meta.frames} frames, box [${box.left}, ${box.top}, ${box.width}, ${box.height}], ` + `${packed.width}x${packed.height}, ${packed.png.byteLength} bytes PNG`, ) } const TARGET_CONST_NAMES: Record = { fire_cast_1: 'FIRE_CAST_1_META', fire_cast_2: 'FIRE_CAST_2_META', ice_cast_1: 'ICE_CAST_1_META', ice_cast_2: 'ICE_CAST_2_META', ice_cast_3: 'ICE_CAST_3_META', light_cast_1: 'LIGHT_CAST_1_META', light_cast_2: 'LIGHT_CAST_2_META', teleport: 'TELEPORT_META', might_front: 'MIGHT_FRONT_AURA_META', might_back: 'MIGHT_BACK_AURA_META', prayer_front: 'PRAYER_FRONT_AURA_META', prayer_back: 'PRAYER_BACK_AURA_META', resist_fire: 'RESIST_FIRE_AURA_META', holy_fire_front: 'HOLY_FIRE_FRONT_AURA_META', holy_fire_back: 'HOLY_FIRE_BACK_AURA_META', thorns_front: 'THORNS_FRONT_AURA_META', thorns_back: 'THORNS_BACK_AURA_META', defiance_front: 'DEFIANCE_FRONT_AURA_META', defiance_back: 'DEFIANCE_BACK_AURA_META', resist_cold: 'RESIST_COLD_AURA_META', blessed_aim_front: 'BLESSED_AIM_FRONT_AURA_META', blessed_aim_back: 'BLESSED_AIM_BACK_AURA_META', cleansing_front: 'CLEANSING_FRONT_AURA_META', cleansing_back: 'CLEANSING_BACK_AURA_META', resist_lightning: 'RESIST_LIGHTNING_AURA_META', concentration_front: 'CONCENTRATION_FRONT_AURA_META', concentration_back: 'CONCENTRATION_BACK_AURA_META', holy_freeze: 'HOLY_FREEZE_AURA_META', vigor_front: 'VIGOR_FRONT_AURA_META', vigor_back: 'VIGOR_BACK_AURA_META', holy_shock_front: 'HOLY_SHOCK_FRONT_AURA_META', holy_shock_back: 'HOLY_SHOCK_BACK_AURA_META', sanctuary_front: 'SANCTUARY_FRONT_AURA_META', sanctuary_back: 'SANCTUARY_BACK_AURA_META', meditation: 'MEDITATION_AURA_META', fanaticism_front: 'FANATICISM_FRONT_AURA_META', fanaticism_back: 'FANATICISM_BACK_AURA_META', conviction: 'CONVICTION_AURA_META', redemption: 'REDEMPTION_AURA_META', salvation_front: 'SALVATION_FRONT_AURA_META', salvation_back: 'SALVATION_BACK_AURA_META', // 9 Sorceress Overlays lightning: 'LIGHTNING_OVERLAY_META', frozenarmor: 'FROZEN_ARMOR_OVERLAY_META', frozenarmor_hit: 'FROZEN_ARMOR_HIT_OVERLAY_META', energyshield: 'ENERGY_SHIELD_OVERLAY_META', energyshieldhit0: 'ENERGY_SHIELD_HIT_OVERLAY_META', thunderstormcast: 'THUNDERSTORM_CAST_OVERLAY_META', thunderstormback: 'THUNDERSTORM_BACK_OVERLAY_META', enchant: 'ENCHANT_OVERLAY_META', ice_explode: 'ICE_EXPLODE_OVERLAY_META', // 15 Necromancer Overlays curseamplifydamage: 'CURSE_AMPLIFY_DAMAGE_OVERLAY_META', cursedimvision: 'CURSE_DIM_VISION_OVERLAY_META', curseweaken: 'CURSE_WEAKEN_OVERLAY_META', curseironmaiden: 'CURSE_IRON_MAIDEN_OVERLAY_META', curseterror: 'CURSE_TERROR_OVERLAY_META', curseconfuse: 'CURSE_CONFUSE_OVERLAY_META', cursereversevampire: 'CURSE_LIFE_TAP_OVERLAY_META', curseattract: 'CURSE_ATTRACT_OVERLAY_META', cursedecrepify: 'CURSE_DECREPIFY_OVERLAY_META', curselowerresist: 'CURSE_LOWER_RESIST_OVERLAY_META', hit_ironmaiden: 'HIT_IRON_MAIDEN_OVERLAY_META', hit_reversevampire: 'HIT_LIFE_TAP_OVERLAY_META', bonearmor_cast: 'BONE_ARMOR_CAST_OVERLAY_META', bonearmor_front: 'BONE_ARMOR_FRONT_OVERLAY_META', bonearmor_back: 'BONE_ARMOR_BACK_OVERLAY_META', // 16 Paladin Overlays charge: 'CHARGE_OVERLAY_META', conversion: 'CONVERSION_OVERLAY_META', conversioncast: 'CONVERSION_CAST_OVERLAY_META', holy_bolt_cast: 'HOLY_BOLT_CAST_OVERLAY_META', fist_of_the_heavens_cast: 'FIST_OF_THE_HEAVENS_CAST_OVERLAY_META', handofgod: 'HAND_OF_GOD_OVERLAY_META', sanctuary_knockback: 'SANCTUARY_KNOCKBACK_OVERLAY_META', holy_fire_hit: 'HOLY_FIRE_HIT_OVERLAY_META', holy_fire_indicate: 'HOLY_FIRE_INDICATE_OVERLAY_META', thorns_hit: 'THORNS_HIT_OVERLAY_META', thorns_indicate: 'THORNS_INDICATE_OVERLAY_META', cast_resistfire: 'CAST_RESIST_FIRE_OVERLAY_META', cast_resistcold: 'CAST_RESIST_COLD_OVERLAY_META', cast_resistlight: 'CAST_RESIST_LIGHTNING_OVERLAY_META', cast_resistall: 'CAST_RESIST_ALL_OVERLAY_META', stun: 'STUN_OVERLAY_META', // 10 Amazon Overlays cast_innersight: 'CAST_INNERSIGHT_OVERLAY_META', innersight: 'INNERSIGHT_OVERLAY_META', cast_slowmissiles: 'CAST_SLOWMISSILES_OVERLAY_META', handofathena: 'HANDOFATHENA_OVERLAY_META', valkyriestart: 'VALKYRIESTART_OVERLAY_META', valkyrie: 'VALKYRIE_OVERLAY_META', dopllezon_appear: 'DOPLLEZON_APPEAR_OVERLAY_META', doubledamage1: 'DOUBLEDAMAGE1_OVERLAY_META', doubledamage2: 'DOUBLEDAMAGE2_OVERLAY_META', doubledamage3: 'DOUBLEDAMAGE3_OVERLAY_META', // 10 Barbarian Overlays shoutstart: 'SHOUTSTART_OVERLAY_META', shout: 'SHOUT_OVERLAY_META', battlecry: 'BATTLECRY_OVERLAY_META', battleorderscast: 'BATTLEORDERSCAST_OVERLAY_META', battleorders: 'BATTLEORDERS_OVERLAY_META', battlecommandcast: 'BATTLECOMMANDCAST_OVERLAY_META', battlecommand: 'BATTLECOMMAND_OVERLAY_META', berserkfront: 'BERSERKFRONT_OVERLAY_META', frenzy: 'FRENZY_OVERLAY_META', warcry: 'WARCRY_OVERLAY_META', // 31 Druid Overlays druid_fire_cast_1: 'DRUID_FIRE_CAST_1_OVERLAY_META', druid_fire_cast_2: 'DRUID_FIRE_CAST_2_OVERLAY_META', druidmorph: 'DRUIDMORPH_OVERLAY_META', cyclonearmor1front: 'CYCLONEARMOR1FRONT_OVERLAY_META', cyclonearmor1back: 'CYCLONEARMOR1BACK_OVERLAY_META', cyclonearmor2front: 'CYCLONEARMOR2FRONT_OVERLAY_META', cyclonearmor2back: 'CYCLONEARMOR2BACK_OVERLAY_META', cyclonearmor3front: 'CYCLONEARMOR3FRONT_OVERLAY_META', cyclonearmor3back: 'CYCLONEARMOR3BACK_OVERLAY_META', rabiesplague: 'RABIESPLAGUE_OVERLAY_META', maul1front: 'MAUL1FRONT_OVERLAY_META', maul2front: 'MAUL2FRONT_OVERLAY_META', maul3front: 'MAUL3FRONT_OVERLAY_META', maul4front: 'MAUL4FRONT_OVERLAY_META', maul5front: 'MAUL5FRONT_OVERLAY_META', maul1back: 'MAUL1BACK_OVERLAY_META', maul2back: 'MAUL2BACK_OVERLAY_META', maul3back: 'MAUL3BACK_OVERLAY_META', maul4back: 'MAUL4BACK_OVERLAY_META', maul5back: 'MAUL5BACK_OVERLAY_META', feralrage1front: 'FERALRAGE1FRONT_OVERLAY_META', feralrage2front: 'FERALRAGE2FRONT_OVERLAY_META', feralrage3front: 'FERALRAGE3FRONT_OVERLAY_META', feralrage4front: 'FERALRAGE4FRONT_OVERLAY_META', feralrage5front: 'FERALRAGE5FRONT_OVERLAY_META', feralrage1back: 'FERALRAGE1BACK_OVERLAY_META', feralrage2back: 'FERALRAGE2BACK_OVERLAY_META', feralrage3back: 'FERALRAGE3BACK_OVERLAY_META', feralrage4back: 'FERALRAGE4BACK_OVERLAY_META', feralrage5back: 'FERALRAGE5BACK_OVERLAY_META', direwolfcharged: 'DIREWOLFCHARGED_OVERLAY_META', } const individualConsts = OVERLAY_TARGETS.map(t => { const constName = TARGET_CONST_NAMES[t.name] ?? `${t.name.toUpperCase()}_META` return `export const ${constName}: OverlayMeta = ${JSON.stringify(metaRecord[t.name], null, 2)}` }).join('\n\n') const compatAliases = ` // Backward compatibility aliases for primary aura constants export const MIGHT_AURA_META: OverlayMeta = MIGHT_FRONT_AURA_META export const PRAYER_AURA_META: OverlayMeta = PRAYER_FRONT_AURA_META export const HOLY_FIRE_AURA_META: OverlayMeta = HOLY_FIRE_FRONT_AURA_META export const THORNS_AURA_META: OverlayMeta = THORNS_FRONT_AURA_META export const DEFIANCE_AURA_META: OverlayMeta = DEFIANCE_FRONT_AURA_META export const BLESSED_AIM_AURA_META: OverlayMeta = BLESSED_AIM_FRONT_AURA_META export const CLEANSING_AURA_META: OverlayMeta = CLEANSING_FRONT_AURA_META export const CONCENTRATION_AURA_META: OverlayMeta = CONCENTRATION_FRONT_AURA_META export const VIGOR_AURA_META: OverlayMeta = VIGOR_FRONT_AURA_META export const HOLY_SHOCK_AURA_META: OverlayMeta = HOLY_SHOCK_FRONT_AURA_META export const SANCTUARY_AURA_META: OverlayMeta = SANCTUARY_FRONT_AURA_META export const FANATICISM_AURA_META: OverlayMeta = FANATICISM_BACK_AURA_META export const SALVATION_AURA_META: OverlayMeta = SALVATION_FRONT_AURA_META // Backward compatibility aliases for cold armor and energy shield overlays export const SHIVER_ARMOR_OVERLAY_META: OverlayMeta = FROZEN_ARMOR_OVERLAY_META export const CHILLING_ARMOR_OVERLAY_META: OverlayMeta = FROZEN_ARMOR_OVERLAY_META export const SHIVER_ARMOR_HIT_OVERLAY_META: OverlayMeta = FROZEN_ARMOR_HIT_OVERLAY_META export const CHILLING_ARMOR_HIT_OVERLAY_META: OverlayMeta = FROZEN_ARMOR_HIT_OVERLAY_META export const ENERGY_SHIELD_BALL_OVERLAY_META: OverlayMeta = ENERGY_SHIELD_OVERLAY_META export const THUNDERSTORM_FRONT_OVERLAY_META: OverlayMeta = THUNDERSTORM_CAST_OVERLAY_META // Backward compatibility aliases for Necromancer curse & bone armor overlays export const AMPLIFY_DAMAGE_OVERLAY_META: OverlayMeta = CURSE_AMPLIFY_DAMAGE_OVERLAY_META export const DIM_VISION_OVERLAY_META: OverlayMeta = CURSE_DIM_VISION_OVERLAY_META export const WEAKEN_OVERLAY_META: OverlayMeta = CURSE_WEAKEN_OVERLAY_META export const IRON_MAIDEN_OVERLAY_META: OverlayMeta = CURSE_IRON_MAIDEN_OVERLAY_META export const TERROR_OVERLAY_META: OverlayMeta = CURSE_TERROR_OVERLAY_META export const CONFUSE_OVERLAY_META: OverlayMeta = CURSE_CONFUSE_OVERLAY_META export const LIFE_TAP_OVERLAY_META: OverlayMeta = CURSE_LIFE_TAP_OVERLAY_META export const ATTRACT_OVERLAY_META: OverlayMeta = CURSE_ATTRACT_OVERLAY_META export const DECREPIFY_OVERLAY_META: OverlayMeta = CURSE_DECREPIFY_OVERLAY_META export const LOWER_RESIST_OVERLAY_META: OverlayMeta = CURSE_LOWER_RESIST_OVERLAY_META export const BONE_ARMOR_OVERLAY_META: OverlayMeta = BONE_ARMOR_FRONT_OVERLAY_META export const BONE_ARMOR_FRONT_META: OverlayMeta = BONE_ARMOR_FRONT_OVERLAY_META export const BONE_ARMOR_BACK_META: OverlayMeta = BONE_ARMOR_BACK_OVERLAY_META export const BONE_ARMOR_CAST_META: OverlayMeta = BONE_ARMOR_CAST_OVERLAY_META // Aliases for Paladin combat & state overlays export const CONVERSION_AURA_OVERLAY_META: OverlayMeta = CONVERSION_OVERLAY_META export const FIST_OF_THE_HEAVENS_HIT_OVERLAY_META: OverlayMeta = HAND_OF_GOD_OVERLAY_META export const HANDOFGOD_OVERLAY_META: OverlayMeta = HAND_OF_GOD_OVERLAY_META // Aliases for Amazon overlays export const INNERSIGHT_CAST_OVERLAY_META: OverlayMeta = CAST_INNERSIGHT_OVERLAY_META export const INNERSIGHT_TARGET_OVERLAY_META: OverlayMeta = INNERSIGHT_OVERLAY_META export const SLOWMISSILES_CAST_OVERLAY_META: OverlayMeta = CAST_SLOWMISSILES_OVERLAY_META export const SLOWMISSILES_AURA_OVERLAY_META: OverlayMeta = HANDOFATHENA_OVERLAY_META export const VALKYRIE_CAST_OVERLAY_META: OverlayMeta = VALKYRIESTART_OVERLAY_META export const VALKYRIE_AURA_OVERLAY_META: OverlayMeta = VALKYRIE_OVERLAY_META export const DECOY_APPEAR_OVERLAY_META: OverlayMeta = DOPLLEZON_APPEAR_OVERLAY_META export const CRITICAL_STRIKE_1_OVERLAY_META: OverlayMeta = DOUBLEDAMAGE1_OVERLAY_META export const CRITICAL_STRIKE_2_OVERLAY_META: OverlayMeta = DOUBLEDAMAGE2_OVERLAY_META export const CRITICAL_STRIKE_3_OVERLAY_META: OverlayMeta = DOUBLEDAMAGE3_OVERLAY_META // Aliases for Barbarian overlays export const SHOUT_START_OVERLAY_META: OverlayMeta = SHOUTSTART_OVERLAY_META export const SHOUT_CAST_OVERLAY_META: OverlayMeta = SHOUTSTART_OVERLAY_META export const BATTLE_CRY_OVERLAY_META: OverlayMeta = BATTLECRY_OVERLAY_META export const BATTLE_ORDERS_CAST_OVERLAY_META: OverlayMeta = BATTLEORDERSCAST_OVERLAY_META export const BATTLE_ORDERS_OVERLAY_META: OverlayMeta = BATTLEORDERS_OVERLAY_META export const BATTLE_COMMAND_CAST_OVERLAY_META: OverlayMeta = BATTLECOMMANDCAST_OVERLAY_META export const BATTLE_COMMAND_OVERLAY_META: OverlayMeta = BATTLECOMMAND_OVERLAY_META export const BERSERK_OVERLAY_META: OverlayMeta = BERSERKFRONT_OVERLAY_META export const WARCRY_HIT_OVERLAY_META: OverlayMeta = WARCRY_OVERLAY_META export const TAUNT_OVERLAY_META: OverlayMeta = CURSE_CONFUSE_OVERLAY_META // Aliases for Druid overlays export const DRUID_FIRE_CAST_1_META: OverlayMeta = DRUID_FIRE_CAST_1_OVERLAY_META export const DRUID_FIRE_CAST_2_META: OverlayMeta = DRUID_FIRE_CAST_2_OVERLAY_META export const WOLF_INTO_OVERLAY_META: OverlayMeta = DRUIDMORPH_OVERLAY_META export const WOLF_UNDO_OVERLAY_META: OverlayMeta = DRUIDMORPH_OVERLAY_META export const BEAR_INTO_OVERLAY_META: OverlayMeta = DRUIDMORPH_OVERLAY_META export const BEAR_UNDO_OVERLAY_META: OverlayMeta = DRUIDMORPH_OVERLAY_META export const CYCLONE_ARMOR_1_FRONT_OVERLAY_META: OverlayMeta = CYCLONEARMOR1FRONT_OVERLAY_META export const CYCLONE_ARMOR_1_BACK_OVERLAY_META: OverlayMeta = CYCLONEARMOR1BACK_OVERLAY_META export const CYCLONE_ARMOR_2_FRONT_OVERLAY_META: OverlayMeta = CYCLONEARMOR2FRONT_OVERLAY_META export const CYCLONE_ARMOR_2_BACK_OVERLAY_META: OverlayMeta = CYCLONEARMOR2BACK_OVERLAY_META export const CYCLONE_ARMOR_3_FRONT_OVERLAY_META: OverlayMeta = CYCLONEARMOR3FRONT_OVERLAY_META export const CYCLONE_ARMOR_3_BACK_OVERLAY_META: OverlayMeta = CYCLONEARMOR3BACK_OVERLAY_META export const RABIES_OVERLAY_META: OverlayMeta = RABIESPLAGUE_OVERLAY_META export const MAUL_1_FRONT_OVERLAY_META: OverlayMeta = MAUL1FRONT_OVERLAY_META export const MAUL_2_FRONT_OVERLAY_META: OverlayMeta = MAUL2FRONT_OVERLAY_META export const MAUL_3_FRONT_OVERLAY_META: OverlayMeta = MAUL3FRONT_OVERLAY_META export const MAUL_4_FRONT_OVERLAY_META: OverlayMeta = MAUL4FRONT_OVERLAY_META export const MAUL_5_FRONT_OVERLAY_META: OverlayMeta = MAUL5FRONT_OVERLAY_META export const MAUL_1_BACK_OVERLAY_META: OverlayMeta = MAUL1BACK_OVERLAY_META export const MAUL_2_BACK_OVERLAY_META: OverlayMeta = MAUL2BACK_OVERLAY_META export const MAUL_3_BACK_OVERLAY_META: OverlayMeta = MAUL3BACK_OVERLAY_META export const MAUL_4_BACK_OVERLAY_META: OverlayMeta = MAUL4BACK_OVERLAY_META export const MAUL_5_BACK_OVERLAY_META: OverlayMeta = MAUL5BACK_OVERLAY_META export const FERAL_RAGE_1_FRONT_OVERLAY_META: OverlayMeta = FERALRAGE1FRONT_OVERLAY_META export const FERAL_RAGE_2_FRONT_OVERLAY_META: OverlayMeta = FERALRAGE2FRONT_OVERLAY_META export const FERAL_RAGE_3_FRONT_OVERLAY_META: OverlayMeta = FERALRAGE3FRONT_OVERLAY_META export const FERAL_RAGE_4_FRONT_OVERLAY_META: OverlayMeta = FERALRAGE4FRONT_OVERLAY_META export const FERAL_RAGE_5_FRONT_OVERLAY_META: OverlayMeta = FERALRAGE5FRONT_OVERLAY_META export const FERAL_RAGE_1_BACK_OVERLAY_META: OverlayMeta = FERALRAGE1BACK_OVERLAY_META export const FERAL_RAGE_2_BACK_OVERLAY_META: OverlayMeta = FERALRAGE2BACK_OVERLAY_META export const FERAL_RAGE_3_BACK_OVERLAY_META: OverlayMeta = FERALRAGE3BACK_OVERLAY_META export const FERAL_RAGE_4_BACK_OVERLAY_META: OverlayMeta = FERALRAGE4BACK_OVERLAY_META export const FERAL_RAGE_5_BACK_OVERLAY_META: OverlayMeta = FERALRAGE5BACK_OVERLAY_META export const DIRE_WOLF_CHARGED_OVERLAY_META: OverlayMeta = DIREWOLFCHARGED_OVERLAY_META ` const overlayMetasEntries: string[] = [] const seenKeys = new Set() for (const t of OVERLAY_TARGETS) { const constName = TARGET_CONST_NAMES[t.name] ?? `${t.name.toUpperCase()}_META` const keys = new Set([t.name, t.celFile, ...(t.aliases ?? [])]) for (const k of keys) { if (!seenKeys.has(k)) { seenKeys.add(k) overlayMetasEntries.push(` ${JSON.stringify(k)}: ${constName},`) } const lower = k.toLowerCase() if (!seenKeys.has(lower)) { seenKeys.add(lower) overlayMetasEntries.push(` ${JSON.stringify(lower)}: ${constName},`) } } } // Generate src/render/overlays-meta.ts const metaTsContent = `/** * Authentic Diablo II v1.13c Cast & Aura Overlay Atlas Metadata. * Auto-generated by scripts/pack-overlays.ts. * Enforces Zero Runtime MPQ/DLL Architectural Invariant. */ export interface OverlayFrameMeta { readonly x: number readonly y: number readonly w: number readonly h: number readonly anchorX: number readonly anchorY: number } export type OverlayFrameTuple = readonly [ x: number, y: number, width: number, height: number, anchorX: number, anchorY: number, ] export interface OverlayBox { readonly left: number readonly top: number readonly width: number readonly height: number } export interface OverlayMeta { readonly name: string readonly celFile: string readonly width: number readonly height: number readonly directions: number readonly frames: number readonly framesPerDirection: number readonly animRate: number readonly trans: number readonly preDraw: boolean readonly lightRadius: number readonly lightColor: readonly [number, number, number] readonly box: OverlayBox readonly groups: readonly (readonly OverlayFrameTuple[])[] } ${individualConsts} ${compatAliases} export const ALL_OVERLAY_NAMES: readonly string[] = [ ${OVERLAY_TARGETS.map(t => ` ${JSON.stringify(t.name)},`).join('\n')} ] export const OVERLAY_METAS: Readonly> = { ${overlayMetasEntries.join('\n')} } ` if (!options?.skipMetaTs && !process.env.D2_PACK_OUT_DIR && !options?.outDir) { const metaTsPath = join(projectRoot, 'src', 'render', 'overlays-meta.ts') writeFileSync(metaTsPath, metaTsContent, 'utf-8') console.log(`[Overlay Packer] Wrote ${metaTsPath}`) } else if (process.env.D2_PACK_OUT_DIR || options?.outDir) { const sandboxMetaPath = join(targetRoot, 'src', 'render', 'overlays-meta.ts') mkdirSync(join(targetRoot, 'src', 'render'), { recursive: true }) writeFileSync(sandboxMetaPath, metaTsContent, 'utf-8') console.log(`[Overlay Packer] Wrote sandboxed ${sandboxMetaPath}`) } return metaRecord } // Execute standalone if called from CLI if (process.argv[1]?.endsWith('pack-overlays.ts')) { bakeOverlays().catch((err: unknown) => { console.error('Bake overlays failed:', err) process.exit(1) }) }