diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index f9b5378fd7f..ecedfbdbff8 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -238,6 +238,12 @@ public class AiCostDecision extends CostDecisionMakerBase { return res.isEmpty() ? null : PaymentDecision.card(res); } + @Override + public PaymentDecision visit(final CostEnlist cost) { + // currently unused + return null; + } + @Override public PaymentDecision visit(CostFlipCoin cost) { int c = cost.getAbilityAmount(ability); diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 74b6b1bc93c..f9f2834e527 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -337,6 +337,12 @@ public class PlayerControllerAi extends PlayerController { public List exertAttackers(List attackers) { return AiAttackController.exertAttackers(attackers, brains.getAttackAggression()); } + + @Override + public List enlistAttackers(List attackers) { + // not able yet + return Lists.newArrayList(); + } @Override public CardCollection orderBlocker(Card attacker, Card blocker, CardCollection oldBlockers) { 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 2b39fa9ae76..8dff735a913 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -1211,7 +1211,6 @@ public class CardFactoryUtil { trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card)); inst.addTrigger(trigger); - } else if (keyword.equals("For Mirrodin")) { final StringBuilder sbTrig = new StringBuilder(); sbTrig.append("Mode$ ChangesZone | Destination$ Battlefield | "); @@ -1235,7 +1234,6 @@ public class CardFactoryUtil { saRebel.setIntrinsic(intrinsic); inst.addTrigger(etbTrigger); - } else if (keyword.startsWith("Graft")) { final StringBuilder sb = new StringBuilder(); sb.append("DB$ MoveCounter | Source$ Self | Defined$ TriggeredCardLKICopy"); @@ -3708,12 +3706,19 @@ public class CardFactoryUtil { + " | Amount$ Escalate | Cost$ "+ manacost +" | EffectZone$ All" + " | Description$ " + sb.toString() + " (" + inst.getReminderText() + ")"; inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); + } else if (keyword.equals("Enlist")) { + String effect = "Mode$ OptionalAttackCost | ValidCard$ Card.Self | Cost$ Enlist<1/CARDNAME> | Secondary$ True" + + "| Trigger$ TrigEnlist | Description$ Enlist ( " + inst.getReminderText() + ")"; + StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic); + st.setSVar("TrigEnlist", "DB$ Pump | NumAtt$ TriggerRemembered$CardPower" + + " | SpellDescription$ When you do, add its power to this creature’s until end of turn."); + inst.addStaticAbility(st); } else if (keyword.equals("Fear")) { - String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+nonBlack | Secondary$ True " + + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+nonBlack | Secondary$ True" + " | Description$ Fear ( " + inst.getReminderText() + ")"; inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Flying")) { - String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True " + + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True" + " | Description$ Flying ( " + inst.getReminderText() + ")"; inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.startsWith("Hexproof")) { diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index ae2377c7fe2..9c816a476a5 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -297,8 +297,8 @@ public class CombatUtil { * @param attacker * a {@link forge.game.card.Card} object. */ - public static boolean checkPropagandaEffects(final Game game, final Card attacker, final Combat combat, final List exerters) { - final Cost attackCost = getAttackCost(game, attacker, combat.getDefenderByAttacker(attacker), exerters); + public static boolean checkPropagandaEffects(final Game game, final Card attacker, final Combat combat, final List attackersWithOptionalCost) { + final Cost attackCost = getAttackCost(game, attacker, combat.getDefenderByAttacker(attacker), attackersWithOptionalCost); if (attackCost == null) { return true; } @@ -327,13 +327,13 @@ public class CombatUtil { * @return the {@link Cost} of attacking, or {@code null} if there is no * cost. */ - public static Cost getAttackCost(final Game game, final Card attacker, final GameEntity defender, final List exerters) { + public static Cost getAttackCost(final Game game, final Card attacker, final GameEntity defender, final List attackersWithOptionalCost) { final Cost attackCost = new Cost(ManaCost.ZERO, true); boolean hasCost = false; // Sort abilities to apply them in proper order for (final Card card : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final StaticAbility stAb : card.getStaticAbilities()) { - final Cost additionalCost = stAb.getAttackCost(attacker, defender, exerters); + final Cost additionalCost = stAb.getAttackCost(attacker, defender, attackersWithOptionalCost); if (null != additionalCost) { attackCost.add(additionalCost); hasCost = true; diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index d9a2ad21d0f..894879a7e48 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -514,6 +514,12 @@ public class Cost implements Serializable { return new CostExert(splitStr[0], splitStr[1], description); } + if (parse.startsWith("Enlist<")) { + final String[] splitStr = abCostParse(parse, 3); + final String description = splitStr.length > 2 ? splitStr[2] : null; + return new CostEnlist(splitStr[0], splitStr[1], description); + } + if (parse.equals("RevealChosenPlayer")) { return new CostRevealChosenPlayer(); } diff --git a/forge-game/src/main/java/forge/game/cost/CostEnlist.java b/forge-game/src/main/java/forge/game/cost/CostEnlist.java new file mode 100644 index 00000000000..9c0d9d96997 --- /dev/null +++ b/forge-game/src/main/java/forge/game/cost/CostEnlist.java @@ -0,0 +1,96 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * 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 . + */ +package forge.game.cost; + +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +/** + * The Class CostExert. + */ +public class CostEnlist extends CostPartWithTrigger { + + private static final long serialVersionUID = 1L; + + /** + * Instantiates a new cost Exert. + * + * @param amount + * the amount + * @param type + * the type + * @param description + * the description + */ + public CostEnlist(final String amount, final String type, final String description) { + super(amount, type, description); + } + + /* + * (non-Javadoc) + * + * @see forge.card.cost.CostPart#toString() + */ + @Override + public final String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Enlist " + this.getType()); + return sb.toString(); + } + + /* + * (non-Javadoc) + * + * @see + * forge.card.cost.CostPart#canPay(forge.card.spellability.SpellAbility, + * forge.Card, forge.Player, forge.card.cost.Cost) + */ + @Override + public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { + return true; + } + + @Override + protected Card doPayment(SpellAbility ability, Card targetCard, final boolean effect) { + targetCard.tap(true); + // need to transfer info + for (Card c : getLKIList()) { + payTrig.addRemembered(c); + } + return targetCard; + } + + /* (non-Javadoc) + * @see forge.card.cost.CostPartWithList#getHashForList() + */ + @Override + public String getHashForLKIList() { + return "Enlisted"; + } + @Override + public String getHashForCardList() { + return "EnlistedCards"; + } + + // Inputs + public T accept(ICostVisitor visitor) { + return visitor.visit(this); + } + +} diff --git a/forge-game/src/main/java/forge/game/cost/CostPartWithTrigger.java b/forge-game/src/main/java/forge/game/cost/CostPartWithTrigger.java index d776dbcf55a..240f154fe5d 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPartWithTrigger.java +++ b/forge-game/src/main/java/forge/game/cost/CostPartWithTrigger.java @@ -18,6 +18,8 @@ public abstract class CostPartWithTrigger extends CostPartWithList { super(amount, type, description); } + protected Trigger payTrig; + @Override protected final void handleBeforePayment(Player ai, SpellAbility ability, CardCollectionView targetCards) { if (payingTrigSA != null) { @@ -30,13 +32,13 @@ public abstract class CostPartWithTrigger extends CostPartWithList { SpellAbility sa = payingTrigSA.copy(source, ability.getActivatingPlayer(), false); sa.changeText(); - final Trigger immediateTrig = TriggerHandler.parseTrigger(mapParams, source, sa.isIntrinsic(), null); - immediateTrig.setSpawningAbility(ability); // make the StaticAbility the Spawning one? + payTrig = TriggerHandler.parseTrigger(mapParams, source, sa.isIntrinsic(), null); + payTrig.setSpawningAbility(ability); // make the StaticAbility the Spawning one? - immediateTrig.setOverridingAbility(sa); + payTrig.setOverridingAbility(sa); // Instead of registering this, add to the delayed triggers as an immediate trigger type? Which means it'll fire as soon as possible - ai.getGame().getTriggerHandler().registerDelayedTrigger(immediateTrig); + ai.getGame().getTriggerHandler().registerDelayedTrigger(payTrig); } } diff --git a/forge-game/src/main/java/forge/game/cost/ICostVisitor.java b/forge-game/src/main/java/forge/game/cost/ICostVisitor.java index 29df064dd01..fab4d46e486 100644 --- a/forge-game/src/main/java/forge/game/cost/ICostVisitor.java +++ b/forge-game/src/main/java/forge/game/cost/ICostVisitor.java @@ -12,6 +12,7 @@ public interface ICostVisitor { T visit(CostExileFromStack cost); T visit(CostExiledMoveToGrave cost); T visit(CostExert cost); + T visit(CostEnlist cost); T visit(CostFlipCoin cost); T visit(CostRollDice cost); T visit(CostMill cost); @@ -86,6 +87,11 @@ public interface ICostVisitor { return null; } + @Override + public T visit(CostEnlist cost) { + return null; + } + @Override public T visit(CostFlipCoin cost) { return null; 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 ba3288344de..22073210873 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -67,6 +67,7 @@ public enum Keyword { EMERGE("Emerge", KeywordWithCost.class, false, "You may cast this spell by sacrificing a creature and paying the emerge cost reduced by that creature's mana value."), ENCHANT("Enchant", KeywordWithType.class, false, "Target a %s as you cast this. This card enters the battlefield attached to that %s."), ENCORE("Encore", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: For each opponent, create a token copy that attacks that opponent this turn if able. They gain haste. Sacrifice them at the beginning of the next end step. Activate only as a sorcery."), + ENLIST("Enlist", SimpleKeyword.class, false, "As this creature attacks, you may tap a nonattacking creature you control without summoning sickness. When you do, add its power to this creature’s until end of turn."), ENTWINE("Entwine", KeywordWithCost.class, true, "Choose both if you pay the entwine cost."), EPIC("Epic", SimpleKeyword.class, true, "For the rest of the game, you can't cast spells. At the beginning of each of your upkeeps for the rest of the game, copy this spell except for its epic ability. If the spell has any targets, you may choose new targets for the copy."), EQUIP("Equip", Equip.class, false, "%s: Attach to target %s you control. Equip only as a sorcery."), diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index ad01140dbb3..ce1aad33cf5 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -40,6 +40,7 @@ import forge.game.card.CardZoneTable; import forge.game.card.CounterEnumType; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; +import forge.game.cost.CostEnlist; import forge.game.cost.CostExert; import forge.game.event.GameEventAttackersDeclared; import forge.game.event.GameEventBlockersDeclared; @@ -566,6 +567,13 @@ public class PhaseHandler implements java.io.Serializable { possibleExerters = whoDeclares.getController().exertAttackers(possibleExerters); } + List possibleEnlisters = CombatUtil.getOptionalAttackCostCreatures(combat.getAttackers(), CostEnlist.class); + if (!possibleEnlisters.isEmpty()) { + // TODO might want to skip if can't be paid + possibleEnlisters = whoDeclares.getController().enlistAttackers(possibleEnlisters); + possibleExerters.addAll(possibleEnlisters); + } + for (final Card attacker : combat.getAttackers()) { // TODO currently doesn't refund previous attackers (can really only happen if you cancel paying for a creature with an attack requirement that could be satisfied without a tax) final boolean canAttack = CombatUtil.checkPropagandaEffects(game, attacker, combat, possibleExerters); 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 ae46721fbb5..3eb731d1a5f 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -144,6 +144,8 @@ public abstract class PlayerController { public abstract CardCollection orderBlockers(Card attacker, CardCollection blockers); public abstract List exertAttackers(List attackers); + public abstract List enlistAttackers(List attackers); + /** * Add a card to a pre-existing blocking order. * @param attacker the attacking creature. diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index affd5c6ea66..8f2c2415017 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -325,8 +325,8 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone return false; } - public final Cost getAttackCost(final Card attacker, final GameEntity target, final List exerters) { - if (!getParam("Mode").equals("CantAttackUnless") && (!getParam("Mode").equals("OptionalAttackCost") || !exerters.contains(attacker))) { + public final Cost getAttackCost(final Card attacker, final GameEntity target, final List attackersWithOptionalCost) { + if (!getParam("Mode").equals("CantAttackUnless") && (!getParam("Mode").equals("OptionalAttackCost") || !attackersWithOptionalCost.contains(attacker))) { return null; } if (this.isSuppressed() || !this.checkConditions()) { diff --git a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index 11a54ec0284..d9d649f6492 100644 --- a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java +++ b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java @@ -224,6 +224,11 @@ public class PlayerControllerForTests extends PlayerController { return Lists.newArrayList(attackers); } + @Override + public List enlistAttackers(List attackers) { + return Lists.newArrayList(); + } + @Override public CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) { final CardCollection allBlockers = new CardCollection(oldBlockers); diff --git a/forge-gui/res/cardsfolder/b/barkweave_crusher.txt b/forge-gui/res/cardsfolder/b/barkweave_crusher.txt new file mode 100644 index 00000000000..dbfbcb08304 --- /dev/null +++ b/forge-gui/res/cardsfolder/b/barkweave_crusher.txt @@ -0,0 +1,6 @@ +Name:Barkweave Crusher +ManaCost:1 G +Types:Creature Elemental Warrior +PT:2/5 +K:Enlist +Oracle:Enlist (As this creature attacks, you may tap a nonattacking creature you control without summoning sickness. When you do, add its power to this creature’s until end of turn.) diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index 61c21acdcf1..8f67cc0b020 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -466,6 +466,22 @@ public class HumanCostDecision extends CostDecisionMakerBase { } + @Override + public PaymentDecision visit(final CostEnlist cost) { + CardCollectionView list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), "Creature.notattacking+untapped+withHaste,Creature.notattacking+untapped+notFirstTurnControlled", player, source, ability); + if (list.isEmpty()) { + return null; + } + final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, list, ability); + inp.setMessage(Localizer.getInstance().getMessage("lblSelectACostToEnlist", cost.getDescriptiveType(), "%d")); + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled()) { + return null; + } + return PaymentDecision.card(inp.getSelected()); + } + @Override public PaymentDecision visit(final CostFlipCoin cost) { Integer c = cost.getAbilityAmount(ability); diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index c62001316fa..db63beeaa83 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -283,6 +283,7 @@ public class HumanPlay { || part instanceof CostFlipCoin || part instanceof CostRollDice || part instanceof CostDamage + || part instanceof CostEnlist || part instanceof CostPutCounter || part instanceof CostRemoveCounter || part instanceof CostRemoveAnyCounter diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 655e12e6416..5e82dc62570 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -875,6 +875,17 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont return chosenCards; } + @Override + public List enlistAttackers(List attackers) { + GameEntityViewMap gameCacheExert = GameEntityView.getMap(attackers); + List chosen = getGui().order(localizer.getMessage("lblEnlistAttackersConfirm"), localizer.getMessage("lblEnlisted"), + 0, gameCacheExert.size(), gameCacheExert.getTrackableKeys(), null, null, false); + + List chosenCards = new CardCollection(); + gameCacheExert.addToList(chosen, chosenCards); + return chosenCards; + } + @Override public CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) { GameEntityViewMap gameCacheBlockers = GameEntityView.getMap(oldBlockers);