Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
fb140bc
Plan: battle items VWF rendering
manz May 16, 2026
b190181
Wire BATTLE_ITEMS_VWF scaffolding (flag off pending region-local clear)
manz May 16, 2026
2851b46
Flip BATTLE_ITEMS_VWF on with no-clear init for inventory region
manz May 16, 2026
e0fe510
Extend phase-1 DMA to cover the inventory region
manz May 16, 2026
45d13be
Reset VWF allocator once per inventory pass, not per item
manz May 16, 2026
bf78cfc
Move battle_render state past inventory tiles + deinit at pass end
manz May 16, 2026
1dfd118
Add inventory-open dump + frame-diff diagnostic
manz May 16, 2026
14085ae
Per-slot inventory allocator reset via init_inventory_for_current_slot
manz May 16, 2026
1e52f6b
Add WRAM/buffer SRAM dump for inventory diagnosis
manz May 16, 2026
e43362d
Save A across set_vwf_render to preserve tile_id base
manz May 16, 2026
649401e
Clear per-slot CHR slice before VWF draw
manz May 16, 2026
b71425c
Revert stz.l (no long absolute STZ in 65816) + re-record goldens
manz May 16, 2026
83655d6
Escape colon as fixed tile (0x03 0xC8) so it bypasses VWF allocator
manz May 16, 2026
82d194e
Bump per-slot tile_id budget 9 -> 10 so names like Collure fit
manz May 16, 2026
59c0f95
Escape spaces inside item names so they bypass the VWF allocator
manz May 16, 2026
ac44766
Own the inventory text renderer in bank-20 (messages_vwf.draw_invento…
manz May 16, 2026
6b1e641
Add 0x0F toggle escape + format reshape
manz May 16, 2026
9ce79e6
Inventory items render cleanly: 0x0F toggle + drop spurious ora 0x01
manz May 16, 2026
89f202f
Inventory items render cleanly: actually commit the source
manz May 16, 2026
f23da07
Route inventory rolling through messages_vwf.draw_inventory_text
manz May 16, 2026
f9e4cd1
Apply 0x0F toggle to all three inventory format builders
manz May 16, 2026
a0c9264
Actually commit the toggle-format edits in inventory_rolling.s
manz May 16, 2026
ae27079
Route via < 0x42 char threshold instead of an explicit toggle escape
manz May 16, 2026
98f9ed6
Tighten char-range threshold + treat 0x0F as no-op skip
manz May 16, 2026
9aa7546
Add 0xFE / 0xFD / 0xFC escape codes for explicit VWF control + pad
manz May 16, 2026
7be4dc6
Single 0xFE toggle + 0xFC NN goto + pre-clear slot tilemap to spaces
manz May 16, 2026
5c94ba5
Clear VWF battle_flag at end of draw_inventory_text
manz May 16, 2026
a53170e
Fix battle inventory rolling-buffer scroll, qty, DMA
manz May 16, 2026
3ee7e2b
Pin inventory bottom border via per-NMI HDMA footer override
manz May 17, 2026
f9e6603
Restore vanilla footer scroll on inv close + shift items 1 tile right
manz May 17, 2026
b85533c
Add per-slot CHR dirty bitmask + multi-frame partial DMA for inv
manz May 17, 2026
4f5bbb7
Wire battle inventory through items_unleashed (17-byte stride)
manz May 17, 2026
12fc5d3
Fix flying HDMA freeze when battle inventory open
manz May 17, 2026
d230d4d
Move inv VWF fixed-mode flag off DP $37 (button shadow)
manz May 17, 2026
35cfcb6
Mirror BG3 main->cmd region via WRAM DMA + propagate monster dirty
manz May 17, 2026
cb8bb73
Swap mirror_main_to_cmd from WRAM DMA to MVN, drop CMD_DIRTY gate
manz May 17, 2026
7d3a1ee
Resolve active char_id to slot before stamping name palette
manz May 18, 2026
c14e0ef
Honor CharOrderTbl when mapping active slot to highlight row
manz May 18, 2026
1d7e56e
Wire field/drops/treasure menus through assets_items_unleashed_dat
manz May 18, 2026
fafa58c
Wire key-item picker through assets_items_unleashed_dat
manz May 18, 2026
8646488
Resize small_vwf CHR + vram-save buffers for field-item VWF port
manz May 18, 2026
680a056
Hook DrawItemName / DrawEquipItemName through bank-20 helper
manz May 18, 2026
d413637
WIP: route field DrawItemName through small_vwf (broken slot index)
manz May 18, 2026
f82cb22
Add src/vwf_state.i: shared VWF SRAM addresses + config-struct scaffold
manz May 18, 2026
f036b2a
Point battle_render + small_vwf buffer_ptr at shared VWF_CHR_BUFFER
manz May 18, 2026
20f44db
Add small_vwf.render.draw_text_buffer entry for VWF_TEXT_BUFFER
manz May 18, 2026
2c89da8
WIP: route field DrawItemName through render_with_config + VWF_TEXT_B…
manz May 18, 2026
e5f398d
Move VWF_CONFIG / VWF_TEXT_BUFFER inside SRAM ($70:0000..$70:7FFF)
manz May 18, 2026
23a0419
Bump FIELD_ITEM_VWF_TILE_BUDGET from 5 to 10
manz May 18, 2026
b60b123
Flush VWF CHR to VRAM at end of field item-name render
manz May 18, 2026
2eb133c
Add render.flush_chr_to_vram + capture fresh-open kss
manz May 18, 2026
8d87b46
Move VWF CHR flush to NMI hook + drive VRAM dest from VwfConfig
manz May 18, 2026
3f7c1e7
Revert NMI tail-call to render.flush_chr_to_vram (channel-7 clash)
manz May 18, 2026
7e5de47
Wire VWF CHR flush to NMI on DMA channel 6 + fix stack mismatch
manz May 18, 2026
052617f
Re-aim field VWF CHR flush at BG3 CHR base $4000 (was $A000)
manz May 18, 2026
f066b30
Document field-menu VWF regions in src/items.i
manz May 18, 2026
fb3c3b7
Fix VWF text-buffer copy + add per-slot CHR clear
manz May 18, 2026
98a4882
Use 16-bit allocator + word tile_id_base in VwfConfig
manz May 18, 2026
a0b2d1f
Replace inline VRAM target with named FIELD_VWF_VRAM_DEST_WORD
manz May 18, 2026
318ca4f
Fix tilemap-base Y masking so slots 2..10 stop stomping slot 0
manz May 18, 2026
029230c
Move VWF region to tile_id $100+ + fix 9-bit CHR address calc
manz May 18, 2026
55d0de4
Bump chr_byte_count so high slots' CHR reaches VRAM
manz May 18, 2026
a31d4bd
Restore caller Y across draw_field_item_name so qty column lands
manz May 18, 2026
aa11373
Fix stack imbalance in render.init that BRKed item description path
manz May 18, 2026
189c181
Split VWF CHR regions so descriptions + field items stop fighting
manz May 18, 2026
72ddff4
Restore M=8 before draw_string char loop + document VRAM save range
manz May 18, 2026
c005410
Document field / treasure / drops shared VWF tile_id region
manz May 18, 2026
85260b8
Preserve Y across render.init in item-description draw
manz May 18, 2026
b3ce6cb
Drop ff4-battle-ext.kss dup, point refs at ff4-battle.kss
manz May 18, 2026
adea7af
Add rolling inventory engine design doc
manz May 18, 2026
29d4542
Trim VwfConfig: drop unused font_ptr + kerning_ptr fields
manz May 18, 2026
4dc7eeb
Split drops VWF tile range from treasure inventory to kill clobber
manz May 18, 2026
0ee117d
Document accurate drops CHR range + flush trade-off
manz May 18, 2026
c534939
Add two-descriptor VWF flush so drops + treasure each own their slot
manz May 18, 2026
9b6299c
Pre-blank bottom-row tilemap in items_menu_vwf to drop stale tile refs
manz May 18, 2026
25a065d
Route item description through the secondary VWF flush descriptor
manz May 18, 2026
987d412
Zero VWF secondary-descriptor SRAM at boot, fix item-desc src offset
manz May 18, 2026
959c7c9
Defend secondary VWF flush against stale-savestate SRAM
manz May 18, 2026
b6dc24a
Bump primary VWF flush to cover field-items 11-slot window
manz May 18, 2026
7cfc6da
Whitelist secondary VWF flush vram_word to the two armed targets
manz May 18, 2026
e5b7226
Drive field-inventory tests through a fresh menu-open so VRAM is live
manz May 18, 2026
efc6044
Clamp display_char bits_left_on_tile so kerning overshoot stops BRKing
manz May 18, 2026
ecffc47
Stop save-screen sprite mayhem: shrink $01:9060 patch to 4 bytes
manz May 18, 2026
50176cd
Arm treasure HDMA channel 6 immediately at popup open
manz May 18, 2026
54fb68f
Save/restore HDMAEN bit 6 around VWF flush so HDMA stops hijacking it
manz May 18, 2026
1eba71d
Reset prev_char + bits_left_on_tile at render_with_config entry
manz May 18, 2026
41a28d4
Add ff4-before-mayhem2.kss fixture for treasure-popup first-render bug
manz May 18, 2026
f868889
Debounce treasure held-DOWN auto-repeat with a 24-frame cooldown
manz May 18, 2026
03bd1c9
Phase 1: rolling-inventory engine skeleton in bank-20
manz May 18, 2026
79d093f
Phase 1.1: implement engine cursor + dirty-mask entries with unit tests
manz May 18, 2026
77613d6
Phase 1.2: implement rolling_engine_init + rolling_engine_shutdown
manz May 18, 2026
e179819
Phase 1.3: implement rolling_engine_tick with FSM tests
manz May 18, 2026
3920713
Phase 1.4: implement rolling_engine_vblank_flush
manz May 18, 2026
287cc0c
Phase 2.0: extend RollingBufferState with hook far-ptrs + integration…
manz May 18, 2026
ee539c5
Phase 2.1: arm field-menu engine config + hook far-ptrs at init
manz May 18, 2026
33bf892
Phase 2.2: rolling_engine_init JSLs through fn_draw_window hook
manz May 18, 2026
7280736
Phase 2.3: per-slot render loop + ensure-HDMA dispatch in engine_init
manz May 18, 2026
287a6dd
Document phase 2.4 rollback path in init_menu_rolling_buffer_impl
manz May 18, 2026
c75cb02
Document phase 2.4 deferral: NMI HDMAEN read bug under engine init
manz May 18, 2026
630998b
Phase 2.4: fix engine_init swap RTS->RTL stack imbalance
manz May 18, 2026
b7c90e6
Revert phase 2.4 engine_init swap on field menu
manz May 18, 2026
05897ce
Fix engine_call_hook DP-relative stash clobbering BG1HOFS
manz May 19, 2026
ed1eba2
Phase 2.4: route field-menu init through rolling_engine_init
manz May 19, 2026
8e8aa4f
Route shop owner messages through small-VWF description region
manz May 19, 2026
f4466b1
Extend menu VRAM save range to cover the VWF region union
manz May 19, 2026
3f6e256
Zero VWF VRAM regions once at boot
manz May 19, 2026
d2aecfc
Relocate treasure_rolling to $7E:9C00 and port init to engine
manz May 19, 2026
047b1a3
Relocate drops_rolling to $7E:9C30 and port init to engine
manz May 19, 2026
2279ef1
Relocate key_item_rolling to $7E:9C60 and port init to engine
manz May 19, 2026
cbe05f3
Delete engine_init_rolling_buffer macro
manz May 19, 2026
dc85cda
Port engine_refresh_slots macro to engine entry
manz May 19, 2026
d08aacc
Port engine_update_scroll_frame macro to engine entry
manz May 19, 2026
198c8d4
Delete dead engine_scroll_down_prepare + engine_scroll_up_prepare
manz May 19, 2026
b7d2bd4
Port engine_start_scroll_down + engine_start_scroll_up to engine
manz May 19, 2026
cab2569
Port engine_finish_scroll to engine
manz May 19, 2026
319ade3
Port engine_swap_redraw to engine
manz May 19, 2026
e75f58d
Inline engine_update_scroll_hdma at each call site, drop the macro
manz May 19, 2026
a5b4f85
Move treasure_scroll_cooldown past the engine-extended struct
manz May 19, 2026
25f1049
Document picker VRAM hazard, leave hijack unwired
manz May 19, 2026
fde382a
Add menu rewrite plan (FF6-derived architecture target)
manz May 19, 2026
eecb47b
Migrate to module-owns-placement (a816 HEAD)
manz May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def build_patch(input, output, lang):
symbols={"LANG": lang},
include_paths=[Path("src")],
prelude_file=Path("config.i"),
overlap_mode="warn",
)

if result.exit_code != 0:
Expand Down Expand Up @@ -165,7 +166,9 @@ def build_fixed_asset(table, input_file, binary_text_file):
write_pointers_value_as_binary(pointers, binary_text_file)


def build_fixed_to_ptr_asset(table, input_file, binary_text_file, pointers_file, buffer_width=None):
def build_fixed_to_ptr_asset(
table, input_file, binary_text_file, pointers_file, buffer_width=None
):
pointers = read_fixed_from_xml(
input_file, table, formatter=lambda t: t.strip() + "[end]"
)
Expand All @@ -184,7 +187,7 @@ def build_fixed_to_ptr_asset(table, input_file, binary_text_file, pointers_file,
print(f"{text} is too long ({ptr_len}px , {math.ceil(ptr_len / 8)} tiles)")

text = table.to_text(max_ptr[1].value)
print(f"{text} is the largest ({max_length}px ({math.ceil(max_length/8)})")
print(f"{text} is the largest ({max_length}px ({math.ceil(max_length / 8)})")
write_pointers_value_as_binary(pointers, binary_text_file)

write_pointers_addresses_as_binary(
Expand Down Expand Up @@ -327,8 +330,7 @@ def add_custom_kernings(text: str, advance: int) -> None:
known_pairs_to_kern = [
"Ya",
"Pa",
"Po"
"Fa",
"PoFa",
"Fe",
"Fo",
"Fu",
Expand Down Expand Up @@ -489,6 +491,12 @@ def build_assets(assets):
menu_table,
),
("fixed", menu_table, os.path.join(text_root, "items.xml"), "assets/items.dat"),
(
"fixed",
menu_table,
os.path.join(text_root, "items_unleashed.xml"),
"assets/items_unleashed.dat",
),
("fixed", menu_table, os.path.join(text_root, "magic.xml"), "assets/magic.dat"),
(
"fixed",
Expand Down Expand Up @@ -528,7 +536,7 @@ def build_assets(assets):
os.path.join(text_root, "monsters_long.xml"),
"assets/monsters_long.dat",
"assets/monsters_long.ptr",
80
80,
),
(
"nullterminated",
Expand Down
1 change: 1 addition & 0 deletions config.i
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ENABLE_BUTTON_DISPLAY := 1
BATTLE_CMD_VWF := 1
BATTLE_NAMES_VWF := 1
BATTLE_MONSTERS_VWF := 1
BATTLE_ITEMS_VWF := 1
INVENTORY_ROLLING_BUFFER := 1
TREASURE_INVENTORY_ROLLING := 1 ; WIP, treasure rolling-buffer scaffolding only
TREASURE_DEBUG_ALWAYS_DROP := 1 ; force every win to drop an item (testing)
Expand Down
121 changes: 95 additions & 26 deletions ff4.s
Original file line number Diff line number Diff line change
Expand Up @@ -53,42 +53,48 @@ conditional_bg1_vofs := 0x208000
dialog_bank_ptr_base = 0x218000


*=0xFFC0
.alloc at 0x00FFC0 {
; patch snes cartridge type
; original PCB: SHVC-1A3B
; target PCB: SHVC-1A5B
; original PCB: SHVC-1A3B ; target PCB: SHVC-1A5B
.ascii "Final Fantasy IV "
}

;FFD5 20H / 30H Map Mode

*=0xFFD6
.alloc at 0x00FFD6 {
; FFD5 20H / 30H Map Mode
.db 0x02 ; Cartridge Type
.db 0x0B ; ~ 0BH ROM Size
.db 0x07 ; RAM Size
}

.if ENABLE_BRK_HANDLER {
*=0x00FFE0
; JML trampoline in vector-table padding ; native/emu BRK vectors point here.
jmp.l brk_handler
*=0x00FFE6
.dw 0xFFE0
*=0x00FFFE
.dw 0xFFE0
; JML trampoline in vector-table padding; native/emu BRK vectors point here.
.alloc at 0x00FFE0 {
jmp.l brk_handler
}
.alloc at 0x00FFE6 {
.dw 0xFFE0
}
.alloc at 0x00FFFE {
.dw 0xFFE0
}
}


*=0x008031
; déroutage pour ajouter le splash screen
.if ENABLE_INTRO {
jsr.l start_splash_screen
} else {
jsr.l clear_ram
; déroutage pour ajouter le splash screen
.alloc at 0x008031 {
.if ENABLE_INTRO {
jsr.l start_splash_screen
} else {
jsr.l clear_ram
}
}


*=0x00B463
; déroutage pour utiliser la vwf dans les dialogues.
; déroutage pour utiliser la vwf dans les dialogues.
.alloc at 0x00B463 {
jsr.l vwfstart
rts
}

; ============================================================================
; Bank-20 relocated region.
Expand Down Expand Up @@ -129,8 +135,14 @@ _cond_skip_bg1vofs:

clear_ram:
"""
Clear the dialog VWF tile buffer at $702000-$706FFF (16-bit zeroes) after letting the boot ROM init at
$15C9AA.
Clear the dialog VWF tile buffer + engine scratch at $702000-$7070FF
(includes VWF_CONFIG_BASE, VWF_CHR_DIRTY / DIRTY_B, VWF_CALLER_CTX, and
the secondary descriptor fields) after letting the boot ROM init at
$15C9AA. Range was $5000 bytes pre-secondary-descriptor ; bumped to
$5100 so the new dirty / vram_word / byte_count / src_offset bytes
land zero on cold boot instead of inheriting random SRAM and
triggering a bogus secondary flush on the very first NMI (which trashed
the save-selection sprite CHR).
"""


Expand All @@ -142,7 +154,7 @@ $15C9AA.
_loop:
sta.l 0x702000, x
inx
cpx.w #0x5000
cpx.w #0x5100
bne _loop
}
rtl
Expand All @@ -167,6 +179,30 @@ Output: X = offset into ItemName table.
rtl


multiply_item_index_17:
"""
Relocated multiply-by-ITEM_UNLEASHED_RECORD_SIZE for the items_unleashed
name offset. Called from $019023 via JSL when the field menu is
wired to the 17-byte assets_items_unleashed_dat table.
Input: $43 = item ID (16-bit mode active).
Output: X = offset into ItemName table.
"""


; ITEM_UNLEASHED_RECORD_SIZE = 17 = (id << 4) + id.
lda 0x43
pha
asl
asl
asl
asl ; * 16
clc
adc 0x01, s ; * 16 + id = * 17
tax
pla ; balance stack
rtl


multiply_by_12:
"""A: value to multiply ; returns A*12 in A."""
php
Expand All @@ -184,6 +220,29 @@ multiply_by_12:
rtl


multiply_by_17:
"""
A: value to multiply ; returns A*17 in A. Mirror of multiply_by_12
sized for the 17-byte assets_items_unleashed_dat stride.
"""


php
rep #0x20
and.w #0x00FF
pha
asl
asl
asl
asl
clc
adc 0x01, s ; * 16 + value = * 17
sta 0x01, s
pla
plp
rtl


brk_handler:
"""
BRK trap: mask interrupts, disable NMI, fetch the BRK signature byte
Expand Down Expand Up @@ -223,7 +282,6 @@ signature byte sits at PB:(PC - 1).
; and matches the legacy `*=0x208000` chain so .import modules without
; their own `*=` directive land in bank-20 as expected.

*=0x208100
; --- Imported modules ---------------------------------------------------

.import "libmz"
Expand Down Expand Up @@ -253,6 +311,7 @@ signature byte sits at PB:(PC - 1).
}

.import "ingame/places_names_window"
.import "ingame/items_menu_vwf"
.import "menus/system_menus_text"
.import "dakuten"
.import "menus/start_screen_text"
Expand All @@ -265,6 +324,7 @@ signature byte sits at PB:(PC - 1).
.if INVENTORY_ROLLING_BUFFER {
.import "ingame/init_bg_scroll_hdma"
.include "src/ingame/inventory_rolling.s"
.include "src/lib/rolling_inventory_engine.s"
}

.if TREASURE_INVENTORY_ROLLING {
Expand All @@ -275,6 +335,7 @@ signature byte sits at PB:(PC - 1).

; --- Binary text assets -------------------------------------------------


.incbin "assets/attack_names.ptr"
.incbin "assets/attack_names.dat"
.incbin "assets/monsters_long.ptr"
Expand Down Expand Up @@ -310,4 +371,12 @@ signature byte sits at PB:(PC - 1).
stx 0xb3
jmp.w 0xED96
}
;end

; Park the 17-byte-stride items_unleashed.dat in an empty bank so the
; full 4352-byte table fits without crossing a LoROM bank boundary
; (which would otherwise leave the upper half of the table at
; $21:0xxx, an address LoROM does not map back to ROM data).

.alloc at 0x238000 {
.incbin "assets/items_unleashed.dat"
}
117 changes: 117 additions & 0 deletions plans/battle_items_vwf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Battle items VWF

## What

Replace the fixed-width 12-character item-name rendering in the battle
inventory with the existing 8x8 VWF blitter (`battle_render.display_char`).
Lets proportionally-spaced French names fit the 15-tile slot budget that
the rolling inventory already reserves, removes hard truncation, frees
the per-glyph padding wasted on narrow characters.

## Why

Vanilla `DrawInventoryItemText` packs item names as 12 fixed tile IDs
into a 48-byte buffer. Our rolling inventory expanded to 60 bytes (15
tiles per row × 2 bytes), but the renderer still emits one tile-id per
character, so a 12-character French name like `Médaille d'Or` either
overflows or truncates. VWF rendering pulls the same name through
`battle_render.display_char`, allocating CHR dynamically and packing
proportional glyphs into 15 tiles of horizontal pixels (~120 px).

## Boundaries

- In scope: battle inventory (the in-battle item menu).
- In scope: the rolling-buffer slot refresh hook (re-render on
scroll edge).
- Out of scope: equipped-items buffer ($9A00). Equip menu rendering
stays fixed-width for this PR; can follow up.
- Out of scope: item descriptions (already VWF via small_vwf).
- Out of scope: field inventory VWF. Same approach, different surface.

## Approach

1. **Reserve a battle_render region for inventory**. Current regions
(`src/battle/message.s:143-150`):
```
0x00-0x3F messages
0x40-0x7F monster names
0x80-0xAF char names
0xB0-0xEF commands
```
Add `0xF0-0xFF` (16 tiles = 512B CHR) for inventory item name. With
6 ring slots and 15 tiles per name, the ring needs ~90 tiles. Either
extend the CHR window past $BDE0 (free runway up to roughly
$BFE0-ish per the audit) or reuse the commands region during the
item-menu mode (commands hide when inventory is open).
2. **Patch `DrawInventoryItemText`** at `$02:9FA4`. Replace the
per-character tile-id copy with a `battle_render.init_inventory`
+ per-byte `display_char` loop, then write the resulting tile IDs
into the existing 60-byte slot at `$97A6 + slot * 60`.
3. **Quantity digits**. Stay fixed-width for now (the rightmost 2
tiles per row in the slot layout). Avoids a special-case digit
path in display_char.
4. **Trash icon**. Existing 2x2 fixed glyph stays. Item id $FF skips
the VWF render path.
5. **Tilemap layout unchanged**. The slot still consumes 30 tiles
(15 × 2 rows), still gets DMA'd to VRAM tilemap by the existing
rolling-buffer transfer. Only the CHR pointed at by the tile IDs
changes.

## Files

- `src/battle/items_patches.s` — hook `DrawInventoryItemText` text
copy loop.
- `src/battle/message.s` — add inventory region constants +
`init_inventory` entry.
- `src/battle/inventory_rolling.s` — slot-refresh callsite. Already
pre-renders on swap / scroll; VWF render lands at the same site.
- `tests/test_battle_init.py` — extend with an "open battle
inventory" frame that captures the VWF rendered names.

## Tests

- Battle init smoke unchanged (no item menu open).
- New `tests/test_battle_inventory_vwf.py`: load a battle-with-items
kss, open the item menu, capture screen golden showing
VWF-rendered names.
- `tests/_profile/profile_battle_vwf.py` exercised with the inventory
open; confirm `_display_char` calls fit within budget on edge
frames (re-render is one slot, ~15 chars × 4.4k cy = 66k cy ≈ 18%
of frame).

## Risks

- **CHR overflow**: 90 tiles for the ring needs $A000-base allocation
past current region. Audit the $BDE0-? runway carefully before
expanding.
- **Tile-id collision with commands**: if commands region 0xB0-0xEF
is the chosen reuse target, items render must wait for cmd window
to be hidden. The existing battle inventory open path already
hides the cmd window via the slice-2 gates, but verify.
- **Edge-slot render time**: VWF blit of one item name on scroll
edge must fit in ~1 vblank. Item names are ~12-15 chars × 4.4k cy
= 53-66k cy ≈ 15-18% of frame budget. Headroom OK.
- **dakuten/special chars**: item names with accents flow through
`battle_display_dakuten_char`. Already handled by the existing VWF
path. Verify on `Médaille` etc.

## Phasing

| phase | scope | size |
|---|---|---|
| 1 | Region reservation + region-init helper | ~50 lines asm |
| 2 | `DrawInventoryItemText` VWF hook + slot write | ~150 lines asm |
| 3 | Rolling-buffer slot-refresh wiring | ~80 lines asm |
| 4 | Goldens + smoke test for battle inventory | tests |
| 5 | Polish: quantity-digit alignment, trash icon | small |

Each phase commits independently. PR can land as either one chunk or
phase-by-phase depending on review cadence.

## Crack

The 90-tile CHR budget is the load-bearing assumption. If the
$A000-$BFFF window proves too tight, fall back to reusing commands
region 0xB0-0xEF during item-menu mode and round-trip the cmd window
on close. Adds a tilemap clear on transition but keeps the CHR
footprint where it is.
Loading
Loading