mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 20:58:03 +00:00
@@ -794,6 +794,7 @@ public class AiAttackController {
|
|||||||
if (bAssault) {
|
if (bAssault) {
|
||||||
return prefDefender;
|
return prefDefender;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. attack planeswalkers
|
// 2. attack planeswalkers
|
||||||
List<Card> pwDefending = c.getDefendingPlaneswalkers();
|
List<Card> pwDefending = c.getDefendingPlaneswalkers();
|
||||||
if (!pwDefending.isEmpty()) {
|
if (!pwDefending.isEmpty()) {
|
||||||
@@ -801,7 +802,7 @@ public class AiAttackController {
|
|||||||
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
|
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();
|
final CardCollection defBattles = c.getDefendingBattles();
|
||||||
List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
|
List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
|
||||||
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
|
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
|
||||||
@@ -1168,10 +1169,8 @@ public class AiAttackController {
|
|||||||
attritionalAttackers.remove(attritionalAttackers.size() - 1);
|
attritionalAttackers.remove(attritionalAttackers.size() - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attackRounds += 1;
|
attackRounds++;
|
||||||
if (humanLife <= 0) {
|
doAttritionalAttack = humanLife <= 0;
|
||||||
doAttritionalAttack = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// *********************
|
// *********************
|
||||||
// end attritional attack calculation
|
// end attritional attack calculation
|
||||||
@@ -1332,74 +1331,48 @@ public class AiAttackController {
|
|||||||
return aiAggression;
|
return aiAggression;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private class SpellAbilityFactors {
|
||||||
* <p>
|
Card attacker = null;
|
||||||
* shouldAttack.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param attacker
|
|
||||||
* a {@link forge.game.card.Card} object.
|
|
||||||
* @param defenders
|
|
||||||
* a object.
|
|
||||||
* @param combat
|
|
||||||
* a {@link forge.game.combat.Combat} object.
|
|
||||||
* @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 canBeKilled = false; // indicates if the attacker can be killed
|
||||||
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
|
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 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 canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
|
||||||
boolean isWorthLessThanAllKillers = true;
|
boolean isWorthLessThanAllKillers = true;
|
||||||
boolean canBeBlocked = false;
|
boolean hasAttackEffect = false;
|
||||||
|
boolean hasCombatEffect = false;
|
||||||
|
boolean dangerousBlockersPresent = false;
|
||||||
|
boolean canTrampleOverDefenders = false;
|
||||||
int numberOfPossibleBlockers = 0;
|
int numberOfPossibleBlockers = 0;
|
||||||
|
int defPower = 0;
|
||||||
|
|
||||||
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
|
SpellAbilityFactors(Card c) {
|
||||||
if (attacker.hasSVar("NonCombatPriority") && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
attacker = c;
|
||||||
// For each level of priority, enemy has to have life as much as the creature's power
|
|
||||||
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
|
|
||||||
// the lower the priroity, the less willing the AI is to use the creature for attacking.
|
|
||||||
// TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?)
|
|
||||||
if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) {
|
|
||||||
// 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 (ComputerUtilCost.canPayCost(sa, ai, false)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// TODO Eventually The Ai will need to learn to predict if they have any use for the ability before next untap or not.
|
|
||||||
// TODO abilities that tap enemy creatures should probably only be saved if the enemy has nonzero creatures? Haste can be a threat though...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEffectiveAttacker(ai, attacker, combat, defender)) {
|
private boolean canBeBlocked() {
|
||||||
return false;
|
return numberOfPossibleBlockers > 2
|
||||||
|
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|
||||||
|
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent));
|
||||||
}
|
}
|
||||||
boolean hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasKeyword(Keyword.ANNIHILATOR);
|
|
||||||
|
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,...)
|
// 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"));
|
hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"))
|
||||||
|
|| attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT);
|
||||||
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
|
// contains only the defender's blockers that can actually block the attacker
|
||||||
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
|
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
|
||||||
|
|
||||||
boolean canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, Card::getNetToughness);
|
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
|
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
|
||||||
boolean dangerousBlockersPresent = Iterables.any(validBlockers, Predicates.or(
|
dangerousBlockersPresent = Iterables.any(validBlockers, Predicates.or(
|
||||||
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
|
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
|
||||||
CardPredicates.hasKeyword(Keyword.LIFELINK)));
|
CardPredicates.hasKeyword(Keyword.LIFELINK)));
|
||||||
|
|
||||||
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
|
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
|
||||||
int defPower = CardLists.getTotalPower(validBlockers, true, false);
|
defPower = CardLists.getTotalPower(validBlockers, true, false);
|
||||||
|
|
||||||
// look at the attacker in relation to the blockers to establish a
|
// look at the attacker in relation to the blockers to establish a
|
||||||
// number of factors about the attacking context that will be relevant
|
// number of factors about the attacking context that will be relevant
|
||||||
@@ -1422,11 +1395,9 @@ public class AiAttackController {
|
|||||||
// not record that it can't kill everything
|
// not record that it can't kill everything
|
||||||
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
|
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
|
||||||
canKillAll = false;
|
canKillAll = false;
|
||||||
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")) {
|
|
||||||
canKillAllDangerous = false;
|
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")
|
||||||
} else {
|
|| blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT) || blocker.hasKeyword(Keyword.LIFELINK)) {
|
||||||
if (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)
|
|
||||||
|| blocker.hasKeyword(Keyword.LIFELINK)) {
|
|
||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
// there is a creature that can survive an attack from this creature
|
// there is a creature that can survive an attack from this creature
|
||||||
// and combat will have negative effects
|
// and combat will have negative effects
|
||||||
@@ -1438,7 +1409,7 @@ public class AiAttackController {
|
|||||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
|
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
|
||||||
boolean attackerWillDie = defPower >= attacker.getNetToughness();
|
boolean attackerWillDie = defPower >= attacker.getNetToughness();
|
||||||
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
|
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
|
||||||
boolean noContributionToAttack = this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
|
boolean noContributionToAttack = attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
|
||||||
|
|
||||||
// We are attacking too recklessly if we can't kill a single blocker and:
|
// We are attacking too recklessly if we can't kill a single blocker and:
|
||||||
// - our creature will die for sure (chump attack)
|
// - our creature will die for sure (chump attack)
|
||||||
@@ -1451,8 +1422,8 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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)) {
|
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
canBeKilled = true;
|
canBeKilled = true;
|
||||||
@@ -1463,28 +1434,70 @@ public class AiAttackController {
|
|||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
canBeKilled = true;
|
canBeKilled = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* shouldAttack.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param attacker
|
||||||
|
* a {@link forge.game.card.Card} object.
|
||||||
|
* @param defenders
|
||||||
|
* a object.
|
||||||
|
* @param combat
|
||||||
|
* a {@link forge.game.combat.Combat} object.
|
||||||
|
* @return a boolean.
|
||||||
|
*/
|
||||||
|
public final boolean shouldAttack(final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) {
|
||||||
|
// 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
|
||||||
|
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
|
||||||
|
// the lower the priroity, the less willing the AI is to use the creature for attacking.
|
||||||
|
// TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?)
|
||||||
|
if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) {
|
||||||
|
// 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() && sa.getPayCosts().hasTapCost()) {
|
||||||
|
if (ComputerUtilCost.canPayCost(sa, ai, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// TODO Eventually The Ai will need to learn to predict if they have any use for the ability before next untap or not.
|
||||||
|
// TODO abilities that tap enemy creatures should probably only be saved if the enemy has nonzero creatures? Haste can be a threat though...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEffectiveAttacker(ai, attacker, combat, defender)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// if the creature cannot block and can kill all opponents they might as
|
||||||
// well attack, they do nothing staying back
|
// 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)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player");
|
System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player");
|
||||||
return true;
|
return true;
|
||||||
} else if (!canBeKilled && !dangerousBlockersPresent && canTrampleOverDefenders) {
|
}
|
||||||
|
if (!saf.canBeKilled && !saf.dangerousBlockersPresent && saf.canTrampleOverDefenders) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through");
|
System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through");
|
||||||
return true;
|
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
|
// decide if the creature should attack based on the prevailing strategy choice in aiAggression
|
||||||
switch (aiAggression) {
|
switch (aiAggression) {
|
||||||
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
|
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)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
|
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
|
||||||
return true;
|
return true;
|
||||||
@@ -1495,32 +1508,32 @@ public class AiAttackController {
|
|||||||
System.out.println(attacker.getName() + " = all out attacking");
|
System.out.println(attacker.getName() + " = all out attacking");
|
||||||
return true;
|
return true;
|
||||||
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
|
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
|
||||||
if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
|
if (saf.canKillAll || (saf.dangerousBlockersPresent && saf.canKillAllDangerous && !saf.canBeKilledByOne) || !saf.canBeBlocked()
|
||||||
|| (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
|
|| saf.defPower == 0) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
|
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3: // expecting to at least kill a creature of equal value or not be blocked
|
case 3: // expecting to at least kill a creature of equal value or not be blocked
|
||||||
if ((canKillAll && isWorthLessThanAllKillers)
|
if ((saf.canKillAll && saf.isWorthLessThanAllKillers)
|
||||||
|| (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|
|| (((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne)
|
||||||
|| !canBeBlocked) {
|
|| !saf.canBeBlocked()) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
|
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
|
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
|
||||||
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
|
if (!saf.canBeBlocked() || ((saf.canKillAll || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne &&
|
||||||
((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
|
((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || !saf.canBeKilled))) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
|
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1: // unblockable creatures only
|
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)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
|
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -724,7 +724,6 @@ public class ComputerUtilCombat {
|
|||||||
return totalDamageOfBlockers(attacker, blockers) >= getDamageToKill(attacker, false);
|
return totalDamageOfBlockers(attacker, blockers) >= getDamageToKill(attacker, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will this trigger trigger?
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* combatTriggerWillTrigger.
|
* combatTriggerWillTrigger.
|
||||||
|
|||||||
@@ -4097,7 +4097,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
|||||||
return state.getType();
|
return state.getType();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add changed type by card text
|
|
||||||
public Iterable<CardChangedType> getChangedCardTypes() {
|
public Iterable<CardChangedType> getChangedCardTypes() {
|
||||||
// If there are no changed types, just return an empty immutable list, which actually
|
// 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
|
// produces a surprisingly large speedup by avoid lots of temp objects and making iteration
|
||||||
@@ -5702,7 +5701,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
|||||||
|
|
||||||
// Just phased in, time to run the phased in trigger
|
// Just phased in, time to run the phased in trigger
|
||||||
getGame().getTriggerHandler().registerActiveTrigger(this, false);
|
getGame().getTriggerHandler().registerActiveTrigger(this, false);
|
||||||
getGame().getTriggerHandler().runTrigger(TriggerType.PhaseIn, runParams, false);
|
getGame().getTriggerHandler().runTrigger(TriggerType.PhaseIn, runParams, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
game.updateLastStateForCard(this);
|
game.updateLastStateForCard(this);
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ public class CardLists {
|
|||||||
public static int getTotalPower(Iterable<Card> cardList, boolean ignoreNegativePower, boolean crew) {
|
public static int getTotalPower(Iterable<Card> cardList, boolean ignoreNegativePower, boolean crew) {
|
||||||
int total = 0;
|
int total = 0;
|
||||||
for (final Card crd : cardList) {
|
for (final Card crd : cardList) {
|
||||||
if (crew && StaticAbilityCrewValue.hasAnyCrewValue(crd)) {
|
if (crew) {
|
||||||
if (StaticAbilityCrewValue.crewsWithToughness(crd)) {
|
if (StaticAbilityCrewValue.crewsWithToughness(crd)) {
|
||||||
total += ignoreNegativePower ? Math.max(0, crd.getNetToughness()) : crd.getNetToughness();
|
total += ignoreNegativePower ? Math.max(0, crd.getNetToughness()) : crd.getNetToughness();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1628,8 +1628,7 @@ public class CardProperty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (property.startsWith("attacking ")) { // generic "attacking [DefinedGameEntity]"
|
if (property.startsWith("attacking ")) { // generic "attacking [DefinedGameEntity]"
|
||||||
FCollection<GameEntity> defined = AbilityUtils.getDefinedEntities(source, property.split(" ")[1],
|
FCollection<GameEntity> defined = AbilityUtils.getDefinedEntities(source, property.split(" ")[1], spellAbility);
|
||||||
spellAbility);
|
|
||||||
final GameEntity defender = combat.getDefenderByAttacker(card);
|
final GameEntity defender = combat.getDefenderByAttacker(card);
|
||||||
if (!defined.contains(defender)) {
|
if (!defined.contains(defender)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
|
|||||||
// will be handled by original "cause" instead
|
// will be handled by original "cause" instead
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isEmpty()) {
|
||||||
// this should still refresh for empty battlefield
|
// this should still refresh for empty battlefield
|
||||||
if (lastStateBattlefield != CardCollection.EMPTY) {
|
if (lastStateBattlefield != CardCollection.EMPTY) {
|
||||||
game.getTriggerHandler().resetActiveTriggers(false);
|
game.getTriggerHandler().resetActiveTriggers(false);
|
||||||
@@ -108,7 +109,7 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
|
|||||||
game.getTriggerHandler().registerActiveLTBTrigger(lki);
|
game.getTriggerHandler().registerActiveLTBTrigger(lki);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isEmpty()) {
|
|
||||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||||
runParams.put(AbilityKey.Cards, new CardZoneTable(this));
|
runParams.put(AbilityKey.Cards, new CardZoneTable(this));
|
||||||
runParams.put(AbilityKey.Cause, cause);
|
runParams.put(AbilityKey.Cause, cause);
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ public class Untap extends Phase {
|
|||||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||||
runParams.put(AbilityKey.Map, untapMap);
|
runParams.put(AbilityKey.Map, untapMap);
|
||||||
game.getTriggerHandler().runTrigger(TriggerType.UntapAll, runParams, false);
|
game.getTriggerHandler().runTrigger(TriggerType.UntapAll, runParams, false);
|
||||||
} // end doUntap
|
}
|
||||||
|
|
||||||
private static boolean optionalUntap(final Card c) {
|
private static boolean optionalUntap(final Card c) {
|
||||||
boolean untap = true;
|
boolean untap = true;
|
||||||
@@ -301,6 +301,10 @@ public class Untap extends Phase {
|
|||||||
runParams.put(AbilityKey.Cards, phasedOut);
|
runParams.put(AbilityKey.Cards, phasedOut);
|
||||||
turn.getGame().getTriggerHandler().runTrigger(TriggerType.PhaseOutAll, runParams, false);
|
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) {
|
private static void doDayTime(final Player previous) {
|
||||||
|
|||||||
@@ -8,25 +8,6 @@ public class StaticAbilityCrewValue {
|
|||||||
|
|
||||||
static String MODE = "CrewValue";
|
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) {
|
public static boolean crewsWithToughness(final Card card) {
|
||||||
final Game game = card.getGame();
|
final Game game = card.getGame();
|
||||||
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
|
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ ManaCost:1 G
|
|||||||
Types:Snow Creature Mammoth
|
Types:Snow Creature Mammoth
|
||||||
PT:4/5
|
PT:4/5
|
||||||
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigToken | Static$ True | TriggerDescription$ CARDNAME enters with a token copy of Pacifism attached to it.
|
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigToken | Static$ True | TriggerDescription$ CARDNAME enters with a token copy of Pacifism attached to it.
|
||||||
SVar:TrigToken:DB$ Token | TokenAmount$ 1 | TokenScript$ pacifism | TokenOwner$ You | AttachedTo$ Self
|
SVar:TrigToken:DB$ CopyPermanent | DefinedName$ Pacifism | AttachedTo$ Self
|
||||||
DeckHas:Ability$Token
|
DeckHas:Ability$Token
|
||||||
Oracle:Domesticated Mammoth enters with a token copy of Pacifism attached to it.
|
Oracle:Domesticated Mammoth enters with a token copy of Pacifism attached to it.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ ManaCost:2 R R
|
|||||||
Types:Legendary Creature Human Knight
|
Types:Legendary Creature Human Knight
|
||||||
PT:4/4
|
PT:4/4
|
||||||
K:Haste
|
K:Haste
|
||||||
T:Mode$ ChangesZoneAll | Origin$ Battlefield | Destination$ Graveyard | ValidCards$ Creature.attackingLKI+Legendary+Other+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigUntapAll | ActivationLimit$ 1 | TriggerDescription$ Whenever one or more other attacking legendary creatures you control die, untap all creatures you control. After this phase, there is an additional combat phase. This ability triggers only once each turn.
|
T:Mode$ ChangesZoneAll | Origin$ Battlefield | Destination$ Graveyard | ValidCards$ Creature.attacking+Legendary+Other+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigUntapAll | ActivationLimit$ 1 | TriggerDescription$ Whenever one or more other attacking legendary creatures you control die, untap all creatures you control. After this phase, there is an additional combat phase. This ability triggers only once each turn.
|
||||||
SVar:TrigUntapAll:DB$ UntapAll | ValidCards$ Creature.YouCtrl | SubAbility$ DBAddCombat
|
SVar:TrigUntapAll:DB$ UntapAll | ValidCards$ Creature.YouCtrl | SubAbility$ DBAddCombat
|
||||||
SVar:DBAddCombat:DB$ AddPhase | ExtraPhase$ Combat | AfterPhase$ EndCombat
|
SVar:DBAddCombat:DB$ AddPhase | ExtraPhase$ Combat | AfterPhase$ EndCombat
|
||||||
DeckHints:Type$Legendary
|
DeckHints:Type$Legendary
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ T:Mode$ ChangesZone | ValidCard$ Creature.Other+YouCtrl | Origin$ Battlefield |
|
|||||||
SVar:TrigBranch:DB$ Branch | BranchConditionSVar$ X | BranchConditionSVarCompare$ GE1 | TrueSubAbility$ DBDraw | FalseSubAbility$ DBDamage
|
SVar:TrigBranch:DB$ Branch | BranchConditionSVar$ X | BranchConditionSVarCompare$ GE1 | TrueSubAbility$ DBDraw | FalseSubAbility$ DBDamage
|
||||||
SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ 1
|
SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ 1
|
||||||
SVar:DBDamage:DB$ DealDamage | Defined$ Opponent | NumDmg$ 1
|
SVar:DBDamage:DB$ DealDamage | Defined$ Opponent | NumDmg$ 1
|
||||||
SVar:X:TriggeredCard$Valid Creature.attackingLKI
|
SVar:X:TriggeredCard$Valid Creature.attacking
|
||||||
DeckHas:Ability$Sacrifice
|
DeckHas:Ability$Sacrifice
|
||||||
Oracle:Whenever another creature you control dies, draw a card if it was attacking. Otherwise, Garna, Bloodfist of Keld deals 1 damage to each opponent.
|
Oracle:Whenever another creature you control dies, draw a card if it was attacking. Otherwise, Garna, Bloodfist of Keld deals 1 damage to each opponent.
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ Name:Generated Horizons
|
|||||||
ManaCost:2 G G
|
ManaCost:2 G G
|
||||||
Types:Enchantment
|
Types:Enchantment
|
||||||
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ At the beginning of your upkeep, you create a Forest land token.
|
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ At the beginning of your upkeep, you create a Forest land token.
|
||||||
SVar:TrigToken:DB$ Token | TokenAmount$ 1 | TokenScript$ c_l_forest | TokenOwner$ You
|
SVar:TrigToken:DB$ CopyPermanent | DefinedName$ Forest
|
||||||
Oracle:At the beginning of your upkeep, create a Forest land token.
|
Oracle:At the beginning of your upkeep, create a Forest land token.
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ Name:Rootcast Apprenticeship
|
|||||||
ManaCost:3 G
|
ManaCost:3 G
|
||||||
Types:Sorcery
|
Types:Sorcery
|
||||||
A:SP$ Charm | Choices$ DBPutCounter,DBCopy,DBToken,DBSacrifice | CharmNum$ 3 | CanRepeatModes$ True
|
A:SP$ Charm | Choices$ DBPutCounter,DBCopy,DBToken,DBSacrifice | CharmNum$ 3 | CanRepeatModes$ True
|
||||||
SVar:DBPutCounter:DB$ PutCounter | ValidTgts$ Creature | CounterType$ P1P1 | CounterNum$ 3 | SpellDescription$ Put three +1/+1 counters on target creature.
|
SVar:DBPutCounter:DB$ PutCounter | ValidTgts$ Creature | CounterType$ P1P1 | CounterNum$ 2 | SpellDescription$ Put two +1/+1 counters on target creature.
|
||||||
SVar:DBCopy:DB$ CopyPermanent | ValidTgts$ Permanent.token+YouCtrl | TgtPrompt$ Select target token you control | SpellDescription$ Create a token that's a copy of target token you control.
|
SVar:DBCopy:DB$ CopyPermanent | ValidTgts$ Permanent.token+YouCtrl | TgtPrompt$ Select target token you control | SpellDescription$ Create a token that's a copy of target token you control.
|
||||||
SVar:DBToken:DB$ Token | ValidTgts$ Player | TokenAmount$ 1 | TokenScript$ g_1_1_squirrel | StackDescription$ SpellDescription | SpellDescription$ Target player creates a 1/1 green Squirrel creature token.
|
SVar:DBToken:DB$ Token | ValidTgts$ Player | TokenAmount$ 1 | TokenScript$ g_1_1_squirrel | StackDescription$ SpellDescription | SpellDescription$ Target player creates a 1/1 green Squirrel creature token.
|
||||||
SVar:DBSacrifice:DB$ Sacrifice | ValidTgts$ Opponent | SacValid$ Artifact.nonToken | SpellDescription$ Target opponent sacrifices a nontoken artifact. | SacMessage$ nontoken artifact
|
SVar:DBSacrifice:DB$ Sacrifice | ValidTgts$ Opponent | SacValid$ Artifact.nonToken | SpellDescription$ Target opponent sacrifices a nontoken artifact. | SacMessage$ nontoken artifact
|
||||||
DeckHas:Ability$Counters|Token
|
DeckHas:Ability$Counters|Token
|
||||||
Oracle:Choose three. You may choose the same mode more than once.\n• Put three +1/+1 counters on target creature.\n• Create a token that's a copy of target token you control.\n• Target player creates a 1/1 green Squirrel creature token.\n• Target opponent sacrifices a nontoken artifact.
|
Oracle:Choose three. You may choose the same mode more than once.\n• Put two +1/+1 counters on target creature.\n• Create a token that's a copy of target token you control.\n• Target player creates a 1/1 green Squirrel creature token.\n• Target opponent sacrifices a nontoken artifact.
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ ManaCost:no cost
|
|||||||
Types:Land
|
Types:Land
|
||||||
A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}.
|
A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}.
|
||||||
A:AB$ Destroy | Cost$ T Sac<1/CARDNAME> | ValidTgts$ Land.nonBasic | TgtPrompt$ Select target land. | SubAbility$ DBToken | AILogic$ GhostQuarter | AITgts$ Land.nonBasic | SpellDescription$ Destroy target nonbasic land. That land's controller creates a Wastes land token. (It has {T}: Add {C}.)
|
A:AB$ Destroy | Cost$ T Sac<1/CARDNAME> | ValidTgts$ Land.nonBasic | TgtPrompt$ Select target land. | SubAbility$ DBToken | AILogic$ GhostQuarter | AITgts$ Land.nonBasic | SpellDescription$ Destroy target nonbasic land. That land's controller creates a Wastes land token. (It has {T}: Add {C}.)
|
||||||
SVar:DBToken:DB$ Token | TokenScript$ c_l_wastes | TokenOwner$ TargetedController
|
SVar:DBToken:DB$ CopyPermanent | DefinedName$ Wastes | Controller$ TargetedController
|
||||||
DeckHas:Ability$Mana.Colorless
|
DeckHas:Ability$Mana.Colorless
|
||||||
Oracle:{T}: Add {C}.\n{T}, Sacrifice Waste Land: Destroy target nonbasic land. That land's controller creates a Wastes token. (It's a land with {T}: Add {C}.)
|
Oracle:{T}: Add {C}.\n{T}, Sacrifice Waste Land: Destroy target nonbasic land. That land's controller creates a Wastes token. (It's a land with {T}: Add {C}.)
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
Name:Forest Token
|
|
||||||
ManaCost:no cost
|
|
||||||
Types:Land Forest
|
|
||||||
Oracle:
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Name:Wastes Token
|
|
||||||
ManaCost:no cost
|
|
||||||
Types:Land
|
|
||||||
A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}.
|
|
||||||
Oracle:{T}: Add {C}.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Name:Pacifism
|
|
||||||
ManaCost:1 W
|
|
||||||
Types:Enchantment Aura
|
|
||||||
K:Enchant creature
|
|
||||||
A:SP$ Attach | Cost$ 1 W | ValidTgts$ Creature | AILogic$ Curse
|
|
||||||
S:Mode$ Continuous | Affected$ Creature.EnchantedBy | AddHiddenKeyword$ CARDNAME can't attack or block. | Description$ Enchanted creature can't attack or block.
|
|
||||||
Oracle:Enchant creature\nEnchanted creature can't attack or block.
|
|
||||||
@@ -3,6 +3,6 @@ ManaCost:no cost
|
|||||||
Types:Enchantment Aura Role
|
Types:Enchantment Aura Role
|
||||||
K:Enchant creature
|
K:Enchant creature
|
||||||
A:SP$ Attach | Cost$ 0 | ValidTgts$ Creature | AILogic$ Curse
|
A:SP$ Attach | Cost$ 0 | ValidTgts$ Creature | AILogic$ Curse
|
||||||
S:Mode$ Continuous | Affected$ Creature.EnchantedBy | SetPower$ 1 | SetToughness$ 1 | Description$ Enchanted creature is 1/1
|
S:Mode$ Continuous | Affected$ Creature.EnchantedBy | SetPower$ 1 | SetToughness$ 1 | Description$ Enchanted creature is 1/1.
|
||||||
SVar:SacMe:2
|
SVar:SacMe:2
|
||||||
Oracle:Enchant Creature\nEnchanted creature is 1/1
|
Oracle:Enchant Creature\nEnchanted creature is 1/1.
|
||||||
|
|||||||
Reference in New Issue
Block a user