diff --git a/ARCHIPELAGO.md b/ARCHIPELAGO.md new file mode 100644 index 000000000000..942e3bc5570c --- /dev/null +++ b/ARCHIPELAGO.md @@ -0,0 +1,85 @@ +## Offline Randomizer + +### 🎲 Randomized Adventure Mode +Play the Adventure mode on the Shandalar world but with randomized items & biomes. Unlock each region of the map as you progress. +- All cards outside your starting deck will be locked by default. +- Biomes, cards and equipment unlocks are randomized and can be unlocked by completing various objectives in the game such as: + - Collecting cards + - Completing quests + - Winning battles + - Competing at Inn events + - Defeating bosses and mini-bosses +- Equipment shop contents are fully randomized and (almost) any piece of equipment in game can show up. +- The Leather Boots in your starting equipment are removed regardless of difficulty, all entities' base speeds are increased to compensate. + +## Archipelago Multiworld Randomizer +Archipelago is a cross-game modification system which randomizes different games, then uses the result to build a single +unified multi-player game. Items from one game may be present in another, and you will need your fellow players to find +items you need in their games to help you complete your own. + +### 🏝️ Archipelago Adventure Mode +Play the Randomized Adventure Mode using Archipelago to shuffle all items and locations amongst a pool of other Archipelago compatible games. +
Archipelago adds the following locations to the pool: +- Buying items from equipment & item shops +- Defeating a mini-boss +- Defeating a castle boss +- Winning a certain number of battles per region +- Completing a certain number of quests per region +- Finishing a certain number of inn events per region +- Collecting a certain number of cards per rarity + +This Archipelago implementation adds the following rewards and filler rewards to the pool: +- Region unlocks in the form of "region runes", one per color of region +- Gold in various amounts +- Mana Shards in various amounts +- Bronze/Silver/Gold Challenge Coins +- Sets of cards including a free matching booster pack +- Max life in various amounts +- Pieces of equipment for the player to wear + +This Archipelago implementation has the following win-conditions: +- Defeating X amount of Castle bosses, this defaults to 3 but when set to all 6, Emrakul must also be defeated in order to win. + +#### Generating the Archipelago Multiworld +1. Download the [latest release](https://github.com/BramTeurlings/forge-archipelago/releases) of this repository +2. Download the [latest release](https://github.com/BramTeurlings/forge-APWorld/releases) of the Forge APWorld repository +3. Download the [latest release](https://github.com/ArchipelagoMW/Archipelago/releases) of the Archipelago Launcher +4. Install Card-Forge using the `forge-installer-.jar` file bundled in this repository's release files +5. Install the Archipelago Launcher by opening the `Setup.Archipelago..exe` bundled in Archipelago Launcher release files +6. Open the Archipelago Launcher and click "Open" on the "Install APWorld" option.![img.png](docs/Adventure/Archipelago/archipelago_launcher_1.png) +7. When prompted, select the `forge.apworld` file downloaded from the Forge APWorld repository release files. +8. Once complete, restart the launcher. +9. Click "Open" on the "Options Creator" option. +10. Scroll down the list on the left side until you see "Forge" or "ForgeAP" and click on it.![img.png](docs/Adventure/Archipelago/archipelago_launcher_2.png) +11. Configure the options in the section marked in blue as you choose, if you're not sure what to change it is recommended you leave things on their default values. +12. Fill your name into the "Player Name" field. +13. Click on the "Export Options" to export your player profile as a `.yaml` file. +14. Gather the other player's `.yaml` files and place them into the same "Players" folder. In our example, the Archipelago Launcher put them in `C:\ProgramData\Archipelago\Players`. +15. Click on the "Generate" option.![img.png](docs/Adventure/Archipelago/archipelago_launcher_3.png) +16. If everything went well, the Archipelago Launcher will now have generated a `.zip` file for your Archipelago game. This file can be hosted via [Archipelago's Official Website](https://archipelago.gg/uploads) to start a lobby. Our `.zip` file was generated in `C:\ProgramData\Archipelago\output`. + +#### Connecting the game client +17. Assuming you installed Card-Forge in step 4, open `forge-adventure.exe` from the location you installed Card-Forge in. +18. Select "Adventure Mode" on the main menu.![img.png](docs/Adventure/Archipelago/forge_adventure_1.png) +19. Select "New Game" on the Adventure Mode screen.![img.png](docs/Adventure/Archipelago/forge_adventure_2.png) +20. On the New Game screen, choose any settings you want but make sure to scroll down and select "Archipelago" as the "Randomizer" option.![img.png](docs/Adventure/Archipelago/forge_adventure_3.png) +21. Click on the "Start" button. +22. You will be brought to the game's opening scene, click through the tutorial quest with the Adept Black Wizard and walk through the port. The game will alert you that you need to connect to the Archipelago Server. Click the "Take me there" button.![img.png](docs/Adventure/Archipelago/forge_adventure_4.png) +23. Fill in the server host address, the server port and the slot name for your character and click on "Connect".![img.png](docs/Adventure/Archipelago/forge_adventure_5.png) +24. **Note: You must reconnect to Archipelago each time you load your save file or restart the software. You can find the connection page by opening the Adventure Mode menu on the overworld via the hamburger-menu icon or by pressing the `esc` key. Click on the Archipelago icon to open the connection menu. This button is only available in Archipelago Randomizer mode.**![img.png](docs/Adventure/Archipelago/forge_adventure_6.png) +25. You're all set to go! Enjoy playing the Archipelago! + +**Extra: If you want the game to automatically download relevant card-artwork, open the Adventure Mode menu and click on "Settings", then scroll down until you see "Automatically Download Missing Card Art" and enable the option.** + +## Cheating +In rare cases due to the nature of the game, it's possible to get stuck, either literally (like, in a wall or something) or other times due to difficulty. +I should note that the generation of the multiworld **should** ensure that you can't be progression locked by Archipelago but nevertheless, sometimes you might have a lot more fun if you just had that little bit of extra gold or that one card you need. + +While cheating is obviously not in the spirit of the game, it can genuinely improve certain player's enjoyment so we leave the final call to you. +Here are some handy cheats that will help you progress and speed up your game. Note that each of these commands must be filled into the in-game console, you can open the console by pressing `F9`: +1. `give gold [amount]` > gives the specified amount of gold to the player. +2. `give shards [amount]` > gives the specified amount of shards to the player. +3. `give item cheat` > gives the player a completely busted piece of equipment that will auto-win almost any combat on the first turn, don't forget to equip it, you can un-equip it at any time. +4. `give boosters [set-code] [amount]` > gives the player the specified amount of packs of the specified set code. Set codes can be found online. +5. `heal full` > fully heals your character +6. `teleport to poi Spawn` > teleports you to the starting cave, handy if you ever get literally stuck. It's case sensitive so don't forget the capital "S". \ No newline at end of file diff --git a/README.md b/README.md index b41ae60fd728..b2b081735362 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Embark on a thrilling single-player journey where you can: - Explore an overworld map. - Challenge diverse AI opponents. - Collect cards and items to boost your abilities. +- Archipelago Multiworld compatibility Shandalar World diff --git a/docs/Adventure/Archipelago/archipelago_launcher_1.png b/docs/Adventure/Archipelago/archipelago_launcher_1.png new file mode 100644 index 000000000000..4c41e6ede158 Binary files /dev/null and b/docs/Adventure/Archipelago/archipelago_launcher_1.png differ diff --git a/docs/Adventure/Archipelago/archipelago_launcher_2.png b/docs/Adventure/Archipelago/archipelago_launcher_2.png new file mode 100644 index 000000000000..6ba1fd1d8fc7 Binary files /dev/null and b/docs/Adventure/Archipelago/archipelago_launcher_2.png differ diff --git a/docs/Adventure/Archipelago/archipelago_launcher_3.png b/docs/Adventure/Archipelago/archipelago_launcher_3.png new file mode 100644 index 000000000000..f2b87d2fd8c8 Binary files /dev/null and b/docs/Adventure/Archipelago/archipelago_launcher_3.png differ diff --git a/docs/Adventure/Archipelago/forge_adventure_1.png b/docs/Adventure/Archipelago/forge_adventure_1.png new file mode 100644 index 000000000000..07a77f07d59d Binary files /dev/null and b/docs/Adventure/Archipelago/forge_adventure_1.png differ diff --git a/docs/Adventure/Archipelago/forge_adventure_2.png b/docs/Adventure/Archipelago/forge_adventure_2.png new file mode 100644 index 000000000000..fa7fa93cb5b0 Binary files /dev/null and b/docs/Adventure/Archipelago/forge_adventure_2.png differ diff --git a/docs/Adventure/Archipelago/forge_adventure_3.png b/docs/Adventure/Archipelago/forge_adventure_3.png new file mode 100644 index 000000000000..dcfe89853ea2 Binary files /dev/null and b/docs/Adventure/Archipelago/forge_adventure_3.png differ diff --git a/docs/Adventure/Archipelago/forge_adventure_4.png b/docs/Adventure/Archipelago/forge_adventure_4.png new file mode 100644 index 000000000000..78511a54c861 Binary files /dev/null and b/docs/Adventure/Archipelago/forge_adventure_4.png differ diff --git a/docs/Adventure/Archipelago/forge_adventure_5.png b/docs/Adventure/Archipelago/forge_adventure_5.png new file mode 100644 index 000000000000..3cd92292b8d3 Binary files /dev/null and b/docs/Adventure/Archipelago/forge_adventure_5.png differ diff --git a/docs/Adventure/Archipelago/forge_adventure_6.png b/docs/Adventure/Archipelago/forge_adventure_6.png new file mode 100644 index 000000000000..80b343850b58 Binary files /dev/null and b/docs/Adventure/Archipelago/forge_adventure_6.png differ diff --git "a/docs/Un\342\200\220cards,-Playtest-Cards,-and-Other-Funny-Cards.md" b/docs/Un-cards,-Playtest-Cards,-and-Other-Funny-Cards.md similarity index 100% rename from "docs/Un\342\200\220cards,-Playtest-Cards,-and-Other-Funny-Cards.md" rename to docs/Un-cards,-Playtest-Cards,-and-Other-Funny-Cards.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 718637856bec..b075862d619a 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -62,6 +62,6 @@ - [Fantasy Blocks](fantasy-blocks.md) - [Missing Cards in Forge](Missing-Cards-in-Forge.md) - - [Un‐cards, Playtest Cards, and Other Funny Cards](Un‐cards,-Playtest-Cards,-and-Other-Funny-Cards.md) + - [Un‐cards, Playtest Cards, and Other Funny Cards](Un-cards,-Playtest-Cards,-and-Other-Funny-Cards.md) - [Credit & Thanks](Credit-and-Thanks.md) \ No newline at end of file diff --git a/forge-core/src/main/java/forge/item/PaperCard.java b/forge-core/src/main/java/forge/item/PaperCard.java index b8bdd79d1fdd..4650830fe4a0 100644 --- a/forge-core/src/main/java/forge/item/PaperCard.java +++ b/forge-core/src/main/java/forge/item/PaperCard.java @@ -64,6 +64,7 @@ public class PaperCard implements Comparable, InventoryItemFromSet, private transient String displayName; private transient String sortableName; private transient boolean hasFlavorName; + private transient boolean isLocked; @Override public String getName() { @@ -534,6 +535,14 @@ public List getAllFaces() { return this.rules.getAllFaces().stream().map(this::getVariantForFace).collect(Collectors.toList()); } + public boolean isLocked() { + return isLocked; + } + + public void setLocked(boolean locked) { + isLocked = locked; + } + private ICardFace getVariantForFace(ICardFace face) { if(!face.hasFunctionalVariants() || this.functionalVariant.equals(NO_FUNCTIONAL_VARIANT)) return face; diff --git a/forge-gui-android/pom.xml b/forge-gui-android/pom.xml index 4746481e2699..5ac4265b9313 100644 --- a/forge-gui-android/pom.xml +++ b/forge-gui-android/pom.xml @@ -99,6 +99,16 @@ forge-android-${snapshot-version} + + org.java-websocket + Java-WebSocket + 1.5.6 + + + org.apache.httpcomponents.core5 + httpcore5 + 5.2.4 + org.robolectric android-all diff --git a/forge-gui-mobile/pom.xml b/forge-gui-mobile/pom.xml index d2a485a5d411..f87987e30a87 100644 --- a/forge-gui-mobile/pom.xml +++ b/forge-gui-mobile/pom.xml @@ -110,6 +110,11 @@ + + io.github.archipelagomw + Java-Client + 0.2.0 + diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/Archipelago.java b/forge-gui-mobile/src/forge/adventure/archipelago/Archipelago.java new file mode 100644 index 000000000000..3e8dab39a387 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/Archipelago.java @@ -0,0 +1,80 @@ +package forge.adventure.archipelago; + +import com.github.tommyettinger.textra.TypingLabel; +import io.github.archipelagomw.ClientStatus; + +import java.net.URISyntaxException; +import java.util.*; + +public class Archipelago { + private final Random random = new Random(); + + public static Archipelago instance = null; + + static private ArchipelagoClient APClient; + + public Archipelago() { + instance = this; + } + + public static Archipelago getInstance() { + return instance == null ? instance = new Archipelago() : instance; + } + + public void connect(String ip, String port, String slotName, String password, TypingLabel connectStatusLabel) { + if (APClient != null && APClient.isConnected()) APClient.disconnect(); + + APClient = new ArchipelagoClient(connectStatusLabel); + APClient.setName(slotName); + APClient.setPassword(password); + + String address = ip; + if (port != null && !port.isEmpty() && !address.contains(":")) address += ":" + port; + + try { + APClient.connect(address); + } catch (URISyntaxException e) { + connectStatusLabel.setText(""); + connectStatusLabel.setText("{FADE=RED;RED;0.1}Invalid IP"); + } + } + + public void disconnect() { + if (APClient != null && APClient.isConnected()) APClient.disconnect(); + } + + public boolean isConnected() { + return APClient != null && APClient.isConnected(); + } + + public void checkLocation(Long id) { + if (APClient == null || !APClient.isConnected() || !APClient.checkLocation(id)) { + ArchipelagoRandomizer.getInstance().locationQueue.add(id); + } + } + + public void checkLocations(List ids) { + if (APClient == null || !APClient.isConnected() || !APClient.checkLocations(ids)) { + ArchipelagoRandomizer.getInstance().locationQueue.addAll(ids); + } + } + + public boolean sendQueuedLocations(List ids) { + return APClient != null && APClient.isConnected() && APClient.checkLocations(ids); + } + + public void goal() { + APClient.setGameState(ClientStatus.CLIENT_GOAL); + } + + public int getRandomItemPrice() { + int min = 500; + int max = 1000; + SlotData slotData = APClient.getSlotData(); + if (slotData != null) { + min = slotData.MinShopPrice; + max = slotData.MaxShopPrice; + } + return random.nextInt(min, max + 1); + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoCheckTypes.java b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoCheckTypes.java new file mode 100644 index 000000000000..2a702c24a7c6 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoCheckTypes.java @@ -0,0 +1,12 @@ +package forge.adventure.archipelago; + +public enum ArchipelagoCheckTypes { + COLORLESS_BATTLE_WON, WHITE_BATTLE_WON, BLUE_BATTLE_WON, BLACK_BATTLE_WON, RED_BATTLE_WON, GREEN_BATTLE_WON, + COLORLESS_TOWN_QUESTS, WHITE_TOWN_QUESTS, BLUE_TOWN_QUESTS, BLACK_TOWN_QUESTS, RED_TOWN_QUESTS, GREEN_TOWN_QUESTS, + COLORLESS_TOWN_EVENTS, WHITE_TOWN_EVENTS, BLUE_TOWN_EVENTS, BLACK_TOWN_EVENTS, RED_TOWN_EVENTS, GREEN_TOWN_EVENTS, + TOTAL_CARDS_EARNED, + SLIME_MOTHER_DEFEATED, SLOBAD_DEFEATED, XIRA_DEFEATED, NAHIRI_DEFEATED, VALYX_DEFEATED, JACE_DEFEATED, KIORA_DEFEATED, MYR_SUPERION_DEFEATED, SLIVER_QUEEN_DEFEATED, TEFERI_DEFEATED, GROLNOK_DEFEATED, GUARDIAN_ANGEL_DEFEATED, + LILIANA_DEFEATED, SLIMEFOOT_DEFEATED, SORIN_DEFEATED, CHANDRA_DEFEATED, TIBALTS_TORTURER_DEFEATED, TIBALT_DEFEATED, ZEDRUUS_COOK_DEFEATED, CONJURER_DEFEATED, ZEDRUU_DEFEATED, GARRUK_DEFEATED, HYDRA_OF_SHANDALAAR_DEFEATED, SCARECROW_CAPTAIN_DEFEATED, + BOSS_WHITE_DEFEATED, BOSS_BLUE_DEFEATED, BOSS_BLACK_DEFEATED, BOSS_RED_DEFEATED, BOSS_GREEN_DEFEATED, BOSS_COLORLESS_DEFEATED, + WIN_CONDITION_CLEARED +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoClient.java b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoClient.java new file mode 100644 index 000000000000..a5b54f94d83b --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoClient.java @@ -0,0 +1,44 @@ +package forge.adventure.archipelago; + +import com.github.tommyettinger.textra.TypingLabel; +import forge.adventure.archipelago.events.*; +import io.github.archipelagomw.Client; +import io.github.archipelagomw.flags.ItemsHandling; + +import javax.annotation.Nullable; + +public class ArchipelagoClient extends Client { + + @Nullable + public SlotData slotData; + + public ArchipelagoClient(TypingLabel connectStatusLabel) { + super(); + + this.setGame("ForgeAP"); + this.setItemsHandlingFlags(ItemsHandling.SEND_ITEMS | ItemsHandling.SEND_OWN_ITEMS | ItemsHandling.SEND_STARTING_INVENTORY); + this.getEventManager().registerListener(new ConnectResult(this, connectStatusLabel)); + this.getEventManager().registerListener(new ReceiveItem()); + this.getEventManager().registerListener(new LocationInfo(this)); + this.getEventManager().registerListener(new OnDeathLink(this)); + this.getEventManager().registerListener(new PrintJsonListener(this)); + } + + public void setSlotData(SlotData slotData) { + this.slotData = slotData; + } + + @Nullable + public SlotData getSlotData() { + return slotData; + } + + @Override + public void onError(Exception ex) { + ex.getLocalizedMessage(); + } + + @Override + public void onClose(String s, int i) { + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoColors.java b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoColors.java new file mode 100644 index 000000000000..a80536413989 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoColors.java @@ -0,0 +1,10 @@ +package forge.adventure.archipelago; + +public class ArchipelagoColors { + public static final String Cyan = "{COLOR=#00CCCC}"; + public static final String Plum = "{COLOR=#AF99EF}"; + public static final String SlateBlue = "{COLOR=#6D8BE8}"; + public static final String Salmon = "{COLOR=#FA8072}"; + public static final String Green = "{COLOR=#00b359}"; + public static final String Magenta = "{COLOR=#EE00EE}"; +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoData.java b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoData.java new file mode 100644 index 000000000000..75c26555af68 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoData.java @@ -0,0 +1,770 @@ +package forge.adventure.archipelago; + +import forge.StaticData; +import forge.adventure.data.ItemData; +import forge.adventure.data.WorldData; +import forge.adventure.scene.TileMapScene; +import forge.adventure.stage.GameHUD; +import forge.adventure.util.*; +import forge.card.CardEdition; +import forge.deck.CardPool; +import forge.deck.Deck; +import forge.item.PaperCard; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.*; + +// This class will keep track of data relevant for the Archipelago implementation +// Persists and loads data inside/from the user's save file +public class ArchipelagoData implements SaveFileContent { + private static final ArchipelagoData archipelagoDataInstance = new ArchipelagoData(); + protected ArchipelagoMode archipelagoMode = ArchipelagoMode.disabled; + + // Data we need from Forge + private final CardEdition.Collection allEditions = StaticData.instance().getEditions(); + private final Iterable allOrderedEditions = allEditions.getOrderedEditions(); + // Todo: This works fine for singleplayer even when updates come out but the fact that the list of all sets can grow will cause problems in Archipelago due to a variable amount of checks. + protected final Set allCardSets = new HashSet<>(); + // List of teleportation runes that we use to gate regions + protected final Set regionTeleportingRunes = new HashSet<>(Arrays.asList("white rune", "black rune", "blue rune", "red rune", "green rune")); + // List of known main bosses that contribute to APWorld completion + private final Set mainBosses = new HashSet<>(Arrays.asList("lorthos", "emrakul", "lathliss", "ghalta", "griselbrand", "akroma")); + + // Actual user data we want to store + protected final Map colorlessCompletedTownInnEvents = new HashMap<>(); + protected final Map whiteCompletedTownInnEvents = new HashMap<>(); + protected final Map blueCompletedTownInnEvents = new HashMap<>(); + protected final Map blackCompletedTownInnEvents = new HashMap<>(); + protected final Map redCompletedTownInnEvents = new HashMap<>(); + protected final Map greenCompletedTownInnEvents = new HashMap<>(); + + protected final Map colorlessCompletedTownQuests = new HashMap<>(); + protected final Map whiteCompletedTownQuests = new HashMap<>(); + protected final Map blueCompletedTownQuests = new HashMap<>(); + protected final Map blackCompletedTownQuests = new HashMap<>(); + protected final Map redCompletedTownQuests = new HashMap<>(); + protected final Map greenCompletedTownQuests = new HashMap<>(); + + protected final Map cardsEarnedByRarity = new HashMap<>(); + protected final Map itemsGainedByName = new HashMap<>(); + protected final Map packsEarnedBySet = new HashMap<>(); + protected final Set cardsUnlockedByName = new HashSet<>(); + protected final Set setsUnlockedByCode = new HashSet<>(); + protected final Set bossesDefeatedByName = new HashSet<>(); + protected final Set miniBossesDefeatedByName = new HashSet<>(); + protected final Set lockedWorldRegionsByName = new HashSet<>(); + // Todo: Replace the String with another serializable object that is compatible with Archipelago i.e. `NetworkItem.java` + protected int lastArchipelagoRewardIndex = 0; + protected int totalGoldEarned = 0; + protected int totalExtraMaxLifeEarned = 0; + protected int totalShardsEarned = 0; + protected int totalBattlesWonWhite = 0; + protected int totalBattlesWonBlue = 0; + protected int totalBattlesWonBlack = 0; + protected int totalBattlesWonRed = 0; + protected int totalBattlesWonGreen = 0; + protected int totalBattlesWonColorless = 0; + private String lastTraversedRegion = "waste"; + + // List of unlockable checks + // Todo: Fill list based on archipelago xml contents + protected float setUnlockChecksRestAmount = 0; + protected int totalAmountOfSetUnlockChecks = ArchipelagoUtil.TOTAL_AMOUNT_OF_SET_UNLOCK_CHECKS; // This is set based on the value we receive in the APWorld + protected int receivedAmountOfSetUnlockChecks = 0; + + private ArchipelagoData() { + } + + public static ArchipelagoData getInstance() { + return archipelagoDataInstance; + } + + // This must be updated to reset any sets/maps/variables otherwise things persist between loads of different save files! + public void setupFreshSaveFile(ArchipelagoMode archipelagoMode) { + GameHUD.getInstance().setApButtonVisibility(/*archipelagoMode == ArchipelagoMode.networked_archipelago*/false); // Todo: Revert to the commented block once the screen is implemented + cardsUnlockedByName.clear(); + this.addCardUnlockedByName("Plains"); + this.addCardUnlockedByName("Forest"); + this.addCardUnlockedByName("Swamp"); + this.addCardUnlockedByName("Mountain"); + this.addCardUnlockedByName("Island"); + this.addCardUnlockedByName("Wastes"); + + colorlessCompletedTownInnEvents.clear(); + whiteCompletedTownInnEvents.clear(); + blueCompletedTownInnEvents.clear(); + blackCompletedTownInnEvents.clear(); + redCompletedTownInnEvents.clear(); + blackCompletedTownInnEvents.clear(); + + colorlessCompletedTownQuests.clear(); + whiteCompletedTownQuests.clear(); + blueCompletedTownQuests.clear(); + blackCompletedTownQuests.clear(); + redCompletedTownQuests.clear(); + greenCompletedTownQuests.clear(); + cardsEarnedByRarity.clear(); + itemsGainedByName.clear(); + packsEarnedBySet.clear(); + setsUnlockedByCode.clear(); + bossesDefeatedByName.clear(); + miniBossesDefeatedByName.clear(); + lockedWorldRegionsByName.clear(); + lockedWorldRegionsByName.addAll(new HashSet<>(Arrays.asList("white", "blue", "black", "red", "green"))); + + lastArchipelagoRewardIndex = 0; + totalGoldEarned = 0; + totalExtraMaxLifeEarned = 0; + totalShardsEarned = 0; + totalBattlesWonWhite = 0; + totalBattlesWonBlue = 0; + totalBattlesWonBlack = 0; + totalBattlesWonRed = 0; + totalBattlesWonGreen = 0; + totalBattlesWonColorless = 0; + lastTraversedRegion = "waste"; + + setUnlockChecksRestAmount = 0f; + totalAmountOfSetUnlockChecks = ArchipelagoUtil.TOTAL_AMOUNT_OF_SET_UNLOCK_CHECKS; + receivedAmountOfSetUnlockChecks = 0; + + this.archipelagoMode = archipelagoMode; + + loadAllAvailableSets(); + } + + private void updatePlayerChecks(ArchipelagoCheckTypes type, @NonNull String notificationMessage) { + if (archipelagoMode == ArchipelagoMode.disabled) return; + boolean networkedAP = archipelagoMode == ArchipelagoMode.networked_archipelago; + if (networkedAP) { + ArchipelagoRandomizer apRandomizer = ArchipelagoRandomizer.getInstance(); + apRandomizer.updatePlayerChecks(type); + } else { + LocalRandomizer localRandomizer = LocalRandomizer.getInstance(); + localRandomizer.updatePlayerChecks(type, notificationMessage); + } + } + + public boolean isSetUnlocked(String setCode) { + if (archipelagoMode == ArchipelagoMode.disabled) return true; + if (setCode == null || !setsUnlockedByCode.contains(setCode)) { + return false; + } else { + return true; + } + } + + public boolean isRegionUnlocked(String regionName) { + if (archipelagoMode == ArchipelagoMode.disabled) return true; + if (!Arrays.asList(ArchipelagoUtil.REGION_NAMES).contains(regionName.toLowerCase())) return true; + lastTraversedRegion = regionName; + return !archipelagoDataInstance.lockedWorldRegionsByName.contains(lastTraversedRegion); + } + + public ArchipelagoMode getArchipelagoMode() { + return archipelagoMode; + } + + public void setTotalAmountOfSetUnlockChecks(int newTotalAmountOfSetUnlockChecks) { + totalAmountOfSetUnlockChecks = newTotalAmountOfSetUnlockChecks; + } + + public void setLastTraversedRegion(String lastTraversedRegion) { + this.lastTraversedRegion = lastTraversedRegion; + } + + public boolean checkCardUnlocked(PaperCard card) { + if (archipelagoMode == ArchipelagoMode.disabled) return true; + if (card == null || card.getName() == null) { + // If we don't have a valid card or cardname, just ignore it meaning returning true in this case. + return true; + } + String cardName = card.getName(); + + // Card explicitly unlocked + if (cardsUnlockedByName.contains(cardName)) { + return true; + } + + // Card sets unlocked + String setCode = card.getEdition(); + + return isSetUnlocked(setCode); + + // Neither card nor set is unlocked + } + + public boolean checkDeckUnlocked(Deck selectedDeck) { + if (archipelagoMode == ArchipelagoMode.disabled) return true; + if (selectedDeck == null) { + return true; + } + + CardPool pool = selectedDeck.getAllCardsInASinglePool(true, true); + for (PaperCard card : pool.toFlatList()) { + if (!checkCardUnlocked(card)) return false; + } + + return true; + } + + /// --- The checks below can be called from the networked part of the AP implementation. Note that `setLastArchipelagoRewardIndex` must be called manually. --- + private void unlockSetByName(String setToUnlock, String notificationMessage) { + addSetUnlockedByCode(setToUnlock); + String setUnlockedText = String.format("Set Unlock: %s%s{RESET}", ArchipelagoColors.Cyan, setToUnlock); + // Some sets don't have booster packs such as full-art land sets (P23). + if (archipelagoMode == ArchipelagoMode.solo_randomizer || (archipelagoMode == ArchipelagoMode.networked_archipelago && ArchipelagoRandomizer.getInstance().getSlotData() != null && ArchipelagoRandomizer.getInstance().getSlotData().GiftPack == 1)) { + var booster = StaticData.instance().getBoosters().get(setToUnlock); + if (booster != null) { + Current.player().addBooster(AdventureEventController.instance().generateBooster(setToUnlock)); + setUnlockedText += String.format(" + %s%s{RESET}", ArchipelagoColors.Green, "Matching Booster Pack"); + } + } + // Archipelago does not know what set will be unlocked, this is randomized locally. Therefore, we always wanna show the player a notification. + if (archipelagoMode == ArchipelagoMode.networked_archipelago) { + generateGameNotification(String.format("%s%s{RESET}%s", ArchipelagoColors.Salmon, "Forge AP:\n", setUnlockedText)); + } else if (archipelagoMode == ArchipelagoMode.solo_randomizer) { + generateGameNotification(String.format("%s\n%s", notificationMessage, setUnlockedText)); + } + System.out.println(setUnlockedText); + } + + // This produces a different result each time it's rolled, the RNG isn't seeded. + public void unlockRandomSet(String notificationMessage) { + // Subtract unlocked sets from full list. + Set lockedSets = new HashSet<>(allCardSets); + lockedSets.removeAll(setsUnlockedByCode); + + // Nothing left to unlock + if (lockedSets.isEmpty()) { + return; + } + + // If we already received all checks, unlock the entire list of locked sets + if (receivedAmountOfSetUnlockChecks >= totalAmountOfSetUnlockChecks) { + for (String setToUnlock : lockedSets) { + unlockSetByName(setToUnlock, notificationMessage); + } + } + + float amountOfSetsToUnlock = (float) allCardSets.size() / totalAmountOfSetUnlockChecks + setUnlockChecksRestAmount; + int amountOfSetsToUnlockFloored = (int) Math.floor(amountOfSetsToUnlock); + setUnlockChecksRestAmount = (amountOfSetsToUnlock - amountOfSetsToUnlockFloored); + List lockedList = new ArrayList<>(lockedSets); + Random random = new Random(); + String setToUnlock; + + for (int i = 0; i < amountOfSetsToUnlockFloored; i++) { + setToUnlock = lockedList.get(random.nextInt(lockedList.size())); + unlockSetByName(setToUnlock, notificationMessage); + lockedSets.remove(setToUnlock); + } + receivedAmountOfSetUnlockChecks++; + } + + public void generateGameNotification(String message) { + GameHUD.getInstance().addNotification(message, 0.5f, 3f, 0.5f); + } + /// --- End --- + + /// --- The functions below are responsible for mutating the user data we store in the save file for both modes --- + // Todo: Make the functions below private where possible, rewrite other code to account for this. + // Todo: Make sure the functions below trigger all related checks in `updatePlayerChecks` when applicable. + // Todo: Defeating a (mini-)boss should probably count as a check. + // Note that the name of a boss is not unique so we'll need to filter from all enemies which have a `boss` value of `true`. + // Returns `true` if the boss was not already defeated before. + public boolean addMiniBossDefeated(String miniBossName) { + boolean result = miniBossesDefeatedByName.add(miniBossName.toLowerCase()); + String notificationMessage = String.format("%sRandomizer:{RESET}\nMiniboss Defeated: " + miniBossName, ArchipelagoColors.Plum); + System.out.println(notificationMessage); + switch (miniBossName.toLowerCase()) { + case "the mother slime" -> updatePlayerChecks(ArchipelagoCheckTypes.SLIME_MOTHER_DEFEATED, notificationMessage); + case "slobad" -> updatePlayerChecks(ArchipelagoCheckTypes.SLOBAD_DEFEATED, notificationMessage); + case "xira" -> updatePlayerChecks(ArchipelagoCheckTypes.XIRA_DEFEATED, notificationMessage); + case "nahiri" -> updatePlayerChecks(ArchipelagoCheckTypes.NAHIRI_DEFEATED, notificationMessage); + case "valyx feaster of torment" -> updatePlayerChecks(ArchipelagoCheckTypes.VALYX_DEFEATED, notificationMessage); + case "jace" -> updatePlayerChecks(ArchipelagoCheckTypes.JACE_DEFEATED, notificationMessage); + case "kiora" -> updatePlayerChecks(ArchipelagoCheckTypes.KIORA_DEFEATED, notificationMessage); + case "myr superion" -> updatePlayerChecks(ArchipelagoCheckTypes.MYR_SUPERION_DEFEATED, notificationMessage); + case "sliver queen" -> updatePlayerChecks(ArchipelagoCheckTypes.SLIVER_QUEEN_DEFEATED, notificationMessage); + case "teferi" -> updatePlayerChecks(ArchipelagoCheckTypes.TEFERI_DEFEATED, notificationMessage); + case "grolnok" -> updatePlayerChecks(ArchipelagoCheckTypes.GROLNOK_DEFEATED, notificationMessage); + case "guardian angel" -> updatePlayerChecks(ArchipelagoCheckTypes.GUARDIAN_ANGEL_DEFEATED, notificationMessage); + case "liliana" -> updatePlayerChecks(ArchipelagoCheckTypes.LILIANA_DEFEATED, notificationMessage); + case "slimefoot" -> updatePlayerChecks(ArchipelagoCheckTypes.SLIMEFOOT_DEFEATED, notificationMessage); + case "sorin" -> updatePlayerChecks(ArchipelagoCheckTypes.SORIN_DEFEATED, notificationMessage); + case "chandra" -> updatePlayerChecks(ArchipelagoCheckTypes.CHANDRA_DEFEATED, notificationMessage); + case "tibalt's torturer" -> updatePlayerChecks(ArchipelagoCheckTypes.TIBALTS_TORTURER_DEFEATED, notificationMessage); + case "tibalt" -> updatePlayerChecks(ArchipelagoCheckTypes.TIBALT_DEFEATED, notificationMessage); + case "zedruu's cook" -> updatePlayerChecks(ArchipelagoCheckTypes.ZEDRUUS_COOK_DEFEATED, notificationMessage); + case "conjurer" -> updatePlayerChecks(ArchipelagoCheckTypes.CONJURER_DEFEATED, notificationMessage); + case "zedruu" -> updatePlayerChecks(ArchipelagoCheckTypes.ZEDRUU_DEFEATED, notificationMessage); + case "garruk" -> updatePlayerChecks(ArchipelagoCheckTypes.GARRUK_DEFEATED, notificationMessage); + case "the hydra of shandalaar" -> updatePlayerChecks(ArchipelagoCheckTypes.HYDRA_OF_SHANDALAAR_DEFEATED, notificationMessage); + case "scarecrow captain" -> updatePlayerChecks(ArchipelagoCheckTypes.SCARECROW_CAPTAIN_DEFEATED, notificationMessage); + } + return result; + } + + public boolean addBossDefeated(String bossName) { + boolean result = bossesDefeatedByName.add(bossName.toLowerCase()); + String notificationMessage = String.format("%sRandomizer:{RESET}\nCastle Boss Defeated: " + bossName, ArchipelagoColors.Plum); + System.out.println(notificationMessage); + switch (bossName.toLowerCase()) { + case "akroma" -> { + updatePlayerChecks(ArchipelagoCheckTypes.BOSS_WHITE_DEFEATED, notificationMessage); + } + case "lorthos" -> { + updatePlayerChecks(ArchipelagoCheckTypes.BOSS_BLUE_DEFEATED, notificationMessage); + } + case "griselbrand" -> { + updatePlayerChecks(ArchipelagoCheckTypes.BOSS_BLACK_DEFEATED, notificationMessage); + } + case "lathliss" -> { + updatePlayerChecks(ArchipelagoCheckTypes.BOSS_RED_DEFEATED, notificationMessage); + } + case "ghalta" -> { + updatePlayerChecks(ArchipelagoCheckTypes.BOSS_GREEN_DEFEATED, notificationMessage); + } + case "emrakul" -> { + updatePlayerChecks(ArchipelagoCheckTypes.BOSS_COLORLESS_DEFEATED, notificationMessage); + } + } + // Win condition is reached if all bosses have been defeated. + ArchipelagoRandomizer apRandomizer = ArchipelagoRandomizer.getInstance(); + if (bossesDefeatedByName.size() >= apRandomizer.getSlotData().CastlesRequired) { + updatePlayerChecks(ArchipelagoCheckTypes.WIN_CONDITION_CLEARED, notificationMessage); + } + + return result; + } + + public boolean addCardUnlockedByName(String cardName) { + return cardsUnlockedByName.add(cardName); + } + + public boolean addSetUnlockedByCode(String setCode) { + return setsUnlockedByCode.add(setCode); + } + + public void addCompletedTownInnEvents() { + String townName = TileMapScene.instance().rootPoint.getDisplayName(); + switch (lastTraversedRegion) { + case "waste" -> { + colorlessCompletedTownInnEvents.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nInn Event Completed: " + townName + " - " + colorlessCompletedTownInnEvents.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.COLORLESS_TOWN_EVENTS, notificationMessage); + } + case "white" -> { + whiteCompletedTownInnEvents.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nInn Event Completed: " + townName + " - " + whiteCompletedTownInnEvents.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.WHITE_TOWN_EVENTS, notificationMessage); + } + case "blue" -> { + blueCompletedTownInnEvents.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nInn Event Completed: " + townName + " - " + blueCompletedTownInnEvents.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.BLUE_TOWN_EVENTS, notificationMessage); + } + case "black" -> { + blackCompletedTownInnEvents.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nInn Event Completed: " + townName + " - " + blackCompletedTownInnEvents.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.BLACK_TOWN_EVENTS, notificationMessage); + } + case "red" -> { + redCompletedTownInnEvents.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nInn Event Completed: " + townName + " - " + redCompletedTownInnEvents.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.RED_TOWN_EVENTS, notificationMessage); + } + case "green" -> { + greenCompletedTownInnEvents.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nInn Event Completed: " + townName + " - " + greenCompletedTownInnEvents.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.GREEN_TOWN_EVENTS, notificationMessage); + } + } + } + + public void addCompletedQuests(AdventureQuestEvent event) { + String townName = event.poi.getDisplayName(); + + switch (lastTraversedRegion) { + case "waste" -> { + colorlessCompletedTownQuests.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nQuest Completed: " + townName + " - " + colorlessCompletedTownQuests.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.COLORLESS_TOWN_QUESTS, notificationMessage); + } + case "white" -> { + whiteCompletedTownQuests.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nQuest Completed: " + townName + " - " + whiteCompletedTownQuests.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.WHITE_TOWN_QUESTS, notificationMessage); + } + case "blue" -> { + blueCompletedTownQuests.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nQuest Completed: " + townName + " - " + blueCompletedTownQuests.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.BLUE_TOWN_QUESTS, notificationMessage); + } + case "black" -> { + blackCompletedTownQuests.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nQuest Completed: " + townName + " - " + blackCompletedTownQuests.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.BLACK_TOWN_QUESTS, notificationMessage); + } + case "red" -> { + redCompletedTownQuests.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nQuest Completed: " + townName + " - " + redCompletedTownQuests.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.RED_TOWN_QUESTS, notificationMessage); + } + case "green" -> { + greenCompletedTownQuests.merge(townName, 1L, Long::sum); + String notificationMessage = String.format("%sRandomizer:{RESET}\nQuest Completed: " + townName + " - " + greenCompletedTownQuests.get(townName), ArchipelagoColors.Plum); + System.out.println(notificationMessage); + updatePlayerChecks(ArchipelagoCheckTypes.GREEN_TOWN_QUESTS, notificationMessage); + } + } + } + + public void addCardByRarity(String rarity) { + cardsEarnedByRarity.merge(rarity, 1L, Long::sum); + updatePlayerChecks(ArchipelagoCheckTypes.TOTAL_CARDS_EARNED, String.format("%sRandomizer:{RESET}\nCards of Rarity: " + rarity + " - " + cardsEarnedByRarity.get(rarity), ArchipelagoColors.Plum)); + } + + public void addGold(int amount) { + totalGoldEarned += amount; + } + + private void addUnlockedRegionByName(String regionName) { + lockedWorldRegionsByName.remove(regionName); + System.out.println("Randomizer:\n Region Unlocked: " + regionName); + } + + // Due to MapDialog.SetEffects() using just a name string to add items to the player's inventory, it's likely that the name is unique. + public void addItem(String itemName) { + if (regionTeleportingRunes.contains(itemName.toLowerCase())) { + // Unlock the region based on the color found in the itemName + if (itemName.toLowerCase().contains("white")) { + addUnlockedRegionByName("white"); + } else if (itemName.toLowerCase().contains("blue")) { + addUnlockedRegionByName("blue"); + } else if (itemName.toLowerCase().contains("black")) { + addUnlockedRegionByName("black"); + } else if (itemName.toLowerCase().contains("red")) { + addUnlockedRegionByName("red"); + } else if (itemName.toLowerCase().contains("green")) { + addUnlockedRegionByName("green"); + } + } + itemsGainedByName.merge(itemName, 1L, Long::sum); + } + + // This function only tracks how many packs the player has earned of each type. + // There are no checks that depend on this at the moment. + public void addPack(String boosterPackName) { + packsEarnedBySet.merge(boosterPackName, 1L, Long::sum); + } + + public void addMaxLife(int amount) { + totalExtraMaxLifeEarned += amount; + } + + public void addShards(int amount) { + totalShardsEarned += amount; + } + + public void addTotalBattlesWon(int amount) { + switch (lastTraversedRegion) { + case "waste" -> { + totalBattlesWonColorless += amount; + String notificationMessage = String.format("%sRandomizer:{RESET}\nWastes Region Battles Won: " + totalBattlesWonColorless, ArchipelagoColors.Plum); + updatePlayerChecks(ArchipelagoCheckTypes.COLORLESS_BATTLE_WON, notificationMessage); + } + case "white" -> { + totalBattlesWonWhite += amount; + String notificationMessage = String.format("%sRandomizer:{RESET}\nWhite Region Battles Won: " + totalBattlesWonWhite, ArchipelagoColors.Plum); + updatePlayerChecks(ArchipelagoCheckTypes.WHITE_BATTLE_WON, notificationMessage); + } + case "blue" -> { + totalBattlesWonBlue += amount; + String notificationMessage = String.format("%sRandomizer:{RESET}\nBlue Region Battles Won: " + totalBattlesWonBlue, ArchipelagoColors.Plum); + updatePlayerChecks(ArchipelagoCheckTypes.BLUE_BATTLE_WON, notificationMessage); + } + case "black" -> { + totalBattlesWonBlack += amount; + String notificationMessage = String.format("%sRandomizer:{RESET}\nBlack Region Battles Won: " + totalBattlesWonBlack, ArchipelagoColors.Plum); + updatePlayerChecks(ArchipelagoCheckTypes.BLACK_BATTLE_WON, notificationMessage); + } + case "red" -> { + totalBattlesWonRed += amount; + String notificationMessage = String.format("%sRandomizer:{RESET}\nRed Region Battles Won: " + totalBattlesWonRed, ArchipelagoColors.Plum); + updatePlayerChecks(ArchipelagoCheckTypes.RED_BATTLE_WON, notificationMessage); + } + case "green" -> { + totalBattlesWonGreen += amount; + String notificationMessage = String.format("%sRandomizer:{RESET}\nGreen Region Battles Won: " + totalBattlesWonGreen, ArchipelagoColors.Plum); + updatePlayerChecks(ArchipelagoCheckTypes.GREEN_BATTLE_WON, notificationMessage); + } + } + } + /// --- End --- + + /// --- Helper functions for saving and loading --- + private static void saveStringSet(SaveFileData parent, String key, Set set) { + parent.storeObject(key, set.toArray(new String[0])); + } + + private static void loadStringSet(SaveFileData parent, String key, Set set) { + set.clear(); + + if (!parent.containsKey(key)) return; + + String[] values = (String[]) parent.readObject(key); + Collections.addAll(set, values); + } + + private static void saveStringLongMap(SaveFileData parent, String prefix, Map map) { + String[] keys = map.keySet().toArray(new String[0]); + parent.storeObject(prefix + "_keys", keys); + + for (int i = 0; i < keys.length; i++) { + SaveFileData valueData = new SaveFileData(); + valueData.store("value", map.get(keys[i])); + parent.store(prefix + "_value_" + i, valueData); + System.out.println( + "Saving " + prefix + ": " + map.size() + " entries" + ); + } + } + + private static void loadStringLongMap(SaveFileData parent, String prefix, Map map) { + map.clear(); + + if (!parent.containsKey(prefix + "_keys")) return; + + String[] keys = (String[]) parent.readObject(prefix + "_keys"); + + for (int i = 0; i < keys.length; i++) { + SaveFileData valueData = parent.readSubData(prefix + "_value_" + i); + if (valueData != null) { + map.put(keys[i], valueData.readLong("value")); + } + } + } + + private static void saveItemDataSet(SaveFileData parent, String key, Set set) { + parent.storeObject(key, set.toArray(new ItemData[0])); + } + + private static void loadItemDataSet(SaveFileData parent, String key, Set set) { + set.clear(); + + if (!parent.containsKey(key)) return; + + ItemData[] values = (ItemData[]) parent.readObject(key); + Collections.addAll(set, values); + } + + private static void saveLongList(SaveFileData parent, String key, List list) { + parent.storeObject(key, list.toArray(new Long[0])); + } + + private static void loadLongList(SaveFileData parent, String key, List list) { + list.clear(); + + if (!parent.containsKey(key)) return; + + Long[] values = (Long[]) parent.readObject(key); + Collections.addAll(list, values); + } + + private void loadAllAvailableSets() { + Set newSetCodes = new HashSet<>(); + for (CardEdition edition : allOrderedEditions) { + newSetCodes.add(edition.getCode()); + } + if (!newSetCodes.equals(allCardSets)) { + allCardSets.clear(); + allCardSets.addAll(newSetCodes); + } + } + + @Override + public void load(SaveFileData data) { + if (data == null) { + // No archipelago data found, treat archipelago as inactive for this save file. + setupFreshSaveFile(ArchipelagoMode.disabled); + return; + } + loadAllAvailableSets(); + LocalRandomizer localRandomizer = LocalRandomizer.getInstance(); + ArchipelagoRandomizer networkedRandomizer = ArchipelagoRandomizer.getInstance(); + // Load save data + archipelagoMode = ArchipelagoMode.values()[data.containsKey("archipelagoMode") ? data.readInt("archipelagoMode") : 0]; + loadStringLongMap(data, "colorlessTownEvents", colorlessCompletedTownInnEvents); + loadStringLongMap(data, "whiteTownEvents", whiteCompletedTownInnEvents); + loadStringLongMap(data, "blueTownEvents", blueCompletedTownInnEvents); + loadStringLongMap(data, "blackTownEvents", blackCompletedTownInnEvents); + loadStringLongMap(data, "redTownEvents", redCompletedTownInnEvents); + loadStringLongMap(data, "greenTownEvents", greenCompletedTownInnEvents); + loadStringLongMap(data, "colorlessTownQuests", colorlessCompletedTownQuests); + loadStringLongMap(data, "whiteTownQuests", whiteCompletedTownQuests); + loadStringLongMap(data, "blueTownQuests", blueCompletedTownQuests); + loadStringLongMap(data, "blackTownQuests", blackCompletedTownQuests); + loadStringLongMap(data, "redTownQuests", redCompletedTownQuests); + loadStringLongMap(data, "greenTownQuests", greenCompletedTownQuests); + loadStringLongMap(data, "cardsByRarity", cardsEarnedByRarity); + loadStringLongMap(data, "items", itemsGainedByName); + loadStringLongMap(data, "packs", packsEarnedBySet); + loadStringSet(data, "bossesDefeated", bossesDefeatedByName); + loadStringSet(data, "miniBossesDefeated", miniBossesDefeatedByName); + loadStringSet(data, "cardsUnlocked", cardsUnlockedByName); + loadStringSet(data, "setsUnlocked", setsUnlockedByCode); + loadStringSet(data, "lockedRegions", lockedWorldRegionsByName); + // Todo: Swap this for the ArchipelagoRandomizer data when that's enabled + if (archipelagoMode == ArchipelagoMode.solo_randomizer) { + WorldData.resetShopLists(); + loadStringSet(data, "colorlessEquipmentShop", localRandomizer.colorlessEquipmentShopList); + loadStringSet(data, "whiteEquipmentShop", localRandomizer.whiteEquipmentShopList); + loadStringSet(data, "blueEquipmentShop", localRandomizer.blueEquipmentShopList); + loadStringSet(data, "blackEquipmentShop", localRandomizer.blackEquipmentShopList); + loadStringSet(data, "redEquipmentShop", localRandomizer.redEquipmentShopList); + loadStringSet(data, "greenEquipmentShop", localRandomizer.greenEquipmentShopList); + loadStringSet(data, "whiteItemShop", localRandomizer.whiteItemShopList); + loadStringSet(data, "blueItemShop", localRandomizer.blueItemShopList); + loadStringSet(data, "blackItemShop", localRandomizer.blackItemShopList); + loadStringSet(data, "redItemShop", localRandomizer.redItemShopList); + loadStringSet(data, "greenItemShop", localRandomizer.greenItemShopList); + loadStringSet(data, "remainingEquipment", localRandomizer.remainingEquipmentPool); + if (data.containsKey("checksSinceLastRegionReward")) localRandomizer.checksSinceLastRegionReward = data.readInt("checksSinceLastRegionReward"); + } + if (archipelagoMode == ArchipelagoMode.networked_archipelago) { + WorldData.resetShopLists(); + loadItemDataSet(data, "colorlessEquipmentShop", networkedRandomizer.colorlessEquipmentShopList); + loadItemDataSet(data, "whiteEquipmentShop", networkedRandomizer.whiteEquipmentShopList); + loadItemDataSet(data, "blueEquipmentShop", networkedRandomizer.blueEquipmentShopList); + loadItemDataSet(data, "blackEquipmentShop", networkedRandomizer.blackEquipmentShopList); + loadItemDataSet(data, "redEquipmentShop", networkedRandomizer.redEquipmentShopList); + loadItemDataSet(data, "greenEquipmentShop", networkedRandomizer.greenEquipmentShopList); + loadItemDataSet(data, "whiteItemShop", networkedRandomizer.whiteItemShopList); + loadItemDataSet(data, "blueItemShop", networkedRandomizer.blueItemShopList); + loadItemDataSet(data, "blackItemShop", networkedRandomizer.blackItemShopList); + loadItemDataSet(data, "redItemShop", networkedRandomizer.redItemShopList); + loadItemDataSet(data, "greenItemShop", networkedRandomizer.greenItemShopList); + loadLongList(data,"locationQueue", networkedRandomizer.locationQueue); + if (data.containsKey("slotData")) networkedRandomizer.setSlotData((SlotData) data.readObject("slotData")); + if (data.containsKey("lastIp")) networkedRandomizer.setLastIp(data.readString("lastIp")); + if (data.containsKey("lastPort")) networkedRandomizer.setLastPort(data.readString("lastPort")); + if (data.containsKey("lastSlotName")) networkedRandomizer.setLastSlotName(data.readString("lastSlotName")); + if (data.containsKey("lastPassword")) networkedRandomizer.setLastPassword(data.readString("lastPassword")); + networkedRandomizer.setupAPSettingScene(); + } + + setUnlockChecksRestAmount = data.containsKey("setUnlocksReceivedRest") ? data.readFloat("setUnlocksReceivedRest") : 0; + receivedAmountOfSetUnlockChecks = data.containsKey("setUnlocksReceived") ? data.readInt("setUnlocksReceived") : 0; + totalBattlesWonColorless = data.containsKey("totalBattlesWonColorless") ? data.readInt("totalBattlesWonColorless") : 0; + totalBattlesWonWhite = data.containsKey("totalBattlesWonWhite") ? data.readInt("totalBattlesWonWhite") : 0; + totalBattlesWonBlue = data.containsKey("totalBattlesWonBlue") ? data.readInt("totalBattlesWonBlue") : 0; + totalBattlesWonBlack = data.containsKey("totalBattlesWonBlack") ? data.readInt("totalBattlesWonBlack") : 0; + totalBattlesWonRed = data.containsKey("totalBattlesWonRed") ? data.readInt("totalBattlesWonRed") : 0; + totalBattlesWonGreen = data.containsKey("totalBattlesWonGreen") ? data.readInt("totalBattlesWonGreen") : 0; + lastTraversedRegion = data.containsKey("lastTraversedRegion") ? data.readString("lastTraversedRegion") : "waste"; + totalGoldEarned = data.containsKey("totalGold") ? data.readInt("totalGold") : 0; + totalExtraMaxLifeEarned = data.containsKey("extraLife") ? data.readInt("extraLife") : 0; + totalShardsEarned = data.containsKey("shards") ? data.readInt("shards") : 0; + lastArchipelagoRewardIndex = data.containsKey("lastArchipelagoRewardIndex") ? data.readInt("lastArchipelagoRewardIndex") : 0; + setTotalAmountOfSetUnlockChecks(data.containsKey("totalSetUnlockChecks") ? data.readInt("totalSetUnlockChecks") : 100); + GameHUD.getInstance().setApButtonVisibility(/*archipelagoMode == ArchipelagoMode.networked_archipelago*/false); // Todo: Revert to the commented block once the screen is implemented + if (archipelagoMode == ArchipelagoMode.networked_archipelago && Archipelago.getInstance().isConnected()) { + Archipelago.getInstance().disconnect(); + } + } + + @Override + public SaveFileData save() { + SaveFileData data = new SaveFileData(); + LocalRandomizer localRandomizer = LocalRandomizer.getInstance(); + ArchipelagoRandomizer networkedRandomizer = ArchipelagoRandomizer.getInstance(); + + saveStringLongMap(data, "colorlessTownEvents", colorlessCompletedTownInnEvents); + saveStringLongMap(data, "whiteTownEvents", whiteCompletedTownInnEvents); + saveStringLongMap(data, "blueTownEvents", blueCompletedTownInnEvents); + saveStringLongMap(data, "blackTownEvents", blackCompletedTownInnEvents); + saveStringLongMap(data, "redTownEvents", redCompletedTownInnEvents); + saveStringLongMap(data, "greenTownEvents", greenCompletedTownInnEvents); + saveStringLongMap(data, "colorlessTownQuests", colorlessCompletedTownQuests); + saveStringLongMap(data, "whiteTownQuests", whiteCompletedTownQuests); + saveStringLongMap(data, "blueTownQuests", blueCompletedTownQuests); + saveStringLongMap(data, "blackTownQuests", blackCompletedTownQuests); + saveStringLongMap(data, "redTownQuests", redCompletedTownQuests); + saveStringLongMap(data, "greenTownQuests", greenCompletedTownQuests); + saveStringLongMap(data, "cardsByRarity", cardsEarnedByRarity); + saveStringLongMap(data, "items", itemsGainedByName); + saveStringLongMap(data, "packs", packsEarnedBySet); + saveStringSet(data, "bossesDefeated", bossesDefeatedByName); + saveStringSet(data, "miniBossesDefeated", miniBossesDefeatedByName); + saveStringSet(data, "cardsUnlocked", cardsUnlockedByName); + saveStringSet(data, "setsUnlocked", setsUnlockedByCode); + saveStringSet(data, "lockedRegions", lockedWorldRegionsByName); + // Todo: Swap this for the ArchipelagoRandomizer data when that's enabled + if (archipelagoMode == ArchipelagoMode.solo_randomizer) { + saveStringSet(data, "colorlessEquipmentShop", localRandomizer.colorlessEquipmentShopList); + saveStringSet(data, "whiteEquipmentShop", localRandomizer.whiteEquipmentShopList); + saveStringSet(data, "blueEquipmentShop", localRandomizer.blueEquipmentShopList); + saveStringSet(data, "blackEquipmentShop", localRandomizer.blackEquipmentShopList); + saveStringSet(data, "redEquipmentShop", localRandomizer.redEquipmentShopList); + saveStringSet(data, "greenEquipmentShop", localRandomizer.greenEquipmentShopList); + saveStringSet(data, "whiteItemShop", localRandomizer.whiteItemShopList); + saveStringSet(data, "blueItemShop", localRandomizer.blueItemShopList); + saveStringSet(data, "blackItemShop", localRandomizer.blackItemShopList); + saveStringSet(data, "redItemShop", localRandomizer.redItemShopList); + saveStringSet(data, "greenItemShop", localRandomizer.greenItemShopList); + saveStringSet(data, "remainingEquipment", localRandomizer.remainingEquipmentPool); + data.store("checksSinceLastRegionReward", localRandomizer.checksSinceLastRegionReward); + } + if (archipelagoMode == ArchipelagoMode.networked_archipelago) { + saveItemDataSet(data, "colorlessEquipmentShop", networkedRandomizer.colorlessEquipmentShopList); + saveItemDataSet(data, "whiteEquipmentShop", networkedRandomizer.whiteEquipmentShopList); + saveItemDataSet(data, "blueEquipmentShop", networkedRandomizer.blueEquipmentShopList); + saveItemDataSet(data, "blackEquipmentShop", networkedRandomizer.blackEquipmentShopList); + saveItemDataSet(data, "redEquipmentShop", networkedRandomizer.redEquipmentShopList); + saveItemDataSet(data, "greenEquipmentShop", networkedRandomizer.greenEquipmentShopList); + saveItemDataSet(data, "whiteItemShop", networkedRandomizer.whiteItemShopList); + saveItemDataSet(data, "blueItemShop", networkedRandomizer.blueItemShopList); + saveItemDataSet(data, "blackItemShop", networkedRandomizer.blackItemShopList); + saveItemDataSet(data, "redItemShop", networkedRandomizer.redItemShopList); + saveItemDataSet(data, "greenItemShop", networkedRandomizer.greenItemShopList); + saveLongList(data,"locationQueue", networkedRandomizer.locationQueue); + data.storeObject("slotData", networkedRandomizer.getSlotData()); + data.store("lastIp", networkedRandomizer.getLastIp()); + data.store("lastPort", networkedRandomizer.getLastPort()); + data.store("lastSlotName", networkedRandomizer.getLastSlotName()); + data.store("lastPassword", networkedRandomizer.getLastPassword()); + } + + data.store("setUnlocksReceivedRest", setUnlockChecksRestAmount); + data.store("setUnlocksReceived", receivedAmountOfSetUnlockChecks); + data.store("totalBattlesWonColorless", totalBattlesWonColorless); + data.store("totalBattlesWonWhite", totalBattlesWonWhite); + data.store("totalBattlesWonBlue", totalBattlesWonBlue); + data.store("totalBattlesWonBlack", totalBattlesWonBlack); + data.store("totalBattlesWonRed", totalBattlesWonRed); + data.store("totalBattlesWonGreen", totalBattlesWonGreen); + data.store("lastTraversedRegion", lastTraversedRegion); + data.store("totalGold", totalGoldEarned); + data.store("extraLife", totalExtraMaxLifeEarned); + data.store("shards", totalShardsEarned); + data.store("lastArchipelagoRewardIndex", lastArchipelagoRewardIndex); + data.store("archipelagoMode", archipelagoMode.ordinal()); + data.store("totalSetUnlockChecks", totalAmountOfSetUnlockChecks); + + return data; + } + /// --- End --- +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoMode.java b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoMode.java new file mode 100644 index 000000000000..454db0b172fd --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoMode.java @@ -0,0 +1,7 @@ +package forge.adventure.archipelago; + +public enum ArchipelagoMode { + disabled, + solo_randomizer, + networked_archipelago +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoRandomizer.java b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoRandomizer.java new file mode 100644 index 000000000000..e0465b295c10 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoRandomizer.java @@ -0,0 +1,380 @@ +package forge.adventure.archipelago; +import forge.adventure.data.ItemData; +import forge.adventure.scene.ArchipelagoSettingsScene; +import forge.adventure.util.Current; +import io.github.archipelagomw.parts.NetworkItem; + +import java.util.*; + +public class ArchipelagoRandomizer { + private static final ArchipelagoRandomizer archipelagoRandomizerInstance = new ArchipelagoRandomizer(); + private final ArchipelagoData archipelagoDataInstance; + protected final Set colorlessEquipmentShopList = new HashSet<>(); + protected final Set whiteEquipmentShopList = new HashSet<>(); + protected final Set blueEquipmentShopList = new HashSet<>(); + protected final Set blackEquipmentShopList = new HashSet<>(); + protected final Set redEquipmentShopList = new HashSet<>(); + protected final Set greenEquipmentShopList = new HashSet<>(); + protected final Set whiteItemShopList = new HashSet<>(); + protected final Set blueItemShopList = new HashSet<>(); + protected final Set blackItemShopList = new HashSet<>(); + protected final Set redItemShopList = new HashSet<>(); + protected final Set greenItemShopList = new HashSet<>(); + protected final Set remainingEquipmentPool = new HashSet<>(); + protected final List locationQueue = new ArrayList<>(); + private SlotData slotData; + private String lastIp = ""; + private String lastPort = ""; + private String lastSlotName = ""; + private String lastPassword = ""; + + private ArchipelagoRandomizer() { + archipelagoDataInstance = ArchipelagoData.getInstance(); + } + + public static ArchipelagoRandomizer getInstance() { + return archipelagoRandomizerInstance; + } + + public void setupFreshSaveFile() { + clearShopData(); + remainingEquipmentPool.clear(); + locationQueue.clear(); + slotData = null; + ArchipelagoData.getInstance().setupFreshSaveFile(ArchipelagoMode.networked_archipelago); + } + + private void clearShopData() { + colorlessEquipmentShopList.clear(); + whiteEquipmentShopList.clear(); + blueEquipmentShopList.clear(); + blackEquipmentShopList.clear(); + redEquipmentShopList.clear(); + greenEquipmentShopList.clear(); + whiteItemShopList.clear(); + blueItemShopList.clear(); + blackItemShopList.clear(); + redItemShopList.clear(); + greenItemShopList.clear(); + } + + public void updatePlayerChecks(ArchipelagoCheckTypes type) { + if (slotData == null) { + System.err.print("SlotData was null somehow. Should be impossible."); + } else { + switch (type) { + case TOTAL_CARDS_EARNED -> { + for (Map.Entry entry : archipelagoDataInstance.cardsEarnedByRarity.entrySet()) { + switch (entry.getKey()) { + case "C" -> { + if (entry.getValue() > 0 && entry.getValue() % slotData.CommonCardsPerLocation == 0 && entry.getValue() / slotData.CommonCardsPerLocation <= slotData.CommonCardLocations) { + Archipelago.getInstance().checkLocation(4999L + entry.getValue() / slotData.CommonCardsPerLocation); + } + } + case "U" -> { + if (entry.getValue() > 0 && entry.getValue() % slotData.UncommonCardsPerLocation == 0 && entry.getValue() / slotData.UncommonCardsPerLocation <= slotData.UncommonCardLocations) { + Archipelago.getInstance().checkLocation(5099L + entry.getValue() / slotData.UncommonCardsPerLocation); + } + } + case "R" -> { + if (entry.getValue() > 0 && entry.getValue() % slotData.RareCardsPerLocation == 0 && entry.getValue() / slotData.RareCardsPerLocation <= slotData.RareCardLocations) { + Archipelago.getInstance().checkLocation(5199L + entry.getValue() / slotData.RareCardsPerLocation); + } + } + case "M" -> { + if (entry.getValue() > 0 && entry.getValue() % slotData.MythicRareCardsPerLocation == 0 && entry.getValue() / slotData.MythicRareCardsPerLocation <= slotData.MythicRareCardLocations) { + Archipelago.getInstance().checkLocation(5299L + entry.getValue() / slotData.MythicRareCardsPerLocation); + } + } + } + } + } + case COLORLESS_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonColorless > 0 && archipelagoDataInstance.totalBattlesWonColorless % slotData.FightsPerLocation == 0 && archipelagoDataInstance.totalBattlesWonColorless / slotData.FightsPerLocation <= slotData.FightLocations) { + Archipelago.getInstance().checkLocation(9999L + archipelagoDataInstance.totalBattlesWonColorless / slotData.FightsPerLocation); + } + } + case WHITE_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonWhite > 0 && archipelagoDataInstance.totalBattlesWonWhite % slotData.FightsPerLocation == 0 && archipelagoDataInstance.totalBattlesWonWhite / slotData.FightsPerLocation <= slotData.FightLocations) { + Archipelago.getInstance().checkLocation(19999L + archipelagoDataInstance.totalBattlesWonWhite / slotData.FightsPerLocation); + } + } + case BLUE_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonBlue > 0 && archipelagoDataInstance.totalBattlesWonBlue % slotData.FightsPerLocation == 0 && archipelagoDataInstance.totalBattlesWonBlue / slotData.FightsPerLocation <= slotData.FightLocations) { + Archipelago.getInstance().checkLocation(29999L + archipelagoDataInstance.totalBattlesWonBlue / slotData.FightsPerLocation); + } + } + case BLACK_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonBlack > 0 && archipelagoDataInstance.totalBattlesWonBlack % slotData.FightsPerLocation == 0 && archipelagoDataInstance.totalBattlesWonBlack / slotData.FightsPerLocation <= slotData.FightLocations) { + Archipelago.getInstance().checkLocation(39999L + archipelagoDataInstance.totalBattlesWonBlack / slotData.FightsPerLocation); + } + } + case RED_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonRed > 0 && archipelagoDataInstance.totalBattlesWonRed % slotData.FightsPerLocation == 0 && archipelagoDataInstance.totalBattlesWonRed / slotData.FightsPerLocation <= slotData.FightLocations) { + Archipelago.getInstance().checkLocation(49999L + archipelagoDataInstance.totalBattlesWonRed / slotData.FightsPerLocation); + } + } + case GREEN_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonGreen > 0 && archipelagoDataInstance.totalBattlesWonGreen % slotData.FightsPerLocation == 0 && archipelagoDataInstance.totalBattlesWonGreen / slotData.FightsPerLocation <= slotData.FightLocations) { + Archipelago.getInstance().checkLocation(59999L + archipelagoDataInstance.totalBattlesWonGreen / slotData.FightsPerLocation); + } + } + case COLORLESS_TOWN_EVENTS -> { + if (!archipelagoDataInstance.colorlessCompletedTownInnEvents.isEmpty() && archipelagoDataInstance.colorlessCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum() <= slotData.EventLocations) { + Archipelago.getInstance().checkLocation(10999L + archipelagoDataInstance.colorlessCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum()); + } + } + case WHITE_TOWN_EVENTS -> { + if (!archipelagoDataInstance.whiteCompletedTownInnEvents.isEmpty() && archipelagoDataInstance.whiteCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum() <= slotData.EventLocations) { + Archipelago.getInstance().checkLocation(20999L + archipelagoDataInstance.whiteCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum()); + } + } + case BLUE_TOWN_EVENTS -> { + if (!archipelagoDataInstance.blueCompletedTownInnEvents.isEmpty() && archipelagoDataInstance.blueCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum() <= slotData.EventLocations) { + Archipelago.getInstance().checkLocation(30999L + archipelagoDataInstance.blueCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum()); + } + } + case BLACK_TOWN_EVENTS -> { + if (!archipelagoDataInstance.blackCompletedTownInnEvents.isEmpty() && archipelagoDataInstance.blackCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum() <= slotData.EventLocations) { + Archipelago.getInstance().checkLocation(40999L + archipelagoDataInstance.blackCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum()); + } + } + case RED_TOWN_EVENTS -> { + if (!archipelagoDataInstance.redCompletedTownInnEvents.isEmpty() && archipelagoDataInstance.redCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum() <= slotData.EventLocations) { + Archipelago.getInstance().checkLocation(50999L + archipelagoDataInstance.redCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum()); + } + } + case GREEN_TOWN_EVENTS -> { + if (!archipelagoDataInstance.greenCompletedTownInnEvents.isEmpty() && archipelagoDataInstance.greenCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum() <= slotData.EventLocations) { + Archipelago.getInstance().checkLocation(60999L + archipelagoDataInstance.greenCompletedTownInnEvents.values().stream().mapToLong(Long::longValue).sum()); + } + } + case COLORLESS_TOWN_QUESTS -> { + if (!archipelagoDataInstance.colorlessCompletedTownQuests.isEmpty() && archipelagoDataInstance.colorlessCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum() <= slotData.QuestLocations) { + Archipelago.getInstance().checkLocation(11999L + archipelagoDataInstance.colorlessCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum()); + } + } + case WHITE_TOWN_QUESTS -> { + if (!archipelagoDataInstance.whiteCompletedTownQuests.isEmpty() && archipelagoDataInstance.whiteCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum() <= slotData.QuestLocations) { + Archipelago.getInstance().checkLocation(21999L + archipelagoDataInstance.whiteCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum()); + } + } + case BLUE_TOWN_QUESTS -> { + if (!archipelagoDataInstance.blueCompletedTownQuests.isEmpty() && archipelagoDataInstance.blueCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum() <= slotData.QuestLocations) { + Archipelago.getInstance().checkLocation(31999L + archipelagoDataInstance.blueCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum()); + } + } + case BLACK_TOWN_QUESTS -> { + if (!archipelagoDataInstance.blackCompletedTownQuests.isEmpty() && archipelagoDataInstance.blackCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum() <= slotData.QuestLocations) { + Archipelago.getInstance().checkLocation(41999L + archipelagoDataInstance.blackCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum()); + } + } + case RED_TOWN_QUESTS -> { + if (!archipelagoDataInstance.redCompletedTownQuests.isEmpty() && archipelagoDataInstance.redCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum() <= slotData.QuestLocations) { + Archipelago.getInstance().checkLocation(51999L + archipelagoDataInstance.redCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum()); + } + } + case GREEN_TOWN_QUESTS -> { + if (!archipelagoDataInstance.greenCompletedTownQuests.isEmpty() && archipelagoDataInstance.greenCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum() <= slotData.QuestLocations) { + Archipelago.getInstance().checkLocation(61999L + archipelagoDataInstance.greenCompletedTownQuests.values().stream().mapToLong(Long::longValue).sum()); + } + } + case SLIME_MOTHER_DEFEATED -> Archipelago.getInstance().checkLocation(100L); + case SLOBAD_DEFEATED -> Archipelago.getInstance().checkLocation(101L); + case XIRA_DEFEATED -> Archipelago.getInstance().checkLocation(102L); + case NAHIRI_DEFEATED -> Archipelago.getInstance().checkLocation(200L); + case VALYX_DEFEATED -> Archipelago.getInstance().checkLocation(201L); + case JACE_DEFEATED -> Archipelago.getInstance().checkLocation(300L); + case KIORA_DEFEATED -> Archipelago.getInstance().checkLocation(301L); + case MYR_SUPERION_DEFEATED -> Archipelago.getInstance().checkLocation(302L); + case SLIVER_QUEEN_DEFEATED -> Archipelago.getInstance().checkLocation(303L); + case TEFERI_DEFEATED -> Archipelago.getInstance().checkLocation(304L); + case GROLNOK_DEFEATED -> Archipelago.getInstance().checkLocation(400L); + case GUARDIAN_ANGEL_DEFEATED -> Archipelago.getInstance().checkLocation(401L); + case LILIANA_DEFEATED -> Archipelago.getInstance().checkLocation(402L); + case SLIMEFOOT_DEFEATED -> Archipelago.getInstance().checkLocation(403L); + case SORIN_DEFEATED -> Archipelago.getInstance().checkLocation(404L); + case CHANDRA_DEFEATED -> Archipelago.getInstance().checkLocation(500L); + case TIBALTS_TORTURER_DEFEATED -> Archipelago.getInstance().checkLocation(501L); + case TIBALT_DEFEATED -> Archipelago.getInstance().checkLocation(502L); + case ZEDRUUS_COOK_DEFEATED -> Archipelago.getInstance().checkLocation(503L); + case CONJURER_DEFEATED -> Archipelago.getInstance().checkLocation(504L); + case ZEDRUU_DEFEATED -> Archipelago.getInstance().checkLocation(505L); + case GARRUK_DEFEATED -> Archipelago.getInstance().checkLocation(600L); + case HYDRA_OF_SHANDALAAR_DEFEATED -> Archipelago.getInstance().checkLocation(601L); + case SCARECROW_CAPTAIN_DEFEATED -> Archipelago.getInstance().checkLocation(602L); + case BOSS_WHITE_DEFEATED -> Archipelago.getInstance().checkLocation(1L); + case BOSS_BLUE_DEFEATED -> Archipelago.getInstance().checkLocation(2L); + case BOSS_BLACK_DEFEATED -> Archipelago.getInstance().checkLocation(3L); + case BOSS_RED_DEFEATED -> Archipelago.getInstance().checkLocation(4L); + case BOSS_GREEN_DEFEATED -> Archipelago.getInstance().checkLocation(5L); + case WIN_CONDITION_CLEARED -> Archipelago.getInstance().goal(); + } + } + } + + // Todo: Verify that this is actually what we want and it's working + public void unlockMultipleItemReward(Map itemNamesAndAmounts) { + for (Map.Entry item : itemNamesAndAmounts.entrySet()) { + for (int i = 0; i < item.getValue(); i++) { + Current.player().addItem(item.getKey()); + archipelagoDataInstance.addItem(item.getKey()); + } + } + } + + public void unlockCardPackReward(String boosterPackName) { + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Forge AP:\n", "Card Pack Reward: ", ArchipelagoColors.Cyan, boosterPackName)); + archipelagoDataInstance.addPack(boosterPackName); + } + + public void unlockMaxLifeReward(int amount) { + Current.player().addMaxLife(amount); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Forge AP:\n", "Max Life Reward: ", ArchipelagoColors.Cyan, amount)); + archipelagoDataInstance.addMaxLife(amount); + } + + public void unlockItemReward(String itemName) { + Current.player().addItem(itemName); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Forge AP:\n", "Item Reward: ", ArchipelagoColors.Cyan, itemName)); + archipelagoDataInstance.addItem(itemName); + } + + public void unlockManaCrystalReward(Integer amount) { + Current.player().addShards(amount); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Forge AP:\n", "Shard Reward: ", ArchipelagoColors.Cyan, amount)); + archipelagoDataInstance.addShards(amount); + } + + public void unlockGoldReward(int amount) { + Current.player().giveGold(amount); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Forge AP:\n", "Gold Reward: ", ArchipelagoColors.Cyan, amount)); + archipelagoDataInstance.addGold(amount); + } + + public void handleShopData(List shopLocationScouts, Set checkedLocations) { + clearShopData(); + for (NetworkItem shopLocation : shopLocationScouts) { + boolean alreadyChecked = checkedLocations.contains(shopLocation.locationID); + + if (shopLocation.locationID >= 1000 && shopLocation.locationID < 1100) { + colorlessEquipmentShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1100 && shopLocation.locationID < 1200) { + whiteEquipmentShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1200 && shopLocation.locationID < 1300) { + whiteItemShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1300 && shopLocation.locationID < 1400) { + blueEquipmentShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1400 && shopLocation.locationID < 1500) { + blueItemShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1500 && shopLocation.locationID < 1600) { + blackEquipmentShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1600 && shopLocation.locationID < 1700) { + blackItemShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1700 && shopLocation.locationID < 1800) { + redEquipmentShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1800 && shopLocation.locationID < 1900) { + redItemShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 1900 && shopLocation.locationID < 2000) { + greenEquipmentShopList.add(new ItemData(shopLocation, alreadyChecked)); + } else if (shopLocation.locationID >= 2000 && shopLocation.locationID < 2100) { + greenItemShopList.add(new ItemData(shopLocation, alreadyChecked)); + } + } + ArchipelagoData.getInstance().setTotalAmountOfSetUnlockChecks(slotData.SetUnlockCount); + } + + public Set getShopItems(String shopName) { + if (shopName.toLowerCase().contains("items")) { + // Items shop name + if (shopName.toLowerCase().contains("white")) { + return whiteItemShopList; + } else if (shopName.toLowerCase().contains("blue")) { + return blueItemShopList; + } else if (shopName.toLowerCase().contains("black")) { + return blackItemShopList; + } else if (shopName.toLowerCase().contains("red")) { + return redItemShopList; + } else if (shopName.toLowerCase().contains("green")) { + return greenItemShopList; + } + } else { + // Equipment shop name + if (shopName.toLowerCase().contains("white")) { + return whiteEquipmentShopList; + } else if (shopName.toLowerCase().contains("blue")) { + return blueEquipmentShopList; + } else if (shopName.toLowerCase().contains("black")) { + return blackEquipmentShopList; + } else if (shopName.toLowerCase().contains("red")) { + return redEquipmentShopList; + } else if (shopName.toLowerCase().contains("green")) { + return greenEquipmentShopList; + } else { + return colorlessEquipmentShopList; + } + } + return null; + } + + public void setSlotData(SlotData slotData) { + this.slotData = slotData; + } + + public SlotData getSlotData() { + return slotData; + } + + public void setLastIp(String ip) { + lastIp = ip; + } + + public String getLastIp() { + return lastIp; + } + + public void setLastPort(String port) { + lastPort = port; + } + + public String getLastPort() { + return lastPort; + } + + public void setLastSlotName(String slotName) { + lastSlotName = slotName; + } + + public String getLastSlotName() { + return lastSlotName; + } + + public void setLastPassword(String password) { + lastPassword = password; + } + + public String getLastPassword() { + return lastPassword; + } + + public void sendQueuedLocations() { + Archipelago AP = Archipelago.getInstance(); + if (!locationQueue.isEmpty() && AP.sendQueuedLocations(locationQueue)) locationQueue.clear(); + } + + public void setupAPSettingScene() { + ArchipelagoSettingsScene apSettingsScene = ArchipelagoSettingsScene.instance(); + apSettingsScene.setIpTextField(lastIp); + apSettingsScene.setPortTextField(lastPort); + apSettingsScene.setSlotNameTextField(lastSlotName); + apSettingsScene.setPasswordTextField(lastPassword); + } + + public void incrementLastArchipelagoRewardIndex() { + archipelagoDataInstance.lastArchipelagoRewardIndex++; + } + + public int getLastArchipelagoRewardIndex() { + return archipelagoDataInstance.lastArchipelagoRewardIndex; + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoUtil.java b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoUtil.java new file mode 100644 index 000000000000..f3b9f3754af3 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoUtil.java @@ -0,0 +1,127 @@ +package forge.adventure.archipelago; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.utils.Array; +import forge.Graphics; +import forge.adventure.data.RewardData; +import forge.adventure.util.Config; +import forge.adventure.util.Reward; +import forge.assets.FImage; +import forge.assets.FSkinImage; + +import java.util.Arrays; +import java.util.List; + +public class ArchipelagoUtil { + private static final FImage CARD_LOCKED_ICON = FSkinImage.LOCK; + public static final int TOTAL_AMOUNT_OF_SET_UNLOCK_CHECKS = 100; + public static final String[] REGION_NAMES = {"waste", "white", "blue", "black", "red", "green"}; + public static void drawLockedCardOverlay( + Batch batch, float x, float y, float w, float h) { + // Draw lock icon + // Normalize bounds (works for flipped cards too) + float drawX = Math.min(x, x + w); + float drawY = Math.min(y, y + h); + float drawW = Math.abs(w); + float drawH = Math.abs(h); + + // Lock size + float lockSize = drawW * 0.25f; + + // Centered horizontally, near visual top + float lockX = drawX + (drawW - lockSize) / 2f; + float lockY = drawY + drawH - lockSize - (drawH * 0.05f); + + // Draw lock + TextureRegion lockIcon = FSkinImage.LOCK.getTextureRegion(); + batch.draw(lockIcon, lockX, lockY, lockSize, lockSize); + } + + public static void drawLockedCardOverlay(Graphics g, float x, float y, float w, float h) { + float lockSize = w * 0.25f; + float lockX = x + (w - lockSize) / 2f; + float lockY = y + h * 0.05f; + CARD_LOCKED_ICON.draw(g, lockX, lockY, lockSize, lockSize); + + // Draw the card darker than the rest to show it's not unlocked. + float oldalpha = g.getfloatAlphaComposite(); + g.setAlphaComposite(0.25f); + g.fillRect(Color.BLACK, x, y, w, h); + g.setAlphaComposite(oldalpha); + } + + private static TextureRegion getRegionRune(String regionName) { + return switch (regionName.toLowerCase()) { + case "white" -> Config.instance().getItemSprite("WhiteRune"); + case "blue" -> Config.instance().getItemSprite("BlueRune"); + case "black" -> Config.instance().getItemSprite("BlackRune"); + case "red" -> Config.instance().getItemSprite("RedRune"); + case "green" -> Config.instance().getItemSprite("GreenRune"); + default -> null; + }; + } + + public static void updateLastTraversedRegionOnTeleport(String command) { + String loweredCommand = command.toLowerCase(); + ArchipelagoData apData = ArchipelagoData.getInstance(); + if (loweredCommand.contains("spawn")) { apData.setLastTraversedRegion("waste"); } + if (loweredCommand.contains("plains")) { apData.setLastTraversedRegion("white"); } + if (loweredCommand.contains("island")) { apData.setLastTraversedRegion("blue"); } + if (loweredCommand.contains("swamp")) { apData.setLastTraversedRegion("black"); } + if (loweredCommand.contains("mountain")) { apData.setLastTraversedRegion("red"); } + if (loweredCommand.contains("forest")) { apData.setLastTraversedRegion("green"); } + } + + public static void drawLockedRegionOverhead(Batch batch, String regionName, float centerX, float baseY, float alpha) { + TextureRegion rune = getRegionRune(regionName); + if (rune == null) + return; + + float size = 24f; // world-space size; tweak if needed + float x = centerX - size / 2f; + float y = baseY + 16f; // slightly above head + + Color old = new Color(batch.getColor()); + + // Draw rune + batch.setColor(1f, 1f, 1f, alpha); + batch.draw(rune, x, y, size, size); + + // Draw red cross overlay + TextureRegion cross = FSkinImage.WARNING.getTextureRegion(); + batch.draw(cross, x, y, size / 2, size / 2); + + batch.setColor(old); + } + + public static RewardData generateRewardData(String type, int count, String itemName) { + RewardData data = new RewardData(); + data.type = type; + data.count = count; + data.itemName = itemName; + return data; + } + + public static Reward generateReward(String type, int count, String itemName) { + RewardData data = generateRewardData(type, count, itemName); + Array replacedAPReward = data.generate(false, true); + if (replacedAPReward != null && !replacedAPReward.isEmpty()) { + return replacedAPReward.get(0); + } + return null; + } + + /// We depend on these POI names to have these exact names. + /// These are used as identifiers to make sure that when generating a world in networked archipelago they are placed close enough to the center to always be well accessible within their region. + /// This is to prevent logic errors where a region unlock might be locked behind a certain miniboss dungeon, but that dungeon happened to generate in a random bubble within another region. + public static List getArchipelagoPois() { + return Arrays.asList("slime cave", "slobads factory", "xiras hive", "emrakul", "quest_aportaltonowhere", "quest_digsite", "quest_primaljungle", "quest_banditcave", + "nahiri encampment", "unhallowedabbey", + "kiora island", "teferi hideout", "jacehold", "skep", "quest_libraryofvarsil", + "slimefoots lair", "temple of liliana", "grolnoks bog", "vampirecastle3", + "temple of chandra", "tibalts fortress", "zedruu city", "quest_shardmines", + "garruk forest", "scarecrow farm", "quest_frostbittencavern", "grove7"); + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/ItemRegistry.java b/forge-gui-mobile/src/forge/adventure/archipelago/ItemRegistry.java new file mode 100644 index 000000000000..1de5ac716288 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ItemRegistry.java @@ -0,0 +1,160 @@ +package forge.adventure.archipelago; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public class ItemRegistry { + private static final Map DEFAULT_ITEMS = ImmutableMap.builder() + .put(1L, "White rune") + .put(2L, "Blue rune") + .put(3L, "Black rune") + .put(4L, "Red rune") + .put(5L, "Green rune") + .put(1000L, "Gold (S)") + .put(1001L, "Gold (M)") + .put(1002L, "Gold (L)") + .put(1003L, "Mana Shards (S)") + .put(1004L, "Mana Shards (M)") + .put(1005L, "Mana Shards (L)") + .put(1006L, "Bronze Challenge Coin") + .put(1007L, "Silver Challenge Coin") + .put(1008L, "Gold Challenge Coin") + .put(1009L, "Set Unlock") + .put(1010L, "Life +1") + .put(1011L, "Life +2") + .put(2000L, "Unlock White") + .put(2001L, "Unlock Blue") + .put(2002L, "Unlock Black") + .put(2003L, "Unlock Red") + .put(2004L, "Unlock Green") + .put(3000L, "Chandra's Tome") + .put(3001L, "Phoenix Charm") + .put(3002L, "Demonic Contract") + .put(3003L, "Piper's Charm") + .put(3004L, "Sleep Wand") + .put(3005L, "Hill Giant Club") + .put(3006L, "Cursed Treasure") + .put(3007L, "Farmer's Tools") + .put(3008L, "Battle Standard") + .put(3009L, "Hivestone") + .put(3010L, "Life Amulet") + .put(3011L, "Axt") + .put(3012L, "Bronze Sword") + .put(3013L, "Iron Boots") + .put(3014L, "Iron Shield") + .put(3015L, "Iron Armor") + .put(3016L, "Steel Sword") + .put(3017L, "Steel Boots") + .put(3018L, "Steel Shield") + .put(3019L, "Steel Armor") + .put(3020L, "Armor of the Hivelord") + .put(3021L, "Leather Boots") + .put(3022L, "Jungle Shield") + .put(3023L, "Sorin's Amulet") + .put(3024L, "Dagger") + .put(3025L, "Aladdin's Ring") + .put(3026L, "Spell Book") + .put(3027L, "Cursed Ring") + .put(3028L, "Mithril Boots") + .put(3029L, "Mithril Shield") + .put(3030L, "Mithril Armor") + .put(3031L, "Presence of the Hydra") + .put(3032L, "Death Ring") + .put(3033L, "Flame Sword") + .put(3034L, "Mirror Shield") + .put(3035L, "Dungeon Map") + .put(3036L, "Aladdin's Lamp") + .put(3037L, "Heart-Piercer") + .put(3038L, "Wood Bow") + .put(3039L, "Sandals") + .put(3040L, "Gold Boots") + .put(3041L, "Gold Shield") + .put(3042L, "Gold Armor") + .put(3043L, "Dark Boots") + .put(3044L, "Dark Shield") + .put(3045L, "Dark Armor") + .put(3046L, "Blood Vial") + .put(3047L, "Charm") + .put(3048L, "Snack") + .put(3049L, "Change") + .put(3050L, "Treasure") + .put(3051L, "Magic Shard") + .put(3052L, "Mad Staff") + .put(3053L, "Dark Amulet") + .put(3054L, "Pandora's Box") + .put(3055L, "Disrupting Scepter") + .put(3056L, "Entrancing Lyre") + .put(3057L, "Heavy Arbalest") + .put(3058L, "Ring of Three Wishes") + .put(3059L, "The Blackstaff of Waterdeep") + .put(3060L, "Unerring Sling") + .put(3061L, "Jeweled Amulet") + .put(3062L, "Traveler's Amulet") + .put(3063L, "Relic Amulet") + .put(3064L, "Amulet of Kroog") + .put(3065L, "Amulet of Vigor") + .put(3066L, "Veilstone Amulet") + .put(3067L, "Jandor's Ring") + .put(3068L, "Jinxed Ring") + .put(3069L, "Nine-Ringed Bo") + .put(3070L, "Ring of Immortals") + .put(3071L, "Prism Ring") + .put(3072L, "Ring of Renewal") + .put(3073L, "Kite Shield") + .put(3074L, "Shell Wand") + .put(3075L, "Manasight Amulet") + .put(3076L, "Lightbringers Boots") + .put(3077L, "Fortune Coin") + .put(3078L, "White Staff") + .put(3079L, "Black Staff") + .put(3080L, "Blue Staff") + .put(3081L, "Red Staff") + .put(3082L, "Green Staff") + .put(3083L, "Slimefoot's Slimy Staff") + .put(3084L, "Kiora's Bident") + .put(3085L, "Slime-Covered Boots") + .put(3086L, "Amulet of the Deceiver") + .put(3087L, "Jace's Signature Hoodie") + .put(3088L, "Teferi's Staff") + .put(3089L, "Garruk's Mighty Axe") + .put(3090L, "Nahiri's Armory") + .put(3091L, "Giant Scythe") + .put(3092L, "Chicken Egg") + .put(3093L, "Tibalt's Bag of Tricks") + .put(3094L, "Xira's Fancy Hat") + .put(3095L, "The Underworld Cookbook") + .put(3096L, "Mantle of Ancient Lore") + .put(3097L, "Zedruu's Lantern") + .put(3098L, "Grolnok's Skin") + .put(3099L, "Slobad's Iron Boots") + .put(3100L, "Hallowed Sigil") + .put(3101L, "Unhallowed Sigil") + .put(3102L, "Basilisk Collar") + .put(3103L, "Evil Ankh") + .put(3104L, "Staff of the Demoncaller") + .put(3105L, "Concordant Boots") + .put(3106L, "Crown of Growth") + .put(3107L, "Raptor Bondband") + .put(3108L, "Mantle of Denial") + .put(3109L, "Golden Egg") + .put(3110L, "Amulet of Telepathy") + .put(3111L, "Warren Tender's Baton") + .put(3112L, "Prayerbook of Ire") + .put(3113L, "Robes of Omniscience") + .put(3114L, "Angelic Armaments") + .put(3115L, "Angelic Greaves") + .put(4000L, "Sol Ring") + .put(4001L, "Black Lotus") + .put(4002L, "Mox Pearl") + .put(4003L, "Mox Sapphire") + .put(4004L, "Mox Jet") + .put(4005L, "Mox Ruby") + .put(4006L, "Mox Emerald") + .put(5000L, "Cheat") + .build(); + + public static String getItem(Long input) { + return DEFAULT_ITEMS.get(input); + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/LocalRandomizer.java b/forge-gui-mobile/src/forge/adventure/archipelago/LocalRandomizer.java new file mode 100644 index 000000000000..f5f02b682537 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/LocalRandomizer.java @@ -0,0 +1,364 @@ +package forge.adventure.archipelago; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Json; +import forge.adventure.data.ItemData; +import forge.adventure.util.*; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.*; + +/// --- The checks below are mostly functional offline and should not be called from the networked part of the AP implementation. --- +public class LocalRandomizer { + private static final LocalRandomizer localRandomizerInstance = new LocalRandomizer(); + private final ArchipelagoData archipelagoDataInstance; + + protected final Set colorlessEquipmentShopList = new HashSet<>(); + protected final Set whiteEquipmentShopList = new HashSet<>(); + protected final Set blueEquipmentShopList = new HashSet<>(); + protected final Set blackEquipmentShopList = new HashSet<>(); + protected final Set redEquipmentShopList = new HashSet<>(); + protected final Set greenEquipmentShopList = new HashSet<>(); + protected final Set whiteItemShopList = new HashSet<>(); + protected final Set blueItemShopList = new HashSet<>(); + protected final Set blackItemShopList = new HashSet<>(); + protected final Set redItemShopList = new HashSet<>(); + protected final Set greenItemShopList = new HashSet<>(); + protected final Set remainingEquipmentPool = new HashSet<>(); + protected int checksSinceLastRegionReward = 0; + + private final int totalBattlesWonBreakpoint = 3; // Reward for every 3 battles won. + private final int totalTownQuestsBreakpoint = 1; // Reward for every 1 town quests done. + private final int totalTownEventsBreakpoint = 1; // Reward for every 1 town events done. + private final int totalCardsEarnedBreakPoint = 80; // Reward for every 80 unique cards gained. + private final int regionUnlockBreakpoint = 8; + private final int regionUnlockChance = 10; + private final int goldRewardChance = 20; + private final int manaRewardChance = 20; + private final int maxLifeRewardChance = 5; + private final int equipmentRewardChance = 5; + + private LocalRandomizer() { + archipelagoDataInstance = ArchipelagoData.getInstance(); + } + + public static LocalRandomizer getInstance() { + return localRandomizerInstance; + } + + public void setupFreshSaveFile() { + colorlessEquipmentShopList.clear(); + whiteEquipmentShopList.clear(); + blueEquipmentShopList.clear(); + blackEquipmentShopList.clear(); + redEquipmentShopList.clear(); + greenEquipmentShopList.clear(); + whiteItemShopList.clear(); + blueItemShopList.clear(); + blackItemShopList.clear(); + redItemShopList.clear(); + greenItemShopList.clear(); + remainingEquipmentPool.clear(); + randomizeLocalEquipment(); + ArchipelagoData.getInstance().setupFreshSaveFile(ArchipelagoMode.solo_randomizer); + } + + public void updatePlayerChecks(ArchipelagoCheckTypes type, @NonNull String notificationMessage) { + switch (type) { + case TOTAL_CARDS_EARNED -> { + long totalCardsEarned = 0; + for (long value : archipelagoDataInstance.cardsEarnedByRarity.values()) { + totalCardsEarned += value; + } + if (totalCardsEarned > 0 && totalCardsEarned % totalCardsEarnedBreakPoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + case COLORLESS_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonColorless > 0 && archipelagoDataInstance.totalBattlesWonColorless % totalBattlesWonBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + case WHITE_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonWhite > 0 && archipelagoDataInstance.totalBattlesWonWhite % totalBattlesWonBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + case BLUE_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonBlue > 0 && archipelagoDataInstance.totalBattlesWonBlue % totalBattlesWonBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + case BLACK_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonBlack > 0 && archipelagoDataInstance.totalBattlesWonBlack % totalBattlesWonBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + case RED_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonRed > 0 && archipelagoDataInstance.totalBattlesWonRed % totalBattlesWonBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + case GREEN_BATTLE_WON -> { + if (archipelagoDataInstance.totalBattlesWonGreen > 0 && archipelagoDataInstance.totalBattlesWonGreen % totalBattlesWonBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + case COLORLESS_TOWN_EVENTS -> handleTownEventDone(archipelagoDataInstance.colorlessCompletedTownInnEvents, notificationMessage); + case WHITE_TOWN_EVENTS -> handleTownEventDone(archipelagoDataInstance.whiteCompletedTownInnEvents, notificationMessage); + case BLUE_TOWN_EVENTS -> handleTownEventDone(archipelagoDataInstance.blueCompletedTownInnEvents, notificationMessage); + case BLACK_TOWN_EVENTS -> handleTownEventDone(archipelagoDataInstance.blackCompletedTownInnEvents, notificationMessage); + case RED_TOWN_EVENTS -> handleTownEventDone(archipelagoDataInstance.redCompletedTownInnEvents, notificationMessage); + case GREEN_TOWN_EVENTS -> handleTownEventDone(archipelagoDataInstance.greenCompletedTownInnEvents, notificationMessage); + case COLORLESS_TOWN_QUESTS -> handleTownQuestDone(archipelagoDataInstance.colorlessCompletedTownQuests, notificationMessage); + case WHITE_TOWN_QUESTS -> handleTownQuestDone(archipelagoDataInstance.whiteCompletedTownQuests, notificationMessage); + case BLUE_TOWN_QUESTS -> handleTownQuestDone(archipelagoDataInstance.blueCompletedTownQuests, notificationMessage); + case BLACK_TOWN_QUESTS -> handleTownQuestDone(archipelagoDataInstance.blackCompletedTownQuests, notificationMessage); + case RED_TOWN_QUESTS -> handleTownQuestDone(archipelagoDataInstance.redCompletedTownQuests, notificationMessage); + case GREEN_TOWN_QUESTS -> handleTownQuestDone(archipelagoDataInstance.greenCompletedTownQuests, notificationMessage); + } + } + + private void handleTownEventDone(Map completedTownEventsList, String notificationMessage) { + int totalTownEventsDone = 0; + totalTownEventsDone += (int) completedTownEventsList.values().stream().mapToLong(Long::longValue).sum(); + if (archipelagoDataInstance.archipelagoMode == ArchipelagoMode.solo_randomizer) { + if (totalTownEventsDone > 0 && totalTownEventsDone % totalTownEventsBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + } + + private void handleTownQuestDone(Map completedTownQuestsList, String notificationMessage) { + int totalTownQuestsDone = 0; + totalTownQuestsDone += (int) completedTownQuestsList.values().stream().mapToLong(Long::longValue).sum(); + if (archipelagoDataInstance.archipelagoMode == ArchipelagoMode.solo_randomizer) { + if (totalTownQuestsDone > 0 && totalTownQuestsDone % totalTownQuestsBreakpoint == 0) { + generateRandomizedReward(notificationMessage); + } + } + } + + // Randomly picks between the 4 reward options. + private void generateRandomizedReward(String notificationMessage) { + Random random = new Random(); + int roll; + if (archipelagoDataInstance.lockedWorldRegionsByName.isEmpty()) { + // Skip the region reward chance if the pool is empty. + roll = random.nextInt(regionUnlockChance, 100); + } else { + roll = random.nextInt(100); + } + + // Guarantee a region reward after X checks where X is regionUnlockBreakpoint. + if (!archipelagoDataInstance.lockedWorldRegionsByName.isEmpty() && (roll < regionUnlockChance || checksSinceLastRegionReward >= regionUnlockBreakpoint)) { + unlockRandomRegion(notificationMessage); + checksSinceLastRegionReward = 0; + } else { + checksSinceLastRegionReward++; + if (roll < regionUnlockChance + goldRewardChance) { + roll = random.nextInt(3); + int goldAmount = 750; + switch (roll) { + case 1 -> goldAmount = 1500; + case 2 -> goldAmount = 3000; + } + archipelagoDataInstance.generateGameNotification(String.format(notificationMessage + ":\n%s%s%s{RESET}", ArchipelagoColors.Cyan, goldAmount, " Gold")); + unlockGoldReward(goldAmount); + } else if (roll < regionUnlockChance + goldRewardChance + manaRewardChance) { + roll = random.nextInt(3); + int manaAmount = 20; + switch (roll) { + case 1 -> manaAmount = 30; + case 2 -> manaAmount = 50; + } + archipelagoDataInstance.generateGameNotification(String.format(notificationMessage + ":\n%s%s%s{RESET}", ArchipelagoColors.Cyan, manaAmount, " Shards")); + unlockManaCrystalReward(manaAmount); + } else if (roll < regionUnlockChance + goldRewardChance + manaRewardChance + maxLifeRewardChance) { + archipelagoDataInstance.generateGameNotification(String.format(notificationMessage + ":\n%s%s{RESET}", ArchipelagoColors.Cyan, "Max Life +1")); + unlockMaxLifeReward(1); + } else if (roll < regionUnlockChance + goldRewardChance + manaRewardChance + maxLifeRewardChance + equipmentRewardChance) { + Reward reward = takeSingleEquipmentOutOfRemainingPool(); + switch (reward.getType()) { + case Gold -> { + archipelagoDataInstance.generateGameNotification(String.format(notificationMessage + ":\n%s%s%s{RESET}", ArchipelagoColors.Cyan, reward.getCount(), " Gold")); + unlockGoldReward(reward.getCount()); + } + case Shards -> { + archipelagoDataInstance.generateGameNotification(String.format(notificationMessage + ":\n%s%s%s{RESET}", ArchipelagoColors.Cyan, reward.getCount(), "S")); + unlockManaCrystalReward(reward.getCount()); + } + case Item -> { + archipelagoDataInstance.generateGameNotification(String.format(notificationMessage + ":\n%s%s{RESET}", ArchipelagoColors.Cyan, reward.getItem().name)); + unlockItemReward(reward.getItem().name); + } + } + } else { + // If no other change was hit, generate a set unlock. At the moment there is a 40% chance of this by default which becomes 50% once all regions are unlocked. + archipelagoDataInstance.unlockRandomSet(notificationMessage); + } + } + } + + // Todo: Make the max life upgrades available through another method (more checks perhaps) + // Todo: Figure out what to do with the "overpowered" equipment. + // Each reward has a RewardType of "item" and comes pre-defined with an itemName. + // They are defined in Shandalar/Shops.json as Equipment, Item and Equipment. We can dynamically detect those names and replace their items if AP mode is enabled here. + // Equipment: 6 slots to randomize + // Equipment: 6 slots to randomize + // Items: 8 slots to randomize including 1 slot that is not equipment but rather a max health upgrade + public void randomizeLocalEquipment() { + // First we get the names of all the items in the pool. + ArrayList equipmentNames = new ArrayList<>(); + // We also filter out the "overpowered" cards into this separate list, they might be fun to throw in later. + ArrayList powerEquipmentNames = new ArrayList<>(); + FileHandle handle = Config.instance().getFile(Paths.ITEMS); + if (handle.exists()) { + Json json = new Json(); + Array shopList = json.fromJson(Array.class, ItemData.class, handle); + + // First we read all the items from `adventure/common/world/items.json` + for (int i = 0; i < shopList.size; i++) { + if (shopList.get(i).equipmentSlot != null && !shopList.get(i).equipmentSlot.isEmpty()) { + // We've found an equipment item, add it to the list but exclude the "overpowered" items. + if (shopList.get(i).name.toLowerCase().contains("rune")) continue; + if (shopList.get(i).name.toLowerCase().contains("mox") || shopList.get(i).name.toLowerCase().contains("black lotus") || shopList.get(i).name.toLowerCase().contains("sol ring") || shopList.get(i).name.toLowerCase().contains("cheat")) { + powerEquipmentNames.add(shopList.get(i).name); + continue; + } + equipmentNames.add(shopList.get(i).name); + } + } + } + + // Scramble the equipment names + Collections.shuffle(equipmentNames); + // Return if we didn't find enough items. + if (equipmentNames.size() < 72) return; + // Redistribute the items over each list. + LocalRandomizer localRandomizer = LocalRandomizer.getInstance(); + colorlessEquipmentShopList.addAll(equipmentNames.subList(0, 6)); + whiteEquipmentShopList.addAll(equipmentNames.subList(6, 12)); + blueEquipmentShopList.addAll(equipmentNames.subList(12, 18)); + blackEquipmentShopList.addAll(equipmentNames.subList(18, 24)); + redEquipmentShopList.addAll(equipmentNames.subList(24, 30)); + greenEquipmentShopList.addAll(equipmentNames.subList(30, 36)); + whiteItemShopList.addAll(equipmentNames.subList(36, 43)); + blueItemShopList.addAll(equipmentNames.subList(43, 50)); + blackItemShopList.addAll(equipmentNames.subList(50, 57)); + redItemShopList.addAll(equipmentNames.subList(57, 64)); + greenItemShopList.addAll(equipmentNames.subList(64, 71)); + // Remove the first 72 items from the equipment list so we can use the remaining ones for future equipment rewards. + equipmentNames.removeAll(equipmentNames.subList(0, 71)); + remainingEquipmentPool.addAll(equipmentNames); + } + + // Todo: Expand this function to be able to return a list of rewards so that we can award the player gold, shards & a pack in place of their item. + // Returns & rewards the player a random item from the remainingEquipmentPool and then removes it from the list. If no item is left, the player is instead rewarded with gold or shards. + // This produces a different result each time it's rolled, the RNG isn't seeded. + public Reward takeSingleEquipmentOutOfRemainingPool() { + Random random = new Random(); + while (true) { + if (!remainingEquipmentPool.isEmpty()) { + List remainingEquipmentList = new ArrayList<>(remainingEquipmentPool); + String equipmentCandidate = remainingEquipmentList.get(random.nextInt(remainingEquipmentList.size())); + remainingEquipmentPool.remove(equipmentCandidate); + return ArchipelagoUtil.generateReward("item", 1, equipmentCandidate); + } else { + // Generate a random standard reward in place of the item. + int chosenItemType = random.nextInt(2); + switch (chosenItemType) { + case 0: + return new Reward(Reward.Type.Gold, 3000); + case 1: + return new Reward(Reward.Type.Shards, 50); + } + } + } + } + + // Todo: Create a function that returns a list of equipment for any given shop to sell based on the previously randomized lists. + public Object[] getItemsForEquipmentShop(String shopName) { + if (shopName.toLowerCase().contains("items")) { + // Items shop name + if (shopName.toLowerCase().contains("white")) { + return whiteItemShopList.toArray(); + } else if (shopName.toLowerCase().contains("blue")) { + return blueItemShopList.toArray(); + } else if (shopName.toLowerCase().contains("black")) { + return blackItemShopList.toArray(); + } else if (shopName.toLowerCase().contains("red")) { + return redItemShopList.toArray(); + } else if (shopName.toLowerCase().contains("green")) { + return greenItemShopList.toArray(); + } + } else { + // Equipment shop name + if (shopName.toLowerCase().contains("white")) { + return whiteEquipmentShopList.toArray(); + } else if (shopName.toLowerCase().contains("blue")) { + return blueEquipmentShopList.toArray(); + } else if (shopName.toLowerCase().contains("black")) { + return blackEquipmentShopList.toArray(); + } else if (shopName.toLowerCase().contains("red")) { + return redEquipmentShopList.toArray(); + } else if (shopName.toLowerCase().contains("green")) { + return greenEquipmentShopList.toArray(); + } else { + return colorlessEquipmentShopList.toArray(); + } + } + return null; + } + + public void unlockCardPackReward(String boosterPackName) { + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Randomizer:\n", "Card Pack Reward: ", ArchipelagoColors.Cyan, boosterPackName)); + archipelagoDataInstance.addPack(boosterPackName); + } + + public void unlockMaxLifeReward(int amount) { + Current.player().addMaxLife(amount); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Randomizer:\n", "Max Life Reward: ", ArchipelagoColors.Cyan, amount)); + archipelagoDataInstance.addMaxLife(amount); + } + + public void unlockItemReward(String itemName) { + Current.player().addItem(itemName); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Randomizer:\n", "Item Reward: ", ArchipelagoColors.Cyan, itemName)); + archipelagoDataInstance.addItem(itemName); + } + + public void unlockManaCrystalReward(Integer amount) { + Current.player().addShards(amount); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Randomizer:\n", "Shard Reward: ", ArchipelagoColors.Cyan, amount)); + archipelagoDataInstance.addShards(amount); + } + + public void unlockGoldReward(int amount) { + Current.player().giveGold(amount); + System.out.println(String.format("%s%s{RESET}%s%s%s{RESET}", ArchipelagoColors.Salmon, "Randomizer:\n", "Gold Reward: ", ArchipelagoColors.Cyan, amount)); + archipelagoDataInstance.addGold(amount); + } + + public void unlockRandomRegion(String notificationMessage) { + if (archipelagoDataInstance.lockedWorldRegionsByName.isEmpty()) { + return; + } + + int targetRegionIndex = new Random().nextInt(archipelagoDataInstance.lockedWorldRegionsByName.size()); + int setIndex = 0; + for (String region : archipelagoDataInstance.lockedWorldRegionsByName) { + if (setIndex++ == targetRegionIndex) { + for (String runeName : archipelagoDataInstance.regionTeleportingRunes) { + if (runeName.toLowerCase().contains(region.toLowerCase())) { + archipelagoDataInstance.generateGameNotification(String.format(notificationMessage + ":\nRegion Rune: %s%s{RESET}", ArchipelagoColors.Cyan, runeName)); + Current.player().addItem(runeName); + return; + } + } + } + } + } +} +/// --- End --- diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/SlotData.java b/forge-gui-mobile/src/forge/adventure/archipelago/SlotData.java new file mode 100644 index 000000000000..085d29cc6ad8 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/SlotData.java @@ -0,0 +1,54 @@ +package forge.adventure.archipelago; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class SlotData implements Serializable { + @SerializedName("castles_required") + public int CastlesRequired; + @SerializedName("color_sanity") + public int ColorSanity; + @SerializedName("starting_color") + public int StartColor; + @SerializedName("fight_locations") + public int FightLocations; + @SerializedName("fight_amount_per_location") + public int FightsPerLocation; + @SerializedName("quest_locations") + public int QuestLocations; + @SerializedName("event_locations") + public int EventLocations; + @SerializedName("include_miniboss_locations") + public int IncludeMinibossLocations; + @SerializedName("common_card_locations") + public int CommonCardLocations; + @SerializedName("common_cards_per_location") + public int CommonCardsPerLocation; + @SerializedName("uncommon_card_locations") + public int UncommonCardLocations; + @SerializedName("uncommon_cards_per_location") + public int UncommonCardsPerLocation; + @SerializedName("rare_card_locations") + public int RareCardLocations; + @SerializedName("rare_cards_per_location") + public int RareCardsPerLocation; + @SerializedName("mythic_rare_card_locations") + public int MythicRareCardLocations; + @SerializedName("mythic_rare_cards_per_location") + public int MythicRareCardsPerLocation; + @SerializedName("gift_pack") + public int GiftPack; + @SerializedName("min_shop_price") + public int MinShopPrice; + @SerializedName("max_shop_price") + public int MaxShopPrice; + @SerializedName("gold_multiplier_percentage") + public int GoldMultiplierPercentage; + @SerializedName("death_link") + public int DeathLink; + @SerializedName("set_unlock_count") + public int SetUnlockCount; + @SerializedName("seed") + public String Seed; +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/events/ConnectResult.java b/forge-gui-mobile/src/forge/adventure/archipelago/events/ConnectResult.java new file mode 100644 index 000000000000..b602e616a58b --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/events/ConnectResult.java @@ -0,0 +1,77 @@ +package forge.adventure.archipelago.events; + +import com.github.tommyettinger.textra.TypingLabel; +import forge.adventure.archipelago.ArchipelagoRandomizer; +import forge.adventure.archipelago.SlotData; +import forge.adventure.archipelago.ArchipelagoClient; +import forge.adventure.scene.ArchipelagoSettingsScene; +import io.github.archipelagomw.events.ArchipelagoEventListener; +import io.github.archipelagomw.events.ConnectionResultEvent; +import io.github.archipelagomw.network.ConnectionResult; + +import java.util.ArrayList; + +public class ConnectResult { + + private final ArchipelagoClient APClient; + private TypingLabel connectStatusLabel; + + public ConnectResult(ArchipelagoClient APClient, TypingLabel connectStatusLabel) { + this.APClient = APClient; + this.connectStatusLabel = connectStatusLabel; + } + + @ArchipelagoEventListener + public void onConnectResult(ConnectionResultEvent event) { + if (event.getResult() == ConnectionResult.Success) { + try { + ArchipelagoRandomizer APRandomizer = ArchipelagoRandomizer.getInstance(); + + if (APRandomizer.getSlotData() != null && !APRandomizer.getSlotData().Seed.equals(event.getSlotData(SlotData.class).Seed)) { + APClient.disconnect(); + connectStatusLabel.setText(""); + connectStatusLabel.setText("{FADE=PURPLE;PURPLE;0.1}Seed Mismatch"); + return; + } + + APRandomizer.setLastIp(ArchipelagoSettingsScene.instance().getIpTextField()); + APRandomizer.setLastPort(ArchipelagoSettingsScene.instance().getPortTextField()); + APRandomizer.setLastSlotName(ArchipelagoSettingsScene.instance().getSlotNameTextField()); + APRandomizer.setLastPassword(ArchipelagoSettingsScene.instance().getPasswordTextField()); + + connectStatusLabel.setText(""); + connectStatusLabel.setText("{FADE=GREEN;GREEN;0.1}Connected!"); + APClient.setSlotData(event.getSlotData(SlotData.class)); + if (APRandomizer.getSlotData() == null){ + // Initial connection setup + APRandomizer.setSlotData(APClient.slotData); + ArrayList locations = new ArrayList<>(APClient.getLocationManager().getMissingLocations()); + locations.addAll(APClient.getLocationManager().getCheckedLocations()); + APClient.scoutLocations(locations); + } + APRandomizer.sendQueuedLocations(); + // Starting items currently not supported, commenting this out for now. +// APClient.slotData.parseStartingItems(registries); + } catch (Exception e) { + e.printStackTrace(); + } + + // deathlink currently not supported, commenting this out for now. +// if (APClient.slotData.DeathLink == 1) { +// APClient.setDeathLinkEnabled(true); +// } + } else if (event.getResult() == ConnectionResult.InvalidPassword) { + connectStatusLabel.setText(""); + connectStatusLabel.setText("{FADE=RED;RED;0.1}Invalid Password"); + } else if (event.getResult() == ConnectionResult.IncompatibleVersion) { + connectStatusLabel.setText(""); + connectStatusLabel.setText("{FADE=RED;RED;0.1}Incompatible Version"); + } else if (event.getResult() == ConnectionResult.InvalidSlot) { + connectStatusLabel.setText(""); + connectStatusLabel.setText("{FADE=RED;RED;0.1}Invalid Slot"); + } else if (event.getResult() == ConnectionResult.SlotAlreadyTaken) { + connectStatusLabel.setText(""); + connectStatusLabel.setText("{FADE=RED;RED;0.1}Slot Taken"); + } + } +} \ No newline at end of file diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/events/LocationInfo.java b/forge-gui-mobile/src/forge/adventure/archipelago/events/LocationInfo.java new file mode 100644 index 000000000000..3de6797e1fd8 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/events/LocationInfo.java @@ -0,0 +1,25 @@ +package forge.adventure.archipelago.events; + +import forge.adventure.archipelago.ArchipelagoClient; +import forge.adventure.archipelago.ArchipelagoRandomizer; +import io.github.archipelagomw.events.ArchipelagoEventListener; +import io.github.archipelagomw.events.LocationInfoEvent; + +public class LocationInfo { + + private final ArchipelagoClient APClient; + + public LocationInfo(ArchipelagoClient APClient) { + this.APClient = APClient; + } + + @ArchipelagoEventListener + public void onLocationInfo(LocationInfoEvent event) { + int locationCount = APClient.getLocationManager().getMissingLocations().size() + APClient.getLocationManager().getCheckedLocations().size(); + if (event.locations.size() == locationCount) { + ArchipelagoRandomizer.getInstance().handleShopData(event.locations, APClient.getLocationManager().getCheckedLocations()); + } else { + // Todo: Show hints in console, maybe handle this in PrintJsonListener later? + } + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/events/OnDeathLink.java b/forge-gui-mobile/src/forge/adventure/archipelago/events/OnDeathLink.java new file mode 100644 index 000000000000..b01b8442f70d --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/events/OnDeathLink.java @@ -0,0 +1,22 @@ +package forge.adventure.archipelago.events; + +import forge.adventure.archipelago.ArchipelagoClient; +import forge.adventure.archipelago.SlotData; +import io.github.archipelagomw.events.ArchipelagoEventListener; +import io.github.archipelagomw.events.DeathLinkEvent; + +public class OnDeathLink { + private final ArchipelagoClient APClient; + + public OnDeathLink(ArchipelagoClient APClient) { + this.APClient = APClient; + } + + @ArchipelagoEventListener + public void onDeath(DeathLinkEvent event) { + SlotData slotData = APClient.getSlotData(); + if (APClient.isConnected() && slotData != null && slotData.DeathLink == 1) { + // Todo: Handle deathlink + } + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/events/PrintJsonListener.java b/forge-gui-mobile/src/forge/adventure/archipelago/events/PrintJsonListener.java new file mode 100644 index 000000000000..db5e36486ade --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/events/PrintJsonListener.java @@ -0,0 +1,64 @@ +package forge.adventure.archipelago.events; + +import forge.adventure.archipelago.ArchipelagoClient; +import forge.adventure.archipelago.ArchipelagoColors; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoRandomizer; +import io.github.archipelagomw.Print.APPrintPart; +import io.github.archipelagomw.events.ArchipelagoEventListener; +import io.github.archipelagomw.events.PrintJSONEvent; + +public class PrintJsonListener { + private final ArchipelagoClient APClient; + + public PrintJsonListener(ArchipelagoClient APClient) { + this.APClient = APClient; + } + + @ArchipelagoEventListener + public void onPrintJson(PrintJSONEvent event) { + ArchipelagoRandomizer randomizer = ArchipelagoRandomizer.getInstance(); + switch (event.type) { + case Chat: + // Don't print chat messages originating from ourselves. + if (APClient != null && event.apPrint.slot == APClient.getSlot()) { + return; + } else { + // Todo: parse chat message + } + case ItemSend: + StringBuilder messageString = new StringBuilder(); + String itemColor = ArchipelagoColors.Cyan; + switch (event.item.flags){ + case 0b001: + itemColor = ArchipelagoColors.Plum; + break; + case 0b010: + itemColor = ArchipelagoColors.SlateBlue; + break; + case 0b100: + itemColor = ArchipelagoColors.Salmon; + break; + } + + for (APPrintPart part : event.apPrint.parts) { + switch (part.type) { + case playerID -> { + if (APClient != null && part.text.equals(APClient.getMyName())) { + messageString.append(ArchipelagoColors.Magenta).append(part.text).append("{RESET}"); + } else { + messageString.append(part.text); + } + } + case text -> messageString.append(part.text); + case itemID -> messageString.append(itemColor).append(part.text).append("{RESET}"); + case locationID -> messageString.append(ArchipelagoColors.Green).append(part.text).append("{RESET}"); + } + } + ArchipelagoData APData = ArchipelagoData.getInstance(); + APData.generateGameNotification(messageString.toString()); + case Hint: + // Todo: parse hint + } + } +} diff --git a/forge-gui-mobile/src/forge/adventure/archipelago/events/ReceiveItem.java b/forge-gui-mobile/src/forge/adventure/archipelago/events/ReceiveItem.java new file mode 100644 index 000000000000..73b370c3e2c5 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/events/ReceiveItem.java @@ -0,0 +1,62 @@ +package forge.adventure.archipelago.events; + +import forge.adventure.archipelago.Archipelago; +import forge.adventure.archipelago.ArchipelagoRandomizer; +import forge.adventure.archipelago.ItemRegistry; +import forge.adventure.archipelago.ArchipelagoData; +import io.github.archipelagomw.events.ArchipelagoEventListener; +import io.github.archipelagomw.events.ReceiveItemEvent; +import io.github.archipelagomw.parts.NetworkItem; + +public class ReceiveItem { + + @ArchipelagoEventListener + public void onReceiveItem(ReceiveItemEvent event) { + if (Archipelago.getInstance().isConnected()) { + ArchipelagoRandomizer APRandomizer = ArchipelagoRandomizer.getInstance(); + ArchipelagoData APData = ArchipelagoData.getInstance(); + NetworkItem item = event.getItem(); + if (event.getIndex() > APRandomizer.getLastArchipelagoRewardIndex()) { + switch ((int)item.itemID) { + case 1000: + APRandomizer.unlockGoldReward(750); + break; + case 1001: + APRandomizer.unlockGoldReward(1500); + break; + case 1002: + APRandomizer.unlockGoldReward(3000); + break; + case 1003: + APRandomizer.unlockManaCrystalReward(20); + break; + case 1004: + APRandomizer.unlockManaCrystalReward(30); + break; + case 1005: + APRandomizer.unlockManaCrystalReward(50); + break; + case 1009: + APData.unlockRandomSet(""); + break; + case 1010: + APRandomizer.unlockMaxLifeReward(1); + break; + case 1011: + APRandomizer.unlockMaxLifeReward(2); + break; + case 2000: + case 2001: + case 2002: + case 2003: + case 2004: + // Todo: implement color sanity + break; + default: + APRandomizer.unlockItemReward(ItemRegistry.getItem(item.itemID)); + } + APRandomizer.incrementLastArchipelagoRewardIndex(); + } + } + } +} \ No newline at end of file diff --git a/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java b/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java index cca0ced5f8c9..6990d7e13ce8 100644 --- a/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java +++ b/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java @@ -1,12 +1,19 @@ package forge.adventure.character; +import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Vector2; import forge.Forge; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; +import forge.adventure.data.BiomeData; import forge.adventure.player.AdventurePlayer; import forge.adventure.scene.Scene; import forge.adventure.stage.GameStage; +import forge.adventure.archipelago.ArchipelagoUtil; import forge.adventure.util.Config; import forge.adventure.util.Current; +import forge.adventure.world.World; +import forge.adventure.world.WorldSave; /** * Class that will represent the player sprite on the map @@ -14,8 +21,12 @@ public class PlayerSprite extends CharacterSprite { private final float playerSpeed; private final Vector2 direction = Vector2.Zero.cpy(); + private final Vector2 lastLegalPosition = new Vector2(); private float playerSpeedModifier = 1f; private float playerSpeedEquipmentModifier = 1f; + private float playerSpeedArchipelagoAdjustment = 0f; + private boolean showLockedRegionOverhead = false; + private String lastBlockedRegionName = null; GameStage gameStage; public PlayerSprite(GameStage gameStage) { @@ -29,6 +40,12 @@ public PlayerSprite(GameStage gameStage) { //Attach signals here. Current.player().onBlessing(() -> playerSpeedEquipmentModifier = Current.player().equipmentSpeed()); Current.player().onEquipmentChanged(() -> playerSpeedEquipmentModifier = Current.player().equipmentSpeed()); + + // Set initial last legal position + lastLegalPosition.set(Current.player().getWorldPosX(), Current.player().getWorldPosY()); + + // Check if archipelago mode is enabled + playerSpeedArchipelagoAdjustment += ArchipelagoData.getInstance().getArchipelagoMode() != ArchipelagoMode.disabled ? 0.15f : 0; } private void updatePlayer() { @@ -60,12 +77,30 @@ public void setMoveModifier(float speed) { playerSpeedModifier = speed; } + private boolean isInUnlockedBiome() { + World world = WorldSave.getCurrentSave().getWorld(); + + int tileX = (int)(getX() / world.getTileSize()); + int tileY = (int)(getY() / world.getTileSize()); + + int biomeId = World.highestBiome(world.getBiome(tileX, tileY)); + int maxBiomeIndex = world.getData().GetBiomes().size(); + if (maxBiomeIndex <= biomeId) { + // If the region is not one we recognize, we should assume that the player is allowed to be there. + return true; + } + BiomeData biome = world.getData().GetBiomes().get(biomeId); + lastBlockedRegionName = biome.name; + + return ArchipelagoData.getInstance().isRegionUnlocked(biome.name); + } + @Override public void act(float delta) { super.act(delta); if (Forge.advFreezePlayerControls) return; - direction.setLength(playerSpeed * delta * playerSpeedModifier*playerSpeedEquipmentModifier); + direction.setLength(playerSpeed * delta * playerSpeedModifier * (playerSpeedEquipmentModifier + playerSpeedArchipelagoAdjustment)); Vector2 previousDirection = getMovementDirection().cpy(); Scene previousScene = forge.Forge.getCurrentScene(); @@ -74,6 +109,15 @@ public void act(float delta) { direction.set(gameStage.adjustMovement(direction,boundingRect)); moveBy(direction.x, direction.y); + // Archipelago: Check if it's a legal biome for the player to be in, if not, refuse to update their position. + if (!isInUnlockedBiome()) { + setPosition(lastLegalPosition.x, lastLegalPosition.y); + showLockedRegionOverhead = true; + } else { + lastLegalPosition.set(getX(), getY()); + showLockedRegionOverhead = false; + } + // If the player is blocked by an obstacle, and they haven't changed scenes, // they will keep trying to move in that direction if (previousScene == forge.Forge.getCurrentScene()) { @@ -86,6 +130,21 @@ public boolean isMoving() { return !direction.isZero(); } + @Override + public void draw(Batch batch, float parentAlpha) { + super.draw(batch, parentAlpha); + + if (shouldShowLockedRegionOverhead()) { + ArchipelagoUtil.drawLockedRegionOverhead( + batch, + getLastBlockedRegionName(), + getX() + getWidth() / 2f, + getY() + getHeight(), + 0.9f + ); + } + } + public void stop() { direction.setZero(); setAnimation(AnimationTypes.Idle); @@ -94,4 +153,12 @@ public void stop() { public void setPosition(Vector2 oldPosition) { setPosition(oldPosition.x, oldPosition.y); } + + public String getLastBlockedRegionName() { + return lastBlockedRegionName; + } + + public boolean shouldShowLockedRegionOverhead() { + return showLockedRegionOverhead; + } } diff --git a/forge-gui-mobile/src/forge/adventure/character/PortalActor.java b/forge-gui-mobile/src/forge/adventure/character/PortalActor.java index 99d1dba680a9..71127da7e12c 100644 --- a/forge-gui-mobile/src/forge/adventure/character/PortalActor.java +++ b/forge-gui-mobile/src/forge/adventure/character/PortalActor.java @@ -3,9 +3,14 @@ import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.*; import com.badlogic.gdx.utils.Array; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; +import forge.adventure.archipelago.ArchipelagoRandomizer; +import forge.adventure.data.DialogData; import forge.adventure.scene.TileMapScene; import forge.adventure.stage.MapStage; import forge.adventure.util.Config; +import forge.adventure.util.MapDialog; import forge.adventure.util.Paths; import java.util.HashMap; @@ -38,6 +43,28 @@ public void onPlayerCollide() { //Activate portal? Launch Dialog? } if (currentAnimationType == PortalAnimationTypes.Active) { + if (ArchipelagoData.getInstance().getArchipelagoMode() == ArchipelagoMode.networked_archipelago && ArchipelagoRandomizer.getInstance().getSlotData() == null) { + // Check if the player has filled slotData, if not, tell them to connect to Archipelago before continuing. + DialogData dialog = new DialogData(); + dialog.text = + """ + You must first connect to the Archipelago server before continuing your adventure. + \s + Please open the game menu and look for the Archipelago button to set up your connection. + """; + + DialogData button = new DialogData(); + button.name = "Take me there"; + DialogData.ActionData action = new DialogData.ActionData(); + action.openArchipelagoSettings = true; + button.action = new DialogData.ActionData[] { action }; + dialog.options = new DialogData[] { button }; + MapDialog mapDialog = new MapDialog(dialog, stage, -1, null); + stage.showDialog(); + mapDialog.activate(); + return; + } + if (targetMap == null || targetMap.isEmpty()) { stage.exitDungeon(false, false); } else { diff --git a/forge-gui-mobile/src/forge/adventure/data/AdventureQuestData.java b/forge-gui-mobile/src/forge/adventure/data/AdventureQuestData.java index f036117871dd..a01d7f85d2f9 100644 --- a/forge-gui-mobile/src/forge/adventure/data/AdventureQuestData.java +++ b/forge-gui-mobile/src/forge/adventure/data/AdventureQuestData.java @@ -2,6 +2,7 @@ import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; +import forge.adventure.archipelago.ArchipelagoData; import forge.adventure.character.EnemySprite; import forge.adventure.pointofintrest.PointOfInterest; import forge.adventure.scene.GameScene; @@ -399,6 +400,10 @@ public void updateStages(AdventureQuestEvent event){ switch (stage.getStatus()) { case ACTIVE: done = stage.handleEvent(event) == COMPLETE && done; + // If complete is true for the final stage of the quest, update the completed quests in ArchipelagoData + if (stage == stages[stages.length - 1] && stages[stages.length - 1].getStatus() == COMPLETE) { + ArchipelagoData.getInstance().addCompletedQuests(event); + } break; case COMPLETE: continue; diff --git a/forge-gui-mobile/src/forge/adventure/data/DialogData.java b/forge-gui-mobile/src/forge/adventure/data/DialogData.java index 407ab3b7fa70..08a16b8b1fd4 100644 --- a/forge-gui-mobile/src/forge/adventure/data/DialogData.java +++ b/forge-gui-mobile/src/forge/adventure/data/DialogData.java @@ -82,6 +82,8 @@ static public class QuestFlag implements Serializable{ public int addMapReputation = 0; //Gives the player X reputation points in this POI. Negative to take. public String POIReference; //used with addMapReputation when a quest step affects reputation in another location + public boolean openArchipelagoSettings = false; + public ActionData(){} public ActionData(ActionData other){ diff --git a/forge-gui-mobile/src/forge/adventure/data/ItemData.java b/forge-gui-mobile/src/forge/adventure/data/ItemData.java index 30bda471f38d..48ae86ba0330 100644 --- a/forge-gui-mobile/src/forge/adventure/data/ItemData.java +++ b/forge-gui-mobile/src/forge/adventure/data/ItemData.java @@ -1,7 +1,9 @@ package forge.adventure.data; import com.badlogic.gdx.graphics.g2d.Sprite; +import forge.adventure.archipelago.Archipelago; import forge.adventure.util.Config; +import io.github.archipelagomw.parts.NetworkItem; import java.io.Serializable; import java.util.UUID; @@ -29,26 +31,80 @@ public class ItemData implements Serializable, Cloneable { public String commandOnUse; public int shardsNeeded; public DialogData dialogOnUse; + public long archipelagoItemId = -1; + public long archilepagoLocationId = -1; + public int archipelagoPlayerId = -1; + public int archipelagoFlags = 0; + public String archipelagoItemName = ""; + public String archipelagoLocationName = ""; + public String archipelagoPlayerName = ""; + public boolean archipelagoAlreadyChecked = false; + public ItemData() { - public ItemData() - { + } + + public ItemData(ItemData cpy) { + name = cpy.name; + equipmentSlot = cpy.equipmentSlot; + effect = new EffectData(cpy.effect); + description = cpy.description; + iconName = cpy.iconName; + questItem = cpy.questItem; + cost = cpy.cost; + usableInPoi = cpy.usableInPoi; + usableOnWorldMap = cpy.usableOnWorldMap; + commandOnUse = cpy.commandOnUse; + shardsNeeded = cpy.shardsNeeded; + dialogOnUse = cpy.dialogOnUse; + archipelagoItemId = cpy.archipelagoItemId; + archilepagoLocationId = cpy.archilepagoLocationId; + archipelagoPlayerId = cpy.archipelagoPlayerId; + archipelagoFlags = cpy.archipelagoFlags; + archipelagoItemName = cpy.archipelagoItemName; + archipelagoLocationName = cpy.archipelagoLocationName; + archipelagoPlayerName = cpy.archipelagoPlayerName; + } + public ItemData(NetworkItem networkItem) { + this(networkItem, false); } - public ItemData(ItemData cpy) - { - name = cpy.name; - equipmentSlot = cpy.equipmentSlot; - effect = new EffectData(cpy.effect); - description = cpy.description; - iconName = cpy.iconName; - questItem = cpy.questItem; - cost = cpy.cost; - usableInPoi = cpy.usableInPoi; - usableOnWorldMap = cpy.usableOnWorldMap; - commandOnUse = cpy.commandOnUse; - shardsNeeded = cpy.shardsNeeded; - dialogOnUse = cpy.dialogOnUse; + + public ItemData(NetworkItem networkItem, boolean alreadyChecked) { + String itemType = "Filler"; + switch (networkItem.flags){ + case 0b001: + itemType = "Progression"; + break; + case 0b010: + itemType = "Useful"; + break; + case 0b100: + itemType = "Trap"; + break; + } + + name = networkItem.itemName; + if (!itemType.isEmpty()) name += "\n" + itemType + " item for " + networkItem.playerName; + iconName = "APIconSmall"; + // Item name is recognised as a known item, show its information: + if (ItemListData.getItem(networkItem.itemName) != null) { + ItemData data = ItemListData.getItem(networkItem.itemName); + iconName = data.iconName; + equipmentSlot = data.equipmentSlot; + effect = data.effect; + description = data.description; + } + cost = Archipelago.getInstance().getRandomItemPrice(); + + archipelagoItemId = networkItem.itemID; + archilepagoLocationId = networkItem.locationID; + archipelagoPlayerId = networkItem.playerID; + archipelagoFlags = networkItem.flags; + archipelagoItemName = networkItem.itemName; + archipelagoLocationName = networkItem.locationName; + archipelagoPlayerName = networkItem.playerName; + archipelagoAlreadyChecked = alreadyChecked; } public Sprite sprite() { diff --git a/forge-gui-mobile/src/forge/adventure/data/WorldData.java b/forge-gui-mobile/src/forge/adventure/data/WorldData.java index 2753a851b5bd..b8e41185311a 100644 --- a/forge-gui-mobile/src/forge/adventure/data/WorldData.java +++ b/forge-gui-mobile/src/forge/adventure/data/WorldData.java @@ -38,6 +38,9 @@ public class WorldData implements Serializable { private static Array allEnemies; private static Array shopList; + public static void resetShopLists() { + shopList = null; + } public static Array getShopList() { if (shopList == null) { diff --git a/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java b/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java index c7cfba8e1bfe..619c746c70d6 100644 --- a/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java +++ b/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java @@ -9,6 +9,8 @@ import com.google.common.collect.Lists; import forge.Forge; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; import forge.adventure.data.*; import forge.adventure.pointofintrest.PointOfInterestChanges; import forge.adventure.scene.AdventureDeckEditor; @@ -157,7 +159,7 @@ static public AdventurePlayer current() { public final Set favoriteCards = new HashSet<>(); public void create(String n, Deck startingDeck, boolean male, int race, int avatar, boolean isFantasy, - boolean isUsingCustomDeck, DifficultyData difficultyData, AdventureModes adventureMode) { + boolean isUsingCustomDeck, DifficultyData difficultyData, AdventureModes adventureMode, ArchipelagoMode archipelagoMode) { clear(); this.adventureMode = adventureMode; announceFantasy = fantasyMode = isFantasy; //Set Chaos mode first. @@ -172,6 +174,10 @@ public void create(String n, Deck startingDeck, boolean male, int race, int avat decks.set(0, deck); cards.addAllFlat(deck.getAllCardsInASinglePool(true, true).toFlatList()); + ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); + for (PaperCard card : cards.toFlatList()) { + archipelagoData.addCardUnlockedByName(card.getCardName()); + } this.difficultyData.startingLife = difficultyData.startingLife; this.difficultyData.startingMoney = difficultyData.startingMoney; @@ -973,6 +979,7 @@ public void addCards(ItemPool cardPool) { } public void addReward(Reward reward) { + ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); switch (reward.getType()) { case Card: cards.add(reward.getCard()); @@ -981,17 +988,20 @@ public void addReward(Reward reward) { autoSellCards.add(reward.getCard()); refreshEditor(); } + archipelagoData.addCardByRarity(reward.getCard().getRarity().toString()); break; case Gold: addGold(reward.getCount()); break; case Item: - if (reward.getItem() != null) + if (reward.getItem() != null) { addItem(reward.getItem().name); + } break; case CardPack: if (reward.getDeck() != null) { boostersOwned.add(reward.getDeck()); + archipelagoData.addPack(reward.getDeck().getName()); } break; case Life: @@ -1011,6 +1021,7 @@ private void refreshEditor() { private void addGold(int goldCount) { gold += goldCount; + ArchipelagoData.getInstance().addGold(goldCount); onGoldChangeList.emit(); } @@ -1102,6 +1113,7 @@ public void win() { public void addMaxLife(int count) { maxLife += count; life += count; + ArchipelagoData.getInstance().addMaxLife(count); onLifeTotalChangeList.emit(); } @@ -1117,6 +1129,7 @@ public void takeGold(int price) { } public void addShards(int number) { + ArchipelagoData.getInstance().addShards(number); takeShards(-number); } @@ -1402,6 +1415,7 @@ public boolean addItem(String name, boolean updateEvent) { ItemData item = ItemListData.getItem(name); if (item == null) return false; + ArchipelagoData.getInstance().addItem(name); inventoryItems.add(item); if (updateEvent) AdventureQuestController.instance().updateItemReceived(item); diff --git a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java index 1aeea83d3e82..8c2d22fd0709 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java +++ b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java @@ -5,7 +5,9 @@ import com.badlogic.gdx.utils.Align; import forge.Forge; import forge.Graphics; +import forge.adventure.archipelago.ArchipelagoMode; import forge.adventure.data.AdventureEventData; +import forge.adventure.archipelago.ArchipelagoData; import forge.adventure.data.ItemData; import forge.adventure.player.AdventurePlayer; import forge.adventure.util.AdventureEventController; @@ -26,6 +28,7 @@ import forge.item.PaperCard; import forge.itemmanager.*; import forge.itemmanager.filters.CardColorFilter; +import forge.itemmanager.filters.CardLockFilter; import forge.itemmanager.filters.CardTypeFilter; import forge.menu.FDropDownMenu; import forge.menu.FMenuItem; @@ -90,6 +93,12 @@ protected DeckEditorPage[] getInitialPages() { public ItemPool getCardPool() { ItemPool pool = new ItemPool<>(PaperCard.class); pool.addAll(Current.player().getCards()); + ArchipelagoData apData = ArchipelagoData.getInstance(); + if (apData.getArchipelagoMode() != ArchipelagoMode.disabled) { + for (Map.Entry card : pool) { + card.getKey().setLocked(!apData.checkCardUnlocked(card.getKey())); + } + } return pool; } @@ -976,6 +985,7 @@ public AdventureCardManager() { protected void addDefaultFilters() { this.addFilter(new CardColorFilter(this)); this.addFilter(new CardTypeFilter(this)); + if (ArchipelagoData.getInstance().getArchipelagoMode() != ArchipelagoMode.disabled) this.addFilter(new CardLockFilter(this)); } @Override diff --git a/forge-gui-mobile/src/forge/adventure/scene/ArchipelagoSettingsScene.java b/forge-gui-mobile/src/forge/adventure/scene/ArchipelagoSettingsScene.java new file mode 100644 index 000000000000..5c726d23de24 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/scene/ArchipelagoSettingsScene.java @@ -0,0 +1,98 @@ +package forge.adventure.scene; + +import com.badlogic.gdx.scenes.scene2d.ui.*; +import com.github.tommyettinger.textra.TextraButton; +import com.github.tommyettinger.textra.TypingLabel; +import forge.Forge; +import forge.adventure.archipelago.Archipelago; + +/** + * Scene to handle settings of the archipelago client + */ +public class ArchipelagoSettingsScene extends UIScene { + + private TextraButton backButton; + private TypingLabel connectStatusLabel; + private TextField ipTextField; + private TextField portTextField; + private TextField slotNameTextField; + private TextField passwordTextField; + + private ArchipelagoSettingsScene() { + super("ui/apsettings.json"); + + backButton = ui.findActor("return"); + ipTextField = ui.findActor("apIP"); + portTextField = ui.findActor("apPort"); + slotNameTextField = ui.findActor("apSlotName"); + passwordTextField = ui.findActor("apPassword"); + + connectStatusLabel = ui.findActor("connectStatusLabel"); + connectStatusLabel.setText("{FADE=RED;RED;0.1}Not Connected..."); + ui.onButtonPress("return", ArchipelagoSettingsScene.this::back); + ui.onButtonPress("connect", ArchipelagoSettingsScene.this::connect); + } + + + public boolean back() { + Forge.switchToLast(); + return true; + } + + public void connect() { + Archipelago.getInstance().connect(ipTextField.getText(), portTextField.getText(), slotNameTextField.getText(), passwordTextField.getText(), connectStatusLabel); + } + + + private static ArchipelagoSettingsScene object; + + public static ArchipelagoSettingsScene instance() { + if (object == null) + object = new ArchipelagoSettingsScene(); + return object; + } + + public void setConnectStatusLabel(String string) { + connectStatusLabel.setText(""); + connectStatusLabel.setText(string); + } + + public void setIpTextField(String string) { + ipTextField.setText(string); + } + + public String getIpTextField() { + return ipTextField.getText(); + } + + public void setPortTextField(String string) { + portTextField.setText(string); + } + + public String getPortTextField() { + return portTextField.getText(); + } + + public void setSlotNameTextField(String string) { + slotNameTextField.setText(string); + } + + public String getSlotNameTextField() { + return slotNameTextField.getText(); + } + + public void setPasswordTextField(String string) { + passwordTextField.setText(string); + } + + public String getPasswordTextField() { + return passwordTextField.getText(); + } + + @Override + public void dispose() { + if (stage != null) + stage.dispose(); + } + +} diff --git a/forge-gui-mobile/src/forge/adventure/scene/ArchipelagoTrackerScene.java b/forge-gui-mobile/src/forge/adventure/scene/ArchipelagoTrackerScene.java new file mode 100644 index 000000000000..fb0c6b7c0e45 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/scene/ArchipelagoTrackerScene.java @@ -0,0 +1,41 @@ +package forge.adventure.scene; + +import com.github.tommyettinger.textra.TextraButton; +import forge.Forge; + +/** + * Scene to show the current archipelago progress in Forge + */ +public class ArchipelagoTrackerScene extends UIScene { + + TextraButton backButton; + + private ArchipelagoTrackerScene() { + super("ui/aptracker.json"); + + backButton = ui.findActor("return"); + ui.onButtonPress("return", ArchipelagoTrackerScene.this::back); + } + + + public boolean back() { + Forge.switchToLast(); + return true; + } + + private static ArchipelagoTrackerScene object; + + public static ArchipelagoTrackerScene instance() { + if (object == null) + object = new ArchipelagoTrackerScene(); + return object; + } + + + @Override + public void dispose() { + if (stage != null) + stage.dispose(); + } + +} diff --git a/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java b/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java index 7bf5fc6f1ae5..a964c1162f8b 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java @@ -158,6 +158,7 @@ private void startArena() { @Override public void setWinner(boolean winner, boolean isArena) { + IAfterMatch.super.setWinner(winner, isArena); enable = false; Array winners = new Array<>(); Array winnersEnemies = new Array<>(); diff --git a/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java b/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java index d2dfad086c0c..0e98ec3e813f 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java @@ -9,6 +9,8 @@ import com.github.tommyettinger.textra.TextraButton; import com.github.tommyettinger.textra.TextraLabel; import forge.Forge; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; import forge.adventure.player.AdventurePlayer; import forge.adventure.stage.GameHUD; import forge.adventure.util.Controls; @@ -221,6 +223,30 @@ public boolean select(int slot) { } + @Override + public boolean back() { + // Check active deck here for locked cards, show error dialog if not all cards are unlocked. + ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); + if (archipelagoData.getArchipelagoMode() != ArchipelagoMode.disabled && !archipelagoData.checkDeckUnlocked(Current.player().getSelectedDeck())) { + // Todo: Localize deckNotFullyUnlockedDialog text + Dialog deckNotFullyUnlockedDialog = createGenericDialog("Deck contains locked cards", null, + Forge.getLocalizer().getMessage("lblOK"), + null, + this::removeDialog, + this::removeDialog); + // Add actual message content + deckNotFullyUnlockedDialog.getContentTable().add( + Controls.newLabel( + "The selected deck contains locked cards.\nPlease select a deck containing only unlocked cards." + ) + ).colspan(2).padTop(12).padBottom(8); + showDialog(deckNotFullyUnlockedDialog); + return false; + } + + return super.back(); + } + @Override public void enter() { refreshDeckButtons(); diff --git a/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java b/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java index bf626731e13b..0787a8b1593a 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java @@ -7,6 +7,8 @@ import forge.Forge; import forge.Graphics; import forge.LobbyPlayer; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; import forge.card.CardRenderer; import forge.card.CardRenderer.CardStackPosition; import forge.card.CardZoom; @@ -158,10 +160,20 @@ public void GameEnd() { boolean isBossLoss = (chaosBattle || showMessages) && !finalWinner; boolean hasAnteResults = !anteWonCards.isEmpty() || !anteLostCards.isEmpty(); - // No popups needed, preserve original behavior - if (!hasAnteResults && !isBossLoss) { - afterGameEnd(enemyName, finalWinner); - return; + if (!isBossLoss) { + if (enemy.getData().boss && ArchipelagoData.getInstance().getArchipelagoMode() != ArchipelagoMode.disabled) { + // Check if the enemy's deck is stored in a path containing "miniboss" or just "boss". + if (enemy.getData().deck[0].toLowerCase().contains("miniboss")) { + ArchipelagoData.getInstance().addMiniBossDefeated(enemyName); + } else { + ArchipelagoData.getInstance().addBossDefeated(enemyName); + } + } + // No popups needed, preserve original behavior + if (!hasAnteResults) { + afterGameEnd(enemyName, finalWinner); + return; + } } // Build popup chain: ante results -> boss dialogue -> exit diff --git a/forge-gui-mobile/src/forge/adventure/scene/EventScene.java b/forge-gui-mobile/src/forge/adventure/scene/EventScene.java index d21140945de0..2e642451d40e 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/EventScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/EventScene.java @@ -16,6 +16,7 @@ import forge.Forge; import forge.adventure.character.EnemySprite; import forge.adventure.data.AdventureEventData; +import forge.adventure.archipelago.ArchipelagoData; import forge.adventure.data.DialogData; import forge.adventure.player.AdventurePlayer; import forge.adventure.pointofintrest.PointOfInterestChanges; @@ -536,6 +537,7 @@ public void startRound() { } public void setWinner(boolean winner, boolean isArena) { + IAfterMatch.super.setWinner(winner, isArena); if (winner) { humanMatch.winner = humanMatch.p1; humanMatch.p1.wins++; @@ -571,6 +573,7 @@ public void finishRound() { public void finishEvent() { currentEvent.eventStatus = AdventureEventController.EventStatus.Completed; + ArchipelagoData.getInstance().addCompletedTownInnEvents(); } public void loadMetaDraft() { diff --git a/forge-gui-mobile/src/forge/adventure/scene/HudScene.java b/forge-gui-mobile/src/forge/adventure/scene/HudScene.java index f6fd47843608..cdbe74457d80 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/HudScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/HudScene.java @@ -184,6 +184,7 @@ public boolean axisMoved(Controller controller, int axisIndex, float value) { @Override public void setWinner(boolean winner, boolean isArena) { + IAfterMatch.super.setWinner(winner, isArena); stage.setWinner(winner, isArena); } diff --git a/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java b/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java index 95b008c982e9..295c96a5ceaa 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java @@ -265,7 +265,7 @@ private void triggerUse() { return; ItemData data = itemLocation.get(selected).getRight(); if (data == null) return; - Current.player().addShards(-data.shardsNeeded); + Current.player().takeShards(data.shardsNeeded); done(); if (data.commandOnUse != null && !data.commandOnUse.isEmpty()) ConsoleCommandInterpreter.getInstance().command(data.commandOnUse); diff --git a/forge-gui-mobile/src/forge/adventure/scene/NewGameScene.java b/forge-gui-mobile/src/forge/adventure/scene/NewGameScene.java index 872692bcab71..7f989699755d 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/NewGameScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/NewGameScene.java @@ -2,9 +2,7 @@ import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; -import com.badlogic.gdx.scenes.scene2d.ui.Image; -import com.badlogic.gdx.scenes.scene2d.ui.ImageButton; -import com.badlogic.gdx.scenes.scene2d.ui.TextField; +import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; @@ -12,9 +10,11 @@ import com.github.tommyettinger.textra.TextraLabel; import forge.Forge; +import forge.adventure.archipelago.ArchipelagoMode; import forge.adventure.data.DialogData; import forge.adventure.data.DifficultyData; import forge.adventure.data.HeroListData; +import forge.adventure.data.WorldData; import forge.adventure.player.AdventurePlayer; import forge.adventure.stage.WorldStage; import forge.adventure.util.*; @@ -29,6 +29,7 @@ import forge.sound.SoundSystem; import forge.util.NameGenerator; +import java.util.Objects; import java.util.Random; /** @@ -39,6 +40,8 @@ public class NewGameScene extends MenuScene { TextField selectedName; ColorSet[] colorIds; CardEdition[] editionIds; + ScrollPane scrollPane; + private final Table settingGroup; private final Image avatarImage; private int avatarIndex = 0; private final Selector race; @@ -47,21 +50,35 @@ public class NewGameScene extends MenuScene { private final Selector mode; private final Selector difficulty; private final Selector starterEdition; - private final TextraLabel starterEditionLabel; + private final Selector enableArchipelago; + private boolean isArchipelagoSupported = false; private final Array custom; + private final TextraLabel starterEditionLabel; + private final TextraLabel raceLabel; + private final TextraLabel genderLabel; + private final TextraLabel difficultyLabel; private final TextraLabel colorLabel; + private final TextraLabel modeLabel; + private final TextraLabel enableArchipelagoLabel; private final ImageButton difficultyHelp; private DialogData difficultySummary; private final ImageButton modeHelp; private DialogData modeSummary; + private final ImageButton archipelagoHelp; + private DialogData archipelagoSummary; private final Random rand = new Random(); private String originalEditionLabelText; private Array originalEditionNames; private final Array modes = new Array<>(); + private final Array archipelagoModes = new Array<>(); + + private boolean isShandalar; private NewGameScene() { super(Forge.isLandscapeMode() ? "ui/new_game.json" : "ui/new_game_portrait.json"); + + settingGroup = new Table(); gender = ui.findActor("gender"); selectedName = ui.findActor("nameField"); generateName(); @@ -109,6 +126,15 @@ private NewGameScene() { break; } + isShandalar = Objects.equals( + Config.instance().getSettingData().plane, + "Shandalar" + ); + + enableArchipelago = ui.findActor("enableArchipelago"); + enableArchipelagoLabel = ui.findActor("enableArchipelagoL"); + archipelagoHelp = ui.findActor("archipelagoHelp"); + starterEdition = ui.findActor("starterEdition"); starterEditionLabel = ui.findActor("starterEditionL"); originalEditionLabelText = starterEditionLabel.storedText; @@ -233,6 +259,50 @@ public void clicked(InputEvent e, float x, float y) { showModeHelp(); } }); + + raceLabel = ui.findActor("raceL"); + genderLabel = ui.findActor("genderL"); + difficultyLabel = ui.findActor("difficultyL"); + modeLabel = ui.findActor("modeL"); + + addSelectorToScrollGroup(raceLabel, race, null); + addSelectorToScrollGroup(genderLabel, gender, null); + addSelectorToScrollGroup(difficultyLabel, difficulty, difficultyHelp); + addSelectorToScrollGroup(colorLabel, colorId, null); + + if (enableArchipelago != null) { + isArchipelagoSupported = true; + enableArchipelago.setTextList(new String[]{"Disabled", "Enabled", "Archipelago"}); + archipelagoHelp.addListener(new ClickListener() { + public void clicked(InputEvent e, float x, float y) { + archipelagoHelp(); + } + }); + if (isShandalar) { + addSelectorToScrollGroup(enableArchipelagoLabel, enableArchipelago, archipelagoHelp); + } + enableArchipelago.setVisible(isShandalar); + enableArchipelagoLabel.setVisible(isShandalar); + archipelagoHelp.setVisible(isShandalar); + } + + addSelectorToScrollGroup(modeLabel, mode, modeHelp); + addSelectorToScrollGroup(starterEditionLabel, starterEdition, null); + + scrollPane = ui.findActor("selectorScroll"); + scrollPane.setActor(settingGroup); + addToSelectable(settingGroup); + } + + void addSelectorToScrollGroup(TextraLabel label, Selector selector, ImageButton helpLabel) { + settingGroup.row(); + settingGroup.add(label).left().padLeft(19); + if (helpLabel != null) { + settingGroup.add(helpLabel).padRight(10); + } else { + settingGroup.add().width(16); + } + settingGroup.add(selector).expandX().right().fillX().padRight(40); } // class field @@ -330,9 +400,21 @@ public boolean start() { if (selectedName.getText().isEmpty()) { generateName(); } + ArchipelagoMode archipelagoMode; + if (isArchipelagoSupported) { + if (isShandalar && enableArchipelago.getCurrentIndex() < ArchipelagoMode.values().length && enableArchipelago.getCurrentIndex() > -1) { + archipelagoMode = ArchipelagoMode.values()[enableArchipelago.getCurrentIndex()]; + } else { + archipelagoMode = ArchipelagoMode.disabled; + } + } else { + archipelagoMode = ArchipelagoMode.disabled; + } + Runnable runnable = () -> { started = false; //FModel.getPreferences().setPref(ForgePreferences.FPref.UI_ENABLE_MUSIC, false); + WorldData.resetShopLists(); WorldSave.generateNewWorld(selectedName.getText(), gender.getCurrentIndex() == 0, race.getCurrentIndex(), @@ -340,13 +422,16 @@ public boolean start() { getStartingColor(), Config.instance().getConfigData().difficulties[difficulty.getCurrentIndex()], modes.get(mode.getCurrentIndex()), colorId.getCurrentIndex(), - getStartingEdition(), 0); + getStartingEdition(), 0, archipelagoMode); GamePlayerUtil.getGuiPlayer().setName(selectedName.getText()); SoundSystem.instance.changeBackgroundTrack(); WorldStage.getInstance().enterSpawnPOI(); if (AdventurePlayer.current().getQuests().stream().noneMatch(q -> q.getID() == 28)) { AdventurePlayer.current().addQuest("28", true); //Temporary link to Shandalar main questline } + if (archipelagoMode != ArchipelagoMode.disabled) { + AdventurePlayer.current().removeItem("Leather Boots"); + } Forge.switchScene(GameScene.instance()); }; Forge.setTransitionScreen(new TransitionScreen(runnable, null, false, true, Forge.getLocalizer().getMessage("lblGeneratingWorld"))); @@ -454,6 +539,36 @@ private void showDifficultyHelp() { loadDialog(difficultySummary); } + private void archipelagoHelp() { + + ArchipelagoMode selectedMode = ArchipelagoMode.values()[enableArchipelago.getCurrentIndex()]; + + archipelagoSummary = new DialogData(); + archipelagoSummary.name = "Summary"; + + StringBuilder summaryText = new StringBuilder(); + switch (selectedMode) { + case disabled: + summaryText.append("Randomizer: Disabled\n\nNo changes, the regular Adventure mode experience.\n\n"); + break; + case solo_randomizer: + summaryText.append("Randomizer: Solo Randomizer\n\nAll cards outside of your starting deck will be locked by default. Biomes, cards and equipment unlocks are randomized and can be unlocked by completing various objectives in the game.\nThe Leather Boots in your starting equipment are removed regardless of difficulty, all entities' base speed are increased to compensate.\n\n"); + break; + case networked_archipelago: + summaryText.append("Randomizer: Networked Archipelago\n\nAs 'Solo Randomizer' except that unlocks will be distributed through an online Archipelago server. More info at https://multiworld.gg/\nPlease ensure your Archipelago client is configured and connected.\n\n"); + break; + default: + summaryText.append("No summary available for this randomizer."); + break; + } + + DialogData dismiss = new DialogData(); + dismiss.name = "OK"; + archipelagoSummary.text = summaryText.toString(); + archipelagoSummary.options = new DialogData[1]; + archipelagoSummary.options[0] = dismiss; + loadDialog(archipelagoSummary); + } private void showModeHelp() { AdventureModes selectedMode = modes.get(mode.getCurrentIndex()); diff --git a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java index af81388d7924..96d19889f28b 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java @@ -13,9 +13,11 @@ import com.github.tommyettinger.textra.TextraLabel; import com.github.tommyettinger.textra.TypingLabel; import forge.Forge; +import forge.adventure.archipelago.*; import forge.adventure.character.ShopActor; import forge.haptic.HapticEngine; import forge.localinstance.properties.ForgePreferences.FPref; +import forge.adventure.data.ItemData; import forge.adventure.data.RewardData; import forge.adventure.data.ShopData; import forge.adventure.player.AdventurePlayer; @@ -321,6 +323,7 @@ void updateCollectionPool() { public void loadRewards(Array newRewards, Type type, ShopActor shopActor) { // Merge Gold and Shards rewards into single entries + ArchipelagoMode archipelagoMode = ArchipelagoData.getInstance().getArchipelagoMode(); int totalGold = 0; int totalShards = 0; Array others = new Array<>(); @@ -339,6 +342,9 @@ public void loadRewards(Array newRewards, Type type, ShopActor shopActor } newRewards.clear(); if (totalGold > 0) { + if (archipelagoMode == ArchipelagoMode.networked_archipelago && ArchipelagoRandomizer.getInstance().getSlotData() != null) { + totalGold = Math.round(totalGold * (ArchipelagoRandomizer.getInstance().getSlotData().GoldMultiplierPercentage / 100f)); + } newRewards.add(new Reward(Reward.Type.Gold, totalGold)); } if (totalShards > 0) { @@ -534,7 +540,31 @@ else if (fW / fH >= 2f) lastRowXAdjust = ((numberOfColumns * cardWidth) - (lastRowCount * cardWidth)) / 2; } - RewardActor actor = new RewardActor(reward, type == Type.Loot || type == Type.QuestReward, type, type == Type.Shop && (numberOfRows > 2 || numberOfColumns > 2)); + RewardActor actor; + + if (archipelagoMode != ArchipelagoMode.disabled) { + boolean itemAlreadySold = false; + if (type == Type.Loot && reward.getType() == Reward.Type.Item && reward.getItem().equipmentSlot != null && !reward.getItem().equipmentSlot.isEmpty()) { + if (archipelagoMode == ArchipelagoMode.solo_randomizer) { + reward = LocalRandomizer.getInstance().takeSingleEquipmentOutOfRemainingPool(); + } else { + ItemData itemData = new ItemData(); + itemData.iconName = "APIconSmall"; + itemData.name = "Archipelago Reward"; + reward = new Reward(itemData); + } + } else if (archipelagoMode == ArchipelagoMode.networked_archipelago && type == Type.Shop && (shopActor.getName().toLowerCase().contains("equipment") || shopActor.getName().toLowerCase().contains("items"))) { + // Prevent the game from removing the shop entry so we can show it as sold. + skipCard = false; + itemAlreadySold = reward.getItem().archipelagoAlreadyChecked; + } + actor = new RewardActor(reward, type == Type.Loot || type == Type.QuestReward, type, type == Type.Shop && (numberOfRows > 2 || numberOfColumns > 2)); + if (itemAlreadySold) { + actor.sold(); + } + } else { + actor = new RewardActor(reward, type == Type.Loot || type == Type.QuestReward, type, type == Type.Shop && (numberOfRows > 2 || numberOfColumns > 2)); + } actor.setBounds(lastRowXAdjust + xOff + cardWidth * (i % numberOfColumns) + spacing, yOff + cardHeight * currentRow + spacing, cardWidth - spacing * 2, cardHeight - spacing * 2); @@ -626,6 +656,13 @@ public BuyButton(int id, int i, RewardActor actor, Reward reward, TextraButton s price *= shopModifier; setText("[+GoldCoin] " + price); updateOwned(); + if (rewardActor.isSold()) { + isSold = true; + setDisabled(true); + getColor().a = 0.5f; + updateBuyButtons(); + return; + } addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { @@ -634,7 +671,13 @@ public void clicked(InputEvent event, float x, float y) { changes.buyCard(objectID, index); Current.player().takeGold(price); - Current.player().addReward(rewardActor.getReward()); + if (ArchipelagoData.getInstance().getArchipelagoMode() == ArchipelagoMode.networked_archipelago && rewardActor.getReward().getItem() != null && rewardActor.getReward().getItem().archilepagoLocationId >= 0) { + ItemData itemData = rewardActor.getReward().getItem(); + Archipelago.getInstance().checkLocation(itemData.archilepagoLocationId); + itemData.archipelagoAlreadyChecked = true; + } else { + Current.player().addReward(rewardActor.getReward()); + } HapticEngine.vibrate(FPref.UI_VIBRATE_ON_SHOP_ACTION, 5); SoundSystem.instance.play(SoundEffectType.FlipCoin, false); diff --git a/forge-gui-mobile/src/forge/adventure/scene/StartScene.java b/forge-gui-mobile/src/forge/adventure/scene/StartScene.java index 4ded210a4789..52d9f123cab7 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/StartScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/StartScene.java @@ -6,6 +6,9 @@ import com.github.tommyettinger.textra.TextraButton; import com.github.tommyettinger.textra.TypingLabel; import forge.Forge; +import forge.adventure.archipelago.Archipelago; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; import forge.adventure.stage.GameHUD; import forge.adventure.stage.GameStage; import forge.adventure.stage.MapStage; @@ -27,8 +30,9 @@ */ public class StartScene extends UIScene { private static StartScene object; + private boolean isArchipelagoSupported = false; Dialog exitDialog, backupDialog, zipDialog, unzipDialog; - TextraButton saveButton, resumeButton, continueButton; + TextraButton saveButton, resumeButton, continueButton, settingsButton, dataButton, apSettingsButton; TypingLabel version = Controls.newTypingLabel("{GRADIENT}[%80]v." + Forge.getDeviceAdapter().getVersionString() + "{ENDGRADIENT}"); @@ -44,17 +48,26 @@ public StartScene() { ui.onButtonPress("Backup", StartScene.this::backup); ui.onButtonPress("Exit", StartScene.this::Exit); ui.onButtonPress("Switch", StartScene.this::switchToClassic); - + ui.onButtonPress("ApSettings", StartScene.this::apSettings); saveButton = ui.findActor("Save"); resumeButton = ui.findActor("Resume"); continueButton = ui.findActor("Continue"); + settingsButton = ui.findActor("Settings"); + dataButton = ui.findActor("Backup"); + apSettingsButton = ui.findActor("ApSettings"); saveButton.setVisible(false); resumeButton.setVisible(false); version.setHeight(5); version.skipToTheEnd(); ui.addActor(version); + + if (apSettingsButton != null) { + isArchipelagoSupported = true; + apSettingsButton.pad(0, 0, 0, 2.5f); + updateSettingRow(); + } } public static StartScene instance() { @@ -229,6 +242,12 @@ public void switchToClassic() { Forge.switchToClassic(); } + public boolean apSettings() { + ArchipelagoSettingsScene.instance().setConnectStatusLabel(Archipelago.getInstance().isConnected() ? "{FADE=GREEN;GREEN;0.1}Connected!" : "{FADE=RED;RED;0.1}Not Connected..."); + Forge.switchScene(ArchipelagoSettingsScene.instance()); + return true; + } + public void updateResumeContinue() { boolean hasResumeButton = WorldSave.getCurrentSave().getWorld().getData() != null; resumeButton.setVisible(hasResumeButton); @@ -245,6 +264,36 @@ public void updateResumeContinue() { } } + private void updateSettingRow() { + boolean isArchipelagoNetworked = ArchipelagoData.getInstance().getArchipelagoMode() == ArchipelagoMode.networked_archipelago; + + if (isArchipelagoNetworked && Forge.isLandscapeMode()) { + apSettingsButton.setVisible(true); + settingsButton.setX(190); + settingsButton.setWidth(70); + dataButton.setX(260); + dataButton.setWidth(60); + } else if (!isArchipelagoNetworked && Forge.isLandscapeMode()) { + apSettingsButton.setVisible(false); + settingsButton.setX(160); + settingsButton.setWidth(80); + dataButton.setX(240); + dataButton.setWidth(80); + } else if (isArchipelagoNetworked) { + apSettingsButton.setVisible(true); + settingsButton.setX(54); + settingsButton.setWidth(100); + dataButton.setX(154); + dataButton.setWidth(100); + } else { + apSettingsButton.setVisible(false); + settingsButton.setX(16); + settingsButton.setWidth(119); + dataButton.setX(135); + dataButton.setWidth(119); + } + } + @Override public void enter() { boolean hasSaveButton = WorldSave.getCurrentSave().getWorld().getData() != null; @@ -255,6 +304,9 @@ public void enter() { saveButton.setVisible(hasSaveButton); saveButton.setDisabled(TileMapScene.instance().currentMap().isInMap()); updateResumeContinue(); + if (isArchipelagoSupported) { + updateSettingRow(); + } FSkinTexture.invalidateAdventureTextures(); GuiBase.setAdventureDirectory(Config.instance().getPrefix()); diff --git a/forge-gui-mobile/src/forge/adventure/stage/Console.java b/forge-gui-mobile/src/forge/adventure/stage/Console.java index 5bc67052d157..03823d7d364a 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/Console.java +++ b/forge-gui-mobile/src/forge/adventure/stage/Console.java @@ -18,11 +18,13 @@ public class Console extends Window { public void toggle() { if (isVisible()) { + Forge.advFreezePlayerControls = false; setVisible(false); getStage().unfocus(input); Gdx.input.setOnscreenKeyboardVisible(false); } else { if (!Forge.advFreezePlayerControls) { + Forge.advFreezePlayerControls = true; setVisible(true); getStage().setKeyboardFocus(input); } @@ -58,7 +60,6 @@ public boolean keyUp(InputEvent event, int keycode) { textField.setCursorPosition(Integer.MAX_VALUE); } else { index = 0; - textField.setText(commands.get(index)); textField.setCursorPosition(Integer.MAX_VALUE); } } else if (!commands.isEmpty()) { @@ -78,7 +79,7 @@ public boolean keyUp(InputEvent event, int keycode) { textField.setCursorPosition(Integer.MAX_VALUE); } else { index = commands.size - 1; - textField.setText(commands.get(index)); + textField.setText(""); textField.setCursorPosition(Integer.MAX_VALUE); } } else if (!commands.isEmpty()) { diff --git a/forge-gui-mobile/src/forge/adventure/stage/ConsoleCommandInterpreter.java b/forge-gui-mobile/src/forge/adventure/stage/ConsoleCommandInterpreter.java index 73a433a9d32d..2d8e0497a740 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/ConsoleCommandInterpreter.java +++ b/forge-gui-mobile/src/forge/adventure/stage/ConsoleCommandInterpreter.java @@ -5,6 +5,9 @@ import com.badlogic.gdx.utils.Array; import forge.Forge; import forge.StaticData; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; +import forge.adventure.archipelago.ArchipelagoUtil; import forge.adventure.character.PlayerSprite; import forge.adventure.data.*; import forge.adventure.pointofintrest.PointOfInterest; @@ -95,6 +98,11 @@ public String command(String text) { return "Command not found. Available commands:\n" + String.join(" ", Arrays.copyOfRange(words, 0, i)) + "\n" + String.join("\n", currentCommand.children.keySet()); } String[] parameters = Arrays.copyOfRange(words, i, words.length); + if (ArchipelagoData.getInstance().getArchipelagoMode() != ArchipelagoMode.disabled) { + if (text.toLowerCase().contains("teleport to poi")) { + ArchipelagoUtil.updateLastTraversedRegionOnTeleport(text); + } + } // this removes apostrophe... /*for (int j = 0; j < parameters.length; j++) parameters[j] = parameters[j].replaceAll("[\"']", "");*/ diff --git a/forge-gui-mobile/src/forge/adventure/stage/GameHUD.java b/forge-gui-mobile/src/forge/adventure/stage/GameHUD.java index 80d21a4f209f..658fec71eb04 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/GameHUD.java +++ b/forge-gui-mobile/src/forge/adventure/stage/GameHUD.java @@ -30,7 +30,6 @@ import com.badlogic.gdx.utils.Timer; import com.badlogic.gdx.utils.viewport.ScalingViewport; import com.github.tommyettinger.textra.TextraButton; -import com.github.tommyettinger.textra.TextraLabel; import com.github.tommyettinger.textra.TypingLabel; import java.util.EnumSet; @@ -41,13 +40,7 @@ import forge.adventure.data.AdventureQuestData; import forge.adventure.data.ItemData; import forge.adventure.player.AdventurePlayer; -import forge.adventure.scene.DeckSelectScene; -import forge.adventure.scene.GameScene; -import forge.adventure.scene.InventoryScene; -import forge.adventure.scene.MapViewScene; -import forge.adventure.scene.QuestLogScene; -import forge.adventure.scene.Scene; -import forge.adventure.scene.TileMapScene; +import forge.adventure.scene.*; import forge.adventure.util.AdventureQuestController; import forge.adventure.util.Config; import forge.adventure.util.Controls; @@ -73,12 +66,12 @@ public class GameHUD extends Stage { private final TypingLabel lifePoints; private final TypingLabel money; private final TypingLabel shards; + private final TypingLabel notificationText = Controls.newTypingLabel(""); private final TypingLabel enemyCounterText; private final Image enemyCounterBackground; - private final TextraLabel notificationText = Controls.newTextraLabel(""); private final Image miniMap, gamehud, mapborder, avatarborder, blank; private final InputEvent eventTouchDown, eventTouchUp; - private final TextraButton deckActor, openMapActor, menuActor, logbookActor, inventoryActor, exitToWorldMapActor, bookmarkActor; + private final TextraButton deckActor, openMapActor, menuActor, logbookActor, inventoryActor, exitToWorldMapActor, bookmarkActor, apSettingsActor; public final UIActor ui; private final Touchpad touchpad; private final Console console; @@ -125,7 +118,10 @@ private GameHUD(GameStage gameStage) { gamehud = ui.findActor("gamehud"); exitToWorldMapActor = ui.findActor("exittoworldmap"); bookmarkActor = ui.findActor("bookmark"); + apSettingsActor = ui.findActor("aptracker"); dialog = Controls.newDialog(""); + apSettingsActor.pad(0, 0, 0, 2); + apSettingsActor.setVisible(false); miniMapPlayer = new Image(Forge.getAssets().getTexture(Config.instance().getFile("ui/minimap_player.png"))); //create touchpad @@ -157,6 +153,7 @@ public void changed(ChangeEvent changeEvent, Actor actor) { ui.onButtonPress("deck", this::openDeck); ui.onButtonPress("exittoworldmap", this::exitToWorldMap); ui.onButtonPress("bookmark", this::bookmark); + ui.onButtonPress("aptracker", this::openApTracker); keyCollection = ui.findActor("keyCollection"); keyCollection.skipToTheEnd(); lifePoints = ui.findActor("lifePoints"); @@ -246,6 +243,7 @@ public void changed(ChangeEvent changeEvent, Actor actor) { menuGroup.addActor(inventoryActor); menuGroup.addActor(exitToWorldMapActor); menuGroup.addActor(bookmarkActor); + menuGroup.addActor(apSettingsActor); ui.addActor(menuGroup); //AVATAR avatarGroup.addActor(avatar); @@ -338,6 +336,7 @@ public boolean touchDown(int screenX, int screenY, int pointer, int button) { && !(Controls.actorContainsVector(openMapActor, touch)) //not inside openmap button && !(Controls.actorContainsVector(logbookActor, touch)) //not inside stats button && !(Controls.actorContainsVector(inventoryActor, touch)) //not inside inventory button + && !(Controls.actorContainsVector(apSettingsActor, touch)) //not inside apSettings button && !(Controls.actorContainsVector(exitToWorldMapActor, touch)) //not inside exit button && !(Controls.actorContainsVector(bookmarkActor, touch)) //not inside bookmark button && !(Controls.actorContainsVector(abilityButtonMap, touch)) //not inside abilityButtonMap @@ -581,7 +580,7 @@ void setAbilityButton(ItemData data) { return; if (data.shardsNeeded > Current.player().getShards()) return; - Current.player().addShards(-data.shardsNeeded); + Current.player().takeShards(data.shardsNeeded); ConsoleCommandInterpreter.getInstance().command(data.commandOnUse); AdventureQuestController.instance().updateItemUsed(data); }, "menu"); @@ -783,6 +782,14 @@ private void bookmark() { } } + private void openApTracker() { + if (console.isVisible()) + return; + if (Forge.advFreezePlayerControls) + return; + Forge.switchScene(ArchipelagoTrackerScene.instance()); + } + private void updateBookmarkActor(boolean value) { if (bookmarkActor == null) return; @@ -1093,13 +1100,13 @@ void restorePlayerCollision() { flicker(WorldStage.getInstance().getPlayerSprite()); } - public void addNotification(String text) { + public void addNotification(String text, float moveDuration, float delayDuration, float alphaDuration) { Action preconfigureNotification = new Action() { @Override public boolean act(float delta) { notificationText.setWrap(false); + notificationText.setDefaultToken("{COLOR=BLACK}"); notificationText.setText(text); - notificationText.setColor(Color.BLACK); notificationText.setWidth(Math.min(notificationText.getPrefWidth(), Forge.isLandscapeMode() ? getWidth() * 0.25f : getWidth() - 25)); notificationText.setWrap(true); notificationText.layout(); @@ -1119,25 +1126,33 @@ public boolean act(float delta) { if (Forge.isLandscapeMode()) { newNotification = Actions.after(Actions.sequence(preconfigureNotification, - Actions.moveTo(5, 0, 2f), - Actions.delay(10f), - Actions.alpha(0f, 3f), + Actions.moveTo(5, 0, moveDuration), + Actions.delay(delayDuration), + Actions.alpha(0f, alphaDuration), Actions.sizeTo(0, 0))); } else { newNotification = Actions.after(Actions.sequence(preconfigureNotification, - Actions.moveToAligned(5, getHeight(), Align.topLeft, 2f), - Actions.delay(10f), - Actions.alpha(0f, 3f), + Actions.moveToAligned(5, getHeight(), Align.topLeft, moveDuration), + Actions.delay(delayDuration), + Actions.alpha(0f, alphaDuration), Actions.sizeTo(0, 0))); } notificationPane.addAction(newNotification); } + public void addNotification(String text) { + addNotification(text, 2f, 10f, 3f); + } + public void clearNotifications() { notificationText.setText(""); notificationPane.setBounds(5, Forge.isLandscapeMode() ? -notificationText.getPrefHeight() : getHeight(), getWidth() * 0.4f, 25); notificationPane.setStyle(Controls.getSkin().get("paper", ScrollPane.ScrollPaneStyle.class)); notificationPane.getColor().a = 0f; } + + public void setApButtonVisibility(boolean visible) { + apSettingsActor.setVisible(visible); + } } diff --git a/forge-gui-mobile/src/forge/adventure/stage/GameStage.java b/forge-gui-mobile/src/forge/adventure/stage/GameStage.java index 4944c9822a4e..422f1c0e3c3b 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/GameStage.java +++ b/forge-gui-mobile/src/forge/adventure/stage/GameStage.java @@ -1,5 +1,6 @@ package forge.adventure.stage; +import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.controllers.Controller; import com.badlogic.gdx.files.FileHandle; @@ -75,6 +76,9 @@ public abstract class GameStage extends Stage { private float animationTimeout = 0; public static float maximumScrollDistance = 1.5f; public static float minimumScrollDistance = 0.3f; + private final Vector2 keyboardInput = new Vector2(); + private final Vector2 controllerInput = new Vector2(); + private final Vector2 touchInput = new Vector2(); private String extraAnnouncement = ""; @@ -248,16 +252,11 @@ public void showDeckAwardDialog(String message, Deck deck, Runnable runnable) { showDialog(); } - public boolean axisMoved(Controller controller, int axisIndex, float value) { if (MapStage.getInstance().isDialogOnlyInput() || isPaused()) { return true; } - player.getMovementDirection().x = controller.getAxis(0); - player.getMovementDirection().y = -controller.getAxis(1); - if (player.getMovementDirection().len() < 0.2) { - player.stop(); - } + controllerInput.set(controller.getAxis(0), -controller.getAxis(1)); return true; } @@ -268,7 +267,6 @@ enum PlayerModification { } - HashMap currentModifications = new HashMap<>(); public void modifyPlayer(PlayerModification mod, float value) { @@ -360,6 +358,7 @@ public final void act(float delta) { animationTimeout -= delta; return; } + Array modsToRemove = new Array<>(); for (Map.Entry mod : currentModifications.entrySet()) { mod.setValue(mod.getValue() - delta); @@ -371,28 +370,75 @@ public final void act(float delta) { onRemoveEffect(mod); } - if (isPaused()) { - return; - } - - if (onEndAction != null) { - onEndAction.run(); onEndAction = null; } - if (touchX >= 0) { - Vector2 target = this.screenToStageCoordinates(new Vector2(touchX, touchY)); - target.x -= player.getWidth() / 2f; - Vector2 diff = target.sub(player.pos()); + if (isPaused() || isDialogOnlyInput() || Forge.advFreezePlayerControls) { + keyboardInput.setZero(); + controllerInput.setZero(); + touchInput.setZero(); + player.getMovementDirection().setZero(); + player.stop(); + } else { + keyboardInput.setZero(); + for (int key : KeyBinding.Left.getBindings()) { + if (Gdx.input.isKeyPressed(key)) { + keyboardInput.x = -1; + break; + } + } + for (int key : KeyBinding.Right.getBindings()) { + if (Gdx.input.isKeyPressed(key)) { + keyboardInput.x = 1; + break; + } + } + for (int key : KeyBinding.Up.getBindings()) { + if (Gdx.input.isKeyPressed(key)) { + keyboardInput.y = 1; + break; + } + } + for (int key : KeyBinding.Down.getBindings()) { + if (Gdx.input.isKeyPressed(key)) { + keyboardInput.y = -1; + break; + } + } - if (diff.len() < 2) { - diff.setZero(); + // Input priority: touch > controller > keyboard + Vector2 dir = new Vector2(); + if (touchX >= 0 && touchInput.len() > 0.2f) { + dir.set(touchInput); + + } else if (controllerInput.len() > 0.2f) { + dir.set(controllerInput); + + } else { + dir.set(keyboardInput); + } + if (dir.len() < 0.01f) { player.stop(); + } else { + player.getMovementDirection().set(dir); + } + + if (touchX >= 0) { + Vector2 target = this.screenToStageCoordinates(new Vector2(touchX, touchY)); + target.x -= player.getWidth() / 2f; + Vector2 diff = target.sub(player.pos()); + + if (diff.len() < 2) { + touchInput.setZero(); + player.stop(); + } else { + touchInput.set(diff); + } } - player.setMovementDirection(diff); } + camera.position.x = Math.min(Math.max(Scene.getIntendedWidth() / 2f, player.pos().x), getViewport().getWorldWidth() - Scene.getIntendedWidth() / 2f); camera.position.y = Math.min(Math.max(Scene.getIntendedHeight() / 2f, player.pos().y), getViewport().getWorldHeight() - Scene.getIntendedHeight() / 2f); @@ -415,23 +461,22 @@ private void onRemoveEffect(PlayerModification mod) { abstract protected void onActing(float delta); - @Override public boolean keyDown(int keycode) { super.keyDown(keycode); if (isPaused()) return true; if (KeyBinding.Left.isPressed(keycode)) { - player.getMovementDirection().x = -1; + keyboardInput.x = -1; } if (KeyBinding.Right.isPressed(keycode)) { - player.getMovementDirection().x = +1; + keyboardInput.x = +1; } if (KeyBinding.Up.isPressed(keycode)) { - player.getMovementDirection().y = +1; + keyboardInput.y = +1; } if (KeyBinding.Down.isPressed(keycode)) { - player.getMovementDirection().y = -1; + keyboardInput.y = -1; } if (keycode == Input.Keys.F5)//todo config { @@ -542,6 +587,9 @@ public boolean touchDown(int screenX, int screenY, int pointer, int button) { public void stop() { WorldStage.getInstance().getPlayerSprite().setMovementDirection(Vector2.Zero); MapStage.getInstance().getPlayerSprite().setMovementDirection(Vector2.Zero); + touchInput.setZero(); + keyboardInput.setZero(); + controllerInput.setZero(); touchX = -1; touchY = -1; player.stop(); diff --git a/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java b/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java index 941a367d5dea..e8ac0b833fd4 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java +++ b/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java @@ -1,5 +1,17 @@ package forge.adventure.stage; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; + public interface IAfterMatch { - void setWinner(boolean winner, boolean isArena); + default void setWinner(boolean winner, boolean isArena) { + if (ArchipelagoData.getInstance().getArchipelagoMode() != ArchipelagoMode.disabled) { + String playerResultText = "LOST!"; + if (winner) { + playerResultText = "WON!"; + ArchipelagoData.getInstance().addTotalBattlesWon(1); + } + System.out.println("Randomizer: Battle Concluded. The player has " + playerResultText); + } + } } diff --git a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java index b7020ae9b4e5..a308c1ce3707 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java +++ b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java @@ -23,6 +23,7 @@ import com.github.tommyettinger.textra.TypingAdapter; import com.github.tommyettinger.textra.TypingLabel; import forge.Forge; +import forge.adventure.archipelago.*; import forge.adventure.character.*; import forge.adventure.data.*; import forge.adventure.player.AdventurePlayer; @@ -562,7 +563,10 @@ else if (prop.containsKey("portalState")) { { mob.speedModifier = Float.parseFloat(prop.get("speedModifier").toString()); } - + if (ArchipelagoData.getInstance().getArchipelagoMode() != ArchipelagoMode.disabled) { + // Increase base speed of mobs & player to compensate for removal of starting boots. + mob.speedModifier *= 1.15f; + } enemies.add(mob); addMapActor(obj, mob); } @@ -724,8 +728,46 @@ else if (prop.containsKey("portalState")) { shopsAlreadyPresent.add(data.name); Array ret = new Array<>(); WorldSave.getCurrentSave().getWorld().getRandom().setSeed(changes.getShopSeed(id)); - for (RewardData rdata : new Array.ArrayIterator<>(data.rewards)) { - ret.addAll(rdata.generate(false, false)); + // This is where equipment shops load their rewards. Each reward has a RewardType of "item" and comes pre-defined with an item name. + // They are defined in Shandalar/Shops.json as Equipment, Item and Equipment. + // We dynamically detect those names and replace their items if AP mode is enabled here. + // The "Equipment" shop is used generically for all non-capital equipment vendors and as such always carries the same items. + switch (ArchipelagoData.getInstance().getArchipelagoMode()) { + case disabled: + for (RewardData rdata : new Array.ArrayIterator<>(data.rewards)) { + ret.addAll(rdata.generate(false, false)); + } + break; + case solo_randomizer: + if (data.name.toLowerCase().contains("equipment") || data.name.toLowerCase().contains("items")) { + // Get list of items for specific shop. + Object[] randomizedEquipmentList = LocalRandomizer.getInstance().getItemsForEquipmentShop(data.name); + // Swap the shop's rewards + if (randomizedEquipmentList != null && randomizedEquipmentList.length <= data.rewards.size) { + for (int i = 0; i < randomizedEquipmentList.length; i++) { + data.rewards.set(i, ArchipelagoUtil.generateRewardData("item", 1, randomizedEquipmentList[i].toString())); + } + } + } + for (RewardData rdata : new Array.ArrayIterator<>(data.rewards)) { + ret.addAll(rdata.generate(false, false)); + } + break; + case networked_archipelago: + if (data.name.toLowerCase().contains("equipment") || data.name.toLowerCase().contains("items")) { + Set shopItems = ArchipelagoRandomizer.getInstance().getShopItems(data.name); + + if (shopItems != null && shopItems.size() <= data.rewards.size) { + for (ItemData item : shopItems) { + ret.add(new Reward(item)); + } + } + } else { + for (RewardData rdata : new Array.ArrayIterator<>(data.rewards)) { + ret.addAll(rdata.generate(false, false)); + } + } + break; } ShopActor actor = new ShopActor(this, id, ret, data); addMapActor(obj, actor); diff --git a/forge-gui-mobile/src/forge/adventure/stage/WorldStage.java b/forge-gui-mobile/src/forge/adventure/stage/WorldStage.java index a843e4b434ae..043005647efa 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/WorldStage.java +++ b/forge-gui-mobile/src/forge/adventure/stage/WorldStage.java @@ -230,8 +230,8 @@ public boolean handlePointsOfInterestCollision() { public void loadPOI(PointOfInterest poi) { try { - TileMapScene.instance().load(poi); stop(); + TileMapScene.instance().load(poi); TileMapScene.instance().setFromWorldMap(true); Forge.switchScene(TileMapScene.instance()); } catch (Exception e) { @@ -266,7 +266,7 @@ private void handleMonsterSpawn(float delta) { } World world = WorldSave.getCurrentSave().getWorld(); - int currentBiome = World.highestBiome(world.getBiome((int) player.getX() / world.getTileSize(), (int) player.getY() / world.getTileSize())); + int currentBiome = World.highestBiome(world.getBiome((int) ((player.getX() + player.getWidth() / 2f) / world.getTileSize()), (int) (player.getY() / world.getTileSize()))); List biomeData = WorldSave.getCurrentSave().getWorld().getData().GetBiomes(); float sprintingMod = currentModifications.containsKey(PlayerModification.Sprint) ? 2 : 1; if (biomeData.size() <= currentBiome) {// "if isOnRoad diff --git a/forge-gui-mobile/src/forge/adventure/util/KeyBinding.java b/forge-gui-mobile/src/forge/adventure/util/KeyBinding.java index cb103c6b8a9b..a1d1fbb7d7ee 100644 --- a/forge-gui-mobile/src/forge/adventure/util/KeyBinding.java +++ b/forge-gui-mobile/src/forge/adventure/util/KeyBinding.java @@ -49,6 +49,10 @@ public boolean isPressed(int key, Boolean requiredCondition) { return false; } + public int[] getBindings() { + return bindings; + } + // The controller binding always has index 1. final static String controllerPrefix = "XBox_"; diff --git a/forge-gui-mobile/src/forge/adventure/util/MapDialog.java b/forge-gui-mobile/src/forge/adventure/util/MapDialog.java index 59eadffa0687..9adc7955c2e4 100644 --- a/forge-gui-mobile/src/forge/adventure/util/MapDialog.java +++ b/forge-gui-mobile/src/forge/adventure/util/MapDialog.java @@ -16,12 +16,15 @@ import com.github.tommyettinger.textra.TypingAdapter; import com.github.tommyettinger.textra.TypingLabel; import forge.Forge; +import forge.adventure.archipelago.Archipelago; import forge.adventure.character.CharacterSprite; import forge.adventure.character.EnemySprite; import forge.adventure.data.AdventureQuestData; +import forge.adventure.archipelago.ArchipelagoData; import forge.adventure.data.DialogData; import forge.adventure.data.RewardData; import forge.adventure.player.AdventurePlayer; +import forge.adventure.scene.ArchipelagoSettingsScene; import forge.adventure.scene.TileMapScene; import forge.adventure.stage.GameHUD; import forge.adventure.scene.RewardScene; @@ -324,16 +327,23 @@ void setEffects(DialogData.ActionData[] data) { } if (E.addItem != null&& (!E.addItem.isEmpty())) { //Gives an item to the player. Current.player().addItem(E.addItem); + ArchipelagoData.getInstance().addItem(E.addItem); } if (E.addLife != 0) { //Gives (positive or negative) life to the player. Cannot go over max health. Current.player().heal(E.addLife); } if (E.addGold != 0) { //Gives (positive or negative) gold to the player. - if (E.addGold > 0) Current.player().giveGold(E.addGold); + if (E.addGold > 0) { + Current.player().giveGold(E.addGold); + ArchipelagoData.getInstance().addGold(E.addGold); + } else Current.player().takeGold(-E.addGold); } if (E.addShards != 0) { //Gives (positive or negative) mana shards to the player. - if (E.addShards > 0) Current.player().addShards(E.addShards); + if (E.addShards > 0) { + Current.player().addShards(E.addShards); + ArchipelagoData.getInstance().addShards(E.addShards); + } else Current.player().takeShards(-E.addShards); } if (E.addMapReputation != 0) { @@ -417,6 +427,10 @@ void setEffects(DialogData.ActionData[] data) { questAccepted = E.issueQuest; emitQuestAccepted(); } + if (E.openArchipelagoSettings) { + ArchipelagoSettingsScene.instance().setConnectStatusLabel(Archipelago.getInstance().isConnected() ? "{FADE=GREEN;GREEN;0.1}Connected!" : "{FADE=RED;RED;0.1}Not Connected..."); + Forge.switchScene(ArchipelagoSettingsScene.instance()); + } } } diff --git a/forge-gui-mobile/src/forge/adventure/util/RewardActor.java b/forge-gui-mobile/src/forge/adventure/util/RewardActor.java index f96f2ce038cf..d41bff5b1853 100644 --- a/forge-gui-mobile/src/forge/adventure/util/RewardActor.java +++ b/forge-gui-mobile/src/forge/adventure/util/RewardActor.java @@ -33,6 +33,8 @@ import forge.Forge; import forge.Graphics; import forge.ImageKeys; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoUtil; import forge.adventure.data.ItemData; import forge.adventure.player.AdventurePlayer; import forge.adventure.scene.RewardScene; @@ -107,6 +109,7 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb private boolean shouldDisplayText = false; private boolean isDragging = false; private boolean isNew = false; + private ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); @Override public void dispose() { @@ -804,8 +807,16 @@ private void processSprite(Sprite sprite, Sprite item, TextraLabel itemText, int float iw = item.getWidth() * 4; float ih = item.getHeight() * 4; batch.draw(item, pw / 2f - iw / 2f, (ph / 2f - ih / 2f), iw, ih); - } else + } else { batch.draw(item, pw / 4f, ph / 4f, pw / 2f, ph / 2f); + + if (reward != null && reward.getDeck() != null && reward.getDeck().getComment() != null) { + if (!archipelagoData.isSetUnlocked(reward.getDeck().getComment())){ + ArchipelagoUtil.drawLockedCardOverlay(batch, getWidth()/2, ph/2f, pw, ph/2f); + } + } + } + } if (itemText != null) { itemText.setWrap(true); @@ -971,6 +982,10 @@ public void sold() { getColor().a = 0.5f; } + public boolean isSold() { + return sold; + } + private static boolean inCollectionLike(PaperCard pc) { var coll = AdventurePlayer.current().getCollectionCards(true).toFlatList(); String name = pc.getName(); @@ -1006,7 +1021,7 @@ public void act(float delta) { PaperCard pc = reward.getCard(); if (pc != null) { - DeckFormat deckFormat = AdventurePlayer.current().isCommanderMode() + DeckFormat deckFormat = AdventurePlayer.current().isCommanderMode() ? DeckFormat.Commander : DeckFormat.Adventure; int maxCopies = deckFormat.getMaxCardCopies(pc); @@ -1169,6 +1184,9 @@ private void drawCard(Batch batch, Texture image, float x, float width) { float scale = getHeight() / 4f; batch.draw(icon, getOriginX() - scale / 2f, getOriginY() - scale / 2f, scale, scale); } + if (!archipelagoData.checkCardUnlocked(reward.getCard())) { + ArchipelagoUtil.drawLockedCardOverlay(batch, x, -getHeight() / 2, width, getHeight()); + } } } diff --git a/forge-gui-mobile/src/forge/adventure/world/World.java b/forge-gui-mobile/src/forge/adventure/world/World.java index 48d0a6d40b64..120484ace152 100644 --- a/forge-gui-mobile/src/forge/adventure/world/World.java +++ b/forge-gui-mobile/src/forge/adventure/world/World.java @@ -13,6 +13,9 @@ import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.Json; import forge.Forge; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoMode; +import forge.adventure.archipelago.ArchipelagoUtil; import forge.adventure.data.*; import forge.adventure.pointofintrest.PointOfInterest; import forge.adventure.pointofintrest.PointOfInterestMap; @@ -451,6 +454,10 @@ public boolean generateNew(long seed) { clearTerrain((int) (data.width * data.playerStartPosX), (int) (data.height * data.playerStartPosY), 10); //otherPoints.add(new Rectangle(((float) data.width * data.playerStartPosX * (float) data.tileSize) - data.tileSize * 3, ((float) data.height * data.playerStartPosY * data.tileSize) - data.tileSize * 3, data.tileSize * 6, data.tileSize * 6)); boolean running = true; + boolean isNetworkedAP = ArchipelagoData.getInstance().getArchipelagoMode() == ArchipelagoMode.networked_archipelago; + List archipelagoPois = new ArrayList<>(); + if (isNetworkedAP) + archipelagoPois = ArchipelagoUtil.getArchipelagoPois(); here: while (running) { mapPoiIds = new PointOfInterestMap(getChunkSize(), data.tileSize, data.width / getChunkSize(), data.height / getChunkSize()); @@ -462,7 +469,14 @@ public boolean generateNew(long seed) { for (int i = 0; i < poi.count; i++) { for (int counter = 0; counter < 500; counter++)//tries 500 times to find a free point { - float radius = (float) Math.sqrt(((random.nextDouble()) / 2 * poi.radiusFactor)); + boolean isArchipelagoPoi = archipelagoPois.contains(poi.name.toLowerCase()); + float radius; + if (isArchipelagoPoi) { + radius = (float) (Math.sqrt(random.nextDouble()) * (biome.name.equals("waste") ? 0.15f : 0.25f)); + } else { + radius = (float) Math.sqrt((random.nextDouble() / 2f) * poi.radiusFactor); + } + float theta = (float) (random.nextDouble() * 2 * Math.PI); float x = (float) (radius * Math.cos(theta)); x *= (biome.width * width / 2); diff --git a/forge-gui-mobile/src/forge/adventure/world/WorldSave.java b/forge-gui-mobile/src/forge/adventure/world/WorldSave.java index 4119f15cf9ee..318b268b5fff 100644 --- a/forge-gui-mobile/src/forge/adventure/world/WorldSave.java +++ b/forge-gui-mobile/src/forge/adventure/world/WorldSave.java @@ -1,5 +1,6 @@ package forge.adventure.world; +import forge.adventure.archipelago.*; import forge.adventure.data.DifficultyData; import forge.adventure.player.AdventurePlayer; import forge.adventure.pointofintrest.PointOfInterest; @@ -75,6 +76,7 @@ static public boolean load(int currentSlot) { try { currentSave.world.load(mainData.readSubData("world")); currentSave.pointOfInterestChanges.load(mainData.readSubData("pointOfInterestChanges")); + ArchipelagoData.getInstance().load(mainData.readSubData("archipelago")); WorldStage.getInstance().load(mainData.readSubData("worldStage")); } catch (Exception e) { @@ -128,13 +130,28 @@ public static WorldSave getCurrentSave() { } public static WorldSave generateNewWorld(String name, boolean male, int race, int avatarIndex, ColorSet startingColorIdentity, DifficultyData diff, AdventureModes mode, int customDeckIndex, CardEdition starterEdition, long seed) { + return generateNewWorld(name, male, race, avatarIndex, startingColorIdentity, diff, mode, customDeckIndex, starterEdition, seed, ArchipelagoMode.disabled); + } + + public static WorldSave generateNewWorld(String name, boolean male, int race, int avatarIndex, ColorSet startingColorIdentity, DifficultyData diff, AdventureModes mode, int customDeckIndex, CardEdition starterEdition, long seed, ArchipelagoMode archipelagoMode) { + ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); + Archipelago.getInstance().disconnect(); + // Initial archipelago setup + if (archipelagoMode == ArchipelagoMode.solo_randomizer) { + LocalRandomizer.getInstance().setupFreshSaveFile(); + } else if (archipelagoMode == ArchipelagoMode.networked_archipelago) { + ArchipelagoRandomizer.getInstance().setupFreshSaveFile(); + } else { + archipelagoData.setupFreshSaveFile(archipelagoMode); + } + currentSave.world.generateNew(seed); currentSave.pointOfInterestChanges.clear(); boolean chaos = mode == AdventureModes.Chaos; boolean custom = mode == AdventureModes.Custom; Deck starterDeck = Config.instance().starterDeck(startingColorIdentity, diff, mode, customDeckIndex, starterEdition); - currentSave.player.create(name, starterDeck, male, race, avatarIndex, chaos, custom, diff, mode); + currentSave.player.create(name, starterDeck, male, race, avatarIndex, chaos, custom, diff, mode, archipelagoMode); currentSave.player.setWorldPosY((int) (currentSave.world.getData().playerStartPosY * currentSave.world.getData().height * currentSave.world.getTileSize())); currentSave.player.setWorldPosX((int) (currentSave.world.getData().playerStartPosX * currentSave.world.getData().width * currentSave.world.getTileSize())); @@ -172,6 +189,10 @@ public boolean save(String text, int currentSlot) { SaveFileData player = currentSave.player.save(); SaveFileData world = currentSave.world.save(); SaveFileData worldStage = WorldStage.getInstance().save(); + SaveFileData archipelago = null; + if (ArchipelagoData.getInstance().getArchipelagoMode() != ArchipelagoMode.disabled) { + archipelago = ArchipelagoData.getInstance().save(); + } SaveFileData poiChanges = currentSave.pointOfInterestChanges.save(); String message = getExceptionMessage(player, world, worldStage, poiChanges); @@ -188,6 +209,9 @@ public boolean save(String text, int currentSlot) { mainData.store("world", world); mainData.store("worldStage", worldStage); mainData.store("pointOfInterestChanges", poiChanges); + if (archipelago != null) { + mainData.store("archipelago", archipelago); + } if (mainData.readString("IOException") != null) { oos.close(); diff --git a/forge-gui-mobile/src/forge/card/CardImageRenderer.java b/forge-gui-mobile/src/forge/card/CardImageRenderer.java index f41aac42c72f..502d7898f2ae 100644 --- a/forge-gui-mobile/src/forge/card/CardImageRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardImageRenderer.java @@ -1,8 +1,7 @@ package forge.card; import static forge.assets.FSkin.getDefaultSkinFile; -import static forge.card.CardRenderer.CROP_MULTIPLIER; -import static forge.card.CardRenderer.isModernFrame; +import static forge.card.CardRenderer.*; import java.util.ArrayList; import java.util.List; diff --git a/forge-gui-mobile/src/forge/card/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index a30b78b2f0ae..9725ec6c9285 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -7,6 +7,9 @@ import java.util.Map; import forge.ImageKeys; +import forge.adventure.archipelago.ArchipelagoData; +import forge.adventure.archipelago.ArchipelagoUtil; +import forge.item.PaperCard; import forge.localinstance.properties.ForgeConstants; import forge.util.*; import org.apache.commons.lang3.StringUtils; @@ -68,6 +71,8 @@ public enum CardStackPosition { BehindVert } + static final ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); + /** Pref is normalized to 6 hex chars on the write side; this just parses, * falling back to the FPref default if the stored value is malformed. */ private static Color parseActionableHighlightColor() { @@ -494,12 +499,19 @@ public static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreC public static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreColor, IPaperCard pc, int count, String suffix, float x, float y, float w, float h, boolean compactMode) { final CardView card = CardView.getCardForUi(pc); final CardStateView state = card.getCurrentState(); - drawCardListItem(g, font, foreColor, getCardArt(pc), card, pc.getEdition(), + boolean isUnlocked = archipelagoData.checkCardUnlocked((PaperCard) pc); + drawLockAwareCardListItem(g, font, foreColor, getCardArt(pc), card, pc.getEdition(), pc.getRarity(), state.getPower(), state.getToughness(), - state.getLoyalty(), count, suffix, x, y, w, h, compactMode); + state.getLoyalty(), count, suffix, x, y, w, h, compactMode, isUnlocked); } public static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreColor, FImageComplex cardArt, CardView card, String set, CardRarity rarity, int power, int toughness, String loyalty, int count, String suffix, float x, float y, float w, float h, boolean compactMode) { + drawLockAwareCardListItem(g, font, foreColor, cardArt, card, set, + rarity, power, toughness, + loyalty, count, suffix, x, y, w, h, compactMode, true); + } + + public static void drawLockAwareCardListItem(Graphics g, FSkinFont font, FSkinColor foreColor, FImageComplex cardArt, CardView card, String set, CardRarity rarity, int power, int toughness, String loyalty, int count, String suffix, float x, float y, float w, float h, boolean compactMode, boolean isUnlocked) { float cardArtHeight = h + 2 * FList.PADDING; float cardArtWidth = cardArtHeight * CARD_ART_RATIO; CardView.CardStateView cardCurrentState = card.getCurrentState(); @@ -522,6 +534,9 @@ public static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreC g.drawRotatedImage(secondArt.getTexture(), artX - cardArtHeight / 2, artY + cardArtHeight / 2, cardArtHeight / 2, cardArtWidth, artX, artY + cardArtHeight / 2, secondArt.getRegionX(), secondArt.getRegionY(), (int) secondArt.getWidth(), (int) secondArt.getHeight(), 90); } else { g.drawImage(cardArt, artX, artY, cardArtWidth, cardArtHeight); + if (!isUnlocked) { + ArchipelagoUtil.drawLockedCardOverlay(g, artX, artY, cardArtWidth, cardArtHeight); + } } } @@ -615,6 +630,13 @@ public static void drawSetLabel(Graphics g, FSkinFont font, String set, CardRari g.drawText(set, font, foreColor, x, y, w, h, false, Align.center, true); } + private static void drawCardLock(Graphics g, float x, float y, float w, float h, IPaperCard pc) { + // Draw the lock overtop of the card if the card is locked here. + if (!archipelagoData.checkCardUnlocked((PaperCard) pc)) { + ArchipelagoUtil.drawLockedCardOverlay(g, x, y, w, h); + } + } + public static void drawCard(Graphics g, IPaperCard pc, float x, float y, float w, float h, CardStackPosition pos) { Texture image = new RendererCachedCardImage(pc, false).getImage(); final CardView card = CardView.getCardForUi(pc); @@ -653,6 +675,7 @@ public static void drawCard(Graphics g, IPaperCard pc, float x, float y, float w //if card has invalid or no texture due to sudden changes in ImageCache, draw CardImageRenderer instead and wait for it to refresh automatically CardImageRenderer.drawCardImage(g, card, false, x, y, w, h, pos, true, true); } + drawCardLock(g, x, y, w, h, pc); } public static void drawCard(Graphics g, CardView card, float x, float y, float w, float h, CardStackPosition pos, boolean rotate) { diff --git a/forge-gui-mobile/src/forge/itemmanager/filters/CardLockFilter.java b/forge-gui-mobile/src/forge/itemmanager/filters/CardLockFilter.java new file mode 100644 index 000000000000..78b9624dea31 --- /dev/null +++ b/forge-gui-mobile/src/forge/itemmanager/filters/CardLockFilter.java @@ -0,0 +1,30 @@ +package forge.itemmanager.filters; + +import forge.item.PaperCard; +import forge.itemmanager.ItemManager; +import forge.itemmanager.SFilterUtil; +import forge.itemmanager.SItemManagerUtil.StatTypes; + +import java.util.function.Predicate; + + +public class CardLockFilter extends StatTypeFilter { + public CardLockFilter(ItemManager itemManager0) { + super(itemManager0); + } + + @Override + public ItemFilter createCopy() { + return new CardLockFilter(itemManager); + } + + @Override + protected void buildWidget(Widget widget) { + addToggleButton(widget, StatTypes.LOCKED); + } + + @Override + protected final Predicate buildPredicate() { + return SFilterUtil.buildLockedFilter(buttonMap); + } +} diff --git a/forge-gui/res/adventure/Innistrad/ui/new_game.json b/forge-gui/res/adventure/Innistrad/ui/new_game.json index fdb71d59f48f..368d2fdce4bf 100644 --- a/forge-gui/res/adventure/Innistrad/ui/new_game.json +++ b/forge-gui/res/adventure/Innistrad/ui/new_game.json @@ -18,6 +18,15 @@ "width": 280, "height": 250 }, + { + "type": "Scroll", + "name" : "selectorScroll", + "style": "nobg", + "x": 56, + "y": 110, + "width": 280, + "height": 145 + }, { "type": "Label", "name" : "titleL", diff --git a/forge-gui/res/adventure/Innistrad/ui/new_game_portrait.json b/forge-gui/res/adventure/Innistrad/ui/new_game_portrait.json index f4e851e7cccd..4365b4019b45 100644 --- a/forge-gui/res/adventure/Innistrad/ui/new_game_portrait.json +++ b/forge-gui/res/adventure/Innistrad/ui/new_game_portrait.json @@ -18,6 +18,15 @@ "width": 256, "height": 355 }, + { + "type": "Scroll", + "name" : "selectorScroll", + "style": "nobg", + "x": -3, + "y": 168, + "width": 280, + "height": 174 + }, { "type": "Label", "name" : "titleL", diff --git a/forge-gui/res/adventure/common/decks/boss/Fire of Kaladesh.dck b/forge-gui/res/adventure/common/decks/miniboss/Fire of Kaladesh.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/Fire of Kaladesh.dck rename to forge-gui/res/adventure/common/decks/miniboss/Fire of Kaladesh.dck diff --git a/forge-gui/res/adventure/common/decks/boss/angel_rainbow_encounter.dck b/forge-gui/res/adventure/common/decks/miniboss/angel_rainbow_encounter.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/angel_rainbow_encounter.dck rename to forge-gui/res/adventure/common/decks/miniboss/angel_rainbow_encounter.dck diff --git a/forge-gui/res/adventure/common/decks/standard/bluedragon.dck b/forge-gui/res/adventure/common/decks/miniboss/bluedragon.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/standard/bluedragon.dck rename to forge-gui/res/adventure/common/decks/miniboss/bluedragon.dck diff --git a/forge-gui/res/adventure/common/decks/boss/conjure.dck b/forge-gui/res/adventure/common/decks/miniboss/conjure.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/conjure.dck rename to forge-gui/res/adventure/common/decks/miniboss/conjure.dck diff --git a/forge-gui/res/adventure/common/decks/standard/copperhostbrutalizer.dck b/forge-gui/res/adventure/common/decks/miniboss/copperhostbrutalizer.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/standard/copperhostbrutalizer.dck rename to forge-gui/res/adventure/common/decks/miniboss/copperhostbrutalizer.dck diff --git a/forge-gui/res/adventure/common/decks/standard/darkenchanter.dck b/forge-gui/res/adventure/common/decks/miniboss/darkenchanter.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/standard/darkenchanter.dck rename to forge-gui/res/adventure/common/decks/miniboss/darkenchanter.dck diff --git a/forge-gui/res/adventure/common/decks/boss/frogboss.dck b/forge-gui/res/adventure/common/decks/miniboss/frogboss.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/frogboss.dck rename to forge-gui/res/adventure/common/decks/miniboss/frogboss.dck diff --git a/forge-gui/res/adventure/common/decks/boss/garruk.dck b/forge-gui/res/adventure/common/decks/miniboss/garruk.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/garruk.dck rename to forge-gui/res/adventure/common/decks/miniboss/garruk.dck diff --git a/forge-gui/res/adventure/common/decks/standard/hydraboss.dck b/forge-gui/res/adventure/common/decks/miniboss/hydraboss.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/standard/hydraboss.dck rename to forge-gui/res/adventure/common/decks/miniboss/hydraboss.dck diff --git a/forge-gui/res/adventure/common/decks/boss/jace.dck b/forge-gui/res/adventure/common/decks/miniboss/jace.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/jace.dck rename to forge-gui/res/adventure/common/decks/miniboss/jace.dck diff --git a/forge-gui/res/adventure/common/decks/boss/jungle_king.dck b/forge-gui/res/adventure/common/decks/miniboss/jungle_king.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/jungle_king.dck rename to forge-gui/res/adventure/common/decks/miniboss/jungle_king.dck diff --git a/forge-gui/res/adventure/common/decks/boss/kiora.dck b/forge-gui/res/adventure/common/decks/miniboss/kiora.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/kiora.dck rename to forge-gui/res/adventure/common/decks/miniboss/kiora.dck diff --git a/forge-gui/res/adventure/common/decks/boss/liliana_zombies.dck b/forge-gui/res/adventure/common/decks/miniboss/liliana_zombies.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/liliana_zombies.dck rename to forge-gui/res/adventure/common/decks/miniboss/liliana_zombies.dck diff --git a/forge-gui/res/adventure/common/decks/boss/myr_superion.dck b/forge-gui/res/adventure/common/decks/miniboss/myr_superion.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/myr_superion.dck rename to forge-gui/res/adventure/common/decks/miniboss/myr_superion.dck diff --git a/forge-gui/res/adventure/common/decks/boss/nahiri.dck b/forge-gui/res/adventure/common/decks/miniboss/nahiri.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/nahiri.dck rename to forge-gui/res/adventure/common/decks/miniboss/nahiri.dck diff --git a/forge-gui/res/adventure/common/decks/boss/ooze_boss.dck b/forge-gui/res/adventure/common/decks/miniboss/ooze_boss.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/ooze_boss.dck rename to forge-gui/res/adventure/common/decks/miniboss/ooze_boss.dck diff --git a/forge-gui/res/adventure/common/decks/standard/scarecrowcaptain.dck b/forge-gui/res/adventure/common/decks/miniboss/scarecrowcaptain.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/standard/scarecrowcaptain.dck rename to forge-gui/res/adventure/common/decks/miniboss/scarecrowcaptain.dck diff --git a/forge-gui/res/adventure/common/decks/boss/slimefoot.dck b/forge-gui/res/adventure/common/decks/miniboss/slimefoot.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/slimefoot.dck rename to forge-gui/res/adventure/common/decks/miniboss/slimefoot.dck diff --git a/forge-gui/res/adventure/common/decks/boss/sliver_queen.dck b/forge-gui/res/adventure/common/decks/miniboss/sliver_queen.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/sliver_queen.dck rename to forge-gui/res/adventure/common/decks/miniboss/sliver_queen.dck diff --git a/forge-gui/res/adventure/common/decks/boss/slobad.dck b/forge-gui/res/adventure/common/decks/miniboss/slobad.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/slobad.dck rename to forge-gui/res/adventure/common/decks/miniboss/slobad.dck diff --git a/forge-gui/res/adventure/common/decks/boss/sorin.dck b/forge-gui/res/adventure/common/decks/miniboss/sorin.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/sorin.dck rename to forge-gui/res/adventure/common/decks/miniboss/sorin.dck diff --git a/forge-gui/res/adventure/common/decks/boss/teferi_boss.dck b/forge-gui/res/adventure/common/decks/miniboss/teferi_boss.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/teferi_boss.dck rename to forge-gui/res/adventure/common/decks/miniboss/teferi_boss.dck diff --git a/forge-gui/res/adventure/common/decks/boss/thecheff.dck b/forge-gui/res/adventure/common/decks/miniboss/thecheff.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/thecheff.dck rename to forge-gui/res/adventure/common/decks/miniboss/thecheff.dck diff --git a/forge-gui/res/adventure/common/decks/boss/tibalt.dck b/forge-gui/res/adventure/common/decks/miniboss/tibalt.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/tibalt.dck rename to forge-gui/res/adventure/common/decks/miniboss/tibalt.dck diff --git a/forge-gui/res/adventure/common/decks/boss/torturer.dck b/forge-gui/res/adventure/common/decks/miniboss/torturer.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/torturer.dck rename to forge-gui/res/adventure/common/decks/miniboss/torturer.dck diff --git a/forge-gui/res/adventure/common/decks/boss/valyx.dck b/forge-gui/res/adventure/common/decks/miniboss/valyx.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/valyx.dck rename to forge-gui/res/adventure/common/decks/miniboss/valyx.dck diff --git a/forge-gui/res/adventure/common/decks/boss/walter.dck b/forge-gui/res/adventure/common/decks/miniboss/walter.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/walter.dck rename to forge-gui/res/adventure/common/decks/miniboss/walter.dck diff --git a/forge-gui/res/adventure/common/decks/boss/xira.dck b/forge-gui/res/adventure/common/decks/miniboss/xira.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/xira.dck rename to forge-gui/res/adventure/common/decks/miniboss/xira.dck diff --git a/forge-gui/res/adventure/common/decks/boss/zedruubis.dck b/forge-gui/res/adventure/common/decks/miniboss/zedruubis.dck similarity index 100% rename from forge-gui/res/adventure/common/decks/boss/zedruubis.dck rename to forge-gui/res/adventure/common/decks/miniboss/zedruubis.dck diff --git a/forge-gui/res/adventure/common/maps/map/lavaforge/lavaforge_3_vulcanodragon.tmx b/forge-gui/res/adventure/common/maps/map/lavaforge/lavaforge_3_vulcanodragon.tmx index 5da38634831d..1e7742215a57 100644 --- a/forge-gui/res/adventure/common/maps/map/lavaforge/lavaforge_3_vulcanodragon.tmx +++ b/forge-gui/res/adventure/common/maps/map/lavaforge/lavaforge_3_vulcanodragon.tmx @@ -157,7 +157,7 @@ "lifeModifier": 4, "startBattleWithCard": [ "Fearsome Whelp" ] } - + diff --git a/forge-gui/res/adventure/common/sprites/ap_icon_small.png b/forge-gui/res/adventure/common/sprites/ap_icon_small.png new file mode 100644 index 000000000000..167f66942c4e Binary files /dev/null and b/forge-gui/res/adventure/common/sprites/ap_icon_small.png differ diff --git a/forge-gui/res/adventure/common/sprites/items.atlas b/forge-gui/res/adventure/common/sprites/items.atlas index f0f3b1f0849f..810f13fb7d2e 100644 --- a/forge-gui/res/adventure/common/sprites/items.atlas +++ b/forge-gui/res/adventure/common/sprites/items.atlas @@ -569,4 +569,7 @@ SirDonovansAmulet size:16,16 LandscapeSketchbook xy:80,672 - size:16,16 \ No newline at end of file + size:16,16 +APIconSmall + xy:334,176 + size:18,18 \ No newline at end of file diff --git a/forge-gui/res/adventure/common/sprites/items.png b/forge-gui/res/adventure/common/sprites/items.png index 83e353ff0efe..d73c0445fb03 100644 Binary files a/forge-gui/res/adventure/common/sprites/items.png and b/forge-gui/res/adventure/common/sprites/items.png differ diff --git a/forge-gui/res/adventure/common/ui/apsettings.json b/forge-gui/res/adventure/common/ui/apsettings.json new file mode 100644 index 000000000000..28255ca8e9e3 --- /dev/null +++ b/forge-gui/res/adventure/common/ui/apsettings.json @@ -0,0 +1,122 @@ +{ + "width": 480, + "height": 270, + "yDown": true, + "elements": [ + { + "type": "Image", + "image": "ui/title_bg.png", + "width": 480, + "height": 270 + }, + { + "type": "Scroll", + "name": "settings", + "x": 15, + "y": 18, + "width": 382, + "height": 235 + }, + { + "type": "TextButton", + "name": "return", + "text": "tr(lblBack)", + "binding": "Back", + "width": 52, + "height": 30, + "x": 415, + "y": 120 + }, + { + "type": "Label", + "name": "ipLabel", + "text": "Server IP", + "font": "default", + "width": 60, + "height": 30, + "x": 24, + "y": 30 + }, + { + "type": "TextField", + "name": "apIP", + "width": 303, + "height": 30, + "x": 84, + "y": 28 + }, + { + "type": "Label", + "name": "portLabel", + "text": "Port", + "font": "default", + "width": 60, + "height": 30, + "x": 24, + "y": 60 + }, + { + "type": "TextField", + "name": "apPort", + "width": 303, + "height": 30, + "x": 84, + "y": 58 + }, + { + "type": "Label", + "name": "slotNameLabel", + "text": "Slot Name", + "font": "default", + "width": 60, + "height": 30, + "x": 24, + "y": 90 + }, + { + "type": "TextField", + "name": "apSlotName", + "width": 303, + "height": 30, + "x": 84, + "y": 88 + }, + { + "type": "Label", + "name": "passwordLabel", + "text": "Password", + "font": "default", + "width": 60, + "height": 30, + "x": 24, + "y": 120 + }, + { + "type": "TextField", + "name": "apPassword", + "width": 303, + "height": 30, + "x": 84, + "y": 118 + }, + { + "type": "TypingLabel", + "name": "connectStatusLabel", + "text": "Not Connected...", + "font": "default", + "width": 100, + "height": 25, + "x": 242, + "y": 153 + }, + { + "type": "TextButton", + "name": "connect", + "text": "Connect", + "width": 45, + "height": 25, + "x": 342, + "y": 153 + } + ] +} \ No newline at end of file diff --git a/forge-gui/res/adventure/common/ui/aptracker.json b/forge-gui/res/adventure/common/ui/aptracker.json new file mode 100644 index 000000000000..8e3048992694 --- /dev/null +++ b/forge-gui/res/adventure/common/ui/aptracker.json @@ -0,0 +1,31 @@ +{ + "width": 480, + "height": 270, + "yDown": true, + "elements": [ + { + "type": "Image", + "image": "ui/title_bg.png", + "width": 480, + "height": 270 + }, + { + "type": "Scroll", + "name": "settings", + "x": 15, + "y": 18, + "width": 382, + "height": 235 + }, + { + "type": "TextButton", + "name": "return", + "text": "tr(lblBack)", + "binding": "Back", + "width": 52, + "height": 30, + "x": 415, + "y": 120 + } + ] +} \ No newline at end of file diff --git a/forge-gui/res/adventure/common/ui/hud.json b/forge-gui/res/adventure/common/ui/hud.json index 9a94cbbdcf2c..77b43f3cac40 100644 --- a/forge-gui/res/adventure/common/ui/hud.json +++ b/forge-gui/res/adventure/common/ui/hud.json @@ -131,6 +131,15 @@ "x": 108, "y": 0 }, + { + "type": "TextButton", + "name": "aptracker", + "text": "[%120][+APIconSmall]", + "width": 25, + "height": 25, + "x": 83, + "y": 0 + }, { "type": "TextButton", "name": "openmap", diff --git a/forge-gui/res/adventure/common/ui/hud_landscape.json b/forge-gui/res/adventure/common/ui/hud_landscape.json index 38a96384a661..4ba639641072 100644 --- a/forge-gui/res/adventure/common/ui/hud_landscape.json +++ b/forge-gui/res/adventure/common/ui/hud_landscape.json @@ -131,6 +131,15 @@ "x": 108, "y": 240 }, + { + "type": "TextButton", + "name": "aptracker", + "text": "[%120][+APIconSmall]", + "width": 25, + "height": 25, + "x": 83, + "y": 240 + }, { "type": "TextButton", "name": "openmap", diff --git a/forge-gui/res/adventure/common/ui/hud_portrait.json b/forge-gui/res/adventure/common/ui/hud_portrait.json index 29cd42f7fffe..d92c1229367e 100644 --- a/forge-gui/res/adventure/common/ui/hud_portrait.json +++ b/forge-gui/res/adventure/common/ui/hud_portrait.json @@ -127,6 +127,15 @@ "x": 68, "y": 450 }, + { + "type": "TextButton", + "name": "aptracker", + "text": "[%120][+APIconSmall]", + "width": 25, + "height": 25, + "x": 43, + "y": 450 + }, { "type": "TextButton", "name": "openmap", diff --git a/forge-gui/res/adventure/common/ui/new_game.json b/forge-gui/res/adventure/common/ui/new_game.json index 3cd6eebaafb7..502711cda2f9 100644 --- a/forge-gui/res/adventure/common/ui/new_game.json +++ b/forge-gui/res/adventure/common/ui/new_game.json @@ -18,6 +18,15 @@ "width": 280, "height": 250 }, + { + "type": "Scroll", + "name" : "selectorScroll", + "style": "nobg", + "x": 56, + "y": 110, + "width": 280, + "height": 145 + }, { "type": "Label", "name" : "titleL", @@ -91,6 +100,25 @@ "height": 16, "x": 75, "yOffset": 8 + }, + { + "type": "ImageButton", + "name": "archipelagoHelp", + "style": "roundhint", + "selectable": true, + "width": 12, + "height": 15, + "x": 145, + "yOffset": -17 + }, + { + "type": "Label", + "name" : "enableArchipelagoL", + "text": "[BLACK]tr(lblArchipelagoMode)", + "width": 128, + "height": 16, + "x": 75, + "yOffset": 8 }, { "type": "ImageButton", @@ -193,6 +221,15 @@ "height": 14, "x": 159, "yOffset": 9 + }, + { + "type": "Selector", + "name": "enableArchipelago", + "selectable": true, + "width": 142, + "height": 14, + "x": 159, + "yOffset": 9 }, { "type": "Selector", diff --git a/forge-gui/res/adventure/common/ui/new_game_portrait.json b/forge-gui/res/adventure/common/ui/new_game_portrait.json index a04cfe2b0100..d3ee53a3f55e 100644 --- a/forge-gui/res/adventure/common/ui/new_game_portrait.json +++ b/forge-gui/res/adventure/common/ui/new_game_portrait.json @@ -18,6 +18,15 @@ "width": 256, "height": 355 }, + { + "type": "Scroll", + "name" : "selectorScroll", + "style": "nobg", + "x": -3, + "y": 168, + "width": 280, + "height": 174 + }, { "type": "Label", "name" : "titleL", @@ -91,6 +100,25 @@ "height": 24, "x": 16, "yOffset": 8 + }, + { + "type": "ImageButton", + "name": "archipelagoHelp", + "style": "roundhint", + "selectable": true, + "width": 12, + "height": 15, + "x": 80, + "yOffset": -17 + }, + { + "type": "Label", + "name" : "enableArchipelagoL", + "text": "[BLACK]tr(lblArchipelagoMode)", + "width": 128, + "height": 16, + "x": 75, + "yOffset": 8 }, { "type": "ImageButton", @@ -192,6 +220,15 @@ "height": 24, "x": 96, "yOffset": 8 + }, + { + "type": "Selector", + "name": "enableArchipelago", + "selectable": true, + "width": 142, + "height": 14, + "x": 159, + "yOffset": 9 }, { "type": "Selector", diff --git a/forge-gui/res/adventure/common/ui/start_menu.json b/forge-gui/res/adventure/common/ui/start_menu.json index 7e0d89bdd357..95eb9c7518b9 100644 --- a/forge-gui/res/adventure/common/ui/start_menu.json +++ b/forge-gui/res/adventure/common/ui/start_menu.json @@ -75,9 +75,9 @@ "name": "Settings", "text": "tr(lblSettings)", "selectable": true, - "width": 80, + "width": 65, "height": 30, - "x": 160, + "x": 195, "y": 180 }, { @@ -85,9 +85,9 @@ "name": "Backup", "text": "tr(lblData)", "selectable": true, - "width": 80, + "width": 60, "height": 30, - "x": 240, + "x": 260, "y": 180 }, { @@ -109,6 +109,15 @@ "height": 30, "x": 245, "y": 220 + }, + { + "type": "TextButton", + "name": "ApSettings", + "text": "[%120][+APIconSmall]", + "width": 30, + "height": 30, + "x": 160, + "y": 180 } ] } \ No newline at end of file diff --git a/forge-gui/res/adventure/common/ui/start_menu_portrait.json b/forge-gui/res/adventure/common/ui/start_menu_portrait.json index faf686bc8b5b..ec17a89dd042 100644 --- a/forge-gui/res/adventure/common/ui/start_menu_portrait.json +++ b/forge-gui/res/adventure/common/ui/start_menu_portrait.json @@ -59,24 +59,33 @@ "x": 16, "yOffset": 8 }, + { + "type": "TextButton", + "name": "ApSettings", + "text": "[%120][+APIconSmall]", + "width": 38, + "height": 48, + "x": 16, + "yOffset": 8 + }, { "type": "TextButton", "name": "Settings", "text": "tr(lblSettings)", "selectable": true, - "width": 118, + "width": 119, "height": 48, "x": 16, - "yOffset": 8 + "yOffset": -48 }, { "type": "TextButton", "name": "Backup", "text": "tr(lblData)", "selectable": true, - "width": 118, + "width": 119, "height": 48, - "x": 134, + "x": 135, "yOffset": -48 }, { diff --git a/forge-gui/res/adventure/common/world/enemies.json b/forge-gui/res/adventure/common/world/enemies.json index 60029b622616..f65e73476621 100644 --- a/forge-gui/res/adventure/common/world/enemies.json +++ b/forge-gui/res/adventure/common/world/enemies.json @@ -966,7 +966,7 @@ "nameOverride": "", "sprite": "sprites/enemy/dragon/ancientsilverdragon.atlas", "deck": [ - "decks/standard/bluedragon.dck" + "decks/miniboss/bluedragon.dck" ], "ai": "", "boss": true, @@ -5195,7 +5195,7 @@ "name": "Chandra", "sprite": "sprites/enemy/humanoid/human/shaman/chandra.atlas", "deck": [ - "decks/boss/Fire of Kaladesh.dck" + "decks/miniboss/Fire of Kaladesh.dck" ], "ai": "", "boss": true, @@ -6456,7 +6456,7 @@ "nameOverride": "", "sprite": "sprites/enemy/humanoid/conjurer.atlas", "deck": [ - "decks/boss/conjure.dck" + "decks/miniboss/conjure.dck" ], "ai": "", "boss": true, @@ -7371,7 +7371,7 @@ "name": "Dark Enchanter", "sprite": "sprites/enemy/humanoid/viashino/dreamwalker.atlas", "deck": [ - "decks/standard/darkenchanter.dck" + "decks/miniboss/darkenchanter.dck" ], "ai": "", "boss": true, @@ -9971,7 +9971,7 @@ "name": "Elesh Norn", "sprite": "sprites/enemy/aberration/phyrexian/eleshnorn.atlas", "deck": [ - "decks/standard/copperhostbrutalizer.dck" + "decks/miniboss/copperhostbrutalizer.dck" ], "ai": "", "boss": true, @@ -12511,7 +12511,7 @@ "name": "Garruk", "sprite": "sprites/enemy/humanoid/human/shaman/garruk.atlas", "deck": [ - "decks/boss/garruk.dck" + "decks/miniboss/garruk.dck" ], "ai": "", "boss": true, @@ -15257,7 +15257,7 @@ "name": "Grolnok", "sprite": "sprites/enemy/aberration/gianttoad.atlas", "deck": [ - "decks/boss/frogboss.dck" + "decks/miniboss/frogboss.dck" ], "ai": "", "boss": true, @@ -15360,9 +15360,10 @@ "name": "Guardian Angel", "sprite": "sprites/enemy/celestial/akroma.atlas", "deck": [ - "decks/boss/angel_rainbow_encounter.dck" + "decks/miniboss/angel_rainbow_encounter.dck" ], "ai": "", + "boss": true, "difficulty": 0.2, "speed": 20, "life": 50, @@ -17500,7 +17501,7 @@ "name": "Jace", "sprite": "sprites/enemy/humanoid/human/wizard/jace.atlas", "deck": [ - "decks/boss/jace.dck" + "decks/miniboss/jace.dck" ], "ai": "", "boss": true, @@ -17645,7 +17646,7 @@ "name": "Jin-Gitaxias", "sprite": "sprites/enemy/aberration/phyrexian/jin-gitaxias.atlas", "deck": [ - "decks/standard/copperhostbrutalizer.dck" + "decks/miniboss/copperhostbrutalizer.dck" ], "ai": "", "spawnRate": 0.1, @@ -18033,7 +18034,7 @@ "name": "Kiora", "sprite": "sprites/enemy/humanoid/merfolk/kiora.atlas", "deck": [ - "decks/boss/kiora.dck" + "decks/miniboss/kiora.dck" ], "ai": "", "boss": true, @@ -18788,7 +18789,7 @@ "name": "Kozilek", "sprite": "sprites/enemy/aberration/eldrazi/kozilek.atlas", "deck": [ - "decks/standard/copperhostbrutalizer.dck" + "decks/miniboss/copperhostbrutalizer.dck" ], "ai": "", "spawnRate": 0.1, @@ -19301,7 +19302,7 @@ "name": "Liliana", "sprite": "sprites/enemy/humanoid/human/wizard/black_wiz3.atlas", "deck": [ - "decks/boss/liliana_zombies.dck" + "decks/miniboss/liliana_zombies.dck" ], "ai": "", "boss": true, @@ -21659,7 +21660,7 @@ "name": "Myr Superion", "sprite": "sprites/enemy/construct/myr.atlas", "deck": [ - "decks/boss/myr_superion.dck" + "decks/miniboss/myr_superion.dck" ], "ai": "", "boss": true, @@ -21798,9 +21799,10 @@ "name": "Nahiri", "sprite": "sprites/enemy/humanoid/kor/nahiri.atlas", "deck": [ - "decks/boss/nahiri.dck" + "decks/miniboss/nahiri.dck" ], "ai": "", + "boss": true, "spawnRate": 1, "difficulty": 0.1, "speed": 30, @@ -22343,7 +22345,7 @@ "name": "Nissa", "sprite": "sprites/enemy/humanoid/elf/nissa.atlas", "deck": [ - "decks/standard/copperhostbrutalizer.dck" + "decks/miniboss/copperhostbrutalizer.dck" ], "ai": "", "spawnRate": 0.1, @@ -22558,7 +22560,7 @@ "name": "Ooze Boss", "sprite": "sprites/enemy/ooze/giant_slime.atlas", "deck": [ - "decks/boss/ooze_boss.dck" + "decks/miniboss/ooze_boss.dck" ], "ai": "", "boss": true, @@ -25598,7 +25600,7 @@ "name": "Scarecrow Captain", "sprite": "sprites/enemy/construct/scarecrow.atlas", "deck": [ - "decks/standard/scarecrowcaptain.dck" + "decks/miniboss/scarecrowcaptain.dck" ], "ai": "", "boss": true, @@ -26318,7 +26320,7 @@ "nameOverride": "Fungus of Slimefoot", "sprite": "sprites/enemy/plant/slimefoot.atlas", "deck": [ - "decks/boss/slimefoot.dck" + "decks/miniboss/slimefoot.dck" ], "ai": "", "boss": true, @@ -26499,7 +26501,7 @@ "name": "Sliver Queen", "sprite": "sprites/enemy/aberration/sliver_queen.atlas", "deck": [ - "decks/boss/sliver_queen.dck" + "decks/miniboss/sliver_queen.dck" ], "ai": "", "boss": true, @@ -26830,7 +26832,7 @@ "name": "Slobad", "sprite": "sprites/enemy/humanoid/goblin/slobad.atlas", "deck": [ - "decks/boss/slobad.dck" + "decks/miniboss/slobad.dck" ], "ai": "", "boss": true, @@ -27110,7 +27112,7 @@ "name": "Sorin", "sprite": "sprites/enemy/undead/sorin.atlas", "deck": [ - "decks/boss/sorin.dck" + "decks/miniboss/sorin.dck" ], "ai": "", "boss": true, @@ -28113,9 +28115,10 @@ "name": "Teferi", "sprite": "sprites/enemy/humanoid/human/wizard/teferi.atlas", "deck": [ - "decks/boss/teferi_boss.dck" + "decks/miniboss/teferi_boss.dck" ], "ai": "", + "boss": true, "spawnRate": 1, "difficulty": 0.8, "speed": 31, @@ -28200,7 +28203,7 @@ "nameOverride": "Zedruu's Cook", "sprite": "sprites/enemy/humanoid/halfling/korred.atlas", "deck": [ - "decks/boss/thecheff.dck" + "decks/miniboss/thecheff.dck" ], "ai": "", "boss": true, @@ -28325,7 +28328,7 @@ "name": "The Hydra of Shandalaar", "sprite": "sprites/enemy/monstrosity/hydra_large.atlas", "deck": [ - "decks/standard/hydraboss.dck" + "decks/miniboss/hydraboss.dck" ], "ai": "", "boss": true, @@ -28371,7 +28374,7 @@ "name": "Tibalt", "sprite": "sprites/enemy/fiend/tibalt.atlas", "deck": [ - "decks/boss/tibalt.dck" + "decks/miniboss/tibalt.dck" ], "ai": "", "boss": true, @@ -28615,9 +28618,10 @@ "name": "Torturer", "sprite": "sprites/enemy/humanoid/human/bandit/bandittorturer.atlas", "deck": [ - "decks/boss/torturer.dck" + "decks/miniboss/torturer.dck" ], "ai": "", + "boss": true, "spawnRate": 1, "difficulty": 0.1, "speed": 20, @@ -29504,7 +29508,7 @@ "name": "Urabrask", "sprite": "sprites/enemy/aberration/phyrexian/urabrask.atlas", "deck": [ - "decks/standard/copperhostbrutalizer.dck" + "decks/miniboss/copperhostbrutalizer.dck" ], "ai": "", "boss": true, @@ -29587,9 +29591,10 @@ "name": "Valyx Feaster of Torment", "sprite": "sprites/enemy/fiend/valyx.atlas", "deck": [ - "decks/boss/valyx.dck" + "decks/miniboss/valyx.dck" ], "ai": "", + "boss": true, "spawnRate": 1, "difficulty": 0.1, "speed": 31, @@ -30052,7 +30057,7 @@ "name": "Vorinclex", "sprite": "sprites/enemy/aberration/phyrexian/vorinclex.atlas", "deck": [ - "decks/standard/copperhostbrutalizer.dck" + "decks/miniboss/copperhostbrutalizer.dck" ], "ai": "", "spawnRate": 0.1, @@ -30256,7 +30261,7 @@ "name": "Walter, the Stray Camel", "sprite": "sprites/enemy/beast/largemammals/camel.atlas", "deck": [ - "decks/boss/walter.dck" + "decks/miniboss/walter.dck" ], "ai": "", "boss": true, @@ -31315,7 +31320,7 @@ "name": "Xira", "sprite": "sprites/enemy/fey/xira.atlas", "deck": [ - "decks/boss/xira.dck" + "decks/miniboss/xira.dck" ], "ai": "", "boss": true, @@ -31502,7 +31507,7 @@ "name": "Zedruu", "sprite": "sprites/enemy/humanoid/minotaur/zedru.atlas", "deck": [ - "decks/boss/zedruubis.dck" + "decks/miniboss/zedruubis.dck" ], "ai": "", "boss": true, diff --git a/forge-gui/res/adventure/common/world/items.json b/forge-gui/res/adventure/common/world/items.json index 7b55e0fd4148..9fba59a1d01d 100644 --- a/forge-gui/res/adventure/common/world/items.json +++ b/forge-gui/res/adventure/common/world/items.json @@ -365,6 +365,7 @@ "description": "Draft a Chandra themed card that can be cast with mana of any color", "equipmentSlot": "Left", "iconName": "ChandrasTome", + "cost": 8000, "effect": { "startBattleWithCardInCommandZone": [ "Chandra's Tome" @@ -376,6 +377,7 @@ "description": "Draft a Phoenix card, or conjure one to be cast with mana of any color", "equipmentSlot": "Neck", "iconName": "PhoenixCharm", + "cost": 8000, "effect": { "startBattleWithCardInCommandZone": [ "Phoenix Charm" @@ -408,6 +410,7 @@ "description": "Sign away your soul to know what the future holds", "equipmentSlot": "Right", "iconName": "DemonicContract", + "cost": 15000, "effect": { "startBattleWithCardInCommandZone": [ "Demonic Contract" @@ -473,6 +476,7 @@ "name": "Sol Ring", "equipmentSlot": "Left", "iconName": "SolRing", + "cost": 25000, "effect": { "startBattleWithCard": [ "Sol Ring" @@ -483,6 +487,7 @@ "name": "Mox Emerald", "equipmentSlot": "Neck", "iconName": "MoxEmerald", + "cost": 25000, "effect": { "startBattleWithCard": [ "Mox Emerald" @@ -493,6 +498,7 @@ "name": "Black Lotus", "equipmentSlot": "Right", "iconName": "BlackLotus", + "cost": 30000, "effect": { "startBattleWithCard": [ "Black Lotus" @@ -503,6 +509,7 @@ "name": "Mox Jet", "equipmentSlot": "Neck", "iconName": "MoxJet", + "cost": 25000, "effect": { "startBattleWithCard": [ "Mox Jet" @@ -513,6 +520,7 @@ "name": "Mox Pearl", "equipmentSlot": "Neck", "iconName": "MoxPearl", + "cost": 25000, "effect": { "startBattleWithCard": [ "Mox Pearl" @@ -523,6 +531,7 @@ "name": "Mox Ruby", "equipmentSlot": "Neck", "iconName": "MoxRuby", + "cost": 25000, "effect": { "startBattleWithCard": [ "Mox Ruby" @@ -533,6 +542,7 @@ "name": "Mox Sapphire", "equipmentSlot": "Neck", "iconName": "MoxSapphire", + "cost": 25000, "effect": { "startBattleWithCard": [ "Mox Sapphire" @@ -543,6 +553,7 @@ "name": "Battle Standard", "equipmentSlot": "Left", "iconName": "BattleStandard", + "cost": 1500, "effect": { "lifeModifier": -1, "startBattleWithCard": [ @@ -554,6 +565,7 @@ "name": "Hivestone", "equipmentSlot": "Left", "iconName": "Hivestone", + "cost": 4000, "effect": { "startBattleWithCard": [ "Hivestone" @@ -738,6 +750,7 @@ "name": "Armor of the Hivelord", "equipmentSlot": "Body", "iconName": "HiveLordArmor", + "cost": 20000, "effect": { "lifeModifier": 5, "changeStartCards": 1 @@ -747,6 +760,7 @@ "name": "Leather Boots", "equipmentSlot": "Boots", "iconName": "LeatherBoots", + "cost": 500, "effect": { "moveSpeed": 1.15 } @@ -766,6 +780,7 @@ "name": "Sorin's Amulet", "equipmentSlot": "Neck", "iconName": "SorinAmulet", + "cost": 12000, "effect": { "lifeModifier": 2, "startBattleWithCardInCommandZone": [ @@ -788,6 +803,7 @@ "name": "Cheat", "equipmentSlot": "Neck", "iconName": "Goose", + "cost": 50000, "effect": { "startBattleWithCard": [ "Blightsteel Colossus", @@ -820,6 +836,7 @@ "name": "Cursed Ring", "equipmentSlot": "Right", "iconName": "CursedRing", + "cost": 4000, "effect": { "startBattleWithCard": [ "c_0_1_a_goblin_construct_noblock_ping", @@ -863,6 +880,7 @@ "name": "Presence of the Hydra", "equipmentSlot": "Left", "iconName": "PresenceoftheHydra", + "cost": 4000, "effect": { "lifeModifier": 2, "startBattleWithCardInCommandZone": [ @@ -1000,6 +1018,7 @@ "description": "4/2 dinosaur", "equipmentSlot": "Boots", "iconName": "RaptorBondband", + "cost": 4000, "effect": { "lifeModifier": -3, "startBattleWithCard": [ @@ -1012,6 +1031,7 @@ "description": "Filter mana or pay 2 for 3 life", "equipmentSlot": "Left", "iconName": "GoldenEgg", + "cost": 2000, "effect": { "startBattleWithCard": [ "Golden Egg|ELD" @@ -1056,6 +1076,7 @@ "name": "Warren Tender's Baton", "equipmentSlot": "Left", "iconName": "WarrenTendersBaton", + "cost": 3000, "effect": { "lifeModifier": 1, "startBattleWithCard": [ @@ -1069,6 +1090,7 @@ "description": "3R for 5 damage to a player", "equipmentSlot": "Left", "iconName": "PrayerbookofIre", + "cost": 3000, "effect": { "startBattleWithCard": [ "Font of Ire|JOU" @@ -1088,6 +1110,7 @@ "name": "Robes of Omniscience", "equipmentSlot": "Body", "iconName": "RobesOfOmniscience", + "cost": 10000, "effect": { "lifeModifier": -6, "changeStartCards": 2 @@ -1099,6 +1122,7 @@ "description": "Equip 4, +2/+2 and flying and become an angel", "equipmentSlot": "Right", "iconName": "AngelicArmaments", + "cost": 10000, "effect": { "lifeModifier": 4, "startBattleWithCard": [ @@ -1111,6 +1135,7 @@ "description": "0/4 wall with flying", "equipmentSlot": "Boots", "iconName": "AngelicGreaves", + "cost": 8000, "effect": { "lifeModifier": 2, "startBattleWithCard": [ @@ -1122,6 +1147,7 @@ "name": "Sandals", "equipmentSlot": "Boots", "iconName": "Sandals", + "cost": 100, "effect": { "moveSpeed": 1.1 } @@ -1261,6 +1287,7 @@ "name": "Mad Staff", "equipmentSlot": "Left", "iconName": "MadStaff", + "cost": 4000, "effect": { "startBattleWithCard": [ "Power Struggle" @@ -1304,6 +1331,7 @@ "name": "Entrancing Lyre", "equipmentSlot": "Right", "iconName": "EntrancingLyre", + "cost": 6000, "effect": { "startBattleWithCard": [ "Entrancing Lyre" @@ -1391,6 +1419,7 @@ "name": "Amulet of Kroog", "equipmentSlot": "Neck", "iconName": "AmuletofKroog", + "cost": 3000, "effect": { "startBattleWithCard": [ "Amulet of Kroog" @@ -1434,6 +1463,7 @@ "name": "Jinxed Ring", "equipmentSlot": "Right", "iconName": "JinxedRing", + "cost": 2500, "effect": { "opponent": { "startBattleWithCard": [ @@ -1446,6 +1476,7 @@ "name": "Nine-Ringed Bo", "equipmentSlot": "Left", "iconName": "Nine-RingedBo", + "cost": 1000, "effect": { "startBattleWithCard": [ "Nine-Ringed Bo" @@ -1456,6 +1487,7 @@ "name": "Ring of Immortals", "equipmentSlot": "Right", "iconName": "RingofImmortals", + "cost": 4000, "effect": { "startBattleWithCard": [ "Ring of Immortals" @@ -1477,6 +1509,7 @@ "name": "Ring of Renewal", "equipmentSlot": "Right", "iconName": "RingofRenewal", + "cost": 5000, "effect": { "startBattleWithCard": [ "Ring of Renewal" @@ -1513,7 +1546,8 @@ "colorView": true }, "description": "Grants Manasight, letting you know the colors used by your adversaries", - "iconName": "RelicAmulet" + "iconName": "RelicAmulet", + "cost": 4000 }, { "name": "Lightbringers Boots", @@ -1539,7 +1573,8 @@ "cardRewardBonus": 1 }, "description": "", - "iconName": "FortuneCoin" + "iconName": "FortuneCoin", + "cost": 10000 }, { "name": "Colorless rune", @@ -1694,6 +1729,7 @@ "name": "Slimefoot's Slimy Staff", "equipmentSlot": "Left", "iconName": "GreenStaff", + "cost": 8000, "effect": { "lifeModifier": 2, "startBattleWithCardInCommandZone": [ @@ -1705,6 +1741,7 @@ "name": "Kiora's Bident", "equipmentSlot": "Left", "iconName": "SleepWand", + "cost": 10000, "effect": { "lifeModifier": -1, "startBattleWithCard": [ @@ -1719,6 +1756,7 @@ "name": "Slime-Covered Boots", "equipmentSlot": "Boots", "iconName": "SteelBoots", + "cost": 6000, "effect": { "lifeModifier": -1, "moveSpeed": 1.20, @@ -1731,6 +1769,7 @@ "name": "Amulet of the Deceiver", "equipmentSlot": "Neck", "iconName": "MoxJet", + "cost": 10000, "effect": { "lifeModifier": -1, "moveSpeed": 1.20, @@ -1748,6 +1787,7 @@ "name": "Jace's Signature Hoodie", "equipmentSlot": "Body", "iconName": "BlueRobes", + "cost": 12000, "effect": { "lifeModifier": -1, "cardRewardBonus": 1, @@ -1760,6 +1800,7 @@ "name": "Teferi's Staff", "equipmentSlot": "Left", "iconName": "MadStaff", + "cost": 12000, "effect": { "lifeModifier": 1, "cardRewardBonus": 1, @@ -1772,6 +1813,7 @@ "name": "Garruk's Mighty Axe", "equipmentSlot": "Left", "iconName": "GarrukAxe", + "cost": 12000, "effect": { "lifeModifier": 3, "startBattleWithCardInCommandZone": [ @@ -1783,6 +1825,7 @@ "name": "Nahiri's Armory", "equipmentSlot": "Right", "iconName": "Armory", + "cost": 12000, "effect": { "lifeModifier": 3, "startBattleWithCardInCommandZone": [ @@ -1794,6 +1837,7 @@ "name": "Giant Scythe", "equipmentSlot": "Left", "iconName": "Scythe", + "cost": 6000, "effect": { "lifeModifier": 1, "startBattleWithCardInCommandZone": [ @@ -1805,6 +1849,7 @@ "name": "Chicken Egg", "equipmentSlot": "Right", "iconName": "ChickenEgg", + "cost": 3000, "effect": { "lifeModifier": 1, "startBattleWithCard": [ @@ -1816,6 +1861,7 @@ "name": "Tibalt's Bag of Tricks", "equipmentSlot": "Left", "iconName": "BurningBook", + "cost": 6000, "effect": { "lifeModifier": 1, "startBattleWithCardInCommandZone": [ @@ -1827,6 +1873,7 @@ "name": "Xira's Fancy Hat", "equipmentSlot": "Neck", "iconName": "BrownHat", + "cost": 10000, "effect": { "lifeModifier": 2, "startBattleWithCard": [ @@ -1838,6 +1885,7 @@ "name": "The Underworld Cookbook", "equipmentSlot": "Left", "iconName": "UnderworldCookbook", + "cost": 4000, "effect": { "startBattleWithCard": [ "The Underworld Cookbook" @@ -1848,6 +1896,7 @@ "name": "Mantle of Ancient Lore", "equipmentSlot": "Body", "iconName": "Conjurer's Mantle", + "cost": 5000, "effect": { "lifeModifier": 1, "startBattleWithCardInCommandZone": [ @@ -1859,6 +1908,7 @@ "name": "Zedruu's Lantern", "equipmentSlot": "Left", "iconName": "ZedruuLantern", + "cost": 8000, "effect": { "lifeModifier": 1, "startBattleWithCardInCommandZone": [ @@ -1870,6 +1920,7 @@ "name": "Grolnok's Skin", "equipmentSlot": "Right", "iconName": "FrogSkin", + "cost": 10000, "effect": { "lifeModifier": 3, "startBattleWithCardInCommandZone": [ @@ -1881,6 +1932,7 @@ "name": "Slobad's Iron Boots", "equipmentSlot": "Boots", "iconName": "MetallicBoots", + "cost": 10000, "effect": { "lifeModifier": 1, "moveSpeed": 1.35, @@ -1894,6 +1946,7 @@ "description": "Give a creature hexproof until end of turn", "equipmentSlot": "Neck", "iconName": "HallowedSigil", + "cost": 6000, "effect": { "startBattleWithCard": [ "Hallowed Sigil" @@ -1905,6 +1958,7 @@ "description": "Devour the life of an enemy creature, killing it", "equipmentSlot": "Right", "iconName": "UnhallowedSigil", + "cost": 6000, "effect": { "startBattleWithCard": [ "Sigil of Torment" diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index e7f9535f8fab..85ca1c4c8f3c 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -894,6 +894,7 @@ lblRedcards=Rote Karten lblGreencards=Grüne Karten lblColorlesscards=Farblose Karten lblMulticolorcards=Mehrfarbige Karten +lblLockedCards=Gesperrte Karten lblPackordeck=Kartenpacks und vorkonstruierte Decks lblLands=Länder lblArtifacts=Artefakte @@ -1543,6 +1544,7 @@ lblLoadingNewDraft=Lade neuen Draft... #LoadDraftScreen.java lblDoubleTapToEditDeck=Doppelklick zum Bearbeiten. Lange drücken für Anzeige. lblMode=Modus: +lblArchipelagoMode=Zufallsgenerator: lblYouMustSelectExistingDeck=Du mußt eine bestehendes Deck wählen oder ein neues Draft-Deck erstellen. lblWhichOpponentWouldYouLikeToFace=Wähle deinen Gegner! lblSingleMatch=Einzelnes Spiel diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 8534573c2cee..8844ba890436 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -976,6 +976,7 @@ lblRedcards=Red cards lblGreencards=Green cards lblColorlesscards=Colorless cards lblMulticolorcards=Multicolor cards +lblLockedCards=Locked cards lblPackordeck=Card packs and prebuilt decks lblLands=Lands lblArtifacts=Artifacts @@ -1745,6 +1746,7 @@ lblLoadingNewDraft=Loading new draft... #LoadDraftScreen.java lblDoubleTapToEditDeck=Double-tap to edit deck (Long-press to view) lblMode=Mode: +lblArchipelagoMode=Randomizer: lblYouMustSelectExistingDeck=You must select an existing deck or build a deck from a new booster draft game. lblWhichOpponentWouldYouLikeToFace=Which opponent would you like to face? lblSingleMatch=Single Match diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index 501ceb406956..49d3ae4a57bf 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -872,6 +872,7 @@ lblRedcards=Cartas rojas lblGreencards=Cartas verdes lblColorlesscards=Cartas incoloras lblMulticolorcards=Cartas multicolor +lblLockedCards=Tarjetas bloqueadas lblPackordeck=Pack de cartas y mazos Preconstruidos lblLands=Tierras lblArtifacts=Artefactos @@ -1525,6 +1526,7 @@ lblLoadingNewDraft=Cargando nuevo Draft... #LoadDraftScreen.java lblDoubleTapToEditDeck=Pulsa 2 veces para editar el mazo (Pulsación prologanda para verlo) lblMode=Modo: +lblArchipelagoMode=Aleatorizador: lblYouMustSelectExistingDeck=Debes seleccionar un mazo existente o construir un mazo a partir de un nuevo juego de booster draft. lblWhichOpponentWouldYouLikeToFace=¿A qué oponente te gustaría enfrentarte? lblSingleMatch=Partida individual diff --git a/forge-gui/res/languages/fr-FR.properties b/forge-gui/res/languages/fr-FR.properties index 09e81c2b3a00..5a86bcb8ec05 100644 --- a/forge-gui/res/languages/fr-FR.properties +++ b/forge-gui/res/languages/fr-FR.properties @@ -871,6 +871,7 @@ lblRedcards=Cartons rouges lblGreencards=Cartes vertes lblColorlesscards=Cartes incolores lblMulticolorcards=Cartes multicolores +lblLockedCards=Cartes verrouilles lblPackordeck=Packs de cartes et decks préconstruits lblLands=Terrains lblArtifacts=Artefacts @@ -1520,6 +1521,7 @@ lblLoadingNewDraft=Chargement du nouveau brouillon... #LoadDraftScreen.java lblDoubleTapToEditDeck=Appuyez deux fois pour modifier le deck (appuyez longuement pour afficher) lblMode=Mode : +lblArchipelagoMode=Gnrateur alatoire : lblYouMustSelectExistingDeck=Vous devez sélectionner un deck existant ou construire un deck à partir d'un nouveau booster draft. lblWhichOpponentWouldYouLikeToFace=Quel adversaire aimeriez-vous affronter ? lblSingleMatch=Correspondance unique diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index 2da51f819a82..9aaab28a45a4 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -869,6 +869,7 @@ lblRedcards=Carte rosse lblGreencards=Carte verdi lblColorlesscards=Carte incolore lblMulticolorcards=Carte multicolore +lblLockedCards=Carte bloccate lblPackordeck=Pacchetti di carte e mazzi precostruiti lblLands=Terre lblArtifacts=Artefatti @@ -1517,6 +1518,7 @@ lblLoadingNewDraft=Caricamento nuovo Draft... #LoadDraftScreen.java lblDoubleTapToEditDeck=Tocca due volte per modificare il mazzo (premi a lungo per visualizzare) lblMode=Modalità: +lblArchipelagoMode=Generatore casuale: lblYouMustSelectExistingDeck=Devi selezionare un mazzo esistente o costruire un mazzo da una nuova partita Draft. lblWhichOpponentWouldYouLikeToFace=Quale avversario vorresti affrontare? lblSingleMatch=Partita singola diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index ac67478d33d7..258c886e6c45 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -870,6 +870,7 @@ lblRedcards=赤カードのみ表示 lblGreencards=緑カードのみ表示 lblColorlesscards=無色カードのみ表示 lblMulticolorcards=多色カードのみ表示 +lblLockedCards=ロックされたカード lblPackordeck=カードパックと構築済みのデッキ lblLands=土地のみ表示 lblArtifacts=アーティファクトのみ表示 @@ -1518,6 +1519,7 @@ lblLoadingNewDraft=新しいドラフトを読み込んでいます... #LoadDraftScreen.java lblDoubleTapToEditDeck=ダブルタップしてデッキを編集(長押しして表示) lblMode=モード: +lblArchipelagoMode=ランダマイザー: lblYouMustSelectExistingDeck=既存のデッキを選択するか、新しいブースタードラフトゲームからデッキを構築する必要があります。 lblWhichOpponentWouldYouLikeToFace=どの相手と対戦しますか? lblSingleMatch=シングルマッチ diff --git a/forge-gui/res/languages/pt-BR.properties b/forge-gui/res/languages/pt-BR.properties index 0f05face87b3..a94e34c5961a 100644 --- a/forge-gui/res/languages/pt-BR.properties +++ b/forge-gui/res/languages/pt-BR.properties @@ -895,6 +895,7 @@ lblRedcards=Cartas Vermelhas lblGreencards=Cartas Verdes lblColorlesscards=Cartas Incolores lblMulticolorcards=Cartas Multicores +lblLockedCards=Cartas bloqueados lblPackordeck=Pacotes de cartas e decks pré-construído lblLands=Terrenos lblArtifacts=Artefatos @@ -1552,6 +1553,7 @@ lblLoadingNewDraft=Carregando novo draft... #LoadDraftScreen.java lblDoubleTapToEditDeck=Toque duplo para editar deck (Longo para ver) lblMode=Modo\: +lblArchipelagoMode=Aleatorizador\: lblYouMustSelectExistingDeck=Selecione um deck existente ou crie um deck a partir de um novo jogo de draft. lblWhichOpponentWouldYouLikeToFace=Qual adversário você gostaria de encarar? lblSingleMatch=Partida Única diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index b7401bec3744..b3bf7abc7194 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -872,6 +872,7 @@ lblRedcards=红色牌 lblGreencards=绿色牌 lblColorlesscards=无色牌 lblMulticolorcards=多色牌 +lblLockedCards=已锁定的卡片 lblPackordeck=卡包和预组套牌 lblLands=地 lblArtifacts=神器 @@ -1524,6 +1525,7 @@ lblLoadingNewDraft=加载新的轮抽中 #LoadDraftScreen.java lblDoubleTapToEditDeck=双击以编辑套牌(长按可以查看) lblMode=模式: +lblArchipelagoMode=随机模式: lblYouMustSelectExistingDeck=你必须选择一个已有的卡组或者从新的补充包轮抽游戏中构筑一个卡组。 lblWhichOpponentWouldYouLikeToFace=你想面对哪个对手? lblSingleMatch=单场比赛 diff --git a/forge-gui/src/main/java/forge/itemmanager/SFilterUtil.java b/forge-gui/src/main/java/forge/itemmanager/SFilterUtil.java index 3bcd47869269..6e40ccbaf6f5 100644 --- a/forge-gui/src/main/java/forge/itemmanager/SFilterUtil.java +++ b/forge-gui/src/main/java/forge/itemmanager/SFilterUtil.java @@ -431,6 +431,16 @@ public static Predicate buildFoilFilter(Map buildLockedFilter(Map buttonMap) { + return card -> { + if (buttonMap.get(StatTypes.LOCKED).isSelected()) { + return card.isLocked(); + } else { + return !card.isLocked(); + } + }; + } + public static Predicate buildColorFilter(Map buttonMap) { byte colors0 = 0; diff --git a/forge-gui/src/main/java/forge/itemmanager/SItemManagerUtil.java b/forge-gui/src/main/java/forge/itemmanager/SItemManagerUtil.java index 1e338c281cd5..da467f62ccbb 100644 --- a/forge-gui/src/main/java/forge/itemmanager/SItemManagerUtil.java +++ b/forge-gui/src/main/java/forge/itemmanager/SItemManagerUtil.java @@ -74,8 +74,9 @@ public enum StatTypes implements IHasSkinProp { RATE_2 (FSkinProp.IMG_FAV2, null, "lbl2starcards"), RATE_3 (FSkinProp.IMG_FAV3, null, "lbl3starcards"), RATE_4 (FSkinProp.IMG_FAV4, null, "lbl4starcards"), - RATE_5 (FSkinProp.IMG_FAV5, null, "lbl5starcards"); + RATE_5 (FSkinProp.IMG_FAV5, null, "lbl5starcards"), + LOCKED (FSkinProp.ICO_LOCK, null, "lblLockedCards"); public final FSkinProp skinProp; public final Predicate predicate;