Compare commits

..

1 Commits

Author SHA1 Message Date
Chris H
9bf567a966 Migrate SPM (and some other early spoilers) 2025-09-10 19:01:19 -04:00
942 changed files with 3421 additions and 5399 deletions

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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
);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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) {
/*

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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());

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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 };
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -52,4 +52,9 @@ public interface IPaperCard extends InventoryItem, Serializable {
default String getUntranslatedType() {
return getRules().getType().toString();
}
@Override
default String getUntranslatedOracle() {
return getRules().getOracleText();
}
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 "";
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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());

View File

@@ -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() {

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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")) {

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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")) {

View File

@@ -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();

View File

@@ -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()) {

View File

@@ -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")

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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)));

View File

@@ -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

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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 thats 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);
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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)) {

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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())) {

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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(":");

View File

@@ -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)) {

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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() {

View File

@@ -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")) {

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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),

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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");

View File

@@ -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> {

View File

@@ -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

View File

@@ -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. */

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -43,7 +43,7 @@ public class EntryActor extends MapActor{
{
if(targetMap==null||targetMap.isEmpty())
{
stage.exitDungeon(false, false);
stage.exitDungeon(false);
}
else
{

View File

@@ -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