mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-15 02:08:00 +00:00
Compare commits
1 Commits
adv-draft-
...
migrate-sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bf567a966 |
4
.github/workflows/maven-publish.yml
vendored
4
.github/workflows/maven-publish.yml
vendored
@@ -129,9 +129,7 @@ jobs:
|
||||
makeLatest: true
|
||||
|
||||
- name: 🔧 Install XML tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
run: sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: 🔼 Bump versionCode in root POM
|
||||
id: bump_version
|
||||
|
||||
@@ -97,7 +97,6 @@ public class AiController {
|
||||
private int lastAttackAggression;
|
||||
private boolean useLivingEnd;
|
||||
private List<SpellAbility> skipped;
|
||||
private boolean timeoutReached;
|
||||
|
||||
public AiController(final Player computerPlayer, final Game game0) {
|
||||
player = computerPlayer;
|
||||
@@ -887,8 +886,27 @@ public class AiController {
|
||||
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
// Check a predefined condition
|
||||
if (sa.hasParam("AICheckSVar")) {
|
||||
final String svarToCheck = sa.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
|
||||
if (sa.hasParam("AISVarCompare")) {
|
||||
final String fullCmp = sa.getParam("AISVarCompare");
|
||||
comparator = fullCmp.substring(0, 2);
|
||||
final String strCmpTo = fullCmp.substring(2);
|
||||
try {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
|
||||
int left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
if (!Expressions.compare(left, comparator, compareTo)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
}
|
||||
}
|
||||
|
||||
// this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
@@ -906,7 +924,7 @@ public class AiController {
|
||||
|
||||
// check if enough left (pass memory indirectly because we don't want to include those)
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST);
|
||||
if (tappedForMana != null && !tappedForMana.isEmpty() &&
|
||||
if (tappedForMana != null && tappedForMana.isEmpty() &&
|
||||
!ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) {
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
@@ -1646,9 +1664,6 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
// in case of infinite loop reset below would not be reached
|
||||
timeoutReached = false;
|
||||
|
||||
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
@@ -1658,11 +1673,6 @@ public class AiController {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timeoutReached) {
|
||||
timeoutReached = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.getHostCard().hasKeyword(Keyword.STORM)
|
||||
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
|
||||
&& player.getZone(ZoneType.Hand).contains(
|
||||
@@ -1742,10 +1752,7 @@ public class AiController {
|
||||
t.stop();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||
timeoutReached = true;
|
||||
future.cancel(true);
|
||||
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
|
||||
// TODO mark some as skipped to increase chance to find something playable next priority
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1798,9 +1805,14 @@ public class AiController {
|
||||
* @param sa the sa
|
||||
* @return true, if successful
|
||||
*/
|
||||
public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) {
|
||||
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
|
||||
Card hostCard = effect.getHostCard();
|
||||
if (hostCard.hasAlternateState()) {
|
||||
hostCard = game.getCardState(hostCard);
|
||||
}
|
||||
|
||||
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
|
||||
final Player controller = host.getController();
|
||||
final Player controller = hostCard.getController();
|
||||
if (affected instanceof Player) {
|
||||
return !((Player) affected).isOpponentOf(controller);
|
||||
}
|
||||
@@ -1809,6 +1821,7 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
if (effect.hasParam("AICheckSVar")) {
|
||||
System.out.println("aiShouldRun?" + sa);
|
||||
final String svarToCheck = effect.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
@@ -1821,9 +1834,9 @@ public class AiController {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
if (sa == null) {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect);
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
|
||||
} else {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1831,12 +1844,13 @@ public class AiController {
|
||||
int left = 0;
|
||||
|
||||
if (sa == null) {
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, effect);
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
|
||||
} else {
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
|
||||
}
|
||||
System.out.println("aiShouldRun?" + left + comparator + compareTo);
|
||||
return Expressions.compare(left, comparator, compareTo);
|
||||
} else if (effect.isKeyword(Keyword.DREDGE)) {
|
||||
} else if (effect.hasParam("AICheckDredge")) {
|
||||
return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac");
|
||||
} else return sa != null && doTrigger(sa, false);
|
||||
}
|
||||
|
||||
@@ -29,15 +29,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
private final CardCollection tapped;
|
||||
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
|
||||
this(ai0, sa, effect, false);
|
||||
}
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) {
|
||||
super(ai0, effect, sa, sa.getHostCard());
|
||||
|
||||
discarded = new CardCollection();
|
||||
tapped = new CardCollection();
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
|
||||
if (!payMana && tappedForMana != null) {
|
||||
if (tappedForMana != null) {
|
||||
tapped.addAll(tappedForMana);
|
||||
}
|
||||
}
|
||||
@@ -113,7 +110,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
|
||||
}
|
||||
return PaymentDecision.card(randomSubset);
|
||||
} else if (type.contains("+WithDifferentNames")) {
|
||||
} else if (type.equals("DifferentNames")) {
|
||||
CardCollection differentNames = new CardCollection();
|
||||
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
|
||||
while (c > 0) {
|
||||
|
||||
@@ -3104,38 +3104,41 @@ public class ComputerUtil {
|
||||
|
||||
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
||||
final Card source = sa.getHostCard();
|
||||
if (source == null || !sa.hasParam("AITgts")) {
|
||||
return srcList;
|
||||
}
|
||||
if (source == null) { return srcList; }
|
||||
|
||||
CardCollection list;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
if (sa.hasParam("AITgts")) {
|
||||
CardCollection list;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
} else {
|
||||
return srcList;
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
}
|
||||
return srcList;
|
||||
}
|
||||
|
||||
|
||||
@@ -287,6 +287,10 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
|
||||
if (amount <= 0) {
|
||||
// wrong gamestate for variable amount
|
||||
@@ -353,14 +357,9 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
// these should come last since they reserve the paying cards
|
||||
// (this means if a mana ability has both parts it doesn't currently undo reservations if the second part fails)
|
||||
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma, ma.isTrigger())) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return paymentChoice;
|
||||
}
|
||||
@@ -816,11 +815,11 @@ public class ComputerUtilMana {
|
||||
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
|
||||
payMultipleMana(cost, manaProduced, ai);
|
||||
|
||||
// remove to prevent re-usage since resources don't get consumed
|
||||
// remove from available lists
|
||||
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
|
||||
} else {
|
||||
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
|
||||
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect, true))) {
|
||||
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect))) {
|
||||
saList.remove(saPayment);
|
||||
continue;
|
||||
}
|
||||
@@ -829,10 +828,8 @@ public class ComputerUtilMana {
|
||||
// subtract mana from mana pool
|
||||
manapool.payManaFromAbility(sa, cost, saPayment);
|
||||
|
||||
// need to consider if another use is now prevented
|
||||
if (!cost.isPaid() && saPayment.isActivatedAbility() && !saPayment.getRestrictions().canPlay(saPayment.getHostCard(), saPayment)) {
|
||||
sourcesForShards.values().removeIf(s -> s == saPayment);
|
||||
}
|
||||
// no need to remove abilities from resource map,
|
||||
// once their costs are paid and consume resources, they can not be used again
|
||||
|
||||
if (hasConverge) {
|
||||
// hack to prevent converge re-using sources
|
||||
@@ -1665,6 +1662,7 @@ public class ComputerUtilMana {
|
||||
if (replaced.contains("C")) {
|
||||
manaMap.put(ManaAtom.COLORLESS, m);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,11 +460,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
|
||||
Card host = replacementEffect.getHostCard();
|
||||
if (host.hasAlternateState()) {
|
||||
host = host.getGame().getCardState(host);
|
||||
}
|
||||
return brains.aiShouldRun(replacementEffect, effectSA, host, affected);
|
||||
return brains.aiShouldRun(replacementEffect, effectSA, affected);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -101,7 +101,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.getHostCard().removeSVar("AIPreferenceOverride");
|
||||
}
|
||||
|
||||
if (aiLogic.equals("SurpriseBlock")) {
|
||||
if (aiLogic.equals("BeforeCombat")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
|
||||
return false;
|
||||
}
|
||||
} else if (aiLogic.equals("SurpriseBlock")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
@@ -294,7 +298,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
try {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// This happens when Origin is something like
|
||||
// "Graveyard,Library" (Doomsday)
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
final String destination = sa.getParam("Destination");
|
||||
|
||||
@@ -761,8 +771,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
|
||||
return true;
|
||||
} else if (aiLogic.equals("BeforeCombat")) {
|
||||
return !ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN);
|
||||
}
|
||||
|
||||
if (sa.isHidden()) {
|
||||
@@ -889,6 +897,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
|
||||
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
|
||||
}
|
||||
|
||||
if (source.isInZone(ZoneType.Hand)) {
|
||||
list = CardLists.filter(list, CardPredicates.nameNotEquals(source.getName())); // Don't get the same card back.
|
||||
@@ -897,6 +908,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
|
||||
// list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1239,12 +1252,53 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// if max CMC exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetCMC")) {
|
||||
if (choice.getCMC() > sa.getTargetRestrictions().getMaxTotalCMC(choice, sa) - sa.getTargets().getTotalTargetedCMC()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if max power exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetPower")) {
|
||||
if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// honor the Same Creature Type restriction
|
||||
if (sa.getTargetRestrictions().isWithSameCreatureType()) {
|
||||
Card firstTarget = sa.getTargetCard();
|
||||
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
|
||||
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
|
||||
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
|
||||
// vs. two cards in the second player's graveyard, which cards are more relevant to be targeted, etc.). Consider improving.
|
||||
if (sa.getTargetRestrictions().isSingleZone()) {
|
||||
Card firstTgt = sa.getTargetCard();
|
||||
CardCollection toRemove = new CardCollection();
|
||||
if (firstTgt != null) {
|
||||
for (Card t : sa.getTargets().getTargetCards()) {
|
||||
if (!t.getController().equals(firstTgt.getController())) {
|
||||
toRemove.add(t);
|
||||
}
|
||||
}
|
||||
sa.getTargets().removeAll(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,10 +96,6 @@ public class CloneAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
chance = cloneTgtAI(sa);
|
||||
} else {
|
||||
if (sa.isReplacementAbility() && host.isCloned()) {
|
||||
// prevent StackOverflow from infinite loop copying another ETB RE
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
|
||||
}
|
||||
if (sa.hasParam("Choices")) {
|
||||
CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getParam("Choices"), host.getController(), host, sa);
|
||||
@@ -192,7 +188,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
|
||||
|
||||
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
|
||||
: "Permanent.YouDontCtrl+!named" + name + ",Permanent.nonLegendary+!named" + name;
|
||||
: "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name;
|
||||
|
||||
// TODO: rewrite this block so that this is done somehow more elegantly
|
||||
if (canCloneLegendary) {
|
||||
|
||||
@@ -119,7 +119,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(
|
||||
sa.isTargetNumberValid() ? 100 : 0,
|
||||
sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0,
|
||||
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,15 +53,17 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (mandatory) {
|
||||
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return decision;
|
||||
} else {
|
||||
return canPlay(aiPlayer, sa);
|
||||
if (mandatory) {
|
||||
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return decision;
|
||||
} else {
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
// Not at EOT phase
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
} else if ("DuplicatePerms".equals(aiLogic)) {
|
||||
} if ("DuplicatePerms".equals(aiLogic)) {
|
||||
final List<Card> valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if (valid.size() < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
@@ -212,7 +212,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +92,9 @@ public class CountersPutAi extends CountersAi {
|
||||
return false;
|
||||
}
|
||||
return chance > MyRandom.getRandom().nextFloat();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.isKeyword(Keyword.LEVEL_UP)) {
|
||||
@@ -123,6 +124,7 @@ public class CountersPutAi extends CountersAi {
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
CardCollection list;
|
||||
Card choice = null;
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
@@ -290,8 +292,10 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
if (willActivate) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
} else if (logic.equals("ChargeToBestCMC")) {
|
||||
return doChargeToCMCLogic(ai, sa);
|
||||
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
|
||||
@@ -344,7 +348,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (type.equals("P1P1")) {
|
||||
nPump = amount;
|
||||
}
|
||||
return FightAi.canFight(ai, sa, nPump, nPump);
|
||||
return FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
}
|
||||
|
||||
if (amountStr.equals("X")) {
|
||||
@@ -447,7 +451,6 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
CardCollection list;
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
@@ -743,7 +746,7 @@ public class CountersPutAi extends CountersAi {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParam("AILogic");
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||
@@ -762,10 +765,14 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if ("ChargeToBestCMC".equals(aiLogic)) {
|
||||
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return doChargeToCMCLogic(ai, sa);
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -789,6 +796,7 @@ public class CountersPutAi extends CountersAi {
|
||||
// things like Powder Keg, which are way too complex for the AI
|
||||
}
|
||||
} else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) {
|
||||
// can only target opponent
|
||||
PlayerCollection playerList = new PlayerCollection(IterableUtil.filter(
|
||||
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
|
||||
|
||||
@@ -803,12 +811,13 @@ public class CountersPutAi extends CountersAi {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
} else {
|
||||
if ("Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic)) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("Fight".equals(logic) || "PowerDmg".equals(logic)) {
|
||||
int nPump = 0;
|
||||
if (type.equals("P1P1")) {
|
||||
nPump = amount;
|
||||
}
|
||||
AiAbilityDecision decision = FightAi.canFight(ai, sa, nPump, nPump);
|
||||
AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
@@ -829,6 +838,7 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (mandatory) {
|
||||
// When things are mandatory, gotta handle a little differently
|
||||
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
@@ -853,7 +863,7 @@ public class CountersPutAi extends CountersAi {
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card choice;
|
||||
Card choice = null;
|
||||
|
||||
// Choose targets here:
|
||||
if (sa.isCurse()) {
|
||||
@@ -879,10 +889,10 @@ public class CountersPutAi extends CountersAi {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
if (choice != null && divided) {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
|
||||
sa.addDividedAllocation(choice, left);
|
||||
} else {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
sa.addDividedAllocation(choice, alloc);
|
||||
left -= alloc;
|
||||
}
|
||||
@@ -972,7 +982,9 @@ public class CountersPutAi extends CountersAi {
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||
|
||||
if (sa.isCurse()) {
|
||||
final boolean isCurse = sa.isCurse();
|
||||
|
||||
if (isCurse) {
|
||||
final CardCollection opponents = CardLists.filterControlledBy(options, ai.getOpponents());
|
||||
|
||||
if (!opponents.isEmpty()) {
|
||||
@@ -1198,8 +1210,9 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
if (numCtrs < optimalCMC) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
|
||||
|
||||
@@ -270,7 +270,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("Fight")) {
|
||||
return FightAi.canFight(ai, sa, 0,0);
|
||||
return FightAi.canFightAi(ai, sa, 0,0);
|
||||
} else if (logic.equals("Pump")) {
|
||||
sa.resetTargets();
|
||||
List<Card> options = CardUtil.getValidCardsToTarget(sa);
|
||||
|
||||
@@ -177,7 +177,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
* @param power bonus to power
|
||||
* @return true if fight effect should be played, false otherwise
|
||||
*/
|
||||
public static AiAbilityDecision canFight(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
AbilitySub tgtFight = sa.getSubAbility();
|
||||
|
||||
@@ -12,13 +12,15 @@ import forge.game.spellability.SpellAbility;
|
||||
public class FlipACoinAi extends SpellAbilityAi {
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#checkApiLogic(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String ailogic = sa.getParam("AILogic");
|
||||
if (ailogic.equals("PhaseOut")) {
|
||||
if (ailogic.equals("Never")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (ailogic.equals("PhaseOut")) {
|
||||
if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,12 @@ public class MillAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if (aiLogic.equals("LilianaMill")) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
|
||||
if (aiLogic.equals("Main1")) {
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|
||||
|| ComputerUtil.castSpellInMain1(ai, sa);
|
||||
} else if (aiLogic.equals("LilianaMill")) {
|
||||
// TODO convert to AICheckSVar
|
||||
// Only mill if a "Raise Dead" target is available, in case of control decks with few creatures
|
||||
return CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES).size() >= 1;
|
||||
@@ -50,10 +55,9 @@ public class MillAi extends SpellAbilityAi {
|
||||
// because they are also potentially useful for combat
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
}
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|
||||
|| ComputerUtil.castSpellInMain1(ai, sa);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
/*
|
||||
|
||||
@@ -6,10 +6,10 @@ import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
@@ -38,6 +38,9 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (blocker == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getTargets().add(blocker);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
@@ -60,6 +63,11 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// only use on creatures that can attack
|
||||
if (!ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card attacker = source;
|
||||
if (sa.hasParam("DefinedAttacker")) {
|
||||
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedAttacker"), sa);
|
||||
@@ -73,9 +81,13 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
boolean chance = false;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
List<Card> list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true);
|
||||
if (list.isEmpty() && mandatory) {
|
||||
list = CardUtil.getValidCardsToTarget(sa);
|
||||
final List<Card> list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true);
|
||||
if (list.isEmpty()) {
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (blocker == null) {
|
||||
|
||||
@@ -33,8 +33,10 @@ public class PhasesAi extends SpellAbilityAi {
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source);
|
||||
if (isThreatened) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
@@ -453,7 +453,7 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
|
||||
if (isFight) {
|
||||
return FightAi.canFight(ai, sa, attack, defense).willingToPlay();
|
||||
return FightAi.canFightAi(ai, sa, attack, defense).willingToPlay();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -878,7 +878,7 @@ public class StaticData {
|
||||
}
|
||||
}
|
||||
}
|
||||
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
|
||||
// stream().toList() causes crash on Android, use Collectors.toList()
|
||||
List<String> NIF = new ArrayList<>(NIF_Q).stream().sorted().collect(Collectors.toList());
|
||||
List<String> CNI = new ArrayList<>(CNI_Q).stream().sorted().collect(Collectors.toList());
|
||||
List<String> TOK = new ArrayList<>(TOKEN_Q).stream().sorted().collect(Collectors.toList());
|
||||
|
||||
@@ -1018,13 +1018,16 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public static final Predicate<CardEdition> HAS_BOOSTER_BOX = edition -> edition.getBoosterBoxCount() > 0;
|
||||
|
||||
@Deprecated //Use CardEdition::hasBasicLands and a nonnull test.
|
||||
public static final Predicate<CardEdition> hasBasicLands = ed -> {
|
||||
if (ed == null) {
|
||||
// Happens for new sets with "???" code
|
||||
return false;
|
||||
}
|
||||
return ed.hasBasicLands();
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, ed.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1045,7 +1048,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public boolean hasBasicLands() {
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (this.getCardInSet(landName).isEmpty())
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -168,7 +168,21 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public boolean isTransformable() {
|
||||
return CardSplitType.Transform == getSplitType() || CardSplitType.Modal == getSplitType();
|
||||
if (CardSplitType.Transform == getSplitType()) {
|
||||
return true;
|
||||
}
|
||||
if (CardSplitType.Modal != getSplitType()) {
|
||||
return false;
|
||||
}
|
||||
for (ICardFace face : getAllFaces()) {
|
||||
for (String spell : face.getAbilities()) {
|
||||
if (spell.contains("AB$ SetState") && spell.contains("Mode$ Transform")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// TODO check keywords if needed
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ICardFace getWSpecialize() {
|
||||
@@ -324,12 +338,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
if (hasKeyword("Friends forever") && b.hasKeyword("Friends forever")) {
|
||||
legal = true; // Stranger Things Secret Lair gimmick partner commander
|
||||
}
|
||||
if (hasKeyword("Partner - Survivors") && b.hasKeyword("Partner - Survivors")) {
|
||||
legal = true; // The Last of Us Secret Lair gimmick partner commander
|
||||
}
|
||||
if (hasKeyword("Partner - Father & Son") && b.hasKeyword("Partner - Father & Son")) {
|
||||
legal = true; // God of War Secret Lair gimmick partner commander
|
||||
}
|
||||
if (hasKeyword("Choose a Background") && b.canBeBackground()
|
||||
|| b.hasKeyword("Choose a Background") && canBeBackground()) {
|
||||
@@ -348,7 +356,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty() ||
|
||||
hasKeyword("Friends forever") || hasKeyword("Choose a Background") ||
|
||||
hasKeyword("Partner - Father & Son") || hasKeyword("Partner - Survivors") ||
|
||||
hasKeyword("Doctor's companion") || isDoctor());
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package forge.card;
|
||||
import com.google.common.collect.UnmodifiableIterator;
|
||||
import forge.card.MagicColor.Color;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
import forge.util.BinaryUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -40,95 +41,25 @@ import java.util.stream.Stream;
|
||||
public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Serializable {
|
||||
private static final long serialVersionUID = 794691267379929080L;
|
||||
|
||||
// needs to be before other static
|
||||
private static final ColorSet[] cache = new ColorSet[MagicColor.ALL_COLORS + 1];
|
||||
static {
|
||||
byte COLORLESS = MagicColor.COLORLESS;
|
||||
byte WHITE = MagicColor.WHITE;
|
||||
byte BLUE = MagicColor.BLUE;
|
||||
byte BLACK = MagicColor.BLACK;
|
||||
byte RED = MagicColor.RED;
|
||||
byte GREEN = MagicColor.GREEN;
|
||||
Color C = Color.COLORLESS;
|
||||
Color W = Color.WHITE;
|
||||
Color U = Color.BLUE;
|
||||
Color B = Color.BLACK;
|
||||
Color R = Color.RED;
|
||||
Color G = Color.GREEN;
|
||||
|
||||
//colorless
|
||||
cache[COLORLESS] = new ColorSet(C);
|
||||
|
||||
//mono-color
|
||||
cache[WHITE] = new ColorSet(W);
|
||||
cache[BLUE] = new ColorSet(U);
|
||||
cache[BLACK] = new ColorSet(B);
|
||||
cache[RED] = new ColorSet(R);
|
||||
cache[GREEN] = new ColorSet(G);
|
||||
|
||||
//two-color
|
||||
cache[WHITE | BLUE] = new ColorSet(W, U);
|
||||
cache[WHITE | BLACK] = new ColorSet(W, B);
|
||||
cache[BLUE | BLACK] = new ColorSet(U, B);
|
||||
cache[BLUE | RED] = new ColorSet(U, R);
|
||||
cache[BLACK | RED] = new ColorSet(B, R);
|
||||
cache[BLACK | GREEN] = new ColorSet(B, G);
|
||||
cache[RED | GREEN] = new ColorSet(R, G);
|
||||
cache[RED | WHITE] = new ColorSet(R, W);
|
||||
cache[GREEN | WHITE] = new ColorSet(G, W);
|
||||
cache[GREEN | BLUE] = new ColorSet(G, U);
|
||||
|
||||
//three-color
|
||||
cache[WHITE | BLUE | BLACK] = new ColorSet(W, U, B);
|
||||
cache[WHITE | BLACK | GREEN] = new ColorSet(W, B, G);
|
||||
cache[BLUE | BLACK | RED] = new ColorSet(U, B, R);
|
||||
cache[BLUE | RED | WHITE] = new ColorSet(U, R, W);
|
||||
cache[BLACK | RED | GREEN] = new ColorSet(B, R, G);
|
||||
cache[BLACK | GREEN | BLUE] = new ColorSet(B, G, U);
|
||||
cache[RED | GREEN | WHITE] = new ColorSet(R, G, W);
|
||||
cache[RED | WHITE | BLACK] = new ColorSet(R, W, B);
|
||||
cache[GREEN | WHITE | BLUE] = new ColorSet(G, W, U);
|
||||
cache[GREEN | BLUE | RED] = new ColorSet(G, U, R);
|
||||
|
||||
//four-color
|
||||
cache[WHITE | BLUE | BLACK | RED] = new ColorSet(W, U, B, R);
|
||||
cache[BLUE | BLACK | RED | GREEN] = new ColorSet(U, B, R, G);
|
||||
cache[BLACK | RED | GREEN | WHITE] = new ColorSet(B, R, G, W);
|
||||
cache[RED | GREEN | WHITE | BLUE] = new ColorSet(R, G, W, U);
|
||||
cache[GREEN | WHITE | BLUE | BLACK] = new ColorSet(G, W, U, B);
|
||||
|
||||
//five-color
|
||||
cache[WHITE | BLUE | BLACK | RED | GREEN] = new ColorSet(W, U, B, R, G);
|
||||
}
|
||||
|
||||
private final Collection<Color> orderedShards;
|
||||
private final byte myColor;
|
||||
private final float orderWeight;
|
||||
private final Set<Color> enumSet;
|
||||
private final String desc;
|
||||
|
||||
private static final ColorSet[] cache = new ColorSet[32];
|
||||
|
||||
public static final ColorSet ALL_COLORS = fromMask(MagicColor.ALL_COLORS);
|
||||
public static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
|
||||
private static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
|
||||
|
||||
private ColorSet(final Color... ordered) {
|
||||
this.orderedShards = Arrays.asList(ordered);
|
||||
this.myColor = orderedShards.stream().map(Color::getColorMask).reduce((byte)0, (a, b) -> (byte)(a | b));
|
||||
private ColorSet(final byte mask) {
|
||||
this.myColor = mask;
|
||||
this.orderWeight = this.getOrderWeight();
|
||||
this.enumSet = EnumSet.copyOf(orderedShards);
|
||||
this.desc = orderedShards.stream().map(Color::getShortName).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
public static ColorSet fromMask(final int mask) {
|
||||
final int mask32 = mask & MagicColor.ALL_COLORS;
|
||||
return cache[mask32];
|
||||
}
|
||||
|
||||
public static ColorSet fromEnums(final Color... colors) {
|
||||
byte mask = 0;
|
||||
for (Color e : colors) {
|
||||
mask |= e.getColorMask();
|
||||
if (cache[mask32] == null) {
|
||||
cache[mask32] = new ColorSet((byte) mask32);
|
||||
}
|
||||
return fromMask(mask);
|
||||
return cache[mask32];
|
||||
}
|
||||
|
||||
public static ColorSet fromNames(final String... colors) {
|
||||
@@ -362,7 +293,17 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return desc;
|
||||
final ManaCostShard[] orderedShards = getOrderedShards();
|
||||
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the null color.
|
||||
*
|
||||
* @return the nullColor
|
||||
*/
|
||||
public static ColorSet getNullColor() {
|
||||
return NO_COLORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +325,16 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
|
||||
public Set<Color> toEnumSet() {
|
||||
return EnumSet.copyOf(enumSet);
|
||||
if (isColorless()) {
|
||||
return EnumSet.of(Color.COLORLESS);
|
||||
}
|
||||
List<Color> list = new ArrayList<>();
|
||||
for (Color c : Color.values()) {
|
||||
if (hasAnyColor(c.getColormask())) {
|
||||
list.add(c);
|
||||
}
|
||||
}
|
||||
return EnumSet.copyOf(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -422,12 +372,72 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Color> stream() {
|
||||
public Stream<MagicColor.Color> stream() {
|
||||
return this.toEnumSet().stream();
|
||||
}
|
||||
|
||||
//Get array of mana cost shards for color set in the proper order
|
||||
public Collection<Color> getOrderedColors() {
|
||||
return orderedShards;
|
||||
public ManaCostShard[] getOrderedShards() {
|
||||
return shardOrderLookup[myColor];
|
||||
}
|
||||
|
||||
private static final ManaCostShard[][] shardOrderLookup = new ManaCostShard[MagicColor.ALL_COLORS + 1][];
|
||||
static {
|
||||
byte COLORLESS = MagicColor.COLORLESS;
|
||||
byte WHITE = MagicColor.WHITE;
|
||||
byte BLUE = MagicColor.BLUE;
|
||||
byte BLACK = MagicColor.BLACK;
|
||||
byte RED = MagicColor.RED;
|
||||
byte GREEN = MagicColor.GREEN;
|
||||
ManaCostShard C = ManaCostShard.COLORLESS;
|
||||
ManaCostShard W = ManaCostShard.WHITE;
|
||||
ManaCostShard U = ManaCostShard.BLUE;
|
||||
ManaCostShard B = ManaCostShard.BLACK;
|
||||
ManaCostShard R = ManaCostShard.RED;
|
||||
ManaCostShard G = ManaCostShard.GREEN;
|
||||
|
||||
//colorless
|
||||
shardOrderLookup[COLORLESS] = new ManaCostShard[] { C };
|
||||
|
||||
//mono-color
|
||||
shardOrderLookup[WHITE] = new ManaCostShard[] { W };
|
||||
shardOrderLookup[BLUE] = new ManaCostShard[] { U };
|
||||
shardOrderLookup[BLACK] = new ManaCostShard[] { B };
|
||||
shardOrderLookup[RED] = new ManaCostShard[] { R };
|
||||
shardOrderLookup[GREEN] = new ManaCostShard[] { G };
|
||||
|
||||
//two-color
|
||||
shardOrderLookup[WHITE | BLUE] = new ManaCostShard[] { W, U };
|
||||
shardOrderLookup[WHITE | BLACK] = new ManaCostShard[] { W, B };
|
||||
shardOrderLookup[BLUE | BLACK] = new ManaCostShard[] { U, B };
|
||||
shardOrderLookup[BLUE | RED] = new ManaCostShard[] { U, R };
|
||||
shardOrderLookup[BLACK | RED] = new ManaCostShard[] { B, R };
|
||||
shardOrderLookup[BLACK | GREEN] = new ManaCostShard[] { B, G };
|
||||
shardOrderLookup[RED | GREEN] = new ManaCostShard[] { R, G };
|
||||
shardOrderLookup[RED | WHITE] = new ManaCostShard[] { R, W };
|
||||
shardOrderLookup[GREEN | WHITE] = new ManaCostShard[] { G, W };
|
||||
shardOrderLookup[GREEN | BLUE] = new ManaCostShard[] { G, U };
|
||||
|
||||
//three-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK] = new ManaCostShard[] { W, U, B };
|
||||
shardOrderLookup[WHITE | BLACK | GREEN] = new ManaCostShard[] { W, B, G };
|
||||
shardOrderLookup[BLUE | BLACK | RED] = new ManaCostShard[] { U, B, R };
|
||||
shardOrderLookup[BLUE | RED | WHITE] = new ManaCostShard[] { U, R, W };
|
||||
shardOrderLookup[BLACK | RED | GREEN] = new ManaCostShard[] { B, R, G };
|
||||
shardOrderLookup[BLACK | GREEN | BLUE] = new ManaCostShard[] { B, G, U };
|
||||
shardOrderLookup[RED | GREEN | WHITE] = new ManaCostShard[] { R, G, W };
|
||||
shardOrderLookup[RED | WHITE | BLACK] = new ManaCostShard[] { R, W, B };
|
||||
shardOrderLookup[GREEN | WHITE | BLUE] = new ManaCostShard[] { G, W, U };
|
||||
shardOrderLookup[GREEN | BLUE | RED] = new ManaCostShard[] { G, U, R };
|
||||
|
||||
//four-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK | RED] = new ManaCostShard[] { W, U, B, R };
|
||||
shardOrderLookup[BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { U, B, R, G };
|
||||
shardOrderLookup[BLACK | RED | GREEN | WHITE] = new ManaCostShard[] { B, R, G, W };
|
||||
shardOrderLookup[RED | GREEN | WHITE | BLUE] = new ManaCostShard[] { R, G, W, U };
|
||||
shardOrderLookup[GREEN | WHITE | BLUE | BLACK] = new ManaCostShard[] { G, W, U, B };
|
||||
|
||||
//five-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { W, U, B, R, G };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package forge.card;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import forge.util.ITranslatable;
|
||||
import forge.util.Localizer;
|
||||
import forge.deck.DeckRecognizer;
|
||||
|
||||
/**
|
||||
* Holds byte values for each color magic has.
|
||||
@@ -159,24 +157,21 @@ public final class MagicColor {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Color implements ITranslatable {
|
||||
WHITE(Constant.WHITE, MagicColor.WHITE, "W", "lblWhite"),
|
||||
BLUE(Constant.BLUE, MagicColor.BLUE, "U", "lblBlue"),
|
||||
BLACK(Constant.BLACK, MagicColor.BLACK, "B", "lblBlack"),
|
||||
RED(Constant.RED, MagicColor.RED, "R", "lblRed"),
|
||||
GREEN(Constant.GREEN, MagicColor.GREEN, "G", "lblGreen"),
|
||||
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "C", "lblColorless");
|
||||
public enum Color {
|
||||
WHITE(Constant.WHITE, MagicColor.WHITE, "{W}"),
|
||||
BLUE(Constant.BLUE, MagicColor.BLUE, "{U}"),
|
||||
BLACK(Constant.BLACK, MagicColor.BLACK, "{B}"),
|
||||
RED(Constant.RED, MagicColor.RED, "{R}"),
|
||||
GREEN(Constant.GREEN, MagicColor.GREEN, "{G}"),
|
||||
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "{C}");
|
||||
|
||||
private final String name, shortName, symbol;
|
||||
private final String label;
|
||||
private final String name, symbol;
|
||||
private final byte colormask;
|
||||
|
||||
Color(String name0, byte colormask0, String shortName, String label) {
|
||||
Color(String name0, byte colormask0, String symbol0) {
|
||||
name = name0;
|
||||
colormask = colormask0;
|
||||
this.shortName = shortName;
|
||||
symbol = "{" + shortName + "}";
|
||||
this.label = label;
|
||||
symbol = symbol0;
|
||||
}
|
||||
|
||||
public static Color fromByte(final byte color) {
|
||||
@@ -190,25 +185,25 @@ public final class MagicColor {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
|
||||
public String getLocalizedName() {
|
||||
//Should probably move some of this logic back here, or at least to a more general location.
|
||||
return DeckRecognizer.getLocalisedMagicColorName(getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTranslatedName() {
|
||||
return Localizer.getInstance().getMessage(label);
|
||||
}
|
||||
|
||||
public byte getColorMask() {
|
||||
public byte getColormask() {
|
||||
return colormask;
|
||||
}
|
||||
public String getSymbol() {
|
||||
return symbol;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,16 +49,6 @@ public class DeckRecognizer {
|
||||
LIMITED_CARD,
|
||||
CARD_FROM_NOT_ALLOWED_SET,
|
||||
CARD_FROM_INVALID_SET,
|
||||
/**
|
||||
* Valid card request, but can't be imported because the player does not have enough copies.
|
||||
* Should be replaced with a different printing if possible.
|
||||
*/
|
||||
CARD_NOT_IN_INVENTORY,
|
||||
/**
|
||||
* Valid card request for a card that isn't in the player's inventory, but new copies can be acquired freely.
|
||||
* Usually used for basic lands. Should be supplied to the import controller by the editor.
|
||||
*/
|
||||
FREE_CARD_NOT_IN_INVENTORY,
|
||||
// Warning messages
|
||||
WARNING_MESSAGE,
|
||||
UNKNOWN_CARD,
|
||||
@@ -73,14 +63,10 @@ public class DeckRecognizer {
|
||||
CARD_TYPE,
|
||||
CARD_RARITY,
|
||||
CARD_CMC,
|
||||
MANA_COLOUR;
|
||||
|
||||
public static final EnumSet<TokenType> CARD_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY);
|
||||
public static final EnumSet<TokenType> IN_DECK_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, DECK_NAME, FREE_CARD_NOT_IN_INVENTORY);
|
||||
public static final EnumSet<TokenType> CARD_PLACEHOLDER_TOKEN_TYPES = EnumSet.of(CARD_TYPE, CARD_RARITY, CARD_CMC, MANA_COLOUR);
|
||||
MANA_COLOUR
|
||||
}
|
||||
|
||||
public enum LimitedCardType {
|
||||
public enum LimitedCardType{
|
||||
BANNED,
|
||||
RESTRICTED,
|
||||
}
|
||||
@@ -122,10 +108,6 @@ public class DeckRecognizer {
|
||||
return new Token(TokenType.CARD_FROM_INVALID_SET, count, card, cardRequestHasSetCode);
|
||||
}
|
||||
|
||||
public static Token NotInInventoryFree(final PaperCard card, final int count, final DeckSection section) {
|
||||
return new Token(TokenType.FREE_CARD_NOT_IN_INVENTORY, count, card, section, true);
|
||||
}
|
||||
|
||||
// WARNING MESSAGES
|
||||
// ================
|
||||
public static Token UnknownCard(final String cardName, final String setCode, final int count) {
|
||||
@@ -144,10 +126,6 @@ public class DeckRecognizer {
|
||||
return new Token(TokenType.WARNING_MESSAGE, msg);
|
||||
}
|
||||
|
||||
public static Token NotInInventory(final PaperCard card, final int count, final DeckSection section) {
|
||||
return new Token(TokenType.CARD_NOT_IN_INVENTORY, count, card, section, false);
|
||||
}
|
||||
|
||||
/* =================================
|
||||
* DECK SECTIONS
|
||||
* ================================= */
|
||||
@@ -261,11 +239,14 @@ public class DeckRecognizer {
|
||||
/**
|
||||
* Filters all token types that have a PaperCard instance set (not null)
|
||||
* @return true for tokens of type:
|
||||
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY.
|
||||
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET.
|
||||
* False otherwise.
|
||||
*/
|
||||
public boolean isCardToken() {
|
||||
return TokenType.CARD_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.LEGAL_CARD ||
|
||||
this.type == TokenType.LIMITED_CARD ||
|
||||
this.type == TokenType.CARD_FROM_NOT_ALLOWED_SET ||
|
||||
this.type == TokenType.CARD_FROM_INVALID_SET);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,7 +255,9 @@ public class DeckRecognizer {
|
||||
* LEGAL_CARD, LIMITED_CARD, DECK_NAME; false otherwise.
|
||||
*/
|
||||
public boolean isTokenForDeck() {
|
||||
return TokenType.IN_DECK_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.LEGAL_CARD ||
|
||||
this.type == TokenType.LIMITED_CARD ||
|
||||
this.type == TokenType.DECK_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +266,7 @@ public class DeckRecognizer {
|
||||
* False otherwise.
|
||||
*/
|
||||
public boolean isCardTokenForDeck() {
|
||||
return isCardToken() && isTokenForDeck();
|
||||
return (this.type == TokenType.LEGAL_CARD || this.type == TokenType.LIMITED_CARD);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,7 +276,10 @@ public class DeckRecognizer {
|
||||
* CARD_RARITY, CARD_CMC, CARD_TYPE, MANA_COLOUR
|
||||
*/
|
||||
public boolean isCardPlaceholder(){
|
||||
return TokenType.CARD_PLACEHOLDER_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.CARD_RARITY ||
|
||||
this.type == TokenType.CARD_CMC ||
|
||||
this.type == TokenType.MANA_COLOUR ||
|
||||
this.type == TokenType.CARD_TYPE);
|
||||
}
|
||||
|
||||
/** Determines if current token is a Deck Section token
|
||||
@@ -550,7 +536,7 @@ public class DeckRecognizer {
|
||||
PaperCard tokenCard = token.getCard();
|
||||
|
||||
if (isAllowed(tokenSection)) {
|
||||
if (tokenSection != referenceDeckSectionInParsing) {
|
||||
if (!tokenSection.equals(referenceDeckSectionInParsing)) {
|
||||
Token sectionToken = Token.DeckSection(tokenSection.name(), this.allowedDeckSections);
|
||||
// just check that last token is stack is a card placeholder.
|
||||
// In that case, add the new section token before the placeholder
|
||||
@@ -589,7 +575,7 @@ public class DeckRecognizer {
|
||||
refLine = purgeAllLinks(refLine);
|
||||
|
||||
String line;
|
||||
if (refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER))
|
||||
if (StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER))
|
||||
line = refLine.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, "");
|
||||
else
|
||||
line = refLine.trim(); // Remove any trailing formatting
|
||||
@@ -598,7 +584,7 @@ public class DeckRecognizer {
|
||||
// Final fantasy cards like Summon: Choco/Mog should be ommited to be recognized. TODO: fix maybe for future cards
|
||||
if (!line.contains("Summon:"))
|
||||
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
|
||||
if (line.startsWith(ASTERISK)) // Markdown lists (tappedout md export)
|
||||
if (StringUtils.startsWith(line, ASTERISK)) // markdown lists (tappedout md export)
|
||||
line = line.substring(2);
|
||||
|
||||
// == Patches to Corner Cases
|
||||
@@ -614,8 +600,8 @@ public class DeckRecognizer {
|
||||
Token result = recogniseCardToken(line, referenceSection);
|
||||
if (result == null)
|
||||
result = recogniseNonCardToken(line);
|
||||
return result != null ? result : refLine.startsWith(DOUBLE_SLASH) ||
|
||||
refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
|
||||
return result != null ? result : StringUtils.startsWith(refLine, DOUBLE_SLASH) ||
|
||||
StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
|
||||
new Token(TokenType.COMMENT, 0, refLine) : new Token(TokenType.UNKNOWN_TEXT, 0, refLine);
|
||||
}
|
||||
|
||||
@@ -627,7 +613,7 @@ public class DeckRecognizer {
|
||||
while (m.find()) {
|
||||
line = line.replaceAll(m.group(), "").trim();
|
||||
}
|
||||
if (line.endsWith("()"))
|
||||
if (StringUtils.endsWith(line, "()"))
|
||||
return line.substring(0, line.length()-2);
|
||||
return line;
|
||||
}
|
||||
@@ -755,12 +741,21 @@ public class DeckRecognizer {
|
||||
// This would save tons of time in parsing Input + would also allow to return UnsupportedCardTokens beforehand
|
||||
private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection, PaperCard card){
|
||||
if (deckSec != null) {
|
||||
DeckSection cardSection = switch (deckSec.toUpperCase().trim()) {
|
||||
case "MB" -> DeckSection.Main;
|
||||
case "SB" -> DeckSection.Sideboard;
|
||||
case "CM" -> DeckSection.Commander;
|
||||
default -> DeckSection.matchingSection(card);
|
||||
};
|
||||
DeckSection cardSection;
|
||||
switch (deckSec.toUpperCase().trim()) {
|
||||
case "MB":
|
||||
cardSection = DeckSection.Main;
|
||||
break;
|
||||
case "SB":
|
||||
cardSection = DeckSection.Sideboard;
|
||||
break;
|
||||
case "CM":
|
||||
cardSection = DeckSection.Commander;
|
||||
break;
|
||||
default:
|
||||
cardSection = DeckSection.matchingSection(card);
|
||||
break;
|
||||
}
|
||||
if (cardSection.validate(card))
|
||||
return cardSection;
|
||||
}
|
||||
@@ -994,7 +989,7 @@ public class DeckRecognizer {
|
||||
private static String getMagicColourLabel(MagicColor.Color magicColor) {
|
||||
if (magicColor == null) // Multicolour
|
||||
return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour"));
|
||||
return String.format("%s %s", magicColor.getTranslatedName(), magicColor.getSymbol());
|
||||
return String.format("%s %s", magicColor.getLocalizedName(), magicColor.getSymbol());
|
||||
}
|
||||
|
||||
private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{
|
||||
@@ -1013,30 +1008,60 @@ public class DeckRecognizer {
|
||||
if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS
|
||||
|| magicColor1 == MagicColor.Color.COLORLESS)
|
||||
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
|
||||
String localisedName1 = magicColor1.getTranslatedName();
|
||||
String localisedName2 = magicColor2.getTranslatedName();
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColorMask() | magicColor2.getColorMask());
|
||||
String localisedName1 = magicColor1.getLocalizedName();
|
||||
String localisedName2 = magicColor2.getLocalizedName();
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
|
||||
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
|
||||
}
|
||||
|
||||
private static MagicColor.Color getMagicColor(String colorName){
|
||||
if (colorName.toLowerCase().startsWith("multi") || colorName.equalsIgnoreCase("m"))
|
||||
return null; // will be handled separately
|
||||
return MagicColor.Color.fromByte(MagicColor.fromName(colorName.toLowerCase()));
|
||||
|
||||
byte color = MagicColor.fromName(colorName.toLowerCase());
|
||||
switch (color) {
|
||||
case MagicColor.WHITE:
|
||||
return MagicColor.Color.WHITE;
|
||||
case MagicColor.BLUE:
|
||||
return MagicColor.Color.BLUE;
|
||||
case MagicColor.BLACK:
|
||||
return MagicColor.Color.BLACK;
|
||||
case MagicColor.RED:
|
||||
return MagicColor.Color.RED;
|
||||
case MagicColor.GREEN:
|
||||
return MagicColor.Color.GREEN;
|
||||
default:
|
||||
return MagicColor.Color.COLORLESS;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLocalisedMagicColorName(String colorName){
|
||||
Localizer localizer = Localizer.getInstance();
|
||||
return switch (colorName.toLowerCase()) {
|
||||
case MagicColor.Constant.WHITE -> localizer.getMessage("lblWhite");
|
||||
case MagicColor.Constant.BLUE -> localizer.getMessage("lblBlue");
|
||||
case MagicColor.Constant.BLACK -> localizer.getMessage("lblBlack");
|
||||
case MagicColor.Constant.RED -> localizer.getMessage("lblRed");
|
||||
case MagicColor.Constant.GREEN -> localizer.getMessage("lblGreen");
|
||||
case MagicColor.Constant.COLORLESS -> localizer.getMessage("lblColorless");
|
||||
case "multicolour", "multicolor" -> localizer.getMessage("lblMulticolor");
|
||||
default -> "";
|
||||
};
|
||||
switch(colorName.toLowerCase()){
|
||||
case MagicColor.Constant.WHITE:
|
||||
return localizer.getMessage("lblWhite");
|
||||
|
||||
case MagicColor.Constant.BLUE:
|
||||
return localizer.getMessage("lblBlue");
|
||||
|
||||
case MagicColor.Constant.BLACK:
|
||||
return localizer.getMessage("lblBlack");
|
||||
|
||||
case MagicColor.Constant.RED:
|
||||
return localizer.getMessage("lblRed");
|
||||
|
||||
case MagicColor.Constant.GREEN:
|
||||
return localizer.getMessage("lblGreen");
|
||||
|
||||
case MagicColor.Constant.COLORLESS:
|
||||
return localizer.getMessage("lblColorless");
|
||||
case "multicolour":
|
||||
case "multicolor":
|
||||
return localizer.getMessage("lblMulticolor");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1055,6 +1080,37 @@ public class DeckRecognizer {
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
private static Pair<String, String> getManaNameAndSymbol(String matchedMana) {
|
||||
if (matchedMana == null)
|
||||
return null;
|
||||
|
||||
Localizer localizer = Localizer.getInstance();
|
||||
switch (matchedMana.toLowerCase()) {
|
||||
case MagicColor.Constant.WHITE:
|
||||
case "w":
|
||||
return Pair.of(localizer.getMessage("lblWhite"), MagicColor.Color.WHITE.getSymbol());
|
||||
case MagicColor.Constant.BLUE:
|
||||
case "u":
|
||||
return Pair.of(localizer.getMessage("lblBlue"), MagicColor.Color.BLUE.getSymbol());
|
||||
case MagicColor.Constant.BLACK:
|
||||
case "b":
|
||||
return Pair.of(localizer.getMessage("lblBlack"), MagicColor.Color.BLACK.getSymbol());
|
||||
case MagicColor.Constant.RED:
|
||||
case "r":
|
||||
return Pair.of(localizer.getMessage("lblRed"), MagicColor.Color.RED.getSymbol());
|
||||
case MagicColor.Constant.GREEN:
|
||||
case "g":
|
||||
return Pair.of(localizer.getMessage("lblGreen"), MagicColor.Color.GREEN.getSymbol());
|
||||
case MagicColor.Constant.COLORLESS:
|
||||
case "c":
|
||||
return Pair.of(localizer.getMessage("lblColorless"), MagicColor.Color.COLORLESS.getSymbol());
|
||||
default: // Multicolour
|
||||
return Pair.of(localizer.getMessage("lblMulticolor"), "");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDeckName(final String lineAsIs) {
|
||||
if (lineAsIs == null)
|
||||
return false;
|
||||
|
||||
@@ -52,4 +52,9 @@ public interface IPaperCard extends InventoryItem, Serializable {
|
||||
default String getUntranslatedType() {
|
||||
return getRules().getType().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getUntranslatedOracle() {
|
||||
return getRules().getOracleText();
|
||||
}
|
||||
}
|
||||
@@ -593,7 +593,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
|
||||
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
|
||||
if(markedColors == null)
|
||||
markedColors = ColorSet.NO_COLORS;
|
||||
markedColors = ColorSet.getNullColor();
|
||||
return new PaperCardFlags(this, markedColors, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
return false;
|
||||
CardSplitType cst = this.cardRules.getSplitType();
|
||||
//expand this on future for other tokens that has other backsides besides transform..
|
||||
return cst == CardSplitType.Transform || cst == CardSplitType.Modal;
|
||||
return cst == CardSplitType.Transform;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -633,10 +633,7 @@ public class BoosterGenerator {
|
||||
System.out.println("Parsing from main code: " + mainCode);
|
||||
String sheetName = StringUtils.strip(mainCode.substring(10), "()\" ");
|
||||
System.out.println("Attempting to lookup: " + sheetName);
|
||||
PrintSheet fromSheet = tryGetStaticSheet(sheetName);
|
||||
if (fromSheet == null)
|
||||
throw new RuntimeException("PrintSheet Error: " + ps.getName() + " didn't find " + sheetName + " from " + mainCode);
|
||||
src = fromSheet.toFlatList();
|
||||
src = tryGetStaticSheet(sheetName).toFlatList();
|
||||
setPred = x -> true;
|
||||
|
||||
} else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet
|
||||
|
||||
@@ -10,11 +10,13 @@ public interface ITranslatable extends IHasName {
|
||||
default String getUntranslatedName() {
|
||||
return getName();
|
||||
}
|
||||
default String getTranslatedName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
default String getUntranslatedType() {
|
||||
return "";
|
||||
}
|
||||
|
||||
default String getUntranslatedOracle() {
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-logback</artifactId>
|
||||
<version>8.21.1</version>
|
||||
<version>8.19.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
|
||||
@@ -62,9 +62,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
|
||||
/** Keys of descriptive (text) parameters. */
|
||||
private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder()
|
||||
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription")
|
||||
.add("ChangeTypeDesc")
|
||||
.build();
|
||||
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription").build();
|
||||
|
||||
/**
|
||||
* Keys that should not changed
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ForgeScript {
|
||||
boolean withSource = property.endsWith("Source");
|
||||
final ColorSet colors;
|
||||
if (withSource && StaticAbilityColorlessDamageSource.colorlessDamageSource(cardState)) {
|
||||
colors = ColorSet.NO_COLORS;
|
||||
colors = ColorSet.getNullColor();
|
||||
} else {
|
||||
colors = cardState.getCard().getColor(cardState);
|
||||
}
|
||||
@@ -166,6 +166,8 @@ public class ForgeScript {
|
||||
Card source, CardTraitBase spellAbility) {
|
||||
if (property.equals("ManaAbility")) {
|
||||
return sa.isManaAbility();
|
||||
} else if (property.equals("nonManaAbility")) {
|
||||
return !sa.isManaAbility();
|
||||
} else if (property.equals("withoutXCost")) {
|
||||
return !sa.costHasManaX();
|
||||
} else if (property.startsWith("XCost")) {
|
||||
@@ -410,8 +412,6 @@ public class ForgeScript {
|
||||
return !sa.isPwAbility() && !sa.getRestrictions().isSorcerySpeed();
|
||||
}
|
||||
return true;
|
||||
} else if(property.startsWith("NamedAbility")) {
|
||||
return sa.getName().equals(property.substring(12));
|
||||
} else if (sa.getHostCard() != null) {
|
||||
return sa.getHostCard().hasProperty(property, sourceController, source, spellAbility);
|
||||
}
|
||||
|
||||
@@ -845,8 +845,6 @@ public class Game {
|
||||
p.revealFaceDownCards();
|
||||
}
|
||||
|
||||
// TODO free any mindslaves
|
||||
|
||||
for (Card c : cards) {
|
||||
// CR 800.4d if card is controlled by opponent, LTB should trigger
|
||||
if (c.getOwner().equals(p) && c.getController().equals(p)) {
|
||||
@@ -882,6 +880,8 @@ public class Game {
|
||||
}
|
||||
triggerList.put(c.getZone().getZoneType(), null, c);
|
||||
getAction().ceaseToExist(c, false);
|
||||
// CR 603.2f owner of trigger source lost game
|
||||
getTriggerHandler().clearDelayedTrigger(c);
|
||||
}
|
||||
} else {
|
||||
// return stolen permanents
|
||||
|
||||
@@ -57,8 +57,6 @@ import forge.item.PaperCard;
|
||||
import forge.util.*;
|
||||
import forge.util.collect.FCollection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles;
|
||||
import org.jgrapht.graph.DefaultDirectedGraph;
|
||||
@@ -222,6 +220,10 @@ public class GameAction {
|
||||
//copied.setGamePieceType(GamePieceType.COPIED_SPELL);
|
||||
}
|
||||
|
||||
if (c.isTransformed()) {
|
||||
copied.incrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
|
||||
copied.setCastSA(cause);
|
||||
copied.setSplitStateToPlayAbility(cause);
|
||||
@@ -751,29 +753,26 @@ public class GameAction {
|
||||
|
||||
public final Card moveTo(final ZoneType name, final Card c, final int libPosition, SpellAbility cause, Map<AbilityKey, Object> params) {
|
||||
// Call specific functions to set PlayerZone, then move onto moveTo
|
||||
try {
|
||||
return switch (name) {
|
||||
case Hand -> moveToHand(c, cause, params);
|
||||
case Library -> moveToLibrary(c, libPosition, cause, params);
|
||||
case Battlefield -> moveToPlay(c, c.getController(), cause, params);
|
||||
case Graveyard -> moveToGraveyard(c, cause, params);
|
||||
case Exile -> !c.canExiledBy(cause, true) ? null : exile(c, cause, params);
|
||||
case Stack -> moveToStack(c, cause, params);
|
||||
case PlanarDeck, SchemeDeck, AttractionDeck, ContraptionDeck -> moveToVariantDeck(c, name, libPosition, cause, params);
|
||||
case Junkyard -> moveToJunkyard(c, cause, params);
|
||||
default -> moveTo(c.getOwner().getZone(name), c, cause); // sideboard will also get there
|
||||
};
|
||||
} catch (Exception e) {
|
||||
String msg = "GameAction:moveTo: Exception occured";
|
||||
|
||||
Breadcrumb bread = new Breadcrumb(msg);
|
||||
bread.setData("Card", c.getName());
|
||||
bread.setData("SA", cause.toString());
|
||||
bread.setData("ZoneType", name.name());
|
||||
bread.setData("Player", c.getOwner());
|
||||
Sentry.addBreadcrumb(bread);
|
||||
|
||||
throw new RuntimeException("Error in GameAction moveTo " + c.getName() + " to Player Zone " + name.name(), e);
|
||||
switch(name) {
|
||||
case Hand: return moveToHand(c, cause, params);
|
||||
case Library: return moveToLibrary(c, libPosition, cause, params);
|
||||
case Battlefield: return moveToPlay(c, c.getController(), cause, params);
|
||||
case Graveyard: return moveToGraveyard(c, cause, params);
|
||||
case Exile:
|
||||
if (!c.canExiledBy(cause, true)) {
|
||||
return null;
|
||||
}
|
||||
return exile(c, cause, params);
|
||||
case Stack: return moveToStack(c, cause, params);
|
||||
case PlanarDeck:
|
||||
case SchemeDeck:
|
||||
case AttractionDeck:
|
||||
case ContraptionDeck:
|
||||
return moveToVariantDeck(c, name, libPosition, cause, params);
|
||||
case Junkyard:
|
||||
return moveToJunkyard(c, cause, params);
|
||||
default: // sideboard will also get there
|
||||
return moveTo(c.getOwner().getZone(name), c, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,7 +974,6 @@ public class GameAction {
|
||||
// in some corner cases there's no zone yet (copied spell that failed targeting)
|
||||
if (z != null) {
|
||||
z.remove(c);
|
||||
c.setZone(c.getOwner().getZone(ZoneType.None));
|
||||
if (z.is(ZoneType.Battlefield)) {
|
||||
c.runLeavesPlayCommands();
|
||||
}
|
||||
@@ -1602,7 +1600,9 @@ public class GameAction {
|
||||
}
|
||||
|
||||
// recheck the game over condition at this point to make sure no other win conditions apply now.
|
||||
checkGameOverCondition();
|
||||
if (!game.isGameOver()) {
|
||||
checkGameOverCondition();
|
||||
}
|
||||
|
||||
if (game.getAge() != GameStage.Play) {
|
||||
return false;
|
||||
@@ -1883,10 +1883,6 @@ public class GameAction {
|
||||
}
|
||||
|
||||
public void checkGameOverCondition() {
|
||||
if (game.isGameOver()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// award loses as SBE
|
||||
GameEndReason reason = null;
|
||||
List<Player> losers = null;
|
||||
|
||||
@@ -993,6 +993,9 @@ public final class GameActionUtil {
|
||||
oldCard.setBackSide(false);
|
||||
oldCard.setState(oldCard.getFaceupCardStateName(), true);
|
||||
oldCard.unanimateBestow();
|
||||
if (ability.isDisturb() || ability.hasParam("CastTransformed")) {
|
||||
oldCard.undoIncrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
if (ability.hasParam("Prototype")) {
|
||||
oldCard.removeCloneState(oldCard.getPrototypeTimestamp());
|
||||
|
||||
@@ -34,17 +34,15 @@ import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.event.GameEventCardAttachment;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.keyword.KeywordWithType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityCantAttach;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Lang;
|
||||
|
||||
public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
protected int id;
|
||||
@@ -198,12 +196,14 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
public final void addAttachedCard(final Card c) {
|
||||
if (attachedCards.add(c)) {
|
||||
updateAttachedCards();
|
||||
getGame().fireEvent(new GameEventCardAttachment(c, null, this));
|
||||
}
|
||||
}
|
||||
|
||||
public final void removeAttachedCard(final Card c) {
|
||||
if (attachedCards.remove(c)) {
|
||||
updateAttachedCards();
|
||||
getGame().fireEvent(new GameEventCardAttachment(c, this, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,83 +221,63 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
return canBeAttached(attach, sa, false);
|
||||
}
|
||||
public boolean canBeAttached(final Card attach, SpellAbility sa, boolean checkSBA) {
|
||||
return cantBeAttachedMsg(attach, sa, checkSBA) == null;
|
||||
}
|
||||
|
||||
public String cantBeAttachedMsg(final Card attach, SpellAbility sa) {
|
||||
return cantBeAttachedMsg(attach, sa, false);
|
||||
}
|
||||
public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) {
|
||||
if (!attach.isAttachment()) {
|
||||
return attach.getName() + " is not an attachment";
|
||||
}
|
||||
if (equals(attach)) {
|
||||
return attach.getName() + " can't attach to itself";
|
||||
}
|
||||
|
||||
if (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) {
|
||||
return attach.getName() + " is a creature without reconfigure";
|
||||
// master mode
|
||||
if (!attach.isAttachment() || (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE))
|
||||
|| equals(attach)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attach.isPhasedOut()) {
|
||||
return attach.getName() + " is phased out";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attach.isAura()) {
|
||||
String msg = cantBeEnchantedByMsg(attach);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
}
|
||||
// check for rules
|
||||
if (attach.isAura() && !canBeEnchantedBy(attach)) {
|
||||
return false;
|
||||
}
|
||||
if (attach.isEquipment()) {
|
||||
String msg = cantBeEquippedByMsg(attach, sa);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
}
|
||||
if (attach.isEquipment() && !canBeEquippedBy(attach, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (attach.isFortification()) {
|
||||
String msg = cantBeFortifiedByMsg(attach);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
}
|
||||
if (attach.isFortification() && !canBeFortifiedBy(attach)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticAbility stAb = StaticAbilityCantAttach.cantAttach(this, attach, checkSBA);
|
||||
if (stAb != null) {
|
||||
return stAb.toString();
|
||||
// check for can't attach static
|
||||
if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
// true for all
|
||||
return true;
|
||||
}
|
||||
|
||||
protected String cantBeEquippedByMsg(final Card aura, SpellAbility sa) {
|
||||
protected boolean canBeEquippedBy(final Card aura, SpellAbility sa) {
|
||||
/**
|
||||
* Equip only to Creatures which are cards
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean canBeFortifiedBy(final Card aura) {
|
||||
/**
|
||||
* Equip only to Lands which are cards
|
||||
*/
|
||||
return getName() + " is not a Creature";
|
||||
return false;
|
||||
}
|
||||
|
||||
protected String cantBeFortifiedByMsg(final Card fort) {
|
||||
/**
|
||||
* Equip only to Lands which are cards
|
||||
*/
|
||||
return getName() + " is not a Land";
|
||||
}
|
||||
|
||||
protected String cantBeEnchantedByMsg(final Card aura) {
|
||||
protected boolean canBeEnchantedBy(final Card aura) {
|
||||
if (!aura.hasKeyword(Keyword.ENCHANT)) {
|
||||
return "No Enchant Keyword";
|
||||
return false;
|
||||
}
|
||||
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
|
||||
if (ki instanceof KeywordWithType kwt) {
|
||||
String v = kwt.getValidType();
|
||||
String desc = kwt.getTypeDescription();
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null)) {
|
||||
return getName() + " is not " + Lang.nounWithAmount(1, desc);
|
||||
}
|
||||
String k = ki.getOriginal();
|
||||
String m[] = k.split(":");
|
||||
String v = m[1];
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasCounters() {
|
||||
|
||||
@@ -239,10 +239,6 @@ public final class AbilityFactory {
|
||||
spellAbility.putParam("PrecostDesc", "Exhaust — ");
|
||||
}
|
||||
|
||||
if (mapParams.containsKey("Named")) {
|
||||
spellAbility.setName(mapParams.get("Named"));
|
||||
}
|
||||
|
||||
// *********************************************
|
||||
// set universal properties of the SpellAbility
|
||||
|
||||
@@ -363,6 +359,9 @@ public final class AbilityFactory {
|
||||
if (mapParams.containsKey("TargetUnique")) {
|
||||
abTgt.setUniqueTargets(true);
|
||||
}
|
||||
if (mapParams.containsKey("TargetsFromSingleZone")) {
|
||||
abTgt.setSingleZone(true);
|
||||
}
|
||||
if (mapParams.containsKey("TargetsWithoutSameCreatureType")) {
|
||||
abTgt.setWithoutSameCreatureType(true);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class AbilityUtils {
|
||||
private final static ImmutableList<String> cmpList = ImmutableList.of("LT", "LE", "EQ", "GE", "GT", "NE");
|
||||
|
||||
@@ -522,8 +523,6 @@ public class AbilityUtils {
|
||||
}
|
||||
} else if (calcX[0].equals("OriginalHost")) {
|
||||
val = xCount(ability.getOriginalHost(), calcX[1], ability);
|
||||
} else if (calcX[0].equals("DungeonsCompleted")) {
|
||||
val = handlePaid(player.getCompletedDungeons(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("ExiledWith")) {
|
||||
val = handlePaid(card.getExiledCards(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("Convoked")) {
|
||||
@@ -1871,14 +1870,6 @@ public class AbilityUtils {
|
||||
}
|
||||
return doXMath(v, expr, c, ctb);
|
||||
}
|
||||
|
||||
// Count$FromNamedAbility[abilityName].<True>.<False>
|
||||
if (sq[0].startsWith("FromNamedAbility")) {
|
||||
String abilityNamed = sq[0].substring(16);
|
||||
SpellAbility trigSA = sa.getHostCard().getCastSA();
|
||||
boolean fromNamedAbility = trigSA != null && trigSA.getName().equals(abilityNamed);
|
||||
return doXMath(calculateAmount(c, sq[fromNamedAbility ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
} else {
|
||||
// fallback if ctb isn't a spellability
|
||||
if (sq[0].startsWith("LastStateBattlefield")) {
|
||||
@@ -2890,6 +2881,21 @@ public class AbilityUtils {
|
||||
return max;
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("DifferentCardNames_")) {
|
||||
final List<String> crdname = Lists.newArrayList();
|
||||
final String restriction = l[0].substring(19);
|
||||
CardCollection list = CardLists.getValidCards(game.getCardsInGame(), restriction, player, c, ctb);
|
||||
// TODO rewrite with sharesName to respect Spy Kit
|
||||
for (final Card card : list) {
|
||||
String name = card.getName();
|
||||
// CR 201.2b Those objects have different names only if each of them has at least one name and no two objects in that group have a name in common
|
||||
if (!crdname.contains(name) && !name.isEmpty()) {
|
||||
crdname.add(name);
|
||||
}
|
||||
}
|
||||
return doXMath(crdname.size(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("MostProminentCreatureType")) {
|
||||
String restriction = l[0].split(" ")[1];
|
||||
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
|
||||
@@ -2904,6 +2910,13 @@ public class AbilityUtils {
|
||||
}
|
||||
|
||||
// TODO move below to handlePaid
|
||||
if (sq[0].startsWith("SumPower")) {
|
||||
final String[] restrictions = l[0].split("_");
|
||||
int sumPower = game.getCardsIn(ZoneType.Battlefield).stream()
|
||||
.filter(CardPredicates.restriction(restrictions[1], player, c, ctb))
|
||||
.mapToInt(Card::getNetPower).sum();
|
||||
return doXMath(sumPower, expr, c, ctb);
|
||||
}
|
||||
if (sq[0].startsWith("DifferentPower_")) {
|
||||
final String restriction = l[0].substring(15);
|
||||
final int uniquePowers = (int) game.getCardsIn(ZoneType.Battlefield).stream()
|
||||
@@ -3423,7 +3436,6 @@ public class AbilityUtils {
|
||||
}
|
||||
|
||||
public static int playerXProperty(final Player player, final String s, final Card source, CardTraitBase ctb) {
|
||||
|
||||
final String[] l = s.split("/");
|
||||
final String m = CardFactoryUtil.extractOperators(s);
|
||||
|
||||
@@ -3610,10 +3622,46 @@ public class AbilityUtils {
|
||||
return doXMath(player.hasBeenDealtCombatDamageSinceLastTurn() ? 1 : 0, m, source, ctb);
|
||||
}
|
||||
|
||||
if (value.equals("DungeonsCompleted")) {
|
||||
return doXMath(player.getCompletedDungeons().size(), m, source, ctb);
|
||||
}
|
||||
|
||||
if (value.equals("RingTemptedYou")) {
|
||||
return doXMath(player.getNumRingTemptedYou(), m, source, ctb);
|
||||
}
|
||||
|
||||
if (value.startsWith("DungeonCompletedNamed")) {
|
||||
String [] full = value.split("_");
|
||||
String name = full[1];
|
||||
int completed = 0;
|
||||
List<Card> dungeons = player.getCompletedDungeons();
|
||||
for (Card c : dungeons) {
|
||||
if (c.getName().equals(name)) {
|
||||
++completed;
|
||||
}
|
||||
}
|
||||
return doXMath(completed, m, source, ctb);
|
||||
}
|
||||
if (value.equals("DifferentlyNamedDungeonsCompleted")) {
|
||||
int amount = 0;
|
||||
List<Card> dungeons = player.getCompletedDungeons();
|
||||
for (int i = 0; i < dungeons.size(); ++i) {
|
||||
Card d1 = dungeons.get(i);
|
||||
boolean hasSameName = false;
|
||||
for (int j = i - 1; j >= 0; --j) {
|
||||
Card d2 = dungeons.get(j);
|
||||
if (d1.getName().equals(d2.getName())) {
|
||||
hasSameName = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasSameName) {
|
||||
++amount;
|
||||
}
|
||||
}
|
||||
return doXMath(amount, m, source, ctb);
|
||||
}
|
||||
|
||||
if (value.equals("AttractionsVisitedThisTurn")) {
|
||||
return doXMath(player.getAttractionsVisitedThisTurn(), m, source, ctb);
|
||||
}
|
||||
@@ -3692,6 +3740,10 @@ public class AbilityUtils {
|
||||
return CardLists.getTotalPower(paidList, ctb);
|
||||
}
|
||||
|
||||
if (string.startsWith("SumToughness")) {
|
||||
return Aggregates.sum(paidList, Card::getNetToughness);
|
||||
}
|
||||
|
||||
if (string.startsWith("GreatestCMC")) {
|
||||
return Aggregates.max(paidList, Card::getCMC);
|
||||
}
|
||||
@@ -3700,10 +3752,6 @@ public class AbilityUtils {
|
||||
return CardUtil.getColorsFromCards(paidList).countColors();
|
||||
}
|
||||
|
||||
if (string.startsWith("DifferentCardNames")) {
|
||||
return doXMath(CardLists.getDifferentNamesCount(paidList), CardFactoryUtil.extractOperators(string), source, ctb);
|
||||
}
|
||||
|
||||
if (string.equals("DifferentColorPair")) {
|
||||
final Set<ColorSet> diffPair = new HashSet<>();
|
||||
for (final Card card : paidList) {
|
||||
|
||||
@@ -1069,7 +1069,7 @@ public abstract class SpellAbilityEffect {
|
||||
// if ability was granted use that source so they can be kept apart later
|
||||
if (cause.isCopiedTrait()) {
|
||||
exilingSource = cause.getOriginalHost();
|
||||
} else if (!cause.isSpell() && cause.getKeyword() != null && cause.getKeyword().getStatic() != null) {
|
||||
} else if (cause.getKeyword() != null && cause.getKeyword().getStatic() != null) {
|
||||
exilingSource = cause.getKeyword().getStatic().getOriginalHost();
|
||||
}
|
||||
movedCard.setExiledWith(exilingSource);
|
||||
|
||||
@@ -20,14 +20,14 @@ public class AirbendEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
final StringBuilder sb = new StringBuilder("Airbend ");
|
||||
|
||||
|
||||
Iterable<Card> tgts;
|
||||
if (sa.usesTargeting()) {
|
||||
tgts = getCardsfromTargets(sa);
|
||||
} else { // otherwise add self to list and go from there
|
||||
tgts = sa.knownDetermineDefined(sa.getParam("Defined"));
|
||||
}
|
||||
|
||||
|
||||
sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts)));
|
||||
sb.append(".");
|
||||
if (Iterables.size(tgts) > 1) {
|
||||
@@ -46,7 +46,7 @@ public class AirbendEffect extends SpellAbilityEffect {
|
||||
final Player pl = sa.getActivatingPlayer();
|
||||
|
||||
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
|
||||
|
||||
|
||||
for (Card c : getTargetCards(sa)) {
|
||||
final Card gameCard = game.getCardState(c, null);
|
||||
// gameCard is LKI in that case, the card is not in game anymore
|
||||
@@ -55,7 +55,7 @@ public class AirbendEffect extends SpellAbilityEffect {
|
||||
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!gameCard.canExiledBy(sa, true)) {
|
||||
continue;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public class AirbendEffect extends SpellAbilityEffect {
|
||||
}
|
||||
triggerList.triggerChangesZoneAll(game, sa);
|
||||
handleExiledWith(triggerList.allCards(), sa);
|
||||
|
||||
|
||||
pl.triggerElementalBend(TriggerType.Airbend);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
|
||||
if (perpetual) {
|
||||
c.addPerpetual(new PerpetualColors(timestamp, colors, overwrite));
|
||||
}
|
||||
c.addColor(colors, !overwrite, timestamp, null);
|
||||
c.addColor(colors, !overwrite, timestamp, 0, false);
|
||||
}
|
||||
|
||||
if (sa.hasParam("LeaveBattlefield")) {
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.apache.commons.lang3.tuple.Pair;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
|
||||
@@ -104,7 +103,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
final String destination = sa.getParam("Destination");
|
||||
|
||||
final int num = sa.hasParam("ChangeNum") ? AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa) : 1;
|
||||
String type = "card";
|
||||
boolean defined = false;
|
||||
if (sa.hasParam("ChangeTypeDesc")) {
|
||||
@@ -119,11 +117,12 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
type = Lang.joinHomogenous(tgts);
|
||||
defined = true;
|
||||
} else if (sa.hasParam("ChangeType") && !sa.getParam("ChangeType").equals("Card")) {
|
||||
List<String> typeList = Arrays.stream(sa.getParam("ChangeType").split(",")).map(ct -> CardType.isACardType(ct) ? ct.toLowerCase() : ct).collect(Collectors.toList());
|
||||
type = Lang.joinHomogenous(typeList, null, num == 1 ? "or" : "and/or");
|
||||
final String ct = sa.getParam("ChangeType");
|
||||
type = CardType.CoreType.isValidEnum(ct) ? ct.toLowerCase() : ct;
|
||||
}
|
||||
final String cardTag = type.contains("card") ? "" : " card";
|
||||
|
||||
final int num = sa.hasParam("ChangeNum") ? AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa) : 1;
|
||||
boolean tapped = sa.hasParam("Tapped");
|
||||
boolean attacking = sa.hasParam("Attacking");
|
||||
if (sa.isNinjutsu()) {
|
||||
@@ -153,9 +152,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
} else {
|
||||
sb.append(" for ");
|
||||
}
|
||||
if (num != 1) {
|
||||
sb.append(" up to ");
|
||||
}
|
||||
sb.append(Lang.nounWithNumeralExceptOne(num, type + cardTag)).append(", ");
|
||||
if (!sa.hasParam("NoReveal") && ZoneType.smartValueOf(destination) != null && ZoneType.smartValueOf(destination).isHidden()) {
|
||||
if (choosers.size() == 1) {
|
||||
@@ -932,7 +928,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
|
||||
List<ZoneType> origin = Lists.newArrayList();
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin.addAll(ZoneType.listValueOf(sa.getParam("Origin")));
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
}
|
||||
ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
|
||||
@@ -1478,7 +1474,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
if (ZoneType.Exile.equals(destination) && sa.hasParam("WithCountersType")) {
|
||||
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
|
||||
int cAmount = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("WithCountersAmount", "1"), sa);
|
||||
int cAmount = AbilityUtils.calculateAmount(sa.getOriginalHost(), sa.getParamOrDefault("WithCountersAmount", "1"), sa);
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
movedCard.addCounter(cType, cAmount, player, table);
|
||||
table.replaceCounterEffect(game, sa, true);
|
||||
|
||||
@@ -287,17 +287,22 @@ public class CopyPermanentEffect extends TokenEffectBase {
|
||||
int id = newOwner == null ? 0 : newOwner.getGame().nextCardId();
|
||||
// need to create a physical card first, i need the original card faces
|
||||
copy = CardFactory.getCard(original.getPaperCard(), newOwner, id, host.getGame());
|
||||
|
||||
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
// 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield,
|
||||
// the resulting token is a transforming token that has both a front face and a back face.
|
||||
// The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
|
||||
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up.
|
||||
// This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
|
||||
copy.setBackSide(original.isBackSide());
|
||||
if (original.isTransformed()) {
|
||||
copy.incrementTransformedTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
} else {
|
||||
copy.setState(copy.getCurrentStateName(), true, true);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ public class DamageResolveEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
CardDamageMap damageMap = sa.getDamageMap();
|
||||
if (damageMap == null) {
|
||||
// this can happen if damagesource was missing
|
||||
return;
|
||||
}
|
||||
CardDamageMap preventMap = sa.getPreventMap();
|
||||
GameEntityCounterTable counterTable = sa.getCounterTable();
|
||||
|
||||
|
||||
@@ -73,11 +73,6 @@ import java.util.*;
|
||||
}
|
||||
|
||||
Card made = game.getAction().moveTo(zone, c, sa, moveParams);
|
||||
if (zone.equals(ZoneType.Battlefield)) {
|
||||
if (sa.hasParam("Tapped")) {
|
||||
made.setTapped(true);
|
||||
}
|
||||
}
|
||||
if (zone.equals(ZoneType.Exile)) {
|
||||
handleExiledWith(made, sa);
|
||||
if (sa.hasParam("ExileFaceDown")) {
|
||||
|
||||
@@ -47,7 +47,7 @@ public class EarthbendEffect extends SpellAbilityEffect {
|
||||
final Game game = source.getGame();
|
||||
final Player pl = sa.getActivatingPlayer();
|
||||
int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa);
|
||||
|
||||
|
||||
long ts = game.getNextTimestamp();
|
||||
|
||||
String desc = "When it dies or is exiled, return it to the battlefield tapped.";
|
||||
@@ -59,17 +59,17 @@ public class EarthbendEffect extends SpellAbilityEffect {
|
||||
c.addNewPT(0, 0, ts, 0);
|
||||
c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false);
|
||||
c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null);
|
||||
|
||||
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
c.addCounter(CounterEnumType.P1P1, num, pl, table);
|
||||
table.replaceCounterEffect(game, sa, true);
|
||||
|
||||
|
||||
buildTrigger(sa, c, sbTrigA, "Graveyard");
|
||||
buildTrigger(sa, c, sbTrigB, "Exile");
|
||||
}
|
||||
pl.triggerElementalBend(TriggerType.Earthbend);
|
||||
}
|
||||
|
||||
|
||||
protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
|
||||
@@ -269,22 +269,22 @@ public class EffectEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
|
||||
// Set Chosen Color(s)
|
||||
if (hostCard.hasChosenColor()) {
|
||||
eff.setChosenColors(Lists.newArrayList(hostCard.getChosenColors()));
|
||||
}
|
||||
|
||||
// Set Chosen Cards
|
||||
if (hostCard.hasChosenCard()) {
|
||||
eff.setChosenCards(hostCard.getChosenCards());
|
||||
}
|
||||
|
||||
// Set Chosen Player
|
||||
if (hostCard.hasChosenPlayer()) {
|
||||
eff.setChosenPlayer(hostCard.getChosenPlayer());
|
||||
}
|
||||
|
||||
if (hostCard.getChosenDirection() != null) {
|
||||
eff.setChosenDirection(hostCard.getChosenDirection());
|
||||
}
|
||||
|
||||
// Set Chosen Type
|
||||
if (hostCard.hasChosenType()) {
|
||||
eff.setChosenType(hostCard.getChosenType());
|
||||
}
|
||||
@@ -292,10 +292,12 @@ public class EffectEffect extends SpellAbilityEffect {
|
||||
eff.setChosenType2(hostCard.getChosenType2());
|
||||
}
|
||||
|
||||
// Set Chosen name
|
||||
if (hostCard.hasNamedCard()) {
|
||||
eff.setNamedCards(Lists.newArrayList(hostCard.getNamedCards()));
|
||||
}
|
||||
|
||||
// chosen number
|
||||
if (sa.hasParam("SetChosenNumber")) {
|
||||
eff.setChosenNumber(AbilityUtils.calculateAmount(hostCard, sa.getParam("SetChosenNumber"), sa));
|
||||
} else if (hostCard.hasChosenNumber()) {
|
||||
|
||||
@@ -428,10 +428,6 @@ public class PlayEffect extends SpellAbilityEffect {
|
||||
tgtSA.getTargetRestrictions().setMandatory(true);
|
||||
}
|
||||
|
||||
if (sa.hasParam("Named")) {
|
||||
tgtSA.setName(sa.getName());
|
||||
}
|
||||
|
||||
// can't be done later
|
||||
if (sa.hasParam("ReplaceGraveyard")) {
|
||||
if (!sa.hasParam("ReplaceGraveyardValid")
|
||||
|
||||
@@ -75,8 +75,9 @@ public class RestartGameEffect extends SpellAbilityEffect {
|
||||
p.clearController();
|
||||
|
||||
CardCollection newLibrary = new CardCollection(p.getCardsIn(restartZones, false));
|
||||
List<Card> filteredCards = null;
|
||||
if (leaveZone != null) {
|
||||
List<Card> filteredCards = CardLists.getValidCards(p.getCardsIn(leaveZone), leaveRestriction, p, sa.getHostCard(), sa);
|
||||
filteredCards = CardLists.getValidCards(p.getCardsIn(leaveZone), leaveRestriction, p, sa.getHostCard(), sa);
|
||||
newLibrary.addAll(filteredCards);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,11 +92,12 @@ public class SacrificeEffect extends SpellAbilityEffect {
|
||||
CardZoneTable zoneMovements = AbilityKey.addCardZoneTableParams(params, sa);
|
||||
|
||||
if (valid.equals("Self") && game.getZoneOf(host) != null) {
|
||||
if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield) &&
|
||||
(!optional || activator.getController().confirmAction(sa, null,
|
||||
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null))) {
|
||||
if (game.getAction().sacrifice(new CardCollection(host), sa, true, params) != null && remSacrificed) {
|
||||
host.addRemembered(host);
|
||||
if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield)) {
|
||||
if (!optional || activator.getController().confirmAction(sa, null,
|
||||
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null)) {
|
||||
if (game.getAction().sacrifice(new CardCollection(host), sa, true, params) != null && remSacrificed) {
|
||||
host.addRemembered(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,6 @@ import forge.game.GameEntityCounterTable;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
@@ -21,6 +20,7 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.CardTranslation;
|
||||
import forge.util.Localizer;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
public class TimeTravelEffect extends SpellAbilityEffect {
|
||||
|
||||
@@ -41,8 +41,10 @@ public class TimeTravelEffect extends SpellAbilityEffect {
|
||||
final CounterType counterType = CounterEnumType.TIME;
|
||||
|
||||
for (int i = 0; i < num; i++) {
|
||||
FCollection<Card> list = new FCollection<>();
|
||||
|
||||
// card you own that is suspended
|
||||
CardCollection list = CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend());
|
||||
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend()));
|
||||
// permanent you control with time counter
|
||||
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounter(counterType)));
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
private long worldTimestamp = -1;
|
||||
private long bestowTimestamp = -1;
|
||||
private long transformedTimestamp = -1;
|
||||
private long transformedTimestamp = 0;
|
||||
private long prototypeTimestamp = -1;
|
||||
private long mutatedTimestamp = -1;
|
||||
private int timesMutated = 0;
|
||||
@@ -425,7 +425,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
public long getPrototypeTimestamp() { return prototypeTimestamp; }
|
||||
|
||||
public long getTransformedTimestamp() { return transformedTimestamp; }
|
||||
public void setTransformedTimestamp(long ts) { this.transformedTimestamp = ts; }
|
||||
public void incrementTransformedTimestamp() { this.transformedTimestamp++; }
|
||||
public void undoIncrementTransformedTimestamp() { this.transformedTimestamp--; }
|
||||
|
||||
// The following methods are used to selectively update certain view components (text,
|
||||
// P/T, card types) in order to avoid card flickering due to aggressive full update
|
||||
@@ -449,7 +450,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
public final void updateColorForView() {
|
||||
currentState.getView().updateColors(this);
|
||||
currentState.getView().updateHasChangeColors(hasChangedCardColors());
|
||||
currentState.getView().updateHasChangeColors(!Iterables.isEmpty(getChangedCardColors()));
|
||||
}
|
||||
|
||||
public void updateAttackingForView() {
|
||||
@@ -695,7 +696,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(this);
|
||||
getGame().getTriggerHandler().runTrigger(TriggerType.Transformed, runParams, false);
|
||||
}
|
||||
setTransformedTimestamp(ts);
|
||||
incrementTransformedTimestamp();
|
||||
|
||||
return retResult;
|
||||
} else if (mode.equals("Flip")) {
|
||||
@@ -965,7 +966,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
String name = state.getName();
|
||||
for (CardChangedName change : this.changedCardNames.values()) {
|
||||
if (change.isOverwrite()) {
|
||||
name = change.newName();
|
||||
name = change.getNewName();
|
||||
}
|
||||
}
|
||||
return alt ? StaticData.instance().getCommonCards().getName(name, true) : name;
|
||||
@@ -980,7 +981,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
for (CardChangedName change : this.changedCardNames.values()) {
|
||||
if (change.isOverwrite()) {
|
||||
result = false;
|
||||
} else if (change.addNonLegendaryCreatureNames()) {
|
||||
} else if (change.isAddNonLegendaryCreatureNames()) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
@@ -1013,12 +1014,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
currentState.getView().updateName(currentState);
|
||||
}
|
||||
|
||||
private record CardChangedName(String newName, boolean addNonLegendaryCreatureNames) {
|
||||
public boolean isOverwrite() {
|
||||
return newName != null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setGamePieceType(GamePieceType gamePieceType) {
|
||||
this.gamePieceType = gamePieceType;
|
||||
this.view.updateGamePieceType(this);
|
||||
@@ -1075,7 +1070,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public final boolean isDoubleFaced() {
|
||||
return isTransformable() || isMeldable();
|
||||
return isTransformable() || isMeldable() || isModal();
|
||||
}
|
||||
|
||||
public final boolean isFlipCard() {
|
||||
@@ -1137,10 +1132,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public final boolean isTransformed() {
|
||||
if (isMeldable() || hasMergedCard()) {
|
||||
return false;
|
||||
}
|
||||
return this.isTransformable() && isBackSide();
|
||||
return getTransformedTimestamp() != 0;
|
||||
}
|
||||
|
||||
public final boolean isFlipped() {
|
||||
@@ -2271,7 +2263,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
public final ColorSet getMarkedColors() {
|
||||
if (markedColor == null) {
|
||||
return ColorSet.NO_COLORS;
|
||||
return ColorSet.getNullColor();
|
||||
}
|
||||
return markedColor;
|
||||
}
|
||||
@@ -2459,8 +2451,17 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
} else if (keyword.startsWith("DeckLimit")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[2]).append("\r\n");
|
||||
} else if (keyword.startsWith("Enchant") && inst instanceof KeywordWithType kwt) {
|
||||
String desc = kwt.getTypeDescription();
|
||||
} else if (keyword.startsWith("Enchant")) {
|
||||
String m[] = keyword.split(":");
|
||||
String desc;
|
||||
if (m.length > 2) {
|
||||
desc = m[2];
|
||||
} else {
|
||||
desc = m[1];
|
||||
if (CardType.isACardType(desc) || "Permanent".equals(desc) || "Player".equals(desc) || "Opponent".equals(desc)) {
|
||||
desc = desc.toLowerCase();
|
||||
}
|
||||
}
|
||||
sbLong.append("Enchant ").append(desc).append("\r\n");
|
||||
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")
|
||||
|| keyword.startsWith("Disguise") || keyword.startsWith("Reflect")
|
||||
@@ -2604,7 +2605,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|| keyword.equals("Battle cry") || keyword.equals("Devoid") || keyword.equals("Riot")
|
||||
|| keyword.equals("Daybound") || keyword.equals("Nightbound")
|
||||
|| keyword.equals("Friends forever") || keyword.equals("Choose a Background")
|
||||
|| keyword.equals("Partner - Father & Son") || keyword.equals("Partner - Survivors")
|
||||
|| keyword.equals("Space sculptor") || keyword.equals("Doctor's companion")
|
||||
|| keyword.equals("Start your engines")) {
|
||||
sbLong.append(keyword).append(" (").append(inst.getReminderText()).append(")");
|
||||
@@ -3795,7 +3795,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
public final void addLeavesPlayCommand(final GameCommand c) {
|
||||
leavePlayCommandList.add(c);
|
||||
}
|
||||
|
||||
|
||||
public void addStaticCommandList(Object[] objects) {
|
||||
staticCommandList.add(objects);
|
||||
}
|
||||
@@ -4297,10 +4297,18 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public boolean clearChangedCardColors() {
|
||||
boolean changed = hasChangedCardColors();
|
||||
boolean changed = false;
|
||||
|
||||
if (!changedCardColorsByText.isEmpty())
|
||||
changed = true;
|
||||
changedCardColorsByText.clear();
|
||||
|
||||
if (!changedCardTypesCharacterDefining.isEmpty())
|
||||
changed = true;
|
||||
changedCardTypesCharacterDefining.clear();
|
||||
|
||||
if (!changedCardColors.isEmpty())
|
||||
changed = true;
|
||||
changedCardColors.clear();
|
||||
|
||||
return changed;
|
||||
@@ -4386,19 +4394,17 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasChangedCardColors() {
|
||||
return !changedCardColorsByText.isEmpty() || !changedCardColorsCharacterDefining.isEmpty() || !changedCardColors.isEmpty();
|
||||
public Iterable<CardColor> getChangedCardColors() {
|
||||
return Iterables.concat(changedCardColorsByText.values(), changedCardColorsCharacterDefining.values(), changedCardColors.values());
|
||||
}
|
||||
|
||||
public void addColorByText(final ColorSet color, final long timestamp, final StaticAbility stAb) {
|
||||
changedCardColorsByText.put(timestamp, (long)stAb.getId(), new CardColor(color, false));
|
||||
public void addColorByText(final ColorSet color, final long timestamp, final long staticId) {
|
||||
changedCardColorsByText.put(timestamp, staticId, new CardColor(color, false));
|
||||
updateColorForView();
|
||||
}
|
||||
|
||||
public final void addColor(final ColorSet color, final boolean addToColors, final long timestamp, final StaticAbility stAb) {
|
||||
(stAb != null && stAb.isCharacteristicDefining() ? changedCardColorsCharacterDefining : changedCardColors).put(
|
||||
timestamp, stAb != null ? stAb.getId() : (long)0, new CardColor(color, addToColors)
|
||||
);
|
||||
public final void addColor(final ColorSet color, final boolean addToColors, final long timestamp, final long staticId, final boolean cda) {
|
||||
(cda ? changedCardColorsCharacterDefining : changedCardColors).put(timestamp, staticId, new CardColor(color, addToColors));
|
||||
updateColorForView();
|
||||
}
|
||||
|
||||
@@ -4425,20 +4431,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
public final ColorSet getColor(CardState state) {
|
||||
byte colors = state.getColor();
|
||||
for (final CardColor cc : Iterables.concat(changedCardColorsByText.values(), changedCardColorsCharacterDefining.values(), changedCardColors.values())) {
|
||||
if (cc.additional()) {
|
||||
colors |= cc.color().getColor();
|
||||
for (final CardColor cc : getChangedCardColors()) {
|
||||
if (cc.isAdditional()) {
|
||||
colors |= cc.getColorMask();
|
||||
} else {
|
||||
colors = cc.color().getColor();
|
||||
colors = cc.getColorMask();
|
||||
}
|
||||
}
|
||||
return ColorSet.fromMask(colors);
|
||||
}
|
||||
|
||||
private record CardColor(ColorSet color, boolean additional) {
|
||||
|
||||
}
|
||||
|
||||
public final int getCurrentLoyalty() {
|
||||
return getCounters(CounterEnumType.LOYALTY);
|
||||
}
|
||||
@@ -4808,7 +4810,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
public void addDraftAction(String s) {
|
||||
draftActions.add(s);
|
||||
}
|
||||
|
||||
|
||||
private int intensity = 0;
|
||||
public final void addIntensity(final int n) {
|
||||
intensity += n;
|
||||
@@ -6857,7 +6859,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public boolean isWebSlinged() {
|
||||
return getCastSA() != null && getCastSA().isAlternativeCost(AlternativeCost.WebSlinging);
|
||||
return getCastSA() != null & getCastSA().isAlternativeCost(AlternativeCost.WebSlinging);
|
||||
}
|
||||
|
||||
public boolean isSpecialized() {
|
||||
@@ -7167,62 +7169,51 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final String cantBeEnchantedByMsg(final Card aura) {
|
||||
protected final boolean canBeEnchantedBy(final Card aura) {
|
||||
if (!aura.hasKeyword(Keyword.ENCHANT)) {
|
||||
return "No Enchant Keyword";
|
||||
return false;
|
||||
}
|
||||
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
|
||||
if (ki instanceof KeywordWithType kwt) {
|
||||
String v = kwt.getValidType();
|
||||
String desc = kwt.getTypeDescription();
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null) || (!v.contains("inZone") && !isInPlay())) {
|
||||
return getName() + " is not " + Lang.nounWithAmount(1, desc);
|
||||
}
|
||||
String k = ki.getOriginal();
|
||||
String m[] = k.split(":");
|
||||
String v = m[1];
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null)) {
|
||||
return false;
|
||||
}
|
||||
if (!v.contains("inZone") && !isInPlay()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String cantBeEquippedByMsg(final Card equip, SpellAbility sa) {
|
||||
protected final boolean canBeEquippedBy(final Card equip, SpellAbility sa) {
|
||||
if (!isInPlay()) {
|
||||
return getName() + " is not in play";
|
||||
return false;
|
||||
}
|
||||
if (sa != null && sa.isEquip()) {
|
||||
if (!isValid(sa.getTargetRestrictions().getValidTgts(), sa.getActivatingPlayer(), equip, sa)) {
|
||||
Equip eq = (Equip) sa.getKeyword();
|
||||
return getName() + " is not " + Lang.nounWithAmount(1, eq.getValidDescription());
|
||||
}
|
||||
return null;
|
||||
return isValid(sa.getTargetRestrictions().getValidTgts(), sa.getActivatingPlayer(), equip, sa);
|
||||
}
|
||||
if (!isCreature()) {
|
||||
return getName() + " is not a creature";
|
||||
}
|
||||
return null;
|
||||
return isCreature();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String cantBeFortifiedByMsg(final Card fort) {
|
||||
if (!isLand()) {
|
||||
return getName() + " is not a Land";
|
||||
}
|
||||
if (!isInPlay()) {
|
||||
return getName() + " is not in play";
|
||||
}
|
||||
if (fort.isLand()) {
|
||||
return fort.getName() + " is a Land";
|
||||
}
|
||||
|
||||
return null;
|
||||
protected boolean canBeFortifiedBy(final Card fort) {
|
||||
return isLand() && isInPlay() && !fort.isLand();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.game.GameEntity#canBeAttached(forge.game.card.Card, boolean)
|
||||
*/
|
||||
@Override
|
||||
public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) {
|
||||
public boolean canBeAttached(Card attach, SpellAbility sa, boolean checkSBA) {
|
||||
// phase check there
|
||||
if (isPhasedOut() && !attach.isPhasedOut()) {
|
||||
return getName() + " is phased out";
|
||||
return false;
|
||||
}
|
||||
return super.cantBeAttachedMsg(attach, sa, checkSBA);
|
||||
|
||||
return super.canBeAttached(attach, sa, checkSBA);
|
||||
}
|
||||
|
||||
public final boolean canBeSacrificedBy(final SpellAbility source, final boolean effect) {
|
||||
@@ -7647,6 +7638,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
if (sa.isBestow()) {
|
||||
animateBestow();
|
||||
}
|
||||
if (sa.isDisturb() || sa.hasParam("CastTransformed")) {
|
||||
incrementTransformedTimestamp();
|
||||
}
|
||||
if (sa.hasParam("Prototype") && prototypeTimestamp == -1) {
|
||||
long next = game.getNextTimestamp();
|
||||
addCloneState(CardFactory.getCloneStates(this, this, sa), next);
|
||||
@@ -7856,8 +7850,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
return currentState.getUntranslatedType();
|
||||
}
|
||||
@Override
|
||||
public String getTranslatedName() {
|
||||
return CardTranslation.getTranslatedName(this);
|
||||
public String getUntranslatedOracle() {
|
||||
return currentState.getUntranslatedOracle();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package forge.game.card;
|
||||
|
||||
public class CardChangedName {
|
||||
|
||||
protected String newName;
|
||||
protected boolean addNonLegendaryCreatureNames = false;
|
||||
|
||||
public CardChangedName(String newName, boolean addNonLegendaryCreatureNames) {
|
||||
this.newName = newName;
|
||||
this.addNonLegendaryCreatureNames = addNonLegendaryCreatureNames;
|
||||
}
|
||||
|
||||
public String getNewName() {
|
||||
return newName;
|
||||
}
|
||||
|
||||
public boolean isOverwrite() {
|
||||
return newName != null;
|
||||
}
|
||||
|
||||
public boolean isAddNonLegendaryCreatureNames() {
|
||||
return addNonLegendaryCreatureNames;
|
||||
}
|
||||
}
|
||||
45
forge-game/src/main/java/forge/game/card/CardColor.java
Normal file
45
forge-game/src/main/java/forge/game/card/CardColor.java
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Forge: Play Magic: the Gathering.
|
||||
* Copyright (C) 2011 Forge Team
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package forge.game.card;
|
||||
|
||||
import forge.card.ColorSet;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Card_Color class.
|
||||
* </p>
|
||||
*
|
||||
* @author Forge
|
||||
* @version $Id$
|
||||
*/
|
||||
public class CardColor {
|
||||
private final byte colorMask;
|
||||
public final byte getColorMask() {
|
||||
return colorMask;
|
||||
}
|
||||
|
||||
private final boolean additional;
|
||||
public final boolean isAdditional() {
|
||||
return this.additional;
|
||||
}
|
||||
|
||||
CardColor(final ColorSet colors, final boolean addToColors) {
|
||||
this.colorMask = colors.getColor();
|
||||
this.additional = addToColors;
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,9 @@ public class CardCopyService {
|
||||
|
||||
c.setState(in.getCurrentStateName(), false);
|
||||
c.setRules(in.getRules());
|
||||
c.setBackSide(in.isBackSide());
|
||||
if (in.isTransformed()) {
|
||||
c.incrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -166,6 +168,9 @@ public class CardCopyService {
|
||||
// The characteristics of its front and back face are determined by the copiable values of the same face of the spell it is a copy of, as modified by any other copy effects.
|
||||
// If the spell it is a copy of has its back face up, the copy is created with its back face up. The token that’s put onto the battlefield as that spell resolves is a transforming token.
|
||||
to.setBackSide(copyFrom.isBackSide());
|
||||
if (copyFrom.isTransformed()) {
|
||||
to.incrementTransformedTimestamp();
|
||||
}
|
||||
} else if (fromIsTransformedCard) {
|
||||
copyState(copyFrom, copyFrom.getCurrentStateName(), to, CardStateName.Original);
|
||||
} else {
|
||||
@@ -269,6 +274,9 @@ public class CardCopyService {
|
||||
}
|
||||
newCopy.setFlipped(copyFrom.isFlipped());
|
||||
newCopy.setBackSide(copyFrom.isBackSide());
|
||||
if (copyFrom.isTransformed()) {
|
||||
newCopy.incrementTransformedTimestamp();
|
||||
}
|
||||
if (newCopy.hasAlternateState()) {
|
||||
newCopy.setState(copyFrom.getCurrentStateName(), false, true);
|
||||
}
|
||||
|
||||
@@ -87,16 +87,22 @@ public class CardFactory {
|
||||
// need to create a physical card first, i need the original card faces
|
||||
final Card copy = getCard(original.getPaperCard(), controller, id, game);
|
||||
|
||||
copy.setStates(getCloneStates(original, copy, sourceSA));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
// 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield,
|
||||
// the resulting token is a transforming token that has both a front face and a back face.
|
||||
// The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
|
||||
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up.
|
||||
// This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
|
||||
copy.setBackSide(original.isBackSide());
|
||||
if (original.isTransformed()) {
|
||||
copy.incrementTransformedTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
copy.setStates(getCloneStates(original, copy, sourceSA));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
} else {
|
||||
copy.setState(copy.getCurrentStateName(), true, true);
|
||||
}
|
||||
@@ -466,29 +472,29 @@ public class CardFactory {
|
||||
return new WrappedAbility(sa.getTrigger(), sa.getWrappedAbility().copy(newHost, controller, false), sa.getDecider());
|
||||
}
|
||||
|
||||
public static CardCloneStates getCloneStates(final Card in, final Card out, final CardTraitBase cause) {
|
||||
final Card host = cause.getHostCard();
|
||||
public static CardCloneStates getCloneStates(final Card in, final Card out, final CardTraitBase sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Map<String,String> origSVars = host.getSVars();
|
||||
final List<String> types = Lists.newArrayList();
|
||||
final List<String> keywords = Lists.newArrayList();
|
||||
boolean KWifNew = false;
|
||||
final List<String> removeKeywords = Lists.newArrayList();
|
||||
List<String> creatureTypes = null;
|
||||
final CardCloneStates result = new CardCloneStates(in, cause);
|
||||
final CardCloneStates result = new CardCloneStates(in, sa);
|
||||
|
||||
final String newName = cause.getParam("NewName");
|
||||
final String newName = sa.getParam("NewName");
|
||||
ColorSet colors = null;
|
||||
|
||||
if (cause.hasParam("AddTypes")) {
|
||||
types.addAll(Arrays.asList(cause.getParam("AddTypes").split(" & ")));
|
||||
if (sa.hasParam("AddTypes")) {
|
||||
types.addAll(Arrays.asList(sa.getParam("AddTypes").split(" & ")));
|
||||
}
|
||||
|
||||
if (cause.hasParam("SetCreatureTypes")) {
|
||||
creatureTypes = ImmutableList.copyOf(cause.getParam("SetCreatureTypes").split(" "));
|
||||
if (sa.hasParam("SetCreatureTypes")) {
|
||||
creatureTypes = ImmutableList.copyOf(sa.getParam("SetCreatureTypes").split(" "));
|
||||
}
|
||||
|
||||
if (cause.hasParam("AddKeywords")) {
|
||||
String kwString = cause.getParam("AddKeywords");
|
||||
if (sa.hasParam("AddKeywords")) {
|
||||
String kwString = sa.getParam("AddKeywords");
|
||||
if (kwString.startsWith("IfNew ")) {
|
||||
KWifNew = true;
|
||||
kwString = kwString.substring(6);
|
||||
@@ -496,21 +502,21 @@ public class CardFactory {
|
||||
keywords.addAll(Arrays.asList(kwString.split(" & ")));
|
||||
}
|
||||
|
||||
if (cause.hasParam("RemoveKeywords")) {
|
||||
removeKeywords.addAll(Arrays.asList(cause.getParam("RemoveKeywords").split(" & ")));
|
||||
if (sa.hasParam("RemoveKeywords")) {
|
||||
removeKeywords.addAll(Arrays.asList(sa.getParam("RemoveKeywords").split(" & ")));
|
||||
}
|
||||
|
||||
if (cause.hasParam("AddColors")) {
|
||||
colors = ColorSet.fromNames(cause.getParam("AddColors").split(","));
|
||||
if (sa.hasParam("AddColors")) {
|
||||
colors = ColorSet.fromNames(sa.getParam("AddColors").split(","));
|
||||
}
|
||||
|
||||
if (cause.hasParam("SetColor")) {
|
||||
colors = ColorSet.fromNames(cause.getParam("SetColor").split(","));
|
||||
if (sa.hasParam("SetColor")) {
|
||||
colors = ColorSet.fromNames(sa.getParam("SetColor").split(","));
|
||||
}
|
||||
|
||||
if (cause.hasParam("SetColorByManaCost")) {
|
||||
if (cause.hasParam("SetManaCost")) {
|
||||
colors = ColorSet.fromManaCost(new ManaCost(new ManaCostParser(cause.getParam("SetManaCost"))));
|
||||
if (sa.hasParam("SetColorByManaCost")) {
|
||||
if (sa.hasParam("SetManaCost")) {
|
||||
colors = ColorSet.fromManaCost(new ManaCost(new ManaCostParser(sa.getParam("SetManaCost"))));
|
||||
} else {
|
||||
colors = ColorSet.fromManaCost(host.getManaCost());
|
||||
}
|
||||
@@ -522,55 +528,56 @@ public class CardFactory {
|
||||
// if something is cloning a facedown card, it only clones the
|
||||
// facedown state into original
|
||||
final CardState ret = new CardState(out, CardStateName.Original);
|
||||
ret.copyFrom(in.getFaceDownState(), false, cause);
|
||||
ret.copyFrom(in.getFaceDownState(), false, sa);
|
||||
result.put(CardStateName.Original, ret);
|
||||
} else if (in.isFlipCard()) {
|
||||
// if something is cloning a flip card, copy both original and
|
||||
// flipped state
|
||||
final CardState ret1 = new CardState(out, CardStateName.Original);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, cause);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
|
||||
result.put(CardStateName.Original, ret1);
|
||||
|
||||
final CardState ret2 = new CardState(out, CardStateName.Flipped);
|
||||
ret2.copyFrom(in.getState(CardStateName.Flipped), false, cause);
|
||||
ret2.copyFrom(in.getState(CardStateName.Flipped), false, sa);
|
||||
result.put(CardStateName.Flipped, ret2);
|
||||
} else if (in.hasState(CardStateName.Secondary)) {
|
||||
final CardState ret1 = new CardState(out, CardStateName.Original);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, cause);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
|
||||
result.put(CardStateName.Original, ret1);
|
||||
|
||||
final CardState ret2 = new CardState(out, CardStateName.Secondary);
|
||||
ret2.copyFrom(in.getState(CardStateName.Secondary), false, cause);
|
||||
ret2.copyFrom(in.getState(CardStateName.Secondary), false, sa);
|
||||
result.put(CardStateName.Secondary, ret2);
|
||||
} else if (in.isTransformable() && cause instanceof SpellAbility sa && (
|
||||
ApiType.CopyPermanent.equals(sa.getApi()) ||
|
||||
ApiType.CopySpellAbility.equals(sa.getApi()) ||
|
||||
ApiType.ReplaceToken.equals(sa.getApi()))) {
|
||||
} else if (in.isTransformable() && sa instanceof SpellAbility && (
|
||||
ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi()) ||
|
||||
ApiType.CopySpellAbility.equals(((SpellAbility)sa).getApi()) ||
|
||||
ApiType.ReplaceToken.equals(((SpellAbility)sa).getApi())
|
||||
)) {
|
||||
// CopyPermanent can copy token
|
||||
final CardState ret1 = new CardState(out, CardStateName.Original);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, cause);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
|
||||
result.put(CardStateName.Original, ret1);
|
||||
|
||||
final CardState ret2 = new CardState(out, CardStateName.Backside);
|
||||
ret2.copyFrom(in.getState(CardStateName.Backside), false, cause);
|
||||
ret2.copyFrom(in.getState(CardStateName.Backside), false, sa);
|
||||
result.put(CardStateName.Backside, ret2);
|
||||
} else if (in.isSplitCard()) {
|
||||
// for split cards, copy all three states
|
||||
final CardState ret1 = new CardState(out, CardStateName.Original);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, cause);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
|
||||
result.put(CardStateName.Original, ret1);
|
||||
|
||||
final CardState ret2 = new CardState(out, CardStateName.LeftSplit);
|
||||
ret2.copyFrom(in.getState(CardStateName.LeftSplit), false, cause);
|
||||
ret2.copyFrom(in.getState(CardStateName.LeftSplit), false, sa);
|
||||
result.put(CardStateName.LeftSplit, ret2);
|
||||
|
||||
final CardState ret3 = new CardState(out, CardStateName.RightSplit);
|
||||
ret3.copyFrom(in.getState(CardStateName.RightSplit), false, cause);
|
||||
ret3.copyFrom(in.getState(CardStateName.RightSplit), false, sa);
|
||||
result.put(CardStateName.RightSplit, ret3);
|
||||
} else {
|
||||
// in all other cases just copy the current state to original
|
||||
final CardState ret = new CardState(out, CardStateName.Original);
|
||||
ret.copyFrom(in.getState(in.getCurrentStateName()), false, cause);
|
||||
ret.copyFrom(in.getState(in.getCurrentStateName()), false, sa);
|
||||
result.put(CardStateName.Original, ret);
|
||||
}
|
||||
|
||||
@@ -580,32 +587,32 @@ public class CardFactory {
|
||||
final CardState state = e.getValue();
|
||||
|
||||
// has Embalm Condition for extra changes of Vizier of Many Faces
|
||||
if (cause.hasParam("Embalm") && !out.isEmbalmed()) {
|
||||
if (sa.hasParam("Embalm") && !out.isEmbalmed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// update the names for the states
|
||||
if (cause.hasParam("KeepName")) {
|
||||
if (sa.hasParam("KeepName")) {
|
||||
state.setName(originalState.getName());
|
||||
} else if (newName != null) {
|
||||
// convert NICKNAME descriptions?
|
||||
state.setName(newName);
|
||||
}
|
||||
|
||||
if (cause.hasParam("AddColors")) {
|
||||
if (sa.hasParam("AddColors")) {
|
||||
state.addColor(colors.getColor());
|
||||
}
|
||||
|
||||
if (cause.hasParam("SetColor") || cause.hasParam("SetColorByManaCost")) {
|
||||
if (sa.hasParam("SetColor") || sa.hasParam("SetColorByManaCost")) {
|
||||
state.setColor(colors.getColor());
|
||||
}
|
||||
|
||||
if (cause.hasParam("NonLegendary")) {
|
||||
if (sa.hasParam("NonLegendary")) {
|
||||
state.removeType(CardType.Supertype.Legendary);
|
||||
}
|
||||
|
||||
if (cause.hasParam("RemoveCardTypes")) {
|
||||
state.removeCardTypes(cause.hasParam("RemoveSubTypes"));
|
||||
if (sa.hasParam("RemoveCardTypes")) {
|
||||
state.removeCardTypes(sa.hasParam("RemoveSubTypes"));
|
||||
}
|
||||
|
||||
state.addType(types);
|
||||
@@ -637,31 +644,31 @@ public class CardFactory {
|
||||
|
||||
// CR 208.3 A noncreature object not on the battlefield has power or toughness only if it has a power and toughness printed on it.
|
||||
// currently only LKI can be trusted?
|
||||
if ((cause.hasParam("SetPower") || cause.hasParam("SetToughness")) &&
|
||||
if ((sa.hasParam("SetPower") || sa.hasParam("SetToughness")) &&
|
||||
(state.getType().isCreature() || (originalState != null && in.getOriginalState(originalState.getStateName()).getBasePowerString() != null))) {
|
||||
if (cause.hasParam("SetPower")) {
|
||||
state.setBasePower(AbilityUtils.calculateAmount(host, cause.getParam("SetPower"), cause));
|
||||
if (sa.hasParam("SetPower")) {
|
||||
state.setBasePower(AbilityUtils.calculateAmount(host, sa.getParam("SetPower"), sa));
|
||||
}
|
||||
if (cause.hasParam("SetToughness")) {
|
||||
state.setBaseToughness(AbilityUtils.calculateAmount(host, cause.getParam("SetToughness"), cause));
|
||||
if (sa.hasParam("SetToughness")) {
|
||||
state.setBaseToughness(AbilityUtils.calculateAmount(host, sa.getParam("SetToughness"), sa));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.getType().isPlaneswalker() && cause.hasParam("SetLoyalty")) {
|
||||
state.setBaseLoyalty(String.valueOf(AbilityUtils.calculateAmount(host, cause.getParam("SetLoyalty"), cause)));
|
||||
if (state.getType().isPlaneswalker() && sa.hasParam("SetLoyalty")) {
|
||||
state.setBaseLoyalty(String.valueOf(AbilityUtils.calculateAmount(host, sa.getParam("SetLoyalty"), sa)));
|
||||
}
|
||||
|
||||
if (cause.hasParam("RemoveCost")) {
|
||||
if (sa.hasParam("RemoveCost")) {
|
||||
state.setManaCost(ManaCost.NO_COST);
|
||||
}
|
||||
|
||||
if (cause.hasParam("SetManaCost")) {
|
||||
state.setManaCost(new ManaCost(new ManaCostParser(cause.getParam("SetManaCost"))));
|
||||
if (sa.hasParam("SetManaCost")) {
|
||||
state.setManaCost(new ManaCost(new ManaCostParser(sa.getParam("SetManaCost"))));
|
||||
}
|
||||
|
||||
// SVars to add to clone
|
||||
if (cause.hasParam("AddSVars") || cause.hasParam("GainTextSVars")) {
|
||||
final String str = cause.getParamOrDefault("GainTextSVars", cause.getParam("AddSVars"));
|
||||
if (sa.hasParam("AddSVars") || sa.hasParam("GainTextSVars")) {
|
||||
final String str = sa.getParamOrDefault("GainTextSVars", sa.getParam("AddSVars"));
|
||||
for (final String s : str.split(",")) {
|
||||
if (origSVars.containsKey(s)) {
|
||||
final String actualsVar = origSVars.get(s);
|
||||
@@ -671,8 +678,8 @@ public class CardFactory {
|
||||
}
|
||||
|
||||
// triggers to add to clone
|
||||
if (cause.hasParam("AddTriggers")) {
|
||||
for (final String s : cause.getParam("AddTriggers").split(",")) {
|
||||
if (sa.hasParam("AddTriggers")) {
|
||||
for (final String s : sa.getParam("AddTriggers").split(",")) {
|
||||
if (origSVars.containsKey(s)) {
|
||||
final String actualTrigger = origSVars.get(s);
|
||||
final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, out, true, state);
|
||||
@@ -682,8 +689,8 @@ public class CardFactory {
|
||||
}
|
||||
|
||||
// abilities to add to clone
|
||||
if (cause.hasParam("AddAbilities") || cause.hasParam("GainTextAbilities")) {
|
||||
final String str = cause.getParamOrDefault("GainTextAbilities", cause.getParam("AddAbilities"));
|
||||
if (sa.hasParam("AddAbilities") || sa.hasParam("GainTextAbilities")) {
|
||||
final String str = sa.getParamOrDefault("GainTextAbilities", sa.getParam("AddAbilities"));
|
||||
for (final String s : str.split(",")) {
|
||||
if (origSVars.containsKey(s)) {
|
||||
final String actualAbility = origSVars.get(s);
|
||||
@@ -695,18 +702,18 @@ public class CardFactory {
|
||||
}
|
||||
|
||||
// static abilities to add to clone
|
||||
if (cause.hasParam("AddStaticAbilities")) {
|
||||
final String str = cause.getParam("AddStaticAbilities");
|
||||
if (sa.hasParam("AddStaticAbilities")) {
|
||||
final String str = sa.getParam("AddStaticAbilities");
|
||||
for (final String s : str.split(",")) {
|
||||
if (origSVars.containsKey(s)) {
|
||||
final String actualStatic = origSVars.get(s);
|
||||
state.addStaticAbility(StaticAbility.create(actualStatic, out, cause.getCardState(), true));
|
||||
state.addStaticAbility(StaticAbility.create(actualStatic, out, sa.getCardState(), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cause.hasParam("GainThisAbility") && cause instanceof SpellAbility sa) {
|
||||
SpellAbility root = sa.getRootAbility();
|
||||
if (sa.hasParam("GainThisAbility") && sa instanceof SpellAbility) {
|
||||
SpellAbility root = ((SpellAbility) sa).getRootAbility();
|
||||
|
||||
// Aurora Shifter
|
||||
if (root.isTrigger() && root.getTrigger().getSpawningAbility() != null) {
|
||||
@@ -723,35 +730,35 @@ public class CardFactory {
|
||||
}
|
||||
|
||||
// Special Rules for Embalm and Eternalize
|
||||
if (cause.isEmbalm() && cause.isIntrinsic()) {
|
||||
if (sa.isEmbalm() && sa.isIntrinsic()) {
|
||||
String name = "embalm_" + TextUtil.fastReplace(
|
||||
TextUtil.fastReplace(host.getName(), ",", ""),
|
||||
" ", "_").toLowerCase();
|
||||
state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode()));
|
||||
}
|
||||
|
||||
if (cause.isEternalize() && cause.isIntrinsic()) {
|
||||
if (sa.isEternalize() && sa.isIntrinsic()) {
|
||||
String name = "eternalize_" + TextUtil.fastReplace(
|
||||
TextUtil.fastReplace(host.getName(), ",", ""),
|
||||
" ", "_").toLowerCase();
|
||||
state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode()));
|
||||
}
|
||||
|
||||
if (cause.isKeyword(Keyword.OFFSPRING) && cause.isIntrinsic()) {
|
||||
if (sa.isKeyword(Keyword.OFFSPRING) && sa.isIntrinsic()) {
|
||||
String name = "offspring_" + TextUtil.fastReplace(
|
||||
TextUtil.fastReplace(host.getName(), ",", ""),
|
||||
" ", "_").toLowerCase();
|
||||
state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode()));
|
||||
}
|
||||
|
||||
if (cause.isKeyword(Keyword.SQUAD) && cause.isIntrinsic()) {
|
||||
if (sa.isKeyword(Keyword.SQUAD) && sa.isIntrinsic()) {
|
||||
String name = "squad_" + TextUtil.fastReplace(
|
||||
TextUtil.fastReplace(host.getName(), ",", ""),
|
||||
" ", "_").toLowerCase();
|
||||
state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode()));
|
||||
}
|
||||
|
||||
if (cause.hasParam("GainTextOf") && originalState != null) {
|
||||
if (sa.hasParam("GainTextOf") && originalState != null) {
|
||||
state.setSetCode(originalState.getSetCode());
|
||||
state.setRarity(originalState.getRarity());
|
||||
state.setImageKey(originalState.getImageKey());
|
||||
@@ -763,27 +770,27 @@ public class CardFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cause.hasParam("SetPower") && sta.hasParam("SetPower"))
|
||||
if (sa.hasParam("SetPower") && sta.hasParam("SetPower"))
|
||||
state.removeStaticAbility(sta);
|
||||
|
||||
if (cause.hasParam("SetToughness") && sta.hasParam("SetToughness"))
|
||||
if (sa.hasParam("SetToughness") && sta.hasParam("SetToughness"))
|
||||
state.removeStaticAbility(sta);
|
||||
|
||||
// currently only Changeling and similar should be affected by that
|
||||
// other cards using AddType$ ChosenType should not
|
||||
if (cause.hasParam("SetCreatureTypes") && sta.hasParam("AddAllCreatureTypes")) {
|
||||
if (sa.hasParam("SetCreatureTypes") && sta.hasParam("AddAllCreatureTypes")) {
|
||||
state.removeStaticAbility(sta);
|
||||
}
|
||||
if ((cause.hasParam("SetColor") || cause.hasParam("SetColorByManaCost")) && sta.hasParam("SetColor")) {
|
||||
if ((sa.hasParam("SetColor") || sa.hasParam("SetColorByManaCost")) && sta.hasParam("SetColor")) {
|
||||
state.removeStaticAbility(sta);
|
||||
}
|
||||
}
|
||||
|
||||
// remove some keywords
|
||||
if (cause.hasParam("SetCreatureTypes")) {
|
||||
if (sa.hasParam("SetCreatureTypes")) {
|
||||
state.removeIntrinsicKeyword(Keyword.CHANGELING);
|
||||
}
|
||||
if (cause.hasParam("SetColor") || cause.hasParam("SetColorByManaCost")) {
|
||||
if (sa.hasParam("SetColor") || sa.hasParam("SetColorByManaCost")) {
|
||||
state.removeIntrinsicKeyword(Keyword.DEVOID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2238,7 +2238,7 @@ public class CardFactoryUtil {
|
||||
final String actualRep = "Event$ Draw | ActiveZones$ Graveyard | ValidPlayer$ You | "
|
||||
+ "Secondary$ True | Optional$ True | CheckSVar$ "
|
||||
+ "DredgeCheckLib | SVarCompare$ GE" + dredgeAmount
|
||||
+ " | Description$ CARDNAME - Dredge " + dredgeAmount;
|
||||
+ " | AICheckDredge$ True | Description$ CARDNAME - Dredge " + dredgeAmount;
|
||||
|
||||
final String abString = "DB$ Mill | Defined$ You | NumCards$ " + dredgeAmount;
|
||||
|
||||
|
||||
@@ -26,17 +26,12 @@ import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityTapPowerValue;
|
||||
import forge.util.IterableUtil;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.StreamUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -485,26 +480,4 @@ public class CardLists {
|
||||
// (b) including the last element
|
||||
return isSubsetSum(numList, sum) || isSubsetSum(numList, sum - last);
|
||||
}
|
||||
|
||||
public static int getDifferentNamesCount(Iterable<Card> cardList) {
|
||||
// first part the ones with SpyKit, and already collect them via
|
||||
Map<Boolean, List<Card>> parted = StreamUtil.stream(cardList).collect(Collectors
|
||||
.partitioningBy(Card::hasNonLegendaryCreatureNames, Collector.of(ArrayList::new, (list, c) -> {
|
||||
if (!c.hasNoName() && list.stream().noneMatch(c2 -> c.sharesNameWith(c2))) {
|
||||
list.add(c);
|
||||
}
|
||||
}, (l1, l2) -> {
|
||||
l1.addAll(l2);
|
||||
return l1;
|
||||
})));
|
||||
List<Card> preList = parted.get(Boolean.FALSE);
|
||||
|
||||
// then try to apply the SpyKit ones
|
||||
for (Card c : parted.get(Boolean.TRUE)) {
|
||||
if (preList.stream().noneMatch(c2 -> c.sharesNameWith(c2))) {
|
||||
preList.add(c);
|
||||
}
|
||||
}
|
||||
return preList.size();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,12 @@ public class CardProperty {
|
||||
if (!card.sharesNameWith(name)) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("notnamed")) {
|
||||
String name = TextUtil.fastReplace(property.substring(8), ";", ","); // workaround for card name with ","
|
||||
name = TextUtil.fastReplace(name, "_", " ");
|
||||
if (card.sharesNameWith(name)) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.equals("NamedCard")) {
|
||||
boolean found = false;
|
||||
for (String name : source.getNamedCards()) {
|
||||
@@ -1558,6 +1564,8 @@ public class CardProperty {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (property.startsWith("notattacking")) {
|
||||
return null == combat || !combat.isAttacking(card);
|
||||
} else if (property.startsWith("enlistedThisCombat")) {
|
||||
if (card.getEnlistedThisCombat() == false) return false;
|
||||
} else if (property.startsWith("attackedThisCombat")) {
|
||||
@@ -1611,6 +1619,8 @@ public class CardProperty {
|
||||
if (Collections.disjoint(combat.getAttackersBlockedBy(source), combat.getAttackersBlockedBy(card))) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("notblocking")) {
|
||||
return null == combat || !combat.isBlocking(card);
|
||||
}
|
||||
// Nex predicates refer to past combat and don't need a reference to actual combat
|
||||
else if (property.equals("blocked")) {
|
||||
@@ -2063,6 +2073,16 @@ public class CardProperty {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("NotTriggered")) {
|
||||
final String key = property.substring("NotTriggered".length());
|
||||
if (spellAbility instanceof SpellAbility) {
|
||||
SpellAbility sa = (SpellAbility) spellAbility;
|
||||
if (card.equals(sa.getRootAbility().getTriggeringObject(AbilityKey.fromString(key)))) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("NotDefined")) {
|
||||
final String key = property.substring("NotDefined".length());
|
||||
if (AbilityUtils.getDefinedCards(source, key, spellAbility).contains(card)) {
|
||||
|
||||
@@ -32,7 +32,6 @@ import forge.game.card.CardView.CardStateView;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordCollection;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.keyword.KeywordWithType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.spellability.LandAbility;
|
||||
@@ -41,7 +40,6 @@ import forge.game.spellability.SpellAbilityPredicates;
|
||||
import forge.game.spellability.SpellPermanent;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.util.CardTranslation;
|
||||
import forge.util.ITranslatable;
|
||||
import forge.util.IterableUtil;
|
||||
import forge.util.collect.FCollection;
|
||||
@@ -369,7 +367,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
public final FCollectionView<SpellAbility> getManaAbilities() {
|
||||
FCollection<SpellAbility> newCol = new FCollection<>();
|
||||
updateSpellAbilities(newCol, true);
|
||||
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
|
||||
// stream().toList() causes crash on Android, use Collectors.toList()
|
||||
newCol.addAll(abilities.stream().filter(SpellAbility::isManaAbility).collect(Collectors.toList()));
|
||||
card.updateSpellAbilities(newCol, this, true);
|
||||
return newCol;
|
||||
@@ -377,7 +375,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
public final FCollectionView<SpellAbility> getNonManaAbilities() {
|
||||
FCollection<SpellAbility> newCol = new FCollection<>();
|
||||
updateSpellAbilities(newCol, false);
|
||||
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
|
||||
// stream().toList() causes crash on Android, use Collectors.toList()
|
||||
newCol.addAll(abilities.stream().filter(Predicate.not(SpellAbility::isManaAbility)).collect(Collectors.toList()));
|
||||
card.updateSpellAbilities(newCol, this, false);
|
||||
return newCol;
|
||||
@@ -392,7 +390,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
if (null != mana) {
|
||||
leftAbilities = leftAbilities.stream()
|
||||
.filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility))
|
||||
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
|
||||
// stream().toList() causes crash on Android, use Collectors.toList()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
newCol.addAll(leftAbilities);
|
||||
@@ -404,7 +402,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
if (null != mana) {
|
||||
rightAbilities = rightAbilities.stream()
|
||||
.filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility))
|
||||
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
|
||||
// stream().toList() causes crash on Android, use Collectors.toList()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
newCol.addAll(rightAbilities);
|
||||
@@ -502,8 +500,15 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
String desc = "";
|
||||
String extra = "";
|
||||
for (KeywordInterface ki : this.getCachedKeyword(Keyword.ENCHANT)) {
|
||||
if (ki instanceof KeywordWithType kwt) {
|
||||
desc = kwt.getTypeDescription();
|
||||
String o = ki.getOriginal();
|
||||
String m[] = o.split(":");
|
||||
if (m.length > 2) {
|
||||
desc = m[2];
|
||||
} else {
|
||||
desc = m[1];
|
||||
if (CardType.isACardType(desc) || "Permanent".equals(desc) || "Player".equals(desc) || "Opponent".equals(desc)) {
|
||||
desc = desc.toLowerCase();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -941,7 +946,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTranslatedName() {
|
||||
return CardTranslation.getTranslatedName(this);
|
||||
public String getUntranslatedOracle() {
|
||||
return getOracleText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1095,7 +1095,7 @@ public class CardView extends GameEntityView {
|
||||
if (c.getGame() != null) {
|
||||
if (c.hasPerpetual()) currentStateView.updateColors(c);
|
||||
else currentStateView.updateColors(currentState);
|
||||
currentStateView.updateHasChangeColors(c.hasChangedCardColors());
|
||||
currentStateView.updateHasChangeColors(!Iterables.isEmpty(c.getChangedCardColors()));
|
||||
}
|
||||
} else {
|
||||
currentStateView.updateLoyalty(currentState);
|
||||
@@ -1841,8 +1841,8 @@ public class CardView extends GameEntityView {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTranslatedName() {
|
||||
return CardTranslation.getTranslatedName(this);
|
||||
public String getUntranslatedOracle() {
|
||||
return getOracleText();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ public record PerpetualColors(long timestamp, ColorSet colors, boolean overwrite
|
||||
|
||||
@Override
|
||||
public void applyEffect(Card c) {
|
||||
c.addColor(colors, !overwrite, timestamp, null);
|
||||
c.addColor(colors, !overwrite, timestamp, (long) 0, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public record PerpetualIncorporate(long timestamp, ManaCost incorporate) impleme
|
||||
ColorSet colors = ColorSet.fromMask(incorporate.getColorProfile());
|
||||
final ManaCost newCost = ManaCost.combine(c.getManaCost(), incorporate);
|
||||
c.addChangedManaCost(newCost, timestamp, (long) 0);
|
||||
c.addColor(colors, true, timestamp, null);
|
||||
c.addColor(colors, true, timestamp, (long) 0, false);
|
||||
c.updateManaCostForView();
|
||||
|
||||
if (c.getFirstSpellAbility() != null) {
|
||||
|
||||
@@ -237,17 +237,17 @@ public class Cost implements Serializable {
|
||||
CostPartMana parsedMana = null;
|
||||
for (String part : parts) {
|
||||
if (part.startsWith("XMin")) {
|
||||
xMin = part;
|
||||
xMin = (part);
|
||||
} else if ("Mandatory".equals(part)) {
|
||||
this.isMandatory = true;
|
||||
} else {
|
||||
CostPart cp = parseCostPart(part, tapCost, untapCost);
|
||||
if (null != cp)
|
||||
if (cp instanceof CostPartMana p) {
|
||||
parsedMana = p;
|
||||
if (cp instanceof CostPartMana) {
|
||||
parsedMana = (CostPartMana) cp;
|
||||
} else {
|
||||
if (cp instanceof CostPartWithList p) {
|
||||
p.setIntrinsic(intrinsic);
|
||||
if (cp instanceof CostPartWithList) {
|
||||
((CostPartWithList)cp).setIntrinsic(intrinsic);
|
||||
}
|
||||
this.costParts.add(cp);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package forge.game.cost;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.card.*;
|
||||
import forge.game.player.Player;
|
||||
@@ -28,6 +29,7 @@ import forge.util.TextUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The Class CostDiscard.
|
||||
@@ -61,20 +63,11 @@ public class CostDiscard extends CostPartWithList {
|
||||
public Integer getMaxAmountX(SpellAbility ability, Player payer, final boolean effect) {
|
||||
final Card source = ability.getHostCard();
|
||||
String type = this.getType();
|
||||
|
||||
boolean differentNames = false;
|
||||
if (type.contains("+WithDifferentNames")) {
|
||||
type = type.replace("+WithDifferentNames", "");
|
||||
differentNames = true;
|
||||
}
|
||||
CardCollectionView handList = payer.canDiscardBy(ability, effect) ? payer.getCardsIn(ZoneType.Hand) : CardCollection.EMPTY;
|
||||
|
||||
if (!type.equals("Random")) {
|
||||
handList = CardLists.getValidCards(handList, type.split(";"), payer, source, ability);
|
||||
}
|
||||
if (differentNames) {
|
||||
return CardLists.getDifferentNamesCount(handList);
|
||||
}
|
||||
return handList.size();
|
||||
}
|
||||
|
||||
@@ -99,7 +92,7 @@ public class CostDiscard extends CostPartWithList {
|
||||
else if (this.getType().equals("LastDrawn")) {
|
||||
sb.append("the last card you drew this turn");
|
||||
}
|
||||
else if (this.getType().contains("+WithDifferentNames")) {
|
||||
else if (this.getType().equals("DifferentNames")) {
|
||||
sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), "Card")).append(" with different names");
|
||||
}
|
||||
else {
|
||||
@@ -152,17 +145,21 @@ public class CostDiscard extends CostPartWithList {
|
||||
final Card c = payer.getLastDrawnCard();
|
||||
return handList.contains(c);
|
||||
}
|
||||
else if (type.equals("DifferentNames")) {
|
||||
Set<String> cardNames = Sets.newHashSet();
|
||||
for (Card c : handList) {
|
||||
if (!c.hasNoName()) {
|
||||
cardNames.add(c.getName());
|
||||
}
|
||||
}
|
||||
return cardNames.size() >= amount;
|
||||
}
|
||||
else {
|
||||
boolean sameName = false;
|
||||
boolean differentNames = false;
|
||||
if (type.contains("+WithSameName")) {
|
||||
sameName = true;
|
||||
type = TextUtil.fastReplace(type, "+WithSameName", "");
|
||||
}
|
||||
if (type.contains("+WithDifferentNames")) {
|
||||
type = type.replace("+WithDifferentNames", "");
|
||||
differentNames = true;
|
||||
}
|
||||
if (type.contains("ChosenColor") && !source.hasChosenColor()) {
|
||||
//color hasn't been chosen yet, so skip getValidCards
|
||||
} else if (!type.equals("Random") && !type.contains("X")) {
|
||||
@@ -176,10 +173,6 @@ public class CostDiscard extends CostPartWithList {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (differentNames) {
|
||||
if (CardLists.getDifferentNamesCount(handList) < amount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
int adjustment = 0;
|
||||
if (source.isInZone(ZoneType.Hand) && payer.equals(source.getOwner())) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package forge.game.cost;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import forge.card.CardType;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityKey;
|
||||
@@ -30,6 +31,7 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.Lang;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The Class CostSacrifice.
|
||||
@@ -72,7 +74,16 @@ public class CostSacrifice extends CostPartWithList {
|
||||
}
|
||||
typeList = CardLists.filter(typeList, CardPredicates.canBeSacrificedBy(ability, effect));
|
||||
if (differentNames) {
|
||||
return CardLists.getDifferentNamesCount(typeList);
|
||||
// TODO rewrite with sharesName to respect Spy Kit
|
||||
final Set<String> crdname = Sets.newHashSet();
|
||||
for (final Card card : typeList) {
|
||||
String name = card.getName();
|
||||
// CR 201.2b Those objects have different names only if each of them has at least one name and no two objects in that group have a name in common
|
||||
if (!card.hasNoName()) {
|
||||
crdname.add(name);
|
||||
}
|
||||
}
|
||||
return crdname.size();
|
||||
}
|
||||
return typeList.size();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package forge.game.event;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.card.Card;
|
||||
|
||||
public record GameEventCardAttachment(Card equipment, GameEntity oldEntity, GameEntity newTarget) implements GameEvent {
|
||||
public record GameEventCardAttachment(Card equipment, GameEntity newTarget, GameEntity oldEntity) implements GameEvent {
|
||||
|
||||
@Override
|
||||
public <T> T visit(IGameEventVisitor<T> visitor) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.game.event;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
import forge.util.CardTranslation;
|
||||
import forge.util.Lang;
|
||||
|
||||
public record GameEventDoorChanged(Player activatingPlayer, Card card, CardStateName state, boolean unlock) implements GameEvent {
|
||||
@@ -14,7 +15,7 @@ public record GameEventDoorChanged(Player activatingPlayer, Card card, CardState
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String doorName = card.getState(state).getTranslatedName();
|
||||
String doorName = CardTranslation.getTranslatedName(card.getState(state));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(activatingPlayer);
|
||||
|
||||
@@ -7,8 +7,6 @@ public class Equip extends KeywordWithCost {
|
||||
public Equip() {
|
||||
}
|
||||
|
||||
public String getValidDescription() { return type; }
|
||||
|
||||
@Override
|
||||
protected void parse(String details) {
|
||||
String[] k = details.split(":");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package forge.game.keyword;
|
||||
|
||||
import forge.card.CardSplitType;
|
||||
import forge.StaticData;
|
||||
import forge.game.card.Card;
|
||||
import forge.item.PaperCard;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -141,8 +141,6 @@ public enum Keyword {
|
||||
OFFSPRING("Offspring", KeywordWithCost.class, false, "You may pay an additional %s as you cast this spell. If you do, when this creature enters, create a 1/1 token copy of it."),
|
||||
OVERLOAD("Overload", KeywordWithCost.class, false, "You may cast this spell for its overload cost. If you do, change its text by replacing all instances of \"target\" with \"each.\""),
|
||||
PARTNER("Partner", Partner.class, true, "You can have two commanders if both have partner."),
|
||||
PARTNER_SURVIVOR("Partner - Survivors", Partner.class, true, "You can have two commanders if both have this ability."),
|
||||
PARTNER_FATHER_AND_SON("Partner - Father & Son", Partner.class, true, "You can have two commanders if both have this ability."),
|
||||
PERSIST("Persist", SimpleKeyword.class, false, "When this creature dies, if it had no -1/-1 counters on it, return it to the battlefield under its owner's control with a -1/-1 counter on it."),
|
||||
PHASING("Phasing", SimpleKeyword.class, true, "This phases in or out before you untap during each of your untap steps. While it's phased out, it's treated as though it doesn't exist."),
|
||||
PLOT("Plot", KeywordWithCost.class, false, "You may pay %s and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery."),
|
||||
@@ -225,7 +223,7 @@ public enum Keyword {
|
||||
displayName = displayName0;
|
||||
}
|
||||
|
||||
private static Pair<Keyword, String> getKeywordDetails(String k) {
|
||||
public static KeywordInterface getInstance(String k) {
|
||||
Keyword keyword = Keyword.UNDEFINED;
|
||||
String details = k;
|
||||
// try to get real part
|
||||
@@ -257,20 +255,15 @@ public enum Keyword {
|
||||
keyword = smartValueOf(k);
|
||||
details = "";
|
||||
}
|
||||
return Pair.of(keyword, details);
|
||||
}
|
||||
|
||||
public static KeywordInterface getInstance(String k) {
|
||||
Pair<Keyword, String> p = getKeywordDetails(k);
|
||||
|
||||
KeywordInstance<?> inst;
|
||||
try {
|
||||
inst = p.getKey().type.getConstructor().newInstance();
|
||||
inst = keyword.type.getConstructor().newInstance();
|
||||
}
|
||||
catch (Exception e) {
|
||||
inst = new UndefinedKeyword();
|
||||
}
|
||||
inst.initialize(k, p.getKey(), p.getValue());
|
||||
inst.initialize(k, keyword, details);
|
||||
return inst;
|
||||
}
|
||||
|
||||
@@ -285,44 +278,36 @@ public enum Keyword {
|
||||
return keywords;
|
||||
}
|
||||
|
||||
private static Keyword get(String k) {
|
||||
if (k == null || k.isEmpty())
|
||||
return Keyword.UNDEFINED;
|
||||
|
||||
return getKeywordDetails(k).getKey();
|
||||
}
|
||||
|
||||
private static final Map<String, Set<Keyword>> cardKeywordSetLookup = new HashMap<>();
|
||||
|
||||
public static Set<Keyword> getKeywordSet(PaperCard card) {
|
||||
String name = card.getName();
|
||||
Set<Keyword> keywordSet = cardKeywordSetLookup.get(name);
|
||||
String key = card.getName();
|
||||
Set<Keyword> keywordSet = cardKeywordSetLookup.get(key);
|
||||
if (keywordSet == null) {
|
||||
CardSplitType cardSplitType = card.getRules().getSplitType();
|
||||
keywordSet = EnumSet.noneOf(Keyword.class);
|
||||
if (cardSplitType != CardSplitType.None && cardSplitType != CardSplitType.Split) {
|
||||
if (card.getRules().getOtherPart() != null) {
|
||||
if (card.getRules().getOtherPart().getKeywords() != null) {
|
||||
for (String key : card.getRules().getOtherPart().getKeywords()) {
|
||||
Keyword keyword = get(key);
|
||||
if (!Keyword.UNDEFINED.equals(keyword))
|
||||
keywordSet.add(keyword);
|
||||
}
|
||||
}
|
||||
keywordSet = new HashSet<>();
|
||||
for (KeywordInterface inst : Card.getCardForUi(card).getKeywords()) {
|
||||
final Keyword keyword = inst.getKeyword();
|
||||
if (keyword != Keyword.UNDEFINED) {
|
||||
keywordSet.add(keyword);
|
||||
}
|
||||
}
|
||||
if (card.getRules().getMainPart().getKeywords() != null) {
|
||||
for (String key : card.getRules().getMainPart().getKeywords()) {
|
||||
Keyword keyword = get(key);
|
||||
if (!Keyword.UNDEFINED.equals(keyword))
|
||||
keywordSet.add(keyword);
|
||||
}
|
||||
}
|
||||
cardKeywordSetLookup.put(name, keywordSet);
|
||||
cardKeywordSetLookup.put(card.getName(), keywordSet);
|
||||
}
|
||||
return keywordSet;
|
||||
}
|
||||
|
||||
public static Runnable getPreloadTask() {
|
||||
if (cardKeywordSetLookup.size() < 10000) { //allow preloading even if some but not all cards loaded
|
||||
return () -> {
|
||||
final Collection<PaperCard> cards = StaticData.instance().getCommonCards().getUniqueCards();
|
||||
for (PaperCard card : cards) {
|
||||
getKeywordSet(card);
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Keyword smartValueOf(String value) {
|
||||
for (final Keyword v : Keyword.values()) {
|
||||
if (v.displayName.equalsIgnoreCase(value)) {
|
||||
|
||||
@@ -3,50 +3,38 @@ package forge.game.keyword;
|
||||
import forge.card.CardType;
|
||||
|
||||
public class KeywordWithType extends KeywordInstance<KeywordWithType> {
|
||||
protected String type = null;
|
||||
protected String descType = null;
|
||||
protected String reminderType = null;
|
||||
|
||||
public String getValidType() { return type; }
|
||||
public String getTypeDescription() { return descType; }
|
||||
protected String type;
|
||||
|
||||
@Override
|
||||
protected void parse(String details) {
|
||||
String k[];
|
||||
if (details.contains(":")) {
|
||||
if (CardType.isACardType(details)) {
|
||||
type = details.toLowerCase();
|
||||
} else if (details.contains(":")) {
|
||||
switch (getKeyword()) {
|
||||
case AFFINITY:
|
||||
type = details.split(":")[1];
|
||||
// type lists defined by rules should not be changed by TextChange in reminder text
|
||||
if (type.equalsIgnoreCase("Outlaw")) {
|
||||
type = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock";
|
||||
} else if (type.equalsIgnoreCase("historic permanent")) {
|
||||
type = "artifact, legendary, and/or Saga permanent";
|
||||
}
|
||||
break;
|
||||
case BANDSWITH:
|
||||
case ENCHANT:
|
||||
case HEXPROOF:
|
||||
case LANDWALK:
|
||||
k = details.split(":");
|
||||
type = k[0];
|
||||
descType = k[1];
|
||||
type = details.split(":")[1];
|
||||
break;
|
||||
default:
|
||||
k = details.split(":");
|
||||
type = k[1];
|
||||
descType = k[0];
|
||||
type = details.split(":")[0];
|
||||
}
|
||||
} else {
|
||||
descType = type = details;
|
||||
}
|
||||
|
||||
if (CardType.isACardType(descType) || "Permanent".equals(descType) || "Player".equals(descType) || "Opponent".equals(descType)) {
|
||||
descType = descType.toLowerCase();
|
||||
} else if (descType.equalsIgnoreCase("Outlaw")) {
|
||||
reminderType = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock";
|
||||
} else if (type.equalsIgnoreCase("historic permanent")) {
|
||||
reminderType = "artifact, legendary, and/or Saga permanent";
|
||||
}
|
||||
if (reminderType == null) {
|
||||
reminderType = type;
|
||||
type = details;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String formatReminderText(String reminderText) {
|
||||
return String.format(reminderText, reminderType);
|
||||
return String.format(reminderText, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,6 @@ import java.util.*;
|
||||
public class PhaseHandler implements java.io.Serializable {
|
||||
private static final long serialVersionUID = 5207222278370963197L;
|
||||
|
||||
// used for debugging phase timing
|
||||
private final StopWatch sw = new StopWatch();
|
||||
|
||||
// Start turn at 0, since we start even before first untap
|
||||
private PhaseType phase = null;
|
||||
private int turn = 0;
|
||||
@@ -95,7 +92,6 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
|
||||
private final transient Game game;
|
||||
|
||||
|
||||
public PhaseHandler(final Game game0) {
|
||||
game = game0;
|
||||
}
|
||||
@@ -1007,7 +1003,12 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
|
||||
private final static boolean DEBUG_PHASES = false;
|
||||
|
||||
public void setupFirstTurn(Player goesFirst, Runnable startGameHook) {
|
||||
public void startFirstTurn(Player goesFirst) {
|
||||
startFirstTurn(goesFirst, null);
|
||||
}
|
||||
public void startFirstTurn(Player goesFirst, Runnable startGameHook) {
|
||||
StopWatch sw = new StopWatch();
|
||||
|
||||
if (phase != null) {
|
||||
throw new IllegalStateException("Turns already started, call this only once per game");
|
||||
}
|
||||
@@ -1023,146 +1024,132 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
startGameHook.run();
|
||||
givePriorityToPlayer = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void startFirstTurn(Player goesFirst) {
|
||||
startFirstTurn(goesFirst, null);
|
||||
}
|
||||
public void startFirstTurn(Player goesFirst, Runnable startGameHook) {
|
||||
setupFirstTurn(goesFirst, startGameHook);
|
||||
mainGameLoop();
|
||||
}
|
||||
|
||||
public void mainGameLoop() {
|
||||
// MAIN GAME LOOP
|
||||
while (!game.isGameOver() && !(game.getAge() == GameStage.RestartedByKarn)) {
|
||||
mainLoopStep();
|
||||
}
|
||||
}
|
||||
|
||||
public void mainLoopStep() {
|
||||
if (givePriorityToPlayer) {
|
||||
if (DEBUG_PHASES) {
|
||||
sw.start();
|
||||
}
|
||||
|
||||
game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer()));
|
||||
List<SpellAbility> chosenSa = null;
|
||||
|
||||
int loopCount = 0;
|
||||
do {
|
||||
if (checkStateBasedEffects()) {
|
||||
// state-based effects check could lead to game over
|
||||
return;
|
||||
}
|
||||
game.stashGameState();
|
||||
|
||||
chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay();
|
||||
|
||||
// this needs to come after chosenSa so it sees you conceding on own turn
|
||||
if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) {
|
||||
// If the active player has lost, and they have priority, set the next player to have priority
|
||||
System.out.println("Active player is no longer in the game...");
|
||||
pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer());
|
||||
pFirstPriority = pPlayerPriority;
|
||||
}
|
||||
|
||||
if (chosenSa == null) {
|
||||
break; // that means 'I pass'
|
||||
}
|
||||
while (!game.isGameOver()) {
|
||||
if (givePriorityToPlayer) {
|
||||
if (DEBUG_PHASES) {
|
||||
System.out.print("... " + pPlayerPriority + " plays " + chosenSa);
|
||||
sw.start();
|
||||
}
|
||||
|
||||
boolean rollback = false;
|
||||
for (SpellAbility sa : chosenSa) {
|
||||
Card saHost = sa.getHostCard();
|
||||
final Zone originZone = saHost.getZone();
|
||||
final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
|
||||
game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer()));
|
||||
List<SpellAbility> chosenSa = null;
|
||||
|
||||
if (pPlayerPriority.getController().playChosenSpellAbility(sa)) {
|
||||
// 117.3c If a player has priority when they cast a spell, activate an ability, [play a land]
|
||||
// that player receives priority afterward.
|
||||
pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve
|
||||
} else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) {
|
||||
rollback = true;
|
||||
int loopCount = 0;
|
||||
do {
|
||||
if (checkStateBasedEffects()) {
|
||||
// state-based effects check could lead to game over
|
||||
return;
|
||||
}
|
||||
game.stashGameState();
|
||||
|
||||
chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay();
|
||||
|
||||
// this needs to come after chosenSa so it sees you conceding on own turn
|
||||
if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) {
|
||||
// If the active player has lost, and they have priority, set the next player to have priority
|
||||
System.out.println("Active player is no longer in the game...");
|
||||
pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer());
|
||||
pFirstPriority = pPlayerPriority;
|
||||
}
|
||||
|
||||
saHost = game.getCardState(saHost);
|
||||
final Zone currentZone = saHost.getZone();
|
||||
|
||||
// Need to check if Zone did change
|
||||
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) {
|
||||
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
|
||||
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);
|
||||
triggerList.triggerChangesZoneAll(game, sa);
|
||||
if (chosenSa == null) {
|
||||
break; // that means 'I pass'
|
||||
}
|
||||
if (DEBUG_PHASES) {
|
||||
System.out.print("... " + pPlayerPriority + " plays " + chosenSa);
|
||||
}
|
||||
}
|
||||
// Don't copy last state if we're in the middle of rolling back a spell...
|
||||
if (!rollback) {
|
||||
game.copyLastState();
|
||||
}
|
||||
loopCount++;
|
||||
} while (loopCount < 999 || !pPlayerPriority.getController().isAI());
|
||||
|
||||
if (loopCount >= 999 && pPlayerPriority.getController().isAI()) {
|
||||
System.out.print("AI looped too much with: " + chosenSa);
|
||||
boolean rollback = false;
|
||||
for (SpellAbility sa : chosenSa) {
|
||||
Card saHost = sa.getHostCard();
|
||||
final Zone originZone = saHost.getZone();
|
||||
final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
|
||||
|
||||
if (pPlayerPriority.getController().playChosenSpellAbility(sa)) {
|
||||
// 117.3c If a player has priority when they cast a spell, activate an ability, [play a land]
|
||||
// that player receives priority afterward.
|
||||
pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve
|
||||
} else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) {
|
||||
rollback = true;
|
||||
}
|
||||
|
||||
saHost = game.getCardState(saHost);
|
||||
final Zone currentZone = saHost.getZone();
|
||||
|
||||
// Need to check if Zone did change
|
||||
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) {
|
||||
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
|
||||
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);
|
||||
triggerList.triggerChangesZoneAll(game, sa);
|
||||
}
|
||||
}
|
||||
// Don't copy last state if we're in the middle of rolling back a spell...
|
||||
if (!rollback) {
|
||||
game.copyLastState();
|
||||
}
|
||||
loopCount++;
|
||||
} while (loopCount < 999 || !pPlayerPriority.getController().isAI());
|
||||
|
||||
if (loopCount >= 999 && pPlayerPriority.getController().isAI()) {
|
||||
System.out.print("AI looped too much with: " + chosenSa);
|
||||
}
|
||||
|
||||
if (DEBUG_PHASES) {
|
||||
sw.stop();
|
||||
System.out.print("... passed in " + sw.getTime()/1000f + " s\n");
|
||||
System.out.println("\t\tStack: " + game.getStack());
|
||||
sw.reset();
|
||||
}
|
||||
}
|
||||
else if (DEBUG_PHASES) {
|
||||
System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n");
|
||||
}
|
||||
|
||||
// actingPlayer is the player who may act
|
||||
// the firstAction is the player who gained Priority First in this segment
|
||||
// of Priority
|
||||
Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer());
|
||||
|
||||
if (game.isGameOver() || nextPlayer == null) { return; } // conceded?
|
||||
|
||||
if (DEBUG_PHASES) {
|
||||
sw.stop();
|
||||
System.out.print("... passed in " + sw.getTime()/1000f + " s\n");
|
||||
System.out.println("\t\tStack: " + game.getStack());
|
||||
sw.reset();
|
||||
System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString()));
|
||||
}
|
||||
}
|
||||
else if (DEBUG_PHASES) {
|
||||
System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n");
|
||||
}
|
||||
if (pFirstPriority == nextPlayer) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
if (playerTurn.hasLost()) {
|
||||
setPriority(game.getNextPlayerAfter(playerTurn));
|
||||
} else {
|
||||
setPriority(playerTurn);
|
||||
}
|
||||
|
||||
// actingPlayer is the player who may act
|
||||
// the firstAction is the player who gained Priority First in this segment
|
||||
// of Priority
|
||||
Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer());
|
||||
|
||||
if (game.isGameOver() || nextPlayer == null) { return; } // conceded?
|
||||
|
||||
if (DEBUG_PHASES) {
|
||||
System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString()));
|
||||
}
|
||||
if (pFirstPriority == nextPlayer) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
if (playerTurn.hasLost()) {
|
||||
setPriority(game.getNextPlayerAfter(playerTurn));
|
||||
} else {
|
||||
setPriority(playerTurn);
|
||||
// end phase
|
||||
givePriorityToPlayer = true;
|
||||
onPhaseEnd();
|
||||
advanceToNextPhase();
|
||||
onPhaseBegin();
|
||||
}
|
||||
|
||||
// end phase
|
||||
givePriorityToPlayer = true;
|
||||
onPhaseEnd();
|
||||
advanceToNextPhase();
|
||||
onPhaseBegin();
|
||||
else if (!game.getStack().hasSimultaneousStackEntries()) {
|
||||
game.getStack().resolveStack();
|
||||
}
|
||||
} else {
|
||||
// pass the priority to other player
|
||||
pPlayerPriority = nextPlayer;
|
||||
}
|
||||
else if (!game.getStack().hasSimultaneousStackEntries()) {
|
||||
game.getStack().resolveStack();
|
||||
|
||||
// If ever the karn's ultimate resolved
|
||||
if (game.getAge() == GameStage.RestartedByKarn) {
|
||||
setPhase(null);
|
||||
game.updatePhaseForView();
|
||||
game.fireEvent(new GameEventGameRestarted(playerTurn));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// pass the priority to other player
|
||||
pPlayerPriority = nextPlayer;
|
||||
}
|
||||
|
||||
// If ever the karn's ultimate resolved
|
||||
if (game.getAge() == GameStage.RestartedByKarn) {
|
||||
setPhase(null);
|
||||
game.updatePhaseForView();
|
||||
game.fireEvent(new GameEventGameRestarted(playerTurn));
|
||||
return;
|
||||
}
|
||||
|
||||
// update Priority for all players
|
||||
for (final Player p : game.getPlayers()) {
|
||||
p.setHasPriority(getPriorityPlayer() == p);
|
||||
// update Priority for all players
|
||||
for (final Player p : game.getPlayers()) {
|
||||
p.setHasPriority(getPriorityPlayer() == p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1120,14 +1120,13 @@ public class Player extends GameEntity implements Comparable<Player> {
|
||||
getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave));
|
||||
}
|
||||
|
||||
surveilThisTurn++;
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
|
||||
runParams.put(AbilityKey.FirstTime, surveilThisTurn == 0);
|
||||
runParams.put(AbilityKey.FirstTime, surveilThisTurn == 1);
|
||||
if (params != null) {
|
||||
runParams.putAll(params);
|
||||
}
|
||||
getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false);
|
||||
|
||||
surveilThisTurn++;
|
||||
}
|
||||
|
||||
public int getSurveilThisTurn() {
|
||||
|
||||
@@ -242,7 +242,7 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
|
||||
String desc = AbilityUtils.applyDescriptionTextChangeEffects(getParam("Description"), this);
|
||||
ITranslatable nameSource = getHostName(this);
|
||||
desc = CardTranslation.translateMultipleDescriptionText(desc, nameSource);
|
||||
String translatedName = nameSource.getTranslatedName();
|
||||
String translatedName = CardTranslation.getTranslatedName(nameSource);
|
||||
desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName);
|
||||
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName));
|
||||
if (desc.contains("EFFECTSOURCE")) {
|
||||
|
||||
@@ -280,8 +280,15 @@ public class ReplacementHandler {
|
||||
host = game.getCardState(host);
|
||||
}
|
||||
|
||||
// TODO: the source of replacement effect should be the source of the original effect
|
||||
effectSA = replacementEffect.ensureAbility();
|
||||
if (replacementEffect.getOverridingAbility() == null && replacementEffect.hasParam("ReplaceWith")) {
|
||||
// TODO: the source of replacement effect should be the source of the original effect
|
||||
effectSA = AbilityFactory.getAbility(host, replacementEffect.getParam("ReplaceWith"), replacementEffect);
|
||||
//replacementEffect.setOverridingAbility(effectSA);
|
||||
//effectSA.setTrigger(true);
|
||||
} else if (replacementEffect.getOverridingAbility() != null) {
|
||||
effectSA = replacementEffect.getOverridingAbility();
|
||||
}
|
||||
|
||||
if (effectSA != null) {
|
||||
SpellAbility tailend = effectSA;
|
||||
do {
|
||||
|
||||
@@ -92,7 +92,7 @@ public abstract class AbilityActivated extends SpellAbility implements Cloneable
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getRestrictions().canPlay(c, this)) {
|
||||
if (!(this.getRestrictions().canPlay(c, this))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -174,8 +174,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
private CardZoneTable changeZoneTable;
|
||||
private Map<Player, Integer> loseLifeMap;
|
||||
|
||||
private String name = "";
|
||||
|
||||
public CardCollection getLastStateBattlefield() {
|
||||
return lastStateBattlefield;
|
||||
}
|
||||
@@ -1121,7 +1119,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
if (node.getHostCard() != null && !desc.isEmpty()) {
|
||||
ITranslatable nameSource = getHostName(node);
|
||||
desc = CardTranslation.translateMultipleDescriptionText(desc, nameSource);
|
||||
String translatedName = nameSource.getTranslatedName();
|
||||
String translatedName = CardTranslation.getTranslatedName(nameSource);
|
||||
desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName);
|
||||
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName));
|
||||
if (node.getOriginalHost() != null) {
|
||||
@@ -1248,9 +1246,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
clone.mayChooseNewTargets = false;
|
||||
|
||||
clone.triggeringObjects = AbilityKey.newMap(this.triggeringObjects);
|
||||
if (!lki) {
|
||||
clone.replacingObjects = AbilityKey.newMap();
|
||||
}
|
||||
|
||||
clone.setPayCosts(getPayCosts().copy());
|
||||
if (manaPart != null) {
|
||||
@@ -2680,12 +2675,4 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
public void clearOptionalKeywordAmount() {
|
||||
optionalKeywordAmount.clear();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ public class TargetRestrictions {
|
||||
|
||||
// Additional restrictions that may not fit into Valid
|
||||
private boolean uniqueTargets = false;
|
||||
private boolean singleZone = false;
|
||||
private boolean forEachPlayer = false;
|
||||
private boolean differentControllers = false;
|
||||
private boolean differentCMC = false;
|
||||
@@ -99,6 +100,7 @@ public class TargetRestrictions {
|
||||
this.tgtZone = target.getZone();
|
||||
this.saValidTargeting = target.getSAValidTargeting();
|
||||
this.uniqueTargets = target.isUniqueTargets();
|
||||
this.singleZone = target.isSingleZone();
|
||||
this.forEachPlayer = target.isForEachPlayer();
|
||||
this.differentControllers = target.isDifferentControllers();
|
||||
this.differentCMC = target.isDifferentCMC();
|
||||
@@ -536,6 +538,12 @@ public class TargetRestrictions {
|
||||
public final void setUniqueTargets(final boolean unique) {
|
||||
this.uniqueTargets = unique;
|
||||
}
|
||||
public final boolean isSingleZone() {
|
||||
return this.singleZone;
|
||||
}
|
||||
public final void setSingleZone(final boolean single) {
|
||||
this.singleZone = single;
|
||||
}
|
||||
public boolean isWithoutSameCreatureType() {
|
||||
return withoutSameCreatureType;
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
|
||||
if (hasParam("Description") && !this.isSuppressed()) {
|
||||
ITranslatable nameSource = getHostName(this);
|
||||
String desc = CardTranslation.translateSingleDescriptionText(getParam("Description"), nameSource);
|
||||
String translatedName = nameSource.getTranslatedName();
|
||||
String translatedName = CardTranslation.getTranslatedName(nameSource);
|
||||
desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName);
|
||||
desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName));
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
public class StaticAbilityCantAttach {
|
||||
|
||||
public static StaticAbility cantAttach(final GameEntity target, final Card card, boolean checkSBA) {
|
||||
public static boolean cantAttach(final GameEntity target, final Card card, boolean checkSBA) {
|
||||
// CantTarget static abilities
|
||||
for (final Card ca : target.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
|
||||
for (final StaticAbility stAb : ca.getStaticAbilities()) {
|
||||
@@ -15,11 +15,11 @@ public class StaticAbilityCantAttach {
|
||||
}
|
||||
|
||||
if (applyCantAttachAbility(stAb, card, target, checkSBA)) {
|
||||
return stAb;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target, boolean checkSBA) {
|
||||
|
||||
@@ -623,7 +623,7 @@ public final class StaticAbilityContinuous {
|
||||
// Mana cost
|
||||
affectedCard.addChangedManaCost(state.getManaCost(), se.getTimestamp(), stAb.getId());
|
||||
// color
|
||||
affectedCard.addColorByText(ColorSet.fromMask(state.getColor()), se.getTimestamp(), stAb);
|
||||
affectedCard.addColorByText(ColorSet.fromMask(state.getColor()), se.getTimestamp(), stAb.getId());
|
||||
// type
|
||||
affectedCard.addChangedCardTypesByText(new CardType(state.getType()), se.getTimestamp(), stAb.getId());
|
||||
// abilities
|
||||
@@ -856,7 +856,7 @@ public final class StaticAbilityContinuous {
|
||||
|
||||
// add colors
|
||||
if (addColors != null) {
|
||||
affectedCard.addColor(addColors, !overwriteColors, se.getTimestamp(), stAb);
|
||||
affectedCard.addColor(addColors, !overwriteColors, se.getTimestamp(), stAb.getId(), stAb.isCharacteristicDefining());
|
||||
}
|
||||
|
||||
if (layer == StaticAbilityLayer.RULES) {
|
||||
|
||||
@@ -124,7 +124,7 @@ public abstract class Trigger extends TriggerReplacementBase {
|
||||
String desc = getParam("TriggerDescription");
|
||||
if (!desc.contains("ABILITY")) {
|
||||
desc = CardTranslation.translateSingleDescriptionText(getParam("TriggerDescription"), nameSource);
|
||||
String translatedName = nameSource.getTranslatedName();
|
||||
String translatedName = CardTranslation.getTranslatedName(nameSource);
|
||||
desc = TextUtil.fastReplace(desc,"CARDNAME", translatedName);
|
||||
desc = TextUtil.fastReplace(desc,"NICKNAME", Lang.getInstance().getNickName(translatedName));
|
||||
if (desc.contains("ORIGINALHOST") && this.getOriginalHost() != null) {
|
||||
@@ -218,7 +218,7 @@ public abstract class Trigger extends TriggerReplacementBase {
|
||||
result = TextUtil.fastReplace(result, "ABILITY", saDesc);
|
||||
|
||||
result = CardTranslation.translateMultipleDescriptionText(result, sa.getHostCard());
|
||||
String translatedName = sa.getHostCard().getTranslatedName();
|
||||
String translatedName = CardTranslation.getTranslatedName(sa.getHostCard());
|
||||
result = TextUtil.fastReplace(result,"CARDNAME", translatedName);
|
||||
result = TextUtil.fastReplace(result,"NICKNAME", Lang.getInstance().getNickName(translatedName));
|
||||
}
|
||||
@@ -392,10 +392,6 @@ public abstract class Trigger extends TriggerReplacementBase {
|
||||
}
|
||||
}
|
||||
|
||||
if (condition == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("LifePaid".equals(condition)) {
|
||||
final SpellAbility trigSA = (SpellAbility) runParams.get(AbilityKey.SpellAbility);
|
||||
if (trigSA != null && trigSA.getAmountLifePaid() <= 0) {
|
||||
@@ -446,15 +442,7 @@ public abstract class Trigger extends TriggerReplacementBase {
|
||||
if (game.getCombat().getAttackersAndDefenders().values().containsAll(attacker.getOpponents())) {
|
||||
return false;
|
||||
}
|
||||
} else if (condition.startsWith("FromNamedAbility")) {
|
||||
var rest = condition.substring(16);
|
||||
final SpellAbility trigSA = (SpellAbility) runParams.get(AbilityKey.Cause);
|
||||
|
||||
if (trigSA != null && !trigSA.getName().equals(rest)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,8 @@ import java.util.Map;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Expressions;
|
||||
import forge.util.Localizer;
|
||||
|
||||
/**
|
||||
@@ -63,13 +59,8 @@ public class TriggerAttackerBlocked extends Trigger {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasParam("ValidBlocker")) {
|
||||
String param = getParamOrDefault("ValidBlockerAmount", "GE1");
|
||||
int attackers = CardLists.getValidCardCount((CardCollection) runParams.get(AbilityKey.Blockers), getParam("ValidBlocker"), getHostCard().getController(), getHostCard(), this);
|
||||
int amount = AbilityUtils.calculateAmount(getHostCard(), param.substring(2), this);
|
||||
if (!Expressions.compare(attackers, param, amount)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesValidParam("ValidBlocker", runParams.get(AbilityKey.Blockers))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -139,7 +139,6 @@ public enum TriggerType {
|
||||
SpellCast(TriggerSpellAbilityCastOrCopy.class),
|
||||
SpellCastOrCopy(TriggerSpellAbilityCastOrCopy.class),
|
||||
SpellCopy(TriggerSpellAbilityCastOrCopy.class),
|
||||
Stationed(TriggerCrewedSaddled.class),
|
||||
Surveil(TriggerSurveil.class),
|
||||
TakesInitiative(TriggerTakesInitiative.class),
|
||||
TapAll(TriggerTapAll.class),
|
||||
|
||||
@@ -453,16 +453,6 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sp.isKeyword(Keyword.STATION) && (sp.getHostCard().getType().hasSubtype("Spacecraft") || (sp.getHostCard().getType().hasSubtype("Planet")))) {
|
||||
Iterable<Card> crews = sp.getPaidList("Tapped", true);
|
||||
if (crews != null) {
|
||||
for (Card c : crews) {
|
||||
Map<AbilityKey, Object> stationParams = AbilityKey.mapFromCard(sp.getHostCard());
|
||||
stationParams.put(AbilityKey.Crew, c);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Stationed, stationParams, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Run Copy triggers
|
||||
if (sp.isSpell()) {
|
||||
|
||||
@@ -464,7 +464,7 @@ public class TrackableTypes {
|
||||
public static final TrackableType<ColorSet> ColorSetType = new TrackableType<ColorSet>() {
|
||||
@Override
|
||||
public ColorSet getDefaultValue() {
|
||||
return ColorSet.NO_COLORS;
|
||||
return ColorSet.getNullColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<groupId>org.robolectric</groupId>
|
||||
<artifactId>android-all</artifactId>
|
||||
<!-- update version: 16-robolectric-13921718 but needs to fix Android 16 Edge to edge enforcement -->
|
||||
<version>15-robolectric-13954326</version>
|
||||
<version>15-robolectric-12650502</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -156,7 +156,7 @@
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-android</artifactId>
|
||||
<version>8.21.1</version>
|
||||
<version>8.19.1</version>
|
||||
<type>aar</type>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@@ -177,7 +177,7 @@
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-android-core</artifactId>
|
||||
<version>8.21.1</version>
|
||||
<version>8.19.1</version>
|
||||
<type>aar</type>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@@ -201,7 +201,7 @@
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-android-ndk</artifactId>
|
||||
<version>8.21.1</version>
|
||||
<version>8.19.1</version>
|
||||
<type>aar</type>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
|
||||
@@ -6,7 +6,7 @@ import java.awt.Graphics;
|
||||
import javax.swing.JTable;
|
||||
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
import forge.toolbox.CardFaceSymbols;
|
||||
|
||||
public class ColorSetRenderer extends ItemCellRenderer {
|
||||
@@ -33,7 +33,7 @@ public class ColorSetRenderer extends ItemCellRenderer {
|
||||
this.cs = (ColorSet) value;
|
||||
}
|
||||
else {
|
||||
this.cs = ColorSet.NO_COLORS;
|
||||
this.cs = ColorSet.getNullColor();
|
||||
}
|
||||
this.setToolTipText(cs.toString());
|
||||
return super.getTableCellRendererComponent(table, "", isSelected, hasFocus, row, column);
|
||||
@@ -57,8 +57,8 @@ public class ColorSetRenderer extends ItemCellRenderer {
|
||||
final int offsetIfNoSpace = cntGlyphs > 1 ? (cellWidth - padding0 - elemtWidth) / (cntGlyphs - 1) : elemtWidth + elemtGap;
|
||||
final int dx = Math.min(elemtWidth + elemtGap, offsetIfNoSpace);
|
||||
|
||||
for (final MagicColor.Color s : cs.getOrderedColors()) {
|
||||
CardFaceSymbols.drawManaSymbol(s.getShortName(), g, x, y);
|
||||
for (final ManaCostShard s : cs.getOrderedShards()) {
|
||||
CardFaceSymbols.drawManaSymbol(s.getImageKey(), g, x, y);
|
||||
x += dx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public class AddBasicLandsDialog {
|
||||
private static final int LAND_PANEL_PADDING = 3;
|
||||
|
||||
private final FComboBoxPanel<CardEdition> cbLandSet = new FComboBoxPanel<>(Localizer.getInstance().getMessage("lblLandSet") + ":", FlowLayout.CENTER,
|
||||
IterableUtil.filter(StaticData.instance().getSortedEditions(), CardEdition::hasBasicLands));
|
||||
IterableUtil.filter(StaticData.instance().getSortedEditions(), CardEdition.Predicates.hasBasicLands));
|
||||
|
||||
private final MainPanel panel = new MainPanel();
|
||||
private final LandPanel pnlPlains = new LandPanel("Plains");
|
||||
|
||||
@@ -46,6 +46,7 @@ import forge.toolbox.*;
|
||||
import forge.util.Localizer;
|
||||
import forge.view.FDialog;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import static forge.deck.DeckRecognizer.TokenType.*;
|
||||
|
||||
@@ -522,7 +523,7 @@ public class DeckImport<TModel extends DeckBase> extends FDialog {
|
||||
else
|
||||
deck.setName(currentDeckName);
|
||||
}
|
||||
host.getDeckController().loadDeck(deck, controller.getImportBehavior() != DeckImportController.ImportBehavior.MERGE);
|
||||
host.getDeckController().loadDeck(deck, controller.getCreateNewDeck());
|
||||
processWindowEvent(new WindowEvent(DeckImport.this, WindowEvent.WINDOW_CLOSING));
|
||||
});
|
||||
|
||||
@@ -530,7 +531,7 @@ public class DeckImport<TModel extends DeckBase> extends FDialog {
|
||||
this.createNewDeckCheckbox.setSelected(false);
|
||||
this.createNewDeckCheckbox.addActionListener(e -> {
|
||||
boolean createNewDeck = createNewDeckCheckbox.isSelected();
|
||||
controller.setImportBehavior(createNewDeck ? DeckImportController.ImportBehavior.CREATE_NEW : DeckImportController.ImportBehavior.MERGE);
|
||||
controller.setCreateNewDeck(createNewDeck);
|
||||
String cmdAcceptLabel = createNewDeck ? CREATE_NEW_DECK_CMD_LABEL : IMPORT_CARDS_CMD_LABEL;
|
||||
cmdAcceptButton.setText(cmdAcceptLabel);
|
||||
String smartCardArtChboxTooltip = createNewDeck ? SMART_CARDART_TT_NO_DECK : SMART_CARDART_TT_WITH_DECK;
|
||||
@@ -599,7 +600,7 @@ public class DeckImport<TModel extends DeckBase> extends FDialog {
|
||||
if (token.getType() == LIMITED_CARD)
|
||||
cssClass = WARN_MSG_CLASS;
|
||||
String statusMsg = String.format("<span class=\"%s\" style=\"font-size: 9px;\">%s</span>", cssClass,
|
||||
controller.getTokenStatusMessage(token));
|
||||
getTokenStatusMessage(token));
|
||||
statusLbl.append(statusMsg);
|
||||
}
|
||||
|
||||
@@ -739,12 +740,12 @@ public class DeckImport<TModel extends DeckBase> extends FDialog {
|
||||
private String toHTML(final DeckRecognizer.Token token) {
|
||||
if (token == null)
|
||||
return "";
|
||||
String tokenMsg = controller.getTokenMessage(token);
|
||||
String tokenMsg = getTokenMessage(token);
|
||||
if (tokenMsg == null)
|
||||
return "";
|
||||
String tokenStatus = controller.getTokenStatusMessage(token);
|
||||
String tokenStatus = getTokenStatusMessage(token);
|
||||
String cssClass = getTokenCSSClass(token.getType());
|
||||
if (tokenStatus.isEmpty())
|
||||
if (tokenStatus.length() == 0)
|
||||
tokenMsg = padEndWithHTMLSpaces(tokenMsg, 2*PADDING_TOKEN_MSG_LENGTH+10);
|
||||
else {
|
||||
tokenMsg = padEndWithHTMLSpaces(tokenMsg, PADDING_TOKEN_MSG_LENGTH);
|
||||
@@ -754,6 +755,11 @@ public class DeckImport<TModel extends DeckBase> extends FDialog {
|
||||
tokenMsg = String.format("<a class=\"%s\" href=\"%s\">%s</a>", cssClass,
|
||||
token.getKey().toString(), tokenMsg);
|
||||
|
||||
if (tokenStatus == null) {
|
||||
String tokenTag = String.format("<td colspan=\"2\" class=\"%s\">%s</td>", cssClass, tokenMsg);
|
||||
return String.format("<tr>%s</tr>", tokenTag);
|
||||
}
|
||||
|
||||
String tokenTag = "<td class=\"%s\">%s</td>";
|
||||
String tokenMsgTag = String.format(tokenTag, cssClass, tokenMsg);
|
||||
String tokenStatusTag;
|
||||
@@ -770,6 +776,97 @@ public class DeckImport<TModel extends DeckBase> extends FDialog {
|
||||
return String.format("%s%s", targetMsg, spacer);
|
||||
}
|
||||
|
||||
private String getTokenMessage(DeckRecognizer.Token token) {
|
||||
switch (token.getType()) {
|
||||
case LEGAL_CARD:
|
||||
case LIMITED_CARD:
|
||||
case CARD_FROM_NOT_ALLOWED_SET:
|
||||
case CARD_FROM_INVALID_SET:
|
||||
return String.format("%s x %s %s", token.getQuantity(), token.getText(), getTokenFoilLabel(token));
|
||||
// Card Warning Msgs
|
||||
case UNKNOWN_CARD:
|
||||
case UNSUPPORTED_CARD:
|
||||
return token.getQuantity() > 0 ? String.format("%s x %s", token.getQuantity(), token.getText())
|
||||
: token.getText();
|
||||
|
||||
case UNSUPPORTED_DECK_SECTION:
|
||||
return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"),
|
||||
Localizer.getInstance()
|
||||
.getMessage("lblWarnDeckSectionNotAllowedInEditor", token.getText(),
|
||||
this.currentGameType));
|
||||
|
||||
// Special Case of Card moved into another section (e.g. Commander from Sideboard)
|
||||
case WARNING_MESSAGE:
|
||||
return String.format("%s: %s", Localizer.getInstance()
|
||||
.getMessage("lblWarningMsgPrefix"), token.getText());
|
||||
|
||||
// Placeholders
|
||||
case DECK_SECTION_NAME:
|
||||
return String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckSection"),
|
||||
token.getText());
|
||||
|
||||
case CARD_RARITY:
|
||||
return String.format("%s: %s", Localizer.getInstance().getMessage("lblRarity"),
|
||||
token.getText());
|
||||
|
||||
case CARD_TYPE:
|
||||
case CARD_CMC:
|
||||
case MANA_COLOUR:
|
||||
case COMMENT:
|
||||
return token.getText();
|
||||
|
||||
case DECK_NAME:
|
||||
return String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckName"),
|
||||
token.getText());
|
||||
|
||||
case UNKNOWN_TEXT:
|
||||
default:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private String getTokenStatusMessage(DeckRecognizer.Token token){
|
||||
if (token == null)
|
||||
return "";
|
||||
|
||||
switch (token.getType()) {
|
||||
case LIMITED_CARD:
|
||||
return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"),
|
||||
Localizer.getInstance().getMessage("lblWarnLimitedCard",
|
||||
StringUtils.capitalize(token.getLimitedCardType().name()), getGameFormatLabel()));
|
||||
|
||||
case CARD_FROM_NOT_ALLOWED_SET:
|
||||
return Localizer.getInstance().getMessage("lblErrNotAllowedCard", getGameFormatLabel());
|
||||
|
||||
case CARD_FROM_INVALID_SET:
|
||||
return Localizer.getInstance().getMessage("lblErrCardEditionDate");
|
||||
|
||||
case UNSUPPORTED_CARD:
|
||||
return Localizer.getInstance().getMessage("lblErrUnsupportedCard", this.currentGameType);
|
||||
|
||||
case UNKNOWN_CARD:
|
||||
return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"),
|
||||
Localizer.getInstance().getMessage("lblWarnUnknownCardMsg"));
|
||||
|
||||
case UNSUPPORTED_DECK_SECTION:
|
||||
case WARNING_MESSAGE:
|
||||
case COMMENT:
|
||||
case CARD_CMC:
|
||||
case MANA_COLOUR:
|
||||
case CARD_TYPE:
|
||||
case DECK_SECTION_NAME:
|
||||
case CARD_RARITY:
|
||||
case DECK_NAME:
|
||||
case LEGAL_CARD:
|
||||
case UNKNOWN_TEXT:
|
||||
default:
|
||||
return "";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getTokenCSSClass(DeckRecognizer.TokenType tokenType){
|
||||
switch (tokenType){
|
||||
case LEGAL_CARD:
|
||||
@@ -802,6 +899,17 @@ public class DeckImport<TModel extends DeckBase> extends FDialog {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String getTokenFoilLabel(DeckRecognizer.Token token) {
|
||||
if (!token.isCardToken())
|
||||
return "";
|
||||
final String foilMarker = "- (Foil)";
|
||||
return token.getCard().isFoil() ? foilMarker : "";
|
||||
}
|
||||
|
||||
private String getGameFormatLabel() {
|
||||
return String.format("\"%s\"", this.controller.getCurrentGameFormatName());
|
||||
}
|
||||
}
|
||||
|
||||
class GameFormatDropdownRenderer extends JLabel implements ListCellRenderer<GameFormat> {
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.util.StringTokenizer;
|
||||
import com.esotericsoftware.minlog.Log;
|
||||
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
import forge.gui.GuiBase;
|
||||
@@ -205,9 +204,9 @@ public class CardFaceSymbols {
|
||||
}
|
||||
|
||||
public static void drawColorSet(Graphics g, ColorSet colorSet, int x, int y, int imageSize, boolean vertical) {
|
||||
for (final MagicColor.Color s : colorSet.getOrderedColors()) {
|
||||
if (DECK_COLORSET.get(s.getShortName())!=null)
|
||||
FSkin.drawImage(g, DECK_COLORSET.get(s.getShortName()), x, y, imageSize, imageSize);
|
||||
for (final ManaCostShard s : colorSet.getOrderedShards()) {
|
||||
if (DECK_COLORSET.get(s.getImageKey())!=null)
|
||||
FSkin.drawImage(g, DECK_COLORSET.get(s.getImageKey()), x, y, imageSize, imageSize);
|
||||
if (!vertical)
|
||||
x += imageSize;
|
||||
else
|
||||
|
||||
@@ -69,7 +69,22 @@ public class PlayerDetailsPanel extends JPanel {
|
||||
}
|
||||
|
||||
public static FSkinProp iconFromZone(ZoneType zoneType) {
|
||||
return FSkinProp.iconFromZone(zoneType, false);
|
||||
switch (zoneType) {
|
||||
case Hand: return FSkinProp.IMG_ZONE_HAND;
|
||||
case Library: return FSkinProp.IMG_ZONE_LIBRARY;
|
||||
case Graveyard: return FSkinProp.IMG_ZONE_GRAVEYARD;
|
||||
case Exile: return FSkinProp.IMG_ZONE_EXILE;
|
||||
case Sideboard: return FSkinProp.IMG_ZONE_SIDEBOARD;
|
||||
case Flashback: return FSkinProp.IMG_ZONE_FLASHBACK;
|
||||
case Command: return FSkinProp.IMG_ZONE_COMMAND; //IMG_PLANESWALKER
|
||||
case PlanarDeck: return FSkinProp.IMG_ZONE_PLANAR;
|
||||
case SchemeDeck: return FSkinProp.IMG_ZONE_SCHEME;
|
||||
case AttractionDeck: return FSkinProp.IMG_ZONE_ATTRACTION;
|
||||
case ContraptionDeck: return FSkinProp.IMG_ZONE_CONTRAPTION;
|
||||
case Ante: return FSkinProp.IMG_ZONE_ANTE;
|
||||
case Junkyard: return FSkinProp.IMG_ZONE_JUNKYARD;
|
||||
default: return FSkinProp.IMG_HDZONE_LIBRARY;
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds various labels to pool area JPanel container. */
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package forge.ai;
|
||||
|
||||
import forge.game.Game;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.zone.ZoneType;
|
||||
import org.testng.AssertJUnit;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class AIIntegrationTests extends AITest {
|
||||
@Test
|
||||
public void testSwingForLethal() {
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(1);
|
||||
p.setTeam(0);
|
||||
addCard("Nest Robber", p);
|
||||
addCard("Nest Robber", p);
|
||||
|
||||
Player opponent = game.getPlayers().get(0);
|
||||
opponent.setTeam(1);
|
||||
|
||||
addCard("Runeclaw Bear", opponent);
|
||||
opponent.setLife(2, null);
|
||||
|
||||
this.playUntilPhase(game, PhaseType.END_OF_TURN);
|
||||
|
||||
AssertJUnit.assertTrue(game.isGameOver());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuspendAI() {
|
||||
// Test that the AI can cast a suspend spell
|
||||
// They should suspend it, then deal three damage to their opponent
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(1);
|
||||
p.setTeam(0);
|
||||
addCard("Mountain", p);
|
||||
addCardToZone("Rift Bolt", p, ZoneType.Hand);
|
||||
|
||||
Player opponent = game.getPlayers().get(0);
|
||||
opponent.setTeam(1);
|
||||
|
||||
// Fill deck with lands so we can goldfish a few turns
|
||||
for (int i = 0; i < 60; i++) {
|
||||
addCardToZone("Island", opponent, ZoneType.Library);
|
||||
// Add something they can't cast
|
||||
addCardToZone("Stone Golem", p, ZoneType.Library);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
this.playUntilNextTurn(game);
|
||||
}
|
||||
|
||||
AssertJUnit.assertEquals(17, opponent.getLife());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttackTriggers() {
|
||||
// Test that attack triggers actually trigger
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(1);
|
||||
p.setTeam(0);
|
||||
addCard("Hellrider", p);
|
||||
addCard("Raging Goblin", p);
|
||||
|
||||
Player opponent = game.getPlayers().get(0);
|
||||
opponent.setTeam(1);
|
||||
|
||||
// Fill deck with lands so we can goldfish a few turns
|
||||
for (int i = 0; i < 60; i++) {
|
||||
addCardToZone("Island", opponent, ZoneType.Library);
|
||||
// Add something they can't cast
|
||||
addCardToZone("Stone Golem", p, ZoneType.Library);
|
||||
}
|
||||
|
||||
this.playUntilNextTurn(game);
|
||||
|
||||
AssertJUnit.assertEquals(14, opponent.getLife());
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.GuiDesktop;
|
||||
import forge.StaticData;
|
||||
import forge.deck.Deck;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameRules;
|
||||
import forge.game.GameStage;
|
||||
import forge.game.GameType;
|
||||
import forge.game.Match;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.RegisteredPlayer;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.gui.GuiBase;
|
||||
import forge.item.IPaperCard;
|
||||
import forge.item.PaperToken;
|
||||
import forge.localinstance.properties.ForgePreferences.FPref;
|
||||
import forge.model.FModel;
|
||||
|
||||
public class AITest {
|
||||
private static boolean initialized = false;
|
||||
|
||||
public Game resetGame() {
|
||||
// need to be done after FModel.initialize, or the Localizer isn't loaded yet
|
||||
List<RegisteredPlayer> players = Lists.newArrayList();
|
||||
Deck d1 = new Deck();
|
||||
players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p2", null)));
|
||||
players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p1", null)));
|
||||
GameRules rules = new GameRules(GameType.Constructed);
|
||||
Match match = new Match(rules, players, "Test");
|
||||
Game game = new Game(players, rules, match);
|
||||
Player p = game.getPlayers().get(1);
|
||||
game.setAge(GameStage.Play);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
game.getPhaseHandler().onStackResolved();
|
||||
// game.getAction().checkStateEffects(true);
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
protected Game initAndCreateGame() {
|
||||
if (!initialized) {
|
||||
GuiBase.setInterface(new GuiDesktop());
|
||||
FModel.initialize(null, preferences -> {
|
||||
preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false);
|
||||
preferences.setPref(FPref.UI_LANGUAGE, "en-US");
|
||||
return null;
|
||||
});
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
return resetGame();
|
||||
}
|
||||
|
||||
protected int countCardsWithName(Game game, String name) {
|
||||
int i = 0;
|
||||
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
if (c.getName().equals(name)) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
protected Card findCardWithName(Game game, String name) {
|
||||
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
if (c.getName().equals(name)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected SpellAbility findSAWithPrefix(Card c, String prefix) {
|
||||
return findSAWithPrefix(c.getSpellAbilities(), prefix);
|
||||
}
|
||||
|
||||
protected SpellAbility findSAWithPrefix(Iterable<SpellAbility> abilities, String prefix) {
|
||||
for (SpellAbility sa : abilities) {
|
||||
if (sa.getDescription().startsWith(prefix)) {
|
||||
return sa;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Card createCard(String name, Player p) {
|
||||
IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
|
||||
if (paperCard == null) {
|
||||
StaticData.instance().attemptToLoadCard(name);
|
||||
paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
|
||||
}
|
||||
return Card.fromPaperCard(paperCard, p);
|
||||
}
|
||||
|
||||
protected Card addCardToZone(String name, Player p, ZoneType zone) {
|
||||
Card c = createCard(name, p);
|
||||
// card need a new Timestamp otherwise Static Abilities might collide
|
||||
c.setGameTimestamp(p.getGame().getNextTimestamp());
|
||||
p.getZone(zone).add(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
protected Card addCard(String name, Player p) {
|
||||
return addCardToZone(name, p, ZoneType.Battlefield);
|
||||
}
|
||||
|
||||
protected List<Card> addCards(String name, int count, Player p) {
|
||||
List<Card> cards = Lists.newArrayList();
|
||||
for (int i = 0; i < count; i++) {
|
||||
cards.add(addCard(name, p));
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
protected Card createToken(String name, Player p) {
|
||||
PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name);
|
||||
if (token == null) {
|
||||
System.out.println("Failed to find token name " + name);
|
||||
return null;
|
||||
}
|
||||
return CardFactory.getCard(token, p, p.getGame());
|
||||
}
|
||||
|
||||
protected List<Card> addTokens(String name, int amount, Player p) {
|
||||
List<Card> cards = new ArrayList<>();
|
||||
|
||||
for(int i = 0; i < amount; i++) {
|
||||
cards.add(addToken(name, p));
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
protected Card addToken(String name, Player p) {
|
||||
Card c = createToken(name, p);
|
||||
// card need a new Timestamp otherwise Static Abilities might collide
|
||||
c.setGameTimestamp(p.getGame().getNextTimestamp());
|
||||
p.getZone(ZoneType.Battlefield).add(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
void playUntilStackClear(Game game) {
|
||||
do {
|
||||
game.getPhaseHandler().mainLoopStep();
|
||||
} while (!game.isGameOver() && !game.getStack().isEmpty());
|
||||
}
|
||||
|
||||
void playUntilPhase(Game game, PhaseType phase) {
|
||||
do {
|
||||
game.getPhaseHandler().mainLoopStep();
|
||||
} while (!game.isGameOver() && !game.getPhaseHandler().is(phase));
|
||||
}
|
||||
|
||||
void playUntilNextTurn(Game game) {
|
||||
Player current = game.getPhaseHandler().getPlayerTurn();
|
||||
do {
|
||||
game.getPhaseHandler().mainLoopStep();
|
||||
} while (!game.isGameOver() && game.getPhaseHandler().getPlayerTurn().equals(current));
|
||||
}
|
||||
|
||||
protected String gameStateToString(Game game) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (ZoneType zone : ZoneType.values()) {
|
||||
CardCollectionView cards = game.getCardsIn(zone);
|
||||
if (!cards.isEmpty()) {
|
||||
sb.append("Zone ").append(zone.name()).append(":\n");
|
||||
for (Card c : game.getCardsIn(zone)) {
|
||||
sb.append(" ").append(c);
|
||||
if (c.isTapped()) {
|
||||
sb.append(" (T)");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package forge.ai.simulation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.GuiDesktop;
|
||||
import forge.StaticData;
|
||||
import forge.ai.AIOption;
|
||||
import forge.ai.AITest;
|
||||
import forge.ai.LobbyPlayerAi;
|
||||
import forge.ai.simulation.GameStateEvaluator.Score;
|
||||
import forge.deck.Deck;
|
||||
@@ -16,12 +18,21 @@ import forge.game.GameRules;
|
||||
import forge.game.GameStage;
|
||||
import forge.game.GameType;
|
||||
import forge.game.Match;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.RegisteredPlayer;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.gui.GuiBase;
|
||||
import forge.item.IPaperCard;
|
||||
import forge.item.PaperToken;
|
||||
import forge.localinstance.properties.ForgePreferences.FPref;
|
||||
import forge.model.FModel;
|
||||
|
||||
public class SimulationTest extends AITest {
|
||||
public class SimulationTest {
|
||||
private static boolean initialized = false;
|
||||
|
||||
public Game resetGame() {
|
||||
// need to be done after FModel.initialize, or the Localizer isn't loaded yet
|
||||
@@ -42,6 +53,19 @@ public class SimulationTest extends AITest {
|
||||
return game;
|
||||
}
|
||||
|
||||
protected Game initAndCreateGame() {
|
||||
if (!initialized) {
|
||||
GuiBase.setInterface(new GuiDesktop());
|
||||
FModel.initialize(null, preferences -> {
|
||||
preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false);
|
||||
preferences.setPref(FPref.UI_LANGUAGE, "en-US");
|
||||
return null;
|
||||
});
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
return resetGame();
|
||||
}
|
||||
|
||||
protected GameSimulator createSimulator(Game game, Player p) {
|
||||
return new GameSimulator(new SimulationController(new Score(0)) {
|
||||
@@ -51,4 +75,106 @@ public class SimulationTest extends AITest {
|
||||
}
|
||||
}, game, p, null);
|
||||
}
|
||||
|
||||
protected int countCardsWithName(Game game, String name) {
|
||||
int i = 0;
|
||||
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
if (c.getName().equals(name)) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
protected Card findCardWithName(Game game, String name) {
|
||||
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
if (c.getName().equals(name)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String gameStateToString(Game game) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (ZoneType zone : ZoneType.values()) {
|
||||
CardCollectionView cards = game.getCardsIn(zone);
|
||||
if (!cards.isEmpty()) {
|
||||
sb.append("Zone ").append(zone.name()).append(":\n");
|
||||
for (Card c : game.getCardsIn(zone)) {
|
||||
sb.append(" ").append(c).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected SpellAbility findSAWithPrefix(Card c, String prefix) {
|
||||
return findSAWithPrefix(c.getSpellAbilities(), prefix);
|
||||
}
|
||||
|
||||
protected SpellAbility findSAWithPrefix(Iterable<SpellAbility> abilities, String prefix) {
|
||||
for (SpellAbility sa : abilities) {
|
||||
if (sa.getDescription().startsWith(prefix)) {
|
||||
return sa;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Card createCard(String name, Player p) {
|
||||
IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
|
||||
if (paperCard == null) {
|
||||
StaticData.instance().attemptToLoadCard(name);
|
||||
paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
|
||||
}
|
||||
return Card.fromPaperCard(paperCard, p);
|
||||
}
|
||||
|
||||
protected Card addCardToZone(String name, Player p, ZoneType zone) {
|
||||
Card c = createCard(name, p);
|
||||
// card need a new Timestamp otherwise Static Abilities might collide
|
||||
c.setGameTimestamp(p.getGame().getNextTimestamp());
|
||||
p.getZone(zone).add(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
protected Card addCard(String name, Player p) {
|
||||
return addCardToZone(name, p, ZoneType.Battlefield);
|
||||
}
|
||||
|
||||
protected List<Card> addCards(String name, int count, Player p) {
|
||||
List<Card> cards = Lists.newArrayList();
|
||||
for (int i = 0; i < count; i++) {
|
||||
cards.add(addCard(name, p));
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
protected Card createToken(String name, Player p) {
|
||||
PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name);
|
||||
if (token == null) {
|
||||
System.out.println("Failed to find token name " + name);
|
||||
return null;
|
||||
}
|
||||
return CardFactory.getCard(token, p, p.getGame());
|
||||
}
|
||||
|
||||
protected List<Card> addTokens(String name, int amount, Player p) {
|
||||
List<Card> cards = new ArrayList<>();
|
||||
|
||||
for(int i = 0; i < amount; i++) {
|
||||
cards.add(addToken(name, p));
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
protected Card addToken(String name, Player p) {
|
||||
Card c = createToken(name, p);
|
||||
// card need a new Timestamp otherwise Static Abilities might collide
|
||||
c.setGameTimestamp(p.getGame().getNextTimestamp());
|
||||
p.getZone(ZoneType.Battlefield).add(c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>6.9.0</version>
|
||||
<version>6.8.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -101,7 +101,6 @@ public class Forge implements ApplicationListener {
|
||||
public static boolean allowCardBG = false;
|
||||
public static boolean altPlayerLayout = false;
|
||||
public static boolean altZoneTabs = false;
|
||||
public static String altZoneTabMode = "Off";
|
||||
public static boolean animatedCardTapUntap = false;
|
||||
public static String enableUIMask = "Crop";
|
||||
public static String selector = "Default";
|
||||
@@ -223,7 +222,7 @@ public class Forge implements ApplicationListener {
|
||||
reversedPrompt = getForgePreferences().getPrefBoolean(FPref.UI_REVERSE_PROMPT_BUTTON);
|
||||
autoAIDeckSelection = getForgePreferences().getPrefBoolean(FPref.UI_AUTO_AIDECK_SELECTION);
|
||||
altPlayerLayout = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERINFOLAYOUT);
|
||||
setAltZoneTabMode(getForgePreferences().getPref(FPref.UI_ALT_PLAYERZONETABS));
|
||||
altZoneTabs = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS);
|
||||
animatedCardTapUntap = getForgePreferences().getPrefBoolean(FPref.UI_ANIMATED_CARD_TAPUNTAP);
|
||||
enableUIMask = getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING);
|
||||
if (getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("true")) //override old settings if not updated
|
||||
@@ -261,17 +260,6 @@ public class Forge implements ApplicationListener {
|
||||
FThreads.invokeInBackgroundThread(() -> AssetsDownloader.checkForUpdates(exited, runnable));
|
||||
}
|
||||
}
|
||||
public static void setAltZoneTabMode(String mode) {
|
||||
Forge.altZoneTabMode = mode;
|
||||
switch (Forge.altZoneTabMode) {
|
||||
case "Vertical", "Horizontal" -> Forge.altZoneTabs = true;
|
||||
case "Off" -> Forge.altZoneTabs = false;
|
||||
default -> Forge.altZoneTabs = false;
|
||||
}
|
||||
}
|
||||
public static boolean isHorizontalTabLayout() {
|
||||
return Forge.altZoneTabs && "Horizontal".equalsIgnoreCase(Forge.altZoneTabMode);
|
||||
}
|
||||
public static boolean hasGamepad() {
|
||||
//Classic Mode Various Screen GUI are not yet supported, needs control mapping for each screens
|
||||
if (isMobileAdventureMode) {
|
||||
@@ -349,11 +337,8 @@ public class Forge implements ApplicationListener {
|
||||
GuiBase.setIsAdventureMode(true);
|
||||
advStartup = false;
|
||||
isMobileAdventureMode = true;
|
||||
//force it for adventure mode if the prefs is not updated from boolean value to string value
|
||||
if ("true".equalsIgnoreCase(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS)) ||
|
||||
"false".equalsIgnoreCase(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS))) {
|
||||
setAltZoneTabMode("Vertical");
|
||||
}
|
||||
if (GuiBase.isAndroid()) //force it for adventure mode
|
||||
altZoneTabs = true;
|
||||
//pixl cursor for adventure
|
||||
setCursor(null, "0");
|
||||
if (!GuiBase.isAndroid() || !getDeviceAdapter().getGamepads().isEmpty())
|
||||
@@ -770,7 +755,7 @@ public class Forge implements ApplicationListener {
|
||||
isMobileAdventureMode = false;
|
||||
GuiBase.setIsAdventureMode(false);
|
||||
setCursor(FSkin.getCursor().get(0), "0");
|
||||
setAltZoneTabMode(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS));
|
||||
altZoneTabs = FModel.getPreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS);
|
||||
Gdx.input.setInputProcessor(getInputProcessor());
|
||||
clearTransitionScreen();
|
||||
openHomeDefault();
|
||||
@@ -1483,7 +1468,7 @@ public class Forge implements ApplicationListener {
|
||||
|
||||
boolean handled;
|
||||
if (KeyInputAdapter.isShiftKeyDown()) {
|
||||
handled = pan(mouseMovedX, mouseMovedY, -Utils.AVG_FINGER_WIDTH * amountY, 0, false);
|
||||
handled = pan(mouseMovedX, mouseMovedY, -Utils.AVG_FINGER_WIDTH * amountX, 0, false);
|
||||
} else {
|
||||
handled = pan(mouseMovedX, mouseMovedY, 0, -Utils.AVG_FINGER_HEIGHT * amountY, true);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class EntryActor extends MapActor{
|
||||
{
|
||||
if(targetMap==null||targetMap.isEmpty())
|
||||
{
|
||||
stage.exitDungeon(false, false);
|
||||
stage.exitDungeon(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public class PortalActor extends EntryActor {
|
||||
}
|
||||
if (currentAnimationType == PortalAnimationTypes.Active) {
|
||||
if (targetMap == null || targetMap.isEmpty()) {
|
||||
stage.exitDungeon(false, false);
|
||||
stage.exitDungeon(false);
|
||||
} else {
|
||||
if (targetMap.equals(currentMap)) {
|
||||
stage.spawn(entryTargetObject);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user