diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs index aaf306a..9c0c36a 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs @@ -149,6 +149,12 @@ const ANSWER_REOPEN_FRAMES: u32 = 24; /// animation is about three. const HEAL_WAIT_FRAMES: u32 = 360; +/// Frames running the overworld has to be the fly's before a push-back is written as a refusal +/// ([`MacroMachine::observe_push`], row 59). A trainer's challenge text closes onto five frames of +/// overworld before the battle is decided; six times that is still half a second, and a refusal +/// the cartridge really made loses nothing by being written half a second late. +pub const PUSH_SETTLE_FRAMES: u32 = 30; + /// How a macro ended, i.e. the `outcome` field of the `macro` feed event (section 5: "outcome = /// done/blocked/timeout/refused"). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -662,6 +668,9 @@ pub struct MacroMachine { /// the overworld is a refusal and is written as one; a battle is a battle, and nothing about /// the target or the ground is learned from it. pending_push: Vec, + /// How many frames running the overworld has been the fly's while [`Self::pending_push`] + /// waits: the push-back is decided at [`PUSH_SETTLE_FRAMES`] (row 59). + pending_push_calm: u32, /// A finished `TALK`'s target, waiting to be taken into the session's talked ledger. /// /// The machine records rather than keeps: the ledger is the driver's @@ -696,6 +705,7 @@ impl MacroMachine { pending_talk: None, pending_answer: None, pending_push: Vec::new(), + pending_push_calm: 0, talked: None, rng: if seed == 0 { 1 } else { seed }, } @@ -979,6 +989,7 @@ impl MacroMachine { self.pending_answer = None; // Nor the cartridge refusing a step: the frames it happened in are being thrown away too. self.pending_push.clear(); + self.pending_push_calm = 0; } /// Whether the fly is standing somewhere other than where the running macro began. @@ -1036,17 +1047,32 @@ impl MacroMachine { /// One frame after the cartridge took the joypad from a macro: decide what it was (row 58). /// - /// Back in the overworld with the buttons the fly's again: a refusal, written exactly as - /// section 12.4 and row 37 always wrote it. A battle: a trainer's challenge, and it teaches the - /// ledgers nothing. Anything else -- the text, the walk, the frames between -- is still the - /// cartridge's, and the decision waits. + /// Back in the overworld with the buttons the fly's again, for [`PUSH_SETTLE_FRAMES`] running: + /// a refusal, written exactly as section 12.4 and row 37 always wrote it. A battle: a + /// trainer's challenge, and it teaches the ledgers nothing. Anything else -- the text, the + /// walk, the frames between -- is still the cartridge's, and the decision waits. + /// + /// The window is row 59's. A trainer's challenge text closes onto five frames of an ordinary + /// overworld -- no text, no script, no joypad bit, `wCurOpponent` still clear -- before + /// `StartTrainerBattle` runs (`home/trainers.asm`: it follows `DisplayTextID`, whose + /// close-down redraws the map first). Decided on the first of them, Route 3's first trainer + /// walled (11, 6), the one gap between the road's west end and the rest of it, for the + /// session, and the fly walked between Pewter City and that end for hours. fn observe_push(&mut self, state: &mut dyn MacroState) { if self.pending_push.is_empty() { + self.pending_push_calm = 0; return; } match class(state.scene()) { - Class::Battle | Class::ForcedSwitch => self.pending_push.clear(), + Class::Battle | Class::ForcedSwitch => { + self.pending_push.clear(); + self.pending_push_calm = 0; + } + Class::Overworld if self.pending_push_calm + 1 < PUSH_SETTLE_FRAMES => { + self.pending_push_calm += 1; + } Class::Overworld => { + self.pending_push_calm = 0; // Every macro the script ended while it held the joypad -- the walk it interrupted // and any press made into its text -- in the order they ended. for pending in std::mem::take(&mut self.pending_push) { @@ -1058,7 +1084,7 @@ impl MacroMachine { } } } - _ => {} + _ => self.pending_push_calm = 0, } } 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 3c51456..49513e2 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 @@ -854,14 +854,16 @@ fn settle(machine: &mut MacroMachine, world: &mut World) { } } -/// The cartridge gives the joypad back in the overworld: one frame of it, observed, and whatever -/// it decided taken into the ledgers (row 58). +/// The cartridge gives the joypad back in the overworld: the settle window of it, observed, and +/// whatever it decided taken into the ledgers (rows 58 and 59). fn hand_back(machine: &mut MacroMachine, world: &mut World) { world.scene = Scene::Overworld; world.scripted = false; world.scripted_at = None; world.switch = None; - machine.observe_frame(world); + for _ in 0..super::executor::PUSH_SETTLE_FRAMES { + machine.observe_frame(world); + } settle(machine, world); } @@ -5428,6 +5430,37 @@ fn facing_one_of_the_rungs_people_is_the_arrival() { assert!(on_the_pad(&mut world, MacroKind::GoObjective), "turned away, the walk is back"); } +#[test] +fn a_challenge_closing_onto_a_few_frames_of_overworld_is_still_a_challenge() { + // Row 59, Route 3's first trainer. The challenge text closes, and for five frames the screen + // is an ordinary overworld with nothing set -- no text, no script, no joypad bit, the battle + // not yet decided -- before `StartTrainerBattle` runs. Decided on the first of them, the walk + // the trainer interrupted walled (11, 6) for the session: the one gap east on Route 3. + let mut world = World::room().at(3, 3); + world.map = 0x00; + world.connections = Connections { north: true, south: false, east: false, west: false }; + world.switch = Some((4, Scene::Dialog)); + world.scripted_at = Some(4); + let north = TargetKey::Exit(ExitId::Edge(Edge::North)); + let mut machine = MacroMachine::new(0x1234_5678); + assert_eq!(run_with(&mut machine, &mut world, MacroKind::GoRoute), Ok(MacroAbort::Done)); + + // The text closes onto five frames of overworld, and then the battle is decided. + world.scene = Scene::Overworld; + world.scripted = false; + world.scripted_at = None; + world.switch = None; + for _ in 0..5 { + machine.observe_frame(&mut world); + } + world.scene = Scene::Battle { own_turn: false, forced_switch: false }; + machine.observe_frame(&mut world); + settle(&mut machine, &mut world); + hand_back(&mut machine, &mut world); + assert!(!world.targets.blocked(world.map, north), "a challenge is not the road refusing"); + assert!(world.pushes.is_empty(), "and the gap it was walking through is still ground"); +} + #[test] fn a_trainer_walking_up_teaches_the_ledgers_nothing() { // The other half of the gym. A walk toward the leader crossed the Jr. Trainer's line of sight;