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

@@ -3,7 +3,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.50-SNAPSHOT</version>
<version>1.6.54-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -119,10 +119,26 @@
<opts>
<opt>-Dfile.encoding=UTF-8</opt>
<opt>--add-opens java.base/java.lang=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.math=ALL-UNNAMED</opt>
<opt>--add-opens java.base/jdk.internal.misc=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.nio=ALL-UNNAMED</opt>
<opt>--add-opens=java.base/sun.nio.ch=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.util=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.lang.reflect=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.text=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt.font=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt.image=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt.color=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/sun.awt.image=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/javax.swing=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/javax.swing.border=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/javax.swing.event=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/sun.swing=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.beans=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.util.concurrent=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.net=ALL-UNNAMED</opt>
<opt>-Dio.netty.tryReflectionSetAccessible=true</opt>
</opts>
</jre>
<versionInfo>
@@ -274,7 +290,7 @@
<dependency>
<groupId>forge</groupId>
<artifactId>forge-gui-mobile</artifactId>
<version>1.6.50-SNAPSHOT</version>
<version>1.6.54-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -10,7 +10,7 @@ java -version 1>nul 2>nul || (
for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j"
if %jver% GEQ 17 (
java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
java --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED --add-opens java.desktop/sun.swing=ALL-UNNAMED --add-opens java.desktop/java.awt.image=ALL-UNNAMED --add-opens java.desktop/java.awt.color=ALL-UNNAMED --add-opens java.desktop/sun.awt.image=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd
exit /b 0
)

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-ai</artifactId>

View File

@@ -20,6 +20,7 @@ package forge.ai;
import java.util.ArrayList;
import java.util.List;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
@@ -540,7 +541,7 @@ public class AiAttackController {
for (Card attacker : categorizedAttackers) {
if (!CombatUtil.canBeBlocked(attacker, accountedBlockers, null)
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) {
unblockedAttackers.add(attacker);
} else {
if (predictEvasion) {

View File

@@ -42,6 +42,7 @@ import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -204,7 +205,7 @@ public class AiBlockController {
}
blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers);
// 2.Blockers that won't get destroyed
} else if (!attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
} else if (!StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)
&& !ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
blocker = ComputerUtilCard.getWorstCreatureAI(safeBlockers);
// check whether it's better to block a creature without trample to absorb more damage
@@ -215,7 +216,7 @@ public class AiBlockController {
|| other.hasKeyword(Keyword.TRAMPLE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
|| ComputerUtilCombat.canDestroyBlocker(ai, blocker, other, combat, false)
|| other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(other)) {
continue;
}
@@ -668,7 +669,7 @@ public class AiBlockController {
Card attacker = attackers.get(0);
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0);
makeChumpBlocks(combat, attackers);
@@ -689,7 +690,7 @@ public class AiBlockController {
}
if (other.getNetCombatDamage() >= damageAbsorbed
&& !other.hasKeyword(Keyword.TRAMPLE)
&& !other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
&& !StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(other)
&& !ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
&& CombatUtil.canBlock(other, blocker, combat)) {
combat.addBlocker(other, blocker);
@@ -756,7 +757,7 @@ public class AiBlockController {
for (final Card attacker : tramplingAttackers) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
@@ -945,6 +946,39 @@ public class AiBlockController {
}
}
private void makeRequiredBlocks(Combat combat) {
// assign blockers that have to block
final CardCollection chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able.");
// if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
chumpBlockers.add(blocker);
}
}
if (!chumpBlockers.isEmpty()) {
for (final Card attacker : attackers) {
List<Card> blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false);
for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
&& (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker);
if (!blocker.getMustBlockCards().isEmpty()) {
int mustBlockAmt = blocker.getMustBlockCards().size();
final CardCollectionView blockedSoFar = combat.getAttackersBlockedBy(blocker);
boolean canBlockAnother = CombatUtil.canBlockMoreCreatures(blocker, blockedSoFar);
if (!canBlockAnother || mustBlockAmt == blockedSoFar.size()) {
blockersLeft.remove(blocker);
}
} else {
blockersLeft.remove(blocker);
}
}
}
}
}
}
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) {
for (final Card blocker : CardLists.filterControlledBy(combat.getAllBlockers(), ai)) {
// don't touch other player's blockers
@@ -1008,9 +1042,6 @@ public class AiBlockController {
clearBlockers(combat, possibleBlockers);
List<Card> blockers;
List<Card> chumpBlockers;
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) {
diff = 0;
@@ -1106,37 +1137,9 @@ public class AiBlockController {
}
}
// assign blockers that have to block
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able.");
// if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
chumpBlockers.add(blocker);
}
}
if (!chumpBlockers.isEmpty()) {
CardLists.shuffle(attackers);
for (final Card attacker : attackers) {
blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false);
for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
&& (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker);
if (!blocker.getMustBlockCards().isEmpty()) {
int mustBlockAmt = blocker.getMustBlockCards().size();
final CardCollectionView blockedSoFar = combat.getAttackersBlockedBy(blocker);
boolean canBlockAnother = CombatUtil.canBlockMoreCreatures(blocker, blockedSoFar);
if (!canBlockAnother || mustBlockAmt == blockedSoFar.size()) {
blockersLeft.remove(blocker);
}
} else {
blockersLeft.remove(blocker);
}
}
}
}
}
// block requirements
// TODO because this isn't done earlier, sometimes a good block will enforce a restriction that prevents another for the requirement
makeRequiredBlocks(combat);
// check to see if it's possible to defend a Planeswalker under attack with a chump block,
// unless life is low enough to be more worried about saving preserving the life total
@@ -1186,8 +1189,7 @@ public class AiBlockController {
* Orders a blocker that put onto the battlefield blocking. Depends heavily
* on the implementation of orderBlockers().
*/
public static CardCollection orderBlocker(final Card attacker, final Card blocker,
final CardCollection oldBlockers) {
public static CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) {
// add blocker to existing ordering
// sort by evaluate, then insert it appropriately
// relies on current implementation of orderBlockers()

View File

@@ -889,6 +889,10 @@ public class AiController {
spellHost.setLastKnownZone(game.getStackZone()); // need to add to stack to make check Restrictions respect stack cmc
spellHost.setCastFrom(card.getZone());
}
// TODO maybe other location for this?
if (!sa.isLegalAfterStack()) {
return AiPlayDecision.AnotherTime;
}
if (!sa.checkRestrictions(spellHost, player)) {
return AiPlayDecision.AnotherTime;
}
@@ -1322,7 +1326,7 @@ public class AiController {
return discardList;
}
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
if (mode == PlayerActionConfirmMode.AlternativeDamageAssignment) {
return true;
}
@@ -1335,7 +1339,7 @@ public class AiController {
mode);
throw new IllegalArgumentException(exMsg);
}
return SpellApiToAi.Converter.get(api).confirmAction(player, sa, mode, message);
return SpellApiToAi.Converter.get(api).confirmAction(player, sa, mode, message, params);
}
public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, int bid, Player winner) {
@@ -1925,8 +1929,8 @@ public class AiController {
return Math.min(player.getLife() -1,MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1);
} else if ("HighestGetCounter".equals(logic)) {
return MyRandom.getRandom().nextInt(3);
} else if (source.hasSVar("EnergyToPay")) {
return AbilityUtils.calculateAmount(source, source.getSVar("EnergyToPay"), sa);
} else if (sa.hasSVar("EnergyToPay")) {
return AbilityUtils.calculateAmount(source, sa.getSVar("EnergyToPay"), sa);
} else if ("Vermin".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.max(player.getLife() - 5, 0));
} else if ("SweepCreatures".equals(logic)) {

View File

@@ -48,6 +48,7 @@ import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -325,8 +326,7 @@ public class ComputerUtilCombat {
final List<Card> blockers = combat.getBlockers(attacker);
if (blockers.size() == 0
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage "
+ "as though it weren't blocked.")) {
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) {
unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& getAttack(attacker) > totalShieldDamage(attacker, blockers)) {
@@ -367,8 +367,7 @@ public class ComputerUtilCombat {
final List<Card> blockers = combat.getBlockers(attacker);
if (blockers.size() == 0
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage"
+ " as though it weren't blocked.")) {
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) {
unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& getAttack(attacker) > totalShieldDamage(attacker, blockers)) {

View File

@@ -10,6 +10,7 @@ import forge.game.card.CounterEnumType;
import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMustAttack;
import java.util.List;
@@ -62,7 +63,8 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword("Unblockable")) {
value += addValue(power * 10, "unblockable");
} else {
if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
if (StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(c)
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(c, false)) {
value += addValue(power * 6, "thorns");
}
if (c.hasKeyword(Keyword.FEAR)) {

View File

@@ -15,7 +15,6 @@ import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.Card;
@@ -207,7 +206,7 @@ public abstract class GameState {
cardsReferencedByID.add(card.getExiledWith());
}
if (zone == ZoneType.Battlefield) {
if (!card.getAttachedCards().isEmpty()) {
if (card.hasCardAttachments()) {
// Remember the ID of cards that have attachments
cardsReferencedByID.add(card);
}
@@ -376,7 +375,7 @@ public abstract class GameState {
newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ","));
}
if (!c.getMergedCards().isEmpty()) {
if (c.hasMergedCard()) {
List<String> mergedCardNames = new ArrayList<>();
for (Card merged : c.getMergedCards()) {
if (c.getTopMergedCard() == merged) {
@@ -862,9 +861,7 @@ public abstract class GameState {
}
if (sa.hasParam("RememberTargets")) {
for (final GameObject o : sa.getTargets()) {
sa.getHostCard().addRemembered(o);
}
sa.getHostCard().addRemembered(sa.getTargets());
}
}

View File

@@ -270,8 +270,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return getAi().confirmAction(sa, mode, message);
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return getAi().confirmAction(sa, mode, message, params);
}
@Override

View File

@@ -305,7 +305,7 @@ public abstract class SpellAbilityAi {
return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
System.err.println("Warning: default (ie. inherited from base class) implementation of confirmAction is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return true;
}

View File

@@ -71,6 +71,7 @@ public enum SpellApiToAi {
.put(ApiType.DigMultiple, DigMultipleAi.class)
.put(ApiType.DigUntil, DigUntilAi.class)
.put(ApiType.Discard, DiscardAi.class)
.put(ApiType.Draft, ChooseCardNameAi.class)
.put(ApiType.DrainMana, DrainManaAi.class)
.put(ApiType.Draw, DrawAi.class)
.put(ApiType.EachDamage, DamageEachAi.class)
@@ -169,6 +170,7 @@ public enum SpellApiToAi {
.put(ApiType.StoreSVar, StoreSVarAi.class)
.put(ApiType.Subgame, AlwaysPlayAi.class)
.put(ApiType.Surveil, SurveilAi.class)
.put(ApiType.TakeInitiative, AlwaysPlayAi.class)
.put(ApiType.Tap, TapAi.class)
.put(ApiType.TapAll, TapAllAi.class)
.put(ApiType.TapOrUntap, TapOrUntapAi.class)
@@ -184,7 +186,6 @@ public enum SpellApiToAi {
.put(ApiType.WinsGame, GameWinAi.class)
.put(ApiType.DamageResolve, AlwaysPlayAi.class)
.put(ApiType.InternalEtbReplacement, CanPlayAsDrawbackAi.class)
.put(ApiType.InternalLegendaryRule, LegendaryRuleAi.class)
.put(ApiType.InternalIgnoreEffect, CannotPlayAi.class)
.build());

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -16,7 +18,7 @@ public class AlwaysPlayAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -85,7 +85,7 @@ public class AmassAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -240,7 +240,7 @@ public class AnimateAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2);
}
@@ -500,17 +500,19 @@ public class AnimateAi extends SpellAbilityAi {
}
// give sVars
if (sVars.size() > 0) {
for (final String s : sVars) {
String actualsVar = source.getSVar(s);
if (sa.hasParam("sVars")) {
Map<String, String> sVarsMap = Maps.newHashMap();
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];
}
card.setSVar(name, actualsVar);
sVarsMap.put(name, actualsVar);
}
card.addChangedSVars(sVarsMap, timestamp, 0);
}
ComputerUtilCard.applyStaticContPT(game, card, null);
}

View File

@@ -1729,7 +1729,7 @@ public class AttachAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -1,35 +0,0 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false;
}
/**
* <p>
* copySpellTriggerAI.
* </p>
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @param mandatory
* a boolean.
* @param af
* a {@link forge.game.ability.AbilityFactory} object.
*
* @return a boolean.
*/
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return false;
}
}

View File

@@ -1739,7 +1739,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
// AI was never asked
return true;
}

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import java.util.Collections;
import java.util.Map;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
@@ -334,7 +335,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
final Card source = sa.getHostCard();
final String hostName = source.getName();
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);

View File

@@ -154,7 +154,7 @@ public class CloneAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
if (sa.hasParam("AILogic") && (!sa.usesTargeting() || sa.isTargetNumberValid())) {
// Had a special logic for it and managed to target, so confirm if viable
if ("CloneBestCreature".equals(sa.getParam("AILogic"))) {

View File

@@ -224,7 +224,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
//TODO: add logic here
return true;
}

View File

@@ -134,7 +134,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
// Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then
// run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents.
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {

View File

@@ -752,14 +752,16 @@ public class CountersPutAi extends CountersAi {
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
int left = amount;
final String[] types;
String type = "";
if (sa.hasParam("CounterType")) {
// TODO some cards let you choose types, should check each
types = sa.getParam("CounterType").split(",");
} else {
type = types[0];
} else if (sa.hasParam("CounterTypes")) {
// all types will be added
types = sa.getParam("CounterTypes").split(",");
type = types[0];
}
final String type = types[0];
if (!sa.usesTargeting()) {
// No target. So must be defined
@@ -911,7 +913,7 @@ public class CountersPutAi extends CountersAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
final Card source = sa.getHostCard();
if (mode == PlayerActionConfirmMode.Tribute) {
// add counter if that opponent has a giant creature

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
@@ -151,7 +152,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return player.getCreaturesInPlay().size() >= player.getWeakestOpponent().getCreaturesInPlay().size();
}

View File

@@ -53,6 +53,7 @@ import forge.util.MyRandom;
public class DamageDealAi extends DamageAiBase {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final SpellAbility root = sa.getRootAbility();
final String damage = sa.getParam("NumDmg");
Card source = sa.getHostCard();
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
@@ -76,7 +77,7 @@ public class DamageDealAi extends DamageAiBase {
if (dmg > energy || dmg < 1) {
continue; // in case the calculation gets messed up somewhere
}
source.setSVar("EnergyToPay", "Number$" + dmg);
root.setSVar("EnergyToPay", "Number$" + dmg);
return true;
}
}
@@ -1075,6 +1076,11 @@ public class DamageDealAi extends DamageAiBase {
return null;
}
// chaining to this could miscalculate
if (sa.isDividedAsYouChoose()) {
return null;
}
// Try to chain damage/debuff effects
if (StringUtils.isNumeric(damage) || (damage.startsWith("-") && StringUtils.isNumeric(damage.substring(1)))) {
// currently only works for predictable numeric damage

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -30,7 +32,7 @@ public class DayTimeAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -353,12 +353,10 @@ public class DestroyAi extends SpellAbilityAi {
// Filter AI-specific targets if provided
preferred = ComputerUtil.filterAITgts(sa, ai, preferred, true);
for (final Card c : preferred) {
list.remove(c);
}
list.removeAll(preferred);
if (preferred.isEmpty() && !mandatory) {
return false;
return false;
}
while (sa.canAddMoreTarget()) {

View File

@@ -209,7 +209,7 @@ public class DigAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
Card topc = player.getZone(ZoneType.Library).get(0);
// AI actions for individual cards (until this AI can be generalized)

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
@@ -94,7 +96,7 @@ public class DigMultipleAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCost;
@@ -122,7 +123,7 @@ public class DigUntilAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
if ("OathOfDruids".equals(logic)) {

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
@@ -211,11 +212,11 @@ public class DiscardAi extends SpellAbilityAi {
return true;
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
if (mode == PlayerActionConfirmMode.Random) {
// TODO For now AI will always discard Random used currently with: Balduvian Horde and similar cards
return true;
}
return super.confirmAction(player, sa, mode, message);
return super.confirmAction(player, sa, mode, message, params);
}
}

View File

@@ -18,6 +18,8 @@
*/
package forge.ai.ability;
import java.util.Map;
import forge.ai.AiCostDecision;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
@@ -102,7 +104,7 @@ public class DrawAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source,sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
AiCostDecision aiDecisions = new AiCostDecision(ai, sa, false);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard) {
@@ -541,7 +543,7 @@ public class DrawAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
int numCards = sa.hasParam("NumCards") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa) : 1;
// AI shouldn't mill itself
if (numCards < player.getZone(ZoneType.Library).size())

View File

@@ -70,7 +70,7 @@ public final class EncodeAi extends SpellAbilityAi {
* forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
// only try to encode if there is a creature it can be used on
return chooseCard(player, player.getCreaturesInPlay(), true) != null;
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.base.Predicate;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -42,7 +44,7 @@ public class FlipOntoBattlefieldAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -31,6 +31,11 @@ public class ImmediateTriggerAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
// always add to stack, targeting happens after payment
if (mandatory) {
return true;
}
String logic = sa.getParamOrDefault("AILogic", "");
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
@@ -45,11 +50,7 @@ public class ImmediateTriggerAi extends SpellAbilityAi {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
trigsa.setActivatingPlayer(ai);
if (!sa.hasParam("OptionalDecider")) {
return aic.doTrigger(trigsa, true);
} else {
return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"));
}
return aic.doTrigger(trigsa, !"You".equals(sa.getParamOrDefault("OptionalDecider", "You")));
}
@Override

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -20,7 +22,7 @@ public class InvestigateAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.ComputerUtilCard;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
@@ -32,7 +34,7 @@ public class LearnAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -43,7 +43,7 @@ public class ManifestAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -196,7 +196,7 @@ public class MillAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
if ("TimmerianFiends".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.TimmerianFiends.consider(player, sa);
}

View File

@@ -65,7 +65,7 @@ public class MutateAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
@@ -70,7 +72,7 @@ public class PeekAndRevealAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
AbilitySub subAb = sa.getSubAbility();
return subAb != null && SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(player, subAb);
}

View File

@@ -131,7 +131,7 @@ public class PlayAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -24,6 +24,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class PumpAi extends PumpAiBase {
@@ -731,7 +732,7 @@ public class PumpAi extends PumpAiBase {
if (minus > energy || minus < 1) {
continue; // in case the calculation gets messed up somewhere
}
source.setSVar("EnergyToPay", "Number$" + minus);
root.setSVar("EnergyToPay", "Number$" + minus);
return true;
}
}
@@ -781,7 +782,7 @@ public class PumpAi extends PumpAiBase {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
//TODO Add logic here if necessary but I think the AI won't cast
//the spell in the first place if it would curse its own creature
//and the pump isn't mandatory

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.AiProps;
@@ -92,7 +94,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
// Confirming this action means shuffling the library if asked.
// First, let's check if we can play the top card of the library

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.*;
@@ -40,7 +42,7 @@ public class RepeatAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
//TODO add logic to have computer make better choice (ArsenalNut)
return false;
}

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.card.Card;
@@ -43,7 +45,7 @@ public class RollDiceAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
@@ -175,7 +176,7 @@ public class SacrificeAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtilMana;
@@ -132,7 +134,7 @@ public class ScryAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
@@ -264,7 +265,7 @@ public class SetStateAi extends SpellAbilityAi {
return true;
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
// TODO: improve the AI for when it may want to transform something that's optional to transform
return isSafeToTransformIntoLegendary(player, sa.getHostCard());
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -54,7 +56,7 @@ public class ShuffleAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
// ai could analyze parameter denoting the player to shuffle
return true;
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
@@ -18,7 +20,7 @@ public class SkipPhaseAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.AiCardMemory;
import forge.ai.AiProps;
import forge.ai.ComputerUtilCost;
@@ -119,7 +121,7 @@ public class SurveilAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}
}

View File

@@ -106,10 +106,8 @@ public abstract class TapAiBase extends SpellAbilityAi {
* @return a boolean.
*/
protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Game game = ai.getGame();
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tapList = CardLists.getTargetableCards(tapList, sa);
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
tapList = CardLists.filter(tapList, Presets.UNTAPPED);
tapList = CardLists.filter(tapList, new Predicate<Card>() {
@Override
@@ -129,8 +127,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
//use broader approach when the cost is a positive thing
if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) {
tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tapList = CardLists.getTargetableCards(tapList, sa);
tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
tapList = CardLists.filter(tapList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -150,8 +147,10 @@ public abstract class TapAiBase extends SpellAbilityAi {
//try to exclude things that will already be tapped due to something on stack or because something is
//already targeted in a parent or sub SA
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, tapList, sa, ApiType.Tap);
tapList.removeAll(toExclude);
if (!sa.isTrigger() || mandatory) { // but if just confirming trigger no need to look for other targets and might still help anyway
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, tapList, sa, ApiType.Tap);
tapList.removeAll(toExclude);
}
if (tapList.isEmpty()) {
return false;
@@ -176,6 +175,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
}
PhaseHandler phase = game.getPhaseHandler();
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, tapList);
if (primeTarget != null) {
choice = primeTarget;
@@ -193,7 +193,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
return CombatUtil.canAttack(c, opp);
}
});
attackers.remove(sa.getHostCard());
attackers.remove(source);
}
Predicate<Card> findBlockers = CardPredicates.possibleBlockerForAtLeastOne(attackers);
List<Card> creatureList = CardLists.filter(tapList, findBlockers);
@@ -202,7 +202,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else if (sa.getRootAbility().isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(tapList);
}
} else if (phase.isPlayerTurn(opp)
@@ -272,7 +272,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
return true;
}
// filter by enchantments and planeswalkers, their tapped state doesn't matter.
// filter by enchantments and planeswalkers, their tapped state (usually) doesn't matter.
final String[] tappablePermanents = { "Enchantment", "Planeswalker" };
tapList = CardLists.getValidCards(list, tappablePermanents, source.getController(), source, sa);

View File

@@ -311,7 +311,7 @@ public class TokenAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
// TODO: AILogic
return true;
}

View File

@@ -53,7 +53,7 @@ public class UntapAi extends SpellAbilityAi {
return false;
}
return ComputerUtilCost.checkDiscardCost(ai, cost, sa.getHostCard(), sa);
return ComputerUtilCost.checkDiscardCost(ai, cost, source, sa);
}
@Override
@@ -174,8 +174,10 @@ public class UntapAi extends SpellAbilityAi {
//try to exclude things that will already be untapped due to something on stack or because something is
//already targeted in a parent or sub SA
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, untapList, sa, ApiType.Untap);
untapList.removeAll(toExclude);
if (!sa.isTrigger() || mandatory) { // but if just confirming trigger no need to look for other targets and might still help anyway
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, untapList, sa, ApiType.Untap);
untapList.removeAll(toExclude);
}
sa.resetTargets();
while (sa.canAddMoreTarget()) {
@@ -199,7 +201,7 @@ public class UntapAi extends SpellAbilityAi {
if (choice == null) {
if (CardLists.getNotType(untapList, "Creature").isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(untapList); // if only creatures take the best
} else if (!sa.getPayCosts().hasManaCost() || sa.getRootAbility().isTrigger()
} else if (!sa.getPayCosts().hasManaCost() || sa.isTrigger()
|| "Always".equals(sa.getParam("AILogic"))) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(untapList);
}
@@ -290,7 +292,7 @@ public class UntapAi extends SpellAbilityAi {
choice = ComputerUtilCard.getBestAI(tapList);
if (choice == null) { // can't find anything left
if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().size() == 0) {
if (sa.getTargets().size() < tgt.getMinTargets(source, sa) || sa.getTargets().size() == 0) {
if (!mandatory) {
sa.resetTargets();
}
@@ -310,9 +312,7 @@ public class UntapAi extends SpellAbilityAi {
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> list, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
PlayerCollection pl = new PlayerCollection();
pl.add(ai);
pl.addAll(ai.getAllies());
PlayerCollection pl = ai.getYourTeam();
return ComputerUtilCard.getBestAI(CardLists.filterControlledBy(list, pl));
}

View File

@@ -36,7 +36,7 @@ public class VentureAi extends SpellAbilityAi {
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return true;
}

View File

@@ -91,6 +91,7 @@ public class GameCopier {
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
newPlayer.setLifeGainedThisTurn(origPlayer.getLifeGainedThisTurn());
// TODO creatureAttackedThisTurn
for (Mana m : origPlayer.getManaPool()) {
newPlayer.getManaPool().addMana(m, false);
}
@@ -207,6 +208,13 @@ public class GameCopier {
private void copyGameState(Game newGame) {
newGame.setAge(origGame.getAge());
// TODO countersAddedThisTurn
if (origGame.getMonarch() != null) {
newGame.setMonarch(playerMap.get(origGame.getMonarch()));
}
for (ZoneType zone : ZONES) {
for (Card card : origGame.getCardsIn(zone)) {
addCard(newGame, zone, card);
@@ -300,6 +308,7 @@ public class GameCopier {
newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable());
newCard.setPTBoost(c.getPTBoostTable());
// TODO copy by map
newCard.setDamage(c.getDamage());
newCard.setChangedCardColors(c.getChangedCardColorsTable());

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-core</artifactId>

View File

@@ -3,6 +3,7 @@ package forge;
import forge.item.PaperCard;
import forge.util.FileUtil;
import forge.util.TextUtil;
import forge.util.ThreadUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
@@ -171,10 +172,18 @@ public final class ImageKeys {
return file;
}
//setlookup
file = setLookUpFile(filename, fullborderFile);
if (file != null) {
cachedCards.put(filename, file);
return file;
if (hasSetLookup(filename)) {
//delay processing so gui is responsive
ThreadUtil.delay(60, new Runnable() {
@Override
public void run() {
File f = setLookUpFile(filename, fullborderFile);
if (f != null)
cachedCards.put(filename, f);
else //is null
missingCards.add(filename);
}
});
}
}
//if an image, like phenomenon or planes is missing .full in their filenames but you have an existing images that have .full/.fullborder
@@ -250,7 +259,7 @@ public final class ImageKeys {
// System.out.println("File not found, no image created: " + key);
//add missing cards - disable for desktop version for compatibility reasons with autodownloader
if (isLibGDXPort)
if (isLibGDXPort && !hasSetLookup(filename)) //missing cards with setlookup is handled differently
missingCards.add(filename);
return null;
}
@@ -260,6 +269,13 @@ public final class ImageKeys {
? StaticData.instance().getEditions().getCode2ByCode(edition) // by default 2-letter codes from MWS are used
: CACHE_CARD_PICS_SUBDIR.get(edition); // may use custom paths though
}
static boolean hasSetLookup(String filename) {
if (!StaticData.instance().getSetLookup().isEmpty()) {
return StaticData.instance().getSetLookup().keySet().stream().anyMatch(setKey -> filename.startsWith(setKey));
}
return false;
}
private static File setLookUpFile(String filename, String fullborderFile) {
if (!StaticData.instance().getSetLookup().isEmpty()) {
for (String setKey : StaticData.instance().getSetLookup().keySet()) {

View File

@@ -214,8 +214,15 @@ public final class CardRules implements ICardCharacteristics {
}
}
public boolean isEnterableDungeon() {
if (mainPart.getOracleText().contains("You can't enter this dungeon unless")) {
return false;
}
return getType().isDungeon();
}
public boolean canBeCommander() {
if (mainPart.getOracleText().contains("can be your commander")) {
if (mainPart.getOracleText().contains("can be your commander") || canBeBackground()) {
return true;
}
CardType type = mainPart.getType();
@@ -232,8 +239,15 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean canBePartnerCommander() {
if (canBeBackground()) {
return true;
}
return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty() ||
hasKeyword("Friends forever"));
hasKeyword("Friends forever") || hasKeyword("Choose a Background"));
}
public boolean canBeBackground() {
return mainPart.getType().hasSubtype("Background");
}
public boolean canBeOathbreaker() {

View File

@@ -209,6 +209,16 @@ public final class CardRulesPredicates {
};
}
public static Predicate<CardRules> deckHasExactly(final DeckHints.Type type, final String has[]) {
return new Predicate<CardRules>() {
@Override
public boolean apply(final CardRules card) {
DeckHints deckHas = card.getAiHints().getDeckHas();
return deckHas != null && deckHas.isValid() && deckHas.is(type, has);
}
};
}
/**
* Core type.
*

View File

@@ -33,6 +33,7 @@ import com.google.common.base.Predicate;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -50,6 +51,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
private static final long serialVersionUID = 4629853583167022151L;
public static final CardTypeView EMPTY = new CardType(false);
private static final Set<String> multiWordTypes = ImmutableSet.of("Serra's Realm", "Bolas's Meditation Realm", "Dungeon Master");
public enum CoreType {
Artifact(true, "artifacts"),
@@ -71,6 +73,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
public final String pluralName;
private static Map<String, CoreType> stringToCoreType = EnumUtils.getEnumMap(CoreType.class);
private static final Set<String> allCoreTypeNames = stringToCoreType.keySet();
public static final Set<CoreType> spellTypes = ImmutableSet.of(Instant, Sorcery);
public static CoreType getEnum(String name) {
return stringToCoreType.get(name);
@@ -535,7 +538,8 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
newType = new CardType(CardType.this);
if (ct.isRemoveCardTypes()) {
newType.coreTypes.clear();
// 205.1a However, an object with either the instant or sorcery card type retains that type.
newType.coreTypes.retainAll(CoreType.spellTypes);
}
if (ct.isRemoveSuperTypes()) {
newType.supertypes.clear();
@@ -612,6 +616,9 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
if (!isPlaneswalker()) {
Iterables.removeIf(subtypes, Predicates.IS_WALKER_TYPE);
}
if (!isDungeon()) {
Iterables.removeIf(subtypes, Predicates.IS_DUNGEON_TYPE);
}
}
@Override
@@ -746,12 +753,14 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
while (hasMoreTypes) {
final String type = typeText.substring(iTypeStart, iSpace == -1 ? typeText.length() : iSpace);
hasMoreTypes = iSpace != -1;
if (!isMultiwordType(type) || !hasMoreTypes) {
iTypeStart = iSpace + 1;
if (!"-".equals(type)) {
result.add(type);
}
final String rest = typeText.substring(iTypeStart);
if (isMultiwordType(rest)) {
result.add(rest);
break;
}
iTypeStart = iSpace + 1;
result.add(type);
iSpace = typeText.indexOf(space, iSpace + 1);
}
return result;
@@ -769,13 +778,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
}
private static boolean isMultiwordType(final String type) {
final String[] multiWordTypes = { "Serra's Realm", "Bolas's Meditation Realm", "Dungeon Master" };
for (int i = 0; i < multiWordTypes.length; ++i) {
if (multiWordTypes[i].startsWith(type) && !multiWordTypes[i].equals(type)) {
return true;
}
}
return false;
return multiWordTypes.contains(type);
}
public static class Constant {
@@ -787,6 +790,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
public static final Set<String> ENCHANTMENT_TYPES = Sets.newHashSet();
public static final Set<String> ARTIFACT_TYPES = Sets.newHashSet();
public static final Set<String> WALKER_TYPES = Sets.newHashSet();
public static final Set<String> DUNGEON_TYPES = Sets.newHashSet();
// singular -> plural
public static final BiMap<String,String> pluralTypes = HashBiMap.create();
@@ -846,6 +850,12 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return CardType.isAPlaneswalkerType(input);
}
};
public static Predicate<String> IS_DUNGEON_TYPE = new Predicate<String>() {
@Override
public boolean apply(String input) {
return CardType.isADungeonType(input);
}
};
}
///////// Utility methods
@@ -878,6 +888,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
sortedSubTypes.addAll(Constant.ENCHANTMENT_TYPES);
sortedSubTypes.addAll(Constant.ARTIFACT_TYPES);
sortedSubTypes.addAll(Constant.WALKER_TYPES);
sortedSubTypes.addAll(Constant.DUNGEON_TYPES);
Collections.sort(sortedSubTypes);
}
return sortedSubTypes;
@@ -933,6 +944,9 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return (Constant.SPELL_TYPES.contains(cardType));
}
public static boolean isADungeonType(final String cardType) {
return (Constant.DUNGEON_TYPES.contains(cardType));
}
/**
* If the input is a plural type, return the corresponding singular form.
* Otherwise, simply return the input.

View File

@@ -14,6 +14,7 @@ import com.google.common.collect.Iterables;
import forge.item.PaperCard;
import forge.util.PredicateString.StringOp;
import forge.util.collect.FCollection;
/**
* DeckHints provides the ability for a Card to "want" another Card or type of
@@ -73,12 +74,26 @@ public class DeckHints {
return false;
}
for (Pair<Type, String> filter : filters) {
if (filter.getLeft() == type && filter.getRight().equals(hint)) {
if (filter.getLeft() == type && filter.getRight().contains(hint)) {
return true;
}
}
return false;
}
public boolean is(Type type, String hints[]) {
if (filters == null) {
return false;
}
for (String hint : hints) {
for (Pair<Type, String> filter : filters) {
if (filter.getLeft() == type && filter.getRight().equals(hint)) {
continue;
}
}
return false;
}
return true;
}
/**
* Returns a Map of Cards by Type from the given Iterable<PaperCard> that match this
@@ -95,6 +110,10 @@ public class DeckHints {
String param = pair.getRight();
Iterable<PaperCard> cards = getCardsForFilter(cardList, type, param);
if (cards != null) {
// if a type is used more than once intersect respective matches
if (ret.get(type) != null) {
Iterables.retainAll(cards, new FCollection<>(ret.get(type)));
}
ret.put(type, cards);
}
}
@@ -143,13 +162,16 @@ public class DeckHints {
private Iterable<PaperCard> getCardsForFilter(Iterable<PaperCard> cardList, Type type, String param) {
List<PaperCard> cards = new ArrayList<>();
// this is case ABILITY, but other types can also use this when the implicit parsing would miss
String[] abilities = param.split("\\|");
for (String ability : abilities) {
Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.deckHas(type, ability), PaperCard.FN_GET_RULES));
}
// bonus if a DeckHas can satisfy the type with multiple ones
Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.deckHasExactly(type, abilities), PaperCard.FN_GET_RULES));
switch (type) {
case ABILITY:
String[] abilities = param.split("\\|");
for (String ability : abilities) {
Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.deckHas(Type.ABILITY, ability), PaperCard.FN_GET_RULES));
}
break;
case COLOR:
String[] colors = param.split("\\|");
for (String color : colors) {
@@ -187,6 +209,7 @@ public class DeckHints {
}
break;
case NONE:
case ABILITY: // already done above
break;
}
return cards;

View File

@@ -262,6 +262,9 @@ public enum DeckFormat {
} else if (a.getRules().hasKeyword("Friends forever") &&
b.getRules().hasKeyword("Friends forever")) {
// Stranger Things Secret Lair gimmick partner commander
} else if (a.getRules().hasKeyword("Choose a Background") && b.getRules().canBeBackground()
|| b.getRules().hasKeyword("Choose a Background") && a.getRules().canBeBackground()) {
// commander with background
} else {
return "has an illegal commander partnership";
}

View File

@@ -76,13 +76,19 @@ public class Localizer {
return defaultValue;
}
}
public String getEnglishMessage(final String key, final Object... messageArguments) {
return getMessage(true, key, messageArguments);
}
//FIXME: localizer should return default value from english locale or it will crash some GUI element like the NewGameMenu->NewGameScreen Popup when returned null...
public String getMessage(final String key, final Object... messageArguments) {
return getMessage(false, key, messageArguments);
}
public String getMessage(final boolean forcedEnglish, final String key, final Object... messageArguments) {
MessageFormat formatter = null;
try {
//formatter = new MessageFormat(resourceBundle.getString(key.toLowerCase()), locale);
formatter = new MessageFormat(english ? englishBundle.getString(key) : resourceBundle.getString(key), english ? Locale.ENGLISH : locale);
formatter = new MessageFormat(english || forcedEnglish ? englishBundle.getString(key) : resourceBundle.getString(key), english || forcedEnglish ? Locale.ENGLISH : locale);
} catch (final IllegalArgumentException | MissingResourceException e) {
if (!silent)
e.printStackTrace();
@@ -95,12 +101,12 @@ public class Localizer {
silent = false;
formatter.setLocale(english ? Locale.ENGLISH : locale);
formatter.setLocale(english || forcedEnglish ? Locale.ENGLISH : locale);
String formattedMessage = "CHAR ENCODING ERROR";
final String[] charsets = { "ISO-8859-1", "UTF-8" };
//Support non-English-standard characters
String detectedCharset = charset(english ? englishBundle.getString(key) : resourceBundle.getString(key), charsets);
String detectedCharset = charset(english || forcedEnglish ? englishBundle.getString(key) : resourceBundle.getString(key), charsets);
final int argLength = messageArguments.length;
Object[] syncEncodingMessageArguments = new Object[argLength];
@@ -149,7 +155,7 @@ public class Localizer {
englishBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
} catch (NullPointerException | MissingResourceException e) {
//If the language can't be loaded, default to US English
resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en_US"), loader);
e.printStackTrace();
}

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);
}
};

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