macros: a cursor step waits for the list it was built for
THROW BALL on the cartridge was 63 starts and 63 `blocked`, mean sixty-nine frames: the cursor to ITEM, the A that confirms it, the twenty settle frames, and then a refusal on the very next frame. Red keeps one cursor for every menu in the game, the bag takes longer than twenty frames to draw, and `listing` was still answering for the battle *menu* -- four entries, max 3. A ball whose bag index was above that read as off the end of the list and the step gave up at once; one inside it was walked for by pressing UP and LEFT at the battle menu, which is the blind pressing section 4 forbids. So a Listing now says which list it is (ListKind) and a cursor step says which list its target indexes into. A step whose list is not up waits exactly as it waits for a list that reports no cursor at all, and takes its press order and its budget from that list on the first frame it accepts input rather than from whatever was up when the script was built. A confirming press that has already begun still finishes: the press is what answers the list, so the cartridge is already drawing the next one while it is issued. The crossings are named: FIGHT and ITEM and PKMN and RUN over the battle menu, a move slot over the move list, a bag index over the bag, a party slot over the party list. A shop's own screens are one list and stay as they were. The fake gained the one thing it could not express -- a list that takes frames to draw -- and two of its fixtures had FIGHT and a potion opening nothing at all, which is how they passed while the cartridge could not.
This commit is contained in:
parent
029a3446fb
commit
80325c8d57
4 changed files with 241 additions and 59 deletions
|
|
@ -272,12 +272,44 @@ pub enum TargetKey {
|
|||
Tile(Tile),
|
||||
}
|
||||
|
||||
/// A cursor the current scene's macros navigate: where it is, and how far it can go.
|
||||
/// Which list the shared cursor belongs to right now.
|
||||
///
|
||||
/// Red keeps one cursor for every menu in the game (`wCurrentMenuItem`), so "where is the cursor"
|
||||
/// is only half a question: a script that opens the bag from the battle menu and then navigates to
|
||||
/// a bag index has to know that the index it is aiming at belongs to the *bag* and not to the four
|
||||
/// entries it was reading a moment ago. Measured on the cartridge 2026-09-22: `THROW BALL` was
|
||||
/// **63 starts and 63 `blocked`**, mean sixty-nine frames, because the bag takes longer than the
|
||||
/// twenty settle frames to draw -- so the step that should have walked the bag list read the
|
||||
/// battle menu's `max` of 3, found the ball's bag index above it, and gave up at once
|
||||
/// (`docs/design/macros.md` section 12.11).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ListKind {
|
||||
/// FIGHT / PKMN / ITEM / RUN.
|
||||
BattleMain,
|
||||
/// The move list.
|
||||
BattleMoves,
|
||||
/// The party list, inside a battle.
|
||||
BattleParty,
|
||||
/// The bag, inside a battle.
|
||||
BattleBag,
|
||||
/// The start menu.
|
||||
StartMenu,
|
||||
/// A mart's counter, on whichever of its screens is up.
|
||||
Shop,
|
||||
/// A PC.
|
||||
Pc,
|
||||
}
|
||||
|
||||
/// A cursor the current scene's macros navigate: which list it is, where it is, and how far it can
|
||||
/// go.
|
||||
///
|
||||
/// Derived from whichever of agent A's menus is up, so a script asks "where is the cursor" once
|
||||
/// and does not care whether it is in a battle, a mart or the start menu.
|
||||
/// and does not care whether it is in a battle, a mart or the start menu -- but it can ask *which*
|
||||
/// list answered, which is what a script that crosses from one list into another needs
|
||||
/// ([`ListKind`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Listing {
|
||||
pub kind: ListKind,
|
||||
pub current: u8,
|
||||
pub max: u8,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ use crate::adapter::MemoryReader;
|
|||
use crate::emulator::buttons;
|
||||
|
||||
use super::cartridge::{
|
||||
FACINGS, MacroState, TalkTarget, TargetKey, Tile, battle_entry, button, item, opposite,
|
||||
FACINGS, ListKind, MacroState, TalkTarget, TargetKey, Tile, battle_entry, button, item,
|
||||
opposite,
|
||||
};
|
||||
use super::geography::Amenity;
|
||||
use super::palette::{
|
||||
|
|
@ -411,14 +412,32 @@ struct Walk {
|
|||
struct Cursor {
|
||||
target: u8,
|
||||
confirm: bool,
|
||||
/// The directions to try, in order, aimed at the target by the first comparison.
|
||||
order: [Facing; 4],
|
||||
/// Which list `target` indexes into, when the script knows -- and it always does when it has
|
||||
/// just pressed A to open one.
|
||||
///
|
||||
/// `None` navigates whatever list is up, which is what a step that does not cross a boundary
|
||||
/// wants: the shop's own screens are one [`ListKind`] and the step after a purchase's A press
|
||||
/// is still the counter's.
|
||||
///
|
||||
/// While the list up is a *different* one, the step waits rather than pressing at it, exactly
|
||||
/// as it waits for a list that reports no cursor at all (section 4: "never by counting
|
||||
/// presses"). Section 12.11, measured: `THROW BALL` pressed ITEM, spent its twenty settle
|
||||
/// frames, and then read the battle *menu*'s `max` of 3 -- so a ball at bag index 4 was
|
||||
/// "off the end of the list" and the macro reported `blocked` on the first frame of the step,
|
||||
/// 63 times out of 63.
|
||||
want: Option<ListKind>,
|
||||
/// The directions to try, in order, aimed at the target from where the cursor actually is.
|
||||
///
|
||||
/// `None` until the list this step is for is accepting input, because "which way is the
|
||||
/// target" is a fact about that list and not about whatever was up when the script was built.
|
||||
order: Option<[Facing; 4]>,
|
||||
/// Which of them is being tried now.
|
||||
at: u8,
|
||||
/// The cursor value the current pulse started from.
|
||||
before: u8,
|
||||
/// Presses left in the budget.
|
||||
left: u8,
|
||||
/// Presses left in the budget, `None` until the list is up: twice the list plus slack, and
|
||||
/// the list whose length that is has to be the one being walked.
|
||||
left: Option<u8>,
|
||||
phase: u32,
|
||||
/// Frames spent waiting for the list to accept input.
|
||||
waited: u32,
|
||||
|
|
@ -1370,9 +1389,19 @@ fn goal_tile(walk: &Walk) -> Tile {
|
|||
/// which is what section 4 means by "never by counting presses": a list that is a column moves
|
||||
/// under DOWN and UP, Red's two-by-two battle menu also needs RIGHT and LEFT, and this finds out
|
||||
/// which by watching rather than by knowing. A list that is not accepting input yet is *waited*
|
||||
/// for, never pressed at.
|
||||
/// for, never pressed at -- and since section 12.11 so is a list that is up but is **not the one
|
||||
/// this step is walking**, because Red keeps one cursor for every menu in the game and reading the
|
||||
/// wrong one is not reading.
|
||||
fn cursor_frame(cursor: &mut Cursor, state: &mut dyn MacroState) -> Progress {
|
||||
let Some(list) = listing(state) else {
|
||||
// A confirming press that has begun finishes, and it is read before the list: the press is
|
||||
// what *answers* the list, so on the very frames it is being issued the cartridge is already
|
||||
// drawing the next one, and waiting for the list this step walked would wait for a list the
|
||||
// step has just left.
|
||||
if cursor.confirmed {
|
||||
return pulse(buttons::A, &mut cursor.phase, PRESS_HOLD, PRESS_GAP);
|
||||
}
|
||||
let list = listing(state).filter(|list| cursor.want.is_none_or(|want| want == list.kind));
|
||||
let Some(list) = list else {
|
||||
cursor.waited += 1;
|
||||
return if cursor.waited > CURSOR_WAIT {
|
||||
Progress::Blocked
|
||||
|
|
@ -1381,9 +1410,6 @@ fn cursor_frame(cursor: &mut Cursor, state: &mut dyn MacroState) -> Progress {
|
|||
};
|
||||
};
|
||||
let here = list.current;
|
||||
if cursor.confirmed {
|
||||
return pulse(buttons::A, &mut cursor.phase, PRESS_HOLD, PRESS_GAP);
|
||||
}
|
||||
if here == cursor.target {
|
||||
if !cursor.confirm {
|
||||
return Progress::Next;
|
||||
|
|
@ -1392,18 +1418,25 @@ fn cursor_frame(cursor: &mut Cursor, state: &mut dyn MacroState) -> Progress {
|
|||
cursor.phase = 0;
|
||||
return pulse(buttons::A, &mut cursor.phase, PRESS_HOLD, PRESS_GAP);
|
||||
}
|
||||
if cursor.left == 0 || cursor.target > list.max {
|
||||
// The first frame the list this step is for is accepting input is where the navigation is
|
||||
// aimed from and budgeted by. Both are facts about *this* list, and a script that opened it
|
||||
// could not have known either when it was built.
|
||||
let aimed = aim_order(cursor.target, here);
|
||||
let order = *cursor.order.get_or_insert(aimed);
|
||||
let sized = list.max.saturating_add(1).saturating_mul(2).saturating_add(CURSOR_SLACK);
|
||||
let budget = *cursor.left.get_or_insert(sized);
|
||||
if budget == 0 || cursor.target > list.max {
|
||||
return Progress::Blocked;
|
||||
}
|
||||
if cursor.phase == 0 {
|
||||
cursor.before = here;
|
||||
}
|
||||
let facing = cursor.order[usize::from(cursor.at) % cursor.order.len()];
|
||||
let facing = order[usize::from(cursor.at) % order.len()];
|
||||
match pulse(button(facing), &mut cursor.phase, PRESS_HOLD, PRESS_GAP) {
|
||||
Progress::Next => {
|
||||
let closer = here.abs_diff(cursor.target) < cursor.before.abs_diff(cursor.target);
|
||||
cursor.at = if closer { 0 } else { (cursor.at + 1) % 4 };
|
||||
cursor.left -= 1;
|
||||
cursor.left = Some(budget.saturating_sub(1));
|
||||
cursor.phase = 0;
|
||||
Progress::Hold(buttons::NONE)
|
||||
}
|
||||
|
|
@ -1411,6 +1444,19 @@ fn cursor_frame(cursor: &mut Cursor, state: &mut dyn MacroState) -> Progress {
|
|||
}
|
||||
}
|
||||
|
||||
/// The directions a cursor tries, in order, to get from `here` to `target`.
|
||||
///
|
||||
/// A column moves under DOWN and UP; Red's two-by-two battle menu needs RIGHT and LEFT as well,
|
||||
/// and which of the four works is found by watching the cursor rather than by knowing the
|
||||
/// geometry, so this only decides which to try *first*.
|
||||
const fn aim_order(target: u8, here: u8) -> [Facing; 4] {
|
||||
if target > here {
|
||||
[Facing::Down, Facing::Right, Facing::Up, Facing::Left]
|
||||
} else {
|
||||
[Facing::Up, Facing::Left, Facing::Down, Facing::Right]
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the script for one macro, or `None` when there is nothing to walk to.
|
||||
///
|
||||
/// Every target is computed here, once, from the state the macro started in — which move, which
|
||||
|
|
@ -1520,7 +1566,7 @@ fn script(
|
|||
} else {
|
||||
cursor_at.filter(|at| *at < count)?
|
||||
};
|
||||
steps.push(cursor(state, target, true));
|
||||
steps.push(cursor_on(target, true, Some(ListKind::BattleMoves)));
|
||||
return Some((steps.into(), aimed));
|
||||
}
|
||||
// From the menu above, FIGHT has to be chosen first, whether or not anything has PP:
|
||||
|
|
@ -1529,7 +1575,7 @@ fn script(
|
|||
if !in_main_menu(state) {
|
||||
return None;
|
||||
}
|
||||
steps.push(cursor(state, battle_entry::FIGHT, true));
|
||||
steps.push(cursor_on(battle_entry::FIGHT, true, Some(ListKind::BattleMain)));
|
||||
steps.push(settle());
|
||||
// What happens after FIGHT is the cartridge's own answer and it is measured rather
|
||||
// than assumed (row 34): with a move that has PP the list opens and this slot is
|
||||
|
|
@ -1538,7 +1584,7 @@ fn script(
|
|||
// second cursor step would press A at text. `MOVE 1` is the button that reaches that
|
||||
// state, because it is the only one bound there.
|
||||
if slot_has_pp(state, slot) {
|
||||
steps.push(cursor(state, slot, true));
|
||||
steps.push(cursor_on(slot, true, Some(ListKind::BattleMoves)));
|
||||
}
|
||||
steps
|
||||
}
|
||||
|
|
@ -1550,10 +1596,13 @@ fn script(
|
|||
let bag_slot = throw_slot(state)?;
|
||||
let mut steps = Vec::new();
|
||||
if in_main_menu(state) {
|
||||
steps.push(cursor(state, battle_entry::ITEM, true));
|
||||
steps.push(cursor_on(battle_entry::ITEM, true, Some(ListKind::BattleMain)));
|
||||
steps.push(settle());
|
||||
}
|
||||
steps.push(cursor(state, bag_slot, true));
|
||||
// The bag, and it has to *be* the bag: this is the step that reported `blocked` 63
|
||||
// times out of 63 on the cartridge while it was allowed to read the battle menu's
|
||||
// cursor instead (section 12.11).
|
||||
steps.push(cursor_on(bag_slot, true, Some(ListKind::BattleBag)));
|
||||
steps
|
||||
}
|
||||
MacroKind::Switch => {
|
||||
|
|
@ -1562,10 +1611,10 @@ fn script(
|
|||
// A forced switch is already looking at the party list; a chosen switch has to get
|
||||
// there through the battle menu's PKMN entry first.
|
||||
if in_main_menu(state) {
|
||||
steps.push(cursor(state, battle_entry::PKMN, true));
|
||||
steps.push(cursor_on(battle_entry::PKMN, true, Some(ListKind::BattleMain)));
|
||||
steps.push(settle());
|
||||
}
|
||||
steps.push(cursor(state, slot, true));
|
||||
steps.push(cursor_on(slot, true, Some(ListKind::BattleParty)));
|
||||
steps.push(settle());
|
||||
// The party entry's action list opens on SWITCH.
|
||||
steps.push(press(buttons::A));
|
||||
|
|
@ -1576,17 +1625,19 @@ fn script(
|
|||
let active = state.battle().and_then(|battle| battle.own).map(|mon| mon.slot)?;
|
||||
let mut steps = Vec::new();
|
||||
if in_main_menu(state) {
|
||||
steps.push(cursor(state, battle_entry::ITEM, true));
|
||||
steps.push(cursor_on(battle_entry::ITEM, true, Some(ListKind::BattleMain)));
|
||||
steps.push(settle());
|
||||
}
|
||||
steps.push(cursor(state, bag_slot, true));
|
||||
steps.push(cursor_on(bag_slot, true, Some(ListKind::BattleBag)));
|
||||
steps.push(settle());
|
||||
// Which Pokémon to heal: the one that is out, because that is the HP the precondition
|
||||
// measured.
|
||||
steps.push(cursor(state, active, true));
|
||||
steps.push(cursor_on(active, true, Some(ListKind::BattleParty)));
|
||||
steps
|
||||
}
|
||||
MacroKind::Run => vec![cursor(state, battle_entry::RUN, true)],
|
||||
MacroKind::Run => {
|
||||
vec![cursor_on(battle_entry::RUN, true, Some(ListKind::BattleMain))]
|
||||
}
|
||||
MacroKind::BuyPotion => shop_plan(state, item::POTION)?,
|
||||
MacroKind::BuyBall => shop_plan(state, item::POKE_BALL)?,
|
||||
MacroKind::BuyAntidote => shop_plan(state, item::ANTIDOTE)?,
|
||||
|
|
@ -1675,24 +1726,31 @@ fn settle() -> Step {
|
|||
Step::Settle { phase: 0 }
|
||||
}
|
||||
|
||||
/// A cursor step aimed at `target`, with its press order and budget taken from the list that is
|
||||
/// up right now.
|
||||
fn cursor(state: &mut dyn MacroState, target: u8, confirm: bool) -> Step {
|
||||
let list = listing(state);
|
||||
let from = list.map_or(0, |list| list.current);
|
||||
let max = list.map_or(target, |list| list.max);
|
||||
/// A cursor step aimed at `target` in whichever list is accepting input when it runs.
|
||||
///
|
||||
/// For a step that does not cross from one list into another: the shop's own screens, and the
|
||||
/// first step of any script, which reads the list its macro was dealt on.
|
||||
fn cursor(target: u8, confirm: bool) -> Step {
|
||||
cursor_on(target, confirm, None)
|
||||
}
|
||||
|
||||
/// A cursor step aimed at `target` in `want`, waiting for that list rather than pressing at
|
||||
/// whichever one happens to be up.
|
||||
///
|
||||
/// The press order and the budget are taken from the list on the first frame it accepts input
|
||||
/// ([`cursor_frame`]) and not from here, because a script that has just pressed A to open a list
|
||||
/// is still reading the list it pressed A *in*: twenty settle frames is not always enough for the
|
||||
/// cartridge to draw the next one, and the one it has is the wrong length and points the wrong way
|
||||
/// (section 12.11).
|
||||
fn cursor_on(target: u8, confirm: bool, want: Option<ListKind>) -> Step {
|
||||
Step::Cursor(Cursor {
|
||||
target,
|
||||
confirm,
|
||||
order: if target > from {
|
||||
[Facing::Down, Facing::Right, Facing::Up, Facing::Left]
|
||||
} else {
|
||||
[Facing::Up, Facing::Left, Facing::Down, Facing::Right]
|
||||
},
|
||||
want,
|
||||
order: None,
|
||||
at: 0,
|
||||
before: from,
|
||||
// Twice the list, plus slack for a press the game swallows while a menu draws.
|
||||
left: max.saturating_add(1).saturating_mul(2).saturating_add(CURSOR_SLACK),
|
||||
before: target,
|
||||
left: None,
|
||||
phase: 0,
|
||||
waited: 0,
|
||||
confirmed: false,
|
||||
|
|
@ -1902,10 +1960,10 @@ fn shop_plan(state: &mut dyn MacroState, want: u8) -> Option<Vec<Step>> {
|
|||
let mut steps = Vec::new();
|
||||
if shop_screen(state)? == ShopScreen::BuySellQuit {
|
||||
// BUY is the counter menu's first entry.
|
||||
steps.push(cursor(state, 0, true));
|
||||
steps.push(cursor(0, true));
|
||||
steps.push(settle());
|
||||
}
|
||||
steps.push(cursor(state, index, true));
|
||||
steps.push(cursor(index, true));
|
||||
steps.push(settle());
|
||||
// The quantity prompt opens on one, and the price confirmation opens on YES.
|
||||
steps.push(press(buttons::A));
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
use super::cartridge::{
|
||||
CHEAPEST_PURCHASE, FACINGS, opposite,
|
||||
ExitId, Listing, MacroState, Objective, PARTY_CAPACITY, PURCHASES, TalkTarget, TargetKey,
|
||||
Tile, item, outdoors,
|
||||
ExitId, ListKind, Listing, MacroState, Objective, PARTY_CAPACITY, PURCHASES, TalkTarget,
|
||||
TargetKey, Tile, item, outdoors,
|
||||
};
|
||||
use crate::adapter::PlaceKind;
|
||||
|
||||
|
|
@ -1735,28 +1735,42 @@ pub fn throw_slot(state: &mut dyn MacroState) -> Option<u8> {
|
|||
pub fn listing(state: &mut dyn MacroState) -> Option<Listing> {
|
||||
if let Some(battle) = state.battle() {
|
||||
return match battle.menu {
|
||||
BattleMenu::Main { cursor } => Some(Listing { current: cursor, max: 3 }),
|
||||
BattleMenu::Moves { cursor: Some(cursor), count } => {
|
||||
Some(Listing { current: cursor, max: count.saturating_sub(1) })
|
||||
BattleMenu::Main { cursor } => {
|
||||
Some(Listing { kind: ListKind::BattleMain, current: cursor, max: 3 })
|
||||
}
|
||||
BattleMenu::Moves { cursor: Some(cursor), count } => Some(Listing {
|
||||
kind: ListKind::BattleMoves,
|
||||
current: cursor,
|
||||
max: count.saturating_sub(1),
|
||||
}),
|
||||
BattleMenu::Moves { cursor: None, .. } | BattleMenu::None => None,
|
||||
BattleMenu::Bag { cursor, count } => {
|
||||
(count > 0).then(|| Listing { current: cursor, max: count.saturating_sub(1) })
|
||||
}
|
||||
BattleMenu::Bag { cursor, count } => (count > 0).then(|| Listing {
|
||||
kind: ListKind::BattleBag,
|
||||
current: cursor,
|
||||
max: count.saturating_sub(1),
|
||||
}),
|
||||
BattleMenu::Party { cursor } => {
|
||||
let max = u8::try_from(state.party().mons.len().saturating_sub(1)).unwrap_or(0);
|
||||
Some(Listing { current: cursor, max })
|
||||
Some(Listing { kind: ListKind::BattleParty, current: cursor, max })
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(menu) = state.start_menu() {
|
||||
return Some(Listing { current: menu.cursor.current, max: menu.cursor.max });
|
||||
return Some(Listing {
|
||||
kind: ListKind::StartMenu,
|
||||
current: menu.cursor.current,
|
||||
max: menu.cursor.max,
|
||||
});
|
||||
}
|
||||
if let Some(shop) = state.shop() {
|
||||
return Some(Listing { current: shop.cursor.current, max: shop.cursor.max });
|
||||
return Some(Listing {
|
||||
kind: ListKind::Shop,
|
||||
current: shop.cursor.current,
|
||||
max: shop.cursor.max,
|
||||
});
|
||||
}
|
||||
if let Some(pc) = state.pc() {
|
||||
return Some(Listing { current: pc.cursor.current, max: pc.cursor.max });
|
||||
return Some(Listing { kind: ListKind::Pc, current: pc.cursor.current, max: pc.cursor.max });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,15 @@ struct World {
|
|||
start_to_open: u8,
|
||||
/// What the next A presses open.
|
||||
opens: VecDeque<Opens>,
|
||||
/// Frames the cartridge spends *drawing* a list an A press opened, before it accepts input.
|
||||
///
|
||||
/// Zero everywhere but the one test this is for. On the cartridge it is not zero and it is not
|
||||
/// bounded by the twenty frames a script's `settle` waits: measured 2026-09-22, `THROW BALL`
|
||||
/// pressed ITEM, settled, and then read the battle *menu*'s cursor because the bag had not
|
||||
/// drawn yet -- 63 starts, 63 `blocked` (section 12.11).
|
||||
opens_draw_in: u32,
|
||||
/// The list an A press opened and the frame it starts accepting input on.
|
||||
pending: Option<(u32, Opens)>,
|
||||
/// A scene the world switches to at this frame, for the abort rule.
|
||||
switch: Option<(u32, Scene)>,
|
||||
/// A frame at which the cartridge heals the party, which is what a Pokémon Center does while
|
||||
|
|
@ -223,6 +232,8 @@ impl World {
|
|||
b_to_close: 1,
|
||||
start_to_open: 1,
|
||||
opens: VecDeque::new(),
|
||||
opens_draw_in: 0,
|
||||
pending: None,
|
||||
switch: None,
|
||||
heal_at: None,
|
||||
scripted: false,
|
||||
|
|
@ -342,6 +353,15 @@ impl World {
|
|||
|
||||
fn frame(&mut self, mask: u8) {
|
||||
self.frames += 1;
|
||||
if let Some((at, next)) = self.pending
|
||||
&& self.frames >= at
|
||||
{
|
||||
self.list = next.list;
|
||||
self.cursor = next.cursor;
|
||||
self.cursor_max = next.max;
|
||||
self.grid = next.grid;
|
||||
self.pending = None;
|
||||
}
|
||||
if let Some(at) = self.scripted_at
|
||||
&& self.frames >= at
|
||||
{
|
||||
|
|
@ -411,10 +431,14 @@ impl World {
|
|||
if mask == buttons::A
|
||||
&& let Some(next) = self.opens.pop_front()
|
||||
{
|
||||
self.list = next.list;
|
||||
self.cursor = next.cursor;
|
||||
self.cursor_max = next.max;
|
||||
self.grid = next.grid;
|
||||
if self.opens_draw_in > 0 {
|
||||
self.pending = Some((self.frames + self.opens_draw_in, next));
|
||||
} else {
|
||||
self.list = next.list;
|
||||
self.cursor = next.cursor;
|
||||
self.cursor_max = next.max;
|
||||
self.grid = next.grid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1042,8 +1066,12 @@ fn a_move_button_confirms_its_own_slot_over_an_open_list() {
|
|||
assert_eq!(world.cursor, 2, "the third slot, by watching where the cursor went");
|
||||
assert_eq!(world.pulses.last(), Some(&buttons::A));
|
||||
|
||||
// And from the menu above, FIGHT first and then the slot.
|
||||
// And from the menu above, FIGHT first and then the slot. The list has to actually open:
|
||||
// since section 12.11 the step that walks it *waits* for the move list rather than reading
|
||||
// whatever cursor is up, so a fixture where FIGHT opens nothing waits out `CURSOR_WAIT` --
|
||||
// which is the right answer, and is what the cartridge was doing to `THROW BALL` in reverse.
|
||||
let mut world = World::battle();
|
||||
world.opens.push_back(Opens { list: List::Moves(3), cursor: 0, max: 2, grid: false });
|
||||
assert_eq!(run(&mut world, MacroKind::Move2).unwrap(), MacroAbort::Done);
|
||||
assert!(
|
||||
world.pulses.contains(&buttons::A),
|
||||
|
|
@ -2142,6 +2170,10 @@ fn item_reaches_the_potion_by_reading_the_bags_cursor() {
|
|||
let mut world = World::battle();
|
||||
world.bag = vec![(item::POKE_BALL, 3), (item::POTION, 2)];
|
||||
world.opens.push_back(Opens { list: List::BattleBag, cursor: 0, max: 1, grid: false });
|
||||
// And confirming the potion opens the party list, which is where the script says which
|
||||
// Pokémon to heal. Section 12.11: that step waits for the *party* list, so the fixture has to
|
||||
// open it -- the cartridge does.
|
||||
world.opens.push_back(Opens { list: List::BattleParty, cursor: 0, max: 2, grid: false });
|
||||
assert_eq!(run(&mut world, MacroKind::Item).unwrap(), MacroAbort::Done);
|
||||
assert!(
|
||||
world.pulses.iter().filter(|mask| **mask == buttons::A).count() >= 2,
|
||||
|
|
@ -4346,6 +4378,52 @@ fn the_move_list_deals_back_only_where_the_moves_can_be_read() {
|
|||
assert_eq!(world.pulses.last(), Some(&buttons::A));
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// starts and 63 `blocked`**, mean sixty-nine frames -- which is the cursor to ITEM, the A that
|
||||
/// confirms it, the twenty settle frames, and then a refusal on the very next frame. The bag had
|
||||
/// not drawn yet, so `listing` still answered for the battle *menu*: four entries, `max` 3. The
|
||||
/// ball's own bag index was above that, so the step read "off the end of the list" and gave up at
|
||||
/// once -- and where the index was inside it, the step pressed UP and LEFT at the battle menu
|
||||
/// instead, which is the blind pressing section 4 forbids.
|
||||
#[test]
|
||||
fn throw_ball_waits_for_the_bag_rather_than_reading_the_menu_it_came_from() {
|
||||
let mut world = World::battle();
|
||||
world.mons.truncate(1);
|
||||
// Five items with the ball last, so its bag index is 4 -- above the battle menu's `max` of 3,
|
||||
// which is the number the old step compared it against.
|
||||
world.bag = vec![
|
||||
(item::POTION, 1),
|
||||
(item::ANTIDOTE, 1),
|
||||
(item::REPEL, 1),
|
||||
(item::POTION, 1),
|
||||
(item::POKE_BALL, 5),
|
||||
];
|
||||
assert_eq!(throw_slot(&mut world), Some(4));
|
||||
// The bag takes longer to draw than the script's twenty settle frames, which is the cartridge's
|
||||
// own timing and the whole of the trap.
|
||||
world.opens_draw_in = 40;
|
||||
world.opens.push_back(Opens { list: List::BattleBag, cursor: 0, max: 4, grid: false });
|
||||
assert_eq!(run(&mut world, MacroKind::ThrowBall).unwrap(), MacroAbort::Done);
|
||||
assert_eq!(world.cursor, 4, "the ball's own bag index, by reading the bag's cursor");
|
||||
assert_eq!(world.pulses.last(), Some(&buttons::A));
|
||||
// Nothing was pressed at the menu it came from while the bag was drawing: the presses are the
|
||||
// one that chose ITEM and then the bag's own.
|
||||
assert_eq!(
|
||||
world.pulses.iter().filter(|mask| **mask == buttons::UP).count(),
|
||||
0,
|
||||
"no blind press at the list it had already answered: {:?}",
|
||||
world.pulses
|
||||
);
|
||||
|
||||
// And a list that never opens is `blocked` rather than pressed at blind.
|
||||
let mut never = World::battle();
|
||||
never.mons.truncate(1);
|
||||
never.bag = vec![(item::POKE_BALL, 5)];
|
||||
assert_eq!(run(&mut never, MacroKind::ThrowBall).unwrap(), MacroAbort::Blocked);
|
||||
}
|
||||
|
||||
/// Row 37 of `infra/docs/macros-traps.md`: a tile the cartridge pushes the fly off is not a tile
|
||||
/// to stand on, and the ground beside a villager is not the villager's fault.
|
||||
///
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue