Merge branch 'master' into feature/implement-harmonize-ability

This commit is contained in:
theelk801 2025-03-31 09:15:25 -04:00
commit 970699de59
54 changed files with 1327 additions and 511 deletions

View file

@ -40,7 +40,7 @@ public class CombatGroupView implements Serializable {
attackers.put(id, new PermanentView(attacker, game.getCard(attacker.getId()),null, game));
}
}
for (UUID id: combatGroup.getBlockerOrder()) {
for (UUID id: combatGroup.getBlockers()) {
Permanent blocker = game.getPermanent(id);
if (blocker != null) {
blockers.put(id, new PermanentView(blocker, game.getCard(blocker.getId()), null, game));

View file

@ -13,10 +13,8 @@ import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.constants.RangeOfInfluence;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.tournament.Tournament;
import mage.players.Player;
import mage.target.Target;
@ -287,24 +285,6 @@ public class ComputerPlayerControllableProxy extends ComputerPlayer7 {
}
}
@Override
public UUID chooseAttackerOrder(java.util.List<Permanent> attackers, Game game) {
if (isUnderMe(game)) {
return super.chooseAttackerOrder(attackers, game);
} else {
return getControllingPlayer(game).chooseAttackerOrder(attackers, game);
}
}
@Override
public UUID chooseBlockerOrder(java.util.List<Permanent> blockers, CombatGroup combatGroup, java.util.List<UUID> blockerOrder, Game game) {
if (isUnderMe(game)) {
return super.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
} else {
return getControllingPlayer(game).chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
}
@Override
public int getAmount(int min, int max, String message, Game game) {
if (isUnderMe(game)) {

View file

@ -2177,9 +2177,6 @@ public class ComputerPlayer extends PlayerImpl {
// TODO: add AI support with outcome and replace random with min/max
public int getAmount(int min, int max, String message, Game game) {
log.debug("getAmount");
if (message.startsWith("Assign damage to ")) {
return min;
}
if (min < max && min == 0) {
return RandomUtil.nextInt(CardUtil.overflowInc(max, 1));
}
@ -2192,7 +2189,7 @@ public class ComputerPlayer extends PlayerImpl {
log.debug("getMultiAmount");
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, totalMin, totalMax);
List<Integer> defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0) {
return defaultList;
}
@ -2210,18 +2207,6 @@ public class ComputerPlayer extends PlayerImpl {
return MultiAmountType.prepareMaxValues(messages, totalMin, totalMax);
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attackers, Game game) {
//TODO: improve this
return attackers.iterator().next().getId();
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game) {
//TODO: improve this
return blockers.iterator().next().getId();
}
@Override
public List<MageObject> getAvailableManaProducers(Game game) {
return super.getAvailableManaProducers(game);

View file

@ -374,22 +374,6 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
return super.chooseMode(modes, source, game);
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attackers, Game game) {
if (this.isHuman()) {
return attackers.get(RandomUtil.nextInt(attackers.size())).getId();
}
return super.chooseAttackerOrder(attackers, game);
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game) {
if (this.isHuman()) {
return blockers.get(RandomUtil.nextInt(blockers.size())).getId();
}
return super.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
@Override
public int getAmount(int min, int max, String message, Game game) {
if (this.isHuman()) {

View file

@ -2122,56 +2122,6 @@ public class HumanPlayer extends PlayerImpl {
}
}
@Override
public UUID chooseAttackerOrder(java.util.List<Permanent> attackers, Game game) {
if (gameInCheckPlayableState(game)) {
return null;
}
while (canRespond()) {
prepareForResponse(game);
if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, "Pick attacker", attackers, true);
}
waitForResponse(game);
UUID responseId = getFixedResponseUUID(game);
if (responseId != null) {
for (Permanent perm : attackers) {
if (perm.getId().equals(responseId)) {
return perm.getId();
}
}
}
}
return null;
}
@Override
public UUID chooseBlockerOrder(java.util.List<Permanent> blockers, CombatGroup combatGroup, java.util.List<UUID> blockerOrder, Game game) {
if (gameInCheckPlayableState(game)) {
return null;
}
while (canRespond()) {
prepareForResponse(game);
if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, "Pick blocker", blockers, true);
}
waitForResponse(game);
UUID responseId = getFixedResponseUUID(game);
if (responseId != null) {
for (Permanent perm : blockers) {
if (perm.getId().equals(responseId)) {
return perm.getId();
}
}
}
}
return null;
}
protected void selectCombatGroup(UUID defenderId, UUID blockerId, Game game) {
if (gameInCheckPlayableState(game)) {
return;
@ -2260,7 +2210,7 @@ public class HumanPlayer extends PlayerImpl {
Game game
) {
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, totalMin, totalMax);
List<Integer> defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0 || (needCount == 1 && totalMin == totalMax)
|| messages.stream().map(m -> m.min == m.max).reduce(true, Boolean::logicalAnd)) {
// nothing to choose

View file

@ -153,10 +153,6 @@ class BalduvianWarlordUnblockEffect extends OneShotEffect {
game.fireEvent(new BlockerDeclaredEvent(chosenPermanent.getId(), permanent.getId(), permanent.getControllerId()));
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.CREATURE_BLOCKS, permanent.getId(), source, null));
}
CombatGroup blockGroup = findBlockingGroup(permanent, game); // a new blockingGroup is formed, so it's necessary to find it again
if (blockGroup != null) {
blockGroup.pickAttackerOrder(permanent.getControllerId(), game);
}
}
}
return true;
@ -164,15 +160,4 @@ class BalduvianWarlordUnblockEffect extends OneShotEffect {
}
return false;
}
private CombatGroup findBlockingGroup(Permanent blocker, Game game) {
if (game.getCombat().blockingGroupsContains(blocker.getId())) { // if (blocker.getBlocking() > 1) {
for (CombatGroup group : game.getCombat().getBlockingGroups()) {
if (group.getBlockers().contains(blocker.getId())) {
return group;
}
}
}
return null;
}
}

View file

@ -1,7 +1,6 @@
package mage.cards.b;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
@ -22,6 +21,8 @@ import mage.game.permanent.token.CatSoldierCreatureToken;
import mage.game.permanent.token.Token;
import mage.players.Player;
import java.util.UUID;
/**
*
* @author LevelX2
@ -99,7 +100,6 @@ class BrimazKingOfOreskosEffect extends OneShotEffect {
combatGroup.addBlocker(tokenId, source.getControllerId(), game);
game.getCombat().addBlockingGroup(tokenId, attackingCreature.getId(), controller.getId(), game);
}
combatGroup.pickBlockerOrder(attackingCreature.getControllerId(), game);
return true;
}

View file

@ -169,21 +169,6 @@ class FalseOrdersUnblockEffect extends OneShotEffect {
game.fireEvent(new BlockerDeclaredEvent(chosenPermanent.getId(), permanent.getId(), permanent.getControllerId()));
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.CREATURE_BLOCKS, permanent.getId(), source, null));
}
CombatGroup blockGroup = findBlockingGroup(permanent, game); // a new blockingGroup is formed, so it's necessary to find it again
if (blockGroup != null) {
blockGroup.pickAttackerOrder(permanent.getControllerId(), game);
}
return true;
}
private CombatGroup findBlockingGroup(Permanent blocker, Game game) {
if (game.getCombat().blockingGroupsContains(blocker.getId())) { // if (blocker.getBlocking() > 1) {
for (CombatGroup group : game.getCombat().getBlockingGroups()) {
if (group.getBlockers().contains(blocker.getId())) {
return group;
}
}
}
return null;
}
}

View file

@ -0,0 +1,55 @@
package mage.cards.f;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.abilities.keyword.FlashAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class FireRimForm extends CardImpl {
public FireRimForm(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}");
this.subtype.add(SubType.AURA);
// Flash
this.addAbility(FlashAbility.getInstance());
// Enchant creature
TargetPermanent auraTarget = new TargetCreaturePermanent();
this.getSpellAbility().addTarget(auraTarget);
this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(new EnchantAbility(auraTarget));
// When this Aura enters, enchanted creature gains first strike until end of turn.
this.addAbility(new EntersBattlefieldTriggeredAbility(new GainAbilityAttachedEffect(
FirstStrikeAbility.getInstance(), AttachmentType.AURA, Duration.EndOfTurn
)));
// Enchanted creature gets +2/+0.
this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(2, 0)));
}
private FireRimForm(final FireRimForm card) {
super(card);
}
@Override
public FireRimForm copy() {
return new FireRimForm(this);
}
}

View file

@ -90,7 +90,6 @@ class FlashFoliageEffect extends OneShotEffect {
game.getCombat().addBlockingGroup(tokenId, attackingCreature.getId(), controller.getId(), game);
}
}
combatGroup.pickBlockerOrder(attackingCreature.getControllerId(), game);
}
}
return true;

View file

@ -0,0 +1,50 @@
package mage.cards.f;
import mage.MageInt;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.ReturnToHandSourceEffect;
import mage.abilities.effects.common.continuous.BoostSourceEffect;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class FleetingEffigy extends CardImpl {
public FleetingEffigy(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}");
this.subtype.add(SubType.ELEMENTAL);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
// Haste
this.addAbility(HasteAbility.getInstance());
// At the beginning of your end step, return this creature to its owner's hand.
this.addAbility(new BeginningOfEndStepTriggeredAbility(new ReturnToHandSourceEffect()));
// {2}{R}: This creature gets +2/+0 until end of turn.
this.addAbility(new SimpleActivatedAbility(
new BoostSourceEffect(2, 0, Duration.EndOfTurn), new ManaCostsImpl<>("{2}{R}")
));
}
private FleetingEffigy(final FleetingEffigy card) {
super(card);
}
@Override
public FleetingEffigy copy() {
return new FleetingEffigy(this);
}
}

View file

@ -1,10 +1,6 @@
package mage.cards.g;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.condition.common.IsStepCondition;
@ -13,18 +9,18 @@ import mage.abilities.decorator.ConditionalActivatedAbility;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.constants.*;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetAttackingCreature;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
*
* @author L_J
@ -138,11 +134,9 @@ class GeneralJarkeldSwitchBlockersEffect extends OneShotEffect {
// the ability doesn't unblock a group that loses all blockers, however it will newly block a previously unblocked group if it gains a blocker this way
if (!(chosenGroup1.getBlockers().isEmpty())) {
chosenGroup1.setBlocked(true, game);
chosenGroup1.pickBlockerOrder(attacker1.getControllerId(), game);
}
if (!(chosenGroup2.getBlockers().isEmpty())) {
chosenGroup2.setBlocked(true, game);
chosenGroup2.pickBlockerOrder(attacker2.getControllerId(), game);
}
return true;
}
@ -197,7 +191,6 @@ class GeneralJarkeldSwitchBlockersEffect extends OneShotEffect {
// 10/4/2004 The new blocker does not trigger any abilities which trigger on creatures becoming blockers, because the creatures were already blockers and the simple change of who is blocking does not trigger such abilities.
game.getCombat().addBlockingGroup(blocker.getId(), attacker, controller.getId(), game);
}
blockGroup.pickAttackerOrder(blocker.getControllerId(), game);
}
}
}

View file

@ -0,0 +1,37 @@
package mage.cards.k;
import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.counters.CounterType;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetOpponentsCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class KnockoutManeuver extends CardImpl {
public KnockoutManeuver(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{G}");
// Put a +1/+1 counter on target creature you control, then it deals damage equal to its power to target creature an opponent controls.
this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance()));
this.getSpellAbility().addEffect(new DamageWithPowerFromOneToAnotherTargetEffect(", then it"));
this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent());
this.getSpellAbility().addTarget(new TargetOpponentsCreaturePermanent());
}
private KnockoutManeuver(final KnockoutManeuver card) {
super(card);
}
@Override
public KnockoutManeuver copy() {
return new KnockoutManeuver(this);
}
}

View file

@ -0,0 +1,87 @@
package mage.cards.k;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.IndestructibleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class KotisTheFangkeeper extends CardImpl {
public KotisTheFangkeeper(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{G}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.ZOMBIE);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(2);
this.toughness = new MageInt(1);
// Indestructible
this.addAbility(IndestructibleAbility.getInstance());
// Whenever Kotis deals combat damage to a player, exile the top X cards of their library, where X is the amount of damage dealt. You may cast any number of spells with mana value X or less from among them without paying their mana costs.
this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(
new KotisTheFangkeeperEffect(), false, true
));
}
private KotisTheFangkeeper(final KotisTheFangkeeper card) {
super(card);
}
@Override
public KotisTheFangkeeper copy() {
return new KotisTheFangkeeper(this);
}
}
class KotisTheFangkeeperEffect extends OneShotEffect {
KotisTheFangkeeperEffect() {
super(Outcome.Benefit);
staticText = "exile the top X cards of their library, where X is the amount of damage dealt. You may " +
"cast any number of spells with mana value X or less from among them without paying their mana costs";
}
private KotisTheFangkeeperEffect(final KotisTheFangkeeperEffect effect) {
super(effect);
}
@Override
public KotisTheFangkeeperEffect copy() {
return new KotisTheFangkeeperEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Player player = game.getPlayer(getTargetPointer().getFirst(game, source));
int xValue = GetXValue.instance.calculate(game, source, this);
if (controller == null || player == null || xValue < 1) {
return false;
}
Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, xValue));
controller.moveCards(cards, Zone.EXILED, source, game);
FilterCard filter = new FilterCard();
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, xValue + 1));
CardUtil.castMultipleWithAttributeForFree(controller, source, game, cards, filter);
return true;
}
}

View file

@ -0,0 +1,54 @@
package mage.cards.m;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.AttacksWithCreaturesTriggeredAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount;
import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.abilities.keyword.DeathtouchAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class MarshalOfTheLost extends CardImpl {
private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(StaticFilters.FILTER_ATTACKING_CREATURES, null);
private static final Hint hint = new ValueHint("Attacking creatures", xValue);
public MarshalOfTheLost(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{B}");
this.subtype.add(SubType.ORC);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// Deathtouch
this.addAbility(DeathtouchAbility.getInstance());
// Whenever you attack, target creature gets +X/+X until end of turn, where X is the number of attacking creatures.
Ability ability = new AttacksWithCreaturesTriggeredAbility(new BoostTargetEffect(xValue, xValue), 1);
ability.addTarget(new TargetCreaturePermanent());
this.addAbility(ability.addHint(hint));
}
private MarshalOfTheLost(final MarshalOfTheLost card) {
super(card);
}
@Override
public MarshalOfTheLost copy() {
return new MarshalOfTheLost(this);
}
}

View file

@ -1,13 +1,12 @@
package mage.cards.m;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.CastOnlyDuringPhaseStepSourceAbility;
import mage.abilities.common.delayed.AtTheEndOfCombatDelayedTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.abilities.effects.common.CreateTokenCopyTargetEffect;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -20,6 +19,8 @@ import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.target.targetpointer.FixedTargets;
import java.util.UUID;
/**
*
* @author LevelX2
@ -75,20 +76,15 @@ class MirrorMatchEffect extends OneShotEffect {
effect.setTargetPointer(new FixedTarget(attacker, game));
effect.apply(game, source);
CombatGroup group = game.getCombat().findGroup(attacker.getId());
boolean isCreature = false;
if (group != null) {
for (Permanent addedToken : effect.getAddedPermanents()) {
if (addedToken.isCreature(game)) {
group.addBlockerToGroup(addedToken.getId(), attackerId, game);
isCreature = true;
}
}
ExileTargetEffect exileEffect = new ExileTargetEffect("Exile those tokens at end of combat");
exileEffect.setTargetPointer(new FixedTargets(effect.getAddedPermanents(), game));
game.addDelayedTriggeredAbility(new AtTheEndOfCombatDelayedTriggeredAbility(exileEffect), source);
if (isCreature) {
group.pickBlockerOrder(attacker.getControllerId(), game);
}
}
}
}

View file

@ -0,0 +1,40 @@
package mage.cards.m;
import mage.abilities.common.PayMoreToCastAsThoughtItHadFlashAbility;
import mage.abilities.costs.common.BeholdDragonCost;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.target.common.TargetCreatureOrPlaneswalker;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class MoltenExhale extends CardImpl {
public MoltenExhale(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}");
// You may cast this spell as though it had flash if you behold a Dragon as an additional cost to cast it.
this.addAbility(new PayMoreToCastAsThoughtItHadFlashAbility(
this, new BeholdDragonCost(), "you may cast this spell as though " +
"it had flash if you behold a Dragon as an additional cost to cast it"
));
// Molten Exhale deals 4 damage to target creature or planeswalker.
this.getSpellAbility().addEffect(new DamageTargetEffect(4));
this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker());
}
private MoltenExhale(final MoltenExhale card) {
super(card);
}
@Override
public MoltenExhale copy() {
return new MoltenExhale(this);
}
}

View file

@ -0,0 +1,69 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAllTriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.costs.common.BeholdDragonCost;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.abilities.effects.common.continuous.AddCardSubTypeSourceEffect;
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.game.permanent.token.TreasureToken;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SarkhanDragonAscendant extends CardImpl {
private static final FilterPermanent filter = new FilterControlledPermanent(SubType.DRAGON, "a Dragon you control");
public SarkhanDragonAscendant(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.DRUID);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
// When Sarkhan enters, you may behold a Dragon. If you do, create a Treasure token.
this.addAbility(new EntersBattlefieldTriggeredAbility(
new DoIfCostPaid(new CreateTokenEffect(new TreasureToken()), new BeholdDragonCost())
));
// Whenever a Dragon you control enters, put a +1/+1 counter on Sarkhan. Until end of turn, Sarkhan becomes a Dragon in addition to its other types and gains flying.
Ability ability = new EntersBattlefieldAllTriggeredAbility(
new AddCountersSourceEffect(CounterType.P1P1.createInstance()), filter
);
ability.addEffect(new AddCardSubTypeSourceEffect(
Duration.EndOfTurn, SubType.DRAGON
).setText("until end of turn, {this} becomes a Dragon in addition to its other types"));
ability.addEffect(new GainAbilitySourceEffect(
FlyingAbility.getInstance(), Duration.EndOfTurn
).setText("and gains flying"));
this.addAbility(ability);
}
private SarkhanDragonAscendant(final SarkhanDragonAscendant card) {
super(card);
}
@Override
public SarkhanDragonAscendant copy() {
return new SarkhanDragonAscendant(this);
}
}

View file

@ -0,0 +1,80 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.keyword.FlashAbility;
import mage.abilities.keyword.HarmonizeAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.target.common.TargetCardInYourGraveyard;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SongcrafterMage extends CardImpl {
public SongcrafterMage(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}{R}");
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.BARD);
this.power = new MageInt(3);
this.toughness = new MageInt(2);
// Flash
this.addAbility(FlashAbility.getInstance());
// When this creature enters, target instant or sorcery card in your graveyard gains harmonize until end of turn. Its harmonize cost is equal to its mana cost.
Ability ability = new EntersBattlefieldTriggeredAbility(new SongcrafterMageEffect());
ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD));
this.addAbility(ability);
}
private SongcrafterMage(final SongcrafterMage card) {
super(card);
}
@Override
public SongcrafterMage copy() {
return new SongcrafterMage(this);
}
}
class SongcrafterMageEffect extends ContinuousEffectImpl {
SongcrafterMageEffect() {
super(Duration.EndOfTurn, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.staticText = "target instant or sorcery card in your graveyard gains harmonize until end of turn. " +
"The harmonize cost is equal to its mana cost";
}
private SongcrafterMageEffect(final SongcrafterMageEffect effect) {
super(effect);
}
@Override
public SongcrafterMageEffect copy() {
return new SongcrafterMageEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = game.getCard(getTargetPointer().getFirst(game, source));
if (card == null) {
return false;
}
Ability ability = new HarmonizeAbility(card, card.getManaCost().getText());
ability.setSourceId(card.getId());
ability.setControllerId(card.getOwnerId());
game.getState().addOtherAbility(card, ability);
return true;
}
}

View file

@ -0,0 +1,93 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetAnyTarget;
import java.util.Optional;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SonicShrieker extends CardImpl {
public SonicShrieker(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{W}{B}");
this.subtype.add(SubType.DRAGON);
this.power = new MageInt(4);
this.toughness = new MageInt(4);
// Flying
this.addAbility(FlyingAbility.getInstance());
// When this creature enters, it deals 2 damage to any target and you gain 2 life. If a player is dealt damage this way, they discard a card.
Ability ability = new EntersBattlefieldTriggeredAbility(new SonicShriekerEffect());
ability.addTarget(new TargetAnyTarget());
this.addAbility(ability);
}
private SonicShrieker(final SonicShrieker card) {
super(card);
}
@Override
public SonicShrieker copy() {
return new SonicShrieker(this);
}
}
class SonicShriekerEffect extends OneShotEffect {
SonicShriekerEffect() {
super(Outcome.Benefit);
staticText = "it deals 2 damage to any target and you gain 2 life. " +
"If a player is dealt damage this way, they discard a card";
}
private SonicShriekerEffect(final SonicShriekerEffect effect) {
super(effect);
}
@Override
public SonicShriekerEffect copy() {
return new SonicShriekerEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = dealDamage(getTargetPointer().getFirst(game, source), game, source);
Optional.ofNullable(source.getControllerId())
.map(game::getPlayer)
.ifPresent(controller -> controller.gainLife(2, game, source));
if (player != null) {
player.discard(1, false, false, source, game);
}
return true;
}
private static Player dealDamage(UUID targetId, Game game, Ability source) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
permanent.damage(2, source, game);
return null;
}
Player player = game.getPlayer(targetId);
if (player != null && player.damage(2, source, game) > 0) {
return player;
}
return null;
}
}

View file

@ -176,14 +176,8 @@ class SorrowsPathSwitchBlockersEffect extends OneShotEffect {
group.addBlockerToGroup(blocker.getId(), blocker.getControllerId(), game);
game.getCombat().addBlockingGroup(blocker.getId(), attacker.getId(), blocker.getControllerId(), game);
game.fireEvent(new BlockerDeclaredEvent(attacker.getId(), blocker.getId(), blocker.getControllerId()));
group.pickBlockerOrder(attacker.getControllerId(), game);
}
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.CREATURE_BLOCKS, blocker.getId(), source, null));
CombatGroup blockGroup = findBlockingGroup(blocker, game); // a new blockingGroup is formed, so it's necessary to find it again
if (blockGroup != null) {
blockGroup.pickAttackerOrder(blocker.getControllerId(), game);
}
}
}

View file

@ -0,0 +1,52 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.dynamicvalue.common.SourcePermanentPowerValue;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.keyword.MobilizeAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class StadiumHeadliner extends CardImpl {
public StadiumHeadliner(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}");
this.subtype.add(SubType.GOBLIN);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(1);
this.toughness = new MageInt(1);
// Mobilize 1
this.addAbility(new MobilizeAbility(1));
// {1}{R}, Sacrifice this creature: It deals damage equal to the number of creatures you control to target creature.
Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(
SourcePermanentPowerValue.NOT_NEGATIVE, "it"
), new ManaCostsImpl<>("{1}{R}"));
ability.addCost(new SacrificeSourceCost());
ability.addTarget(new TargetCreaturePermanent());
this.addAbility(ability);
}
private StadiumHeadliner(final StadiumHeadliner card) {
super(card);
}
@Override
public StadiumHeadliner copy() {
return new StadiumHeadliner(this);
}
}

View file

@ -0,0 +1,55 @@
package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount;
import mage.abilities.effects.common.ExileUntilSourceLeavesEffect;
import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.abilities.keyword.FlashAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class StaticSnare extends CardImpl {
private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(StaticFilters.FILTER_ATTACKING_CREATURE);
private static final Hint hint = new ValueHint("Attacking creatures", xValue);
public StaticSnare(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{W}");
// Flash
this.addAbility(FlashAbility.getInstance());
// This spell costs {1} less to cast for each attacking creature.
this.addAbility(new SimpleStaticAbility(
Zone.ALL, new SpellCostReductionForEachSourceEffect(1, xValue)
).addHint(hint));
// When this enchantment enters, exile target artifact or creature an opponent controls until this enchantment leaves the battlefield.
Ability ability = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect());
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_ARTIFACT_OR_CREATURE));
this.addAbility(ability);
}
private StaticSnare(final StaticSnare card) {
super(card);
}
@Override
public StaticSnare copy() {
return new StaticSnare(this);
}
}

View file

@ -0,0 +1,57 @@
package mage.cards.s;
import mage.abilities.common.AttacksAttachedTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.continuous.BoostEquippedEffect;
import mage.abilities.keyword.EquipAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.AttachmentType;
import mage.constants.CardType;
import mage.constants.ComparisonType;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class StormbeaconBlade extends CardImpl {
private static final Condition condition = new PermanentsOnTheBattlefieldCondition(
StaticFilters.FILTER_ATTACKING_CREATURE,
ComparisonType.MORE_THAN, 2, true
);
public StormbeaconBlade(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{W}");
this.subtype.add(SubType.EQUIPMENT);
// Equipped creature gets +3/+0.
this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(3, 0)));
// Whenever equipped creature attacks, draw a card if you control three or more attacking creatures.
this.addAbility(new AttacksAttachedTriggeredAbility(new ConditionalOneShotEffect(
new DrawCardSourceControllerEffect(1), condition,
"draw a card if you control three or more attacking creatures"
), AttachmentType.EQUIPMENT, false));
// Equip {2}
this.addAbility(new EquipAbility(2));
}
private StormbeaconBlade(final StormbeaconBlade card) {
super(card);
}
@Override
public StormbeaconBlade copy() {
return new StormbeaconBlade(this);
}
}

View file

@ -0,0 +1,77 @@
package mage.cards.t;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAllTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SetTargetPointer;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterCreatureCard;
import mage.filter.predicate.Predicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.ZombieDruidToken;
import mage.game.stack.Spell;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class TheSibsigCeremony extends CardImpl {
private static final FilterCard filter = new FilterCreatureCard("creature spells");
private static final FilterPermanent filter2 = new FilterControlledCreaturePermanent();
static {
filter2.add(TheSibsigCeremonyPredicate.instance);
}
public TheSibsigCeremony(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}{B}{B}");
this.supertype.add(SuperType.LEGENDARY);
// Creature spells you cast cost {2} less to cast.
this.addAbility(new SimpleStaticAbility(new SpellsCostReductionControllerEffect(filter, 2)));
// Whenever a creature you control enters, if you cast it, destroy that creature, then create a 2/2 black Zombie Druid creature token.
Ability ability = new EntersBattlefieldAllTriggeredAbility(
Zone.BATTLEFIELD, new DestroyTargetEffect()
.setText("if you cast it, destroy that creature"),
filter2, false, SetTargetPointer.PERMANENT
);
ability.addEffect(new CreateTokenEffect(new ZombieDruidToken()).concatBy(", then"));
this.addAbility(ability);
}
private TheSibsigCeremony(final TheSibsigCeremony card) {
super(card);
}
@Override
public TheSibsigCeremony copy() {
return new TheSibsigCeremony(this);
}
}
enum TheSibsigCeremonyPredicate implements Predicate<Permanent> {
instance;
@Override
public boolean apply(Permanent input, Game game) {
int zcc = input.getZoneChangeCounter(game);
Spell spell = game.getStack().getSpell(input.getId());
return (spell != null && spell.getZoneChangeCounter(game) == zcc - 1)
|| game.getLastKnownInformation(input.getId(), Zone.STACK, zcc - 1) != null;
}
}

View file

@ -0,0 +1,108 @@
package mage.cards.t;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ShuffleIntoLibraryTargetEffect;
import mage.cards.*;
import mage.choices.FaceVillainousChoice;
import mage.choices.VillainousChoice;
import mage.constants.*;
import mage.game.Game;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent;
import mage.players.Player;
import mage.target.common.TargetCreaturePermanent;
/**
*
* @author padfoothelix
*/
public final class ThisIsHowItEnds extends CardImpl {
public ThisIsHowItEnds(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}");
// Target creature's owner shuffles it into their library, then faces a villainous choice -- They lose 5 life, or they shuffle another creature they own into their library.
this.getSpellAbility().addEffect(
new ShuffleIntoLibraryTargetEffect()
.setText("target creature's owner shuffles it into their library,")
);
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
this.getSpellAbility().addEffect(new ThisIsHowItEndsEffect());
}
private ThisIsHowItEnds(final ThisIsHowItEnds card) {
super(card);
}
@Override
public ThisIsHowItEnds copy() {
return new ThisIsHowItEnds(this);
}
}
class ThisIsHowItEndsEffect extends OneShotEffect {
private static final FaceVillainousChoice choice = new FaceVillainousChoice(
Outcome.Removal, new ThisIsHowItEndsFirstChoice(), new ThisIsHowItEndsSecondChoice()
);
ThisIsHowItEndsEffect() {
super(Outcome.Benefit);
staticText = "then " + choice.generateRule();
}
private ThisIsHowItEndsEffect(final ThisIsHowItEndsEffect effect) {
super(effect);
}
@Override
public ThisIsHowItEndsEffect copy() {
return new ThisIsHowItEndsEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
UUID targetOwnerId = game.getOwnerId(getTargetPointer().getFirst(game, source));
Player targetOwner = game.getPlayer(targetOwnerId);
choice.faceChoice(targetOwner, game, source);
return true;
}
}
class ThisIsHowItEndsFirstChoice extends VillainousChoice {
ThisIsHowItEndsFirstChoice() {
super("They lose 5 life","You lose 5 life");
}
@Override
public boolean doChoice(Player player, Game game, Ability source) {
player.loseLife(5, game, source, false);
return true;
}
}
class ThisIsHowItEndsSecondChoice extends VillainousChoice {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature you own");
static {
filter.add(TargetController.YOU.getOwnerPredicate());
}
ThisIsHowItEndsSecondChoice() {
super("they shuffle another creature they own into their library", "you shuffle a creature you own into your library");
}
@Override
public boolean doChoice(Player player, Game game, Ability source) {
TargetCreaturePermanent target = new TargetCreaturePermanent(1, 1, filter, true);
target.withChooseHint("to shuffle into your library");
player.chooseTarget(Outcome.Detriment, target, source, game);
Cards cards = new CardsImpl(target.getTargets());
player.shuffleCardsToLibrary(cards, game, source);
return true;
}
}

View file

@ -0,0 +1,58 @@
package mage.cards.z;
import mage.MageInt;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.IsStepCondition;
import mage.abilities.decorator.ConditionalContinuousEffect;
import mage.abilities.effects.common.continuous.CantBeSacrificedSourceEffect;
import mage.abilities.effects.common.continuous.GainAbilityControlledEffect;
import mage.abilities.keyword.MobilizeAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.predicate.permanent.TokenPredicate;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class ZurgoThundersDecree extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent(SubType.WARRIOR, "");
static {
filter.add(TokenPredicate.TRUE);
}
private static final Condition condition = new IsStepCondition(PhaseStep.END_TURN, true);
public ZurgoThundersDecree(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{W}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.ORC);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(2);
this.toughness = new MageInt(4);
// Mobilize 2
this.addAbility(new MobilizeAbility(2));
// During your end step, Warrior tokens you control have "This token can't be sacrificed."
this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect(new GainAbilityControlledEffect(
new SimpleStaticAbility(new CantBeSacrificedSourceEffect()), Duration.WhileOnBattlefield
), condition, "during your end step, Warrior tokens you control have \"This token can't be sacrificed.\"")));
}
private ZurgoThundersDecree(final ZurgoThundersDecree card) {
super(card);
}
@Override
public ZurgoThundersDecree copy() {
return new ZurgoThundersDecree(this);
}
}

View file

@ -1093,10 +1093,10 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("Thijarian Witness", 716, Rarity.UNCOMMON, mage.cards.t.ThijarianWitness.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Think Twice", 220, Rarity.COMMON, mage.cards.t.ThinkTwice.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Think Twice", 811, Rarity.COMMON, mage.cards.t.ThinkTwice.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("This Is How It Ends", 373, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("This Is How It Ends", 675, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("This Is How It Ends", 70, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("This Is How It Ends", 964, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("This Is How It Ends", 373, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("This Is How It Ends", 675, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("This Is How It Ends", 70, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("This Is How It Ends", 964, Rarity.RARE, mage.cards.t.ThisIsHowItEnds.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Thought Vessel", 255, Rarity.UNCOMMON, mage.cards.t.ThoughtVessel.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Thought Vessel", 846, Rarity.UNCOMMON, mage.cards.t.ThoughtVessel.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Three Visits", 235, Rarity.UNCOMMON, mage.cards.t.ThreeVisits.class, NON_FULL_USE_VARIOUS));

View file

@ -80,6 +80,8 @@ public final class TarkirDragonstorm extends ExpansionSet {
cards.add(new SetCardInfo("Evolving Wilds", 255, Rarity.COMMON, mage.cards.e.EvolvingWilds.class));
cards.add(new SetCardInfo("Fangkeeper's Familiar", 183, Rarity.RARE, mage.cards.f.FangkeepersFamiliar.class));
cards.add(new SetCardInfo("Felothar, Dawn of the Abzan", 184, Rarity.RARE, mage.cards.f.FelotharDawnOfTheAbzan.class));
cards.add(new SetCardInfo("Fire-Rim Form", 107, Rarity.COMMON, mage.cards.f.FireRimForm.class));
cards.add(new SetCardInfo("Fleeting Effigy", 108, Rarity.UNCOMMON, mage.cards.f.FleetingEffigy.class));
cards.add(new SetCardInfo("Forest", 285, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Fortress Kin-Guard", 12, Rarity.COMMON, mage.cards.f.FortressKinGuard.class));
cards.add(new SetCardInfo("Fresh Start", 46, Rarity.UNCOMMON, mage.cards.f.FreshStart.class));
@ -107,13 +109,17 @@ public final class TarkirDragonstorm extends ExpansionSet {
cards.add(new SetCardInfo("Kishla Skimmer", 201, Rarity.UNCOMMON, mage.cards.k.KishlaSkimmer.class));
cards.add(new SetCardInfo("Kishla Trawlers", 50, Rarity.UNCOMMON, mage.cards.k.KishlaTrawlers.class));
cards.add(new SetCardInfo("Kishla Village", 259, Rarity.RARE, mage.cards.k.KishlaVillage.class));
cards.add(new SetCardInfo("Knockout Maneuver", 147, Rarity.UNCOMMON, mage.cards.k.KnockoutManeuver.class));
cards.add(new SetCardInfo("Kotis, the Fangkeeper", 202, Rarity.RARE, mage.cards.k.KotisTheFangkeeper.class));
cards.add(new SetCardInfo("Krotiq Nestguard", 148, Rarity.COMMON, mage.cards.k.KrotiqNestguard.class));
cards.add(new SetCardInfo("Lightfoot Technique", 14, Rarity.COMMON, mage.cards.l.LightfootTechnique.class));
cards.add(new SetCardInfo("Loxodon Battle Priest", 15, Rarity.UNCOMMON, mage.cards.l.LoxodonBattlePriest.class));
cards.add(new SetCardInfo("Mammoth Bellow", 205, Rarity.UNCOMMON, mage.cards.m.MammothBellow.class));
cards.add(new SetCardInfo("Mardu Devotee", 16, Rarity.COMMON, mage.cards.m.MarduDevotee.class));
cards.add(new SetCardInfo("Mardu Monument", 245, Rarity.UNCOMMON, mage.cards.m.MarduMonument.class));
cards.add(new SetCardInfo("Marshal of the Lost", 207, Rarity.UNCOMMON, mage.cards.m.MarshalOfTheLost.class));
cards.add(new SetCardInfo("Meticulous Artisan", 112, Rarity.COMMON, mage.cards.m.MeticulousArtisan.class));
cards.add(new SetCardInfo("Molten Exhale", 113, Rarity.COMMON, mage.cards.m.MoltenExhale.class));
cards.add(new SetCardInfo("Monastery Messenger", 208, Rarity.COMMON, mage.cards.m.MonasteryMessenger.class));
cards.add(new SetCardInfo("Mountain", 283, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Mox Jasper", 246, Rarity.MYTHIC, mage.cards.m.MoxJasper.class));
@ -150,6 +156,7 @@ public final class TarkirDragonstorm extends ExpansionSet {
cards.add(new SetCardInfo("Sandskitter Outrider", 89, Rarity.COMMON, mage.cards.s.SandskitterOutrider.class));
cards.add(new SetCardInfo("Sandsteppe Citadel", 266, Rarity.UNCOMMON, mage.cards.s.SandsteppeCitadel.class));
cards.add(new SetCardInfo("Sarkhan's Resolve", 158, Rarity.COMMON, mage.cards.s.SarkhansResolve.class));
cards.add(new SetCardInfo("Sarkhan, Dragon Ascendant", 118, Rarity.RARE, mage.cards.s.SarkhanDragonAscendant.class));
cards.add(new SetCardInfo("Scoured Barrens", 267, Rarity.COMMON, mage.cards.s.ScouredBarrens.class));
cards.add(new SetCardInfo("Seize Opportunity", 119, Rarity.COMMON, mage.cards.s.SeizeOpportunity.class));
cards.add(new SetCardInfo("Shiko, Paragon of the Way", 223, Rarity.MYTHIC, mage.cards.s.ShikoParagonOfTheWay.class));
@ -161,7 +168,12 @@ public final class TarkirDragonstorm extends ExpansionSet {
cards.add(new SetCardInfo("Smile at Death", 24, Rarity.MYTHIC, mage.cards.s.SmileAtDeath.class));
cards.add(new SetCardInfo("Snakeskin Veil", 159, Rarity.COMMON, mage.cards.s.SnakeskinVeil.class));
cards.add(new SetCardInfo("Snowmelt Stag", 57, Rarity.COMMON, mage.cards.s.SnowmeltStag.class));
cards.add(new SetCardInfo("Songcrafter Mage", 225, Rarity.RARE, mage.cards.s.SongcrafterMage.class));
cards.add(new SetCardInfo("Sonic Shrieker", 226, Rarity.UNCOMMON, mage.cards.s.SonicShrieker.class));
cards.add(new SetCardInfo("Spectral Denial", 58, Rarity.UNCOMMON, mage.cards.s.SpectralDenial.class));
cards.add(new SetCardInfo("Stadium Headliner", 122, Rarity.RARE, mage.cards.s.StadiumHeadliner.class));
cards.add(new SetCardInfo("Static Snare", 26, Rarity.UNCOMMON, mage.cards.s.StaticSnare.class));
cards.add(new SetCardInfo("Stormbeacon Blade", 27, Rarity.UNCOMMON, mage.cards.s.StormbeaconBlade.class));
cards.add(new SetCardInfo("Stormplain Detainment", 28, Rarity.COMMON, mage.cards.s.StormplainDetainment.class));
cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class));
cards.add(new SetCardInfo("Sultai Devotee", 160, Rarity.COMMON, mage.cards.s.SultaiDevotee.class));
@ -175,6 +187,7 @@ public final class TarkirDragonstorm extends ExpansionSet {
cards.add(new SetCardInfo("Temur Devotee", 61, Rarity.COMMON, mage.cards.t.TemurDevotee.class));
cards.add(new SetCardInfo("Temur Monument", 248, Rarity.UNCOMMON, mage.cards.t.TemurMonument.class));
cards.add(new SetCardInfo("Temur Tawnyback", 229, Rarity.COMMON, mage.cards.t.TemurTawnyback.class));
cards.add(new SetCardInfo("The Sibsig Ceremony", 91, Rarity.RARE, mage.cards.t.TheSibsigCeremony.class));
cards.add(new SetCardInfo("Thornwood Falls", 269, Rarity.COMMON, mage.cards.t.ThornwoodFalls.class));
cards.add(new SetCardInfo("Tranquil Cove", 270, Rarity.COMMON, mage.cards.t.TranquilCove.class));
cards.add(new SetCardInfo("Twin Bolt", 128, Rarity.COMMON, mage.cards.t.TwinBolt.class));
@ -199,5 +212,6 @@ public final class TarkirDragonstorm extends ExpansionSet {
cards.add(new SetCardInfo("Worthy Cost", 99, Rarity.COMMON, mage.cards.w.WorthyCost.class));
cards.add(new SetCardInfo("Yathan Tombguard", 100, Rarity.UNCOMMON, mage.cards.y.YathanTombguard.class));
cards.add(new SetCardInfo("Zurgo's Vanguard", 133, Rarity.UNCOMMON, mage.cards.z.ZurgosVanguard.class));
cards.add(new SetCardInfo("Zurgo, Thunder's Decree", 237, Rarity.RARE, mage.cards.z.ZurgoThundersDecree.class));
}
}

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class AfflictTest extends CardTestPlayerBase {
private final String khenra = "Khenra Eternal";
@ -35,7 +37,7 @@ public class AfflictTest extends CardTestPlayerBase {
attack(1, playerA, khenra);
block(1, playerB, elves + ":0", khenra);
block(1, playerB, elves + ":1", khenra);
setChoice(playerA, "X=1"); // assign damage
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -0,0 +1,104 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author notgreat
*/
public class BandingTest extends CardTestPlayerBase {
@Test
public void BandingAttackSimple() {
addCard(Zone.BATTLEFIELD, playerA, "Squire"); // 1/2
addCard(Zone.BATTLEFIELD, playerA, "Benalish Infantry"); // Banding 1/3
addCard(Zone.BATTLEFIELD, playerA, "Eager Cadet"); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Naga Eternal"); // 3/2
attack(1, playerA, "Squire");
attack(1, playerA, "Benalish Infantry");
attack(1, playerA, "Eager Cadet");
setChoice(playerA, true);
setChoice(playerA, "Squire");
block(1, playerB, "Naga Eternal", "Squire");
setChoiceAmount(playerA, 1, 2); //1 to Squire, 2 to Infantry, attacking player chooses
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, 0);
assertDamageReceived(playerA, "Squire", 1);
assertDamageReceived(playerA, "Benalish Infantry", 2);
assertGraveyardCount(playerB, 1);
assertLife(playerB, 19); // Only Eager Cadet gets through
}
@Test
public void BandingBlockSimple() {
addCard(Zone.BATTLEFIELD, playerA, "Alpine Grizzly"); // 4/2
addCard(Zone.BATTLEFIELD, playerB, "Squire"); // 1/2
addCard(Zone.BATTLEFIELD, playerB, "Sanctuary Cat"); // 1/2
addCard(Zone.BATTLEFIELD, playerB, "Benalish Infantry"); // Banding 1/3
attack(1, playerA, "Alpine Grizzly");
block(1, playerB, "Squire", "Alpine Grizzly");
block(1, playerB, "Sanctuary Cat", "Alpine Grizzly");
block(1, playerB, "Benalish Infantry", "Alpine Grizzly");
setChoiceAmount(playerB, 1, 1, 2); //1 to Squire, 1 to Cat, 2 to Infantry, defending player chooses
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, 1);
assertGraveyardCount(playerB, 0);
assertDamageReceived(playerB, "Squire", 1);
assertDamageReceived(playerB, "Sanctuary Cat", 1);
assertDamageReceived(playerB, "Benalish Infantry", 2);
assertLife(playerB, 20);
}
@Test
public void DoubleBanding() {
addCard(Zone.BATTLEFIELD, playerA, "Benalish Infantry"); // Banding 1/3
addCard(Zone.BATTLEFIELD, playerA, "Fortress Crab"); // 1/6
addCard(Zone.BATTLEFIELD, playerA, "Eager Cadet"); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Catacomb Slug"); // 2/6
addCard(Zone.BATTLEFIELD, playerB, "War Elephant"); // Banding 2/2 Trample
attack(1, playerA, "Benalish Infantry");
attack(1, playerA, "Fortress Crab");
attack(1, playerA, "Eager Cadet");
setChoice(playerA, true);
setChoice(playerA, "Fortress Crab");
block(1, playerB, "Catacomb Slug", "Benalish Infantry");
block(1, playerB, "War Elephant", "Benalish Infantry");
setChoiceAmount(playerB, 0, 1); // Damage from Benalish Infantry
setChoiceAmount(playerB, 0, 1); // Damage from Fortress Crab
setChoiceAmount(playerA, 1, 1); // Damage from War Elephant
setChoiceAmount(playerA, 0, 2); // Damage from Catacomb Slug
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, 0);
assertDamageReceived(playerA, "Benalish Infantry", 1);
assertDamageReceived(playerA, "Fortress Crab", 3);
assertGraveyardCount(playerB, "War Elephant", 1);
assertDamageReceived(playerB, "Catacomb Slug", 0);
assertLife(playerB, 19); // Only Eager Cadet gets through
}
}

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author noxx
*/
@ -21,6 +23,7 @@ public class BushidoTest extends CardTestPlayerBase {
attack(2, playerB, "Isao, Enlightened Bushi");
block(2, playerA, "Elite Vanguard", "Isao, Enlightened Bushi");
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_COMBAT);
execute();
@ -39,6 +42,7 @@ public class BushidoTest extends CardTestPlayerBase {
attack(2, playerB, "Elite Vanguard");
block(2, playerA, "Isao, Enlightened Bushi", "Elite Vanguard");
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_COMBAT);
execute();
@ -58,9 +62,9 @@ public class BushidoTest extends CardTestPlayerBase {
attack(2, playerB, "Isao, Enlightened Bushi");
block(2, playerA, "Llanowar Elves", "Isao, Enlightened Bushi");
block(2, playerA, "Elvish Mystic", "Isao, Enlightened Bushi");
setChoice(playerB, "X=1"); // assign damage
setChoice(playerB, "X=1"); // assign damage
setChoice(playerB, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_COMBAT);
execute();

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author TheElk801
*/
@ -158,15 +160,7 @@ public class ExcessDamageTest extends CardTestPlayerBase {
block(2, playerA, bear, myrSuperion);
block(2, playerA, envoy, myrSuperion);
block(2, playerA, bondedConstruct, myrSuperion);
//Assign this much damage to the first blocking creature
setChoice(playerB, "X=2");
//Assign this much damage to the second blocking creature
setChoice(playerB, "X=1");
//Assign this much damage to the third blocking creature
setChoice(playerB, "X=1");
setChoice(playerB, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_TURN);

View file

@ -15,6 +15,8 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author JayDi85
*/
@ -247,9 +249,10 @@ public class TheRingEmblemTest extends CardTestPlayerBase {
attack(3, playerA, "Ashiok's Skulker");
block(3, playerB, "Alabaster Kirin", "Ashiok's Skulker");
block(3, playerB, "Alaborn Trooper", "Ashiok's Skulker");
setChoice(playerA, "Whenever your Ring-bearer becomes blocked"); // 2x triggers from two blockers
setChoice(playerA, "At end of combat, that permanent"); // 2x triggers from two blockers
setChoice(playerA, "Mountain"); // draw/discard on attack trigger
setChoice(playerA, "Whenever your Ring-bearer becomes blocked"); // 2x triggers from two blockers
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setChoice(playerA, "At end of combat, that permanent"); // 2x triggers from two blockers
checkPermanentCount("after attack on 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerA, "Ashiok's Skulker", 1);
checkPermanentCount("after attack on 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerB, "Academy Manufactor", 0);
checkPermanentCount("after attack on 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerB, "Alabaster Kirin", 0);

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
@Test
@ -67,7 +69,7 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
attack(3, playerA, "Magma Pummeler", playerB);
block(3, playerB, "Memnite", "Magma Pummeler");
block(3, playerB, "Goblin Piker", "Magma Pummeler");
setChoice(playerA, "X=5"); // damage for Pummeler, does not really matter for this test.
setChoice(playerA, CHOICE_SKIP); // Assign default damage
addTarget(playerA, playerB); // For the one trigger
setStopAt(3, PhaseStep.END_TURN);
@ -117,7 +119,7 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
attack(3, playerA, "Magma Pummeler", playerB);
block(3, playerB, "Centaur Courser", "Magma Pummeler");
block(3, playerB, "Air Elemental", "Magma Pummeler");
setChoice(playerA, "X=5"); // damage for Pummeler, does not really matter for this test.
setChoice(playerA, CHOICE_SKIP); // Assign default damage
addTarget(playerA, playerB); // For the one trigger
setStopAt(3, PhaseStep.END_TURN);
@ -148,7 +150,7 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
attack(1, playerA, "Undergrowth Champion", playerB);
block(1, playerB, "Grizzly Bears", "Undergrowth Champion");
block(1, playerB, "Elite Vanguard", "Undergrowth Champion");
setChoice(playerA, "X=2"); // damage attribution
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_COMBAT);
execute();

View file

@ -6,6 +6,8 @@ import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class RiteOfPassageTest extends CardTestPlayerBase {
@ -38,12 +40,7 @@ public class RiteOfPassageTest extends CardTestPlayerBase {
attack(1, playerA, "Watchwolf", playerB);
block(1, playerB, "Memnite", "Watchwolf");
block(1, playerB, "Agent of Stromgald", "Watchwolf");
// Assign this much damage to Memnite
setChoice(playerA, "X=1");
// Assign this much damage to Agent of Stromgald
setChoice(playerA, "X=1");
setChoice(playerA, CHOICE_SKIP);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author Susucr
*/
@ -33,7 +35,9 @@ public class BindingAgonyTest extends CardTestPlayerBase {
attack(1, playerA, "Grizzly Bears");
block(1, playerB, "Centaur Courser", "Grizzly Bears");
block(1, playerB, "Memnite", "Grizzly Bears");
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -53,6 +57,7 @@ public class BindingAgonyTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, agony, "Grizzly Bears", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

View file

@ -6,6 +6,8 @@ import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author Susucr
*/
@ -32,6 +34,7 @@ public class BloatflySwarmTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, swarm, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", swarm);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
@ -55,7 +58,9 @@ public class BloatflySwarmTest extends CardTestPlayerBase {
attack(1, playerA, swarm);
block(1, playerB, "Brimstone Dragon", swarm);
block(1, playerB, "Giant Spider", swarm);
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -79,8 +84,9 @@ public class BloatflySwarmTest extends CardTestPlayerBase {
attack(1, playerA, swarm);
block(1, playerB, "Wind Drake", swarm);
block(1, playerB, "Giant Spider", swarm);
setChoice(playerA, "X=5"); // damage attribution
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();

View file

@ -48,7 +48,7 @@ public class WallOfEssenceTest extends CardTestPlayerBase {
block(1, playerB, "Memnite", "Grizzly Bears");
block(1, playerB, wall, "Grizzly Bears");
setChoice(playerA, "X=2"); // 2 damage on Memnite, no damage to Wall
setChoiceAmount(playerA, 2, 0); // 2 damage on Memnite, no damage to Wall
setStopAt(1, PhaseStep.END_TURN);
execute();

View file

@ -6,6 +6,8 @@ import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author Susucr
*/
@ -31,9 +33,7 @@ public class PhantomWurmTest extends CardTestPlayerBase {
attack(1, playerA, wurm, playerB);
block(1, playerB, "Memnite", wurm);
block(1, playerB, "Eager Cadet", wurm);
setChoice(playerA, "X=1"); // damage assignment
setChoice(playerA, "X=3"); // damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -115,9 +115,7 @@ public class PhantomWurmTest extends CardTestPlayerBase {
attack(1, playerA, wurm, playerB);
block(1, playerB, "Memnite", wurm);
block(1, playerB, "Goblin Striker", wurm);
setChoice(playerA, "X=1"); // damage assignment
setChoice(playerA, "X=3"); // damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -139,9 +137,7 @@ public class PhantomWurmTest extends CardTestPlayerBase {
attack(1, playerA, wurm, playerB);
block(1, playerB, "Boros Recruit", wurm);
block(1, playerB, "Goblin Striker", wurm);
setChoice(playerA, "X=1"); // damage assignment
setChoice(playerA, "X=3"); // damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_TURN);
execute();

View file

@ -57,7 +57,7 @@ public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
private void assertDefaultValuesUnconstrained(String need, int count, int min, int max) {
List<MultiAmountMessage> constraints = getUnconstrainedConstraints(count);
List<Integer> defaultValues = MultiAmountType.prepareDefaltValues(constraints, min, max);
List<Integer> defaultValues = MultiAmountType.prepareDefaultValues(constraints, min, max);
String current = defaultValues
.stream()
.map(String::valueOf)
@ -122,7 +122,7 @@ public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
getUnconstrainedConstraints(4));
// good values are checking in test_DefaultValues, it's an additional
List<Integer> list = MultiAmountType.prepareDefaltValues(constraints.get(3), 0, 0);
List<Integer> list = MultiAmountType.prepareDefaultValues(constraints.get(3), 0, 0);
// count (0, 0, 0)
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(0), 0, 0));

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestCommander4Players;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
*
* @author jimga150
@ -32,12 +34,7 @@ public class DonnaNobleTests extends CardTestCommander4Players {
attack(5, playerA, "Impervious Greatwurm", playerB);
block(5, playerB, "Memnite", "Impervious Greatwurm");
block(5, playerB, "Expedition Envoy", "Impervious Greatwurm");
//Assign this much damage to the first blocking creature
setChoice(playerA, "X=1");
//Assign this much damage to the second blocking creature
setChoice(playerA, "X=1");
setChoice(playerA, CHOICE_SKIP); // Assign default damage
//Target this player with Donna Noble
addTarget(playerA, playerB);

View file

@ -6,6 +6,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestCommander4Players;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
*
* @author jimga150
@ -25,9 +27,7 @@ public class DamagedBatchTests extends CardTestCommander4Players {
attack(1, playerA, "Donna Noble", playerB);
block(1, playerB, "Memnite", "Donna Noble");
block(1, playerB, "Expedition Envoy", "Donna Noble");
//Assign this much damage to the first blocking creature
setChoice(playerA, "X=1");
setChoice(playerA, CHOICE_SKIP); // Assign default damage
//Target this player with Donna Noble
addTarget(playerA, playerB);

View file

@ -11,6 +11,7 @@ import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* Test restrictions for choosing attackers and blockers.
@ -753,9 +754,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
setChoice(playerA, CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
@ -783,7 +784,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
// ai must choose all blockers anyway
attack(1, playerA, "Sonorous Howlbonder");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
setChoiceAmount(playerA, 1, 1, 0); // assign damage to blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
@ -813,9 +814,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
setChoice(playerA, CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
@ -845,7 +846,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
// ai must choose all blockers
attack(1, playerA, "Sonorous Howlbonder");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
setChoiceAmount(playerA, 1, 1, 0, 0, 0); // assign damage to blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
@ -879,7 +880,6 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("one possible blocker", 1, playerB, "Memnite");
@ -998,8 +998,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
setChoiceAmount(playerA, 1); // assign damage to 1 of 2 blocking memnites
setChoiceAmount(playerA, 1); // assign damage to 2 of 2 blocking memnites
setChoiceAmount(playerA, 1, 1); // assign damage to blocking memnites
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite");

View file

@ -197,4 +197,35 @@ public class DamageDistributionTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 5);
}
@Test
public void test2x2Block() {
addCard(Zone.BATTLEFIELD, playerA, "Catacomb Slug"); // 2/6
addCard(Zone.BATTLEFIELD, playerA, "Catacomb Crocodile"); // 3/7
addCard(Zone.BATTLEFIELD, playerB, "Brave the Sands"); //can block 2
addCard(Zone.BATTLEFIELD, playerB, "Marsh Hulk"); // 4/6
addCard(Zone.BATTLEFIELD, playerB, "Fortress Crab"); // 1/6
attack(1, playerA, "Catacomb Slug");
attack(1, playerA, "Catacomb Crocodile");
block(1, playerB, "Fortress Crab", "Catacomb Slug");
block(1, playerB, "Fortress Crab", "Catacomb Crocodile");
block(1, playerB, "Marsh Hulk", "Catacomb Slug");
block(1, playerB, "Marsh Hulk", "Catacomb Crocodile");
setChoiceAmount(playerA, 1, 1); // Catacomb Slug
setChoiceAmount(playerA, 1, 2); // Catacomb Crocodile
setChoiceAmount(playerB, 1, 0); // Fortress Crab
setChoiceAmount(playerB, 2, 2); // Marsh Hulk
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertDamageReceived(playerA, "Catacomb Slug", 3);
assertDamageReceived(playerA, "Catacomb Crocodile", 2);
assertDamageReceived(playerB, "Fortress Crab", 2);
assertDamageReceived(playerB, "Marsh Hulk", 3);
}
}

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class LifelinkInCombatTest extends CardTestPlayerBase {
@Test
public void testOneBlockerTrample() {
@ -43,8 +45,7 @@ public class LifelinkInCombatTest extends CardTestPlayerBase {
attack(1, playerA, "Brion Stoutarm");
block(1, playerB, "Boros Recruit", "Brion Stoutarm");
block(1, playerB, "Suntail Hawk", "Brion Stoutarm");
setChoice(playerA, "X=1"); // Damage assignment
setChoice(playerA, "X=1"); // Damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
addTarget(playerA, "Brion Stoutarm");
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -2975,11 +2975,12 @@ public class TestPlayer implements Player {
assertAliasSupportInChoices(false);
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, totalMin, totalMax);
List<Integer> defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0) {
return defaultList;
}
List<Integer> answer = new ArrayList<>(defaultList);
if (!choices.isEmpty()) {
// must fill all possible choices or skip it
@ -4477,19 +4478,6 @@ public class TestPlayer implements Player {
return computerPlayer.playMana(ability, unpaid, promptText, game);
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attacker, Game game
) {
return computerPlayer.chooseAttackerOrder(attacker, game);
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup,
List<UUID> blockerOrder, Game game
) {
return computerPlayer.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
@Override
public void sideboard(Match match, Deck deck
) {

View file

@ -16,24 +16,30 @@ import mage.util.CardUtil;
public class PayMoreToCastAsThoughtItHadFlashAbility extends SpellAbility {
private final Cost costsToAdd;
private final String rule;
public PayMoreToCastAsThoughtItHadFlashAbility(Card card, Cost costsToAdd) {
this(card, costsToAdd, null);
}
public PayMoreToCastAsThoughtItHadFlashAbility(Card card, Cost costsToAdd, String rule) {
super(card.getSpellAbility().getManaCosts().copy(), card.getName(), Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
this.costsToAdd = costsToAdd;
this.rule = rule;
this.timing = TimingRule.INSTANT;
this.setRuleAtTheTop(true);
if(costsToAdd instanceof ManaCosts<?>) {
if (costsToAdd instanceof ManaCosts<?>) {
ManaCosts<ManaCost> manaCosts = (ManaCosts<ManaCost>) costsToAdd;
CardUtil.increaseCost(this, manaCosts);
}
else {
} else {
this.addCost(costsToAdd);
}
}
protected PayMoreToCastAsThoughtItHadFlashAbility(final PayMoreToCastAsThoughtItHadFlashAbility ability) {
super(ability);
this.rule = ability.rule;
this.costsToAdd = ability.costsToAdd;
}
@ -46,8 +52,12 @@ public class PayMoreToCastAsThoughtItHadFlashAbility extends SpellAbility {
public String getRule(boolean all) {
return getRule();
}
@Override
public String getRule() {
if (rule != null && !rule.isEmpty()) {
return rule;
}
if (costsToAdd instanceof ManaCosts) {
return "You may cast {this} as though it had flash if you pay " + costsToAdd.getText() + " more to cast it. <i>(You may cast it any time you could cast an instant.)</i>";
} else {
@ -55,4 +65,4 @@ public class PayMoreToCastAsThoughtItHadFlashAbility extends SpellAbility {
}
}
}
}

View file

@ -8,23 +8,24 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public enum MultiAmountType {
public class MultiAmountType {
MANA("Add mana", "Distribute mana among colors"),
DAMAGE("Assign damage", "Assign damage among targets"),
P1P1("Add +1/+1 counters", "Distribute +1/+1 counters among creatures"),
COUNTERS("Choose counters", "Move counters"),
CHEAT_LANDS("Choose lands", "Add lands to your battlefield", true);
public static final MultiAmountType MANA = new MultiAmountType("Add mana", "Distribute mana among colors");
public static final MultiAmountType DAMAGE = new MultiAmountType("Assign damage", "Assign damage among targets");
public static final MultiAmountType P1P1 = new MultiAmountType("Add +1/+1 counters", "Distribute +1/+1 counters among creatures");
public static final MultiAmountType COUNTERS = new MultiAmountType("Choose counters", "Move counters");
public static final MultiAmountType CHEAT_LANDS = new MultiAmountType("Choose lands", "Add lands to your battlefield", true);
private final String title;
private final String header;
private final boolean canCancel; // choice dialog will return null instead default values
MultiAmountType(String title, String header) {
public MultiAmountType(String title, String header) {
this(title, header, false);
}
MultiAmountType(String title, String header, boolean canCancel) {
public MultiAmountType(String title, String header, boolean canCancel) {
this.title = title;
this.header = header;
this.canCancel = canCancel;
@ -42,7 +43,7 @@ public enum MultiAmountType {
return canCancel;
}
public static List<Integer> prepareDefaltValues(List<MultiAmountMessage> constraints, int min, int max) {
public static List<Integer> prepareDefaultValues(List<MultiAmountMessage> constraints, int min, int max) {
// default values must be assigned from first to last by minimum values
List<Integer> res = constraints.stream().map(m -> m.defaultValue > Integer.MIN_VALUE ? m.defaultValue : Math.min(0, max))
.collect(Collectors.toList());
@ -50,7 +51,7 @@ public enum MultiAmountType {
return res;
}
int total = res.stream().mapToInt(x -> x).sum();;
int total = res.stream().mapToInt(x -> x).sum();
// Fill values until we reach the overall minimum. Do this by filling values up until either their max or however much is leftover, starting with the first option.
if (min > 0 && total < min) {
@ -174,7 +175,7 @@ public enum MultiAmountType {
// data check
if (returnDefaultOnError && !isGoodValues(res, constraints, min, max)) {
// on broken data - return default
return prepareDefaltValues(constraints, min, max);
return prepareDefaultValues(constraints, min, max);
}
return res;

View file

@ -60,7 +60,7 @@ public class Combat implements Serializable, Copyable<Combat> {
protected List<CombatGroup> groups = new ArrayList<>();
protected List<CombatGroup> formerGroups = new ArrayList<>();
protected Map<UUID, CombatGroup> blockingGroups = new HashMap<>();
protected Map<UUID, CombatGroup> blockingGroups = new LinkedHashMap<>();
// all possible defenders (players, planeswalkers or battle)
protected Set<UUID> defenders = new HashSet<>();
// how many creatures attack defending player
@ -200,7 +200,7 @@ public class Combat implements Serializable, Copyable<Combat> {
StringBuilder sb = new StringBuilder();
sb.append(attackingPlayerId).append(defenders);
for (CombatGroup group : groups) {
sb.append(group.defenderId).append(group.attackers).append(group.attackerOrder).append(group.blockers).append(group.blockerOrder);
sb.append(group.defenderId).append(group.attackers).append(group.blockers);
}
return sb.toString();
}
@ -785,7 +785,7 @@ public class Combat implements Serializable, Copyable<Combat> {
if (attackerExists) {
if (!group.getBlockers().isEmpty()) {
sb.append("blocked by ");
for (UUID blockingCreatureId : group.getBlockerOrder()) {
for (UUID blockingCreatureId : group.getBlockers()) {
Permanent blockingCreature = game.getPermanent(blockingCreatureId);
if (blockingCreature != null) {
sb.append(blockingCreature.getLogName()).append(" (");
@ -1799,24 +1799,11 @@ public class Combat implements Serializable, Copyable<Combat> {
return playerDefenders;
}
public void damageAssignmentOrder(Game game) {
for (CombatGroup group : groups) {
group.pickBlockerOrder(attackingPlayerId, game);
}
for (Map.Entry<UUID, CombatGroup> blockingGroup : blockingGroups.entrySet()) {
Permanent blocker = game.getPermanent(blockingGroup.getKey());
if (blocker != null) {
blockingGroup.getValue().pickAttackerOrder(blocker.getControllerId(), game);
}
}
}
@SuppressWarnings("deprecation")
public void removeAttacker(UUID attackerId, Game game) {
for (CombatGroup group : groups) {
if (group.attackers.contains(attackerId)) {
group.attackers.remove(attackerId);
group.attackerOrder.remove(attackerId);
for (Set<UUID> attackingCreatures : numberCreaturesDefenderAttackedBy.values()) {
attackingCreatures.remove(attackerId);
}
@ -1869,7 +1856,6 @@ public class Combat implements Serializable, Copyable<Combat> {
}
for (CombatGroup group : groupsToCheck) {
group.blockers.remove(blockerId);
group.blockerOrder.remove(blockerId);
if (group.blockers.isEmpty()) {
group.blocked = false;
}
@ -1885,11 +1871,9 @@ public class Combat implements Serializable, Copyable<Combat> {
if (blockGroup.blockers.contains(blockerId)) {
for (UUID attackerId : group.getAttackers()) {
blockGroup.attackers.remove(attackerId);
blockGroup.attackerOrder.remove(attackerId);
}
if (creature.getBlocking() == 0) {
blockGroup.blockers.remove(blockerId);
blockGroup.attackerOrder.clear();
}
}
if (blockGroup.blockers.isEmpty()) {
@ -1914,7 +1898,6 @@ public class Combat implements Serializable, Copyable<Combat> {
for (CombatGroup group : groups) {
if (group.blockers.contains(blockerId)) {
group.blockers.remove(blockerId);
group.blockerOrder.remove(blockerId);
if (group.blockers.isEmpty()) {
group.blocked = false;
}
@ -1924,7 +1907,6 @@ public class Combat implements Serializable, Copyable<Combat> {
for (CombatGroup group : getBlockingGroups()) {
if (group.blockers.contains(blockerId)) {
group.blockers.remove(blockerId);
group.attackerOrder.clear();
}
if (group.blockers.isEmpty()) {
canRemove = true;

View file

@ -6,6 +6,7 @@ import mage.abilities.common.ControllerDivideCombatDamageAbility;
import mage.abilities.common.DamageAsThoughNotBlockedAbility;
import mage.abilities.keyword.*;
import mage.constants.AsThoughEffectType;
import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.filter.StaticFilters;
import mage.game.Game;
@ -15,6 +16,7 @@ import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.Copyable;
import mage.util.MultiAmountMessage;
import mage.watchers.common.FirstStrikeWatcher;
import java.io.Serializable;
@ -29,8 +31,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
protected List<UUID> attackers = new ArrayList<>();
protected List<UUID> formerAttackers = new ArrayList<>();
protected List<UUID> blockers = new ArrayList<>();
protected List<UUID> blockerOrder = new ArrayList<>();
protected List<UUID> attackerOrder = new ArrayList<>();
protected Map<UUID, UUID> players = new HashMap<>();
protected boolean blocked;
protected UUID defenderId; // planeswalker, player, or battle id, can be null after remove from combat (e.g. due damage)
@ -52,8 +52,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
this.attackers.addAll(group.attackers);
this.formerAttackers.addAll(group.formerAttackers);
this.blockers.addAll(group.blockers);
this.blockerOrder.addAll(group.blockerOrder);
this.attackerOrder.addAll(group.attackerOrder);
this.players.putAll(group.players);
this.blocked = group.blocked;
this.defenderId = group.defenderId;
@ -91,10 +89,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
return blockers;
}
public List<UUID> getBlockerOrder() {
return blockerOrder;
}
private static boolean hasFirstOrDoubleStrike(Permanent perm) {
return hasFirstStrike(perm) || hasDoubleStrike(perm);
}
@ -175,7 +169,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
if (attacker != null && !assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(attacker, attacker.getControllerId(), first, game, true)) {
if (blockers.isEmpty()) {
unblockedDamage(first, game);
return;
} else {
Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : attacker.getControllerId());
if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) &&
@ -186,11 +179,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
blocked = false;
unblockedDamage(first, game);
}
if (blockers.size() == 1) {
singleBlockerDamage(player, first, game);
} else {
multiBlockerDamage(player, first, game);
}
blockerDamage(player, first, game);
}
}
}
@ -206,9 +195,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
if (attackers.size() != 1) {
multiAttackerDamage(first, game);
// } else {
// singleAttackerDamage(first, game);
attackerDamage(first, game);
}
}
}
@ -269,51 +256,16 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
}
private void singleBlockerDamage(Player player, boolean first, Game game) {
Permanent blocker = game.getPermanent(blockers.get(0));
Permanent attacker = game.getPermanent(attackers.get(0));
if (blocker != null && attacker != null) {
int blockerDamage = getDamageValueFromPermanent(blocker, game); // must be set before attacker damage marking because of effects like Test of Faith
if (blocked && dealsDamageThisStep(attacker, first, game)) {
int damage = getDamageValueFromPermanent(attacker, game);
if (hasTrample(attacker)) {
int lethalDamage = getLethalDamage(blocker, attacker, game);
if (lethalDamage >= damage) {
blocker.markDamage(damage, attacker.getId(), null, game, true, true);
} else {
int damageAssigned = player.getAmount(lethalDamage, damage, "Assign damage to " + blocker.getName(), game);
blocker.markDamage(damageAssigned, attacker.getId(), null, game, true, true);
damage -= damageAssigned;
if (damage > 0) {
defenderDamage(attacker, damage, game, false);
}
}
} else {
blocker.markDamage(damage, attacker.getId(), null, game, true, true);
}
}
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
attacker.markDamage(blockerDamage, blocker.getId(), null, game, true, true);
}
}
}
}
}
private void multiBlockerDamage(Player player, boolean first, Game game) {
private void blockerDamage(Player player, boolean first, Game game) {
Permanent attacker = game.getPermanent(attackers.get(0));
if (attacker == null) {
return;
}
boolean oldRuleDamage = (Objects.equals(player.getId(), defendingPlayerId));
int damage = getDamageValueFromPermanent(attacker, game);
if (dealsDamageThisStep(attacker, first, game)) {
// must be set before attacker damage marking because of effects like Test of Faith
Map<UUID, Integer> blockerPower = new HashMap<>();
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
@ -322,42 +274,62 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
Map<UUID, Integer> assigned = new HashMap<>();
List<MultiAmountMessage> damageDivision = new ArrayList<>();
List<UUID> blockersCopy = new ArrayList<>(blockers);
if (blocked) {
boolean excessDamageToDefender = true;
for (UUID blockerId : new ArrayList<>(blockerOrder)) { // prevent ConcurrentModificationException
int remainingDamage = damage;
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (blocker != null) {
int lethalDamage = getLethalDamage(blocker, attacker, game);
if (lethalDamage >= damage) {
if (!oldRuleDamage) {
assigned.put(blockerId, damage);
damage = 0;
break;
} else if (damage == 0) {
break;
}
}
int damageAssigned = 0;
if (!oldRuleDamage) {
damageAssigned = player.getAmount(lethalDamage, damage, "Assign damage to " + blocker.getName(), game);
} else {
damageAssigned = player.getAmount(0, damage, "Assign damage to " + blocker.getName(), game);
if (damageAssigned < lethalDamage) {
excessDamageToDefender = false; // all blockers need to have lethal damage assigned before it can trample over to the defender
}
}
assigned.put(blockerId, damageAssigned);
damage -= damageAssigned;
int defaultDamage = Math.min(remainingDamage, blocker.getLethalDamage(attacker.getId(), game));
remainingDamage -= defaultDamage;
String message = String.format("%s, P/T: %d/%d",
blocker.getLogName(),
blocker.getPower().getValue(),
blocker.getToughness().getValue());
damageDivision.add(new MultiAmountMessage(message, 0, damage, defaultDamage));
}
}
if (damage > 0 && hasTrample(attacker) && excessDamageToDefender) {
defenderDamage(attacker, damage, game, false);
} else if (!blockerOrder.isEmpty()) {
// Assign the damage left to first blocker
assigned.put(blockerOrder.get(0), assigned.get(blockerOrder.get(0)) == null ? 0 : assigned.get(blockerOrder.get(0)) + damage);
List<Integer> amounts;
if (hasTrample(attacker)){
if (remainingDamage > 0 || damageDivision.size() > 1) {
MultiAmountType dialogue = new MultiAmountType("Assign combat damage (with trample)",
String.format("Assign combat damage among creatures blocking %s, P/T: %d/%d (Unassigned damage tramples through)",
attacker.getLogName(), attacker.getPower().getValue(), attacker.getToughness().getValue()));
amounts = player.getMultiAmountWithIndividualConstraints(Outcome.Damage, damageDivision, damage - remainingDamage, damage, dialogue, game);
} else {
amounts = new ArrayList<>();
if (damageDivision.size() == 1) { // Assign all damage to one blocker
amounts.add(damage);
}
}
int trampleDamage = damage - (amounts.stream().mapToInt(x -> x).sum());
if (trampleDamage > 0) {
defenderDamage(attacker, trampleDamage, game, false);
}
} else {
if (remainingDamage > 0){
damageDivision.get(0).defaultValue += remainingDamage;
}
if (damageDivision.size() > 1) {
MultiAmountType dialogue = new MultiAmountType("Assign combat damage",
String.format("Assign combat damage among creatures blocking %s, P/T: %d/%d",
attacker.getLogName(), attacker.getPower().getValue(), attacker.getToughness().getValue()));
amounts = player.getMultiAmountWithIndividualConstraints(Outcome.Damage, damageDivision, damage, damage, dialogue, game);
} else {
amounts = new LinkedList<>();
if (damageDivision.size() == 1) { // Assign all damage to one blocker
amounts.add(damage);
}
}
}
if (!damageDivision.isEmpty()){
for (int i=0; i<blockersCopy.size(); i++) {
assigned.put(blockersCopy.get(i), amounts.get(i));
}
}
}
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Integer power = blockerPower.get(blockerId);
if (power != null) {
// might be missing canDamage condition?
@ -374,7 +346,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
} else {
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
@ -395,7 +367,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
if (dealsDamageThisStep(attacker, first, game)) {
// must be set before attacker damage marking because of effects like Test of Faith
Map<UUID, Integer> blockerPower = new HashMap<>();
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
@ -422,7 +394,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
if (isAttacking) {
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Integer power = blockerPower.get(blockerId);
if (power != null) {
// might be missing canDamage condition?
@ -439,7 +411,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
} else {
if (isAttacking) {
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
@ -473,81 +445,62 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
/**
* Damages attacking creatures by a creature that blocked several ones
* Damages only attackers as blocker was damage in
* {@link #singleBlockerDamage}.
* {@link #blockerDamage}.
* <p>
* Handles abilities like "{this} an block any number of creatures.".
* <p>
* Blocker damage for blockers blocking single creatures is handled in the
* single/multi blocker methods, so this shouldn't be used anymore.
*
* @param first
* @param game
* @deprecated
*/
@Deprecated
private void singleAttackerDamage(boolean first, Game game) {
Permanent blocker = game.getPermanent(blockers.get(0));
Permanent attacker = game.getPermanent(attackers.get(0));
if (blocker != null && attacker != null) {
if (dealsDamageThisStep(blocker, first, game)) {
int damage = getDamageValueFromPermanent(blocker, game);
attacker.markDamage(damage, blocker.getId(), null, game, true, true);
}
}
}
/**
* Damages attacking creatures by a creature that blocked several ones
* Damages only attackers as blocker was damage in either
* {@link #singleBlockerDamage} or {@link #multiBlockerDamage}.
* <p>
* Handles abilities like "{this} an block any number of creatures.".
* Handles abilities like "{this} can block any number of creatures.".
*
* @param first
* @param game
*/
private void multiAttackerDamage(boolean first, Game game) {
private void attackerDamage(boolean first, Game game) {
Permanent blocker = game.getPermanent(blockers.get(0));
if (blocker == null) {
return;
}
boolean oldRuleDamage = attackerAssignsCombatDamage(game); // handles banding
Player player = game.getPlayer(oldRuleDamage ? game.getCombat().getAttackingPlayerId() : blocker.getControllerId());
//Handle Banding
Player player = game.getPlayer(attackerAssignsCombatDamage(game) ? game.getCombat().getAttackingPlayerId() : blocker.getControllerId());
int damage = getDamageValueFromPermanent(blocker, game);
if (dealsDamageThisStep(blocker, first, game)) {
Map<UUID, Integer> assigned = new HashMap<>();
for (UUID attackerId : attackerOrder) {
List<MultiAmountMessage> damageDivision = new ArrayList<>();
List<UUID> attackersCopy = new ArrayList<>(attackers);
int remainingDamage = damage;
for (UUID attackerId : attackers) {
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null) {
int lethalDamage = getLethalDamage(attacker, blocker, game);
if (lethalDamage >= damage) {
if (!oldRuleDamage) {
assigned.put(attackerId, damage);
damage = 0;
break;
} else if (damage == 0) {
break;
}
}
int damageAssigned = 0;
if (!oldRuleDamage) {
damageAssigned = player.getAmount(lethalDamage, damage, "Assign damage to " + attacker.getName(), game);
} else {
damageAssigned = player.getAmount(0, damage, "Assign damage to " + attacker.getName(), game);
}
assigned.put(attackerId, damageAssigned);
damage -= damageAssigned;
int defaultDamage = Math.min(remainingDamage, attacker.getLethalDamage(blocker.getId(), game));
remainingDamage -= defaultDamage;
String message = String.format("%s, P/T: %d/%d",
attacker.getLogName(),
attacker.getPower().getValue(),
attacker.getToughness().getValue());
damageDivision.add(new MultiAmountMessage(message, 0, damage, defaultDamage));
}
}
if (damage > 0) {
// Assign the damage left to first attacker
assigned.put(attackerOrder.get(0), assigned.get(attackerOrder.get(0)) + damage);
List<Integer> amounts;
if (remainingDamage > 0){
damageDivision.get(0).defaultValue += remainingDamage;
}
if (damageDivision.size() > 1) {
MultiAmountType dialogue = new MultiAmountType("Assign blocker combat damage",
String.format("Assign combat damage among creatures blocked by %s, P/T: %d/%d",
blocker.getLogName(), blocker.getPower().getValue(), blocker.getToughness().getValue()));
amounts = player.getMultiAmountWithIndividualConstraints(Outcome.Damage, damageDivision, damage, damage, dialogue, game);
} else {
amounts = new LinkedList<>();
amounts.add(damage);
}
if (!damageDivision.isEmpty()){
for (int i=0; i<attackersCopy.size(); i++) {
assigned.put(attackersCopy.get(i), amounts.get(i));
}
}
for (Map.Entry<UUID, Integer> entry : assigned.entrySet()) {
Permanent attacker = game.getPermanent(entry.getKey());
attacker.markDamage(entry.getValue(), blocker.getId(), null, game, true, true);
if (attacker != null) {
attacker.markDamage(entry.getValue(), blocker.getId(), null, game, true, true);
}
}
}
}
@ -632,74 +585,11 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
if (blockerId != null && blocker != null) {
blocker.setBlocking(blocker.getBlocking() + 1);
blockers.add(blockerId);
blockerOrder.add(blockerId);
this.blocked = true;
this.players.put(blockerId, playerId);
}
}
public void pickBlockerOrder(UUID playerId, Game game) {
if (blockers.isEmpty()) {
return;
}
Player player = game.getPlayer(playerId); // game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : playerId); // this was incorrect because defenderAssignsCombatDamage might be false by the time damage is dealt
List<UUID> blockerList = new ArrayList<>(blockers);
blockerOrder.clear();
while (player.canRespond()) {
if (blockerList.size() == 1) {
blockerOrder.add(blockerList.get(0));
break;
} else {
List<Permanent> blockerPerms = new ArrayList<>();
for (UUID blockerId : blockerList) {
blockerPerms.add(game.getPermanent(blockerId));
}
UUID blockerId = player.chooseBlockerOrder(blockerPerms, this, blockerOrder, game);
blockerOrder.add(blockerId);
blockerList.remove(blockerId);
}
}
if (!game.isSimulation() && blockerOrder.size() > 1) {
logDamageAssignmentOrder("Creatures blocking ", attackers, blockerOrder, game);
}
}
public void pickAttackerOrder(UUID playerId, Game game) {
Player player = game.getPlayer(playerId);
if (attackers.isEmpty() || player == null) {
return;
}
List<UUID> attackerList = new ArrayList<>(attackers);
List<UUID> newAttackerOrder = new ArrayList<>();
while (true) {
if (attackerList.size() == 1) {
newAttackerOrder.add(attackerList.get(0));
break;
} else {
List<Permanent> attackerPerms = new ArrayList<>();
for (UUID attackerId : attackerList) {
attackerPerms.add(game.getPermanent(attackerId));
}
UUID attackerId = player.chooseAttackerOrder(attackerPerms, game);
if (attackerId == null) {
break;
}
newAttackerOrder.add(attackerId);
attackerList.remove(attackerId);
}
}
if (attackerOrder.isEmpty() || newAttackerOrder.size() == attackerOrder.size()) {
attackerOrder.clear();
attackerOrder.addAll(newAttackerOrder);
if (!game.isSimulation() && attackerOrder.size() > 1) {
logDamageAssignmentOrder("Creatures blocked by ", blockers, attackerOrder, game);
}
} else {
game.informPlayers(player.getLogName() + " try to skip choose attacker order");
}
}
private void logDamageAssignmentOrder(String prefix, List<UUID> assignedFor, List<UUID> assignedOrder, Game game) {
StringBuilder sb = new StringBuilder(prefix);
boolean first = true;
@ -746,12 +636,9 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
formerAttackers.add(creatureId);
attackers.remove(creatureId);
result = true;
attackerOrder.remove(creatureId);
} else if (blockers.contains(creatureId)) {
blockers.remove(creatureId);
result = true;
//20100423 - 509.2a
blockerOrder.remove(creatureId);
}
return result;
}
@ -825,7 +712,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
game.getCombat().removeBlocker(blockerId, game);
}
blockers.clear();
blockerOrder.clear();
if (!game.isSimulation()) {
game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded.");
}
@ -844,7 +730,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
game.getCombat().removeBlocker(blockerId, game);
}
blockers.clear();
blockerOrder.clear();
if (!game.isSimulation()) {
game.informPlayers(new StringBuilder(attacker.getLogName())
.append(" can't be blocked by more than ").append(attacker.getMaxBlockedBy())

View file

@ -1,12 +1,12 @@
package mage.game.turn;
import java.util.UUID;
import mage.constants.PhaseStep;
import mage.game.Game;
import mage.game.events.GameEvent.EventType;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
@ -37,7 +37,6 @@ public class DeclareBlockersStep extends Step {
game.getCombat().selectBlockers(game);
if (!game.isPaused() && !game.executingRollback()) {
game.getCombat().acceptBlockers(game);
game.getCombat().damageAssignmentOrder(game);
}
}
@ -46,7 +45,6 @@ public class DeclareBlockersStep extends Step {
super.resumeBeginStep(game, activePlayerId);
game.getCombat().resumeSelectBlockers(game);
game.getCombat().acceptBlockers(game);
game.getCombat().damageAssignmentOrder(game);
}
@Override

View file

@ -23,7 +23,6 @@ import mage.filter.FilterCard;
import mage.filter.FilterMana;
import mage.filter.FilterPermanent;
import mage.game.*;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.events.GameEvent;
import mage.game.match.Match;
@ -761,19 +760,6 @@ public interface Player extends MageItem, Copyable<Player> {
void selectBlockers(Ability source, Game game, UUID defendingPlayerId);
UUID chooseAttackerOrder(List<Permanent> attacker, Game game);
/**
* Choose the order in which blockers get damage assigned to
*
* @param blockers list of blockers where to choose the next one from
* @param combatGroup the concerning combat group
* @param blockerOrder the already set order of blockers
* @param game
* @return blocker next to add to the blocker order
*/
UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game);
int getAmount(int min, int max, String message, Game game);
/**

View file

@ -18,10 +18,8 @@ import mage.constants.Outcome;
import mage.constants.RangeOfInfluence;
import mage.filter.FilterMana;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.tournament.Tournament;
import mage.target.Target;
import mage.target.TargetAmount;
@ -190,16 +188,6 @@ public class StubPlayer extends PlayerImpl {
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attacker, Game game) {
return null;
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game) {
return null;
}
@Override
public int getAmount(int min, int max, String message, Game game) {
return 0;