Commit bdcf991a authored by Walter Adolph's avatar Walter Adolph

Merge remote-tracking branch 'remotes/upstream/master'

# Conflicts:
#	forge-gui/res/cardsfolder/p/plague_mare.txt
parents 22a5cab9 27bae4a7
......@@ -164,8 +164,34 @@ public class PlayerControllerAi extends PlayerController {
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
Player targetedPlayer) {
// this isn't used
return null;
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
FCollection<T> remaining = new FCollection<T>(optionList);
List<T> selecteds = new ArrayList<T>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
return selecteds;
}
@Override
public <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2,
boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
T selected1 = chooseSingleEntityForEffect(optionList1, null, sa, title, optional, targetedPlayer);
T selected2 = chooseSingleEntityForEffect(optionList2, null, sa, title, optional || selected1!=null, targetedPlayer);
List<T> selecteds = new ArrayList<T>();
if ( selected1 != null ) { selecteds.add(selected1); }
if ( selected2 != null ) { selecteds.add(selected2); }
return selecteds;
}
@Override
......
......@@ -102,6 +102,7 @@ public class DeckRecognizer {
// Pattern.compile("(.*)[^A-Za-wyz]*\\s+([\\d]{1,2})");
private static final Pattern SEARCH_NUMBERS_IN_FRONT = Pattern.compile("([\\d]{1,2})[^A-Za-wyz]*\\s+(.*)");
//private static final Pattern READ_SEPARATED_EDITION = Pattern.compile("[[\\(\\{]([a-zA-Z0-9]){1,3})[]*\\s+(.*)");
private static final Pattern SEARCH_SINGLE_SLASH = Pattern.compile("(?<=[^/])\\s*/\\s*(?=[^/])");
private final SetPreference useLastSet;
private final ICardDatabase db;
......@@ -125,7 +126,10 @@ public class DeckRecognizer {
return new Token(TokenType.Comment, 0, rawLine);
}
final char smartQuote = (char) 8217;
final String line = rawLine.trim().replace(smartQuote, '\'');
String line = rawLine.trim().replace(smartQuote, '\'');
// Some websites export split card names with a single slash. Replace with double slash.
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
Token result = null;
final Matcher foundNumbersInFront = DeckRecognizer.SEARCH_NUMBERS_IN_FRONT.matcher(line);
......
......@@ -29,6 +29,7 @@ import forge.game.ability.effects.AttachEffect;
import forge.game.card.*;
import forge.game.event.*;
import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordsChange;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
......@@ -292,6 +293,33 @@ public class GameAction {
copied.getOwner().addInboundToken(copied);
}
if (toBattlefield) {
// HACK for making the RIOT enchantment look into the Future
// need to check the Keywords what it would have on the Battlefield
Card riotLKI = CardUtil.getLKICopy(copied);
riotLKI.setLastKnownZone(zoneTo);
CardCollection preList = new CardCollection(riotLKI);
checkStaticAbilities(false, Sets.newHashSet(riotLKI), preList);
List<Long> changedTimeStamps = Lists.newArrayList();
for(Map.Entry<Long, KeywordsChange> e : riotLKI.getChangedCardKeywords().entrySet()) {
if (!copied.hasChangedCardKeywords(e.getKey())) {
KeywordsChange o = e.getValue();
o.setHostCard(copied);
for (KeywordInterface k : o.getKeywords()) {
for (ReplacementEffect re : k.getReplacements()) {
// this param need to be set, otherwise in ReplaceMoved it fails
re.getMapParams().put("BypassEtbCheck", "True");
}
}
copied.addChangedCardKeywordsInternal(o, e.getKey());
changedTimeStamps.add(e.getKey());
}
}
checkStaticAbilities(false);
}
Map<String, Object> repParams = Maps.newHashMap();
repParams.put("Event", "Moved");
repParams.put("Affected", copied);
......
......@@ -121,6 +121,9 @@ public class DamageAllEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
......
......@@ -30,7 +30,7 @@ public class DamageDealEffect extends DamageBaseEffect {
final int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
List<GameObject> tgts = getTargets(sa);
if (tgts.isEmpty())
if (tgts.isEmpty())
return "";
final List<Card> definedSources = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DamageSource"), sa);
......@@ -131,15 +131,15 @@ public class DamageDealEffect extends DamageBaseEffect {
sa.setPreventMap(preventMap);
usedDamageMap = true;
}
final List<Card> definedSources = AbilityUtils.getDefinedCards(hostCard, sa.getParam("DamageSource"), sa);
if (definedSources == null || definedSources.isEmpty()) {
return;
}
for (Card source : definedSources) {
final Card sourceLKI = hostCard.getGame().getChangeZoneLKIInfo(source);
if (divideOnResolution) {
// Dividing Damage up to multiple targets using combat damage box
// Currently only used for Master of the Wild Hunt
......@@ -147,7 +147,7 @@ public class DamageDealEffect extends DamageBaseEffect {
if (players.isEmpty()) {
return;
}
CardCollection assigneeCards = new CardCollection();
// Do we have a way of doing this in a better fashion?
for (GameObject obj : tgts) {
......@@ -155,7 +155,7 @@ public class DamageDealEffect extends DamageBaseEffect {
assigneeCards.add((Card)obj);
}
}
Player assigningPlayer = players.get(0);
Map<Card, Integer> map = assigningPlayer.getController().assignCombatDamage(sourceLKI, assigneeCards, dmg, null, true);
for (Entry<Card, Integer> dt : map.entrySet()) {
......@@ -166,6 +166,9 @@ public class DamageDealEffect extends DamageBaseEffect {
preventMap.triggerPreventDamage(false);
// non combat damage cause lifegain there
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
return;
......@@ -201,7 +204,7 @@ public class DamageDealEffect extends DamageBaseEffect {
}
}
}
if (remember) {
source.addRemembered(damageMap.row(sourceLKI).keySet());
}
......@@ -210,6 +213,9 @@ public class DamageDealEffect extends DamageBaseEffect {
preventMap.triggerPreventDamage(false);
// non combat damage cause lifegain there
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
}
......
......@@ -132,6 +132,9 @@ public class DamageEachEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
......
......@@ -21,10 +21,12 @@ public class DamageResolveEffect extends SpellAbilityEffect {
if (preventMap != null) {
preventMap.triggerPreventDamage(false);
preventMap.clear();
}
// non combat damage cause lifegain there
if (damageMap != null) {
damageMap.triggerDamageDoneOnce(false, sa);
damageMap.clear();
}
}
......
......@@ -187,14 +187,14 @@ public class DigEffect extends SpellAbilityEffect {
if (!andOrValid.equals("")) {
andOrCards = CardLists.getValidCards(top, andOrValid.split(","), host.getController(), host, sa);
andOrCards.removeAll((Collection<?>)valid);
valid.addAll(andOrCards);
valid.addAll(andOrCards); //pfps need to add andOr cards to valid to have set of all valid cards set up
}
else {
andOrCards = new CardCollection();
}
}
else {
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser.
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser. pfps??
if (p == chooser && destZone1ChangeNum > 1) {
delayedReveal = null;
}
......@@ -238,55 +238,41 @@ public class DigEffect extends SpellAbilityEffect {
if (sa.hasParam("RandomOrder")) {
CardLists.shuffle(movedCards);
}
}
else {
} else {
String prompt;
if (sa.hasParam("PrimaryPrompt")) {
prompt = sa.getParam("PrimaryPrompt");
} else {
prompt = "Choose a card to put into " + destZone1.name();
if (destZone1.equals(ZoneType.Library)) {
if (libraryPosition == -1) {
prompt = "Choose a card to put on the bottom of {player's} library";
}
else if (libraryPosition == 0) {
prompt = "Choose a card to put on top of {player's} library";
}
}
}
movedCards = new CardCollection();
for (int i = 0; i < destZone1ChangeNum || (anyNumber && i < numToDig); i++) {
// let user get choice
Card chosen = null;
if (!valid.isEmpty()) {
// If we're choosing multiple cards, only need to show the reveal dialog the first time through.
boolean shouldReveal = (i == 0);
chosen = chooser.getController().chooseSingleEntityForEffect(valid, shouldReveal ? delayedReveal : null, sa, prompt, anyNumber || optional, p);
}
else {
if (i == 0) {
chooser.getController().notifyOfValue(sa, null, "No valid cards");
}
}
if (chosen == null) {
break;
}
movedCards.add(chosen);
valid.remove(chosen);
if (!andOrValid.equals("")) {
andOrCards.remove(chosen);
if (!chosen.isValid(andOrValid.split(","), host.getController(), host, sa)) {
valid = new CardCollection(andOrCards);
}
else if (!chosen.isValid(changeValid.split(","), host.getController(), host, sa)) {
valid.removeAll((Collection<?>)andOrCards);
}
}
}
if (sa.hasParam("PrimaryPrompt")) {
prompt = sa.getParam("PrimaryPrompt");
} else {
prompt = "Choose card(s) to put into " + destZone1.name();
if (destZone1.equals(ZoneType.Library)) {
if (libraryPosition == -1) {
prompt = "Choose card(s) to put on the bottom of {player's} library";
} else if (libraryPosition == 0) {
prompt = "Choose card(s) to put on top of {player's} library";
}
}
}
movedCards = new CardCollection();
if (valid.isEmpty()) {
chooser.getController().notifyOfValue(sa, null, "No valid cards");
} else {
if ( p == chooser ) { // the digger can still see all the dug cards when choosing
chooser.getController().tempShowCards(top);
}
List<Card> chosen;
if (!andOrValid.equals("")) {
valid.removeAll(andOrCards); //pfps remove andOr cards to get two two choices set up correctly
chosen = chooser.getController().chooseFromTwoListsForEffect(valid, andOrCards, optional, delayedReveal, sa, prompt, p);
} else {
int max = anyNumber ? valid.size() : Math.min(valid.size(),destZone1ChangeNum);
int min = (anyNumber || optional) ? 0 : max;
chosen = chooser.getController().chooseEntitiesForEffect(valid, min, max, delayedReveal, sa, prompt, p);
}
chooser.getController().endTempShowCards();
movedCards.addAll(chosen);
}
if (!changeValid.isEmpty() && !sa.hasParam("ExileFaceDown") && !sa.hasParam("NoReveal")) {
game.getAction().reveal(movedCards, chooser, true,
......
......@@ -153,6 +153,9 @@ public class FightEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
......
......@@ -1465,7 +1465,8 @@ public class Card extends GameEntity implements Comparable<Card> {
}
}
}
if (keyword.startsWith("CantBeCounteredBy")) {
if (keyword.startsWith("CantBeCounteredBy") || keyword.startsWith("Panharmonicon")
|| keyword.startsWith("Dieharmonicon")) {
final String[] p = keyword.split(":");
sbLong.append(p[2]).append("\r\n");
} else if (keyword.startsWith("etbCounter")) {
......@@ -3404,6 +3405,15 @@ public class Card extends GameEntity implements Comparable<Card> {
return change;
}
public final boolean hasChangedCardKeywords(final long timestamp) {
return changedCardKeywords.containsKey(timestamp);
}
public final void addChangedCardKeywordsInternal(final KeywordsChange change, final long timestamp) {
changedCardKeywords.put(timestamp, change);
updateKeywordsCache(currentState);
}
// Hidden keywords will be left out
public final Collection<KeywordInterface> getUnhiddenKeywords() {
return getUnhiddenKeywords(currentState);
......@@ -5731,7 +5741,7 @@ public class Card extends GameEntity implements Comparable<Card> {
public void setChangedCardKeywords(Map<Long, KeywordsChange> changedCardKeywords) {
this.changedCardKeywords.clear();
for (Entry<Long, KeywordsChange> entry : changedCardKeywords.entrySet()) {
this.changedCardKeywords.put(entry.getKey(), entry.getValue());
this.changedCardKeywords.put(entry.getKey(), entry.getValue().copy(this, true));
}
}
......
......@@ -19,6 +19,13 @@ import forge.game.trigger.TriggerType;
public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
private Table<Card, GameEntity, Integer> dataMap = HashBasedTable.create();
public CardDamageMap(Table<Card, GameEntity, Integer> damageMap) {
this.putAll(damageMap);
}
public CardDamageMap() {
}
public void triggerPreventDamage(boolean isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> e : this.columnMap().entrySet()) {
int sum = 0;
......
......@@ -11,6 +11,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Table;
import forge.game.Game;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
......@@ -45,4 +46,39 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
game.getTriggerHandler().runTrigger(TriggerType.ChangesZoneAll, runParams, false);
}
}
public CardCollection filterCards(Iterable<ZoneType> origin, ZoneType destination, String valid, Card host, SpellAbility sa) {
CardCollection allCards = new CardCollection();
if (destination != null) {
if (!containsColumn(destination)) {
return allCards;
}
}
if (origin != null) {
for (ZoneType z : origin) {
if (containsRow(z)) {
if (destination != null) {
allCards.addAll(row(z).get(destination));
} else {
for (CardCollection c : row(z).values()) {
allCards.addAll(c);
}
}
}
}
} else if (destination != null) {
for (CardCollection c : column(destination).values()) {
allCards.addAll(c);
}
} else {
for (CardCollection c : values()) {
allCards.addAll(c);
}
}
if (valid != null) {
allCards = CardLists.getValidCards(allCards, valid.split(","), host.getController(), host, sa);
}
return allCards;
}
}
......@@ -816,6 +816,7 @@ public class Combat {
}
preventMap.triggerPreventDamage(true);
preventMap.clear();
// This was deeper before, but that resulted in the stack entry acting like before.
// Run the trigger to deal combat damage once
......
......@@ -366,10 +366,9 @@ public class CostAdjustment {
if (manaCost.toString().equals("{0}")) {
return 0;
}
final Map<String, String> params = staticAbility.getMapParams();
final Card hostCard = staticAbility.getHostCard();
final Card card = sa.getHostCard();
final String amount = params.get("Amount");
final String amount = staticAbility.getParam("Amount");
if (!checkRequirement(sa, staticAbility)) {
return 0;
......@@ -380,14 +379,16 @@ public class CostAdjustment {
value = CardFactoryUtil.xCount(card, hostCard.getSVar(amount));
} else if ("Undaunted".equals(amount)) {
value = card.getController().getOpponents().size();
} else if (staticAbility.hasParam("Relative")) {
value = AbilityUtils.calculateAmount(hostCard, amount, sa);
} else {
value = AbilityUtils.calculateAmount(hostCard, amount, staticAbility);
}
if (!params.containsKey("Cost") && ! params.containsKey("Color")) {
if (!staticAbility.hasParam("Cost") && ! staticAbility.hasParam("Color")) {
int minMana = 0;
if (params.containsKey("MinMana")) {
minMana = Integer.valueOf(params.get("MinMana"));
if (staticAbility.hasParam("MinMana")) {
minMana = Integer.valueOf(staticAbility.getParam("MinMana"));
}
final int maxReduction = Math.max(0, manaCost.getConvertedManaCost() - minMana);
......@@ -395,7 +396,7 @@ public class CostAdjustment {
return Math.min(value, maxReduction);
}
} else {
final String color = params.containsKey("Cost") ? params.get("Cost") : params.get("Color");
final String color = staticAbility.getParamOrDefault("Cost", staticAbility.getParam("Color"));
int sumGeneric = 0;
// might be problematic for wierd hybrid combinations
for (final String cost : color.split(" ")) {
......
......@@ -74,6 +74,8 @@ public class CostDamage extends CostPart {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
return decision.c > 0;
}
......
......@@ -8,6 +8,8 @@ import java.util.Iterator;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import forge.game.card.Card;
public class KeywordCollection implements Iterable<String>, Serializable {
private static final long serialVersionUID = -2882986558147844702L;
......@@ -151,6 +153,12 @@ public class KeywordCollection implements Iterable<String>, Serializable {
return map.get(keyword);
}
public void setHostCard(final Card host) {
for (KeywordInterface k : map.values()) {
k.setHostCard(host);
}
}
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
......
......@@ -202,7 +202,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
public Collection<StaticAbility> getStaticAbilities() {
return staticAbilities;
}
/*
* (non-Javadoc)
* @see forge.game.keyword.KeywordInterface#copy()
......@@ -233,7 +233,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
return result;
} catch (final Exception ex) {
throw new RuntimeException("KeywordInstance : clone() error, " + ex);
throw new RuntimeException("KeywordInstance : clone() error", ex);
}
}
......@@ -252,4 +252,26 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
public boolean redundant(Collection<KeywordInterface> list) {
return !list.isEmpty() && keyword.isMultipleRedundant;
}
/* (non-Javadoc)
* @see forge.game.keyword.KeywordInterface#setHostCard(forge.game.card.Card)
*/
@Override
public void setHostCard(Card host) {
for (SpellAbility sa : this.abilities) {
sa.setHostCard(host);