state: the move table, and whether the battle engine will answer a move with nothing
move_data reads a row of Moves from the cartridge image ($0E:$4000, each row checked against its own id); move_without_effect answers the refusals the effect routines make on bytes already in WRAM: a stat stage at its limit or a stat at 1/999, Mist or a substitute against a stat-lowering move, a status move against a statused, Poison-type or (Electric) Ground-type target. MacroState::move_without_effect defaults to false.
This commit is contained in:
parent
c8ecf06db5
commit
438b2d8540
4 changed files with 358 additions and 0 deletions
|
|
@ -416,6 +416,38 @@ impl Wram {
|
||||||
pub const BLOCKSET_BANK: u8 = 0x11;
|
pub const BLOCKSET_BANK: u8 = 0x11;
|
||||||
pub const BLOCKSET_BASE: u16 = 0x4000;
|
pub const BLOCKSET_BASE: u16 = 0x4000;
|
||||||
|
|
||||||
|
/// Rows of the cartridge's move table, where `data/moves/moves.asm` puts it: `$0E:$4000`,
|
||||||
|
/// six bytes a row in move-id order, each opening with its own id. `(id, effect, power,
|
||||||
|
/// type)`; accuracy 100 and PP 35 stand in for the two bytes nothing here reads.
|
||||||
|
pub fn move_table(&mut self, rows: &[(u8, u8, u8, u8)]) -> &mut Self {
|
||||||
|
use super::state::poke::moves::{ROW_BYTES, TABLE_ADDRESS, TABLE_BANK};
|
||||||
|
for (id, effect, power, kind) in rows {
|
||||||
|
let base = TABLE_ADDRESS + u16::from(id - 1) * ROW_BYTES;
|
||||||
|
for (offset, byte) in [*id, *effect, *power, *kind, 0xff, 35].into_iter().enumerate() {
|
||||||
|
self.rom.insert((TABLE_BANK, base + offset as u16), byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One byte of a fake cartridge bank.
|
||||||
|
pub fn rom_byte(&mut self, bank: u8, address: u16, byte: u8) -> &mut Self {
|
||||||
|
self.rom.insert((bank, address), byte);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every stage of both battlers at normal (7), as `InitBattleVariables`-era code leaves them.
|
||||||
|
pub fn normal_stages(&mut self) -> &mut Self {
|
||||||
|
for stat in 0..6 {
|
||||||
|
self.set(ram::wPlayerMonStatMods + stat, 7).set(ram::wEnemyMonStatMods + stat, 7);
|
||||||
|
}
|
||||||
|
for stat in 0..4 {
|
||||||
|
self.set_word_be(ram::wBattleMonAttack + 2 * stat, 12)
|
||||||
|
.set_word_be(ram::wEnemyMonAttack + 2 * stat, 9);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Which tileset the loaded map uses, for the tile-pair collision lists.
|
/// Which tileset the loaded map uses, for the tile-pair collision lists.
|
||||||
pub fn tileset(&mut self, id: u8) -> &mut Self {
|
pub fn tileset(&mut self, id: u8) -> &mut Self {
|
||||||
self.set(ram::wCurMapTileset, id)
|
self.set(ram::wCurMapTileset, id)
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,17 @@ pub trait MacroState: GameState {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the battle engine will answer the fly's move `id` with nothing on this frame: a
|
||||||
|
/// stat stage already at its limit, a status move against a target it cannot affect.
|
||||||
|
///
|
||||||
|
/// Row 60 (`docs/design/macros.md` 12.23), read from the cartridge's own move table and the
|
||||||
|
/// bytes its effect routines test ([`crate::pokemon_red::state::move_without_effect`]). The
|
||||||
|
/// default is `false`: a state that cannot read the table has proved nothing, so the `MOVE n`
|
||||||
|
/// buttons stay where they were.
|
||||||
|
fn move_without_effect(&mut self, _id: u8) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a `GO FRONTIER` on this map has already proved its frontier unreachable.
|
/// Whether a `GO FRONTIER` on this map has already proved its frontier unreachable.
|
||||||
///
|
///
|
||||||
/// [`FrontierLedger`] is the evidence and the measurement. The default is `false`: a state
|
/// [`FrontierLedger`] is the evidence and the measurement. The default is `false`: a state
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,54 @@ pub mod poke {
|
||||||
/// this fixed point.
|
/// this fixed point.
|
||||||
pub const PLAYER_SCREEN_X: i32 = 8;
|
pub const PLAYER_SCREEN_X: i32 = 8;
|
||||||
pub const PLAYER_SCREEN_Y: i32 = 9;
|
pub const PLAYER_SCREEN_Y: i32 = 9;
|
||||||
|
|
||||||
|
/// The cartridge's move table and the battle engine's answers to it (row 60,
|
||||||
|
/// `docs/design/macros.md` 12.23).
|
||||||
|
pub mod moves {
|
||||||
|
/// `data/moves/moves.asm`: `Moves` opens `SECTION "Battle Engine 7"`, which
|
||||||
|
/// `layout.link` places first in ROM bank `$0E`, so the table starts at `$0E:$4000`.
|
||||||
|
/// Six bytes a row (`MOVE_LENGTH`): animation (the move id itself), effect, power,
|
||||||
|
/// type, accuracy, PP, rows in move-id order from `POUND` (1).
|
||||||
|
pub const TABLE_BANK: u8 = 0x0e;
|
||||||
|
pub const TABLE_ADDRESS: u16 = 0x4000;
|
||||||
|
pub const ROW_BYTES: u16 = 6;
|
||||||
|
/// `constants/move_constants.asm`: `NUM_ATTACKS`, `STRUGGLE` (`$a5`) the last.
|
||||||
|
pub const LAST_MOVE: u8 = 0xa5;
|
||||||
|
|
||||||
|
/// `constants/move_effect_constants.asm`: the stat-stage effects, each run in stage
|
||||||
|
/// order ATTACK, DEFENSE, SPEED, SPECIAL, ACCURACY, EVASION.
|
||||||
|
pub const ATTACK_UP1: u8 = 0x0a;
|
||||||
|
pub const EVASION_UP1: u8 = 0x0f;
|
||||||
|
pub const ATTACK_DOWN1: u8 = 0x12;
|
||||||
|
pub const EVASION_DOWN1: u8 = 0x17;
|
||||||
|
pub const SLEEP: u8 = 0x20;
|
||||||
|
pub const ATTACK_UP2: u8 = 0x32;
|
||||||
|
pub const EVASION_UP2: u8 = 0x37;
|
||||||
|
pub const ATTACK_DOWN2: u8 = 0x3a;
|
||||||
|
pub const EVASION_DOWN2: u8 = 0x3f;
|
||||||
|
pub const POISON: u8 = 0x42;
|
||||||
|
pub const PARALYZE: u8 = 0x43;
|
||||||
|
|
||||||
|
/// `constants/battle_constants.asm`: a stage byte is 1 (-6) to `MAX_STAT_LEVEL` 13 (+6),
|
||||||
|
/// 7 normal; `MAX_STAT_VALUE` 999. The first four stages have a stat behind them
|
||||||
|
/// (`wBattleMonAttack` onwards, big-endian words); accuracy and evasion do not.
|
||||||
|
pub const MIN_STAGE: u8 = 1;
|
||||||
|
pub const MAX_STAGE: u8 = 13;
|
||||||
|
pub const STATS_WITH_VALUES: u8 = 4;
|
||||||
|
pub const MIN_STAT: u16 = 1;
|
||||||
|
pub const MAX_STAT: u16 = 999;
|
||||||
|
|
||||||
|
/// `wEnemyBattleStatus2` bits: `PROTECTED_BY_MIST` 1, `HAS_SUBSTITUTE_UP` 4,
|
||||||
|
/// `NEEDS_TO_RECHARGE` 5.
|
||||||
|
pub const MIST: u8 = 1 << 1;
|
||||||
|
pub const SUBSTITUTE: u8 = 1 << 4;
|
||||||
|
pub const RECHARGE: u8 = 1 << 5;
|
||||||
|
|
||||||
|
/// `constants/type_constants.asm`.
|
||||||
|
pub const TYPE_POISON: u8 = 0x03;
|
||||||
|
pub const TYPE_GROUND: u8 = 0x04;
|
||||||
|
pub const TYPE_ELECTRIC: u8 = 0x17;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(memory: &mut dyn MemoryReader, address: u16) -> u8 {
|
fn read(memory: &mut dyn MemoryReader, address: u16) -> u8 {
|
||||||
|
|
@ -625,6 +673,114 @@ fn enemy_mon(memory: &mut dyn MemoryReader) -> Option<EnemyMon> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One row of the cartridge's move table (`data/moves/moves.asm`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct MoveData {
|
||||||
|
pub id: u8,
|
||||||
|
pub effect: u8,
|
||||||
|
pub power: u8,
|
||||||
|
pub kind: u8,
|
||||||
|
pub accuracy: u8,
|
||||||
|
pub pp: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move `id`'s row of the move table, read from the cartridge image.
|
||||||
|
///
|
||||||
|
/// `None` when the seam has no cartridge behind it, when `id` is not a move, or when the row does
|
||||||
|
/// not open with its own id -- every row of `Moves` does (`move`'s first byte is the animation,
|
||||||
|
/// "interchangeable with move id"), so a table that is not where the disassembly puts it answers
|
||||||
|
/// nothing rather than a neighbour's effect.
|
||||||
|
pub fn move_data(memory: &mut dyn MemoryReader, id: u8) -> Option<MoveData> {
|
||||||
|
use poke::moves::{LAST_MOVE, ROW_BYTES, TABLE_ADDRESS, TABLE_BANK};
|
||||||
|
if id == 0 || id > LAST_MOVE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let base = TABLE_ADDRESS + u16::from(id - 1) * ROW_BYTES;
|
||||||
|
let mut row = [0u8; 6];
|
||||||
|
for (offset, byte) in row.iter_mut().enumerate() {
|
||||||
|
*byte = memory.read_rom(TABLE_BANK, base + offset as u16)?;
|
||||||
|
}
|
||||||
|
if row[0] != id {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(MoveData {
|
||||||
|
id,
|
||||||
|
effect: row[1],
|
||||||
|
power: row[2],
|
||||||
|
kind: row[3],
|
||||||
|
accuracy: row[4],
|
||||||
|
pp: row[5],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the battle engine will answer the fly's move `id` with nothing at all, on this frame.
|
||||||
|
///
|
||||||
|
/// Row 60 (`docs/design/macros.md` 12.23): Squirtle's TAIL WHIP against a Pidgey whose DEFENSE
|
||||||
|
/// was already at -6 printed "Nothing happened!" 183 times on Route 1. These are the refusals the
|
||||||
|
/// effect routines in `engine/battle/effects.asm` make on bytes that are already in WRAM when the
|
||||||
|
/// move is chosen, for a move that deals no damage (a move with power always does something):
|
||||||
|
///
|
||||||
|
/// - a stat-raising effect (`StatModifierUpEffect`): the user's stage is already +6, or the stat
|
||||||
|
/// itself is already 999;
|
||||||
|
/// - a stat-lowering effect (`StatModifierDownEffect`, `MoveHitTest`): the target has a
|
||||||
|
/// substitute or Mist, its stage is already -6, or the stat itself is already 1;
|
||||||
|
/// - `SleepEffect`: the target already has a status and is not recharging;
|
||||||
|
/// - `PoisonEffect`: a substitute, a status, or a Poison type;
|
||||||
|
/// - `ParalyzeEffect`: a status, or an Electric move against a Ground type.
|
||||||
|
///
|
||||||
|
/// `Some(false)` for every other move, which the cartridge may still miss -- a miss is a roll,
|
||||||
|
/// and this answers only what is already decided. `None` outside a battle this module
|
||||||
|
/// understands, when the move table cannot be read, or when a stage byte is out of its range:
|
||||||
|
/// a refusal this module cannot read is not one it reports.
|
||||||
|
pub fn move_without_effect(memory: &mut dyn MemoryReader, id: u8) -> Option<bool> {
|
||||||
|
use poke::moves::*;
|
||||||
|
in_battle(memory)?;
|
||||||
|
let data = move_data(memory, id)?;
|
||||||
|
if data.power != 0 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let stage = |memory: &mut dyn MemoryReader, base: u16, stat: u8| -> Option<u8> {
|
||||||
|
Some(read(memory, base + u16::from(stat))).filter(|stage| (MIN_STAGE..=MAX_STAGE).contains(stage))
|
||||||
|
};
|
||||||
|
let value = |memory: &mut dyn MemoryReader, base: u16, stat: u8| -> Option<u16> {
|
||||||
|
(stat < STATS_WITH_VALUES).then(|| word_be(memory, base + 2 * u16::from(stat)))
|
||||||
|
};
|
||||||
|
let effect = data.effect;
|
||||||
|
let raised = match effect {
|
||||||
|
ATTACK_UP1..=EVASION_UP1 => Some(effect - ATTACK_UP1),
|
||||||
|
ATTACK_UP2..=EVASION_UP2 => Some(effect - ATTACK_UP2),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(stat) = raised {
|
||||||
|
let at = stage(memory, ram::wPlayerMonStatMods, stat)?;
|
||||||
|
return Some(at >= MAX_STAGE || value(memory, ram::wBattleMonAttack, stat) == Some(MAX_STAT));
|
||||||
|
}
|
||||||
|
let target = read(memory, ram::wEnemyBattleStatus2);
|
||||||
|
let lowered = match effect {
|
||||||
|
ATTACK_DOWN1..=EVASION_DOWN1 => Some(effect - ATTACK_DOWN1),
|
||||||
|
ATTACK_DOWN2..=EVASION_DOWN2 => Some(effect - ATTACK_DOWN2),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(stat) = lowered {
|
||||||
|
let at = stage(memory, ram::wEnemyMonStatMods, stat)?;
|
||||||
|
return Some(
|
||||||
|
target & (SUBSTITUTE | MIST) != 0
|
||||||
|
|| at <= MIN_STAGE
|
||||||
|
|| value(memory, ram::wEnemyMonAttack, stat) == Some(MIN_STAT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let status = read(memory, ram::wEnemyMonStatus);
|
||||||
|
let types = [read(memory, ram::wEnemyMonType1), read(memory, ram::wEnemyMonType1 + 1)];
|
||||||
|
Some(match effect {
|
||||||
|
SLEEP => status != 0 && target & RECHARGE == 0,
|
||||||
|
POISON => target & SUBSTITUTE != 0 || status != 0 || types.contains(&TYPE_POISON),
|
||||||
|
PARALYZE => {
|
||||||
|
status != 0 || (data.kind == TYPE_ELECTRIC && types.contains(&TYPE_GROUND))
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a text box is open, and whether the bottom-of-screen dialogue box is the one drawn.
|
/// Whether a text box is open, and whether the bottom-of-screen dialogue box is the one drawn.
|
||||||
///
|
///
|
||||||
/// `open` is `wFontLoaded`'s bit 0, which `DisplayTextIDInit` sets for every text display — the
|
/// `open` is `wFontLoaded`'s bit 0, which `DisplayTextIDInit` sets for every text display — the
|
||||||
|
|
@ -1782,6 +1938,10 @@ impl MacroState for PokeState<'_> {
|
||||||
yes_no_prompt(self.memory)
|
yes_no_prompt(self.memory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_without_effect(&mut self, id: u8) -> bool {
|
||||||
|
move_without_effect(self.memory, id).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// The whole loaded map's walkability, from the cache when it is for this map
|
/// The whole loaded map's walkability, from the cache when it is for this map
|
||||||
/// (`docs/design/macros.md` section 15).
|
/// (`docs/design/macros.md` section 15).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1067,3 +1067,158 @@ fn a_sprite_the_cartridge_hides_off_the_screen_is_still_on_the_map() {
|
||||||
let off: Vec<u8> = offscreen_npcs(&mut wram).iter().map(|npc| npc.slot).collect();
|
let off: Vec<u8> = offscreen_npcs(&mut wram).iter().map(|npc| npc.slot).collect();
|
||||||
assert_eq!(off, vec![1], "only the leader is still off the screen from (4, 8)");
|
assert_eq!(off, vec![1], "only the leader is still off the screen from (4, 8)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Row 60's cartridge rows, as `data/moves/moves.asm` has them: `(id, effect, power, type)`.
|
||||||
|
const TACKLE: (u8, u8, u8, u8) = (0x21, 0x00, 35, 0x00);
|
||||||
|
const TAIL_WHIP: (u8, u8, u8, u8) = (0x27, 0x13, 0, 0x00);
|
||||||
|
const GROWL: (u8, u8, u8, u8) = (0x2d, 0x12, 0, 0x00);
|
||||||
|
const SCREECH: (u8, u8, u8, u8) = (0x67, 0x3b, 0, 0x00);
|
||||||
|
const WITHDRAW: (u8, u8, u8, u8) = (0x6e, 0x0b, 0, 0x15);
|
||||||
|
const SAND_ATTACK: (u8, u8, u8, u8) = (0x1c, 0x16, 0, 0x00);
|
||||||
|
const SLEEP_POWDER: (u8, u8, u8, u8) = (0x4f, 0x20, 0, 0x16);
|
||||||
|
const POISONPOWDER: (u8, u8, u8, u8) = (0x4d, 0x42, 0, 0x03);
|
||||||
|
const THUNDER_WAVE: (u8, u8, u8, u8) = (0x56, 0x43, 0, 0x17);
|
||||||
|
/// `AURORA_BEAM`: a damaging move with a stat side effect, which always does something.
|
||||||
|
const AURORA_BEAM: (u8, u8, u8, u8) = (0x3e, 0x44, 65, 0x19);
|
||||||
|
|
||||||
|
fn stage_battle() -> Wram {
|
||||||
|
let mut wram = Wram::new();
|
||||||
|
wram.battle_mon(0, 0xb1, 5, 8, 20, 0, &[(0x21, 35), (0x27, 30)])
|
||||||
|
.enemy_mon(0x24, 2, 13, 13)
|
||||||
|
.battle(1)
|
||||||
|
.normal_stages()
|
||||||
|
.move_table(&[
|
||||||
|
TACKLE,
|
||||||
|
TAIL_WHIP,
|
||||||
|
GROWL,
|
||||||
|
SCREECH,
|
||||||
|
WITHDRAW,
|
||||||
|
SAND_ATTACK,
|
||||||
|
SLEEP_POWDER,
|
||||||
|
POISONPOWDER,
|
||||||
|
THUNDER_WAVE,
|
||||||
|
AURORA_BEAM,
|
||||||
|
]);
|
||||||
|
wram
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_move_table_is_read_by_id_and_checked_against_its_own_first_byte() {
|
||||||
|
let mut wram = stage_battle();
|
||||||
|
let tail_whip = move_data(&mut wram, TAIL_WHIP.0).expect("a row");
|
||||||
|
assert_eq!((tail_whip.effect, tail_whip.power), (0x13, 0));
|
||||||
|
assert_eq!(move_data(&mut wram, 0), None, "no move zero");
|
||||||
|
assert_eq!(move_data(&mut wram, 0xa6), None, "past STRUGGLE");
|
||||||
|
assert_eq!(move_data(&mut wram, 0x22), None, "a row the cartridge image does not answer");
|
||||||
|
// A table that is not where the disassembly says: a row that does not open with its own id
|
||||||
|
// is somebody else's row, and it is not read as this one.
|
||||||
|
let mut shifted = Wram::new();
|
||||||
|
shifted.move_table(&[(0x28, 0x13, 0, 0)]);
|
||||||
|
let base = poke::moves::TABLE_ADDRESS + 0x26 * poke::moves::ROW_BYTES;
|
||||||
|
for offset in 0..6 {
|
||||||
|
let byte = shifted.read_rom(poke::moves::TABLE_BANK, base + 6 + offset).unwrap();
|
||||||
|
shifted.rom_byte(poke::moves::TABLE_BANK, base + offset, byte);
|
||||||
|
}
|
||||||
|
assert_eq!(move_data(&mut shifted, 0x27), None);
|
||||||
|
assert!(move_data(&mut shifted, 0x28).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tail_whip_does_nothing_at_minus_six_and_something_before() {
|
||||||
|
// Row 60: the Pidgey's DEFENSE stage walked down from 7 to 1 by TAIL WHIP after TAIL WHIP.
|
||||||
|
let mut wram = stage_battle();
|
||||||
|
for stage in 2..=7 {
|
||||||
|
wram.set(ram::wEnemyMonStatMods + 1, stage);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), Some(false), "stage {stage}");
|
||||||
|
}
|
||||||
|
wram.set(ram::wEnemyMonStatMods + 1, 1);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), Some(true));
|
||||||
|
// The -2 variant reads the same stage.
|
||||||
|
assert_eq!(move_without_effect(&mut wram, SCREECH.0), Some(true));
|
||||||
|
// TACKLE deals damage whatever the stages say.
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TACKLE.0), Some(false));
|
||||||
|
// And a damaging move with a stat side effect is never "nothing".
|
||||||
|
wram.set(ram::wEnemyMonStatMods + 3, 1);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, AURORA_BEAM.0), Some(false));
|
||||||
|
// GROWL reads ATTACK's stage, not DEFENSE's.
|
||||||
|
assert_eq!(move_without_effect(&mut wram, GROWL.0), Some(false));
|
||||||
|
wram.set(ram::wEnemyMonStatMods, 1);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, GROWL.0), Some(true));
|
||||||
|
// SAND-ATTACK reads ACCURACY's, which has no stat value behind it.
|
||||||
|
assert_eq!(move_without_effect(&mut wram, SAND_ATTACK.0), Some(false));
|
||||||
|
wram.set(ram::wEnemyMonStatMods + 4, 1);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, SAND_ATTACK.0), Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_stat_already_at_one_or_nine_hundred_ninety_nine_refuses_before_the_stage_does() {
|
||||||
|
// `StatModifierDownEffect` restores the stage and prints "Nothing happened!" when the stat
|
||||||
|
// itself is already 1 -- a level-2 Pidgey's DEFENSE gets there before -6.
|
||||||
|
let mut wram = stage_battle();
|
||||||
|
wram.set(ram::wEnemyMonStatMods + 1, 4).set_word_be(ram::wEnemyMonAttack + 2, 1);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), Some(true));
|
||||||
|
wram.set_word_be(ram::wEnemyMonAttack + 2, 2);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), Some(false));
|
||||||
|
// Raising: +6, or a stat of 999.
|
||||||
|
wram.set(ram::wPlayerMonStatMods + 1, 12);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, WITHDRAW.0), Some(false));
|
||||||
|
wram.set(ram::wPlayerMonStatMods + 1, 13);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, WITHDRAW.0), Some(true));
|
||||||
|
wram.set(ram::wPlayerMonStatMods + 1, 9).set_word_be(ram::wBattleMonAttack + 2, 999);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, WITHDRAW.0), Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mist_and_a_substitute_turn_a_stat_lowering_move_away() {
|
||||||
|
let mut wram = stage_battle();
|
||||||
|
wram.set(ram::wEnemyBattleStatus2, poke::moves::MIST);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), Some(true));
|
||||||
|
wram.set(ram::wEnemyBattleStatus2, poke::moves::SUBSTITUTE);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), Some(true));
|
||||||
|
assert_eq!(move_without_effect(&mut wram, POISONPOWDER.0), Some(true));
|
||||||
|
// A raise is the user's own business.
|
||||||
|
assert_eq!(move_without_effect(&mut wram, WITHDRAW.0), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_status_move_against_a_target_it_cannot_affect_does_nothing() {
|
||||||
|
let mut wram = stage_battle();
|
||||||
|
for id in [SLEEP_POWDER.0, POISONPOWDER.0, THUNDER_WAVE.0] {
|
||||||
|
assert_eq!(move_without_effect(&mut wram, id), Some(false), "healthy target, {id:#04x}");
|
||||||
|
}
|
||||||
|
// Any status: already asleep, poisoned, paralysed.
|
||||||
|
wram.set(ram::wEnemyMonStatus, 1 << 6);
|
||||||
|
for id in [SLEEP_POWDER.0, POISONPOWDER.0, THUNDER_WAVE.0] {
|
||||||
|
assert_eq!(move_without_effect(&mut wram, id), Some(true), "statused target, {id:#04x}");
|
||||||
|
}
|
||||||
|
// A target that must recharge is put to sleep whatever its status (`SleepEffect`).
|
||||||
|
wram.set(ram::wEnemyBattleStatus2, poke::moves::RECHARGE);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, SLEEP_POWDER.0), Some(false));
|
||||||
|
// Types: a Poison type is not poisoned; a Ground type is not paralysed by an Electric move.
|
||||||
|
let mut typed = stage_battle();
|
||||||
|
typed.set(ram::wEnemyMonType1 + 1, poke::moves::TYPE_POISON);
|
||||||
|
assert_eq!(move_without_effect(&mut typed, POISONPOWDER.0), Some(true));
|
||||||
|
typed.set(ram::wEnemyMonType1 + 1, 0).set(ram::wEnemyMonType1, poke::moves::TYPE_GROUND);
|
||||||
|
assert_eq!(move_without_effect(&mut typed, THUNDER_WAVE.0), Some(true));
|
||||||
|
assert_eq!(move_without_effect(&mut typed, POISONPOWDER.0), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_refusal_that_cannot_be_read_is_not_reported() {
|
||||||
|
// Out of battle, no cartridge behind the seam, or a stage byte out of its 1..=13 range.
|
||||||
|
let mut wram = stage_battle();
|
||||||
|
wram.set(ram::wIsInBattle, 0);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), None);
|
||||||
|
let mut bare = Wram::new();
|
||||||
|
bare.battle(1).normal_stages().set(ram::wEnemyMonStatMods + 1, 1);
|
||||||
|
assert_eq!(move_without_effect(&mut bare, TAIL_WHIP.0), None, "no move table");
|
||||||
|
let mut wram = stage_battle();
|
||||||
|
wram.set(ram::wEnemyMonStatMods + 1, 0);
|
||||||
|
assert_eq!(move_without_effect(&mut wram, TAIL_WHIP.0), None);
|
||||||
|
// And the seam's own answer is "leave the button where it was".
|
||||||
|
let mut unread = Wram::new();
|
||||||
|
assert!(!PokeState::new(&mut unread).move_without_effect(TAIL_WHIP.0));
|
||||||
|
let mut read = stage_battle();
|
||||||
|
read.set(ram::wEnemyMonStatMods + 1, 1);
|
||||||
|
assert!(PokeState::new(&mut read).move_without_effect(TAIL_WHIP.0));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue