...
 
Commits (95)
......@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.23-SNAPSHOT</version>
<version>1.6.24-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>
......
......@@ -18,12 +18,10 @@
package forge.ai;
import com.esotericsoftware.minlog.Log;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.ExploreAi;
import forge.ai.simulation.SpellAbilityPicker;
......@@ -1784,75 +1782,6 @@ public class AiController {
throw new UnsupportedOperationException("AI is not supposed to reach this code at the moment");
}
public Map<GameEntity, CounterType> chooseProliferation(final SpellAbility sa) {
final Map<GameEntity, CounterType> result = Maps.newHashMap();
final List<Player> allies = player.getAllies();
allies.add(player);
final List<Player> enemies = player.getOpponents();
final Function<Card, CounterType> predProliferate = new Function<Card, CounterType>() {
@Override
public CounterType apply(Card crd) {
//fast way out, no need to check other stuff
if (!crd.hasCounters()) {
return null;
}
// cards controlled by ai or ally with Vanishing or Fading
// and exaclty one counter of the specifice type gets high priority to keep the card
if (allies.contains(crd.getController())) {
// except if its a Chronozoa, because it WANTS to be removed to make more
if (crd.hasKeyword(Keyword.VANISHING) && !"Chronozoa".equals(crd.getName())) {
if (crd.getCounters(CounterType.TIME) == 1) {
return CounterType.TIME;
}
} else if (crd.hasKeyword(Keyword.FADING)) {
if (crd.getCounters(CounterType.FADE) == 1) {
return CounterType.FADE;
}
}
}
for (final Entry<CounterType, Integer> c1 : crd.getCounters().entrySet()) {
// if card can not recive the given counter, try another one
if (!crd.canReceiveCounters(c1.getKey())) {
continue;
}
if (ComputerUtil.isNegativeCounter(c1.getKey(), crd) && enemies.contains(crd.getController())) {
return c1.getKey();
}
if (!ComputerUtil.isNegativeCounter(c1.getKey(), crd) && allies.contains(crd.getController())) {
return c1.getKey();
}
}
return null;
}
};
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
CounterType ct = predProliferate.apply(c);
if (ct != null)
result.put(c, ct);
}
for (Player e : enemies) {
// TODO In the future check of enemies can get poison counters and give them some other bad counter type
if (e.getCounters(CounterType.POISON) > 0) {
result.put(e, CounterType.POISON);
}
}
for (Player pl : allies) {
if (pl.getCounters(CounterType.EXPERIENCE) > 0) {
result.put(pl, CounterType.EXPERIENCE);
} else if (pl.getCounters(CounterType.ENERGY) > 0) {
result.put(pl, CounterType.ENERGY);
}
}
return result;
}
public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional) {
if (sa == null || sa.getApi() == null) {
throw new UnsupportedOperationException();
......
......@@ -953,14 +953,12 @@ public class PlayerControllerAi extends PlayerController {
return true;
}
public Map<GameEntity, CounterType> chooseProliferation(SpellAbility sa) {
return brains.chooseProliferation(sa);
}
@Override
public boolean chooseTargetsFor(SpellAbility currentAbility) {
return brains.doTrigger(currentAbility, true);
}
@Override
public boolean chooseCardsPile(SpellAbility sa, CardCollectionView pile1, CardCollectionView pile2, String faceUp) {
if (faceUp.equals("True")) {
// AI will choose the first pile if it is larger or the same
......
......@@ -21,6 +21,7 @@ public enum SpellApiToAi {
.put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class)
.put(ApiType.AddPhase, AddPhaseAi.class)
.put(ApiType.AddTurn, AddTurnAi.class)
.put(ApiType.Amass, AmassAi.class)
.put(ApiType.Animate, AnimateAi.class)
.put(ApiType.AnimateAll, AnimateAllAi.class)
.put(ApiType.Attach, AttachAi.class)
......
package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.phase.PhaseHandler;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class AmassAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
CardCollection aiArmies = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army"));
Card host = sa.getHostCard();
final Game game = ai.getGame();
if (!aiArmies.isEmpty()) {
if (CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterType.P1P1)) <= 0) {
return false;
}
} else {
final String tokenScript = "b_0_0_zombie_army";
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
Card token = TokenInfo.getProtoType(tokenScript, sa);
if (token == null) {
return false;
}
token.setController(ai, 0);
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
boolean result = true;
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(token);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
if (token.canReceiveCounters(CounterType.P1P1)) {
token.setCounters(CounterType.P1P1, amount);
}
if (token.isCreature() && token.getNetToughness() < 1) {
result = false;
}
//reset static abilities
game.getAction().checkStaticAbilities(false);
if (!result) {
return false;
}
}
return true;
}
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
// TODO: Special check for instant speed logic? Something like Lazotep Plating.
/*
boolean isInstant = sa.getRestrictions().isInstantSpeed();
CardCollection aiArmies = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army"));
if (isInstant) {
}
*/
return true;
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || checkApiLogic(ai, sa);
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
Iterable<Card> better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterType.P1P1));
if (Iterables.isEmpty(better)) {
better = options;
}
return ComputerUtilCard.getBestAI(better);
}
}
......@@ -38,10 +38,14 @@ public class CountersProliferateAi extends SpellAbilityAi {
cperms.addAll(CardLists.filter(p.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
if (crd.hasCounters()) {
if (!crd.hasCounters()) {
return false;
}
if (crd.isPlaneswalker()) {
return true;
}
// iterate only over existing counters
for (final Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= 1 && !ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
......@@ -61,7 +65,11 @@ public class CountersProliferateAi extends SpellAbilityAi {
hperms.addAll(CardLists.filter(o.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
if (crd.hasCounters()) {
if (!crd.hasCounters()) {
return false;
}
if (crd.isPlaneswalker()) {
return false;
}
......@@ -123,6 +131,15 @@ public class CountersProliferateAi extends SpellAbilityAi {
}
for (final Card c : Iterables.filter(options, Card.class)) {
// AI planeswalker always, opponent planeswalkers never
if (c.isPlaneswalker()) {
if (c.getController().isOpponentOf(ai)) {
continue;
} else {
return (T)c;
}
}
final Card lki = CardUtil.getLKICopy(c);
// update all the counters there
boolean hasNegative = false;
......
......@@ -150,14 +150,17 @@ public class DiscardAi extends SpellAbilityAi {
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = ComputerUtil.getOpponentFor(ai);
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
return false;
}
if (tgt != null) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
return true;
for (Player opp : ai.getOpponents()) {
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
continue;
} else if (!opp.canDiscardBy(sa)) { // e.g. Tamiyo, Collector of Tales
continue;
}
if (tgt != null) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
return true;
}
}
}
return false;
......
......@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.23-SNAPSHOT</version>
<version>1.6.24-SNAPSHOT</version>
</parent>
<artifactId>forge-core</artifactId>
......
......@@ -138,7 +138,8 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override public CardRarity getRarity() { return CardRarity.None; }
// Unfortunately this is a property of token, cannot move it outside of class
public String getImageFilename() { return imageFileName.get(0); }
public String getImageFilename() { return getImageFilename(1); }
public String getImageFilename(int idx) { return imageFileName.get(idx-1); }
@Override public String getItemType() { return "Token"; }
......@@ -147,6 +148,10 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override
public String getImageKey(boolean altState) {
int idx = MyRandom.getRandom().nextInt(artIndex);
return ImageKeys.TOKEN_PREFIX + imageFileName.get(idx).replace(" ", "_");
return getImageKey(idx);
}
public String getImageKey(int artIndex) {
return ImageKeys.TOKEN_PREFIX + imageFileName.get(artIndex).replace(" ", "_");
}
}
......@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.23-SNAPSHOT</version>
<version>1.6.24-SNAPSHOT</version>
</parent>
<artifactId>forge-game</artifactId>
......
......@@ -7,28 +7,30 @@ import forge.deck.Deck;
import forge.deck.DeckFormat;
import forge.deck.DeckSection;
import forge.game.player.RegisteredPlayer;
import forge.util.Localizer;
public enum GameType {
Sealed (DeckFormat.Limited, true, true, true, "Sealed", ""),
Draft (DeckFormat.Limited, true, true, true, "Draft", ""),
Winston (DeckFormat.Limited, true, true, true, "Winston", ""),
Gauntlet (DeckFormat.Constructed, false, true, true, "Gauntlet", ""),
Tournament (DeckFormat.Constructed, false, true, true, "Tournament", ""),
Quest (DeckFormat.QuestDeck, true, true, false, "Quest", ""),
QuestDraft (DeckFormat.Limited, true, true, true, "Quest Draft", ""),
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "Planar Conquest", ""),
Puzzle (DeckFormat.Puzzle, false, false, false, "Puzzle", "Solve a puzzle from the given game state"),
Constructed (DeckFormat.Constructed, false, true, true, "Constructed", ""),
DeckManager (DeckFormat.Constructed, false, true, true, "Deck Manager", ""),
Vanguard (DeckFormat.Vanguard, true, true, true, "Vanguard", "Each player has a special \"Avatar\" card that affects the game."),
Commander (DeckFormat.Commander, false, false, false, "Commander", "Each player has a legendary \"General\" card which can be cast at any time and determines deck colors."),
TinyLeaders (DeckFormat.TinyLeaders, false, false, false, "Tiny Leaders", "Each player has a legendary \"General\" card which can be cast at any time and determines deck colors. Each card must have CMC less than 4."),
Brawl (DeckFormat.Brawl, false, false, false, "Brawl", "Each player has a legendary \"General\" card which can be cast at any time and determines deck colors. Only cards legal in Standard may be used."),
Planeswalker (DeckFormat.PlanarConquest, false, false, true, "Planeswalker", "Each player has a Planeswalker card which can be cast at any time."),
Planechase (DeckFormat.Planechase, false, false, true, "Planechase", "Plane cards apply global effects. The Plane card changes when a player rolls \"Planeswalk\" on the planar die."),
Archenemy (DeckFormat.Archenemy, false, false, true, "Archenemy", "One player is the Archenemy and fights the other players by playing Scheme cards."),
ArchenemyRumble (DeckFormat.Archenemy, false, false, true, "Archenemy Rumble", "All players are Archenemies and can play Scheme cards."),
MomirBasic (DeckFormat.Constructed, false, false, false, "Momir Basic", "Each player has a deck containing 60 basic lands and the Momir Vig avatar.", new Function<RegisteredPlayer, Deck>() {
Sealed (DeckFormat.Limited, true, true, true, "lblSealed", ""),
Draft (DeckFormat.Limited, true, true, true, "lblDraft", ""),
Winston (DeckFormat.Limited, true, true, true, "lblWinston", ""),
Gauntlet (DeckFormat.Constructed, false, true, true, "lblGauntlet", ""),
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),
Puzzle (DeckFormat.Puzzle, false, false, false, "lblPuzzle", "lblPuzzleDesc"),
Constructed (DeckFormat.Constructed, false, true, true, "lblConstructed", ""),
DeckManager (DeckFormat.Constructed, false, true, true, "lblDeckManager", ""),
Vanguard (DeckFormat.Vanguard, true, true, true, "lblVanguard", "lblVanguardDesc"),
Commander (DeckFormat.Commander, false, false, false, "lblCommander", "lblCommanderDesc"),
TinyLeaders (DeckFormat.TinyLeaders, false, false, false, "lblTinyLeaders", "lblTinyLeadersDesc"),
Brawl (DeckFormat.Brawl, false, false, false, "lblBrawl", "lblBrawlDesc"),
Planeswalker (DeckFormat.PlanarConquest, false, false, true, "lblPlaneswalker", "lblPlaneswalkerDesc"),
Planechase (DeckFormat.Planechase, false, false, true, "lblPlanechase", "lblPlanechaseDesc"),
Archenemy (DeckFormat.Archenemy, false, false, true, "lblArchenemy", "lblArchenemyDesc"),
ArchenemyRumble (DeckFormat.Archenemy, false, false, true, "lblArchenemyRumble", "lblArchenemyRumbleDesc"),
MomirBasic (DeckFormat.Constructed, false, false, false, "lblMomirBasic", "lblMomirBasicDesc", new Function<RegisteredPlayer, Deck>() {
@Override
public Deck apply(RegisteredPlayer player) {
Deck deck = new Deck();
......@@ -43,7 +45,7 @@ public enum GameType {
return deck;
}
}),
MoJhoSto (DeckFormat.Constructed, false, false, false, "MoJhoSto", "Each player has a deck containing 60 basic lands and the Momir Vig, Jhoira of the Ghitu, and Stonehewer Giant avatars.", new Function<RegisteredPlayer, Deck>() {
MoJhoSto (DeckFormat.Constructed, false, false, false, "lblMoJhoSto", "lblMoJhoStoDesc", new Function<RegisteredPlayer, Deck>() {
@Override
public Deck apply(RegisteredPlayer player) {
Deck deck = new Deck();
......@@ -69,14 +71,19 @@ public enum GameType {
private final Function<RegisteredPlayer, Deck> deckAutoGenerator;
private GameType(DeckFormat deckFormat0, boolean isCardPoolLimited0, boolean canSideboard0, boolean addWonCardsMidgame0, String name0, String description0) {
this(deckFormat0, isCardPoolLimited0, canSideboard0, addWonCardsMidgame0, name0, description0, null);
}
private GameType(DeckFormat deckFormat0, boolean isCardPoolLimited0, boolean canSideboard0, boolean addWonCardsMidgame0, String name0, String description0, Function<RegisteredPlayer, Deck> deckAutoGenerator0) {
final Localizer localizer = forge.util.Localizer.getInstance();
deckFormat = deckFormat0;
isCardPoolLimited = isCardPoolLimited0;
canSideboard = canSideboard0;
addWonCardsMidGame = addWonCardsMidgame0;
name = name0;
name = localizer.getMessage(name0);
if (description0.length()>0) {
description0 = localizer.getMessage(description0);
}
description = description0;
deckAutoGenerator = deckAutoGenerator0;
}
......
......@@ -787,6 +787,9 @@ public class AbilityUtils {
if (type.contains("Card")) {
o = sa.getTriggeringObject("Card");
}
else if (type.contains("Object")) {
o = sa.getTriggeringObject("Object");
}
else if (type.contains("Attacker")) {
o = sa.getTriggeringObject("Attacker");
}
......@@ -801,8 +804,8 @@ public class AbilityUtils {
return new CardCollection();
}
if (type.equals("Triggered") || (type.equals("TriggeredCard")) || (type.equals("TriggeredAttacker"))
|| (type.equals("TriggeredBlocker"))) {
if (type.equals("Triggered") || type.equals("TriggeredCard") || type.equals("TriggeredObject")
|| type.equals("TriggeredAttacker") || type.equals("TriggeredBlocker")) {
type = "Card.Self";
}
......@@ -810,6 +813,9 @@ public class AbilityUtils {
if (type.contains("TriggeredCard")) {
type = TextUtil.fastReplace(type, "TriggeredCard", "Card");
}
else if (type.contains("TriggeredObject")) {
type = TextUtil.fastReplace(type, "TriggeredObject", "Card");
}
else if (type.contains("TriggeredAttacker")) {
type = TextUtil.fastReplace(type, "TriggeredAttacker", "Card");
}
......
......@@ -17,6 +17,7 @@ public enum ApiType {
AddOrRemoveCounter (CountersPutOrRemoveEffect.class),
AddPhase (AddPhaseEffect.class),
AddTurn (AddTurnEffect.class),
Amass (AmassEffect.class),
Animate (AnimateEffect.class),
AnimateAll (AnimateAllEffect.class),
Attach (AttachEffect.class),
......
package forge.game.ability.effects;
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.CardPredicates;
import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventTokenCreated;
import forge.game.player.Player;
import forge.game.player.PlayerController;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Lang;
public class AmassEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Amass ");
final Card card = sa.getHostCard();
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
sb.append(amount).append(" (Put ");
sb.append(Lang.nounWithNumeral(amount, "+1/+1 counter"));
sb.append("on an Army you control. If you don’t control one, create a 0/0 black Zombie Army creature token first.)");
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
final Game game = card.getGame();
final Player activator = sa.getActivatingPlayer();
final PlayerController pc = activator.getController();
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
final boolean remember = sa.hasParam("RememberAmass");
// create army token if needed
if (CardLists.count(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army")) == 0) {
final String tokenScript = "b_0_0_zombie_army";
final Card prototype = TokenInfo.getProtoType(tokenScript, sa);
for (final Card tok : TokenInfo.makeTokensFromPrototype(prototype, activator, 1, true)) {
// Should this be catching the Card that's returned?
Card c = game.getAction().moveToPlay(tok, sa);
c.updateStateForView();
}
game.fireEvent(new GameEventTokenCreated());
}
CardCollectionView tgtCards = CardLists.getType(activator.getCardsIn(ZoneType.Battlefield), "Army");
tgtCards = pc.chooseCardsForEffect(tgtCards, sa, "Choose an army to put counters on", 1, 1, false);
for(final Card tgtCard : tgtCards) {
tgtCard.addCounter(CounterType.P1P1, amount, activator, true);
game.updateLastStateForCard(tgtCard);
if (remember) {
card.addRemembered(tgtCard);
}
}
}
}
......@@ -48,8 +48,11 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
// Sac a permanent in presence of Sigarda, Host of Herons
// TODO: generalize this by testing if the unless cost can be paid
if (unlessCost.startsWith("Sac<")) {
if (saChoice.getActivatingPlayer().isOpponentOf(p)
&& p.hasKeyword("Spells and abilities your opponents control can't cause you to sacrifice permanents.")) {
if (!p.canSacrificeBy(saChoice)) {
saToRemove.add(saChoice);
}
} else if (unlessCost.startsWith("Discard<")) {
if (!p.canDiscardBy(sa)) {
saToRemove.add(saChoice);
}
}
......
......@@ -21,8 +21,8 @@ public class CountersProliferateEffect extends SpellAbilityEffect {
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
sb.append("Proliferate.");
sb.append(" (You choose any number of permanents and/or players with ");
sb.append("counters on them, then give each another counter of each kind already there.)");
sb.append(" (Choose any number of permanents and/or players,");
sb.append(" then give each another counter of each kind already there.)");
return sb.toString();
}
......
......@@ -2031,7 +2031,7 @@ public class Card extends GameEntity implements Comparable<Card> {
final String[] k = keyword.split(":");
final String manacost = k[1];
final Cost cost = new Cost(manacost, false);
StringBuilder sbCost = new StringBuilder(k[0]);
if (!cost.isOnlyManaCost()) {
sbCost.append("—");
......@@ -2081,7 +2081,7 @@ public class Card extends GameEntity implements Comparable<Card> {
|| keyword.startsWith("Miracle") || keyword.startsWith("Recover")) {
final String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false);
StringBuilder sbCost = new StringBuilder(k[0]);
if (!cost.isOnlyManaCost()) {
sbCost.append("—");
......@@ -2107,13 +2107,13 @@ public class Card extends GameEntity implements Comparable<Card> {
sbAfter.append(" (" + inst.getReminderText() + ")").append("\r\n");
} else if (keyword.equals("Storm")) {
sbAfter.append("Storm (");
sbAfter.append("When you cast this spell, copy it for each spell cast before it this turn.");
if (strSpell.contains("Target") || strSpell.contains("target")) {
sbAfter.append(" You may choose new targets for the copies.");
}
sbAfter.append(")");
sbAfter.append("\r\n");
} else if (keyword.startsWith("Replicate")) {
......@@ -5523,8 +5523,7 @@ public class Card extends GameEntity implements Comparable<Card> {
}
}
if (getController().isOpponentOf(source.getActivatingPlayer())
&& getController().hasKeyword("Spells and abilities your opponents control can't cause you to sacrifice permanents.")) {
if (!getController().canSacrificeBy(source)) {
return false;
}
......@@ -5964,4 +5963,16 @@ public class Card extends GameEntity implements Comparable<Card> {
rE.setTemporarilySuppressed(false);
}
}
public boolean canBeDiscardedBy(SpellAbility sa) {
if (!isInZone(ZoneType.Hand)) {
return false;
}
if (!getOwner().canDiscardBy(sa)) {
return false;
}
return true;
}
}
......@@ -1032,8 +1032,9 @@ public class CardFactoryUtil {
// Count$TopOfLibraryCMC
if (sq[0].contains("TopOfLibraryCMC")) {
final Card topCard = cc.getCardsIn(ZoneType.Library).getFirst();
return doXMath(topCard == null ? 0 : topCard.getCMC(), m, c);
int cmc = cc.getCardsIn(ZoneType.Library).isEmpty() ? 0 :
cc.getCardsIn(ZoneType.Library).getFirst().getCMC();
return doXMath(cmc, m, c);
}
// Count$EnchantedControllerCreatures
......
......@@ -18,6 +18,7 @@
package forge.game.cost;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
......@@ -106,17 +107,20 @@ public class CostDiscard extends CostPartWithList {
public final boolean canPay(final SpellAbility ability, final Player payer) {
final Card source = ability.getHostCard();
CardCollectionView handList = payer.getCardsIn(ZoneType.Hand);
CardCollectionView handList = payer.canDiscardBy(ability) ? payer.getCardsIn(ZoneType.Hand) : CardCollection.EMPTY;
String type = this.getType();
final Integer amount = this.convertAmount();
if (this.payCostFromSource()) {
if (!source.isInZone(ZoneType.Hand)) {
if (!source.canBeDiscardedBy(ability)) {
return false;
}
}
else {
if (type.equals("Hand")) {
if (!payer.canDiscardBy(ability)) {
return false;
}
// this will always work
}
else if (type.equals("LastDrawn")) {
......
......@@ -455,11 +455,11 @@ public class Player extends GameEntity implements Comparable<Player> {
}
return true;
}
public final int loseLife(final int toLose) {
return loseLife(toLose,false);
}
public final int loseLife(final int toLose, final boolean manaBurn) {
int lifeLost = 0;
if (!canLoseLife()) {
......@@ -1555,6 +1555,10 @@ public class Player extends GameEntity implements Comparable<Player> {
}
public final Card discard(final Card c, final SpellAbility sa, CardZoneTable table) {
if (!c.canBeDiscardedBy(sa)) {
return null;
}
// TODO: This line should be moved inside CostPayment somehow
/*if (sa != null) {
sa.addCostToHashList(c, "Discarded");
......@@ -2935,4 +2939,27 @@ public class Player extends GameEntity implements Comparable<Player> {
return CardLists.count(attachedCards, CardPredicates.Presets.CURSE) > 0;
}
public boolean canDiscardBy(SpellAbility sa) {
if (sa == null) {
return true;
}
if (isOpponentOf(sa.getActivatingPlayer()) && hasKeyword("Spells and abilities your opponents control can't cause you to discard cards.")) {
return false;
}
return true;
}
public boolean canSacrificeBy(SpellAbility sa) {
if (sa == null) {
return true;
}
if (isOpponentOf(sa.getActivatingPlayer()) && hasKeyword("Spells and abilities your opponents control can't cause you to sacrifice permanents.")) {
return false;
}
return true;
}
}
......@@ -21,7 +21,16 @@ public final class PlayerPredicates {
}
};
}
public static final Predicate<Player> canDiscardBy(final SpellAbility source) {
return new Predicate<Player>() {
@Override
public boolean apply(final Player p) {
return p.canDiscardBy(source);
}
};
}
public static final Predicate<Player> isOpponentOf(final Player player) {
return new Predicate<Player>() {
@Override
......
......@@ -48,7 +48,15 @@ public class StaticAbilityCantTarget {
final Player activator = spellAbility.getActivatingPlayer();
if (params.containsKey("AffectedZone")) {
if (!card.isInZone(ZoneType.smartValueOf(params.get("AffectedZone")))) {
boolean inZone = false;
for (final ZoneType zt : ZoneType.listValueOf(params.get("AffectedZone"))) {
if (card.isInZone(zt)) {
inZone = true;
break;
}
}
if (!inZone) {
return false;
}
} else { // default zone is battlefield
......
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.game.trigger;
import java.util.Map;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
/**
* <p>
* Trigger_SpellAbilityCopy class.
* </p>
*
* @author Forge
* @version $Id$
*/
public class TriggerSpellAbilityCopy extends Trigger {
/**
* <p>
* Constructor for Trigger_SpellAbilityCopy.
* </p>
*
* @param params
* a {@link java.util.HashMap} object.
* @param host
* a {@link forge.game.card.Card} object.
* @param intrinsic
* the intrinsic
*/
public TriggerSpellAbilityCopy(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
/** {@inheritDoc} */
@Override
public final boolean performTest(final Map<String, Object> runParams2) {
final SpellAbility spellAbility = (SpellAbility) runParams2.get("CopySA");
if (spellAbility == null) {
System.out.println("TriggerSpellAbilityCopy performTest encountered spellAbility == null. runParams2 = " + runParams2);
return false;
}
final Card cast = spellAbility.getHostCard();
final Game game = cast.getGame();
final SpellAbilityStackInstance si = game.getStack().getInstanceFromSpellAbility(spellAbility);
if (this.getMode() == TriggerType.SpellCopy) {
if (!spellAbility.isSpell()) {
return false;
}
}
if (hasParam("ValidCard")) {
if (!matchesValid(cast, getParam("ValidCard").split(","), getHostCard())) {
return false;
}
}
if (hasParam("ValidSA")) {
if (!matchesValid(spellAbility, getParam("ValidSA").split(","), getHostCard())) {
return false;
}
}
if (hasParam("ValidActivatingPlayer")) {
if (si == null || !matchesValid(si.getSpellAbility(true).getActivatingPlayer(), getParam("ValidActivatingPlayer")
.split(","), getHostCard())) {
return false;
}
}
return true;
}
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa) {
final SpellAbility copySA = (SpellAbility) getRunParams().get("CopySA");
final SpellAbilityStackInstance si = sa.getHostCard().getGame().getStack().getInstanceFromSpellAbility(copySA);
sa.setTriggeringObject("Card", copySA.getHostCard());
sa.setTriggeringObject("SpellAbility", copySA);
sa.setTriggeringObject("StackInstance", si);
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append("Card: ").append(sa.getTriggeringObject("Card")).