Files
oracle-of-secrets/Docs/Guides/SpriteCreationGuide.md
2025-10-03 16:07:58 -04:00

878 lines
31 KiB
Markdown

# Sprite Creation Guide
This guide provides a step-by-step walkthrough for creating a new custom sprite in Oracle of Secrets using the project's modern sprite system.
## 1. File Setup
1. **Create the Sprite File:** Create a new `.asm` file for your sprite in the appropriate subdirectory of `Sprites/`:
* `Sprites/Enemies/` - For enemy sprites
* `Sprites/Bosses/` - For boss sprites
* `Sprites/NPCs/` - For non-playable character sprites
* `Sprites/Objects/` - For interactive objects and items
2. **Include the File:** Open `Sprites/all_sprites.asm` and add an `incsrc` directive to include your new file. The file must be placed in the correct bank section:
* **Bank 30** (`$308000`) - First bank, includes `sprite_new_table.asm` and some core sprites
* **Bank 31** (`$318000`) - Second bank, includes `sprite_functions.asm` and more sprites
* **Bank 32** (`$328000`) - Third bank for additional sprites
* **Bank 2C** (Dungeon bank) - For sprites that are part of dungeon-specific content
Example:
```asm
; In Sprites/all_sprites.asm
org $318000 ; Bank 31
%log_start("my_new_enemy", !LOG_SPRITES)
incsrc "Sprites/Enemies/MyNewEnemy.asm"
%log_end("my_new_enemy", !LOG_SPRITES)
```
3. **Assign a Sprite ID:** Choose an unused sprite ID for your sprite. You can either:
* Use a completely new ID (e.g., `$A0` through `$FF` range)
* Override a vanilla sprite ID (for replacing existing sprites)
* Share an ID with another sprite and use `SprSubtype` to differentiate behaviors
## 2. Sprite Properties
At the top of your new sprite file, define its core properties using the provided template. These `!` constants are used by the `%Set_Sprite_Properties` macro to automatically configure the sprite's behavior and integrate it into the game.
```asm
; =========================================================
; Sprite Properties
; =========================================================
!SPRID = $XX ; CHOOSE AN UNUSED SPRITE ID or use a constant like Sprite_MyNewEnemy
!NbrTiles = 02 ; Number of 8x8 tiles used in the largest frame
!Harmless = 00 ; 00 = Harmful, 01 = Harmless
!HVelocity = 00 ; Is your sprite going super fast? put 01 if it is
!Health = 10 ; Number of Health the sprite has
!Damage = 04 ; Damage dealt to Link on contact (08 = whole heart, 04 = half heart)
!DeathAnimation = 00 ; 00 = normal death, 01 = no death animation
!ImperviousAll = 00 ; 00 = Can be attacked, 01 = all attacks clink harmlessly
!SmallShadow = 00 ; 01 = small shadow, 00 = no shadow
!Shadow = 00 ; 00 = don't draw shadow, 01 = draw a shadow
!Palette = 00 ; Unused in this template (can be 0 to 7)
!Hitbox = 08 ; 00 to 31, can be viewed in sprite draw tool
!Persist = 00 ; 01 = sprite continues to live offscreen
!Statis = 00 ; 00 = sprite is alive? (kill all enemies room)
!CollisionLayer = 00 ; 01 = will check both layers for collision
!CanFall = 00 ; 01 = sprite can fall in holes, 00 = can't fall
!DeflectArrow = 00 ; 01 = deflect arrows
!WaterSprite = 00 ; 01 = can only walk in shallow water
!Blockable = 00 ; 01 = can be blocked by Link's shield
!Prize = 01 ; 00-15 = the prize pack the sprite will drop from
!Sound = 00 ; 01 = Play different sound when taking damage
!Interaction = 00 ; ?? No documentation
!Statue = 00 ; 01 = Sprite is a statue
!DeflectProjectiles = 00 ; 01 = Sprite will deflect ALL projectiles
!ImperviousArrow = 00 ; 01 = Impervious to arrows
!ImpervSwordHammer = 00 ; 01 = Impervious to sword and hammer attacks
!Boss = 00 ; 00 = normal sprite, 01 = sprite is a boss
; This macro MUST be called after the properties
%Set_Sprite_Properties(Sprite_MyNewEnemy_Prep, Sprite_MyNewEnemy_Long)
```
### Property Details
**Memory Mapping:** The `%Set_Sprite_Properties` macro writes these properties to specific ROM addresses:
* `$0DB080+!SPRID` - OAM/Harmless/HVelocity/NbrTiles
* `$0DB173+!SPRID` - Sprite HP
* `$0DB266+!SPRID` - Sprite Damage
* `$0DB359+!SPRID` - Death Animation/Impervious/Shadow/Palette flags
* `$0DB44C+!SPRID` - Collision Layer/Statis/Persist/Hitbox
* `$0DB53F+!SPRID` - DeflectArrow/Boss/CanFall flags
* `$0DB632+!SPRID` - Interaction/WaterSprite/Blockable/Sound/Prize
* `$0DB725+!SPRID` - Statue/DeflectProjectiles/Impervious flags
The macro also sets up the jump table entries at:
* `$069283+(!SPRID*2)` - Vanilla Sprite Main Pointer
* `$06865B+(!SPRID*2)` - Vanilla Sprite Prep Pointer
* `NewSprRoutinesLong+(!SPRID*3)` - New Long Sprite Pointer
* `NewSprPrepRoutinesLong+(!SPRID*3)` - New Long Sprite Prep Pointer
### Design Considerations
* **Multi-purpose Sprite IDs:** A single `!SPRID` can be used for multiple distinct behaviors (e.g., Keese, Fire Keese, Ice Keese, Vampire Bat all share sprite IDs) through the use of `SprSubtype`. This is a powerful technique for reusing sprite slots and creating variations of enemies.
* **Damage Handling for Bosses:** For boss sprites, `!Damage = 00` is common if damage is applied through other means, such as spawned projectiles or direct contact logic within the main routine.
* **Dynamic Health:** Many sprites set health dynamically in their `_Prep` routine based on game progression (e.g., Booki sets health based on Link's sword level, Darknut based on sword upgrades).
* **Custom Boss Logic:** Setting `!Boss = 00` for a boss sprite indicates that custom boss logic is being used, rather than relying on vanilla boss flags.
* **Shared Sprite IDs:** Multiple distinct NPCs or objects can share a single `!SPRID` by using `SprSubtype` for differentiation (e.g., `Sprite_Mermaid = $F0` is used for Mermaid, Maple, and Librarian with different subtypes).
## 3. Main Structure (`_Long` routine)
This is the main entry point for your sprite, called by the game engine every frame. Its primary job is to call the drawing and logic routines.
```asm
Sprite_MyNewEnemy_Long:
{
PHB : PHK : PLB ; Set up bank registers (Push Bank, Push K, Pull Bank)
JSR Sprite_MyNewEnemy_Draw
JSL Sprite_DrawShadow ; Optional: Draw a shadow (use appropriate shadow function)
JSL Sprite_CheckActive : BCC .SpriteIsNotActive ; Only run logic if active
JSR Sprite_MyNewEnemy_Main
.SpriteIsNotActive
PLB ; Restore bank register
RTL ; Return from long routine
}
```
### Important Notes
* **Bank Register Management:** Always use `PHB : PHK : PLB` at the start and `PLB` before `RTL` to ensure proper bank context.
* **Sprite_CheckActive:** This critical function checks if the sprite should execute logic based on its state, freeze status, and pause flags. Returns carry set if active.
* **Drawing Order:** Drawing is typically done before the main logic, though the order can vary based on sprite needs.
* **Conditional Drawing:** Shadow drawing might be conditional based on the sprite's current action or state (e.g., Thunder Ghost only draws shadow when grounded).
## 4. Initialization (`_Prep` routine)
This routine runs *once* when the sprite is first spawned. Use it to set initial values for timers, its action state, and any other properties. For dynamic difficulty scaling, you can adjust properties based on game progression here.
```asm
Sprite_MyNewEnemy_Prep:
{
PHB : PHK : PLB
; Set dynamic health based on sword level (optional)
LDA.l Sword : DEC A : TAY
LDA.w .health, Y : STA.w SprHealth, X
%GotoAction(0) ; Set the initial state to the first one in the jump table
%SetTimerA(120) ; Set a general-purpose timer to 120 frames (2 seconds)
PLB
RTL
; Optional: Dynamic health table
.health
db $04, $08, $10, $18 ; Health values for sword levels 1-4
}
```
### Available Sprite RAM Variables
The following WRAM addresses are available for sprite-specific data (all indexed by X):
**Position & Movement:**
* `SprY, SprX` ($0D00, $0D10) - 8-bit position coordinates (low byte)
* `SprYH, SprXH` ($0D20, $0D30) - High bytes of position
* `SprYSpeed, SprXSpeed` ($0D40, $0D50) - Movement velocities
* `SprYRound, SprXRound` ($0D60, $0D70) - Sub-pixel precision
* `SprCachedX, SprCachedY` ($0FD8, $0FDA) - Cached coordinates
**Animation & Graphics:**
* `SprAction` ($0D80) - Current state in state machine
* `SprFrame` ($0D90) - Current animation frame index
* `SprGfx` ($0DC0) - Graphics offset for drawing
* `SprFlash` ($0B89) - Flash color for damage indication
**Timers:**
* `SprTimerA-F` ($0DF0, $0E00, $0E10, $0EE0, $0F10, $0F80) - Six general-purpose timers
* Note: `SprTimerF` decreases by 2 each frame (used for gravity)
**Miscellaneous Data:**
* `SprMiscA-G` ($0DA0, $0DB0, $0DE0, $0E90, $0EB0, $0EC0, $0ED0) - Seven general-purpose variables
* `SprCustom` ($1CC0) - Additional custom data storage
**State & Properties:**
* `SprState` ($0DD0) - Sprite state (0x00=dead, 0x08=spawning, 0x09=active, etc.)
* `SprType` ($0E20) - Sprite ID
* `SprSubtype` ($0E30) - Sprite subtype for variations
* `SprHealth` ($0E50) - Current health
* `SprNbrOAM` ($0E40) - Number of OAM slots + flags
* `SprFloor` ($0F20) - Layer (0=top, 1=bottom)
* `SprHeight` ($0F80) - Z-position for altitude/jumping
### Common Initialization Patterns
```asm
; Set sprite to be impervious initially (e.g., for a boss with phases)
LDA.b #$80 : STA.w SprDefl, X
; Configure tile collision behavior
LDA.b #%01100000 : STA.w SprTileDie, X
; Set bump damage type
LDA.b #$09 : STA.w SprBump, X
; Initialize custom variables
STZ.w SprMiscA, X
STZ.w SprMiscB, X
```
## 5. Main Logic & State Machine (`_Main` routine)
This is the heart of your sprite. Use the `%SpriteJumpTable` macro to create a state machine. The sprite's current state is stored in `SprAction, X`.
```asm
Sprite_MyNewEnemy_Main:
{
%SpriteJumpTable(State_Idle, State_Attacking, State_Hurt)
State_Idle:
{
%PlayAnimation(0, 1, 15) ; Animate between frames 0 and 1 every 15 game frames
; Check distance to player. If less than 80 pixels, switch to attacking state.
JSL GetDistance8bit_Long : CMP.b #$50 : BCS .player_is_far
%GotoAction(1) ; Switch to State_Attacking
.player_is_far
RTS
}
State_Attacking:
{
%PlayAnimation(2, 3, 8)
%MoveTowardPlayer(12) ; Move toward the player with speed 12
%DoDamageToPlayerSameLayerOnContact()
; Check if the player has hit the sprite
JSL Sprite_CheckDamageFromPlayer : BCC .no_damage
%GotoAction(2) ; Switch to State_Hurt
.no_damage
RTS
}
State_Hurt:
{
; Sprite was hit, flash and get knocked back
JSL Sprite_DamageFlash_Long
JSL Sprite_CheckIfRecoiling
; Return to attacking after recoil
LDA.w SprRecoil, X : BNE .still_recoiling
%GotoAction(1)
.still_recoiling
RTS
}
}
```
### Available Macros
**State Management:**
* `%GotoAction(action)` - Set `SprAction` to switch states
* `%SpriteJumpTable(state1, state2, ...)` - Create state machine jump table
* `%JumpTable(index, state1, state2, ...)` - Jump table with custom index
**Animation:**
* `%PlayAnimation(start, end, speed)` - Animate frames (uses `SprTimerB`)
* `%PlayAnimBackwards(start, end, speed)` - Animate in reverse
* `%StartOnFrame(frame)` - Ensure animation starts at a minimum frame
* `%SetFrame(frame)` - Directly set animation frame
**Movement:**
* `%MoveTowardPlayer(speed)` - Apply speed toward player and move
* `%SetSpriteSpeedX(speed)` - Set horizontal velocity
* `%SetSpriteSpeedY(speed)` - Set vertical velocity
**Timers:**
* `%SetTimerA-F(length)` - Set timer values
**Player Interaction:**
* `%DoDamageToPlayerSameLayerOnContact()` - Damage on contact (same layer only)
* `%PlayerCantPassThrough()` - Prevent Link from passing through sprite
* `%ShowSolicitedMessage(id)` - Show message when player presses A
* `%ShowMessageOnContact(id)` - Show message on contact
* `%ShowUnconditionalMessage(id)` - Show message immediately
**Sprite Properties:**
* `%SetHarmless(value)` - 0=harmful, 1=harmless
* `%SetImpervious(value)` - Toggle invulnerability
* `%SetRoomFlag(value)` - Set room completion flag
**Audio:**
* `%PlaySFX1(id)`, `%PlaySFX2(id)` - Play sound effect
* `%PlayMusic(id)` - Change background music
* `%ErrorBeep()` - Play error sound
**Utility:**
* `%ProbCheck(mask, label)` - Random check, branch if result is non-zero
* `%ProbCheck2(mask, label)` - Random check, branch if result is zero
* `%SetupDistanceFromSprite()` - Setup distance calculation
### Common Functions
**Movement & Physics:**
* `Sprite_Move` / `Sprite_MoveLong` - Apply velocity to position
* `Sprite_MoveHoriz` / `Sprite_MoveVert` - Move in one axis
* `Sprite_BounceFromTileCollision` - Bounce off walls
* `Sprite_CheckTileCollision` - Check for tile collision
* `Sprite_ApplySpeedTowardsPlayer` - Calculate speed toward player
* `Sprite_FloatTowardPlayer` - Float toward player with altitude
* `Sprite_FloatAwayFromPlayer` - Float away from player
* `Sprite_InvertSpeed_X` / `Sprite_InvertSpeed_Y` - Reverse velocity
**Combat:**
* `Sprite_CheckDamageFromPlayer` - Check if player attacked sprite
* `Sprite_CheckDamageToPlayer` - Check if sprite damaged player
* `Sprite_DamageFlash_Long` - Flash sprite when damaged
* `Sprite_CheckIfRecoiling` - Handle knockback after being hit
* `Guard_ParrySwordAttacks` - Parry sword attacks (like Darknut)
**Spawning:**
* `Sprite_SpawnDynamically` - Spawn a new sprite
* `Sprite_SpawnProbeAlways_long` - Spawn probe projectile
* `Sprite_SpawnSparkleGarnish` - Spawn sparkle effect
**Distance & Direction:**
* `GetDistance8bit_Long` - Get 8-bit distance to player
* `Sprite_DirectionToFacePlayer` - Get direction to face player
* `Sprite_IsToRightOfPlayer` - Check if sprite is to right of player
**Randomness:**
* `GetRandomInt` - Get random 8-bit value
### Code Style Guidelines
* **Named Constants:** Always use named constants for magic numbers:
```asm
GoriyaMovementSpeed = 10
LDA.b #GoriyaMovementSpeed : STA.w SprXSpeed, X
```
* **Processor Status Flags:** Explicitly manage 8-bit/16-bit mode with `REP #$20` (16-bit) and `SEP #$20` (8-bit), especially during OAM calculations
* **State Machine Pattern:** Use `SprAction` with `%SpriteJumpTable` for clear state management
* **Timer Usage:** Use dedicated timers for different purposes (e.g., `SprTimerA` for state changes, `SprTimerB` for animation, `SprTimerC` for cooldowns)
## 6. Drawing (`_Draw` routine)
This routine renders your sprite's graphics. The project provides the `%DrawSprite()` macro for standard drawing, which reads from data tables you define.
### Standard Drawing with %DrawSprite()
```asm
Sprite_MyNewEnemy_Draw:
{
JSL Sprite_PrepOamCoord ; Prepare OAM coordinates
JSL Sprite_OAM_AllocateDeferToPlayer ; Allocate OAM slots
%DrawSprite()
; --- OAM Data Tables ---
.start_index ; Starting index in the tables for each animation frame
db $00, $02, $04, $06
.nbr_of_tiles ; Number of tiles to draw for each frame (actual count minus 1)
db 1, 1, 1, 1
.x_offsets ; X-position offset for each tile (16-bit values)
dw -8, 8, -8, 8, -8, 8, -8, 8
.y_offsets ; Y-position offset for each tile (16-bit values)
dw -8, -8, -8, -8, -8, -8, -8, -8
.chr ; The character (tile) number from the graphics sheet
db $C0, $C2, $C4, $C6, $C8, $CA, $CC, $CE
.properties ; OAM properties (palette, priority, flips)
db $3B, $7B, $3B, $7B, $3B, $7B, $3B, $7B
.sizes ; Size of each tile (e.g., $02 for 16x16)
db $02, $02, $02, $02, $02, $02, $02, $02
}
```
### OAM Property Byte Format
The `.properties` byte contains flags for each tile:
* Bits 0-2: Palette (0-7)
* Bit 3: Priority (0=front, 1=behind BG)
* Bit 4: Unused
* Bit 5: Horizontal flip
* Bit 6: Vertical flip
* Bit 7: Unused
Example values:
* `$39` = Palette 1, no flip, front priority
* `$79` = Palette 1, horizontal flip, front priority
* `$B9` = Palette 1, vertical flip, front priority
### Custom Drawing Logic
For complex drawing needs (multi-part sprites, dynamic flipping, etc.), implement custom drawing:
```asm
Sprite_MyNewEnemy_Draw:
{
JSL Sprite_PrepOamCoord
JSL Sprite_OAM_AllocateDeferToPlayer
LDA.w SprGfx, X : CLC : ADC.w SprFrame, X : TAY ; Get animation frame
LDA.w .start_index, Y : STA $06 ; Get starting index
LDA.w SprFlash, X : STA $08 ; Store flash value
LDA.w SprMiscC, X : STA $09 ; Store direction for flipping
PHX
LDX .nbr_of_tiles, Y ; Load number of tiles minus 1
LDY.b #$00 ; OAM buffer index
.nextTile
PHX ; Save tile index
TXA : CLC : ADC $06 : PHA ; Calculate absolute tile index
ASL A : TAX ; Multiply by 2 for 16-bit offsets
REP #$20 ; 16-bit accumulator
LDA $00 : CLC : ADC .x_offsets, X : STA ($90), Y ; Write X position
AND.w #$0100 : STA $0E ; Store X high bit
INY
LDA $02 : CLC : ADC .y_offsets, X : STA ($90), Y ; Write Y position
CLC : ADC #$0010 : CMP.w #$0100 ; Check if on screen
SEP #$20 ; Back to 8-bit
BCC .on_screen_y
LDA.b #$F0 : STA ($90), Y : STA $0E ; Move offscreen
.on_screen_y
PLX ; Restore absolute tile index
INY
LDA .chr, X : STA ($90), Y ; Write character
INY
; Apply horizontal flip based on direction
LDA.b $09 : BEQ .no_flip
LDA.b #$79 : JMP .write_prop
.no_flip
LDA .properties, X
.write_prop
ORA $08 : STA ($90), Y ; Write properties with flash
PHY
TYA : LSR #2 : TAY
LDA .sizes, X : ORA $0F : STA ($92), Y ; Write size
PLY : INY
PLX : DEX : BPL .nextTile
PLX
RTS
; Data tables follow...
}
```
### Important Drawing Notes
* **16-bit Calculations:** Always use `REP #$20` before 16-bit position calculations and `SEP #$20` afterward
* **OAM Allocation:** Different allocation functions for different scenarios:
* `Sprite_OAM_AllocateDeferToPlayer` - Standard allocation
* `OAM_AllocateFromRegionE` - For large sprites (bosses)
* `Sprite_OAM_AllocateDeferToPlayerLong` - Long version
* **Shadow Drawing:** Call `Sprite_DrawShadow` in the `_Long` routine, not in `_Draw`
* **Multi-Layer Drawing:** For objects like minecarts that Link can be "inside", draw in multiple parts from different OAM regions to create depth
* **Conditional Drawing:** Some sprites (like followers or bosses) dispatch to different draw routines based on `SprSubtype` or current state
## 7. Final Integration
The `%Set_Sprite_Properties()` macro you added in Step 2 handles the final integration automatically. It:
1. Writes your sprite properties to the appropriate ROM addresses
2. Sets up pointers in the vanilla sprite jump tables
3. Adds your `_Prep` and `_Long` routines to the new sprite table in `Core/sprite_new_table.asm`
Your sprite is now ready to be placed in the game world using your level editor!
## 8. Testing Your Sprite
1. **Build the ROM:** Run your build script (`build.sh` or `build.bat`)
2. **Place in Editor:** Use your level editor to place the sprite in a room
3. **Test Behavior:** Load the room and verify:
* Sprite spawns correctly
* Animation plays as expected
* Movement works properly
* Collision detection functions
* Damage and health mechanics work
* State transitions occur correctly
## 9. Common Issues and Solutions
### Sprite Doesn't Appear
* Check that the sprite ID is not already in use
* Verify the `incsrc` directive is in the correct bank
* Ensure `%Set_Sprite_Properties` is called after property definitions
* Check that the sprite is being placed in a compatible room type
### Graphics are Corrupted
* Verify 16-bit mode (`REP #$20`) is used for OAM calculations
* Check that `.start_index`, `.nbr_of_tiles`, and data tables are correctly sized
* Ensure `.sizes` table uses correct values ($00=8x8, $02=16x16)
* Verify character numbers (`.chr`) match your graphics sheet
### Sprite Behaves Incorrectly
* Check that timers are being set and checked correctly
* Verify state transitions in the jump table
* Ensure `Sprite_CheckActive` is called before main logic
* Check that collision functions are being called in the right order
### Performance Issues
* Reduce `!NbrTiles` if using too many tiles
* Optimize drawing routine (avoid redundant calculations)
* Use simpler collision detection where possible
* Consider using `!Persist = 00` for non-critical sprites
## 10. Advanced Sprite Design Patterns
## 10. Advanced Sprite Design Patterns
### 10.1. Multi-Part Sprites and Child Sprites
For complex bosses or entities, break them down into a main parent sprite and multiple child sprites. Examples include Kydreeok (body + heads), Darknut (knight + probes), Goriya (enemy + boomerang), and Helmet Chuchu (body + detachable helmet).
**Parent Sprite Responsibilities:**
* Spawns and manages child sprites using `Sprite_SpawnDynamically`
* Stores child sprite IDs in global variables or `SprMisc` slots
* Monitors child sprite states to determine phases or defeat conditions
* Handles overall movement, phase transitions, and global effects
**Child Sprite Responsibilities:**
* Handles independent logic, movement, and attacks
* May be positioned relative to parent sprite
* Uses `SprSubtype` to differentiate between multiple instances
**Example: Kydreeok Boss**
```asm
; In Kydreeok body sprite
SpawnLeftHead:
{
LDA #$CF ; Kydreeok Head sprite ID
JSL Sprite_SpawnDynamically : BMI .return
TYA : STA.w Offspring1_Id ; Store child ID globally
LDA.b #$00 : STA.w SprSubtype, Y ; Subtype 0 = left head
; Position relative to parent
REP #$20
LDA.w SprCachedX : SEC : SBC.w #$0010
SEP #$20
STA.w SprX, Y : XBA : STA.w SprXH, Y
; ... more initialization
.return
RTS
}
; Check if all heads are defeated
Sprite_Kydreeok_CheckIfDead:
{
LDA.w Offspring1_Id : TAY
LDA.w SprState, Y : BNE .not_dead ; Check if left head alive
LDA.w Offspring2_Id : TAY
LDA.w SprState, Y : BNE .not_dead ; Check if right head alive
; All heads defeated - trigger death sequence
.not_dead
RTS
}
```
**Shared Sprite IDs for Variations:**
A single sprite ID can represent different enemy types using `SprSubtype`:
* Keese sprite ID shared by: Regular Keese, Fire Keese, Ice Keese, Vampire Bat
* Mermaid sprite ID ($F0) shared by: Mermaid, Maple, Librarian (all using different subtypes)
* This efficiently reuses sprite slots and base logic
### 10.2. Quest Integration and Dynamic Progression
Boss fights and NPC interactions can be deeply integrated with quest progression using SRAM flags, dynamic health management, and multi-phase battles.
**Phase Transitions:**
Trigger new phases based on health thresholds, timers, or child sprite states:
```asm
; Check health threshold for phase change
LDA.w SprHealth, X : CMP.b #$10 : BCS .phase_one
LDA.w SprMiscD, X : CMP.b #$02 : BEQ .already_phase_two
LDA.b #$02 : STA.w SprMiscD, X ; Switch to phase 2
JSR LoadPhase2Graphics
JSR SpawnPhase2Adds
.already_phase_two
.phase_one
```
**Health Management:**
* **Direct Health:** Use `SprHealth` for straightforward health tracking
* **Indirect Health:** Base defeat on child sprite states (e.g., Kydreeok defeated when all heads are killed)
* **Phase-Based Health:** Refill health between phases for extended boss fights
* **Dynamic Scaling:** Adjust health based on Link's sword level or progression
**Quest Integration Examples:**
* **Wolfos:** After being subdued, plays Song of Healing animation and grants Wolf Mask
* **Bug Net Kid:** Dialogue changes based on whether Link has the Bug Net
* **Maple:** Spawns items and interacts with Link differently based on quest flags
* **Mask Salesman:** Complex shop system with inventory checks and rupee deduction
* **Zora Princess:** Quest rewards and dialogue conditional on SRAM flags
**SRAM Flag Usage:**
```asm
; Check if quest item has been obtained
LDA.l $7EF3XX : CMP.b #$XX : BNE .not_obtained
; Quest item obtained - change behavior
%ShowUnconditionalMessage(MessageID)
JMP .quest_complete
.not_obtained
```
### 10.4. Code Reusability and Best Practices
**Shared Logic Functions:**
Create reusable functions for common behaviors across multiple sprites:
```asm
; Shared by Goriya and Darknut
Goriya_HandleTileCollision:
{
JSL Sprite_CheckTileCollision
LDA.w SprCollision, X : BEQ .no_collision
JSL GetRandomInt : AND.b #$03 : STA.w SprAction, X
STA.w SprMiscE, X
%SetTimerC(60)
.no_collision
RTS
}
```
**Named Constants:**
Always use named constants instead of magic numbers:
```asm
; Good
GoriyaMovementSpeed = 10
MinecartSpeed = 20
DoubleSpeed = 30
LDA.b #GoriyaMovementSpeed : STA.w SprXSpeed, X
; Bad
LDA.b #10 : STA.w SprXSpeed, X ; What does 10 mean?
```
**Processor Status Management:**
Explicitly manage 8-bit/16-bit modes:
```asm
REP #$20 ; 16-bit accumulator
LDA $00 : CLC : ADC .x_offsets, X : STA ($90), Y
SEP #$20 ; Back to 8-bit
```
**State Machine Pattern:**
Use `SprAction` with jump tables for clear state management:
```asm
Sprite_Enemy_Main:
{
%SpriteJumpTable(State_Idle, State_Chase, State_Attack, State_Retreat)
State_Idle: { /* ... */ RTS }
State_Chase: { /* ... */ RTS }
State_Attack: { /* ... */ RTS }
State_Retreat: { /* ... */ RTS }
}
```
**Timer Management:**
Use different timers for different purposes:
* `SprTimerA` - State transitions, cooldowns
* `SprTimerB` - Animation (automatically used by `%PlayAnimation`)
* `SprTimerC` - Movement changes, direction changes
* `SprTimerD` - Attack cooldowns
* `SprTimerE` - Special effects
* `SprTimerF` - Gravity/altitude (decrements by 2)
### 10.5. Centralized Handlers and Multi-Purpose Sprites
Many sprite files serve as central handlers for multiple distinct entities, using conditional logic to dispatch behaviors.
**Examples:**
* **Followers** (`followers.asm`) - Zora Baby, Old Man, Kiki
* **Mermaid** (`mermaid.asm`) - Mermaid (subtype 0), Maple (subtype 1), Librarian (subtype 2)
* **Zora** (`zora.asm`) - Various Zora NPCs with different roles
* **Collectible** (`collectible.asm`) - Different collectible items
* **Deku Leaf** (`deku_leaf.asm`) - Deku Leaf and Beach Whirlpool
**Implementation Pattern:**
```asm
Sprite_MultiPurpose_Long:
{
PHB : PHK : PLB
; Dispatch based on subtype
LDA.w SprSubtype, X
JSL JumpTableLocal
dw Type0_Routine
dw Type1_Routine
dw Type2_Routine
Type0_Routine:
JSR Type0_Draw
JSR Type0_Main
PLB : RTL
Type1_Routine:
JSR Type1_Draw
JSR Type1_Main
PLB : RTL
}
```
### 10.6. Overriding Vanilla Sprites
To replace vanilla sprite behavior while keeping the original sprite ID:
```asm
; In a patch file or at the start of your sprite file
pushpc
org $069283+($XX*2) ; Replace vanilla main pointer
dw NewCustomBehavior_Main
org $06865B+($XX*2) ; Replace vanilla prep pointer
dw NewCustomBehavior_Prep
pullpc
NewCustomBehavior_Main:
{
; Check if custom behavior should activate
LDA.l $7EF3XX : CMP.b #$YY : BNE .use_vanilla
JSL CustomImplementation_Long
RTS
.use_vanilla
JML $OriginalVanillaAddress
}
```
### 10.7. Interactive Objects and Environmental Triggers
**Player-Manipulated Objects:**
Objects like Ice Block and Minecart require precise collision and alignment:
```asm
; Round position to 8-pixel grid for proper alignment
RoundCoords:
{
LDA.b $00 : CLC : ADC.b #$04 : AND.b #$F8 : STA.b $00 : STA.w SprY, X
LDA.b $02 : CLC : ADC.b #$04 : AND.b #$F8 : STA.b $02 : STA.w SprX, X
JSR UpdateCachedCoords
RTS
}
```
**Environmental Triggers:**
Switch objects respond to player actions and modify game state:
```asm
; Mine switch changes track configuration
Sprite_Mineswitch_OnActivate:
{
LDA.w SprMiscA, X : BEQ .currently_off
; Switch is on, turn it off
STZ.w SprMiscA, X
JSR UpdateTrackTiles_Off
JMP .done
.currently_off
; Switch is off, turn it on
LDA.b #$01 : STA.w SprMiscA, X
JSR UpdateTrackTiles_On
.done
%PlaySFX2($14) ; Switch sound
RTS
}
```
### 10.8. Shop and Item Management
**Transaction System:**
```asm
Shopkeeper_SellItem:
{
; Check if player has enough rupees
REP #$20
LDA.l $7EF360 : CMP.w #ItemCost : BCC .not_enough
; Deduct rupees
SEC : SBC.w #ItemCost : STA.l $7EF360
SEP #$20
; Grant item
LDA.b #ItemID : STA.l $7EF3XX
%ShowUnconditionalMessage(ThankYouMessage)
RTS
.not_enough
SEP #$20
%ErrorBeep()
%ShowUnconditionalMessage(NotEnoughRupeesMessage)
RTS
}
```
**Item Granting with Quest Tracking:**
```asm
NPC_GrantQuestItem:
{
; Check if already received
LDA.l $7EF3XX : BNE .already_obtained
; Grant item
LDA.b #$01 : STA.l $7EF3XX
LDA.b #ItemID
JSL Link_ReceiveItem
%ShowUnconditionalMessage(ItemReceivedMessage)
RTS
.already_obtained
%ShowUnconditionalMessage(AlreadyHaveMessage)
RTS
}
```
### 10.9. Player State Manipulation
For cinematic sequences and special interactions:
```asm
Cutscene_LinkSleep:
{
; Prevent player input
%PreventPlayerMovement()
; Set Link's animation
LDA.b #$XX : STA.w LinkAction
; Play sleep animation
LDA.b #$XX : STA.w LinkGraphics
; Wait for timer
LDA.w SprTimerA, X : BNE .still_waiting
%AllowPlayerMovement()
%GotoAction(NextState)
.still_waiting
RTS
}
```
### 10.10. Error Handling and Player Feedback
**Robust Error Prevention:**
```asm
; Portal sprite checks for valid placement
Sprite_Portal_CheckValidTile:
{
LDA.w CurrentTileType : CMP.b #ValidTileMin : BCC .invalid
CMP.b #ValidTileMax : BCS .invalid
CMP.b #$XX : BEQ .invalid ; Check specific invalid tiles
; Valid placement
SEC
RTS
.invalid
%ErrorBeep()
STZ.w SprState, X ; Despawn sprite
CLC
RTS
}
```
**Clear Player Feedback:**
```asm
; Provide audio/visual feedback
%ErrorBeep() ; Sound for errors
%PlaySFX1($14) ; Sound for success
JSL Sprite_ShowMessageUnconditional ; Text feedback
```
## 11. Additional Resources
**Core Files:**
* `Core/sprite_macros.asm` - All available macros and their implementations
* `Core/sprite_functions.asm` - Reusable sprite functions
* `Core/sprite_new_table.asm` - Sprite table initialization
* `Core/symbols.asm` - RAM address definitions
* `Core/structs.asm` - Sprite structure definitions
**Documentation:**
* `Docs/Sprites/` - Detailed documentation for existing sprites
* `Docs/Sprites/Overlords.md` - Overlord system documentation
* `Sprites/all_sprites.asm` - See how sprites are organized and included
**Example Sprites:**
* **Simple Enemy:** `Sprites/Enemies/sea_urchin.asm` - Basic enemy with minimal logic
* **Advanced Enemy:** `Sprites/Enemies/booki.asm` - Dynamic AI with state management
* **Boss:** `Sprites/Bosses/kydreeok.asm` - Multi-part boss with child sprites
* **Interactive Object:** `Sprites/Objects/minecart.asm` - Complex player interaction
* **NPC:** `Sprites/NPCs/mask_salesman.asm` - Shop system and dialogue