diff --git a/forge-core/src/main/java/forge/item/AdventurePaperCard.java b/forge-core/src/main/java/forge/item/AdventurePaperCard.java new file mode 100644 index 000000000000..f186f69c994d --- /dev/null +++ b/forge-core/src/main/java/forge/item/AdventurePaperCard.java @@ -0,0 +1,33 @@ +package forge.item; + +import forge.card.CardRarity; +import forge.card.CardRules; + +/// Extension of the PaperCard class with features specific to Adventure mode so as not to pollute the model used for the classic game mode. +public class AdventurePaperCard extends PaperCard { + private transient boolean isLocked; + + public AdventurePaperCard(CardRules rules0, String edition0, CardRarity rarity0) { + super(rules0, edition0, rarity0); + } + + public AdventurePaperCard(PaperCard copyFrom, PaperCardFlags flags) { + super(copyFrom, flags); + } + + public AdventurePaperCard(CardRules rules0, String edition0, CardRarity rarity0, int artIndex0, boolean foil0, String collectorNumber0, String artist0, String functionalVariant) { + super(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant); + } + + protected AdventurePaperCard(CardRules rules, String edition, CardRarity rarity, int artIndex, boolean foil, String collectorNumber, String artist, String functionalVariant, PaperCardFlags flags) { + super(rules, edition, rarity, artIndex, foil, collectorNumber, artist, functionalVariant, flags); + } + + public boolean isLocked() { + return isLocked; + } + + public void setLocked(boolean locked) { + isLocked = locked; + } +} 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..dffada6417b4 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoCheckTypes.java @@ -0,0 +1,8 @@ +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 +} 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..ae3d4e50fed9 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoColors.java @@ -0,0 +1,8 @@ +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 Salmon = "{COLOR=#FA8072}"; + public static final String Green = "{COLOR=#00b359}"; +} 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..c987b6915863 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoData.java @@ -0,0 +1,614 @@ +package forge.adventure.archipelago; + +import forge.StaticData; +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")); + + // 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) { + 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; + 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) { + 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. + 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 --- + + 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 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(); + // 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"); + } + + 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); + } + + @Override + public SaveFileData save() { + SaveFileData data = new SaveFileData(); + LocalRandomizer localRandomizer = LocalRandomizer.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); + } + + 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..3db18c04d21d --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoMode.java @@ -0,0 +1,6 @@ +package forge.adventure.archipelago; + +public enum ArchipelagoMode { + disabled, + solo_randomizer +} 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..171b756062e7 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/archipelago/ArchipelagoUtil.java @@ -0,0 +1,112 @@ +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; + +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; + } +} 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/character/PlayerSprite.java b/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java index cca0ced5f8c9..2f77d18f77c4 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()); + + // Increase the base speed of the player in AP mode. + 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/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/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..74a6ac9ec02e 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; @@ -23,6 +25,7 @@ import forge.deck.Deck; import forge.deck.DeckProxy; import forge.deck.DeckSection; +import forge.item.AdventurePaperCard; import forge.item.InventoryItem; import forge.item.PaperCard; import forge.sound.SoundEffectType; @@ -157,7 +160,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. @@ -167,11 +170,24 @@ public void create(String n, Deck startingDeck, boolean male, int race, int avat // Sanity Check make sure the number is not insane and make sure it is at least 20 this.maxDeckCount = Math.max(Math.min(this.maxDeckCount, 99), 20); + CardPool pool = new CardPool(); + CardPool playerPool = startingDeck.getAllCardsInASinglePool(true, true); + for (Map.Entry entry : playerPool) { + pool.add(new AdventurePaperCard(entry.getKey(), entry.getKey().getMarkedFlags()), entry.getValue()); + } + + startingDeck.putSection(DeckSection.Main, pool); clearDecks(); // Reset the empty decks to now already have the commander in the command zone. deck = startingDeck; decks.set(0, deck); - cards.addAllFlat(deck.getAllCardsInASinglePool(true, true).toFlatList()); + cards.addAllFlat(pool.toFlatList()); + if (archipelagoMode != ArchipelagoMode.disabled) { + ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); + for (PaperCard card : cards.toFlatList()) { + archipelagoData.addCardUnlockedByName(card.getCardName()); + } + } this.difficultyData.startingLife = difficultyData.startingLife; this.difficultyData.startingMoney = difficultyData.startingMoney; @@ -701,7 +717,12 @@ public void load(SaveFileData data) { // Use false to skip loadout switching during load (equippedItems already loaded correctly above) setSelectedDeckSlot(data.readInt("selectedDeckIndex"), false); CardPool cardPool = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("cards"))); - cards.addAll(cardPool.getFilteredPool(isValid)); + CardPool adventureCardPool = new CardPool(); + CardPool playerPool = cardPool.getFilteredPool(isValid); + for (int i = 0; i < playerPool.countAll(); i++) { + adventureCardPool.add(new AdventurePaperCard(playerPool.get(i), playerPool.get(i).getMarkedFlags())); + } + cards.addAll(adventureCardPool); unsupportedCards.addAll(cardPool.getFilteredPool(isUnsupported).toFlatList()); if (data.containsKey("newCards")) { @@ -738,7 +759,7 @@ public void load(SaveFileData data) { int remainingSellableCopies = totalCopies - noSellCopies; - PaperCard noSellVersion = item.getNoSellVersion(); + AdventurePaperCard noSellVersion = new AdventurePaperCard(item.getNoSellVersion(), item.getNoSellVersion().getMarkedFlags()); cards.add(noSellVersion, noSellCopies); System.out.printf("Converted legacy noSellCards item - %s (%d / %d copies)%n", item, noSellCopies, totalCopies); @@ -763,19 +784,25 @@ public void load(SaveFileData data) { if (data.containsKey("autoSellCards")) { PaperCard[] items = (PaperCard[]) data.readObject("autoSellCards"); for (PaperCard pc : items) { - if (isUnsupported.test(pc)) - unsupportedCards.add(pc); + AdventurePaperCard apc = (pc instanceof AdventurePaperCard) + ? (AdventurePaperCard) pc + : new AdventurePaperCard(pc, pc.getMarkedFlags()); + if (isUnsupported.test(apc)) + unsupportedCards.add(apc); else - autoSellCards.add(pc); + autoSellCards.add(apc); } } if (data.containsKey("favoriteCards")) { PaperCard[] items = (PaperCard[]) data.readObject("favoriteCards"); for (PaperCard pc : items) { - if (isUnsupported.test(pc)) - unsupportedCards.add(pc); + AdventurePaperCard fapc = (pc instanceof AdventurePaperCard) + ? (AdventurePaperCard) pc + : new AdventurePaperCard(pc, pc.getMarkedFlags()); + if (isUnsupported.test(fapc)) + unsupportedCards.add(fapc); else - favoriteCards.add(pc); + favoriteCards.add(fapc); } } @@ -963,35 +990,43 @@ public void addCard(PaperCard card) { } public void addCard(PaperCard card, int amount) { - cards.add(card, amount); - newCards.add(card, amount); + AdventurePaperCard adventurePaperCard = new AdventurePaperCard(card, card.getMarkedFlags()); + cards.add(adventurePaperCard, amount); + newCards.add(adventurePaperCard, amount); } public void addCards(ItemPool cardPool) { - cards.addAll(cardPool); - newCards.addAll(cardPool); + for (Map.Entry p : cardPool) { + cards.add(new AdventurePaperCard(p.getKey(), p.getKey().getMarkedFlags()), p.getValue()); + newCards.add(new AdventurePaperCard(p.getKey(), p.getKey().getMarkedFlags()), p.getValue()); + } } public void addReward(Reward reward) { + ArchipelagoData archipelagoData = ArchipelagoData.getInstance(); switch (reward.getType()) { case Card: - cards.add(reward.getCard()); - newCards.add(reward.getCard()); + AdventurePaperCard adventurePaperCard = new AdventurePaperCard(reward.getCard(), reward.getCard().getMarkedFlags()); + cards.add(adventurePaperCard); + newCards.add(adventurePaperCard); if (reward.isAutoSell()) { - autoSellCards.add(reward.getCard()); + autoSellCards.add(adventurePaperCard); 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 +1046,7 @@ private void refreshEditor() { private void addGold(int goldCount) { gold += goldCount; + ArchipelagoData.getInstance().addGold(goldCount); onGoldChangeList.emit(); } @@ -1102,6 +1138,7 @@ public void win() { public void addMaxLife(int count) { maxLife += count; life += count; + ArchipelagoData.getInstance().addMaxLife(count); onLifeTotalChangeList.emit(); } @@ -1117,6 +1154,7 @@ public void takeGold(int price) { } public void addShards(int number) { + ArchipelagoData.getInstance().addShards(number); takeShards(-number); } @@ -1402,6 +1440,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); @@ -1602,7 +1641,13 @@ public void removeEvent(AdventureEventData completedEvent) { } public ItemPool getAutoSellCards() { - return autoSellCards; + ItemPool result = new ItemPool<>(PaperCard.class); + + for (Map.Entry e : autoSellCards) { + result.add(e.getKey(), e.getValue()); + } + + return result; } /** diff --git a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java index 1aeea83d3e82..a77c5c2f2008 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; @@ -22,10 +24,12 @@ import forge.game.GameType; import forge.gamemodes.limited.BoosterDraft; import forge.gui.FThreads; +import forge.item.AdventurePaperCard; import forge.item.InventoryItem; 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 +94,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) { + ((AdventurePaperCard)card.getKey()).setLocked(!apData.checkCardUnlocked(card.getKey())); + } + } return pool; } @@ -976,6 +986,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/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/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 f964aa82c84e..8b93eedabc0c 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,51 @@ 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); + + // Check if the UI has Archipelago elements loaded + if (enableArchipelago != null) { + isArchipelagoSupported = true; + enableArchipelago.setTextList(new String[]{"Disabled", "Enabled"}); + 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 +401,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 +423,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"))); @@ -457,6 +543,33 @@ 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; + 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..e89a1c0c2137 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java @@ -13,6 +13,7 @@ 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; @@ -321,6 +322,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<>(); @@ -534,7 +536,14 @@ 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) { + if (type == Type.Loot && reward.getType() == Reward.Type.Item && reward.getItem().equipmentSlot != null && !reward.getItem().equipmentSlot.isEmpty()) { + reward = LocalRandomizer.getInstance().takeSingleEquipmentOutOfRemainingPool(); + } + } + 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); 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..8e205eb93a17 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,9 +66,9 @@ 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; @@ -581,7 +574,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"); @@ -1093,13 +1086,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,21 +1112,25 @@ 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); diff --git a/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java b/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java index 941a367d5dea..77d81c90a964 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java +++ b/forge-gui-mobile/src/forge/adventure/stage/IAfterMatch.java @@ -1,5 +1,12 @@ 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 && winner) { + ArchipelagoData.getInstance().addTotalBattlesWon(1); + } + } } diff --git a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java index 276c8597f327..8ed9bd93c119 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; @@ -557,7 +558,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 in AP mode. + mob.speedModifier *= 1.15f; + } enemies.add(mob); addMapActor(obj, mob); } @@ -718,8 +722,32 @@ 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. + // Todo: Since we're storing the equipment in a Set object, the order isn't guaranteed so we have to deploy a similar system to the Archipelago shop system or store the data as arraylists. + 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; } ShopActor actor = new ShopActor(this, id, ret, data); addMapActor(obj, actor); diff --git a/forge-gui-mobile/src/forge/adventure/util/MapDialog.java b/forge-gui-mobile/src/forge/adventure/util/MapDialog.java index 43b7e1f70905..13a390ccfadd 100644 --- a/forge-gui-mobile/src/forge/adventure/util/MapDialog.java +++ b/forge-gui-mobile/src/forge/adventure/util/MapDialog.java @@ -19,6 +19,7 @@ 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; @@ -327,16 +328,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) { diff --git a/forge-gui-mobile/src/forge/adventure/util/RewardActor.java b/forge-gui-mobile/src/forge/adventure/util/RewardActor.java index 6faa9d19da5c..919bd4bf2376 100644 --- a/forge-gui-mobile/src/forge/adventure/util/RewardActor.java +++ b/forge-gui-mobile/src/forge/adventure/util/RewardActor.java @@ -34,6 +34,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; @@ -108,6 +110,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() { @@ -827,8 +830,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); @@ -1192,6 +1203,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/WorldSave.java b/forge-gui-mobile/src/forge/adventure/world/WorldSave.java index 4119f15cf9ee..6355ac507243 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,24 @@ 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) { + // Initial archipelago setup + if (archipelagoMode == ArchipelagoMode.solo_randomizer) { + LocalRandomizer.getInstance().setupFreshSaveFile(); + } else { + ArchipelagoData.getInstance().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 +185,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 +205,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/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index 8a85495a3eef..a2a81e09f01b 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; @@ -69,6 +72,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() { @@ -505,12 +510,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(); @@ -533,6 +545,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); + } } } @@ -626,6 +641,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); @@ -664,6 +686,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..157cbcf0882b --- /dev/null +++ b/forge-gui-mobile/src/forge/itemmanager/filters/CardLockFilter.java @@ -0,0 +1,31 @@ +package forge.itemmanager.filters; + +import forge.item.AdventurePaperCard; +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/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/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 fc4082cc5586..662cabf5b955 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -978,6 +978,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 @@ -1747,6 +1748,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 d54615883a9c..3e51e4b510ef 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..822f86dbd284 100644 --- a/forge-gui/src/main/java/forge/itemmanager/SFilterUtil.java +++ b/forge-gui/src/main/java/forge/itemmanager/SFilterUtil.java @@ -17,6 +17,7 @@ import forge.game.GameFormat; import forge.gamemodes.quest.data.StarRating; import forge.gui.interfaces.IButton; +import forge.item.AdventurePaperCard; import forge.item.InventoryItem; import forge.item.PaperCard; import forge.item.PaperCardPredicates; @@ -431,6 +432,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;