Merge branch 'External-master'
All checks were successful
/ build_release (push) Successful in 25m3s

This commit is contained in:
Failure 2025-04-23 01:31:03 -07:00
commit 2cd85af552
332 changed files with 22134 additions and 16115 deletions

View file

@ -35,6 +35,7 @@ public enum MageIdentifier {
CourtOfLocthwainWatcher("Without paying manacost"),
LaraCroftTombRaiderWatcher,
CoramTheUndertakerWatcher,
ThundermanDragonWatcher,
// ----------------------------//
// alternate casts //

View file

@ -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;

View file

@ -549,6 +549,8 @@ public interface Ability extends Controllable, Serializable {
*/
Ability setCostAdjuster(CostAdjuster costAdjuster);
CostAdjuster getCostAdjuster();
/**
* Prepare {X} settings for announce
*/

View file

@ -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) {

View file

@ -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() {

View file

@ -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> &mdash; 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> &mdash; 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);
}
}
}

View file

@ -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));

View file

@ -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));

View file

@ -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()));

View file

@ -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));

View file

@ -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;

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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);

View file

@ -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

View file

@ -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 {

View file

@ -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.");

View file

@ -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);
}
}

View file

@ -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())));

View file

@ -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)));
}
}

View file

@ -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;
}

View file

@ -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(" &mdash; ");
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()
+ " &mdash; "
+ super.getRule(false) // without cost
+ " <i>(Then exile this card. You may cast the creature later from exile.)</i>";
}
@Override

View file

@ -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)),

View file

@ -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.

View file

@ -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

View file

@ -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> {

View file

@ -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);
/**

View file

@ -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(" &mdash; ");
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()
+ " &mdash; "
+ super.getRule(false) // without cost
+ " <i>(Then shuffle this card into its owner's library.)</i>";
}
@Override

View file

@ -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),

View file

@ -34,6 +34,7 @@ public class BoosterDraft extends DraftImpl {
}
boosterNum++;
}
this.boosterSendingEnd();
this.fireEndDraftEvent();
}

View file

@ -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();

View file

@ -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();
}

View file

@ -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() {

View file

@ -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());
}
}
}

View file

@ -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());
}
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -336,7 +336,7 @@ public abstract class MatchImpl implements Match {
while (!isDoneSideboarding()) {
try {
this.wait();
} catch (InterruptedException ex) {
} catch (InterruptedException ignore) {
}
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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) {

View file

@ -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)

View file

@ -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);

View file

@ -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

View file

@ -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.

View file

@ -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());