23 Commits

Author SHA1 Message Date
kuroppoi
72d5a10ffc Limit accepted file types 2023-07-15 23:18:19 +02:00
kuroppoi
6f1853fc50 Check if uploaded file is actually an image 2023-07-15 22:59:58 +02:00
kuroppoi
6999f1f976 Skip invalid tile mapping data 2023-07-15 22:23:51 +02:00
kuroppoi
16d1b99f1f Amend 229cfaf4ca 2023-07-15 21:20:47 +02:00
kuroppoi
229cfaf4ca Fix C-Gear skin preview inaccuracy 2023-07-15 21:11:31 +02:00
kuroppoi
639a487530 Minor tweaks 2023-07-15 18:07:54 +02:00
kuroppoi
08d354e814 Update comment 2023-07-15 17:37:15 +02:00
kuroppoi
adc0c16c68 Overwrite existing files when migrating player data 2023-07-15 17:25:28 +02:00
kuroppoi
7fa718af48 Fix player data migration oopsie 2023-07-15 17:18:38 +02:00
kuroppoi
ff0e541fe8 Add option to upload custom skins 2023-07-15 17:05:25 +02:00
kuroppoi
4628fee251 Restrict gender options if species is exclusively male, female or genderless 2023-07-12 19:29:24 +02:00
kuroppoi
a6ed715377 Update README.md 2023-07-12 19:20:47 +02:00
kuroppoi
4e56d2532f Update README.md 2023-07-12 19:19:12 +02:00
kuroppoi
eeb6048f54 Add Shaymin sky forme data 2023-07-12 16:29:49 +02:00
kuroppoi
875500d302 Display female encounter sprite if applicable 2023-07-12 16:28:04 +02:00
kuroppoi
1990519088 Change encounter defaults 2023-07-12 16:05:38 +02:00
kuroppoi
eb1605dbe7 Change how popup forms work so they don't rely on anchors 2023-07-11 23:27:50 +02:00
kuroppoi
d232de7307 Fix item form close button not working 2023-07-11 23:15:22 +02:00
kuroppoi
d57c7434a0 Display ability in dreamer summary 2023-07-11 04:20:37 +02:00
kuroppoi
022ee2f1e2 linguist-vendored test 2023-07-10 20:39:34 +02:00
kuroppoi
4f8a717ffb Use the first color index to determine foreground transparency 2023-07-10 16:57:39 +02:00
kuroppoi
a479c158db Fix bottleneck 2023-07-10 05:17:21 +02:00
kuroppoi
9a875cdf77 Apply background to Pokédex skins on read 2023-07-10 04:49:34 +02:00
25 changed files with 1401 additions and 404 deletions

6
.gitattributes vendored
View File

@@ -1,6 +1,2 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
src/main/resources/dashboard/scripts/pokedata.js linguist-vendored

View File

@@ -1,5 +1,6 @@
# Entralinked
[![build](https://github.com/kuroppoi/entralinked/actions/workflows/dist-upload-artifact.yml/badge.svg)](https://github.com/kuroppoi/entralinked/actions)
[![release](https://img.shields.io/github/v/release/kuroppoi/entralinked?labelColor=30373D&label=Release&logoColor=959DA5&logo=github)](https://github.com/kuroppoi/entralinked/releases/latest)
Entralinked is a standalone Game Sync emulator developed for use with Pokémon Black & White and its sequels.\
Its purpose is to serve as a simple utility for downloading Pokémon, Items, C-Gear skins, Pokédex skins, Musicals\
@@ -28,5 +29,5 @@ Entralinked has a built-in DNS server.\
In order for your game to connect, you must configure the DNS settings of your DS.\
By default, Entralinked is configured to automatically use the local host of the system.\
This approach is not always accurate, however, and you may need to manually configure it in `config.json`.\
If you receive error code `60000` when trying to connect, erase the Nintendo WFC Configuration of your DS and try again.\
After tucking in a Pokémon, navigate to `http://localhost/dashboard/profile.html` in a web browser to configure Game Sync settings.
If you receive error code `60000` when trying to connect, erase the WFC Configuration of your DS and try again.\
After tucking in a Pokémon, navigate to http://localhost/dashboard/profile.html to configure Game Sync settings.

View File

@@ -79,6 +79,15 @@ public class DlcList {
// DLC Content level
for(File dlcFile : subFile.listFiles()) {
String name = dlcFile.getName();
// Check if DLC name is reserved as an internal identifier
if(name.equals("none") || name.equals("custom")) {
logger.warn("DLC '{}/{}/{}' could not be loaded because it uses a reserved name.",
file.getName(), subFile.getName(), name);
continue;
}
// Load DLC data
Dlc dlc = loadDlcFile(file.getName(), subFile.getName(), index, dlcFile);

View File

@@ -7,17 +7,18 @@ import com.fasterxml.jackson.annotation.JsonProperty;
* Record containing information about a Pokémon
*/
public record PkmnInfo(
@JsonProperty(required = true) int personality,
@JsonProperty(required = true) int species,
@JsonProperty(required = true) int heldItem,
@JsonProperty(required = true) int trainerId,
@JsonProperty(required = true) int trainerSecretId,
@JsonProperty(required = true) int level,
@JsonProperty(required = true) int form,
@JsonProperty(required = true) PkmnNature nature,
@JsonProperty(required = true) PkmnGender gender,
@JsonProperty(required = true) String nickname,
@JsonProperty(required = true) String trainerName) {
@JsonProperty(required = true) String nickname,
@JsonProperty(required = true) String trainerName,
@JsonProperty(required = true) PkmnNature nature,
@JsonProperty(required = true) PkmnGender gender,
@JsonProperty(required = true) int species,
@JsonProperty(required = true) int personality,
@JsonProperty(required = true) int trainerId,
@JsonProperty(required = true) int trainerSecretId,
@JsonProperty(required = true) int level,
@JsonProperty(required = false) int form,
@JsonProperty(required = false) int ability,
@JsonProperty(required = false) int heldItem) {
@JsonIgnore
public boolean isShiny() {

View File

@@ -58,7 +58,7 @@ public class PkmnInfoReader {
// Read Pokémon data
int species = buffer.getShortLE(8);
int item = buffer.getShortLE(10) & 0xFFFF;
int heldItem = buffer.getShortLE(10) & 0xFFFF;
int trainerId = buffer.getShortLE(12) & 0xFFFF;
int trainerSecretId = buffer.getShortLE(14) & 0xFFFF;
int level = buffer.getByte(140);
@@ -78,14 +78,14 @@ public class PkmnInfoReader {
// Loosely verify data
if(species < 1 || species > 649) throw new IOException("Invalid species");
if(item < 0 || item > 638) throw new IOException("Invalid held item");
if(heldItem < 0 || heldItem > 638) throw new IOException("Invalid held item");
if(ability < 1 || ability > 164) throw new IOException("Invalid ability");
if(level < 1 || level > 100) throw new IOException("Level is out of range");
if(nature == null) throw new IOException("Invalid nature");
// Create record
PkmnInfo info = new PkmnInfo(personality, species, item, trainerId, trainerSecretId, level, form, nature, gender, nickname, trainerName);
return info;
return new PkmnInfo(nickname, trainerName, nature, gender, species, personality,
trainerId, trainerSecretId, level, form, ability, heldItem);
}
private static void decryptData(ByteBuf buffer, int offset, int length, int seed) throws IOException {

View File

@@ -1,5 +1,6 @@
package entralinked.model.player;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -22,6 +23,9 @@ public class Player {
private String cgearSkin;
private String dexSkin;
private String musical;
private String customCGearSkin;
private String customDexSkin;
private File dataDirectory;
public Player(String gameSyncId) {
this.gameSyncId = gameSyncId;
@@ -132,4 +136,46 @@ public class Player {
public String getMusical() {
return musical;
}
public void setCustomCGearSkin(String customCGearSkin) {
this.customCGearSkin = customCGearSkin;
}
public String getCustomCGearSkin() {
return customCGearSkin;
}
public void setCustomDexSkin(String customDexSkin) {
this.customDexSkin = customDexSkin;
}
public String getCustomDexSkin() {
return customDexSkin;
}
// IO stuff
public void setDataDirectory(File dataDirectory) {
this.dataDirectory = dataDirectory;
}
public File getDataDirectory() {
return dataDirectory;
}
public File getDataFile() {
return new File(dataDirectory, "data.json");
}
public File getSaveFile() {
return new File(dataDirectory, "save.bin");
}
public File getCGearSkinFile() {
return new File(dataDirectory, "cgear.bin");
}
public File getDexSkinFile() {
return new File(dataDirectory, "zukan.bin");
}
}

View File

@@ -21,6 +21,8 @@ public record PlayerDto(
String cgearSkin,
String dexSkin,
String musical,
String customCGearSkin,
String customDexSkin,
int levelsGained,
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
@@ -28,8 +30,9 @@ public record PlayerDto(
public PlayerDto(Player player) {
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getLevelsGained(),
player.getEncounters(), player.getItems(),player.getAvenueVisitors());
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
player.getAvenueVisitors());
}
/**
@@ -43,6 +46,8 @@ public record PlayerDto(
player.setCGearSkin(cgearSkin);
player.setDexSkin(dexSkin);
player.setMusical(musical);
player.setCustomCGearSkin(customCGearSkin);
player.setCustomDexSkin(customDexSkin);
player.setLevelsGained(levelsGained);
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
player.setItems(items == null ? Collections.emptyList() : items);

View File

@@ -5,6 +5,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
@@ -25,7 +26,6 @@ public class PlayerManager {
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
private final Map<String, Player> playerMap = new ConcurrentHashMap<>();
private final File dataDirectory = new File("players");
private final File gameSaveDirectory = new File(dataDirectory, "savedata");
public PlayerManager() {
logger.info("Loading player data ...");
@@ -35,16 +35,76 @@ public class PlayerManager {
return;
}
// Migrate player data if necessary
migrateLegacyData();
// Load player data
for(File file : dataDirectory.listFiles()) {
if(!file.isDirectory()) {
loadPlayer(file);
if(file.isDirectory()) {
loadPlayer(new File(file, "data.json"));
}
}
logger.info("Loaded {} player(s)", playerMap.size());
}
/**
* Migrates player data from the old storage format.
*/
private void migrateLegacyData() {
// Migrate player configuration files
for(File file : dataDirectory.listFiles()) {
if(!file.isDirectory()) {
try {
PlayerDto player = mapper.readValue(file, PlayerDto.class);
File outputDirectory = new File(dataDirectory, player.gameSyncId());
outputDirectory.mkdirs();
Files.copy(file.toPath(), new File(outputDirectory, "data.json").toPath(), StandardCopyOption.REPLACE_EXISTING);
file.delete();
} catch(IOException e) {
logger.error("Could not migrate player data {}", file.getName());
}
}
}
// Migrate game save data files
File gameSaveDirectory = new File(dataDirectory, "savedata");
if(gameSaveDirectory.exists()) {
for(File file : gameSaveDirectory.listFiles()) {
if(!file.isDirectory()) {
String name = file.getName();
try {
// Skip if game save data file has an invalid name
if(!name.matches("^SAV-[A-HJ-NP-Z2-9]{10}.bin$")) {
logger.warn("Game save data {} will not be migrated because the file name is invalid", name);
file.delete();
continue;
}
String gameSyncId = name.split("-")[1].replace(".bin", "");
File outputDirectory = new File(dataDirectory, gameSyncId);
// Skip if game save data does not belong to any existing Game Sync data
if(!outputDirectory.isDirectory()) {
logger.warn("Game save data {} will not be migrated because no Game Sync data exists for it", name);
file.delete();
continue;
}
Files.copy(file.toPath(), new File(outputDirectory, "save.bin").toPath(), StandardCopyOption.REPLACE_EXISTING);
file.delete();
} catch(IOException e) {
logger.error("Could not migrate game save data {}", name, e);
}
}
}
gameSaveDirectory.delete();
}
}
/**
* Loads a {@link Player} from the specified input file.
* The loaded player instance is automatically mapped, unless it has an already-mapped Game Sync ID.
@@ -60,6 +120,7 @@ public class PlayerManager {
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId));
}
player.setDataDirectory(inputFile.getParentFile());
playerMap.put(gameSyncId, player);
} catch(IOException e) {
logger.error("Could not load player data at {}", inputFile.getAbsolutePath(), e);
@@ -75,12 +136,9 @@ public class PlayerManager {
/**
* Saves the data of the specified player to disk, and returns {@code true} if it succeeds.
* The output file is generated as follows:
*
* {@code new File(dataDirectory, "PGL-%s".formatted(gameSyncId))}
*/
public boolean savePlayer(Player player) {
return savePlayer(player, new File(dataDirectory, "PGL-%s.json".formatted(player.getGameSyncId())));
return savePlayer(player, player.getDataFile());
}
/**
@@ -124,9 +182,18 @@ public class PlayerManager {
return null;
}
// Create data directory
File playerDataDirectory = new File(dataDirectory, gameSyncId);
if(playerDataDirectory.exists()) {
logger.warn("Can't register player {} because the data directory already exists!", gameSyncId);
return null;
}
// Construct player object
Player player = new Player(gameSyncId);
player.setStatus(PlayerStatus.AWAKE);
player.setDataDirectory(playerDataDirectory);
// Try to save player data
if(!savePlayer(player)) {
@@ -140,20 +207,11 @@ public class PlayerManager {
/**
* Stores a game save file read from the specified input stream to a file.
* The output file is generated as follows:
*
* {@code new File(gameSaveDirectory, "SAV-%s.bin".formatted(gameSyncId))}
*
* @return {@code true} if the game save file was written successfully, otherwise {@code false}.
*/
public boolean storePlayerGameSaveFile(Player player, InputStream inputStream) {
// Try create directories
if(!gameSaveDirectory.exists() && !gameSaveDirectory.mkdirs()) {
return false;
}
// Write the save data
try(FileOutputStream outputStream = new FileOutputStream(getPlayerGameSaveFile(player))) {
try(FileOutputStream outputStream = new FileOutputStream(player.getSaveFile())) {
inputStream.transferTo(outputStream);
return true;
} catch(IOException e) {
@@ -162,13 +220,6 @@ public class PlayerManager {
}
}
/**
* @return The game save data {@link File} object associated with this player.
*/
public File getPlayerGameSaveFile(Player player) {
return new File(gameSaveDirectory, "SAV-%s.bin".formatted(player.getGameSyncId()));
}
/**
* @return {@code true} if a player with the specified Game Sync ID exists, {@code false} otherwise.
*/

View File

@@ -5,11 +5,14 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import entralinked.model.dlc.Dlc;
public class User {
private final String id;
private final String password; // I debated hashing it, but.. it's a 3-digit password...
private final Map<String, GameProfile> profiles = new HashMap<>();
private final Map<String, Dlc> dlcOverrides = new HashMap<>();
public User(String id, String password) {
this.id = id;
@@ -47,4 +50,24 @@ public class User {
protected Map<String, GameProfile> getProfileMap() {
return Collections.unmodifiableMap(profiles);
}
public void setDlcOverride(String type, Dlc target) {
if(target == null) {
dlcOverrides.remove(type);
} else {
dlcOverrides.put(type, target);
}
}
public void removeDlcOverride(String type) {
dlcOverrides.remove(type);
}
public boolean hasDlcOverride(String type) {
return dlcOverrides.containsKey(type);
}
public Dlc getDlcOverride(String type) {
return dlcOverrides.get(type);
}
}

View File

@@ -1,7 +1,11 @@
package entralinked.network.http.dashboard;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
@@ -15,6 +19,7 @@ import javax.imageio.ImageIO;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -30,8 +35,11 @@ import entralinked.model.player.Player;
import entralinked.model.player.PlayerManager;
import entralinked.model.player.PlayerStatus;
import entralinked.network.http.HttpHandler;
import entralinked.utility.ColorUtility;
import entralinked.utility.Crc16;
import entralinked.utility.GsidUtility;
import entralinked.utility.TiledImageReader;
import entralinked.utility.LEOutputStream;
import entralinked.utility.TiledImageUtility;
import io.javalin.Javalin;
import io.javalin.config.JavalinConfig;
import io.javalin.http.Context;
@@ -45,7 +53,7 @@ import io.javalin.json.JavalinJackson;
public class DashboardHandler implements HttpHandler {
private static final Logger logger = LogManager.getLogger();
private final Map<Dlc, BufferedImage> skinPreviewCache = new HashMap<>();
private final Map<Object, BufferedImage> skinPreviewCache = new HashMap<>();
private final DlcList dlcList;
private final PlayerManager playerManager;
@@ -57,18 +65,52 @@ public class DashboardHandler implements HttpHandler {
logger.info("Loading C-Gear and Pokédex skin previews ...");
List<Dlc> skins = dlcList.getDlcList(dlc -> dlc.type().startsWith("CGEAR") || dlc.type().equals("ZUKAN"));
// Cache default skins
for(Dlc skin : skins) {
try(FileInputStream inputStream = new FileInputStream(skin.path())) {
BufferedImage image =
skin.type().equals("ZUKAN") ? TiledImageReader.readDexSkin(inputStream) :
skin.type().equals("CGEAR") ? TiledImageReader.readCGearSkin(inputStream, true) :
TiledImageReader.readCGearSkin(inputStream, false); // CGEAR2
BufferedImage image = skin.type().equals("ZUKAN") ? TiledImageUtility.readDexSkin(inputStream, true)
: skin.type().equals("CGEAR") ? TiledImageUtility.readCGearSkin(inputStream, true)
: TiledImageUtility.readCGearSkin(inputStream, false); // CGEAR2
skinPreviewCache.put(skin, image);
} catch(IOException | IndexOutOfBoundsException e) {
logger.error("Could not load image for skin {} of type {}", skin.name(), skin.type(), e);
}
}
// Cache custom skins for each player
for(Player player : playerManager.getPlayers()) {
File cgearSkinFile = player.getCGearSkinFile();
File dexSkinFile = player.getDexSkinFile();
// Cache custom C-Gear skin preview if it exists
if(cgearSkinFile.exists()) {
try(FileInputStream inputStream = new FileInputStream(cgearSkinFile)) {
boolean version2 = player.getGameVersion().isVersion2();
// Read the C-Gear skin image
BufferedImage image = TiledImageUtility.readCGearSkin(inputStream, !version2);
// Cache the result
skinPreviewCache.put("%s/%s".formatted(player.getGameSyncId(), version2 ? "CGEAR2" : "CGEAR"), image);
} catch(IOException | IndexOutOfBoundsException e) {
logger.error("Could not load custom C-Gear skin preview for player {}", player.getGameSyncId(), e);
}
}
// Cache custom Pokédex skin preview if it exists
if(dexSkinFile.exists()) {
try(FileInputStream inputStream = new FileInputStream(dexSkinFile)) {
// Read the Pokédex skin image
BufferedImage image = TiledImageUtility.readDexSkin(inputStream, true);
// Cache the result
skinPreviewCache.put("%s/ZUKAN".formatted(player.getGameSyncId()), image);
} catch(IOException | IndexOutOfBoundsException e) {
logger.error("Could not load custom Pokédex skin preview for player {}", player.getGameSyncId(), e);
}
}
}
logger.info("Cached {} skin previews", skinPreviewCache.size());
}
@@ -77,6 +119,7 @@ public class DashboardHandler implements HttpHandler {
javalin.get("/dashboard/previewskin", this::handlePreviewSkin);
javalin.get("/dashboard/dlc", this::handleRetrieveDlcList);
javalin.get("/dashboard/profile", this::handleRetrieveProfile);
javalin.post("/dashboard/uploadskin", this::handleUploadSkin);
javalin.post("/dashboard/profile", this::handleUpdateProfile);
javalin.post("/dashboard/login", this::handleLogin);
javalin.post("/dashboard/logout", this::handleLogout);
@@ -111,15 +154,38 @@ public class DashboardHandler implements HttpHandler {
// Make sure query parameters are present
if(type == null || name == null) {
ctx.status(404);
ctx.status(HttpStatus.NOT_FOUND);
return;
}
// Handle custom skin preview
if(name.equals("custom")) {
Player player = ctx.sessionAttribute("player");
// Check if player exists
if(player == null) {
ctx.status(HttpStatus.NOT_FOUND);
return;
}
BufferedImage previewImage = skinPreviewCache.get("%s/%s".formatted(player.getGameSyncId(), type));
// Check if preview image exists
if(previewImage == null) {
ctx.status(HttpStatus.NOT_FOUND);
return;
}
ImageIO.write(previewImage, "png", ctx.outputStream());
return;
}
// Handle DLC skin preview
Dlc dlc = dlcList.getDlc("IRAO", type, name);
// Check if DLC exists
if(dlc == null) {
ctx.status(404);
ctx.status(HttpStatus.NOT_FOUND);
return;
}
@@ -188,7 +254,124 @@ public class DashboardHandler implements HttpHandler {
// Store session attribute and send response
ctx.sessionAttribute("player", player);
ctx.json(new DashboardStatusMessage("ok")); // heh
ctx.json(Collections.EMPTY_MAP);
}
/**
* POST request handler for {@code /dashboard/uploadskin}
*/
private void handleUploadSkin(Context ctx) throws IOException {
Player player = ctx.sessionAttribute("player");
// Check if player exists
if(player == null) {
ctx.json(new DashboardStatusMessage("Unauthorized", true));
ctx.status(HttpStatus.UNAUTHORIZED);
return;
}
String type = ctx.queryParam("type");
String fileName = ctx.queryParam("filename");
// Check if type & file name are present and are valid
if(type == null || fileName == null ||
(!type.equals("CGEAR") && !type.equals("CGEAR2") && !type.equals("ZUKAN"))) {
ctx.status(HttpStatus.BAD_REQUEST);
return;
}
try {
BufferedImage image = ImageIO.read(ctx.bodyInputStream());
BufferedImage previewImage = null;
// Make sure file is a valid image
if(image == null) {
ctx.json(new DashboardStatusMessage("File is either not an image or uses an unsupported format.", true));
return;
}
// Make sure image has the correct dimensions
if(image.getWidth() != 256 || image.getHeight() != 192) {
ctx.json(new DashboardStatusMessage("Image must be 256 x 192 pixels.", true));
return;
}
File outputFile = type.equals("ZUKAN") ? player.getDexSkinFile() : player.getCGearSkinFile();
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
byte[] skinBytes = null;
// Write skin data to buffer and generate a preview image
switch(type) {
case "CGEAR":
case "CGEAR2":
boolean offsetIndices = type.equals("CGEAR");
TiledImageUtility.writeCGearSkin(byteOutputStream, image, offsetIndices);
skinBytes = byteOutputStream.toByteArray();
previewImage = TiledImageUtility.readCGearSkin(new ByteArrayInputStream(skinBytes), offsetIndices);
break;
case "ZUKAN":
// Generate some background colors based roughly on what's in the image
int[] backgroundColors = Arrays.copyOf(TiledImageUtility.DEFAULT_DEX_BACKGROUND_COLORS, 64);
int backgroundColor = image.getRGB(0, 0);
int dexColor = image.getRGB(0, 134);
int buttonColor = image.getRGB(128, 134);
// Background colors (58, 59, 60)
for(int i = 0; i < 3; i++) {
backgroundColors[i + 58] = ColorUtility.multiplyColor(backgroundColor, 0.7 + i * 0.15);
}
// Pokédex colors (48, 49, 50, 51, 52)
for(int i = 0; i < 5; i++) {
backgroundColors[i + 48] = ColorUtility.multiplyColor(dexColor, 1.15 - i * 0.15);
}
// Pokédex button colors (53, 54, 55, 56)
for(int i = 0; i < 4; i++) {
backgroundColors[i + 53] = ColorUtility.multiplyColor(buttonColor, 0.55 + i * 0.15);
}
// Process skin data
TiledImageUtility.writeDexSkin(byteOutputStream, image, backgroundColors);
skinBytes = byteOutputStream.toByteArray();
previewImage = TiledImageUtility.readDexSkin(new ByteArrayInputStream(skinBytes), true);
break;
}
// Write custom skin to output file
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(outputFile))) {
outputStream.write(skinBytes);
outputStream.writeShort(Crc16.calc(skinBytes));
}
// Cache preview image
skinPreviewCache.put("%s/%s".formatted(player.getGameSyncId(), type), previewImage);
} catch(IOException e) {
logger.error("An error occured while processing custom skin data", e);
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR);
return;
} catch(IllegalArgumentException e) {
// Silently discard exception, only send feedback to the client
ctx.json(new DashboardStatusMessage(e.getMessage(), true));
return;
}
// Update player information
if(type.equals("ZUKAN")) {
player.setDexSkin("custom");
player.setCustomDexSkin(fileName);
} else {
player.setCGearSkin("custom");
player.setCustomCGearSkin(fileName);
}
// Try to save player data
if(!playerManager.savePlayer(player)) {
ctx.json(new DashboardStatusMessage("Profile data could not be saved due to an error.", true));
return;
}
ctx.json(Collections.EMPTY_MAP);
}
/**

View File

@@ -15,6 +15,8 @@ public record DashboardProfileMessage(
String cgearSkin,
String dexSkin,
String musical,
String customCGearSkin,
String customDexSkin,
int levelsGained,
Collection<DreamEncounter> encounters,
Collection<DreamItem> items,
@@ -22,7 +24,8 @@ public record DashboardProfileMessage(
public DashboardProfileMessage(String dreamerSprite, Player player) {
this(player.getGameVersion().getDisplayName(), dreamerSprite, player.getDreamerInfo(),
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getLevelsGained(),
player.getEncounters(), player.getItems(), player.getAvenueVisitors());
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
player.getAvenueVisitors());
}
}

View File

@@ -2,6 +2,7 @@ package entralinked.network.http.dls;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -9,6 +10,7 @@ import entralinked.Entralinked;
import entralinked.model.dlc.Dlc;
import entralinked.model.dlc.DlcList;
import entralinked.model.user.ServiceSession;
import entralinked.model.user.User;
import entralinked.model.user.UserManager;
import entralinked.network.http.HttpHandler;
import entralinked.network.http.HttpRequestHandler;
@@ -59,6 +61,9 @@ public class DlsHandler implements HttpHandler {
default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action());
};
// Store user attribute for subsequent handlers
ctx.attribute("user", session.user());
// Handle the request
handler.process(request, ctx);
}
@@ -67,9 +72,16 @@ public class DlsHandler implements HttpHandler {
* POST handler for {@code /download action=list}
*/
private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException {
User user = ctx.attribute("user");
String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType());
// If an overriding DLC is present, send the data for that instead.
if(user.hasDlcOverride(type)) {
ctx.result(dlcList.getDlcListString(List.of(user.getDlcOverride(type))));
return;
}
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
}
@@ -78,9 +90,10 @@ public class DlsHandler implements HttpHandler {
* POST handler for {@code /download action=contents}
*/
private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException {
User user = ctx.attribute("user");
String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType());
Dlc dlc = dlcList.getDlc(gameCode, type, request.dlcName());
Dlc dlc = user.hasDlcOverride(type) ? user.getDlcOverride(type) : dlcList.getDlc(gameCode, type, request.dlcName());
// Check if the requested DLC exists
if(dlc == null) {

View File

@@ -117,6 +117,14 @@ public class NasHandler implements HttpHandler {
* POST handler for {@code /ac action=SVCLOC}
*/
private void handleRetrieveServiceLocation(NasRequest request, Context ctx) throws IOException {
// Authenticate user
User user = userManager.authenticateUser(request.userId(), request.password());
if(user == null) {
result(ctx, NasReturnCode.USER_NOT_FOUND);
return;
}
// Determine service location from type
String service = switch(request.serviceType()) {
case "0000" -> "external"; // External game-specific service
@@ -125,7 +133,7 @@ public class NasHandler implements HttpHandler {
};
// Prepare user credentials
ServiceCredentials credentials = userManager.createServiceSession(null, service, null);
ServiceCredentials credentials = userManager.createServiceSession(user, service, null);
result(ctx, new NasServiceLocationResponse(true, service, credentials.authToken()));
}

View File

@@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Configuration;
import entralinked.Entralinked;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.dlc.Dlc;
import entralinked.model.dlc.DlcList;
import entralinked.model.pkmn.PkmnInfo;
import entralinked.model.pkmn.PkmnInfoReader;
@@ -26,6 +27,7 @@ import entralinked.model.player.Player;
import entralinked.model.player.PlayerManager;
import entralinked.model.player.PlayerStatus;
import entralinked.model.user.ServiceSession;
import entralinked.model.user.User;
import entralinked.model.user.UserManager;
import entralinked.network.http.HttpHandler;
import entralinked.network.http.HttpRequestHandler;
@@ -108,8 +110,9 @@ public class PglHandler implements HttpHandler {
return;
}
// Store request object for subsequent handlers
// Store attributes for subsequent handlers
ctx.attribute("request", request);
ctx.attribute("user", session.user());
}
/**
@@ -142,7 +145,7 @@ public class PglHandler implements HttpHandler {
writeStatusCode(outputStream, 1); // Unauthorized
return;
}
// Create bitlist
byte[] bitlist = new byte[128]; // TODO pool? maybe just cache
@@ -187,6 +190,7 @@ public class PglHandler implements HttpHandler {
private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOException {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
Player player = playerManager.getPlayer(request.gameSyncId());
User user = ctx.attribute("user");
// Check if player exists
if(player == null) {
@@ -207,6 +211,33 @@ public class PglHandler implements HttpHandler {
List<DreamEncounter> encounters = player.getEncounters();
List<DreamItem> items = player.getItems();
// Prepare DLC information
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
String cgearSkin = player.getCGearSkin();
String dexSkin = player.getDexSkin();
int cgearSkinIndex = 0;
int dexSkinIndex = 0;
// Create or remove custom C-Gear skin DLC override
if("custom".equals(cgearSkin)) {
cgearSkinIndex = 1;
user.setDlcOverride(cgearType, new Dlc(player.getCGearSkinFile().getAbsolutePath(),
"custom", "IRAO", cgearType, cgearSkinIndex, 9730, 0, true));
} else {
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
user.removeDlcOverride(cgearType);
}
// Create or remove custom Pokédex skin DLC override
if("custom".equals(dexSkin)) {
dexSkinIndex = 1;
user.setDlcOverride("ZUKAN", new Dlc(player.getDexSkinFile().getAbsolutePath(),
"custom", "IRAO", "ZUKAN", dexSkinIndex, 25090, 0, true));
} else {
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
user.removeDlcOverride("ZUKAN");
}
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
// and no content will be downloaded.
@@ -229,8 +260,8 @@ public class PglHandler implements HttpHandler {
outputStream.writeShort(player.getLevelsGained());
outputStream.write(0); // Unknown
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
outputStream.write(dlcList.getDlcIndex("IRAO", player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR", player.getCGearSkin()));
outputStream.write(dlcList.getDlcIndex("IRAO", "ZUKAN", player.getDexSkin()));
outputStream.write(cgearSkinIndex);
outputStream.write(dexSkinIndex);
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
outputStream.write(0); // Must be zero?
@@ -326,7 +357,7 @@ public class PglHandler implements HttpHandler {
}
// Check if the game save data exists
File file = playerManager.getPlayerGameSaveFile(player);
File file = player.getSaveFile();
if(!file.exists()) {
writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID
@@ -419,7 +450,7 @@ public class PglHandler implements HttpHandler {
// Read save data
PkmnInfo dreamerInfo = null;
try(FileInputStream inputStream = new FileInputStream(playerManager.getPlayerGameSaveFile(player))) {
try(FileInputStream inputStream = new FileInputStream(player.getSaveFile())) {
inputStream.skip(0x1D300); // Skip to dream world data
inputStream.skip(8); // Skip to Pokémon data
dreamerInfo = PkmnInfoReader.readPokeInfo(inputStream);

View File

@@ -0,0 +1,48 @@
package entralinked.utility;
/**
* Utility class for various color-related shenanigans.
*/
public class ColorUtility {
/**
* Multiplies the red, green and blue components of the specified RGB888 color value by the given factor.
*
* @param color The color to multiply.
* @param factor The factor to use.
* @return The multiplied color.
*/
public static int multiplyColor(int color, double factor) {
int red = color >> 16 & 0xFF;
int green = color >> 8 & 0xFF;
int blue = color & 0xFF;
red = Math.max(0, Math.min(255, (int)(red * factor)));
green = Math.max(0, Math.min(255, (int)(green * factor)));
blue = Math.max(0, Math.min(255, (int)(blue * factor)));
return (red << 16) | (green << 8) | blue;
}
/**
* Converts the input BGR555 color value to an RGB888 color value.
*
* @return The RGB888 color value.
*/
public static int convertBGR555ToRGB888(int color) {
int red = (color & 0x1F) << 3;
int green = ((color & 0x3E0) >> 5) << 3;
int blue = ((color & 0x7C00) >> 10) << 3;
return (red << 16) | (green << 8) | blue;
}
/**
* Converts the input RGB888 color value to a BGR555 color value.
*
* @return The BGR555 color value.
*/
public static int convertRGB888ToBGR555(int color) {
int red = color >> 16 & 0xFF;
int green = color >> 8 & 0xFF;
int blue = color & 0xFF;
return (red >> 3) | (green >> 3 << 5) | (blue >> 3 << 10);
}
}

View File

@@ -0,0 +1,51 @@
package entralinked.utility;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Like {@link LEOutputStream}, but {@link InputStream}.
*/
public class LEInputStream extends FilterInputStream {
protected final byte[] buffer = new byte[8];
public LEInputStream(InputStream inputStream) {
super(inputStream);
}
public short readShort() throws IOException {
in.read(buffer, 0, 2);
return (short)(buffer[0] & 0xFF
| ((buffer[1] & 0xFF) << 8));
}
public int readInt() throws IOException {
in.read(buffer, 0, 4);
return buffer[0] & 0xFF
| ((buffer[1] & 0xFF) << 8)
| ((buffer[2] & 0xFF) << 16)
| ((buffer[3] & 0xFF) << 24);
}
public double readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
public long readLong() throws IOException {
in.read(buffer, 0, 8);
return buffer[0] & 0xFF
| ((buffer[1] & 0xFF) << 8)
| ((buffer[2] & 0xFF) << 16)
| ((long)(buffer[3] & 0xFF) << 24)
| ((long)(buffer[4] & 0xFF) << 32)
| ((long)(buffer[5] & 0xFF) << 40)
| ((long)(buffer[6] & 0xFF) << 48)
| ((long)(buffer[7] & 0xFF) << 56);
}
public double readDouble() throws IOException {
return Double.longBitsToDouble(readLong());
}
}

View File

@@ -1,130 +0,0 @@
package entralinked.utility;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
/**
* Utility class for reading tiled images (C-Gear & Pokédex skin data) into a usable {@link BufferedImage}.
*/
public class TiledImageReader {
public static final int TILE_WIDTH = 8;
public static final int TILE_HEIGHT = 8;
public static final int TILE_SIZE = TILE_WIDTH * TILE_HEIGHT;
public static final int COLOR_PALETTE_SIZE = 16;
public static final int SCREEN_WIDTH = 256;
public static final int SCREEN_HEIGHT = 192;
public static final int SCREEN_TILE_COUNT = SCREEN_WIDTH * SCREEN_HEIGHT / TILE_SIZE;
/**
* Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 255.
*
* @param normalizeIndices Should be {@code true} if the provided C-Gear skin data is from the original Black & White.
* @return A {@link BufferedImage} representing the read C-Gear skin data.
*/
public static BufferedImage readCGearSkin(InputStream inputStream, boolean normalizeIndices) throws IOException {
return readTiledImage(inputStream, 255, normalizeIndices);
}
/**
* Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 768 and index normalization disabled.
*
* @return A {@link BufferedImage} representing the read Pokédex skin data.
*/
public static BufferedImage readDexSkin(InputStream inputStream) throws IOException {
return readTiledImage(inputStream, 768, false);
}
/**
* Reads tiled image data from the provided {@link InputStream} and returns a {@link BufferedImage} representing the read image data.
*
* @param tileCount The number of tiles to be read. This should be equal to the maximum number of tiles for this image.
* @param normalizedIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be normalized.
* @return A {@link BufferedImage} representing the read image data.
*/
public static BufferedImage readTiledImage(InputStream inputStream, int tileCount, boolean normalizeIndices) throws IOException {
BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); // Result image
int[] tileData = new int[tileCount * TILE_SIZE];
int[] tileIndices = new int[tileCount]; // Tile index lookup table
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
// Read tile data.
for(int i = 0; i < tileCount; i++) {
for(int j = 0; j < TILE_SIZE / 2; j++) {
int paletteIndices = inputStream.read(); // Contains color palette indices for 2 adjacent pixels.
tileData[i * TILE_SIZE + j * 2] = paletteIndices & (COLOR_PALETTE_SIZE - 1);
tileData[i * TILE_SIZE + j * 2 + 1] = (paletteIndices >> 4) & (COLOR_PALETTE_SIZE - 1);
}
// Store the index of the tile as it would be in memory so we can look it up later when we're mapping the tiles.
tileIndices[i] = normalizeIndices ? i + i / 17 * 15 + 0xA0A0 : i;
}
// Read color data.
// Pokédex skins contain room for 240 extra colors, 64 of which are defined and appear to be used as the
// 'background' colors in cases where the skin is only an overlay that is displayed on top of the 'true' Pokédex.
for(int i = 0; i < COLOR_PALETTE_SIZE; i++) {
int color = inputStream.read() | inputStream.read() << 8;
// Convert BGR555 to RGB888
int red = (color & 0x1F) << 3;
int green = ((color & 0x3E0) >> 5) << 3;
int blue = ((color & 0x7C00) >> 10) << 3;
colorPalette[i] = (red << 16) | (green << 8) | blue;
}
// Map tiles to the resulting image.
// In cases where the tile count is 768 or greater, which is exactly enough tiles to fill the entire screen,
// the tiles will be applied in the order they are provided and no additional mapping data will be read.
// This is always the case for Pokédex skins, and never the case for C-Gear skins.
if(tileCount < SCREEN_TILE_COUNT) {
// Not enough tiles -- read additional mapping data to figure out their placement.
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_HEIGHT;
int leftBits = inputStream.read();
int rightBits = inputStream.read();
int memoryIndex = leftBits | (rightBits & ~12) << 8;
int flipBits = rightBits & 12;
// The normalized tile index is the index of the in-memory tile index in the lookup table.
int tileIndex = 0;
for(int k = 0; k < tileIndices.length; k++) {
if(memoryIndex == tileIndices[k]) {
tileIndex = k;
break;
}
}
// Apply the pixels of this tile to the resulting image.
for(int j = 0; j < TILE_SIZE; j++) {
// Get the color index for this pixel based on how the tile is flipped.
int tilePixelIndex = switch(flipBits) {
case 4 -> (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) - j % TILE_WIDTH - 1; // Flip horizontally
case 8 -> TILE_SIZE - (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) + j % TILE_WIDTH; // Flip vertically
case 12 -> TILE_SIZE - j - 1; // Flip horizontally & vertically
default -> j; // Don't flip
};
// Finally, set the pixel!
int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex];
image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[paletteIndex]);
}
}
} else {
// There are enough tiles to fill up the entire screen, so let's just place them in order.
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
for(int j = 0; j < TILE_SIZE; j++) {
image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[tileData[i * TILE_SIZE + j]]);
}
}
}
return image;
}
}

View File

@@ -0,0 +1,378 @@
package entralinked.utility;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Utility class for reading/writing tiled images to/from {@link BufferedImage} objects.
*/
public class TiledImageUtility {
public static final int[] DEFAULT_DEX_BACKGROUND_COLORS = {
0xE8E8E8, 0xE0E0E0, 0xD8D8D8, 0xD0D0D0, 0xD0C8C8, 0xC8C8C8, 0xC0C0C0, 0xB8B8B8, 0xB0B0B0, 0xA8A8A8, 0xA0A0A0,
0xA0A098, 0x989898, 0x909090, 0x888888, 0x808080, 0x787878, 0x707070, 0x686868, 0x606060, 0x585858, 0x505050,
0x484850, 0x484848, 0x484840, 0x404040, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0x282828,
0x202020, 0x181818, 0x101010, 0xF800F8, 0x303030, 0x404040, 0x505050, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8,
0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF870A0, 0xF84878, 0xC03860, 0x802848, 0x482030, 0x505858, 0x909090,
0xC8C8C8, 0xE0E8E8, 0xF800F8, 0x404848, 0x707878, 0xA8B0B0, 0xF800F8, 0xF800F8, 0xF800F8}; // Unsafe
public static final int TILE_WIDTH = 8;
public static final int TILE_HEIGHT = 8;
public static final int TILE_SIZE = TILE_WIDTH * TILE_HEIGHT;
public static final int COLOR_PALETTE_SIZE = 16;
public static final int SCREEN_WIDTH = 256;
public static final int SCREEN_HEIGHT = 192;
public static final int SCREEN_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT;
public static final int SCREEN_TILE_COUNT = SCREEN_SIZE / TILE_SIZE;
private static final byte[] dexBackgroundColorIndices = new byte[SCREEN_SIZE];
private static final boolean[] dexBackgroundOverrides = new boolean[SCREEN_SIZE];
private static final Logger logger = LogManager.getLogger();
static {
// Load Pokédex skin background data
try(LEInputStream inputStream = new LEInputStream(TiledImageUtility.class.getResourceAsStream("/zukan.bin"))) {
int index = 0;
int pairs = inputStream.readShort();
// Read background color indices
for(int i = 0; i < pairs; i++) {
int amount = inputStream.read();
int value = inputStream.read();
for(int j = 0; j < amount; j++) {
dexBackgroundColorIndices[index++] = (byte)(value & 63);
}
}
index = 0;
pairs = inputStream.readShort();
// Read background override bits
for(int i = 0; i < pairs; i++) {
int amount = inputStream.readShort();
int value = inputStream.read();
for(int j = 0; j < amount; j++) {
for(int k = 0; k < 8; k++) {
dexBackgroundOverrides[index++] = (value & 1 << k) != 0;
}
}
}
} catch(IOException e) {
logger.error("Could not load Pokédex background data", e);
}
}
/**
* Reads a C-Gear skin from the specified {@link InputStream}.
*
* @param inputStream The input stream to read from.
* @param normalizeIndices Should be {@code true} if the provided C-Gear skin data is from the original Black & White.
* @return A {@link BufferedImage} representing the read C-Gear skin data.
*/
public static BufferedImage readCGearSkin(InputStream inputStream, boolean normalizeIndices) throws IOException {
return readTiledImage(new LEInputStream(inputStream), 255, 0, null, null, normalizeIndices);
}
/**
* Reads a Pokédex skin from the specified {@link InputStream}.
* If the skin in question is an overlay, the Pokédex background will be applied to the resulting image automatically.
*
* @param inputStream The input stream to read from.
* @param applyBackground Whether to apply the Pokédex background to the result image, or read the data as-is.
* @return A {@link BufferedImage} representing the read Pokédex skin data.
*/
public static BufferedImage readDexSkin(InputStream inputStream, boolean applyBackground) throws IOException {
return readTiledImage(new LEInputStream(inputStream), 768, 64,
applyBackground ? dexBackgroundColorIndices : null, applyBackground ? dexBackgroundOverrides : null, false);
}
/**
* Reads tiled image data from the provided {@link InputStream} and returns a {@link BufferedImage} representing the read image data.
*
* @param inputStream The input stream to read from.
* @param tileCount The number of tiles to be read. This should be equal to the maximum number of tiles for this image.
* @param backgroundColorCount The number of background colors this image has.
* @param backgroundColorIndices The background color indices of the background image. If {@code null}, no background will be used.
* @param backgroundOverrides An array of booleans representing pixels that forcibly use the background color. Can be {@code null}.
* @param normalizedIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be normalized.
* @return A {@link BufferedImage} representing the read image data.
*/
private static BufferedImage readTiledImage(LEInputStream inputStream, int tileCount, int backgroundColorCount,
byte[] backgroundColorIndices, boolean[] backgroundOverrides, boolean normalizeIndices) throws IOException {
BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); // Result image
int[] tileData = new int[tileCount * TILE_SIZE];
int[] tileIndices = new int[tileCount]; // Tile index lookup table
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
int[] backgroundColorPalette = new int[backgroundColorCount];
// Read tile data.
for(int i = 0; i < tileCount; i++) {
byte[] rawTileData = inputStream.readNBytes(TILE_SIZE / 2);
for(int j = 0; j < rawTileData.length; j++) {
int paletteIndices = rawTileData[j]; // Contains color palette indices for 2 adjacent pixels.
tileData[i * TILE_SIZE + j * 2] = paletteIndices & (COLOR_PALETTE_SIZE - 1);
tileData[i * TILE_SIZE + j * 2 + 1] = (paletteIndices >> 4) & (COLOR_PALETTE_SIZE - 1);
}
// Store the index of the tile as it would be in memory so we can look it up later when we're mapping the tiles.
tileIndices[i] = normalizeIndices ? i + i / 17 * 15 + 0xA0A0 : i;
}
// Read foreground color data.
// In cases where background colors are present, pixels that use the *first* foreground color
// will be replaced by the background color at that pixel's location.
for(int i = 0; i < COLOR_PALETTE_SIZE; i++) {
int color = inputStream.readShort();
// The game seems to always replace the first color of the foreground with the background,
// so let's just set it to black so that C-Gear skin previews are more accurate.
// This won't do anything for Pokédex skins, as they use a special background color palette.
colorPalette[i] = i == 0 ? 0 : ColorUtility.convertBGR555ToRGB888(color);
}
// Read background color data.
// Pokédex skins contain room for 240 extra colors, 64 of which are defined and appear to be used as the
// 'background' colors in cases where the skin is only an overlay that is displayed on top of the 'true' Pokédex.
for(int i = 0; i < backgroundColorCount; i++) {
backgroundColorPalette[i] = ColorUtility.convertBGR555ToRGB888(inputStream.readShort());
}
// Map tiles to the resulting image.
// In cases where the tile count is 768 or greater, which is exactly enough tiles to fill the entire screen,
// the tiles will be applied in the order they are provided and no additional mapping data will be read.
// This is always the case for Pokédex skins, and never the case for C-Gear skins.
if(tileCount < SCREEN_TILE_COUNT) {
// Not enough tiles -- read additional mapping data to figure out their placement.
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_HEIGHT;
int leftBits = inputStream.read();
int rightBits = inputStream.read();
int memoryIndex = leftBits | (rightBits & ~12) << 8;
int flipBits = rightBits & 12;
// The normalized tile index is the index of the in-memory tile index in the lookup table.
int tileIndex = -1;
for(int k = 0; k < tileIndices.length; k++) {
if(memoryIndex == tileIndices[k]) {
tileIndex = k;
break;
}
}
// If no tile could be found, then just skip it.
// TODO It is possible that it uses background colors in this case, but just leaving the tile black should be good enough.
if(tileIndex == -1) {
continue;
}
// Apply the pixels of this tile to the resulting image.
for(int j = 0; j < TILE_SIZE; j++) {
// Get the color index for this pixel based on how the tile is flipped.
int tilePixelIndex = switch(flipBits) {
case 4 -> (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) - j % TILE_WIDTH - 1; // Flip horizontally
case 8 -> TILE_SIZE - (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) + j % TILE_WIDTH; // Flip vertically
case 12 -> TILE_SIZE - j - 1; // Flip horizontally & vertically
default -> j; // Don't flip
};
int pixelX = x + j % TILE_WIDTH;
int pixelY = y + j / TILE_WIDTH;
int pixelIndex = pixelY * SCREEN_WIDTH + pixelX;
int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex];
// Determine whether the foreground or background color should be used.
int color = backgroundColorIndices == null ||
(paletteIndex > 0 && (backgroundOverrides == null || !backgroundOverrides[pixelIndex]))
? colorPalette[paletteIndex] : backgroundColorPalette[backgroundColorIndices[pixelIndex]];
// Finally, set the pixel!
image.setRGB(pixelX, pixelY, color);
}
}
} else {
// There are enough tiles to fill up the entire screen, so let's just place them in order.
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
for(int j = 0; j < TILE_SIZE; j++) {
int pixelX = x + j % TILE_WIDTH;
int pixelY = y + j / TILE_WIDTH;
int pixelIndex = pixelY * SCREEN_WIDTH + pixelX;
int paletteIndex = tileData[i * TILE_SIZE + j];
int color = backgroundColorIndices == null ||
(paletteIndex > 0 && (backgroundOverrides == null || !backgroundOverrides[pixelIndex]))
? colorPalette[paletteIndex] : backgroundColorPalette[backgroundColorIndices[pixelIndex]];
image.setRGB(pixelX, pixelY, color);
}
}
}
return image;
}
/**
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a C-Gear skin.
*
* @param outputStream The output stream to write to.
* @param image The image to write.
* @param offsetIndices Should be {@code true} if the C-Gear skin is intended for use with the original Black & White games.
*/
public static void writeCGearSkin(OutputStream outputStream, BufferedImage image, boolean offsetIndices) throws IOException {
writeTiledImage(new LEOutputStream(outputStream), image, 255, null, offsetIndices);
}
/**
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a Pokédex skin.
*
* @param outputStream The output stream to write to.
* @param image The image to write.
* @param backgroundColors The background colors this skin will use in-game. Should contain 64 RGB888 color values.
*/
public static void writeDexSkin(OutputStream outputStream, BufferedImage image, int[] backgroundColors) throws IOException {
if(backgroundColors == null || backgroundColors.length != 64) {
throw new IllegalArgumentException("Background color array must contain 64 colors");
}
writeTiledImage(new LEOutputStream(outputStream), image, 768, backgroundColors, false);
outputStream.write(new byte[352]); // Extra data that writeTiledImage doesn't write
}
/**
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a tiled image.
*
* @param outputStream The output stream to write to.
* @param image The image to write.
* @param tileCount The maximum number of unique tiles that can be used.
* @param backgroundColors Additional background colors that the game might use. Can be {@code null}.
* @param offsetIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be offset.
*/
private static void writeTiledImage(LEOutputStream outputStream, BufferedImage image,
int tileCount, int[] backgroundColors, boolean offsetTileIndices) throws IOException {
int[] tileData = new int[TILE_SIZE * tileCount]; // Stores colors, NOT indices!
int[] tileMappingData = new int[SCREEN_TILE_COUNT];
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
int currentTileCount = 0;
int currentColorCount = 0;
// Create tile data
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
// If tile count is limited, check if we can replicate this tile by flipping an existing tile.
if(tileCount < SCREEN_TILE_COUNT) {
boolean duplicateTile = false;
for(int j = 0; j < currentTileCount; j++) {
boolean equal = true;
boolean flipX = true;
boolean flipY = true;
boolean flipXY = true;
for(int k = 0; k < TILE_SIZE; k++) {
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
int color = image.getRGB(x + k % TILE_WIDTH, y + k / TILE_WIDTH);
equal &= color == tileData[j * TILE_SIZE + k];
flipX &= color == tileData[j * TILE_SIZE + ((TILE_WIDTH * (k / TILE_WIDTH) + TILE_WIDTH) - k % TILE_WIDTH - 1)];
flipY &= color == tileData[j * TILE_SIZE + (TILE_SIZE - (TILE_WIDTH * (k / TILE_WIDTH) + TILE_WIDTH) + k % TILE_WIDTH)];
flipXY &= color == tileData[j * TILE_SIZE + (TILE_SIZE - k - 1)];
}
int flipBits = equal ? 0 : flipX ? 4 : flipY ? 8 : flipXY ? 12 : -1;
// If this tile is equal to an existing tile or can be replicated by flipping another tile,
// create mapping data for it and skip to the next tile.
if(flipBits != -1) {
tileMappingData[i] = (offsetTileIndices ? (j + (15 * (j / 17)) + 0xA0A0) : j) | flipBits << 8;
duplicateTile = true;
break;
}
}
// Skip to next tile if it is duplicate.
if(duplicateTile) {
continue;
}
}
// Throw exception if a new tile needs to be added but we have reached the limit.
if(currentTileCount >= tileCount) {
throw new IllegalArgumentException(
"Image is too complex. Only %s unique tiles (8x8 pixel sections) are permitted.".formatted(tileCount));
}
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
for(int j = 0; j < TILE_SIZE; j++) {
int pixelX = x + j % TILE_WIDTH;
int pixelY = y + j / TILE_WIDTH;
int color = image.getRGB(pixelX, pixelY);
int paletteIndex = indexOf(colorPalette, color);
// Add color to the palette if it hasn't been already.
if(paletteIndex == -1) {
// Throw exception if we have already reached the maximum amount of colors.
if(currentColorCount >= COLOR_PALETTE_SIZE) {
throw new IllegalArgumentException(
"Too many unique colors. Only %s unique colors are permitted.".formatted(COLOR_PALETTE_SIZE));
}
paletteIndex = currentColorCount++;
colorPalette[paletteIndex] = color;
}
tileData[currentTileCount * TILE_SIZE + j] = color;
}
// Create mapping data for this tile.
tileMappingData[i] = offsetTileIndices ? (currentTileCount + currentTileCount / 17 * 15 + 0xA0A0) : currentTileCount;
currentTileCount++;
}
// Write tile data.
for(int i = 0; i < tileData.length; i += 2) {
int paletteIndices = indexOf(colorPalette, tileData[i]) | (indexOf(colorPalette, tileData[i + 1]) << 4);
outputStream.write(paletteIndices);
}
// Write foreground color data.
for(int color : colorPalette) {
outputStream.writeShort(ColorUtility.convertRGB888ToBGR555(color));
}
// Write background color data if it is present.
if(backgroundColors != null) {
for(int color : backgroundColors) {
outputStream.writeShort(ColorUtility.convertRGB888ToBGR555(color));
}
}
// Write tile mapping data.
if(tileCount < SCREEN_TILE_COUNT) {
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
outputStream.writeShort(tileMappingData[i]);
}
}
}
/**
* @return The index of the element in the array, or {@code -1} if no such element exists.
*/
private static int indexOf(int[] array, int element) {
for(int i = 0; i < array.length; i++) {
if(element == array[i]) {
return i;
}
}
return -1;
}
}

View File

@@ -21,24 +21,26 @@
<tr>
<th>Species</th>
<td id="dreamer-species"></td>
<th>Nature</th>
<td id="dreamer-nature"></td>
<th>Ability</th>
<td id="dreamer-ability"></td>
</tr>
<tr>
<th>Name</th>
<td id="dreamer-name"></td>
<th>Gender</th>
<td id="dreamer-gender"></td>
<th>Nature</th>
<td id="dreamer-nature"></td>
</tr>
<tr>
<th>Trainer</th>
<td id="dreamer-trainer"></td>
<th>Level</th>
<td id="dreamer-level"></td>
<th>Gender</th>
<td id="dreamer-gender"></td>
</tr>
<tr>
<th>Trainer ID</th>
<td id="dreamer-trainer-id"></td>
<th>Level</th>
<td id="dreamer-level"></td>
</tr>
</table>
</div>
@@ -48,18 +50,18 @@
<div>
<table id="encounter-table" class="encounter-image-table">
<tr>
<td><a id="encounter0" href="#configureEncounter" onclick="configureEncounter(0)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter1" href="#configureEncounter" onclick="configureEncounter(1)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter2" href="#configureEncounter" onclick="configureEncounter(2)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter3" href="#configureEncounter" onclick="configureEncounter(3)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter4" href="#configureEncounter" onclick="configureEncounter(4)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td id="encounter0" onclick="configureEncounter(0)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter1" onclick="configureEncounter(1)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter2" onclick="configureEncounter(2)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter3" onclick="configureEncounter(3)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter4" onclick="configureEncounter(4)"><image src="/sprites/pokemon/normal/0.png"/></td>
</tr>
<tr>
<td><a id="encounter5" href="#configureEncounter" onclick="configureEncounter(5)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter6" href="#configureEncounter" onclick="configureEncounter(6)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter7" href="#configureEncounter" onclick="configureEncounter(7)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter8" href="#configureEncounter" onclick="configureEncounter(8)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td><a id="encounter9" href="#configureEncounter" onclick="configureEncounter(9)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
<td id="encounter5" onclick="configureEncounter(5)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter6" onclick="configureEncounter(6)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter7" onclick="configureEncounter(7)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter8" onclick="configureEncounter(8)"><image src="/sprites/pokemon/normal/0.png"/></td>
<td id="encounter9" onclick="configureEncounter(9)"><image src="/sprites/pokemon/normal/0.png"/></td>
</tr>
</table>
</div>
@@ -70,20 +72,20 @@
<div>
<table id="visitor-table" class="visitor-table">
<tr>
<td><a id="visitor0" href="#configureVisitor" onclick="configureVisitor(0)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor1" href="#configureVisitor" onclick="configureVisitor(1)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor2" href="#configureVisitor" onclick="configureVisitor(2)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor3" href="#configureVisitor" onclick="configureVisitor(3)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor4" href="#configureVisitor" onclick="configureVisitor(4)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor5" href="#configureVisitor" onclick="configureVisitor(5)"><image src="/sprites/trainers/none.png"/></a></td>
<td id="visitor0" onclick="configureVisitor(0)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor1" onclick="configureVisitor(1)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor2" onclick="configureVisitor(2)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor3" onclick="configureVisitor(3)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor4" onclick="configureVisitor(4)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor5" onclick="configureVisitor(5)"><image src="/sprites/trainers/none.png"/></td>
</tr>
<tr>
<td><a id="visitor6" href="#configureVisitor" onclick="configureVisitor(6)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor7" href="#configureVisitor" onclick="configureVisitor(7)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor8" href="#configureVisitor" onclick="configureVisitor(8)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor9" href="#configureVisitor" onclick="configureVisitor(9)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor10" href="#configureVisitor" onclick="configureVisitor(10)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor11" href="#configureVisitor" onclick="configureVisitor(11)"><image src="/sprites/trainers/none.png"/></a></td>
<td id="visitor6" onclick="configureVisitor(6)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor7" onclick="configureVisitor(7)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor8" onclick="configureVisitor(8)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor9" onclick="configureVisitor(9)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor10" onclick="configureVisitor(10)"><image src="/sprites/trainers/none.png"/></td>
<td id="visitor11" onclick="configureVisitor(11)"><image src="/sprites/trainers/none.png"/></td>
</tr>
</table>
</div>
@@ -94,28 +96,28 @@
<div>
<table id="item-table" class="item-table">
<tr>
<td><a id="item0" href="#configureItem" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item1" href="#configureItem" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item2" href="#configureItem" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item3" href="#configureItem" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item4" href="#configureItem" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item5" href="#configureItem" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item6" href="#configureItem" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item7" href="#configureItem" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item8" href="#configureItem" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item9" href="#configureItem" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></a></td>
<td id="item0" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></td>
<td id="item1" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></td>
<td id="item2" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></td>
<td id="item3" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></td>
<td id="item4" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></td>
<td id="item5" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></td>
<td id="item6" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></td>
<td id="item7" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></td>
<td id="item8" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></td>
<td id="item9" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></td>
</tr>
<tr>
<td><a id="item10" href="#configureItem" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item11" href="#configureItem" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item12" href="#configureItem" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item13" href="#configureItem" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item14" href="#configureItem" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item15" href="#configureItem" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item16" href="#configureItem" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item17" href="#configureItem" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item18" href="#configureItem" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></a></td>
<td><a id="item19" href="#configureItem" onclick="configureItem(19)"><image src="/sprites/items/0.png"/></a></td>
<td id="item10" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></td>
<td id="item11" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></td>
<td id="item12" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></td>
<td id="item13" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></td>
<td id="item14" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></td>
<td id="item15" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></td>
<td id="item16" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></td>
<td id="item17" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></td>
<td id="item18" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></td>
<td id="item19" onclick="configureItem(19)"><image src="/sprites/items/0.png"/></td>
</tr>
</table>
</div>
@@ -125,17 +127,21 @@
<div class="grid-container">
<div>
<label>C-Gear Skin | </label><a href="#" onclick="return previewSkin('cgear-skin', 'CGEAR')">Preview</a>
<select id="cgear-skin">
<select id="cgear-skin" style="margin-bottom:0px;">
<option value="none">Do not change</option>
<option disabled>──────────────────────</option>
</select>
<button onclick="openFileChooser('cgear-file-input')">Upload Custom</button>
<input id="cgear-file-input" type="file" accept=".png, .bmp, .jpg, .jpeg" onchange="uploadSkin('CGEAR', this.files[0])" onclick="this.value = null;" hidden/>
</div>
<div>
<label>Pokédex Skin | </label><a href="#" onclick="return previewSkin('dex-skin', 'ZUKAN')">Preview</a>
<select id="dex-skin">
<select id="dex-skin" style="margin-bottom:0px;">
<option value="none">Do not change</option>
<option disabled>──────────────────────</option>
</select>
<button onclick="openFileChooser('dex-file-input')">Upload Custom</button>
<input id="dex-file-input" type="file" accept=".png, .bmp, .jpg, .jpeg" onchange="uploadSkin('ZUKAN', this.files[0])" onclick="this.value = null;" hidden/>
</div>
<div>
<label>Musical Show</label>
@@ -157,7 +163,7 @@
</div>
</div>
<!-- Entree Forest Encounter Configuration Form -->
<div id="configureEncounter" class="popup">
<div id="encounter-config" class="popup">
<div class="content">
<button class="close-button" onclick="closeEncounterForm()">X</button>
<form id="encounter-form">
@@ -198,9 +204,9 @@
</div>
</div>
<!-- Item Configuration Form -->
<div id="configureItem" class="popup">
<div id="item-config" class="popup">
<div class="content">
<button class="close-button" onclick="closeEncounterForm()">X</button>
<button class="close-button" onclick="closeItemForm()">X</button>
<form id="item-form">
<label for="item-form-id">Item</label>
<select id="item-form-id" name="id" value="1">
@@ -214,7 +220,7 @@
</div>
</div>
<!-- Join Avenue Visitor Configuration Form -->
<div id="configureVisitor" class="popup">
<div id="visitor-config" class="popup">
<div class="content">
<button class="close-button" onclick="closeVisitorForm()">X</button>
<form id="visitor-form">

View File

@@ -1,7 +1,9 @@
const POKE_SPECIES_MAP = {};
const POKE_SPECIES_LIST = [];
const POKE_MOVE_MAP = {};
const POKE_MOVE_LIST = [];
const SPECIES_MAP = {};
const SPECIES_LIST = [];
const MOVE_MAP = {};
const MOVE_LIST = [];
const ABILITY_MAP = {};
const ABILITY_LIST = [];
const ITEM_MAP = {};
const ITEM_LIST = [];
const REGION_MAP = {};
@@ -38,12 +40,12 @@ const REGION_LIST = [];
{id: 26, name: "Raichu", downloadable: true},
{id: 27, name: "Sandshrew", downloadable: true},
{id: 28, name: "Sandslash", downloadable: true},
{id: 29, name: "Nidoran♀", downloadable: true},
{id: 30, name: "Nidorina", downloadable: true},
{id: 31, name: "Nidoqueen", downloadable: true},
{id: 32, name: "Nidoran♂", downloadable: true},
{id: 33, name: "Nidorino", downloadable: true},
{id: 34, name: "Nidoking", downloadable: true},
{id: 29, name: "Nidoran♀", downloadable: true, gender: "female"},
{id: 30, name: "Nidorina", downloadable: true, gender: "female"},
{id: 31, name: "Nidoqueen", downloadable: true, gender: "female"},
{id: 32, name: "Nidoran♂", downloadable: true, gender: "male"},
{id: 33, name: "Nidorino", downloadable: true, gender: "male"},
{id: 34, name: "Nidoking", downloadable: true, gender: "male"},
{id: 35, name: "Clefairy", downloadable: true},
{id: 36, name: "Clefable", downloadable: true},
{id: 37, name: "Vulpix", downloadable: true},
@@ -90,8 +92,8 @@ const REGION_LIST = [];
{id: 78, name: "Rapidash", downloadable: true},
{id: 79, name: "Slowpoke", downloadable: true},
{id: 80, name: "Slowbro", downloadable: true},
{id: 81, name: "Magnemite", downloadable: true},
{id: 82, name: "Magneton", downloadable: true},
{id: 81, name: "Magnemite", downloadable: true, gender: "unknown"},
{id: 82, name: "Magneton", downloadable: true, gender: "unknown"},
{id: 83, name: "Farfetch'd", downloadable: true},
{id: 84, name: "Doduo", downloadable: true},
{id: 85, name: "Dodrio", downloadable: true},
@@ -109,58 +111,58 @@ const REGION_LIST = [];
{id: 97, name: "Hypno", downloadable: true},
{id: 98, name: "Krabby", downloadable: true},
{id: 99, name: "Kingler", downloadable: true},
{id: 100, name: "Voltorb", downloadable: true},
{id: 101, name: "Electrode", downloadable: true},
{id: 100, name: "Voltorb", downloadable: true, gender: "unknown"},
{id: 101, name: "Electrode", downloadable: true, gender: "unknown"},
{id: 102, name: "Exeggcute", downloadable: true},
{id: 103, name: "Exeggutor", downloadable: true},
{id: 104, name: "Cubone", downloadable: true},
{id: 105, name: "Marowak", downloadable: true},
{id: 106, name: "Hitmonlee", downloadable: true},
{id: 107, name: "Hitmonchan", downloadable: true},
{id: 106, name: "Hitmonlee", downloadable: true, gender: "male"},
{id: 107, name: "Hitmonchan", downloadable: true, gender: "male"},
{id: 108, name: "Lickitung", downloadable: true},
{id: 109, name: "Koffing", downloadable: true},
{id: 110, name: "Weezing", downloadable: true},
{id: 111, name: "Rhyhorn", downloadable: true},
{id: 112, name: "Rhydon", downloadable: true},
{id: 113, name: "Chansey", downloadable: true},
{id: 113, name: "Chansey", downloadable: true, gender: "female"},
{id: 114, name: "Tangela", downloadable: true},
{id: 115, name: "Kangaskhan", downloadable: true},
{id: 115, name: "Kangaskhan", downloadable: true, gender: "female"},
{id: 116, name: "Horsea", downloadable: true},
{id: 117, name: "Seadra", downloadable: true},
{id: 118, name: "Goldeen", downloadable: true},
{id: 119, name: "Seaking", downloadable: true},
{id: 120, name: "Staryu", downloadable: true},
{id: 121, name: "Starmie", downloadable: true},
{id: 120, name: "Staryu", downloadable: true, gender: "unknown"},
{id: 121, name: "Starmie", downloadable: true, gender: "unknown"},
{id: 122, name: "Mr. Mime", downloadable: true},
{id: 123, name: "Scyther", downloadable: true},
{id: 124, name: "Jynx", downloadable: true},
{id: 124, name: "Jynx", downloadable: true, gender: "female"},
{id: 125, name: "Electabuzz", downloadable: true},
{id: 126, name: "Magmar", downloadable: true},
{id: 127, name: "Pinsir", downloadable: true},
{id: 128, name: "Tauros", downloadable: true},
{id: 128, name: "Tauros", downloadable: true, gender: "male"},
{id: 129, name: "Magikarp", downloadable: true},
{id: 130, name: "Gyarados", downloadable: true},
{id: 131, name: "Lapras", downloadable: true},
{id: 132, name: "Ditto", downloadable: true},
{id: 132, name: "Ditto", downloadable: true, gender: "unknown"},
{id: 133, name: "Eevee", downloadable: true},
{id: 134, name: "Vaporeon", downloadable: true},
{id: 135, name: "Jolteon", downloadable: true},
{id: 136, name: "Flareon", downloadable: true},
{id: 137, name: "Porygon", downloadable: true},
{id: 137, name: "Porygon", downloadable: true, gender: "unknown"},
{id: 138, name: "Omanyte", downloadable: true},
{id: 139, name: "Omastar", downloadable: true},
{id: 140, name: "Kabuto", downloadable: true},
{id: 141, name: "Kabutops", downloadable: true},
{id: 142, name: "Aerodactyl", downloadable: true},
{id: 143, name: "Snorlax", downloadable: true},
{id: 144, name: "Articuno", downloadable: true},
{id: 145, name: "Zapdos", downloadable: true},
{id: 146, name: "Moltres", downloadable: true},
{id: 144, name: "Articuno", downloadable: true, gender: "unknown"},
{id: 145, name: "Zapdos", downloadable: true, gender: "unknown"},
{id: 146, name: "Moltres", downloadable: true, gender: "unknown"},
{id: 147, name: "Dratini", downloadable: true},
{id: 148, name: "Dragonair", downloadable: true},
{id: 149, name: "Dragonite", downloadable: true},
{id: 150, name: "Mewtwo", downloadable: true},
{id: 151, name: "Mew", downloadable: true},
{id: 150, name: "Mewtwo", downloadable: true, gender: "unknown"},
{id: 151, name: "Mew", downloadable: true, gender: "unknown"},
{id: 152, name: "Chikorita", downloadable: true},
{id: 153, name: "Bayleef", downloadable: true},
{id: 154, name: "Meganium", downloadable: true},
@@ -210,7 +212,7 @@ const REGION_LIST = [];
{id: 198, name: "Murkrow", downloadable: true},
{id: 199, name: "Slowking", downloadable: true},
{id: 200, name: "Misdreavus", downloadable: true},
{id: 201, name: "Unown", downloadable: true, forms: [
{id: 201, name: "Unown", downloadable: true, gender: "unknown", forms: [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
"O", "P","Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "!", "?"]},
{id: 202, name: "Wobbuffet", downloadable: true},
@@ -244,25 +246,25 @@ const REGION_LIST = [];
{id: 230, name: "Kingdra", downloadable: true},
{id: 231, name: "Phanpy", downloadable: true},
{id: 232, name: "Donphan", downloadable: true},
{id: 233, name: "Porygon2", downloadable: true},
{id: 233, name: "Porygon2", downloadable: true, gender: "unknown"},
{id: 234, name: "Stantler", downloadable: true},
{id: 235, name: "Smeargle", downloadable: true},
{id: 236, name: "Tyrogue", downloadable: true},
{id: 237, name: "Hitmontop", downloadable: true},
{id: 236, name: "Tyrogue", downloadable: true, gender: "male"},
{id: 237, name: "Hitmontop", downloadable: true, gender: "male"},
{id: 238, name: "Smoochum", downloadable: true},
{id: 239, name: "Elekid", downloadable: true},
{id: 240, name: "Magby", downloadable: true},
{id: 241, name: "Miltank", downloadable: true},
{id: 242, name: "Blissey", downloadable: true},
{id: 243, name: "Raikou", downloadable: true},
{id: 244, name: "Entei", downloadable: true},
{id: 245, name: "Suicune", downloadable: true},
{id: 241, name: "Miltank", downloadable: true, gender: "female"},
{id: 242, name: "Blissey", downloadable: true, gender: "female"},
{id: 243, name: "Raikou", downloadable: true, gender: "unknown"},
{id: 244, name: "Entei", downloadable: true, gender: "unknown"},
{id: 245, name: "Suicune", downloadable: true, gender: "unknown"},
{id: 246, name: "Larvitar", downloadable: true},
{id: 247, name: "Pupitar", downloadable: true},
{id: 248, name: "Tyranitar", downloadable: true},
{id: 249, name: "Lugia", downloadable: true},
{id: 250, name: "Ho-Oh", downloadable: true},
{id: 251, name: "Celebi", downloadable: true},
{id: 249, name: "Lugia", downloadable: true, gender: "unknown"},
{id: 250, name: "Ho-Oh", downloadable: true, gender: "unknown"},
{id: 251, name: "Celebi", downloadable: true, gender: "unknown"},
{id: 252, name: "Treecko", downloadable: true},
{id: 253, name: "Grovyle", downloadable: true},
{id: 254, name: "Sceptile", downloadable: true},
@@ -303,7 +305,7 @@ const REGION_LIST = [];
{id: 289, name: "Slaking", downloadable: true},
{id: 290, name: "Nincada", downloadable: true},
{id: 291, name: "Ninjask", downloadable: true},
{id: 292, name: "Shedinja", downloadable: true},
{id: 292, name: "Shedinja", downloadable: true, gender: "unknown"},
{id: 293, name: "Whismur", downloadable: true},
{id: 294, name: "Loudred", downloadable: true},
{id: 295, name: "Exploud", downloadable: true},
@@ -324,8 +326,8 @@ const REGION_LIST = [];
{id: 310, name: "Manectric", downloadable: true},
{id: 311, name: "Plusle", downloadable: true},
{id: 312, name: "Minun", downloadable: true},
{id: 313, name: "Volbeat", downloadable: true},
{id: 314, name: "Illumise", downloadable: true},
{id: 313, name: "Volbeat", downloadable: true, gender: "male"},
{id: 314, name: "Illumise", downloadable: true, gender: "female"},
{id: 315, name: "Roselia", downloadable: true},
{id: 316, name: "Gulpin", downloadable: true},
{id: 317, name: "Swalot", downloadable: true},
@@ -348,14 +350,14 @@ const REGION_LIST = [];
{id: 334, name: "Altaria", downloadable: true},
{id: 335, name: "Zangoose", downloadable: true},
{id: 336, name: "Seviper", downloadable: true},
{id: 337, name: "Lunatone", downloadable: true},
{id: 338, name: "Solrock", downloadable: true},
{id: 337, name: "Lunatone", downloadable: true, gender: "unknown"},
{id: 338, name: "Solrock", downloadable: true, gender: "unknown"},
{id: 339, name: "Barboach", downloadable: true},
{id: 340, name: "Whiscash", downloadable: true},
{id: 341, name: "Corphish", downloadable: true},
{id: 342, name: "Crawdaunt", downloadable: true},
{id: 343, name: "Baltoy", downloadable: true},
{id: 344, name: "Claydol", downloadable: true},
{id: 343, name: "Baltoy", downloadable: true, gender: "unknown"},
{id: 344, name: "Claydol", downloadable: true, gender: "unknown"},
{id: 345, name: "Lileep", downloadable: true},
{id: 346, name: "Cradily", downloadable: true},
{id: 347, name: "Anorith", downloadable: true},
@@ -385,19 +387,19 @@ const REGION_LIST = [];
{id: 371, name: "Bagon", downloadable: true},
{id: 372, name: "Shelgon", downloadable: true},
{id: 373, name: "Salamence", downloadable: true},
{id: 374, name: "Beldum", downloadable: true},
{id: 375, name: "Metang", downloadable: true},
{id: 376, name: "Metagross", downloadable: true},
{id: 377, name: "Regirock", downloadable: true},
{id: 378, name: "Regice", downloadable: true},
{id: 379, name: "Registeel", downloadable: true},
{id: 380, name: "Latias", downloadable: true},
{id: 381, name: "Latios", downloadable: true},
{id: 382, name: "Kyogre", downloadable: true},
{id: 383, name: "Groudon", downloadable: true},
{id: 384, name: "Rayquaza", downloadable: true},
{id: 385, name: "Jirachi", downloadable: true},
{id: 386, name: "Deoxys", downloadable: true, forms: ["Normal", "Attack", "Defense", "Speed"]},
{id: 374, name: "Beldum", downloadable: true, gender: "unknown"},
{id: 375, name: "Metang", downloadable: true, gender: "unknown"},
{id: 376, name: "Metagross", downloadable: true, gender: "unknown"},
{id: 377, name: "Regirock", downloadable: true, gender: "unknown"},
{id: 378, name: "Regice", downloadable: true, gender: "unknown"},
{id: 379, name: "Registeel", downloadable: true, gender: "unknown"},
{id: 380, name: "Latias", downloadable: true, gender: "female"},
{id: 381, name: "Latios", downloadable: true, gender: "male"},
{id: 382, name: "Kyogre", downloadable: true, gender: "unknown"},
{id: 383, name: "Groudon", downloadable: true, gender: "unknown"},
{id: 384, name: "Rayquaza", downloadable: true, gender: "unknown"},
{id: 385, name: "Jirachi", downloadable: true, gender: "unknown"},
{id: 386, name: "Deoxys", downloadable: true, gender: "unknown", forms: ["Normal", "Attack", "Defense", "Speed"]},
{id: 387, name: "Turtwig", downloadable: true},
{id: 388, name: "Grotle", downloadable: true},
{id: 389, name: "Torterra", downloadable: true},
@@ -424,10 +426,10 @@ const REGION_LIST = [];
{id: 410, name: "Shieldon", downloadable: true},
{id: 411, name: "Bastiodon", downloadable: true},
{id: 412, name: "Burmy", downloadable: true, forms: ["Plant Cloak", "Sandy Cloak", "Trash Cloak"]},
{id: 413, name: "Wormadam", downloadable: true, forms: ["Plant Cloak", "Sandy Cloak", "Trash Cloak"]},
{id: 414, name: "Mothim", downloadable: true},
{id: 413, name: "Wormadam", downloadable: true, gender: "female", forms: ["Plant Cloak", "Sandy Cloak", "Trash Cloak"]},
{id: 414, name: "Mothim", downloadable: true, gender: "male"},
{id: 415, name: "Combee", downloadable: true},
{id: 416, name: "Vespiquen", downloadable: true},
{id: 416, name: "Vespiquen", downloadable: true, gender: "female"},
{id: 417, name: "Pachirisu", downloadable: true},
{id: 418, name: "Buizel", downloadable: true},
{id: 419, name: "Floatzel", downloadable: true},
@@ -447,8 +449,8 @@ const REGION_LIST = [];
{id: 433, name: "Chingling", downloadable: true},
{id: 434, name: "Stunky", downloadable: true},
{id: 435, name: "Skuntank", downloadable: true},
{id: 436, name: "Bronzor", downloadable: true},
{id: 437, name: "Bronzong", downloadable: true},
{id: 436, name: "Bronzor", downloadable: true, gender: "unknown"},
{id: 437, name: "Bronzong", downloadable: true, gender: "unknown"},
{id: 438, name: "Bonsly", downloadable: true},
{id: 439, name: "Mime Jr.", downloadable: true},
{id: 440, name: "Happiny", downloadable: true},
@@ -473,7 +475,7 @@ const REGION_LIST = [];
{id: 459, name: "Snover", downloadable: true},
{id: 460, name: "Abomasnow", downloadable: true},
{id: 461, name: "Weavile", downloadable: true},
{id: 462, name: "Magnezone", downloadable: true},
{id: 462, name: "Magnezone", downloadable: true, gender: "unknown"},
{id: 463, name: "Lickilicky", downloadable: true},
{id: 464, name: "Rhyperior", downloadable: true},
{id: 465, name: "Tangrowth", downloadable: true},
@@ -485,29 +487,29 @@ const REGION_LIST = [];
{id: 471, name: "Glaceon", downloadable: true},
{id: 472, name: "Gliscor", downloadable: true},
{id: 473, name: "Mamoswine", downloadable: true},
{id: 474, name: "Porygon-Z", downloadable: true},
{id: 475, name: "Gallade", downloadable: true},
{id: 474, name: "Porygon-Z", downloadable: true, gender: "unknown"},
{id: 475, name: "Gallade", downloadable: true, gender: "male"},
{id: 476, name: "Probopass", downloadable: true},
{id: 477, name: "Dusknoir", downloadable: true},
{id: 478, name: "Froslass", downloadable: true},
{id: 479, name: "Rotom", downloadable: true, forms: ["Normal", "Heat", "Wash", "Frost", "Fan", "Mow"]},
{id: 480, name: "Uxie", downloadable: true},
{id: 481, name: "Mesprit", downloadable: true},
{id: 482, name: "Azelf", downloadable: true},
{id: 483, name: "Dialga", downloadable: true},
{id: 484, name: "Palkia", downloadable: true},
{id: 478, name: "Froslass", downloadable: true, gender: "female"},
{id: 479, name: "Rotom", downloadable: true, gender: "unknown", forms: ["Normal", "Heat", "Wash", "Frost", "Fan", "Mow"]},
{id: 480, name: "Uxie", downloadable: true, gender: "unknown"},
{id: 481, name: "Mesprit", downloadable: true, gender: "unknown"},
{id: 482, name: "Azelf", downloadable: true, gender: "unknown"},
{id: 483, name: "Dialga", downloadable: true, gender: "unknown"},
{id: 484, name: "Palkia", downloadable: true, gender: "unknown"},
{id: 485, name: "Heatran", downloadable: true},
{id: 486, name: "Regigigas", downloadable: true},
{id: 487, name: "Giratina", downloadable: true, forms: ["Altered", "Origin"]},
{id: 488, name: "Cresselia", downloadable: true},
{id: 489, name: "Phione", downloadable: true},
{id: 490, name: "Manaphy", downloadable: true},
{id: 491, name: "Darkrai", downloadable: true},
{id: 492, name: "Shaymin", downloadable: true},
{id: 493, name: "Arceus", downloadable: true, forms: [
"Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost", "Steel",
"Fire", "Water", "Grass", "Electric", "Psychic", "Ice", "Dragon", "Dark"]},
{id: 494, name: "Victini", downloadable: true},
{id: 486, name: "Regigigas", downloadable: true, gender: "unknown"},
{id: 487, name: "Giratina", downloadable: true, gender: "unknown", forms: ["Altered", "Origin"]},
{id: 488, name: "Cresselia", downloadable: true, gender: "female"},
{id: 489, name: "Phione", downloadable: true, gender: "unknown"},
{id: 490, name: "Manaphy", downloadable: true, gender: "unknown"},
{id: 491, name: "Darkrai", downloadable: true, gender: "unknown"},
{id: 492, name: "Shaymin", downloadable: true, gender: "unknown", forms: ["Land", "Sky"]},
{id: 493, name: "Arceus", downloadable: true, gender: "unknown", forms: [
"Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost",
"Steel", "Fire", "Water", "Grass", "Electric", "Psychic", "Ice", "Dragon", "Dark"]},
{id: 494, name: "Victini", downloadable: true, gender: "unknown"},
{id: 495, name: "Snivy", downloadable: false},
{id: 496, name: "Servine", downloadable: false},
{id: 497, name: "Serperior", downloadable: false},
@@ -551,8 +553,8 @@ const REGION_LIST = [];
{id: 535, name: "Tympole", downloadable: true},
{id: 536, name: "Palpitoad", downloadable: false},
{id: 537, name: "Seismitoad", downloadable: false},
{id: 538, name: "Throh", downloadable: true},
{id: 539, name: "Sawk", downloadable: true},
{id: 538, name: "Throh", downloadable: true, gender: "male"},
{id: 539, name: "Sawk", downloadable: true, gender: "male"},
{id: 540, name: "Sewaddle", downloadable: false},
{id: 541, name: "Swadloon", downloadable: false},
{id: 542, name: "Leavanny", downloadable: true},
@@ -561,8 +563,8 @@ const REGION_LIST = [];
{id: 545, name: "Scolipede", downloadable: true},
{id: 546, name: "Cottonee", downloadable: true},
{id: 547, name: "Whimsicott", downloadable: false},
{id: 548, name: "Petilil", downloadable: true},
{id: 549, name: "Lilligant", downloadable: false},
{id: 548, name: "Petilil", downloadable: true, gender: "female"},
{id: 549, name: "Lilligant", downloadable: false, gender: "female"},
{id: 550, name: "Basculin", downloadable: true, forms: ["Red-striped", "Blue-striped"]},
{id: 551, name: "Sandile", downloadable: false},
{id: 552, name: "Krokorok", downloadable: false},
@@ -612,9 +614,9 @@ const REGION_LIST = [];
{id: 596, name: "Galvantula", downloadable: true},
{id: 597, name: "Ferroseed", downloadable: false},
{id: 598, name: "Ferrothorn", downloadable: false},
{id: 599, name: "Klink", downloadable: false},
{id: 600, name: "Klang", downloadable: true},
{id: 601, name: "Klinklang", downloadable: false},
{id: 599, name: "Klink", downloadable: false, gender: "unknown"},
{id: 600, name: "Klang", downloadable: true, gender: "unknown"},
{id: 601, name: "Klinklang", downloadable: false, gender: "unknown"},
{id: 602, name: "Tynamo", downloadable: false},
{id: 603, name: "Eelektrik", downloadable: false},
{id: 604, name: "Eelektross", downloadable: false},
@@ -628,22 +630,22 @@ const REGION_LIST = [];
{id: 612, name: "Haxorus", downloadable: false},
{id: 613, name: "Cubchoo", downloadable: true},
{id: 614, name: "Beartic", downloadable: false},
{id: 615, name: "Cryogonal", downloadable: false},
{id: 615, name: "Cryogonal", downloadable: false, gender: "unknown"},
{id: 616, name: "Shelmet", downloadable: true},
{id: 617, name: "Accelgor", downloadable: false},
{id: 618, name: "Stunfisk", downloadable: true},
{id: 619, name: "Mienfoo", downloadable: true},
{id: 620, name: "Mienshao", downloadable: false},
{id: 621, name: "Druddigon", downloadable: true},
{id: 622, name: "Golett", downloadable: true},
{id: 623, name: "Golurk", downloadable: false},
{id: 622, name: "Golett", downloadable: true, gender: "unknown"},
{id: 623, name: "Golurk", downloadable: false, gender: "unknown"},
{id: 624, name: "Pawniard", downloadable: true},
{id: 625, name: "Bisharp", downloadable: false},
{id: 626, name: "Bouffalant", downloadable: true},
{id: 627, name: "Rufflet", downloadable: false},
{id: 628, name: "Braviary", downloadable: true},
{id: 629, name: "Vullaby", downloadable: false},
{id: 630, name: "Mandibuzz", downloadable: true},
{id: 627, name: "Rufflet", downloadable: false, gender: "male"},
{id: 628, name: "Braviary", downloadable: true, gender: "male"},
{id: 629, name: "Vullaby", downloadable: false, gender: "female"},
{id: 630, name: "Mandibuzz", downloadable: true, gender: "female"},
{id: 631, name: "Heatmor", downloadable: true},
{id: 632, name: "Durant", downloadable: true},
{id: 633, name: "Deino", downloadable: false},
@@ -651,18 +653,18 @@ const REGION_LIST = [];
{id: 635, name: "Hydreigon", downloadable: false},
{id: 636, name: "Larvesta", downloadable: false},
{id: 637, name: "Volcarona", downloadable: false},
{id: 638, name: "Cobalion", downloadable: false},
{id: 639, name: "Terrakion", downloadable: false},
{id: 640, name: "Virizion", downloadable: false},
{id: 641, name: "Tornadus", downloadable: false, forms: ["Incarnate", "Therian"]},
{id: 642, name: "Thundurus", downloadable: false, forms: ["Incarnate", "Therian"]},
{id: 643, name: "Reshiram", downloadable: false},
{id: 644, name: "Zekrom", downloadable: false},
{id: 645, name: "Landorus", downloadable: false, forms: ["Incarnate", "Therian"]},
{id: 646, name: "Kyurem", downloadable: false, forms: ["Normal", "White", "Black"]},
{id: 647, name: "Keldeo", downloadable: false, forms: ["Ordinary", "Resolute"]},
{id: 648, name: "Meloetta", downloadable: false, forms: ["Aria", "Pirouette"]},
{id: 649, name: "Genesect", downloadable: false, forms: ["Normal", "Douse", "Shock", "Burn", "Chill"]}
{id: 638, name: "Cobalion", downloadable: false, gender: "unknown"},
{id: 639, name: "Terrakion", downloadable: false, gender: "unknown"},
{id: 640, name: "Virizion", downloadable: false, gender: "unknown"},
{id: 641, name: "Tornadus", downloadable: false, gender: "male", forms: ["Incarnate", "Therian"]},
{id: 642, name: "Thundurus", downloadable: false, gender: "male", forms: ["Incarnate", "Therian"]},
{id: 643, name: "Reshiram", downloadable: false, gender: "unknown"},
{id: 644, name: "Zekrom", downloadable: false, gender: "unknown"},
{id: 645, name: "Landorus", downloadable: false, gender: "male", forms: ["Incarnate", "Therian"]},
{id: 646, name: "Kyurem", downloadable: false, gender: "unknown", forms: ["Normal", "White", "Black"]},
{id: 647, name: "Keldeo", downloadable: false, gender: "unknown", forms: ["Ordinary", "Resolute"]},
{id: 648, name: "Meloetta", downloadable: false, gender: "unknown", forms: ["Aria", "Pirouette"]},
{id: 649, name: "Genesect", downloadable: false, gender: "unknown", forms: ["Normal", "Douse", "Shock", "Burn", "Chill"]}
];
// Moves
@@ -1228,6 +1230,174 @@ const REGION_LIST = [];
{id: 559, name: "Fusion Bolt"}
];
// Abilities
let abilities = [
{id: 1, name: "Stench"},
{id: 2, name: "Drizzle"},
{id: 3, name: "Speed Boost"},
{id: 4, name: "Battle Armor"},
{id: 5, name: "Sturdy"},
{id: 6, name: "Damp"},
{id: 7, name: "Limber"},
{id: 8, name: "Sand Veil"},
{id: 9, name: "Static"},
{id: 10, name: "Volt Absorb"},
{id: 11, name: "Water Absorb"},
{id: 12, name: "Oblivious"},
{id: 13, name: "Cloud Nine"},
{id: 14, name: "Compound Eyes"},
{id: 15, name: "Insomnia"},
{id: 16, name: "Color Change"},
{id: 17, name: "Immunity"},
{id: 18, name: "Flash Fire"},
{id: 19, name: "Shield Dust"},
{id: 20, name: "Own Tempo"},
{id: 21, name: "Suction Cups"},
{id: 22, name: "Intimidate"},
{id: 23, name: "Shadow Tag"},
{id: 24, name: "Rough Skin"},
{id: 25, name: "Wonder Guard"},
{id: 26, name: "Levitate"},
{id: 27, name: "Effect Spore"},
{id: 28, name: "Synchronize"},
{id: 29, name: "Clear Body"},
{id: 30, name: "Natural Cure"},
{id: 31, name: "Lightning Rod"},
{id: 32, name: "Serene Grace"},
{id: 33, name: "Swift Swim"},
{id: 34, name: "Chlorophyll"},
{id: 35, name: "Illuminate"},
{id: 36, name: "Trace"},
{id: 37, name: "Huge Power"},
{id: 38, name: "Poison Point"},
{id: 39, name: "Inner Focus"},
{id: 40, name: "Magma Armor"},
{id: 41, name: "Water Veil"},
{id: 42, name: "Magnet Pull"},
{id: 43, name: "Soundproof"},
{id: 44, name: "Rain Dish"},
{id: 45, name: "Sand Stream"},
{id: 46, name: "Pressure"},
{id: 47, name: "Thick Fat"},
{id: 48, name: "Early Bird"},
{id: 49, name: "Flame Body"},
{id: 50, name: "Run Away"},
{id: 51, name: "Keen Eye"},
{id: 52, name: "Hyper Cutter"},
{id: 53, name: "Pickup"},
{id: 54, name: "Truant"},
{id: 55, name: "Hustle"},
{id: 56, name: "Cute Charm"},
{id: 57, name: "Plus"},
{id: 58, name: "Minus"},
{id: 59, name: "Forecast"},
{id: 60, name: "Sticky Hold"},
{id: 61, name: "Shed Skin"},
{id: 62, name: "Guts"},
{id: 63, name: "Marvel Scale"},
{id: 64, name: "Liquid Ooze"},
{id: 65, name: "Overgrow"},
{id: 66, name: "Blaze"},
{id: 67, name: "Torrent"},
{id: 68, name: "Swarm"},
{id: 69, name: "Rock Head"},
{id: 70, name: "Drought"},
{id: 71, name: "Arena Trap"},
{id: 72, name: "Vital Spirit"},
{id: 73, name: "White Smoke"},
{id: 74, name: "Pure Power"},
{id: 75, name: "Shell Armor"},
{id: 76, name: "Air Lock"},
{id: 77, name: "Tangled Feet"},
{id: 78, name: "Motor Drive"},
{id: 79, name: "Rivalry"},
{id: 80, name: "Steadfast"},
{id: 81, name: "Snow Cloak"},
{id: 82, name: "Gluttony"},
{id: 83, name: "Anger Point"},
{id: 84, name: "Unburden"},
{id: 85, name: "Heatproof"},
{id: 86, name: "Simple"},
{id: 87, name: "Dry Skin"},
{id: 88, name: "Download"},
{id: 89, name: "Iron Fist"},
{id: 90, name: "Poison Heal"},
{id: 91, name: "Adaptability"},
{id: 92, name: "Skill Link"},
{id: 93, name: "Hydration"},
{id: 94, name: "Solar Power"},
{id: 95, name: "Quick Feet"},
{id: 96, name: "Normalize"},
{id: 97, name: "Sniper"},
{id: 98, name: "Magic Guard"},
{id: 99, name: "No Guard"},
{id: 100, name: "Stall"},
{id: 101, name: "Technician"},
{id: 102, name: "Leaf Guard"},
{id: 103, name: "Klutz"},
{id: 104, name: "Mold Breaker"},
{id: 105, name: "Super Luck"},
{id: 106, name: "Aftermath"},
{id: 107, name: "Anticipation"},
{id: 108, name: "Forewarn"},
{id: 109, name: "Unaware"},
{id: 110, name: "Tinted Lens"},
{id: 111, name: "Filter"},
{id: 112, name: "Slow Start"},
{id: 113, name: "Scrappy"},
{id: 114, name: "Storm Drain"},
{id: 115, name: "Ice Body"},
{id: 116, name: "Solid Rock"},
{id: 117, name: "Snow Warning"},
{id: 118, name: "Honey Gather"},
{id: 119, name: "Frisk"},
{id: 120, name: "Reckless"},
{id: 121, name: "Multitype"},
{id: 122, name: "Flower Gift"},
{id: 123, name: "Bad Dreams"},
{id: 124, name: "Pickpocket"},
{id: 125, name: "Sheer Force"},
{id: 126, name: "Contrary"},
{id: 127, name: "Unnerve"},
{id: 128, name: "Defiant"},
{id: 129, name: "Defeatist"},
{id: 130, name: "Cursed Body"},
{id: 131, name: "Healer"},
{id: 132, name: "Friend Guard"},
{id: 133, name: "Weak Armor"},
{id: 134, name: "Heavy Metal"},
{id: 135, name: "Light Metal"},
{id: 136, name: "Multiscale"},
{id: 137, name: "Toxic Boost"},
{id: 138, name: "Flare Boost"},
{id: 139, name: "Harvest"},
{id: 140, name: "Telepathy"},
{id: 141, name: "Moody"},
{id: 142, name: "Overcoat"},
{id: 143, name: "Poison Touch"},
{id: 144, name: "Regenerator"},
{id: 145, name: "Big Pecks"},
{id: 146, name: "Sand Rush"},
{id: 147, name: "Wonder Skin"},
{id: 148, name: "Analytic"},
{id: 149, name: "Illusion"},
{id: 150, name: "Imposter"},
{id: 151, name: "Infiltrator"},
{id: 152, name: "Mummy"},
{id: 153, name: "Moxie"},
{id: 154, name: "Justified"},
{id: 155, name: "Rattled"},
{id: 156, name: "Magic Bounce"},
{id: 157, name: "Sap Sipper"},
{id: 158, name: "Prankster"},
{id: 159, name: "Sand Force"},
{id: 160, name: "Iron Barbs"},
{id: 161, name: "Zen Mode"},
{id: 162, name: "Victory Star"},
{id: 163, name: "Turboblaze"},
{id: 164, name: "Teravolt"}
];
// Items
let items = [
{id: 1, name: "Master Ball"},
@@ -2390,14 +2560,20 @@ const REGION_LIST = [];
// Index data
for(let i in species) {
let entry = species[i];
POKE_SPECIES_MAP[entry.id] = entry;
POKE_SPECIES_LIST[i] = entry;
SPECIES_MAP[entry.id] = entry;
SPECIES_LIST[i] = entry;
}
for(let i in moves) {
let move = moves[i];
POKE_MOVE_MAP[move.id] = move;
POKE_MOVE_LIST[i] = move;
MOVE_MAP[move.id] = move;
MOVE_LIST[i] = move;
}
for(let i in abilities) {
let ability = abilities[i];
ABILITY_MAP[ability.id] = ability;
ABILITY_LIST[i] = ability;
}
for(let i in items) {

View File

@@ -3,6 +3,7 @@ const ELEMENT_GAME_SUMMARY = document.getElementById("game-summary");
const ELEMENT_DREAMER_SPRITE = document.getElementById("dreamer-sprite");
const ELEMENT_DREAMER_SPECIES = document.getElementById("dreamer-species");
const ELEMENT_DREAMER_ABILITY = document.getElementById("dreamer-ability");
const ELEMENT_DREAMER_NATURE = document.getElementById("dreamer-nature");
const ELEMENT_DREAMER_NAME = document.getElementById("dreamer-name");
const ELEMENT_DREAMER_GENDER = document.getElementById("dreamer-gender");
@@ -10,15 +11,18 @@ const ELEMENT_DREAMER_TRAINER = document.getElementById("dreamer-trainer");
const ELEMENT_DREAMER_TRAINER_ID = document.getElementById("dreamer-trainer-id");
const ELEMENT_DREAMER_LEVEL = document.getElementById("dreamer-level");
const ELEMENT_ENCOUNTER_CONFIG = document.getElementById("encounter-config");
const ELEMENT_ENCOUNTER_SPECIES = document.getElementById("encounter-form-species");
const ELEMENT_ENCOUNTER_MOVE = document.getElementById("encounter-form-move");
const ELEMENT_ENCOUNTER_FORM = document.getElementById("encounter-form-form");
const ELEMENT_ENCOUNTER_GENDER = document.getElementById("encounter-form-gender");
const ELEMENT_ENCOUNTER_ANIMATION = document.getElementById("encounter-form-animation");
const ELEMENT_ITEM_CONFIG = document.getElementById("item-config");
const ELEMENT_ITEM_ID = document.getElementById("item-form-id");
const ELEMENT_ITEM_QUANTITY = document.getElementById("item-form-quantity");
const ELEMENT_VISITOR_CONFIG = document.getElementById("visitor-config");
const ELEMENT_VISITOR_NAME = document.getElementById("visitor-form-name");
const ELEMENT_VISITOR_TYPE = document.getElementById("visitor-form-type");
const ELEMENT_VISITOR_SHOP_TYPE = document.getElementById("visitor-form-shop-type");
@@ -59,7 +63,8 @@ var profile = {
if(response.dreamerInfo) {
let dreamerInfo = response.dreamerInfo;
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + response.dreamerSprite + "'/>";
ELEMENT_DREAMER_SPECIES.innerHTML = POKE_SPECIES_LIST[dreamerInfo.species - 1].name;
ELEMENT_DREAMER_SPECIES.innerHTML = SPECIES_MAP[dreamerInfo.species] ? (SPECIES_MAP[dreamerInfo.species].name) : "N/A";
ELEMENT_DREAMER_ABILITY.innerHTML = ABILITY_MAP[dreamerInfo.ability] ? ABILITY_MAP[dreamerInfo.ability].name : "N/A";
ELEMENT_DREAMER_NATURE.innerHTML = stringToWord(dreamerInfo.nature);
ELEMENT_DREAMER_NAME.innerHTML = dreamerInfo.nickname;
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(dreamerInfo.gender);
@@ -90,6 +95,17 @@ var profile = {
profile.cgearSkin = response.cgearSkin ? response.cgearSkin : "none";
profile.dexSkin = response.dexSkin ? response.dexSkin : "none";
profile.musical = response.musical ? response.musical : "none";
if(response.customCGearSkin) {
profile.customCGearSkin = response.customCGearSkin;
ELEMENT_CGEAR_SKIN_INPUT.add(new Option(response.customCGearSkin, "custom"), 1);
}
if(response.customDexSkin) {
profile.customDexSkin = response.customDexSkin;
ELEMENT_DEX_SKIN_INPUT.add(new Option(response.customDexSkin, "custom"), 1);
}
fetchDlcData();
ELEMENT_LEVEL_GAIN_INPUT.value = response.levelsGained;
@@ -103,8 +119,8 @@ var profile = {
});
// Sort data lists alphabetically
let sortedSpecies = [...POKE_SPECIES_LIST].sort((a, b) => a.name.localeCompare(b.name));
let sortedMoves = [...POKE_MOVE_LIST].sort((a, b) => a.name.localeCompare(b.name));
let sortedSpecies = [...SPECIES_LIST].sort((a, b) => a.name.localeCompare(b.name));
let sortedMoves = [...MOVE_LIST].sort((a, b) => a.name.localeCompare(b.name));
let sortedItems = [...ITEM_LIST].sort((a, b) => a.name.localeCompare(b.name));
// Add species data
@@ -138,10 +154,10 @@ var profile = {
ELEMENT_VISITOR_REGION.options[ELEMENT_VISITOR_REGION.options.length] = new Option(region.name, region.id);
}
// Event listener for changing the form selector contents when species changes
// Event listener for changing the form & gender selector contents when species changes
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", function() {
updateEncounterFormOptions();
ELEMENT_ENCOUNTER_FORM.value = 0;
ELEMENT_ENCOUNTER_FORM.value = updateEncounterFormOptions();
ELEMENT_ENCOUNTER_GENDER.value = updateEncounterGenderOptions();
});
// Same thing, but for Join Avenue visitor region & subregion
@@ -156,7 +172,7 @@ var profile = {
function updateEncounterFormOptions() {
clearSelectOptions(ELEMENT_ENCOUNTER_FORM);
let species = POKE_SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
// Update special form options
if(species.forms) {
@@ -166,17 +182,47 @@ function updateEncounterFormOptions() {
} else {
ELEMENT_ENCOUNTER_FORM.options[ELEMENT_ENCOUNTER_FORM.options.length] = new Option("N/A", 0);
}
return 0;
}
function updateEncounterGenderOptions() {
clearSelectOptions(ELEMENT_ENCOUNTER_GENDER);
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
// Update gender options
if(species.gender) {
console.log(species.gender);
switch(species.gender) {
case "male":
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Male", "MALE");
return "MALE";
case "female":
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Female", "FEMALE");
return "FEMALE";
case "unknown":
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("N/A", "GENDERLESS");
return "GENDERLESS";
}
}
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Male", "MALE");
ELEMENT_ENCOUNTER_GENDER.options[1] = new Option("Female", "FEMALE");
ELEMENT_ENCOUNTER_GENDER.options[2] = new Option("Random", "GENDERLESS");
return "GENDERLESS";
}
function configureEncounter(index) {
encounterTableIndex = Math.min(10, Math.min(index, profile.encounters.length));
let encounter = profile.encounters[encounterTableIndex];
ELEMENT_ENCOUNTER_SPECIES.value = encounter ? encounter.species : 1;
updateEncounterFormOptions();
ELEMENT_ENCOUNTER_SPECIES.value = encounter ? encounter.species : 493;
let form = updateEncounterFormOptions();
let gender = updateEncounterGenderOptions();
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 0;
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : 0;
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : "GENDERLESS";
ELEMENT_ENCOUNTER_ANIMATION.value = encounter ? encounter.animation : "WALK_AROUND";
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : form;
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : gender;
ELEMENT_ENCOUNTER_ANIMATION.value = encounter ? encounter.animation : "LOOK_AROUND";
ELEMENT_ENCOUNTER_CONFIG.style.display = "block";
}
function saveEncounter() {
@@ -223,13 +269,20 @@ function updateEncounterCell(index) {
if(encounterData) {
spriteImage = spriteBase + encounterData.species + ".png";
// Use unique form sprite if it exists
if(encounterData.form > 0) {
// Use unique form sprite if it exists
let formSpriteImage = spriteBase + encounterData.species + "-" + encounterData.form + ".png";
if(checkURL(formSpriteImage)){
spriteImage = formSpriteImage;
}
} else if(encounterData.gender == "FEMALE") {
// Otherwise, use female sprite if it exists
let femaleSpriteImage = spriteBase + "female/" + encounterData.species + ".png";
if(checkURL(femaleSpriteImage)){
spriteImage = femaleSpriteImage;
}
}
}
@@ -238,7 +291,7 @@ function updateEncounterCell(index) {
function closeEncounterForm() {
encounterTableIndex = -1;
window.location.href = "#";
ELEMENT_ENCOUNTER_CONFIG.style.display = "none";
}
/**
@@ -278,6 +331,7 @@ function configureVisitor(index) {
ELEMENT_VISITOR_SUBREGION.value = visitor ? visitor.stateProvinceCode : 0;
ELEMENT_VISITOR_PERSONALITY.value = visitor ? visitor.personality : 0;
ELEMENT_VISITOR_DREAMER.value = visitor ? visitor.dreamerSpecies : 1;
ELEMENT_VISITOR_CONFIG.style.display = "block";
}
function saveVisitor() {
@@ -355,7 +409,7 @@ function updateVisitorCell(index) {
function closeVisitorForm() {
visitorTableIndex = -1;
window.location.href = "#";
ELEMENT_VISITOR_CONFIG.style.display = "none";
}
/**
@@ -367,6 +421,7 @@ function configureItem(index) {
let item = profile.items[itemTableIndex];
ELEMENT_ITEM_ID.value = item ? item.id : 1;
ELEMENT_ITEM_QUANTITY.value = item ? item.quantity : 1;
ELEMENT_ITEM_CONFIG.style.display = "block";
}
function saveItem() {
@@ -422,13 +477,49 @@ function updateItemCell(index) {
function closeItemForm() {
itemTableIndex = -1;
window.location.href = "#";
ELEMENT_ITEM_CONFIG.style.display = "none";
}
/**
* Miscellaneous stuff
*/
function openFileChooser(inputDocumentId) {
document.getElementById(inputDocumentId).click();
}
function uploadSkin(type, file) {
fetchData("POST", "/dashboard/uploadskin?type=" + (type == "CGEAR" && isVersion2() ? "CGEAR2" : type)
+ "&filename=" + file.name, file).then((response) => {
if(!response) {
return;
}
// Display message if there is one
if(response.message) {
window.alert(response.message);
}
// Return if error
if(response.error) {
return;
}
// Update selector option
let selectElement = type == "CGEAR" ? ELEMENT_CGEAR_SKIN_INPUT : ELEMENT_DEX_SKIN_INPUT;
// Add option for custom skin if one does not already exist
// Otherwise, update the existing one
if(selectElement.options[1].value == "custom") {
selectElement.options[1].innerHTML = file.name;
} else{
selectElement.add(new Option(file.name, "custom"), 1);
}
selectElement.value = "custom";
});
}
function previewSkin(inputElementId, type) {
let value = document.getElementById(inputElementId).value;
@@ -448,20 +539,28 @@ function previewSkin(inputElementId, type) {
function fetchDlcData() {
// Fetch C-Gear skins
fetchData("GET", "/dashboard/dlc?type=" + (isVersion2() ? "CGEAR2" : "CGEAR")).then((response) => {
addDlcNames(ELEMENT_CGEAR_SKIN_INPUT, response);
ELEMENT_CGEAR_SKIN_INPUT.value = response.includes(profile.cgearSkin) ? profile.cgearSkin : "none";
if(response) {
addDlcNames(ELEMENT_CGEAR_SKIN_INPUT, response);
ELEMENT_CGEAR_SKIN_INPUT.value = profile.cgearSkin == "custom" && profile.customCGearSkin ? "custom"
: response.includes(profile.cgearSkin) ? profile.cgearSkin : "none";
}
});
// Fetch Dex skins
fetchData("GET", "/dashboard/dlc?type=ZUKAN").then((response) => {
addDlcNames(ELEMENT_DEX_SKIN_INPUT, response);
ELEMENT_DEX_SKIN_INPUT.value = response.includes(profile.dexSkin) ? profile.dexSkin : "none";
if(response) {
addDlcNames(ELEMENT_DEX_SKIN_INPUT, response);
ELEMENT_DEX_SKIN_INPUT.value = profile.dexSkin == "custom" && profile.customDexSkin ? "custom"
: response.includes(profile.dexSkin) ? profile.dexSkin : "none";
}
});
// Fetch musicals
fetchData("GET", "/dashboard/dlc?type=MUSICAL").then((response) => {
addDlcNames(ELEMENT_MUSICAL_INPUT, response);
ELEMENT_MUSICAL_INPUT.value = response.includes(profile.musical) ? profile.musical : "none";
if(response) {
addDlcNames(ELEMENT_MUSICAL_INPUT, response);
ELEMENT_MUSICAL_INPUT.value = response.includes(profile.musical) ? profile.musical : "none";
}
});
}
@@ -482,7 +581,9 @@ function postProfileData() {
musical: ELEMENT_MUSICAL_INPUT.value,
gainedLevels: ELEMENT_LEVEL_GAIN_INPUT.value
})).then((response) => {
alert(response.message);
if(response) {
alert(response.message);
}
});
}

View File

@@ -42,11 +42,11 @@ async function fetchData(method, path, body) {
if(response.status != 200) {
if(response.status == 401) {
window.location.href = "/dashboard/login.html"; // TODO not epic idea to put this here
return {};
return false;
}
window.alert("Server returned status code " + response.status + " while fetching " + path);
return {};
return false;
}
try {
@@ -55,5 +55,5 @@ async function fetchData(method, path, body) {
window.alert("Could not deserialize JSON response: " + error);
}
return {};
return false;
}

View File

@@ -57,7 +57,8 @@ a:link, a:visited {
gap: 5px 10px;
}
.grid-container input {
.grid-container input,
.grid-container button {
width: 100%;
margin-top: 4px;
margin-bottom: 16px;
@@ -100,20 +101,13 @@ a:link, a:visited {
}
.popup {
display: none;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
opacity: 0;
transition: opacity 200ms;
visibility: hidden;
}
.popup:target {
opacity: 1;
visibility: visible;
}
.popup .close-button {
@@ -147,6 +141,7 @@ a:link, a:visited {
width: 96px;
height: 96px;
text-align: center;
cursor: pointer;
}
.item-table {
@@ -163,6 +158,7 @@ a:link, a:visited {
width: 48px;
height: 48px;
text-align: center;
cursor: pointer;
}
.item-table td a {
@@ -185,6 +181,7 @@ a:link, a:visited {
width: 80px;
height: 80px;
text-align: center;
cursor: pointer;
}
.big-button {

Binary file not shown.