Merge branch 'companion' into 'master'

Companion

See merge request core-developers/forge!2714
This commit is contained in:
Michael Kamensky
2020-05-08 03:43:23 +00:00
7 changed files with 166 additions and 13 deletions

View File

@@ -183,7 +183,7 @@ public class Match {
return myRemovedAnteCards;
}
private static void preparePlayerLibrary(Player player, final ZoneType zoneType, CardPool section, boolean canRandomFoil) {
private static void preparePlayerZone(Player player, final ZoneType zoneType, CardPool section, boolean canRandomFoil) {
PlayerZone library = player.getZone(zoneType);
List<Card> newLibrary = new ArrayList<>();
for (final Entry<PaperCard, Integer> stackOfCards : section) {
@@ -234,9 +234,9 @@ public class Match {
for (int i = 0; i < playersConditions.size(); i++) {
final Player player = players.get(i);
final RegisteredPlayer psc = playersConditions.get(i);
PlayerController person = player.getController();
if (canSideBoard) {
PlayerController person = player.getController();
if (sideboardProxy != null && person.isAI()) {
person = sideboardProxy;
}
@@ -270,16 +270,26 @@ public class Match {
}
}
preparePlayerLibrary(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil());
preparePlayerZone(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil());
if (myDeck.has(DeckSection.Sideboard)) {
preparePlayerLibrary(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil());
preparePlayerZone(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil());
// Assign Companion
Card companion = player.assignCompanion(game, person);
// Create an effect that lets you cast your companion from your sideboard
if (companion != null) {
PlayerZone commandZone = player.getZone(ZoneType.Command);
companion = game.getAction().moveTo(ZoneType.Command, companion, null);
commandZone.add(Player.createCompanionEffect(game, companion));
player.updateZoneForView(commandZone);
}
}
player.initVariantsZones(psc);
player.shuffle(null);
if (isFirstGame) {
Collection<? extends PaperCard> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck);
if (null != cardsComplained) {

View File

@@ -40,10 +40,7 @@ import forge.game.cost.Cost;
import forge.game.cost.CostSacrifice;
import forge.game.event.*;
import forge.game.event.GameEventCardDamaged.DamageType;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection;
import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordsChange;
import forge.game.keyword.*;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplaceMoved;
@@ -1904,6 +1901,9 @@ public class Card extends GameEntity implements Comparable<Card> {
String desc = "(As this Saga enters and after your draw step, "
+ " add a lore counter. Sacrifice after " + Strings.repeat("I", Integer.valueOf(k[1])) + ".)";
sbLong.append(desc);
} else if (inst.getKeyword().equals(Keyword.COMPANION)) {
sbLong.append("Companion - ");
sbLong.append(((Companion)inst).getDescription());
}
else {
if ((i != 0) && (sb.length() != 0)) {

View File

@@ -0,0 +1,44 @@
package forge.game.keyword;
public class Companion extends SimpleKeyword {
private String deckRestriction = null;
private String description = null;
private String specialRules = null;
public Companion() { }
@Override
protected void parse(String details) {
String[] splitString = details.split(":");
int descriptionIndex = splitString.length - 1;
if (splitString.length < 2) {
System.out.println("Did not parse a long enough value for Companion.");
return;
}
deckRestriction = splitString[0];
if (deckRestriction.equals("Special")) {
specialRules = splitString[1];
}
description = splitString[descriptionIndex];
}
public String getDeckRestriction() {
return deckRestriction;
}
public boolean hasSpecialRestriction() {
return specialRules != null;
}
public String getDescription() {
return description;
}
public String getSpecialRules() {
return specialRules;
}
}

View File

@@ -30,6 +30,7 @@ public enum Keyword {
CHAMPION("Champion", KeywordWithType.class, false, "When this enters the battlefield, sacrifice it unless you exile another %s you control. When this leaves the battlefield, that card returns to the battlefield."),
CHANGELING("Changeling", SimpleKeyword.class, true, "This card is every creature type."),
CIPHER("Cipher", SimpleKeyword.class, true, "Then you may exile this spell card encoded on a creature you control. Whenever that creature deals combat damage to a player, its controller may cast a copy of the encoded card without paying its mana cost."),
COMPANION("Companion", Companion.class, true, "Reveal your companion from outside the game if your deck meets the companion restriction."),
CONSPIRE("Conspire", SimpleKeyword.class, false, "As an additional cost to cast this spell, you may tap two untapped creatures you control that each share a color with it. If you do, copy it."),
CONVOKE("Convoke", SimpleKeyword.class, true, "Your creatures can help cast this spell. Each creature you tap while playing this spell reduces its cost by {1} or by one mana of that creature's color."),
CREW("Crew", KeywordWithAmount.class, false, "Tap any number of creatures you control with total power %1$d or more: This Vehicle becomes an artifact creature until end of turn."),

View File

@@ -23,6 +23,7 @@ import com.google.common.collect.*;
import forge.ImageKeys;
import forge.LobbyPlayer;
import forge.card.CardType;
import forge.card.MagicColor;
import forge.game.*;
import forge.game.ability.AbilityFactory;
@@ -33,11 +34,8 @@ import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.event.*;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection;
import forge.game.keyword.*;
import forge.game.keyword.KeywordCollection.KeywordCollectionView;
import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordsChange;
import forge.game.mana.ManaPool;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -1510,6 +1508,7 @@ public class Player extends GameEntity implements Comparable<Player> {
cl.addAll(getZone(ZoneType.Library).getCardsPlayerCanActivate(this));
if (includeCommandZone) {
cl.addAll(getZone(ZoneType.Command).getCardsPlayerCanActivate(this));
cl.addAll(getZone(ZoneType.Sideboard).getCardsPlayerCanActivate(this));
}
//External activatables from all opponents
@@ -2852,6 +2851,93 @@ public class Player extends GameEntity implements Comparable<Player> {
}
}
public Card assignCompanion(Game game, PlayerController player) {
List<Card> legalCompanions = Lists.newArrayList();
boolean uniqueNames = true;
Set<String> cardNames = new HashSet<>();
Set<CardType.CoreType> cardTypes = new HashSet<>();
for (final Card c : getCardsIn(ZoneType.Library)) {
if (uniqueNames) {
if (cardNames.contains(c.getName())) {
uniqueNames = false;
} else {
cardNames.add(c.getName());
}
}
if (!c.isLand()) {
for(CardType.CoreType type : c.getPaperCard().getRules().getType().getCoreTypes()) {
cardTypes.add(type);
}
}
}
int deckSize = getCardsIn(ZoneType.Library).size();
int minSize = game.getMatch().getRules().getGameType().getDeckFormat().getMainRange().getMinimum();
for (final Card c : getCardsIn(ZoneType.Sideboard)) {
for (KeywordInterface inst : c.getKeywords()) {
if (!(inst instanceof Companion)) {
continue;
}
Companion kwInstance = (Companion) inst;
if (kwInstance.hasSpecialRestriction()) {
String specialRules = kwInstance.getSpecialRules();
if (specialRules.equals("UniqueNames")) {
if (uniqueNames) {
legalCompanions.add(c);
}
} else if (specialRules.equals("DeckSizePlus20")) {
// +20 deck size to min deck size
if (deckSize >= minSize + 20) {
legalCompanions.add(c);
}
} else if (specialRules.equals("SharesCardType")) {
// Shares card type
if (cardTypes.size() == 1) {
legalCompanions.add(c);
}
}
} else{
String restriction = kwInstance.getDeckRestriction();
if (deckMatchesDeckRestriction(c, restriction)) {
legalCompanions.add(c);
}
}
}
}
if (legalCompanions.isEmpty()) {
return null;
}
CardCollectionView view = CardCollection.getView(legalCompanions);
return controller.chooseSingleEntityForEffect(view, null, "Choose a companion", true);
}
public boolean deckMatchesDeckRestriction(Card source, String restriction) {
for (final Card c : getCardsIn(ZoneType.Library)) {
if (!c.isValid(restriction, this, source, null)) {
return false;
}
}
return true;
}
public static DetachedCardEffect createCompanionEffect(Game game, Card companion) {
final String name = Lang.getPossesive(companion.getName()) + " Companion Effect";
DetachedCardEffect eff = new DetachedCardEffect(companion, name);
String mayBePlayedAbility = "Mode$ Continuous | EffectZone$ Command | MayPlay$ True | Affected$ Card.YouOwn+EffectSource | AffectedZone$ Command";
eff.addStaticAbility(mayBePlayedAbility);
// Probably remove this effect when the spell is cast via a static trigger
return eff;
}
public static DetachedCardEffect createCommanderEffect(Game game, Card commander) {
final String name = Lang.getPossesive(commander.getName()) + " Commander Effect";
DetachedCardEffect eff = new DetachedCardEffect(commander, name);

View File

@@ -464,6 +464,7 @@ public class PlayerView extends GameEntityView {
//update flashback zone when graveyard, library, or exile zones updated
switch (zone.getZoneType()) {
case Command:
case Graveyard:
case Library:
case Exile:

View File

@@ -0,0 +1,11 @@
Name:Gyruda, Doom of Depths
ManaCost:4 U/B U/B
Types:Legendary Creature Demon Kraken
PT:6/6
K:Companion:Card.cmcM20:Your starting deck contains only cards with even converted mana costs. (If this card is your chosen companion, you may cast it once from outside the game.)
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigMill | TriggerDescription$ When CARDNAME enters the battlefield, each player puts the top four cards of their library into their graveyard. Put a creature card with an even converted mana cost from those cards onto the battlefield under your control.
SVar:TrigMill:DB$ Mill | NumCards$ 4 | Defined$ Player | RememberMilled$ True | SubAbility$ DBChoose
SVar:DBChoose:DB$ ChooseCard | Defined$ You | ChoiceTitle$ Choose a creature card with an even converted mana cost | Choices$ Card.Creature+IsRemembered+cmcM20 | References$ X | ChoiceZone$ Graveyard | AILogic$ BestCard | Mandatory$ True | SubAbility$ DBChangeZone
SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | GainControl$ True | Defined$ ChosenCard | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:Companion — Your starting deck contains only cards with even converted mana costs. (If this card is your chosen companion, you may cast it once from outside the game.)\nWhen Gyruda, Doom of Depths enters the battlefield, each player puts the top four cards of their library into their graveyard. Put a creature card with an even converted mana cost from among those cards onto the battlefield under your control.