[BLB] Implement Season of Gathering and Pawprints mechanic (#12617)

* Add skeleton

* Implement Pawprints modal functionality

* Implement Seasons of Gathering

* remove unused imports

* Add Pawprints test

* use withPawPRintValue() instead of setter

* use 0 for non-pawprint mode and modes classes and move mode validation to addMode

* Use GreatestPowerAmongControlledCreaturesValue

* Fix pawprints check

* calcualte sleected pawprint count based on selected modes

* move max pawprints check to getAvailableModes

* fix max pawprints checks
This commit is contained in:
jimga150 2024-08-14 21:13:09 -04:00 committed by GitHub
parent d5c76489ac
commit e976920e2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 449 additions and 8 deletions

View file

@ -2578,20 +2578,38 @@ public class HumanPlayer extends PlayerImpl {
if (!modeText.isEmpty()) {
modeText = Character.toUpperCase(modeText.charAt(0)) + modeText.substring(1);
}
modeMap.put(mode.getId(), modeIndex + ". " + modeText);
StringBuilder sb = new StringBuilder();
if (mode.getPawPrintValue() > 0){
for (int i = 0; i < mode.getPawPrintValue(); ++i){
sb.append("{P}");
}
sb.append(": ");
} else {
sb.append(modeIndex).append(". ");
}
modeMap.put(mode.getId(), sb.append(modeText).toString());
}
}
// done button for "for up" choices only
boolean canEndChoice = modes.getSelectedModes().size() >= modes.getMinModes() || modes.isMayChooseNone();
boolean canEndChoice = (modes.getSelectedModes().size() >= modes.getMinModes() && modes.getMaxPawPrints() == 0) ||
(modes.getSelectedPawPrints() >= modes.getMaxPawPrints() && modes.getMaxPawPrints() > 0) ||
modes.isMayChooseNone();
if (canEndChoice) {
modeMap.put(Modes.CHOOSE_OPTION_DONE_ID, "Done");
}
modeMap.put(Modes.CHOOSE_OPTION_CANCEL_ID, "Cancel");
// prepare dialog
String message = "Choose mode (selected " + modes.getSelectedModes().size() + " of " + modes.getMaxModes(game, source)
+ ", min " + modes.getMinModes() + ")";
String message;
if (modes.getMaxPawPrints() == 0){
message = "Choose mode (selected " + modes.getSelectedModes().size() + " of " + modes.getMaxModes(game, source)
+ ", min " + modes.getMinModes() + ")";
} else {
message = "Choose mode (selected " + modes.getSelectedPawPrints() + " of " + modes.getMaxPawPrints()
+ " {P})";
}
if (obj != null) {
message = message + "<br>" + obj.getLogName();
}

View file

@ -0,0 +1,162 @@
package mage.cards.s;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.common.GreatestPowerAmongControlledCreaturesValue;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.*;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.keyword.TrampleAbility;
import mage.abilities.keyword.VigilanceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.targetpointer.FixedTarget;
/**
*
* @author jimga150
*/
public final class SeasonOfGathering extends CardImpl {
public SeasonOfGathering(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{G}{G}");
// Choose up to five {P} worth of modes. You may choose the same mode more than once.
this.getSpellAbility().getModes().setMaxPawPrints(5);
this.getSpellAbility().getModes().setMinModes(0);
this.getSpellAbility().getModes().setMaxModes(5);
this.getSpellAbility().getModes().setMayChooseSameModeMoreThanOnce(true);
// {P} -- Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.
this.getSpellAbility().addEffect(new SeasonOfGatheringCounterEffect());
this.spellAbility.getModes().getMode().withPawPrintValue(1);
// {P}{P} -- Choose artifact or enchantment. Destroy all permanents of the chosen type.
Mode mode2 = new Mode(new SeasonOfGatheringRemovalEffect());
this.getSpellAbility().addMode(mode2.withPawPrintValue(2));
// {P}{P}{P} -- Draw cards equal to the greatest power among creatures you control.
Mode mode3 = new Mode(
new DrawCardSourceControllerEffect(GreatestPowerAmongControlledCreaturesValue.instance)
.setText("Draw cards equal to the greatest power among creatures you control.")
);
this.getSpellAbility().addMode(mode3.withPawPrintValue(3));
}
private SeasonOfGathering(final SeasonOfGathering card) {
super(card);
}
@Override
public SeasonOfGathering copy() {
return new SeasonOfGathering(this);
}
}
// Based on KaylasCommandCounterEffect
class SeasonOfGatheringCounterEffect extends OneShotEffect {
SeasonOfGatheringCounterEffect() {
super(Outcome.BoostCreature);
this.staticText = "Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.";
}
private SeasonOfGatheringCounterEffect(final SeasonOfGatheringCounterEffect effect) {
super(effect);
}
@Override
public SeasonOfGatheringCounterEffect copy() {
return new SeasonOfGatheringCounterEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
TargetControlledCreaturePermanent target = new TargetControlledCreaturePermanent();
target.withNotTarget(true);
controller.chooseTarget(outcome, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent == null) {
return false;
}
permanent.addCounters(CounterType.P1P1.createInstance(), source, game);
GainAbilityTargetEffect effect = new GainAbilityTargetEffect(VigilanceAbility.getInstance());
effect.setTargetPointer(new FixedTarget(permanent, game));
game.addEffect(effect, source);
GainAbilityTargetEffect effect2 = new GainAbilityTargetEffect(TrampleAbility.getInstance());
effect2.setTargetPointer(new FixedTarget(permanent, game));
game.addEffect(effect2, source);
return true;
}
}
// Based on KindredDominanceEffect and TurnaboutEffect
class SeasonOfGatheringRemovalEffect extends OneShotEffect {
private static final Set<String> choice = new HashSet<>();
static {
choice.add(CardType.ARTIFACT.toString());
choice.add(CardType.ENCHANTMENT.toString());
}
SeasonOfGatheringRemovalEffect() {
super(Outcome.DestroyPermanent);
this.staticText = "Choose artifact or enchantment. Destroy all permanents of the chosen type.";
}
private SeasonOfGatheringRemovalEffect(final SeasonOfGatheringRemovalEffect effect) {
super(effect);
}
@Override
public SeasonOfGatheringRemovalEffect copy() {
return new SeasonOfGatheringRemovalEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
Choice choiceImpl = new ChoiceImpl(true);
choiceImpl.setMessage("Choose card type to destroy");
choiceImpl.setChoices(choice);
if (!controller.choose(Outcome.Neutral, choiceImpl, game)) {
return false;
}
FilterPermanent filter;
switch (choiceImpl.getChoice()) {
case "Artifact":
filter = StaticFilters.FILTER_PERMANENT_ARTIFACT;
break;
case "Enchantment":
filter = StaticFilters.FILTER_PERMANENT_ENCHANTMENT;
break;
default:
throw new IllegalArgumentException("Choice is required");
}
game.informPlayers(controller.getLogName() + " has chosen " + choiceImpl.getChoiceKey());
return new DestroyAllEffect(filter).apply(game, source);
}
}

View file

@ -203,6 +203,7 @@ public final class Bloomburrow extends ExpansionSet {
cards.add(new SetCardInfo("Scales of Shale", 110, Rarity.COMMON, mage.cards.s.ScalesOfShale.class));
cards.add(new SetCardInfo("Scavenger's Talent", 111, Rarity.RARE, mage.cards.s.ScavengersTalent.class));
cards.add(new SetCardInfo("Scrapshooter", 191, Rarity.RARE, mage.cards.s.Scrapshooter.class));
cards.add(new SetCardInfo("Season of Gathering", 192, Rarity.MYTHIC, mage.cards.s.SeasonOfGathering.class));
cards.add(new SetCardInfo("Seasoned Warrenguard", 30, Rarity.UNCOMMON, mage.cards.s.SeasonedWarrenguard.class));
cards.add(new SetCardInfo("Seedglaive Mentor", 231, Rarity.UNCOMMON, mage.cards.s.SeedglaiveMentor.class));
cards.add(new SetCardInfo("Seedpod Squire", 232, Rarity.COMMON, mage.cards.s.SeedpodSquire.class));

View file

@ -0,0 +1,218 @@
package org.mage.test.cards.modal;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author jimga150
*/
public class PawPrintsTest extends CardTestPlayerBase {
@Test
public void test_Choose113() {
// Test that draw effect sees power affected by counter effect
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
// Choose up to five {P} worth of modes. You may choose the same mode more than once.
// {P} -- Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.
// {P}{P} -- Choose artifact or enchantment. Destroy all permanents of the chosen type.
// {P}{P}{P} -- Draw cards equal to the greatest power among creatures you control.
addCard(Zone.HAND, playerA, "Season of Gathering"); // Instant {4}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Memnite");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Season of Gathering");
setModeChoice(playerA, "1");
setModeChoice(playerA, "1");
setModeChoice(playerA, "3");
addTarget(playerA, "Memnite"); // for 1
addTarget(playerA, "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertHandCount(playerA, 3);
assertPowerToughness(playerA, "Memnite", 3, 3);
assertCounterCount("Memnite", CounterType.P1P1, 2);
}
@Test
public void test_Choose123() {
// Test that 1, 2, and 3 cannot all be selected (and that 1 and 2 will fire in that order)
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
// Choose up to five {P} worth of modes. You may choose the same mode more than once.
// {P} -- Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.
// {P}{P} -- Choose artifact or enchantment. Destroy all permanents of the chosen type.
// {P}{P}{P} -- Draw cards equal to the greatest power among creatures you control.
addCard(Zone.HAND, playerA, "Season of Gathering"); // Instant {4}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Memnite");
// If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead.
addCard(Zone.BATTLEFIELD, playerA, "Hardened Scales");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Season of Gathering");
setModeChoice(playerA, "1");
setModeChoice(playerA, "2");
setModeChoice(playerA, "3"); // Will be unused
addTarget(playerA, "Memnite"); // for 1
setChoice(playerA, "Enchantment");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
// Add one more counter from Hardened Scales, which was still on the battlefield when the counter placing effect triggered
assertPowerToughness(playerA, "Memnite", 3, 3);
assertCounterCount("Memnite", CounterType.P1P1, 2);
// But not anymore...
assertPermanentCount(playerA, "Hardened Scales", 0);
assertGraveyardCount(playerA, "Hardened Scales", 1);
// Draw effect didnt trigger
assertHandCount(playerA, 0);
}
@Test
public void test_Choose2111() {
// Test that 1 and 2 will fire in that order when choices are made in reverse
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
// Choose up to five {P} worth of modes. You may choose the same mode more than once.
// {P} -- Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.
// {P}{P} -- Choose artifact or enchantment. Destroy all permanents of the chosen type.
// {P}{P}{P} -- Draw cards equal to the greatest power among creatures you control.
addCard(Zone.HAND, playerA, "Season of Gathering"); // Instant {4}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Memnite");
// If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead.
addCard(Zone.BATTLEFIELD, playerA, "Hardened Scales");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Season of Gathering");
setModeChoice(playerA, "2");
setModeChoice(playerA, "1");
setModeChoice(playerA, "1");
setModeChoice(playerA, "1");
addTarget(playerA, "Memnite"); // for 1
addTarget(playerA, "Memnite"); // for 1
addTarget(playerA, "Memnite"); // for 1
setChoice(playerA, "Enchantment");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
// Add one more counter per choice from Hardened Scales, which was still on the battlefield when the counter placing effect triggered
assertPowerToughness(playerA, "Memnite", 7, 7);
assertCounterCount("Memnite", CounterType.P1P1, 6);
// But not anymore...
assertPermanentCount(playerA, "Hardened Scales", 0);
assertGraveyardCount(playerA, "Hardened Scales", 1);
}
@Test
public void test_Choose1x5() {
// Test that max amount of modes can be chosen
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
// Choose up to five {P} worth of modes. You may choose the same mode more than once.
// {P} -- Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.
// {P}{P} -- Choose artifact or enchantment. Destroy all permanents of the chosen type.
// {P}{P}{P} -- Draw cards equal to the greatest power among creatures you control.
addCard(Zone.HAND, playerA, "Season of Gathering"); // Instant {4}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Memnite");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Season of Gathering");
for (int i = 0; i < 5; ++i){
setModeChoice(playerA, "1");
addTarget(playerA, "Memnite");
}
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPowerToughness(playerA, "Memnite", 6, 6);
assertCounterCount("Memnite", CounterType.P1P1, 5);
}
@Test
public void test_Choose23() {
// Test that 2 and 3 fire in that order
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
// Choose up to five {P} worth of modes. You may choose the same mode more than once.
// {P} -- Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.
// {P}{P} -- Choose artifact or enchantment. Destroy all permanents of the chosen type.
// {P}{P}{P} -- Draw cards equal to the greatest power among creatures you control.
addCard(Zone.HAND, playerA, "Season of Gathering"); // Instant {4}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Memnite");
// If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead.
addCard(Zone.BATTLEFIELD, playerA, "Hardened Scales");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Season of Gathering");
setModeChoice(playerA, "3");
setModeChoice(playerA, "2");
setChoice(playerA, "Artifact");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Hardened Scales", 1);
assertGraveyardCount(playerA, "Memnite", 1);
// Draw effect saw no creatures, so no cards
assertHandCount(playerA, 0);
}
@Test
public void test_Choose122() {
// Test destroying both artifacts and enchantments
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
// Choose up to five {P} worth of modes. You may choose the same mode more than once.
// {P} -- Put a +1/+1 counter on a creature you control. It gains vigilance and trample until end of turn.
// {P}{P} -- Choose artifact or enchantment. Destroy all permanents of the chosen type.
// {P}{P}{P} -- Draw cards equal to the greatest power among creatures you control.
addCard(Zone.HAND, playerA, "Season of Gathering"); // Instant {4}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Memnite");
// If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead.
addCard(Zone.BATTLEFIELD, playerA, "Hardened Scales");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Season of Gathering");
setModeChoice(playerA, "1");
setModeChoice(playerA, "2");
setModeChoice(playerA, "2");
addTarget(playerA, "Memnite");
setChoice(playerA, "Artifact");
setChoice(playerA, "Enchantment");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Hardened Scales", 1);
assertGraveyardCount(playerA, "Memnite", 1);
}
}

View file

@ -19,6 +19,7 @@ public class Mode implements Serializable {
protected final Effects effects;
protected String flavorWord;
protected Cost cost = null;
protected int pawPrintValue = 0; //0 = does not use pawprints
/**
* Optional Tag to distinguish this mode from others.
* In the case of modes that players can only choose once,
@ -42,6 +43,7 @@ public class Mode implements Serializable {
this.flavorWord = mode.flavorWord;
this.modeTag = mode.modeTag;
this.cost = mode.cost != null ? mode.cost.copy() : null;
this.pawPrintValue = mode.pawPrintValue;
}
public UUID setRandomId() {
@ -119,4 +121,13 @@ public class Mode implements Serializable {
public Cost getCost() {
return cost;
}
public Mode withPawPrintValue(int pawPrintValue) {
this.pawPrintValue = pawPrintValue;
return this;
}
public int getPawPrintValue() {
return pawPrintValue;
}
}

View file

@ -35,6 +35,7 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
private int minModes;
private int maxModes;
private int maxPawPrints;
private Filter maxModesFilter; // calculates the max number of available modes
private Condition moreCondition; // allows multiple modes choose (example: choose one... if condition, you may choose both)
@ -53,6 +54,7 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
this.put(currentMode.getId(), currentMode);
this.minModes = 1;
this.maxModes = 1;
this.maxPawPrints = 0; // 0 = does not use pawprints
this.addSelectedMode(currentMode.getId());
this.chooseController = TargetController.YOU;
}
@ -68,6 +70,7 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
this.minModes = modes.minModes;
this.maxModes = modes.maxModes;
this.maxPawPrints = modes.maxPawPrints;
this.maxModesFilter = modes.maxModesFilter; // can't change so no copy needed
this.moreCondition = modes.moreCondition;
@ -194,6 +197,12 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
return count;
}
public int getSelectedPawPrints(){
return this.selectedModes.stream()
.mapToInt(modeID -> get(modeID).getPawPrintValue())
.sum();
}
public void setMinModes(int minModes) {
this.minModes = minModes;
}
@ -256,6 +265,14 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
return realMaxModes;
}
public void setMaxPawPrints(int maxPawPrints) {
this.maxPawPrints = maxPawPrints;
}
public int getMaxPawPrints() {
return this.maxPawPrints;
}
public void setChooseController(TargetController chooseController) {
this.chooseController = chooseController;
}
@ -275,6 +292,12 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
}
public void addMode(Mode mode) {
if (this.maxPawPrints > 0 && mode.getPawPrintValue() == 0){
throw new IllegalArgumentException("Mode must have nonzero pawprints value in a pawprints mode set.");
}
if (this.maxPawPrints == 0 && mode.getPawPrintValue() > 0){
throw new IllegalArgumentException("Cannot add pawprints mode to non-pawprints mode set.");
}
this.put(mode.getId(), mode);
}
@ -317,7 +340,7 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
return isSelectedValid(source, game);
}
// modal spells must show choose dialog even for 1 option, so check this.size instead evailableModes.size here
// modal spells must show choose dialog even for 1 option, so check this.size instead availableModes.size here
if (this.size() > 1) {
// multiple modes
@ -375,7 +398,8 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
this.currentMode = null;
int currentMaxModes = this.getMaxModes(game, source);
while (this.selectedModes.size() < currentMaxModes) {
while ((this.selectedModes.size() < currentMaxModes && maxPawPrints == 0) ||
(this.getSelectedPawPrints() < maxPawPrints && maxPawPrints > 0)) {
Mode choice = player.chooseMode(this, source, game);
if (choice == null) {
// user press cancel/stop in choose dialog or nothing to choose
@ -437,8 +461,10 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
throw new IllegalArgumentException("Unknown modeId to select");
}
Mode mode = get(modeId);
if (selectedModes.contains(modeId) && mayChooseSameModeMoreThanOnce) {
Mode duplicateMode = get(modeId).copy();
Mode duplicateMode = mode.copy();
UUID originalId = modeId;
duplicateMode.setRandomId();
modeId = duplicateMode.getId();
@ -526,6 +552,9 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
if (isLimitUsageByOnce() && nonAvailableModes.contains(mode.getId())) {
continue;
}
if (getMaxPawPrints() > 0 && getSelectedPawPrints() + mode.getPawPrintValue() > getMaxPawPrints()){
continue;
}
availableModes.add(mode);
}
return availableModes;
@ -545,7 +574,9 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
}
sb.append("choose ");
}
if (this.getMinModes() == 0 && this.getMaxModes(null, null) == 1) {
if (this.getMaxPawPrints() > 0){
sb.append("up to ").append(CardUtil.numberToText(this.getMaxPawPrints())).append(" {P} worth of modes");
} else if (this.getMinModes() == 0 && this.getMaxModes(null, null) == 1) {
sb.append("up to one");
} else if (this.getMinModes() == 0 && this.getMaxModes(null, null) > 2) {
sb.append("any number");