summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh <32826900+ShinyDragonHunter@users.noreply.github.com>2022-03-04 08:01:48 +0000
committerJosh <32826900+ShinyDragonHunter@users.noreply.github.com>2022-03-04 08:01:48 +0000
commit2a9892f8cd4e05418f607d6130b65e635b1b8215 (patch)
treeeb24bfe62355255ca8db36e73c092865eeb20b70
parent41f974ee60f1f5c677ef6e5992de4c69b25387f2 (diff)
Created Improve the Loading of Battle Terrain (markdown)
-rw-r--r--Improve-the-Loading-of-Battle-Terrain.md493
1 files changed, 493 insertions, 0 deletions
diff --git a/Improve-the-Loading-of-Battle-Terrain.md b/Improve-the-Loading-of-Battle-Terrain.md
new file mode 100644
index 0000000..db12388
--- /dev/null
+++ b/Improve-the-Loading-of-Battle-Terrain.md
@@ -0,0 +1,493 @@
+WARNING: a bit of a long tutorial.
+
+When making a ROM hack, it's natural to want to add new battle terrain for battles. Emerald unfortunately handles loading these through big switch statements in functions.
+FRLG on the other hand, has a few a extra functions that handle the loading of these terrain by giving them the required battle terrain ID for example `BATTLE_TERRAIN_PLAIN`
+at the cost of needing every single battle terrain in `sBattleTerrainTable`. These changes are done in [battle_bg.c](../blob/master/src/battle_bg.c).
+
+This will walk you through porting this behaviour over to Emerald. Firstly, we're going to look at some things from pokefirered.
+
+```c
+// Maps map scene values to battle terrain IDs.
+// Maps with any of the MAP_BATTLE_SCENE_* values seen here will use these
+// regardless of whether the player is on water or in tall grass etc
+static const struct {
+ u8 mapScene;
+ u8 battleTerrain;
+} sMapBattleSceneMapping[] = {
+ {MAP_BATTLE_SCENE_GYM, BATTLE_TERRAIN_GYM},
+ {MAP_BATTLE_SCENE_INDOOR_1, BATTLE_TERRAIN_INDOOR_1},
+ {MAP_BATTLE_SCENE_INDOOR_2, BATTLE_TERRAIN_INDOOR_2},
+ {MAP_BATTLE_SCENE_LORELEI, BATTLE_TERRAIN_LORELEI},
+ {MAP_BATTLE_SCENE_BRUNO, BATTLE_TERRAIN_BRUNO},
+ {MAP_BATTLE_SCENE_AGATHA, BATTLE_TERRAIN_AGATHA},
+ {MAP_BATTLE_SCENE_LANCE, BATTLE_TERRAIN_LANCE},
+ {MAP_BATTLE_SCENE_LINK, BATTLE_TERRAIN_LINK}
+};
+```
+
+```c
+// If current map scene equals any of the values in sMapBattleSceneMapping,
+// use its battle terrain value. Otherwise, use the default.
+static u8 GetBattleTerrainByMapScene(u8 mapBattleScene)
+{
+ int i;
+ for (i = 0; i < NELEMS(sMapBattleSceneMapping); i++)
+ {
+ if (mapBattleScene == sMapBattleSceneMapping[i].mapScene)
+ return sMapBattleSceneMapping[i].battleTerrain;
+ }
+ return BATTLE_TERRAIN_PLAIN;
+}
+```
+
+```c
+// Loads the initial battle terrain.
+static void LoadBattleTerrainGfx(u16 terrain)
+{
+ if (terrain >= NELEMS(sBattleTerrainTable))
+ terrain = BATTLE_TERRAIN_PLAIN; // If higher than the number of entries in sBattleTerrainTable, use the default.
+ // Copy to bg3
+ LZDecompressVram(sBattleTerrainTable[terrain].tileset, (void *)BG_CHAR_ADDR(2));
+ LZDecompressVram(sBattleTerrainTable[terrain].tilemap, (void *)BG_SCREEN_ADDR(26));
+ LoadCompressedPalette(sBattleTerrainTable[terrain].palette, 0x20, 0x60);
+}
+```
+
+```c
+// Loads the entry associated with the battle terrain.
+// This can be the grass moving on the screen at the start of a wild encounter in tall grass.
+static void LoadBattleTerrainEntryGfx(u16 terrain)
+{
+ if (terrain >= NELEMS(sBattleTerrainTable))
+ terrain = BATTLE_TERRAIN_PLAIN;
+ // Copy to bg1
+ LZDecompressVram(sBattleTerrainTable[terrain].entryTileset, (void *)BG_CHAR_ADDR(1));
+ LZDecompressVram(sBattleTerrainTable[terrain].entryTilemap, (void *)BG_SCREEN_ADDR(28));
+}
+```
+
+```c
+// Gets the battle terrain value if conditions are met.
+// This could be a specific trainer class, battle type or wild Pokémon.
+// If no conditions are met or if the map scene isn't MAP_BATTLE_SCENE_NORMAL,
+// gets the battle terrain mapped to map scenes.
+static u8 GetBattleTerrainOverride(void)
+{
+ u8 battleScene;
+ if (gBattleTypeFlags & (BATTLE_TYPE_TRAINER_TOWER | BATTLE_TYPE_LINK | BATTLE_TYPE_BATTLE_TOWER | BATTLE_TYPE_EREADER_TRAINER))
+ {
+ return BATTLE_TERRAIN_LINK;
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_POKEDUDE)
+ {
+ gBattleTerrain = BATTLE_TERRAIN_GRASS;
+ return BATTLE_TERRAIN_GRASS;
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_TRAINER)
+ {
+ if (gTrainers[gTrainerBattleOpponent_A].trainerClass == CLASS_LEADER_2)
+ {
+ return BATTLE_TERRAIN_LEADER;
+ }
+ else if (gTrainers[gTrainerBattleOpponent_A].trainerClass == CLASS_CHAMPION_2)
+ {
+ return BATTLE_TERRAIN_CHAMPION;
+ }
+ }
+ battleScene = GetCurrentMapBattleScene();
+ if (battleScene == MAP_BATTLE_SCENE_NORMAL)
+ {
+ return gBattleTerrain;
+ }
+ return GetBattleTerrainByMapScene(battleScene);
+}
+```
+
+We will basically taking advantage of these functions to get the battle terrain we are wanted to use. We will need to add some more BATTLE_TERRAIN_* values. In `include/constants/battle.h`,
+We should see these:
+```c
+// Battle terrain defines for gBattleTerrain.
+#define BATTLE_TERRAIN_GRASS 0
+#define BATTLE_TERRAIN_LONG_GRASS 1
+#define BATTLE_TERRAIN_SAND 2
+#define BATTLE_TERRAIN_UNDERWATER 3
+#define BATTLE_TERRAIN_WATER 4
+#define BATTLE_TERRAIN_POND 5
+#define BATTLE_TERRAIN_MOUNTAIN 6
+#define BATTLE_TERRAIN_CAVE 7
+#define BATTLE_TERRAIN_BUILDING 8
+#define BATTLE_TERRAIN_PLAIN 9
+```
+It's going to need to look like this:
+```c
+// Battle terrain defines for gBattleTerrain.
+#define BATTLE_TERRAIN_GRASS 0
+#define BATTLE_TERRAIN_LONG_GRASS 1
+#define BATTLE_TERRAIN_SAND 2
+#define BATTLE_TERRAIN_UNDERWATER 3
+#define BATTLE_TERRAIN_WATER 4
+#define BATTLE_TERRAIN_POND 5
+#define BATTLE_TERRAIN_MOUNTAIN 6
+#define BATTLE_TERRAIN_CAVE 7
+#define BATTLE_TERRAIN_BUILDING 8
+#define BATTLE_TERRAIN_PLAIN 9
+#define BATTLE_TERRAIN_FRONTIER 10
+#define BATTLE_TERRAIN_GYM 11
+#define BATTLE_TERRAIN_LEADER 12
+#define BATTLE_TERRAIN_MAGMA 13
+#define BATTLE_TERRAIN_AQUA 14
+#define BATTLE_TERRAIN_SIDNEY 15
+#define BATTLE_TERRAIN_PHOEBE 16
+#define BATTLE_TERRAIN_GLACIA 17
+#define BATTLE_TERRAIN_DRAKE 18
+#define BATTLE_TERRAIN_CHAMPION 19
+#define BATTLE_TERRAIN_GROUDON 20
+#define BATTLE_TERRAIN_KYOGRE 21
+#define BATTLE_TERRAIN_RAYQUAZA 22
+```
+
+We then need to update `sBattleTerrainTable` with our new `BATTLE_TERRAIN_*` values. They need to be in order of these values.
+So if you change `BATTLE_TERRAIN_PLAIN` to be for example 20, you will need to move it further down the terrain table.
+Changes needed are large so I'll show individually what's needed.
+
+```c
+ [BATTLE_TERRAIN_FRONTIER] =
+ {
+ .tileset = gBattleTerrainTiles_Building,
+ .tilemap = gBattleTerrainTilemap_Building,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_Frontier,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_GYM] =
+ {
+ .tileset = gBattleTerrainTiles_Building,
+ .tilemap = gBattleTerrainTilemap_Building,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_BuildingGym,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_LEADER] =
+ {
+ .tileset = gBattleTerrainTiles_Building,
+ .tilemap = gBattleTerrainTilemap_Building,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_BuildingLeader,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_MAGMA] =
+ {
+ .tileset = gBattleTerrainTiles_Stadium,
+ .tilemap = gBattleTerrainTilemap_Stadium,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_StadiumMagma,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_AQUA] =
+ {
+ .tileset = gBattleTerrainTiles_Stadium,
+ .tilemap = gBattleTerrainTilemap_Stadium,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_StadiumAqua,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_SIDNEY] =
+ {
+ .tileset = gBattleTerrainTiles_Stadium,
+ .tilemap = gBattleTerrainTilemap_Stadium,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_StadiumSidney,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_PHOEBE] =
+ {
+ .tileset = gBattleTerrainTiles_Stadium,
+ .tilemap = gBattleTerrainTilemap_Stadium,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_StadiumPhoebe,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_GLACIA] =
+ {
+ .tileset = gBattleTerrainTiles_Stadium,
+ .tilemap = gBattleTerrainTilemap_Stadium,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_StadiumGlacia,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_DRAKE] =
+ {
+ .tileset = gBattleTerrainTiles_Stadium,
+ .tilemap = gBattleTerrainTilemap_Stadium,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_StadiumDrake,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_CHAMPION] =
+ {
+ .tileset = gBattleTerrainTiles_Stadium,
+ .tilemap = gBattleTerrainTilemap_Stadium,
+ .entryTileset = gBattleTerrainAnimTiles_Building,
+ .entryTilemap = gBattleTerrainAnimTilemap_Building,
+ .palette = gBattleTerrainPalette_StadiumWallace,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_GROUDON] =
+ {
+ .tileset = gBattleTerrainTiles_Cave,
+ .tilemap = gBattleTerrainTilemap_Cave,
+ .entryTileset = gBattleTerrainAnimTiles_Cave,
+ .entryTilemap = gBattleTerrainAnimTilemap_Cave,
+ .palette = gBattleTerrainPalette_Groudon,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_KYOGRE] =
+ {
+ .tileset = gBattleTerrainTiles_Water,
+ .tilemap = gBattleTerrainTilemap_Water,
+ .entryTileset = gBattleTerrainAnimTiles_Underwater,
+ .entryTilemap = gBattleTerrainAnimTilemap_Underwater,
+ .palette = gBattleTerrainPalette_Kyogre,
+ },
+```
+
+```c
+ [BATTLE_TERRAIN_RAYQUAZA] =
+ {
+ .tileset = gBattleTerrainTiles_Rayquaza,
+ .tilemap = gBattleTerrainTilemap_Rayquaza,
+ .entryTileset = gBattleTerrainAnimTiles_Rayquaza,
+ .entryTilemap = gBattleTerrainAnimTilemap_Rayquaza,
+ .palette = gBattleTerrainPalette_Rayquaza,
+ },
+```
+
+The tedious bit is done. Now we need to start adding in the functions mentioned above but making changes to them so they work for Emerald's scenarios.
+
+Our `sMapBattleSceneMapping` should look like this. I recommend putting this just below the terrain table but it's a matter of preference.
+```c
+static const struct {
+ u8 mapScene;
+ u8 battleTerrain;
+} sMapBattleSceneMapping[] = {
+ {MAP_BATTLE_SCENE_GYM, BATTLE_TERRAIN_GYM},
+ {MAP_BATTLE_SCENE_MAGMA, BATTLE_TERRAIN_MAGMA},
+ {MAP_BATTLE_SCENE_AQUA, BATTLE_TERRAIN_AQUA},
+ {MAP_BATTLE_SCENE_SIDNEY, BATTLE_TERRAIN_SIDNEY},
+ {MAP_BATTLE_SCENE_PHOEBE, BATTLE_TERRAIN_PHOEBE},
+ {MAP_BATTLE_SCENE_GLACIA, BATTLE_TERRAIN_GLACIA},
+ {MAP_BATTLE_SCENE_DRAKE, BATTLE_TERRAIN_DRAKE},
+ {MAP_BATTLE_SCENE_FRONTIER, BATTLE_TERRAIN_FRONTIER}
+};
+```
+
+`GetBattleTerrainByMapScene`,`LoadBattleTerrainGfx` and `LoadBattleTerrainEntryGfx` mentioned above can be used as is. I recommend putting these below `sMapBattleSceneMapping`, but again,
+up to preference.
+
+Now we need to add `GetBattleTerrainOverride` with our changes, this should look like this:
+```c
+static u8 GetBattleTerrainOverride(void)
+{
+ u8 battleScene;
+ if (gBattleTypeFlags & (BATTLE_TYPE_FRONTIER | BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_EREADER_TRAINER))
+ {
+ return BATTLE_TERRAIN_FRONTIER;
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_GROUDON)
+ {
+ return BATTLE_TERRAIN_GROUDON;
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_KYOGRE)
+ {
+ return BATTLE_TERRAIN_KYOGRE;
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_RAYQUAZA)
+ {
+ return BATTLE_TERRAIN_RAYQUAZA;
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_TRAINER)
+ {
+ if (gTrainers[gTrainerBattleOpponent_A].trainerClass == TRAINER_CLASS_LEADER)
+ {
+ return BATTLE_TERRAIN_LEADER;
+ }
+ else if (gTrainers[gTrainerBattleOpponent_A].trainerClass == TRAINER_CLASS_CHAMPION)
+ {
+ return BATTLE_TERRAIN_CHAMPION;
+ }
+ }
+ battleScene = GetCurrentMapBattleScene();
+ if (battleScene == MAP_BATTLE_SCENE_NORMAL)
+ {
+ return gBattleTerrain;
+ }
+ return GetBattleTerrainByMapScene(battleScene);
+}
+```
+I recommend putting this just before `LoadChosenBattleElement` but up to preference.
+
+Don't forget to extern it under `static void CB2_UnusedBattleInit(void);` like so:
+```c
+static void CB2_UnusedBattleInit(void);
+static u8 GetBattleTerrainOverride(void);
+```
+
+We then need to replace the entirety of `DrawMainBattleBackground` with:
+```c
+void DrawMainBattleBackground(void)
+{
+ LoadBattleTerrainGfx(GetBattleTerrainOverride());
+}
+```
+
+We need to look for `DrawBattleEntryBackground` and replace that with:
+```c
+void DrawBattleEntryBackground(void)
+{
+ if (gBattleTypeFlags & BATTLE_TYPE_LINK)
+ {
+ LZDecompressVram(gBattleVSFrame_Gfx, (void*)(BG_CHAR_ADDR(1)));
+ LZDecompressVram(gVsLettersGfx, (void*)OBJ_VRAM0);
+ LoadCompressedPalette(gBattleVSFrame_Pal, 0x60, 0x20);
+ SetBgAttribute(1, BG_ATTR_SCREENSIZE, 1);
+ SetGpuReg(REG_OFFSET_BG1CNT, 0x5C04);
+ CopyToBgTilemapBuffer(1, gBattleVSFrame_Tilemap, 0, 0);
+ CopyToBgTilemapBuffer(2, gBattleVSFrame_Tilemap, 0, 0);
+ CopyBgTilemapBufferToVram(1);
+ CopyBgTilemapBufferToVram(2);
+ SetGpuReg(REG_OFFSET_WININ, WININ_WIN0_BG1 | WININ_WIN0_BG2 | WININ_WIN0_OBJ | WININ_WIN0_CLR);
+ SetGpuReg(REG_OFFSET_WINOUT, WINOUT_WIN01_BG1 | WINOUT_WIN01_BG2 | WINOUT_WIN01_OBJ | WINOUT_WIN01_CLR);
+ gBattle_BG1_Y = 0xFF5C;
+ gBattle_BG2_Y = 0xFF5C;
+ LoadCompressedSpriteSheetUsingHeap(&sVsLettersSpriteSheet);
+ }
+ else if (gBattleTypeFlags & (BATTLE_TYPE_FRONTIER | BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_EREADER_TRAINER))
+ {
+ if (!(gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER) || gPartnerTrainerId == TRAINER_STEVEN_PARTNER)
+ {
+ LoadBattleTerrainEntryGfx(BATTLE_TERRAIN_BUILDING);
+ }
+ else
+ {
+ // Set up bg for the multi battle intro where both teams slide in facing the screen.
+ // Note Steven's multi battle (which has a dedicated back pic) is excluded above.
+ SetBgAttribute(1, BG_ATTR_CHARBASEINDEX, 2);
+ SetBgAttribute(2, BG_ATTR_CHARBASEINDEX, 2);
+ CopyToBgTilemapBuffer(1, gMultiBattleIntroBg_Opponent_Tilemap, 0, 0);
+ CopyToBgTilemapBuffer(2, gMultiBattleIntroBg_Player_Tilemap, 0, 0);
+ CopyBgTilemapBufferToVram(1);
+ CopyBgTilemapBufferToVram(2);
+ }
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_GROUDON)
+ {
+ LoadBattleTerrainEntryGfx(BATTLE_TERRAIN_CAVE);
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_KYOGRE)
+ {
+ LoadBattleTerrainEntryGfx(BATTLE_TERRAIN_UNDERWATER);
+ }
+ else if (gBattleTypeFlags & BATTLE_TYPE_RAYQUAZA)
+ {
+ LoadBattleTerrainEntryGfx(BATTLE_TERRAIN_RAYQUAZA);
+ }
+ else
+ {
+ if (gBattleTypeFlags & BATTLE_TYPE_TRAINER)
+ {
+ u8 trainerClass = gTrainers[gTrainerBattleOpponent_A].trainerClass;
+ if (trainerClass == TRAINER_CLASS_LEADER)
+ {
+ LoadBattleTerrainEntryGfx(BATTLE_TERRAIN_BUILDING);
+ return;
+ }
+ else if (trainerClass == TRAINER_CLASS_CHAMPION)
+ {
+ LoadBattleTerrainEntryGfx(BATTLE_TERRAIN_BUILDING);
+ return;
+ }
+ }
+
+ if (GetCurrentMapBattleScene() == MAP_BATTLE_SCENE_NORMAL)
+ {
+ LoadBattleTerrainEntryGfx(gBattleTerrain);
+ }
+ else
+ {
+ LoadBattleTerrainEntryGfx(BATTLE_TERRAIN_BUILDING);
+ }
+ }
+}
+```
+
+And then finally, we replace `LoadChosenBattleElement` with:
+```c
+bool8 LoadChosenBattleElement(u8 caseId)
+{
+ bool8 ret = FALSE;
+
+ switch (caseId)
+ {
+ case 0:
+ LZDecompressVram(gBattleTextboxTiles, (void*)(BG_CHAR_ADDR(0)));
+ break;
+ case 1:
+ CopyToBgTilemapBuffer(0, gBattleTextboxTilemap, 0, 0);
+ CopyBgTilemapBufferToVram(0);
+ break;
+ case 2:
+ LoadCompressedPalette(gBattleTextboxPalette, 0, 0x40);
+ break;
+ case 3:
+ LZDecompressVram(sBattleTerrainTable[GetBattleTerrainOverride()].tileset, (void *)BG_CHAR_ADDR(2));
+ break;
+ case 4:
+ LZDecompressVram(sBattleTerrainTable[GetBattleTerrainOverride()].tilemap, (void *)BG_SCREEN_ADDR(26));
+ break;
+ case 5:
+ LoadCompressedPalette(sBattleTerrainTable[GetBattleTerrainOverride()].palette, 0x20, 0x60);
+ break;
+ case 6:
+ LoadBattleMenuWindowGfx();
+ break;
+ default:
+ ret = TRUE;
+ break;
+ }
+
+ return ret;
+}
+```
+
+And with that, if I didn't miss anything out, battle terrain should be functionally equivalent to before without the need for big switch statements which can be a pain to work with and just a waste, in my opinion. \ No newline at end of file