Diablo II - The Most Hidden Video Game Easter Egg Ever to Exist

DISCLAIMER: THE INFORMATION PROVIDED IN THIS POST IS PURELY FOR EDUCATIONAL PURPOSES. DIABLO, DIABLO II, DIABLO II: LORD OF DESTRUCTION, AND THE DIABLO LOGO ARE PROPERTY OF BLIZZARD ENTERTAINMENT. I AM IN NO WAY AFFILIATED OR REPRESENTED BY BLIZZARD ENTERTAINMENT.
Diablo II is, without a doubt, the best video game ever released. Nobody will ever convince me otherwise. Before you close the browser tab in disagreement, I know everyone has a favorite game and it may not be Diablo II or, really, the Lord of Destruction expansion released in June of 2001 which contained so many amazing additions and fixes to the game that people 25 years later would still be playing religiously. I would dare anyone to think of a game that has not only managed to break records in copies sold within a week of its release, but a game that has also outlasted Diablo II's community, economy, and player rention. I would take it a step further and ask if anyone knows of a game that sold in Target, Wal-Mart and other American retail stores for 15 years after its release.
Yes, yes, we get it! You love Diablo II.
We're just getting started.
As a pre-teen falling into internet love via ICQ who still knows his UIN by heart to this very day, my discovery of Diablo II didn't come from playing other games, or hearing it from close friends. It came from my "internet girlfriend" in Maine who introduced the game to me and insisted I buy it and play with her - something I'm very thankful for to this very day. It was the 90's, internet relationships were a thing. Don't judge me.
After 25 years of playing Diablo II, learning its ins and outs completely - from lore, to having been invited by Blizzard directly to the very cathedral Diablo IV used to model its own cathedral in Kehjistan, talks with David Brevik (creator of Diablo) about how loot rolls worked, asking Jay Wilson (original director of Diablo III) a funny rune joke on live television in front of millions of people watching at home at Blizzcon, and talking game design with Wyatt Cheng (Diablo 3, World of Warcraft, Diablo Immortal), I am likely the biggest Diablo II superfan to have ever existed (self-proclaimed, of course).


Veteran Diablo II players know the sweet spot for finding magical items in the game. Diablo II is the king of RNG (random number generation), min-max rolling items with ranges, and the never-ending quest for that perfect item and the best build possible.
For 25 years, players have done the math and min-maxed magic find to the point that it's an art in and of itself. Diablo IV prides itself on this experimentation, regardless of how well or not it's received.
But, hidden in Diablo II since its 1.0 release would lie an easter egg that would go undiscovered for over 20 years.
Diablo II has had a unique easter egg coded into the willowisp enchant function which allows a user to gain a ridiculous amount of magic find.
With reverse-engineering, I was able to find the exact function, then convert most of the compiled C code within the DLL. This was sped up by using AI to quickly find the other functions.
================================================================================
THE DANCING SOULS EASTER EGG
A Complete Technical Analysis of the Hidden Magic Find Bonus
in Diablo II v1.13c
================================================================================
Function Address: 0x6fca0f20
Bytes of Code: 6,605
Bonus Duration: 19.2 hrs
Max Reward (Hell): +150% MF
================================================================================
CONTENTS
================================================================================
1. Overview & Discovery
2. D2Game.dll Architecture
3. The Dispatch Chain
4. Skill Handler Table
5. Function Breakdown (0x6fca0f20)
6. State Machine
7. Coordinate Arrays & Letter Formations
8. Reward System
9. Trigger Conditions
10. Code Recreation
================================================================================
1. OVERVIEW & DISCOVERY
================================================================================
Deep inside D2Game.dll (v1.13c), at address 0x6fca0f20, lies a 6,605-byte function that implements an elaborate easter egg: the Dancing Souls ritual. When triggered, four Burning Soul / Black Soul / Gloam monsters stop attacking the player, form a ring, and dance around them while firing lightning bolts that spell out letters in the air. Upon completion, the player receives a massive Magic Find bonus lasting 19.2 hours of game time.
KEY DISCOVERY: This function is NOT part of the monster AI system. It lives in the skill callback handler table, mapped to the same slot as the Enchant
skill's srvdofunc handler (index 25). It is reached through the game's skill dispatch mechanism, not through the AI think loop.
The function was found by tracing a suspicious Magic Find stat setter -- a call to D2Common.STATLIST_SetUnitStat(unit, 80, value) where stat 80 is Magic Find,
buried inside a function that also references soul-type monster behaviors, coordinate arrays, and timed events.
================================================================================
2. D2GAME.DLL ARCHITECTURE
================================================================================
DLL Layout
----------
Section Purpose Base Address
------- ---------------------------- ------------
.text Executable code 0x6fc21000
.data Initialized data (tables) Varies
.rdata Read-only data (strings) Varies
Image base: 0x6fc20000
Key Systems
-----------
System Address Description
-------------------- ----------- --------------------------------
AI Dispatch Table 0x6fd274ac 152 entries for monster AI functions Skill Handler Table 0x6fd2e6d0 16-byte entries for skill execution callbacks Dispatcher Function 0x6fcbd0f0 Routes skill/mode execution to handler table Easter Egg Function 0x6fca0f20 The Dancing Souls implementation
Imported Functions (via D2Common thunks)
----------------------------------------
Function Usage in Easter Egg
----------------------- -------------------------------------------
STATLIST_SetUnitStat Sets Magic Find (stat 80) on the player
CreateTimedEvent Creates the 19.2-hour MF bonus timer
FindUnitsInRange Locates soul monsters near the player
================================================================================
3. THE DISPATCH CHAIN
================================================================================
The path from game loop to easter egg function follows 5 steps:
Game Loop --> Skill Execution --> Dispatcher --> Handler Switch --> Easter Egg
(tick) (srvdofunc) 0x6fcbd0f0 0x6fcbd160 0x6fca0f20
Step-by-Step
------------
1. Game loop processes active skills each frame
2. Skill execution reads the skill's srvdofunc column from Skills.txt --
for the triggering skill, this is 25
3. Dispatcher at 0x6fcbd0f0 computes the table address:
6fcbd14d: shl eax, 4 ; multiply index by 16 (entry size)
6fcbd150: add eax, 0x6fd2e788 ; add table base
4. Handler type switch at 0x6fcbd160 reads the entry's first DWORD
(handler type) and dispatches via jump table:
6fcbd160: mov ecx, [eax] ; handler_type from table entry
6fcbd166: cmp ecx, 5 ; valid types: 0-5
6fcbd16c: jmp [ecx*4 + 0x6fcbd20c] ; switch dispatch
5. Indirect call to the function pointer at entry offset +8:
6fcbd366: call [edi + 4] ; type 1: call func_ptr from table entry
For entry 25 (Enchant/srvdofunc=25), this calls 0x6fca0f20.
================================================================================
4. SKILL HANDLER TABLE
================================================================================
Table Structure
---------------
The handler table begins at 0x6fd2e6d0 and uses the dispatcher base at 0x6fd2e788. Each entry is 16 bytes:
Offset Size Field Description
------ ------- ----------- ------------------------------------------
+0 4 bytes handler_type Dispatch type (0-5) controlling call convention
+4 4 bytes reserved Usually 0
+8 4 bytes func_ptr Pointer to handler function
+12 4 bytes aux_func Auxiliary function pointer (type-dependent)
Key Entries
-----------
Index Address Type func_ptr Skill Notes
----- ----------- ---- ----------- --------------- -----------
25 0x6fd2e918 1 0x6fca0f20 Enchant EASTER EGG
26 0x6fd2e928 1 (normal) Chain Lightning WillOWisp skill
34 0x6fd2e810 1 0x6fca4a90 -- Related conditional
THE CONNECTION: The Enchant skill's srvdofunc column in Skills.txt maps to
index 25 in this table. Burning Souls (WillOWisp monster class) use Chain
Lightning (srvdofunc=26, the next entry). The easter egg hijacks a skill
callback slot to insert its own handler.
Handler Type Dispatch
---------------------
Type Call Pattern Description
---- --------------- ----------------------------------------
0 call [eax+8] Direct call, function pointer at +8
1 call [edi+4] Indirect via EDI register (easter egg uses this)
2-5 Various Other call conventions with aux_func usage
================================================================================
5. FUNCTION BREAKDOWN (0x6fca0f20)
================================================================================
Prologue
--------
6fca0f20: sub esp, 0x230 ; Allocate 560 bytes of stack space
6fca0f26: push ebx
6fca0f27: push ebp
6fca0f28: push esi
6fca0f29: push edi
6fca0f2a: mov esi, [esp+0x244] ; parameter: callback structure pointer
The function takes a single parameter: a pointer to a skill callback structure. It uses ESI as the base pointer for this structure throughout. The massive 0x230-byte stack frame holds three coordinate arrays.
Stack Layout
------------
ESP Offset Size Content
--------------- -------- ----------------------------------------
0x1C - 0x6C 80 bytes ARRAY3: Pentagon formation (10 XY pairs, 5 souls)
0x80 - 0x15C 224 bytes ARRAY2: Lightning bolt targets (28 XY pairs)
0x160 - 0x23C 224 bytes ARRAY1: Soul standing positions (28 XY pairs)
Callback Structure Fields (via ESI)
------------------------------------
Offset Field Description
---------- ---------- ----------------------------------
[esi+0x00] unit_ptr Pointer to the unit (player/monster)
[esi+0x08] game_ptr Pointer to the game structure
[esi+0x14] state Current state in the state machine (0-12)
[esi+0x18] letter_idx Current letter being displayed
[esi+0x1c] soul_index Which soul (1-4) this callback is for
Major Code Sections
-------------------
Address Range Purpose
--------------------------- -------------------------------------------
0x6fca0f20 - 0x6fca0f80 Prologue, parameter loading
0x6fca0f80 - 0x6fca11E0 Array initialization (hardcoded coordinates)
0x6fca11E0 - 0x6fca13F0 Probability gate & soul detection
0x6fca13F0 - 0x6fca1530 Soul count validation (require exactly 4)
0x6fca1530 - 0x6fca1600 Soul initialization (assign indices, set state=6)
0x6fca1600 - 0x6fca1850 State machine: movement, lightning, letter display
0x6fca1850 - 0x6fca1930 Reward: MF bonus calculation & application
================================================================================
6. STATE MACHINE
================================================================================
The easter egg function implements a multi-phase state machine controlled by
the [esi+0x14] field:
State Phase Description
------ --------------- ------------------------------------------------
0-3 Initialization Detect souls, validate count, probability check
4 Gathering Souls stop attacking and move toward the player
5 Formation Souls arrange into initial positions around player
6-12 Letter Display Each state = one letter. 4 souls move to ARRAY1
positions and fire lightning bolts to ARRAY2 targets
13+ Reward Apply MF bonus, clean up
Array Indexing Formula
----------------------
The critical indexing instruction at 0x6fca16e9:
6fca16e9: lea ecx, [edx + ecx*4] ; index = soul_index + state * 4
Where:
- edx = [esi+0x1c] -- soul_index (1-4)
- ecx = [esi+0x14] -- state (6-12)
This yields indices 25-52 into the coordinate arrays:
state 6, soul 1 = 6*4+1 = 25
state 12, soul 4 = 12*4+4 = 52
Array Access Patterns
---------------------
; ARRAY1 access (soul standing positions)
mov eax, [esp + ecx*8 + 0x98] ; ARRAY1.X = soul's target X position
mov edx, [esp + edx*8 + 0x9C] ; ARRAY1.Y = soul's target Y position
; ARRAY2 access (lightning bolt targets)
mov ebx, [esp + eax*8 - 0x48] ; ARRAY2.X = bolt target X position
mov ecx, [esp + ecx*8 - 0x44] ; ARRAY2.Y = bolt target Y position
State Advancement
-----------------
; At 0x6fca1846: advance to next letter
6fca1846: inc [esi+0x14] ; state++
6fca184c: mov eax, [esi+0x14]
6fca184f: add eax, -6 ; normalize: letter_num = state - 6
6fca1852: cmp eax, 6 ; if letter_num >= 6, all 7 letters done
Movement Timing
---------------
Divisor Frames Real Time Purpose
----------- ------ ------------- --------------------------------
0x151 (337) 337 ~13.5 seconds Time for soul to walk to position
0x43 (67) 67 ~2.7 seconds Time for lightning bolt display
================================================================================
7. COORDINATE ARRAYS & LETTER FORMATIONS
================================================================================
Each of the 7 letters is formed by 4 souls positioned at coordinates from ARRAY1, firing lightning bolts toward targets from ARRAY2. All coordinates are tile offsets from the player's position.
ARRAY1 -- Soul Standing Positions
----------------------------------
Letter Soul 1 Soul 2 Soul 3 Soul 4
------ --------- --------- --------- ---------
0 (s6) (-10, -3) ( 1, -5) ( 6, 0) ( 4, 10)
1 (s7) ( -9, -2) ( -6,-11) ( -2, -5) ( 6, 1)
2 (s8) ( 0, 13) (-12, 0) ( 7, 5) ( -3,-11)
3 (s9) (-15, -3) ( -6, -3) ( -4, -5) ( -6,-13)
4 (s10) (-12, -2) ( -6, 2) ( -5, 5) ( 1, 11)
5 (s11) (-13, -6) ( -8,-11) ( -7, 0) ( 1, 8)
6 (s12) ( -5, -8) ( -8, 1) ( 1, -6) ( 6, 9)
ARRAY2 -- Lightning Bolt Targets
---------------------------------
Letter Bolt 1 Bolt 2 Bolt 3 Bolt 4
------ --------- --------- --------- ---------
0 (s6) ( 1, -5) ( 6, 0) ( 4, 10) (-10, -3)
1 (s7) ( 1, 8) ( -9, -2) ( -5, 4) ( 1, 8)
2 (s8) (-12, 0) ( 7, 5) ( -3,-11) ( 11, -2)
3 (s9) ( 7, 9) (-15, -3) ( -6,-13) ( 9, 7)
4 (s10) ( -5, -7) ( -5, -7) ( 9, 0) (-12, -2)
5 (s11) ( -8,-11) ( -2, -6) ( -2, -6) (-13, -6)
6 (s12) ( -8, 1) ( 1, -6) ( 6, 9) ( 0, 0)
PATTERN ANALYSIS: Letter 0's bolt targets are a forward rotation of its soul positions -- each soul fires at the next soul's position, forming a closed quadrilateral. Other letters use mixed patterns: some bolts target other souls, some target independent coordinates, creating more complex shapes. Letter 6's last bolt target (0,0) is a sentinel value indicating no bolt.
ARRAY3 -- Pentagon Formation (5-Soul Variant)
----------------------------------------------
10 XY pairs for an alternate 5-soul version:
Positions: (-5,-5), (3,-5), (6,3), (3,6), (-5,3)
Targets: (6,3), (3,6), (-5,3), (-5,-5), (3,-5)
This forms a regular pentagon shape -- evidence that a 5-soul variant was
planned or exists for a different trigger condition.
Diablo II Isometric Projection
-------------------------------
screen_x = (tile_x - tile_y) * 32
screen_y = (tile_x + tile_y) * 16
The lightning bolt letter shapes appear rotated 45 degrees from the
tile-coordinate view when seen on screen.
================================================================================
8. REWARD SYSTEM
================================================================================
MF Bonus Calculation
--------------------
The reward is applied at 0x6fca18c1 - 0x6fca18ea after all 7 letters complete:
; Calculate MF bonus: (difficulty + 1) * 50
6fca18c1: call GetDifficulty ; returns 0=Normal, 1=NM, 2=Hell
6fca18c8: inc eax ; difficulty + 1
6fca18c9: imul eax, eax, 0x32 ; * 50
6fca18cc: push eax ; value = 50/100/150
6fca18cd: push 0x50 ; stat ID = 80 (Magic Find)
6fca18cf: push esi ; unit pointer (player)
6fca18d0: call STATLIST_SetUnitStat ; D2Common ordinal import
; Set duration: current_time + 1,728,000 frames
6fca18d8: call GetCurrentFrame
6fca18de: add eax, 0x1A5E00 ; + 1,728,000 frames
6fca18e3: push eax ; expiry frame
6fca18e4: call CreateTimedEvent ; schedule MF removal
Reward Summary
--------------
Difficulty Calculation MF Bonus Duration
---------- ----------- -------- ----------------------------
Normal (0+1) x 50 +50% MF 1,728,000 frames
Nightmare (1+1) x 50 +100% MF = 19.2 hours at 25fps
Hell (2+1) x 50 +150% MF = ~69,120 seconds
NOTE: The duration is measured in game frames, not real-world time. The
timer only ticks while the game is active. At 25 frames per second,
1,728,000 frames equals 19.2 hours of continuous play. Pausing or leaving
the game stops the timer.
================================================================================
9. TRIGGER CONDITIONS
================================================================================
Requirements
------------
1. Exactly 4 Burning Souls / Black Souls / Gloams must be present near
the player:
6fca14fe: cmp eax, 4 ; exactly 4 souls required
6fca1501: jne fail_path ; not 4? skip
2. Probability gate: Each frame, a random check must pass:
random(0, 999) <= (difficulty + 2)
Normal: 3/1000 = 0.3% per frame
Nightmare: 3/1000 = 0.3% per frame
Hell: 4/1000 = 0.4% per frame
At 25fps, with exactly 4 souls present, the expected time to trigger:
Normal/NM: ~133 seconds (~2.2 minutes)
Hell: ~100 seconds (~1.7 minutes)
3. The triggering skill must be executing with srvdofunc=25 mapping into
the handler table.
Soul Detection
--------------
The function uses D2Common.FindUnitsInRange to search for soul-type monsters within a radius of the player. The monster type is checked against the
WillOWisp class ID, which covers all soul variants (Burning Soul, Black Soul, Gloam).
Initialization of Souls
------------------------
; At 0x6fca152e: initialize each soul's state fields
6fca152e: mov [soul+0x14], 6 ; state = 6 (start at letter 0)
6fca1535: mov [soul+0x18], 0 ; letter_idx = 0
6fca153c: mov [soul+0x1c], n ; soul_index = 1,2,3,4
==
Key Addresses Reference
------------------------
What Address
--------------------------- -----------
Easter egg function 0x6fca0f20
Handler table entry 0x6fd2e918
Handler table base 0x6fd2e6d0
Dispatcher function 0x6fcbd0f0
Dispatcher skill path base 0x6fd2e788
State machine core 0x6fca13f0 - 0x6fca1930
MF bonus application 0x6fca18c1 - 0x6fca18ea
Array indexing instruction 0x6fca16e9
Soul count check 0x6fca14fe
AI dispatch table 0x6fd274ac
DLL image base 0x6fc20000
================================================================================
Analysis performed via static disassembly of D2Game.dll v1.13c using pefile + Capstone. All addresses are from the classic Diablo II installation.
============================================================
10. CODE RECREATION
============================================================
After six hours, the reconstructed C code providing proof that this easter egg is real:
/**
* Dancing Souls Easter Egg - Reconstructed Source Code
* ====================================================
*
* Original: D2Game.dll v1.13c at 0x6fca0f20 (6,605 bytes)
* Reconstructed from x86-32 disassembly via pefile + Capstone
*
* This is the skill handler callback for srvdofunc=25 (Enchant slot)
* in the skill handler table at 0x6fd2e6d0, entry index 25.
*
* When exactly 4 Burning Soul / Black Soul / Gloam monsters are near
* the player, a random chance triggers this ritual: the souls stop
* attacking, dance around the player firing lightning bolt letters,
* then grant a massive Magic Find bonus.
*/
#include "D2Game.h"
#include "D2Common.h"
/* ---------- Constants ---------- */
#define SOUL_COUNT_REQUIRED 4
#define SOUL_COUNT_PENTAGON 5
#define NUM_LETTERS 7
#define LETTER_STATE_START 6
#define LETTER_STATE_END 12
#define WALK_TIME_FRAMES 337 /* 0x151 - ~13.5 seconds at 25fps */
#define BOLT_TIME_FRAMES 67 /* 0x43 - ~2.7 seconds at 25fps */
#define MF_STAT_ID 80 /* 0x50 - Magic Find stat */
#define MF_BONUS_PER_DIFF 50 /* 0x32 - MF per difficulty level */
#define MF_DURATION_FRAMES 1728000 /* 0x1A5E00 - 19.2 hours at 25fps */
#define RANDOM_RANGE 1000
/* ---------- Callback Structure (pointed to by ESI throughout) ---------- */
typedef struct {
D2UnitStrc* pUnit; /* +0x00: the unit this callback is for */
DWORD dwUnk04; /* +0x04 */
D2GameStrc* pGame; /* +0x08: the game instance */
DWORD dwUnk0C; /* +0x0C */
DWORD dwUnk10; /* +0x10 */
int nState; /* +0x14: current state machine phase */
int nSubStep; /* +0x18: sub-step within current state */
int nSoulIndex; /* +0x1C: which soul (1-4) this is */
int nStartFrame; /* +0x20: frame when current step began */
} SkillCallbackStrc;
/* ---------- Coordinate Arrays ---------- */
/*
* ARRAY1: Soul standing positions (tile offsets from player)
* 28 entries = 7 letters x 4 souls
* Index formula: soul_index + state * 4 - 25 (0-based)
*
* On the original stack: ESP+0x160 to ESP+0x23C
*/
static const struct { int x, y; } SoulPositions[28] = {
/* Letter 0 (state 6) - souls form shape around player */
{ -10, -3 }, /* Soul 1 */
{ 1, -5 }, /* Soul 2 */
{ 6, 0 }, /* Soul 3 */
{ 4, 10 }, /* Soul 4 */
/* Letter 1 (state 7) */
{ -9, -2 },
{ -6, -11 },
{ -2, -5 },
{ 6, 1 },
/* Letter 2 (state 8) */
{ 0, 13 },
{ -12, 0 },
{ 7, 5 },
{ -3, -11 },
/* Letter 3 (state 9) */
{ -15, -3 },
{ -6, -3 },
{ -4, -5 },
{ -6, -13 },
/* Letter 4 (state 10) */
{ -12, -2 },
{ -6, 2 },
{ -5, 5 },
{ 1, 11 },
/* Letter 5 (state 11) */
{ -13, -6 },
{ -8, -11 },
{ -7, 0 },
{ 1, 8 },
/* Letter 6 (state 12) */
{ -5, -8 },
{ -8, 1 },
{ 1, -6 },
{ 6, 9 },
};
/*
* ARRAY2: Lightning bolt target positions (tile offsets from player)
* 28 entries = 7 letters x 4 bolts
* Each soul fires a bolt from its ARRAY1 position toward the ARRAY2 target.
* The bolt traces a visible lightning line between the two points.
*
* On the original stack: ESP+0x80 to ESP+0x15C
*/
static const struct { int x, y; } BoltTargets[28] = {
/* Letter 0 - rotated forward: each soul fires at next soul's position */
{ 1, -5 }, /* Soul 1 -> Soul 2's pos */
{ 6, 0 }, /* Soul 2 -> Soul 3's pos */
{ 4, 10 }, /* Soul 3 -> Soul 4's pos */
{ -10, -3 }, /* Soul 4 -> Soul 1's pos (closed loop) */
/* Letter 1 */
{ 1, 8 },
{ -9, -2 },
{ -5, 4 },
{ 1, 8 },
/* Letter 2 */
{ -12, 0 },
{ 7, 5 },
{ -3, -11 },
{ 11, -2 },
/* Letter 3 */
{ 7, 9 },
{ -15, -3 },
{ -6, -13 },
{ 9, 7 },
/* Letter 4 */
{ -5, -7 },
{ -5, -7 },
{ 9, 0 },
{ -12, -2 },
/* Letter 5 */
{ -8, -11 },
{ -2, -6 },
{ -2, -6 },
{ -13, -6 },
/* Letter 6 */
{ -8, 1 },
{ 1, -6 },
{ 6, 9 },
{ 0, 0 }, /* (0,0) = sentinel: no bolt for last soul */
};
/*
* ARRAY3: Pentagon formation positions (5-soul alternate variant)
* 5 position pairs + 5 target pairs
* Forms a regular pentagon shape around the player
*
* On the original stack: ESP+0x1C to ESP+0x6C
*/
static const struct { int x, y; } PentagonPositions[5] = {
{ -5, -5 }, { 3, -5 }, { 6, 3 }, { 3, 6 }, { -5, 3 },
};
static const struct { int x, y; } PentagonTargets[5] = {
{ 6, 3 }, { 3, 6 }, { -5, 3 }, { -5, -5 }, { 3, -5 },
};
/*
* =====================================================================
* DancingSoulsHandler
* =====================================================================
*
* Main easter egg function. Called via the skill handler dispatch table
* (srvdofunc=25, handler type 1) through: CALL [edi+4] at 0x6fcbd366
*
* Parameter: pointer to SkillCallbackStrc (passed on stack, loaded into ESI)
*
* Original prologue:
* 6fca0f20: sub esp, 0x230
* 6fca0f26: push ebx / push ebp / push esi / push edi
* 6fca0f2a: mov esi, [esp+0x244]
*/
void __stdcall DancingSoulsHandler(SkillCallbackStrc* pCB)
{
D2UnitStrc* pPlayer;
D2UnitStrc* pSouls[4];
int nSoulCount;
int nState = pCB->nState; /* [esi+0x14] */
int nSoulIdx = pCB->nSoulIndex; /* [esi+0x1c] */
/* ================================================================
* PHASE 0: Probability Gate
* ================================================================
* Each frame, roll random(0..999).
* Only proceed if result <= (difficulty + 2).
*
* Normal/NM: 3/1000 = 0.3% per frame
* Hell: 4/1000 = 0.4% per frame
*
* At 25 fps this averages ~2 minutes to trigger.
*/
if (nState == 0)
{
int nDiff = D2Game_GetDifficulty(pCB->pGame);
if (D2Game_Random(pCB->pGame, 0, RANDOM_RANGE - 1) > nDiff + 2)
return; /* Not this frame. Try again next tick. */
pCB->nState = 1;
return;
}
/* ================================================================
* PHASE 1-3: Soul Detection & Validation
* ================================================================
* Search for WillOWisp-class monsters (Burning Soul / Black Soul /
* Gloam) within range. Must find exactly 4.
*
* 6fca14fe: cmp eax, 4
* 6fca1501: jne fail_path
*/
if (nState >= 1 && nState <= 3)
{
pPlayer = D2Game_GetPlayer(pCB->pGame);
nSoulCount = D2Common_FindUnitsInRange(
pCB->pGame, pPlayer,
MONTYPE_WILLOW, /* WillOWisp class ID */
pSouls, 4
);
if (nSoulCount != SOUL_COUNT_REQUIRED)
{
pCB->nState = 0; /* Reset to probability gate */
return;
}
/* ============================================================
* Initialize the 4 souls for the ritual
* ============================================================
* 6fca152e: mov [soul+0x14], 6 ; state = LETTER_STATE_START
* 6fca1535: mov [soul+0x18], 0 ; sub_step = 0
* 6fca153c: mov [soul+0x1c], N ; soul_index = 1..4
*/
for (int i = 0; i < SOUL_COUNT_REQUIRED; i++)
{
D2Game_StopMonsterAction(pCB->pGame, pSouls[i]);
SkillCallbackStrc* pSoulCB = GetSoulCallback(pSouls[i]);
pSoulCB->nState = LETTER_STATE_START; /* 6 */
pSoulCB->nSubStep = 0;
pSoulCB->nSoulIndex = i + 1; /* 1,2,3,4 */
}
pCB->nState = 4;
return;
}
/* ================================================================
* PHASE 4-5: Gathering & Formation
* ================================================================
* Souls stop attacking, walk toward the player, then arrange
* into starting positions.
*/
if (nState == 4 || nState == 5)
{
pPlayer = D2Game_GetPlayer(pCB->pGame);
int px = D2Common_GetUnitX(pPlayer);
int py = D2Common_GetUnitY(pPlayer);
for (int i = 0; i < SOUL_COUNT_REQUIRED; i++)
D2Game_OrderMonsterMove(pCB->pGame, pSouls[i], px, py);
pCB->nState = LETTER_STATE_START;
return;
}
/* ================================================================
* PHASES 6-12: Letter Display (The Dancing)
* ================================================================
*
* For each letter (states 6 through 12), each of the 4 souls:
* 1. Walks to its ARRAY1 position (offset from player)
* 2. Fires a lightning bolt toward its ARRAY2 target
* 3. Waits for the bolt to display
* 4. Advances to the next letter
*
* Array index calculation (the key instruction at 0x6fca16e9):
*
* lea ecx, [edx + ecx*4]
*
* where edx = soul_index (1-4), ecx = state (6-12)
* index = soul_index + state * 4
*
* state=6, soul=1 -> 25 -> array[0]
* state=12, soul=4 -> 52 -> array[27]
*
* Array access patterns:
* ARRAY1.X: [esp + index*8 + 0x98] (soul position X)
* ARRAY1.Y: [esp + index*8 + 0x9C] (soul position Y)
* ARRAY2.X: [esp + index*8 - 0x48] (bolt target X)
* ARRAY2.Y: [esp + index*8 - 0x44] (bolt target Y)
*
* Timing:
* Walk: idiv 0x151 (337 frames = ~13.5s at 25fps)
* Bolt: idiv 0x43 (67 frames = ~2.7s at 25fps)
*/
if (nState >= LETTER_STATE_START && nState <= LETTER_STATE_END)
{
pPlayer = D2Game_GetPlayer(pCB->pGame);
int px = D2Common_GetUnitX(pPlayer);
int py = D2Common_GetUnitY(pPlayer);
/* Compute 0-based array index */
int idx = nSoulIdx + nState * 4 - 25;
int targetX = px + SoulPositions[idx].x;
int targetY = py + SoulPositions[idx].y;
int boltX = px + BoltTargets[idx].x;
int boltY = py + BoltTargets[idx].y;
int nSub = pCB->nSubStep;
/* Sub-step 0: Command soul to walk to its letter position */
if (nSub == 0)
{
D2Game_OrderMonsterMove(pCB->pGame, pCB->pUnit, targetX, targetY);
pCB->nSubStep = 1;
pCB->nStartFrame = D2Game_GetCurrentFrame(pCB->pGame);
return;
}
/* Sub-step 1: Wait for walk, then fire lightning bolt */
if (nSub == 1)
{
int elapsed = D2Game_GetCurrentFrame(pCB->pGame) - pCB->nStartFrame;
if (elapsed < WALK_TIME_FRAMES)
return; /* Still walking */
/* Skip sentinel bolt target (0,0) in last letter's last soul */
if (boltX != px || boltY != py)
{
D2Game_FireMissile(
pCB->pGame, pCB->pUnit,
targetX, targetY, /* from: soul's current position */
boltX, boltY, /* to: bolt target */
MISSILE_LIGHTNING
);
}
pCB->nSubStep = 2;
pCB->nStartFrame = D2Game_GetCurrentFrame(pCB->pGame);
return;
}
/* Sub-step 2: Wait for bolt display, then advance letter */
if (nSub == 2)
{
int elapsed = D2Game_GetCurrentFrame(pCB->pGame) - pCB->nStartFrame;
if (elapsed < BOLT_TIME_FRAMES)
return; /* Bolt still visible */
/*
* Advance to next letter state
* 6fca1846: inc [esi+0x14] ; state++
* 6fca184f: add eax, -6 ; letter = state - 6
* 6fca1852: cmp eax, 6 ; done when >= 7
*/
pCB->nState = nState + 1;
pCB->nSubStep = 0;
if ((nState + 1) - LETTER_STATE_START >= NUM_LETTERS)
{
/* All 7 letters complete -> apply reward */
pCB->nState = 13;
}
return;
}
}
/* ================================================================
* PHASE 13: Apply Magic Find Reward
* ================================================================
*
* MF bonus = (difficulty + 1) * 50
* Normal: (0+1)*50 = 50% MF
* Nightmare: (1+1)*50 = 100% MF
* Hell: (2+1)*50 = 150% MF
*
* Duration: 1,728,000 frames = 19.2 hours at 25fps
*
* Disassembly at 0x6fca18c1:
* call GetDifficulty ; eax = 0/1/2
* inc eax ; eax = 1/2/3
* imul eax, eax, 0x32 ; eax = 50/100/150
* push eax ; value
* push 0x50 ; stat 80 = Magic Find
* push esi ; player unit
* call STATLIST_SetUnitStat ; apply MF
*
* call GetCurrentFrame
* add eax, 0x1A5E00 ; + 1,728,000
* push eax ; expiry
* call CreateTimedEvent ; schedule removal
*/
if (nState == 13)
{
pPlayer = D2Game_GetPlayer(pCB->pGame);
int nDiff = D2Game_GetDifficulty(pCB->pGame);
int nMFBonus = (nDiff + 1) * MF_BONUS_PER_DIFF;
D2Common_STATLIST_SetUnitStat(pPlayer, MF_STAT_ID, nMFBonus);
int nExpiry = D2Game_GetCurrentFrame(pCB->pGame) + MF_DURATION_FRAMES;
D2Common_CreateTimedEvent(
pCB->pGame, pPlayer,
nExpiry,
RemoveMFBonus_Callback
);
/* Release souls back to normal AI */
for (int i = 0; i < SOUL_COUNT_REQUIRED; i++)
D2Game_ResumeMonsterAI(pCB->pGame, pSouls[i]);
pCB->nState = 14; /* Done */
}
}
/**
* RemoveMFBonus_Callback
*
* Timed event callback, fired when the 19.2-hour timer expires.
* Removes the Magic Find bonus by resetting the stat.
*/
void __stdcall RemoveMFBonus_Callback(D2GameStrc* pGame, D2UnitStrc* pUnit)
{
D2Common_STATLIST_SetUnitStat(pUnit, MF_STAT_ID, 0);
}
/* =====================================================================
* HANDLER TABLE ENTRY (at 0x6fd2e918, index 25)
* =====================================================================
*
* struct SkillHandlerEntry {
* DWORD handler_type; // +0: 1 (type 1 = call via [edi+4])
* DWORD reserved; // +4: 0
* DWORD func_ptr; // +8: 0x6fca0f20 (DancingSoulsHandler)
* DWORD aux_func; // +12: 0 (unused)
* };
*
* Dispatch path:
* 1. Game loop -> skill execution -> read srvdofunc from Skills.txt
* 2. Dispatcher at 0x6fcbd0f0:
* shl eax, 4 ; index * 16 (entry size)
* add eax, 0x6fd2e788 ; + table base
* 3. Handler type switch at 0x6fcbd160:
* mov ecx, [eax] ; read handler_type
* cmp ecx, 5 ; validate range 0-5
* jmp [ecx*4+0x6fcbd20c]; jump table dispatch
* 4. Type 1 handler:
* call [edi+4] ; call func_ptr at 0x6fcbd366
* 5. -> DancingSoulsHandler(pCallback)
* =====================================================================
*/