palette: a move the cartridge answers with nothing is not dealt beside one it does not
Row 60: TAIL WHIP against a Pidgey at DEFENSE -6 was MOVE 2 183 times. The rule is PP's: a refused move leaves the pad while another move is usable, and with none usable the moves stay as PP deals them, so an open list never comes down to BACK alone.
This commit is contained in:
parent
438b2d8540
commit
f3e3e9cf98
2 changed files with 122 additions and 7 deletions
|
|
@ -1893,7 +1893,8 @@ pub const fn move_index(kind: MacroKind) -> Option<u8> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether `kind`'s move slot holds a move with PP: the four buttons' precondition (section 14).
|
||||
/// Whether `kind`'s move slot holds a move with PP that the battle engine will not answer with
|
||||
/// nothing: the four buttons' precondition (section 14, row 60).
|
||||
///
|
||||
/// Three things it is *not*, each of them a bug this palette has had:
|
||||
///
|
||||
|
|
@ -1921,23 +1922,41 @@ pub fn move_slot_bound(state: &mut dyn MacroState, kind: MacroKind) -> bool {
|
|||
// all, so the button is bound there whatever the seam can make of the battler. That is the
|
||||
// backstop `NEXT` used to be on this row (section 12.10): the own turn's main menu always has
|
||||
// a button that ends the turn, and it is never one that merely reopens a list.
|
||||
if index == 0 && matches!(battle.menu, BattleMenu::Main { .. }) {
|
||||
return true;
|
||||
}
|
||||
let main = matches!(battle.menu, BattleMenu::Main { .. });
|
||||
// And the same backstop over an **open move list** whose battler the seam cannot read
|
||||
// (section 12.11). That frame used to deal `BACK` alone -- the only button on it closed the
|
||||
// list `MOVE 1` on the menu underneath had just opened, which is 12.10's pair with `MOVE 1` in
|
||||
// `NEXT`'s place. `MOVE 1`'s script over an open list confirms wherever the cursor stands, so
|
||||
// it reads no move either, and confirming a move is what ends a turn.
|
||||
let Some(own) = battle.own else {
|
||||
return index == 0 && matches!(battle.menu, BattleMenu::Moves { cursor: Some(_), .. });
|
||||
return index == 0
|
||||
&& (main || matches!(battle.menu, BattleMenu::Moves { cursor: Some(_), .. }));
|
||||
};
|
||||
let holds = |slot: usize| -> Option<&Move> {
|
||||
own.moves.get(slot).and_then(|entry| entry.as_ref()).filter(|entry| entry.id != 0)
|
||||
};
|
||||
let Some(entry) = holds(usize::from(index)) else { return false };
|
||||
// **A move the cartridge will answer with nothing is not dealt beside one it will not**
|
||||
// (row 60, section 12.23). Live on Route 1: Squirtle's TAIL WHIP against a Pidgey whose
|
||||
// DEFENSE was already at -6 was `MOVE 2` 183 times, "Nothing happened!" every time, and no
|
||||
// battle ended by the fly's hand. What the move does is the move table's and the effect
|
||||
// routine's answer ([`MacroState::move_without_effect`]), read the same way for every move,
|
||||
// and it is PP's rule over again: a spent move is not offered beside a usable one, and when
|
||||
// nothing is usable what was dealt stays dealt -- taking the last moves away would leave a list
|
||||
// whose only button is `BACK`, which is 12.11's pair.
|
||||
let mut useful = [false; 4];
|
||||
for (slot, flag) in useful.iter_mut().enumerate() {
|
||||
if let Some(entry) = holds(slot).copied() {
|
||||
*flag = entry.pp > 0 && !state.move_without_effect(entry.id);
|
||||
}
|
||||
}
|
||||
let any_useful = useful.iter().any(|flag| *flag);
|
||||
let entry = holds(usize::from(index)).copied();
|
||||
if index == 0 && main {
|
||||
return !any_useful || useful[0] || entry.is_none_or(|entry| entry.pp == 0);
|
||||
}
|
||||
let Some(entry) = entry else { return false };
|
||||
if entry.pp > 0 {
|
||||
return true;
|
||||
return useful[usize::from(index)] || !any_useful;
|
||||
}
|
||||
// Out of PP. Only `MOVE 1` stays, and only when nothing else has any either -- otherwise the
|
||||
// fly would be offered a spent move beside a usable one.
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ struct World {
|
|||
money: u32,
|
||||
bag: Vec<(u8, u8)>,
|
||||
stock: Vec<u8>,
|
||||
/// Move ids the battle engine would answer with "Nothing happened!" on this frame (row 60).
|
||||
no_effect: BTreeSet<u8>,
|
||||
/// Tiles the game lets the player talk *over*: a mart's or a centre's counter.
|
||||
counters: BTreeSet<Tile>,
|
||||
/// Errands this run has discharged (`docs/design/macros.md` section 13).
|
||||
|
|
@ -242,6 +244,7 @@ impl World {
|
|||
pushes: BTreeSet::new(),
|
||||
exhausted: BTreeSet::new(),
|
||||
stock: Vec::new(),
|
||||
no_effect: BTreeSet::new(),
|
||||
visited: BTreeSet::new(),
|
||||
stood: BTreeSet::new(),
|
||||
seen_maps: BTreeSet::new(),
|
||||
|
|
@ -696,6 +699,10 @@ impl MacroState for World {
|
|||
self.prompt && self.scene == Scene::Dialog
|
||||
}
|
||||
|
||||
fn move_without_effect(&mut self, id: u8) -> bool {
|
||||
self.no_effect.contains(&id)
|
||||
}
|
||||
|
||||
fn shop_stock(&mut self) -> Vec<u8> {
|
||||
self.stock.clone()
|
||||
}
|
||||
|
|
@ -3939,6 +3946,95 @@ fn a_turn_with_nothing_to_attack_switch_or_flee_with_still_has_a_button() {
|
|||
assert!(pad_of(&mut world).contains(&"MOVE 1"));
|
||||
}
|
||||
|
||||
/// `constants/move_constants.asm`: the two moves a level-5 Squirtle knows.
|
||||
const TACKLE: u8 = 0x21;
|
||||
const TAIL_WHIP: u8 = 0x27;
|
||||
|
||||
#[test]
|
||||
fn a_move_the_cartridge_answers_with_nothing_is_off_the_pad_beside_one_it_does_not() {
|
||||
// Row 60, live on Route 1: Squirtle L5 with TACKLE and TAIL WHIP, a Pidgey whose DEFENSE is
|
||||
// already at -6, and `MOVE 2` chosen 183 times to "Nothing happened!". Over the menu and over
|
||||
// the open list, TAIL WHIP leaves the pad and TACKLE stays.
|
||||
let mut world = World::battle();
|
||||
world.mons = vec![mon(0, 8, 20, &[(TACKLE, 35), (TAIL_WHIP, 30)])];
|
||||
world.active = Some(0);
|
||||
world.list = List::BattleMain;
|
||||
assert!(on_the_pad(&mut world, MacroKind::Move2), "before the stage is at its limit");
|
||||
|
||||
world.no_effect.insert(TAIL_WHIP);
|
||||
for list in [List::BattleMain, List::Moves(2)] {
|
||||
world.list = list;
|
||||
world.grid = list == List::BattleMain;
|
||||
let pad = pad_of(&mut world);
|
||||
assert!(!pad.contains(&"MOVE 2"), "{list:?} deals {pad:?}");
|
||||
assert!(pad.contains(&"MOVE 1"), "{list:?} deals {pad:?}");
|
||||
}
|
||||
// Nothing presses for the fly: the button is gone, and nothing is chosen in its place.
|
||||
world.list = List::BattleMain;
|
||||
world.grid = true;
|
||||
assert!(!move_slot_bound(&mut world, MacroKind::Move2));
|
||||
|
||||
// Slot one is read the same way: FIGHT's backstop over the menu is not a way to deal a move
|
||||
// that does nothing beside one that does.
|
||||
let mut swapped = World::battle();
|
||||
swapped.mons = vec![mon(0, 8, 20, &[(TAIL_WHIP, 30), (TACKLE, 35)])];
|
||||
swapped.active = Some(0);
|
||||
swapped.list = List::BattleMain;
|
||||
swapped.no_effect.insert(TAIL_WHIP);
|
||||
assert_eq!(
|
||||
pad_of(&mut swapped).iter().filter(|name| name.starts_with("MOVE")).collect::<Vec<_>>(),
|
||||
[&"MOVE 2"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_no_move_that_does_anything_the_moves_stay_as_pp_deals_them() {
|
||||
// PP's own rule (section 12.8, row 30a): with nothing usable, what ends the turn stays on the
|
||||
// pad. Taking every move away over an open list would leave `BACK` alone, which closes what
|
||||
// `MOVE 1` on the menu underneath opened -- 12.11's pair.
|
||||
let mut world = World::battle();
|
||||
world.mons = vec![mon(0, 8, 20, &[(TACKLE, 0), (TAIL_WHIP, 30)])];
|
||||
world.active = Some(0);
|
||||
world.no_effect.insert(TAIL_WHIP);
|
||||
world.list = List::Moves(2);
|
||||
world.grid = false;
|
||||
world.cursor_max = 1;
|
||||
let pad = pad_of(&mut world);
|
||||
assert!(pad.contains(&"MOVE 2"), "the one move with PP still ends the turn: {pad:?}");
|
||||
assert_ne!(pad, ["BACK"]);
|
||||
|
||||
world.list = List::BattleMain;
|
||||
world.grid = true;
|
||||
let pad = pad_of(&mut world);
|
||||
assert!(pad.contains(&"MOVE 1") && pad.contains(&"MOVE 2"), "{pad:?}");
|
||||
|
||||
// And both moves without effect: nothing changes from what PP alone deals.
|
||||
let mut both = World::battle();
|
||||
both.mons = vec![mon(0, 8, 20, &[(TACKLE, 35), (TAIL_WHIP, 30)])];
|
||||
both.active = Some(0);
|
||||
both.list = List::BattleMain;
|
||||
let before = pad_of(&mut both);
|
||||
both.no_effect.extend([TACKLE, TAIL_WHIP]);
|
||||
assert_eq!(pad_of(&mut both), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_spent_move_and_a_move_without_effect_leave_the_one_that_works() {
|
||||
// The two readings together: slot one spent, slot two refused, slot three usable. Only
|
||||
// `MOVE 3` is a move; `MOVE 1` over the menu is FIGHT's backstop only while slot one is the
|
||||
// thing that can end the turn, and it is spent -- row 34's behaviour, unchanged.
|
||||
let mut world = World::battle();
|
||||
world.mons = vec![mon(0, 8, 20, &[(TACKLE, 0), (TAIL_WHIP, 30), (0x2d, 40)])];
|
||||
world.active = Some(0);
|
||||
world.no_effect.insert(TAIL_WHIP);
|
||||
world.list = List::Moves(3);
|
||||
world.grid = false;
|
||||
world.cursor_max = 2;
|
||||
let moves: Vec<&str> =
|
||||
pad_of(&mut world).into_iter().filter(|name| name.starts_with("MOVE")).collect();
|
||||
assert_eq!(moves, ["MOVE 3"]);
|
||||
}
|
||||
|
||||
/// The bound buttons of the macros-mode pad for the scene the world is in, unbound slots dropped.
|
||||
fn pad_of(world: &mut World) -> Vec<&'static str> {
|
||||
let scene = world.scene();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue