Merge branch 'master' into '1111-dismantle-rework'

# Conflicts:
#   forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java
This commit is contained in:
Hans Mackowiak
2022-03-08 08:18:34 +00:00
69 changed files with 448 additions and 378 deletions

View File

@@ -50,7 +50,6 @@ import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
@@ -333,25 +332,6 @@ public class AiAttackController {
return attackers;
}
// no need to block if an effect is in play which untaps all creatures (pseudo-Vigilance akin to
// Awakening or Prophet of Kruphix)
for (Card card : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
boolean untapsEachTurn = card.hasSVar("UntapsEachTurn");
boolean untapsEachOtherTurn = card.hasSVar("UntapsEachOtherPlayerTurn");
if (untapsEachTurn || untapsEachOtherTurn) {
String affected = untapsEachTurn ? card.getSVar("UntapsEachTurn")
: card.getSVar("UntapsEachOtherPlayerTurn");
for (String aff : TextUtil.split(affected, ',')) {
if (aff.equals("Creature")
&& (untapsEachTurn || (untapsEachOtherTurn && ai.equals(card.getController())))) {
return attackers;
}
}
}
}
List<Card> opponentsAttackers = new ArrayList<>(oppList);
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() {
@Override
@@ -370,7 +350,9 @@ public class AiAttackController {
}
continue;
}
if (c.hasKeyword(Keyword.VIGILANCE)) {
// no need to block if an effect is in play which untaps all creatures
// (pseudo-Vigilance akin to Awakening or or Prophet of Kruphix)
if (c.hasKeyword(Keyword.VIGILANCE) || ComputerUtilCard.willUntap(ai, c)) {
vigilantes.add(c);
notNeededAsBlockers.remove(c); // they will be re-added later
if (canBlockAnAttacker(c, opponentsAttackers, false)) {
@@ -392,7 +374,7 @@ public class AiAttackController {
}
}
int blockersStillNeeded = blockersNeeded - fixedBlockers;
blockersStillNeeded = Math.min(blockersNeeded, list.size());
blockersStillNeeded = Math.min(blockersStillNeeded, list.size());
for (int i = 0; i < blockersStillNeeded; i++) {
notNeededAsBlockers.remove(list.get(i));
}
@@ -850,7 +832,7 @@ public class AiAttackController {
return aiAggression;
}
if (simAI && ai.isCardInPlay("Reconnaissance")) {
if (simAI && ComputerUtilCard.isNonDisabledCardInPlay(ai, "Reconnaissance")) {
for (Card attacker : attackersLeft) {
if (canAttackWrapper(attacker, defender)) {
// simulation will decide if attacker stays in combat based on blocks

View File

@@ -927,30 +927,8 @@ public class AiController {
return AiPlayDecision.WillPlay;
}
public boolean isNonDisabledCardInPlay(final String cardName) {
for (Card card : player.getCardsIn(ZoneType.Battlefield)) {
if (card.getName().equals(cardName)) {
// TODO - Better logic to determine if a permanent is disabled by local effects
// currently assuming any permanent enchanted by another player
// is disabled and a second copy is necessary
// will need actual logic that determines if the enchantment is able
// to disable the permanent or it's still functional and a duplicate is unneeded.
boolean disabledByEnemy = false;
for (Card card2 : card.getEnchantedBy()) {
if (card2.getOwner() != player) {
disabledByEnemy = true;
}
}
if (!disabledByEnemy) {
return true;
}
}
}
return false;
}
private AiPlayDecision canPlaySpellBasic(final Card card, final SpellAbility sa) {
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
if ("True".equals(card.getSVar("NonStackingEffect")) && ComputerUtilCard.isNonDisabledCardInPlay(player, card.getName())) {
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
}
@@ -1170,12 +1148,11 @@ public class AiController {
public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa) {
return getCardsToDiscard(numDiscard, uTypes, sa, CardCollection.EMPTY);
}
public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa, final CardCollectionView exclude) {
boolean noFiltering = (sa != null) && "DiscardCMCX".equals(sa.getParam("AILogic")); // list AI logic for which filtering is taken care of elsewhere
boolean noFiltering = sa != null && "DiscardCMCX".equals(sa.getParam("AILogic")); // list AI logic for which filtering is taken care of elsewhere
CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand));
hand.removeAll(exclude);
if ((uTypes != null) && (sa != null) && !noFiltering) {
if (uTypes != null && sa != null && !noFiltering) {
hand = CardLists.getValidCards(hand, uTypes, sa.getActivatingPlayer(), sa.getHostCard(), sa);
}
return getCardsToDiscard(numDiscard, numDiscard, hand, sa);
@@ -1214,7 +1191,7 @@ public class AiController {
if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) {
CardCollection discards = new CardCollection();
final CardCollectionView inHand = player.getCardsIn(ZoneType.Hand);
final int numLandsOTB = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
final int numLandsOTB = CardLists.count(player.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS);
int numOppInHand = 0;
for (Player p : player.getGame().getPlayers()) {
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
@@ -1274,7 +1251,7 @@ public class AiController {
if (validCards.isEmpty()) {
continue;
}
final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS);
final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(validCards, CardPredicates.Presets.LANDS);
final int numLandsInHand = landsInHand.size();
@@ -2361,7 +2338,7 @@ public class AiController {
return Iterables.getFirst(doubleLife, null);
}
} else if (mode.equals(ReplacementType.DamageDone)) {
List<ReplacementEffect> prevention = filterList(list, CardTraitPredicates.hasParam("Prevention"));
List<ReplacementEffect> prevention = filterList(list, CardTraitPredicates.hasParam("Prevent"));
// TODO when Protection is done as ReplacementEffect do them
// before normal prevention

View File

@@ -487,11 +487,11 @@ public class ComputerUtil {
// Discard lands
final CardCollection landsInHand = CardLists.getType(typeList, "Land");
if (!landsInHand.isEmpty()) {
final CardCollection landsInPlay = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Land");
final int numLandsInPlay = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, CardPredicates.Accessors.fnGetCmc));
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)) {
if (numLandsInPlay >= highestCMC
|| (numLandsInPlay + landsInHand.size() > 6 && landsInHand.size() > 1)) {
// Don't need more land.
return ComputerUtilCard.getWorstLand(landsInHand);
}
@@ -1190,7 +1190,7 @@ public class ComputerUtil {
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS);
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, CardPredicates.Accessors.fnGetCmc));

View File

@@ -63,6 +63,7 @@ import forge.item.PaperCard;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.TextUtil;
public class ComputerUtilCard {
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
@@ -1816,6 +1817,9 @@ public class ComputerUtilCard {
}
public static boolean hasActiveUndyingOrPersist(final Card c) {
if (c.isToken()) {
return false;
}
if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterEnumType.P1P1) == 0) {
return true;
}
@@ -1982,6 +1986,50 @@ public class ComputerUtilCard {
return totalCost;
}
public static boolean willUntap(Player ai, Card tapped) {
// TODO use AiLogic on trigger in case card loses all abilities
// if it's from a static need to also check canUntap
for (Card card : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
boolean untapsEachTurn = card.hasSVar("UntapsEachTurn");
boolean untapsEachOtherTurn = card.hasSVar("UntapsEachOtherPlayerTurn");
if (untapsEachTurn || untapsEachOtherTurn) {
String affected = untapsEachTurn ? card.getSVar("UntapsEachTurn")
: card.getSVar("UntapsEachOtherPlayerTurn");
for (String aff : TextUtil.split(affected, ',')) {
if (tapped.isValid(aff, ai, tapped, null)
&& (untapsEachTurn || (untapsEachOtherTurn && ai.equals(card.getController())))) {
return true;
}
}
}
}
return false;
}
public static boolean isNonDisabledCardInPlay(final Player ai, final String cardName) {
for (Card card : ai.getCardsIn(ZoneType.Battlefield)) {
if (card.getName().equals(cardName)) {
// TODO - Better logic to determine if a permanent is disabled by local effects
// currently assuming any permanent enchanted by another player
// is disabled and a second copy is necessary
// will need actual logic that determines if the enchantment is able
// to disable the permanent or it's still functional and a duplicate is unneeded.
boolean disabledByEnemy = false;
for (Card card2 : card.getEnchantedBy()) {
if (card2.getOwner() != ai) {
disabledByEnemy = true;
}
}
if (!disabledByEnemy) {
return true;
}
}
}
return false;
}
// Determine if the AI has an AI:RemoveDeck:All or an AI:RemoveDeck:Random hint specified.
// Includes a NPE guard on getRules() which might otherwise be tripped on some cards (e.g. tokens).
public static boolean isCardRemAIDeck(final Card card) {

View File

@@ -11,13 +11,6 @@ import forge.game.keyword.Keyword;
import forge.game.spellability.SpellAbility;
public class CreatureEvaluator implements Function<Card, Integer> {
protected int getEffectivePower(final Card c) {
return c.getNetCombatDamage();
}
protected int getEffectiveToughness(final Card c) {
return c.getNetToughness();
}
@Override
public Integer apply(Card c) {
return evaluateCreature(c);
@@ -31,8 +24,8 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (!c.isToken()) {
value += addValue(20, "non-token"); // tokens should be worth less than actual cards
}
int power = getEffectivePower(c);
final int toughness = getEffectiveToughness(c);
int power = c.getNetCombatDamage();
final int toughness = c.getNetToughness();
// TODO replace with ReplacementEffect checks
if (c.hasKeyword("Prevent all combat damage that would be dealt by CARDNAME.")
@@ -45,6 +38,11 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (considerPT) {
value += addValue(power * 15, "power");
value += addValue(toughness * 10, "toughness: " + toughness);
// because backside is always stronger the potential makes it better than a single faced card
if (c.hasKeyword(Keyword.DAYBOUND)) {
value += addValue(power * 10, "transforming");
}
}
if (considerCMC) {
value += addValue(c.getCMC() * 5, "cmc");
@@ -72,8 +70,8 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword(Keyword.MENACE)) {
value += addValue(power * 4, "menace");
}
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
value += addValue(power * 3, "block-restrict");
if (c.hasKeyword(Keyword.SKULK)) {
value += addValue(power * 3, "skulk");
}
}
@@ -106,19 +104,18 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(c.getKeywordMagnitude(Keyword.AFFLICT) * 5, "afflict");
}
value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido");
value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking");
value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted");
value += addValue(c.getKeywordMagnitude(Keyword.ANNIHILATOR) * 50, "eldrazi");
value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb");
// Keywords that may produce temporary or permanent buffs over time
if (c.hasKeyword(Keyword.PROWESS)) {
value += addValue(5, "prowess");
}
if (c.hasKeyword(Keyword.OUTLAST)) {
value += addValue(10, "outlast");
}
value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido");
value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking");
value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted");
value += addValue(c.getAmountOfKeyword(Keyword.MELEE) * 18, "melee");
value += addValue(c.getAmountOfKeyword(Keyword.PROWESS) * 5, "prowess");
// Defensive Keywords
if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) {
@@ -141,18 +138,41 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(35, "hexproof");
} else if (c.hasKeyword(Keyword.SHROUD)) {
value += addValue(30, "shroud");
} else if (c.hasKeyword(Keyword.WARD)) {
value += addValue(10, "ward");
}
if (c.hasKeyword(Keyword.PROTECTION)) {
value += addValue(20, "protection");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
// paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) {
value += addValue(14, "paired");
}
if (c.hasEncodedCard()) {
value += addValue(24, "encoded");
}
if (ComputerUtilCard.hasActiveUndyingOrPersist(c)) {
value += addValue(30, "revive");
}
// Bad keywords
if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) {
value -= subValue((power * 9) + 40, "defender");
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
value -= subValue(40, "sac-end");
}
if (c.hasKeyword("CARDNAME can't block.")) {
if (c.hasKeyword("CARDNAME can't attack or block.")) {
value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
} else if (c.hasKeyword("CARDNAME can't block.")) {
value -= subValue(10, "cant-block");
} else if (c.hasKeyword("CARDNAME attacks each combat if able.")) {
value -= subValue(10, "must-attack");
@@ -165,10 +185,18 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasSVar("DestroyWhenDamaged")) {
value -= subValue((toughness - 1) * 9, "dies-to-dmg");
}
if (c.hasKeyword("CARDNAME can't attack or block.")) {
value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
if (c.getSVar("Targeting").equals("Dies")) {
value -= subValue(25, "dies");
}
if (c.isUntapped()) {
value += addValue(1, "untapped");
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
@@ -185,40 +213,19 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
value -= subValue(10, "echo-unpaid");
}
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
value -= subValue(20, "upkeep-dmg");
}
if (c.hasKeyword(Keyword.FADING)) {
value -= subValue(20, "fading");
}
if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20, "vanishing");
}
if (c.getSVar("Targeting").equals("Dies")) {
value -= subValue(25, "dies");
if (c.hasKeyword(Keyword.PHASING)) {
value -= subValue(10, "phasing");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.isUntapped()) {
value += addValue(1, "untapped");
}
// paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) {
value += addValue(14, "paired");
}
if (!c.hasEncodedCard()) {
value += addValue(24, "encoded");
// TODO no longer a KW
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
value -= subValue(20, "upkeep-dmg");
}
// card-specific evaluation modifier
@@ -240,7 +247,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
// Electrostatic Pummeler, can be expanded for similar cards
int initPower = getEffectivePower(sa.getHostCard());
int initPower = sa.getHostCard().getNetPower();
int pumpedPower = initPower;
int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY);
if (energy > 0) {

View File

@@ -462,7 +462,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
int landsOTB = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
if (!p.isOpponentOf(player)) {
if (landsOTB <= 2) {
@@ -611,7 +611,7 @@ public class PlayerControllerAi extends PlayerController {
hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.filter(toReturn, Presets.LANDS).size();
int numLandsInHand = landsInHand.size() - CardLists.count(toReturn, Presets.LANDS);
// If we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {

View File

@@ -1558,8 +1558,7 @@ public class AttachAi extends SpellAbilityAi {
final boolean evasive = keyword.equals("Unblockable") || keyword.equals("Fear")
|| keyword.equals("Intimidate") || keyword.equals("Shadow")
|| keyword.equals("Flying") || keyword.equals("Horsemanship")
|| keyword.endsWith("walk") || keyword.startsWith("CantBeBlockedBy")
|| keyword.equals("All creatures able to block CARDNAME do so.");
|| keyword.endsWith("walk") || keyword.equals("All creatures able to block CARDNAME do so.");
// give evasive keywords to creatures that can attack and deal damage
boolean canBeBlocked = false;

View File

@@ -122,10 +122,7 @@ public class ControlGainAi extends SpellAbilityAi {
return true;
}
CardCollection list = new CardCollection();
for (Player pl : opponents) {
list.addAll(pl.getCardsIn(ZoneType.Battlefield));
}
CardCollection list = opponents.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, tgt.getValidTgts(), sa.getActivatingPlayer(), sa.getHostCard(), sa);
@@ -325,7 +322,7 @@ public class ControlGainAi extends SpellAbilityAi {
} else {
return this.canPlayAI(ai, sa);
}
} // pumpDrawbackAI()
}
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {

View File

@@ -124,7 +124,7 @@ public class CounterAi extends SpellAbilityAi {
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of the time
if (!SpellAbilityAi.playReusable(ai,sa)) {
if (!SpellAbilityAi.playReusable(ai, sa)) {
return false;
}
}

View File

@@ -356,7 +356,8 @@ public class DamageDealAi extends DamageAiBase {
return c.getSVar("Targeting").equals("Dies")
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d)
&& !ComputerUtil.canRegenerate(ai, c)
&& !(c.getSVar("SacMe").length() > 0);
&& !(c.getSVar("SacMe").length() > 0)
&& !ComputerUtilCard.hasActiveUndyingOrPersist(c);
}
});
@@ -489,10 +490,7 @@ public class DamageDealAi extends DamageAiBase {
*/
private boolean damageTargetAI(final Player ai, final SpellAbility saMe, final int dmg, final boolean immediately) {
final TargetRestrictions tgt = saMe.getTargetRestrictions();
if ("Atarka's Command".equals(ComputerUtilAbility.getAbilitySourceName(saMe))) {
// playReusable in damageChooseNontargeted wrongly assumes that CharmEffect options are re-usable
return shouldTgtP(ai, saMe, dmg, false);
}
if (tgt == null) {
return damageChooseNontargeted(ai, saMe, dmg);
}
@@ -834,6 +832,10 @@ public class DamageDealAi extends DamageAiBase {
}
}
}
if ("Atarka's Command".equals(ComputerUtilAbility.getAbilitySourceName(saMe))) {
// playReusable wrongly assumes that CharmEffect options are re-usable
return positive;
}
if (!positive && !(saMe instanceof AbilitySub)) {
return false;
}

View File

@@ -208,7 +208,7 @@ public class DestroyAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return (c.getShieldCount() == 0 && !ComputerUtil.canRegenerate(ai, c));
return c.getShieldCount() == 0 && !ComputerUtil.canRegenerate(ai, c);
}
});
}
@@ -440,8 +440,8 @@ public class DestroyAi extends SpellAbilityAi {
boolean nonBasicTgt = !tgtLand.isBasicLand();
// Try not to lose tempo too much and not to mana-screw yourself when considering this logic
int numLandsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
int numLandsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
int numLandsInHand = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
// If the opponent skipped a land drop, consider not looking at having the extra land in hand if the profile allows it
boolean isHighPriority = highPriorityIfNoLandDrop && oppSkippedLandDrop;

View File

@@ -93,7 +93,7 @@ public class DiscardAi extends SpellAbilityAi {
if (sa.hasParam("AnyNumber")) {
if ("DiscardUncastableAndExcess".equals(aiLogic)) {
final CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
final int numLandsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
final int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS);
int numDiscard = 0;
int numOppInHand = 0;
for (Player p : ai.getGame().getPlayers()) {
@@ -143,7 +143,7 @@ public class DiscardAi extends SpellAbilityAi {
// some other variables here, like handsize vs. maxHandSize
return randomReturn;
} // discardCanPlayAI()
}
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
final PlayerCollection opps = ai.getOpponents();
@@ -164,7 +164,7 @@ public class DiscardAi extends SpellAbilityAi {
}
}
return false;
} // discardTargetAI()
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
@@ -198,7 +198,7 @@ public class DiscardAi extends SpellAbilityAi {
}
return true;
} // discardTrigger()
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
@@ -209,10 +209,10 @@ public class DiscardAi extends SpellAbilityAi {
}
// TODO: check for some extra things
return true;
} // discardCheckDrawbackAI()
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
if ( mode == PlayerActionConfirmMode.Random ) {
if (mode == PlayerActionConfirmMode.Random) {
// TODO For now AI will always discard Random used currently with: Balduvian Horde and similar cards
return true;
}

View File

@@ -8,6 +8,7 @@ import com.google.common.base.Predicates;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
@@ -16,7 +17,6 @@ import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.card.mana.ManaCost;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -65,7 +65,7 @@ public class ManaEffectAi extends SpellAbilityAi {
*/
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && canRampPool(ai, sa.getHostCard())) {
if (ph.is(PhaseType.END_OF_TURN) && (ph.getNextTurn() == ai || ComputerUtilCard.willUntap(ai, sa.getHostCard())) && canRampPool(ai, sa.getHostCard())) {
return true;
}
if (!ph.is(PhaseType.MAIN2) || !ComputerUtil.activateForCost(sa, ai)) {
@@ -87,7 +87,7 @@ public class ManaEffectAi extends SpellAbilityAi {
if (logic.startsWith("ManaRitual")) {
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
} else if ("AtOppEOT".equals(logic)) {
return !ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.manaBurn) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
return !ai.getManaPool().hasBurn() && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
@@ -106,7 +106,7 @@ public class ManaEffectAi extends SpellAbilityAi {
PhaseHandler ph = ai.getGame().getPhaseHandler();
boolean moreManaNextTurn = false;
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && canRampPool(ai, sa.getHostCard())) {
if (ph.is(PhaseType.END_OF_TURN) && (ph.getNextTurn() == ai || ComputerUtilCard.willUntap(ai, sa.getHostCard())) && canRampPool(ai, sa.getHostCard())) {
moreManaNextTurn = true;
}
@@ -259,7 +259,7 @@ public class ManaEffectAi extends SpellAbilityAi {
return castableSpells.size() > 0;
}
private boolean canRampPool(Player ai, Card source) {
public static boolean canRampPool(Player ai, Card source) {
ManaPool mp = ai.getManaPool();
Mana test = null;
if (mp.isEmpty()) {

View File

@@ -244,7 +244,7 @@ public class PermanentAi extends SpellAbilityAi {
// Only cast if there are X or more mana sources controlled by the AI *or*
// if there are X-1 mana sources in play but the AI has an extra land in hand
CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
int extraMana = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size() > 0 ? 1 : 0;
int extraMana = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS) > 0 ? 1 : 0;
if (card.getName().equals("Illusions of Grandeur")) {
// TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic.
extraMana += Math.min(3, CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.or(CardPredicates.nameEquals("Sapphire Medallion"), CardPredicates.nameEquals("Helm of Awakening"))).size()) * 2; // each cost-reduction spell accounts for {1} in both Illusions and Donate

View File

@@ -139,7 +139,7 @@ public class PermanentCreatureAi extends PermanentAi {
boolean hasETBTrigger = card.hasETBTrigger(true);
boolean hasAmbushAI = card.hasSVar("AmbushAI");
boolean defOnlyAmbushAI = hasAmbushAI && "BlockOnly".equals(card.getSVar("AmbushAI"));
boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
boolean loseFloatMana = ai.getManaPool().totalMana() > 0 && !ManaEffectAi.canRampPool(ai, card);
boolean willDiscardNow = isOwnEOT && !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
boolean wantToCastInMain1 = ph.is(PhaseType.MAIN1, ai) && ComputerUtil.castPermanentInMain1(ai, sa);
@@ -176,7 +176,7 @@ public class PermanentCreatureAi extends PermanentAi {
}
}
if (hasFloatMana || willDiscardNow || willDieNow) {
if (loseFloatMana || willDiscardNow || willDieNow) {
// Will lose mana in pool or about to discard a card in cleanup or about to die in combat, so use this opportunity
return true;
} else if (isCommander && isMyMain1OrLater) {

View File

@@ -37,7 +37,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
return c.canBeTargetedBy(sa) && c.getController() != ai;
}
});
CardLists.sortByPowerAsc(list);
CardLists.sortByPowerDesc(list);
c1 = list.isEmpty() ? null : list.get(0);
if (sa.hasParam("Defined")) {
c2 = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa).get(0);
@@ -52,7 +52,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
if (c1 == null || c2 == null) {
return false;
}
if (ComputerUtilCard.evaluateCreature(c1) > ComputerUtilCard.evaluateCreature(c2) + 40) {
if (sa.isMandatory() || ComputerUtilCard.evaluateCreature(c1) > ComputerUtilCard.evaluateCreature(c2) + 40) {
sa.getTargets().add(c1);
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
}

View File

@@ -200,7 +200,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
final boolean evasive = (keyword.endsWith("Unblockable") || keyword.endsWith("Shadow") || keyword.startsWith("CantBeBlockedBy"));
final boolean evasive = keyword.endsWith("Unblockable") || keyword.endsWith("Shadow");
// give evasive keywords to creatures that can or do attack
if (evasive) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))

View File

@@ -109,7 +109,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
Player p = pc.getFirst(); // currently always a single target spell
Card top = p.getCardsIn(ZoneType.Library).getFirst();
int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
int landsOTB = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
int cmc = top.isSplitCard() ? Math.min(top.getCMC(Card.SplitCMCMode.LeftSplitCMC), top.getCMC(Card.SplitCMCMode.RightSplitCMC))
: top.getCMC();
int maxCastable = ComputerUtilMana.getAvailableManaEstimate(p, false);

View File

@@ -24,6 +24,10 @@ public abstract class RevealAiBase extends SpellAbilityAi {
opps = Lists.newArrayList(Iterables.filter(opps, PlayerPredicates.isTargetableBy(sa)));
if (opps.isEmpty()) {
if (mandatory && sa.canTarget(ai)) {
sa.getTargets().add(ai);
return true;
}
return false;
}

View File

@@ -198,12 +198,13 @@ public abstract class TapAiBase extends SpellAbilityAi {
Predicate<Card> findBlockers = CardPredicates.possibleBlockerForAtLeastOne(attackers);
List<Card> creatureList = CardLists.filter(tapList, findBlockers);
// TODO check if own creature would be forced to attack and we want to keep it alive
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else if (sa.getRootAbility().isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(tapList);
}
} else if (phase.isPlayerTurn(opp)
&& phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
// Tap creatures possible blockers before combat during AI's turn.

View File

@@ -26,6 +26,7 @@ import forge.game.cost.CostTap;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
@@ -213,6 +214,7 @@ public class UntapAi extends SpellAbilityAi {
untapList.remove(choice);
list.remove(choice);
// TODO ComputerUtilCard.willUntap(ai, choice)
sa.getTargets().add(choice);
}
return true;
@@ -321,7 +323,7 @@ public class UntapAi extends SpellAbilityAi {
}
// See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself
CardCollection noAutoUntap = CardLists.filter(untapList, CardPredicates.hasKeyword("CARDNAME doesn't untap during your untap step."));
CardCollection noAutoUntap = CardLists.filter(untapList, Predicates.not(Untap.CANUNTAP));
if (!noAutoUntap.isEmpty()) {
return ComputerUtilCard.getBestAI(noAutoUntap);
}

View File

@@ -86,10 +86,12 @@ public class StaticData {
final Map<String, CardRules> variantsCards = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
final Map<String, CardRules> customizedCards = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (CardEdition e : editions) {
if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) {
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
funnyCards.add(cis.name);
if (!loadNonLegalCards) {
for (CardEdition e : editions) {
if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) {
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
funnyCards.add(cis.name);
}
}
}
}
@@ -705,8 +707,6 @@ public class StaticData {
return altCandidate;
}
/**
* Get the Art Count for a given <code>PaperCard</code> looking for a candidate in all
* available databases.
@@ -734,7 +734,6 @@ public class StaticData {
public void setMulliganRule(MulliganDefs.MulliganRule rule) {
mulliganRule = rule;
}
public MulliganDefs.MulliganRule getMulliganRule() {
return mulliganRule;
}
@@ -754,7 +753,7 @@ public class StaticData {
}
public CardDb.CardArtPreference getCardArtPreference(boolean latestArt, boolean coreExpansionOnly) {
if (latestArt){
if (latestArt) {
return coreExpansionOnly ? CardDb.CardArtPreference.LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY : CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS;
}
return coreExpansionOnly ? CardDb.CardArtPreference.ORIGINAL_ART_CORE_EXPANSIONS_REPRINT_ONLY : CardDb.CardArtPreference.ORIGINAL_ART_ALL_EDITIONS;

View File

@@ -1037,7 +1037,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
public PaperCard createUnsupportedCard(String cardRequest) {
CardRequest request = CardRequest.fromString(cardRequest);
CardEdition cardEdition = CardEdition.UNKNOWN;
CardRarity cardRarity = CardRarity.Unknown;
@@ -1078,7 +1077,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity);
}
private final Editor editor = new Editor();

View File

@@ -93,7 +93,7 @@ public final class CardRules implements ICardCharacteristics {
int len = oracleText.length();
for (int i = 0; i < len; i++) {
char c = oracleText.charAt(i); // This is to avoid needless allocations performed by toCharArray()
switch(c) {
switch (c) {
case('('): isReminder = i > 0; break; // if oracle has only reminder, consider it valid rules (basic and true lands need this)
case(')'): isReminder = false; break;
case('{'): isSymbol = true; break;
@@ -132,7 +132,7 @@ public final class CardRules implements ICardCharacteristics {
}
public String getName() {
switch(splitType.getAggregationMethod()) {
switch (splitType.getAggregationMethod()) {
case COMBINE:
return mainPart.getName() + " // " + otherPart.getName();
default:
@@ -149,7 +149,7 @@ public final class CardRules implements ICardCharacteristics {
@Override
public CardType getType() {
switch(splitType.getAggregationMethod()) {
switch (splitType.getAggregationMethod()) {
case COMBINE: // no cards currently have different types
return CardType.combine(mainPart.getType(), otherPart.getType());
default:
@@ -159,7 +159,7 @@ public final class CardRules implements ICardCharacteristics {
@Override
public ManaCost getManaCost() {
switch(splitType.getAggregationMethod()) {
switch (splitType.getAggregationMethod()) {
case COMBINE:
return ManaCost.combine(mainPart.getManaCost(), otherPart.getManaCost());
default:
@@ -169,7 +169,7 @@ public final class CardRules implements ICardCharacteristics {
@Override
public ColorSet getColor() {
switch(splitType.getAggregationMethod()) {
switch (splitType.getAggregationMethod()) {
case COMBINE:
return ColorSet.fromMask(mainPart.getColor().getColor() | otherPart.getColor().getColor());
default:
@@ -186,7 +186,7 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean canCastWithAvailable(byte colorCode) {
switch(splitType.getAggregationMethod()) {
switch (splitType.getAggregationMethod()) {
case COMBINE:
return canCastFace(mainPart, colorCode) || canCastFace(otherPart, colorCode);
default:
@@ -202,7 +202,7 @@ public final class CardRules implements ICardCharacteristics {
@Override
public String getOracleText() {
switch(splitType.getAggregationMethod()) {
switch (splitType.getAggregationMethod()) {
case COMBINE:
return mainPart.getOracleText() + "\r\n\r\n" + otherPart.getOracleText();
default:

View File

@@ -60,7 +60,7 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
// Calculated fields are below:
private transient CardRarity rarity; // rarity is given in ctor when set is assigned
// Reference to a new instance of Self, but foiled!
private transient PaperCard foiledVersion = null;
private transient PaperCard foiledVersion;
@Override
public String getName() {
@@ -170,7 +170,7 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
}
};
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0){
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0) {
this(rules0, edition0, rarity0, IPaperCard.DEFAULT_ART_INDEX, false,
IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME);
}
@@ -186,7 +186,7 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX);
foil = foil0;
rarity = rarity0;
artist = artist0 != null ? TextUtil.normalizeText(artist0) : IPaperCard.NO_ARTIST_NAME;
artist = TextUtil.normalizeText(artist0);
collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER;
// If the user changes the language this will make cards sort by the old language until they restart the game.
// This is a good tradeoff

View File

@@ -349,36 +349,7 @@ public class AbilityUtils {
cards.addAll(CardLists.getValidCards(candidates, validDefined, hostCard.getController(), hostCard, sa));
return cards;
} else {
CardCollection list = null;
if (sa instanceof SpellAbility) {
SpellAbility root = ((SpellAbility)sa).getRootAbility();
if (defined.startsWith("SacrificedCards")) {
list = root.getPaidList("SacrificedCards");
} else if (defined.startsWith("Sacrificed")) {
list = root.getPaidList("Sacrificed");
} else if (defined.startsWith("Revealed")) {
list = root.getPaidList("Revealed");
} else if (defined.startsWith("DiscardedCards")) {
list = root.getPaidList("DiscardedCards");
} else if (defined.startsWith("Discarded")) {
list = root.getPaidList("Discarded");
} else if (defined.startsWith("ExiledCards")) {
list = root.getPaidList("ExiledCards");
} else if (defined.startsWith("Exiled")) {
list = root.getPaidList("Exiled");
} else if (defined.startsWith("Milled")) {
list = root.getPaidList("Milled");
} else if (defined.startsWith("TappedCards")) {
list = root.getPaidList("TappedCards");
} else if (defined.startsWith("Tapped")) {
list = root.getPaidList("Tapped");
} else if (defined.startsWith("UntappedCards")) {
list = root.getPaidList("UntappedCards");
} else if (defined.startsWith("Untapped")) {
list = root.getPaidList("Untapped");
}
}
CardCollection list = getPaidCards(sa, defined);
if (list != null) {
cards.addAll(list);
}
@@ -1004,7 +975,7 @@ public class AbilityUtils {
final Player player = sa instanceof SpellAbility ? ((SpellAbility)sa).getActivatingPlayer() : card.getController();
if (defined.equals("Self") || defined.equals("ThisTargetedCard")) {
if (defined.equals("Self") || defined.equals("ThisTargetedCard") || getPaidCards(sa, defined) != null) {
// do nothing, Self is for Cards, not Players
} else if (defined.equals("TargetedOrController")) {
players.addAll(getDefinedPlayers(card, "Targeted", sa));
@@ -1225,10 +1196,8 @@ public class AbilityUtils {
players.add(p);
}
}
else if (defined.equals("ChosenCardController")) {
for (final Card chosen : card.getChosenCards()) {
players.add(game.getCardState(chosen).getController());
}
else if (defined.startsWith("ChosenCard")) {
addPlayer(Lists.newArrayList(card.getChosenCards()), defined, players);
}
else if (defined.equals("SourceController")) {
players.add(sa.getHostCard().getController());
@@ -3864,6 +3833,39 @@ public class AbilityUtils {
return someCards;
}
public static CardCollection getPaidCards(CardTraitBase sa, String defined) {
CardCollection list = null;
if (sa instanceof SpellAbility) {
SpellAbility root = ((SpellAbility)sa).getRootAbility();
if (defined.startsWith("SacrificedCards")) {
list = root.getPaidList("SacrificedCards");
} else if (defined.startsWith("Sacrificed")) {
list = root.getPaidList("Sacrificed");
} else if (defined.startsWith("Revealed")) {
list = root.getPaidList("Revealed");
} else if (defined.startsWith("DiscardedCards")) {
list = root.getPaidList("DiscardedCards");
} else if (defined.startsWith("Discarded")) {
list = root.getPaidList("Discarded");
} else if (defined.startsWith("ExiledCards")) {
list = root.getPaidList("ExiledCards");
} else if (defined.startsWith("Exiled")) {
list = root.getPaidList("Exiled");
} else if (defined.startsWith("Milled")) {
list = root.getPaidList("Milled");
} else if (defined.startsWith("TappedCards")) {
list = root.getPaidList("TappedCards");
} else if (defined.startsWith("Tapped")) {
list = root.getPaidList("Tapped");
} else if (defined.startsWith("UntappedCards")) {
list = root.getPaidList("UntappedCards");
} else if (defined.startsWith("Untapped")) {
list = root.getPaidList("Untapped");
}
}
return list;
}
public static int getNumberOfTypes(final Card card) {
EnumSet<CardType.CoreType> types = EnumSet.noneOf(CardType.CoreType.class);
Iterables.addAll(types, card.getType().getCoreTypes());

View File

@@ -133,6 +133,11 @@ public class CountersPutEffect extends SpellAbilityEffect {
boolean existingCounter = sa.hasParam("CounterType") && sa.getParam("CounterType").equals("ExistingCounter");
boolean eachExistingCounter = sa.hasParam("EachExistingCounter");
if (sa.hasParam("Optional") && !pc.confirmAction
(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"))) {
return;
}
List<GameEntity> tgtObjects = Lists.newArrayList();
int divrem = 0;
if (sa.hasParam("Bolster")) {
@@ -198,11 +203,6 @@ public class CountersPutEffect extends SpellAbilityEffect {
}
}
if (sa.hasParam("Optional")
&& !pc.confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"))) {
return;
}
int counterRemain = counterAmount;
if (sa.hasParam("DividedRandomly")) {
CardCollection targets = new CardCollection();

View File

@@ -98,8 +98,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private CardState currentState;
private CardStateName currentStateName = CardStateName.Original;
private Zone castFrom = null;
private SpellAbility castSA = null;
private Zone castFrom;
private SpellAbility castSA;
private CardDamageHistory damageHistory = new CardDamageHistory();
// Hidden keywords won't be displayed on the card
@@ -119,18 +119,18 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith;
private Card mergedTo;
private SpellAbility effectSourceAbility = null;
private SpellAbility effectSourceAbility;
private GameEntity entityAttachedTo = null;
private GameEntity entityAttachedTo;
private GameEntity mustAttackEntity = null;
private GameEntity mustAttackEntityThisTurn = null;
private GameEntity mustAttackEntity;
private GameEntity mustAttackEntityThisTurn;
private final Map<StaticAbility, CardPlayOption> mayPlay = Maps.newHashMap();
// changes by AF animate and continuous static effects
protected CardChangedType changedTypeByText = null; // Layer 3 by Text Change
protected CardChangedType changedTypeByText; // Layer 3 by Text Change
// x=timestamp y=StaticAbility id
private final Table<Long, Long, CardChangedType> changedCardTypesByText = TreeBasedTable.create(); // Layer 3
private final Table<Long, Long, CardChangedType> changedCardTypesCharacterDefining = TreeBasedTable.create(); // Layer 4 CDA
@@ -193,22 +193,22 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private boolean sickness = true; // summoning sickness
private boolean token = false;
private boolean tokenCard = false;
private Card copiedPermanent = null;
private Card copiedPermanent;
private boolean copiedSpell = false;
private boolean canCounter = true;
private boolean unearthed;
private boolean monstrous = false;
private boolean monstrous;
private boolean renowned = false;
private boolean renowned;
private boolean manifested = false;
private boolean manifested;
private boolean foretold = false;
private boolean foretoldThisTurn = false;
private boolean foretoldByEffect = false;
private boolean foretold;
private boolean foretoldThisTurn;
private boolean foretoldByEffect;
private int timesCrewedThisTurn = 0;
@@ -253,21 +253,21 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private String oracleText = "";
private int damage;
private boolean hasBeenDealtDeathtouchDamage = false;
private boolean hasBeenDealtDeathtouchDamage;
// regeneration
private FCollection<Card> shields = new FCollection<>();
private int regeneratedThisTurn = 0;
private int regeneratedThisTurn;
private int turnInZone;
// the player that under which control it enters
private Player turnInController = null;
private Player turnInController;
private Map<String, Integer> xManaCostPaidByColor;
private Player owner = null;
private Player controller = null;
private long controllerTimestamp = 0;
private Player owner;
private Player controller;
private long controllerTimestamp;
private NavigableMap<Long, Player> tempControllers = Maps.newTreeMap();
private String originalText = "", text = "";
@@ -283,8 +283,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private String chosenMode = "";
private String currentRoom = null;
private Card exiledWith = null;
private Player exiledBy = null;
private Card exiledWith;
private Player exiledBy;
private Map<Long, Player> goad = Maps.newTreeMap();
@@ -298,11 +298,11 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private final List<Object[]> staticCommandList = Lists.newArrayList();
// Zone-changing spells should store card's zone here
private Zone currentZone = null;
private Zone currentZone;
// LKI copies of cards are allowed to store the LKI about the zone the card was known to be in last.
// For all cards except LKI copies this should always be null.
private Zone savedLastKnownZone = null;
private Zone savedLastKnownZone;
// LKI copies of cards store CMC separately to avoid shenanigans with the game state visualization
// breaking when the LKI object is changed to a different card state.
private int lkiCMC = -1;
@@ -314,7 +314,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private SpellAbility[] basicLandAbilities = new SpellAbility[MagicColor.WUBRG.length];
private int planeswalkerAbilityActivated = 0;
private int planeswalkerAbilityActivated;
private final ActivationTable numberTurnActivations = new ActivationTable();
private final ActivationTable numberGameActivations = new ActivationTable();
@@ -326,7 +326,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private final Table<SpellAbility, StaticAbility, List<String>> chosenModesTurnStatic = HashBasedTable.create();
private final Table<SpellAbility, StaticAbility, List<String>> chosenModesGameStatic = HashBasedTable.create();
private CombatLki combatLKI = null;
private CombatLki combatLKI;
// Enumeration for CMC request types
public enum SplitCMCMode {
@@ -3921,11 +3921,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
updatePTforView();
}
public final void addNewPT(final Integer power, final Integer toughness, final long timestamp, final long staticId) {
addNewPT(power, toughness, timestamp, staticId, false);
}
public final void addNewPT(final Integer power, final Integer toughness, final long timestamp, final long staticId, final boolean cda) {
(cda ? newPTCharacterDefining : newPT).put(timestamp, staticId, Pair.of(power, toughness));
updatePTforView();
@@ -6135,7 +6133,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public int getCMC() {
return getCMC(SplitCMCMode.CurrentSideCMC);
}
public int getCMC(SplitCMCMode mode) {
if (isToken() && getCopiedPermanent() == null) {
return 0;

View File

@@ -12,8 +12,8 @@ public class CardCloneStates extends ForwardingMap<CardStateName, CardState> {
private Map<CardStateName, CardState> dataMap = Maps.newEnumMap(CardStateName.class);
private Card origin = null;
private CardTraitBase ctb = null;
private Card origin;
private CardTraitBase ctb;
public CardCloneStates(Card origin, CardTraitBase sa) {
super();

View File

@@ -27,7 +27,7 @@ import forge.card.ColorSet;
* @author Forge
* @version $Id$
*/
public class CardColor {
public class CardColor {
private final byte colorMask;
public final byte getColorMask() {
return colorMask;

View File

@@ -529,7 +529,7 @@ public class CardFactory {
}
public static void copySpellAbility(SpellAbility from, SpellAbility to, final Card host, final Player p, final boolean lki) {
if (from.getTargetRestrictions() != null) {
if (from.usesTargeting()) {
to.setTargetRestrictions(from.getTargetRestrictions());
}
to.setDescription(from.getOriginalDescription());

View File

@@ -81,7 +81,7 @@ public class CardState extends GameObject implements IHasSVars {
private final CardStateView view;
private final Card card;
private ReplacementEffect loyaltyRep = null;
private ReplacementEffect loyaltyRep;
public CardState(Card card, CardStateName name) {
this(card.getView().createAlternateState(name), card);

View File

@@ -91,8 +91,7 @@ public final class CardUtil {
kw = kw.substring(7);
}
return !kw.startsWith("Protection") && !kw.startsWith("CantBeBlockedBy")
&& !NON_STACKING_LIST.contains(kw);
return !kw.startsWith("Protection") && !NON_STACKING_LIST.contains(kw);
}
public static String getShortColorsString(final Iterable<String> colors) {

View File

@@ -148,35 +148,37 @@ public class Player extends GameEntity implements Comparable<Player> {
private int lifeStartedThisTurnWith = startingLife;
private final Map<Card, Integer> assignedDamage = Maps.newHashMap();
private final Map<Card, Integer> assignedCombatDamage = Maps.newHashMap();
private int spellsCastThisTurn = 0;
private int spellsCastThisGame = 0;
private int spellsCastLastTurn = 0;
private int landsPlayedThisTurn = 0;
private int landsPlayedLastTurn = 0;
private int investigatedThisTurn = 0;
private int surveilThisTurn = 0;
private int cycledThisTurn = 0;
private int equippedThisTurn = 0;
private int lifeLostThisTurn = 0;
private int lifeLostLastTurn = 0;
private int lifeGainedThisTurn = 0;
private int lifeGainedTimesThisTurn = 0;
private int lifeGainedByTeamThisTurn = 0;
private int spellsCastThisTurn;
private int spellsCastThisGame;
private int spellsCastLastTurn;
private int landsPlayedThisTurn;
private int landsPlayedLastTurn;
private int investigatedThisTurn;
private int surveilThisTurn;
private int cycledThisTurn;
private int equippedThisTurn;
private int lifeLostThisTurn;
private int lifeLostLastTurn;
private int lifeGainedThisTurn;
private int lifeGainedTimesThisTurn;
private int lifeGainedByTeamThisTurn;
private int numPowerSurgeLands;
private int numLibrarySearchedOwn = 0; //The number of times this player has searched his library
private int numLibrarySearchedOwn; //The number of times this player has searched his library
private int numDrawnThisTurn;
private int numDrawnThisDrawStep;
private int numRollsThisTurn;
private int numDiscardedThisTurn;
private int numTokenCreatedThisTurn;
private int numForetoldThisTurn;
private int numCardsInHandStartedThisTurnWith;
private int attackersDeclaredThisTurn;
private int venturedThisTurn;
private int maxHandSize = 7;
private int startingHandSize = 7;
private boolean unlimitedHandSize = false;
private Card lastDrawnCard = null;
private Card lastDrawnCard;
private String namedCard = "";
private String namedCard2 = "";
private int numDrawnThisTurn = 0;
private int numDrawnThisDrawStep = 0;
private int numRollsThisTurn = 0;
private int numDiscardedThisTurn = 0;
private int numTokenCreatedThisTurn = 0;
private int numForetoldThisTurn = 0;
private int numCardsInHandStartedThisTurnWith = 0;
private int simultaneousDamage = 0;
@@ -200,13 +202,11 @@ public class Player extends GameEntity implements Comparable<Player> {
private Table<Long, Long, KeywordsChange> changedKeywords = TreeBasedTable.create();
private ManaPool manaPool = new ManaPool(this);
private GameEntity mustAttackEntity = null;
private GameEntity mustAttackEntityThisTurn = null;
private GameEntity mustAttackEntity;
private GameEntity mustAttackEntityThisTurn;
private CardCollection creatureAttackedThisTurn = new CardCollection();
private boolean activateLoyaltyAbilityThisTurn = false;
private boolean tappedLandForManaThisTurn = false;
private int attackersDeclaredThisTurn = 0;
private int venturedThisTurn = 0;
private List<Card> completedDungeons = new ArrayList<>();
private final Map<ZoneType, PlayerZone> zones = Maps.newEnumMap(ZoneType.class);
@@ -236,9 +236,9 @@ public class Player extends GameEntity implements Comparable<Player> {
// The SA currently being paid for
private Deque<SpellAbility> paidForStack = new ArrayDeque<>();
private Card monarchEffect = null;
private Card blessingEffect = null;
private Card keywordEffect = null;
private Card monarchEffect;
private Card blessingEffect;
private Card keywordEffect;
private Map<Long, Integer> additionalVotes = Maps.newHashMap();
private Map<Long, Integer> additionalOptionalVotes = Maps.newHashMap();

View File

@@ -39,7 +39,7 @@ public final class AbilitySub extends SpellAbility implements java.io.Serializab
/** Constant <code>serialVersionUID=4650634415821733134L</code>. */
private static final long serialVersionUID = 4650634415821733134L;
private SpellAbility parent = null;
private SpellAbility parent;
/**
* <p>

View File

@@ -110,20 +110,20 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
// choices for constructor isPermanent argument
private String originalDescription = "", description = "";
private String originalStackDescription = "", stackDescription = "";
private ManaCost multiKickerManaCost = null;
private Player activatingPlayer = null;
private Player targetingPlayer = null;
private Pair<Long, Player> controlledByPlayer = null;
private ManaCostBeingPaid manaCostBeingPaid = null;
private ManaCost multiKickerManaCost;
private Player activatingPlayer;
private Player targetingPlayer;
private Pair<Long, Player> controlledByPlayer;
private ManaCostBeingPaid manaCostBeingPaid;
private boolean spentPhyrexian = false;
private SpellAbility grantorOriginal = null;
private StaticAbility grantorStatic = null;
private SpellAbility grantorOriginal;
private StaticAbility grantorStatic;
private CardCollection splicedCards = null;
private boolean basicSpell = true;
private Trigger triggerObj = null;
private Trigger triggerObj;
private boolean optionalTrigger = false;
private ReplacementEffect replacementEffect = null;
private int sourceTrigger = -1;
@@ -141,7 +141,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private Cost payCosts;
private SpellAbilityRestriction restrictions = new SpellAbilityRestriction();
private SpellAbilityCondition conditions = new SpellAbilityCondition();
private AbilitySub subAbility = null;
private AbilitySub subAbility;
private Map<String, SpellAbility> additionalAbilities = Maps.newHashMap();
private Map<String, List<AbilitySub>> additionalAbilityLists = Maps.newHashMap();
@@ -160,10 +160,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private List<AbilitySub> chosenList = null;
private CardCollection tappedForConvoke = new CardCollection();
private Card sacrificedAsOffering = null;
private Card sacrificedAsEmerge = null;
private Card sacrificedAsOffering;
private Card sacrificedAsEmerge;
private AbilityManaPart manaPart = null;
private AbilityManaPart manaPart;
private boolean undoable;
@@ -171,36 +171,35 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private boolean mayChooseNewTargets = false;
private EnumSet<OptionalCost> optionalCosts = EnumSet.noneOf(OptionalCost.class);
private TargetRestrictions targetRestrictions = null;
private TargetRestrictions targetRestrictions;
private TargetChoices targetChosen = new TargetChoices();
private Integer dividedValue = null;
private SpellAbilityView view;
private StaticAbility mayPlay = null;
private StaticAbility mayPlay;
private CardCollection lastStateBattlefield = null;
private CardCollection lastStateGraveyard = null;
private CardCollection rollbackEffects = new CardCollection();
private CardDamageMap damageMap = null;
private CardDamageMap preventMap = null;
private GameEntityCounterTable counterTable = null;
private CardZoneTable changeZoneTable = null;
private CardDamageMap damageMap;
private CardDamageMap preventMap;
private GameEntityCounterTable counterTable;
private CardZoneTable changeZoneTable;
public CardCollection getLastStateBattlefield() {
return lastStateBattlefield;
}
public void setLastStateBattlefield(final CardCollectionView lastStateBattlefield) {
this.lastStateBattlefield = new CardCollection(lastStateBattlefield);
}
public CardCollection getLastStateGraveyard() {
return lastStateGraveyard;
}
public void setLastStateGraveyard(final CardCollectionView lastStateGraveyard) {
this.lastStateGraveyard = new CardCollection(lastStateGraveyard);
}
@@ -1876,7 +1875,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
final List<GameObject> targets = Lists.newArrayList();
SpellAbility child = getParent();
while (child != null) {
if (child.getTargetRestrictions() != null) {
if (child.usesTargeting()) {
Iterables.addAll(targets, child.getTargets());
}
child = child.getParent();

View File

@@ -326,7 +326,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView {
}
while (compare != null && sub != null) {
TargetChoices choices = compare.getTargetRestrictions() != null ? compare.getTargets() : null;
TargetChoices choices = compare.usesTargeting() ? compare.getTargets() : null;
if (choices != null && !choices.equals(sub.getTargetChoices())) {
return false;

View File

@@ -45,7 +45,7 @@ import forge.util.collect.FCollection;
*/
public class TargetChoices extends ForwardingList<GameObject> implements Cloneable {
private final FCollection<GameObject> targets = new FCollection<GameObject>();
private final FCollection<GameObject> targets = new FCollection<>();
private final Map<GameObject, Integer> dividedMap = Maps.newHashMap();

View File

@@ -83,7 +83,7 @@ public abstract class Trigger extends TriggerReplacementBase {
private Set<PhaseType> validPhases;
private SpellAbility spawningAbility = null;
private SpellAbility spawningAbility;
/**
* <p>

View File

@@ -79,7 +79,7 @@ public class TriggerAbilityTriggered extends Trigger {
if (hasParam("ValidCause")) {
boolean match = false;
for (Card cause : causes) {
if(matchesValidParam("ValidCause", cause)) {
if (matchesValidParam("ValidCause", cause)) {
match = true;
}
}

View File

@@ -1000,6 +1000,12 @@ public final class CMatchUI
public void showPromptMessage(final PlayerView playerView, final String message) {
cPrompt.setMessage(message);
}
@Override
public void showCardPromptMessage(PlayerView playerView, String message, CardView card) {
cPrompt.setMessage(message, card);
}
// no override for now
public void showPromptMessage(final PlayerView playerView, final String message, final CardView card ) {
cPrompt.setMessage(message,card);

View File

@@ -339,7 +339,7 @@ public class Forge implements ApplicationListener {
protected void afterDbLoaded() {
//init here to fix crash if the assets are missing
transitionTexture = new Texture(GuiBase.isAndroid() ? Gdx.files.internal("fallback_skin").child("transition.png") : Gdx.files.classpath("fallback_skin").child("transition.png"));
transitionTexture = new Texture(Gdx.files.classpath("fallback_skin").child("transition.png"));
destroyThis = false; //Allow back()

View File

@@ -38,7 +38,6 @@ import forge.toolbox.FLabel;
import forge.toolbox.GuiChoose;
import forge.util.Callback;
import forge.util.ItemPool;
import forge.util.Localizer;
import forge.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;

View File

@@ -200,7 +200,6 @@ public class PlayerStatisticScene extends UIScene {
back.getLabel().setText(Forge.getLocalizer().getMessage("lblBack"));
ScrollPane scrollPane = ui.findActor("enemies");
scrollPane.setActor(enemiesGroup);
enemiesGroup.setFillParent(true);
this.init = true;
}
}

View File

@@ -54,7 +54,7 @@ public class SaveLoadScene extends UIScene {
private TextButton addSaveSlot(String name, int i) {
layout.add(Controls.newLabel(name)).align(Align.left).pad(4, 10, 4, 15);
layout.add(Controls.newLabel(name)).align(Align.left).pad(2, 5, 2, 10);
TextButton button = Controls.newTextButton("...");
button.addListener(new ClickListener() {
@Override
@@ -213,7 +213,6 @@ public class SaveLoadScene extends UIScene {
super.resLoaded();
if (!this.init) {
layout = new Table();
layout.setFillParent(true);
stage.addActor(layout);
dialog = Controls.newDialog(Forge.getLocalizer().getMessage("lblSave"));
textInput = Controls.newTextField("");
@@ -257,9 +256,8 @@ public class SaveLoadScene extends UIScene {
previewImage = ui.findActor("preview");
previewBorder = ui.findActor("preview_border");
header = Controls.newLabel(Forge.getLocalizer().getMessage("lblSave"));
header.setHeight(header.getHeight() * 2);
header.setAlignment(Align.center);
layout.add(header).pad(2).colspan(4).align(Align.center).expand();
layout.add(header).pad(2).colspan(4).align(Align.center).expandX();
layout.row();
autoSave = addSaveSlot(Forge.getLocalizer().getMessage("lblAutoSave"), WorldSave.AUTO_SAVE_SLOT);
quickSave = addSaveSlot(Forge.getLocalizer().getMessage("lblQuickSave"), WorldSave.QUICK_SAVE_SLOT);

View File

@@ -74,8 +74,8 @@ public class AssetsDownloader {
}
return;
}
SOptionPane.showMessageDialog("Could not download update. " +
"Press OK to proceed without update.", "Update Failed");
SOptionPane.showOptionDialog("Could not download update. " +
"Press OK to proceed without update.", "Update Failed", null, ImmutableList.of("Ok"));
}
}
}
@@ -112,9 +112,12 @@ public class AssetsDownloader {
else {
message += "You cannot start the app since you haven't previously downloaded these files.";
}
SOptionPane.showMessageDialog(message, "No Internet Connection");
if (!canIgnoreDownload) {
Forge.exitAnimation(false); //exit if can't ignore download
switch (SOptionPane.showOptionDialog(message, "No Internet Connection", null, ImmutableList.of("Ok"))) {
default: {
if (!canIgnoreDownload) {
Forge.exitAnimation(false); //exit if can't ignore download
}
}
}
return;
}

View File

@@ -108,7 +108,7 @@ public class FSkin {
{
if (!dir.exists() || !dir.isDirectory()) {
//if skins directory doesn't exist, point to internal assets/skin directory instead for the sake of the splash screen
preferredDir = GuiBase.isAndroid() ? Gdx.files.internal("fallback_skin") : Gdx.files.classpath("fallback_skin");
preferredDir = Gdx.files.classpath("fallback_skin");
}
else {
if (splashScreen != null) {

View File

@@ -97,9 +97,7 @@ public class SplashScreen extends FContainer {
private float progress = 0;
private boolean finished, openAdventure;
//for transition image only...
TextureRegion transition_bg = new TextureRegion(new Texture(GuiBase.isAndroid()
? Gdx.files.internal("fallback_skin").child("title_bg_lq.png")
: Gdx.files.classpath("fallback_skin").child("title_bg_lq.png")));
TextureRegion transition_bg = new TextureRegion(new Texture(Gdx.files.classpath("fallback_skin").child("title_bg_lq.png")));
public void drawBackground(Graphics g) {
float percentage = progress / DURATION;

View File

@@ -12,6 +12,7 @@ import forge.deck.Deck;
import forge.game.player.Player;
import forge.item.IPaperCard;
import forge.screens.TransitionScreen;
import forge.util.collect.FCollection;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Function;
@@ -157,13 +158,18 @@ public class MatchController extends AbstractGuiGame {
public void openView(final TrackableCollection<PlayerView> myPlayers) {
final boolean noHumans = !hasLocalPlayers();
final FCollectionView<PlayerView> allPlayers = getGameView().getPlayers();
FCollectionView<PlayerView> players = getGameView().getPlayers();
if (players.size() == 2 && myPlayers != null && myPlayers.size() == 1 && myPlayers.get(0).equals(players.get(1))) {
players = new FCollection<>(new PlayerView[]{players.get(1), players.get(0)});
}
final List<VPlayerPanel> playerPanels = new ArrayList<>();
for (final PlayerView p : allPlayers) {
boolean init = false;
for (final PlayerView p : players) {
final boolean isLocal = isLocalPlayer(p);
final VPlayerPanel playerPanel = new VPlayerPanel(p, isLocal || noHumans, allPlayers.size());
if (isLocal && !playerPanels.isEmpty()) {
final VPlayerPanel playerPanel = new VPlayerPanel(p, isLocal || noHumans, players.size());
if (isLocal && !init) {
playerPanels.add(0, playerPanel); //ensure local player always first among player panels
init = true;
}
else {
playerPanels.add(playerPanel);
@@ -203,7 +209,7 @@ public class MatchController extends AbstractGuiGame {
}
@Override
public void showPromptMessage(final PlayerView player, final String message, final CardView card) {
public void showCardPromptMessage(final PlayerView player, final String message, final CardView card) {
view.getPrompt(player).setMessage(message, card);
}

View File

@@ -12,7 +12,6 @@ import forge.gui.FThreads;
import forge.gui.interfaces.IButton;
import forge.model.FModel;
import forge.screens.LoadingOverlay;
import forge.screens.home.HomeScreen;
import forge.toolbox.FEvent;
import forge.toolbox.FEvent.FEventHandler;
import forge.toolbox.FLabel;

View File

@@ -112,7 +112,7 @@ public class FCardPanel extends FDisplayObject {
return;
}
if (!animate || MatchController.instance.isGameFast() || MatchController.instance.getGameView().isMatchOver()) {
if (!animate || MatchController.instance.isGameFast() || (MatchController.instance.getGameView() != null && MatchController.instance.getGameView().isMatchOver())) {
//don't animate if game is fast or match is over
rotateTransform(g, x, y, w, h, edgeOffset, false);
card.updateNeedsTapAnimation(false);

View File

@@ -1,6 +1,5 @@
#Add one announcement per line
Get in the discord if you aren't yet. https://discord.gg/3v9JCVr
All new Alchemy: Innistrad cards (Y22) have been implemented in Forge. Happy brewing!
Support for rebalanced Arena cards (separate from the original implementations) is implemented.
It's now possible to choose to play Constructed matches in the best of 1, 3, and 5 formats.
Kamigawa: Neon Dynasty (NEO) and Kamigawa: Neon Dynasty Commander (NEC) are fully implemented.
Mobile Forge and Mobile backports now integrate Adventure Mode, an overworld adventure in the spirit of Shandalar.
*** Android 7 & 8 support is now deprecated. Support will be dropped in an upcoming release. ***

View File

@@ -2,6 +2,6 @@ Name:Swallow Whole
ManaCost:W
Types:Sorcery
A:SP$ ChangeZone | Cost$ W tapXType<1/Creature> | ValidTgts$ Creature.tapped | TgtPrompt$ Select target tapped creature | Origin$ Battlefield | Destination$ Exile | SubAbility$ DBPutCounter | SpellDescription$ As an additional cost to cast this spell, tap an untapped creature you control. Exile target tapped creature.
SVar:DBPutCounter:DB$ PutCounter | Defined$ Tapped | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on the creature tapped to cast this spell.
SVar:DBPutCounter:DB$ PutCounter | Defined$ Tapped | CounterType$ P1P1 | SpellDescription$ Put a +1/+1 counter on the creature tapped to cast this spell.
DeckHas:Ability$Counters
Oracle:As an additional cost to cast this spell, tap an untapped creature you control.\nExile target tapped creature. Put a +1/+1 counter on the creature tapped to pay this spell's additional cost.

View File

@@ -34,3 +34,6 @@ ScryfallCode=PPRO
2035 U Saw it Coming @Anato Finnstark
2036 M Kaya the Inexorable @Jason A. Engle
2037 R Faceless Haven @Pablo Mendoza
2038 M Grand Master of Flowers
2039 R Adult Gold Dragon
2040 U Krydle of Baldur's Gate

View File

@@ -4,4 +4,4 @@ Order:108
Subtype:Custom
Type:Casual
Rarities:L, C
Banned:Arcum's Astrolabe; Atog; Bonder's Ornament; Chatterstorm; Cloud of Faeries; Cloudpost; Cranial Plating; Daze; Expedition Map; Empty the Warrens; Fall from Favor; Frantic Search; Gitaxian Probe; Grapeshot; Gush; High Tide; Hymn to Tourach; Invigorate; Mystic Sanctuary; Peregrine Drake; Prophetic Prism; Sinkhole; Sojourner's Companion; Temporal Fissure; Treasure Cruise
Banned:Arcum's Astrolabe; Atog; Bonder's Ornament; Chatterstorm; Cloud of Faeries; Cloudpost; Cranial Plating; Daze; Disciple of the Vault; Empty the Warrens; Fall from Favor; Frantic Search; Galvanic Relay; Gitaxian Probe; Grapeshot; Gush; High Tide; Hymn to Tourach; Invigorate; Mystic Sanctuary; Peregrine Drake; Prophetic Prism; Sinkhole; Sojourner's Companion; Temporal Fissure; Treasure Cruise

View File

@@ -4,4 +4,4 @@ Order:103
Subtype:Modern
Type:Sanctioned
Sets:8ED, MRD, DST, 5DN, CHK, BOK, SOK, 9ED, RAV, GPT, DIS, CSP, TSP, TSB, TSR, PLC, FUT, 10E, LRW, EVE, SHM, MOR, ALA, CFX, ARB, M10, ZEN, WWK, ROE, M11, SOM, MBS, NPH, M12, ISD, DKA, AVR, M13, RTR, GTC, DGM, M14, THS, BNG, JOU, M15, KTK, FRF, DTK, MMA, MM2, MM3, ORI, BFZ, OGW, SOI, EMN, KLD, AER, AKH, W16, W17, HOU, XLN, RIX, DOM, M19, G18, GRN, RNA, WAR, MH1, M20, ELD, THB, IKO, M21, ZNR, KHM, STX, MH2, AFR, MID, VOW, NEO
Banned:Ancient Den; Arcum's Astrolabe; Birthing Pod; Blazing Shoal; Bridge from Below; Chrome Mox; Cloudpost; Dark Depths; Deathrite Shaman; Dig Through Time; Dread Return; Eye of Ugin; Faithless Looting; Field of the Dead; Gitaxian Probe; Glimpse of Nature; Golgari Grave-Troll; Great Furnace; Green Sun's Zenith; Hogaak, Arisen Necropolis; Hypergenesis; Krark-Clan Ironworks; Mental Misstep; Mox Opal; Mycosynth Lattice; Mystic Sanctuary; Oko, Thief of Crowns; Once Upon A Time; Ponder; Preordain; Punishing Fire; Rite of Flame; Seat of the Synod; Second Sunrise; Seething Song; Sensei's Divining Top; Simian Spirit Guide; Skullclamp; Splinter Twin; Summer Bloom; Tibalt's Trickery; Treasure Cruise; Tree of Tales; Umezawa's Jitte; Uro, Titan of Nature's Wrath; Vault of Whispers
Banned:Ancient Den; Arcum's Astrolabe; Birthing Pod; Blazing Shoal; Bridge from Below; Chrome Mox; Cloudpost; Dark Depths; Deathrite Shaman; Dig Through Time; Dread Return; Eye of Ugin; Faithless Looting; Field of the Dead; Gitaxian Probe; Glimpse of Nature; Golgari Grave-Troll; Great Furnace; Green Sun's Zenith; Hogaak, Arisen Necropolis; Hypergenesis; Krark-Clan Ironworks; Lurrus of the Dream-Den; Mental Misstep; Mox Opal; Mycosynth Lattice; Mystic Sanctuary; Oko, Thief of Crowns; Once Upon A Time; Ponder; Preordain; Punishing Fire; Rite of Flame; Seat of the Synod; Second Sunrise; Seething Song; Sensei's Divining Top; Simian Spirit Guide; Skullclamp; Splinter Twin; Summer Bloom; Tibalt's Trickery; Treasure Cruise; Tree of Tales; Umezawa's Jitte; Uro, Titan of Nature's Wrath; Vault of Whispers

View File

@@ -4,4 +4,4 @@ Order:102
Subtype:Pioneer
Type:Sanctioned
Sets:RTR, GTC, DGM, M14, THS, BNG, JOU, M15, KTK, FRF, DTK, ORI, BFZ, OGW, SOI, EMN, KLD, AER, AKH, HOU, XLN, RIX, DOM, M19, G18, GRN, RNA, WAR, M20, ELD, THB, IKO, M21, ZNR, KHM, STX, AFR, MID, VOW, NEO
Banned:Balustrade Spy; Bloodstained Mire; Felidar Guardian; Field of the Dead; Flooded Strand; Inverter of Truth; Kethis, the Hidden Hand; Leyline of Abundance; Nexus of Fate; Oko, Thief of Crowns; Once Upon a Time; Polluted Delta; Smuggler's Copter; Teferi, Time Raveler; Undercity Informer; Underworld Breach; Uro, Titan of Nature's Wrath; Veil of Summer; Walking Ballista; Wilderness Reclamation; Windswept Heath; Wooded Foothills
Banned:Balustrade Spy; Bloodstained Mire; Felidar Guardian; Field of the Dead; Flooded Strand; Inverter of Truth; Kethis, the Hidden Hand; Leyline of Abundance; Lurrus of the Dream-Den; Nexus of Fate; Oko, Thief of Crowns; Once Upon a Time; Polluted Delta; Smuggler's Copter; Teferi, Time Raveler; Undercity Informer; Underworld Breach; Uro, Titan of Nature's Wrath; Veil of Summer; Walking Ballista; Wilderness Reclamation; Windswept Heath; Wooded Foothills

View File

@@ -2690,9 +2690,9 @@ lblUseFormatFilter=Wähle ein Format für die Deckliste
lblIgnoreBnR=Importiere auch gebannte und eingeschränkte Karten
ttIgnoreBnR=Wenn aktiviert, erden auch gebannte oder eingeschränkte Karten ins Deck importiert.
nlIgnoreBnR=Warnung: Das Deck kann bei eingeschalteter Deckkonkonformität nicht spielbar sein.
lblUseSmartCardArt=Enable Smart Card Art Selection
ttUseSmartCardArtNoDeck=If enabled, the art of cards will be automatically chosen to match up with other cards in the Decklist.
ttUseSmartCardArtWithDeck=If enabled, the art of cards will be automatically chosen to match up with other cards in the Decklist, and in current Deck.
lblUseSmartCardArt=Aktiviere intelligente Kartenbildwahl
ttUseSmartCardArtNoDeck=Wenn aktiviert, wird das Kartenbild automatisch passend zu anderen Karten in der Deckliste gewählt.
ttUseSmartCardArtWithDeck=Wenn aktiviert, wird das Kartenbild automatisch passend zu anderen Karten in der Deckliste und dem aktuellen Deck gewählt.
lblExtraOptions=Zeige Optionen
lblHideOptions=Verstecke Optionen
lblCardPreview=Karten-Vorschau
@@ -2875,6 +2875,6 @@ lblAbort=Abbrechen
lblNameYourSaveFile=Nennen Sie Ihre neue Save-Datei
lblEdit=Bearbeiten
lblWinProper=Sieg
lblLossProper=Verlust
lblWinLossRatio=Verlustquote gewinnen
lblLossProper=Niederlage
lblWinLossRatio=Sieg/Niederlage-Quote
lblHeal=Heilen

View File

@@ -6,6 +6,10 @@ Budget Modern Metagame | https://downloads.cardforge.org/decks/budgetmodernmetag
Building on a Budget | https://downloads.cardforge.org/decks/buildingonabudget.zip
Card Preview | https://downloads.cardforge.org/decks/cardpreview.zip
Community Cup | https://downloads.cardforge.org/decks/communitycup.zip
Current Alchemy Metagame | https://downloads.cardforge.org/decks/currentalchemymetagame.zip
Current Bo1 Alchemy Metagame | https://downloads.cardforge.org/decks/currentbo1alchemymetagame.zip
Current Bo1 Historic Metagame | https://downloads.cardforge.org/decks/currentbo1historicmetagame.zip
Current Bo1 Standard Metagame | https://downloads.cardforge.org/decks/currentbo1standardmetagame.zip
Current Historic Metagame | https://downloads.cardforge.org/decks/currenthistoricmetagame.zip
Current Legacy Metagame | https://downloads.cardforge.org/decks/currentlegacymetagame.zip
Current Modern Metagame | https://downloads.cardforge.org/decks/currentmodernmetagame.zip

View File

@@ -0,0 +1,17 @@
[metadata]
Name:Possibility Storm - Kamigawa: Neon Dynasty #01
URL:https://i2.wp.com/www.possibilitystorm.com/wp-content/uploads/2022/01/latest-3-scaled.jpg?ssl=1
Goal:Win
Turns:1
Difficulty:Uncommon
Description:Win this turn. The top card of your library is Bloodthirsty Adversary. Assume any other cards you could access are irrelevant to the solution. Good luck!
[state]
humanlife=20
ailife=11
turn=1
activeplayer=human
activephase=MAIN1
humanhand=Hero's Downfall;The Meathook Massacre;Kappa Tech-Wrecker
humanlibrary=Bloodthirsty Adversary;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt
humanbattlefield=Bookwurm;Atsushi, the Blazing Sky;Hidetsugu, Devouring Chaos;Swamp;Swamp;Swamp;Swamp;Mountain;Mountain;Mountain;Forest
aibattlefield=Angel of Destiny;Henrika Domnathi|Transformed;Vengeful Reaper

View File

@@ -0,0 +1,17 @@
[metadata]
Name:Possibility Storm - Kamigawa: Neon Dynasty #02
URL:https://i0.wp.com/www.possibilitystorm.com/wp-content/uploads/2022/02/latest-scaled.jpg?ssl=1
Goal:Win
Turns:1
Difficulty:Mythic
Description:Win this turn. Start in your first main phase (Saga has resolved). Assume opponent has no mana.
[state]
humanlife=4
ailife=8
turn=1
activeplayer=human
activephase=MAIN1
humanhand=Jugan Defends the Temple;Satsuki, the Living Lore;The Kami War;Fall of the Impostor;Croaking Counterpart
humanbattlefield=Battle for Bretagard|Counters:LORE=2;Tuktuk Rubblefort;Overgrown Farmland;Overgrown Farmland;Overgrown Farmland;Overgrown Farmland;Vineglimmer Snarl;Vineglimmer Snarl;Vineglimmer Snarl;The World Tree
aibattlefield=Flamescroll Celebrant;Colossal Skyturtle;Weaver of Harmony;Greater Tanuki
humanprecast=Battle for Bretagard:TrigToken1;Battle for Bretagard:TrigToken2

View File

@@ -123,10 +123,10 @@ public abstract class InputBase implements java.io.Serializable, Input {
controller.getGui().showPromptMessage(getOwner(), message);
}
protected final void showMessage(final String message, final SpellAbilityView sav) {
controller.getGui().showPromptMessage(getOwner(), message, sav.getHostCard());
controller.getGui().showCardPromptMessage(getOwner(), message, sav.getHostCard());
}
protected final void showMessage(final String message, final CardView card) {
controller.getGui().showPromptMessage(getOwner(), message, card);
controller.getGui().showCardPromptMessage(getOwner(), message, card);
}
protected String getTurnPhasePriorityMessage(final Game game) {

View File

@@ -5,7 +5,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.StreamCorruptedException;
import io.netty.handler.codec.serialization.ClassResolver;
@@ -23,16 +22,19 @@ public class CObjectInputStream extends ObjectInputStream {
if (type < 0) {
throw new EOFException();
} else {
switch(type) {
case 0:
return super.readClassDescriptor();
case 1:
String className = readUTF();
Class<?> clazz = classResolver.resolve(className);
return ObjectStreamClass.lookupAny(clazz);
default:
throw new StreamCorruptedException("Unexpected class descriptor type: " + type);
ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();
Class localClass;
try {
localClass = Class.forName(resultClassDescriptor.getName());
} catch (ClassNotFoundException e) {
System.err.println("[Class Not Found Exception]\nNo local class for " + resultClassDescriptor.getName());
return resultClassDescriptor;
}
ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookupAny(localClass);
if (localClassDescriptor != null && type == 1) {
resultClassDescriptor = localClassDescriptor; // Use local class descriptor for deserialization by default
}
return resultClassDescriptor;
}
}

View File

@@ -32,21 +32,22 @@ public enum ProtocolMethod {
// Server -> Client
setGameView (Mode.SERVER, Void.TYPE, GameView.class),
openView (Mode.SERVER, Void.TYPE, TrackableCollection/*PlayerView*/.class),
afterGameEnd (Mode.SERVER),
showCombat (Mode.SERVER),
afterGameEnd (Mode.SERVER, Void.TYPE),
showCombat (Mode.SERVER, Void.TYPE),
showPromptMessage (Mode.SERVER, Void.TYPE, PlayerView.class, String.class),
showCardPromptMessage (Mode.SERVER, Void.TYPE, PlayerView.class, String.class, CardView.class),
updateButtons (Mode.SERVER, Void.TYPE, PlayerView.class, String.class, String.class, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE),
flashIncorrectAction(Mode.SERVER),
alertUser (Mode.SERVER),
flashIncorrectAction(Mode.SERVER, Void.TYPE),
alertUser (Mode.SERVER, Void.TYPE),
updatePhase (Mode.SERVER, Void.TYPE, Boolean.TYPE),
updateTurn (Mode.SERVER, Void.TYPE, PlayerView.class),
updatePlayerControl (Mode.SERVER),
enableOverlay (Mode.SERVER),
disableOverlay (Mode.SERVER),
finishGame (Mode.SERVER),
updatePlayerControl (Mode.SERVER, Void.TYPE),
enableOverlay (Mode.SERVER, Void.TYPE),
disableOverlay (Mode.SERVER, Void.TYPE),
finishGame (Mode.SERVER, Void.TYPE),
showManaPool (Mode.SERVER, Void.TYPE, PlayerView.class),
hideManaPool (Mode.SERVER, Void.TYPE, PlayerView.class),
updateStack (Mode.SERVER),
updateStack (Mode.SERVER, Void.TYPE),
updateZones (Mode.SERVER, Void.TYPE, Iterable/*PlayerZoneUpdate*/.class),
tempShowZones (Mode.SERVER, Iterable/*PlayerZoneUpdate*/.class, PlayerView.class, Iterable/*PlayerZoneUpdate*/.class),
hideZones (Mode.SERVER, Void.TYPE, PlayerView.class, Iterable/*PlayerZoneUpdate*/.class),
@@ -71,11 +72,11 @@ public enum ProtocolMethod {
manipulateCardList (Mode.SERVER, List.class, String.class, Iterable.class, Iterable.class, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE),
setCard (Mode.SERVER, Void.TYPE, CardView.class),
setSelectables (Mode.SERVER, Void.TYPE, Iterable/*CardView*/.class),
clearSelectables (Mode.SERVER),
refreshField (Mode.SERVER),
clearSelectables (Mode.SERVER, Void.TYPE),
refreshField (Mode.SERVER, Void.TYPE),
// TODO case "setPlayerAvatar":
openZones (Mode.SERVER, PlayerZoneUpdates.class, PlayerView.class, Collection/*ZoneType*/.class, Map/*PlayerView,Object*/.class),
restoreOldZones (Mode.SERVER, Void.TYPE, PlayerView.class, PlayerZoneUpdates.class),
restoreOldZones (Mode.SERVER, Void.TYPE, PlayerView.class, Iterable/*PlayerZoneUpdates*/.class),
isUiSetToSkipPhase (Mode.SERVER, Boolean.TYPE, PlayerView.class, PhaseType.class),
setRememberedActions(Mode.SERVER, Void.TYPE),
nextRememberedAction(Mode.SERVER, Void.TYPE),
@@ -85,18 +86,18 @@ public enum ProtocolMethod {
// which client and server wait for one another's response and block
// the threads that're supposed to give that response
useMana (Mode.CLIENT, Void.TYPE, Byte.TYPE),
undoLastAction (Mode.CLIENT, Void.TYPE, Boolean.TYPE),
undoLastAction (Mode.CLIENT, Void.TYPE),
selectPlayer (Mode.CLIENT, Void.TYPE, PlayerView.class, ITriggerEvent.class),
selectCard (Mode.CLIENT, Void.TYPE, CardView.class, List.class, ITriggerEvent.class),
selectButtonOk (Mode.CLIENT),
selectButtonCancel (Mode.CLIENT),
selectButtonOk (Mode.CLIENT, Void.TYPE),
selectButtonCancel (Mode.CLIENT, Void.TYPE),
selectAbility (Mode.CLIENT, Void.TYPE, SpellAbilityView.class),
passPriorityUntilEndOfTurn(Mode.CLIENT),
passPriority (Mode.CLIENT),
passPriorityUntilEndOfTurn(Mode.CLIENT, Void.TYPE),
passPriority (Mode.CLIENT, Void.TYPE),
nextGameDecision (Mode.CLIENT, Void.TYPE, NextGameDecision.class),
getActivateDescription (Mode.CLIENT, String.class, CardView.class),
concede (Mode.CLIENT),
alphaStrike (Mode.CLIENT),
concede (Mode.CLIENT, Void.TYPE),
alphaStrike (Mode.CLIENT, Void.TYPE),
reorderHand (Mode.CLIENT, Void.TYPE, CardView.class, Integer.TYPE);
private enum Mode {
@@ -161,13 +162,17 @@ public enum ProtocolMethod {
if(!GuiBase.hasPropertyConfig())
return; //if the experimental network option is enabled, then check the args, else let the default decoder handle it
for (int iArg = 0; iArg < args.length; iArg++) {
final Object arg = args[iArg];
final Class<?> type = this.args[iArg];
if (!ReflectionUtil.isInstance(arg, type)) {
//throw new InternalError(String.format("Protocol method %s: illegal argument (%d) of type %s, %s expected", name(), iArg, arg.getClass().getName(), type.getName()));
System.err.println(String.format("InternalError: Protocol method %s: illegal argument (%d) of type %s, %s expected (ProtocolMethod.java)", name(), iArg, arg.getClass().getName(), type.getName()));
try {
for (int iArg = 0; iArg < args.length; iArg++) {
final Object arg = args[iArg];
final Class<?> type = this.args[iArg];
if (!ReflectionUtil.isInstance(arg, type)) {
//throw new InternalError(String.format("Protocol method %s: illegal argument (%d) of type %s, %s expected", name(), iArg, arg.getClass().getName(), type.getName()));
System.err.println(String.format("InternalError: Protocol method %s: illegal argument (%d) of type %s, %s expected (ProtocolMethod.java)", name(), iArg, arg.getClass().getName(), type.getName()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

View File

@@ -76,9 +76,9 @@ public class NetGuiGame extends AbstractGuiGame {
}
@Override
public void showPromptMessage(final PlayerView playerView, final String message, final CardView card) {
public void showCardPromptMessage(final PlayerView playerView, final String message, final CardView card) {
updateGameView();
send(ProtocolMethod.showPromptMessage, playerView, message);
send(ProtocolMethod.showCardPromptMessage, playerView, message, card);
}
@Override

View File

@@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map.Entry;
import com.google.common.collect.Lists;
import forge.gui.GuiBase;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
@@ -304,8 +305,9 @@ public class QuestWinLoseController {
if (altReward > 0) {
credGameplay += altReward;
sb.append(TextUtil.concatNoSpace("Alternate win condition: <u>",
winConditionName, "</u>! Bonus: ", String.valueOf(altReward), " credits.\n"));
sb.append(GuiBase.getInterface().isLibgdxPort()
? TextUtil.concatNoSpace("Alternate win condition: ", winConditionName, "! Bonus: ", String.valueOf(altReward), " credits.\n")
: TextUtil.concatNoSpace("Alternate win condition: <u>", winConditionName, "</u>! Bonus: ", String.valueOf(altReward), " credits.\n"));
}
}
// Mulligan to zero

View File

@@ -39,7 +39,7 @@ public interface IGuiGame {
void afterGameEnd();
void showCombat();
void showPromptMessage(PlayerView playerView, String message);
void showPromptMessage(PlayerView playerView, String message, CardView card);
void showCardPromptMessage(PlayerView playerView, String message, CardView card);
void updateButtons(PlayerView owner, boolean okEnabled, boolean cancelEnabled, boolean focusOk);
void updateButtons(PlayerView owner, String label1, String label2, boolean enable1, boolean enable2, boolean focus1);
void flashIncorrectAction();

View File

@@ -87,7 +87,7 @@ public class TargetSelection {
final int maxTargets = numTargets != null ? numTargets.intValue() : ability.getMaxTargets();
//final int maxTotalCMC = tgt.getMaxTotalCMC(ability.getHostCard(), ability);
final int numTargeted = ability.getTargets().size();
final boolean isSingleZone = ability.getTargetRestrictions().isSingleZone();
final boolean isSingleZone = getTgt().isSingleZone();
final boolean hasEnoughTargets = minTargets == 0 || numTargeted >= minTargets;
final boolean hasAllTargets = numTargeted == maxTargets && maxTargets > 0;
@@ -219,7 +219,7 @@ public class TargetSelection {
for (final Card inZone : choices) {
Zone zz = game.getZoneOf(inZone);
CardView cardView = CardView.get(inZone);
if (this.ability.getTargetRestrictions() != null && this.ability.getTargetRestrictions().isWithSameCreatureType()) {
if (getTgt() != null && getTgt().isWithSameCreatureType()) {
Card firstTgt = this.ability.getTargetCard();
if (firstTgt != null && !firstTgt.sharesCreatureTypeWith(inZone)) {
continue;