diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 942f1f1f5..5393aa155 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/Makefile b/Makefile index 2644a9acd..696820cd9 100644 --- a/Makefile +++ b/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) -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 ../../$@ " + @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) diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc index 940f30160..4d0b9a614 100644 --- a/asm/macros/battle_script.inc +++ b/asm/macros/battle_script.inc @@ -2237,3 +2237,8 @@ .endif waitmessage B_WAIT_TIME_LONG .endm + + .macro jumpifemergencyexited battler:req, ptr:req + various \battler, VARIOUS_JUMP_IF_EMERGENCY_EXITED + .4byte \ptr + .endm diff --git a/common_syms/main.txt b/common_syms/main.txt index a620083d1..f1f8076ad 100644 --- a/common_syms/main.txt +++ b/common_syms/main.txt @@ -7,3 +7,4 @@ gIntrTable gLinkVSyncDisabled IntrMain_Buffer gPcmDmaCounter +gAgbMainLoop_sp diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 6d95a8d0b..f79576710 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -3055,6 +3055,7 @@ BattleScript_EffectHitEscape: jumpifbyte CMP_NOT_EQUAL gBattleOutcome 0, BattleScript_HitEscapeEnd jumpifbattletype BATTLE_TYPE_ARENA, BattleScript_HitEscapeEnd jumpifcantswitch SWITCH_IGNORE_ESCAPE_PREVENTION | BS_ATTACKER, BattleScript_HitEscapeEnd + jumpifemergencyexited BS_TARGET, BattleScript_HitEscapeEnd openpartyscreen BS_ATTACKER, BattleScript_HitEscapeEnd switchoutabilities BS_ATTACKER waitstate diff --git a/data/specials.inc b/data/specials.inc index cb49a935d..9c336b376 100644 --- a/data/specials.inc +++ b/data/specials.inc @@ -258,7 +258,7 @@ gSpecials:: def_special CallSlateportTentFunction def_special ChoosePartyForBattleFrontier def_special ValidateEReaderTrainer - def_special GetBestBattleTowerStreak + def_special GetBattleTowerSinglesStreak def_special ReducePlayerPartyToSelectedMons def_special BedroomPC def_special PlayerPC diff --git a/include/battle.h b/include/battle.h index 517b95eda..36d751b97 100644 --- a/include/battle.h +++ b/include/battle.h @@ -196,6 +196,7 @@ struct SpecialStatus // End of byte u8 weatherAbilityDone:1; u8 terrainAbilityDone:1; + u8 emergencyExited:1; }; struct SideTimer @@ -483,7 +484,6 @@ struct MegaEvolutionData u8 battlerId; bool8 playerSelect; u8 triggerSpriteId; - bool8 isWishMegaEvo; }; struct Illusion diff --git a/include/battle_anim.h b/include/battle_anim.h index 883934f14..c4b29d26e 100644 --- a/include/battle_anim.h +++ b/include/battle_anim.h @@ -5,6 +5,14 @@ #include "constants/battle_anim.h" #include "task.h" +enum +{ + ANIM_TYPE_GENERAL, + ANIM_TYPE_MOVE, + ANIM_TYPE_STATUS, + ANIM_TYPE_SPECIAL, +}; + enum { BG_ANIM_SCREEN_SIZE, @@ -54,7 +62,7 @@ extern u16 gAnimMoveIndex; void ClearBattleAnimationVars(void); void DoMoveAnim(u16 move); -void LaunchBattleAnimation(const u8 *const animsTable[], u16 tableId, bool8 isMoveAnim); +void LaunchBattleAnimation(u32 animType, u32 animId); void DestroyAnimSprite(struct Sprite *sprite); void DestroyAnimVisualTask(u8 taskId); void DestroyAnimSoundTask(u8 taskId); diff --git a/include/battle_gfx_sfx_util.h b/include/battle_gfx_sfx_util.h index 3e940995d..491d5fefd 100644 --- a/include/battle_gfx_sfx_util.h +++ b/include/battle_gfx_sfx_util.h @@ -23,7 +23,7 @@ bool8 BattleInitAllSprites(u8 *state1, u8 *battlerId); void ClearSpritesHealthboxAnimData(void); void CopyAllBattleSpritesInvisibilities(void); void CopyBattleSpriteInvisibility(u8 battlerId); -void HandleSpeciesGfxDataChange(u8 attacker, u8 target, bool8 notTransform, bool32 megaEvo); +void HandleSpeciesGfxDataChange(u8 attacker, u8 target, bool8 notTransform, bool32 megaEvo, bool8 trackEnemyPersonality); void BattleLoadSubstituteOrMonSpriteGfx(u8 battlerId, bool8 loadMonSprite); void LoadBattleMonGfxAndAnimate(u8 battlerId, bool8 loadMonSprite, u8 spriteId); void TrySetBehindSubstituteSpriteBit(u8 battlerId, u16 move); diff --git a/include/constants/battle_script_commands.h b/include/constants/battle_script_commands.h index fdf703c1a..123fb5d49 100644 --- a/include/constants/battle_script_commands.h +++ b/include/constants/battle_script_commands.h @@ -255,6 +255,7 @@ #define VARIOUS_ACTIVATE_WEATHER_CHANGE_ABILITIES 164 #define VARIOUS_ACTIVATE_TERRAIN_CHANGE_ABILITIES 165 #define VARIOUS_JUMP_IF_NO_VALID_TARGETS 166 +#define VARIOUS_JUMP_IF_EMERGENCY_EXITED 167 // Cmd_manipulatedamage #define DMG_CHANGE_SIGN 0 diff --git a/include/constants/daycare.h b/include/constants/daycare.h index ac48e4f71..782bbe954 100644 --- a/include/constants/daycare.h +++ b/include/constants/daycare.h @@ -13,7 +13,6 @@ #define DAYCARE_ONE_MON 2 #define DAYCARE_TWO_MONS 3 -#define INHERITED_IV_COUNT 3 #if P_EGG_HATCH_LEVEL >= GEN_4 #define EGG_HATCH_LEVEL 1 #else diff --git a/include/constants/game_stat.h b/include/constants/game_stat.h index 2acdfd8a3..053e4a8f9 100644 --- a/include/constants/game_stat.h +++ b/include/constants/game_stat.h @@ -1,60 +1,60 @@ #ifndef GUARD_CONSTANTS_GAME_STAT_H #define GUARD_CONSTANTS_GAME_STAT_H -#define GAME_STAT_SAVED_GAME 0 -#define GAME_STAT_FIRST_HOF_PLAY_TIME 1 -#define GAME_STAT_STARTED_TRENDS 2 -#define GAME_STAT_PLANTED_BERRIES 3 -#define GAME_STAT_TRADED_BIKES 4 -#define GAME_STAT_STEPS 5 -#define GAME_STAT_GOT_INTERVIEWED 6 -#define GAME_STAT_TOTAL_BATTLES 7 -#define GAME_STAT_WILD_BATTLES 8 -#define GAME_STAT_TRAINER_BATTLES 9 -#define GAME_STAT_ENTERED_HOF 10 -#define GAME_STAT_POKEMON_CAPTURES 11 -#define GAME_STAT_FISHING_CAPTURES 12 -#define GAME_STAT_HATCHED_EGGS 13 -#define GAME_STAT_EVOLVED_POKEMON 14 -#define GAME_STAT_USED_POKECENTER 15 -#define GAME_STAT_RESTED_AT_HOME 16 -#define GAME_STAT_ENTERED_SAFARI_ZONE 17 -#define GAME_STAT_USED_CUT 18 -#define GAME_STAT_USED_ROCK_SMASH 19 -#define GAME_STAT_MOVED_SECRET_BASE 20 -#define GAME_STAT_POKEMON_TRADES 21 -#define GAME_STAT_UNKNOWN_22 22 -#define GAME_STAT_LINK_BATTLE_WINS 23 -#define GAME_STAT_LINK_BATTLE_LOSSES 24 -#define GAME_STAT_LINK_BATTLE_DRAWS 25 -#define GAME_STAT_USED_SPLASH 26 -#define GAME_STAT_USED_STRUGGLE 27 -#define GAME_STAT_SLOT_JACKPOTS 28 -#define GAME_STAT_CONSECUTIVE_ROULETTE_WINS 29 -#define GAME_STAT_ENTERED_BATTLE_TOWER 30 -#define GAME_STAT_UNKNOWN_31 31 -#define GAME_STAT_BATTLE_TOWER_BEST_STREAK 32 -#define GAME_STAT_POKEBLOCKS 33 -#define GAME_STAT_POKEBLOCKS_WITH_FRIENDS 34 -#define GAME_STAT_WON_LINK_CONTEST 35 -#define GAME_STAT_ENTERED_CONTEST 36 -#define GAME_STAT_WON_CONTEST 37 -#define GAME_STAT_SHOPPED 38 -#define GAME_STAT_USED_ITEMFINDER 39 -#define GAME_STAT_GOT_RAINED_ON 40 -#define GAME_STAT_CHECKED_POKEDEX 41 -#define GAME_STAT_RECEIVED_RIBBONS 42 -#define GAME_STAT_JUMPED_DOWN_LEDGES 43 -#define GAME_STAT_WATCHED_TV 44 -#define GAME_STAT_CHECKED_CLOCK 45 -#define GAME_STAT_WON_POKEMON_LOTTERY 46 -#define GAME_STAT_USED_DAYCARE 47 -#define GAME_STAT_RODE_CABLE_CAR 48 -#define GAME_STAT_ENTERED_HOT_SPRINGS 49 -#define GAME_STAT_NUM_UNION_ROOM_BATTLES 50 -#define GAME_STAT_PLAYED_BERRY_CRUSH 51 +#define GAME_STAT_SAVED_GAME 0 +#define GAME_STAT_FIRST_HOF_PLAY_TIME 1 +#define GAME_STAT_STARTED_TRENDS 2 +#define GAME_STAT_PLANTED_BERRIES 3 +#define GAME_STAT_TRADED_BIKES 4 +#define GAME_STAT_STEPS 5 +#define GAME_STAT_GOT_INTERVIEWED 6 +#define GAME_STAT_TOTAL_BATTLES 7 +#define GAME_STAT_WILD_BATTLES 8 +#define GAME_STAT_TRAINER_BATTLES 9 +#define GAME_STAT_ENTERED_HOF 10 +#define GAME_STAT_POKEMON_CAPTURES 11 +#define GAME_STAT_FISHING_CAPTURES 12 +#define GAME_STAT_HATCHED_EGGS 13 +#define GAME_STAT_EVOLVED_POKEMON 14 +#define GAME_STAT_USED_POKECENTER 15 +#define GAME_STAT_RESTED_AT_HOME 16 +#define GAME_STAT_ENTERED_SAFARI_ZONE 17 +#define GAME_STAT_USED_CUT 18 +#define GAME_STAT_USED_ROCK_SMASH 19 +#define GAME_STAT_MOVED_SECRET_BASE 20 +#define GAME_STAT_POKEMON_TRADES 21 +#define GAME_STAT_UNKNOWN_22 22 +#define GAME_STAT_LINK_BATTLE_WINS 23 +#define GAME_STAT_LINK_BATTLE_LOSSES 24 +#define GAME_STAT_LINK_BATTLE_DRAWS 25 +#define GAME_STAT_USED_SPLASH 26 +#define GAME_STAT_USED_STRUGGLE 27 +#define GAME_STAT_SLOT_JACKPOTS 28 +#define GAME_STAT_CONSECUTIVE_ROULETTE_WINS 29 +#define GAME_STAT_ENTERED_BATTLE_TOWER 30 +#define GAME_STAT_UNKNOWN_31 31 +#define GAME_STAT_BATTLE_TOWER_SINGLES_STREAK 32 +#define GAME_STAT_POKEBLOCKS 33 +#define GAME_STAT_POKEBLOCKS_WITH_FRIENDS 34 +#define GAME_STAT_WON_LINK_CONTEST 35 +#define GAME_STAT_ENTERED_CONTEST 36 +#define GAME_STAT_WON_CONTEST 37 +#define GAME_STAT_SHOPPED 38 +#define GAME_STAT_USED_ITEMFINDER 39 +#define GAME_STAT_GOT_RAINED_ON 40 +#define GAME_STAT_CHECKED_POKEDEX 41 +#define GAME_STAT_RECEIVED_RIBBONS 42 +#define GAME_STAT_JUMPED_DOWN_LEDGES 43 +#define GAME_STAT_WATCHED_TV 44 +#define GAME_STAT_CHECKED_CLOCK 45 +#define GAME_STAT_WON_POKEMON_LOTTERY 46 +#define GAME_STAT_USED_DAYCARE 47 +#define GAME_STAT_RODE_CABLE_CAR 48 +#define GAME_STAT_ENTERED_HOT_SPRINGS 49 +#define GAME_STAT_NUM_UNION_ROOM_BATTLES 50 +#define GAME_STAT_PLAYED_BERRY_CRUSH 51 -#define NUM_USED_GAME_STATS 52 -#define NUM_GAME_STATS 64 +#define NUM_USED_GAME_STATS 52 +#define NUM_GAME_STATS 64 #endif // GUARD_CONSTANTS_GAME_STAT_H diff --git a/include/constants/items.h b/include/constants/items.h index 9f931686e..1cb2803f3 100644 --- a/include/constants/items.h +++ b/include/constants/items.h @@ -928,7 +928,13 @@ #define ITEM_RUBY 756 #define ITEM_SAPPHIRE 757 -#define ITEMS_COUNT 758 +#define ITEM_ABILITY_SHIELD 758 +#define ITEM_CLEAR_AMULET 759 +#define ITEM_PUNCHING_GLOVE 760 +#define ITEM_COVERT_CLOAK 761 +#define ITEM_LOADED_DICE 762 + +#define ITEMS_COUNT 763 #define ITEM_FIELD_ARROW ITEMS_COUNT // A special item id associated with "Cancel"/"Exit" etc. in a list of items or decorations diff --git a/include/global.h b/include/global.h index 8e307e867..7324335d2 100644 --- a/include/global.h +++ b/include/global.h @@ -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]; diff --git a/include/main.h b/include/main.h index 5ccb20df8..eba04cbaa 100644 --- a/include/main.h +++ b/include/main.h @@ -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); diff --git a/include/pokemon.h b/include/pokemon.h index 007419bd3..b7c9d6ad0 100644 --- a/include/pokemon.h +++ b/include/pokemon.h @@ -566,5 +566,6 @@ bool32 ShouldShowFemaleDifferences(u16 species, u32 personality); bool32 TryFormChange(u32 monId, u32 side, u16 method); void TryToSetBattleFormChangeMoves(struct Pokemon *mon, u16 method); u32 GetMonFriendshipScore(struct Pokemon *pokemon); +void UpdateMonPersonality(struct BoxPokemon *boxMon, u32 personality); #endif // GUARD_POKEMON_H diff --git a/include/recorded_battle.h b/include/recorded_battle.h index 9b8939403..fbe14a656 100644 --- a/include/recorded_battle.h +++ b/include/recorded_battle.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); diff --git a/include/test_runner.h b/include/test_runner.h new file mode 100644 index 000000000..2fc0a55e3 --- /dev/null +++ b/include/test_runner.h @@ -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 diff --git a/ld_script.txt b/ld_script.txt index 629649adf..4c3085f59 100644 --- a/ld_script.txt +++ b/ld_script.txt @@ -2,6 +2,7 @@ ENTRY(Start) gNumMusicPlayers = 4; gMaxLines = 0; +gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup; SECTIONS { . = 0x2000000; diff --git a/ld_script_modern.txt b/ld_script_modern.txt index dd4eeed6a..082d69429 100644 --- a/ld_script_modern.txt +++ b/ld_script_modern.txt @@ -2,6 +2,7 @@ ENTRY(Start) gNumMusicPlayers = 4; gMaxLines = 0; +gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup; SECTIONS { . = 0x2000000; diff --git a/ld_script_test.txt b/ld_script_test.txt new file mode 100644 index 000000000..3fcac51d4 --- /dev/null +++ b/ld_script_test.txt @@ -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/ : + { + *(*); + } +} diff --git a/make_tools.mk b/make_tools.mk index 697897a69..36dbe8c90 100644 --- a/make_tools.mk +++ b/make_tools.mk @@ -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) diff --git a/src/battle_anim.c b/src/battle_anim.c index fe95c5bb5..610c24321 100644 --- a/src/battle_anim.c +++ b/src/battle_anim.c @@ -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" @@ -27,7 +28,10 @@ #define ANIM_SPRITE_INDEX_COUNT 8 extern const u16 gMovesWithQuietBGM[]; +extern const u8 *const gBattleAnims_General[]; extern const u8 *const gBattleAnims_Moves[]; +extern const u8 *const gBattleAnims_Special[]; +extern const u8 *const gBattleAnims_StatusConditions[]; static void Cmd_loadspritegfx(void); static void Cmd_unloadspritegfx(void); @@ -211,17 +215,50 @@ void DoMoveAnim(u16 move) gBattleAnimTarget = 0; } } - LaunchBattleAnimation(gBattleAnims_Moves, move, TRUE); + LaunchBattleAnimation(ANIM_TYPE_MOVE, move); } -void LaunchBattleAnimation(const u8 *const animsTable[], u16 tableId, bool8 isMoveAnim) +static void Nop(void) +{ +} + +void LaunchBattleAnimation(u32 animType, u32 animId) { s32 i; - bool32 hideHpBoxes = (tableId == MOVE_TRANSFORM) ? FALSE : TRUE; + const u8 *const *animsTable; + bool32 hideHpBoxes; - if (!isMoveAnim) + if (gTestRunnerEnabled) { - switch (tableId) + TestRunner_Battle_RecordAnimation(animType, animId); + if (gTestRunnerHeadless) + { + gAnimScriptCallback = Nop; + gAnimScriptActive = FALSE; + return; + } + } + + switch (animType) + { + case ANIM_TYPE_GENERAL: + animsTable = gBattleAnims_General; + break; + case ANIM_TYPE_MOVE: + animsTable = gBattleAnims_Moves; + break; + case ANIM_TYPE_STATUS: + animsTable = gBattleAnims_StatusConditions; + break; + case ANIM_TYPE_SPECIAL: + animsTable = gBattleAnims_Special; + break; + } + + hideHpBoxes = !(animType == ANIM_TYPE_MOVE && animId == MOVE_TRANSFORM); + if (animType != ANIM_TYPE_MOVE) + { + switch (animId) { case B_ANIM_TURN_TRAP: case B_ANIM_LEECH_SEED_DRAIN: @@ -258,17 +295,17 @@ void LaunchBattleAnimation(const u8 *const animsTable[], u16 tableId, bool8 isMo gAnimBattlerSpecies[i] = gContestResources->moveAnim->species; } - if (!isMoveAnim) + if (animType != ANIM_TYPE_MOVE) gAnimMoveIndex = 0; else - gAnimMoveIndex = tableId; + gAnimMoveIndex = animId; for (i = 0; i < ANIM_ARGS_COUNT; i++) gBattleAnimArgs[i] = 0; sMonAnimTaskIdArray[0] = TASK_NONE; sMonAnimTaskIdArray[1] = TASK_NONE; - sBattleAnimScriptPtr = animsTable[tableId]; + sBattleAnimScriptPtr = animsTable[animId]; gAnimScriptActive = TRUE; sAnimFramesToWait = 0; gAnimScriptCallback = RunAnimScriptCommand; @@ -276,11 +313,11 @@ void LaunchBattleAnimation(const u8 *const animsTable[], u16 tableId, bool8 isMo for (i = 0; i < ANIM_SPRITE_INDEX_COUNT; i++) sAnimSpriteIndexArray[i] = 0xFFFF; - if (isMoveAnim) + if (animType == ANIM_TYPE_MOVE) { for (i = 0; gMovesWithQuietBGM[i] != 0xFFFF; i++) { - if (tableId == gMovesWithQuietBGM[i]) + if (animId == gMovesWithQuietBGM[i]) { m4aMPlayVolumeControl(&gMPlayInfo_BGM, TRACKS_ALL, 128); break; diff --git a/src/battle_anim_effects_3.c b/src/battle_anim_effects_3.c index c7dca55f1..ff5f04446 100755 --- a/src/battle_anim_effects_3.c +++ b/src/battle_anim_effects_3.c @@ -2364,7 +2364,7 @@ void AnimTask_TransformMon(u8 taskId) } break; case 2: - HandleSpeciesGfxDataChange(gBattleAnimAttacker, gBattleAnimTarget, gTasks[taskId].data[10], gBattleAnimArgs[1]); + HandleSpeciesGfxDataChange(gBattleAnimAttacker, gBattleAnimTarget, gTasks[taskId].data[10], gBattleAnimArgs[1], TRUE); GetBgDataForTransform(&animBg, gBattleAnimAttacker); if (IsContest()) @@ -2451,7 +2451,7 @@ void AnimTask_IsMonInvisible(u8 taskId) void AnimTask_CastformGfxDataChange(u8 taskId) { - HandleSpeciesGfxDataChange(gBattleAnimAttacker, gBattleAnimTarget, TRUE, FALSE); + HandleSpeciesGfxDataChange(gBattleAnimAttacker, gBattleAnimTarget, TRUE, FALSE, FALSE); DestroyAnimVisualTask(taskId); } diff --git a/src/battle_anim_status_effects.c b/src/battle_anim_status_effects.c index 6b194cbcd..0111b4d6d 100644 --- a/src/battle_anim_status_effects.c +++ b/src/battle_anim_status_effects.c @@ -13,7 +13,6 @@ extern const struct CompressedSpriteSheet gBattleAnimPicTable[]; extern const struct CompressedSpritePalette gBattleAnimPaletteTable[]; -extern const u8 *const gBattleAnims_StatusConditions[]; extern const struct OamData gOamData_AffineOff_ObjNormal_8x8; extern const struct OamData gOamData_AffineOff_ObjBlend_64x64; @@ -568,7 +567,7 @@ void LaunchStatusAnimation(u8 battlerId, u8 statusAnimId) gBattleAnimAttacker = battlerId; gBattleAnimTarget = battlerId; - LaunchBattleAnimation(gBattleAnims_StatusConditions, statusAnimId, FALSE); + LaunchBattleAnimation(ANIM_TYPE_STATUS, statusAnimId); taskId = CreateTask(Task_DoStatusAnimation, 10); gTasks[taskId].data[0] = battlerId; } diff --git a/src/battle_controller_recorded_opponent.c b/src/battle_controller_recorded_opponent.c index f1243ee2b..2e93d625d 100644 --- a/src/battle_controller_recorded_opponent.c +++ b/src/battle_controller_recorded_opponent.c @@ -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)); } } diff --git a/src/battle_controller_recorded_player.c b/src/battle_controller_recorded_player.c index 82d0a4334..1906df8a8 100644 --- a/src/battle_controller_recorded_player.c +++ b/src/battle_controller_recorded_player.c @@ -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)); } } diff --git a/src/battle_debug.c b/src/battle_debug.c index 1fd2d9b00..027be03a4 100644 --- a/src/battle_debug.c +++ b/src/battle_debug.c @@ -1107,7 +1107,7 @@ static void Task_DebugMenuProcessInput(u8 taskId) struct BattleDebugMenu *data = GetStructPtr(taskId); // Exit the menu. - if (JOY_NEW(SELECT_BUTTON)) + if (JOY_NEW(SELECT_BUTTON) || ((JOY_NEW(B_BUTTON)) && data->activeWindow == ACTIVE_WIN_MAIN)) { BeginNormalPaletteFade(-1, 0, 0, 0x10, 0); gTasks[taskId].func = Task_DebugMenuFadeOut; diff --git a/src/battle_dome.c b/src/battle_dome.c index d9acbef2b..a5f73316e 100644 --- a/src/battle_dome.c +++ b/src/battle_dome.c @@ -4237,7 +4237,7 @@ static u8 Task_GetInfoCardInput(u8 taskId) #undef tUsingAlternateSlot // allocatedArray below needs to be large enough to hold stat totals for each mon, or totals of each type of move points -#define ALLOC_ARRAY_SIZE (NUM_STATS * FRONTIER_PARTY_SIZE >= NUM_MOVE_POINT_TYPES ? (NUM_STATS * FRONTIER_PARTY_SIZE) : NUM_MOVE_POINT_TYPES) +#define ALLOC_ARRAY_SIZE max(NUM_STATS * FRONTIER_PARTY_SIZE, NUM_MOVE_POINT_TYPES) static void DisplayTrainerInfoOnCard(u8 flags, u8 trainerTourneyId) { diff --git a/src/battle_gfx_sfx_util.c b/src/battle_gfx_sfx_util.c index afc297745..193452613 100644 --- a/src/battle_gfx_sfx_util.c +++ b/src/battle_gfx_sfx_util.c @@ -27,8 +27,6 @@ #include "constants/battle_palace.h" extern const u8 gBattlePalaceNatureToMoveTarget[]; -extern const u8 *const gBattleAnims_General[]; -extern const u8 *const gBattleAnims_Special[]; extern const struct CompressedSpriteSheet gSpriteSheet_EnemyShadow; extern const struct SpriteTemplate gSpriteTemplate_EnemyShadow; @@ -465,7 +463,7 @@ bool8 TryHandleLaunchBattleTableAnimation(u8 activeBattler, u8 atkBattler, u8 de gBattleAnimAttacker = atkBattler; gBattleAnimTarget = defBattler; gBattleSpritesDataPtr->animationData->animArg = argument; - LaunchBattleAnimation(gBattleAnims_General, tableId, FALSE); + LaunchBattleAnimation(ANIM_TYPE_GENERAL, tableId); taskId = CreateTask(Task_ClearBitWhenBattleTableAnimDone, 10); gTasks[taskId].tBattlerId = activeBattler; gBattleSpritesDataPtr->healthBoxesData[gTasks[taskId].tBattlerId].animFromTableActive = 1; @@ -509,7 +507,7 @@ void InitAndLaunchSpecialAnimation(u8 activeBattler, u8 atkBattler, u8 defBattle gBattleAnimAttacker = atkBattler; gBattleAnimTarget = defBattler; - LaunchBattleAnimation(gBattleAnims_Special, tableId, FALSE); + LaunchBattleAnimation(ANIM_TYPE_SPECIAL, tableId); taskId = CreateTask(Task_ClearBitWhenSpecialAnimDone, 10); gTasks[taskId].tBattlerId = activeBattler; gBattleSpritesDataPtr->healthBoxesData[gTasks[taskId].tBattlerId].specialAnimActive = 1; @@ -849,7 +847,7 @@ void CopyBattleSpriteInvisibility(u8 battlerId) gBattleSpritesDataPtr->battlerData[battlerId].invisible = gSprites[gBattlerSpriteIds[battlerId]].invisible; } -void HandleSpeciesGfxDataChange(u8 battlerAtk, u8 battlerDef, bool8 castform, bool32 megaEvo) +void HandleSpeciesGfxDataChange(u8 battlerAtk, u8 battlerDef, bool8 castform, bool32 megaEvo, bool8 trackEnemyPersonality) { u32 personalityValue, otId, position, paletteOffset, targetSpecies; const void *lzPaletteData, *src; @@ -878,7 +876,10 @@ void HandleSpeciesGfxDataChange(u8 battlerAtk, u8 battlerDef, bool8 castform, bo if (GetBattlerSide(battlerAtk) == B_SIDE_PLAYER) { - personalityValue = GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_PERSONALITY); + if (trackEnemyPersonality) + personalityValue = GetMonData(&gEnemyParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_PERSONALITY); + else + personalityValue = GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_PERSONALITY); otId = GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_OT_ID); HandleLoadSpecialPokePic(FALSE, @@ -888,7 +889,10 @@ void HandleSpeciesGfxDataChange(u8 battlerAtk, u8 battlerDef, bool8 castform, bo } else { - personalityValue = GetMonData(&gEnemyParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_PERSONALITY); + if (trackEnemyPersonality) + personalityValue = GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_PERSONALITY); + else + personalityValue = GetMonData(&gEnemyParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_PERSONALITY); otId = GetMonData(&gEnemyParty[gBattlerPartyIndexes[battlerAtk]], MON_DATA_OT_ID); HandleLoadSpecialPokePic(TRUE, diff --git a/src/battle_interface.c b/src/battle_interface.c index 099aafe1b..2dc16cea1 100644 --- a/src/battle_interface.c +++ b/src/battle_interface.c @@ -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" @@ -3069,6 +3070,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; @@ -3184,9 +3192,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) diff --git a/src/battle_main.c b/src/battle_main.c index 18a314331..6bf680772 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -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(); @@ -1793,6 +1795,8 @@ void CB2_QuitRecordedBattle(void) { m4aMPlayStop(&gMPlayInfo_SE1); m4aMPlayStop(&gMPlayInfo_SE2); + if (gTestRunnerEnabled) + TestRunner_Battle_AfterLastTurn(); FreeRestoreBattleData(); FreeAllWindowBuffers(); SetMainCallback2(gMain.savedCallback); @@ -4859,9 +4863,14 @@ static void CheckMegaEvolutionBeforeTurn(void) if (gBattleStruct->mega.toEvolve & gBitTable[gActiveBattler] && !(gProtectStructs[gActiveBattler].noValidMoves)) { + struct Pokemon *mon; + if (GetBattlerSide(gActiveBattler) == B_SIDE_OPPONENT) + mon = &gEnemyParty[gBattlerPartyIndexes[gActiveBattler]]; + else + mon = &gPlayerParty[gBattlerPartyIndexes[gActiveBattler]]; gBattleStruct->mega.toEvolve &= ~(gBitTable[gActiveBattler]); gLastUsedItem = gBattleMons[gActiveBattler].item; - if (gBattleStruct->mega.isWishMegaEvo == TRUE) + if (GetBattleFormChangeTargetSpecies(gActiveBattler, FORM_CHANGE_BATTLE_MEGA_EVOLUTION_MOVE) != SPECIES_NONE) BattleScriptExecute(BattleScript_WishMegaEvolution); else BattleScriptExecute(BattleScript_MegaEvolution); @@ -5204,6 +5213,8 @@ static void HandleEndTurn_FinishBattle(void) } RecordedBattle_SetPlaybackFinished(); + if (gTestRunnerEnabled) + TestRunner_Battle_AfterLastTurn(); BeginFastPaletteFade(3); FadeOutMapMusic(5); #if B_TRAINERS_KNOCK_OFF_ITEMS == TRUE diff --git a/src/battle_message.c b/src/battle_message.c index c1ebe8d79..34542e58d 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -209,8 +209,8 @@ static const u8 sText_PkmnFellIntoNightmare[] = _("{B_DEF_NAME_WITH_PREFIX} fell static const u8 sText_PkmnLockedInNightmare[] = _("{B_ATK_NAME_WITH_PREFIX} is locked\nin a NIGHTMARE!"); static const u8 sText_PkmnLaidCurse[] = _("{B_ATK_NAME_WITH_PREFIX} cut its own HP and\nlaid a CURSE on {B_DEF_NAME_WITH_PREFIX}!"); static const u8 sText_PkmnAfflictedByCurse[] = _("{B_ATK_NAME_WITH_PREFIX} is afflicted\nby the CURSE!"); -static const u8 sText_SpikesScattered[] = _("SPIKES were scattered all around\nthe opponent's side!"); -static const u8 sText_PkmnHurtBySpikes[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is hurt\nby SPIKES!"); +static const u8 sText_SpikesScattered[] = _("Spikes were scattered all around\nthe opponent's side!"); +static const u8 sText_PkmnHurtBySpikes[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is hurt\nby spikes!"); static const u8 sText_PkmnIdentified[] = _("{B_ATK_NAME_WITH_PREFIX} identified\n{B_DEF_NAME_WITH_PREFIX}!"); static const u8 sText_PkmnPerishCountFell[] = _("{B_ATK_NAME_WITH_PREFIX}'s PERISH count\nfell to {B_BUFF1}!"); static const u8 sText_PkmnBracedItself[] = _("{B_ATK_NAME_WITH_PREFIX} braced\nitself!"); @@ -220,7 +220,7 @@ static const u8 sText_PkmnCutHPMaxedAttack[] = _("{B_ATK_NAME_WITH_PREFIX} cut i static const u8 sText_PkmnCopiedStatChanges[] = _("{B_ATK_NAME_WITH_PREFIX} copied\n{B_DEF_NAME_WITH_PREFIX}'s stat changes!"); static const u8 sText_PkmnGotFree[] = _("{B_ATK_NAME_WITH_PREFIX} got free of\n{B_DEF_NAME_WITH_PREFIX}'s {B_BUFF1}!"); static const u8 sText_PkmnShedLeechSeed[] = _("{B_ATK_NAME_WITH_PREFIX} shed\nLEECH SEED!"); -static const u8 sText_PkmnBlewAwaySpikes[] = _("{B_ATK_NAME_WITH_PREFIX} blew away\nSPIKES!"); +static const u8 sText_PkmnBlewAwaySpikes[] = _("{B_ATK_NAME_WITH_PREFIX} blew away\nspikes!"); static const u8 sText_PkmnFledFromBattle[] = _("{B_ATK_NAME_WITH_PREFIX} fled from\nbattle!"); static const u8 sText_PkmnForesawAttack[] = _("{B_ATK_NAME_WITH_PREFIX} foresaw\nan attack!"); static const u8 sText_PkmnTookAttack[] = _("{B_DEF_NAME_WITH_PREFIX} took the\n{B_BUFF1} attack!"); @@ -230,7 +230,7 @@ static const u8 sText_PkmnCenterAttention[] = _("{B_DEF_NAME_WITH_PREFIX} became static const u8 sText_PkmnChargingPower[] = _("{B_ATK_NAME_WITH_PREFIX} began\ncharging power!"); static const u8 sText_NaturePowerTurnedInto[] = _("NATURE POWER turned into\n{B_CURRENT_MOVE}!"); static const u8 sText_PkmnStatusNormal[] = _("{B_ATK_NAME_WITH_PREFIX}'s status\nreturned to normal!"); -static const u8 sText_PkmnSubjectedToTorment[] = _("{B_DEF_NAME_WITH_PREFIX} was subjected\nto TORMENT!"); +static const u8 sText_PkmnSubjectedToTorment[] = _("{B_DEF_NAME_WITH_PREFIX} was subjected\nto torment!"); static const u8 sText_PkmnTighteningFocus[] = _("{B_ATK_NAME_WITH_PREFIX} is tightening\nits focus!"); static const u8 sText_PkmnFellForTaunt[] = _("{B_DEF_NAME_WITH_PREFIX} fell for\nthe Taunt!"); static const u8 sText_PkmnReadyToHelp[] = _("{B_ATK_NAME_WITH_PREFIX} is ready to\nhelp {B_DEF_NAME_WITH_PREFIX}!"); @@ -433,13 +433,19 @@ static const u8 sText_ExclamationMark2[] = _("!"); static const u8 sText_ExclamationMark3[] = _("!"); static const u8 sText_ExclamationMark4[] = _("!"); static const u8 sText_ExclamationMark5[] = _("!"); +static const u8 sText_HP[] = _("HP"); +static const u8 sText_Attack[] = _("attack"); +static const u8 sText_Defense[] = _("defense"); +static const u8 sText_Speed[] = _("speed"); +static const u8 sText_SpAttack[] = _("sp. attack"); +static const u8 sText_SpDefense[] = _("sp. defense"); static const u8 sText_Accuracy[] = _("accuracy"); static const u8 sText_Evasiveness[] = _("evasiveness"); const u8 *const gStatNamesTable[NUM_BATTLE_STATS] = { - gText_HP3, gText_Attack, gText_Defense, - gText_Speed, gText_SpAtk, gText_SpDef, + sText_HP, sText_Attack, sText_Defense, + sText_Speed, sText_SpAttack, sText_SpDefense, sText_Accuracy, sText_Evasiveness }; @@ -515,14 +521,14 @@ static const u8 sText_TwoInGameTrainersDefeated[]; static const u8 sText_Trainer2LoseText[]; // New battle strings. -static const s8 sText_EnduredViaSturdy[] = _("{B_DEF_NAME_WITH_PREFIX} Endured\nthe hit using {B_DEF_ABILITY}!"); +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!"); @@ -591,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!"); @@ -636,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 "); diff --git a/src/battle_pike.c b/src/battle_pike.c index 61e289243..9751d565d 100644 --- a/src/battle_pike.c +++ b/src/battle_pike.c @@ -834,7 +834,7 @@ static bool8 DoesAbilityPreventStatus(struct Pokemon *mon, u32 status) ret = TRUE; break; case STATUS1_TOXIC_POISON: - if (ability == ABILITY_IMMUNITY) + if (ability == ABILITY_IMMUNITY || ability == ABILITY_PASTEL_VEIL) ret = TRUE; break; } diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index f1f89a987..d032f4da5 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -2880,11 +2880,11 @@ void SetMoveEffect(bool32 primary, u32 certain) statusChanged = TRUE; break; case STATUS1_POISON: - if (battlerAbility == ABILITY_IMMUNITY + if ((battlerAbility == ABILITY_IMMUNITY || battlerAbility == ABILITY_PASTEL_VEIL) && (primary == TRUE || certain == MOVE_EFFECT_CERTAIN)) { - gLastUsedAbility = ABILITY_IMMUNITY; - RecordAbilityBattle(gEffectBattler, ABILITY_IMMUNITY); + gLastUsedAbility = battlerAbility; + RecordAbilityBattle(gEffectBattler, battlerAbility); BattleScriptPush(gBattlescriptCurrInstr + 1); gBattlescriptCurrInstr = BattleScript_PSNPrevention; @@ -3004,10 +3004,11 @@ void SetMoveEffect(bool32 primary, u32 certain) statusChanged = TRUE; break; case STATUS1_TOXIC_POISON: - if (battlerAbility == ABILITY_IMMUNITY && (primary == TRUE || certain == MOVE_EFFECT_CERTAIN)) + if ((battlerAbility == ABILITY_IMMUNITY || battlerAbility == ABILITY_PASTEL_VEIL) + && (primary == TRUE || certain == MOVE_EFFECT_CERTAIN)) { - gLastUsedAbility = ABILITY_IMMUNITY; - RecordAbilityBattle(gEffectBattler, ABILITY_IMMUNITY); + gLastUsedAbility = battlerAbility; + RecordAbilityBattle(gEffectBattler, battlerAbility); BattleScriptPush(gBattlescriptCurrInstr + 1); gBattlescriptCurrInstr = BattleScript_PSNPrevention; @@ -3485,7 +3486,9 @@ void SetMoveEffect(bool32 primary, u32 certain) } break; case MOVE_EFFECT_FLAME_BURST: - if (IsBattlerAlive(BATTLE_PARTNER(gBattlerTarget)) && GetBattlerAbility(BATTLE_PARTNER(gBattlerTarget)) != ABILITY_MAGIC_GUARD) + if (IsBattlerAlive(BATTLE_PARTNER(gBattlerTarget)) + && !(gStatuses3[BATTLE_PARTNER(gBattlerTarget)] & STATUS3_SEMI_INVULNERABLE) + && GetBattlerAbility(BATTLE_PARTNER(gBattlerTarget)) != ABILITY_MAGIC_GUARD) { gBattleScripting.savedBattler = BATTLE_PARTNER(gBattlerTarget); gBattleMoveDamage = gBattleMons[BATTLE_PARTNER(gBattlerTarget)].hp / 16; @@ -5402,7 +5405,7 @@ static void Cmd_moveend(void) gStatuses3[gBattlerTarget] |= STATUS3_SMACKED_DOWN; gStatuses3[gBattlerTarget] &= ~(STATUS3_MAGNET_RISE | STATUS3_TELEKINESIS | STATUS3_ON_AIR); effect = TRUE; - BattleScriptPush(gBattlescriptCurrInstr + 1); + BattleScriptPush(gBattlescriptCurrInstr); gBattlescriptCurrInstr = BattleScript_MoveEffectSmackDown; } break; @@ -5415,7 +5418,7 @@ static void Cmd_moveend(void) BtlController_EmitSetMonData(0, REQUEST_STATUS_BATTLE, 0, 4, &gBattleMons[gActiveBattler].status1); MarkBattlerForControllerExec(gActiveBattler); effect = TRUE; - BattleScriptPush(gBattlescriptCurrInstr + 1); + BattleScriptPush(gBattlescriptCurrInstr); switch (gBattleMoves[gCurrentMove].argument) { case STATUS1_PARALYSIS: @@ -5912,6 +5915,7 @@ static void Cmd_moveend(void) if (gBattleResources->flags->flags[i] & RESOURCE_FLAG_EMERGENCY_EXIT) { gBattleResources->flags->flags[i] &= ~RESOURCE_FLAG_EMERGENCY_EXIT; + gSpecialStatuses[i].emergencyExited = TRUE; gBattlerTarget = gBattlerAbility = i; BattleScriptPushCursor(); if (gBattleTypeFlags & BATTLE_TYPE_TRAINER || GetBattlerSide(i) == B_SIDE_PLAYER) @@ -6754,6 +6758,7 @@ static void Cmd_switchineffects(void) if (!(gBattleMons[gActiveBattler].status1 & STATUS1_ANY) && !IS_BATTLER_OF_TYPE(gActiveBattler, TYPE_STEEL) && GetBattlerAbility(gActiveBattler) != ABILITY_IMMUNITY + && !IsAbilityOnSide(gActiveBattler, ABILITY_PASTEL_VEIL) && !(gSideStatuses[GetBattlerSide(gActiveBattler)] & SIDE_STATUS_SAFEGUARD) && !(gFieldStatuses & STATUS_FIELD_MISTY_TERRAIN)) { @@ -10048,9 +10053,9 @@ static void Cmd_various(void) return; case VARIOUS_JUMP_IF_LAST_USED_ITEM_BERRY: if (ItemId_GetPocket(gLastUsedItem) == POCKET_BERRIES) - gBattlescriptCurrInstr += 7; - else gBattlescriptCurrInstr = T1_READ_PTR(gBattlescriptCurrInstr + 3); + else + gBattlescriptCurrInstr += 7; return; case VARIOUS_JUMP_IF_LAST_USED_ITEM_HOLD_EFFECT: if (ItemId_GetHoldEffect(gLastUsedItem) == gBattlescriptCurrInstr[3]) @@ -10133,6 +10138,8 @@ static void Cmd_various(void) break; } PREPARE_STAT_BUFFER(gBattleTextBuff1, statId); + gBattlescriptCurrInstr += 4; + return; } break; case VARIOUS_TEATIME_TARGETS: @@ -10214,6 +10221,12 @@ static void Cmd_various(void) gBattlescriptCurrInstr += 7; } return; + case VARIOUS_JUMP_IF_EMERGENCY_EXITED: + if (gSpecialStatuses[gActiveBattler].emergencyExited) + gBattlescriptCurrInstr = T1_READ_PTR(gBattlescriptCurrInstr + 3); + else + gBattlescriptCurrInstr += 7; + return; } // End of switch (gBattlescriptCurrInstr[2]) gBattlescriptCurrInstr += 3; @@ -12058,7 +12071,9 @@ static void Cmd_trysetencore(void) break; } - if (gLastMoves[gBattlerTarget] == MOVE_STRUGGLE + if (gLastMoves[gBattlerTarget] == MOVE_NONE + || gLastMoves[gBattlerTarget] == MOVE_UNAVAILABLE + || gLastMoves[gBattlerTarget] == MOVE_STRUGGLE || gLastMoves[gBattlerTarget] == MOVE_ENCORE || gLastMoves[gBattlerTarget] == MOVE_MIRROR_MOVE || gLastMoves[gBattlerTarget] == MOVE_SHELL_TRAP) diff --git a/src/battle_util.c b/src/battle_util.c index a61f42b63..6d3d81678 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -515,9 +515,11 @@ void HandleAction_UseMove(void) if (gBattleTypeFlags & BATTLE_TYPE_ARENA) BattleArena_AddMindPoints(gBattlerAttacker); - // Record HP of each battler for (i = 0; i < MAX_BATTLERS_COUNT; i++) + { gBattleStruct->hpBefore[i] = gBattleMons[i].hp; + gSpecialStatuses[i].emergencyExited = FALSE; + } gCurrentActionFuncId = B_ACTION_EXEC_SCRIPT; } @@ -5566,7 +5568,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move && gBattleMons[gBattlerAttacker].hp != 0 && !gProtectStructs[gBattlerAttacker].confusionSelfDmg && TARGET_TURN_DAMAGED - && CanBePoisoned(gBattlerAttacker, gBattlerTarget) + && CanBePoisoned(gBattlerTarget, gBattlerAttacker) && IsMoveMakingContact(move, gBattlerAttacker) && (Random() % 3) == 0) { @@ -5803,7 +5805,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move && !gProtectStructs[gBattlerAttacker].confusionSelfDmg && IS_MOVE_PHYSICAL(gCurrentMove) && TARGET_TURN_DAMAGED - && !(gSideStatuses[gBattlerAttacker] & SIDE_STATUS_TOXIC_SPIKES) + && !(gSideStatuses[GetBattlerSide(gBattlerAttacker)] & SIDE_STATUS_TOXIC_SPIKES) && IsBattlerAlive(gBattlerTarget)) { gBattlerTarget = gBattlerAttacker; @@ -5895,6 +5897,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move switch (GetBattlerAbility(battler)) { case ABILITY_IMMUNITY: + case ABILITY_PASTEL_VEIL: if (gBattleMons[battler].status1 & (STATUS1_POISON | STATUS1_TOXIC_POISON | STATUS1_TOXIC_COUNTER)) { StringCopy(gBattleTextBuff1, gStatusConditionString_PoisonJpn); @@ -8142,7 +8145,7 @@ u32 GetBattlerHoldEffectParam(u8 battlerId) bool32 IsMoveMakingContact(u16 move, u8 battlerAtk) { u16 atkHoldEffect = GetBattlerHoldEffect(battlerAtk, TRUE); - + if (!(gBattleMoves[move].flags & FLAG_MAKES_CONTACT)) { if (gBattleMoves[move].effect == EFFECT_SHELL_SIDE_ARM && gBattleStruct->swapDamageCategory) @@ -8150,11 +8153,9 @@ bool32 IsMoveMakingContact(u16 move, u8 battlerAtk) else return FALSE; } - else if (GetBattlerAbility(battlerAtk) == ABILITY_LONG_REACH || atkHoldEffect == HOLD_EFFECT_PUNCHING_GLOVE) - { - return FALSE; - } - else if (atkHoldEffect == HOLD_EFFECT_PROTECTIVE_PADS) + else if ((atkHoldEffect == HOLD_EFFECT_PUNCHING_GLOVE && (gBattleMoves[move].flags & FLAG_IRON_FIST_BOOST)) + || atkHoldEffect == HOLD_EFFECT_PROTECTIVE_PADS + || GetBattlerAbility(battlerAtk) == ABILITY_LONG_REACH) { return FALSE; } @@ -10027,18 +10028,12 @@ bool32 CanMegaEvolve(u8 battlerId) // Can Mega Evolve via Mega Stone. if (holdEffect == HOLD_EFFECT_MEGA_STONE) - { - gBattleStruct->mega.isWishMegaEvo = FALSE; return TRUE; - } } // Check if there is an entry in the evolution table for Wish Mega Evolution. if (GetBattleFormChangeTargetSpecies(battlerId, FORM_CHANGE_BATTLE_MEGA_EVOLUTION_MOVE) != SPECIES_NONE) - { - gBattleStruct->mega.isWishMegaEvo = TRUE; return TRUE; - } // No checks passed, the mon CAN'T mega evolve. return FALSE; diff --git a/src/data/battle_moves.h b/src/data/battle_moves.h index 67a65c638..c34ad89d0 100644 --- a/src/data/battle_moves.h +++ b/src/data/battle_moves.h @@ -8150,7 +8150,7 @@ const struct BattleMove gBattleMoves[MOVES_COUNT_Z] = [MOVE_WOOD_HAMMER] = { - .effect = EFFECT_RECOIL_25, + .effect = EFFECT_RECOIL_33, .power = 120, .type = TYPE_GRASS, .accuracy = 100, diff --git a/src/data/graphics/items.h b/src/data/graphics/items.h index a755c7e6f..35ce13db2 100644 --- a/src/data/graphics/items.h +++ b/src/data/graphics/items.h @@ -1930,3 +1930,18 @@ const u32 gItemIcon_Gem[] = INCBIN_U32("graphics/items/icons/gem.4bpp.lz"); const u32 gItemIconPalette_Ruby[] = INCBIN_U32("graphics/items/icon_palettes/ruby.gbapal.lz"); const u32 gItemIconPalette_Sapphire[] = INCBIN_U32("graphics/items/icon_palettes/sapphire.gbapal.lz"); + +//const u32 gItemIcon_AbilityShield[] = INCBIN_U32("graphics/items/icons/ability_shield.4bpp.lz"); +//const u32 gItemIconPalette_AbilityShield[] = INCBIN_U32("graphics/items/icon_palettes/ability_shield.gbapal.lz"); + +//const u32 gItemIcon_ClearAmulet[] = INCBIN_U32("graphics/items/icons/clear_amulet.4bpp.lz"); +//const u32 gItemIconPalette_ClearAmulet[] = INCBIN_U32("graphics/items/icon_palettes/clear_amulet.gbapal.lz"); + +//const u32 gItemIcon_PunchingGlove[] = INCBIN_U32("graphics/items/icons/punching_glove.4bpp.lz"); +//const u32 gItemIconPalette_PunchingGlove[] = INCBIN_U32("graphics/items/icon_palettes/punching_glove.gbapal.lz"); + +//const u32 gItemIcon_CovertCloak[] = INCBIN_U32("graphics/items/icons/covert_cloak.4bpp.lz"); +//const u32 gItemIconPalette_CovertCloak[] = INCBIN_U32("graphics/items/icon_palettes/covert_cloak.gbapal.lz"); + +//const u32 gItemIcon_LoadedDice[] = INCBIN_U32("graphics/items/icons/loaded_dice.4bpp.lz"); +//const u32 gItemIconPalette_LoadedDice[] = INCBIN_U32("graphics/items/icon_palettes/loaded_dice.gbapal.lz"); diff --git a/src/data/item_icon_table.h b/src/data/item_icon_table.h index 0a95b5dbf..0107b71ab 100644 --- a/src/data/item_icon_table.h +++ b/src/data/item_icon_table.h @@ -803,6 +803,11 @@ const u32 *const gItemIconTable[ITEMS_COUNT + 1][2] = [ITEM_TEA] = {gItemIcon_Tea, gItemIconPalette_Tea}, [ITEM_RUBY] = {gItemIcon_Gem, gItemIconPalette_Ruby}, [ITEM_SAPPHIRE] = {gItemIcon_Gem, gItemIconPalette_Sapphire}, + [ITEM_ABILITY_SHIELD] = {gItemIcon_QuestionMark, gItemIconPalette_QuestionMark}, // {gItemIcon_AbilityShield, gItemIconPalette_AbilityShield}, + [ITEM_CLEAR_AMULET] = {gItemIcon_QuestionMark, gItemIconPalette_QuestionMark}, // {gItemIcon_ClearAmulet, gItemIconPalette_ClearAmulet}, + [ITEM_PUNCHING_GLOVE] = {gItemIcon_QuestionMark, gItemIconPalette_QuestionMark}, // {gItemIcon_PunchingGlove, gItemIconPalette_PunchingGlove}, + [ITEM_COVERT_CLOAK] = {gItemIcon_QuestionMark, gItemIconPalette_QuestionMark}, // {gItemIcon_CovertCloak, gItemIconPalette_CovertCloak}, + [ITEM_LOADED_DICE] = {gItemIcon_QuestionMark, gItemIconPalette_QuestionMark}, // {gItemIcon_LoadedDice, gItemIconPalette_LoadedDice}, // Return to field arrow [ITEMS_COUNT] = {gItemIcon_ReturnToFieldArrow, gItemIconPalette_ReturnToFieldArrow}, }; diff --git a/src/data/items.h b/src/data/items.h index 9a4c9be1d..d9286835f 100644 --- a/src/data/items.h +++ b/src/data/items.h @@ -9841,4 +9841,70 @@ const struct Item gItems[] = .type = ITEM_USE_BAG_MENU, .fieldUseFunc = ItemUseOutOfBattle_CannotUse, }, + + [ITEM_ABILITY_SHIELD] = + { + .name = _("AbilityShield"), + .itemId = ITEM_ABILITY_SHIELD, + .price = 20000, + .holdEffect = HOLD_EFFECT_ABILITY_SHIELD, + .description = sAbilityShieldDesc, + .pocket = POCKET_ITEMS, + .type = ITEM_USE_BAG_MENU, + .fieldUseFunc = ItemUseOutOfBattle_CannotUse, + .flingPower = 30, + }, + + [ITEM_CLEAR_AMULET] = + { + .name = _("Clear Amulet"), + .itemId = ITEM_CLEAR_AMULET, + .price = 30000, + .holdEffect = HOLD_EFFECT_CLEAR_AMULET, + .description = sClearAmuletDesc, + .pocket = POCKET_ITEMS, + .type = ITEM_USE_BAG_MENU, + .fieldUseFunc = ItemUseOutOfBattle_CannotUse, + .flingPower = 30, + }, + + [ITEM_PUNCHING_GLOVE] = + { + .name = _("PunchingGlove"), + .itemId = ITEM_PUNCHING_GLOVE, + .price = 15000, + .holdEffect = HOLD_EFFECT_PUNCHING_GLOVE, + .description = sPunchingGloveDesc, + .pocket = POCKET_ITEMS, + .type = ITEM_USE_BAG_MENU, + .fieldUseFunc = ItemUseOutOfBattle_CannotUse, + .flingPower = 30, + }, + + [ITEM_COVERT_CLOAK] = + { + .name = _("Covert Cloak"), + .itemId = ITEM_COVERT_CLOAK, + .price = 20000, + .holdEffect = HOLD_EFFECT_COVERT_CLOAK, + .description = sCovertCloakDesc, + .pocket = POCKET_ITEMS, + .type = ITEM_USE_BAG_MENU, + .fieldUseFunc = ItemUseOutOfBattle_CannotUse, + .flingPower = 30, + }, + + [ITEM_LOADED_DICE] = + { + //YellwApricorn + .name = _("Loaded Dice"), + .itemId = ITEM_LOADED_DICE, + .price = 20000, + .holdEffect = HOLD_EFFECT_LOADED_DICE, + .description = sLoadedDiceDesc, + .pocket = POCKET_ITEMS, + .type = ITEM_USE_BAG_MENU, + .fieldUseFunc = ItemUseOutOfBattle_CannotUse, + .flingPower = 30, + }, }; diff --git a/src/data/text/item_descriptions.h b/src/data/text/item_descriptions.h index 08e5fc846..e815d8ae1 100644 --- a/src/data/text/item_descriptions.h +++ b/src/data/text/item_descriptions.h @@ -3792,3 +3792,28 @@ static const u8 sSapphireDesc[] = _( "A brilliant blue gem\n" "that symbolizes\n" "honesty."); + +static const u8 sAbilityShieldDesc[] = _( + "Ability changes are\n" + "prevented for this\n" + "items's holder."); + +static const u8 sClearAmuletDesc[] = _( + "Stat lowering is\n" + "prevented for this\n" + "items's holder."); + +static const u8 sPunchingGloveDesc[] = _( + "Powers up punching\n" + "moves and removes\n" + "their contact."); + +static const u8 sCovertCloakDesc[] = _( + "Protects the holder\n" + "from secondary\n" + "move effects."); + +static const u8 sLoadedDiceDesc[] = _( + "Rolls high numbers.\n" + "Multihit strikes\n" + "hit more times."); diff --git a/src/daycare.c b/src/daycare.c index ecdd4b1d7..67e1c2d70 100644 --- a/src/daycare.c +++ b/src/daycare.c @@ -541,11 +541,17 @@ static void RemoveIVIndexFromList(u8 *ivs, u8 selectedIv) static void InheritIVs(struct Pokemon *egg, struct DayCare *daycare) { + u16 motherItem = GetBoxMonData(&daycare->mons[0].mon, MON_DATA_HELD_ITEM); + u16 fatherItem = GetBoxMonData(&daycare->mons[1].mon, MON_DATA_HELD_ITEM); u8 i; - u8 selectedIvs[INHERITED_IV_COUNT]; + u8 selectedIvs[5]; u8 availableIVs[NUM_STATS]; - u8 whichParents[INHERITED_IV_COUNT]; + u8 whichParents[5]; u8 iv; + u8 howManyIVs = 3; + + if (motherItem == ITEM_DESTINY_KNOT || fatherItem == ITEM_DESTINY_KNOT) + howManyIVs = 5; // Initialize a list of IV indices. for (i = 0; i < NUM_STATS; i++) @@ -553,8 +559,8 @@ static void InheritIVs(struct Pokemon *egg, struct DayCare *daycare) availableIVs[i] = i; } - // Select the 3 IVs that will be inherited. - for (i = 0; i < INHERITED_IV_COUNT; i++) + // Select which IVs that will be inherited. + for (i = 0; i < howManyIVs; i++) { // Randomly pick an IV from the available list and stop from being chosen again. // BUG: Instead of removing the IV that was just picked, this @@ -573,13 +579,13 @@ static void InheritIVs(struct Pokemon *egg, struct DayCare *daycare) } // Determine which parent each of the selected IVs should inherit from. - for (i = 0; i < INHERITED_IV_COUNT; i++) + for (i = 0; i < howManyIVs; i++) { whichParents[i] = Random() % DAYCARE_MON_COUNT; } // Set each of inherited IVs on the egg mon. - for (i = 0; i < INHERITED_IV_COUNT; i++) + for (i = 0; i < howManyIVs; i++) { switch (selectedIvs[i]) { diff --git a/src/easy_chat.c b/src/easy_chat.c index afbfebe54..723800828 100644 --- a/src/easy_chat.c +++ b/src/easy_chat.c @@ -867,17 +867,17 @@ static const u8 *const sEasyChatKeyboardAlphabet[NUM_ALPHABET_ROWS] = static const struct SpriteSheet sSpriteSheets[] = { { .data = sTriangleCursor_Gfx, - .size = 0x20, + .size = sizeof(sTriangleCursor_Gfx), .tag = GFXTAG_TRIANGLE_CURSOR }, { .data = sScrollIndicator_Gfx, - .size = 0x100, + .size = sizeof(sScrollIndicator_Gfx), .tag = GFXTAG_SCROLL_INDICATOR }, { .data = sStartSelectButtons_Gfx, - .size = 0x100, + .size = sizeof(sStartSelectButtons_Gfx), .tag = GFXTAG_START_SELECT_BUTTONS }, {0} diff --git a/src/egg_hatch.c b/src/egg_hatch.c index 725ea52dd..3a5937ba4 100644 --- a/src/egg_hatch.c +++ b/src/egg_hatch.c @@ -145,14 +145,14 @@ static const union AnimCmd *const sSpriteAnimTable_Egg[] = static const struct SpriteSheet sEggHatch_Sheet = { .data = sEggHatchTiles, - .size = 0x800, + .size = sizeof(sEggHatchTiles), .tag = GFXTAG_EGG, }; static const struct SpriteSheet sEggShards_Sheet = { .data = sEggShardTiles, - .size = 0x80, + .size = sizeof(sEggShardTiles), .tag = GFXTAG_EGG_SHARD, }; @@ -378,9 +378,6 @@ static void AddHatchedMonToParty(u8 id) GetMonNickname2(mon, gStringVar1); - ball = ITEM_POKE_BALL; - SetMonData(mon, MON_DATA_POKEBALL, &ball); - // A met level of 0 is interpreted on the summary screen as "hatched at" metLevel = 0; SetMonData(mon, MON_DATA_MET_LEVEL, &metLevel); diff --git a/src/field_specials.c b/src/field_specials.c index 22c5caaec..eb622f1f6 100644 --- a/src/field_specials.c +++ b/src/field_specials.c @@ -1240,9 +1240,9 @@ void GetSecretBaseNearbyMapName(void) GetMapName(gStringVar1, VarGet(VAR_SECRET_BASE_MAP), 0); } -u16 GetBestBattleTowerStreak(void) +u16 GetBattleTowerSinglesStreak(void) { - return GetGameStat(GAME_STAT_BATTLE_TOWER_BEST_STREAK); + return GetGameStat(GAME_STAT_BATTLE_TOWER_SINGLES_STREAK); } void BufferEReaderTrainerName(void) diff --git a/src/frontier_util.c b/src/frontier_util.c index fcdf3a02d..1863aa622 100644 --- a/src/frontier_util.c +++ b/src/frontier_util.c @@ -2068,7 +2068,7 @@ static void IncrementWinStreak(void) gSaveBlock2Ptr->frontier.towerWinStreaks[battleMode][lvlMode]++; if (battleMode == FRONTIER_MODE_SINGLES) { - SetGameStat(GAME_STAT_BATTLE_TOWER_BEST_STREAK, gSaveBlock2Ptr->frontier.towerWinStreaks[battleMode][lvlMode]); + SetGameStat(GAME_STAT_BATTLE_TOWER_SINGLES_STREAK, gSaveBlock2Ptr->frontier.towerWinStreaks[battleMode][lvlMode]); gSaveBlock2Ptr->frontier.towerSinglesStreak = gSaveBlock2Ptr->frontier.towerWinStreaks[battleMode][lvlMode]; } } diff --git a/src/item_menu_icons.c b/src/item_menu_icons.c index ff23c3792..4e6339ba3 100644 --- a/src/item_menu_icons.c +++ b/src/item_menu_icons.c @@ -205,7 +205,7 @@ static const union AffineAnimCmd *const sRotatingBallAnimCmds_FullRotation[] = static const struct SpriteSheet sRotatingBallTable = { - sRotatingBall_Gfx, 0x80, TAG_ROTATING_BALL_GFX + sRotatingBall_Gfx, sizeof(sRotatingBall_Gfx), TAG_ROTATING_BALL_GFX }; static const struct SpritePalette sRotatingBallPaletteTable = diff --git a/src/link_rfu_3.c b/src/link_rfu_3.c index 0d028cd48..2ad7a738e 100644 --- a/src/link_rfu_3.c +++ b/src/link_rfu_3.c @@ -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; } diff --git a/src/main.c b/src/main.c index 3a306b2cf..69c6c7a16 100644 --- a/src/main.c +++ b/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; } diff --git a/src/mirage_tower.c b/src/mirage_tower.c index 37096619d..01b87393a 100644 --- a/src/mirage_tower.c +++ b/src/mirage_tower.c @@ -96,7 +96,7 @@ static const s16 sCeilingCrumblePositions[][3] = static const struct SpriteSheet sCeilingCrumbleSpriteSheets[] = { - {sMirageTowerCrumbles_Gfx, 0x80, TAG_CEILING_CRUMBLE}, + {sMirageTowerCrumbles_Gfx, sizeof(sMirageTowerCrumbles_Gfx), TAG_CEILING_CRUMBLE}, {} }; diff --git a/src/move_relearner.c b/src/move_relearner.c index 9b4498a3a..05ec549e1 100644 --- a/src/move_relearner.c +++ b/src/move_relearner.c @@ -145,12 +145,19 @@ // The different versions of hearts are selected using animation // commands. -#define APPEAL_HEART_EMPTY 0 -#define APPEAL_HEART_FULL 1 -#define JAM_HEART_EMPTY 2 -#define JAM_HEART_FULL 3 +enum { + APPEAL_HEART_EMPTY, + APPEAL_HEART_FULL, + JAM_HEART_EMPTY, + JAM_HEART_FULL, +}; -#define MAX_RELEARNER_MOVES (MAX_LEVEL_UP_MOVES > 25 ? MAX_LEVEL_UP_MOVES : 25) +#define TAG_MODE_ARROWS 5325 +#define TAG_LIST_ARROWS 5425 +#define GFXTAG_UI 5525 +#define PALTAG_UI 5526 + +#define MAX_RELEARNER_MOVES max(MAX_LEVEL_UP_MOVES, 25) static EWRAM_DATA struct { @@ -174,11 +181,11 @@ static EWRAM_DATA struct { bool8 showContestInfo; } sMoveRelearnerMenuSate = {0}; -static const u16 sMoveRelearnerPaletteData[] = INCBIN_U16("graphics/interface/ui_learn_move.gbapal"); +static const u16 sUI_Pal[] = INCBIN_U16("graphics/interface/ui_learn_move.gbapal"); // The arrow sprites in this spritesheet aren't used. The scroll-arrow system provides its own // arrow sprites. -static const u8 sMoveRelearnerSpriteSheetData[] = INCBIN_U8("graphics/interface/ui_learn_move.4bpp"); +static const u8 sUI_Tiles[] = INCBIN_U8("graphics/interface/ui_learn_move.4bpp"); static const struct OamData sHeartSpriteOamData = { @@ -233,15 +240,15 @@ static const struct OamData sUnusedOam2 = static const struct SpriteSheet sMoveRelearnerSpriteSheet = { - .data = sMoveRelearnerSpriteSheetData, - .size = 0x180, - .tag = 5525 + .data = sUI_Tiles, + .size = sizeof(sUI_Tiles), + .tag = GFXTAG_UI }; static const struct SpritePalette sMoveRelearnerPalette = { - .data = sMoveRelearnerPaletteData, - .tag = 5526 + .data = sUI_Pal, + .tag = PALTAG_UI }; static const struct ScrollArrowsTemplate sDisplayModeArrowsTemplate = @@ -254,8 +261,8 @@ static const struct ScrollArrowsTemplate sDisplayModeArrowsTemplate = .secondY = 16, .fullyUpThreshold = -1, .fullyDownThreshold = -1, - .tileTag = 5325, - .palTag = 5325, + .tileTag = TAG_MODE_ARROWS, + .palTag = TAG_MODE_ARROWS, .palNum = 0, }; @@ -269,8 +276,8 @@ static const struct ScrollArrowsTemplate sMoveListScrollArrowsTemplate = .secondY = 104, .fullyUpThreshold = 0, .fullyDownThreshold = 0, - .tileTag = 5425, - .palTag = 5425, + .tileTag = TAG_LIST_ARROWS, + .palTag = TAG_LIST_ARROWS, .palNum = 0, }; @@ -308,8 +315,8 @@ static const union AnimCmd *const sHeartSpriteAnimationCommands[] = static const struct SpriteTemplate sConstestMoveHeartSprite = { - .tileTag = 5525, - .paletteTag = 5526, + .tileTag = GFXTAG_UI, + .paletteTag = PALTAG_UI, .oam = &sHeartSpriteOamData, .anims = sHeartSpriteAnimationCommands, .images = NULL, diff --git a/src/pokedex_cry_screen.c b/src/pokedex_cry_screen.c index ed53ebf1e..2c352c67a 100644 --- a/src/pokedex_cry_screen.c +++ b/src/pokedex_cry_screen.c @@ -27,6 +27,8 @@ #define WAVEFORM_WINDOW_HEIGHT 56 +#define TAG_NEEDLE 0x2000 + struct PokedexCryMeterNeedle { s8 rotation; s8 targetRotation; @@ -202,8 +204,8 @@ static const struct OamData sOamData_CryMeterNeedle = static const struct SpriteTemplate sCryMeterNeedleSpriteTemplate = { - .tileTag = 0x2000, - .paletteTag = 0x2000, + .tileTag = TAG_NEEDLE, + .paletteTag = TAG_NEEDLE, .oam = &sOamData_CryMeterNeedle, .anims = sSpriteAnimTable_CryMeterNeedle, .images = NULL, @@ -213,13 +215,13 @@ static const struct SpriteTemplate sCryMeterNeedleSpriteTemplate = static const struct SpriteSheet sCryMeterNeedleSpriteSheets[] = { - {sCryMeterNeedle_Gfx, 0x800, 0x2000}, + {sCryMeterNeedle_Gfx, sizeof(sCryMeterNeedle_Gfx), TAG_NEEDLE}, {} }; static const struct SpritePalette sCryMeterNeedleSpritePalettes[] = { - {sCryMeterNeedle_Pal, 0x2000}, + {sCryMeterNeedle_Pal, TAG_NEEDLE}, {} }; diff --git a/src/pokemon.c b/src/pokemon.c index 361c2ff08..0ef91ec14 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -8737,3 +8737,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); +} diff --git a/src/pokemon_storage_system.c b/src/pokemon_storage_system.c index b7f935c08..675a479a9 100644 --- a/src/pokemon_storage_system.c +++ b/src/pokemon_storage_system.c @@ -258,7 +258,7 @@ enum { // The maximum number of Pokémon icons that can appear on-screen. // By default the limit is 40 (though in practice only 37 can be). -#define MAX_MON_ICONS (IN_BOX_COUNT + PARTY_SIZE + 1 >= 40 ? IN_BOX_COUNT + PARTY_SIZE + 1 : 40) +#define MAX_MON_ICONS max(IN_BOX_COUNT + PARTY_SIZE + 1, 40) // The maximum number of item icons that can appear on-screen while // moving held items. 1 in the cursor, and 2 more while switching @@ -1246,7 +1246,7 @@ static const union AffineAnimCmd *const sAffineAnims_ReleaseMon[] = static const u16 sUnusedColor = RGB(26, 29, 8); -static const struct SpriteSheet sSpriteSheet_Arrow = {sArrow_Gfx, 0x80, GFXTAG_ARROW}; +static const struct SpriteSheet sSpriteSheet_Arrow = {sArrow_Gfx, sizeof(sArrow_Gfx), GFXTAG_ARROW}; static const struct OamData sOamData_BoxTitle = { diff --git a/src/recorded_battle.c b/src/recorded_battle.c index 33228255d..4cfa0e4c0 100644 --- a/src/recorded_battle.c +++ b/src/recorded_battle.c @@ -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]]; diff --git a/src/rotating_gate.c b/src/rotating_gate.c index d1184dae0..f9e732eb7 100644 --- a/src/rotating_gate.c +++ b/src/rotating_gate.c @@ -264,14 +264,14 @@ static const struct OamData sOamData_RotatingGateRegular = static const struct SpriteSheet sRotatingGatesGraphicsTable[] = { - {sRotatingGateTiles_1, 0x200, ROTATING_GATE_TILE_TAG + GATE_SHAPE_L1}, - {sRotatingGateTiles_2, 0x800, ROTATING_GATE_TILE_TAG + GATE_SHAPE_L2}, - {sRotatingGateTiles_3, 0x800, ROTATING_GATE_TILE_TAG + GATE_SHAPE_L3}, - {sRotatingGateTiles_4, 0x800, ROTATING_GATE_TILE_TAG + GATE_SHAPE_L4}, - {sRotatingGateTiles_5, 0x200, ROTATING_GATE_TILE_TAG + GATE_SHAPE_T1}, - {sRotatingGateTiles_6, 0x800, ROTATING_GATE_TILE_TAG + GATE_SHAPE_T2}, - {sRotatingGateTiles_7, 0x800, ROTATING_GATE_TILE_TAG + GATE_SHAPE_T3}, - {sRotatingGateTiles_8, 0x800, ROTATING_GATE_TILE_TAG + GATE_SHAPE_T4}, + {sRotatingGateTiles_1, sizeof(sRotatingGateTiles_1), ROTATING_GATE_TILE_TAG + GATE_SHAPE_L1}, + {sRotatingGateTiles_2, sizeof(sRotatingGateTiles_2), ROTATING_GATE_TILE_TAG + GATE_SHAPE_L2}, + {sRotatingGateTiles_3, sizeof(sRotatingGateTiles_3), ROTATING_GATE_TILE_TAG + GATE_SHAPE_L3}, + {sRotatingGateTiles_4, sizeof(sRotatingGateTiles_4), ROTATING_GATE_TILE_TAG + GATE_SHAPE_L4}, + {sRotatingGateTiles_5, sizeof(sRotatingGateTiles_5), ROTATING_GATE_TILE_TAG + GATE_SHAPE_T1}, + {sRotatingGateTiles_6, sizeof(sRotatingGateTiles_6), ROTATING_GATE_TILE_TAG + GATE_SHAPE_T2}, + {sRotatingGateTiles_7, sizeof(sRotatingGateTiles_7), ROTATING_GATE_TILE_TAG + GATE_SHAPE_T3}, + {sRotatingGateTiles_8, sizeof(sRotatingGateTiles_8), ROTATING_GATE_TILE_TAG + GATE_SHAPE_T4}, {NULL}, }; diff --git a/src/test_runner_stub.c b/src/test_runner_stub.c new file mode 100644 index 000000000..2c8b54020 --- /dev/null +++ b/src/test_runner_stub.c @@ -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) +{ +} diff --git a/test/ability_blaze.c b/test/ability_blaze.c new file mode 100644 index 000000000..259b863ec --- /dev/null +++ b/test/ability_blaze.c @@ -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); + } +} diff --git a/test/ability_cute_charm.c b/test/ability_cute_charm.c new file mode 100644 index 000000000..5e089efdf --- /dev/null +++ b/test/ability_cute_charm.c @@ -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); + } +} diff --git a/test/ability_flame_body.c b/test/ability_flame_body.c new file mode 100644 index 000000000..303337693 --- /dev/null +++ b/test/ability_flame_body.c @@ -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); + } + } +} diff --git a/test/ability_immunity.c b/test/ability_immunity.c new file mode 100644 index 000000000..fea2eb522 --- /dev/null +++ b/test/ability_immunity.c @@ -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); + } +} diff --git a/test/ability_pastel_veil.c b/test/ability_pastel_veil.c new file mode 100644 index 000000000..5d7a8f020 --- /dev/null +++ b/test/ability_pastel_veil.c @@ -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); + } +} diff --git a/test/ability_poison_point.c b/test/ability_poison_point.c new file mode 100644 index 000000000..a85edd85b --- /dev/null +++ b/test/ability_poison_point.c @@ -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); + } + } +} diff --git a/test/ability_static.c b/test/ability_static.c new file mode 100644 index 000000000..022efa5bb --- /dev/null +++ b/test/ability_static.c @@ -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); + } + } +} diff --git a/test/ability_sturdy.c b/test/ability_sturdy.c new file mode 100644 index 000000000..42cdab11f --- /dev/null +++ b/test/ability_sturdy.c @@ -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); + } +} diff --git a/test/hold_effect_leftovers.c b/test/hold_effect_leftovers.c new file mode 100644 index 000000000..ee6520587 --- /dev/null +++ b/test/hold_effect_leftovers.c @@ -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); + } + } +} diff --git a/test/mega_evolution.c b/test/mega_evolution.c new file mode 100644 index 000000000..13e9cd5b2 --- /dev/null +++ b/test/mega_evolution.c @@ -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); + } +} diff --git a/test/move.c b/test/move.c new file mode 100644 index 000000000..d7e759f27 --- /dev/null +++ b/test/move.c @@ -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); + } +} diff --git a/test/move_effect_absorb.c b/test/move_effect_absorb.c new file mode 100644 index 000000000..2ea49ef9e --- /dev/null +++ b/test/move_effect_absorb.c @@ -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); + } +} diff --git a/test/move_effect_accuracy_down.c b/test/move_effect_accuracy_down.c new file mode 100644 index 000000000..2a90d8ea2 --- /dev/null +++ b/test/move_effect_accuracy_down.c @@ -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); + } +} diff --git a/test/move_effect_after_you.c b/test/move_effect_after_you.c new file mode 100644 index 000000000..400fc053f --- /dev/null +++ b/test/move_effect_after_you.c @@ -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); + } +} diff --git a/test/move_effect_attack_down.c b/test/move_effect_attack_down.c new file mode 100644 index 000000000..6333bbea1 --- /dev/null +++ b/test/move_effect_attack_down.c @@ -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); + } +} diff --git a/test/move_effect_attack_up.c b/test/move_effect_attack_up.c new file mode 100644 index 000000000..7b57fa0d2 --- /dev/null +++ b/test/move_effect_attack_up.c @@ -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); + } +} diff --git a/test/move_effect_bide.c b/test/move_effect_bide.c new file mode 100644 index 000000000..3a5c38735 --- /dev/null +++ b/test/move_effect_bide.c @@ -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); + } +} diff --git a/test/move_effect_burn_hit.c b/test/move_effect_burn_hit.c new file mode 100644 index 000000000..88fef17cb --- /dev/null +++ b/test/move_effect_burn_hit.c @@ -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); + } +} diff --git a/test/move_effect_defense_down.c b/test/move_effect_defense_down.c new file mode 100644 index 000000000..6e5a45e84 --- /dev/null +++ b/test/move_effect_defense_down.c @@ -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); + } +} diff --git a/test/move_effect_defense_up.c b/test/move_effect_defense_up.c new file mode 100644 index 000000000..8db9a7f7e --- /dev/null +++ b/test/move_effect_defense_up.c @@ -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); + } +} diff --git a/test/move_effect_dream_eater.c b/test/move_effect_dream_eater.c new file mode 100644 index 000000000..fa17b94a6 --- /dev/null +++ b/test/move_effect_dream_eater.c @@ -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); + } +} diff --git a/test/move_effect_encore.c b/test/move_effect_encore.c new file mode 100644 index 000000000..f8a178512 --- /dev/null +++ b/test/move_effect_encore.c @@ -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); + } +} diff --git a/test/move_effect_evasion_up.c b/test/move_effect_evasion_up.c new file mode 100644 index 000000000..d14d15334 --- /dev/null +++ b/test/move_effect_evasion_up.c @@ -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); + } +} diff --git a/test/move_effect_explosion.c b/test/move_effect_explosion.c new file mode 100644 index 000000000..872f3f709 --- /dev/null +++ b/test/move_effect_explosion.c @@ -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); + } +} diff --git a/test/move_effect_freeze_hit.c b/test/move_effect_freeze_hit.c new file mode 100644 index 000000000..bb0878d0d --- /dev/null +++ b/test/move_effect_freeze_hit.c @@ -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); + } +} diff --git a/test/move_effect_haze.c b/test/move_effect_haze.c new file mode 100644 index 000000000..bd43c6947 --- /dev/null +++ b/test/move_effect_haze.c @@ -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); + } +} diff --git a/test/move_effect_hex.c b/test/move_effect_hex.c new file mode 100644 index 000000000..2847aeb17 --- /dev/null +++ b/test/move_effect_hex.c @@ -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); + } +} diff --git a/test/move_effect_hit_escape.c b/test/move_effect_hit_escape.c new file mode 100644 index 000000000..cc34db2e2 --- /dev/null +++ b/test/move_effect_hit_escape.c @@ -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!"); + } +} diff --git a/test/move_effect_paralyze_hit.c b/test/move_effect_paralyze_hit.c new file mode 100644 index 000000000..8e7d259f7 --- /dev/null +++ b/test/move_effect_paralyze_hit.c @@ -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); + } +} diff --git a/test/move_effect_poison_hit.c b/test/move_effect_poison_hit.c new file mode 100644 index 000000000..b1a154810 --- /dev/null +++ b/test/move_effect_poison_hit.c @@ -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); + } +} diff --git a/test/move_effect_rampage.c b/test/move_effect_rampage.c new file mode 100644 index 000000000..a3afebf6b --- /dev/null +++ b/test/move_effect_rampage.c @@ -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); + } +} diff --git a/test/move_effect_recoil_if_miss.c b/test/move_effect_recoil_if_miss.c new file mode 100644 index 000000000..5c1f1a61a --- /dev/null +++ b/test/move_effect_recoil_if_miss.c @@ -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); + } +} diff --git a/test/move_effect_reflect.c b/test/move_effect_reflect.c new file mode 100644 index 000000000..4ea875f35 --- /dev/null +++ b/test/move_effect_reflect.c @@ -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!"); + } +} diff --git a/test/move_effect_sleep.c b/test/move_effect_sleep.c new file mode 100644 index 000000000..c80faf4bd --- /dev/null +++ b/test/move_effect_sleep.c @@ -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); + } +} diff --git a/test/move_effect_special_attack_down.c b/test/move_effect_special_attack_down.c new file mode 100644 index 000000000..a20b8558e --- /dev/null +++ b/test/move_effect_special_attack_down.c @@ -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); + } +} diff --git a/test/move_effect_special_attack_up_3.c b/test/move_effect_special_attack_up_3.c new file mode 100644 index 000000000..f7e7e11d1 --- /dev/null +++ b/test/move_effect_special_attack_up_3.c @@ -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); + } +} diff --git a/test/move_effect_spikes.c b/test/move_effect_spikes.c new file mode 100644 index 000000000..33b0bad4b --- /dev/null +++ b/test/move_effect_spikes.c @@ -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); + } + } +} diff --git a/test/move_effect_tailwind.c b/test/move_effect_tailwind.c new file mode 100644 index 000000000..7dfffdbe0 --- /dev/null +++ b/test/move_effect_tailwind.c @@ -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!"); + } +} diff --git a/test/move_effect_torment.c b/test/move_effect_torment.c new file mode 100644 index 000000000..43f05e29f --- /dev/null +++ b/test/move_effect_torment.c @@ -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); + } +} diff --git a/test/move_effect_toxic.c b/test/move_effect_toxic.c new file mode 100644 index 000000000..f71d2a972 --- /dev/null +++ b/test/move_effect_toxic.c @@ -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); + } + } +} diff --git a/test/move_effect_toxic_spikes.c b/test/move_effect_toxic_spikes.c new file mode 100644 index 000000000..44780da21 --- /dev/null +++ b/test/move_effect_toxic_spikes.c @@ -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); + } +} diff --git a/test/status1.c b/test/status1.c new file mode 100644 index 000000000..63a9cd041 --- /dev/null +++ b/test/status1.c @@ -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)); + } +} diff --git a/test/test.h b/test/test.h new file mode 100644 index 000000000..cbcf11717 --- /dev/null +++ b/test/test.h @@ -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 diff --git a/test/test_battle.h b/test/test_battle.h new file mode 100644 index 000000000..8fb1ce6d3 --- /dev/null +++ b/test/test_battle.h @@ -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 diff --git a/test/test_runner.c b/test/test_runner.c new file mode 100644 index 000000000..a10a3a89d --- /dev/null +++ b/test/test_runner.c @@ -0,0 +1,439 @@ +#include +#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; +} diff --git a/test/test_runner_args.c b/test/test_runner_args.c new file mode 100644 index 000000000..9c0bf2ee5 --- /dev/null +++ b/test/test_runner_args.c @@ -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'}; diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c new file mode 100644 index 000000000..e40ed3e50 --- /dev/null +++ b/test/test_runner_battle.c @@ -0,0 +1,1545 @@ +#include "global.h" +#include "battle.h" +#include "battle_anim.h" +#include "battle_controllers.h" +#include "characters.h" +#include "main.h" +#include "malloc.h" +#include "random.h" +#include "test_battle.h" +#include "window.h" + +#define INVALID(fmt, ...) Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: " fmt, gTestRunnerState.test->filename, sourceLine, ##__VA_ARGS__) +#define INVALID_IF(c, fmt, ...) do { if (c) Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: " fmt, gTestRunnerState.test->filename, sourceLine, ##__VA_ARGS__); } while (0) + +#define STATE gBattleTestRunnerState +#define DATA gBattleTestRunnerState->data + +/* RNG seeds for controlling the first move of the turn. + * Found via brute force. */ + +/* Default seed, triggers most things. + * The 1st roll % 100 is <= 29, to make 30%+ accuracycheck pass. + * The 2nd roll is not a critical hit at the regular crit stage. + * The 3rd roll is consumed by damagecalc. + * The 4th roll is consumed by adjustdamage. + * The 5th roll % 100 is <= 9, to make 10%+ seteffectwithchance pass + * and % 3 is == 0, to make Poison Point and other 1/3s pass. */ +#define RNG_SEED_DEFAULT 0x000002BE + +/* Causes the first attack to critical hit if B_CRIT_CHANCE >= GEN_6. + * The 2nd roll % 24 == 0 to be a critical hit at any stage. + * The other rolls match RNG_SEED_DEFAULT. */ +#define RNG_SEED_CRITICAL_HIT 0x0000A9F4 + +/* Causes the first attack to miss if possible. + * The 1st roll % 100 is 99, to make 99%- accuracycheck fail. */ +#define RNG_SEED_MISS 0x00000074 + +EWRAM_DATA struct BattleTestRunnerState *gBattleTestRunnerState = NULL; + +static void CB2_BattleTest_NextParameter(void); +static void CB2_BattleTest_NextTrial(void); +static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32 byte); + +NAKED static void InvokeSingleTestFunctionWithStack(void *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent, SingleBattleTestFunction function, void *stack) +{ + asm("push {r4-r6,lr}\n\ + ldr r4, [sp, #16] @ function\n\ + ldr r5, [sp, #20] @ stack\n\ + mov r6, sp\n\ + mov sp, r5\n\ + push {r6}\n\ + ldr r6, =SingleRestoreSP + 1\n\ + mov lr, r6\n\ + bx r4\n\ + SingleRestoreSP:\n\ + pop {r0}\n\ + mov sp, r0\n\ + pop {r4-r6}\n\ + pop {r0}\n\ + bx r0\n\ + .pool"); +} + +NAKED static void InvokeDoubleTestFunctionWithStack(void *results, u32 i, struct BattlePokemon *playerLeft, struct BattlePokemon *opponentLeft, struct BattlePokemon *playerRight, struct BattlePokemon *opponentRight, SingleBattleTestFunction function, void *stack) +{ + asm("push {r4-r7,lr}\n\ + ldr r4, [sp, #28] @ function\n\ + ldr r5, [sp, #32] @ stack\n\ + mov r6, sp\n\ + mov sp, r5\n\ + push {r6}\n\ + add r6, #20\n\ + ldmia r6, {r6, r7} @ playerRight, opponentRight\n\ + push {r6, r7}\n\ + ldr r6, =DoubleRestoreSP + 1\n\ + mov lr, r6\n\ + bx r4\n\ + DoubleRestoreSP:\n\ + add sp, #8\n\ + pop {r0}\n\ + mov sp, r0\n\ + pop {r4-r7}\n\ + pop {r0}\n\ + bx r0\n\ + .pool"); +} + +// Calls test->function, but pointing its stack at DATA.stack so that +// local variables are live after the function returns (and so can be +// referenced by HP_BAR, or the next call, etc). +// NOTE: This places the stack in EWRAM which has longer waitstates than +// IWRAM so could be much slower, but a) not that much code executes, +// and b) mga-rom-test isn't meaningfully limited by the GBA frame rate. +static void InvokeTestFunction(const struct BattleTest *test) +{ + STATE->parametersCount = 0; + switch (test->type) + { + case BATTLE_TEST_SINGLES: + InvokeSingleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]); + break; + case BATTLE_TEST_DOUBLES: + InvokeDoubleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], &gBattleMons[B_POSITION_PLAYER_RIGHT], &gBattleMons[B_POSITION_OPPONENT_RIGHT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]); + break; + } +} + +static u32 SourceLine(u32 sourceLineOffset) +{ + const struct BattleTest *test = gTestRunnerState.test->data; + return test->sourceLine + sourceLineOffset; +} + +static u32 SourceLineOffset(u32 sourceLine) +{ + const struct BattleTest *test = gTestRunnerState.test->data; + if (sourceLine - test->sourceLine > 0xFF) + return 0; + else + return sourceLine - test->sourceLine; +} + +static u32 BattleTest_EstimateCost(void *data) +{ + u32 cost; + const struct BattleTest *test = data; + STATE = AllocZeroed(sizeof(*STATE)); + if (!STATE) + return 0; + STATE->runRandomly = TRUE; + DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT; + InvokeTestFunction(test); + cost = 1; + if (STATE->parametersCount != 0) + cost *= STATE->parametersCount; + if (STATE->trials != 0) + cost *= STATE->trials; + FREE_AND_SET_NULL(STATE); + return cost; +} + +static void BattleTest_SetUp(void *data) +{ + const struct BattleTest *test = data; + STATE = AllocZeroed(sizeof(*STATE)); + if (!STATE) + Test_ExitWithResult(TEST_RESULT_ERROR, "OOM: STATE = AllocZerod(%d)", sizeof(*STATE)); + InvokeTestFunction(test); + STATE->parameters = STATE->parametersCount; + STATE->results = AllocZeroed(test->resultsSize * STATE->parameters); + if (!STATE->results) + Test_ExitWithResult(TEST_RESULT_ERROR, "OOM: STATE->results = AllocZerod(%d)", sizeof(test->resultsSize * STATE->parameters)); + switch (test->type) + { + case BATTLE_TEST_SINGLES: + STATE->battlersCount = 2; + break; + case BATTLE_TEST_DOUBLES: + STATE->battlersCount = 4; + break; + } +} + +// This does not take into account priority, statuses, or any other +// modifiers. +static void SetImplicitSpeeds(void) +{ + s32 i, j; + u32 speed = 12; + u32 hasSpeeds = 0; + u32 allSpeeds = ((1 << DATA.playerPartySize) - 1) | (((1 << DATA.opponentPartySize) - 1) << 6); + bool32 madeProgress; + while (hasSpeeds != allSpeeds) + { + madeProgress = FALSE; + for (i = 0; i < DATA.playerPartySize; i++) + { + if (!(hasSpeeds & (1 << i)) + && !(DATA.slowerThan[B_SIDE_PLAYER][i] & ~hasSpeeds)) + { + SetMonData(&DATA.recordedBattle.playerParty[i], MON_DATA_SPEED, &speed); + speed--; + hasSpeeds |= 1 << i; + madeProgress = TRUE; + } + } + for (i = 0; i < DATA.opponentPartySize; i++) + { + if (!(hasSpeeds & ((1 << 6) << i)) + && !(DATA.slowerThan[B_SIDE_OPPONENT][i] & ~hasSpeeds)) + { + SetMonData(&DATA.recordedBattle.opponentParty[i], MON_DATA_SPEED, &speed); + speed--; + hasSpeeds |= (1 << 6) << i; + madeProgress = TRUE; + } + } + if (!madeProgress) + Test_ExitWithResult(TEST_RESULT_INVALID, "TURNs have contradictory speeds"); + } +} + +static void BattleTest_Run(void *data) +{ + s32 i; + u32 requiredPlayerPartySize; + u32 requiredOpponentPartySize; + const struct BattleTest *test = data; + + memset(&DATA, 0, sizeof(DATA)); + + DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT; + + DATA.recordedBattle.textSpeed = OPTIONS_TEXT_SPEED_FAST; + DATA.recordedBattle.battleFlags = BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_IS_MASTER; + if (test->type == BATTLE_TEST_DOUBLES) + DATA.recordedBattle.battleFlags |= BATTLE_TYPE_DOUBLE; + for (i = 0; i < STATE->battlersCount; i++) + { + DATA.recordedBattle.playersName[i][0] = CHAR_1 + i; + DATA.recordedBattle.playersName[i][1] = EOS; + DATA.recordedBattle.playersLanguage[i] = GAME_LANGUAGE; + DATA.recordedBattle.playersBattlers[i] = i; + + DATA.currentMonIndexes[i] = (i & BIT_FLANK) == B_FLANK_LEFT ? 0 : 1; + } + + STATE->runRandomly = TRUE; + STATE->runGiven = TRUE; + STATE->runWhen = TRUE; + STATE->runScene = TRUE; + InvokeTestFunction(test); + STATE->runRandomly = FALSE; + STATE->runGiven = FALSE; + STATE->runWhen = FALSE; + STATE->runScene = FALSE; + + requiredPlayerPartySize = 0; + requiredOpponentPartySize = 0; + for (i = 0; i < STATE->battlersCount; i++) + { + if ((i & BIT_SIDE) == B_SIDE_PLAYER) + requiredPlayerPartySize = DATA.currentMonIndexes[i] + 1; + else + requiredOpponentPartySize = DATA.currentMonIndexes[i] + 1; + } + if (DATA.playerPartySize < requiredPlayerPartySize) + Test_ExitWithResult(TEST_RESULT_INVALID, "%d PLAYER Pokemon required", requiredPlayerPartySize); + if (DATA.opponentPartySize < requiredOpponentPartySize) + Test_ExitWithResult(TEST_RESULT_INVALID, "%d OPPONENT Pokemon required", requiredOpponentPartySize); + + for (i = 0; i < STATE->battlersCount; i++) + PushBattlerAction(0, i, RECORDED_BYTE, 0xFF); + + if (DATA.hasExplicitSpeeds) + { + if (DATA.explicitSpeeds[B_SIDE_PLAYER] != (1 << DATA.playerPartySize) - 1 + && DATA.explicitSpeeds[B_SIDE_OPPONENT] != (1 << DATA.opponentPartySize) - 1) + { + Test_ExitWithResult(TEST_RESULT_INVALID, "Speed required for all PLAYERs and OPPONENTs"); + } + } + else + { + SetImplicitSpeeds(); + } + + SetVariablesForRecordedBattle(&DATA.recordedBattle); + + if (STATE->trials) + gMain.savedCallback = CB2_BattleTest_NextTrial; + else if (STATE->parameters) + gMain.savedCallback = CB2_BattleTest_NextParameter; + else + gMain.savedCallback = CB2_TestRunner; + SetMainCallback2(CB2_InitBattle); + + STATE->checkProgressParameter = 0; + STATE->checkProgressTrial = 0; + STATE->checkProgressTurn = 0; + + if (STATE->trials && STATE->parameters) + MgbaPrintf_(":N%s %d/%d (%d/%d)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1, STATE->trials); + else if (STATE->trials) + MgbaPrintf_(":N%s (%d/%d)", gTestRunnerState.test->name, STATE->runTrial + 1, STATE->trials); + else if (STATE->parameters) + MgbaPrintf_(":N%s %d/%d", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters); +} + +static s32 TryAbilityPopUp(s32 i, s32 n, u32 battlerId, u32 ability) +{ + struct QueuedAbilityEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_ABILITY_POPUP_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.ability; + + if (event->battlerId == battlerId + && (event->ability == ABILITY_NONE || event->ability == ability)) + return i; + } + return -1; +} + +void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryAbilityPopUp(DATA.queuedEvent, event->groupSize, battlerId, ability) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryAbilityPopUp(queuedEvent, event->groupSize, battlerId, ability)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched ABILITY_POPUP", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryAbilityPopUp(queuedEvent, event->groupSize, battlerId, ability) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryAnimation(s32 i, s32 n, u32 animType, u32 animId) +{ + struct QueuedAnimationEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_ANIMATION_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.animation; + + if (event->type == animType + && event->id == animId + && (event->attacker == 0xF || event->attacker == gBattleAnimAttacker) + && (event->target == 0xF || event->target == gBattleAnimTarget)) + return i; + } + return -1; +} + +void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryAnimation(DATA.queuedEvent, event->groupSize, animType, animId) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryAnimation(queuedEvent, event->groupSize, animType, animId)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched ANIMATION", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryAnimation(queuedEvent, event->groupSize, animType, animId) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryHP(s32 i, s32 n, u32 battlerId, u32 oldHP, u32 newHP) +{ + struct QueuedHPEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_HP_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.hp; + + if (event->battlerId == battlerId) + { + if (event->address <= 0xFFFF) + { + switch (event->type) + { + case HP_EVENT_NEW_HP: + if (event->address == newHP) + return i; + break; + case HP_EVENT_DELTA_HP: + if (event->address == 0) + return i; + else if ((s16)event->address == oldHP - newHP) + return i; + break; + } + } + else + { + switch (event->type) + { + case HP_EVENT_NEW_HP: + *(u16 *)event->address = newHP; + break; + case HP_EVENT_DELTA_HP: + *(s16 *)event->address = oldHP - newHP; + break; + } + return i; + } + } + } + return -1; +} + +void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryHP(DATA.queuedEvent, event->groupSize, battlerId, oldHP, newHP) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryHP(queuedEvent, event->groupSize, battlerId, oldHP, newHP)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched HP_BAR", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryHP(queuedEvent, event->groupSize, battlerId, oldHP, newHP) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryMessage(s32 i, s32 n, const u8 *string) +{ + s32 j, k; + struct QueuedMessageEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_MESSAGE_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.message; + for (j = k = 0; ; j++, k++) + { + if (event->pattern[k] == CHAR_SPACE) + { + switch (string[j]) + { + case CHAR_SPACE: + case CHAR_PROMPT_SCROLL: + case CHAR_PROMPT_CLEAR: + case CHAR_NEWLINE: + j++; + k++; + break; + } + } + if (event->pattern[k] == EOS) + { + // Consume any trailing '\p'. + if (string[j] == CHAR_PROMPT_CLEAR) + j++; + } + if (string[j] != event->pattern[k]) + { + break; + } + else if (string[j] == EOS) + { + return i; + } + } + } + return -1; +} + +void TestRunner_Battle_RecordMessage(const u8 *string) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryMessage(DATA.queuedEvent, event->groupSize, string) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryMessage(queuedEvent, event->groupSize, string)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched MESSAGE", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryMessage(queuedEvent, event->groupSize, string) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryStatus(s32 i, s32 n, u32 battlerId, u32 status1) +{ + struct QueuedStatusEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_STATUS_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.status; + + if (event->battlerId == battlerId) + { + if (event->mask == 0 && status1 == STATUS1_NONE) + return i; + else if (event->mask & status1) + return i; + } + } + return -1; +} + +void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryStatus(DATA.queuedEvent, event->groupSize, battlerId, status1) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryStatus(queuedEvent, event->groupSize, battlerId, status1)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched STATUS_ICON", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryStatus(queuedEvent, event->groupSize, battlerId, status1) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static const char *const sEventTypeMacros[] = +{ + [QUEUED_ABILITY_POPUP_EVENT] = "ABILITY_POPUP", + [QUEUED_ANIMATION_EVENT] = "ANIMATION", + [QUEUED_HP_EVENT] = "HP_BAR", + [QUEUED_MESSAGE_EVENT] = "MESSAGE", + [QUEUED_STATUS_EVENT] = "STATUS_ICON", +}; + +void TestRunner_Battle_AfterLastTurn(void) +{ + const struct BattleTest *test = gTestRunnerState.test->data; + + if (DATA.turns - 1 != DATA.lastActionTurn) + { + const char *filename = gTestRunnerState.test->filename; + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: %d TURNs specified, but %d ran", filename, SourceLine(0), DATA.turns, DATA.lastActionTurn + 1); + } + + while (DATA.queuedEvent < DATA.queuedEventsCount + && DATA.queuedEvents[DATA.queuedEvent].groupType == QUEUE_GROUP_NONE_OF) + { + DATA.queuedEvent += DATA.queuedEvents[DATA.queuedEvent].groupSize; + } + if (DATA.queuedEvent != DATA.queuedEventsCount) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[DATA.queuedEvent].sourceLineOffset); + const char *macro = sEventTypeMacros[DATA.queuedEvents[DATA.queuedEvent].type]; + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched %s", filename, line, macro); + } + + STATE->runThen = TRUE; + STATE->runFinally = STATE->runParameter + 1 == STATE->parameters; + InvokeTestFunction(test); + STATE->runThen = FALSE; + STATE->runFinally = FALSE; +} + +static void CB2_BattleTest_NextParameter(void) +{ + if (++STATE->runParameter >= STATE->parameters) + SetMainCallback2(CB2_TestRunner); + else + BattleTest_Run(gTestRunnerState.test->data); +} + +static void CB2_BattleTest_NextTrial(void) +{ + FreeMonSpritesGfx(); + FreeBattleSpritesData(); + FreeBattleResources(); + FreeAllWindowBuffers(); + + SetMainCallback2(CB2_BattleTest_NextParameter); + + if (++STATE->runTrial < STATE->trials) + { + switch (gTestRunnerState.result) + { + case TEST_RESULT_FAIL: + break; + case TEST_RESULT_PASS: + STATE->observedPasses++; + break; + case TEST_RESULT_SKIP: + STATE->skippedTrials++; + if (STATE->skippedTrials > STATE->trials / 4) + Test_ExitWithResult(TEST_RESULT_INVALID, "25%% of the trials were SKIPed"); + break; + default: + return; + } + if (STATE->parameters) + MgbaPrintf_(":N%s %d/%d (%d/%d)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1, STATE->trials); + else + MgbaPrintf_(":N%s (%d/%d)", gTestRunnerState.test->name, STATE->runTrial + 1, STATE->trials); + gTestRunnerState.result = TEST_RESULT_PASS; + DATA.recordedBattle.rngSeed = ISO_RANDOMIZE1(STATE->runTrial); + DATA.queuedEvent = 0; + DATA.lastActionTurn = 0; + DATA.nextRNGTurn = 0; + SetVariablesForRecordedBattle(&DATA.recordedBattle); + SetMainCallback2(CB2_InitBattle); + } + else + { + // This is a tolerance of +/- 4%. + if (abs(STATE->observedPasses - STATE->expectedPasses) <= 2) + gTestRunnerState.result = TEST_RESULT_PASS; + else + Test_ExitWithResult(TEST_RESULT_FAIL, "Expected %d/%d passes, observed %d/%d", STATE->expectedPasses, STATE->trials, STATE->observedPasses, STATE->trials); + } +} + +static void BattleTest_TearDown(void *data) +{ + if (STATE) + { + FREE_AND_SET_NULL(STATE->results); + FREE_AND_SET_NULL(STATE); + } +} + +static bool32 BattleTest_CheckProgress(void *data) +{ + bool32 madeProgress + = STATE->checkProgressParameter < STATE->runParameter + || STATE->checkProgressTrial < STATE->runTrial + || STATE->checkProgressTurn < gBattleResults.battleTurnCounter; + STATE->checkProgressParameter = STATE->runParameter; + STATE->checkProgressTrial = STATE->runTrial; + STATE->checkProgressTurn = gBattleResults.battleTurnCounter; + return madeProgress; +} + +static bool32 BattleTest_HandleExitWithResult(void *data, enum TestResult result) +{ + if (result != TEST_RESULT_INVALID + && result != TEST_RESULT_ERROR + && result != TEST_RESULT_TIMEOUT + && STATE->runTrial < STATE->trials) + { + SetMainCallback2(CB2_BattleTest_NextTrial); + return TRUE; + } + else + { + return FALSE; + } +} + +void Randomly(u32 sourceLine, u32 passes, u32 trials) +{ + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + // This is a precision of 2%. + STATE->trials = 50; + STATE->expectedPasses = STATE->trials * passes / trials; + STATE->observedPasses = 0; + STATE->skippedTrials = 0; + STATE->runTrial = 0; + DATA.recordedBattle.rngSeed = 0; +} + +void RNGSeed_(u32 sourceLine, u32 seed) +{ + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + DATA.recordedBattle.rngSeed = seed; +} + +const struct TestRunner gBattleTestRunner = +{ + .estimateCost = BattleTest_EstimateCost, + .setUp = BattleTest_SetUp, + .run = BattleTest_Run, + .tearDown = BattleTest_TearDown, + .checkProgress = BattleTest_CheckProgress, + .handleExitWithResult = BattleTest_HandleExitWithResult, +}; + +void OpenPokemon(u32 sourceLine, u32 side, u32 species) +{ + s32 i, data; + u8 *partySize; + struct Pokemon *party; + INVALID_IF(species >= SPECIES_EGG, "Invalid species: %d", species); + if (side == B_SIDE_PLAYER) + { + partySize = &DATA.playerPartySize; + party = DATA.recordedBattle.playerParty; + } + else + { + partySize = &DATA.opponentPartySize; + party = DATA.recordedBattle.opponentParty; + } + INVALID_IF(*partySize == PARTY_SIZE, "Too many Pokemon in party"); + DATA.currentSide = side; + DATA.currentPartyIndex = *partySize; + DATA.currentMon = &party[DATA.currentPartyIndex]; + DATA.gender = MON_MALE; + DATA.nature = NATURE_HARDY; + (*partySize)++; + + CreateMon(DATA.currentMon, species, 100, 0, TRUE, 0, OT_ID_PRESET, 0); + data = MOVE_NONE; + for (i = 0; i < MAX_MON_MOVES; i++) + SetMonData(DATA.currentMon, MON_DATA_MOVE1 + i, &data); +} + +// (sNaturePersonalities[i] % NUM_NATURES) == i +// (sNaturePersonalities[i] & 0xFF) == 0 +// NOTE: Using 25 << 8 rather than 0 << 8 to prevent shiny females. +static const u16 sNaturePersonalities[NUM_NATURES] = +{ + 25 << 8, 21 << 8, 17 << 8, 13 << 8, 9 << 8, + 5 << 8, 1 << 8, 22 << 8, 18 << 8, 14 << 8, + 10 << 8, 6 << 8, 2 << 8, 23 << 8, 19 << 8, + 15 << 8, 11 << 8, 7 << 8, 3 << 8, 24 << 8, + 20 << 8, 16 << 8, 12 << 8, 8 << 8, 4 << 8, +}; + +static u32 GenerateNature(u32 nature, u32 offset) +{ + int i; + if (offset <= nature) + nature -= offset; + else + nature = nature + NUM_NATURES - offset; + return sNaturePersonalities[nature]; +} + +void ClosePokemon(u32 sourceLine) +{ + s32 i; + INVALID_IF(DATA.hasExplicitSpeeds && !(DATA.explicitSpeeds[DATA.currentSide] & (1 << DATA.currentPartyIndex)), "Speed required"); + for (i = 0; i < STATE->battlersCount; i++) + { + if ((i & BIT_SIDE) == DATA.currentSide + && DATA.currentMonIndexes[i] == DATA.currentPartyIndex) + { + INVALID_IF(GetMonData(DATA.currentMon, MON_DATA_HP) == 0, "Battlers cannot be fainted"); + } + } + UpdateMonPersonality(&DATA.currentMon->box, GenerateNature(DATA.nature, DATA.gender % NUM_NATURES) | DATA.gender); + DATA.currentMon = NULL; +} + +void Gender_(u32 sourceLine, u32 gender) +{ + const struct SpeciesInfo *info; + INVALID_IF(!DATA.currentMon, "Gender outside of PLAYER/OPPONENT"); + info = &gSpeciesInfo[GetMonData(DATA.currentMon, MON_DATA_SPECIES)]; + switch (gender) + { + case MON_MALE: + DATA.gender = 0xFF; + INVALID_IF(info->genderRatio == MON_GENDERLESS || info->genderRatio == MON_FEMALE, "Illegal male"); + break; + case MON_FEMALE: + DATA.gender = 0x00; + INVALID_IF(info->genderRatio == MON_GENDERLESS || info->genderRatio == MON_MALE, "Illegal female"); + break; + case MON_GENDERLESS: + INVALID_IF(info->genderRatio != gender, "Illegal genderless"); + break; + } +} + +void Nature_(u32 sourceLine, u32 nature) +{ + INVALID_IF(!DATA.currentMon, "Nature outside of PLAYER/OPPONENT"); + INVALID_IF(nature >= NUM_NATURES, "Illegal nature: %d", nature); + DATA.nature = nature; +} + +void Ability_(u32 sourceLine, u32 ability) +{ + s32 i; + u32 species; + const struct SpeciesInfo *info; + INVALID_IF(!DATA.currentMon, "Ability outside of PLAYER/OPPONENT"); + species = GetMonData(DATA.currentMon, MON_DATA_SPECIES); + info = &gSpeciesInfo[species]; + for (i = 0; i < NUM_ABILITY_SLOTS; i++) + { + if (info->abilities[i] == ability) + { + SetMonData(DATA.currentMon, MON_DATA_ABILITY_NUM, &i); + break; + } + } + INVALID_IF(i == NUM_ABILITY_SLOTS, "%S cannot have %S", gSpeciesNames[species], gAbilityNames[ability]); +} + +void Level_(u32 sourceLine, u32 level) +{ + // TODO: Preserve any explicitly-set stats. + INVALID_IF(!DATA.currentMon, "Level outside of PLAYER/OPPONENT"); + INVALID_IF(level == 0 || level > MAX_LEVEL, "Illegal level: %d", level); + SetMonData(DATA.currentMon, MON_DATA_LEVEL, &level); +} + +void MaxHP_(u32 sourceLine, u32 maxHP) +{ + INVALID_IF(!DATA.currentMon, "MaxHP outside of PLAYER/OPPONENT"); + INVALID_IF(maxHP == 0, "Illegal max HP: %d", maxHP); + SetMonData(DATA.currentMon, MON_DATA_MAX_HP, &maxHP); +} + +void HP_(u32 sourceLine, u32 hp) +{ + INVALID_IF(!DATA.currentMon, "HP outside of PLAYER/OPPONENT"); + if (hp > GetMonData(DATA.currentMon, MON_DATA_MAX_HP)) + SetMonData(DATA.currentMon, MON_DATA_MAX_HP, &hp); + SetMonData(DATA.currentMon, MON_DATA_HP, &hp); +} + +void Attack_(u32 sourceLine, u32 attack) +{ + INVALID_IF(!DATA.currentMon, "Attack outside of PLAYER/OPPONENT"); + INVALID_IF(attack == 0, "Illegal attack: %d", attack); + SetMonData(DATA.currentMon, MON_DATA_ATK, &attack); +} + +void Defense_(u32 sourceLine, u32 defense) +{ + INVALID_IF(!DATA.currentMon, "Defense outside of PLAYER/OPPONENT"); + INVALID_IF(defense == 0, "Illegal defense: %d", defense); + SetMonData(DATA.currentMon, MON_DATA_DEF, &defense); +} + +void SpAttack_(u32 sourceLine, u32 spAttack) +{ + INVALID_IF(!DATA.currentMon, "SpAttack outside of PLAYER/OPPONENT"); + INVALID_IF(spAttack == 0, "Illegal special attack: %d", spAttack); + SetMonData(DATA.currentMon, MON_DATA_SPATK, &spAttack); +} + +void SpDefense_(u32 sourceLine, u32 spDefense) +{ + INVALID_IF(!DATA.currentMon, "SpDefense outside of PLAYER/OPPONENT"); + INVALID_IF(spDefense == 0, "Illegal special defense: %d", spDefense); + SetMonData(DATA.currentMon, MON_DATA_SPDEF, &spDefense); +} + +void Speed_(u32 sourceLine, u32 speed) +{ + INVALID_IF(!DATA.currentMon, "Speed outside of PLAYER/OPPONENT"); + INVALID_IF(speed == 0, "Illegal speed: %d", speed); + SetMonData(DATA.currentMon, MON_DATA_SPEED, &speed); + DATA.hasExplicitSpeeds = TRUE; + DATA.explicitSpeeds[DATA.currentSide] |= 1 << DATA.currentPartyIndex; +} + +void Item_(u32 sourceLine, u32 item) +{ + INVALID_IF(!DATA.currentMon, "Item outside of PLAYER/OPPONENT"); + INVALID_IF(item >= ITEMS_COUNT, "Illegal item: %d", item); + SetMonData(DATA.currentMon, MON_DATA_HELD_ITEM, &item); +} + +void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]) +{ + s32 i; + INVALID_IF(!DATA.currentMon, "Moves outside of PLAYER/OPPONENT"); + for (i = 0; i < MAX_MON_MOVES; i++) + { + if (moves[i] == MOVE_NONE) + break; + INVALID_IF(moves[i] >= MOVES_COUNT, "Illegal move: %d", moves[i]); + SetMonData(DATA.currentMon, MON_DATA_MOVE1 + i, &moves[i]); + SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[moves[i]].pp); + } + DATA.explicitMoves[DATA.currentSide] |= 1 << DATA.currentPartyIndex; +} + +void Friendship_(u32 sourceLine, u32 friendship) +{ + INVALID_IF(!DATA.currentMon, "Friendship outside of PLAYER/OPPONENT"); + SetMonData(DATA.currentMon, MON_DATA_FRIENDSHIP, &friendship); +} + +void Status1_(u32 sourceLine, u32 status1) +{ + INVALID_IF(!DATA.currentMon, "Status1 outside of PLAYER/OPPONENT"); + INVALID_IF(status1 & STATUS1_TOXIC_COUNTER, "Illegal status1: has TOXIC_TURN"); + SetMonData(DATA.currentMon, MON_DATA_STATUS, &status1); +} + +static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32 byte) +{ + u32 recordIndex = DATA.recordIndexes[battlerId]++; + if (recordIndex >= BATTLER_RECORD_SIZE) + Test_ExitWithResult(TEST_RESULT_INVALID, "Too many actions"); + DATA.battleRecordTypes[battlerId][recordIndex] = actionType; + DATA.battleRecordSourceLineOffsets[battlerId][recordIndex] = SourceLineOffset(sourceLine); + DATA.recordedBattle.battleRecord[battlerId][recordIndex] = byte; +} + +void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType) +{ + // TODO: Support explicit seeds for each turn? + if (DATA.nextRNGTurn == gBattleResults.battleTurnCounter + && (DATA.recordedBattle.rngSeed == RNG_SEED_DEFAULT + || DATA.recordedBattle.rngSeed == RNG_SEED_CRITICAL_HIT + || DATA.recordedBattle.rngSeed == RNG_SEED_MISS)) + { + gRngValue = DATA.recordedBattle.rngSeed; + DATA.nextRNGTurn++; + } + + // An illegal move choice will cause the battle to request a new + // move slot and target. This detects the move slot. + if (actionType == RECORDED_MOVE_SLOT + && recordIndex > 0 + && DATA.battleRecordTypes[battlerId][recordIndex-1] != RECORDED_ACTION_TYPE) + { + s32 i; + const char *filename = gTestRunnerState.test->filename; + for (i = recordIndex; i > 0; i--) + { + if (DATA.battleRecordTypes[battlerId][i-1] == RECORDED_ACTION_TYPE + && DATA.recordedBattle.battleRecord[battlerId][i-1] == B_ACTION_USE_MOVE) + { + u32 line = SourceLine(DATA.battleRecordSourceLineOffsets[battlerId][i-1]); + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Illegal MOVE", filename, line); + } + } + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Illegal MOVE", filename, SourceLine(0)); + } + + if (DATA.battleRecordTypes[battlerId][recordIndex] != RECORDED_BYTE) + { + DATA.lastActionTurn = gBattleResults.battleTurnCounter; + + if (actionType != DATA.battleRecordTypes[battlerId][recordIndex]) + { + const char *actualMacro = NULL; + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.battleRecordSourceLineOffsets[battlerId][recordIndex]); + + switch (DATA.battleRecordTypes[battlerId][recordIndex]) + { + case RECORDED_ACTION_TYPE: + switch (DATA.recordedBattle.battleRecord[battlerId][recordIndex]) + { + case B_ACTION_USE_MOVE: + actualMacro = "MOVE"; + break; + case B_ACTION_SWITCH: + actualMacro = "SWITCH"; + break; + } + break; + case RECORDED_PARTY_INDEX: + actualMacro = "SEND_OUT"; + break; + } + + if (actualMacro) + { + switch (actionType) + { + case RECORDED_ACTION_TYPE: + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Expected MOVE/SWITCH, got %s", filename, line, actualMacro); + case RECORDED_PARTY_INDEX: + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Expected SEND_OUT, got %s", filename, line, actualMacro); + } + } + + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: Illegal battle record", filename, line); + } + } + else + { + if (DATA.lastActionTurn == gBattleResults.battleTurnCounter) + { + const char *filename = gTestRunnerState.test->filename; + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: TURN %d incomplete", filename, SourceLine(0), gBattleResults.battleTurnCounter + 1); + } + } +} + +void OpenTurn(u32 sourceLine) +{ + INVALID_IF(DATA.turnState != TURN_CLOSED, "Nested TURN"); + DATA.turnState = TURN_OPEN; + DATA.actionBattlers = 0x00; + DATA.moveBattlers = 0x00; + DATA.hasRNGActions = FALSE; +} + +static void SetSlowerThan(s32 battlerId) +{ + s32 i, slowerThan; + slowerThan = 0; + for (i = 0; i < STATE->battlersCount; i++) + { + if (i == battlerId) + continue; + if (DATA.moveBattlers & (1 << i)) + { + if ((i & BIT_SIDE) == B_SIDE_PLAYER) + slowerThan |= 1 << DATA.currentMonIndexes[i]; + else + slowerThan |= (1 << 6) << DATA.currentMonIndexes[i]; + } + } + DATA.slowerThan[battlerId & BIT_SIDE][DATA.currentMonIndexes[battlerId]] |= slowerThan; +} + +void CloseTurn(u32 sourceLine) +{ + s32 i; + INVALID_IF(DATA.turnState != TURN_OPEN, "Nested TURN"); + DATA.turnState = TURN_CLOSING; + for (i = 0; i < STATE->battlersCount; i++) + { + if (!(DATA.actionBattlers & (1 << i))) + Move(sourceLine, &gBattleMons[i], (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE }); + } + DATA.turnState = TURN_CLOSED; + DATA.turns++; +} + +static struct Pokemon *CurrentMon(s32 battlerId) +{ + struct Pokemon *party; + if ((battlerId & BIT_SIDE) == B_SIDE_PLAYER) + party = DATA.recordedBattle.playerParty; + else + party = DATA.recordedBattle.opponentParty; + return &party[DATA.currentMonIndexes[battlerId]]; +} + +void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx) +{ + s32 i; + s32 battlerId = battler - gBattleMons; + struct Pokemon *mon = CurrentMon(battlerId); + u32 moveId, moveSlot; + s32 target; + + INVALID_IF(DATA.turnState == TURN_CLOSED, "MOVE outside TURN"); + + if (ctx.explicitMove) + { + INVALID_IF(ctx.move == MOVE_NONE || ctx.move >= MOVES_COUNT, "Illegal move: %d", ctx.move); + for (i = 0; i < MAX_MON_MOVES; i++) + { + moveId = GetMonData(mon, MON_DATA_MOVE1 + i); + if (moveId == ctx.move) + { + moveSlot = i; + break; + } + else if (moveId == MOVE_NONE) + { + INVALID_IF(DATA.explicitMoves[battlerId & BIT_SIDE] & (1 << DATA.currentMonIndexes[battlerId]), "Missing explicit %S", gMoveNames[ctx.move]); + INVALID_IF(i == MAX_MON_MOVES, "Too many different moves"); + SetMonData(mon, MON_DATA_MOVE1 + i, &ctx.move); + SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[ctx.move].pp); + moveSlot = i; + moveId = ctx.move; + break; + } + } + } + else if (ctx.explicitMoveSlot) + { + moveSlot = ctx.moveSlot; + moveId = GetMonData(mon, MON_DATA_MOVE1 + moveSlot); + INVALID_IF(moveId == MOVE_NONE, "Empty moveSlot: %d", ctx.moveSlot); + } + else + { + INVALID("No move or moveSlot"); + } + + if (ctx.explicitMegaEvolve && ctx.megaEvolve) + moveSlot |= RET_MEGA_EVOLUTION; + + if (ctx.explicitTarget) + { + target = ctx.target - gBattleMons; + } + else + { + const struct BattleMove *move = &gBattleMoves[moveId]; + if (move->target == MOVE_TARGET_RANDOM + || move->target == MOVE_TARGET_BOTH + || move->target == MOVE_TARGET_FOES_AND_ALLY + || move->target == MOVE_TARGET_OPPONENTS_FIELD + || move->target == MOVE_TARGET_ALL_BATTLERS) + { + target = BATTLE_OPPOSITE(battlerId); + } + else if (move->target == MOVE_TARGET_SELECTED) + { + INVALID_IF(STATE->battlersCount > 2, "%S requires explicit target", gMoveNames[moveId]); + + target = BATTLE_OPPOSITE(battlerId); + } + else if (move->target == MOVE_TARGET_USER) + { + target = battlerId; + } + else if (move->target == MOVE_TARGET_ALLY) + { + target = BATTLE_PARTNER(battlerId); + } + else + { + INVALID("%S requires explicit target", gMoveNames[moveId]); + } + } + + if (ctx.explicitHit && !ctx.hit) + { + if (DATA.hasRNGActions != 0) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: hit only supported on the first move", gTestRunnerState.test->filename, sourceLine); + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + DATA.recordedBattle.rngSeed = RNG_SEED_MISS; + } + + if (ctx.explicitCriticalHit && ctx.criticalHit) + { + if (DATA.hasRNGActions != 0) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: criticalHit only supported on the first move", gTestRunnerState.test->filename, sourceLine); + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + DATA.recordedBattle.rngSeed = RNG_SEED_CRITICAL_HIT; + } + + if (!(DATA.actionBattlers & (1 << battlerId))) + { + PushBattlerAction(sourceLine, battlerId, RECORDED_ACTION_TYPE, B_ACTION_USE_MOVE); + } + + if (!ctx.explicitAllowed || ctx.allowed) + { + PushBattlerAction(sourceLine, battlerId, RECORDED_MOVE_SLOT, moveSlot); + PushBattlerAction(sourceLine, battlerId, RECORDED_MOVE_TARGET, target); + } + + if (DATA.turnState == TURN_OPEN) + { + if (!DATA.hasExplicitSpeeds) + SetSlowerThan(battlerId); + + DATA.actionBattlers |= 1 << battlerId; + DATA.moveBattlers |= 1 << battlerId; + } + + // WARNING: Approximation. The move could still cause the RNG to + // advance. + if (gBattleMoves[moveId].accuracy != 0 + || gBattleMoves[moveId].split != SPLIT_STATUS) + { + DATA.hasRNGActions = TRUE; + } +} + +void ForcedMove(u32 sourceLine, struct BattlePokemon *battler) +{ + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SKIP_TURN outside TURN"); + PushBattlerAction(sourceLine, battlerId, RECORDED_ACTION_TYPE, B_ACTION_USE_MOVE); + if (DATA.turnState == TURN_OPEN) + { + if (!DATA.hasExplicitSpeeds) + SetSlowerThan(battlerId); + + DATA.actionBattlers |= 1 << battlerId; + DATA.moveBattlers |= 1 << battlerId; + } +} + +void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) +{ + s32 i; + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SWITCH outside TURN"); + INVALID_IF(DATA.actionBattlers & (1 << battlerId), "Multiple battler actions"); + INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index"); + + for (i = 0; i < STATE->battlersCount; i++) + { + if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE)) + INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "SWITCH to battler"); + } + + PushBattlerAction(sourceLine, battlerId, RECORDED_ACTION_TYPE, B_ACTION_SWITCH); + PushBattlerAction(sourceLine, battlerId, RECORDED_PARTY_INDEX, partyIndex); + DATA.currentMonIndexes[battlerId] = partyIndex; + + DATA.actionBattlers |= 1 << battlerId; +} + +void SkipTurn(u32 sourceLine, struct BattlePokemon *battler) +{ + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SKIP_TURN outside TURN"); + DATA.actionBattlers |= 1 << battlerId; +} + +void SendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) +{ + s32 i; + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SEND_OUT outside TURN"); + INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index"); + for (i = 0; i < STATE->battlersCount; i++) + { + if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE)) + INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "SEND_OUT to battler"); + } + if (!(DATA.actionBattlers & (1 << battlerId))) + Move(sourceLine, battler, (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE }); + PushBattlerAction(sourceLine, battlerId, RECORDED_PARTY_INDEX, partyIndex); + DATA.currentMonIndexes[battlerId] = partyIndex; +} + +static const char *const sQueueGroupTypeMacros[] = +{ + [QUEUE_GROUP_NONE] = NULL, + [QUEUE_GROUP_ONE_OF] = "ONE_OF", + [QUEUE_GROUP_NONE_OF] = "NONE_OF", +}; + +void OpenQueueGroup(u32 sourceLine, enum QueueGroupType type) +{ + INVALID_IF(DATA.queueGroupType, "%s inside %s", sQueueGroupTypeMacros[type], sQueueGroupTypeMacros[DATA.queueGroupType]); + DATA.queueGroupType = type; + DATA.queueGroupStart = DATA.queuedEventsCount; +} + +void CloseQueueGroup(u32 sourceLine) +{ + u32 groupSize = DATA.queuedEventsCount - DATA.queueGroupStart; + if (groupSize > 0) + { + DATA.queuedEvents[DATA.queueGroupStart].groupType = DATA.queueGroupType; + DATA.queuedEvents[DATA.queueGroupStart].groupSize = groupSize; + DATA.queueGroupType = QUEUE_GROUP_NONE; + } +} + +void QueueAbility(u32 sourceLine, struct BattlePokemon *battler, struct AbilityEventContext ctx) +{ +#if B_ABILITY_POP_UP + s32 battlerId = battler - gBattleMons; + INVALID_IF(!STATE->runScene, "ABILITY_POPUP outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: ABILITY exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_ABILITY_POPUP_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .ability = { + .battlerId = battlerId, + .ability = ctx.ability, + }}, + }; +#endif +} + +void QueueAnimation(u32 sourceLine, u32 type, u32 id, struct AnimationEventContext ctx) +{ + s32 attackerId, targetId; + + INVALID_IF(!STATE->runScene, "ANIMATION outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: ANIMATION exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + + attackerId = ctx.attacker ? ctx.attacker - gBattleMons : 0xF; + if (type == ANIM_TYPE_MOVE) + { + targetId = ctx.target ? ctx.target - gBattleMons : 0xF; + } + else + { + INVALID_IF(ctx.target, "ANIMATION target set for non-ANIM_TYPE_MOVE"); + targetId = 0xF; + } + + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_ANIMATION_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .animation = { + .type = type, + .id = id, + .attacker = attackerId, + .target = targetId, + }}, + }; +} + +void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContext ctx) +{ + s32 battlerId = battler - gBattleMons; + u32 type; + uintptr_t address; + + INVALID_IF(!STATE->runScene, "HP_BAR outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: HP_BAR exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + + if (ctx.explicitHP) + { + type = HP_EVENT_NEW_HP; + address = (u16)ctx.hp; + } + else if (ctx.explicitDamage) + { + INVALID_IF(ctx.damage == 0, "damage is 0"); + type = HP_EVENT_DELTA_HP; + address = (u16)ctx.damage; + } + else if (ctx.explicitCaptureHP) + { + INVALID_IF(ctx.captureHP == NULL, "captureHP is NULL"); + type = HP_EVENT_NEW_HP; + address = (uintptr_t)ctx.captureHP; + } + else if (ctx.explicitCaptureDamage) + { + INVALID_IF(ctx.captureDamage == NULL, "captureDamage is NULL"); + type = HP_EVENT_DELTA_HP; + *ctx.captureDamage = 0; + address = (uintptr_t)ctx.captureDamage; + } + else + { + type = HP_EVENT_DELTA_HP; + address = 0; + } + + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_HP_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .hp = { + .battlerId = battlerId, + .type = type, + .address = address, + }}, + }; +} + +void QueueMessage(u32 sourceLine, const u8 *pattern) +{ + INVALID_IF(!STATE->runScene, "MESSAGE outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: MESSAGE exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_MESSAGE_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .message = { + .pattern = pattern, + }}, + }; +} + + +void QueueStatus(u32 sourceLine, struct BattlePokemon *battler, struct StatusEventContext ctx) +{ + s32 battlerId = battler - gBattleMons; + u32 mask; + + INVALID_IF(!STATE->runScene, "STATUS_ICON outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: STATUS_ICON exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + + if (ctx.none) + mask = 0; + else if (ctx.sleep) + mask = STATUS1_SLEEP; + else if (ctx.poison) + mask = STATUS1_POISON; + else if (ctx.burn) + mask = STATUS1_BURN; + else if (ctx.freeze) + mask = STATUS1_FREEZE; + else if (ctx.paralysis) + mask = STATUS1_PARALYSIS; + else if (ctx.badPoison) + mask = STATUS1_TOXIC_POISON; + else + mask = ctx.status1; + + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_STATUS_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .status = { + .battlerId = battlerId, + .mask = mask, + }}, + }; +} diff --git a/tools/mapjson/mapjson.cpp b/tools/mapjson/mapjson.cpp index 71814269c..dc8b9080a 100644 --- a/tools/mapjson/mapjson.cpp +++ b/tools/mapjson/mapjson.cpp @@ -60,13 +60,41 @@ void write_text_file(string filepath, string text) { out_file.close(); } + +string json_to_string(const Json &data, const string &field = "") { + const Json value = !field.empty() ? data[field] : data; + string output = ""; + switch (value.type()) { + case Json::Type::STRING: + output = value.string_value(); + break; + case Json::Type::NUMBER: + output = std::to_string(value.int_value()); + break; + case Json::Type::BOOL: + output = value.bool_value() ? "TRUE" : "FALSE"; + break; + default:{ + string s = !field.empty() ? ("Value for '" + field + "'") : "JSON field"; + FATAL_ERROR("%s is unexpected type; expected string, number, or bool.\n", s.c_str()); + } + } + + if (output.empty()){ + string s = !field.empty() ? ("Value for '" + field + "'") : "JSON field"; + FATAL_ERROR("%s cannot be empty.\n", s.c_str()); + } + + return output; +} + string generate_map_header_text(Json map_data, Json layouts_data, string version) { - string map_layout_id = map_data["layout"].string_value(); + string map_layout_id = json_to_string(map_data, "layout"); vector matched; for (auto &field : layouts_data["layouts"].array_items()) { - if (map_layout_id == field["id"].string_value()) + if (map_layout_id == json_to_string(field, "id")) matched.push_back(field); } @@ -77,47 +105,47 @@ string generate_map_header_text(Json map_data, Json layouts_data, string version ostringstream text; - text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/" - << map_data["name"].string_value() - << "/map.json\n@\n\n"; + string mapName = json_to_string(map_data, "name"); - text << map_data["name"].string_value() << ":\n" - << "\t.4byte " << layout["name"].string_value() << "\n"; + text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/" << mapName << "/map.json\n@\n\n"; + + text << mapName << ":\n" + << "\t.4byte " << json_to_string(layout, "name") << "\n"; if (map_data.object_items().find("shared_events_map") != map_data.object_items().end()) - text << "\t.4byte " << map_data["shared_events_map"].string_value() << "_MapEvents\n"; + text << "\t.4byte " << json_to_string(map_data, "shared_events_map") << "_MapEvents\n"; else - text << "\t.4byte " << map_data["name"].string_value() << "_MapEvents\n"; + text << "\t.4byte " << mapName << "_MapEvents\n"; if (map_data.object_items().find("shared_scripts_map") != map_data.object_items().end()) - text << "\t.4byte " << map_data["shared_scripts_map"].string_value() << "_MapScripts\n"; + text << "\t.4byte " << json_to_string(map_data, "shared_scripts_map") << "_MapScripts\n"; else - text << "\t.4byte " << map_data["name"].string_value() << "_MapScripts\n"; + text << "\t.4byte " << mapName << "_MapScripts\n"; if (map_data.object_items().find("connections") != map_data.object_items().end() && map_data["connections"].array_items().size() > 0) - text << "\t.4byte " << map_data["name"].string_value() << "_MapConnections\n"; + text << "\t.4byte " << mapName << "_MapConnections\n"; else text << "\t.4byte 0x0\n"; - text << "\t.2byte " << map_data["music"].string_value() << "\n" - << "\t.2byte " << layout["id"].string_value() << "\n" - << "\t.byte " << map_data["region_map_section"].string_value() << "\n" - << "\t.byte " << map_data["requires_flash"].bool_value() << "\n" - << "\t.byte " << map_data["weather"].string_value() << "\n" - << "\t.byte " << map_data["map_type"].string_value() << "\n" + text << "\t.2byte " << json_to_string(map_data, "music") << "\n" + << "\t.2byte " << json_to_string(layout, "id") << "\n" + << "\t.byte " << json_to_string(map_data, "region_map_section") << "\n" + << "\t.byte " << json_to_string(map_data, "requires_flash") << "\n" + << "\t.byte " << json_to_string(map_data, "weather") << "\n" + << "\t.byte " << json_to_string(map_data, "map_type") << "\n" << "\t.2byte 0\n"; if (version == "ruby") - text << "\t.byte " << map_data["show_map_name"].bool_value() << "\n"; + text << "\t.byte " << json_to_string(map_data, "show_map_name") << "\n"; else if (version == "emerald") text << "\tmap_header_flags " - << "allow_cycling=" << map_data["allow_cycling"].bool_value() << ", " - << "allow_escaping=" << map_data["allow_escaping"].bool_value() << ", " - << "allow_running=" << map_data["allow_running"].bool_value() << ", " - << "show_map_name=" << map_data["show_map_name"].bool_value() << "\n"; + << "allow_cycling=" << json_to_string(map_data, "allow_cycling") << ", " + << "allow_escaping=" << json_to_string(map_data, "allow_escaping") << ", " + << "allow_running=" << json_to_string(map_data, "allow_running") << ", " + << "show_map_name=" << json_to_string(map_data, "show_map_name") << "\n"; - text << "\t.byte " << map_data["battle_scene"].string_value() << "\n\n"; + text << "\t.byte " << json_to_string(map_data, "battle_scene") << "\n\n"; return text.str(); } @@ -128,22 +156,22 @@ string generate_map_connections_text(Json map_data) { ostringstream text; - text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/" - << map_data["name"].string_value() - << "/map.json\n@\n\n"; + string mapName = json_to_string(map_data, "name"); - text << map_data["name"].string_value() << "_MapConnectionsList:\n"; + text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/" << mapName << "/map.json\n@\n\n"; + + text << mapName << "_MapConnectionsList:\n"; for (auto &connection : map_data["connections"].array_items()) { text << "\tconnection " - << connection["direction"].string_value() << ", " - << connection["offset"].int_value() << ", " - << connection["map"].string_value() << "\n"; + << json_to_string(connection, "direction") << ", " + << json_to_string(connection, "offset") << ", " + << json_to_string(connection, "map") << "\n"; } - text << "\n" << map_data["name"].string_value() << "_MapConnections:\n" + text << "\n" << mapName << "_MapConnections:\n" << "\t.4byte " << map_data["connections"].array_items().size() << "\n" - << "\t.4byte " << map_data["name"].string_value() << "_MapConnectionsList\n\n"; + << "\t.4byte " << mapName << "_MapConnectionsList\n\n"; return text.str(); } @@ -154,29 +182,29 @@ string generate_map_events_text(Json map_data) { ostringstream text; - text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/" - << map_data["name"].string_value() - << "/map.json\n@\n\n"; + string mapName = json_to_string(map_data, "name"); + + text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/" << mapName << "/map.json\n@\n\n"; string objects_label, warps_label, coords_label, bgs_label; if (map_data["object_events"].array_items().size() > 0) { - objects_label = map_data["name"].string_value() + "_ObjectEvents"; + objects_label = mapName + "_ObjectEvents"; text << objects_label << ":\n"; for (unsigned int i = 0; i < map_data["object_events"].array_items().size(); i++) { auto obj_event = map_data["object_events"].array_items()[i]; text << "\tobject_event " << i + 1 << ", " - << obj_event["graphics_id"].string_value() << ", 0, " - << obj_event["x"].int_value() << ", " - << obj_event["y"].int_value() << ", " - << obj_event["elevation"].int_value() << ", " - << obj_event["movement_type"].string_value() << ", " - << obj_event["movement_range_x"].int_value() << ", " - << obj_event["movement_range_y"].int_value() << ", " - << obj_event["trainer_type"].string_value() << ", " - << obj_event["trainer_sight_or_berry_tree_id"].string_value() << ", " - << obj_event["script"].string_value() << ", " - << obj_event["flag"].string_value() << "\n"; + << json_to_string(obj_event, "graphics_id") << ", 0, " + << json_to_string(obj_event, "x") << ", " + << json_to_string(obj_event, "y") << ", " + << json_to_string(obj_event, "elevation") << ", " + << json_to_string(obj_event, "movement_type") << ", " + << json_to_string(obj_event, "movement_range_x") << ", " + << json_to_string(obj_event, "movement_range_y") << ", " + << json_to_string(obj_event, "trainer_type") << ", " + << json_to_string(obj_event, "trainer_sight_or_berry_tree_id") << ", " + << json_to_string(obj_event, "script") << ", " + << json_to_string(obj_event, "flag") << "\n"; } text << "\n"; } else { @@ -184,15 +212,15 @@ string generate_map_events_text(Json map_data) { } if (map_data["warp_events"].array_items().size() > 0) { - warps_label = map_data["name"].string_value() + "_MapWarps"; + warps_label = mapName + "_MapWarps"; text << warps_label << ":\n"; for (auto &warp_event : map_data["warp_events"].array_items()) { text << "\twarp_def " - << warp_event["x"].int_value() << ", " - << warp_event["y"].int_value() << ", " - << warp_event["elevation"].int_value() << ", " - << warp_event["dest_warp_id"].string_value() << ", " - << warp_event["dest_map"].string_value() << "\n"; + << json_to_string(warp_event, "x") << ", " + << json_to_string(warp_event, "y") << ", " + << json_to_string(warp_event, "elevation") << ", " + << json_to_string(warp_event, "dest_warp_id") << ", " + << json_to_string(warp_event, "dest_map") << "\n"; } text << "\n"; } else { @@ -200,24 +228,24 @@ string generate_map_events_text(Json map_data) { } if (map_data["coord_events"].array_items().size() > 0) { - coords_label = map_data["name"].string_value() + "_MapCoordEvents"; + coords_label = mapName + "_MapCoordEvents"; text << coords_label << ":\n"; for (auto &coord_event : map_data["coord_events"].array_items()) { - if (coord_event["type"].string_value() == "trigger") { + if (json_to_string(coord_event, "type") == "trigger") { text << "\tcoord_event " - << coord_event["x"].int_value() << ", " - << coord_event["y"].int_value() << ", " - << coord_event["elevation"].int_value() << ", " - << coord_event["var"].string_value() << ", " - << coord_event["var_value"].string_value() << ", " - << coord_event["script"].string_value() << "\n"; + << json_to_string(coord_event, "x") << ", " + << json_to_string(coord_event, "y") << ", " + << json_to_string(coord_event, "elevation") << ", " + << json_to_string(coord_event, "var") << ", " + << json_to_string(coord_event, "var_value") << ", " + << json_to_string(coord_event, "script") << "\n"; } else if (coord_event["type"] == "weather") { text << "\tcoord_weather_event " - << coord_event["x"].int_value() << ", " - << coord_event["y"].int_value() << ", " - << coord_event["elevation"].int_value() << ", " - << coord_event["weather"].string_value() << "\n"; + << json_to_string(coord_event, "x") << ", " + << json_to_string(coord_event, "y") << ", " + << json_to_string(coord_event, "elevation") << ", " + << json_to_string(coord_event, "weather") << "\n"; } } text << "\n"; @@ -226,31 +254,31 @@ string generate_map_events_text(Json map_data) { } if (map_data["bg_events"].array_items().size() > 0) { - bgs_label = map_data["name"].string_value() + "_MapBGEvents"; + bgs_label = mapName + "_MapBGEvents"; text << bgs_label << ":\n"; for (auto &bg_event : map_data["bg_events"].array_items()) { if (bg_event["type"] == "sign") { text << "\tbg_sign_event " - << bg_event["x"].int_value() << ", " - << bg_event["y"].int_value() << ", " - << bg_event["elevation"].int_value() << ", " - << bg_event["player_facing_dir"].string_value() << ", " - << bg_event["script"].string_value() << "\n"; + << json_to_string(bg_event, "x") << ", " + << json_to_string(bg_event, "y") << ", " + << json_to_string(bg_event, "elevation") << ", " + << json_to_string(bg_event, "player_facing_dir") << ", " + << json_to_string(bg_event, "script") << "\n"; } else if (bg_event["type"] == "hidden_item") { text << "\tbg_hidden_item_event " - << bg_event["x"].int_value() << ", " - << bg_event["y"].int_value() << ", " - << bg_event["elevation"].int_value() << ", " - << bg_event["item"].string_value() << ", " - << bg_event["flag"].string_value() << "\n"; + << json_to_string(bg_event, "x") << ", " + << json_to_string(bg_event, "y") << ", " + << json_to_string(bg_event, "elevation") << ", " + << json_to_string(bg_event, "item") << ", " + << json_to_string(bg_event, "flag") << "\n"; } else if (bg_event["type"] == "secret_base") { text << "\tbg_secret_base_event " - << bg_event["x"].int_value() << ", " - << bg_event["y"].int_value() << ", " - << bg_event["elevation"].int_value() << ", " - << bg_event["secret_base_id"].string_value() << "\n"; + << json_to_string(bg_event, "x") << ", " + << json_to_string(bg_event, "y") << ", " + << json_to_string(bg_event, "elevation") << ", " + << json_to_string(bg_event, "secret_base_id") << "\n"; } } text << "\n"; @@ -258,7 +286,7 @@ string generate_map_events_text(Json map_data) { bgs_label = "0x0"; } - text << map_data["name"].string_value() << "_MapEvents::\n" + text << mapName << "_MapEvents::\n" << "\tmap_events " << objects_label << ", " << warps_label << ", " << coords_label << ", " << bgs_label << "\n\n"; @@ -301,17 +329,17 @@ string generate_groups_text(Json groups_data) { text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/map_groups.json\n@\n\n"; for (auto &key : groups_data["group_order"].array_items()) { - string group = key.string_value(); + string group = json_to_string(key); text << group << "::\n"; auto maps = groups_data[group].array_items(); for (Json &map_name : maps) - text << "\t.4byte " << map_name.string_value() << "\n"; + text << "\t.4byte " << json_to_string(map_name) << "\n"; text << "\n"; } text << "\t.align 2\n" << "gMapGroups::\n"; for (auto &group : groups_data["group_order"].array_items()) - text << "\t.4byte " << group.string_value() << "\n"; + text << "\t.4byte " << json_to_string(group) << "\n"; text << "\n"; return text.str(); @@ -321,7 +349,7 @@ string generate_connections_text(Json groups_data) { vector map_names; for (auto &group : groups_data["group_order"].array_items()) - for (auto map_name : groups_data[group.string_value()].array_items()) + for (auto map_name : groups_data[json_to_string(group)].array_items()) map_names.push_back(map_name); vector connections_include_order = groups_data["connections_include_order"].array_items(); @@ -342,7 +370,7 @@ string generate_connections_text(Json groups_data) { text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/maps/map_groups.json\n@\n\n"; for (Json map_name : map_names) - text << "\t.include \"data/maps/" << map_name.string_value() << "/connections.inc\"\n"; + text << "\t.include \"data/maps/" << json_to_string(map_name) << "/connections.inc\"\n"; return text.str(); } @@ -351,8 +379,8 @@ string generate_headers_text(Json groups_data) { vector map_names; for (auto &group : groups_data["group_order"].array_items()) - for (auto map_name : groups_data[group.string_value()].array_items()) - map_names.push_back(map_name.string_value()); + for (auto map_name : groups_data[json_to_string(group)].array_items()) + map_names.push_back(json_to_string(map_name)); ostringstream text; @@ -368,8 +396,8 @@ string generate_events_text(Json groups_data) { vector map_names; for (auto &group : groups_data["group_order"].array_items()) - for (auto map_name : groups_data[group.string_value()].array_items()) - map_names.push_back(map_name.string_value()); + for (auto map_name : groups_data[json_to_string(group)].array_items()) + map_names.push_back(json_to_string(map_name)); ostringstream text; @@ -397,25 +425,28 @@ string generate_map_constants_text(string groups_filepath, Json groups_data) { vector map_count_vec; //DEBUG for (auto &group : groups_data["group_order"].array_items()) { - text << "// " << group.string_value() << "\n"; + string groupName = json_to_string(group); + text << "// " << groupName << "\n"; vector map_ids; size_t max_length = 0; int map_count = 0; //DEBUG - for (auto &map_name : groups_data[group.string_value()].array_items()) { - string header_filepath = file_dir + map_name.string_value() + dir_separator + "map.json"; + for (auto &map_name : groups_data[groupName].array_items()) { + string header_filepath = file_dir + json_to_string(map_name) + dir_separator + "map.json"; string err_str; Json map_data = Json::parse(read_text_file(header_filepath), err_str); map_ids.push_back(map_data["id"]); - if (map_data["id"].string_value().length() > max_length) - max_length = map_data["id"].string_value().length(); + string id = json_to_string(map_data, "id"); + if (id.length() > max_length) + max_length = id.length(); map_count++; //DEBUG } int map_id_num = 0; for (Json map_id : map_ids) { - text << "#define " << map_id.string_value() << string((max_length - map_id.string_value().length() + 1), ' ') + string id = json_to_string(map_id); + text << "#define " << id << string((max_length - id.length() + 1), ' ') << "(" << map_id_num++ << " | (" << group_num << " << 8))\n"; } text << "\n"; @@ -467,20 +498,21 @@ string generate_layout_headers_text(Json layouts_data) { text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/layouts/layouts.json\n@\n\n"; for (auto &layout : layouts_data["layouts"].array_items()) { - string border_label = layout["name"].string_value() + "_Border"; - string blockdata_label = layout["name"].string_value() + "_Blockdata"; + string layoutName = json_to_string(layout, "name"); + string border_label = layoutName + "_Border"; + string blockdata_label = layoutName + "_Blockdata"; text << border_label << "::\n" - << "\t.incbin \"" << layout["border_filepath"].string_value() << "\"\n\n" + << "\t.incbin \"" << json_to_string(layout, "border_filepath") << "\"\n\n" << blockdata_label << "::\n" - << "\t.incbin \"" << layout["blockdata_filepath"].string_value() << "\"\n\n" + << "\t.incbin \"" << json_to_string(layout, "blockdata_filepath") << "\"\n\n" << "\t.align 2\n" - << layout["name"].string_value() << "::\n" - << "\t.4byte " << layout["width"].int_value() << "\n" - << "\t.4byte " << layout["height"].int_value() << "\n" + << layoutName << "::\n" + << "\t.4byte " << json_to_string(layout, "width") << "\n" + << "\t.4byte " << json_to_string(layout, "height") << "\n" << "\t.4byte " << border_label << "\n" << "\t.4byte " << blockdata_label << "\n" - << "\t.4byte " << layout["primary_tileset"].string_value() << "\n" - << "\t.4byte " << layout["secondary_tileset"].string_value() << "\n\n"; + << "\t.4byte " << json_to_string(layout, "primary_tileset") << "\n" + << "\t.4byte " << json_to_string(layout, "secondary_tileset") << "\n\n"; } return text.str(); @@ -492,10 +524,10 @@ string generate_layouts_table_text(Json layouts_data) { text << "@\n@ DO NOT MODIFY THIS FILE! It is auto-generated from data/layouts/layouts.json\n@\n\n"; text << "\t.align 2\n" - << layouts_data["layouts_table_label"].string_value() << "::\n"; + << json_to_string(layouts_data, "layouts_table_label") << "::\n"; for (auto &layout : layouts_data["layouts"].array_items()) - text << "\t.4byte " << layout["name"].string_value() << "\n"; + text << "\t.4byte " << json_to_string(layout, "name") << "\n"; return text.str(); } @@ -510,7 +542,7 @@ string generate_layouts_constants_text(Json layouts_data) { int i = 0; for (auto &layout : layouts_data["layouts"].array_items()) - text << "#define " << layout["id"].string_value() << " " << ++i << "\n"; + text << "#define " << json_to_string(layout, "id") << " " << ++i << "\n"; text << "\n#endif // GUARD_CONSTANTS_LAYOUTS_H\n"; diff --git a/tools/mgba-rom-test-hydra/.gitignore b/tools/mgba-rom-test-hydra/.gitignore new file mode 100644 index 000000000..b3b89034b --- /dev/null +++ b/tools/mgba-rom-test-hydra/.gitignore @@ -0,0 +1 @@ +mgba-rom-test-hydra diff --git a/tools/mgba-rom-test-hydra/Makefile b/tools/mgba-rom-test-hydra/Makefile new file mode 100644 index 000000000..5f33f001b --- /dev/null +++ b/tools/mgba-rom-test-hydra/Makefile @@ -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) diff --git a/tools/mgba-rom-test-hydra/main.c b/tools/mgba-rom-test-hydra/main.c new file mode 100644 index 000000000..f9c51d505 --- /dev/null +++ b/tools/mgba-rom-test-hydra/main.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/tools/mgba/README.md b/tools/mgba/README.md new file mode 100644 index 000000000..617e6e058 --- /dev/null +++ b/tools/mgba/README.md @@ -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: . +The source code for these specific builds is available from: + + - Windows: + - Linux: diff --git a/tools/mgba/mgba-rom-test b/tools/mgba/mgba-rom-test new file mode 100755 index 000000000..09c683a2f Binary files /dev/null and b/tools/mgba/mgba-rom-test differ diff --git a/tools/mgba/mgba-rom-test.exe b/tools/mgba/mgba-rom-test.exe new file mode 100755 index 000000000..5b25e16f6 Binary files /dev/null and b/tools/mgba/mgba-rom-test.exe differ diff --git a/tools/patchelf/.gitignore b/tools/patchelf/.gitignore new file mode 100644 index 000000000..fca468008 --- /dev/null +++ b/tools/patchelf/.gitignore @@ -0,0 +1 @@ +patchelf diff --git a/tools/patchelf/Makefile b/tools/patchelf/Makefile new file mode 100644 index 000000000..4e60bd631 --- /dev/null +++ b/tools/patchelf/Makefile @@ -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) diff --git a/tools/patchelf/patchelf.c b/tools/patchelf/patchelf.c new file mode 100644 index 000000000..19363c55f --- /dev/null +++ b/tools/patchelf/patchelf.c @@ -0,0 +1,191 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 [ ]...\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; +}