From 4d349e9210f15c67b3f5b645ee085ed2429e53e9 Mon Sep 17 00:00:00 2001 From: GGbond Date: Sun, 8 Feb 2026 16:15:57 +0800 Subject: [PATCH 1/4] Fix Aroma Veil target-side check for ally-targeted limiting moves (#9157) --- src/battle_script_commands.c | 2 +- test/battle/ability/aroma_veil.c | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 7a29f9c5b9..9d4f679208 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -4442,7 +4442,7 @@ static void Cmd_jumpifability(void) } break; case BS_TARGET_SIDE: - battler = IsAbilityOnOpposingSide(gBattlerAttacker, ability); + battler = IsAbilityOnSide(gBattlerTarget, ability); if (battler) { battler--; diff --git a/test/battle/ability/aroma_veil.c b/test/battle/ability/aroma_veil.c index 9a911f5d21..d9efde13c1 100644 --- a/test/battle/ability/aroma_veil.c +++ b/test/battle/ability/aroma_veil.c @@ -22,6 +22,24 @@ DOUBLE_BATTLE_TEST("Aroma Veil protects the Pokémon's side from Taunt") } } +DOUBLE_BATTLE_TEST("Aroma Veil protects the Pokémon's side from ally Taunt") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_TAUNT) == EFFECT_TAUNT); + ASSUME(GetMoveCategory(MOVE_HARDEN) == DAMAGE_CATEGORY_STATUS); + PLAYER(SPECIES_AROMATISSE) { Ability(ABILITY_AROMA_VEIL); Speed(1); } + PLAYER(SPECIES_WOBBUFFET) { Speed(2); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(3); } + OPPONENT(SPECIES_WYNAUT) { Speed(4); } + } WHEN { + TURN { MOVE(playerRight, MOVE_TAUNT, target: playerLeft); MOVE(playerLeft, MOVE_HARDEN); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_TAUNT, playerRight); + ABILITY_POPUP(playerLeft, ABILITY_AROMA_VEIL); + ANIMATION(ANIM_TYPE_MOVE, MOVE_HARDEN, playerLeft); + } +} + DOUBLE_BATTLE_TEST("Aroma Veil protects the Pokémon's side from Torment") { struct BattlePokemon *moveTarget = NULL; From d88b82eab217f4d8418c505372bc1a9fe4e8fd67 Mon Sep 17 00:00:00 2001 From: GGbond Date: Sun, 8 Feb 2026 16:19:10 +0800 Subject: [PATCH 2/4] Fix immunity ability status sync to avoid persisting Toxic counter when only volatile conditions are cured (#9156) --- src/battle_util.c | 7 +++++-- test/battle/ability/oblivious.c | 26 ++++++++++++++++++++++++++ test/battle/ability/own_tempo.c | 26 ++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/battle_util.c b/src/battle_util.c index cad82d035e..e352173577 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -9703,8 +9703,11 @@ u32 TryImmunityAbilityHealStatus(u32 battler, enum AbilityEffect caseID) } gBattleScripting.battler = gBattlerAbility = battler; - BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_STATUS_BATTLE, 0, 4, &gBattleMons[battler].status1); - MarkBattlerForControllerExec(battler); + if (effect == 1) // Only primary status changes should sync party status. + { + BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_STATUS_BATTLE, 0, 4, &gBattleMons[battler].status1); + MarkBattlerForControllerExec(battler); + } return effect; } diff --git a/test/battle/ability/oblivious.c b/test/battle/ability/oblivious.c index ecbf2d3f5f..e8340b2955 100644 --- a/test/battle/ability/oblivious.c +++ b/test/battle/ability/oblivious.c @@ -99,3 +99,29 @@ SINGLE_BATTLE_TEST("Oblivious prevents Intimidate (Gen8+)") MESSAGE("Slowpoke's Oblivious prevents stat loss!"); } } + +SINGLE_BATTLE_TEST("Oblivious cured infatuation should not persist toxic counter after switching") +{ + s16 firstTick, secondTick, postSwitchTick; + + GIVEN { + ASSUME(GetMoveEffect(MOVE_ATTRACT) == EFFECT_ATTRACT); + ASSUME(GetMoveEffect(MOVE_SKILL_SWAP) == EFFECT_SKILL_SWAP); + PLAYER(SPECIES_WOBBUFFET) { Gender(MON_MALE); Status1(STATUS1_TOXIC_POISON); MaxHP(160); HP(160); Speed(100); } + PLAYER(SPECIES_WYNAUT) { Speed(90); } + OPPONENT(SPECIES_SLOWPOKE) { Gender(MON_FEMALE); Ability(ABILITY_OBLIVIOUS); Speed(80); } + } WHEN { + TURN { MOVE(opponent, MOVE_ATTRACT); } + TURN { MOVE(opponent, MOVE_SKILL_SWAP); } + TURN { SWITCH(player, 1); } + TURN { SWITCH(player, 0); } + } SCENE { + HP_BAR(player, captureDamage: &firstTick); + HP_BAR(player, captureDamage: &secondTick); + HP_BAR(player, captureDamage: &postSwitchTick); + } THEN { + EXPECT_EQ(firstTick, 10); + EXPECT_EQ(secondTick, 20); + EXPECT_EQ(postSwitchTick, 10); + } +} diff --git a/test/battle/ability/own_tempo.c b/test/battle/ability/own_tempo.c index 3195f2fcf1..5f8413e56b 100644 --- a/test/battle/ability/own_tempo.c +++ b/test/battle/ability/own_tempo.c @@ -143,3 +143,29 @@ SINGLE_BATTLE_TEST("Own Tempo prevents confusion from items") ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); } } + +SINGLE_BATTLE_TEST("Own Tempo cured confusion should not persist toxic counter after switching") +{ + s16 firstTick, secondTick, postSwitchTick; + + GIVEN { + ASSUME(GetMoveEffect(MOVE_CONFUSE_RAY) == EFFECT_CONFUSE); + ASSUME(GetMoveEffect(MOVE_SKILL_SWAP) == EFFECT_SKILL_SWAP); + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_TOXIC_POISON); MaxHP(160); HP(160); Speed(100); } + PLAYER(SPECIES_WYNAUT) { Speed(90); } + OPPONENT(SPECIES_SLOWPOKE) { Ability(ABILITY_OWN_TEMPO); Speed(80); } + } WHEN { + TURN { MOVE(opponent, MOVE_CONFUSE_RAY); } + TURN { MOVE(opponent, MOVE_SKILL_SWAP); MOVE(player, MOVE_CELEBRATE, WITH_RNG(RNG_CONFUSION, FALSE)); } + TURN { SWITCH(player, 1); } + TURN { SWITCH(player, 0); } + } SCENE { + HP_BAR(player, captureDamage: &firstTick); + HP_BAR(player, captureDamage: &secondTick); + HP_BAR(player, captureDamage: &postSwitchTick); + } THEN { + EXPECT_EQ(firstTick, 10); + EXPECT_EQ(secondTick, 20); + EXPECT_EQ(postSwitchTick, 10); + } +} From 75500c86ab9b3a73ab7c1c15f82a83d78c2e36d8 Mon Sep 17 00:00:00 2001 From: GGbond Date: Sun, 8 Feb 2026 18:50:59 +0800 Subject: [PATCH 3/4] Add missing weather checks for AI sandstorm/hail damage helpers (#9155) --- src/battle_ai_util.c | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index c607450315..83a93bb60c 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -3162,23 +3162,31 @@ static u32 GetPoisonDamage(u32 battlerId) return damage; } -static bool32 BattlerAffectedBySandstorm(u32 battlerId, enum Ability ability) +static bool32 DoesBattlerTakeSandstormDamage(u32 battlerId, enum Ability ability) { + if (!(AI_GetWeather() & B_WEATHER_SANDSTORM)) + return FALSE; + if (!IS_BATTLER_ANY_TYPE(battlerId, TYPE_ROCK, TYPE_GROUND, TYPE_STEEL) && ability != ABILITY_SAND_VEIL && ability != ABILITY_SAND_FORCE && ability != ABILITY_SAND_RUSH + && ability != ABILITY_MAGIC_GUARD && ability != ABILITY_OVERCOAT) return TRUE; return FALSE; } -static bool32 BattlerAffectedByHail(u32 battlerId, enum Ability ability) +static bool32 DoesBattlerTakeHailDamage(u32 battlerId, enum Ability ability) { + if (!(AI_GetWeather() & B_WEATHER_HAIL)) + return FALSE; + if (!IS_BATTLER_OF_TYPE(battlerId, TYPE_ICE) && ability != ABILITY_SNOW_CLOAK - && ability != ABILITY_OVERCOAT - && ability != ABILITY_ICE_BODY) + && ability != ABILITY_ICE_BODY + && ability != ABILITY_MAGIC_GUARD + && ability != ABILITY_OVERCOAT) return TRUE; return FALSE; } @@ -3194,7 +3202,7 @@ static u32 GetWeatherDamage(u32 battlerId) if (weather & B_WEATHER_SANDSTORM) { - if (BattlerAffectedBySandstorm(battlerId, ability) + if (DoesBattlerTakeSandstormDamage(battlerId, ability) && gBattleMons[battlerId].volatiles.semiInvulnerable != STATE_UNDERGROUND && gBattleMons[battlerId].volatiles.semiInvulnerable != STATE_UNDERWATER && holdEffect != HOLD_EFFECT_SAFETY_GOGGLES) @@ -3206,7 +3214,7 @@ static u32 GetWeatherDamage(u32 battlerId) } if ((weather & B_WEATHER_HAIL) && ability != ABILITY_ICE_BODY) { - if (BattlerAffectedByHail(battlerId, ability) + if (DoesBattlerTakeHailDamage(battlerId, ability) && gBattleMons[battlerId].volatiles.semiInvulnerable != STATE_UNDERGROUND && gBattleMons[battlerId].volatiles.semiInvulnerable != STATE_UNDERWATER && holdEffect != HOLD_EFFECT_SAFETY_GOGGLES) @@ -3238,8 +3246,11 @@ u32 GetBattlerSecondaryDamage(u32 battlerId) bool32 BattlerWillFaintFromWeather(u32 battler, enum Ability ability) { - if ((BattlerAffectedBySandstorm(battler, ability) || BattlerAffectedByHail(battler, ability)) - && gBattleMons[battler].hp <= max(1, gBattleMons[battler].maxHP / 16)) + if (gAiLogicData->holdEffects[battler] == HOLD_EFFECT_SAFETY_GOGGLES) + return FALSE; + + if ((DoesBattlerTakeSandstormDamage(battler, ability) || DoesBattlerTakeHailDamage(battler, ability)) + && gBattleMons[battler].hp <= max(1, GetNonDynamaxMaxHP(battler) / 16)) return TRUE; return FALSE; From ac7e45351d4e35b30082083b4c9d499371520980 Mon Sep 17 00:00:00 2001 From: Kildemal <206095739+izrofid@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:04:40 +0530 Subject: [PATCH 4/4] docs(dns): add nighttime palette user guide (#9158) --- docs/tutorials/dns.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/tutorials/dns.md b/docs/tutorials/dns.md index 498d62caa5..3948a60c6d 100644 --- a/docs/tutorials/dns.md +++ b/docs/tutorials/dns.md @@ -67,3 +67,46 @@ A: Shadows can be disabled for certain locations by modifying the `CurrentMapHas ### Q: How do I change the default light-blend color? A: The default color is handled by the `#define DEFAULT_LIGHT_COLOR` in `src/palette.c`. + +### Q. How do I use alternate nighttime palettes? + +In addition to palette tinting, the DNS allows tilesets to define alternate nighttime palettes. +These palettes are automatically blended with their corresponding daytime palettes with the passage of time. + +Each tileset has `16` total palette slots. `3` of the BG palettes (`13 - 15`) are reserved for the UI leaving `13` (`0-12`) usable for tilesets. Because primary tilesets load `6` (`NUM_PALS_IN_PRIMARY`) palettes (`0-5`) and secondary Tilesets load `7` palettes (6-12), some slots are unused for each. DNS repurposes these unused slots to store alternate nighttime palettes. + +To avoid overlap with active palettes, each nighttime palette is stored in a different slot determined by the formula: `night_pal = (day_pal + 9) % 16` + +**Day palette index vs. Night palette index** + +| Day | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | +|-----|---|---|---|---|---|---|---|---|---|----|----|----|----|----|----|----| +| Night | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | + + +For instance, in a secondary Tileset (slots `6-12`), the nighttime palettes for index `8` would be stored at index `1` (which is unused in a secondary tileset). Slots `0` and `13-15` are not used for blending so the corresponding slots `9` and `6-8` are never used for nighttime palettes. + +_Note that palette `0` is not used for blending._ + +Once the appropriate nighttime `.pal` file has been added, add a `swapPalettes` field to the tileset struct definition in `src/data/tilesets/headers.h`. The macro `SWAP_PAL(x)` is provided for this purpose. + +`swapPalettes` is a bitmask so to use nighttime palettes for indices 7 and 9 in the Petalburg Tileset we add a `swapPalettes` with `SWAP_PAL(7) | SWAP_PAL(9)`. + +Note that the palette index to specify here is the palette index that you want to swap at night (**NOT** the corresponding nighttime palette). + +```diff +const struct Tileset gTileset_Petalburg = +{ + .isCompressed = TRUE, + .isSecondary = TRUE, + .tiles = gTilesetTiles_Petalburg, ++ .swapPalettes = SWAP_PAL(7) | SWAP_PAL(9), // Enable nighttime variants for slots 7 and 9 + .palettes = gTilesetPalettes_Petalburg, + .metatiles = gMetatiles_Petalburg, + .metatileAttributes = gMetatileAttributes_Petalburg, + .callback = InitTilesetAnim_Petalburg, +}; +``` + + +