diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 0d06433d2c5..da73cbaba50 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1,11 +1,7 @@ package forge.ai; import java.security.InvalidParameterException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import forge.game.keyword.Keyword; import org.apache.commons.lang3.StringUtils; @@ -591,6 +587,12 @@ public class PlayerControllerAi extends PlayerController { return ComputerUtil.vote(player, options, sa, votes, forPlayer); } + @Override + public String chooseSector(Card assignee, String ai) { + final List sectors = Arrays.asList("Alpha", "Beta", "Gamma"); + return Aggregates.random(sectors); + } + @Override public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) { return !ComputerUtil.wantMulligan(player, cardsToReturn); diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 18742c0de7e..164f92e4eb0 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -47,6 +47,7 @@ public enum SpellApiToAi { .put(ApiType.ChooseEvenOdd, ChooseEvenOddAi.class) .put(ApiType.ChooseNumber, ChooseNumberAi.class) .put(ApiType.ChoosePlayer, ChoosePlayerAi.class) + .put(ApiType.ChooseSector, AlwaysPlayAi.class) .put(ApiType.ChooseSource, ChooseSourceAi.class) .put(ApiType.ChooseType, ChooseTypeAi.class) .put(ApiType.Clash, ClashAi.class) diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 142090a8eb6..1836f5a76c0 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -37,6 +37,7 @@ import forge.game.mulligan.MulliganService; import forge.game.player.GameLossReason; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; +import forge.game.player.PlayerCollection; import forge.game.player.PlayerPredicates; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementResult; @@ -1317,7 +1318,11 @@ public class GameAction { CardCollection desCreats = null; CardCollection unAttachList = new CardCollection(); CardCollection sacrificeList = new CardCollection(); + PlayerCollection spaceSculptors = new PlayerCollection(); for (final Card c : game.getCardsIn(ZoneType.Battlefield)) { + if (c.hasKeyword(Keyword.SPACE_SCULPTOR)) { + spaceSculptors.add(c.getController()); + } if (c.isCreature()) { // Rule 704.5f - Put into grave (no regeneration) for toughness <= 0 if (c.getNetToughness() <= 0) { @@ -1393,6 +1398,9 @@ public class GameAction { } for (Player p : game.getPlayers()) { + if (!spaceSculptors.isEmpty() && !spaceSculptors.contains(p)) { + checkAgain |= stateBasedAction704_5u(p); + } if (handleLegendRule(p, noRegCreats)) { checkAgain = true; } @@ -1412,6 +1420,11 @@ public class GameAction { checkAgain = true; } } + if (!spaceSculptors.isEmpty()) { + for (Player p : spaceSculptors) { + checkAgain |= stateBasedAction704_5u(p); + } + } // 704.5m World rule checkAgain |= handleWorldRule(noRegCreats); // only check static abilities once after destroying all the creatures @@ -1551,6 +1564,26 @@ public class GameAction { return checkAgain; } + private boolean stateBasedAction704_5u(Player p) { + boolean checkAgain = false; + + CardCollection toAssign = new CardCollection(); + + for (final Card c : p.getCreaturesInPlay().threadSafeIterable()) { + if (!c.hasSector()) { + toAssign.add(c); + checkAgain = true; + } + } + + for (Card assignee : toAssign) { // probably would be nice for players to pick order of assigning? + assignee.assignSector(p.getController().chooseSector(assignee, "Assign")); + toAssign.remove(assignee); + } + + return checkAgain; + } + private boolean stateBasedAction903_9a(Card c) { if (c.isRealCommander() && c.canMoveToCommandZone()) { // FIXME: need to flush the tracker to make sure the Commander is properly updated diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index 1d1e13a525b..624f996fd5e 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -45,6 +45,7 @@ public enum ApiType { ChooseEvenOdd (ChooseEvenOddEffect.class), ChooseNumber (ChooseNumberEffect.class), ChoosePlayer (ChoosePlayerEffect.class), + ChooseSector (ChooseSectorEffect.class), ChooseSource (ChooseSourceEffect.class), ChooseType (ChooseTypeEffect.class), Clash (ClashEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseSectorEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseSectorEffect.java new file mode 100644 index 00000000000..e879777f386 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseSectorEffect.java @@ -0,0 +1,16 @@ +package forge.game.ability.effects; + +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +public class ChooseSectorEffect extends SpellAbilityEffect { + + @Override + public void resolve(SpellAbility sa) { + final Card card = sa.getHostCard(); + final String chosen = card.getController().getController().chooseSector(null, sa.getParamOrDefault("AILogic", "")); + card.setChosenSector(chosen); + } +} diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 4ed120de491..1c3fdb8f152 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -290,6 +290,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private Direction chosenDirection = null; private String chosenMode = ""; private String currentRoom = null; + private String sector = null; + private String chosenSector = null; private Card exiledWith; private Player exiledBy; @@ -1898,6 +1900,23 @@ public class Card extends GameEntity implements Comparable, IHasSVars { return false; } + public String getSector() { + return sector; + } + public void assignSector(String s) { + sector = s; + view.updateSector(this); + } + public boolean hasSector() { + return sector != null; + } + public String getChosenSector() { + return chosenSector; + } + public final void setChosenSector(final String s) { + chosenSector = s; + } + // used for cards like Meddling Mage... public final String getNamedCard() { return getChosenName(); @@ -2183,7 +2202,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { || keyword.equals("Ascend") || keyword.equals("Totem armor") || keyword.equals("Battle cry") || keyword.equals("Devoid") || keyword.equals("Riot") || keyword.equals("Daybound") || keyword.equals("Nightbound") - || keyword.equals("Friends forever") || keyword.equals("Choose a Background")) { + || keyword.equals("Friends forever") || keyword.equals("Choose a Background") + || keyword.equals("Space sculptor")) { sbLong.append(keyword).append(" (").append(inst.getReminderText()).append(")"); } else if (keyword.startsWith("Partner:")) { final String[] k = keyword.split(":"); diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index e4a40ffc851..6b8fe6423be 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -120,6 +120,14 @@ public class CardProperty { if (source.hasChosenCard(card)) { return false; } + } else if (property.equals("ChosenSector")) { + if (!source.getChosenSector().equals(card.getSector())) { + return false; + } + } else if (property.equals("DifferentSector")) { + if (source.getSector().equals(card.getSector())) { + return false; + } } else if (property.equals("DoubleFaced")) { if (!card.isDoubleFaced()) { return false; diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index a343f683b3f..3283407dfd4 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -479,6 +479,13 @@ public class CardView extends GameEntityView { set(TrackableProperty.Remembered, sb.toString()); } + public String getSector() { + return get(TrackableProperty.Sector); + } + void updateSector(Card c) { + set(TrackableProperty.Sector, c.getSector()); + } + public String getNamedCard() { return get(TrackableProperty.NamedCard); } diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 0800a10f04b..cb40335221f 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -157,6 +157,7 @@ public enum Keyword { SCAVENGE("Scavenge", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Put a number of +1/+1 counters equal to this card's power on target creature. Scavenge only as a sorcery."), SOULBOND("Soulbond", SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters the battlefield. They remain paired for as long as you control both of them."), SOULSHIFT("Soulshift", KeywordWithAmount.class, false, "When this creature dies, you may return target Spirit card with mana value %d or less from your graveyard to your hand."), + SPACE_SCULPTOR("Space sculptor", SimpleKeyword.class, true, "CARDNAME divides the battlefield into alpha, beta, and gamma sectors. If a creature isn't assigned to a sector, its controller assigns it to one. Opponents assign first."), SPECIALIZE("Specialize", KeywordWithCost.class, false, "%s, Choose a color, discard a card of that color or associated basic land type: This card perpetually specializes into that color. Activate only as a sorcery."), SPECTACLE("Spectacle", KeywordWithCost.class, false, "You may cast this spell for its spectacle cost rather than its mana cost if an opponent lost life this turn."), SPLICE("Splice", KeywordWithCostAndType.class, false, "As you cast an %2$s spell, you may reveal this card from your hand and pay its splice cost. If you do, add this card's effects to that spell."), diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index f151ef47876..cfc676e22d0 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -183,6 +183,8 @@ public abstract class PlayerController { return chooseSomeType(kindOfType, sa, validTypes, invalidTypes, false); } + public abstract String chooseSector(Card assignee, String ai); + public abstract Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer); public abstract CardCollectionView getCardsToMulligan(Player firstPlayer); diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index 91ba6cade07..80144b983d6 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -68,6 +68,8 @@ public enum TrackableProperty { ChosenDirection(TrackableTypes.EnumType(Direction.class)), ChosenEvenOdd(TrackableTypes.EnumType(EvenOdd.class)), ChosenMode(TrackableTypes.StringType), + ChosenSector(TrackableTypes.StringType), + Sector(TrackableTypes.StringType), ClassLevel(TrackableTypes.IntegerType), CurrentRoom(TrackableTypes.StringType), Intensity(TrackableTypes.IntegerType), diff --git a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index 714c55a055e..a840a5f1189 100644 --- a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java +++ b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java @@ -55,10 +55,7 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import com.google.common.collect.Lists; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Default harmless implementation for tests. @@ -496,6 +493,12 @@ public class PlayerControllerForTests extends PlayerController { return chooseItem(validTypes); } + @Override + public String chooseSector(Card assignee, String ai) { + final List sectors = Arrays.asList("Alpha", "Beta", "Gamma"); + return chooseItem(sectors); + } + @Override public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer) { return chooseItem(options); diff --git a/forge-gui/res/cardsfolder/upcoming/space_beleren.txt b/forge-gui/res/cardsfolder/upcoming/space_beleren.txt new file mode 100644 index 00000000000..ebfc44a5ccc --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/space_beleren.txt @@ -0,0 +1,14 @@ +Name:Space Beleren +ManaCost:2 W U +Types:Legendary Planeswalker Jace +Loyalty:3 +K:Space sculptor +A:AB$ Effect | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | StaticAbilities$ SectorBlock | SpellDescription$ Creatures in each sector can be blocked this turn only by creatures in the same sector. +SVar:SectorBlock:Mode$ CantBlockBy | ValidAttacker$ Creature | ValidBlockerRelative$ Creature.DifferentSector | Description$ Creatures in each sector can be blocked this turn only by creatures in the same sector. +A:AB$ ChooseSector | Cost$ SubCounter<1/LOYALTY> | Planeswalker$ True | SubAbility$ DBPutCounterAll | AILogic$ Pump | SpellDescription$ Put a +1/+1 counter on each creature in the sector of your choice. +SVar:DBPutCounterAll:DB$ PutCounterAll | ValidCards$ Creature.ChosenSector | CounterType$ P1P1 | StackDescription$ None +A:AB$ ChooseSector | Cost$ SubCounter<5/LOYALTY> | Planeswalker$ True | Ultimate$ True | SubAbility$ DBDestroyAll | AILogic$ Destroy | SpellDescription$ Destroy all creatures in the sector of your choice. +SVar:DBDestroyAll:DB$ DestroyAll | ValidCards$ Creature.ChosenSector | StackDescription$ None +DeckHas:Ability$Counters +AI:RemoveDeck:All +Oracle:Space sculptor (Space Beleren divides the battlefield into alpha, beta, and gamma sectors. If a creature isn't assigned to a sector, its controller assigns it to one. Opponents assign first.)\n[+1]: Creatures in each sector can be blocked this turn only by creatures in the same sector.\n[−1]: Put a +1/+1 counter on each creature in the sector of your choice.\n[−5]: Destroy all creatures in the sector of your choice. diff --git a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java index cb507ba5fd7..9aade51b490 100644 --- a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java +++ b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java @@ -527,6 +527,14 @@ public class CardDetailUtil { area.append("(Class Level:").append(card.getClassLevel()).append(")"); } + // sector + if (!card.getSector().isEmpty()) { + if (area.length() != 0) { + area.append("\n"); + } + area.append("Sector: ").append(card.getSector()); + } + // a card has something attached to it if (card.hasCardAttachments()) { if (area.length() != 0) { diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 07a17901f8c..c44e37c4c50 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -1364,6 +1364,17 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont } } + @Override + public String chooseSector(Card assignee, String ai) { + final List sectors = Arrays.asList("Alpha", "Beta", "Gamma"); + // turn this into two separate localized prompts + String prompt = "Choose sector"; + if (assignee != null) { + prompt = prompt + " for " + assignee.getName(); + } + return getGui().one(prompt, sectors); + } + @Override public Object vote(final SpellAbility sa, final String prompt, final List options, final ListMultimap votes, Player forPlayer) {