Merge branch 'master' into code-cleanup

# Conflicts:
#	forge-ai/src/main/java/forge/ai/AiAttackController.java
#	forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
#	forge-core/src/main/java/forge/item/IPaperCard.java
#	forge-game/src/main/java/forge/game/ForgeScript.java
#	forge-game/src/main/java/forge/game/ability/effects/ManifestBaseEffect.java
#	forge-game/src/main/java/forge/game/card/CardState.java
#	forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java
#	forge-game/src/main/java/forge/game/trigger/Trigger.java
This commit is contained in:
Jetz
2024-09-15 18:11:24 -04:00
1066 changed files with 6295 additions and 1479 deletions

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '8', '11' ]
java: [ '11' ]
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -790,6 +790,7 @@ public class AiAttackController {
if (bAssault) {
return prefDefender;
}
// 2. attack planeswalkers
List<Card> pwDefending = c.getDefendingPlaneswalkers();
if (!pwDefending.isEmpty()) {
@@ -797,7 +798,7 @@ public class AiAttackController {
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
}
// Get the preferred battle (prefer own battles, then ally battles)
// 3. Get the preferred battle (prefer own battles, then ally battles)
final CardCollection defBattles = c.getDefendingBattles();
List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
@@ -1164,10 +1165,8 @@ public class AiAttackController {
attritionalAttackers.remove(attritionalAttackers.size() - 1);
}
}
attackRounds += 1;
if (humanLife <= 0) {
doAttritionalAttack = true;
}
attackRounds++;
doAttritionalAttack = humanLife <= 0;
}
// *********************
// end attritional attack calculation
@@ -1328,6 +1327,114 @@ public class AiAttackController {
return aiAggression;
}
private class SpellAbilityFactors {
Card attacker = null;
boolean canBeKilled = false; // indicates if the attacker can be killed
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
boolean canKillAll = true; // indicates if the attacker can kill all single blockers
boolean canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
boolean isWorthLessThanAllKillers = true;
boolean hasAttackEffect = false;
boolean hasCombatEffect = false;
boolean dangerousBlockersPresent = false;
boolean canTrampleOverDefenders = false;
int numberOfPossibleBlockers = 0;
int defPower = 0;
SpellAbilityFactors(Card c) {
attacker = c;
}
private boolean canBeBlocked() {
return numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent));
}
private void calculate(final List<Card> defenders, final Combat combat) {
hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasKeyword(Keyword.ANNIHILATOR);
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"))
|| attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT);
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, Card::getNetToughness);
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
dangerousBlockersPresent = validBlockers.anyMatch(
CardPredicates.hasKeyword(Keyword.WITHER)
.or(CardPredicates.hasKeyword(Keyword.INFECT))
.or(CardPredicates.hasKeyword(Keyword.LIFELINK))
);
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
defPower = CardLists.getTotalPower(validBlockers, true, false);
// look at the attacker in relation to the blockers to establish a
// number of factors about the attacking context that will be relevant
// to the attackers decision according to the selected strategy
for (final Card blocker : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
if (isWorthLessThanAllKillers && !attacker.hasSVar("SacMe")
&& ComputerUtilCard.evaluateCreature(blocker) <= ComputerUtilCard.evaluateCreature(attacker)) {
isWorthLessThanAllKillers = false;
}
}
// see if this attacking creature can destroy this defender, if
// not record that it can't kill everything
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
canKillAll = false;
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")
|| blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT) || blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
}
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
boolean avoidAttackingIntoBlock = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
boolean attackerWillDie = defPower >= attacker.getNetToughness();
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
boolean noContributionToAttack = attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
// We are attacking too recklessly if we can't kill a single blocker and:
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
canKillAllDangerous = false;
}
}
}
}
}
// performance-wise it doesn't seem worth it to check attackVigilance() instead (only includes a single niche card)
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
isWorthLessThanAllKillers = false;
hasCombatEffect = false;
} else if ((canKillAllDangerous || !canBeKilled) && ComputerUtilCard.canBeBlockedProfitably(defendingOpponent, attacker, true)) {
canKillAllDangerous = false;
canBeKilled = true;
}
}
}
/**
* <p>
* shouldAttack.
@@ -1342,14 +1449,6 @@ public class AiAttackController {
* @return a boolean.
*/
public final boolean shouldAttack(final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) {
boolean canBeKilled = false; // indicates if the attacker can be killed
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
boolean canKillAll = true; // indicates if the attacker can kill all single blockers
boolean canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
boolean isWorthLessThanAllKillers = true;
boolean canBeBlocked = false;
int numberOfPossibleBlockers = 0;
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
if (attacker.hasSVar("NonCombatPriority") && !attacker.hasKeyword(Keyword.VIGILANCE)) {
// For each level of priority, enemy has to have life as much as the creature's power
@@ -1360,7 +1459,7 @@ public class AiAttackController {
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine!
for (SpellAbility sa : attacker.getSpellAbilities()) {
// Do not attack if we can afford using the ability.
if (sa.isActivatedAbility()) {
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) {
if (ComputerUtilCost.canPayCost(sa, ai, false)) {
return false;
}
@@ -1374,115 +1473,29 @@ public class AiAttackController {
if (!isEffectiveAttacker(ai, attacker, combat, defender)) {
return false;
}
boolean hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasKeyword(Keyword.ANNIHILATOR);
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"));
if (!hasCombatEffect) {
if (attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT)) {
hasCombatEffect = true;
}
}
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
boolean canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, Card::getNetToughness);
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
boolean dangerousBlockersPresent = validBlockers.anyMatch(
CardPredicates.hasKeyword(Keyword.WITHER)
.or(CardPredicates.hasKeyword(Keyword.INFECT))
.or(CardPredicates.hasKeyword(Keyword.LIFELINK))
);
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
int defPower = CardLists.getTotalPower(validBlockers, true, false);
// look at the attacker in relation to the blockers to establish a
// number of factors about the attacking context that will be relevant
// to the attackers decision according to the selected strategy
for (final Card blocker : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
if (isWorthLessThanAllKillers && !attacker.hasSVar("SacMe")
&& ComputerUtilCard.evaluateCreature(blocker) <= ComputerUtilCard.evaluateCreature(attacker)) {
isWorthLessThanAllKillers = false;
}
}
// see if this attacking creature can destroy this defender, if
// not record that it can't kill everything
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
canKillAll = false;
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")) {
canKillAllDangerous = false;
} else {
if (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)
|| blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
}
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
boolean avoidAttackingIntoBlock = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
boolean attackerWillDie = defPower >= attacker.getNetToughness();
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
boolean noContributionToAttack = this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
// We are attacking too recklessly if we can't kill a single blocker and:
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
canKillAllDangerous = false;
}
}
}
}
}
}
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
isWorthLessThanAllKillers = false;
hasCombatEffect = false;
} else if ((canKillAllDangerous || !canBeKilled) && ComputerUtilCard.canBeBlockedProfitably(defendingOpponent, attacker, true)) {
canKillAllDangerous = false;
canBeKilled = true;
SpellAbilityFactors saf = new SpellAbilityFactors(attacker);
if (aiAggression != 5) {
saf.calculate(defenders, combat);
}
// if the creature cannot block and can kill all opponents they might as
// well attack, they do nothing staying back
if (canKillAll && isWorthLessThanAllKillers && !CombatUtil.canBlock(attacker)) {
if (saf.canKillAll && saf.isWorthLessThanAllKillers && !CombatUtil.canBlock(attacker)) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player");
return true;
} else if (!canBeKilled && !dangerousBlockersPresent && canTrampleOverDefenders) {
}
if (!saf.canBeKilled && !saf.dangerousBlockersPresent && saf.canTrampleOverDefenders) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through");
return true;
}
if (numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent))) {
canBeBlocked = true;
}
// decide if the creature should attack based on the prevailing strategy choice in aiAggression
switch (aiAggression) {
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
if ((saf.canKillAll && saf.isWorthLessThanAllKillers) || !saf.canBeBlocked()) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
return true;
@@ -1493,32 +1506,32 @@ public class AiAttackController {
System.out.println(attacker.getName() + " = all out attacking");
return true;
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
|| (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
if (saf.canKillAll || (saf.dangerousBlockersPresent && saf.canKillAllDangerous && !saf.canBeKilledByOne) || !saf.canBeBlocked()
|| saf.defPower == 0) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
return true;
}
break;
case 3: // expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers)
|| (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|| !canBeBlocked) {
if ((saf.canKillAll && saf.isWorthLessThanAllKillers)
|| (((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne)
|| !saf.canBeBlocked()) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
return true;
}
break;
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
if (!saf.canBeBlocked() || ((saf.canKillAll || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne &&
((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || !saf.canBeKilled))) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
return true;
}
break;
case 1: // unblockable creatures only
if (!canBeBlocked || (numberOfPossibleBlockers == 1 && canKillAll && !canBeKilledByOne)) {
if (!saf.canBeBlocked() || (saf.numberOfPossibleBlockers == 1 && saf.canKillAll && !saf.canBeKilledByOne)) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
return true;

View File

@@ -53,6 +53,7 @@ import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
@@ -918,7 +919,7 @@ public class ComputerUtil {
}
} else if (isOptional && source.getActivatingPlayer().isOpponentOf(ai)) {
if ("Pillar Tombs of Aku".equals(host.getName())) {
if (!ai.canLoseLife() || ai.cantLose()) {
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
return sacrificed; // sacrifice none
}
} else {
@@ -2686,7 +2687,7 @@ public class ComputerUtil {
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
// try to mill opponent with Quill vote
if (opponent && !controller.cantLose()) {
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
int numQuill = votes.get("Quill").size();
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
@@ -3249,7 +3250,7 @@ public class ComputerUtil {
// performance shortcut
// TODO if checking upcoming turn it should be a permanent effect
if (ai.cantLose()) {
if (ai.cantLoseForZeroOrLessLife()) {
return remainingLife;
}
@@ -3308,8 +3309,7 @@ public class ComputerUtil {
repParams.put(AbilityKey.EffectOnly, true);
repParams.put(AbilityKey.CounterTable, table);
repParams.put(AbilityKey.CounterMap, table.column(c));
List<ReplacementEffect> list = c.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.CantHappen);
return !list.isEmpty();
return c.getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Moved, repParams);
}
public static boolean shouldSacrificeThreatenedCard(Player ai, Card c, SpellAbility sa) {

View File

@@ -353,7 +353,7 @@ public class ComputerUtilAbility {
}
// 1. increase chance of using Surge effects
// 2. non-surged versions are usually inefficient
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
if (source.hasKeyword(Keyword.SURGE) && !sa.isSurged()) {
p -= 9;
}
// move snap-casted spells to front
@@ -386,8 +386,10 @@ public class ComputerUtilAbility {
}
if (ApiType.DestroyAll == sa.getApi()) {
// check boardwipe earlier
p += 4;
} else if (ApiType.Mana == sa.getApi()) {
// keep mana abilities for paying
p -= 9;
}
@@ -398,7 +400,7 @@ public class ComputerUtilAbility {
return p;
}
};
}
public static List<SpellAbility> sortCreatureSpells(final List<SpellAbility> all) {
// try to smoothen power creep by making CMC less of a factor

View File

@@ -389,7 +389,7 @@ public class ComputerUtilCard {
if (Iterables.size(list) == 1) {
return Iterables.get(list, 0);
}
return Aggregates.itemWithMax(Iterables.filter(list, CardPredicates.LANDS), ComputerUtilCard.landEvaluator);
return Aggregates.itemWithMax(Iterables.filter(list, Card::hasPlayableLandFace), ComputerUtilCard.landEvaluator);
}
/**
@@ -1752,7 +1752,7 @@ public class ComputerUtilCard {
pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0);
if (!kws.isEmpty()) {
pumped.addChangedCardKeywords(kws, null, false, timestamp, 0, false);
pumped.addChangedCardKeywords(kws, null, false, timestamp, null, false);
}
if (!hiddenKws.isEmpty()) {
pumped.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws);
@@ -1773,7 +1773,7 @@ public class ComputerUtilCard {
}
}
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
pumped.addChangedCardKeywordsInternal(toCopy, null, false, timestamp2, 0, false);
pumped.addChangedCardKeywordsInternal(toCopy, null, false, timestamp2, null, false);
pumped.updateKeywordsCache(pumped.getCurrentState());
applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
return pumped;

View File

@@ -724,7 +724,6 @@ public class ComputerUtilCombat {
return totalDamageOfBlockers(attacker, blockers) >= getDamageToKill(attacker, false);
}
// Will this trigger trigger?
/**
* <p>
* combatTriggerWillTrigger.

View File

@@ -391,6 +391,10 @@ public abstract class GameState {
}
newText.append("|MergedCards:").append(TextUtil.join(mergedCardNames, ","));
}
if (c.getClassLevel() > 1) {
newText.append("|ClassLevel:").append(c.getClassLevel());
}
}
if (zoneType == ZoneType.Exile) {
@@ -1179,9 +1183,8 @@ public abstract class GameState {
zone.setCards(kv.getValue());
}
}
for (Card cmd : p.getCommanders()) {
p.getZone(ZoneType.Command).add(Player.createCommanderEffect(p.getGame(), cmd));
}
if (!p.getCommanders().isEmpty())
p.createCommanderEffect(); //Original one was lost, and the one made by addCommander would have been erased by setCards.
updateManaPool(p, state.manaPool, true, false);
updateManaPool(p, state.persistentMana, false, true);
@@ -1327,10 +1330,7 @@ public abstract class GameState {
c.setExiledWith(c); // This seems to be the way it's set up internally. Potentially not needed here?
c.setExiledBy(c.getController());
} else if (info.startsWith("IsCommander")) {
c.setCommander(true);
List<Card> cmd = Lists.newArrayList(player.getCommanders());
cmd.add(c);
player.setCommanders(cmd);
player.addCommander(c);
} else if (info.startsWith("IsRingBearer")) {
c.setRingBearer(true);
player.setRingBearer(c);
@@ -1398,6 +1398,8 @@ public abstract class GameState {
c.setTurnInZone(turn);
} else if (info.equals("IsToken")) {
c.setGamePieceType(GamePieceType.TOKEN);
} else if (info.equals("ClassLevel:")) {
c.setClassLevel(Integer.parseInt(info.substring(info.indexOf(':') + 1)));
}
}

View File

@@ -722,6 +722,7 @@ public class DamageDealAi extends DamageAiBase {
if (sa.canTarget(enemy) && sa.canAddMoreTarget()) {
if ((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (isSorcerySpeed(sa, ai) && phase.is(PhaseType.MAIN2))
|| ("BurnCreatures".equals(logic) && !enemy.getCreaturesInPlay().isEmpty())
|| immediately) {
boolean pingAfterAttack = "PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai);
boolean isPWAbility = sa.isPwAbility() && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class);

View File

@@ -38,6 +38,7 @@ import forge.game.card.CounterType;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
@@ -325,7 +326,7 @@ public class DrawAi extends SpellAbilityAi {
}
// try to kill opponent
if (oppA.cantLose() || !oppA.canDraw()) {
if (oppA.cantLoseCheck(GameLossReason.Milled) || !oppA.canDraw()) {
continue;
}

View File

@@ -28,7 +28,7 @@ public class FlipACoinAi extends SpellAbilityAi {
}
sa.resetTargets();
for (Player o : ai.getOpponents()) {
if (sa.canTarget(o) && o.canLoseLife() && !o.cantLose()) {
if (sa.canTarget(o) && o.canLoseLife() && !o.cantLoseForZeroOrLessLife()) {
sa.getTargets().add(o);
return true;
}

View File

@@ -199,7 +199,7 @@ public class LifeLoseAi extends SpellAbilityAi {
// try first to find Opponent that can lose life and lose the game
if (!opps.isEmpty()) {
for (Player opp : opps) {
if (opp.canLoseLife() && !opp.cantLose()) {
if (opp.canLoseLife() && !opp.cantLoseForZeroOrLessLife()) {
sa.getTargets().add(opp);
return true;
}

View File

@@ -7,6 +7,7 @@ import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
@@ -85,7 +86,7 @@ public class PoisonAi extends SpellAbilityAi {
if (!tgts.isEmpty()) {
// try to select a opponent that can lose through poison counters
PlayerCollection betterTgts = tgts.filter(input -> {
if (input.cantLose()) {
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return false;
} else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
return false;
@@ -106,7 +107,7 @@ public class PoisonAi extends SpellAbilityAi {
if (tgts.isEmpty()) {
if (mandatory) {
// AI is uneffected
if (ai.canBeTargetedBy(sa) && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
sa.getTargets().add(ai);
return true;
}
@@ -115,7 +116,7 @@ public class PoisonAi extends SpellAbilityAi {
if (!allies.isEmpty()) {
// some ally would be unaffected
PlayerCollection betterAllies = allies.filter(input -> {
if (input.cantLose()) {
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return true;
}
return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON));

View File

@@ -17,6 +17,7 @@ import forge.ai.LobbyPlayerAi;
import forge.card.CardRarity;
import forge.card.CardRules;
import forge.game.*;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
@@ -66,11 +67,7 @@ public class GameCopier {
}
public Game makeCopy() {
if (origGame.EXPERIMENTAL_RESTORE_SNAPSHOT) {
return snapshot.makeCopy();
} else {
return makeCopy(null, null);
}
return makeCopy(null, null);
}
public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) {
if (origGame.EXPERIMENTAL_RESTORE_SNAPSHOT) {
@@ -115,7 +112,6 @@ public class GameCopier {
for (Mana m : origPlayer.getManaPool()) {
newPlayer.getManaPool().addMana(m, false);
}
newPlayer.setCommanders(origPlayer.getCommanders()); // will be fixed up below
playerMap.put(origPlayer, newPlayer);
}
@@ -129,27 +125,10 @@ public class GameCopier {
copyGameState(newGame, aiPlayer);
for (Player p : newGame.getPlayers()) {
List<Card> commanders = Lists.newArrayList();
for (final Card c : p.getCommanders()) {
commanders.add(gameObjectMap.map(c));
}
p.setCommanders(commanders);
((PlayerZoneBattlefield) p.getZone(ZoneType.Battlefield)).setTriggers(true);
}
for (Player origPlayer : playerMap.keySet()) {
Player newPlayer = playerMap.get(origPlayer);
for (final Card c : origPlayer.getCommanders()) {
Card newCommander = gameObjectMap.map(c);
int castTimes = origPlayer.getCommanderCast(c);
for (int i = 0; i < castTimes; i++) {
newPlayer.incCommanderCast(newCommander);
}
}
for (Map.Entry<Card, Integer> entry : origPlayer.getCommanderDamage()) {
Card newCommander = gameObjectMap.map(entry.getKey());
newPlayer.addCommanderDamage(newCommander, entry.getValue());
}
origPlayer.copyCommandersToSnapshot(newPlayer, gameObjectMap::map);
((PlayerZoneBattlefield) newPlayer.getZone(ZoneType.Battlefield)).setTriggers(true);
}
newGame.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
@@ -328,7 +307,11 @@ public class GameCopier {
// The issue is that it requires parsing the original card from scratch from the paper card. We should
// improve the copier to accurately copy the card from its actual state, so that the paper card shouldn't
// be needed. Once the below code accurately copies the card, remove the USE_FROM_PAPER_CARD code path.
Card newCard = new Card(newGame.nextCardId(), c.getPaperCard(), newGame);
Card newCard;
if (c instanceof DetachedCardEffect)
newCard = new DetachedCardEffect((DetachedCardEffect) c, newGame, true);
else
newCard = new Card(newGame.nextCardId(), c.getPaperCard(), newGame);
newCard.setOwner(newOwner);
newCard.setName(c.getName());
newCard.setCommander(c.isCommander());

View File

@@ -132,8 +132,11 @@ public final class CardEdition implements Comparable<CardEdition> {
SPECIAL_SLOT("special slot"), //to help with convoluted boosters
PRECON_PRODUCT("precon product"),
BORDERLESS("borderless"),
BORDERLESS_PROFILE("borderless profile"),
BORDERLESS_FRAME("borderless frame"),
ETCHED("etched"),
SHOWCASE("showcase"),
FULL_ART("full art"),
EXTENDED_ART("extended art"),
ALTERNATE_ART("alternate art"),
ALTERNATE_FRAME("alternate frame"),

View File

@@ -156,6 +156,9 @@ final class CardFace implements ICardFace, Cloneable {
return null;
return this.functionalVariants.get(variant);
}
@Override public Map<String, ? extends ICardFace> getFunctionalVariants() {
return this.functionalVariants;
}
CardFace getOrCreateFunctionalVariant(String variant) {
if (this.functionalVariants == null) {
this.functionalVariants = new HashMap<>();

View File

@@ -323,6 +323,17 @@ public final class CardRulesPredicates {
if (face == null) {
return false;
}
if (face.hasFunctionalVariants()) {
for (Map.Entry<String, ? extends ICardFace> v : face.getFunctionalVariants().entrySet()) {
//Not a very pretty implementation, but an ICardFace doesn't have a specific variant, so they all need to be checked.
String origOracle = v.getValue().getOracleText();
if(op(origOracle, operand))
return true;
String name = v.getValue().getName() + " $" + v.getKey();
if(op(CardTranslation.getTranslatedOracle(name), operand))
return true;
}
}
if (op(face.getOracleText(), operand) || op(CardTranslation.getTranslatedOracle(face.getName()), operand)) {
return true;
}
@@ -332,6 +343,16 @@ public final class CardRulesPredicates {
if (face == null) {
return false;
}
if (face.hasFunctionalVariants()) {
for (Map.Entry<String, ? extends ICardFace> v : face.getFunctionalVariants().entrySet()) {
String origType = v.getValue().getType().toString();
if(op(origType, operand))
return true;
String name = v.getValue().getName() + " $" + v.getKey();
if(op(CardTranslation.getTranslatedType(name, origType), operand))
return true;
}
}
return (op(CardTranslation.getTranslatedType(face.getName(), face.getType().toString()), operand) || op(face.getType().toString(), operand));
}

View File

@@ -47,6 +47,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
public static final CardTypeView EMPTY = new CardType(false);
public enum CoreType {
Kindred(false, "kindreds"), // always printed first
Artifact(true, "artifacts"),
Battle(true, "battles"),
Conspiracy(false, "conspiracies"),
@@ -60,7 +61,6 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
Planeswalker(true, "planeswalkers"),
Scheme(false, "schemes"),
Sorcery(false, "sorceries"),
Kindred(false, "kindreds"),
Vanguard(false, "vanguards");
public final boolean isPermanent;

View File

@@ -1,5 +1,7 @@
package forge.card;
import java.util.Map;
/**
* TODO: Write javadoc for this type.
*
@@ -9,4 +11,5 @@ public interface ICardFace extends ICardCharacteristics, ICardRawAbilites, Compa
boolean hasFunctionalVariants();
ICardFace getFunctionalVariant(String variant);
Map<String, ? extends ICardFace> getFunctionalVariants();
}

View File

@@ -45,7 +45,7 @@ public class BoosterSlot {
}
public String replaceSlot() {
double rand = Math.random() * 100;
float rand = (float) Math.random();
for (Float key : slotPercentages.keySet()) {
if (rand < key) {
System.out.println("Replaced a base slot! " + slotName + " -> " + slotPercentages.get(key));

View File

@@ -37,5 +37,22 @@ public interface IPaperCard extends InventoryItem, Serializable {
String getCardRSpecImageKey();
String getCardGSpecImageKey();
public boolean isRebalanced();
boolean isRebalanced();
@Override
default String getTranslationKey() {
if(!NO_FUNCTIONAL_VARIANT.equals(getFunctionalVariant()))
return getName() + " $" + getFunctionalVariant();
return getName();
}
@Override
default String getUntranslatedType() {
return getRules().getType().toString();
}
@Override
default String getUntranslatedOracle() {
return getRules().getOracleText();
}
}

View File

@@ -17,13 +17,18 @@
*/
package forge.item;
import forge.util.IHasName;
import forge.util.ITranslatable;
/**
* Interface to define a player's inventory may hold. Should include
* CardPrinted, Booster, Pets, Plants... etc
*/
public interface InventoryItem extends IHasName {
public interface InventoryItem extends ITranslatable {
String getItemType();
String getImageKey(boolean altState);
@Override
default String getUntranslatedType() {
return getItemType();
}
}

View File

@@ -138,11 +138,6 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return unFoiledVersion;
}
// @Override
// public String getImageKey() {
// return getImageLocator(getImageName(), getArtIndex(), true, false);
// }
@Override
public String getItemType() {
final Localizer localizer = Localizer.getInstance();

View File

@@ -410,6 +410,11 @@ public class BoosterGenerator {
BoosterSlot boosterSlot = boosterSlots.get(slotType);
String determineSheet = boosterSlot.replaceSlot();
if (determineSheet.endsWith("+")) {
determineSheet = determineSheet.substring(0, determineSheet.length() - 1);
convertCardFoil = true;
}
String setCode = template.getEdition();
// Ok, so we have a sheet now. Most should be standard sheets, but some named edition sheets

View File

@@ -28,6 +28,15 @@ public class CardTranslation {
for (String line : translationFile.readLines()) {
String[] matches = line.split("\\|");
if (matches.length >= 2) {
if (matches[0].indexOf('$') > 0) {
//Functional variant, e.g. "Garbage Elemental $C"
String[] variantSplit = matches[0].split("\\s*\\$", 2);
if(variantSplit.length > 1) {
//Add the base name to the translated names.
translatednames.put(variantSplit[0], matches[1]);
matches[0] = variantSplit[0] + " $" + variantSplit[1]; //Standardize storage.
}
}
translatednames.put(matches[0], matches[1]);
}
if (matches.length >= 3) {
@@ -53,7 +62,7 @@ public class CardTranslation {
if (name.contains(" // ")) {
int splitIndex = name.indexOf(" // ");
String leftname = name.substring(0, splitIndex);
String rightname = name.substring(splitIndex + 4, name.length());
String rightname = name.substring(splitIndex + 4);
return translatednames.getOrDefault(leftname, leftname) + " // " + translatednames.getOrDefault(rightname, rightname);
}
try {
@@ -74,6 +83,10 @@ public class CardTranslation {
return name;
}
public static String getTranslatedName(ITranslatable card) {
return getTranslatedName(card.getUntranslatedName());
}
private static String translateTokenName(String name) {
if (translatedTokenNames == null)
translatedTokenNames = new HashMap<>();
@@ -203,6 +216,12 @@ public class CardTranslation {
return originaltype;
}
public static String getTranslatedType(ITranslatable item) {
if (!needsTranslation())
return item.getUntranslatedType();
return translatedtypes.getOrDefault(item.getTranslationKey(), item.getUntranslatedType());
}
public static String getTranslatedOracle(String name) {
if (needsTranslation()) {
String toracle = translatedoracles.get(name);
@@ -212,13 +231,30 @@ public class CardTranslation {
return "";
}
public static HashMap<String, String> getTranslationTexts(String cardname, String altcardname) {
if (!needsTranslation()) return null;
public static String getTranslatedOracle(ITranslatable card) {
if(!needsTranslation())
return ""; //card.getUntranslatedOracle();
//Fallbacks and english versions of oracle texts are handled elsewhere.
return translatedoracles.getOrDefault(card.getTranslationKey(), "");
}
public static HashMap<String, String> getTranslationTexts(ITranslatable card) {
return getTranslationTexts(card, null);
}
public static HashMap<String, String> getTranslationTexts(ITranslatable cardMain, ITranslatable cardOther) {
if(!needsTranslation()) return null;
HashMap<String, String> translations = new HashMap<>();
translations.put("name", getTranslatedName(cardname));
translations.put("oracle", getTranslatedOracle(cardname));
translations.put("altname", getTranslatedName(altcardname));
translations.put("altoracle", getTranslatedOracle(altcardname));
translations.put("name", getTranslatedName(cardMain));
translations.put("oracle", getTranslatedOracle(cardMain));
if(cardOther == null) {
translations.put("altname", "");
translations.put("altoracle", "");
}
else {
translations.put("altname", getTranslatedName(cardOther));
translations.put("altoracle", getTranslatedOracle(cardOther));
}
return translations;
}
@@ -248,14 +284,17 @@ public class CardTranslation {
return result;
}
public static void buildOracleMapping(String faceName, String oracleText) {
if (!needsTranslation() || oracleMappings.containsKey(faceName)) return;
String translatedText = getTranslatedOracle(faceName);
public static void buildOracleMapping(String faceName, String oracleText, String variantName) {
String translationKey = faceName;
if(variantName != null)
translationKey = faceName + " $" + variantName;
if (!needsTranslation() || oracleMappings.containsKey(translationKey)) return;
String translatedText = getTranslatedOracle(translationKey);
if (translatedText.isEmpty()) {
// english card only, fall back
return;
}
String translatedName = getTranslatedName(faceName);
String translatedName = getTranslatedName(translationKey);
List <Pair <String, String> > mapping = new ArrayList<>();
String [] splitOracleText = oracleText.split("\\\\n");
String [] splitTranslatedText = translatedText.split("\r\n\r\n");
@@ -269,17 +308,17 @@ public class CardTranslation {
}
mapping.add(Pair.of(toracle, ttranslated));
}
oracleMappings.put(faceName, mapping);
oracleMappings.put(translationKey, mapping);
}
public static String translateMultipleDescriptionText(String descText, String cardName) {
public static String translateMultipleDescriptionText(String descText, ITranslatable card) {
if (!needsTranslation()) return descText;
String [] splitDescText = descText.split("\n");
String result = descText;
for (String text : splitDescText) {
text = text.trim();
if (text.isEmpty()) continue;
String translated = translateSingleDescriptionText(text, cardName);
String translated = translateSingleDescriptionText(text, card);
if (!text.equals(translated)) {
result = TextUtil.fastReplace(result, text, translated);
} else {
@@ -288,7 +327,7 @@ public class CardTranslation {
if (splitKeywords.length <= 1) continue;
for (String keyword : splitKeywords) {
if (keyword.contains(" ")) continue;
translated = translateSingleDescriptionText(keyword, cardName);
translated = translateSingleDescriptionText(keyword, card);
if (!keyword.equals(translated)) {
result = TextUtil.fastReplace(result, keyword, translated);
}
@@ -298,13 +337,13 @@ public class CardTranslation {
return result;
}
public static String translateSingleDescriptionText(String descText, String cardName) {
public static String translateSingleDescriptionText(String descText, ITranslatable card) {
if (descText == null)
return "";
if (!needsTranslation()) return descText;
if (translatedCaches.containsKey(descText)) return translatedCaches.get(descText);
List <Pair <String, String> > mapping = oracleMappings.get(cardName);
List <Pair <String, String> > mapping = oracleMappings.get(card.getTranslationKey());
if (mapping == null) return descText;
String result = descText;
if (!mapping.isEmpty()) {

View File

@@ -0,0 +1,22 @@
package forge.util;
public interface ITranslatable extends IHasName {
default String getTranslationKey() {
return getName();
}
//Fallback methods - used if no translation is found for the given key.
default String getUntranslatedName() {
return getName();
}
default String getUntranslatedType() {
return "";
}
default String getUntranslatedOracle() {
return "";
}
}

View File

@@ -22,11 +22,13 @@ import forge.game.card.CardView;
import forge.game.card.IHasCardView;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.zone.ZoneType;
import forge.util.Expressions;
import forge.util.ITranslatable;
/**
* Base class for Triggers,ReplacementEffects and StaticAbilities.
@@ -234,6 +236,13 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
return true;
}
}
} else if (o instanceof GameLossReason) {
for (String s : valids) {
GameLossReason valid = GameLossReason.smartValueOf(s);
if (((GameLossReason) o).name().equals(valid.name())) {
return true;
}
}
}
return false;
@@ -302,7 +311,10 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if ("True".equalsIgnoreCase(params.get("Bloodthirst")) != hostController.hasBloodthirst()) return false;
}
if (params.containsKey("FatefulHour")) {
if ("True".equalsIgnoreCase(params.get("FatefulHour")) != (hostController.getLife() > 5)) return false;
if ("True".equalsIgnoreCase(params.get("FatefulHour")) != (hostController.getLife() <= 5)) return false;
}
if (params.containsKey("Monarch")) {
if ("True".equalsIgnoreCase(params.get("Monarch")) != hostController.isMonarch()) return false;
}
if (params.containsKey("Revolt")) {
if ("True".equalsIgnoreCase(params.get("Revolt")) != hostController.hasRevolt()) return false;
@@ -412,15 +424,20 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (params.containsKey("PresentZone")) {
presentZone = ZoneType.smartValueOf(params.get("PresentZone"));
}
CardCollection list = new CardCollection();
if (presentPlayer.equals("You") || presentPlayer.equals("Any")) {
list.addAll(hostController.getCardsIn(presentZone));
}
if (presentPlayer.equals("Opponent") || presentPlayer.equals("Any")) {
list.addAll(hostController.getOpponents().getCardsIn(presentZone));
}
if (presentPlayer.equals("Any")) {
list.addAll(hostController.getAllies().getCardsIn(presentZone));
CardCollection list;
if (params.containsKey("PresentDefined")) {
list = AbilityUtils.getDefinedCards(getHostCard(), params.get("PresentDefined"), this);
} else {
list = new CardCollection();
if (presentPlayer.equals("You") || presentPlayer.equals("Any")) {
list.addAll(hostController.getCardsIn(presentZone));
}
if (presentPlayer.equals("Opponent") || presentPlayer.equals("Any")) {
list.addAll(hostController.getOpponents().getCardsIn(presentZone));
}
if (presentPlayer.equals("Any")) {
list.addAll(hostController.getAllies().getCardsIn(presentZone));
}
}
list = CardLists.getValidCards(list, sIsPresent, hostController, this.getHostCard(), this);
@@ -553,6 +570,9 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
}
protected IHasSVars getSVarFallback() {
if (this.getKeyword() != null && this.getKeyword().getStatic() != null) {
return this.getKeyword().getStatic();
}
if (getCardState() != null)
return getCardState();
return getHostCard();
@@ -623,6 +643,14 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
return getCardState().getView().getState();
}
public ITranslatable getHostName(CardTraitBase node) {
// if alternate state is viewed while card uses original
if (node.isIntrinsic() && node.cardState != null && !node.cardState.getStateName().equals(getHostCard().getCurrentStateName())) {
return node.cardState;
}
return node.getHostCard();
}
public Card getOriginalHost() {
if (getCardState() != null)
return getCardState().getCard();

View File

@@ -1,5 +1,6 @@
package forge.game;
import forge.card.CardTypeView;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
@@ -18,6 +19,7 @@ import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.TargetChoices;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCastWithFlash;
import forge.game.staticability.StaticAbilityColorlessDamageSource;
import forge.game.trigger.Trigger;
import forge.game.zone.ZoneType;
import forge.util.Expressions;
@@ -31,36 +33,30 @@ public class ForgeScript {
public static boolean cardStateHasProperty(CardState cardState, String property, Player sourceController,
Card source, CardTraitBase spellAbility) {
final boolean isColorlessSource = cardState.getCard().hasKeyword("Colorless Damage Source", cardState);
final ColorSet colors = cardState.getCard().getColor(cardState);
boolean withSource = property.endsWith("Source");
final ColorSet colors;
if (withSource && StaticAbilityColorlessDamageSource.colorlessDamageSource(cardState)) {
colors = ColorSet.getNullColor();
} else {
colors = cardState.getCard().getColor(cardState);
}
final CardTypeView type = cardState.getTypeWithChanges();
if (property.contains("White") || property.contains("Blue") || property.contains("Black")
|| property.contains("Red") || property.contains("Green")) {
boolean mustHave = !property.startsWith("non");
boolean withSource = property.endsWith("Source");
if (withSource && isColorlessSource) {
return false;
}
final String colorName = property.substring(mustHave ? 0 : 3, property.length() - (withSource ? 6 : 0));
int desiredColor = MagicColor.fromName(colorName);
boolean hasColor = colors.hasAnyColor(desiredColor);
return mustHave == hasColor;
} else if (property.contains("Colorless")) { // ... Card is colorless
} else if (property.contains("Colorless")) {
boolean non = property.startsWith("non");
boolean withSource = property.endsWith("Source");
if (non && withSource && isColorlessSource) {
return false;
}
return non != colors.isColorless();
} else if (property.contains("MultiColor")) {
} else if (property.startsWith("MultiColor")) {
// ... Card is multicolored
if (property.endsWith("Source") && isColorlessSource)
return false;
return property.startsWith("non") != colors.isMulticolor();
} else if (property.contains("EnemyColor")) {
if (property.endsWith("Source") && isColorlessSource)
return false;
return colors.isMulticolor();
} else if (property.startsWith("EnemyColor")) {
if (colors.countColors() != 2) {
return false;
}
@@ -71,44 +67,36 @@ public class ForgeScript {
}
}
return false;
} else if (property.contains("AllColors")) {
if (property.endsWith("Source") && isColorlessSource)
return false;
return property.startsWith("non") != colors.isAllColors();
} else if (property.contains("MonoColor")) { // ... Card is monocolored
if (property.endsWith("Source") && isColorlessSource)
return false;
return property.startsWith("non") != colors.isMonoColor();
} else if (property.startsWith("AllColors")) {
return colors.isAllColors();
} else if (property.startsWith("MonoColor")) {
return colors.isMonoColor();
} else if (property.startsWith("ChosenColor")) {
if (property.endsWith("Source") && isColorlessSource)
return false;
return source.hasChosenColor() && colors.hasAnyColor(MagicColor.fromName(source.getChosenColor()));
} else if (property.startsWith("AnyChosenColor")) {
if (property.endsWith("Source") && isColorlessSource)
return false;
return source.hasChosenColor()
&& colors.hasAnyColor(ColorSet.fromNames(source.getChosenColors()).getColor());
} else if (property.equals("AssociatedWithChosenColor")) {
final String color = source.getChosenColor();
switch (color) {
case "white":
return cardState.getTypeWithChanges().getLandTypes().contains("Plains");
return type.hasSubtype("Plains");
case "blue":
return cardState.getTypeWithChanges().getLandTypes().contains("Island");
return type.hasSubtype("Island");
case "black":
return cardState.getTypeWithChanges().getLandTypes().contains("Swamp");
return type.hasSubtype("Swamp");
case "red":
return cardState.getTypeWithChanges().getLandTypes().contains("Mountain");
return type.hasSubtype("Mountain");
case "green":
return cardState.getTypeWithChanges().getLandTypes().contains("Forest");
return type.hasSubtype("Forest");
default:
return false;
}
} else if (property.equals("Outlaw")) {
return cardState.getTypeWithChanges().isOutlaw();
return type.isOutlaw();
} else if (property.startsWith("non")) {
// ... Other Card types
return !cardState.getTypeWithChanges().hasStringType(property.substring(3));
return !type.hasStringType(property.substring(3));
} else if (property.equals("CostsPhyrexianMana")) {
return cardState.getManaCost().hasPhyrexian();
} else if (property.startsWith("HasSVar")) {
@@ -117,19 +105,19 @@ public class ForgeScript {
} else if (property.equals("ChosenType")) {
String chosenType = source.getChosenType();
if (chosenType.startsWith("Non")) {
return !cardState.getTypeWithChanges().hasStringType(StringUtils.capitalize(chosenType.substring(3)));
return !type.hasStringType(StringUtils.capitalize(chosenType.substring(3)));
}
return cardState.getTypeWithChanges().hasStringType(chosenType);
return type.hasStringType(chosenType);
} else if (property.equals("IsNotChosenType")) {
return !cardState.getTypeWithChanges().hasStringType(source.getChosenType());
return !type.hasStringType(source.getChosenType());
} else if (property.equals("ChosenType2")) {
return cardState.getTypeWithChanges().hasStringType(source.getChosenType2());
return type.hasStringType(source.getChosenType2());
} else if (property.equals("IsNotChosenType2")) {
return !cardState.getTypeWithChanges().hasStringType(source.getChosenType2());
return !type.hasStringType(source.getChosenType2());
} else if (property.equals("NotedType")) {
boolean found = false;
for (String s : source.getNotedTypes()) {
if (cardState.getTypeWithChanges().hasStringType(s)) {
if (type.hasStringType(s)) {
found = true;
break;
}
@@ -185,7 +173,7 @@ public class ForgeScript {
int x = AbilityUtils.calculateAmount(source, rhs, spellAbility);
return Expressions.compare(y, property, x);
} else return cardState.getTypeWithChanges().hasStringType(property);
} else return type.hasStringType(property);
}
public static boolean spellAbilityHasProperty(SpellAbility sa, String property, Player sourceController,
@@ -241,6 +229,10 @@ public class ForgeScript {
return sa.isMorphUp();
} else if (property.equals("ManifestUp")) {
return sa.isManifestUp();
} else if (property.equals("Unlock")) {
return sa.isUnlock();
} else if (property.equals("isTurnFaceUp")) {
return sa.isTurnFaceUp();
} else if (property.equals("isCastFaceDown")) {
return sa.isCastFaceDown();
} else if (property.equals("Modular")) {

View File

@@ -170,7 +170,6 @@ public class GameAction {
Card copied = null;
Card lastKnownInfo = null;
Card commanderEffect = null; // The effect card of commander replacement effect
// get the LKI from above like ChangeZoneEffect
if (params != null && params.containsKey(AbilityKey.CardLKI)) {
@@ -320,23 +319,8 @@ public class GameAction {
// Temporary disable commander replacement effect
// 903.9a
if (fromBattlefield && !toBattlefield && c.isCommander() && c.hasMergedCard()) {
// Find the commander replacement effect "card"
CardCollectionView comCards = c.getOwner().getCardsIn(ZoneType.Command);
for (final Card effCard : comCards) {
for (final ReplacementEffect re : effCard.getReplacementEffects()) {
if (re.hasParam("CommanderMoveReplacement") && c.getMergedCards().contains(effCard.getEffectSource())) {
commanderEffect = effCard;
break;
}
}
if (commanderEffect != null) break;
}
// Disable the commander replacement effect
if (commanderEffect != null) {
for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) {
re.setSuppressed(true);
}
}
c.getOwner().setCommanderReplacementSuppressed(true);
}
// in addition to actual tokens, cards "made" by digital-only mechanics
@@ -502,17 +486,17 @@ public class GameAction {
if (c.getCastSA() != null && !c.getCastSA().isIntrinsic() && c.getCastSA().getKeyword() != null) {
KeywordInterface ki = c.getCastSA().getKeyword();
ki.setHostCard(copied);
copied.addChangedCardKeywordsInternal(ImmutableList.of(ki), null, false, copied.getGameTimestamp(), 0, true);
copied.addChangedCardKeywordsInternal(ImmutableList.of(ki), null, false, copied.getGameTimestamp(), null, true);
}
// TODO hot fix for non-intrinsic offspring
Multimap<Long, KeywordInterface> addKw = MultimapBuilder.hashKeys().arrayListValues().build();
Multimap<StaticAbility, KeywordInterface> addKw = MultimapBuilder.hashKeys().arrayListValues().build();
for (KeywordInterface kw : c.getKeywords(Keyword.OFFSPRING)) {
if (!kw.isIntrinsic()) {
addKw.put(kw.getStaticId(), kw);
addKw.put(kw.getStatic(), kw);
}
}
if (!addKw.isEmpty()) {
for (Map.Entry<Long, Collection<KeywordInterface>> e : addKw.asMap().entrySet()) {
for (Map.Entry<StaticAbility, Collection<KeywordInterface>> e : addKw.asMap().entrySet()) {
copied.addChangedCardKeywordsInternal(e.getValue(), null, false, copied.getGameTimestamp(), e.getKey(), true);
}
}
@@ -530,11 +514,7 @@ public class GameAction {
// Move components of merged permanent here
// Also handle 723.3e and 903.9a
boolean wasToken = c.isToken();
if (commanderEffect != null) {
for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) {
re.setSuppressed(false);
}
}
c.getOwner().setCommanderReplacementSuppressed(false);
// Change zone of original card so components isToken() and isCommander() return correct value
// when running replacement effects here
c.setZone(zoneTo);
@@ -622,7 +602,7 @@ public class GameAction {
// 400.7g try adding keyword back into card if it doesn't already have it
if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) {
if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) {
copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), 0, true);
copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), null, true);
}
}
@@ -713,7 +693,6 @@ public class GameAction {
if (wasFacedown) {
Card revealLKI = CardCopyService.getLKICopy(c);
revealLKI.forceTurnFaceUp();
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the battlefield: ");
copied.setState(CardStateName.Original, true);
@@ -1400,10 +1379,10 @@ public class GameAction {
|| game.getRules().hasAppliedVariant(GameType.Brawl)
|| game.getRules().hasAppliedVariant(GameType.Planeswalker)) && !checkAgain) {
for (final Card c : p.getCardsIn(ZoneType.Graveyard).threadSafeIterable()) {
checkAgain |= stateBasedAction903_9a(c);
checkAgain |= stateBasedAction_Commander(c, mapParams);
}
for (final Card c : p.getCardsIn(ZoneType.Exile).threadSafeIterable()) {
checkAgain |= stateBasedAction903_9a(c);
checkAgain |= stateBasedAction_Commander(c, mapParams);
}
}
@@ -1508,6 +1487,7 @@ public class GameAction {
if (!c.isSaga()) {
return false;
}
// needs to be effect, because otherwise it might be a cost?
if (!c.canBeSacrificedBy(null, true)) {
return false;
}
@@ -1515,7 +1495,6 @@ public class GameAction {
return false;
}
if (!game.getStack().hasSourceOnStack(c, SpellAbilityPredicates.isChapter())) {
// needs to be effect, because otherwise it might be a cost?
sacrificeList.add(c);
checkAgain = true;
}
@@ -1538,6 +1517,7 @@ public class GameAction {
}
return checkAgain;
}
private boolean stateBasedAction_Role(Card c, CardCollection removeList) {
if (!c.hasCardAttachments()) {
return false;
@@ -1553,7 +1533,6 @@ public class GameAction {
if (rolesByPlayer.size() <= 1) {
continue;
}
// sort by game timestamp
rolesByPlayer.sort(CardPredicates.compareByGameTimestamp());
removeList.addAll(rolesByPlayer.subList(0, rolesByPlayer.size() - 1));
checkAgain = true;
@@ -1634,14 +1613,15 @@ public class GameAction {
return checkAgain;
}
private boolean stateBasedAction903_9a(Card c) {
private boolean stateBasedAction_Commander(Card c, Map<AbilityKey, Object> mapParams) {
// CR 903.9a
if (c.isRealCommander() && c.canMoveToCommandZone()) {
// FIXME: need to flush the tracker to make sure the Commander is properly updated
c.getGame().getTracker().flush();
c.setMoveToCommandZone(false);
if (c.getOwner().getController().confirmAction(c.getFirstSpellAbility(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.", null)) {
moveTo(c.getOwner().getZone(ZoneType.Command), c, null);
moveTo(c.getOwner().getZone(ZoneType.Command), c, null, mapParams);
return true;
}
}
@@ -1781,7 +1761,6 @@ public class GameAction {
}
private boolean handlePlaneswalkerRule(Player p, CardCollection noRegCreats) {
// get all Planeswalkers
final List<Card> list = p.getPlaneswalkersInPlay();
boolean recheck = false;
@@ -2585,7 +2564,6 @@ public class GameAction {
player.addCompletedDungeon(dungeon);
ceaseToExist(dungeon, true);
// Run RoomEntered trigger
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(dungeon);
runParams.put(AbilityKey.Player, player);
game.getTriggerHandler().runTrigger(TriggerType.DungeonCompleted, runParams, false);

View File

@@ -339,7 +339,7 @@ public final class GameActionUtil {
newSA.setMayPlay(o);
final StringBuilder sb = new StringBuilder(sa.getDescription());
if (!source.equals(host)) {
if (!source.equals(host) && host.getCardForUi() != null) {
sb.append(" by ");
if (host.isImmutable() && host.getEffectSource() != null) {
sb.append(host.getEffectSource());
@@ -620,7 +620,10 @@ public final class GameActionUtil {
result.getPayCosts().add(cost);
reset = true;
}
result.setOptionalKeywordAmount(ki, v);
if (result != null) {
result.setOptionalKeywordAmount(ki, v);
}
} else if (o.startsWith("Offspring")) {
String[] k = o.split(":");
final Cost cost = new Cost(k[1], false);
@@ -839,6 +842,10 @@ public final class GameActionUtil {
CardCollection subList = new CardCollection();
for (Card c : list) {
Player decider = dest == ZoneType.Battlefield ? c.getController() : c.getOwner();
if (sa != null && sa.hasParam("GainControl")) {
// TODO this doesn't account for changes from e.g. Gather Specimens yet
decider = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("GainControl"), sa).get(0);
}
if (decider.equals(p)) {
subList.add(c);
}

View File

@@ -95,6 +95,10 @@ public class GameRules {
this.appliedVariants.addAll(appliedVariants);
}
public void addAppliedVariant(final GameType variant) {
this.appliedVariants.add(variant);
}
public boolean hasAppliedVariant(final GameType variant) {
return appliedVariants.contains(variant);
}

View File

@@ -80,25 +80,7 @@ public class GameSnapshot {
for (Player p : fromGame.getPlayers()) {
Player toPlayer = findBy(toGame, p);
List<Card> commanders = Lists.newArrayList();
// Commander cast times are stored in the player, not the card
toPlayer.resetCommanderStats();
for (final Card c : p.getCommanders()) {
Card newCommander = findBy(toGame, c);
commanders.add(newCommander);
int castTimes = p.getCommanderCast(c);
for (int i = 0; i < castTimes; i++) {
toPlayer.incCommanderCast(newCommander);
}
}
for (Map.Entry<Card, Integer> entry : p.getCommanderDamage()) {
Card commander = findBy(toGame, entry.getKey());
int damage = entry.getValue();
toPlayer.addCommanderDamage(commander, damage);
}
toPlayer.setCommanders(commanders);
p.copyCommandersToSnapshot(toPlayer, c -> findBy(toGame, c));
((PlayerZoneBattlefield) toPlayer.getZone(ZoneType.Battlefield)).setTriggers(true);
}
toGame.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
@@ -204,8 +186,6 @@ public class GameSnapshot {
// Copy mana pool
copyManaPool(origPlayer, newPlayer);
newPlayer.setCommanders(origPlayer.getCommanders()); // will be fixed up below
}
private void copyManaPool(Player fromPlayer, Player toPlayer) {

View File

@@ -30,16 +30,7 @@ public class AbilityApiBased extends AbilityActivated {
@Override
public String getStackDescription() {
StringBuilder sb = new StringBuilder();
if (this.hostCard.hasPromisedGift() && this.isSpell() && !this.hostCard.isPermanent()) {
sb.append("Gift a ").
append(this.getAdditionalAbility("GiftAbility").getParam("GiftDescription")).
append(" to ").append(this.hostCard.getPromisedGift()).
append(". ");
}
sb.append(effect.getStackDescriptionWithSubs(mapParams, this));
return sb.toString();
return effect.getStackDescriptionWithSubs(mapParams, this);
}
/* (non-Javadoc)

View File

@@ -88,6 +88,7 @@ public enum AbilityKey {
LastStateGraveyard("LastStateGraveyard"),
LifeAmount("LifeAmount"), //TODO confirm that this and LifeGained can be merged
LifeGained("LifeGained"),
LoseReason("LoseReason"),
Map("Map"),
Mana("Mana"),
MergedCards("MergedCards"),

View File

@@ -1,6 +1,8 @@
package forge.game.ability;
import com.google.common.collect.*;
import com.google.common.math.IntMath;
import forge.card.CardStateName;
import forge.card.CardType;
import forge.card.ColorSet;
@@ -1541,6 +1543,11 @@ public class AbilityUtils {
host.clearRemembered();
}
host.addRemembered(sa.getTargets());
if (sa.hasParam("IncludeAllComponentCards")) {
for (Card c : sa.getTargets().getTargetCards()) {
host.addRemembered(c.getAllComponentCards(false));
}
}
}
if (sa.hasParam("RememberCostMana")) {
@@ -1626,6 +1633,14 @@ public class AbilityUtils {
return doXMath(calculateAmount(c, sq[v ? 1 : 2], ctb), expr, c, ctb);
}
// Count$IsPrime <SVar>.<True>.<False>
if (sq[0].startsWith("IsPrime")) {
final String[] compString = sq[0].split(" ");
final int lhs = calculateAmount(c, compString[1], ctb);
boolean v = IntMath.isPrime(lhs);
return doXMath(calculateAmount(c, sq[v ? 1 : 2], ctb), expr, c, ctb);
}
if (ctb instanceof SpellAbility) {
final SpellAbility sa = (SpellAbility) ctb;

View File

@@ -60,6 +60,11 @@ public abstract class SpellAbilityEffect {
// prelude for when this is root ability
if (!(sa instanceof AbilitySub)) {
sb.append(sa.getHostCard()).append(" -");
if (sa.getHostCard().hasPromisedGift()) {
sb.append(" Gift ").
append(sa.getAdditionalAbility("GiftAbility").getParam("GiftDescription")).
append(" to ").append(sa.getHostCard().getPromisedGift()).append(". ");
}
}
sb.append(" ");
}
@@ -78,8 +83,7 @@ public abstract class SpellAbilityEffect {
if (params.containsKey("SpellDescription")) {
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replaceAll(",,,,,,", " ");
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replaceAll(",,,", " ");
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc,
sa.getHostCard().getName());
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc, sa.getHostCard());
//trim reminder text from StackDesc
int idxL = spellDesc.indexOf(" (");
@@ -109,7 +113,7 @@ public abstract class SpellAbilityEffect {
} else {
final String condDesc = sa.getParam("ConditionDescription");
final String afterDesc = sa.getParam("AfterDescription");
final String baseDesc = CardTranslation.translateSingleDescriptionText(this.getStackDescription(sa), sa.getHostCard().getName());
final String baseDesc = CardTranslation.translateSingleDescriptionText(this.getStackDescription(sa), sa.getHostCard());
if (condDesc != null) {
sb.append(condDesc).append(" ");
}
@@ -242,6 +246,15 @@ public abstract class SpellAbilityEffect {
}
}
}
if (resultUnique == null)
return null;
if (sa.hasParam("IncludeAllComponentCards")) {
CardCollection components = new CardCollection();
for (Card c : resultUnique) {
components.addAll(c.getAllComponentCards(false));
}
resultUnique.addAll(components);
}
return resultUnique;
}
@@ -446,6 +459,11 @@ public abstract class SpellAbilityEffect {
card.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "AtEOT"), card.getGame().getNextTimestamp(), 0);
}
protected static SpellAbility getExileSpellAbility(final Card card) {
String effect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
return AbilityFactory.getAbility(effect, card);
}
protected static SpellAbility getForgetSpellAbility(final Card card) {
String forgetEffect = "DB$ Pump | ForgetObjects$ TriggeredCard";
String exileEffect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile"
@@ -459,6 +477,7 @@ public abstract class SpellAbilityEffect {
public static void addForgetOnMovedTrigger(final Card card, final String zone) {
String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ " + zone + " | ExcludedDestinations$ Stack,Exile | Destination$ Any | TriggerZones$ Command | Static$ True";
// CR 400.8 Exiled card becomes new object when it's exiled
String trig2 = "Mode$ Exiled | ValidCard$ Card.IsRemembered | ValidCause$ SpellAbility.!EffectSource | TriggerZones$ Command | Static$ True";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
@@ -480,17 +499,15 @@ public abstract class SpellAbilityEffect {
protected static void addExileOnMovedTrigger(final Card card, final String zone) {
String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ " + zone + " | Destination$ Any | TriggerZones$ Command | Static$ True";
String effect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
parsedTrigger.setOverridingAbility(getExileSpellAbility(card));
card.addTrigger(parsedTrigger);
}
protected static void addExileOnCounteredTrigger(final Card card) {
String trig = "Mode$ Countered | ValidCard$ Card.IsRemembered | TriggerZones$ Command | Static$ True";
String effect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
parsedTrigger.setOverridingAbility(getExileSpellAbility(card));
card.addTrigger(parsedTrigger);
}
@@ -502,6 +519,13 @@ public abstract class SpellAbilityEffect {
card.addTrigger(parsedTrigger);
}
protected static void addExileCounterTrigger(final Card card, final String counterType) {
String trig = "Mode$ CounterRemoved | TriggerZones$ Command | ValidCard$ Card.EffectSource | CounterType$ " + counterType + " | NewCounterAmount$ 0 | Static$ True";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(getExileSpellAbility(card));
card.addTrigger(parsedTrigger);
}
protected static void addForgetCounterTrigger(final Card card, final String counterType) {
String trig = "Mode$ CounterRemoved | TriggerZones$ Command | ValidCard$ Card.IsRemembered | CounterType$ " + counterType + " | NewCounterAmount$ 0 | Static$ True";
String trig2 = "Mode$ PhaseOut | TriggerZones$ Command | ValidCard$ Card.phasedOutIsRemembered | Static$ True";
@@ -518,9 +542,8 @@ public abstract class SpellAbilityEffect {
protected static void addExileOnLostTrigger(final Card card) {
String trig = "Mode$ LosesGame | ValidPlayer$ You | TriggerController$ Player | TriggerZones$ Command | Static$ True";
String effect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
parsedTrigger.setOverridingAbility(getExileSpellAbility(card));
card.addTrigger(parsedTrigger);
}
@@ -860,6 +883,8 @@ public abstract class SpellAbilityEffect {
} else {
game.getUpkeep().addUntilEnd(controller, until);
}
} else if ("UntilNextEndStep".equals(duration)) {
game.getEndOfTurn().addAt(until);
} else if ("UntilYourNextEndStep".equals(duration)) {
game.getEndOfTurn().addUntil(controller, until);
} else if ("UntilYourNextTurn".equals(duration)) {
@@ -895,6 +920,9 @@ public abstract class SpellAbilityEffect {
} else if ("UntilHostLeavesPlayOrEOT".equals(duration)) {
host.addLeavesPlayCommand(until);
game.getEndOfTurn().addUntil(until);
} else if ("UntilHostLeavesPlayOrEndOfCombat".equals(duration)) {
host.addLeavesPlayCommand(until);
game.getEndOfCombat().addUntil(until);
} else if ("UntilLoseControlOfHost".equals(duration)) {
host.addLeavesPlayCommand(until);
host.addChangeControllerCommand(until);

View File

@@ -2,7 +2,6 @@ package forge.game.ability.effects;
import java.util.Map;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -29,22 +28,16 @@ public class AbandonEffect extends SpellAbilityEffect {
return;
}
final Game game = controller.getGame();
if (sa.hasParam("RememberAbandoned")) {
source.addRemembered(source);
}
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
controller.getZone(ZoneType.Command).remove(source);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
controller.getZone(ZoneType.SchemeDeck).add(source);
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Scheme, source);
game.getTriggerHandler().runTrigger(TriggerType.Abandoned, runParams, false);
controller.getGame().getTriggerHandler().runTrigger(TriggerType.Abandoned, runParams, false);
}
}

View File

@@ -2,10 +2,13 @@ package forge.game.ability.effects;
import java.util.Map;
import forge.game.GameLogEntryType;
import forge.game.GameType;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.util.Lang;
@@ -63,6 +66,26 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
c.getGame().getTriggerHandler().runTrigger(TriggerType.BecomesSaddled, runParams, false);
}
break;
case "Commander":
//This implementation doesn't let a card make someone else's creature your commander. But that's an edge case among edge cases.
Player p = c.getOwner();
if (c.isCommander() == activate || p.getCommanders().contains(c) == activate)
break; //Isn't changing status.
if (activate) {
if(!c.getGame().getRules().hasCommander()) {
System.out.println("Commander status applied in non-commander format. Applying Commander variant.");
c.getGame().getRules().addAppliedVariant(GameType.Commander);
}
p.addCommander(c);
//Seems important enough to mention in the game log.
c.getGame().getGameLog().add(GameLogEntryType.STACK_RESOLVE, String.format("%s is now %s's commander.", c.getPaperCard().getName(), p));
}
else {
p.removeCommander(c);
c.getGame().getGameLog().add(GameLogEntryType.STACK_RESOLVE, String.format("%s is no longer %s's commander.", c.getPaperCard().getName(), p));
}
altered = true;
break;
// Other attributes: renown, monstrous, suspected, etc

View File

@@ -28,7 +28,6 @@ public class AnimateAllEffect extends AnimateEffectBase {
public void resolve(final SpellAbility sa) {
final Card host = sa.getHostCard();
// AF specific sa
Integer power = null;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(host, sa.getParam("Power"), sa);

View File

@@ -126,7 +126,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
params.put("Category", "Keywords");
c.addPerpetual(params);
}
c.addChangedCardKeywords(keywords, removeKeywords, removeAll, timestamp, 0);
c.addChangedCardKeywords(keywords, removeKeywords, removeAll, timestamp, null);
}
// do this after changing types in case it wasn't a creature before

View File

@@ -461,8 +461,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
origin.addAll(ZoneType.listValueOf(sa.getParam("Origin")));
}
int libraryPosition = sa.hasParam("LibraryPosition") ?
AbilityUtils.calculateAmount(hostCard, sa.getParam("LibraryPosition"), sa) : 0;
int libraryPosition = sa.hasParam("LibraryPosition") ? AbilityUtils.calculateAmount(hostCard, sa.getParam("LibraryPosition"), sa) : 0;
if (sa.hasParam("DestinationAlternative")) {
Pair<ZoneType, Integer> pair = handleAltDest(sa, hostCard, destination, libraryPosition, activator);
destination = pair.getKey();
@@ -503,6 +502,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
final boolean shuffle = sa.hasParam("Shuffle") && "True".equals(sa.getParam("Shuffle"));
boolean combatChanged = false;
if (sa.hasParam("ShuffleNonMandatory") &&
!activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantShuffleTheLibrary"), null)) {
return;
}
Player chooser = activator;
if (sa.hasParam("Chooser")) {
chooser = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("Chooser"), sa).get(0);
@@ -535,8 +539,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
final String prompt = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblDoYouWantMoveTargetFromOriToDest", CardTranslation.getTranslatedName(gameCard.getName()), Lang.joinHomogenous(origin, ZoneType::getTranslatedName), destination.getTranslatedName()));
if (optional && !chooser.getController().confirmAction(sa, null, prompt, null))
if (optional && !chooser.getController().confirmAction(sa, null, prompt, null)) {
continue;
}
final Zone originZone = game.getZoneOf(gameCard);
@@ -670,7 +675,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (sa.hasParam("Unearth") && movedCard.isInPlay()) {
movedCard.setUnearthed(true);
movedCard.addChangedCardKeywords(Lists.newArrayList("Haste"), null, false,
game.getNextTimestamp(), 0, true);
game.getNextTimestamp(), null, true);
registerDelayedTrigger(sa, "Exile", Lists.newArrayList(movedCard));
addLeaveBattlefieldReplacement(movedCard, sa, "Exile");
}
@@ -1045,28 +1050,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
player.addController(controlTimestamp, searchControlPlayer.getValue());
}
decider.incLibrarySearched();
// should only count the number of searching player's own library
// Panglacial Wurm
CardCollection canCastWhileSearching = CardLists.getKeyword(fetchList,
"While you're searching your library, you may cast CARDNAME from your library.");
decider.getController().tempShowCards(canCastWhileSearching);
for (final Card tgtCard : canCastWhileSearching) {
List<SpellAbility> sas = AbilityUtils.getSpellsFromPlayEffect(tgtCard, decider, CardStateName.Original, true);
if (sas.isEmpty()) {
continue;
}
SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas);
if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) {
continue;
}
// if played, that card cannot be found
if (decider.getController().playSaFromPlayEffect(tgtSA)) {
fetchList.remove(tgtCard);
}
//some kind of reset here?
}
decider.getController().endTempShowCards();
decider.incLibrarySearched();
handleCastWhileSearching(fetchList, decider);
}
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(decider);
runParams.put(AbilityKey.Target, Lists.newArrayList(player));
@@ -1506,6 +1493,29 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
}
private void handleCastWhileSearching(final CardCollection fetchList, final Player decider) {
// Panglacial Wurm
CardCollection canCastWhileSearching = CardLists.getKeyword(fetchList,
"While you're searching your library, you may cast CARDNAME from your library.");
decider.getController().tempShowCards(canCastWhileSearching);
for (final Card tgtCard : canCastWhileSearching) {
List<SpellAbility> sas = AbilityUtils.getSpellsFromPlayEffect(tgtCard, decider, CardStateName.Original, true);
if (sas.isEmpty()) {
continue;
}
SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas);
if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) {
continue;
}
// if played, that card cannot be found
if (decider.getController().playSaFromPlayEffect(tgtSA)) {
fetchList.remove(tgtCard);
}
//some kind of reset here?
}
decider.getController().endTempShowCards();
}
private static class HiddenOriginChoices {
boolean shuffleMandatory;
boolean searchedLibrary;

View File

@@ -151,7 +151,7 @@ public class CloneEffect extends SpellAbilityEffect {
}
if (!pumpKeywords.isEmpty()) {
tgtCard.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, ts, 0);
tgtCard.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, ts, null);
TokenEffectBase.addPumpUntil(sa, tgtCard, ts);
}

View File

@@ -174,7 +174,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
}
if (keywords != null) {
tgtC.addChangedCardKeywords(keywords, Lists.newArrayList(), false, tStamp, 0);
tgtC.addChangedCardKeywords(keywords, Lists.newArrayList(), false, tStamp, null);
game.fireEvent(new GameEventCardStatsChanged(tgtC));
}

View File

@@ -204,7 +204,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
Player chooser = activator;
if (sa.hasParam("Chooser")) {
final String choose = sa.getParam("Chooser");
chooser = AbilityUtils.getDefinedPlayers(sa.getHostCard(), choose, sa).get(0);
chooser = AbilityUtils.getDefinedPlayers(host, choose, sa).get(0);
}
// For Mimic Vat with mutated creature, need to choose one imprinted card
@@ -268,7 +268,6 @@ public class CopyPermanentEffect extends TokenEffectBase {
if (!useZoneTable) {
triggerList.triggerChangesZoneAll(game, sa);
triggerList.clear();
}
if (combatChanged.isTrue()) {
game.updateCombatForView();

View File

@@ -86,13 +86,12 @@ public class CountersPutEffect extends SpellAbilityEffect {
// skip the StringBuilder if no targets are chosen ("up to" scenario)
if (sa.usesTargeting()) {
final List<Card> targetCards = getTargetCards(sa);
if (targetCards.size() == 0) {
if (targetCards.isEmpty()) {
return stringBuilder.toString();
}
}
final String key = forEach ? "ForEachNum" : "CounterNum";
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault(key, "1"), sa);
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("CounterNum", "1"), sa);
if (sa.hasParam("Bolster")) {
stringBuilder.append("bolsters ").append(amount).append(".");
@@ -303,7 +302,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
for (int i = 0; i < num; i++) {
CounterType ct = chooseTypeFromList(sa, options, obj, pc);
typesToAdd.add(ct);
options = options.replace(ct.getName(),"");
options = options.replace(ct.getName(), "");
}
for (CounterType ct : typesToAdd) {
if (obj instanceof Player) {
@@ -333,7 +332,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
CardCollectionView counterCards =
CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield),
type.split("_")[1], activator, card, sa);
List <CounterType> counterTypes = Lists.newArrayList();
List<CounterType> counterTypes = Lists.newArrayList();
for (Card c : counterCards) {
for (final Map.Entry<CounterType, Integer> map : c.getCounters().entrySet()) {
if (!counterTypes.contains(map.getKey())) {
@@ -352,7 +351,23 @@ public class CountersPutEffect extends SpellAbilityEffect {
typesToAdd.add(CounterType.getType(type));
}
}
int remaining = counterAmount;
for (CounterType ct : typesToAdd) {
if (sa.hasParam("SplitAmount")) {
if (typesToAdd.size() - typesToAdd.indexOf(ct) > 1) {
Map<String, Object> params = Maps.newHashMap();
params.put("Target", obj);
params.put("CounterType", counterType);
counterAmount = pc.chooseNumber(sa, ct.toString() + ": " +
Localizer.getInstance().getMessage("lblHowManyCounters"), 0, remaining, params);
if (counterAmount == 0) {
continue;
}
remaining -= counterAmount;
} else {
counterAmount = remaining;
}
}
if (obj instanceof Player) {
((Player) obj).addCounter(ct, counterAmount, placer, table);
}
@@ -478,11 +493,9 @@ public class CountersPutEffect extends SpellAbilityEffect {
}
// Adapt need extra logic
if (sa.hasParam("Adapt")) {
if (!(gameCard.getCounters(CounterEnumType.P1P1) == 0
|| StaticAbilityAdapt.anyWithAdapt(sa, gameCard))) {
continue;
}
if (sa.hasParam("Adapt") &&
!(gameCard.getCounters(CounterEnumType.P1P1) == 0 || StaticAbilityAdapt.anyWithAdapt(sa, gameCard))) {
continue;
}
if (sa.isKeyword(Keyword.TRIBUTE)) {

View File

@@ -56,8 +56,7 @@ public class DamageAllEffect extends DamageBaseEffect {
final Card sourceLKI = card.getGame().getChangeZoneLKIInfo(card);
final Game game = sa.getActivatingPlayer().getGame();
final String damage = sa.getParam("NumDmg");
final int dmg = AbilityUtils.calculateAmount(source, damage, sa);
final int dmg = AbilityUtils.calculateAmount(source, sa.getParam("NumDmg"), sa);
//Remember params from this effect have been moved to dealDamage in GameAction
Player targetPlayer = sa.getTargets().getFirstTargetedPlayer();

View File

@@ -150,7 +150,7 @@ public class DebuffEffect extends SpellAbilityEffect {
}
removedKW.addAll(kws);
tgtC.addChangedCardKeywords(addedKW, removedKW, false, timestamp, 0);
tgtC.addChangedCardKeywords(addedKW, removedKW, false, timestamp, null);
if (!"Permanent".equals(sa.getParam("Duration"))) {
final GameCommand until = new GameCommand() {

View File

@@ -1,7 +1,9 @@
package forge.game.ability.effects;
import forge.card.GamePieceType;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.player.Player;
//Class for an effect that acts as its own card instead of being attached to a card
//Example: Commander Effect
@@ -19,6 +21,29 @@ public class DetachedCardEffect extends Card {
setEffectSource(card0);
}
public DetachedCardEffect(Player owner, String name) {
super(owner.getGame().nextCardId(), null, owner.getGame());
this.card = null;
this.setName(name);
this.setOwner(owner);
this.setGamePieceType(GamePieceType.EFFECT);
}
public DetachedCardEffect(DetachedCardEffect from, boolean assignNewId) {
this(from, from.getGame(), assignNewId);
}
public DetachedCardEffect(DetachedCardEffect from, Game game, boolean assignNewId) {
super(assignNewId ? game.nextCardId() : from.id, from.getPaperCard(), game);
this.setName(from.getName());
this.setGamePieceType(GamePieceType.EFFECT);
if(from.getGame() == game) {
this.setOwner(from.getOwner());
this.setEffectSource(from.getEffectSource());
}
}
@Override
public Card getCardForUi() {
return card; //use linked card for the sake of UI display logic

View File

@@ -251,6 +251,10 @@ public class EffectEffect extends SpellAbilityEffect {
addExileOnLostTrigger(eff);
}
if (sa.hasParam("ExileOnCounter")) {
addExileCounterTrigger(eff, sa.getParam("ExileOnCounter"));
}
// Set Imprinted
if (effectImprinted != null) {
eff.addImprintedCards(AbilityUtils.getDefinedCards(hostCard, effectImprinted, sa));

View File

@@ -3,7 +3,6 @@ package forge.game.ability.effects;
import java.util.List;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -28,10 +27,8 @@ public class GameLossEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
for (final Player p : getTargetPlayers(sa)) {
p.loseConditionMet(GameLossReason.SpellEffect, card.getName());
p.loseConditionMet(GameLossReason.SpellEffect, sa.getHostCard().getName());
}
}

View File

@@ -14,7 +14,7 @@ public class HauntEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
Card host = sa.getHostCard();
if (host.isPermanent()) {
if (host.isPermanent() && sa.hasTriggeringObject(AbilityKey.NewCard)) {
// get new version instead of battlefield lki
host = (Card) sa.getTriggeringObject(AbilityKey.NewCard);
}

View File

@@ -227,7 +227,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
}
}
private List<ICardFace> parseFaces (final SpellAbility sa, final String param) {
private List<ICardFace> parseFaces(final SpellAbility sa, final String param) {
List<ICardFace> parsedFaces = new ArrayList<>();
for (String s : sa.getParam(param).split(",")) {
// Cardnames that include "," must use ";" instead (i.e. Tovolar; Dire Overlord)
@@ -241,7 +241,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
return parsedFaces;
}
private Card finishMaking (final SpellAbility sa, final Card made, final Card source) {
private Card finishMaking(final SpellAbility sa, final Card made, final Card source) {
if (sa.hasParam("FaceDown")) made.turnFaceDown(true);
if (sa.hasParam("RememberMade")) source.addRemembered(made);
if (sa.hasParam("ImprintMade")) source.addImprintedCard(made);

View File

@@ -24,57 +24,74 @@ public abstract class ManifestBaseEffect extends SpellAbilityEffect {
final Game game = source.getGame();
// Usually a number leaving possibility for X, Sacrifice X land: Manifest X creatures.
final int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(source, sa.getParam("Amount"), sa) : 1;
final int times = sa.hasParam("Times") ? AbilityUtils.calculateAmount(source, sa.getParam("Times"), sa) : 1;
for (final Player p : getTargetPlayers(sa, "DefinedPlayer")) {
CardCollection tgtCards;
boolean fromLibrary = false;
if (sa.hasParam("Choices") || sa.hasParam("ChoiceZone")) {
ZoneType choiceZone = ZoneType.Hand;
if (sa.hasParam("ChoiceZone")) {
choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
fromLibrary = choiceZone.equals(ZoneType.Library);
}
CardCollectionView choices = p.getCardsIn(choiceZone);
if (sa.hasParam("Choices")) {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, source, sa);
}
if (choices.isEmpty()) {
continue;
}
for (int i = 0; i < times; i++) {
for (final Player p : getTargetPlayers(sa, "DefinedPlayer")) {
CardCollection tgtCards;
Card toGrave = null;
boolean fromLibrary = false;
if (sa.hasParam("Choices") || sa.hasParam("ChoiceZone")) {
ZoneType choiceZone = ZoneType.Hand;
if (sa.hasParam("ChoiceZone")) {
choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
fromLibrary = choiceZone.equals(ZoneType.Library);
}
CardCollectionView choices = p.getCardsIn(choiceZone);
if (sa.hasParam("Choices")) {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, source, sa);
}
if (choices.isEmpty()) {
continue;
}
String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : getDefaultMessage() + " ";
String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : getDefaultMessage() + " ";
tgtCards = new CardCollection(p.getController().chooseCardsForEffect(choices, sa, title, amount, amount, false, null));
} else if ("TopOfLibrary".equals(sa.getParamOrDefault("Defined", "TopOfLibrary"))) {
tgtCards = p.getTopXCardsFromLibrary(amount);
fromLibrary = true;
} else {
tgtCards = getTargetCards(sa);
if (tgtCards.allMatch(CardPredicates.inZone(ZoneType.Library))) {
tgtCards = new CardCollection(p.getController().chooseCardsForEffect(choices, sa, title, amount, amount, false, null));
} else if (sa.hasParam("Dread")) {
tgtCards = p.getTopXCardsFromLibrary(2);
if (!tgtCards.isEmpty()) {
Card manifest = p.getController().chooseSingleEntityForEffect(tgtCards, sa, getDefaultMessage(), null);
tgtCards.remove(manifest);
toGrave = tgtCards.isEmpty() ? null : tgtCards.getFirst();
tgtCards = new CardCollection(manifest);
}
fromLibrary = true;
} else if ("TopOfLibrary".equals(sa.getParamOrDefault("Defined", "TopOfLibrary"))) {
tgtCards = p.getTopXCardsFromLibrary(amount);
fromLibrary = true;
} else {
tgtCards = getTargetCards(sa);
if (tgtCards.allMatch(CardPredicates.inZone(ZoneType.Library))) {
fromLibrary = true;
}
}
}
if (sa.hasParam("Shuffle")) {
CardLists.shuffle(tgtCards);
}
if (sa.hasParam("Shuffle")) {
CardLists.shuffle(tgtCards);
}
if (fromLibrary) {
for (Card c : tgtCards) {
// CR 701.34d If an effect instructs a player to manifest multiple cards from their library, those cards are manifested one at a time.
if (fromLibrary) {
for (Card c : tgtCards) {
// CR 701.34d If an effect instructs a player to manifest multiple cards from their library, those cards are manifested one at a time.
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa);
internalEffect(c, p, sa, moveParams);
if (sa.hasParam("Dread") && toGrave != null) {
game.getAction().moveToGraveyard(toGrave, sa, moveParams);
toGrave = null;
}
triggerList.triggerChangesZoneAll(game, sa);
}
} else {
// manifest from other zones should be done at the same time
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa);
internalEffect(c, p, sa, moveParams);
for (Card c : tgtCards) {
internalEffect(c, p, sa, moveParams);
}
triggerList.triggerChangesZoneAll(game, sa);
}
} else {
// manifest from other zones should be done at the same time
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa);
for (Card c : tgtCards) {
internalEffect(c, p, sa, moveParams);
}
triggerList.triggerChangesZoneAll(game, sa);
}
}
}

View File

@@ -86,7 +86,7 @@ public class ProtectAllEffect extends SpellAbilityEffect {
CardCollectionView list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), valid, sa.getActivatingPlayer(), host, sa);
for (final Card tgtC : list) {
tgtC.addChangedCardKeywords(gainsKWList, null, false, timestamp, 0, true);
tgtC.addChangedCardKeywords(gainsKWList, null, false, timestamp, null, true);
if (!"Permanent".equals(sa.getParam("Duration"))) {
// If not Permanent, remove protection at EOT

View File

@@ -153,7 +153,7 @@ public class ProtectEffect extends SpellAbilityEffect {
continue;
}
tgtC.addChangedCardKeywords(gainsKWList, null, false, timestamp, 0, true);
tgtC.addChangedCardKeywords(gainsKWList, null, false, timestamp, null, true);
if (!"Permanent".equals(sa.getParam("Duration"))) {
// If not Permanent, remove protection at EOT

View File

@@ -68,7 +68,7 @@ public class PumpAllEffect extends SpellAbilityEffect {
params.put("Category", "Keywords");
tgtC.addPerpetual(params);
}
tgtC.addChangedCardKeywords(kws, null, false, timestamp, 0);
tgtC.addChangedCardKeywords(kws, null, false, timestamp, null);
}
if (redrawPT) {
tgtC.updatePowerToughnessForView();

View File

@@ -78,7 +78,7 @@ public class PumpEffect extends SpellAbilityEffect {
params.put("Category", "Keywords");
gameCard.addPerpetual(params);
}
gameCard.addChangedCardKeywords(kws, Lists.newArrayList(), false, timestamp, 0);
gameCard.addChangedCardKeywords(kws, Lists.newArrayList(), false, timestamp, null);
}
if (!hiddenKws.isEmpty()) {

View File

@@ -48,7 +48,7 @@ public class ReplaceEffect extends SpellAbilityEffect {
for (Player key : AbilityUtils.getDefinedPlayers(card, sa.getParam("VarKey"), sa)) {
m.put(key, m.getOrDefault(key, 0) + AbilityUtils.calculateAmount(card, varValue, sa));
}
} else {
} else if (varName != null) {
params.put(varName, AbilityUtils.calculateAmount(card, varValue, sa));
}

View File

@@ -96,6 +96,15 @@ public class SetStateEffect extends SpellAbilityEffect {
continue;
}
if (sa.hasParam("RevealFirst")) {
Card lki = CardCopyService.getLKICopy(tgtCard);
lki.forceTurnFaceUp();
game.getAction().reveal(new CardCollection(lki), lki.getOwner(), true, Localizer.getInstance().getMessage("lblRevealFaceDownCards"));
if (sa.hasParam("ValidNewFace") && !lki.isValid(sa.getParam("ValidNewFace").split(","), p, host, sa)) {
continue;
}
}
// facedown cards that are not Permanent, can't turn faceup there
if ("TurnFaceUp".equals(mode) && gameCard.isFaceDown() && gameCard.isInPlay()) {
if (gameCard.hasMergedCard()) {
@@ -118,7 +127,6 @@ public class SetStateEffect extends SpellAbilityEffect {
Card lki = CardCopyService.getLKICopy(gameCard);
lki.forceTurnFaceUp();
game.getAction().reveal(new CardCollection(lki), lki.getOwner(), true, Localizer.getInstance().getMessage("lblFaceDownCardCantTurnFaceUp"));
continue;
}
}

View File

@@ -71,7 +71,6 @@ public class SubgameEffect extends SpellAbilityEffect {
}
// Commander
List<Card> commanders = Lists.newArrayList();
final CardCollectionView commandCards = maingamePlayer.getCardsIn(ZoneType.Command);
for (final Card card : commandCards) {
if (card.isCommander()) {
@@ -85,15 +84,10 @@ public class SubgameEffect extends SpellAbilityEffect {
cmd.setChosenColors(chosenColors);
subgame.getAction().notifyOfValue(cmdColorsa, cmd, Localizer.getInstance().getMessage("lblPlayerPickedChosen", player.getName(), Lang.joinHomogenous(chosenColors)), player);
}
cmd.setCommander(true);
com.add(cmd);
commanders.add(cmd);
com.add(Player.createCommanderEffect(subgame, cmd));
player.addCommander(cmd);
}
}
if (!commanders.isEmpty()) {
player.setCommanders(commanders);
}
// Conspiracies
// 720.2 doesn't mention Conspiracy cards so I guess they don't move

View File

@@ -33,6 +33,9 @@ public class SurveilEffect extends SpellAbilityEffect {
if (sa.hasParam("Amount")) {
num = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa);
}
if (num == 0) {
return;
}
boolean isOptional = sa.hasParam("Optional");

View File

@@ -177,7 +177,7 @@ public abstract class TokenEffectBase extends SpellAbilityEffect {
}
if (!pumpKeywords.isEmpty()) {
moved.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, timestamp, 0);
moved.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, timestamp, null);
addPumpUntil(sa, moved, timestamp);
}

View File

@@ -75,7 +75,7 @@ import java.util.Map.Entry;
* @author Forge
* @version $Id$
*/
public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITranslatable {
private Game game;
private final IPaperCard paperCard;
@@ -805,9 +805,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Check replacement effects
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(ReplacementType.TurnFaceUp,
repParams, ReplacementLayer.CantHappen);
if (!list.isEmpty()) return false;
if (game.getReplacementHandler().cantHappenCheck(ReplacementType.TurnFaceUp, repParams)) return false;
CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this);
boolean retResult = false;
@@ -1321,6 +1319,26 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
}
/**
* Gives a collection of all cards that are melded, merged, or are otherwise representing
* a single permanent alongside this one.
* @param includeSelf Whether this card is included in the resulting CardCollection.
*/
public final CardCollection getAllComponentCards(boolean includeSelf) {
CardCollection out = new CardCollection();
if(includeSelf)
out.add(this);
if(this.getMeldedWith() != null)
out.add(this.getMeldedWith());
if(mergedTo != null) //Should be safe to recurse here so long as mergedTo remains a one-way relationship.
out.addAll(mergedTo.getAllComponentCards(true));
if(this.hasMergedCard())
out.addAll(mergedCards);
if(!includeSelf) //mergedCards includes self.
out.remove(this);
return out;
}
public final void moveMergedToSubgame(SpellAbility cause) {
if (hasMergedCard()) {
Zone zone = getZone();
@@ -1677,7 +1695,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
if (!Keyword.smartValueOf(counterType.toString().split(":")[0]).isMultipleRedundant()) {
num = getCounters(counterType);
}
addChangedCardKeywords(Collections.nCopies(num, counterType.toString()), null, false, timestamp, 0, updateView);
addChangedCardKeywords(Collections.nCopies(num, counterType.toString()), null, false, timestamp, null, updateView);
return true;
}
@@ -2226,7 +2244,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
sbLong.append("\r\n");
}
sb.append(sbLong);
return CardTranslation.translateMultipleDescriptionText(sb.toString(), getName());
return CardTranslation.translateMultipleDescriptionText(sb.toString(), this);
}
// convert a keyword list to the String that should be displayed in game
@@ -2410,9 +2428,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
String pip = mc.getFirstPhyrexianPip();
String[] parts = pip.substring(1, pip.length() - 1).split("/");
final StringBuilder rem = new StringBuilder();
rem.append(pip).append(" can be paid with {").append(parts[1]).append("}");
rem.append(pip).append(" can be paid with {").append(parts[0]).append("}");
if (parts.length > 2) {
rem.append(", {").append(parts[2]).append("},");
rem.append(", {").append(parts[1]).append("},");
}
rem.append(" or 2 life. ");
if (mc.getPhyrexianCount() > 1) {
@@ -2631,7 +2649,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
sbLong.append("\r\n");
}
sb.append(sbLong);
return CardTranslation.translateMultipleDescriptionText(sb.toString(), getName());
return CardTranslation.translateMultipleDescriptionText(sb.toString(), this);
}
private String kickerDesc(String keyword, String remText) {
@@ -3193,7 +3211,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
}
sb.append(CardTranslation.translateMultipleDescriptionText(sbBefore.toString(), state.getName()));
sb.append(CardTranslation.translateMultipleDescriptionText(sbBefore.toString(), state));
// add Spells there to main StringBuilder
sb.append(strSpell);
@@ -3222,7 +3240,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
}
sb.append(CardTranslation.translateMultipleDescriptionText(sbAfter.toString(), state.getName()));
sb.append(CardTranslation.translateMultipleDescriptionText(sbAfter.toString(), state));
return sb;
}
@@ -3550,8 +3568,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public final void setCopiedPermanent(final Card c) {
if (copiedPermanent == c) { return; }
copiedPermanent = c;
if(c != null)
if(c != null) {
currentState.setOracleText(c.getOracleText());
currentState.setFunctionalVariantName(c.getCurrentState().getFunctionalVariantName());
}
//Could fetch the card rules oracle text in an "else" clause here,
//but CardRules isn't aware of the card's state. May be better to
//just stash the original oracle text if this comes up.
@@ -4074,7 +4094,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return state.getType();
}
// TODO add changed type by card text
public Iterable<CardChangedType> getChangedCardTypes() {
// If there are no changed types, just return an empty immutable list, which actually
// produces a surprisingly large speedup by avoid lots of temp objects and making iteration
@@ -4586,19 +4605,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return StaticAbilityCombatDamageToughness.combatDamageToughness(this);
}
// How much combat damage does the card deal
public final StatBreakdown getNetCombatDamageBreakdown() {
if (hasKeyword("CARDNAME assigns no combat damage")) {
return new StatBreakdown();
}
if (toughnessAssignsDamage()) {
return getNetToughnessBreakdown();
}
return getNetPowerBreakdown();
public final boolean assignNoCombatDamage() {
return StaticAbilityAssignNoCombatDamage.assignNoCombatDamage(this);
}
// How much combat damage does the card deal
public final int getNetCombatDamage() {
return getNetCombatDamageBreakdown().getTotal();
return assignNoCombatDamage() ? 0 : (toughnessAssignsDamage() ? getNetToughnessBreakdown() : getNetPowerBreakdown()).getTotal();
}
private int intensity = 0;
@@ -4641,7 +4654,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} else if (category.equals("Keywords")) {
boolean removeAll = p.containsKey("RemoveAll") && (boolean) p.get("RemoveAll") == true;
addChangedCardKeywords((List<String>) p.get("AddKeywords"), Lists.newArrayList(), removeAll,
(long) p.get("Timestamp"), (long) 0);
(long) p.get("Timestamp"), null);
} else if (category.equals("Types")) {
addChangedCardTypes((CardType) p.get("AddTypes"), (CardType) p.get("RemoveTypes"),
false, (Set<RemoveType>) p.get("RemoveXTypes"),
@@ -4751,9 +4764,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Check replacement effects
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.IsCombat, attacker); // right name for parameter?
List<ReplacementEffect> list = getGame().getReplacementHandler().getReplacementList(ReplacementType.Tap, repParams, ReplacementLayer.CantHappen);
return list.isEmpty();
return !getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Tap, repParams);
}
public final boolean tap(boolean tapAnimation, SpellAbility cause, Player tapper) {
@@ -4949,7 +4960,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
KeywordInterface result = storedKeywordByText.get(triple);
if (result == null) {
result = ki.copy(this, false);
result.setStaticId(stAb.getId());
result.setStatic(stAb);
result.setIdx(idx);
result.setIntrinsic(true);
storedKeywordByText.put(triple, result);
@@ -5072,11 +5083,11 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final void addChangedCardKeywords(final List<String> keywords, final List<String> removeKeywords,
final boolean removeAllKeywords, final long timestamp, final long staticId) {
addChangedCardKeywords(keywords, removeKeywords, removeAllKeywords, timestamp, staticId, true);
final boolean removeAllKeywords, final long timestamp, final StaticAbility st) {
addChangedCardKeywords(keywords, removeKeywords, removeAllKeywords, timestamp, st, true);
}
public final void addChangedCardKeywords(final List<String> keywords, final List<String> removeKeywords,
final boolean removeAllKeywords, final long timestamp, final long staticId, final boolean updateView) {
final boolean removeAllKeywords, final long timestamp, final StaticAbility st, final boolean updateView) {
List<KeywordInterface> kws = Lists.newArrayList();
if (keywords != null) {
long idx = 1;
@@ -5090,14 +5101,14 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
}
if (canHave) {
kws.add(getKeywordForStaticAbility(kw, staticId, idx));
kws.add(getKeywordForStaticAbility(kw, st, idx));
}
idx++;
}
}
final KeywordsChange newCks = new KeywordsChange(kws, removeKeywords, removeAllKeywords);
changedCardKeywords.put(timestamp, staticId, newCks);
changedCardKeywords.put(timestamp, st == null ? 0l : st.getId(), newCks);
if (updateView) {
updateKeywords();
@@ -5106,12 +5117,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
}
public final KeywordInterface getKeywordForStaticAbility(String kw, final long staticId, final long idx) {
public final KeywordInterface getKeywordForStaticAbility(String kw, final StaticAbility st, final long idx) {
KeywordInterface result;
long staticId = st == null ? 0 : st.getId();
Triple<String, Long, Long> triple = Triple.of(kw, staticId, idx);
if (staticId < 1 || !storedKeywords.containsKey(triple)) {
result = Keyword.getInstance(kw);
result.setStaticId(staticId);
result.setStatic(st);
result.setIdx(idx);
result.createTraits(this, false);
if (staticId > 0) {
@@ -5124,8 +5136,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final void addKeywordForStaticAbility(KeywordInterface kw) {
if (kw.getStaticId() > 0) {
storedKeywords.put(Triple.of(kw.getOriginal(), kw.getStaticId(), kw.getIdx()), kw);
if (kw.getStatic() != null) {
storedKeywords.put(Triple.of(kw.getOriginal(), (long)kw.getStatic().getId(), kw.getIdx()), kw);
}
}
@@ -5170,8 +5182,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public final void addChangedCardKeywordsInternal(
final Collection<KeywordInterface> keywords, final Collection<KeywordInterface> removeKeywords,
final boolean removeAllKeywords,
final long timestamp, final long staticId, final boolean updateView) {
final long timestamp, final StaticAbility st, final boolean updateView) {
final KeywordsChange newCks = new KeywordsChange(keywords, removeKeywords, removeAllKeywords);
long staticId = st == null ? 0 : st.getId();
changedCardKeywords.put(timestamp, staticId, newCks);
if (updateView) {
@@ -5688,7 +5701,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Just phased in, time to run the phased in trigger
getGame().getTriggerHandler().registerActiveTrigger(this, false);
getGame().getTriggerHandler().runTrigger(TriggerType.PhaseIn, runParams, false);
getGame().getTriggerHandler().runTrigger(TriggerType.PhaseIn, runParams, true);
}
game.updateLastStateForCard(this);
@@ -5808,9 +5821,15 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
if (StringUtils.isNumeric(s)) {
count += Integer.parseInt(s);
} else {
String svar = StringUtils.join(parse);
if (state.hasSVar(svar)) {
count += AbilityUtils.calculateAmount(this, state.getSVar(svar), null);
StaticAbility st = inst.getStatic();
// TODO make keywordinterface inherit from CardTrait somehow, or invent new interface
if (st != null && st.hasSVar(s)) {
count += AbilityUtils.calculateAmount(this, st.getSVar(s), null);
} else {
String svar = StringUtils.join(parse);
if (state.hasSVar(svar)) {
count += AbilityUtils.calculateAmount(this, state.getSVar(svar), null);
}
}
}
}
@@ -6352,10 +6371,17 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final String getImageKey() {
return getCardForUi().currentState.getImageKey();
Card uiCard = getCardForUi();
if(uiCard == null)
return "";
return uiCard.currentState.getImageKey();
}
public final void setImageKey(final String iFN) {
getCardForUi().currentState.setImageKey(iFN);
Card uiCard = getCardForUi();
if(uiCard == null)
this.currentState.setImageKey(iFN); //Shouldn't really matter; the card isn't supposed to show in the UI anyway.
else
uiCard.currentState.setImageKey(iFN);
}
public final void setImageKey(final IPaperCard ipc, final CardStateName stateName) {
if (ipc == null)
@@ -6382,7 +6408,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public String getImageKey(CardStateName state) {
CardState c = getCardForUi().states.get(state);
Card uiCard = getCardForUi();
if(uiCard == null)
return "";
CardState c = uiCard.states.get(state);
return (c != null ? c.getImageKey() : "");
}
@@ -6545,7 +6574,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
suspectedTimestamp = getGame().getNextTimestamp();
// use this for CantHaveKeyword
addChangedCardKeywords(ImmutableList.of("Menace"), ImmutableList.<String>of(), false, suspectedTimestamp, 0, true);
addChangedCardKeywords(ImmutableList.of("Menace"), ImmutableList.<String>of(), false, suspectedTimestamp, null, true);
if (suspectedStatic == null) {
String effect = "Mode$ CantBlockBy | ValidBlocker$ Creature.Self | Description$ CARDNAME can't block.";
@@ -6696,7 +6725,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
new CardType(Collections.singletonList("Creature"), true),
false, EnumSet.of(RemoveType.EnchantmentTypes), bestowTimestamp, 0, updateView, false);
addChangedCardKeywords(Collections.singletonList("Enchant creature"), Lists.newArrayList(),
false, bestowTimestamp, 0, updateView);
false, bestowTimestamp, null, updateView);
}
public final void unanimateBestow() {
@@ -7558,6 +7587,23 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
currentState.setOracleText(oracleText);
}
@Override
public String getTranslationKey() {
return currentState.getTranslationKey();
}
@Override
public String getUntranslatedName() {
return this.getName();
}
@Override
public String getUntranslatedType() {
return currentState.getUntranslatedType();
}
@Override
public String getUntranslatedOracle() {
return currentState.getUntranslatedOracle();
}
@Override
public CardView getView() {
return view;

View File

@@ -7,6 +7,7 @@ import forge.card.CardType;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.ApiType;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import io.sentry.Breadcrumb;
@@ -108,7 +109,11 @@ public class CardCopyService {
if (assignNewId) {
id = newOwner == null ? 0 : newOwner.getGame().nextCardId();
}
final Card c = new Card(id, in.getPaperCard(), in.getGame());
final Card c;
if(in instanceof DetachedCardEffect)
c = new DetachedCardEffect((DetachedCardEffect) in, assignNewId);
else
c = new Card(id, in.getPaperCard(), in.getGame());
c.setOwner(newOwner);
c.setSetCode(in.getSetCode());
@@ -217,7 +222,11 @@ public class CardCopyService {
bread.setData("Player", copyFrom.getController().getName());
Sentry.addBreadcrumb(bread);
final Card newCopy = new Card(copyFrom.getId(), copyFrom.getPaperCard(), copyFrom.getGame(), null);
final Card newCopy;
if(copyFrom instanceof DetachedCardEffect)
newCopy = new DetachedCardEffect((DetachedCardEffect) copyFrom, false);
else
newCopy = new Card(copyFrom.getId(), copyFrom.getPaperCard(), copyFrom.getGame(), null);
cachedMap.put(copyFrom.getId(), newCopy);
newCopy.setSetCode(copyFrom.getSetCode());
newCopy.setOwner(copyFrom.getOwner());

View File

@@ -356,13 +356,16 @@ public class CardFactory {
}
private static void readCardFace(Card c, ICardFace face) {
String variantName = null;
//If it's a functional variant card, switch to that first.
if(face.hasFunctionalVariants()) {
String variantName = c.getPaperCard().getFunctionalVariant();
variantName = c.getPaperCard().getFunctionalVariant();
if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(variantName)) {
ICardFace variant = face.getFunctionalVariant(variantName);
if (variant != null)
if (variant != null) {
face = variant;
c.getCurrentState().setFunctionalVariantName(variantName);
}
else
System.err.printf("Tried to apply unknown or unsupported variant - Card: \"%s\"; Variant: %s\n", face.getName(), variantName);
}
@@ -370,7 +373,7 @@ public class CardFactory {
// Build English oracle and translated oracle mapping
if (c.getId() >= 0) {
CardTranslation.buildOracleMapping(face.getName(), face.getOracleText());
CardTranslation.buildOracleMapping(face.getName(), face.getOracleText(), variantName);
}
// Name first so Senty has the Card name

View File

@@ -2354,7 +2354,17 @@ public class CardFactoryUtil {
final String effect = "DB$ PutCounter | Defined$ ReplacedCard | CounterType$ TIME | CounterNum$ " + m
+ " | ETB$ True | SpellDescription$ " + desc;
final ReplacementEffect re = createETBReplacement(card, ReplacementLayer.Other, effect, false, true, intrinsic, "Card.Self+impended", "");
SpellAbility repAb = AbilityFactory.getAbility(effect, card);
String staticEffect = "DB$ Effect | StaticAbilities$ NoCreature | ExileOnCounter$ TIME | Duration$ UntilHostLeavesPlay";
String staticNoCreature = "Mode$ Continuous | Affected$ Card.EffectSource+counters_GE1_TIME | RemoveType$ Creature | Description$ EFFECTSOURCE isn't a creature.";
AbilitySub effectAb = (AbilitySub)AbilityFactory.getAbility(staticEffect, card);
effectAb.setSVar("NoCreature", staticNoCreature);
repAb.setSubAbility(effectAb);
final ReplacementEffect re = createETBReplacement(card, ReplacementLayer.Other, repAb, false, true, intrinsic, "Card.Self+impended", "");
inst.addReplacement(re);
} else if (keyword.equals("Jump-start")) {
@@ -2821,12 +2831,7 @@ public class CardFactoryUtil {
final String[] k = keyword.split(":");
final Cost disturbCost = new Cost(k[1], true);
SpellAbility newSA;
if (host.getAlternateState().getType().hasSubtype("Aura")) {
newSA = host.getAlternateState().getFirstAbility().copyWithDefinedCost(disturbCost);
} else {
newSA = new SpellPermanent(host, host.getAlternateState(), disturbCost);
}
SpellAbility newSA = host.getAlternateState().getFirstSpellAbilityWithFallback().copyWithDefinedCost(disturbCost);
newSA.setCardState(host.getAlternateState());
StringBuilder sbCost = new StringBuilder("Disturb");
@@ -3137,7 +3142,7 @@ public class CardFactoryUtil {
} else if (keyword.startsWith("Freerunning")) {
final String[] k = keyword.split(":");
final Cost freerunningCost = new Cost(k[1], false);
final SpellAbility newSA = card.getFirstSpellAbility().copyWithDefinedCost(freerunningCost);
final SpellAbility newSA = card.getFirstSpellAbilityWithFallback().copyWithDefinedCost(freerunningCost);
if (host.isInstant() || host.isSorcery()) {
newSA.putParam("Secondary", "True");
@@ -3463,7 +3468,7 @@ public class CardFactoryUtil {
} else if (keyword.startsWith("Prowl")) {
final String[] k = keyword.split(":");
final Cost prowlCost = new Cost(k[1], false);
final SpellAbility newSA = card.getFirstSpellAbility().copyWithDefinedCost(prowlCost);
final SpellAbility newSA = card.getFirstSpellAbilityWithFallback().copyWithDefinedCost(prowlCost);
if (host.isInstant() || host.isSorcery()) {
newSA.putParam("Secondary", "True");
@@ -3993,11 +3998,8 @@ public class CardFactoryUtil {
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutHorsemanship | Secondary$ True " +
" | Description$ Horsemanship (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Impending")) {
String effect = "Mode$ Continuous | Affected$ Card.Self+counters_GE1_TIME | RemoveType$ Creature | Secondary$ True";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Intimidate")) {
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+notSharesColorWith | Secondary$ True " +
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+!SharesColorWith | Secondary$ True " +
" | Description$ Intimidate (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Landwalk")) {

View File

@@ -414,7 +414,7 @@ public class CardLists {
public static int getTotalPower(Iterable<Card> cardList, boolean ignoreNegativePower, boolean crew) {
int total = 0;
for (final Card crd : cardList) {
if (crew && StaticAbilityCrewValue.hasAnyCrewValue(crd)) {
if (crew) {
if (StaticAbilityCrewValue.crewsWithToughness(crd)) {
total += ignoreNegativePower ? Math.max(0, crd.getNetToughness()) : crd.getNetToughness();
} else {

View File

@@ -670,6 +670,11 @@ public class CardProperty {
if (cards.isEmpty() || !card.equals(cards.get(0))) {
return false;
}
} else if (property.startsWith("TopLibraryLand")) {
CardCollection cards = CardLists.filter(card.getOwner().getCardsIn(ZoneType.Library), CardPredicates.Presets.LANDS);
if (cards.isEmpty() || !card.equals(cards.get(0))) {
return false;
}
} else if (property.startsWith("TopLibrary")) {
final CardCollectionView cards = card.getOwner().getCardsIn(ZoneType.Library);
if (cards.isEmpty() || !card.equals(cards.get(0))) {
@@ -717,7 +722,6 @@ public class CardProperty {
}
final String restriction = property.split("SharesColorWith ")[1];
switch (restriction) {
case "MostProminentColor":
byte mask = CardFactoryUtil.getMostProminentColors(game.getCardsIn(ZoneType.Battlefield));
@@ -769,19 +773,6 @@ public class CardProperty {
byte mostProm = CardFactoryUtil.getMostProminentColors(game.getCardsIn(ZoneType.Battlefield));
return ColorSet.fromMask(mostProm).hasAnyColor(MagicColor.fromName(color));
} else if (property.startsWith("notSharesColorWith")) {
if (property.equals("notSharesColorWith")) {
if (card.sharesColorWith(source)) {
return false;
}
} else {
final String restriction = property.split("notSharesColorWith ")[1];
for (final Card c : sourceController.getCardsIn(ZoneType.Battlefield)) {
if (c.isValid(restriction, sourceController, source, spellAbility) && card.sharesColorWith(c)) {
return false;
}
}
}
} else if (property.startsWith("MostProminentCreatureTypeInLibrary")) {
final CardCollectionView list = sourceController.getCardsIn(ZoneType.Library);
for (String s : CardFactoryUtil.getMostProminentCreatureType(list)) {
@@ -840,6 +831,14 @@ public class CardProperty {
return false;
}
} else {
// Special case to prevent list from comparing with itself
if (property.startsWith("sharesCardTypeWithOther")) {
final String restriction = property.split("sharesCardTypeWithOther ")[1];
CardCollection list = AbilityUtils.getDefinedCards(source, restriction, spellAbility);
list.remove(card);
return Iterables.any(list, CardPredicates.sharesCardTypeWith(card));
}
final String restriction = property.split("sharesCardTypeWith ")[1];
switch (restriction) {
case "Imprinted":
@@ -1499,6 +1498,14 @@ public class CardProperty {
if (card.getCMC() % 2 != 1) {
return false;
}
} else if (property.equals("powerEven")) {
if (card.getNetPower() % 2 != 0) {
return false;
}
} else if (property.equals("powerOdd")) {
if (card.getNetPower() % 2 != 1) {
return false;
}
} else if (property.equals("cmcChosenEvenOdd")) {
if (!source.hasChosenEvenOdd()) {
return false;
@@ -1620,8 +1627,7 @@ public class CardProperty {
}
}
if (property.startsWith("attacking ")) { // generic "attacking [DefinedGameEntity]"
FCollection<GameEntity> defined = AbilityUtils.getDefinedEntities(source, property.split(" ")[1],
spellAbility);
FCollection<GameEntity> defined = AbilityUtils.getDefinedEntities(source, property.split(" ")[1], spellAbility);
final GameEntity defender = combat.getDefenderByAttacker(card);
if (!defined.contains(defender)) {
return false;

View File

@@ -44,20 +44,24 @@ import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.SpellPermanent;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.util.Iterables;
import forge.util.ITranslatable;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import org.apache.commons.lang3.StringUtils;
public class CardState extends GameObject implements IHasSVars {
public class CardState extends GameObject implements IHasSVars, ITranslatable {
private String name = "";
private CardType type = new CardType(false);
private ManaCost manaCost = ManaCost.NO_COST;
private byte color = MagicColor.COLORLESS;
private String oracleText = "";
private String functionalVariantName = null;
private int basePower = 0;
private int baseToughness = 0;
private String basePowerString = null;
@@ -202,6 +206,16 @@ public class CardState extends GameObject implements IHasSVars {
view.setOracleText(oracleText);
}
public String getFunctionalVariantName() {
return functionalVariantName;
}
public void setFunctionalVariantName(String functionalVariantName) {
if(functionalVariantName != null && functionalVariantName.isEmpty())
functionalVariantName = null;
this.functionalVariantName = functionalVariantName;
view.setFunctionalVariantName(functionalVariantName);
}
public final int getBasePower() {
return basePower;
@@ -364,6 +378,15 @@ public class CardState extends GameObject implements IHasSVars {
return Iterables.getFirst(getNonManaAbilities(), null);
}
public final SpellAbility getFirstSpellAbilityWithFallback() {
SpellAbility sa = getFirstSpellAbility();
if (sa != null || getTypeWithChanges().isLand()) {
return sa;
}
// this happens if it's transformed backside (e.g. Disturbed)
return new SpellPermanent(getCard(), this);
}
public final boolean hasSpellAbility(final SpellAbility sa) {
return getSpellAbilities().contains(sa);
}
@@ -605,6 +628,7 @@ public class CardState extends GameObject implements IHasSVars {
setManaCost(source.getManaCost());
setColor(source.getColor());
setOracleText(source.getOracleText());
setFunctionalVariantName(source.getFunctionalVariantName());
setBasePower(source.getBasePower());
setBaseToughness(source.getBaseToughness());
setBaseLoyalty(source.getBaseLoyalty());
@@ -804,4 +828,21 @@ public class CardState extends GameObject implements IHasSVars {
}
return cloakUp;
}
@Override
public String getTranslationKey() {
if(StringUtils.isNotEmpty(functionalVariantName))
return name + " $" + functionalVariantName;
return name;
}
@Override
public String getUntranslatedType() {
return getType().toString();
}
@Override
public String getUntranslatedOracle() {
return getOracleText();
}
}

View File

@@ -1161,7 +1161,7 @@ public class CardView extends GameEntityView {
return (zone + ' ' + CardTranslation.getTranslatedName(name) + " (" + getId() + ")").trim();
}
public class CardStateView extends TrackableObject {
public class CardStateView extends TrackableObject implements ITranslatable {
private static final long serialVersionUID = 6673944200513430607L;
private final CardStateName state;
@@ -1314,6 +1314,13 @@ public class CardView extends GameEntityView {
set(TrackableProperty.OracleText, oracleText.replace("\\n", "\r\n\r\n").trim());
}
public String getFunctionalVariantName() {
return get(TrackableProperty.FunctionalVariant);
}
void setFunctionalVariantName(String functionalVariant) {
set(TrackableProperty.FunctionalVariant, functionalVariant);
}
public String getRulesText() {
return get(TrackableProperty.RulesText);
}
@@ -1737,6 +1744,25 @@ public class CardView extends GameEntityView {
public boolean isAttraction() {
return getType().isAttraction();
}
@Override
public String getTranslationKey() {
String key = getName();
String variant = getFunctionalVariantName();
if(StringUtils.isNotEmpty(variant))
key = key + " $" + variant;
return key;
}
@Override
public String getUntranslatedType() {
return getType().toString();
}
@Override
public String getUntranslatedOracle() {
return getOracleText();
}
}
//special methods for updating card and player properties as needed and returning the new collection

View File

@@ -102,15 +102,16 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
// will be handled by original "cause" instead
return;
}
// this should still refresh for empty battlefield
if (lastStateBattlefield != CardCollection.EMPTY) {
game.getTriggerHandler().resetActiveTriggers(false);
// register all LTB trigger from last state battlefield
for (Card lki : lastStateBattlefield) {
game.getTriggerHandler().registerActiveLTBTrigger(lki);
}
}
if (!isEmpty()) {
// this should still refresh for empty battlefield
if (lastStateBattlefield != CardCollection.EMPTY) {
game.getTriggerHandler().resetActiveTriggers(false);
// register all LTB trigger from last state battlefield
for (Card lki : lastStateBattlefield) {
game.getTriggerHandler().registerActiveLTBTrigger(lki);
}
}
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Cards, new CardZoneTable(this));
runParams.put(AbilityKey.Cause, cause);

View File

@@ -327,6 +327,8 @@ public enum CounterEnumType {
POLYP("POLYP", 236, 185, 198),
POSSESSION("POSSN", 60, 65, 85),
PREY("PREY", 240, 0, 0),
PUPA("PUPA", 0, 223, 203),
@@ -345,6 +347,8 @@ public enum CounterEnumType {
QUEST("QUEST", 251, 189, 0),
RELEASE("RELEASE", 200, 210, 50),
REPRIEVE("REPR", 240, 120, 50),
REJECTION("REJECT", 212, 235, 242),

View File

@@ -11,14 +11,12 @@ public class GameEventSpellAbilityCast extends GameEvent {
public final SpellAbility sa;
public final SpellAbilityStackInstance si;
public final boolean replicate;
public final int stackIndex;
public GameEventSpellAbilityCast(SpellAbility sp, SpellAbilityStackInstance si, int stackIndex, boolean replicate) {
public GameEventSpellAbilityCast(SpellAbility sp, SpellAbilityStackInstance si, int stackIndex) {
sa = sp;
this.si = si;
this.stackIndex = stackIndex;
this.replicate = replicate;
}
/* (non-Javadoc)

View File

@@ -25,7 +25,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
private Keyword keyword;
private String original;
private long staticId = 0;
private StaticAbility st = null;
private long idx = -1;
private List<Trigger> triggers = Lists.newArrayList();
@@ -367,11 +367,11 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
}
}
public long getStaticId() {
return this.staticId;
public StaticAbility getStatic() {
return this.st;
}
public void setStaticId(long v) {
this.staticId = v;
public void setStatic(StaticAbility st) {
this.st = st;
}
public long getIdx() {

View File

@@ -23,8 +23,10 @@ public interface KeywordInterface extends Cloneable {
String getReminderText();
int getAmount();
long getStaticId();
void setStaticId(long v);
StaticAbility getStatic();
void setStatic(StaticAbility st);
long getIdx();
void setIdx(long i);

View File

@@ -264,12 +264,6 @@ public class PhaseHandler implements java.io.Serializable {
p.resetNumDrawnThisDrawStep();
}
playerTurn.drawCard();
for (Player p : game.getPlayers()) {
if (p.isOpponentOf(playerTurn) &&
p.hasKeyword("You draw a card during each opponent's draw step.")) {
p.drawCard();
}
}
break;
case MAIN1:

View File

@@ -229,7 +229,7 @@ public class Untap extends Phase {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Map, untapMap);
game.getTriggerHandler().runTrigger(TriggerType.UntapAll, runParams, false);
} // end doUntap
}
private static boolean optionalUntap(final Card c) {
boolean untap = true;
@@ -298,6 +298,10 @@ public class Untap extends Phase {
runParams.put(AbilityKey.Cards, phasedOut);
turn.getGame().getTriggerHandler().runTrigger(TriggerType.PhaseOutAll, runParams, false);
}
if (!toPhase.isEmpty()) {
// collect now before some zone change during Untap resets triggers
turn.getGame().getTriggerHandler().collectTriggerForWaiting();
}
}
private static void doDayTime(final Player previous) {

View File

@@ -39,33 +39,21 @@ public enum GameLossReason {
OpponentWon,
IntentionalDraw // Not a real "game loss" as such, but a reason not to continue playing.
;
/*
* DoorToNothingness, // Door To Nothingness's ability activated
*
* // TODO: Implement game logics for the ones below Transcendence20Life, //
* When you have 20 or more life, you lose the game. FailedToPayPactUpkeep,
* // Pacts from Future Sight series (cost 0 but you must pay their real
* cost at next turn's upkeep, otherwise GL) PhageTheUntouchableDamage, //
* Whenever Phage deals combat damage to a player, that player loses the
* game. PhageTheUntouchableWrongETB, // When Phage the Untouchable ETB, if
* you didn't cast it from your hand, you lose the game.
* NefariousLichLeavesTB, // When Nefarious Lich leaves the battlefield, you
* lose the game. NefariousLichCannotExileGrave, // If damage would be dealt
* to you, exile that many cards from your graveyard instead. If you can't,
* you lose the game. LichWasPutToGraveyard, // When Lich is put into a
* graveyard from the battlefield, you lose the game. FinalFortune, // same
* as Warrior's Oath - lose at the granted extra turn's end step
* ImmortalCoilEmptyGraveyard, // When there are no cards in your graveyard,
* you lose the game. ForbiddenCryptEmptyGraveyard, // If you would draw a
* card, return a card from your graveyard to your hand instead. If you
* can't, you lose the game.
*
* // Amulet of quoz skipped for using ante, // Form of the Squirrel and
* Rocket-Powered Turbo Slug skipped for being part of UN- set
/**
* Parses a string into an enum member.
* @param string to parse
* @return enum equivalent
*/
public static GameLossReason smartValueOf(String value) {
final String valToCompate = value.trim();
for (final GameLossReason v : GameLossReason.values()) {
if (v.name().compareToIgnoreCase(valToCompate) == 0) {
return v;
}
}
// refer to
// http://gatherer.wizards.com/Pages/Search/Default.aspx?output=standard&text=+[%22lose+the+game%22]
// for more cards when they are printed
throw new RuntimeException("Element " + value + " not found in GameLossReason enum");
}
}

View File

@@ -62,6 +62,7 @@ import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
/**
* <p>
@@ -172,8 +173,9 @@ public class Player extends GameEntity implements Comparable<Player> {
private int teamNumber = -1;
private Card activeScheme = null;
private List<Card> commanders = Lists.newArrayList();
private Map<Card, Integer> commanderCast = Maps.newHashMap();
private final CardCollection commanders = new CardCollection();
private final Map<Card, Integer> commanderCast = Maps.newHashMap();
private DetachedCardEffect commanderEffect = null;
private final Game game;
private boolean triedToDrawFromEmptyLibrary = false;
private CardCollection lostOwnership = new CardCollection();
@@ -286,7 +288,6 @@ public class Player extends GameEntity implements Comparable<Player> {
activeScheme = getZone(ZoneType.SchemeDeck).get(0);
game.getAction().moveToCommand(activeScheme, cause);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Scheme, activeScheme);
@@ -501,52 +502,43 @@ public class Player extends GameEntity implements Comparable<Player> {
}
public final int loseLife(int toLose, final boolean damage, final boolean manaBurn) {
int lifeLost = 0;
if (!canLoseLife()) {
// Rule 118.4
// this is for players being able to pay 0 life nothing to do
// no trigger for lost no life
if (toLose <= 0 || !canLoseLife()) {
return 0;
}
if (toLose > 0) {
int oldLife = life;
// Run applicable replacement effects
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.Amount, toLose);
repParams.put(AbilityKey.IsDamage, damage);
int oldLife = life;
// Run applicable replacement effects
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.Amount, toLose);
repParams.put(AbilityKey.IsDamage, damage);
switch (getGame().getReplacementHandler().run(ReplacementType.LifeReduced, repParams)) {
case NotReplaced:
break;
case Updated:
// check if this is still the affected player
if (this.equals(repParams.get(AbilityKey.Affected))) {
toLose = (int) repParams.get(AbilityKey.Amount);
// there is nothing that changes lifegain into lifeloss this way
if (toLose <= 0) {
return 0;
}
} else {
switch (getGame().getReplacementHandler().run(ReplacementType.LifeReduced, repParams)) {
case NotReplaced:
break;
case Updated:
// check if this is still the affected player
if (this.equals(repParams.get(AbilityKey.Affected))) {
toLose = (int) repParams.get(AbilityKey.Amount);
// there is nothing that changes lifegain into lifeloss this way
if (toLose <= 0) {
return 0;
}
break;
default:
} else {
return 0;
}
break;
default:
return 0;
}
life -= toLose;
view.updateLife(this);
lifeLost = toLose;
if (manaBurn) {
game.fireEvent(new GameEventManaBurn(this, lifeLost, true));
} else {
game.fireEvent(new GameEventPlayerLivesChanged(this, oldLife, life));
}
} else if (toLose == 0) {
// Rule 118.4
// this is for players being able to pay 0 life nothing to do
// no trigger for lost no life
return 0;
life -= toLose;
view.updateLife(this);
if (manaBurn) {
game.fireEvent(new GameEventManaBurn(this, toLose, true));
} else {
System.out.println("Player - trying to lose negative life");
return 0;
game.fireEvent(new GameEventPlayerLivesChanged(this, oldLife, life));
}
boolean firstLost = lifeLostThisTurn == 0;
@@ -561,7 +553,7 @@ public class Player extends GameEntity implements Comparable<Player> {
game.getTriggerHandler().runTrigger(TriggerType.LifeChanged, runParams, false);
return lifeLost;
return toLose;
}
public final boolean canLoseLife() {
@@ -693,7 +685,7 @@ public class Player extends GameEntity implements Comparable<Player> {
if (infect) {
poisonCounters += amount;
}
else if (!hasKeyword("Damage doesn't cause you to lose life.")) {
else {
// rule 118.2. Damage dealt to a player normally causes that player to lose that much life.
simultaneousDamage += amount;
}
@@ -1117,23 +1109,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
public void surveil(int num, SpellAbility cause, Map<AbilityKey, Object> params) {
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.Source, cause);
repParams.put(AbilityKey.SurveilNum, num);
if (params != null) {
repParams.putAll(params);
}
switch (getGame().getReplacementHandler().run(ReplacementType.Surveil, repParams)) {
case NotReplaced:
break;
case Updated: {
num = (int) repParams.get(AbilityKey.SurveilNum);
break;
}
default:
return;
}
num += StaticAbilitySurveilNum.surveilNumMod(this);
final CardCollection topN = getTopXCardsFromLibrary(num);
@@ -1840,6 +1816,7 @@ public class Player extends GameEntity implements Comparable<Player> {
lastDrawnCard = c;
return lastDrawnCard;
}
public final Card getRingBearer() {
return ringBearer;
}
@@ -1862,6 +1839,7 @@ public class Player extends GameEntity implements Comparable<Player> {
ringBearer.setRingBearer(false);
ringBearer = null;
}
public final String getNamedCard() {
return namedCard;
}
@@ -1974,7 +1952,6 @@ public class Player extends GameEntity implements Comparable<Player> {
public final void altWinBySpellEffect(final String sourceName) {
if (cantWin()) {
System.out.println("Tried to win, but currently can't.");
return;
}
setOutcome(PlayerOutcome.altWin(sourceName));
@@ -1982,16 +1959,14 @@ public class Player extends GameEntity implements Comparable<Player> {
public final boolean loseConditionMet(final GameLossReason state, final String spellName) {
if (state != GameLossReason.OpponentWon) {
if (cantLose()) {
System.out.println("Tried to lose, but currently can't.");
return false;
}
// Replacement effects
if (game.getReplacementHandler().run(ReplacementType.GameLoss, AbilityKey.mapFromAffected(this)) != ReplacementResult.NotReplaced) {
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.LoseReason, state);
if (game.getReplacementHandler().run(ReplacementType.GameLoss, repParams) != ReplacementResult.NotReplaced) {
return false;
}
}
//final String spellName = sa != null ? sa.getHostCard().getName() : null;
setOutcome(PlayerOutcome.loss(state, spellName));
return true;
}
@@ -2004,32 +1979,39 @@ public class Player extends GameEntity implements Comparable<Player> {
setOutcome(PlayerOutcome.draw());
}
public final boolean conceded() {
return getOutcome() != null && getOutcome().lossState == GameLossReason.Conceded;
}
public final boolean cantLose() {
if (getOutcome() != null && getOutcome().lossState == GameLossReason.Conceded) {
if (conceded()) {
return false;
}
return hasKeyword("You can't lose the game.");
return cantLoseCheck(null);
}
public final boolean cantLoseForZeroOrLessLife() {
return cantLose() || hasKeyword("You don't lose the game for having 0 or less life.");
if (conceded()) {
return false;
}
return cantLoseCheck(GameLossReason.LifeReachedZero);
}
public final boolean cantLoseCheck(final GameLossReason state) {
// Replacement effects
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.LoseReason, state);
return game.getReplacementHandler().cantHappenCheck(ReplacementType.GameLoss, repParams);
}
public final boolean cantWin() {
boolean isAnyOppLoseProof = false;
for (Player p : game.getPlayers()) {
if (p == this || p.getOutcome() != null) {
continue; // except self and already dead
}
isAnyOppLoseProof |= p.hasKeyword("You can't lose the game.");
}
return hasKeyword("You can't win the game.") || isAnyOppLoseProof;
return game.getReplacementHandler().cantHappenCheck(ReplacementType.GameWin, AbilityKey.mapFromAffected(this));
}
public final boolean checkLoseCondition() {
// Just in case player already lost
if (getOutcome() != null) {
return getOutcome().lossState != null;
if (hasLost()) {
return true;
}
// check this first because of Lich's Mirror (704.7)
@@ -2038,26 +2020,27 @@ public class Player extends GameEntity implements Comparable<Player> {
if (triedToDrawFromEmptyLibrary) {
triedToDrawFromEmptyLibrary = false; // one-shot check
// Mine, Mine, Mine! prevents decking
if (!hasKeyword("You don't lose the game for drawing from an empty library.")) {
return loseConditionMet(GameLossReason.Milled, null);
if (loseConditionMet(GameLossReason.Milled, null)) {
return true;
}
}
// Rule 704.5a - If a player has 0 or less life, he or she loses the game.
final boolean hasNoLife = getLife() <= 0;
if (hasNoLife && !cantLoseForZeroOrLessLife()) {
return loseConditionMet(GameLossReason.LifeReachedZero, null);
if (hasNoLife && loseConditionMet(GameLossReason.LifeReachedZero, null)) {
return true;
}
// Rule 704.5c - If a player has ten or more poison counters, he or she loses the game.
if (getCounters(CounterEnumType.POISON) >= 10) {
return loseConditionMet(GameLossReason.Poisoned, null);
// 704.6b In a Two-Headed Giant game, if a team has fifteen or more poison counters, that team loses the game. See rule 810, “Two-Headed Giant Variant.”
if (getCounters(CounterEnumType.POISON) >= 10 && loseConditionMet(GameLossReason.Poisoned, null)) {
return true;
}
if (game.getRules().hasAppliedVariant(GameType.Commander)) {
for (Entry<Card, Integer> entry : getCommanderDamage()) {
if (entry.getValue() >= 21) {
return loseConditionMet(GameLossReason.CommanderDamage, null);
if (entry.getValue() >= 21 && loseConditionMet(GameLossReason.CommanderDamage, null)) {
return true;
}
}
}
@@ -2755,12 +2738,99 @@ public class Player extends GameEntity implements Comparable<Player> {
public List<Card> getCommanders() {
return commanders;
}
public void setCommanders(List<Card> commanders0) {
if (commanders0 == commanders) { return; }
commanders = commanders0;
public void setCommanders(List<Card> commanders) {
boolean needsUpdate = false;
//Remove any existing commanders not in the new list.
for(Card oldCommander : this.commanders) {
if(commanders.contains(oldCommander))
continue;
needsUpdate = true;
this.commanders.remove(oldCommander);
oldCommander.setCommander(false);
}
if(this.commanderEffect == null && !commanders.isEmpty())
this.createCommanderEffect();
//Add any new commanders that aren't in the existing list.
for(Card newCommander : commanders) {
assert(this.equals(newCommander.getOwner()));
if(this.commanders.contains(newCommander))
continue;
needsUpdate = true;
this.commanders.add(newCommander);
newCommander.setCommander(true);
}
if(needsUpdate) {
view.updateCommander(this);
}
}
public void copyCommandersToSnapshot(Player toPlayer, Function<Card, Card> mapper) {
//Seems like the rest of the logic for copying players should be in this class too.
//For now, doing this here can retain the links between commander effects and commanders.
toPlayer.resetCommanderStats();
toPlayer.commanders.clear();
for (final Card c : this.getCommanders()) {
Card newCommander = mapper.apply(c);
if(newCommander == null)
throw new RuntimeException("Unable to find commander in game snapshot: " + c);
toPlayer.commanders.add(newCommander);
newCommander.setCommander(true);
}
for (Map.Entry<Card, Integer> entry : this.commanderCast.entrySet()) {
//Have to iterate over this separately in case commanders change mid-game.
Card commander = mapper.apply(entry.getKey());
toPlayer.commanderCast.put(commander, entry.getValue());
}
for (Map.Entry<Card, Integer> entry : this.getCommanderDamage()) {
Card commander = mapper.apply(entry.getKey());
if(commander == null) //Ceased to exist?
continue;
int damage = entry.getValue();
toPlayer.addCommanderDamage(commander, damage);
}
if (this.commanderEffect != null) {
Card commanderEffect = mapper.apply(this.commanderEffect);
toPlayer.commanderEffect = (DetachedCardEffect) commanderEffect;
}
}
public void addCommander(Card commander) {
assert(this.equals(commander.getOwner())); //Making someone else's card your commander isn't currently supported.
if(this.commanders.contains(commander))
return;
this.commanders.add(commander);
if(this.commanderEffect == null)
this.createCommanderEffect();
commander.setCommander(true);
view.updateCommander(this);
}
public void removeCommander(Card commander) {
if(!this.commanders.remove(commander))
return;
commander.setCommander(false);
view.updateCommander(this);
}
/**
* Toggles whether the commander replacement effect is active.
* (i.e. the option to send your commander to the command zone when
* it would otherwise be sent to your library)
* <p>
* Used when moving a merged permanent with a commander component.
* Causes the commander replacement to only be applied after the merged
* permanent is split up, rather than causing every component card to be moved.
*/
public void setCommanderReplacementSuppressed(boolean suppress) {
if(this.commanderEffect == null)
return;
for (final ReplacementEffect re : this.commanderEffect.getReplacementEffects()) {
re.setSuppressed(suppress);
}
}
public Iterable<Entry<Card, Integer>> getCommanderDamage() {
return commanderDamage.entrySet();
}
@@ -2913,7 +2983,6 @@ public class Player extends GameEntity implements Comparable<Player> {
// Commander
if (!registeredPlayer.getCommanders().isEmpty()) {
List<Card> commanders = Lists.newArrayList();
for (PaperCard pc : registeredPlayer.getCommanders()) {
Card cmd = Card.fromPaperCard(pc, this);
boolean color = false;
@@ -2937,20 +3006,16 @@ public class Player extends GameEntity implements Comparable<Player> {
Lang.joinHomogenous(chosenColors)), p);
}
cmd.setCollectible(true);
cmd.setCommander(true);
com.add(cmd);
commanders.add(cmd);
com.add(createCommanderEffect(game, cmd));
this.addCommander(cmd);
}
this.setCommanders(commanders);
}
else if (registeredPlayer.getPlaneswalker() != null) { // Planeswalker
Card cmd = Card.fromPaperCard(registeredPlayer.getPlaneswalker(), this);
cmd.setCollectible(true);
cmd.setCommander(true);
com.add(cmd);
setCommanders(Lists.newArrayList(cmd));
com.add(createCommanderEffect(game, cmd));
this.addCommander(cmd);
}
// Conspiracies
@@ -3128,49 +3193,56 @@ public class Player extends GameEntity implements Comparable<Player> {
return eff;
}
public static DetachedCardEffect createCommanderEffect(Game game, Card commander) {
final String name = Lang.getInstance().getPossesive(commander.getName()) + " Commander Effect";
DetachedCardEffect eff = new DetachedCardEffect(commander, name);
public void createCommanderEffect() {
PlayerZone com = getZone(ZoneType.Command);
if(this.commanderEffect != null)
com.remove(this.commanderEffect);
if (game.getRules().hasAppliedVariant(GameType.Oathbreaker) && commander.getRules().canBeSignatureSpell()) {
DetachedCardEffect eff = new DetachedCardEffect(this, "Commander Effect");
String validCommander = "Card.IsCommander+YouOwn";
if (game.getRules().hasAppliedVariant(GameType.Oathbreaker)) {
//signature spells can only reside on the stack or in the command zone
String effStr = "DB$ ChangeZone | Origin$ Stack | Destination$ Command | Defined$ ReplacedCard";
String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Destination$ Graveyard,Exile,Hand,Library | " +
String moved = "Event$ Moved | ValidCard$ Spell.IsCommander+YouOwn | Secondary$ True | Destination$ Graveyard,Exile,Hand,Library | " +
"Description$ If a signature spell would be put into another zone from the stack, put it into the command zone instead.";
ReplacementEffect re = ReplacementHandler.parseReplacement(moved, eff, true);
re.setOverridingAbility(AbilityFactory.getAbility(effStr, eff));
eff.addReplacementEffect(re);
//TODO: Actual recognition for signature spells as their own thing, separate from commanders.
validCommander = "Permanent.IsCommander+YouOwn";
//signature spells can only be cast if your oathbreaker is in on the battlefield under your control
String castRestriction = "Mode$ CantBeCast | ValidCard$ Card.EffectSource+YouOwn | EffectZone$ Command | IsPresent$ Card.IsCommander+YouOwn+YouCtrl | PresentZone$ Battlefield | PresentCompare$ EQ0 | " +
String castRestriction = "Mode$ CantBeCast | ValidCard$ Spell.IsCommander+YouOwn | EffectZone$ Command | IsPresent$ Permanent.IsCommander+YouOwn+YouCtrl | PresentZone$ Battlefield | PresentCompare$ EQ0 | " +
"Description$ Signature spell can only be cast if your oathbreaker is on the battlefield under your control.";
eff.addStaticAbility(castRestriction);
}
else {
String effStr = "DB$ ChangeZone | Origin$ Battlefield,Graveyard,Exile,Library,Hand | Destination$ Command | Defined$ ReplacedCard";
String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Optional$ True | OptionalDecider$ You | CommanderMoveReplacement$ True ";
if (game.getRules().hasAppliedVariant(GameType.TinyLeaders)) {
moved += " | Destination$ Graveyard,Exile | Description$ If a commander would be put into its owner's graveyard or exile from anywhere, that player may put it into the command zone instead.";
}
else if (game.getRules().hasAppliedVariant(GameType.Oathbreaker)) {
moved += " | Destination$ Graveyard,Exile,Hand,Library | Description$ If a commander would be exiled or put into hand, graveyard, or library from anywhere, that player may put it into the command zone instead.";
} else {
// rule 903.9b
moved += " | Destination$ Hand,Library | Description$ If a commander would be put into its owner's hand or library from anywhere, its owner may put it into the command zone instead.";
}
ReplacementEffect re = ReplacementHandler.parseReplacement(moved, eff, true);
re.setOverridingAbility(AbilityFactory.getAbility(effStr, eff));
eff.addReplacementEffect(re);
String effStr = "DB$ ChangeZone | Origin$ Battlefield,Graveyard,Exile,Library,Hand | Destination$ Command | Defined$ ReplacedCard";
String moved = "Event$ Moved | ValidCard$ " + validCommander + " | Secondary$ True | Optional$ True | OptionalDecider$ You | CommanderMoveReplacement$ True ";
if (game.getRules().hasAppliedVariant(GameType.TinyLeaders)) {
moved += " | Destination$ Graveyard,Exile | Description$ If a commander would be put into its owner's graveyard or exile from anywhere, that player may put it into the command zone instead.";
}
else if (game.getRules().hasAppliedVariant(GameType.Oathbreaker)) {
moved += " | Destination$ Graveyard,Exile,Hand,Library | Description$ If a commander would be exiled or put into hand, graveyard, or library from anywhere, that player may put it into the command zone instead.";
} else {
// rule 903.9b
moved += " | Destination$ Hand,Library | Description$ If a commander would be put into its owner's hand or library from anywhere, its owner may put it into the command zone instead.";
}
ReplacementEffect re = ReplacementHandler.parseReplacement(moved, eff, true);
re.setOverridingAbility(AbilityFactory.getAbility(effStr, eff));
eff.addReplacementEffect(re);
String mayBePlayedAbility = "Mode$ Continuous | EffectZone$ Command | MayPlay$ True | Affected$ Card.YouOwn+EffectSource | AffectedZone$ Command";
String mayBePlayedAbility = "Mode$ Continuous | EffectZone$ Command | MayPlay$ True | Affected$ Card.IsCommander+YouOwn | AffectedZone$ Command";
if (game.getRules().hasAppliedVariant(GameType.Planeswalker)) { //support paying for Planeswalker with any color mana
mayBePlayedAbility += " | MayPlayIgnoreColor$ True";
}
eff.addStaticAbility(mayBePlayedAbility);
return eff;
this.commanderEffect = eff;
com.add(eff);
}
public void createPlanechaseEffects(Game game) {

View File

@@ -95,5 +95,4 @@ public final class PlayerPredicates {
}
public static final Predicate<Player> NOT_LOST = p -> p.getOutcome() == null || p.getOutcome().hasWon();
public static final Predicate<Player> CANT_WIN = p -> p.hasKeyword("You can't win the game.");
}

View File

@@ -10,7 +10,6 @@ import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.util.Iterables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -19,7 +18,7 @@ public class RegisteredPlayer {
private final Deck originalDeck; // never return or modify this instance (it's a reference to game resources)
private Deck currentDeck;
private static final Iterable<PaperCard> EmptyList = Collections.unmodifiableList(new ArrayList<>());
private static final Iterable<PaperCard> EmptyList = Collections.emptyList();
private LobbyPlayer player = null;
@@ -49,7 +48,6 @@ public class RegisteredPlayer {
public final Integer getId() {
return id;
}
public final void setId(Integer id0) {
id = id0;
}
@@ -61,15 +59,6 @@ public class RegisteredPlayer {
public final int getStartingLife() {
return startingLife;
}
public final Iterable<? extends IPaperCard> getCardsOnBattlefield() {
return Iterables.concat(cardsOnBattlefield == null ? EmptyList : cardsOnBattlefield,
extraCardsOnBattlefield == null ? EmptyList : extraCardsOnBattlefield);
}
public final Iterable<? extends IPaperCard> getExtraCardsInCommandZone() {
return extraCardsInCommandZone == null ? EmptyList : extraCardsInCommandZone;
}
public final void setStartingLife(int startingLife) {
this.startingLife = startingLife;
}
@@ -77,7 +66,6 @@ public class RegisteredPlayer {
public final int getManaShards() {
return manaShards;
}
public final void setManaShards(int manaShards) {
this.manaShards = manaShards;
}
@@ -89,6 +77,15 @@ public class RegisteredPlayer {
enableETBCountersEffect = value;
}
public final Iterable<? extends IPaperCard> getCardsOnBattlefield() {
return Iterables.concat(cardsOnBattlefield == null ? EmptyList : cardsOnBattlefield,
extraCardsOnBattlefield == null ? EmptyList : extraCardsOnBattlefield);
}
public final Iterable<? extends IPaperCard> getExtraCardsInCommandZone() {
return extraCardsInCommandZone == null ? EmptyList : extraCardsInCommandZone;
}
public final void setCardsOnBattlefield(Iterable<IPaperCard> cardsOnTable) {
this.cardsOnBattlefield = cardsOnTable;
}
@@ -137,7 +134,6 @@ public class RegisteredPlayer {
public int getTeamNumber() {
return teamNumber;
}
public void setTeamNumber(int teamNumber0) {
this.teamNumber = teamNumber0;
}
@@ -192,7 +188,6 @@ public class RegisteredPlayer {
public LobbyPlayer getPlayer() {
return player;
}
public RegisteredPlayer setPlayer(LobbyPlayer player0) {
this.player = player0;
return this;
@@ -219,7 +214,6 @@ public class RegisteredPlayer {
setStartingLife(getStartingLife() + avatar.getRules().getLife());
setStartingHand(getStartingHand() + avatar.getRules().getHand());
}
}
public PaperCard getPlaneswalker() {

View File

@@ -109,7 +109,6 @@ public class ReplaceAddCounter extends ReplacementEffect {
@Override
public boolean modeCheck(ReplacementType event, Map<AbilityKey, Object> runParams) {
// TODO Auto-generated method stub
if (super.modeCheck(event, runParams)) {
return true;
}

View File

@@ -30,6 +30,10 @@ public class ReplaceGameLoss extends ReplacementEffect {
return false;
}
if (!matchesValidParam("ValidLoseReason", runParams.get(AbilityKey.LoseReason))) {
return false;
}
return true;
}

View File

@@ -0,0 +1,26 @@
package forge.game.replacement;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
public class ReplaceGameWin extends ReplacementEffect {
public ReplaceGameWin(Map<String, String> map, Card host, boolean intrinsic) {
super(map, host, intrinsic);
}
/* (non-Javadoc)
* @see forge.card.replacement.ReplacementEffect#canReplace(java.util.HashMap)
*/
@Override
public boolean canReplace(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) {
return false;
}
return true;
}
}

View File

@@ -1,49 +0,0 @@
package forge.game.replacement;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
/**
* TODO: Write javadoc for this type.
*
*/
public class ReplaceSurveil extends ReplacementEffect {
/**
*
* ReplaceProduceMana.
* @param mapParams &emsp; HashMap<String, String>
* @param host &emsp; Card
*/
public ReplaceSurveil(final Map<String, String> mapParams, final Card host, final boolean intrinsic) {
super(mapParams, host, intrinsic);
}
/* (non-Javadoc)
* @see forge.card.replacement.ReplacementEffect#canReplace(java.util.Map)
*/
@Override
public boolean canReplace(Map<AbilityKey, Object> runParams) {
if (((int) runParams.get(AbilityKey.SurveilNum)) <= 0) {
return false;
}
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) {
return false;
}
return true;
}
/* (non-Javadoc)
* @see forge.card.replacement.ReplacementEffect#setReplacingObjects(java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
public void setReplacingObjects(Map<AbilityKey, Object> runParams, SpellAbility sa) {
sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Affected));
sa.setReplacingObject(AbilityKey.SurveilNum, runParams.get(AbilityKey.SurveilNum));
}
}

View File

@@ -23,6 +23,7 @@ import java.util.Objects;
import com.google.common.collect.*;
import forge.util.ITranslatable;
import org.apache.commons.lang3.StringUtils;
import forge.game.Game;
@@ -221,15 +222,11 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
public String getDescription() {
if (hasParam("Description") && !this.isSuppressed()) {
String desc = AbilityUtils.applyDescriptionTextChangeEffects(getParam("Description"), this);
String currentName;
if (this.isIntrinsic() && cardState != null && cardState.getCard() == getHostCard()) {
currentName = cardState.getName();
} else {
currentName = getHostCard().getName();
}
desc = CardTranslation.translateSingleDescriptionText(desc, currentName);
desc = TextUtil.fastReplace(desc, "CARDNAME", CardTranslation.getTranslatedName(currentName));
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(CardTranslation.getTranslatedName(currentName)));
ITranslatable nameSource = getHostName(this);
desc = CardTranslation.translateMultipleDescriptionText(desc, nameSource);
String translatedName = CardTranslation.getTranslatedName(nameSource);
desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName);
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName));
if (desc.contains("EFFECTSOURCE")) {
desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", getHostCard().getEffectSource().toString());
}

View File

@@ -19,6 +19,7 @@ package forge.game.replacement;
import java.util.*;
import com.google.common.base.MoreObjects;
import forge.game.card.*;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -143,7 +144,7 @@ public class ReplacementHandler {
return true;
}
});
}, affectedCard != null && affectedCard.isInZone(ZoneType.Sideboard));
if (checkAgain) {
if (affectedLKI != null && affectedCard != null) {
@@ -168,6 +169,10 @@ public class ReplacementHandler {
return possibleReplacers;
}
public boolean cantHappenCheck(final ReplacementType event, final Map<AbilityKey, Object> runParams) {
return !getReplacementList(event, runParams, ReplacementLayer.CantHappen).isEmpty();
}
/**
*
* Runs any applicable replacement effects.
@@ -322,7 +327,7 @@ public class ReplacementHandler {
replacementEffect.getParam("OptionalDecider"), effectSA).get(0);
}
String name = CardTranslation.getTranslatedName(host.getCardForUi().getName());
String name = CardTranslation.getTranslatedName(MoreObjects.firstNonNull(host.getCardForUi(), host).getName());
String effectDesc = TextUtil.fastReplace(replacementEffect.getDescription(), "CARDNAME", name);
final String question = runParams.containsKey(AbilityKey.Card)
? Localizer.getInstance().getMessage("lblApplyCardReplacementEffectToCardConfirm", name, runParams.get(AbilityKey.Card).toString(), effectDesc)

View File

@@ -28,6 +28,7 @@ public enum ReplacementType {
Explore(ReplaceExplore.class),
GainLife(ReplaceGainLife.class),
GameLoss(ReplaceGameLoss.class),
GameWin(ReplaceGameWin.class),
Learn(ReplaceLearn.class),
LifeReduced(ReplaceLifeReduced.class),
LoseMana(ReplaceLoseMana.class),
@@ -43,7 +44,6 @@ public enum ReplacementType {
RollPlanarDice(ReplaceRollPlanarDice.class),
Scry(ReplaceScry.class),
SetInMotion(ReplaceSetInMotion.class),
Surveil(ReplaceSurveil.class),
Tap(ReplaceTap.class),
Transform(ReplaceTransform.class),
TurnFaceUp(ReplaceTurnFaceUp.class),

View File

@@ -409,14 +409,6 @@ public class AbilityManaPart implements java.io.Serializable {
continue;
}
if (restriction.equals("FaceDownOrTurnFaceUp")) {
if ((sa.isSpell() && sa.getHostCard().isCreature() && sa.isCastFaceDown())
|| sa.isTurnFaceUp()) {
return true;
}
continue;
}
if (restriction.equals("MorphOrManifest")) {
if ((sa.isSpell() && sa.getHostCard().isCreature() && sa.isCastFaceDown())
|| sa.isManifestUp() || sa.isMorphUp()) {

View File

@@ -21,11 +21,8 @@ import forge.card.mana.ManaCost;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import java.util.List;
import java.util.Map;
/**
@@ -63,9 +60,7 @@ public abstract class AbilityStatic extends Ability implements Cloneable {
// Initial usage is Karlov Watchdog preventing disguise/morph/cloak/manifest turning face up
if (this.isTurnFaceUp()) {
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(c);
List<ReplacementEffect> list = c.getGame().getReplacementHandler().getReplacementList
(ReplacementType.TurnFaceUp, repParams, ReplacementLayer.CantHappen);
if (!list.isEmpty()) return false;
if (c.getGame().getReplacementHandler().cantHappenCheck(ReplacementType.TurnFaceUp, repParams)) return false;
}
return this.getRestrictions().canPlay(c, this);

View File

@@ -19,7 +19,6 @@ package forge.game.spellability;
import java.util.Map;
import forge.card.CardStateName;
import forge.game.IHasSVars;
import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
@@ -107,7 +106,8 @@ public final class AbilitySub extends SpellAbility implements java.io.Serializab
@Override
protected IHasSVars getSVarFallback() {
if (getCardState() != null && getCardStateName().equals(CardStateName.RightSplit)) {
// fused or spliced
if (getRootAbility().getCardState() != getCardState()) {
return getCardState();
}
return super.getSVarFallback();

View File

@@ -17,7 +17,6 @@
*/
package forge.game.spellability;
import java.util.List;
import java.util.Map;
import forge.game.card.CardCopyService;
@@ -32,8 +31,6 @@ import forge.game.card.CardFactory;
import forge.game.cost.Cost;
import forge.game.cost.CostPayment;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.staticability.StaticAbilityCantBeCast;
import forge.game.zone.ZoneType;
@@ -216,7 +213,6 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(getHostCard());
repParams.put(AbilityKey.SpellAbility, this);
repParams.put(AbilityKey.Cause, sa);
List<ReplacementEffect> list = getHostCard().getGame().getReplacementHandler().getReplacementList(ReplacementType.Counter, repParams, ReplacementLayer.CantHappen);
return list.isEmpty();
return !getHostCard().getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Counter, repParams);
}
}

View File

@@ -552,6 +552,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return hasParam("CloakUp");
}
public boolean isUnlock() {
return hasParam("Unlock");
}
public boolean isCycling() {
return isKeyword(Keyword.CYCLING) || isKeyword(Keyword.TYPECYCLING);
}
@@ -706,7 +710,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
mana.getSourceCard().getController(), mana.getSourceCard(), null)) {
final long timestamp = host.getGame().getNextTimestamp();
final List<String> kws = Arrays.asList(mana.getAddedKeywords().split(" & "));
host.addChangedCardKeywords(kws, null, false, timestamp, 0);
host.addChangedCardKeywords(kws, null, false, timestamp, null);
if (mana.addsKeywordsUntil()) {
final GameCommand untilEOT = new GameCommand() {
private static final long serialVersionUID = -8285169579025607693L;
@@ -989,16 +993,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
String desc = node.getDescription();
if (node.getHostCard() != null) {
String currentName;
// if alternate state is viewed while card uses original
if (node.isIntrinsic() && node.cardState != null && node.cardState.getCard() == node.getHostCard()) {
currentName = node.cardState.getName();
} else {
currentName = node.getHostCard().getName();
}
desc = CardTranslation.translateMultipleDescriptionText(desc, currentName);
desc = TextUtil.fastReplace(desc, "CARDNAME", CardTranslation.getTranslatedName(currentName));
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(CardTranslation.getTranslatedName(currentName)));
ITranslatable nameSource = getHostName(node);
desc = CardTranslation.translateMultipleDescriptionText(desc, nameSource);
String translatedName = CardTranslation.getTranslatedName(nameSource);
desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName);
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName));
if (node.getOriginalHost() != null) {
desc = TextUtil.fastReplace(desc, "ORIGINALHOST", node.getOriginalHost().getName());
}
@@ -2596,7 +2595,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
public boolean hasOptionalKeywordAmount(KeywordInterface kw) {
return this.optionalKeywordAmount.contains(kw.getKeyword(), Pair.of(kw.getIdx(), kw.getStaticId()));
long staticId = kw.getStatic() == null ? 0 : kw.getStatic().getId();
return this.optionalKeywordAmount.contains(kw.getKeyword(), Pair.of(kw.getIdx(), staticId));
}
public boolean hasOptionalKeywordAmount(Keyword kw) {
return this.optionalKeywordAmount.containsRow(kw);
@@ -2606,13 +2606,15 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
public int getOptionalKeywordAmount(KeywordInterface kw) {
return ObjectUtils.firstNonNull(this.optionalKeywordAmount.get(kw.getKeyword(), Pair.of(kw.getIdx(), kw.getStaticId())), 0);
long staticId = kw.getStatic() == null ? 0 : kw.getStatic().getId();
return ObjectUtils.firstNonNull(this.optionalKeywordAmount.get(kw.getKeyword(), Pair.of(kw.getIdx(), staticId)), 0);
}
public int getOptionalKeywordAmount(Keyword kw) {
return this.optionalKeywordAmount.row(kw).values().stream().mapToInt(i->i).sum();
}
public void setOptionalKeywordAmount(KeywordInterface kw, int amount) {
this.optionalKeywordAmount.put(kw.getKeyword(), Pair.of(kw.getIdx(), kw.getStaticId()), amount);
long staticId = kw.getStatic() == null ? 0 : kw.getStatic().getId();
this.optionalKeywordAmount.put(kw.getKeyword(), Pair.of(kw.getIdx(), staticId), amount);
}
public void clearOptionalKeywordAmount() {
optionalKeywordAmount.clear();

View File

@@ -45,11 +45,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.CardTranslation;
import forge.util.Expressions;
import forge.util.FileSection;
import forge.util.Lang;
import forge.util.TextUtil;
import forge.util.*;
/**
* The Class StaticAbility.
@@ -187,15 +183,11 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
@Override
public final String toString() {
if (hasParam("Description") && !this.isSuppressed()) {
String currentName;
if (this.isIntrinsic() && cardState != null && cardState.getCard() == getHostCard()) {
currentName = cardState.getName();
} else {
currentName = getHostCard().getName();
}
String desc = CardTranslation.translateSingleDescriptionText(getParam("Description"), currentName);
desc = TextUtil.fastReplace(desc, "CARDNAME", CardTranslation.getTranslatedName(currentName));
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(CardTranslation.getTranslatedName(currentName)));
ITranslatable nameSource = getHostName(this);
String desc = CardTranslation.translateSingleDescriptionText(getParam("Description"), nameSource);
String translatedName = CardTranslation.getTranslatedName(nameSource);
desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName);
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName));
return desc;
} else {

View File

@@ -0,0 +1,34 @@
package forge.game.staticability;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.zone.ZoneType;
public class StaticAbilityAssignNoCombatDamage {
static String MODE = "AssignNoCombatDamage";
public static boolean assignNoCombatDamage(final Card card) {
CardCollection list = new CardCollection(card.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES));
list.add(card);
for (final Card ca : list) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions(MODE)) {
continue;
}
if (applyAssignNoCombatDamage(stAb, card)) {
return true;
}
}
}
return false;
}
public static boolean applyAssignNoCombatDamage(final StaticAbility stAb, final Card card) {
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,32 @@
package forge.game.staticability;
import forge.game.card.Card;
import forge.game.card.CardState;
import forge.game.zone.ZoneType;
public class StaticAbilityColorlessDamageSource {
static String MODE = "ColorlessDamageSource";
public static boolean colorlessDamageSource(final CardState state) {
final Card card = state.getCard();
for (final Card ca : card.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions(MODE)) {
continue;
}
if (applyColorlessDamageSource(stAb, card)) {
return true;
}
}
}
return false;
}
public static boolean applyColorlessDamageSource(final StaticAbility stAb, final Card card) {
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
return true;
}
}

View File

@@ -22,6 +22,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.GameCommand;
import forge.card.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.StaticEffect;
import forge.game.StaticEffects;
@@ -31,6 +32,7 @@ import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplacementEffect;
@@ -423,26 +425,30 @@ public final class StaticAbilityContinuous {
if (params.containsKey("AddAllCreatureTypes")) {
addAllCreatureTypes = true;
}
if (params.containsKey("RemoveSuperTypes")) {
remove.add(RemoveType.SuperTypes);
}
if (params.containsKey("RemoveCardTypes")) {
remove.add(RemoveType.CardTypes);
}
if (params.containsKey("RemoveSubTypes")) {
remove.add(RemoveType.SubTypes);
}
if (params.containsKey("RemoveLandTypes")) {
remove.add(RemoveType.LandTypes);
}
if (params.containsKey("RemoveCreatureTypes")) {
remove.add(RemoveType.CreatureTypes);
}
if (params.containsKey("RemoveArtifactTypes")) {
remove.add(RemoveType.ArtifactTypes);
}
if (params.containsKey("RemoveEnchantmentTypes")) {
remove.add(RemoveType.EnchantmentTypes);
// overwrite doesn't work without new value (e.g. Conspiracy missing choice)
if (addTypes == null || !addTypes.isEmpty()) {
if (params.containsKey("RemoveSuperTypes")) {
remove.add(RemoveType.SuperTypes);
}
if (params.containsKey("RemoveCardTypes")) {
remove.add(RemoveType.CardTypes);
}
if (params.containsKey("RemoveSubTypes")) {
remove.add(RemoveType.SubTypes);
}
if (params.containsKey("RemoveLandTypes")) {
remove.add(RemoveType.LandTypes);
}
if (params.containsKey("RemoveCreatureTypes")) {
remove.add(RemoveType.CreatureTypes);
}
if (params.containsKey("RemoveArtifactTypes")) {
remove.add(RemoveType.ArtifactTypes);
}
if (params.containsKey("RemoveEnchantmentTypes")) {
remove.add(RemoveType.EnchantmentTypes);
}
}
}
@@ -511,7 +517,7 @@ public final class StaticAbilityContinuous {
// modify players
for (final Player p : affectedPlayers) {
// add keywords
if (addKeywords != null) {
if (addKeywords != null && !addKeywords.isEmpty()) {
p.addChangedKeywords(addKeywords, removeKeywords, se.getTimestamp(), stAb.getId());
}
@@ -719,7 +725,7 @@ public final class StaticAbilityContinuous {
}
// add keywords
if (addKeywords != null || removeKeywords != null || removeAllAbilities) {
if ((addKeywords != null && !addKeywords.isEmpty()) || removeKeywords != null || removeAllAbilities) {
List<String> newKeywords = null;
if (addKeywords != null) {
newKeywords = Lists.newArrayList(addKeywords);
@@ -742,8 +748,20 @@ public final class StaticAbilityContinuous {
newKeywords.addAll(extraKeywords);
newKeywords = newKeywords.stream().map(input -> {
int reduced = 0;
if (stAb.hasParam("ReduceCost")) {
reduced = AbilityUtils.calculateAmount(hostCard, stAb.getParam("ReduceCost"), stAb);
}
if (input.contains("CardManaCost")) {
input = input.replace("CardManaCost", affectedCard.getManaCost().getShortString());
ManaCost cost;
if (reduced > 0) {
ManaCostBeingPaid mcbp = new ManaCostBeingPaid(affectedCard.getManaCost());
mcbp.decreaseGenericMana(reduced);
cost = mcbp.toManaCost();
} else {
cost = affectedCard.getManaCost();
}
input = input.replace("CardManaCost", cost.getShortString());
} else if (input.contains("ConvertedManaCost")) {
final String costcmc = Integer.toString(affectedCard.getCMC());
input = input.replace("ConvertedManaCost", costcmc);
@@ -753,7 +771,7 @@ public final class StaticAbilityContinuous {
}
affectedCard.addChangedCardKeywords(newKeywords, removeKeywords,
removeAllAbilities, se.getTimestamp(), stAb.getId(), true);
removeAllAbilities, se.getTimestamp(), stAb, true);
}
// add HIDDEN keywords
@@ -877,7 +895,7 @@ public final class StaticAbilityContinuous {
}
// add Types
if (addTypes != null || removeTypes != null || addAllCreatureTypes || !remove.isEmpty()) {
if ((addTypes != null && !addTypes.isEmpty()) || (removeTypes != null && !removeTypes.isEmpty()) || addAllCreatureTypes || !remove.isEmpty()) {
affectedCard.addChangedCardTypes(addTypes, removeTypes, addAllCreatureTypes, remove,
se.getTimestamp(), stAb.getId(), true, stAb.hasParam("CharacteristicDefining"));
}

View File

@@ -8,25 +8,6 @@ public class StaticAbilityCrewValue {
static String MODE = "CrewValue";
public static boolean hasAnyCrewValue(final Card card) {
final Game game = card.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions(MODE)) {
continue;
}
if (hasAnyCrewValue(stAb, card)) {
return true;
}
}
}
return false;
}
public static boolean hasAnyCrewValue(final StaticAbility stAb, final Card card) {
return stAb.matchesValidParam("ValidCard", card);
}
public static boolean crewsWithToughness(final Card card) {
final Game game = card.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {

View File

@@ -60,6 +60,10 @@ public class StaticAbilityPanharmonicon {
continue;
}
// it can't trigger more times than the limit allows
if (t.hasParam("GameActivationLimit") &&
t.getActivationsThisGame() + n + 1 >= Integer.parseInt(t.getParam("GameActivationLimit"))) {
break;
}
if (t.hasParam("ActivationLimit") &&
t.getActivationsThisTurn() + n + 1 >= Integer.parseInt(t.getParam("ActivationLimit"))) {
break;

Some files were not shown because too many files have changed in this diff Show More