Merge pull request #8500 from Jetz72/setEventCommand

Refactor event generation, add `set event` command.
This commit is contained in:
Jetz72
2025-10-16 10:24:20 -04:00
committed by GitHub
6 changed files with 196 additions and 131 deletions

View File

@@ -9,9 +9,7 @@ import forge.util.storage.StorageExtendable;
import forge.util.storage.StorageReaderFileSections;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Predicate;
@@ -93,19 +91,6 @@ public class PrintSheet {
return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip
}
public boolean containsCardNamed(String name,int atLeast) {
int count=0;
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
for (int i = 0; i < kv.getValue(); i++) {
if(kv.getKey().getName().equals(name))
{
count++;
if(count>=atLeast)return true;
}
}
}
return false;
}
public String getName() {
return name;
}
@@ -147,6 +132,10 @@ public class PrintSheet {
return cardsWithWeights.toFlatList();
}
public Map<String, Integer> toNameLookup() {
return cardsWithWeights.toNameLookup();
}
public static class Reader extends StorageReaderFileSections<PrintSheet> {
public Reader(File file) {
super(file, PrintSheet::getName);

View File

@@ -23,14 +23,15 @@ import forge.util.Aggregates;
import forge.util.IterableUtil;
import forge.util.MyRandom;
import forge.util.StreamUtil;
import org.apache.commons.lang3.tuple.Pair;
import java.io.Serial;
import java.io.Serializable;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class AdventureEventData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private static final int JUMPSTART_TO_PICK_FROM = 6;
public transient BoosterDraft draft;
@@ -69,6 +70,7 @@ public class AdventureEventData implements Serializable {
style = other.style;
random.setSeed(eventSeed);
eventStatus = other.eventStatus;
format = other.format;
registeredDeck = other.registeredDeck;
isDraftComplete = other.isDraftComplete;
description = other.description;
@@ -94,28 +96,39 @@ public class AdventureEventData implements Serializable {
}
public AdventureEventData(Long seed, AdventureEventController.EventFormat selectedFormat) {
this(seed, selectedFormat, null, pickCardBlockByFormat(selectedFormat));
}
public AdventureEventData(Long seed, AdventureEventController.EventFormat selectedFormat, CardBlock cardBlock) {
this(seed, selectedFormat, null, cardBlock);
}
public AdventureEventData(Long seed, AdventureEventController.EventFormat selectedFormat, AdventureEventController.EventStyle style, CardBlock cardBlock) {
setEventSeed(seed);
eventStatus = AdventureEventController.EventStatus.Available;
registeredDeck = new Deck();
format = selectedFormat;
this.cardBlock = cardBlock;
if (cardBlock == null)
return;
cardBlockName = cardBlock.getName();
if (format == AdventureEventController.EventFormat.Draft) {
cardBlock = pickWeightedCardBlock();
if (cardBlock == null)
return;
cardBlockName = cardBlock.getName();
setupDraftRewards();
} else if (format == AdventureEventController.EventFormat.Jumpstart) {
cardBlock = pickJumpstartCardBlock();
if (cardBlock == null)
return;
cardBlockName = cardBlock.getName();
jumpstartBoosters = AdventureEventController.instance().getJumpstartBoosters(cardBlock, JUMPSTART_TO_PICK_FROM);
packConfiguration = new String[]{cardBlock.getLandSet().getCode(), cardBlock.getLandSet().getCode(), cardBlock.getLandSet().getCode()};
setupJumpstartRewards();
}
if(style == null) {
// If the chosen event seed recommends a four-person pod, run it as a RoundRobin
if (getRecommendedPodSize(cardBlock) == 4)
style = AdventureEventController.EventStyle.RoundRobin;
else
style = AdventureEventController.EventStyle.Bracket;
}
this.style = style;
}
public void setEventSeed(long seed) {
@@ -147,6 +160,15 @@ public class AdventureEventData implements Serializable {
return draft;
}
private static CardBlock pickCardBlockByFormat(AdventureEventController.EventFormat format) {
return switch (format) {
case Draft -> pickWeightedCardBlock();
case Jumpstart -> pickJumpstartCardBlock();
case Constructed -> null;
case Sealed -> null;
};
}
private static final Predicate<CardEdition> filterPioneer = FModel.getFormats().getPioneer().editionLegalPredicate;
private static final Predicate<CardEdition> filterModern = FModel.getFormats().getModern().editionLegalPredicate;
private static final Predicate<CardEdition> filterVintage = FModel.getFormats().getVintage().editionLegalPredicate;
@@ -168,10 +190,11 @@ public class AdventureEventData implements Serializable {
return rolledFilter;
}
private CardBlock pickWeightedCardBlock() {
private static final Set<String> POWER_NINE = Set.of("Black Lotus", "Mox Emerald", "Mox Pearl", "Mox Ruby", "Mox Sapphire", "Mox Jet", "Ancestral Recall", "Timetwister", "Time Walk");
private static CardBlock pickWeightedCardBlock() {
CardEdition.Collection editions = FModel.getMagicDb().getEditions();
ConfigData configData = Config.instance().getConfigData();
Iterable<CardBlock> src = FModel.getBlocks(); //all blocks
Predicate<CardEdition> filter = CardEdition.Predicates.CAN_MAKE_BOOSTER.and(selectSetPool());
if(configData.restrictedEvents != null) {
@@ -195,52 +218,41 @@ public class AdventureEventData implements Serializable {
.filter(CardEdition::hasBoosterTemplate)
.forEach(allEditions::add);
List<CardBlock> legalBlocks = new ArrayList<>();
for (CardBlock b : src) { // for each block
if (b.getSets().isEmpty() || (b.getCntBoostersDraft() < 1))
continue;
boolean isOkay = true;
for (CardEdition c : b.getSets()) {
if (!allEditions.contains(c)) {
isOkay = false;
break;
}
if (!c.hasBoosterTemplate()) {
isOkay = false;
break;
} else {
final List<Pair<String, Integer>> slots = c.getBoosterTemplate().getSlots();
int boosterSize = 0;
for (Pair<String, Integer> slot : slots) {
boosterSize += slot.getRight();
}
isOkay = boosterSize > 11;
}
for (PrintSheet ps : c.getPrintSheetsBySection()) {
//exclude block with sets containing P9 cards..
if (ps.containsCardNamed("Black Lotus", 1)
|| ps.containsCardNamed("Mox Emerald", 1)
|| ps.containsCardNamed("Mox Pearl", 1)
|| ps.containsCardNamed("Mox Ruby", 1)
|| ps.containsCardNamed("Mox Sapphire", 1)
|| ps.containsCardNamed("Mox Jet", 1)
|| ps.containsCardNamed("Ancestral Recall", 1)
|| ps.containsCardNamed("Timetwister", 1)
|| ps.containsCardNamed("Time Walk", 1)) {
isOkay = false;
break;
}
}
}
if (isOkay) {
legalBlocks.add(b);
}
}
List<CardBlock> legalBlocks = getValidDraftBlocks(allEditions);
return legalBlocks.isEmpty() ? null : Aggregates.random(legalBlocks);
}
private CardBlock pickJumpstartCardBlock() {
public static List<CardBlock> getValidDraftBlocks(List<CardEdition> validEditions) {
List<CardBlock> legalBlocks = new ArrayList<>();
for (CardBlock b : FModel.getBlocks()) { // for each block
if (b.getSets().isEmpty() || (b.getCntBoostersDraft() < 1))
continue;
if (!isValidDraftBlock(b, validEditions))
continue;
legalBlocks.add(b);
}
return legalBlocks;
}
private static boolean isValidDraftBlock(CardBlock b, List<CardEdition> validEditions) {
for (CardEdition c : b.getSets()) {
if (!validEditions.contains(c))
return false;
if (!c.hasBoosterTemplate())
return false;
if(c.getBoosterTemplate().getNumberOfCardsExpected() <= 11)
return false;
for (PrintSheet ps : c.getPrintSheetsBySection()) {
//exclude block with sets containing P9 cards.
if(!Collections.disjoint(ps.toNameLookup().keySet(), POWER_NINE))
return false;
}
}
return true;
}
private static CardBlock pickJumpstartCardBlock() {
Iterable<CardBlock> src = FModel.getBlocks(); //all blocks
List<CardBlock> legalBlocks = new ArrayList<>();
ConfigData configData = Config.instance().getConfigData();
@@ -359,6 +371,10 @@ public class AdventureEventData implements Serializable {
}
}
public void generateParticipants() {
this.generateParticipants(getRecommendedPodSize(this.cardBlock) - 1); //-1 to account for the player
}
public void generateParticipants(int numberOfOpponents) {
participants = new AdventureEventParticipant[numberOfOpponents + 1];
@@ -457,11 +473,10 @@ public class AdventureEventData implements Serializable {
//3. If no matching color found and need more packs, add any available at random.
if (packConfiguration.length > chosenPacks.size() && colorAdded.isEmpty() && !availableOptions.isEmpty()) {
chosenPacks.add(Aggregates.removeRandom(availableOptions));
colorAdded = "";
} else {
done = colorAdded.isEmpty() || packConfiguration.length <= chosenPacks.size();
colorAdded = "";
}
colorAdded = "";
}
participant.registeredDeck = new Deck();
for (Deck chosen : chosenPacks) {
@@ -469,6 +484,22 @@ public class AdventureEventData implements Serializable {
}
}
}
switch (this.style) {
case Swiss:
case Bracket:
this.rounds = (participants.length / 2) - 1;
break;
case RoundRobin:
this.rounds = participants.length - 1;
break;
}
}
public static int getRecommendedPodSize(CardBlock cardBlock) {
// Set can be null when it is only a meta set such as some Jumpstart events.
CardEdition firstSet = cardBlock.getSets().isEmpty() ? null : cardBlock.getSets().get(0);
return firstSet == null ? 8 : firstSet.getDraftOptions().getRecommendedPodSize();
}
private void assignPlayerNames(BoosterDraft draft) {
@@ -664,29 +695,13 @@ public class AdventureEventData implements Serializable {
eventStatus = AdventureEventController.EventStatus.Awarded;
}
public String getPairingDescription() {
switch (eventRules.pairingStyle) {
case Swiss:
return "swiss";
case SwissWithCut:
return "swiss (with cut)";
case RoundRobin:
return "round robin";
case SingleElimination:
return "single elimination";
case DoubleElimination:
return "double elimination";
}
return "";
}
public String getDescription(PointOfInterestChanges changes) {
float townPriceModifier = changes == null ? 1f : changes.getTownPriceModifier();
if (format.equals(AdventureEventController.EventFormat.Draft)) {
if (format == AdventureEventController.EventFormat.Draft) {
description = "Event Type: Booster Draft\n";
description += "Block: " + getCardBlock() + "\n";
description += "Boosters: " + String.join(", ", packConfiguration) + "\n";
description += "Competition Style: " + participants.length + " players, matches played as best of " + eventRules.gamesPerMatch + ", " + (getPairingDescription()) + "\n\n";
description += "Competition Style: " + participants.length + " players, matches played as best of " + eventRules.gamesPerMatch + ", " + (eventRules.getPairingDescription()) + "\n\n";
if (eventStatus == AdventureEventController.EventStatus.Available) {
description += String.format("Pay 1 Entry Fee\n- Gold %d[][+Gold][BLACK]\n- Mana Shards %d[][+Shards][BLACK]\n", Math.round(eventRules.goldToEnter * townPriceModifier), Math.round(eventRules.shardsToEnter * townPriceModifier));
@@ -701,10 +716,10 @@ public class AdventureEventData implements Serializable {
}
}
description += String.format("Prizes\nChampion: Keep drafted deck\n2+ round wins: Challenge Coin \n1+ round wins: %s Booster, %s Booster\n0 round wins: %s Booster", rewardPacks[0].getComment(), rewardPacks[1].getComment(), rewardPacks[2].getComment());
} else if (format.equals(AdventureEventController.EventFormat.Jumpstart)) {
} else if (format == AdventureEventController.EventFormat.Jumpstart) {
description = "Event Type: Jumpstart\n";
description += "Block: " + getCardBlock() + "\n";
description += "Competition Style: " + participants.length + " players, matches played as best of " + eventRules.gamesPerMatch + ", " + (getPairingDescription()) + "\n\n";
description += "Competition Style: " + participants.length + " players, matches played as best of " + eventRules.gamesPerMatch + ", " + (eventRules.getPairingDescription()) + "\n\n";
description += String.format("Pay 1 Entry Fee\n- Gold %d[][+Gold][BLACK]\n- Mana Shards %d[][+Shards][BLACK]\n", Math.round(eventRules.goldToEnter * townPriceModifier), Math.round(eventRules.shardsToEnter * townPriceModifier));
if (eventRules.acceptsBronzeChallengeCoin) {
description += "- Bronze Challenge Coin [][+BronzeChallengeCoin][BLACK]\n\n";
@@ -723,6 +738,7 @@ public class AdventureEventData implements Serializable {
public static class AdventureEventParticipant implements Serializable, Comparable<AdventureEventParticipant> {
@Serial
private static final long serialVersionUID = 1L;
private transient EnemySprite sprite;
String enemyDataName;
@@ -810,6 +826,7 @@ public class AdventureEventData implements Serializable {
}
public static class AdventureEventRules implements Serializable {
@Serial
private static final long serialVersionUID = -2902188278147984885L;
public int goldToEnter;
public int shardsToEnter;
@@ -869,9 +886,20 @@ public class AdventureEventData implements Serializable {
goldToEnter = baseGoldEntry;
shardsToEnter = baseShardEntry;
}
public String getPairingDescription() {
return switch (pairingStyle) {
case Swiss -> "swiss";
case SwissWithCut -> "swiss (with cut)";
case RoundRobin -> "round robin";
case SingleElimination -> "single elimination";
case DoubleElimination -> "double elimination";
};
}
}
public static class AdventureEventMatch implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
public AdventureEventParticipant p1;
public AdventureEventParticipant p2;
@@ -880,6 +908,7 @@ public class AdventureEventData implements Serializable {
}
public static class AdventureEventReward implements Serializable {
@Serial
private final static long serialVersionUID = -2605375040895115477L;
public int minWins = -1;
public int maxWins = -1;

View File

@@ -11,6 +11,7 @@ import forge.adventure.stage.GameHUD;
import forge.adventure.util.AdventureEventController;
import forge.adventure.util.Controls;
import forge.adventure.util.Current;
import forge.model.CardBlock;
/**
* Scene for the Inn in towns
@@ -34,7 +35,7 @@ public class InnScene extends UIScene {
localObjectId = objectId;
if (lastGameScene != null)
object.lastGameScene=lastGameScene;
getLocalEvent();
initLocalEvent();
return object;
}
@@ -109,7 +110,7 @@ public class InnScene extends UIScene {
tempHitPointCost.setDisabled(!purchaseable);
tempHitPointCost.setText("[+GoldCoin] " + tempHealthCost);
getLocalEvent();
initLocalEvent();
if (localEvent == null){
eventDescription.setText("[GREY]No events at this time");
event.setDisabled(true);
@@ -148,9 +149,7 @@ public class InnScene extends UIScene {
Forge.switchScene(ShopScene.instance());
}
private static void getLocalEvent() {
private static void initLocalEvent() {
localEvent = null;
for (AdventureEventData data : AdventurePlayer.current().getEvents()){
if (data.sourceID.equals(localPointOfInterestId) && data.eventOrigin == localObjectId){
@@ -158,7 +157,18 @@ public class InnScene extends UIScene {
return;
}
}
localEvent = AdventureEventController.instance().createEvent(AdventureEventController.EventStyle.Bracket, localPointOfInterestId, localObjectId, changes);
AdventureEventController controller = AdventureEventController.instance();
localEvent = controller.createEvent(localPointOfInterestId);
if(localEvent != null)
controller.initializeEvent(localEvent, localPointOfInterestId, localObjectId, changes);
}
public static void replaceLocalEvent(AdventureEventController.EventFormat format, CardBlock cardBlock) {
AdventurePlayer.current().getEvents().removeIf((data) -> data.sourceID.equals(localPointOfInterestId) && data.eventOrigin == localObjectId);
AdventureEventController controller = AdventureEventController.instance();
localEvent = controller.createEvent(format, cardBlock, localPointOfInterestId);
if(localEvent != null)
controller.initializeEvent(localEvent, localPointOfInterestId, localObjectId, changes);
}
private void startEvent(){

View File

@@ -8,6 +8,7 @@ import forge.StaticData;
import forge.adventure.character.PlayerSprite;
import forge.adventure.data.*;
import forge.adventure.pointofintrest.PointOfInterest;
import forge.adventure.scene.InnScene;
import forge.adventure.scene.InventoryScene;
import forge.adventure.util.AdventureEventController;
import forge.adventure.util.Current;
@@ -21,7 +22,10 @@ import forge.deck.DeckProxy;
import forge.game.GameType;
import forge.gui.FThreads;
import forge.item.PaperCard;
import forge.model.CardBlock;
import forge.model.FModel;
import forge.screens.CoverScreen;
import forge.util.Aggregates;
import java.util.ArrayList;
import java.util.Arrays;
@@ -510,5 +514,26 @@ public class ConsoleCommandInterpreter {
}
return message;
});
registerCommand(new String[]{"set", "event"}, s -> {
if(s.length < 1) return "Command needs 1 parameter: Block or edition name. ";
String blockName = s[0];
if(MapStage.getInstance().findLocalInn() == null)
return "Must be used within a town with an inn.";
CardBlock eventCardBlock = FModel.getBlocks().find(b -> b.getName().equalsIgnoreCase(blockName));
if(eventCardBlock == null) {
CardEdition edition = FModel.getMagicDb().getEditions().find(e -> e.getCode().equalsIgnoreCase(blockName) || e.getName().equalsIgnoreCase(blockName));
if(edition == null)
return "Unable to find edition or block: " + blockName;
eventCardBlock = Aggregates.random(AdventureEventData.getValidDraftBlocks(List.of(edition)));
if(eventCardBlock == null)
return "Unable to find a valid event block that exclusively contains edition " + edition.getName();
}
AdventureEventController.EventFormat eventFormat = s.length > 1 ? AdventureEventController.EventFormat.smartValueOf(s[1])
: eventCardBlock.getName().contains("Jumpstart") ? AdventureEventController.EventFormat.Jumpstart : AdventureEventController.EventFormat.Draft;
if(eventFormat == null)
return "Unknown event format: " + s[1];
InnScene.replaceLocalEvent(eventFormat, eventCardBlock);
return "Replaced local event with " + eventFormat.name() + " - " + eventCardBlock.getName();
});
}
}

View File

@@ -260,6 +260,7 @@ public class MapStage extends GameStage {
spawnClassified.clear();
sourceMapMatch.clear();
enemies.clear();
localInnID = -1;
for (MapLayer layer : map.getLayers()) {
if (layer.getProperties().containsKey("spriteLayer") && layer.getProperties().get("spriteLayer", boolean.class)) {
spriteLayer = layer;
@@ -578,6 +579,7 @@ public class MapStage extends GameStage {
//TODO: Ability to move them (using a sequence such as "UULU" for up, up, left, up).
break;
case "inn":
localInnID = id;
addMapActor(obj, new OnCollide(() -> Forge.switchScene(InnScene.instance(TileMapScene.instance(), TileMapScene.instance().rootPoint.getID(), changes, id))));
break;
case "spellsmith":
@@ -750,6 +752,14 @@ public class MapStage extends GameStage {
}
}
//We could track MapObject IDs more generally but for now this is the only one we might need.
private int localInnID = -1;
public InnScene findLocalInn() {
if(localInnID == -1)
return null;
return InnScene.instance(TileMapScene.instance(), TileMapScene.instance().rootPoint.getID(), changes, localInnID);
}
public boolean exitDungeon(boolean defeated, boolean defeatedByBoss) {
AdventureQuestController.instance().updateQuestsLeave();
clearIsInMap();

View File

@@ -4,7 +4,6 @@ import forge.StaticData;
import forge.adventure.data.AdventureEventData;
import forge.adventure.player.AdventurePlayer;
import forge.adventure.pointofintrest.PointOfInterestChanges;
import forge.card.CardEdition;
import forge.deck.Deck;
import forge.item.BoosterPack;
import forge.item.PaperCard;
@@ -29,7 +28,13 @@ public class AdventureEventController implements Serializable {
Draft,
Sealed,
Jumpstart,
Constructed
Constructed;
public static EventFormat smartValueOf(String name) {
return Arrays.stream(EventFormat.values())
.filter(e -> e.name().equalsIgnoreCase(name))
.findFirst().orElse(null);
}
}
public enum EventStyle {
@@ -67,27 +72,16 @@ public class AdventureEventController implements Serializable {
object = null;
}
public AdventureEventData createEvent(EventStyle style, String pointID, int eventOrigin, PointOfInterestChanges changes) {
public AdventureEventData createEvent(String pointID) {
if (nextEventDate.containsKey(pointID) && nextEventDate.get(pointID) >= LocalDate.now().toEpochDay()) {
// No event currently available here
return null;
}
long eventSeed;
long timeSeed = LocalDate.now().toEpochDay();
long placeSeed = Long.parseLong(pointID.replaceAll("[^0-9]", ""));
long room = Long.MAX_VALUE - placeSeed;
if (timeSeed > room) {
//ensuring we don't ever hit an overflow
eventSeed = Long.MIN_VALUE + timeSeed - room;
} else {
eventSeed = timeSeed + placeSeed;
}
long eventSeed = getEventSeed(pointID);
Random random = new Random(eventSeed);
AdventureEventData e;
// After a certain number of wins, stop offering Jumpstart events
if (Current.player().getStatistic().totalWins() < 10 &&
random.nextInt(10) <= 2) {
@@ -100,15 +94,34 @@ public class AdventureEventController implements Serializable {
//covers cases where (somehow) editions that do not match the event style have been picked up
return null;
}
return e;
}
// If the chosen event seed recommends a four-person pod, run it as a RoundRobin
// Set can be null when it is only a meta set such as some Jumpstart events.
CardEdition firstSet = e.cardBlock.getSets().isEmpty() ? null : e.cardBlock.getSets().get(0);
int podSize = firstSet == null ? 8 : firstSet.getDraftOptions().getRecommendedPodSize();
public AdventureEventData createEvent(EventFormat format, CardBlock cardBlock, String pointID) {
long eventSeed = getEventSeed(pointID);
AdventureEventData e = new AdventureEventData(eventSeed, format, cardBlock);
if(e.cardBlock == null)
return null;
return e;
}
private static long getEventSeed(String pointID) {
long eventSeed;
long timeSeed = LocalDate.now().toEpochDay();
long placeSeed = Long.parseLong(pointID.replaceAll("[^0-9]", ""));
long room = Long.MAX_VALUE - placeSeed;
if (timeSeed > room) {
//ensuring we don't ever hit an overflow
eventSeed = Long.MIN_VALUE + timeSeed - room;
} else {
eventSeed = timeSeed + placeSeed;
}
return eventSeed;
}
public void initializeEvent(AdventureEventData e, String pointID, int eventOrigin, PointOfInterestChanges changes) {
e.sourceID = pointID;
e.eventOrigin = eventOrigin;
e.style = podSize == 4 ? EventStyle.RoundRobin : style;
AdventureEventData.PairingStyle pairingStyle;
if (e.style == EventStyle.RoundRobin) {
@@ -118,21 +131,11 @@ public class AdventureEventController implements Serializable {
}
e.eventRules = new AdventureEventData.AdventureEventRules(e.format, pairingStyle, changes == null ? 1f : changes.getTownPriceModifier());
e.generateParticipants(podSize - 1); //-1 to account for the player
switch (e.style) {
case Swiss:
case Bracket:
e.rounds = (e.participants.length / 2) - 1;
break;
case RoundRobin:
e.rounds = e.participants.length - 1;
break;
}
e.generateParticipants();
AdventurePlayer.current().addEvent(e);
nextEventDate.put(pointID, LocalDate.now().toEpochDay() + new Random().nextInt(2)); //next local event availability date
return e;
}
public Deck generateBooster(String setCode) {
@@ -144,7 +147,6 @@ public class AdventureEventController implements Serializable {
output.setComment(setCode);
return output;
}
public Deck generateBoosterByColor(String color) {
List<PaperCard> cards = BoosterPack.fromColor(color).getCards();
Deck output = new Deck();