Merge branch 'cantblock' into 'master'

CantBlockBy as Static Ability

See merge request core-developers/forge!218
This commit is contained in:
Michael Kamensky
2018-02-18 08:05:59 +00:00
48 changed files with 291 additions and 203 deletions

View File

@@ -229,6 +229,10 @@ public class EffectEffect extends SpellAbilityEffect {
eff.copyChangedTextFrom(hostCard);
}
if (sa.hasParam("AtEOT")) {
registerDelayedTrigger(sa, sa.getParam("AtEOT"), Lists.newArrayList(hostCard));
}
// Duration
final String duration = sa.getParam("Duration");
if ((duration == null) || !duration.equals("Permanent")) {

View File

@@ -18,7 +18,6 @@
package forge.game.card;
import com.esotericsoftware.minlog.Log;
import com.google.common.base.Function;
import com.google.common.collect.*;
import forge.GameCommand;
import forge.ImageKeys;
@@ -1537,6 +1536,7 @@ public class Card extends GameEntity implements Comparable<Card> {
sbLong.append(keyword).append("\r\n");
} else if (keyword.startsWith("Strive") || keyword.startsWith("Escalate")
|| keyword.startsWith("ETBReplacement")
|| keyword.startsWith("CantBeBlockedBy ")
|| keyword.equals("CARDNAME enters the battlefield tapped.")
|| keyword.startsWith("UpkeepCost")) {
} else if (keyword.startsWith("Provoke") || keyword.startsWith("Ingest") || keyword.equals("Unleash")
@@ -1546,7 +1546,7 @@ public class Card extends GameEntity implements Comparable<Card> {
|| (keyword.startsWith("Split second") && !sb.toString().contains("Split second"))
|| keyword.equals("Suspend") // for the ones without amounnt
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|| keyword.equals("Totem armor")
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|| keyword.equals("Devoid")){
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Modular") || keyword.startsWith("Bloodthirst")
@@ -1595,12 +1595,9 @@ public class Card extends GameEntity implements Comparable<Card> {
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu")
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")) {
// keyword parsing takes care of adding a proper description
} else if (keyword.startsWith("CantBeBlockedBy")) {
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
sbLong.append(getName()).append(" can't be blocked ");
if (keyword.startsWith("CantBeBlockedByAmount"))
sbLong.append(getTextForKwCantBeBlockedByAmount(keyword));
else
sbLong.append(getTextForKwCantBeBlockedByType(keyword));
sbLong.append(getTextForKwCantBeBlockedByAmount(keyword));
} else if (keyword.startsWith("CantBlock")) {
sbLong.append(getName()).append(" can't block ");
if (keyword.contains("CardUID")) {
@@ -1657,112 +1654,6 @@ public class Card extends GameEntity implements Comparable<Card> {
return byClause + Lang.nounWithNumeral(cnt, isLT ? "or more creature" : "creature");
}
private static String getTextForKwCantBeBlockedByType(final String keyword) {
boolean negative = true;
final List<String> subs = Lists.newArrayList(TextUtil.split(keyword.split(" ", 2)[1], ','));
final List<List<String>> subsAnd = Lists.newArrayList();
final List<String> orClauses = Lists.newArrayList();
for (final String expession : subs) {
final List<String> parts = Lists.newArrayList(expession.split("[.+]"));
for (int p = 0; p < parts.size(); p++) {
final String part = parts.get(p);
if (part.equalsIgnoreCase("creature")) {
parts.remove(p--);
continue;
}
// based on suppossition that each expression has at least 1 predicate except 'creature'
negative &= part.contains("non") || part.contains("without");
}
subsAnd.add(parts);
}
final boolean allNegative = negative;
final String byClause = allNegative ? "except by " : "by ";
final Function<Pair<Boolean, String>, String> withToString = new Function<Pair<Boolean, String>, String>() {
@Override
public String apply(Pair<Boolean, String> inp) {
boolean useNon = inp.getKey() == allNegative;
return (useNon ? "*NO* " : "") + inp.getRight();
}
};
for (final List<String> andOperands : subsAnd) {
final List<Pair<Boolean, String>> prependedAdjectives = Lists.newArrayList();
final List<Pair<Boolean, String>> postponedAdjectives = Lists.newArrayList();
String creatures = null;
for (String part : andOperands) {
boolean positive = true;
if (part.startsWith("non")) {
part = part.substring(3);
positive = false;
}
if (part.startsWith("with")) {
positive = !part.startsWith("without");
postponedAdjectives.add(Pair.of(positive, part.substring(positive ? 4 : 7)));
} else if (part.startsWith("powerLEX")) {// Kraken of the Straits
postponedAdjectives.add(Pair.of(true, "power less than the number of islands you control"));
} else if (part.startsWith("power")) {
int kwLength = 5;
String opName = Expressions.operatorName(part.substring(kwLength, kwLength + 2));
String operand = part.substring(kwLength + 2);
postponedAdjectives.add(Pair.of(true, "power" + opName + operand));
} else if (CardType.isACreatureType(part)) {
if (creatures != null && CardType.isACreatureType(creatures)) { // e.g. Kor Castigator
creatures = StringUtils.capitalize(Lang.getPlural(part)) + creatures;
} else {
creatures = StringUtils.capitalize(Lang.getPlural(part)) + (creatures == null ? "" : " or " + creatures);
}
// Kor Castigator and other similar creatures with composite subtype Eldrazi Scion in their text
creatures = TextUtil.fastReplace(creatures, "Scions or Eldrazis", "Eldrazi Scions");
} else {
prependedAdjectives.add(Pair.of(positive, part.toLowerCase()));
}
}
StringBuilder sbShort = new StringBuilder();
if (allNegative) {
boolean isFirst = true;
for (Pair<Boolean, String> pre : prependedAdjectives) {
if (isFirst) isFirst = false;
else sbShort.append(" and/or ");
boolean useNon = pre.getKey() == allNegative;
if (useNon) sbShort.append("non-");
sbShort.append(pre.getValue()).append(" ").append(creatures == null ? "creatures" : creatures);
}
if (prependedAdjectives.isEmpty())
sbShort.append(creatures == null ? "creatures" : creatures);
if (!postponedAdjectives.isEmpty()) {
if (!prependedAdjectives.isEmpty()) {
sbShort.append(" and/or creatures");
}
sbShort.append(" with ");
sbShort.append(Lang.joinHomogenous(postponedAdjectives, withToString, allNegative ? "or" : "and"));
}
} else {
for (Pair<Boolean, String> pre : prependedAdjectives) {
boolean useNon = pre.getKey() == allNegative;
if (useNon) sbShort.append("non-");
sbShort.append(pre.getValue()).append(" ");
}
sbShort.append(creatures == null ? "creatures" : creatures);
if (!postponedAdjectives.isEmpty()) {
sbShort.append(" with ");
sbShort.append(Lang.joinHomogenous(postponedAdjectives, withToString, allNegative ? "or" : "and"));
}
}
orClauses.add(sbShort.toString());
}
return byClause + StringUtils.join(orClauses, " or ") + ".";
}
// get the text of the abilities of a card
public String getAbilityText() {
return getAbilityText(currentState);
@@ -1910,6 +1801,33 @@ public class Card extends GameEntity implements Comparable<Card> {
}
}
// CantBlockBy static abilities
if (game != null && isCreature() && isInZone(ZoneType.Battlefield)) {
for (final Card ca : game.getCardsIn(ZoneType.listValueOf("Battlefield,Command"))) {
if (equals(ca)) {
continue;
}
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (stAb.isSecondary() ||
!stAb.getParam("Mode").equals("CantBlockBy") ||
stAb.isSuppressed() || !stAb.checkConditions() ||
!stAb.hasParam("ValidAttacker")) {
continue;
}
final Card host = stAb.getHostCard();
if (isValid(stAb.getParam("ValidAttacker").split(","), host.getController(), host, null)) {
String desc = stAb.toString();
desc = TextUtil.fastReplace(desc, "CARDNAME", host.getName());
if (host.getEffectSource() != null) {
desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", host.getEffectSource().getName());
}
sb.append(desc);
sb.append("\r\n");
}
}
}
}
// NOTE:
if (sb.toString().contains(" (NOTE: ")) {
sb.insert(sb.indexOf("(NOTE: "), "\r\n");

View File

@@ -17,6 +17,7 @@
*/
package forge.game.card;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
@@ -51,10 +52,12 @@ import forge.game.trigger.TriggerHandler;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.Lang;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
@@ -2114,6 +2117,18 @@ public class CardFactoryUtil {
sa.setBlessing(true);
}
}
} else if (keyword.equals("Battle cry")) {
final String trig = "Mode$ Attacks | ValidCard$ Card.Self | TriggerZones$ Battlefield | Secondary$ True "
+ " | TriggerDescription$ " + keyword + " (" + inst.getReminderText() + ")";
String pumpStr = "DB$ PumpAll | ValidCards$ Creature.attacking+Other | NumAtt$ 1";
SpellAbility sa = AbilityFactory.getAbility(pumpStr, card);
sa.setIntrinsic(intrinsic);
final Trigger trigger = TriggerHandler.parseTrigger(trig, card, intrinsic);
trigger.setOverridingAbility(sa);
inst.addTrigger(trigger);
} else if (keyword.startsWith("Bushido")) {
final String[] k = keyword.split(" ", 2);
final String n = k[1];
@@ -4214,6 +4229,11 @@ public class CardFactoryUtil {
} else if (keyword.equals("Undaunted")) {
effect = "Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True"
+ "| Amount$ Undaunted | EffectZone$ All | Description$ Undaunted (" + inst.getReminderText() + ")";
} else if (keyword.startsWith("CantBeBlockedBy ")) {
final String[] k = keyword.split(" ", 2);
effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ " + k[1]
+ " | Description$ CARDNAME can't be blocked " + getTextForKwCantBeBlockedByType(keyword);
}
if (effect != null) {
@@ -4265,4 +4285,110 @@ public class CardFactoryUtil {
return as;
// ETBReplacementMove(sa.getHostCard(), null));
}
private static String getTextForKwCantBeBlockedByType(final String keyword) {
boolean negative = true;
final List<String> subs = Lists.newArrayList(TextUtil.split(keyword.split(" ", 2)[1], ','));
final List<List<String>> subsAnd = Lists.newArrayList();
final List<String> orClauses = Lists.newArrayList();
for (final String expession : subs) {
final List<String> parts = Lists.newArrayList(expession.split("[.+]"));
for (int p = 0; p < parts.size(); p++) {
final String part = parts.get(p);
if (part.equalsIgnoreCase("creature")) {
parts.remove(p--);
continue;
}
// based on suppossition that each expression has at least 1 predicate except 'creature'
negative &= part.contains("non") || part.contains("without");
}
subsAnd.add(parts);
}
final boolean allNegative = negative;
final String byClause = allNegative ? "except by " : "by ";
final Function<Pair<Boolean, String>, String> withToString = new Function<Pair<Boolean, String>, String>() {
@Override
public String apply(Pair<Boolean, String> inp) {
boolean useNon = inp.getKey() == allNegative;
return (useNon ? "*NO* " : "") + inp.getRight();
}
};
for (final List<String> andOperands : subsAnd) {
final List<Pair<Boolean, String>> prependedAdjectives = Lists.newArrayList();
final List<Pair<Boolean, String>> postponedAdjectives = Lists.newArrayList();
String creatures = null;
for (String part : andOperands) {
boolean positive = true;
if (part.startsWith("non")) {
part = part.substring(3);
positive = false;
}
if (part.startsWith("with")) {
positive = !part.startsWith("without");
postponedAdjectives.add(Pair.of(positive, part.substring(positive ? 4 : 7)));
} else if (part.startsWith("powerLEX")) {// Kraken of the Straits
postponedAdjectives.add(Pair.of(true, "power less than the number of islands you control"));
} else if (part.startsWith("power")) {
int kwLength = 5;
String opName = Expressions.operatorName(part.substring(kwLength, kwLength + 2));
String operand = part.substring(kwLength + 2);
postponedAdjectives.add(Pair.of(true, "power" + opName + operand));
} else if (CardType.isACreatureType(part)) {
if (creatures != null && CardType.isACreatureType(creatures)) { // e.g. Kor Castigator
creatures = StringUtils.capitalize(Lang.getPlural(part)) + creatures;
} else {
creatures = StringUtils.capitalize(Lang.getPlural(part)) + (creatures == null ? "" : " or " + creatures);
}
// Kor Castigator and other similar creatures with composite subtype Eldrazi Scion in their text
creatures = TextUtil.fastReplace(creatures, "Scions or Eldrazis", "Eldrazi Scions");
} else {
prependedAdjectives.add(Pair.of(positive, part.toLowerCase()));
}
}
StringBuilder sbShort = new StringBuilder();
if (allNegative) {
boolean isFirst = true;
for (Pair<Boolean, String> pre : prependedAdjectives) {
if (isFirst) isFirst = false;
else sbShort.append(" and/or ");
boolean useNon = pre.getKey() == allNegative;
if (useNon) sbShort.append("non-");
sbShort.append(pre.getValue()).append(" ").append(creatures == null ? "creatures" : creatures);
}
if (prependedAdjectives.isEmpty())
sbShort.append(creatures == null ? "creatures" : creatures);
if (!postponedAdjectives.isEmpty()) {
if (!prependedAdjectives.isEmpty()) {
sbShort.append(" and/or creatures");
}
sbShort.append(" with ");
sbShort.append(Lang.joinHomogenous(postponedAdjectives, withToString, allNegative ? "or" : "and"));
}
} else {
for (Pair<Boolean, String> pre : prependedAdjectives) {
boolean useNon = pre.getKey() == allNegative;
if (useNon) sbShort.append("non-");
sbShort.append(pre.getValue()).append(" ");
}
sbShort.append(creatures == null ? "creatures" : creatures);
if (!postponedAdjectives.isEmpty()) {
sbShort.append(" with ");
sbShort.append(Lang.joinHomogenous(postponedAdjectives, withToString, allNegative ? "or" : "and"));
}
}
orClauses.add(sbShort.toString());
}
return byClause + StringUtils.join(orClauses, " or ") + ".";
}
}

View File

@@ -955,6 +955,7 @@ public class CombatUtil {
return false;
}
final Game game = attacker.getGame();
if (!CombatUtil.canBlock(blocker, nextTurn)) {
return false;
}
@@ -995,32 +996,15 @@ public class CombatUtil {
return false;
}
for (KeywordInterface inst1 : attacker.getKeywords()) {
String k = inst1.getOriginal();
if (k.startsWith("CantBeBlockedBy ")) {
final String[] n = k.split(" ", 2);
final String[] restrictions = n[1].split(",");
if (blocker.isValid(restrictions, attacker.getController(), attacker, null)) {
boolean stillblock = false;
//Dragon Hunter check
if (n[1].contains("withoutReach") && blocker.hasStartOfKeyword("IfReach")) {
for (KeywordInterface inst2 : blocker.getKeywords()) {
String k2 = inst2.getOriginal();
if (k2.startsWith("IfReach")) {
String n2[] = k2.split(":");
if (attacker.getType().hasCreatureType(n2[1])) {
stillblock = true;
break;
}
}
}
}
if (!stillblock) {
return false;
}
// CantBlockBy static abilities
for (final Card ca : game.getCardsIn(ZoneType.listValueOf("Battlefield,Command"))) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (stAb.applyAbility("CantBlockBy", attacker, blocker)) {
return false;
}
}
}
for (KeywordInterface inst : blocker.getKeywords()) {
String keyword = inst.getOriginal();
if (keyword.startsWith("CantBlockCardUID")) {

View File

@@ -453,6 +453,8 @@ public class StaticAbility extends CardTraitBase implements Comparable<StaticAbi
if (mode.equals("CantAttack")) {
return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target);
} else if (mode.equals("CantBlockBy") && target instanceof Card) {
return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target);
}
return false;

View File

@@ -23,6 +23,7 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
@@ -89,6 +90,46 @@ public class StaticAbilityCantAttackBlock {
return true;
}
/**
* returns true if attacker can be blocked by blocker
* @param stAb
* @param attacker
* @param blocker
* @return boolean
*/
public static boolean applyCantBlockByAbility(final StaticAbility stAb, final Card attacker, final Card blocker) {
final Card host = stAb.getHostCard();
if (stAb.hasParam("ValidAttacker")) {
if (!attacker.isValid(stAb.getParam("ValidAttacker").split(","), host.getController(), host, null)) {
return false;
}
}
if (stAb.hasParam("ValidBlocker")) {
for (final String v : stAb.getParam("ValidBlocker").split(",")) {
if (blocker.isValid(v, host.getController(), host, null)) {
boolean stillblock = false;
//Dragon Hunter check
if (v.contains("withoutReach") && blocker.hasStartOfKeyword("IfReach")) {
for (KeywordInterface inst : blocker.getKeywords()) {
String k = inst.getOriginal();
if (k.startsWith("IfReach")) {
String n[] = k.split(":");
if (attacker.getType().hasCreatureType(n[1])) {
stillblock = true;
break;
}
}
}
}
if (!stillblock) {
return true;
}
}
}
}
return false;
}
/**
* TODO Write javadoc for this method.
*