Merge branch 'partnerWith' into 'master'

Partner with

See merge request core-developers/forge!596
This commit is contained in:
Michael Kamensky
2018-05-27 14:54:26 +00:00
10 changed files with 131 additions and 35 deletions

View File

@@ -152,7 +152,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
return doReturnCommanderLogic(sa, aiPlayer); return doReturnCommanderLogic(sa, aiPlayer);
} }
if ("IfNotBuffed".equals(sa.getParam("AILogic"))) { if ("Always".equals(sa.getParam("AILogic"))) {
return true;
} else if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) { if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
return true; // debuffed by opponent's auras to the level that it becomes useless return true; // debuffed by opponent's auras to the level that it becomes useless
} }

View File

@@ -41,6 +41,7 @@ public final class CardRules implements ICardCharacteristics {
private CardAiHints aiHints; private CardAiHints aiHints;
private ColorSet colorIdentity; private ColorSet colorIdentity;
private String meldWith; private String meldWith;
private String partnerWith;
private CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) { private CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
splitType = altMode; splitType = altMode;
@@ -48,6 +49,7 @@ public final class CardRules implements ICardCharacteristics {
otherPart = faces[1]; otherPart = faces[1];
aiHints = cah; aiHints = cah;
meldWith = ""; meldWith = "";
partnerWith = "";
//calculate color identity //calculate color identity
byte colMask = calculateColorIdentity(mainPart); byte colMask = calculateColorIdentity(mainPart);
@@ -68,6 +70,7 @@ public final class CardRules implements ICardCharacteristics {
aiHints = newRules.aiHints; aiHints = newRules.aiHints;
colorIdentity = newRules.colorIdentity; colorIdentity = newRules.colorIdentity;
meldWith = newRules.meldWith; meldWith = newRules.meldWith;
partnerWith = newRules.partnerWith;
} }
private static byte calculateColorIdentity(final ICardFace face) { private static byte calculateColorIdentity(final ICardFace face) {
@@ -204,7 +207,7 @@ public final class CardRules implements ICardCharacteristics {
} }
public boolean canBePartnerCommander() { public boolean canBePartnerCommander() {
return canBeCommander() && Iterables.contains(mainPart.getKeywords(), "Partner"); return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty());
} }
public boolean canBeBrawlCommander() { public boolean canBeBrawlCommander() {
@@ -216,6 +219,10 @@ public final class CardRules implements ICardCharacteristics {
return meldWith; return meldWith;
} }
public String getParterWith() {
return partnerWith;
}
// vanguard card fields, they don't use sides. // vanguard card fields, they don't use sides.
private int deltaHand; private int deltaHand;
private int deltaLife; private int deltaLife;
@@ -262,6 +269,7 @@ public final class CardRules implements ICardCharacteristics {
private int curFace = 0; private int curFace = 0;
private CardSplitType altMode = CardSplitType.None; private CardSplitType altMode = CardSplitType.None;
private String meldWith = ""; private String meldWith = "";
private String partnerWith = "";
private String handLife = null; private String handLife = null;
private String normalizedName = ""; private String normalizedName = "";
@@ -291,6 +299,7 @@ public final class CardRules implements ICardCharacteristics {
this.hints = null; this.hints = null;
this.has = null; this.has = null;
this.meldWith = ""; this.meldWith = "";
this.partnerWith = "";
this.normalizedName = ""; this.normalizedName = "";
} }
@@ -307,6 +316,7 @@ public final class CardRules implements ICardCharacteristics {
result.setNormalizedName(this.normalizedName); result.setNormalizedName(this.normalizedName);
result.meldWith = this.meldWith; result.meldWith = this.meldWith;
result.partnerWith = this.partnerWith;
result.setDlUrls(pictureUrl); result.setDlUrls(pictureUrl);
if (StringUtils.isNotBlank(handLife)) if (StringUtils.isNotBlank(handLife))
result.setVanguardProperties(handLife); result.setVanguardProperties(handLife);
@@ -382,6 +392,9 @@ public final class CardRules implements ICardCharacteristics {
case 'K': case 'K':
if ("K".equals(key)) { if ("K".equals(key)) {
this.faces[this.curFace].addKeyword(value); this.faces[this.curFace].addKeyword(value);
if (value.startsWith("Partner:")) {
this.partnerWith = value.split(":")[1];
}
} }
break; break;
@@ -533,4 +546,8 @@ public final class CardRules implements ICardCharacteristics {
return result; return result;
} }
public boolean hasKeyword(final String k) {
return Iterables.contains(mainPart.getKeywords(), k);
}
} }

View File

@@ -10,7 +10,6 @@ import com.google.common.collect.Iterables;
import forge.util.ComparableOp; import forge.util.ComparableOp;
import forge.util.PredicateString; import forge.util.PredicateString;
import forge.util.PredicateString.StringOp;
/** /**
* Filtering conditions specific for CardRules class, defined here along with * Filtering conditions specific for CardRules class, defined here along with
@@ -558,6 +557,19 @@ public final class CardRulesPredicates {
} }
}; };
public static final Predicate<CardRules> CAN_BE_COMMANDER = new Predicate<CardRules>() {
@Override
public boolean apply(final CardRules subject) {
return subject.canBeCommander();
}
};
public static final Predicate<CardRules> CAN_BE_PARTNER_COMMANDER = new Predicate<CardRules>() {
@Override
public boolean apply(final CardRules subject) {
return subject.canBePartnerCommander();
}
};
public static final Predicate<CardRules> IS_PLANESWALKER = CardRulesPredicates.coreType(true, CardType.CoreType.Planeswalker); public static final Predicate<CardRules> IS_PLANESWALKER = CardRulesPredicates.coreType(true, CardType.CoreType.Planeswalker);
public static final Predicate<CardRules> IS_INSTANT = CardRulesPredicates.coreType(true, CardType.CoreType.Instant); public static final Predicate<CardRules> IS_INSTANT = CardRulesPredicates.coreType(true, CardType.CoreType.Instant);
public static final Predicate<CardRules> IS_SORCERY = CardRulesPredicates.coreType(true, CardType.CoreType.Sorcery); public static final Predicate<CardRules> IS_SORCERY = CardRulesPredicates.coreType(true, CardType.CoreType.Sorcery);
@@ -570,13 +582,10 @@ public final class CardRulesPredicates {
public static final Predicate<CardRules> IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy); public static final Predicate<CardRules> IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy);
public static final Predicate<CardRules> IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land); public static final Predicate<CardRules> IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land);
public static final Predicate<CardRules> IS_NON_CREATURE_SPELL = Predicates.not(Predicates.or(Presets.IS_CREATURE, Presets.IS_LAND)); public static final Predicate<CardRules> IS_NON_CREATURE_SPELL = Predicates.not(Predicates.or(Presets.IS_CREATURE, Presets.IS_LAND));
public static final Predicate<CardRules> CAN_BE_COMMANDER = Predicates.or(CardRulesPredicates.rules(StringOp.CONTAINS_IC, "can be your commander"),
Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY));
public static final Predicate<CardRules> CAN_BE_BRAWL_COMMANDER = Predicates.or(Presets.IS_PLANESWALKER, public static final Predicate<CardRules> CAN_BE_BRAWL_COMMANDER = Predicates.or(Presets.IS_PLANESWALKER,
Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY)); Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY));
/** The Constant IS_NONCREATURE_SPELL_FOR_GENERATOR. **/ /** The Constant IS_NONCREATURE_SPELL_FOR_GENERATOR. **/
@SuppressWarnings("unchecked")
public static final Predicate<CardRules> IS_NONCREATURE_SPELL_FOR_GENERATOR = com.google.common.base.Predicates public static final Predicate<CardRules> IS_NONCREATURE_SPELL_FOR_GENERATOR = com.google.common.base.Predicates
.or(Presets.IS_SORCERY, Presets.IS_INSTANT, Presets.IS_PLANESWALKER, Presets.IS_ENCHANTMENT, .or(Presets.IS_SORCERY, Presets.IS_INSTANT, Presets.IS_PLANESWALKER, Presets.IS_ENCHANTMENT,
Predicates.and(Presets.IS_ARTIFACT, Predicates.not(Presets.IS_CREATURE))); Predicates.and(Presets.IS_ARTIFACT, Predicates.not(Presets.IS_CREATURE)));

View File

@@ -248,41 +248,33 @@ public enum DeckFormat {
return "too many commanders"; return "too many commanders";
} }
// Bring values up to 100
min++;
max++;
byte cmdCI = 0; byte cmdCI = 0;
Boolean hasPartner = null;
for (PaperCard pc : commanders) { for (PaperCard pc : commanders) {
// For each commander decrement size by 1 (99 for 1, 98 for 2)
min--;
max--;
if (!isLegalCommander(pc.getRules())) { if (!isLegalCommander(pc.getRules())) {
return "has an illegal commander"; return "has an illegal commander";
} }
if (hasPartner != null && !hasPartner) {
return "has an illegal commander partnership";
}
boolean isPartner = false;
for(String s : pc.getRules().getMainPart().getKeywords()) {
if (s.equals("Partner")) {
isPartner = true;
break;
}
}
if (hasPartner == null) {
hasPartner = isPartner;
} else if (!isPartner) {
return "has an illegal commander partnership";
}
cmdCI |= pc.getRules().getColorIdentity().getColor(); cmdCI |= pc.getRules().getColorIdentity().getColor();
} }
// special check for Partner
if (commanders.size() == 2) {
// two commander = 98 cards
min--;
max--;
PaperCard a = commanders.get(0);
PaperCard b = commanders.get(1);
if (a.getRules().hasKeyword("Partner") && b.getRules().hasKeyword("Partner")) {
// normal partner commander
} else if (a.getName().equals(b.getRules().getParterWith())
&& b.getName().equals(a.getRules().getParterWith())) {
// paired partner commander
} else {
return "has an illegal commander partnership";
}
}
final List<PaperCard> erroneousCI = new ArrayList<PaperCard>(); final List<PaperCard> erroneousCI = new ArrayList<PaperCard>();
Set<String> basicLandNames = new HashSet<>(); Set<String> basicLandNames = new HashSet<>();
@@ -521,6 +513,8 @@ public enum DeckFormat {
for (final PaperCard p : commanders) { for (final PaperCard p : commanders) {
cmdCI |= p.getRules().getColorIdentity().getColor(); cmdCI |= p.getRules().getColorIdentity().getColor();
} }
return Predicates.compose(Predicates.or(CardRulesPredicates.hasColorIdentity(cmdCI), CardRulesPredicates.hasKeyword("Partner")), PaperCard.FN_GET_RULES); // TODO : check commander what kind of Partner it needs
return Predicates.compose(Predicates.or(CardRulesPredicates.hasColorIdentity(cmdCI),
CardRulesPredicates.Presets.CAN_BE_PARTNER_COMMANDER), PaperCard.FN_GET_RULES);
} }
} }

View File

@@ -1561,6 +1561,9 @@ public class Card extends GameEntity implements Comparable<Card> {
|| keyword.equals("Totem armor") || keyword.equals("Battle cry") || keyword.equals("Totem armor") || keyword.equals("Battle cry")
|| keyword.equals("Devoid")){ || keyword.equals("Devoid")){
sbLong.append(keyword + " (" + inst.getReminderText() + ")"); sbLong.append(keyword + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Partner:")) {
final String[] k = keyword.split(":");
sbLong.append("Partner with " + k[1] + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Modular") || keyword.startsWith("Bloodthirst") } else if (keyword.startsWith("Modular") || keyword.startsWith("Bloodthirst")
|| keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift") || keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift")
|| keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb") || keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")

View File

@@ -2711,6 +2711,23 @@ public class CardFactoryUtil {
card.setSVar("MyriadCleanup", dbString4); card.setSVar("MyriadCleanup", dbString4);
inst.addTrigger(parsedTrigger); inst.addTrigger(parsedTrigger);
} else if (keyword.startsWith("Partner:")) {
// Partner With
final String[] k = keyword.split(":");
final String trigStr = "Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield " +
"| ValidCard$ Card.Self | Secondary$ True " +
"| TriggerDescription$ Partner with " + k[1] + " (" + inst.getReminderText() + ")";
// replace , for ; in the ChangeZone
k[1] = k[1].replace(",", ";");
final String effect = "DB$ ChangeZone | ValidTgts$ Player | TgtPrompt$ Select target player" +
" | Origin$ Library | Destination$ Hand | ChangeType$ Card.named" + k[1] +
" | ChangeNum$ 1 | Hidden$ True | Chooser$ Targeted | Optional$ Targeted | AILogic$ Always";
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
inst.addTrigger(trigger);
} else if (keyword.equals("Persist")) { } else if (keyword.equals("Persist")) {
final String trigStr = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | OncePerEffect$ True " + final String trigStr = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | OncePerEffect$ True " +
" | ValidCard$ Card.Self+counters_EQ0_M1M1 | Secondary$ True" + " | ValidCard$ Card.Self+counters_EQ0_M1M1 | Secondary$ True" +

View File

@@ -100,7 +100,7 @@ public enum Keyword {
NINJUTSU(KeywordWithCost.class, false, "%s, Return an unblocked attacker you control to hand: Put this card onto the battlefield from your hand tapped and attacking."), NINJUTSU(KeywordWithCost.class, false, "%s, Return an unblocked attacker you control to hand: Put this card onto the battlefield from your hand tapped and attacking."),
OUTLAST(KeywordWithCost.class, false, "%s, {T}: Put a +1/+1 counter on this creature. Outlast only as a sorcery."), OUTLAST(KeywordWithCost.class, false, "%s, {T}: Put a +1/+1 counter on this creature. Outlast only as a sorcery."),
OFFERING(KeywordWithType.class, false, "You may cast this card any time you could cast an instant by sacrificing a %1$s and paying the difference in mana costs between this and the sacrificed %1$s. Mana cost includes color."), OFFERING(KeywordWithType.class, false, "You may cast this card any time you could cast an instant by sacrificing a %1$s and paying the difference in mana costs between this and the sacrificed %1$s. Mana cost includes color."),
PARTNER(SimpleKeyword.class, true, "You can have two commanders if both have partner."), PARTNER(Partner.class, true, "You can have two commanders if both have partner."),
PERSIST(SimpleKeyword.class, true, "When this creature dies, if it had no -1/-1 counters on it, return it to the battlefield under its owner's control with a -1/-1 counter on it."), PERSIST(SimpleKeyword.class, true, "When this creature dies, if it had no -1/-1 counters on it, return it to the battlefield under its owner's control with a -1/-1 counter on it."),
PHASING(SimpleKeyword.class, true, "This phases in or out before you untap during each of your untap steps. While it's phased out, it's treated as though it doesn't exist."), PHASING(SimpleKeyword.class, true, "This phases in or out before you untap during each of your untap steps. While it's phased out, it's treated as though it doesn't exist."),
POISONOUS(KeywordWithAmount.class, false, "Whenever this creature deals combat damage to a player, that player gets {%d:poison counter}."), POISONOUS(KeywordWithAmount.class, false, "Whenever this creature deals combat damage to a player, that player gets {%d:poison counter}."),

View File

@@ -0,0 +1,35 @@
package forge.game.keyword;
public class Partner extends SimpleKeyword {
private String with = null;
public Partner() {
}
@Override
protected void parse(String details) {
if (!details.isEmpty()) {
if (details.contains(":")) {
with = details.split(":")[1];
} else {
with = details;
}
}
}
@Override
protected String formatReminderText(String reminderText) {
if (with == null) {
return reminderText;
} else {
final StringBuilder sb = new StringBuilder();
sb.append("When this creature enters the battlefield, target player may put ");
sb.append(with);
sb.append(" into their hand from their library, then shuffle.");
return sb.toString();
}
}
}

View File

@@ -0,0 +1,10 @@
Name:Khorvath Brightflame
ManaCost:5 R
Types:Legendary Creature Dragon
PT:3/4
K:Partner:Sylvia Brightspear:Sylvia
K:Flying
K:Haste
S:Mode$ Continuous | Affected$ Knight.YourTeamCtrl | AddKeyword$ Flying & Haste | Description$ Knights your team controls have flying and haste.
SVar:Picture:http://www.wizards.com/global/images/magic/general/khorvath_brightflame.jpg
Oracle:Partner with Sylvia Brightspear (When this creature enters the battlefield, target player may put Sylvia into their hand from their library, then shuffle.)\nFlying, haste\nKnights your team controls have flying and haste.

View File

@@ -0,0 +1,9 @@
Name:Sylvia Brightspear
ManaCost:2 W
Types:Legendary Creature Human Knight
PT:2/2
K:Partner:Khorvath Brightflame:Khorvath
K:Double Strike
S:Mode$ Continuous | Affected$ Dragon.YourTeamCtrl | AddKeyword$ Double Strike | Description$ Dragons your team controls have double strike.
SVar:Picture:http://www.wizards.com/global/images/magic/general/sylvia_brightspear.jpg
Oracle:Partner with Khorvath Brightflame (When this creature enters the battlefield, target player may put Khorvath into their hand from their library, then shuffle.)\nDouble strike\nDragons your team controls have double strike.