31 KiB
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
-
Create the Sprite File: Create a new
.asmfile for your sprite in the appropriate subdirectory ofSprites/:Sprites/Enemies/- For enemy spritesSprites/Bosses/- For boss spritesSprites/NPCs/- For non-playable character spritesSprites/Objects/- For interactive objects and items
-
Include the File: Open
Sprites/all_sprites.asmand add anincsrcdirective to include your new file. The file must be placed in the correct bank section:- Bank 30 (
$308000) - First bank, includessprite_new_table.asmand some core sprites - Bank 31 (
$318000) - Second bank, includessprite_functions.asmand more sprites - Bank 32 (
$328000) - Third bank for additional sprites - Bank 2C (Dungeon bank) - For sprites that are part of dungeon-specific content
Example:
; 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) - Bank 30 (
-
Assign a Sprite ID: Choose an unused sprite ID for your sprite. You can either:
- Use a completely new ID (e.g.,
$A0through$FFrange) - Override a vanilla sprite ID (for replacing existing sprites)
- Share an ID with another sprite and use
SprSubtypeto differentiate behaviors
- Use a completely new ID (e.g.,
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.
; =========================================================
; 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 PointerNewSprRoutinesLong+(!SPRID*3)- New Long Sprite PointerNewSprPrepRoutinesLong+(!SPRID*3)- New Long Sprite Prep Pointer
Design Considerations
- Multi-purpose Sprite IDs: A single
!SPRIDcan be used for multiple distinct behaviors (e.g., Keese, Fire Keese, Ice Keese, Vampire Bat all share sprite IDs) through the use ofSprSubtype. This is a powerful technique for reusing sprite slots and creating variations of enemies. - Damage Handling for Bosses: For boss sprites,
!Damage = 00is 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
_Preproutine 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 = 00for 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
!SPRIDby usingSprSubtypefor differentiation (e.g.,Sprite_Mermaid = $F0is 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.
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 : PLBat the start andPLBbeforeRTLto 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.
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 positionSprYSpeed, SprXSpeed($0D40, $0D50) - Movement velocitiesSprYRound, SprXRound($0D60, $0D70) - Sub-pixel precisionSprCachedX, SprCachedY($0FD8, $0FDA) - Cached coordinates
Animation & Graphics:
SprAction($0D80) - Current state in state machineSprFrame($0D90) - Current animation frame indexSprGfx($0DC0) - Graphics offset for drawingSprFlash($0B89) - Flash color for damage indication
Timers:
SprTimerA-F($0DF0, $0E00, $0E10, $0EE0, $0F10, $0F80) - Six general-purpose timers- Note:
SprTimerFdecreases by 2 each frame (used for gravity)
Miscellaneous Data:
SprMiscA-G($0DA0, $0DB0, $0DE0, $0E90, $0EB0, $0EC0, $0ED0) - Seven general-purpose variablesSprCustom($1CC0) - Additional custom data storage
State & Properties:
SprState($0DD0) - Sprite state (0x00=dead, 0x08=spawning, 0x09=active, etc.)SprType($0E20) - Sprite IDSprSubtype($0E30) - Sprite subtype for variationsSprHealth($0E50) - Current healthSprNbrOAM($0E40) - Number of OAM slots + flagsSprFloor($0F20) - Layer (0=top, 1=bottom)SprHeight($0F80) - Z-position for altitude/jumping
Common Initialization Patterns
; 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.
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)- SetSprActionto 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 (usesSprTimerB)%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 positionSprite_MoveHoriz/Sprite_MoveVert- Move in one axisSprite_BounceFromTileCollision- Bounce off wallsSprite_CheckTileCollision- Check for tile collisionSprite_ApplySpeedTowardsPlayer- Calculate speed toward playerSprite_FloatTowardPlayer- Float toward player with altitudeSprite_FloatAwayFromPlayer- Float away from playerSprite_InvertSpeed_X/Sprite_InvertSpeed_Y- Reverse velocity
Combat:
Sprite_CheckDamageFromPlayer- Check if player attacked spriteSprite_CheckDamageToPlayer- Check if sprite damaged playerSprite_DamageFlash_Long- Flash sprite when damagedSprite_CheckIfRecoiling- Handle knockback after being hitGuard_ParrySwordAttacks- Parry sword attacks (like Darknut)
Spawning:
Sprite_SpawnDynamically- Spawn a new spriteSprite_SpawnProbeAlways_long- Spawn probe projectileSprite_SpawnSparkleGarnish- Spawn sparkle effect
Distance & Direction:
GetDistance8bit_Long- Get 8-bit distance to playerSprite_DirectionToFacePlayer- Get direction to face playerSprite_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:
GoriyaMovementSpeed = 10 LDA.b #GoriyaMovementSpeed : STA.w SprXSpeed, X - Processor Status Flags: Explicitly manage 8-bit/16-bit mode with
REP #$20(16-bit) andSEP #$20(8-bit), especially during OAM calculations - State Machine Pattern: Use
SprActionwith%SpriteJumpTablefor clear state management - Timer Usage: Use dedicated timers for different purposes (e.g.,
SprTimerAfor state changes,SprTimerBfor animation,SprTimerCfor 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()
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:
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 #$20before 16-bit position calculations andSEP #$20afterward - OAM Allocation: Different allocation functions for different scenarios:
Sprite_OAM_AllocateDeferToPlayer- Standard allocationOAM_AllocateFromRegionE- For large sprites (bosses)Sprite_OAM_AllocateDeferToPlayerLong- Long version
- Shadow Drawing: Call
Sprite_DrawShadowin the_Longroutine, 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
SprSubtypeor current state
7. Final Integration
The %Set_Sprite_Properties() macro you added in Step 2 handles the final integration automatically. It:
- Writes your sprite properties to the appropriate ROM addresses
- Sets up pointers in the vanilla sprite jump tables
- Adds your
_Prepand_Longroutines to the new sprite table inCore/sprite_new_table.asm
Your sprite is now ready to be placed in the game world using your level editor!
8. Testing Your Sprite
- Build the ROM: Run your build script (
build.shorbuild.bat) - Place in Editor: Use your level editor to place the sprite in a room
- 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
incsrcdirective is in the correct bank - Ensure
%Set_Sprite_Propertiesis 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
.sizestable 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_CheckActiveis called before main logic - Check that collision functions are being called in the right order
Performance Issues
- Reduce
!NbrTilesif using too many tiles - Optimize drawing routine (avoid redundant calculations)
- Use simpler collision detection where possible
- Consider using
!Persist = 00for 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
SprMiscslots - 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
SprSubtypeto differentiate between multiple instances
Example: Kydreeok Boss
; 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:
; 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
SprHealthfor 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:
; 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:
; 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:
; 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:
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:
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, cooldownsSprTimerB- Animation (automatically used by%PlayAnimation)SprTimerC- Movement changes, direction changesSprTimerD- Attack cooldownsSprTimerE- Special effectsSprTimerF- 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:
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:
; 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:
; 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:
; 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:
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:
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:
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:
; 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:
; 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 implementationsCore/sprite_functions.asm- Reusable sprite functionsCore/sprite_new_table.asm- Sprite table initializationCore/symbols.asm- RAM address definitionsCore/structs.asm- Sprite structure definitions
Documentation:
Docs/Sprites/- Detailed documentation for existing spritesDocs/Sprites/Overlords.md- Overlord system documentationSprites/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