a two-option box is the one the cartridge drew, not the one row 41 pinned
Row 41 read the border at (11, 6)-(19, 11) because that is where the Pokemon Center's script puts it, and named the limit in its own residual: "Red places a two-option menu where the script asking for it says, so a prompt drawn elsewhere reads false and keeps the pad it had". The Pewter Gym guide draws the same menu at (14, 7)-(19, 11) with the cursor at column 15. Surveyed over 260 presses from the live checkpoint: the box is drawn on 10 frames, `yes_no_prompt` answered false on all 260, and `wTextBoxID` read `TWO_OPTION_MENU` on exactly the 10. So the pad was `NEXT, YES, NO` on a box that was a choice -- two channels for one A press, which is 12.10's forbidden pair -- and the reopened-prompt exclusion never armed, because it only judges an answer to a prompt this crate can read. The screen half is now the border drawn **around the cursor the game parked in it**. One fact about `DisplayTwoOptionMenu` rather than about any script: the cursor goes in the box's first interior column, so the left edge is one column to its left, in both boxes surveyed. The top is not fixed -- the nurse's box begins two rows above the first item and the guide's one -- so the top is found and the figure is then read whole, as `waiting` and the move list are.
This commit is contained in:
parent
a6a8f797ff
commit
5f9237efcd
3 changed files with 173 additions and 16 deletions
|
|
@ -238,6 +238,29 @@ impl Wram {
|
||||||
self.set(ram::wFontLoaded, poke::BIT_FONT_LOADED).draw_box(0, 12, 19, 17)
|
self.set(ram::wFontLoaded, poke::BIT_FONT_LOADED).draw_box(0, 12, 19, 17)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The two-option YES/NO box where a Pokémon Center's script draws it (row 41): the box at
|
||||||
|
/// (11, 6)-(19, 11) over the dialogue box, with the cursor parked in its first interior column.
|
||||||
|
pub fn yes_no_prompt(&mut self) -> &mut Self {
|
||||||
|
let (left, top, right, bottom) = poke::YES_NO_BOX;
|
||||||
|
self.dialogue_box().draw_box(left, top, right, bottom).yes_no_cursor(poke::YES_NO_CURSOR_X)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same menu where the **Pewter Gym guide's** script draws it (row 56): (14, 7)-(19, 11),
|
||||||
|
/// three columns over and one row shorter, cursor at column 15. This is the box the pinned
|
||||||
|
/// reading could not see, and the reason every frame of his conversation read as plain text.
|
||||||
|
pub fn gym_yes_no_prompt(&mut self) -> &mut Self {
|
||||||
|
let (left, top, right, bottom) = poke::GYM_GUIDE_YES_NO_BOX;
|
||||||
|
self.dialogue_box()
|
||||||
|
.draw_box(left, top, right, bottom)
|
||||||
|
.yes_no_cursor(poke::GYM_GUIDE_YES_NO_CURSOR_X)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cursor bytes `DisplayTwoOptionMenu` parks and **nothing clears**, with no box drawn:
|
||||||
|
/// what every other frame of the conversation reads back (rows 41 and 56).
|
||||||
|
pub fn yes_no_cursor(&mut self, column: u8) -> &mut Self {
|
||||||
|
self.cursor(poke::YES_NO_CURSOR_Y, column, 0, 1, poke::pad::A | poke::pad::B)
|
||||||
|
}
|
||||||
|
|
||||||
/// The start menu, Pokédex entry included.
|
/// The start menu, Pokédex entry included.
|
||||||
pub fn start_menu(&mut self) -> &mut Self {
|
pub fn start_menu(&mut self) -> &mut Self {
|
||||||
self.set(ram::wFontLoaded, poke::BIT_FONT_LOADED)
|
self.set(ram::wFontLoaded, poke::BIT_FONT_LOADED)
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,24 @@ pub mod poke {
|
||||||
pub const YES_NO_CURSOR_Y: u8 = 8;
|
pub const YES_NO_CURSOR_Y: u8 = 8;
|
||||||
pub const YES_NO_CURSOR_X: u8 = 12;
|
pub const YES_NO_CURSOR_X: u8 = 12;
|
||||||
|
|
||||||
|
/// The Pewter Gym guide's two-option box, surveyed the same way (row 56): a *second* place the
|
||||||
|
/// same routine draws the same menu, which is what took the pinned rectangle above off its
|
||||||
|
/// pedestal.
|
||||||
|
pub const GYM_GUIDE_YES_NO_BOX: (u16, u16, u16, u16) = (14, 7, 19, 11);
|
||||||
|
pub const GYM_GUIDE_YES_NO_CURSOR_X: u8 = 15;
|
||||||
|
|
||||||
|
/// How far above the first item the two-option box's top edge is looked for (row 56).
|
||||||
|
///
|
||||||
|
/// `DisplayTwoOptionMenu` puts the cursor in the box's first interior *column*, so the left
|
||||||
|
/// edge is one column left of `wTopMenuItemX` in both boxes surveyed. The *top* is not fixed:
|
||||||
|
/// the nurse's is two rows above the first item and the gym guide's is one, because one menu
|
||||||
|
/// carries a caption line and the other does not. So the top is found rather than computed,
|
||||||
|
/// looking up at most this many rows for the border's own corner.
|
||||||
|
pub const TWO_OPTION_CAPTION_ROWS: u16 = 3;
|
||||||
|
/// And how far below the first item the bottom edge is looked for: two options and the border.
|
||||||
|
/// Both surveyed boxes end three rows below the first item.
|
||||||
|
pub const TWO_OPTION_BOX_ROWS: u16 = 4;
|
||||||
|
|
||||||
/// The move list's own box, and the junction tile in its top edge
|
/// The move list's own box, and the junction tile in its top edge
|
||||||
/// (`infra/docs/macros-traps.md`, row 50).
|
/// (`infra/docs/macros-traps.md`, row 50).
|
||||||
///
|
///
|
||||||
|
|
@ -594,30 +612,71 @@ pub fn text_box(memory: &mut dyn MemoryReader) -> TextBox {
|
||||||
///
|
///
|
||||||
/// `docs/design/macros-wram.md` says there is no "a choice is open" flag, and there is not -- so
|
/// `docs/design/macros-wram.md` says there is no "a choice is open" flag, and there is not -- so
|
||||||
/// this is the same construction [`text_box`] makes for `waiting`: a WRAM flag plus the figure the
|
/// this is the same construction [`text_box`] makes for `waiting`: a WRAM flag plus the figure the
|
||||||
/// game draws. `DisplayTwoOptionMenu` draws its own little box in the top right and parks the
|
/// game draws. **Both halves are needed**: the cursor bytes are not cleared when the box closes,
|
||||||
/// shared cursor inside it, and **both halves are needed**: the cursor bytes are not cleared when
|
/// so at the rung-10 Pokemon Center every one of the nurse's forty-six text frames reads
|
||||||
/// the box closes, so at the rung-10 checkpoint every one of the nurse's forty-six text frames
|
/// `wTopMenuItemY` 8, `wTopMenuItemX` 12, `wMaxMenuItem` 1 and `wMenuWatchedKeys` `$03` while the
|
||||||
/// reads `wTopMenuItemY` 8, `wTopMenuItemX` 12, `wMaxMenuItem` 1 and `wMenuWatchedKeys` `$03`
|
/// box itself is drawn on exactly one of them (`infra/docs/macros-traps.md`, row 41).
|
||||||
/// while the box itself is drawn on exactly one of them (`infra/docs/macros-traps.md`, row 41).
|
|
||||||
///
|
///
|
||||||
/// **What it does not claim.** Red places a two-option menu where the script that asks for it
|
/// **The figure is found rather than pinned, since row 56.** Row 41 read one rectangle,
|
||||||
/// says, so a prompt drawn somewhere else reads `false` here and its dialog keeps the pad it has
|
/// (11, 6)-(19, 11), because that is where the centre's script puts it, and named the limit in its
|
||||||
/// always had. This is the box the nurse's "heal your POKeMON?" is drawn in, surveyed; it is not a
|
/// own residual: "Red places a two-option menu where the script asking for it says, so a prompt
|
||||||
/// general answer to "is a choice open", and nothing in the palette treats it as one.
|
/// drawn elsewhere reads `false` and keeps the pad it had". Row 56 is that residual, measured. The
|
||||||
|
/// Pewter Gym guide's "Let me take you to the top!" draws the same menu at
|
||||||
|
/// **(14, 7)-(19, 11)** with the cursor at column 15, so this read `false` on every frame of his
|
||||||
|
/// conversation: the pad was `NEXT, YES, NO` on a box that was a choice -- 12.10's forbidden pair,
|
||||||
|
/// because an A press at a two-option menu *is* `YES` -- and the reopened-prompt exclusion never
|
||||||
|
/// armed, because it only judges an answer to a prompt this crate can read. Surveyed over 260
|
||||||
|
/// presses (`examples/scene_probe.rs`, `FLY_PROBE_CATCH=dialog`): the box was drawn on 10 frames,
|
||||||
|
/// this answered `false` on all 260, and `wTextBoxID` read `TWO_OPTION_MENU` on exactly the 10.
|
||||||
|
///
|
||||||
|
/// So the screen half is now [`two_option_box_drawn`], which asks for the border **around the
|
||||||
|
/// cursor the game parked in it**, wherever on screen that is.
|
||||||
|
///
|
||||||
|
/// **What it still does not claim.** A frame with a two-option cursor and no border anywhere near
|
||||||
|
/// it reads `false`, which is the whole point of reading the figure; and a menu of two options that
|
||||||
|
/// is not a question about the world is still just a menu -- what the pad makes of a readable
|
||||||
|
/// prompt is [`super::macros::palette`]'s business, not this function's.
|
||||||
pub fn yes_no_prompt(memory: &mut dyn MemoryReader) -> bool {
|
pub fn yes_no_prompt(memory: &mut dyn MemoryReader) -> bool {
|
||||||
if read(memory, ram::wFontLoaded) & poke::BIT_FONT_LOADED == 0 {
|
if read(memory, ram::wFontLoaded) & poke::BIT_FONT_LOADED == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let cursor = cursor(memory);
|
let cursor = cursor(memory);
|
||||||
if cursor.top_y != poke::YES_NO_CURSOR_Y
|
if cursor.max != 1 || cursor.watched_keys != poke::pad::A | poke::pad::B {
|
||||||
|| cursor.top_x != poke::YES_NO_CURSOR_X
|
|
||||||
|| cursor.max != 1
|
|
||||||
|| cursor.watched_keys != poke::pad::A | poke::pad::B
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let (left, top, right, bottom) = poke::YES_NO_BOX;
|
two_option_box_drawn(memory, cursor.top_x, cursor.top_y)
|
||||||
border_drawn(memory, left, top, right, bottom)
|
}
|
||||||
|
|
||||||
|
/// Whether `DisplayTwoOptionMenu`'s own box is drawn around the cursor the game parked in it.
|
||||||
|
///
|
||||||
|
/// One fact about the routine rather than about any one script (row 56): the cursor goes in the
|
||||||
|
/// box's **first interior column**, so the border's left edge is one column to the left of
|
||||||
|
/// `wTopMenuItemX`. Both surveyed boxes satisfy it -- the nurse's left edge is 11 with the cursor
|
||||||
|
/// at 12, the gym guide's is 14 with the cursor at 15 -- and the *top* satisfies no such rule,
|
||||||
|
/// because the nurse's box begins two rows above the first item and the guide's one. So the top is
|
||||||
|
/// found: the nearest row above the cursor whose left column holds the border's top-left corner,
|
||||||
|
/// looking up at most [`poke::TWO_OPTION_CAPTION_ROWS`]. The rest of the figure is then read
|
||||||
|
/// **whole** by [`border_drawn`], exactly as `waiting` and the move list are, because a single
|
||||||
|
/// frame tile id is an ordinary character.
|
||||||
|
fn two_option_box_drawn(memory: &mut dyn MemoryReader, cursor_x: u8, cursor_y: u8) -> bool {
|
||||||
|
let Some(left) = u16::from(cursor_x).checked_sub(1) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let row = u16::from(cursor_y);
|
||||||
|
if row == 0 || left + 2 >= poke::SCREEN_WIDTH || row + 1 >= poke::SCREEN_HEIGHT {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(top) = (row.saturating_sub(poke::TWO_OPTION_CAPTION_ROWS)..row)
|
||||||
|
.rev()
|
||||||
|
.find(|top| screen_tile(memory, left, *top) == poke::frame::TOP_LEFT)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let last = (row + poke::TWO_OPTION_BOX_ROWS).min(poke::SCREEN_HEIGHT - 1);
|
||||||
|
((row + 1)..=last).any(|bottom| {
|
||||||
|
((left + 2)..poke::SCREEN_WIDTH)
|
||||||
|
.any(|right| border_drawn(memory, left, top, right, bottom))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `MoveSelectionMenu`'s own box is the figure on screen (`infra/docs/macros-traps.md`,
|
/// Whether `MoveSelectionMenu`'s own box is the figure on screen (`infra/docs/macros-traps.md`,
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,81 @@ fn a_text_box_is_open_from_the_font_flag_and_waiting_from_the_box() {
|
||||||
assert!(!text_box(&mut wram).waiting);
|
assert!(!text_box(&mut wram).waiting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Row 56: a two-option box is the one Red drew, not the one row 41 pinned.
|
||||||
|
///
|
||||||
|
/// Row 41 read the border at (11, 6)-(19, 11) because that is where the Pokémon Center's script
|
||||||
|
/// puts it, and named the limit in its own residual. The Pewter Gym guide's "Let me take you to the
|
||||||
|
/// top!" draws the same menu at **(14, 7)-(19, 11)** with the cursor at column 15, so the pinned
|
||||||
|
/// reading answered `false` on all 260 frames of a surveyed conversation while the box was drawn on
|
||||||
|
/// ten of them (`examples/scene_probe.rs`, `FLY_PROBE_CATCH=dialog`). The pad was therefore
|
||||||
|
/// `NEXT, YES, NO` on a box that was a choice -- two channels for one A press, 12.10's forbidden
|
||||||
|
/// pair -- and the reopened-prompt exclusion never armed, because it only judges an answer to a
|
||||||
|
/// prompt this crate can read.
|
||||||
|
///
|
||||||
|
/// What is the same in both boxes is a fact about `DisplayTwoOptionMenu` rather than about a
|
||||||
|
/// script: the cursor goes in the box's first interior column. The top is not, so it is found.
|
||||||
|
#[test]
|
||||||
|
fn a_yes_no_prompt_is_the_box_drawn_around_the_cursor_wherever_red_draws_it() {
|
||||||
|
// The centre's own box, which row 41 surveyed: still a prompt.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.yes_no_prompt();
|
||||||
|
assert!(yes_no_prompt(&mut wram), "the box at (11, 6)-(19, 11)");
|
||||||
|
|
||||||
|
// The gym guide's, three columns over and one row shorter. This is row 56.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.gym_yes_no_prompt();
|
||||||
|
assert!(yes_no_prompt(&mut wram), "the box at (14, 7)-(19, 11)");
|
||||||
|
|
||||||
|
// Both halves stay load-bearing: the cursor bytes outlive the box on every other frame of the
|
||||||
|
// conversation, and a frame with no box drawn is not a choice.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.dialogue_box().yes_no_cursor(poke::GYM_GUIDE_YES_NO_CURSOR_X);
|
||||||
|
assert!(!yes_no_prompt(&mut wram), "the cursor bytes with no box are not a prompt");
|
||||||
|
|
||||||
|
// The font flag gates it, exactly as it gates `waiting`.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.gym_yes_no_prompt().set(ram::wFontLoaded, 0);
|
||||||
|
assert!(!yes_no_prompt(&mut wram), "no text display, no prompt");
|
||||||
|
|
||||||
|
// A box the cursor is not parked in is not the cursor's box: the left edge is one column left
|
||||||
|
// of the first item, and that is the whole of what ties the two together.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.dialogue_box()
|
||||||
|
.draw_box(4, 7, 9, 11)
|
||||||
|
.yes_no_cursor(poke::GYM_GUIDE_YES_NO_CURSOR_X);
|
||||||
|
assert!(!yes_no_prompt(&mut wram), "a box somewhere else on the screen");
|
||||||
|
|
||||||
|
// And a menu of more than two options is not this menu, box or no box.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.gym_yes_no_prompt().set(ram::wMaxMenuItem, 2);
|
||||||
|
assert!(!yes_no_prompt(&mut wram), "three options is not a two-option box");
|
||||||
|
|
||||||
|
// The pads the two frames are dealt: a readable choice is the choice's own answers and `NEXT`
|
||||||
|
// is off it (12.10 in a dialog), and a plain box of the same conversation keeps all three.
|
||||||
|
use crate::pokemon_red::macros::palette::{MacroKind, scene_set};
|
||||||
|
let pad = |wram: &mut Wram| {
|
||||||
|
let scene = crate::pokemon_red::scene::detect(wram);
|
||||||
|
let mut poke = PokeState::new(wram);
|
||||||
|
scene_set(scene, &mut poke)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.gym_yes_no_prompt();
|
||||||
|
assert_eq!(
|
||||||
|
pad(&mut wram),
|
||||||
|
vec![MacroKind::Yes, MacroKind::No],
|
||||||
|
"the guide's box is a choice, so the pad is its answers"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.dialogue_box().yes_no_cursor(poke::GYM_GUIDE_YES_NO_CURSOR_X);
|
||||||
|
assert_eq!(
|
||||||
|
pad(&mut wram),
|
||||||
|
vec![MacroKind::Next, MacroKind::Yes, MacroKind::No],
|
||||||
|
"a plain box of the same conversation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_start_menu_counts_its_items() {
|
fn the_start_menu_counts_its_items() {
|
||||||
let mut wram = Wram::overworld();
|
let mut wram = Wram::overworld();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue