mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 12:18:00 +00:00
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:
2
.github/workflows/test-build.yaml
vendored
2
.github/workflows/test-build.yaml
vendored
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -724,7 +724,6 @@ public class ComputerUtilCombat {
|
||||
return totalDamageOfBlockers(attacker, blockers) >= getDamageToKill(attacker, false);
|
||||
}
|
||||
|
||||
// Will this trigger trigger?
|
||||
/**
|
||||
* <p>
|
||||
* combatTriggerWillTrigger.
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
22
forge-core/src/main/java/forge/util/ITranslatable.java
Normal file
22
forge-core/src/main/java/forge/util/ITranslatable.java
Normal 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 "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ public class ReplaceGameLoss extends ReplacementEffect {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!matchesValidParam("ValidLoseReason", runParams.get(AbilityKey.LoseReason))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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   HashMap<String, String>
|
||||
* @param host   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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user