diff --git a/forge-ai/pom.xml b/forge-ai/pom.xml index 3f176653a65..b8d21660c54 100644 --- a/forge-ai/pom.xml +++ b/forge-ai/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.34-SNAPSHOT forge-ai diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index ff73d875785..bec6e19c4d6 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -91,9 +91,6 @@ public class ComputerUtil { } } - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -219,9 +216,6 @@ public class ComputerUtil { final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -246,9 +240,6 @@ public class ComputerUtil { final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -267,9 +258,6 @@ public class ComputerUtil { final Card source = newSA.getHostCard(); if (newSA.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(newSA); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); newSA.setHostCard(game.getAction().moveToStack(source, sa)); if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) { @@ -290,9 +278,6 @@ public class ComputerUtil { if (ComputerUtilCost.canPayCost(sa, ai)) { final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -2338,7 +2323,7 @@ public class ComputerUtil { return chosen; } - public static Object vote(Player ai, List options, SpellAbility sa, Multimap votes) { + public static Object vote(Player ai, List options, SpellAbility sa, Multimap votes, Player forPlayer) { final Card source = sa.getHostCard(); final Player controller = source.getController(); final Game game = controller.getGame(); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 4102df2fefb..3eaa075f4ea 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -1169,7 +1169,7 @@ public class ComputerUtilMana { cost.increaseShard(shardToGrow, manaToAdd); if (!test) { - card.setXManaCostPaid(manaToAdd / cost.getXcounter()); + sa.setXManaCostPaid(manaToAdd / cost.getXcounter()); } } diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index 9d71da36c6a..e07b65d95c4 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -981,14 +981,27 @@ public abstract class GameState { spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim(); } - PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef); + Card c = null; - if (pc == null) { - System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!"); - return; + if (StringUtils.isNumeric(spellDef)) { + // Precast from a specific host + c = idToCard.get(Integer.parseInt(spellDef)); + if (c == null) { + System.err.println("ERROR: Could not find a card with ID " + spellDef + " to precast!"); + return; + } + } else { + // Precast from a card by name + PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef); + + if (pc == null) { + System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!"); + return; + } + + c = Card.fromPaperCard(pc, activator); } - Card c = Card.fromPaperCard(pc, activator); SpellAbility sa = null; if (!scriptID.isEmpty()) { diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 04a9f303832..56d592d5b8e 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -513,8 +513,8 @@ public class PlayerControllerAi extends PlayerController { } @Override - public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes) { - return ComputerUtil.vote(player, options, sa, votes); + public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer) { + return ComputerUtil.vote(player, options, sa, votes, forPlayer); } @Override diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index 27700e2455b..f5f4435c491 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -1295,6 +1295,26 @@ public class SpecialCardAi { } } + // Timmerian Fiends + public static class TimmerianFiends { + public static boolean consider(final Player ai, final SpellAbility sa) { + final Card targeted = sa.getParentTargetingCard().getTargetCard(); + if (targeted == null) { + return false; + } + + if (targeted.isCreature()) { + if (ComputerUtil.aiLifeInDanger(ai, true, 0)) { + return true; // do it, hoping to save a valuable potential blocker etc. + } + return ComputerUtilCard.evaluateCreature(targeted) >= 200; // might need tweaking + } else { + // TODO: this currently compares purely by CMC. To be somehow improved, especially for stuff like the Power Nine etc. + return ComputerUtilCard.evaluatePermanentList(new CardCollection(targeted)) >= 3; + } + } + } + // Volrath's Shapeshifter public static class VolrathsShapeshifter { public static boolean consider(final Player ai, final SpellAbility sa) { diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java index fba432c9d48..52f0e8cda25 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java @@ -1,15 +1,10 @@ package forge.ai.ability; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - import com.google.common.collect.Lists; import com.google.common.collect.Maps; - import forge.ai.ComputerUtil; import forge.ai.ComputerUtilMana; +import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -24,6 +19,11 @@ import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + public class MillAi extends SpellAbilityAi { @Override @@ -196,6 +196,10 @@ public class MillAi extends SpellAbilityAi { */ @Override public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + if ("TimmerianFiends".equals(sa.getParam("AILogic"))) { + return SpecialCardAi.TimmerianFiends.consider(player, sa); + } + return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java index 58aab03ecbb..33d23dd3b22 100644 --- a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java @@ -46,6 +46,12 @@ public class VoteAi extends SpellAbilityAi { @Override public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map params) { + if (params.containsKey("Voter")) { + Player p = (Player)params.get("Voter"); + if (p.isOpponentOf(player)) { + return min; + } + } if (sa.getActivatingPlayer().isOpponentOf(player)) { return min; } diff --git a/forge-core/pom.xml b/forge-core/pom.xml index 9c13b0d69aa..763d968dc05 100644 --- a/forge-core/pom.xml +++ b/forge-core/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.34-SNAPSHOT forge-core diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index e07f836566d..4ea30d2a17d 100644 --- a/forge-core/src/main/java/forge/ImageKeys.java +++ b/forge-core/src/main/java/forge/ImageKeys.java @@ -6,6 +6,8 @@ import forge.util.ImageUtil; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; + + import java.io.File; import java.util.HashMap; import java.util.Map; @@ -113,7 +115,17 @@ public final class ImageKeys { } //try fullborder... if (filename.contains(".full")) { - file = findFile(dir, TextUtil.fastReplace(filename, ".full", ".fullborder")); + String fullborderFile = TextUtil.fastReplace(filename, ".full", ".fullborder"); + file = findFile(dir, fullborderFile); + if (file != null) { return file; } + // if there's a 1st art variant try without it for .fullborder images + file = findFile(dir, TextUtil.fastReplace(fullborderFile, "1.fullborder", ".fullborder")); + if (file != null) { return file; } + // if there's an art variant try without it for .full images + file = findFile(dir, filename.replaceAll("[0-9].full]",".full")); + if (file != null) { return file; } + // if there's a 1st art variant try with it for .full images + file = findFile(dir, filename.replaceAll("[0-9]*.full", "1.full")); if (file != null) { return file; } } //if an image, like phenomenon or planes is missing .full in their filenames but you have an existing images that have .full/.fullborder diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java index 6b65424daab..af533330a5b 100644 --- a/forge-core/src/main/java/forge/StaticData.java +++ b/forge-core/src/main/java/forge/StaticData.java @@ -54,11 +54,11 @@ public class StaticData { private static StaticData lastInstance = null; - public StaticData(CardStorageReader cardReader, String editionFolder, String blockDataFolder) { - this(cardReader, null, editionFolder, blockDataFolder); + public StaticData(CardStorageReader cardReader, String editionFolder, String blockDataFolder, boolean enableUnknownCards) { + this(cardReader, null, editionFolder, blockDataFolder, enableUnknownCards); } - public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, String editionFolder, String blockDataFolder) { + public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, String editionFolder, String blockDataFolder, boolean enableUnknownCards) { this.cardReader = cardReader; this.tokenReader = tokenReader; this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder))); @@ -84,8 +84,8 @@ public class StaticData { variantCards = new CardDb(variantsCards, editions); //must initialize after establish field values for the sake of card image logic - commonCards.initialize(false, false); - variantCards.initialize(false, false); + commonCards.initialize(false, false, enableUnknownCards); + variantCards.initialize(false, false, enableUnknownCards); } { @@ -215,7 +215,7 @@ public class StaticData { public Predicate getStandardPredicate() { return standardPredicate; } public Predicate getPioneerPredicate() { return pioneerPredicate; } - + public Predicate getModernPredicate() { return modernPredicate; } public Predicate getCommanderPredicate() { return commanderPredicate; } diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index 2c4e54ad21a..a7b10ec600e 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -165,7 +165,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { reIndex(); } - public void initialize(boolean logMissingPerEdition, boolean logMissingSummary) { + public void initialize(boolean logMissingPerEdition, boolean logMissingSummary, boolean enableUnknownCards) { Set allMissingCards = new LinkedHashSet<>(); List missingCards = new ArrayList<>(); CardEdition upcomingSet = null; @@ -218,7 +218,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { if (!contains(cr.getName())) { if (upcomingSet != null) { addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown, 1)); - } else { + } else if(enableUnknownCards) { System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. "); addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special, 1)); } @@ -312,17 +312,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { return tryGetCard(request); } - public int getCardCollectorNumber(String cardName, String reqEdition) { + public String getCardCollectorNumber(String cardName, String reqEdition, int artIndex) { cardName = getName(cardName); CardEdition edition = editions.get(reqEdition); if (edition == null) - return -1; + return null; + int numMatches = 0; for (CardInSet card : edition.getCards()) { if (card.name.equalsIgnoreCase(cardName)) { - return card.collectorNumber; + numMatches += 1; + if (numMatches == artIndex) { + return card.collectorNumber; + } } } - return -1; + return null; } private PaperCard tryGetCard(CardRequest request) { diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index e2b786c0322..ce6289bb5f5 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -38,6 +38,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** @@ -75,10 +77,10 @@ public final class CardEdition implements Comparable { // immutable public static class CardInSet { public final CardRarity rarity; - public final int collectorNumber; + public final String collectorNumber; public final String name; - public CardInSet(final String name, final int collectorNumber, final CardRarity rarity) { + public CardInSet(final String name, final String collectorNumber, final CardRarity rarity) { this.name = name; this.collectorNumber = collectorNumber; this.rarity = rarity; @@ -86,7 +88,7 @@ public final class CardEdition implements Comparable { // immutable public String toString() { StringBuilder sb = new StringBuilder(); - if (collectorNumber != -1) { + if (collectorNumber != null) { sb.append(collectorNumber); sb.append(' '); } @@ -190,6 +192,7 @@ public final class CardEdition implements Comparable { // immutable public boolean getSmallSetOverride() { return smallSetOverride; } public String getBoosterMustContain() { return boosterMustContain; } public CardInSet[] getCards() { return cards; } + public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others public Map getTokens() { return tokenNormalized; } @@ -266,24 +269,33 @@ public final class CardEdition implements Comparable { // immutable Map tokenNormalized = new HashMap<>(); List processedCards = new ArrayList<>(); if (contents.containsKey("cards")) { + final Pattern pattern = Pattern.compile( + /* + The following pattern will match the WAR Japanese art entries, + it should also match the Un-set and older alternate art cards + like Merseine from FEM (should the editions files ever be updated) + */ + //"(^(?[0-9]+.?) )?((?[SCURML]) )?(?.*)$" + /* Ideally we'd use the named group above, but Android 6 and + earlier don't appear to support named groups. + So, untill support for those devices is officially dropped, + we'll have to suffice with numbered groups. + We are looking for: + * cnum - grouping #2 + * rarity - grouping #4 + * name - grouping #5 + */ + "(^([0-9]+.?) )?(([SCURML]) )?(.*)$" + ); for(String line : contents.get("cards")) { - if (StringUtils.isBlank(line)) - continue; - - // Optional collector number at the start. - String[] split = line.split(" ", 2); - int collectorNumber = -1; - if (split.length >= 2 && StringUtils.isNumeric(split[0])) { - collectorNumber = Integer.parseInt(split[0]); - line = split[1]; + Matcher matcher = pattern.matcher(line); + if (matcher.matches()) { + String collectorNumber = matcher.group(2); + CardRarity r = CardRarity.smartValueOf(matcher.group(4)); + String cardName = matcher.group(5); + CardInSet cis = new CardInSet(cardName, collectorNumber, r); + processedCards.add(cis); } - - // You may omit rarity for early development - CardRarity r = CardRarity.smartValueOf(line.substring(0, 1)); - boolean hadRarity = r != CardRarity.Unknown && line.charAt(1) == ' '; - String cardName = hadRarity ? line.substring(2) : line; - CardInSet cis = new CardInSet(cardName, collectorNumber, r); - processedCards.add(cis); } } diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index 322c003b6f2..a27b60adf2c 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -222,7 +222,12 @@ public final class CardRules implements ICardCharacteristics { public boolean canBeBrawlCommander() { CardType type = mainPart.getType(); - return (type.isLegendary() && type.isCreature()) || type.isPlaneswalker(); + return type.isLegendary() && (type.isCreature() || type.isPlaneswalker()); + } + + public boolean canBeTinyLeadersCommander() { + CardType type = mainPart.getType(); + return type.isLegendary() && (type.isCreature() || type.isPlaneswalker()); } public String getMeldWith() { diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java index 6679b3ad673..66dca47b592 100644 --- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java +++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java @@ -594,8 +594,10 @@ public final class CardRulesPredicates { public static final Predicate IS_VANGUARD = CardRulesPredicates.coreType(true, CardType.CoreType.Vanguard); public static final Predicate IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy); public static final Predicate IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land); - public static final Predicate CAN_BE_BRAWL_COMMANDER = Predicates.or(Presets.IS_PLANESWALKER, - Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY)); + public static final Predicate CAN_BE_BRAWL_COMMANDER = Predicates.and(Presets.IS_LEGENDARY, + Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER)); + public static final Predicate CAN_BE_TINY_LEADERS_COMMANDER = Predicates.and(Presets.IS_LEGENDARY, + Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER)); /** The Constant IS_NON_CREATURE_SPELL. **/ public static final Predicate IS_NON_CREATURE_SPELL = com.google.common.base.Predicates diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 3ea356c6a6d..e369fccc075 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -463,6 +463,9 @@ public enum DeckFormat { if (this.equals(DeckFormat.Brawl)) { return rules.canBeBrawlCommander(); } + if (this.equals(DeckFormat.TinyLeaders)) { + return rules.canBeTinyLeadersCommander(); + } return rules.canBeCommander(); } diff --git a/forge-core/src/main/java/forge/item/IPaperCard.java b/forge-core/src/main/java/forge/item/IPaperCard.java index 0c14064e059..53ea1d066e4 100644 --- a/forge-core/src/main/java/forge/item/IPaperCard.java +++ b/forge-core/src/main/java/forge/item/IPaperCard.java @@ -6,6 +6,7 @@ import forge.card.CardRarity; import forge.card.CardRules; import forge.card.CardType.CoreType; import forge.card.MagicColor; +import forge.util.PredicateCard; import forge.util.PredicateString; import org.apache.commons.lang3.StringUtils; @@ -60,6 +61,8 @@ public interface IPaperCard extends InventoryItem { return new PredicateNames(what); } + public static PredicateCards cards(final List what) { return new PredicateCards(what); } + private static final class PredicateColor implements Predicate { private final byte operand; @@ -161,6 +164,25 @@ public interface IPaperCard extends InventoryItem { } } + private static final class PredicateCards extends PredicateCard { + private final List operand; + + @Override + public boolean apply(final PaperCard card) { + for (final PaperCard element : this.operand) { + if (this.op(card, element)) { + return true; + } + } + return false; + } + + private PredicateCards(final List operand) { + super(StringOp.EQUALS); + this.operand = operand; + } + } + /** * Pre-built predicates are stored here to allow their re-usage and * easier access from code. diff --git a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java index 896e8d6de6b..833ee7e6bf7 100644 --- a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java +++ b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java @@ -566,12 +566,8 @@ public class BoosterGenerator { toAdd = IPaperCard.Predicates.printedInSets(sets); } else if (operator.startsWith("fromSheet(") && invert) { String sheetName = StringUtils.strip(operator.substring(9), "()\" "); - Iterable src = StaticData.instance().getPrintSheets().get(sheetName).toFlatList(); - List cardNames = Lists.newArrayList(); - for (PaperCard card : src) { - cardNames.add(card.getName()); - } - toAdd = IPaperCard.Predicates.names(Lists.newArrayList(cardNames)); + Iterable cards = StaticData.instance().getPrintSheets().get(sheetName).toFlatList(); + toAdd = IPaperCard.Predicates.cards(Lists.newArrayList(cards)); } if (toAdd == null) { diff --git a/forge-core/src/main/java/forge/util/PredicateCard.java b/forge-core/src/main/java/forge/util/PredicateCard.java new file mode 100644 index 00000000000..308b34f68ca --- /dev/null +++ b/forge-core/src/main/java/forge/util/PredicateCard.java @@ -0,0 +1,83 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2020 Jamin W. Collins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.util; + +import com.google.common.base.Predicate; +import forge.item.PaperCard; + +/** + * Special predicate class to perform string operations. + * + * @param + * the generic type + */ +public abstract class PredicateCard implements Predicate { + /** Possible operators for string operands. */ + public enum StringOp { + /** The EQUALS. */ + EQUALS, + } + + /** The operator. */ + private final StringOp operator; + + /** + * Op. + * + * @param op1 + * the op1 + * @param op2 + * the op2 + * @return true, if successful + */ + protected final boolean op(final PaperCard op1, final PaperCard op2) { + switch (this.getOperator()) { + case EQUALS: + return op1.equals(op2); + default: + return false; + } + } + + /** + * Instantiates a new predicate string. + * + * @param operator + * the operator + */ + public PredicateCard(final StringOp operator) { + this.operator = operator; + } + + /** + * @return the operator + */ + public StringOp getOperator() { + return operator; + } + + public static PredicateCard equals(final PaperCard what) { + return new PredicateCard(StringOp.EQUALS) { + @Override + public boolean apply(PaperCard subject) { + return op(subject, what); + } + }; + } + +} diff --git a/forge-core/src/main/java/forge/util/TextUtil.java b/forge-core/src/main/java/forge/util/TextUtil.java index 4974a8e087f..06a706b20f0 100644 --- a/forge-core/src/main/java/forge/util/TextUtil.java +++ b/forge-core/src/main/java/forge/util/TextUtil.java @@ -5,6 +5,8 @@ import forge.item.PaperCard; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import com.google.common.collect.ImmutableSortedMap; + import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -17,6 +19,22 @@ import java.util.Map.Entry; */ public class TextUtil { + static ImmutableSortedMap romanMap = ImmutableSortedMap.naturalOrder() + .put(1000, "M").put(900, "CM") + .put(500, "D").put(400, "CD") + .put(100, "C").put(90, "XC") + .put(50, "L").put(40, "XL") + .put(10, "X").put(9, "IX") + .put(5, "V").put(4, "IV").put(1, "I").build(); + + public final static String toRoman(int number) { + if (number <= 0) { + return ""; + } + int l = romanMap.floorKey(number); + return romanMap.get(l) + toRoman(number-l); + } + /** * Safely converts an object to a String. * diff --git a/forge-game/pom.xml b/forge-game/pom.xml index 117c4abbebd..e9cf2c327e8 100644 --- a/forge-game/pom.xml +++ b/forge-game/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.34-SNAPSHOT forge-game diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 849e2de4a0d..2a49bae9d59 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -914,4 +914,17 @@ public class Game { } return false; } + + public Player getControlVote() { + Player result = null; + long maxValue = 0; + for (Player p : getPlayers()) { + Long v = p.getHighestControlVote(); + if (v != null && v > maxValue) { + maxValue = v; + result = p; + } + } + return result; + } } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 16b67a2d4e7..c4e1a378be8 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -157,11 +157,6 @@ public class GameAction { c.removeSVar("EndOfTurnLeavePlay"); } - // Clean up temporary variables such as Sunburst value or announced PayX value - if (!(zoneTo.is(ZoneType.Stack) || zoneTo.is(ZoneType.Battlefield))) { - c.clearTemporaryVars(); - } - if (fromBattlefield && !toBattlefield) { c.getController().setRevolt(true); } @@ -173,7 +168,7 @@ public class GameAction { // if to Battlefield and it is caused by an replacement effect, // try to get previous LKI if able - if (zoneTo.is(ZoneType.Battlefield)) { + if (toBattlefield) { if (cause != null && cause.isReplacementAbility()) { ReplacementEffect re = cause.getReplacementEffect(); if (ReplacementType.Moved.equals(re.getMode())) { @@ -244,6 +239,12 @@ public class GameAction { } } + // Clean up temporary variables such as Sunburst value or announced PayX value + if (!(zoneTo.is(ZoneType.Stack) || zoneTo.is(ZoneType.Battlefield))) { + copied.clearTemporaryVars(); + } + + if (!suppress) { if (zoneFrom == null) { copied.getOwner().addInboundToken(copied); @@ -547,6 +548,13 @@ public class GameAction { c.setCastSA(null); } else if (zoneTo.is(ZoneType.Stack)) { c.setCastFrom(zoneFrom.getZoneType()); + if (cause != null && cause.isSpell() && c.equals(cause.getHostCard()) && !c.isCopiedSpell()) { + cause.setLastStateBattlefield(game.getLastStateBattlefield()); + cause.setLastStateGraveyard(game.getLastStateGraveyard()); + c.setCastSA(cause); + } else { + c.setCastSA(null); + } } else if (!(zoneTo.is(ZoneType.Battlefield) && zoneFrom.is(ZoneType.Stack))) { c.setCastFrom(null); c.setCastSA(null); @@ -977,7 +985,7 @@ public class GameAction { for (final Card c : game.getCardsIn(ZoneType.Battlefield)) { if (c.isCreature()) { // Rule 704.5f - Put into grave (no regeneration) for toughness <= 0 - if (c.getNetToughness() <= 0) { + if (c.getLethal() <= 0) { if (noRegCreats == null) { noRegCreats = new CardCollection(); } @@ -985,7 +993,7 @@ public class GameAction { checkAgain = true; } else if (c.hasKeyword("CARDNAME can't be destroyed by lethal damage unless lethal damage dealt by a single source is marked on it.")) { for (final Integer dmg : c.getReceivedDamageFromThisTurn().values()) { - if (c.getNetToughness() <= dmg.intValue()) { + if (c.getLethal() <= dmg.intValue()) { if (desCreats == null) { desCreats = new CardCollection(); } @@ -997,7 +1005,7 @@ public class GameAction { } // Rule 704.5g - Destroy due to lethal damage // Rule 704.5h - Destroy due to deathtouch - else if (c.getNetToughness() <= c.getDamage() || c.hasBeenDealtDeathtouchDamage()) { + else if (c.getLethal() <= c.getDamage() || c.hasBeenDealtDeathtouchDamage()) { if (desCreats == null) { desCreats = new CardCollection(); } diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index b54c727370f..be1b30a9199 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -22,19 +22,28 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import forge.card.MagicColor; import forge.card.mana.ManaCost; import forge.card.mana.ManaCostParser; +import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.card.CardPlayOption.PayManaCost; import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; import forge.game.player.Player; import forge.game.player.PlayerController; +import forge.game.replacement.ReplacementEffect; +import forge.game.replacement.ReplacementHandler; +import forge.game.replacement.ReplacementLayer; import forge.game.spellability.*; import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerHandler; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; +import forge.util.Lang; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -210,23 +219,34 @@ public final class GameActionUtil { } } - if (!sa.isBasicSpell()) { - return alternatives; - } - + // below are for some special cases of activated abilities if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) { - // set the cost to this directly to buypass non mana cost - final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>"); - newSA.setActivatingPlayer(activator); - newSA.setBasicSpell(false); - newSA.getMapParams().put("CostDesc", ManaCostParser.parse("0")); - // makes new SpellDescription - final StringBuilder sb = new StringBuilder(); - sb.append(newSA.getCostDescription()); - sb.append(newSA.getParam("SpellDescription")); - newSA.setDescription(sb.toString()); - alternatives.add(newSA); + for (final KeywordInterface inst : source.getKeywords()) { + // need to find the correct Keyword from which this Ability is from + if (!inst.getAbilities().contains(sa)) { + continue; + } + + // set the cost to this directly to buypass non mana cost + final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>"); + newSA.setActivatingPlayer(activator); + newSA.getMapParams().put("CostDesc", ManaCostParser.parse("0")); + + // need to build a new Keyword to get better Reminder Text + String data[] = inst.getOriginal().split(":"); + data[1] = "0"; + KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":")); + + // makes new SpellDescription + final StringBuilder sb = new StringBuilder(); + sb.append(newSA.getCostDescription()); + sb.append("(").append(newKi.getReminderText()).append(")"); + newSA.setDescription(sb.toString()); + + alternatives.add(newSA); + break; + } } if (sa.hasParam("Equip") && activator.hasKeyword("EquipInstantSpeed")) { @@ -363,10 +383,11 @@ public final class GameActionUtil { } SpellAbility result = null; final Card host = sa.getHostCard(); + final Game game = host.getGame(); final Player activator = sa.getActivatingPlayer(); final PlayerController pc = activator.getController(); - host.getGame().getAction().checkStaticAbilities(false); + game.getAction().checkStaticAbilities(false); boolean reset = false; @@ -429,7 +450,60 @@ public final class GameActionUtil { int v = pc.chooseNumberForKeywordCost(sa, cost, ki, str, Integer.MAX_VALUE); if (v > 0) { - host.addReplacementEffect(CardFactoryUtil.makeEtbCounter("etbCounter:P1P1:" + v, host, false)); + + final Card eff = new Card(game.nextCardId(), game); + eff.setTimestamp(game.getNextTimestamp()); + eff.setName(c.getName() + "'s Effect"); + eff.addType("Effect"); + eff.setToken(true); // Set token to true, so when leaving play it gets nuked + eff.setOwner(activator); + + eff.setImageKey(c.getImageKey()); + eff.setColor(MagicColor.COLORLESS); + eff.setImmutable(true); + // try to get the SpellAbility from the mana ability + //eff.setEffectSource((SpellAbility)null); + + eff.addRemembered(host); + + String abStr = "DB$ PutCounter | Defined$ ReplacedCard | CounterType$ P1P1 | ETB$ True | CounterNum$ " + v; + + SpellAbility saAb = AbilityFactory.getAbility(abStr, c); + + CardFactoryUtil.setupETBReplacementAbility(saAb); + + String desc = "It enters the battlefield with "; + desc += Lang.nounWithNumeral(v, CounterType.P1P1.getName() + " counter"); + desc += " on it."; + + String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | Description$ " + desc; + + ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true); + re.setLayer(ReplacementLayer.Other); + re.setOverridingAbility(saAb); + + eff.addReplacementEffect(re); + + // Forgot Trigger + String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Any | TriggerZones$ Command | Static$ True"; + String forgetEffect = "DB$ Pump | ForgetObjects$ TriggeredCard"; + String exileEffect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile" + + " | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0"; + + SpellAbility saForget = AbilityFactory.getAbility(forgetEffect, eff); + AbilitySub saExile = (AbilitySub) AbilityFactory.getAbility(exileEffect, eff); + saForget.setSubAbility(saExile); + + final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, eff, true); + parsedTrigger.setOverridingAbility(saForget); + eff.addTrigger(parsedTrigger); + eff.updateStateForView(); + + // TODO: Add targeting to the effect so it knows who it's dealing with + game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); + game.getAction().moveTo(ZoneType.Command, eff, null); + game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); + if (result == null) { result = sa.copy(); } diff --git a/forge-game/src/main/java/forge/game/GameFormat.java b/forge-game/src/main/java/forge/game/GameFormat.java index e4efda21fb6..a66395ce300 100644 --- a/forge-game/src/main/java/forge/game/GameFormat.java +++ b/forge-game/src/main/java/forge/game/GameFormat.java @@ -47,7 +47,7 @@ import java.util.Map.Entry; public class GameFormat implements Comparable { private final String name; public enum FormatType {Sanctioned, Casual, Historic, Digital, Custom} - public enum FormatSubType {Block, Standard, Extended, Pioneer, Modern, Legacy, Vintage, Commander, Planechase, Videogame, MTGO, Custom} + public enum FormatSubType {Block, Standard, Extended, Pioneer, Modern, Legacy, Vintage, Commander, Planechase, Videogame, MTGO, Arena, Custom} // contains allowed sets, when empty allows all sets private FormatType formatType; diff --git a/forge-game/src/main/java/forge/game/StaticEffect.java b/forge-game/src/main/java/forge/game/StaticEffect.java index dc9d51094a0..d6ce1813d7e 100644 --- a/forge-game/src/main/java/forge/game/StaticEffect.java +++ b/forge-game/src/main/java/forge/game/StaticEffect.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -37,7 +37,7 @@ import com.google.common.collect.Maps; *

* StaticEffect class. *

- * + * * @author Forge * @version $Id$ */ @@ -72,7 +72,7 @@ public class StaticEffect { /** * setTimestamp TODO Write javadoc for this method. - * + * * @param t * a long */ @@ -82,7 +82,7 @@ public class StaticEffect { /** * getTimestamp. TODO Write javadoc for this method. - * + * * @return a long */ public final long getTimestamp() { @@ -93,7 +93,7 @@ public class StaticEffect { *

* Getter for the field source. *

- * + * * @return a {@link forge.game.card.Card} object. */ public final Card getSource() { @@ -104,7 +104,7 @@ public class StaticEffect { *

* Getter for the field affectedCards. *

- * + * * @return a {@link forge.CardList} object. */ public final CardCollectionView getAffectedCards() { @@ -115,7 +115,7 @@ public class StaticEffect { *

* Setter for the field affectedCards. *

- * + * * @param list * a {@link forge.CardList} object. */ @@ -125,7 +125,7 @@ public class StaticEffect { /** * Gets the affected players. - * + * * @return the affected players */ public final List getAffectedPlayers() { @@ -134,7 +134,7 @@ public class StaticEffect { /** * Sets the affected players. - * + * * @param list * the new affected players */ @@ -144,7 +144,7 @@ public class StaticEffect { /** * setParams. TODO Write javadoc for this method. - * + * * @param params * a HashMap */ @@ -154,7 +154,7 @@ public class StaticEffect { /** * Gets the params. - * + * * @return the params */ public final Map getParams() { @@ -171,13 +171,12 @@ public class StaticEffect { /** * Undo everything that was changed by this effect. - * + * * @return a {@link CardCollectionView} of all affected cards. */ final CardCollectionView remove() { final CardCollectionView affectedCards = getAffectedCards(); final List affectedPlayers = getAffectedPlayers(); - //final Map params = getParams(); String changeColorWordsTo = null; @@ -245,6 +244,10 @@ public class StaticEffect { p.removeMaxLandPlays(getTimestamp()); p.removeMaxLandPlaysInfinite(getTimestamp()); + + p.removeControlVote(getTimestamp()); + p.removeAdditionalVote(getTimestamp()); + p.removeAdditionalOptionalVote(getTimestamp()); } // modify the affected card diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index f5b40e4ca3a..b748e8814d8 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -25,6 +25,8 @@ import forge.game.player.Player; import forge.game.player.PlayerCollection; import forge.game.player.PlayerPredicates; import forge.game.spellability.*; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.Expressions; import forge.util.TextUtil; @@ -1573,11 +1575,51 @@ public class AbilityUtils { // special logic for xPaid in SpellAbility if (sq[0].contains("xPaid")) { - // ETB effects of cloned cards have xPaid = 0 - if (sa.hasParam("ETB") && sa.getOriginalHost() != null) { + SpellAbility root = sa.getRootAbility(); + + // 107.3i If an object gains an ability, the value of X within that ability is the value defined by that ability, + // or 0 if that ability doesn’t define a value of X. This is an exception to rule 107.3h. This may occur with ability-adding effects, text-changing effects, or copy effects. + if (root.getXManaCostPaid() != null) { + return CardFactoryUtil.doXMath(root.getXManaCostPaid(), expr, c); + } + + // If the chosen creature has X in its mana cost, that X is considered to be 0. + // The value of X in Altered Ego’s last ability will be whatever value was chosen for X while casting Altered Ego. + if (sa.getOriginalHost() != null || !sa.getHostCard().equals(c)) { return 0; } - return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + + if (root.isTrigger()) { + Trigger t = root.getTrigger(); + if (t == null) { + return 0; + } + + // 107.3k If an object’s enters-the-battlefield triggered ability or replacement effect refers to X, + // and the spell that became that object as it resolved had a value of X chosen for any of its costs, + // the value of X for that ability is the same as the value of X for that spell, although the value of X for that permanent is 0. + if (TriggerType.ChangesZone.equals(t.getMode()) + && ZoneType.Battlefield.name().equals(t.getParam("Destination"))) { + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } else if (TriggerType.SpellCast.equals(t.getMode())) { + // Cast Trigger like Hydroid Krasis + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } else if (TriggerType.Cycled.equals(t.getMode())) { + SpellAbility cycleSA = (SpellAbility) sa.getTriggeringObject(AbilityKey.Cause); + if (cycleSA == null) { + return 0; + } + return CardFactoryUtil.doXMath(cycleSA.getXManaCostPaid(), expr, c); + } + } + + if (root.isReplacementAbility()) { + if (sa.hasParam("ETB")) { + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } + } + + return 0; } // Count$Kicked.. diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index d90b9c90396..81f48f70209 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -112,9 +112,8 @@ public abstract class SpellAbilityEffect { sb.append(" "); sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace(svar,"=",String.valueOf(amount)))); } else{ - if (sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null && - sa.getPayCosts().getCostMana().getAmountOfX() > 0) { - int amount = sa.getHostCard().getXManaCostPaid(); + if (sa.costHasManaX()) { + int amount = sa.getXManaCostPaid() == null ? 0 : sa.getXManaCostPaid(); sb.append(" "); sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace("X","=",String.valueOf(amount)))); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java index c5a46346887..a1d21c94c0f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java @@ -67,8 +67,9 @@ public class ChooseGenericEffect extends SpellAbilityEffect { SpellAbility chosenSA = null; if (sa.hasParam("AtRandom")) { - int idxChosen = MyRandom.getRandom().nextInt(abilities.size()); - chosenSA = abilities.get(idxChosen); + if (!abilities.isEmpty()) { + chosenSA = abilities.get(MyRandom.getRandom().nextInt(abilities.size())); + } } else { chosenSA = p.getController().chooseSingleSpellForEffect(abilities, sa, Localizer.getInstance().getMessage("lblChooseOne"), ImmutableMap.of()); @@ -90,7 +91,7 @@ public class ChooseGenericEffect extends SpellAbilityEffect { if (fallback != null) { p.getGame().fireEvent(new GameEventCardModeChosen(p, host.getName(), fallback.getDescription(), sa.hasParam("ShowChoice"))); AbilityUtils.resolve(fallback); - } else { + } else if (!sa.hasParam("AtRandom")) { System.err.println("Warning: all Unless costs were unpayable for " + host.getName() +", but it had no FallbackAbility defined. Doing nothing (this is most likely incorrect behavior)."); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java index b2705b69516..9cbd6c7258e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java @@ -28,9 +28,9 @@ public class CountersNoteEffect extends SpellAbilityEffect { GameEntityCounterTable table = new GameEntityCounterTable(); for (Card c : getDefinedCardsOrTargeted(sa)) { if (mode.equals(MODE_STORE)) { - noteCounters(c, source); + noteCounters(c, c); } else if (mode.equals(MODE_LOAD)) { - loadCounters(c, source, p, table); + loadCounters(c, c, p, table); } } table.triggerCountersPutAll(game); diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java index 2f48a5a20f6..c7ba5ebf244 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java @@ -78,7 +78,9 @@ public class CountersPutEffect extends SpellAbilityEffect { final List targetCards = SpellAbilityEffect.getTargetCards(spellAbility); for(int i = 0; i < targetCards.size(); i++) { Card targetCard = targetCards.get(i); - stringBuilder.append(targetCard).append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" counter)"); + stringBuilder.append(targetCard); + if (spellAbility.getTargetRestrictions().getDividedMap().get(targetCard) != null) // fix null counter stack description + stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" counter)"); if(i == targetCards.size() - 2) { stringBuilder.append(" and "); @@ -104,7 +106,6 @@ public class CountersPutEffect extends SpellAbilityEffect { } } stringBuilder.append("."); - return stringBuilder.toString(); } @@ -147,8 +148,30 @@ public class CountersPutEffect extends SpellAbilityEffect { if (sa.hasParam("Bolster")) { CardCollection creatsYouCtrl = CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), Presets.CREATURES); CardCollection leastToughness = new CardCollection(Aggregates.listWithMin(creatsYouCtrl, CardPredicates.Accessors.fnGetDefense)); - tgtCards.addAll(pc.chooseCardsForEffect(leastToughness, sa, Localizer.getInstance().getMessage("lblChooseACreatureWithLeastToughness"), 1, 1, false)); + tgtCards.addAll(activator.getController().chooseCardsForEffect(leastToughness, sa, Localizer.getInstance().getMessage("lblChooseACreatureWithLeastToughness"), 1, 1, false)); tgtObjects.addAll(tgtCards); + } else if (sa.hasParam("Choices")) { + ZoneType choiceZone = ZoneType.Battlefield; + if (sa.hasParam("ChoiceZone")) { + choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone")); + } + Player chooser = activator; + if (sa.hasParam("Chooser")) { + List choosers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa); + if (choosers.isEmpty()) { + return; + } + chooser = choosers.get(0); + } + + CardCollection choices = new CardCollection(game.getCardsIn(choiceZone)); + + int n = sa.hasParam("ChoiceAmount") ? Integer.parseInt(sa.getParam("ChoiceAmount")) : 1; + + choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, card, sa); + + String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseaCard") + " "; + tgtObjects.addAll(new CardCollection(chooser.getController().chooseCardsForEffect(choices, sa, title, n, n, sa.hasParam("ChoiceOptional")))); } else { tgtObjects.addAll(getDefinedOrTargeted(sa, "Defined")); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java index 9a80817d6e3..2162c2d6b1c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java @@ -30,7 +30,12 @@ public class DamageDealEffect extends DamageBaseEffect { // when damageStackDescription is called, just build exactly what is happening final StringBuilder stringBuilder = new StringBuilder(); final String damage = spellAbility.getParam("NumDmg"); - final int dmg = AbilityUtils.calculateAmount(spellAbility.getHostCard(), damage, spellAbility); + int dmg; + try { // try-catch to fix Volcano Hellion Crash + dmg = AbilityUtils.calculateAmount(spellAbility.getHostCard(), damage, spellAbility); + } catch (NullPointerException e) { + dmg = 0; + } List targets = SpellAbilityEffect.getTargets(spellAbility); if (targets.isEmpty()) { @@ -53,7 +58,8 @@ public class DamageDealEffect extends DamageBaseEffect { stringBuilder.append("divided evenly (rounded down) to\n"); } else if (spellAbility.hasParam("DividedAsYouChoose")) { stringBuilder.append("divided to\n"); - } + } else + stringBuilder.append("to "); final List targetCards = SpellAbilityEffect.getTargetCards(spellAbility); final List players = SpellAbilityEffect.getTargetPlayers(spellAbility); @@ -63,7 +69,9 @@ public class DamageDealEffect extends DamageBaseEffect { // target cards for (int i = 0; i < targetCards.size(); i++) { Card targetCard = targetCards.get(i); - stringBuilder.append(targetCard).append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" damage)"); + stringBuilder.append(targetCard); + if (spellAbility.getTargetRestrictions().getDividedMap().get(targetCard) != null) //fix null damage stack description + stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" damage)"); if (i == targetCount - 2) { stringBuilder.append(" and "); @@ -74,9 +82,10 @@ public class DamageDealEffect extends DamageBaseEffect { // target players for (int i = 0; i < players.size(); i++) { - Player targetPlayer = players.get(i); - stringBuilder.append(targetPlayer).append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer)).append(" damage)"); + stringBuilder.append(targetPlayer); + if (spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer) != null) //fix null damage stack description + stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer)).append(" damage)"); if (i == players.size() - 2) { stringBuilder.append(" and "); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java index 442fe3c5340..2595c792dec 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java @@ -70,7 +70,7 @@ public class DebuffEffect extends SpellAbilityEffect { for (final Card tgtC : getTargetCards(sa)) { final List addedKW = Lists.newArrayList(); final List removedKW = Lists.newArrayList(); - if (tgtC.isInPlay() && tgtC.canBeTargetedBy(sa)) { + if (tgtC.isInPlay() && (!sa.usesTargeting() || tgtC.canBeTargetedBy(sa))) { if (sa.hasParam("AllSuffixKeywords")) { String suffix = sa.getParam("AllSuffixKeywords"); for (final KeywordInterface kw : tgtC.getKeywords()) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java b/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java index 769e758388c..cc572a9c5fd 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java @@ -1,6 +1,7 @@ package forge.game.ability.effects; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -9,9 +10,9 @@ import forge.game.ability.AbilityKey; import org.apache.commons.lang3.StringUtils; import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.game.Game; import forge.game.ability.AbilityFactory; @@ -19,10 +20,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardLists; -import forge.game.card.CardPredicates; import forge.game.player.Player; -import forge.game.player.PlayerCollection; -import forge.game.player.PlayerPredicates; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.trigger.TriggerType; @@ -67,33 +65,29 @@ public class VoteEffect extends SpellAbilityEffect { return; } - // starting with the activator - int pSize = tgtPlayers.size(); Player activator = sa.getActivatingPlayer(); - while (tgtPlayers.contains(activator) && !activator.equals(Iterables.getFirst(tgtPlayers, null))) { - tgtPlayers.add(pSize - 1, tgtPlayers.remove(0)); + + // starting with the activator + int aidx = tgtPlayers.indexOf(activator); + if (aidx != -1) { + Collections.rotate(tgtPlayers, -aidx); } + ListMultimap votes = ArrayListMultimap.create(); - Player voter = null; - - PlayerCollection voters = game.getPlayers().filter(PlayerPredicates.hasKeyword("You choose how each player votes this turn.")); - - if (voters.size() > 1) { - List illusions = CardLists.filter(voters.getCardsIn(ZoneType.Command), CardPredicates.nameEquals("Illusion of Choice Effect")); - voter = Collections.max(illusions, CardPredicates.compareByTimestamp()).getController(); - } else if (voters.size() == 1) { - voter = voters.get(0); - } + Player voter = game.getControlVote(); for (final Player p : tgtPlayers) { - int voteAmount = p.getKeywords().getAmount("You get an additional vote.") + 1; - int optionalVotes = p.getKeywords().getAmount("You may vote an additional time."); - voteAmount += p.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblHowManyAdditionalVotesDoYouWant"), 0, optionalVotes); + int voteAmount = p.getAdditionalVotesAmount() + 1; + int optionalVotes = p.getAdditionalOptionalVotesAmount(); Player realVoter = voter == null ? p : voter; + Map params = Maps.newHashMap(); + params.put("Voter", realVoter); + voteAmount += p.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblHowManyAdditionalVotesDoYouWant"), 0, optionalVotes, params); + for (int i = 0; i < voteAmount; i++) { - Object result = realVoter.getController().vote(sa, host + Localizer.getInstance().getMessage("lblVote") + ":", voteType, votes); + Object result = realVoter.getController().vote(sa, host + Localizer.getInstance().getMessage("lblVote") + ":", voteType, votes, p); votes.put(result, p); host.getGame().getAction().nofityOfValue(sa, p, result + "\r\n" + Localizer.getInstance().getMessage("lblCurrentVote") + ":" + votes, p); @@ -104,34 +98,49 @@ public class VoteEffect extends SpellAbilityEffect { runParams.put(AbilityKey.AllVotes, votes); game.getTriggerHandler().runTrigger(TriggerType.Vote, runParams, false); - List subAbs = Lists.newArrayList(); - final List mostVotes = getMostVotes(votes); - if (sa.hasParam("Tied") && mostVotes.size() > 1) { - subAbs.add(sa.getParam("Tied")); - } else if (sa.hasParam("VoteSubAbility")) { - for (final Object o : mostVotes) { - host.addRemembered(o); - } - subAbs.add(sa.getParam("VoteSubAbility")); - } else { - for (Object type : mostVotes) { - subAbs.add(sa.getParam("Vote" + type.toString())); - } - } - if (sa.hasParam("StoreVoteNum")) { - for (final Object type : voteType) { - host.setSVar("VoteNum" + type, "Number$" + votes.get(type).size()); - } - } else { - for (final String subAb : subAbs) { - final SpellAbility action = AbilityFactory.getAbility(host.getSVar(subAb), host); + if (sa.hasParam("EachVote")) { + for (Map.Entry> e : votes.asMap().entrySet()) { + final SpellAbility action = AbilityFactory.getAbility(host, sa.getParam("Vote" + e.getKey().toString())); + action.setActivatingPlayer(sa.getActivatingPlayer()); ((AbilitySub) action).setParent(sa); - AbilityUtils.resolve(action); + + for (Player p : e.getValue()) { + host.addRemembered(p); + AbilityUtils.resolve(action); + host.removeRemembered(p); + } + } + } else { + List subAbs = Lists.newArrayList(); + final List mostVotes = getMostVotes(votes); + if (sa.hasParam("Tied") && mostVotes.size() > 1) { + subAbs.add(sa.getParam("Tied")); + } else if (sa.hasParam("VoteSubAbility")) { + for (final Object o : mostVotes) { + host.addRemembered(o); + } + subAbs.add(sa.getParam("VoteSubAbility")); + } else { + for (Object type : mostVotes) { + subAbs.add(sa.getParam("Vote" + type.toString())); + } + } + if (sa.hasParam("StoreVoteNum")) { + for (final Object type : voteType) { + host.setSVar("VoteNum" + type, "Number$" + votes.get(type).size()); + } + } else { + for (final String subAb : subAbs) { + final SpellAbility action = AbilityFactory.getAbility(host, subAb); + action.setActivatingPlayer(sa.getActivatingPlayer()); + ((AbilitySub) action).setParent(sa); + AbilityUtils.resolve(action); + } + } + if (sa.hasParam("VoteSubAbility")) { + host.clearRemembered(); } - } - if (sa.hasParam("VoteSubAbility")) { - host.clearRemembered(); } } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 0be3279d3e4..548e7001496 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -129,6 +129,8 @@ public class Card extends GameEntity implements Comparable { private final Multimap cantHaveKeywords = MultimapBuilder.hashKeys().enumSetValues(Keyword.class).build(); + private final Map counterTypeTimestamps = Maps.newEnumMap(CounterType.class); + private final Map canBlockAdditional = Maps.newTreeMap(); private final Set canBlockAny = Sets.newHashSet(); @@ -220,7 +222,6 @@ public class Card extends GameEntity implements Comparable { private int turnInZone; - private int xManaCostPaid = 0; private Map xManaCostPaidByColor; private int sunburstValue = 0; @@ -1087,10 +1088,11 @@ public class Card extends GameEntity implements Comparable { } public final int getXManaCostPaid() { - return xManaCostPaid; - } - public final void setXManaCostPaid(final int n) { - xManaCostPaid = n; + if (getCastSA() != null) { + Integer paid = getCastSA().getXManaCostPaid(); + return paid == null ? 0 : paid; + } + return 0; } public final Map getXManaCostPaidByColor() { @@ -1305,12 +1307,44 @@ public class Card extends GameEntity implements Comparable { getController().addCounterToPermThisTurn(counterType, addAmount); view.updateCounters(this); } + if (newValue <= 0) { + removeCounterTimestamp(counterType); + } else { + addCounterTimestamp(counterType); + } if (table != null) { table.put(this, counterType, addAmount); } return addAmount; } + public boolean addCounterTimestamp(CounterType counterType) { + return addCounterTimestamp(counterType, true); + } + public boolean addCounterTimestamp(CounterType counterType, boolean updateView) { + if (!counterType.isKeywordCounter()) { + return false; + } + removeCounterTimestamp(counterType); + + long timestamp = game.getNextTimestamp(); + counterTypeTimestamps.put(counterType, timestamp); + addChangedCardKeywords(ImmutableList.of(counterType.getKeyword().toString()), null, false, false, timestamp, updateView); + return true; + } + + public boolean removeCounterTimestamp(CounterType counterType) { + return removeCounterTimestamp(counterType, true); + } + + public boolean removeCounterTimestamp(CounterType counterType, boolean updateView) { + Long old = counterTypeTimestamps.remove(counterType); + if (old != null) { + removeChangedCardKeywords(old, updateView); + } + return old != null; + } + /** *

* addCountersAddedBy. @@ -1358,6 +1392,10 @@ public class Card extends GameEntity implements Comparable { setCounters(counterName, newValue); view.updateCounters(this); + if (newValue <= 0) { + this.removeCounterTimestamp(counterName); + } + //fire card stats changed event if p/t bonuses or loyalty changed from subtracted counters if (powerBonusBefore != getPowerBonusFromCounters() || toughnessBonusBefore != getToughnessBonusFromCounters() || loyaltyBefore != getCurrentLoyalty()) { getGame().fireEvent(new GameEventCardStatsChanged(this)); @@ -1380,8 +1418,23 @@ public class Card extends GameEntity implements Comparable { @Override public final void setCounters(final Map allCounters) { + boolean changed = false; + for (CounterType ct : counters.keySet()) { + if (removeCounterTimestamp(ct, false)) { + changed = true; + } + } counters = allCounters; view.updateCounters(this); + + for (CounterType ct : counters.keySet()) { + if (addCounterTimestamp(ct, false)) { + changed = true; + } + } + if (changed) { + updateKeywords(); + } } @Override @@ -1389,6 +1442,16 @@ public class Card extends GameEntity implements Comparable { if (counters.isEmpty()) { return; } counters.clear(); view.updateCounters(this); + + boolean changed = false; + for (CounterType ct : counterTypeTimestamps.keySet()) { + if (removeCounterTimestamp(ct, false)) { + changed = true; + } + } + if (changed) { + updateKeywords(); + } } public final String getSVar(final String var) { @@ -3938,7 +4001,9 @@ public class Card extends GameEntity implements Comparable { keywordsGrantedByTextChanges.add(newKw); } } - addChangedCardKeywordsInternal(addKeywords, removeKeywords, false, false, timestamp, true); + if (!addKeywords.isEmpty() || !removeKeywords.isEmpty()) { + addChangedCardKeywordsInternal(addKeywords, removeKeywords, false, false, timestamp, true); + } } private void updateKeywordsOnRemoveChangedText(final KeywordsChange k) { @@ -4729,9 +4794,21 @@ public class Card extends GameEntity implements Comparable { return false; } + // this is the amount of damage a creature needs to receive before it dies + public final int getLethal() { + if (getAmountOfKeyword("Lethal damage dealt to CARDNAME is determined by its power rather than its toughness.") % 2 !=0) { + return getNetPower(); } + else { + return getNetToughness(); } + } + // this is the minimal damage a trampling creature has to assign to a blocker public final int getLethalDamage() { - return getNetToughness() - getDamage() - getTotalAssignedDamage(); + if (getAmountOfKeyword("Lethal damage dealt to CARDNAME is determined by its power rather than its toughness.") % 2 !=0) { + return getNetPower() - getDamage() - getTotalAssignedDamage(); + } + else { + return getNetToughness() - getDamage() - getTotalAssignedDamage();} } public final int getDamage() { @@ -6273,10 +6350,13 @@ public class Card extends GameEntity implements Comparable { public final void addGoad(Long timestamp, final Player p) { goad.put(timestamp, p); + updateAbilityTextForView(); } public final void removeGoad(Long timestamp) { - goad.remove(timestamp); + if (goad.remove(timestamp) != null) { + updateAbilityTextForView(); + } } public final boolean isGoaded() { @@ -6353,6 +6433,9 @@ public class Card extends GameEntity implements Comparable { removeSVar("PayX"); // Temporary AI X announcement variable removeSVar("IsCastFromPlayEffect"); // Temporary SVar indicating that the spell is cast indirectly via AF Play setSunburstValue(0); // Sunburst + setXManaCostPaidByColor(null); + setKickerMagnitude(0); + setPseudoMultiKickerMagnitude(0); } public final int getFinalChapterNr() { diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 5fa4868727e..163df5aefb8 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -185,18 +185,12 @@ public class CardFactory { c.setCopiedSpell(true); if (bCopyDetails) { - c.setXManaCostPaid(original.getXManaCostPaid()); c.setXManaCostPaidByColor(original.getXManaCostPaidByColor()); c.setKickerMagnitude(original.getKickerMagnitude()); // Rule 706.10 : Madness is copied if (original.isInZone(ZoneType.Stack)) { c.setMadness(original.isMadness()); - - final SpellAbilityStackInstance si = controller.getGame().getStack().getInstanceFromSpellAbility(sa); - if (si != null) { - c.setXManaCostPaid(si.getXManaPaid()); - } } for (OptionalCost cost : original.getOptionalCostsPaid()) { @@ -875,6 +869,13 @@ public class CardFactory { } } + for (final Trigger trigger : state.getTriggers()) { + final SpellAbility newSa = trigger.getOverridingAbility(); + if (newSa != null && newSa.getOriginalHost() == null) { + newSa.setOriginalHost(in); + } + } + if (sa.hasParam("GainTextOf") && originalState != null) { state.setSetCode(originalState.getSetCode()); state.setRarity(originalState.getRarity()); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index e60f3e16bd1..88307959cdb 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -20,7 +20,6 @@ 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.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -3010,24 +3009,43 @@ public class CardFactoryUtil { inst.addTrigger(parsedTrigger); } else if (keyword.startsWith("Saga")) { - // Saga there doesn't need Max value anymore? final String[] k = keyword.split(":"); - final String[] abs = k[2].split(","); + final List abs = Arrays.asList(k[2].split(",")); + if (abs.size() != Integer.valueOf(k[1])) { + throw new RuntimeException("Saga max differ from Ability amount"); + } - int i = 1; - for (String ab : abs) { - SpellAbility sa = AbilityFactory.getAbility(card, ab); - sa.setChapter(i); + int idx = 0; + int skipId = 0; + for(String ab : abs) { + idx += 1; + if (idx <= skipId) { + continue; + } - // TODO better logic for Roman numbers - // In the Description try to merge Chapter trigger with the Same Effect - String trigStr = "Mode$ CounterAdded | ValidCard$ Card.Self | TriggerZones$ Battlefield" - + "| CounterType$ LORE | CounterAmount$ EQ" + i - + "| TriggerDescription$ " + Strings.repeat("I", i) + " - " + sa.getDescription(); - final Trigger t = TriggerHandler.parseTrigger(trigStr, card, intrinsic); - t.setOverridingAbility(sa); - inst.addTrigger(t); - ++i; + skipId = idx + abs.subList(idx - 1, abs.size()).lastIndexOf(ab); + StringBuilder desc = new StringBuilder(); + for (int i = idx; i <= skipId; i++) { + if (i != idx) { + desc.append(", "); + } + desc.append(TextUtil.toRoman(i)); + } + + for (int i = idx; i <= skipId; i++) { + SpellAbility sa = AbilityFactory.getAbility(card, ab); + sa.setChapter(i); + + StringBuilder trigStr = new StringBuilder("Mode$ CounterAdded | ValidCard$ Card.Self | TriggerZones$ Battlefield"); + trigStr.append("| CounterType$ LORE | CounterAmount$ EQ").append(i); + if (i != idx) { + trigStr.append(" | Secondary$ True"); + } + trigStr.append("| TriggerDescription$ ").append(desc).append(" — ").append(sa.getDescription()); + final Trigger t = TriggerHandler.parseTrigger(trigStr.toString(), card, intrinsic); + t.setOverridingAbility(sa); + inst.addTrigger(t); + } } } else if (keyword.equals("Soulbond")) { // Setup ETB trigger for card with Soulbond keyword diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index 53e52c0a69c..c14d8610cff 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -1518,27 +1518,38 @@ public class CardProperty { } else if (property.startsWith("blockedByThisTurn")) { return !card.getBlockedByThisTurn().isEmpty(); } else if (property.startsWith("blockedValidThisTurn ")) { - if (card.getBlockedThisTurn() == null) { + CardCollectionView blocked = card.getBlockedThisTurn(); + if (blocked == null) { return false; } - String valid = property.split(" ")[1]; - for(Card c : card.getBlockedThisTurn()) { + for(Card c : blocked) { if (c.isValid(valid, card.getController(), source, spellAbility)) { return true; } } + for(Card c : AbilityUtils.getDefinedCards(source, valid, spellAbility)) { + if (blocked.contains(c)) { + return true; + } + }; return false; } else if (property.startsWith("blockedByValidThisTurn ")) { - if (card.getBlockedByThisTurn() == null) { + CardCollectionView blocked = card.getBlockedByThisTurn(); + if (blocked == null) { return false; } String valid = property.split(" ")[1]; - for(Card c : card.getBlockedByThisTurn()) { + for(Card c : blocked) { if (c.isValid(valid, card.getController(), source, spellAbility)) { return true; } } + for(Card c : AbilityUtils.getDefinedCards(source, valid, spellAbility)) { + if (blocked.contains(c)) { + return true; + } + }; return false; } else if (property.startsWith("blockedBySourceThisTurn")) { return source.getBlockedByThisTurn().contains(card); @@ -1779,4 +1790,4 @@ public class CardProperty { return true; } -} \ No newline at end of file +} diff --git a/forge-game/src/main/java/forge/game/card/CounterType.java b/forge-game/src/main/java/forge/game/card/CounterType.java index 5d9fd5970d6..bbaf7204876 100644 --- a/forge-game/src/main/java/forge/game/card/CounterType.java +++ b/forge-game/src/main/java/forge/game/card/CounterType.java @@ -20,6 +20,8 @@ package forge.game.card; import com.google.common.collect.ImmutableList; +import forge.game.keyword.Keyword; + /** * The class Counters. * @@ -109,6 +111,8 @@ public enum CounterType { FLOOD("FLOOD", 0, 203, 255), + FORESHADOW("FRSHD",144,99, 207), + FUNGUS("FNGUS", 121, 219, 151), FURY("FURY", 255, 120, 89), @@ -309,7 +313,23 @@ public enum CounterType { EXPERIENCE("EXP"), - POISON("POISN"); + POISON("POISN"), + + // Keyword Counters + + FLYING("Flying"), + FIRSTSTRIKE("First Strike"), + DOUBLESTRIKE("Double Strike"), + DEATHTOUCH("Deathtouch"), + HEXPROOF("Hexproof"), + INDESTRUCTIBLE("Indestructible"), + LIFELINK("Lifelink"), + MENACE("Menace"), + REACH("Reach"), + TRAMPLE("Trample"), + VIGILANCE("Vigilance") + + ; private String name, counterOnCardDisplayName; private int red, green, blue; @@ -365,6 +385,39 @@ public enum CounterType { return Enum.valueOf(CounterType.class, replacedName); } + public boolean isKeywordCounter() { + return this.getKeyword() != null; + } + + public Keyword getKeyword() { + switch (this) { + case FLYING: + return Keyword.FLYING; + case FIRSTSTRIKE: + return Keyword.FIRST_STRIKE; + case DOUBLESTRIKE: + return Keyword.DOUBLE_STRIKE; + case DEATHTOUCH: + return Keyword.DEATHTOUCH; + case HEXPROOF: + return Keyword.HEXPROOF; + case INDESTRUCTIBLE: + return Keyword.INDESTRUCTIBLE; + case LIFELINK: + return Keyword.LIFELINK; + case MENACE: + return Keyword.MENACE; + case REACH: + return Keyword.REACH; + case TRAMPLE: + return Keyword.TRAMPLE; + case VIGILANCE: + return Keyword.VIGILANCE; + default: + return null; + } + } + public static final ImmutableList values = ImmutableList.copyOf(values()); } diff --git a/forge-game/src/main/java/forge/game/combat/Combat.java b/forge-game/src/main/java/forge/game/combat/Combat.java index 4fc83734d74..4c85ccd5cac 100644 --- a/forge-game/src/main/java/forge/game/combat/Combat.java +++ b/forge-game/src/main/java/forge/game/combat/Combat.java @@ -882,6 +882,10 @@ public class Combat { return true; // is blocking something at the moment } + if (!blocker.isLKI()) { + return false; + } + CombatLki lki = lkiCache.get(blocker); return null != lki && !lki.isAttacker; // was blocking something anyway } @@ -892,7 +896,11 @@ public class Combat { if (blockers != null && blockers.contains(blocker)) { return true; // is blocking the attacker's band at the moment } - + + if (!blocker.isLKI()) { + return false; + } + CombatLki lki = lkiCache.get(blocker); return null != lki && !lki.isAttacker && lki.relatedBands.contains(ab); // was blocking that very band } diff --git a/forge-game/src/main/java/forge/game/cost/CostPartWithList.java b/forge-game/src/main/java/forge/game/cost/CostPartWithList.java index b934d3d0be7..a8dabcbc86f 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPartWithList.java +++ b/forge-game/src/main/java/forge/game/cost/CostPartWithList.java @@ -122,8 +122,10 @@ public abstract class CostPartWithList extends CostPart { // always returns true, made this to inline with return public boolean executePayment(SpellAbility ability, CardCollectionView targetCards) { - if (canPayListAtOnce()) { // This is used by reveal. Without it when opponent would reveal hand, you'll get N message boxes. - lkiList.addAll(targetCards); + if (canPayListAtOnce()) { // This is used by reveal. Without it when opponent would reveal hand, you'll get N message boxes. + for (Card c: targetCards) { + lkiList.add(CardUtil.getLKICopy(c)); + } cardList.addAll(doListPayment(ability, targetCards)); handleChangeZoneTrigger(ability); return true; diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 5a6e3c33c6c..711aeb2d65c 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -127,7 +127,7 @@ public enum Keyword { SHROUD("Shroud", SimpleKeyword.class, true, "This can't be the target of spells or abilities."), SKULK("Skulk", SimpleKeyword.class, true, "This creature can't be blocked by creatures with greater power."), SCAVENGE("Scavenge", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Put a number of +1/+1 counters equal to this card's power on target creature. Scavenge only as a sorcery."), - SOULBOND("Souldbond", SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters the battlefield. They remain paired for as long as you control both of them"), + SOULBOND("Soulbond", SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters the battlefield. They remain paired for as long as you control both of them."), SOULSHIFT("Soulshift", KeywordWithAmount.class, false, "When this creature dies, you may return target Spirit card with converted mana cost %d or less from your graveyard to your hand."), SPECTACLE("Spectacle", KeywordWithCost.class, false, "You may cast this spell for its spectacle cost rather than its mana cost if an opponent lost life this turn."), SPLICE("Splice", KeywordWithCostAndType.class, false, "As you cast an %2$s spell, you may reveal this card from your hand and pay its splice cost. If you do, add this card's effects to that spell."), diff --git a/forge-game/src/main/java/forge/game/mana/ManaPool.java b/forge-game/src/main/java/forge/game/mana/ManaPool.java index e90e6eb9b50..d11afbe776c 100644 --- a/forge-game/src/main/java/forge/game/mana/ManaPool.java +++ b/forge-game/src/main/java/forge/game/mana/ManaPool.java @@ -254,7 +254,7 @@ public class ManaPool extends ManaConversionMatrix implements Iterable { } } if (mana.addsCounters(sa)) { - mana.getManaAbility().createETBCounters(host); + mana.getManaAbility().createETBCounters(host, this.owner); } if (mana.triggersWhenSpent()) { mana.getManaAbility().addTriggersWhenSpent(sa, host); diff --git a/forge-game/src/main/java/forge/game/phase/PhaseType.java b/forge-game/src/main/java/forge/game/phase/PhaseType.java index 0cea056d0ab..a1f5a208a75 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseType.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseType.java @@ -55,6 +55,11 @@ public enum PhaseType { return result; } + public final boolean isCombatPhase() { + return ((ALL_PHASES.indexOf(this) >= ALL_PHASES.indexOf(COMBAT_BEGIN)) + && (ALL_PHASES.indexOf(this) <= ALL_PHASES.indexOf(COMBAT_END))); + } + public final boolean isAfter(final PhaseType phase) { return ALL_PHASES.indexOf(this) > ALL_PHASES.indexOf(phase); } diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 93e53b59b3d..71e34fc7a8b 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -162,6 +162,10 @@ public class Player extends GameEntity implements Comparable { private Card blessingEffect = null; private Card keywordEffect = null; + private Map additionalVotes = Maps.newHashMap(); + private Map additionalOptionalVotes = Maps.newHashMap(); + private SortedSet controlVotes = Sets.newTreeSet(); + private final AchievementTracker achievementTracker = new AchievementTracker(); private final PlayerView view; @@ -2521,6 +2525,10 @@ public class Player extends GameEntity implements Comparable { } public boolean isSkippingCombat() { + if (hasLost()) { + return true; + } + if (hasKeyword("Skip your next combat phase.")) { return true; } @@ -3067,4 +3075,86 @@ public class Player extends GameEntity implements Comparable { this.updateZoneForView(com); return keywordEffect; } + + public void addAdditionalVote(long timestamp, int value) { + additionalVotes.put(timestamp, value); + getView().updateAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + + public void removeAdditionalVote(long timestamp) { + if (additionalVotes.remove(timestamp) != null) { + getView().updateAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + } + + public int getAdditionalVotesAmount() { + int value = 0; + for (Integer i : additionalVotes.values()) { + value += i; + } + return value; + } + + public void addAdditionalOptionalVote(long timestamp, int value) { + additionalOptionalVotes.put(timestamp, value); + getView().updateOptionalAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + public void removeAdditionalOptionalVote(long timestamp) { + if (additionalOptionalVotes.remove(timestamp) != null) { + getView().updateOptionalAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + } + + public int getAdditionalOptionalVotesAmount() { + int value = 0; + for (Integer i : additionalOptionalVotes.values()) { + value += i; + } + return value; + } + + public boolean addControlVote(long timestamp) { + if (controlVotes.add(timestamp)) { + updateControlVote(); + return true; + } + return false; + } + public boolean removeControlVote(long timestamp) { + if (controlVotes.remove(timestamp)) { + updateControlVote(); + return true; + } + return false; + } + + void updateControlVote() { + // need to update all players because it can't know + Player control = getGame().getControlVote(); + for (Player pl : getGame().getPlayers()) { + pl.getView().updateControlVote(pl.equals(control)); + getGame().fireEvent(new GameEventPlayerStatsChanged(pl, false)); + } + } + + public Set getControlVote() { + return controlVotes; + } + + public void setControlVote(Set value) { + controlVotes.clear(); + controlVotes.addAll(value); + updateControlVote(); + } + + public Long getHighestControlVote() { + if (controlVotes.isEmpty()) { + return null; + } + return controlVotes.last(); + } } diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index 62d57539212..ec79ece50b7 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -166,7 +166,7 @@ public abstract class PlayerController { return chooseSomeType(kindOfType, sa, validTypes, invalidTypes, false); } - public abstract Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes); + public abstract Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer); public abstract boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question); public abstract CardCollectionView getCardsToMulligan(Player firstPlayer); diff --git a/forge-game/src/main/java/forge/game/player/PlayerView.java b/forge-game/src/main/java/forge/game/player/PlayerView.java index 6aa49b215b0..2da3dac4bf6 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerView.java +++ b/forge-game/src/main/java/forge/game/player/PlayerView.java @@ -271,6 +271,27 @@ public class PlayerView extends GameEntityView { set(TrackableProperty.NumDrawnThisTurn, p.getNumDrawnThisTurn()); } + public int getAdditionalVote() { + return get(TrackableProperty.AdditionalVote); + } + public void updateAdditionalVote(Player p) { + set(TrackableProperty.AdditionalVote, p.getAdditionalVotesAmount()); + } + + public int getOptionalAdditionalVote() { + return get(TrackableProperty.OptionalAdditionalVote); + } + public void updateOptionalAdditionalVote(Player p) { + set(TrackableProperty.OptionalAdditionalVote, p.getAdditionalOptionalVotesAmount()); + } + + public boolean getControlVote() { + return get(TrackableProperty.ControlVotes); + } + public void updateControlVote(boolean val) { + set(TrackableProperty.ControlVotes, val); + } + public ImmutableMultiset getKeywords() { return get(TrackableProperty.Keywords); } @@ -497,6 +518,19 @@ public class PlayerView extends GameEntityView { details.add(Localizer.getInstance().getMessage("lblCardDrawnThisTurnHas", String.valueOf(getNumDrawnThisTurn()))); details.add(Localizer.getInstance().getMessage("lblDamagepreventionHas", String.valueOf(getPreventNextDamage()))); + int v = getAdditionalVote(); + if (v > 0) { + details.add(Localizer.getInstance().getMessage("lblAdditionalVotes", String.valueOf(v))); + } + v = getOptionalAdditionalVote(); + if (v > 0) { + details.add(Localizer.getInstance().getMessage("lblOptionalAdditionalVotes", String.valueOf(v))); + } + + if (getControlVote()) { + details.add(Localizer.getInstance().getMessage("lblControlsVote")); + } + if (getIsExtraTurn()) { details.add(Localizer.getInstance().getMessage("lblIsExtraTurn")); } diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java index da064a4cbd6..510458a6189 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -19,9 +19,11 @@ package forge.game.spellability; import com.google.common.collect.Lists; import com.google.common.collect.Maps; + import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaAtom; +import forge.game.Game; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; import forge.game.card.Card; @@ -34,6 +36,8 @@ import forge.game.replacement.*; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; +import forge.game.zone.ZoneType; +import forge.util.Lang; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -46,7 +50,7 @@ import java.util.regex.Pattern; *

* Abstract AbilityMana class. *

- * + * * @author Forge * @version $Id$ */ @@ -78,7 +82,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* Constructor for AbilityMana. *

- * + * * @param sourceCard * a {@link forge.game.card.Card} object. */ @@ -111,7 +115,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* produceMana. *

- * + * * @param produced * a {@link java.lang.String} object. * @param player @@ -169,7 +173,7 @@ public class AbilityManaPart implements java.io.Serializable { * cannotCounterPaidWith. *

* @param saBeingPaid - * + * * @return a {@link java.lang.String} object. */ public boolean cannotCounterPaidWith(SpellAbility saBeingPaid) { @@ -186,7 +190,7 @@ public class AbilityManaPart implements java.io.Serializable { * addKeywords. *

* @param saBeingPaid - * + * * @return a {@link java.lang.String} object. */ public boolean addKeywords(SpellAbility saBeingPaid) { @@ -205,7 +209,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* getKeywords. *

- * + * * @return a {@link java.lang.String} object. */ public String getKeywords() { @@ -217,7 +221,7 @@ public class AbilityManaPart implements java.io.Serializable { * addsCounters. *

* @param saBeingPaid - * + * * @return a {@link java.lang.String} object. */ public boolean addsCounters(SpellAbility saBeingPaid) { @@ -227,10 +231,26 @@ public class AbilityManaPart implements java.io.Serializable { /** * createETBCounters */ - public void createETBCounters(Card c) { + public void createETBCounters(Card c, Player controller) { String[] parse = this.addsCounters.split("_"); // Convert random SVars if there are other cards with this effect if (c.isValid(parse[0], c.getController(), c, null)) { + final Game game = this.sourceCard.getGame(); + final Card eff = new Card(game.nextCardId(), game); + eff.setTimestamp(game.getNextTimestamp()); + eff.setName(sourceCard.getName() + "'s Effect"); + eff.addType("Effect"); + eff.setToken(true); // Set token to true, so when leaving play it gets nuked + eff.setOwner(controller); + + eff.setImageKey(sourceCard.getImageKey()); + eff.setColor(MagicColor.COLORLESS); + eff.setImmutable(true); + // try to get the SpellAbility from the mana ability + //eff.setEffectSource((SpellAbility)null); + + eff.addRemembered(c); + String abStr = "DB$ PutCounter | Defined$ ReplacedCard | CounterType$ " + parse[1] + " | ETB$ True | CounterNum$ " + parse[2]; @@ -240,15 +260,37 @@ public class AbilityManaPart implements java.io.Serializable { } CardFactoryUtil.setupETBReplacementAbility(sa); - String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield " - + " | Secondary$ True | Description$ CARDNAME" - + " enters the battlefield with " + CounterType.valueOf(parse[1]).getName() + " counters."; + String desc = "It enters the battlefield with "; + desc += Lang.nounWithNumeral(parse[2], CounterType.valueOf(parse[1]).getName() + " counter"); + desc += " on it."; - ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, c, false); + String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | Description$ " + desc; + + ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true); re.setLayer(ReplacementLayer.Other); re.setOverridingAbility(sa); - c.addReplacementEffect(re); + eff.addReplacementEffect(re); + + // Forgot Trigger + String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Any | TriggerZones$ Command | Static$ True"; + String forgetEffect = "DB$ Pump | ForgetObjects$ TriggeredCard"; + String exileEffect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile" + + " | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0"; + + SpellAbility saForget = AbilityFactory.getAbility(forgetEffect, eff); + AbilitySub saExile = (AbilitySub) AbilityFactory.getAbility(exileEffect, eff); + saForget.setSubAbility(saExile); + + final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, eff, true); + parsedTrigger.setOverridingAbility(saForget); + eff.addTrigger(parsedTrigger); + eff.updateStateForView(); + + // TODO: Add targeting to the effect so it knows who it's dealing with + game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); + game.getAction().moveTo(ZoneType.Command, eff, null); + game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); } } @@ -269,7 +311,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* getManaRestrictions. *

- * + * * @return a {@link java.lang.String} object. */ public String getManaRestrictions() { @@ -280,7 +322,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* meetsManaRestrictions. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. @@ -296,7 +338,7 @@ public class AbilityManaPart implements java.io.Serializable { if (restriction.equals("nonSpell")) { return !sa.isSpell(); } - + if (restriction.equals("CumulativeUpkeep")) { if (sa.isCumulativeupkeep()) { return true; @@ -349,7 +391,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* mana. *

- * + * * @return a {@link java.lang.String} object. */ public final String mana() { @@ -438,7 +480,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* canProduce. *

- * + * * @param s * a {@link java.lang.String} object. * @return a boolean. @@ -468,7 +510,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* isBasic. *

- * + * * @return a boolean. */ public final boolean isBasic() { @@ -541,7 +583,7 @@ public class AbilityManaPart implements java.io.Serializable { public Card getSourceCard() { return sourceCard; } - + public void setSourceCard(final Card host) { sourceCard = host; } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 75479eaadfc..256bbf07a3a 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -32,6 +32,7 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardDamageMap; import forge.game.card.CardFactory; +import forge.game.card.CardPredicates; import forge.game.card.CardZoneTable; import forge.game.cost.Cost; import forge.game.cost.CostPart; @@ -46,6 +47,7 @@ import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; import forge.game.trigger.WrappedAbility; import forge.game.zone.ZoneType; +import forge.util.Aggregates; import forge.util.Expressions; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -130,6 +132,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private final List payingMana = Lists.newArrayList(); private final List paidAbilities = Lists.newArrayList(); + private Integer xManaCostPaid = null; private HashMap paidLists = Maps.newHashMap(); @@ -447,6 +450,23 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit payCosts = abCost; } + public boolean costHasX() { + if (getPayCosts() == null) { + return false; + } + return getPayCosts().hasXInAnyCostPart(); + } + + public boolean costHasManaX() { + if (getPayCosts() == null) { + return false; + } + if (getPayCosts().hasNoManaCost()) { + return false; + } + return getPayCosts().getCostMana().getAmountOfX() > 0; + } + public SpellAbilityRestriction getRestrictions() { return restrictions; } @@ -1081,8 +1101,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } if (hasParam("MaxTotalTargetCMC") && entity instanceof Card) { - final Card c = (Card) entity; - if (c.getCMC() > tr.getMaxTotalCMC(c, this)) { + int soFar = Aggregates.sum(getTargets().getTargetCards(), CardPredicates.Accessors.fnGetCmc); + // only add if it isn't already targeting + if (!isTargeting(entity)) { + final Card c = (Card) entity; + soFar += c.getCMC(); + } + + if (soFar > tr.getMaxTotalCMC(getHostCard(), this)) { return false; } } @@ -1299,6 +1325,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit String announce = getParam("Announce"); if (StringUtils.isBlank(announce)) { mapParams.put("Announce", variable); + originalMapParams.put("Announce", variable); return; } String[] announcedOnes = TextUtil.split(announce, ','); @@ -1308,6 +1335,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } } mapParams.put("Announce", announce + ";" + variable); + originalMapParams.put("Announce", announce + ";" + variable); } public boolean isXCost() { @@ -1961,4 +1989,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void setAlternativeCost(AlternativeCost ac) { altCost = ac; } + + public Integer getXManaCostPaid() { + return xManaCostPaid; + } + public void setXManaCostPaid(final Integer n) { + xManaCostPaid = n; + } } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java index f6f5954cdc7..1653c2066ca 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java @@ -197,6 +197,10 @@ public class SpellAbilityCondition extends SpellAbilityVariables { this.setManaSpent(params.get("ConditionManaSpent")); } + if (params.containsKey("ConditionManaNotSpent")) { + this.setManaNotSpent(params.get("ConditionManaNotSpent")); + } + if (params.containsKey("ConditionCheckSVar")) { this.setSvarToCheck(params.get("ConditionCheckSVar")); } @@ -432,10 +436,21 @@ public class SpellAbilityCondition extends SpellAbilityVariables { } } - if (StringUtils.isNotEmpty(this.getManaSpent())) { - byte manaSpent = MagicColor.fromName(getManaSpent()); // they always check for single color - if( 0 == (manaSpent & sa.getHostCard().getColorsPaid())) // no match of colors + if (StringUtils.isNotEmpty(getManaSpent())) { + for (String s : getManaSpent().split(" ")) { + byte manaSpent = MagicColor.fromName(s); + if( 0 == (manaSpent & sa.getHostCard().getColorsPaid())) // no match of colors + return false; + } + } + if (StringUtils.isNotEmpty(getManaNotSpent())) { + byte toPay = 0; + for (String s : getManaNotSpent().split(" ")) { + toPay |= MagicColor.fromName(s); + } + if (toPay == (toPay & sa.getHostCard().getColorsPaid())) { return false; + } } if (this.getsVarToCheck() != null) { diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java index 15dd3573998..6a0b8b90de9 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java @@ -83,7 +83,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { // private ArrayList payingMana = new ArrayList(); // private ArrayList paidAbilities = new // ArrayList(); - private int xManaPaid = 0; + private Integer xManaPaid = null; // Other Paid things private final HashMap paidHash; @@ -116,7 +116,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { splicedCards = sa.getSplicedCards(); // TODO getXManaCostPaid should be on the SA, not the Card - xManaPaid = sa.getHostCard().getXManaCostPaid(); + xManaPaid = sa.getXManaCostPaid(); // Triggering info triggeringObjects = sa.getTriggeringObjects(); @@ -200,7 +200,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { // Set Cost specific things here ability.setPaidHash(paidHash); ability.setSplicedCards(splicedCards); - ability.getHostCard().setXManaCostPaid(xManaPaid); + ability.setXManaCostPaid(xManaPaid); // Triggered ability.setTriggeringObjects(triggeringObjects); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java index a40263e218f..11a6c93a998 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java @@ -89,6 +89,7 @@ public class SpellAbilityVariables implements Cloneable { this.lifeTotal = sav.getLifeTotal(); this.lifeAmount = sav.getLifeAmount(); this.manaSpent = sav.getManaSpent(); + this.manaNotSpent = sav.getManaNotSpent(); this.targetValidTargeting = sav.getTargetValidTargeting(); this.targetsSingleTarget = sav.targetsSingleTarget(); this.presenceCondition = sav.getPresenceCondition(); @@ -193,6 +194,7 @@ public class SpellAbilityVariables implements Cloneable { /** The mana spent. */ private String manaSpent = ""; + private String manaNotSpent = ""; /** The chosen colors string. */ private String chosenColors = null; @@ -229,6 +231,13 @@ public class SpellAbilityVariables implements Cloneable { return this.manaSpent; } + public final void setManaNotSpent(final String s) { + this.manaNotSpent = s; + } + public final String getManaNotSpent() { + return this.manaNotSpent; + } + /** *

* Setter for the field zone. diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index f8442310ec0..421edc80b60 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -181,15 +181,9 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone layers.add(StaticAbilityLayer.MODIFYPT); } - if (hasParam("AddHiddenKeyword")) { - layers.add(StaticAbilityLayer.RULES); - } - - if (hasParam("IgnoreEffectCost") || hasParam("Goad") || hasParam("CanBlockAny") || hasParam("CanBlockAmount")) { - layers.add(StaticAbilityLayer.RULES); - } - - if (hasParam("AdjustLandPlays")) { + if (hasParam("AddHiddenKeyword") + || hasParam("IgnoreEffectCost") || hasParam("Goad") || hasParam("CanBlockAny") || hasParam("CanBlockAmount") + || hasParam("AdjustLandPlays") || hasParam("ControlVote") || hasParam("AdditionalVote") || hasParam("AdditionalOptionalVote")) { layers.add(StaticAbilityLayer.RULES); } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index f0b0813bfc3..f6d8a9b6984 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -513,6 +513,20 @@ public final class StaticAbilityContinuous { } } + if (params.containsKey("ControlVote")) { + p.addControlVote(se.getTimestamp()); + } + if (params.containsKey("AdditionalVote")) { + String mhs = params.get("AdditionalVote"); + int add = AbilityUtils.calculateAmount(hostCard, mhs, stAb); + p.addAdditionalVote(se.getTimestamp(), add); + } + if (params.containsKey("AdditionalOptionalVote")) { + String mhs = params.get("AdditionalOptionalVote"); + int add = AbilityUtils.calculateAmount(hostCard, mhs, stAb); + p.addAdditionalOptionalVote(se.getTimestamp(), add); + } + if (params.containsKey("RaiseMaxHandSize")) { String rmhs = params.get("RaiseMaxHandSize"); int rmax = AbilityUtils.calculateAmount(hostCard, rmhs, stAb); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java index 3fc236f94bc..88311eadac2 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java @@ -53,7 +53,7 @@ public class TriggerCycled extends Trigger { /** {@inheritDoc} */ @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { - sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card); + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card, AbilityKey.Cause); } @Override diff --git a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java index 1a3a9e84fe2..55532468591 100644 --- a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java +++ b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java @@ -473,13 +473,13 @@ public class WrappedAbility extends Ability { TriggerHandler th = game.getTriggerHandler(); + // set Trigger + sa.setTrigger(regtrig); + if (decider != null && !decider.getController().confirmTrigger(this, triggerParams, this.isMandatory())) { return; } - // set Trigger - sa.setTrigger(regtrig); - if (!triggerParams.containsKey("NoTimestampCheck")) { timestampCheck(); } @@ -556,4 +556,11 @@ public class WrappedAbility extends Ability { public void setAlternativeCost(AlternativeCost ac) { sa.setAlternativeCost(ac); } + + public Integer getXManaCostPaid() { + return sa.getXManaCostPaid(); + } + public void setXManaCostPaid(final Integer n) { + sa.setXManaCostPaid(n); + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index ee8571e5eb5..f3e90a992f8 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -314,9 +314,9 @@ public class MagicStack /* extends MyObservable */ implements Iterable cycleParams = AbilityKey.mapFromCard(sp.getHostCard()); + cycleParams.put(AbilityKey.Cause, sp); + game.getTriggerHandler().runTrigger(TriggerType.Cycled, cycleParams, false); } if (sp.hasParam("Crew")) { diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index 2660337d82f..039085ee9d0 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -134,6 +134,9 @@ public enum TrackableProperty { HasUnlimitedLandPlay(TrackableTypes.BooleanType), NumLandThisTurn(TrackableTypes.IntegerType), NumDrawnThisTurn(TrackableTypes.IntegerType), + AdditionalVote(TrackableTypes.IntegerType), + OptionalAdditionalVote(TrackableTypes.IntegerType), + ControlVotes(TrackableTypes.BooleanType), Keywords(TrackableTypes.KeywordCollectionViewType, FreezeMode.IgnoresFreeze), Commander(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), CommanderCast(TrackableTypes.IntegerMapType), diff --git a/forge-game/src/main/java/forge/trackable/TrackableTypes.java b/forge-game/src/main/java/forge/trackable/TrackableTypes.java index 6214563f8f7..0dce88f71e5 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableTypes.java +++ b/forge-game/src/main/java/forge/trackable/TrackableTypes.java @@ -100,17 +100,21 @@ public class TrackableTypes { if (newCollection != null) { //swap in objects in old collection for objects in new collection for (int i = 0; i < newCollection.size(); i++) { - T newObj = newCollection.get(i); - if (newObj != null) { - T existingObj = from.getTracker().getObj(itemType, newObj.getId()); - if (existingObj != null) { //if object exists already, update its changed properties - existingObj.copyChangedProps(newObj); - newCollection.remove(i); - newCollection.add(i, existingObj); - } - else { //if object is new, cache in object lookup - from.getTracker().putObj(itemType, newObj.getId(), newObj); + try { + T newObj = newCollection.get(i); + if (newObj != null) { + T existingObj = from.getTracker().getObj(itemType, newObj.getId()); + if (existingObj != null) { //if object exists already, update its changed properties + existingObj.copyChangedProps(newObj); + newCollection.remove(i); + newCollection.add(i, existingObj); + } + else { //if object is new, cache in object lookup + from.getTracker().putObj(itemType, newObj.getId(), newObj); + } } + } catch (IndexOutOfBoundsException e) { + System.err.println("got an IndexOutOfBoundsException, trying to continue ..."); } } } diff --git a/forge-gui-android/AndroidManifest.xml b/forge-gui-android/AndroidManifest.xml index 5edbcfad66e..1f2dc8ae144 100644 --- a/forge-gui-android/AndroidManifest.xml +++ b/forge-gui-android/AndroidManifest.xml @@ -6,7 +6,8 @@ + android:targetSdkVersion="26" /> + diff --git a/forge-gui-android/libs/android-support-v4.jar b/forge-gui-android/libs/android-support-v4.jar index 187bdf48b1d..653cb767665 100644 Binary files a/forge-gui-android/libs/android-support-v4.jar and b/forge-gui-android/libs/android-support-v4.jar differ diff --git a/forge-gui-android/libs/gdx-backend-android-sources.jar b/forge-gui-android/libs/gdx-backend-android-sources.jar index 414fa961cfd..d328206fb3a 100644 Binary files a/forge-gui-android/libs/gdx-backend-android-sources.jar and b/forge-gui-android/libs/gdx-backend-android-sources.jar differ diff --git a/forge-gui-android/libs/gdx-backend-android.jar b/forge-gui-android/libs/gdx-backend-android.jar index d780673aa6b..40af9131f01 100644 Binary files a/forge-gui-android/libs/gdx-backend-android.jar and b/forge-gui-android/libs/gdx-backend-android.jar differ diff --git a/forge-gui-android/libs/gdx-freetype.jar b/forge-gui-android/libs/gdx-freetype.jar index eda8b39b168..3181586a666 100644 Binary files a/forge-gui-android/libs/gdx-freetype.jar and b/forge-gui-android/libs/gdx-freetype.jar differ diff --git a/forge-gui-android/libs/gdx-sources.jar b/forge-gui-android/libs/gdx-sources.jar index 798fee80a7b..74b5aadb926 100644 Binary files a/forge-gui-android/libs/gdx-sources.jar and b/forge-gui-android/libs/gdx-sources.jar differ diff --git a/forge-gui-android/libs/gdx.jar b/forge-gui-android/libs/gdx.jar index 0ba4a3f2bb4..2c603b032c5 100644 Binary files a/forge-gui-android/libs/gdx.jar and b/forge-gui-android/libs/gdx.jar differ diff --git a/forge-gui-android/pom.xml b/forge-gui-android/pom.xml index 76158aee896..a556f6c9698 100644 --- a/forge-gui-android/pom.xml +++ b/forge-gui-android/pom.xml @@ -6,7 +6,7 @@ jar -Xms1024m -Xmx1536m - 1.6.32.001 + 1.6.33.001 keystore alias storepass @@ -19,7 +19,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.34-SNAPSHOT forge-gui-android @@ -104,24 +104,6 @@ gdx-backend-android 1.9.10 - - org.cache2k - cache2k-base-bom - 1.2.4.Final - pom - - - org.cache2k - cache2k-core - 1.2.4.Final - compile - - - org.cache2k - cache2k-api - 1.2.4.Final - compile - @@ -142,7 +124,7 @@ true - 25 + 26 true ${project.basedir}/AndroidManifest.xml @@ -183,7 +165,7 @@ false - 25 + 26 false diff --git a/forge-gui-android/proguard.cfg b/forge-gui-android/proguard.cfg index 57fa2061f4d..ab6fbdeecf0 100644 --- a/forge-gui-android/proguard.cfg +++ b/forge-gui-android/proguard.cfg @@ -29,20 +29,8 @@ -dontwarn javax.** -dontwarn org.apache.logging.log4j.** -dontwarn module-info - -# mandatory proguard rules for cache2k to keep the core implementation --dontwarn org.cache2k.impl.xmlConfiguration.** --dontwarn org.cache2k.impl.serverSide.** --keep interface org.cache2k.spi.Cache2kCoreProvider --keep public class * extends org.cache2k.spi.Cache2kCoreProvider -# optional proguard rules for cache2k, to keep XML configuration code -# if only programmatic configuration is used, these rules may be ommitted --keep interface org.cache2k.core.spi.CacheConfigurationProvider --keep public class * extends org.cache2k.core.spi.CacheConfigurationProvider --keepclassmembers public class * extends org.cache2k.configuration.ConfigurationBean { - public void set*(...); - public ** get*(); -} +## Support library +-dontwarn android.support.** -keep class forge.** { *; } -keep class com.thoughtworks.xstream.** { *; } @@ -51,6 +39,7 @@ -keep class com.google.common.** { *; } -keep class io.sentry.event.Event { *; } -keep class io.netty.util.internal.logging.** { *; } +-keep class net.jpountz.** { *; } -keepclassmembers class com.badlogic.gdx.backends.android.AndroidInput* { (com.badlogic.gdx.Application, android.content.Context, java.lang.Object, com.badlogic.gdx.backends.android.AndroidApplicationConfiguration); diff --git a/forge-gui-android/project.properties b/forge-gui-android/project.properties index 735ea3db305..94206b9059a 100644 --- a/forge-gui-android/project.properties +++ b/forge-gui-android/project.properties @@ -9,4 +9,4 @@ # Project target. project.type=0 -target=android-20 +target=android-26 diff --git a/forge-gui-android/res/mipmap-hdpi/ic_launcher.png b/forge-gui-android/res/mipmap-hdpi/ic_launcher.png index 13ac6220632..4271e4c73a0 100644 Binary files a/forge-gui-android/res/mipmap-hdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-hdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png index a29e46aa510..a749b470515 100644 Binary files a/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png index 07a75d76bfa..23880358c4f 100644 Binary files a/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-ldpi/ic_launcher.png b/forge-gui-android/res/mipmap-ldpi/ic_launcher.png index ee25ac432d9..ae40d73a33c 100644 Binary files a/forge-gui-android/res/mipmap-ldpi/ic_launcher.png and b/forge-gui-android/res/mipmap-ldpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-mdpi/ic_launcher.png b/forge-gui-android/res/mipmap-mdpi/ic_launcher.png index 5fdd9db84cf..75cac9e8a25 100644 Binary files a/forge-gui-android/res/mipmap-mdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-mdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png index 524a6232021..bcb5f32aa66 100644 Binary files a/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png index bb574ba21ae..fa0400329c5 100644 Binary files a/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png b/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png index 5d5de8a444f..ad7c6ed96bb 100644 Binary files a/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png index 0e93289f2d1..25275afddb0 100644 Binary files a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png index 96328be12a9..42f78d72996 100644 Binary files a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png index e6ea9ce1897..3f9d487ce92 100644 Binary files a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png index a01a5036f4f..a01aaa6e9df 100644 Binary files a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png index e4d1b24e68e..c1f4153cef3 100644 Binary files a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png index 3380a1063bb..6d186851cd6 100644 Binary files a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png index e8ccb890b9e..aaa2395dcb9 100644 Binary files a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png index 68aca8009d4..8ad3506407f 100644 Binary files a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/values/ic_launcher_background.xml b/forge-gui-android/res/values/ic_launcher_background.xml index 7f8bb682c03..ab983282473 100644 --- a/forge-gui-android/res/values/ic_launcher_background.xml +++ b/forge-gui-android/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #f0f0f0 + #ffffff \ No newline at end of file diff --git a/forge-gui-android/src/forge/app/Main.java b/forge-gui-android/src/forge/app/Main.java index 70aa9389704..a54df3b32e3 100644 --- a/forge-gui-android/src/forge/app/Main.java +++ b/forge-gui-android/src/forge/app/Main.java @@ -1,16 +1,22 @@ package forge.app; import android.app.AlarmManager; +import android.app.AlertDialog; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -18,8 +24,17 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.PowerManager; +import android.provider.Settings; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import android.view.Gravity; +import android.view.View; import android.view.WindowManager; import android.webkit.MimeTypeMap; +import android.widget.Button; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.backends.android.AndroidApplication; import forge.Forge; @@ -35,13 +50,143 @@ import java.io.OutputStream; import java.util.concurrent.Callable; public class Main extends AndroidApplication { + AndroidAdapter Gadapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - AndroidAdapter adapter = new AndroidAdapter(this.getContext()); + boolean permissiongranted = checkPermission(); + Gadapter = new AndroidAdapter(this.getContext()); + initForge(Gadapter, permissiongranted); + //permission + if(!permissiongranted){ + //requestPermission(); + displayMessage(Gadapter); + } + } + + private void displayMessage(AndroidAdapter adapter){ + TableLayout TL = new TableLayout(this); + TableRow row = new TableRow(this); + TableRow row2 = new TableRow(this); + TextView text = new TextView(this); + text.setGravity(Gravity.LEFT); + text.setTypeface(Typeface.SERIF); + + String title="Forge needs Storage Permission to run properly...\n" + + "Follow these simple steps:\n\n"; + String steps = " 1) Tap \"Open App Details\" Button.\n" + + " 2) Tap Permissions\n"+ + " 3) Turn on the Storage Permission.\n\n"+ + "(You can tap anywhere to exit and restart the app)\n\n"; + + SpannableString ss1= new SpannableString(title); + ss1.setSpan(new StyleSpan(Typeface.BOLD), 0, ss1.length(), 0); + text.append(ss1); + text.append(steps); + row.addView(text); + row.setGravity(Gravity.CENTER); + + int[] colors = {Color.TRANSPARENT,Color.TRANSPARENT}; + int[] pressed = {Color.GREEN,Color.GREEN}; + GradientDrawable gd = new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, colors); + gd.setStroke(3, Color.DKGRAY); + gd.setCornerRadius(100); + + GradientDrawable gd2 = new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, pressed); + gd2.setStroke(3, Color.DKGRAY); + gd2.setCornerRadius(100); + + Button button = new Button(this); + button.setText("Open App Details"); + + StateListDrawable states = new StateListDrawable(); + + states.addState(new int[] {android.R.attr.state_pressed}, gd2); + states.addState(new int[] { }, gd); + + button.setBackground(states); + + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + } + }); + + row2.addView(button); + row2.setGravity(Gravity.CENTER); + + TL.addView(row, new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT, TableLayout.LayoutParams.WRAP_CONTENT)); + TL.addView(row2, new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT, TableLayout.LayoutParams.WRAP_CONTENT)); + TL.setGravity(Gravity.CENTER); + TL.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + adapter.exit(); + } + }); + setContentView(TL); + } + @Override + public void onBackPressed() { + if (Gadapter!=null) + Gadapter.exit(); + + super.onBackPressed(); + } + private boolean checkPermission() { + int pid = android.os.Process.myPid(); + int uid = android.os.Process.myUid(); + try { + int result = this.getBaseContext().checkPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, pid, uid); + if (result == PackageManager.PERMISSION_GRANTED) { + return true; + } else { + return false; + } + } catch (NullPointerException e) { + return false; + } + } + private void requestPermission() { + //Show Information about why you need the permission + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Storage Permission Denied..."); + builder.setMessage("This app needs storage permission to run properly.\n\n\n\n"); + builder.setPositiveButton("Open App Details", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + //ActivityCompat crashes... maybe it needs the appcompat v7??? + //ActivityCompat.requestPermissions(Main.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); + } + }); + /*builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + + } + });*/ + builder.show(); + } + + private void initForge(AndroidAdapter adapter, boolean permissiongranted){ + boolean isPortrait; //establish assets directory if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { Gdx.app.error("Forge", "Can't access external storage"); @@ -65,15 +210,25 @@ public class Main extends AndroidApplication { //enforce orientation based on whether device is a tablet and user preference adapter.switchOrientationFile = assetsDir + "switch_orientation.ini"; boolean landscapeMode = adapter.isTablet == !FileUtil.doesFileExist(adapter.switchOrientationFile); - if (landscapeMode) { - Main.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } - else { - Main.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + if (permissiongranted){ + if (landscapeMode) { + isPortrait = false; + Main.this.setRequestedOrientation(Build.VERSION.SDK_INT >= 26 ? + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : //Oreo and above has virtual back/menu buttons + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } else { + isPortrait = true; + Main.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + } else { + isPortrait = true; + //set current orientation + Main.this.setRequestedOrientation(Main.this.getResources().getConfiguration().orientation); } - boolean value = Build.VERSION.SDK_INT >= 26; - initialize(Forge.getApp(new AndroidClipboard(), adapter, assetsDir, value)); + ForgePreferences prefs = FModel.getPreferences(); + boolean propertyConfig = prefs != null && prefs.getPrefBoolean(ForgePreferences.FPref.UI_NETPLAY_COMPAT); + initialize(Forge.getApp(new AndroidClipboard(), adapter, assetsDir, propertyConfig, isPortrait)); } /*@Override diff --git a/forge-gui-desktop/pom.xml b/forge-gui-desktop/pom.xml index c2bb8f2388f..e1fa8d95747 100644 --- a/forge-gui-desktop/pom.xml +++ b/forge-gui-desktop/pom.xml @@ -4,7 +4,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.34-SNAPSHOT forge-gui-desktop @@ -213,7 +213,7 @@ com.akathist.maven.plugins.launch4j launch4j-maven-plugin - 1.5.2 + 1.7.25 l4j-gui @@ -235,7 +235,10 @@ 1.8.0 - 1024 + 4096 + + -Dfile.encoding=UTF-8 + @@ -367,7 +370,7 @@ com.akathist.maven.plugins.launch4j launch4j-maven-plugin - 1.5.2 + 1.7.25 l4j-gui @@ -389,7 +392,10 @@ 1.8.0 - 1024 + 4096 + + -Dfile.encoding=UTF-8 + @@ -564,7 +570,7 @@