37 Commits

Author SHA1 Message Date
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
kuroppoi
0f4160459e Add option to configure Join Avenue visitor region 2023-07-09 22:00:39 +02:00
kuroppoi
3dea11ff50 Sort large lists alphabetically to make them easier to navigate 2023-07-09 04:35:50 +02:00
kuroppoi
b38b26f9ce Refactor dashboard stuff & ditch magic numbers for drop-down lists (#9) 2023-07-08 18:22:15 +02:00
kuroppoi
a95e7e2f01 Switcheroo 2023-07-08 15:09:45 +02:00
kuroppoi
53f05acab9 Center popup forms 2023-07-08 02:35:11 +02:00
kuroppoi
49a15a0156 Change DLC names to be clearer & add missing skins 2023-07-08 02:13:17 +02:00
kuroppoi
329e1de30a Rely on DLC game code & type instead of names not being duplicate 2023-07-08 02:09:05 +02:00
kuroppoi
0e12cb54d7 Check if address is actually a local address before returning it 2023-07-07 03:34:44 +02:00
kuroppoi
8849265805 Add option to preview C-Gear & Pokédex skins 2023-07-06 22:52:42 +02:00
kuroppoi
0d964ec129 Sort DLC names alphabetically 2023-07-06 20:28:46 +02:00
kuroppoi
e4fff87b87 Empty default encounter move 2023-07-03 23:21:36 +02:00
kuroppoi
aae1abd18f Swap places of visitor configuration and item configuration 2023-07-03 22:16:22 +02:00
kuroppoi
038f7f5136 Update README.md 2023-07-02 22:44:13 +02:00
kuroppoi
f41888ba58 Fix scroll issue 2023-07-02 03:17:39 +02:00
kuroppoi
328aac45e1 Join Avenue visitor stuff (#7) 2023-07-02 02:56:33 +02:00
kuroppoi
806bd5cc17 Use a different centering strategy 2023-07-01 20:14:09 +02:00
kuroppoi
06e388ab89 Make user dashboard a bit more consistent 2023-06-30 20:49:50 +02:00
kuroppoi
60d6698f6b Add pixel image filter to list style 2023-06-30 20:46:59 +02:00
kuroppoi
4800f7b6c2 Send mock decor data in savedata.download (#6) 2023-06-30 19:52:13 +02:00
kuroppoi
a1e8cc97bc Change a couple labels 2023-06-30 15:48:35 +02:00
kuroppoi
2aebbac230 Update submodule 2023-06-30 15:40:22 +02:00
kuroppoi
09ce3a26ce Pad Trainer ID with leading zeroes 2023-06-30 15:38:52 +02:00
kuroppoi
e4f7a26f9c Add option to not specify a special move for encounters 2023-06-30 15:34:28 +02:00
kuroppoi
d90200e4ca Fix compatibility issue with Japanese Black & White versions 2023-06-30 00:25:30 +02:00
94 changed files with 3998 additions and 669 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,9 +1,11 @@
# 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 and Musicals\
to your game without needing to edit your save file.
Its purpose is to serve as a simple utility for downloading Pokémon, Items, C-Gear skins, Pokédex skins, Musicals\
and, in Black 2 & White 2 only, Join Avenue visitors to your game without needing to edit your save file.\
It can also be used to Memory Link with a Black or White save file if you don't have a second DS system.
## Building
@@ -27,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

@@ -0,0 +1,15 @@
package entralinked.model.avenue;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum AvenueShopType {
@JsonEnumDefaultValue
RAFFLE,
FLORIST,
SALON,
ANTIQUE,
DOJO,
CAFE,
MARKET
}

View File

@@ -0,0 +1,15 @@
package entralinked.model.avenue;
import com.fasterxml.jackson.annotation.JsonProperty;
import entralinked.GameVersion;
public record AvenueVisitor(
@JsonProperty(required = true) String name,
@JsonProperty(required = true) AvenueVisitorType type,
@JsonProperty(required = true) AvenueShopType shopType,
@JsonProperty(required = true) GameVersion gameVersion,
@JsonProperty(required = true) int countryCode,
@JsonProperty(required = true) int stateProvinceCode,
@JsonProperty(required = true) int personality,
@JsonProperty(required = true) int dreamerSpecies) {}

View File

@@ -0,0 +1,59 @@
package entralinked.model.avenue;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum AvenueVisitorType {
// 0
@JsonEnumDefaultValue
YOUNGSTER(0),
LASS(0, true),
// 1
ACE_TRAINER_MALE(1),
ACE_TRAINER_FEMALE(1, true),
// 2
RANGER_MALE(2),
RANGER_FEMALE(2, true),
// 3
BREEDER_MALE(3),
BREEDER_FEMALE(3, true),
// 4
SCIENTIST_MALE(4),
SCIENTIST_FEMALE(4, true),
// 5
HIKER(5),
PARASOL_LADY(5, true),
// 6
ROUGHNECK(6),
NURSE(6, true),
// 7
PRESCHOOLER_MALE(7),
PRESCHOOLER_FEMALE(7, true);
private final int clientId;
private final boolean female;
private AvenueVisitorType(int clientId, boolean female) {
this.clientId = clientId;
this.female = female;
}
private AvenueVisitorType(int clientId) {
this(clientId, false);
}
public int getClientId() {
return clientId;
}
public boolean isFemale() {
return female;
}
}

View File

@@ -6,11 +6,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -22,7 +21,7 @@ import entralinked.utility.Crc16;
public class DlcList {
private static final Logger logger = LogManager.getLogger();
private final Map<String, Dlc> dlcMap = new ConcurrentHashMap<>();
private final List<Dlc> dlcList = new ArrayList<>();
private final File dataDirectory = new File("dlc");
public DlcList() {
@@ -83,26 +82,20 @@ public class DlcList {
// Load DLC data
Dlc dlc = loadDlcFile(file.getName(), subFile.getName(), index, dlcFile);
// Index DLC object if loading succeeded
if(dlc != null) {
dlcMap.put(dlc.name(), dlc);
dlcList.add(dlc);
index++;
}
}
}
}
logger.info("Loaded {} DLC file(s)", dlcMap.size());
logger.info("Loaded {} DLC file(s)", dlcList.size());
}
private Dlc loadDlcFile(String gameCode, String type, int index, File dlcFile) {
String name = dlcFile.getName();
if(dlcMap.containsKey(name)) {
logger.warn("Duplicate DLC name {}", name);
return null;
}
if(dlcFile.isDirectory()) {
logger.warn("Directory '{}' in {} DLC folder", name, gameCode);
return null;
@@ -123,6 +116,7 @@ public class DlcList {
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
if(checksum != checksumInFile) {
logger.warn("Checksum mismatch in DLC '{}'", name);
projectedSize += 2;
checksum = Crc16.calc(bytes, 0, bytes.length);
checksumEmbedded = false;
@@ -167,19 +161,18 @@ public class DlcList {
return builder.toString();
}
public Dlc getDlc(String name) {
return dlcMap.get(name);
public Dlc getDlc(String gameCode, String type, String name) {
List<Dlc> dlcList = getDlcList(gameCode, type).stream()
.filter(dlc -> dlc.name().equals(name)).collect(Collectors.toList());
return dlcList.isEmpty() ? null : dlcList.get(0);
}
public int getDlcIndex(String name) {
return dlcExists(name) ? getDlc(name).index() : 0;
}
public boolean dlcExists(String name) {
return name != null && dlcMap.containsKey(name);
public int getDlcIndex(String gameCode, String type, String name) {
Dlc dlc = getDlc(gameCode, type, name);
return dlc == null ? 0 : dlc.index();
}
public Collection<Dlc> getDlc() {
return Collections.unmodifiableCollection(dlcMap.values());
return Collections.unmodifiableCollection(dlcList);
}
}

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

@@ -0,0 +1,7 @@
package entralinked.model.player;
import com.fasterxml.jackson.annotation.JsonProperty;
public record DreamDecor(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name) {}

View File

@@ -6,31 +6,34 @@ import java.util.Collections;
import java.util.List;
import entralinked.GameVersion;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.pkmn.PkmnInfo;
public class Player {
private final String gameSyncId;
private final GameVersion gameVersion;
private final List<DreamEncounter> encounters = new ArrayList<>();
private final List<DreamItem> items = new ArrayList<>();
private final List<AvenueVisitor> avenueVisitors = new ArrayList<>();
private PlayerStatus status;
private GameVersion gameVersion;
private PkmnInfo dreamerInfo;
private int levelsGained;
private String cgearSkin;
private String dexSkin;
private String musical;
public Player(String gameSyncId, GameVersion gameVersion) {
public Player(String gameSyncId) {
this.gameSyncId = gameSyncId;
this.gameVersion = gameVersion;
}
public void resetDreamInfo() {
status = PlayerStatus.AWAKE;
gameVersion = null;
dreamerInfo = null;
encounters.clear();
items.clear();
avenueVisitors.clear();
levelsGained = 0;
cgearSkin = null;
dexSkin = null;
@@ -41,10 +44,6 @@ public class Player {
return gameSyncId;
}
public GameVersion getGameVersion() {
return gameVersion;
}
public void setEncounters(Collection<DreamEncounter> encounters) {
if(encounters.size() <= 10) {
this.encounters.clear();
@@ -67,6 +66,17 @@ public class Player {
return Collections.unmodifiableList(items);
}
public void setAvenueVisitors(Collection<AvenueVisitor> avenueVisitors) {
if(avenueVisitors.size() <= 12) {
this.avenueVisitors.clear();
this.avenueVisitors.addAll(avenueVisitors);
}
}
public List<AvenueVisitor> getAvenueVisitors() {
return Collections.unmodifiableList(avenueVisitors);
}
public void setStatus(PlayerStatus status) {
this.status = status;
}
@@ -75,6 +85,14 @@ public class Player {
return status;
}
public void setGameVersion(GameVersion gameVersion) {
this.gameVersion = gameVersion;
}
public GameVersion getGameVersion() {
return gameVersion;
}
public void setDreamerInfo(PkmnInfo dreamerInfo) {
this.dreamerInfo = dreamerInfo;
}

View File

@@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import entralinked.GameVersion;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.pkmn.PkmnInfo;
/**
@@ -22,19 +23,22 @@ public record PlayerDto(
String musical,
int levelsGained,
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items) {
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors) {
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());
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getLevelsGained(),
player.getEncounters(), player.getItems(),player.getAvenueVisitors());
}
/**
* Constructs a new {@link Player} object using the data in this DTO.
*/
public Player toPlayer() {
Player player = new Player(gameSyncId, gameVersion);
Player player = new Player(gameSyncId);
player.setStatus(status);
player.setGameVersion(gameVersion);
player.setDreamerInfo(dreamerInfo);
player.setCGearSkin(cgearSkin);
player.setDexSkin(dexSkin);
@@ -42,6 +46,7 @@ public record PlayerDto(
player.setLevelsGained(levelsGained);
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
player.setItems(items == null ? Collections.emptyList() : items);
player.setAvenueVisitors(avenueVisitors == null ? Collections.emptyList() : avenueVisitors);
return player;
}
}

View File

@@ -16,8 +16,6 @@ import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.GameVersion;
/**
* Manager class for managing {@link Player} information (Global Link users)
*/
@@ -119,7 +117,7 @@ public class PlayerManager {
* That is, the specified Game Sync ID wasn't already registered and the player data
* was saved without any errors.
*/
public Player registerPlayer(String gameSyncId, GameVersion gameVersion) {
public Player registerPlayer(String gameSyncId) {
// Check for duplicate Game Sync ID
if(playerMap.containsKey(gameSyncId)) {
logger.warn("Attempted to register duplicate player {}", gameSyncId);
@@ -127,7 +125,7 @@ public class PlayerManager {
}
// Construct player object
Player player = new Player(gameSyncId, gameVersion);
Player player = new Player(gameSyncId);
player.setStatus(PlayerStatus.AWAKE);
// Try to save player data

View File

@@ -1,12 +1,25 @@
package entralinked.network.http.dashboard;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Entralinked;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.dlc.Dlc;
import entralinked.model.dlc.DlcList;
import entralinked.model.pkmn.PkmnGender;
@@ -18,6 +31,7 @@ import entralinked.model.player.PlayerManager;
import entralinked.model.player.PlayerStatus;
import entralinked.network.http.HttpHandler;
import entralinked.utility.GsidUtility;
import entralinked.utility.TiledImageReader;
import io.javalin.Javalin;
import io.javalin.config.JavalinConfig;
import io.javalin.http.Context;
@@ -30,20 +44,37 @@ import io.javalin.json.JavalinJackson;
*/
public class DashboardHandler implements HttpHandler {
private final Set<Integer> availableBlackAndWhiteSpecies = Set.of(
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632);
private static final Logger logger = LogManager.getLogger();
private final Map<Dlc, BufferedImage> skinPreviewCache = new HashMap<>();
private final DlcList dlcList;
private final PlayerManager playerManager;
public DashboardHandler(Entralinked entralinked) {
this.dlcList = entralinked.getDlcList();
this.playerManager = entralinked.getPlayerManager();
// Load & cache skin previews
logger.info("Loading C-Gear and Pokédex skin previews ...");
List<Dlc> skins = dlcList.getDlcList(dlc -> dlc.type().startsWith("CGEAR") || dlc.type().equals("ZUKAN"));
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
skinPreviewCache.put(skin, image);
} catch(IOException | IndexOutOfBoundsException e) {
logger.error("Could not load image for skin {} of type {}", skin.name(), skin.type(), e);
}
}
logger.info("Cached {} skin previews", skinPreviewCache.size());
}
@Override
public void addHandlers(Javalin javalin) {
javalin.get("/dashboard/previewskin", this::handlePreviewSkin);
javalin.get("/dashboard/dlc", this::handleRetrieveDlcList);
javalin.get("/dashboard/profile", this::handleRetrieveProfile);
javalin.post("/dashboard/profile", this::handleUpdateProfile);
@@ -71,6 +102,31 @@ public class DashboardHandler implements HttpHandler {
});
}
/**
* GET request handler for {@code /dashboard/previewskin}
*/
private void handlePreviewSkin(Context ctx) throws IOException {
String type = ctx.queryParam("type");
String name = ctx.queryParam("name");
// Make sure query parameters are present
if(type == null || name == null) {
ctx.status(404);
return;
}
Dlc dlc = dlcList.getDlc("IRAO", type, name);
// Check if DLC exists
if(dlc == null) {
ctx.status(404);
return;
}
// Write cached image data
ImageIO.write(skinPreviewCache.get(dlc), "png", ctx.outputStream());
}
/**
* GET request handler for {@code /dashboard/dlc}
*/
@@ -84,7 +140,7 @@ public class DashboardHandler implements HttpHandler {
}
// Send result
ctx.json(dlcList.getDlcList("IRAO", type).stream().map(Dlc::name).collect(Collectors.toList()));
ctx.json(dlcList.getDlcList("IRAO", type).stream().map(Dlc::name).sorted().collect(Collectors.toList()));
}
/**
@@ -170,6 +226,7 @@ public class DashboardHandler implements HttpHandler {
player.setStatus(PlayerStatus.WAKE_READY);
player.setEncounters(request.encounters());
player.setItems(request.items());
player.setAvenueVisitors(request.avenueVisitors());
player.setCGearSkin(request.cgearSkin().equals("none") ? null : request.cgearSkin());
player.setDexSkin(request.dexSkin().equals("none") ? null : request.dexSkin());
player.setMusical(request.musical().equals("none") ? null : request.musical());
@@ -196,15 +253,9 @@ public class DashboardHandler implements HttpHandler {
}
for(DreamEncounter encounter : request.encounters()) {
if(encounter.species() < 1) {
if(encounter.species() < 1 || encounter.species() > 649) {
return "Species is out of range.";
} else if(encounter.species() > 493) {
if(!player.getGameVersion().isVersion2()) {
return "Sorry, Generation V Pokémon are exclusive to Black Version 2 and White Version 2.";
} else if(!availableBlackAndWhiteSpecies.contains(encounter.species())) {
return "You have selected one or more Pokémon species that cannot be downloaded.";
}
} else if(encounter.move() < 1 || encounter.move() > 559) {
} else if(encounter.move() < 0 || encounter.move() > 559) {
return "Move ID is out of range.";
} else if(encounter.gender() == null) {
return "Gender is undefined.";
@@ -223,15 +274,36 @@ public class DashboardHandler implements HttpHandler {
for(DreamItem item : request.items()) {
if(item.id() < 0 || item.id() > 638) {
return "Item ID is out of range";
} else if(item.id() > 626 && !player.getGameVersion().isVersion2()) {
return "You have selected one or more items that are exclusive to Black Version 2 and White Version 2.";
}
if(item.quantity() < 0 || item.quantity() > 20) {
} else if(item.quantity() < 0 || item.quantity() > 20) {
return "Item quantity is out of range.";
}
}
// Validate Join Avenue visitors
Set<String> avenueVisitorNames = new HashSet<>(); // For duplicate checking
if(request.avenueVisitors().size() > 12) {
return "Join Avenue visitor list size exceeds the limit.";
}
for(AvenueVisitor visitor : request.avenueVisitors()) {
if(visitor.type() == null) {
return "Join Avenue visitor type is undefined.";
} else if(visitor.shopType() == null) {
return "Join Avenue visitor shop type is undefined.";
} else if(visitor.name().isBlank() || visitor.name().length() > 7) {
return "Join Avenue visitor name must be between 1 and 7 characters in length.";
} else if(avenueVisitorNames.contains(visitor.name())) {
return "Join Avenue visitors cannot have the same name!";
} else if(visitor.gameVersion() == null) {
return "Join Avenue visitor game version is undefined.";
} else if(visitor.dreamerSpecies() < 1 || visitor.dreamerSpecies() > 649) {
return "Tucked-in Pokémon species of the Join Avenue visitor is out of range.";
}
avenueVisitorNames.add(visitor.name());
}
// Validate gained levels
if(request.gainedLevels() < 0 || request.gainedLevels() > 99) {
return "Gained levels is out of range.";

View File

@@ -2,6 +2,7 @@ package entralinked.network.http.dashboard;
import java.util.Collection;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.pkmn.PkmnInfo;
import entralinked.model.player.DreamEncounter;
import entralinked.model.player.DreamItem;
@@ -16,10 +17,12 @@ public record DashboardProfileMessage(
String musical,
int levelsGained,
Collection<DreamEncounter> encounters,
Collection<DreamItem> items) {
Collection<DreamItem> items,
Collection<AvenueVisitor> avenueVisitors) {
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());
this(player.getGameVersion().getDisplayName(), dreamerSprite, player.getDreamerInfo(),
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getLevelsGained(),
player.getEncounters(), player.getItems(), player.getAvenueVisitors());
}
}

View File

@@ -5,12 +5,14 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.player.DreamEncounter;
import entralinked.model.player.DreamItem;
public record DashboardProfileUpdateRequest(
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamEncounter.class) List<DreamEncounter> encounters,
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamItem.class) List<DreamItem> items,
@JsonProperty(required = true) @JsonDeserialize(contentAs = AvenueVisitor.class) List<AvenueVisitor> avenueVisitors,
@JsonProperty(required = true) String cgearSkin,
@JsonProperty(required = true) String dexSkin,
@JsonProperty(required = true) String musical,

View File

@@ -67,26 +67,22 @@ public class DlsHandler implements HttpHandler {
* POST handler for {@code /download action=list}
*/
private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException {
// Map to generic type, I doubt there is a real difference between the language codes anyway.
String type = switch(request.dlcType()) {
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
default -> request.dlcType();
};
String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType());
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(request.dlcGameCode(), type, request.dlcIndex())));
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
}
/**
* POST handler for {@code /download action=contents}
*/
private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException {
// Check if the requested DLC exists
Dlc dlc = dlcList.getDlc(request.dlcName());
String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType());
Dlc dlc = dlcList.getDlc(gameCode, type, request.dlcName());
// Check if the requested DLC exists
if(dlc == null) {
ctx.status(HttpStatus.NOT_FOUND);
return;
@@ -103,4 +99,27 @@ public class DlsHandler implements HttpHandler {
}
}
}
/**
* @return The game serial that should be used for downloading DLC based on the provided input.
*/
private String getDlcGameCode(String gameCode) {
return switch(gameCode) {
case "IRAJ" -> "IRAO";
default -> gameCode;
};
}
/**
* @return The DLC type without the region identifier, or the input if it is an unknown type.
*/
private String getRegionlessDlcType(String dlcType) {
return switch(dlcType) {
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
default -> dlcType;
};
}
}

View File

@@ -3,6 +3,7 @@ package entralinked.network.http.pgl;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -14,9 +15,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Configuration;
import entralinked.Entralinked;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.dlc.DlcList;
import entralinked.model.pkmn.PkmnInfo;
import entralinked.model.pkmn.PkmnInfoReader;
import entralinked.model.player.DreamDecor;
import entralinked.model.player.DreamEncounter;
import entralinked.model.player.DreamItem;
import entralinked.model.player.Player;
@@ -28,6 +31,7 @@ import entralinked.network.http.HttpHandler;
import entralinked.network.http.HttpRequestHandler;
import entralinked.serialization.UrlEncodedFormFactory;
import entralinked.serialization.UrlEncodedFormParser;
import entralinked.utility.GsidUtility;
import entralinked.utility.LEOutputStream;
import io.javalin.Javalin;
import io.javalin.http.Context;
@@ -45,6 +49,12 @@ public class PglHandler implements HttpHandler {
private static final String password = "2Phfv9MY"; // Best security in the world
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES));
private final List<DreamDecor> decorList = List.of(
new DreamDecor(1, "+----------+"),
new DreamDecor(2, "Thank you"),
new DreamDecor(3, "for using"),
new DreamDecor(4, "Entralinked!"),
new DreamDecor(5, "+----------+"));
private final Set<Integer> sleepyList = new HashSet<>();
private final Configuration configuration;
private final DlcList dlcList;
@@ -89,13 +99,6 @@ public class PglHandler implements HttpHandler {
// Deserialize the request
PglRequest request = mapper.readValue(ctx.queryString(), PglRequest.class);
// Check game version
if(request.gameVersion() == null) {
ctx.status(HttpStatus.BAD_REQUEST);
clearTasks(ctx);
return;
}
// Verify the service session token
ServiceSession session = userManager.getServiceSession(request.token(), "external");
@@ -225,10 +228,10 @@ public class PglHandler implements HttpHandler {
// Write misc stuff and DLC information
outputStream.writeShort(player.getLevelsGained());
outputStream.write(0); // Unknown
outputStream.write(dlcList.getDlcIndex(player.getMusical()));
outputStream.write(dlcList.getDlcIndex(player.getCGearSkin()));
outputStream.write(dlcList.getDlcIndex(player.getDexSkin()));
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(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
outputStream.write(0); // Must be zero?
// Write item IDs
@@ -246,6 +249,61 @@ public class PglHandler implements HttpHandler {
// Write quantity padding
outputStream.writeBytes(0, (20 - items.size()));
// Decor data -- copied to 0x1D420 in the save file
// Need to send 5 entries or nothing will happen
// After decor is selected in Nacrene City, the *index* of it (default: 0x7F) will be saved to 0x1D4A6 in the save file.
for(DreamDecor decor : decorList) {
byte[] nameBytes = decor.name().getBytes(StandardCharsets.UTF_16LE);
// If any ID is 0x7E it will not work. It also appears as the default in the save file.
outputStream.writeShort(decor.id());
// Name can't have more than 12 characters
outputStream.write(nameBytes, 0, Math.min(24, nameBytes.length));
outputStream.writeBytes(-1, 24 - nameBytes.length);
}
// Write decor padding
outputStream.writeBytes(0, (5 - decorList.size()) * 26);
outputStream.writeShort(0); // ?
// Join Avenue visitor data -- copied in parts to 0x2422C in the save file.
// Black Version 2 and White Version 2 only.
if(player.getGameVersion().isVersion2()) {
List<AvenueVisitor> avenueVisitors = player.getAvenueVisitors();
for(AvenueVisitor visitor : avenueVisitors) {
// Write visitor name + padding. Names cannot be duplicate.
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
outputStream.writeBytes(-1, 14 - nameBytes.length);
// Full visitor type consists of a trainer class and what I call a 'personality' index
// that, along with the trainer class, determines which phrases the visitor uses.
// The shope type is calculated in such an odd manner because for some reason,
// the 'starting' index of the shop type used increases by 2 for each visitor type.
// For example, if the visitor type is '0', then shop type '0' would be a raffle.
// However, if the visitor type is '2', then shop type '0' results in a dojo instead.
int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
outputStream.writeShort(-1); // Does nothing, seems to be read as part of the name.
outputStream.write(visitorType);
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
outputStream.writeShort(0); // Does nothing
outputStream.writeInt(1); // [20] Ignores if 0
outputStream.write(visitor.countryCode());
outputStream.write(visitor.stateProvinceCode());
outputStream.write(0); // [26] Ignores if 1
outputStream.write(visitor.gameVersion().getRomCode()); // Affects shop stock
outputStream.write(visitor.type().isFemale() ? 1 : 0);
outputStream.write(0); // [29] Does.. something
outputStream.writeShort(visitor.dreamerSpecies());
}
// Write visitor padding
outputStream.writeBytes(0, (12 - avenueVisitors.size()) * 32);
outputStream.writeInt(0); // 672 is the total -- there shouldn't be anything left after this. Hooray!
}
}
/**
@@ -294,6 +352,7 @@ public class PglHandler implements HttpHandler {
case "savedata.upload" -> this::handleUploadSaveData;
case "savedata.download.finish" -> this::handleDownloadSaveDataFinish;
case "account.create.upload" -> this::handleCreateAccount;
case "account.createdata" -> this::handleCreateData;
default -> throw new IllegalArgumentException("Invalid POST request type: " + request.type());
};
@@ -335,11 +394,10 @@ public class PglHandler implements HttpHandler {
// Prepare response
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
// Check if player exists, has the same game version and does not already have a Pokémon tucked in
// Check if the player exists and does not already have a Pokémon tucked in
Player player = playerManager.getPlayer(request.gameSyncId());
if(player == null || player.getGameVersion() != request.gameVersion()
|| (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
if(player == null || (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
// Skip everything
ServletInputStream inputStream = ctx.req().getInputStream();
@@ -369,6 +427,7 @@ public class PglHandler implements HttpHandler {
// Update and save player information
player.setStatus(PlayerStatus.SLEEPING);
player.setGameVersion(request.gameVersion());
player.setDreamerInfo(dreamerInfo);
if(!playerManager.savePlayer(player)) {
@@ -394,7 +453,37 @@ public class PglHandler implements HttpHandler {
// Prepare response
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
String gameSyncId = request.gameSyncId();
// Make sure Game Sync ID is present
if(request.gameSyncId() == null) {
writeStatusCode(outputStream, 1); // Unauthorized
return;
}
// Check if player doesn't exist already
if(playerManager.doesPlayerExist(request.gameSyncId())) {
writeStatusCode(outputStream, 2); // Duplicate Game Sync ID
return;
}
// Try to register player
if(playerManager.registerPlayer(request.gameSyncId()) == null) {
writeStatusCode(outputStream, 3); // Registration error
return;
}
// Write status code
writeStatusCode(outputStream, 0);
}
/**
* POST handler for {@code /dsio/gw?p=account.createdata}
*
* Seems to be a funny Japanese version quirk
*/
private void handleCreateData(PglRequest request, Context ctx) throws IOException {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky
// Check if player doesn't exist already
if(playerManager.doesPlayerExist(gameSyncId)) {
@@ -403,7 +492,7 @@ public class PglHandler implements HttpHandler {
}
// Try to register player
if(playerManager.registerPlayer(gameSyncId, request.gameVersion()) == null) {
if(playerManager.registerPlayer(gameSyncId) == null) {
writeStatusCode(outputStream, 3); // Registration error
return;
}

View File

@@ -7,12 +7,12 @@ import entralinked.GameVersion;
import entralinked.serialization.GsidDeserializer;
public record PglRequest(
@JsonProperty(value = "gsid", required = true) @JsonDeserialize(using = GsidDeserializer.class) String gameSyncId,
@JsonProperty(value = "p", required = true) String type,
@JsonProperty(value = "rom", required = true) int romCode,
@JsonProperty(value = "langcode", required = true) int languageCode,
@JsonProperty(value = "dreamw", required = true) int dreamWorld, // Always 1, but what is it for?
@JsonProperty(value = "tok", required = true) String token) {
@JsonProperty(value = "gsid") @JsonDeserialize(using = GsidDeserializer.class) String gameSyncId,
@JsonProperty(value = "p", required = true) String type,
@JsonProperty(value = "tok", required = true) String token,
@JsonProperty(value = "rom") int romCode,
@JsonProperty(value = "langcode") int languageCode,
@JsonProperty(value = "dreamw") int dreamWorld) { // Always 1, but what is it for?
public GameVersion gameVersion() {
return GameVersion.lookup(romCode(), languageCode());

View File

@@ -18,7 +18,14 @@ public class NetworkUtility {
public static InetAddress getLocalHost() {
try(Socket socket = new Socket()){
socket.connect(new InetSocketAddress("github.com", 80));
return socket.getLocalAddress();
InetAddress address = socket.getLocalAddress();
// Fall back to the network interface method if this is not a private IP address
if(!address.isSiteLocalAddress()) {
return getLocalHostFromNetworkInterfaces();
}
return address;
} catch(IOException e) {
logger.error("Couldn't get local host using socket - falling back to the network interface method", e);
return getLocalHostFromNetworkInterfaces();
@@ -42,8 +49,8 @@ public class NetworkUtility {
while(addresses.hasMoreElements()) {
InetAddress address = addresses.nextElement();
// Return if IPv4
if(address instanceof Inet4Address) {
// Return this address if it is a local IPv4 address
if(address.isSiteLocalAddress() && address instanceof Inet4Address) {
return address;
}
}
@@ -52,6 +59,7 @@ public class NetworkUtility {
logger.error("Could not determine local host - falling back to loopback address", e);
}
logger.warn("No local host candidate could be found - loopback address will be used");
return InetAddress.getLoopbackAddress();
}
}

View File

@@ -0,0 +1,187 @@
package entralinked.utility;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* 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_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 Logger logger = LogManager.getLogger();
static {
// Load Pokédex skin background color indices
try(InputStream inputStream = TiledImageReader.class.getResourceAsStream("/zukan.bin")) {
int currentIndex = 0;
int valueAmount = -1;
int value = -1;
// Read until end of stream
while((valueAmount = inputStream.read()) != -1 && (value = inputStream.read()) != -1) {
for(int i = 0; i < valueAmount; i++) {
dexBackgroundColorIndices[currentIndex++] = (byte)(value & 63);
}
}
} catch(IOException e) {
logger.error("Could not load Pokédex background data", e);
}
}
/**
* Reads a C-Gear skin from the specified {@link InputStream}.
*
* @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, 0, 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.
*
* @return A {@link BufferedImage} representing the read Pokédex skin data.
*/
public static BufferedImage readDexSkin(InputStream inputStream) throws IOException {
return readTiledImage(inputStream, 768, 64, dexBackgroundColorIndices, 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 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 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, int backgroundColorCount,
byte[] backgroundColorIndices, 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++) {
colorPalette[i] = convertColor(inputStream.read() | inputStream.read() << 8);
}
// 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] = convertColor(inputStream.read() | inputStream.read() << 8);
}
// 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
};
int pixelX = x + j % TILE_WIDTH;
int pixelY = y + j / TILE_WIDTH;
int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex];
// If a background is present and the foreground color index of this pixel is 0, use the background color instead.
// The background color index is determined by the pixel location.
int color = backgroundColorIndices == null || paletteIndex > 0 ? colorPalette[paletteIndex]
: backgroundColorPalette[backgroundColorIndices[pixelY * SCREEN_WIDTH + pixelX]];
// 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 paletteIndex = tileData[i * TILE_SIZE + j];
int color = backgroundColorIndices == null || paletteIndex > 0 ? colorPalette[paletteIndex]
: backgroundColorPalette[backgroundColorIndices[pixelY * SCREEN_WIDTH + pixelX]];
image.setRGB(pixelX, pixelY, color);
}
}
}
return image;
}
/**
* Converts the input BGR555 color value to an RGB888 color value.
*
* @return The RGB888 color value.
*/
private static int convertColor(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;
}
}

View File

@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles/list.css">
</head>
<body onload="fillTableWithItems()">
<div id="main-container" class="root-container">
<label class="big-label">List of items that can be downloaded</label>
<label>NOTE: Item IDs greater than 626 are exclusive to Black Version 2 and White Version 2</label>
<table id="item-table">
<!-- Filled by list.js -->
</table>
</div>
</body>
<script src="scripts/list.js"></script>
</html>

View File

@@ -5,10 +5,13 @@
</head>
<body>
<div class="root-container">
<label for="gsid">Game Sync ID</label><br>
<input type='text' id="gsid" name='gsid' placeholder='XXXXXXXXXX'>
<button id="login" onclick="postLogin()">Login</button>
</div>
<div>
<label for="gsid">Game Sync ID</label><br>
<input type='text' id="gsid" name='gsid' placeholder='XXXXXXXXXX'>
<button id="login" onclick="postLogin()">Login</button>
</div>
</div>
</body>
<script src="scripts/utility.js"></script>
<script src="scripts/login.js"></script>
</html>

View File

@@ -3,141 +3,180 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="styles/profile.css">
</head>
<body onload="fetchProfileData()">
<body>
<div id="main-container" class="root-container" style="display:none;">
<div>
<label id="game-summary" class="header-text"></label>
</div>
<div>
<!-- Dreamer Summary -->
<label>Tucked-in Pokémon Summary</label><br>
<table id="dreamer-summary" class="dreamer-summary">
<tr>
<td id="dreamer-sprite" class="dreamer-sprite" rowspan="5">
<image src="/sprites/pokemon/normal/0.png"/>
</td>
</tr>
<tr>
<th>Species</th>
<td id="dreamer-species"></td>
<th>Nature</th>
<td id="dreamer-nature"></td>
</tr>
<tr>
<th>Name</th>
<td id="dreamer-name"></td>
<th>Gender</th>
<td id="dreamer-gender"></td>
</tr>
<tr>
<th>Trainer</th>
<td id="dreamer-trainer"></td>
<th>Level</th>
<td id="dreamer-level"></td>
</tr>
<tr>
<th>Trainer ID</th>
<td id="dreamer-trainer-id"></td>
</tr>
</table>
</div>
<div>
<!-- Entree Forest Encounter Configuration -->
<label>Entree Forest Encounters (Max. 10)</label><br>
<div>
<table id="encounter-table" class="encounter-image-table">
<label id="game-summary" class="header-text"></label>
</div>
<div>
<!-- Dreamer Summary -->
<label>Tucked-in Pokémon Summary</label><br>
<table id="dreamer-summary" class="dreamer-summary">
<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="dreamer-sprite" class="dreamer-sprite" rowspan="5">
<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>
<th>Species</th>
<td id="dreamer-species"></td>
<th>Ability</th>
<td id="dreamer-ability"></td>
</tr>
<tr>
<th>Name</th>
<td id="dreamer-name"></td>
<th>Nature</th>
<td id="dreamer-nature"></td>
</tr>
<tr>
<th>Trainer</th>
<td id="dreamer-trainer"></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>
</div>
<div>
<!-- Item Configuration -->
<label>Items (Max. 20)</label><br>
<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>
</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>
</tr>
</table>
</div>
</div>
<div>
<!-- Misc Configurations -->
<div class="grid-container">
<!-- Entree Forest Encounter Configuration -->
<label>Entree Forest Encounters (Max. 10)</label><br>
<div>
<label>CGear Skin</label>
<select id="cgear-skin">
<option value="none">Do not change</option>
</select>
</div>
<div>
<label>Pokédex Skin</label>
<select id="dex-skin">
<option value="none">Do not change</option>
</select>
</div>
<div>
<label>Musical</label>
<select id="musical">
<option value="none">Do not change</option>
</select>
</div>
<div>
<label>Level Gain</label>
<input id="level-gain-input" type="number" value="0" min="0" max="99"/>
<table id="encounter-table" class="encounter-image-table">
<tr>
<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 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>
</div>
</div>
<div>
<div id="visitor-table-container" style="display:none;">
<!-- Join Avenue Visitor Configuration -->
<label>Join Avenue Visitors (Max. 12)</label><br>
<div>
<table id="visitor-table" class="visitor-table">
<tr>
<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 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>
</div>
<div>
<!-- Item Configuration -->
<label>Items (Max. 20)</label><br>
<div>
<table id="item-table" class="item-table">
<tr>
<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 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>
</div>
<div>
<!-- Misc Configurations -->
<div class="grid-container">
<div>
<label>C-Gear Skin | </label><a href="#" onclick="return previewSkin('cgear-skin', 'CGEAR')">Preview</a>
<select id="cgear-skin">
<option value="none">Do not change</option>
<option disabled>──────────────────────</option>
</select>
</div>
<div>
<label>Pokédex Skin | </label><a href="#" onclick="return previewSkin('dex-skin', 'ZUKAN')">Preview</a>
<select id="dex-skin">
<option value="none">Do not change</option>
<option disabled>──────────────────────</option>
</select>
</div>
<div>
<label>Musical Show</label>
<select id="musical">
<option value="none">Do not change</option>
<option disabled>──────────────────────</option>
</select>
</div>
<div>
<label>Level Gain</label>
<input id="level-gain-input" type="number" value="0" min="0" max="99"/>
</div>
</div>
</div>
<div>
<button id="save" class="big-button" onclick="postProfileData()">Save Profile</button>
<button id="logout" class="big-button" onclick="postLogout()">Log Out</button>
</div>
</div>
</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">
<label for="encounter-form-species">Species ID | </label><a href="/dashboard/species.html" target="_blank">View list</a>
<input id="encounter-form-species" name="species" type="number" value="1" min="1" max="493"/>
<label for="encounter-form-move">Move ID</label>
<input id="encounter-form-move" name="move" type="number" value="1" min="1" max="559"/>
<label for="encounter-form-form">Forme Index</label>
<input id="encounter-form-form" name="form" type="number" value="0" min="0" max="31"/>
<label for="encounter-form-species">Species</label>
<select id="encounter-form-species" name="species" value="1">
<!-- Filled by profile.js -->
</select>
<label for="encounter-form-move">Special Move</label>
<select id="encounter-form-move" name="move" value="0">
<option value="0">None</option>
<!-- Filled by profile.js -->
</select>
<label for="encounter-form-form">Form</label>
<select id="encounter-form-form" name="form" value="0">
<option value="0">N/A</option>
<!-- Filled by profile.js -->
</select>
<label for="encounter-form-gender">Gender</label>
<select id="encounter-form-gender" name="gender" value="GENDERLESS">
<option value="MALE">Male</option>
@@ -161,12 +200,14 @@
</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 ID | </label><a href="/dashboard/items.html" target="_blank">View list</a>
<input id="item-form-id" name="id" type="number" value="1" min="1" max="626"/>
<label for="item-form-id">Item</label>
<select id="item-form-id" name="id" value="1">
<!-- Filled by profile.js -->
</select>
<label for="item-form-quantity">Quantity</label>
<input id="item-form-quantity" name="quantity" type="number" value="1" min="1" max="20"/>
</form>
@@ -174,6 +215,71 @@
<button class="big-button" onclick="removeItem()">Remove</button>
</div>
</div>
<!-- Join Avenue Visitor Configuration Form -->
<div id="visitor-config" class="popup">
<div class="content">
<button class="close-button" onclick="closeVisitorForm()">X</button>
<form id="visitor-form">
<label for="visitor-form-name">Name (Max. 7 characters)</label>
<input id="visitor-form-name" name="name" placeholder="Trainer" maxlength="7"/>
<label for="visitor-form-type">Trainer Class</label>
<select id="visitor-form-type" name="type" value="ACE_TRAINER_MALE">
<option value="YOUNGSTER">Youngster</option>
<option value="LASS">Lass</option>
<option value="ACE_TRAINER_MALE">Ace Trainer (Male)</option>
<option value="ACE_TRAINER_FEMALE">Ace Trainer (Female)</option>
<option value="RANGER_MALE">Pokémon Ranger (Male)</option>
<option value="RANGER_FEMALE">Pokémon Ranger (Female)</option>
<option value="BREEDER_MALE">Pokémon Breeder (Male)</option>
<option value="BREEDER_FEMALE">Pokémon Breeder (Female)</option>
<option value="SCIENTIST_MALE">Scientist (Male)</option>
<option value="SCIENTIST_FEMALE">Scientist (Female)</option>
<option value="HIKER">Hiker</option>
<option value="PARASOL_LADY">Parasol Lady</option>
<option value="ROUGHNECK">Roughneck</option>
<option value="NURSE">Nurse</option>
<option value="PRESCHOOLER_MALE">Preschooler (Male)</option>
<option value="PRESCHOOLER_FEMALE">Preschooler (Female)</option>
</select>
<label for="visitor-form-shop-type">Shop Type</label>
<select id="visitor-form-shop-type" name="shopType" value="RAFFLE">
<option value="RAFFLE">Raffle Shop</option>
<option value="FLORIST">Flower Shop</option>
<option value="SALON">Beauty Salon</option>
<option value="ANTIQUE">Antique Shop</option>
<option value="DOJO">Training Dojo</option>
<option value="CAFE">Café</option>
<option value="MARKET">Market</option>
</select>
<label for="visitor-form-game">Game of Origin (Affects which goods/services are sold)</label>
<select id="visitor-form-game" name="gameVersion" value="BLACK_ENGLISH">
<option value="BLACK_ENGLISH">Black Version</option>
<option value="WHITE_ENGLISH">White Version</option>
<option value="BLACK_2_ENGLISH">Black Version 2</option>
<option value="WHITE_2_ENGLISH">White Version 2</option>
</select>
<label for="visitor-form-region">Country</label>
<select id="visitor-form-region" name="region" value="1">
<!-- Filled by profile.js -->
</select>
<label for="visitor-form-subregion">State/Province</label>
<select id="visitor-form-subregion" name="subregion" value="0">
<option value="0">N/A</option>
<!-- Filled by profile.js -->
</select>
<label for="visitor-form-personality">Personality (Affects phrases used)</label>
<input id="visitor-form-personality" name="personality" type="number" value="0" min="0" max="7"/>
<label for="visitor-form-dreamer">Tucked-in Pokémon Species</label>
<select id="visitor-form-dreamer" name="dreamerSpecies" value="1">
<!-- Filled by profile.js -->
</select>
</form>
<button class="big-button" onclick="saveVisitor()">Confirm</button>
<button class="big-button" onclick="removeVisitor()">Remove</button>
</div>
</div>
</body>
<script src="scripts/utility.js"></script>
<script src="scripts/pokedata.js"></script>
<script src="scripts/profile.js"></script>
</html>

View File

@@ -1,61 +0,0 @@
const AVAILABLE_GENERATION_V_POKEMON = new Array(
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632);
const ITEMS_TO_EXCLUDE = new Array(113, 114, 115, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 426, 427);
function fillTable(tableId, rowSize, elementCount, htmlAppender) {
let tableHTML = "";
let index = 0;
for(let value = 1; value <= elementCount; value++) {
// Call the provided HTML appender to see what should be appended to the table
let toAppend = htmlAppender(value);
// If there is nothing to append (that is, the function has decided this element should be skipped)
// then continue to the next loop.
if(!toAppend) {
continue;
}
// Open a new table row if it should
if(index % rowSize == 0) {
tableHTML += "<tr>";
}
// Append the HTML data
tableHTML += toAppend;
// Close the table row if it has reached the maximum number of elements
if(index % rowSize == rowSize) {
tableHTML += "<tr>";
}
index++;
}
// Set the inner HTML
document.getElementById(tableId).innerHTML = tableHTML;
}
function fillTableWithSpecies() {
fillTable("species-table", 10, 649, (species) => {
// Skip this element if species is from Generation V and cannot be downloaded
if(species > 493 && !AVAILABLE_GENERATION_V_POKEMON.includes(species)) {
return false;
}
return "<td style='width:96px;height:96px;'><image src='/sprites/pokemon/normal/" + species + ".png'/><br>#" + species + "</td>";
});
}
function fillTableWithItems() {
fillTable("item-table", 20, 638, (item) => {
if(ITEMS_TO_EXCLUDE.includes(item)) {
return false;
}
return "<td style='width:48px;height:48px;'><image src='/sprites/items/" + item + ".png'/><br>#" + item + "</td>"
});
}

View File

@@ -1,20 +1,11 @@
const ELEMENT_GSID_INPUT = document.getElementById("gsid");
function postLogin() {
let loginData = {
fetchData("POST", "/dashboard/login", new URLSearchParams({
gsid: ELEMENT_GSID_INPUT.value
}
fetch("/dashboard/login", {
method: "POST",
body: new URLSearchParams(loginData)
}).then((response) => {
return response.json();
}).then((response) => {
console.log(response);
if(response.error) {
alert(response.message);
})).then((response) => {
if(response.error) {
window.alert(response.message);
} else {
window.location.href = "/dashboard/profile.html";
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
// HTML document elements
const ELEMENT_GAME_SUMMARY = document.getElementById("game-summary");
// Dreamer elements
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,128 +11,242 @@ 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");
// Encounter form elements
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");
// Item form elements
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");
// Misc input elements
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");
const ELEMENT_VISITOR_GAME = document.getElementById("visitor-form-game");
const ELEMENT_VISITOR_REGION = document.getElementById("visitor-form-region");
const ELEMENT_VISITOR_SUBREGION = document.getElementById("visitor-form-subregion");
const ELEMENT_VISITOR_PERSONALITY = document.getElementById("visitor-form-personality");
const ELEMENT_VISITOR_DREAMER = document.getElementById("visitor-form-dreamer");
const ELEMENT_CGEAR_SKIN_INPUT = document.getElementById("cgear-skin");
const ELEMENT_DEX_SKIN_INPUT = document.getElementById("dex-skin");
const ELEMENT_MUSICAL_INPUT = document.getElementById("musical");
const ELEMENT_LEVEL_GAIN_INPUT = document.getElementById("level-gain-input");
// Create event listeners
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", clampValue);
ELEMENT_ENCOUNTER_MOVE.addEventListener("change", clampValue);
ELEMENT_ITEM_ID.addEventListener("change", clampValue);
ELEMENT_ITEM_QUANTITY.addEventListener("change", clampValue);
ELEMENT_LEVEL_GAIN_INPUT.addEventListener("change", clampValue);
function clampValue() {
let value = parseInt(this.value);
if(value < this.min) {
this.value = this.min;
} else if(value > this.max) {
console.log(value);
this.value = this.max;
}
}
// Other constant stuff
const AVAILABLE_GENERATION_V_POKEMON = new Array(
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632); // Defining this 3 times is a brilliant idea.
// Local variables
var encounterTableIndex = -1;
var itemTableIndex = -1;
var visitorTableIndex = -1;
var profile = {
encounters: [],
items: []
items: [],
visitors: []
};
(async function() {
// Create event listeners
clampOnChange(ELEMENT_ITEM_QUANTITY);
clampOnChange(ELEMENT_LEVEL_GAIN_INPUT);
clampOnChange(ELEMENT_VISITOR_PERSONALITY);
// Fetch profile data
await fetchData("GET", "/dashboard/profile").then((response) => {
// Update game summary
profile.gameVersion = response.gameVersion;
ELEMENT_GAME_SUMMARY.innerHTML = "Game Card in use: " + profile.gameVersion;
// Update dreamer summary
if(response.dreamerInfo) {
let dreamerInfo = response.dreamerInfo;
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + response.dreamerSprite + "'/>";
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);
ELEMENT_DREAMER_TRAINER.innerHTML = dreamerInfo.trainerName;
ELEMENT_DREAMER_TRAINER_ID.innerHTML = ("0000" + dreamerInfo.trainerId).slice(-5);
ELEMENT_DREAMER_LEVEL.innerHTML = dreamerInfo.level;
}
// Update encounter table
if(response.encounters){
profile.encounters = response.encounters;
updateEncounterTable(0, 10);
}
// Update item table
if(response.items){
profile.items = response.items;
updateItemTable(0, 20);
}
// Update Join Avenue visitor table
if(response.avenueVisitors) {
profile.visitors = response.avenueVisitors;
updateVisitorTable(0, 12);
}
// Update selected DLC
profile.cgearSkin = response.cgearSkin ? response.cgearSkin : "none";
profile.dexSkin = response.dexSkin ? response.dexSkin : "none";
profile.musical = response.musical ? response.musical : "none";
fetchDlcData();
ELEMENT_LEVEL_GAIN_INPUT.value = response.levelsGained;
// Show Join Avenue visitor table if Black 2 or White 2
if(isVersion2()) {
document.getElementById("visitor-table-container").style.display = "block";
}
// Show div
document.getElementById("main-container").style.display = "flex";
});
// Sort data lists alphabetically
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
for(let i in sortedSpecies) {
let species = sortedSpecies[i];
ELEMENT_VISITOR_DREAMER.options[ELEMENT_VISITOR_DREAMER.options.length] = new Option(species.name, species.id);
if(species.downloadable && (isVersion2() || species.id <= 493)) {
ELEMENT_ENCOUNTER_SPECIES.options[ELEMENT_ENCOUNTER_SPECIES.options.length] = new Option(species.name, species.id);
}
}
// Add move data
for(let i in sortedMoves) {
let move = sortedMoves[i];
ELEMENT_ENCOUNTER_MOVE.options[ELEMENT_ENCOUNTER_MOVE.options.length] = new Option(move.name, move.id);
}
// Add item data
for(let i in sortedItems) {
let item = sortedItems[i];
if(isVersion2() || item.id <= 626) {
ELEMENT_ITEM_ID.options[ELEMENT_ITEM_ID.options.length] = new Option(item.name, item.id);
}
}
// Add region data (already sorted alphabetically)
for(let i in REGION_LIST) {
let region = REGION_LIST[i];
ELEMENT_VISITOR_REGION.options[ELEMENT_VISITOR_REGION.options.length] = new Option(region.name, region.id);
}
// Event listener for changing the form & gender selector contents when species changes
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", function() {
ELEMENT_ENCOUNTER_FORM.value = updateEncounterFormOptions();
ELEMENT_ENCOUNTER_GENDER.value = updateEncounterGenderOptions();
});
// Same thing, but for Join Avenue visitor region & subregion
ELEMENT_VISITOR_REGION.addEventListener("change", function() {
ELEMENT_VISITOR_SUBREGION.value = updateVisitorSubregionOptions();
});
})();
/**
* Encounter configuration stuff
*/
function updateEncounterFormOptions() {
clearSelectOptions(ELEMENT_ENCOUNTER_FORM);
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
// Update special form options
if(species.forms) {
for(let i in species.forms) {
ELEMENT_ENCOUNTER_FORM.options[ELEMENT_ENCOUNTER_FORM.options.length] = new Option(species.forms[i], i);
}
} 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));
// Load existing settings
let encounter = profile.encounters[encounterTableIndex];
ELEMENT_ENCOUNTER_SPECIES.value = encounter ? encounter.species : 1;
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 1;
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_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 : 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() {
if(encounterTableIndex < 0) {
closeEncounterForm();
return;
}
// Check if this species can be downloaded
let species = parseInt(ELEMENT_ENCOUNTER_SPECIES.value);
if(species > 493 && !AVAILABLE_GENERATION_V_POKEMON.includes(species)) {
alert("This Pokémon species cannot be downloaded. Click 'View list' in the encounter form to view a list of available Pokémon.");
return;
}
// Create encounter data
let encounterData = {
species: species,
profile.encounters[encounterTableIndex] = {
species: ELEMENT_ENCOUNTER_SPECIES.value,
move: ELEMENT_ENCOUNTER_MOVE.value,
form: ELEMENT_ENCOUNTER_FORM.value,
gender: ELEMENT_ENCOUNTER_GENDER.value,
animation: ELEMENT_ENCOUNTER_ANIMATION.value
}
// Set form to highest form available if it too great
let maxForm = 0;
switch(species) {
case 201: maxForm = 27; break; // Unown
case 386: maxForm = 3; break; // Deoxys
case 412:
case 413: maxForm = 2; break; // Burmy & Wormadam
case 422:
case 423:
case 487: maxForm = 1; break; // Shellos, Gastrodon & Giratina
case 479: maxForm = 5; break; // Rotom
case 493: maxForm = 16; break; // Arceus
case 550: maxForm = 1; break; // Basculin
}
if(encounterData.form > maxForm) {
encounterData.form = maxForm;
}
profile.encounters[encounterTableIndex] = encounterData;
};
updateEncounterCell(encounterTableIndex);
closeEncounterForm();
}
function removeEncounter() {
if(encounterTableIndex < 0) {
closeEncounterForm();
return;
}
let oldLength = profile.encounters.length;
profile.encounters.splice(encounterTableIndex, 1);
for(let i = encounterTableIndex; i < oldLength; i++) {
updateEncounterTable(encounterTableIndex, oldLength);
closeEncounterForm();
}
function updateEncounterTable(startIndex, endIndex) {
for(let i = startIndex; i < endIndex; i++) {
updateEncounterCell(i);
}
closeEncounterForm();
}
function updateEncounterCell(index) {
@@ -144,11 +259,19 @@ function updateEncounterCell(index) {
spriteImage = spriteBase + encounterData.species + ".png";
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;
}
}
}
@@ -157,46 +280,169 @@ function updateEncounterCell(index) {
function closeEncounterForm() {
encounterTableIndex = -1;
window.location.href = "#";
ELEMENT_ENCOUNTER_CONFIG.style.display = "none";
}
/**
* Join Avenue visitor configuration stuff
*/
function updateVisitorSubregionOptions() {
clearSelectOptions(ELEMENT_VISITOR_SUBREGION);
let region = REGION_MAP[ELEMENT_VISITOR_REGION.value];
// Update subregion options
if(region.subregions) {
let sortedSubregions = [...region.subregions].sort((a, b) => a.name.localeCompare(b.name));
for(let i in sortedSubregions) {
let subregion = sortedSubregions[i];
ELEMENT_VISITOR_SUBREGION.options[ELEMENT_VISITOR_SUBREGION.options.length] = new Option(subregion.name, subregion.id);
}
return 1;
} else {
ELEMENT_VISITOR_SUBREGION.options[ELEMENT_VISITOR_SUBREGION.options.length] = new Option("N/A", 0);
}
return 0;
}
function configureVisitor(index) {
visitorTableIndex = Math.min(12, Math.min(index, profile.visitors.length));
let visitor = profile.visitors[visitorTableIndex];
ELEMENT_VISITOR_NAME.value = visitor ? visitor.name : "";
ELEMENT_VISITOR_TYPE.value = visitor ? visitor.type : "ACE_TRAINER_MALE";
ELEMENT_VISITOR_SHOP_TYPE.value = visitor ? visitor.shopType : "RAFFLE";
ELEMENT_VISITOR_GAME.value = visitor ? visitor.gameVersion : "BLACK_ENGLISH";
ELEMENT_VISITOR_REGION.value = visitor ? visitor.countryCode : 1;
updateVisitorSubregionOptions();
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() {
if(visitorTableIndex < 0) {
closeVisitorForm();
return;
}
// Check if name is empty
if(ELEMENT_VISITOR_NAME.value == "") {
alert("Please enter a name for this visitor.");
return;
}
// Check if name is duplicate
for(i in profile.visitors) {
if(i != visitorTableIndex) {
let visitor = profile.visitors[i];
if(visitor.name == ELEMENT_VISITOR_NAME.value) {
alert("A visitor with this name already exists!")
return;
}
}
}
profile.visitors[visitorTableIndex] = {
name: ELEMENT_VISITOR_NAME.value,
type: ELEMENT_VISITOR_TYPE.value,
shopType: ELEMENT_VISITOR_SHOP_TYPE.value,
gameVersion: ELEMENT_VISITOR_GAME.value,
countryCode: ELEMENT_VISITOR_REGION.value,
stateProvinceCode: ELEMENT_VISITOR_SUBREGION.value,
personality: ELEMENT_VISITOR_PERSONALITY.value,
dreamerSpecies: ELEMENT_VISITOR_DREAMER.value
};
updateVisitorCell(visitorTableIndex);
closeVisitorForm();
}
function removeVisitor() {
if(visitorTableIndex < 0) {
closeVisitorForm();
return;
}
let oldLength = profile.visitors.length;
profile.visitors.splice(visitorTableIndex, 1);
updateVisitorTable(visitorTableIndex, oldLength);
closeVisitorForm();
}
function updateVisitorTable(startIndex, endIndex) {
for(let i = startIndex; i < endIndex; i++) {
updateVisitorCell(i);
}
}
function updateVisitorCell(index) {
let cell = document.getElementById("visitor" + index);
let visitor = profile.visitors[index];
let spriteBase = "/sprites/trainers/";
let spriteImage = spriteBase + "none.png";
if(visitor) {
let newSpriteImage = spriteBase + visitor.type.toLowerCase() + ".png";
if(checkURL(newSpriteImage)){
spriteImage = newSpriteImage;
}
}
cell.innerHTML = "<img src='" + spriteImage + "'/>";
}
function closeVisitorForm() {
visitorTableIndex = -1;
ELEMENT_VISITOR_CONFIG.style.display = "none";
}
/**
* Item configuration stuff
*/
function configureItem(index) {
itemTableIndex = Math.min(20, Math.min(index, profile.items.length));
// Loadg existing settings
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() {
if(itemTableIndex < 0) {
closeItemForm();
return;
}
let itemData = {
profile.items[itemTableIndex] = {
id: ELEMENT_ITEM_ID.value,
quantity: ELEMENT_ITEM_QUANTITY.value
}
profile.items[itemTableIndex] = itemData;
};
updateItemCell(itemTableIndex);
closeItemForm();
}
function removeItem() {
if(itemTableIndex < 0) {
closeItemForm();
return;
}
let oldLength = profile.items.length;
profile.items.splice(itemTableIndex, 1);
for(let i = itemTableIndex; i < oldLength; i++) {
updateItemTable(itemTableIndex, oldLength);
closeItemForm();
}
function updateItemTable(startIndex, endIndex) {
for(let i = startIndex; i < endIndex; i++) {
updateItemCell(i);
}
closeItemForm();
}
function updateItemCell(index) {
@@ -220,171 +466,77 @@ function updateItemCell(index) {
function closeItemForm() {
itemTableIndex = -1;
window.location.href = "#";
ELEMENT_ITEM_CONFIG.style.display = "none";
}
async function fetchData(path) {
return fetchData(path, "GET", null);
}
/**
* Miscellaneous stuff
*/
async function fetchData(path, method, body) {
let response = await fetch(path, {
method: method,
body: body
});
function previewSkin(inputElementId, type) {
let value = document.getElementById(inputElementId).value;
// Return to login page if unauthorized
if(response.status == 401) {
window.location.href = "/dashboard/login.html";
return;
if(value == "none") {
window.alert("Please select a skin to preview it.");
return false;
}
try {
return await response.json();
} catch(error) {
window.alert(error);
if(type == "CGEAR" && isVersion2()) {
type = "CGEAR2";
}
return null;
window.open("/dashboard/previewskin?type=" + type + "&name=" + value);
return false;
}
function fetchDlcData() {
let cgearType = profile.gameVersion.includes("2") ? "CGEAR2" : "CGEAR"; // Not a good way to do this!
// Fetch CGear skins
fetchData("/dashboard/dlc?type=" + cgearType).then((response) => {
addValuesToComboBox(ELEMENT_CGEAR_SKIN_INPUT, response);
ELEMENT_CGEAR_SKIN_INPUT.value = profile.cgearSkin;
// 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";
});
// Fetch Dex skins
fetchData("/dashboard/dlc?type=ZUKAN").then((response) => {
addValuesToComboBox(ELEMENT_DEX_SKIN_INPUT, response);
ELEMENT_DEX_SKIN_INPUT.value = profile.dexSkin;
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";
});
// Fetch musicals
fetchData("/dashboard/dlc?type=MUSICAL").then((response) => {
addValuesToComboBox(ELEMENT_MUSICAL_INPUT, response);
ELEMENT_MUSICAL_INPUT.value = profile.musical;
fetchData("GET", "/dashboard/dlc?type=MUSICAL").then((response) => {
addDlcNames(ELEMENT_MUSICAL_INPUT, response);
ELEMENT_MUSICAL_INPUT.value = response.includes(profile.musical) ? profile.musical : "none";
});
}
// TODO
function fetchProfileData() {
fetchData("/dashboard/profile").then((response) => {
let gameVersion = response["gameVersion"];
let dreamerSprite = response["dreamerSprite"];
let dreamerInfo = response["dreamerInfo"];
let encounters = response["encounters"];
let items = response["items"];
let cgearSkin = response["cgearSkin"];
let dexSkin = response["dexSkin"];
let musical = response["musical"];
let levelsGained = response["levelsGained"];
// Update game summary
profile.gameVersion = gameVersion;
ELEMENT_GAME_SUMMARY.innerHTML = "Game Card in use: " + gameVersion;
// Still don't like this!
if(gameVersion.includes("2")) {
ELEMENT_ENCOUNTER_SPECIES.max = 649;
ELEMENT_ITEM_ID.max = 638;
}
// Update dreamer summary
if(dreamerInfo) {
let species = dreamerInfo["species"];
let nature = dreamerInfo["nature"];
let nickname = dreamerInfo["nickname"];
let gender = dreamerInfo["gender"];
let trainerName = dreamerInfo["trainerName"];
let trainerId = dreamerInfo["trainerId"];
let level = dreamerInfo["level"];
// Set element values
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + dreamerSprite + "'/>";
ELEMENT_DREAMER_SPECIES.innerHTML = "#" + species;
ELEMENT_DREAMER_NATURE.innerHTML = stringToWord(nature);
ELEMENT_DREAMER_NAME.innerHTML = nickname;
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(gender);
ELEMENT_DREAMER_TRAINER.innerHTML = trainerName;
ELEMENT_DREAMER_TRAINER_ID.innerHTML = trainerId;
ELEMENT_DREAMER_LEVEL.innerHTML = level;
}
// Update encounter table
if(encounters){
profile.encounters = encounters;
for(let i = 0; i < 10; i++) {
updateEncounterCell(i);
}
}
// Update item table
if(items){
profile.items = items;
for(let i = 0; i < 20; i++) {
updateItemCell(i);
}
}
// Update selected DLC
profile.cgearSkin = cgearSkin ? cgearSkin : "none";
profile.dexSkin = dexSkin ? dexSkin : "none";
profile.musical = musical ? musical : "none";
fetchDlcData();
// Update level gain
ELEMENT_LEVEL_GAIN_INPUT.value = levelsGained;
// Show div
document.getElementById("main-container").style.display = "grid";
});
function addDlcNames(selectElement, names) {
for(let i in names) {
let name = names[i];
selectElement.options[selectElement.options.length] = new Option(name.replace(/\.[^/.]+$/, ""), name);
}
}
function postProfileData() {
// Construct body
let profileData = {
fetchData("POST", "/dashboard/profile", JSON.stringify({
encounters: profile.encounters,
items: profile.items,
avenueVisitors: profile.visitors,
cgearSkin: ELEMENT_CGEAR_SKIN_INPUT.value,
dexSkin: ELEMENT_DEX_SKIN_INPUT.value,
musical: ELEMENT_MUSICAL_INPUT.value,
gainedLevels: ELEMENT_LEVEL_GAIN_INPUT.value
}
// Send data
fetchData("/dashboard/profile", "POST", JSON.stringify(profileData)).then((response) => {
})).then((response) => {
alert(response.message);
});
}
function postLogout() {
fetchData("/dashboard/logout", "POST", null).then((response) => {
fetchData("POST", "/dashboard/logout").then((response) => {
// Assume it succeeded
window.location.href = "/dashboard/login.html";
});
}
// TODO bad
function checkURL(url) {
var request = new XMLHttpRequest();
request.open('HEAD', url, false);
request.send();
return request.status == 200;
}
function stringToWord(string) {
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
}
function addValuesToComboBox(selectorElement, values) {
for(i in values) {
let value = values[i];
selectorElement.options[selectorElement.options.length] = new Option(value, value);
}
function isVersion2() {
return profile.gameVersion.includes("2");
}

View File

@@ -0,0 +1,59 @@
function clampOnChange(inputElement) {
inputElement.addEventListener("change", function() {
let value = parseInt(this.value);
if(value < this.min) {
this.value = this.min;
} else if(value > this.max) {
this.value = this.max;
}
});
}
function clearSelectOptions(selectElement) {
let startIndex = selectElement.options.length - 1;
for(i = startIndex; i >= 0; i--) {
selectElement.remove(i);
}
}
function stringToWord(string) {
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
}
function checkURL(url) {
var request = new XMLHttpRequest();
request.open('HEAD', url, false);
request.send();
return request.status == 200;
}
async function fetchData(method, path) {
return fetchData(method, path, null);
}
async function fetchData(method, path, body) {
let response = await fetch(path, {
method: method,
body: body
});
if(response.status != 200) {
if(response.status == 401) {
window.location.href = "/dashboard/login.html"; // TODO not epic idea to put this here
return {};
}
window.alert("Server returned status code " + response.status + " while fetching " + path);
return {};
}
try {
return await response.json();
} catch(error) {
window.alert("Could not deserialize JSON response: " + error);
}
return {};
}

View File

@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles/list.css">
</head>
<body onload="fillTableWithSpecies()">
<div id="main-container" class="root-container"">
<label class="big-label">List of Pokémon species that can be downloaded</label>
<label>NOTE: Generation V Pokémon are exclusive to Black Version 2 and White Version 2</label>
<table id="species-table">
<!-- Filled by list.js -->
</table>
</div>
</body>
<script src="scripts/list.js"></script>
</html>

View File

@@ -1,33 +0,0 @@
body {
background-color: #191919;
color: white;
font-size: 20px;
}
label {
text-align: center;
}
table {
border-spacing: 8px;
margin-top: 15px;
margin-bottom: 20px;
}
td {
background-color: #262626;
border-radius: 10px;
text-align: center;
}
.root-container {
display: grid;
position: absolute;
left: 50%;
transform: translate(-50%, 0%);
width: 1136; /* Hack-ish */
}
.big-label {
font-size: 36px;
}

View File

@@ -30,9 +30,8 @@ button:active {
}
.root-container {
position: absolute;
left: 50%;
top: 25%;
transform: translate(-50%, 0%);
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@@ -40,18 +40,20 @@ a:link, a:visited {
}
.root-container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 10px;
max-width: 600px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.root-container div:first-child {
max-width: 532px;
margin: auto;
}
.grid-container {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
grid-template-columns: auto auto;
gap: 5px 10px;
}
@@ -65,26 +67,29 @@ a:link, a:visited {
}
.dreamer-summary {
width: 100%;
/* Idk why it's like this but yeah nonsense like this is why I hate CSS */
width: calc(100% + 4px);
margin-top: 3px;
margin-left: -2px;
margin-bottom: 20px;
table-layout: fixed;
}
.dreamer-summary th {
text-align: left;
padding-left: 10px;
width: 64px;
}
.dreamer-summary td {
width: 96px;
background-color: #262626;
border-radius: 10px;
padding-left: 5px;
}
.dreamer-sprite {
.dreamer-summary td#dreamer-sprite {
width: 96px;
text-align: center;
padding-left: 0px;
}
.header-text {
@@ -95,20 +100,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 {
@@ -122,16 +120,18 @@ a:link, a:visited {
background-color: #191919;
position: absolute;
left: 50%;
top: 25%;
transform: translate(-50%, 0%);
top: 50%;
transform: translate(-50%, -50%);
padding: 20px;
width: 300px;
width: 500px;
}
.encounter-image-table {
width: 100%;
width: calc(100% + 16px);
border-spacing: 8px;
margin-bottom: 20px;
margin-left: -8px;
margin-top: -4px;
margin-bottom: 14px;
}
.encounter-image-table td {
@@ -140,12 +140,15 @@ a:link, a:visited {
width: 96px;
height: 96px;
text-align: center;
cursor: pointer;
}
.item-table {
width: 100%;
width: calc(100% + 16px);
border-spacing: 8px;
margin-bottom: 20px;
margin-left: -8px;
margin-top: -4px;
margin-bottom: 14px;
}
.item-table td {
@@ -154,6 +157,7 @@ a:link, a:visited {
width: 48px;
height: 48px;
text-align: center;
cursor: pointer;
}
.item-table td a {
@@ -162,7 +166,25 @@ a:link, a:visited {
text-decoration: none;
}
.visitor-table {
width: calc(100% + 16px);
border-spacing: 8px;
margin-left: -8px;
margin-top: -4px;
margin-bottom: 14px;
}
.visitor-table td {
background-color: #262626;
border-radius: 10px;
width: 80px;
height: 80px;
text-align: center;
cursor: pointer;
}
.big-button {
font-size: 16px;
padding: 16px 32px;
margin-bottom: 8px;
}

View File

@@ -1,44 +1,59 @@
/dlc/IRAO/CGEAR/C003_munna_1_en.bin
/dlc/IRAO/CGEAR/C004_pikachu_1_e.bin
/dlc/IRAO/CGEAR/C005_chirami_1_e.bin
/dlc/IRAO/CGEAR/C007_tabunne_1_e.bin
/dlc/IRAO/CGEAR/C008_pocchama_1_e.bin
/dlc/IRAO/CGEAR/C009_guregguru_1_e.bin
/dlc/IRAO/CGEAR/C010_fushigibana_1_e.bin
/dlc/IRAO/CGEAR/C011_rizadon_1_e.bin
/dlc/IRAO/CGEAR/C012_kamekkusu_1_e.bin
/dlc/IRAO/CGEAR/C013_zekurom_1_e_v02.bin
/dlc/IRAO/CGEAR/C014_reshiram_1_e.bin
/dlc/IRAO/CGEAR/C015_vikutini_1_e.bin
/dlc/IRAO/CGEAR/C017_porigonz_0_e.bin
/dlc/IRAO/CGEAR/C018_WCS_1_e.bin
/dlc/IRAO/CGEAR/C019_shikijikaharu_1_e.bin
/dlc/IRAO/CGEAR/C020_shikijikanatu_1_e.bin
/dlc/IRAO/CGEAR/C021_shikijikaaki_1_e.bin
/dlc/IRAO/CGEAR/C022_shikijikafuyu_1_e.bin
/dlc/IRAO/CGEAR/C023_iwaparesu_1_e.bin
/dlc/IRAO/CGEAR/C024_zoroark_1_e.bin
/dlc/IRAO/CGEAR/C025_giaru_1_e.bin
/dlc/IRAO/CGEAR/C030_kerudhio_1_en.bin
/dlc/IRAO/CGEAR/C031-1_meroetta_1_e.bin
/dlc/IRAO/CGEAR/C032-1_wcs2012_1_e.bin
/dlc/IRAO/CGEAR/C100_defo_1_en.bin
/dlc/IRAO/CGEAR2/C003-2_munna_1_en.bin
/dlc/IRAO/CGEAR2/C013-2_zekrom_1_e.bin
/dlc/IRAO/CGEAR2/C014-2_reshiram_1_e.bin
/dlc/IRAO/CGEAR2/C015-2_victini_1_e.bin
/dlc/IRAO/CGEAR2/C030-2_kerudhio_1_en.bin
/dlc/IRAO/CGEAR2/C031-2_meroetta_1_e.bin
/dlc/IRAO/CGEAR2/C033_halloween_1_en.bin
/dlc/IRAO/CGEAR2/C035_BK_1_e.bin
/dlc/IRAO/CGEAR2/C036_WK_1_e.bin
/dlc/IRAO/CGEAR2/C100-2_default_1_e.bin
/dlc/IRAO/MUSICAL/M010_munna_1_e_02.bin
/dlc/IRAO/MUSICAL/M013_meloetta_1_e.bin
/dlc/IRAO/ZUKAN/Z003_BWsyokigirl_1_en.bin
/dlc/IRAO/ZUKAN/Z004_BWsyokiboy_1_en.bin
/dlc/IRAO/ZUKAN/Z007_hyuu_1_en.bin
/dlc/IRAO/ZUKAN/Z008_bell_1_e.bin
/dlc/IRAO/ZUKAN/Z009_tyeren_1_e.bin
/dlc/IRAO/ZUKAN/Z100_defogirl_1_en.bin
/dlc/IRAO/ZUKAN/Z101_defoboy_1_en.bin
/dlc/IRAO/CGEAR/01 - Default.bin
/dlc/IRAO/CGEAR/02 - Meadow Munna.bin
/dlc/IRAO/CGEAR/03 - Aim for the Top.bin
/dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin
/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin
/dlc/IRAO/CGEAR/06 - Earful Audino.bin
/dlc/IRAO/CGEAR/07 - Happy Piplup.bin
/dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin
/dlc/IRAO/CGEAR/09 - Venusaur!.bin
/dlc/IRAO/CGEAR/10 - Charizard!.bin
/dlc/IRAO/CGEAR/11 - Blastoise!.bin
/dlc/IRAO/CGEAR/12 - V for Victory!.bin
/dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin
/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin
/dlc/IRAO/CGEAR/15 - Hero Reshiram.bin
/dlc/IRAO/CGEAR/16 - Hero Zekrom.bin
/dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin
/dlc/IRAO/CGEAR/18 - Zoroark!.bin
/dlc/IRAO/CGEAR/19 - Spring Deerling.bin
/dlc/IRAO/CGEAR/20 - Summer Deerling.bin
/dlc/IRAO/CGEAR/21 - Autumn Deerling.bin
/dlc/IRAO/CGEAR/22 - Winter Deerling.bin
/dlc/IRAO/CGEAR/23 - CRUSTLE!.bin
/dlc/IRAO/CGEAR/24 - KLINK!.bin
/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin
/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin
/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin
/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin
/dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin
/dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin
/dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin
/dlc/IRAO/CGEAR/32 - Red Genesect.bin
/dlc/IRAO/CGEAR2/01 - Default.bin
/dlc/IRAO/CGEAR2/02 - Meadow Munna.bin
/dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin
/dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin
/dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin
/dlc/IRAO/CGEAR2/06 - V for Victory!.bin
/dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin
/dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin
/dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin
/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin
/dlc/IRAO/CGEAR2/11 - Venusaur!.bin
/dlc/IRAO/CGEAR2/12 - Charizard!.bin
/dlc/IRAO/CGEAR2/13 - Blastoise!.bin
/dlc/IRAO/CGEAR2/14 - Red Genesect.bin
/dlc/IRAO/CGEAR2/15 - Black Kyurem.bin
/dlc/IRAO/CGEAR2/16 - White Kyurem.bin
/dlc/IRAO/MUSICAL/01 - Charming Munna.bin
/dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin
/dlc/IRAO/ZUKAN/01 - Default (Pink).bin
/dlc/IRAO/ZUKAN/02 - Default (Red).bin
/dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin
/dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin
/dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin
/dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin
/dlc/IRAO/ZUKAN/07 - Hugh.bin
/dlc/IRAO/ZUKAN/08 - Bianca.bin
/dlc/IRAO/ZUKAN/09 - Cheren.bin

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.