diff --git a/.gitattributes b/.gitattributes index 4addcf72a32..f730ec23d61 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10366,6 +10366,7 @@ res/cardsfolder/s/spellgorger_barbarian.txt svneol=native#text/plain res/cardsfolder/s/spelljack.txt -text res/cardsfolder/s/spellshift.txt -text res/cardsfolder/s/spellshock.txt svneol=native#text/plain +res/cardsfolder/s/spellskite.txt -text res/cardsfolder/s/spellstutter_sprite.txt svneol=native#text/plain res/cardsfolder/s/spelltithe_enforcer.txt -text res/cardsfolder/s/spelltwine.txt -text diff --git a/res/cardsfolder/s/spellskite.txt b/res/cardsfolder/s/spellskite.txt new file mode 100644 index 00000000000..00c04465223 --- /dev/null +++ b/res/cardsfolder/s/spellskite.txt @@ -0,0 +1,7 @@ +Name:Spellskite +ManaCost:2 +Types:Artifact Creature Horror +PT:0/4 +A:AB$ ChangeTargets | Cost$ PU | TargetType$ Spell | ValidTgts$ Card | SpellSkite$ True | SpellDescription$ Change a target of target spell or ability to Spellskite. +SVar:Picture:http://www.wizards.com/global/images/magic/general/spellskite.jpg +Oracle:{UP}: Change a target of target spell or ability to Spellskite. ({UP} can be paid with either {U} or 2 life.) \ No newline at end of file diff --git a/src/main/java/forge/card/ability/effects/ChangeTargetsEffect.java b/src/main/java/forge/card/ability/effects/ChangeTargetsEffect.java index 588c2751a5c..cd5ac0b61a6 100644 --- a/src/main/java/forge/card/ability/effects/ChangeTargetsEffect.java +++ b/src/main/java/forge/card/ability/effects/ChangeTargetsEffect.java @@ -1,11 +1,18 @@ package forge.card.ability.effects; +import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; + +import forge.Card; +import forge.ITargetable; import forge.card.ability.SpellAbilityEffect; import forge.card.spellability.SpellAbility; import forge.card.spellability.SpellAbilityStackInstance; import forge.card.spellability.TargetChoices; +import forge.game.player.Player; import forge.game.zone.MagicStack; /** @@ -30,14 +37,52 @@ public class ChangeTargetsEffect extends SpellAbilityEffect { continue; } + boolean preserveNumber = sa.hasParam("PreserveNumber"); // Redirect is not supposed to change number of targets + boolean isSpellskite = sa.hasParam("SpellSkite"); // The only card known to replace targets with self is Spellskite + SpellAbilityStackInstance changingTgtSI = si; - while(changingTgtSI != null) { - // Update targets, with a potential new target - SpellAbility changingTgtSA = changingTgtSI.getSpellAbility(); - TargetChoices newTarget = sa.getActivatingPlayer().getController().chooseTargets(changingTgtSA); - changingTgtSI.updateTarget(newTarget); - changingTgtSI = changingTgtSI.getSubInstace(); - } + Player chooser = sa.getActivatingPlayer(); + + if( isSpellskite ) { + Card srcCard = sa.getSourceCard(); + // 1. choose a target of target spell + List> allTargets = new ArrayList<>(); + while(changingTgtSI != null) { + SpellAbility changedSa = changingTgtSI.getSpellAbility(); + if(changedSa.usesTargeting()) { + for(ITargetable it : changedSa.getTargets().getTargets()) + allTargets.add(ImmutablePair.of(changingTgtSI, it)); + } + changingTgtSI = changingTgtSI.getSubInstace(); + } + if( allTargets.isEmpty() ) { + System.err.println("Player managed to target a spell without targets with Spellskite's ability."); + return; + } + + Pair chosenTarget = chooser.getController().chooseTarget(sa, allTargets); + // 2. replace with spellskite + SpellAbilityStackInstance replaceIn = chosenTarget.getKey(); + ITargetable oldTarget = chosenTarget.getValue(); + TargetChoices oldTargetBlock = replaceIn.getTargetChoices(); + TargetChoices newTargetBlock = oldTargetBlock.clone(); + newTargetBlock.remove(oldTarget); + replaceIn.updateTarget(newTargetBlock); + // 3. test if replacing is correct. + if(replaceIn.getSpellAbility().canTarget(srcCard)) { + newTargetBlock.add(srcCard); + replaceIn.updateTarget(newTargetBlock); + } else + replaceIn.updateTarget(oldTargetBlock); + } else + while(changingTgtSI != null) { + // Update targets, with a potential new target + SpellAbility changingTgtSA = changingTgtSI.getSpellAbility(); + TargetChoices newTarget = sa.getActivatingPlayer().getController().chooseNewTargetsFor(changingTgtSA); + if ( null != newTarget) + changingTgtSI.updateTarget(newTarget); + changingTgtSI = changingTgtSI.getSubInstace(); + } if (remember) { sa.getSourceCard().addRemembered(tgtSA.getSourceCard()); diff --git a/src/main/java/forge/card/spellability/TargetChoices.java b/src/main/java/forge/card/spellability/TargetChoices.java index 41dce15f6d0..e00fa8f4c14 100644 --- a/src/main/java/forge/card/spellability/TargetChoices.java +++ b/src/main/java/forge/card/spellability/TargetChoices.java @@ -34,7 +34,7 @@ import forge.game.player.Player; * @author Forge * @version $Id$ */ -public class TargetChoices { +public class TargetChoices implements Cloneable { private int numTargeted = 0; // Card or Player are legal targets. @@ -140,9 +140,7 @@ public class TargetChoices { public final boolean remove(final ITargetable target) { // remove returns true if element was found in given list if (this.targetCards.remove(target) || this.targetPlayers.remove(target) || this.targetSpells.remove(target)) { - - // Do I decrement numTargeted for fizzling targets? - //this.numTargeted--; + this.numTargeted--; return true; } return false; @@ -255,4 +253,13 @@ public class TargetChoices { public final boolean isEmpty() { return targetCards.isEmpty() && targetSpells.isEmpty() && targetPlayers.isEmpty(); } + + @Override + public TargetChoices clone() { + TargetChoices tc = new TargetChoices(); + tc.targetCards.addAll(this.targetCards); + tc.targetPlayers.addAll(this.targetPlayers); + tc.targetSpells.addAll(this.targetSpells); + return tc; + } } diff --git a/src/main/java/forge/card/spellability/TargetSelection.java b/src/main/java/forge/card/spellability/TargetSelection.java index 11bfd8d396e..7f875a075a9 100644 --- a/src/main/java/forge/card/spellability/TargetSelection.java +++ b/src/main/java/forge/card/spellability/TargetSelection.java @@ -107,7 +107,7 @@ public class TargetSelection { return this.chooseCardFromStack(mandatory); } else { List validTargets = this.getValidCardsToTarget(); - if (zone.size() == 1 && zone.get(0) == ZoneType.Battlefield) { + if (zone.size() == 1 && (zone.get(0) == ZoneType.Battlefield || zone.get(0) == ZoneType.Hand)) { InputSelectTargets inp = new InputSelectTargets(validTargets, ability, mandatory); Singletons.getControl().getInputQueue().setInputAndWait(inp); choiceResult = !inp.hasCancelled(); diff --git a/src/main/java/forge/game/ai/AiAttackController.java b/src/main/java/forge/game/ai/AiAttackController.java index b1ef12da3d4..756a4c7d4c5 100644 --- a/src/main/java/forge/game/ai/AiAttackController.java +++ b/src/main/java/forge/game/ai/AiAttackController.java @@ -491,7 +491,7 @@ public class AiAttackController { // I know this is a little confusing Game game = ai.getGame(); - random.setSeed(game.getPhaseHandler().getTurn() + this.randomInt); + random.setSeed(game.getPhaseHandler().getTurn() + AiAttackController.randomInt); final Combat combat = new Combat(); combat.setAttackingPlayer(game.getCombat().getAttackingPlayer()); diff --git a/src/main/java/forge/game/player/PlayerController.java b/src/main/java/forge/game/player/PlayerController.java index 56fa4c7a62f..b5487c25dc9 100644 --- a/src/main/java/forge/game/player/PlayerController.java +++ b/src/main/java/forge/game/player/PlayerController.java @@ -6,13 +6,16 @@ import java.util.List; import java.util.Map; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; import forge.Card; import forge.GameEntity; +import forge.ITargetable; import forge.card.cost.Cost; import forge.card.mana.Mana; import forge.card.replacement.ReplacementEffect; import forge.card.spellability.SpellAbility; +import forge.card.spellability.SpellAbilityStackInstance; import forge.card.spellability.TargetChoices; import forge.deck.Deck; import forge.game.Game; @@ -101,7 +104,10 @@ public abstract class PlayerController { public abstract Integer announceRequirements(SpellAbility ability, String announce, boolean allowZero); public abstract List choosePermanentsToSacrifice(SpellAbility sa, int min, int max, List validTargets, String message); public abstract List choosePermanentsToDestroy(SpellAbility sa, int min, int max, List validTargets, String message); - public abstract TargetChoices chooseTargets(SpellAbility ability); + public abstract TargetChoices chooseNewTargetsFor(SpellAbility ability); + + // Specify a target of a spell (Spellskite) + public abstract Pair chooseTarget(SpellAbility sa, List> allTargets); public Card chooseSingleCardForEffect(List sourceList, SpellAbility sa, String title) { return chooseSingleCardForEffect(sourceList, sa, title, false); } public abstract Card chooseSingleCardForEffect(List sourceList, SpellAbility sa, String title, boolean isOptional); diff --git a/src/main/java/forge/game/player/PlayerControllerAi.java b/src/main/java/forge/game/player/PlayerControllerAi.java index 494fd5678af..0e0d561df19 100644 --- a/src/main/java/forge/game/player/PlayerControllerAi.java +++ b/src/main/java/forge/game/player/PlayerControllerAi.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; import com.google.common.collect.Lists; @@ -15,6 +16,7 @@ import forge.Card; import forge.CardLists; import forge.CardPredicates; import forge.GameEntity; +import forge.ITargetable; import forge.card.ability.ApiType; import forge.card.cost.Cost; import forge.card.mana.Mana; @@ -23,6 +25,7 @@ import forge.card.spellability.Ability; import forge.card.spellability.AbilityStatic; import forge.card.spellability.Spell; import forge.card.spellability.SpellAbility; +import forge.card.spellability.SpellAbilityStackInstance; import forge.card.spellability.TargetChoices; import forge.deck.Deck; import forge.game.Game; @@ -255,7 +258,7 @@ public class PlayerControllerAi extends PlayerController { * @see forge.game.player.PlayerController#chooseTargets(forge.card.spellability.SpellAbility, forge.card.spellability.SpellAbilityStackInstance) */ @Override - public TargetChoices chooseTargets(SpellAbility ability) { + public TargetChoices chooseNewTargetsFor(SpellAbility ability) { // AI currently can't do this. But when it can it will need to be based on Ability API return null; } @@ -378,4 +381,10 @@ public class PlayerControllerAi extends PlayerController { return results[i]; } } + + @Override + public Pair chooseTarget(SpellAbility saSrc, List> allTargets) { + // TODO Teach AI how to use Spellskite + return allTargets.get(0); + } } diff --git a/src/main/java/forge/game/player/PlayerControllerHuman.java b/src/main/java/forge/game/player/PlayerControllerHuman.java index 8c5a1bd43e4..1550e1c8341 100644 --- a/src/main/java/forge/game/player/PlayerControllerHuman.java +++ b/src/main/java/forge/game/player/PlayerControllerHuman.java @@ -11,16 +11,21 @@ import javax.swing.JOptionPane; import org.apache.commons.lang.math.IntRange; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import com.google.common.base.Function; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import forge.Card; import forge.GameEntity; +import forge.ITargetable; import forge.Singletons; import forge.card.cost.Cost; import forge.card.mana.Mana; import forge.card.replacement.ReplacementEffect; import forge.card.spellability.SpellAbility; +import forge.card.spellability.SpellAbilityStackInstance; import forge.card.spellability.TargetSelection; import forge.card.spellability.TargetChoices; import forge.deck.CardPool; @@ -440,7 +445,7 @@ public class PlayerControllerHuman extends PlayerController { * @see forge.game.player.PlayerController#chooseTargets(forge.card.spellability.SpellAbility, forge.card.spellability.SpellAbilityStackInstance) */ @Override - public TargetChoices chooseTargets(SpellAbility ability) { + public TargetChoices chooseNewTargetsFor(SpellAbility ability) { if (ability.getTargetRestrictions() == null) { return null; } @@ -623,4 +628,21 @@ public class PlayerControllerHuman extends PlayerController { public String chooseFilpResult(Card source, Player flipper, String[] results, boolean call) { return GuiChoose.one(source.getName() + " - Choose a result", results); } + + + @Override + public Pair chooseTarget(SpellAbility saSpellskite, List> allTargets) { + if( allTargets.size() < 2) + return Iterables.getFirst(allTargets, null); + + final Function, String> fnToString = new Function, String>() { + @Override + public String apply(Pair targ) { + return targ.getRight().toString() + " - " + targ.getLeft().getStackDescription(); + } + }; + + List> chosen = GuiChoose.getChoices(saSpellskite.getSourceCard().getName(), 1, 1, allTargets, null, fnToString); + return Iterables.getFirst(chosen, null); + } } diff --git a/src/main/java/forge/gui/GuiChoose.java b/src/main/java/forge/gui/GuiChoose.java index 8acb09edef0..eb12ab41e58 100644 --- a/src/main/java/forge/gui/GuiChoose.java +++ b/src/main/java/forge/gui/GuiChoose.java @@ -17,6 +17,8 @@ import javax.swing.WindowConstants; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import com.google.common.base.Function; + import forge.Card; import forge.FThreads; import forge.Singletons; @@ -89,19 +91,19 @@ public class GuiChoose { } public static List noneOrMany(final String message, final Collection choices) { - return GuiChoose.getChoices(message, 0, choices.size(), choices, null); + return GuiChoose.getChoices(message, 0, choices.size(), choices, null, null); } // returned Object will never be null public static List getChoices(final String message, final int min, final int max, final T[] choices) { - return getChoices(message, min, max, Arrays.asList(choices), null); + return getChoices(message, min, max, Arrays.asList(choices), null, null); } public static List getChoices(final String message, final int min, final int max, final Collection choices) { - return getChoices(message, min, max, choices, null); + return getChoices(message, min, max, choices, null, null); } - public static List getChoices(final String message, final int min, final int max, final Collection choices,final T selected) { + public static List getChoices(final String message, final int min, final int max, final Collection choices, final T selected, final Function display) { if (null == choices || choices.isEmpty()) { if (0 == min) { return new ArrayList(); @@ -113,7 +115,7 @@ public class GuiChoose { Callable> showChoice = new Callable>() { @Override public List call() { - ListChooser c = new ListChooser(message, min, max, choices); + ListChooser c = new ListChooser(message, min, max, choices, display); final JList list = c.getJList(); list.addListSelectionListener(new ListSelectionListener() { @Override diff --git a/src/main/java/forge/gui/ListChooser.java b/src/main/java/forge/gui/ListChooser.java index 7143bfeb187..84527feb662 100644 --- a/src/main/java/forge/gui/ListChooser.java +++ b/src/main/java/forge/gui/ListChooser.java @@ -18,6 +18,7 @@ package forge.gui; +import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -29,16 +30,19 @@ import java.util.List; import javax.swing.AbstractAction; import javax.swing.AbstractListModel; import javax.swing.Action; +import javax.swing.DefaultListCellRenderer; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JScrollPane; +import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.WindowConstants; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import com.google.common.base.Function; import com.google.common.collect.Lists; import forge.FThreads; @@ -84,7 +88,7 @@ public class ListChooser { private JOptionPane optionPane; private Action ok, cancel; - public ListChooser(final String title, final int minChoices, final int maxChoices, final Collection list) { + public ListChooser(final String title, final int minChoices, final int maxChoices, final Collection list, final Function display) { FThreads.assertExecutedByEdt(true); this.title = title; this.minChoices = minChoices; @@ -104,6 +108,9 @@ public class ListChooser { if (maxChoices == 1) { this.jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); } + + if( null != display ) + this.jList.setCellRenderer(new TransformedCellRenderer(display)); this.optionPane = new JOptionPane(new JScrollPane(this.jList), JOptionPane.QUESTION_MESSAGE, JOptionPane.DEFAULT_OPTION, null, options, options[0]); @@ -296,4 +303,30 @@ public class ListChooser { } } } + + private class TransformedCellRenderer implements ListCellRenderer { + public final Function transformer; + public final DefaultListCellRenderer defRenderer; + + /** + * TODO: Write javadoc for Constructor. + */ + public TransformedCellRenderer(final Function t1) { + transformer = t1; + defRenderer = new DefaultListCellRenderer(); + } + + /* (non-Javadoc) + * @see javax.swing.ListCellRenderer#getListCellRendererComponent(javax.swing.JList, java.lang.Object, int, boolean, boolean) + */ + @Override + public Component getListCellRendererComponent(JList list, T value, int index, boolean isSelected, + boolean cellHasFocus) { + // TODO Auto-generated method stub + return defRenderer.getListCellRendererComponent(list, transformer.apply(value), index, isSelected, cellHasFocus); + } + + + + } } diff --git a/src/main/java/forge/gui/match/QuestWinLose.java b/src/main/java/forge/gui/match/QuestWinLose.java index ac8a19b89be..d830550c7e7 100644 --- a/src/main/java/forge/gui/match/QuestWinLose.java +++ b/src/main/java/forge/gui/match/QuestWinLose.java @@ -550,7 +550,7 @@ public class QuestWinLose extends ControlWinLose { Collections.sort(formats); - final GameFormat selected = GuiChoose.getChoices("Choose bonus booster format", 1, 1, formats, pref).get(0); //ch.getSelectedValue(); + final GameFormat selected = GuiChoose.getChoices("Choose bonus booster format", 1, 1, formats, pref, null).get(0); //ch.getSelectedValue(); Singletons.getModel().getQuestPreferences().setPref(QPref.BOOSTER_FORMAT, selected.toString()); cardsWon = qData.getCards().generateQuestBooster(selected.getFilterPrinted()); diff --git a/src/test/java/forge/gui/ListChooserTest.java b/src/test/java/forge/gui/ListChooserTest.java index 2ceb758250e..da5703b8c6a 100644 --- a/src/test/java/forge/gui/ListChooserTest.java +++ b/src/test/java/forge/gui/ListChooserTest.java @@ -15,7 +15,7 @@ public class ListChooserTest { */ @Test(groups = { "UnitTest", "fast" }, timeOut = 1000, enabled = false) public void listChooserTest1() { - final ListChooser c = new ListChooser("choose a or b", 0, 2, Arrays.asList("a", "b")); + final ListChooser c = new ListChooser("choose a or b", 0, 2, Arrays.asList("a", "b"), null); System.out.println(c.show()); for (final String s : c.getSelectedValues()) { System.out.println(s);