Compare commits

..

5 Commits

Author SHA1 Message Date
Chris H
432d152d9a Manually fixing version touches 2025-09-21 20:46:30 -04:00
GitHub Actions
f7c0ef9428 Restore POM files for preparation of next release 2025-09-21 23:40:14 +00:00
GitHub Actions
5f6934782c [maven-release-plugin] prepare for next development iteration 2025-09-21 23:31:38 +00:00
GitHub Actions
a69333b528 [maven-release-plugin] prepare release forge-2.0.06 2025-09-21 23:31:36 +00:00
Chris H
2beb71cdce Update apt-get so latest libxml2-utils is available 2025-09-21 19:21:23 -04:00
689 changed files with 1440 additions and 2521 deletions

View File

@@ -887,9 +887,28 @@ public class AiController {
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
final Card host = sa.getHostCard();
if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) {
// 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.
AiPlayDecision canPlay = canPlaySa(sa);
@@ -906,7 +925,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;
}
@@ -1798,9 +1817,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 +1833,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 +1846,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 +1856,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,10 +3104,9 @@ 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; }
if (sa.hasParam("AITgts")) {
CardCollection list;
String aiTgts = sa.getParam("AITgts");
if (aiTgts.startsWith("BetterThan")) {
@@ -3135,7 +3134,11 @@ public class ComputerUtil {
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
return list;
} else {
return srcList;
}
}
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;
}
@@ -761,8 +765,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 +891,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.

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,7 +53,8 @@ public class ControlExchangeAi extends SpellAbilityAi {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (mandatory) {
} else {
if (mandatory) {
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
@@ -63,6 +64,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
} 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,9 +92,10 @@ public class CountersPutAi extends CountersAi {
return false;
}
return chance > MyRandom.getRandom().nextFloat();
}
} else {
return false;
}
}
if (sa.isKeyword(Keyword.LEVEL_UP)) {
// creatures enchanted by curse auras have low priority
@@ -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);
}
} 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,9 +1210,10 @@ public class CountersPutAi extends CountersAi {
}
if (numCtrs < optimalCMC) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();

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

@@ -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,8 +55,7 @@ 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

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

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

@@ -325,12 +325,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()) {
legal = true; // commander with background
@@ -348,7 +342,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

@@ -1,8 +1,6 @@
package forge.card;
import com.google.common.collect.ImmutableList;
import forge.util.ITranslatable;
import forge.util.Localizer;
/**
@@ -159,7 +157,7 @@ public final class MagicColor {
}
}
public enum Color implements ITranslatable {
public enum Color {
WHITE(Constant.WHITE, MagicColor.WHITE, "W", "lblWhite"),
BLUE(Constant.BLUE, MagicColor.BLUE, "U", "lblBlue"),
BLACK(Constant.BLACK, MagicColor.BLACK, "B", "lblBlack"),
@@ -190,7 +188,6 @@ public final class MagicColor {
}
}
@Override
public String getName() {
return name;
}
@@ -198,8 +195,7 @@ public final class MagicColor {
return shortName;
}
@Override
public String getTranslatedName() {
public String getLocalizedName() {
return Localizer.getInstance().getMessage(label);
}
@@ -209,6 +205,10 @@ public final class MagicColor {
public String getSymbol() {
return symbol;
}
@Override
public String toString() {
return getLocalizedName();
}
}
}

View File

@@ -994,7 +994,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,8 +1013,8 @@ 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 localisedName1 = magicColor1.getLocalizedName();
String localisedName2 = magicColor2.getLocalizedName();
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColorMask() | magicColor2.getColorMask());
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
}

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

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

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

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;
@@ -751,29 +749,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);
}
}
@@ -1602,7 +1597,9 @@ public class GameAction {
}
// recheck the game over condition at this point to make sure no other win conditions apply now.
if (!game.isGameOver()) {
checkGameOverCondition();
}
if (game.getAge() != GameStage.Play) {
return false;
@@ -1883,10 +1880,6 @@ public class GameAction {
}
public void checkGameOverCondition() {
if (game.isGameOver()) {
return;
}
// award loses as SBE
GameEndReason reason = null;
List<Player> losers = null;

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() && !canBeEquippedBy(attach, sa)) {
return false;
}
if (attach.isEquipment()) {
String msg = cantBeEquippedByMsg(attach, sa);
if (msg != null) {
return msg;
}
}
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();
String k = ki.getOriginal();
String m[] = k.split(":");
String v = m[1];
if (!isValid(v.split(","), aura.getController(), aura, null)) {
return getName() + " is not " + Lang.nounWithAmount(1, desc);
return false;
}
}
}
return null;
return true;
}
public boolean hasCounters() {

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");
@@ -2888,6 +2889,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);
@@ -2902,6 +2918,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()
@@ -3725,6 +3748,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);
}
@@ -3733,10 +3760,6 @@ public class AbilityUtils {
return CardUtil.getColorsFromCards(paidList).countColors();
}
if (string.equals("DifferentCardNames")) {
return CardLists.getDifferentNamesCount(paidList);
}
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

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

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

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

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

@@ -449,7 +449,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() {
@@ -965,7 +965,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 +980,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 +1013,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);
@@ -2459,8 +2453,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 +2607,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(")");
@@ -4297,10 +4299,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 +4396,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 +4433,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);
}
@@ -6857,7 +6861,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 +7171,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 true;
}
return null;
}
@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 isValid(sa.getTargetRestrictions().getValidTgts(), sa.getActivatingPlayer(), equip, sa);
}
return null;
}
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) {
@@ -7856,8 +7849,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

@@ -466,29 +466,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 +496,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 +522,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 +581,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 +638,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 +672,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 +683,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 +696,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 +724,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 +764,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;
@@ -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

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

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

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

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

@@ -1121,7 +1121,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) {

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

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,14 +59,9 @@ 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)) {
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

@@ -56,7 +56,7 @@ public class PlayerZone extends Zone {
boolean graveyardCastable = c.hasKeyword(Keyword.FLASHBACK) ||
c.hasKeyword(Keyword.RETRACE) || c.hasKeyword(Keyword.JUMP_START) || c.hasKeyword(Keyword.ESCAPE) ||
c.hasKeyword(Keyword.DISTURB) || c.hasKeyword(Keyword.MAYHEM);
c.hasKeyword(Keyword.DISTURB);
boolean exileCastable = c.isForetold() || c.isOnAdventure();
for (final SpellAbility sa : c.getSpellAbilities()) {
final ZoneType restrictZone = sa.getRestrictions().getZone();

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

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

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

@@ -32,7 +32,6 @@ import java.util.stream.Collectors;
public class AdventureEventData implements Serializable {
private static final long serialVersionUID = 1L;
private static final int JUMPSTART_TO_PICK_FROM = 6;
public transient BoosterDraft draft;
public AdventureEventParticipant[] participants;
public int rounds;
@@ -79,6 +78,7 @@ public class AdventureEventData implements Serializable {
matchesLost = other.matchesLost;
}
public Deck[] getRewardPacks(int count) {
Deck[] ret = new Deck[count];
for (int i = 0; i < count; i++) {
@@ -104,17 +104,173 @@ public class AdventureEventData implements Serializable {
return;
cardBlockName = cardBlock.getName();
setupDraftRewards();
//Below all to be fully generated in later release
rewardPacks = getRewardPacks(3);
generateParticipants(7);
if (cardBlock != null) {
packConfiguration = getBoosterConfiguration(cardBlock);
rewards = new AdventureEventData.AdventureEventReward[4];
AdventureEventData.AdventureEventReward r0 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r1 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r2 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r3 = new AdventureEventData.AdventureEventReward();
r0.minWins = 0;
r0.maxWins = 0;
r0.cardRewards = new Deck[]{rewardPacks[0]};
rewards[0] = r0;
r1.minWins = 1;
r1.maxWins = 3;
r1.cardRewards = new Deck[]{rewardPacks[1], rewardPacks[2]};
rewards[1] = r1;
r2.minWins = 2;
r2.maxWins = 3;
r2.itemRewards = new String[]{"Challenge Coin"};
rewards[2] = r2;
}
} else if (format == AdventureEventController.EventFormat.Jumpstart) {
int numPacksToPickFrom = 6;
generateParticipants(7);
cardBlock = pickJumpstartCardBlock();
if (cardBlock == null)
return;
cardBlockName = cardBlock.getName();
jumpstartBoosters = AdventureEventController.instance().getJumpstartBoosters(cardBlock, JUMPSTART_TO_PICK_FROM);
jumpstartBoosters = AdventureEventController.instance().getJumpstartBoosters(cardBlock, numPacksToPickFrom);
packConfiguration = new String[]{cardBlock.getLandSet().getCode(), cardBlock.getLandSet().getCode(), cardBlock.getLandSet().getCode()};
setupJumpstartRewards();
for (AdventureEventParticipant participant : participants) {
List<Deck> availableOptions = AdventureEventController.instance().getJumpstartBoosters(cardBlock, numPacksToPickFrom);
List<Deck> chosenPacks = new ArrayList<>();
Map<String, List<Deck>> themeMap = new HashMap<>();
//1. Search for matching themes from deck names, fill deck with them if possible
for (Deck option : availableOptions) {
// This matches up theme for all except DMU - with only 2 per color the next part will handle that
String theme = option.getName().replaceAll("\\d$", "").trim();
if (!themeMap.containsKey(theme)) {
themeMap.put(theme, new ArrayList<>());
}
themeMap.get(theme).add(option);
}
String themeAdded = "";
boolean done = false;
while (!done) {
for (int i = packConfiguration.length - chosenPacks.size(); i > 1; i--) {
if (themeAdded.isEmpty()) {
for (String theme : themeMap.keySet()) {
if (themeMap.get(theme).size() >= i) {
themeAdded = theme;
break;
}
}
}
}
if (themeAdded.isEmpty()) {
done = true;
} else {
chosenPacks.addAll(themeMap.get(themeAdded).subList(0, Math.min(themeMap.get(themeAdded).size(), packConfiguration.length - chosenPacks.size())));
availableOptions.removeAll(themeMap.get(themeAdded));
themeMap.remove(themeAdded);
themeAdded = "";
}
}
//2. Fill remaining slots with colors already picked whenever possible
Map<String, List<Deck>> colorMap = new HashMap<>();
for (Deck option : availableOptions) {
if (option.getTags().contains("black"))
colorMap.computeIfAbsent("black", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("blue"))
colorMap.computeIfAbsent("blue", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("green"))
colorMap.computeIfAbsent("green", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("red"))
colorMap.computeIfAbsent("red", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("white"))
colorMap.computeIfAbsent("white", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("multicolor"))
colorMap.computeIfAbsent("multicolor", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("colorless"))
colorMap.computeIfAbsent("colorless", (k) -> new ArrayList<>()).add(option);
}
done = false;
String colorAdded = "";
while (!done) {
List<String> colorsAlreadyPicked = new ArrayList<>();
for (Deck picked : chosenPacks) {
if (picked.getTags().contains("black")) colorsAlreadyPicked.add("black");
if (picked.getTags().contains("blue")) colorsAlreadyPicked.add("blue");
if (picked.getTags().contains("green")) colorsAlreadyPicked.add("green");
if (picked.getTags().contains("red")) colorsAlreadyPicked.add("red");
if (picked.getTags().contains("white")) colorsAlreadyPicked.add("white");
if (picked.getTags().contains("multicolor")) colorsAlreadyPicked.add("multicolor");
if (picked.getTags().contains("colorless")) colorsAlreadyPicked.add("colorless");
}
while (colorAdded.isEmpty() && !colorsAlreadyPicked.isEmpty()) {
String colorToTry = Aggregates.removeRandom(colorsAlreadyPicked);
for (Deck toCheck : availableOptions) {
if (toCheck.getTags().contains(colorToTry)) {
colorAdded = colorToTry;
chosenPacks.add(toCheck);
availableOptions.remove(toCheck);
break;
}
}
}
//3. If no matching color found and need more packs, add any available at random.
if (packConfiguration.length > chosenPacks.size() && colorAdded.isEmpty() && !availableOptions.isEmpty()) {
chosenPacks.add(Aggregates.removeRandom(availableOptions));
colorAdded = "";
} else {
done = colorAdded.isEmpty() || packConfiguration.length <= chosenPacks.size();
colorAdded = "";
}
}
participant.registeredDeck = new Deck();
for (Deck chosen : chosenPacks) {
participant.registeredDeck.getMain().addAllFlat(chosen.getMain().toFlatList());
}
}
rewards = new AdventureEventData.AdventureEventReward[4];
AdventureEventData.AdventureEventReward r0 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r1 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r2 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r3 = new AdventureEventData.AdventureEventReward();
RewardData r0gold = new RewardData();
r0gold.count = 100;
r0gold.type = "gold";
r0.rewards = new RewardData[]{r0gold};
r0.minWins = 1;
r0.maxWins = 1;
rewards[0] = r0;
RewardData r1gold = new RewardData();
r1gold.count = 200;
r1gold.type = "gold";
r1.rewards = new RewardData[]{r1gold};
r1.minWins = 2;
r1.maxWins = 2;
rewards[1] = r1;
r2.minWins = 3;
r2.maxWins = 3;
RewardData r2gold = new RewardData();
r2gold.count = 500;
r2gold.type = "gold";
r2.rewards = new RewardData[]{r2gold};
rewards[2] = r2;
r3.minWins = 0;
r3.maxWins = 3;
rewards[3] = r3;
//r3 will be the selected card packs
}
}
@@ -136,7 +292,7 @@ public class AdventureEventData implements Serializable {
Random placeholder = MyRandom.getRandom();
MyRandom.setRandom(getEventRandom());
if (draft == null && (eventStatus == AdventureEventController.EventStatus.Available || eventStatus == AdventureEventController.EventStatus.Entered)) {
draft = BoosterDraft.createDraft(LimitedPoolType.Block, getCardBlock(), packConfiguration, participants.length);
draft = BoosterDraft.createDraft(LimitedPoolType.Block, getCardBlock(), packConfiguration, 8);
registeredDeck = draft.getHumanPlayer().getDeck();
assignPlayerNames(draft);
}
@@ -153,7 +309,6 @@ public class AdventureEventData implements Serializable {
private static final Predicate<CardEdition> filterStandard = FModel.getFormats().getStandard().editionLegalPredicate;
public static Predicate<CardEdition> selectSetPool() {
// Should we negate any of these to avoid overlap?
final int rollD100 = MyRandom.getRandom().nextInt(100);
Predicate<CardEdition> rolledFilter;
if (rollD100 < 30) {
@@ -168,6 +323,7 @@ public class AdventureEventData implements Serializable {
return rolledFilter;
}
private CardBlock pickWeightedCardBlock() {
CardEdition.Collection editions = FModel.getMagicDb().getEditions();
ConfigData configData = Config.instance().getConfigData();
@@ -276,66 +432,6 @@ public class AdventureEventData implements Serializable {
return legalBlocks.isEmpty() ? null : Aggregates.random(legalBlocks);
}
private void setupDraftRewards() {
//Below all to be fully generated in later release
rewardPacks = getRewardPacks(3);
if (cardBlock != null) {
packConfiguration = getBoosterConfiguration(cardBlock);
rewards = new AdventureEventData.AdventureEventReward[4];
AdventureEventData.AdventureEventReward r0 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r1 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r2 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r3 = new AdventureEventData.AdventureEventReward();
r0.minWins = 0;
r0.maxWins = 0;
r0.cardRewards = new Deck[]{rewardPacks[0]};
rewards[0] = r0;
r1.minWins = 1;
r1.maxWins = 3;
r1.cardRewards = new Deck[]{rewardPacks[1], rewardPacks[2]};
rewards[1] = r1;
r2.minWins = 2;
r2.maxWins = 3;
r2.itemRewards = new String[]{"Challenge Coin"};
rewards[2] = r2;
}
}
private void setupJumpstartRewards() {
rewards = new AdventureEventData.AdventureEventReward[4];
AdventureEventData.AdventureEventReward r0 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r1 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r2 = new AdventureEventData.AdventureEventReward();
AdventureEventData.AdventureEventReward r3 = new AdventureEventData.AdventureEventReward();
RewardData r0gold = new RewardData();
r0gold.count = 100;
r0gold.type = "gold";
r0.rewards = new RewardData[]{r0gold};
r0.minWins = 1;
r0.maxWins = 1;
rewards[0] = r0;
RewardData r1gold = new RewardData();
r1gold.count = 200;
r1gold.type = "gold";
r1.rewards = new RewardData[]{r1gold};
r1.minWins = 2;
r1.maxWins = 2;
rewards[1] = r1;
r2.minWins = 3;
r2.maxWins = 3;
RewardData r2gold = new RewardData();
r2gold.count = 500;
r2gold.type = "gold";
r2.rewards = new RewardData[]{r2gold};
rewards[2] = r2;
r3.minWins = 0;
r3.maxWins = 3;
rewards[3] = r3;
//r3 will be the selected card packs
}
public String[] getBoosterConfiguration(CardBlock selectedBlock) {
Random placeholder = MyRandom.getRandom();
@@ -369,106 +465,6 @@ public class AdventureEventData implements Serializable {
}
participants[numberOfOpponents] = getHumanPlayer();
if (format == AdventureEventController.EventFormat.Jumpstart) {
for (AdventureEventParticipant participant : participants) {
List<Deck> availableOptions = AdventureEventController.instance().getJumpstartBoosters(cardBlock, JUMPSTART_TO_PICK_FROM);
List<Deck> chosenPacks = new ArrayList<>();
Map<String, List<Deck>> themeMap = new HashMap<>();
//1. Search for matching themes from deck names, fill deck with them if possible
for (Deck option : availableOptions) {
// This matches up theme for all except DMU - with only 2 per color the next part will handle that
String theme = option.getName().replaceAll("\\d$", "").trim();
if (!themeMap.containsKey(theme)) {
themeMap.put(theme, new ArrayList<>());
}
themeMap.get(theme).add(option);
}
String themeAdded = "";
boolean done = false;
while (!done) {
for (int i = packConfiguration.length - chosenPacks.size(); i > 1; i--) {
if (themeAdded.isEmpty()) {
for (String theme : themeMap.keySet()) {
if (themeMap.get(theme).size() >= i) {
themeAdded = theme;
break;
}
}
}
}
if (themeAdded.isEmpty()) {
done = true;
} else {
chosenPacks.addAll(themeMap.get(themeAdded).subList(0, Math.min(themeMap.get(themeAdded).size(), packConfiguration.length - chosenPacks.size())));
availableOptions.removeAll(themeMap.get(themeAdded));
themeMap.remove(themeAdded);
themeAdded = "";
}
}
//2. Fill remaining slots with colors already picked whenever possible
Map<String, List<Deck>> colorMap = new HashMap<>();
for (Deck option : availableOptions) {
if (option.getTags().contains("black"))
colorMap.computeIfAbsent("black", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("blue"))
colorMap.computeIfAbsent("blue", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("green"))
colorMap.computeIfAbsent("green", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("red"))
colorMap.computeIfAbsent("red", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("white"))
colorMap.computeIfAbsent("white", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("multicolor"))
colorMap.computeIfAbsent("multicolor", (k) -> new ArrayList<>()).add(option);
if (option.getTags().contains("colorless"))
colorMap.computeIfAbsent("colorless", (k) -> new ArrayList<>()).add(option);
}
done = false;
String colorAdded = "";
while (!done) {
List<String> colorsAlreadyPicked = new ArrayList<>();
for (Deck picked : chosenPacks) {
if (picked.getTags().contains("black")) colorsAlreadyPicked.add("black");
if (picked.getTags().contains("blue")) colorsAlreadyPicked.add("blue");
if (picked.getTags().contains("green")) colorsAlreadyPicked.add("green");
if (picked.getTags().contains("red")) colorsAlreadyPicked.add("red");
if (picked.getTags().contains("white")) colorsAlreadyPicked.add("white");
if (picked.getTags().contains("multicolor")) colorsAlreadyPicked.add("multicolor");
if (picked.getTags().contains("colorless")) colorsAlreadyPicked.add("colorless");
}
while (colorAdded.isEmpty() && !colorsAlreadyPicked.isEmpty()) {
String colorToTry = Aggregates.removeRandom(colorsAlreadyPicked);
for (Deck toCheck : availableOptions) {
if (toCheck.getTags().contains(colorToTry)) {
colorAdded = colorToTry;
chosenPacks.add(toCheck);
availableOptions.remove(toCheck);
break;
}
}
}
//3. If no matching color found and need more packs, add any available at random.
if (packConfiguration.length > chosenPacks.size() && colorAdded.isEmpty() && !availableOptions.isEmpty()) {
chosenPacks.add(Aggregates.removeRandom(availableOptions));
colorAdded = "";
} else {
done = colorAdded.isEmpty() || packConfiguration.length <= chosenPacks.size();
colorAdded = "";
}
}
participant.registeredDeck = new Deck();
for (Deck chosen : chosenPacks) {
participant.registeredDeck.getMain().addAllFlat(chosen.getMain().toFlatList());
}
}
}
}
private void assignPlayerNames(BoosterDraft draft) {
@@ -521,31 +517,6 @@ public class AdventureEventData implements Serializable {
}
matches.get(round).add(match);
}
} else if (style == AdventureEventController.EventStyle.RoundRobin) {
// In a roundrobin everyone plays everyone else once
// We do have this logic already in ForgeTOurnament, we should see if we could reuse it
matches.put(round, new ArrayList<>());
activePlayers = Arrays.stream(participants).collect(Collectors.toList());
if (round > 1) {
AdventureEventParticipant pivot = activePlayers.remove(0);
for(int i = 1; i < round; i++) {
// Rotate X amount of players, where X is the current round-1
AdventureEventParticipant rotate = activePlayers.remove(0);
activePlayers.add(rotate);
}
activePlayers.add(0, pivot);
}
int numPlayers = activePlayers.size();
for (int i = 0; i < numPlayers / 2; i++) {
AdventureEventMatch match = new AdventureEventMatch();
match.p1 = activePlayers.get(i);
match.p2 = activePlayers.get(numPlayers - i - 1);
matches.get(round).add(match);
}
} else {
System.out.println(style + " not yet implemented!!!");
}
return matches.get(currentRound);
}
@@ -608,58 +579,7 @@ public class AdventureEventData implements Serializable {
}
//todo: more robust logic for event types that can be won without perfect record (Swiss w/cut, round robin)
if (style == AdventureEventController.EventStyle.Bracket) {
playerWon = matchesLost == 0 || matchesWon == rounds;
} else if (style == AdventureEventController.EventStyle.RoundRobin) {
if (matchesWon == rounds) {
playerWon = true;
} else {
//If multiple players are tied for first, only the one with the best tiebreaker wins
List<AdventureEventParticipant> topPlayers = new ArrayList<>();
int bestRecord = 0;
for (AdventureEventParticipant p : participants) {
if (p.wins > bestRecord) {
bestRecord = p.wins;
topPlayers.clear();
topPlayers.add(p);
} else if (p.wins == bestRecord) {
topPlayers.add(p);
}
}
if (topPlayers.size() == 1) {
playerWon = topPlayers.get(0).getName().equals(getHumanPlayer().getName());
} else {
//multiple players tied for first, use tiebreaker
Map<AdventureEventParticipant, Integer> tiebreakers = new HashMap<>();
for (AdventureEventParticipant p : topPlayers) {
int tb = 0;
for (AdventureEventMatch m : matches.values().stream().flatMap(List::stream).collect(Collectors.toList())) {
if (m.p1 == p && m.winner != null && m.winner != p) {
tb += m.p2.wins;
} else if (m.p2 == p && m.winner != null && m.winner != p) {
tb += m.p1.wins;
}
}
tiebreakers.put(p, tb);
}
int bestTiebreaker = 0;
AdventureEventParticipant winner = null;
boolean tie = false;
for (AdventureEventParticipant p : tiebreakers.keySet()) {
if (tiebreakers.get(p) > bestTiebreaker) {
bestTiebreaker = tiebreakers.get(p);
winner = p;
tie = false;
} else if (tiebreakers.get(p) == bestTiebreaker) {
tie = true;
}
}
playerWon = !tie && winner != null && winner.getName().equals(getHumanPlayer().getName());
}
}
} else {
playerWon = false;
}
eventStatus = AdventureEventController.EventStatus.Awarded;
}
@@ -889,7 +809,7 @@ public class AdventureEventData implements Serializable {
public boolean isNoSell = false;
}
public enum PairingStyle {
enum PairingStyle {
SingleElimination,
DoubleElimination,
Swiss,

View File

@@ -1194,7 +1194,9 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
public void removeItem(String name) {
inventoryItems.stream().filter(itemData -> name.equalsIgnoreCase(itemData.name)).findFirst().ifPresent(this::removeItem);
ItemData item = ItemListData.getItem(name);
if (item != null)
removeItem(item);
}
public void removeItem(ItemData item) {

View File

@@ -277,7 +277,7 @@ public class DuelScene extends ForgeScene {
currentEnemy = enemy.getData();
boolean bossBattle = currentEnemy.boss;
for (int i = 0; i < playerCount && currentEnemy != null; i++) {
for (int i = 0; i < 8 && currentEnemy != null; i++) {
Deck deck;
if (this.chaosBattle) { //random challenge for chaos mode

View File

@@ -51,7 +51,6 @@ public class EventScene extends MenuScene implements IAfterMatch {
static PointOfInterestChanges changes;
private Array<DialogData> entryDialog;
private AdventureEventData.AdventureEventMatch humanMatch = null;
private int packsSelected = 0; //Used for meta drafts, booster drafts will use existing logic.
@@ -491,7 +490,7 @@ public class EventScene extends MenuScene implements IAfterMatch {
}
public void startRound() {
for (AdventureEventData.AdventureEventMatch match : currentEvent.getMatches(currentEvent.currentRound)) {
for (AdventureEventData.AdventureEventMatch match : currentEvent.matches.get(currentEvent.currentRound)) {
match.round = currentEvent.currentRound;
if (match.winner != null) continue;
@@ -540,6 +539,8 @@ public class EventScene extends MenuScene implements IAfterMatch {
}
}
AdventureEventData.AdventureEventMatch humanMatch = null;
public void setWinner(boolean winner, boolean isArena) {
if (winner) {
humanMatch.winner = humanMatch.p1;

View File

@@ -5,7 +5,6 @@ import forge.StaticData;
import forge.adventure.data.AdventureEventData;
import forge.adventure.player.AdventurePlayer;
import forge.adventure.pointofintrest.PointOfInterestChanges;
import forge.card.CardEdition;
import forge.deck.Deck;
import forge.item.BoosterPack;
import forge.item.PaperCard;
@@ -100,9 +99,8 @@ public class AdventureEventController implements Serializable {
AdventureEventData e;
// After a certain amount of wins, stop offering jump start events
if (Current.player().getStatistic().totalWins() < 10 &&
random.nextInt(10) <= 2) {
// TODO After a certain amount of wins, stop offering jump start events
if (random.nextInt(10) <= 2) {
e = new AdventureEventData(eventSeed, EventFormat.Jumpstart);
} else {
e = new AdventureEventData(eventSeed, EventFormat.Draft);
@@ -112,26 +110,12 @@ public class AdventureEventController implements Serializable {
//covers cases where (somehow) editions that do not match the event style have been picked up
return null;
}
// If chosen event seed recommends a 4 person pod, run it as a RoundRobin
CardEdition firstSet = e.cardBlock.getSets().get(0);
int podSize = firstSet.getDraftOptions().getRecommendedPodSize();
e.sourceID = pointID;
e.eventOrigin = eventOrigin;
e.style = podSize == 4 ? EventStyle.RoundRobin : style;
e.eventRules = new AdventureEventData.AdventureEventRules(e.format, changes == null ? 1f : changes.getTownPriceModifier());
e.style = style;
AdventureEventData.PairingStyle pairingStyle;
if (e.style == EventStyle.RoundRobin) {
pairingStyle = AdventureEventData.PairingStyle.RoundRobin;
} else {
pairingStyle = AdventureEventData.PairingStyle.SingleElimination;
}
e.eventRules = new AdventureEventData.AdventureEventRules(e.format, pairingStyle, changes == null ? 1f : changes.getTownPriceModifier());
e.generateParticipants(podSize - 1); //-1 to account for the player
switch (e.style) {
switch (style) {
case Swiss:
case Bracket:
e.rounds = (e.participants.length / 2) - 1;
@@ -155,8 +139,9 @@ public class AdventureEventController implements Serializable {
output.setComment(setCode);
return output;
}
public Deck generateBoosterByColor(String color)
{
public Deck generateBoosterByColor(String color) {
List<PaperCard> cards = BoosterPack.fromColor(color).getCards();
Deck output = new Deck();
output.getMain().add(cards);

View File

@@ -30,7 +30,6 @@ import forge.itemmanager.ItemManager.ContextMenuBuilder;
import forge.itemmanager.ItemManagerConfig;
import forge.itemmanager.filters.ItemFilter;
import forge.localinstance.properties.ForgePreferences.FPref;
import forge.localinstance.skin.FSkinProp;
import forge.menu.*;
import forge.model.FModel;
import forge.screens.FScreen;
@@ -410,7 +409,18 @@ public class FDeckEditor extends TabPageScreen<FDeckEditor> {
}
public static FImage iconFromDeckSection(DeckSection deckSection) {
return FSkin.getImages().get(FSkinProp.iconFromDeckSection(deckSection, Forge.hdbuttons));
return switch (deckSection) {
case Main -> MAIN_DECK_ICON;
case Sideboard -> SIDEBOARD_ICON;
case Commander -> FSkinImage.COMMAND;
case Avatar -> FSkinImage.AVATAR;
case Conspiracy -> FSkinImage.CONSPIRACY;
case Planes -> FSkinImage.PLANAR;
case Schemes -> FSkinImage.SCHEME;
case Attractions -> FSkinImage.ATTRACTION;
case Contraptions -> FSkinImage.CONTRAPTION;
default -> FSkinImage.HDSIDEBOARD;
};
}
private final DeckEditorConfig editorConfig;

View File

@@ -92,12 +92,7 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
private T get(int index) {
synchronized (lock) {
try {
// TODO: Find cause why index is invalid on some cases...
return internalList.get(index);
} catch (Exception e) {
return null;
}
}
}
@@ -584,8 +579,6 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
maxPileHeight = 0;
for (int j = 0; j < group.piles.size(); j++) {
Pile pile = group.piles.get(j);
if (pile == null)
continue;
y = pileY;
for (int k = 0; k < pile.items.size(); k++) {
ItemInfo itemInfo = pile.items.get(k);
@@ -595,10 +588,7 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
itemInfo.setBounds(x, y, itemWidth, itemHeight);
y += dy;
}
ItemInfo itemInfo = pile.items.get(pile.items.size() - 1);
if (itemInfo == null)
continue;
itemInfo.pos = CardStackPosition.Top;
pile.items.get(pile.items.size() - 1).pos = CardStackPosition.Top;
pileHeight = y + itemHeight - dy - pileY;
if (pileHeight > maxPileHeight) {
maxPileHeight = pileHeight;
@@ -715,13 +705,9 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
float relX = x + group.getScrollLeft() - group.getLeft();
float relY = y + getScrollValue();
Pile pile = group.piles.get(j);
if (pile == null)
continue;
if (pile.contains(relX, relY)) {
for (int k = pile.items.size() - 1; k >= 0; k--) {
ItemInfo item = pile.items.get(k);
if (item == null)
continue;
if (item.contains(relX, relY)) {
return item;
}

View File

@@ -124,7 +124,7 @@ public class VAvatar extends FDisplayObject {
float w = isHovered() ? getWidth()/16f+getWidth() : getWidth();
float h = isHovered() ? getWidth()/16f+getHeight() : getHeight();
if (avatarAnimation != null && MatchController.instance.getGameView() != null && !MatchController.instance.getGameView().isMatchOver()) {
if (avatarAnimation != null && !MatchController.instance.getGameView().isMatchOver()) {
if (player.wasAvatarLifeChanged()) {
avatarAnimation.start();
avatarAnimation.drawAvatar(g, image, 0, 0, w, h);

View File

@@ -70,7 +70,7 @@ public class VManaPool extends VDisplayArea {
float x = 0;
float y = 0;
if (Forge.isLandscapeMode() && (!Forge.altZoneTabs || !"Horizontal".equalsIgnoreCase(Forge.altZoneTabMode))) {
if (Forge.isLandscapeMode() && !Forge.altZoneTabs) {
float labelWidth = visibleWidth / 2;
float labelHeight = visibleHeight / 3;

View File

@@ -9,7 +9,6 @@ import com.badlogic.gdx.utils.Align;
import forge.Forge;
import forge.Graphics;
import forge.assets.FSkin;
import forge.assets.FSkinColor;
import forge.assets.FSkinColor.Colors;
import forge.assets.FSkinFont;
@@ -20,7 +19,6 @@ import forge.game.card.CounterEnumType;
import forge.game.player.PlayerView;
import forge.game.zone.ZoneType;
import forge.localinstance.properties.ForgePreferences.FPref;
import forge.localinstance.skin.FSkinProp;
import forge.menu.FMenuBar;
import forge.menu.FMenuItem;
import forge.menu.FPopupMenu;
@@ -141,8 +139,23 @@ public class VPlayerPanel extends FContainer {
tabs.add(zoneTab);
}
public static FSkinImageInterface iconFromZone(ZoneType zoneType) {
return FSkin.getImages().get(FSkinProp.iconFromZone(zoneType, Forge.hdbuttons));
public static FSkinImage iconFromZone(ZoneType zoneType) {
return switch (zoneType) {
case Hand -> Forge.hdbuttons ? FSkinImage.HDHAND : FSkinImage.HAND;
case Library -> Forge.hdbuttons ? FSkinImage.HDLIBRARY : FSkinImage.LIBRARY;
case Graveyard -> Forge.hdbuttons ? FSkinImage.HDGRAVEYARD : FSkinImage.GRAVEYARD;
case Exile -> Forge.hdbuttons ? FSkinImage.HDEXILE : FSkinImage.EXILE;
case Sideboard -> Forge.hdbuttons ? FSkinImage.HDSIDEBOARD : FSkinImage.SIDEBOARD;
case Flashback -> Forge.hdbuttons ? FSkinImage.HDFLASHBACK : FSkinImage.FLASHBACK;
case Command -> FSkinImage.COMMAND;
case PlanarDeck -> FSkinImage.PLANAR;
case SchemeDeck -> FSkinImage.SCHEME;
case AttractionDeck -> FSkinImage.ATTRACTION;
case ContraptionDeck -> FSkinImage.CONTRAPTION;
case Ante -> FSkinImage.ANTE;
case Junkyard -> FSkinImage.JUNKYARD;
default -> FSkinImage.HDLIBRARY;
};
}
public Iterable<InfoTab> getTabs() {
@@ -295,7 +308,6 @@ public class VPlayerPanel extends FContainer {
tabManaPool.update();
}
@SuppressWarnings("incomplete-switch")
public void updateZone(ZoneType zoneType) {
if (zoneType == ZoneType.Battlefield) {
field.update(true);

View File

@@ -2,8 +2,8 @@ Name:Aang's Journey
ManaCost:2
Types:Sorcery Lesson
K:Kicker:2
A:SP$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Land.Basic | ChangeTypeDesc$ basic land | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ0 | SubAbility$ DBChangeZone | SpellDescription$ Search your library for a basic land card. If this spell was kicked, instead search your library for a basic land card and a Shrine card. Reveal those cards, put them into your hand, then shuffle. You gain 2 life.
SVar:DBChangeZone:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ EACH Land.Basic & Shrine | ChangeTypeDesc$ basic land card and a Shrine card | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ1 | SubAbility$ DBGainLife
A:SP$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Land.Basic | ChangeNum$ 1 | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ0 | SubAbility$ DBChangeZone | SpellDescription$ Search your library for a basic land card. If this spell was kicked, instead search your library for a basic land card and a Shrine card. Reveal those cards, put them into your hand, then shuffle. You gain 2 life.
SVar:DBChangeZone:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ EACH Land.Basic & Shrine | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ1 | SubAbility$ DBGainLife
SVar:DBGainLife:DB$ GainLife | LifeAmount$ 2
SVar:X:Count$TimesKicked
Oracle:Kicker {2} (You may pay an additional {2} as you cast this spell.)\nSearch your library for a basic land card. If this spell was kicked, instead search your library for a basic land card and a Shrine card. Reveal those cards, put them into your hand, then shuffle.\nYou gain 2 life.

View File

@@ -3,6 +3,6 @@ ManaCost:1 W
Types:Instant
A:SP$ Effect | ValidTgts$ Player | StaticAbilities$ STCantBeCast,STCantBeActivated | RememberObjects$ Targeted | AILogic$ BeginningOfOppTurn | SubAbility$ DBDraw | SpellDescription$ Until end of turn, target player can't cast instant or sorcery spells, and that player can't activate abilities that aren't mana abilities.
SVar:STCantBeCast:Mode$ CantBeCast | ValidCard$ Instant,Sorcery | Caster$ Player.IsRemembered | Description$ Target player can't cast instant or sorcery spells, and that player can't activate abilities that aren't mana abilities.
SVar:STCantBeActivated:Mode$ CantBeActivated | ValidCard$ Card | ValidSA$ Activated.!ManaAbility | Activator$ Player.IsRemembered
SVar:STCantBeActivated:Mode$ CantBeActivated | ValidCard$ Card | ValidSA$ Activated.nonManaAbility | Activator$ Player.IsRemembered
SVar:DBDraw:DB$ Draw | SpellDescription$ Draw a card.
Oracle:Until end of turn, target player can't cast instant or sorcery spells, and that player can't activate abilities that aren't mana abilities.\nDraw a card.

View File

@@ -2,7 +2,7 @@ Name:Abzan Monument
ManaCost:2
Types:Artifact
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | TriggerDescription$ When this artifact enters, search your library for a basic Plains, Swamp, or Forest card, reveal it, put it into your hand, then shuffle.
SVar:TrigChange:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Plains.Basic,Swamp.Basic,Forest.Basic | ChangeTypeDesc$ basic Plains, Swamp, or Forest
SVar:TrigChange:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Land.Plains+Basic,Land.Swamp+Basic,Land.Forest+Basic
A:AB$ Token | Cost$ 1 W B G T Sac<1/CARDNAME> | TokenAmount$ 1 | TokenPower$ X | TokenToughness$ X | TokenScript$ w_x_x_spirit | TokenOwner$ You | SorcerySpeed$ True | SpellDescription$ Create an X/X white Spirit creature token, where X is the greatest toughness among creatures you control. Activate only as a sorcery.
SVar:X:Count$Valid Creature.YouCtrl$GreatestToughness
DeckHas:Ability$Token

View File

@@ -6,6 +6,6 @@ K:Emerge:5 W
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigToken | TriggerDescription$ When CARDNAME enters, create a 2/2 white Alien creature token. If CARDNAME's emerge cost was paid, instead create X of those tokens, where X is the sacrificed creature's toughness.
SVar:TrigToken:DB$ Token | TokenAmount$ N | TokenScript$ w_2_2_alien | TokenOwner$ You
SVar:N:Count$Emerged.T.1
SVar:T:Emerged$CardToughness
SVar:T:Emerged$SumToughness
DeckHas:Ability$Token
Oracle:Emerge {5}{W} (You may cast this spell by sacrificing a creature and paying the emerge cost reduced by that creature's mana value.)\nWhen Adipose Offspring enters, create a 2/2 white Alien creature token. If Adipose Offspring's emerge cost was paid, instead create X of those tokens, where X is the sacrificed creature's toughness.

View File

@@ -5,6 +5,6 @@ PT:1/1
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigCharm | TriggerDescription$ When CARDNAME enters, ABILITY
SVar:TrigCharm:DB$ Charm | Choices$ DBCounter,DBSearch
SVar:DBCounter:DB$ PutCounter | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on CARDNAME.
SVar:DBSearch:DB$ ChangeZone | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Land.Basic | ChangeTypeDesc$ basic land | SpellDescription$ Search your library for a basic land card, reveal it, then shuffle and put that card on top.
SVar:DBSearch:DB$ ChangeZone | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Land.Basic | SpellDescription$ Search your library for a basic land card, reveal it, then shuffle and put that card on top.
DeckHas:Ability$Counters
Oracle:When Ainok Guide enters, choose one —\n• Put a +1/+1 counter on Ainok Guide.\n• Search your library for a basic land card, reveal it, then shuffle and put that card on top.

View File

@@ -2,7 +2,7 @@ Name:Akron Legionnaire
ManaCost:6 W W
Types:Creature Giant Soldier
PT:8/4
S:Mode$ CantAttack | ValidCard$ Creature.YouCtrl+nonArtifact+!namedAkron Legionnaire | Description$ Except for creatures named Akron Legionnaire and artifact creatures, creatures you control can't attack.
S:Mode$ CantAttack | ValidCard$ Creature.YouCtrl+nonArtifact+notnamedAkron Legionnaire | Description$ Except for creatures named Akron Legionnaire and artifact creatures, creatures you control can't attack.
DeckHints:Type$Artifact
DeckNeeds:Name$Akron Legionnaire
Oracle:Except for creatures named Akron Legionnaire and artifact creatures, creatures you control can't attack.

View File

@@ -1,12 +1,14 @@
Name:Aladdin's Lamp
ManaCost:10
Types:Artifact
A:AB$ Effect | Cost$ XMin1 X T | Name$ Aladdin's Wish | ReplacementEffects$ DrawReplace | SetChosenNumber$ X | SpellDescription$ The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0.
A:AB$ StoreSVar | Cost$ XMin1 X T | SVar$ DigNum | Type$ Count | Expression$ xPaid | SubAbility$ TheMagic | SpellDescription$ The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0.
SVar:TheMagic:DB$ Effect | Name$ Aladdin's Wish | ReplacementEffects$ DrawReplace
SVar:DrawReplace:Event$ Draw | ValidPlayer$ You | ReplaceWith$ AladdinDraw | Description$ The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card.
SVar:AladdinDraw:DB$ Dig | DigNum$ Count$ChosenNumber | ChangeNum$ 1 | RestRandomOrder$ True | DestinationZone$ Library | LibraryPosition$ 0 | SubAbility$ DBDraw
SVar:AladdinDraw:DB$ Dig | DigNum$ DigNum | ChangeNum$ 1 | RestRandomOrder$ True | DestinationZone$ Library | LibraryPosition$ 0 | SubAbility$ DBDraw
SVar:DBDraw:DB$ Draw | SubAbility$ ExileEffect
SVar:ExileEffect:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile
SVar:X:Count$xPaid
SVar:DigNum:Number$0
AI:RemoveDeck:Random
AI:RemoveDeck:All
Oracle:{X}, {T}: The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0.

View File

@@ -1,6 +1,6 @@
Name:Alarum
ManaCost:1 W
Types:Instant
A:SP$ Pump | ValidTgts$ Creature.!attacking | TgtPrompt$ Select target nonattacking creature | NumAtt$ +1 | NumDef$ +3 | SubAbility$ DBUntap | SpellDescription$ Untap target nonattacking creature. It gets +1/+3 until end of turn.
A:SP$ Pump | ValidTgts$ Creature.notattacking | TgtPrompt$ Select target nonattacking creature | NumAtt$ +1 | NumDef$ +3 | SubAbility$ DBUntap | SpellDescription$ Untap target nonattacking creature. It gets +1/+3 until end of turn.
SVar:DBUntap:DB$ Untap | Defined$ Targeted
Oracle:Untap target nonattacking creature. It gets +1/+3 until end of turn.

View File

@@ -3,5 +3,5 @@ ManaCost:3
Types:Artifact
A:AB$ Mana | Cost$ T | Produced$ Any | SpellDescription$ Add one mana of any color.
A:AB$ Draw | Cost$ 7 T Sac<1/CARDNAME> | NumCards$ X | SpellDescription$ Draw X cards, where X is the number of differently named lands you control.
SVar:X:Count$Valid Land.YouCtrl$DifferentCardNames
SVar:X:Count$DifferentCardNames_Land.YouCtrl+inZoneBattlefield
Oracle:{T}: Add one mana of any color.\n{7}, {T}, Sacrifice this artifact: Draw X cards, where X is the number of differently named lands you control.

View File

@@ -2,7 +2,7 @@ Name:Analyze the Pollen
ManaCost:G
Types:Sorcery
S:Mode$ OptionalCost | EffectZone$ All | ValidCard$ Card.Self | ValidSA$ Spell | Cost$ CollectEvidence<8> | Description$ As an additional cost to cast this spell, you may collect evidence 8. (Exile cards with total mana value 8 or greater from your graveyard.)
A:SP$ ChangeZone | Origin$ Library | Destination$ Hand | ConditionDefined$ Collected | ConditionPresent$ Card | ConditionCompare$ EQ0 | ChangeType$ Land.Basic | ChangeTypeDesc$ basic land | SubAbility$ DBChangeZone | SpellDescription$ Search your library for a basic land card. If evidence was collected, instead search your library for a creature or land card. Reveal that card, put it into your hand, then shuffle.
A:SP$ ChangeZone | Origin$ Library | Destination$ Hand | ConditionDefined$ Collected | ConditionPresent$ Card | ConditionCompare$ EQ0 | ChangeType$ Land.Basic | ChangeNum$ 1 | SubAbility$ DBChangeZone | SpellDescription$ Search your library for a basic land card. If evidence was collected, instead search your library for a creature or land card. Reveal that card, put it into your hand, then shuffle.
SVar:DBChangeZone:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Land,Creature | ChangeNum$ 1 | ConditionDefined$ Collected | ConditionPresent$ Card
DeckHints:Ability$Graveyard|Discard|Dredge|Mill
Oracle:As an additional cost to cast this spell, you may collect evidence 8. (Exile cards with total mana value 8 or greater from your graveyard.)\nSearch your library for a basic land card. If evidence was collected, instead search your library for a creature or land card. Reveal that card, put it into your hand, then shuffle.

View File

@@ -1,7 +1,7 @@
Name:Anchor to Reality
ManaCost:2 U U
Types:Sorcery
A:SP$ ChangeZone | Cost$ 2 U U Sac<1/Artifact;Creature/artifact or creature> | Origin$ Library | Destination$ Battlefield | ChangeType$ Equipment,Vehicle | RememberChanged$ True | SubAbility$ DBScry | SpellDescription$ Search your library for an Equipment or Vehicle card, put that card onto the battlefield, then shuffle.
A:SP$ ChangeZone | Cost$ 2 U U Sac<1/Artifact;Creature/artifact or creature> | Origin$ Library | Destination$ Battlefield | ChangeType$ Equipment,Vehicle | ChangeTypeDesc$ Equipment or Vehicle card | ChangeNum$ 1 | RememberChanged$ True | SubAbility$ DBScry | SpellDescription$ Search your library for an Equipment or Vehicle card, put that card onto the battlefield, then shuffle.
SVar:DBScry:DB$ Scry | ConditionDefined$ Remembered | ConditionPresent$ Card | ScryNum$ X | SubAbility$ DBCleanup | SpellDescription$ If it has mana value less than the sacrificed permanent's mana value, scry 2.
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Count$Compare Y LTZ.2.0

View File

@@ -3,7 +3,7 @@ ManaCost:1 B
Types:Enchantment Aura
K:Enchant:Creature.inZoneGraveyard:creature card in a graveyard
SVar:AttachAILogic:Reanimate
SVar:AttachAITgts:Creature.!namedWorldgorger Dragon
SVar:AttachAITgts:Creature.notnamedWorldgorger Dragon
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | IsPresent$ Card.StrictlySelf | Execute$ TrigReanimate | TriggerDescription$ When CARDNAME enters, if it's on the battlefield, it loses "enchant creature card in a graveyard" and gains "enchant creature put onto the battlefield with CARDNAME." Return enchanted creature card to the battlefield under your control and attach CARDNAME to it. When CARDNAME leaves the battlefield, that creature's controller sacrifices it.
SVar:TrigReanimate:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | Defined$ Enchanted | RememberChanged$ True | GainControl$ True | SubAbility$ DBAnimate
SVar:DBAnimate:DB$ Animate | Defined$ Self | Keywords$ Enchant:Creature.IsRemembered:creature put onto the battlefield with CARDNAME | RemoveKeywords$ Enchant:Creature.inZoneGraveyard:creature card in a graveyard | Duration$ Permanent | SubAbility$ DBAttach

View File

@@ -9,6 +9,6 @@ SVar:DBLook:DB$ RevealHand | Defined$ ChosenPlayer | Look$ True | SubAbility$ DB
SVar:DBNameCard:DB$ NameCard | Defined$ You | SubAbility$ DBClear
SVar:DBClear:DB$ Cleanup | ClearChosenPlayer$ True
S:Mode$ RaiseCost | EffectZone$ Battlefield | ValidCard$ Card.NamedCard | Type$ Spell | Amount$ 2 | Activator$ Opponent | Description$ Spells your opponents cast with the chosen name cost {2} more to cast.
S:Mode$ RaiseCost | EffectZone$ Battlefield | ValidCard$ Card.NamedCard | ValidSpell$ Activated.!ManaAbility | Amount$ 2 | Description$ Activated abilities of sources with the chosen name cost {2} more to activate unless they're mana abilities.
S:Mode$ RaiseCost | EffectZone$ Battlefield | ValidCard$ Card.NamedCard | ValidSpell$ Activated.nonManaAbility | Amount$ 2 | Description$ Activated abilities of sources with the chosen name cost {2} more to activate unless they're mana abilities.
AI:RemoveDeck:Random
Oracle:Vigilance\nAs Anointed Peacekeeper enters, look at an opponent's hand, then choose any card name.\nSpells your opponents cast with the chosen name cost {2} more to cast.\nActivated abilities of sources with the chosen name cost {2} more to activate unless they're mana abilities.

View File

@@ -5,7 +5,7 @@ PT:7/7
K:Flying
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigUpkeep | TriggerDescription$ At the beginning of your upkeep, sacrifice CARDNAME unless you pay {G}{W}{U}.
SVar:TrigUpkeep:DB$ Sacrifice | UnlessPayer$ You | UnlessCost$ G W U
S:Mode$ Continuous | Affected$ Creature.!attacking+untapped+YouCtrl | AddToughness$ 2 | Description$ Each untapped creature you control gets +0/+2 as long as it's not attacking.
S:Mode$ Continuous | Affected$ Creature.notattacking+untapped+YouCtrl | AddToughness$ 2 | Description$ Each untapped creature you control gets +0/+2 as long as it's not attacking.
A:AB$ Pump | Cost$ W | Defined$ Self | NumDef$ +1 | SpellDescription$ CARDNAME gets +0/+1 until end of turn.
DeckHints:Type$Wall & Keyword$Defender
Oracle:Flying\nAt the beginning of your upkeep, sacrifice Arcades Sabboth unless you pay {G}{W}{U}.\nEach untapped creature you control gets +0/+2 as long as it's not attacking.\n{W}: Arcades Sabboth gets +0/+1 until end of turn.

View File

@@ -1,5 +1,5 @@
Name:Armillary Sphere
ManaCost:2
Types:Artifact
A:AB$ ChangeZone | Cost$ 2 T Sac<1/CARDNAME> | Origin$ Library | Destination$ Hand | ChangeType$ Land.Basic | ChangeTypeDesc$ basic land | ChangeNum$ 2 | SpellDescription$ Search your library for up to two basic land cards, reveal them, put them into your hand, then shuffle.
A:AB$ ChangeZone | Cost$ 2 T Sac<1/CARDNAME> | Origin$ Library | Destination$ Hand | ChangeType$ Land.Basic | ChangeNum$ 2 | SpellDescription$ Search your library for up to two basic land cards, reveal them, put them into your hand, then shuffle.
Oracle:{2}, {T}, Sacrifice Armillary Sphere: Search your library for up to two basic land cards, reveal them, put them into your hand, then shuffle.

View File

@@ -3,7 +3,7 @@ ManaCost:2 U B R
Types:Legendary Creature Human Artificer
PT:1/4
K:Deathtouch
T:Mode$ AbilityCast | ValidCard$ Artifact.inZoneBattlefield,Creature.inZoneBattlefield | ValidSA$ SpellAbility.!ManaAbility | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | Condition$ Sacrificed | Execute$ TrigCopy | OptionalDecider$ You | TriggerDescription$ Whenever you activate an ability of an artifact or creature that isn't a mana ability, if one or more permanents were sacrificed to activate it, you may copy that ability. You may choose new targets for the copy. (Sacrificing an artifact for mana to activate an ability doesn't count.)
T:Mode$ AbilityCast | ValidCard$ Artifact.inZoneBattlefield,Creature.inZoneBattlefield | ValidSA$ SpellAbility.nonManaAbility | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | Condition$ Sacrificed | Execute$ TrigCopy | OptionalDecider$ You | TriggerDescription$ Whenever you activate an ability of an artifact or creature that isn't a mana ability, if one or more permanents were sacrificed to activate it, you may copy that ability. You may choose new targets for the copy. (Sacrificing an artifact for mana to activate an ability doesn't count.)
SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | MayChooseTarget$ True
DeckNeeds:Ability$Sacrifice
DeckHints:Type$Artifact

View File

@@ -2,5 +2,5 @@ Name:Assassin's Trophy
ManaCost:B G
Types:Instant
A:SP$ Destroy | ValidTgts$ Permanent.OppCtrl | AITgts$ Permanent.nonLand,Land.nonBasic | TgtPrompt$ Select target permanent an opponent controls | SubAbility$ DBChange | SpellDescription$ Destroy target permanent an opponent controls. Its controller may search their library for a basic land card, put it onto the battlefield, then shuffle.
SVar:DBChange:DB$ ChangeZone | Optional$ True | Origin$ Library | Destination$ Battlefield | ChangeType$ Land.Basic | ChangeTypeDesc$ basic land | DefinedPlayer$ TargetedController | ShuffleNonMandatory$ True
SVar:DBChange:DB$ ChangeZone | Optional$ True | Origin$ Library | Destination$ Battlefield | ChangeType$ Land.Basic | DefinedPlayer$ TargetedController | ShuffleNonMandatory$ True
Oracle:Destroy target permanent an opponent controls. Its controller may search their library for a basic land card, put it onto the battlefield, then shuffle.

View File

@@ -5,5 +5,5 @@ PT:2/2
K:Trample
K:Haste
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | TriggerZones$ Battlefield | Execute$ TrigSearch | CombatDamage$ True | OptionalDecider$ You | TriggerDescription$ Skilled Outrider — Whenever CARDNAME deals combat damage to a player, you may search your library for a basic land card, put it onto the battlefield tapped, then shuffle.
SVar:TrigSearch:DB$ ChangeZone | Origin$ Library | Destination$ Battlefield | Tapped$ True | ChangeType$ Land.Basic | ChangeTypeDesc$ basic land
SVar:TrigSearch:DB$ ChangeZone | Origin$ Library | Destination$ Battlefield | Tapped$ True | ChangeType$ Land.Basic | ChangeNum$ 1
Oracle:Trample, haste\nSkilled Outrider — Whenever Atalan Jackal deals combat damage to a player, you may search your library for a basic land card, put it onto the battlefield tapped, then shuffle.

View File

@@ -3,5 +3,5 @@ ManaCost:1 G
Types:Creature Human Warrior
PT:2/2
A:AB$ Pump | Cost$ 4 G | Defined$ Self | NumAtt$ +4 | NumDef$ +4 | CheckSVar$ FormidableTest | SVarCompare$ GE8 | PrecostDesc$ Formidable — | SpellDescription$ CARDNAME gets +4/+4 until end of turn. Activate only if creatures you control have total power 8 or greater.
SVar:FormidableTest:Count$Valid Creature.YouCtrl$CardPower
SVar:FormidableTest:Count$SumPower_Creature.YouCtrl
Oracle:Formidable — {4}{G}: Atarka Beastbreaker gets +4/+4 until end of turn. Activate only if creatures you control have total power 8 or greater.

View File

@@ -3,5 +3,5 @@ ManaCost:4 R
Types:Creature Ogre Warrior
PT:4/5
A:AB$ PumpAll | Cost$ 3 R R | ValidCards$ Creature.YouCtrl | KW$ Menace | CheckSVar$ FormidableTest | SVarCompare$ GE8 | PrecostDesc$ Formidable — | SpellDescription$ Creatures you control gain menace until end of turn. Activate only if creatures you control have total power 8 or greater.
SVar:FormidableTest:Count$Valid Creature.YouCtrl$CardPower
SVar:FormidableTest:Count$SumPower_Creature.YouCtrl
Oracle:Formidable — {3}{R}{R}: Creatures you control gain menace until end of turn. Activate only if creatures you control have total power 8 or greater. (They can't be blocked except by two or more creatures.)

View File

@@ -3,7 +3,7 @@ ManaCost:1 R
Types:Creature Atog
PT:1/2
A:AB$ Pump | Cost$ Sac<1/Artifact> | Defined$ Self | NumAtt$ +2 | NumDef$ +2 | SpellDescription$ CARDNAME gets +2/+2 until end of turn.
SVar:AIPreference:SacCost$Artifact.token,Artifact.cmcEQ0+nonLegendary+!namedBlack Lotus,Artifact.cmcEQ1,Artifact.cmcEQ2,Artifact.cmcEQ3
SVar:AIPreference:SacCost$Artifact.token,Artifact.cmcEQ0+nonLegendary+notnamedBlack Lotus,Artifact.cmcEQ1,Artifact.cmcEQ2,Artifact.cmcEQ3
DeckHas:Ability$Sacrifice
DeckNeeds:Type$Artifact
Oracle:Sacrifice an artifact: Atog gets +2/+2 until end of turn.

View File

@@ -1,6 +1,6 @@
Name:Attune with Aether
ManaCost:G
Types:Sorcery
A:SP$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Land.Basic | ChangeTypeDesc$ basic land | SubAbility$ DBEnergy | SpellDescription$ Search your library for a basic land card, reveal it, put it into your hand, then shuffle. You get {E}{E} (two energy counters).
A:SP$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Land.Basic | SubAbility$ DBEnergy | SpellDescription$ Search your library for a basic land card, reveal it, put it into your hand, then shuffle. You get {E}{E} (two energy counters).
SVar:DBEnergy:DB$ PutCounter | Defined$ You | CounterType$ ENERGY | CounterNum$ 2
Oracle:Search your library for a basic land card, reveal it, put it into your hand, then shuffle. You get {E}{E} (two energy counters).

View File

@@ -3,7 +3,7 @@ ManaCost:2 G
Types:Sorcery
A:SP$ Token | TokenScript$ g_0_1_plant | SubAbility$ DBDraw | SpellDescription$ Create a 0/1 green Plant creature token, then draw cards equal to the number of differently named creature tokens you control.
SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X | StackDescription$ None
SVar:X:Count$Valid Creature.YouCtrl+token$DifferentCardNames
SVar:X:Count$DifferentCardNames_Creature.YouCtrl+token+inZoneBattlefield
DeckHas:Ability$Token & Type$Plant
DeckHints:Ability$Token
Oracle:Create a 0/1 green Plant creature token, then draw cards equal to the number of differently named creature tokens you control.

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