...
 
Commits (169)
......@@ -1211,7 +1211,7 @@ public class AiController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
ApiType api = sa.getApi();
// Abilities without api may also use this routine, However they should provide a unique mode value
// Abilities without api may also use this routine, However they should provide a unique mode value ?? How could this work?
if (api == null) {
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
mode);
......
......@@ -2847,7 +2847,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
return false;
......@@ -2878,7 +2878,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
// no life gain is not negative
......
......@@ -2590,7 +2590,7 @@ public class ComputerUtilCombat {
// repParams.put("PreventMap", preventMap);
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
return !list.isEmpty();
}
......
......@@ -162,10 +162,36 @@ public class PlayerControllerAi extends PlayerController {
@Override
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title,
FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
Player targetedPlayer) {
// this isn't used
return null;
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
FCollection<T> remaining = new FCollection<T>(optionList);
List<T> selecteds = new ArrayList<T>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
return selecteds;
}
@Override
public <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2,
boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
T selected1 = chooseSingleEntityForEffect(optionList1, null, sa, title, optional, targetedPlayer);
T selected2 = chooseSingleEntityForEffect(optionList2, null, sa, title, optional || selected1!=null, targetedPlayer);
List<T> selecteds = new ArrayList<T>();
if ( selected1 != null ) { selecteds.add(selected1); }
if ( selected2 != null ) { selecteds.add(selected2); }
return selecteds;
}
@Override
......@@ -1090,7 +1116,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List<Card> chooseCardsForZoneChange(
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList,
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max,
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
// this isn't used
return null;
......@@ -1164,4 +1190,10 @@ public class PlayerControllerAi extends PlayerController {
return chosenOptCosts;
}
@Override
public boolean confirmMulliganScry(Player p) {
// Always true?
return true;
}
}
......@@ -113,7 +113,7 @@ public class ManifestAi extends SpellAbilityAi {
repParams.put("Origin", ZoneType.Library);
repParams.put("Destination", ZoneType.Battlefield);
repParams.put("Source", sa.getHostCard());
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.None);
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.Other);
if (!list.isEmpty()) {
return false;
}
......
......@@ -20,6 +20,8 @@ package forge.deck;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import forge.StaticData;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
......@@ -47,7 +49,7 @@ public enum DeckFormat {
QuestDeck ( Range.between(40, Integer.MAX_VALUE), Range.between(0, 15), 4),
Limited ( Range.between(40, Integer.MAX_VALUE), null, Integer.MAX_VALUE),
Commander ( Range.is(99), Range.between(0, 10), 1, new Predicate<CardRules>() {
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
private final Set<String> bannedCards = ImmutableSet.of(
"Adriana's Valor", "Advantageous Proclamation", "Amulet of Quoz", "Ancestral Recall", "Assemble the Rank and Vile",
"Backup Plan", "Balance", "Biorhythm", "Black Lotus", "Brago's Favor", "Braids, Cabal Minion", "Bronze Tablet",
"Channel", "Chaos Orb", "Coalition Victory", "Contract from Below", "Darkpact", "Demonic Attorney", "Double Stroke",
......@@ -59,7 +61,7 @@ public enum DeckFormat {
"Rebirth", "Recurring Nightmare", "Rofellos, Llanowar Emissary", "Secret Summoning", "Secrets of Paradise",
"Sentinel Dispatch", "Shahrazad", "Sovereign's Realm", "Summoner's Bond", "Sundering Titan", "Sway of the Stars",
"Sylvan Primordial", "Tempest Efreet", "Time Vault", "Time Walk", "Timmerian Fiends", "Tinker", "Tolarian Academy",
"Trade Secrets", "Unexpected Potential", "Upheaval", "Weight Advantage", "Worldfire", "Worldknit", "Yawgmoth's Bargain"));
"Trade Secrets", "Unexpected Potential", "Upheaval", "Weight Advantage", "Worldfire", "Worldknit", "Yawgmoth's Bargain");
@Override
public boolean apply(CardRules rules) {
if (bannedCards.contains(rules.getName())) {
......@@ -70,8 +72,8 @@ public enum DeckFormat {
}),
Pauper ( Range.is(60), Range.between(0, 10), 1),
Brawl ( Range.is(59), Range.between(0, 15), 1, null, new Predicate<PaperCard>() {
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
"Baral, Chief of Compliance","Smuggler's Copter","Sorcerous Spyglass"));
private final Set<String> bannedCards = ImmutableSet.of(
"Baral, Chief of Compliance","Smuggler's Copter","Sorcerous Spyglass");
@Override
public boolean apply(PaperCard card) {
//why do we need to hard code the bannings here - they are defined in the GameFormat predicate used below
......@@ -81,7 +83,7 @@ public enum DeckFormat {
return StaticData.instance() == null ? false : StaticData.instance().getBrawlPredicate().apply(card);
}
}) {
private final ImmutableSet<String> bannedCommanders = ImmutableSet.of("Baral, Chief of Compliance");
private final Set<String> bannedCommanders = ImmutableSet.of("Baral, Chief of Compliance");
@Override
public boolean isLegalCommander(CardRules rules) {
......@@ -89,11 +91,11 @@ public enum DeckFormat {
}
},
TinyLeaders ( Range.is(49), Range.between(0, 10), 1, new Predicate<CardRules>() {
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
private final Set<String> bannedCards = ImmutableSet.of(
"Ancestral Recall", "Balance", "Black Lotus", "Black Vise", "Channel", "Chaos Orb", "Contract From Below", "Counterbalance", "Darkpact", "Demonic Attorney", "Demonic Tutor", "Earthcraft", "Edric, Spymaster of Trest", "Falling Star",
"Fastbond", "Flash", "Goblin Recruiter", "Grindstone", "Hermit Druid", "Imperial Seal", "Jeweled Bird", "Karakas", "Library of Alexandria", "Mana Crypt", "Mana Drain", "Mana Vault", "Metalworker", "Mind Twist", "Mishra's Workshop",
"Mox Emerald", "Mox Jet", "Mox Pearl", "Mox Ruby", "Mox Sapphire", "Necropotence", "Shahrazad", "Skullclamp", "Sol Ring", "Strip Mine", "Survival of the Fittest", "Sword of Body and Mind", "Time Vault", "Time Walk", "Timetwister",
"Timmerian Fiends", "Tolarian Academy", "Umezawa's Jitte", "Vampiric Tutor", "Wheel of Fortune", "Yawgmoth's Will"));
"Timmerian Fiends", "Tolarian Academy", "Umezawa's Jitte", "Vampiric Tutor", "Wheel of Fortune", "Yawgmoth's Will");
@Override
public boolean apply(CardRules rules) {
......@@ -112,7 +114,7 @@ public enum DeckFormat {
return true;
}
}) {
private final ImmutableSet<String> bannedCommanders = ImmutableSet.of("Derevi, Empyrial Tactician", "Erayo, Soratami Ascendant", "Rofellos, Llanowar Emissary");
private final Set<String> bannedCommanders = ImmutableSet.of("Derevi, Empyrial Tactician", "Erayo, Soratami Ascendant", "Rofellos, Llanowar Emissary");
@Override
public boolean isLegalCommander(CardRules rules) {
......@@ -141,13 +143,6 @@ public enum DeckFormat {
private final static String ADVPROCLAMATION = "Advantageous Proclamation";
private final static String SOVREALM = "Sovereign's Realm";
private static final List<String> limitExceptions = Arrays.asList(
new String[]{"Relentless Rats", "Shadowborn Apostle", "Rat Colony"});
public static List<String> getLimitExceptions(){
return limitExceptions;
}
private DeckFormat(Range<Integer> mainRange0, Range<Integer> sideRange0, int maxCardCopies0, Predicate<CardRules> cardPoolFilter0, Predicate<PaperCard> paperCardPoolFilter0) {
mainRange = mainRange0;
sideRange = sideRange0;
......@@ -342,7 +337,6 @@ public enum DeckFormat {
//basic lands, Shadowborn Apostle, Relentless Rats and Rat Colony
final CardPool allCards = deck.getAllCardsInASinglePool(hasCommander());
final ImmutableSet<String> limitExceptions = ImmutableSet.of("Relentless Rats", "Shadowborn Apostle", "Rat Colony");
// should group all cards by name, so that different editions of same card are really counted as the same card
for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, PaperCard.FN_GET_NAME)) {
......@@ -351,8 +345,7 @@ public enum DeckFormat {
return TextUtil.concatWithSpace("contains the nonexisting card", cp.getKey());
}
final boolean canHaveMultiple = simpleCard.getRules().getType().isBasicLand() || limitExceptions.contains(cp.getKey());
if (!canHaveMultiple && cp.getValue() > maxCopies) {
if (!canHaveAnyNumberOf(simpleCard) && cp.getValue() > maxCopies) {
return TextUtil.concatWithSpace("must not contain more than", String.valueOf(maxCopies), "copies of the card", cp.getKey());
}
}
......@@ -370,6 +363,12 @@ public enum DeckFormat {
return null;
}
public static boolean canHaveAnyNumberOf(final IPaperCard icard) {
return icard.getRules().getType().isBasicLand()
|| Iterables.contains(icard.getRules().getMainPart().getKeywords(),
"A deck can have any number of cards named CARDNAME.");
}
public static String getPlaneSectionConformanceProblem(final CardPool planes) {
//Must contain at least 10 planes/phenomenons, but max 2 phenomenons. Singleton.
if (planes == null || planes.countAll() < 10) {
......
......@@ -102,6 +102,7 @@ public class DeckRecognizer {
// Pattern.compile("(.*)[^A-Za-wyz]*\\s+([\\d]{1,2})");
private static final Pattern SEARCH_NUMBERS_IN_FRONT = Pattern.compile("([\\d]{1,2})[^A-Za-wyz]*\\s+(.*)");
//private static final Pattern READ_SEPARATED_EDITION = Pattern.compile("[[\\(\\{]([a-zA-Z0-9]){1,3})[]*\\s+(.*)");
private static final Pattern SEARCH_SINGLE_SLASH = Pattern.compile("(?<=[^/])\\s*/\\s*(?=[^/])");
private final SetPreference useLastSet;
private final ICardDatabase db;
......@@ -125,7 +126,10 @@ public class DeckRecognizer {
return new Token(TokenType.Comment, 0, rawLine);
}
final char smartQuote = (char) 8217;
final String line = rawLine.trim().replace(smartQuote, '\'');
String line = rawLine.trim().replace(smartQuote, '\'');
// Some websites export split card names with a single slash. Replace with double slash.
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
Token result = null;
final Matcher foundNumbersInFront = DeckRecognizer.SEARCH_NUMBERS_IN_FRONT.matcher(line);
......
......@@ -100,8 +100,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
build.add(edition.getCode());
}
// Should future image file names be all lower case? Instead of Up case sets?
return StringUtils.join(build, "_").toLowerCase();
return StringUtils.join(build, "_").replace('*', 'x').toLowerCase();
}
public PaperToken(final CardRules c) { this(c, null, null); }
......
......@@ -86,7 +86,7 @@ public class Localizer {
resourceBundle = ResourceBundle.getBundle(languageRegionID, new Locale(splitLocale[0], splitLocale[1]), loader);
} catch (NullPointerException | MissingResourceException e) {
//If the language can't be loaded, default to US English
resourceBundle = ResourceBundle.getBundle("en-GB", new Locale("en", "GB"), loader);
resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
e.printStackTrace();
}
......
......@@ -29,6 +29,7 @@ import forge.game.ability.effects.AttachEffect;
import forge.game.card.*;
import forge.game.event.*;
import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordsChange;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
......@@ -50,9 +51,9 @@ import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import forge.util.maps.HashMapOfLists;
import forge.util.maps.MapOfLists;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*;
import java.util.Map.Entry;
/**
* Methods for common actions performed during a game.
......@@ -292,6 +293,33 @@ public class GameAction {
copied.getOwner().addInboundToken(copied);
}
if (toBattlefield) {
// HACK for making the RIOT enchantment look into the Future
// need to check the Keywords what it would have on the Battlefield
Card riotLKI = CardUtil.getLKICopy(copied);
riotLKI.setLastKnownZone(zoneTo);
CardCollection preList = new CardCollection(riotLKI);
checkStaticAbilities(false, Sets.newHashSet(riotLKI), preList);
List<Long> changedTimeStamps = Lists.newArrayList();
for(Map.Entry<Long, KeywordsChange> e : riotLKI.getChangedCardKeywords().entrySet()) {
if (!copied.hasChangedCardKeywords(e.getKey())) {
KeywordsChange o = e.getValue();
o.setHostCard(copied);
for (KeywordInterface k : o.getKeywords()) {
for (ReplacementEffect re : k.getReplacements()) {
// this param need to be set, otherwise in ReplaceMoved it fails
re.getMapParams().put("BypassEtbCheck", "True");
}
}
copied.addChangedCardKeywordsInternal(o, e.getKey());
changedTimeStamps.add(e.getKey());
}
}
checkStaticAbilities(false);
}
Map<String, Object> repParams = Maps.newHashMap();
repParams.put("Event", "Moved");
repParams.put("Affected", copied);
......@@ -1674,12 +1702,8 @@ public class GameAction {
// rule 103.4b
boolean isMultiPlayer = game.getPlayers().size() > 2;
int mulliganDelta = isMultiPlayer ? 0 : 1;
// https://magic.wizards.com/en/articles/archive/feature/checking-brawl-2018-07-09
if (game.getRules().hasAppliedVariant(GameType.Brawl) && !isMultiPlayer){
mulliganDelta = 0;
}
int mulliganDelta = isMultiPlayer || game.getRules().hasAppliedVariant(GameType.Brawl) ? 0 : 1;
boolean allKept;
do {
......@@ -1695,32 +1719,17 @@ public class GameAction {
}
if (toMulligan != null && !toMulligan.isEmpty()) {
if (!isCommander) {
toMulligan = new CardCollection(p.getCardsIn(ZoneType.Hand));
for (final Card c : toMulligan) {
moveToLibrary(c, null, null);
}
try {
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
} catch (InterruptedException e) {
e.printStackTrace();
}
p.shuffle(null);
p.drawCards(handSize[i] - mulliganDelta);
} else {
List<Card> toExile = Lists.newArrayList(toMulligan);
for (Card c : toExile) {
exile(c, null, null);
}
exiledDuringMulligans.addAll(p, toExile);
try {
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
} catch (InterruptedException e) {
e.printStackTrace();
}
p.drawCards(toExile.size() - 1);
toMulligan = new CardCollection(p.getCardsIn(ZoneType.Hand));
for (final Card c : toMulligan) {
moveToLibrary(c, null, null);
}
try {
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
} catch (InterruptedException e) {
e.printStackTrace();
}
p.shuffle(null);
p.drawCards(handSize[i] - mulliganDelta);
p.onMulliganned();
allKept = false;
} else {
......@@ -1731,21 +1740,17 @@ public class GameAction {
mulliganDelta++;
} while (!allKept);
if (isCommander) {
for (Entry<Player, Collection<Card>> kv : exiledDuringMulligans.entrySet()) {
Player p = kv.getKey();
Collection<Card> cc = kv.getValue();
for (Card c : cc) {
moveToLibrary(c, null, null);
}
p.shuffle(null);
//Vancouver Mulligan as a scry with the decisions inside
List<Player> scryers = Lists.newArrayList();
for(Player p : whoCanMulligan) {
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
scryers.add(p);
}
}
//Vancouver Mulligan
for(Player p : whoCanMulligan) {
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
p.scry(1, null);
for(Player p : scryers) {
if (p.getController().confirmMulliganScry(p)) {
scry(ImmutableList.of(p), 1, null);
}
}
}
......@@ -1843,4 +1848,68 @@ public class GameAction {
runParams.put("Player", p);
game.getTriggerHandler().runTrigger(TriggerType.BecomeMonarch, runParams, false);
}
// Make scry an action function so that it can be used for mulligans (with a null cause)
// Assumes that the list of players is in APNAP order, which should be the case
// Optional here as well to handle the way that mulligans do the choice
// 701.17. Scry
// 701.17a To “scry N” means to look at the top N cards of your library, then put any number of them
// on the bottom of your library in any order and the rest on top of your library in any order.
// 701.17b If a player is instructed to scry 0, no scry event occurs. Abilities that trigger whenever a
// player scries won’t trigger.
// 701.17c If multiple players scry at once, each of those players looks at the top cards of their library
// at the same time. Those players decide in APNAP order (see rule 101.4) where to put those
// cards, then those cards move at the same time.
public void scry(List<Player> players, int numScry, SpellAbility cause) {
if (numScry == 0) {
return;
}
// reveal the top N library cards to the player (only)
// no real need to separate out the look if
// there is only one player scrying
if (players.size() > 1) {
for (final Player p : players) {
final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, numScry));
revealTo(topN, p);
}
}
// make the decisions
List<ImmutablePair<CardCollection, CardCollection>> decisions = Lists.newArrayList();
for (final Player p : players) {
final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, numScry));
ImmutablePair<CardCollection, CardCollection> decision = p.getController().arrangeForScry(topN);
decisions.add(decision);
int numToTop = decision.getLeft() == null ? 0 : decision.getLeft().size();
int numToBottom = decision.getRight() == null ? 0 : decision.getRight().size();
// publicize the decision
game.fireEvent(new GameEventScry(p, numToTop, numToBottom));
}
// do the moves after all the decisions (maybe not necesssary, but let's
// do it the official way)
for (int i = 0; i < players.size(); i++) {
// no good iterate simultaneously in Java
final Player p = players.get(i);
final CardCollection toTop = decisions.get(i).getLeft();
final CardCollection toBottom = decisions.get(i).getRight();
if (toTop != null) {
Collections.reverse(toTop); // reverse to get the correct order
for (Card c : toTop) {
moveToLibrary(c, cause, null);
}
}
if (toBottom != null) {
for (Card c : toBottom) {
moveToBottomOfLibrary(c, cause, null);
}
}
if (cause != null) {
// set up triggers (but not actually do them until later)
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Player", p);
game.getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
}
}
}
}
......@@ -28,6 +28,8 @@ import forge.game.cost.Cost;
import forge.game.spellability.*;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder;
import java.util.List;
import java.util.Map;
......@@ -130,7 +132,16 @@ public final class AbilityFactory {
String source = state.getName().isEmpty() ? abString : state.getName();
throw new RuntimeException("AbilityFactory : getAbility -- no API in " + source + ": " + abString);
}
return getAbility(mapParams, type, state, parent);
try {
return getAbility(mapParams, type, state, parent);
} catch (Error | Exception ex) {
String msg = "AbilityFactory:getAbility: crash when trying to create ability ";
Sentry.getContext().recordBreadcrumb(
new BreadcrumbBuilder().setMessage(msg)
.withData("Card", state.getName()).withData("Ability", abString).build()
);
throw new RuntimeException(msg + " of card: " + state.getName(), ex);
}
}
public static final SpellAbility getAbility(final Card hostCard, final String svar) {
......
......@@ -852,7 +852,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
// ensure that selection is within maximum allowed changeNum
do {
selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, delayedReveal, selectPrompt, decider);
selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, 0, changeNum, delayedReveal, selectPrompt, decider);
} while (selectedCards != null && selectedCards.size() > changeNum);
if (selectedCards != null) {
for (Card card : selectedCards) {
......
......@@ -4,6 +4,8 @@ import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.player.PlayerController;
......@@ -69,11 +71,14 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
final Card card = sa.getHostCard();
final Game game = card.getGame();
final Player player = sa.getActivatingPlayer();
PlayerController pc = player.getController();
final String type = sa.getParam("CounterType");
final String num = sa.getParam("CounterNum");
int cntToRemove = 0;
if (!num.equals("All") && !num.equals("Remembered")) {
if (!num.equals("All") && !num.equals("Any") && !num.equals("Remembered")) {
cntToRemove = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
}
......@@ -96,6 +101,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
boolean rememberRemoved = sa.hasParam("RememberRemoved");
boolean rememberAmount = sa.hasParam("RememberAmount");
for (final Player tgtPlayer : getTargetPlayers(sa)) {
// Removing energy
......@@ -107,7 +113,23 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
}
for (final Card tgtCard : getTargetCards(sa)) {
CardCollectionView srcCards = null;
if (sa.hasParam("ValidSource")) {
srcCards = game.getCardsIn(ZoneType.Battlefield);
srcCards = CardLists.getValidCards(srcCards, sa.getParam("ValidSource"), player, card, sa);
if (num.equals("Any")) {
StringBuilder sb = new StringBuilder();
sb.append("Choose cards to take ").append(counterType.getName()).append(" counters from");
srcCards = player.getController().chooseCardsForEffect(srcCards, sa, sb.toString(), 0, srcCards.size(), true);
}
} else {
srcCards = getTargetCards(sa);
}
int totalRemoved = 0;
for (final Card tgtCard : srcCards) {
Card gameCard = game.getCardState(tgtCard, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
......@@ -123,14 +145,12 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
game.updateLastStateForCard(gameCard);
continue;
} else if (num.equals("All")) {
} else if (num.equals("All") || num.equals("Any")) {
cntToRemove = gameCard.getCounters(counterType);
} else if (sa.getParam("CounterNum").equals("Remembered")) {
} else if (num.equals("Remembered")) {
cntToRemove = gameCard.getCountersAddedBy(card, counterType);
}
PlayerController pc = sa.getActivatingPlayer().getController();
if (type.equals("Any")) {
while (cntToRemove > 0 && gameCard.hasCounters()) {
final Map<CounterType, Integer> tgtCounters = gameCard.getCounters();
......@@ -162,7 +182,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
cntToRemove = Math.min(cntToRemove, gameCard.getCounters(counterType));
if (zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) {
if (sa.hasParam("UpTo")) {
if (sa.hasParam("UpTo") || num.equals("Any")) {
Map<String, Object> params = Maps.newHashMap();
params.put("Target", gameCard);
params.put("CounterType", type);
......@@ -179,10 +199,17 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
}
game.updateLastStateForCard(gameCard);
totalRemoved += cntToRemove;
}
}
}
}
if (totalRemoved > 0 && rememberAmount) {
// TODO use SpellAbility Remember later
card.addRemembered(Integer.valueOf(totalRemoved));
}
}
}
......@@ -121,6 +121,9 @@ public class DamageAllEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
......
......@@ -30,7 +30,7 @@ public class DamageDealEffect extends DamageBaseEffect {
final int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
List<GameObject> tgts = getTargets(sa);
if (tgts.isEmpty())
if (tgts.isEmpty())
return "";
final List<Card> definedSources = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DamageSource"), sa);
......@@ -131,15 +131,15 @@ public class DamageDealEffect extends DamageBaseEffect {
sa.setPreventMap(preventMap);
usedDamageMap = true;
}
final List<Card> definedSources = AbilityUtils.getDefinedCards(hostCard, sa.getParam("DamageSource"), sa);
if (definedSources == null || definedSources.isEmpty()) {
return;
}
for (Card source : definedSources) {
final Card sourceLKI = hostCard.getGame().getChangeZoneLKIInfo(source);
if (divideOnResolution) {
// Dividing Damage up to multiple targets using combat damage box
// Currently only used for Master of the Wild Hunt
......@@ -147,7 +147,7 @@ public class DamageDealEffect extends DamageBaseEffect {
if (players.isEmpty()) {
return;
}
CardCollection assigneeCards = new CardCollection();
// Do we have a way of doing this in a better fashion?
for (GameObject obj : tgts) {
......@@ -155,7 +155,7 @@ public class DamageDealEffect extends DamageBaseEffect {
assigneeCards.add((Card)obj);
}
}
Player assigningPlayer = players.get(0);
Map<Card, Integer> map = assigningPlayer.getController().assignCombatDamage(sourceLKI, assigneeCards, dmg, null, true);
for (Entry<Card, Integer> dt : map.entrySet()) {
......@@ -166,6 +166,9 @@ public class DamageDealEffect extends DamageBaseEffect {
preventMap.triggerPreventDamage(false);
// non combat damage cause lifegain there
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
return;
......@@ -201,7 +204,7 @@ public class DamageDealEffect extends DamageBaseEffect {
}
}
}
if (remember) {
source.addRemembered(damageMap.row(sourceLKI).keySet());
}
......@@ -210,6 +213,9 @@ public class DamageDealEffect extends DamageBaseEffect {
preventMap.triggerPreventDamage(false);
// non combat damage cause lifegain there
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
}
......
......@@ -132,6 +132,9 @@ public class DamageEachEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
......
......@@ -21,10 +21,12 @@ public class DamageResolveEffect extends SpellAbilityEffect {
if (preventMap != null) {
preventMap.triggerPreventDamage(false);
preventMap.clear();
}
// non combat damage cause lifegain there
if (damageMap != null) {
damageMap.triggerDamageDoneOnce(false, sa);
damageMap.clear();
}
}
......
......@@ -187,14 +187,14 @@ public class DigEffect extends SpellAbilityEffect {
if (!andOrValid.equals("")) {
andOrCards = CardLists.getValidCards(top, andOrValid.split(","), host.getController(), host, sa);
andOrCards.removeAll((Collection<?>)valid);
valid.addAll(andOrCards);
valid.addAll(andOrCards); //pfps need to add andOr cards to valid to have set of all valid cards set up
}
else {
andOrCards = new CardCollection();
}
}
else {
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser.
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser. pfps??
if (p == chooser && destZone1ChangeNum > 1) {
delayedReveal = null;
}
......@@ -238,55 +238,41 @@ public class DigEffect extends SpellAbilityEffect {
if (sa.hasParam("RandomOrder")) {
CardLists.shuffle(movedCards);
}
}
else {
} else {
String prompt;
if (sa.hasParam("PrimaryPrompt")) {
prompt = sa.getParam("PrimaryPrompt");
} else {
prompt = "Choose a card to put into " + destZone1.name();
if (destZone1.equals(ZoneType.Library)) {
if (libraryPosition == -1) {
prompt = "Choose a card to put on the bottom of {player's} library";
}
else if (libraryPosition == 0) {
prompt = "Choose a card to put on top of {player's} library";
}
}
}
movedCards = new CardCollection();
for (int i = 0; i < destZone1ChangeNum || (anyNumber && i < numToDig); i++) {
// let user get choice
Card chosen = null;
if (!valid.isEmpty()) {
// If we're choosing multiple cards, only need to show the reveal dialog the first time through.
boolean shouldReveal = (i == 0);
chosen = chooser.getController().chooseSingleEntityForEffect(valid, shouldReveal ? delayedReveal : null, sa, prompt, anyNumber || optional, p);
}
else {
if (i == 0) {
chooser.getController().notifyOfValue(sa, null, "No valid cards");
}
}
if (chosen == null) {
break;
}
movedCards.add(chosen);
valid.remove(chosen);
if (!andOrValid.equals("")) {
andOrCards.remove(chosen);
if (!chosen.isValid(andOrValid.split(","), host.getController(), host, sa)) {
valid = new CardCollection(andOrCards);
}
else if (!chosen.isValid(changeValid.split(","), host.getController(), host, sa)) {
valid.removeAll((Collection<?>)andOrCards);
}
}
}
if (sa.hasParam("PrimaryPrompt")) {
prompt = sa.getParam("PrimaryPrompt");
} else {
prompt = "Choose card(s) to put into " + destZone1.name();
if (destZone1.equals(ZoneType.Library)) {
if (libraryPosition == -1) {
prompt = "Choose card(s) to put on the bottom of {player's} library";
} else if (libraryPosition == 0) {
prompt = "Choose card(s) to put on top of {player's} library";
}
}
}
movedCards = new CardCollection();
if (valid.isEmpty()) {
chooser.getController().notifyOfValue(sa, null, "No valid cards");
} else {
if ( p == chooser ) { // the digger can still see all the dug cards when choosing
chooser.getController().tempShowCards(top);
}
List<Card> chosen;
if (!andOrValid.equals("")) {
valid.removeAll(andOrCards); //pfps remove andOr cards to get two two choices set up correctly
chosen = chooser.getController().chooseFromTwoListsForEffect(valid, andOrCards, optional, delayedReveal, sa, prompt, p);
} else {
int max = anyNumber ? valid.size() : Math.min(valid.size(),destZone1ChangeNum);
int min = (anyNumber || optional) ? 0 : max;
chosen = chooser.getController().chooseEntitiesForEffect(valid, min, max, delayedReveal, sa, prompt, p);
}
chooser.getController().endTempShowCards();
movedCards.addAll(chosen);
}
if (!changeValid.isEmpty() && !sa.hasParam("ExileFaceDown") && !sa.hasParam("NoReveal")) {
game.getAction().reveal(movedCards, chooser, true,
......
......@@ -153,6 +153,9 @@ public class FightEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
......
......@@ -4,17 +4,17 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import java.util.List;
import com.google.common.collect.Lists;
public class ScryEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final List<Player> tgtPlayers = getTargetPlayers(sa);
for (final Player p : tgtPlayers) {
for (final Player p : getTargetPlayers(sa)) {
sb.append(p.toString()).append(" ");
}
......@@ -36,19 +36,16 @@ public class ScryEffect extends SpellAbilityEffect {
boolean isOptional = sa.hasParam("Optional");
final TargetRestrictions tgt = sa.getTargetRestrictions();
final List<Player> tgtPlayers = getTargetPlayers(sa);
final List<Player> players = Lists.newArrayList(); // players really affected
for (final Player p : tgtPlayers) {
if ((tgt == null) || p.canBeTargetedBy(sa)) {
if (isOptional && !p.getController().confirmAction(sa, null, "Do you want to scry?")) {
continue;
}
p.scry(num, sa);
}
}
// Optional here for spells that have optional multi-player scrying
for (final Player p : getTargetPlayers(sa)) {
if ( (!sa.usesTargeting() || p.canBeTargetedBy(sa)) &&
(!isOptional || p.getController().confirmAction(sa, null, "Do you want to scry?")) ) {
players.add(p);
}
}
sa.getActivatingPlayer().getGame().getAction().scry(players, num, sa);
}
}
......@@ -19,6 +19,13 @@ import forge.game.trigger.TriggerType;
public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
private Table<Card, GameEntity, Integer> dataMap = HashBasedTable.create();
public CardDamageMap(Table<Card, GameEntity, Integer> damageMap) {
this.putAll(damageMap);
}
public CardDamageMap() {
}
public void triggerPreventDamage(boolean isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> e : this.columnMap().entrySet()) {
int sum = 0;
......
......@@ -430,6 +430,9 @@ public class CardFactory {
private static void readCardFace(Card c, ICardFace face) {
// Name first so Senty has the Card name
c.setName(face.getName());
for (String r : face.getReplacements()) c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true));
for (String s : face.getStaticAbilities()) c.addStaticAbility(s);
for (String t : face.getTriggers()) c.addTrigger(TriggerHandler.parseTrigger(t, c, true));
......@@ -439,7 +442,6 @@ public class CardFactory {
// keywords not before variables
c.addIntrinsicKeywords(face.getKeywords(), false);
c.setName(face.getName());
c.setManaCost(face.getManaCost());
c.setText(face.getNonAbilityText());
......
......@@ -25,7 +25,6 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import forge.GameCommand;
import forge.card.*;
import forge.card.mana.ManaAtom;
import forge.card.mana.ManaCost;
......@@ -2099,7 +2098,7 @@ public class CardFactoryUtil {
String abStr = "DB$ PutCounter | Defined$ Self | CounterType$ " + splitkw[1]
+ " | ETB$ True | CounterNum$ " + amount;
if (!StringUtils.isNumeric(amount)) {
if (!StringUtils.isNumeric(amount) && card.hasSVar(amount)) {
abStr += " | References$ " + amount;
}
......@@ -3452,12 +3451,10 @@ public class CardFactoryUtil {
sb.append(m);
sb.append(" (").append(inst.getReminderText()).append(")");
final ReplacementEffect re = makeEtbCounter(sb.toString(), card, intrinsic);
if ("Sunburst".equals(m)) {
card.setSVar(m, "Count$Converge");
re.getOverridingAbility().setSVar("Sunburst", "Count$Converge");
}
final ReplacementEffect re = makeEtbCounter(sb.toString(), card, intrinsic);
inst.addReplacement(re);
} else if (keyword.equals("Rebound")) {
String repeffstr = "Event$ Moved | ValidCard$ Card.Self+wasCastFromHand+YouOwn+YouCtrl "
......@@ -3512,6 +3509,19 @@ public class CardFactoryUtil {
String sb = "etbCounter:LORE:1:no Condition:no desc";
final ReplacementEffect re = makeEtbCounter(sb, card, intrinsic);
inst.addReplacement(re);
} else if (keyword.equals("Sunburst")) {
// Rule 702.43a If this object is entering the battlefield as a creature,
// ignoring any type-changing effects that would affect it
CounterType t = card.isCreature() ? CounterType.P1P1 : CounterType.CHARGE;
StringBuilder sb = new StringBuilder("etbCounter:");
sb.append(t).append(":Sunburst:no Condition:");
sb.append("Sunburst (").append(inst.getReminderText()).append(")");
final ReplacementEffect re = makeEtbCounter(sb.toString(), card, intrinsic);
re.getOverridingAbility().setSVar("Sunburst", "Count$Converge");
inst.addReplacement(re);
} else if (keyword.equals("Totem armor")) {
String repeffstr = "Event$ Destroy | ActiveZones$ Battlefield | ValidCard$ Card.EnchantedBy"
......@@ -3703,12 +3713,24 @@ public class CardFactoryUtil {
final String[] k = keyword.split(":");
final String magnitude = k[1];
final String manacost = k[2];
final String reduceCost = k.length > 3 ? k[3] : null;
Set<String> references = Sets.newHashSet();
String desc = "Adapt " + magnitude;
String effect = "AB$ PutCounter | Cost$ " + manacost + " | Adapt$ True | CounterNum$ " + magnitude
+ " | CounterType$ P1P1 | StackDescription$ SpellDescription";
if (reduceCost != null) {
effect += "| ReduceCost$ " + reduceCost;
references.add(reduceCost);
desc += ". This ability costs {1} less to activate for each instant and sorcery card in your graveyard.";
}
if (!references.isEmpty()) {
effect += "| References$ " + TextUtil.join(references, ",");
}
effect += "| SpellDescription$ " + desc + " (" + inst.getReminderText() + ")";
final SpellAbility sa = AbilityFactory.getAbility(effect, card);
......@@ -4170,18 +4192,17 @@ public class CardFactoryUtil {
String effect = "AB$ PutCounter | Cost$ " + manacost + " ExileFromGrave<1/CARDNAME> " +
"| ActivationZone$ Graveyard | ValidTgts$ Creature | CounterType$ P1P1 " +
"| CounterNum$ ScavengeX | SorcerySpeed$ True | References$ ScavengeX " +
"| CounterNum$ ScavengeX | SorcerySpeed$ True " +
"| PrecostDesc$ Scavenge | CostDesc$ " + ManaCostParser.parse(manacost) +
"| SpellDescription$ (" + inst.getReminderText() + ")";
card.setSVar("ScavengeX", "Count$CardPower");