diff options
author | Josh <32826900+ShinyDragonHunter@users.noreply.github.com> | 2022-03-04 08:01:48 +0000 |
---|---|---|
committer | Josh <32826900+ShinyDragonHunter@users.noreply.github.com> | 2022-03-04 08:01:48 +0000 |
commit | 2a9892f8cd4e05418f607d6130b65e635b1b8215 (patch) | |
tree | eb24bfe62355255ca8db36e73c092865eeb20b70 | |
parent | 41f974ee60f1f5c677ef6e5992de4c69b25387f2 (diff) |
Created Improve the Loading of Battle Terrain (markdown)
-rw-r--r-- | Improve-the-Loading-of-Battle-Terrain.md | 493 |
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 |