Add implementation plan for Castle Ambush & Guard Capture System, including probe detection and capture mechanics
This commit is contained in:
610
Docs/Features/CastleAmbush_Plan.md
Normal file
610
Docs/Features/CastleAmbush_Plan.md
Normal file
@@ -0,0 +1,610 @@
|
||||
# Castle Ambush & Guard Capture System - Implementation Plan
|
||||
|
||||
**Status:** 🚧 Planning Phase
|
||||
**Created:** October 3, 2025
|
||||
**Target:** Future Update
|
||||
**Related Files:** `Core/capture.asm`, `Sprites/Enemies/custom_guard.asm`, `Sprites/overlord_ref.asm`
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **Castle Ambush System** will create a dynamic encounter where Link is detected by guards in the castle, captured, and warped to a dungeon. This combines:
|
||||
|
||||
1. **Probe Detection System** - Guards detect Link entering restricted areas
|
||||
2. **Guard Capture Mechanics** - Guards surround and capture Link
|
||||
3. **Warp System** - Link is transported to a dungeon entrance
|
||||
4. **Overlord Management** - Multi-screen guard coordination
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Existing Components
|
||||
|
||||
#### ✅ Core/capture.asm
|
||||
**Status:** Implemented but untested
|
||||
|
||||
```asm
|
||||
Oracle_CaptureAndWarp:
|
||||
{
|
||||
STA.w $010E ; Set the target entrance ID
|
||||
LDA.b #$05 ; Game Mode 05: (hole/whirlpool transition)
|
||||
STA.b $10 ; Set the game mode
|
||||
STZ.b $2F ; Clear Link's action state
|
||||
STZ.b $5D ; Clear Link's state
|
||||
LDA.b #$02 : STA.b $71 ; Set transition flag
|
||||
RTL
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Warps Link to a specific dungeon entrance (like WallMaster)
|
||||
|
||||
**Issues to Address:**
|
||||
- [ ] Test entrance ID values (need to determine correct dungeon entrance)
|
||||
- [ ] Verify game mode $05 works for this use case
|
||||
- [ ] Add screen fade/transition effect
|
||||
- [ ] Play capture sound effect
|
||||
- [ ] Store pre-capture location for potential escape sequence
|
||||
|
||||
#### 🚧 Sprites/Enemies/custom_guard.asm
|
||||
**Status:** Prototype with duplicate code
|
||||
|
||||
**Contains:**
|
||||
1. `Oracle_CaptureAndWarp` (DUPLICATE - already in Core/capture.asm)
|
||||
2. `Hooked_Guard_Main` - Modified guard behavior
|
||||
|
||||
**Issues:**
|
||||
- [ ] Remove duplicate `Oracle_CaptureAndWarp` function
|
||||
- [ ] Complete `Hooked_Guard_Main` implementation
|
||||
- [ ] Test guard capture trigger conditions
|
||||
- [ ] Integrate with vanilla guard sprites (ID $41, $42, $43)
|
||||
|
||||
#### 📚 Sprites/overlord_ref.asm
|
||||
**Status:** Reference material (now in experimental/)
|
||||
|
||||
**Purpose:** Documents overlord path patterns for crumbling tiles
|
||||
|
||||
**Relevance:** Can be adapted for guard patrol paths
|
||||
|
||||
---
|
||||
|
||||
## System Architecture
|
||||
|
||||
### Phase 1: Detection (Probe System)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Link enters castle restricted area │
|
||||
│ SRAM flag: $7EF??? = Castle infiltration active │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Guard spawns probe sprites every 32 frames │
|
||||
│ Probe checks: │
|
||||
│ - Link within 16px radius │
|
||||
│ - Same floor layer ($0F20) │
|
||||
│ - Not invisible/bunny │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────┴────────┐
|
||||
│ Probe Hit? │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ YES │ NO
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Trigger │ │ Continue │
|
||||
│ Alert State │ │ Patrol │
|
||||
└──────┬───────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ Play alert sound ($1D) │
|
||||
│ Set SprTimerD = $B0 (176 frames) │
|
||||
│ Spawn reinforcement guards │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Phase 2: Pursuit & Capture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Alert state active (SprTimerD > 0) │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Guards converge on Link's position │
|
||||
│ - Use Guard_ChaseLinkOnOneAxis │
|
||||
│ - Spawn additional guards from off-screen │
|
||||
│ - Maximum 4 guards active │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Check capture conditions (every frame) │
|
||||
│ - Link is surrounded (guards on 3+ sides) │
|
||||
│ - Link is not moving (speed = 0) │
|
||||
│ - Link has taken damage from guard │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌────────┴────────┐
|
||||
│ Captured? │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ YES │ NO
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Initiate │ │ Continue │
|
||||
│ Capture │ │ Chase │
|
||||
└──────┬───────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ Phase 3: Warp Sequence │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Phase 3: Warp Sequence
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Freeze Link (disable input) │
|
||||
│ Play capture animation │
|
||||
│ - Link's sprite changes to "captured" pose │
|
||||
│ - Guards move to surround positions │
|
||||
│ - Screen shake effect (3 frames) │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Fade out screen ($0012 = $01) │
|
||||
│ Wait 32 frames │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Call Oracle_CaptureAndWarp │
|
||||
│ - A register = Dungeon entrance ID │
|
||||
│ - Sets game mode to $05 (transition) │
|
||||
│ - Clears Link state │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Link spawns in dungeon cell │
|
||||
│ - Set SRAM flag: $7EF??? = Captured │
|
||||
│ - Play jingle ($06) │
|
||||
│ - Trigger escape sequence │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Consolidation Plan
|
||||
|
||||
### Step 1: Organize Core Utilities
|
||||
|
||||
**Keep in `Core/capture.asm`:**
|
||||
```asm
|
||||
; Core warp functionality
|
||||
Oracle_CaptureAndWarp:
|
||||
{
|
||||
; (existing implementation)
|
||||
}
|
||||
|
||||
; NEW: Enhanced version with effects
|
||||
Oracle_CaptureAndWarp_Enhanced:
|
||||
{
|
||||
PHP
|
||||
|
||||
; Store entrance ID
|
||||
STA.w $010E
|
||||
|
||||
; Store capture flag in SRAM
|
||||
LDA.b #$01
|
||||
STA.l $7EF3D8 ; Custom flag: Has been captured
|
||||
|
||||
; Play capture sound
|
||||
LDA.b #$1D ; Alert/Capture sound
|
||||
STA.w $012E
|
||||
|
||||
; Fade out screen
|
||||
LDA.b #$01
|
||||
STA.w $0012 ; Request fade
|
||||
|
||||
; Set up timer for transition
|
||||
LDA.b #$40 ; Wait 64 frames
|
||||
STA.b $00
|
||||
|
||||
.wait_fade
|
||||
LDA.b $00 : BNE .wait_fade
|
||||
|
||||
; Execute warp
|
||||
LDA.w $010E ; Get entrance ID back
|
||||
STA.w $010E
|
||||
|
||||
LDA.b #$05 ; Game Mode 05
|
||||
STA.b $10
|
||||
STZ.b $2F
|
||||
STZ.b $5D
|
||||
|
||||
LDA.b #$02
|
||||
STA.b $71
|
||||
|
||||
PLP
|
||||
RTL
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Create Unified Guard Sprite
|
||||
|
||||
**New file: `Sprites/Enemies/castle_guard.asm`**
|
||||
|
||||
This will replace `Sprites/Enemies/custom_guard.asm` with a complete implementation:
|
||||
|
||||
```asm
|
||||
; =========================================================
|
||||
; Castle Guard - Ambush & Capture Variant
|
||||
; =========================================================
|
||||
|
||||
!SPRID = Sprite_CastleGuard ; Use new sprite ID or override vanilla
|
||||
!NbrTiles = 02
|
||||
!Health = 08
|
||||
!Damage = 04
|
||||
; ... other properties ...
|
||||
|
||||
%Set_Sprite_Properties(Sprite_CastleGuard_Prep, Sprite_CastleGuard_Long)
|
||||
|
||||
; States
|
||||
!STATE_PATROL = 0
|
||||
!STATE_ALERT = 1
|
||||
!STATE_CHASE = 2
|
||||
!STATE_CAPTURE = 3
|
||||
|
||||
Sprite_CastleGuard_Long:
|
||||
{
|
||||
PHB : PHK : PLB
|
||||
JSR Sprite_CastleGuard_Draw
|
||||
JSL Sprite_DrawShadow
|
||||
JSL Sprite_CheckActive : BCC .inactive
|
||||
JSR Sprite_CastleGuard_Main
|
||||
.inactive
|
||||
PLB
|
||||
RTL
|
||||
}
|
||||
|
||||
Sprite_CastleGuard_Prep:
|
||||
{
|
||||
PHB : PHK : PLB
|
||||
|
||||
; Check if castle ambush is active
|
||||
LDA.l $7EF3D7 : BEQ .no_ambush ; Custom flag: Castle ambush active
|
||||
; Enable enhanced AI
|
||||
LDA.b #$01 : STA.w $0E80, X ; Custom flag for this guard
|
||||
.no_ambush
|
||||
|
||||
; Standard health based on sword level
|
||||
LDA.l $7EF359 : TAY
|
||||
LDA.w .health, Y : STA.w SprHealth, X
|
||||
|
||||
; Enable parrying
|
||||
LDA.b #$80 : STA.w SprDefl, X
|
||||
|
||||
PLB
|
||||
RTL
|
||||
|
||||
.health
|
||||
db $04, $06, $08, $0A
|
||||
}
|
||||
|
||||
Sprite_CastleGuard_Main:
|
||||
{
|
||||
; State machine
|
||||
LDA.w SprAction, X
|
||||
JSL JumpTableLocal
|
||||
|
||||
dw CastleGuard_Patrol
|
||||
dw CastleGuard_Alert
|
||||
dw CastleGuard_Chase
|
||||
dw CastleGuard_Capture
|
||||
}
|
||||
|
||||
CastleGuard_Patrol:
|
||||
{
|
||||
; Check if castle ambush should activate
|
||||
LDA.l $7EF3D7 : BEQ .normal_patrol
|
||||
|
||||
; Check distance to Link
|
||||
JSL GetDistance8bit_Long : CMP.b #$80 : BCS .no_probe
|
||||
|
||||
; Spawn probe for detection
|
||||
LDA.w SprTimerA, X : BNE .no_probe
|
||||
LDA.b #$20 : STA.w SprTimerA, X ; Spawn every 32 frames
|
||||
JSL Sprite_SpawnProbeAlways_long
|
||||
.no_probe
|
||||
|
||||
; Check if probe triggered alert
|
||||
LDA.w SprTimerD, X : BEQ .normal_patrol
|
||||
; Probe detected Link!
|
||||
LDA.b #!STATE_ALERT : STA.w SprAction, X
|
||||
|
||||
; Play alert sound
|
||||
LDA.b #$1D : STA.w $012E
|
||||
|
||||
; Spawn reinforcements
|
||||
JSR CastleGuard_SpawnReinforcements
|
||||
|
||||
RTS
|
||||
|
||||
.normal_patrol
|
||||
; Standard patrol behavior
|
||||
JSR Guard_StandardPatrol
|
||||
RTS
|
||||
}
|
||||
|
||||
CastleGuard_Alert:
|
||||
{
|
||||
; Transition to chase after alert plays
|
||||
LDA.w SprTimerD, X : CMP.b #$A0 : BCS .stay_alert
|
||||
LDA.b #!STATE_CHASE : STA.w SprAction, X
|
||||
.stay_alert
|
||||
|
||||
; Face Link
|
||||
JSL Sprite_DirectionToFacePlayer : TYA : STA.w SprMiscC, X
|
||||
|
||||
; Draw with alert animation
|
||||
LDA.b #$08 : STA.w SprGfx, X
|
||||
RTS
|
||||
}
|
||||
|
||||
CastleGuard_Chase:
|
||||
{
|
||||
; Move toward Link
|
||||
LDA.b #$0C : JSL Sprite_ApplySpeedTowardsPlayer
|
||||
JSL Guard_ChaseLinkOnOneAxis
|
||||
JSL Sprite_Move
|
||||
JSL Sprite_BounceFromTileCollision
|
||||
|
||||
; Check if Link is surrounded
|
||||
JSR CastleGuard_CheckSurrounded : BCC .not_surrounded
|
||||
; Capture Link!
|
||||
LDA.b #!STATE_CAPTURE : STA.w SprAction, X
|
||||
LDA.b #$60 : STA.w SprTimerC, X ; Capture animation duration
|
||||
|
||||
; Freeze Link
|
||||
LDA.b #$17 : STA.b $5D ; Link state: captured
|
||||
STZ.b $67 ; Stop Link's movement
|
||||
RTS
|
||||
|
||||
.not_surrounded
|
||||
; Continue chase
|
||||
JSL Guard_ParrySwordAttacks
|
||||
JSL Sprite_CheckDamageFromPlayer
|
||||
RTS
|
||||
}
|
||||
|
||||
CastleGuard_Capture:
|
||||
{
|
||||
; Animate capture sequence
|
||||
LDA.w SprTimerC, X : BNE .animating
|
||||
; Capture complete - warp Link
|
||||
LDA.b #$42 ; Entrance ID for dungeon cell
|
||||
JSL Oracle_CaptureAndWarp_Enhanced
|
||||
RTS
|
||||
|
||||
.animating
|
||||
; Guards surround Link
|
||||
; ... capture animation logic ...
|
||||
RTS
|
||||
}
|
||||
|
||||
CastleGuard_CheckSurrounded:
|
||||
{
|
||||
; Count guards within range in each direction
|
||||
; Return carry set if Link is surrounded
|
||||
; (3+ guards within 32px, covering different quadrants)
|
||||
|
||||
; ... implementation ...
|
||||
|
||||
CLC ; Not surrounded
|
||||
RTS
|
||||
}
|
||||
|
||||
CastleGuard_SpawnReinforcements:
|
||||
{
|
||||
; Spawn additional castle guards off-screen
|
||||
LDY.b #$00
|
||||
LDX.b #$0F
|
||||
|
||||
.spawn_loop
|
||||
LDA.w $0DD0, X : BEQ .found_slot
|
||||
DEX : BPL .spawn_loop
|
||||
RTS ; No free slots
|
||||
|
||||
.found_slot
|
||||
; Spawn guard sprite
|
||||
LDA.b #Sprite_CastleGuard : STA.w $0E20, X
|
||||
LDA.b #$09 : STA.w $0DD0, X ; Active state
|
||||
|
||||
; Position off-screen based on Link's position
|
||||
; ... positioning logic ...
|
||||
|
||||
; Set to chase state immediately
|
||||
LDA.b #!STATE_CHASE : STA.w SprAction, X
|
||||
|
||||
INY
|
||||
CPY.b #$03 : BCC .spawn_loop ; Spawn up to 3 reinforcements
|
||||
|
||||
RTS
|
||||
}
|
||||
|
||||
; ... drawing routine ...
|
||||
```
|
||||
|
||||
### Step 3: Integrate with Existing Systems
|
||||
|
||||
**Modify `Sprites/all_sprites.asm`:**
|
||||
```asm
|
||||
org $318000
|
||||
%log_start("castle_guard", !LOG_SPRITES)
|
||||
incsrc "Sprites/Enemies/castle_guard.asm"
|
||||
%log_end("castle_guard", !LOG_SPRITES)
|
||||
```
|
||||
|
||||
**Add SRAM Flags to `Core/sram.asm`:**
|
||||
```asm
|
||||
; Castle Ambush System
|
||||
$7EF3D7 = CastleAmbushActive ; 01 = ambush scenario active
|
||||
$7EF3D8 = HasBeenCaptured ; 01 = player has been captured before
|
||||
$7EF3D9 = CaptureCount ; Number of times captured
|
||||
```
|
||||
|
||||
**Add Constants to `Core/symbols.asm`:**
|
||||
```asm
|
||||
; Castle Ambush
|
||||
CastleAmbushActive = $7EF3D7
|
||||
HasBeenCaptured = $7EF3D8
|
||||
CaptureCount = $7EF3D9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Test Case 1: Detection
|
||||
- [ ] Enter castle area with ambush flag set
|
||||
- [ ] Walk near guard
|
||||
- [ ] Verify probe spawns every 32 frames
|
||||
- [ ] Walk into probe's path
|
||||
- [ ] Verify alert sound plays
|
||||
- [ ] Verify guard enters alert state
|
||||
|
||||
### Test Case 2: Chase
|
||||
- [ ] Continue from Test Case 1
|
||||
- [ ] Verify guard chases Link
|
||||
- [ ] Verify reinforcements spawn
|
||||
- [ ] Verify multiple guards coordinate
|
||||
- [ ] Verify guards use parrying
|
||||
|
||||
### Test Case 3: Capture
|
||||
- [ ] Let guards surround Link
|
||||
- [ ] Verify capture check works
|
||||
- [ ] Verify Link is frozen
|
||||
- [ ] Verify capture animation plays
|
||||
- [ ] Verify screen fades out
|
||||
|
||||
### Test Case 4: Warp
|
||||
- [ ] Continue from Test Case 3
|
||||
- [ ] Verify Link warps to dungeon
|
||||
- [ ] Verify SRAM flag is set
|
||||
- [ ] Verify Link spawns in correct room
|
||||
- [ ] Verify capture count increments
|
||||
|
||||
### Test Case 5: Escape
|
||||
- [ ] Escape from dungeon cell
|
||||
- [ ] Return to castle
|
||||
- [ ] Verify guards remember previous capture
|
||||
- [ ] Verify harder difficulty on subsequent captures
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase A: Core Functionality (Week 1)
|
||||
- [ ] Clean up `Core/capture.asm`
|
||||
- [ ] Add `Oracle_CaptureAndWarp_Enhanced`
|
||||
- [ ] Test basic warp functionality
|
||||
- [ ] Determine correct entrance ID for dungeon cell
|
||||
|
||||
### Phase B: Guard AI (Week 2)
|
||||
- [ ] Create `Sprites/Enemies/castle_guard.asm`
|
||||
- [ ] Implement probe detection
|
||||
- [ ] Implement state machine
|
||||
- [ ] Test patrol → alert → chase transitions
|
||||
|
||||
### Phase C: Capture Mechanics (Week 3)
|
||||
- [ ] Implement surround check
|
||||
- [ ] Implement capture animation
|
||||
- [ ] Test capture trigger conditions
|
||||
- [ ] Add sound effects
|
||||
|
||||
### Phase D: Integration (Week 4)
|
||||
- [ ] Add SRAM flags
|
||||
- [ ] Integrate with quest system
|
||||
- [ ] Create dungeon escape sequence
|
||||
- [ ] Test full cycle
|
||||
|
||||
### Phase E: Polish (Week 5)
|
||||
- [ ] Add dialogue/cutscenes
|
||||
- [ ] Add visual effects
|
||||
- [ ] Balance difficulty
|
||||
- [ ] Add achievements/tracking
|
||||
|
||||
---
|
||||
|
||||
## Entrance IDs Reference
|
||||
|
||||
Need to determine correct entrance for dungeon cell:
|
||||
|
||||
```asm
|
||||
; Common dungeon entrances
|
||||
$00 = Hyrule Castle (main entrance)
|
||||
$04 = Hyrule Castle (throne room)
|
||||
$0E = Hyrule Castle (dark passage)
|
||||
$20 = Eastern Palace
|
||||
$42 = Dark Palace
|
||||
$?? = Custom dungeon cell (TBD)
|
||||
```
|
||||
|
||||
**Action Required:** Find or create appropriate dungeon cell entrance
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### Create:
|
||||
- [ ] `Sprites/Enemies/castle_guard.asm` - Main guard implementation
|
||||
- [ ] `Docs/Features/CastleAmbush.md` - System documentation
|
||||
- [ ] `Docs/Sprites/Enemies/CastleGuard.md` - Sprite documentation
|
||||
|
||||
### Modify:
|
||||
- [ ] `Core/capture.asm` - Add enhanced version
|
||||
- [ ] `Core/sram.asm` - Add SRAM flags
|
||||
- [ ] `Core/symbols.asm` - Add constants
|
||||
- [ ] `Sprites/all_sprites.asm` - Include castle_guard.asm
|
||||
|
||||
### Delete/Consolidate:
|
||||
- [ ] `Sprites/Enemies/custom_guard.asm` - Consolidate into castle_guard.asm
|
||||
- [ ] Remove duplicate `Oracle_CaptureAndWarp` from custom_guard.asm
|
||||
|
||||
---
|
||||
|
||||
## Questions to Resolve
|
||||
|
||||
1. **Entrance ID:** Which dungeon entrance should be used for the cell?
|
||||
2. **Quest Integration:** When should castle ambush activate?
|
||||
- After certain quest milestone?
|
||||
- When Link enters specific castle area?
|
||||
- Triggered by dialogue/cutscene?
|
||||
3. **Difficulty Scaling:** Should capture difficulty increase after first capture?
|
||||
4. **Escape Sequence:** How should the escape play out?
|
||||
- Find key item?
|
||||
- Stealth section?
|
||||
- Fight way out?
|
||||
5. **Sprite Slot:** New sprite ID or override vanilla guard ($41/$42/$43)?
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- `Docs/Sprites/ProbeSprites.md` - Probe detection system
|
||||
- `Docs/Sprites/Enemies/Darknut.md` - Similar guard-type enemy
|
||||
- `Docs/Guides/SpriteCreationGuide.md` - Sprite creation reference
|
||||
- `Core/capture.asm` - Core warp functionality
|
||||
- `Sprites/experimental/probe.asm` - Probe system reference
|
||||
432
Docs/Sprites/ProbeSprites.md
Normal file
432
Docs/Sprites/ProbeSprites.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# Probe Sprite System
|
||||
|
||||
**Last Updated:** October 3, 2025
|
||||
**Purpose:** Document the probe sprite detection system used by guards and intelligent enemies
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **Probe Sprite System** is a collision detection mechanism used by guard-type sprites (Blue Guard, Green Guard, Red Guard) and intelligent enemies to detect Link's presence. Instead of constantly checking collision with Link directly, these enemies spawn invisible "probe" sprites that move in the direction they're facing. When a probe makes contact with Link, it triggers the parent sprite to enter an alert/chase state.
|
||||
|
||||
This system is more efficient than constant direct collision checks and creates more realistic enemy behavior where guards react when Link enters their "line of sight."
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Probe Spawning
|
||||
|
||||
When an enemy wants to check for Link in a specific direction, it spawns a probe sprite:
|
||||
|
||||
```asm
|
||||
; From Sprites/Enemies/darknut.asm
|
||||
JSL GetDistance8bit_Long : CMP.b #$80 : BCS .no_probe
|
||||
JSL Sprite_SpawnProbeAlways_long ; Spawn probe if Link is nearby
|
||||
.no_probe
|
||||
```
|
||||
|
||||
**Key Details:**
|
||||
- Probes are spawned only when Link is within range (distance < $80)
|
||||
- Uses sprite ID $41 (Blue Guard sprite slot)
|
||||
- Probes are invisible and have no collision with tiles
|
||||
- Probes live for a very short time (usually < 1 second)
|
||||
|
||||
### 2. Probe Properties
|
||||
|
||||
When spawned, probes are configured with special properties:
|
||||
|
||||
```asm
|
||||
; From Sprites/experimental/probe.asm (reference)
|
||||
Sprite_SpawnProbeAlways:
|
||||
{
|
||||
LDA.b #$41 ; Use guard sprite ID
|
||||
LDY.b #$0A ; Sprite slot limit
|
||||
JSL Sprite_SpawnDynamically_slot_limited
|
||||
BMI .exit
|
||||
|
||||
; Set position (slightly offset from spawner)
|
||||
LDA.b $00 : CLC : ADC.b #$08 ; +8 pixels X
|
||||
STA.w SprX, Y
|
||||
|
||||
LDA.b $02 : CLC : ADC.b #$04 ; +4 pixels Y
|
||||
STA.w SprY, Y
|
||||
|
||||
; Set velocity based on direction
|
||||
LDA.w .speed_x, X
|
||||
STA.w SprXSpeed, Y
|
||||
|
||||
LDA.w .speed_y, X
|
||||
STA.w SprYSpeed, Y
|
||||
|
||||
; Store parent sprite ID
|
||||
TXA : INC A
|
||||
STA.w $0DB0, Y ; Parent sprite index + 1
|
||||
STA.w $0BA0, Y
|
||||
|
||||
; Set timers
|
||||
LDA.b #$40
|
||||
STA.w $0F60, Y ; Lifetime timer
|
||||
STA.w $0E60, Y
|
||||
|
||||
LDA.b #$02
|
||||
STA.w $0CAA, Y ; Priority
|
||||
|
||||
.exit
|
||||
RTS
|
||||
}
|
||||
```
|
||||
|
||||
**Speed Tables:**
|
||||
```asm
|
||||
.speed_x: ; X velocities for 64 directions
|
||||
db $00, $01, $03, $04, $05, $06, $07, $08
|
||||
db $08, $08, $07, $06, $05, $04, $03, $01
|
||||
db $00, -$01, -$03, -$04, -$05, -$06, -$07, -$08
|
||||
; ... (continues for all 64 directions)
|
||||
|
||||
.speed_y: ; Y velocities for 64 directions
|
||||
db -$08, -$08, -$07, -$06, -$05, -$04, -$03, -$01
|
||||
db $00, $01, $03, $04, $05, $06, $07, $08
|
||||
; ... (continues for all 64 directions)
|
||||
```
|
||||
|
||||
### 3. Probe Detection Logic
|
||||
|
||||
The probe sprite checks for collision every frame:
|
||||
|
||||
```asm
|
||||
Probe:
|
||||
{
|
||||
; Move the probe
|
||||
LDY.b #$00
|
||||
LDA.w SprXSpeed, X : BPL .positive_x
|
||||
DEY
|
||||
.positive_x
|
||||
CLC : ADC.w SprX, X : STA.w SprX, X
|
||||
TYA : ADC.w SprXH, X : STA.w SprXH, X
|
||||
|
||||
; Same for Y...
|
||||
|
||||
; Check if probe hit Link
|
||||
REP #$20
|
||||
LDA.w $0FD8 : SEC : SBC.b $22 ; Link X - Probe X
|
||||
CLC : ADC.w #$0010 : CMP.w #$0020 ; Within hitbox?
|
||||
SEP #$20
|
||||
BCS .no_contact
|
||||
|
||||
REP #$20
|
||||
LDA.b $20 : SEC : SBC.w $0FDA ; Link Y - Probe Y
|
||||
CLC : ADC.w #$0018 : CMP.w #$0020
|
||||
SEP #$20
|
||||
BCS .no_contact
|
||||
|
||||
; Check same floor layer
|
||||
LDA.w $0F20, X : CMP.b $EE : BNE .no_contact
|
||||
|
||||
.made_contact
|
||||
; Get parent sprite index from $0DB0
|
||||
LDA.w $0DB0, X : DEC A : PHX : TAX
|
||||
|
||||
; Trigger parent sprite's alert state
|
||||
LDA.w SprAction, X : CMP.b #$03 : BEQ .dont_trigger_parent
|
||||
LDA.b #$03 : STA.w SprAction, X ; Set to chase state
|
||||
LDA.b #$10 : STA.w SprTimerA, X ; Alert duration
|
||||
STZ.w SprDelay, X
|
||||
.dont_trigger_parent
|
||||
|
||||
PLX
|
||||
|
||||
.no_contact
|
||||
; Probe has served its purpose, despawn
|
||||
STZ.w $0DD0, X
|
||||
RTS
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Parent Sprite Response
|
||||
|
||||
When a probe detects Link, the parent sprite (guard/enemy) reacts:
|
||||
|
||||
```asm
|
||||
; From darknut.asm
|
||||
LDA.w SprTimerD, X : BEQ .not_alerted
|
||||
; Probe detected Link - chase behavior
|
||||
LDA.b #$08 : JSL Sprite_ApplySpeedTowardsPlayer
|
||||
JSL Sprite_DirectionToFacePlayer
|
||||
TYA
|
||||
STA.w SprMiscC, X ; Store facing direction
|
||||
STA.w SprMiscE, X
|
||||
STA.w SprAction, X
|
||||
JSL Guard_ChaseLinkOnOneAxis
|
||||
JMP .continue
|
||||
|
||||
.not_alerted
|
||||
; Normal patrol behavior
|
||||
JSR Sprite_Darknut_BasicMove
|
||||
.continue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage in Enemies
|
||||
|
||||
### Darknut Example
|
||||
|
||||
The Darknut uses probes for detection:
|
||||
|
||||
```asm
|
||||
Sprite_Darknut_Main:
|
||||
{
|
||||
; Only spawn probe if Link is nearby
|
||||
JSL GetDistance8bit_Long : CMP.b #$80 : BCS .no_probe
|
||||
JSL Sprite_SpawnProbeAlways_long
|
||||
.no_probe
|
||||
|
||||
; Check if probe triggered alert
|
||||
LDA.w SprTimerD, X : BEQ .not_alerted
|
||||
LDA.b #$90 : STA.w SprTimerD, X ; Refresh alert timer
|
||||
; ... chase logic ...
|
||||
.not_alerted
|
||||
; ... patrol logic ...
|
||||
}
|
||||
```
|
||||
|
||||
### Guard Example (Vanilla)
|
||||
|
||||
Vanilla guards use a more sophisticated probe system:
|
||||
|
||||
```asm
|
||||
Guard_ShootProbeAndStuff:
|
||||
{
|
||||
; Calculate probe direction based on guard facing
|
||||
LDA.w SprMiscC, X ; Guard's facing direction
|
||||
|
||||
; Use direction tables to set probe velocity
|
||||
LDY.b #$00
|
||||
LDA.w ProbeAndSparkCheckDirXSpeed, Y : STA.b $00
|
||||
LDA.w ProbeAndSparkCheckDirYSpeed, Y : STA.b $01
|
||||
|
||||
; Check tile collision before spawning
|
||||
JSL Probe_CheckTileSolidity : BCC .passable
|
||||
; Don't spawn probe if blocked by wall
|
||||
RTS
|
||||
.passable
|
||||
|
||||
; Spawn the probe
|
||||
JSR Sprite_SpawnProbeAlways
|
||||
|
||||
; Set probe type and properties
|
||||
LDA.w ProbeType, Y : STA.w $0DB0, Y
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Probes
|
||||
|
||||
✅ **Good Use Cases:**
|
||||
- Guard-type enemies with "line of sight" detection
|
||||
- Enemies that patrol and should react when Link crosses their path
|
||||
- Boss phases where the boss "looks" for Link
|
||||
- Security systems or alert mechanisms
|
||||
|
||||
❌ **Poor Use Cases:**
|
||||
- Enemies that should always chase Link (use direct distance checks)
|
||||
- Fast-moving enemies (probes are too slow)
|
||||
- Enemies that need precise collision (use `Sprite_CheckDamageToLink`)
|
||||
- Passive enemies that don't react to Link
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Distance Check First:** Always check if Link is nearby before spawning probes
|
||||
```asm
|
||||
JSL GetDistance8bit_Long : CMP.b #$80 : BCS .skip_probe
|
||||
```
|
||||
|
||||
2. **Spawn Frequency:** Don't spawn probes every frame
|
||||
```asm
|
||||
LDA.w SprTimerA, X : BNE .dont_spawn ; Only spawn when timer expires
|
||||
```
|
||||
|
||||
3. **Probe Lifetime:** Keep probes short-lived (max 64 frames / ~1 second)
|
||||
|
||||
4. **Sprite Slot Limit:** Probes use `Sprite_SpawnDynamically_slot_limited` with a limit of $0A (10 sprites max)
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Pattern 1: Continuous Scanning**
|
||||
```asm
|
||||
; Spawn probe every N frames
|
||||
LDA.w SprTimerA, X : BNE .skip
|
||||
LDA.b #$20 : STA.w SprTimerA, X ; Every 32 frames
|
||||
JSL Sprite_SpawnProbeAlways_long
|
||||
.skip
|
||||
```
|
||||
|
||||
**Pattern 2: Direction-Based Detection**
|
||||
```asm
|
||||
; Only probe in facing direction
|
||||
LDA.w SprAction, X ; Current facing direction
|
||||
ASL A : TAY
|
||||
LDA.w .probe_angles, Y ; Get angle for this direction
|
||||
JSL Sprite_SpawnProbeAlways_long
|
||||
```
|
||||
|
||||
**Pattern 3: Alert State Management**
|
||||
```asm
|
||||
; Probe triggers alert, which decays over time
|
||||
LDA.w SprTimerD, X : BEQ .calm
|
||||
; Alert state - chase Link
|
||||
DEC.w SprTimerD, X ; Decay alert
|
||||
BNE .still_alert
|
||||
; Return to patrol when timer expires
|
||||
JSR Enemy_ReturnToPatrol
|
||||
.still_alert
|
||||
JSR Enemy_ChaseLink
|
||||
RTS
|
||||
.calm
|
||||
JSR Enemy_Patrol
|
||||
RTS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Other Systems
|
||||
|
||||
### Parrying System
|
||||
|
||||
Probes work alongside the guard parrying system:
|
||||
|
||||
```asm
|
||||
; Darknut blocks sword attacks while alerted
|
||||
LDA.w SprTimerD, X : BEQ .not_alert
|
||||
JSL Guard_ParrySwordAttacks ; Active parrying when alert
|
||||
.not_alert
|
||||
```
|
||||
|
||||
### Multi-Part Sprites
|
||||
|
||||
For multi-part enemies (like Kydreeok), each segment can spawn probes:
|
||||
|
||||
```asm
|
||||
; Head segment spawns probe
|
||||
LDA.w SprSubtype, X : BEQ .not_head
|
||||
JSL Sprite_SpawnProbeAlways_long
|
||||
.not_head
|
||||
```
|
||||
|
||||
### Boss Mechanics
|
||||
|
||||
Bosses can use probes for phase transitions:
|
||||
|
||||
```asm
|
||||
; Boss becomes aggressive when probe detects Link
|
||||
.phase_check
|
||||
LDA.w SprTimerD, X : BEQ .passive_phase
|
||||
LDA.b #$02 : STA.w SprAction, X ; Aggressive phase
|
||||
JMP .continue
|
||||
.passive_phase
|
||||
; Spawn probe occasionally
|
||||
LDA.w SprFrame : AND.b #$3F : BNE .continue
|
||||
JSL Sprite_SpawnProbeAlways_long
|
||||
.continue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Probes
|
||||
|
||||
### Making Probes Visible
|
||||
|
||||
For debugging, you can make probes draw sprites:
|
||||
|
||||
```asm
|
||||
; In probe sprite's main routine (normally invisible)
|
||||
JSR Sprite_PrepOamCoord
|
||||
LDA.b #$00 : JSL SpriteDraw_SingleLarge ; Draw debug sprite
|
||||
```
|
||||
|
||||
### Checking Probe State
|
||||
|
||||
Use WRAM viewer in Mesen-S:
|
||||
- `$0DB0,X` - Parent sprite index (should be parent + 1)
|
||||
- `$0DD0,X` - Probe state (should be $09 = active)
|
||||
- `$0F60,X` - Probe lifetime timer
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Problem:** Probe doesn't despawn
|
||||
- **Solution:** Ensure `STZ.w $0DD0, X` is called in all exit paths
|
||||
|
||||
**Problem:** Parent doesn't react
|
||||
- **Solution:** Check that `$0DB0,X` correctly stores parent index + 1
|
||||
|
||||
**Problem:** Probe spawns too frequently
|
||||
- **Solution:** Add distance check and timer between spawns
|
||||
|
||||
---
|
||||
|
||||
## WRAM Variables
|
||||
|
||||
### Probe-Specific Variables
|
||||
|
||||
| Address | Variable | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `$0DB0,X` | Probe Parent Index | Parent sprite slot + 1 (0 = no parent) |
|
||||
| `$0BA0,X` | Probe Parent Copy | Backup of parent index |
|
||||
| `$0F60,X` | Probe Lifetime | Frames until probe despawns |
|
||||
| `$0E60,X` | Probe Timer Copy | Backup lifetime timer |
|
||||
| `$0CAA,X` | Probe Priority | Draw priority (usually $02) |
|
||||
|
||||
### Parent Sprite Variables
|
||||
|
||||
| Address | Variable | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `SprTimerD,X` | Alert Timer | Frames remaining in alert state |
|
||||
| `SprTimerA,X` | Spawn Cooldown | Frames until next probe spawn |
|
||||
| `SprAction,X` | Behavior State | Current AI state (patrol/chase) |
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
See `Sprites/experimental/probe.asm` for the complete vanilla probe system implementation. Key enemies using probes:
|
||||
|
||||
- **Blue/Green/Red Guards** (`Sprite_41`, `Sprite_42`, `Sprite_43`) - Full probe system
|
||||
- **Darknut** (`Sprites/Enemies/darknut.asm`) - Simplified probe detection
|
||||
- **Blind Boss** (Special case: probes check for boss-specific collision)
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Proposed Features
|
||||
|
||||
1. **Directional Probes:** Spawn probes in multiple directions simultaneously
|
||||
2. **Cone of Vision:** Multiple probes in a fan pattern for wider detection
|
||||
3. **Probe Types:** Different probe behaviors (slow/fast, short/long range)
|
||||
4. **Team Alerts:** Probes trigger multiple nearby enemies
|
||||
5. **Sound Integration:** Play alert sound when probe detects Link
|
||||
|
||||
### Castle Ambush System
|
||||
|
||||
Probes will be used in the upcoming castle ambush feature:
|
||||
- Guards patrol castle corridors
|
||||
- Probes detect Link entering restricted areas
|
||||
- Detection triggers reinforcement spawn
|
||||
- Multiple guards share alert state
|
||||
- Capture sequence activates on detection
|
||||
|
||||
See: `Core/capture.asm` and `Sprites/Enemies/custom_guard.asm` (experimental)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- `Docs/Sprites/Enemies/Darknut.md` - Darknut implementation with probes
|
||||
- `Docs/Guides/SpriteCreationGuide.md` Section 10.9 - Advanced AI Patterns
|
||||
- `Core/sprite_functions.asm` - `Sprite_SpawnProbeAlways_long` function
|
||||
- `Core/symbols.asm` - Probe-related function addresses
|
||||
153
Sprites/Enemies/custom_guard.asm
Normal file
153
Sprites/Enemies/custom_guard.asm
Normal file
@@ -0,0 +1,153 @@
|
||||
; This file contains the custom logic for the guard capture sequence.
|
||||
; WIP
|
||||
|
||||
; 1. Place the new, expanded logic in a free bank ($1E).
|
||||
freedata bank $1E
|
||||
|
||||
; This routine captures the player and warps them to a specific dungeon entrance.
|
||||
; It is a modified version of the WallMaster capture logic.
|
||||
; Expects the desired entrance ID to be in the A register.
|
||||
Oracle_CaptureAndWarp:
|
||||
{
|
||||
STA.w $010E ; Set the target entrance ID
|
||||
|
||||
LDA.b #$05 ; Game Mode 05: (hole/whirlpool transition)
|
||||
STA.b $10 ; Set the game mode
|
||||
|
||||
STZ.b $2F ; Clear Link's action state
|
||||
STZ.b $5D ; Clear Link's state (e.g., walking)
|
||||
|
||||
LDA.b #$02
|
||||
STA.b $71 ; Set some kind of transition/camera flag
|
||||
|
||||
RTL
|
||||
}
|
||||
|
||||
; This is a custom version of the Guard_Main routine from bank $05.
|
||||
; It is hooked to add the capture-and-warp logic.
|
||||
Hooked_Guard_Main:
|
||||
{
|
||||
; First, execute the two instructions that were overwritten by our JSL hook.
|
||||
LDA.w $0DC0, X
|
||||
PHA
|
||||
|
||||
; --- Start of Ported Vanilla Code ---
|
||||
LDY.w SprMiscC, X
|
||||
PHY
|
||||
|
||||
LDA.w SprTimerB, X
|
||||
BEQ .looking_around
|
||||
|
||||
LDA.w $05C234, Y ; SpriteDirections_Bank05, Y
|
||||
STA.w SprMiscC, X
|
||||
|
||||
LDA.w $05C23A, Y ; SpriteDrawSteps_Bank05, Y
|
||||
STA.w $0DC0, X
|
||||
|
||||
.looking_around
|
||||
JSR $05C4B8 ; Guard_HandleAllAnimation
|
||||
|
||||
PLA
|
||||
STA.w SprMiscC, X
|
||||
|
||||
PLA
|
||||
STA.w $0DC0, X
|
||||
|
||||
LDA.w $0DD0, X
|
||||
CMP.b #$05
|
||||
BNE .Guard_NotFalling
|
||||
|
||||
LDA.b $11
|
||||
BNE .Return
|
||||
|
||||
JSR $05C4B5 ; Guard_TickTwiceAndUpdateBody
|
||||
JMP $05C4B5 ; Guard_TickTwiceAndUpdateBody
|
||||
|
||||
.Guard_NotFalling
|
||||
JSR $05C1E1 ; Sprite_CheckIfActive_Bank05
|
||||
JSL $05C55E ; Guard_ParrySwordAttacks
|
||||
|
||||
JSL $07E934 ; Sprite_CheckDamageToLink_long
|
||||
BCS .hit_im
|
||||
|
||||
LDA.w $0FDC
|
||||
BEQ .not_triggered
|
||||
|
||||
.hit_im
|
||||
; --- MODIFICATION ---
|
||||
; This is where the custom capture logic goes.
|
||||
; When a guard touches link, capture him and warp to the prison cell.
|
||||
LDA.b #$F0 ; Placeholder Entrance ID for the prison cell.
|
||||
JSL Oracle_CaptureAndWarp
|
||||
RTL
|
||||
; --- END MODIFICATION ---
|
||||
|
||||
.not_triggered
|
||||
LDA.w $0EA0, X
|
||||
BEQ .not_recoiling
|
||||
|
||||
CMP.b #$04
|
||||
BCC .not_recoiling
|
||||
|
||||
LDA.b #$04
|
||||
STA.w SprAction, X
|
||||
|
||||
LDA.b #$80
|
||||
|
||||
.continue
|
||||
JSR $05C4AF ; Guard_SetTimerAndAssertTileHitbox
|
||||
|
||||
.not_recoiling
|
||||
JSR $05C291 ; Sprite_CheckIfRecoiling_Bank05
|
||||
|
||||
LDA.w SprSubtype, X
|
||||
AND.b #$07
|
||||
CMP.b #$05
|
||||
BCS .cant_go_over_short_tiles
|
||||
|
||||
LDA.w SprCollision, X
|
||||
BNE .tile_collision
|
||||
|
||||
JSR $05C1D4 ; Sprite_Move_XY_Bank05
|
||||
|
||||
.tile_collision
|
||||
JSR $05C2A5 ; Sprite_CheckTileCollision_Bank05
|
||||
|
||||
BRA .continue_after_move
|
||||
|
||||
.cant_go_over_short_tiles
|
||||
JSR $05C1D4 ; Sprite_Move_XY_Bank05
|
||||
|
||||
.continue_after_move
|
||||
LDA.w SprAction, X
|
||||
CMP.b #$04
|
||||
BEQ .not_chasing
|
||||
|
||||
STZ.w $0ED0, X
|
||||
|
||||
.not_chasing
|
||||
REP #$30
|
||||
|
||||
AND.w #$00FF
|
||||
ASL A
|
||||
TAY
|
||||
|
||||
LDA.w $05C2C6, Y ; .vectors, Y
|
||||
DEC A
|
||||
PHA
|
||||
|
||||
SEP #$30
|
||||
|
||||
.Return
|
||||
RTS
|
||||
; --- End of Ported Vanilla Code ---
|
||||
}
|
||||
freedata clean
|
||||
|
||||
; 2. Go to the vanilla code address and inject the jump.
|
||||
; The original instructions were LDA.w $0DC0,X (3 bytes) and PHA (1 byte).
|
||||
; A JSL is 4 bytes, so this is a perfect 1-to-1 replacement in size.
|
||||
pushpc
|
||||
org $05C227 ; Start of vanilla Guard_Main
|
||||
JSL Hooked_Guard_Main
|
||||
pullpc
|
||||
Reference in New Issue
Block a user