mirror of
https://github.com/Ninjdai1/pokeemerald.git
synced 2024-12-26 19:54:21 +01:00
Test moves, items, and abilities in battle
Thank you to SBird for providing mgba-rom-test binaries and Spikes/Toxic Spikes tests! Co-authored-by: sbird <sbird@no.tld>
This commit is contained in:
parent
c11774acb8
commit
f1b9872bf0
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
repository: pret/agbcc
|
||||
|
||||
- name: Install binutils
|
||||
run: sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi
|
||||
run: sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi libelf-dev
|
||||
# build-essential, git, and libpng-dev are already installed
|
||||
# gcc-arm-none-eabi is only needed for the modern build
|
||||
# as an alternative to dkP
|
||||
@ -41,10 +41,15 @@ jobs:
|
||||
working-directory: agbcc
|
||||
|
||||
- name: Agbcc
|
||||
run: make -j${nproc} all
|
||||
run: make -j${nproc} -O all
|
||||
|
||||
- name: Modern
|
||||
env:
|
||||
MODERN: 1
|
||||
COMPARE: 0
|
||||
run: make -j${nproc} all
|
||||
run: make -j${nproc} -O all
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
make -j${nproc} -O pokeemerald-test.elf
|
||||
make -j${nproc} check
|
||||
|
51
Makefile
51
Makefile
@ -79,6 +79,9 @@ ELF = $(ROM:.gba=.elf)
|
||||
MAP = $(ROM:.gba=.map)
|
||||
SYM = $(ROM:.gba=.sym)
|
||||
|
||||
TESTELF = $(ROM:.gba=-test.elf)
|
||||
HEADLESSELF = $(ROM:.gba=-test-headless.elf)
|
||||
|
||||
C_SUBDIR = src
|
||||
GFLIB_SUBDIR = gflib
|
||||
ASM_SUBDIR = asm
|
||||
@ -88,6 +91,7 @@ SONG_SUBDIR = sound/songs
|
||||
MID_SUBDIR = sound/songs/midi
|
||||
SAMPLE_SUBDIR = sound/direct_sound_samples
|
||||
CRY_SUBDIR = sound/direct_sound_samples/cries
|
||||
TEST_SUBDIR = test
|
||||
|
||||
C_BUILDDIR = $(OBJ_DIR)/$(C_SUBDIR)
|
||||
GFLIB_BUILDDIR = $(OBJ_DIR)/$(GFLIB_SUBDIR)
|
||||
@ -95,6 +99,7 @@ ASM_BUILDDIR = $(OBJ_DIR)/$(ASM_SUBDIR)
|
||||
DATA_ASM_BUILDDIR = $(OBJ_DIR)/$(DATA_ASM_SUBDIR)
|
||||
SONG_BUILDDIR = $(OBJ_DIR)/$(SONG_SUBDIR)
|
||||
MID_BUILDDIR = $(OBJ_DIR)/$(MID_SUBDIR)
|
||||
TEST_BUILDDIR = $(OBJ_DIR)/$(TEST_SUBDIR)
|
||||
|
||||
ASFLAGS := -mcpu=arm7tdmi --defsym MODERN=$(MODERN)
|
||||
|
||||
@ -131,10 +136,13 @@ RAMSCRGEN := tools/ramscrgen/ramscrgen$(EXE)
|
||||
FIX := tools/gbafix/gbafix$(EXE)
|
||||
MAPJSON := tools/mapjson/mapjson$(EXE)
|
||||
JSONPROC := tools/jsonproc/jsonproc$(EXE)
|
||||
PATCHELF := tools/patchelf/patchelf$(EXE)
|
||||
ROMTEST ?= $(shell { command -v mgba-rom-test || command -v tools/mgba/mgba-rom-test$(EXE); } 2>/dev/null)
|
||||
ROMTESTHYDRA := tools/mgba-rom-test-hydra/mgba-rom-test-hydra$(EXE)
|
||||
|
||||
PERL := perl
|
||||
|
||||
TOOLDIRS := $(filter-out tools/agbcc tools/binutils,$(wildcard tools/*))
|
||||
TOOLDIRS := $(filter-out tools/mgba tools/agbcc tools/binutils,$(wildcard tools/*))
|
||||
TOOLBASE = $(TOOLDIRS:tools/%=%)
|
||||
TOOLS = $(foreach tool,$(TOOLBASE),tools/$(tool)/$(tool)$(EXE))
|
||||
|
||||
@ -150,7 +158,7 @@ MAKEFLAGS += --no-print-directory
|
||||
# Secondary expansion is required for dependency variables in object rules.
|
||||
.SECONDEXPANSION:
|
||||
|
||||
.PHONY: all rom clean compare tidy tools mostlyclean clean-tools $(TOOLDIRS) libagbsyscall modern tidymodern tidynonmodern
|
||||
.PHONY: all rom clean compare tidy tools mostlyclean clean-tools $(TOOLDIRS) libagbsyscall modern tidymodern tidynonmodern check
|
||||
|
||||
infoshell = $(foreach line, $(shell $1 | sed "s/ /__SPACE__/g"), $(info $(subst __SPACE__, ,$(line))))
|
||||
|
||||
@ -158,7 +166,7 @@ infoshell = $(foreach line, $(shell $1 | sed "s/ /__SPACE__/g"), $(info $(subst
|
||||
# Disable dependency scanning for clean/tidy/tools
|
||||
# Use a separate minimal makefile for speed
|
||||
# Since we don't need to reload most of this makefile
|
||||
ifeq (,$(filter-out all rom compare modern libagbsyscall syms,$(MAKECMDGOALS)))
|
||||
ifeq (,$(filter-out all rom compare modern check libagbsyscall syms,$(MAKECMDGOALS)))
|
||||
$(call infoshell, $(MAKE) -f make_tools.mk)
|
||||
else
|
||||
NODEP ?= 1
|
||||
@ -182,6 +190,11 @@ C_SRCS_IN := $(wildcard $(C_SUBDIR)/*.c $(C_SUBDIR)/*/*.c $(C_SUBDIR)/*/*/*.c)
|
||||
C_SRCS := $(foreach src,$(C_SRCS_IN),$(if $(findstring .inc.c,$(src)),,$(src)))
|
||||
C_OBJS := $(patsubst $(C_SUBDIR)/%.c,$(C_BUILDDIR)/%.o,$(C_SRCS))
|
||||
|
||||
TEST_SRCS_IN := $(wildcard $(TEST_SUBDIR)/*.c $(TEST_SUBDIR)/*/*.c $(TEST_SUBDIR)/*/*/*.c)
|
||||
TEST_SRCS := $(foreach src,$(TEST_SRCS_IN),$(if $(findstring .inc.c,$(src)),,$(src)))
|
||||
TEST_OBJS := $(patsubst $(TEST_SUBDIR)/%.c,$(TEST_BUILDDIR)/%.o,$(TEST_SRCS))
|
||||
TEST_OBJS_REL := $(patsubst $(OBJ_DIR)/%,%,$(TEST_OBJS))
|
||||
|
||||
GFLIB_SRCS := $(wildcard $(GFLIB_SUBDIR)/*.c)
|
||||
GFLIB_OBJS := $(patsubst $(GFLIB_SUBDIR)/%.c,$(GFLIB_BUILDDIR)/%.o,$(GFLIB_SRCS))
|
||||
|
||||
@ -206,7 +219,7 @@ MID_OBJS := $(patsubst $(MID_SUBDIR)/%.mid,$(MID_BUILDDIR)/%.o,$(MID_SRCS))
|
||||
OBJS := $(C_OBJS) $(GFLIB_OBJS) $(C_ASM_OBJS) $(ASM_OBJS) $(DATA_ASM_OBJS) $(SONG_OBJS) $(MID_OBJS)
|
||||
OBJS_REL := $(patsubst $(OBJ_DIR)/%,%,$(OBJS))
|
||||
|
||||
SUBDIRS := $(sort $(dir $(OBJS)))
|
||||
SUBDIRS := $(sort $(dir $(OBJS) $(dir $(TEST_OBJS))))
|
||||
$(shell mkdir -p $(SUBDIRS))
|
||||
endif
|
||||
|
||||
@ -407,6 +420,14 @@ $(OBJ_DIR)/sym_common.ld: sym_common.txt $(C_OBJS) $(wildcard common_syms/*.txt)
|
||||
$(OBJ_DIR)/sym_ewram.ld: sym_ewram.txt
|
||||
$(RAMSCRGEN) ewram_data $< ENGLISH > $@
|
||||
|
||||
# NOTE: Based on C_DEP above, but without NODEP and KEEP_TEMPS handling.
|
||||
define TEST_DEP
|
||||
$1: $2 $$(shell $(SCANINC) -I include -I tools/agbcc/include -I gflib -I test $2)
|
||||
@echo "$$(CC1) <flags> -o $$@ $$<"
|
||||
@$$(CPP) $$(CPPFLAGS) $$< | $$(PREPROC) $$< charmap.txt -i | $$(CC1) $$(CFLAGS) -o - - | cat - <(echo -e ".text\n\t.align\t2, 0") | $$(AS) $$(ASFLAGS) -o $$@ -
|
||||
endef
|
||||
$(foreach src, $(TEST_SRCS), $(eval $(call TEST_DEP,$(patsubst $(TEST_SUBDIR)/%.c,$(TEST_BUILDDIR)/%.o,$(src)),$(src),$(patsubst $(TEST_SUBDIR)/%.c,%,$(src)))))
|
||||
|
||||
ifeq ($(MODERN),0)
|
||||
LD_SCRIPT := ld_script.txt
|
||||
LD_SCRIPT_DEPS := $(OBJ_DIR)/sym_bss.ld $(OBJ_DIR)/sym_common.ld $(OBJ_DIR)/sym_ewram.ld
|
||||
@ -429,6 +450,28 @@ $(ROM): $(ELF)
|
||||
|
||||
modern: all
|
||||
|
||||
LD_SCRIPT_TEST := ld_script_test.txt
|
||||
|
||||
$(OBJ_DIR)/ld_script_test.ld: $(LD_SCRIPT_TEST) $(LD_SCRIPT_DEPS)
|
||||
cd $(OBJ_DIR) && sed "s#tools/#../../tools/#g" ../../$(LD_SCRIPT_TEST) > ld_script_test.ld
|
||||
|
||||
$(TESTELF): $(OBJ_DIR)/ld_script_test.ld $(OBJS) $(TEST_OBJS) libagbsyscall
|
||||
@echo "cd $(OBJ_DIR) && $(LD) -T ld_script_test.ld -o ../../$@ <objects> <test-objects> <lib>"
|
||||
@cd $(OBJ_DIR) && $(LD) $(TESTLDFLAGS) -T ld_script_test.ld -o ../../$@ $(OBJS_REL) $(TEST_OBJS_REL) $(LIB)
|
||||
$(FIX) $@ -t"$(TITLE)" -c$(GAME_CODE) -m$(MAKER_CODE) -r$(REVISION) --silent
|
||||
$(PATCHELF) pokeemerald-test.elf gTestRunnerArgv "$(TESTS)\0"
|
||||
|
||||
ifeq ($(GITHUB_REPOSITORY_OWNER),rh-hideout)
|
||||
TEST_SKIP_IS_FAIL := \x01
|
||||
else
|
||||
TEST_SKIP_IS_FAIL := \x00
|
||||
endif
|
||||
|
||||
check: $(TESTELF)
|
||||
@cp $< $(HEADLESSELF)
|
||||
$(PATCHELF) $(HEADLESSELF) gTestRunnerHeadless '\x01' gTestRunnerSkipIsFail "$(TEST_SKIP_IS_FAIL)"
|
||||
$(ROMTESTHYDRA) $(ROMTEST) $(HEADLESSELF)
|
||||
|
||||
libagbsyscall:
|
||||
@$(MAKE) -C libagbsyscall TOOLCHAIN=$(TOOLCHAIN) MODERN=$(MODERN)
|
||||
|
||||
|
@ -7,3 +7,4 @@ gIntrTable
|
||||
gLinkVSyncDisabled
|
||||
IntrMain_Buffer
|
||||
gPcmDmaCounter
|
||||
gAgbMainLoop_sp
|
||||
|
@ -139,6 +139,14 @@
|
||||
#define NUM_FLAG_BYTES ROUND_BITS_TO_BYTES(FLAGS_COUNT)
|
||||
#define NUM_ADDITIONAL_PHRASE_BYTES ROUND_BITS_TO_BYTES(NUM_ADDITIONAL_PHRASES)
|
||||
|
||||
// Calls m0/m1/.../m8 depending on how many arguments are passed.
|
||||
#define VARARG_8(m, ...) CAT(m, NARG_8(__VA_ARGS__))(__VA_ARGS__)
|
||||
#define NARG_8(...) NARG_8_(_, ##__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0)
|
||||
#define NARG_8_(_, a, b, c, d, e, f, g, h, N, ...) N
|
||||
|
||||
#define CAT(a, b) CAT_(a, b)
|
||||
#define CAT_(a, b) a ## b
|
||||
|
||||
// This produces an error at compile-time if expr is zero.
|
||||
// It looks like file.c:line: size of array `id' is negative
|
||||
#define STATIC_ASSERT(expr, id) typedef char id[(expr) ? 1 : -1];
|
||||
|
@ -57,6 +57,7 @@ extern u32 IntrMain_Buffer[];
|
||||
extern s8 gPcmDmaCounter;
|
||||
|
||||
void AgbMain(void);
|
||||
void AgbMainLoop(void);
|
||||
void SetMainCallback2(MainCallback callback);
|
||||
void InitKeys(void);
|
||||
void SetVBlankCallback(IntrCallback callback);
|
||||
|
@ -563,5 +563,6 @@ u16 MonTryLearningNewMoveEvolution(struct Pokemon *mon, bool8 firstMove);
|
||||
bool32 ShouldShowFemaleDifferences(u16 species, u32 personality);
|
||||
void TryToSetBattleFormChangeMoves(struct Pokemon *mon);
|
||||
u32 GetMonFriendshipScore(struct Pokemon *pokemon);
|
||||
void UpdateMonPersonality(struct BoxPokemon *boxMon, u32 personality);
|
||||
|
||||
#endif // GUARD_POKEMON_H
|
||||
|
@ -1,6 +1,51 @@
|
||||
#ifndef GUARD_RECORDED_BATTLE_H
|
||||
#define GUARD_RECORDED_BATTLE_H
|
||||
|
||||
#include "constants/battle.h"
|
||||
|
||||
#define BATTLER_RECORD_SIZE 664
|
||||
|
||||
struct RecordedBattleSave
|
||||
{
|
||||
struct Pokemon playerParty[PARTY_SIZE];
|
||||
struct Pokemon opponentParty[PARTY_SIZE];
|
||||
u8 playersName[MAX_BATTLERS_COUNT][PLAYER_NAME_LENGTH + 1];
|
||||
u8 playersGender[MAX_BATTLERS_COUNT];
|
||||
u32 playersTrainerId[MAX_BATTLERS_COUNT];
|
||||
u8 playersLanguage[MAX_BATTLERS_COUNT];
|
||||
u32 rngSeed;
|
||||
u32 battleFlags;
|
||||
u8 playersBattlers[MAX_BATTLERS_COUNT];
|
||||
u16 opponentA;
|
||||
u16 opponentB;
|
||||
u16 partnerId;
|
||||
u16 multiplayerId;
|
||||
u8 lvlMode;
|
||||
u8 frontierFacility;
|
||||
u8 frontierBrainSymbol;
|
||||
u8 battleScene:1;
|
||||
u8 textSpeed:3;
|
||||
u32 AI_scripts;
|
||||
u8 recordMixFriendName[PLAYER_NAME_LENGTH + 1];
|
||||
u8 recordMixFriendClass;
|
||||
u8 apprenticeId;
|
||||
u16 easyChatSpeech[EASY_CHAT_BATTLE_WORDS_COUNT];
|
||||
u8 recordMixFriendLanguage;
|
||||
u8 apprenticeLanguage;
|
||||
u8 battleRecord[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
|
||||
u32 checksum;
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
RECORDED_BYTE, // Generic.
|
||||
RECORDED_ACTION_TYPE,
|
||||
RECORDED_MOVE_SLOT,
|
||||
RECORDED_MOVE_TARGET,
|
||||
RECORDED_PARTY_INDEX,
|
||||
RECORDED_BATTLE_PALACE_ACTION,
|
||||
};
|
||||
|
||||
extern u32 gRecordedBattleRngSeed;
|
||||
extern u32 gBattlePalaceMoveSelectionRngValue;
|
||||
extern u8 gRecordedBattleMultiplayerId;
|
||||
@ -12,11 +57,12 @@ void RecordedBattle_Init(u8 mode);
|
||||
void RecordedBattle_SetTrainerInfo(void);
|
||||
void RecordedBattle_SetBattlerAction(u8 battlerId, u8 action);
|
||||
void RecordedBattle_ClearBattlerAction(u8 battlerId, u8 bytesToClear);
|
||||
u8 RecordedBattle_GetBattlerAction(u8 battlerId);
|
||||
u8 RecordedBattle_GetBattlerAction(u32 actionType, u8 battlerId);
|
||||
u8 RecordedBattle_BufferNewBattlerData(u8 *dst);
|
||||
void RecordedBattle_RecordAllBattlerData(u8 *data);
|
||||
bool32 CanCopyRecordedBattleSaveData(void);
|
||||
bool32 MoveRecordedBattleToSaveData(void);
|
||||
void SetVariablesForRecordedBattle(struct RecordedBattleSave *);
|
||||
void PlayRecordedBattle(void (*CB2_After)(void));
|
||||
u8 GetRecordedBattleFrontierFacility(void);
|
||||
u8 GetRecordedBattleFronterBrainSymbol(void);
|
||||
|
17
include/test_runner.h
Normal file
17
include/test_runner.h
Normal file
@ -0,0 +1,17 @@
|
||||
#ifndef GUARD_TEST_RUNNER_H
|
||||
#define GUARD_TEST_RUNNER_H
|
||||
|
||||
extern const bool8 gTestRunnerEnabled;
|
||||
extern const bool8 gTestRunnerHeadless;
|
||||
extern const bool8 gTestRunnerSkipIsFail;
|
||||
|
||||
void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability);
|
||||
void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId);
|
||||
void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP);
|
||||
void TestRunner_Battle_RecordMessage(const u8 *message);
|
||||
void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1);
|
||||
void TestRunner_Battle_AfterLastTurn(void);
|
||||
|
||||
void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType);
|
||||
|
||||
#endif
|
@ -2,6 +2,7 @@ ENTRY(Start)
|
||||
|
||||
gNumMusicPlayers = 4;
|
||||
gMaxLines = 0;
|
||||
gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup;
|
||||
|
||||
SECTIONS {
|
||||
. = 0x2000000;
|
||||
|
@ -2,6 +2,7 @@ ENTRY(Start)
|
||||
|
||||
gNumMusicPlayers = 4;
|
||||
gMaxLines = 0;
|
||||
gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup;
|
||||
|
||||
SECTIONS {
|
||||
. = 0x2000000;
|
||||
|
140
ld_script_test.txt
Normal file
140
ld_script_test.txt
Normal file
@ -0,0 +1,140 @@
|
||||
ENTRY(Start)
|
||||
|
||||
gNumMusicPlayers = 4;
|
||||
gMaxLines = 0;
|
||||
gInitialMainCB2 = CB2_TestRunner;
|
||||
|
||||
SECTIONS {
|
||||
. = 0x2000000;
|
||||
|
||||
ewram (NOLOAD) :
|
||||
ALIGN(4)
|
||||
{
|
||||
gHeap = .;
|
||||
|
||||
. = 0x1C000;
|
||||
|
||||
src/*.o(ewram_data);
|
||||
gflib/*.o(ewram_data);
|
||||
test/*.o(ewram_data);
|
||||
|
||||
. = 0x40000;
|
||||
}
|
||||
|
||||
. = 0x3000000;
|
||||
|
||||
iwram (NOLOAD) :
|
||||
ALIGN(4)
|
||||
{
|
||||
/* .bss starts at 0x3000000 */
|
||||
src/*.o(.bss);
|
||||
gflib/*.o(.bss);
|
||||
data/*.o(.bss);
|
||||
test/*.o(.bss);
|
||||
*libc.a:*.o(.bss*);
|
||||
*libgcc.a:*.o(.bss*);
|
||||
*libnosys.a:*.o(.bss*);
|
||||
|
||||
/* .bss.code starts at 0x3001AA8 */
|
||||
src/m4a.o(.bss.code);
|
||||
|
||||
/* COMMON starts at 0x30022A8 */
|
||||
src/*.o(COMMON);
|
||||
gflib/*.o(COMMON);
|
||||
data/*.o(COMMON);
|
||||
test/*.o(COMMON);
|
||||
*libc.a:sbrkr.o(COMMON);
|
||||
end = .;
|
||||
. = 0x8000;
|
||||
}
|
||||
|
||||
. = 0x8000000;
|
||||
|
||||
.text :
|
||||
ALIGN(4)
|
||||
{
|
||||
src/rom_header.o(.text);
|
||||
src/rom_header_gf.o(.text.*);
|
||||
src/*.o(.text);
|
||||
gflib/*.o(.text);
|
||||
} =0
|
||||
|
||||
script_data :
|
||||
ALIGN(4)
|
||||
{
|
||||
data/*.o(script_data);
|
||||
} =0
|
||||
|
||||
lib_text :
|
||||
ALIGN(4)
|
||||
{
|
||||
*libagbsyscall.a:*.o(.text*);
|
||||
*libgcc.a:*.o(.text*);
|
||||
*libc.a:*.o(.text*);
|
||||
*libnosys.a:*.o(.text*);
|
||||
} =0
|
||||
|
||||
.rodata :
|
||||
ALIGN(4)
|
||||
{
|
||||
src/*.o(.rodata);
|
||||
gflib/*.o(.rodata);
|
||||
data/*.o(.rodata);
|
||||
} =0
|
||||
|
||||
song_data :
|
||||
ALIGN(4)
|
||||
{
|
||||
sound/songs/*.o(.rodata);
|
||||
} =0
|
||||
|
||||
lib_rodata :
|
||||
SUBALIGN(4)
|
||||
{
|
||||
*libgcc.a:*.o(.rodata*);
|
||||
*libc.a:*.o(.rodata*);
|
||||
*libc.a:*.o(.data*);
|
||||
src/libisagbprn.o(.rodata);
|
||||
} =0
|
||||
|
||||
tests :
|
||||
ALIGN(4)
|
||||
{
|
||||
__start_tests = .;
|
||||
test/*.o(.tests);
|
||||
__stop_tests = .;
|
||||
test/*.o(.text);
|
||||
test/*.o(.rodata);
|
||||
} =0
|
||||
|
||||
/* DWARF debug sections.
|
||||
Symbols in the DWARF debugging sections are relative to the beginning
|
||||
of the section so we begin them at 0. */
|
||||
|
||||
/* DWARF 1 */
|
||||
.debug 0 : { *(.debug) }
|
||||
.line 0 : { *(.line) }
|
||||
|
||||
/* GNU DWARF 1 extensions */
|
||||
.debug_srcinfo 0 : { *(.debug_srcinfo) }
|
||||
.debug_sfnames 0 : { *(.debug_sfnames) }
|
||||
|
||||
/* DWARF 1.1 and DWARF 2 */
|
||||
.debug_aranges 0 : { *(.debug_aranges) }
|
||||
.debug_pubnames 0 : { *(.debug_pubnames) }
|
||||
|
||||
/* DWARF 2 */
|
||||
.debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) }
|
||||
.debug_abbrev 0 : { *(.debug_abbrev) }
|
||||
.debug_line 0 : { *(.debug_line) }
|
||||
.debug_frame 0 : { *(.debug_frame) }
|
||||
.debug_str 0 : { *(.debug_str) }
|
||||
.debug_loc 0 : { *(.debug_loc) }
|
||||
.debug_macinfo 0 : { *(.debug_macinfo) }
|
||||
|
||||
/* Discard everything not specifically mentioned above. */
|
||||
/DISCARD/ :
|
||||
{
|
||||
*(*);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
|
||||
MAKEFLAGS += --no-print-directory
|
||||
|
||||
TOOLDIRS := $(filter-out tools/agbcc tools/binutils,$(wildcard tools/*))
|
||||
TOOLDIRS := $(filter-out tools/mgba tools/agbcc tools/binutils,$(wildcard tools/*))
|
||||
|
||||
.PHONY: all $(TOOLDIRS)
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include "sound.h"
|
||||
#include "sprite.h"
|
||||
#include "task.h"
|
||||
#include "test_runner.h"
|
||||
#include "constants/battle_anim.h"
|
||||
#include "constants/moves.h"
|
||||
|
||||
@ -217,12 +218,27 @@ void DoMoveAnim(u16 move)
|
||||
LaunchBattleAnimation(ANIM_TYPE_MOVE, move);
|
||||
}
|
||||
|
||||
static void Nop(void)
|
||||
{
|
||||
}
|
||||
|
||||
void LaunchBattleAnimation(u32 animType, u32 animId)
|
||||
{
|
||||
s32 i;
|
||||
const u8 *const *animsTable;
|
||||
bool32 hideHpBoxes;
|
||||
|
||||
if (gTestRunnerEnabled)
|
||||
{
|
||||
TestRunner_Battle_RecordAnimation(animType, animId);
|
||||
if (gTestRunnerHeadless)
|
||||
{
|
||||
gAnimScriptCallback = Nop;
|
||||
gAnimScriptActive = FALSE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (animType)
|
||||
{
|
||||
case ANIM_TYPE_GENERAL:
|
||||
@ -239,7 +255,7 @@ void LaunchBattleAnimation(u32 animType, u32 animId)
|
||||
break;
|
||||
}
|
||||
|
||||
hideHpBoxes = !(animType == ANIM_TYPE_MOVE && animId == MOVE_TRANSFORM);
|
||||
hideHpBoxes = !(animType == ANIM_TYPE_MOVE && animId == MOVE_TRANSFORM);
|
||||
if (animType != ANIM_TYPE_MOVE)
|
||||
{
|
||||
switch (animId)
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "sound.h"
|
||||
#include "string_util.h"
|
||||
#include "task.h"
|
||||
#include "test_runner.h"
|
||||
#include "text.h"
|
||||
#include "util.h"
|
||||
#include "window.h"
|
||||
@ -1386,6 +1387,17 @@ static void RecordedOpponentHandlePrintString(void)
|
||||
gBattle_BG0_Y = 0;
|
||||
stringId = (u16 *)(&gBattleResources->bufferA[gActiveBattler][2]);
|
||||
BufferStringBattle(*stringId);
|
||||
|
||||
if (gTestRunnerEnabled)
|
||||
{
|
||||
TestRunner_Battle_RecordMessage(gDisplayedStringBattle);
|
||||
if (gTestRunnerHeadless)
|
||||
{
|
||||
RecordedOpponentBufferExecCompleted();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG);
|
||||
gBattlerControllerFuncs[gActiveBattler] = CompleteOnInactiveTextPrinter;
|
||||
}
|
||||
@ -1397,7 +1409,7 @@ static void RecordedOpponentHandlePrintSelectionString(void)
|
||||
|
||||
static void RecordedOpponentHandleChooseAction(void)
|
||||
{
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0);
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_ACTION_TYPE, gActiveBattler), 0);
|
||||
RecordedOpponentBufferExecCompleted();
|
||||
}
|
||||
|
||||
@ -1414,8 +1426,8 @@ static void RecordedOpponentHandleChooseMove(void)
|
||||
}
|
||||
else
|
||||
{
|
||||
u8 moveId = RecordedBattle_GetBattlerAction(gActiveBattler);
|
||||
u8 target = RecordedBattle_GetBattlerAction(gActiveBattler);
|
||||
u8 moveId = RecordedBattle_GetBattlerAction(RECORDED_MOVE_SLOT, gActiveBattler);
|
||||
u8 target = RecordedBattle_GetBattlerAction(RECORDED_MOVE_TARGET, gActiveBattler);
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, 10, moveId | (target << 8));
|
||||
}
|
||||
|
||||
@ -1429,7 +1441,7 @@ static void RecordedOpponentHandleChooseItem(void)
|
||||
|
||||
static void RecordedOpponentHandleChoosePokemon(void)
|
||||
{
|
||||
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(gActiveBattler);
|
||||
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(RECORDED_PARTY_INDEX, gActiveBattler);
|
||||
BtlController_EmitChosenMonReturnValue(BUFFER_B, *(gBattleStruct->monToSwitchIntoId + gActiveBattler), NULL);
|
||||
RecordedOpponentBufferExecCompleted();
|
||||
}
|
||||
@ -1442,22 +1454,23 @@ static void RecordedOpponentHandleCmd23(void)
|
||||
static void RecordedOpponentHandleHealthBarUpdate(void)
|
||||
{
|
||||
s16 hpVal;
|
||||
s32 maxHP, curHP;
|
||||
|
||||
LoadBattleBarGfx(0);
|
||||
hpVal = gBattleResources->bufferA[gActiveBattler][2] | (gBattleResources->bufferA[gActiveBattler][3] << 8);
|
||||
|
||||
maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
|
||||
curHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
|
||||
|
||||
if (hpVal != INSTANT_HP_BAR_DROP)
|
||||
{
|
||||
u32 maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
|
||||
u32 curHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
|
||||
|
||||
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, curHP, hpVal);
|
||||
TestRunner_Battle_RecordHP(gActiveBattler, curHP, min(maxHP, max(0, curHP - hpVal)));
|
||||
}
|
||||
else
|
||||
{
|
||||
u32 maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
|
||||
|
||||
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, 0, hpVal);
|
||||
TestRunner_Battle_RecordHP(gActiveBattler, curHP, 0);
|
||||
}
|
||||
|
||||
gBattlerControllerFuncs[gActiveBattler] = CompleteOnHealthbarDone;
|
||||
@ -1478,6 +1491,9 @@ static void RecordedOpponentHandleStatusIconUpdate(void)
|
||||
battlerId = gActiveBattler;
|
||||
gBattleSpritesDataPtr->healthBoxesData[battlerId].statusAnimActive = 0;
|
||||
gBattlerControllerFuncs[gActiveBattler] = CompleteOnFinishedStatusAnimation;
|
||||
|
||||
if (gTestRunnerEnabled)
|
||||
TestRunner_Battle_RecordStatus1(battlerId, GetMonData(&gEnemyParty[gBattlerPartyIndexes[battlerId]], MON_DATA_STATUS));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "sound.h"
|
||||
#include "string_util.h"
|
||||
#include "task.h"
|
||||
#include "test_runner.h"
|
||||
#include "text.h"
|
||||
#include "util.h"
|
||||
#include "window.h"
|
||||
@ -1394,6 +1395,17 @@ static void RecordedPlayerHandlePrintString(void)
|
||||
gBattle_BG0_Y = 0;
|
||||
stringId = (u16 *)(&gBattleResources->bufferA[gActiveBattler][2]);
|
||||
BufferStringBattle(*stringId);
|
||||
|
||||
if (gTestRunnerEnabled)
|
||||
{
|
||||
TestRunner_Battle_RecordMessage(gDisplayedStringBattle);
|
||||
if (gTestRunnerHeadless)
|
||||
{
|
||||
RecordedPlayerBufferExecCompleted();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG);
|
||||
gBattlerControllerFuncs[gActiveBattler] = CompleteOnInactiveTextPrinter;
|
||||
}
|
||||
@ -1407,7 +1419,7 @@ static void ChooseActionInBattlePalace(void)
|
||||
{
|
||||
if (gBattleCommunication[4] >= gBattlersCount / 2)
|
||||
{
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0);
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_BATTLE_PALACE_ACTION, gActiveBattler), 0);
|
||||
RecordedPlayerBufferExecCompleted();
|
||||
}
|
||||
}
|
||||
@ -1420,7 +1432,7 @@ static void RecordedPlayerHandleChooseAction(void)
|
||||
}
|
||||
else
|
||||
{
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0);
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_ACTION_TYPE, gActiveBattler), 0);
|
||||
RecordedPlayerBufferExecCompleted();
|
||||
}
|
||||
}
|
||||
@ -1438,8 +1450,8 @@ static void RecordedPlayerHandleChooseMove(void)
|
||||
}
|
||||
else
|
||||
{
|
||||
u8 moveId = RecordedBattle_GetBattlerAction(gActiveBattler);
|
||||
u8 target = RecordedBattle_GetBattlerAction(gActiveBattler);
|
||||
u8 moveId = RecordedBattle_GetBattlerAction(RECORDED_MOVE_SLOT, gActiveBattler);
|
||||
u8 target = RecordedBattle_GetBattlerAction(RECORDED_MOVE_TARGET, gActiveBattler);
|
||||
BtlController_EmitTwoReturnValues(BUFFER_B, 10, moveId | (target << 8));
|
||||
}
|
||||
|
||||
@ -1453,7 +1465,7 @@ static void RecordedPlayerHandleChooseItem(void)
|
||||
|
||||
static void RecordedPlayerHandleChoosePokemon(void)
|
||||
{
|
||||
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(gActiveBattler);
|
||||
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(RECORDED_PARTY_INDEX, gActiveBattler);
|
||||
BtlController_EmitChosenMonReturnValue(BUFFER_B, *(gBattleStruct->monToSwitchIntoId + gActiveBattler), NULL);
|
||||
RecordedPlayerBufferExecCompleted();
|
||||
}
|
||||
@ -1466,23 +1478,24 @@ static void RecordedPlayerHandleCmd23(void)
|
||||
static void RecordedPlayerHandleHealthBarUpdate(void)
|
||||
{
|
||||
s16 hpVal;
|
||||
s32 maxHP, curHP;
|
||||
|
||||
LoadBattleBarGfx(0);
|
||||
hpVal = gBattleResources->bufferA[gActiveBattler][2] | (gBattleResources->bufferA[gActiveBattler][3] << 8);
|
||||
|
||||
maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
|
||||
curHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
|
||||
|
||||
if (hpVal != INSTANT_HP_BAR_DROP)
|
||||
{
|
||||
u32 maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
|
||||
u32 curHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
|
||||
|
||||
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, curHP, hpVal);
|
||||
TestRunner_Battle_RecordHP(gActiveBattler, curHP, min(maxHP, max(0, curHP - hpVal)));
|
||||
}
|
||||
else
|
||||
{
|
||||
u32 maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
|
||||
|
||||
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, 0, hpVal);
|
||||
UpdateHpTextInHealthbox(gHealthboxSpriteIds[gActiveBattler], HP_CURRENT, 0, maxHP);
|
||||
TestRunner_Battle_RecordHP(gActiveBattler, curHP, 0);
|
||||
}
|
||||
|
||||
gBattlerControllerFuncs[gActiveBattler] = CompleteOnHealthbarDone;
|
||||
@ -1503,6 +1516,9 @@ static void RecordedPlayerHandleStatusIconUpdate(void)
|
||||
battlerId = gActiveBattler;
|
||||
gBattleSpritesDataPtr->healthBoxesData[battlerId].statusAnimActive = 0;
|
||||
gBattlerControllerFuncs[gActiveBattler] = CompleteOnFinishedStatusAnimation;
|
||||
|
||||
if (gTestRunnerEnabled)
|
||||
TestRunner_Battle_RecordStatus1(battlerId, GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerId]], MON_DATA_STATUS));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@
|
||||
#include "item.h"
|
||||
#include "item_icon.h"
|
||||
#include "item_use.h"
|
||||
#include "test_runner.h"
|
||||
#include "constants/battle_anim.h"
|
||||
#include "constants/rgb.h"
|
||||
#include "constants/songs.h"
|
||||
@ -3072,6 +3073,13 @@ void CreateAbilityPopUp(u8 battlerId, u32 ability, bool32 isDoubleBattle)
|
||||
const s16 (*coords)[2];
|
||||
u8 spriteId1, spriteId2, battlerPosition, taskId;
|
||||
|
||||
if (gTestRunnerEnabled)
|
||||
{
|
||||
TestRunner_Battle_RecordAbilityPopUp(battlerId, ability);
|
||||
if (gTestRunnerHeadless)
|
||||
return;
|
||||
}
|
||||
|
||||
if (gBattleScripting.abilityPopupOverwrite != 0)
|
||||
ability = gBattleScripting.abilityPopupOverwrite;
|
||||
|
||||
@ -3187,9 +3195,12 @@ static void SpriteCb_AbilityPopUp(struct Sprite *sprite)
|
||||
|
||||
void DestroyAbilityPopUp(u8 battlerId)
|
||||
{
|
||||
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][0]].tFrames = 0;
|
||||
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][1]].tFrames = 0;
|
||||
gBattleScripting.fixedPopup = FALSE;
|
||||
if (gBattleStruct->activeAbilityPopUps & gBitTable[battlerId])
|
||||
{
|
||||
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][0]].tFrames = 0;
|
||||
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][1]].tFrames = 0;
|
||||
gBattleScripting.fixedPopup = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static void Task_FreeAbilityPopUpGfx(u8 taskId)
|
||||
|
@ -45,6 +45,7 @@
|
||||
#include "string_util.h"
|
||||
#include "strings.h"
|
||||
#include "task.h"
|
||||
#include "test_runner.h"
|
||||
#include "text.h"
|
||||
#include "trig.h"
|
||||
#include "tv.h"
|
||||
@ -474,7 +475,8 @@ const u8 *const gStatusConditionStringsTable[][2] =
|
||||
|
||||
void CB2_InitBattle(void)
|
||||
{
|
||||
MoveSaveBlocks_ResetHeap();
|
||||
if (!gTestRunnerEnabled)
|
||||
MoveSaveBlocks_ResetHeap();
|
||||
AllocateBattleResources();
|
||||
AllocateBattleSpritesData();
|
||||
AllocateMonSpritesGfx();
|
||||
@ -1805,6 +1807,8 @@ void CB2_QuitRecordedBattle(void)
|
||||
{
|
||||
m4aMPlayStop(&gMPlayInfo_SE1);
|
||||
m4aMPlayStop(&gMPlayInfo_SE2);
|
||||
if (gTestRunnerEnabled)
|
||||
TestRunner_Battle_AfterLastTurn();
|
||||
FreeRestoreBattleData();
|
||||
FreeAllWindowBuffers();
|
||||
SetMainCallback2(gMain.savedCallback);
|
||||
@ -5233,6 +5237,8 @@ static void HandleEndTurn_FinishBattle(void)
|
||||
}
|
||||
|
||||
RecordedBattle_SetPlaybackFinished();
|
||||
if (gTestRunnerEnabled)
|
||||
TestRunner_Battle_AfterLastTurn();
|
||||
BeginFastPaletteFade(3);
|
||||
FadeOutMapMusic(5);
|
||||
#if B_TRAINERS_KNOCK_OFF_ITEMS == TRUE
|
||||
|
@ -524,11 +524,11 @@ static const u8 sText_Trainer2LoseText[];
|
||||
static const s8 sText_EnduredViaSturdy[] = _("{B_DEF_NAME_WITH_PREFIX} endured\nthe hit using {B_DEF_ABILITY}!");
|
||||
static const s8 sText_PowerHerbActivation[] = _("{B_ATK_NAME_WITH_PREFIX} became fully charged\ndue to its {B_LAST_ITEM}!");
|
||||
static const s8 sText_HurtByItem[] = _("{B_ATK_NAME_WITH_PREFIX} was hurt\nby its {B_LAST_ITEM}!");
|
||||
static const s8 sText_BadlyPoisonedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was badly \npoisoned by the {B_LAST_ITEM}!");
|
||||
static const s8 sText_BadlyPoisonedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was badly\npoisoned by the {B_LAST_ITEM}!");
|
||||
static const s8 sText_BurnedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was burned\nby the {B_LAST_ITEM}!");
|
||||
static const s8 sText_TargetAbilityActivates[] = _("{B_DEF_NAME_WITH_PREFIX}'s {B_DEF_ABILITY} activates!");
|
||||
static const u8 sText_GravityIntensified[] = _("Gravity intensified!");
|
||||
static const u8 sText_TargetIdentified[] = _("{B_DEF_NAME_WITH_PREFIX} was \nidentified!");
|
||||
static const u8 sText_TargetIdentified[] = _("{B_DEF_NAME_WITH_PREFIX} was\nidentified!");
|
||||
static const u8 sText_TargetWokeUp[] = _("{B_DEF_NAME_WITH_PREFIX} woke up!");
|
||||
static const u8 sText_PkmnStoleAndAteItem[] = _("{B_ATK_NAME_WITH_PREFIX} stole and\nate {B_DEF_NAME_WITH_PREFIX}'s {B_LAST_ITEM}!");
|
||||
static const u8 sText_TailWindBlew[] = _("The tailwind blew from\nbehind {B_ATK_TEAM2} team!");
|
||||
@ -597,7 +597,7 @@ static const u8 sText_TargetsStatWasMaxedOut[] = _("{B_DEF_NAME_WITH_PREFIX}'s {
|
||||
static const u8 sText_PoisonHealHpUp[] = _("The poisoning healed {B_ATK_NAME_WITH_PREFIX}\na little bit!");
|
||||
static const u8 sText_BadDreamsDmg[] = _("{B_DEF_NAME_WITH_PREFIX} is tormented!");
|
||||
static const u8 sText_MoldBreakerEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} breaks the mold!");
|
||||
static const u8 sText_TeravoltEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating \na bursting aura!");
|
||||
static const u8 sText_TeravoltEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating\na bursting aura!");
|
||||
static const u8 sText_TurboblazeEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating\na blazing aura!");
|
||||
static const u8 sText_SlowStartEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} can't get it going!");
|
||||
static const u8 sText_SlowStartEnd[] = _("{B_ATK_NAME_WITH_PREFIX} finally got\nits act together!");
|
||||
@ -642,7 +642,7 @@ static const u8 sText_TargetElectrified[] = _("The {B_DEF_NAME_WITH_PREFIX}'s mo
|
||||
static const u8 sText_AssaultVestDoesntAllow[] = _("{B_LAST_ITEM}'s effects prevent\nstatus moves from being used!\p");
|
||||
static const u8 sText_GravityPreventsUsage[] = _("{B_ATK_NAME_WITH_PREFIX} can't use {B_CURRENT_MOVE}\nbecause of gravity!\p");
|
||||
static const u8 sText_HealBlockPreventsUsage[] = _("{B_ATK_NAME_WITH_PREFIX} was\nprevented from healing!\p");
|
||||
static const u8 sText_MegaEvoReacting[] = _("{B_ATK_NAME_WITH_PREFIX}'s {B_LAST_ITEM} is \nreacting to {B_ATK_TRAINER_NAME}'s Mega Ring!");
|
||||
static const u8 sText_MegaEvoReacting[] = _("{B_ATK_NAME_WITH_PREFIX}'s {B_LAST_ITEM} is\nreacting to {B_ATK_TRAINER_NAME}'s Mega Ring!");
|
||||
static const u8 sText_FerventWishReached[] = _("{B_ATK_TRAINER_NAME}'s fervent wish\nhas reached {B_ATK_NAME_WITH_PREFIX}!");
|
||||
static const u8 sText_MegaEvoEvolved[] = _("{B_ATK_NAME_WITH_PREFIX} has Mega Evolved into\nMega {B_BUFF1}!");
|
||||
static const u8 sText_drastically[] = _("drastically ");
|
||||
|
@ -134,7 +134,7 @@ static const u8 sWireless_ASCIItoRSETable[256] = {
|
||||
0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94
|
||||
};
|
||||
|
||||
static const u8 sWireless_RSEtoASCIITable[256] = {
|
||||
const u8 gWireless_RSEtoASCIITable[256] = {
|
||||
[CHAR_SPACE] = ' ',
|
||||
0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
|
||||
0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95,
|
||||
@ -612,7 +612,7 @@ static void PkmnStrToASCII(u8 *asciiStr, const u8 *pkmnStr)
|
||||
s32 i;
|
||||
|
||||
for (i = 0; pkmnStr[i] != EOS; i++)
|
||||
asciiStr[i] = sWireless_RSEtoASCIITable[pkmnStr[i]];
|
||||
asciiStr[i] = gWireless_RSEtoASCIITable[pkmnStr[i]];
|
||||
asciiStr[i] = 0;
|
||||
}
|
||||
|
||||
|
12
src/main.c
12
src/main.c
@ -31,6 +31,9 @@ static void VCountIntr(void);
|
||||
static void SerialIntr(void);
|
||||
static void IntrDummy(void);
|
||||
|
||||
// Defined in the linker script so that the test build can override it.
|
||||
extern void gInitialMainCB2(void);
|
||||
|
||||
const u8 gGameVersion = GAME_VERSION;
|
||||
|
||||
const u8 gGameLanguage = GAME_LANGUAGE; // English
|
||||
@ -68,6 +71,7 @@ IntrFunc gIntrTable[INTR_COUNT];
|
||||
u8 gLinkVSyncDisabled;
|
||||
u32 IntrMain_Buffer[0x200];
|
||||
s8 gPcmDmaCounter;
|
||||
void *gAgbMainLoop_sp;
|
||||
|
||||
static EWRAM_DATA u16 sTrainerId = 0;
|
||||
|
||||
@ -126,6 +130,12 @@ void AgbMain()
|
||||
AGBPrintfInit();
|
||||
#endif
|
||||
#endif
|
||||
gAgbMainLoop_sp = __builtin_frame_address(0);
|
||||
AgbMainLoop();
|
||||
}
|
||||
|
||||
void AgbMainLoop(void)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
ReadKeys();
|
||||
@ -178,7 +188,7 @@ static void InitMainCallbacks(void)
|
||||
gTrainerHillVBlankCounter = NULL;
|
||||
gMain.vblankCounter2 = 0;
|
||||
gMain.callback1 = NULL;
|
||||
SetMainCallback2(CB2_InitCopyrightScreenAfterBootup);
|
||||
SetMainCallback2(gInitialMainCB2);
|
||||
gSaveBlock2Ptr = &gSaveblock2.block;
|
||||
gPokemonStoragePtr = &gPokemonStorage.block;
|
||||
}
|
||||
|
@ -8681,3 +8681,32 @@ u32 GetMonFriendshipScore(struct Pokemon *pokemon)
|
||||
|
||||
return FRIENDSHIP_NONE;
|
||||
}
|
||||
|
||||
void UpdateMonPersonality(struct BoxPokemon *boxMon, u32 personality)
|
||||
{
|
||||
struct PokemonSubstruct0 *old0, *new0;
|
||||
struct PokemonSubstruct1 *old1, *new1;
|
||||
struct PokemonSubstruct2 *old2, *new2;
|
||||
struct PokemonSubstruct3 *old3, *new3;
|
||||
struct BoxPokemon old;
|
||||
|
||||
old = *boxMon;
|
||||
old0 = &(GetSubstruct(&old, old.personality, 0)->type0);
|
||||
old1 = &(GetSubstruct(&old, old.personality, 1)->type1);
|
||||
old2 = &(GetSubstruct(&old, old.personality, 2)->type2);
|
||||
old3 = &(GetSubstruct(&old, old.personality, 3)->type3);
|
||||
|
||||
new0 = &(GetSubstruct(boxMon, personality, 0)->type0);
|
||||
new1 = &(GetSubstruct(boxMon, personality, 1)->type1);
|
||||
new2 = &(GetSubstruct(boxMon, personality, 2)->type2);
|
||||
new3 = &(GetSubstruct(boxMon, personality, 3)->type3);
|
||||
|
||||
DecryptBoxMon(&old);
|
||||
boxMon->personality = personality;
|
||||
*new0 = *old0;
|
||||
*new1 = *old1;
|
||||
*new2 = *old2;
|
||||
*new3 = *old3;
|
||||
boxMon->checksum = CalculateBoxMonChecksum(boxMon);
|
||||
EncryptBoxMon(boxMon);
|
||||
}
|
||||
|
@ -14,14 +14,13 @@
|
||||
#include "malloc.h"
|
||||
#include "util.h"
|
||||
#include "task.h"
|
||||
#include "test_runner.h"
|
||||
#include "text.h"
|
||||
#include "battle_setup.h"
|
||||
#include "frontier_util.h"
|
||||
#include "constants/trainers.h"
|
||||
#include "constants/rgb.h"
|
||||
|
||||
#define BATTLER_RECORD_SIZE 664
|
||||
|
||||
struct PlayerInfo
|
||||
{
|
||||
u32 trainerId;
|
||||
@ -31,37 +30,6 @@ struct PlayerInfo
|
||||
u16 language;
|
||||
};
|
||||
|
||||
struct RecordedBattleSave
|
||||
{
|
||||
struct Pokemon playerParty[PARTY_SIZE];
|
||||
struct Pokemon opponentParty[PARTY_SIZE];
|
||||
u8 playersName[MAX_BATTLERS_COUNT][PLAYER_NAME_LENGTH + 1];
|
||||
u8 playersGender[MAX_BATTLERS_COUNT];
|
||||
u32 playersTrainerId[MAX_BATTLERS_COUNT];
|
||||
u8 playersLanguage[MAX_BATTLERS_COUNT];
|
||||
u32 rngSeed;
|
||||
u32 battleFlags;
|
||||
u8 playersBattlers[MAX_BATTLERS_COUNT];
|
||||
u16 opponentA;
|
||||
u16 opponentB;
|
||||
u16 partnerId;
|
||||
u16 multiplayerId;
|
||||
u8 lvlMode;
|
||||
u8 frontierFacility;
|
||||
u8 frontierBrainSymbol;
|
||||
u8 battleScene:1;
|
||||
u8 textSpeed:3;
|
||||
u32 AI_scripts;
|
||||
u8 recordMixFriendName[PLAYER_NAME_LENGTH + 1];
|
||||
u8 recordMixFriendClass;
|
||||
u8 apprenticeId;
|
||||
u16 easyChatSpeech[EASY_CHAT_BATTLE_WORDS_COUNT];
|
||||
u8 recordMixFriendLanguage;
|
||||
u8 apprenticeLanguage;
|
||||
u8 battleRecord[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
|
||||
u32 checksum;
|
||||
};
|
||||
|
||||
// Save data using TryWriteSpecialSaveSector is allowed to exceed SECTOR_DATA_SIZE (up to the counter field)
|
||||
STATIC_ASSERT(sizeof(struct RecordedBattleSave) <= SECTOR_COUNTER_OFFSET, RecordedBattleSaveFreeSpace);
|
||||
|
||||
@ -205,8 +173,11 @@ void RecordedBattle_ClearBattlerAction(u8 battlerId, u8 bytesToClear)
|
||||
}
|
||||
}
|
||||
|
||||
u8 RecordedBattle_GetBattlerAction(u8 battlerId)
|
||||
u8 RecordedBattle_GetBattlerAction(u32 actionType, u8 battlerId)
|
||||
{
|
||||
if (gTestRunnerEnabled)
|
||||
BattleTest_CheckBattleRecordActionType(battlerId, sBattlerRecordSizes[battlerId], actionType);
|
||||
|
||||
// Trying to read past array or invalid action byte, battle is over.
|
||||
if (sBattlerRecordSizes[battlerId] >= BATTLER_RECORD_SIZE || sBattleRecords[battlerId][sBattlerRecordSizes[battlerId]] == 0xFF)
|
||||
{
|
||||
@ -522,7 +493,7 @@ static void Task_StartAfterCountdown(u8 taskId)
|
||||
}
|
||||
}
|
||||
|
||||
static void SetVariablesForRecordedBattle(struct RecordedBattleSave *src)
|
||||
void SetVariablesForRecordedBattle(struct RecordedBattleSave *src)
|
||||
{
|
||||
bool8 var;
|
||||
s32 i, j;
|
||||
@ -755,14 +726,14 @@ void RecordedBattle_CheckMovesetChanges(u8 mode)
|
||||
|
||||
// We know the current action is ACTION_MOVE_CHANGE, retrieve
|
||||
// it without saving it to move on to the next action.
|
||||
RecordedBattle_GetBattlerAction(battlerId);
|
||||
RecordedBattle_GetBattlerAction(RECORDED_BYTE, battlerId);
|
||||
|
||||
for (j = 0; j < MAX_MON_MOVES; j++)
|
||||
ppBonuses[j] = ((gBattleMons[battlerId].ppBonuses & (3 << (j << 1))) >> (j << 1));
|
||||
|
||||
for (j = 0; j < MAX_MON_MOVES; j++)
|
||||
{
|
||||
moveSlots[j] = RecordedBattle_GetBattlerAction(battlerId);
|
||||
moveSlots[j] = RecordedBattle_GetBattlerAction(RECORDED_BYTE, battlerId);
|
||||
movePp.moves[j] = gBattleMons[battlerId].moves[moveSlots[j]];
|
||||
movePp.currentPp[j] = gBattleMons[battlerId].pp[moveSlots[j]];
|
||||
movePp.maxPp[j] = ppBonuses[moveSlots[j]];
|
||||
|
46
src/test_runner_stub.c
Normal file
46
src/test_runner_stub.c
Normal file
@ -0,0 +1,46 @@
|
||||
#include "global.h"
|
||||
#include "test_runner.h"
|
||||
|
||||
__attribute__((weak))
|
||||
const bool8 gTestRunnerEnabled = FALSE;
|
||||
|
||||
// The Makefile patches gTestRunnerHeadless as part of make test.
|
||||
// This allows us to open the ROM in an mgba with a UI and see the
|
||||
// animations and messages play, which helps when debugging a test.
|
||||
const bool8 gTestRunnerHeadless = FALSE;
|
||||
const bool8 gTestRunnerSkipIsFail = FALSE;
|
||||
|
||||
__attribute__((weak))
|
||||
void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability)
|
||||
{
|
||||
}
|
||||
|
||||
__attribute__((weak))
|
||||
void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId)
|
||||
{
|
||||
}
|
||||
|
||||
__attribute__((weak))
|
||||
void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP)
|
||||
{
|
||||
}
|
||||
|
||||
__attribute__((weak))
|
||||
void TestRunner_Battle_RecordMessage(const u8 *string)
|
||||
{
|
||||
}
|
||||
|
||||
__attribute__((weak))
|
||||
void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1)
|
||||
{
|
||||
}
|
||||
|
||||
__attribute__((weak))
|
||||
void TestRunner_Battle_AfterLastTurn(void)
|
||||
{
|
||||
}
|
||||
|
||||
__attribute__((weak))
|
||||
void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType)
|
||||
{
|
||||
}
|
20
test/ability_blaze.c
Normal file
20
test/ability_blaze.c
Normal file
@ -0,0 +1,20 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Blaze boosts Fire-type moves in a pinch", s16 damage)
|
||||
{
|
||||
u16 hp;
|
||||
PARAMETRIZE { hp = 99; }
|
||||
PARAMETRIZE { hp = 33; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE);
|
||||
PLAYER(SPECIES_CHARMANDER) { Ability(ABILITY_BLAZE); MaxHP(99); HP(hp); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_EMBER); }
|
||||
} SCENE {
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_GT(results[1].damage, results[0].damage);
|
||||
}
|
||||
}
|
48
test/ability_cute_charm.c
Normal file
48
test/ability_cute_charm.c
Normal file
@ -0,0 +1,48 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
// TODO: Currently PASSES_RANDOMLY is incapable of testing Cute Charm
|
||||
// because it only activates 33% of the time, but we only want to
|
||||
// measure the 50% of the time that the infatuation prevents our move.
|
||||
SINGLE_BATTLE_TEST("Cute Charm inflicts infatuation on contact")
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_TACKLE; }
|
||||
PARAMETRIZE { move = MOVE_SWIFT; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
|
||||
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
|
||||
PLAYER(SPECIES_WOBBUFFET) { Gender(MON_MALE); }
|
||||
OPPONENT(SPECIES_CLEFAIRY) { Gender(MON_FEMALE); Ability(ABILITY_CUTE_CHARM); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); }
|
||||
TURN { MOVE(player, move); }
|
||||
} SCENE {
|
||||
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
|
||||
ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_INFATUATION, player);
|
||||
MESSAGE("Foe Clefairy's Cute Charm infatuated Wobbuffet!");
|
||||
MESSAGE("Wobbuffet is in love with Foe Clefairy!");
|
||||
} else {
|
||||
NOT ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_INFATUATION, player);
|
||||
NOT MESSAGE("Foe Clefairy's Cute Charm infatuated Wobbuffet!");
|
||||
NOT MESSAGE("Wobbuffet is in love with Foe Clefairy!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Cute Charm cannot infatuate same gender")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Gender(MON_MALE); }
|
||||
OPPONENT(SPECIES_CLEFAIRY) { Gender(MON_MALE); Ability(ABILITY_CUTE_CHARM); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TACKLE); }
|
||||
TURN { MOVE(player, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
|
||||
NOT ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
|
||||
}
|
||||
}
|
29
test/ability_flame_body.c
Normal file
29
test/ability_flame_body.c
Normal file
@ -0,0 +1,29 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Flame Body inflicts burn on contact")
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_TACKLE; }
|
||||
PARAMETRIZE { move = MOVE_SWIFT; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
|
||||
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_MAGMAR) { Ability(ABILITY_FLAME_BODY); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); }
|
||||
} SCENE {
|
||||
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
|
||||
ABILITY_POPUP(opponent, ABILITY_FLAME_BODY);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, player);
|
||||
MESSAGE("Foe Magmar's Flame Body burned Wobbuffet!");
|
||||
STATUS_ICON(player, burn: TRUE);
|
||||
} else {
|
||||
NOT ABILITY_POPUP(opponent, ABILITY_FLAME_BODY);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, player);
|
||||
NOT MESSAGE("Foe Magmar's Flame Body burned Wobbuffet!");
|
||||
NOT STATUS_ICON(player, burn: TRUE);
|
||||
}
|
||||
}
|
||||
}
|
47
test/ability_immunity.c
Normal file
47
test/ability_immunity.c
Normal file
@ -0,0 +1,47 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Immunity prevents Poison Sting poison")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_POISON_STING); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Immunity prevents Toxic bad poison")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet used Toxic!");
|
||||
ABILITY_POPUP(opponent, ABILITY_IMMUNITY);
|
||||
MESSAGE("Foe Snorlax's Immunity prevents poisoning!");
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Immunity prevents Toxic Spikes poison")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
} SCENE {
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
169
test/ability_pastel_veil.c
Normal file
169
test/ability_pastel_veil.c
Normal file
@ -0,0 +1,169 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Pastel Veil prevents Poison Sting poison")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_POISON_STING); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("Pastel Veil prevents Poison Sting poison on partner")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(playerLeft, MOVE_POISON_STING, target: opponentRight); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, playerLeft);
|
||||
NOT STATUS_ICON(opponentRight, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Pastel Veil immediately cures Mold Breaker poison")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
|
||||
PLAYER(SPECIES_DRILBUR) { Ability(ABILITY_MOLD_BREAKER); }
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
|
||||
STATUS_ICON(opponent, badPoison: TRUE);
|
||||
ABILITY_POPUP(opponent, ABILITY_PASTEL_VEIL);
|
||||
MESSAGE("Foe Ponyta's Pastel Veil cured its poison problem!");
|
||||
STATUS_ICON(opponent, none: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("Pastel Veil does not cure Mold Breaker poison on partner")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
|
||||
PLAYER(SPECIES_DRILBUR) { Ability(ABILITY_MOLD_BREAKER); }
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(playerLeft, MOVE_TOXIC, target: opponentRight); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, playerLeft, target: opponentRight);
|
||||
STATUS_ICON(opponentRight, badPoison: TRUE);
|
||||
NOT STATUS_ICON(opponentRight, none: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Pastel Veil prevents Toxic bad poison")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet used Toxic!");
|
||||
ABILITY_POPUP(opponent, ABILITY_PASTEL_VEIL);
|
||||
MESSAGE("Foe Ponyta is protected by a pastel veil!");
|
||||
NOT STATUS_ICON(opponent, badPoison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("Pastel Veil prevents Toxic bad poison on partner")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(playerLeft, MOVE_TOXIC, target: opponentRight); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet used Toxic!");
|
||||
ABILITY_POPUP(opponentLeft, ABILITY_PASTEL_VEIL);
|
||||
MESSAGE("Foe Wynaut is protected by a pastel veil!");
|
||||
NOT STATUS_ICON(opponentRight, badPoison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Pastel Veil prevents Toxic Spikes poison")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
} SCENE {
|
||||
MESSAGE("2 sent out Ponyta!");
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("Pastel Veil prevents Toxic Spikes poison on partner")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(playerLeft, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponentRight, 2); }
|
||||
} SCENE {
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
NOT STATUS_ICON(opponentRight, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("Pastel Veil cures partner's poison on initial switch in")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
} WHEN {
|
||||
TURN {}
|
||||
} SCENE {
|
||||
MESSAGE("2 sent out Wobbuffet and Ponyta!");
|
||||
ABILITY_POPUP(opponentRight, ABILITY_PASTEL_VEIL);
|
||||
MESSAGE("Foe Wobbuffet was cured of its poisoning!");
|
||||
STATUS_ICON(opponentLeft, none: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("Pastel Veil cures partner's poison on switch in")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
|
||||
} WHEN {
|
||||
TURN { SWITCH(opponentRight, 2); }
|
||||
} SCENE {
|
||||
MESSAGE("2 sent out Ponyta!");
|
||||
ABILITY_POPUP(opponentRight, ABILITY_PASTEL_VEIL);
|
||||
MESSAGE("Foe Wobbuffet was cured of its poisoning!");
|
||||
STATUS_ICON(opponentLeft, none: TRUE);
|
||||
}
|
||||
}
|
30
test/ability_poison_point.c
Normal file
30
test/ability_poison_point.c
Normal file
@ -0,0 +1,30 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Poison Point inflicts poison on contact")
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_TACKLE; }
|
||||
PARAMETRIZE { move = MOVE_SWIFT; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
|
||||
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_NIDORAN_M) { Ability(ABILITY_POISON_POINT); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); }
|
||||
TURN {}
|
||||
} SCENE {
|
||||
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
|
||||
ABILITY_POPUP(opponent, ABILITY_POISON_POINT);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, player);
|
||||
MESSAGE("Wobbuffet was poisoned by Foe Nidoran♂'s Poison Point!");
|
||||
STATUS_ICON(player, poison: TRUE);
|
||||
} else {
|
||||
NOT ABILITY_POPUP(opponent, ABILITY_POISON_POINT);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, player);
|
||||
NOT MESSAGE("Wobbuffet was poisoned by Foe Nidoran♂'s Poison Point!");
|
||||
NOT STATUS_ICON(player, poison: TRUE);
|
||||
}
|
||||
}
|
||||
}
|
29
test/ability_static.c
Normal file
29
test/ability_static.c
Normal file
@ -0,0 +1,29 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Static inflicts paralysis on contact")
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_TACKLE; }
|
||||
PARAMETRIZE { move = MOVE_SWIFT; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
|
||||
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_PIKACHU) { Ability(ABILITY_STATIC); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); }
|
||||
} SCENE {
|
||||
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
|
||||
ABILITY_POPUP(opponent, ABILITY_STATIC);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, player);
|
||||
MESSAGE("Foe Pikachu's Static paralyzed Wobbuffet! It may be unable to move!");
|
||||
STATUS_ICON(player, paralysis: TRUE);
|
||||
} else {
|
||||
NOT ABILITY_POPUP(opponent, ABILITY_STATIC);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, player);
|
||||
NOT MESSAGE("Foe Pikachu's Static paralyzed Wobbuffet! It may be unable to move!");
|
||||
NOT STATUS_ICON(player, paralysis: TRUE);
|
||||
}
|
||||
}
|
||||
}
|
47
test/ability_sturdy.c
Normal file
47
test/ability_sturdy.c
Normal file
@ -0,0 +1,47 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Sturdy prevents OHKO moves")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_FISSURE].effect == EFFECT_OHKO);
|
||||
PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_FISSURE); }
|
||||
} SCENE {
|
||||
MESSAGE("Foe Wobbuffet used Fissure!");
|
||||
ABILITY_POPUP(player, ABILITY_STURDY);
|
||||
MESSAGE("Geodude was protected by Sturdy!");
|
||||
} THEN {
|
||||
EXPECT_EQ(player->hp, player->maxHP);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Sturdy prevents OHKOs")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); MaxHP(100); HP(100); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_SEISMIC_TOSS); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SEISMIC_TOSS, opponent);
|
||||
HP_BAR(player, hp: 1);
|
||||
ABILITY_POPUP(player, ABILITY_STURDY);
|
||||
MESSAGE("Geodude endured the hit using Sturdy!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Sturdy does not prevent non-OHKOs")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); MaxHP(100); HP(99); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_SEISMIC_TOSS); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SEISMIC_TOSS, opponent);
|
||||
HP_BAR(player, hp: 0);
|
||||
}
|
||||
}
|
54
test/hold_effect_leftovers.c
Normal file
54
test/hold_effect_leftovers.c
Normal file
@ -0,0 +1,54 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
gItems[ITEM_LEFTOVERS].holdEffect == HOLD_EFFECT_LEFTOVERS;
|
||||
};
|
||||
|
||||
SINGLE_BATTLE_TEST("Leftovers recovers 1/16th HP at end of turn")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { MaxHP(100); HP(1); Item(ITEM_LEFTOVERS); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN {}
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player);
|
||||
MESSAGE("Wobbuffet's Leftovers restored its HP a little!");
|
||||
HP_BAR(player, damage: -maxHP / 16);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Leftovers does nothing if max HP")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_LEFTOVERS); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN {}
|
||||
} SCENE {
|
||||
NONE_OF {
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player);
|
||||
MESSAGE("Wobbuffet's Leftovers restored its HP a little!");
|
||||
HP_BAR(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Leftovers does nothing if Heal Block applies")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { MaxHP(100); HP(1); Item(ITEM_LEFTOVERS); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_HEAL_BLOCK); }
|
||||
} SCENE {
|
||||
NONE_OF {
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player);
|
||||
MESSAGE("Wobbuffet's Leftovers restored its HP a little!");
|
||||
HP_BAR(player);
|
||||
}
|
||||
}
|
||||
}
|
68
test/mega_evolution.c
Normal file
68
test/mega_evolution.c
Normal file
@ -0,0 +1,68 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Venusaur can Mega Evolve holding Venusaurite")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_VENUSAUR) { Item(ITEM_VENUSAURITE); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
|
||||
} SCENE {
|
||||
MESSAGE("Venusaur's Venusaurite is reacting to 1's Mega Ring!");
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, player);
|
||||
MESSAGE("Venusaur has Mega Evolved into Mega Venusaur!");
|
||||
} THEN {
|
||||
EXPECT_EQ(player->species, SPECIES_VENUSAUR_MEGA);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Rayquaza can Mega Evolve knowing Dragon Ascent")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_RAYQUAZA) { Moves(MOVE_DRAGON_ASCENT, MOVE_CELEBRATE); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
|
||||
} SCENE {
|
||||
MESSAGE("1's fervent wish has reached Rayquaza!");
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, player);
|
||||
MESSAGE("Rayquaza has Mega Evolved into Mega Rayquaza!");
|
||||
} THEN {
|
||||
EXPECT_EQ(player->species, SPECIES_RAYQUAZA_MEGA);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Mega Evolution affects turn order")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_MEGA_EVO_TURN_ORDER);
|
||||
PLAYER(SPECIES_DIANCIE) { Item(ITEM_DIANCITE); Speed(105); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(106); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
|
||||
} SCENE {
|
||||
MESSAGE("Diancie used Celebrate!");
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
} THEN {
|
||||
ASSUME(player->speed == 225);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Abilities replaced by Mega Evolution do not affect turn order")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_MEGA_EVO_TURN_ORDER);
|
||||
ASSUME(gSpeciesInfo[SPECIES_SABLEYE_MEGA].abilities[0] != ABILITY_STALL
|
||||
&& gSpeciesInfo[SPECIES_SABLEYE_MEGA].abilities[1] != ABILITY_STALL);
|
||||
PLAYER(SPECIES_SABLEYE) { Item(ITEM_SABLENITE); Ability(ABILITY_STALL); Speed(105); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(44); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
|
||||
} SCENE {
|
||||
MESSAGE("Sableye used Celebrate!");
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
} THEN {
|
||||
ASSUME(player->speed == 45);
|
||||
}
|
||||
}
|
158
test/move.c
Normal file
158
test/move.c
Normal file
@ -0,0 +1,158 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Accuracy controls the proportion of misses")
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_DYNAMIC_PUNCH; }
|
||||
PARAMETRIZE { move = MOVE_THUNDER; }
|
||||
PARAMETRIZE { move = MOVE_HYDRO_PUMP; }
|
||||
PARAMETRIZE { move = MOVE_RAZOR_LEAF; }
|
||||
PARAMETRIZE { move = MOVE_SCRATCH; }
|
||||
ASSUME(0 < gBattleMoves[move].accuracy && gBattleMoves[move].accuracy <= 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, move, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Secondary Effect Chance controls the proportion of secondary effects")
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_THUNDER_SHOCK; }
|
||||
PARAMETRIZE { move = MOVE_DISCHARGE; }
|
||||
PARAMETRIZE { move = MOVE_NUZZLE; }
|
||||
ASSUME(gBattleMoves[move].accuracy == 100);
|
||||
ASSUME(gBattleMoves[move].effect == EFFECT_PARALYZE_HIT);
|
||||
ASSUME(0 < gBattleMoves[move].secondaryEffectChance && gBattleMoves[move].secondaryEffectChance <= 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[move].secondaryEffectChance, 100);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); }
|
||||
} SCENE {
|
||||
STATUS_ICON(opponent, paralysis: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Turn order is determined by priority")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_QUICK_ATTACK].priority > gBattleMoves[MOVE_TACKLE].priority);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Turn order is determined by speed if priority ties")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Speed(2); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(1); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_QUICK_ATTACK); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and speed tie")
|
||||
{
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(1); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_QUICK_ATTACK); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Critical hits occur at a 1/24 rate")
|
||||
{
|
||||
ASSUME(B_CRIT_CHANCE >= GEN_7);
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
|
||||
PASSES_RANDOMLY(100 / 24, 100);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SCRATCH); }
|
||||
} SCENE {
|
||||
MESSAGE("It's a critical hit!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Critical hits deal 50% more damage", s16 damage)
|
||||
{
|
||||
bool32 criticalHit;
|
||||
PARAMETRIZE { criticalHit = FALSE; }
|
||||
PARAMETRIZE { criticalHit = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(B_CRIT_MULTIPLIER >= GEN_6);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SCRATCH, criticalHit: criticalHit); }
|
||||
} SCENE {
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Critical hits do not ignore positive stat stages", s16 damage)
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_CELEBRATE; }
|
||||
PARAMETRIZE { move = MOVE_HOWL; }
|
||||
PARAMETRIZE { move = MOVE_TAIL_WHIP; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].split == SPLIT_PHYSICAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); }
|
||||
TURN { MOVE(player, MOVE_SCRATCH, criticalHit: TRUE); }
|
||||
} SCENE {
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} THEN {
|
||||
if (i > 0)
|
||||
EXPECT_LT(results[0].damage, results[i].damage);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Critical hits ignore negative stat stages", s16 damage)
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_CELEBRATE; }
|
||||
PARAMETRIZE { move = MOVE_HARDEN; }
|
||||
PARAMETRIZE { move = MOVE_GROWL; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].split == SPLIT_PHYSICAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, move); }
|
||||
TURN { MOVE(player, MOVE_SCRATCH, criticalHit: TRUE); }
|
||||
} SCENE {
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} THEN {
|
||||
if (i > 0)
|
||||
EXPECT_EQ(results[0].damage, results[i].damage);
|
||||
}
|
||||
}
|
41
test/move_effect_absorb.c
Normal file
41
test/move_effect_absorb.c
Normal file
@ -0,0 +1,41 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_ABSORB].effect == EFFECT_ABSORB);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Absorb recovers 50% of the damage dealt")
|
||||
{
|
||||
s16 damage;
|
||||
s16 healed;
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_ABSORB); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_ABSORB, player);
|
||||
HP_BAR(opponent, captureDamage: &damage);
|
||||
HP_BAR(player, captureDamage: &healed);
|
||||
} THEN {
|
||||
EXPECT_MUL_EQ(damage, Q_4_12(-0.5), healed);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Absorb fails if Heal Block applies")
|
||||
{
|
||||
ASSUME(B_HEAL_BLOCKING >= GEN_6);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_ABSORB); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet was prevented from healing!");
|
||||
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_ABSORB, player);
|
||||
NOT HP_BAR(opponent);
|
||||
NOT HP_BAR(player);
|
||||
}
|
||||
}
|
24
test/move_effect_accuracy_down.c
Normal file
24
test/move_effect_accuracy_down.c
Normal file
@ -0,0 +1,24 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_SAND_ATTACK].effect == EFFECT_ACCURACY_DOWN);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Sand Attack lowers Accuracy")
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SAND_ATTACK); MOVE(opponent, MOVE_SCRATCH); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SAND_ATTACK, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
|
||||
MESSAGE("Foe Wobbuffet's accuracy fell!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent);
|
||||
}
|
||||
}
|
54
test/move_effect_after_you.c
Normal file
54
test/move_effect_after_you.c
Normal file
@ -0,0 +1,54 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_AFTER_YOU].effect == EFFECT_AFTER_YOU);
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("After You makes the target move after user")
|
||||
{
|
||||
if (B_RECALC_TURN_AFTER_ACTIONS >= GEN_8) KNOWN_FAILING; // #2615.
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Speed(4); }
|
||||
PLAYER(SPECIES_WYNAUT) { Speed(1); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(3); }
|
||||
OPPONENT(SPECIES_WYNAUT) { Speed(2); }
|
||||
} WHEN {
|
||||
TURN {
|
||||
MOVE(playerLeft, MOVE_AFTER_YOU, target: playerRight);
|
||||
MOVE(playerRight, MOVE_CELEBRATE);
|
||||
MOVE(opponentLeft, MOVE_CELEBRATE);
|
||||
MOVE(opponentRight, MOVE_CELEBRATE);
|
||||
}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_AFTER_YOU, playerLeft);
|
||||
MESSAGE("Wynaut took the kind offer!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerRight);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentRight);
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("After You does nothing if the target has already moved")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Speed(4); }
|
||||
PLAYER(SPECIES_WYNAUT) { Speed(1); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(3); }
|
||||
OPPONENT(SPECIES_WYNAUT) { Speed(2); }
|
||||
} WHEN {
|
||||
TURN {
|
||||
MOVE(playerLeft, MOVE_CELEBRATE);
|
||||
MOVE(playerRight, MOVE_CELEBRATE);
|
||||
MOVE(opponentLeft, MOVE_CELEBRATE);
|
||||
MOVE(opponentRight, MOVE_AFTER_YOU, target: opponentLeft);
|
||||
}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerLeft);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft);
|
||||
MESSAGE("Foe Wynaut used After You!");
|
||||
MESSAGE("But it failed!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerRight);
|
||||
}
|
||||
}
|
32
test/move_effect_attack_down.c
Normal file
32
test/move_effect_attack_down.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_GROWL].effect == EFFECT_ATTACK_DOWN);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Growl lowers Attack", s16 damage)
|
||||
{
|
||||
bool32 lowerAttack;
|
||||
PARAMETRIZE { lowerAttack = FALSE; }
|
||||
PARAMETRIZE { lowerAttack = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
if (lowerAttack) TURN { MOVE(player, MOVE_GROWL); }
|
||||
TURN { MOVE(opponent, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
if (lowerAttack) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_GROWL, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
|
||||
MESSAGE("Foe Wobbuffet's attack fell!");
|
||||
}
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage);
|
||||
}
|
||||
}
|
32
test/move_effect_attack_up.c
Normal file
32
test/move_effect_attack_up.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_MEDITATE].effect == EFFECT_ATTACK_UP);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Meditate raises Attack", s16 damage)
|
||||
{
|
||||
bool32 raiseAttack;
|
||||
PARAMETRIZE { raiseAttack = FALSE; }
|
||||
PARAMETRIZE { raiseAttack = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
if (raiseAttack) TURN { MOVE(player, MOVE_MEDITATE); }
|
||||
TURN { MOVE(player, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
if (raiseAttack) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_MEDITATE, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
|
||||
MESSAGE("Wobbuffet's attack rose!");
|
||||
}
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage);
|
||||
}
|
||||
}
|
34
test/move_effect_bide.c
Normal file
34
test/move_effect_bide.c
Normal file
@ -0,0 +1,34 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_BIDE].effect == EFFECT_BIDE);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Bide deals twice the taken damage over two turns")
|
||||
{
|
||||
s16 damage1;
|
||||
s16 damage2;
|
||||
s16 bideDamage;
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_BIDE); MOVE(opponent, MOVE_TACKLE); }
|
||||
TURN { SKIP_TURN(player); MOVE(opponent, MOVE_TACKLE); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_BIDE, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage1);
|
||||
MESSAGE("Wobbuffet is storing energy!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage2);
|
||||
MESSAGE("Wobbuffet unleashed energy!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_BIDE, player);
|
||||
HP_BAR(opponent, captureDamage: &bideDamage);
|
||||
} FINALLY {
|
||||
EXPECT_EQ(bideDamage, damage1 + damage2);
|
||||
}
|
||||
}
|
38
test/move_effect_burn_hit.c
Normal file
38
test/move_effect_burn_hit.c
Normal file
@ -0,0 +1,38 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_EMBER].effect == EFFECT_BURN_HIT);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Ember inflicts burn")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_EMBER); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_EMBER, player);
|
||||
HP_BAR(opponent);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, opponent);
|
||||
STATUS_ICON(opponent, burn: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Ember cannot burn a Fire-type")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gSpeciesInfo[SPECIES_CHARMANDER].types[0] == TYPE_FIRE);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_CHARMANDER);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_EMBER); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_EMBER, player);
|
||||
HP_BAR(opponent);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, opponent);
|
||||
NOT STATUS_ICON(opponent, burn: TRUE);
|
||||
}
|
||||
}
|
32
test/move_effect_defense_down.c
Normal file
32
test/move_effect_defense_down.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_TAIL_WHIP].effect == EFFECT_DEFENSE_DOWN);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Tail Whip lowers Defense", s16 damage)
|
||||
{
|
||||
bool32 lowerDefense;
|
||||
PARAMETRIZE { lowerDefense = FALSE; }
|
||||
PARAMETRIZE { lowerDefense = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
if (lowerDefense) TURN { MOVE(player, MOVE_TAIL_WHIP); }
|
||||
TURN { MOVE(player, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
if (lowerDefense) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TAIL_WHIP, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
|
||||
MESSAGE("Foe Wobbuffet's defense fell!");
|
||||
}
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage);
|
||||
}
|
||||
}
|
32
test/move_effect_defense_up.c
Normal file
32
test/move_effect_defense_up.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_HARDEN].effect == EFFECT_DEFENSE_UP);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Harden raises Defense", s16 damage)
|
||||
{
|
||||
bool32 raiseDefense;
|
||||
PARAMETRIZE { raiseDefense = FALSE; }
|
||||
PARAMETRIZE { raiseDefense = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
if (raiseDefense) TURN { MOVE(player, MOVE_HARDEN); }
|
||||
TURN { MOVE(opponent, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
if (raiseDefense) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_HARDEN, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
|
||||
MESSAGE("Wobbuffet's defense rose!");
|
||||
}
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage);
|
||||
}
|
||||
}
|
54
test/move_effect_dream_eater.c
Normal file
54
test/move_effect_dream_eater.c
Normal file
@ -0,0 +1,54 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_DREAM_EATER].effect == EFFECT_DREAM_EATER);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Dream Eater recovers 50% of the damage dealt")
|
||||
{
|
||||
s16 damage;
|
||||
s16 healed;
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_SLEEP); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_DREAM_EATER); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_DREAM_EATER, player);
|
||||
HP_BAR(opponent, captureDamage: &damage);
|
||||
HP_BAR(player, captureDamage: &healed);
|
||||
} THEN {
|
||||
EXPECT_MUL_EQ(damage, Q_4_12(-1.0/2.0), healed);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Dream Eater fails on awake targets")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_DREAM_EATER); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet used Dream Eater!");
|
||||
MESSAGE("Foe Wobbuffet wasn't affected!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Dream Eater fails if Heal Block applies")
|
||||
{
|
||||
ASSUME(B_HEAL_BLOCKING >= GEN_6);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_DREAM_EATER); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet was prevented from healing!");
|
||||
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_DREAM_EATER, player);
|
||||
NOT HP_BAR(opponent);
|
||||
NOT HP_BAR(player);
|
||||
}
|
||||
}
|
54
test/move_effect_encore.c
Normal file
54
test/move_effect_encore.c
Normal file
@ -0,0 +1,54 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_ENCORE].effect == EFFECT_ENCORE);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Encore forces consecutive move uses for 2 turns")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_ENCORE); }
|
||||
TURN { FORCED_MOVE(player); }
|
||||
TURN { FORCED_MOVE(player); }
|
||||
TURN { MOVE(player, MOVE_SPLASH); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_ENCORE, opponent);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Encore has no effect if no previous move")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_ENCORE); MOVE(player, MOVE_CELEBRATE); }
|
||||
} SCENE {
|
||||
MESSAGE("Foe Wobbuffet used Encore!");
|
||||
MESSAGE("But it failed!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Encore overrides the chosen move if it occurs first")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); }
|
||||
TURN { MOVE(opponent, MOVE_ENCORE); MOVE(player, MOVE_SPLASH); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_ENCORE, opponent);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
|
||||
}
|
||||
}
|
24
test/move_effect_evasion_up.c
Normal file
24
test/move_effect_evasion_up.c
Normal file
@ -0,0 +1,24 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_DOUBLE_TEAM].effect == EFFECT_EVASION_UP);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Double Team raises Evasion")
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_DOUBLE_TEAM); MOVE(opponent, MOVE_SCRATCH); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_DOUBLE_TEAM, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
|
||||
MESSAGE("Wobbuffet's evasiveness rose!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent);
|
||||
}
|
||||
}
|
53
test/move_effect_explosion.c
Normal file
53
test/move_effect_explosion.c
Normal file
@ -0,0 +1,53 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_EXPLOSION].effect == EFFECT_EXPLOSION);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Explosion causes the user to faint")
|
||||
{
|
||||
u16 remainingHP;
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_EXPLOSION); }
|
||||
} SCENE {
|
||||
HP_BAR(player, hp: 0);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Explosion causes the user to faint even if it misses")
|
||||
{
|
||||
u16 remainingHP;
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_EXPLOSION, hit: FALSE); }
|
||||
} SCENE {
|
||||
HP_BAR(player, hp: 0);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Explosion causes the user to faint even if it has no effect")
|
||||
{
|
||||
u16 remainingHP;
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_EXPLOSION].type == TYPE_NORMAL);
|
||||
ASSUME(gSpeciesInfo[SPECIES_GASTLY].types[0] == TYPE_GHOST);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_GASTLY);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_EXPLOSION); }
|
||||
} SCENE {
|
||||
HP_BAR(player, hp: 0);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player);
|
||||
MESSAGE("It doesn't affect Foe Gastly…");
|
||||
NOT HP_BAR(opponent);
|
||||
}
|
||||
}
|
38
test/move_effect_freeze_hit.c
Normal file
38
test/move_effect_freeze_hit.c
Normal file
@ -0,0 +1,38 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_POWDER_SNOW].effect == EFFECT_FREEZE_HIT);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Powder Snow inflicts freeze")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_POWDER_SNOW); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_POWDER_SNOW, player);
|
||||
HP_BAR(opponent);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_FRZ, opponent);
|
||||
STATUS_ICON(opponent, freeze: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Powder Snow cannot freeze an Ice-type")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gSpeciesInfo[SPECIES_SNORUNT].types[0] == TYPE_ICE);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_SNORUNT);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_POWDER_SNOW); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_POWDER_SNOW, player);
|
||||
HP_BAR(opponent);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_FRZ, opponent);
|
||||
NOT STATUS_ICON(opponent, freeze: TRUE);
|
||||
}
|
||||
}
|
32
test/move_effect_haze.c
Normal file
32
test/move_effect_haze.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_HAZE].effect == EFFECT_HAZE);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Haze resets stat changes", s16 damage)
|
||||
{
|
||||
bool32 haze;
|
||||
PARAMETRIZE { haze = FALSE; }
|
||||
PARAMETRIZE { haze = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_MEDITATE].effect == EFFECT_ATTACK_UP);
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
if (haze) TURN { MOVE(player, MOVE_MEDITATE); MOVE(opponent, MOVE_HAZE); }
|
||||
TURN { MOVE(player, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
if (haze) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_HAZE, opponent);
|
||||
MESSAGE("All stat changes were eliminated!");
|
||||
}
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_EQ(results[0].damage, results[1].damage);
|
||||
}
|
||||
}
|
33
test/move_effect_hex.c
Normal file
33
test/move_effect_hex.c
Normal file
@ -0,0 +1,33 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_HEX].effect == EFFECT_HEX);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Hex deals double damage to foes with a status", s16 damage)
|
||||
{
|
||||
u32 status1;
|
||||
PARAMETRIZE { status1 = STATUS1_NONE; }
|
||||
PARAMETRIZE { status1 = STATUS1_SLEEP; }
|
||||
PARAMETRIZE { status1 = STATUS1_POISON; }
|
||||
PARAMETRIZE { status1 = STATUS1_BURN; }
|
||||
PARAMETRIZE { status1 = STATUS1_FREEZE; }
|
||||
PARAMETRIZE { status1 = STATUS1_PARALYSIS; }
|
||||
PARAMETRIZE { status1 = STATUS1_TOXIC_POISON; }
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Status1(status1); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_HEX); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_HEX, player);
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} THEN {
|
||||
if (i > 0)
|
||||
EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.0), results[i].damage);
|
||||
if (i > 1)
|
||||
EXPECT_EQ(results[i-1].damage, results[i].damage);
|
||||
}
|
||||
}
|
96
test/move_effect_hit_escape.c
Normal file
96
test/move_effect_hit_escape.c
Normal file
@ -0,0 +1,96 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_U_TURN].effect == EFFECT_HIT_ESCAPE);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("U-turn switches the user out")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(player, 1); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
|
||||
HP_BAR(opponent);
|
||||
MESSAGE("Go! Wynaut!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("U-turn does not switch the user out if the battle ends")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_WOBBUFFET) { HP(1); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_U_TURN); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
|
||||
HP_BAR(opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("U-turn does not switch the user out if no replacements")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_U_TURN); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
|
||||
HP_BAR(opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("U-turn does not switch the user out if replacements fainted")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT) { HP(0); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_U_TURN); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
|
||||
HP_BAR(opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("U-turn does not switch the user out if Wimp Out activates")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_WIMPOD) { MaxHP(100); HP(51); Ability(ABILITY_WIMP_OUT); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(opponent, 1); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
|
||||
HP_BAR(opponent);
|
||||
ABILITY_POPUP(opponent, ABILITY_WIMP_OUT);
|
||||
MESSAGE("2 sent out Wobbuffet!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("U-turn switches the user out if Wimp Out fails to activate")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_WIMPOD) { MaxHP(100); HP(51); Ability(ABILITY_WIMP_OUT); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(player, 1); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
|
||||
HP_BAR(opponent);
|
||||
NOT ABILITY_POPUP(opponent);
|
||||
MESSAGE("Your foe's weak! Get 'em, Wynaut!");
|
||||
}
|
||||
}
|
39
test/move_effect_paralyze_hit.c
Normal file
39
test/move_effect_paralyze_hit.c
Normal file
@ -0,0 +1,39 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_THUNDER_SHOCK].effect == EFFECT_PARALYZE_HIT);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Thunder Shock inflicts paralysis")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_THUNDER_SHOCK); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, player);
|
||||
HP_BAR(opponent);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, opponent);
|
||||
STATUS_ICON(opponent, paralysis: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Thunder Shock cannot paralyze an Electric-type")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_PARALYZE_ELECTRIC >= GEN_6);
|
||||
ASSUME(gSpeciesInfo[SPECIES_PIKACHU].types[0] == TYPE_ELECTRIC);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_PIKACHU);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_THUNDER_SHOCK); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, player);
|
||||
HP_BAR(opponent);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, opponent);
|
||||
NOT STATUS_ICON(opponent, paralysis: TRUE);
|
||||
}
|
||||
}
|
39
test/move_effect_poison_hit.c
Normal file
39
test/move_effect_poison_hit.c
Normal file
@ -0,0 +1,39 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Poison Sting inflicts poison")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_POISON_STING); }
|
||||
TURN {}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
|
||||
HP_BAR(opponent);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Poison Sting cannot poison Poison-type")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gSpeciesInfo[SPECIES_NIDORAN_M].types[0] == TYPE_POISON);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_NIDORAN_M);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_POISON_STING); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
|
||||
HP_BAR(opponent);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
91
test/move_effect_rampage.c
Normal file
91
test/move_effect_rampage.c
Normal file
@ -0,0 +1,91 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_THRASH].effect == EFFECT_RAMPAGE);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Thrash lasts for 2 or 3 turns")
|
||||
{
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_THRASH); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Thrash confuses the user after it finishes")
|
||||
{
|
||||
GIVEN {
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_THRASH); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 1 of 3")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_THRASH); }
|
||||
TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
} SCENE {
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 2 of 3")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_THRASH); }
|
||||
TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
} SCENE {
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Thrash confuses the user if it is canceled on turn 3 of 3")
|
||||
{
|
||||
KNOWN_FAILING;
|
||||
GIVEN {
|
||||
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_THRASH); }
|
||||
TURN { SKIP_TURN(player); }
|
||||
TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
|
||||
}
|
||||
}
|
57
test/move_effect_recoil_if_miss.c
Normal file
57
test/move_effect_recoil_if_miss.c
Normal file
@ -0,0 +1,57 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_JUMP_KICK].effect == EFFECT_RECOIL_IF_MISS);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on miss")
|
||||
{
|
||||
s16 recoil;
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_JUMP_KICK, hit: FALSE); }
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
MESSAGE("Wobbuffet used Jump Kick!");
|
||||
MESSAGE("Wobbuffet's attack missed!");
|
||||
MESSAGE("Wobbuffet kept going and crashed!");
|
||||
HP_BAR(player, damage: maxHP / 2);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on protect")
|
||||
{
|
||||
s16 recoil;
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_JUMP_KICK].flags & FLAG_PROTECT_AFFECTED);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_PROTECT); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); }
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_PROTECT, opponent);
|
||||
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_JUMP_KICK, player);
|
||||
HP_BAR(player, damage: maxHP / 2);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Jump Kick has no recoil if no target")
|
||||
{
|
||||
KNOWN_FAILING; // #2596.
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_HEALING_WISH); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); SEND_OUT(opponent, 1); }
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_HEALING_WISH, opponent);
|
||||
NOT HP_BAR(player, damage: maxHP / 2);
|
||||
}
|
||||
}
|
77
test/move_effect_reflect.c
Normal file
77
test/move_effect_reflect.c
Normal file
@ -0,0 +1,77 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_REFLECT].effect == EFFECT_REFLECT);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Reflect reduces physical damage", s16 damage)
|
||||
{
|
||||
u32 move;
|
||||
PARAMETRIZE { move = MOVE_CELEBRATE; }
|
||||
PARAMETRIZE { move = MOVE_REFLECT; }
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, move); MOVE(opponent, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, move, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_LT(results[1].damage, results[0].damage);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Reflect applies for 5 turns")
|
||||
{
|
||||
u16 damage[6];
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_REFLECT); MOVE(opponent, MOVE_TACKLE); }
|
||||
TURN { MOVE(opponent, MOVE_TACKLE); }
|
||||
TURN { MOVE(opponent, MOVE_TACKLE); }
|
||||
TURN { MOVE(opponent, MOVE_TACKLE); }
|
||||
TURN { MOVE(opponent, MOVE_TACKLE); }
|
||||
TURN { MOVE(opponent, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_REFLECT, player);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage[0]);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage[1]);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage[2]);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage[3]);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage[4]);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
|
||||
HP_BAR(player, captureDamage: &damage[5]);
|
||||
} THEN {
|
||||
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[1]);
|
||||
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[2]);
|
||||
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[3]);
|
||||
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[4]);
|
||||
EXPECT_LT(damage[0], damage[5]);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Reflect fails if already active")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_REFLECT); }
|
||||
TURN { MOVE(player, MOVE_REFLECT); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_REFLECT, player);
|
||||
MESSAGE("Wobbuffet used Reflect!");
|
||||
MESSAGE("But it failed!");
|
||||
}
|
||||
}
|
21
test/move_effect_sleep.c
Normal file
21
test/move_effect_sleep.c
Normal file
@ -0,0 +1,21 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_HYPNOSIS].effect == EFFECT_SLEEP);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Hypnosis inflicts sleep")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_HYPNOSIS); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPNOSIS, player);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_SLP, opponent);
|
||||
STATUS_ICON(opponent, sleep: TRUE);
|
||||
}
|
||||
}
|
32
test/move_effect_special_attack_down.c
Normal file
32
test/move_effect_special_attack_down.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_CONFIDE].effect == EFFECT_SPECIAL_ATTACK_DOWN);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Confide lowers Special Attack", s16 damage)
|
||||
{
|
||||
bool32 lowerSpecialAttack;
|
||||
PARAMETRIZE { lowerSpecialAttack = FALSE; }
|
||||
PARAMETRIZE { lowerSpecialAttack = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_GUST].split == SPLIT_SPECIAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
if (lowerSpecialAttack) TURN { MOVE(player, MOVE_CONFIDE); }
|
||||
TURN { MOVE(opponent, MOVE_GUST); }
|
||||
} SCENE {
|
||||
if (lowerSpecialAttack) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CONFIDE, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
|
||||
MESSAGE("Foe Wobbuffet's sp. attack fell!");
|
||||
}
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_GUST, opponent);
|
||||
HP_BAR(player, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage);
|
||||
}
|
||||
}
|
32
test/move_effect_special_attack_up_3.c
Normal file
32
test/move_effect_special_attack_up_3.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_TAIL_GLOW].effect == EFFECT_SPECIAL_ATTACK_UP_3);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Tail Glow drastically raises Special Attack", s16 damage)
|
||||
{
|
||||
bool32 raiseSpecialAttack;
|
||||
PARAMETRIZE { raiseSpecialAttack = FALSE; }
|
||||
PARAMETRIZE { raiseSpecialAttack = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_GUST].split == SPLIT_SPECIAL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
if (raiseSpecialAttack) TURN { MOVE(player, MOVE_TAIL_GLOW); }
|
||||
TURN { MOVE(player, MOVE_GUST); }
|
||||
} SCENE {
|
||||
if (raiseSpecialAttack) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TAIL_GLOW, player);
|
||||
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
|
||||
MESSAGE("Wobbuffet's sp. attack drastically rose!");
|
||||
}
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_GUST, player);
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.5), results[1].damage);
|
||||
}
|
||||
}
|
135
test/move_effect_spikes.c
Normal file
135
test/move_effect_spikes.c
Normal file
@ -0,0 +1,135 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_SPIKES].effect == EFFECT_SPIKES);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Spikes damage on switch in")
|
||||
{
|
||||
u32 layers;
|
||||
u32 divisor;
|
||||
PARAMETRIZE { layers = 1; divisor = 8; }
|
||||
PARAMETRIZE { layers = 2; divisor = 6; }
|
||||
PARAMETRIZE { layers = 3; divisor = 4; }
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
u32 count;
|
||||
for (count = 0; count < layers; ++count) {
|
||||
TURN { MOVE(player, MOVE_SPIKES); }
|
||||
}
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
} SCENE {
|
||||
u32 count;
|
||||
s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
|
||||
for (count = 0; count < layers; ++count) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
|
||||
MESSAGE("Spikes were scattered all around the opponent's side!");
|
||||
}
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
HP_BAR(opponent, damage: maxHP / divisor);
|
||||
MESSAGE("Foe Wynaut is hurt by spikes!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Spikes fails after 3 layers")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SPIKES); }
|
||||
TURN { MOVE(player, MOVE_SPIKES); }
|
||||
TURN { MOVE(player, MOVE_SPIKES); }
|
||||
TURN { MOVE(player, MOVE_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
|
||||
MESSAGE("Spikes were scattered all around the opponent's side!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
|
||||
MESSAGE("Spikes were scattered all around the opponent's side!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
|
||||
MESSAGE("Spikes were scattered all around the opponent's side!");
|
||||
MESSAGE("Wobbuffet used Spikes!");
|
||||
MESSAGE("But it failed!");
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
HP_BAR(opponent, damage: maxHP / 4);
|
||||
MESSAGE("Foe Wynaut is hurt by spikes!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Spikes damage on subsequent switch ins")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
TURN { SWITCH(opponent, 0); }
|
||||
} SCENE {
|
||||
s32 maxHP0 = GetMonData(&OPPONENT_PARTY[0], MON_DATA_MAX_HP);
|
||||
s32 maxHP1 = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
HP_BAR(opponent, damage: maxHP1 / 8);
|
||||
MESSAGE("Foe Wynaut is hurt by spikes!");
|
||||
MESSAGE("2 sent out Wobbuffet!");
|
||||
HP_BAR(opponent, damage: maxHP0 / 8);
|
||||
MESSAGE("Foe Wobbuffet is hurt by spikes!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Spikes do not damage airborne Pokemon")
|
||||
{
|
||||
u32 species = SPECIES_WOBBUFFET;
|
||||
u32 item = ITEM_NONE;
|
||||
u32 move1 = MOVE_CELEBRATE;
|
||||
u32 move2 = MOVE_CELEBRATE;
|
||||
bool32 airborne;
|
||||
|
||||
ASSUME(gSpeciesInfo[SPECIES_PIDGEY].types[1] == TYPE_FLYING);
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; airborne = TRUE; }
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; item = ITEM_IRON_BALL; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_INGRAIN; airborne = FALSE; }
|
||||
|
||||
ASSUME(gSpeciesInfo[SPECIES_UNOWN].abilities[0] == ABILITY_LEVITATE);
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; airborne = TRUE; }
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; item = ITEM_IRON_BALL; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_INGRAIN; airborne = FALSE; }
|
||||
|
||||
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; airborne = TRUE; }
|
||||
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; item = ITEM_IRON_BALL; airborne = FALSE; }
|
||||
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; move2 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
// Magnet Rise fails under Gravity.
|
||||
// Magnet Rise fails under Ingrain and vice-versa.
|
||||
|
||||
PARAMETRIZE { item = ITEM_AIR_BALLOON; airborne = TRUE; }
|
||||
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_INGRAIN; airborne = FALSE; }
|
||||
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(species) { Item(item); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SPIKES); MOVE(opponent, move1); }
|
||||
TURN { MOVE(opponent, move2); }
|
||||
TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
|
||||
if (airborne) {
|
||||
NOT HP_BAR(opponent, damage: maxHP / 8);
|
||||
} else {
|
||||
HP_BAR(opponent, damage: maxHP / 8);
|
||||
}
|
||||
}
|
||||
}
|
55
test/move_effect_tailwind.c
Normal file
55
test/move_effect_tailwind.c
Normal file
@ -0,0 +1,55 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_TAILWIND].effect == EFFECT_TAILWIND);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Tailwind applies for 4 turns")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_TAILWIND_TURNS >= GEN_5);
|
||||
PLAYER(SPECIES_WOBBUFFET) { Speed(10); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(15); }
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_CELEBRATE); MOVE(player, MOVE_TAILWIND); }
|
||||
TURN {}
|
||||
TURN {}
|
||||
TURN {}
|
||||
TURN {}
|
||||
} SCENE {
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
MESSAGE("Wobbuffet used Tailwind!");
|
||||
|
||||
MESSAGE("Wobbuffet used Celebrate!");
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
|
||||
MESSAGE("Wobbuffet used Celebrate!");
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
|
||||
MESSAGE("Wobbuffet used Celebrate!");
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
MESSAGE("Wobbuffet used Celebrate!");
|
||||
}
|
||||
}
|
||||
|
||||
DOUBLE_BATTLE_TEST("Tailwind affects partner on first turn")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_RECALC_TURN_AFTER_ACTIONS);
|
||||
PLAYER(SPECIES_WOBBUFFET) { Speed(20); }
|
||||
PLAYER(SPECIES_WYNAUT) { Speed(10); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(15); }
|
||||
OPPONENT(SPECIES_WYNAUT) { Speed(14); }
|
||||
} WHEN {
|
||||
TURN { MOVE(playerLeft, MOVE_TAILWIND); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet used Tailwind!");
|
||||
MESSAGE("Wynaut used Celebrate!");
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
MESSAGE("Foe Wynaut used Celebrate!");
|
||||
}
|
||||
}
|
53
test/move_effect_torment.c
Normal file
53
test/move_effect_torment.c
Normal file
@ -0,0 +1,53 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_TORMENT].effect == EFFECT_TORMENT);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Torment prevents consecutive move uses")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SPLASH, MOVE_CELEBRATE); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); }
|
||||
TURN { MOVE(opponent, MOVE_SPLASH, allowed: FALSE); MOVE(opponent, MOVE_CELEBRATE); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TORMENT, player);
|
||||
MESSAGE("Foe Wobbuffet was subjected to torment!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Torment forces Struggle if the only move is prevented")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SPLASH); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); }
|
||||
TURN { MOVE(opponent, MOVE_SPLASH, allowed: FALSE); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_STRUGGLE, opponent);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Torment allows non-consecutive move uses")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); }
|
||||
TURN { MOVE(opponent, MOVE_CELEBRATE); }
|
||||
TURN { MOVE(opponent, MOVE_SPLASH); }
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
|
||||
}
|
||||
}
|
48
test/move_effect_toxic.c
Normal file
48
test/move_effect_toxic.c
Normal file
@ -0,0 +1,48 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic inflicts bad poison")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC); }
|
||||
TURN {}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
STATUS_ICON(opponent, badPoison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic cannot miss if used by a Poison-type")
|
||||
{
|
||||
u32 species;
|
||||
bool32 hit;
|
||||
PARAMETRIZE { species = SPECIES_WOBBUFFET; hit = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_NIDORAN_M; hit = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(B_TOXIC_NEVER_MISS >= GEN_6);
|
||||
ASSUME(gSpeciesInfo[SPECIES_NIDORAN_M].types[0] == TYPE_POISON);
|
||||
PLAYER(species);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC, hit: FALSE); }
|
||||
} SCENE {
|
||||
if (hit) {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
STATUS_ICON(opponent, badPoison: TRUE);
|
||||
} else {
|
||||
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
|
||||
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
NOT STATUS_ICON(opponent, badPoison: TRUE);
|
||||
}
|
||||
}
|
||||
}
|
210
test/move_effect_toxic_spikes.c
Normal file
210
test/move_effect_toxic_spikes.c
Normal file
@ -0,0 +1,210 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
ASSUMPTIONS
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes inflicts poison on switch in")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
TURN {}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
|
||||
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes inflicts bad poison on switch in")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
TURN {}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
|
||||
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
|
||||
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
STATUS_ICON(opponent, badPoison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes fails after 2 layers")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
TURN {}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
|
||||
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
|
||||
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
|
||||
MESSAGE("Wobbuffet used Toxic Spikes!");
|
||||
MESSAGE("But it failed!");
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
|
||||
STATUS_ICON(opponent, badPoison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes inflicts poison on subsequent switch ins")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WYNAUT);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
TURN { SWITCH(opponent, 0); }
|
||||
TURN {}
|
||||
} SCENE {
|
||||
MESSAGE("2 sent out Wynaut!");
|
||||
STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes do not poison airborne Pokemon")
|
||||
{
|
||||
u32 species = SPECIES_WOBBUFFET;
|
||||
u32 item = ITEM_NONE;
|
||||
u32 move1 = MOVE_CELEBRATE;
|
||||
u32 move2 = MOVE_CELEBRATE;
|
||||
bool32 airborne;
|
||||
|
||||
ASSUME(gSpeciesInfo[SPECIES_PIDGEY].types[1] == TYPE_FLYING);
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; airborne = TRUE; }
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; item = ITEM_IRON_BALL; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_INGRAIN; airborne = FALSE; }
|
||||
|
||||
ASSUME(gSpeciesInfo[SPECIES_UNOWN].abilities[0] == ABILITY_LEVITATE);
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; airborne = TRUE; }
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; item = ITEM_IRON_BALL; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_INGRAIN; airborne = FALSE; }
|
||||
|
||||
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; airborne = TRUE; }
|
||||
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; item = ITEM_IRON_BALL; airborne = FALSE; }
|
||||
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; move2 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
// Magnet Rise fails under Gravity.
|
||||
// Magnet Rise fails under Ingrain and vice-versa.
|
||||
|
||||
PARAMETRIZE { item = ITEM_AIR_BALLOON; airborne = TRUE; }
|
||||
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_GRAVITY; airborne = FALSE; }
|
||||
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_INGRAIN; airborne = FALSE; }
|
||||
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(species) { Item(item); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, move1); }
|
||||
TURN { MOVE(opponent, move2); }
|
||||
TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
|
||||
} SCENE {
|
||||
if (airborne) {
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
} else {
|
||||
STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes do not affect Steel-types")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gSpeciesInfo[SPECIES_STEELIX].types[0] == TYPE_STEEL);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_STEELIX);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
|
||||
TURN { SWITCH(opponent, 1); }
|
||||
} SCENE {
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes are removed by grounded Poison-types")
|
||||
{
|
||||
u32 species;
|
||||
u32 item = ITEM_NONE;
|
||||
u32 move = MOVE_CELEBRATE;
|
||||
bool32 grounded;
|
||||
PARAMETRIZE { species = SPECIES_EKANS; grounded = TRUE; }
|
||||
PARAMETRIZE { species = SPECIES_ZUBAT; grounded = FALSE; }
|
||||
PARAMETRIZE { species = SPECIES_ZUBAT; item = ITEM_IRON_BALL; grounded = TRUE; }
|
||||
PARAMETRIZE { species = SPECIES_ZUBAT; move = MOVE_GRAVITY; grounded = TRUE; }
|
||||
PARAMETRIZE { species = SPECIES_ZUBAT; move = MOVE_INGRAIN; grounded = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(gSpeciesInfo[SPECIES_EKANS].types[0] == TYPE_POISON);
|
||||
ASSUME(gSpeciesInfo[SPECIES_ZUBAT].types[0] == TYPE_POISON);
|
||||
ASSUME(gSpeciesInfo[SPECIES_ZUBAT].types[1] == TYPE_FLYING);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(species) { Item(item); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, move); }
|
||||
TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
|
||||
TURN { SWITCH(opponent, 0); }
|
||||
} SCENE {
|
||||
if (grounded) {
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
MESSAGE("The poison spikes disappeared from around the opposing team's feet!");
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
} else {
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_BATON_PASS, opponent);
|
||||
STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This would test for what I believe to be a bug in the mainline games.
|
||||
// A Pokémon that gets passed magnet rise should still remove the Toxic
|
||||
// Spikes even though it is airborne.
|
||||
// The test currently fails, because we don't incorporate this bug.
|
||||
SINGLE_BATTLE_TEST("Toxic Spikes are removed by Poison-types affected by Magnet Rise")
|
||||
{
|
||||
KNOWN_FAILING;
|
||||
GIVEN {
|
||||
ASSUME(gSpeciesInfo[SPECIES_EKANS].types[0] == TYPE_POISON);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_EKANS);
|
||||
} WHEN {
|
||||
TURN { MOVE(opponent, MOVE_MAGNET_RISE); }
|
||||
TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
|
||||
TURN { SWITCH(opponent, 0); }
|
||||
} SCENE {
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
MESSAGE("The poison spikes disappeared from around the opposing team's feet!");
|
||||
NOT STATUS_ICON(opponent, poison: TRUE);
|
||||
}
|
||||
}
|
194
test/status1.c
Normal file
194
test/status1.c
Normal file
@ -0,0 +1,194 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
SINGLE_BATTLE_TEST("Sleep prevents the battler from using a move")
|
||||
{
|
||||
u32 turns;
|
||||
PARAMETRIZE { turns = 1; }
|
||||
PARAMETRIZE { turns = 2; }
|
||||
PARAMETRIZE { turns = 3; }
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_SLEEP_TURN(turns)); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
for (i = 0; i < turns; i++)
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); }
|
||||
} SCENE {
|
||||
for (i = 0; i < turns - 1; i++)
|
||||
MESSAGE("Wobbuffet is fast asleep.");
|
||||
MESSAGE("Wobbuffet woke up!");
|
||||
STATUS_ICON(player, none: TRUE);
|
||||
MESSAGE("Wobbuffet used Celebrate!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Poison deals 1/8th damage per turn")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
for (i = 0; i < 4; i++)
|
||||
TURN {}
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
for (i = 0; i < 4; i++)
|
||||
HP_BAR(player, damage: maxHP / 8);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Burn deals 1/16th damage per turn")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_BURN_DAMAGE >= GEN_LATEST);
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_BURN); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
for (i = 0; i < 4; i++)
|
||||
TURN {}
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
for (i = 0; i < 4; i++)
|
||||
HP_BAR(player, damage: maxHP / 16);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Burn reduces attack by 50%", s16 damage)
|
||||
{
|
||||
bool32 burned;
|
||||
PARAMETRIZE { burned = FALSE; }
|
||||
PARAMETRIZE { burned = TRUE; }
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { if (burned) Status1(STATUS1_BURN); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_TACKLE); }
|
||||
} SCENE {
|
||||
HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
} FINALLY {
|
||||
EXPECT_MUL_EQ(results[0].damage, Q_4_12(0.5), results[1].damage);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Freeze has a 20% chance of being thawed")
|
||||
{
|
||||
PASSES_RANDOMLY(20, 100);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); }
|
||||
} SCENE {
|
||||
STATUS_ICON(player, none: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Freeze is thawed by opponent's Fire-type attacks")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE);
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_EMBER); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet is frozen solid!");
|
||||
MESSAGE("Foe Wobbuffet used Ember!");
|
||||
MESSAGE("Wobbuffet was defrosted!");
|
||||
STATUS_ICON(player, none: TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Freeze is thawed by user's Flame Wheel")
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_FLAME_WHEEL].flags & FLAG_THAW_USER);
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_FLAME_WHEEL); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet was defrosted by Flame Wheel!");
|
||||
STATUS_ICON(player, none: TRUE);
|
||||
MESSAGE("Wobbuffet used Flame Wheel!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Paralysis reduces speed by 50%")
|
||||
{
|
||||
u16 playerSpeed;
|
||||
bool32 playerFirst;
|
||||
PARAMETRIZE { playerSpeed = 98; playerFirst = FALSE; }
|
||||
PARAMETRIZE { playerSpeed = 102; playerFirst = TRUE; }
|
||||
GIVEN {
|
||||
ASSUME(B_PARALYSIS_SPEED >= GEN_7);
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); Speed(playerSpeed); }
|
||||
OPPONENT(SPECIES_WOBBUFFET) { Speed(50); }
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_CELEBRATE); }
|
||||
} SCENE {
|
||||
if (playerFirst) {
|
||||
ONE_OF {
|
||||
MESSAGE("Wobbuffet used Celebrate!");
|
||||
MESSAGE("Wobbuffet is paralyzed! It can't move!");
|
||||
}
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
} else {
|
||||
MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
ONE_OF {
|
||||
MESSAGE("Wobbuffet used Celebrate!");
|
||||
MESSAGE("Wobbuffet is paralyzed! It can't move!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Paralysis has a 25% chance of skipping the turn")
|
||||
{
|
||||
PASSES_RANDOMLY(25, 100);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet is paralyzed! It can't move!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Bad poison deals 1/16th cumulative damage per turn")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_TOXIC_POISON); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
for (i = 0; i < 4; i++)
|
||||
TURN {}
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
for (i = 0; i < 4; i++)
|
||||
HP_BAR(player, damage: maxHP / 16 * (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Bad poison cumulative damage resets on switch")
|
||||
{
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_TOXIC_POISON); }
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN {}
|
||||
TURN {}
|
||||
TURN { SWITCH(player, 1); }
|
||||
TURN { SWITCH(player, 0); }
|
||||
TURN {}
|
||||
TURN {}
|
||||
} SCENE {
|
||||
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
|
||||
for (i = 0; i < 2; i++)
|
||||
HP_BAR(player, damage: maxHP / 16 * (i + 1));
|
||||
for (i = 0; i < 2; i++)
|
||||
HP_BAR(player, damage: maxHP / 16 * (i + 1));
|
||||
}
|
||||
}
|
142
test/test.h
Normal file
142
test/test.h
Normal file
@ -0,0 +1,142 @@
|
||||
#ifndef GUARD_TEST_H
|
||||
#define GUARD_TEST_H
|
||||
|
||||
#include "test_runner.h"
|
||||
|
||||
#define MAX_PROCESSES 32 // See also tools/mgba-rom-test-hydra/main.c
|
||||
|
||||
enum TestResult
|
||||
{
|
||||
TEST_RESULT_FAIL,
|
||||
TEST_RESULT_PASS,
|
||||
TEST_RESULT_SKIP,
|
||||
TEST_RESULT_INVALID,
|
||||
TEST_RESULT_ERROR,
|
||||
TEST_RESULT_TIMEOUT,
|
||||
};
|
||||
|
||||
struct TestRunner
|
||||
{
|
||||
u32 (*estimateCost)(void *);
|
||||
void (*setUp)(void *);
|
||||
void (*run)(void *);
|
||||
void (*tearDown)(void *);
|
||||
bool32 (*checkProgress)(void *);
|
||||
bool32 (*handleExitWithResult)(void *, enum TestResult);
|
||||
};
|
||||
|
||||
struct Test
|
||||
{
|
||||
const char *name;
|
||||
const char *filename;
|
||||
const struct TestRunner *runner;
|
||||
void *data;
|
||||
};
|
||||
|
||||
struct TestRunnerState
|
||||
{
|
||||
u8 state;
|
||||
u8 exitCode;
|
||||
s32 tests;
|
||||
s32 passes;
|
||||
s32 skips;
|
||||
const char *skipFilename;
|
||||
const struct Test *test;
|
||||
u32 processCosts[MAX_PROCESSES];
|
||||
|
||||
u8 result;
|
||||
u8 expectedResult;
|
||||
u32 timeoutSeconds;
|
||||
};
|
||||
|
||||
extern const u8 gTestRunnerN;
|
||||
extern const u8 gTestRunnerI;
|
||||
extern const char gTestRunnerArgv[256];
|
||||
|
||||
extern const struct TestRunner gAssumptionsRunner;
|
||||
extern struct TestRunnerState gTestRunnerState;
|
||||
|
||||
void CB2_TestRunner(void);
|
||||
|
||||
void Test_ExpectedResult(enum TestResult);
|
||||
void Test_ExitWithResult(enum TestResult, const char *fmt, ...);
|
||||
|
||||
s32 MgbaPrintf_(const char *fmt, ...);
|
||||
|
||||
#define ASSUMPTIONS \
|
||||
static void Assumptions(void); \
|
||||
__attribute__((section(".tests"))) static const struct Test sAssumptions = \
|
||||
{ \
|
||||
.name = "ASSUMPTIONS: " __FILE__, \
|
||||
.filename = __FILE__, \
|
||||
.runner = &gAssumptionsRunner, \
|
||||
.data = Assumptions, \
|
||||
}; \
|
||||
static void Assumptions(void)
|
||||
|
||||
#define ASSUME(c) \
|
||||
do \
|
||||
{ \
|
||||
if (!(c)) \
|
||||
Test_ExitWithResult(TEST_RESULT_SKIP, "%s:%d: ASSUME failed", gTestRunnerState.test->filename, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
#define EXPECT(c) \
|
||||
do \
|
||||
{ \
|
||||
if (!(c)) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT failed", gTestRunnerState.test->filename, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
#define EXPECT_EQ(a, b) \
|
||||
do \
|
||||
{ \
|
||||
typeof(a) _a = (a), _b = (b); \
|
||||
if (_a != _b) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_EQ(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
|
||||
} while (0)
|
||||
|
||||
#define EXPECT_NE(a, b) \
|
||||
do \
|
||||
{ \
|
||||
typeof(a) _a = (a), _b = (b); \
|
||||
if (_a == _b) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_NE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
|
||||
} while (0)
|
||||
|
||||
#define EXPECT_LT(a, b) \
|
||||
do \
|
||||
{ \
|
||||
typeof(a) _a = (a), _b = (b); \
|
||||
if (_a >= _b) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_LT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
|
||||
} while (0)
|
||||
|
||||
#define EXPECT_LE(a, b) \
|
||||
do \
|
||||
{ \
|
||||
typeof(a) _a = (a), _b = (b); \
|
||||
if (_a > _b) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_LE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
|
||||
} while (0)
|
||||
|
||||
#define EXPECT_GT(a, b) \
|
||||
do \
|
||||
{ \
|
||||
typeof(a) _a = (a), _b = (b); \
|
||||
if (_a <= _b) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_GT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
|
||||
} while (0)
|
||||
|
||||
#define EXPECT_GE(a, b) \
|
||||
do \
|
||||
{ \
|
||||
typeof(a) _a = (a), _b = (b); \
|
||||
if (_a < _b) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_GE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
|
||||
} while (0)
|
||||
|
||||
#define KNOWN_FAILING \
|
||||
Test_ExpectedResult(TEST_RESULT_FAIL)
|
||||
|
||||
#endif
|
826
test/test_battle.h
Normal file
826
test/test_battle.h
Normal file
@ -0,0 +1,826 @@
|
||||
/* Embedded DSL for automated black-box testing of battle mechanics.
|
||||
*
|
||||
* To run all the tests use:
|
||||
* make check
|
||||
* To run specific tests, e.g. Spikes ones, use:
|
||||
* make check TESTS='Spikes'
|
||||
* To build a ROM (pokemerald-test.elf) that can be opened in mgba to
|
||||
* view specific tests, e.g. Spikes ones, use:
|
||||
* make pokeemerald-test.elf TESTS='Spikes'
|
||||
*
|
||||
* Manually testing a battle mechanic often follows this pattern:
|
||||
* 1. Create a party which can activate the mechanic.
|
||||
* 2. Start a battle and play a few turns which activate the mechanic.
|
||||
* 3. Look at the UI outputs to decide if the mechanic works.
|
||||
*
|
||||
* Automated testing follows the same pattern:
|
||||
* 1. Initialize the party in GIVEN.
|
||||
* 2. Play the turns in WHEN.
|
||||
* 3. Check the UI outputs in SCENE.
|
||||
*
|
||||
* As a concrete example, to manually test EFFECT_PARALYZE, e.g. the
|
||||
* effect of Stun Spore you might:
|
||||
* 1. Put a Wobbuffet that knows Stun Spore in your party.
|
||||
* 2. Battle a wild Wobbuffet.
|
||||
* 3. Use Stun Spore.
|
||||
* 4. Check that the Wobbuffet is paralyzed.
|
||||
*
|
||||
* This can be translated to an automated test as follows:
|
||||
*
|
||||
* ASSUMPTIONS
|
||||
* {
|
||||
* ASSUME(gBattleMoves[MOVE_STUN_SPORE].effect == EFFECT_PARALYZE);
|
||||
* }
|
||||
*
|
||||
* SINGLE_BATTLE_TEST("Stun Spore inflicts paralysis")
|
||||
* {
|
||||
* GIVEN {
|
||||
* PLAYER(SPECIES_WOBBUFFET); // 1.
|
||||
* OPPONENT(SPECIES_WOBBUFFET); // 2.
|
||||
* } WHEN {
|
||||
* TURN { MOVE(player, MOVE_STUN_SPORE); } // 3.
|
||||
* } SCENE {
|
||||
* ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, player);
|
||||
* MESSAGE("Foe Wobbuffet is paralyzed! It may be unable to move!"); // 4
|
||||
* STATUS_ICON(opponent, paralysis: TRUE); // 4.
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The ASSUMPTIONS block documents that Stun Spore has EFFECT_PARALYZE.
|
||||
* If Stun Spore did not have that effect it would cause the tests in
|
||||
* the file to be skipped. We write our tests like this so that hackers
|
||||
* can change the effects of moves without causing tests to fail.
|
||||
*
|
||||
* SINGLE_BATTLE_TEST defines the name of the test. Related tests should
|
||||
* start with the same prefix, e.g. Stun Spore tests should start with
|
||||
* "Stun Spore", this allows just the Stun Spore-related tests to be run
|
||||
* with:
|
||||
* make check TESTS='Stun Spore'
|
||||
*
|
||||
* GIVEN initializes the parties, PLAYER and OPPONENT add a Pokémon to
|
||||
* their respective parties. They can both accept a block which further
|
||||
* customizes the Pokémon's stats, moves, item, ability, etc.
|
||||
*
|
||||
* WHEN describes the turns, and TURN describes the choices made in a
|
||||
* single turn. MOVE causes the player to use Stun Spore and adds the
|
||||
* move to the Pokémon's moveset if an explicit Moves was not specified.
|
||||
* Pokémon that are not mentioned in a TURN use Celebrate.
|
||||
* The test runner attempts to rig the RNG so that the first move used
|
||||
* in a turn does not miss and activates its secondary effects (if any).
|
||||
*
|
||||
* SCENE describes the player-visible output of the battle. In this case
|
||||
* ANIMATION checks that the Stun Spore animation played, MESSAGE checks
|
||||
* the paralysis message was shown, and STATUS_ICON checks that the
|
||||
* opponent's HP bar shows a PRZ icon.
|
||||
*
|
||||
* As a second example, to manually test that Stun Spore does not effect
|
||||
* Grass-types you might:
|
||||
* 1. Put a Wobbuffet that knows Stun Spore in your party.
|
||||
* 2. Battle a wild Oddish.
|
||||
* 3. Use Stun Spore.
|
||||
* 4. Check that the move animation does not play.
|
||||
* 5. Check that a "It doesn't affect Foe Oddish…" message is shown.
|
||||
*
|
||||
* This can again be translated as follows:
|
||||
*
|
||||
* SINGLE_BATTLE_TEST("Stun Spore does not affect Grass-types")
|
||||
* {
|
||||
* GIVEN {
|
||||
* ASSUME(gBattleMoves[MOVE_STUN_SPORE].flags & FLAG_POWDER);
|
||||
* ASSUME(gSpeciesInfo[SPECIES_ODDISH].types[0] == TYPE_GRASS);
|
||||
* PLAYER(SPECIES_ODDISH); // 1.
|
||||
* OPPONENT(SPECIES_ODDISH); // 2.
|
||||
* } WHEN {
|
||||
* TURN { MOVE(player, MOVE_STUN_SPORE); } // 3.
|
||||
* } SCENE {
|
||||
* NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, player); // 4.
|
||||
* MESSAGE("It doesn't affect Foe Oddish…"); // 5.
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The ASSUMEs are documenting the reasons why Stun Spore does not
|
||||
* affect Oddish, namely that Stun Spore is a powder move, and Oddish
|
||||
* is a Grass-type. These ASSUMEs function similarly to the ones in
|
||||
* ASSUMPTIONS but apply only to the one test.
|
||||
*
|
||||
* NOT inverts the meaning of a SCENE check, so applying it to ANIMATION
|
||||
* requires that the Stun Spore animation does not play. MESSAGE checks
|
||||
* that the message was shown. The checks in SCENE are ordered, so
|
||||
* together this says "The doesn't affect message is shown, and the Stun
|
||||
* Spore animation does not play at any time before that". Normally you
|
||||
* would only test one or the other, or even better, just
|
||||
* NOT STATUS_ICON(opponent, paralysis: TRUE); to say that Oddish was
|
||||
* not paralyzed without specifying the exact outputs which led to that.
|
||||
*
|
||||
* As a final example, to test that Howl works you might:
|
||||
* 1. Put a Wobbuffet that knows Howl and Tackle in your party.
|
||||
* 2. Battle a wild Wobbuffet.
|
||||
* 3. Use Tackle and note the amount the HP bar reduced.
|
||||
* 4. Battle a wild Wobbuffet.
|
||||
* 5. Use Howl and that that the stat change animation and message play.
|
||||
* 6. Use Tackle and check that the HP bar reduced by more than in 3.
|
||||
*
|
||||
* This can be translated to an automated test as follows:
|
||||
*
|
||||
* SINGLE_BATTLE_TEST("Howl raises Attack", s16 damage)
|
||||
* {
|
||||
* bool32 raiseAttack;
|
||||
* PARAMETRIZE { raiseAttack = FALSE; }
|
||||
* PARAMETRIZE { raiseAttack = TRUE; }
|
||||
* GIVEN {
|
||||
* ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
|
||||
* PLAYER(SPECIES_WOBBUFFET);
|
||||
* OPPONENT(SPECIES_WOBBUFFET);
|
||||
* } WHEN {
|
||||
* if (raiseAttack) TURN { MOVE(player, MOVE_HOWL); } // 5.
|
||||
* TURN { MOVE(player, MOVE_TACKLE); } // 3 & 6.
|
||||
* } SCENE {
|
||||
* if (raiseAttack) {
|
||||
* ANIMATION(ANIM_TYPE_MOVE, MOVE_HOWL, player);
|
||||
* ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); // 5.
|
||||
* MESSAGE("Wobbuffet's attack rose!"); // 5.
|
||||
* }
|
||||
* ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
|
||||
* HP_BAR(opponent, captureDamage: &results[i].damage); // 3 & 6.
|
||||
* } FINALLY {
|
||||
* EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); // 6.
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* PARAMETRIZE causes a test to run multiple times, once per PARAMETRIZE
|
||||
* block (e.g. once with raiseAttack = FALSE and once with raiseAttack =
|
||||
* TRUE).
|
||||
* HP_BAR's captureDamage causes the change in HP to be stored in a
|
||||
* variable, and the variable chosen is results[i].damage. results[i]
|
||||
* contains all the variables defined at the end of SINGLE_BATTLE_TEST,
|
||||
* i is the current PARAMETRIZE index.
|
||||
* FINALLY runs after the last parameter has finished, and uses
|
||||
* EXPECT_MUL_EQ to check that the second battle deals 1.5× the damage
|
||||
* of the first battle (with a small tolerance to account for rounding).
|
||||
*
|
||||
* You might notice that all the tests check the outputs the player
|
||||
* could see rather than the internal battle state. e.g. the Howl test
|
||||
* could have used gBattleMons[B_POSITION_OPPONENT_LEFT].hp instead of
|
||||
* using HP_BAR to capture the damage. This is a deliberate choice, by
|
||||
* checking what the player can observe the tests are more robust to
|
||||
* refactoring, e.g. if gBattleMons got moved into gBattleStruct then
|
||||
* any test that used it would need to be updated.
|
||||
*
|
||||
* REFERENCE
|
||||
* =========
|
||||
*
|
||||
* ASSUME(cond)
|
||||
* Causes the test to be skipped if cond is false. Used to document any
|
||||
* prerequisites of the test, e.g. to test Burn reducing the Attack of a
|
||||
* Pokémon we can observe the damage of a physical attack with and
|
||||
* without the burn. To document that this test assumes the attack is
|
||||
* physical we can use:
|
||||
* ASSUME(gBattleMoves[MOVE_WHATEVER].split == SPLIT_PHYSICAL);
|
||||
*
|
||||
* ASSUMPTIONS
|
||||
* Should be placed immediately after any #includes and contain any
|
||||
* ASSUMEs which should apply to the whole file, e.g. to test
|
||||
* EFFECT_POISON_HIT we need to choose a move with that effect, if
|
||||
* we chose to use Poison Sting in every test then the top of
|
||||
* move_effect_poison_hit.c should be:
|
||||
* ASSUMPTIONS
|
||||
* {
|
||||
* ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
|
||||
* }
|
||||
*
|
||||
* SINGLE_BATTLE_TEST(name, results...) and DOUBLE_BATTLE_TEST(name, results...)
|
||||
* Define single- and double- battles. The names should start with the
|
||||
* name of the mechanic being tested so that it is easier to run all the
|
||||
* related tests. results contains variable declarations to be placed
|
||||
* into the results array which is available in PARAMETRIZEd tests.
|
||||
* The main differences for doubles are:
|
||||
* - Move targets sometimes need to be explicit.
|
||||
* - Instead of player and opponent there is playerLeft, playerRight,
|
||||
* opponentLeft, and opponentRight.
|
||||
*
|
||||
* KNOWN_FAILING
|
||||
* Marks a test as not passing due to a bug. If there is an issue number
|
||||
* associated with the bug it should be included in a comment. If the
|
||||
* test passes the developer will be notified to remove KNOWN_FAILING.
|
||||
* For example:
|
||||
* SINGLE_BATTLE_TEST("Jump Kick has no recoil if no target")
|
||||
* {
|
||||
* KNOWN_FAILING; // #2596.
|
||||
*
|
||||
* PARAMETRIZE
|
||||
* Runs a test multiple times. i will be set to which parameter is
|
||||
* running, and results will contain an entry for each parameter, e.g.:
|
||||
* SINGLE_BATTLE_TEST("Blaze boosts Fire-type moves in a pinch", s16 damage)
|
||||
* {
|
||||
* u16 hp;
|
||||
* PARAMETRIZE { hp = 99; }
|
||||
* PARAMETRIZE { hp = 33; }
|
||||
* GIVEN {
|
||||
* ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE);
|
||||
* PLAYER(SPECIES_CHARMANDER) { Ability(ABILITY_BLAZE); MaxHP(99); HP(hp); }
|
||||
* OPPONENT(SPECIES_WOBBUFFET);
|
||||
* } WHEN {
|
||||
* TURN { MOVE(player, MOVE_EMBER); }
|
||||
* } SCENE {
|
||||
* HP_BAR(opponent, captureDamage: &results[i].damage);
|
||||
* } FINALLY {
|
||||
* EXPECT(results[1].damage > results[0].damage);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* PASSES_RANDOMLY(successes, trials)
|
||||
* Checks that the test passes approximately successes/trials. Used for
|
||||
* testing RNG-based attacks, e.g.:
|
||||
* PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100);
|
||||
* Note that PASSES_RANDOMLY makes the tests run very slowly and should
|
||||
* be avoided where possible.
|
||||
*
|
||||
* GIVEN
|
||||
* Contains the initial state of the parties before the battle.
|
||||
*
|
||||
* RNGSeed(seed)
|
||||
* Explicitly sets the RNG seed. Try to avoid using this because it is a
|
||||
* very fragile tool.
|
||||
* Example:
|
||||
* GIVEN {
|
||||
* RNGSeed(0xC0DEIDEA);
|
||||
*
|
||||
* PLAYER(species) and OPPONENT(species)
|
||||
* Adds the species to the player's or opponent's party respectively.
|
||||
* The Pokémon can be further customized with the following functions:
|
||||
* - Gender(MON_MALE | MON_FEMALE)
|
||||
* - Nature(nature)
|
||||
* - Ability(ability)
|
||||
* - Level(level)
|
||||
* - MaxHP(n), HP(n), Attack(n), Defense(n), SpAttack(n), SpDefense(n)
|
||||
* - Speed(n)
|
||||
* - Item(item)
|
||||
* - Moves(moves...)
|
||||
* - Friendship(friendship)
|
||||
* - Status1(status1)
|
||||
* For example to create a Wobbuffet that is poisoned:
|
||||
* PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
|
||||
* Note if Speed is specified for any Pokémon then it must be specified
|
||||
* for all Pokémon.
|
||||
* Note if Moves is specified then MOVE will not automatically add moves
|
||||
* to the moveset.
|
||||
*
|
||||
* WHEN
|
||||
* Contains the choices that battlers make during the battle.
|
||||
*
|
||||
* TURN
|
||||
* Groups the choices made by the battlers on a single turn. If Speeds
|
||||
* have not been explicitly specified then the order of the MOVEs in the
|
||||
* TURN will be used to infer the Speeds of the Pokémon, e.g.:
|
||||
* // player's speed will be greater than opponent's speed.
|
||||
* TURN { MOVE(player, MOVE_SPLASH); MOVE(opponent, MOVE_SPLASH); }
|
||||
* // opponent's speed will be greater than player's speed.
|
||||
* TURN { MOVE(opponent, MOVE_SPLASH); MOVE(player, MOVE_SPLASH); }
|
||||
* The inference process is naive, if your test contains anything that
|
||||
* modifies the speed of a battler you should specify them explicitly.
|
||||
*
|
||||
* MOVE(battler, move | moveSlot:, [megaEvolve:], [hit:], [criticalHit:], [target:], [allowed:])
|
||||
* Used when the battler chooses Fight. Either the move ID or move slot
|
||||
* must be specified. megaEvolve: TRUE causes the battler to Mega Evolve
|
||||
* if able, hit: FALSE causes the move to miss, criticalHit: TRUE causes
|
||||
* the move to land a critical hit, target: is used in double battles to
|
||||
* choose the target (when necessary), and allowed: FALSE is used to
|
||||
* reject an illegal move e.g. a Disabled one.
|
||||
* MOVE(playerLeft, MOVE_TACKLE, target: opponentRight);
|
||||
* If the battler does not have an explicit Moves specified the moveset
|
||||
* will be populated based on the MOVEs it uses.
|
||||
*
|
||||
* FORCED_MOVE(battler)
|
||||
* Used when the battler chooses Fight and then their move is chosen for
|
||||
* them, e.g. when affected by Encore.
|
||||
* FORCED_MOVE(player);
|
||||
*
|
||||
* SWITCH(battler, partyIndex)
|
||||
* Used when the battler chooses Switch.
|
||||
* SWITCH(player, 1);
|
||||
*
|
||||
* SKIP_TURN(battler)
|
||||
* Used when the battler cannot choose an action, e.g. when locked into
|
||||
* Thrash.
|
||||
* SKIP_TURN(player);
|
||||
*
|
||||
* SEND_OUT(battler, partyIndex)
|
||||
* Used when the battler chooses to switch to another Pokémon but not
|
||||
* via Switch, e.g. after fainting or due to a U-turn.
|
||||
* SEND_OUT(player, 1);
|
||||
*
|
||||
* SCENE
|
||||
* Contains an abridged description of the UI during the THEN. The order
|
||||
* of the description must match too, e.g.
|
||||
* // ABILITY_POPUP followed by a MESSAGE
|
||||
* ABILITY_POPUP(player, ABILITY_STURDY);
|
||||
* MESSAGE("Geodude was protected by Sturdy!");
|
||||
*
|
||||
* ABILITY_POPUP(battler, [ability])
|
||||
* Causes the test to fail if the battler's ability pop-up is not shown.
|
||||
* If specified, ability is the ability shown in the pop-up.
|
||||
* ABILITY_POPUP(opponent, ABILITY_MOLD_BREAKER);
|
||||
*
|
||||
* ANIMATION(type, animId, [battler], [target:])
|
||||
* Causes the test to fail if the animation does not play. A common use
|
||||
* of this command is to check if a move was successful, e.g.:
|
||||
* ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
|
||||
* target can only be specified for ANIM_TYPE_MOVE.
|
||||
*
|
||||
* HP_BAR(battler, [damage: | hp: | captureDamage: | captureHP:])
|
||||
* If hp: or damage: are used, causes the test to fail if that amount of
|
||||
* damage is not dealt, e.g.:
|
||||
* HP_BAR(player, hp: 0);
|
||||
* If captureDamage: or captureHP: are used, causes the test to fail if
|
||||
* the HP bar does not change, and then writes that change to the
|
||||
* pointer, e.g.:
|
||||
* s16 damage;
|
||||
* HP_BAR(player, captureDamage: &damage);
|
||||
* If none of the above are used, causes the test to fail if the HP
|
||||
* changes at all.
|
||||
*
|
||||
* MESSAGE(pattern)
|
||||
* Causes the test to fail if the message in pattern is not displayed.
|
||||
* Spaces in pattern match newlines (\n, \l, and \p) in the message.
|
||||
* Often used to check that a battler took its turn but it failed, e.g.:
|
||||
* MESSAGE("Wobbuffet used Dream Eater!");
|
||||
* MESSAGE("Foe Wobbuffet wasn't affected!");
|
||||
*
|
||||
* STATUS_ICON(battler, status1 | none: | sleep: | poison: | burn: | freeze: | paralysis:, badPoison:)
|
||||
* Causes the test to fail if the battler's status is not changed to the
|
||||
* specified status.
|
||||
* STATUS_ICON(player, badPoison: TRUE);
|
||||
* If the expected status icon is parametrized the corresponding STATUS1
|
||||
* constant can be provided, e.g.:
|
||||
* u32 status1;
|
||||
* PARAMETRIZE { status1 = STATUS1_NONE; }
|
||||
* PARAMETRIZE { status1 = STATUS1_BURN; }
|
||||
* ...
|
||||
* STATUS_ICON(player, status1);
|
||||
*
|
||||
* NOT
|
||||
* Causes the test to fail if the SCENE command succeeds before the
|
||||
* following command succeeds.
|
||||
* // Our Wobbuffet does not Celebrate before the foe's.
|
||||
* NOT MESSAGE("Wobbuffet used Celebrate!");
|
||||
* MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
* WARNING: NOT is an alias of NONE_OF, so it behaves surprisingly when
|
||||
* applied to multiple commands wrapped in braces.
|
||||
*
|
||||
* ONE_OF
|
||||
* Causes the test to fail unless one of the SCENE commands succeeds.
|
||||
* ONE_OF {
|
||||
* MESSAGE("Wobbuffet used Celebrate!");
|
||||
* MESSAGE("Wobbuffet is paralyzed! It can't move!");
|
||||
* }
|
||||
*
|
||||
* NONE_OF
|
||||
* Causes the test to fail if one of the SCENE commands succeeds before
|
||||
* the command after the NONE_OF succeeds.
|
||||
* // Our Wobbuffet does not move before the foe's.
|
||||
* NONE_OF {
|
||||
* MESSAGE("Wobbuffet used Celebrate!");
|
||||
* MESSAGE("Wobbuffet is paralyzed! It can't move!");
|
||||
* }
|
||||
* MESSAGE("Foe Wobbuffet used Celebrate!");
|
||||
*
|
||||
* PLAYER_PARTY and OPPONENT_PARTY
|
||||
* Refer to the party members defined in GIVEN, e.g.:
|
||||
* s32 maxHP = GetMonData(&OPPONENT_PARTY[0], MON_DATA_MAX_HP);
|
||||
* HP_BAR(opponent, damage: maxHP / 2);
|
||||
*
|
||||
* THEN
|
||||
* Contains code to run after the battle has finished. If the test is
|
||||
* PARAMETRIZEd then EXPECTs between the results should go here. Is also
|
||||
* occasionally used to check the internal battle state when checking
|
||||
* the behavior via a SCENE is too difficult, verbose, or error-prone.
|
||||
*
|
||||
* FINALLY
|
||||
* Contains checks to run after all PARAMETERIZEs have run. Prefer to
|
||||
* write your checks in THEN where possible, because a failure in THEN
|
||||
* will be tagged with which parameter it corresponds to.
|
||||
*
|
||||
* EXPECT(cond)
|
||||
* Causes the test to fail if cond is false.
|
||||
*
|
||||
* EXPECT_EQ(a, b), EXPECT_NE(a, b), EXPECT_LT(a, b), EXPECT_LE(a, b), EXPECT_GT(a, b), EXPECT_GE(a, b)
|
||||
* Causes the test to fail if a and b compare incorrectly, e.g.
|
||||
* EXPECT_EQ(results[0].damage, results[1].damage);
|
||||
*
|
||||
* EXPECT_MUL_EQ(a, m, b)
|
||||
* Causes the test to fail if a*m != b (within a threshold), e.g.
|
||||
* // Expect results[0].damage * 1.5 == results[1].damage.
|
||||
* EXPECT_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); */
|
||||
|
||||
#ifndef GUARD_TEST_BATTLE_H
|
||||
#define GUARD_TEST_BATTLE_H
|
||||
|
||||
#include "battle.h"
|
||||
#include "battle_anim.h"
|
||||
#include "data.h"
|
||||
#include "item.h"
|
||||
#include "recorded_battle.h"
|
||||
#include "test.h"
|
||||
#include "util.h"
|
||||
#include "constants/abilities.h"
|
||||
#include "constants/battle_anim.h"
|
||||
#include "constants/battle_move_effects.h"
|
||||
#include "constants/hold_effects.h"
|
||||
#include "constants/items.h"
|
||||
#include "constants/moves.h"
|
||||
#include "constants/species.h"
|
||||
|
||||
// NOTE: If the stack is too small the test runner will probably crash
|
||||
// or loop.
|
||||
#define BATTLE_TEST_STACK_SIZE 1024
|
||||
#define MAX_QUEUED_EVENTS 16
|
||||
|
||||
enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES };
|
||||
|
||||
typedef void (*SingleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *);
|
||||
typedef void (*DoubleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *);
|
||||
|
||||
struct BattleTest
|
||||
{
|
||||
u8 type;
|
||||
u16 sourceLine;
|
||||
union
|
||||
{
|
||||
SingleBattleTestFunction singles;
|
||||
DoubleBattleTestFunction doubles;
|
||||
} function;
|
||||
size_t resultsSize;
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
QUEUED_ABILITY_POPUP_EVENT,
|
||||
QUEUED_ANIMATION_EVENT,
|
||||
QUEUED_HP_EVENT,
|
||||
QUEUED_MESSAGE_EVENT,
|
||||
QUEUED_STATUS_EVENT,
|
||||
};
|
||||
|
||||
struct QueuedAbilityEvent
|
||||
{
|
||||
u8 battlerId;
|
||||
u16 ability;
|
||||
};
|
||||
|
||||
struct QueuedAnimationEvent
|
||||
{
|
||||
u8 type;
|
||||
u16 id;
|
||||
u8 attacker:4;
|
||||
u8 target:4;
|
||||
};
|
||||
|
||||
enum { HP_EVENT_NEW_HP, HP_EVENT_DELTA_HP };
|
||||
|
||||
struct QueuedHPEvent
|
||||
{
|
||||
u32 battlerId:3;
|
||||
u32 type:1;
|
||||
u32 address:28;
|
||||
};
|
||||
|
||||
struct QueuedMessageEvent
|
||||
{
|
||||
const u8 *pattern;
|
||||
};
|
||||
|
||||
struct QueuedStatusEvent
|
||||
{
|
||||
u32 battlerId:3;
|
||||
u32 mask:8;
|
||||
u32 unused_01:21;
|
||||
};
|
||||
|
||||
struct QueuedEvent
|
||||
{
|
||||
u8 type;
|
||||
u8 sourceLineOffset;
|
||||
u8 groupType:2;
|
||||
u8 groupSize:6;
|
||||
union
|
||||
{
|
||||
struct QueuedAbilityEvent ability;
|
||||
struct QueuedAnimationEvent animation;
|
||||
struct QueuedHPEvent hp;
|
||||
struct QueuedMessageEvent message;
|
||||
struct QueuedStatusEvent status;
|
||||
} as;
|
||||
};
|
||||
|
||||
struct BattleTestData
|
||||
{
|
||||
u8 stack[BATTLE_TEST_STACK_SIZE];
|
||||
|
||||
u8 playerPartySize;
|
||||
u8 opponentPartySize;
|
||||
u8 explicitMoves[NUM_BATTLE_SIDES];
|
||||
bool8 hasExplicitSpeeds;
|
||||
u8 explicitSpeeds[NUM_BATTLE_SIDES];
|
||||
u16 slowerThan[NUM_BATTLE_SIDES][PARTY_SIZE];
|
||||
u8 currentSide;
|
||||
u8 currentPartyIndex;
|
||||
struct Pokemon *currentMon;
|
||||
u8 gender;
|
||||
u8 nature;
|
||||
|
||||
u8 currentMonIndexes[MAX_BATTLERS_COUNT];
|
||||
u8 turnState;
|
||||
u8 turns;
|
||||
u8 actionBattlers;
|
||||
u8 moveBattlers;
|
||||
bool8 hasRNGActions:1;
|
||||
|
||||
struct RecordedBattleSave recordedBattle;
|
||||
u8 battleRecordTypes[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
|
||||
u8 battleRecordSourceLineOffsets[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
|
||||
u16 recordIndexes[MAX_BATTLERS_COUNT];
|
||||
u8 lastActionTurn;
|
||||
u8 nextRNGTurn;
|
||||
|
||||
u8 queuedEventsCount;
|
||||
u8 queueGroupType;
|
||||
u8 queueGroupStart;
|
||||
u8 queuedEvent;
|
||||
struct QueuedEvent queuedEvents[MAX_QUEUED_EVENTS];
|
||||
};
|
||||
|
||||
struct BattleTestRunnerState
|
||||
{
|
||||
u8 battlersCount;
|
||||
u8 parametersCount; // Valid only in BattleTest_Setup.
|
||||
u8 parameters;
|
||||
u8 runParameter;
|
||||
u8 trials;
|
||||
u8 expectedPasses;
|
||||
u8 observedPasses;
|
||||
u8 skippedTrials;
|
||||
u8 runTrial;
|
||||
bool8 runRandomly:1;
|
||||
bool8 runGiven:1;
|
||||
bool8 runWhen:1;
|
||||
bool8 runScene:1;
|
||||
bool8 runThen:1;
|
||||
bool8 runFinally:1;
|
||||
bool8 runningFinally:1;
|
||||
struct BattleTestData data;
|
||||
u8 *results;
|
||||
u8 checkProgressParameter;
|
||||
u8 checkProgressTrial;
|
||||
u8 checkProgressTurn;
|
||||
};
|
||||
|
||||
extern const struct TestRunner gBattleTestRunner;
|
||||
extern struct BattleTestRunnerState *gBattleTestRunnerState;
|
||||
|
||||
#define MEMBERS(...) VARARG_8(MEMBERS_, __VA_ARGS__)
|
||||
#define MEMBERS_0()
|
||||
#define MEMBERS_1(a) a;
|
||||
#define MEMBERS_2(a, b) a; b;
|
||||
#define MEMBERS_3(a, b, c) a; b; c;
|
||||
#define MEMBERS_4(a, b, c, d) a; b; c; d;
|
||||
#define MEMBERS_5(a, b, c, d, e) a; b; c; d; e;
|
||||
#define MEMBERS_6(a, b, c, d, e, f) a; b; c; d; e; f;
|
||||
#define MEMBERS_7(a, b, c, d, e, f, g) a; b; c; d; e; f; g;
|
||||
#define MEMBERS_8(a, b, c, d, e, f, g, h) a; b; c; d; e; f; g; h;
|
||||
|
||||
#define APPEND_TRUE(...) VARARG_8(APPEND_TRUE_, __VA_ARGS__)
|
||||
#define APPEND_TRUE_0()
|
||||
#define APPEND_TRUE_1(a) a, TRUE
|
||||
#define APPEND_TRUE_2(a, b) a, TRUE, b, TRUE
|
||||
#define APPEND_TRUE_3(a, b, c) a, TRUE, b, TRUE, c, TRUE
|
||||
#define APPEND_TRUE_4(a, b, c, d) a, TRUE, b, TRUE, c, TRUE, d, TRUE
|
||||
#define APPEND_TRUE_5(a, b, c, d, e) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE
|
||||
#define APPEND_TRUE_6(a, b, c, d, e, f) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE
|
||||
#define APPEND_TRUE_7(a, b, c, d, e, f, g) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE, g, TRUE
|
||||
#define APPEND_TRUE_8(a, b, c, d, e, f, g, h) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE, g, TRUE, h, TRUE
|
||||
|
||||
/* Test */
|
||||
|
||||
#define SINGLE_BATTLE_TEST(_name, ...) \
|
||||
struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \
|
||||
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \
|
||||
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
|
||||
{ \
|
||||
.name = _name, \
|
||||
.filename = __FILE__, \
|
||||
.runner = &gBattleTestRunner, \
|
||||
.data = (void *)&(const struct BattleTest) \
|
||||
{ \
|
||||
.type = BATTLE_TEST_SINGLES, \
|
||||
.sourceLine = __LINE__, \
|
||||
.function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \
|
||||
.resultsSize = sizeof(struct CAT(Result, __LINE__)), \
|
||||
}, \
|
||||
}; \
|
||||
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent)
|
||||
|
||||
#define DOUBLE_BATTLE_TEST(_name, ...) \
|
||||
struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \
|
||||
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); \
|
||||
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
|
||||
{ \
|
||||
.name = _name, \
|
||||
.filename = __FILE__, \
|
||||
.runner = &gBattleTestRunner, \
|
||||
.data = (void *)&(const struct BattleTest) \
|
||||
{ \
|
||||
.type = BATTLE_TEST_DOUBLES, \
|
||||
.sourceLine = __LINE__, \
|
||||
.function = { .doubles = (DoubleBattleTestFunction)CAT(Test, __LINE__) }, \
|
||||
.resultsSize = sizeof(struct CAT(Result, __LINE__)), \
|
||||
}, \
|
||||
}; \
|
||||
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *playerLeft, struct BattlePokemon *opponentLeft, struct BattlePokemon *playerRight, struct BattlePokemon *opponentRight)
|
||||
|
||||
/* Parametrize */
|
||||
|
||||
#define PARAMETRIZE if (gBattleTestRunnerState->parametersCount++ == i)
|
||||
|
||||
/* Randomly */
|
||||
|
||||
#define PASSES_RANDOMLY(passes, trials) for (; gBattleTestRunnerState->runRandomly; gBattleTestRunnerState->runRandomly = FALSE) Randomly(__LINE__, passes, trials)
|
||||
|
||||
void Randomly(u32 sourceLine, u32 passes, u32 trials);
|
||||
|
||||
/* Given */
|
||||
|
||||
#define GIVEN for (; gBattleTestRunnerState->runGiven; gBattleTestRunnerState->runGiven = FALSE)
|
||||
|
||||
#define RNGSeed(seed) RNGSeed_(__LINE__, seed)
|
||||
|
||||
#define PLAYER(species) for (OpenPokemon(__LINE__, B_SIDE_PLAYER, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__))
|
||||
#define OPPONENT(species) for (OpenPokemon(__LINE__, B_SIDE_OPPONENT, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__))
|
||||
|
||||
#define Gender(gender) Gender_(__LINE__, gender)
|
||||
#define Nature(nature) Nature_(__LINE__, nature)
|
||||
#define Ability(ability) Ability_(__LINE__, ability)
|
||||
#define Level(level) Level_(__LINE__, level)
|
||||
#define MaxHP(maxHP) MaxHP_(__LINE__, maxHP)
|
||||
#define HP(hp) HP_(__LINE__, hp)
|
||||
#define Attack(attack) Attack_(__LINE__, attack)
|
||||
#define Defense(defense) Defense_(__LINE__, defense)
|
||||
#define SpAttack(spAttack) SpAttack_(__LINE__, spAttack)
|
||||
#define SpDefense(spDefense) SpDefense_(__LINE__, spDefense)
|
||||
#define Speed(speed) Speed_(__LINE__, speed)
|
||||
#define Item(item) Item_(__LINE__, item)
|
||||
#define Moves(move1, ...) Moves_(__LINE__, (const u16 [MAX_MON_MOVES]) { move1, __VA_ARGS__ })
|
||||
#define Friendship(friendship) Friendship_(__LINE__, friendship)
|
||||
#define Status1(status1) Status1_(__LINE__, status1)
|
||||
|
||||
void OpenPokemon(u32 sourceLine, u32 side, u32 species);
|
||||
void ClosePokemon(u32 sourceLine);
|
||||
|
||||
void RNGSeed_(u32 sourceLine, u32 seed);
|
||||
void Gender_(u32 sourceLine, u32 gender);
|
||||
void Nature_(u32 sourceLine, u32 nature);
|
||||
void Ability_(u32 sourceLine, u32 ability);
|
||||
void Level_(u32 sourceLine, u32 level);
|
||||
void MaxHP_(u32 sourceLine, u32 maxHP);
|
||||
void HP_(u32 sourceLine, u32 hp);
|
||||
void Attack_(u32 sourceLine, u32 attack);
|
||||
void Defense_(u32 sourceLine, u32 defense);
|
||||
void SpAttack_(u32 sourceLine, u32 spAttack);
|
||||
void SpDefense_(u32 sourceLine, u32 spDefense);
|
||||
void Speed_(u32 sourceLine, u32 speed);
|
||||
void Item_(u32 sourceLine, u32 item);
|
||||
void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]);
|
||||
void Friendship_(u32 sourceLine, u32 friendship);
|
||||
void Status1_(u32 sourceLine, u32 status1);
|
||||
|
||||
#define PLAYER_PARTY (gBattleTestRunnerState->data.recordedBattle.playerParty)
|
||||
#define OPPONENT_PARTY (gBattleTestRunnerState->data.recordedBattle.opponentParty)
|
||||
|
||||
/* When */
|
||||
|
||||
#define WHEN for (; gBattleTestRunnerState->runWhen; gBattleTestRunnerState->runWhen = FALSE)
|
||||
|
||||
enum { TURN_CLOSED, TURN_OPEN, TURN_CLOSING };
|
||||
|
||||
#define TURN for (OpenTurn(__LINE__); gBattleTestRunnerState->data.turnState == TURN_OPEN; CloseTurn(__LINE__))
|
||||
|
||||
#define MOVE(battler, ...) Move(__LINE__, battler, (struct MoveContext) { APPEND_TRUE(__VA_ARGS__) })
|
||||
#define FORCED_MOVE(battler) ForcedMove(__LINE__, battler)
|
||||
#define SWITCH(battler, partyIndex) Switch(__LINE__, battler, partyIndex)
|
||||
#define SKIP_TURN(battler) SkipTurn(__LINE__, battler)
|
||||
#define SEND_OUT(battler, partyIndex) SendOut(__LINE__, battler, partyIndex)
|
||||
|
||||
struct MoveContext
|
||||
{
|
||||
u16 move;
|
||||
u16 explicitMove:1;
|
||||
u16 moveSlot:2;
|
||||
u16 explicitMoveSlot:1;
|
||||
u16 hit:1;
|
||||
u16 explicitHit:1;
|
||||
u16 criticalHit:1;
|
||||
u16 explicitCriticalHit:1;
|
||||
u16 megaEvolve:1;
|
||||
u16 explicitMegaEvolve:1;
|
||||
// TODO: u8 zMove:1;
|
||||
u16 allowed:1;
|
||||
u16 explicitAllowed:1;
|
||||
struct BattlePokemon *target;
|
||||
bool8 explicitTarget;
|
||||
};
|
||||
|
||||
void OpenTurn(u32 sourceLine);
|
||||
void CloseTurn(u32 sourceLine);
|
||||
void Move(u32 sourceLine, struct BattlePokemon *, struct MoveContext);
|
||||
void ForcedMove(u32 sourceLine, struct BattlePokemon *);
|
||||
void Switch(u32 sourceLine, struct BattlePokemon *, u32 partyIndex);
|
||||
void SkipTurn(u32 sourceLine, struct BattlePokemon *);
|
||||
|
||||
void SendOut(u32 sourceLine, struct BattlePokemon *, u32 partyIndex);
|
||||
|
||||
/* Scene */
|
||||
|
||||
#define SCENE for (; gBattleTestRunnerState->runScene; gBattleTestRunnerState->runScene = FALSE)
|
||||
|
||||
#define ONE_OF for (OpenQueueGroup(__LINE__, QUEUE_GROUP_ONE_OF); gBattleTestRunnerState->data.queueGroupType != QUEUE_GROUP_NONE; CloseQueueGroup(__LINE__))
|
||||
#define NONE_OF for (OpenQueueGroup(__LINE__, QUEUE_GROUP_NONE_OF); gBattleTestRunnerState->data.queueGroupType != QUEUE_GROUP_NONE; CloseQueueGroup(__LINE__))
|
||||
#define NOT NONE_OF
|
||||
|
||||
#define ABILITY_POPUP(battler, ...) QueueAbility(__LINE__, battler, (struct AbilityEventContext) { __VA_ARGS__ })
|
||||
#define ANIMATION(type, id, ...) QueueAnimation(__LINE__, type, id, (struct AnimationEventContext) { __VA_ARGS__ })
|
||||
#define HP_BAR(battler, ...) QueueHP(__LINE__, battler, (struct HPEventContext) { APPEND_TRUE(__VA_ARGS__) })
|
||||
#define MESSAGE(pattern) QueueMessage(__LINE__, (const u8 []) _(pattern))
|
||||
#define STATUS_ICON(battler, status) QueueStatus(__LINE__, battler, (struct StatusEventContext) { status })
|
||||
|
||||
enum QueueGroupType
|
||||
{
|
||||
QUEUE_GROUP_NONE,
|
||||
QUEUE_GROUP_ONE_OF,
|
||||
QUEUE_GROUP_NONE_OF,
|
||||
};
|
||||
|
||||
struct AbilityEventContext
|
||||
{
|
||||
u16 ability;
|
||||
};
|
||||
|
||||
struct AnimationEventContext
|
||||
{
|
||||
struct BattlePokemon *attacker;
|
||||
struct BattlePokemon *target;
|
||||
};
|
||||
|
||||
struct HPEventContext
|
||||
{
|
||||
u8 _;
|
||||
u16 hp;
|
||||
bool8 explicitHP;
|
||||
s16 damage;
|
||||
bool8 explicitDamage;
|
||||
u16 *captureHP;
|
||||
bool8 explicitCaptureHP;
|
||||
s16 *captureDamage;
|
||||
bool8 explicitCaptureDamage;
|
||||
};
|
||||
|
||||
struct StatusEventContext
|
||||
{
|
||||
u8 status1;
|
||||
bool8 none:1;
|
||||
bool8 sleep:1;
|
||||
bool8 poison:1;
|
||||
bool8 burn:1;
|
||||
bool8 freeze:1;
|
||||
bool8 paralysis:1;
|
||||
bool8 badPoison:1;
|
||||
};
|
||||
|
||||
void OpenQueueGroup(u32 sourceLine, enum QueueGroupType);
|
||||
void CloseQueueGroup(u32 sourceLine);
|
||||
|
||||
void QueueAbility(u32 sourceLine, struct BattlePokemon *battler, struct AbilityEventContext);
|
||||
void QueueAnimation(u32 sourceLine, u32 type, u32 id, struct AnimationEventContext);
|
||||
void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContext);
|
||||
void QueueMessage(u32 sourceLine, const u8 *pattern);
|
||||
void QueueStatus(u32 sourceLine, struct BattlePokemon *battler, struct StatusEventContext);
|
||||
|
||||
/* Then */
|
||||
|
||||
#define THEN for (; gBattleTestRunnerState->runThen; gBattleTestRunnerState->runThen = FALSE)
|
||||
|
||||
/* Finally */
|
||||
|
||||
#define FINALLY for (; gBattleTestRunnerState->runFinally; gBattleTestRunnerState->runFinally = FALSE) if ((gBattleTestRunnerState->runningFinally = TRUE))
|
||||
|
||||
/* Expect */
|
||||
|
||||
#define EXPECT_MUL_EQ(a, m, b) \
|
||||
do \
|
||||
{ \
|
||||
s32 _a = (a), _m = (m), _b = (b); \
|
||||
s32 _am = Q_4_12_TO_INT(_a * _m); \
|
||||
s32 _t = Q_4_12_TO_INT(abs(_m) + Q_4_12_ROUND); \
|
||||
if (abs(_am-_b) > _t) \
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_MUL_EQ(%d, %q, %d) failed: %d not in [%d..%d]", gTestRunnerState.test->filename, __LINE__, _a, _m, _b, _am, _b-_t, _b+_t); \
|
||||
} while (0)
|
||||
|
||||
#endif
|
439
test/test_runner.c
Normal file
439
test/test_runner.c
Normal file
@ -0,0 +1,439 @@
|
||||
#include <stdarg.h>
|
||||
#include "global.h"
|
||||
#include "characters.h"
|
||||
#include "gpu_regs.h"
|
||||
#include "main.h"
|
||||
#include "malloc.h"
|
||||
#include "test.h"
|
||||
#include "test_runner.h"
|
||||
|
||||
#define TIMEOUT_SECONDS 30
|
||||
|
||||
void CB2_TestRunner(void);
|
||||
|
||||
EWRAM_DATA struct TestRunnerState gTestRunnerState;
|
||||
|
||||
void TestRunner_Battle(const struct Test *);
|
||||
|
||||
static bool32 MgbaOpen_(void);
|
||||
static void MgbaExit_(u8 exitCode);
|
||||
static s32 MgbaPuts_(const char *s);
|
||||
static s32 MgbaVPrintf_(const char *fmt, va_list va);
|
||||
static void Intr_Timer2(void);
|
||||
|
||||
extern const struct Test __start_tests[];
|
||||
extern const struct Test __stop_tests[];
|
||||
|
||||
static bool32 PrefixMatch(const char *pattern, const char *string)
|
||||
{
|
||||
if (string == NULL)
|
||||
return TRUE;
|
||||
|
||||
while (TRUE)
|
||||
{
|
||||
if (!*pattern)
|
||||
return TRUE;
|
||||
if (*pattern != *string)
|
||||
return FALSE;
|
||||
pattern++;
|
||||
string++;
|
||||
}
|
||||
}
|
||||
|
||||
enum { STATE_INIT, STATE_NEXT_TEST, STATE_REPORT_RESULT, STATE_EXIT };
|
||||
|
||||
void CB2_TestRunner(void)
|
||||
{
|
||||
switch (gTestRunnerState.state)
|
||||
{
|
||||
case STATE_INIT:
|
||||
if (!MgbaOpen_())
|
||||
{
|
||||
gTestRunnerState.state = STATE_EXIT;
|
||||
gTestRunnerState.exitCode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
gIntrTable[7] = Intr_Timer2;
|
||||
|
||||
gTestRunnerState.state = STATE_NEXT_TEST;
|
||||
gTestRunnerState.exitCode = 0;
|
||||
gTestRunnerState.tests = 0;
|
||||
gTestRunnerState.passes = 0;
|
||||
gTestRunnerState.skips = 0;
|
||||
gTestRunnerState.skipFilename = NULL;
|
||||
gTestRunnerState.test = __start_tests - 1;
|
||||
break;
|
||||
|
||||
case STATE_NEXT_TEST:
|
||||
gTestRunnerState.test++;
|
||||
|
||||
if (gTestRunnerState.test == __stop_tests)
|
||||
{
|
||||
MgbaPrintf_("%s%d/%d PASSED\e[0m", gTestRunnerState.exitCode == 0 ? "\e[32m" : "\e[31m", gTestRunnerState.passes, gTestRunnerState.tests);
|
||||
if (gTestRunnerState.skips)
|
||||
{
|
||||
if (gTestRunnerSkipIsFail)
|
||||
MgbaPrintf_("\e[31m%d SKIPPED\e[0m", gTestRunnerState.skips);
|
||||
else
|
||||
MgbaPrintf_("%d SKIPPED", gTestRunnerState.skips);
|
||||
}
|
||||
gTestRunnerState.state = STATE_EXIT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PrefixMatch(gTestRunnerArgv, gTestRunnerState.test->name))
|
||||
return;
|
||||
|
||||
// Greedily assign tests to processes based on estimated cost.
|
||||
// TODO: Make processCosts a min heap.
|
||||
if (gTestRunnerState.test->runner != &gAssumptionsRunner)
|
||||
{
|
||||
u32 i;
|
||||
u32 minCost, minCostProcess;
|
||||
minCost = gTestRunnerState.processCosts[0];
|
||||
minCostProcess = 0;
|
||||
for (i = 1; i < gTestRunnerN; i++)
|
||||
{
|
||||
if (gTestRunnerState.processCosts[i] < minCost)
|
||||
{
|
||||
minCost = gTestRunnerState.processCosts[i];
|
||||
minCostProcess = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (gTestRunnerState.test->runner->estimateCost)
|
||||
gTestRunnerState.processCosts[minCostProcess] += gTestRunnerState.test->runner->estimateCost(gTestRunnerState.test->data);
|
||||
else
|
||||
gTestRunnerState.processCosts[minCostProcess] += 1;
|
||||
|
||||
if (minCostProcess != gTestRunnerI)
|
||||
return;
|
||||
}
|
||||
|
||||
gTestRunnerState.state = STATE_REPORT_RESULT;
|
||||
gTestRunnerState.result = TEST_RESULT_PASS;
|
||||
gTestRunnerState.expectedResult = TEST_RESULT_PASS;
|
||||
if (gTestRunnerHeadless)
|
||||
gTestRunnerState.timeoutSeconds = TIMEOUT_SECONDS;
|
||||
else
|
||||
gTestRunnerState.timeoutSeconds = UINT_MAX;
|
||||
InitHeap(gHeap, HEAP_SIZE);
|
||||
EnableInterrupts(INTR_FLAG_TIMER2);
|
||||
REG_TM2CNT_L = UINT16_MAX - (274 * 60); // Approx. 1 second.
|
||||
REG_TM2CNT_H = TIMER_ENABLE | TIMER_INTR_ENABLE | TIMER_1024CLK;
|
||||
|
||||
// NOTE: Assumes that the compiler interns __FILE__.
|
||||
if (gTestRunnerState.skipFilename == gTestRunnerState.test->filename)
|
||||
{
|
||||
gTestRunnerState.result = TEST_RESULT_SKIP;
|
||||
}
|
||||
else
|
||||
{
|
||||
MgbaPrintf_(":N%s", gTestRunnerState.test->name);
|
||||
if (gTestRunnerState.test->runner->setUp)
|
||||
gTestRunnerState.test->runner->setUp(gTestRunnerState.test->data);
|
||||
gTestRunnerState.test->runner->run(gTestRunnerState.test->data);
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_REPORT_RESULT:
|
||||
REG_TM2CNT_H = 0;
|
||||
|
||||
gTestRunnerState.state = STATE_NEXT_TEST;
|
||||
|
||||
if (gTestRunnerState.test->runner->tearDown)
|
||||
gTestRunnerState.test->runner->tearDown(gTestRunnerState.test->data);
|
||||
|
||||
if (gTestRunnerState.test->runner == &gAssumptionsRunner)
|
||||
{
|
||||
if (gTestRunnerState.result != TEST_RESULT_PASS)
|
||||
gTestRunnerState.skipFilename = gTestRunnerState.test->filename;
|
||||
}
|
||||
else if (gTestRunnerState.result == TEST_RESULT_SKIP)
|
||||
{
|
||||
gTestRunnerState.skips++;
|
||||
if (gTestRunnerSkipIsFail)
|
||||
gTestRunnerState.exitCode = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
const char *color;
|
||||
const char *result;
|
||||
|
||||
gTestRunnerState.tests++;
|
||||
|
||||
if (gTestRunnerState.result == gTestRunnerState.expectedResult)
|
||||
{
|
||||
gTestRunnerState.passes++;
|
||||
color = "\e[32m";
|
||||
MgbaPrintf_(":N%s", gTestRunnerState.test->name);
|
||||
}
|
||||
else if (gTestRunnerState.result != TEST_RESULT_SKIP || gTestRunnerSkipIsFail)
|
||||
{
|
||||
gTestRunnerState.exitCode = 1;
|
||||
color = "\e[31m";
|
||||
}
|
||||
else
|
||||
{
|
||||
color = "";
|
||||
}
|
||||
|
||||
if (gTestRunnerState.result == TEST_RESULT_PASS
|
||||
&& gTestRunnerState.result != gTestRunnerState.expectedResult)
|
||||
{
|
||||
MgbaPuts_("\e[31mPlease remove KNOWN_FAILING if this test intentionally PASSes\e[0m");
|
||||
}
|
||||
|
||||
switch (gTestRunnerState.result)
|
||||
{
|
||||
case TEST_RESULT_FAIL: result = "FAIL"; break;
|
||||
case TEST_RESULT_PASS: result = "PASS"; break;
|
||||
case TEST_RESULT_SKIP: result = "SKIP"; break;
|
||||
case TEST_RESULT_INVALID: result = "INVALID"; break;
|
||||
case TEST_RESULT_ERROR: result = "ERROR"; break;
|
||||
case TEST_RESULT_TIMEOUT: result = "TIMEOUT"; break;
|
||||
default: result = "UNKNOWN"; break;
|
||||
}
|
||||
|
||||
MgbaPrintf_(":R%s%s\e[0m", color, result);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_EXIT:
|
||||
MgbaExit_(gTestRunnerState.exitCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Test_ExpectedResult(enum TestResult result)
|
||||
{
|
||||
gTestRunnerState.expectedResult = result;
|
||||
}
|
||||
|
||||
static void Assumptions_Run(void *data)
|
||||
{
|
||||
void (*function)(void) = data;
|
||||
function();
|
||||
}
|
||||
|
||||
const struct TestRunner gAssumptionsRunner =
|
||||
{
|
||||
.run = Assumptions_Run,
|
||||
};
|
||||
|
||||
#define IRQ_LR (*(vu32 *)0x3007F9C)
|
||||
|
||||
/* Returns to AgbMainLoop.
|
||||
* Similar to a longjmp except that we only restore sp (and cpsr via
|
||||
* overwriting the value of lr_irq on the stack).
|
||||
*
|
||||
* WARNING: This could potentially be flaky because other global state
|
||||
* will not be cleaned up, we may decide to Exit on a timeout instead. */
|
||||
static NAKED void JumpToAgbMainLoop(void)
|
||||
{
|
||||
asm(".arm\n\
|
||||
.word 0xe3104778\n\
|
||||
ldr r0, =gAgbMainLoop_sp\n\
|
||||
ldr sp, [r0]\n\
|
||||
ldr r0, =AgbMainLoop\n\
|
||||
bx r0\n\
|
||||
.pool");
|
||||
}
|
||||
|
||||
void ReinitCallbacks(void)
|
||||
{
|
||||
gMain.callback1 = NULL;
|
||||
SetMainCallback2(CB2_TestRunner);
|
||||
gMain.vblankCallback = NULL;
|
||||
gMain.hblankCallback = NULL;
|
||||
}
|
||||
|
||||
static void Intr_Timer2(void)
|
||||
{
|
||||
if (--gTestRunnerState.timeoutSeconds == 0)
|
||||
{
|
||||
if (gTestRunnerState.test->runner->checkProgress
|
||||
&& gTestRunnerState.test->runner->checkProgress(gTestRunnerState.test->data))
|
||||
{
|
||||
gTestRunnerState.timeoutSeconds = TIMEOUT_SECONDS;
|
||||
}
|
||||
else
|
||||
{
|
||||
gTestRunnerState.result = TEST_RESULT_TIMEOUT;
|
||||
ReinitCallbacks();
|
||||
IRQ_LR = ((uintptr_t)JumpToAgbMainLoop & ~1) + 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Test_ExitWithResult(enum TestResult result, const char *fmt, ...)
|
||||
{
|
||||
gTestRunnerState.result = result;
|
||||
ReinitCallbacks();
|
||||
if (gTestRunnerState.test->runner->handleExitWithResult
|
||||
&& !gTestRunnerState.test->runner->handleExitWithResult(gTestRunnerState.test->data, result)
|
||||
&& gTestRunnerState.result != gTestRunnerState.expectedResult)
|
||||
{
|
||||
va_list va;
|
||||
va_start(va, fmt);
|
||||
MgbaVPrintf_(fmt, va);
|
||||
}
|
||||
JumpToAgbMainLoop();
|
||||
}
|
||||
|
||||
#define REG_DEBUG_ENABLE (*(vu16 *)0x4FFF780)
|
||||
#define REG_DEBUG_FLAGS (*(vu16 *)0x4FFF700)
|
||||
#define REG_DEBUG_STRING ((char *)0x4FFF600)
|
||||
|
||||
static bool32 MgbaOpen_(void)
|
||||
{
|
||||
REG_DEBUG_ENABLE = 0xC0DE;
|
||||
return REG_DEBUG_ENABLE == 0x1DEA;
|
||||
}
|
||||
|
||||
static void MgbaExit_(u8 exitCode)
|
||||
{
|
||||
register u32 _exitCode asm("r0") = exitCode;
|
||||
asm("swi 0x3" :: "r" (_exitCode));
|
||||
}
|
||||
|
||||
static s32 MgbaPuts_(const char *s)
|
||||
{
|
||||
return MgbaPrintf_("%s", s);
|
||||
}
|
||||
|
||||
s32 MgbaPrintf_(const char *fmt, ...)
|
||||
{
|
||||
va_list va;
|
||||
va_start(va, fmt);
|
||||
return MgbaVPrintf_(fmt, va);
|
||||
}
|
||||
|
||||
static s32 MgbaPutchar_(s32 i, s32 c)
|
||||
{
|
||||
REG_DEBUG_STRING[i++] = c;
|
||||
if (i == 255)
|
||||
{
|
||||
REG_DEBUG_STRING[i] = '\0';
|
||||
REG_DEBUG_FLAGS = MGBA_LOG_INFO | 0x100;
|
||||
i = 0;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
extern const u8 gWireless_RSEtoASCIITable[];
|
||||
|
||||
// Bare-bones, only supports plain %s, %S, and %d.
|
||||
static s32 MgbaVPrintf_(const char *fmt, va_list va)
|
||||
{
|
||||
s32 i = 0;
|
||||
s32 c, d;
|
||||
const char *s;
|
||||
while (*fmt)
|
||||
{
|
||||
switch ((c = *fmt++))
|
||||
{
|
||||
case '%':
|
||||
switch (*fmt++)
|
||||
{
|
||||
case '%':
|
||||
i = MgbaPutchar_(i, '%');
|
||||
break;
|
||||
case 'd':
|
||||
d = va_arg(va, int);
|
||||
if (d == 0)
|
||||
{
|
||||
i = MgbaPutchar_(i, '0');
|
||||
}
|
||||
else
|
||||
{
|
||||
char buffer[10];
|
||||
s32 n = 0;
|
||||
u32 u = abs(d);
|
||||
if (d < 0)
|
||||
i = MgbaPutchar_(i, '-');
|
||||
while (u > 0)
|
||||
{
|
||||
buffer[n++] = '0' + (u % 10);
|
||||
u /= 10;
|
||||
}
|
||||
while (n > 0)
|
||||
i = MgbaPutchar_(i, buffer[--n]);
|
||||
}
|
||||
break;
|
||||
case 'q':
|
||||
d = va_arg(va, int);
|
||||
{
|
||||
char buffer[10];
|
||||
s32 n = 0;
|
||||
u32 u = abs(d) >> 12;
|
||||
if (u == 0)
|
||||
{
|
||||
i = MgbaPutchar_(i, '0');
|
||||
}
|
||||
else
|
||||
{
|
||||
if (d < 0)
|
||||
i = MgbaPutchar_(i, '-');
|
||||
while (u > 0)
|
||||
{
|
||||
buffer[n++] = '0' + (u % 10);
|
||||
u /= 10;
|
||||
}
|
||||
while (n > 0)
|
||||
i = MgbaPutchar_(i, buffer[--n]);
|
||||
}
|
||||
|
||||
n = 0;
|
||||
i = MgbaPutchar_(i, '.');
|
||||
u = d & 0xFFF;
|
||||
while (TRUE)
|
||||
{
|
||||
u *= 10;
|
||||
i = MgbaPutchar_(i, '0' + (u >> 12));
|
||||
u &= 0xFFF;
|
||||
if (u == 0)
|
||||
break;
|
||||
if (++n == 2)
|
||||
{
|
||||
u *= 10;
|
||||
i = MgbaPutchar_(i, '0' + ((u + UQ_4_12_ROUND) >> 12));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
s = va_arg(va, const char *);
|
||||
while ((c = *s++) != '\0')
|
||||
i = MgbaPutchar_(i, c);
|
||||
break;
|
||||
case 'S':
|
||||
s = va_arg(va, const u8 *);
|
||||
while ((c = *s++) != EOS)
|
||||
{
|
||||
if ((c = gWireless_RSEtoASCIITable[c]) != '\0')
|
||||
i = MgbaPutchar_(i, c);
|
||||
else
|
||||
i = MgbaPutchar_(i, '?');
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case '\n':
|
||||
i = 254;
|
||||
i = MgbaPutchar_(i, '\0');
|
||||
break;
|
||||
default:
|
||||
i = MgbaPutchar_(i, c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i != 0)
|
||||
{
|
||||
REG_DEBUG_FLAGS = MGBA_LOG_INFO | 0x100;
|
||||
}
|
||||
return i;
|
||||
}
|
8
test/test_runner_args.c
Normal file
8
test/test_runner_args.c
Normal file
@ -0,0 +1,8 @@
|
||||
#include "global.h"
|
||||
|
||||
// These values are patched by patchelf. Therefore we have put them in
|
||||
// their own TU so that the optimizer cannot inline them.
|
||||
const bool8 gTestRunnerEnabled = TRUE;
|
||||
const u8 gTestRunnerN = 0;
|
||||
const u8 gTestRunnerI = 0;
|
||||
const char gTestRunnerArgv[256] = {'\0'};
|
1545
test/test_runner_battle.c
Normal file
1545
test/test_runner_battle.c
Normal file
File diff suppressed because it is too large
Load Diff
1
tools/mgba-rom-test-hydra/.gitignore
vendored
Normal file
1
tools/mgba-rom-test-hydra/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
mgba-rom-test-hydra
|
18
tools/mgba-rom-test-hydra/Makefile
Normal file
18
tools/mgba-rom-test-hydra/Makefile
Normal file
@ -0,0 +1,18 @@
|
||||
.PHONY: all clean
|
||||
|
||||
SRCS = main.c
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
EXE := .exe
|
||||
else
|
||||
EXE :=
|
||||
endif
|
||||
|
||||
all: mgba-rom-test-hydra$(EXE)
|
||||
@:
|
||||
|
||||
mgba-rom-test-hydra$(EXE): $(SRCS)
|
||||
$(CC) $(SRCS) -o $@ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
$(RM) mgba-rom-test-hydra$(EXE)
|
421
tools/mgba-rom-test-hydra/main.c
Normal file
421
tools/mgba-rom-test-hydra/main.c
Normal file
@ -0,0 +1,421 @@
|
||||
/* mgba-rom-test-hydra. Runs multiple mgba-rom-test processes and
|
||||
* parses the output to display human-readable progress.
|
||||
*
|
||||
* Output lines starting with "GBA Debug: :" are parsed as commands to
|
||||
* Hydra, other output lines starting with "GBA Debug: " are parsed as
|
||||
* output from the current test, and any other lines are parsed as
|
||||
* output from the mgba-rom-test process itself.
|
||||
*
|
||||
* COMMANDS
|
||||
* N: Sets the test name to the remainder of the line.
|
||||
* R: Sets the result to the remainder of the line, and flushes any
|
||||
* output buffered since the previous R. */
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define MAX_PROCESSES 32 // See also test/test.h
|
||||
|
||||
struct Runner
|
||||
{
|
||||
pid_t pid;
|
||||
int outfd;
|
||||
char rom_path[L_tmpnam];
|
||||
char test_name[256];
|
||||
size_t input_buffer_size;
|
||||
size_t input_buffer_capacity;
|
||||
char *input_buffer;
|
||||
size_t output_buffer_size;
|
||||
size_t output_buffer_capacity;
|
||||
char *output_buffer;
|
||||
};
|
||||
|
||||
static unsigned nrunners = 0;
|
||||
static struct Runner *runners = NULL;
|
||||
|
||||
static void handle_read(struct Runner *runner)
|
||||
{
|
||||
char *sol = runner->input_buffer;
|
||||
char *eol;
|
||||
size_t consumed = 0;
|
||||
size_t remaining = runner->input_buffer_size;
|
||||
while ((eol = memchr(sol, '\n', remaining)))
|
||||
{
|
||||
eol++;
|
||||
size_t n = eol - sol;
|
||||
if (runner->input_buffer_size >= strlen("GBA Debug: ")
|
||||
&& !strncmp(sol, "GBA Debug: ", strlen("GBA Debug: ")))
|
||||
{
|
||||
char *soc = sol + strlen("GBA Debug: ");
|
||||
if (soc[0] == ':')
|
||||
{
|
||||
switch (soc[1])
|
||||
{
|
||||
case 'N':
|
||||
soc += 2;
|
||||
if (sizeof(runner->test_name) <= eol - soc - 1)
|
||||
{
|
||||
fprintf(stderr, "test_name too long\n");
|
||||
exit(2);
|
||||
}
|
||||
strncpy(runner->test_name, soc, eol - soc - 1);
|
||||
runner->test_name[eol - soc - 1] = '\0';
|
||||
break;
|
||||
|
||||
case 'R':
|
||||
soc += 2;
|
||||
fprintf(stdout, "%s: ", runner->test_name);
|
||||
fwrite(soc, 1, eol - soc, stdout);
|
||||
fwrite(runner->output_buffer, 1, runner->output_buffer_size, stdout);
|
||||
strcpy(runner->test_name, "WAITING...");
|
||||
runner->output_buffer_size = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
goto buffer_output;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer_output:
|
||||
if (runner->output_buffer_size + eol - soc >= runner->output_buffer_capacity)
|
||||
{
|
||||
runner->output_buffer_capacity *= 2;
|
||||
if (runner->output_buffer_capacity < runner->output_buffer_size + eol - soc)
|
||||
runner->output_buffer_capacity = runner->output_buffer_size + eol - soc;
|
||||
runner->output_buffer = realloc(runner->output_buffer, runner->output_buffer_capacity);
|
||||
if (!runner->output_buffer)
|
||||
{
|
||||
perror("realloc output_buffer failed");
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
memcpy(runner->output_buffer + runner->output_buffer_size, soc, eol - soc);
|
||||
runner->output_buffer_size += eol - soc;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (write(STDOUT_FILENO, sol, eol - sol) == -1)
|
||||
{
|
||||
perror("write failed");
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
sol += n;
|
||||
consumed += n;
|
||||
remaining -= n;
|
||||
}
|
||||
|
||||
memcpy(runner->input_buffer, sol, remaining);
|
||||
runner->input_buffer_size -= consumed;
|
||||
|
||||
if (runner->input_buffer_size == runner->input_buffer_capacity)
|
||||
{
|
||||
runner->input_buffer_capacity *= 2;
|
||||
runner->input_buffer = realloc(runner->input_buffer, runner->input_buffer_capacity);
|
||||
if (!runner->input_buffer)
|
||||
{
|
||||
perror("realloc input_buffer failed");
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unlink_roms(void)
|
||||
{
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
if (runners[i].rom_path[0])
|
||||
{
|
||||
if (unlink(runners[i].rom_path) == -1)
|
||||
perror("unlink rom_path failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void exit2(int _)
|
||||
{
|
||||
exit(2);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (argc < 3)
|
||||
{
|
||||
fprintf(stderr, "usage %s mgba-rom-test rom\n", argv[0]);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
bool tty = isatty(STDOUT_FILENO);
|
||||
if (!tty)
|
||||
{
|
||||
const char *v = getenv("MAKE_TERMOUT");
|
||||
tty = v && v[0] == '\0';
|
||||
}
|
||||
|
||||
if (tty)
|
||||
{
|
||||
char *stdout_buffer = malloc(64 * 1024);
|
||||
if (!stdout_buffer)
|
||||
{
|
||||
perror("malloc stdout_buffer failed");
|
||||
exit(2);
|
||||
}
|
||||
setvbuf(stdout, stdout_buffer, _IOFBF, 64 * 1024);
|
||||
}
|
||||
else
|
||||
{
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
}
|
||||
|
||||
int elffd;
|
||||
if ((elffd = open(argv[2], O_RDONLY)) == -1)
|
||||
{
|
||||
perror("open elffd failed");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
struct stat elfst;
|
||||
if (fstat(elffd, &elfst) == -1)
|
||||
{
|
||||
perror("stat elffd failed");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
void *elf;
|
||||
if ((elf = mmap(NULL, elfst.st_size, PROT_READ, MAP_PRIVATE, elffd, 0)) == MAP_FAILED)
|
||||
{
|
||||
perror("mmap elffd failed");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
nrunners = sysconf(_SC_NPROCESSORS_ONLN);
|
||||
if (nrunners > MAX_PROCESSES)
|
||||
nrunners = MAX_PROCESSES;
|
||||
runners = calloc(nrunners, sizeof(*runners));
|
||||
if (!runners)
|
||||
{
|
||||
perror("calloc runners failed");
|
||||
exit(2);
|
||||
}
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
runners[i].input_buffer_capacity = 4096;
|
||||
runners[i].input_buffer = malloc(runners[i].input_buffer_capacity);
|
||||
runners[i].output_buffer_capacity = 4096;
|
||||
runners[i].output_buffer = malloc(runners[i].output_buffer_capacity);
|
||||
strcpy(runners[i].test_name, "WAITING...");
|
||||
if (tty)
|
||||
fprintf(stdout, "%s\n", runners[i].test_name);
|
||||
}
|
||||
fflush(stdout);
|
||||
atexit(unlink_roms);
|
||||
signal(SIGINT, exit2);
|
||||
signal(SIGTERM, exit2);
|
||||
|
||||
// Start test runners.
|
||||
pid_t parent_pid = getpid();
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
int pipefds[2];
|
||||
if (pipe(pipefds) == -1)
|
||||
{
|
||||
perror("pipe failed");
|
||||
exit(2);
|
||||
}
|
||||
if (!tmpnam(runners[i].rom_path))
|
||||
{
|
||||
perror("tmpnam rom_path failed");
|
||||
exit(2);
|
||||
}
|
||||
int tmpfd;
|
||||
if ((tmpfd = open(runners[i].rom_path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)) == -1)
|
||||
{
|
||||
perror("open tmpfd failed");
|
||||
_exit(2);
|
||||
}
|
||||
if ((write(tmpfd, elf, elfst.st_size)) == -1)
|
||||
{
|
||||
perror("write tmpfd failed");
|
||||
_exit(2);
|
||||
}
|
||||
pid_t patchelfpid = fork();
|
||||
if (patchelfpid == -1)
|
||||
{
|
||||
perror("fork patchelf failed");
|
||||
_exit(2);
|
||||
}
|
||||
else if (patchelfpid == 0)
|
||||
{
|
||||
char n_arg[5], i_arg[5];
|
||||
snprintf(n_arg, sizeof(n_arg), "\\x%02x", nrunners);
|
||||
snprintf(i_arg, sizeof(i_arg), "\\x%02x", i);
|
||||
if (execlp("tools/patchelf/patchelf", "tools/patchelf/patchelf", runners[i].rom_path, "gTestRunnerN", n_arg, "gTestRunnerI", i_arg, NULL) == -1)
|
||||
{
|
||||
perror("execlp patchelf failed");
|
||||
_exit(2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int wstatus;
|
||||
if (waitpid(patchelfpid, &wstatus, 0) == -1)
|
||||
{
|
||||
perror("waitpid patchelfpid failed");
|
||||
_exit(2);
|
||||
}
|
||||
if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0)
|
||||
{
|
||||
fprintf(stderr, "patchelf exited with an error\n");
|
||||
_exit(2);
|
||||
}
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) {
|
||||
perror("fork mgba-rom-test failed");
|
||||
exit(2);
|
||||
} else if (pid == 0) {
|
||||
if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1)
|
||||
{
|
||||
perror("prctl failed");
|
||||
_exit(2);
|
||||
}
|
||||
if (getppid() != parent_pid) // Parent died.
|
||||
{
|
||||
_exit(2);
|
||||
}
|
||||
if (close(pipefds[0]) == -1)
|
||||
{
|
||||
perror("close pipefds[0] failed");
|
||||
_exit(2);
|
||||
}
|
||||
if (dup2(pipefds[1], STDOUT_FILENO) == -1)
|
||||
{
|
||||
perror("dup2 stdout failed");
|
||||
_exit(2);
|
||||
}
|
||||
if (close(pipefds[1]) == -1)
|
||||
{
|
||||
perror("close pipefds[1] failed");
|
||||
_exit(2);
|
||||
}
|
||||
// stdbuf is required because otherwise mgba never flushes
|
||||
// stdout.
|
||||
if (execlp("stdbuf", "stdbuf", "-oL", argv[1], "-l15", "-ClogLevel.gba.dma=16", "-Rr0", runners[i].rom_path, NULL) == -1)
|
||||
{
|
||||
perror("execl stdbuf mgba-rom-test failed");
|
||||
_exit(2);
|
||||
}
|
||||
} else {
|
||||
runners[i].pid = pid;
|
||||
runners[i].outfd = pipefds[0];
|
||||
if (close(pipefds[1]) == -1)
|
||||
{
|
||||
perror("close pipefds[1] failed");
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process test runner output.
|
||||
int openfds = nrunners;
|
||||
struct pollfd *pollfds = calloc(nrunners, sizeof(*pollfds));
|
||||
if (!pollfds)
|
||||
{
|
||||
perror("calloc pollfds failed");
|
||||
exit(2);
|
||||
}
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
pollfds[i].fd = runners[i].outfd;
|
||||
pollfds[i].events = POLLIN;
|
||||
}
|
||||
while (openfds > 0)
|
||||
{
|
||||
if (tty)
|
||||
{
|
||||
struct winsize winsize;
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) == -1)
|
||||
{
|
||||
perror("ioctl TIOCGWINSZ failed");
|
||||
exit(2);
|
||||
}
|
||||
int scrollback = 0;
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
if (runners[i].outfd >= 0)
|
||||
scrollback += (strlen(runners[i].test_name) + winsize.ws_col - 1) / winsize.ws_col;
|
||||
}
|
||||
if (scrollback > 0)
|
||||
fprintf(stdout, "\e[%dF\e[J", scrollback);
|
||||
}
|
||||
|
||||
if (poll(pollfds, nrunners, -1) == -1)
|
||||
{
|
||||
perror("poll failed");
|
||||
exit(2);
|
||||
}
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
if (pollfds[i].revents & POLLIN)
|
||||
{
|
||||
int n;
|
||||
if ((n = read(pollfds[i].fd, runners[i].input_buffer + runners[i].input_buffer_size, runners[i].input_buffer_capacity - runners[i].input_buffer_size)) == -1)
|
||||
{
|
||||
perror("read pollfds[i] failed");
|
||||
exit(2);
|
||||
}
|
||||
runners[i].input_buffer_size += n;
|
||||
handle_read(&runners[i]);
|
||||
}
|
||||
|
||||
if (pollfds[i].revents & (POLLERR | POLLHUP))
|
||||
{
|
||||
if (close(pollfds[i].fd) == -1)
|
||||
{
|
||||
perror("close pollfds[i] failed");
|
||||
exit(2);
|
||||
}
|
||||
runners[i].outfd = pollfds[i].fd = -pollfds[i].fd;
|
||||
openfds--;
|
||||
}
|
||||
}
|
||||
|
||||
if (tty)
|
||||
{
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
if (runners[i].outfd >= 0)
|
||||
fprintf(stdout, "%s\n", runners[i].test_name);
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
// Reap test runners and collate exit codes.
|
||||
int exit_code = 0;
|
||||
for (int i = 0; i < nrunners; i++)
|
||||
{
|
||||
int wstatus;
|
||||
if (waitpid(runners[i].pid, &wstatus, 0) == -1)
|
||||
{
|
||||
perror("waitpid runners[i] failed");
|
||||
exit(2);
|
||||
}
|
||||
if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) > exit_code)
|
||||
exit_code = WEXITSTATUS(wstatus);
|
||||
}
|
||||
return exit_code;
|
||||
}
|
7
tools/mgba/README.md
Normal file
7
tools/mgba/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# mGBA
|
||||
|
||||
The binaries in this folder are built from `mGBA`, an emulator for running Game Boy Advance games. The source code is available here: <https://github.com/mgba-emu/mgba>.
|
||||
The source code for these specific builds is available from:
|
||||
|
||||
- Windows: <https://github.com/mgba-emu/mgba/tree/7ee2be6c96222dca12a9a579b747fe5ff1829def>
|
||||
- Linux: <https://github.com/mgba-emu/mgba/tree/dbffb46c4e7d2e7a2cbed7c3488cece4c2176d4c>
|
BIN
tools/mgba/mgba-rom-test
Executable file
BIN
tools/mgba/mgba-rom-test
Executable file
Binary file not shown.
BIN
tools/mgba/mgba-rom-test.exe
Executable file
BIN
tools/mgba/mgba-rom-test.exe
Executable file
Binary file not shown.
1
tools/patchelf/.gitignore
vendored
Normal file
1
tools/patchelf/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
patchelf
|
18
tools/patchelf/Makefile
Normal file
18
tools/patchelf/Makefile
Normal file
@ -0,0 +1,18 @@
|
||||
.PHONY: all clean
|
||||
|
||||
SRCS = patchelf.c
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
EXE := .exe
|
||||
else
|
||||
EXE :=
|
||||
endif
|
||||
|
||||
all: patchelf$(EXE)
|
||||
@:
|
||||
|
||||
patchelf$(EXE): $(SRCS)
|
||||
$(CC) $(SRCS) -o $@ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
$(RM) patchelf$(EXE)
|
191
tools/patchelf/patchelf.c
Normal file
191
tools/patchelf/patchelf.c
Normal file
@ -0,0 +1,191 @@
|
||||
#include <ctype.h>
|
||||
#include <elf.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static bool try_patch_value(const char *sym, char *dest, const char *source, size_t size);
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int exit_code = 1;
|
||||
|
||||
int fd = -1;
|
||||
char *f = MAP_FAILED;
|
||||
|
||||
if (argc < 2 || argc % 2 != 0)
|
||||
{
|
||||
fprintf(stderr, "Usage: %s <filename> [<symbol> <value>]...\n", argv[0]);
|
||||
goto error;
|
||||
}
|
||||
|
||||
if ((fd = open(argv[1], O_RDWR)) == -1)
|
||||
{
|
||||
fprintf(stderr, "open(%s, O_RDWR) failed: %s\n", argv[1], strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) == -1)
|
||||
{
|
||||
perror("stat failed");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if ((f = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
|
||||
{
|
||||
perror("mmap failed");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (memcmp(f, ELFMAG, 4) != 0)
|
||||
{
|
||||
fprintf(stderr, "not an ELF file\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
const Elf32_Ehdr *ehdr = (Elf32_Ehdr *)f;
|
||||
const Elf32_Shdr *shdrs = (Elf32_Shdr *)(f + ehdr->e_shoff);
|
||||
|
||||
if (ehdr->e_shstrndx == SHN_UNDEF)
|
||||
{
|
||||
fprintf(stderr, "no section name string table\n");
|
||||
goto error;
|
||||
}
|
||||
const Elf32_Shdr *shdr_shstr = &shdrs[ehdr->e_shstrndx];
|
||||
const char *shstr = (const char *)(f + shdr_shstr->sh_offset);
|
||||
const Elf32_Shdr *shdr_symtab = NULL;
|
||||
const Elf32_Shdr *shdr_strtab = NULL;
|
||||
for (int i = 0; i < ehdr->e_shnum; i++)
|
||||
{
|
||||
const char *sh_name = shstr + shdrs[i].sh_name;
|
||||
if (strcmp(sh_name, ".symtab") == 0)
|
||||
shdr_symtab = &shdrs[i];
|
||||
else if (strcmp(sh_name, ".strtab") == 0)
|
||||
shdr_strtab = &shdrs[i];
|
||||
}
|
||||
if (!shdr_symtab)
|
||||
{
|
||||
fprintf(stderr, "no .symtab section\n");
|
||||
goto error;
|
||||
}
|
||||
if (!shdr_strtab)
|
||||
{
|
||||
fprintf(stderr, "no .strtab section\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
const Elf32_Sym *symtab = (Elf32_Sym *)(f + shdr_symtab->sh_offset);
|
||||
const char *strtab = (const char *)(f + shdr_strtab->sh_offset);
|
||||
for (int i = 0; i < shdr_symtab->sh_size / shdr_symtab->sh_entsize; i++)
|
||||
{
|
||||
if (symtab[i].st_name == 0) continue;
|
||||
if (symtab[i].st_shndx > ehdr->e_shnum) continue;
|
||||
const char *st_name = strtab + symtab[i].st_name;
|
||||
const Elf32_Shdr *shdr = &shdrs[symtab[i].st_shndx];
|
||||
uint32_t sym_offset = symtab[i].st_value - shdr->sh_addr;
|
||||
for (int j = 2; j < argc; j += 2)
|
||||
{
|
||||
const char *arg_name = argv[j + 0];
|
||||
const char *arg_value = argv[j + 1];
|
||||
if (strcmp(st_name, arg_name) == 0)
|
||||
{
|
||||
char *value = (char *)(f + shdr->sh_offset + sym_offset);
|
||||
if (!try_patch_value(st_name, value, arg_value, symtab[i].st_size))
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit_code = 0;
|
||||
|
||||
error:
|
||||
if (f != MAP_FAILED)
|
||||
{
|
||||
if (msync(f, st.st_size, MS_SYNC) == -1)
|
||||
{
|
||||
perror("msync failed");
|
||||
f = MAP_FAILED;
|
||||
}
|
||||
}
|
||||
if (f != MAP_FAILED)
|
||||
{
|
||||
if (munmap(f, st.st_size) == -1)
|
||||
{
|
||||
perror("munmap failed");
|
||||
}
|
||||
}
|
||||
if (fd != -1)
|
||||
{
|
||||
if (close(fd) == -1)
|
||||
{
|
||||
perror("close failed");
|
||||
}
|
||||
}
|
||||
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
static int parsexdigit(char c)
|
||||
{
|
||||
if ('0' <= c && c <= '9')
|
||||
return c - '0';
|
||||
else if ('a' <= c && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
else if ('A' <= c && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool try_patch_value(const char *sym, char *dest, const char *source, size_t size)
|
||||
{
|
||||
int i = 0;
|
||||
while (*source)
|
||||
{
|
||||
if (i == size)
|
||||
{
|
||||
fprintf(stderr, "%s: overflows size (%lu)\n", sym, size);
|
||||
return false;
|
||||
}
|
||||
char c, value;
|
||||
switch ((c = *source++))
|
||||
{
|
||||
case '\\':
|
||||
switch ((c = *source++))
|
||||
{
|
||||
case '0':
|
||||
value = 0;
|
||||
break;
|
||||
case 'x':
|
||||
if (!isxdigit((c = *source++)))
|
||||
{
|
||||
fprintf(stderr, "%s: illegal escape \\x%c\n", sym, c);
|
||||
return false;
|
||||
}
|
||||
value = parsexdigit(c);
|
||||
if (!isxdigit((c = *source++)))
|
||||
{
|
||||
fprintf(stderr, "%s: illegal escape \\x%c%c\n", sym, *(source - 2), c);
|
||||
return false;
|
||||
}
|
||||
value = value * 16 + parsexdigit(c);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "%s: illegal escape \\%c\n", sym, c);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
value = c;
|
||||
break;
|
||||
}
|
||||
dest[i++] = value;
|
||||
}
|
||||
return true;
|
||||
}
|
Loading…
Reference in New Issue
Block a user