- Use the original implementation for orderAndPlaySimultaneousSa for the AI for the time being until the AI side of the implementation can be improved.

This commit is contained in:
Hans Mackowiak
2021-02-10 04:00:37 +00:00
committed by Michael Kamensky
parent 06e0ed9a79
commit 9d0433f812
27 changed files with 347 additions and 380 deletions

View File

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

View File

@@ -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<Pair<SpellAbilityStackInstance, GameObject>> 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<GameEntity> 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<GameObject> 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);
}
}

View File

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

View File

@@ -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<Card> 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<Card> 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;
}

View File

@@ -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<String, Object> 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;
}
}

View File

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

View File

@@ -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))) {

View File

@@ -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"))) {

View File

@@ -117,10 +117,9 @@ public class CardFactory {
* which wouldn't ordinarily get set during a simple Card.copy() call.
* </p>
* */
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) {

View File

@@ -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<GameObject> 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)

View File

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

View File

@@ -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<String, CardCollection> paidHash;
private final Map<String, CardCollection> paidHash;
// Additional info
// is Kicked, is Buyback
@@ -96,7 +95,6 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView {
private final Map<String, String> storedSVars = Maps.newHashMap();
private final List<ZoneType> zonesToOpen;
private final Map<Player, Object> 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<ZoneType> getZonesToOpen() {
return zonesToOpen;
}
public final Map<Player, Object> 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<Object,Integer> 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<AbilityKey, Object> runParams = AbilityKey.newMap();
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.SourceSA, ability);
Set<Object> distinctObjects = new HashSet<>();
for (final Object tgt : target) {
if (distinctObjects.contains(tgt)) {
Set<GameObject> 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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
@@ -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;
/**
* <p>
* Target_Choices class.
* </p>
*
*
* @author Forge
* @version $Id$
*/
@@ -44,6 +47,8 @@ public class TargetChoices extends ForwardingList<GameObject> implements Cloneab
private final FCollection<GameObject> targets = new FCollection<GameObject>();
private final Map<GameObject, Integer> 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<GameObject> 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<GameObject> 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<GameObject> implements Cloneab
public TargetChoices clone() {
TargetChoices tc = new TargetChoices();
tc.targets.addAll(targets);
tc.dividedMap.putAll(dividedMap);
return tc;
}
@Override
protected List<GameObject> 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<Integer> getDividedValues() {
return dividedMap.values();
}
public int getTotalDividedValue() {
int result = 0;
for (Integer i : getDividedValues()) {
if (i != null)
result += i;
}
return result;
}
}

View File

@@ -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<Object, Integer> 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<Object, Integer> 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;
}
}

View File

@@ -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<GameObject> targets = (List<GameObject>) runParams.get(AbilityKey.Targets);
boolean valid = false;
for (GameObject b : targets) {
for (GameObject b : (Iterable<GameObject>) runParams.get(AbilityKey.Targets)) {
if (matchesValid(b, getParam("ValidTarget").split(","), this.getHostCard())) {
valid = true;
break;

View File

@@ -247,7 +247,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
}
}
if (!hasLegalTargeting(sp, source)) {
if (!sp.isCopied() && !hasLegalTargeting(sp, source)) {
String str = source + " - [Couldn't add to stack, failed to target] - " + sp.getDescription();
System.err.println(str + sp.getAllTargetChoices());
game.getGameLog().add(GameLogEntryType.STACK_ADD, str);