Merge branch 'Card-Forge:master' into charming

This commit is contained in:
tool4ever
2022-07-08 17:19:46 +02:00
committed by GitHub
3427 changed files with 29490 additions and 22325 deletions

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.50-SNAPSHOT</version>
<version>1.6.54-SNAPSHOT</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -20,6 +20,7 @@ import forge.game.card.CardPredicates;
import forge.game.card.CardState;
import forge.game.card.CardView;
import forge.game.card.IHasCardView;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
@@ -34,6 +35,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
/** The host card. */
protected Card hostCard;
protected CardState cardState = null;
protected KeywordInterface keyword = null;
/** The map params. */
protected Map<String, String> originalMapParams = Maps.newHashMap(),
@@ -55,15 +57,13 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
/** Keys of descriptive (text) parameters. */
private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder()
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription").build();
/** Keys to be followed as SVar names when changing text. */
private static final ImmutableList<String> mutableKeys = ImmutableList.<String>builder()
.add("AddAbility").build();
/**
* Keys that should not changed
*/
private static final ImmutableList<String> noChangeKeys = ImmutableList.<String>builder()
.add("TokenScript", "LegacyImage", "TokenImage", "NewName", "ChooseFromList").build();
.add("TokenScript", "LegacyImage", "TokenImage", "NewName", "ChooseFromList")
.add("AddAbility").build();
/**
* <p>
@@ -140,6 +140,14 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
this.hostCard = c;
}
public KeywordInterface getKeyword() {
return this.keyword;
}
public void setKeyword(final KeywordInterface kw) {
this.keyword = kw;
}
/**
* <p>
* isSecondary.
@@ -259,6 +267,18 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
}
if (params.containsKey("Revolt")) {
if ("True".equalsIgnoreCase(params.get("Revolt")) != hostController.hasRevolt()) return false;
else if ("None".equalsIgnoreCase(params.get("Revolt"))) {
boolean none = true;
for (Player p : game.getRegisteredPlayers()) {
if (p.hasRevolt()) {
none = false;
break;
}
}
if (!none) {
return false;
}
}
}
if (params.containsKey("Desert")) {
if ("True".equalsIgnoreCase(params.get("Desert")) != hostController.hasDesert()) return false;
@@ -422,6 +442,16 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (!Expressions.compare(sVar, svarOperator, operandValue)) {
return false;
}
if (hasParam("CheckSecondSVar")) {
final int sVar2 = AbilityUtils.calculateAmount(this.hostCard, getParam("CheckSecondSVar"), this);
final String comparator2 = getParamOrDefault("SecondSVarCompare", "GE1");
final String svarOperator2 = comparator2.substring(0, 2);
final String svarOperand2 = comparator2.substring(2);
final int operandValue2 = AbilityUtils.calculateAmount(this.hostCard, svarOperand2, this);
if (!Expressions.compare(sVar2, svarOperator2, operandValue2)) {
return false;
}
}
}
if (params.containsKey("ManaSpent")) {
@@ -489,11 +519,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
} else if (descriptiveKeys.contains(key)) {
// change descriptions differently
newValue = AbilityUtils.applyDescriptionTextChangeEffects(value, this);
} else if (mutableKeys.contains(key)) {
// follow SVar and change it
final String originalSVarValue = hostCard.getSVar(value);
hostCard.changeSVar(value, AbilityUtils.applyAbilityTextChangeEffects(originalSVarValue, this));
newValue = null;
} else if (this.getHostCard().hasSVar(value)) {
// don't change literal SVar names!
newValue = null;
@@ -652,6 +677,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
copy.setCardState(cardState);
// dont use setHostCard to not trigger the not copied parts yet
copy.hostCard = host;
copy.keyword = this.keyword;
}
abstract public List<Object> getTriggerRemembered();

View File

@@ -202,6 +202,11 @@ public class ForgeScript {
return sa.hasParam("Nightbound");
} else if (property.equals("paidPhyrexianMana")) {
return sa.getSpendPhyrexianMana();
} else if (property.startsWith("ManaSpent")) {
String[] k = property.split(" ", 2);
String comparator = k[1].substring(0, 2);
int y = AbilityUtils.calculateAmount(sa.getHostCard(), k[1].substring(2), sa);
return Expressions.compare(sa.getPayingMana().size(), comparator, y);
} else if (property.startsWith("ManaFrom")) {
final String fromWhat = property.substring(8);
boolean found = false;
@@ -247,7 +252,7 @@ public class ForgeScript {
} else if (property.startsWith("cmc")) {
int y = 0;
// spell was on the stack
if (sa.getCardState().getCard().isInZone(ZoneType.Stack)) {
if (sa.getHostCard().isInZone(ZoneType.Stack)) {
y = sa.getHostCard().getCMC();
} else {
y = sa.getPayCosts().getTotalMana().getCMC();

View File

@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -47,6 +48,7 @@ import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardDamageHistory;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
@@ -78,6 +80,7 @@ import forge.trackable.Tracker;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.Visitor;
import forge.util.collect.FCollection;
/**
* Represents the state of a <i>single game</i>, a new instance is created for each game.
@@ -119,10 +122,14 @@ public class Game {
private Table<CounterType, Player, List<Pair<Card, Integer>>> countersAddedThisTurn = HashBasedTable.create();
private FCollection<CardDamageHistory> globalDamageHistory = new FCollection<>();
private IdentityHashMap<Pair<Integer, Boolean>, Pair<Card, GameEntity>> damageThisTurnLKI = new IdentityHashMap<>();
private Map<Player, Card> topLibsCast = Maps.newHashMap();
private Map<Card, Integer> facedownWhileCasting = Maps.newHashMap();
private Player monarch = null;
private Player initiative = null;
private Player monarchBeginTurn = null;
private Player startingPlayer;
@@ -173,6 +180,13 @@ public class Game {
this.monarchBeginTurn = monarchBeginTurn;
}
public Player getHasInitiative() {
return initiative;
}
public void setHasInitiative(final Player p ) {
initiative = p;
}
public CardCollectionView getLastStateBattlefield() {
return lastStateBattlefield;
}
@@ -243,7 +257,7 @@ public class Game {
if (c == null) {
return null;
}
return changeZoneLKIInfo.containsKey(c.getId()) ? changeZoneLKIInfo.get(c.getId()) : c;
return changeZoneLKIInfo.getOrDefault(c.getId(), c);
}
public final void clearChangeZoneLKIInfo() {
changeZoneLKIInfo.clear();
@@ -846,6 +860,18 @@ public class Game {
}
}
if (p.hasInitiative()) {
// The third way to take the initiative is if the player who currently has the initiative leaves the game.
// When that happens, the player whose turn it is takes the initiative.
// If the player who has the initiative leaves the game on their own turn,
// or the active player left the game at the same time, the next player in turn order takes the initiative.
if (p.equals(getPhaseHandler().getPlayerTurn())) {
getAction().takeInitiative(getNextPlayerAfter(p), null);
} else {
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), null);
}
}
// Remove leftover items from
getStack().removeInstancesControlledBy(p);
@@ -1068,6 +1094,7 @@ public class Game {
public void onCleanupPhase() {
clearCounterAddedThisTurn();
clearGlobalDamageHistory();
// some cards need this info updated even after a player lost, so don't skip them
for (Player player : getRegisteredPlayers()) {
player.onCleanupPhase();
@@ -1109,6 +1136,48 @@ public class Game {
countersAddedThisTurn.clear();
}
/**
* Gets the damage instances done this turn.
* @param isCombat if true only combat damage matters, pass null for both
* @param anyIsEnough if true returns early once result has an entry
* @param validSourceCard
* @param validTargetEntity
* @param source
* @param sourceController
* @param ctb
* @return List<Integer> for each source
*/
public List<Integer> getDamageDoneThisTurn(Boolean isCombat, boolean anyIsEnough, String validSourceCard, String validTargetEntity, Card source, Player sourceController, CardTraitBase ctb) {
final List<Integer> dmgList = Lists.newArrayList();
for (CardDamageHistory cdh : globalDamageHistory) {
int dmg = cdh.getDamageDoneThisTurn(isCombat, anyIsEnough, validSourceCard, validTargetEntity, source, sourceController, ctb);
if (dmg == 0) {
continue;
}
dmgList.add(dmg);
if (anyIsEnough) {
break;
}
}
return dmgList;
}
public void addGlobalDamageHistory(CardDamageHistory cdh, Pair<Integer, Boolean> dmg, Card source, GameEntity target) {
globalDamageHistory.add(cdh);
damageThisTurnLKI.put(dmg, Pair.of(source, target));
}
public void clearGlobalDamageHistory() {
globalDamageHistory.clear();
damageThisTurnLKI.clear();
}
public Pair<Card, GameEntity> getDamageLKI(Pair<Integer, Boolean> dmg) {
return damageThisTurnLKI.get(dmg);
}
public Card getTopLibForPlayer(Player P) {
return topLibsCast.get(P);
}

View File

@@ -27,7 +27,6 @@ import java.util.Set;
import forge.util.*;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.collect.ComparisonChain;
@@ -37,6 +36,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import forge.GameCommand;
import forge.StaticData;
@@ -66,6 +66,7 @@ import forge.game.event.GameEventGameStarted;
import forge.game.event.GameEventScry;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordsChange;
import forge.game.mulligan.MulliganService;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
@@ -161,14 +162,32 @@ public class GameAction {
// need to check before it enters
if (c.isAura() && !c.isAttachedToEntity() && toBattlefield && (zoneFrom == null || !zoneFrom.is(ZoneType.Stack))) {
boolean found = false;
if (Iterables.any(game.getPlayers(), PlayerPredicates.canBeAttached(c, null))) {
found = true;
try {
if (Iterables.any(game.getPlayers(), PlayerPredicates.canBeAttached(c, null))) {
found = true;
}
} catch (Exception e1) {
found = false;
}
else if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateBattlefield), CardPredicates.canBeAttached(c, null))) {
found = true;
if (!found) {
try {
if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateBattlefield), CardPredicates.canBeAttached(c, null))) {
found = true;
}
} catch (Exception e2) {
found = false;
}
}
else if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateGraveyard), CardPredicates.canBeAttached(c, null))) {
found = true;
if (!found) {
try {
if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateGraveyard), CardPredicates.canBeAttached(c, null))) {
found = true;
}
} catch (Exception e3) {
found = false;
}
}
if (!found) {
c.clearControllers();
@@ -233,13 +252,6 @@ public class GameAction {
}
}
// Clean up the temporary Dash/Blitz SVar when the card leaves the battlefield
// Clean up the temporary AtEOT SVar
String endofTurn = c.getSVar("EndOfTurnLeavePlay");
if (fromBattlefield && (endofTurn.equals("Dash") || endofTurn.equals("Blitz") || endofTurn.equals("AtEOT"))) {
c.removeSVar("EndOfTurnLeavePlay");
}
if (fromBattlefield && !toBattlefield) {
c.getController().setRevolt(true);
}
@@ -253,10 +265,6 @@ public class GameAction {
lastKnownInfo = CardUtil.getLKICopy(c);
}
if (!suppress) {
copied.setTimestamp(game.getNextTimestamp());
}
if (!lastKnownInfo.hasKeyword("Counters remain on CARDNAME as it moves to any zone other than a player's hand or library.")) {
copied.clearCounters();
}
@@ -281,6 +289,8 @@ public class GameAction {
copied = CardFactory.copyCard(c, false);
}
copied.setTimestamp(c.getTimestamp());
if (zoneTo.is(ZoneType.Stack)) {
// when moving to stack, copy changed card information
copied.setChangedCardColors(c.getChangedCardColorsTable());
@@ -293,7 +303,6 @@ public class GameAction {
copied.setDrawnThisTurn(c.getDrawnThisTurn());
copied.copyChangedTextFrom(c);
copied.setTimestamp(c.getTimestamp());
// clean up changes that come from its own static abilities
copied.cleanupCopiedChangesFrom(c);
@@ -305,15 +314,20 @@ public class GameAction {
// copy bestow timestamp
copied.setBestowTimestamp(c.getBestowTimestamp());
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
copied.setCastSA(cause);
KeywordInterface kw = cause.getKeyword();
if (kw != null) {
copied.addKeywordForStaticAbility(kw);
}
}
} else {
// when a card leaves the battlefield, ensure it's in its original state
// (we need to do this on the object before copying it, or it won't work correctly e.g.
// on Transformed objects)
copied.setState(CardStateName.Original, false);
copied.setBackSide(false);
// reset timestamp in changezone effects so they have same timestamp if ETB simultaneously
copied.setTimestamp(game.getNextTimestamp());
}
copied.setUnearthed(c.isUnearthed());
@@ -337,7 +351,7 @@ public class GameAction {
CardCollectionView comCards = c.getOwner().getCardsIn(ZoneType.Command);
for (final Card effCard : comCards) {
for (final ReplacementEffect re : effCard.getReplacementEffects()) {
if (re.hasSVar("CommanderMoveReplacement") && effCard.getEffectSource().getName().equals(c.getRealCommander().getName())) {
if (re.hasParam("CommanderMoveReplacement") && c.getMergedCards().contains(effCard.getEffectSource())) {
commanderEffect = effCard;
break;
}
@@ -345,8 +359,10 @@ public class GameAction {
if (commanderEffect != null) break;
}
// Disable the commander replacement effect
for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) {
re.setSuppressed(true);
if (commanderEffect != null) {
for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) {
re.setSuppressed(true);
}
}
}
@@ -365,7 +381,7 @@ public class GameAction {
}
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams);
if (repres != ReplacementResult.NotReplaced) {
if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) {
// reset failed manifested Cards back to original
if (c.isManifested() && !c.isInPlay()) {
c.forceTurnFaceUp();
@@ -395,6 +411,11 @@ public class GameAction {
}
}
if (!zoneTo.is(ZoneType.Stack) && !suppress) {
// reset timestamp in changezone effects so they have same timestamp if ETB simultaneously
copied.setTimestamp(game.getNextTimestamp());
}
copied.getOwner().removeInboundToken(copied);
// Aura entering as Copy from stack
@@ -489,6 +510,37 @@ public class GameAction {
if (!zoneTo.is(ZoneType.Exile) && !zoneTo.is(ZoneType.Stack)) {
c.cleanupExiledWith();
}
// 400.7a Effects from static abilities that give a permanent spell on the stack an ability
// that allows it to be cast for an alternative cost continue to apply to the permanent that spell becomes.
if (zoneFrom.is(ZoneType.Stack) && toBattlefield) {
List<KeywordInterface> newKw = Lists.newArrayList();
for (Table.Cell<Long, Long, KeywordsChange> cell : c.getChangedCardKeywords().cellSet()) {
// comes from a static ability
if (cell.getColumnKey() == 0) {
continue;
}
for (KeywordInterface ki : cell.getValue().getKeywords()) {
boolean keepKeyword = false;
for (SpellAbility sa : ki.getAbilities()) {
if (!sa.isSpell()) {
continue;
}
if (sa.getAlternativeCost() != null) {
keepKeyword = true;
break;
}
}
if (keepKeyword) {
ki.setHostCard(copied);
newKw.add(ki);
}
}
}
if (!newKw.isEmpty()) {
copied.addChangedCardKeywordsInternal(newKw, null, false, copied.getTimestamp(), 0, true);
}
}
}
// if an adventureCard is put from Stack somewhere else, need to reset to Original State
@@ -498,15 +550,6 @@ public class GameAction {
GameEntityCounterTable table = new GameEntityCounterTable();
// need to suspend cards own replacement effects
if (!suppress) {
if (toBattlefield && !copied.getEtbCounters().isEmpty()) {
for (final ReplacementEffect re : copied.getReplacementEffects()) {
re.setSuppressed(true);
}
}
}
if (mergedCards != null) {
// Move components of merged permanent here
// Also handle 723.3e and 903.9a
@@ -553,14 +596,9 @@ public class GameAction {
}
// do ETB counters after zone add
if (!suppress) {
if (toBattlefield) {
copied.putEtbCounters(table);
// enable replacement effects again
for (final ReplacementEffect re : copied.getReplacementEffects()) {
re.setSuppressed(false);
}
}
if (!suppress && toBattlefield && !copied.getEtbCounters().isEmpty()) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
copied.putEtbCounters(table);
copied.clearEtbCounters();
}
@@ -581,7 +619,7 @@ public class GameAction {
}
}
table.replaceCounterEffect(game, null, true);
table.replaceCounterEffect(game, null, true, true, params);
// Need to apply any static effects to produce correct triggers
checkStaticAbilities();
@@ -1059,6 +1097,24 @@ public class GameAction {
return holdCheckingStaticAbilities;
}
// This doesn't check layers or if the ability gets removed by other effects
public boolean hasStaticAbilityAffectingZone(ZoneType zone, StaticAbilityLayer layer) {
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.getParam("Mode").equals("Continuous") || stAb.isSuppressed() || !stAb.checkConditions()) {
continue;
}
if (layer != null && !stAb.getLayers().contains(layer)) {
continue;
}
if (ZoneType.listValueOf(stAb.getParamOrDefault("AffectedZone", ZoneType.Battlefield.toString())).contains(zone)) {
return true;
}
}
}
return false;
}
public final void checkStaticAbilities() {
checkStaticAbilities(true);
}
@@ -1183,26 +1239,26 @@ public class GameAction {
}
}
for (Player p : game.getPlayers()) {
for (Card c : p.getCardsIn(ZoneType.Battlefield).threadSafeIterable()) {
if (!c.getController().equals(p)) {
controllerChangeZoneCorrection(c);
affectedCards.add(c);
}
if (c.isCreature() && c.isPaired()) {
Card partner = c.getPairedWith();
if (!partner.isCreature() || c.getController() != partner.getController() || !c.isInPlay()) {
c.setPairedWith(null);
partner.setPairedWith(null);
affectedCards.add(c);
}
}
}
}
// preList means that this is run by a pre Check with LKI objects
// in that case Always trigger should not Run
if (preList.isEmpty()) {
for (Player p : game.getPlayers()) {
for (Card c : p.getCardsIn(ZoneType.Battlefield).threadSafeIterable()) {
if (!c.getController().equals(p)) {
controllerChangeZoneCorrection(c);
affectedCards.add(c);
}
if (c.isCreature() && c.isPaired()) {
Card partner = c.getPairedWith();
if (!partner.isCreature() || c.getController() != partner.getController() || !c.isInPlay()) {
c.setPairedWith(null);
partner.setPairedWith(null);
affectedCards.add(c);
}
}
}
}
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
game.getTriggerHandler().runTrigger(TriggerType.Always, runParams, false);
@@ -1217,6 +1273,8 @@ public class GameAction {
c.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering
}
// TODO filter out old copies from zone change
if (runEvents && !affectedCards.isEmpty()) {
game.fireEvent(new GameEventCardStatsChanged(affectedCards));
}
@@ -1293,33 +1351,13 @@ public class GameAction {
noRegCreats.add(c);
checkAgain = true;
} else if (c.hasKeyword("CARDNAME can't be destroyed by lethal damage unless lethal damage dealt by a single source is marked on it.")) {
// merge entries with same source
List<Integer> dmgList = Lists.newArrayList();
List<Pair<Card, Integer>> remainingDamaged = Lists.newArrayList(c.getReceivedDamageFromThisTurn());
while (!remainingDamaged.isEmpty()) {
Pair <Card, Integer> damaged = remainingDamaged.get(0);
int sum = damaged.getRight();
remainingDamaged.remove(damaged);
for (Pair<Card, Integer> other : Lists.newArrayList(remainingDamaged)) {
if (other.getLeft().equalsWithTimestamp(damaged.getLeft())) {
sum += other.getRight();
// once it got counted keep it out
remainingDamaged.remove(other);
}
}
dmgList.add(sum);
}
for (final Integer dmg : dmgList) {
if (c.getLethal() <= dmg.intValue() || c.hasBeenDealtDeathtouchDamage()) {
if (desCreats == null) {
desCreats = new CardCollection();
}
desCreats.add(c);
c.setHasBeenDealtDeathtouchDamage(false);
checkAgain = true;
break;
if (c.getLethal() <= c.getMaxDamageFromSource() || c.hasBeenDealtDeathtouchDamage()) {
if (desCreats == null) {
desCreats = new CardCollection();
}
desCreats.add(c);
c.setHasBeenDealtDeathtouchDamage(false);
checkAgain = true;
}
}
// Rule 704.5g - Destroy due to lethal damage
@@ -1546,7 +1584,7 @@ public class GameAction {
c.getGame().getTracker().flush();
c.setMoveToCommandZone(false);
if (c.getOwner().getController().confirmAction(c.getFirstSpellAbility(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.")) {
if (c.getOwner().getController().confirmAction(c.getFirstSpellAbility(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.", null)) {
moveTo(c.getOwner().getZone(ZoneType.Command), c, null);
return true;
}
@@ -2203,6 +2241,32 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.BecomeMonarch, runParams, false);
}
public void takeInitiative(final Player p, final String set) {
final Player previous = game.getHasInitiative();
if (p == null) {
return;
}
if (!p.equals(previous)) {
if (previous != null) {
previous.removeInitiativeEffect();
}
if (p.hasLost()) { // the person who should take initiative is gone, it goes to next player
takeInitiative(game.getNextPlayerAfter(p), set);
}
game.setHasInitiative(p);
p.createInitiativeEffect(set);
}
// You can take the initiative even if you already have it
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, p);
game.getTriggerHandler().runTrigger(TriggerType.TakesInitiative, 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
@@ -2271,13 +2335,16 @@ public class GameAction {
final Player p = e.getKey();
final CardCollection toTop = e.getValue().getLeft();
final CardCollection toBottom = e.getValue().getRight();
int numLookedAt = 0;
if (toTop != null) {
numLookedAt += toTop.size();
Collections.reverse(toTop); // reverse to get the correct order
for (Card c : toTop) {
moveToLibrary(c, cause, null);
}
}
if (toBottom != null) {
numLookedAt += toBottom.size();
for (Card c : toBottom) {
moveToBottomOfLibrary(c, cause, null);
}
@@ -2287,6 +2354,7 @@ public class GameAction {
// set up triggers (but not actually do them until later)
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, p);
runParams.put(AbilityKey.ScryNum, numLookedAt);
game.getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
}
}
@@ -2308,6 +2376,7 @@ public class GameAction {
game.getReplacementHandler().runReplaceDamage(isCombat, damageMap, preventMap, counterTable, cause);
Map<Card, Integer> lethalDamage = Maps.newHashMap();
Map<Integer, Card> lkiCache = Maps.newHashMap();
// Actually deal damage according to replaced damage map
for (Map.Entry<Card, Map<GameEntity, Integer>> et : damageMap.rowMap().entrySet()) {
@@ -2334,6 +2403,8 @@ public class GameAction {
e.setValue(Integer.valueOf(e.getKey().addDamageAfterPrevention(e.getValue(), sourceLKI, isCombat, counterTable)));
sum += e.getValue();
sourceLKI.getDamageHistory().registerDamage(e.getValue(), isCombat, sourceLKI, e.getKey(), lkiCache);
}
if (sum > 0 && sourceLKI.hasKeyword(Keyword.LIFELINK)) {

View File

@@ -42,17 +42,12 @@ import forge.game.player.PlayerController;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.AlternativeCost;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityRestriction;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Lang;
import forge.util.TextUtil;
@@ -91,7 +86,11 @@ public final class GameActionUtil {
Card source = sa.getHostCard();
final Game game = source.getGame();
if (sa.isSpell() && !source.isInPlay()) {
if (sa.isSpell() && source.isInPlay()) {
return alternatives;
}
if (sa.isSpell()) {
boolean lkicheck = false;
Card newHost = ((Spell)sa).getAlternateHost(source);
@@ -179,12 +178,13 @@ public final class GameActionUtil {
SpellAbility newSA;
if (source.getAlternateState().getType().hasSubtype("Aura")) {
newSA = source.getAlternateState().getFirstAbility().copyWithManaCostReplaced(activator,
disturbCost);
newSA = source.getAlternateState().getFirstAbility().copyWithManaCostReplaced(activator, disturbCost);
} else {
newSA = sa.copyWithManaCostReplaced(activator, disturbCost);
newSA = new SpellPermanent(source);
newSA.setCardState(source.getAlternateState());
newSA.setPayCosts(disturbCost);
newSA.setActivatingPlayer(activator);
}
newSA.setActivatingPlayer(activator);
newSA.putParam("PrecostDesc", "Disturb —");
newSA.putParam("CostDesc", disturbCost.toString());
@@ -210,7 +210,6 @@ public final class GameActionUtil {
final Cost escapeCost = new Cost(k[1], true);
final SpellAbility newSA = sa.copyWithManaCostReplaced(activator, escapeCost);
newSA.setActivatingPlayer(activator);
newSA.putParam("PrecostDesc", "Escape—");
newSA.putParam("CostDesc", escapeCost.toString());
@@ -290,6 +289,35 @@ public final class GameActionUtil {
foretold.putParam("AfterDescription", "(Foretold)");
alternatives.add(foretold);
}
// some needs to check after ability was put on the stack
// Currently this is only checked for Toolbox and that only cares about creature spells
if (source.isCreature() && game.getAction().hasStaticAbilityAffectingZone(ZoneType.Stack, StaticAbilityLayer.ABILITIES)) {
Zone oldZone = source.getLastKnownZone();
Card blitzCopy = source;
if (!source.isLKI()) {
blitzCopy = CardUtil.getLKICopy(source);
}
blitzCopy.setLastKnownZone(game.getStackZone());
lkicheck = true;
blitzCopy.clearStaticChangedCardKeywords(false);
CardCollection preList = new CardCollection(blitzCopy);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(blitzCopy), preList);
// currently only for Keyword BLitz, but should affect Dash probably too
for (final KeywordInterface inst : blitzCopy.getKeywords(Keyword.BLITZ)) {
// TODO with mana value 4 or greater has blitz.
for (SpellAbility iSa : inst.getAbilities()) {
// do only non intrinsic
if (!iSa.isIntrinsic()) {
alternatives.add(iSa);
}
}
}
// need to reset to Old Zone, or canPlay would fail
blitzCopy.setLastKnownZone(oldZone);
}
}
// reset static abilities
@@ -300,74 +328,73 @@ public final class GameActionUtil {
// need to unfreeze tracker
game.getTracker().unfreeze();
}
}
} else {
if (sa.isManaAbility() && sa.isActivatedAbility() && activator.hasKeyword("Piracy") && source.isLand() && source.isInPlay() && !activator.equals(source.getController()) && sa.getPayCosts().hasTapCost()) {
SpellAbility newSA = sa.copy(activator);
// to bypass Activator restriction, set Activator to Player
newSA.getRestrictions().setActivator("Player");
if (sa.isManaAbility() && sa.isActivatedAbility() && activator.hasKeyword("Piracy") && source.isLand() && source.isInPlay() && !activator.equals(source.getController()) && sa.getPayCosts().hasTapCost()) {
SpellAbility newSA = sa.copy(activator);
// to bypass Activator restriction, set Activator to Player
newSA.getRestrictions().setActivator("Player");
// extra Mana restriction to only Spells
for (AbilityManaPart mp : newSA.getAllManaParts()) {
mp.setExtraManaRestriction("Spell");
}
alternatives.add(newSA);
}
// below are for some special cases of activated abilities
if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) {
for (final KeywordInterface inst : source.getKeywords()) {
// need to find the correct Keyword from which this Ability is from
if (!inst.getAbilities().contains(sa)) {
continue;
// extra Mana restriction to only Spells
for (AbilityManaPart mp : newSA.getAllManaParts()) {
mp.setExtraManaRestriction("Spell");
}
// set the cost to this directly to bypass non mana cost
final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>");
newSA.setActivatingPlayer(activator);
newSA.putParam("CostDesc", ManaCostParser.parse("0"));
// need to build a new Keyword to get better Reminder Text
String data[] = inst.getOriginal().split(":");
data[1] = "0";
KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":"));
// makes new SpellDescription
final StringBuilder sb = new StringBuilder();
sb.append(newSA.getCostDescription());
sb.append("(").append(newKi.getReminderText()).append(")");
newSA.setDescription(sb.toString());
alternatives.add(newSA);
}
}
if (sa.isEquip() && activator.hasKeyword("You may pay 0 rather than pay equip costs.")) {
for (final KeywordInterface inst : source.getKeywords()) {
// need to find the correct Keyword from which this Ability is from
if (!inst.getAbilities().contains(sa)) {
continue;
// below are for some special cases of activated abilities
if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) {
for (final KeywordInterface inst : source.getKeywords()) {
// need to find the correct Keyword from which this Ability is from
if (!inst.getAbilities().contains(sa)) {
continue;
}
// set the cost to this directly to bypass non mana cost
final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>");
newSA.setActivatingPlayer(activator);
newSA.putParam("CostDesc", ManaCostParser.parse("0"));
// need to build a new Keyword to get better Reminder Text
String data[] = inst.getOriginal().split(":");
data[1] = "0";
KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":"));
// makes new SpellDescription
final StringBuilder sb = new StringBuilder();
sb.append(newSA.getCostDescription());
sb.append("(").append(newKi.getReminderText()).append(")");
newSA.setDescription(sb.toString());
alternatives.add(newSA);
}
}
if (sa.isEquip() && activator.hasKeyword("You may pay 0 rather than pay equip costs.")) {
for (final KeywordInterface inst : source.getKeywords()) {
// need to find the correct Keyword from which this Ability is from
if (!inst.getAbilities().contains(sa)) {
continue;
}
// set the cost to this directly to bypass non mana cost
SpellAbility newSA = sa.copyWithDefinedCost("0");
newSA.setActivatingPlayer(activator);
newSA.putParam("CostDesc", ManaCostParser.parse("0"));
// set the cost to this directly to bypass non mana cost
SpellAbility newSA = sa.copyWithDefinedCost("0");
newSA.setActivatingPlayer(activator);
newSA.putParam("CostDesc", ManaCostParser.parse("0"));
// need to build a new Keyword to get better Reminder Text
String data[] = inst.getOriginal().split(":");
data[1] = "0";
KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":"));
// need to build a new Keyword to get better Reminder Text
String data[] = inst.getOriginal().split(":");
data[1] = "0";
KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":"));
// makes new SpellDescription
final StringBuilder sb = new StringBuilder();
sb.append(newSA.getCostDescription());
sb.append("(").append(newKi.getReminderText()).append(")");
newSA.setDescription(sb.toString());
// makes new SpellDescription
final StringBuilder sb = new StringBuilder();
sb.append(newSA.getCostDescription());
sb.append("(").append(newKi.getReminderText()).append(")");
newSA.setDescription(sb.toString());
alternatives.add(newSA);
alternatives.add(newSA);
}
}
}
return alternatives;
}
@@ -677,13 +704,12 @@ public final class GameActionUtil {
if (!StringUtils.isNumeric(amount)) {
sa.setSVar(amount, sourceCard.getSVar(amount));
}
CardFactoryUtil.setupETBReplacementAbility(sa);
String desc = "It enters the battlefield with ";
desc += Lang.nounWithNumeral(amount, CounterType.getType(counter).getName() + " counter");
desc += " on it.";
String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | Description$ " + desc;
String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | ReplacementResult$ Updated | Description$ " + desc;
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
re.setLayer(ReplacementLayer.Other);

View File

@@ -17,9 +17,13 @@
*/
package forge.game;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.game.ability.AbilityUtils;
@@ -46,6 +50,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
private String name = "";
protected CardCollection attachedCards = new CardCollection();
protected Map<CounterType, Integer> counters = Maps.newHashMap();
protected List<Pair<Integer, Boolean>> damageReceivedThisTurn = Lists.newArrayList();
protected GameEntity(int id0) {
id = id0;
@@ -330,6 +335,30 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table);
}
public void receiveDamage(Pair<Integer, Boolean> dmg) {
damageReceivedThisTurn.add(dmg);
}
public final int getAssignedDamage() {
return getAssignedDamage(null, null);
}
public final int getAssignedCombatDamage() {
return getAssignedDamage(true, null);
}
public final int getAssignedDamage(Boolean isCombat, final Card source) {
int num = 0;
for (Pair<Integer, Boolean> dmg : damageReceivedThisTurn) {
if (isCombat != null && dmg.getRight() != isCombat) {
continue;
}
if (source != null && !getGame().getDamageLKI(dmg).getLeft().equalsWithTimestamp(source)) {
continue;
}
num += dmg.getLeft();
}
return num;
}
@Override
public final boolean equals(Object o) {
if (o == null) { return false; }

View File

@@ -94,7 +94,9 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
for (Map<CounterType, Integer> cm : gm.getValue().values()) {
Integer old = ObjectUtils.firstNonNull(result.get(gm.getKey()), 0);
Integer v = ObjectUtils.firstNonNull(cm.get(type), 0);
result.put(gm.getKey(), old + v);
if (old + v > 0) {
result.put(gm.getKey(), old + v);
}
}
}
}
@@ -120,8 +122,12 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
game.getTriggerHandler().runTrigger(TriggerType.CounterAddedAll, runParams, false);
}
@SuppressWarnings("unchecked")
public void replaceCounterEffect(final Game game, final SpellAbility cause, final boolean effect) {
replaceCounterEffect(game, cause, effect, false, null);
}
@SuppressWarnings("unchecked")
public void replaceCounterEffect(final Game game, final SpellAbility cause, final boolean effect, final boolean etb, Map<AbilityKey, Object> params) {
if (isEmpty()) {
return;
}
@@ -133,6 +139,10 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
repParams.put(AbilityKey.Cause, cause);
repParams.put(AbilityKey.EffectOnly, effect);
repParams.put(AbilityKey.CounterMap, values);
repParams.put(AbilityKey.ETB, etb);
if (params != null) {
repParams.putAll(params);
}
switch (game.getReplacementHandler().run(ReplacementType.AddCounter, repParams)) {
case NotReplaced:

View File

@@ -48,7 +48,7 @@ public class GameFormat implements Comparable<GameFormat> {
public enum FormatType {
SANCTIONED,
CASUAL,
HISTORIC,
ARCHIVED,
DIGITAL,
CUSTOM
}
@@ -290,7 +290,7 @@ public class GameFormat implements Comparable<GameFormat> {
if (other.formatSubType != formatSubType){
return formatSubType.compareTo(other.formatSubType);
}
if (formatType.equals(FormatType.HISTORIC)){
if (formatType.equals(FormatType.ARCHIVED)){
int compareDates = this.effectiveDate.compareTo(other.effectiveDate);
if (compareDates != 0)
return compareDates;
@@ -306,7 +306,7 @@ public class GameFormat implements Comparable<GameFormat> {
public static class Reader extends StorageReaderRecursiveFolderWithUserFolder<GameFormat> {
List<GameFormat> naturallyOrdered = new ArrayList<>();
boolean includeHistoric;
boolean includeArchived;
private List<String> coreFormats = new ArrayList<>();
{
coreFormats.add("Standard.txt");
@@ -321,14 +321,14 @@ public class GameFormat implements Comparable<GameFormat> {
coreFormats.add("Oathbreaker.txt");
}
public Reader(File forgeFormats, File customFormats, boolean includeHistoric) {
public Reader(File forgeFormats, File customFormats, boolean includeArchived) {
super(forgeFormats, customFormats, GameFormat.FN_GET_NAME);
this.includeHistoric=includeHistoric;
this.includeArchived=includeArchived;
}
@Override
protected GameFormat read(File file) {
if (!includeHistoric && !coreFormats.contains(file.getName())) {
if (!includeArchived && !coreFormats.contains(file.getName())) {
return null;
}
final Map<String, List<String>> contents = FileSection.parseSections(FileUtil.readFile(file));
@@ -348,7 +348,12 @@ public class GameFormat implements Comparable<GameFormat> {
try {
formatType = FormatType.valueOf(section.get("type").toUpperCase());
} catch (Exception e) {
formatType = FormatType.CUSTOM;
if ("HISTORIC".equals(section.get("type").toUpperCase())) {
System.out.println("Historic is no longer used as a format Type. Please update " + file.getAbsolutePath() + " to use 'Archived' instead");
formatType = FormatType.ARCHIVED;
} else {
formatType = FormatType.CUSTOM;
}
}
FormatSubType formatsubType;
try {
@@ -450,7 +455,7 @@ public class GameFormat implements Comparable<GameFormat> {
public Iterable<GameFormat> getFilterList() {
List<GameFormat> coreList = new ArrayList<>();
for (GameFormat format: naturallyOrdered) {
if (!format.getFormatType().equals(FormatType.HISTORIC)
if (!format.getFormatType().equals(FormatType.ARCHIVED)
&&!format.getFormatType().equals(FormatType.DIGITAL)){
coreList.add(format);
}
@@ -458,10 +463,10 @@ public class GameFormat implements Comparable<GameFormat> {
return coreList;
}
public Iterable<GameFormat> getHistoricList() {
public Iterable<GameFormat> getArchivedList() {
List<GameFormat> coreList = new ArrayList<>();
for (GameFormat format: naturallyOrdered) {
if (format.getFormatType().equals(FormatType.HISTORIC)){
if (format.getFormatType().equals(FormatType.ARCHIVED)){
coreList.add(format);
}
}
@@ -470,7 +475,7 @@ public class GameFormat implements Comparable<GameFormat> {
public Iterable<GameFormat> getBlockList() {
List<GameFormat> blockFormats = new ArrayList<>();
for (GameFormat format : this.getHistoricList()){
for (GameFormat format : this.getArchivedList()){
if (format.getFormatSubType() != GameFormat.FormatSubType.BLOCK)
continue;
if (!format.getName().endsWith("Block"))
@@ -481,10 +486,10 @@ public class GameFormat implements Comparable<GameFormat> {
return blockFormats;
}
public Map<String, List<GameFormat>> getHistoricMap() {
public Map<String, List<GameFormat>> getArchivedMap() {
Map<String, List<GameFormat>> coreList = new HashMap<>();
for (GameFormat format: naturallyOrdered){
if (format.getFormatType().equals(FormatType.HISTORIC)){
if (format.getFormatType().equals(FormatType.ARCHIVED)){
String alpha = format.getName().substring(0,1);
if (!coreList.containsKey(alpha)) {
coreList.put(alpha,new ArrayList<>());
@@ -557,9 +562,9 @@ public class GameFormat implements Comparable<GameFormat> {
//exclude Commander format as other deck checks are not performed here
continue;
}
if (gf.getFormatType().equals(FormatType.HISTORIC) && coveredTypes.contains(gf.getFormatSubType())
if (gf.getFormatType().equals(FormatType.ARCHIVED) && coveredTypes.contains(gf.getFormatSubType())
&& !exhaustive){
//exclude duplicate formats - only keep first of e.g. Standard historical
//exclude duplicate formats - only keep first of e.g. Standard archived
continue;
}
if (gf.isPoolLegal(allCards)) {
@@ -590,7 +595,7 @@ public class GameFormat implements Comparable<GameFormat> {
if (gf2.formatSubType != gf1.formatSubType){
return gf1.formatSubType.compareTo(gf2.formatSubType);
}
if (gf1.formatType.equals(FormatType.HISTORIC)){
if (gf1.formatType.equals(FormatType.ARCHIVED)){
if (gf1.effectiveDate!=gf2.effectiveDate) {//for matching dates or default dates default to name sorting
return gf1.effectiveDate.compareTo(gf2.effectiveDate);
}

View File

@@ -81,7 +81,8 @@ public class GameRules {
}
public void setAppliedVariants(final Set<GameType> appliedVariants) {
this.appliedVariants.addAll(appliedVariants);
if (appliedVariants != null && !appliedVariants.isEmpty())
this.appliedVariants.addAll(appliedVariants);
}
public boolean hasAppliedVariant(final GameType variant) {

View File

@@ -76,7 +76,7 @@ public enum GameType {
private final DeckFormat deckFormat;
private final boolean isCardPoolLimited, canSideboard, addWonCardsMidGame;
private final String name, description;
private final String name, englishName, description;
private final Function<RegisteredPlayer, Deck> deckAutoGenerator;
GameType(DeckFormat deckFormat0, boolean isCardPoolLimited0, boolean canSideboard0, boolean addWonCardsMidgame0, String name0, String description0) {
@@ -90,6 +90,7 @@ public enum GameType {
canSideboard = canSideboard0;
addWonCardsMidGame = addWonCardsMidgame0;
name = localizer.getMessage(name0);
englishName = localizer.getEnglishMessage(name0);
if (description0.length()>0) {
description0 = localizer.getMessage(description0);
}
@@ -148,6 +149,9 @@ public enum GameType {
public String toString() {
return name;
}
public String getEnglishName() {
return englishName;
}
public String getDescription() {
return description;

View File

@@ -33,7 +33,6 @@ public enum GlobalRuleChange {
onlyOneBlocker ("No more than one creature can block each combat."),
onlyOneBlockerPerOpponent ("Each opponent can't block with more than one creature."),
onlyTwoBlockers ("No more than two creatures can block each combat."),
toughnessAssignsDamage ("Each creature assigns combat damage equal to its toughness rather than its power."),
blankIsChaos("Each blank roll of the planar dice is a {CHAOS} roll.");
private final String ruleText;

View File

@@ -288,6 +288,8 @@ public class StaticEffect {
affectedCard.removeCanBlockAdditional(getTimestamp());
}
affectedCard.removeChangedSVars(getTimestamp(), ability.getId());
affectedCard.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering
}
return affectedCards;

View File

@@ -327,8 +327,8 @@ public final class AbilityFactory {
}
private static final TargetRestrictions readTarget(Map<String, String> mapParams) {
final String min = mapParams.containsKey("TargetMin") ? mapParams.get("TargetMin") : "1";
final String max = mapParams.containsKey("TargetMax") ? mapParams.get("TargetMax") : "1";
final String min = mapParams.getOrDefault("TargetMin", "1");
final String max = mapParams.getOrDefault("TargetMax", "1");
// TgtPrompt should only be needed for more complicated ValidTgts
String tgtWhat = mapParams.get("ValidTgts");

View File

@@ -67,6 +67,7 @@ public enum AbilityKey {
Explorer("Explorer"),
ExtraTurn("ExtraTurn"),
Event("Event"),
ETB("ETB"),
Fighter("Fighter"),
Fighters("Fighters"),
FirstTime("FirstTime"),
@@ -112,12 +113,14 @@ public enum AbilityKey {
Result("Result"),
RoomName("RoomName"),
Scheme("Scheme"),
ScryNum("ScryNum"),
Sides("Sides"),
Source("Source"),
Sources("Sources"),
SourceSA("SourceSA"),
SpellAbility("SpellAbility"),
SpellAbilityStackInstance("SpellAbilityStackInstance"),
SpellAbilityTarget("SpellAbilityTarget"),
SpellAbilityTargetingCards("SpellAbilityTargetingCards"),
StackInstance("StackInstance"),
StackSa("StackSa"),

View File

@@ -14,6 +14,7 @@ import java.util.regex.Pattern;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
@@ -146,7 +147,7 @@ public class AbilityUtils {
} else if (defined.equals("TopOfGraveyard")) {
final CardCollectionView grave = hostCard.getController().getCardsIn(ZoneType.Graveyard);
if (grave.size() > 0) { // TopOfLibrary or BottomOfLibrary
if (grave.size() > 0) {
c = grave.getLast();
} else {
// we don't want this to fall through and return the "Self"
@@ -220,7 +221,7 @@ public class AbilityUtils {
final Object crd = root.getReplacingObject(type);
if (crd instanceof Card) {
c = game.getCardState((Card) crd);
c = (Card) crd;
} else if (crd instanceof Iterable<?>) {
cards.addAll(Iterables.filter((Iterable<?>) crd, Card.class));
}
@@ -501,7 +502,7 @@ public class AbilityUtils {
players.addAll(player.getOpponents());
val = playerXCount(players, calcX[1], card, ability);
} else if (hType.equals("RegisteredOpponents")) {
players.addAll(Iterables.filter(game.getRegisteredPlayers(),PlayerPredicates.isOpponentOf(player)));
players.addAll(Iterables.filter(game.getRegisteredPlayers(), PlayerPredicates.isOpponentOf(player)));
val = playerXCount(players, calcX[1], card, ability);
} else if (hType.equals("Other")) {
players.addAll(player.getAllOtherPlayers());
@@ -535,6 +536,9 @@ public class AbilityUtils {
}
}
val = playerXCount(players, calcX[1], card, ability);
} else if (hType.startsWith("Defined")) {
String defined = hType.split("Defined")[1];
val = playerXCount(getDefinedPlayers(card, defined, ability), calcX[1], card, ability);
} else {
val = 0;
}
@@ -1060,17 +1064,16 @@ public class AbilityUtils {
final SpellAbility root = ((SpellAbility)sa).getRootAbility();
Object o = null;
if (defParsed.endsWith("Controller")) {
String triggeringType = defParsed.substring(9);
triggeringType = triggeringType.substring(0, triggeringType.length() - 10);
final boolean orCont = defParsed.endsWith("OrController");
String triggeringType = defParsed.substring(9, defParsed.length() - (orCont ? 12 : 10));
final Object c = root.getTriggeringObject(AbilityKey.fromString(triggeringType));
if (c instanceof Card) {
if (orCont && c instanceof Player) {
o = c;
} else if (c instanceof Card) {
o = ((Card) c).getController();
}
if (c instanceof SpellAbility) {
} else if (c instanceof SpellAbility) {
o = ((SpellAbility) c).getActivatingPlayer();
}
// For merged permanent
if (c instanceof CardCollection) {
} else if (c instanceof CardCollection) { // For merged permanent
o = ((CardCollection) c).get(0).getController();
}
}
@@ -1413,11 +1416,7 @@ public class AbilityUtils {
// Needed - Equip an untapped creature with Sword of the Paruns then cast Deadshot on it. Should deal 2 more damage.
game.getAction().checkStaticAbilities(); // this will refresh continuous abilities for players and permanents.
if (sa.isReplacementAbility() && abSub.getApi() == ApiType.InternalEtbReplacement) {
game.getTriggerHandler().resetActiveTriggers(false);
} else {
game.getTriggerHandler().resetActiveTriggers();
}
game.getTriggerHandler().resetActiveTriggers(!sa.isReplacementAbility());
AbilityUtils.resolveApiAbility(abSub, game);
}
@@ -1448,7 +1447,7 @@ public class AbilityUtils {
// The player who has the chance to cancel the ability
final String pays = sa.getParamOrDefault("UnlessPayer", "TargetedController");
final FCollectionView<Player> allPayers = getDefinedPlayers(sa.getHostCard(), pays, sa);
final FCollectionView<Player> allPayers = getDefinedPlayers(source, pays, sa);
final String resolveSubs = sa.getParam("UnlessResolveSubs"); // no value means 'Always'
final boolean execSubsWhenPaid = "WhenPaid".equals(resolveSubs) || StringUtils.isBlank(resolveSubs);
final boolean execSubsWhenNotPaid = "WhenNotPaid".equals(resolveSubs) || StringUtils.isBlank(resolveSubs);
@@ -1485,7 +1484,7 @@ public class AbilityUtils {
cost = new Cost(new ManaCost(new ManaCostParser(String.valueOf(source.getChosenNumber()))), true);
}
else if (unlessCost.startsWith("DefinedCost")) {
CardCollection definedCards = getDefinedCards(sa.getHostCard(), unlessCost.split("_")[1], sa);
CardCollection definedCards = getDefinedCards(source, unlessCost.split("_")[1], sa);
if (definedCards.isEmpty()) {
sa.resolve();
resolveSubAbilities(sa, game);
@@ -1505,7 +1504,7 @@ public class AbilityUtils {
cost = new Cost(newCost.toManaCost(), true);
}
else if (unlessCost.startsWith("DefinedSACost")) {
FCollection<SpellAbility> definedSAs = getDefinedSpellAbilities(sa.getHostCard(), unlessCost.split("_")[1], sa);
FCollection<SpellAbility> definedSAs = getDefinedSpellAbilities(source, unlessCost.split("_")[1], sa);
if (definedSAs.isEmpty()) {
sa.resolve();
resolveSubAbilities(sa, game);
@@ -2032,7 +2031,7 @@ public class AbilityUtils {
return doXMath(c.getTotalDamageDoneBy(), expr, c, ctb);
}
if (sq[0].equals("TotalDamageReceivedThisTurn")) {
return doXMath(c.getTotalDamageReceivedThisTurn(), expr, c, ctb);
return doXMath(c.getAssignedDamage(), expr, c, ctb);
}
if (sq[0].contains("CardPower")) {
@@ -2093,13 +2092,6 @@ public class AbilityUtils {
return doXMath(c.getTimesMutated(), expr, c, ctb);
}
if (sq[0].startsWith("DamageDoneByPlayerThisTurn")) {
int sum = 0;
for (Player p : getDefinedPlayers(c, sq[1], ctb)) {
sum += c.getReceivedDamageByPlayerThisTurn(p);
}
return doXMath(sum, expr, c, ctb);
}
if (sq[0].equals("RegeneratedThisTurn")) {
return doXMath(c.getRegeneratedThisTurn(), expr, c, ctb);
}
@@ -2260,6 +2252,9 @@ public class AbilityUtils {
if (sq[0].equals("Monarch")) {
return doXMath(calculateAmount(c, sq[player.isMonarch() ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("Initiative")) {
return doXMath(calculateAmount(c, sq[player.hasInitiative() ? 1: 2], ctb), expr, c, ctb);
}
if (sq[0].equals("StartingPlayer")) {
return doXMath(calculateAmount(c, sq[player.isStartingPlayer() ? 1: 2], ctb), expr, c, ctb);
}
@@ -2358,20 +2353,28 @@ public class AbilityUtils {
return doXMath(player.getOpponentsTotalPoisonCounters(), expr, c, ctb);
}
if (sq[0].equals("YourDamageThisTurn")) {
return doXMath(player.getAssignedDamage(), expr, c, ctb);
}
if (sq[0].equals("TotalOppDamageThisTurn")) {
return doXMath(player.getOpponentsAssignedDamage(), expr, c, ctb);
}
if (sq[0].equals("MaxOppDamageThisTurn")) {
return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb);
}
if (sq[0].startsWith("YourDamageSourcesThisTurn")) {
Iterable<Card> allSrc = player.getAssignedDamageSources();
String restriction = sq[0].split(" ")[1];
return doXMath(CardLists.getValidCardCount(allSrc, restriction, player, c, ctb), expr, c, ctb);
if (sq[0].contains("TotalDamageThisTurn")) {
String[] props = l[0].split(" ");
int sum = 0;
for (Pair<Integer, Boolean> p : c.getDamageReceivedThisTurn()) {
if (game.getDamageLKI(p).getLeft().isValid(props[1], player, c, ctb)) {
sum += p.getLeft();
}
}
return doXMath(sum, expr, c, ctb);
}
if (sq[0].contains("DamageThisTurn")) {
String[] props = l[0].split(" ");
Boolean isCombat = null;
if (sq[0].contains("CombatDamage")) {
isCombat = true;
}
return doXMath(game.getDamageDoneThisTurn(isCombat, false, props[1], props[2], c, player, ctb).size(), expr, c, ctb);
}
if (sq[0].equals("YourTurns")) {
@@ -3324,12 +3327,6 @@ public class AbilityUtils {
totDmg += p.getAssignedDamage();
}
return doXMath(totDmg, m, source, ctb);
} else if (sq[0].contains("LifeLostThisTurn")) {
int totDmg = 0;
for (Player p : players) {
totDmg += p.getLifeLostThisTurn();
}
return doXMath(totDmg, m, source, ctb);
}
if (players.size() > 0) {
@@ -3386,7 +3383,7 @@ public class AbilityUtils {
if (value.contains("DomainPlayer")) {
int n = 0;
final CardCollectionView someCards = player.getCardsIn(ZoneType.Battlefield);
final CardCollectionView someCards = player.getLandsInPlay();
final List<String> basic = MagicColor.Constant.BASIC_LANDS;
for (int i = 0; i < basic.size(); i++) {
@@ -3483,6 +3480,10 @@ public class AbilityUtils {
return doXMath(opps == null ? 0 : opps.size(), m, source, ctb);
}
if (value.equals("OpponentsAttackedThisCombat")) {
return doXMath(game.getCombat().getAttackedOpponents(player).size(), m, source, ctb);
}
if (value.equals("DungeonsCompleted")) {
return doXMath(player.getCompletedDungeons().size(), m, source, ctb);
}

View File

@@ -71,6 +71,7 @@ public enum ApiType {
DigUntil (DigUntilEffect.class),
Discard (DiscardEffect.class),
DrainMana (DrainManaEffect.class),
Draft (DraftEffect.class),
Draw (DrawEffect.class),
EachDamage (DamageEachEffect.class),
Effect (EffectEffect.class),
@@ -171,6 +172,7 @@ public enum ApiType {
Subgame (SubgameEffect.class),
Surveil (SurveilEffect.class),
SwitchBlock (SwitchBlockEffect.class),
TakeInitiative (TakeInitiativeEffect.class),
Tap (TapEffect.class),
TapAll (TapAllEffect.class),
TapOrUntap (TapOrUntapEffect.class),
@@ -188,7 +190,6 @@ public enum ApiType {
DamageResolve (DamageResolveEffect.class),
ChangeZoneResolve (ChangeZoneResolveEffect.class),
InternalEtbReplacement (ETBReplacementEffect.class),
InternalLegendaryRule (CharmEffect.class),
InternalIgnoreEffect (CharmEffect.class),
UpdateRemember (UpdateRememberEffect.class);

View File

@@ -300,13 +300,12 @@ public abstract class SpellAbilityEffect {
delTrig.append("| TriggerDescription$ ").append(desc);
final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), CardUtil.getLKICopy(sa.getHostCard()), intrinsic);
long ts = sa.getHostCard().getGame().getNextTimestamp();
for (final Card c : crds) {
trig.addRemembered(c);
// Svar for AI
if (!c.hasSVar("EndOfTurnLeavePlay")) {
c.setSVar("EndOfTurnLeavePlay", "AtEOT");
}
c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "AtEOT"), ts, 0);
}
String trigSA = "";
if (location.equals("Hand")) {
@@ -346,9 +345,7 @@ public abstract class SpellAbilityEffect {
card.addTrigger(trig);
// Svar for AI
if (!card.hasSVar("EndOfTurnLeavePlay")) {
card.setSVar("EndOfTurnLeavePlay", "AtEOT");
}
card.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "AtEOT"), card.getGame().getNextTimestamp(), 0);
}
protected static SpellAbility getForgetSpellAbility(final Card card) {

View File

@@ -25,7 +25,7 @@ public class AbandonEffect extends SpellAbilityEffect {
Player controller = source.getController();
boolean isOptional = sa.hasParam("Optional");
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeAbandonSource", CardTranslation.getTranslatedName(source.getName())))) {
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeAbandonSource", CardTranslation.getTranslatedName(source.getName())), null)) {
return;
}

View File

@@ -1,10 +1,11 @@
package forge.game.ability.effects;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.GameCommand;
import forge.card.CardType;
@@ -64,17 +65,17 @@ public class AnimateAllEffect extends AnimateEffectBase {
types.add(host.getChosenType2());
}
final List<String> keywords = new ArrayList<>();
final List<String> keywords = Lists.newArrayList();
if (sa.hasParam("Keywords")) {
keywords.addAll(Arrays.asList(sa.getParam("Keywords").split(" & ")));
}
final List<String> removeKeywords = new ArrayList<>();
final List<String> removeKeywords = Lists.newArrayList();
if (sa.hasParam("RemoveKeywords")) {
removeKeywords.addAll(Arrays.asList(sa.getParam("RemoveKeywords").split(" & ")));
}
final List<String> hiddenKeywords = new ArrayList<>();
final List<String> hiddenKeywords = Lists.newArrayList();
if (sa.hasParam("HiddenKeywords")) {
hiddenKeywords.addAll(Arrays.asList(sa.getParam("HiddenKeywords").split(" & ")));
}
@@ -99,27 +100,38 @@ public class AnimateAllEffect extends AnimateEffectBase {
}
// abilities to add to the animated being
final List<String> abilities = new ArrayList<>();
final List<String> abilities = Lists.newArrayList();
if (sa.hasParam("Abilities")) {
abilities.addAll(Arrays.asList(sa.getParam("Abilities").split(",")));
}
// replacement effects to add to the animated being
final List<String> replacements = new ArrayList<>();
final List<String> replacements = Lists.newArrayList();
if (sa.hasParam("Replacements")) {
replacements.addAll(Arrays.asList(sa.getParam("Replacements").split(",")));
}
// triggers to add to the animated being
final List<String> triggers = new ArrayList<>();
final List<String> triggers = Lists.newArrayList();
if (sa.hasParam("Triggers")) {
triggers.addAll(Arrays.asList(sa.getParam("Triggers").split(",")));
}
// sVars to add to the animated being
final List<String> sVars = new ArrayList<>();
final List<String> sVars = Lists.newArrayList();
if (sa.hasParam("sVars")) {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
}
// static abilities to add to the animated being
final List<String> stAbs = Lists.newArrayList();
if (sa.hasParam("staticAbilities")) {
stAbs.addAll(Arrays.asList(sa.getParam("staticAbilities").split(",")));
}
Map<String, String> sVarsMap = Maps.newHashMap();
for (final String s : sVars) {
sVarsMap.put(s, AbilityUtils.getSVar(sa, s));
}
final String valid = sa.getParamOrDefault("ValidCards", "");
CardCollectionView list;
@@ -133,15 +145,14 @@ public class AnimateAllEffect extends AnimateEffectBase {
list = CardLists.getValidCards(list, valid, host.getController(), host, sa);
for (final Card c : list) {
doAnimate(c, sa, power, toughness, types, removeTypes, finalColors,
keywords, removeKeywords, hiddenKeywords,
abilities, triggers, replacements, ImmutableList.of(),
timestamp);
doAnimate(c, sa, power, toughness, types, removeTypes, finalColors, keywords, removeKeywords,
hiddenKeywords, abilities, triggers, replacements, stAbs, timestamp);
// give sVars
for (final String s : sVars) {
c.setSVar(s, AbilityUtils.getSVar(sa, s));
if (!sVarsMap.isEmpty() ) {
c.addChangedSVars(sVarsMap, timestamp, 0);
}
game.fireEvent(new GameEventCardStatsChanged(c));
final GameCommand unanimate = new GameCommand() {

View File

@@ -2,7 +2,9 @@ package forge.game.ability.effects;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Maps;
import com.google.common.collect.Lists;
import forge.card.CardType;
@@ -138,11 +140,22 @@ public class AnimateEffect extends AnimateEffectBase {
}
// sVars to add to the animated being
final List<String> sVars = Lists.newArrayList();
Map<String, String> sVarsMap = Maps.newHashMap();
if (sa.hasParam("sVars")) {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
for (final String s : sa.getParam("sVars").split(",")) {
String actualsVar = AbilityUtils.getSVar(sa, s);
String name = s;
if (actualsVar.startsWith("SVar:")) {
actualsVar = actualsVar.split("SVar:")[1];
name = actualsVar.split(":")[0];
actualsVar = actualsVar.split(":")[1];
}
sVarsMap.put(name, actualsVar);
}
}
List<Card> tgts = getCardsfromTargets(sa);
if (sa.hasParam("Optional")) {
@@ -151,7 +164,7 @@ public class AnimateEffect extends AnimateEffectBase {
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
: getStackDescription(sa);
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message)) {
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message, null)) {
return;
}
}
@@ -166,15 +179,8 @@ public class AnimateEffect extends AnimateEffectBase {
}
// give sVars
for (final String s : sVars) {
String actualsVar = AbilityUtils.getSVar(sa, s);
String name = s;
if (actualsVar.startsWith("SVar:")) {
actualsVar = actualsVar.split("SVar:")[1];
name = actualsVar.split(":")[0];
actualsVar = actualsVar.split(":")[1];
}
c.setSVar(name, actualsVar);
if (!sVarsMap.isEmpty()) {
c.addChangedSVars(sVarsMap, timestamp, 0);
}
// give Remembered

View File

@@ -137,6 +137,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
public void run() {
doUnanimate(c, timestamp);
c.removeChangedSVars(timestamp, 0);
c.removeChangedName(timestamp, 0);
c.updateStateForView();

View File

@@ -134,7 +134,7 @@ public class AttachEffect extends SpellAbilityEffect {
// If Cast Targets will be checked on the Stack
for (final Card attachment : attachments) {
String message = Localizer.getInstance().getMessage("lblDoYouWantAttachSourceToTarget", CardTranslation.getTranslatedName(attachment.getName()), attachToName);
if (sa.hasParam("Optional") && !p.getController().confirmAction(sa, null, message))
if (sa.hasParam("Optional") && !p.getController().confirmAction(sa, null, message, null))
// TODO add params for message
continue;

View File

@@ -2,11 +2,10 @@ package forge.game.ability.effects;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Lang;
public class BecomeMonarchEffect extends SpellAbilityEffect {
@@ -16,8 +15,8 @@ public class BecomeMonarchEffect extends SpellAbilityEffect {
final List<Player> tgtPlayers = getTargetPlayers(sa);
sb.append(StringUtils.join(tgtPlayers, ", "));
sb.append(" becomes the Monarch.");
sb.append(Lang.joinHomogenous(tgtPlayers)).append(tgtPlayers.size() == 1 ? " becomes" : " become");
sb.append(" the monarch.");
return sb.toString();
}

View File

@@ -21,7 +21,7 @@ import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.util.CardTranslation;
import forge.util.Localizer;
import forge.util.collect.FCollectionView;
import forge.util.collect.FCollection;
public class ChangeCombatantsEffect extends SpellAbilityEffect {
@@ -47,7 +47,8 @@ public class ChangeCombatantsEffect extends SpellAbilityEffect {
if ((tgt == null) || c.canBeTargetedBy(sa)) {
final Combat combat = game.getCombat();
final GameEntity originalDefender = combat.getDefenderByAttacker(c);
final FCollectionView<GameEntity> defs = combat.getDefenders();
final FCollection<GameEntity> defs = new FCollection<>();
defs.addAll(sa.hasParam("PlayerOnly") ? combat.getDefendingPlayers() : combat.getDefenders());
String title = Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName()));
Map<String, Object> params = Maps.newHashMap();

View File

@@ -50,7 +50,7 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
// Redirect rules read 'you MAY choose new targets' ... okay!
// TODO: Don't even ask to change targets, if the SA and subs don't actually have targets
boolean isOptional = sa.hasParam("Optional");
if (isOptional && !chooser.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantChangeAbilityTargets", tgtSA.getHostCard().toString()))) {
if (isOptional && !chooser.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantChangeAbilityTargets", tgtSA.getHostCard().toString()), null)) {
continue;
}
if (sa.hasParam("ChangeSingleTarget")) {

View File

@@ -120,7 +120,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
message = Localizer.getInstance().getMessage("lblMoveTargetFromOriginToDestination", targets, Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME), destination.getTranslatedName());
}
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message)) {
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message, null)) {
return;
}
}

View File

@@ -196,8 +196,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (destination.equals("Battlefield")) {
sb.append("onto the battlefield");
if (tapped) {
sb.append(" tapped");
sb.append(" tapped").append(attacking ? " and" : "");
}
sb.append(attacking ? " attacking" : "");
if (sa.hasParam("GainControl")) {
sb.append(" under ").append(chooserNames).append("'s control");
}
@@ -259,14 +260,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (destination.equals("Battlefield")) {
sb.append(" onto the battlefield");
if (tapped) {
sb.append(" tapped");
if (attacking) {
sb.append(" and");
}
}
if (attacking) {
sb.append(" attacking");
sb.append(" tapped").append(attacking ? " and" : "");
}
sb.append(attacking ? " attacking" : "");
if (sa.hasParam("GainControl")) {
sb.append(" under ").append(chooserNames).append("'s control");
}
@@ -366,14 +362,16 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
final String fromGraveyard = " from the graveyard";
if (destination.equals(ZoneType.Battlefield)) {
final boolean attacking = (sa.hasParam("Attacking"));
if (ZoneType.Graveyard.equals(origin)) {
sb.append("Return").append(targetname).append(fromGraveyard).append(" to the battlefield");
} else {
sb.append("Put").append(targetname).append(" onto the battlefield");
}
if (sa.hasParam("Tapped")) {
sb.append(" tapped");
sb.append(" tapped").append(attacking ? " and" : "");
}
sb.append(attacking ? " attacking" : "");
if (sa.hasParam("GainControl")) {
sb.append(" under your control");
}
@@ -494,7 +492,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
PlayerCollection deciders = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("AlternativeDecider"), sa);
alterDecider = deciders.isEmpty() ? null : deciders.get(0);
}
if (alterDecider != null && !alterDecider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString())) {
if (alterDecider != null && !alterDecider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString(), null)) {
destination = ZoneType.smartValueOf(sa.getParam("DestinationAlternative"));
altDest = true;
}
@@ -513,7 +511,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
continue;
}
removeFromStack(tgtSA, sa, si, game, triggerList);
removeFromStack(tgtSA, sa, si, game, triggerList, counterTable);
} // End of change from stack
final String remember = sa.getParam("RememberChanged");
@@ -566,7 +564,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
final String prompt = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblDoYouWantMoveTargetFromOriToDest", CardTranslation.getTranslatedName(gameCard.getName()), Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME), destination.getTranslatedName()));
if (optional && !chooser.getController().confirmAction(sa, null, prompt) )
if (optional && !chooser.getController().confirmAction(sa, null, prompt, null) )
continue;
final Zone originZone = game.getZoneOf(gameCard);
@@ -958,7 +956,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
sb.append(sa.getParam("AlternativeMessage")).append(" ");
sb.append(altFetchList.size()).append(" " + Localizer.getInstance().getMessage("lblCardMatchSearchingTypeInAlternateZones"));
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneFromAltSource, sb.toString())) {
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneFromAltSource, sb.toString(), null)) {
origin = alt;
}
}
@@ -970,7 +968,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
final StringBuilder sb = new StringBuilder();
sb.append(sa.getParam("AlternativeDestinationMessage"));
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString())) {
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString(), null)) {
destination = ZoneType.smartValueOf(sa.getParam("DestinationAlternative"));
libraryPos = sa.hasParam("LibraryPositionAlternative") ? Integer.parseInt(sa.getParam("LibraryPositionAlternative")) : 0;
}
@@ -987,7 +985,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase());
}
String message = MessageUtil.formatMessage(prompt , decider, player);
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message)) {
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) {
return;
}
}
@@ -1073,7 +1071,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
continue;
}
SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas);
if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) {
if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) {
continue;
}
tgtSA.setSVar("IsCastFromPlayEffect", "True");
@@ -1189,7 +1187,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
String message = Localizer.getInstance().getMessage("lblCancelSearchUpToSelectNumCards", String.valueOf(num));
if (fetchList.isEmpty() || sa.hasParam("SkipCancelPrompt") ||
decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message)) {
decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) {
break;
}
i--;
@@ -1545,7 +1543,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
* object.
* @param game
*/
private static void removeFromStack(final SpellAbility tgtSA, final SpellAbility srcSA, final SpellAbilityStackInstance si, final Game game, CardZoneTable triggerList) {
private static void removeFromStack(final SpellAbility tgtSA, final SpellAbility srcSA, final SpellAbilityStackInstance si, final Game game, CardZoneTable triggerList, GameEntityCounterTable counterTable) {
final Card tgtHost = tgtSA.getHostCard();
final Zone originZone = tgtHost.getZone();
game.getStack().remove(si);
@@ -1557,7 +1555,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
Card movedCard = null;
if (srcSA.hasParam("Destination")) {
final boolean remember = srcSA.hasParam("RememberChanged");
final boolean rememberSpell = srcSA.hasParam("RememberSpell");
final boolean imprint = srcSA.hasParam("Imprint");
if (tgtSA.isAbility()) {
// Shouldn't be able to target Abilities but leaving this in for now
@@ -1589,13 +1586,20 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
+ srcSA.getHostCard().getName());
}
if (srcSA.hasParam("WithCountersType")) {
Player placer = srcSA.getActivatingPlayer();
if (srcSA.hasParam("WithCountersPlacer")) {
placer = AbilityUtils.getDefinedPlayers(srcSA.getHostCard(), srcSA.getParam("WithCountersPlacer"), srcSA).get(0);
}
CounterType cType = CounterType.getType(srcSA.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(srcSA.getHostCard(), srcSA.getParamOrDefault("WithCountersAmount", "1"), srcSA);
movedCard.addCounter(cType, cAmount, placer, counterTable);
}
if (remember) {
srcSA.getHostCard().addRemembered(tgtHost);
// TODO or remember moved?
}
if (rememberSpell) {
srcSA.getHostCard().addRemembered(tgtSA);
}
if (imprint) {
srcSA.getHostCard().addImprintedCard(tgtHost);
}

View File

@@ -180,7 +180,7 @@ public class CharmEffect extends SpellAbilityEffect {
num = Math.min(num, choices.size());
boolean isOptional = sa.hasParam("Optional");
if (isOptional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeCharm", CardTranslation.getTranslatedName(source.getName())))) {
if (isOptional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeCharm", CardTranslation.getTranslatedName(source.getName())), null)) {
return false;
}

View File

@@ -131,7 +131,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
Localizer.getInstance().getMessage("lblSelectCreatureWithTotalPowerLessOrEqualTo", (totP - chosenP - negativeNum))
+ "\r\n(" + Localizer.getInstance().getMessage("lblSelected") + ":" + chosenPool + ")\r\n(" + Localizer.getInstance().getMessage("lblTotalPowerNum", chosenP) + ")", chosenP <= totP, null);
if (c == null) {
if (p.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"))) {
if (p.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"), null)) {
break;
}
} else {

View File

@@ -54,7 +54,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
}
boolean randomChoice = sa.hasParam("AtRandom");
boolean draft = sa.hasParam("Draft"); //for digital "draft from spellbook" mechanic
boolean chooseFromDefined = sa.hasParam("ChooseFromDefinedCards");
boolean chooseFromList = sa.hasParam("ChooseFromList");
boolean chooseFromOneTimeList = sa.hasParam("ChooseFromOneTimeList");
@@ -62,8 +61,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
if (!randomChoice) {
if (sa.hasParam("SelectPrompt")) {
message = sa.getParam("SelectPrompt");
} else if (draft) {
message = Localizer.getInstance().getMessage("lblChooseCardDraft");
} else if (null == validDesc) {
message = Localizer.getInstance().getMessage("lblChooseACardName");
} else {
@@ -108,12 +105,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
chosen = p.getController().chooseCardName(sa, faces, message);
} else if (chooseFromList) {
String [] names = sa.getParam("ChooseFromList").split(",");
if (sa.hasParam("Draft")) {
List<String> options = Arrays.asList(names);
Collections.shuffle(options);
List<String> draftChoices = options.subList(0,3);
names = draftChoices.toArray(new String[0]);
}
List<ICardFace> faces = new ArrayList<>();
for (String name : names) {
// Cardnames that include "," must use ";" instead in ChooseFromList$ (i.e. Tovolar; Dire Overlord)
@@ -169,9 +160,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
host.setNamedCard(chosen);
if (!randomChoice) {
p.setNamedCard(chosen);
if (!draft) { //drafting is secret
p.getGame().getAction().notifyOfValue(sa, host, Localizer.getInstance().getMessage("lblPlayerPickedChosen", p.getName(), chosen), p);
}
}
if (sa.hasParam("NoteFor")) {
p.addNoteForName(sa.getParam("NoteFor"), "Name:" + chosen);

View File

@@ -108,7 +108,7 @@ public class CloneEffect extends SpellAbilityEffect {
}
final boolean optional = sa.hasParam("Optional");
if (optional && !host.getController().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantCopy", CardTranslation.getTranslatedName(cardToCopy.getName())))) {
if (optional && !host.getController().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantCopy", CardTranslation.getTranslatedName(cardToCopy.getName())), null)) {
return;
}

View File

@@ -12,7 +12,10 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Lang;
import forge.util.Localizer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -24,13 +27,14 @@ public class ConniveEffect extends SpellAbilityEffect {
*/
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
List<Card> tgt = getTargetCards(sa);
if (tgt.size() <= 0) {
return "";
} else {
final StringBuilder sb = new StringBuilder();
sb.append(Lang.joinHomogenous(tgt)).append(tgt.size() > 1 ? " connive." : " connives.");
return sb.toString();
}
}
/* (non-Javadoc)
@@ -39,55 +43,70 @@ public class ConniveEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Player hostCon = host.getController();
final Game game = host.getGame();
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
GameEntityCounterTable table = new GameEntityCounterTable();
final CardZoneTable triggerList = new CardZoneTable();
Map<Player, CardCollectionView> discardedMap = Maps.newHashMap();
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, sa.getLastStateBattlefield());
moveParams.put(AbilityKey.LastStateGraveyard, sa.getLastStateGraveyard());
CardCollection toConnive = getTargetCards(sa);
if (toConnive.isEmpty()) { // if nothing is conniving, we're done
return;
}
for (final Card c : getTargetCards(sa)) {
final Player p = c.getController();
p.drawCards(num, sa, moveParams);
CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand);
dPHand = CardLists.filter(dPHand, CardPredicates.Presets.NON_TOKEN);
if (dPHand.isEmpty()) { // seems unlikely, but just to be safe
continue; // for loop over players
}
CardCollection validCards = CardLists.getValidCards(dPHand, "Card", hostCon, host, sa);
if (!p.canDiscardBy(sa, true)) {
continue;
}
int amt = Math.min(validCards.size(), num);
CardCollectionView toBeDiscarded = amt == 0 ? CardCollection.EMPTY :
p.getController().chooseCardsToDiscardFrom(p, sa, validCards, amt, amt);
if (toBeDiscarded.size() > 1) {
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
}
discardedMap.put(p, toBeDiscarded);
int numCntrs = CardLists.getValidCardCount(toBeDiscarded, "Card.nonLand", hostCon, host, sa);
// need to get newest game state to check if it is still on the battlefield and the timestamp didn't change
Card gamec = game.getCardState(c);
// if the card is not in the game anymore, this might still return true, but it's no problem
if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(c)) {
c.addCounter(CounterEnumType.P1P1, numCntrs, p, table);
List<Player> controllers = new ArrayList<>();
for (Card c : toConnive) {
final Player controller = c.getController();
if (!controllers.contains(controller)) {
controllers.add(controller);
}
}
//order controllers by APNAP
int indexAP = controllers.indexOf(game.getPhaseHandler().getPlayerTurn());
if (indexAP != -1) {
Collections.rotate(controllers, - indexAP);
}
for (final Player p : controllers) {
CardCollection connivers = CardLists.filterControlledBy(toConnive, p);
while (connivers.size() > 0) {
GameEntityCounterTable table = new GameEntityCounterTable();
final CardZoneTable triggerList = new CardZoneTable();
Map<Player, CardCollectionView> discardedMap = Maps.newHashMap();
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, sa.getLastStateBattlefield());
moveParams.put(AbilityKey.LastStateGraveyard, sa.getLastStateGraveyard());
Card conniver = connivers.size() > 1 ? p.getController().chooseSingleEntityForEffect(connivers, sa,
Localizer.getInstance().getMessage("lblChooseConniver"), null) : connivers.get(0);
p.drawCards(num, sa, moveParams);
CardCollection validDisards =
CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN);
if (validDisards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe
continue;
}
int amt = Math.min(validDisards.size(), num);
CardCollectionView toBeDiscarded = amt == 0 ? CardCollection.EMPTY :
p.getController().chooseCardsToDiscardFrom(p, sa, validDisards, amt, amt);
if (toBeDiscarded.size() > 1) {
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
}
int numCntrs = CardLists.getValidCardCount(toBeDiscarded, "Card.nonLand", p, host, sa);
// need to get newest game state to check if it is still on the battlefield and the timestamp didn't change
Card gamec = game.getCardState(conniver);
// if the card is not in the game anymore, this might still return true, but it's no problem
if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(conniver)) {
conniver.addCounter(CounterEnumType.P1P1, numCntrs, p, table);
}
connivers.remove(conniver);
discardedMap.put(p, CardCollection.getView(toBeDiscarded));
discard(sa, triggerList, true, discardedMap, moveParams);
table.replaceCounterEffect(game, sa, true);
triggerList.triggerChangesZoneAll(game, sa);
}
}
discard(sa, triggerList, true, discardedMap, moveParams);
table.replaceCounterEffect(game, sa, true);
triggerList.triggerChangesZoneAll(game, sa);
}
}

View File

@@ -89,7 +89,7 @@ public class ControlExchangeEffect extends SpellAbilityEffect {
if (sa.hasParam("Optional") && !sa.getActivatingPlayer().getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblExchangeControl",
CardTranslation.getTranslatedName(object1.getName()),
CardTranslation.getTranslatedName(object2.getName())))) {
CardTranslation.getTranslatedName(object2.getName())), null)) {
return;
}

View File

@@ -1,5 +1,9 @@
package forge.game.ability.effects;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.google.common.collect.Lists;
import forge.GameCommand;
@@ -16,9 +20,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Localizer;
import java.util.Arrays;
import java.util.List;
public class ControlGainEffect extends SpellAbilityEffect {
@Override
@@ -118,22 +119,24 @@ public class ControlGainEffect extends SpellAbilityEffect {
final Game game = newController.getGame();
CardCollectionView tgtCards = null;
if (sa.hasParam("ControlledByTarget")) {
tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa));
} else if (sa.hasParam("Choices")) {
if (sa.hasParam("Choices")) {
Player chooser = sa.hasParam("Chooser") ? AbilityUtils.getDefinedPlayers(source,
sa.getParam("Chooser"), sa).get(0) : activator;
CardCollectionView choices = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield),
sa.getParam("Choices"), activator, source, sa);
if (!choices.isEmpty()) {
String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") :
Localizer.getInstance().getMessage("lblChooseaCard") +" ";
tgtCards = activator.getController().chooseCardsForEffect(choices, sa, title, 1, 1, false, null);
Localizer.getInstance().getMessage("lblChooseaCard") +" ";
tgtCards = chooser.getController().chooseCardsForEffect(choices, sa, title, 1, 1, false, null);
}
} else {
tgtCards = getDefinedCards(sa);
}
if (tgtCards != null & sa.hasParam("ControlledByTarget")) {
tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa));
}
// in case source was LKI or still resolving
if (source.isLKI() || source.getZone().is(ZoneType.Stack)) {
source = game.getCardState(source);
@@ -208,11 +211,11 @@ public class ControlGainEffect extends SpellAbilityEffect {
}
if (lose.contains("EOT")) {
game.getEndOfTurn().addUntil(loseControl);
tgtC.setSVar("SacMe", "6");
tgtC.addChangedSVars(Collections.singletonMap("SacMe", "6"), tStamp, 0);
}
if (lose.contains("EndOfCombat")) {
game.getEndOfCombat().addUntil(loseControl);
tgtC.setSVar("SacMe", "6");
tgtC.addChangedSVars(Collections.singletonMap("SacMe", "6"), tStamp, 0);
}
if (lose.contains("StaticCommandCheck")) {
String leftVar = sa.getSVar(sa.getParam("StaticCommandCheckSVar"));
@@ -274,7 +277,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
@Override
public void run() {
doLoseControl(c, hostCard, bTapOnLose, tStamp);
c.removeSVar("SacMe");
c.removeChangedSVars(tStamp, 0);
}
};

View File

@@ -119,7 +119,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
TokenCreateTable tokenTable = new TokenCreateTable();
if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblCopyPermanentConfirm"))) {
Localizer.getInstance().getMessage("lblCopyPermanentConfirm"), null)) {
return;
}
@@ -208,7 +208,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
if (choosen != null) {
tgtCards.add(choosen);
choices = CardLists.filter(choices, Predicates.not(CardPredicates.sharesNameWith(choosen)));
} else if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"))) {
} else if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"), null)) {
break;
}
}
@@ -224,6 +224,13 @@ public class CopyPermanentEffect extends TokenEffectBase {
}
for (final Card c : tgtCards) {
// 111.5. Similarly, if an effect would create a token that is a copy of an instant or sorcery card, no token is created.
// instant and sorcery can't enter the battlefield
// and it can't be replaced by other tokens
if (c.isInstant() || c.isSorcery()) {
continue;
}
// if it only targets player, it already got all needed cards from defined
if (sa.usesTargeting() && !sa.getTargetRestrictions().canTgtPlayer() && !c.canBeTargetedBy(sa)) {
continue;

View File

@@ -26,6 +26,7 @@ import forge.util.Aggregates;
import forge.util.CardTranslation;
import forge.util.Localizer;
import forge.util.Lang;
import forge.util.collect.FCollection;
public class CopySpellAbilityEffect extends SpellAbilityEffect {
@@ -88,7 +89,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa,
Localizer.getInstance().getMessage("lblSelectASpellCopy"), ImmutableMap.of());
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", CardTranslation.getTranslatedName(chosenSA.getHostCard().getName())))) {
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", CardTranslation.getTranslatedName(chosenSA.getHostCard().getName())), null)) {
continue;
}
@@ -128,32 +129,38 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
final Player p = (Player) o;
if (p.equals(originalTargetPlayer))
continue;
if (p.isValid(type.split(","), chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa)) {
if (p.isValid(type.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa)) {
players.add(p);
}
}
}
valid = CardLists.getValidCards(valid, type, chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa);
valid = CardLists.getValidCards(valid, type, sa.getActivatingPlayer(), sa.getHostCard(), sa);
Card originalTarget = Iterables.getFirst(getTargetCards(chosenSA), null);
valid.remove(originalTarget);
if (sa.hasParam("ChooseOnlyOne")) {
Card choice = controller.getController().chooseSingleEntityForEffect(valid, sa, Localizer.getInstance().getMessage("lblChooseOne"), null);
FCollection<GameEntity> all = new FCollection<>(valid);
all.addAll(players);
GameEntity choice = controller.getController().chooseSingleEntityForEffect(all, sa,
Localizer.getInstance().getMessage("lblChooseOne"), null);
if (choice != null) {
valid = new CardCollection(choice);
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller);
resetFirstTargetOnCopy(copy, choice, targetedSA);
copies.add(copy);
}
} else {
for (final Card c : valid) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller);
resetFirstTargetOnCopy(copy, c, targetedSA);
copies.add(copy);
}
for (final Player p : players) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller);
resetFirstTargetOnCopy(copy, p, targetedSA);
copies.add(copy);
}
}
for (final Card c : valid) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller);
resetFirstTargetOnCopy(copy, c, targetedSA);
copies.add(copy);
}
for (final Player p : players) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller);
resetFirstTargetOnCopy(copy, p, targetedSA);
copies.add(copy);
}
}
} else {
for (int i = 0; i < amount; i++) {

View File

@@ -123,6 +123,7 @@ public class CounterEffect extends SpellAbilityEffect {
for (final SpellAbility tgtSA : sas) {
final Card tgtSACard = tgtSA.getHostCard();
// should remember even that spell cannot be countered, e.g. Dovescape
// TODO use LKI in case the spell gets countered before (else X amounts would be missing)
if (sa.hasParam("RememberCounteredCMC")) {
sa.getHostCard().addRemembered(Integer.valueOf(tgtSACard.getCMC()));
}
@@ -152,9 +153,7 @@ public class CounterEffect extends SpellAbilityEffect {
}
if (sa.hasParam("RememberCountered")) {
if (sa.getParam("RememberCountered").equals("True")) {
sa.getHostCard().addRemembered(tgtSACard);
}
sa.getHostCard().addRemembered(tgtSACard);
}
if (sa.hasParam("RememberSplicedOntoCounteredSpell")) {

View File

@@ -20,7 +20,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
final StringBuilder sb = new StringBuilder();
final CounterType cType = CounterType.getType(sa.getParam("CounterType"));
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa);
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParamOrDefault("CounterNum", "1"), sa);
final String zone = sa.getParamOrDefault("ValidZone", "Battlefield");
sb.append("Put ");
@@ -45,7 +45,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
final Card host = sa.getHostCard();
final Player activator = sa.getActivatingPlayer();
final CounterType type = CounterType.getType(sa.getParam("CounterType"));
final int counterAmount = AbilityUtils.calculateAmount(host, sa.getParam("CounterNum"), sa);
final int counterAmount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("CounterNum", "1"), sa);
final String valid = sa.getParam("ValidCards");
final ZoneType zone = sa.hasParam("ValidZone") ? ZoneType.smartValueOf(sa.getParam("ValidZone")) : ZoneType.Battlefield;
final Game game = activator.getGame();

View File

@@ -174,7 +174,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
boolean putOnDefined = sa.hasParam("PutOnDefined");
if (sa.hasParam("Optional") && !pc.confirmAction
(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"))) {
(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"), null)) {
return;
}
@@ -231,7 +231,9 @@ public class CountersPutEffect extends SpellAbilityEffect {
}
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", counterType);
if (counterType != null) {
params.put("CounterType", counterType);
}
if (sa.hasParam("DividedRandomly")) {
tgtObjects.addAll(choices);
} else {
@@ -509,7 +511,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
Player chooser = pc.chooseSingleEntityForEffect(activator.getOpponents(), sa,
Localizer.getInstance().getMessage("lblChooseAnOpponent"), params);
if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.Tribute, message)) {
if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.Tribute, message, null)) {
gameCard.setTributed(true);
} else {
continue;

View File

@@ -76,7 +76,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
}
if (!eachExisting && sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(),
CardTranslation.getTranslatedName(gameCard.getName())))) {
CardTranslation.getTranslatedName(gameCard.getName())), null)) {
continue;
}
if (!sa.usesTargeting() || gameCard.canBeTargetedBy(sa)) {

View File

@@ -83,7 +83,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
if (sa.hasParam("Optional")) {
String ctrs = cntToRemove > 1 ? Localizer.getInstance().getMessage("lblCounters") : num.equals("All") ? Localizer.getInstance().getMessage("lblAllCounters") : Localizer.getInstance().getMessage("lblACounters");
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRemove") + " " + ctrs + "?")) {
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRemove") + " " + ctrs + "?", null)) {
return;
}
}

View File

@@ -156,7 +156,7 @@ public class DamageDealEffect extends DamageBaseEffect {
List<GameObject> tgts = getTargets(sa);
if (sa.hasParam("OptionalDecider")) {
Player decider = Iterables.getFirst(AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("OptionalDecider"), sa), null);
if (decider != null && !decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantDealTargetDamageToTarget", String.valueOf(dmg), tgts.toString()))) {
if (decider != null && !decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantDealTargetDamageToTarget", String.valueOf(dmg), tgts.toString()), null)) {
return;
}
}
@@ -299,7 +299,7 @@ public class DamageDealEffect extends DamageBaseEffect {
} else {
damageMap.put(sourceLKI, c, dmg);
if (sa.hasParam("ExcessSVar")) {
hostCard.setSVar(sa.getParam("ExcessSVar"), Integer.toString(excess));
sa.setSVar(sa.getParam("ExcessSVar"), Integer.toString(excess));
}
}
}

View File

@@ -72,7 +72,7 @@ public class DelayedTriggerEffect extends SpellAbilityEffect {
}
if (sa.hasParam("RememberSVarAmount")) {
delTrig.addRemembered(AbilityUtils.calculateAmount(host, host.getSVar(sa.getParam("RememberSVarAmount")), sa));
delTrig.addRemembered(AbilityUtils.calculateAmount(host, sa.getSVar(sa.getParam("RememberSVarAmount")), sa));
}
if (sa.hasAdditionalAbility("Execute")) {

View File

@@ -34,73 +34,77 @@ public class DigEffect extends SpellAbilityEffect {
protected String getStackDescription(SpellAbility sa) {
final Card host = sa.getHostCard();
final StringBuilder sb = new StringBuilder();
final int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
final String toChange = sa.getParamOrDefault("ChangeNum", "1");
final int numToChange = toChange.startsWith("All") ? numToDig : AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa);
final List<Player> tgtPlayers = getTargetPlayers(sa);
String verb = " looks at ";
if (sa.hasParam("Reveal") && sa.getParam("Reveal").equals("True")) {
verb = " reveals ";
} else if (sa.hasParam("DestinationZone") && sa.getParam("DestinationZone").equals("Exile") &&
numToDig == numToChange) {
verb = " exiles ";
}
sb.append(host.getController()).append(verb).append("the top ");
sb.append(numToDig == 1 ? "card" : Lang.getNumeral(numToDig) + " cards").append(" of ");
if (tgtPlayers.contains(host.getController())) {
sb.append("their ");
final String spellDesc = sa.getParamOrDefault("SpellDescription", "");
if (spellDesc.contains("X card")) { // X value can be changed after this goes to the stack, so use set desc
sb.append("[").append(host.getController()).append("] ").append(spellDesc);
} else {
for (final Player p : tgtPlayers) {
sb.append(Lang.getInstance().getPossesive(p.getName())).append(" ");
}
}
sb.append("library.");
final int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
final String toChange = sa.getParamOrDefault("ChangeNum", "1");
final int numToChange = toChange.startsWith("All") ? numToDig : AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa);
if (numToDig != numToChange) {
String destZone1 = sa.hasParam("DestinationZone") ?
sa.getParam("DestinationZone").toLowerCase() : "hand";
String destZone2 = sa.hasParam("DestinationZone2") ?
sa.getParam("DestinationZone2").toLowerCase() : "on the bottom of their library in any order.";
if (sa.hasParam("RestRandomOrder")) {
destZone2 = destZone2.replace("any", "a random");
String verb = " looks at ";
if (sa.hasParam("DestinationZone") && sa.getParam("DestinationZone").equals("Exile") &&
numToDig == numToChange) {
verb = " exiles ";
} else if (sa.hasParam("Reveal") && sa.getParam("Reveal").equals("True")) {
verb = " reveals ";
}
sb.append(host.getController()).append(verb).append("the top ");
sb.append(numToDig == 1 ? "card" : Lang.getNumeral(numToDig) + " cards").append(" of ");
String verb2 = "put ";
String where = " into their hand ";
if (destZone1.equals("exile")) {
verb2 = "exile ";
where = " ";
} else if (destZone1.equals("battlefield")) {
verb2 = "put ";
where = " onto the battlefield ";
}
sb.append(" They ").append(sa.hasParam("Optional") ? "may " : "").append(verb2);
if (sa.hasParam("ChangeValid")) {
String what = sa.hasParam("ChangeValidDesc") ? sa.getParam("ChangeValidDesc") :
sa.getParam("ChangeValid");
if (!StringUtils.containsIgnoreCase(what, "card")) {
what = what + " card";
if (tgtPlayers.contains(host.getController())) {
sb.append("their ");
} else {
for (final Player p : tgtPlayers) {
sb.append(Lang.getInstance().getPossesive(p.getName())).append(" ");
}
sb.append(Lang.nounWithNumeralExceptOne(numToChange, what)).append(" from among them").append(where);
} else {
sb.append(Lang.getNumeral(numToChange)).append(" of them").append(where);
}
sb.append(sa.hasParam("ExileFaceDown") ? "face down " : "");
if (sa.hasParam("WithCounter") || sa.hasParam("ExileWithCounter")) {
String ctr = sa.hasParam("WithCounter") ? sa.getParam("WithCounter") :
sa.getParam("ExileWithCounter");
sb.append("with a ");
sb.append(CounterType.getType(ctr).getName().toLowerCase());
sb.append(" counter on it. They ");
} else {
sb.append("and ");
}
sb.append("put the rest ").append(destZone2);
}
sb.append("library.");
if (numToDig != numToChange) {
String destZone1 = sa.hasParam("DestinationZone") ?
sa.getParam("DestinationZone").toLowerCase() : "hand";
String destZone2 = sa.hasParam("DestinationZone2") ?
sa.getParam("DestinationZone2").toLowerCase() : "on the bottom of their library in any order.";
if (sa.hasParam("RestRandomOrder")) {
destZone2 = destZone2.replace("any", "a random");
}
String verb2 = "put ";
String where = " into their hand ";
if (destZone1.equals("exile")) {
verb2 = "exile ";
where = " ";
} else if (destZone1.equals("battlefield")) {
verb2 = "put ";
where = " onto the battlefield ";
}
sb.append(" They ").append(sa.hasParam("Optional") ? "may " : "").append(verb2);
if (sa.hasParam("ChangeValid")) {
String what = sa.hasParam("ChangeValidDesc") ? sa.getParam("ChangeValidDesc") :
sa.getParam("ChangeValid");
if (!StringUtils.containsIgnoreCase(what, "card")) {
what = what + " card";
}
sb.append(Lang.nounWithNumeralExceptOne(numToChange, what)).append(" from among them").append(where);
} else {
sb.append(Lang.getNumeral(numToChange)).append(" of them").append(where);
}
sb.append(sa.hasParam("ExileFaceDown") ? "face down " : "");
if (sa.hasParam("WithCounter") || sa.hasParam("ExileWithCounter")) {
String ctr = sa.hasParam("WithCounter") ? sa.getParam("WithCounter") :
sa.getParam("ExileWithCounter");
sb.append("with a ");
sb.append(CounterType.getType(ctr).getName().toLowerCase());
sb.append(" counter on it. They ");
} else {
sb.append("and ");
}
sb.append("put the rest ").append(destZone2);
}
}
return sb.toString();
}
@@ -199,7 +203,7 @@ public class DigEffect extends SpellAbilityEffect {
else if (sa.hasParam("RevealOptional")) {
String question = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblReveal") + ":", TextUtil.addSuffix(Lang.joinHomogenous(top),"?"));
hasRevealed = p.getController().confirmAction(sa, null, question);
hasRevealed = p.getController().confirmAction(sa, null, question, null);
if (hasRevealed) {
game.getAction().reveal(top, p);
}
@@ -272,7 +276,7 @@ public class DigEffect extends SpellAbilityEffect {
// Optional abilities that use a dialog box to prompt the user to skip the ability (e.g. Explorer's Scope, Quest for Ula's Temple)
if (optional && mayBeSkipped && !valid.isEmpty()) {
String prompt = !optionalAbilityPrompt.isEmpty() ? optionalAbilityPrompt : Localizer.getInstance().getMessage("lblWouldYouLikeProceedWithOptionalAbility") + " " + host + "?\n\n(" + sa.getDescription() + ")";
if (!p.getController().confirmAction(sa, null, TextUtil.fastReplace(prompt, "CARDNAME", CardTranslation.getTranslatedName(host.getName())))) {
if (!p.getController().confirmAction(sa, null, TextUtil.fastReplace(prompt, "CARDNAME", CardTranslation.getTranslatedName(host.getName())), null)) {
return;
}
}
@@ -413,7 +417,9 @@ public class DigEffect extends SpellAbilityEffect {
c.setTapped(true);
}
if (destZone1.equals(ZoneType.Battlefield) && sa.hasParam("WithCounter")) {
c.addEtbCounter(CounterType.getType(sa.getParam("WithCounter")), 1, player);
final int numCtr = AbilityUtils.calculateAmount(host,
sa.getParamOrDefault("WithCounterNum", "1"), sa);
c.addEtbCounter(CounterType.getType(sa.getParam("WithCounter")), numCtr, player);
}
c = game.getAction().moveTo(zone, c, sa, moveParams);
if (destZone1.equals(ZoneType.Battlefield)) {

View File

@@ -120,7 +120,7 @@ public class DigUntilEffect extends SpellAbilityEffect {
continue;
}
if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) {
if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDigYourLibrary"))) {
if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDigYourLibrary"), null)) {
continue;
}
CardCollection found = new CardCollection();
@@ -171,7 +171,7 @@ public class DigUntilEffect extends SpellAbilityEffect {
final Card c = itr.next();
final ZoneType origin = c.getZone().getZoneType();
if (optionalFound && !p.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblDoYouWantPutCardToZone", foundDest.getTranslatedName()))) {
Localizer.getInstance().getMessage("lblDoYouWantPutCardToZone", foundDest.getTranslatedName()), null)) {
continue;
}
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();

View File

@@ -153,7 +153,7 @@ public class DiscardEffect extends SpellAbilityEffect {
}
boolean runDiscard = !sa.hasParam("Optional")
|| p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage"));
|| p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage"), null);
if (runDiscard) {
toBeDiscarded = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa);
@@ -197,7 +197,7 @@ public class DiscardEffect extends SpellAbilityEffect {
continue;
}
String message = Localizer.getInstance().getMessage("lblWouldYouLikeRandomDiscardTargetCard", String.valueOf(numCards));
boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message);
boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message, null);
if (runDiscard) {
final String valid = sa.getParamOrDefault("DiscardValid", "Card");

View File

@@ -0,0 +1,88 @@
package forge.game.ability.effects;
import forge.StaticData;
import forge.card.ICardFace;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardZoneTable;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Localizer;
import java.util.*;
public class DraftEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final Card source = sa.getHostCard();
final Player player = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa).get(0);
final ZoneType zone = ZoneType.smartValueOf(sa.getParamOrDefault("Zone", "Hand"));
final StringBuilder sb = new StringBuilder();
sb.append(player).append(" drafts a card from ").append(source.getName()).append("'s spellbook");
if (zone.equals("Hand")) {
sb.append(".");
} else if (zone.equals("Battlefield")) {
sb.append(" and puts it onto the battlefield.");
} else if (zone.equals("Exile")) {
sb.append(", then exiles it.");
}
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, sa.getLastStateBattlefield());
moveParams.put(AbilityKey.LastStateGraveyard, sa.getLastStateGraveyard());
final Card source = sa.getHostCard();
final Player player = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa).get(0);
final Game game = player.getGame();
final ZoneType zone = ZoneType.smartValueOf(sa.getParamOrDefault("Zone", "Hand"));
List<String> spellbook = Arrays.asList(sa.getParam("Spellbook").split(","));
final int numToDraft = AbilityUtils.calculateAmount(source,
sa.getParamOrDefault("DraftNum", "1"), sa);
CardCollection drafted = new CardCollection();
for (int i = 0; i < numToDraft; i++) {
String chosen = "";
Collections.shuffle(spellbook);
List<ICardFace> faces = new ArrayList<>();
for (String name : spellbook.subList(0, 3)) {
// Cardnames that include "," must use ";" instead in Spellbook$ (i.e. Tovolar; Dire Overlord)
name = name.replace(";", ",");
faces.add(StaticData.instance().getCommonCards().getFaceByName(name));
}
chosen = player.getController().chooseCardName(sa, faces,
Localizer.getInstance().getMessage("lblChooseCardDraft"));
if (!chosen.equals("")) {
Card card = Card.fromPaperCard(StaticData.instance().getCommonCards().getUniqueByName(chosen), player);
card.setTokenCard(true);
game.getAction().moveTo(ZoneType.None, card, sa, moveParams);
drafted.add(card);
}
}
final CardZoneTable triggerList = new CardZoneTable();
for (final Card c : drafted) {
Card made = game.getAction().moveTo(zone, c, sa, moveParams);
if (c != null) {
triggerList.put(ZoneType.None, made.getZone().getZoneType(), made);
}
if (sa.hasParam("RememberDrafted")) {
source.addRemembered(made);
}
}
triggerList.triggerChangesZoneAll(game, sa);
if (zone.equals(ZoneType.Library)) {
player.shuffle(sa);
}
}
}

View File

@@ -72,7 +72,7 @@ public class DrawEffect extends SpellAbilityEffect {
continue;
}
if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDrawCards", Lang.nounWithAmount(numCards, " card")))) {
if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDrawCards", Lang.nounWithAmount(numCards, " card")), null)) {
continue;
}

View File

@@ -1,32 +0,0 @@
package forge.game.ability.effects;
import java.util.Map;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
/**
* TODO: Write javadoc for this type.
*
*/
public class ETBReplacementEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final Card card = (Card) sa.getReplacingObject(AbilityKey.Card);
Map<AbilityKey, Object> params = AbilityKey.newMap();
params.put(AbilityKey.CardLKI, sa.getReplacingObject(AbilityKey.CardLKI));
params.put(AbilityKey.ReplacementEffect, sa.getReplacementEffect());
params.put(AbilityKey.LastStateBattlefield, sa.getReplacingObject(AbilityKey.LastStateBattlefield));
params.put(AbilityKey.LastStateGraveyard, sa.getReplacingObject(AbilityKey.LastStateGraveyard));
final SpellAbility root = sa.getRootAbility();
SpellAbility cause = (SpellAbility) root.getReplacingObject(AbilityKey.Cause);
game.getAction().moveToPlay(card, card.getController(), cause, params);
}
}

View File

@@ -60,7 +60,7 @@ public class EffectEffect extends SpellAbilityEffect {
boolean imprintOnHost = false;
final String duration = sa.getParam("Duration");
if (("UntilHostLeavesPlay".equals(duration) || "UntilLoseControlOfHost".equals(duration))
if (((duration != null && duration.startsWith("UntilHostLeavesPlay")) || "UntilLoseControlOfHost".equals(duration))
&& !(hostCard.isInPlay() || hostCard.isInZone(ZoneType.Stack))) {
return;
}
@@ -88,6 +88,13 @@ public class EffectEffect extends SpellAbilityEffect {
effectKeywords = sa.getParam("Keywords").split(",");
}
if (sa.hasParam("RememberSpell")) {
rememberList = new FCollection<>();
for (final String rem : sa.getParam("RememberSpell").split(",")) {
rememberList.addAll(AbilityUtils.getDefinedSpellAbilities(hostCard, rem, sa));
}
}
if (sa.hasParam("RememberObjects")) {
rememberList = new FCollection<>();
for (final String rem : sa.getParam("RememberObjects").split(",")) {
@@ -271,7 +278,7 @@ public class EffectEffect extends SpellAbilityEffect {
}
if (sa.hasParam("CopySVar")) {
eff.setSVar(sa.getParam("CopySVar"), sa.getHostCard().getSVar(sa.getParam("CopySVar")));
eff.setSVar(sa.getParam("CopySVar"), hostCard.getSVar(sa.getParam("CopySVar")));
}
// Copy text changes

View File

@@ -48,7 +48,7 @@ public class EncodeEffect extends SpellAbilityEffect {
}
// Handle choice of whether or not to encoded
if (!player.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantExileCardAndEncodeOntoYouCreature", CardTranslation.getTranslatedName(host.getName())))) {
if (!player.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantExileCardAndEncodeOntoYouCreature", CardTranslation.getTranslatedName(host.getName())), null)) {
return;
}

View File

@@ -20,7 +20,7 @@ public class EndTurnEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final List<Player> enders = getDefinedPlayersOrTargeted(sa, "Defined");
final Player ender = enders.isEmpty() ? sa.getActivatingPlayer() : enders.get(0);
if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"))) {
if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"), null)) {
return;
}
Game game = ender.getGame();

View File

@@ -59,7 +59,7 @@ public class FightEffect extends DamageBaseEffect {
Player controller = host.getController();
boolean isOptional = sa.hasParam("Optional");
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeFight", CardTranslation.getTranslatedName(fighters.get(0).getName()), CardTranslation.getTranslatedName(fighters.get(1).getName())))) {
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeFight", CardTranslation.getTranslatedName(fighters.get(0).getName()), CardTranslation.getTranslatedName(fighters.get(1).getName())), null)) {
return;
}

View File

@@ -7,9 +7,25 @@ import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Lang;
import java.util.List;
public class GoadEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final Player player = sa.getActivatingPlayer();
List<Card> tgt = getTargetCards(sa);
if (tgt.size() <= 0) {
return "";
} else {
final StringBuilder sb = new StringBuilder();
sb.append(player).append(" goads ").append(Lang.joinHomogenous(tgt)).append(".");
return sb.toString();
}
}
@Override
public void resolve(SpellAbility sa) {
final Player player = sa.getActivatingPlayer();

View File

@@ -67,7 +67,7 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect {
if (sa.hasParam("RememberSVarAmount")) {
immediateTrig.addRemembered(AbilityUtils.calculateAmount(host,
host.getSVar(sa.getParam("RememberSVarAmount")), sa));
sa.getSVar(sa.getParam("RememberSVarAmount")), sa));
}
if (sa.hasAdditionalAbility("Execute")) {

View File

@@ -40,7 +40,7 @@ public class InvestigateEffect extends TokenEffectBase {
for (final Player p : getTargetPlayers(sa)) {
for (int i = 0; i < amount; i++) {
if (sa.hasParam("Optional") && !p.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblWouldYouLikeInvestigate"))) {
Localizer.getInstance().getMessage("lblWouldYouLikeInvestigate"), null)) {
return;
}

View File

@@ -2,7 +2,6 @@ package forge.game.ability.effects;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -41,14 +40,7 @@ public class LifeLoseEffect extends SpellAbilityEffect {
lifeLost += p.loseLife(lifeAmount, false, false);
}
}
sa.getHostCard().setSVar("AFLifeLost", "Number$" + lifeLost);
// Exceptional case for Extort: must propagate the amount of life lost to subability,
// otherwise the first Extort trigger per game won't work
if (sa.getSubAbility() != null && ApiType.GainLife.equals(sa.getSubAbility().getApi())) {
sa.getSubAbility().setSVar("AFLifeLost", "Number$" + lifeLost);
}
sa.setSVar("AFLifeLost", "Number$" + lifeLost);
}
}

View File

@@ -9,6 +9,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardZoneTable;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
@@ -60,15 +61,18 @@ public class MakeCardEffect extends SpellAbilityEffect {
amount--;
}
final CardZoneTable triggerList = new CardZoneTable();
for (final Card c : cards) {
game.getAction().moveTo(zone, c, sa, moveParams);
Card made = game.getAction().moveTo(zone, c, sa, moveParams);
triggerList.put(ZoneType.None, made.getZone().getZoneType(), made);
if (sa.hasParam("RememberMade")) {
sa.getHostCard().addRemembered(c);
sa.getHostCard().addRemembered(made);
}
if (sa.hasParam("ImprintMade")) {
sa.getHostCard().addImprintedCard(c);
sa.getHostCard().addImprintedCard(made);
}
}
triggerList.triggerChangesZoneAll(game, sa);
if (zone.equals(ZoneType.Library)) {
player.shuffle(sa);
}

View File

@@ -40,7 +40,7 @@ public class ManaEffect extends SpellAbilityEffect {
final boolean optional = sa.hasParam("Optional");
final Game game = sa.getActivatingPlayer().getGame();
if (optional && !sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"))) {
if (optional && !sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"), null)) {
return;
}

View File

@@ -48,7 +48,7 @@ public class MillEffect extends SpellAbilityEffect {
if (sa.hasParam("Optional")) {
final String prompt = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblDoYouWantPutLibraryCardsTo", destination.getTranslatedName()));
// CR 701.13b
if (numCards > p.getZone(ZoneType.Library).size() || !p.getController().confirmAction(sa, null, prompt)) {
if (numCards > p.getZone(ZoneType.Library).size() || !p.getController().confirmAction(sa, null, prompt, null)) {
continue;
}
}

View File

@@ -87,7 +87,7 @@ public class PeekAndRevealEffect extends SpellAbilityEffect {
}
if (doReveal && sa.hasParam("RevealOptional"))
doReveal = peekingPlayer.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRevealCardToOtherPlayers"));
doReveal = peekingPlayer.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRevealCardToOtherPlayers"), null);
if (doReveal) {
peekingPlayer.getGame().getAction().reveal(revealableCards, ZoneType.Library, libraryToPeek, !noPeek,

View File

@@ -1,5 +1,6 @@
package forge.game.ability.effects;
import java.util.Collections;
import java.util.Map;
import com.google.common.collect.Lists;
@@ -42,12 +43,12 @@ public class PermanentEffect extends SpellAbilityEffect {
// some extra for Dashing
if (sa.isDash() && c.isInPlay()) {
c.setSVar("EndOfTurnLeavePlay", "Dash");
c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "Dash"), c.getGame().getNextTimestamp(), 0);
registerDelayedTrigger(sa, "Hand", Lists.newArrayList(c));
}
// similar for Blitz keyword
if (sa.isBlitz() && c.isInPlay()) {
c.setSVar("EndOfTurnLeavePlay", "Blitz");
c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "Blitz"), c.getGame().getNextTimestamp(), 0);
registerDelayedTrigger(sa, "Sacrifice", Lists.newArrayList(c));
}

View File

@@ -259,7 +259,7 @@ public class PlayEffect extends SpellAbilityEffect {
}
if (singleOption && sa.getTargetCard() == null)
sa.setPlayEffectCard(tgtCard);// show card to play rather than showing the source card
if (singleOption && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) {
if (singleOption && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) {
if (wasFaceDown) {
tgtCard.turnFaceDownNoUpdate();
tgtCard.updateStateForView();
@@ -285,6 +285,7 @@ public class PlayEffect extends SpellAbilityEffect {
}
}
// TODO if cost isn't replaced should include alternative ones
// get basic spells (no flashback, etc.)
List<SpellAbility> sas = AbilityUtils.getBasicSpellsFromPlayEffect(tgtCard, controller);
if (sa.hasParam("ValidSA")) {
@@ -435,8 +436,9 @@ public class PlayEffect extends SpellAbilityEffect {
}
final Zone currentZone = game.getCardState(tgtCard).getZone();
if (!originZone.equals(currentZone)) {
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), game.getCardState(tgtCard));
if (!currentZone.equals(originZone)) {
//fix Garth One-Eye activated ability and the likes..
triggerList.put(originZone == null ? null : originZone.getZoneType(), currentZone.getZoneType(), game.getCardState(tgtCard));
}
triggerList.triggerChangesZoneAll(game, sa);
}

View File

@@ -37,13 +37,15 @@ public class PumpEffect extends SpellAbilityEffect {
final long timestamp) {
final Card host = sa.getHostCard();
final Game game = host.getGame();
final String duration = sa.getParam("Duration");
//if host is not on the battlefield don't apply
// Suspend should does Affect the Stack
if (("UntilHostLeavesPlay".equals(sa.getParam("Duration")) || "UntilLoseControlOfHost".equals(sa.getParam("Duration")))
if (((duration != null && duration.startsWith("UntilHostLeavesPlay")) || "UntilLoseControlOfHost".equals(duration))
&& !(host.isInPlay() || host.isInZone(ZoneType.Stack))) {
return;
}
if ("UntilLoseControlOfHost".equals(sa.getParam("Duration")) && host.getController() != sa.getActivatingPlayer()) {
if ("UntilLoseControlOfHost".equals(duration) && host.getController() != sa.getActivatingPlayer()) {
return;
}
@@ -92,7 +94,7 @@ public class PumpEffect extends SpellAbilityEffect {
addLeaveBattlefieldReplacement(gameCard, sa, sa.getParam("LeaveBattlefield"));
}
if (!"Permanent".equals(sa.getParam("Duration"))) {
if (!"Permanent".equals(duration)) {
// If not Permanent, remove Pumped at EOT
final GameCommand untilEOT = new GameCommand() {
private static final long serialVersionUID = -42244224L;
@@ -123,9 +125,11 @@ public class PumpEffect extends SpellAbilityEffect {
private static void applyPump(final SpellAbility sa, final Player p,
final List<String> keywords, final long timestamp) {
final Card host = sa.getHostCard();
final String duration = sa.getParam("Duration");
//if host is not on the battlefield don't apply
// Suspend should does Affect the Stack
if (("UntilHostLeavesPlay".equals(sa.getParam("Duration")) || "UntilLoseControlOfHost".equals(sa.getParam("Duration")))
if (((duration != null && duration.startsWith("UntilHostLeavesPlay")) || "UntilLoseControlOfHost".equals(duration))
&& !(host.isInPlay() || host.isInZone(ZoneType.Stack))) {
return;
}
@@ -134,7 +138,7 @@ public class PumpEffect extends SpellAbilityEffect {
p.addChangedKeywords(keywords, ImmutableList.of(), timestamp, 0);
}
if (!"Permanent".equals(sa.getParam("Duration"))) {
if (!"Permanent".equals(duration)) {
// If not Permanent, remove Pumped at EOT
final GameCommand untilEOT = new GameCommand() {
private static final long serialVersionUID = -32453460L;
@@ -348,7 +352,7 @@ public class PumpEffect extends SpellAbilityEffect {
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
: Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets);
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message)) {
if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message, null)) {
return;
}
}

View File

@@ -116,7 +116,7 @@ public class RearrangeTopOfLibraryEffect extends SpellAbilityEffect {
Card next = orderedCards.get(i);
player.getGame().getAction().moveToLibrary(next, 0, sa);
}
if (mayshuffle && activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantShuffleTheLibrary"))) {
if (mayshuffle && activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantShuffleTheLibrary"), null)) {
player.shuffle(sa);
}
}

View File

@@ -47,7 +47,7 @@ public class RepeatEachEffect extends SpellAbilityEffect {
final Player player = sa.getActivatingPlayer();
final Game game = player.getGame();
if (sa.hasParam("Optional") && sa.hasParam("OptionPrompt") && //for now, OptionPrompt is needed
!player.getController().confirmAction(sa, null, sa.getParam("OptionPrompt"))) {
!player.getController().confirmAction(sa, null, sa.getParam("OptionPrompt"), null)) {
return;
}
@@ -151,7 +151,7 @@ public class RepeatEachEffect extends SpellAbilityEffect {
}
}
for (final Player p : repeatPlayers) {
if (optional && !p.getController().confirmAction(repeat, null, sa.getParam("RepeatOptionalMessage"))) {
if (optional && !p.getController().confirmAction(repeat, null, sa.getParam("RepeatOptionalMessage"), null)) {
continue;
}
if (nextTurn) {

View File

@@ -115,7 +115,7 @@ public class RepeatEffect extends SpellAbilityEffect {
Player decider = sa.hasParam("RepeatOptionalDecider")
? AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("RepeatOptionalDecider"), sa).get(0)
: sa.getActivatingPlayer();
return decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRepeatProcessAgain"));
return decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRepeatProcessAgain"), null);
}
return true;

View File

@@ -55,6 +55,7 @@ public class RestartGameEffect extends SpellAbilityEffect {
game.getStack().reset();
game.clearCounterAddedThisTurn();
game.setMonarch(null);
game.setHasInitiative(null);
game.setDayTime(null);
GameAction action = game.getAction();

View File

@@ -41,7 +41,7 @@ public class RevealHandEffect extends SpellAbilityEffect {
for (final Player p : getTargetPlayers(sa)) {
if ((tgt == null) || p.canBeTargetedBy(sa)) {
if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRevealYourHand"))) {
if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRevealYourHand"), null)) {
continue;
}
CardCollectionView hand = p.getCardsIn(ZoneType.Hand);

View File

@@ -126,13 +126,13 @@ public class RollDiceEffect extends SpellAbilityEffect {
total += modifier;
if (sa.hasParam("ResultSVar")) {
host.setSVar(sa.getParam("ResultSVar"), Integer.toString(total));
sa.setSVar(sa.getParam("ResultSVar"), Integer.toString(total));
}
if (sa.hasParam("ChosenSVar")) {
int chosen = player.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblChooseAResult"), rolls, player);
String message = Localizer.getInstance().getMessage("lblPlayerChooseValue", player, chosen);
player.getGame().getAction().notifyOfValue(sa, player, message, player);
host.setSVar(sa.getParam("ChosenSVar"), Integer.toString(chosen));
sa.setSVar(sa.getParam("ChosenSVar"), Integer.toString(chosen));
if (sa.hasParam("OtherSVar")) {
int other = rolls.get(0);
for (int i = 1; i < rolls.size(); ++i) {
@@ -141,7 +141,7 @@ public class RollDiceEffect extends SpellAbilityEffect {
break;
}
}
host.setSVar(sa.getParam("OtherSVar"), Integer.toString(other));
sa.setSVar(sa.getParam("OtherSVar"), Integer.toString(other));
}
}

View File

@@ -46,7 +46,7 @@ public class SacrificeEffect extends SpellAbilityEffect {
if (sa.hasParam("Echo")) {
boolean isPaid;
if (activator.hasKeyword("You may pay 0 rather than pay the echo cost for permanents you control.")
&& activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPayEcho") + " {0}?")) {
&& activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPayEcho") + " {0}?", null)) {
isPaid = true;
} else {
isPaid = activator.getController().payManaOptional(card, new Cost(sa.getParam("Echo"), true),
@@ -110,7 +110,7 @@ public class SacrificeEffect extends SpellAbilityEffect {
if (valid.equals("Self") && game.getZoneOf(card) != null) {
if (game.getZoneOf(card).is(ZoneType.Battlefield)) {
if (!optional || activator.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", card.getName()))) {
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", card.getName()), null)) {
if (game.getAction().sacrifice(card, sa, true, table, params) != null) {
if (remSacrificed) {
card.addRemembered(card);
@@ -153,7 +153,7 @@ public class SacrificeEffect extends SpellAbilityEffect {
if (sa.hasParam("Random")) {
choosenToSacrifice = Aggregates.random(validTargets, Math.min(amount, validTargets.size()), new CardCollection());
} else if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSacrifice"))) {
} else if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSacrifice"), null)) {
choosenToSacrifice = CardCollection.EMPTY;
} else {
boolean isStrict = sa.hasParam("StrictAmount");

View File

@@ -44,7 +44,7 @@ public class ScryEffect extends SpellAbilityEffect {
// 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, Localizer.getInstance().getMessage("lblDoYouWanttoScry"))) ) {
(!isOptional || p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWanttoScry"), null)) ) {
players.add(p);
}
}

View File

@@ -150,7 +150,7 @@ public class SetStateEffect extends SpellAbilityEffect {
if (optional) {
String message = TextUtil.concatWithSpace("Transform", gameCard.getName(), "?");
if (!p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message)) {
if (!p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message, null)) {
return;
}
}

View File

@@ -21,7 +21,7 @@ public class ShuffleEffect extends SpellAbilityEffect {
for (final Player p : tgtPlayers) {
if ((tgt == null) || p.canBeTargetedBy(sa)) {
boolean mustShuffle = !optional || sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblHaveTargetShuffle", p.getName()));
boolean mustShuffle = !optional || sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblHaveTargetShuffle", p.getName()), null);
if (mustShuffle)
p.shuffle(sa);
}

View File

@@ -43,7 +43,7 @@ public class SurveilEffect extends SpellAbilityEffect {
for (final Player p : getTargetPlayers(sa)) {
if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) {
if (isOptional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSurveil"))) {
if (isOptional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSurveil"), null)) {
continue;
}

View File

@@ -0,0 +1,35 @@
package forge.game.ability.effects;
import java.util.List;
import forge.game.ability.SpellAbilityEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Lang;
public class TakeInitiativeEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final List<Player> tgtPlayers = getTargetPlayers(sa);
sb.append(Lang.joinHomogenous(tgtPlayers)).append(tgtPlayers.size() == 1 ? " takes" : " take");
sb.append(" the initiative.");
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
// TODO: improve ai and fix corner cases
final String set = sa.getHostCard().getSetCode();
for (final Player p : getTargetPlayers(sa)) {
if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) {
p.getGame().getAction().takeInitiative(p, set);
}
}
}
}

View File

@@ -64,7 +64,8 @@ public class TokenEffect extends TokenEffectBase {
if (numTokens != 0) { //0 probably means calculation isn't ready in time for stack
if (numTokens != 1) { //if we are making more than one, substitute the numeral for a/an
String numeral = " " + Lang.getNumeral(numTokens) + " ";
String target = " " + words.get(words.indexOf(verb) + 1) + " ";
List<String> words2 = Arrays.asList(desc.split(" "));
String target = " " + words2.get(words2.indexOf(verb) + 1) + " ";
desc = desc.replaceFirst(target, numeral);
}
//try to cut out unneeded description, which would now be confusing

View File

@@ -24,6 +24,7 @@ import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.Localizer;
import forge.util.PredicateString.StringOp;
public class VentureEffect extends SpellAbilityEffect {
@@ -42,10 +43,19 @@ public class VentureEffect extends SpellAbilityEffect {
}
}
// Create a new dungeon card chosen by player in command zone.
List<PaperCard> dungeonCards = StaticData.instance().getVariantCards().getAllCards(
Predicates.compose(CardRulesPredicates.Presets.IS_DUNGEON, PaperCard.FN_GET_RULES));
List<PaperCard> dungeonCards = null;
if (sa.hasParam("Dungeon")) {
dungeonCards = StaticData.instance().getVariantCards()
.getAllCards(Predicates.compose(
Predicates.and(CardRulesPredicates.Presets.IS_DUNGEON,
CardRulesPredicates.subType(StringOp.EQUALS, sa.getParam("Dungeon"))),
PaperCard.FN_GET_RULES));
} else {
// Create a new dungeon card chosen by player in command zone.
dungeonCards = StaticData.instance().getVariantCards().getAllCards(
Predicates.compose(CardRulesPredicates.Presets.IS_DUNGEON, PaperCard.FN_GET_RULES));
dungeonCards.removeIf(c -> !c.getRules().isEnterableDungeon());
}
String message = Localizer.getInstance().getMessage("lblChooseDungeon");
Card dungeon = player.getController().chooseDungeon(player, dungeonCards, message);

View File

@@ -126,7 +126,7 @@ public class VoteEffect extends SpellAbilityEffect {
}
if (sa.hasParam("StoreVoteNum")) {
for (final Object type : voteType) {
host.setSVar("VoteNum" + type, "Number$" + votes.get(type).size());
sa.setSVar("VoteNum" + type, "Number$" + votes.get(type).size());
}
} else {
for (final String subAb : subAbs) {

View File

@@ -54,13 +54,7 @@ import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityCantPutCounter;
import forge.game.staticability.StaticAbilityCantSacrifice;
import forge.game.staticability.StaticAbilityCantTarget;
import forge.game.staticability.StaticAbilityCantTransform;
import forge.game.staticability.StaticAbilityIgnoreLegendRule;
import forge.game.staticability.*;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
@@ -165,6 +159,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private final NavigableMap<Long, CardCloneStates> clonedStates = Maps.newTreeMap(); // Layer 1
private final Table<Long, Long, Map<String, String>> changedSVars = TreeBasedTable.create();
private final Map<Long, PlayerCollection> mayLook = Maps.newHashMap();
private final PlayerCollection mayLookFaceDownExile = new PlayerCollection();
private final PlayerCollection mayLookTemp = new PlayerCollection();
@@ -181,15 +177,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private final CardChangedWords changedTextColors = new CardChangedWords();
private final CardChangedWords changedTextTypes = new CardChangedWords();
/** Original values of SVars changed by text changes. */
private Map<String, String> originalSVars = Maps.newHashMap();
private final Set<Object> rememberedObjects = Sets.newLinkedHashSet();
private Map<Player, String> flipResult;
private List<Pair<Card, Integer>> receivedDamageFromThisTurn = Lists.newArrayList();
private Map<Player, Integer> receivedDamageFromPlayerThisTurn = Maps.newHashMap();
private final Map<Card, Integer> assignedDamageMap = Maps.newTreeMap();
private boolean isCommander = false;
@@ -264,7 +254,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private String oracleText = "";
private int damage;
private Map<Integer, Integer> damage = Maps.newHashMap();
private boolean hasBeenDealtDeathtouchDamage;
// regeneration
@@ -510,7 +500,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
if (game != null) {
// update Type, color and keywords again if they have changed
if (!changedCardTypes.isEmpty()) {
currentState.getView().updateType(currentState);
updateTypesForView();
}
updateColorForView();
@@ -958,7 +948,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
devouredCards.add(c);
}
public final void clearDevoured() {
devouredCards = null;
}
@@ -989,7 +978,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
delvedCards = null;
}
public final CardCollectionView getConvoked() {
return CardCollection.getView(convokedCards);
}
@@ -1592,10 +1580,20 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final String getSVar(final String var) {
for (Map<String, String> map : changedSVars.values()) {
if (map.containsKey(var)) {
return map.get(var);
}
}
return currentState.getSVar(var);
}
public final boolean hasSVar(final String var) {
for (Map<String, String> map : changedSVars.values()) {
if (map.containsKey(var)) {
return true;
}
}
return currentState.hasSVar(var);
}
@@ -1621,6 +1619,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
currentState.removeSVar(var);
}
public final void addChangedSVars(Map<String, String> map, long timestamp, long staticId) {
this.changedSVars.put(timestamp, staticId, map);
}
public final void removeChangedSVars(long timestamp, long staticId) {
this.changedSVars.remove(timestamp, staticId);
}
public final int getTurnInZone() {
return turnInZone;
}
@@ -2091,7 +2096,7 @@ public class Card extends GameEntity implements Comparable<Card>, 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("Friends forever") || keyword.equals("Choose a Background")) {
sbLong.append(keyword).append(" (").append(inst.getReminderText()).append(")");
} else if (keyword.startsWith("Partner:")) {
final String[] k = keyword.split(":");
@@ -2583,7 +2588,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.equals("Devoid") || keyword.equals("Lifelink")
|| keyword.equals("Split second")) {
sbBefore.append(keyword).append(" (").append(inst.getReminderText()).append(")");
sbBefore.append("\r\n");
sbBefore.append("\r\n\r\n");
} else if (keyword.equals("Conspire") || keyword.equals("Epic")
|| keyword.equals("Suspend") || keyword.equals("Jump-start")
|| keyword.equals("Fuse")) {
@@ -3306,8 +3311,14 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final void removeTempController(final Player player) {
boolean changed = false;
// Remove each key that yields this player
this.tempControllers.values().remove(player);
while (tempControllers.values().remove(player)) {
changed = true;
}
if (changed) {
view.updateController(this);
}
}
public final void clearTempControllers() {
@@ -4124,8 +4135,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final boolean toughnessAssignsDamage() {
return getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.toughnessAssignsDamage)
|| hasKeyword("CARDNAME assigns combat damage equal to its toughness rather than its power");
return StaticAbilityCombatDamageToughness.combatDamageToughness(this);
}
// How much combat damage does the card deal
@@ -4448,6 +4458,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
KeywordInterface result;
if (staticId < 1 || !storedKeywords.contains(staticId, kw)) {
result = Keyword.getInstance(kw);
result.setStaticId(staticId);
result.createTraits(this, false);
if (staticId > 0) {
storedKeywords.put(staticId, kw, result);
@@ -4457,6 +4468,12 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
return result;
}
public final void addKeywordForStaticAbility(KeywordInterface kw) {
if (kw.getStaticId() > 0) {
storedKeywords.put(kw.getStaticId(), kw.getOriginal(), kw);
}
}
public final void addChangedCardKeywordsByText(final List<KeywordInterface> keywords, final long timestamp, final long staticId, final boolean updateView) {
// keywords should already created for Card, so no addKeywordsToCard
@@ -4597,7 +4614,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
* Update the changed text of the intrinsic spell abilities and keywords.
*/
public void updateChangedText() {
resetChangedSVars();
// update type
List<String> toAdd = Lists.newArrayList();
@@ -4665,26 +4681,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
changedTextTypes.copyFrom(other.changedTextTypes);
}
/**
* Change a SVar due to a text change effect. Change is volatile and will be
* reverted upon refreshing text changes (unless it is changed again at that
* time).
*
* @param key the SVar name.
* @param value the new SVar value.
*/
public final void changeSVar(final String key, final String value) {
originalSVars.put(key, getSVar(key));
setSVar(key, value);
}
private void resetChangedSVars() {
for (final Entry<String, String> svar : originalSVars.entrySet()) {
setSVar(svar.getKey(), svar.getValue());
}
originalSVars.clear();
}
public final KeywordInterface addIntrinsicKeyword(final String s) {
KeywordInterface inst = currentState.addIntrinsicKeyword(s, true);
if (inst != null) {
@@ -5298,62 +5294,15 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
damageHistory = history;
}
public final List<Pair<Card, Integer>> getReceivedDamageFromThisTurn() {
return receivedDamageFromThisTurn;
public List<Pair<Integer, Boolean>> getDamageReceivedThisTurn() {
return damageReceivedThisTurn;
}
public final void setReceivedDamageFromThisTurn(final List<Pair<Card, Integer>> receivedDamageList) {
receivedDamageFromThisTurn = Lists.newArrayList(receivedDamageList);
}
public final Map<Player, Integer> getReceivedDamageFromPlayerThisTurn() {
return receivedDamageFromPlayerThisTurn;
}
public final void setReceivedDamageFromPlayerThisTurn(final Map<Player, Integer> receivedDamageMap) {
receivedDamageFromPlayerThisTurn = Maps.newHashMap(receivedDamageMap);
}
public int getReceivedDamageByPlayerThisTurn(final Player p) {
if (receivedDamageFromPlayerThisTurn.containsKey(p)) {
return receivedDamageFromPlayerThisTurn.get(p);
}
return 0;
}
public final void addReceivedDamageFromThisTurn(Card c, final int damage) {
int currentDamage = 0;
// because Aegar cares about the past state we need to keep all LKI instances
receivedDamageFromThisTurn.add(Pair.of(c.isLKI() ? c : CardUtil.getLKICopy(c), damage));
Player p = c.getController();
if (p != null) {
if (receivedDamageFromPlayerThisTurn.containsKey(p)) {
currentDamage = receivedDamageFromPlayerThisTurn.get(p);
}
receivedDamageFromPlayerThisTurn.put(p, damage+currentDamage);
}
}
public final void resetReceivedDamageFromThisTurn() {
receivedDamageFromThisTurn.clear();
receivedDamageFromPlayerThisTurn.clear();
}
public final int getTotalDamageReceivedThisTurn() {
int total = 0;
for (Integer i : receivedDamageFromPlayerThisTurn.values()) {
total += i;
}
return total;
public void setDamageReceivedThisTurn(List<Pair<Integer, Boolean>> dmg) {
damageReceivedThisTurn.addAll(dmg);
}
public final boolean hasDealtDamageToOpponentThisTurn() {
for (final GameEntity e : getDamageHistory().getThisTurnDamaged().keySet()) {
if (e instanceof Player) {
final Player p = (Player) e;
if (getController().isOpponentOf(p)) {
return true;
}
}
}
return false;
return getDamageHistory().getDamageDoneThisTurn(null, true, null, "Player.Opponent", this, getController(), null) > 0;
}
/**
@@ -5362,11 +5311,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
* @return the damage done to player p this turn
*/
public final int getTotalDamageDoneBy() {
int sum = 0;
for (final GameEntity e : getDamageHistory().getThisTurnDamaged().keySet()) {
sum += getDamageHistory().getThisTurnDamaged().get(e);
}
return sum;
return getDamageHistory().getDamageDoneThisTurn(null, false, null, null, this, getController(), null);
}
// this is the amount of damage a creature needs to receive before it dies
@@ -5389,15 +5334,26 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final int getDamage() {
return damage;
int sum = 0;
for (int i : damage.values()) {
sum += i;
}
return sum;
}
public final void setDamage(int damage0) {
if (damage == damage0) { return; }
damage = damage0;
if (getDamage() == damage0) { return; }
damage.clear();
if (damage0 != 0) {
damage.put(0, damage0);
}
view.updateDamage(this);
getGame().fireEvent(new GameEventCardStatsChanged(this));
}
public int getMaxDamageFromSource() {
return damage.isEmpty() ? 0 : Collections.max(damage.values());
}
public final boolean hasBeenDealtDeathtouchDamage() {
return hasBeenDealtDeathtouchDamage;
}
@@ -5548,14 +5504,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Run replacement effects
getGame().getReplacementHandler().run(ReplacementType.DealtDamage, AbilityKey.mapFromAffected(this));
addReceivedDamageFromThisTurn(source, damageIn);
source.getDamageHistory().registerDamage(this, damageIn);
if (isCombat) {
source.getDamageHistory().registerCombatDamage(this, damageIn);
} else {
getDamageHistory().setHasBeenDealtNonCombatDamageThisTurn(true);
}
// Run triggers
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.DamageSource, source);
@@ -5582,7 +5530,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
damageType = DamageType.M1M1Counters;
}
else { // 120.3e
damage += damageIn;
int old = damage.getOrDefault(Objects.hash(source.getId(), source.getTimestamp()), 0);
damage.put(Objects.hash(source.getId(), source.getTimestamp()), old + damageIn);
view.updateDamage(this);
}
@@ -5720,7 +5669,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
return false;
}
public final void setForetold(final boolean foretold) {
this.foretold = foretold;
}
@@ -5738,7 +5686,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public final void setForetoldThisTurn(final boolean foretoldThisTurn) {
this.foretoldThisTurn = foretoldThisTurn;
}
public void resetForetoldThisTurn() {
foretoldThisTurn = false;
}
@@ -5809,7 +5756,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public final long getBestowTimestamp() {
return bestowTimestamp;
}
public final void setBestowTimestamp(final long t) {
bestowTimestamp = t;
}
@@ -6212,7 +6158,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
setDamage(0);
}
setHasBeenDealtDeathtouchDamage(false);
resetReceivedDamageFromThisTurn();
setRegeneratedThisTurn(0);
resetShield();
setBecameTargetThisTurn(false);
@@ -6220,6 +6165,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
clearMustBlockCards();
getDamageHistory().setCreatureAttackedLastTurnOf(turn, getDamageHistory().getCreatureAttacksThisTurn() > 0);
getDamageHistory().newTurn();
damageReceivedThisTurn.clear();
clearBlockedByThisTurn();
clearBlockedThisTurn();
resetMayPlayTurn();

View File

@@ -4,11 +4,15 @@ package forge.game.card;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.collect.Lists;
import forge.game.CardTraitBase;
import forge.game.GameEntity;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollection;
/**
* TODO: Write javadoc for this type.
@@ -19,7 +23,7 @@ public class CardDamageHistory {
private boolean creatureAttackedThisCombat = false;
private boolean creatureBlockedThisCombat = false;
private boolean creatureGotBlockedThisCombat = false;
private boolean receivedNonCombatDamageThisTurn = false;
private List<GameEntity> attackedThisTurn = Lists.newArrayList();
private final List<Player> creatureAttackedLastTurnOf = Lists.newArrayList();
@@ -27,13 +31,16 @@ public class CardDamageHistory {
private final List<Player> NotBlockedSinceLastUpkeepOf = Lists.newArrayList();
private final List<Player> NotBeenBlockedSinceLastUpkeepOf = Lists.newArrayList();
private final Map<GameEntity, Integer> damagedThisCombat = Maps.newHashMap();
private final Map<GameEntity, Integer> damagedThisTurn = Maps.newHashMap();
private final Map<GameEntity, Integer> damagedThisTurnInCombat = Maps.newHashMap();
private final Map<GameEntity, Integer> damagedThisGame = Maps.newHashMap();
private List<Pair<Integer, Boolean>> damageDoneThisTurn = Lists.newArrayList();
// only needed for Glen Elendra (Plane)
private final List<Player> damagedThisCombat = Lists.newArrayList();
// only needed for The Fallen
private final FCollection<GameEntity> damagedThisGame = new FCollection<>();
boolean hasdealtDamagetoAny = false;
public final boolean getHasdealtDamagetoAny() {
return !damagedThisGame.isEmpty();
return hasdealtDamagetoAny;
}
// used to see if an attacking creature with a triggering attack ability
@@ -218,87 +225,68 @@ public class CardDamageHistory {
return this.creatureGotBlockedThisCombat;
}
/**
* <p>
* Getter for the field <code>receivedNonCombatDamageThisTurn</code>.
* </p>
*
* @return a boolean.
*/
public boolean hasBeenDealtNonCombatDamageThisTurn() {
return this.receivedNonCombatDamageThisTurn;
}
/**
* <p>
* Setter for the field <code>receivedNonCombatDamageThisTurn</code>.
* </p>
*
* @param b
* a boolean.
*/
public void setHasBeenDealtNonCombatDamageThisTurn(boolean b) {
this.receivedNonCombatDamageThisTurn = b;
}
public final Map<GameEntity, Integer> getThisCombatDamaged() {
public final List<Player> getThisCombatDamaged() {
return damagedThisCombat;
}
public final Map<GameEntity, Integer> getThisTurnDamaged() {
return damagedThisTurn;
}
public final Map<GameEntity, Integer> getThisTurnCombatDamaged() {
return damagedThisTurnInCombat;
}
public final Map<GameEntity, Integer> getThisGameDamaged() {
public final FCollection<GameEntity> getThisGameDamaged() {
return damagedThisGame;
}
/**
* TODO: Write javadoc for this method.
* @param player
*/
public void registerCombatDamage(GameEntity entity, int amount) {
int old = 0;
if (damagedThisCombat.containsKey(entity)) {
old = damagedThisCombat.get(entity);
public void registerDamage(int damage, boolean isCombat, Card sourceLKI, GameEntity target, Map<Integer, Card> lkiCache) {
damagedThisGame.add(target);
hasdealtDamagetoAny = true;
if (isCombat && target instanceof Player) {
damagedThisCombat.add((Player) target);
}
damagedThisCombat.put(entity, old + amount);
old = 0;
if (damagedThisTurnInCombat.containsKey(entity)) {
old = damagedThisTurnInCombat.get(entity);
}
damagedThisTurnInCombat.put(entity, old + amount);
Pair<Integer, Boolean> dmg = Pair.of(damage, isCombat);
damageDoneThisTurn.add(dmg);
target.receiveDamage(dmg);
sourceLKI.getGame().addGlobalDamageHistory(this, dmg, sourceLKI.isLKI() ? sourceLKI : CardUtil.getLKICopy(sourceLKI, lkiCache), CardUtil.getLKICopy(target, lkiCache));
}
/**
* TODO: Write javadoc for this method.
*/
public int getDamageDoneThisTurn(Boolean isCombat, boolean anyIsEnough, String validSourceCard, String validTargetEntity, Card source, Player sourceController, CardTraitBase ctb) {
int sum = 0;
for (Pair<Integer, Boolean> damage : damageDoneThisTurn) {
Pair<Card, GameEntity> sourceToTarget = sourceController.getGame().getDamageLKI(damage);
if (isCombat != null && damage.getRight() != isCombat) {
continue;
}
if (validSourceCard != null && !sourceToTarget.getLeft().isValid(validSourceCard.split(","), sourceController, source == null ? sourceToTarget.getLeft() : source, ctb)) {
continue;
}
if (validTargetEntity != null && !sourceToTarget.getRight().isValid(validTargetEntity.split(","), sourceController, source, ctb)) {
continue;
}
sum += damage.getLeft();
if (anyIsEnough) {
break;
}
}
return sum;
}
public void newTurn() {
damagedThisCombat.clear();
damagedThisTurnInCombat.clear();
damagedThisTurn.clear();
attackedThisTurn.clear();
setHasBeenDealtNonCombatDamageThisTurn(false);
damagedThisCombat.clear();
damageDoneThisTurn.clear();
// if card already LTB we can safely dereference (allows quite a few objects to be cleaned up earlier for bigger boardstates)
CardCollection toRemove = new CardCollection();
for (GameEntity e : damagedThisGame) {
if (e instanceof Card) {
if (((Card) e).getZone().getZoneType() != ZoneType.Battlefield) {
toRemove.add((Card)e);
}
}
}
damagedThisGame.removeAll(toRemove);
}
public void endCombat() {
damagedThisCombat.clear();
}
/**
* TODO: Write javadoc for this method.
* @param player
*/
public void registerDamage(GameEntity entity, int amount) {
int old = 0;
if (damagedThisTurn.containsKey(entity)) {
old = damagedThisTurn.get(entity);
}
damagedThisTurn.put(entity, old + amount);
old = 0;
if (damagedThisGame.containsKey(entity)) {
old = damagedThisGame.get(entity);
}
damagedThisGame.put(entity, old + amount);
}
}

View File

@@ -19,6 +19,8 @@ import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObjectPredicates;
import forge.game.ability.AbilityKey;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.trigger.TriggerType;
public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
@@ -66,11 +68,15 @@ public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
game.getTriggerHandler().runTrigger(TriggerType.DamageDealtOnce, runParams, false);
}
}
// Targets -> Source
for (Map.Entry<GameEntity, Map<Card, Integer>> e : columnMap().entrySet()) {
int sum = 0;
for (final int i : e.getValue().values()) {
sum += i;
// controller list
PlayerCollection controllers = new PlayerCollection();
for (Entry<Card, Integer> ec : e.getValue().entrySet()) {
sum += ec.getValue();
controllers.add(ec.getKey().getController());
}
if (sum > 0) {
final GameEntity ge = e.getKey();
@@ -81,6 +87,15 @@ public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
game.getTriggerHandler().runTrigger(TriggerType.DamageDoneOnce, runParams, false);
}
for (Player p : controllers) {
final GameEntity ge = e.getKey();
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.DamageTarget, ge);
runParams.put(AbilityKey.DamageSource, p);
runParams.put(AbilityKey.IsCombatDamage, isCombat);
game.getTriggerHandler().runTrigger(TriggerType.DamageDoneOnceByController, runParams, false);
}
}
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();

View File

@@ -29,6 +29,7 @@ import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.cost.Cost;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.*;
@@ -203,12 +204,25 @@ public class CardFactory {
} else {
copySA = targetSA.copy(c, controller, false);
c.setCastSA(copySA);
// need to copy keyword
if (targetSA.getKeyword() != null) {
KeywordInterface kw = targetSA.getKeyword().copy(c, false);
copySA.setKeyword(kw);
// need to add the keyword to so static doesn't make new keyword
c.addKeywordForStaticAbility(kw);
}
}
copySA.setCopied(true);
// 707.10b
copySA.setOriginalAbility(targetSA);
// Copied spell is not cast face down
if (copySA instanceof Spell) {
Spell spell = (Spell) copySA;
spell.setCastFaceDown(false);
}
if (targetSA.usesTargeting()) {
// do for SubAbilities too?
copySA.setTargets(targetSA.getTargets().clone());

View File

@@ -52,7 +52,6 @@ import forge.game.GameLogEntryType;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
@@ -651,14 +650,13 @@ public class CardFactoryUtil {
final boolean intrinsic, final String valid, final String zone) {
Card host = card.getCard();
String desc = repAb.getDescription();
setupETBReplacementAbility(repAb);
if (!intrinsic) {
repAb.setIntrinsic(false);
}
StringBuilder repEffsb = new StringBuilder();
repEffsb.append("Event$ Moved | ValidCard$ ").append(valid);
repEffsb.append(" | Destination$ Battlefield | Description$ ").append(desc);
repEffsb.append(" | Destination$ Battlefield | ReplacementResult$ Updated | Description$ ").append(desc);
if (optional) {
repEffsb.append(" | Optional$ True");
}
@@ -751,13 +749,12 @@ public class CardFactoryUtil {
}
SpellAbility sa = AbilityFactory.getAbility(abStr, card);
setupETBReplacementAbility(sa);
if (!intrinsic) {
sa.setIntrinsic(false);
}
String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield "
+ "| Secondary$ True | Description$ " + desc + (!extraparams.equals("") ? " | " + extraparams : "");
+ "| Secondary$ True | ReplacementResult$ Updated | Description$ " + desc + (!extraparams.equals("") ? " | " + extraparams : "");
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card.getCard(), intrinsic, card);
@@ -866,7 +863,7 @@ public class CardFactoryUtil {
inst.addTrigger(bushidoTrigger1);
inst.addTrigger(bushidoTrigger2);
} else if (keyword.equals("Cascade")) {
final StringBuilder trigScript = new StringBuilder("Mode$ SpellCast | ValidCard$ Card.Self" +
final StringBuilder trigScript = new StringBuilder("Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack" +
" | Secondary$ True | TriggerDescription$ Cascade - CARDNAME");
final String abString = "DB$ DigUntil | Defined$ You | Amount$ 1 | Valid$ Card.nonLand+cmcLTCascadeX" +
@@ -959,7 +956,7 @@ public class CardFactoryUtil {
inst.addTrigger(parsedTrigger);
inst.addTrigger(parsedTrigReturn);
} else if (keyword.startsWith("Casualty")) {
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Casualty | Secondary$ True";
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Casualty | TriggerZones$ Stack | Secondary$ True";
String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ 1 | MayChooseTarget$ True";
String[] k = keyword.split(":");
if (k.length > 2) {
@@ -972,7 +969,7 @@ public class CardFactoryUtil {
inst.addTrigger(casualtyTrigger);
} else if (keyword.equals("Conspire")) {
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Conspire | Secondary$ True | TriggerDescription$ Copy CARDNAME if its conspire cost was paid";
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Conspire | TriggerZones$ Stack | Secondary$ True | TriggerDescription$ Copy CARDNAME if its conspire cost was paid";
final String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ 1 | MayChooseTarget$ True";
final Trigger conspireTrigger = TriggerHandler.parseTrigger(trigScript, card, intrinsic);
@@ -1037,7 +1034,7 @@ public class CardFactoryUtil {
parsedTrigger.setOverridingAbility(delayTrigSA);
inst.addTrigger(parsedTrigger);
} else if (keyword.equals("Demonstrate")) {
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerDescription$ Demonstrate (" + inst.getReminderText() + ")";
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | TriggerDescription$ Demonstrate (" + inst.getReminderText() + ")";
final String youCopyStr = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | MayChooseTarget$ True | Optional$ True | RememberCopies$ True | IgnoreFreeze$ True";
final String chooseOppStr = "DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent | ConditionDefined$ Remembered | ConditionPresent$ Spell";
final String oppCopyStr = "DB$ CopySpellAbility | Controller$ ChosenPlayer | Defined$ TriggeredSpellAbility | MayChooseTarget$ True | ConditionDefined$ Remembered | ConditionPresent$ Spell";
@@ -1226,7 +1223,7 @@ public class CardFactoryUtil {
inst.addTrigger(trigger);
} else if (keyword.startsWith("Gravestorm")) {
String trigStr = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerDescription$ Gravestorm (" + inst.getReminderText() + ")";
String trigStr = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | TriggerDescription$ Gravestorm (" + inst.getReminderText() + ")";
String copyStr = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ GravestormCount | MayChooseTarget$ True";
SpellAbility copySa = AbilityFactory.getAbility(copyStr, card);
@@ -1683,7 +1680,7 @@ public class CardFactoryUtil {
final String abString = "DB$ PeekAndReveal | PeekAmount$ " + num + " | RememberRevealed$ True";
final String dbCast = "DB$ Play | Valid$ Card.IsRemembered+sameName | " +
final String dbCast = "DB$ Play | Valid$ Card.IsRemembered+sameName | ValidSA$ Spell | " +
"ValidZone$ Library | WithoutManaCost$ True | Optional$ True | " +
"Amount$ All";
@@ -1784,7 +1781,7 @@ public class CardFactoryUtil {
inst.addTrigger(parsedTrigger);
} else if (keyword.equals("Storm")) {
final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | Secondary$ True"
final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | Secondary$ True"
+ "| TriggerDescription$ Storm (" + inst.getReminderText() + ")";
String effect = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ StormCount | MayChooseTarget$ True";
@@ -1896,7 +1893,7 @@ public class CardFactoryUtil {
String upkeepTrig = "Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | " +
"TriggerDescription$ " + sb.toString();
String effect = "DB$ Sacrifice | SacValid$ Self | UnlessPayer$ You | UnlessCost$ " + k[1];
String effect = "DB$ SacrificeAll | Defined$ Self | Controller$ You | UnlessPayer$ You | UnlessCost$ " + k[1];
final Trigger parsedTrigger = TriggerHandler.parseTrigger(upkeepTrig, card, intrinsic);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
@@ -2055,7 +2052,7 @@ public class CardFactoryUtil {
// Setup ETB replacement effects
final String actualRep = "Event$ Moved | Destination$ Battlefield | ValidCard$ Card.Self |"
+ " | Description$ Amplify " + amplifyMagnitude + " ("
+ " | ReplacementResult$ Updated | Description$ Amplify " + amplifyMagnitude + " ("
+ inst.getReminderText() + ")";
final String abString = "DB$ Reveal | AnyNumber$ True | RevealValid$ "
@@ -2075,7 +2072,6 @@ public class CardFactoryUtil {
AbilitySub saCleanup = (AbilitySub) AbilityFactory.getAbility(dbClean, card);
saPut.setSubAbility(saCleanup);
setupETBReplacementAbility(saCleanup);
saReveal.setSubAbility(saPut);
@@ -2168,13 +2164,12 @@ public class CardFactoryUtil {
inst.addReplacement(re);
} else if (keyword.equals("Daybound")) {
final String actualRep = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | DayTime$ Night | Secondary$ True | Layer$ Transform | Description$ If it is night, this permanent enters the battlefield transformed.";
final String actualRep = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | DayTime$ Night | Secondary$ True | Layer$ Transform | ReplacementResult$ Updated | Description$ If it is night, this permanent enters the battlefield transformed.";
final String abTransform = "DB$ SetState | Defined$ ReplacedCard | Mode$ Transform | ETB$ True | Daybound$ True";
ReplacementEffect re = ReplacementHandler.parseReplacement(actualRep, host, intrinsic, card);
SpellAbility saTransform = AbilityFactory.getAbility(abTransform, card);
setupETBReplacementAbility(saTransform);
re.setOverridingAbility(saTransform);
inst.addReplacement(re);
@@ -2206,11 +2201,10 @@ public class CardFactoryUtil {
AbilitySub cleanupSA = (AbilitySub) AbilityFactory.getAbility(cleanupStr, card);
counterSA.setSubAbility(cleanupSA);
String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | Secondary$ True | Description$ Devour " + magnitude + " ("+ inst.getReminderText() + ")";
String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | Secondary$ True | ReplacementResult$ Updated | Description$ Devour " + magnitude + " ("+ inst.getReminderText() + ")";
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, host, intrinsic, card);
setupETBReplacementAbility(cleanupSA);
re.setOverridingAbility(sacrificeSA);
inst.addReplacement(re);
@@ -2329,12 +2323,11 @@ public class CardFactoryUtil {
+ " | Origin$ Stack | Destination$ Graveyard | Fizzle$ False "
+ " | Description$ Rebound (" + inst.getReminderText() + ")";
String abExile = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile";
String abExile = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile | RememberChanged$ True";
String delTrig = "DB$ DelayedTrigger | Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You " +
" | OptionalDecider$ You | RememberObjects$ ReplacedCard | TriggerDescription$"
" | OptionalDecider$ You | RememberObjects$ Remembered | TriggerDescription$"
+ " At the beginning of your next upkeep, you may cast " + card.toString() + " without paying its mana cost.";
// TODO add check for still in exile
String abPlay = "DB$ Play | Defined$ DelayTriggerRemembered | WithoutManaCost$ True | Optional$ True";
String abPlay = "DB$ Play | Defined$ DelayTriggerRememberedLKI | WithoutManaCost$ True | Optional$ True";
SpellAbility saExile = AbilityFactory.getAbility(abExile, card);
@@ -2686,7 +2679,11 @@ public class CardFactoryUtil {
final String[] k = keyword.split(":");
final Cost blitzCost = new Cost(k[1], false);
final SpellAbility newSA = card.getFirstSpellAbility().copyWithDefinedCost(blitzCost);
final SpellAbility newSA = card.getFirstSpellAbility().copyWithManaCostReplaced(host.getController(), blitzCost);
if (k.length > 2) {
newSA.getMapParams().put("ValidAfterStack", k[2]);
}
final StringBuilder desc = new StringBuilder();
desc.append("Blitz ").append(blitzCost.toSimpleString()).append(" (");
@@ -2797,7 +2794,18 @@ public class CardFactoryUtil {
abilityStr.append(equipCost);
abilityStr.append("| ValidTgts$ ").append(valid);
abilityStr.append(" | TgtPrompt$ Select target ").append(vstr).append(" you control ");
abilityStr.append("| SorcerySpeed$ True | Equip$ True | AILogic$ Pump | IsPresent$ Equipment.Self+nonCreature ");
if (card.hasKeyword(Keyword.RECONFIGURE)) {
/*
* 301.5c An Equipment thats also a creature cant equip a creature unless that Equipment has reconfigure (see rule 702.151, “Reconfigure”).
* An Equipment that loses the subtype “Equipment” cant equip a creature. An Equipment cant equip itself. An Equipment that equips an illegal
* or nonexistent permanent becomes unattached from that permanent but remains on the battlefield. (This is a state-based action. See rule 704.)
* An Equipment cant equip more than one creature. If a spell or ability would cause an Equipment to equip more than one creature,
* the Equipments controller chooses which creature it equips.
*/
abilityStr.append("| SorcerySpeed$ True | Equip$ True | AILogic$ Pump | IsPresent$ Equipment.Self ");
} else {
abilityStr.append("| SorcerySpeed$ True | Equip$ True | AILogic$ Pump | IsPresent$ Equipment.Self+nonCreature ");
}
// add AttachAi for some special cards
if (card.hasSVar("AttachAi")) {
abilityStr.append("| ").append(card.getSVar("AttachAi"));
@@ -3440,9 +3448,21 @@ public class CardFactoryUtil {
st.setSVar("AffinityX", "Count$Valid " + t + ".YouCtrl");
inst.addStaticAbility(st);
} else if (keyword.startsWith("Blitz")) {
String effect = "Mode$ Continuous | Affected$ Card.Self+blitzed | AddKeyword$ Haste | AddTrigger$ Dies";
String trig = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | " +
"Execute$ TrigDraw | TriggerDescription$ When this creature dies, draw a card.";
final String[] k = keyword.split(":");
final String manacost = k[1];
final Cost cost = new Cost(manacost, false);
StringBuilder sb = new StringBuilder("Blitz");
if (!cost.isOnlyManaCost()) {
sb.append("");
} else {
sb.append(" ");
}
sb.append(cost.toSimpleString());
String effect = "Mode$ Continuous | Affected$ Card.Self+blitzed+castKeyword | AddKeyword$ Haste | AddTrigger$ Dies"
+ " | Secondary$ True | Description$ " + sb.toString() + " (" + inst.getReminderText() + ")";
String trig = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self" +
" | Execute$ TrigDraw | Secondary$ True | TriggerDescription$ When this creature dies, draw a card.";
String ab = "DB$ Draw | NumCards$ 1";
StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic);
@@ -3516,7 +3536,7 @@ public class CardFactoryUtil {
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Dash")) {
String effect = "Mode$ Continuous | Affected$ Card.Self+dashed | AddKeyword$ Haste";
String effect = "Mode$ Continuous | Affected$ Card.Self+dashed+castKeyword | AddKeyword$ Haste";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Daybound")) {
String effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Daybound | Secondary$ True | Description$ This permanent can't be transformed except by its daybound ability.";
@@ -3693,14 +3713,6 @@ public class CardFactoryUtil {
return altCostSA;
}
private static final Map<String,String> emptyMap = Maps.newTreeMap();
public static SpellAbility setupETBReplacementAbility(SpellAbility sa) {
AbilitySub as = new AbilitySub(ApiType.InternalEtbReplacement, sa.getHostCard(), null, emptyMap);
sa.appendSubAbility(as);
return as;
// ETBReplacementMove(sa.getHostCard(), null));
}
public static void setupAdventureAbility(Card card) {
if (card.getCurrentStateName() != CardStateName.Adventure) {
return;

View File

@@ -13,6 +13,7 @@ import forge.game.*;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.AttackRequirement;
import forge.game.combat.AttackingBand;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
@@ -34,6 +35,7 @@ import org.apache.commons.lang3.tuple.Pair;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public class CardProperty {
@@ -109,8 +111,13 @@ public class CardProperty {
if (card.getId() != Integer.parseInt(property.split("CardUID_")[1])) {
return false;
}
} else if (property.equals("ChosenCard")) {
if (!source.hasChosenCard(card)) {
} else if (property.startsWith("ChosenCard")) {
CardCollectionView chosen = source.getChosenCards();
int i = chosen.indexOf(card);
if (i == -1) {
return false;
}
if (property.contains("Strict") && !chosen.get(i).equalsWithTimestamp(card)) {
return false;
}
} else if (property.equals("nonChosenCard")) {
@@ -250,21 +257,6 @@ public class CardProperty {
if (!AbilityUtils.getDefinedPlayers(source, "TargetedPlayer", spellAbility).contains(controller)) {
return false;
}
} else if (property.equals("TargetedControllerCtrl")) {
final CardCollectionView cards = AbilityUtils.getDefinedCards(source, "Targeted", spellAbility);
final List<SpellAbility> sas = AbilityUtils.getDefinedSpellAbilities(source, "Targeted", spellAbility);
for (final Card c : cards) {
final Player p = c.getController();
if (!controller.equals(p)) {
return false;
}
}
for (final SpellAbility s : sas) {
final Player p = s.getHostCard().getController();
if (!controller.equals(p)) {
return false;
}
}
} else if (property.startsWith("ActivePlayerCtrl")) {
if (!game.getPhaseHandler().isPlayerTurn(controller)) {
return false;
@@ -618,35 +610,6 @@ public class CardProperty {
if ((card.getCloneOrigin() == null) || !card.getCloneOrigin().equals(source)) {
return false;
}
} else if (property.startsWith("DamagedBy")) {
List<Card> damaged = Lists.newArrayList();
for (Pair<Card, Integer> pair : card.getReceivedDamageFromThisTurn()) {
damaged.add(pair.getLeft());
}
if (property.endsWith("Source") || property.equals("DamagedBy")) {
if (!damaged.contains(source)) {
return false;
}
} else {
String prop = property.substring("DamagedBy".length());
boolean found = Iterables.any(damaged, CardPredicates.restriction(prop, sourceController, source, spellAbility));
if (!found) {
for (Card d : AbilityUtils.getDefinedCards(source, prop, spellAbility)) {
if (damaged.contains(d)) {
found = true;
break;
}
}
}
if (!found) {
return false;
}
}
} else if (property.startsWith("Damaged")) {
if (!card.getDamageHistory().getThisTurnDamaged().containsKey(source)) {
return false;
}
} else if (property.startsWith("SharesCMCWith")) {
if (property.equals("SharesCMCWith")) {
if (!card.sharesCMCWith(source)) {
@@ -1143,38 +1106,87 @@ public class CardProperty {
&& !card.getDamageHistory().hasBlockedSinceLastUpkeepOf(sourceController)) {
return false;
}
} else if (property.startsWith("DamagedBy")) {
String prop = property.substring("DamagedBy".length());
CardCollection def = null;
if (prop.startsWith(" ")) {
def = AbilityUtils.getDefinedCards(source, prop.substring(1), spellAbility);
}
boolean found = false;
for (Pair<Integer, Boolean> p : card.getDamageReceivedThisTurn()) {
Card dmgSource = game.getDamageLKI(p).getLeft();
if (def != null) {
for (Card c : def) {
if (dmgSource.equalsWithTimestamp(c)) {
found = true;
}
}
}
else if (prop.isEmpty() && dmgSource.equalsWithTimestamp(source)) {
found = true;
} else if (dmgSource.isValid(prop.split(","), sourceController, source, spellAbility)) {
found = true;
}
if (found) {
break;
}
}
if (!found) {
return false;
}
} else if (property.startsWith("Damaged")) {
for (Pair<Integer, Boolean> p : source.getDamageReceivedThisTurn()) {
boolean found = false;
if (game.getDamageLKI(p).getLeft().equalsWithTimestamp(card)) {
found = true;
break;
}
if (!found) {
return false;
}
}
} else if (property.startsWith("dealtCombatDamageThisCombat")) {
if (card.getDamageHistory().getThisCombatDamaged().isEmpty()) {
return false;
}
} else if (property.startsWith("dealtDamageToYouThisTurn")) {
if (!card.getDamageHistory().getThisTurnDamaged().containsKey(sourceController)) {
if (card.getDamageHistory().getDamageDoneThisTurn(null, true, null, "You", card, sourceController, spellAbility) == 0) {
return false;
}
} else if (property.startsWith("dealtDamageToOppThisTurn")) {
if (!card.hasDealtDamageToOpponentThisTurn()) {
return false;
}
//dealtCombatDamageThisCombat <valid>, dealtCombatDamageThisTurn <valid>, and notDealt versions
} else if (property.startsWith("dealtCombatDamage") || property.startsWith("notDealtCombatDamage")) {
final String[] v = property.split(" ")[1].split(",");
final Iterable<GameEntity> list = property.contains("ThisCombat") ?
card.getDamageHistory().getThisCombatDamaged().keySet() :
card.getDamageHistory().getThisTurnCombatDamaged().keySet();
boolean found = Iterables.any(list, GameObjectPredicates.restriction(v, sourceController, source, spellAbility));
final String v = property.split(" ")[1];
boolean found = card.getDamageHistory().getDamageDoneThisTurn(true, true, null, v, card, sourceController, spellAbility) > 0;
if (found == property.startsWith("not")) {
return false;
}
} else if (property.startsWith("controllerWasDealtCombatDamageByThisTurn")) {
if (!source.getDamageHistory().getThisTurnCombatDamaged().containsKey(controller)) {
if (source.getDamageHistory().getDamageDoneThisTurn(true, true, null, "You", card, controller, spellAbility) == 0) {
return false;
}
} else if (property.startsWith("controllerWasDealtDamageByThisTurn")) {
if (!source.getDamageHistory().getThisTurnDamaged().containsKey(controller)) {
if (source.getDamageHistory().getDamageDoneThisTurn(null, true, null, "You", card, controller, spellAbility) == 0) {
return false;
}
} else if (property.startsWith("wasDealtDamageThisTurn")) {
if (card.getReceivedDamageFromPlayerThisTurn().isEmpty()) {
if (card.getAssignedDamage() == 0) {
return false;
}
} else if (property.equals("wasDealtNonCombatDamageThisTurn")) {
if (!card.getDamageHistory().hasBeenDealtNonCombatDamageThisTurn()) {
if (card.getAssignedDamage(false, null) == 0) {
return false;
}
} else if (property.startsWith("wasDealtDamageByThisGame")) {
int idx = source.getDamageHistory().getThisGameDamaged().indexOf(card);
if (idx == -1) {
return false;
}
Card c = (Card) source.getDamageHistory().getThisGameDamaged().get(idx);
if (!c.equalsWithTimestamp(game.getCardState(card))) {
return false;
}
} else if (property.startsWith("dealtDamageThisTurn")) {
@@ -1343,6 +1355,10 @@ public class CardProperty {
if (!card.isToken() && !card.isTokenCard()) {
return false;
}
// copied spell don't count
if (property.contains("Created") && card.getCastSA() != null) {
return false;
}
} else if (property.startsWith("nonToken")) {
if (card.isToken() || card.isTokenCard()) {
return false;
@@ -1674,6 +1690,11 @@ public class CardProperty {
if (band == null || !band.getAttackers().contains(card)) {
return false;
}
} else if (property.equals("hadToAttackThisCombat")) {
AttackRequirement e = game.getCombat().getAttackConstraints().getRequirements().get(card);
if (e == null || !e.hasCreatureRequirement() || !e.getAttacker().equalsWithTimestamp(card)) {
return false;
}
} else if (property.equals("couldAttackButNotAttacking")) {
if (!game.getPhaseHandler().isPlayerTurn(controller)) return false;
return CombatUtil.couldAttackButNotAttacking(combat, card);
@@ -1794,6 +1815,21 @@ public class CardProperty {
if (card.hasCounters()) {
return false;
}
} else if (property.equals("castKeyword")) {
SpellAbility castSA = card.getCastSA();
if (castSA == null) {
return false;
}
// intrinsic keyword might be a new one when the zone changes
if (castSA.isIntrinsic()) {
// so just check if the static is intrinsic too
if (!spellAbility.isIntrinsic()) {
return false;
}
} else {
// otherwise check for keyword object
return Objects.equals(castSA.getKeyword(), spellAbility.getKeyword());
}
} else if (property.startsWith("CastSa")) {
SpellAbility castSA = card.getCastSA();
if (castSA == null) {

View File

@@ -250,9 +250,8 @@ public final class CardUtil {
newCopy.setColor(in.getColor().getColor());
newCopy.setPhasedOut(in.isPhasedOut());
newCopy.setReceivedDamageFromThisTurn(in.getReceivedDamageFromThisTurn());
newCopy.setReceivedDamageFromPlayerThisTurn(in.getReceivedDamageFromPlayerThisTurn());
newCopy.setDamageHistory(in.getDamageHistory());
newCopy.setDamageReceivedThisTurn(in.getDamageReceivedThisTurn());
for (Card c : in.getBlockedThisTurn()) {
newCopy.addBlockedThisTurn(c);
}

View File

@@ -160,7 +160,7 @@ public class AttackConstraints {
}
}
}
myPossibleAttackers.removeAll((Iterable<Card>) attackersToRemove);
myPossibleAttackers.removeAll(attackersToRemove);
for (final Card toRemove : attackersToRemove) {
reqs.removeAll(findAll(reqs, toRemove));
}
@@ -186,8 +186,9 @@ public class AttackConstraints {
// Now try all others (plus empty attack) and count their violations
final FCollection<Map<Card, GameEntity>> legalAttackers = collectLegalAttackers(reqs, myMax);
possible.putAll(Maps.asMap(legalAttackers.asSet(), FN_COUNT_VIOLATIONS));
if (countViolations(Collections.emptyMap()) != -1) {
possible.put(Collections.emptyMap(), countViolations(Collections.emptyMap()));
int empty = countViolations(Collections.emptyMap());
if (empty != -1) {
possible.put(Collections.emptyMap(), empty);
}
// take the case with the fewest violations
@@ -392,7 +393,7 @@ public class AttackConstraints {
* restriction is violated.
*/
public final int countViolations(final Map<Card, GameEntity> attackers) {
if (!globalRestrictions.isLegal(attackers, possibleAttackers)) {
if (!globalRestrictions.isLegal(attackers)) {
return -1;
}
for (final Entry<Card, GameEntity> attacker : attackers.entrySet()) {

View File

@@ -107,8 +107,17 @@ public class AttackRequirement {
}
}
public Card getAttacker() {
return attacker;
}
public boolean hasRequirement() {
return !defenderSpecific.isEmpty() || !causesToAttack.isEmpty() || !defenderOrPWSpecific.isEmpty();
return defenderSpecific.countAll() > 0 || causesToAttack.countAll() > 0 || defenderOrPWSpecific.countAll() > 0;
}
// according to Firkraag ruling Trove of Temptation applies to players, not creatures
public boolean hasCreatureRequirement() {
return defenderSpecific.countAll() > 0 || causesToAttack.countAll() > 0;
}
public final MapToAmount<Card> getCausesToAttack() {
@@ -124,29 +133,24 @@ public class AttackRequirement {
int violations = 0;
// first. check to see if "must attack X or Y with at least one creature" requirements are satisfied
//List<GameEntity> toRemoveFromDefSpecific = Lists.newArrayList();
if (!defenderOrPWSpecific.isEmpty()) {
for (GameEntity def : defenderOrPWSpecific.keySet()) {
if (defenderSpecificAlternatives.containsKey(def)) {
boolean isAttackingDefender = false;
outer: for (Card atk : attackers.keySet()) {
// is anyone attacking this defender or any of the alternative defenders?
if (attackers.get(atk).equals(def)) {
isAttackingDefender = true;
break;
}
for (GameEntity altDef : defenderSpecificAlternatives.get(def)) {
if (attackers.get(atk).equals(altDef)) {
isAttackingDefender = true;
break outer;
}
}
}
if (!isAttackingDefender && CombatUtil.getAttackCost(attacker.getGame(), attacker, def) == null) {
violations++; // no one is attacking that defender or any of his PWs
for (GameEntity def : defenderOrPWSpecific.keySet()) {
boolean isAttackingDefender = false;
outer: for (Card atk : attackers.keySet()) {
// is anyone attacking this defender or any of the alternative defenders?
if (attackers.get(atk).equals(def)) {
isAttackingDefender = true;
break;
}
for (GameEntity altDef : defenderSpecificAlternatives.get(def)) {
if (attackers.get(atk).equals(altDef)) {
isAttackingDefender = true;
break outer;
}
}
}
if (!isAttackingDefender) {
violations++; // no one is attacking that defender or any of his PWs
}
}
// now, count everything else

View File

@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Function;
@@ -200,6 +201,19 @@ public class Combat {
return attackableEntries;
}
//gets attacked player opponents (ignores planeswalkers)
public final FCollection<Player> getAttackedOpponents(Player atk) {
FCollection<Player> attackedOpps = new FCollection<Player>();
if (atk == playerWhoAttacks) {
for (Player defender : getDefendingPlayers()) {
if (!getAttackersOf(defender).isEmpty()) {
attackedOpps.add(defender);
}
}
}
return attackedOpps;
}
public final FCollection<GameEntity> getDefendersControlledBy(Player who) {
FCollection<GameEntity> res = new FCollection<>();
for (GameEntity ge : attackableEntries) {
@@ -711,7 +725,7 @@ public class Combat {
"defending player and/or any number of creatures they control.")
&& blocker.getController().getController().confirmAction(null, null,
Localizer.getInstance().getMessage("lblAssignCombatDamageAsChoose",
CardTranslation.getTranslatedName(blocker.getName())));
CardTranslation.getTranslatedName(blocker.getName())), null);
// choose defending player
if (divideCombatDamageAsChoose) {
defender = blocker.getController().getController().chooseSingleEntityForEffect(attackingPlayer.getOpponents(), null, Localizer.getInstance().getMessage("lblChoosePlayer"), null);
@@ -784,13 +798,14 @@ public class Combat {
final SpellAbility emptySA = new SpellAbility.EmptySa(attacker);
boolean assignToPlayer = false;
if (attacker.hasKeyword("CARDNAME assigns its combat damage as though it weren't blocked.")) {
if (StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker, false)) {
assignToPlayer = true;
}
if (!assignToPlayer && attacker.getGame().getCombat().isBlocked(attacker) && attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
if (!assignToPlayer && attacker.getGame().getCombat().isBlocked(attacker)
&& StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) {
assignToPlayer = assigningPlayer.getController().confirmAction(emptySA, PlayerActionConfirmMode.AlternativeDamageAssignment,
Localizer.getInstance().getMessage("lblAssignCombatDamageWerentBlocked",
CardTranslation.getTranslatedName(attacker.getName())));
CardTranslation.getTranslatedName(attacker.getName())), null);
}
boolean divideCombatDamageAsChoose = false;
@@ -802,7 +817,7 @@ public class Combat {
"defending player and/or any number of creatures they control.")
&& assigningPlayer.getController().confirmAction(emptySA, PlayerActionConfirmMode.AlternativeDamageAssignment,
Localizer.getInstance().getMessage("lblAssignCombatDamageAsChoose",
CardTranslation.getTranslatedName(attacker.getName())));
CardTranslation.getTranslatedName(attacker.getName())), null);
if (defender instanceof Card && divideCombatDamageAsChoose) {
defender = getDefenderPlayerByAttacker(attacker);
}
@@ -813,7 +828,7 @@ public class Combat {
"a creature defending player controls.") &&
assigningPlayer.getController().confirmAction(emptySA, PlayerActionConfirmMode.AlternativeDamageAssignment,
Localizer.getInstance().getMessage("lblAssignCombatDamageToCreature",
CardTranslation.getTranslatedName(attacker.getName())));
CardTranslation.getTranslatedName(attacker.getName())), null);
if (divideCombatDamageAsChoose) {
if (orderedBlockers == null || orderedBlockers.isEmpty()) {
orderedBlockers = getDefendersCreatures();

View File

@@ -408,7 +408,7 @@ public class CombatUtil {
c.getDamageHistory().setCreatureAttackedThisCombat(defender);
c.getDamageHistory().clearNotAttackedSinceLastUpkeepOf();
c.getController().addCreaturesAttackedThisTurn(CardUtil.getLKICopy(c));
if (combat.getDefenderByAttacker(c) instanceof Player) {
if (defender instanceof Player) {
c.getController().addAttackedPlayersMyTurn(combat.getDefenderPlayerByAttacker(c));
}
}

View File

@@ -9,9 +9,7 @@ import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
import forge.util.maps.LinkedHashMapToAmount;
@@ -22,11 +20,9 @@ public class GlobalAttackRestrictions {
private final int max;
private final MapToAmount<GameEntity> defenderMax;
private final PlayerCollection mustBeAttackedByEachOpp;
private GlobalAttackRestrictions(final int max, final MapToAmount<GameEntity> defenderMax, PlayerCollection mustBeAttackedByEachOpp) {
private GlobalAttackRestrictions(final int max, final MapToAmount<GameEntity> defenderMax) {
this.max = max;
this.defenderMax = defenderMax;
this.mustBeAttackedByEachOpp = mustBeAttackedByEachOpp;
}
public int getMax() {
@@ -36,17 +32,17 @@ public class GlobalAttackRestrictions {
return defenderMax;
}
public boolean isLegal(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) {
return !getViolations(attackers, possibleAttackers, true).isViolated();
public boolean isLegal(final Map<Card, GameEntity> attackers) {
return !getViolations(attackers, true).isViolated();
}
public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) {
return getViolations(attackers, possibleAttackers, false);
public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers) {
return getViolations(attackers, false);
}
private GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers, final boolean returnQuickly) {
private GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final boolean returnQuickly) {
final int nTooMany = max < 0 ? 0 : attackers.size() - max;
if (returnQuickly && nTooMany > 0) {
return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.emptyMap(), MapToAmountUtil.emptyMap());
return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.emptyMap());
}
final MapToAmount<GameEntity> defenderTooMany = new LinkedHashMapToAmount<>(defenderMax.size());
@@ -77,49 +73,18 @@ public class GlobalAttackRestrictions {
}
}
final MapToAmount<GameEntity> defenderTooFew = new LinkedHashMapToAmount<>(defenderMax.size());
for (final GameEntity mandatoryDef : mustBeAttackedByEachOpp) {
// check to ensure that this defender can even legally be attacked in the first place
boolean canAttackThisDef = false;
for (Card c : possibleAttackers) {
if (CombatUtil.canAttack(c, mandatoryDef) && null == CombatUtil.getAttackCost(c.getGame(), c, mandatoryDef)) {
canAttackThisDef = true;
break;
}
}
if (!canAttackThisDef) {
continue;
}
boolean isAttacked = false;
for (final GameEntity defender : attackers.values()) {
if (defender.equals(mandatoryDef)) {
isAttacked = true;
break;
} else if (defender instanceof Card && ((Card)defender).getController().equals(mandatoryDef)) {
isAttacked = true;
break;
}
}
if (!isAttacked) {
defenderTooFew.add(mandatoryDef);
}
}
return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany, defenderTooFew);
return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany);
}
final class GlobalAttackRestrictionViolations {
private final boolean isViolated;
private final int globalTooMany;
private final MapToAmount<GameEntity> defenderTooMany;
private final MapToAmount<GameEntity> defenderTooFew;
public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount<GameEntity> defenderTooMany, final MapToAmount<GameEntity> defenderTooFew) {
this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty() || !defenderTooFew.isEmpty();
public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount<GameEntity> defenderTooMany) {
this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty();
this.globalTooMany = globalTooMany;
this.defenderTooMany = defenderTooMany;
this.defenderTooFew = defenderTooFew;
}
public boolean isViolated() {
return isViolated;
@@ -127,9 +92,6 @@ public class GlobalAttackRestrictions {
public int getGlobalTooMany() {
return globalTooMany;
}
public MapToAmount<GameEntity> getDefenderTooFew() {
return defenderTooFew;
}
public MapToAmount<GameEntity> getDefenderTooMany() {
return defenderTooMany;
}
@@ -147,7 +109,6 @@ public class GlobalAttackRestrictions {
public static GlobalAttackRestrictions getGlobalRestrictions(final Player attackingPlayer, final FCollectionView<GameEntity> possibleDefenders) {
int max = -1;
final MapToAmount<GameEntity> defenderMax = new LinkedHashMapToAmount<>(possibleDefenders.size());
final PlayerCollection mustBeAttacked = new PlayerCollection();
final Game game = attackingPlayer.getGame();
/* if (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.onlyOneAttackerATurn)) {
@@ -171,14 +132,6 @@ public class GlobalAttackRestrictions {
}
}
for (final Card card : game.getCardsIn(ZoneType.Battlefield)) {
if (card.hasKeyword("Each opponent must attack you or a planeswalker you control with at least one creature each combat if able.")) {
if (attackingPlayer.isOpponentOf(card.getController())) {
mustBeAttacked.add(card.getController());
}
}
}
for (final GameEntity defender : possibleDefenders) {
final int defMax = getMaxAttackTo(defender);
if (defMax != -1) {
@@ -190,7 +143,7 @@ public class GlobalAttackRestrictions {
max = Ints.min(max, defenderMax.countAll());
}
return new GlobalAttackRestrictions(max, defenderMax, mustBeAttacked);
return new GlobalAttackRestrictions(max, defenderMax);
}
/**

View File

@@ -20,6 +20,7 @@ package forge.game.cost;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityActivateAbilityAsIfHaste;
/**
* The Class CostTap.
@@ -61,7 +62,7 @@ public class CostTap extends CostPart {
@Override
public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) {
final Card source = ability.getHostCard();
return source.isUntapped() && (!source.isSick() || source.hasKeyword("CARDNAME may activate abilities as though it has haste."));
return source.isUntapped() && !isAbilitySick(source);
}
@Override
@@ -74,4 +75,10 @@ public class CostTap extends CostPart {
return visitor.visit(this);
}
public boolean isAbilitySick(final Card source) {
if (!source.isSick()) {
return false;
}
return !StaticAbilityActivateAbilityAsIfHaste.canActivate(source);
}
}

View File

@@ -20,6 +20,7 @@ package forge.game.cost;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityActivateAbilityAsIfHaste;
/**
* The Class CostUntap.
@@ -72,7 +73,7 @@ public class CostUntap extends CostPart {
@Override
public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) {
final Card source = ability.getHostCard();
return source.isTapped() && (!source.isSick() || source.hasKeyword("CARDNAME may activate abilities as though it has haste."));
return source.isTapped() && !isAbilitySick(source);
}
@Override
@@ -85,4 +86,10 @@ public class CostUntap extends CostPart {
return visitor.visit(this);
}
public boolean isAbilitySick(final Card source) {
if (!source.isSick()) {
return false;
}
return !StaticAbilityActivateAbilityAsIfHaste.canActivate(source);
}
}

View File

@@ -39,6 +39,7 @@ public enum Keyword {
CASUALTY("Casualty", KeywordWithAmount.class, false, "As you cast this spell, you may sacrifice a creature with power %1$d or greater. When you do, copy this spell."),
CHAMPION("Champion", KeywordWithType.class, false, "When this enters the battlefield, sacrifice it unless you exile another %s you control. When this leaves the battlefield, that card returns to the battlefield."),
CHANGELING("Changeling", SimpleKeyword.class, true, "This card is every creature type."),
CHOOSE_A_BACKGROUND("Choose a Background", Partner.class, true, "You can have a Background as a second commander."),
CIPHER("Cipher", SimpleKeyword.class, true, "Then you may exile this spell card encoded on a creature you control. Whenever that creature deals combat damage to a player, its controller may cast a copy of the encoded card without paying its mana cost."),
COMPANION("Companion", Companion.class, true, "Reveal your companion from outside the game if your deck meets the companion restriction."),
COMPLEATED("Compleated", SimpleKeyword.class, true, "{$0} can be paid with {$1}, {$2}, or 2 life. If life was paid, this planeswalker enters with two fewer loyalty counters."),

View File

@@ -22,6 +22,7 @@ import io.sentry.Sentry;
public abstract class KeywordInstance<T extends KeywordInstance<?>> implements KeywordInterface {
private Keyword keyword;
private String original;
private long staticId = 0;
private List<Trigger> triggers = Lists.newArrayList();
private List<ReplacementEffect> replacements = Lists.newArrayList();
@@ -181,6 +182,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
* @see forge.game.keyword.KeywordInterface#addTrigger(forge.game.trigger.Trigger)
*/
public final void addTrigger(final Trigger trg) {
trg.setKeyword(this);
triggers.add(trg);
}
@@ -189,6 +191,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
* @see forge.game.keyword.KeywordInterface#addReplacement(forge.game.replacement.ReplacementEffect)
*/
public final void addReplacement(final ReplacementEffect trg) {
trg.setKeyword(this);
replacements.add(trg);
}
@@ -197,6 +200,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
* @see forge.game.keyword.KeywordInterface#addSpellAbility(forge.game.spellability.SpellAbility)
*/
public final void addSpellAbility(final SpellAbility s) {
s.setKeyword(this);
abilities.add(s);
}
@@ -205,6 +209,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
* @see forge.game.keyword.KeywordInterface#addStaticAbility(forge.game.staticability.StaticAbility)
*/
public final void addStaticAbility(final StaticAbility st) {
st.setKeyword(this);
staticAbilities.add(st);
}
@@ -247,22 +252,30 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
result.abilities = Lists.newArrayList();
for (SpellAbility sa : this.abilities) {
result.abilities.add(sa.copy(host, lki));
SpellAbility copy = sa.copy(host, lki);
copy.setKeyword(result);
result.abilities.add(copy);
}
result.triggers = Lists.newArrayList();
for (Trigger tr : this.triggers) {
result.triggers.add(tr.copy(host, lki));
Trigger copy = tr.copy(host, lki);
copy.setKeyword(result);
result.triggers.add(copy);
}
result.replacements = Lists.newArrayList();
for (ReplacementEffect re : this.replacements) {
result.replacements.add(re.copy(host, lki));
ReplacementEffect copy = re.copy(host, lki);
copy.setKeyword(result);
result.replacements.add(copy);
}
result.staticAbilities = Lists.newArrayList();
for (StaticAbility sa : this.staticAbilities) {
result.staticAbilities.add(sa.copy(host, lki));
StaticAbility copy = sa.copy(host, lki);
copy.setKeyword(result);
result.staticAbilities.add(copy);
}
return result;
@@ -327,4 +340,11 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
sa.setIntrinsic(value);
}
}
public long getStaticId() {
return this.staticId;
}
public void setStaticId(long v) {
this.staticId = v;
}
}

View File

@@ -18,6 +18,8 @@ public interface KeywordInterface extends Cloneable {
String getReminderText();
int getAmount();
long getStaticId();
void setStaticId(long v);
void createTraits(final Card host, final boolean intrinsic);
void createTraits(final Card host, final boolean intrinsic, final boolean clear);

View File

@@ -641,6 +641,7 @@ public class PhaseHandler implements java.io.Serializable {
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
}
playerTurn.clearAttackedPlayersMyCombat();
for (final Card c : combat.getAttackers()) {
CombatUtil.checkDeclaredAttacker(game, c, combat, true);
}
@@ -1063,7 +1064,7 @@ public class PhaseHandler implements java.io.Serializable {
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
final CardZoneTable triggerList = new CardZoneTable();
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);
triggerList.triggerChangesZoneAll(game, null);
triggerList.triggerChangesZoneAll(game, sa);
}
}

View File

@@ -98,6 +98,7 @@ import forge.game.keyword.KeywordsChange;
import forge.game.mana.ManaPool;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
@@ -146,8 +147,6 @@ public class Player extends GameEntity implements Comparable<Player> {
private int life = 20;
private int startingLife = 20;
private int lifeStartedThisTurnWith = startingLife;
private final Map<Card, Integer> assignedDamage = Maps.newHashMap();
private final Map<Card, Integer> assignedCombatDamage = Maps.newHashMap();
private int spellsCastThisTurn;
private int spellsCastThisGame;
private int spellsCastLastTurn;
@@ -206,6 +205,8 @@ public class Player extends GameEntity implements Comparable<Player> {
private List<Card> creatureAttackedThisTurn = new ArrayList<>();
private List<Player> attackedPlayersThisTurn = new ArrayList<>();
private List <Player> attackedPlayersLastTurn = new ArrayList<>();
private List<Player> attackedPlayersThisCombat = new ArrayList<>();
private boolean activateLoyaltyAbilityThisTurn = false;
private boolean tappedLandForManaThisTurn = false;
private List<Card> completedDungeons = new ArrayList<>();
@@ -216,7 +217,6 @@ public class Player extends GameEntity implements Comparable<Player> {
private Map<Card, Card> maingameCardsMap = Maps.newHashMap();
private CardCollection currentPlanes = new CardCollection();
private Set<String> prowl = Sets.newHashSet();
private PlayerStatistics stats = new PlayerStatistics();
private PlayerController controller;
@@ -238,6 +238,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private Deque<SpellAbility> paidForStack = new ArrayDeque<>();
private Card monarchEffect;
private Card initiativeEffect;
private Card blessingEffect;
private Card keywordEffect;
@@ -700,19 +701,6 @@ public class Player extends GameEntity implements Comparable<Player> {
}
}
int old = assignedDamage.containsKey(source) ? assignedDamage.get(source) : 0;
assignedDamage.put(source, old + amount);
source.getDamageHistory().registerDamage(this, amount);
if (isCombat) {
old = assignedCombatDamage.containsKey(source) ? assignedCombatDamage.get(source) : 0;
assignedCombatDamage.put(source, old + amount);
for (final String type : source.getType().getCreatureTypes()) {
source.getController().addProwlType(type);
}
source.getDamageHistory().registerCombatDamage(this, amount);
}
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.DamageSource, source);
@@ -826,49 +814,6 @@ public class Player extends GameEntity implements Comparable<Player> {
simultaneousDamage = 0;
}
public final void clearAssignedDamage() {
assignedDamage.clear();
assignedCombatDamage.clear();
}
public final int getAssignedDamage() {
int num = 0;
for (final Integer value : assignedDamage.values()) {
num += value;
}
return num;
}
public final int getAssignedCombatDamage() {
int num = 0;
for (final Integer value : assignedCombatDamage.values()) {
num += value;
}
return num;
}
public final Iterable<Card> getAssignedDamageSources() {
return assignedDamage.keySet();
}
public final int getAssignedDamage(final Card c) {
return assignedDamage.get(c);
}
public final int getAssignedDamage(final String type) {
final Map<Card, Integer> valueMap = Maps.newHashMap();
for (final Card c : assignedDamage.keySet()) {
if (c.getType().hasStringType(type)) {
valueMap.put(c, assignedDamage.get(c));
}
}
int num = 0;
for (final Integer value : valueMap.values()) {
num += value;
}
return num;
}
/**
* Get the total damage assigned to this player's opponents this turn.
*/
@@ -1886,6 +1831,7 @@ public class Player extends GameEntity implements Comparable<Player> {
public final void addAttackedPlayersMyTurn(final Player p) {
if (!attackedPlayersThisTurn.contains(p)) {
attackedPlayersThisCombat.add(p);
attackedPlayersThisTurn.add(p);
}
}
@@ -1903,6 +1849,13 @@ public class Player extends GameEntity implements Comparable<Player> {
attackedPlayersLastTurn.addAll(players);
}
public final List<Player> getAttackedPlayersMyCombat() {
return attackedPlayersThisTurn;
}
public final void clearAttackedPlayersMyCombat() {
attackedPlayersThisCombat.clear();
}
public final int getVenturedThisTurn() {
return venturedThisTurn;
}
@@ -2061,27 +2014,27 @@ public class Player extends GameEntity implements Comparable<Player> {
return Iterables.any(getZone(ZoneType.Battlefield).getCardsAddedThisTurn(null), CardPredicates.Presets.LANDS);
}
public boolean hasFerocious() {
return !CardLists.filterPower(getCreaturesInPlay(), 4).isEmpty();
}
public final boolean hasSurge() {
return !CardLists.filterControlledBy(game.getStack().getSpellsCastThisTurn(), getYourTeam()).isEmpty();
}
public final boolean hasBloodthirst() {
for (Player p : game.getRegisteredPlayers()) {
if (p.isOpponentOf(this) && p.getAssignedDamage() > 0) {
for (Player p : getRegisteredOpponents()) {
if (p.getAssignedDamage() > 0) {
return true;
}
}
return false;
}
public boolean hasFerocious() {
return !CardLists.filterPower(getCreaturesInPlay(), 4).isEmpty();
}
public final int getBloodthirstAmount() {
return Aggregates.sum(getRegisteredOpponents(), Accessors.FN_GET_ASSIGNED_DAMAGE);
}
public final boolean hasSurge() {
return !CardLists.filterControlledBy(game.getStack().getSpellsCastThisTurn(), getYourTeam()).isEmpty();
}
public final int getOpponentLostLifeThisTurn() {
int lost = 0;
for (Player opp : getRegisteredOpponents()) {
@@ -2090,14 +2043,12 @@ public class Player extends GameEntity implements Comparable<Player> {
return lost;
}
public final boolean hasProwl(final String type) {
return prowl.contains(type);
}
public final void addProwlType(final String type) {
prowl.add(type);
}
public final void resetProwl() {
prowl.clear();
public final boolean hasProwl(final Set<String> types) {
StringBuilder sb = new StringBuilder();
for (String type : types) {
sb.append("Card.YouCtrl+").append(type).append(",");
}
return !game.getDamageDoneThisTurn(true, true, sb.toString(), "Player", null, this, null).isEmpty();
}
public final void setLibrarySearched(final int l) {
@@ -2138,7 +2089,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
} else if (incR[0].equals("EnchantedController")) {
final GameEntity enchanted = source.getEntityAttachedTo();
if ((enchanted == null) || !(enchanted instanceof Card)) {
if (enchanted == null || !(enchanted instanceof Card)) {
return false;
}
final Card enchantedCard = (Card) enchanted;
@@ -2227,10 +2178,6 @@ public class Player extends GameEntity implements Comparable<Player> {
investigatedThisTurn = 0;
}
public final List<Card> getSacrificedThisTurn() {
return sacrificedThisTurn;
}
public final void addSacrificedThisTurn(final Card c, final SpellAbility source) {
// Play the Sacrifice sound
game.fireEvent(new GameEventCardSacrificed());
@@ -2248,6 +2195,9 @@ public class Player extends GameEntity implements Comparable<Player> {
game.getTriggerHandler().runTrigger(TriggerType.Sacrificed, runParams, false);
}
public final List<Card> getSacrificedThisTurn() {
return sacrificedThisTurn;
}
public final void resetSacrificedThisTurn() {
sacrificedThisTurn.clear();
}
@@ -2440,10 +2390,8 @@ public class Player extends GameEntity implements Comparable<Player> {
resetCycledThisTurn();
resetEquippedThisTurn();
resetSacrificedThisTurn();
clearAssignedDamage();
resetVenturedThisTurn();
setRevolt(false);
resetProwl();
setSpellsCastLastTurn(getSpellsCastThisTurn());
resetSpellsCastThisTurn();
setLifeLostLastTurn(getLifeLostThisTurn());
@@ -2455,6 +2403,8 @@ public class Player extends GameEntity implements Comparable<Player> {
setLibrarySearched(0);
setNumManaConversion(0);
damageReceivedThisTurn.clear();
// set last turn nr
if (game.getPhaseHandler().isPlayerTurn(this)) {
setAttackedPlayersMyLastTurn(attackedPlayersThisTurn);
@@ -3021,8 +2971,10 @@ public class Player extends GameEntity implements Comparable<Player> {
String addToHandAbility = "Mode$ Continuous | EffectZone$ Command | Affected$ Card.YouOwn+EffectSource | AffectedZone$ Command | AddAbility$ MoveToHand";
String moveToHand = "ST$ ChangeZone | Cost$ 3 | Defined$ Self | Origin$ Command | Destination$ Hand | SorcerySpeed$ True | ActivationZone$ Command | SpellDescription$ Companion - Put CARDNAME in to your hand";
eff.setSVar("MoveToHand", moveToHand);
eff.addStaticAbility(addToHandAbility);
StaticAbility stAb = StaticAbility.create(addToHandAbility, eff, eff.getCurrentState(), true);
stAb.setSVar("MoveToHand", moveToHand);
eff.addStaticAbility(stAb);
return eff;
}
@@ -3033,11 +2985,13 @@ public class Player extends GameEntity implements Comparable<Player> {
if (game.getRules().hasAppliedVariant(GameType.Oathbreaker) && commander.getRules().canBeSignatureSpell()) {
//signature spells can only reside on the stack or in the command zone
eff.setSVar("SignatureSpellMoveReplacement", "DB$ ChangeZone | Origin$ Stack | Destination$ Command | Defined$ ReplacedCard");
String effStr = "DB$ ChangeZone | Origin$ Stack | Destination$ Command | Defined$ ReplacedCard";
String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | ReplaceWith$ SignatureSpellMoveReplacement | Destination$ Graveyard,Exile,Hand,Library | " +
String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Destination$ Graveyard,Exile,Hand,Library | " +
"Description$ If a signature spell would be put into another zone from the stack, put it into the command zone instead.";
eff.addReplacementEffect(ReplacementHandler.parseReplacement(moved, eff, true));
ReplacementEffect re = ReplacementHandler.parseReplacement(moved, eff, true);
re.setOverridingAbility(AbilityFactory.getAbility(effStr, eff));
eff.addReplacementEffect(re);
//signature spells can only be cast if your oathbreaker is in on the battlefield under your control
String castRestriction = "Mode$ CantBeCast | ValidCard$ Card.EffectSource+YouOwn | EffectZone$ Command | IsPresent$ Card.IsCommander+YouOwn+YouCtrl | PresentZone$ Battlefield | PresentCompare$ EQ0 | " +
@@ -3045,9 +2999,9 @@ public class Player extends GameEntity implements Comparable<Player> {
eff.addStaticAbility(castRestriction);
}
else {
eff.setSVar("CommanderMoveReplacement", "DB$ ChangeZone | Origin$ Battlefield,Graveyard,Exile,Library,Hand | Destination$ Command | Defined$ ReplacedCard");
String effStr = "DB$ ChangeZone | Origin$ Battlefield,Graveyard,Exile,Library,Hand | Destination$ Command | Defined$ ReplacedCard";
String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Optional$ True | OptionalDecider$ You | ReplaceWith$ CommanderMoveReplacement ";
String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Optional$ True | OptionalDecider$ You | CommanderMoveReplacement$ True ";
if (game.getRules().hasAppliedVariant(GameType.TinyLeaders)) {
moved += " | Destination$ Graveyard,Exile | Description$ If a commander would be put into its owner's graveyard or exile from anywhere, that player may put it into the command zone instead.";
}
@@ -3057,7 +3011,9 @@ public class Player extends GameEntity implements Comparable<Player> {
// rule 903.9b
moved += " | Destination$ Hand,Library | Description$ If a commander would be put into its owner's hand or library from anywhere, its owner may put it into the command zone instead.";
}
eff.addReplacementEffect(ReplacementHandler.parseReplacement(moved, eff, true));
ReplacementEffect re = ReplacementHandler.parseReplacement(moved, eff, true);
re.setOverridingAbility(AbilityFactory.getAbility(effStr, eff));
eff.addReplacementEffect(re);
}
String mayBePlayedAbility = "Mode$ Continuous | EffectZone$ Command | MayPlay$ True | Affected$ Card.YouOwn+EffectSource | AffectedZone$ Command";
@@ -3177,6 +3133,77 @@ public class Player extends GameEntity implements Comparable<Player> {
return !StaticAbilityCantBecomeMonarch.anyCantBecomeMonarch(this);
}
public void createInitiativeEffect(final String set) {
final PlayerZone com = getZone(ZoneType.Command);
if (initiativeEffect == null) {
initiativeEffect = new Card(game.nextCardId(), null, game);
initiativeEffect.setOwner(this);
initiativeEffect.setImmutable(true);
if (set != null) {
initiativeEffect.setImageKey("t:initiative_" + set.toLowerCase());
initiativeEffect.setSetCode(set);
} else {
initiativeEffect.setImageKey("t:initiative");
}
initiativeEffect.setName("The Initiative");
//Set up damage trigger
final String damageTrig = "Mode$ DamageDoneOnceByController | ValidSource$ Player | ValidTarget$ You | " +
"CombatDamage$ True | TriggerZones$ Command | TriggerDescription$ Whenever one or more " +
"creatures a player controls deal combat damage to you, that player takes the initiative.";
final String damageEff = "DB$ TakeInitiative | Defined$ TriggeredSource";
final Trigger damageTrigger = TriggerHandler.parseTrigger(damageTrig, initiativeEffect, true);
damageTrigger.setOverridingAbility(AbilityFactory.getAbility(damageEff, initiativeEffect));
initiativeEffect.addTrigger(damageTrigger);
//Set up triggers to venture into Undercity
final String ventureTakeTrig = "Mode$ TakesInitiative | ValidPlayer$ You | TriggerZones$ Command | " +
"TriggerDescription$ Whenever you take the initiative and at the beginning of your upkeep, " +
"venture into Undercity. (If you're in a dungeon, advance to the next room. If not, enter " +
"Undercity. You can take the initiative even if you already have it.)";
final String ventureUpkpTrig = "Mode$ Phase | Phase$ Upkeep | TriggerZones$ Command | ValidPlayer$ You " +
"| TriggerDescription$ Whenever you take the initiative and at the beginning of your upkeep, " +
"venture into Undercity. (If you're in a dungeon, advance to the next room. If not, enter " +
"Undercity. You can take the initiative even if you already have it.) | Secondary$ True";
final String ventureEff = "DB$ Venture | Dungeon$ Undercity";
final Trigger ventureUTrigger = TriggerHandler.parseTrigger(ventureUpkpTrig, initiativeEffect, true);
ventureUTrigger.setOverridingAbility(AbilityFactory.getAbility(ventureEff, initiativeEffect));
initiativeEffect.addTrigger(ventureUTrigger);
final Trigger ventureTTrigger = TriggerHandler.parseTrigger(ventureTakeTrig, initiativeEffect, true);
ventureTTrigger.setOverridingAbility(AbilityFactory.getAbility(ventureEff, initiativeEffect));
initiativeEffect.addTrigger(ventureTTrigger);
initiativeEffect.updateStateForView();
}
final TriggerHandler triggerHandler = game.getTriggerHandler();
triggerHandler.suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, initiativeEffect, null, null);
triggerHandler.clearSuppression(TriggerType.ChangesZone);
triggerHandler.clearActiveTriggers(initiativeEffect, null);
triggerHandler.registerActiveTrigger(initiativeEffect, false);
this.updateZoneForView(com);
}
public boolean hasInitiative() {
return equals(game.getHasInitiative());
}
public void removeInitiativeEffect() {
final PlayerZone com = getZone(ZoneType.Command);
if (initiativeEffect != null) {
com.remove(initiativeEffect);
this.updateZoneForView(com);
}
}
public void updateKeywordCardAbilityText() {
if (getKeywordCard() == null)
return;

View File

@@ -134,7 +134,7 @@ public abstract class PlayerController {
public abstract <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer, Map<String, Object> params);
public abstract boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message);
public abstract boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params);
public abstract boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode bidlife, String string, int bid, Player winner);
public abstract boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question);
public abstract boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message);

Some files were not shown because too many files have changed in this diff Show More