diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index a96ee50cd61..b88caf8c09a 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -494,7 +494,7 @@ public class PlayerControllerAi extends PlayerController { } @Override - public TargetChoices chooseNewTargetsFor(SpellAbility ability) { + public TargetChoices chooseNewTargetsFor(SpellAbility ability, Predicate filter, boolean optional) { // AI currently can't do this. But when it can it will need to be based on Ability API return null; } @@ -978,9 +978,13 @@ public class PlayerControllerAi extends PlayerController { if (sa.isSpell()) { player.getGame().getStackZone().add(sa.getHostCard()); } - // TODO check if static abilities needs to be run for things affecting the copy? + + /* FIXME: the new implementation (below) requires implementing setupNewTargets in the AI controller, among other possible changes, otherwise breaks AI + if (sa.isMayChooseNewTargets()) { + sa.setupNewTargets(player); + } + */ if (sa.isMayChooseNewTargets() && !sa.setupTargets()) { - // if targets can't be done, remove copy from existence if (sa.isSpell()) { sa.getHostCard().ceaseToExist(); } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index dee21c7340b..36e8a3839d2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -126,7 +126,7 @@ public class CountersPutAi extends SpellAbilityAi { Card choice = null; final String type = sa.getParam("CounterType"); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final String logic = sa.getParamOrDefault("AILogic", ""); PhaseHandler ph = ai.getGame().getPhaseHandler(); @@ -398,16 +398,15 @@ public class CountersPutAi extends SpellAbilityAi { } if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa)) { - final TargetRestrictions abTgt = sa.getTargetRestrictions(); // only evaluates case where all tokens are placed on a single target - if (sa.usesTargeting() && abTgt.getMinTargets(source, sa) < 2) { + if (sa.usesTargeting() && sa.getMinTargets() < 2) { if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) { Card c = sa.getTargets().getFirstTargetedCard(); if (sa.getTargets().size() > 1) { sa.resetTargets(); sa.getTargets().add(c); } - abTgt.addDividedAllocation(sa.getTargetCard(), amount); + sa.addDividedAllocation(sa.getTargetCard(), amount); return true; } else { return false; @@ -462,7 +461,6 @@ public class CountersPutAi extends SpellAbilityAi { } if (sourceName.equals("Abzan Charm")) { - final TargetRestrictions abTgt = sa.getTargetRestrictions(); // specific AI for instant with distribute two +1/+1 counters ComputerUtilCard.sortByEvaluateCreature(list); // maximise the number of targets @@ -472,11 +470,11 @@ public class CountersPutAi extends SpellAbilityAi { if (ComputerUtilCard.shouldPumpCard(ai, sa, c, i, i, Lists.newArrayList())) { sa.getTargets().add(c); - abTgt.addDividedAllocation(c, i); + sa.addDividedAllocation(c, i); left -= i; } - if (left < i || sa.getTargets().size() == abTgt.getMaxTargets(source, sa)) { - abTgt.addDividedAllocation(sa.getTargets().getFirstTargetedCard(), left + i); + if (left < i || sa.getTargets().size() == sa.getMaxTargets()) { + sa.addDividedAllocation(sa.getTargets().getFirstTargetedCard(), left + i); left = 0; break; } @@ -542,7 +540,7 @@ public class CountersPutAi extends SpellAbilityAi { list.remove(choice); sa.getTargets().add(choice); if (divided) { - sa.getTargetRestrictions().addDividedAllocation(choice, amount); + sa.addDividedAllocation(choice, amount); break; } choice = null; @@ -609,7 +607,7 @@ public class CountersPutAi extends SpellAbilityAi { final String logic = sa.getParamOrDefault("AILogic", ""); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger()) @@ -672,7 +670,7 @@ public class CountersPutAi extends SpellAbilityAi { list.remove(choice); sa.getTargets().add(choice); if (divided) { - sa.getTargetRestrictions().addDividedAllocation(choice, amount); + sa.addDividedAllocation(choice, amount); break; } } @@ -690,7 +688,7 @@ public class CountersPutAi extends SpellAbilityAi { CardCollection list; final String type = sa.getParam("CounterType"); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); int left = amount; @@ -818,12 +816,11 @@ public class CountersPutAi extends SpellAbilityAi { } } if (choice != null && divided) { - final TargetRestrictions abTgt = sa.getTargetRestrictions(); int alloc = Math.max(amount / totalTargets, 1); - if (sa.getTargets().size() == Math.min(totalTargets, abTgt.getMaxTargets(sa.getHostCard(), sa)) - 1) { - abTgt.addDividedAllocation(choice, left); + if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) { + sa.addDividedAllocation(choice, left); } else { - abTgt.addDividedAllocation(choice, alloc); + sa.addDividedAllocation(choice, alloc); left -= alloc; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index 35a2ba27f47..471cf90696f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -268,7 +268,7 @@ public class DamageDealAi extends DamageAiBase { if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) || sourceName.equals("Crater's Claws")){ // If I can kill my target by paying less mana, do it - if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.hasParam("DividedAsYouChoose")) { + if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.isDividedAsYouChoose()) { int actualPay = dmg; final boolean noPrevention = sa.hasParam("NoPrevention"); for (final Card c : sa.getTargets().getTargetCards()) { @@ -547,7 +547,7 @@ public class DamageDealAi extends DamageAiBase { final boolean noPrevention = sa.hasParam("NoPrevention"); final Game game = source.getGame(); final PhaseHandler phase = game.getPhaseHandler(); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer"); final String logic = sa.getParamOrDefault("AILogic", ""); @@ -613,7 +613,7 @@ public class DamageDealAi extends DamageAiBase { if (assignedDamage <= dmg && humanCreature.getShieldCount() == 0 && !ComputerUtil.canRegenerate(humanCreature.getController(), humanCreature)) { tcs.add(humanCreature); - tgt.addDividedAllocation(humanCreature, assignedDamage); + sa.addDividedAllocation(humanCreature, assignedDamage); lastTgt = humanCreature; dmg -= assignedDamage; } @@ -625,7 +625,7 @@ public class DamageDealAi extends DamageAiBase { } } if (dmg > 0 && lastTgt != null) { - tgt.addDividedAllocation(lastTgt, tgt.getDividedValue(lastTgt) + dmg); + sa.addDividedAllocation(lastTgt, sa.getDividedValue(lastTgt) + dmg); dmg = 0; return true; } @@ -635,14 +635,14 @@ public class DamageDealAi extends DamageAiBase { continue; } tcs.add(humanCreature); - tgt.addDividedAllocation(humanCreature, dmg); + sa.addDividedAllocation(humanCreature, dmg); dmg = 0; return true; } } int totalTargetedSoFar = -1; - while (tcs.size() < tgt.getMaxTargets(source, sa)) { + while (sa.canAddMoreTarget()) { if (totalTargetedSoFar == tcs.size()) { // Avoid looping endlessly when choosing targets for cards with variable target number and type // like Jaya's Immolating Inferno @@ -664,7 +664,7 @@ public class DamageDealAi extends DamageAiBase { if (divided) { int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention); assignedDamage = Math.min(dmg, assignedDamage); - tgt.addDividedAllocation(c, assignedDamage); + sa.addDividedAllocation(c, assignedDamage); dmg = dmg - assignedDamage; if (dmg <= 0) { break; @@ -680,7 +680,7 @@ public class DamageDealAi extends DamageAiBase { if (this.shouldTgtP(ai, sa, dmg, noPrevention)) { tcs.add(enemy); if (divided) { - tgt.addDividedAllocation(enemy, dmg); + sa.addDividedAllocation(enemy, dmg); break; } continue; @@ -702,7 +702,7 @@ public class DamageDealAi extends DamageAiBase { if (divided) { final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention); if (assignedDamage <= dmg) { - tgt.addDividedAllocation(c, assignedDamage); + sa.addDividedAllocation(c, assignedDamage); } dmg = dmg - assignedDamage; if (dmg <= 0) { @@ -739,7 +739,7 @@ public class DamageDealAi extends DamageAiBase { if (freePing && sa.canTarget(enemy) && (!avoidTargetP(ai, sa))) { tcs.add(enemy); if (divided) { - tgt.addDividedAllocation(enemy, dmg); + sa.addDividedAllocation(enemy, dmg); break; } } @@ -757,9 +757,9 @@ public class DamageDealAi extends DamageAiBase { if (divided) { final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention); if (assignedDamage <= dmg) { - tgt.addDividedAllocation(c, assignedDamage); + sa.addDividedAllocation(c, assignedDamage); } else { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); } dmg = dmg - assignedDamage; if (dmg <= 0) { @@ -785,7 +785,7 @@ public class DamageDealAi extends DamageAiBase { (!avoidTargetP(ai, sa))) { tcs.add(enemy); if (divided) { - tgt.addDividedAllocation(enemy, dmg); + sa.addDividedAllocation(enemy, dmg); break; } continue; @@ -880,16 +880,16 @@ public class DamageDealAi extends DamageAiBase { private boolean damageChooseRequiredTargets(final Player ai, final SpellAbility sa, final TargetRestrictions tgt, final int dmg) { // this is for Triggered targets that are mandatory final boolean noPrevention = sa.hasParam("NoPrevention"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final Player opp = ai.getWeakestOpponent(); - while (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { if (tgt.canTgtPlaneswalker()) { final Card c = this.dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, ai, true); if (c != null) { sa.getTargets().add(c); if (divided) { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); break; } continue; @@ -902,7 +902,7 @@ public class DamageDealAi extends DamageAiBase { if (c != null) { sa.getTargets().add(c); if (divided) { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); break; } continue; @@ -912,7 +912,7 @@ public class DamageDealAi extends DamageAiBase { if (sa.canTarget(opp)) { if (sa.getTargets().add(opp)) { if (divided) { - tgt.addDividedAllocation(opp, dmg); + sa.addDividedAllocation(opp, dmg); break; } continue; @@ -927,7 +927,7 @@ public class DamageDealAi extends DamageAiBase { Card c = ComputerUtilCard.getWorstPermanentAI(indestructible, false, false, false, false); sa.getTargets().add(c); if (divided) { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); break; } continue; @@ -938,7 +938,7 @@ public class DamageDealAi extends DamageAiBase { if (c != null) { sa.getTargets().add(c); if (divided) { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); break; } continue; @@ -948,7 +948,7 @@ public class DamageDealAi extends DamageAiBase { if (sa.canTarget(ai)) { if (sa.getTargets().add(ai)) { if (divided) { - tgt.addDividedAllocation(ai, dmg); + sa.addDividedAllocation(ai, dmg); break; } continue; @@ -988,7 +988,7 @@ public class DamageDealAi extends DamageAiBase { return false; } - if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.hasParam("DividedAsYouChoose")) { + if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) { // If I can kill my target by paying less mana, do it int actualPay = 0; final boolean noPrevention = sa.hasParam("NoPrevention"); diff --git a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java index 6e5259f25cb..964c427c649 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java @@ -146,8 +146,8 @@ public class DamagePreventAi extends SpellAbilityAi { } } } - if (tgt != null && sa.hasParam("DividedAsYouChoose") && sa.getTargets() != null && !sa.getTargets().isEmpty()) { - tgt.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); + if (sa.usesTargeting() && sa.isDividedAsYouChoose() && !sa.getTargets().isEmpty()) { + sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); } return chance; @@ -179,12 +179,11 @@ public class DamagePreventAi extends SpellAbilityAi { * @return a boolean. */ private boolean preventDamageMandatoryTarget(final Player ai, final SpellAbility sa, final boolean mandatory) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); sa.resetTargets(); // filter AIs battlefield by what I can target final Game game = ai.getGame(); CardCollectionView targetables = game.getCardsIn(ZoneType.Battlefield); - targetables = CardLists.getValidCards(targetables, tgt.getValidTgts(), ai, sa.getHostCard(), sa); + targetables = CardLists.getTargetableCards(targetables, sa); final List compTargetables = CardLists.filterControlledBy(targetables, ai); Card target = null; @@ -215,8 +214,8 @@ public class DamagePreventAi extends SpellAbilityAi { target = ComputerUtilCard.getCheapestPermanentAI(targetables, sa, true); } sa.getTargets().add(target); - if (sa.hasParam("DividedAsYouChoose")) { - tgt.addDividedAllocation(target, AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); + if (sa.isDividedAsYouChoose()) { + sa.addDividedAllocation(target, AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); } return true; } diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java index 04308485a79..927b315bf00 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java @@ -16,7 +16,6 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetChoices; -import forge.game.spellability.TargetRestrictions; public class GameSimulator { public static boolean COPY_STACK = false; @@ -164,14 +163,12 @@ public class GameSimulator { SpellAbility saOrSubSa = sa; do { if (origSaOrSubSa.usesTargeting()) { - final boolean divided = origSaOrSubSa.hasParam("DividedAsYouChoose"); - final TargetRestrictions origTgtRes = origSaOrSubSa.getTargetRestrictions(); - final TargetRestrictions tgtRes = saOrSubSa.getTargetRestrictions(); + final boolean divided = origSaOrSubSa.isDividedAsYouChoose(); for (final GameObject o : origSaOrSubSa.getTargets()) { final GameObject target = copier.find(o); saOrSubSa.getTargets().add(target); if (divided) { - tgtRes.addDividedAllocation(target, origTgtRes.getDividedValue(o)); + saOrSubSa.addDividedAllocation(target, origSaOrSubSa.getDividedValue(o)); } } } diff --git a/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java b/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java index 164e564a15e..18e158e9a23 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java +++ b/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java @@ -173,16 +173,15 @@ public class PossibleTargetSelector { // Divide up counters, since AI is expected to do this. For now, // divided evenly with left-overs going to the first target. - if (targetingSa.hasParam("DividedAsYouChoose")) { + if (targetingSa.isDividedAsYouChoose()) { final int targetCount = targetingSa.getTargets().getTargetCards().size(); if (targetCount > 0) { final String amountStr = targetingSa.getParam("CounterNum"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, targetingSa); final int amountPerCard = amount / targetCount; int amountLeftOver = amount - (amountPerCard * targetCount); - final TargetRestrictions tgtRes = targetingSa.getTargetRestrictions(); for (GameObject target : targetingSa.getTargets()) { - tgtRes.addDividedAllocation(target, amountPerCard + amountLeftOver); + targetingSa.addDividedAllocation(target, amountPerCard + amountLeftOver); amountLeftOver = 0; } } diff --git a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index b64e55edf59..752dfbbfdc7 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -364,10 +364,6 @@ public final class AbilityFactory { if (mapParams.containsKey("TargetsWithDifferentCMC")) { abTgt.setDifferentCMC(true); } - if (mapParams.containsKey("DividedAsYouChoose")) { - abTgt.calculateStillToDivide(mapParams.get("DividedAsYouChoose"), null, null); - abTgt.setDividedAsYouChoose(true); - } if (mapParams.containsKey("TargetsAtRandom")) { abTgt.setRandomTarget(true); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java index 56477884311..c52942c35d5 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java @@ -1,9 +1,11 @@ package forge.game.ability.effects; +import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import forge.game.GameEntity; import forge.game.GameObject; +import forge.game.GameObjectPredicates; import forge.game.ability.SpellAbilityEffect; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -19,7 +21,7 @@ import org.apache.commons.lang3.tuple.Pair; import java.util.ArrayList; import java.util.List; -/** +/** * TODO: Write javadoc for this type. * */ @@ -42,9 +44,6 @@ public class ChangeTargetsEffect extends SpellAbilityEffect { continue; } - boolean changesOneTarget = sa.hasParam("ChangeSingleTarget"); // The only card known to replace targets with self is Spellskite - // There is also Muck Drubb but it replaces ALL occurences of a single target with itself (unlike Spellskite that has to be activated for each). - SpellAbilityStackInstance changingTgtSI = si; Player chooser = sa.getActivatingPlayer(); @@ -54,11 +53,11 @@ public class ChangeTargetsEffect extends SpellAbilityEffect { if (isOptional && !chooser.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantChangeAbilityTargets", tgtSA.getHostCard().toString()))) { continue; } - if (changesOneTarget) { + if (sa.hasParam("ChangeSingleTarget")) { // 1. choose a target of target spell List> allTargets = new ArrayList<>(); while(changingTgtSI != null) { - SpellAbility changedSa = changingTgtSI.getSpellAbility(true); + SpellAbility changedSa = changingTgtSI.getSpellAbility(true); if (changedSa.usesTargeting()) { for(GameObject it : changedSa.getTargets()) allTargets.add(ImmutablePair.of(changingTgtSI, it)); @@ -77,6 +76,8 @@ public class ChangeTargetsEffect extends SpellAbilityEffect { GameObject oldTarget = chosenTarget.getValue(); TargetChoices oldTargetBlock = replaceIn.getTargetChoices(); TargetChoices newTargetBlock = oldTargetBlock.clone(); + // gets the divied value from old target + Integer div = oldTargetBlock.getDividedValue(oldTarget); newTargetBlock.remove(oldTarget); replaceIn.updateTarget(newTargetBlock); // 3. test if updated choices would be correct. @@ -84,7 +85,10 @@ public class ChangeTargetsEffect extends SpellAbilityEffect { if (replaceIn.getSpellAbility(true).canTarget(newTarget)) { newTargetBlock.add(newTarget); - replaceIn.updateTarget(newTargetBlock, oldTarget, newTarget); + if (div != null) { + newTargetBlock.addDividedAllocation(newTarget, div); + } + replaceIn.updateTarget(newTargetBlock); } else { replaceIn.updateTarget(oldTargetBlock); @@ -93,36 +97,38 @@ public class ChangeTargetsEffect extends SpellAbilityEffect { else { while(changingTgtSI != null) { SpellAbility changingTgtSA = changingTgtSI.getSpellAbility(true); - if (sa.hasParam("RandomTarget")){ - if (changingTgtSA.usesTargeting()) { + if (changingTgtSA.usesTargeting()) { + // random target and DefinedMagnet works on single targets + if (sa.hasParam("RandomTarget")){ + int div = changingTgtSA.getTotalDividedValue(); changingTgtSA.resetTargets(); List candidates = changingTgtSA.getTargetRestrictions().getAllCandidates(changingTgtSA, true); GameEntity choice = Aggregates.random(candidates); changingTgtSA.getTargets().add(choice); - changingTgtSI.updateTarget(changingTgtSA.getTargets(), null, choice); + if (changingTgtSA.isDividedAsYouChoose()) { + changingTgtSA.addDividedAllocation(choice, div); + } + + changingTgtSI.updateTarget(changingTgtSA.getTargets()); } - } - else if (sa.hasParam("DefinedMagnet")){ - GameObject newTarget = Iterables.getFirst(getDefinedCardsOrTargeted(sa, "DefinedMagnet"), null); - if (newTarget != null && changingTgtSA.canTarget(newTarget)) { - changingTgtSA.resetTargets(); - changingTgtSA.getTargets().add(newTarget); - changingTgtSI.updateTarget(changingTgtSA.getTargets(), null, newTarget); - } - } - else { - // Update targets, with a potential new target - TargetChoices newTarget = sa.getActivatingPlayer().getController().chooseNewTargetsFor(changingTgtSA); - if (null != newTarget) { - if (sa.hasParam("TargetRestriction")) { - if (newTarget.getFirstTargetedCard() != null && newTarget.getFirstTargetedCard(). - isValid(sa.getParam("TargetRestriction").split(","), activator, sa.getHostCard(), sa)) { - changingTgtSI.updateTarget(newTarget); - } else if (newTarget.getFirstTargetedPlayer() != null && newTarget.getFirstTargetedPlayer(). - isValid(sa.getParam("TargetRestriction").split(","), activator, sa.getHostCard(), sa)) { - changingTgtSI.updateTarget(newTarget); + else if (sa.hasParam("DefinedMagnet")){ + GameObject newTarget = Iterables.getFirst(getDefinedCardsOrTargeted(sa, "DefinedMagnet"), null); + if (newTarget != null && changingTgtSA.canTarget(newTarget)) { + int div = changingTgtSA.getTotalDividedValue(); + changingTgtSA.resetTargets(); + changingTgtSA.getTargets().add(newTarget); + changingTgtSI.updateTarget(changingTgtSA.getTargets()); + if (changingTgtSA.isDividedAsYouChoose()) { + changingTgtSA.addDividedAllocation(newTarget, div); } - } else { + } + } + else { + // Update targets, with a potential new target + Predicate filter = sa.hasParam("TargetRestriction") ? GameObjectPredicates.restriction(sa.getParam("TargetRestriction").split(","), activator, sa.getHostCard(), sa) : null; + // TODO Creature.Other might not work yet as it should + TargetChoices newTarget = sa.getActivatingPlayer().getController().chooseNewTargetsFor(changingTgtSA, filter, false); + if (null != newTarget) { changingTgtSI.updateTarget(newTarget); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java index 3c1ea43e9f3..50a18e79e60 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java @@ -111,7 +111,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect { candidates.remove(p); for (GameEntity o : candidates) { - SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA); + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); resetFirstTargetOnCopy(copy, o, targetedSA); copies.add(copy); } @@ -144,12 +144,12 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect { } for (final Card c : valid) { - SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA); + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); resetFirstTargetOnCopy(copy, c, targetedSA); copies.add(copy); } for (final Player p : players) { - SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA); + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); resetFirstTargetOnCopy(copy, p, targetedSA); copies.add(copy); } @@ -157,12 +157,9 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect { } else { for (int i = 0; i < amount; i++) { - SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA); + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); if (sa.hasParam("MayChooseTarget")) { copy.setMayChooseNewTargets(true); - if (copy.usesTargeting()) { - copy.getTargetRestrictions().setMandatory(true); - } } // extra case for Epic to remove the keyword and the last part of the SpellAbility @@ -206,12 +203,9 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect { } int extraAmount = addAmount - copies.size(); for (int i = 0; i < extraAmount; i++) { - SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA); + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); // extra copies added with CopySpellReplacenment currently always has new choose targets copy.setMayChooseNewTargets(true); - if (copy.usesTargeting()) { - copy.getTargetRestrictions().setMandatory(true); - } copies.add(copy); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java index 9bef78eab96..598fc58446e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java @@ -27,13 +27,12 @@ public class CountersMoveEffect extends SpellAbilityEffect { @Override protected String getStackDescription(SpellAbility sa) { - final Card host = sa.getHostCard(); final StringBuilder sb = new StringBuilder(); final List tgtCards = getDefinedCardsOrTargeted(sa); Card source = null; - if (sa.usesTargeting() && sa.getTargetRestrictions().getMinTargets(host, sa) == 2) { + if (sa.usesTargeting() && sa.getMinTargets() == 2) { if (tgtCards.size() < 2) { return ""; } @@ -241,7 +240,7 @@ public class CountersMoveEffect extends SpellAbilityEffect { Card source = null; List tgtCards = getDefinedCardsOrTargeted(sa); // special logic for moving from Target to Target - if (sa.usesTargeting() && sa.getTargetRestrictions().getMinTargets(host, sa) == 2) { + if (sa.usesTargeting() && sa.getMinTargets() == 2) { if (tgtCards.size() < 2) { return; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java index d3ad905eaf2..1dad1191eef 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java @@ -46,7 +46,6 @@ public class CountersPutEffect extends SpellAbilityEffect { protected String getStackDescription(SpellAbility spellAbility) { final StringBuilder stringBuilder = new StringBuilder(); final Card card = spellAbility.getHostCard(); - final boolean dividedAsYouChoose = spellAbility.hasParam("DividedAsYouChoose"); final int amount = AbilityUtils.calculateAmount(card, spellAbility.getParamOrDefault("CounterNum", "1"), spellAbility); //skip the StringBuilder if no targets are chosen ("up to" scenario) @@ -60,7 +59,7 @@ public class CountersPutEffect extends SpellAbilityEffect { stringBuilder.append("Bolster ").append(amount); return stringBuilder.toString(); } - if (dividedAsYouChoose) { + if (spellAbility.isDividedAsYouChoose()) { stringBuilder.append("Distribute "); } else { stringBuilder.append("Put "); @@ -84,7 +83,7 @@ public class CountersPutEffect extends SpellAbilityEffect { stringBuilder.append("s"); } - if (dividedAsYouChoose) { + if (spellAbility.isDividedAsYouChoose()) { stringBuilder.append(" among "); } else { stringBuilder.append(" on "); @@ -96,8 +95,9 @@ public class CountersPutEffect extends SpellAbilityEffect { for(int i = 0; i < targetCards.size(); i++) { Card targetCard = targetCards.get(i); stringBuilder.append(targetCard); - if (spellAbility.getTargetRestrictions().getDividedMap().get(targetCard) != null) // fix null counter stack description - stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" counter)"); + Integer v = spellAbility.getDividedValue(targetCard); + if (v != null) // fix null counter stack description + stringBuilder.append(" (").append(v).append(" counter)"); if(i == targetCards.size() - 2) { stringBuilder.append(" and "); @@ -259,7 +259,7 @@ public class CountersPutEffect extends SpellAbilityEffect { if (obj instanceof Card) { boolean counterAdded = false; - counterAmount = sa.usesTargeting() && sa.hasParam("DividedAsYouChoose") ? sa.getTargetRestrictions().getDividedValue(gameCard) : counterAmount; + counterAmount = sa.usesTargeting() && sa.isDividedAsYouChoose() ? sa.getDividedValue(gameCard) : counterAmount; if (!sa.usesTargeting() || gameCard.canBeTargetedBy(sa)) { if (max != -1) { counterAmount = Math.max(Math.min(max - gameCard.getCounters(counterType), counterAmount), 0); @@ -270,7 +270,7 @@ public class CountersPutEffect extends SpellAbilityEffect { params.put("CounterType", counterType); counterAmount = pc.chooseNumber(sa, Localizer.getInstance().getMessage("lblHowManyCounters"), 0, counterAmount, params); } - if (sa.hasParam("DividedAsYouChoose") && !sa.usesTargeting()) { + if (sa.isDividedAsYouChoose() && !sa.usesTargeting()) { Map params = Maps.newHashMap(); params.put("Target", obj); params.put("CounterType", counterType); @@ -378,7 +378,7 @@ public class CountersPutEffect extends SpellAbilityEffect { card.addRemembered(gameCard); } game.updateLastStateForCard(gameCard); - if (sa.hasParam("DividedAsYouChoose") && !sa.usesTargeting()) { + if (sa.isDividedAsYouChoose() && !sa.usesTargeting()) { counterRemain = counterRemain - counterAmount; } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java index 5f0b4608c55..5b9e0542f5e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java @@ -61,7 +61,7 @@ public class DamageDealEffect extends DamageBaseEffect { if (spellAbility.usesTargeting()) { if (spellAbility.hasParam("DivideEvenly")) { stringBuilder.append("divided evenly (rounded down) to\n"); - } else if (spellAbility.hasParam("DividedAsYouChoose")) { + } else if (spellAbility.isDividedAsYouChoose()) { stringBuilder.append("divided to\n"); } else stringBuilder.append("to "); @@ -75,8 +75,9 @@ public class DamageDealEffect extends DamageBaseEffect { for (int i = 0; i < targetCards.size(); i++) { Card targetCard = targetCards.get(i); stringBuilder.append(targetCard); - if (spellAbility.getTargetRestrictions().getDividedMap().get(targetCard) != null) //fix null damage stack description - stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" damage)"); + Integer v = spellAbility.getDividedValue(targetCard); + if (v != null) //fix null damage stack description + stringBuilder.append(" (").append(v).append(" damage)"); if (i == targetCount - 2) { stringBuilder.append(" and "); @@ -89,8 +90,9 @@ public class DamageDealEffect extends DamageBaseEffect { for (int i = 0; i < players.size(); i++) { Player targetPlayer = players.get(i); stringBuilder.append(targetPlayer); - if (spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer) != null) //fix null damage stack description - stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer)).append(" damage)"); + Integer v = spellAbility.getDividedValue(targetPlayer); + if (v != null) //fix null damage stack description + stringBuilder.append(" (").append(v).append(" damage)"); if (i == players.size() - 2) { stringBuilder.append(" and "); @@ -102,7 +104,7 @@ public class DamageDealEffect extends DamageBaseEffect { } else { if (spellAbility.hasParam("DivideEvenly")) { stringBuilder.append("divided evenly (rounded down) "); - } else if (spellAbility.hasParam("DividedAsYouChoose")) { + } else if (spellAbility.isDividedAsYouChoose()) { stringBuilder.append("divided as you choose "); } stringBuilder.append("to ").append(Lang.joinHomogenous(targets)); @@ -229,7 +231,7 @@ public class DamageDealEffect extends DamageBaseEffect { for (final GameObject o : tgts) { if (!removeDamage) { - dmg = (sa.usesTargeting() && sa.hasParam("DividedAsYouChoose")) ? sa.getTargetRestrictions().getDividedValue(o) : dmg; + dmg = (sa.usesTargeting() && sa.isDividedAsYouChoose()) ? sa.getDividedValue(o) : dmg; if (dmg <= 0) { continue; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/DamagePreventEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DamagePreventEffect.java index 710f2f747b6..54e0395a34e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DamagePreventEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DamagePreventEffect.java @@ -25,7 +25,7 @@ public class DamagePreventEffect extends SpellAbilityEffect { sb.append("Prevent the next "); sb.append(sa.getParam("Amount")); sb.append(" damage that would be dealt "); - if (sa.hasParam("DividedAsYouChoose")) { + if (sa.isDividedAsYouChoose()) { sb.append("between "); } else { sb.append("to "); @@ -75,8 +75,8 @@ public class DamagePreventEffect extends SpellAbilityEffect { final boolean targeted = (sa.usesTargeting()); final boolean preventionWithEffect = sa.hasParam("PreventionSubAbility"); - for (final Object o : tgts) { - numDam = (sa.usesTargeting() && sa.hasParam("DividedAsYouChoose")) ? sa.getTargetRestrictions().getDividedValue(o) : numDam; + for (final GameObject o : tgts) { + numDam = (sa.usesTargeting() && sa.isDividedAsYouChoose()) ? sa.getDividedValue(o) : numDam; if (o instanceof Card) { final Card c = (Card) o; if (c.isInPlay() && (!targeted || c.canBeTargetedBy(sa))) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index 17490640e78..d6f59f35b1c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -260,13 +260,8 @@ public class PlayEffect extends SpellAbilityEffect { continue; } - final boolean noManaCost = sa.hasParam("WithoutManaCost"); - if (noManaCost) { + if (sa.hasParam("WithoutManaCost")) { tgtSA = tgtSA.copyWithNoManaCost(); - // FIXME: a hack to get cards like Detonate only allow legal targets when cast without paying mana cost (with X=0). - if (tgtSA.hasParam("ValidTgtsWithoutManaCost")) { - tgtSA.getTargetRestrictions().changeValidTargets(tgtSA.getParam("ValidTgtsWithoutManaCost").split(",")); - } } else if (sa.hasParam("PlayCost")) { Cost abCost; if ("ManaCost".equals(sa.getParam("PlayCost"))) { diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index dce30d6d5b7..bbcc57a8d4b 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -117,10 +117,9 @@ public class CardFactory { * which wouldn't ordinarily get set during a simple Card.copy() call. *

* */ - private final static Card copySpellHost(final SpellAbility sourceSA, final SpellAbility targetSA){ + private final static Card copySpellHost(final SpellAbility sourceSA, final SpellAbility targetSA, Player controller) { final Card source = sourceSA.getHostCard(); final Card original = targetSA.getHostCard(); - Player controller = sourceSA.getActivatingPlayer(); final Card c = copyCard(original, true); // change the color of the copy (eg: Fork) @@ -168,17 +167,15 @@ public class CardFactory { * @param bCopyDetails * a boolean. */ - public final static SpellAbility copySpellAbilityAndPossiblyHost(final SpellAbility sourceSA, final SpellAbility targetSA) { - Player controller = sourceSA.getActivatingPlayer(); - + public final static SpellAbility copySpellAbilityAndPossiblyHost(final SpellAbility sourceSA, final SpellAbility targetSA, final Player controller) { //it is only necessary to copy the host card if the SpellAbility is a spell, not an ability - final Card c = targetSA.isSpell() ? copySpellHost(sourceSA, targetSA) : targetSA.getHostCard(); + final Card c = targetSA.isSpell() ? copySpellHost(sourceSA, targetSA, controller) : targetSA.getHostCard(); final SpellAbility copySA; if (targetSA.isTrigger() && targetSA.isWrapper()) { - copySA = getCopiedTriggeredAbility((WrappedAbility)targetSA, c); + copySA = getCopiedTriggeredAbility((WrappedAbility)targetSA, c, controller); } else { - copySA = targetSA.copy(c, false); + copySA = targetSA.copy(c, controller, false); } copySA.setCopied(true); @@ -555,12 +552,12 @@ public class CardFactory { * * return a wrapped ability */ - public static SpellAbility getCopiedTriggeredAbility(final WrappedAbility sa, final Card newHost) { + public static SpellAbility getCopiedTriggeredAbility(final WrappedAbility sa, final Card newHost, final Player controller) { if (!sa.isTrigger()) { return null; } - return new WrappedAbility(sa.getTrigger(), sa.getWrappedAbility().copy(newHost, false), sa.getDecider()); + return new WrappedAbility(sa.getTrigger(), sa.getWrappedAbility().copy(newHost, controller, false), controller); } public static CardCloneStates getCloneStates(final Card in, final Card out, final CardTraitBase sa) { diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index bcada42ecd5..b09df0a2b7b 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -112,7 +112,7 @@ public abstract class PlayerController { public abstract Integer announceRequirements(SpellAbility ability, String announce); public abstract CardCollectionView choosePermanentsToSacrifice(SpellAbility sa, int min, int max, CardCollectionView validTargets, String message); public abstract CardCollectionView choosePermanentsToDestroy(SpellAbility sa, int min, int max, CardCollectionView validTargets, String message); - public abstract TargetChoices chooseNewTargetsFor(SpellAbility ability); + public abstract TargetChoices chooseNewTargetsFor(SpellAbility ability, Predicate filter, boolean optional); public abstract boolean chooseTargetsFor(SpellAbility currentAbility); // this is bad a function for it assigns targets to sa inside its body // Specify a target of a spell (Spellskite) diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 11fef9d1084..ac1ef541453 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -152,6 +152,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private TargetRestrictions targetRestrictions = null; private TargetChoices targetChosen = new TargetChoices(); + private Integer dividedValue = null; + private SpellAbilityView view; private StaticAbility mayPlay = null; @@ -1101,7 +1103,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit if (hasParam("TargetingPlayerControls") && entity instanceof Card) { final Card c = (Card) entity; - if (!c.getController().equals(targetingPlayer)) { + if (!c.getController().equals(getTargetingPlayer())) { return false; } } @@ -1417,6 +1419,35 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit targetChosen = new TargetChoices(); } + /** + * @return a boolean dividedAsYouChoose + */ + public boolean isDividedAsYouChoose() { + return hasParam("DividedAsYouChoose"); + } + + public final void addDividedAllocation(final GameObject tgt, final Integer portionAllocated) { + getTargets().addDividedAllocation(tgt, portionAllocated); + } + public Integer getDividedValue(GameObject c) { + return getTargets().getDividedValue(c); + } + + public int getTotalDividedValue() { + return getTargets().getTotalDividedValue(); + } + + public Integer getDividedValue() { + return this.dividedValue; + } + + public int getStillToDivide() { + if (!isDividedAsYouChoose() || dividedValue == null) { + return 0; + } + return dividedValue - getTotalDividedValue(); + } + /** * Reset the first target. * @@ -1424,16 +1455,15 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void resetFirstTarget(GameObject c, SpellAbility originalSA) { SpellAbility sa = this; while (sa != null) { - if (sa.targetRestrictions != null) { - sa.targetChosen = new TargetChoices(); - sa.targetChosen.add(c); - if (!originalSA.targetRestrictions.getDividedMap().isEmpty()) { - sa.targetRestrictions.addDividedAllocation(c, - Iterables.getFirst(originalSA.targetRestrictions.getDividedMap().values(), null)); + if (sa.usesTargeting()) { + sa.resetTargets(); + sa.getTargets().add(c); + if (!originalSA.getTargets().getDividedValues().isEmpty()) { + sa.addDividedAllocation(c, Iterables.getFirst(originalSA.getTargets().getDividedValues(), null)); } break; } - sa = sa.subAbility; + sa = sa.getSubAbility(); } } @@ -1450,7 +1480,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } public boolean isZeroTargets() { - return getTargetRestrictions().getMinTargets(hostCard, this) == 0 && getTargets().size() == 0; + return getMinTargets() == 0 && getTargets().size() == 0; } public boolean isMinTargetChosen() { @@ -1724,6 +1754,27 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return p != null && p.isTargeting(o); } + public boolean setupNewTargets(Player forceTargetingPlayer) { + // Skip to paying if parent ability doesn't target and has no subAbilities. + // (or trigger case where its already targeted) + SpellAbility currentAbility = this; + do { + if (currentAbility.usesTargeting()) { + TargetChoices oldTargets = currentAbility.getTargets(); + if (forceTargetingPlayer.getController().chooseNewTargetsFor(currentAbility, null, true) == null) { + currentAbility.setTargets(oldTargets); + } + } + final AbilitySub subAbility = currentAbility.getSubAbility(); + if (subAbility != null) { + // This is necessary for "TargetsWithDefinedController$ ParentTarget" + subAbility.setParent(currentAbility); + } + currentAbility = subAbility; + } while (currentAbility != null); + return true; + } + public boolean setupTargets() { // Skip to paying if parent ability doesn't target and has no subAbilities. // (or trigger case where its already targeted) @@ -1741,6 +1792,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } else { targetingPlayer = getActivatingPlayer(); } + // don't set targeting player when forceful target, + // "targeting player controls" should not be reset when the spell is copied currentAbility.setTargetingPlayer(targetingPlayer); if (!targetingPlayer.getController().chooseTargetsFor(currentAbility)) { return false; @@ -1756,11 +1809,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return true; } public final void clearTargets() { - final TargetRestrictions tg = getTargetRestrictions(); - if (tg != null) { + if (usesTargeting()) { resetTargets(); - if (hasParam("DividedAsYouChoose")) { - tg.calculateStillToDivide(getParam("DividedAsYouChoose"), getHostCard(), this); + if (isDividedAsYouChoose()) { + this.dividedValue = AbilityUtils.calculateAmount(getHostCard(), this.getParam("DividedAsYouChoose"), this); } } } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java index ee737cb3b05..14106f5c977 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java @@ -32,11 +32,10 @@ import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + import forge.game.GameObject; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -85,7 +84,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { private Integer xManaPaid = null; // Other Paid things - private final HashMap paidHash; + private final Map paidHash; // Additional info // is Kicked, is Buyback @@ -96,7 +95,6 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { private final Map storedSVars = Maps.newHashMap(); - private final List zonesToOpen; private final Map playersWithValidTargets; private final StackItemView view; @@ -109,7 +107,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { activatingPlayer = sa.getActivatingPlayer(); // Payment info - paidHash = new HashMap<>(ability.getPaidHash()); + paidHash = Maps.newHashMap(ability.getPaidHash()); ability.resetPaidHash(); splicedCards = sa.getSplicedCards(); @@ -149,18 +147,13 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { //store zones to open and players to open them for at the time the SpellAbility first goes on the stack based on the selected targets if (tc == null) { - zonesToOpen = null; playersWithValidTargets = null; } else { - zonesToOpen = new ArrayList<>(); - playersWithValidTargets = new HashMap<>(); + playersWithValidTargets = Maps.newHashMap(); for (Card card : tc.getTargetCards()) { ZoneType zoneType = card.getZone() != null ? card.getZone().getZoneType() : null; if (zoneType != ZoneType.Battlefield) { //don't need to worry about targets on battlefield - if (zoneType != null && !zonesToOpen.contains(zoneType)) { - zonesToOpen.add(zoneType); - } playersWithValidTargets.put(card.getController(), null); } } @@ -253,75 +246,32 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { return tc; } - public final List getZonesToOpen() { - return zonesToOpen; - } - public final Map getPlayersWithValidTargets() { return playersWithValidTargets; } public void updateTarget(TargetChoices target) { - updateTarget(target, null, null); - } - - public void updateTarget(TargetChoices target, GameObject oldTarget, GameObject newTarget) { if (target != null) { + TargetChoices oldTarget = tc; tc = target; ability.setTargets(tc); stackDescription = ability.getStackDescription(); view.updateTargetCards(this); view.updateTargetPlayers(this); view.updateText(this); - - if (ability.hasParam("DividedAsYouChoose")) { - // try to update DividedAsYouChoose after retargeting - Object toRemove = null; - Object toAdd = null; - HashMap map = ability.getTargetRestrictions().getDividedMap(); - - if (oldTarget != null) { - toRemove = oldTarget; - } else { - // try to deduce which target has been replaced - // (this may be imprecise, updateTarget should specify old target if possible) - for (Object obj : map.keySet()) { - if (!target.contains(obj)) { - toRemove = obj; - break; - } - } - } - - if (newTarget != null) { - toAdd = newTarget; - } else { - // try to deduce which target was added - // (this may be imprecise, updateTarget should specify new target if possible) - for (Object newTgts : target) { - if (!map.containsKey(newTgts)) { - toAdd = newTgts; - break; - } - } - } - - if (toRemove != null && toAdd != null) { - int div = map.get(toRemove); - map.remove(toRemove); - ability.getTargetRestrictions().addDividedAllocation(toAdd, div); - } - } // Run BecomesTargetTrigger - final Map runParams = AbilityKey.newMap(); + Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.SourceSA, ability); - Set distinctObjects = new HashSet<>(); - for (final Object tgt : target) { - if (distinctObjects.contains(tgt)) { + Set distinctObjects = Sets.newHashSet(); + for (final GameObject tgt : target) { + if (oldTarget != null && oldTarget.contains(tgt)) { + // it was an old target, so don't trigger becomes target + continue; + } + if (!distinctObjects.add(tgt)) { continue; } - distinctObjects.add(tgt); if (tgt instanceof Card && !((Card) tgt).hasBecomeTargetThisTurn()) { runParams.put(AbilityKey.FirstTime, null); @@ -330,7 +280,8 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { runParams.put(AbilityKey.Target, tgt); getSourceCard().getGame().getTriggerHandler().runTrigger(TriggerType.BecomesTarget, runParams, false); } - runParams.put(AbilityKey.Targets, target); + runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Targets, distinctObjects); getSourceCard().getGame().getTriggerHandler().runTrigger(TriggerType.BecomesTargetOnce, runParams, false); } } diff --git a/forge-game/src/main/java/forge/game/spellability/TargetChoices.java b/forge-game/src/main/java/forge/game/spellability/TargetChoices.java index 79061ec1ddc..32aa3158a6c 100644 --- a/forge-game/src/main/java/forge/game/spellability/TargetChoices.java +++ b/forge-game/src/main/java/forge/game/spellability/TargetChoices.java @@ -6,12 +6,12 @@ * 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 . */ @@ -21,6 +21,7 @@ import com.google.common.base.Predicates; import com.google.common.collect.ForwardingList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.game.GameEntity; import forge.game.GameObject; @@ -30,13 +31,15 @@ import forge.game.card.CardCollectionView; import forge.game.player.Player; import forge.util.collect.FCollection; +import java.util.Collection; import java.util.List; +import java.util.Map; /** *

* Target_Choices class. *

- * + * * @author Forge * @version $Id$ */ @@ -44,6 +47,8 @@ public class TargetChoices extends ForwardingList implements Cloneab private final FCollection targets = new FCollection(); + private final Map dividedMap = Maps.newHashMap(); + public final int getTotalTargetedCMC() { int totalCMC = 0; for (Card c : Iterables.filter(targets, Card.class)) { @@ -52,6 +57,7 @@ public class TargetChoices extends ForwardingList implements Cloneab return totalCMC; } + @Override public final boolean add(final GameObject o) { if (o instanceof Player || o instanceof Card || o instanceof SpellAbility) { return super.add(o); @@ -59,6 +65,22 @@ public class TargetChoices extends ForwardingList implements Cloneab return false; } + @Override + public boolean removeAll(Collection collection) { + boolean result = super.removeAll(collection); + for (Object e : collection) { + this.dividedMap.remove(e); + } + return result; + } + + @Override + public boolean remove(Object object) { + boolean result = super.remove(object); + dividedMap.remove(object); + return result; + } + public final CardCollectionView getTargetCards() { return new CardCollection(Iterables.filter(targets, Card.class)); } @@ -103,10 +125,31 @@ public class TargetChoices extends ForwardingList implements Cloneab public TargetChoices clone() { TargetChoices tc = new TargetChoices(); tc.targets.addAll(targets); + tc.dividedMap.putAll(dividedMap); return tc; } @Override protected List delegate() { return targets; } + + public final void addDividedAllocation(final GameObject tgt, final Integer portionAllocated) { + this.dividedMap.put(tgt, portionAllocated); + } + public Integer getDividedValue(GameObject c) { + return dividedMap.get(c); + } + + public Collection getDividedValues() { + return dividedMap.values(); + } + + public int getTotalDividedValue() { + int result = 0; + for (Integer i : getDividedValues()) { + if (i != null) + result += i; + } + return result; + } } diff --git a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java index eef0d88b541..5eeca0fb5c3 100644 --- a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java +++ b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java @@ -18,11 +18,9 @@ package forge.game.spellability; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import forge.util.TextUtil; -import org.apache.commons.lang3.StringUtils; import com.google.common.collect.Lists; @@ -75,11 +73,6 @@ public class TargetRestrictions { // What's the max total CMC of targets? private String maxTotalCMC; - - // For "Divided" cards. Is this better in TargetChoices? - private boolean dividedAsYouChoose = false; - private HashMap dividedMap = new HashMap<>(); - private int stillToDivide = 0; // Not sure what's up with Mandatory? Why wouldn't targeting be mandatory? private boolean bMandatory = false; @@ -101,7 +94,6 @@ public class TargetRestrictions { this.maxTotalCMC = target.getMaxTotalCMC(); this.tgtZone = target.getZone(); this.saValidTargeting = target.getSAValidTargeting(); - this.dividedAsYouChoose = target.isDividedAsYouChoose(); this.uniqueTargets = target.isUniqueTargets(); this.singleZone = target.isSingleZone(); this.differentControllers = target.isDifferentControllers(); @@ -728,82 +720,9 @@ public class TargetRestrictions { this.singleTarget = singleTarget; } - /** - * @return a boolean dividedAsYouChoose - */ - public boolean isDividedAsYouChoose() { - return this.dividedAsYouChoose; - } - - /** - * @param divided the boolean to set - */ - public void setDividedAsYouChoose(boolean divided) { - this.dividedAsYouChoose = divided; - } - - /** - * Get the amount remaining to distribute. - * @return int stillToDivide - */ - public int getStillToDivide() { - return this.stillToDivide; - } - - /** - * @param remaining set the amount still to be divided - */ - public void setStillToDivide(final int remaining) { - this.stillToDivide = remaining; - } - - public void calculateStillToDivide(String toDistribute, Card source, SpellAbility sa) { - // Recalculate this value just in case it's variable - if (!this.dividedAsYouChoose) { - return; - } - - if (StringUtils.isNumeric(toDistribute)) { - this.setStillToDivide(Integer.parseInt(toDistribute)); - } else if ( source == null ) { - return; // such calls come from AbilityFactory.readTarget - at this moment we don't yet know X or any other variables - } else if (source.getSVar(toDistribute).equals("xPaid")) { - this.setStillToDivide(source.getXManaCostPaid()); - } else { - this.setStillToDivide(AbilityUtils.calculateAmount(source, toDistribute, sa)); - } - } - - /** - * Store divided amount relative to a specific card/player. - * @param tgt the targeted object - * @param portionAllocated the divided portion allocated - */ - public final void addDividedAllocation(final Object tgt, final Integer portionAllocated) { - this.dividedMap.remove(tgt); - this.dividedMap.put(tgt, portionAllocated); - } - - /** - * Get the divided amount relative to a specific card/player. - * @param tgt the targeted object - * @return an int. - */ - public int getDividedValue(Object tgt) { - return this.dividedMap.get(tgt); - } - - public HashMap getDividedMap() { - return this.dividedMap; - } - public final void applyTargetTextChanges(final SpellAbility sa) { for (int i = 0; i < validTgts.length; i++) { validTgts[i] = AbilityUtils.applyAbilityTextChangeEffects(originalValidTgts[i], sa); } } - - public final void changeValidTargets(final String[] validTgts) { - this.originalValidTgts = validTgts; - } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerBecomesTargetOnce.java b/forge-game/src/main/java/forge/game/trigger/TriggerBecomesTargetOnce.java index 821c1866fa6..2b071edb560 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerBecomesTargetOnce.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerBecomesTargetOnce.java @@ -23,7 +23,6 @@ import forge.game.card.Card; import forge.game.spellability.SpellAbility; import forge.util.Localizer; -import java.util.List; import java.util.Map; /** @@ -64,9 +63,8 @@ public class TriggerBecomesTargetOnce extends Trigger { } } if (hasParam("ValidTarget")) { - List targets = (List) runParams.get(AbilityKey.Targets); boolean valid = false; - for (GameObject b : targets) { + for (GameObject b : (Iterable) runParams.get(AbilityKey.Targets)) { if (matchesValid(b, getParam("ValidTarget").split(","), this.getHostCard())) { valid = true; break; diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 21dcc1c897b..5fd898c86c1 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -247,7 +247,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable filter, boolean optional) { throw new IllegalStateException("Erring on the side of caution here..."); } diff --git a/forge-gui/res/cardsfolder/d/detonate.txt b/forge-gui/res/cardsfolder/d/detonate.txt index 0401d47344a..811c8ad7f95 100644 --- a/forge-gui/res/cardsfolder/d/detonate.txt +++ b/forge-gui/res/cardsfolder/d/detonate.txt @@ -1,8 +1,7 @@ Name:Detonate ManaCost:X R Types:Sorcery -A:SP$ Destroy | Cost$ X R | ValidTgts$ Artifact | ValidTgtsWithoutManaCost$ Artifact.cmcEQ0 | TgtPrompt$ Select target artifact | NoRegen$ True | SubAbility$ DBDamage | References$ X | SpellDescription$ Destroy target artifact with converted mana cost X. It can't be regenerated. CARDNAME deals X damage to that artifact's controller. +A:SP$ Destroy | Cost$ X R | ValidTgts$ Artifact | TgtPrompt$ Select target artifact | NoRegen$ True | SubAbility$ DBDamage | References$ X | SpellDescription$ Destroy target artifact with converted mana cost X. It can't be regenerated. CARDNAME deals X damage to that artifact's controller. SVar:DBDamage:DB$DealDamage | Defined$ TargetedController | NumDmg$ X | References$ X -SVar:X:Targeted$CardManaCost -SVar:Picture:http://www.wizards.com/global/images/magic/general/detonate.jpg +SVar:X:Count$xPaid Oracle:Destroy target artifact with converted mana cost X. It can't be regenerated. Detonate deals X damage to that artifact's controller. diff --git a/forge-gui/src/main/java/forge/match/input/InputSelectTargets.java b/forge-gui/src/main/java/forge/match/input/InputSelectTargets.java index 83c3dc9ae3e..a8b1622baed 100644 --- a/forge-gui/src/main/java/forge/match/input/InputSelectTargets.java +++ b/forge-gui/src/main/java/forge/match/input/InputSelectTargets.java @@ -1,6 +1,9 @@ package forge.match.input; +import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + import forge.FThreads; import forge.game.GameEntity; import forge.game.GameObject; @@ -22,6 +25,7 @@ import forge.util.ITriggerEvent; import forge.util.TextUtil; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,21 +37,25 @@ public final class InputSelectTargets extends InputSyncronizedBase { private final Map targetDepth = new HashMap<>(); private final TargetRestrictions tgt; private final SpellAbility sa; + private final Collection divisionValues; private Card lastTarget = null; private boolean bCancel = false; private boolean bOk = false; private final boolean mandatory; + private Predicate filter; private static final long serialVersionUID = -1091595663541356356L; public final boolean hasCancelled() { return bCancel; } public final boolean hasPressedOk() { return bOk; } - public InputSelectTargets(final PlayerControllerHuman controller, final List choices, final SpellAbility sa, final boolean mandatory) { + public InputSelectTargets(final PlayerControllerHuman controller, final List choices, final SpellAbility sa, final boolean mandatory, Collection divisionValues, Predicate filter) { super(controller); this.choices = choices; this.tgt = sa.getTargetRestrictions(); this.sa = sa; this.mandatory = mandatory; + this.divisionValues = divisionValues; + this.filter = filter; controller.getGui().setSelectables(CardView.getCollection(choices)); final PlayerZoneUpdates zonesToUpdate = new PlayerZoneUpdates(); for (final Card c : choices) { @@ -97,7 +105,7 @@ public final class InputSelectTargets extends InputSyncronizedBase { sb.append(sa.getUniqueTargets()); } - final int maxTargets = tgt.getMaxTargets(sa.getHostCard(), sa); + final int maxTargets = sa.getMaxTargets(); final int targeted = sa.getTargets().size(); if(maxTargets > 1) { sb.append(TextUtil.concatNoSpace("\n(", String.valueOf(maxTargets - targeted), " more can be targeted)")); @@ -108,10 +116,10 @@ public final class InputSelectTargets extends InputSyncronizedBase { "(Targeting ERROR)", ""); showMessage(message, sa.getView()); - if (tgt.isDividedAsYouChoose() && tgt.getMinTargets(sa.getHostCard(), sa) == 0 && sa.getTargets().size() == 0) { + if (sa.isDividedAsYouChoose() && sa.getMinTargets() == 0 && sa.getTargets().size() == 0) { // extra logic for Divided with min targets = 0, should only work if num targets are 0 too getController().getGui().updateButtons(getOwner(), true, true, false); - } else if (!tgt.isMinTargetsChosen(sa.getHostCard(), sa) || tgt.isDividedAsYouChoose()) { + } else if (!sa.isMinTargetChosen() || sa.isDividedAsYouChoose()) { // If reached Minimum targets, enable OK button if (mandatory && tgt.hasCandidates(sa, true)) { // Player has to click on a target @@ -239,40 +247,11 @@ public final class InputSelectTargets extends InputSyncronizedBase { return false; } - if (tgt.isDividedAsYouChoose()) { - final int stillToDivide = tgt.getStillToDivide(); - int allocatedPortion = 0; - // allow allocation only if the max targets isn't reached and there are more candidates - if ((sa.getTargets().size() + 1 < tgt.getMaxTargets(sa.getHostCard(), sa)) - && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { - final ImmutableList.Builder choices = ImmutableList.builder(); - for (int i = 1; i <= stillToDivide; i++) { - choices.add(Integer.valueOf(i)); - } - String apiBasedMessage = "Distribute how much to "; - if (sa.getApi() == ApiType.DealDamage) { - apiBasedMessage = "Select how much damage to deal to "; - } - else if (sa.getApi() == ApiType.PreventDamage) { - apiBasedMessage = "Select how much damage to prevent to "; - } - else if (sa.getApi() == ApiType.PutCounter) { - apiBasedMessage = "Select how many counters to distribute to "; - } - final StringBuilder sb = new StringBuilder(); - sb.append(apiBasedMessage); - sb.append(card.toString()); - final Integer chosen = getController().getGui().oneOrNone(sb.toString(), choices.build()); - if (chosen == null) { - return true; //still return true since there was a valid choice - } - allocatedPortion = chosen; + if (sa.isDividedAsYouChoose()) { + Boolean val = onDividedAsYouChoose(card); + if (val != null) { + return val; } - else { // otherwise assign the rest of the damage/protection - allocatedPortion = stillToDivide; - } - tgt.setStillToDivide(stillToDivide - allocatedPortion); - tgt.addDividedAllocation(card, allocatedPortion); } addTarget(card); return true; @@ -305,12 +284,49 @@ public final class InputSelectTargets extends InputSyncronizedBase { showMessage(sa.getHostCard() + " - Cannot target this player (Hexproof? Protection? Restrictions?)."); return; } + if (filter != null && !filter.apply(player)) { + showMessage(sa.getHostCard() + " - Cannot target this player (Hexproof? Protection? Restrictions?)."); + return; + } - if (tgt.isDividedAsYouChoose()) { - final int stillToDivide = tgt.getStillToDivide(); + if (sa.isDividedAsYouChoose()) { + Boolean val = onDividedAsYouChoose(player); + if (val != null) { + return; + } + } + addTarget(player); + } + + protected Boolean onDividedAsYouChoose(GameObject go) { + if (divisionValues != null) { + if (divisionValues.isEmpty()) { + return false; + } + String apiBasedMessage = "Distribute how much to "; + if (sa.getApi() == ApiType.DealDamage) { + apiBasedMessage = "Select how much damage to deal to "; + } + else if (sa.getApi() == ApiType.PreventDamage) { + apiBasedMessage = "Select how much damage to prevent to "; + } + else if (sa.getApi() == ApiType.PutCounter) { + apiBasedMessage = "Select how many counters to distribute to "; + } + final StringBuilder sb = new StringBuilder(); + sb.append(apiBasedMessage); + sb.append(go.toString()); + final Integer chosen = getController().getGui().oneOrNone(sb.toString(), Lists.newArrayList(divisionValues)); + if (chosen == null) { + return true; //still return true since there was a valid choice + } + divisionValues.remove(chosen); + sa.addDividedAllocation(go, chosen); + } else { + final int stillToDivide = sa.getStillToDivide(); int allocatedPortion = 0; // allow allocation only if the max targets isn't reached and there are more candidates - if ((sa.getTargets().size() + 1 < tgt.getMaxTargets(sa.getHostCard(), sa)) && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { + if ((sa.getTargets().size() + 1 < sa.getMaxTargets()) && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { final ImmutableList.Builder choices = ImmutableList.builder(); for (int i = 1; i <= stillToDivide; i++) { choices.add(Integer.valueOf(i)); @@ -323,19 +339,18 @@ public final class InputSelectTargets extends InputSyncronizedBase { } final StringBuilder sb = new StringBuilder(); sb.append(apiBasedMessage); - sb.append(player.getName()); + sb.append(go.toString()); final Integer chosen = getController().getGui().oneOrNone(sb.toString(), choices.build()); if (null == chosen) { - return; + return true; } allocatedPortion = chosen; } else { // otherwise assign the rest of the damage/protection allocatedPortion = stillToDivide; } - tgt.setStillToDivide(stillToDivide - allocatedPortion); - tgt.addDividedAllocation(player, allocatedPortion); + sa.addDividedAllocation(go, allocatedPortion); } - addTarget(player); + return null; } private void addTarget(final GameEntity ge) { @@ -366,7 +381,7 @@ public final class InputSelectTargets extends InputSyncronizedBase { } private boolean hasAllTargets() { - return tgt.isMaxTargetsChosen(sa.getHostCard(), sa) || ( tgt.getStillToDivide() == 0 && tgt.isDividedAsYouChoose()); + return sa.isMaxTargetChosen() || (sa.isDividedAsYouChoose() && sa.getStillToDivide() == 0); } @Override diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 8ccb11d36b5..addd56ebe0c 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -1061,19 +1061,20 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont * SpellAbility, forge.card.spellability.SpellAbilityStackInstance) */ @Override - public TargetChoices chooseNewTargetsFor(final SpellAbility ability) { + public TargetChoices chooseNewTargetsFor(final SpellAbility ability, Predicate filter, boolean optional) { final SpellAbility sa = ability.isWrapper() ? ((WrappedAbility) ability).getWrappedAbility() : ability; - if (sa.getTargetRestrictions() == null) { + if (!sa.usesTargeting()) { return null; } final TargetChoices oldTarget = sa.getTargets(); final TargetSelection select = new TargetSelection(this, sa); sa.resetTargets(); - if (select.chooseTargets(oldTarget.size())) { + if (select.chooseTargets(oldTarget.size(), Lists.newArrayList(oldTarget.getDividedValues()), filter, optional)) { return sa.getTargets(); } else { + sa.setTargets(oldTarget); // Return old target, since we had to reset them above - return oldTarget; + return null; } } @@ -1795,11 +1796,8 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont player.getGame().getStackZone().add(next.getHostCard()); } // TODO check if static abilities needs to be run for things affecting the copy? - if (next.isMayChooseNewTargets() && !next.setupTargets()) { - // if targets can't be done, remove copy from existence - if (next.isSpell()) { - next.getHostCard().ceaseToExist(); - } + if (next.isMayChooseNewTargets()) { + next.setupNewTargets(player); } } player.getGame().getStack().add(next); @@ -1820,7 +1818,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont @Override public boolean chooseTargetsFor(final SpellAbility currentAbility) { final TargetSelection select = new TargetSelection(this, currentAbility); - return select.chooseTargets(null); + return select.chooseTargets(null, null, null, false); } @Override diff --git a/forge-gui/src/main/java/forge/player/TargetSelection.java b/forge-gui/src/main/java/forge/player/TargetSelection.java index f62b1a41e29..24cf167ed87 100644 --- a/forge-gui/src/main/java/forge/player/TargetSelection.java +++ b/forge-gui/src/main/java/forge/player/TargetSelection.java @@ -17,6 +17,8 @@ */ package forge.player; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.game.Game; @@ -25,6 +27,7 @@ import forge.game.GameEntityView; import forge.game.GameEntityViewMap; import forge.game.GameObject; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardUtil; import forge.game.card.CardView; import forge.game.player.PlayerView; @@ -38,6 +41,7 @@ import forge.match.input.InputSelectTargets; import forge.util.Aggregates; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -70,15 +74,15 @@ public class TargetSelection { return ability.isTrigger() || getTgt().getMandatory(); } - public final boolean chooseTargets(Integer numTargets) { + public final boolean chooseTargets(Integer numTargets, Collection divisionValues, Predicate filter, boolean optional) { if (!ability.usesTargeting()) { throw new RuntimeException("TargetSelection.chooseTargets called for ability that does not target - " + ability); } final TargetRestrictions tgt = getTgt(); // Number of targets is explicitly set only if spell is being redirected (ex. Swerve or Redirect) - final int minTargets = numTargets != null ? numTargets.intValue() : tgt.getMinTargets(ability.getHostCard(), ability); - final int maxTargets = numTargets != null ? numTargets.intValue() : tgt.getMaxTargets(ability.getHostCard(), ability); + final int minTargets = numTargets != null ? numTargets.intValue() : ability.getMinTargets(); + final int maxTargets = numTargets != null ? numTargets.intValue() : ability.getMaxTargets(); //final int maxTotalCMC = tgt.getMaxTotalCMC(ability.getHostCard(), ability); final int numTargeted = ability.getTargets().size(); final boolean isSingleZone = ability.getTargetRestrictions().isSingleZone(); @@ -92,7 +96,7 @@ public class TargetSelection { return false; } - if (this.bTargetingDone && hasEnoughTargets || hasAllTargets || tgt.isDividedAsYouChoose() && tgt.getStillToDivide() == 0) { + if (this.bTargetingDone && hasEnoughTargets || hasAllTargets || ability.isDividedAsYouChoose() && divisionValues == null && ability.getStillToDivide() == 0) { return true; } @@ -107,11 +111,10 @@ public class TargetSelection { } final List zones = tgt.getZone(); - final boolean mandatory = isMandatory() && hasCandidates; + final boolean mandatory = isMandatory() && hasCandidates && !optional; final boolean choiceResult; - final boolean random = tgt.isRandomTarget(); - if (random) { + if (tgt.isRandomTarget() && numTargets == null) { final List candidates = tgt.getAllCandidates(this.ability, true); final GameObject choice = Aggregates.random(candidates); return ability.getTargets().add(choice); @@ -122,7 +125,11 @@ public class TargetSelection { return this.chooseCardFromStack(mandatory); } else { - final List validTargets = CardUtil.getValidCardsToTarget(tgt, ability); + List validTargets = CardUtil.getValidCardsToTarget(tgt, ability); + if (filter != null) { + validTargets = new CardCollection(Iterables.filter(validTargets, filter)); + } + // single zone if (isSingleZone) { final List removeCandidates = new ArrayList<>(); @@ -149,8 +156,8 @@ public class TargetSelection { //if only one valid target card for triggered ability, auto-target that card //only do this for triggered abilities to prevent auto-targeting when user chooses //to play a spell or activat an ability - if (tgt.isDividedAsYouChoose()) { - tgt.addDividedAllocation(validTargets.get(0), tgt.getStillToDivide()); + if (ability.isDividedAsYouChoose()) { + ability.addDividedAllocation(validTargets.get(0), ability.getStillToDivide()); } return ability.getTargets().add(validTargets.get(0)); } @@ -162,7 +169,7 @@ public class TargetSelection { PlayerView playerView = controller.getLocalPlayerView(); PlayerZoneUpdates playerZoneUpdates = controller.getGui().openZones(playerView, zones, playersWithValidTargets); if (!zones.contains(ZoneType.Stack)) { - InputSelectTargets inp = new InputSelectTargets(controller, validTargets, ability, mandatory); + InputSelectTargets inp = new InputSelectTargets(controller, validTargets, ability, mandatory, divisionValues, filter); inp.showAndWait(); choiceResult = !inp.hasCancelled(); bTargetingDone = inp.hasPressedOk(); @@ -174,7 +181,7 @@ public class TargetSelection { } } // some inputs choose cards one-by-one and need to be called again - return choiceResult && chooseTargets(numTargets); + return choiceResult && chooseTargets(numTargets, divisionValues, filter, optional); } private final boolean chooseCardFromList(final List choices, final boolean targeted, final boolean mandatory) { @@ -232,7 +239,7 @@ public class TargetSelection { } final String msgDone = "[FINISH TARGETING]"; - if (this.getTgt().isMinTargetsChosen(this.ability.getHostCard(), this.ability)) { + if (ability.isMinTargetChosen()) { // is there a more elegant way of doing this? choicesFiltered.add(msgDone); } @@ -282,12 +289,12 @@ public class TargetSelection { } while(!bTargetingDone) { - if (tgt.isMaxTargetsChosen(this.ability.getHostCard(), this.ability)) { + if (ability.isMaxTargetChosen()) { bTargetingDone = true; return true; } - if (!selectOptions.contains("[FINISH TARGETING]") && tgt.isMinTargetsChosen(this.ability.getHostCard(), this.ability)) { + if (!selectOptions.contains("[FINISH TARGETING]") && ability.isMinTargetChosen()) { selectOptions.add("[FINISH TARGETING]"); }