Skip to content

Instantly share code, notes, and snippets.

@PatrickJS
Forked from ThatMapleDev/Archer.md
Created July 14, 2026 08:14
Show Gist options
  • Select an option

  • Save PatrickJS/70bfff85133779797688bede619d6342 to your computer and use it in GitHub Desktop.

Select an option

Save PatrickJS/70bfff85133779797688bede619d6342 to your computer and use it in GitHub Desktop.
Archer Damage

Archer Combat Mechanics (MapleStory v40b)

SpookStory Server Implementation Guide

This document provides comprehensive documentation of the Archer combat system, covering damage formulas, the 7-random heartbeat RNG model, Final Attack mechanics, Mortal Blow, and the RNG recovery system. All formulas and logic are IDA-verified against the v40 client binary.


Table of Contents

  1. Overview
  2. Attack Types and Classifications
  3. Damage Formulas
  4. The 7-Random Heartbeat Model
  5. Final Attack Mechanics
  6. Mortal Blow Mechanics
  7. Dispatcher Decision Flow
  8. RNG Recovery Mechanism
  9. Implementation Architecture
  10. Skill ID Reference

1. Overview

Archer combat in MapleStory v40 is uniquely complex compared to other job classes. Unlike Warrior or Mage attacks which follow straightforward damage paths, Archers have multiple damage formulas that branch based on:

  • Distance to target (ranged vs. bonk)
  • Attack method (skill vs. basic attack)
  • Display byte (determines animation and formula path)
  • Packet opcode (0x16 melee vs. 0x17 ranged)

Additionally, Archers are the only class with Final Attack passive procs, which require dispatcher-level RNG consumption separate from the per-hit damage buffer.

Key Complexity Points

Mechanic Complexity
Two-tier RNG consumption (Dispatcher + Heartbeat) High
Display byte ≠ attack type (22 can be ranged OR bonk) High
Mortal Blow shares dispatcher path with FA Medium
AOE skills skip dispatcher FA check Medium
Manual swings use different formula than bonks Medium

2. Attack Types and Classifications

Display Byte Decoding

The display byte from attack packets encodes both animation type and facing direction:

Display Byte Structure:
┌─────────┬─────────────────────┐
│ Bit 7   │ Bits 0-6            │
│ Facing  │ Internal Attack Type│
│ (0x80)  │ (0x7F mask)         │
└─────────┴─────────────────────┘
  • Facing: Bit 7 = 1 means facing LEFT
  • Internal Type: Bits 0-6 determine the formula branch

Attack Type Classifications

Internal Type Name Description
9, 11 Manual Swing Out of arrows / point-blank physical swing
22-27 Bow Animations Standard ranged, bonk, or skills
51 Bonk Variant Close-range basic attack variant

Bonk vs. Ranged Determination

Critical: Display byte 22 is ambiguous! The same animation is used for:

  • Regular ranged shots (skill or basic)
  • Close-range "bonk" melee attacks

The Packet Opcode is the authoritative source:

Opcode Path Formula
0x16 (Melee) Bonk Path STR + (DEX × 3.4)
0x17 (Ranged) Standard Path DEX × 3.4 + STR

True Bonk Definition

A True Bonk occurs when ALL of these conditions are met:

  1. Display byte is 22-27
  2. Skill ID is 0 (basic attack)
  3. Packet opcode is 0x16 (processed as melee)

True bonks:

  • Skip defense reduction entirely
  • Skip critical hit checks
  • Use a specialized damage formula
  • Clamp minimum damage to 1

3. Damage Formulas

Standard Ranged Formula

Used for regular ranged attacks, skills, and non-true-bonk animations:

BaseDamage = (DEX_interp × WeaponMult + STR) × WATK × 0.01

Where:

  • DEX_interp = DEX_min + (DEX_max - DEX_min) × RNG_factor
  • DEX_min = DEX × mastery_factor
  • DEX_max = DEX
  • WeaponMult = 3.4 (Bow) or 3.6 (Crossbow)
  • WATK = Weapon Attack + Projectile Attack

Mastery Factor Calculation

mastery_factor = (mastery_property × 5.0 + 10.0) × 0.009
Mastery Level Property Factor
0 (none) 0 0.09 (9%)
3 15 0.225 (22.5%)
10 20 0.99 (99%)

Note: Use the skill's mastery property value, not the skill level directly.

True Bonk Formula

Used when display 22-27 + skillId = 0 + opcode 0x16:

BaseDamage = (STR + DEX_interp × WeaponMult) × WATK × 0.01

Key differences from Standard:

  • STR is primary (added first, not interpolated)
  • No defense reduction (damage is not reduced by mob PDD)
  • No critical hits (crit RNG is skipped)
  • Minimum damage = 1 (clamped at 0x514FA0)

Manual Swing Formula (Type 9/11)

Used for out-of-arrows attacks or point-blank melee swings:

BaseDamage = (STR + DEX_interp) × WATK × 0.005

Key characteristics:

  • Uses 0.005 coefficient (half of standard 0.01)
  • Uses default mastery (0.09 = 9%)
  • Applies defense (unlike bonks)
  • No critical hits
  • WATK excludes projectile attack (no arrows)

Defense Reduction

Standard attacks apply physical defense reduction:

# Ranged attacks (empirically verified for v40 parity):
Reduction = (PDD × 0.6) - (PDD × 0.1) × RNG_factor

# Result: Higher RNG = LESS reduction (inverted from melee)

Note: True bonks (type 22-27 with skillId=0) skip defense entirely.

Critical Hits

Critical bonus is additive, not multiplicative:

critBonus = preSkillBase × (critDamage - 100) × 0.01
finalDamage = (preSkillBase × skillPct × 0.01) + critBonus

Critical Rate = 1 + (skillLevel × 2) (e.g., level 10 = 21%)


4. The 7-Random Heartbeat Model

Upfront Buffer Fill

At the start of CalcDamagePerHit (0x513AAA), the client fills a local 7-value buffer:

┌────────────────────────────────────────────────┐
│ CalcDamagePerHit Entry (0x513AD4)              │
│                                                │
│   for (int i = 0; i < 7; i++) {                │
│       buffer[i] = CRand32::Random();           │
│   }                                            │
│                                                │
│   // Buffer is now filled with 7 values       │
│   // Indexed via: index = (hitIndex × steps + │
│   //               offset) % 7                 │
└────────────────────────────────────────────────┘

Buffer Index Usage

The buffer is accessed using cyclic indexing:

index = (hitIndex × stepsPerHit + offset) % 7
Offset Purpose Used By
0 Accuracy All attacks
1 Damage Interpolation All attacks
2 Defense Reduction Non-bonk attacks
3 Critical Hit Attacks with crit skill

Steps Per Hit by Attack Type

Attack Type Acc Dmg Def Crit Steps
Magic 3
Manual Swing (9/11) 3
Ranged (with crit) 4
Ranged (no crit) 3
True Bonk 3

Multi-Target RNG Consumption

Total RNG = 7 × NumberOfMobs

Each mob gets its own fresh 7-value buffer. Multi-hit skills hitting the same mob reuse the same buffer with different indices.

Example scenarios:

  • 1 mob hit 1 time = 7 RNG values
  • 3 mobs hit 1 time = 21 RNG values (7 × 3)
  • 1 mob hit 4 times (Strafe) = 7 RNG values (single buffer, 4 hits)

5. Final Attack Mechanics

What is Final Attack?

Final Attack (FA) is a 2nd job Archer passive skill that triggers additional attacks after regular attacks:

  • Hunter FA: Skill ID 3101002 (enables proc)
  • Crossbowman FA: Skill ID 3201002 (enables proc)
  • FA Attack Packet: Skill ID 3100001 (Bow) / 3200001 (Crossbow)

The Dispatcher Gate

FA proc checks occur in the attack dispatcher (sub_59446E), BEFORE damage calculation:

┌──────────────────────────────────────────────────────┐
│ Attack Dispatcher (sub_59446E)                       │
│                                                      │
│   1. Receive attack packet                           │
│   2. Check job = Archer?                             │
│   3. Check FA skill leveled?                         │
│   4. Check collision: ANY mob in range?              │
│   5. If (2) && (3) && (4):                          │
│        └─► Consume 1 RNG for proc roll               │
│   6. Continue to CalcDamagePerHit                    │
└──────────────────────────────────────────────────────┘

FA Proc Roll Logic (0x594BB2)

When the dispatcher determines a check is needed:

// IDA pseudocode at 0x594BB2
rng = CRand32::Random();
remainder = rng % 100;
probability = GetFASkillProbability(skillLevel);  // Level-based!

if (remainder < probability) {
    // FA PROCS - dispatch FA skill packet
    goto sub_595864;  // FA attack dispatcher
} else {
    // NO PROC
    goto loc_594890;  // Normal path
}

FA Proc Rate by Skill Level

The FA proc probability is determined by the skill's prop property, which scales with level:

FA Skill Level Proc Rate
1 ~10%
5 ~30%
10 ~40%
15 ~50%
20 ~60%

Implementation Note: Use GetSkillProbSuccess(skillId, skillLevel) to retrieve the actual prop value from skill data.

Collision Detection for FA

The client uses hitbox collision to determine if FA should be checked:

  • Hitbox: Always uses swingT1 action hitbox
  • Afterimage: Determined by weapon type
  • Orientation: From display byte bit 7
// Server implementation in FinalAttackDispatcher.cs
bool anyMobInRange = _collisionService.HasMobInAttackRange(
    playerX, playerY, facingLeft, 
    afterimage: "swordTS", 
    action: "swingT1",
    allMobs, 
    margin: 40  // 40px safety margin
);

if (anyMobInRange) {
    uint procRng = character.damageRng.Random();  // Consume FA RNG
}

FA Skip Conditions

The dispatcher skips FA consumption for:

Condition Skip FA? Reason
Display 22-27 with skillId=0 (True Bonk) Bonks don't trigger FA
FA Skill packet (3100001/3200001) FA can't trigger itself
AOE Skills (Arrow Rain/Bomb) FA in per-mob buffer
Mortal Blow Uses dispatcher for SUCCESS RATE

6. Mortal Blow Mechanics

What is Mortal Blow?

Mortal Blow is a 3rd job Hunter/Sniper skill that replaces basic bonk attacks:

  • Ranger Mortal Blow: Skill ID 3110001
  • Sniper Mortal Blow: Skill ID 3210001

Key Distinction from Final Attack

CRITICAL: Mortal Blow uses the same dispatcher path as FA, but the RNG roll determines the skill's success rate, NOT an FA proc. This must be handled separately!

Mortal Blow Success Rate by Skill Level

MB Skill Level Success Rate
1 ~5%
5 ~15%
10 ~25%
15 ~32%
20 ~40%

Note: When successful, Mortal Blow deals 150% damage + has a chance for instant kill if mob HP < 26%.

Mortal Blow Dispatcher Logic

┌──────────────────────────────────────────────────────┐
│ Mortal Blow Dispatcher Path                          │
│                                                      │
│   1. Receives packet with skillId = 3110001/3210001  │
│   2. Uses display 22 (bonk animation)                │
│   3. ALWAYS consumes 1 RNG at dispatcher level       │
│   4. This roll is for SUCCESS RATE (40%)             │
│   5. If success: High damage or instant kill         │
│   6. If fail: Normal bonk damage                     │
│                                                      │
│   NOTE: Even 0-hit whiffs consume this RNG!          │
└──────────────────────────────────────────────────────┘

Server Implementation

// In FinalAttackDispatcher.Dispatch()
if (decision.isMortalBlow)
{
    uint propRng = character.damageRng.Random();
    tracer.TraceDispatcherRng("MortalBlowProp", propRng);
}

Mortal Blow vs. Final Attack Comparison

Aspect Final Attack Mortal Blow
Skill Type Passive proc Active skill
Dispatcher RNG FA proc check Success rate check
Display Byte Varies Always 22
Skip for Bonk Display Yes No (exception)
Triggers on 0-hit Only if mob in range Always

7. Dispatcher Decision Flow

Complete Decision Tree

┌─────────────────────────────────────────────────────────────┐
│                   Attack Packet Received                    │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                Extract display byte & skillId               │
│    internalType = display & 0x7F                            │
│    isBonkDisplay = internalType >= 22 && <= 27              │
└──────────────────────────────┬──────────────────────────────┘
                               │
              ┌────────────────┴────────────────┐
              ▼                                 ▼
    ┌─────────────────────┐           ┌─────────────────────┐
    │ skillId = 3110001?  │           │ skillId = 3100001?  │
    │ (Mortal Blow)       │           │ (FA Skill Packet)   │
    └──────────┬──────────┘           └──────────┬──────────┘
               │ YES                             │ YES
               ▼                                 ▼
    ┌─────────────────────┐           ┌─────────────────────┐
    │ Consume 1 RNG for   │           │ SKIP dispatcher FA  │
    │ success rate check  │           │ (prevents recursion)│
    └─────────────────────┘           └─────────────────────┘
               │ NO                              │ NO
               ▼                                 ▼
    ┌─────────────────────────────────────────────────────────┐
    │                Is AOE Arrow Skill?                       │
    │              (Arrow Rain / Arrow Bomb)                   │
    └──────────────────────────┬──────────────────────────────┘
                               │
              ┌────────────────┴────────────────┐
              │ YES                             │ NO
              ▼                                 ▼
    ┌─────────────────────┐    ┌─────────────────────────────┐
    │ SKIP dispatcher FA  │    │ Is Bonk Display && not MB?  │
    │ (FA in per-mob buf) │    │ (display 22-27 && !mortal)  │
    └─────────────────────┘    └──────────────┬──────────────┘
                                              │
                              ┌───────────────┴───────────────┐
                              │ YES                           │ NO
                              ▼                               ▼
                   ┌─────────────────────┐      ┌─────────────────────┐
                   │ SKIP dispatcher FA  │      │ Has FA Skill?       │
                   │ (bonks skip FA)     │      │ Mob in range?       │
                   └─────────────────────┘      └──────────┬──────────┘
                                                           │ YES to both
                                                           ▼
                                               ┌─────────────────────┐
                                               │ Consume 1 RNG for   │
                                               │ FA proc roll        │
                                               └─────────────────────┘

DispatcherDecision Record

The server encapsulates this logic in a record:

public record DispatcherDecision(
    bool hasFinalAttack,        // Should check FA collision
    bool isBonkDisplay,         // Display 22-27
    bool isTrueBonk,            // Bonk + skillId=0
    bool isMortalBlow,          // 3110001/3210001
    bool isFinalAttackSkill,    // 3100001/3200001
    bool isAoeArrowSkill,       // Arrow Rain/Bomb
    bool shouldSkipFAForBonkDisplay,
    bool isMeleeSwing,          // Type 9/11
    int internalAttackType,
    int weaponType
);

8. RNG Recovery Mechanism

The Problem

Server and client can desync due to:

  • Position drift: Server's mob positions ≠ client's mob positions
  • FA collision mismatch: Server thinks mob is in range, client doesn't (or vice versa)
  • Off-by-one errors: Missing a whiff packet

The Solution: Deterministic Retry

┌─────────────────────────────────────────────────────────────┐
│                  RNG Recovery Flow                           │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  1. Save RNG state BEFORE FA decision                       │
│     stateBeforeFA = character.damageRng.GetState()          │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  2. Make FA decision (consume or skip based on collision)   │
│     faConsumed = CheckCollisionAndConsume()                 │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  3. Calculate damage for first hit                          │
│     serverDmg = CalculateDamage(...)                        │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  4. Compare with client damage                              │
│     diff = abs(serverDmg - clientDmg)                       │
└──────────────────────────────┬──────────────────────────────┘
                               │
              ┌────────────────┴────────────────┐
              │ diff > 5                        │ diff <= 5
              ▼                                 ▼
┌─────────────────────────────┐         ┌─────────────────────┐
│  5. RESTORE RNG state       │         │  Continue normally  │
│     SetState(stateBeforeFA) │         │                     │
└──────────────────┬──────────┘         └─────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│  6. TOGGLE FA consumption                                   │
│     if (faConsumed) { skip it } else { consume it }        │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  7. Retry damage calculation                                │
│     (Should now match client)                               │
└─────────────────────────────────────────────────────────────┘

Implementation

// RngRecoveryService.cs
public async Task ProcessWithRecoveryAsync(
    RngRecoveryContext context,
    IDamageTracer tracer,
    Func<Task<bool>> processMobs)
{
    bool needsRetry;
    int retryCount = 0;
    bool currentFaConsumed = context.faDispatchResult.faConsumed;
    
    do
    {
        needsRetry = await processMobs();
        
        if (needsRetry && retryCount < MaxRetries)
        {
            retryCount++;
            
            // Restore RNG state to BEFORE FA decision
            context.character.damageRng.SetState(
                context.faDispatchResult.stateBeforeFA);
            
            // Toggle FA consumption
            if (currentFaConsumed)
            {
                currentFaConsumed = false;
                // Don't consume RNG this time
            }
            else
            {
                currentFaConsumed = true;
                context.character.damageRng.Random(); // Consume
            }
        }
        else
        {
            needsRetry = false;
        }
    } while (needsRetry);
}

9. Implementation Architecture

Service Overview

┌─────────────────────────────────────────────────────────────┐
│                    AttackHandlerBase                         │
│  - Receives attack packets                                   │
│  - Orchestrates damage calculation                           │
│  - Sends damage packets to mobs                              │
└──────────────────────────────┬──────────────────────────────┘
                               │ uses
        ┌──────────────────────┼──────────────────────┐
        ▼                      ▼                      ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ IDispatcherDecision│ │ IFinalAttack     │ │ IRngRecovery     │
│ Service            │ │ Dispatcher       │ │ Service          │
├───────────────────┤ ├───────────────────┤ ├───────────────────┤
│ Analyze()         │ │ Dispatch()        │ │ ProcessWith      │
│ └─► Decision      │ │ └─► RNG consume   │ │ RecoveryAsync()  │
│     record        │ │     + state       │ │ └─► retry loop   │
└───────────────────┘ └───────────────────┘ └───────────────────┘

File Locations

MapleWorld/
├── Services/Combat/Dispatching/
│   ├── DispatcherDecision.cs        # Decision record
│   ├── IDispatcherDecisionService.cs
│   ├── DispatcherDecisionService.cs # Analyze attacks
│   ├── FinalAttackDispatchResult.cs # FA result record
│   ├── IFinalAttackDispatcher.cs
│   ├── FinalAttackDispatcher.cs     # FA collision + RNG
│   ├── RngRecoveryContext.cs        # Recovery records
│   ├── IRngRecoveryService.cs
│   └── RngRecoveryService.cs        # Retry mechanism
│
├── Handlers/Network/Attacks/
│   ├── AttackHandlerBase.cs         # Base attack handler
│   ├── MapleRangedAttackHandler.cs  # 0x17 ranged
│   ├── MapleMeleeAttackHandler.cs   # 0x16 melee
│   └── MapleMagicAttackHandler.cs   # 0x18 magic
│
└── Services/Skills/Calculators/
    ├── DamageConstants.cs           # Skill IDs, formulas
    ├── DamageOrchestrator.cs        # Pipeline orchestration
    └── Providers/
        └── BowDamageCalculator.cs   # Bow-specific formulas

10. Skill ID Reference

Passive Skills (Enable Mechanics)

Skill ID Name Effect
3000001 Critical Shot Enables critical hits
3101002 Hunter Final Attack Enables FA procs (Bow)
3201002 Crossbowman Final Attack Enables FA procs (Xbow)

Attack Skills (When FA Procs)

Skill ID Name Description
3100001 Final Attack (Bow) Actual FA attack packet
3200001 Final Attack (Crossbow) Actual FA attack packet

Special Skills

Skill ID Name Notes
3101005 Arrow Bomb AOE, FA in per-mob buffer
3111004 Arrow Rain AOE, FA in per-mob buffer
3110001 Ranger Mortal Blow Dispatcher RNG = success rate
3210001 Sniper Mortal Blow Dispatcher RNG = success rate

Code Constants

// DamageConstants.cs
public const int HunterFinalAttackSkillId = 3101002;
public const int CrossbowmanFinalAttackSkillId = 3201002;
public const int FinalAttackBowSkillId = 3100001;
public const int FinalAttackCrossbowSkillId = 3200001;
public const int RangerMortalBlowSkillId = 3110001;
public const int SniperMortalBlowSkillId = 3210001;
public const int ArrowBombSkillId = 3101005;
public const int ArrowRainSkillId = 3111004;

Appendix: Quick Reference

RNG Consumption Summary

Attack Type Dispatcher RNG Heartbeat RNG Total
Standard Ranged + FA 1 (if mob in range) 7 7-8
True Bonk (skillId=0) 0 7 7
Manual Swing (9/11) 1 (FA check) 7 8
Mortal Blow 1 (success rate) 7 8
Arrow Rain/Bomb 0 7 per mob 7×n
FA Skill Packet 0 7 7

Formula Quick Reference

Standard Ranged:  (DEX_interp × Mult + STR) × WATK × 0.01 − Defense
True Bonk:        (STR + DEX_interp × Mult) × WATK × 0.01 (no defense)
Manual Swing:     (STR + DEX_interp) × WATK × 0.005 − Defense

Last updated: 2026-01-20 Based on IDA analysis of MapleStory v40 client binary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment