Ready for review: Implement Craft mechanic (#11352)

* [LCI] Implement Spring-Loaded Sawblades / Bladewheel Chariot

* [LCI] Implement Sunbird Standard / Sunbird Effigy

* card filter needs to have an owner predicate

* [LCI] Implement Throne of the Grim Captain / The Grim Captain

* make default constructor for craft with artifact

* dedupe some code

* refactor constructors for simplicity

* add currently failing test
This commit is contained in:
Evan Kranzler 2023-10-27 22:32:11 -04:00 committed by GitHub
parent 0f7db0c69d
commit bc4aa6931f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 884 additions and 8 deletions

View file

@ -56,8 +56,12 @@ public abstract class RoleAssignment<T> implements Serializable {
}
public int getRoleCount(Cards cards, Game game) {
return getRoleCount(cards.getCards(game), game);
}
public int getRoleCount(Set<? extends Card> cards, Game game) {
Map<UUID, Set<T>> attributeMap = new HashMap<>();
cards.getCards(game).forEach(card -> attributeMap.put(card.getId(), this.makeSet(card, game)));
cards.forEach(card -> attributeMap.put(card.getId(), this.makeSet(card, game)));
if (attributeMap.size() < 2) {
return attributeMap.size();
}

View file

@ -0,0 +1,176 @@
package mage.abilities.keyword;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.common.ExileSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterArtifactCard;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.common.FilterOwnedCard;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetCardInGraveyardBattlefieldOrStack;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public class CraftAbility extends ActivatedAbilityImpl {
private static final FilterCard artifactFilter = new FilterArtifactCard("artifact");
static {
artifactFilter.add(TargetController.YOU.getOwnerPredicate());
}
private final String description;
private final String manaString;
public CraftAbility(String manaString) {
this(manaString, "artifact", "another artifact you control or an artifact card from your graveyard", CardType.ARTIFACT.getPredicate());
}
public CraftAbility(String manaString, String description, String targetDescription, Predicate<MageObject>... predicates) {
this(manaString, description, targetDescription, 1, 1, predicates);
}
public CraftAbility(String manaString, String description, String targetDescription, int minTargets, int maxTargets, Predicate<MageObject>... predicates) {
this(manaString, description, makeTarget(minTargets, maxTargets, targetDescription, predicates));
}
public CraftAbility(String manaString, String description, TargetCardInGraveyardBattlefieldOrStack target) {
super(Zone.BATTLEFIELD, new CraftEffect(), new ManaCostsImpl<>(manaString));
this.addCost(new ExileSourceCost());
this.addCost(new CraftCost(target));
this.addSubAbility(new TransformAbility());
this.timing = TimingRule.SORCERY;
this.manaString = manaString;
this.description = description;
}
private CraftAbility(final CraftAbility ability) {
super(ability);
this.manaString = ability.manaString;
this.description = ability.description;
}
@Override
public CraftAbility copy() {
return new CraftAbility(this);
}
@Override
public String getRule() {
return "Craft with " + description + ' ' + manaString;
}
private static TargetCardInGraveyardBattlefieldOrStack makeTarget(int minTargets, int maxTargets, String targetDescription, Predicate<MageObject>... predicates) {
FilterPermanent filterPermanent = new FilterControlledPermanent();
filterPermanent.add(AnotherPredicate.instance);
FilterCard filterCard = new FilterOwnedCard();
for (Predicate<MageObject> predicate : predicates) {
filterPermanent.add(predicate);
filterCard.add(predicate);
}
return new TargetCardInGraveyardBattlefieldOrStack(minTargets, maxTargets, filterCard, filterPermanent, targetDescription);
}
}
class CraftCost extends CostImpl {
private final TargetCardInGraveyardBattlefieldOrStack target;
CraftCost(TargetCardInGraveyardBattlefieldOrStack target) {
super();
this.target = target;
target.withNotTarget(true);
}
private CraftCost(final CraftCost cost) {
super(cost);
this.target = cost.target.copy();
}
@Override
public CraftCost copy() {
return new CraftCost(this);
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return target.canChoose(controllerId, source, game);
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
paid = false;
return paid;
}
player.chooseTarget(Outcome.Exile, target, source, game);
Set<Card> cards = target
.getTargets()
.stream()
.map(uuid -> {
Permanent permanent = game.getPermanent(uuid);
if (permanent != null) {
return permanent;
}
return game.getCard(uuid);
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
player.moveCardsToExile(
cards, source, game, true,
CardUtil.getExileZoneId(game, source),
CardUtil.getSourceName(game, source)
);
paid = true;
return paid;
}
}
class CraftEffect extends OneShotEffect {
CraftEffect() {
super(Outcome.Benefit);
}
private CraftEffect(final CraftEffect effect) {
super(effect);
}
@Override
public CraftEffect copy() {
return new CraftEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
Card card = game.getCard(source.getSourceId());
if (player == null || card == null || card.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter() + 1) {
return false;
}
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE);
player.moveCards(card, Zone.BATTLEFIELD, source, game);
return true;
}
}

View file

@ -375,13 +375,20 @@ public final class StaticFilters {
FILTER_CONTROLLED_A_PERMANENT.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_CONTROLLED_ANOTHER_PERMANENT = new FilterControlledPermanent("another target permanent you control");
public static final FilterControlledPermanent FILTER_CONTROLLED_ANOTHER_PERMANENT = new FilterControlledPermanent("another permanent you control");
static {
FILTER_CONTROLLED_ANOTHER_PERMANENT.add(AnotherPredicate.instance);
FILTER_CONTROLLED_ANOTHER_PERMANENT.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT = new FilterControlledPermanent("another target permanent you control");
static {
FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT.add(AnotherPredicate.instance);
FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENTS = new FilterControlledPermanent("permanents you control");
static {

View file

@ -32,7 +32,11 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
protected final FilterSpell filterSpell;
public TargetCardInGraveyardBattlefieldOrStack(int minNumTargets, int maxNumTargets, FilterCard filterGraveyard, FilterPermanent filterBattlefield) {
this(minNumTargets, maxNumTargets, filterGraveyard, filterBattlefield, defaultSpellFilter, null);
this(minNumTargets, maxNumTargets, filterGraveyard, filterBattlefield, null);
}
public TargetCardInGraveyardBattlefieldOrStack(int minNumTargets, int maxNumTargets, FilterCard filterGraveyard, FilterPermanent filterBattlefield, String targetName) {
this(minNumTargets, maxNumTargets, filterGraveyard, filterBattlefield, defaultSpellFilter, targetName);
}
public TargetCardInGraveyardBattlefieldOrStack(int minNumTargets, int maxNumTargets, FilterCard filterGraveyard, FilterPermanent filterBattlefield, FilterSpell filterSpell, String targetName) {
@ -40,7 +44,7 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
this.filterPermanent = filterBattlefield;
this.filterSpell = filterSpell;
this.targetName = targetName != null ? targetName : filter.getMessage()
+ " in a graveyard "
+ " in a graveyard"
+ (maxNumTargets > 1 ? " and/or " : " or ")
+ this.filterPermanent.getMessage()
+ " on the battlefield";