├── GUIDE.md ├── INSTALL.md ├── README.md ├── animation.js ├── animations_pokeemerald.js ├── animations_pokefirered.js ├── animations_pokeruby.js └── settings.js /GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Settings 2 | 3 | After installation, the first thing you should take a look at is the `settings.js` file. Each of the settings should be largely self-explanatory, but they are described below. 4 | 5 | 6 | | Setting | Description | Default value | 7 | ----------|-------------|----------------- 8 | | `toggleShortcut` | The keyboard shortcut for `Toggle Map Animations` | `"Ctrl+A"` | 9 | | `animateOnLaunch` | Whether animations should be running when Porymap is first opened | `true` | 10 | | `tilesetsPath` | The base filepath for all animation images | `"data/tilesets/"` | 11 | | `primaryPath` | The base filepath for animation images in primary tilesets | `tilesetsPath + "primary/"` | 12 | | `secondaryPath` | The base filepath for animation images in secondary tilesets | `tilesetsPath + "secondary/"` | 13 | | `animFileExtension` | The file extension of all animation images* | `".png"` | 14 | | `logPrefix` | All logs produced by the plug-in will use this string as a prefix | `"ANIM: "` | 15 | | `logBasicInfo` | Logs when animations are turned on/off, and when they start/finish loading | `true` | 16 | | `logUsageInfo` | Logs which overlays have been used** | `false` | 17 | | `logDebugInfo` | Logs all the loaded animation data** | `false` | 18 | | `refreshTime` | The time in milliseconds between animation updates | `Math.round(1000 / 59.73)` | 19 | | `mapExceptions` | An array of map names for maps that should never animate | `[""]` | 20 | 21 | \* Animation images having more than one file extension is not currently supported as it seems like an unlikely use case. If your project needs this support please open an issue and it will be added. 22 | 23 | \** Setting `logUsageInfo` and/or `logDebugInfo` to `true` will negatively affect Porymap's performance. 24 | 25 | ### Animation files 26 | Note also the following section: 27 | ``` 28 | // Animation data 29 | import {tilesetsData as em} from './animations_pokeemerald.js'; 30 | import {tilesetsData as frlg} from './animations_pokefirered.js'; 31 | import {tilesetsData as rs} from './animations_pokeruby.js'; 32 | export const versionData = [rs, frlg, em]; 33 | ``` 34 | These are the files containing the animation data. If you would like to use your own file you need only change the filepath. 35 | 36 | By default these files contain animation data for all the vanilla animations in each game version. If you would like to delete the files for the versions you are not using you may replace all 3 filepaths with the same file, or replace the exports with empty objects, e.g. 37 | ``` 38 | import {tilesetsData as em} from './animations_pokeemerald.js'; 39 | export const versionData = [{}, {}, em]; 40 | ``` 41 | Which data set in `versionData` that gets used depends on the value of `base_game_version` in your project's `porymap.project.cfg` file. `pokeruby` will use the first, `pokefirered` the second, and `pokeemerald` the third. 42 | 43 | ## Animation data format 44 | 45 | After customizing settings you may want to add or change tile animations. Navigate to and open the `animations_.js` file your project uses. If you're not sure which of the 3 it uses, see above. 46 | 47 | ### Tileset properties 48 | 49 | Each file contains a single object called `tilesetsData`. Each entry in this object is all the animation data for a given tileset. For example, let's look at the entry for `gTileset_Rustboro` 50 | 51 | ``` 52 | "gTileset_Rustboro": { 53 | folder: "rustboro/anim", 54 | primary: false, 55 | tileAnimations: { 56 | 640: { // (0x280) 57 | folder: "windy_water", 58 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 59 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 60 | numTiles: 4, 61 | interval: 8, 62 | imageWidth: 16, 63 | }, 64 | 960: { // (0x3C0) 65 | folder: "fountain", 66 | frames: ["0", "1"], 67 | numTiles: 4, 68 | interval: 8, 69 | imageWidth: 16, 70 | }, 71 | }, 72 | }, 73 | ``` 74 | - The top property `"gTileset_Rustboro"` is the name of the tileset this data belongs to 75 | - `folder: "rustboro/anim",` defines the base filepath for all animation images in this tileset, in this case `rustboro/anim`. 76 | - `primary: false,` is whether or not this tileset is a primary tileset. This is not required; if it is excluded it will be assumed to be `false`. 77 | - `tileAnimations` defines the object that contains data for each animating tile in this tileset. 78 | 79 | ### Required Animation properties 80 | 81 | Let's look at the second entry in `tileAnimations` 82 | ``` 83 | 960: { // (0x3C0) 84 | folder: "fountain", 85 | frames: ["0", "1"], 86 | numTiles: 4, 87 | interval: 8, 88 | imageWidth: 16, 89 | }, 90 | ``` 91 | - `960` is the tile id this animation starts at. The commented value next to it is just for convenience; you may use either the decimal or hexadecimal tile id values. 92 | - `folder: "fountain",` is the name of the folder containing the animation images, in this case `fountain`. 93 | - `frames: ["0", "1"],` is an array of filenames for the animation images, and the order in which they should appear before looping. If they need to repeat they can be listed again in the array. 94 | - `numTiles: 4,` is the number of tiles this animation spans. This means tiles 960, 961, 962, and 963 are all part of this animation. 95 | - `interval: 8,` is the number of animation updates that will pass before the frame changes. A lower value is a faster animation. 96 | - `imageWidth: 16,` is the width of each image for this animation. 97 | 98 | These are all the basic, required properties to add for any new animation. There are also a few optional properties: 99 | 100 | ### Optional animation properties 101 | - `frameOffsets`: Some tile animations are identical to another tile animation but start at a different time. For example, in `gTileset_Rustboro` the `windy_water` animation is repeated in-game to produce a wave ripple effect. It has the following: 102 | ``` 103 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 104 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 105 | ``` 106 | For each value `n` in `frameOffsets`, the animation will be duplicated and start `n` frames earlier. With the above `frameOffsets` there would be duplicate animations with the following frames: 107 | ``` 108 | frames: ["7", "0", "1", "2", "3", "4", "5", "6"], // Offset 1 109 | frames: ["6", "7", "0", "1", "2", "3", "4", "5"], // Offset 2 110 | frames: ["5", "6", "7", "0", "1", "2", "3", "4"], // Offset 3 111 | frames: ["4", "5", "6", "7", "0", "1", "2", "3"], // Offset 4 112 | frames: ["3", "4", "5", "6", "7", "0", "1", "2"], // Offset 5 113 | frames: ["2", "3", "4", "5", "6", "7", "0", "1"], // Offset 6 114 | frames: ["1", "2", "3", "4", "5", "6", "7", "0"], // Offset 7 115 | ``` 116 | Additionally, it will be assumed that each offset animation has `numTiles` tiles of its own that start immediately after the previous group of tiles. For example the original animation has 4 tiles starting at tile 640, so the animation at the first offset should have 4 tiles starting at tile 644. 117 | 118 | 119 | - `externalFolder`: Normally the `folder` for an animation is assumed to be at the base path of the tileset it belongs to. If you'd like the path to instead be treated as absolute (starting from the project root) you can add `externalFolder: true,` to the animation. For example, in `gTileset_Lavaridge` tile 672 has 120 | ``` 121 | 672: { // (0x2A0) 122 | folder: "data/tilesets/secondary/cave/anim/lava", 123 | externalFolder: true, 124 | frames: ["0", "1", "2", "3"], 125 | numTiles: 4, 126 | interval: 16, 127 | imageWidth: 16, 128 | }, 129 | ``` 130 | The animation images for this tileset will therefore come from `data/tilesets/secondary/cave/anim/lava`, instead of `data/tilesets/secondary/lavaridge/anim/ + folder` 131 | 132 | ## Creating an animation using `tileset_anims.c` 133 | 134 | `src/tileset_anims.c` in your project is where in-game animations are defined. The plug-in will not interact with this file. This is a short explanation of how to derive the above data format from the information in this file. As an example we'll look at the floating log animations in `gTileset_Pacifidlog`. In this plug-in, its data looks like this: 135 | ``` 136 | 976: { // (0x3D0) 137 | folder: "log_bridges", 138 | frames: ["0", "1", "2", "1], 139 | numTiles: 30, 140 | interval: 16, 141 | imageWidth: 16, 142 | }, 143 | ``` 144 | Let's see how the `folder`, `frames`, `numTiles`, `interval`, `imageWidth` and start tile `976` were obtained. 145 | 146 | ### Getting `folder` and `imageWidth` 147 | Navigate to the tileset animation folder for `gTileset_Pacifidlog`, which is `data/tilesets/secondary/pacifidlog/anim`. This is where the `log_bridges` folder is, and the images in this folder have a width of `16` pixels, so we add `folder: "log_bridges",` and `imageWidth: 16,`. It also has 3 images in it named `0.png`, `1.png`, and `2.png`. These are the frames, but first we need to know what order they go in. 148 | 149 | ### Getting `frames` 150 | 151 | Navigate to `src/tileset_anims.c` and find where the above files are included 152 | ```c 153 | const u16 gTilesetAnims_Pacifidlog_LogBridges_Frame0[] = INCBIN_U16("data/tilesets/secondary/pacifidlog/anim/log_bridges/0.4bpp"); 154 | const u16 gTilesetAnims_Pacifidlog_LogBridges_Frame1[] = INCBIN_U16("data/tilesets/secondary/pacifidlog/anim/log_bridges/1.4bpp"); 155 | const u16 gTilesetAnims_Pacifidlog_LogBridges_Frame2[] = INCBIN_U16("data/tilesets/secondary/pacifidlog/anim/log_bridges/2.4bpp"); 156 | ``` 157 | These are the names given to the frames in the project. Next find where they're used 158 | ```c 159 | const u16 *const gTilesetAnims_Pacifidlog_LogBridges[] = { 160 | gTilesetAnims_Pacifidlog_LogBridges_Frame0, 161 | gTilesetAnims_Pacifidlog_LogBridges_Frame1, 162 | gTilesetAnims_Pacifidlog_LogBridges_Frame2, 163 | gTilesetAnims_Pacifidlog_LogBridges_Frame1 164 | }; 165 | ``` 166 | Here we can see the order of the frames for this animation goes 0, 1, 2, 1. Using the filenames we can now add `frames: ["0", "1", "2", "1"],` to the animation. 167 | 168 | ### Getting the start tile id and `numTiles` 169 | Find where the above frame table (`gTilesetAnims_Pacifidlog_LogBridges`) is used. 170 | ```c 171 | static void QueueAnimTiles_Pacifidlog_LogBridges(u8 timer) 172 | { 173 | u8 i = timer % 4; 174 | AppendTilesetAnimToBuffer(gTilesetAnims_Pacifidlog_LogBridges[i], (u16 *)(BG_VRAM + TILE_OFFSET_4BPP(NUM_TILES_IN_PRIMARY + 464)), 0x3C0); 175 | } 176 | ``` 177 | `timer % 4` just ensures that `i` is within bounds of `gTilesetAnims_Pacifidlog_LogBridges`. This is equivalent to `u8 i = timer % ARRAY_COUNT(gTilesetAnims_Pacifidlog_LogBridges);`, it's not relevant for us here. We can get the start tile id from the second argument to `AppendTilesetAnimToBuffer`: 178 | ```c 179 | (u16 *)(BG_VRAM + TILE_OFFSET_4BPP(NUM_TILES_IN_PRIMARY + 464)) 180 | ``` 181 | By finding the definition of `NUM_TILES_IN_PRIMARY` we can see it's `512`, and `512+464` is `976`. This is the start tile for this animation. The last argument to `AppendTilesetAnimToBuffer` is the size to copy over. `0x3C0` is `960` in decimal. Each tile has a size of 32, so `960/32` gives us `numTiles`, which is `30`. Depending on your project this last argument may look like `30 * TILE_SIZE_4BPP` already, so it's even clearer that the number of tiles is `30`. 182 | 183 | 184 | ### Getting `interval` 185 | Find where the above function (`QueueAnimTiles_Pacifidlog_LogBridges`) is used. 186 | ```c 187 | static void TilesetAnim_Pacifidlog(u16 timer) 188 | { 189 | if (timer % 16 == 0) 190 | QueueAnimTiles_Pacifidlog_LogBridges(timer >> 4); 191 | if (timer % 16 == 1) 192 | QueueAnimTiles_Pacifidlog_WaterCurrents(timer >> 4); 193 | } 194 | ``` 195 | `if (timer % 16 == 0)` tells us that `QueueAnimTiles_Pacifidlog_LogBridges` will get called once every 16 updates, which gives us an interval of `16`. And that's it, now we have all the data we need to build this animation in the plug-in. 196 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | This plug-in works with Porymap, which can be [downloaded here](https://github.com/huderlem/porymap/releases). Version 5.2.0 or newer is suggested. Version 5.0.0 is the oldest supported version. 4 | 5 | If you are using Porymap version 4.5.0, you can instead download the [older version of this script](https://github.com/GriffinRichards/Porymap-Animation/releases/tag/v1.0.0). 6 | 7 | You are assumed to already have a Pokémon generation 3 decompilation project set up. If you do not, see the `INSTALL.md` at [pokeemerald](https://github.com/pret/pokeemerald), [pokefirered](https://github.com/pret/pokefirered) or [pokeruby](https://github.com/pret/pokeruby), or the fork of your choice. 8 | There is very little this plug-in needs from your project, and what little it does (filepaths) can be changed in `settings.js` and `animations_.js`, so if your project works with a compatible version of Porymap it should work with this plug-in. 9 | 10 | ## Installation 11 | 12 | 1. Clone this repository to the location of your choice. 13 | ``` 14 | git clone https://github.com/GriffinRichards/Porymap-Animation 15 | ``` 16 | 17 | 2. Launch Porymap and open the `Options -> Custom Scripts...` window. 18 | 19 |
20 | If using a Porymap version older than 5.2.0... 21 | 22 | > You won't have `Options -> Custom Scripts...` available. You'll need to manually specify the path to `animation.js` under `custom_scripts` in `porymap.user.cfg` or `porymap.project.cfg`. 23 | > After specifying this path you can skip the remaining steps. 24 |
25 | 26 | 3. Select the `Load Script` button, then in the file prompt navigate to and select `Porymap-Animation/animation.js`. 27 | 28 | 4. Close the window by selecting `OK`. 29 | 30 | 31 | That's it! You should now see the new option `Toggle Map Animations` available under `Tools`, which you can use to turn the animations on or off. 32 | 33 | For information on creating your own animations or changing the animation settings see [GUIDE.md](https://github.com/GriffinRichards/Porymap-Animation/blob/master/GUIDE.md) 34 | 35 | If you have questions, see [the FAQ](https://github.com/GriffinRichards/Porymap-Animation#faq). 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Porymap Animation 2 | 3 | An animation plug-in for [Porymap][porymap], the Pokémon generation 3 decompilation map editor. 4 | 5 | To start using this plug-in, see [INSTALL.md][install]. 6 | 7 | For help using this plug-in, see [GUIDE.md][guide] or the FAQ below. 8 | 9 | ![Animation preview](https://user-images.githubusercontent.com/25753467/170611564-94576b17-1551-4109-873f-e78494550031.gif) 10 | 11 | ## FAQ 12 | 13 | - [What does this do?](https://github.com/GriffinRichards/Porymap-Animation/blob/master/README.md#what-does-this-do) 14 | - [How do I use this?](https://github.com/GriffinRichards/Porymap-Animation#how-do-i-use-this) 15 | - [Will this modify my maps/tilesets/project in any way?](https://github.com/GriffinRichards/Porymap-Animation#will-this-modify-my-mapstilesetsproject-in-any-way) 16 | - [My animation isn't working, what's wrong?](https://github.com/GriffinRichards/Porymap-Animation#my-animation-isnt-working-whats-wrong) 17 | - [Why isn't this animation I added working?](https://github.com/GriffinRichards/Porymap-Animation#why-isnt-this-animation-i-added-working) 18 | - [Why aren't the border/map connections animating?](https://github.com/GriffinRichards/Porymap-Animation#why-arent-the-bordermap-connections-animating) 19 | - [Are the animations supposed to slow down/stop when I move the mouse rapidly?](https://github.com/GriffinRichards/Porymap-Animation#are-the-animations-supposed-to-slow-downstop-when-i-move-the-mouse-rapidly) 20 | - [Why is this not built in to Porymap?](https://github.com/GriffinRichards/Porymap-Animation#why-is-this-not-built-in-to-porymap) 21 | 22 | ### What does this do? 23 | 24 | This plug-in animates the tiles on maps viewed in Porymap so that you can see what they would look like in-game. 25 | 26 | 27 | ### How do I use this? 28 | 29 | See [INSTALL.md][install] to install the plug-in. 30 | 31 | After installation it will run automatically whenever you open your project with Porymap. You can change the plug-in's settings in `settings.js`. See [GUIDE.md][guide] for how to create your own animations. 32 | 33 | 34 | ### Will this modify my maps/tilesets/project in any way? 35 | 36 | No. This does not write to any files and will not make any changes to your project. It will only read the images specified by your `animations_.js` file and use them to create visual effects in Porymap. These effects can be toggled on or off at any time. 37 | 38 | 39 | ### My animation isn't working, what's wrong? 40 | 41 | See the troubleshooting list below. 42 | 43 | **Note**: If the plug-in encounters an error it will be logged in Porymap's log file, located at `%Appdata%\pret\porymap\porymap.log` for Windows or `~/Library/Application Support/pret/porymap/porymap.log` for macOS. Anything in the log related to this plug-in will have the prefix `ANIM: ` 44 | 45 | 1. Make sure you've followed the instructions in [INSTALL.md][install]. 46 | 2. Open `Options -> Project Settings`, find your entry for `animation.js`, and make sure the check box next to it is checked. If you don't see an entry for `animation.js`, go back to [INSTALL.md][install]! 47 | 3. Make sure `Tools -> Toggle Map Animations` is checked. If you don't see `Tools -> Toggle Map Animations` then the script hasn't loaded! Check Porymap's log file for errors and look over your changes to `settings.js` and `animations_.js` for mistakes. 48 | 4. In Porymap select both the `Map` tab on the top bar, and either the `Metatiles` or `Prefabs` tab in the right panel. Animations run in these tab views only. 49 | 5. Have you added animation data? All tile animations must have data in your `animations_.js` file to animate. The script comes pre-loaded with data for all the vanilla animations, but if you add or change any animations you must update this file accordingly. See [GUIDE.md][guide] for more information. 50 | 6. Remove the map's name from `mapExceptions` in `settings.js` if it has been added there. 51 | 7. If you are still unable to get animations working you can join pret's [Discord](https://discord.gg/d5dubZ3) and ask for help in the `#porymap` channel. 52 | 53 | 54 | ### Why isn't this animation I added working? 55 | 56 | For any new tileset animations you've created you'll need to add animation data to tell the plug-in how to recreate it (the plug-in comes pre-loaded with data for all the animations in the original games). Check the [GUIDE.md][guide] to see how to do this, and make sure your new animation data follows the correct format. If the original game's animations also aren't working see the troubleshooting list above. If you're still stuck you can join pret's [Discord](https://discord.gg/d5dubZ3) and ask for help in the `#porymap` channel. 57 | 58 | ### Why aren't the border/map connections animating? 59 | 60 | Porymap's API doesn't currently support reading tiles from the connecting maps, which is necessary to create animations there. The API does support reading tiles in the current map's border, but animating the border and not connecting maps would look strange. I may decide to add support for this in the future. 61 | 62 | 63 | ### Are the animations supposed to slow down/stop when I move the mouse rapidly? 64 | 65 | Sort of. This is an issue with Porymap, not the plug-in, and has to do with what happens when the mouse enters a new map square. 66 | 67 | 68 | ### Why is this not built in to Porymap? 69 | 70 | There'd be a lot of hassle to do it well. It's the kind of thing that can definitely be done much better as part of Porymap, but is probably best left to personal forks where people can implement it in a way that works for them. A plug-in offers some of this freedom without needing to fork Porymap. 71 | 72 | Also a bit of author preference here; I wanted an excuse to test the limits of Porymap's API and expand what it has to offer. 73 | 74 | 75 | 76 | 77 | [porymap]: https://github.com/huderlem/porymap 78 | [pokeemerald]: https://github.com/pret/pokeemerald 79 | [pokefirered]: https://github.com/pret/pokefirered 80 | [pokeruby]: https://github.com/pret/pokeruby 81 | [install]: https://github.com/GriffinRichards/Porymap-Animation/blob/master/INSTALL.md 82 | [guide]: https://github.com/GriffinRichards/Porymap-Animation/blob/master/GUIDE.md 83 | -------------------------------------------------------------------------------- /animation.js: -------------------------------------------------------------------------------- 1 | /* 2 | SECTION LABELS 3 | = Data = 4 | = Main callbacks = 5 | = Animation running = 6 | = Animation loading = 7 | = Image creation = 8 | = Coordinates = 9 | = Data building = 10 | = Logging = 11 | 12 | */ 13 | 14 | //==================== 15 | // Data 16 | //==================== 17 | 18 | import { 19 | toggleShortcut, 20 | animateOnLaunch, 21 | versionData, 22 | tilesetsPath, 23 | primaryPath, 24 | secondaryPath, 25 | animFileExtension, 26 | logPrefix, 27 | logBasicInfo, 28 | logDebugInfo, 29 | logUsageInfo, 30 | refreshTime, 31 | mapExceptions 32 | } from "./settings.js" 33 | 34 | var root = ""; 35 | var animating = false; 36 | var inAnimatedView = true; 37 | var animateFuncActive = false; 38 | var loadAnimations = true; 39 | var numLayers = 1; 40 | 41 | var mapName; 42 | var mapWidth; 43 | var mapHeight; 44 | 45 | var tilesetsData; 46 | 47 | // 2D array of objects tracking which layers belong to which map spaces. 48 | // It's accessed with x/y map coordinates, e.g. layerRangeMap[x][y], and 49 | // returns an object with a 'start' and 'end' property which are the first 50 | // and max layer used by that map space. This is used to clear the layers 51 | // for a space when it's drawn on. 52 | var layerRangeMap; 53 | 54 | // These objects map layers to the intervals of the animation they're associated with. 55 | // Each property is an interval. For animLayerMap, each value is an array of layer ids; each 56 | // layer is only associated with 1 interval. For staticLayerMap, each value is an object 57 | // containing an array of layer ids and a value for whether or not they are currently hidden. 58 | // Static layers are associated with every interval that exists on the map space they are 59 | // associated with. The array below them is for temporarily tracking which intervals are being 60 | // used in order to build staticLayerMap. 61 | var animLayerMap = {}; 62 | var staticLayerMap = {}; 63 | var curAnimIntervals = []; 64 | 65 | // Object for caching data about how to build an animation for a given metatile so 66 | // that when it's encountered again the animation can be created faster. 67 | // Each metatile id is a property, the value of which is an object that holds the 68 | // animation data. See getMetatileAnimData for the format of this data. 69 | var metatileCache = {}; 70 | 71 | // Object for storing data on all the possible tile animations in the current primary/secondary tilesets. 72 | // Each tile id is a property. The values are objects with the same properties as those in tileAnimations 73 | var curTilesetsAnimData = {}; 74 | 75 | // Object for tracking which animations have been encountered already on the current 76 | // metatile layer so that they can be grouped together on the same layers. 77 | // It takes both the interval and number of frames as properties, and the returned 78 | // value is the layer the first frame belongs to. 79 | var curAnimToLayerMap = {}; 80 | 81 | // Basic tile/metatile size information 82 | const tileWidth = 8; 83 | const tileHeight = 8; 84 | const metatileTileWidth = 2; 85 | const metatileTileHeight = 2; 86 | const tilesPerLayer = metatileTileWidth * metatileTileHeight; 87 | const metatileWidth = tileWidth * metatileTileWidth; 88 | const metatileHeight = tileHeight * metatileTileHeight; 89 | var tilesPerMetatile; 90 | var maxMetatileLayer; 91 | var maxPrimaryTile; 92 | var maxSecondaryTile; 93 | 94 | // For getting least common multiple of animation intervals 95 | // https://stackoverflow.com/questions/47047682 96 | const gcd = (a, b) => a ? gcd(b % a, a) : b; 97 | const lcm = (a, b) => a * b / gcd(a, b); 98 | 99 | // Arbitrary "highly composite" number. Only used when no 100 | // animations are loaded, so its value is mostly irrelevant. 101 | const defaultTimerMax = 55440; 102 | var timer = 0; 103 | var timerMax = defaultTimerMax; 104 | 105 | const versionMap = { 106 | "pokeruby": 0, 107 | "pokefirered": 1, 108 | "pokeemerald": 2, 109 | }; 110 | 111 | 112 | //==================== 113 | // Main callbacks 114 | //==================== 115 | 116 | export function onProjectOpened(projectPath) { 117 | root = projectPath + "/"; 118 | tilesPerMetatile = constants.tiles_per_metatile; 119 | maxMetatileLayer = constants.layers_per_metatile; 120 | maxPrimaryTile = constants.max_primary_tiles; 121 | maxSecondaryTile = maxPrimaryTile + constants.max_secondary_tiles; 122 | updateTabs(utility.getMainTab(), utility.getMapViewTab()); 123 | if (verifyPorymapVersion(5,1,1)) { // registerToggleAction was introduced in 5.1.1 124 | utility.registerToggleAction("toggleAnimation", "Toggle Map Animations", toggleShortcut, animateOnLaunch); 125 | } else { 126 | utility.registerAction("toggleAnimation", "Toggle Map Animations", toggleShortcut); 127 | } 128 | buildTilesetsData(); 129 | if (animateOnLaunch) toggleAnimation(); 130 | } 131 | 132 | export function onMapOpened(newMapName) { 133 | mapName = newMapName; 134 | if (!verifyPorymapVersion(6,0,0)) { 135 | // onLayoutOpened was introduced in 6.0.0, and will handle this behavior instead. 136 | setMapSize(map.getWidth(), map.getHeight()); 137 | } 138 | } 139 | 140 | export function onLayoutOpened(newLayoutName) { 141 | mapName = newLayoutName; 142 | setMapSize(map.getWidth(), map.getHeight()); 143 | } 144 | 145 | export function onMapResized(oldWidth, oldHeight, newWidth, newHeight) { 146 | // Arguments for onMapResized were changed in 6.0.0 147 | if (verifyPorymapVersion(6,0,0)) 148 | return; 149 | setMapSize(newWidth, newHeight); 150 | } 151 | 152 | export function onMapResized(oldWidth, oldHeight, delta) { 153 | // Arguments for onMapResized were changed in 6.0.0 154 | if (!verifyPorymapVersion(6,0,0)) 155 | return; 156 | 157 | setMapSize(oldWidth + delta.left + delta.right, 158 | oldHeight + delta.top + delta.bottom); 159 | 160 | if (delta.left != 0 || delta.top != 0) { 161 | onMapShifted(delta.left, delta.top); 162 | } 163 | } 164 | 165 | function setMapSize(w, h) { 166 | overlay.clear(); 167 | mapWidth = w; 168 | mapHeight = h; 169 | loadAnimations = true; 170 | } 171 | 172 | export function onMapShifted(xDelta, yDelta) { 173 | if (xDelta == 0 && yDelta == 0) return; 174 | 175 | // Move and wrap the layers and reconstruct layerRangeMap 176 | let newMap = {}; 177 | for (let x = 0; x < mapWidth; x++) { 178 | if (!newMap[x]) newMap[x] = {}; 179 | for (let y = 0; y < mapHeight; y++) { 180 | if (!newMap[x][y]) newMap[x][y] = {start: -1, end: -1}; 181 | let layerStart = layerRangeMap[x][y].start; 182 | if (layerStart == -1) continue; 183 | let newX = getWrappedMapCoord(x + xDelta, mapWidth); 184 | let newY = getWrappedMapCoord(y + yDelta, mapHeight); 185 | let layerEnd = layerRangeMap[x][y].end; 186 | for (let i = layerStart; i < layerEnd; i++) 187 | setLayerMapPos(newX, newY, i); 188 | if (!newMap[newX]) newMap[newX] = {} 189 | newMap[newX][newY] = {start: layerStart, end: layerEnd}; 190 | } 191 | } 192 | layerRangeMap = newMap; 193 | } 194 | 195 | export function onTilesetUpdated(tilesetName) { 196 | overlay.clear(); 197 | loadAnimations = true; 198 | } 199 | 200 | function updateTabs(mainTab, mapViewTab) { 201 | // Animations only run on the Map tab, and Metatiles or Prefab sub-tab 202 | inAnimatedView = (mainTab == 0) && (mapViewTab == 0 || mapViewTab == 2); 203 | } 204 | 205 | export function onMainTabChanged(oldTab, newTab) { 206 | updateTabs(newTab, utility.getMapViewTab()); 207 | tryStartAnimation(); 208 | } 209 | 210 | export function onMapViewTabChanged(oldTab, newTab) { 211 | updateTabs(0, newTab); // Main tab assumed to be map tab 212 | tryStartAnimation(); 213 | } 214 | 215 | export function onBlockChanged(x, y, prevBlock, newBlock) { 216 | if (newBlock.metatileId == prevBlock.metatileId) 217 | return; 218 | tryRemoveAnimation(x, y); 219 | tryAddAnimation(x, y); 220 | } 221 | 222 | 223 | //===================== 224 | // Animation running 225 | //===================== 226 | 227 | //------------------------------------------------------------------------------- 228 | // This is the main animation loop. It's initially called by tryStartAnimation, 229 | // and it will call itself at a regular interval via setTimeout. Other functions 230 | // can interact with the animation loop by setting 'animating' to false to stop 231 | // animation or 'loadAnimations' to true to reload animation data. 232 | //------------------------------------------------------------------------------- 233 | export function animate() { 234 | if (!shouldAnimate()) { 235 | // Stop animation 236 | animateFuncActive = false; 237 | hideOverlay(); 238 | return; 239 | } 240 | if (loadAnimations) { 241 | resetAnimation(); 242 | loadMapAnimations(); 243 | timerMax = calculateTimerMax(); 244 | if (logDebugInfo) log("Timer max: " + timerMax); 245 | if (logUsageInfo) { 246 | log("Layers used: " + (numLayers - 1)); 247 | debug_printLayers(); 248 | } 249 | } 250 | updateOverlay(timer); 251 | if (++timer >= timerMax) 252 | timer = 0; 253 | utility.setTimeout(animate, refreshTime); 254 | } 255 | 256 | export function toggleAnimation() { 257 | animating = !animating; 258 | if (logBasicInfo) log("Animations " + (animating ? "on" : "off")); 259 | tryStartAnimation(); 260 | } 261 | 262 | function shouldAnimate() { 263 | return animating && inAnimatedView; 264 | } 265 | 266 | function tryStartAnimation() { 267 | if (!shouldAnimate()) return; 268 | 269 | // Only call animation loop if it's not already running. 270 | if (!animateFuncActive) { 271 | animateFuncActive = true; 272 | timer = 0; 273 | animate(); 274 | } 275 | } 276 | 277 | function hideOverlay() { 278 | overlay.hide(); 279 | for (const interval in staticLayerMap) 280 | staticLayerMap[interval].hidden = true; 281 | } 282 | 283 | function resetAnimation() { 284 | overlay.clear(); 285 | numLayers = 1; 286 | timer = 0; 287 | animLayerMap = {}; 288 | staticLayerMap = {}; 289 | metatileCache = {}; 290 | } 291 | 292 | //-------------------------------------------------------------------- 293 | // This function is responsible for visually updating the animation. 294 | // It does this by selectively hiding and showing layers that each 295 | // have different tile frame images on them. 296 | //-------------------------------------------------------------------- 297 | function updateOverlay(timer) { 298 | // For each timing interval of the current animations 299 | for (const interval in animLayerMap) { 300 | if (timer % interval == 0) { 301 | // For each tile animating at this interval, 302 | // hide the previous frame and show the next frame 303 | let layerLists = animLayerMap[interval]; 304 | if (!layerLists) continue; 305 | for (let i = 0; i < layerLists.length; i++) { 306 | let layerList = layerLists[i]; 307 | let curFrame = (timer / interval) % layerList.length; 308 | let prevFrame = curFrame ? curFrame - 1 : layerList.length - 1; 309 | overlay.hide(layerList[prevFrame]); 310 | overlay.show(layerList[curFrame]); 311 | } 312 | 313 | // Show all the unrevealed static layers associated 314 | // with animations at this interval 315 | if (staticLayerMap[interval] && staticLayerMap[interval].hidden) { 316 | for (let i = 0; i < staticLayerMap[interval].layers.length; i++) 317 | overlay.show(staticLayerMap[interval].layers[i]) 318 | staticLayerMap[interval].hidden = false; 319 | } 320 | } 321 | } 322 | } 323 | 324 | //---------------------------------------------------------------------------- 325 | // Timer max is the least common multiple of the animation interval * the 326 | // number of frames for each animation in the currently loaded tilesets. 327 | //---------------------------------------------------------------------------- 328 | function calculateTimerMax() { 329 | let fullIntervals = []; 330 | for (const tileId in curTilesetsAnimData) { 331 | let anim = curTilesetsAnimData[tileId]; 332 | let fullInterval = anim.frames.length * anim.interval; 333 | if (!fullIntervals.includes(fullInterval)) 334 | fullIntervals.push(fullInterval); 335 | } 336 | if (fullIntervals.length == 0) 337 | return defaultTimerMax; 338 | return fullIntervals.reduce(lcm); 339 | } 340 | 341 | 342 | //===================== 343 | // Animation loading 344 | //===================== 345 | 346 | //------------------------------------------------------------------- 347 | // This is the main animation loading function. 348 | // It retrieves the animation data for the current tilesets, 349 | // then scans the map and tries to add an animation at each space. 350 | //------------------------------------------------------------------- 351 | function loadMapAnimations() { 352 | loadAnimations = false; 353 | 354 | // Initialize layerRangeMap 355 | layerRangeMap = {}; 356 | for (let x = 0; x < mapWidth; x++) { 357 | layerRangeMap[x] = {}; 358 | for (let y = 0; y < mapHeight; y++) { 359 | layerRangeMap[x][y] = {start: -1, end: -1}; 360 | } 361 | } 362 | 363 | curTilesetsAnimData = getCurrentTileAnimationData(); 364 | if (curTilesetsAnimData == undefined) { 365 | return; 366 | } 367 | debug_printAnimData(curTilesetsAnimData); 368 | 369 | for (let x = 0; x < mapWidth; x++) 370 | for (let y = 0; y < mapHeight; y++) { 371 | tryAddAnimation(x, y); 372 | } 373 | } 374 | 375 | //------------------------------------------------------------------ 376 | // Returns the tile animations present in the current tilesets. 377 | // If neither tileset has animation data or if the current map is 378 | // in the list of map exceptions it will return undefined. 379 | //------------------------------------------------------------------ 380 | function getCurrentTileAnimationData() { 381 | let p_TilesetData = tilesetsData[map.getPrimaryTileset()]; 382 | let s_TilesetData = tilesetsData[map.getSecondaryTileset()]; 383 | 384 | if ((p_TilesetData == undefined && s_TilesetData == undefined) || mapExceptions.includes(mapName)) 385 | return undefined; 386 | if (s_TilesetData == undefined) 387 | return p_TilesetData.tileAnimations; 388 | if (p_TilesetData == undefined) 389 | return s_TilesetData.tileAnimations; 390 | 391 | // Both tilesets have data, combine them 392 | return Object.assign(s_TilesetData.tileAnimations, p_TilesetData.tileAnimations); 393 | } 394 | 395 | //---------------------------------------------------------------------------- 396 | // Removes the animation (if it exists) at the given map coordinates. 397 | // Layers are not re-used unless animations are fully reloaded, so this 398 | // doesn't bother to remove layers from the layer maps. Over a very long 399 | // period this could impact performance, but it keeps drawing speed high. 400 | //---------------------------------------------------------------------------- 401 | function tryRemoveAnimation(x, y) { 402 | if (layerRangeMap[x][y].start != -1) { 403 | for (let i = layerRangeMap[x][y].start; i < layerRangeMap[x][y].end; i++) 404 | overlay.clear(i); 405 | } 406 | } 407 | 408 | //----------------------------------------------------------------- 409 | // Tries to create a new animation at the given map coordinates. 410 | // If the metatile at this position has not been encountered yet, 411 | // examine it to determine if and how it animates, then cache the 412 | // result. If it should animate, add the images and save which 413 | // layers were used. 414 | //----------------------------------------------------------------- 415 | function tryAddAnimation(x, y) { 416 | let curStaticLayers = []; 417 | let metatileId = map.getMetatileId(x, y); 418 | let metatileData = metatileCache[metatileId]; 419 | 420 | // If we haven't encountered this metatile yet try to build an animation for it. 421 | if (metatileData == undefined) 422 | metatileData = metatileCache[metatileId] = getMetatileAnimData(metatileId); 423 | 424 | // Stop if the metatile has no animating tiles 425 | if (metatileData.length == 0) return; 426 | 427 | let tiles = metatileData.tiles; 428 | let len = metatileData.length; 429 | 430 | // Save starting layer for this map space 431 | layerRangeMap[x][y].start = numLayers; 432 | curAnimIntervals = []; 433 | 434 | // Add tile images. 435 | // metatileData is sorted first by layer, then by whether the tile is static or animated. 436 | // Most of the way this is laid out is to simplify tracking layers for allowing as many 437 | // images as possible to be grouped together on the same layers. 438 | let i = 0; 439 | let layer = -1; 440 | while (i < len) { 441 | // Draw static tiles on a shared layer until we hit an animated tile or the end of the array 442 | let newStaticLayer = false; 443 | while(metatileData[i] && !metatileData[i].animates) { 444 | addStaticTileImage(metatileData[i]); 445 | newStaticLayer = true; 446 | i++; 447 | } 448 | // Added static tile images, save and increment layers 449 | if (newStaticLayer) { 450 | setLayerMapPos(x, y, numLayers); 451 | curStaticLayers.push(numLayers); 452 | numLayers++; 453 | } 454 | 455 | // Draw animated tiles until we hit a static tile or the end of the array. 456 | // Layer usage is handled already by addAnimTileFrames / curAnimToLayerMap 457 | while (metatileData[i] && metatileData[i].animates) { 458 | // Reset cache between layers 459 | if (metatileData[i].layer != layer) { 460 | curAnimToLayerMap = {}; 461 | layer = metatileData[i].layer; 462 | } 463 | addAnimTileFrames(x, y, metatileData[i]); 464 | i++; 465 | } 466 | } 467 | 468 | // Save static layers to array for each animation interval this metatile has. 469 | // Whichever interval occurs next will reveal the static layers. 470 | // This is done so the neither the animated or static layers are ever revealed 471 | // without the other, which could result in visual mistakes like flickering. 472 | if (curStaticLayers.length != 0) { 473 | for (let i = 0; i < curAnimIntervals.length; i++) { 474 | let interval = curAnimIntervals[i]; 475 | if (staticLayerMap[interval] == undefined) 476 | staticLayerMap[interval] = {hidden: true, layers: []}; 477 | staticLayerMap[interval].layers = staticLayerMap[interval].layers.concat(curStaticLayers); 478 | staticLayerMap[interval].hidden = true; 479 | } 480 | } 481 | 482 | // Save end of layer range for this map space 483 | layerRangeMap[x][y].end = numLayers; 484 | if (logUsageInfo) log("Using layers " + layerRangeMap[x][y].start + "-" + (layerRangeMap[x][y].end - 1) + " at " + x + "," + y); 485 | } 486 | 487 | //------------------------------------------------------------------------ 488 | // Examines the specified metatile and returns an array of objects, 489 | // each object containing data about how to draw one of the images 490 | // for this metatile. For static tiles, this is the tile and tile 491 | // position. For animated tiles, this is the tile, tile position, layer, 492 | // image width and height, and the pixel offset into the image data. 493 | // If this metatile has no animating tiles it returns an empty array. 494 | //------------------------------------------------------------------------ 495 | function getMetatileAnimData(metatileId) { 496 | let metatileData = []; 497 | let tiles = map.getMetatileTiles(metatileId); 498 | if (!tiles) return metatileData; 499 | if (logDebugInfo) log("Scanning " + metatileId); 500 | let positions = scanTiles(tiles); 501 | 502 | // No animating tiles, end early 503 | if (positions.anim.length == 0) return metatileData; 504 | debug_printObject(positions); 505 | 506 | let dimensions = getTileImageDimensions(tiles); 507 | 508 | // Merge static and animated tile arrays into one object array 509 | // sorted first by layer, then by static vs animated tiles. 510 | positions.static.sort((a, b) => b - a); 511 | positions.anim.sort((a, b) => b - a); 512 | for (let layer = 0; layer < maxMetatileLayer; layer++) { 513 | while (positions.static.length && Math.floor(positions.static.slice(-1) / tilesPerLayer) == layer) { 514 | // Assemble data entry for static tile 515 | let tilePos = positions.static.pop(); 516 | metatileData.push({animates: false, pos: tilePos, tile: tiles[tilePos]}); 517 | } 518 | while (positions.anim.length && Math.floor(positions.anim.slice(-1) / tilesPerLayer) == layer) { 519 | // Assemble data entry for animated tile 520 | let tilePos = positions.anim.pop(); 521 | let dim = dimensions[tilePos]; 522 | if (!dim) continue; 523 | metatileData.push({animates: true, pos: tilePos, layer: layer, tile: tiles[tilePos], w: dim.w, h: dim.h, xOffset: dim.xOffset, yOffset: dim.yOffset}); 524 | } 525 | } 526 | debug_printObjectArr(metatileData); 527 | return metatileData; 528 | } 529 | 530 | //----------------------------------------------------- 531 | // Reads the given tile array and returns an object 532 | // containing the positions of its animated tiles and 533 | // the positions of any static tiles layered above or 534 | // below the animated tiles. 535 | //----------------------------------------------------- 536 | function scanTiles(tiles) { 537 | // Scan metatile for animating tiles 538 | let animTilePositions = []; 539 | let staticTilePositions = []; 540 | let savedColumns = []; 541 | for (let i = 0; i < tilesPerMetatile; i++) { 542 | let layerPos = i % tilesPerLayer; 543 | if (!savedColumns.includes(layerPos) && isAnimated(tiles[i].tileId)) { 544 | // Animating tile found, save all tiles in this column 545 | for (let j = layerPos; j < tilesPerMetatile; j += tilesPerLayer) { 546 | if (!tiles[j].tileId) continue; 547 | if (i == j || isAnimated(tiles[j].tileId)) 548 | animTilePositions.push(j); // Save animating tile 549 | else 550 | staticTilePositions.push(j); // Save static tile 551 | } 552 | savedColumns.push(layerPos); 553 | } 554 | } 555 | let positions = {static: staticTilePositions, anim: animTilePositions}; 556 | return positions; 557 | } 558 | 559 | function isAnimated(tileId) { 560 | return curTilesetsAnimData != undefined && curTilesetsAnimData[tileId] != undefined; 561 | } 562 | 563 | 564 | //================== 565 | // Image creation 566 | //================== 567 | 568 | //------------------------------------------------------------------ 569 | // Creates the images for each frame of an animated tile at the 570 | // given position. Most of its job is determining (and saving) which 571 | // layers to use for the images, and it passes the actual image 572 | // creation off to addAnimTileImage. 573 | //------------------------------------------------------------------ 574 | function addAnimTileFrames(x, y, data) { 575 | let tileId = data.tile.tileId; 576 | let frames = curTilesetsAnimData[tileId].frames; 577 | let interval = curTilesetsAnimData[tileId].interval; 578 | 579 | // Get which layer to start creating the frame images on. 580 | // If there is already a set of images on this layer that share 581 | // an interval and number of frames, just use the same layers. 582 | if (!curAnimToLayerMap[interval]) curAnimToLayerMap[interval] = {}; 583 | let baseLayerId = curAnimToLayerMap[interval][frames.length]; 584 | let newLayerSet = (baseLayerId == undefined); 585 | 586 | // If it's a new interval+frame count, start the layer usage at the next available layer (and save to cache) 587 | if (newLayerSet) baseLayerId = curAnimToLayerMap[interval][frames.length] = numLayers; 588 | 589 | // Add frame images for this tile 590 | // NOTE: Nearly all of the animation load time comes from this loop, primarily 591 | // the calls to overlay.createImage in addAnimTileImage. The optimization for repeated 592 | // frames (only creating each frame image once) is almost a wash, because very few 593 | // animations have repeat frames, so the overhead slows down loading on many maps. 594 | // Maps that do have animations with repeat frames however (Route 117 especially) 595 | // benefit significantly. 596 | let layers = []; 597 | let frameLayerMap = {}; 598 | for (let i = 0; i < frames.length; i++) { 599 | // Get layer to use for this frame. Repeated frames will share a layer/image 600 | let layerId = frameLayerMap[frames[i]]; 601 | let newFrame = (layerId == undefined); 602 | if (newFrame) layerId = baseLayerId++; 603 | 604 | // If this a new set of layers, save them to an array so they can be tracked for animation. 605 | // Also hide the layer; animated frame images are hidden until their frame is active 606 | if (newLayerSet) { 607 | layers.push(layerId); 608 | if (newFrame) { 609 | overlay.hide(layerId); 610 | setLayerMapPos(x, y, layerId); 611 | } 612 | } 613 | 614 | // Create new frame image 615 | if (newFrame) { 616 | addAnimTileImage(data, i, layerId); 617 | frameLayerMap[frames[i]] = layerId; 618 | } 619 | } 620 | 621 | if (!newLayerSet) return; 622 | 623 | // Update layer usage 624 | numLayers = baseLayerId; 625 | 626 | // Add layers/interval to animation map 627 | if (animLayerMap[interval] == undefined) 628 | animLayerMap[interval] = []; 629 | animLayerMap[interval].push(layers); 630 | if (!curAnimIntervals.includes(interval)) 631 | curAnimIntervals.push(interval); 632 | } 633 | 634 | //------------------------------------------------------------------- 635 | // Create an image for one frame of an animated tile (or tile group) 636 | //------------------------------------------------------------------- 637 | function addAnimTileImage(data, frame, layerId) { 638 | let tile = data.tile; 639 | let filepath = curTilesetsAnimData[tile.tileId].filepaths[frame]; 640 | let hScale = tile.xflip ? -1 : 1; 641 | let vScale = tile.yflip ? -1 : 1; 642 | overlay.createImage(x_posToScreen(data.pos), y_posToScreen(data.pos), filepath, data.w, data.h, data.xOffset, data.yOffset, hScale, vScale, tile.palette, true, layerId); 643 | } 644 | 645 | //-------------------------------------------------- 646 | // Create an image for one frame of a static tile 647 | //-------------------------------------------------- 648 | function addStaticTileImage(data) { 649 | let tile = data.tile; 650 | overlay.hide(numLayers); 651 | overlay.addTileImage(x_posToScreen(data.pos), y_posToScreen(data.pos), tile.tileId, tile.xflip, tile.yflip, tile.palette, true, numLayers); 652 | } 653 | 654 | //---------------------------------------------------------------- 655 | // Calculate the region of the image each tile should load from. 656 | //---------------------------------------------------------------- 657 | function getTileImageDimensions(tiles) { 658 | let dimensions = []; 659 | for (let layer = 0; layer < maxMetatileLayer; layer++) { 660 | let posOffset = layer * tilesPerLayer; 661 | let posData = {}; 662 | 663 | // Calculate x/y offset and set default dimensions for each animated tile 664 | for (let i = 0; i < tilesPerLayer; i++) { 665 | let tilePos = i + posOffset; 666 | let tile = tiles[tilePos]; 667 | if (!isAnimated(tile.tileId)) continue; 668 | let anim = curTilesetsAnimData[tile.tileId]; 669 | posData[i] = {x: getImageDataX(anim), y: getImageDataY(anim), filepath: anim.filepath, tile: tile}; 670 | dimensions[tilePos] = {w: tileWidth, h: tileHeight, xOffset: posData[i].x, yOffset: posData[i].y}; 671 | } 672 | 673 | // Adjacent sequential positions from the same animations can share an image. 674 | // Determine which positions (if any) can, update their dimensions, and stop tracking old positions 675 | let hasRow1, hasRow2; 676 | if (hasRow1 = canCombine_Horizontal(posData, 0, 1)) { 677 | // Merge positions 0 and 1 into a single wide position 678 | dimensions[0 + posOffset].w = tileWidth * 2; 679 | if (posData[0].tile.xflip) { 680 | dimensions[0 + posOffset].xOffset = dimensions[1 + posOffset].xOffset; 681 | } 682 | dimensions[1 + posOffset] = undefined; 683 | posData[1] = undefined; 684 | } 685 | if (hasRow2 = canCombine_Horizontal(posData, 2, 3)) { 686 | // Merge positions 2 and 3 into a single wide position; 687 | dimensions[2 + posOffset].w = tileWidth * 2; 688 | if (posData[2].tile.xflip) { 689 | dimensions[2 + posOffset].xOffset = dimensions[3 + posOffset].xOffset; 690 | } 691 | dimensions[3 + posOffset] = undefined; 692 | posData[3] = undefined; 693 | } 694 | 695 | // Only 1 horizontal image created, can't combine vertically 696 | if (hasRow1 != hasRow2) continue; 697 | 698 | if (canCombine_Vertical(posData, 0, 2)) { 699 | // Merge positions 0 and 2 into a single tall position 700 | // If 0 and 2 were already wide positions this creates a square 701 | dimensions[0 + posOffset].h = tileHeight * 2; 702 | if (posData[0].tile.yflip) { 703 | dimensions[0 + posOffset].yOffset = dimensions[2 + posOffset].yOffset; 704 | } 705 | dimensions[2 + posOffset] = undefined; 706 | posData[2] = undefined; 707 | } 708 | if (canCombine_Vertical(posData, 1, 3)) { 709 | // Merge positions 1 and 3 into a single tall position 710 | dimensions[1 + posOffset].h = tileHeight * 2; 711 | if (posData[1].tile.yflip) { 712 | dimensions[1 + posOffset].yOffset = dimensions[3 + posOffset].yOffset; 713 | } 714 | dimensions[3 + posOffset] = undefined; 715 | posData[3] = undefined; 716 | } 717 | } 718 | return dimensions; 719 | } 720 | 721 | //--------------------------------------------------------------------------------------------- 722 | // Calculate the pixel coordinates to start loading image data from for the given animation 723 | //--------------------------------------------------------------------------------------------- 724 | function getImageDataX(anim) { return (anim.index * tileWidth) % anim.imageWidth; }; 725 | function getImageDataY(anim) { return Math.floor(anim.index * tileWidth / anim.imageWidth) * tileHeight; } 726 | 727 | //------------------------------------------------------------------------------------ 728 | // Determine whether or not the tiles at two different positions can share an image 729 | //------------------------------------------------------------------------------------ 730 | function canCombine(data, a, b) { 731 | return (data[a] && data[b] 732 | && data[a].filepath == data[b].filepath 733 | && data[a].tile.xflip == data[b].tile.xflip 734 | && data[a].tile.yflip == data[b].tile.yflip 735 | && data[a].tile.palette == data[b].tile.palette); 736 | } 737 | function canCombine_Horizontal(data, a, b) { 738 | if (!canCombine(data, a, b) || data[a].y != data[b].y) 739 | return false; 740 | 741 | let left = data[a].tile.xflip ? b : a; 742 | let right = data[a].tile.xflip ? a : b; 743 | return data[left].x == (data[right].x - tileWidth); 744 | } 745 | function canCombine_Vertical(data, a, b) { 746 | if (!canCombine(data, a, b) || data[a].x != data[b].x) 747 | return false; 748 | 749 | let top = data[a].tile.yflip ? b : a; 750 | let bottom = data[a].tile.yflip ? a : b; 751 | return data[top].y == (data[bottom].y - tileHeight); 752 | } 753 | 754 | 755 | //================== 756 | // Coordinates 757 | //================== 758 | 759 | //------------------------------------------------------------------------ 760 | // The below functions all deal with coordinate conversions. 761 | // - mapToScreen takes a map coordinate and returns a pixel coordinate. 762 | // - posToScreen takes a tile position and returns a pixel offset. 763 | // - setLayerMapPos takes map coordinates and a layer id and sets 764 | // the pixel coordinates of that layer. 765 | // - getWrappedMapCoord takes a map coordinate and a max width or height 766 | // and returns a bounded map coordinate. 767 | //------------------------------------------------------------------------ 768 | function x_mapToScreen(x) { return x * metatileWidth; } 769 | function y_mapToScreen(y) { return y * metatileHeight; } 770 | function x_posToScreen(tilePos) { return (tilePos % metatileTileWidth) * tileWidth; } 771 | function y_posToScreen(tilePos) { return Math.floor((tilePos % tilesPerLayer) / metatileTileWidth) * tileHeight; } 772 | function setLayerMapPos(x, y, layerId) { overlay.setPosition(x_mapToScreen(x), y_mapToScreen(y), layerId); } 773 | function getWrappedMapCoord(coord, max) { return ((coord >= 0) ? coord : (Math.abs(max - Math.abs(coord)))) % max; } 774 | 775 | 776 | //================== 777 | // Data building 778 | //================== 779 | 780 | //----------------------------------------------------------------------------- 781 | // Retrieve the user's animation data based on their project version, then 782 | // populate it. There are properties expected by the program that are not 783 | // written out in the data because they can be calculated, so this saves 784 | // the user manual entry (for instance, copying animation data for each tile, 785 | // or constructing the full filepath of each image). 786 | //----------------------------------------------------------------------------- 787 | function buildTilesetsData() { 788 | tilesetsData = JSON.parse(JSON.stringify(versionData[versionMap[constants.base_game_version]])); 789 | // For each tileset 790 | for (const tilesetName in tilesetsData) { 791 | if (!verifyTilesetData(tilesetName)) continue; 792 | 793 | let basePath = root + (tilesetsData[tilesetName].primary ? primaryPath : secondaryPath); 794 | let tilesetPath = basePath + tilesetsData[tilesetName].folder + "/"; 795 | let anims = tilesetsData[tilesetName].tileAnimations; 796 | let tileIds = Object.keys(anims); 797 | if (tileIds.length == 0) { 798 | // No animations, delete it 799 | warn(tilesetName + " has a header but no tile animations."); 800 | delete tilesetsData[tilesetName]; 801 | continue; 802 | } 803 | 804 | // For each animation start tile 805 | for (let i = 0; i < tileIds.length; i++) { 806 | let tileId = tileIds[i]; 807 | if (!verifyTileAnimData(tileId, tilesetName)) continue; 808 | let anim = anims[tileId]; 809 | 810 | // Set filepaths for animation frames 811 | anim.filepaths = []; 812 | let numFrames = anim.frames.length; 813 | let animPath = (anim.externalFolder ? root : tilesetPath) + anim.folder + "/"; 814 | for (let frame = 0; frame < numFrames; frame++) 815 | anim.filepaths[frame] = animPath + anim.frames[frame] + animFileExtension; 816 | anim.filepath = animPath; 817 | 818 | // Copy first tile animation for the remaining tiles 819 | let tileIdInt = parseInt(tileId); 820 | for (let j = 1; j < anim.numTiles; j++) { 821 | let nextTileId = tileIdInt + j; 822 | if (!verifyAnimCopy(anims, nextTileId, tileId, tilesetName)) break; 823 | anims[nextTileId] = Object.assign({}, anim); 824 | anims[nextTileId].index = j; 825 | } 826 | anim.index = 0; 827 | 828 | // Create copies of animation tiles with offset frame timings (if any). 829 | if (!anim.frameOffsets) continue; 830 | for (let j = 0; j < anim.frameOffsets.length; j++) { 831 | let offset = Math.abs(numFrames - anim.frameOffsets[j]); 832 | 833 | // Shift frames for offset copy (only shifting the filepath really matters) 834 | let copyFrames = []; 835 | let copyFilepaths = []; 836 | for (let frame = 0; frame < numFrames; frame++) { 837 | let shiftedFrame = (frame + offset) % numFrames; 838 | copyFrames[frame] = anim.frames[shiftedFrame]; 839 | copyFilepaths[frame] = anim.filepaths[shiftedFrame]; 840 | } 841 | 842 | // Write animation for each tile of this offset copy 843 | let copyTileIdInt = tileIdInt + anim.numTiles * (j + 1); 844 | for (let k = 0; k < anim.numTiles; k++) { 845 | let nextTileId = copyTileIdInt + k; 846 | if (!verifyAnimCopy(anims, nextTileId, copyTileIdInt, tilesetName)) break; 847 | anims[nextTileId] = Object.assign({}, anim); 848 | anims[nextTileId].index = k; 849 | anims[nextTileId].frames = copyFrames; 850 | anims[nextTileId].filepaths = copyFilepaths; 851 | } 852 | } 853 | } 854 | } 855 | } 856 | 857 | //----------------------------------------------------------------------- 858 | // Verify that the specified tileset data has the required properties. 859 | // If it's empty return false. If it's missing properties delete it and 860 | // return false. Otherwise return true. 861 | //----------------------------------------------------------------------- 862 | function verifyTilesetData(tilesetName) { 863 | let tilesetData = tilesetsData[tilesetName]; 864 | if (tilesetData == undefined) 865 | return false; // A tileset missing a header is invalid but not an error 866 | 867 | let valid = true; 868 | let reqProperties = ["tileAnimations", "folder"]; 869 | let reqPropertyErrors = [verifyObject, verifyString]; 870 | for (let i = 0; i < reqProperties.length; i++) { 871 | if (!tilesetData.hasOwnProperty(reqProperties[i])) { 872 | error(tilesetName + " is missing property '" + reqProperties[i] + "'"); 873 | valid = false; 874 | } else { 875 | let errorMsg = reqPropertyErrors[i](tilesetData[reqProperties[i]]); 876 | if (errorMsg) { 877 | error(tilesetName + " has invalid property '" + reqProperties[i] + "': " + errorMsg); 878 | valid = false; 879 | } 880 | } 881 | } 882 | let optProperties = ["primary"]; 883 | let optPropertyErrors = [verifyBool]; 884 | for (let i = 0; i < optProperties.length; i++) { 885 | if (tilesetData.hasOwnProperty(optProperties[i])) { 886 | let errorMsg = optPropertyErrors[i](tilesetData[optProperties[i]]); 887 | if (errorMsg) { 888 | error(tilesetName + " has invalid property '" + optProperties[i] + "': " + errorMsg); 889 | valid = false; 890 | } 891 | } 892 | } 893 | if (!valid) 894 | delete tilesetsData[tilesetName]; 895 | return valid; 896 | } 897 | 898 | //--------------------------------------------------------------------------- 899 | // Verify that the specified tile animation is valid. If it's empty return 900 | // false. If it's missing properties or it exceeds the total tile limit 901 | // then delete it and return false. Otherwise return true. 902 | //--------------------------------------------------------------------------- 903 | function verifyTileAnimData(tileId, tilesetName) { 904 | // Assumes tileset data has already been verified 905 | let anim = tilesetsData[tilesetName].tileAnimations[tileId]; 906 | if (anim == undefined) 907 | return false; // A missing tile animation is invalid but not an error 908 | 909 | let valid = true; 910 | 911 | if (!verifyTileLimit(tileId, tilesetName)) 912 | valid = false; 913 | 914 | let reqProperties = ["numTiles", "frames", "interval", "folder", "imageWidth"]; 915 | let reqPropertyErrors = [verifyPositive, verifyArray, verifyPositive, verifyString, verifyPositive]; 916 | for (let i = 0; i < reqProperties.length; i++) { 917 | if (!anim.hasOwnProperty(reqProperties[i])) { 918 | error("Animation for tile " + tileId + " of " + tilesetName + " is missing property '" + reqProperties[i] + "'"); 919 | valid = false; 920 | } else { 921 | let errorMsg = reqPropertyErrors[i](anim[reqProperties[i]]); 922 | if (errorMsg) { 923 | error("Animation for tile " + tileId + " of " + tilesetName + " has invalid property '" + reqProperties[i] + "': " + errorMsg); 924 | valid = false; 925 | } 926 | } 927 | } 928 | let optProperties = ["frameOffsets", "externalFolder"]; 929 | let optPropertyErrors = [verifyArray, verifyBool]; 930 | for (let i = 0; i < optProperties.length; i++) { 931 | if (anim.hasOwnProperty(optProperties[i])) { 932 | let errorMsg = optPropertyErrors[i](anim[optProperties[i]]); 933 | if (errorMsg) { 934 | error("Animation for tile " + tileId + " of " + tilesetName + " has invalid property '" + optProperties[i] + "': " + errorMsg); 935 | valid = false; 936 | } 937 | } 938 | } 939 | if (!valid) 940 | delete tilesetsData[tilesetName].tileAnimations[tileId]; 941 | return valid; 942 | } 943 | 944 | //-------------------------------------------------------------------------- 945 | // The below are used for verifying basic validity of a property's value. 946 | // They return an error message if invalid, and an empty string otherwise. 947 | //-------------------------------------------------------------------------- 948 | function verifyPositive(value) { 949 | if (typeof value !== "number" || value <= 0) 950 | return "'" + value + "' is not a positive number"; 951 | return ""; 952 | } 953 | function verifyString(value) { 954 | if (typeof value !== "string") 955 | return "'" + value + "' is not a string"; 956 | return ""; 957 | } 958 | function verifyBool(value) { 959 | if (typeof value !== "boolean") 960 | return "'" + value + "' is not a boolean"; 961 | return ""; 962 | } 963 | function verifyObject(value) { 964 | if (typeof value !== "object") 965 | return "'" + value + "' is not an object"; 966 | return ""; 967 | } 968 | function verifyArray(value) { 969 | if (typeof value !== "object" || !Array.isArray(value) || value.length == 0) 970 | return "'" + value + "' is not a non-empty array"; 971 | return ""; 972 | } 973 | 974 | //--------------------------------------------------------------------------- 975 | // Verify that an animation can be written for targetTileId. If targetTileId 976 | // already has an animation or exceeds the total tile limit then return 977 | // false. Otherwise return true. 978 | //--------------------------------------------------------------------------- 979 | function verifyAnimCopy(anims, targetTileId, srcTileId, tilesetName) { 980 | let valid = true; 981 | if (anims[targetTileId] != undefined) { 982 | error("Animation for tile " + srcTileId + " of " + tilesetName + " would overwrite existing animation at tile " + targetTileId); 983 | valid = false; 984 | } 985 | if (!verifyTileLimit(targetTileId, tilesetName)) 986 | valid = false; 987 | return valid; 988 | } 989 | 990 | //--------------------------------------------------------------------------- 991 | // Verify that the specified tile does not exceed the tileset's limit. 992 | // Exceeding the limit on a primary tileset is 'technically' ok as long as 993 | // it remains within the secondary tileset limit, but warn the user as it's 994 | // likely unintended. If the tile exceeds the secondary tileset, return 995 | // false. Otherwise return true. 996 | //--------------------------------------------------------------------------- 997 | function verifyTileLimit(tileId, tilesetName) { 998 | let primary = tilesetsData[tilesetName].primary; 999 | let maxTile = primary ? maxPrimaryTile : maxSecondaryTile; 1000 | if (tileId >= maxTile) { 1001 | let message = ("Tile " + tileId + " exceeds limit of " + (maxTile - 1) + " for " + tilesetName); 1002 | if (primary && tileId < maxSecondaryTile) { 1003 | warn(message); 1004 | } else { 1005 | error(message); 1006 | return false; 1007 | } 1008 | } 1009 | return true; 1010 | } 1011 | 1012 | //--------------------------------------------------------------------------- 1013 | // Verify if the user's version of Porymap is at least as new as the version 1014 | // numbers provided. Returns false if the user's version is older than the 1015 | // the provided version. Otherwise returns true. 1016 | //--------------------------------------------------------------------------- 1017 | function verifyPorymapVersion(major, minor, patch) { 1018 | if (constants.version.major != major) 1019 | return constants.version.major > major; 1020 | if (constants.version.minor != minor) 1021 | return constants.version.minor > minor; 1022 | return constants.version.patch >= patch; 1023 | } 1024 | 1025 | 1026 | //================== 1027 | // Logging 1028 | //================== 1029 | 1030 | function log(message) { 1031 | utility.log(logPrefix + message); 1032 | } 1033 | 1034 | function warn(message) { 1035 | utility.warn(logPrefix + message); 1036 | } 1037 | 1038 | function error(message) { 1039 | utility.error(logPrefix + message); 1040 | } 1041 | 1042 | function debug_printObject(object) { 1043 | if (!logDebugInfo) return; 1044 | log(JSON.stringify(object)); 1045 | } 1046 | 1047 | function debug_printObjectArr(object) { 1048 | if (!logDebugInfo) return; 1049 | for (var property in object) 1050 | log(JSON.stringify(object[property])); 1051 | } 1052 | 1053 | //---------------------------------------------------- 1054 | // Log all of the calculated animation data. Unused. 1055 | //---------------------------------------------------- 1056 | function debug_printAnimDataByTileset() { 1057 | if (!logDebugInfo) return; 1058 | for (var tilesetName in tilesetsData) { 1059 | log(tilesetName); 1060 | let anims = tilesetsData[tilesetName].tileAnimations; 1061 | debug_printAnimData(anims); 1062 | } 1063 | } 1064 | 1065 | //-------------------------------------------------- 1066 | // Log the specified animation data. Used to print 1067 | // the loaded animation for the current tilesets. 1068 | //-------------------------------------------------- 1069 | function debug_printAnimData(anims) { 1070 | if (!logDebugInfo) return; 1071 | for (var tileId in anims) { 1072 | log(tileId); 1073 | let anim = anims[tileId]; 1074 | for (var property in anim) { 1075 | // Pre-computed filepath list is enormous, skip 1076 | // printing it and use base filepath property instead 1077 | if (property != "filepaths") 1078 | log(property + ": " + anim[property]); 1079 | } 1080 | log(""); 1081 | } 1082 | } 1083 | 1084 | //------------------------------------------------ 1085 | // Log all the layers being used for animation 1086 | // and which timing interval they belong to. 1087 | //------------------------------------------------ 1088 | function debug_printLayers() { 1089 | if (!logUsageInfo) return; 1090 | for (const interval in animLayerMap) { 1091 | log("Layers animating at interval of " + interval + ":"); 1092 | let animLayers = animLayerMap[interval]; 1093 | for (let j = 0; j < animLayers.length; j++) { 1094 | log(animLayers[j]); 1095 | } 1096 | log("Static layers associated with interval " + interval + ":"); 1097 | let staticLayers = staticLayerMap[interval]; 1098 | if (!staticLayers) continue; 1099 | for (let j = 0; j < staticLayers.layers.length; j++) { 1100 | log(staticLayers.layers[j]); 1101 | } 1102 | } 1103 | } 1104 | -------------------------------------------------------------------------------- /animations_pokeemerald.js: -------------------------------------------------------------------------------- 1 | export const tilesetsData = { 2 | "gTileset_General": { 3 | folder: "general/anim", 4 | primary: true, 5 | tileAnimations: { 6 | 432: { // (0x1B0) 7 | folder: "water", 8 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 9 | numTiles: 30, 10 | interval: 16, 11 | imageWidth: 16, 12 | }, 13 | 464: { // (0x1D0) 14 | folder: "sand_water_edge", 15 | frames: ["0", "1", "2", "3", "4", "5", "6"], 16 | numTiles: 10, 17 | interval: 16, 18 | imageWidth: 16, 19 | }, 20 | 480: { // (0x1E0) 21 | folder: "land_water_edge", 22 | frames: ["0", "1", "2", "3"], 23 | numTiles: 10, 24 | interval: 16, 25 | imageWidth: 80, 26 | }, 27 | 496: { // (0x1F0) 28 | folder: "waterfall", 29 | frames: ["0", "1", "2", "3"], 30 | numTiles: 6, 31 | interval: 16, 32 | imageWidth: 8, 33 | }, 34 | 508: { // (0x1FC) 35 | folder: "flower", 36 | frames: ["0", "1", "0", "2"], 37 | numTiles: 4, 38 | interval: 16, 39 | imageWidth: 16, 40 | }, 41 | }, 42 | }, 43 | "gTileset_Building": { 44 | folder: "building/anim", 45 | primary: true, 46 | tileAnimations: { 47 | 496: { // (0x1F0) This is an unused version of the TV that's always on 48 | folder: "tv_turned_on", 49 | frames: ["0", "1"], 50 | numTiles: 4, 51 | interval: 8, 52 | imageWidth: 16, 53 | }, 54 | }, 55 | }, 56 | "gTileset_Rustboro": { 57 | folder: "rustboro/anim", 58 | primary: false, 59 | tileAnimations: { 60 | 640: { // (0x280) 61 | folder: "windy_water", 62 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 63 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 64 | numTiles: 4, 65 | interval: 8, 66 | imageWidth: 16, 67 | }, 68 | 960: { // (0x3C0) 69 | folder: "fountain", 70 | frames: ["0", "1"], 71 | numTiles: 4, 72 | interval: 8, 73 | imageWidth: 16, 74 | }, 75 | }, 76 | }, 77 | "gTileset_Dewford": { 78 | folder: "dewford/anim", 79 | primary: false, 80 | tileAnimations: { 81 | 682: { // (0x2AA) Unused in vanilla 82 | folder: "flag", 83 | frames: ["0", "1", "2", "3"], 84 | numTiles: 6, 85 | interval: 8, 86 | imageWidth: 24, 87 | }, 88 | }, 89 | }, 90 | "gTileset_Slateport": { 91 | folder: "slateport/anim", 92 | primary: false, 93 | tileAnimations: { 94 | 736: { // (0x2E0) 95 | folder: "balloons", 96 | frames: ["0", "1", "2", "3"], 97 | numTiles: 4, 98 | interval: 16, 99 | imageWidth: 16, 100 | }, 101 | }, 102 | }, 103 | "gTileset_Mauville": { 104 | folder: "mauville/anim", 105 | primary: false, 106 | tileAnimations: { 107 | 608: { // (0x260) 108 | folder: "flower_1", 109 | frames: ["0", "0", "1", "2", 110 | "3", "3", "3", "3", 111 | "3", "3", "2", "1", 112 | "0", "0", "4", "4", 113 | "0", "0", "4", "4", 114 | "0", "0", "4", "4", 115 | "0", "0", "4", "4", 116 | "0", "0", "4", "4"], 117 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 118 | numTiles: 4, 119 | interval: 8, 120 | imageWidth: 16, 121 | }, 122 | 640: { // (0x280) 123 | folder: "flower_2", 124 | frames: ["0", "0", "1", "2", 125 | "3", "3", "3", "3", 126 | "3", "3", "2", "1", 127 | "0", "0", "4", "4", 128 | "0", "0", "4", "4", 129 | "0", "0", "4", "4", 130 | "0", "0", "4", "4", 131 | "0", "0", "4", "4"], 132 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 133 | numTiles: 4, 134 | interval: 8, 135 | imageWidth: 16, 136 | }, 137 | }, 138 | }, 139 | "gTileset_Lavaridge": { 140 | folder: "lavaridge/anim", 141 | primary: false, 142 | tileAnimations: { 143 | 800: { // (0x320) 144 | folder: "steam", 145 | frames: ["0", "1", "2", "3"], 146 | frameOffsets: [2], 147 | numTiles: 4, 148 | interval: 16, 149 | imageWidth: 16, 150 | }, 151 | 672: { // (0x2A0) Lavaridge's lava gets its images from the cave tileset 152 | folder: "data/tilesets/secondary/cave/anim/lava", 153 | externalFolder: true, 154 | frames: ["0", "1", "2", "3"], 155 | numTiles: 4, 156 | interval: 16, 157 | imageWidth: 16, 158 | }, 159 | }, 160 | }, 161 | "gTileset_EverGrande": { 162 | folder: "ever_grande/anim", 163 | primary: false, 164 | tileAnimations: { 165 | 736: { // (0x2E0) 166 | folder: "flowers", 167 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 168 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 169 | numTiles: 4, 170 | interval: 8, 171 | imageWidth: 16, 172 | }, 173 | }, 174 | }, 175 | "gTileset_Pacifidlog": { 176 | folder: "pacifidlog/anim", 177 | primary: false, 178 | tileAnimations: { 179 | 976: { // (0x3D0) 180 | folder: "log_bridges", 181 | frames: ["0", "1", "2", "1"], 182 | numTiles: 30, 183 | interval: 16, 184 | imageWidth: 16, 185 | }, 186 | 1008: { // (0x3F0) 187 | folder: "water_currents", 188 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 189 | numTiles: 8, 190 | interval: 16, 191 | imageWidth: 16, 192 | }, 193 | }, 194 | }, 195 | "gTileset_Sootopolis": { 196 | folder: "sootopolis/anim", 197 | primary: false, 198 | tileAnimations: { 199 | 752: { // (0x2F0) 200 | folder: "stormy_water", 201 | frames: ["0_kyogre", "1_kyogre", "2_kyogre", "3_kyogre", "4_kyogre", "5_kyogre", "6_kyogre", "7_kyogre"], 202 | numTiles: 48, 203 | interval: 16, 204 | imageWidth: 64, 205 | }, 206 | 800: { // (0x320) 207 | folder: "stormy_water", 208 | frames: ["0_groudon", "1_groudon", "2_groudon", "3_groudon", "4_groudon", "5_groudon", "6_groudon", "7_groudon"], 209 | numTiles: 48, 210 | interval: 16, 211 | imageWidth: 64, 212 | }, 213 | }, 214 | }, 215 | "gTileset_BattleFrontierOutsideWest": { 216 | folder: "battle_frontier_outside_west/anim", 217 | primary: false, 218 | tileAnimations: { 219 | 730: { // (0x2DA) 220 | folder: "flag", 221 | frames: ["0", "1", "2", "3"], 222 | numTiles: 6, 223 | interval: 8, 224 | imageWidth: 24, 225 | }, 226 | }, 227 | }, 228 | "gTileset_BattleFrontierOutsideEast": { 229 | folder: "battle_frontier_outside_east/anim", 230 | primary: false, 231 | tileAnimations: { 232 | 730: { // (0x2DA) 233 | folder: "flag", 234 | frames: ["0", "1", "2", "3"], 235 | numTiles: 6, 236 | interval: 8, 237 | imageWidth: 24, 238 | }, 239 | }, 240 | }, 241 | "gTileset_Underwater": { 242 | folder: "underwater/anim", 243 | primary: false, 244 | tileAnimations: { 245 | 1008: { // (0x3F0) 246 | folder: "seaweed", 247 | frames: ["0", "1", "2", "3"], 248 | numTiles: 4, 249 | interval: 16, 250 | imageWidth: 16, 251 | }, 252 | }, 253 | }, 254 | "gTileset_SootopolisGym": { 255 | folder: "sootopolis_gym/anim", 256 | primary: false, 257 | tileAnimations: { 258 | 976: { // (0x3D0) 259 | folder: "front_waterfall", 260 | frames: ["0", "1", "2"], 261 | numTiles: 20, 262 | interval: 8, 263 | imageWidth: 32, 264 | }, 265 | 1008: { // (0x3F0) Unused in vanilla 266 | folder: "side_waterfall", 267 | frames: ["0", "1", "2"], 268 | numTiles: 12, 269 | interval: 8, 270 | imageWidth: 16, 271 | }, 272 | }, 273 | }, 274 | "gTileset_Cave": { 275 | folder: "cave/anim", 276 | primary: false, 277 | tileAnimations: { 278 | 928: { // (0x3A0) 279 | folder: "lava", 280 | frames: ["0", "1", "2", "3"], 281 | numTiles: 4, 282 | interval: 16, 283 | imageWidth: 16, 284 | }, 285 | }, 286 | }, 287 | "gTileset_EliteFour": { 288 | folder: "elite_four/anim", 289 | primary: false, 290 | tileAnimations: { 291 | 992: { // (0x3E0) 292 | folder: "floor_light", 293 | frames: ["0", "1"], 294 | numTiles: 4, 295 | interval: 64, 296 | imageWidth: 16, 297 | }, 298 | 1016: { // (0x3F8) 299 | folder: "wall_lights", 300 | frames: ["0", "1", "2", "3"], 301 | numTiles: 1, 302 | interval: 8, 303 | imageWidth: 8, 304 | }, 305 | }, 306 | }, 307 | "gTileset_MauvilleGym": { 308 | folder: "mauville_gym/anim", 309 | primary: false, 310 | tileAnimations: { 311 | 656: { // (0x290) 312 | folder: "electric_gates", 313 | frames: ["0", "1"], 314 | numTiles: 16, 315 | interval: 2, 316 | imageWidth: 16, 317 | }, 318 | }, 319 | }, 320 | "gTileset_BikeShop": { 321 | folder: "bike_shop/anim", 322 | primary: false, 323 | tileAnimations: { 324 | 1008: { // (0x3F0) 325 | folder: "blinking_lights", 326 | frames: ["0", "1"], 327 | numTiles: 9, 328 | interval: 4, 329 | imageWidth: 24, 330 | }, 331 | }, 332 | }, 333 | "gTileset_BattlePyramid": { 334 | folder: "battle_pyramid/anim", 335 | primary: false, 336 | tileAnimations: { 337 | 647: { // (0x287) 338 | folder: "statue_shadow", 339 | frames: ["0", "1", "2"], 340 | numTiles: 8, 341 | interval: 8, 342 | imageWidth: 16, 343 | }, 344 | 663: { // (0x297) 345 | folder: "torch", 346 | frames: ["0", "1", "2"], 347 | numTiles: 8, 348 | interval: 8, 349 | imageWidth: 16, 350 | }, 351 | }, 352 | }, 353 | }; 354 | -------------------------------------------------------------------------------- /animations_pokefirered.js: -------------------------------------------------------------------------------- 1 | export const tilesetsData = { 2 | "gTileset_General": { 3 | folder: "general/anim", 4 | primary: true, 5 | tileAnimations: { 6 | 416: { // (0x1A0) 7 | folder: "water_current_landwatersedge", 8 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 9 | numTiles: 48, 10 | interval: 16, 11 | imageWidth: 16, 12 | }, 13 | 464: { // (0x1D0) 14 | folder: "sandwatersedge", 15 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 16 | numTiles: 18, 17 | interval: 8, 18 | imageWidth: 16, 19 | }, 20 | 508: { // (0x1FC) 21 | folder: "flower", 22 | frames: ["0", "1", "2", "3", "4"], 23 | numTiles: 4, 24 | interval: 16, 25 | imageWidth: 16, 26 | }, 27 | }, 28 | }, 29 | "gTileset_CeladonCity": { 30 | folder: "celadon_city/anim", 31 | primary: false, 32 | tileAnimations: { 33 | 744: { // (0x2E8) 34 | folder: "fountain", 35 | frames: ["0", "1", "2", "3", "4"], 36 | numTiles: 8, 37 | interval: 12, 38 | imageWidth: 16, 39 | }, 40 | }, 41 | }, 42 | "gTileset_CeladonGym": { 43 | folder: "celadon_gym/anim", 44 | primary: false, 45 | tileAnimations: { 46 | 739: { // (0x2E3) 47 | folder: "flowers", 48 | frames: ["0", "1", "2", "1"], 49 | numTiles: 4, 50 | interval: 16, 51 | imageWidth: 16, 52 | }, 53 | }, 54 | }, 55 | "gTileset_MtEmber": { 56 | folder: "mt_ember/anim", 57 | primary: false, 58 | tileAnimations: { 59 | 896: { // (0x380) 60 | folder: "steam", 61 | frames: ["0", "1", "2", "3"], 62 | numTiles: 8, 63 | interval: 16, 64 | imageWidth: 16, 65 | }, 66 | }, 67 | }, 68 | "gTileset_SilphCo": { 69 | folder: "silph_co/anim", 70 | primary: false, 71 | tileAnimations: { 72 | 976: { // (0x3D0) 73 | folder: "fountain", 74 | frames: ["0", "1", "2", "3"], 75 | numTiles: 8, 76 | interval: 10, 77 | imageWidth: 16, 78 | }, 79 | }, 80 | }, 81 | "gTileset_VermilionGym": { 82 | folder: "vermilion_gym/anim", 83 | primary: false, 84 | tileAnimations: { 85 | 880: { // (0x370) 86 | folder: "motorizeddoor", 87 | frames: ["0", "1"], 88 | numTiles: 7, 89 | interval: 2, 90 | imageWidth: 56, 91 | }, 92 | }, 93 | }, 94 | }; 95 | -------------------------------------------------------------------------------- /animations_pokeruby.js: -------------------------------------------------------------------------------- 1 | export const tilesetsData = { 2 | "gTileset_General": { 3 | folder: "general/anim", 4 | primary: true, 5 | tileAnimations: { 6 | 432: { // (0x1B0) 7 | folder: "1", // "water", 8 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 9 | numTiles: 30, 10 | interval: 16, 11 | imageWidth: 16, 12 | }, 13 | 464: { // (0x1D0) 14 | folder: "2", // "sand_water_edge", 15 | frames: ["0", "1", "2", "3", "4", "5", "6"], 16 | numTiles: 10, 17 | interval: 16, 18 | imageWidth: 16, 19 | }, 20 | 480: { // (0x1E0) 21 | folder: "4", // "land_water_edge", 22 | frames: ["0", "1", "2", "3"], 23 | numTiles: 10, 24 | interval: 16, 25 | imageWidth: 80, 26 | 27 | }, 28 | 496: { // (0x1F0) 29 | folder: "3", // "waterfall", 30 | frames: ["0", "1", "2", "3"], 31 | numTiles: 6, 32 | interval: 16, 33 | imageWidth: 48, 34 | }, 35 | 508: { // (0x1FC) 36 | folder: "0", // "flower", 37 | frames: ["0", "1", "0", "2"], 38 | numTiles: 4, 39 | interval: 16, 40 | imageWidth: 16, 41 | }, 42 | }, 43 | }, 44 | "gTileset_Building": { 45 | folder: "building/anim", 46 | primary: true, 47 | tileAnimations: { 48 | 496: { // This is an unused version of the TV that's always on 49 | folder: "", // "tv_turned_on", 50 | frames: ["0", "1"], 51 | numTiles: 4, 52 | interval: 8, 53 | imageWidth: 16, 54 | }, 55 | }, 56 | }, 57 | "gTileset_Rustboro": { 58 | folder: "rustboro/anim", 59 | primary: false, 60 | tileAnimations: { 61 | 640: { // (0x280) 62 | folder: "0", // "windy_water", 63 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 64 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 65 | numTiles: 4, 66 | interval: 8, 67 | imageWidth: 16, 68 | }, 69 | 960: { // (0x3C0) 70 | folder: "1", // "fountain", 71 | frames: ["0", "1"], 72 | numTiles: 4, 73 | interval: 8, 74 | imageWidth: 16, 75 | }, 76 | }, 77 | }, 78 | "gTileset_Mauville": { 79 | folder: "mauville/anim", 80 | primary: false, 81 | tileAnimations: { 82 | 608: { // (0x260) 83 | folder: "0", // "flower_1", 84 | frames: ["0", "0", "1", "2", 85 | "3", "3", "3", "3", 86 | "3", "3", "2", "1", 87 | "0", "0", "4", "4", 88 | "0", "0", "4", "4", 89 | "0", "0", "4", "4", 90 | "0", "0", "4", "4", 91 | "0", "0", "4", "4"], 92 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 93 | numTiles: 4, 94 | interval: 8, 95 | imageWidth: 16, 96 | }, 97 | 640: { // (0x280) 98 | folder: "1", // "flower_2", 99 | frames: ["0", "0", "1", "2", 100 | "3", "3", "3", "3", 101 | "3", "3", "2", "1", 102 | "0", "0", "4", "4", 103 | "0", "0", "4", "4", 104 | "0", "0", "4", "4", 105 | "0", "0", "4", "4", 106 | "0", "0", "4", "4"], 107 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 108 | numTiles: 4, 109 | interval: 8, 110 | imageWidth: 16, 111 | }, 112 | }, 113 | }, 114 | "gTileset_Lavaridge": { 115 | folder: "lavaridge/anim", 116 | primary: false, 117 | tileAnimations: { 118 | 800: { // (0x320) 119 | folder: "", // "steam", 120 | frames: ["0", "1", "2", "3"], 121 | frameOffsets: [2], 122 | numTiles: 4, 123 | interval: 16, 124 | imageWidth: 8, 125 | }, 126 | 672: { // (0x2A0) Lavaridge's lava gets its images from the cave tileset 127 | folder: "data/tilesets/secondary/cave/anim", 128 | externalFolder: true, 129 | frames: ["0", "1", "2", "3"], 130 | numTiles: 4, 131 | interval: 16, 132 | imageWidth: 16, 133 | }, 134 | }, 135 | }, 136 | "gTileset_EverGrande": { 137 | folder: "ever_grande/anim", 138 | primary: false, 139 | tileAnimations: { 140 | 736: { // (0x2E0) 141 | folder: "", // "flowers", 142 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 143 | frameOffsets: [1, 2, 3, 4, 5, 6, 7], 144 | numTiles: 4, 145 | interval: 8, 146 | imageWidth: 32, 147 | }, 148 | }, 149 | }, 150 | "gTileset_Pacifidlog": { 151 | folder: "pacifidlog/anim", 152 | primary: false, 153 | tileAnimations: { 154 | 976: { // (0x3D0) 155 | folder: "0", // "log_bridges", 156 | frames: ["0", "1", "2", "1"], 157 | numTiles: 30, 158 | interval: 16, 159 | imageWidth: 8, 160 | }, 161 | 1008: { // (0x3F0) 162 | folder: "1", // "water_currents", 163 | frames: ["0", "1", "2", "3", "4", "5", "6", "7"], 164 | numTiles: 8, 165 | interval: 16, 166 | imageWidth: 16, 167 | }, 168 | }, 169 | }, 170 | "gTileset_Underwater": { 171 | folder: "underwater/anim", 172 | primary: false, 173 | tileAnimations: { 174 | 1008: { // (0x3F0) 175 | folder: "", // "seaweed", 176 | frames: ["0", "1", "2", "3"], 177 | numTiles: 4, 178 | interval: 16, 179 | imageWidth: 16, 180 | }, 181 | }, 182 | }, 183 | "gTileset_SootopolisGym": { 184 | folder: "sootopolis_gym/anim", 185 | primary: false, 186 | tileAnimations: { 187 | 976: { // (0x3D0) 188 | folder: "1", // "front_waterfall", 189 | frames: ["0", "1", "2"], 190 | numTiles: 20, 191 | interval: 8, 192 | imageWidth: 32, 193 | }, 194 | 1008: { // (0x3F0) Unused in vanilla 195 | folder: "0", // "side_waterfall", 196 | frames: ["0", "1", "2"], 197 | numTiles: 12, 198 | interval: 8, 199 | imageWidth: 16, 200 | }, 201 | }, 202 | }, 203 | "gTileset_Cave": { 204 | folder: "cave/anim", 205 | primary: false, 206 | tileAnimations: { 207 | 928: { // (0x3A0) 208 | folder: "", // "lava", 209 | frames: ["0", "1", "2", "3"], 210 | numTiles: 4, 211 | interval: 16, 212 | imageWidth: 8, 213 | }, 214 | }, 215 | }, 216 | "gTileset_EliteFour": { 217 | folder: "elite_four/anim", 218 | primary: false, 219 | tileAnimations: { 220 | 992: { // (0x3E0) 221 | folder: "1", // "floor_light", 222 | frames: ["0", "1"], 223 | numTiles: 4, 224 | interval: 64, 225 | imageWidth: 16, 226 | }, 227 | 1016: { // (0x3F8) 228 | folder: "0", // "wall_lights", 229 | frames: ["0", "1", "2", "3"], 230 | numTiles: 1, 231 | interval: 8, 232 | imageWidth: 8, 233 | }, 234 | }, 235 | }, 236 | "gTileset_MauvilleGym": { 237 | folder: "mauville_gym/anim", 238 | primary: false, 239 | tileAnimations: { 240 | 656: { // (0x290) 241 | folder: "", // "electric_gates", 242 | frames: ["0", "1"], 243 | numTiles: 16, 244 | interval: 2, 245 | imageWidth: 16, 246 | }, 247 | }, 248 | }, 249 | "gTileset_BikeShop": { 250 | folder: "bike_shop/anim", 251 | primary: false, 252 | tileAnimations: { 253 | 1008: { // (0x3F0) 254 | folder: "", // "blinking_lights", 255 | frames: ["0", "1"], 256 | numTiles: 9, 257 | interval: 4, 258 | imageWidth: 72, 259 | }, 260 | }, 261 | }, 262 | }; 263 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | //==================== 2 | // Settings 3 | //==================== 4 | 5 | export const toggleShortcut = "Ctrl+A"; 6 | export const animateOnLaunch = true; 7 | 8 | // Animation data 9 | import {tilesetsData as em} from './animations_pokeemerald.js'; 10 | import {tilesetsData as frlg} from './animations_pokefirered.js'; 11 | import {tilesetsData as rs} from './animations_pokeruby.js'; 12 | export const versionData = [rs, frlg, em]; 13 | 14 | // Base filepaths 15 | export const tilesetsPath = "data/tilesets/"; 16 | export const primaryPath = tilesetsPath + "primary/"; 17 | export const secondaryPath = tilesetsPath + "secondary/"; 18 | export const animFileExtension = ".png"; 19 | 20 | // Logging 21 | export const logPrefix = "ANIM: "; 22 | export const logBasicInfo = true; 23 | // Setting the logInfo data below to true will impact 24 | // performance. Only turn them on if you need to. 25 | export const logUsageInfo = false; 26 | export const logDebugInfo = false; 27 | 28 | // Timing 29 | // There are 1000ms in a second, and the GBA's refresh rate is ~59.73 frames per second. 30 | // After rounding, the refresh rate will be just slightly slower than the GBA (17ms vs 16.74ms). 31 | // The timer operates in millisecond units, so it is not possible to set a closer interval. 32 | export const refreshTime = Math.round(1000 / 59.73); 33 | 34 | // Exceptions 35 | // If you'd like to always skip animations for certain maps, add them to this list 36 | export const mapExceptions = [""]; // e.g. ["PetalburgCity", ...] 37 | --------------------------------------------------------------------------------