Update SpriteCreationGuide
This commit is contained in:
298
Docs/GEMINI.md
298
Docs/GEMINI.md
@@ -32,6 +32,35 @@ The 65816 is an 8/16-bit microprocessor used in the Super Nintendo Entertainment
|
||||
- **`REP #$20` (or `REP #$30`):** Resets the M flag (and X flag if $30 is used) to 0, switching A (and X/Y) to 16-bit mode.
|
||||
- **Importance:** Mismatched M/X flags between calling and called routines are a common cause of crashes (BRKs) or unexpected behavior. Always ensure the P register is in the expected state for a given routine, or explicitly set it.
|
||||
|
||||
**Practical Example:**
|
||||
```asm
|
||||
; INCORRECT: Size mismatch causes corruption
|
||||
REP #$20 ; A = 16-bit
|
||||
LDA.w #$1234 ; A = $1234
|
||||
|
||||
SEP #$20 ; A = 8-bit now
|
||||
LDA.w #$1234 ; ERROR: Assembler generates LDA #$34, $12 becomes opcode!
|
||||
|
||||
; CORRECT: Match processor state to operation
|
||||
REP #$20 ; A = 16-bit
|
||||
LDA.w #$1234 ; A = $1234
|
||||
|
||||
SEP #$20 ; A = 8-bit
|
||||
LDA.b #$12 ; Load only 8-bit value
|
||||
```
|
||||
|
||||
**Best Practice:**
|
||||
```asm
|
||||
MyFunction:
|
||||
PHP ; Save caller's processor state
|
||||
SEP #$30 ; Set to known 8-bit state
|
||||
; ... your code here ...
|
||||
PLP ; Restore caller's processor state
|
||||
RTL
|
||||
```
|
||||
|
||||
See `Docs/General/Troubleshooting.md` Section 3 for comprehensive processor state troubleshooting.
|
||||
|
||||
### 1.4. Memory Mapping
|
||||
|
||||
- The SNES has a 24-bit address space, allowing access to up to 16MB of ROM/RAM.
|
||||
@@ -84,7 +113,9 @@ The 65816 is an 8/16-bit microprocessor used in the Super Nintendo Entertainment
|
||||
|
||||
### 3.2. Patch Management
|
||||
|
||||
- **Revised Guideline:** Only small, simple patches that modify a few bytes of vanilla code should be considered for centralization in `Core/patches.asm`. Any patch that defines new functions or data in freespace should remain in its original file to preserve context and memory layout.
|
||||
- **Guideline:** Hooks and patches should be placed logically near the code they relate to, not centralized in `Core/patches.asm`. This improves code organization, maintainability, and context preservation.
|
||||
- **Rationale:** When a hook modifies vanilla behavior to add custom functionality, placing the hook in the same file as the custom implementation keeps related code together. This makes it easier to understand the complete feature, debug issues, and maintain the codebase.
|
||||
- **Exception:** Only truly generic, cross-cutting patches that don't belong to any specific feature should be considered for `Core/patches.asm`.
|
||||
|
||||
### 3.3. Debugging
|
||||
|
||||
@@ -103,6 +134,152 @@ The 65816 is an 8/16-bit microprocessor used in the Super Nintendo Entertainment
|
||||
- **Interaction:** To call a function that is inside the `Oracle` namespace from a file that is outside of it (like `ZSCustomOverworld.asm`), you must prefix the function's label with `Oracle_`. For example, to call the `CheckIfNight16Bit` function (defined inside the namespace), you must use `JSL Oracle_CheckIfNight16Bit`.
|
||||
- **Rationale:** The build process correctly resolves these `Oracle_` prefixed labels to their namespaced counterparts (e.g., `Oracle.CheckIfNight16Bit`). Do not add the `Oracle_` prefix to the original function definition; it is only used by the calling code outside the namespace.
|
||||
|
||||
**Practical Example - Oracle to ZScream:**
|
||||
```asm
|
||||
// In ZScream file (no namespace):
|
||||
LoadOverworldSprites_Interupt:
|
||||
{
|
||||
; ZScream code here
|
||||
RTL
|
||||
}
|
||||
|
||||
// Export to Oracle namespace:
|
||||
namespace Oracle
|
||||
{
|
||||
Oracle_LoadOverworldSprites_Interupt = LoadOverworldSprites_Interupt
|
||||
}
|
||||
|
||||
// Now Oracle code can call it:
|
||||
namespace Oracle
|
||||
{
|
||||
MyFunction:
|
||||
JSL Oracle_LoadOverworldSprites_Interupt ; Use prefix!
|
||||
RTL
|
||||
}
|
||||
```
|
||||
|
||||
**Practical Example - ZScream to Oracle (Bridge Pattern):**
|
||||
```asm
|
||||
// Oracle implementation:
|
||||
namespace Oracle
|
||||
{
|
||||
CheckIfNight:
|
||||
LDA.l $7EE000 ; Check time system
|
||||
; ... logic ...
|
||||
RTL
|
||||
}
|
||||
|
||||
// Bridge function (no namespace):
|
||||
ZSO_CheckIfNight:
|
||||
{
|
||||
JSL Oracle_CheckIfNight ; Can call INTO Oracle
|
||||
RTL
|
||||
}
|
||||
|
||||
// Export bridge:
|
||||
namespace Oracle
|
||||
{
|
||||
Oracle_ZSO_CheckIfNight = ZSO_CheckIfNight
|
||||
}
|
||||
|
||||
// ZScream hook can use it:
|
||||
org $09C4C7
|
||||
LoadOverworldSprites_Hook:
|
||||
JSL Oracle_ZSO_CheckIfNight ; Bridge function
|
||||
```
|
||||
|
||||
For comprehensive namespace troubleshooting and advanced patterns, see:
|
||||
- `Docs/World/Overworld/ZSCustomOverworldAdvanced.md` Section 5 (Cross-Namespace Integration)
|
||||
- `Docs/General/Troubleshooting.md` Section 5 (Cross-Namespace Calling)
|
||||
- `Docs/General/DevelopmentGuidelines.md` Section 2.4 (Namespace Architecture)
|
||||
|
||||
### 3.6. Safe Hooking and Code Injection
|
||||
|
||||
When modifying vanilla game logic, it is critical to never edit the disassembly files in `ALTTP/` or `usdasm/` directly. Instead, use the following safe hooking method to inject custom code.
|
||||
|
||||
- **1. Identify a Target and Free Space:**
|
||||
- Locate the exact address in the vanilla code you want to modify (the "hook point").
|
||||
- Identify a free bank or region in the ROM to place your new, expanded code.
|
||||
|
||||
- **2. Choose the Appropriate File for Your Hook:**
|
||||
- **Feature-Specific Hooks:** Place hooks in the same file as the custom implementation they enable. For example, if you're adding a new item feature in `Items/magic_ring.asm`, place the vanilla hook in that same file.
|
||||
- **Module-Specific Hooks:** For hooks that modify core game systems (sprite engine, player engine, etc.), place them in the relevant module file within the `Core/` directory.
|
||||
- **Generic Patches:** Only place truly generic, cross-cutting modifications in `Core/patches.asm` (e.g., fixes to vanilla bugs, performance optimizations).
|
||||
- **Rationale:** Co-locating hooks with their implementations improves code organization, makes features self-contained, and provides better context for future maintenance.
|
||||
|
||||
- **3. Write the Hook:**
|
||||
- Use `pushpc` and `pullpc` to isolate your patch.
|
||||
- Use `org` to navigate to the target address in the vanilla code.
|
||||
- At the target address, overwrite the original instruction(s) with a `JSL` (or `JMP`) to your new custom routine in free space.
|
||||
- **Crucially, ensure your `JSL`/`JMP` instruction and any necessary `NOP`s perfectly replace the original instruction(s) byte-for-byte.** A `JSL` is 4 bytes. If you overwrite an instruction that is only 2 bytes, you must add `NOP` instructions to fill the remaining 2 bytes to avoid corrupting the subsequent instruction.
|
||||
|
||||
- **4. Implement the Custom Routine:**
|
||||
- In a `freedata` block (or using `org` with a free space address), write your new routine in the same file as the hook.
|
||||
- **Preserve Overwritten Code:** The first thing your new routine must do is execute the exact vanilla instruction(s) that you overwrote with your `JSL`. This is essential to maintain the original game's behavior.
|
||||
- After preserving the original logic, add your new custom code.
|
||||
- End your routine with an `RTL` (to return from a `JSL`) or `RTS` (to return from a `JSR`).
|
||||
|
||||
- **Example (Feature-Specific Hook):**
|
||||
```asm
|
||||
; In Items/magic_ring.asm
|
||||
|
||||
; 1. Place the new, expanded logic in a free bank.
|
||||
org $348000
|
||||
MagicRing_CustomEffect:
|
||||
{
|
||||
; 2. First, execute the original instruction(s) that were overwritten.
|
||||
LDA.b #$01 ; Example: Original instruction was LDA #$01 (2 bytes)
|
||||
STA.w $0DD0,X ; Example: Original instruction was STA $0DD0,X (3 bytes)
|
||||
|
||||
; 3. Now, add your new custom logic for the magic ring.
|
||||
LDA.l MagicRing ; Check if player has magic ring
|
||||
BEQ .no_ring
|
||||
LDA.b #$FF ; Apply ring's special effect
|
||||
STA.w $1234,X
|
||||
.no_ring
|
||||
|
||||
; 4. Return to the vanilla code.
|
||||
RTL
|
||||
}
|
||||
|
||||
; 5. Hook placement: In the same file, near the feature implementation
|
||||
pushpc
|
||||
org $05C227 ; Target address in vanilla sprite damage routine
|
||||
JSL MagicRing_CustomEffect ; JSL is 4 bytes.
|
||||
NOP ; Fill the 5th byte since we overwrote two instructions (2+3=5 bytes)
|
||||
pullpc
|
||||
```
|
||||
|
||||
- **Example (Core System Hook):**
|
||||
```asm
|
||||
; In Core/sprite_engine_hooks.asm (or similar)
|
||||
|
||||
org $348100
|
||||
CustomSprite_DeathHandler:
|
||||
{
|
||||
; Preserve original death logic
|
||||
LDA.w SprHealth, X
|
||||
BNE .not_dead
|
||||
; Original vanilla death code here
|
||||
JSL Sprite_SpawnDeathAnimation
|
||||
.not_dead
|
||||
|
||||
; Add custom death effects for Oracle sprites
|
||||
LDA.w SprType, X
|
||||
CMP.b #CustomSpriteID : BNE .skip_custom
|
||||
JSR CustomSprite_SpecialDeath
|
||||
.skip_custom
|
||||
|
||||
RTL
|
||||
}
|
||||
|
||||
; Hook in same file
|
||||
pushpc
|
||||
org $068450
|
||||
JSL CustomSprite_DeathHandler
|
||||
pullpc
|
||||
```
|
||||
|
||||
## 4. Build Process and ROM Management
|
||||
|
||||
- **Clean ROM**: The clean, unmodified "The Legend of Zelda: A Link to the Past" ROM should be placed at `Roms/oos169.sfc`. This path is included in `.gitignore`, so the ROM file will not be committed to the repository.
|
||||
@@ -114,35 +291,111 @@ The 65816 is an 8/16-bit microprocessor used in the Super Nintendo Entertainment
|
||||
|
||||
When encountering unexpected crashes (often indicated by a `BRK` instruction in emulators), especially after modifying code, consider the following:
|
||||
|
||||
- **Processor Status Register (P) Mismatch:** This is a very common cause. If a routine expects 8-bit accumulator/index registers (M=1, X=1) but is called when they are 16-bit (M=0, X=0), or vice-versa, memory accesses and arithmetic operations will be incorrect, leading to crashes. Always verify the M and X flags before and after calling/returning from routines, especially those in different banks or that you've modified.
|
||||
- **Check `PHD`/`PLD`, `PHB`/`PLB`, `PHK`/`PLK`:** These instructions save/restore the Direct Page, Data Bank, and Program Bank registers, respectively. Ensure they are used correctly when switching banks or contexts.
|
||||
- **Check `PHA`/`PLA`, `PHX`/`PLX`, `PHY`/`PLY`:** These save/restore the accumulator and index registers. Ensure they are balanced.
|
||||
- **Check `PHP`/`PLP`:** These save/restore the entire Processor Status Register. Use them when a routine needs a specific P state and you want to restore the caller's state afterwards.
|
||||
**For comprehensive debugging guidance with step-by-step procedures, see `Docs/General/Troubleshooting.md`.**
|
||||
|
||||
### 5.1. Most Common Causes
|
||||
|
||||
- **Processor Status Register (P) Mismatch:** This is a very common cause. If a routine expects 8-bit accumulator/index registers (M=1, X=1) but is called when they are 16-bit (M=0, X=0), or vice-versa, memory accesses and arithmetic operations will be incorrect, leading to crashes.
|
||||
|
||||
**Example:**
|
||||
```asm
|
||||
; BAD: Size mismatch
|
||||
REP #$20 ; A = 16-bit
|
||||
JSL Function ; Function expects 8-bit!
|
||||
|
||||
; Inside Function:
|
||||
SEP #$20 ; Sets 8-bit mode
|
||||
LDA.b #$FF
|
||||
STA.w $1234 ; Only stores $FF, not $00FF!
|
||||
RTL ; ← Doesn't restore caller's 16-bit mode
|
||||
|
||||
; GOOD: Preserve state
|
||||
Function:
|
||||
PHP ; Save caller's state
|
||||
SEP #$20 ; Set to 8-bit
|
||||
LDA.b #$FF
|
||||
STA.w $1234
|
||||
PLP ; Restore caller's state
|
||||
RTL
|
||||
```
|
||||
|
||||
- **Stack Corruption:** JSL/JSR push the return address onto the stack. If a called routine pushes too much data onto the stack without popping it, or if the stack pointer (`S`) is corrupted, the return address can be overwritten, leading to a crash when `RTL`/`RTS` is executed.
|
||||
- **`JSR`/`RTS` vs `JSL`/`RTL` Mismatch:** This is a critical and common error.
|
||||
- `JSR` (Jump to Subroutine) pushes a 2-byte return address. It **must** be paired with `RTS` (Return from Subroutine), which pulls 2 bytes.
|
||||
- `JSL` (Jump to Subroutine Long) pushes a 3-byte return address (including the bank). It **must** be paired with `RTL` (Return from Subroutine Long), which pulls 3 bytes.
|
||||
- Using `RTL` with `JSR` (or `RTS` with `JSL`) will corrupt the stack and almost certainly lead to a crash. Always verify that your subroutine calls and returns are correctly paired.
|
||||
- **Balance Pushes and Pops:** Every `PHA`, `PHX`, `PHY`, `PHP` should ideally have a corresponding `PLA`, `PLX`, `PLY`, `PLP` within the same routine.
|
||||
- **Bank Switching with Stack:** Be extremely careful when performing bank switches (`PHB`/`PLB`, `PHK`/`PLK`) around stack operations, as the stack is in WRAM (bank $7E/$7F).
|
||||
|
||||
- **Incorrect Bank Setup:** When calling a routine in a different bank using `JSL`, ensure the Program Bank (PB) and Data Bank (DB) registers are correctly set for the target routine and restored for the calling routine.
|
||||
**Example:**
|
||||
```asm
|
||||
; BAD: Mismatched call/return
|
||||
MainFunction:
|
||||
JSL SubFunction ; Pushes 3 bytes ($02 $C4 $09)
|
||||
|
||||
SubFunction:
|
||||
; ... code ...
|
||||
RTS ; ← ERROR: Only pops 2 bytes! Stack corrupted!
|
||||
|
||||
- **Memory Overwrites:** A bug in one part of the code might be writing to an unexpected memory location, corrupting data or code that is used later.
|
||||
- **Use an Emulator Debugger:** Step through the code instruction by instruction, paying close attention to register values and memory contents. Set breakpoints at the point of the crash and work backward.
|
||||
- **Memory Watchpoints:** Some emulators allow setting watchpoints that trigger when a specific memory address is read or written. This can help pinpoint where corruption occurs.
|
||||
; GOOD: Matched call/return
|
||||
MainFunction:
|
||||
JSL SubFunction ; Pushes 3 bytes
|
||||
|
||||
SubFunction:
|
||||
; ... code ...
|
||||
RTL ; ← Correct: Pops 3 bytes
|
||||
```
|
||||
|
||||
- **Off-by-One Errors/Table Bounds:** Accessing data outside the bounds of an array or table can lead to reading garbage data or overwriting other parts of memory.
|
||||
### 5.2. Debugging Tools
|
||||
|
||||
- **Unintended Side Effects:** A routine might modify a register or memory location that a calling routine expects to remain unchanged. Always document what registers a routine clobbers.
|
||||
- **Mesen-S (Recommended):** The most powerful SNES debugger:
|
||||
- Set breakpoints with conditions: `A == #$42`
|
||||
- Memory watchpoints: `[W]$7E0730` (break on write)
|
||||
- Stack viewer to trace call history
|
||||
- Event viewer for NMI/IRQ timing
|
||||
- Break on BRK automatically
|
||||
|
||||
- **Debugging Strategy:**
|
||||
1. **Isolate the Problem:** Try to narrow down the exact code change that causes the crash. Revert changes one by one if necessary.
|
||||
2. **Use `print_debug`:** Strategically place `%print_debug()` macros to output register values or memory contents at critical points in the code. This can help track the flow and identify unexpected values.
|
||||
3. **Emulator Debugger:** Learn to use your emulator's debugger effectively. Step-by-step execution, register viewing, and memory inspection are invaluable tools.
|
||||
4. **Check `usdasm`:** Always cross-reference with the `usdasm` disassembly to understand the original vanilla code and how your hooks are interacting with it.
|
||||
5. **Efficiently Search Large Files:** When analyzing large assembly files, especially those with large data blocks like `Overworld/ZSCustomOverworld.asm`, prefer using `search_file_content` or `grep` to find specific labels or code sections instead of reading the entire file in chunks. This avoids confusion and is more efficient.
|
||||
**Quick Mesen-S Workflow:**
|
||||
1. Enable "Break on BRK" in Debugger settings
|
||||
2. When crash occurs, check Stack viewer
|
||||
3. Read return addresses to trace call history
|
||||
4. Set breakpoint before suspected crash location
|
||||
5. Step through code examining registers
|
||||
|
||||
- **Breadcrumb Tracking:**
|
||||
```asm
|
||||
; Add markers to narrow down crash location
|
||||
LDA.b #$01 : STA.l $7F5000 ; Breadcrumb 1
|
||||
JSL SuspiciousFunction
|
||||
LDA.b #$02 : STA.l $7F5000 ; Breadcrumb 2
|
||||
JSL AnotherFunction
|
||||
LDA.b #$03 : STA.l $7F5000 ; Breadcrumb 3
|
||||
|
||||
; After crash, check $7F5000 in memory viewer
|
||||
; If value is $02, crash occurred in AnotherFunction
|
||||
```
|
||||
|
||||
### 5.3. Common Error Patterns
|
||||
|
||||
**Pattern 1: Jumping to $000000 (BRK)**
|
||||
- Cause: Corrupted jump address or return address
|
||||
- Debug: Check stack contents, verify JSL/JSR is called before RTL/RTS
|
||||
|
||||
**Pattern 2: Infinite Loop / Freeze**
|
||||
- Cause: Forgot to increment module/submodule, infinite recursion
|
||||
- Debug: Check that `$10` (module) or `$11` (submodule) is incremented
|
||||
|
||||
**Pattern 3: Wrong Graphics / Corrupted Screen**
|
||||
- Cause: DMA during active display, wrong VRAM address
|
||||
- Debug: Ensure graphics updates only during NMI or Force Blank
|
||||
|
||||
### 5.4. Cross-Reference Documentation
|
||||
|
||||
For specific debugging scenarios:
|
||||
- **BRK Crashes:** `Docs/General/Troubleshooting.md` Section 2
|
||||
- **Stack Corruption:** `Docs/General/Troubleshooting.md` Section 3
|
||||
- **Processor State Issues:** `Docs/General/Troubleshooting.md` Section 4
|
||||
- **Namespace Problems:** `Docs/General/Troubleshooting.md` Section 5
|
||||
- **Memory Conflicts:** `Docs/General/Troubleshooting.md` Section 6
|
||||
- **Graphics Issues:** `Docs/General/Troubleshooting.md` Section 7
|
||||
- **ZScream-Specific:** `Docs/General/Troubleshooting.md` Section 8
|
||||
|
||||
|
||||
|
||||
@@ -155,6 +408,11 @@ When encountering unexpected crashes (often indicated by a `BRK` instruction in
|
||||
|
||||
This section details the layout and purpose of critical memory regions (WRAM and SRAM) and the symbol definition files that give them context.
|
||||
|
||||
**For comprehensive memory documentation, see:**
|
||||
- `Docs/Core/MemoryMap.md` - Complete WRAM/SRAM map with verified custom variables
|
||||
- `Docs/Core/Ram.md` - High-level overview of memory usage
|
||||
- `Docs/General/Troubleshooting.md` Section 6 - Memory conflict resolution
|
||||
|
||||
### 7.1. WRAM (Work RAM) Analysis
|
||||
|
||||
Work RAM (WRAM) holds the active, volatile state of the entire game. The following are some of the most critical variables for understanding real-time game logic.
|
||||
|
||||
@@ -4,53 +4,109 @@ This guide provides a step-by-step walkthrough for creating a new custom sprite
|
||||
|
||||
## 1. File Setup
|
||||
|
||||
1. **Create the Sprite File:** Create a new `.asm` file for your sprite in the appropriate subdirectory of `Sprites/` (e.g., `Sprites/Enemies/MyNewEnemy.asm`).
|
||||
2. **Include the File:** Open `Sprites/all_sprites.asm` and add an `incsrc` directive to include your new file. Make sure to place it in the correct bank section (e.g., Bank 30, 31, or 32) to ensure it gets compiled into the ROM.
|
||||
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
|
||||
; Properties for MyNewEnemy
|
||||
!SPRID = $XX ; CHOOSE AN UNUSED SPRITE ID!
|
||||
!NbrTiles = 02 ; Number of 8x8 tiles used in the largest frame (e.g., Darknut uses 03 for its multi-tile body)
|
||||
!Health = 10 ; Health points
|
||||
!Damage = 04 ; Damage dealt to Link on contact (04 = half a heart)
|
||||
!Harmless = 00 ; 00 = Harmful, 01 = Harmless
|
||||
!Hitbox = 08 ; Hitbox size (0-31)
|
||||
!ImperviousAll = 00 ; 01 = All attacks clink harmlessly
|
||||
!Statue = 00 ; 01 = Behaves like a solid statue
|
||||
!Prize = 01 ; Prize pack dropped on death (0-15)
|
||||
; ... and so on for all properties ...
|
||||
; =========================================================
|
||||
; Sprite Properties
|
||||
; =========================================================
|
||||
|
||||
; --- Design Considerations ---
|
||||
; * **Multi-purpose Sprite IDs:** A single `!SPRID` can be used for multiple distinct behaviors (e.g., Dark Link and Ganon) through the use of `SprSubtype`. This is a powerful technique for reusing sprite slots and creating multi-phase bosses or variations of enemies.
|
||||
* **Damage Handling for Bosses:** For boss sprites, `!Damage = 0` is acceptable if damage is applied through other means, such as spawned projectiles or direct contact logic within the main routine.
|
||||
* **Fixed vs. Dynamic Health:** While many sprites (like Booki) have dynamic health based on Link's sword level, some sprites (like Pols Voice) may have a fixed `!Health` value, simplifying their damage model.
|
||||
* **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. This is important for understanding how boss-specific behaviors are implemented.
|
||||
!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. Sometimes, shadow drawing might be conditional based on the sprite's current action or state, as seen in the Thunder Ghost sprite (`Sprites/Enemies/thunder_ghost.asm`).
|
||||
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
|
||||
PHB : PHK : PLB ; Set up bank registers (Push Bank, Push K, Pull Bank)
|
||||
JSR Sprite_MyNewEnemy_Draw
|
||||
JSL Sprite_DrawShadow ; Optional: Draw a shadow
|
||||
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
|
||||
@@ -61,21 +117,89 @@ Sprite_MyNewEnemy_Long:
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
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`.
|
||||
@@ -109,172 +233,645 @@ Sprite_MyNewEnemy_Main:
|
||||
RTS
|
||||
}
|
||||
|
||||
; State_Hurt:
|
||||
; {
|
||||
; ; Sprite was hit, flash and get knocked back
|
||||
; JSL Sprite_DamageFlash_Long
|
||||
; RTS
|
||||
; }
|
||||
; }
|
||||
;
|
||||
; --- Code Readability Note ---
|
||||
; For improved readability and maintainability, always prefer using named constants for hardcoded values (e.g., timers, speeds, health, magic numbers) and named labels for `JSL` calls to project-specific functions instead of direct numerical addresses. Additionally, be mindful of the Processor Status Register (P) flags (M and X) and use `REP #$20`/`SEP #$20` to explicitly set the accumulator/index register size when necessary, especially when interacting with memory or calling routines that expect a specific state.
|
||||
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 easiest method is to use the `%DrawSprite()` macro, which reads from a set of data tables you define. However, for highly customized drawing logic, such as dynamic tile selection, complex animation sequences, or precise OAM manipulation (as demonstrated in the Booki sprite's `Sprite_Booki_Draw` routine, which uses `REP`/`SEP` for 16-bit coordinate calculations), you may need to implement a custom drawing routine.
|
||||
This routine renders your sprite's graphics. The project provides the `%DrawSprite()` macro for standard drawing, which reads from data tables you define.
|
||||
|
||||
**Important Note on 16-bit OAM Calculations:** When performing OAM (Object Attribute Memory) calculations, especially for sprite positioning and offsets, it is crucial to explicitly manage the Processor Status Register (P) flags. Use `REP #$20` to set the accumulator to 16-bit mode before performing 16-bit arithmetic operations (e.g., adding `x_offsets` or `y_offsets` to sprite coordinates), and `SEP #$20` to restore it to 8-bit mode afterward if necessary. This ensures accurate positioning and prevents unexpected graphical glitches or crashes.
|
||||
### 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 (minus 1)
|
||||
.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
|
||||
.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
|
||||
.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
|
||||
; }
|
||||
;
|
||||
; --- Code Readability Note ---
|
||||
; For improved readability and maintainability, always prefer using named constants for hardcoded values (e.g., OAM properties, tile numbers) and named labels for `JSL` calls to project-specific functions instead of direct numerical addresses. Additionally, be mindful of the Processor Status Register (P) flags (M and X) and use `REP #$20`/`SEP #$20` to explicitly set the accumulator/index register size when necessary, especially when interacting with memory or calling routines that expect a specific state.
|
||||
.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. It automatically adds your sprite's _Prep and _Long routines to the game's sprite tables in Core/sprite_new_table.asm. Your sprite is now ready to be placed in the game world!
|
||||
The `%Set_Sprite_Properties()` macro you added in Step 2 handles the final integration automatically. It:
|
||||
|
||||
## 8. Advanced Sprite Design Patterns
|
||||
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`
|
||||
|
||||
### 8.1. Multi-Part Sprites and Child Sprites
|
||||
For complex bosses or entities, you can break them down into a main parent sprite and multiple child sprites.
|
||||
Your sprite is now ready to be placed in the game world using your level editor!
|
||||
|
||||
* **Parent Sprite Responsibilities:**
|
||||
* Spawns and manages its child sprites (e.g., Kydreeok spawning `kydreeok_head` instances, Darknut spawning probes, Goriya spawning its boomerang, Helmet Chuchu detaching its helmet/mask, Thunder Ghost spawning lightning attacks with directional logic, or Puffstool transforming into a bomb).
|
||||
* Monitors the state/health of its child sprites to determine its own phases, actions, or defeat conditions.
|
||||
* Often handles overall movement, phase transitions, and global effects.
|
||||
* Uses global variables (e.g., `Offspring1_Id`, `Offspring2_Id`) to store the sprite IDs of its children for easy referencing.
|
||||
* **Child Sprite Responsibilities:**
|
||||
* Handles its own independent logic, movement, and attacks.
|
||||
* May be positioned relative to the parent sprite.
|
||||
* Can use `SprSubtype` to differentiate between multiple instances of the same child sprite ID (e.g., left head vs. right head, Goriya vs. its Boomerang, or Helmet Chuchu's detached helmet/mask).
|
||||
* **Shared Sprite IDs for Variations:** A single `!SPRID` can also be used for different enemy variations (e.g., Keese, Fire Keese, Ice Keese, Vampire Bat) by assigning unique `SprSubtype` values. This is an efficient way to reuse sprite slots and base logic.
|
||||
* **Multi-Layered Drawing for Immersion**: For sprites that Link interacts with by appearing "inside" or "behind" them (e.g., the `minecart`), drawing the sprite in multiple parts (e.g., top and bottom) and allocating OAM from different regions can create a convincing sense of depth and immersion.
|
||||
## 8. Testing Your Sprite
|
||||
|
||||
### 8.2. Quest Integration and Dynamic Progression
|
||||
Boss fights and NPC interactions can be made more engaging by implementing multiple phases, dynamic health management, and deep integration with quest progression.
|
||||
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
|
||||
|
||||
* **Phase Transitions:** Trigger new phases based on health thresholds, timers, or the defeat of child sprites. Phases can involve:
|
||||
* Changing the boss's graphics or palette.
|
||||
* Altering movement patterns and attack routines.
|
||||
* Spawning new entities or environmental hazards.
|
||||
* **Health Management:** Boss health can be managed in various ways:
|
||||
* Directly via the parent sprite's `SprHealth`.
|
||||
* Indirectly by monitoring the health or state of child sprites (e.g., Kydreeok's main body health is tied to its heads).
|
||||
* Using custom variables (e.g., `!KydrogPhase`) to track overall boss progression.
|
||||
* **Quest Integration and Pacification:** Sprites can be integrated into quests where "defeat" might mean pacification rather than outright killing. This can involve refilling health and changing the sprite's state to a subdued or quest-complete action (e.g., Wolfos granting a mask after being subdued by the Song of Healing).
|
||||
* **Deep Quest Integration**: Many NPCs (e.g., `bug_net_kid`, `deku_scrub`, `eon_owl`, `farore`, `maple`, `mask_salesman`, `mermaid` (Librarian), `ranch_girl`, `tingle`, `vasu`, `zora_princess`) are deeply integrated into questlines, with their dialogue, actions, and even spawning/despawning being conditional on various SRAM flags, WRAM flags, collected items, and game progression. This allows for rich, evolving narratives and dynamic NPC roles.
|
||||
## 9. Common Issues and Solutions
|
||||
|
||||
### 8.3. Code Reusability and Modularity
|
||||
When designing multiple sprites, especially bosses, look for opportunities to reuse code and create modular components.
|
||||
### 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
|
||||
|
||||
* **Shared Logic:** If multiple sprites perform similar actions (e.g., spawning stalfos, specific movement patterns, or tile collision handling like `Goriya_HandleTileCollision` used by Darknut), consider creating common functions or macros that can be called by different sprites. This reduces code duplication and improves maintainability.
|
||||
* **Refactoring:** Regularly refactor duplicated code into reusable components. This makes it easier to apply changes consistently across related sprites.
|
||||
### 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
|
||||
|
||||
### 8.4. Configurability and Avoiding Hardcoded Values
|
||||
To improve sprite reusability and ease of placement in different maps, avoid hardcoding values that might change.
|
||||
### 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
|
||||
|
||||
* **Activation Triggers:** Instead of hardcoding specific screen coordinates or camera values for activation (e.g., Octoboss's Y-coordinate trigger), consider using:
|
||||
* Sprite properties (`SprMiscA`, `SprMiscB`, etc.) to store configurable trigger values.
|
||||
* Room-specific flags or events that can be set externally.
|
||||
* Relative positioning or distance checks to Link.
|
||||
* **Timers, Speeds, Offsets:** Always prefer using named constants (`!CONSTANT_NAME = value`) for numerical values that control behavior (timers, speeds, offsets, health thresholds). This significantly improves readability and makes it easier to adjust parameters without searching through code.
|
||||
* **Function Calls:** Use named labels for `JSL` calls to project-specific functions instead of direct numerical addresses.
|
||||
* **Conditional Drawing/Flipping:** When drawing, use sprite-specific variables (e.g., `SprMiscC` in the Booki sprite) to store directional information and conditionally adjust OAM offsets or flip bits to achieve horizontal or vertical flipping without hardcoding.
|
||||
* **State Machine Implementation:** For complex AI, utilize `JumpTableLocal` with `SprAction` to create robust state machines, as seen in the Booki sprite's `_Main` routine.
|
||||
* **Randomness:** Incorporate `JSL GetRandomInt` to introduce variability into sprite behaviors, such as movement patterns or state transitions, making enemies less predictable.
|
||||
### 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
|
||||
|
||||
### 8.5. Overriding Vanilla Behavior
|
||||
When creating new boss sequences or modifying existing enemies, it's common to override vanilla sprite behavior.
|
||||
## 10. Advanced Sprite Design Patterns
|
||||
|
||||
* **Hooking Entry Points:** Identify the entry points of vanilla sprites (e.g., `Sprite_Blind_Long`, `Sprite_Blind_Prep`) and use `org` directives to redirect execution to your custom routines.
|
||||
* **Contextual Checks:** Within your custom routines, perform checks (e.g., `LDA.l $7EF3CC : CMP.b #$06`) to determine if the vanilla behavior should be executed or if your custom logic should take over.
|
||||
* **SRAM Flags:** Utilize SRAM flags (`$7EF3XX`) to track quest progression or conditions that dictate whether vanilla or custom behavior is active.
|
||||
## 10. Advanced Sprite Design Patterns
|
||||
|
||||
### 8.6. Dynamic Enemy Behavior
|
||||
For more engaging and adaptive enemies, consider implementing dynamic behaviors:
|
||||
### 10.1. Multi-Part Sprites and Child Sprites
|
||||
|
||||
* **Dynamic Difficulty Scaling:** Adjust enemy properties (health, damage, prize drops) based on player progression (e.g., Link's sword level, number of collected items). This can be done in the `_Prep` routine by indexing into data tables. For example, the Booki sprite (`Sprites/Enemies/booki.asm`) sets its health based on Link's current sword level.
|
||||
* **Dynamic State Management:** Utilize `SprMisc` variables (e.g., `SprMiscB` for sub-states) and timers (e.g., `SprTimerA` for timed transitions) to create complex and adaptive behaviors. The Booki sprite demonstrates this with its `SlowFloat` and `FloatAway` sub-states and timed transitions.
|
||||
* **Advanced Interaction/Guard Mechanics:** Implement behaviors like parrying (`Guard_ParrySwordAttacks` in Darknut) or chasing Link along a single axis (`Guard_ChaseLinkOnOneAxis` in Darknut) to create more sophisticated enemy encounters.
|
||||
* **Random Movement with Collision Response:** Implement enemies that move in random directions and change their behavior upon collision with tiles, as demonstrated by the Goriya sprite's `Goriya_HandleTileCollision` routine.
|
||||
* **Dynamic Appearance and Conditional Vulnerability:** Sprites can dynamically change their appearance and vulnerability based on internal states or player actions. For instance, the Helmet Chuchu (`Sprites/Enemies/helmet_chuchu.asm`) changes its graphics and becomes vulnerable only after its helmet/mask is removed, often by a specific item like the Hookshot.
|
||||
* **Stunned State and Counter-Attack:** Some sprites react to damage by entering a stunned state, then performing a counter-attack (e.g., Puffstool spawning spores or transforming into a bomb).
|
||||
* **Interaction with Thrown Objects:** Sprites can be designed to interact with objects thrown by Link (e.g., `ThrownSprite_TileAndSpriteInteraction_long` used by Puffstool), allowing for environmental puzzles or unique combat strategies.
|
||||
* **Interaction with Non-Combat Player Actions:** Sprites can react to specific player actions beyond direct attacks, suchs as playing a musical instrument. The Pols Voice (`Sprites/Enemies/pols_voice.asm`) despawns and drops a prize if Link plays the flute.
|
||||
* **Specific Movement Routines for Attacks:** During attack animations, sprites may utilize specialized movement routines like `Sprite_MoveLong` (seen in Thunder Ghost) to control their position and trajectory precisely.
|
||||
* **Damage Reaction (Invert Speed):** A common dynamic behavior is for a sprite to invert its speed or change its movement pattern upon taking damage, as seen in Pols Voice.
|
||||
* **Global State-Dependent Behavior:** Sprite properties and behaviors can be dynamically altered based on global game state variables (e.g., `WORLDFLAG` in the Sea Urchin sprite), allowing for different enemy characteristics in various areas or game progression points.
|
||||
* **Simple Movement/Idle:** Not all sprites require complex movement. Some may have simple idle animations and primarily interact through contact damage or being pushable, as exemplified by the Sea Urchin.
|
||||
* **Timed Attack and Stun States:** Sprites can have distinct attack and stunned states that are governed by timers, allowing for predictable attack patterns and temporary incapacitation (e.g., Poltergeist's `Poltergeist_Attack` and `Poltergeist_Stunned` states).
|
||||
* **Conditional Invulnerability:** Invulnerability can be dynamic, changing based on the sprite's state. For example, a sprite might be impervious to certain attacks only when in a specific state, or its `SprDefl` flags might be manipulated to reflect temporary invulnerability (as suggested by the Poltergeist's properties).
|
||||
* **Direct SRAM Interaction:** Implement mechanics that directly interact with Link's inventory or status in SRAM (e.g., stealing items, modifying rupee count). Remember to explicitly manage processor status flags (`REP`/`SEP`) when performing mixed 8-bit/16-bit operations on SRAM addresses to prevent unexpected behavior or crashes.
|
||||
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).
|
||||
|
||||
### 8.7. Subtype-based Behavior and Dynamic Transformations
|
||||
Leverage `SprSubtype` to create diverse enemy variations from a single sprite ID and enable dynamic transformations based on in-game conditions.
|
||||
**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
|
||||
|
||||
* **Multiple Variations per `!SPRID`**: A single `!SPRID` can represent several distinct enemy types (e.g., Ice Keese, Fire Keese, Vampire Bat) by assigning unique `SprSubtype` values. This allows for efficient reuse of sprite slots and base logic while providing varied challenges.
|
||||
* **Dynamic Environmental Transformation**: Sprites can change their `SprSubtype` (and thus their behavior/appearance) in response to environmental factors. For example, an Octorok might transform from a land-based to a water-based variant upon entering water tiles. This adds depth and realism to enemy interactions with the environment.
|
||||
**Child Sprite Responsibilities:**
|
||||
* Handles independent logic, movement, and attacks
|
||||
* May be positioned relative to parent sprite
|
||||
* Uses `SprSubtype` to differentiate between multiple instances
|
||||
|
||||
### 8.8. Vanilla Sprite Overrides and Conditional Logic
|
||||
When modifying existing enemies or creating new boss sequences, it's common to override vanilla sprite behavior. This allows for custom implementations while retaining the original sprite ID.
|
||||
**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
|
||||
}
|
||||
|
||||
* **Hooking Entry Points:** Identify the entry points of vanilla sprites (e.g., `Sprite_Blind_Long`, `Sprite_Blind_Prep`) and use `org` directives to redirect execution to your custom routines. This is crucial for replacing or extending existing behaviors.
|
||||
* **Contextual Checks:** Within your custom routines, perform checks (e.g., `LDA.l $7EF3CC : CMP.b #$06`) to determine if the vanilla behavior should be executed or if your custom logic should take over. This provides flexibility and allows for conditional modifications.
|
||||
; 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
|
||||
}
|
||||
```
|
||||
|
||||
### 8.9. Centralized Handlers and Multi-Purpose Sprites
|
||||
Many sprite definitions serve as central handlers for multiple distinct NPCs or objects, leveraging conditional logic to dispatch to specific behaviors and drawing routines. This approach significantly optimizes resource usage and allows for a diverse range of content from a single sprite ID.
|
||||
**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
|
||||
|
||||
* **Single Sprite, Multiple Roles**: A single `!SPRID` can represent several distinct characters or objects (e.g., `followers.asm` for Zora Baby, Old Man, Kiki; `mermaid.asm` for Mermaid, Maple, Librarian; `zora.asm` for various Zora types; `collectible.asm` for different items; `deku_leaf.asm` for Deku Leaf and Whirlpool). This is achieved by dispatching to different routines based on `SprSubtype`, `AreaIndex`, `WORLDFLAG`, or other custom flags.
|
||||
* **Resource Optimization**: By consolidating related behaviors under one sprite ID, the project reduces the overall number of unique sprite definitions, leading to more efficient memory usage and easier management.
|
||||
### 10.2. Quest Integration and Dynamic Progression
|
||||
|
||||
### 8.10. Interactive Puzzle Elements and Environmental Interaction
|
||||
Many objects are designed as interactive puzzle elements that require specific player actions and often interact with environmental tiles. These sprites contribute to the game's puzzle design and environmental storytelling.
|
||||
Boss fights and NPC interactions can be deeply integrated with quest progression using SRAM flags, dynamic health management, and multi-phase battles.
|
||||
|
||||
* **Player-Manipulated Objects**: Sprites like the `ice_block` and `minecart` are core interactive puzzle elements that Link can push or ride. Their behavior is governed by precise collision detection, alignment checks, and movement physics.
|
||||
* **Environmental Triggers**: Objects like `mineswitch` and `switch_track` respond to player actions (e.g., attacking a switch) or environmental factors (e.g., a minecart moving over a track segment) to change their state or influence other game elements.
|
||||
* **Detailed Collision and Alignment Logic**: Implementing precise collision detection and alignment checks (as seen in `ice_block` and `minecart`) is crucial for creating fair and intuitive interactive puzzle elements.
|
||||
**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
|
||||
```
|
||||
|
||||
### 8.11. Player State Manipulation and Cinematic Control
|
||||
Certain sprites are designed to directly influence Link's state, movement, and the overall cinematic presentation of the game, creating immersive and narrative-driven sequences.
|
||||
**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
|
||||
|
||||
* **Direct Link Control**: Sprites like `farore` and `maple` can directly manipulate Link's movement, animation, and game state during cutscenes or specific interactions (e.g., putting Link to sleep, controlling his auto-movement). The `minecart` also heavily manipulates Link's state while he is riding it.
|
||||
* **Cinematic Transitions**: Routines within these sprites can trigger screen transitions, visual filters, and sound effects to enhance the cinematic quality of key narrative moments.
|
||||
**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
|
||||
|
||||
### 8.12. Robust Shop and Item Management Systems
|
||||
Several NPCs implement sophisticated systems for granting items, selling goods, or offering services, often with complex conditional logic based on Link's inventory, rupee count, and quest progression.
|
||||
**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
|
||||
```
|
||||
|
||||
* **Conditional Transactions**: Shopkeepers like the `mask_salesman` and `tingle` implement systems for selling items, including checks for Link's rupee count and deducting the cost upon purchase.
|
||||
* **Item Granting and Quest Rewards**: NPCs such as `maku_tree`, `hyrule_dream`, `ranch_girl`, `vasu`, and `zora_princess` are responsible for granting key items or rewards to Link, often as part of a questline or in exchange for services.
|
||||
* **Inventory and Progression Checks**: These systems extensively check Link's inventory (e.g., Ocarina, collected masks, found rings) and various game progression flags to determine available options and dialogue.
|
||||
### 10.4. Code Reusability and Best Practices
|
||||
|
||||
### 8.13. Robustness and Player Feedback
|
||||
Implementing mechanisms for error handling and providing clear feedback to the player is crucial for a polished and user-friendly experience.
|
||||
**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
|
||||
}
|
||||
```
|
||||
|
||||
* **Error Handling and Prevention**: Sprites can include logic to prevent unintended behavior or game-breaking scenarios. For example, the `portal_sprite` includes logic to despawn if placed on invalid tiles, preventing Link from getting stuck.
|
||||
* **Clear Player Feedback**: Providing audio or visual cues (e.g., playing an error sound when a portal is placed incorrectly) helps the player understand what is happening and why, improving the overall user experience.
|
||||
**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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user