forked from External/mage
Merge branch 'External-master'
All checks were successful
/ build_release (push) Successful in 25m3s
All checks were successful
/ build_release (push) Successful in 25m3s
This commit is contained in:
commit
2cd85af552
332 changed files with 22134 additions and 16115 deletions
|
|
@ -35,6 +35,7 @@ public enum MageIdentifier {
|
|||
CourtOfLocthwainWatcher("Without paying manacost"),
|
||||
LaraCroftTombRaiderWatcher,
|
||||
CoramTheUndertakerWatcher,
|
||||
ThundermanDragonWatcher,
|
||||
|
||||
// ----------------------------//
|
||||
// alternate casts //
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public class ObjectColor implements Serializable, Copyable<ObjectColor>, Compara
|
|||
public static final ObjectColor GREEN = new ObjectColor("G");
|
||||
|
||||
public static final ObjectColor COLORLESS = new ObjectColor();
|
||||
private static final List<ObjectColor>allColors= Arrays.asList(WHITE,BLUE,BLACK,RED,GREEN);
|
||||
private static final List<ObjectColor> allColors = Arrays.asList(WHITE, BLUE, BLACK, RED, GREEN);
|
||||
private boolean white;
|
||||
private boolean blue;
|
||||
private boolean black;
|
||||
|
|
|
|||
|
|
@ -549,6 +549,8 @@ public interface Ability extends Controllable, Serializable {
|
|||
*/
|
||||
Ability setCostAdjuster(CostAdjuster costAdjuster);
|
||||
|
||||
CostAdjuster getCostAdjuster();
|
||||
|
||||
/**
|
||||
* Prepare {X} settings for announce
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1761,6 +1761,11 @@ public abstract class AbilityImpl implements Ability {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CostAdjuster getCostAdjuster() {
|
||||
return costAdjuster;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustX(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
|
|
|
|||
|
|
@ -238,9 +238,12 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
@Override
|
||||
public String getRule(boolean all) {
|
||||
if (all) {
|
||||
return new StringBuilder(super.getRule(all)).append(name).toString();
|
||||
// show full rules, e.g. for hand
|
||||
return super.getRule(true) + this.name;
|
||||
} else {
|
||||
// hide spell ability, e.g. for permanent
|
||||
return super.getRule(false);
|
||||
}
|
||||
return super.getRule(false);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
|
||||
|
||||
package mage.abilities.abilityword;
|
||||
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.condition.common.CommanderInPlayCondition;
|
||||
import mage.abilities.condition.common.ControlYourCommanderCondition;
|
||||
import mage.abilities.decorator.ConditionalContinuousEffect;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.Effects;
|
||||
import mage.abilities.effects.common.continuous.BoostSourceEffect;
|
||||
import mage.constants.AbilityWord;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Zone;
|
||||
|
||||
|
|
@ -18,16 +15,19 @@ import mage.constants.Zone;
|
|||
|
||||
public class LieutenantAbility extends SimpleStaticAbility {
|
||||
|
||||
public LieutenantAbility(ContinuousEffect effect) {
|
||||
super(Zone.BATTLEFIELD, new ConditionalContinuousEffect(new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), CommanderInPlayCondition.instance, "<i>Lieutenant</i> — As long as you control your commander, {this} gets +2/+2"));
|
||||
this.addEffect(new ConditionalContinuousEffect(effect, CommanderInPlayCondition.instance, effect.getText(null)));
|
||||
public LieutenantAbility(ContinuousEffect effect, String text) {
|
||||
super(Zone.BATTLEFIELD, new ConditionalContinuousEffect(
|
||||
new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield),
|
||||
ControlYourCommanderCondition.instance,
|
||||
"as long as you control your commander, {this} gets +2/+2"
|
||||
));
|
||||
this.setAbilityWord(AbilityWord.LIEUTENANT);
|
||||
this.addLieutenantEffect(effect, text);
|
||||
}
|
||||
|
||||
public LieutenantAbility(Effects effects) {
|
||||
super(Zone.BATTLEFIELD, new ConditionalContinuousEffect(new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), CommanderInPlayCondition.instance, "<i>Lieutenant</i> — As long as you control your commander, {this} gets +2/+2"));
|
||||
for (Effect effect : effects) {
|
||||
this.addEffect(new ConditionalContinuousEffect((ContinuousEffect) effect, CommanderInPlayCondition.instance, effect.getText(null)));
|
||||
}
|
||||
public LieutenantAbility addLieutenantEffect(ContinuousEffect effect, String text) {
|
||||
this.addEffect(new ConditionalContinuousEffect(effect, ControlYourCommanderCondition.instance, text));
|
||||
return this;
|
||||
}
|
||||
|
||||
protected LieutenantAbility(final LieutenantAbility ability) {
|
||||
|
|
@ -38,4 +38,4 @@ public class LieutenantAbility extends SimpleStaticAbility {
|
|||
public LieutenantAbility copy() {
|
||||
return new LieutenantAbility(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,13 +70,10 @@ public class BecomesTargetAnyTriggeredAbility extends TriggeredAbilityImpl {
|
|||
if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
|
||||
return false;
|
||||
}
|
||||
switch (setTargetPointer) {
|
||||
case PERMANENT:
|
||||
this.getAllEffects().setTargetPointer(new FixedTarget(permanent.getId(), game));
|
||||
|
|
|
|||
|
|
@ -54,13 +54,10 @@ public class BecomesTargetAttachedTriggeredAbility extends TriggeredAbilityImpl
|
|||
if (enchantment == null || enchantment.getAttachedTo() == null || !event.getTargetId().equals(enchantment.getAttachedTo())) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
|
||||
return false;
|
||||
}
|
||||
switch (setTargetPointer) {
|
||||
case PLAYER:
|
||||
this.getAllEffects().setTargetPointer(new FixedTarget(targetingObject.getControllerId(), game));
|
||||
|
|
|
|||
|
|
@ -63,13 +63,10 @@ public class BecomesTargetControllerTriggeredAbility extends TriggeredAbilityImp
|
|||
return false;
|
||||
}
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
|
||||
return false;
|
||||
}
|
||||
switch (setTargetPointer) {
|
||||
case SPELL:
|
||||
this.getAllEffects().setTargetPointer(new FixedTarget(targetingObject.getId()));
|
||||
|
|
|
|||
|
|
@ -57,13 +57,10 @@ public class BecomesTargetSourceTriggeredAbility extends TriggeredAbilityImpl {
|
|||
if (!event.getTargetId().equals(getSourceId())) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
|
||||
return false;
|
||||
}
|
||||
switch (setTargetPointer) {
|
||||
case PLAYER:
|
||||
this.getAllEffects().setTargetPointer(new FixedTarget(targetingObject.getControllerId(), game));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import mage.MageIdentifier;
|
|||
import mage.MageObjectReference;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.Costs;
|
||||
import mage.abilities.costs.CostsImpl;
|
||||
import mage.abilities.effects.AsThoughEffectImpl;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.*;
|
||||
|
|
@ -28,7 +31,11 @@ import java.util.UUID;
|
|||
public class CastFromGraveyardOnceEachTurnAbility extends SimpleStaticAbility {
|
||||
|
||||
public CastFromGraveyardOnceEachTurnAbility(FilterCard filter) {
|
||||
super(new CastFromGraveyardOnceEffect(filter));
|
||||
this(filter, null);
|
||||
}
|
||||
|
||||
public CastFromGraveyardOnceEachTurnAbility(FilterCard filter, Cost additionalCost) {
|
||||
super(new CastFromGraveyardOnceEffect(filter, additionalCost));
|
||||
this.addWatcher(new CastFromGraveyardOnceWatcher());
|
||||
this.setIdentifier(MageIdentifier.CastFromGraveyardOnceWatcher);
|
||||
}
|
||||
|
|
@ -46,17 +53,21 @@ public class CastFromGraveyardOnceEachTurnAbility extends SimpleStaticAbility {
|
|||
class CastFromGraveyardOnceEffect extends AsThoughEffectImpl {
|
||||
|
||||
private final FilterCard filter;
|
||||
private final Cost additionalCost;
|
||||
|
||||
CastFromGraveyardOnceEffect(FilterCard filter) {
|
||||
CastFromGraveyardOnceEffect(FilterCard filter, Cost additionalCost) {
|
||||
super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
|
||||
this.filter = filter;
|
||||
this.staticText = "Once during each of your turns, you may cast " + filter.getMessage()
|
||||
+ (filter.getMessage().contains("your graveyard") ? "" : " from your graveyard");
|
||||
+ (filter.getMessage().contains("your graveyard") ? "" : " from your graveyard")
|
||||
+ (additionalCost == null ? "" : " by " + additionalCost.getText() + " in addition to paying its other costs.");
|
||||
this.additionalCost = additionalCost;
|
||||
}
|
||||
|
||||
private CastFromGraveyardOnceEffect(final CastFromGraveyardOnceEffect effect) {
|
||||
super(effect);
|
||||
this.filter = effect.filter;
|
||||
this.additionalCost = effect.additionalCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -97,7 +108,14 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl {
|
|||
}
|
||||
Set<MageIdentifier> allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game);
|
||||
if (allowedToBeCastNow.contains(MageIdentifier.Default)) {
|
||||
return filter.match(cardToCheck, playerId, source, game);
|
||||
boolean matched = filter.match(cardToCheck, playerId, source, game);
|
||||
if (matched && additionalCost != null) {
|
||||
Costs<Cost> costs = new CostsImpl<>();
|
||||
costs.add(additionalCost);
|
||||
controller.setCastSourceIdWithAlternateMana(objectId, spellAbility.getManaCosts(),
|
||||
costs, MageIdentifier.CastFromGraveyardOnceWatcher);
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class WhenTargetDiesDelayedTriggeredAbility extends DelayedTriggeredAbili
|
|||
|
||||
protected MageObjectReference mor;
|
||||
private final SetTargetPointer setTargetPointer;
|
||||
private final boolean onlyControlled;
|
||||
|
||||
public WhenTargetDiesDelayedTriggeredAbility(Effect effect) {
|
||||
this(effect, SetTargetPointer.NONE);
|
||||
|
|
@ -28,15 +29,21 @@ public class WhenTargetDiesDelayedTriggeredAbility extends DelayedTriggeredAbili
|
|||
}
|
||||
|
||||
public WhenTargetDiesDelayedTriggeredAbility(Effect effect, Duration duration, SetTargetPointer setTargetPointer) {
|
||||
this(effect, duration, setTargetPointer, false);
|
||||
}
|
||||
|
||||
public WhenTargetDiesDelayedTriggeredAbility(Effect effect, Duration duration, SetTargetPointer setTargetPointer, boolean onlyControlled) {
|
||||
super(effect, duration, true);
|
||||
this.setTargetPointer = setTargetPointer;
|
||||
setTriggerPhrase("When that creature dies" + (duration == Duration.EndOfTurn ? " this turn, " : ", "));
|
||||
this.onlyControlled = onlyControlled;
|
||||
setTriggerPhrase("When that creature dies" + (onlyControlled ? " under your control" : "") + (duration == Duration.EndOfTurn ? " this turn, " : ", "));
|
||||
}
|
||||
|
||||
protected WhenTargetDiesDelayedTriggeredAbility(final WhenTargetDiesDelayedTriggeredAbility ability) {
|
||||
super(ability);
|
||||
this.mor = ability.mor;
|
||||
this.setTargetPointer = ability.setTargetPointer;
|
||||
this.onlyControlled = ability.onlyControlled;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -62,7 +69,8 @@ public class WhenTargetDiesDelayedTriggeredAbility extends DelayedTriggeredAbili
|
|||
return false;
|
||||
}
|
||||
Permanent permanent = zEvent.getTarget();
|
||||
if (mor == null || !mor.refersTo(permanent, game)) {
|
||||
if (mor == null || !mor.refersTo(permanent, game)
|
||||
|| onlyControlled && !permanent.isControlledBy(getControllerId())) {
|
||||
return false;
|
||||
}
|
||||
switch (setTargetPointer) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package mage.abilities.common.delayed;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import mage.abilities.DelayedTriggeredAbility;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.constants.Duration;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
|
||||
/**
|
||||
* @author androosss
|
||||
*/
|
||||
public class WhenYouAttackDelayedTriggeredAbility extends DelayedTriggeredAbility {
|
||||
|
||||
public WhenYouAttackDelayedTriggeredAbility(Effect effect) {
|
||||
this(effect, Duration.EndOfTurn);
|
||||
}
|
||||
|
||||
public WhenYouAttackDelayedTriggeredAbility(Effect effect, Duration duration) {
|
||||
this(effect, duration, false);
|
||||
}
|
||||
|
||||
public WhenYouAttackDelayedTriggeredAbility(Effect effect, Duration duration, boolean triggerOnlyOnce) {
|
||||
super(effect, duration, triggerOnlyOnce);
|
||||
setTriggerPhrase((triggerOnlyOnce ? "When you next" : "Whenever you") + " attack"
|
||||
+ (duration == Duration.EndOfTurn ? " this turn, " : ", "));
|
||||
}
|
||||
|
||||
protected WhenYouAttackDelayedTriggeredAbility(final WhenYouAttackDelayedTriggeredAbility ability) {
|
||||
super(ability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WhenYouAttackDelayedTriggeredAbility copy() {
|
||||
return new WhenYouAttackDelayedTriggeredAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTrigger(GameEvent event, Game game) {
|
||||
UUID attackerId = game.getCombat().getAttackingPlayerId();
|
||||
return attackerId != null && attackerId.equals(getControllerId()) && !game.getCombat().getAttackers().isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package mage.abilities.condition.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.game.Game;
|
||||
|
||||
/**
|
||||
* Checks if the player has its commander in play and controls it
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public enum CommanderInPlayCondition implements Condition {
|
||||
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return ControlYourCommanderCondition.instance.apply(game, source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "As long as you control your commander";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ package mage.abilities.condition.common;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.constants.CommanderCardType;
|
||||
import mage.game.Controllable;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
|
|
@ -19,21 +19,18 @@ public enum ControlYourCommanderCondition implements Condition {
|
|||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return game.getPlayerList()
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
return player != null && game
|
||||
.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, true)
|
||||
.stream()
|
||||
.map(game::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.map(player -> game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, true)) // must search all card parts (example: mdf commander on battlefield)
|
||||
.flatMap(Collection::stream)
|
||||
.map(game::getPermanent)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(Permanent::isPhasedIn)
|
||||
.map(Permanent::getOwnerId)
|
||||
.anyMatch(source.getControllerId()::equals);
|
||||
.map(Controllable::getControllerId)
|
||||
.anyMatch(source::isControlledBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "If you control your commander";
|
||||
return "you control your commander";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
package mage.abilities.condition.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.game.Game;
|
||||
|
||||
/**
|
||||
* @author androosss
|
||||
*/
|
||||
public enum IsMainPhaseCondition implements Condition {
|
||||
|
||||
YOUR(true),
|
||||
ANY(false);
|
||||
|
||||
private final boolean yourMainPhaseOnly;
|
||||
|
||||
IsMainPhaseCondition(boolean yourMainPhaseOnly) {
|
||||
this.yourMainPhaseOnly = yourMainPhaseOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return game.getTurnPhaseType().isMain() &&
|
||||
(!yourMainPhaseOnly || game.getActivePlayerId().equals(source.getControllerId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "it's" + (yourMainPhaseOnly ? " your " : " ") + "main phase";
|
||||
}
|
||||
}
|
||||
|
|
@ -35,8 +35,7 @@ public class AddCombatAndMainPhaseEffect extends OneShotEffect {
|
|||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
// 15.07.2006 If it's somehow not a main phase when Fury of the Horde resolves, all it does is untap all creatures that attacked that turn. No new phases are created.
|
||||
if (game.getTurnPhaseType() == TurnPhase.PRECOMBAT_MAIN
|
||||
|| game.getTurnPhaseType() == TurnPhase.POSTCOMBAT_MAIN) {
|
||||
if (game.getTurnPhaseType().isMain()) {
|
||||
// we can't add two turn modes at once, will add additional post combat on delayed trigger resolution
|
||||
TurnMod combat = new TurnMod(source.getControllerId()).withExtraPhase(TurnPhase.COMBAT, TurnPhase.POSTCOMBAT_MAIN);
|
||||
game.getState().getTurnMods().add(combat);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import mage.game.permanent.Permanent;
|
|||
import mage.players.Player;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
|
|
@ -33,10 +35,19 @@ public class PutIntoLibraryNFromTopTargetEffect extends OneShotEffect {
|
|||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
|
||||
return player != null && permanent != null
|
||||
&& player.putCardOnTopXOfLibrary(permanent, game, source, position, true);
|
||||
boolean result = false;
|
||||
for (UUID permanentId : getTargetPointer().getTargets(game, source)) {
|
||||
Permanent permanent = game.getPermanent(permanentId);
|
||||
if (permanent == null) {
|
||||
continue;
|
||||
}
|
||||
Player player = game.getPlayer(permanent.getOwnerId());
|
||||
if (player == null) {
|
||||
continue;
|
||||
}
|
||||
result |= player.putCardOnTopXOfLibrary(permanent, game, source, position, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -15,12 +15,20 @@ import mage.util.CardUtil;
|
|||
*/
|
||||
public class TargetPlayerShufflesTargetCardsEffect extends OneShotEffect {
|
||||
|
||||
private final int targetPlayerIndex;
|
||||
|
||||
public TargetPlayerShufflesTargetCardsEffect() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public TargetPlayerShufflesTargetCardsEffect(int targetPlayerIndex) {
|
||||
super(Outcome.Neutral);
|
||||
this.targetPlayerIndex = targetPlayerIndex;
|
||||
}
|
||||
|
||||
private TargetPlayerShufflesTargetCardsEffect(final TargetPlayerShufflesTargetCardsEffect effect) {
|
||||
super(effect);
|
||||
this.targetPlayerIndex = effect.targetPlayerIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -30,8 +38,8 @@ public class TargetPlayerShufflesTargetCardsEffect extends OneShotEffect {
|
|||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player targetPlayer = game.getPlayer(source.getFirstTarget());
|
||||
Cards cards = new CardsImpl(source.getTargets().get(1).getTargets());
|
||||
Player targetPlayer = game.getPlayer(source.getTargets().get(targetPlayerIndex).getFirstTarget());
|
||||
Cards cards = new CardsImpl(source.getTargets().get(targetPlayerIndex + 1).getTargets());
|
||||
if (targetPlayer != null && !cards.isEmpty()) {
|
||||
return targetPlayer.shuffleCardsToLibrary(cards, game, source);
|
||||
}
|
||||
|
|
@ -44,7 +52,7 @@ public class TargetPlayerShufflesTargetCardsEffect extends OneShotEffect {
|
|||
return staticText;
|
||||
}
|
||||
String rule = "target player shuffles ";
|
||||
int targetNumber = mode.getTargets().get(1).getMaxNumberOfTargets();
|
||||
int targetNumber = mode.getTargets().get(targetPlayerIndex + 1).getMaxNumberOfTargets();
|
||||
if (targetNumber == Integer.MAX_VALUE) {
|
||||
rule += "any number of target cards";
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class AttackIfAbleTargetRandomOpponentSourceEffect extends OneShotEffect
|
|||
if (controller == null) {
|
||||
return false;
|
||||
}
|
||||
List<UUID> opponents = new ArrayList<>(game.getOpponents(controller.getId()));
|
||||
List<UUID> opponents = new ArrayList<>(game.getOpponents(controller.getId(), true));
|
||||
Player opponent = game.getPlayer(opponents.get(RandomUtil.nextInt(opponents.size())));
|
||||
if (opponent != null) {
|
||||
game.informPlayers(opponent.getLogName() + " was chosen at random.");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package mage.abilities.effects.common.cost;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.keyword.EquipAbility;
|
||||
import mage.constants.CostModificationType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Game;
|
||||
import mage.target.Target;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class ReduceCostEquipTargetSourceEffect extends CostModificationEffectImpl {
|
||||
|
||||
private final int amount;
|
||||
|
||||
public ReduceCostEquipTargetSourceEffect(int amount) {
|
||||
super(Duration.Custom, Outcome.Benefit, CostModificationType.REDUCE_COST);
|
||||
this.amount = amount;
|
||||
staticText = "equip abilities you activate that target {this} cost {" + amount + "} less to activate";
|
||||
}
|
||||
|
||||
private ReduceCostEquipTargetSourceEffect(final ReduceCostEquipTargetSourceEffect effect) {
|
||||
super(effect);
|
||||
this.amount = effect.amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source, Ability abilityToModify) {
|
||||
CardUtil.reduceCost(abilityToModify, amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(Ability abilityToModify, Ability source, Game game) {
|
||||
if (!(abilityToModify instanceof EquipAbility)
|
||||
|| !abilityToModify.isControlledBy(source.getControllerId())) {
|
||||
return false;
|
||||
}
|
||||
if (game != null && game.inCheckPlayableState()) {
|
||||
return !abilityToModify
|
||||
.getTargets()
|
||||
.isEmpty()
|
||||
&& abilityToModify
|
||||
.getTargets()
|
||||
.get(0)
|
||||
.canTarget(source.getSourceId(), abilityToModify, game);
|
||||
}
|
||||
return abilityToModify
|
||||
.getTargets()
|
||||
.stream()
|
||||
.map(Target::getTargets)
|
||||
.flatMap(Collection::stream)
|
||||
.anyMatch(source.getSourceId()::equals);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReduceCostEquipTargetSourceEffect copy() {
|
||||
return new ReduceCostEquipTargetSourceEffect(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -114,6 +114,7 @@ public abstract class CastFromGraveyardAbility extends SpellAbility {
|
|||
spellAbilityCopy.addCost(this.getCosts().copy());
|
||||
spellAbilityCopy.addCost(this.getManaCosts().copy());
|
||||
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
|
||||
spellAbilityCopy.setCostAdjuster(this.getCostAdjuster());
|
||||
spellAbilityToResolve = spellAbilityCopy;
|
||||
ContinuousEffect effect = new CastFromGraveyardReplacementEffect();
|
||||
effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId())));
|
||||
|
|
|
|||
|
|
@ -4,15 +4,12 @@ import mage.MageInt;
|
|||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.VariableCostImpl;
|
||||
import mage.abilities.costs.VariableCostType;
|
||||
import mage.abilities.costs.CostAdjuster;
|
||||
import mage.abilities.costs.common.TapTargetCost;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.*;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.SpellAbilityCastMode;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.filter.common.FilterControlledPermanent;
|
||||
import mage.filter.predicate.permanent.PermanentIdPredicate;
|
||||
|
|
@ -23,9 +20,6 @@ import mage.target.TargetPermanent;
|
|||
import mage.target.common.TargetControlledPermanent;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
|
|
@ -36,8 +30,7 @@ public class HarmonizeAbility extends CastFromGraveyardAbility {
|
|||
|
||||
public HarmonizeAbility(Card card, String manaString) {
|
||||
super(card, new ManaCostsImpl<>(manaString), SpellAbilityCastMode.HARMONIZE);
|
||||
this.addCost(new HarmonizeCost());
|
||||
this.addSubAbility(new SimpleStaticAbility(Zone.ALL, new HarmonizeCostReductionEffect()).setRuleVisible(false));
|
||||
this.setCostAdjuster(HarmonizeAbilityAdjuster.instance);
|
||||
}
|
||||
|
||||
private HarmonizeAbility(final HarmonizeAbility ability) {
|
||||
|
|
@ -57,123 +50,45 @@ public class HarmonizeAbility extends CastFromGraveyardAbility {
|
|||
}
|
||||
}
|
||||
|
||||
class HarmonizeCostReductionEffect extends CostModificationEffectImpl {
|
||||
|
||||
HarmonizeCostReductionEffect() {
|
||||
super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST);
|
||||
}
|
||||
|
||||
private HarmonizeCostReductionEffect(final HarmonizeCostReductionEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
enum HarmonizeAbilityAdjuster implements CostAdjuster {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source, Ability abilityToModify) {
|
||||
SpellAbility spellAbility = (SpellAbility) abilityToModify;
|
||||
int power;
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
if (game.inCheckPlayableState()) {
|
||||
power = game
|
||||
int amount = game
|
||||
.getBattlefield()
|
||||
.getActivePermanents(
|
||||
StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE,
|
||||
source.getControllerId(), source, game
|
||||
).stream()
|
||||
ability.getControllerId(), ability, game
|
||||
)
|
||||
.stream()
|
||||
.map(MageObject::getPower)
|
||||
.mapToInt(MageInt::getValue)
|
||||
.max()
|
||||
.orElse(0);
|
||||
} else {
|
||||
power = CardUtil
|
||||
.castStream(spellAbility.getCosts().stream(), HarmonizeCost.class)
|
||||
.map(HarmonizeCost::getChosenCreature)
|
||||
.map(game::getPermanent)
|
||||
.filter(Objects::nonNull)
|
||||
.map(MageObject::getPower)
|
||||
.mapToInt(MageInt::getValue)
|
||||
.map(x -> Math.max(x, 0))
|
||||
.sum();
|
||||
CardUtil.reduceCost(ability, amount);
|
||||
return;
|
||||
}
|
||||
if (power > 0) {
|
||||
CardUtil.adjustCost(spellAbility, power);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(Ability abilityToModify, Ability source, Game game) {
|
||||
return abilityToModify instanceof SpellAbility
|
||||
&& abilityToModify.getSourceId().equals(source.getSourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HarmonizeCostReductionEffect copy() {
|
||||
return new HarmonizeCostReductionEffect(this);
|
||||
}
|
||||
}
|
||||
|
||||
class HarmonizeCost extends VariableCostImpl {
|
||||
|
||||
private UUID chosenCreature = null;
|
||||
|
||||
HarmonizeCost() {
|
||||
super(VariableCostType.ADDITIONAL, "", "");
|
||||
}
|
||||
|
||||
private HarmonizeCost(final HarmonizeCost cost) {
|
||||
super(cost);
|
||||
this.chosenCreature = cost.chosenCreature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HarmonizeCost copy() {
|
||||
return new HarmonizeCost(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearPaid() {
|
||||
super.clearPaid();
|
||||
chosenCreature = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxValue(Ability source, Game game) {
|
||||
return game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1) ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int announceXValue(Ability source, Game game) {
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
Player player = game.getPlayer(ability.getControllerId());
|
||||
if (player == null || !game.getBattlefield().contains(
|
||||
StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1
|
||||
StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, ability, game, 1
|
||||
) || !player.chooseUse(
|
||||
Outcome.Benefit, "Tap an untapped creature you control for harmonize?", source, game
|
||||
Outcome.Tap, "Tap a creature to reduce the cost of this spell?", ability, game
|
||||
)) {
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
TargetPermanent target = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE);
|
||||
target.withNotTarget(true);
|
||||
target.withChooseHint("for harmonize");
|
||||
player.choose(Outcome.PlayForFree, target, source, game);
|
||||
target.withChooseHint("to pay for harmonize");
|
||||
player.choose(Outcome.Tap, target, ability, game);
|
||||
Permanent permanent = game.getPermanent(target.getFirstTarget());
|
||||
if (permanent == null) {
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
chosenCreature = permanent.getId();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private FilterControlledPermanent makeFilter() {
|
||||
FilterControlledPermanent filter = new FilterControlledPermanent("tap the chosen creature");
|
||||
filter.add(new PermanentIdPredicate(chosenCreature));
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
|
||||
return new TapTargetCost(new TargetControlledPermanent(xValue, xValue, makeFilter(), true));
|
||||
}
|
||||
|
||||
public UUID getChosenCreature() {
|
||||
return chosenCreature;
|
||||
CardUtil.reduceCost(ability, permanent.getPower().getValue());
|
||||
FilterControlledPermanent filter = new FilterControlledPermanent("creature chosen to tap for harmonize");
|
||||
filter.add(new PermanentIdPredicate(permanent.getId()));
|
||||
ability.addCost(new TapTargetCost(new TargetControlledPermanent(filter)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,13 +77,10 @@ public class WardAbility extends TriggeredAbilityImpl {
|
|||
if (!getSourceId().equals(event.getTargetId())) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) {
|
||||
return false;
|
||||
}
|
||||
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
|
||||
return false;
|
||||
}
|
||||
getEffects().setTargetPointer(new FixedTarget(targetingObject.getId()));
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package mage.cards;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.Modes;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.effects.common.ExileAdventureSpellEffect;
|
||||
import mage.constants.CardType;
|
||||
|
|
@ -162,24 +161,13 @@ class AdventureCardSpellAbility extends SpellAbility {
|
|||
|
||||
@Override
|
||||
public String getRule(boolean all) {
|
||||
return this.getRule();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
StringBuilder sbRule = new StringBuilder();
|
||||
sbRule.append(this.nameFull);
|
||||
sbRule.append(" ");
|
||||
sbRule.append(getManaCosts().getText());
|
||||
sbRule.append(" — ");
|
||||
Modes modes = this.getModes();
|
||||
if (modes.size() <= 1) {
|
||||
sbRule.append(modes.getMode().getEffects().getTextStartingUpperCase(modes.getMode()));
|
||||
} else {
|
||||
sbRule.append(getModes().getText());
|
||||
}
|
||||
sbRule.append(" <i>(Then exile this card. You may cast the creature later from exile.)</i>");
|
||||
return sbRule.toString();
|
||||
// TODO: must hide rules in permanent like SpellAbility, but can't due effects text
|
||||
return this.nameFull
|
||||
+ " "
|
||||
+ getManaCosts().getText()
|
||||
+ " — "
|
||||
+ super.getRule(false) // without cost
|
||||
+ " <i>(Then exile this card. You may cast the creature later from exile.)</i>";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import java.awt.geom.Rectangle2D;
|
|||
*/
|
||||
public enum ArtRect {
|
||||
NORMAL(new Rectangle2D.Double(.079f, .11f, .84f, .42f)),
|
||||
RETRO(new Rectangle2D.Double(.12f, .11f, .77f, .43f)),
|
||||
AFTERMATH_TOP(new Rectangle2D.Double(0.075, 0.113, 0.832, 0.227)),
|
||||
AFTERMATH_BOTTOM(new Rectangle2D.Double(0.546, 0.562, 0.272, 0.346)),
|
||||
SPLIT_LEFT(new Rectangle2D.Double(0.152, 0.539, 0.386, 0.400)),
|
||||
|
|
|
|||
|
|
@ -181,19 +181,21 @@ public interface Card extends MageObject, Ownerable {
|
|||
* Remove {@param amount} counters of the specified kind.
|
||||
*
|
||||
* @param isDamage if the counter removal is a result of being damaged (e.g. for Deification to work)
|
||||
* @return amount of counters removed
|
||||
*/
|
||||
void removeCounters(String counterName, int amount, Ability source, Game game, boolean isDamage);
|
||||
int removeCounters(String counterName, int amount, Ability source, Game game, boolean isDamage);
|
||||
|
||||
default void removeCounters(Counter counter, Ability source, Game game) {
|
||||
removeCounters(counter, source, game, false);
|
||||
default int removeCounters(Counter counter, Ability source, Game game) {
|
||||
return removeCounters(counter, source, game, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all counters of any kind.
|
||||
*
|
||||
* @param isDamage if the counter removal is a result of being damaged (e.g. for Deification to work)
|
||||
* @return amount of counters removed
|
||||
*/
|
||||
void removeCounters(Counter counter, Ability source, Game game, boolean isDamage);
|
||||
int removeCounters(Counter counter, Ability source, Game game, boolean isDamage);
|
||||
|
||||
/**
|
||||
* Remove all counters of any kind.
|
||||
|
|
|
|||
|
|
@ -819,19 +819,19 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void removeCounters(String counterName, int amount, Ability source, Game game, boolean isDamage) {
|
||||
public int removeCounters(String counterName, int amount, Ability source, Game game, boolean isDamage) {
|
||||
|
||||
if (amount <= 0) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (getCounters(game).getCount(counterName) <= 0) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
GameEvent removeCountersEvent = new RemoveCountersEvent(counterName, this, source, amount, isDamage);
|
||||
if (game.replaceEvent(removeCountersEvent)) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int finalAmount = 0;
|
||||
|
|
@ -854,13 +854,12 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
|
||||
GameEvent event = new CountersRemovedEvent(counterName, this, source, finalAmount, isDamage);
|
||||
game.fireEvent(event);
|
||||
return finalAmount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCounters(Counter counter, Ability source, Game game, boolean isDamage) {
|
||||
if (counter != null) {
|
||||
removeCounters(counter.getName(), counter.getCount(), source, game, isDamage);
|
||||
}
|
||||
public int removeCounters(Counter counter, Ability source, Game game, boolean isDamage) {
|
||||
return counter != null ? removeCounters(counter.getName(), counter.getCount(), source, game, isDamage) : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ public abstract class ExpansionSet implements Serializable {
|
|||
// TODO: find or implement really full art in m15 render mode (without card name header)
|
||||
public static final CardGraphicInfo NORMAL_ART = null;
|
||||
public static final CardGraphicInfo NON_FULL_USE_VARIOUS = new CardGraphicInfo(null, true); // TODO: rename to NORMAL_ART_USE_VARIOUS
|
||||
public static final CardGraphicInfo RETRO_ART = new CardGraphicInfo(FrameStyle.RETRO, false);
|
||||
public static final CardGraphicInfo RETRO_ART_USE_VARIOUS = new CardGraphicInfo(FrameStyle.RETRO, true);
|
||||
public static final CardGraphicInfo FULL_ART = new CardGraphicInfo(FrameStyle.MPOP_FULL_ART_BASIC, false);
|
||||
public static final CardGraphicInfo FULL_ART_USE_VARIOUS = new CardGraphicInfo(FrameStyle.MPOP_FULL_ART_BASIC, true);
|
||||
|
||||
|
|
@ -138,6 +140,13 @@ public abstract class ExpansionSet implements Serializable {
|
|||
&& this.graphicInfo.getFrameStyle() != null
|
||||
&& this.graphicInfo.getFrameStyle().isFullArt();
|
||||
}
|
||||
|
||||
public boolean isRetroFrame() {
|
||||
return this.graphicInfo != null
|
||||
&& this.graphicInfo.getFrameStyle() != null
|
||||
&& (this.graphicInfo.getFrameStyle() == FrameStyle.RETRO
|
||||
|| this.graphicInfo.getFrameStyle() == FrameStyle.LEA_ORIGINAL_DUAL_LAND_ART_BASIC);
|
||||
}
|
||||
}
|
||||
|
||||
private enum ExpansionSetComparator implements Comparator<ExpansionSet> {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,11 @@ public enum FrameStyle {
|
|||
/**
|
||||
* Original Dual lands (box pattern in the text box)
|
||||
*/
|
||||
LEA_ORIGINAL_DUAL_LAND_ART_BASIC(BorderType.M15, false);
|
||||
LEA_ORIGINAL_DUAL_LAND_ART_BASIC(BorderType.OLD, false),
|
||||
/**
|
||||
* Retro frame
|
||||
*/
|
||||
RETRO(BorderType.OLD, false);
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package mage.cards;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.Modes;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.common.ShuffleIntoLibrarySourceEffect;
|
||||
|
|
@ -134,8 +133,8 @@ class OmenCardSpellAbility extends SpellAbility {
|
|||
this.nameFull = ability.nameFull;
|
||||
if (!ability.finalized) {
|
||||
throw new IllegalStateException("Wrong code usage. "
|
||||
+ "Omen (" + cardName + ") "
|
||||
+ "need to call finalizeOmen() at the very end of the card's constructor.");
|
||||
+ "Omen (" + cardName + ") "
|
||||
+ "need to call finalizeOmen() at the very end of the card's constructor.");
|
||||
}
|
||||
this.finalized = true;
|
||||
}
|
||||
|
|
@ -147,24 +146,13 @@ class OmenCardSpellAbility extends SpellAbility {
|
|||
|
||||
@Override
|
||||
public String getRule(boolean all) {
|
||||
return this.getRule();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
StringBuilder sbRule = new StringBuilder();
|
||||
sbRule.append(this.nameFull);
|
||||
sbRule.append(" ");
|
||||
sbRule.append(getManaCosts().getText());
|
||||
sbRule.append(" — ");
|
||||
Modes modes = this.getModes();
|
||||
if (modes.size() <= 1) {
|
||||
sbRule.append(modes.getMode().getEffects().getTextStartingUpperCase(modes.getMode()));
|
||||
} else {
|
||||
sbRule.append(getModes().getText());
|
||||
}
|
||||
sbRule.append(" <i>(Then shuffle this card into its owner's library.)<i>");
|
||||
return sbRule.toString();
|
||||
// TODO: must hide rules in permanent like SpellAbility, but can't due effects text
|
||||
return this.nameFull
|
||||
+ " "
|
||||
+ getManaCosts().getText()
|
||||
+ " — "
|
||||
+ super.getRule(false) // without cost
|
||||
+ " <i>(Then shuffle this card into its owner's library.)</i>";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ public enum SubType {
|
|||
SULLUSTAN("Sullustan", SubTypeSet.CreatureType, true), // Star Wars
|
||||
SURRAKAR("Surrakar", SubTypeSet.CreatureType),
|
||||
SURVIVOR("Survivor", SubTypeSet.CreatureType),
|
||||
SYMBIOTE("Symbiote", SubTypeSet.CreatureType),
|
||||
SYNTH("Synth", SubTypeSet.CreatureType),
|
||||
// T
|
||||
TENTACLE("Tentacle", SubTypeSet.CreatureType),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public class BoosterDraft extends DraftImpl {
|
|||
}
|
||||
boosterNum++;
|
||||
}
|
||||
this.boosterSendingEnd();
|
||||
this.fireEndDraftEvent();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public interface Draft extends MageItem, Serializable {
|
|||
int getCardNum();
|
||||
boolean addPick(UUID playerId, UUID cardId, Set<UUID> hiddenCards);
|
||||
void setBoosterLoaded(UUID playerID);
|
||||
void boosterSendingStart();
|
||||
void start();
|
||||
boolean isStarted();
|
||||
void setStarted();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import java.util.concurrent.ScheduledFuture;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public abstract class DraftImpl implements Draft {
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ public abstract class DraftImpl implements Draft {
|
|||
protected int cardNum = 1; // starts with card number 1, increases by +1 after each picking
|
||||
protected TimingOption timing;
|
||||
protected int boosterLoadingCounter; // number of times the boosters have been sent to players until all are confirmed to have received them
|
||||
protected final int BOOSTER_LOADING_INTERVAL = 2; // interval in seconds
|
||||
protected final int BOOSTER_LOADING_INTERVAL_SECS = 2; // interval in seconds
|
||||
|
||||
protected boolean abort = false;
|
||||
protected boolean started = false;
|
||||
|
|
@ -44,8 +44,8 @@ public abstract class DraftImpl implements Draft {
|
|||
protected transient TableEventSource tableEventSource = new TableEventSource();
|
||||
protected transient PlayerQueryEventSource playerQueryEventSource = new PlayerQueryEventSource();
|
||||
|
||||
protected ScheduledFuture<?> boosterLoadingHandle;
|
||||
protected ScheduledExecutorService boosterLoadingExecutor = null;
|
||||
protected ScheduledFuture<?> boosterSendingWorker;
|
||||
protected ScheduledExecutorService boosterSendingExecutor = null;
|
||||
|
||||
public DraftImpl(DraftOptions options, List<ExpansionSet> sets) {
|
||||
this.id = UUID.randomUUID();
|
||||
|
|
@ -83,7 +83,6 @@ public abstract class DraftImpl implements Draft {
|
|||
if (newPlayer != null) {
|
||||
DraftPlayer newDraftPlayer = new DraftPlayer(newPlayer);
|
||||
DraftPlayer oldDraftPlayer = players.get(oldPlayer.getId());
|
||||
newDraftPlayer.setBooster(oldDraftPlayer.getBooster());
|
||||
Map<UUID, DraftPlayer> newPlayers = new LinkedHashMap<>();
|
||||
synchronized (players) {
|
||||
for (Map.Entry<UUID, DraftPlayer> entry : players.entrySet()) {
|
||||
|
|
@ -110,12 +109,14 @@ public abstract class DraftImpl implements Draft {
|
|||
|
||||
table.setCurrent(currentId);
|
||||
}
|
||||
|
||||
// boosters send to all players by timeout, so don't need to send it manually here
|
||||
newDraftPlayer.setBoosterAndLoad(oldDraftPlayer.getBooster());
|
||||
if (oldDraftPlayer.isPicking()) {
|
||||
newDraftPlayer.setPicking();
|
||||
if (!newDraftPlayer.getBooster().isEmpty()) {
|
||||
newDraftPlayer.getPlayer().pickCard(newDraftPlayer.getBooster(), newDraftPlayer.getDeck(), this);
|
||||
}
|
||||
newDraftPlayer.setPickingAndSending();
|
||||
}
|
||||
boosterSendingStart(); // if it's AI then make pick from it
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -124,7 +125,7 @@ public abstract class DraftImpl implements Draft {
|
|||
@Override
|
||||
public Collection<DraftPlayer> getPlayers() {
|
||||
synchronized (players) {
|
||||
return players.values();
|
||||
return new ArrayList<>(players.values());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +189,7 @@ public abstract class DraftImpl implements Draft {
|
|||
List<Card> currentBooster = current.booster;
|
||||
while (true) {
|
||||
List<Card> nextBooster = next.booster;
|
||||
next.setBooster(currentBooster);
|
||||
next.setBoosterAndLoad(currentBooster);
|
||||
if (Objects.equals(nextId, startId)) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -209,7 +210,7 @@ public abstract class DraftImpl implements Draft {
|
|||
List<Card> currentBooster = current.booster;
|
||||
while (true) {
|
||||
List<Card> prevBooster = prev.booster;
|
||||
prev.setBooster(currentBooster);
|
||||
prev.setBoosterAndLoad(currentBooster);
|
||||
if (Objects.equals(prevId, startId)) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -221,70 +222,71 @@ public abstract class DraftImpl implements Draft {
|
|||
}
|
||||
|
||||
protected void openBooster() {
|
||||
if (boosterNum <= numberBoosters) {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (draftCube != null) {
|
||||
player.setBooster(draftCube.createBooster());
|
||||
} else {
|
||||
player.setBooster(sets.get(boosterNum - 1).createBooster());
|
||||
synchronized (players) {
|
||||
if (boosterNum <= numberBoosters) {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (draftCube != null) {
|
||||
player.setBoosterAndLoad(draftCube.createBooster());
|
||||
} else {
|
||||
player.setBoosterAndLoad(sets.get(boosterNum - 1).createBooster());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean pickCards() {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (player.getBooster().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
player.setPicking();
|
||||
player.setBoosterNotLoaded();
|
||||
}
|
||||
setupBoosterLoadingHandle();
|
||||
synchronized (this) {
|
||||
while (!donePicking()) {
|
||||
try {
|
||||
this.wait(10000); // checked every 10s to make sure the draft moves on
|
||||
} catch (InterruptedException ex) {
|
||||
synchronized (players) {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (player.getBooster().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
player.setPickingAndSending();
|
||||
}
|
||||
}
|
||||
|
||||
while (!donePicking()) {
|
||||
boosterSendingStart();
|
||||
picksWait();
|
||||
}
|
||||
|
||||
cardNum++;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void setupBoosterLoadingHandle() {
|
||||
cancelBoosterLoadingHandle();
|
||||
boosterLoadingCounter = 0;
|
||||
|
||||
if (this.boosterLoadingExecutor == null) {
|
||||
this.boosterLoadingExecutor = Executors.newSingleThreadScheduledExecutor(
|
||||
new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY_BOOSTERS_SEND)
|
||||
public void boosterSendingStart() {
|
||||
if (this.boosterSendingExecutor == null) {
|
||||
this.boosterSendingExecutor = Executors.newSingleThreadScheduledExecutor(
|
||||
new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY_BOOSTERS_SEND + " " + this.getId())
|
||||
);
|
||||
}
|
||||
boosterLoadingCounter = 0;
|
||||
|
||||
boosterLoadingHandle = boosterLoadingExecutor.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
if (loadBoosters()) {
|
||||
cancelBoosterLoadingHandle();
|
||||
} else {
|
||||
boosterLoadingCounter++;
|
||||
if (boosterSendingWorker == null) {
|
||||
boosterSendingWorker = boosterSendingExecutor.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
if (isAbort() || sendBoostersToPlayers()) {
|
||||
boosterSendingEnd();
|
||||
} else {
|
||||
boosterLoadingCounter++;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.fatal("Fatal boosterLoadingHandle error in draft " + id + " pack " + boosterNum + " pick " + cardNum, ex);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.fatal("Fatal boosterLoadingHandle error in draft " + id + " pack " + boosterNum + " pick " + cardNum, ex);
|
||||
}
|
||||
}, 0, BOOSTER_LOADING_INTERVAL, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected void cancelBoosterLoadingHandle() {
|
||||
if (boosterLoadingHandle != null) {
|
||||
boosterLoadingHandle.cancel(true);
|
||||
}, 0, BOOSTER_LOADING_INTERVAL_SECS, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean loadBoosters() {
|
||||
protected void boosterSendingEnd() {
|
||||
if (boosterSendingWorker != null) {
|
||||
boosterSendingWorker.cancel(true);
|
||||
boosterSendingWorker = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean sendBoostersToPlayers() {
|
||||
boolean allBoostersLoaded = true;
|
||||
for (DraftPlayer player : players.values()) {
|
||||
for (DraftPlayer player : getPlayers()) {
|
||||
if (player.isPicking() && !player.isBoosterLoaded()) {
|
||||
allBoostersLoaded = false;
|
||||
player.getPlayer().pickCard(player.getBooster(), player.getDeck(), this);
|
||||
|
|
@ -297,16 +299,20 @@ public abstract class DraftImpl implements Draft {
|
|||
if (isAbort()) {
|
||||
return true;
|
||||
}
|
||||
return players.values()
|
||||
.stream()
|
||||
.noneMatch(DraftPlayer::isPicking);
|
||||
|
||||
synchronized (players) {
|
||||
return players.values()
|
||||
.stream()
|
||||
.noneMatch(DraftPlayer::isPicking);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allJoined() {
|
||||
return players.values().stream()
|
||||
.allMatch(DraftPlayer::isJoined);
|
||||
synchronized (players) {
|
||||
return players.values().stream()
|
||||
.allMatch(DraftPlayer::isJoined);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -342,11 +348,32 @@ public abstract class DraftImpl implements Draft {
|
|||
// if the pack is re-sent to a player because they haven't been able to successfully load it, the pick time is reduced appropriately because of the elapsed time
|
||||
// the time is always at least 1 second unless it's set to 0, i.e. unlimited time
|
||||
if (time > 0) {
|
||||
time = Math.max(1, time - boosterLoadingCounter * BOOSTER_LOADING_INTERVAL);
|
||||
time = Math.max(1, time - boosterLoadingCounter * BOOSTER_LOADING_INTERVAL_SECS);
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
public void picksCheckDone() {
|
||||
// notify main thread about changes, can be called from user's thread
|
||||
synchronized (this) {
|
||||
this.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
protected void picksWait() {
|
||||
// main thread waiting any picks or changes
|
||||
synchronized (this) {
|
||||
try {
|
||||
this.wait(10000); // checked every 10s to make sure the draft moves on
|
||||
} catch (InterruptedException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
if (donePicking()) {
|
||||
boosterSendingEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addPick(UUID playerId, UUID cardId, Set<UUID> hiddenCards) {
|
||||
DraftPlayer player = players.get(playerId);
|
||||
|
|
@ -354,13 +381,10 @@ public abstract class DraftImpl implements Draft {
|
|||
for (Card card : player.booster) {
|
||||
if (card.getId().equals(cardId)) {
|
||||
player.addPick(card, hiddenCards);
|
||||
player.booster.remove(card);
|
||||
break;
|
||||
}
|
||||
}
|
||||
synchronized (this) {
|
||||
this.notifyAll();
|
||||
}
|
||||
picksCheckDone();
|
||||
}
|
||||
return !player.isPicking();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class DraftPlayer {
|
|||
protected Deck deck;
|
||||
protected List<Card> booster;
|
||||
protected boolean picking;
|
||||
protected boolean boosterLoaded;
|
||||
protected boolean boosterLoaded; // client confirmed that it got a booster data (for computer must be always false)
|
||||
protected boolean joined = false;
|
||||
protected Set<UUID> hiddenCards;
|
||||
|
||||
|
|
@ -64,14 +64,13 @@ public class DraftPlayer {
|
|||
if (hiddenCards != null) {
|
||||
this.hiddenCards = hiddenCards;
|
||||
}
|
||||
synchronized (booster) {
|
||||
booster.remove(card);
|
||||
}
|
||||
booster.remove(card);
|
||||
picking = false;
|
||||
}
|
||||
|
||||
public void setBooster(List<Card> booster) {
|
||||
public void setBoosterAndLoad(List<Card> booster) {
|
||||
this.booster = booster;
|
||||
this.boosterLoaded = false; // human will receive new pick, computer with choose new pick
|
||||
}
|
||||
|
||||
public List<Card> getBooster() {
|
||||
|
|
@ -83,8 +82,9 @@ public class DraftPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
public void setPicking() {
|
||||
picking = true;
|
||||
public void setPickingAndSending() {
|
||||
this.picking = true;
|
||||
this.boosterLoaded = false;
|
||||
}
|
||||
|
||||
public boolean isPicking() {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class RandomBoosterDraft extends BoosterDraft {
|
|||
protected void openBooster() {
|
||||
if (boosterNum <= numberBoosters) {
|
||||
for (DraftPlayer player: players.values()) {
|
||||
player.setBooster(getNextBooster().create15CardBooster());
|
||||
player.setBoosterAndLoad(getNextBooster().create15CardBooster());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class ReshuffledBoosterDraft extends BoosterDraft {
|
|||
protected void openBooster() {
|
||||
if (boosterNum <= numberBoosters) {
|
||||
for (DraftPlayer player: players.values()) {
|
||||
player.setBooster(reshuffledSet.createBooster());
|
||||
player.setBoosterAndLoad(reshuffledSet.createBooster());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
|
||||
package mage.game.draft;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.ExpansionSet;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author spjspj
|
||||
*/
|
||||
public class RichManBoosterDraft extends DraftImpl {
|
||||
|
|
@ -38,6 +37,7 @@ public class RichManBoosterDraft extends DraftImpl {
|
|||
}
|
||||
boosterNum++;
|
||||
}
|
||||
this.boosterSendingEnd();
|
||||
this.fireEndDraftEvent();
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ public class RichManBoosterDraft extends DraftImpl {
|
|||
DraftPlayer next = players.get(nextId);
|
||||
while (true) {
|
||||
List<Card> nextBooster = sets.get((cardNum - 1) % sets.size()).createBooster();
|
||||
next.setBooster(nextBooster);
|
||||
next.setBoosterAndLoad(nextBooster);
|
||||
if (Objects.equals(nextId, startId)) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -62,22 +62,21 @@ public class RichManBoosterDraft extends DraftImpl {
|
|||
|
||||
@Override
|
||||
protected boolean pickCards() {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (cardNum > 36) {
|
||||
return false;
|
||||
}
|
||||
player.setPicking();
|
||||
player.getPlayer().pickCard(player.getBooster(), player.getDeck(), this);
|
||||
}
|
||||
cardNum++;
|
||||
synchronized (this) {
|
||||
while (!donePicking()) {
|
||||
try {
|
||||
this.wait();
|
||||
} catch (InterruptedException ex) {
|
||||
synchronized (players) {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (cardNum > 36) {
|
||||
return false;
|
||||
}
|
||||
player.setPickingAndSending();
|
||||
}
|
||||
}
|
||||
|
||||
while (!donePicking()) {
|
||||
boosterSendingStart();
|
||||
picksWait();
|
||||
}
|
||||
|
||||
cardNum++;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +84,7 @@ public class RichManBoosterDraft extends DraftImpl {
|
|||
public void firePickCardEvent(UUID playerId) {
|
||||
DraftPlayer player = players.get(playerId);
|
||||
int cardNum = Math.min(36, this.cardNum);
|
||||
|
||||
// richman uses custom times
|
||||
int time = (int) Math.ceil(customProfiTimes[cardNum - 1] * timing.getCustomTimeoutFactor());
|
||||
playerQueryEventSource.pickCard(playerId, "Pick card", player.getBooster(), time);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
package mage.game.draft;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import mage.cards.Card;
|
||||
import mage.cards.ExpansionSet;
|
||||
import mage.game.draft.DraftCube.CardIdentity;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author spjspj
|
||||
*/
|
||||
public class RichManCubeBoosterDraft extends DraftImpl {
|
||||
|
|
@ -36,6 +35,7 @@ public class RichManCubeBoosterDraft extends DraftImpl {
|
|||
}
|
||||
boosterNum++;
|
||||
}
|
||||
this.boosterSendingEnd();
|
||||
this.fireEndDraftEvent();
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ public class RichManCubeBoosterDraft extends DraftImpl {
|
|||
}
|
||||
|
||||
List<Card> nextBooster = draftCube.createBooster();
|
||||
next.setBooster(nextBooster);
|
||||
next.setBoosterAndLoad(nextBooster);
|
||||
if (Objects.equals(nextId, startId)) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -77,22 +77,21 @@ public class RichManCubeBoosterDraft extends DraftImpl {
|
|||
|
||||
@Override
|
||||
protected boolean pickCards() {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (cardNum > 36) {
|
||||
return false;
|
||||
}
|
||||
player.setPicking();
|
||||
player.getPlayer().pickCard(player.getBooster(), player.getDeck(), this);
|
||||
}
|
||||
cardNum++;
|
||||
synchronized (this) {
|
||||
while (!donePicking()) {
|
||||
try {
|
||||
this.wait();
|
||||
} catch (InterruptedException ex) {
|
||||
synchronized (players) {
|
||||
for (DraftPlayer player : players.values()) {
|
||||
if (cardNum > 36) {
|
||||
return false;
|
||||
}
|
||||
player.setPickingAndSending();
|
||||
}
|
||||
}
|
||||
|
||||
while (!donePicking()) {
|
||||
boosterSendingStart();
|
||||
picksWait();
|
||||
}
|
||||
|
||||
cardNum++;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ public abstract class MatchImpl implements Match {
|
|||
while (!isDoneSideboarding()) {
|
||||
try {
|
||||
this.wait();
|
||||
} catch (InterruptedException ex) {
|
||||
} catch (InterruptedException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package mage.game.permanent.token;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.keyword.DefenderAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public final class Wall13Token extends TokenImpl {
|
||||
|
||||
public Wall13Token() {
|
||||
super("Wall Token", "1/3 white Wall creature token with defender");
|
||||
cardType.add(CardType.CREATURE);
|
||||
subtype.add(SubType.WALL);
|
||||
color.setWhite(true);
|
||||
power = new MageInt(1);
|
||||
toughness = new MageInt(3);
|
||||
|
||||
addAbility(DefenderAbility.getInstance());
|
||||
}
|
||||
|
||||
private Wall13Token(final Wall13Token token) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
public Wall13Token copy() {
|
||||
return new Wall13Token(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -1093,13 +1093,13 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void removeCounters(String counterName, int amount, Ability source, Game game, boolean isDamage) {
|
||||
card.removeCounters(counterName, amount, source, game, isDamage);
|
||||
public int removeCounters(String counterName, int amount, Ability source, Game game, boolean isDamage) {
|
||||
return card.removeCounters(counterName, amount, source, game, isDamage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCounters(Counter counter, Ability source, Game game, boolean isDamage) {
|
||||
card.removeCounters(counter, source, game, isDamage);
|
||||
public int removeCounters(Counter counter, Ability source, Game game, boolean isDamage) {
|
||||
return card.removeCounters(counter, source, game, isDamage);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -787,6 +787,11 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CostAdjuster getCostAdjuster() {
|
||||
return costAdjuster;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustX(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
|
|
|
|||
|
|
@ -781,15 +781,17 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
Card card = isDrawsFromBottom() ? getLibrary().drawFromBottom(game) : getLibrary().drawFromTop(game);
|
||||
if (card != null) {
|
||||
card.moveToZone(Zone.HAND, source, game, false); // if you want to use event.getSourceId() here then thinks x10 times
|
||||
if (isTopCardRevealed()) {
|
||||
if (isTopCardRevealed() && !isDrawsFromBottom()) {
|
||||
game.fireInformEvent(getLogName() + " draws a revealed card (" + card.getLogName() + ')');
|
||||
}
|
||||
game.fireEvent(new DrewCardEvent(card.getId(), getId(), source, event));
|
||||
numDrawn++;
|
||||
}
|
||||
}
|
||||
if (!isTopCardRevealed() && numDrawn > 0) {
|
||||
game.fireInformEvent(getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : ""));
|
||||
if ((!isTopCardRevealed() || isDrawsFromBottom()) && numDrawn > 0) {
|
||||
game.fireInformEvent(getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a")
|
||||
+ " card" + (numDrawn > 1 ? "s" : "")
|
||||
+ (isDrawsFromBottom() ? " from the bottom of their library" : ""));
|
||||
}
|
||||
// if this method was called from a replacement event, pass the number of cards back through
|
||||
// (uncomment conditions if correct ruling is to only count cards drawn by the same player)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ public interface Target extends Serializable {
|
|||
|
||||
boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game);
|
||||
|
||||
/**
|
||||
* Add target from targeting methods like chooseTarget (will check and generate target events and effects)
|
||||
*/
|
||||
void addTarget(UUID id, Ability source, Game game);
|
||||
|
||||
void addTarget(UUID id, int amount, Ability source, Game game);
|
||||
|
|
@ -90,6 +93,9 @@ public interface Target extends Serializable {
|
|||
|
||||
boolean choose(Outcome outcome, UUID playerId, UUID sourceId, Ability source, Game game);
|
||||
|
||||
/**
|
||||
* Add target from non targeting methods like choose
|
||||
*/
|
||||
void add(UUID id, Game game);
|
||||
|
||||
void remove(UUID targetId);
|
||||
|
|
|
|||
|
|
@ -12,12 +12,20 @@ import java.util.UUID;
|
|||
*/
|
||||
public class TargetCardInTargetPlayersGraveyard extends TargetCardInGraveyard {
|
||||
|
||||
private final int targetPlayerIndex;
|
||||
|
||||
public TargetCardInTargetPlayersGraveyard(int targets) {
|
||||
this(targets, 0);
|
||||
}
|
||||
|
||||
public TargetCardInTargetPlayersGraveyard(int targets, int targetPlayerIndex) {
|
||||
super(0, targets, StaticFilters.FILTER_CARD);
|
||||
this.targetPlayerIndex = targetPlayerIndex;
|
||||
}
|
||||
|
||||
private TargetCardInTargetPlayersGraveyard(final TargetCardInTargetPlayersGraveyard target) {
|
||||
super(target);
|
||||
this.targetPlayerIndex = target.targetPlayerIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -26,7 +34,7 @@ public class TargetCardInTargetPlayersGraveyard extends TargetCardInGraveyard {
|
|||
return false;
|
||||
}
|
||||
Card card = game.getCard(id);
|
||||
return card != null && card.isOwnedBy(source.getFirstTarget());
|
||||
return card != null && card.isOwnedBy(source.getTargets().get(targetPlayerIndex).getFirstTarget());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1128,14 +1128,19 @@ public final class CardUtil {
|
|||
|
||||
/**
|
||||
* For finding the spell or ability on the stack for "becomes the target" triggers.
|
||||
*
|
||||
* Also ensures that spells/abilities that target the same object twice only trigger each "becomes the target" ability once.
|
||||
* If this is the first attempt at triggering for a given ability targeting a given object,
|
||||
* this method records that in the game state for later checks by this same method, to not return the same object again.
|
||||
*
|
||||
* @param checkingReference must be unique for each usage (this.getId().toString() of the TriggeredAbility, or this.getKey() of the watcher)
|
||||
* @param event the GameEvent.EventType.TARGETED from checkTrigger() or watch()
|
||||
* @param game the Game from checkTrigger() or watch()
|
||||
* @return the StackObject which targeted the source, or null if not found
|
||||
* @return the StackObject which targeted the source, or null if already used or not found
|
||||
*/
|
||||
public static StackObject getTargetingStackObject(String checkingReference, GameEvent event, Game game) {
|
||||
public static StackObject findTargetingStackObject(String checkingReference, GameEvent event, Game game) {
|
||||
// In case of multiple simultaneous triggered abilities from the same source,
|
||||
// need to get the actual one that targeted, see #8026, #8378
|
||||
// need to get the actual one that targeted, see #8026, #8378, rulings for Battle Mammoth
|
||||
// In case of copied triggered abilities, need to trigger on each independently, see #13498
|
||||
// Also avoids triggering on cancelled selections, see #8802
|
||||
String stateKey = "targetedMap" + checkingReference;
|
||||
Map<UUID, Set<UUID>> targetMap = (Map<UUID, Set<UUID>>) game.getState().getValue(stateKey);
|
||||
|
|
@ -1148,50 +1153,22 @@ public final class CardUtil {
|
|||
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
|
||||
for (StackObject stackObject : game.getStack()) {
|
||||
Ability stackAbility = stackObject.getStackAbility();
|
||||
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId()) || targetingObjects.contains(stackObject.getId())) {
|
||||
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) {
|
||||
continue;
|
||||
}
|
||||
if (CardUtil.getAllSelectedTargets(stackAbility, game).contains(event.getTargetId())) {
|
||||
if (!targetingObjects.add(stackObject.getId())) {
|
||||
continue; // The trigger/watcher already recorded that target of the stack object, check for another
|
||||
}
|
||||
// Otherwise, store this combination of trigger/watcher + target + stack object
|
||||
targetMap.put(event.getTargetId(), targetingObjects);
|
||||
game.getState().setValue(stateKey, targetMap);
|
||||
return stackObject;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For ensuring that spells/abilities that target the same object twice only trigger each "becomes the target" ability once.
|
||||
* If this is the first attempt at triggering for a given ability targeting a given object,
|
||||
* this method records that in the game state for later checks by this same method.
|
||||
*
|
||||
* @param checkingReference must be unique for each usage (this.id.toString() of the TriggeredAbility, or this.getKey() of the watcher)
|
||||
* @param targetingObject from getTargetingStackObject
|
||||
* @param event the GameEvent.EventType.TARGETED from checkTrigger() or watch()
|
||||
* @param game the Game from checkTrigger() or watch()
|
||||
* @return true if already triggered/watched, false if this is the first/only trigger/watch
|
||||
*/
|
||||
public static boolean checkTargetedEventAlreadyUsed(String checkingReference, StackObject targetingObject, GameEvent event, Game game) {
|
||||
String stateKey = "targetedMap" + checkingReference;
|
||||
// If a spell or ability an opponent controls targets a single permanent you control more than once,
|
||||
// Battle Mammoth's triggered ability will trigger only once.
|
||||
// However, if a spell or ability an opponent controls targets multiple permanents you control,
|
||||
// Battle Mammoth's triggered ability will trigger once for each of those permanents. (2021-02-05)
|
||||
Map<UUID, Set<UUID>> targetMap = (Map<UUID, Set<UUID>>) game.getState().getValue(stateKey);
|
||||
// targetMap: key - targetId; value - Set of stackObject Ids
|
||||
if (targetMap == null) {
|
||||
targetMap = new HashMap<>();
|
||||
} else {
|
||||
targetMap = new HashMap<>(targetMap); // must have new object reference if saved back to game state
|
||||
}
|
||||
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
|
||||
if (!targetingObjects.add(targetingObject.getId())) {
|
||||
return true; // The trigger/watcher already recorded that target of the stack object
|
||||
}
|
||||
// Otherwise, store this combination of trigger/watcher + target + stack object
|
||||
targetMap.put(event.getTargetId(), targetingObjects);
|
||||
game.getState().setValue(stateKey, targetMap);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* For overriding `canTarget()` with usages such as "any number of target cards with total mana value X or less".
|
||||
* Call this after super.canTarget() returns true.
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ public class NumberOfTimesPermanentTargetedATurnWatcher extends Watcher {
|
|||
if (event.getType() != GameEvent.EventType.TARGETED) {
|
||||
return;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getKey(), event, game);
|
||||
if (targetingObject == null || CardUtil.checkTargetedEventAlreadyUsed(this.getKey(), targetingObject, event, game)) {
|
||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getKey(), event, game);
|
||||
if (targetingObject == null) {
|
||||
return;
|
||||
}
|
||||
Permanent permanent = game.getPermanent(event.getTargetId());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue