[IKO] Implement Companion and 2 companions

Keruga, the Macrosage and Umori, the Collector
This commit is contained in:
emerald000 2020-04-12 08:23:04 -04:00
parent 395ae9ec11
commit c3684a732b
21 changed files with 866 additions and 128 deletions

View file

@ -0,0 +1,53 @@
package mage.abilities.effects.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.choices.Choice;
import mage.choices.ChoiceCardType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
/**
* @author emerald000
*/
public class ChooseCardTypeEffect extends OneShotEffect {
public ChooseCardTypeEffect(Outcome outcome) {
super(outcome);
staticText = "choose a card type";
}
private ChooseCardTypeEffect(final ChooseCardTypeEffect effect) {
super(effect);
}
@Override
public ChooseCardTypeEffect copy() {
return new ChooseCardTypeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject mageObject = game.getPermanentEntering(source.getSourceId());
if (mageObject == null) {
mageObject = game.getObject(source.getSourceId());
}
if (controller != null && mageObject != null) {
Choice typeChoice = new ChoiceCardType();
if (controller.choose(outcome, typeChoice, game)) {
game.informPlayers(mageObject.getLogName() + ": " + controller.getLogName() + " has chosen: " + typeChoice.getChoice());
game.getState().setValue(source.getSourceId() + "_type", typeChoice.getChoice());
if (mageObject instanceof Permanent) {
((Permanent) mageObject).addInfo("chosen type", CardUtil.addToolTipMarkTags("Chosen type: " + typeChoice.getChoice()), game);
}
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,39 @@
package mage.abilities.effects.common.cost;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.constants.CardType;
import mage.filter.FilterCard;
import mage.game.Game;
/**
* @author emerald000
*/
public class SpellsCostReductionAllOfChosenCardTypeEffect extends SpellsCostReductionAllEffect {
public SpellsCostReductionAllOfChosenCardTypeEffect(FilterCard filter, int amount) {
this(filter, amount, false);
}
public SpellsCostReductionAllOfChosenCardTypeEffect(FilterCard filter, int amount, boolean onlyControlled) {
super(filter, amount, false, onlyControlled);
}
public SpellsCostReductionAllOfChosenCardTypeEffect(final SpellsCostReductionAllOfChosenCardTypeEffect effect) {
super(effect);
}
@Override
public SpellsCostReductionAllOfChosenCardTypeEffect copy() {
return new SpellsCostReductionAllOfChosenCardTypeEffect(this);
}
@Override
protected boolean selectedByRuntimeData(Card card, Ability source, Game game) {
Object savedType = game.getState().getValue(source.getSourceId() + "_type");
if (savedType instanceof String) {
return card.getCardType().contains(CardType.fromString((String) savedType));
}
return false;
}
}

View file

@ -0,0 +1,40 @@
package mage.abilities.effects.keyword;
import mage.abilities.Ability;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import java.util.UUID;
/*
* @author emerald000
*/
public class CompanionEffect extends AsThoughEffectImpl {
public CompanionEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit);
staticText = "Once during the game, you may cast your chosen companion from your sideboard";
}
private CompanionEffect(final CompanionEffect effect) {
super(effect);
}
@Override
public CompanionEffect copy() {
return new CompanionEffect(this);
}
@Override
public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
return objectId.equals(source.getSourceId()) && affectedControllerId.equals(source.getControllerId());
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
}

View file

@ -0,0 +1,41 @@
package mage.abilities.keyword;
import mage.abilities.StaticAbility;
import mage.abilities.effects.keyword.CompanionEffect;
import mage.cards.Card;
import mage.constants.Zone;
import java.util.Set;
/*
* @author emerald000
*/
public class CompanionAbility extends StaticAbility {
private final CompanionCondition condition;
public CompanionAbility(CompanionCondition condition) {
super(Zone.OUTSIDE, new CompanionEffect());
this.condition = condition;
}
private CompanionAbility(final CompanionAbility ability) {
super(ability);
this.condition = ability.condition;
}
@Override
public CompanionAbility copy() {
return new CompanionAbility(this);
}
@Override
public String getRule() {
return "Companion — " + condition.getRule();
}
public boolean isLegal(Set<Card> cards) {
return condition.isLegal(cards);
}
}

View file

@ -0,0 +1,23 @@
package mage.abilities.keyword;
import mage.cards.Card;
import java.io.Serializable;
import java.util.Set;
/*
* @author emerald000
*/
public interface CompanionCondition extends Serializable {
/**
* @return The rule to get added to the card text. (Everything after the dash)
*/
String getRule();
/**
* @param deck The set of cards to check.
* @return Whether the companion is valid for that deck.
*/
boolean isLegal(Set<Card> deck);
}

View file

@ -0,0 +1,31 @@
package mage.choices;
import mage.constants.CardType;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* @author emerald000
*/
public class ChoiceCardType extends ChoiceImpl {
public ChoiceCardType() {
this(true);
}
public ChoiceCardType(boolean required) {
super(required);
this.choices.addAll(Arrays.stream(CardType.values()).map(CardType::toString).collect(Collectors.toList()));
this.message = "Choose a card type";
}
private ChoiceCardType(final ChoiceCardType choice) {
super(choice);
}
@Override
public ChoiceCardType copy() {
return new ChoiceCardType(this);
}
}

View file

@ -17,9 +17,13 @@ public enum CardType {
ENCHANTMENT("Enchantment", true),
INSTANT("Instant", false),
LAND("Land", true),
PHENOMENON("Phenomenon", false),
PLANE("Plane", false),
PLANESWALKER("Planeswalker", true),
SCHEME("Scheme", false),
SORCERY("Sorcery", false),
TRIBAL("Tribal", false);
TRIBAL("Tribal", false),
VANGUARD("Vanguard", false);
private final String text;
private final boolean permanentType;

View file

@ -1,12 +1,11 @@
package mage.game;
import java.util.Map;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.continuous.CommanderReplacementEffect;
import mage.abilities.effects.common.cost.CommanderCostModification;
import mage.abilities.keyword.CompanionAbility;
import mage.cards.Card;
import mage.constants.MultiplayerAttackOption;
import mage.constants.PhaseStep;
@ -18,6 +17,9 @@ import mage.players.Player;
import mage.watchers.common.CommanderInfoWatcher;
import mage.watchers.common.CommanderPlaysCountWatcher;
import java.util.Map;
import java.util.UUID;
public abstract class GameCommanderImpl extends GameImpl {
// private final Map<UUID, Cards> mulliganedCards = new HashMap<>();
@ -54,9 +56,13 @@ public abstract class GameCommanderImpl extends GameImpl {
if (player != null) {
// add new commanders
for (UUID id : player.getSideboard()) {
Card commander = this.getCard(id);
if (commander != null) {
addCommander(commander, player);
Card card = this.getCard(id);
if (card != null) {
// Check for companions. If it is the only card in the sideboard, it is the commander, not a companion.
if (player.getSideboard().size() > 1 && card.getAbilities(this).stream().anyMatch(ability -> ability instanceof CompanionAbility)) {
continue;
}
addCommander(card, player);
}
}

View file

@ -12,6 +12,7 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.PreventionEffectData;
import mage.abilities.effects.common.CopyEffect;
import mage.abilities.keyword.BestowAbility;
import mage.abilities.keyword.CompanionAbility;
import mage.abilities.keyword.MorphAbility;
import mage.abilities.keyword.TransformAbility;
import mage.abilities.mana.DelayedTriggeredManaAbility;
@ -929,6 +930,39 @@ public abstract class GameImpl implements Game, Serializable {
return;
}
// Handle companions
Map<Player, Card> playerCompanionMap = new HashMap<>();
for (Player player : state.getPlayers().values()) {
// Make a list of legal companions present in the sideboard
Set<Card> potentialCompanions = new HashSet<>();
for (Card card : player.getSideboard().getUniqueCards(this)) {
for (Ability ability : card.getAbilities(this)) {
if (ability instanceof CompanionAbility) {
CompanionAbility companionAbility = (CompanionAbility) ability;
if (companionAbility.isLegal(new HashSet<>(player.getLibrary().getCards(this)))) {
potentialCompanions.add(card);
break;
}
}
}
}
// Choose a companion from the list of legal companions
for (Card card : potentialCompanions) {
if (player.chooseUse(Outcome.Benefit, "Use " + card.getName() + " as your companion?", null, this)) {
playerCompanionMap.put(player, card);
break;
}
}
}
// Announce companions and set the companion effect
playerCompanionMap.forEach((player, companion) -> {
if (companion != null) {
this.informPlayers(player.getLogName() + " has chosen " + companion.getLogName() + " as their companion.");
this.getState().getCompanion().update(player.getName() + "'s companion", new CardsImpl(companion));
}
});
//20091005 - 103.1
if (!gameOptions.skipInitShuffling) { //don't shuffle in test mode for card injection on top of player's libraries
for (Player player : state.getPlayers().values()) {

View file

@ -59,6 +59,7 @@ public class GameState implements Serializable, Copyable<GameState> {
// revealed cards <Name, <Cards>>, will be reset if all players pass priority
private final Revealed revealed;
private final Map<UUID, LookedAt> lookedAt = new HashMap<>();
private final Revealed companion;
private DelayedTriggeredAbilities delayed;
private SpecialActions specialActions;
@ -106,6 +107,7 @@ public class GameState implements Serializable, Copyable<GameState> {
command = new Command();
exile = new Exile();
revealed = new Revealed();
companion = new Revealed();
battlefield = new Battlefield();
effects = new ContinuousEffects();
triggers = new TriggeredAbilities();
@ -123,6 +125,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.choosingPlayerId = state.choosingPlayerId;
this.revealed = state.revealed.copy();
this.lookedAt.putAll(state.lookedAt);
this.companion = state.companion.copy();
this.gameOver = state.gameOver;
this.paused = state.paused;
@ -473,6 +476,10 @@ public class GameState implements Serializable, Copyable<GameState> {
return lookedAt.get(playerId);
}
public Revealed getCompanion() {
return companion;
}
public void clearRevealed() {
revealed.clear();
}
@ -481,6 +488,10 @@ public class GameState implements Serializable, Copyable<GameState> {
lookedAt.clear();
}
public void clearCompanion() {
companion.clear();
}
public Turn getTurn() {
return turn;
}
@ -1067,6 +1078,7 @@ public class GameState implements Serializable, Copyable<GameState> {
isPlaneChase = false;
revealed.clear();
lookedAt.clear();
companion.clear();
turnNum = 0;
stepNum = 0;
extraTurn = false;

View file

@ -3446,6 +3446,15 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
// check to play companion cards
if (fromAll || fromZone == Zone.OUTSIDE) {
for (Cards companionCards : game.getState().getCompanion().values()) {
for (Card card : companionCards.getCards(game)) {
getPlayableFromNonHandCardAll(game, Zone.OUTSIDE, card, availableMana, playable);
}
}
}
// check if it's possible to play the top card of a library
if (fromAll || fromZone == Zone.LIBRARY) {
for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {