Create Battle Palettes for Different Times of Day ( Environments, Weather Changes) - pret/pokecrystal GitHub Wiki
In gen II, the color palette of the overworld changes depending on the time of day, but this is not reflected in battles. Here, we will learn how to make a battle palette for night battles.
Contents
- Create a New wram Label
- Load the Time into wBattleTimeOfDay
- Create PALRGB_NIGHT
- Edit LoadPalette_White_Col1_Col2_Black
- Mask Out R, G, and B and Edit Colors
- Bonus: Add Additional Checks for Indoors and Caves
- Apply Palette to entire Battle Screen
- Bonus: Add Additional Checks for the Stats Screen and Sunny Day
1. Create a New wram Label
Edit ram/wram.asm:
wMapTimeOfDay:: db
+wBattleTimeOfDay:: db
- ds 3
+ ds 2
We're going to store a numeric value in wBattleTimeOfDay, and load it at a later point to determine what palette we should load. It is important that this label be created in WRAM0, so it can be called in any bank.
2. Load the Time into wBattleTimeOfDay
Edit engine/battle/start_battle.asm:
FindFirstAliveMonAndStartBattle:
+ ld a, [wTimeOfDay]
+ cp NITE_F
+ jr z, .nightpal
+
+.daypal
+ ld a, 0
+ ld [wBattleTimeOfDay], a
+ jr .timeofdaypalset
+
+.nightpal
+ ld a, 1
+ ld [wBattleTimeOfDay], a
+
+.timeofdaypalset
xor a
Then edit engine/battle/core.asm:
CleanUpBattleRAM:
call BattleEnd_HandleRoamMons
xor a
+ ld [wBattleTimeOfDay], a
ld [wLowHealthAlarm], a
Here, we are setting up for the Time of Day to be read at the start of battle, and loaded into our label. Day = 0, and Night = 1 in this example. We also want to make sure the label is cleared at the end of the battle.
3. Create PALRGB_NIGHT
Create the desired battle background color palette in constants/gfx_constants.asm:
DEF PALRGB_WHITE EQU palred 31 + palgreen 31 + palblue 31 ; $7fff
+DEF PALRGB_NIGHT EQU palred 15 + palgreen 15 + palblue 20
This is the color that will replace the normal 'white' battle BG color. For night colors, we are taking half from red and green, and a bit less from blue.
4. Edit LoadPalette_White_Col1_Col2_Black
Edit LoadPalette_White_Col1_Col2_Black in engine/gfx/color.asm:
LoadPalette_White_Col1_Col2_Black:
ldh a, [rSVBK]
push af
ld a, BANK(wBGPals1)
ldh [rSVBK], a
+ ld a, [wBattleTimeOfDay]
+ and a
+ jr z, .day
+
+ ld a, LOW(PALRGB_NIGHT)
+ ld [de], a
+ inc de
+ ld a, HIGH(PALRGB_NIGHT)
+ ld [de], a
+ inc de
+
+ call NightColors
+ ld c, 2 * PAL_COLOR_SIZE
+
+ jr .black
+
+.day
ld a, LOW(PALRGB_WHITE)
ld [de], a
inc de
ld a, HIGH(PALRGB_WHITE)
ld [de], a
inc de
ld c, 2 * PAL_COLOR_SIZE
.loop
ld a, [hli]
ld [de], a
inc de
dec c
jr nz, .loop
+.black
xor a
Now, we are editing the function that loads the palettes. Add a check for our label, and if it does not pass the day check, it will fall through and begin to load our night color modification. Now comes the tricky part; dissecting and modifying the middle colors.
5. Mask Out R, G, and B and Edit Colors
Edit engine/gfx/color.asm again to create NightColors: & NightColorSwap:
+NightColors:
+ call NightColorSwap
+
+; b = gggrrrrr, c = 0bbbbbGG
+.loop
+ ld a, b
+ ld [de], a
+ inc de
+ inc hl
+
+ ld a, c
+ ld [de], a
+ inc de
+ inc hl
+
+ call NightColorSwap
+
+; b = gggrrrrr, c = 0bbbbbGG
+.loop2
+ ld a, b
+ ld [de], a
+ inc hl
+ inc de
+
+ ld a, c
+ ld [de], a
+ inc hl
+ inc de
+
+ ret
+
+NightColorSwap:
+ push de
+
+; red
+ ld a, [hl] ; gggrrrrr
+ and $1f ; 00011111 -> 000rrrrr
+
+ ld e, a ; red in e
+
+;green
+ ld a, [hli] ; gggrrrrr
+ and $e0 ; 11100000 -> ggg00000
+ ld b, a
+ ld a, [hl] ; 0bbbbbGG
+ and 3 ; 00000011 -> 000000GG
+ or b ; 000000GG + ggg00000
+ swap a ; ggg0 00GG -> 00GGggg0
+ rrca ; 000GGggg
+
+ ld d, a ; green in d
+
+;blue
+ ld a, [hld] ; 0bbbbbGG
+ and $7c ; 1111100 -> 0bbbbb00
+
+ ld c, a ; blue in c
+
+;modify colors here
+ srl e ; 1/2 red
+ srl d ; 1/2 green
+
+; 3/4 blue
+ ld a, c
+ rrca ; 1/2
+ ld b, a
+ rrca ; 1/4
+ add b ; 2/4 + 1/4 = 3/4
+ and %01111100 ; mask the blue bits
+ ld c, a
+
+;reassemble green
+ ld a, d
+ rlca ; 00GGggg0
+ swap a ; 00GG ggg0 -> ggg000GG
+ and $e0 ; 11100000 -> ggg00000
+ ld b, a
+ ld a, d
+ rlca ; 00GGggg0
+ swap a ; 00GG ggg0 -> ggg000GG
+ and 3 ; 00000011 -> 000000GG
+ ld d, a
+
+;red in e, low green in b, high green in d, blue in c
+ ld a, e
+ or b ; 000rrrrr + ggg00000
+ ld b, a ; gggrrrrr
+ ld a, d
+ or c ; 0bbbbb00 + 000000GG
+ ld c, a ; 0bbbbbGG
+ pop de
+ ret
Each color is stored in two pairs, gggrrrrr and 0bbbbbGG. Since we want to modify the 3 RGB values individually, we need to mask out each color and store them in e, d, and c before we can adjust them. Then we must reassemble them before we finish.
6. Bonus: Add Additional Checks for Indoors, Dungeons and Caves
Here we'll add individual checks for indoors and cave environments, and load in the same Day/Night palettes determined by [wBattleTimeOfDay] with 0 = Day and 1 = Night. This just adds an easier structure if you were to later add new unique battle palettes for caves and indoors. Edit engine/battle/start_battle.asm again:
FindFirstAliveMonAndStartBattle:
+ call GetMapEnvironment
+ cp DUNGEON
+ jr z, .dungeonpal
+
+ call GetMapEnvironment
+ cp CAVE
+ jr z, .cavepal
+
+ call GetMapEnvironment
+ cp INDOOR
+ jr z, .indoorpal
+
+ ld a, [wTimeOfDay]
+ cp MORN_F
+ jr z, .mornpal
+
+ ld a, [wTimeOfDay]
+ cp DAY_F
+ jr z, .daypal
+
+ ld a, [wTimeOfDay]
+ cp NITE_F
+ jr z, .nightpal
+
+.mornpal
+ ld a, 0
+ ld [wBattleTimeOfDay], a
+ jr .timeofdaypalset
+
+.daypal
+ ld a, 0
+ ld [wBattleTimeOfDay], a
+ jr .timeofdaypalset
+.nightpal
+ ld a, 1
+ ld [wBattleTimeOfDay], a
+ jr .timeofdaypalset
+.dungeonpal
+ ld a, 0
+ ld [wBattleTimeOfDay], a
+ jr .timeofdaypalset
+.indoorpal
+ ld a, 0
+ ld [wBattleTimeOfDay], a
+ jr .timeofdaypalset
+.cavepal
+ ld a, 1
+ ld [wBattleTimeOfDay], a
.timeofdaypalset
xor a
ldh [hMapAnims], a
And that's it!
7. Apply Palette to entire Battle Screen
Now you have a night palette but it only applies to the upper battle section of the screen, the lower text boxes still appear with a normal palette. If you would rather the entire battle screen have the new night palette this can be easily achieved with the following changes.
Alter the file - gfx/cgb_layouts.asm
_CGB_FinishBattleScreenLayout:
....
call FillBoxCGB
hlcoord 0, 12, wAttrmap
ld bc, 6 * SCREEN_WIDTH
- ld a, PAL_BATTLE_BG_TEXT
+ ld a, PAL_BATTLE_BG_PLAYER
And also the file - home/text.asm
TextboxPalette::
; Fill text box width c height b at hl with pal 7
ld de, wAttrmap - wTilemap
add hl, de
inc b
inc b
inc c
inc c
+ ld a, [wBattleMode]
+ and a
+ jr nz, .battle
+ ld a, PAL_BG_TEXT
+ jr .col
+.battle
+ ld a, PAL_BATTLE_BG_PLAYER
.col
push bc
push hl
There! Now the entire battle area will have the new palette applied with no impact to overworld palettes.
8. Bonus: Add Additional Checks for the Stats Screen and Sunny Day.
This step will fix the palette errors that occur on the Stats Screen when accessed through battle, in particular shifting between pages and Pokemon with the D-Pad, meaning that the battle palette will only affect the battle arena itself. NOTE: It's recommended to do Step 7. as a prelude to this step.
Again, we'll edit LoadPalette_White_Col1_Col2_Black in engine/gfx/color.asm:
LoadPalette_White_Col1_Col2_Black:
ldh a, [rSVBK]
push af
ld a, BANK(wBGPals1)
ldh [rSVBK], a
+; Check whether Stat Pages are active, set them to day palette.
+; First, check whether in a battle, then jump to the Stats Page checks.
+ ld a, [wBattleMode]
+ cp 0
+ jr nz, .StatsFlags
+
+; NOTE: The Stats Pages use flag values 1, 2, and 3. In the case that you added a fourth stats page, it uses value 0. But if we check for page 0, a state that usually would be a zero value free to be overwritten elsewhere, an error occurs in which the incorrect battle palette (wBattleTimeOfDay) gets called in battle due to it being set to the day palette. Avoid checking for 0 (cp 0) here.
+
+.StatsFlags
+ ld a, [wStatsScreenFlags]
+ cp 1
+ jr z, .day
+ cp 2
+ jr z, .day
+ cp 3
+ jr z, .day
+; Check whether the weather is Sunny (re: Sunny Day), set to day palette.
+ ld a, [wBattleWeather]
+ cp WEATHER_SUN
+ jr z, .day
+ ld a, [wBattleTimeOfDay]
+ and a
+ jr z, .day
+; If weather is not Sunny, jump to Night Colors.
+ ld a, [wBattleWeather]
+ cp WEATHER_SUN
+ jr nz, .night1
+
+.night1
+; Call Night background colors
ld a, LOW(PALRGB_NIGHT)
ld [de], a
inc de
ld a, HIGH(PALRGB_NIGHT)
ld [de], a
inc de
+
+; Call Night sprite colors
call NightColors
ld c, 2 * PAL_COLOR_SIZE
jr .black
.day
+; Call Day background colors
ld a, LOW(PALRGB_WHITE)
ld [de], a
inc de
ld a, HIGH(PALRGB_WHITE)
ld [de], a
inc de
+; Call Day sprite colors
ld c, 2 * PAL_COLOR_SIZE
.loop
ld a, [hli]
ld [de], a
inc de
dec c
jr nz, .loop
.black
xor a
ld [de], a
inc de
ld [de], a
inc de
pop af
ldh [rSVBK], a
ret
Now, we need to go edit wram.asm master/ram/wram.asm:
Due to wStatsScreenFlags and wPackJumptableIndex sharing a UNION, and thus sharing the same initial address, entering and exiting the pack in battle can now interfere with your night palette. The fix is simple.
Locate ; pack wPackJumptableIndex:: db wCurPocket:: db wPackUsedItem:: db
and move it to just above it's UNION label.
wJumptableIndex::
wBattleTowerBattleEnded::
db
+; pack
+wPackJumptableIndex:: db
+wCurPocket:: db
+wPackUsedItem:: db
+
UNION
; intro data
wIntroSceneFrameCounter:: db
wIntroSceneTimer:: db
...
This will result in wPackJumptableIndex and wStatsScreenFlags no longer sharing the same address, meaning the pack state won't interfere with your stat page palettes.
Now, when you return to the Battle Screen from the Stats Screen, we need to load the correct palette back into memory. We don't need to do complicated environment checks upon returning, since the battle palette has a lot of variables between the Map Environment, the Time Of Day and the Weather State.
Instead, we need to edit wram.asm again... master/ram/wram.asm:
Underneath wBattleTimeOfDay
we create another label called wBattleTimeOfDayBackup
.
wMapTimeOfDay:: db
wBattleTimeOfDay:: db
+wBattleTimeOfDayBackup:: db
- ds 2
+ ds 1 ; (that is, subtract 1 from whatever value you had here, this presumes it was 2)
Then we need to hop on over to master/engine/pokemon/stats_screen.asm:
We find BattleStatsScreenInit:
BattleStatsScreenInit:
+; Load current Battle Palette into 'a', then load 'a' into [wBattleTimeOfDayBackup]. This ensures you have the correct palette to return to.
+ ld a, [wBattleTimeOfDay]
+ ld [wBattleTimeOfDayBackup], a
+
+ ; Now we load 0, the day palette into [wBattleTimeOfDay], ensuring that the stats screen keeps it's white background palette.
+ ld a, 0
+ ld [wBattleTimeOfDay], a
+
ld a, [wLinkMode]
cp LINK_MOBILE
jr nz, StatsScreenInit
ld a, [wBattleMode]
and a
jr z, StatsScreenInit
jr _MobileStatsScreenInit
Then in the same file, we find StatsScreen_Exit:
StatsScreen_Exit:
ld hl, wJumptableIndex
set 7, [hl]
+ jp StatsScreen_Exit2
ret
+
+; This loads the backed up palette back into [wBattleTimeOfDay] in time for the Stats Screen to exit and the Battle Screen to refresh.
+ StatsScreen_Exit2:
+ ld a, [wBattleTimeOfDayBackup]
+ ld [wBattleTimeOfDay], a
Now, we make the necessary changes to the move Sunny Day and the other weather moves, to trigger the active change in palette. In master/engine/battle/move_effects/, find the files for Sunny Day, Sandstorm, Rain Dance, and if you've added the Hail weather condition, then that too.
The fix is simple. In each file, just insert one line of farcall _CGB_BattleColors
before the animation call. The correct battle palette has already been chosen, due to the value of [wBattleWeather] being "WEATHER_SUN", calling back to the code we added in color.asm, this simply tells the battle screen to prepare those colours to be refreshed, which occurs during the animation sequence, for a reason we'll see in a moment.
BattleCommand_StartSun:
; call ClearBGPalettes
; call ClearTilemap
ld a, WEATHER_SUN
ld [wBattleWeather], a
ld a, 5
ld [wWeatherCount], a
+ farcall _CGB_BattleColors
call AnimateCurrentMove
ld hl, SunGotBrightText
jp StdBattleTextbox
Adding it to Sandstorm, Rain Dance and Hail, makes sure that the battle palette is rechecked upon the activation of a new weather condition. If it is night time and the weather changes from Sunny Day to any of the others, the palette will return to it's night time state.
Now, what about when your weather condition wears off naturally after so many turns?
If we go to master/engine/battle/core.asm, and locate HandleBetweenTurnEffects
, you can add another farcall _CGB_BattleColors
just after call HandleWeather
.
HandleBetweenTurnEffects:
ldh a, [hSerialConnectionStatus]
cp USING_EXTERNAL_CLOCK
jr z, .CheckEnemyFirst
call CheckFaint_PlayerThenEnemy
ret c
call HandleFutureSight
call CheckFaint_PlayerThenEnemy
ret c
call HandleWeather
+ farcall _CGB_BattleColors
call CheckFaint_PlayerThenEnemy
ret c
call HandleWrap
call CheckFaint_PlayerThenEnemy
ret c
call HandlePerishSong
call CheckFaint_PlayerThenEnemy
ret c
jr .NoMoreFaintingConditions
Now, it turns out that farcall _CGB_BattleColors
doesn't actually request the attrmap/palettes update, only prepares it. That's why the palette wouldn't update when the weather faded. You have to actually make sure to signal the update by setting hCGBPalUpdate
to 1. Perhaps this signal could be added to earlier steps to streamline them, but this code works currently.
So, we stay in master/engine/battle/core.asm, and go to HandleWeather
.
HandleWeather:
ld a, [wBattleWeather]
cp WEATHER_NONE
ret z
ld hl, wWeatherCount
dec [hl]
jr nz, .continues
jr z, .ended
.ended
ld hl, .WeatherEndedMessages
call .PrintWeatherMessage
+ farcall _CGB_BattleColors
+ ld a, 1
+ ld [hCGBPalUpdate], a
xor a
ld [wBattleWeather], a
ret
Done! Now the palettes correctly change in the myriad of instances of the following:
Cycling through the Pack Pages and returning to battle.
Cycling through the Stats Screen states.
Exiting Stats Screen and returning to battle.
In an environmental change (CAVE, DUNGEON, ROUTE, etc.).
Time Of Day effects.
Using weather effects.
Changing weather effects.
When weather effects end naturally.