Merge branch 'master' into 'oracle-updates-nonfunctional'

update nonfunctional changes branch

See merge request core-developers/forge!937
This commit is contained in:
Rob Schnautz
2018-09-22 17:51:07 +00:00
2381 changed files with 16950 additions and 5485 deletions

21
checkstyle.xml Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
<!--
Checkstyle is very configurable.
http://checkstyle.sf.net (or in your downloaded distribution).
-->
<module name="Checker">
<module name="TreeWalker">
<module name="RedundantImport"/>
<module name="UnusedImports">
<!-- <property name="processJavadoc" value="false"/> -->
</module>
</module>
</module>

View File

@@ -3,6 +3,7 @@
<name>forge-ai</name>
<comment></comment>
<projects>
<project>forge-game</project>
</projects>
<buildSpec>
<buildCommand>

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.11-SNAPSHOT</version>
<version>1.6.16-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>
@@ -29,4 +29,31 @@
<version>3.6.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>checkstyle-validation</id>
<phase>validate</phase>
<configuration>
<configLocation>../checkstyle.xml</configLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<failOnViolation>true</failOnViolation>
</configuration>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -127,10 +127,12 @@ public class AiController {
private List<SpellAbility> getPossibleETBCounters() {
CardCollection all = new CardCollection(player.getCardsIn(ZoneType.Hand));
CardCollectionView ccvPlayerLibrary = player.getCardsIn(ZoneType.Library);
all.addAll(player.getCardsIn(ZoneType.Exile));
all.addAll(player.getCardsIn(ZoneType.Graveyard));
if (!player.getCardsIn(ZoneType.Library).isEmpty()) {
all.add(player.getCardsIn(ZoneType.Library).get(0));
if (!ccvPlayerLibrary.isEmpty()) {
all.add(ccvPlayerLibrary.get(0));
}
for (final Player opp : player.getOpponents()) {
@@ -153,30 +155,34 @@ public class AiController {
// look for cards on the battlefield that should prevent the AI from using that spellability
private boolean checkCurseEffects(final SpellAbility sa) {
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
CardCollectionView ccvGameBattlefield = game.getCardsIn(ZoneType.Battlefield);
for (final Card c : ccvGameBattlefield) {
if (c.hasSVar("AICurseEffect")) {
final String curse = c.getSVar("AICurseEffect");
final Card host = sa.getHostCard();
if ("NonActive".equals(curse) && !player.equals(game.getPhaseHandler().getPlayerTurn())) {
return true;
} else if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
&& !sa.getHostCard().hasKeyword(Keyword.INDESTRUCTIBLE)) {
return true;
} else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
&& CardFactoryUtil.isCounterable(host)) {
return true;
} else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)
&& host.getCMC() == c.getCounters(CounterType.CHARGE)) {
return true;
} else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
if (!card.isToken() && card.getName().equals(host.getName())) {
return true;
} else {
final Card host = sa.getHostCard();
if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
&& !host.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return true;
} else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
&& CardFactoryUtil.isCounterable(host)) {
return true;
} else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)
&& host.getCMC() == c.getCounters(CounterType.CHARGE)) {
return true;
} else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) {
String hostName = host.getName();
for (Card card : ccvGameBattlefield) {
if (!card.isToken() && card.getName().equals(hostName)) {
return true;
}
}
}
for (Card card : game.getCardsIn(ZoneType.Graveyard)) {
if (card.getName().equals(host.getName())) {
return true;
for (Card card : game.getCardsIn(ZoneType.Graveyard)) {
if (card.getName().equals(hostName)) {
return true;
}
}
}
}
@@ -186,13 +192,14 @@ public class AiController {
}
public boolean checkETBEffects(final Card card, final SpellAbility sa, final ApiType api) {
boolean rightapi = false;
if (card.isCreature()
&& game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureETBTriggers)) {
return api == null;
}
boolean rightapi = false;
String battlefield = ZoneType.Battlefield.toString();
Player activatingPlayer = sa.getActivatingPlayer();
// Trigger play improvements
for (final Trigger tr : card.getTriggers()) {
// These triggers all care for ETB effects
@@ -202,21 +209,22 @@ public class AiController {
continue;
}
if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (params.containsKey("ValidCard")) {
if (!params.get("ValidCard").contains("Self")) {
String validCard = params.get("ValidCard");
if (!validCard.contains("Self")) {
continue;
}
if (params.get("ValidCard").contains("notkicked")) {
if (validCard.contains("notkicked")) {
if (sa.isKicked()) {
continue;
}
} else if (params.get("ValidCard").contains("kicked")) {
if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
String s = params.get("ValidCard").split("kicked ")[1];
} else if (validCard.contains("kicked")) {
if (validCard.contains("kicked ")) { // want a specific kicker
String s = validCard.split("kicked ")[1];
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
} else if (!sa.isKicked()) {
@@ -259,7 +267,7 @@ public class AiController {
}
if (sa != null) {
exSA.setActivatingPlayer(sa.getActivatingPlayer());
exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
@@ -267,13 +275,11 @@ public class AiController {
exSA.setTrigger(true);
// for trigger test, need to ignore the conditions
if (exSA.getConditions() != null) {
SpellAbilityCondition cons = exSA.getConditions();
if (cons.getIsPresent() != null) {
String pres = cons.getIsPresent();
if ("Card.Self".equals(pres) || "Card.StrictlySelf".equals(pres)) {
SpellAbilityCondition cons = exSA.getConditions();
if (cons != null) {
String pres = cons.getIsPresent();
if (pres != null && pres.matches("Card\\.(Strictly)?Self")) {
cons.setIsPresent(null);
}
}
}
@@ -297,21 +303,22 @@ public class AiController {
continue;
}
if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (params.containsKey("ValidCard")) {
if (!params.get("ValidCard").contains("Self")) {
String validCard = params.get("ValidCard");
if (!validCard.contains("Self")) {
continue;
}
if (params.get("ValidCard").contains("notkicked")) {
if (validCard.contains("notkicked")) {
if (sa.isKicked()) {
continue;
}
} else if (params.get("ValidCard").contains("kicked")) {
if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
String s = params.get("ValidCard").split("kicked ")[1];
} else if (validCard.contains("kicked")) {
if (validCard.contains("kicked ")) { // want a specific kicker
String s = validCard.split("kicked ")[1];
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
} else if (!sa.isKicked()) { // otherwise just any must be present
@@ -327,7 +334,7 @@ public class AiController {
if (exSA != null) {
if (sa != null) {
exSA.setActivatingPlayer(sa.getActivatingPlayer());
exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
@@ -375,8 +382,9 @@ public class AiController {
if (landsInPlay.size() + landList.size() > max) {
for (Card c : allCards) {
for (SpellAbility sa : c.getSpellAbilities()) {
if (sa.getPayCosts() != null) {
for (CostPart part : sa.getPayCosts().getCostParts()) {
Cost payCosts = sa.getPayCosts();
if (payCosts != null) {
for (CostPart part : payCosts.getCostParts()) {
if (part instanceof CostDiscard) {
return null;
}
@@ -390,10 +398,11 @@ public class AiController {
landList = CardLists.filter(landList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
canPlaySpellBasic(c, null);
if (c.getType().isLegendary() && !c.getName().equals("Flagstones of Trokair")) {
final CardCollectionView list = player.getCardsIn(ZoneType.Battlefield);
if (Iterables.any(list, CardPredicates.nameEquals(c.getName()))) {
String name = c.getName();
if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) {
if (Iterables.any(battlefield, CardPredicates.nameEquals(name))) {
return false;
}
}
@@ -402,7 +411,7 @@ public class AiController {
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
CardCollection lands = new CardCollection(player.getCardsIn(ZoneType.Battlefield));
CardCollection lands = new CardCollection(battlefield);
lands.addAll(hand);
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
@@ -565,14 +574,17 @@ public class AiController {
Collections.sort(all, saComparator); // put best spells first
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
if (sa.getApi() == ApiType.Counter || sa.getApi() == exceptSA) {
ApiType saApi = sa.getApi();
if (saApi == ApiType.Counter || saApi == exceptSA) {
continue;
}
sa.setActivatingPlayer(player);
// TODO: this currently only works as a limited prediction of permanent spells.
// Ideally this should cast canPlaySa to determine that the AI is truly able/willing to cast a spell,
// but that is currently difficult to implement due to various side effects leading to stack overflow.
if (!ComputerUtil.castPermanentInMain1(player, sa) && sa.getHostCard() != null && !sa.getHostCard().isLand() && ComputerUtilCost.canPayCost(sa, player)) {
Card host = sa.getHostCard();
if (!ComputerUtil.castPermanentInMain1(player, sa) && host != null && !host.isLand() && ComputerUtilCost.canPayCost(sa, player)) {
if (sa instanceof SpellPermanent) {
return sa;
}
@@ -800,8 +812,7 @@ public class AiController {
}
// use Surge and Prowl costs when able to
if (sa.isSurged() ||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
if (sa.isSurged() || sa.isProwl()) {
p += 9;
}
// sort planeswalker abilities with most costly first

View File

@@ -514,19 +514,26 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
if (c == null) {
if (ability.getSVar(cost.getAmount()).equals("XChoice")) {
if ("SacToReduceCost".equals(ability.getParam("AILogic"))) {
String logic = ability.getParamOrDefault("AILogic", "");
if ("SacToReduceCost".equals(logic)) {
// e.g. Torgaar, Famine Incarnate
// TODO: currently returns an empty list, so the AI doesn't sacrifice anything. Trying to make
// the AI decide on creatures to sac makes the AI sacrifice them, but the cost is not reduced and the
// AI pays the full mana cost anyway (despite sacrificing creatures).
return PaymentDecision.card(new CardCollection());
} else if (!logic.isEmpty() && !logic.equals("Never")) {
// If at least some other AI logic is specified, assume that the AI for that API knows how
// to define ChosenX and thus honor that value.
// Cards which have no special logic for this yet but which do work in a simple/suboptimal way
// are currently conventionally flagged with AILogic$ DoSacrifice.
c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null);
} else {
// Other cards are assumed to be flagged RemAIDeck for now
return null;
}
// Other cards are assumed to be flagged RemAIDeck for now
return null;
} else {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);

View File

@@ -93,6 +93,7 @@ public enum AiProps { /** */
SCRY_EVALTHR_CMC_THRESHOLD ("3"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL ("10"), /** */
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */

View File

@@ -95,7 +95,9 @@ public class ComputerUtil {
sa.setHostCard(game.getAction().moveToStack(source, sa));
}
sa.resetPaidHash();
if (sa.isCopied()) {
sa.resetPaidHash();
}
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa);
@@ -260,6 +262,10 @@ public class ComputerUtil {
sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard());
newSA.setHostCard(game.getAction().moveToStack(source, sa));
if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) {
CharmEffect.makeChoices(newSA);
}
}
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
@@ -2629,8 +2635,8 @@ public class ComputerUtil {
// and also on Chronozoa
|| (type == CounterType.TIME && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|| type == CounterType.GOLD || type == CounterType.MUSIC || type == CounterType.PUPA
|| type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
|| type == CounterType.SLEIGHT || type == CounterType.WAGE;
|| type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
|| type == CounterType.SLUMBER || type == CounterType.SLEIGHT || type == CounterType.WAGE;
}
// this countertypes has no effect

View File

@@ -1573,10 +1573,10 @@ public class ComputerUtilCard {
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness);
pumped.addChangedCardKeywords(kws, new ArrayList<String>(), false, timestamp);
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
Set<CounterType> types = c.getCounters().keySet();
for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), c, true);
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true);
}
//Copies tap-state and extra keywords (auras, equipment, etc.)
if (c.isTapped()) {
@@ -1596,7 +1596,7 @@ public class ComputerUtilCard {
}
}
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
pumped.addChangedCardKeywordsInternal(toCopy, Lists.<KeywordInterface>newArrayList(), false, timestamp2, true);
pumped.addChangedCardKeywordsInternal(toCopy, null, false, false, timestamp2, true);
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
return pumped;
}
@@ -1625,6 +1625,9 @@ public class ComputerUtilCard {
if (!params.containsKey("Affected")) {
continue;
}
if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) {
continue;
}
final String valid = params.get("Affected");
if (!vCard.isValid(valid, c.getController(), c, null)) {
continue;

View File

@@ -975,12 +975,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
continue;
}
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump")) {
continue;
}
@@ -1098,12 +1106,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
continue;
}
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
String abType = "";
if (abilityParams.containsKey("AB")) {
abType = abilityParams.get("AB");
@@ -1311,12 +1327,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
continue;
}
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
continue; // targeted pumping not supported
}
@@ -1330,7 +1354,14 @@ public class ComputerUtilCombat {
}
if (abilityParams.containsKey("Cost")) {
final SpellAbility sa = AbilityFactory.getAbility(ability, source);
SpellAbility sa = null;
if (trigger.getOverridingAbility() != null) {
sa = trigger.getOverridingAbility();
} else {
final String ability = source.getSVar(trigParams.get("Execute"));
sa = AbilityFactory.getAbility(ability, source);
}
sa.setActivatingPlayer(source.getController());
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
@@ -1514,12 +1545,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
continue;
}
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
continue; // targeted pumping not supported
}
@@ -1552,7 +1591,14 @@ public class ComputerUtilCombat {
}
if (abilityParams.containsKey("Cost")) {
final SpellAbility sa = AbilityFactory.getAbility(ability, source);
SpellAbility sa = null;
if (trigger.getOverridingAbility() != null) {
sa = trigger.getOverridingAbility();
} else {
final String ability = source.getSVar(trigParams.get("Execute"));
sa = AbilityFactory.getAbility(ability, source);
}
sa.setActivatingPlayer(source.getController());
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;

View File

@@ -5,7 +5,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.game.GameActionUtil;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;

View File

@@ -6,6 +6,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
@@ -19,8 +20,10 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.event.GameEventAttackersDeclared;
import forge.game.event.GameEventCombatChanged;
import forge.game.mana.ManaPool;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
@@ -52,6 +55,14 @@ public abstract class GameState {
private int computerLife = -1;
private String humanCounters = "";
private String computerCounters = "";
private String humanManaPool = "";
private String computerManaPool = "";
private String humanPersistentMana = "";
private String computerPersistentMana = "";
private int humanLandsPlayed = 0;
private int computerLandsPlayed = 0;
private int humanLandsPlayedLastTurn = 0;
private int computerLandsPlayedLastTurn = 0;
private boolean puzzleCreatorState = false;
@@ -60,6 +71,7 @@ public abstract class GameState {
private final Map<Integer, Card> idToCard = new HashMap<>();
private final Map<Card, Integer> cardToAttachId = new HashMap<>();
private final Map<Card, Integer> cardToEnchantPlayerId = new HashMap<>();
private final Map<Card, Integer> markedDamage = new HashMap<>();
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
private final Map<Card, String> cardToChosenType = new HashMap<>();
@@ -79,6 +91,8 @@ public abstract class GameState {
private String tChangePlayer = "NONE";
private String tChangePhase = "NONE";
private String tAdvancePhase = "NONE";
private String precastHuman = null;
private String precastAI = null;
@@ -112,6 +126,10 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("humanlife=", String.valueOf(humanLife), "\n"));
sb.append(TextUtil.concatNoSpace("ailife=", String.valueOf(computerLife), "\n"));
sb.append(TextUtil.concatNoSpace("humanlandsplayed=", String.valueOf(humanLandsPlayed), "\n"));
sb.append(TextUtil.concatNoSpace("ailandsplayed=", String.valueOf(computerLandsPlayed), "\n"));
sb.append(TextUtil.concatNoSpace("humanlandsplayedlastturn=", String.valueOf(humanLandsPlayedLastTurn), "\n"));
sb.append(TextUtil.concatNoSpace("ailandsplayedlastturn=", String.valueOf(computerLandsPlayedLastTurn), "\n"));
sb.append(TextUtil.concatNoSpace("turn=", String.valueOf(turn), "\n"));
if (!humanCounters.isEmpty()) {
@@ -121,6 +139,13 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("aicounters=", computerCounters, "\n"));
}
if (!humanManaPool.isEmpty()) {
sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n"));
}
if (!computerManaPool.isEmpty()) {
sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n"));
}
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
sb.append(TextUtil.concatNoSpace("activephase=", tChangePhase, "\n"));
appendCards(humanCardTexts, "human", sb);
@@ -147,8 +172,14 @@ public abstract class GameState {
}
humanLife = human.getLife();
computerLife = ai.getLife();
humanLandsPlayed = human.getLandsPlayedThisTurn();
computerLandsPlayed = ai.getLandsPlayedThisTurn();
humanLandsPlayedLastTurn = human.getLandsPlayedLastTurn();
computerLandsPlayedLastTurn = ai.getLandsPlayedLastTurn();
humanCounters = countersToString(human.getCounters());
computerCounters = countersToString(ai.getCounters());
humanManaPool = processManaPool(human.getManaPool());
computerManaPool = processManaPool(ai.getManaPool());
tChangePlayer = game.getPhaseHandler().getPlayerTurn() == ai ? "ai" : "human";
tChangePhase = game.getPhaseHandler().getPhase().toString();
@@ -266,6 +297,12 @@ public abstract class GameState {
} else if (c.getEnchantingCard() != null) {
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
}
if (c.getEnchantingPlayer() != null) {
// TODO: improve this for game states with more than two players
newText.append("|EnchantingPlayer:");
Player p = c.getEnchantingPlayer();
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
}
if (c.getDamage() > 0) {
newText.append("|Damage:").append(c.getDamage());
@@ -388,8 +425,10 @@ public abstract class GameState {
if (categoryName.startsWith("active")) {
if (categoryName.endsWith("player"))
tChangePlayer = categoryValue.trim().toLowerCase();
if (categoryName.endsWith("phase"))
else if (categoryName.endsWith("phase"))
tChangePhase = categoryValue.trim().toUpperCase();
else if (categoryName.endsWith("phaseadvance"))
tAdvancePhase = categoryValue.trim().toUpperCase();
return;
}
@@ -413,6 +452,20 @@ public abstract class GameState {
computerCounters = categoryValue;
}
else if (categoryName.endsWith("landsplayed")) {
if (isHuman)
humanLandsPlayed = Integer.parseInt(categoryValue);
else
computerLandsPlayed = Integer.parseInt(categoryValue);
}
else if (categoryName.endsWith("landsplayedlastturn")) {
if (isHuman)
humanLandsPlayedLastTurn = Integer.parseInt(categoryValue);
else
computerLandsPlayedLastTurn = Integer.parseInt(categoryValue);
}
else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) {
if (isHuman)
humanCardTexts.put(ZoneType.Battlefield, categoryValue);
@@ -465,6 +518,21 @@ public abstract class GameState {
else
precastAI = categoryValue;
}
else if (categoryName.endsWith("manapool")) {
if (isHuman)
humanManaPool = categoryValue;
else
computerManaPool = categoryValue;
}
else if (categoryName.endsWith("persistentmana")) {
if (isHuman)
humanPersistentMana = categoryValue;
else
computerPersistentMana = categoryValue;
}
else {
System.out.println("Unknown key: " + categoryName);
}
@@ -485,6 +553,7 @@ public abstract class GameState {
idToCard.clear();
cardToAttachId.clear();
cardToEnchantPlayerId.clear();
cardToRememberedId.clear();
cardToExiledWithId.clear();
markedDamage.clear();
@@ -493,12 +562,18 @@ public abstract class GameState {
cardToScript.clear();
cardAttackMap.clear();
Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
Player newPlayerTurn = tChangePlayer.equalsIgnoreCase("human") ? human : tChangePlayer.equalsIgnoreCase("ai") ? ai : null;
PhaseType newPhase = tChangePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tChangePhase);
PhaseType advPhase = tAdvancePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tAdvancePhase);
// Set stack to resolving so things won't trigger/effects be checked right away
game.getStack().setResolving(true);
updateManaPool(human, humanManaPool, true, false);
updateManaPool(ai, computerManaPool, true, false);
updateManaPool(human, humanPersistentMana, false, true);
updateManaPool(ai, computerPersistentMana, false, true);
if (!humanCounters.isEmpty()) {
applyCountersToGameEntity(human, humanCounters);
}
@@ -510,8 +585,8 @@ public abstract class GameState {
game.getTriggerHandler().setSuppressAllTriggers(true);
setupPlayerState(humanLife, humanCardTexts, human);
setupPlayerState(computerLife, aiCardTexts, ai);
setupPlayerState(humanLife, humanCardTexts, human, humanLandsPlayed, humanLandsPlayedLastTurn);
setupPlayerState(computerLife, aiCardTexts, ai, computerLandsPlayed, computerLandsPlayedLastTurn);
handleCardAttachments();
handleChosenEntities();
@@ -531,9 +606,50 @@ public abstract class GameState {
game.getStack().setResolving(false);
// Advance to a certain phase, activating all triggered abilities
if (advPhase != null) {
game.getPhaseHandler().devAdvanceToPhase(advPhase);
}
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
}
private String processManaPool(ManaPool manaPool) {
String mana = "";
for (final byte c : MagicColor.WUBRGC) {
int amount = manaPool.getAmountOfColor(c);
for (int i = 0; i < amount; i++) {
mana += MagicColor.toShortString(c) + " ";
}
}
return mana.trim();
}
private void updateManaPool(Player p, String manaDef, boolean clearPool, boolean persistent) {
Game game = p.getGame();
if (clearPool) {
p.getManaPool().clearPool(false);
}
if (!manaDef.isEmpty()) {
final Card dummy = new Card(-777777, game);
dummy.setOwner(p);
final Map<String, String> produced = Maps.newHashMap();
produced.put("Produced", manaDef);
if (persistent) {
produced.put("PersistentMana", "True");
}
final AbilityManaPart abMana = new AbilityManaPart(dummy, produced);
game.getAction().invoke(new Runnable() {
@Override
public void run() {
abMana.produceMana(null);
}
});
}
}
private void handleCombat(final Game game, final Player attackingPlayer, final Player defendingPlayer, final boolean toDeclareBlockers) {
// First we need to ensure that all attackers are declared in the Declare Attackers step,
// even if proceeding straight to Declare Blockers
@@ -864,6 +980,16 @@ public abstract class GameState {
attacher.fortifyCard(attachedTo);
}
}
// Enchant players by ID
for(Entry<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
// TODO: improve this for game states with more than two players
Card attacher = entry.getKey();
Game game = attacher.getGame();
Player attachedTo = entry.getValue() == TARGET_AI ? game.getPlayers().get(1) : game.getPlayers().get(0);
attacher.enchantEntity(attachedTo);
}
}
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
@@ -875,7 +1001,7 @@ public abstract class GameState {
}
}
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p, final int landsPlayed, final int landsPlayedLastTurn) {
// Lock check static as we setup player state
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
@@ -885,6 +1011,9 @@ public abstract class GameState {
}
if (life >= 0) p.setLife(life, null);
p.setLandsPlayedThisTurn(landsPlayed);
p.setLandsPlayedLastTurn(landsPlayedLastTurn);
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) {
@@ -1008,6 +1137,10 @@ public abstract class GameState {
} else if (info.startsWith("Attaching:")) {
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
cardToAttachId.put(c, id);
} else if (info.startsWith("EnchantingPlayer:")) {
// TODO: improve this for game states with more than two players
String tgt = info.substring(info.indexOf(':') + 1);
cardToEnchantPlayerId.put(c, tgt.equalsIgnoreCase("AI") ? TARGET_AI : TARGET_HUMAN);
} else if (info.startsWith("Ability:")) {
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));

View File

@@ -168,12 +168,13 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title) {
public SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells);
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params);
}
@Override
@@ -291,9 +292,38 @@ public class PlayerControllerAi extends PlayerController {
return ImmutablePair.of(toTop, toBottom);
}
/* (non-Javadoc)
* @see forge.game.player.PlayerController#arrangeForSurveil(forge.game.card.CardCollection)
*/
@Override
public ImmutablePair<CardCollection, CardCollection> arrangeForSurveil(CardCollection topN) {
CardCollection toGraveyard = new CardCollection();
CardCollection toTop = new CardCollection();
// TODO: Currently this logic uses the same routine as Scry. Possibly differentiate this and implement
// a specific logic for Surveil (e.g. maybe to interact better with Reanimator strategies etc.).
if (getPlayer().getCardsIn(ZoneType.Hand).size() <= getAi().getIntProperty(AiProps.SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL)) {
toTop.addAll(topN);
} else {
for (Card c : topN) {
if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c)) {
toGraveyard.add(c);
} else {
toTop.add(c);
}
}
}
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toGraveyard);
}
@Override
public boolean willPutCardOnTop(Card c) {
return true; // AI does not know what will happen next (another clash or that would become his topdeck)
// This is used for Clash. Currently uses Scry logic to determine whether the card should be put on top.
// Note that the AI does not know what will happen next (another clash or that would become his topdeck)
return !ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c);
}
@Override

View File

@@ -324,7 +324,7 @@ public abstract class SpellAbilityAi {
return null;
}
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleSpellAbility is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return spells.get(0);
}

View File

@@ -25,6 +25,7 @@ public enum SpellApiToAi {
.put(ApiType.AnimateAll, AnimateAllAi.class)
.put(ApiType.Attach, AttachAi.class)
.put(ApiType.Ascend, AlwaysPlayAi.class)
.put(ApiType.AssignGroup, AssignGroupAi.class)
.put(ApiType.Balance, BalanceAi.class)
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
.put(ApiType.BecomesBlocked, BecomesBlockedAi.class)
@@ -144,6 +145,7 @@ public enum SpellApiToAi {
.put(ApiType.SkipTurn, SkipTurnAi.class)
.put(ApiType.StoreMap, StoreMapAi.class)
.put(ApiType.StoreSVar, StoreSVarAi.class)
.put(ApiType.Surveil, SurveilAi.class)
.put(ApiType.Tap, TapAi.class)
.put(ApiType.TapAll, TapAllAi.class)
.put(ApiType.TapOrUntap, TapOrUntapAi.class)

View File

@@ -12,6 +12,7 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@@ -93,7 +94,8 @@ public class ActivateAbilityAi extends SpellAbilityAi {
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
return spells.get(0);
}
}

View File

@@ -23,12 +23,12 @@ import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import forge.game.ability.effects.AnimateEffectBase;
/**
* <p>
@@ -363,11 +363,11 @@ public class AnimateAi extends SpellAbilityAi {
card.setSickness(hasOriginalCardSickness);
// AF specific sa
int power = -1;
Integer power = null;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
}
int toughness = -1;
Integer toughness = null;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
}
@@ -453,65 +453,7 @@ public class AnimateAi extends SpellAbilityAi {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
}
// duplicating AnimateEffectBase.doAnimate
boolean removeSuperTypes = false;
boolean removeCardTypes = false;
boolean removeSubTypes = false;
boolean removeCreatureTypes = false;
boolean removeArtifactTypes = false;
if (sa.hasParam("OverwriteTypes")) {
removeSuperTypes = true;
removeCardTypes = true;
removeSubTypes = true;
removeCreatureTypes = true;
removeArtifactTypes = true;
}
if (sa.hasParam("KeepSupertypes")) {
removeSuperTypes = false;
}
if (sa.hasParam("KeepCardTypes")) {
removeCardTypes = false;
}
if (sa.hasParam("RemoveSuperTypes")) {
removeSuperTypes = true;
}
if (sa.hasParam("RemoveCardTypes")) {
removeCardTypes = true;
}
if (sa.hasParam("RemoveSubTypes")) {
removeSubTypes = true;
}
if (sa.hasParam("RemoveCreatureTypes")) {
removeCreatureTypes = true;
}
if (sa.hasParam("RemoveArtifactTypes")) {
removeArtifactTypes = true;
}
if ((power != -1) || (toughness != -1)) {
card.addNewPT(power, toughness, timestamp);
}
if (!types.isEmpty() || !removeTypes.isEmpty() || removeCreatureTypes) {
card.addChangedCardTypes(types, removeTypes, removeSuperTypes, removeCardTypes, removeSubTypes,
removeCreatureTypes, removeArtifactTypes, timestamp);
}
card.addChangedCardKeywords(keywords, removeKeywords, sa.hasParam("RemoveAllAbilities"), timestamp);
for (final String k : hiddenKeywords) {
card.addHiddenExtrinsicKeyword(k);
}
card.addColor(finalDesc, !sa.hasParam("OverwriteColors"), timestamp);
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, keywords, removeKeywords, hiddenKeywords, timestamp);
// back to duplicating AnimateEffect.resolve
// TODO will all these abilities/triggers/replacements/etc. lead to
@@ -521,10 +463,14 @@ public class AnimateAi extends SpellAbilityAi {
boolean clearAbilities = sa.hasParam("OverwriteAbilities");
boolean clearSpells = sa.hasParam("OverwriteSpells");
boolean removeAll = sa.hasParam("RemoveAllAbilities");
boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
if (clearAbilities || clearSpells || removeAll) {
for (final SpellAbility ab : card.getSpellAbilities()) {
if (removeAll || (ab.isAbility() && clearAbilities) || (ab.isSpell() && clearSpells)) {
if (removeAll
|| (ab.isIntrinsic() && removeIntrinsic && !ab.isBasicLandAbility())
|| (ab.isAbility() && clearAbilities)
|| (ab.isSpell() && clearSpells)) {
card.removeSpellAbility(ab);
removedAbilities.add(ab);
}
@@ -565,9 +511,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress triggers from the animated card
final List<Trigger> removedTriggers = Lists.newArrayList();
if (sa.hasParam("OverwriteTriggers") || removeAll) {
final FCollectionView<Trigger> triggersToRemove = card.getTriggers();
for (final Trigger trigger : triggersToRemove) {
if (sa.hasParam("OverwriteTriggers") || removeAll || removeIntrinsic) {
for (final Trigger trigger : card.getTriggers()) {
if (removeIntrinsic && !trigger.isIntrinsic()) {
continue;
}
trigger.setSuppressed(true);
removedTriggers.add(trigger);
}
@@ -603,9 +551,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress static abilities from the animated card
final List<StaticAbility> removedStatics = Lists.newArrayList();
if (sa.hasParam("OverwriteStatics") || removeAll) {
final FCollectionView<StaticAbility> staticsToRemove = card.getStaticAbilities();
for (final StaticAbility stAb : staticsToRemove) {
if (sa.hasParam("OverwriteStatics") || removeAll || removeIntrinsic) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (removeIntrinsic && !stAb.isIntrinsic()) {
continue;
}
stAb.setTemporarilySuppressed(true);
removedStatics.add(stAb);
}
@@ -613,8 +563,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress static abilities from the animated card
final List<ReplacementEffect> removedReplacements = Lists.newArrayList();
if (sa.hasParam("OverwriteReplacements") || removeAll) {
if (sa.hasParam("OverwriteReplacements") || removeAll || removeIntrinsic) {
for (final ReplacementEffect re : card.getReplacementEffects()) {
if (removeIntrinsic && !re.isIntrinsic()) {
continue;
}
re.setTemporarilySuppressed(true);
removedReplacements.add(re);
}

View File

@@ -0,0 +1,33 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class AssignGroupAi extends SpellAbilityAi {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
// otherwise the AI considers the card playable.
return true;
}
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
final String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("FriendOrFoe")) {
if (params.containsKey("Affected") && spells.size() >= 2) {
Player t = (Player) params.get("Affected");
return spells.get(player.isOpponentOf(t) ? 1 : 0);
}
}
return Iterables.getFirst(spells, null);
}
}

View File

@@ -510,7 +510,7 @@ public class AttachAi extends SpellAbilityAi {
// Prefer "tap to deal damage"
// TODO : Skip this one if triggers on combat damage only?
for (SpellAbility sa2 : card.getSpellAbilities()) {
if ((sa2.getApi().equals(ApiType.DealDamage))
if (ApiType.DealDamage.equals(sa2.getApi())
&& (sa2.getTargetRestrictions().canTgtPlayer())) {
cardPriority += 300;
}
@@ -971,8 +971,8 @@ public class AttachAi extends SpellAbilityAi {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility");

View File

@@ -6,6 +6,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
@@ -37,7 +38,8 @@ public class CanPlayAsDrawbackAi extends SpellAbilityAi {
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
// This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
return spells.get(0);
}

View File

@@ -152,7 +152,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
return doReturnCommanderLogic(sa, aiPlayer);
}
if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
} else if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
return true; // debuffed by opponent's auras to the level that it becomes useless
}

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.Iterables;
@@ -79,7 +80,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
Card host = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Game game = host.getGame();

View File

@@ -7,6 +7,7 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi {
@@ -36,7 +37,8 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
return spells.get(0);
}

View File

@@ -125,7 +125,7 @@ public class CountersPutAi extends SpellAbilityAi {
CardCollection list;
Card choice = null;
final String type = sa.getParam("CounterType");
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final String logic = sa.getParamOrDefault("AILogic", "");
PhaseHandler ph = ai.getGame().getPhaseHandler();
@@ -220,13 +220,9 @@ public class CountersPutAi extends SpellAbilityAi {
if ("Never".equals(logic)) {
return false;
}
if ("PayEnergy".equals(logic)) {
} else if ("PayEnergy".equals(logic)) {
return true;
}
if ("PayEnergyConservatively".equals(logic)) {
} else if ("PayEnergyConservatively".equals(logic)) {
boolean onlyInCombat = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
boolean onlyDefensive = ai.getController().isAI()
@@ -266,9 +262,7 @@ public class CountersPutAi extends SpellAbilityAi {
return true;
}
}
}
if (logic.equals("MarkOppCreature")) {
} else if (logic.equals("MarkOppCreature")) {
if (!ph.is(PhaseType.END_OF_TURN)) {
return false;
}
@@ -283,6 +277,11 @@ public class CountersPutAi extends SpellAbilityAi {
sa.getTargets().add(bestCreat);
return true;
}
} else if (logic.equals("CheckDFC")) {
// for cards like Ludevic's Test Subject
if (!source.canTransform()) {
return false;
}
}
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
@@ -581,7 +580,7 @@ public class CountersPutAi extends SpellAbilityAi {
final String type = sa.getParam("CounterType");
final String logic = sa.getParamOrDefault("AILogic", "");
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
@@ -661,7 +660,7 @@ public class CountersPutAi extends SpellAbilityAi {
boolean preferred = true;
CardCollection list;
final String type = sa.getParam("CounterType");
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
int left = amount;
@@ -804,7 +803,8 @@ public class CountersPutAi extends SpellAbilityAi {
if (mode == PlayerActionConfirmMode.Tribute) {
// add counter if that opponent has a giant creature
final List<Card> creats = player.getCreaturesInPlay();
final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa);
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final boolean isHaste = source.hasKeyword(Keyword.HASTE);
List<Card> threatening = CardLists.filter(creats, new Predicate<Card>() {
@@ -863,7 +863,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
final CounterType type = CounterType.valueOf(sa.getParam("CounterType"));
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final boolean isCurse = sa.isCurse();

View File

@@ -481,10 +481,11 @@ public class DamageDealAi extends DamageAiBase {
final PhaseHandler phase = game.getPhaseHandler();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
final String logic = sa.getParamOrDefault("AILogic", "");
Player enemy = ComputerUtil.getOpponentFor(ai);
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
if ("PowerDmg".equals(logic)) {
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
if (tgt.canTgtCreatureAndPlayer() && this.shouldTgtP(ai, sa, dmg, noPrevention)){
sa.resetTargets();
@@ -504,11 +505,11 @@ public class DamageDealAi extends DamageAiBase {
TargetChoices tcs = sa.getTargets();
// Do not use if would kill self
if (("SelfDamage".equals(sa.getParam("AILogic"))) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
if (("SelfDamage".equals(logic)) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
return false;
}
if ("ChoiceBurn".equals(sa.getParam("AILogic"))) {
if ("ChoiceBurn".equals(logic)) {
// do not waste burns on player if other choices are present
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
tcs.add(enemy);
@@ -517,7 +518,7 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
}
if ("Polukranos".equals(sa.getParam("AILogic"))) {
if ("Polukranos".equals(logic)) {
int dmgTaken = 0;
CardCollection humCreatures = enemy.getCreaturesInPlay();
Card lastTgt = null;
@@ -681,7 +682,7 @@ public class DamageDealAi extends DamageAiBase {
}
continue;
}
} else if ("OppAtTenLife".equals(sa.getParam("AILogic"))) {
} else if ("OppAtTenLife".equals(logic)) {
for (final Player p : ai.getOpponents()) {
if (sa.canTarget(p) && p.getLife() == 10 && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
tcs.add(p);
@@ -690,9 +691,10 @@ public class DamageDealAi extends DamageAiBase {
}
// TODO: Improve Damage, we shouldn't just target the player just
// because we can
else if (sa.canTarget(enemy)) {
if (sa.canTarget(enemy) && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
|| ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai))
|| sa.getPayCosts() == null || immediately
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) &&
(!avoidTargetP(ai, sa))) {

View File

@@ -26,10 +26,7 @@ import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.PaymentDecision;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -252,19 +249,34 @@ public class DrawAi extends SpellAbilityAi {
}
if (num != null && num.equals("ChosenX")) {
// Necrologia, Pay X Life : Draw X Cards
if (sa.getSVar("X").equals("XChoice")) {
// Draw up to max hand size but leave at least 3 in library
numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
// But no more than what's "safe" and doesn't risk a near death experience
// Maybe would be better to check for "serious danger" and take more risk?
while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
numCards--;
if (sa.getPayCosts() != null) {
if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
// [Necrologia, Pay X Life : Draw X Cards]
// Don't draw more than what's "safe" and don't risk a near death experience
// Maybe would be better to check for "serious danger" and take more risk?
while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
numCards--;
}
} else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
// [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards]
// TODO: Add special logic to limit/otherwise modify the ChosenX value here
// Skip this ability if nothing is to be chosen for sacrifice
if (numCards <= 0) {
return false;
}
}
}
sa.setSVar("ChosenX", Integer.toString(numCards));
source.setSVar("ChosenX", Integer.toString(numCards));
}
}
// Logic for cards that require special handling
if ("YawgmothsBargain".equals(logic)) {
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);

View File

@@ -33,6 +33,12 @@ public class FightAi extends SpellAbilityAi {
sa.resetTargets();
final Card source = sa.getHostCard();
// everything is defined or targeted above, can't do anything there?
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
// TODO extend Logic for cards like Arena or Grothama
return true;
}
// Get creature lists
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
aiCreatures = CardLists.getTargetableCards(aiCreatures, sa);

View File

@@ -9,6 +9,7 @@ import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.MagicStack;
public class LifeExchangeVariantAi extends SpellAbilityAi {
@@ -83,7 +84,25 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
return shouldDo;
}
else if ("Evra, Halcyon Witness".equals(sourceName)) {
// TODO add logic
if (!ai.canGainLife())
return false;
int aiLife = ai.getLife();
if (source.getNetPower() > aiLife) {
if (ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat())) {
return true;
}
// check the top of stack
MagicStack stack = ai.getGame().getStack();
if (!stack.isEmpty()) {
SpellAbility saTop = stack.peekAbility();
if (ComputerUtil.predictDamageFromSpell(saTop, ai) >= aiLife) {
return true;
}
}
}
}
return false;

View File

@@ -11,7 +11,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
@@ -22,9 +21,8 @@ public class ScryAi extends SpellAbilityAi {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { // It doesn't appear that Scry ever targets
if (sa.usesTargeting()) { // It doesn't appear that Scry ever targets
// ability is targeted
sa.resetTargets();

View File

@@ -3,7 +3,6 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GlobalRuleChange;
@@ -65,8 +64,6 @@ public class SetStateAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
final Card source = sa.getHostCard();
return super.checkAiLogic(aiPlayer, sa, aiLogic);
}
@@ -87,7 +84,7 @@ public class SetStateAi extends SpellAbilityAi {
if("Transform".equals(mode)) {
if (!sa.usesTargeting()) {
// no Transform with Defined which is not Self
if (source.hasKeyword("CARDNAME can't transform")) {
if (!source.canTransform()) {
return false;
}
return shouldTransformCard(source, ai, ph) || "Always".equals(logic);
@@ -96,15 +93,13 @@ public class SetStateAi extends SpellAbilityAi {
sa.resetTargets();
CardCollection list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
// select only cards with Transform as SplitType
// select only the ones that can transform
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(Card c) {
return c.hasAlternateState() && c.getRules().getSplitType() == CardSplitType.Transform;
return c.canTransform();
}
});
// select only the ones that can transform
list = CardLists.getNotKeyword(list, "CARDNAME can't transform");
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty()) {

View File

@@ -0,0 +1,104 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
public class SurveilAi extends SpellAbilityAi {
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#doTriggerAINoCost(forge.game.player.Player, forge.game.spellability.SpellAbility, boolean)
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
sa.resetTargets();
sa.getTargets().add(ai);
}
return true;
}
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#chkAIDrawback(forge.game.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return doTriggerAINoCost(ai, sa, false);
}
/**
* Checks if the AI will play a SpellAbility based on its phase restrictions
*/
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
// if the Surveil ability requires tapping and has a mana cost, it's best done at the end of opponent's turn
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
// even if there's no mana cost.
if (sa.getPayCosts() != null) {
if (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
&& !SpellAbilityAi.isSorcerySpeed(sa)) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
}
// in the player's turn Surveil should only be done in Main1 or in Upkeep if able
if (ph.isPlayerTurn(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa)) {
return ph.is(PhaseType.MAIN1) || sa.hasParam("Planeswalker");
} else {
return ph.is(PhaseType.UPKEEP);
}
}
return true;
}
/**
* Checks if the AI will play a SpellAbility with the specified AiLogic
*/
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Never".equals(aiLogic)) {
return false;
}
// TODO: add card-specific Surveil AI logic here when/if necessary
return true;
}
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
// Makes no sense to do Surveil when there's nothing in the library
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
return false;
}
double chance = .4; // 40 percent chance for instant speed
if (SpellAbilityAi.isSorcerySpeed(sa)) {
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
if (SpellAbilityAi.playReusable(ai, sa)) {
randomReturn = true;
}
return randomReturn;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
}

View File

@@ -8,6 +8,7 @@ import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.TokenEffect;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
@@ -42,14 +43,11 @@ import java.util.List;
* @version $Id: AbilityFactoryToken.java 17656 2012-10-22 19:32:56Z Max mtg $
*/
public class TokenAi extends SpellAbilityAi {
private String tokenAmount;
private String tokenName;
private String[] tokenTypes;
private String[] tokenKeywords;
private String tokenPower;
private String tokenToughness;
private Card actualToken;
/**
* <p>
* Constructor for AbilityFactory_Token.
@@ -58,23 +56,28 @@ public class TokenAi extends SpellAbilityAi {
* a {@link forge.game.ability.AbilityFactory} object.
*/
private void readParameters(final SpellAbility mapParams) {
String[] keywords;
if (mapParams.hasParam("TokenKeywords")) {
// TODO: Change this Split to a semicolon or something else
keywords = mapParams.getParam("TokenKeywords").split("<>");
} else {
keywords = new String[0];
}
this.tokenAmount = mapParams.getParamOrDefault("TokenAmount", "1");
this.tokenPower = mapParams.getParam("TokenPower");
this.tokenToughness = mapParams.getParam("TokenToughness");
this.tokenName = mapParams.getParam("TokenName");
this.tokenTypes = mapParams.getParam("TokenTypes").split(",");
this.tokenKeywords = keywords;
TokenEffect effect = new TokenEffect();
this.actualToken = effect.loadTokenPrototype(mapParams);
if (actualToken == null) {
String[] keywords;
if (mapParams.hasParam("TokenKeywords")) {
// TODO: Change this Split to a semicolon or something else
keywords = mapParams.getParam("TokenKeywords").split("<>");
} else {
keywords = new String[0];
}
this.tokenPower = mapParams.getParam("TokenPower");
this.tokenToughness = mapParams.getParam("TokenToughness");
} else {
this.tokenPower = actualToken.getBasePowerString();
this.tokenToughness = actualToken.getBaseToughnessString();
}
}
@Override
@@ -103,8 +106,11 @@ public class TokenAi extends SpellAbilityAi {
}
}
final Card token = spawnToken(ai, sa);
if (token == null) {
if (actualToken == null) {
actualToken = spawnToken(ai, sa);
}
if (actualToken == null) {
final AbilitySub sub = sa.getSubAbility();
if (pwPlus || (sub != null && SpellApiToAi.Converter.get(sub.getApi()).chkAIDrawback(sub, ai))) {
return true; // planeswalker plus ability or sub-ability is
@@ -130,24 +136,21 @@ public class TokenAi extends SpellAbilityAi {
}
}
if (canInterruptSacrifice(ai, sa, token)) {
if (canInterruptSacrifice(ai, sa, actualToken)) {
return true;
}
boolean haste = false;
boolean haste = this.actualToken.hasKeyword(Keyword.HASTE);
boolean oneShot = sa.getSubAbility() != null
&& sa.getSubAbility().getApi() == ApiType.DelayedTrigger;
for (final String kw : this.tokenKeywords) {
if (kw.equals("Haste")) {
haste = true;
}
}
boolean isCreature = this.actualToken.getType().isCreature();
// Don't generate tokens without haste before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && ph.isPlayerTurn(ai) && !haste && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
boolean buff = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
if ("Creature".equals(c.getSVar("BuffedBy"))) {
if (isCreature && "Creature".equals(c.getSVar("BuffedBy"))) {
buff = true;
}
}
@@ -180,12 +183,9 @@ public class TokenAi extends SpellAbilityAi {
}
// Don't kill AIs Legendary tokens
for (final String type : this.tokenTypes) {
if (type.equals("Legendary")) {
if (ai.isCardInPlay(this.tokenName)) {
return false;
}
}
if (this.actualToken.getType().isLegendary() && ai.isCardInPlay(this.actualToken.getName())) {
// TODO Check if Token is useless due to an aura or counters?
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -311,6 +311,18 @@ public class TokenAi extends SpellAbilityAi {
}
}
if (mandatory) {
// Necessary because the AI goes into this method twice, first to set up targets (with mandatory=true)
// and then the second time to confirm the trigger (where mandatory may be set to false).
return true;
}
if ("OnlyOnAlliedAttack".equals(sa.getParam("AILogic"))) {
Combat combat = ai.getGame().getCombat();
return combat != null && combat.getAttackingPlayer() != null
&& !combat.getAttackingPlayer().isOpponentOf(ai);
}
return true;
}
/* (non-Javadoc)
@@ -388,6 +400,7 @@ public class TokenAi extends SpellAbilityAi {
* @param notNull if the token would not survive, still return it
* @return token creature created by ability
*/
// TODO Is this just completely copied from TokenEffect? Let's just call that thing
public static Card spawnToken(Player ai, SpellAbility sa, boolean notNull) {
final Card host = sa.getHostCard();
@@ -511,7 +524,7 @@ public class TokenAi extends SpellAbilityAi {
// Apply static abilities and prune dead tokens
final Game game = ai.getGame();
ComputerUtilCard.applyStaticContPT(game, token, null);
if (!notNull && token.getNetToughness() < 1) {
if (!notNull && token.isCreature() && token.getNetToughness() < 1) {
return null;
} else {
return token;

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.11-SNAPSHOT</version>
<version>1.6.16-SNAPSHOT</version>
</parent>
<artifactId>forge-core</artifactId>
@@ -16,7 +16,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>24.1-jre</version>
<version>24.1-android</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@@ -24,4 +24,31 @@
<version>3.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>checkstyle-validation</id>
<phase>validate</phase>
<configuration>
<configLocation>../checkstyle.xml</configLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<failOnViolation>true</failOnViolation>
</configuration>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -413,6 +413,9 @@ public class CardStorageReader {
return reader.readCard(lines, Files.getNameWithoutExtension(file.getName()));
} catch (final FileNotFoundException ex) {
throw new RuntimeException("CardReader : run error -- file not found: " + file.getPath(), ex);
} catch (final Exception ex) {
System.out.println("Error loading cardscript " + file.getName() + ". Please close Forge and resolve this.");
throw ex;
} finally {
try {
assert fileInputStream != null;

View File

@@ -37,6 +37,8 @@ public class StaticData {
private Predicate<PaperCard> brawlPredicate;
private Predicate<PaperCard> modernPredicate;
private boolean filteredHandsEnabled = false;
// Loaded lazily:
private IStorage<SealedProduct.Template> boosters;
private IStorage<SealedProduct.Template> specialBoosters;
@@ -209,6 +211,14 @@ public class StaticData {
return brawlPredicate;
}
public void setFilteredHandsEnabled(boolean filteredHandsEnabled){
this.filteredHandsEnabled = filteredHandsEnabled;
}
public boolean getFilteredHandsEnabled(){
return filteredHandsEnabled;
}
public PaperCard getCardByEditionDate(PaperCard card, Date editionDate) {
PaperCard c = this.getCommonCards().getCardFromEdition(card.getName(), editionDate, CardDb.SetPreference.LatestCoreExp, card.getArtIndex());

View File

@@ -32,19 +32,24 @@ public class CardChangedType {
private final boolean removeSuperTypes;
private final boolean removeCardTypes;
private final boolean removeSubTypes;
private final boolean removeLandTypes;
private final boolean removeCreatureTypes;
private final boolean removeArtifactTypes;
private final boolean removeEnchantmentTypes;
public CardChangedType(final CardType addType0, final CardType removeType0, final boolean removeSuperType0,
final boolean removeCardType0, final boolean removeSubType0, final boolean removeCreatureType0,
final boolean removeArtifactType0) {
final boolean removeCardType0, final boolean removeSubType0, final boolean removeLandType0,
final boolean removeCreatureType0, final boolean removeArtifactType0,
final boolean removeEnchantmentTypes0) {
addType = addType0;
removeType = removeType0;
removeSuperTypes = removeSuperType0;
removeCardTypes = removeCardType0;
removeSubTypes = removeSubType0;
removeLandTypes = removeLandType0;
removeCreatureTypes = removeCreatureType0;
removeArtifactTypes = removeArtifactType0;
removeEnchantmentTypes = removeEnchantmentTypes0;
}
public final CardType getAddType() {
@@ -67,6 +72,10 @@ public class CardChangedType {
return removeSubTypes;
}
public final boolean isRemoveLandTypes() {
return removeLandTypes;
}
public final boolean isRemoveCreatureTypes() {
return removeCreatureTypes;
}
@@ -74,4 +83,8 @@ public class CardChangedType {
public final boolean isRemoveArtifactTypes() {
return removeArtifactTypes;
}
public final boolean isRemoveEnchantmentTypes() {
return removeEnchantmentTypes;
}
}

View File

@@ -123,12 +123,19 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
private boolean smallSetOverride = false;
private String boosterMustContain = "";
private final CardInSet[] cards;
private final String[] tokenNormalized;
private int boosterArts = 1;
private SealedProduct.Template boosterTpl = null;
private CardEdition(CardInSet[] cards) {
this.cards = cards;
tokenNormalized = null;
}
private CardEdition(CardInSet[] cards, String[] tokens) {
this.cards = cards;
this.tokenNormalized = tokens;
}
/**
@@ -254,6 +261,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
protected CardEdition read(File file) {
final Map<String, List<String>> contents = FileSection.parseSections(FileUtil.readFile(file));
List<String> tokenNormalized = new ArrayList<>();
List<CardEdition.CardInSet> processedCards = new ArrayList<>();
if (contents.containsKey("cards")) {
for(String line : contents.get("cards")) {
@@ -277,7 +285,19 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
}
}
CardEdition res = new CardEdition(processedCards.toArray(new CardInSet[processedCards.size()]));
if (contents.containsKey("tokens")) {
for(String line : contents.get("tokens")) {
if (StringUtils.isBlank(line))
continue;
tokenNormalized.add(line);
}
}
CardEdition res = new CardEdition(
processedCards.toArray(new CardInSet[processedCards.size()]),
tokenNormalized.toArray(new String[tokenNormalized.size()])
);
FileSection section = FileSection.parse(contents.get("metadata"), "=");
res.name = section.get("name");

View File

@@ -87,21 +87,27 @@ final class CardFace implements ICardFace {
void setInitialLoyalty(int value) { this.initialLoyalty = value; }
void setPtText(String value) {
final int slashPos = value.indexOf('/');
if (slashPos == -1) {
final String k[] = value.split("/");
if (k.length != 2) {
throw new RuntimeException("Creature '" + this.getName() + "' has bad p/t stats");
}
boolean negPower = value.charAt(0) == '-';
boolean negToughness = value.charAt(slashPos + 1) == '-';
this.power = negPower ? value.substring(1, slashPos) : value.substring(0, slashPos);
this.toughness = negToughness ? value.substring(slashPos + 2) : value.substring(slashPos + 1);
this.power = k[0];
this.toughness = k[1];
this.iPower = StringUtils.isNumeric(this.power) ? Integer.parseInt(this.power) : 0;
this.iToughness = StringUtils.isNumeric(this.toughness) ? Integer.parseInt(this.toughness) : 0;
this.iPower = parsePT(k[0]);
this.iToughness = parsePT(k[1]);
}
if (negPower) { this.iPower *= -1; }
if (negToughness) { this.iToughness *= -1; }
static int parsePT(String val) {
// normalize PT value
if (val.contains("*")) {
val = val.replace("+*", "");
val = val.replace("-*", "");
val = val.replace("*", "0");
}
return Integer.parseInt(val);
}
// Raw fields used for Card creation

View File

@@ -76,6 +76,51 @@ public final class CardFacePredicates {
};
}
static class ValidPredicate implements Predicate<ICardFace> {
private String valid;
public ValidPredicate(final String valid) {
this.valid = valid;
}
@Override
public boolean apply(ICardFace input) {
String k[] = valid.split("\\.", 2);
if ("Card".equals(k[0])) {
// okay
} else if ("Permanent".equals(k[0])) {
if (input.getType().isInstant() || input.getType().isSorcery()) {
return false;
}
} else if (!input.getType().hasStringType(k[0])) {
return false;
}
if (k.length > 1) {
for (final String m : k[1].split("\\+")) {
if (!hasProperty(input, m)) {
return false;
}
}
}
return true;
}
static protected boolean hasProperty(ICardFace input, final String v) {
if (v.startsWith("non")) {
return !hasProperty(input, v.substring(3));
} else if (!input.getType().hasStringType(v)) {
return false;
}
return true;
}
}
public static Predicate<ICardFace> valid(final String val) {
return new ValidPredicate(val);
}
public static class Presets {
/** The Constant isBasicLand. */
public static final Predicate<ICardFace> IS_BASIC_LAND = new Predicate<ICardFace>() {

View File

@@ -41,6 +41,7 @@ public final class CardRules implements ICardCharacteristics {
private CardAiHints aiHints;
private ColorSet colorIdentity;
private String meldWith;
private String partnerWith;
private CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
splitType = altMode;
@@ -48,6 +49,7 @@ public final class CardRules implements ICardCharacteristics {
otherPart = faces[1];
aiHints = cah;
meldWith = "";
partnerWith = "";
//calculate color identity
byte colMask = calculateColorIdentity(mainPart);
@@ -68,6 +70,7 @@ public final class CardRules implements ICardCharacteristics {
aiHints = newRules.aiHints;
colorIdentity = newRules.colorIdentity;
meldWith = newRules.meldWith;
partnerWith = newRules.partnerWith;
}
private static byte calculateColorIdentity(final ICardFace face) {
@@ -204,7 +207,7 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean canBePartnerCommander() {
return canBeCommander() && Iterables.contains(mainPart.getKeywords(), "Partner");
return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty());
}
public boolean canBeBrawlCommander() {
@@ -216,6 +219,10 @@ public final class CardRules implements ICardCharacteristics {
return meldWith;
}
public String getParterWith() {
return partnerWith;
}
// vanguard card fields, they don't use sides.
private int deltaHand;
private int deltaLife;
@@ -262,6 +269,7 @@ public final class CardRules implements ICardCharacteristics {
private int curFace = 0;
private CardSplitType altMode = CardSplitType.None;
private String meldWith = "";
private String partnerWith = "";
private String handLife = null;
private String normalizedName = "";
@@ -291,6 +299,7 @@ public final class CardRules implements ICardCharacteristics {
this.hints = null;
this.has = null;
this.meldWith = "";
this.partnerWith = "";
this.normalizedName = "";
}
@@ -307,6 +316,7 @@ public final class CardRules implements ICardCharacteristics {
result.setNormalizedName(this.normalizedName);
result.meldWith = this.meldWith;
result.partnerWith = this.partnerWith;
result.setDlUrls(pictureUrl);
if (StringUtils.isNotBlank(handLife))
result.setVanguardProperties(handLife);
@@ -382,6 +392,9 @@ public final class CardRules implements ICardCharacteristics {
case 'K':
if ("K".equals(key)) {
this.faces[this.curFace].addKeyword(value);
if (value.startsWith("Partner:")) {
this.partnerWith = value.split(":")[1];
}
}
break;
@@ -533,4 +546,8 @@ public final class CardRules implements ICardCharacteristics {
return result;
}
public boolean hasKeyword(final String k) {
return Iterables.contains(mainPart.getKeywords(), k);
}
}

View File

@@ -10,7 +10,6 @@ import com.google.common.collect.Iterables;
import forge.util.ComparableOp;
import forge.util.PredicateString;
import forge.util.PredicateString.StringOp;
/**
* Filtering conditions specific for CardRules class, defined here along with
@@ -558,6 +557,19 @@ public final class CardRulesPredicates {
}
};
public static final Predicate<CardRules> CAN_BE_COMMANDER = new Predicate<CardRules>() {
@Override
public boolean apply(final CardRules subject) {
return subject.canBeCommander();
}
};
public static final Predicate<CardRules> CAN_BE_PARTNER_COMMANDER = new Predicate<CardRules>() {
@Override
public boolean apply(final CardRules subject) {
return subject.canBePartnerCommander();
}
};
public static final Predicate<CardRules> IS_PLANESWALKER = CardRulesPredicates.coreType(true, CardType.CoreType.Planeswalker);
public static final Predicate<CardRules> IS_INSTANT = CardRulesPredicates.coreType(true, CardType.CoreType.Instant);
public static final Predicate<CardRules> IS_SORCERY = CardRulesPredicates.coreType(true, CardType.CoreType.Sorcery);
@@ -570,13 +582,10 @@ public final class CardRulesPredicates {
public static final Predicate<CardRules> IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy);
public static final Predicate<CardRules> IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land);
public static final Predicate<CardRules> IS_NON_CREATURE_SPELL = Predicates.not(Predicates.or(Presets.IS_CREATURE, Presets.IS_LAND));
public static final Predicate<CardRules> CAN_BE_COMMANDER = Predicates.or(CardRulesPredicates.rules(StringOp.CONTAINS_IC, "can be your commander"),
Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY));
public static final Predicate<CardRules> CAN_BE_BRAWL_COMMANDER = Predicates.or(Presets.IS_PLANESWALKER,
Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY));
/** The Constant IS_NONCREATURE_SPELL_FOR_GENERATOR. **/
@SuppressWarnings("unchecked")
public static final Predicate<CardRules> IS_NONCREATURE_SPELL_FOR_GENERATOR = com.google.common.base.Predicates
.or(Presets.IS_SORCERY, Presets.IS_INSTANT, Presets.IS_PLANESWALKER, Presets.IS_ENCHANTMENT,
Predicates.and(Presets.IS_ARTIFACT, Predicates.not(Presets.IS_CREATURE)));

View File

@@ -190,7 +190,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
public boolean setCreatureTypes(Collection<String> ctypes) {
// if it isn't a creature then this has no effect
if (!coreTypes.contains(CoreType.Creature)) {
if (!isCreature() && !isTribal()) {
return false;
}
boolean changed = Iterables.removeIf(subtypes, Predicates.IS_CREATURE_TYPE);
@@ -236,7 +236,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
final Set<String> landTypes = Sets.newHashSet();
if (isLand()) {
for (final String t : subtypes) {
if (isALandType(t) || isABasicLandType(t)) {
if (isALandType(t)) {
landTypes.add(t);
}
}
@@ -435,6 +435,9 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
@Override
public CardTypeView getTypeWithChanges(final Iterable<CardChangedType> changedCardTypes) {
CardType newType = null;
if (Iterables.isEmpty(changedCardTypes)) {
return this;
}
// we assume that changes are already correctly ordered (taken from TreeMap.values())
for (final CardChangedType ct : changedCardTypes) {
if(null == newType)
@@ -449,7 +452,10 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
if (ct.isRemoveSubTypes()) {
newType.subtypes.clear();
}
else {
else if (!newType.subtypes.isEmpty()) {
if (ct.isRemoveLandTypes()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_LAND_TYPE);
}
if (ct.isRemoveCreatureTypes()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_CREATURE_TYPE);
// need to remove AllCreatureTypes too when removing creature Types
@@ -458,6 +464,9 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
if (ct.isRemoveArtifactTypes()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_ARTIFACT_TYPE);
}
if (ct.isRemoveEnchantmentTypes()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_ENCHANTMENT_TYPE);
}
}
if (ct.getRemoveType() != null) {
newType.removeAll(ct.getRemoveType());
@@ -466,6 +475,28 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
newType.addAll(ct.getAddType());
}
}
// sanisfy subtypes
if (newType != null && !newType.subtypes.isEmpty()) {
if (!newType.isCreature() && !newType.isTribal()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_CREATURE_TYPE);
newType.subtypes.remove("AllCreatureTypes");
}
if (!newType.isLand()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_LAND_TYPE);
}
if (!newType.isArtifact()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_ARTIFACT_TYPE);
}
if (!newType.isEnchantment()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_ENCHANTMENT_TYPE);
}
if (!newType.isInstant() && !newType.isSorcery()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_SPELL_TYPE);
}
if (!newType.isPlaneswalker() && !newType.isEmblem()) {
Iterables.removeIf(newType.subtypes, Predicates.IS_WALKER_TYPE);
}
}
return newType == null ? this : newType;
}
@@ -574,6 +605,13 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
public static final BiMap<String,String> singularTypes = pluralTypes.inverse();
}
public static class Predicates {
public static Predicate<String> IS_LAND_TYPE = new Predicate<String>() {
@Override
public boolean apply(String input) {
return CardType.isALandType(input);
}
};
public static Predicate<String> IS_ARTIFACT_TYPE = new Predicate<String>() {
@Override
public boolean apply(String input) {
@@ -587,6 +625,27 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return CardType.isACreatureType(input);
}
};
public static Predicate<String> IS_ENCHANTMENT_TYPE = new Predicate<String>() {
@Override
public boolean apply(String input) {
return CardType.isAnEnchantmentType(input);
}
};
public static Predicate<String> IS_SPELL_TYPE = new Predicate<String>() {
@Override
public boolean apply(String input) {
return CardType.isASpellType(input);
}
};
public static Predicate<String> IS_WALKER_TYPE = new Predicate<String>() {
@Override
public boolean apply(String input) {
return CardType.isAPlaneswalkerType(input);
}
};
}
@@ -656,7 +715,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
}
public static boolean isALandType(final String cardType) {
return (Constant.LAND_TYPES.contains(cardType));
return Constant.LAND_TYPES.contains(cardType) || isABasicLandType(cardType);
}
public static boolean isAPlaneswalkerType(final String cardType) {
@@ -667,6 +726,13 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return (Constant.BASIC_TYPES.contains(cardType));
}
public static boolean isAnEnchantmentType(final String cardType) {
return (Constant.ENCHANTMENT_TYPES.contains(cardType));
}
public static boolean isASpellType(final String cardType) {
return (Constant.SPELL_TYPES.contains(cardType));
}
/**
* If the input is a plural type, return the corresponding singular form.

View File

@@ -291,17 +291,13 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return true;
}
@Override
public void importDeck(Deck deck) {
deck.loadDeferredSections();
for (DeckSection section: deck.parts.keySet()) {
this.putSection(section, deck.get(section));
}
}
@Override
public String getImageKey(boolean altState) {
return null;
}
}
@Override
public Deck getHumanDeck() {
return this;
}
}

View File

@@ -6,22 +6,20 @@
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.deck;
import forge.item.InventoryItem;
import java.io.Serializable;
public abstract class DeckBase implements Serializable, Comparable<DeckBase>, InventoryItem {
private static final long serialVersionUID = -7538150536939660052L;
// gameType is from Constant.GameType, like GameType.Regular
@@ -59,7 +57,7 @@ public abstract class DeckBase implements Serializable, Comparable<DeckBase>, In
/*
* (non-Javadoc)
*
*
* @see java.lang.Object#hashCode()
*/
@Override
@@ -74,6 +72,7 @@ public abstract class DeckBase implements Serializable, Comparable<DeckBase>, In
public String getDirectory() {
return directory;
}
public void setDirectory(String directory0) {
directory = directory0;
}
@@ -101,7 +100,7 @@ public abstract class DeckBase implements Serializable, Comparable<DeckBase>, In
* <p>
* getComment.
* </p>
*
*
* @return a {@link java.lang.String} object.
*/
public String getComment() {
@@ -149,5 +148,5 @@ public abstract class DeckBase implements Serializable, Comparable<DeckBase>, In
public abstract boolean isEmpty();
public abstract void importDeck(Deck deck);
public abstract Deck getHumanDeck();
}

View File

@@ -248,41 +248,33 @@ public enum DeckFormat {
return "too many commanders";
}
// Bring values up to 100
min++;
max++;
byte cmdCI = 0;
Boolean hasPartner = null;
for (PaperCard pc : commanders) {
// For each commander decrement size by 1 (99 for 1, 98 for 2)
min--;
max--;
if (!isLegalCommander(pc.getRules())) {
return "has an illegal commander";
}
if (hasPartner != null && !hasPartner) {
return "has an illegal commander partnership";
}
boolean isPartner = false;
for(String s : pc.getRules().getMainPart().getKeywords()) {
if (s.equals("Partner")) {
isPartner = true;
break;
}
}
if (hasPartner == null) {
hasPartner = isPartner;
} else if (!isPartner) {
return "has an illegal commander partnership";
}
cmdCI |= pc.getRules().getColorIdentity().getColor();
}
// special check for Partner
if (commanders.size() == 2) {
// two commander = 98 cards
min--;
max--;
PaperCard a = commanders.get(0);
PaperCard b = commanders.get(1);
if (a.getRules().hasKeyword("Partner") && b.getRules().hasKeyword("Partner")) {
// normal partner commander
} else if (a.getName().equals(b.getRules().getParterWith())
&& b.getName().equals(a.getRules().getParterWith())) {
// paired partner commander
} else {
return "has an illegal commander partnership";
}
}
final List<PaperCard> erroneousCI = new ArrayList<PaperCard>();
Set<String> basicLandNames = new HashSet<>();
@@ -521,6 +513,8 @@ public enum DeckFormat {
for (final PaperCard p : commanders) {
cmdCI |= p.getRules().getColorIdentity().getColor();
}
return Predicates.compose(Predicates.or(CardRulesPredicates.hasColorIdentity(cmdCI), CardRulesPredicates.hasKeyword("Partner")), PaperCard.FN_GET_RULES);
// TODO : check commander what kind of Partner it needs
return Predicates.compose(Predicates.or(CardRulesPredicates.hasColorIdentity(cmdCI),
CardRulesPredicates.Presets.CAN_BE_PARTNER_COMMANDER), PaperCard.FN_GET_RULES);
}
}

View File

@@ -18,9 +18,6 @@
package forge.deck;
import com.google.common.base.Function;
import forge.StaticData;
import forge.item.PaperCard;
import java.util.*;
/**
@@ -47,7 +44,8 @@ public class DeckGroup extends DeckBase {
*
* @return the human deck
*/
public final Deck getHumanDeck() {
@Override
public Deck getHumanDeck() {
return humanDeck;
}
@@ -160,100 +158,4 @@ public class DeckGroup extends DeckBase {
public String getImageKey(boolean altState) {
return null;
}
@Override
public void importDeck(Deck deck) {
CardPool draftedCards = this.getHumanDeck().getAllCardsInASinglePool(false);
this.getHumanDeck().putSection(DeckSection.Main, new CardPool());
this.getHumanDeck().putSection(DeckSection.Sideboard, new CardPool());
HashMap<String, Integer> countByName = getCountByName(deck);
addFromDraftedCardPool(countByName, draftedCards);
addBasicLands(deck, countByName, draftedCards);
}
private HashMap<String, Integer> getCountByName(Deck deck) {
HashMap<String, Integer> result = new HashMap<String, Integer>();
for (Map.Entry<PaperCard, Integer> entry: deck.getMain()) {
PaperCard importedCard = entry.getKey();
Integer previousCount = result.getOrDefault(importedCard.getName(), 0);
int countToAdd = entry.getValue();
result.put(importedCard.getName(), countToAdd + previousCount);
}
return result;
}
private void addFromDraftedCardPool(HashMap<String, Integer> countByName, CardPool availableCards) {
for (Map.Entry<PaperCard, Integer> entry: availableCards) {
PaperCard availableCard = entry.getKey();
Integer availableCount = entry.getValue();
int countToAdd = countByName.getOrDefault(availableCard.getName(), 0);
if (availableCard.getRules().getType().isBasicLand()) {
// basic lands are added regardless from drafted cards
continue;
}
int countMain = Math.min(availableCount, countToAdd);
if (countMain > 0) {
this.getHumanDeck().getMain().add(availableCard, countMain);
countByName.put(availableCard.getName(), countToAdd - countMain);
}
int countSideboard = availableCount - countMain;
if (countSideboard > 0) {
CardPool sideboard = this.getHumanDeck().getOrCreate(DeckSection.Sideboard);
sideboard.add(availableCard, countSideboard);
}
}
}
private void addBasicLands(Deck deck, HashMap<String, Integer> countByName, CardPool availableCards) {
HashMap<String, PaperCard> basicLandsByName = getBasicLandsByName(deck, countByName);
Date dateWithAllCards = StaticData.instance().getEditions().getEarliestDateWithAllCards(availableCards);
for (String cardName: countByName.keySet()) {
PaperCard card = basicLandsByName.getOrDefault(cardName, null);
if (card == null) {
continue;
}
int countToAdd = countByName.get(cardName);
card = StaticData.instance().getCardByEditionDate(card, dateWithAllCards);
this.getHumanDeck().getMain().add(card.getName(), card.getEdition(), countToAdd);
}
}
private HashMap<String, PaperCard> getBasicLandsByName(Deck deck, HashMap<String, Integer> countByName) {
HashMap<String, PaperCard> result = new HashMap<String, PaperCard>();
for (Map.Entry<PaperCard, Integer> entry: deck.getMain()) {
PaperCard card = entry.getKey();
if (!card.getRules().getType().isBasicLand()) {
continue;
}
if (result.containsKey(card.getName())) {
continue;
}
result.put(card.getName(), card);
}
return result;
}
}

View File

@@ -21,9 +21,9 @@ public enum DeckSection {
return null;
}
final String valToCompate = value.trim();
final String valToCompare = value.trim();
for (final DeckSection v : DeckSection.values()) {
if (v.name().compareToIgnoreCase(valToCompate) == 0) {
if (v.name().compareToIgnoreCase(valToCompare) == 0) {
return v;
}
}

View File

@@ -137,6 +137,6 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override
public String getImageKey(boolean altState) {
return ImageKeys.TOKEN_PREFIX + imageFileName;
return ImageKeys.TOKEN_PREFIX + imageFileName.replace(" ", "_");
}
}

View File

@@ -39,13 +39,19 @@ public class TokenDb implements ITokenDatabase {
@Override
public PaperToken getToken(String tokenName, String edition) {
try {
PaperToken pt = new PaperToken(rulesByName.get(tokenName), editions.get(edition));
// TODO Cache the token after it's referenced
return pt;
} catch(Exception e) {
return null;
String fullName = String.format("%s_%s", tokenName, edition.toLowerCase());
if (!tokensByName.containsKey(fullName)) {
try {
PaperToken pt = new PaperToken(rulesByName.get(tokenName), editions.get(edition));
tokensByName.put(fullName, pt);
return pt;
} catch(Exception e) {
return null;
}
}
return tokensByName.get(fullName);
}
@Override

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.11-SNAPSHOT</version>
<version>1.6.16-SNAPSHOT</version>
</parent>
<artifactId>forge-game</artifactId>
@@ -30,5 +30,37 @@
<scope>test</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-log4j</artifactId>
<version>1.7.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>checkstyle-validation</id>
<phase>validate</phase>
<configuration>
<configLocation>../checkstyle.xml</configLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<failOnViolation>true</failOnViolation>
</configuration>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -314,7 +314,11 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView {
list.addAll(p.getCardsIn(presentZone));
}
}
if (presentPlayer.equals("Any")) {
for (final Player p : this.getHostCard().getController().getAllies()) {
list.addAll(p.getCardsIn(presentZone));
}
}
list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), null);
int right = 1;

View File

@@ -168,6 +168,10 @@ public class ForgeScript {
if (!sa.isFlashBackAbility()) {
return false;
}
} else if (property.equals("Jumpstart")) {
if (!sa.isJumpstart()) {
return false;
}
} else if (property.equals("Kicked")) {
if (!sa.isKicked()) {
return false;
@@ -204,6 +208,12 @@ public class ForgeScript {
if (!found) {
return false;
}
} else if (property.equals("YouCtrl")) {
return sa.getActivatingPlayer().equals(sourceController);
} else if (sa.getHostCard() != null) {
if (!sa.getHostCard().hasProperty(property, sourceController, source, spellAbility)) {
return false;
}
}
return true;

View File

@@ -20,6 +20,7 @@ package forge.game;
import com.google.common.base.Predicate;
import com.google.common.collect.*;
import forge.GameCommand;
import forge.StaticData;
import forge.card.CardStateName;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
@@ -150,8 +151,9 @@ public class GameAction {
// Cards returned from exile face-down must be reset to their original state, otherwise
// all sort of funky shenanigans may happen later (e.g. their ETB replacement effects are set
// up on the wrong card state etc.).
if (zoneTo.is(ZoneType.Hand) && zoneFrom.is(ZoneType.Exile) && c.isFaceDown()) {
if (c.isFaceDown() && (fromBattlefield || (toHand && zoneFrom.is(ZoneType.Exile)))) {
c.setState(CardStateName.Original, true);
c.runFaceupCommands();
}
// Clean up the temporary Dash SVar when the Dashed card leaves the battlefield
@@ -190,7 +192,7 @@ public class GameAction {
if (!c.isToken()) {
if (c.isCloned()) {
c.switchStates(CardStateName.Cloner, CardStateName.Original, false);
c.switchStates(CardStateName.Original, CardStateName.Cloner, false);
c.setState(CardStateName.Original, false);
c.clearStates(CardStateName.Cloner, false);
if (c.isFlipCard()) {
@@ -250,11 +252,24 @@ public class GameAction {
Card noLandLKI = CardUtil.getLKICopy(c);
// this check needs to check if this card would be on the battlefield
noLandLKI.setLastKnownZone(zoneTo);
CardCollection preList = new CardCollection(noLandLKI);
checkStaticAbilities(false, Sets.newHashSet(noLandLKI), preList);
// fake etb counters thing, then if something changed,
// need to apply checkStaticAbilities again
if(!noLandLKI.isLand()) {
if (noLandLKI.putEtbCounters()) {
// counters are added need to check again
checkStaticAbilities(false, Sets.newHashSet(noLandLKI), preList);
}
}
if(noLandLKI.isLand()) {
// if it isn't on the Stack, it stays in that Zone
if (!c.getZone().is(ZoneType.Stack)) {
return c;
}
// if something would only be a land when entering the battlefield and not before
// put it into the graveyard instead
zoneTo = c.getOwner().getZone(ZoneType.Graveyard);
@@ -262,6 +277,9 @@ public class GameAction {
copied.setState(CardStateName.Original, false);
copied.setManifested(false);
copied.updateStateForView();
// not to battlefield anymore!
toBattlefield = false;
}
}
@@ -357,7 +375,10 @@ public class GameAction {
// do ETB counters after StaticAbilities check
if (!suppress) {
if (toBattlefield) {
copied.putEtbCounters();
if (copied.putEtbCounters()) {
// if counter where put of card, call checkStaticAbilities again
checkStaticAbilities();
}
}
copied.clearEtbCounters();
}
@@ -367,6 +388,7 @@ public class GameAction {
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Card", lastKnownInfo);
runParams.put("Cause", cause);
runParams.put("Origin", zoneFrom != null ? zoneFrom.getZoneType().name() : null);
runParams.put("Destination", zoneTo.getZoneType().name());
runParams.put("SpellAbilityStackInstance", game.stack.peek());
@@ -873,9 +895,13 @@ public class GameAction {
}
}
if (runEvents) {
// preList means that this is run by a pre Check with LKI objects
// in that case Always trigger should not Run
if (preList.isEmpty()) {
final Map<String, Object> runParams = Maps.newHashMap();
game.getTriggerHandler().runTrigger(TriggerType.Always, runParams, false);
game.getTriggerHandler().runTrigger(TriggerType.Immediate, runParams, false);
}
// Update P/T and type in the view only once after all the cards have been processed, to avoid flickering
@@ -1562,6 +1588,53 @@ public class GameAction {
}
}
private void drawStartingHand(Player p1){
//check initial hand
List<Card> lib1 = Lists.newArrayList(p1.getZone(ZoneType.Library).getCards().threadSafeIterable());
List<Card> hand1 = lib1.subList(0,p1.getMaxHandSize());
System.out.println(hand1.toString());
//shuffle
List<Card> shuffledCards = Lists.newArrayList(p1.getZone(ZoneType.Library).getCards().threadSafeIterable());
Collections.shuffle(shuffledCards);
//check a second hand
List<Card> hand2 = shuffledCards.subList(0,p1.getMaxHandSize());
System.out.println(hand2.toString());
//choose better hand according to land count
float averageLandRatio = getLandRatio(lib1);
if(getHandScore(hand1, averageLandRatio)>getHandScore(hand2, averageLandRatio)){
p1.getZone(ZoneType.Library).setCards(shuffledCards);
}
p1.drawCards(p1.getMaxHandSize());
}
private float getLandRatio(List<Card> deck){
int landCount = 0;
for(Card c:deck){
if(c.isLand()){
landCount++;
}
}
if (landCount == 0 ){
return 0;
}
return Float.valueOf(landCount)/Float.valueOf(deck.size());
}
private float getHandScore(List<Card> hand, float landRatio){
int landCount = 0;
for(Card c:hand){
if(c.isLand()){
landCount++;
}
}
float averageCount = landRatio * hand.size();
return Math.abs(averageCount-landCount);
}
public void startGame(GameOutcome lastGameOutcome) {
startGame(lastGameOutcome, null);
}
@@ -1582,7 +1655,11 @@ public class GameAction {
game.setAge(GameStage.Mulligan);
for (final Player p1 : game.getPlayers()) {
p1.drawCards(p1.getMaxHandSize());
if (StaticData.instance().getFilteredHandsEnabled() ) {
drawStartingHand(p1);
} else {
p1.drawCards(p1.getStartingHandSize());
}
// If pl has Backup Plan as a Conspiracy draw that many extra hands
@@ -1702,6 +1779,11 @@ public class GameAction {
boolean isMultiPlayer = game.getPlayers().size() > 2;
int mulliganDelta = isMultiPlayer ? 0 : 1;
// https://magic.wizards.com/en/articles/archive/feature/checking-brawl-2018-07-09
if (game.getRules().hasAppliedVariant(GameType.Brawl) && !isMultiPlayer){
mulliganDelta = 0;
}
boolean allKept;
do {
allKept = true;
@@ -1766,7 +1848,7 @@ public class GameAction {
//Vancouver Mulligan
for(Player p : whoCanMulligan) {
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
p.scry(1);
p.scry(1, null);
}
}
}

View File

@@ -109,7 +109,7 @@ public final class GameActionUtil {
final SpellAbility newSA = sa.copy(activator);
final SpellAbilityRestriction sar = newSA.getRestrictions();
if (o.isWithFlash()) {
sar.setInstantSpeed(true);
sar.setInstantSpeed(true);
}
sar.setZone(null);
newSA.setMayPlay(o.getAbility());
@@ -174,7 +174,8 @@ public final class GameActionUtil {
}
if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) {
final SpellAbility newSA = sa.copyWithNoManaCost();
// set the cost to this directly to buypass non mana cost
final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>");
newSA.setBasicSpell(false);
newSA.getMapParams().put("CostDesc", ManaCostParser.parse("0"));
// makes new SpellDescription
@@ -186,6 +187,15 @@ public final class GameActionUtil {
alternatives.add(newSA);
}
if (sa.hasParam("Equip") && activator.hasKeyword("EquipInstantSpeed")) {
final SpellAbility newSA = sa.copy(activator);
SpellAbilityRestriction sar = newSA.getRestrictions();
sar.setSorcerySpeed(false);
sar.setInstantSpeed(true);
newSA.setDescription(sa.getDescription() + " (you may activate any time you could cast an instant )");
alternatives.add(newSA);
}
for (final KeywordInterface inst : source.getKeywords()) {
final String keyword = inst.getOriginal();
if (sa.isSpell() && keyword.startsWith("Flashback")) {
@@ -201,20 +211,12 @@ public final class GameActionUtil {
flashback.getRestrictions().setZone(ZoneType.Graveyard);
// there is a flashback cost (and not the cards cost)
if (!keyword.equals("Flashback")) {
flashback.setPayCosts(new Cost(keyword.substring(10), false));
if (keyword.contains(":")) {
final String k[] = keyword.split(":");
flashback.setPayCosts(new Cost(k[1], false));
}
alternatives.add(flashback);
}
if (sa.hasParam("Equip") && sa instanceof AbilityActivated && keyword.equals("EquipInstantSpeed")) {
final SpellAbility newSA = sa.copy(activator);
SpellAbilityRestriction sar = newSA.getRestrictions();
sar.setSorcerySpeed(false);
sar.setInstantSpeed(true);
newSA.setDescription(sa.getDescription() + " (you may activate any time you could cast an instant )");
alternatives.add(newSA);
}
}
return alternatives;
}
@@ -259,6 +261,11 @@ public final class GameActionUtil {
final Cost cost = new Cost("Discard<1/Land>", false);
costs.add(new OptionalCostValue(OptionalCost.Retrace, cost));
}
} else if (keyword.equals("Jump-start")) {
if (source.getZone().is(ZoneType.Graveyard)) {
final Cost cost = new Cost("Discard<1/Card>", false);
costs.add(new OptionalCostValue(OptionalCost.Jumpstart, cost));
}
} else if (keyword.startsWith("MayFlashCost")) {
String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false);
@@ -286,6 +293,7 @@ public final class GameActionUtil {
result.addConspireInstance();
break;
case Retrace:
case Jumpstart:
result.getRestrictions().setZone(ZoneType.Graveyard);
break;
case Flash:
@@ -389,8 +397,8 @@ public final class GameActionUtil {
}
}
} else if (keyword.startsWith("Kicker")) {
String[] sCosts = TextUtil.split(keyword.substring(6), ':');
boolean generic = "Generic".equals(sCosts[sCosts.length - 1]);
String[] sCosts = TextUtil.split(keyword.substring(6), ':');
boolean generic = "Generic".equals(sCosts[sCosts.length - 1]);
// If this is a "generic kicker" (Undergrowth), ignore value for kicker creations
int numKickers = sCosts.length - (generic ? 1 : 0);
for (int i = 0; i < abilities.size(); i++) {

View File

@@ -25,7 +25,9 @@ import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment;
import forge.game.event.GameEventCardAttachment.AttachMethod;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.trigger.TriggerType;
import forge.util.collect.FCollection;
@@ -337,6 +339,17 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
}
}
public boolean canBeEnchantedBy(final Card aura) {
SpellAbility sa = aura.getFirstAttachSpell();
TargetRestrictions tgt = null;
if (sa != null) {
tgt = sa.getTargetRestrictions();
}
return !(hasProtectionFrom(aura)
|| ((tgt != null) && !isValid(tgt.getValidTgts(), aura.getController(), aura, sa)));
}
public abstract boolean hasProtectionFrom(final Card source);
// Counters!
@@ -365,7 +378,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
abstract public void setCounters(final Map<CounterType, Integer> allCounters);
abstract public boolean canReceiveCounters(final CounterType type);
abstract public void addCounter(final CounterType counterType, final int n, final Card source, final boolean applyMultiplier, final boolean fireEvents);
abstract public int addCounter(final CounterType counterType, final int n, final Player source, final boolean applyMultiplier, final boolean fireEvents);
abstract public void subtractCounter(final CounterType counterName, final int n);
abstract public void clearCounters();

View File

@@ -24,6 +24,7 @@ import forge.game.event.GameEventPlayerPoisoned;
import forge.game.event.GameEventScry;
import forge.game.event.GameEventSpellAbilityCast;
import forge.game.event.GameEventSpellResolved;
import forge.game.event.GameEventSurveil;
import forge.game.event.GameEventTurnBegan;
import forge.game.event.GameEventTurnPhase;
import forge.game.event.IGameEventVisitor;
@@ -65,7 +66,24 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome);
}
@Override
public GameLogEntry visit(GameEventSurveil ev) {
String surveilOutcome = "";
String toLibrary = Lang.nounWithAmount(ev.toLibrary, "card") + " to the top of the library";
String toGraveyard = Lang.nounWithAmount(ev.toGraveyard, "card") + " to the graveyard";
if (ev.toLibrary > 0 && ev.toGraveyard > 0) {
surveilOutcome = ev.player.toString() + " surveiled " + toLibrary + " and " + toGraveyard;
} else if (ev.toGraveyard == 0) {
surveilOutcome = ev.player.toString() + " surveiled " + toLibrary;
} else {
surveilOutcome = ev.player.toString() + " surveiled " + toGraveyard;
}
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, surveilOutcome);
}
@Override
public GameLogEntry visit(GameEventSpellResolved ev) {
String messageForLog = ev.hasFizzled ? ev.spell.getHostCard().getName() + " ability fizzles." : ev.spell.getStackDescription();

View File

@@ -58,9 +58,6 @@ public class StaticEffect {
private String chosenType;
private Map<String, String> mapParams = Maps.newTreeMap();
// for P/T
private final Map<Card, String> originalPT = Maps.newTreeMap();
// for types
private boolean overwriteTypes = false;
private boolean keepSupertype = false;
@@ -101,7 +98,6 @@ public class StaticEffect {
copy.xValueMap = this.xValueMap;
copy.chosenType = this.chosenType;
copy.mapParams = this.mapParams;
map.fillKeyedMap(copy.originalPT, this.originalPT);
copy.overwriteTypes = this.overwriteTypes;
copy.keepSupertype = this.keepSupertype;
copy.removeSubTypes = this.removeSubTypes;
@@ -345,68 +341,6 @@ public class StaticEffect {
this.originalKeywords.clear();
}
// original power/toughness
/**
* <p>
* addOriginalPT.
* </p>
*
* @param c
* a {@link forge.game.card.Card} object.
* @param power
* a int.
* @param toughness
* a int.
*/
public final void addOriginalPT(final Card c, final int power, final int toughness) {
final String pt = power + "/" + toughness;
if (!this.originalPT.containsKey(c)) {
this.originalPT.put(c, pt);
}
}
/**
* <p>
* getOriginalPower.
* </p>
*
* @param c
* a {@link forge.game.card.Card} object.
* @return a int.
*/
public final int getOriginalPower(final Card c) {
int power = -1;
if (this.originalPT.containsKey(c)) {
power = Integer.parseInt(this.originalPT.get(c).split("/")[0]);
}
return power;
}
/**
* <p>
* getOriginalToughness.
* </p>
*
* @param c
* a {@link forge.game.card.Card} object.
* @return a int.
*/
public final int getOriginalToughness(final Card c) {
int tough = -1;
if (this.originalPT.containsKey(c)) {
tough = Integer.parseInt(this.originalPT.get(c).split("/")[1]);
}
return tough;
}
/**
* <p>
* clearAllOriginalPTs.
* </p>
*/
public final void clearAllOriginalPTs() {
this.originalPT.clear();
}
// should we overwrite types?
/**
@@ -995,7 +929,7 @@ public class StaticEffect {
}
// remove set P/T
if (!params.containsKey("CharacteristicDefining") && setPT) {
if (setPT) {
affectedCard.removeNewPT(getTimestamp());
}
@@ -1036,7 +970,7 @@ public class StaticEffect {
}
// remove abilities
if (params.containsKey("RemoveAllAbilities")) {
if (params.containsKey("RemoveAllAbilities") || params.containsKey("RemoveIntrinsicAbilities")) {
affectedCard.unSuppressCardTraits();
}

View File

@@ -256,7 +256,7 @@ public final class AbilityFactory {
}
}
if (api == ApiType.Charm || api == ApiType.GenericChoice) {
if (api == ApiType.Charm || api == ApiType.GenericChoice || api == ApiType.AssignGroup) {
final String key = "Choices";
if (mapParams.containsKey(key)) {
List<String> names = Lists.newArrayList(mapParams.get(key).split(","));

View File

@@ -381,7 +381,7 @@ public class AbilityUtils {
svarval = ability.getSVar(amount);
}
if (StringUtils.isBlank(svarval)) {
if ((ability != null) && (ability instanceof SpellAbility) && !(ability instanceof SpellPermanent)) {
if ((ability != null) && (ability instanceof SpellAbility) && !(ability instanceof SpellPermanent) && !amount.equals("ChosenX")) {
System.err.printf("SVar '%s' not found in ability, fallback to Card (%s). Ability is (%s)%n", amount, card.getName(), ability);
}
svarval = card.getSVar(amount);
@@ -439,6 +439,10 @@ public class AbilityUtils {
players.addAll(game.getPlayers());
val = CardFactoryUtil.playerXCount(players, calcX[1], card);
}
else if (hType.equals("YourTeam")) {
players.addAll(player.getYourTeam());
val = CardFactoryUtil.playerXCount(players, calcX[1], card);
}
else if (hType.equals("Opponents")) {
players.addAll(player.getOpponents());
val = CardFactoryUtil.playerXCount(players, calcX[1], card);
@@ -891,7 +895,11 @@ public class AbilityUtils {
final Player player = sa == null ? card.getController() : sa.getActivatingPlayer();
if (defined.equals("Targeted") || defined.equals("TargetedPlayer")) {
if (defined.equals("TargetedOrController")) {
players.addAll(getDefinedPlayers(card, "Targeted", sa));
players.addAll(getDefinedPlayers(card, "TargetedController", sa));
}
else if (defined.equals("Targeted") || defined.equals("TargetedPlayer")) {
final SpellAbility saTargeting = sa.getSATargetingPlayer();
if (saTargeting != null) {
players.addAll(saTargeting.getTargets().getTargetPlayers());
@@ -1672,7 +1680,10 @@ public class AbilityUtils {
res.setZone(null);
newSA.setRestrictions(res);
// timing restrictions still apply
if (res.checkTimingRestrictions(tgtCard, newSA) && newSA.checkOtherRestrictions()) {
if (res.checkTimingRestrictions(tgtCard, newSA)
// still need to check the other restrictions like Aftermath
&& res.checkOtherRestrictions(tgtCard, newSA, controller)
&& newSA.checkOtherRestrictions()) {
sas.add(newSA);
}
}

View File

@@ -21,6 +21,7 @@ public enum ApiType {
AnimateAll (AnimateAllEffect.class),
Attach (AttachEffect.class),
Ascend (AscendEffect.class),
AssignGroup (AssignGroupEffect.class),
Balance (BalanceEffect.class),
BecomeMonarch (BecomeMonarchEffect.class),
BecomesBlocked (BecomesBlockedEffect.class),
@@ -81,6 +82,7 @@ public enum ApiType {
GenericChoice (ChooseGenericEffect.class),
Goad (GoadEffect.class),
Haunt (HauntEffect.class),
ImmediateTrigger (ImmediateTriggerEffect.class),
LookAt (LookAtEffect.class),
LoseLife (LifeLoseEffect.class),
LosesGame (GameLossEffect.class),
@@ -142,6 +144,7 @@ public enum ApiType {
SkipTurn (SkipTurnEffect.class),
StoreSVar (StoreSVarEffect.class),
StoreMap (StoreMapEffect.class),
Surveil (SurveilEffect.class),
Tap (TapEffect.class),
TapAll (TapAllEffect.class),
TapOrUntap (TapOrUntapEffect.class),

View File

@@ -10,6 +10,7 @@ import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Lists;
import forge.GameCommand;
import forge.card.CardType;
import forge.game.Game;
import forge.game.GameObject;
@@ -385,4 +386,83 @@ public abstract class SpellAbilityEffect {
return eff;
}
protected static void replaceDying(final SpellAbility sa) {
if (sa.hasParam("ReplaceDyingDefined") || sa.hasParam("ReplaceDyingValid")) {
if (sa.hasParam("ReplaceDyingCondition")) {
// currently there is only one with Kicker
final String condition = sa.getParam("ReplaceDyingCondition");
if ("Kicked".equals(condition)) {
if (!sa.isKicked()) {
return;
}
}
}
final Card host = sa.getHostCard();
final Player controller = sa.getActivatingPlayer();
final Game game = host.getGame();
String zone = sa.getParamOrDefault("ReplaceDyingZone", "Exile");
CardCollection cards = null;
if (sa.hasParam("ReplaceDyingDefined")) {
cards = AbilityUtils.getDefinedCards(host, sa.getParam("ReplaceDyingDefined"), sa);
// no cards, no need for Effect
if (cards.isEmpty()) {
return;
}
}
// build an Effect with that infomation
String name = host.getName() + "'s Effect";
final Card eff = createEffect(host, controller, name, host.getImageKey());
if (cards != null) {
eff.addRemembered(cards);
}
String valid = sa.getParamOrDefault("ReplaceDyingValid", "Card.IsRemembered");
String repeffstr = "Event$ Moved | ValidCard$ " + valid +
"| Origin$ Battlefield | Destination$ Graveyard " +
"| Description$ If the creature would die this turn, exile it instead.";
String effect = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ " + zone;
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
re.setLayer(ReplacementLayer.Other);
re.setOverridingAbility(AbilityFactory.getAbility(effect, eff));
eff.addReplacementEffect(re);
if (cards != null) {
// Add forgot trigger
addForgetOnMovedTrigger(eff, "Battlefield");
}
// Copy text changes
if (sa.isIntrinsic()) {
eff.copyChangedTextFrom(host);
}
final GameCommand endEffect = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
@Override
public void run() {
game.getAction().exile(eff, null);
}
};
game.getEndOfTurn().addUntil(endEffect);
eff.updateStateForView();
// TODO: Add targeting to the effect so it knows who it's dealing with
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
}
}

View File

@@ -12,6 +12,7 @@ import forge.util.Lang;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.util.List;
@@ -49,7 +50,8 @@ public class ActivateAbilityEffect extends SpellAbilityEffect {
if (possibleAb.isEmpty()) {
continue;
}
SpellAbility manaAb = p.getController().chooseSingleSpellForEffect(possibleAb, sa, "Choose a mana ability:");
SpellAbility manaAb = p.getController().chooseSingleSpellForEffect(
possibleAb, sa, "Choose a mana ability:", ImmutableMap.of());
p.getController().playChosenSpellAbility(manaAb);
}
}

View File

@@ -25,6 +25,8 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableList;
public class AnimateAllEffect extends AnimateEffectBase {
@Override
@@ -144,6 +146,9 @@ public class AnimateAllEffect extends AnimateEffectBase {
list = CardLists.getValidCards(list, valid.split(","), host.getController(), host, sa);
boolean removeAll = sa.hasParam("RemoveAllAbilities");
boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
for (final Card c : list) {
doAnimate(c, sa, power, toughness, types, removeTypes, finalDesc,
keywords, removeKeywords, hiddenKeywords, timestamp);
@@ -161,11 +166,14 @@ public class AnimateAllEffect extends AnimateEffectBase {
// remove abilities
final List<SpellAbility> removedAbilities = new ArrayList<SpellAbility>();
if (sa.hasParam("OverwriteAbilities") || sa.hasParam("RemoveAllAbilities")) {
if (sa.hasParam("OverwriteAbilities") || removeAll || removeIntrinsic) {
for (final SpellAbility ab : c.getSpellAbilities()) {
if (ab.isAbility()) {
c.removeSpellAbility(ab);
removedAbilities.add(ab);
if (removeAll
|| (ab.isIntrinsic() && removeIntrinsic && !ab.isBasicLandAbility())) {
ab.setTemporarilySuppressed(true);
removedAbilities.add(ab);
}
}
}
}
@@ -190,19 +198,24 @@ public class AnimateAllEffect extends AnimateEffectBase {
// suppress triggers from the animated card
final List<Trigger> removedTriggers = new ArrayList<Trigger>();
if (sa.hasParam("OverwriteTriggers") || sa.hasParam("RemoveAllAbilities")) {
if (sa.hasParam("OverwriteTriggers") || removeAll || removeIntrinsic) {
final FCollectionView<Trigger> triggersToRemove = c.getTriggers();
for (final Trigger trigger : triggersToRemove) {
trigger.setSuppressed(true);
if (removeIntrinsic && !trigger.isIntrinsic()) {
continue;
}
trigger.setSuppressed(true); // why this not TemporarilySuppressed?
removedTriggers.add(trigger);
}
}
// suppress static abilities from the animated card
final List<StaticAbility> removedStatics = new ArrayList<StaticAbility>();
if (sa.hasParam("OverwriteStatics") || sa.hasParam("RemoveAllAbilities")) {
final FCollectionView<StaticAbility> staticsToRemove = c.getStaticAbilities();
for (final StaticAbility stAb : staticsToRemove) {
if (sa.hasParam("OverwriteStatics") || removeAll || removeIntrinsic) {
for (final StaticAbility stAb : c.getStaticAbilities()) {
if (removeIntrinsic && !stAb.isIntrinsic()) {
continue;
}
stAb.setTemporarilySuppressed(true);
removedStatics.add(stAb);
}
@@ -210,9 +223,11 @@ public class AnimateAllEffect extends AnimateEffectBase {
// suppress static abilities from the animated card
final List<ReplacementEffect> removedReplacements = new ArrayList<ReplacementEffect>();
if (sa.hasParam("OverwriteReplacements") || sa.hasParam("RemoveAllAbilities")) {
final FCollectionView<ReplacementEffect> replacementsToRemove = c.getReplacementEffects();
for (final ReplacementEffect re : replacementsToRemove) {
if (sa.hasParam("OverwriteReplacements") || removeAll || removeIntrinsic) {
for (final ReplacementEffect re : c.getReplacementEffects()) {
if (removeIntrinsic && !re.isIntrinsic()) {
continue;
}
re.setTemporarilySuppressed(true);
removedReplacements.add(re);
}
@@ -234,8 +249,11 @@ public class AnimateAllEffect extends AnimateEffectBase {
public void run() {
doUnanimate(c, sa, finalDesc, hiddenKeywords,
addedAbilities, addedTriggers, addedReplacements,
false, removedAbilities, timestamp);
ImmutableList.of(), timestamp);
for (final SpellAbility sa : removedAbilities) {
sa.setTemporarilySuppressed(false);
}
// give back suppressed triggers
for (final Trigger t : removedTriggers) {
t.setSuppressed(false);

View File

@@ -18,7 +18,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.util.collect.FCollectionView;
import java.util.Arrays;
import java.util.List;
@@ -162,21 +161,20 @@ public class AnimateEffect extends AnimateEffectBase {
boolean clearAbilities = sa.hasParam("OverwriteAbilities");
boolean clearSpells = sa.hasParam("OverwriteSpells");
boolean removeAll = sa.hasParam("RemoveAllAbilities");
boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
if (clearAbilities || clearSpells || removeAll) {
for (final SpellAbility ab : c.getSpellAbilities()) {
if (removeAll || (ab.isAbility() && clearAbilities)
if (removeAll
|| (ab.isIntrinsic() && removeIntrinsic && !ab.isBasicLandAbility())
|| (ab.isAbility() && clearAbilities)
|| (ab.isSpell() && clearSpells)) {
ab.setTemporarilySuppressed(true);
removedAbilities.add(ab);
}
}
}
// Can't rmeove SAs in foreach loop that finds them
for (final SpellAbility ab : removedAbilities) {
c.removeSpellAbility(ab);
}
if (sa.hasParam("RemoveThisAbility") && !removedAbilities.contains(sa)) {
c.removeSpellAbility(sa);
removedAbilities.add(sa);
@@ -215,20 +213,23 @@ public class AnimateEffect extends AnimateEffectBase {
// suppress triggers from the animated card
final List<Trigger> removedTriggers = Lists.newArrayList();
if (sa.hasParam("OverwriteTriggers") || removeAll) {
final FCollectionView<Trigger> triggersToRemove = c.getTriggers();
for (final Trigger trigger : triggersToRemove) {
trigger.setSuppressed(true);
if (sa.hasParam("OverwriteTriggers") || removeAll || removeIntrinsic) {
for (final Trigger trigger : c.getTriggers()) {
if (removeIntrinsic && !trigger.isIntrinsic()) {
continue;
}
trigger.setSuppressed(true); // why this not TemporarilySuppressed?
removedTriggers.add(trigger);
}
}
// give static abilities (should only be used by cards to give
// itself a static ability)
final List<StaticAbility> addedStaticAbilities = Lists.newArrayList();
if (stAbs.size() > 0) {
for (final String s : stAbs) {
final String actualAbility = source.getSVar(s);
c.addStaticAbility(actualAbility);
addedStaticAbilities.add(c.addStaticAbility(actualAbility));
}
}
@@ -248,9 +249,11 @@ public class AnimateEffect extends AnimateEffectBase {
// suppress static abilities from the animated card
final List<StaticAbility> removedStatics = Lists.newArrayList();
if (sa.hasParam("OverwriteStatics") || removeAll) {
final FCollectionView<StaticAbility> staticsToRemove = c.getStaticAbilities();
for (final StaticAbility stAb : staticsToRemove) {
if (sa.hasParam("OverwriteStatics") || removeAll || removeIntrinsic) {
for (final StaticAbility stAb : c.getStaticAbilities()) {
if (removeIntrinsic && !stAb.isIntrinsic()) {
continue;
}
stAb.setTemporarilySuppressed(true);
removedStatics.add(stAb);
}
@@ -258,8 +261,11 @@ public class AnimateEffect extends AnimateEffectBase {
// suppress static abilities from the animated card
final List<ReplacementEffect> removedReplacements = Lists.newArrayList();
if (sa.hasParam("OverwriteReplacements") || removeAll) {
if (sa.hasParam("OverwriteReplacements") || removeAll || removeIntrinsic) {
for (final ReplacementEffect re : c.getReplacementEffects()) {
if (removeIntrinsic && !re.isIntrinsic()) {
continue;
}
re.setTemporarilySuppressed(true);
removedReplacements.add(re);
}
@@ -272,8 +278,6 @@ public class AnimateEffect extends AnimateEffectBase {
}
}
final boolean givesStAbs = (stAbs.size() > 0);
final GameCommand unanimate = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
@@ -281,9 +285,13 @@ public class AnimateEffect extends AnimateEffectBase {
public void run() {
doUnanimate(c, sa, finalDesc, hiddenKeywords,
addedAbilities, addedTriggers, addedReplacements,
givesStAbs, removedAbilities, timestamp);
addedStaticAbilities, timestamp);
game.fireEvent(new GameEventCardStatsChanged(c));
for (final SpellAbility sa : removedAbilities) {
sa.setTemporarilySuppressed(false);
}
// give back suppressed triggers
for (final Trigger t : removedTriggers) {
t.setSuppressed(false);
@@ -458,6 +466,8 @@ public class AnimateEffect extends AnimateEffectBase {
sb.append(" until ").append(host).append(" leaves the battlefield.");
} else if (sa.hasParam("UntilYourNextUpkeep")) {
sb.append(" until your next upkeep.");
} else if (sa.hasParam("UntilYourNextTurn")) {
sb.append(" until your next turn.");
} else if (sa.hasParam("UntilControllerNextUntap")) {
sb.append(" until its controller's next untap step.");
} else {

View File

@@ -24,11 +24,10 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import java.util.ArrayList;
import java.util.List;
public abstract class AnimateEffectBase extends SpellAbilityEffect {
void doAnimate(final Card c, final SpellAbility sa, final Integer power, final Integer toughness,
public static void doAnimate(final Card c, final SpellAbility sa, final Integer power, final Integer toughness,
final CardType addType, final CardType removeType, final String colors,
final List<String> keywords, final List<String> removeKeywords,
final List<String> hiddenKeywords, final long timestamp) {
@@ -36,15 +35,19 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
boolean removeSuperTypes = false;
boolean removeCardTypes = false;
boolean removeSubTypes = false;
boolean removeLandTypes = false;
boolean removeCreatureTypes = false;
boolean removeArtifactTypes = false;
boolean removeEnchantmentTypes = false;
if (sa.hasParam("OverwriteTypes")) {
removeSuperTypes = true;
removeCardTypes = true;
removeSubTypes = true;
removeLandTypes = true;
removeCreatureTypes = true;
removeArtifactTypes = true;
removeEnchantmentTypes = true;
}
if (sa.hasParam("KeepSupertypes")) {
@@ -57,6 +60,10 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
if (sa.hasParam("KeepSubtypes")) {
removeSubTypes = false;
removeLandTypes = false;
removeCreatureTypes = false;
removeArtifactTypes = false;
removeEnchantmentTypes = false;
}
if (sa.hasParam("RemoveSuperTypes")) {
@@ -71,23 +78,30 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
removeSubTypes = true;
}
if (sa.hasParam("RemoveLandTypes")) {
removeCreatureTypes = true;
}
if (sa.hasParam("RemoveCreatureTypes")) {
removeCreatureTypes = true;
}
if (sa.hasParam("RemoveArtifactTypes")) {
removeArtifactTypes = true;
}
if (sa.hasParam("RemoveEnchantmentTypes")) {
removeEnchantmentTypes = true;
}
if ((power != null) || (toughness != null)) {
c.addNewPT(power, toughness, timestamp);
}
if (!addType.isEmpty() || !removeType.isEmpty() || removeCreatureTypes) {
c.addChangedCardTypes(addType, removeType, removeSuperTypes, removeCardTypes, removeSubTypes,
removeCreatureTypes, removeArtifactTypes, timestamp);
removeLandTypes, removeCreatureTypes, removeArtifactTypes, removeEnchantmentTypes, timestamp);
}
c.addChangedCardKeywords(keywords, removeKeywords, sa.hasParam("RemoveAllAbilities"), timestamp);
c.addChangedCardKeywords(keywords, removeKeywords,
sa.hasParam("RemoveAllAbilities"), sa.hasParam("RemoveIntrinsicAbilities"), timestamp);
for (final String k : hiddenKeywords) {
c.addHiddenExtrinsicKeyword(k);
@@ -114,10 +128,10 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
* @param timestamp
* a long.
*/
void doUnanimate(final Card c, SpellAbility sa, final String colorDesc,
static void doUnanimate(final Card c, SpellAbility sa, final String colorDesc,
final List<String> hiddenKeywords, final List<SpellAbility> addedAbilities,
final List<Trigger> addedTriggers, final List<ReplacementEffect> addedReplacements,
final boolean givesStAbs, final List<SpellAbility> removedAbilities, final long timestamp) {
final List<StaticAbility> addedStaticAbilities, final long timestamp) {
if (sa.hasParam("LastsIndefinitely")) {
return;
@@ -127,16 +141,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.removeChangedCardKeywords(timestamp);
// remove all static abilities
if (givesStAbs) {
c.setStaticAbilities(new ArrayList<StaticAbility>());
}
if (sa.hasParam("Types") || sa.hasParam("RemoveTypes")
|| sa.hasParam("RemoveCreatureTypes") || sa.hasParam("RemoveArtifactTypes")) {
c.removeChangedCardTypes(timestamp);
}
c.removeChangedCardTypes(timestamp);
c.removeColor(timestamp);
for (final String k : hiddenKeywords) {
@@ -147,10 +152,6 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.removeSpellAbility(saAdd);
}
for (final SpellAbility saRem : removedAbilities) {
c.addSpellAbility(saRem);
}
for (final Trigger t : addedTriggers) {
c.removeTrigger(t);
}
@@ -159,6 +160,10 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.removeReplacementEffect(rep);
}
for (final StaticAbility stAb : addedStaticAbilities) {
c.removeStaticAbility(stAb);
}
// any other unanimate cleanup
if (!c.isCreature()) {
c.unEquipAllCards();

View File

@@ -0,0 +1,74 @@
package forge.game.ability.effects;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class AssignGroupEffect extends SpellAbilityEffect {
/* (non-Javadoc)
* @see forge.game.ability.SpellAbilityEffect#getStackDescription(forge.game.spellability.SpellAbility)
*/
@Override
protected String getStackDescription(SpellAbility sa) {
return sa.getDescription();
}
/*
* (non-Javadoc)
* @see forge.game.ability.SpellAbilityEffect#resolve(forge.game.spellability.SpellAbility)
*/
@Override
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = host.getGame();
List<GameObject> defined = getDefinedOrTargeted(sa, "Defined");
final List<SpellAbility> abilities = Lists.<SpellAbility>newArrayList(sa.getAdditionalAbilityList("Choices"));
Player chooser = sa.getActivatingPlayer();
if (sa.hasParam("Chooser")) {
final String choose = sa.getParam("Chooser");
chooser = AbilityUtils.getDefinedPlayers(sa.getHostCard(), choose, sa).get(0);
}
Multimap<SpellAbility, GameObject> result = ArrayListMultimap.create();
for (GameObject g : defined) {
final String title = "Choose ability for " + g.toString();
Map<String, Object> params = Maps.newHashMap();
params.put("Affected", g);
result.put(chooser.getController().chooseSingleSpellForEffect(abilities, sa, title, params), g);
}
// in order of choice list
for (SpellAbility s : abilities) {
// is that in Player order?
Collection<GameObject> l = result.get(s);
host.addRemembered(l);
AbilityUtils.resolve(s);
host.removeRemembered(l);
// this will refresh continuous abilities for players and permanents.
game.getAction().checkStaticAbilities();
game.getTriggerHandler().resetActiveTriggers(false);
}
}
}

View File

@@ -156,7 +156,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
// Auras without Candidates stay in their current location
if (c.isAura()) {
final SpellAbility saAura = c.getFirstAttachSpell();
if (!saAura.getTargetRestrictions().hasCandidates(saAura, false)) {
if (saAura != null && !saAura.getTargetRestrictions().hasCandidates(saAura, false)) {
continue;
}
}

View File

@@ -6,7 +6,9 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.GameCommand;
import forge.card.CardStateName;
import forge.card.CardType;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
@@ -32,6 +34,7 @@ import forge.util.collect.*;
import forge.util.Lang;
import forge.util.MessageUtil;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -484,7 +487,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
if (sa.hasParam("WithCounters")) {
String[] parse = sa.getParam("WithCounters").split("_");
tgtC.addEtbCounter(CounterType.getType(parse[0]), Integer.parseInt(parse[1]), hostCard);
tgtC.addEtbCounter(CounterType.getType(parse[0]), Integer.parseInt(parse[1]), player);
}
if (sa.hasParam("GainControl")) {
if (sa.hasParam("NewController")) {
@@ -552,9 +555,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// location
if (tgtC.isAura()) {
final SpellAbility saAura = tgtC.getFirstAttachSpell();
saAura.setActivatingPlayer(sa.getActivatingPlayer());
if (!saAura.getTargetRestrictions().hasCandidates(saAura, false)) {
continue;
if (saAura != null) {
saAura.setActivatingPlayer(sa.getActivatingPlayer());
if (!saAura.getTargetRestrictions().hasCandidates(saAura, false)) {
continue;
}
}
}
@@ -865,7 +870,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
final boolean champion = sa.hasParam("Champion");
final boolean forget = sa.hasParam("ForgetChanged");
final boolean imprint = sa.hasParam("Imprint");
final String selectPrompt = sa.hasParam("SelectPrompt") ? sa.getParam("SelectPrompt") : MessageUtil.formatMessage("Select a card from {player's} " + Lang.joinHomogenous(origin).toLowerCase(), decider, player);
String selectPrompt = sa.hasParam("SelectPrompt") ? sa.getParam("SelectPrompt") : MessageUtil.formatMessage("Select a card from {player's} " + Lang.joinHomogenous(origin).toLowerCase(), decider, player);
final String totalcmc = sa.getParam("WithTotalCMC");
int totcmc = AbilityUtils.calculateAmount(source, totalcmc, sa);
@@ -874,7 +879,20 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
CardCollection chosenCards = new CardCollection();
// only multi-select if player can select more than one
if (changeNum > 1 && allowMultiSelect(decider, sa)) {
for (Card card : decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, delayedReveal, selectPrompt, decider)) {
List<Card> selectedCards;
if (! sa.hasParam("SelectPrompt")) {
// new default messaging for multi select
if (fetchList.size() > changeNum) {
selectPrompt = MessageUtil.formatMessage("Select up to " + changeNum + " cards from {player's} " + Lang.joinHomogenous(origin).toLowerCase(), decider, player);
} else {
selectPrompt = MessageUtil.formatMessage("Select cards from {player's} " + Lang.joinHomogenous(origin).toLowerCase(), decider, player);
}
}
// ensure that selection is within maximum allowed changeNum
do {
selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, delayedReveal, selectPrompt, decider);
} while (selectedCards != null && selectedCards.size() > changeNum);
for (Card card : selectedCards) {
chosenCards.add(card);
};
// maybe prompt the user if they selected fewer than the maximum possible?
@@ -1101,11 +1119,49 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
}
}
// need to be facedown before it hits the battlefield in case of Replacement Effects or Trigger
if (sa.hasParam("FaceDown") && ZoneType.Battlefield.equals(destination)) {
c.setState(CardStateName.FaceDown, true);
// set New Pt doesn't work because this values need to be copyable for clone effects
if (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness")) {
if (sa.hasParam("FaceDownPower")) {
c.setBasePower(AbilityUtils.calculateAmount(
source, sa.getParam("FaceDownPower"), sa));
}
if (sa.hasParam("FaceDownToughness")) {
c.setBaseToughness(AbilityUtils.calculateAmount(
source, sa.getParam("FaceDownToughness"), sa));
}
}
if (sa.hasParam("FaceDownAddType")) {
CardType t = new CardType(c.getCurrentState().getType());
t.addAll(Arrays.asList(sa.getParam("FaceDownAddType").split(",")));
c.getCurrentState().setType(t);
}
if (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness")
|| sa.hasParam("FaceDownAddType")) {
final GameCommand unanimate = new GameCommand() {
private static final long serialVersionUID = 8853789549297846163L;
@Override
public void run() {
c.clearStates(CardStateName.FaceDown, true);
}
};
c.addFaceupCommand(unanimate);
}
}
movedCard = game.getAction().moveTo(c.getController().getZone(destination), c, sa, null);
if (sa.hasParam("Tapped")) {
movedCard.setTapped(true);
}
if (sa.hasParam("FaceDown")) {
// need to do that again?
if (sa.hasParam("FaceDown") && !ZoneType.Battlefield.equals(destination)) {
movedCard.setState(CardStateName.FaceDown, true);
}
movedCard.setTimestamp(ts);
@@ -1181,7 +1237,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
&& !sa.hasParam("DifferentNames")
&& !sa.hasParam("DifferentCMC")
&& !sa.hasParam("AtRandom")
&& !sa.hasParam("ChangeNum") // TODO: doesn't work with card number limits, e.g. Doomsday
&& (!sa.hasParam("Defined") || sa.hasParam("ChooseFromDefined"))
&& sa.getParam("WithTotalCMC") == null;
}

View File

@@ -105,17 +105,8 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
final String message = validDesc.equals("card") ? "Name a card" : "Name a " + validDesc + " card.";
Predicate<ICardFace> cpp = Predicates.alwaysTrue();
if ( StringUtils.containsIgnoreCase(valid, "nonland") ) {
cpp = CardFacePredicates.Presets.IS_NON_LAND;
}
if ( StringUtils.containsIgnoreCase(valid, "nonbasic") ) {
cpp = Predicates.not(CardFacePredicates.Presets.IS_BASIC_LAND);
}
if ( StringUtils.containsIgnoreCase(valid, "noncreature") ) {
cpp = Predicates.not(CardFacePredicates.Presets.IS_CREATURE);
} else if ( StringUtils.containsIgnoreCase(valid, "creature") ) {
cpp = CardFacePredicates.Presets.IS_CREATURE;
if (sa.hasParam("ValidCards")) {
cpp = CardFacePredicates.valid(valid);
}
chosen = p.getController().chooseCardName(sa, cpp, valid, message);

View File

@@ -1,5 +1,6 @@
package forge.game.ability.effects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
@@ -65,7 +66,8 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
int idxChosen = MyRandom.getRandom().nextInt(abilities.size());
chosenSA = abilities.get(idxChosen);
} else {
chosenSA = p.getController().chooseSingleSpellForEffect(abilities, sa, "Choose one");
chosenSA = p.getController().chooseSingleSpellForEffect(abilities, sa, "Choose one",
ImmutableMap.of());
}
if (chosenSA != null) {

View File

@@ -64,11 +64,15 @@ public class CloneEffect extends SpellAbilityEffect {
Card cardToCopy = null;
if (sa.hasParam("Choices")) {
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
ZoneType choiceZone = ZoneType.Battlefield;
if (sa.hasParam("ChoiceZone")) {
choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
}
CardCollectionView choices = game.getCardsIn(choiceZone);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host);
String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : "Choose a card ";
cardToCopy = activator.getController().chooseSingleEntityForEffect(choices, sa, title, true);
cardToCopy = activator.getController().chooseSingleEntityForEffect(choices, sa, title, false);
} else if (sa.hasParam("Defined")) {
List<Card> cloneSources = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
if (!cloneSources.isEmpty()) {
@@ -213,6 +217,9 @@ public class CloneEffect extends SpellAbilityEffect {
else if (duration.equals("UntilYourNextTurn")) {
game.getCleanup().addUntil(host.getController(), unclone);
}
else if (duration.equals("UntilUnattached")) {
sa.getHostCard().addUnattachCommand(unclone);
}
}
game.fireEvent(new GameEventCardStatsChanged(tgtCard));
} // cloneResolve

View File

@@ -5,7 +5,6 @@ import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ImageKeys;
import forge.StaticData;
@@ -22,6 +21,7 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.event.GameEventCombatChanged;
import forge.game.player.Player;
@@ -37,10 +37,10 @@ import forge.util.collect.FCollectionView;
import forge.util.PredicateString.StringOp;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class CopyPermanentEffect extends SpellAbilityEffect {
@@ -167,12 +167,18 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
}
}
} else if (sa.hasParam("Choices")) {
Player chooser = activator;
if (sa.hasParam("Chooser")) {
final String choose = sa.getParam("Chooser");
chooser = AbilityUtils.getDefinedPlayers(sa.getHostCard(), choose, sa).get(0);
}
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host);
if (!choices.isEmpty()) {
String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : "Choose a card ";
Card choosen = activator.getController().chooseSingleEntityForEffect(choices, sa, title, false);
Card choosen = chooser.getController().chooseSingleEntityForEffect(choices, sa, title, false);
if (choosen != null) {
tgtCards.add(choosen);
@@ -186,29 +192,17 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
for (final Card c : tgtCards) {
if (!sa.usesTargeting() || c.canBeTargetedBy(sa)) {
int multiplier = numCopies;
final Map<String, Object> repParams = Maps.newHashMap();
repParams.put("Event", "CreateToken");
repParams.put("Affected", controller);
repParams.put("TokenNum", multiplier);
repParams.put("EffectOnly", true);
Pair<Player, Integer> result = TokenInfo.calculateMultiplier(
game, controller, true, numCopies);
switch (game.getReplacementHandler().run(repParams)) {
case NotReplaced:
break;
case Updated: {
multiplier = (int) repParams.get("TokenNum");
break;
}
default:
return ;
if (result.getRight() <= 0) {
return;
}
final List<Card> crds = Lists.newArrayListWithCapacity(multiplier);
final List<Card> crds = Lists.newArrayListWithCapacity(result.getRight());
for (int i = 0; i < multiplier; i++) {
final Card copy = CardFactory.copyCopiableCharacteristics(c, activator);
for (int i = 0; i < result.getRight(); i++) {
final Card copy = CardFactory.copyCopiableCharacteristics(c, result.getLeft());
copy.setToken(true);
copy.setCopiedPermanent(c);
// add keywords from sa
@@ -337,7 +331,7 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
}
}
// set the controller before move to play: Crafty Cutpurse
copy.setController(controller, 0);
copy.setController(result.getLeft(), 0);
copy.updateStateForView();
// Temporarily register triggers of an object created with CopyPermanent
@@ -350,7 +344,7 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
copyInPlay.setCloneOrigin(host);
sa.getHostCard().addClone(copyInPlay);
if (!pumpKeywords.isEmpty()) {
copyInPlay.addChangedCardKeywords(pumpKeywords, Lists.<String>newArrayList(), false, timestamp);
copyInPlay.addChangedCardKeywords(pumpKeywords, Lists.<String>newArrayList(), false, false, timestamp);
}
crds.add(copyInPlay);
if (sa.hasParam("RememberCopied")) {

View File

@@ -1,5 +1,6 @@
package forge.game.ability.effects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -83,8 +84,9 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
for (int multi = 0; multi < spellCount && !tgtSpells.isEmpty(); multi++) {
String prompt = "Select " + Lang.getOrdinal(multi + 1) + " spell to copy to stack";
SpellAbility chosen = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa, prompt);
SpellAbility copiedSpell = CardFactory.copySpellAbilityAndSrcCard(card, chosen.getHostCard(), chosen, true);
SpellAbility chosen = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa, prompt,
ImmutableMap.of());
SpellAbility copiedSpell = CardFactory.copySpellAbilityAndPossiblyHost(card, chosen.getHostCard(), chosen, true);
copiedSpell.getHostCard().setController(card.getController(), card.getGame().getNextTimestamp());
copiedSpell.setActivatingPlayer(controller);
copies.add(copiedSpell);
@@ -92,7 +94,8 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
}
}
else if (sa.hasParam("CopyForEachCanTarget")) {
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa, "Select a spell to copy");
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa,
"Select a spell to copy", ImmutableMap.of());
chosenSA.setActivatingPlayer(controller);
// Find subability or rootability that has targets
SpellAbility targetedSA = chosenSA;
@@ -114,7 +117,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
mayChooseNewTargets = false;
for (GameEntity o : candidates) {
SpellAbility copy = CardFactory.copySpellAbilityAndSrcCard(card, chosenSA.getHostCard(), chosenSA, true);
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true);
resetFirstTargetOnCopy(copy, o, targetedSA);
copies.add(copy);
}
@@ -140,22 +143,23 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
valid.remove(originalTarget);
mayChooseNewTargets = false;
for (final Card c : valid) {
SpellAbility copy = CardFactory.copySpellAbilityAndSrcCard(card, chosenSA.getHostCard(), chosenSA, true);
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true);
resetFirstTargetOnCopy(copy, c, targetedSA);
copies.add(copy);
}
for (final Player p : players) {
SpellAbility copy = CardFactory.copySpellAbilityAndSrcCard(card, chosenSA.getHostCard(), chosenSA, true);
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true);
resetFirstTargetOnCopy(copy, p, targetedSA);
copies.add(copy);
}
}
}
else {
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa, "Select a spell to copy");
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa,
"Select a spell to copy", ImmutableMap.of());
chosenSA.setActivatingPlayer(controller);
for (int i = 0; i < amount; i++) {
copies.add(CardFactory.copySpellAbilityAndSrcCard(card, chosenSA.getHostCard(), chosenSA, true));
copies.add(CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true));
}
}

View File

@@ -100,6 +100,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
// Test to see if the card we're trying to add is in the expected state
return;
}
dest = cur;
int csum = 0;
@@ -145,7 +146,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
}
if (csum > 0) {
dest.addCounter(cType, csum, host, true);
dest.addCounter(cType, csum, player, true);
game.updateLastStateForCard(dest);
}
return;
@@ -194,15 +195,15 @@ public class CountersMoveEffect extends SpellAbilityEffect {
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", cType);
params.put("Source", source);
params.put("Target", dest);
params.put("Target", cur);
StringBuilder sb = new StringBuilder();
sb.append("Put how many ").append(cType.getName()).append(" counters on ").append(dest).append("?");
sb.append("Put how many ").append(cType.getName()).append(" counters on ").append(cur).append("?");
int cnum = player.getController().chooseNumber(sa, sb.toString(), 0, source.getCounters(cType), params);
if (cnum > 0) {
source.subtractCounter(cType, cnum);
dest.addCounter(cType, cnum, host, true);
game.updateLastStateForCard(dest);
cur.addCounter(cType, cnum, player, true);
game.updateLastStateForCard(cur);
updateSource = true;
}
}
@@ -245,7 +246,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
}
if (!"Any".matches(counterName)) {
if (!dest.canReceiveCounters(cType)) {
if (!cur.canReceiveCounters(cType)) {
continue;
}
@@ -253,7 +254,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", cType);
params.put("Source", source);
params.put("Target", dest);
params.put("Target", cur);
StringBuilder sb = new StringBuilder();
sb.append("Take how many ").append(cType.getName());
sb.append(" counters from ").append(source).append("?");
@@ -262,8 +263,8 @@ public class CountersMoveEffect extends SpellAbilityEffect {
if (source.getCounters(cType) >= cntToMove) {
source.subtractCounter(cType, cntToMove);
dest.addCounter(cType, cntToMove, host, true);
game.updateLastStateForCard(dest);
cur.addCounter(cType, cntToMove, player, true);
game.updateLastStateForCard(cur);
}
} else {
// any counterType currently only Leech Bonder
@@ -296,7 +297,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
sa, sb.toString(), 0, Math.min(tgtCounters.get(chosenType), cntToMove), params);
if (chosenAmount > 0) {
dest.addCounter(chosenType, chosenAmount, host, true);
dest.addCounter(chosenType, chosenAmount, player, true);
source.subtractCounter(chosenType, chosenAmount);
game.updateLastStateForCard(dest);
cntToMove -= chosenAmount;

View File

@@ -7,6 +7,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Lang;
@@ -37,6 +38,7 @@ public class CountersMultiplyEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = host.getGame();
final Player player = sa.getActivatingPlayer();
final CounterType counterType = getCounterType(sa);
final int n = Integer.valueOf(sa.getParamOrDefault("Multiplier", "2")) - 1;
@@ -50,10 +52,10 @@ public class CountersMultiplyEffect extends SpellAbilityEffect {
continue;
}
if (counterType != null) {
gameCard.addCounter(counterType, gameCard.getCounters(counterType) * n, host, true);
gameCard.addCounter(counterType, gameCard.getCounters(counterType) * n, player, true);
} else {
for (Map.Entry<CounterType, Integer> e : gameCard.getCounters().entrySet()) {
gameCard.addCounter(e.getKey(), e.getValue() * n, host, true);
gameCard.addCounter(e.getKey(), e.getValue() * n, player, true);
}
}
game.updateLastStateForCard(gameCard);

View File

@@ -6,6 +6,7 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class CountersNoteEffect extends SpellAbilityEffect {
@@ -26,7 +27,7 @@ public class CountersNoteEffect extends SpellAbilityEffect {
if (mode.equals(MODE_STORE)) {
noteCounters(c, source);
} else if (mode.equals(MODE_LOAD)) {
loadCounters(c, source);
loadCounters(c, source, sa.getActivatingPlayer());
}
}
}
@@ -39,11 +40,11 @@ public class CountersNoteEffect extends SpellAbilityEffect {
}
}
private void loadCounters(Card notee, Card source) {
private void loadCounters(Card notee, Card source, final Player p) {
for(Entry<String, String> svar : source.getSVars().entrySet()) {
String key = svar.getKey();
if (key.startsWith(NOTE_COUNTERS)) {
notee.addCounter(CounterType.getType(key.substring(NOTE_COUNTERS.length())), Integer.parseInt(svar.getValue()), source, false);
notee.addCounter(CounterType.getType(key.substring(NOTE_COUNTERS.length())), Integer.parseInt(svar.getValue()), p, false);
}
// TODO Probably should "remove" the svars that were temporarily used
}

View File

@@ -24,6 +24,7 @@ public class CountersProliferateEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Player p = sa.getActivatingPlayer();
final Card host = sa.getHostCard();
final Game game = host.getGame();
Player controller = host.getController();
@@ -32,10 +33,10 @@ public class CountersProliferateEffect extends SpellAbilityEffect {
return;
for(Entry<GameEntity, CounterType> ge: proliferateChoice.entrySet()) {
if( ge.getKey() instanceof Player )
((Player) ge.getKey()).addCounter(ge.getValue(), 1, host, true);
((Player) ge.getKey()).addCounter(ge.getValue(), 1, p, true);
else if( ge.getKey() instanceof Card) {
Card c = (Card) ge.getKey();
c.addCounter(ge.getValue(), 1, host, true);
c.addCounter(ge.getValue(), 1, p, true);
game.updateLastStateForCard(c);
}
}

View File

@@ -38,30 +38,37 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Player activator = sa.getActivatingPlayer();
final String type = sa.getParam("CounterType");
final int counterAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa);
final String valid = sa.getParam("ValidCards");
final ZoneType zone = sa.hasParam("ValidZone") ? ZoneType.smartValueOf(sa.getParam("ValidZone")) : ZoneType.Battlefield;
final Game game = sa.getActivatingPlayer().getGame();
final Game game = activator.getGame();
if (counterAmount <= 0) {
return;
}
CardCollectionView cards = game.getCardsIn(zone);
cards = CardLists.getValidCards(cards, valid, sa.getHostCard().getController(), sa.getHostCard());
cards = CardLists.getValidCards(cards, valid, host.getController(), sa.getHostCard());
if (sa.usesTargeting()) {
final Player pl = sa.getTargets().getFirstTargetedPlayer();
cards = CardLists.filterControlledBy(cards, pl);
}
Player placer = activator;
if (sa.hasParam("Placer")) {
final String pstr = sa.getParam("Placer");
placer = AbilityUtils.getDefinedPlayers(host, pstr, sa).get(0);
}
for (final Card tgtCard : cards) {
if (game.getZoneOf(tgtCard).is(ZoneType.Battlefield)) {
tgtCard.addCounter(CounterType.valueOf(type), counterAmount, host, true);
tgtCard.addCounter(CounterType.valueOf(type), counterAmount, placer, true);
} else {
// adding counters to something like re-suspend cards
tgtCard.addCounter(CounterType.valueOf(type), counterAmount, host, false);
tgtCard.addCounter(CounterType.valueOf(type), counterAmount, placer, false);
}
game.updateLastStateForCard(tgtCard);
}

View File

@@ -37,7 +37,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
final boolean dividedAsYouChoose = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(card, sa.getParam("CounterNum"), sa);
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("CounterNum", "1"), sa);
if (sa.hasParam("Bolster")) {
sb.append("Bolster ").append(amount);
return sb.toString();
@@ -110,6 +110,12 @@ public class CountersPutEffect extends SpellAbilityEffect {
}
}
Player placer = activator;
if (sa.hasParam("Placer")) {
final String pstr = sa.getParam("Placer");
placer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), pstr, sa).get(0);
}
final boolean etbcounter = sa.hasParam("ETB");
final boolean remember = sa.hasParam("RememberCounters");
final boolean rememberCards = sa.hasParam("RememberCards");
@@ -129,9 +135,10 @@ public class CountersPutEffect extends SpellAbilityEffect {
for (final GameObject obj : tgtObjects) {
// check if the object is still in game or if it was moved
Card gameCard = null;
if (obj instanceof Card) {
Card tgtCard = (Card) obj;
Card gameCard = game.getCardState(tgtCard, null);
gameCard = game.getCardState(tgtCard, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
// this should check Self too
@@ -155,10 +162,10 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (eachExistingCounter) {
for(CounterType ct : choices) {
if (obj instanceof Player) {
((Player) obj).addCounter(ct, counterAmount, card, true);
((Player) obj).addCounter(ct, counterAmount, placer, true);
}
if (obj instanceof Card) {
((Card) obj).addCounter(ct, counterAmount, card, true);
gameCard.addCounter(ct, counterAmount, placer, true);
}
}
continue;
@@ -179,7 +186,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
}
if (obj instanceof Card) {
Card tgtCard = (Card) obj;
Card tgtCard = gameCard;
counterAmount = sa.usesTargeting() && sa.hasParam("DividedAsYouChoose") ? sa.getTargetRestrictions().getDividedValue(tgtCard) : counterAmount;
if (!sa.usesTargeting() || tgtCard.canBeTargetedBy(sa)) {
if (max != -1) {
@@ -232,9 +239,9 @@ public class CountersPutEffect extends SpellAbilityEffect {
final Zone zone = tgtCard.getGame().getZoneOf(tgtCard);
if (zone == null || zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Stack)) {
if (etbcounter) {
tgtCard.addEtbCounter(counterType, counterAmount, card);
tgtCard.addEtbCounter(counterType, counterAmount, placer);
} else {
tgtCard.addCounter(counterType, counterAmount, card, true);
tgtCard.addCounter(counterType, counterAmount, placer, true);
}
if (remember) {
final int value = tgtCard.getTotalCountersToAdd();
@@ -263,9 +270,9 @@ public class CountersPutEffect extends SpellAbilityEffect {
// adding counters to something like re-suspend cards
// etbcounter should apply multiplier
if (etbcounter) {
tgtCard.addEtbCounter(counterType, counterAmount, card);
tgtCard.addEtbCounter(counterType, counterAmount, placer);
} else {
tgtCard.addCounter(counterType, counterAmount, card, false);
tgtCard.addCounter(counterType, counterAmount, placer, false);
}
}
game.updateLastStateForCard(tgtCard);
@@ -273,7 +280,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
} else if (obj instanceof Player) {
// Add Counters to players!
Player pl = (Player) obj;
pl.addCounter(counterType, counterAmount, card, true);
pl.addCounter(counterType, counterAmount, placer, true);
}
}
}

View File

@@ -5,6 +5,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.player.PlayerController;
import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.SpellAbility;
@@ -64,16 +65,16 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
if (gameCard == null || !tgtCard.equalsWithTimestamp(gameCard)) {
continue;
}
if (!sa.usesTargeting() || tgtCard.canBeTargetedBy(sa)) {
if (tgtCard.hasCounters()) {
if (!sa.usesTargeting() || gameCard.canBeTargetedBy(sa)) {
if (gameCard.hasCounters()) {
if (sa.hasParam("EachExistingCounter")) {
for (CounterType listType : Lists.newArrayList(tgtCard.getCounters().keySet())) {
addOrRemoveCounter(sa, tgtCard, listType, counterAmount);
for (CounterType listType : Lists.newArrayList(gameCard.getCounters().keySet())) {
addOrRemoveCounter(sa, gameCard, listType, counterAmount);
}
} else {
addOrRemoveCounter(sa, tgtCard, ctype, counterAmount);
addOrRemoveCounter(sa, gameCard, ctype, counterAmount);
}
game.updateLastStateForCard(tgtCard);
game.updateLastStateForCard(gameCard);
}
}
}
@@ -81,8 +82,8 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
private void addOrRemoveCounter(final SpellAbility sa, final Card tgtCard, CounterType ctype,
final int counterAmount) {
PlayerController pc = sa.getActivatingPlayer().getController();
final Card source = sa.getHostCard();
final Player pl = sa.getActivatingPlayer();
final PlayerController pc = pl.getController();
Map<String, Object> params = Maps.newHashMap();
params.put("Target", tgtCard);
@@ -105,7 +106,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
boolean apply = zone == null || zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Stack);
tgtCard.addCounter(chosenType, counterAmount, source, apply);
tgtCard.addCounter(chosenType, counterAmount, pl, apply);
} else {
tgtCard.subtractCounter(chosenType, counterAmount);
}

View File

@@ -115,27 +115,27 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
if (gameCard == null || !tgtCard.equalsWithTimestamp(gameCard)) {
continue;
}
if (!sa.usesTargeting() || tgtCard.canBeTargetedBy(sa)) {
if (!sa.usesTargeting() || gameCard.canBeTargetedBy(sa)) {
final Zone zone = game.getZoneOf(gameCard);
if (type.equals("All")) {
for (Map.Entry<CounterType, Integer> e : tgtCard.getCounters().entrySet()) {
tgtCard.subtractCounter(e.getKey(), e.getValue());
for (Map.Entry<CounterType, Integer> e : gameCard.getCounters().entrySet()) {
gameCard.subtractCounter(e.getKey(), e.getValue());
}
game.updateLastStateForCard(tgtCard);
game.updateLastStateForCard(gameCard);
continue;
} else if (num.equals("All")) {
cntToRemove = tgtCard.getCounters(counterType);
cntToRemove = gameCard.getCounters(counterType);
} else if (sa.getParam("CounterNum").equals("Remembered")) {
cntToRemove = tgtCard.getCountersAddedBy(card, counterType);
cntToRemove = gameCard.getCountersAddedBy(card, counterType);
}
PlayerController pc = sa.getActivatingPlayer().getController();
if (type.equals("Any")) {
while (cntToRemove > 0 && tgtCard.hasCounters()) {
final Map<CounterType, Integer> tgtCounters = tgtCard.getCounters();
while (cntToRemove > 0 && gameCard.hasCounters()) {
final Map<CounterType, Integer> tgtCounters = gameCard.getCounters();
Map<String, Object> params = Maps.newHashMap();
params.put("Target", tgtCard);
params.put("Target", gameCard);
String prompt = "Select type of counters to remove";
CounterType chosenType = pc.chooseCounterType(
@@ -143,13 +143,13 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
prompt = "Select the number of " + chosenType.getName() + " counters to remove";
int max = Math.min(cntToRemove, tgtCounters.get(chosenType));
params = Maps.newHashMap();
params.put("Target", tgtCard);
params.put("Target", gameCard);
params.put("CounterType", chosenType);
int chosenAmount = pc.chooseNumber(sa, prompt, 1, max, params);
if (chosenAmount > 0) {
tgtCard.subtractCounter(chosenType, chosenAmount);
game.updateLastStateForCard(tgtCard);
gameCard.subtractCounter(chosenType, chosenAmount);
game.updateLastStateForCard(gameCard);
if (rememberRemoved) {
for (int i = 0; i < chosenAmount; i++) {
card.addRemembered(Pair.of(chosenType, i));
@@ -159,12 +159,12 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
}
} else {
cntToRemove = Math.min(cntToRemove, tgtCard.getCounters(counterType));
cntToRemove = Math.min(cntToRemove, gameCard.getCounters(counterType));
if (zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) {
if (sa.hasParam("UpTo")) {
Map<String, Object> params = Maps.newHashMap();
params.put("Target", tgtCard);
params.put("Target", gameCard);
params.put("CounterType", type);
String title = "Select the number of " + type + " counters to remove";
cntToRemove = pc.chooseNumber(sa, title, 0, cntToRemove, params);
@@ -172,13 +172,13 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
if (cntToRemove > 0) {
tgtCard.subtractCounter(counterType, cntToRemove);
gameCard.subtractCounter(counterType, cntToRemove);
if (rememberRemoved) {
for (int i = 0; i < cntToRemove; i++) {
card.addRemembered(Pair.of(counterType, i));
}
}
game.updateLastStateForCard(tgtCard);
game.updateLastStateForCard(gameCard);
}
}
}

View File

@@ -1,87 +1,7 @@
package forge.game.ability.effects;
import forge.GameCommand;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
abstract public class DamageBaseEffect extends SpellAbilityEffect {
static void replaceDying(final SpellAbility sa) {
if (sa.hasParam("ReplaceDyingDefined")) {
if (sa.hasParam("ReplaceDyingCondition")) {
// currently there is only one with Kicker
final String condition = sa.getParam("ReplaceDyingCondition");
if ("Kicked".equals(condition)) {
if (!sa.isKicked()) {
return;
}
}
}
final Card host = sa.getHostCard();
final Player controller = sa.getActivatingPlayer();
final Game game = host.getGame();
String zone = sa.getParamOrDefault("ReplaceDyingZone", "Exile");
CardCollection cards = AbilityUtils.getDefinedCards(host, sa.getParam("ReplaceDyingDefined"), sa);
// no cards, no need for Effect
if (cards.isEmpty()) {
return;
}
// build an Effect with that infomation
String name = host.getName() + "'s Effect";
final Card eff = createEffect(host, controller, name, host.getImageKey());
eff.addRemembered(cards);
String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered " +
"| Origin$ Battlefield | Destination$ Graveyard " +
"| Description$ If the creature would die this turn, exile it instead.";
String effect = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ " + zone;
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
re.setLayer(ReplacementLayer.Other);
re.setOverridingAbility(AbilityFactory.getAbility(effect, eff));
eff.addReplacementEffect(re);
// Add forgot trigger
addForgetOnMovedTrigger(eff, "Battlefield");
// Copy text changes
if (sa.isIntrinsic()) {
eff.copyChangedTextFrom(host);
}
final GameCommand endEffect = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
@Override
public void run() {
game.getAction().exile(eff, null);
}
};
game.getEndOfTurn().addUntil(endEffect);
eff.updateStateForView();
// TODO: Add targeting to the effect so it knows who it's dealing with
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
}
}

View File

@@ -1,6 +1,8 @@
package forge.game.ability.effects;
import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -68,6 +70,7 @@ public class DamageDealEffect extends DamageBaseEffect {
@Override
public void resolve(SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final String damage = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(hostCard, damage, sa);
@@ -176,7 +179,12 @@ public class DamageDealEffect extends DamageBaseEffect {
dmg = (sa.usesTargeting() && sa.hasParam("DividedAsYouChoose")) ? sa.getTargetRestrictions().getDividedValue(o) : dmg;
if (o instanceof Card) {
final Card c = (Card) o;
if (c.isInPlay() && (!targeted || c.canBeTargetedBy(sa))) {
final Card gc = game.getCardState(c, null);
if (gc == null || !c.equalsWithTimestamp(gc) || !gc.isInPlay()) {
// timestamp different or not in play
continue;
}
if (!targeted || c.canBeTargetedBy(sa)) {
if (removeDamage) {
c.setDamage(0);
c.setHasBeenDealtDeathtouchDamage(false);

View File

@@ -137,7 +137,7 @@ public class DebuffEffect extends SpellAbilityEffect {
}
removedKW.addAll(kws);
tgtC.addChangedCardKeywords(addedKW, removedKW, false, timestamp);
tgtC.addChangedCardKeywords(addedKW, removedKW, false, false, timestamp);
}
if (!sa.hasParam("Permanent")) {
game.getEndOfTurn().addUntil(new GameCommand() {

View File

@@ -35,7 +35,7 @@ public class DigEffect extends SpellAbilityEffect {
sb.append(Lang.nounWithAmount(numToDig, "card")).append(" of ");
if (tgtPlayers.contains(host.getController())) {
sb.append("his or her ");
sb.append("their ");
}
else {
for (final Player p : tgtPlayers) {
@@ -377,7 +377,7 @@ public class DigEffect extends SpellAbilityEffect {
}
} else if (destZone2 == ZoneType.Exile) {
if (sa.hasParam("ExileWithCounter")) {
c.addCounter(CounterType.getType(sa.getParam("ExileWithCounter")), 1, effectHost, true);
c.addCounter(CounterType.getType(sa.getParam("ExileWithCounter")), 1, player, true);
}
c.setExiledWith(effectHost);
}

View File

@@ -37,7 +37,7 @@ public class DigUntilEffect extends SpellAbilityEffect {
sb.append(pl).append(" ");
}
sb.append("reveals cards from his or her library until revealing ");
sb.append("reveals cards from their library until revealing ");
sb.append(untilAmount).append(" ").append(desc).append(" card");
if (untilAmount != 1) {
sb.append("s");
@@ -56,18 +56,18 @@ public class DigUntilEffect extends SpellAbilityEffect {
sb.append(" ");
if (found.equals(ZoneType.Hand)) {
sb.append("into his or her hand ");
sb.append("into their hand ");
}
if (revealed.equals(ZoneType.Graveyard)) {
sb.append("and all other cards into his or her graveyard.");
sb.append("and all other cards into their graveyard.");
}
if (revealed.equals(ZoneType.Exile)) {
sb.append("and exile all other cards revealed this way.");
}
} else {
if (revealed.equals(ZoneType.Hand)) {
sb.append("all cards revealed this way into his or her hand");
sb.append("all cards revealed this way into their hand");
}
}
return sb.toString();

View File

@@ -35,9 +35,9 @@ public class DiscardEffect extends SpellAbilityEffect {
}
if (mode.equals("RevealYouChoose")) {
sb.append("reveals his or her hand.").append(" You choose (");
sb.append("reveals their hand.").append(" You choose (");
} else if (mode.equals("RevealDiscardAll")) {
sb.append("reveals his or her hand. Discard (");
sb.append("reveals their hand. Discard (");
} else {
sb.append("discards (");
}
@@ -48,7 +48,7 @@ public class DiscardEffect extends SpellAbilityEffect {
}
if (mode.equals("Hand")) {
sb.append("his or her hand");
sb.append("their hand");
} else if (mode.equals("RevealDiscardAll")) {
sb.append("All");
} else if (sa.hasParam("AnyNumber")) {

View File

@@ -44,7 +44,6 @@ public class ExploreEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
// check if only the activating player counts
final Card card = sa.getHostCard();
final Player pl = sa.getActivatingPlayer();
final PlayerController pc = pl.getController();
final Game game = pl.getGame();
@@ -78,7 +77,7 @@ public class ExploreEffect extends SpellAbilityEffect {
// if the card is not more in the game anymore
// this might still return true but its no problem
if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.getTimestamp() == c.getTimestamp()) {
c.addCounter(CounterType.P1P1, 1, card, true);
c.addCounter(CounterType.P1P1, 1, pl, true);
}
}

View File

@@ -41,10 +41,10 @@ public class FightEffect extends DamageBaseEffect {
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
List<Card> fighters = getFighters(sa);
final Game game = sa.getActivatingPlayer().getGame();
final Game game = host.getGame();
if (fighters.size() < 2 || !fighters.get(0).isInPlay()
|| !fighters.get(1).isInPlay()) {
// check is done in getFighters
if (fighters.size() < 2) {
return;
}
@@ -55,21 +55,7 @@ public class FightEffect extends DamageBaseEffect {
}
}
boolean fightToughness = sa.hasParam("FightWithToughness");
CardDamageMap damageMap = new CardDamageMap();
CardDamageMap preventMap = new CardDamageMap();
// Damage is dealt simultaneously, so we calculate the damage from source to target before it is applied
final int dmg1 = fightToughness ? fighters.get(0).getNetToughness() : fighters.get(0).getNetPower();
final int dmg2 = fightToughness ? fighters.get(1).getNetToughness() : fighters.get(1).getNetPower();
dealDamage(fighters.get(0), fighters.get(1), dmg1, damageMap, preventMap, sa);
dealDamage(fighters.get(1), fighters.get(0), dmg2, damageMap, preventMap, sa);
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
replaceDying(sa);
dealDamage(sa, fighters.get(0), fighters.get(1));
for (Card c : fighters) {
final Map<String, Object> runParams = Maps.newHashMap();
@@ -83,6 +69,8 @@ public class FightEffect extends DamageBaseEffect {
Card fighter1 = null;
Card fighter2 = null;
final Card host = sa.getHostCard();
final Game game = host.getGame();
List<Card> tgts = null;
if (sa.usesTargeting()) {
@@ -92,12 +80,27 @@ public class FightEffect extends DamageBaseEffect {
}
}
if (sa.hasParam("Defined")) {
List<Card> defined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
List<Card> defined = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
// Allow both fighters to come from defined list if first fighter not already found
if (sa.hasParam("ExtraDefined")) {
defined.addAll(AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("ExtraDefined"), sa));
defined.addAll(AbilityUtils.getDefinedCards(host, sa.getParam("ExtraDefined"), sa));
}
List<Card> newDefined = Lists.newArrayList();
for (final Card d : defined) {
final Card g = game.getCardState(d, null);
// 701.12b If a creature instructed to fight is no longer on the battlefield or is no longer a creature,
// no damage is dealt. If a creature is an illegal target
// for a resolving spell or ability that instructs it to fight, no damage is dealt.
if (g == null || !g.equalsWithTimestamp(d) || !d.isInPlay() || !d.isCreature()) {
// Test to see if the card we're trying to add is in the expected state
continue;
}
newDefined.add(g);
}
// replace with new List using CardState
defined = newDefined;
if (!defined.isEmpty()) {
if (defined.size() > 1 && fighter1 == null) {
fighter1 = defined.get(0);
@@ -120,9 +123,38 @@ public class FightEffect extends DamageBaseEffect {
return fighterList;
}
private void dealDamage(Card source, Card target, int damage, CardDamageMap damageMap, CardDamageMap preventMap, final SpellAbility sa) {
target.addDamage(damage, source, damageMap, preventMap, sa);
}
private void dealDamage(final SpellAbility sa, Card fighterA, Card fighterB) {
boolean fightToughness = sa.hasParam("FightWithToughness");
boolean usedDamageMap = true;
CardDamageMap damageMap = sa.getDamageMap();
CardDamageMap preventMap = sa.getPreventMap();
if (damageMap == null) {
// make a new damage map
damageMap = new CardDamageMap();
preventMap = new CardDamageMap();
usedDamageMap = false;
}
// 701.12c If a creature fights itself, it deals damage to itself equal to twice its power.
final int dmg1 = fightToughness ? fighterA.getNetToughness() : fighterA.getNetPower();
if (fighterA.equals(fighterB)) {
fighterA.addDamage(dmg1 * 2, fighterA, damageMap, preventMap, sa);
} else {
final int dmg2 = fightToughness ? fighterB.getNetToughness() : fighterB.getNetPower();
fighterB.addDamage(dmg1, fighterA, damageMap, preventMap, sa);
fighterA.addDamage(dmg2, fighterB, damageMap, preventMap, sa);
}
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
}
replaceDying(sa);
}
}

View File

@@ -118,9 +118,9 @@ public class FlipCoinEffect extends SpellAbilityEffect {
if (sa.getParam("RememberWinner") != null) {
host.addRemembered(host);
}
AbilitySub sub = sa.getAdditionalAbility("WinSubAbility");
if (sub != null) {
AbilityUtils.resolve(sub);
if (sa.hasAdditionalAbility("WinSubAbility")) {
AbilityUtils.resolve(sa.getAdditionalAbility("WinSubAbility"));
}
// runParams.put("Won","True");
} else {
@@ -128,9 +128,8 @@ public class FlipCoinEffect extends SpellAbilityEffect {
host.addRemembered(host);
}
AbilitySub sub = sa.getAdditionalAbility("LoseSubAbility");
if (sub != null) {
AbilityUtils.resolve(sub);
if (sa.hasAdditionalAbility("LoseSubAbility")) {
AbilityUtils.resolve(sa.getAdditionalAbility("LoseSubAbility"));
}
// runParams.put("Won","False");
}
@@ -167,7 +166,7 @@ public class FlipCoinEffect extends SpellAbilityEffect {
flipper.getGame().getAction().nofityOfValue(sa, flipper, result ? "heads" : "tails", null);
} while (sa.hasParam("FlipUntilYouLose") && result != false);
if (sa.hasParam("FlipUntilYouLose")) {
if (sa.hasParam("FlipUntilYouLose") && sa.hasAdditionalAbility("LoseSubAbility")) {
sa.getAdditionalAbility("LoseSubAbility").setSVar(varName, "Number$" + numSuccesses);
}
@@ -216,7 +215,7 @@ public class FlipCoinEffect extends SpellAbilityEffect {
caller.getGame().getTriggerHandler().runTrigger(TriggerType.FlippedCoin, runParams, false);
} while (sa.hasParam("FlipUntilYouLose") && wonFlip);
if (sa.hasParam("FlipUntilYouLose")) {
if (sa.hasParam("FlipUntilYouLose") && sa.hasAdditionalAbility("LoseSubAbility")) {
sa.getAdditionalAbility("LoseSubAbility").setSVar(varName, "Number$" + numSuccesses);
}

View File

@@ -1,5 +1,6 @@
package forge.game.ability.effects;
import forge.game.Game;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
@@ -8,10 +9,14 @@ public class HauntEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
if (sa.usesTargeting() && !card.isToken()) {
Card card = sa.getHostCard();
final Game game = card.getGame();
card = game.getCardState(card, null);
if (card == null) {
return;
} else if (sa.usesTargeting() && !card.isToken()) {
// haunt target but only if card is no token
final Card copy = card.getGame().getAction().exile(card, sa);
final Card copy = game.getAction().exile(card, sa);
sa.getTargets().getFirstTargetedCard().addHauntedBy(copy);
} else if (!sa.usesTargeting() && card.getHaunting() != null) {
// unhaunt

View File

@@ -0,0 +1,80 @@
package forge.game.ability.effects;
import com.google.common.collect.Maps;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import java.util.Map;
public class ImmediateTriggerEffect extends SpellAbilityEffect {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected String getStackDescription(SpellAbility sa) {
if (sa.hasParam("TriggerDescription")) {
return sa.getParam("TriggerDescription");
}
return "";
}
@Override
public void resolve(SpellAbility sa) {
Map<String, String> mapParams = Maps.newHashMap(sa.getMapParams());
if (mapParams.containsKey("Cost")) {
mapParams.remove("Cost");
}
if (mapParams.containsKey("SpellDescription")) {
mapParams.put("TriggerDescription", mapParams.get("SpellDescription"));
mapParams.remove("SpellDescription");
}
String triggerRemembered = null;
// Set Remembered
if (sa.hasParam("RememberObjects")) {
triggerRemembered = sa.getParam("RememberObjects");
}
mapParams.put("Mode", TriggerType.Immediate.name());
final Trigger immediateTrig = TriggerHandler.parseTrigger(mapParams, sa.getHostCard(), true);
if (sa.hasParam("CopyTriggeringObjects")) {
immediateTrig.setStoredTriggeredObjects(sa.getTriggeringObjects());
}
// Need to copy paid costs
if (triggerRemembered != null) {
for (final String rem : triggerRemembered.split(",")) {
for (final Object o : AbilityUtils.getDefinedObjects(sa.getHostCard(), rem, sa)) {
if (o instanceof SpellAbility) {
// "RememberObjects$ Remembered" don't remember spellability
continue;
}
immediateTrig.addRemembered(o);
}
}
}
if (mapParams.containsKey("Execute") || sa.hasAdditionalAbility("Execute")) {
SpellAbility overridingSA = sa.getAdditionalAbility("Execute");
overridingSA.setActivatingPlayer(sa.getActivatingPlayer());
immediateTrig.setOverridingAbility(overridingSA);
}
final TriggerHandler trigHandler = sa.getActivatingPlayer().getGame().getTriggerHandler();
// Instead of registering this, add to the delayed triggers as an immediate trigger type? Which means it'll fire as soon as possible
trigHandler.registerDelayedTrigger(immediateTrig);
}
}

View File

@@ -104,7 +104,7 @@ public class MillEffect extends SpellAbilityEffect {
sb.append("s");
}
final String millPosition = sa.hasParam("FromBottom") ? "bottom" : "top";
sb.append(" from the " + millPosition + " of his or her library.");
sb.append(" from the " + millPosition + " of their library.");
return sb.toString();

View File

@@ -36,7 +36,7 @@ public class MustAttackEffect extends SpellAbilityEffect {
for (final Player player : tgtPlayers) {
sb.append("Creatures ").append(player).append(" controls attack ");
sb.append(defender).append(" during his or her next turn.");
sb.append(defender).append(" during their next turn.");
}
for (final Card c : getTargetCards(sa)) {
sb.append(c).append(" must attack ");

View File

@@ -90,7 +90,7 @@ public class ProtectAllEffect extends SpellAbilityEffect {
for (final Card tgtC : list) {
if (tgtC.isInPlay()) {
tgtC.addChangedCardKeywords(gainsKWList, ImmutableList.<String>of(), false, timestamp, true);
tgtC.addChangedCardKeywords(gainsKWList, null, false, false, timestamp, true);
if (!sa.hasParam("Permanent")) {
// If not Permanent, remove protection at EOT

View File

@@ -1,6 +1,5 @@
package forge.game.ability.effects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import forge.GameCommand;
import forge.card.MagicColor;
@@ -153,7 +152,7 @@ public class ProtectEffect extends SpellAbilityEffect {
continue;
}
tgtC.addChangedCardKeywords(gainsKWList, ImmutableList.<String>of(), false, timestamp, true);
tgtC.addChangedCardKeywords(gainsKWList, null, false, false, timestamp, true);
if (!sa.hasParam("Permanent")) {
// If not Permanent, remove protection at EOT
@@ -181,7 +180,7 @@ public class ProtectEffect extends SpellAbilityEffect {
continue;
}
unTgtC.addChangedCardKeywords(gainsKWList, ImmutableList.<String>of(), false, timestamp, true);
unTgtC.addChangedCardKeywords(gainsKWList, null, false, false, timestamp, true);
if (!sa.hasParam("Permanent")) {
// If not Permanent, remove protection at EOT

View File

@@ -13,10 +13,11 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.google.common.collect.Lists;
public class PumpAllEffect extends SpellAbilityEffect {
private static void applyPumpAll(final SpellAbility sa,
final List<Card> list, final int a, final int d,
@@ -24,23 +25,18 @@ public class PumpAllEffect extends SpellAbilityEffect {
final Game game = sa.getActivatingPlayer().getGame();
final long timestamp = game.getNextTimestamp();
final List<String> kws = new ArrayList<String>();
final List<String> hiddenkws = new ArrayList<String>();
boolean suspend = false;
final List<String> kws = Lists.newArrayList();
final List<String> hiddenkws = Lists.newArrayList();
for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
hiddenkws.add(kw);
} else {
kws.add(kw);
if (kw.equals("Suspend")) {
suspend = true;
}
}
}
for (final Card tgtC : list) {
// only pump things in the affected zones.
boolean found = false;
for (final ZoneType z : affectedZones) {
@@ -55,7 +51,7 @@ public class PumpAllEffect extends SpellAbilityEffect {
tgtC.addTempPowerBoost(a);
tgtC.addTempToughnessBoost(d);
tgtC.addChangedCardKeywords(kws, new ArrayList<String>(), false, timestamp);
tgtC.addChangedCardKeywords(kws, null, false, false, timestamp);
for (String kw : hiddenkws) {
tgtC.addHiddenExtrinsicKeyword(kw);
@@ -118,13 +114,11 @@ public class PumpAllEffect extends SpellAbilityEffect {
@Override
public void resolve(final SpellAbility sa) {
final List<Player> tgtPlayers = getTargetPlayers(sa);
final List<ZoneType> affectedZones = new ArrayList<ZoneType>();
final List<ZoneType> affectedZones = Lists.newArrayList();
final Game game = sa.getActivatingPlayer().getGame();
if (sa.hasParam("PumpZone")) {
for (final String zone : sa.getParam("PumpZone").split(",")) {
affectedZones.add(ZoneType.valueOf(zone));
}
affectedZones.addAll(ZoneType.listValueOf(sa.getParam("PumpZone")));
} else {
affectedZones.add(ZoneType.Battlefield);
}
@@ -149,7 +143,10 @@ public class PumpAllEffect extends SpellAbilityEffect {
list = (CardCollection)AbilityUtils.filterListByType(list, valid, sa);
List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<String>();
List<String> keywords = Lists.newArrayList();
if (sa.hasParam("KW")) {
keywords.addAll(Arrays.asList(sa.getParam("KW").split(" & ")));
}
final int a = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa, true);
final int d = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa, true);
@@ -161,6 +158,8 @@ public class PumpAllEffect extends SpellAbilityEffect {
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard());
}
applyPumpAll(sa, list, a, d, keywords, affectedZones);
replaceDying(sa);
} // pumpAllResolve()
}

View File

@@ -12,7 +12,6 @@ import forge.game.event.GameEventCardStatsChanged;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Lang;
@@ -31,32 +30,40 @@ public class PumpEffect extends SpellAbilityEffect {
final int a, final int d, final List<String> keywords,
final long timestamp) {
final Card host = sa.getHostCard();
final Game game = host.getGame();
//if host is not on the battlefield don't apply
// Suspend should does Affect the Stack
if (sa.hasParam("UntilLoseControlOfHost")
&& !(host.isInPlay() || host.isInZone(ZoneType.Stack))) {
return;
}
final Game game = sa.getActivatingPlayer().getGame();
// do Game Check there in case of LKI
final Card gameCard = game.getCardState(applyTo, null);
if (gameCard == null || !applyTo.equalsWithTimestamp(gameCard)) {
return;
}
final List<String> kws = Lists.newArrayList();
boolean redrawPT = false;
for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
applyTo.addHiddenExtrinsicKeyword(kw);
gameCard.addHiddenExtrinsicKeyword(kw);
redrawPT |= kw.contains("CARDNAME's power and toughness are switched");
} else {
kws.add(kw);
}
}
applyTo.addTempPowerBoost(a);
applyTo.addTempToughnessBoost(d);
applyTo.addChangedCardKeywords(kws, Lists.<String>newArrayList(), false, timestamp);
if (redrawPT) { applyTo.updatePowerToughnessForView(); }
gameCard.addTempPowerBoost(a);
gameCard.addTempToughnessBoost(d);
gameCard.addChangedCardKeywords(kws, Lists.<String>newArrayList(), false, false, timestamp);
if (redrawPT) {
gameCard.updatePowerToughnessForView();
}
if (sa.hasParam("LeaveBattlefield")) {
addLeaveBattlefieldReplacement(applyTo, sa, sa.getParam("LeaveBattlefield"));
addLeaveBattlefieldReplacement(gameCard, sa, sa.getParam("LeaveBattlefield"));
}
if (!sa.hasParam("Permanent")) {
@@ -66,8 +73,8 @@ public class PumpEffect extends SpellAbilityEffect {
@Override
public void run() {
applyTo.addTempPowerBoost(-1 * a);
applyTo.addTempToughnessBoost(-1 * d);
gameCard.addTempPowerBoost(-1 * a);
gameCard.addTempToughnessBoost(-1 * d);
if (keywords.size() > 0) {
boolean redrawPT = false;
@@ -75,16 +82,16 @@ public class PumpEffect extends SpellAbilityEffect {
for (String kw : keywords) {
redrawPT |= kw.contains("CARDNAME's power and toughness are switched");
if (kw.startsWith("HIDDEN")) {
applyTo.removeHiddenExtrinsicKeyword(kw);
gameCard.removeHiddenExtrinsicKeyword(kw);
if (redrawPT) {
applyTo.updatePowerToughnessForView();
gameCard.updatePowerToughnessForView();
}
}
}
applyTo.removeChangedCardKeywords(timestamp);
gameCard.removeChangedCardKeywords(timestamp);
}
game.fireEvent(new GameEventCardStatsChanged(applyTo));
game.fireEvent(new GameEventCardStatsChanged(gameCard));
}
};
if (sa.hasParam("UntilEndOfCombat")) {
@@ -107,12 +114,19 @@ public class PumpEffect extends SpellAbilityEffect {
game.getEndOfTurn().addUntil(untilEOT);
}
}
game.fireEvent(new GameEventCardStatsChanged(applyTo));
game.fireEvent(new GameEventCardStatsChanged(gameCard));
}
private static void applyPump(final SpellAbility sa, final Player p,
final List<String> keywords, final long timestamp) {
final Game game = p.getGame();
final Card host = sa.getHostCard();
//if host is not on the battlefield don't apply
// Suspend should does Affect the Stack
if (sa.hasParam("UntilLoseControlOfHost")
&& !(host.isInPlay() || host.isInZone(ZoneType.Stack))) {
return;
}
p.addChangedKeywords(keywords, ImmutableList.<String>of(), timestamp);
if (!sa.hasParam("Permanent")) {
@@ -134,6 +148,9 @@ public class PumpEffect extends SpellAbilityEffect {
game.getEndOfCombat().addUntil(untilEOT);
} else if (sa.hasParam("UntilYourNextUpkeep")) {
game.getUpkeep().addUntil(sa.getActivatingPlayer(), untilEOT);
} else if (sa.hasParam("UntilLoseControlOfHost")) {
sa.getHostCard().addLeavesPlayCommand(untilEOT);
sa.getHostCard().addChangeControllerCommand(untilEOT);
} else {
game.getEndOfTurn().addUntil(untilEOT);
}
@@ -208,7 +225,6 @@ public class PumpEffect extends SpellAbilityEffect {
public void resolve(final SpellAbility sa) {
final List<Card> untargetedCards = Lists.newArrayList();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = sa.getActivatingPlayer().getGame();
final Card host = sa.getHostCard();
final long timestamp = game.getNextTimestamp();
@@ -251,7 +267,7 @@ public class PumpEffect extends SpellAbilityEffect {
final String landtype = sa.getParam("DefinedLandwalk");
final Card c = AbilityUtils.getDefinedCards(host, landtype, sa).get(0);
for (String type : c.getType()) {
if (CardType.isALandType(type) || CardType.isABasicLandType(type)) {
if (CardType.isALandType(type)) {
keywords.add(type + "walk");
}
}
@@ -340,7 +356,7 @@ public class PumpEffect extends SpellAbilityEffect {
}
// if pump is a target, make sure we can still target now
if ((tgt != null) && !tgtC.canBeTargetedBy(sa)) {
if (sa.usesTargeting() && !tgtC.canBeTargetedBy(sa)) {
continue;
}
@@ -367,5 +383,7 @@ public class PumpEffect extends SpellAbilityEffect {
applyPump(sa, p, keywords, timestamp);
}
replaceDying(sa);
} // pumpResolve()
}

View File

@@ -51,7 +51,7 @@ public class RearrangeTopOfLibraryEffect extends SpellAbilityEffect {
ret.append("that");
}
ret.append(" player shuffle his or her library.");
ret.append(" player shuffle their library.");
}
return ret.toString();

View File

@@ -48,6 +48,10 @@ public class ReplaceEffect extends SpellAbilityEffect {
params.put(varName, AbilityUtils.calculateAmount(card, varValue, sa));
}
if (params.containsKey("EffectOnly")) {
params.put("EffectOnly", true);
}
//try to call replacementHandler with new Params
ReplacementResult result = game.getReplacementHandler().run(params);
switch (result) {

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