macros: a push-back is a refusal once the overworld has stayed the fly's for half a second, not on the first frame of it

This commit is contained in:
acamilo 2026-09-23 14:31:56 +00:00
parent d85ee5026f
commit 4108fec5ce
2 changed files with 68 additions and 9 deletions

View file

@ -149,6 +149,12 @@ const ANSWER_REOPEN_FRAMES: u32 = 24;
/// animation is about three. /// animation is about three.
const HEAL_WAIT_FRAMES: u32 = 360; 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 = /// How a macro ended, i.e. the `outcome` field of the `macro` feed event (section 5: "outcome =
/// done/blocked/timeout/refused"). /// done/blocked/timeout/refused").
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[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 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. /// the target or the ground is learned from it.
pending_push: Vec<PendingPush>, pending_push: Vec<PendingPush>,
/// 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. /// 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 /// The machine records rather than keeps: the ledger is the driver's
@ -696,6 +705,7 @@ impl MacroMachine {
pending_talk: None, pending_talk: None,
pending_answer: None, pending_answer: None,
pending_push: Vec::new(), pending_push: Vec::new(),
pending_push_calm: 0,
talked: None, talked: None,
rng: if seed == 0 { 1 } else { seed }, rng: if seed == 0 { 1 } else { seed },
} }
@ -979,6 +989,7 @@ impl MacroMachine {
self.pending_answer = None; self.pending_answer = None;
// Nor the cartridge refusing a step: the frames it happened in are being thrown away too. // Nor the cartridge refusing a step: the frames it happened in are being thrown away too.
self.pending_push.clear(); self.pending_push.clear();
self.pending_push_calm = 0;
} }
/// Whether the fly is standing somewhere other than where the running macro began. /// 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). /// 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 /// Back in the overworld with the buttons the fly's again, for [`PUSH_SETTLE_FRAMES`] running:
/// section 12.4 and row 37 always wrote it. A battle: a trainer's challenge, and it teaches the /// a refusal, written exactly as section 12.4 and row 37 always wrote it. A battle: a
/// ledgers nothing. Anything else -- the text, the walk, the frames between -- is still the /// trainer's challenge, and it teaches the ledgers nothing. Anything else -- the text, the
/// cartridge's, and the decision waits. /// 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) { fn observe_push(&mut self, state: &mut dyn MacroState) {
if self.pending_push.is_empty() { if self.pending_push.is_empty() {
self.pending_push_calm = 0;
return; return;
} }
match class(state.scene()) { 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 => { Class::Overworld => {
self.pending_push_calm = 0;
// Every macro the script ended while it held the joypad -- the walk it interrupted // 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. // and any press made into its text -- in the order they ended.
for pending in std::mem::take(&mut self.pending_push) { for pending in std::mem::take(&mut self.pending_push) {
@ -1058,7 +1084,7 @@ impl MacroMachine {
} }
} }
} }
_ => {} _ => self.pending_push_calm = 0,
} }
} }

View file

@ -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 /// The cartridge gives the joypad back in the overworld: the settle window of it, observed, and
/// it decided taken into the ledgers (row 58). /// whatever it decided taken into the ledgers (rows 58 and 59).
fn hand_back(machine: &mut MacroMachine, world: &mut World) { fn hand_back(machine: &mut MacroMachine, world: &mut World) {
world.scene = Scene::Overworld; world.scene = Scene::Overworld;
world.scripted = false; world.scripted = false;
world.scripted_at = None; world.scripted_at = None;
world.switch = None; world.switch = None;
machine.observe_frame(world); for _ in 0..super::executor::PUSH_SETTLE_FRAMES {
machine.observe_frame(world);
}
settle(machine, 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"); 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] #[test]
fn a_trainer_walking_up_teaches_the_ledgers_nothing() { 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; // The other half of the gym. A walk toward the leader crossed the Jr. Trainer's line of sight;