diff --git a/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java index 14ce438c8ea..ebdb3e96d0a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java @@ -6,10 +6,12 @@ import java.util.Map; import com.google.common.collect.Lists; import forge.game.Game; +import forge.game.GameEntity; import forge.game.ability.AbilityKey; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardZoneTable; +import forge.game.event.GameEventCombatChanged; import forge.game.spellability.SpellAbility; public class PermanentEffect extends SpellAbilityEffect { @@ -28,24 +30,37 @@ public class PermanentEffect extends SpellAbilityEffect { final Map moveParams = AbilityKey.newMap(); final CardZoneTable table = AbilityKey.addCardZoneTableParams(moveParams, sa); + if ((sa.isIntrinsic() || host.wasCast()) && sa.isSneak()) { + host.setTapped(true); + } + final Card c = game.getAction().moveToPlay(host, sa, moveParams); sa.setHostCard(c); // CR 608.3g - if (sa.isIntrinsic() || c.wasCast()) { - if (sa.isDash() && c.isInPlay()) { + if ((sa.isIntrinsic() || c.wasCast()) && c.isInPlay()) { + if (sa.isDash()) { registerDelayedTrigger(sa, "Hand", Lists.newArrayList(c)); // add AI hint c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "Dash"), c.getGame().getNextTimestamp(), 0); } - if (sa.isBlitz() && c.isInPlay()) { + if (sa.isBlitz()) { registerDelayedTrigger(sa, "Sacrifice", Lists.newArrayList(c)); c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "Blitz"), c.getGame().getNextTimestamp(), 0); } - if (sa.isWarp() && c.isInPlay()) { + if (sa.isWarp()) { registerDelayedTrigger(sa, "Exile", Lists.newArrayList(c)); c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "Warp"), c.getGame().getNextTimestamp(), 0); } + if (sa.isSneak() && c.isCreature()) { + final Card returned = sa.getPaidList("Returned", true).getFirst(); + final GameEntity defender = game.getCombat().getDefenderByAttacker(returned); + game.getCombat().addAttacker(c, defender); + game.getCombat().getBandOfAttacker(c).setBlocked(false); + + game.updateCombatForView(); + game.fireEvent(new GameEventCombatChanged()); + } } table.triggerChangesZoneAll(game, sa); diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index b9cc50ede98..7d67205718f 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -3288,19 +3288,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } else if (keyword.startsWith("Starting intensity")) { sbAfter.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n"); } else if (keyword.startsWith("Escalate") || keyword.startsWith("Buyback") - || keyword.startsWith("Freerunning") || keyword.startsWith("Prowl")) { - final String[] k = keyword.split(":"); - final String manacost = k[1]; - final Cost cost = new Cost(manacost, false); - - StringBuilder sbCost = new StringBuilder(k[0]); - if (!cost.isOnlyManaCost()) { - sbCost.append("—"); - } else { - sbCost.append(" "); - } - sbCost.append(cost.toSimpleString()); - sbBefore.append(sbCost).append(" (").append(inst.getReminderText()).append(")"); + || keyword.startsWith("Freerunning") || keyword.startsWith("Prowl") + || keyword.startsWith("Sneak") || keyword.startsWith("Cleave")) { + sbBefore.append(formatKeywordWithCost(inst)); sbBefore.append("\r\n"); } else if (keyword.startsWith("Multikicker")) { final String[] n = keyword.split(":"); @@ -3329,22 +3319,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr || keyword.startsWith("Escape") || keyword.startsWith("Foretell:") || keyword.startsWith("Disturb") || keyword.startsWith("Overload") || keyword.startsWith("Plot") || keyword.startsWith("Mayhem")) { - final String[] k = keyword.split(":"); - final Cost mCost; - if (k.length < 2 || "ManaCost".equals(k[1])) { - mCost = new Cost(getManaCost(), false); - } else { - mCost = new Cost(k[1], false); - } - - StringBuilder sbCost = new StringBuilder(k[0]); - if (!mCost.isOnlyManaCost()) { - sbCost.append("—"); - } else { - sbCost.append(" "); - } - sbCost.append(mCost.toSimpleString()); - sbAfter.append(sbCost).append(" (").append(inst.getReminderText()).append(")"); + sbAfter.append(formatKeywordWithCost(inst)); sbAfter.append("\r\n"); } else if (keyword.equals("Gift")) { sbBefore.append(keyword); @@ -3456,6 +3431,26 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr return sb; } + private String formatKeywordWithCost(final KeywordInterface inst) { + final String keyword = inst.getOriginal(); + final String[] k = keyword.split(":"); + final Cost mCost; + if (k.length < 2 || "ManaCost".equals(k[1])) { + mCost = new Cost(getManaCost(), false); + } else { + mCost = new Cost(k[1], false); + } + + StringBuilder sbCost = new StringBuilder(k[0]); + if (!mCost.isOnlyManaCost()) { + sbCost.append("—"); + } else { + sbCost.append(" "); + } + sbCost.append(mCost.toSimpleString()); + return sbCost + " (" + inst.getReminderText() + ")"; + } + private String formatSpellAbility(final SpellAbility sa) { final StringBuilder sb = new StringBuilder(); sb.append(sa.toString()).append("\r\n\r\n"); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index f5a3c133177..d4faab44a01 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -3621,6 +3621,31 @@ public class CardFactoryUtil { sa.setSVar("ScavengeX", "Exiled$CardPower"); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); + } else if (keyword.startsWith("Sneak")) { + final String[] k = keyword.split(":"); + final String manaCost = k[1]; + final Cost webCost = new Cost(manaCost + " Return<1/Creature.attacking+unblocked/unblocked attacker>", false); + + final SpellAbility newSA = card.getFirstSpellAbilityWithFallback().copyWithManaCostReplaced(host.getController(), webCost); + + if (k.length > 2) { + newSA.getMapParams().put("ValidAfterStack", k[2]); + } + + final StringBuilder desc = new StringBuilder(); + desc.append("Sneak ").append(ManaCostParser.parse(manaCost)).append(" ("); + desc.append(inst.getReminderText()); + desc.append(")"); + + newSA.setDescription(desc.toString()); + + final StringBuilder sb = new StringBuilder(); + sb.append(card.getName()).append(" (Sneak)"); + newSA.setStackDescription(sb.toString()); + newSA.putParam("Secondary", "True"); + newSA.setAlternativeCost(AlternativeCost.Sneak); + newSA.setIntrinsic(intrinsic); + inst.addSpellAbility(newSA); } else if (keyword.startsWith("Station")) { String effect = "AB$ PutCounter | Cost$ tapXType<1/Creature.Other> | Defined$ Self " + "| CounterType$ CHARGE | CounterNum$ StationX | SorcerySpeed$ True " + diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 65b74ab7276..de9091e62bd 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -168,10 +168,11 @@ public enum Keyword { RIOT("Riot", SimpleKeyword.class, false, "This creature enters with your choice of a +1/+1 counter or haste."), RIPPLE("Ripple", KeywordWithAmount.class, false, "When you cast this spell, you may reveal the top {%d:card} of your library. You may cast any of those cards with the same name as this spell without paying their mana costs. Put the rest on the bottom of your library in any order."), SADDLE("Saddle", KeywordWithAmount.class, false, "Tap any number of other creatures you control with total power %1$d or more: This Mount becomes saddled until end of turn. Saddle only as a sorcery."), + SCAVENGE("Scavenge", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Put a number of +1/+1 counters equal to this card's power on target creature. Scavenge only as a sorcery."), SHADOW("Shadow", SimpleKeyword.class, true, "This creature can block or be blocked by only creatures with shadow."), SHROUD("Shroud", SimpleKeyword.class, true, "This can't be the target of spells or abilities."), SKULK("Skulk", SimpleKeyword.class, true, "This creature can't be blocked by creatures with greater power."), - SCAVENGE("Scavenge", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Put a number of +1/+1 counters equal to this card's power on target creature. Scavenge only as a sorcery."), + SNEAK("Sneak", KeywordWithCost.class, false, "You may cast this spell for %s if you also return an unblocked attacker you control to hand during the declare blockers step."), SOULBOND("Soulbond", SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters. They remain paired for as long as you control both of them."), SOULSHIFT("Soulshift", KeywordWithAmount.class, false, "When this creature dies, you may return target Spirit card with mana value %d or less from your graveyard to your hand."), SPACE_SCULPTOR("Space sculptor", SimpleKeyword.class, true, "CARDNAME divides the battlefield into alpha, beta, and gamma sectors. If a creature isn't assigned to a sector, its controller assigns it to one. Opponents assign first."), diff --git a/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java b/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java index c8b6b0b9893..e27de46839b 100644 --- a/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java +++ b/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java @@ -21,6 +21,7 @@ public enum AlternativeCost { Overload, Prowl, Plotted, + Sneak, Spectacle, Surge, Warp, 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 f404a3bb148..3ef4ba06acf 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -686,6 +686,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return isAlternativeCost(AlternativeCost.Prowl); } + public final boolean isSneak() { + return isAlternativeCost(AlternativeCost.Sneak); + } + public final boolean isSurged() { return isAlternativeCost(AlternativeCost.Surge); } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java index 615d27406c6..cb0d14ac216 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -337,6 +337,11 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { return false; } } + if (sa.isSneak()) { + if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + return false; + } + } return true; } diff --git a/forge-gui/res/cardsfolder/upcoming/raphaels_technique.txt b/forge-gui/res/cardsfolder/upcoming/raphaels_technique.txt new file mode 100644 index 00000000000..7766cde845c --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/raphaels_technique.txt @@ -0,0 +1,8 @@ +Name:Raphael's Technique +ManaCost:4 R R +Types:Instant +K:Sneak:2 R +A:SP$ Discard | Mode$ Hand | Defined$ Player | Optional$ True | RememberDiscardingPlayers$ True | SubAbility$ DBDraw | SpellDescription$ Each player may discard their hand and draw seven cards. +SVar:DBDraw:DB$ Draw | Defined$ Remembered | NumCards$ 7 | SubAbility$ DBCleanup +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True +Oracle:Sneak {2}{R} (You may cast this spell for {2}{R} if you also return an unblocked attacker you control to hand during the declare blockers step.)\nEach player may discard their hand and draw seven cards.