NPC sprite engine, viewport culling fix, file reorganization

src/overworld/ → src/engine/overworld/ (movement.asm, overworld.asm, + new map_sprites.asm)
src/engine/battle/move_effects/: add recoil.asm, reflect_light_screen.asm

NPC sprite engine (previous sessions):
- sprite_oam.asm: PrepareOAMData builds shadow OAM from wSpriteStateData1/2;
  NPC dos_base from MAPY/MAPX + origin+4 offset; sub-block walk-offset smoothing
  (NPCs scroll in lockstep with BG tiles during player walk steps)
- ppu.asm: render_sprites uses spr_dos_sy/sx tables from PrepareOAMData;
  spr_oam_valid count replaces GB 0xA0 OAM-Y sentinel (which falsely culled NPCs
  at extended-viewport distances)
- map_sprites.asm: InitMapSprites + LoadNPCSpriteTiles; NPC sprite assets (Oak,
  Girl, Fisher) embedded in .data; FindOrAssignVramSlot deduplicates VRAM slots
- overworld.asm: NPC slot wiring in EnterMap, UpdateSprites loop calls
  UpdateNonPlayerSprite for each slot
- movement.asm: CheckSpriteAvailability, DetectCollisionBetweenSprites,
  GetTileSpriteStandsOn, InitializeSpriteStatus/ScreenPosition all translated

CheckSpriteAvailability viewport culling fix (this session):
- Old one-sided 8-bit check gave MAPY ∈ [wYCoord, wYCoord+11] / MAPX ∈
  [wXCoord, wXCoord+19] — asymmetric due to origin+4 offset; NPCs disappeared
  5-7 metatile columns too early west, ~2 rows too early north
- New two-sided 32-bit signed range: MAPY ∈ [wYCoord−3, wYCoord+11],
  MAPX ∈ [wXCoord−7, wXCoord+14] — covers full 320×200 viewport + 1-tile buffer
- 32-bit comparison (movzx/lea ecx,[eax-N]/cmp ecx,edx/jg) prevents byte
  underflow when player is within 3/7 tiles of north/west map edge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Happyarch
2026-06-23 13:13:03 -04:00
parent 105eaaf065
commit d4e91edd74
15 changed files with 2470 additions and 741 deletions

View File

@@ -30,7 +30,7 @@ supporting home routines (`src/util/copy_data.asm`, `src/video/lcd_control.asm`,
(`src/gfx/load_font.asm` 1bpp→2bpp expansion from `gfx/font/font.png`,
`src/text/text.asm` PlaceString/TextBoxBorder). The title screen
(`src/movie/title.asm`) and the overworld map loader/renderer
(`src/overworld/overworld.asm`) both render correctly in DOSBox-X: the title
(`src/engine/overworld/overworld.asm`) both render correctly in DOSBox-X: the title
shows "Pokémon Yellow Version", and `SKIP_TITLE=1` boots straight into a fully
drawn Pallet Town (Oak's Lab, tree border, sign) in the DMG-green palette.
Player movement now works: `OverworldLoop` reads the joypad and walks the
@@ -45,7 +45,7 @@ The `UpdatePlayerOAM` scaffold has been replaced by the **faithful sprite
engine**: `PrepareOAMData` (`src/gfx/sprite_oam.asm`) builds shadow OAM from the
16-slot `wSpriteStateData1/2` arrays (facing/animation table, under-grass
priority, OBP→CGB palette mapping, `$80+` tile path), and `UpdateSprites`
(`src/overworld/movement.asm`, with `UpdatePlayerSprite`/`Func_4e32`/`Func_5274`)
(`src/engine/overworld/movement.asm`, with `UpdatePlayerSprite`/`Func_4e32`/`Func_5274`)
advances the player's facing and walk-frame leg animation each `OverworldLoop`
iteration. `frame.asm:update_oam` runs `PrepareOAMData` and DMA-copies shadow OAM
→ `$FE00` in the `DelayFrame` pipeline (gated on `wUpdateSpritesEnabled`).
@@ -64,7 +64,7 @@ pixel offset `(Xoff, Yoff)` derived from the coarse block alignment and the fine
The old 256×256 VRAM torus emulation and related `RedrawRowOrColumn` rings are gone.
**Any new routine that writes VRAM tile data must set `g_tilecache_dirty`**.
**Temporary scaffold — two out-of-map clamps (`src/overworld/overworld.asm`):**
**Temporary scaffold — two out-of-map clamps (`src/engine/overworld/overworld.asm`):**
the extended 40×25-tile viewport draws a larger area than the original 20×18 and
the player is pinned at screen-center, so a player-centered camera near a map
edge reaches past the populated `wOverworldMap` data. Two complementary stopgaps

View File

@@ -51,7 +51,7 @@ Prioritized task list. Check off items as they complete; add new items with phas
and true smaller-X-wins priority — currently reverse-OAM-order draw, which
only honors the index tiebreak.)
- [x] Sprite engine: faithful PrepareOAMData + UpdateSprites (replaces the
UpdatePlayerOAM scaffold) — src/gfx/sprite_oam.asm, src/overworld/movement.asm.
UpdatePlayerOAM scaffold) — src/gfx/sprite_oam.asm, src/engine/overworld/movement.asm.
Player now renders via the real shadow-OAM pipeline driven by
wSpriteStateData1/2: UpdateSprites advances facing + walk-frame animation,
PrepareOAMData (run in the DelayFrame pipeline, DMA-copied to $FE00) builds
@@ -101,7 +101,7 @@ Prioritized task list. Check off items as they complete; add new items with phas
Assets generated by tools/gen_title_gfx_inc.py → dos_port/assets/*.inc.
Verified build: make check + make succeed, pokeyellow_dos.exe produced.
- [x] Translate overworld engine (map loader/renderer; Pallet Town renders)
- [x] Translate player movement — src/overworld/overworld.asm
- [x] Translate player movement — src/engine/overworld/overworld.asm
(OverworldLoop joypad state machine; AdvancePlayerSprite + sliding
wMapViewVRAMPointer + RedrawRowOrColumn edge redraw wired into DelayFrame;
MoveTileBlockMapPointer{E,W,S,N}; land collision via IsTilePassable against
@@ -127,7 +127,7 @@ Prioritized task list. Check off items as they complete; add new items with phas
out-of-map clamps.** The 40×25-tile viewport draws a larger area than the
original 20×18 and the player is screen-centered, so the camera can reach
past the populated `wOverworldMap` data near map edges. Two temporary
clamps in `src/overworld/overworld.asm` stop the garbage:
clamps in `src/engine/overworld/overworld.asm` stop the garbage:
(1) `DrawTileBlock` (~line 727): clamps a block ID past the blockset → block 0;
(2) `LoadCurrentMapView` (~line 816): substitutes `wMapBackgroundTile`
for any block-map read outside `[wOverworldMap, wOverworldMapEnd)`.
@@ -181,7 +181,7 @@ Prioritized task list. Check off items as they complete; add new items with phas
joypad latch. Fix: latch the direction used for the collision check for the
duration of the current step (don't allow mid-step facing updates to influence
the next collision sample). See `OverworldLoop:.handleDirection` TODO comment
in `src/overworld/overworld.asm`. Defer until after Phase 2 NPC work.
in `src/engine/overworld/overworld.asm`. Defer until after Phase 2 NPC work.
- [x] **PERF — heavily optimize render_bg** (2026-06-15). The per-pixel
2bpp→8bpp decode is no longer in the hot path: a 24 KB decoded-tile cache
(`tile_cache`, 384 tiles × 64 B, BGP baked in) pre-decodes $8000-$97FF

View File

@@ -37,7 +37,7 @@ map and transitions place the player correctly.
## Stage A — Native-width BG renderer (COMPLETED 2026-06-16)
### Key insight that makes this clean
`LoadCurrentMapView` (`src/overworld/overworld.asm`) already maintains
`LoadCurrentMapView` (`src/engine/overworld/overworld.asm`) already maintains
**`wSurroundingTiles`** = a **44×32-tile** decoded view (`SURROUNDING_WIDTH=44`,
`SURROUNDING_HEIGHT=32`) with a 2-tile (16px) margin on each side of the 40×25
viewport, rebuilt **once per 16px walk-step** (on `wWalkCounter==7`, the first
@@ -118,7 +118,7 @@ These exist only to feed the torus; with the native surface they are dead:
### Files touched (Stage A)
- `dos_port/src/ppu/ppu.asm` — surface size, `render_bg`, decode helper, drop
torus/diff/base state.
- `dos_port/src/overworld/overworld.asm` — delete the VRAM-ring scroll routines
- `dos_port/src/engine/overworld/overworld.asm` — delete the VRAM-ring scroll routines
and the `wMapViewVRAMPointer` slide; simplify `AdvancePlayerSprite` to: update
coords/blocks → `LoadCurrentMapView` → accumulate `H_SCX/H_SCY`. Drop
`CopyMapViewToVRAM`/`FillExtraVRAMRows` calls in `LoadMapData`/`.mapTransition`
@@ -163,7 +163,7 @@ the out-of-map clamp paints border tiles (clean, but not real map).
- [ ] Delete `W_REDRAW_ROW_OR_COLUMN_SRC_TILES` (80 bytes) entirely, as its corresponding logic was ripped out in Stage A.
- [ ] Shift `W_TILEMAP_BACKUP2` down to `0xED80` (which is `0xE580` + `0x800`) to safely accommodate the expanded `wOverworldMap` buffer.
2. `dos_port/src/overworld/overworld.asm`:
2. `dos_port/src/engine/overworld/overworld.asm`:
- [ ] `SCREEN_BLOCK_WIDTH/HEIGHT` and `SURROUNDING_WIDTH/HEIGHT` are decoupled from `MAP_BORDER` and remain unchanged.
- [ ] The codebase already leverages `MAP_BORDER * 2` symbolically (e.g., `add al, MAP_BORDER * 2`). Merely audit and update the hardcoded math inside the comments (e.g., change `10 + 6 = 16` to `10 + 12 = 22`). Note: Stage C will programmatically fix the `gen_map_headers.py` math.
- [ ] Keep the out-of-map clamp in `DrawTileBlock` strictly as a safety net.

View File

@@ -237,7 +237,7 @@ writes are correct but invisible until Phase 1 OAM pass), MainMenu (→ EnterMap
- **Sources:** `home/overworld.asm` — ResetMapVariables, CopyMapViewToVRAM, DrawTileBlock,
LoadCurrentMapView, LoadTilesetTilePatternData, LoadTileBlockMap, LoadScreenRelatedData,
LoadMapData; Phase 2 scaffold: EnterMap/SetupPalletTown/OverworldLoop
- **Translated:** `dos_port/src/overworld/overworld.asm`
- **Translated:** `dos_port/src/engine/overworld/overworld.asm`
- **Date:** 2026-06-13
- **H-flag:** Not involved — all pure data movement.
- **Bug tags:** None.
@@ -270,7 +270,7 @@ unaffected (zeroed wrong WRAM before; correct zeroing now, same visual result).
---
## Player movement — `src/overworld/overworld.asm` (2026-06-14)
## Player movement — `src/engine/overworld/overworld.asm` (2026-06-14)
Translated the movement-relevant subset of `home/overworld.asm:OverworldLoop` /
`OverworldLoopLessDelay` plus the helpers from
@@ -330,7 +330,7 @@ the screen center.
---
## OAM sprite renderer + player sprite — `src/ppu/ppu.asm`, `src/overworld/overworld.asm` (2026-06-14)
## OAM sprite renderer + player sprite — `src/ppu/ppu.asm`, `src/engine/overworld/overworld.asm` (2026-06-14)
HAL renderer (not a pret translation) plus an overworld scaffold to drive it.
@@ -368,7 +368,7 @@ Pallet Town and faces the direction of movement.
---
## Sprite engine — `src/gfx/sprite_oam.asm`, `src/overworld/movement.asm` (2026-06-15)
## Sprite engine — `src/gfx/sprite_oam.asm`, `src/engine/overworld/movement.asm` (2026-06-15)
Replaced the `UpdatePlayerOAM` / `player_oam_table` scaffold with a faithful
translation of the Yellow sprite engine, so the player renders through the real
@@ -427,7 +427,7 @@ standing tiles present at `$8000`, distinct walking tiles at `$8800`. Default an
---
## BG scanline rewrite + DrawTileBlock clamp — `src/ppu/ppu.asm`, `src/overworld/overworld.asm` (2026-06-15)
## BG scanline rewrite + DrawTileBlock clamp — `src/ppu/ppu.asm`, `src/engine/overworld/overworld.asm` (2026-06-15)
- **Sources:** HAL renderer (`render_bg`, not a pret translation); `DrawTileBlock`
(`home/overworld.asm`).
@@ -595,7 +595,7 @@ map) is a separate follow-on; the DrawTileBlock clamp stays (E/W + past-map-end)
## Native-width BG renderer (Stage A)
- **Sources:** `dos_port/src/ppu/ppu.asm`, `dos_port/src/overworld/overworld.asm`
- **Sources:** `dos_port/src/ppu/ppu.asm`, `dos_port/src/engine/overworld/overworld.asm`
- **Date:** 2026-06-16
- **H-flag:** Not involved.
- **Bug tags:** None.
@@ -610,7 +610,7 @@ Removed dead VRAM-ring scroll routines (`CopyMapViewToVRAM`, `FillExtraVRAMRows`
---
## Movement delay + door-exit logic fixes — `src/overworld/overworld.asm` (2026-06-20)
## Movement delay + door-exit logic fixes — `src/engine/overworld/overworld.asm` (2026-06-20)
- **Sources:** `home/overworld.asm` (OverworldLoop / WarpFound2.done),
`engine/overworld/movement.asm` (UpdatePlayerSprite/.handleDirectionButtonPress),
@@ -1595,7 +1595,7 @@ Worker expertly separated the core box transaction operations (Depositing, Withd
## OverworldLoop warp bug fixes
- **Source:** `home/overworld.asm` (warp resolution logic)
- **Translated:** `dos_port/src/overworld/overworld.asm`
- **Translated:** `dos_port/src/engine/overworld/overworld.asm`
- **Date:** 2026-06-20
- **H-flag:** not involved
- **Bug tags:** none (regression fixes, not known original bugs)
@@ -1898,3 +1898,131 @@ If you need to exclude RGBASM-conditional content, add a filter to the generator
- **Notes:** used test/or with 1<<PROTECTED_BY_MIST for bit/set since it's a bit index
---
## PrepareOAMData — extended viewport + walk-offset NPC tracking
- **Source:** `engine/overworld/movement.asm:PrepareOAMData`
- **Translated:** `dos_port/src/gfx/sprite_oam.asm:PrepareOAMData`
- **Date:** 2026-06-23
- **H-flag:** not involved
- **Bug tags:** none
### Summary
Extended `PrepareOAMData` and `render_sprites` to handle the DOS 320×200 viewport
(44×32 visible blocks), replacing 8-bit OAM coordinate arithmetic that overflowed for
NPCs beyond ~8 blocks from the player.
### Changes
**Problem:** The original `render_sprites` derived the screen position of each sprite
by sign-extending the 8-bit OAM Y/X bytes (`movsx eax, byte [ebp + esi]`), then adding
a fixed letterbox offset. For NPCs whose `(MAPY - wYCoord) * 16 - 4` overflows 8 bits
(≥ 8 blocks from the camera), the OAM byte wraps (e.g., MAPY=18, wYCoord=8 → 0xAC),
producing a wildly wrong screen Y in `render_sprites`. Simultaneously, culling used
`cmp al, 0xA0; jae .nextSprite` (GB convention for inactive entries), which falsely
culled any NPC whose OAM Y byte was ≥ 0xA0 even when the computed DOS position was on-screen.
**Fix — 32-bit position tables:**
- Added BSS globals in `ppu.asm`: `spr_dos_sy[40]`, `spr_dos_sx[40]` (one dword per OAM
entry), and `spr_oam_valid` (count of entries PrepareOAMData wrote this frame).
- `PrepareOAMData` computes a 32-bit `dos_base_y/x` using a hybrid formula:
- Slot 0 (player): `movsx(H_SPRITE_SCREEN_Y) + 36` / `movsx(H_SPRITE_SCREEN_X) + 96`
(safe; YPIXELS ≤ 127 for the player).
- NPC slots 1–15: `(MAPY - wYCoord) * 16 + 32` and `(MAPX - wXCoord) * 16 + 96`
(full 32-bit; no overflow regardless of map size).
- In `tileLoop`, `edx = (edi - W_SHADOW_OAM) >> 2` (OAM entry index 0–39). Each tile's
dos_base + tableY/X offset is written to `spr_dos_sy[edx*4]` and `spr_dos_sx[edx*4]`.
- At `.ret`, `spr_oam_valid = H_OAM_BUFFER_OFFSET / 4`.
- `render_sprites` now reads from the tables instead of recomputing from 8-bit OAM bytes.
The `cmp al, 0xA0` cull is replaced by `cmp ecx, [spr_oam_valid]; jae .nextSprite`.
**Fix — walk-offset NPC smoothing:**
The 32-bit MAPY-based dos_base is block-aligned (constant across a walk step). The BG
scrolls 2 px/frame via `bg_scy`/`bg_scx`. Without compensation, NPCs drift 2 px/frame
against BG tiles and then snap 16 px at the block boundary. Fix: after `.dos_base_done`,
for NPC slots only, subtract `YSTEP_VECTOR * (8 - walk_counter) * 2` (and same for X)
from `dos_base_y/x_tmp`. This is an exact reverse of the BG scroll already applied, so
NPCs track BG tiles smoothly throughout all 8 walk frames.
### Key constants
- `W_SPRITE_PLAYER_Y_STEP_VECTOR = 0xC103` — signed byte; +1 south, -1 north
- `W_SPRITE_PLAYER_X_STEP_VECTOR = 0xC105` — signed byte; +1 east, -1 west
- `W_WALK_COUNTER = 0xCFC4` — 8-frame countdown during a walk step (0 = standing)
- `spr_dos_sy / spr_dos_sx` — BSS arrays declared in `ppu.asm`, externs in `sprite_oam.asm`
---
## render_sprites — extended viewport culling
- **Source:** (DOS-only; no GB equivalent — PPU software renderer)
- **Translated:** `dos_port/src/ppu/ppu.asm:render_sprites`
- **Date:** 2026-06-23
- **H-flag:** not involved
- **Bug tags:** none
### Summary
`render_sprites` was rewritten to use the `spr_dos_sy/sx` position tables filled by
`PrepareOAMData` (see entry above) instead of recomputing positions from 8-bit OAM
bytes. Entry validity is now checked via `spr_oam_valid` count rather than the GB-style
`cmp al, 0xA0` OAM-Y sentinel, which falsely culled on-screen NPCs whose 8-bit OAM Y
had wrapped past 0xA0 due to the extended viewport distance.
The symptom that surfaced the bug: walking RIGHT kept NPCs visible (only X changed;
Y-byte stable). Walking UP/DOWN/LEFT triggered premature NPC disappearance because
those directions changed the Y-byte across the 0xA0 threshold.
---
## InitMapSprites / LoadNPCSpriteTiles
- **Source:** `engine/overworld/map_sprites.asm:InitMapSprites` + `LoadMapSpriteTilePatterns`
- **Translated:** `dos_port/src/engine/overworld/map_sprites.asm`
- **Date:** 2026-06-23
- **H-flag:** not involved
- **Bug tags:** none
### Summary
Implements the data pipeline from map object binary → NPC sprite slots → VRAM:
1. Clears NPC slots 1–15 in `wSpriteStateData1/2`.
2. Reads `sprite_count` + per-NPC 6-byte records from the GB address pointed to
by `W_OBJECT_DATA_PTR_TEMP` (set by `LoadMapHeader`).
3. Populates `PICTUREID`, `MAPY/MAPX`, `MOVEMENTBYTE1/2`, `MOVEMENTDELAY`,
`IMAGEBASEOFFSET`, and `ISTRAINER` for each slot.
4. Trainer NPCs: reads extra 2 bytes (trainer_class, trainer_num) and sets ISTRAINER=1.
5. `FindOrAssignVramSlot`: deduplicates sprite types; each unique type gets a
`imageBaseOffset` (3, 4, 5, …); slots 1=player, 2=Pikachu are reserved.
6. `LoadNPCSpriteTiles`: copies 192 bytes (12 still tiles) per unique sprite type to
`[EBP + GB_VCHARS0 + (imageBaseOffset-1)*192]`; sets `g_tilecache_dirty=1`.
NPC assets (`npc_oak_still.inc`, `npc_girl_still.inc`, `npc_fisher_still.inc`) are
embedded in `.data` section of `map_sprites.asm` via `NpcSpriteAssets` lookup table.
---
## CheckSpriteAvailability — DOS viewport culling fix
- **Source:** `engine/overworld/movement.asm:CheckSpriteAvailability`
- **Translated:** `dos_port/src/engine/overworld/movement.asm:CheckSpriteAvailability`
- **Date:** 2026-06-23
- **H-flag:** not involved
- **Bug tags:** none (DOS-port adaptation, not a GB bug)
### Summary
The original pret visibility range test used 8-bit unsigned byte arithmetic:
`cmp wYCoord, MAPY; jae .invisible` (lower bound) + `add wYCoord, SCREEN_HEIGHT/2-1; jb .invisible` (upper). With `SCREEN_HEIGHT=25` (DOS) this gave `MAPY ∈ [wYCoord, wYCoord+11]`. Due to the `origin+4` offset stored in `MAPY/MAPX`, the actual-tile-delta visible range was `[-4, +7]` Y and `[-4, +15]` X — badly asymmetric with the DOS 320×200 viewport needing `[-6, +6]` Y and `[-10, +9]` X.
**Symptom:** NPCs disappeared 5–7 metatile columns too early to the west (X) and ~2 rows too early to the north (Y). One-sided culling was the fingerprint that isolated this to `CheckSpriteAvailability` rather than the symmetric `render_sprites` or `dos_base` formulas.
**Fix:** Two-sided 32-bit signed range comparisons replacing the old `jae`/`jb` pair:
- Y: `MAPY ∈ [wYCoord−3, wYCoord+11]` → actual delta `[−7, +7]` (1-tile buffer)
- X: `MAPX ∈ [wXCoord−7, wXCoord+14]` → actual delta `[−11, +10]` (1-tile buffer)
**Critical:** Lower-bound subtraction must use 32-bit signed registers — `sub al, 3` wraps to `0xFC` when `wYCoord=0`, culling every NPC. Fix: `movzx eax; lea ecx,[eax-N]; cmp ecx,edx; jg .invisible`.
---

View File

@@ -90,8 +90,9 @@ GAME_SRCS := \
src/gfx/load_font.asm \
src/text/text.asm \
src/movie/title.asm \
src/overworld/overworld.asm \
src/overworld/movement.asm
src/engine/overworld/overworld.asm \
src/engine/overworld/movement.asm \
src/engine/overworld/map_sprites.asm
# Debug-only sources (linked only when the corresponding flag is set).
# Any debug flag implies SKIP_TITLE — debug harnesses boot straight to the overworld.
@@ -138,13 +139,19 @@ assets/map_headers.inc assets/extra_includes.inc:
python3 tools/gen_map_headers.py $(GEN_MAP_HEADERS_FLAGS)
# Overworld object depends on all generated assets
src/overworld/overworld.o: \
src/engine/overworld/overworld.o: \
assets/overworld_gfx.inc \
assets/overworld_blocks.inc \
assets/pallet_town_blk.inc \
assets/map_headers.inc \
assets/extra_includes.inc
# map_sprites depends on the NPC tile asset files
src/engine/overworld/map_sprites.o: \
assets/npc_oak_still.inc \
assets/npc_girl_still.inc \
assets/npc_fisher_still.inc
assets: assets/overworld_gfx.inc assets/overworld_blocks.inc assets/pallet_town_blk.inc \
assets/map_headers.inc assets/extra_includes.inc

File diff suppressed because it is too large Load Diff

View File

@@ -149,8 +149,12 @@ SPRITESTATEDATA2_YDISPLACEMENT equ 0x2 ; Y displacement (init 8)
SPRITESTATEDATA2_XDISPLACEMENT equ 0x3 ; X displacement (init 8)
SPRITESTATEDATA2_MAPY equ 0x4 ; map Y position (metatile units, origin+4)
SPRITESTATEDATA2_MAPX equ 0x5 ; map X position (metatile units, origin+4)
SPRITESTATEDATA2_WALKANIMCOUNTER equ 0x0 ; 8-frame countdown while walking (0=standing)
SPRITESTATEDATA2_MOVEMENTDELAY equ 0x1 ; frames until next random walk attempt
SPRITESTATEDATA2_MOVEMENTBYTE1 equ 0x6 ; $ff=STAY, $fe=WALK(random), <$fe=scripted
SPRITESTATEDATA2_GRASSPRIORITY equ 0x7
SPRITESTATEDATA2_GRASSPRIORITY equ 0x7 ; $80=under grass (set by CheckSpriteAvailability)
SPRITESTATEDATA2_MOVEMENTBYTE2 equ 0x8 ; dir constraint from map object (0x00/0x01/0x02/0xFF)
SPRITESTATEDATA2_ISTRAINER equ 0x9 ; 1 if trainer NPC, 0 otherwise
SPRITESTATEDATA2_IMAGEBASEOFFSET equ 0xE
; NPC movement-byte sentinel values (constants/map_object_constants.asm)
@@ -455,11 +459,19 @@ SPRITE_FACING_LEFT equ 0x08
SPRITE_FACING_RIGHT equ 0x0C
; hJoyHeld bit positions (constants/hardware.inc B_PAD_*; active-high in hJoyHeld)
PAD_A equ 1 << 0 ; B_BTN_A = 0 (button byte low nibble)
PAD_B equ 1 << 1 ; B_BTN_B = 1
PAD_SELECT equ 1 << 2 ; B_BTN_SELECT = 2
PAD_START equ 1 << 3 ; B_BTN_START = 3
PAD_RIGHT equ 1 << 4 ; B_PAD_RIGHT = 4
PAD_LEFT equ 1 << 5 ; B_PAD_LEFT = 5
PAD_UP equ 1 << 6 ; B_PAD_UP = 6
PAD_DOWN equ 1 << 7 ; B_PAD_DOWN = 7
; Map object NPC flags (constants/map_object_constants.asm)
TRAINER_FLAG equ 0x40 ; 1 << BIT_TRAINER
ITEM_FLAG equ 0x80 ; 1 << BIT_ITEM
; RedrawRowOrColumn modes (constants/gfx_constants.asm)
REDRAW_COL equ 1
REDRAW_ROW equ 2

View File

@@ -0,0 +1,107 @@
%include "gb_memmap.inc"
%include "gb_macros.inc"
extern hWhoseTurn
extern wPlayerMoveNum
extern wBattleMonMaxHP
extern wEnemyMoveNum
extern wEnemyMonMaxHP
extern wDamage
extern wBattleMonHP
extern wHPBarMaxHP
extern wHPBarOldHP
extern wHPBarNewHP
extern wTileMap
extern wHPBarType
extern predef_UpdateHPBar2
extern PrintText
extern _HitWithRecoilText
STRUGGLE equ 0xA5
global RecoilEffect_
RecoilEffect_:
mov al, [ebp + hWhoseTurn]
and al, al
mov al, [ebp + wPlayerMoveNum]
mov esi, wBattleMonMaxHP
jz .recoilEffect
mov al, [ebp + wEnemyMoveNum]
mov esi, wEnemyMonMaxHP
.recoilEffect:
mov dh, al
mov al, [ebp + wDamage]
mov bh, al
mov al, [ebp + wDamage + 1]
mov bl, al
shr bh, 1
rcr bl, 1
mov al, dh
cmp al, STRUGGLE
jz .gotRecoilDamage
shr bh, 1
rcr bl, 1
.gotRecoilDamage:
mov al, bh
or al, bl
jnz .updateHP
inc bl
.updateHP:
mov al, [ebp + esi]
inc esi
mov [ebp + wHPBarMaxHP + 1], al
mov al, [ebp + esi]
mov [ebp + wHPBarMaxHP], al
push ebx
mov ebx, wBattleMonHP
sub ebx, wBattleMonMaxHP
add esi, ebx
pop ebx
mov al, [ebp + esi]
mov [ebp + wHPBarOldHP], al
sub al, bl
mov [ebp + esi], al
dec esi
mov [ebp + wHPBarNewHP], al
mov al, [ebp + esi]
mov [ebp + wHPBarOldHP + 1], al
sbb al, bh
mov [ebp + esi], al
mov [ebp + wHPBarNewHP + 1], al
jnc .getHPBarCoords
xor al, al
mov [ebp + esi], al
inc esi
mov [ebp + esi], al
mov esi, wHPBarNewHP
mov [ebp + esi], al
inc esi
mov [ebp + esi], al
.getHPBarCoords:
mov esi, wTileMap + 9 * 20 + 10
mov al, [ebp + hWhoseTurn]
and al, al
mov al, 1
jz .updateHPBar
mov esi, wTileMap + 2 * 20 + 2
xor al, al
.updateHPBar:
mov [ebp + wHPBarType], al
call predef_UpdateHPBar2
mov esi, HitWithRecoilText
jmp PrintText
global HitWithRecoilText
HitWithRecoilText:
db 0x17 ; TX_FAR
dd _HitWithRecoilText
db 0x50 ; TX_END

View File

@@ -0,0 +1,23 @@
%include "gb_memmap.inc"
%include "gb_macros.inc"
extern Bankswitch
extern _LightScreenProtectedText
extern _ReflectGainedArmorText
global EffectCallBattleCore
EffectCallBattleCore:
mov bh, 0
jmp Bankswitch
global LightScreenProtectedText
LightScreenProtectedText:
db 0x17
dd _LightScreenProtectedText
db 0x50
global ReflectGainedArmorText
ReflectGainedArmorText:
db 0x17 ; TX_FAR
dd _ReflectGainedArmorText
db 0x50 ; TX_END

View File

@@ -0,0 +1,290 @@
; map_sprites.asm — NPC sprite slot initialization and tile loading.
;
; Faithful translation (pret cross-reference):
; InitMapSprites engine/overworld/map_sprites.asm:InitMapSprites /
; InitOutsideMapSprites / LoadSpriteSetFromMapHeader
; LoadNPCSpriteTiles engine/overworld/map_sprites.asm:LoadMapSpriteTilePatterns /
; ReadSpriteSheetData
;
; After LoadMapHeader sets W_OBJECT_DATA_PTR_TEMP to the GB address of the
; sprite_count byte, InitMapSprites:
; 1. Clears NPC slots 1-15 in wSpriteStateData1/2.
; 2. Reads the sprite_count and per-NPC 6/7/8-byte records from the map
; object binary emitted by gen_map_headers.py.
; 3. Populates PICTUREID, MAPY/MAPX, MOVEMENTBYTE1/2, IMAGEBASEOFFSET,
; ISTRAINER, and initial MOVEMENTDELAY for each NPC slot.
; 4. Calls LoadNPCSpriteTiles to copy 12 still tiles per unique sprite
; type to the appropriate VRAM slot (imageBaseOffset-1)*192 from $8000.
;
; Walking leg animation (tiles 0x80-0x8B in vChars1) is deferred to Phase 3.
; All WALK NPCs use still tiles only; animFrameCounter is held at 0.
;
; Build: nasm -f coff -I include/ -I . -o map_sprites.o src/engine/overworld/map_sprites.asm
bits 32
%include "gb_memmap.inc"
%include "gb_macros.inc"
extern g_tilecache_dirty
global InitMapSprites
; ---------------------------------------------------------------------------
; Constants
; ---------------------------------------------------------------------------
NPC_TILE_BYTES equ 12 * TILE_SIZE ; 192 bytes = 12 tiles per sprite type
NPC_SLOTS_START equ 1 ; first NPC slot (slot 0 = player)
NPC_SLOTS_MAX equ 15 ; max NPC slots
VRAM_SLOT_START equ 3 ; imageBaseOffset 1=player 2=Pikachu 3+=NPCs
SPRITE_SET_SIZE equ 12 ; max unique sprite types per map
; ---------------------------------------------------------------------------
; BSS — per-map sprite deduplication table (reset at each InitMapSprites call)
; ---------------------------------------------------------------------------
section .bss
npc_sprite_set: resb SPRITE_SET_SIZE ; sprite IDs in current map (0 = unused)
npc_vram_slots: resb SPRITE_SET_SIZE ; imageBaseOffset for each entry
; ---------------------------------------------------------------------------
; NPC sprite tile assets (still poses only; 12 tiles = 192 bytes each)
; Included here so they land in .data and the NpcSpriteAssets table's dd
; pointers resolve to correct flat DS addresses.
; ---------------------------------------------------------------------------
section .data
; NpcSpriteAssets — table of (sprite_id byte, asset_ptr dd) pairs, terminated
; by sprite_id = 0x00. LoadNPCSpriteTiles scans this to find each sprite's
; source data.
NpcSpriteAssets:
db 0x03 ; SPRITE_OAK (0x03)
dd npc_oak_still
db 0x0D ; SPRITE_GIRL (0x0D)
dd npc_girl_still
db 0x2F ; SPRITE_FISHER (0x2F)
dd npc_fisher_still
db 0x00 ; terminator
%include "assets/npc_oak_still.inc"
%include "assets/npc_girl_still.inc"
%include "assets/npc_fisher_still.inc"
; ---------------------------------------------------------------------------
; Code
; ---------------------------------------------------------------------------
section .text
; ---------------------------------------------------------------------------
; InitMapSprites — populate NPC sprite slots from the map's object data.
; Pret ref: engine/overworld/map_sprites.asm:InitMapSprites (outside maps path).
;
; Prerequisites: LoadMapHeader must have run; it sets W_OBJECT_DATA_PTR_TEMP to
; the GB address of the sprite_count byte in the map_object binary.
;
; All registers preserved (pushad/popad).
; ---------------------------------------------------------------------------
InitMapSprites:
pushad
; --- Clear NPC slots 1-15 in wSpriteStateData1 and wSpriteStateData2 ---
lea edi, [ebp + W_SPRITE_STATE_DATA_1 + 0x10] ; slot 1 onward
xor al, al
mov ecx, NPC_SLOTS_MAX * 0x10 ; 15 * 16 = 240 bytes
rep stosb
lea edi, [ebp + W_SPRITE_STATE_DATA_2 + 0x10]
mov ecx, NPC_SLOTS_MAX * 0x10
rep stosb
; --- Clear per-call sprite deduplication table ---
mov edi, npc_sprite_set
xor al, al
mov ecx, SPRITE_SET_SIZE * 2 ; sprite_set + vram_slots = 24 bytes
rep stosb
; --- Read sprite_count from W_OBJECT_DATA_PTR_TEMP ---
movzx esi, word [ebp + W_OBJECT_DATA_PTR_TEMP] ; ESI = GB addr of sprite_count
movzx ecx, byte [ebp + esi]
inc esi ; advance past sprite_count byte
test ecx, ecx
jz .done ; no NPCs on this map
; EBX = current slot byte offset within wSpriteStateData1/2 (slot N at N*0x10)
; EDX = next available imageBaseOffset (starts at VRAM_SLOT_START = 3)
; ESI = read pointer into GB map object binary (already past sprite_count)
; ECX = NPC count remaining
mov ebx, 0x10 ; start at slot 1 (0x10)
mov edx, VRAM_SLOT_START ; next_vram_slot = 3
.slot_loop:
; Read sprite_id byte
movzx eax, byte [ebp + esi]
inc esi
mov [ebp + ebx + W_SPRITE_STATE_DATA_1 + SPRITESTATEDATA1_PICTUREID], al
mov byte [ebp + ebx + W_SPRITE_STATE_DATA_1 + SPRITESTATEDATA1_MOVEMENTSTATUS], 0
mov byte [ebp + ebx + W_SPRITE_STATE_DATA_1 + SPRITESTATEDATA1_FACINGDIRECTION], SPRITE_FACING_DOWN
; Save sprite_id (AL) and count (ECX) — FindOrAssignVramSlot may clobber ECX
push eax
push ecx
; Read mapy
movzx eax, byte [ebp + esi]
inc esi
mov [ebp + ebx + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPY], al
; Read mapx
movzx eax, byte [ebp + esi]
inc esi
mov [ebp + ebx + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPX], al
; Read mov_byte (STAY=0xFF / WALK=0xFE / scripted=<0xFE)
movzx eax, byte [ebp + esi]
inc esi
mov [ebp + ebx + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MOVEMENTBYTE1], al
; Read dir_byte (0x00=ANY / 0x01=UP_DOWN / 0x02=LEFT_RIGHT / 0xFF=NONE)
movzx eax, byte [ebp + esi]
inc esi
mov [ebp + ebx + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MOVEMENTBYTE2], al
; Set initial movement delay (30 frames before first walk attempt)
mov byte [ebp + ebx + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MOVEMENTDELAY], 30
; Read text_byte; check for trainer (TRAINER_FLAG = 0x40)
movzx eax, byte [ebp + esi]
inc esi
xor edi, edi ; is_trainer = 0
test al, TRAINER_FLAG
jz .not_trainer
mov edi, 1 ; is_trainer = 1
add esi, 2 ; skip trainer_class + trainer_num bytes
.not_trainer:
test al, ITEM_FLAG
jz .not_item
inc esi ; skip item_id byte (items handled by text engine, Phase 3+)
.not_item:
mov [ebp + ebx + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_ISTRAINER], edi ; EDI=0 or 1; bytes 0xA-0xC are free
; Assign or look up a VRAM slot for this sprite type
pop ecx
pop eax ; AL = sprite_id
push ecx
call FindOrAssignVramSlot ; In: AL=sprite_id, EDX=next_slot. Out: AL=imageBaseOffset
mov [ebp + ebx + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_IMAGEBASEOFFSET], al
pop ecx
add ebx, 0x10 ; advance to next slot
dec ecx
jnz .slot_loop
.slots_done:
call LoadNPCSpriteTiles
.done:
popad
ret
; ---------------------------------------------------------------------------
; FindOrAssignVramSlot — VRAM deduplication for NPC sprite types.
; Pret ref: engine/overworld/map_sprites.asm:GetSpriteImageBaseOffset (logic).
;
; In: AL = sprite_id
; EDX = next available imageBaseOffset (caller-owned, updated on assign)
; Out: AL = imageBaseOffset for this sprite_id
; EDX = updated (incremented if a new slot was assigned)
; Clobbers: ESI, ECX
; ---------------------------------------------------------------------------
FindOrAssignVramSlot:
push esi
push ecx
mov esi, 0 ; index into npc_sprite_set
.scan:
cmp esi, SPRITE_SET_SIZE
jge .assign ; table full (shouldn't happen for ≤12 maps)
movzx ecx, byte [npc_sprite_set + esi]
test cl, cl
jz .assign ; empty slot → sprite not seen before
cmp cl, al
je .found
inc esi
jmp .scan
.found:
; Return cached imageBaseOffset for this sprite_id
movzx eax, byte [npc_vram_slots + esi]
pop ecx
pop esi
ret
.assign:
; New sprite type — record it and assign next_vram_slot
cmp esi, SPRITE_SET_SIZE
jge .overflow
mov [npc_sprite_set + esi], al
mov [npc_vram_slots + esi], dl
.overflow:
mov al, dl ; return assigned (or last valid) slot
inc edx ; next_vram_slot++
pop ecx
pop esi
ret
; ---------------------------------------------------------------------------
; LoadNPCSpriteTiles — copy still tiles for each unique sprite type to VRAM.
; Pret ref: engine/overworld/map_sprites.asm:LoadMapSpriteTilePatterns /
; ReadSpriteSheetData.
;
; Iterates npc_sprite_set; for each non-zero entry, looks up the source asset
; in NpcSpriteAssets and copies NPC_TILE_BYTES (192) bytes to:
; [EBP + GB_VCHARS0 + (imageBaseOffset - 1) * NPC_TILE_BYTES]
;
; Sets g_tilecache_dirty = 1 after any copy.
; All registers preserved (push/pop).
; ---------------------------------------------------------------------------
LoadNPCSpriteTiles:
push eax
push ebx
push ecx
push esi
push edi
mov ebx, 0 ; sprite_set index
.tile_loop:
cmp ebx, SPRITE_SET_SIZE
jge .done
movzx eax, byte [npc_sprite_set + ebx]
test al, al
jz .done ; end of used entries
; Compute VRAM destination flat address:
; edi = EBP + GB_VCHARS0 + (imageBaseOffset - 1) * NPC_TILE_BYTES
movzx ecx, byte [npc_vram_slots + ebx]
dec ecx ; (imageBaseOffset - 1)
imul ecx, NPC_TILE_BYTES ; * 192
lea edi, [ebp + ecx + GB_VCHARS0] ; flat addr in GB VRAM
; Look up asset source for this sprite_id (AL)
mov esi, NpcSpriteAssets
.asset_scan:
movzx ecx, byte [esi]
test cl, cl
jz .next_sprite ; sprite not in table (no asset loaded)
cmp cl, al
je .found_asset
add esi, 5 ; skip 1-byte id + 4-byte ptr
jmp .asset_scan
.found_asset:
mov esi, [esi + 1] ; load flat asset pointer (the dd value)
mov ecx, NPC_TILE_BYTES ; 192 bytes
rep movsb
mov byte [g_tilecache_dirty], 1
.next_sprite:
inc ebx
jmp .tile_loop
.done:
pop edi
pop esi
pop ecx
pop ebx
pop eax
ret

View File

@@ -17,7 +17,7 @@
; NPC scope: static NPCs only (MOVEMENTSTATUS 0→1 init, CheckSpriteAvailability,
; InitializeSpriteScreenPosition). Random/scripted NPC movement is deferred.
;
; Build: nasm -f coff -I include/ -I . -o movement.o src/overworld/movement.asm
; Build: nasm -f coff -I include/ -I . -o movement.o src/engine/overworld/movement.asm
bits 32
@@ -170,32 +170,36 @@ Func_5033:
;
; In: ESI = slot byte offset, hTilePlayerStandingOn = VRAM tile group byte.
; Out: CF = 1 → invisible (IMAGEINDEX set to $ff); CF = 0 → visible.
; Clobbers AL, BL, CL, EBX, ECX.
; Clobbers AL, CL, EDX, EBX, ECX.
; ---------------------------------------------------------------------------
CheckSpriteAvailability:
; IsObjectHidden stub: no toggleable objects yet — always visible.
mov al, [ebp + esi + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MOVEMENTBYTE1]
cmp al, WALK
jb .skipXYVisibility ; scripted movement: always show, skip range test
; Y range test: visible when wYCoord < MAPY <= wYCoord + SCREEN_HEIGHT/2-1 (8)
mov bl, [ebp + esi + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPY]
mov al, [ebp + W_Y_COORD]
cmp al, bl
je .skipYVisibility ; wYCoord == MAPY → same row, visible
jae .spriteInvisible ; wYCoord >= MAPY → NPC above screen region
add al, SCREEN_HEIGHT / 2 - 1 ; 8 (metatile rows to bottom of visible area)
cmp al, bl
jb .spriteInvisible ; wYCoord+8 < MAPY → NPC below screen region
.skipYVisibility:
; X range test: visible when wXCoord < MAPX <= wXCoord + SCREEN_WIDTH/2-1 (9)
mov bl, [ebp + esi + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPX]
mov al, [ebp + W_X_COORD]
cmp al, bl
je .skipXYVisibility
jae .spriteInvisible ; wXCoord >= MAPX → left of screen region
add al, SCREEN_WIDTH / 2 - 1 ; 9 (metatile columns to right of visible area)
cmp al, bl
jb .spriteInvisible ; wXCoord+9 < MAPX → right of screen region
; Y range test — DOS 320×200 viewport.
; MAPY = actual_tile_y + 4 (origin+4). Player screen y = 96.
; Visible range: actual delta ∈ [−6,+6] + 1-tile buffer → MAPY ∈ [wYCoord−3, wYCoord+11].
; 32-bit signed comparison prevents byte underflow when wYCoord < 3.
movzx edx, byte [ebp + esi + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPY]
movzx eax, byte [ebp + W_Y_COORD]
lea ecx, [eax - 3]
cmp ecx, edx
jg .spriteInvisible ; MAPY < wYCoord−3 → off top of screen
lea ecx, [eax + 11]
cmp ecx, edx
jl .spriteInvisible ; MAPY > wYCoord+11 → off bottom of screen
; X range test — DOS 320×200 viewport.
; MAPX = actual_tile_x + 4 (origin+4). Player screen x = 160.
; Visible range: actual delta ∈ [−10,+9] + 1-tile buffer → MAPX ∈ [wXCoord−7, wXCoord+14].
movzx edx, byte [ebp + esi + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPX]
movzx eax, byte [ebp + W_X_COORD]
lea ecx, [eax - 7]
cmp ecx, edx
jg .spriteInvisible ; MAPX < wXCoord−7 → off left of screen
lea ecx, [eax + 14]
cmp ecx, edx
jl .spriteInvisible ; MAPX > wXCoord+14 → off right of screen
.skipXYVisibility:
; Text-box tile check: if any of the 4 tiles the sprite stands on is a text-box
; tile (ID >= MAP_TILESET_SIZE / $60), the sprite is obscured → invisible.

View File

@@ -21,7 +21,7 @@
; tiles to $8800 (the VRAM layout the sprite engine indexes)
;
; The player now renders through the real sprite engine: UpdateSprites
; (src/overworld/movement.asm) drives the per-slot image index, and PrepareOAMData
; (src/engine/overworld/movement.asm) drives the per-slot image index, and PrepareOAMData
; (src/gfx/sprite_oam.asm, run in the DelayFrame pipeline) builds shadow OAM from it.
;
; Asset layout in ROM window (EBP + $4000–$54FF and $1000+; see gb_memmap.inc):
@@ -32,7 +32,7 @@
; $5000 : Route1.blk, $5200: Route21.blk, $5400: tileset header, $540C: map headers
; $1000+: city/route .blk files (ViridianCity, PewterCity, … — see OW_*_BLK_GBADDR)
;
; Build: nasm -f coff -I include/ -I . -o overworld.o src/overworld/overworld.asm
; Build: nasm -f coff -I include/ -I . -o overworld.o src/engine/overworld/overworld.asm
bits 32
@@ -51,6 +51,7 @@ extern g_player_marker_on
extern UpdateSprites
extern ClearSprites
extern g_tilecache_dirty
extern InitMapSprites
%ifdef DEBUG_DUMP
extern DebugDumpMemory
%endif
@@ -684,7 +685,7 @@ LoadMapData:
call ResetMapVariables
call LoadTextBoxTilePatterns
call LoadMapHeader
; TODO: InitMapSprites — ; TODO: sprite engine (Phase 2)
call InitMapSprites
call LoadScreenRelatedData
call LoadScreenRelatedData

View File

@@ -28,9 +28,14 @@ bits 32
%include "gb_macros.inc"
extern HideSprites
extern spr_dos_sy, spr_dos_sx, spr_oam_valid
global PrepareOAMData
section .bss
dos_base_y_tmp: resd 1 ; per-sprite DOS base Y for extended viewport
dos_base_x_tmp: resd 1 ; per-sprite DOS base X for extended viewport
section .text
; ---------------------------------------------------------------------------
@@ -105,6 +110,51 @@ PrepareOAMData:
call Func_4a7b ; AL = VRAM base tile from image index
mov [ebp + W_SAVED_SPRITE_IMAGE_INDEX], al
; Compute 32-bit DOS base position for extended 320×200 viewport.
; Slot 0 (player): YPIXELS-based (always ≤127, no 8-bit overflow).
; Slots 1-15 (NPCs): MAPY/MAPX-based (32-bit, handles full map range).
test esi, esi
jnz .dos_base_npc
movsx eax, byte [ebp + H_SPRITE_SCREEN_Y]
add eax, 36
mov [dos_base_y_tmp], eax
movsx eax, byte [ebp + H_SPRITE_SCREEN_X]
add eax, 96
mov [dos_base_x_tmp], eax
jmp .dos_base_done
.dos_base_npc:
movsx eax, byte [ebp + esi + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPY]
movsx ecx, byte [ebp + W_Y_COORD]
sub eax, ecx
imul eax, 16
add eax, 32
mov [dos_base_y_tmp], eax
movsx eax, byte [ebp + esi + W_SPRITE_STATE_DATA_2 + SPRITESTATEDATA2_MAPX]
movsx ecx, byte [ebp + W_X_COORD]
sub eax, ecx
imul eax, 16
add eax, 96
mov [dos_base_x_tmp], eax
.dos_base_done:
; Sub-block walk tracking: subtract the player's current walk pixel offset from
; NPC dos_base so NPCs scroll in lockstep with the BG during a walk step.
; Slot 0 (player) tracks sub-block position via YPIXELS — skip.
test esi, esi
jz .no_walk_offset ; slot 0 = player
movzx ecx, byte [ebp + W_WALK_COUNTER]
test ecx, ecx
jz .no_walk_offset ; not walking
mov eax, 8
sub eax, ecx ; frames elapsed = 8 - walk_counter
shl eax, 1 ; * 2 px/frame
movsx ecx, byte [ebp + W_SPRITE_PLAYER_Y_STEP_VECTOR]
imul ecx, eax ; walk_offset_y = YSTEP * elapsed * 2
sub [dos_base_y_tmp], ecx ; NPC tracks BG vertical scroll
movsx ecx, byte [ebp + W_SPRITE_PLAYER_X_STEP_VECTOR]
imul ecx, eax ; walk_offset_x = XSTEP * elapsed * 2
sub [dos_base_x_tmp], ecx ; NPC tracks BG horizontal scroll
.no_walk_offset:
movzx edi, byte [ebp + H_OAM_BUFFER_OFFSET]
add edi, W_SHADOW_OAM ; EDI = GB offset of shadow-OAM write cursor
mov ebx, edx ; EBX walks the facing data block
@@ -112,17 +162,33 @@ PrepareOAMData:
inc ebx
.tileLoop:
; Y = hSpriteScreenY + 16 + tableY
; OAM entry index from write cursor: EDI = W_SHADOW_OAM + N*4 at loop top
mov edx, edi
sub edx, W_SHADOW_OAM
shr edx, 2 ; EDX = OAM entry index 0..39
; spr_dos_sy[N] = dos_base_y + signed(tableY)
movsx eax, byte [ebx]
add eax, [dos_base_y_tmp]
mov [spr_dos_sy + edx*4], eax
; OAM Y = H_SPRITE_SCREEN_Y + 16 + tableY
mov al, [ebp + H_SPRITE_SCREEN_Y]
add al, 0x10
add al, [ebx]
add al, byte [ebx]
mov [ebp + edi], al
inc ebx
inc edi
; X = hSpriteScreenX + 8 + tableX
; spr_dos_sx[N] = dos_base_x + signed(tableX)
movsx eax, byte [ebx]
add eax, [dos_base_x_tmp]
mov [spr_dos_sx + edx*4], eax
; OAM X = H_SPRITE_SCREEN_X + 8 + tableX
mov al, [ebp + H_SPRITE_SCREEN_X]
add al, 0x08
add al, [ebx]
add al, byte [ebx]
mov [ebp + edi], al
inc ebx
inc edi
@@ -186,6 +252,12 @@ PrepareOAMData:
jne .clearLoop
.ret:
; Publish the count of valid OAM entries written this frame.
; render_sprites uses this instead of the OAM Y byte to detect active entries
; (OAM Y can exceed $A0 due to 8-bit YPIXELS overflow for far NPCs).
movzx eax, byte [ebp + H_OAM_BUFFER_OFFSET]
shr eax, 2 ; byte count / 4 = entry count
mov [spr_oam_valid], eax
pop edi
pop esi
pop edx

View File

@@ -102,6 +102,11 @@ bg_scy: resd 1 ; SCY shadow
bg_scx: resd 1 ; SCX shadow
sprite_shift_x: resd 1 ; Dynamic X shift for sprites to align with DOS camera
sprite_shift_y: resd 1 ; Dynamic Y shift for sprites
; 32-bit DOS position tables for each OAM entry (filled by PrepareOAMData)
global spr_dos_sy, spr_dos_sx, spr_oam_valid
spr_dos_sy: resd OAM_COUNT ; signed DOS Y for entry 0..OAM_COUNT-1
spr_dos_sx: resd OAM_COUNT ; signed DOS X for entry 0..OAM_COUNT-1
spr_oam_valid: resd 1 ; count of valid entries written this frame (set by PrepareOAMData)
; bg_surface: 384×288 raw-color mirror of wSurroundingTiles.
bg_surface: resb 384 * 288
@@ -424,18 +429,16 @@ render_sprites:
.spriteLoop:
mov esi, [spr_oam_ptr]
movzx eax, byte [ebp + esi] ; Y (screen Y + 16)
sub eax, OAM_Y_OFS
movsx eax, al ; sign-extend relative Y coordinate
add eax, 36 ; y' = y + 36 (60 -> 96)
add eax, [sprite_shift_y] ; apply camera clamp shift
mov ecx, esi ; entry index = (oam_ptr - GB_OAM) >> 2
sub ecx, GB_OAM
shr ecx, 2
cmp ecx, [spr_oam_valid] ; skip entries PrepareOAMData did not write
jae .nextSprite
mov eax, [spr_dos_sy + ecx*4] ; 32-bit DOS Y set by PrepareOAMData
add eax, [sprite_shift_y]
mov [spr_sy], eax
movzx eax, byte [ebp + esi + 1] ; X (screen X + 8)
sub eax, OAM_X_OFS ; eax = screen X
movsx eax, al ; sign-extend relative X coordinate
add eax, 96 ; x' = x + 96 (64 -> 160)
add eax, [sprite_shift_x] ; apply camera clamp shift
mov eax, [spr_dos_sx + ecx*4] ; 32-bit DOS X set by PrepareOAMData
add eax, [sprite_shift_x]
mov [spr_sx], eax
movzx eax, byte [ebp + esi + 2] ; tile id
shl eax, 4

View File

@@ -4,7 +4,7 @@
Parses:
constants/map_constants.asm → map name → (id, w, h)
data/maps/headers/*.asm → map label → (map_const, tileset_name)
data/maps/objects/*.asm → map label → (border, warp_list, sign_count, sprite_count)
data/maps/objects/*.asm → map label → (border, warp_list, sign_count, sprites)
Emits:
- Tileset dispatch tables (TilesetGfxPtrs, TilesetBlocksPtrs, TilesetCollPtrs,
@@ -138,6 +138,70 @@ TILESET_CANONICAL = [
"beach_house", # 24
]
# ---------------------------------------------------------------------------
# NPC object-event constant resolution tables
# Source: constants/sprite_constants.asm, constants/map_object_constants.asm
# ---------------------------------------------------------------------------
_SPRITE_CONSTS = {
'SPRITE_NONE': 0x00, 'SPRITE_RED': 0x01, 'SPRITE_BLUE': 0x02,
'SPRITE_OAK': 0x03, 'SPRITE_YOUNGSTER': 0x04, 'SPRITE_MONSTER': 0x05,
'SPRITE_COOLTRAINER_F': 0x06, 'SPRITE_COOLTRAINER_M': 0x07,
'SPRITE_LITTLE_GIRL': 0x08, 'SPRITE_BIRD': 0x09,
'SPRITE_MIDDLE_AGED_MAN': 0x0A, 'SPRITE_GAMBLER': 0x0B,
'SPRITE_SUPER_NERD': 0x0C, 'SPRITE_GIRL': 0x0D, 'SPRITE_HIKER': 0x0E,
'SPRITE_BEAUTY': 0x0F, 'SPRITE_GENTLEMAN': 0x10, 'SPRITE_DAISY': 0x11,
'SPRITE_BIKER': 0x12, 'SPRITE_SAILOR': 0x13, 'SPRITE_COOK': 0x14,
'SPRITE_BIKE_SHOP_CLERK': 0x15, 'SPRITE_MR_FUJI': 0x16,
'SPRITE_GIOVANNI': 0x17, 'SPRITE_ROCKET': 0x18, 'SPRITE_CHANNELER': 0x19,
'SPRITE_WAITER': 0x1A, 'SPRITE_SILPH_WORKER_F': 0x1B,
'SPRITE_MIDDLE_AGED_WOMAN': 0x1C, 'SPRITE_BRUNETTE_GIRL': 0x1D,
'SPRITE_LANCE': 0x1E, 'SPRITE_UNUSED_RED_1': 0x1F,
'SPRITE_SCIENTIST': 0x20, 'SPRITE_ROCKER': 0x21, 'SPRITE_SWIMMER': 0x22,
'SPRITE_SAFARI_ZONE_WORKER': 0x23, 'SPRITE_GYM_GUIDE': 0x24,
'SPRITE_GRAMPS': 0x25, 'SPRITE_CLERK': 0x26, 'SPRITE_FISHING_GURU': 0x27,
'SPRITE_GRANNY': 0x28, 'SPRITE_NURSE': 0x29,
'SPRITE_LINK_RECEPTIONIST': 0x2A, 'SPRITE_SILPH_PRESIDENT': 0x2B,
'SPRITE_SILPH_WORKER_M': 0x2C, 'SPRITE_WARDEN': 0x2D,
'SPRITE_CAPTAIN': 0x2E, 'SPRITE_FISHER': 0x2F,
'SPRITE_KOGA': 0x30, 'SPRITE_GUARD': 0x31, 'SPRITE_UNUSED_RED_2': 0x32,
'SPRITE_MOM': 0x33, 'SPRITE_BALDING_GUY': 0x34, 'SPRITE_LITTLE_BOY': 0x35,
'SPRITE_UNUSED_RED_3': 0x36, 'SPRITE_GAMEBOY_KID': 0x37,
'SPRITE_FAIRY': 0x38, 'SPRITE_AGATHA': 0x39, 'SPRITE_BRUNO': 0x3A,
'SPRITE_LORELEI': 0x3B, 'SPRITE_SEEL': 0x3C, 'SPRITE_PIKACHU': 0x3D,
'SPRITE_OFFICER_JENNY': 0x3E, 'SPRITE_SANDSHREW': 0x3F,
'SPRITE_ODDISH': 0x40, 'SPRITE_BULBASAUR': 0x41, 'SPRITE_JIGGLYPUFF': 0x42,
'SPRITE_CLEFAIRY': 0x43, 'SPRITE_CHANSEY': 0x44,
'SPRITE_JESSIE': 0x45, 'SPRITE_JAMES': 0x46,
'SPRITE_POKE_BALL': 0x47, 'SPRITE_FOSSIL': 0x48, 'SPRITE_BOULDER': 0x49,
'SPRITE_PAPER': 0x4A, 'SPRITE_POKEDEX': 0x4B, 'SPRITE_CLIPBOARD': 0x4C,
'SPRITE_SNORLAX': 0x4D, 'SPRITE_UNUSED_OLD_AMBER': 0x4E,
'SPRITE_OLD_AMBER': 0x4F, 'SPRITE_UNUSED_GAMBLER_ASLEEP_1': 0x50,
'SPRITE_UNUSED_GAMBLER_ASLEEP_2': 0x51, 'SPRITE_GAMBLER_ASLEEP': 0x52,
}
_MOV_CONSTS = {'WALK': 0xFE, 'STAY': 0xFF}
_DIR_CONSTS = {
'NONE': 0xFF, 'ANY_DIR': 0x00, 'UP_DOWN': 0x01, 'LEFT_RIGHT': 0x02,
'DOWN': 0xD0, 'UP': 0xD1, 'LEFT': 0xD2, 'RIGHT': 0xD3,
}
_TRAINER_FLAG = 0x40 # constants/map_object_constants.asm: TRAINER = 1 << BIT_TRAINER
_ITEM_FLAG = 0x80 # constants/map_object_constants.asm: ITEM = 1 << BIT_ITEM
def _resolve_const(name, table, context=''):
"""Resolve a named constant or hex/decimal literal to an integer."""
if name in table:
return table[name]
if name.startswith('$'):
return int(name[1:], 16)
if name.lower().startswith('0x'):
return int(name, 16)
try:
return int(name)
except ValueError:
print(f" WARNING: unknown constant '{name}' {context}, using 0x00", file=sys.stderr)
return 0
# Connection data for outdoor maps (preserved exactly from Phase 2)
# (direction, target_const, offset)
CONNECTIONS = {
@@ -294,10 +358,13 @@ def strip_debug_blocks(text: str) -> str:
def parse_object_file(label: str, debug_warps: bool = False):
"""Parse objects/<label>.asm → (border_byte, warp_list, sign_count, sprite_count).
"""Parse objects/<label>.asm → (border_byte, warp_list, sign_count, sprites).
warp_list = [(y, x, dest_map_byte), ...] (ready to emit as db y, x, ...)
dest_map_byte: LAST_MAP→0xFF; others looked up in MAP_IDS.
warp_list = [(y, x, dest_const, warp_id), ...]
sprites = list of dicts:
{sprite_id, mapy, mapx, mov, dir,
is_trainer, trainer_class, trainer_num,
is_item, item_id}
Returns None if file not found.
"""
obj_file = MAP_OBJECTS_DIR / f"{label}.asm"
@@ -318,10 +385,52 @@ def parse_object_file(label: str, debug_warps: bool = False):
x, y, dest_const, warp_id = int(wm.group(1)), int(wm.group(2)), wm.group(3), int(wm.group(4))
warps.append((y, x, dest_const, warp_id))
sign_count = len(re.findall(r"\bbg_event\b", text))
sprite_count = len(re.findall(r"\bobject_event\b", text))
sign_count = len(re.findall(r"\bbg_event\b", text))
return border, warps, sign_count, sprite_count
# NPC object events: object_event x, y, sprite, mov, dir, text [, arg7 [, arg8]]
# Binary layout per macro: sprite_id, y+4, x+4, mov, dir, text_byte [, extra...]
# text_byte: plain text_id (std), ITEM|text_id (item, 7 args), TRAINER|text_id (trainer, 8 args)
sprites = []
for om in re.finditer(
r"\bobject_event\b\s+"
r"([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+)"
r"(?:,\s*([^,\n]+)(?:,\s*([^,\n\s]+))?)?",
text):
x_s = om.group(1).strip()
y_s = om.group(2).strip()
spr_s = om.group(3).strip()
mov_s = om.group(4).strip()
dir_s = om.group(5).strip()
# group 6 = text_const (name only — text byte = sequential index, not resolved here)
arg7 = om.group(7) # item_id (7-arg) or trainer_class (8-arg) or None
arg8 = om.group(8) # trainer_num (8-arg) or None
x = int(x_s)
y = int(y_s)
sprite_id = _resolve_const(spr_s, _SPRITE_CONSTS, f"sprite in {label}")
mov_byte = _resolve_const(mov_s, _MOV_CONSTS, f"mov in {label}")
dir_byte = _resolve_const(dir_s, _DIR_CONSTS, f"dir in {label}")
is_trainer = (arg7 is not None and arg8 is not None)
is_item = (arg7 is not None and arg8 is None)
trainer_class = _resolve_const(arg7.strip(), {}, f"trainer_class in {label}") if is_trainer else 0
trainer_num = _resolve_const(arg8.strip(), {}, f"trainer_num in {label}") if is_trainer else 0
item_id = _resolve_const(arg7.strip(), {}, f"item_id in {label}") if is_item else 0
sprites.append({
'sprite_id': sprite_id,
'mapy': y + 4,
'mapx': x + 4,
'mov': mov_byte,
'dir': dir_byte,
'is_trainer': is_trainer,
'trainer_class': trainer_class,
'trainer_num': trainer_num,
'is_item': is_item,
'item_id': item_id,
})
return border, warps, sign_count, sprites
def get_connection(direction, conn_map_id, offset,
@@ -416,7 +525,7 @@ def main():
const_to_id["LAST_MAP"] = 0xFF
# Parse all object files keyed by label
label_objects = {} # label → (border, warps, sign_count, sprite_count)
label_objects = {} # label → (border, warps, sign_count, sprites)
for f in MAP_OBJECTS_DIR.glob("*.asm"):
label = f.stem
result = parse_object_file(label, debug_warps=debug_warps)
@@ -468,9 +577,10 @@ def main():
# Get object data
obj = label_objects.get(label)
if obj:
border, raw_warps, sign_count, sprite_count = obj
border, raw_warps, sign_count, sprites = obj
else:
border, raw_warps, sign_count, sprite_count = 0, [], 0, 0
border, raw_warps, sign_count, sprites = 0, [], 0, []
sprite_count = len(sprites)
# Resolve warp dest bytes
warps_bytes = []
@@ -491,6 +601,7 @@ def main():
"warps": warps_bytes,
"sign_count": sign_count,
"sprite_count": sprite_count,
"sprites": sprites,
}
# ---- Emit output ----
@@ -695,6 +806,34 @@ def main():
hdr_lines.append(f" db {m['sprite_count']} ; sprite count")
current_addr += 1
# Per-NPC binary records (6 bytes standard, 7 bytes item, 8 bytes trainer)
for i, npc in enumerate(m.get('sprites', [])):
if npc['is_trainer']:
text_byte = _TRAINER_FLAG | i
hdr_lines.append(
f" db 0x{npc['sprite_id']:02X}, 0x{npc['mapy']:02X}, 0x{npc['mapx']:02X},"
f" 0x{npc['mov']:02X}, 0x{npc['dir']:02X}, 0x{text_byte:02X},"
f" 0x{npc['trainer_class']:02X}, 0x{npc['trainer_num']:02X}"
f" ; slot {i+1}: trainer sprite=0x{npc['sprite_id']:02X}"
)
current_addr += 8
elif npc['is_item']:
text_byte = _ITEM_FLAG | i
hdr_lines.append(
f" db 0x{npc['sprite_id']:02X}, 0x{npc['mapy']:02X}, 0x{npc['mapx']:02X},"
f" 0x{npc['mov']:02X}, 0x{npc['dir']:02X}, 0x{text_byte:02X},"
f" 0x{npc['item_id']:02X}"
f" ; slot {i+1}: item sprite=0x{npc['sprite_id']:02X}"
)
current_addr += 7
else:
hdr_lines.append(
f" db 0x{npc['sprite_id']:02X}, 0x{npc['mapy']:02X}, 0x{npc['mapx']:02X},"
f" 0x{npc['mov']:02X}, 0x{npc['dir']:02X}, 0x{i:02X}"
f" ; slot {i+1}: npc sprite=0x{npc['sprite_id']:02X} mapy={npc['mapy']} mapx={npc['mapx']}"
)
current_addr += 6
hdr_lines.append("")
return hdr_lines