diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs index a29bf76..aedded3 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs @@ -107,8 +107,24 @@ pub const CHEAPEST_PURCHASE: u32 = { /// 1 PKMN, 2 ITEM, 3 RUN". pub mod battle_entry { pub const FIGHT: u8 = 0; - pub const PKMN: u8 = 1; - pub const ITEM: u8 = 2; + /// `$01`. **The left column's second row, not the right column's first.** + /// + /// Red draws the battle menu as `FIGHT PKMN` over `ITEM RUN`, which reads as two rows -- and + /// the game's own index is two *columns*: `wCurrentMenuItem` is the row inside the column the + /// cursor is in and selection adds two for the right one. So the order is FIGHT, `ITEM`, + /// `PKMN`, RUN, and this pair was the other way round for as long as the four constants have + /// existed. + /// + /// **Measured on the cartridge** (2026-09-22, `infra/docs/macros-traps.md`): a `THROW BALL` + /// aiming at 2 walked the cursor to `wTopMenuItemX` 15, `wCurrentMenuItem` 0, pressed A, and + /// the **party list** opened -- `wTopMenuItemY` 1, `wTopMenuItemX` 0, `wListMenuID` `$02`. + /// The frame after the press the game wrote `wCurrentMenuItem` 2, which is the right column's + /// first row plus two, and the right column's first row is PKMN. So `THROW BALL` and `ITEM` + /// opened the party list and `SWITCH` opened the bag, every single time: `THROW BALL` was 63 + /// starts and 63 `blocked` on v0.4.3, and `SWITCH` 15 of them. + pub const ITEM: u8 = 1; + /// `$02`. The right column's first row: see [`ITEM`]. + pub const PKMN: u8 = 2; pub const RUN: u8 = 3; } diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs index 12b3e1e..874180d 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs @@ -448,12 +448,16 @@ impl World { } let here = i16::from(self.cursor); let target = if self.grid { - // Two rows of two: up and down change row, left and right change column. + // Red's battle menu: two **columns** of two, indexed by column. `wCurrentMenuItem` is + // the row inside the column the cursor is in and selection adds two for the right + // column, so the order is FIGHT, ITEM, PKMN, RUN -- up and down move one inside a + // column, left and right move two across (surveyed 2026-09-22; the fake had it + // row-major, which is the same mistake `battle_entry` had). match facing { - Facing::Down => here + 2, - Facing::Up => here - 2, - Facing::Right if here % 2 == 0 => here + 1, - Facing::Left if here % 2 == 1 => here - 1, + Facing::Down if here % 2 == 0 => here + 1, + Facing::Up if here % 2 == 1 => here - 1, + Facing::Right => here + 2, + Facing::Left => here - 2, _ => here, } } else { @@ -4378,6 +4382,40 @@ fn the_move_list_deals_back_only_where_the_moves_can_be_read() { assert_eq!(world.pulses.last(), Some(&buttons::A)); } +/// Section 12.11: Red's battle menu is two columns, so its order is FIGHT, ITEM, PKMN, RUN. +/// +/// The screen reads `FIGHT PKMN` over `ITEM RUN` and the game's own index does not: it is the row +/// inside the column the cursor is in, plus two for the right column. `battle_entry` had `PKMN` 1 +/// and `ITEM` 2, which is the row-major reading of the picture, so **every macro that opened the +/// bag opened the party list and every macro that opened the party list opened the bag**. +/// +/// **Surveyed on the cartridge** (`infra/docs/macros-traps.md`): a `THROW BALL` aiming at 2 walked +/// the cursor to `wTopMenuItemX` 15 / `wCurrentMenuItem` 0, pressed A, and the party list opened -- +/// `wTopMenuItemY` 1, `wTopMenuItemX` 0, `wListMenuID` `$02` -- with the game writing +/// `wCurrentMenuItem` 2 on the frame after the press. On v0.4.3 `THROW BALL` was 63 starts and 63 +/// `blocked`, and `SWITCH` 15 of them: never the macro's own list, always the other one. +#[test] +fn the_battle_menus_two_columns_put_item_under_fight_and_pkmn_beside_it() { + use super::cartridge::battle_entry; + assert_eq!( + [battle_entry::FIGHT, battle_entry::ITEM, battle_entry::PKMN, battle_entry::RUN], + [0, 1, 2, 3], + "the left column is FIGHT then ITEM, the right is PKMN then RUN" + ); + // And the seam reads the same order back off the fake's own geometry: DOWN moves one inside a + // column, RIGHT moves two across. + let mut world = World::battle(); + let menu = |world: &mut World| super::palette::battle_menu(world); + assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::FIGHT }); + world.on_pulse(buttons::DOWN); + assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::ITEM }); + world.on_pulse(buttons::UP); + world.on_pulse(buttons::RIGHT); + assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::PKMN }); + world.on_pulse(buttons::DOWN); + assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::RUN }); +} + /// Section 12.11: a cursor step waits for the list it was built for. /// /// **Measured on the cartridge, v0.4.3** (`infra/docs/macros-traps.md`): `THROW BALL` was **63 diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs index e409d76..da6167e 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs @@ -406,8 +406,14 @@ pub fn battle(memory: &mut dyn MemoryReader) -> Option { && (left || right); let menu = if main { - // FIGHT and PKMN are the left column, ITEM and RUN the right; the cursor index the game - // keeps is within the column, and `.rightColumn` adds two to it on selection. + // **FIGHT and ITEM are the left column, PKMN and RUN the right.** The screen reads + // `FIGHT PKMN` over `ITEM RUN` and the game's index is by column: `wCurrentMenuItem` is + // the row inside the column the cursor is in, and `.rightColumn` adds two to it on + // selection -- so the order is FIGHT, ITEM, PKMN, RUN. Surveyed on the cartridge + // 2026-09-22 (`infra/docs/macros-traps.md`): A at `wTopMenuItemX` 15 with + // `wCurrentMenuItem` 0 opens the **party** list and the game then writes + // `wCurrentMenuItem` 2. `macros::cartridge::battle_entry` had this pair the other way + // round, so `ITEM` and `THROW BALL` opened the party list and `SWITCH` opened the bag. let column = if right { 2 } else { 0 }; BattleMenu::Main { cursor: column + cursor.current.min(1) } } else if cursor.top_y == 12 && cursor.top_x == 5 {