Compare commits

..

1 Commits

Author SHA1 Message Date
kevlahnota
25e757cae9 reduce adventure music bitrate 2025-08-11 19:47:54 +08:00
1426 changed files with 29670 additions and 40134 deletions

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: ['17', '21'] java: [ '17' ]
name: Test with Java ${{ matrix.Java }} name: Test with Java ${{ matrix.Java }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

7
.gitignore vendored
View File

@@ -66,9 +66,6 @@ forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat forge-gui/res/cardsfolder/*.bat
# Generated changelog file
forge-gui/release-files/CHANGES.txt
forge-gui/res/PerSetTrackingResults forge-gui/res/PerSetTrackingResults
forge-gui/res/decks forge-gui/res/decks
forge-gui/res/layouts forge-gui/res/layouts
@@ -90,7 +87,3 @@ forge-gui/tools/PerSetTrackingResults
*.tiled-session *.tiled-session
/forge-gui/res/adventure/*.tiled-project /forge-gui/res/adventure/*.tiled-project
/forge-gui/res/adventure/*.tiled-session /forge-gui/res/adventure/*.tiled-session
# Ignore python temporaries
__pycache__
*.pyc

View File

@@ -0,0 +1,33 @@
Summary
(Summarize the bug encountered concisely)
Steps to reproduce
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
What is the current bug behavior?
(What actually happens)
What is the expected correct behavior?
(What you should see instead)
Relevant logs and/or screenshots
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
Possible fixes
(If you can, link to the line of code that might be responsible for the problem)
/label ~needs-investigation

View File

@@ -0,0 +1,15 @@
Summary
(Summarize the feature you wish concisely)
Example screenshots
(If this is a UI change, please provide an example screenshot of how this feature might work)
Feature type
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
/label ~feature request

View File

@@ -15,7 +15,7 @@ public class Main {
public static void main(String[] args) { public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/")); GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/"); GuiBase.setDeviceInfo("", "", 0, 0);
new EditorMainWindow(Config.instance()); new EditorMainWindow(Config.instance());
} }
} }

View File

@@ -563,7 +563,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove); int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) { if (thisRemove > 0) {
removed += thisRemove; removed += thisRemove;
table.put(null, prefCard, cType, thisRemove); table.put(null, prefCard, CounterType.get(cType), thisRemove);
} }
} }
} }
@@ -573,7 +573,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostRemoveAnyCounter cost) { public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability); final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source); final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
if (c <= 0) { if (c <= 0) {
return null; return null;
@@ -716,7 +716,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove); int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) { if (over > 0) {
toRemove += over; toRemove += over;
table.put(null, crd, CounterEnumType.QUEST, over); table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
} }
} }
} }

View File

@@ -767,7 +767,7 @@ public class ComputerUtil {
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) { public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa); CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN)); typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
if (untap) { if (untap) {
typeList.remove(activate); typeList.remove(activate);
@@ -2542,7 +2542,7 @@ public class ComputerUtil {
boolean opponent = controller.isOpponentOf(ai); boolean opponent = controller.isOpponentOf(ai);
final CounterType p1p1Type = CounterEnumType.P1P1; final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
if (!sa.hasParam("AILogic")) { if (!sa.hasParam("AILogic")) {
return Aggregates.random(options); return Aggregates.random(options);

View File

@@ -974,13 +974,17 @@ public class ComputerUtilCombat {
continue; continue;
} }
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) { if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) { if (!ability.hasParam("NumAtt")) {
continue; continue;
} }
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability); if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
} else if (ability.getApi() == ApiType.PutCounter) { } else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue; continue;
@@ -994,13 +998,14 @@ public class ComputerUtilCombat {
continue; continue;
} }
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
} int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (pBonus > 0) {
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
power += pBonus; power += pBonus;
} }
} }
}
}
return power; return power;
} }
@@ -1102,13 +1107,17 @@ public class ComputerUtilCombat {
continue; continue;
} }
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) { if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) { if (!ability.hasParam("NumDef")) {
continue; continue;
} }
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability); if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
} else if (ability.getApi() == ApiType.PutCounter) { } else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue; continue;
@@ -1122,13 +1131,14 @@ public class ComputerUtilCombat {
continue; continue;
} }
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
} int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (tBonus > 0) {
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
toughness += tBonus; toughness += tBonus;
} }
} }
}
}
return toughness; return toughness;
} }
@@ -1295,7 +1305,6 @@ public class ComputerUtilCombat {
continue; continue;
} }
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) { if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) { if (!ability.hasParam("NumAtt")) {
continue; continue;
@@ -1305,8 +1314,11 @@ public class ComputerUtilCombat {
continue; continue;
} }
if (!ability.getPayCosts().hasTapCost()) { if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability); int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
} }
} else if (ability.getApi() == ApiType.PutCounter) { } else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
@@ -1321,15 +1333,14 @@ public class ComputerUtilCombat {
continue; continue;
} }
if (!ability.getPayCosts().hasTapCost()) { if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
} if (pBonus > 0) {
}
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
power += pBonus; power += pBonus;
} }
} }
}
}
return power; return power;
} }
@@ -1519,14 +1530,16 @@ public class ComputerUtilCombat {
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) { if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
continue; continue;
} }
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
continue;
}
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) { if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) { if (!ability.hasParam("NumDef")) {
continue; continue;
} }
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true); toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
} else if (ability.getApi() == ApiType.PutCounter) { } else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue; continue;
@@ -1540,13 +1553,12 @@ public class ComputerUtilCombat {
continue; continue;
} }
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
} if (tBonus > 0) {
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
toughness += tBonus; toughness += tBonus;
} }
} }
}
return toughness; return toughness;
} }

View File

@@ -291,12 +291,6 @@ public class ComputerUtilMana {
continue; continue;
} }
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
if (amount <= 0) {
// wrong gamestate for variable amount
continue;
}
if (sa.getApi() == ApiType.Animate) { if (sa.getApi() == ApiType.Animate) {
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana // For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined")) if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined"))
@@ -449,6 +443,7 @@ public class ComputerUtilMana {
manaProduced = manaProduced.replace(s, color); manaProduced = manaProduced.replace(s, color);
} }
} else if (saMana.hasParam("ReplaceColor")) { } else if (saMana.hasParam("ReplaceColor")) {
// replace color
String color = saMana.getParam("ReplaceColor"); String color = saMana.getParam("ReplaceColor");
if ("Chosen".equals(color)) { if ("Chosen".equals(color)) {
if (card.hasChosenColor()) { if (card.hasChosenColor()) {
@@ -740,8 +735,7 @@ public class ComputerUtilMana {
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) { if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) { if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
// not a good idea to sac a card that you're targeting with the SA you're paying for saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
saExcludeList.add(saPayment);
continue; continue;
} }
} }
@@ -1593,9 +1587,11 @@ public class ComputerUtilMana {
// don't use abilities with dangerous drawbacks // don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility(); AbilitySub sub = m.getSubAbility();
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) { if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue; continue;
} }
}
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list

View File

@@ -1347,11 +1347,6 @@ public class PlayerControllerAi extends PlayerController {
// Ai won't understand that anyway // Ai won't understand that anyway
} }
@Override
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Ai won't understand that anyway
}
@Override @Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) { public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto // TODO check if profile detection set to Auto

View File

@@ -171,7 +171,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) { if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
} }
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT); final boolean isInfect = source.hasKeyword(Keyword.INFECT);
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) { if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
} }

View File

@@ -499,6 +499,19 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Fetching should occur fairly often as it helps cast more spells, and // Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana // have access to more mana
if (sa.hasParam("AILogic")) {
if (sa.getParam("AILogic").equals("Never")) {
/*
* Hack to stop AI from using Aviary Mechanic's "may bounce" trigger.
* Ideally it should look for a good bounce target like "Pacifism"-victims
* but there is no simple way to check that. It is preferable for the AI
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
* (bounce useful permanent).
*/
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
List<ZoneType> origin = new ArrayList<>(); List<ZoneType> origin = new ArrayList<>();
if (sa.hasParam("Origin")) { if (sa.hasParam("Origin")) {
origin = ZoneType.listValueOf(sa.getParam("Origin")); origin = ZoneType.listValueOf(sa.getParam("Origin"));
@@ -1594,7 +1607,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (logic.startsWith("ExilePreference")) { } else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList); return doExilePreferenceLogic(decider, sa, fetchList);
} else if (logic.equals("BounceOwnTrigger")) { } else if (logic.equals("BounceOwnTrigger")) {
return doBounceOwnTriggerLogic(decider, sa, fetchList); return doBounceOwnTriggerLogic(decider, fetchList);
} }
} }
if (fetchList.isEmpty()) { if (fetchList.isEmpty()) {
@@ -2158,19 +2171,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN); return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
} }
private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardCollection choices) { private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai)); CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
// TODO check for threatened cards
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false)); CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
if (!prefChoices.isEmpty()) { if (!prefChoices.isEmpty()) {
return ComputerUtilCard.getBestAI(prefChoices); return ComputerUtilCard.getBestAI(prefChoices);
} } else if (!unprefChoices.isEmpty()) {
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
// some extra benefit like First Responder
return ComputerUtilCard.getWorstAI(unprefChoices); return ComputerUtilCard.getWorstAI(unprefChoices);
} } else {
return null; return null;
} }
}
@Override @Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) { public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {

View File

@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
} else if (type.equals("DIVINITY")) { } else if (type.equals("DIVINITY")) {
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0); final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon); choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
} else if (CounterType.getType(type).isKeywordCounter()) { } else if (CounterType.get(type).isKeywordCounter()) {
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type)); choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
} else { } else {
// The AI really should put counters on cards that can use it. // The AI really should put counters on cards that can use it.

View File

@@ -154,7 +154,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
} }
if (counterType == null || counterType.is(type)) { if (counterType == null || counterType.is(type)) {
addTargetsByCounterType(ai, sa, aiList, type); addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
} }
} }
} }
@@ -163,7 +163,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!oppList.isEmpty()) { if (!oppList.isEmpty()) {
// not enough targets // not enough targets
if (sa.canAddMoreTarget()) { if (sa.canAddMoreTarget()) {
final CounterType type = CounterEnumType.M1M1; final CounterType type = CounterType.get(CounterEnumType.M1M1);
if (counterType == null || counterType == type) { if (counterType == null || counterType == type) {
addTargetsByCounterType(ai, sa, oppList, type); addTargetsByCounterType(ai, sa, oppList, type);
} }

View File

@@ -110,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) { public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Proliferate is always optional for all, no need to select best // Proliferate is always optional for all, no need to select best
final CounterType poison = CounterEnumType.POISON; final CounterType poison = CounterType.get(CounterEnumType.POISON);
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO); boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
// because countertype can't be chosen anymore, only look for poison counters // because countertype can't be chosen anymore, only look for poison counters

View File

@@ -170,7 +170,7 @@ public class CountersPutAi extends CountersAi {
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1)); CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING); oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1)); oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
Card best = ComputerUtilCard.getBestAI(oppCreatM1); Card best = ComputerUtilCard.getBestAI(oppCreatM1);
if (best != null) { if (best != null) {
@@ -336,7 +336,7 @@ public class CountersPutAi extends CountersAi {
Game game = ai.getGame(); Game game = ai.getGame();
Combat combat = game.getCombat(); Combat combat = game.getCombat();
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) { if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { } else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return doCombatAdaptLogic(source, amount, combat); return doCombatAdaptLogic(source, amount, combat);
@@ -442,12 +442,14 @@ public class CountersPutAi extends CountersAi {
} }
sa.addDividedAllocation(c, amount); sa.addDividedAllocation(c, amount);
return decision; return decision;
} else if (!hasSacCost) { } else {
if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision; return decision;
} }
} }
} }
}
sa.resetTargets(); sa.resetTargets();
@@ -606,21 +608,7 @@ public class CountersPutAi extends CountersAi {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
final int currCounters = cards.get(0).getCounters(CounterType.getType(type)); final int currCounters = cards.get(0).getCounters(CounterType.get(type));
// adding counters would cause counter amount to overflow
if (Integer.MAX_VALUE - currCounters <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (type.equals("P1P1")) {
if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// each non +1/+1 counter on the card is a 10% chance of not // each non +1/+1 counter on the card is a 10% chance of not
// activating this ability. // activating this ability.
@@ -635,7 +623,7 @@ public class CountersPutAi extends CountersAi {
} }
// Useless since the card already has the keyword (or for another reason) // Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) { if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
@@ -682,12 +670,14 @@ public class CountersPutAi extends CountersAi {
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger()); || (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
CardCollection list; CardCollection list = null;
if (sa.isCurse()) { if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else { } else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
} }
list = CardLists.getTargetableCards(list, sa); list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty() && isMandatoryTrigger) { if (list.isEmpty() && isMandatoryTrigger) {
@@ -703,9 +693,10 @@ public class CountersPutAi extends CountersAi {
|| sa.getTargets().isEmpty()) { || sa.getTargets().isEmpty()) {
sa.resetTargets(); sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} } else {
break; break;
} }
}
if (sa.isCurse()) { if (sa.isCurse()) {
choice = chooseCursedTarget(list, type, amount, ai); choice = chooseCursedTarget(list, type, amount, ai);
@@ -747,6 +738,8 @@ public class CountersPutAi extends CountersAi {
final SpellAbility root = sa.getRootAbility(); final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String aiLogic = sa.getParamOrDefault("AILogic", ""); final String aiLogic = sa.getParamOrDefault("AILogic", "");
boolean preferred = true;
CardCollection list;
final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose(); final boolean divided = sa.isDividedAsYouChoose();
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa); final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
@@ -768,12 +761,12 @@ public class CountersPutAi extends CountersAi {
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa); AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
if (decision.willingToPlay()) { if (decision.willingToPlay()) {
return decision; return decision;
} } else if (mandatory) {
if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} } else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
}
if (!sa.usesTargeting()) { if (!sa.usesTargeting()) {
// No target. So must be defined. (Unused at the moment) // No target. So must be defined. (Unused at the moment)
@@ -823,19 +816,19 @@ public class CountersPutAi extends CountersAi {
} }
} }
sa.resetTargets();
Iterable<Card> filteredField;
if (sa.isCurse()) { if (sa.isCurse()) {
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield); list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else { } else {
filteredField = ai.getCardsIn(ZoneType.Battlefield); list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
} }
CardCollection list = CardLists.getTargetableCards(filteredField, sa); list = CardLists.getTargetableCards(list, sa);
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
boolean preferred = true;
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
sa.resetTargets();
while (sa.canAddMoreTarget()) { while (sa.canAddMoreTarget()) {
if (mandatory) { if (mandatory) {
// When things are mandatory, gotta handle a little differently // When things are mandatory, gotta handle a little differently
@@ -872,22 +865,28 @@ public class CountersPutAi extends CountersAi {
if (choice == null && mandatory) { if (choice == null && mandatory) {
choice = Aggregates.random(list); choice = Aggregates.random(list);
} }
} else if (type.equals("M1M1")) { } else {
if (type.equals("M1M1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list); choice = ComputerUtilCard.getWorstCreatureAI(list);
} else { } else {
choice = Aggregates.random(list); choice = Aggregates.random(list);
} }
} else if (preferred) { }
} else {
if (preferred) {
list = ComputerUtil.getSafeTargets(ai, sa, list); list = ComputerUtil.getSafeTargets(ai, sa, list);
choice = chooseBoonTarget(list, type); choice = chooseBoonTarget(list, type);
if (choice == null && mandatory) { if (choice == null && mandatory) {
choice = Aggregates.random(list); choice = Aggregates.random(list);
} }
} else if (type.equals("P1P1")) { } else {
if (type.equals("P1P1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list); choice = ComputerUtilCard.getWorstCreatureAI(list);
} else { } else {
choice = Aggregates.random(list); choice = Aggregates.random(list);
} }
}
}
if (choice != null && divided) { if (choice != null && divided) {
int alloc = Math.max(amount / totalTargets, 1); int alloc = Math.max(amount / totalTargets, 1);
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) { if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
@@ -962,8 +961,8 @@ public class CountersPutAi extends CountersAi {
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) { protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Bolster does use this // Bolster does use this
// TODO need more or less logic there? // TODO need more or less logic there?
final CounterType m1m1 = CounterEnumType.M1M1; final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
final CounterType p1p1 = CounterEnumType.P1P1; final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
// no logic if there is no options or no to choice // no logic if there is no options or no to choice
if (!isOptional && Iterables.size(options) <= 1) { if (!isOptional && Iterables.size(options) <= 1) {
@@ -1081,10 +1080,11 @@ public class CountersPutAi extends CountersAi {
Player ai = sa.getActivatingPlayer(); Player ai = sa.getActivatingPlayer();
GameEntity e = (GameEntity) params.get("Target"); GameEntity e = (GameEntity) params.get("Target");
// for Card try to select not useless counter // for Card try to select not useless counter
if (e instanceof Card c) { if (e instanceof Card) {
Card c = (Card) e;
if (c.getController().isOpponentOf(ai)) { if (c.getController().isOpponentOf(ai)) {
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) { if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.M1M1; return CounterType.get(CounterEnumType.M1M1);
} }
for (CounterType type : options) { for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, c)) { if (ComputerUtil.isNegativeCounter(type, c)) {
@@ -1098,14 +1098,15 @@ public class CountersPutAi extends CountersAi {
} }
} }
} }
} else if (e instanceof Player p) { } else if (e instanceof Player) {
Player p = (Player) e;
if (p.isOpponentOf(ai)) { if (p.isOpponentOf(ai)) {
if (options.contains(CounterEnumType.POISON)) { if (options.contains(CounterType.get(CounterEnumType.POISON))) {
return CounterEnumType.POISON; return CounterType.get(CounterEnumType.POISON);
} }
} else { } else {
if (options.contains(CounterEnumType.EXPERIENCE)) { if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
return CounterEnumType.EXPERIENCE; return CounterType.get(CounterEnumType.EXPERIENCE);
} }
} }
@@ -1232,8 +1233,9 @@ public class CountersPutAi extends CountersAi {
if (numCtrs < optimalCMC) { if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability. // If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} } else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability. // If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
}

View File

@@ -218,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
Card tgt = (Card) params.get("Target"); Card tgt = (Card) params.get("Target");
// planeswalker has high priority for loyalty counters // planeswalker has high priority for loyalty counters
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) { if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
return CounterEnumType.LOYALTY; return CounterType.get(CounterEnumType.LOYALTY);
} }
if (tgt.getController().isOpponentOf(ai)) { if (tgt.getController().isOpponentOf(ai)) {
// creatures with BaseToughness below or equal zero might be // creatures with BaseToughness below or equal zero might be
// killed if their counters are removed // killed if their counters are removed
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) { if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
if (options.contains(CounterEnumType.P1P1)) { if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterEnumType.P1P1; return CounterType.get(CounterEnumType.P1P1);
} else if (options.contains(CounterEnumType.M1M1)) { } else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterEnumType.M1M1; return CounterType.get(CounterEnumType.M1M1);
} }
} }
@@ -241,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
} }
} else { } else {
// this counters are treat first to be removed // this counters are treat first to be removed
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) { if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage"); CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule); boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
if (maritEmpty) { if (maritEmpty) {
return CounterEnumType.ICE; return CounterType.get(CounterEnumType.ICE);
} }
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) { } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterEnumType.P1P1; return CounterType.get(CounterEnumType.P1P1);
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) { } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterEnumType.M1M1; return CounterType.get(CounterEnumType.M1M1);
} }
// fallback logic, select positive counter to add more // fallback logic, select positive counter to add more

View File

@@ -384,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (targetCard.getController().isOpponentOf(ai)) { if (targetCard.getController().isOpponentOf(ai)) {
// if its a Planeswalker try to remove Loyality first // if its a Planeswalker try to remove Loyality first
if (targetCard.isPlaneswalker()) { if (targetCard.isPlaneswalker()) {
return CounterEnumType.LOYALTY; return CounterType.get(CounterEnumType.LOYALTY);
} }
for (CounterType type : options) { for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, targetCard)) { if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
@@ -392,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
} }
} }
} else { } else {
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) { if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterEnumType.M1M1; return CounterType.get(CounterEnumType.M1M1);
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) { } else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.P1P1; return CounterType.get(CounterEnumType.P1P1);
} }
for (CounterType type : options) { for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, targetCard)) { if (ComputerUtil.isNegativeCounter(type, targetCard)) {

View File

@@ -133,9 +133,9 @@ public class DamageAllAi extends SpellAbilityAi {
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) { if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
// When using Pestilence to hurt players, do it at // When using Pestilence to hurt players, do it at
// the end of the opponent's turn only // the end of the opponent's turn only
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")) if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) || ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai))) && (ai.getGame().getNonactivePlayers().contains(ai)))))
// Need further improvement : if able to kill immediately with repeated activations, do not wait // Need further improvement : if able to kill immediately with repeated activations, do not wait
// for phases! Will also need to implement considering repeated activations for killed creatures! // for phases! Will also need to implement considering repeated activations for killed creatures!
// || (ai.sa.getPayCosts(). ??? ) // || (ai.sa.getPayCosts(). ??? )

View File

@@ -26,6 +26,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.*; import forge.game.cost.*;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -369,7 +370,7 @@ public class DrawAi extends SpellAbilityAi {
// try to make opponent lose to poison // try to make opponent lose to poison
// currently only Caress of Phyrexia // currently only Caress of Phyrexia
if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) { if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (oppA.getPoisonCounters() + numCards > 9) { if (oppA.getPoisonCounters() + numCards > 9) {
sa.getTargets().add(oppA); sa.getTargets().add(oppA);
return true; return true;
@@ -413,7 +414,7 @@ public class DrawAi extends SpellAbilityAi {
} }
} }
if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) { if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (numCards + ai.getPoisonCounters() >= 8) { if (numCards + ai.getPoisonCounters() >= 8) {
aiTarget = false; aiTarget = false;
} }
@@ -471,7 +472,7 @@ public class DrawAi extends SpellAbilityAi {
} }
// ally would lose because of poison // ally would lose because of poison
if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) { if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) {
continue; continue;
} }

View File

@@ -1,15 +1,17 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.ai.*;
import forge.game.Game; import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.Game;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.token.TokenInfo; import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayLife;
import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -34,26 +36,6 @@ public class EndureAi extends SpellAbilityAi {
sa.getTargets().add(bestCreature); sa.getTargets().add(bestCreature);
} }
// Card-specific logic
final String num = sa.getParamOrDefault("Num", "1");
if ("X".equals(num) && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
if (!aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
int curLife = aiPlayer.getLife();
int dangerLife = (((PlayerControllerAi) aiPlayer.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
if (curLife <= dangerLife) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
int availableMana = ComputerUtilMana.getAvailableManaEstimate(aiPlayer) - 1;
int maxEndureX = Math.min(availableMana, curLife - dangerLife);
if (maxEndureX > 0) {
sa.setXManaCostPaid(maxEndureX);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }

View File

@@ -159,7 +159,7 @@ public class ManaAi extends SpellAbilityAi {
int numCounters = 0; int numCounters = 0;
int manaSurplus = 0; int manaSurplus = 0;
if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
CounterType ctrType = CounterEnumType.KI; // Petalmane Baku CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) { for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) { if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter; ctrType = ((CostRemoveCounter)part).counter;

View File

@@ -78,7 +78,7 @@ public class PermanentCreatureAi extends PermanentAi {
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN)) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
&& ai.getManaPool().totalMana() <= 0 && ai.getManaPool().totalMana() <= 0
&& (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) && (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
&& !card.hasETBTrigger(true) && !card.hasSVar("AmbushAI") && (!card.hasETBTrigger(true) && !card.hasSVar("AmbushAI"))
&& game.getStack().isEmpty() && game.getStack().isEmpty()
&& !ComputerUtil.castPermanentInMain1(ai, sa)) { && !ComputerUtil.castPermanentInMain1(ai, sa)) {
// AiPlayDecision.AnotherTime; // AiPlayDecision.AnotherTime;

View File

@@ -6,6 +6,7 @@ import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.GameLossReason; import forge.game.player.GameLossReason;
@@ -64,7 +65,7 @@ public class PoisonAi extends SpellAbilityAi {
boolean result; boolean result;
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
result = tgtPlayer(ai, sa, mandatory); result = tgtPlayer(ai, sa, mandatory);
} else if (mandatory || !ai.canReceiveCounters(CounterEnumType.POISON)) { } else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
// mandatory or ai is uneffected // mandatory or ai is uneffected
result = true; result = true;
} else { } else {
@@ -89,7 +90,7 @@ public class PoisonAi extends SpellAbilityAi {
PlayerCollection betterTgts = tgts.filter(input -> { PlayerCollection betterTgts = tgts.filter(input -> {
if (input.cantLoseCheck(GameLossReason.Poisoned)) { if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return false; return false;
} else if (!input.canReceiveCounters(CounterEnumType.POISON)) { } else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
return false; return false;
} }
return true; return true;
@@ -108,7 +109,7 @@ public class PoisonAi extends SpellAbilityAi {
if (tgts.isEmpty()) { if (tgts.isEmpty()) {
if (mandatory) { if (mandatory) {
// AI is uneffected // AI is uneffected
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterEnumType.POISON)) { if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
sa.getTargets().add(ai); sa.getTargets().add(ai);
return true; return true;
} }
@@ -120,7 +121,7 @@ public class PoisonAi extends SpellAbilityAi {
if (input.cantLoseCheck(GameLossReason.Poisoned)) { if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return true; return true;
} }
return !input.canReceiveCounters(CounterEnumType.POISON); return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON));
}); });
if (!betterAllies.isEmpty()) { if (!betterAllies.isEmpty()) {
allies = betterAllies; allies = betterAllies;

View File

@@ -8,6 +8,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController; import forge.game.player.PlayerController;
@@ -39,7 +40,7 @@ public class TimeTravelAi extends SpellAbilityAi {
// so removing them is good; stuff on the battlefield is usually stuff like Vanishing or As Foretold, which favors adding Time // so removing them is good; stuff on the battlefield is usually stuff like Vanishing or As Foretold, which favors adding Time
// counters for better effect, but exceptions should be added here). // counters for better effect, but exceptions should be added here).
Card target = (Card)params.get("Target"); Card target = (Card)params.get("Target");
return !ComputerUtil.isNegativeCounter(CounterEnumType.TIME, target); return !ComputerUtil.isNegativeCounter(CounterType.get(CounterEnumType.TIME), target);
} }
@Override @Override

View File

@@ -31,8 +31,6 @@ public final class ImageKeys {
public static final String MONARCH_IMAGE = "monarch"; public static final String MONARCH_IMAGE = "monarch";
public static final String THE_RING_IMAGE = "the_ring"; public static final String THE_RING_IMAGE = "the_ring";
public static final String RADIATION_IMAGE = "radiation"; public static final String RADIATION_IMAGE = "radiation";
public static final String SPEED_IMAGE = "speed";
public static final String MAX_SPEED_IMAGE = "max_speed";
public static final String BACKFACE_POSTFIX = "$alt"; public static final String BACKFACE_POSTFIX = "$alt";
public static final String SPECFACE_W = "$wspec"; public static final String SPECFACE_W = "$wspec";

View File

@@ -18,9 +18,9 @@ import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* The class holding game invariants, such as cards, editions, game formats. All that data, which is not supposed to be changed by player * The class holding game invariants, such as cards, editions, game formats. All that data, which is not supposed to be changed by player
* *
@@ -29,6 +29,8 @@ import java.util.stream.Collectors;
public class StaticData { public class StaticData {
private final CardStorageReader cardReader; private final CardStorageReader cardReader;
private final CardStorageReader tokenReader; private final CardStorageReader tokenReader;
private final CardStorageReader customCardReader;
private final String blockDataFolder; private final String blockDataFolder;
private final CardDb commonCards; private final CardDb commonCards;
private final CardDb variantCards; private final CardDb variantCards;
@@ -77,6 +79,7 @@ public class StaticData {
this.tokenReader = tokenReader; this.tokenReader = tokenReader;
this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder))); this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder)));
this.blockDataFolder = blockDataFolder; this.blockDataFolder = blockDataFolder;
this.customCardReader = customCardReader;
this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance; this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance;
this.enableSmartCardArtSelection = enableSmartCardArtSelection; this.enableSmartCardArtSelection = enableSmartCardArtSelection;
this.loadNonLegalCards = loadNonLegalCards; this.loadNonLegalCards = loadNonLegalCards;
@@ -781,7 +784,6 @@ public class StaticData {
Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>(); Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>();
boolean nifHeader = false; boolean nifHeader = false;
boolean cniHeader = false; boolean cniHeader = false;
final Pattern funnyCardCollectorNumberPattern = Pattern.compile("^F\\d+");
for (CardEdition e : editions) { for (CardEdition e : editions) {
if (CardEdition.Type.FUNNY.equals(e.getType())) if (CardEdition.Type.FUNNY.equals(e.getType()))
continue; continue;
@@ -789,13 +791,11 @@ public class StaticData {
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>(); Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
List<CompletableFuture<?>> futures = new ArrayList<>(); List<CompletableFuture<?>> futures = new ArrayList<>();
for (CardEdition.EditionEntry c : e.getObtainableCards()) { for (CardEdition.EditionEntry c : e.getObtainableCards()) {
int amount = 1;
if (cardCount.containsKey(c.name())) { if (cardCount.containsKey(c.name())) {
amount = cardCount.get(c.name()).getRight() + 1; cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), cardCount.get(c.name()).getRight() + 1));
} else {
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), 1));
} }
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && funnyCardCollectorNumberPattern.matcher(c.collectorNumber()).matches(), amount));
} }
// loop through the cards in this edition, considering art variations... // loop through the cards in this edition, considering art variations...

View File

@@ -21,7 +21,6 @@ import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import forge.ImageKeys;
import forge.StaticData; import forge.StaticData;
import forge.card.CardEdition.EditionEntry; import forge.card.CardEdition.EditionEntry;
import forge.card.CardEdition.Type; import forge.card.CardEdition.Type;
@@ -45,6 +44,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static char NameSetSeparator = '|'; public final static char NameSetSeparator = '|';
public final static String FlagPrefix = "#"; public final static String FlagPrefix = "#";
public static final String FlagSeparator = "\t"; public static final String FlagSeparator = "\t";
private final String exlcudedCardName = "Concentrate";
private final String exlcudedCardSet = "DS0";
// need this to obtain cardReference by name+set+artindex // need this to obtain cardReference by name+set+artindex
private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList); private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
@@ -240,8 +241,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
setCode = info[index]; setCode = info[index];
index++; index++;
} }
if(info.length > index && isArtIndex(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""))) { if(info.length > index && isArtIndex(info[index])) {
artIndex = Integer.parseInt(info[index].replace(ImageKeys.BACKFACE_POSTFIX, "")); artIndex = Integer.parseInt(info[index]);
index++; index++;
} }
if(info.length > index && isCollectorNumber(info[index])) { if(info.length > index && isCollectorNumber(info[index])) {
@@ -301,7 +302,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// create faces list from rules // create faces list from rules
for (final CardRules rule : rules.values()) { for (final CardRules rule : rules.values()) {
if (filteredCards.contains(rule.getName())) if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
continue; continue;
for (ICardFace face : rule.getAllFaces()) { for (ICardFace face : rule.getAllFaces()) {
addFaceToDbNames(face); addFaceToDbNames(face);
@@ -499,9 +500,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
public void addCard(PaperCard paperCard) { public void addCard(PaperCard paperCard) {
if (filtered.contains(paperCard.getName())) { if (excludeCard(paperCard.getName(), paperCard.getEdition()))
return; return;
}
allCardsByName.put(paperCard.getName(), paperCard); allCardsByName.put(paperCard.getName(), paperCard);
@@ -522,6 +522,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
} }
private boolean excludeCard(String cardName, String cardEdition) {
if (filtered.isEmpty())
return false;
if (filtered.contains(cardName)) {
if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName))
return true;
else return !exlcudedCardName.equalsIgnoreCase(cardName);
}
return false;
}
private void reIndex() { private void reIndex() {
uniqueCardsByName.clear(); uniqueCardsByName.clear();
for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) { for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) {

View File

@@ -52,14 +52,6 @@ import java.util.stream.Collectors;
*/ */
public final class CardEdition implements Comparable<CardEdition> { public final class CardEdition implements Comparable<CardEdition> {
public DraftOptions getDraftOptions() {
return draftOptions;
}
public void setDraftOptions(DraftOptions draftOptions) {
this.draftOptions = draftOptions;
}
// immutable // immutable
public enum Type { public enum Type {
UNKNOWN, UNKNOWN,
@@ -283,22 +275,18 @@ public final class CardEdition implements Comparable<CardEdition> {
// Booster/draft info // Booster/draft info
private List<BoosterSlot> boosterSlots = null; private List<BoosterSlot> boosterSlots = null;
private boolean smallSetOverride = false; private boolean smallSetOverride = false;
private String additionalUnlockSet = "";
private FoilType foilType = FoilType.NOT_SUPPORTED;
// Replace all of these things with booster slots
private boolean foilAlwaysInCommonSlot = false; private boolean foilAlwaysInCommonSlot = false;
private FoilType foilType = FoilType.NOT_SUPPORTED;
private double foilChanceInBooster = 0; private double foilChanceInBooster = 0;
private double chanceReplaceCommonWith = 0; private double chanceReplaceCommonWith = 0;
private String slotReplaceCommonWith = "Common"; private String slotReplaceCommonWith = "Common";
private String additionalSheetForFoils = ""; private String additionalSheetForFoils = "";
private String additionalUnlockSet = "";
private String boosterMustContain = ""; private String boosterMustContain = "";
private String boosterReplaceSlotFromPrintSheet = ""; private String boosterReplaceSlotFromPrintSheet = "";
private String sheetReplaceCardFromSheet = ""; private String sheetReplaceCardFromSheet = "";
private String sheetReplaceCardFromSheet2 = ""; private String sheetReplaceCardFromSheet2 = "";
private String doublePickDuringDraft = "";
// Draft options
private DraftOptions draftOptions = null;
private String[] chaosDraftThemes = new String[0]; private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, EditionEntry> cardMap; private final ListMultimap<String, EditionEntry> cardMap;
@@ -385,6 +373,7 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; } public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; } public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
public String getAdditionalUnlockSet() { return additionalUnlockSet; } public String getAdditionalUnlockSet() { return additionalUnlockSet; }
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
public String getBoosterMustContain() { return boosterMustContain; } public String getBoosterMustContain() { return boosterMustContain; }
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; } public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; } public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; }
@@ -563,16 +552,26 @@ public final class CardEdition implements Comparable<CardEdition> {
public List<PrintSheet> getPrintSheetsBySection() { public List<PrintSheet> getPrintSheetsBySection() {
final CardDb cardDb = StaticData.instance().getCommonCards(); final CardDb cardDb = StaticData.instance().getCommonCards();
Map<String, Integer> cardToIndex = new HashMap<>();
List<PrintSheet> sheets = Lists.newArrayList(); List<PrintSheet> sheets = Lists.newArrayList();
for (Map.Entry<String, java.util.Collection<EditionEntry>> section : cardMap.asMap().entrySet()) { for (String sectionName : cardMap.keySet()) {
if (section.getKey().equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) { if (sectionName.equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
continue; continue;
} }
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), section.getKey())); PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
for (EditionEntry card : section.getValue()) { List<EditionEntry> cards = cardMap.get(sectionName);
sheet.add(cardDb.getCard(card.name, this.getCode(), card.collectorNumber)); for (EditionEntry card : cards) {
int index = 1;
if (cardToIndex.containsKey(card.name)) {
index = cardToIndex.get(card.name) + 1;
}
cardToIndex.put(card.name, index);
PaperCard pCard = cardDb.getCard(card.name, this.getCode(), index);
sheet.add(pCard);
} }
sheets.add(sheet); sheets.add(sheet);
@@ -630,7 +629,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* functional variant name - grouping #9 * functional variant name - grouping #9
*/ */
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$" // "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
"(^(.?[0-9A-Z-]+\\S*[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$" "(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
); );
final Pattern tokenPattern = Pattern.compile( final Pattern tokenPattern = Pattern.compile(
@@ -639,7 +638,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* name - grouping #3 * name - grouping #3
* artist name - grouping #5 * artist name - grouping #5
*/ */
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$" "(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
); );
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create(); ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
@@ -660,11 +659,6 @@ public final class CardEdition implements Comparable<CardEdition> {
continue; continue;
} }
if (sectionName.endsWith("Types")) {
CardType.Helper.parseTypes(sectionName, contents.get(sectionName));
} else {
// Parse cards
// parse sections of the format "<collector number> <rarity> <name>" // parse sections of the format "<collector number> <rarity> <name>"
if (editionSectionsWithCollectorNumbers.contains(sectionName)) { if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
for(String line : contents.get(sectionName)) { for(String line : contents.get(sectionName)) {
@@ -692,7 +686,6 @@ public final class CardEdition implements Comparable<CardEdition> {
customPrintSheetsToParse.put(sectionName, contents.get(sectionName)); customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
} }
} }
}
ListMultimap<String, EditionEntry> tokenMap = ArrayListMultimap.create(); ListMultimap<String, EditionEntry> tokenMap = ArrayListMultimap.create();
ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create(); ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
@@ -819,6 +812,7 @@ public final class CardEdition implements Comparable<CardEdition> {
res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral
res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon) res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon)
res.doublePickDuringDraft = metadata.get("DoublePick", ""); // "FirstPick" or "Always"
res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature
res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card
@@ -826,23 +820,6 @@ public final class CardEdition implements Comparable<CardEdition> {
res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", ""); res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", "");
res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names
// Draft options
String doublePick = metadata.get("DoublePick", "Never");
int maxPodSize = metadata.getInt("MaxPodSize", 8);
int recommendedPodSize = metadata.getInt("RecommendedPodSize", 8);
int maxMatchPlayers = metadata.getInt("MaxMatchPlayers", 2);
String deckType = metadata.get("DeckType", "Normal");
String freeCommander = metadata.get("FreeCommander", "");
res.draftOptions = new DraftOptions(
doublePick,
maxPodSize,
recommendedPodSize,
maxMatchPlayers,
deckType,
freeCommander
);
return res; return res;
} }
@@ -873,7 +850,7 @@ public final class CardEdition implements Comparable<CardEdition> {
@Override @Override
public void add(CardEdition item) { //Even though we want it to be read only, make an exception for custom content. public void add(CardEdition item) { //Even though we want it to be read only, make an exception for custom content.
if(lock) throw new UnsupportedOperationException("This is a read-only storage"); if(lock) throw new UnsupportedOperationException("This is a read-only storage");
else map.put(item.getCode(), item); else map.put(item.getName(), item);
} }
public void append(CardEdition.Collection C){ //Append custom editions public void append(CardEdition.Collection C){ //Append custom editions
if (lock) throw new UnsupportedOperationException("This is a read-only storage"); if (lock) throw new UnsupportedOperationException("This is a read-only storage");

View File

@@ -53,7 +53,6 @@ public final class CardRules implements ICardCharacteristics {
private boolean addsWildCardColor; private boolean addsWildCardColor;
private int setColorID; private int setColorID;
private boolean custom; private boolean custom;
private boolean unsupported;
private String path; private String path;
public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) { public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
@@ -223,8 +222,6 @@ public final class CardRules implements ICardCharacteristics {
public boolean isCustom() { return custom; } public boolean isCustom() { return custom; }
public void setCustom() { custom = true; } public void setCustom() { custom = true; }
public boolean isUnsupported() { return unsupported; }
@Override @Override
public CardType getType() { public CardType getType() {
switch (splitType.getAggregationMethod()) { switch (splitType.getAggregationMethod()) {
@@ -364,21 +361,16 @@ public final class CardRules implements ICardCharacteristics {
} }
public boolean isDoctor() { public boolean isDoctor() {
Set<String> subtypes = new HashSet<>();
for (String type : mainPart.getType().getSubtypes()) { for (String type : mainPart.getType().getSubtypes()) {
subtypes.add(type); if (!type.equals("Time Lord") && !type.equals("Doctor")) {
return false;
} }
}
return subtypes.size() == 2 && return true;
subtypes.contains("Time Lord") &&
subtypes.contains("Doctor");
} }
public boolean canBeOathbreaker() { public boolean canBeOathbreaker() {
CardType type = mainPart.getType(); CardType type = mainPart.getType();
if (mainPart.getOracleText().contains("can be your commander")) {
return true;
}
return type.isPlaneswalker(); return type.isPlaneswalker();
} }
@@ -831,8 +823,6 @@ public final class CardRules implements ICardCharacteristics {
faces[0].assignMissingFields(); faces[0].assignMissingFields();
final CardRules result = new CardRules(faces, CardSplitType.None, cah); final CardRules result = new CardRules(faces, CardSplitType.None, cah);
result.unsupported = true;
return result; return result;
} }

View File

@@ -189,38 +189,6 @@ public final class CardRulesPredicates {
return card -> card.getType().hasSupertype(type); return card -> card.getType().hasSupertype(type);
} }
/**
* @return a Predicate that matches cards that are of the split type.
*/
public static Predicate<CardRules> isSplitType(final CardSplitType type) {
return card -> card.getSplitType().equals(type);
}
/**
* @return a Predicate that matches cards that are vanilla.
*/
public static Predicate<CardRules> isVanilla() {
return card -> {
if (!(card.getType().isCreature() || card.getType().isLand()) ||
card.getSplitType() != CardSplitType.None ||
card.hasFunctionalVariants()) {
return false;
}
ICardFace mainPart = card.getMainPart();
boolean hasAny =
mainPart.getKeywords().iterator().hasNext() ||
mainPart.getAbilities().iterator().hasNext() ||
mainPart.getStaticAbilities().iterator().hasNext() ||
mainPart.getTriggers().iterator().hasNext() ||
(mainPart.getDraftActions() != null && mainPart.getDraftActions().iterator().hasNext()) ||
mainPart.getReplacements().iterator().hasNext();
return !hasAny;
};
}
/** /**
* Checks for color. * Checks for color.
* *

View File

@@ -1066,74 +1066,4 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return type; return type;
} }
public static class Helper {
public static final void parseTypes(String sectionName, List<String> content) {
Set<String> addToSection = null;
switch (sectionName) {
case "BasicTypes":
addToSection = CardType.Constant.BASIC_TYPES;
break;
case "LandTypes":
addToSection = CardType.Constant.LAND_TYPES;
break;
case "CreatureTypes":
addToSection = CardType.Constant.CREATURE_TYPES;
break;
case "SpellTypes":
addToSection = CardType.Constant.SPELL_TYPES;
break;
case "EnchantmentTypes":
addToSection = CardType.Constant.ENCHANTMENT_TYPES;
break;
case "ArtifactTypes":
addToSection = CardType.Constant.ARTIFACT_TYPES;
break;
case "WalkerTypes":
addToSection = CardType.Constant.WALKER_TYPES;
break;
case "DungeonTypes":
addToSection = CardType.Constant.DUNGEON_TYPES;
break;
case "BattleTypes":
addToSection = CardType.Constant.BATTLE_TYPES;
break;
case "PlanarTypes":
addToSection = CardType.Constant.PLANAR_TYPES;
break;
}
if (addToSection == null) {
return;
}
for(String line : content) {
if (line.length() == 0) continue;
if (line.contains(":")) {
String[] k = line.split(":");
if (addToSection.contains(k[0])) {
continue;
}
addToSection.add(k[0]);
CardType.Constant.pluralTypes.put(k[0], k[1]);
if (k[0].contains(" ")) {
CardType.Constant.MultiwordTypes.add(k[0]);
}
} else {
if (addToSection.contains(line)) {
continue;
}
addToSection.add(line);
if (line.contains(" ")) {
CardType.Constant.MultiwordTypes.add(line);
}
}
}
}
}
} }

View File

@@ -1,75 +0,0 @@
package forge.card;
public class DraftOptions {
public enum DoublePick {
NEVER,
FIRST_PICK, // only first pick each pack
WHEN_POD_SIZE_IS_4, // only when pod size is 4, so you can pick two cards each time
ALWAYS // each time you receive a pack, you can pick two cards
};
public enum DeckType {
Normal, // Standard deck, usually 40 cards
Commander // Special deck type for Commander format. Important for selection/construction
}
private DoublePick doublePick = DoublePick.NEVER;
private final int maxPodSize; // Usually 8, but could be smaller for cubes. I guess it could be larger too
private final int recommendedPodSize; // Usually 8, but is 4 for new double pick
private final int maxMatchPlayers; // Usually 2, but 4 for things like Commander or Conspiracy
private final DeckType deckType; // Normal or Commander
private final String freeCommander;
public DraftOptions(String doublePickOption, int maxPodSize, int recommendedPodSize, int maxMatchPlayers, String deckType, String freeCommander) {
this.maxPodSize = maxPodSize;
this.recommendedPodSize = recommendedPodSize;
this.maxMatchPlayers = maxMatchPlayers;
this.deckType = DeckType.valueOf(deckType);
this.freeCommander = freeCommander;
if (doublePickOption != null) {
switch (doublePickOption.toLowerCase()) {
case "firstpick":
doublePick = DoublePick.FIRST_PICK;
break;
case "always":
doublePick = DoublePick.ALWAYS;
break;
case "whenpodsizeis4":
doublePick = DoublePick.WHEN_POD_SIZE_IS_4;
break;
}
}
}
public int getMaxPodSize() {
return maxPodSize;
}
public int getRecommendedPodSize() {
return recommendedPodSize;
}
public DoublePick getDoublePick() {
return doublePick;
}
public DoublePick isDoublePick(int podSize) {
if (doublePick == DoublePick.WHEN_POD_SIZE_IS_4) {
if (podSize != 4) {
return DoublePick.NEVER;
}
// only when pod size is 4, so you can pick two cards each time
return DoublePick.ALWAYS;
}
return doublePick;
}
public int getMaxMatchPlayers() {
return maxMatchPlayers;
}
public DeckType getDeckType() {
return deckType;
}
public String getFreeCommander() {
return freeCommander;
}
}

View File

@@ -13,7 +13,6 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.function.Predicate;
/** /**
* TODO: Write javadoc for this type. * TODO: Write javadoc for this type.
@@ -69,13 +68,6 @@ public class PrintSheet {
cardsWithWeights.remove(card); cardsWithWeights.remove(card);
} }
public boolean contains(PaperCard pc) {
return cardsWithWeights.contains(pc);
}
public PaperCard find(Predicate<PaperCard> filter) {
return cardsWithWeights.find(filter);
}
private PaperCard fetchRoulette(int start, int roulette, Collection<PaperCard> toSkip) { private PaperCard fetchRoulette(int start, int roulette, Collection<PaperCard> toSkip) {
int sum = start; int sum = start;
boolean isSecondRun = start > 0; boolean isSecondRun = start > 0;
@@ -93,6 +85,15 @@ public class PrintSheet {
return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip
} }
public List<PaperCard> all() {
List<PaperCard> result = new ArrayList<>();
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
for (int i = 0; i < kv.getValue(); i++) {
result.add(kv.getKey());
}
}
return result;
}
public boolean containsCardNamed(String name,int atLeast) { public boolean containsCardNamed(String name,int atLeast) {
int count=0; int count=0;
for (Entry<PaperCard, Integer> kv : cardsWithWeights) { for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
@@ -143,7 +144,7 @@ public class PrintSheet {
return cardsWithWeights.isEmpty(); return cardsWithWeights.isEmpty();
} }
public List<PaperCard> toFlatList() { public Iterable<PaperCard> toFlatList() {
return cardsWithWeights.toFlatList(); return cardsWithWeights.toFlatList();
} }

View File

@@ -37,6 +37,7 @@ import java.util.function.Predicate;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class CardPool extends ItemPool<PaperCard> { public class CardPool extends ItemPool<PaperCard> {
private static final long serialVersionUID = -5379091255613968393L; private static final long serialVersionUID = -5379091255613968393L;
@@ -77,20 +78,12 @@ public class CardPool extends ItemPool<PaperCard> {
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases(); Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){ for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
CardDb db = entry.getValue(); CardDb db = entry.getValue();
PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags); PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags);
if (paperCard != null) { if (paperCard != null) {
this.add(paperCard, amount); this.add(paperCard, amount);
return; return;
} }
} }
// Try to get non-Alchemy version if it cannot find it.
if (cardName.startsWith("A-")) {
System.out.println("Alchemy card not found for '" + cardName + "'. Trying to get its non-Alchemy equivalent.");
cardName = cardName.replaceFirst("A-", "");
}
//Failed to find it. Fall back accordingly? //Failed to find it. Fall back accordingly?
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags); this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
} }
@@ -426,12 +419,6 @@ public class CardPool extends ItemPool<PaperCard> {
return pool; return pool;
} }
public static CardPool fromSingleCardRequest(String cardRequest) {
if(StringUtils.isBlank(cardRequest))
return new CardPool();
return fromCardList(List.of(cardRequest));
}
public static List<Pair<String, Integer>> processCardList(final Iterable<String> lines) { public static List<Pair<String, Integer>> processCardList(final Iterable<String> lines) {
List<Pair<String, Integer>> cardRequests = new ArrayList<>(); List<Pair<String, Integer>> cardRequests = new ArrayList<>();
if (lines == null) if (lines == null)
@@ -481,7 +468,6 @@ public class CardPool extends ItemPool<PaperCard> {
* @param predicate the Predicate to apply to this CardPool * @param predicate the Predicate to apply to this CardPool
* @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate * @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate
*/ */
@Override
public CardPool getFilteredPool(Predicate<PaperCard> predicate) { public CardPool getFilteredPool(Predicate<PaperCard> predicate) {
CardPool filteredPool = new CardPool(); CardPool filteredPool = new CardPool();
for (PaperCard c : this.items.keySet()) { for (PaperCard c : this.items.keySet()) {

View File

@@ -28,8 +28,6 @@ import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
@@ -115,20 +113,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return parts.get(DeckSection.Main); return parts.get(DeckSection.Main);
} }
public Pair<Deck, List<PaperCard>> getValid() {
List<PaperCard> unsupported = new ArrayList<>();
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool pool = kv.getValue();
for (Entry<PaperCard, Integer> pc : pool) {
if (pc.getKey().getRules() != null && pc.getKey().getRules().isUnsupported()) {
unsupported.add(pc.getKey());
pool.remove(pc.getKey());
}
}
}
return Pair.of(this, unsupported);
}
public List<PaperCard> getCommanders() { public List<PaperCard> getCommanders() {
List<PaperCard> result = Lists.newArrayList(); List<PaperCard> result = Lists.newArrayList();
final CardPool cp = get(DeckSection.Commander); final CardPool cp = get(DeckSection.Commander);
@@ -224,19 +208,14 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
super.cloneFieldsTo(clone); super.cloneFieldsTo(clone);
final Deck result = (Deck) clone; final Deck result = (Deck) clone;
loadDeferredSections(); loadDeferredSections();
// parts shouldn't be null
if (parts != null) {
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) { for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool cp = new CardPool(); CardPool cp = new CardPool();
result.parts.put(kv.getKey(), cp); result.parts.put(kv.getKey(), cp);
cp.addAll(kv.getValue()); cp.addAll(kv.getValue());
} }
}
result.setAiHints(StringUtils.join(aiHints, " | ")); result.setAiHints(StringUtils.join(aiHints, " | "));
result.setDraftNotes(draftNotes); result.setDraftNotes(draftNotes);
//noinspection ConstantValue tags.addAll(result.getTags());
if(tags != null) //Can happen deserializing old Decks.
result.tags.addAll(this.tags);
} }
/* /*
@@ -542,17 +521,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return sum; return sum;
} }
/**
* Counts the number of copies of this exact card print across all deck sections.
*/
public int count(PaperCard card) {
int sum = 0;
for (Entry<DeckSection, CardPool> section : this) {
sum += section.getValue().count(card);
}
return sum;
}
public void setAiHints(String aiHintsInfo) { public void setAiHints(String aiHintsInfo) {
if (aiHintsInfo == null || aiHintsInfo.trim().isEmpty()) { if (aiHintsInfo == null || aiHintsInfo.trim().isEmpty()) {
return; return;
@@ -646,14 +614,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return this; return this;
} }
@Serial
private Object readResolve() throws ObjectStreamException {
//If we deserialized an old deck that doesn't have tags, fix it here.
if(this.tags == null)
return new Deck(this, this.getName() == null ? "" : this.getName());
return this;
}
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {

View File

@@ -32,8 +32,11 @@ import forge.util.TextUtil;
import org.apache.commons.lang3.Range; import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
/** /**
@@ -57,13 +60,6 @@ public enum DeckFormat {
//Limited contraption decks have no restrictions. //Limited contraption decks have no restrictions.
return null; return null;
} }
@Override
public int getExtraSectionMaxCopies(DeckSection section) {
if(section == DeckSection.Attractions || section == DeckSection.Contraptions)
return Integer.MAX_VALUE;
return super.getExtraSectionMaxCopies(section);
}
}, },
Commander ( Range.is(99), Range.of(0, 10), 1, null, Commander ( Range.is(99), Range.of(0, 10), 1, null,
card -> StaticData.instance().getCommanderPredicate().test(card) card -> StaticData.instance().getCommanderPredicate().test(card)
@@ -112,13 +108,7 @@ public enum DeckFormat {
} }
}, },
PlanarConquest ( Range.of(40, Integer.MAX_VALUE), Range.is(0), 1), PlanarConquest ( Range.of(40, Integer.MAX_VALUE), Range.is(0), 1),
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, Integer.MAX_VALUE), 4) { Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, 15), 4),
@Override
public boolean allowCustomCards() {
//If the player has them, may as well allow them.
return true;
}
},
Vanguard ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4), Vanguard ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
Planechase ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4), Planechase ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
Archenemy ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4), Archenemy ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
@@ -201,57 +191,12 @@ public enum DeckFormat {
} }
/** /**
* @return the default maximum copies of a card in this format. * @return the maxCardCopies
*/ */
public int getMaxCardCopies() { public int getMaxCardCopies() {
return maxCardCopies; return maxCardCopies;
} }
/**
* @return the maximum copies of the specified card allowed in this format. This does not include ban or restricted lists.
*/
public int getMaxCardCopies(PaperCard card) {
if(canHaveSpecificNumberInDeck(card) != null)
return canHaveSpecificNumberInDeck(card);
else if (canHaveAnyNumberOf(card))
return Integer.MAX_VALUE;
else if (card.getRules().isVariant()) {
DeckSection section = DeckSection.matchingSection(card);
if(section == DeckSection.Planes && card.getRules().getType().isPhenomenon())
return 2; //These are two-of.
return getExtraSectionMaxCopies(section);
}
else
return this.getMaxCardCopies();
}
public int getExtraSectionMaxCopies(DeckSection section) {
return switch (section) {
case Avatar, Commander, Planes, Dungeon, Attractions, Contraptions -> 1;
case Schemes -> 2;
case Conspiracy -> Integer.MAX_VALUE;
default -> maxCardCopies;
};
}
/**
* @return the deck sections used by most decks in this format.
*/
public EnumSet<DeckSection> getPrimaryDeckSections() {
if(this == Planechase)
return EnumSet.of(DeckSection.Planes);
if(this == Archenemy)
return EnumSet.of(DeckSection.Schemes);
if(this == Vanguard)
return EnumSet.of(DeckSection.Avatar);
EnumSet<DeckSection> out = EnumSet.of(DeckSection.Main);
if(sideRange == null || sideRange.getMaximum() > 0)
out.add(DeckSection.Sideboard);
if(hasCommander())
out.add(DeckSection.Commander);
return out;
}
public String getDeckConformanceProblem(Deck deck) { public String getDeckConformanceProblem(Deck deck) {
if (deck == null) { if (deck == null) {
return "is not selected"; return "is not selected";
@@ -408,7 +353,7 @@ public enum DeckFormat {
// Should group all cards by name, so that different editions of same card are really counted as the same card // Should group all cards by name, so that different editions of same card are really counted as the same card
for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, pc -> StaticData.instance().getCommonCards().getName(pc.getName(), true))) { for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, pc -> StaticData.instance().getCommonCards().getName(pc.getName(), true))) {
IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey()); IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey());
if (simpleCard != null && simpleCard.getRules().isCustom() && !allowCustomCards()) if (simpleCard != null && simpleCard.getRules().isCustom() && !StaticData.instance().allowCustomCardsInDecksConformance())
return TextUtil.concatWithSpace("contains a Custom Card:", cp.getKey(), "\nPlease Enable Custom Cards in Forge Preferences to use this deck."); return TextUtil.concatWithSpace("contains a Custom Card:", cp.getKey(), "\nPlease Enable Custom Cards in Forge Preferences to use this deck.");
// Might cause issues since it ignores "Special" Cards // Might cause issues since it ignores "Special" Cards
if (simpleCard == null) { if (simpleCard == null) {
@@ -539,10 +484,6 @@ public enum DeckFormat {
// Not needed by default // Not needed by default
} }
public boolean allowCustomCards() {
return StaticData.instance().allowCustomCardsInDecksConformance();
}
public boolean isLegalCard(PaperCard pc) { public boolean isLegalCard(PaperCard pc) {
if (cardPoolFilter == null) { if (cardPoolFilter == null) {
if (paperCardPoolFilter == null) { if (paperCardPoolFilter == null) {
@@ -557,13 +498,13 @@ public enum DeckFormat {
if (cardPoolFilter != null && !cardPoolFilter.test(rules)) { if (cardPoolFilter != null && !cardPoolFilter.test(rules)) {
return false; return false;
} }
if (this == DeckFormat.Oathbreaker) { if (this.equals(DeckFormat.Oathbreaker)) {
return rules.canBeOathbreaker(); return rules.canBeOathbreaker();
} }
if (this == DeckFormat.Brawl) { if (this.equals(DeckFormat.Brawl)) {
return rules.canBeBrawlCommander(); return rules.canBeBrawlCommander();
} }
if (this == DeckFormat.TinyLeaders) { if (this.equals(DeckFormat.TinyLeaders)) {
return rules.canBeTinyLeadersCommander(); return rules.canBeTinyLeadersCommander();
} }
return rules.canBeCommander(); return rules.canBeCommander();
@@ -612,8 +553,6 @@ public enum DeckFormat {
for (final PaperCard p : commanders) { for (final PaperCard p : commanders) {
cmdCI |= p.getRules().getColorIdentity().getColor(); cmdCI |= p.getRules().getColorIdentity().getColor();
} }
if(cmdCI == MagicColor.ALL_COLORS)
return x -> true;
Predicate<CardRules> predicate = CardRulesPredicates.hasColorIdentity(cmdCI); Predicate<CardRules> predicate = CardRulesPredicates.hasColorIdentity(cmdCI);
if (commanders.size() == 1 && commanders.get(0).getRules().canBePartnerCommander()) { if (commanders.size() == 1 && commanders.get(0).getRules().canBePartnerCommander()) {
// Also show available partners a commander can have a partner. // Also show available partners a commander can have a partner.

View File

@@ -46,7 +46,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
// These fields are kinda PK for PrintedCard // These fields are kinda PK for PrintedCard
private final String name; private final String name;
private String edition; private final String edition;
/* [NEW] Attribute to store reference to CollectorNumber of each PaperCard. /* [NEW] Attribute to store reference to CollectorNumber of each PaperCard.
By default the attribute is marked as "unset" so that it could be retrieved and set. By default the attribute is marked as "unset" so that it could be retrieved and set.
(see getCollectorNumber()) (see getCollectorNumber())
@@ -154,31 +154,6 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return this.noSellVersion; return this.noSellVersion;
} }
public PaperCard getMeldBaseCard() {
if (getRules().getSplitType() != CardSplitType.Meld) {
return null;
}
// This is the base part of the meld duo
if (getRules().getOtherPart() == null) {
return this;
}
String meldWith = getRules().getMeldWith();
if (meldWith == null) {
return null;
}
List<PrintSheet> sheets = StaticData.instance().getCardEdition(this.edition).getPrintSheetsBySection();
for (PrintSheet sheet : sheets) {
if (sheet.contains(this)) {
return sheet.find(PaperCardPredicates.name(meldWith));
}
}
return null;
}
public PaperCard copyWithoutFlags() { public PaperCard copyWithoutFlags() {
if(this.flaglessVersion == null) { if(this.flaglessVersion == null) {
if(this.flags == PaperCardFlags.IDENTITY_FLAGS) if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
@@ -250,7 +225,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX); this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
this.foil = foil; this.foil = foil;
this.rarity = rarity; this.rarity = rarity;
this.artist = artist; this.artist = TextUtil.normalizeText(artist);
this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER; this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER;
// If the user changes the language this will make cards sort by the old language until they restart the game. // If the user changes the language this will make cards sort by the old language until they restart the game.
// This is a good tradeoff // This is a good tradeoff
@@ -375,8 +350,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex); System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex);
pc = readObjectAlternate(name, edition); pc = readObjectAlternate(name, edition);
if (pc == null) { if (pc == null) {
pc = StaticData.instance().getCommonCards().createUnsupportedCard(name); throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
//throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
} }
System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex()); System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex());
} }

View File

@@ -50,13 +50,6 @@ public abstract class PaperCardPredicates {
return new PredicateNames(what); return new PredicateNames(what);
} }
/**
* Filters on a card foil status
*/
public static Predicate<PaperCard> isFoil(final boolean isFoil) {
return new PredicateFoil(isFoil);
}
private static final class PredicatePrintedWithRarity implements Predicate<PaperCard> { private static final class PredicatePrintedWithRarity implements Predicate<PaperCard> {
private final CardRarity matchingRarity; private final CardRarity matchingRarity;
@@ -100,17 +93,6 @@ public abstract class PaperCardPredicates {
} }
} }
private static final class PredicateFoil implements Predicate<PaperCard> {
private final boolean operand;
@Override
public boolean test(final PaperCard card) { return card.isFoil() == operand; }
private PredicateFoil(final boolean isFoil) {
this.operand = isFoil;
}
}
private static final class PredicateRarity implements Predicate<PaperCard> { private static final class PredicateRarity implements Predicate<PaperCard> {
private final CardRarity operand; private final CardRarity operand;

View File

@@ -19,11 +19,6 @@ public class SealedTemplate {
Pair.of(BoosterSlots.RARE_MYTHIC, 1), Pair.of(BoosterSlots.BASIC_LAND, 1) Pair.of(BoosterSlots.RARE_MYTHIC, 1), Pair.of(BoosterSlots.BASIC_LAND, 1)
)); ));
// This is a generic cube booster. 15 cards, no rarity slots.
public final static SealedTemplate genericNoSlotBooster = new SealedTemplate(null, Lists.newArrayList(
Pair.of(BoosterSlots.ANY, 15)
));
protected final List<Pair<String, Integer>> slots; protected final List<Pair<String, Integer>> slots;
protected final String name; protected final String name;

View File

@@ -254,7 +254,7 @@ public class BoosterGenerator {
if (sheetKey.startsWith("wholeSheet")) { if (sheetKey.startsWith("wholeSheet")) {
PrintSheet ps = getPrintSheet(sheetKey); PrintSheet ps = getPrintSheet(sheetKey);
result.addAll(ps.toFlatList()); result.addAll(ps.all());
continue; continue;
} }
@@ -384,7 +384,7 @@ public class BoosterGenerator {
PrintSheet replaceThis = tryGetStaticSheet(split[0]); PrintSheet replaceThis = tryGetStaticSheet(split[0]);
List<PaperCard> candidates = Lists.newArrayList(); List<PaperCard> candidates = Lists.newArrayList();
for (PaperCard p : result) { for (PaperCard p : result) {
if (replaceThis.contains(p)) { if (replaceThis.all().contains(p)) {
candidates.add(candidates.size(), p); candidates.add(candidates.size(), p);
} }
} }
@@ -398,7 +398,7 @@ public class BoosterGenerator {
PrintSheet replaceThis = tryGetStaticSheet(split[0]); PrintSheet replaceThis = tryGetStaticSheet(split[0]);
List<PaperCard> candidates = Lists.newArrayList(); List<PaperCard> candidates = Lists.newArrayList();
for (PaperCard p : result) { for (PaperCard p : result) {
if (replaceThis.contains(p)) { if (replaceThis.all().contains(p)) {
candidates.add(candidates.size(), p); candidates.add(candidates.size(), p);
} }
} }

View File

@@ -199,14 +199,19 @@ public class ImageUtil {
return getImageRelativePath(cp, face, true, true); return getImageRelativePath(cp, face, true, true);
} }
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){ public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false);
}
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
String editionCode; String editionCode;
if (setCode != null && !setCode.isEmpty()) if (setCode != null && !setCode.isEmpty())
editionCode = setCode; editionCode = setCode;
else else
editionCode = cp.getEdition().toLowerCase(); editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber(); String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.replace("+", "");
// override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode // override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode
if (cardCollectorNumber.startsWith("OHOP")) { if (cardCollectorNumber.startsWith("OHOP")) {
editionCode = "ohop"; editionCode = "ohop";
@@ -217,42 +222,29 @@ public class ImageUtil {
} else if (cardCollectorNumber.startsWith("OPC2")) { } else if (cardCollectorNumber.startsWith("OPC2")) {
editionCode = "opc2"; editionCode = "opc2";
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length()); cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
} else if (hyphenateAlchemy) {
if (!cardCollectorNumber.startsWith("A")) {
return null;
}
cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
} }
String versionParam = useArtCrop ? "art_crop" : "normal"; String versionParam = useArtCrop ? "art_crop" : "normal";
String faceParam = ""; String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
} else if (cp.getRules().getSplitType() == CardSplitType.Meld
&& !cardCollectorNumber.endsWith("a")
&& !cardCollectorNumber.endsWith("b")) {
if (cp.getRules().getSplitType() == CardSplitType.Meld) { // Only the bottom half of a meld card shares a collector number.
// Hanweir Garrison EMN already has a appended.
// Exception: The front facing card doesn't use a in FIN
if (face.equals("back")) { if (face.equals("back")) {
PaperCard meldBasePc = cp.getMeldBaseCard(); cardCollectorNumber += "b";
cardCollectorNumber = meldBasePc.getCollectorNumber(); } else if (!editionCode.equals("fin")) {
String collectorNumberSuffix = ""; cardCollectorNumber += "a";
if (cardCollectorNumber.endsWith("a")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
} else if (cardCollectorNumber.endsWith("as")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
collectorNumberSuffix = "s";
} else if (cardCollectorNumber.endsWith("ap")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
collectorNumberSuffix = "p";
} else if (cp.getCollectorNumber().endsWith("a")) {
// SIR
cardCollectorNumber = cp.getCollectorNumber().substring(0, cp.getCollectorNumber().length() - 1);
} }
cardCollectorNumber += "b" + collectorNumberSuffix;
}
faceParam = "&face=front";
} else if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") && cp.getRules().getSplitType() != CardSplitType.Flip
? "&face=back"
: "&face=front");
}
if (cardCollectorNumber.endsWith("")) {
faceParam = "&face=back";
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
} }
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber), return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber),
@@ -264,10 +256,6 @@ public class ImageUtil {
if (!faceParam.isEmpty()) { if (!faceParam.isEmpty()) {
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front"); faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
} }
if (collectorNumber.endsWith("")) {
faceParam = "&face=back";
collectorNumber = collectorNumber.substring(0, collectorNumber.length() - 1);
}
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, encodeUtf8(collectorNumber), return String.format("%s/%s/%s?format=image&version=%s%s", setCode, encodeUtf8(collectorNumber),
langCode, versionParam, faceParam); langCode, versionParam, faceParam);
} }
@@ -288,7 +276,8 @@ public class ImageUtil {
char c; char c;
for (int i = 0; i < in.length(); i++) { for (int i = 0; i < in.length(); i++) {
c = in.charAt(i); c = in.charAt(i);
if ((c != '"') && (c != '/') && (c != ':') && (c != '?')) { if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) {
} else {
out.append(c); out.append(c);
} }
} }

View File

@@ -269,16 +269,11 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
// need not set out-of-sync: either remove did set, or nothing was removed // need not set out-of-sync: either remove did set, or nothing was removed
} }
public void removeIf(Predicate<T> filter) { public void removeIf(Predicate<T> test) {
items.keySet().removeIf(filter); for (final T item : items.keySet()) {
if (test.test(item))
remove(item);
} }
public void retainIf(Predicate<T> filter) {
items.keySet().removeIf(filter.negate());
}
public T find(Predicate<T> filter) {
return items.keySet().stream().filter(filter).findFirst().orElse(null);
} }
public void clear() { public void clear() {
@@ -290,19 +285,4 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
return (obj instanceof ItemPool ip) && return (obj instanceof ItemPool ip) &&
(this.items.equals(ip.items)); (this.items.equals(ip.items));
} }
/**
* Applies a predicate to this ItemPool's entries.
*
* @param predicate the Predicate to apply to this ItemPool
* @return a new ItemPool made from this ItemPool with only the items that agree with the provided Predicate
*/
public ItemPool<T> getFilteredPool(Predicate<T> predicate) {
ItemPool<T> filteredPool = new ItemPool<>(myClass);
for (T c : this.items.keySet()) {
if (predicate.test(c))
filteredPool.add(c, this.items.get(c));
}
return filteredPool;
}
} }

View File

@@ -32,7 +32,7 @@
<dependency> <dependency>
<groupId>io.sentry</groupId> <groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId> <artifactId>sentry-logback</artifactId>
<version>8.19.1</version> <version>8.18.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jgrapht</groupId> <groupId>org.jgrapht</groupId>

View File

@@ -237,8 +237,6 @@ public class ForgeScript {
return sa.isBoast(); return sa.isBoast();
} else if (property.equals("Exhaust")) { } else if (property.equals("Exhaust")) {
return sa.isExhaust(); return sa.isExhaust();
} else if (property.equals("Mayhem")) {
return sa.isMayhem();
} else if (property.equals("Mutate")) { } else if (property.equals("Mutate")) {
return sa.isMutate(); return sa.isMutate();
} else if (property.equals("Ninjutsu")) { } else if (property.equals("Ninjutsu")) {

View File

@@ -414,6 +414,19 @@ public class Game {
return players; return players;
} }
/**
* Gets the nonactive players who are still fighting to win, in turn order.
*/
public final PlayerCollection getNonactivePlayers() {
// Don't use getPlayersInTurnOrder to prevent copying the player collection twice
final PlayerCollection players = new PlayerCollection(ingamePlayers);
players.remove(phaseHandler.getPlayerTurn());
if (!getTurnOrder().isDefaultDirection()) {
Collections.reverse(players);
}
return players;
}
/** /**
* Gets the players who participated in match (regardless of outcome). * Gets the players who participated in match (regardless of outcome).
* <i>Use this in UI and after match calculations</i> * <i>Use this in UI and after match calculations</i>

View File

@@ -1822,8 +1822,8 @@ public class GameAction {
private boolean stateBasedAction704_5q(Card c) { private boolean stateBasedAction704_5q(Card c) {
boolean checkAgain = false; boolean checkAgain = false;
final CounterType p1p1 = CounterEnumType.P1P1; final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterEnumType.M1M1; final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
int plusOneCounters = c.getCounters(p1p1); int plusOneCounters = c.getCounters(p1p1);
int minusOneCounters = c.getCounters(m1m1); int minusOneCounters = c.getCounters(m1m1);
if (plusOneCounters > 0 && minusOneCounters > 0) { if (plusOneCounters > 0 && minusOneCounters > 0) {
@@ -1843,7 +1843,7 @@ public class GameAction {
return checkAgain; return checkAgain;
} }
private boolean stateBasedAction704_5r(Card c) { private boolean stateBasedAction704_5r(Card c) {
final CounterType dreamType = CounterEnumType.DREAM; final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
int old = c.getCounters(dreamType); int old = c.getCounters(dreamType);
if (old <= 0) { if (old <= 0) {
@@ -2222,13 +2222,6 @@ public class GameAction {
} }
} }
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Notify players
for (Player p : game.getPlayers()) {
p.getController().revealUnsupported(unsupported);
}
}
/** Delivers a message to all players. (use reveal to show Cards) */ /** Delivers a message to all players. (use reveal to show Cards) */
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) { public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
if (saSource != null) { if (saSource != null) {

View File

@@ -125,22 +125,10 @@ public final class GameActionUtil {
// need to be done there before static abilities does reset the card // need to be done there before static abilities does reset the card
// These Keywords depend on the Mana Cost of for Split Cards // These Keywords depend on the Mana Cost of for Split Cards
if (sa.isBasicSpell()) { if (sa.isBasicSpell() && !sa.isLandAbility()) {
for (final KeywordInterface inst : source.getKeywords()) { for (final KeywordInterface inst : source.getKeywords()) {
final String keyword = inst.getOriginal(); final String keyword = inst.getOriginal();
if (keyword.startsWith("Mayhem")) {
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
}
if (sa.isLandAbility()) {
continue;
}
if (keyword.startsWith("Escape")) { if (keyword.startsWith("Escape")) {
if (!source.isInZone(ZoneType.Graveyard)) { if (!source.isInZone(ZoneType.Graveyard)) {
continue; continue;
@@ -178,6 +166,18 @@ public final class GameActionUtil {
} }
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback)); alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback));
} else if (keyword.startsWith("Mayhem")) {
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
continue;
}
// if source has No Mana cost, and Mayhem doesn't have own one,
// Mayhem can't work
if (keyword.equals("Mayhem") && source.getManaCost().isNoCost()) {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
} else if (keyword.startsWith("Harmonize")) { } else if (keyword.startsWith("Harmonize")) {
if (!source.isInZone(ZoneType.Graveyard)) { if (!source.isInZone(ZoneType.Graveyard)) {
continue; continue;
@@ -242,7 +242,6 @@ public final class GameActionUtil {
} }
stackCopy.setLastKnownZone(game.getStackZone()); stackCopy.setLastKnownZone(game.getStackZone());
stackCopy.setCastFrom(oldZone); stackCopy.setCastFrom(oldZone);
stackCopy.setCastSA(sa);
lkicheck = true; lkicheck = true;
stackCopy.clearStaticChangedCardKeywords(false); stackCopy.clearStaticChangedCardKeywords(false);

View File

@@ -33,6 +33,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment; import forge.game.event.GameEventCardAttachment;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
@@ -304,6 +305,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
Integer value = counters.get(counterName); Integer value = counters.get(counterName);
return value == null ? 0 : value; return value == null ? 0 : value;
} }
public final int getCounters(final CounterEnumType counterType) {
return getCounters(CounterType.get(counterType));
}
public void setCounters(final CounterType counterType, final Integer num) { public void setCounters(final CounterType counterType, final Integer num) {
if (num <= 0) { if (num <= 0) {
@@ -312,6 +316,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
counters.put(counterType, num); counters.put(counterType, num);
} }
} }
public void setCounters(final CounterEnumType counterType, final Integer num) {
setCounters(CounterType.get(counterType), num);
}
abstract public void setCounters(final Map<CounterType, Integer> allCounters); abstract public void setCounters(final Map<CounterType, Integer> allCounters);
@@ -321,6 +328,10 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
abstract public int subtractCounter(final CounterType counterName, final int n, final Player remover); abstract public int subtractCounter(final CounterType counterName, final int n, final Player remover);
abstract public void clearCounters(); abstract public void clearCounters();
public boolean canReceiveCounters(final CounterEnumType type) {
return canReceiveCounters(CounterType.get(type));
}
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) { public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
if (n <= 0 || !canReceiveCounters(counterType)) { if (n <= 0 || !canReceiveCounters(counterType)) {
// As per rule 107.1b // As per rule 107.1b
@@ -340,7 +351,18 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
table.put(source, this, counterType, n); table.put(source, this, counterType, n);
} }
public final void addCounter(final CounterEnumType counterType, final int n, final Player source, GameEntityCounterTable table) {
addCounter(CounterType.get(counterType), n, source, table);
}
public int subtractCounter(final CounterEnumType counterName, final int n, final Player remover) {
return subtractCounter(CounterType.get(counterName), n, remover);
}
abstract public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params); abstract public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params);
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
}
public Integer getCounterMax(final CounterType counterType) { public Integer getCounterMax(final CounterType counterType) {
return null; return null;
} }

View File

@@ -6,7 +6,6 @@ public enum GameLogEntryType {
TURN("Turn"), TURN("Turn"),
MULLIGAN("Mulligan"), MULLIGAN("Mulligan"),
ANTE("Ante"), ANTE("Ante"),
DRAFT("Draft"),
ZONE_CHANGE("Zone Change"), ZONE_CHANGE("Zone Change"),
PLAYER_CONTROL("Player control"), PLAYER_CONTROL("Player control"),
COMBAT("Combat"), COMBAT("Combat"),

View File

@@ -29,25 +29,25 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override @Override
public GameLogEntry visit(GameEventGameOutcome ev) { public GameLogEntry visit(GameEventGameOutcome ev) {
// Turn number counted from the starting player // Turn number counted from the starting player
int lastTurn = (int)Math.ceil((float)ev.result().getLastTurnNumber() / 2.0); int lastTurn = (int)Math.ceil((float)ev.result.getLastTurnNumber() / 2.0);
log.add(GameLogEntryType.GAME_OUTCOME, localizer.getMessage("lblTurn") + " " + lastTurn); log.add(GameLogEntryType.GAME_OUTCOME, localizer.getMessage("lblTurn") + " " + lastTurn);
for (String outcome : ev.result().getOutcomeStrings()) { for (String outcome : ev.result.getOutcomeStrings()) {
log.add(GameLogEntryType.GAME_OUTCOME, outcome); log.add(GameLogEntryType.GAME_OUTCOME, outcome);
} }
return generateSummary(ev.history()); return generateSummary(ev.history);
} }
@Override @Override
public GameLogEntry visit(GameEventScry ev) { public GameLogEntry visit(GameEventScry ev) {
String scryOutcome = ""; String scryOutcome = "";
if (ev.toTop() > 0 && ev.toBottom() > 0) { if (ev.toTop > 0 && ev.toBottom > 0) {
scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop())).replace("%bottom", String.valueOf(ev.toBottom())); scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop)).replace("%bottom", String.valueOf(ev.toBottom));
} else if (ev.toBottom() == 0) { } else if (ev.toBottom == 0) {
scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop())); scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop));
} else { } else {
scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player().toString()).replace("%bottom", String.valueOf(ev.toBottom())); scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player.toString()).replace("%bottom", String.valueOf(ev.toBottom));
} }
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome); return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome);
@@ -57,12 +57,12 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
public GameLogEntry visit(GameEventSurveil ev) { public GameLogEntry visit(GameEventSurveil ev) {
String surveilOutcome = ""; String surveilOutcome = "";
if (ev.toLibrary() > 0 && ev.toGraveyard() > 0) { if (ev.toLibrary > 0 && ev.toGraveyard > 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player().toString(), String.valueOf(ev.toLibrary()), String.valueOf(ev.toGraveyard())); surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player.toString(), String.valueOf(ev.toLibrary), String.valueOf(ev.toGraveyard));
} else if (ev.toGraveyard() == 0) { } else if (ev.toGraveyard == 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player().toString(), String.valueOf(ev.toLibrary())); surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player.toString(), String.valueOf(ev.toLibrary));
} else { } else {
surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player().toString(), String.valueOf(ev.toGraveyard())); surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player.toString(), String.valueOf(ev.toGraveyard));
} }
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, surveilOutcome); return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, surveilOutcome);
@@ -70,26 +70,26 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override @Override
public GameLogEntry visit(GameEventSpellResolved ev) { public GameLogEntry visit(GameEventSpellResolved ev) {
String messageForLog = ev.hasFizzled() ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell().getHostCard().toString()) : ev.spell().getStackDescription(); String messageForLog = ev.hasFizzled ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell.getHostCard().toString()) : ev.spell.getStackDescription();
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, messageForLog); return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, messageForLog);
} }
@Override @Override
public GameLogEntry visit(GameEventSpellAbilityCast event) { public GameLogEntry visit(GameEventSpellAbilityCast event) {
String player = event.sa().getActivatingPlayer().getName(); String player = event.sa.getActivatingPlayer().getName();
String action = event.sa().isSpell() ? localizer.getMessage("lblCast") String action = event.sa.isSpell() ? localizer.getMessage("lblCast")
: event.sa().isTrigger() ? localizer.getMessage("lblTriggered") : event.sa.isTrigger() ? localizer.getMessage("lblTriggered")
: localizer.getMessage("lblActivated"); : localizer.getMessage("lblActivated");
String object = event.si().getStackDescription().startsWith("Morph ") String object = event.si.getStackDescription().startsWith("Morph ")
? localizer.getMessage("lblMorph") ? localizer.getMessage("lblMorph")
: event.sa().getHostCard().toString(); : event.sa.getHostCard().toString();
String messageForLog = ""; String messageForLog = "";
if (event.sa().getTargetRestrictions() != null) { if (event.sa.getTargetRestrictions() != null) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (TargetChoices ch : event.sa().getAllTargetChoices()) { for (TargetChoices ch : event.sa.getAllTargetChoices()) {
if (null != ch) { if (null != ch) {
sb.append(ch); sb.append(ch);
} }
@@ -104,18 +104,18 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override @Override
public GameLogEntry visit(GameEventCardModeChosen ev) { public GameLogEntry visit(GameEventCardModeChosen ev) {
if (!ev.log()) { if (!ev.log) {
return null; return null;
} }
String modeChoiceOutcome; String modeChoiceOutcome;
if (ev.random()) { if (ev.random) {
modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName(), ev.mode()); modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName, ev.mode);
} else { } else {
modeChoiceOutcome = localizer.getMessage("lblLogPlayerChosenModeForCard", modeChoiceOutcome = localizer.getMessage("lblLogPlayerChosenModeForCard",
ev.player().toString(), ev.mode(), ev.cardName()); ev.player.toString(), ev.mode, ev.cardName);
} }
String name = CardTranslation.getTranslatedName(ev.cardName()); String name = CardTranslation.getTranslatedName(ev.cardName);
modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "CARDNAME", name); modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "CARDNAME", name);
modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "NICKNAME", modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "NICKNAME",
Lang.getInstance().getNickName(name)); Lang.getInstance().getNickName(name));
@@ -124,7 +124,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override @Override
public GameLogEntry visit(GameEventRandomLog ev) { public GameLogEntry visit(GameEventRandomLog ev) {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message()); return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message);
} }
private static GameLogEntry generateSummary(final Collection<GameOutcome> gamesPlayed) { private static GameLogEntry generateSummary(final Collection<GameOutcome> gamesPlayed) {
@@ -152,8 +152,8 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override @Override
public GameLogEntry visit(final GameEventPlayerControl event) { public GameLogEntry visit(final GameEventPlayerControl event) {
final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer(); final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer;
final Player p = event.player(); final Player p = event.player;
final String message; final String message;
if (newLobbyPlayer == null) { if (newLobbyPlayer == null) {
@@ -166,23 +166,23 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override @Override
public GameLogEntry visit(GameEventTurnPhase ev) { public GameLogEntry visit(GameEventTurnPhase ev) {
Player p = ev.playerTurn(); Player p = ev.playerTurn;
return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc() + Lang.getInstance().getPossessedObject(p.getName(), ev.phase().nameForUi)); return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc + Lang.getInstance().getPossessedObject(p.getName(), ev.phase.nameForUi));
} }
@Override @Override
public GameLogEntry visit(GameEventCardDamaged event) { public GameLogEntry visit(GameEventCardDamaged event) {
String additionalLog = ""; String additionalLog = "";
if (event.type() == DamageType.Deathtouch) { if (event.type == DamageType.Deathtouch) {
additionalLog = localizer.getMessage("lblDeathtouch"); additionalLog = localizer.getMessage("lblDeathtouch");
} }
if (event.type() == DamageType.M1M1Counters) { if (event.type == DamageType.M1M1Counters) {
additionalLog = localizer.getMessage("lblAsM1M1Counters"); additionalLog = localizer.getMessage("lblAsM1M1Counters");
} }
if (event.type() == DamageType.LoyaltyLoss) { if (event.type == DamageType.LoyaltyLoss) {
additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount())); additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount));
} }
String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source().toString(), String.valueOf(event.amount()), additionalLog, event.card().toString()); String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source.toString(), String.valueOf(event.amount), additionalLog, event.card.toString());
return new GameLogEntry(GameLogEntryType.DAMAGE, message); return new GameLogEntry(GameLogEntryType.DAMAGE, message);
} }
@@ -191,43 +191,43 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
*/ */
@Override @Override
public GameLogEntry visit(GameEventLandPlayed ev) { public GameLogEntry visit(GameEventLandPlayed ev) {
String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player().toString(), ev.land().toString()); String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player.toString(), ev.land.toString());
return new GameLogEntry(GameLogEntryType.LAND, message); return new GameLogEntry(GameLogEntryType.LAND, message);
} }
@Override @Override
public GameLogEntry visit(GameEventTurnBegan event) { public GameLogEntry visit(GameEventTurnBegan event) {
String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber()), event.turnOwner().toString()); String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber), event.turnOwner.toString());
return new GameLogEntry(GameLogEntryType.TURN, message); return new GameLogEntry(GameLogEntryType.TURN, message);
} }
@Override @Override
public GameLogEntry visit(GameEventPlayerDamaged ev) { public GameLogEntry visit(GameEventPlayerDamaged ev) {
String extra = ev.infect() ? localizer.getMessage("lblLogAsPoisonCounters") : ""; String extra = ev.infect ? localizer.getMessage("lblLogAsPoisonCounters") : "";
String damageType = ev.combat() ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat"); String damageType = ev.combat ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat");
String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source().toString(), String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source.toString(),
String.valueOf(ev.amount()), damageType, ev.target().toString(), extra); String.valueOf(ev.amount), damageType, ev.target.toString(), extra);
return new GameLogEntry(GameLogEntryType.DAMAGE, message); return new GameLogEntry(GameLogEntryType.DAMAGE, message);
} }
@Override @Override
public GameLogEntry visit(GameEventPlayerPoisoned ev) { public GameLogEntry visit(GameEventPlayerPoisoned ev) {
String message = localizer.getMessage("lblLogPlayerReceivesNPosionCounterFrom", String message = localizer.getMessage("lblLogPlayerReceivesNPosionCounterFrom",
ev.receiver().toString(), String.valueOf(ev.amount()), ev.source().toString()); ev.receiver.toString(), String.valueOf(ev.amount), ev.source.toString());
return new GameLogEntry(GameLogEntryType.DAMAGE, message); return new GameLogEntry(GameLogEntryType.DAMAGE, message);
} }
@Override @Override
public GameLogEntry visit(GameEventPlayerRadiation ev) { public GameLogEntry visit(GameEventPlayerRadiation ev) {
String message; String message;
final int change = ev.change(); final int change = ev.change;
String radCtr = CounterEnumType.RAD.getName().toLowerCase() + " " + String radCtr = CounterEnumType.RAD.getName().toLowerCase() + " " +
Localizer.getInstance().getMessage("lblCounter").toLowerCase(); Localizer.getInstance().getMessage("lblCounter").toLowerCase();
if (change >= 0) message = localizer.getMessage("lblLogPlayerRadiation", if (change >= 0) message = localizer.getMessage("lblLogPlayerRadiation",
ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr), ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr),
ev.source().toString()); ev.source.toString());
else message = localizer.getMessage("lblLogPlayerRadRemove", else message = localizer.getMessage("lblLogPlayerRadRemove",
ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr)); ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr));
return new GameLogEntry(GameLogEntryType.DAMAGE, message); return new GameLogEntry(GameLogEntryType.DAMAGE, message);
} }
@@ -239,16 +239,16 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
// Append Defending Player/Planeswalker // Append Defending Player/Planeswalker
// Not a big fan of the triple nested loop here // Not a big fan of the triple nested loop here
for (GameEntity k : ev.attackersMap().keySet()) { for (GameEntity k : ev.attackersMap.keySet()) {
Collection<Card> attackers = ev.attackersMap().get(k); Collection<Card> attackers = ev.attackersMap.get(k);
if (attackers == null || attackers.isEmpty()) { if (attackers == null || attackers.isEmpty()) {
continue; continue;
} }
if (sb.length() > 0) sb.append("\n"); if (sb.length() > 0) sb.append("\n");
sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player(), Lang.joinHomogenous(attackers), k)); sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player, Lang.joinHomogenous(attackers), k));
} }
if (sb.length() == 0) { if (sb.length() == 0) {
sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player().toString())); sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player.toString()));
} }
return new GameLogEntry(GameLogEntryType.COMBAT, sb.toString()); return new GameLogEntry(GameLogEntryType.COMBAT, sb.toString());
} }
@@ -262,7 +262,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
Collection<Card> blockers = null; Collection<Card> blockers = null;
for (Entry<GameEntity, MapOfLists<Card, Card>> kv : ev.blockers().entrySet()) { for (Entry<GameEntity, MapOfLists<Card, Card>> kv : ev.blockers.entrySet()) {
GameEntity defender = kv.getKey(); GameEntity defender = kv.getKey();
MapOfLists<Card, Card> attackers = kv.getValue(); MapOfLists<Card, Card> attackers = kv.getValue();
if (attackers == null || attackers.isEmpty()) { if (attackers == null || attackers.isEmpty()) {
@@ -298,7 +298,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override @Override
public GameLogEntry visit(GameEventMulligan ev) { public GameLogEntry visit(GameEventMulligan ev) {
String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player().getZone(ZoneType.Hand).size())).replace("%s", ev.player().toString()); String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player.getZone(ZoneType.Hand).size())).replace("%s", ev.player.toString());
return new GameLogEntry(GameLogEntryType.MULLIGAN, message); return new GameLogEntry(GameLogEntryType.MULLIGAN, message);
} }

View File

@@ -15,7 +15,6 @@ public class GameRules {
private boolean AISideboardingEnabled = false; private boolean AISideboardingEnabled = false;
private boolean sideboardForAI = false; private boolean sideboardForAI = false;
private final Set<GameType> appliedVariants = EnumSet.noneOf(GameType.class); private final Set<GameType> appliedVariants = EnumSet.noneOf(GameType.class);
private int simTimeout = 120;
// it's a preference, not rule... but I could hardly find a better place for it // it's a preference, not rule... but I could hardly find a better place for it
private boolean useGrayText; private boolean useGrayText;
@@ -125,12 +124,4 @@ public class GameRules {
public void setWarnAboutAICards(final boolean warnAboutAICards) { public void setWarnAboutAICards(final boolean warnAboutAICards) {
this.warnAboutAICards = warnAboutAICards; this.warnAboutAICards = warnAboutAICards;
} }
public int getSimTimeout() {
return this.simTimeout;
}
public void setSimTimeout(final int duration) {
this.simTimeout = duration;
}
} }

View File

@@ -24,7 +24,6 @@ public enum GameType {
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""), Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"), CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"),
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""), Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
QuestCommander (DeckFormat.Commander, true, true, false, "lblQuestCommander", ""),
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""), QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""), PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),
Adventure (DeckFormat.Adventure, true, false, false, "lblAdventure", ""), Adventure (DeckFormat.Adventure, true, false, false, "lblAdventure", ""),
@@ -72,8 +71,6 @@ public enum GameType {
return deck; return deck;
}); });
private static final EnumSet<GameType> DRAFT_FORMATS = EnumSet.of(Draft, QuestDraft, AdventureEvent);
private final DeckFormat deckFormat; private final DeckFormat deckFormat;
private final boolean isCardPoolLimited, canSideboard, addWonCardsMidGame; private final boolean isCardPoolLimited, canSideboard, addWonCardsMidGame;
private final String name, englishName, description; private final String name, englishName, description;
@@ -90,7 +87,7 @@ public enum GameType {
addWonCardsMidGame = addWonCardsMidgame0; addWonCardsMidGame = addWonCardsMidgame0;
name = localizer.getMessage(name0); name = localizer.getMessage(name0);
englishName = localizer.getEnglishMessage(name0); englishName = localizer.getEnglishMessage(name0);
if (!description0.isEmpty()) { if (description0.length()>0) {
description0 = localizer.getMessage(description0); description0 = localizer.getMessage(description0);
} }
description = description0; description = description0;
@@ -130,8 +127,19 @@ public enum GameType {
return addWonCardsMidGame; return addWonCardsMidGame;
} }
public boolean isDraft() { public boolean isCommandZoneNeeded() {
return DRAFT_FORMATS.contains(this); return true; //TODO: Figure out way to move command zone into field so it can be hidden when empty
/*switch (this) {
case Archenemy:
case Commander:
case Oathbreaker:
case TinyLeaders:
case Planechase:
case Vanguard:
return true;
default:
return false;
}*/
} }
public String toString() { public String toString() {
@@ -145,27 +153,6 @@ public enum GameType {
return description; return description;
} }
/**
* @return the deck sections used by most decks in this game type.
*/
public EnumSet<DeckSection> getPrimaryDeckSections() {
return deckFormat.getPrimaryDeckSections();
}
/**
* @return the set of variant card sections that decks for this game type can include.
*/
public EnumSet<DeckSection> getSupplimentalDeckSections() {
if(!deckFormat.getPrimaryDeckSections().contains(DeckSection.Main))
return EnumSet.noneOf(DeckSection.class); //Already an extra deck, like a dedicated Scheme or Planar deck.
if(deckFormat == DeckFormat.Limited)
return EnumSet.of(DeckSection.Conspiracy, DeckSection.Contraptions, DeckSection.Attractions);
if(this == Constructed || this == Commander)
return EnumSet.of(DeckSection.Avatar, DeckSection.Schemes, DeckSection.Planes, DeckSection.Conspiracy,
DeckSection.Attractions, DeckSection.Contraptions);
return EnumSet.of(DeckSection.Attractions, DeckSection.Contraptions);
}
public static GameType smartValueOf(String name) { public static GameType smartValueOf(String name) {
return Enums.getIfPresent(GameType.class, name).orNull(); return Enums.getIfPresent(GameType.class, name).orNull();
} }

View File

@@ -215,7 +215,6 @@ public class GameView extends TrackableObject {
} }
public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) { public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
if (dependencies.isEmpty()) { if (dependencies.isEmpty()) {
set(TrackableProperty.Dependencies, "");
return; return;
} }
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View File

@@ -23,7 +23,6 @@ import forge.item.PaperCard;
import forge.util.Localizer; import forge.util.Localizer;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
@@ -225,7 +224,6 @@ public class Match {
// friendliness // friendliness
Map<Player, Map<DeckSection, List<? extends PaperCard>>> rAICards = new HashMap<>(); Map<Player, Map<DeckSection, List<? extends PaperCard>>> rAICards = new HashMap<>();
Multimap<Player, PaperCard> removedAnteCards = ArrayListMultimap.create(); Multimap<Player, PaperCard> removedAnteCards = ArrayListMultimap.create();
Map<Player, List<PaperCard>> unsupported = new HashMap<>();
final FCollectionView<Player> players = game.getPlayers(); final FCollectionView<Player> players = game.getPlayers();
final List<RegisteredPlayer> playersConditions = game.getMatch().getPlayers(); final List<RegisteredPlayer> playersConditions = game.getMatch().getPlayers();
@@ -290,32 +288,22 @@ public class Match {
} }
} }
Deck toCheck = psc.getDeck(); Deck myDeck = psc.getDeck();
if (toCheck == null) { player.setDraftNotes(myDeck.getDraftNotes());
try {
System.err.println(psc.getPlayer().getName() + " Deck is NULL...");
int val = rules.getGameType().getDeckFormat().getMainRange().getMinimum();
toCheck = new Deck("NULL");
if (val > 0)
toCheck.getMain().add("Wastes", val);
} catch (Exception ignored) {}
}
Pair<Deck, List<PaperCard>> myDeck = toCheck.getValid();
player.setDraftNotes(myDeck.getLeft().getDraftNotes());
Set<PaperCard> myRemovedAnteCards = null; Set<PaperCard> myRemovedAnteCards = null;
if (!rules.useAnte()) { if (!rules.useAnte()) {
myRemovedAnteCards = getRemovedAnteCards(myDeck.getLeft()); myRemovedAnteCards = getRemovedAnteCards(myDeck);
for (PaperCard cp: myRemovedAnteCards) { for (PaperCard cp: myRemovedAnteCards) {
for (Entry<DeckSection, CardPool> ds : myDeck.getLeft()) { for (Entry<DeckSection, CardPool> ds : myDeck) {
ds.getValue().removeAll(cp); ds.getValue().removeAll(cp);
} }
} }
} }
preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil()); preparePlayerZone(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil());
if (myDeck.getLeft().has(DeckSection.Sideboard)) { if (myDeck.has(DeckSection.Sideboard)) {
preparePlayerZone(player, ZoneType.Sideboard, myDeck.getLeft().get(DeckSection.Sideboard), psc.useRandomFoil()); preparePlayerZone(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil());
// Assign Companion // Assign Companion
Card companion = player.assignCompanion(game, person); Card companion = player.assignCompanion(game, person);
@@ -334,7 +322,7 @@ public class Match {
player.shuffle(null); player.shuffle(null);
if (isFirstGame) { if (isFirstGame) {
Map<DeckSection, List<? extends PaperCard>> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck.getLeft()); Map<DeckSection, List<? extends PaperCard>> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck);
if (cardsComplained != null && !cardsComplained.isEmpty()) { if (cardsComplained != null && !cardsComplained.isEmpty()) {
rAICards.put(player, cardsComplained); rAICards.put(player, cardsComplained);
} }
@@ -349,7 +337,6 @@ public class Match {
if (myRemovedAnteCards != null && !myRemovedAnteCards.isEmpty()) { if (myRemovedAnteCards != null && !myRemovedAnteCards.isEmpty()) {
removedAnteCards.putAll(player, myRemovedAnteCards); removedAnteCards.putAll(player, myRemovedAnteCards);
} }
unsupported.put(player, myDeck.getRight());
} }
final Localizer localizer = Localizer.getInstance(); final Localizer localizer = Localizer.getInstance();
@@ -360,10 +347,6 @@ public class Match {
if (!removedAnteCards.isEmpty()) { if (!removedAnteCards.isEmpty()) {
game.getAction().revealAnte(localizer.getMessage("lblAnteCardsRemoved"), removedAnteCards); game.getAction().revealAnte(localizer.getMessage("lblAnteCardsRemoved"), removedAnteCards);
} }
if (!unsupported.isEmpty()) {
game.getAction().revealUnsupported(unsupported);
}
} }
private void executeAnte(Game lastGame) { private void executeAnte(Game lastGame) {

View File

@@ -3,6 +3,7 @@ package forge.game.ability;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.spellability.AbilityActivated; import forge.game.spellability.AbilityActivated;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import java.util.Map; import java.util.Map;
@@ -17,7 +18,14 @@ public class AbilityApiBased extends AbilityActivated {
api = api0; api = api0;
effect = api.getSpellEffect(); effect = api.getSpellEffect();
effect.buildSpellAbility(this); if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
this.setUndoable(true); // will try at least
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
} }
@Override @Override

View File

@@ -202,6 +202,15 @@ public final class AbilityFactory {
final Card hostCard = state.getCard(); final Card hostCard = state.getCard();
TargetRestrictions abTgt = mapParams.containsKey("ValidTgts") ? readTarget(mapParams) : null; TargetRestrictions abTgt = mapParams.containsKey("ValidTgts") ? readTarget(mapParams) : null;
if (api == ApiType.CopySpellAbility || api == ApiType.Counter || api == ApiType.ChangeTargets || api == ApiType.ControlSpell) {
// Since all "CopySpell" ABs copy things on the Stack no need for it to be everywhere
// Since all "Counter" or "ChangeTargets" abilities only target the Stack Zone
// No need to have each of those scripts have that info
if (abTgt != null) {
abTgt.setZone(ZoneType.Stack);
}
}
if (abCost == null) { if (abCost == null) {
abCost = parseAbilityCost(state, mapParams, type); abCost = parseAbilityCost(state, mapParams, type);
} }
@@ -383,9 +392,6 @@ public final class AbilityFactory {
if (mapParams.containsKey("TargetsWithDifferentCMC")) { if (mapParams.containsKey("TargetsWithDifferentCMC")) {
abTgt.setDifferentCMC(true); abTgt.setDifferentCMC(true);
} }
if (mapParams.containsKey("TargetsWithDifferentNames")) {
abTgt.setDifferentNames(true);
}
if (mapParams.containsKey("TargetsWithEqualToughness")) { if (mapParams.containsKey("TargetsWithEqualToughness")) {
abTgt.setEqualToughness(true); abTgt.setEqualToughness(true);
} }

View File

@@ -113,6 +113,13 @@ public class AbilityUtils {
} }
} else if (defined.equals("Enchanted")) { } else if (defined.equals("Enchanted")) {
c = hostCard.getEnchantingCard(); c = hostCard.getEnchantingCard();
if (c == null && sa instanceof SpellAbility) {
SpellAbility root = ((SpellAbility)sa).getRootAbility();
CardCollection sacrificed = root.getPaidList("Sacrificed", true);
if (sacrificed != null && !sacrificed.isEmpty()) {
c = sacrificed.getFirst().getEnchantingCard();
}
}
} else if (defined.equals("TopOfGraveyard")) { } else if (defined.equals("TopOfGraveyard")) {
final CardCollectionView grave = player.getCardsIn(ZoneType.Graveyard); final CardCollectionView grave = player.getCardsIn(ZoneType.Graveyard);
@@ -2338,9 +2345,6 @@ public class AbilityUtils {
if (sq[0].equals("YourSpeed")) { if (sq[0].equals("YourSpeed")) {
return doXMath(player.getSpeed(), expr, c, ctb); return doXMath(player.getSpeed(), expr, c, ctb);
} }
if (sq[0].equals("AllFourBend")) {
return doXMath(calculateAmount(c, sq[player.hasAllElementBend() ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("Night")) { if (sq[0].equals("Night")) {
return doXMath(calculateAmount(c, sq[game.isNight() ? 1 : 2], ctb), expr, c, ctb); return doXMath(calculateAmount(c, sq[game.isNight() ? 1 : 2], ctb), expr, c, ctb);

View File

@@ -19,7 +19,6 @@ public enum ApiType {
AddPhase (AddPhaseEffect.class), AddPhase (AddPhaseEffect.class),
AddTurn (AddTurnEffect.class), AddTurn (AddTurnEffect.class),
AdvanceCrank (AdvanceCrankEffect.class), AdvanceCrank (AdvanceCrankEffect.class),
Airbend (AirbendEffect.class),
AlterAttribute (AlterAttributeEffect.class), AlterAttribute (AlterAttributeEffect.class),
Amass (AmassEffect.class), Amass (AmassEffect.class),
Animate (AnimateEffect.class), Animate (AnimateEffect.class),
@@ -82,7 +81,6 @@ public enum ApiType {
Draft (DraftEffect.class), Draft (DraftEffect.class),
Draw (DrawEffect.class), Draw (DrawEffect.class),
EachDamage (DamageEachEffect.class), EachDamage (DamageEachEffect.class),
Earthbend (EarthbendEffect.class),
Effect (EffectEffect.class), Effect (EffectEffect.class),
Encode (EncodeEffect.class), Encode (EncodeEffect.class),
EndCombatPhase (EndCombatPhaseEffect.class), EndCombatPhase (EndCombatPhaseEffect.class),

View File

@@ -49,8 +49,6 @@ public abstract class SpellAbilityEffect {
return sa.getDescription(); return sa.getDescription();
} }
public void buildSpellAbility(final SpellAbility sa) {}
/** /**
* Returns this effect description with needed prelude and epilogue. * Returns this effect description with needed prelude and epilogue.
* @param params * @param params

View File

@@ -4,6 +4,7 @@ import java.util.Map;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.Spell; import forge.game.spellability.Spell;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
@@ -23,7 +24,13 @@ public class SpellApiBased extends Spell {
// A spell is always intrinsic // A spell is always intrinsic
this.setIntrinsic(true); this.setIntrinsic(true);
effect.buildSpellAbility(this); if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
} }
@Override @Override

View File

@@ -2,6 +2,8 @@ package forge.game.ability;
import java.util.Map; import java.util.Map;
import forge.game.ability.effects.ChangeZoneAllEffect;
import forge.game.ability.effects.ChangeZoneEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.spellability.AbilityStatic; import forge.game.spellability.AbilityStatic;
@@ -18,7 +20,9 @@ public class StaticAbilityApiBased extends AbilityStatic {
api = api0; api = api0;
effect = api.getSpellEffect(); effect = api.getSpellEffect();
effect.buildSpellAbility(this); if (effect instanceof ChangeZoneEffect || effect instanceof ChangeZoneAllEffect) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
} }
@Override @Override

View File

@@ -1,93 +0,0 @@
package forge.game.ability.effects;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardZoneTable;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Lang;
public class AirbendEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Airbend ");
Iterable<Card> tgts;
if (sa.usesTargeting()) {
tgts = getCardsfromTargets(sa);
} else { // otherwise add self to list and go from there
tgts = sa.knownDetermineDefined(sa.getParam("Defined"));
}
sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts)));
sb.append(".");
if (Iterables.size(tgts) > 1) {
sb.append(" (Exile them. While each one is exiled, its owner may cast it for {2} rather than its mana cost.)");
} else {
sb.append(" (Exile it. While its exiled, its owner may cast it for {2} rather than its mana cost.)");
}
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final Player pl = sa.getActivatingPlayer();
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
for (Card c : getTargetCards(sa)) {
final Card gameCard = game.getCardState(c, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
// this should check Self too
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue;
}
if (!gameCard.canExiledBy(sa, true)) {
continue;
}
handleExiledWith(gameCard, sa);
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, triggerList);
Card movedCard = game.getAction().exile(gameCard, sa, moveParams);
if (movedCard == null || !movedCard.isInZone(ZoneType.Exile)) {
continue;
}
// Effect to cast for 2 from exile
Card eff = createEffect(sa, movedCard.getOwner(), "Airbend" + movedCard, hostCard.getImageKey());
eff.addRemembered(movedCard);
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | MayPlayAltManaCost$ 2 | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
eff.addStaticAbility(sbPlay.toString());
addForgetOnMovedTrigger(eff, "Exile");
addForgetOnCastTrigger(eff, "Card.IsRemembered");
game.getAction().moveToCommand(eff, sa);
}
triggerList.triggerChangesZoneAll(game, sa);
handleExiledWith(triggerList.allCards(), sa);
pl.triggerElementalBend(TriggerType.Airbend);
}
}

View File

@@ -46,9 +46,6 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
boolean altered = false; boolean altered = false;
switch (attr.trim()) { switch (attr.trim()) {
case "Harnessed":
altered = gameCard.setHarnessed(activate);
break;
case "Plotted": case "Plotted":
altered = gameCard.setPlotted(activate); altered = gameCard.setPlotted(activate);

View File

@@ -17,6 +17,7 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CardZoneTable; import forge.game.card.CardZoneTable;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo; import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCombatChanged; import forge.game.event.GameEventCombatChanged;
import forge.game.event.GameEventTokenCreated; import forge.game.event.GameEventTokenCreated;
@@ -85,7 +86,7 @@ public class AmassEffect extends TokenEffectBase {
} }
Map<String, Object> params = Maps.newHashMap(); Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", CounterEnumType.P1P1); params.put("CounterType", CounterType.get(CounterEnumType.P1P1));
params.put("Amount", amount); params.put("Amount", amount);
Card tgt = activator.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), false, params); Card tgt = activator.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), false, params);

View File

@@ -90,16 +90,6 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.addPerpetual(p); c.addPerpetual(p);
p.applyEffect(c); p.applyEffect(c);
} }
if (sa.hasParam("ManaCost")) {
final ManaCost manaCost = new ManaCost(new ManaCostParser(sa.getParam("ManaCost")));
if (perpetual) {
PerpetualManaCost p = new PerpetualManaCost(timestamp, manaCost);
c.addPerpetual(p);
p.applyEffect(c);
} else {
c.addChangedManaCost(manaCost, timestamp, (long) 0);
}
}
if (!addType.isEmpty() || !removeType.isEmpty() || addAllCreatureTypes || !remove.isEmpty()) { if (!addType.isEmpty() || !removeType.isEmpty() || addAllCreatureTypes || !remove.isEmpty()) {
if (perpetual) { if (perpetual) {

View File

@@ -18,7 +18,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetChoices; import forge.game.spellability.TargetChoices;
import forge.game.zone.MagicStack; import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.Localizer; import forge.util.Localizer;
@@ -28,13 +27,6 @@ import forge.util.Localizer;
*/ */
public class ChangeTargetsEffect extends SpellAbilityEffect { public class ChangeTargetsEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.ability.SpellAbilityEffect#resolve(forge.card.spellability.SpellAbility) * @see forge.card.ability.SpellAbilityEffect#resolve(forge.card.spellability.SpellAbility)
*/ */

View File

@@ -7,7 +7,6 @@ import com.google.common.collect.Iterables;
import forge.game.Game; import forge.game.Game;
import forge.game.GameActionUtil; import forge.game.GameActionUtil;
import forge.game.GameEntityCounterTable; import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
@@ -22,12 +21,6 @@ import forge.util.Localizer;
import forge.util.TextUtil; import forge.util.TextUtil;
public class ChangeZoneAllEffect extends SpellAbilityEffect { public class ChangeZoneAllEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
}
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
// TODO build Stack Description will need expansion as more cards are added // TODO build Stack Description will need expansion as more cards are added

View File

@@ -6,7 +6,6 @@ import com.google.common.collect.Maps;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.CardType; import forge.card.CardType;
import forge.game.*; import forge.game.*;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
@@ -19,6 +18,7 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.Zone; import forge.game.zone.Zone;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -34,11 +34,6 @@ import java.util.Map;
public class ChangeZoneEffect extends SpellAbilityEffect { public class ChangeZoneEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
}
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
if (sa.isHidden()) { if (sa.isHidden()) {
@@ -764,7 +759,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
StringBuilder sbPlay = new StringBuilder(); StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand+!ThisTurnEntered"); sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand+!ThisTurnEntered");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card."); sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
eff.addStaticAbility(sbPlay.toString()); final StaticAbility st = eff.addStaticAbility(sbPlay.toString());
eff.addRemembered(movedCard); eff.addRemembered(movedCard);
addForgetOnMovedTrigger(eff, "Exile"); addForgetOnMovedTrigger(eff, "Exile");
addForgetOnCastTrigger(eff, "Card.IsRemembered"); addForgetOnCastTrigger(eff, "Card.IsRemembered");
@@ -969,11 +964,13 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
String prompt; String prompt;
if (sa.hasParam("OptionalPrompt")) { if (sa.hasParam("OptionalPrompt")) {
prompt = sa.getParam("OptionalPrompt"); prompt = sa.getParam("OptionalPrompt");
} else if (defined) { } else {
if (defined) {
prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase(), destination.getTranslatedName().toLowerCase()); prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase(), destination.getTranslatedName().toLowerCase());
} else { } else {
prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase()); prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase());
} }
}
String message = MessageUtil.formatMessage(prompt , decider, player); String message = MessageUtil.formatMessage(prompt , decider, player);
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) { if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) {
continue; continue;

View File

@@ -10,17 +10,8 @@ import forge.game.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
public class ControlSpellEffect extends SpellAbilityEffect { public class ControlSpellEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility) * @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility)
*/ */

View File

@@ -23,12 +23,6 @@ import java.util.Map;
public class CopySpellAbilityEffect extends SpellAbilityEffect { public class CopySpellAbilityEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {

View File

@@ -21,13 +21,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class CounterEffect extends SpellAbilityEffect { public class CounterEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();

View File

@@ -102,29 +102,24 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
int totalRemoved = 0; int totalRemoved = 0;
CardCollectionView srcCards; CardCollectionView srcCards;
if (sa.hasParam("Choices")) { if (sa.hasParam("Choices")) {
ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone")) ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone"))
: ZoneType.Battlefield; : ZoneType.Battlefield;
srcCards = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"),
activator, source, sa);
} else {
srcCards = getTargetCards(sa);
}
if (sa.isReplacementAbility()) {
srcCards = new CardCollection(srcCards).filter(c -> !c.isInPlay() || sa.getLastStateBattlefield().contains(c));
}
if (sa.hasParam("Choices")) { CardCollection choices = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"),
activator, source, sa);
int min = 1; int min = 1;
int max = 1; int max = 1;
if (sa.hasParam("ChoiceOptional")) { if (sa.hasParam("ChoiceOptional")) {
min = 0; min = 0;
max = srcCards.size(); max = choices.size();
} }
if (sa.hasParam("ChoiceNum")) { if (sa.hasParam("ChoiceNum")) {
min = max = AbilityUtils.calculateAmount(source, sa.getParam("ChoiceNum"), sa); min = max = AbilityUtils.calculateAmount(source, sa.getParam("ChoiceNum"), sa);
} }
if (srcCards.size() < min) { if (choices.size() < min) {
return; return;
} }
@@ -133,12 +128,13 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
title = title.replace(" ", " "); title = title.replace(" ", " ");
Map<String, Object> params = Maps.newHashMap(); Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", counterType); params.put("CounterType", counterType);
srcCards = pc.chooseCardsForEffect(srcCards, sa, title, min, max, min == 0, params); srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params);
} else { } else {
for (final Player tgtPlayer : getTargetPlayers(sa)) { for (final Player tgtPlayer : getTargetPlayers(sa)) {
if (!tgtPlayer.isInGame()) { if (!tgtPlayer.isInGame()) {
continue; continue;
} }
// Removing energy
if (type.equals("All")) { if (type.equals("All")) {
for (Map.Entry<CounterType, Integer> e : Lists.newArrayList(tgtPlayer.getCounters().entrySet())) { for (Map.Entry<CounterType, Integer> e : Lists.newArrayList(tgtPlayer.getCounters().entrySet())) {
totalRemoved += tgtPlayer.subtractCounter(e.getKey(), e.getValue(), activator); totalRemoved += tgtPlayer.subtractCounter(e.getKey(), e.getValue(), activator);
@@ -154,6 +150,8 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
} }
} }
} }
srcCards = getTargetCards(sa);
} }
for (final Card tgtCard : srcCards) { for (final Card tgtCard : srcCards) {

View File

@@ -1,88 +0,0 @@
package forge.game.ability.effects;
import java.util.Arrays;
import java.util.EnumSet;
import forge.card.RemoveType;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CounterEnumType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import forge.util.Lang;
public class EarthbendEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Earthbend ");
final Card card = sa.getHostCard();
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
sb.append(amount).append(". (Target land you control becomes a 0/0 creature with haste that's still a land. Put ");
sb.append(Lang.nounWithNumeral(amount, "+1/+1 counter"));
sb.append(" on it. When it dies or is exiled, return it to the battlefield tapped.)");
return sb.toString();
}
@Override
public void buildSpellAbility(final SpellAbility sa) {
TargetRestrictions abTgt = new TargetRestrictions("Select target land you control", "Land.YouCtrl".split(","), "1", "1");
sa.setTargetRestrictions(abTgt);
}
@Override
public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
final Player pl = sa.getActivatingPlayer();
int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa);
long ts = game.getNextTimestamp();
String desc = "When it dies or is exiled, return it to the battlefield tapped.";
String sbTrigA = "Mode$ ChangesZone | ValidCard$ Card.IsTriggerRemembered | Origin$ Battlefield | Destination$ Graveyard | TriggerDescription$ " + desc;
String sbTrigB = "Mode$ Exiled | Origin$ Battlefield | ValidCard$ Card.IsTriggerRemembered | TriggerZones$ Battlefield | TriggerDescription$ " + desc;
// Earthbend should only target one land
for (Card c : getTargetCards(sa)) {
c.addNewPT(0, 0, ts, 0);
c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false);
c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null);
GameEntityCounterTable table = new GameEntityCounterTable();
c.addCounter(CounterEnumType.P1P1, num, pl, table);
table.replaceCounterEffect(game, sa, true);
buildTrigger(sa, c, sbTrigA, "Graveyard");
buildTrigger(sa, c, sbTrigB, "Exile");
}
pl.triggerElementalBend(TriggerType.Earthbend);
}
protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
String trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ " + zone + " | Destination$ Battlefield | Tapped$ True";
final Trigger trig = TriggerHandler.parseTrigger(sbTrig, CardCopyService.getLKICopy(source), sa.isIntrinsic());
final SpellAbility newSa = AbilityFactory.getAbility(trigSA, sa.getHostCard());
newSa.setIntrinsic(sa.isIntrinsic());
trig.addRemembered(c);
trig.setOverridingAbility(newSa);
trig.setSpawningAbility(sa.copy(sa.getHostCard(), true));
trig.setKeyword(trig.getSpawningAbility().getKeyword());
game.getTriggerHandler().registerDelayedTrigger(trig);
}
}

View File

@@ -8,7 +8,6 @@ import java.util.Map;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ImageKeys; import forge.ImageKeys;
import forge.StaticData;
import forge.game.Game; import forge.game.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
@@ -143,20 +142,15 @@ public class EffectEffect extends SpellAbilityEffect {
} }
String image; String image;
if (name.startsWith("Emblem")) {
if (sa.hasParam("Image")) { if (sa.hasParam("Image")) {
image = StaticData.instance().getOtherImageKey(sa.getParam("Image"), hostCard.getSetCode()); image = ImageKeys.getTokenKey(sa.getParam("Image"));
} else { } else if (name.startsWith("Emblem")) { // try to get the image from name
// try to get the image from name image = ImageKeys.getTokenKey(
String imageKey = TextUtil.fastReplace( TextUtil.fastReplace(
TextUtil.fastReplace( TextUtil.fastReplace(
TextUtil.fastReplace(name.toLowerCase(), "", "_"), TextUtil.fastReplace(name.toLowerCase(), "", "_"),
",", ""), ",", ""),
" ", "_"); " ", "_").toLowerCase());
image = StaticData.instance().getOtherImageKey(imageKey, hostCard.getSetCode());
}
} else if (sa.hasParam("Image")) {
image = ImageKeys.getTokenKey(sa.getParam("Image"));
} else { // use host image } else { // use host image
image = hostCard.getImageKey(); image = hostCard.getImageKey();
} }

View File

@@ -34,9 +34,6 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final Game game = host.getGame(); final Game game = host.getGame();
// CR 603.12a if the trigger event or events occur multiple times during the resolution of the spell or ability that created it,
// the reflexive triggered ability will trigger once for each of those times
int amt = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("TriggerAmount", "1"), sa); int amt = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("TriggerAmount", "1"), sa);
if (amt <= 0) { if (amt <= 0) {
return; return;

View File

@@ -54,29 +54,33 @@ public class LifeExchangeEffect extends SpellAbilityEffect {
final int life1 = p1.getLife(); final int life1 = p1.getLife();
final int life2 = p2.getLife(); final int life2 = p2.getLife();
final int diff = Math.abs(life1 - life2);
if (life2 > life1) { if (sa.hasParam("RememberDifference")) {
// swap players final int diff = life1 - life2;
Player tmp = p2; source.addRemembered(diff);
p2 = p1;
p1 = tmp;
} }
if (diff > 0 && p1.canLoseLife() && p2.canGainLife()) {
final Map<Player, Integer> lossMap = Maps.newHashMap();
if ((life1 > life2) && p1.canLoseLife() && p2.canGainLife()) {
final int diff = life1 - life2;
final int lost = p1.loseLife(diff, false, false); final int lost = p1.loseLife(diff, false, false);
p2.gainLife(diff, source, sa); p2.gainLife(diff, source, sa);
if (lost > 0) { if (lost > 0) {
final Map<Player, Integer> lossMap = Maps.newHashMap();
lossMap.put(p1, lost); lossMap.put(p1, lost);
}
} else if ((life2 > life1) && p2.canLoseLife() && p1.canGainLife()) {
final int diff = life2 - life1;
final int lost = p2.loseLife(diff, false, false);
p1.gainLife(diff, source, sa);
if (lost > 0) {
lossMap.put(p2, lost);
}
} else {
// they are equal or can't be exchanged, so nothing to do
}
if (!lossMap.isEmpty()) { // Run triggers if any player actually lost life
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPIMap(lossMap); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPIMap(lossMap);
source.getGame().getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false); source.getGame().getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false);
if (sa.hasParam("RememberOwnLoss") && p1.equals(sa.getActivatingPlayer())) {
source.addRemembered(lost);
}
}
}
if (sa.hasParam("RememberDifference")) {
source.addRemembered(p1.getLife() - p2.getLife());
} }
} }
} }

View File

@@ -51,7 +51,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
if (source.hasNamedCard()) { if (source.hasNamedCard()) {
names.addAll(source.getNamedCards()); names.addAll(source.getNamedCards());
} else { } else {
System.err.println("Malformed MakeCard entry! - " + source); System.err.println("Malformed MakeCard entry! - " + source.toString());
} }
} else { } else {
names.add(n); names.add(n);
@@ -72,8 +72,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
cards = AbilityUtils.getDefinedCards(source, def, sa); cards = AbilityUtils.getDefinedCards(source, def, sa);
} }
for (final Card c : cards) { for (final Card c : cards) {
//get the original papercard name names.add(c.getName());
names.add(c.getPaperCard().getName());
} }
} else if (sa.hasParam("Spellbook")) { } else if (sa.hasParam("Spellbook")) {
faces.addAll(parseFaces(sa, "Spellbook")); faces.addAll(parseFaces(sa, "Spellbook"));

View File

@@ -19,11 +19,9 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart; import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Localizer; import forge.util.Localizer;
import io.sentry.Breadcrumb; import io.sentry.Breadcrumb;
@@ -31,14 +29,6 @@ import io.sentry.Sentry;
public class ManaEffect extends SpellAbilityEffect { public class ManaEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
if (sa.getParent() == null) {
sa.setUndoable(true); // will try at least
}
}
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard(); final Card card = sa.getHostCard();
@@ -271,14 +261,10 @@ public class ManaEffect extends SpellAbilityEffect {
producedMana.append(abMana.produceMana(mana, p, sa)); producedMana.append(abMana.produceMana(mana, p, sa));
} }
// Only clear express choice after mana has been produced
abMana.clearExpressChoice();
abMana.tapsForMana(sa.getRootAbility(), producedMana.toString()); abMana.tapsForMana(sa.getRootAbility(), producedMana.toString());
if (sa.isKeyword(Keyword.FIREBENDING)) { // Only clear express choice after mana has been produced
activator.triggerElementalBend(TriggerType.Firebend); abMana.clearExpressChoice();
}
} }
/** /**

View File

@@ -17,14 +17,6 @@ import forge.util.Localizer;
public class ManaReflectedEffect extends SpellAbilityEffect { public class ManaReflectedEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
if (sa.getParent() == null) {
sa.setUndoable(true); // will try at least
}
}
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility) * @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility)
*/ */

View File

@@ -15,9 +15,8 @@ public class PermanentCreatureEffect extends PermanentEffect {
public String getStackDescription(final SpellAbility sa) { public String getStackDescription(final SpellAbility sa) {
final CardState source = sa.getCardState(); final CardState source = sa.getCardState();
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" "); sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ").append(source.getBasePowerString());
sb.append(sa.getParamOrDefault("SetPower", source.getBasePowerString())); sb.append(" / ").append(source.getBaseToughnessString());
sb.append(" / ").append(sa.getParamOrDefault("SetToughness", source.getBaseToughnessString()));
return sb.toString(); return sb.toString();
} }
} }

View File

@@ -17,6 +17,7 @@ import forge.game.GameEntity;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil; import forge.game.card.CardFactoryUtil;
import forge.game.card.CardUtil; import forge.game.card.CardUtil;
import forge.game.card.perpetual.PerpetualKeywords; import forge.game.card.perpetual.PerpetualKeywords;
@@ -281,17 +282,6 @@ public class PumpEffect extends SpellAbilityEffect {
List<Card> tgtCards = getCardsfromTargets(sa); List<Card> tgtCards = getCardsfromTargets(sa);
List<Player> tgtPlayers = getTargetPlayers(sa); List<Player> tgtPlayers = getTargetPlayers(sa);
if (sa.hasParam("Optional")) {
final String targets = Lang.joinHomogenous(tgtCards);
final String message = sa.hasParam("OptionQuestion")
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
: Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets);
if (!activator.getController().confirmAction(sa, null, message, null)) {
return;
}
}
List<String> keywords = Lists.newArrayList(); List<String> keywords = Lists.newArrayList();
if (sa.hasParam("KW")) { if (sa.hasParam("KW")) {
keywords.addAll(Arrays.asList(sa.getParam("KW").split(" & "))); keywords.addAll(Arrays.asList(sa.getParam("KW").split(" & ")));
@@ -317,6 +307,8 @@ public class PumpEffect extends SpellAbilityEffect {
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, host, sa); keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, host, sa);
} }
final CardCollection untargetedCards = CardUtil.getRadiance(sa);
if (sa.hasParam("DefinedKW")) { if (sa.hasParam("DefinedKW")) {
String defined = sa.getParam("DefinedKW"); String defined = sa.getParam("DefinedKW");
if (defined.equals("ChosenType")) { if (defined.equals("ChosenType")) {
@@ -402,6 +394,17 @@ public class PumpEffect extends SpellAbilityEffect {
keywords = choice; keywords = choice;
} }
if (sa.hasParam("Optional")) {
final String targets = Lang.joinHomogenous(tgtCards);
final String message = sa.hasParam("OptionQuestion")
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
: Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets);
if (!activator.getController().confirmAction(sa, null, message, null)) {
return;
}
}
if (sa.hasParam("RememberObjects")) { if (sa.hasParam("RememberObjects")) {
host.addRemembered(AbilityUtils.getDefinedObjects(host, sa.getParam("RememberObjects"), sa)); host.addRemembered(AbilityUtils.getDefinedObjects(host, sa.getParam("RememberObjects"), sa));
} }
@@ -491,7 +494,7 @@ public class PumpEffect extends SpellAbilityEffect {
registerDelayedTrigger(sa, sa.getParam("AtEOT"), tgtCards); registerDelayedTrigger(sa, sa.getParam("AtEOT"), tgtCards);
} }
for (final Card tgtC : CardUtil.getRadiance(sa)) { for (final Card tgtC : untargetedCards) {
// only pump things in PumpZone // only pump things in PumpZone
if (!tgtC.isInZones(pumpZones)) { if (!tgtC.isInZones(pumpZones)) {
continue; continue;

View File

@@ -369,8 +369,8 @@ public class RollDiceEffect extends SpellAbilityEffect {
List<Card> canIncrementDice = new ArrayList<>(); List<Card> canIncrementDice = new ArrayList<>();
for (Card c : xenosquirrels) { for (Card c : xenosquirrels) {
// Xenosquirrels must have a P1P1 counter on it to remove in order to modify // Xenosquirrels must have a P1P1 counter on it to remove in order to modify
Integer P1P1Counters = c.getCounters().get(CounterEnumType.P1P1); Integer P1P1Counters = c.getCounters().get(CounterType.get(CounterEnumType.P1P1));
if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterEnumType.P1P1)) { if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterType.get(CounterEnumType.P1P1))) {
canIncrementDice.add(c); canIncrementDice.add(c);
} }
} }
@@ -399,7 +399,6 @@ public class RollDiceEffect extends SpellAbilityEffect {
* @param repParams replacement effect parameters * @param repParams replacement effect parameters
* @return list of final roll results after applying ignores and replacements, sorted in ascending order * @return list of final roll results after applying ignores and replacements, sorted in ascending order
*/ */
@SuppressWarnings("unchecked")
private static List<Integer> rollAction(int amount, int sides, int ignore, List<Integer> rollsResult, List<Integer> ignored, Map<Player, Integer> ignoreChosenMap, Set<Card> dicePTExchanges, Player player, Map<AbilityKey, Object> repParams) { private static List<Integer> rollAction(int amount, int sides, int ignore, List<Integer> rollsResult, List<Integer> ignored, Map<Player, Integer> ignoreChosenMap, Set<Card> dicePTExchanges, Player player, Map<AbilityKey, Object> repParams) {
repParams.put(AbilityKey.Sides, sides); repParams.put(AbilityKey.Sides, sides);
@@ -417,8 +416,6 @@ public class RollDiceEffect extends SpellAbilityEffect {
ignoreChosenMap = (Map<Player, Integer>) repParams.get(AbilityKey.IgnoreChosen); ignoreChosenMap = (Map<Player, Integer>) repParams.get(AbilityKey.IgnoreChosen);
break; break;
} }
default:
break;
} }
List<Integer> naturalRolls = (rollsResult == null ? new ArrayList<>() : rollsResult); List<Integer> naturalRolls = (rollsResult == null ? new ArrayList<>() : rollsResult);

View File

@@ -41,7 +41,6 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
tapper = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Tapper"), sa).getFirst(); tapper = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Tapper"), sa).getFirst();
} }
PlayerController pc = tapper.getController(); PlayerController pc = tapper.getController();
boolean toggle = sa.hasParam("Toggle");
CardCollection tapped = new CardCollection(); CardCollection tapped = new CardCollection();
final Map<Player, CardCollection> untapMap = Maps.newHashMap(); final Map<Player, CardCollection> untapMap = Maps.newHashMap();
@@ -62,12 +61,8 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
continue; continue;
} }
// If the effected card is controlled by the same controller of the SA, default to untap. // If the effected card is controlled by the same controller of the SA, default to untap.
boolean tap; boolean tap = pc.chooseBinary(sa, Localizer.getInstance().getMessage("lblTapOrUntapTarget", CardTranslation.getTranslatedName(gameCard.getName())), PlayerController.BinaryChoiceType.TapOrUntap,
if(!toggle)
tap = pc.chooseBinary(sa, Localizer.getInstance().getMessage("lblTapOrUntapTarget", CardTranslation.getTranslatedName(gameCard.getName())), PlayerController.BinaryChoiceType.TapOrUntap,
!gameCard.getController().equals(tapper)); !gameCard.getController().equals(tapper));
else
tap = !gameCard.isTapped();
if (tap) { if (tap) {
if (gameCard.tap(true, sa, tapper)) tapped.add(gameCard); if (gameCard.tap(true, sa, tapper)) tapped.add(gameCard);
} else if (gameCard.untap()) { } else if (gameCard.untap()) {

View File

@@ -11,7 +11,6 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerController; import forge.game.player.PlayerController;
@@ -38,7 +37,7 @@ public class TimeTravelEffect extends SpellAbilityEffect {
PlayerController pc = activator.getController(); PlayerController pc = activator.getController();
final CounterType counterType = CounterEnumType.TIME; final CounterEnumType counterType = CounterEnumType.TIME;
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
FCollection<Card> list = new FCollection<>(); FCollection<Card> list = new FCollection<>();

View File

@@ -203,7 +203,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
private boolean unearthed; private boolean unearthed;
private boolean ringbearer; private boolean ringbearer;
private boolean monstrous; private boolean monstrous;
private boolean harnessed;
private boolean renowned; private boolean renowned;
private boolean solved; private boolean solved;
private boolean tributed; private boolean tributed;
@@ -1052,7 +1051,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
// it was always created), adjust threshold based on its existence. // it was always created), adjust threshold based on its existence.
int threshold = states.containsKey(CardStateName.FaceDown) ? 2 : 1; int threshold = states.containsKey(CardStateName.FaceDown) ? 2 : 1;
int numStates = states.size(); int numStates = states.keySet().size();
return numStates > threshold; return numStates > threshold;
} }
@@ -2463,8 +2462,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
} }
} }
sbLong.append("Enchant ").append(desc).append("\r\n"); sbLong.append("Enchant ").append(desc).append("\r\n");
} else if (keyword.startsWith("Ripple")) {
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") } else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")
|| keyword.startsWith("Disguise") || keyword.startsWith("Reflect") || keyword.startsWith("Disguise")
|| keyword.startsWith("Escape") || keyword.startsWith("Foretell:") || keyword.startsWith("Escape") || keyword.startsWith("Foretell:")
|| keyword.startsWith("Madness:")|| keyword.startsWith("Recover") || keyword.startsWith("Madness:")|| keyword.startsWith("Recover")
|| keyword.startsWith("Reconfigure") || keyword.startsWith("Squad") || keyword.startsWith("Reconfigure") || keyword.startsWith("Squad")
@@ -2503,15 +2504,17 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
sbLong.append("."); sbLong.append(".");
} }
if (k.length > 3) { if (k.length > 3) {
sbLong.append(". ").append(k[3]); sbLong.append(". " + k[3]);
} }
} }
sbLong.append(" (").append(inst.getReminderText()).append(")"); sbLong.append(" (").append(inst.getReminderText()).append(")");
sbLong.append("\r\n"); sbLong.append("\r\n");
} else if (keyword.equals("Mayhem")) { }
} else if (keyword.startsWith("Reflect")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
sbLong.append(" (").append(inst.getReminderText()).append(")"); sbLong.append(" (").append(inst.getReminderText()).append(")");
sbLong.append("\r\n"); sbLong.append("\r\n");
}
} else if (keyword.startsWith("Echo")) { } else if (keyword.startsWith("Echo")) {
sbLong.append("Echo "); sbLong.append("Echo ");
final String[] upkeepCostParams = keyword.split(":"); final String[] upkeepCostParams = keyword.split(":");
@@ -2650,7 +2653,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing:") || keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing:")
|| keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic") || keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic")
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage") || keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Ripple")) { || keyword.startsWith("Renown") || keyword.startsWith("Annihilator")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")"); sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")");
} else if (keyword.startsWith("Crew")) { } else if (keyword.startsWith("Crew")) {
@@ -2759,7 +2762,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling") || keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon") || keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon")
|| keyword.startsWith("Class") || keyword.startsWith("Blitz") || keyword.startsWith("Web-slinging") || keyword.startsWith("Class") || keyword.startsWith("Blitz") || keyword.startsWith("Web-slinging")
|| keyword.startsWith("Specialize") || keyword.equals("Ravenous") || keyword.startsWith("Firebending") || keyword.startsWith("Specialize") || keyword.equals("Ravenous")
|| keyword.equals("For Mirrodin") || keyword.equals("Job select") || keyword.startsWith("Craft") || keyword.equals("For Mirrodin") || keyword.equals("Job select") || keyword.startsWith("Craft")
|| keyword.startsWith("Landwalk") || keyword.startsWith("Visit") || keyword.startsWith("Mobilize") || keyword.startsWith("Landwalk") || keyword.startsWith("Visit") || keyword.startsWith("Mobilize")
|| keyword.startsWith("Station") || keyword.startsWith("Warp") || keyword.startsWith("Devour")) { || keyword.startsWith("Station") || keyword.startsWith("Warp") || keyword.startsWith("Devour")) {
@@ -2966,9 +2969,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (monstrous) { if (monstrous) {
sb.append("Monstrous\r\n"); sb.append("Monstrous\r\n");
} }
if (harnessed) {
sb.append("Harnessed\r\n");
}
if (renowned) { if (renowned) {
sb.append("Renowned\r\n"); sb.append("Renowned\r\n");
} }
@@ -3219,10 +3219,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", getEffectSource().getName()); desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", getEffectSource().getName());
} }
// Ensure no more escaped linebreak are present
desc = desc.replace("\\r", "\r")
.replace("\\n", "\n");
return desc.trim(); return desc.trim();
} }
@@ -3561,23 +3557,14 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (!getStaticAbilities().isEmpty()) { if (!getStaticAbilities().isEmpty()) {
return false; return false;
} }
if (!getReplacementEffects().isEmpty() if (!getReplacementEffects().isEmpty()) {
&& (getReplacementEffects().size() > 1 || !isSaga() || hasKeyword(Keyword.READ_AHEAD))) {
return false; return false;
} }
if (!getTriggers().isEmpty()) { if (!getTriggers().isEmpty()) {
return false; return false;
} }
for (SpellAbility sa : getSpellAbilities()) { for (SpellAbility sa : getSpellAbilities()) {
// morph up and disguise up are not part of the card if (!(sa instanceof SpellPermanent && sa.isBasicSpell()) && !sa.isMorphUp() && !sa.isDisguiseUp()) {
if (sa.isMorphUp() || sa.isDisguiseUp()) {
continue;
}
// while Adventure and Omen are part of Secondary
if ((sa.isAdventure() || sa.isOmen()) && !getCurrentStateName().equals(sa.getCardState())) {
continue;
}
if (!(sa instanceof SpellPermanent && sa.isBasicSpell())) {
return false; return false;
} }
} }
@@ -3614,14 +3601,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
} }
} }
} }
} else if (hasState(CardStateName.Secondary) && state.getStateName() == CardStateName.Original) { } else {
// Adventure and Omen may only be cast not from Battlefield // Adventure and Omen may only be cast not from Battlefield
if (hasState(CardStateName.Secondary) && state.getStateName() == CardStateName.Original) {
for (SpellAbility sa : getState(CardStateName.Secondary).getSpellAbilities()) { for (SpellAbility sa : getState(CardStateName.Secondary).getSpellAbilities()) {
if (mana == null || mana == sa.isManaAbility()) { if (mana == null || mana == sa.isManaAbility()) {
list.add(sa); list.add(sa);
} }
} }
} }
}
// keywords should already been cleanup by layers // keywords should already been cleanup by layers
for (KeywordInterface kw : getUnhiddenKeywords(state)) { for (KeywordInterface kw : getUnhiddenKeywords(state)) {
@@ -6442,10 +6431,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
DamageType damageType = DamageType.Normal; DamageType damageType = DamageType.Normal;
if (isPlaneswalker()) { // 120.3c if (isPlaneswalker()) { // 120.3c
subtractCounter(CounterEnumType.LOYALTY, damageIn, null, true); subtractCounter(CounterType.get(CounterEnumType.LOYALTY), damageIn, null, true);
} }
if (isBattle()) { if (isBattle()) {
subtractCounter(CounterEnumType.DEFENSE, damageIn, null, true); subtractCounter(CounterType.get(CounterEnumType.DEFENSE), damageIn, null, true);
} }
if (isCreature()) { if (isCreature()) {
if (source.isWitherDamage()) { // 120.3d if (source.isWitherDamage()) { // 120.3d
@@ -6693,14 +6682,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
setRingBearer(false); setRingBearer(false);
} }
public final boolean isHarnessed() {
return harnessed;
}
public final boolean setHarnessed(final boolean harnessed0) {
harnessed = harnessed0;
return true;
}
public final boolean isMonstrous() { public final boolean isMonstrous() {
return monstrous; return monstrous;
} }
@@ -6858,10 +6839,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return exiledSA.isKeyword(Keyword.WARP); return exiledSA.isKeyword(Keyword.WARP);
} }
public boolean isWebSlinged() {
return getCastSA() != null & getCastSA().isAlternativeCost(AlternativeCost.WebSlinging);
}
public boolean isSpecialized() { public boolean isSpecialized() {
return specialized; return specialized;
} }
@@ -7157,7 +7134,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return false; return false;
} }
if (StaticAbilityCantTarget.cantTarget(this, sa) != null) { if (StaticAbilityCantTarget.cantTarget(this, sa)) {
return false; return false;
} }

View File

@@ -198,9 +198,7 @@ public class CardFactory {
if (c.hasAlternateState()) { if (c.hasAlternateState()) {
if (c.isFlipCard()) { if (c.isFlipCard()) {
c.setState(CardStateName.Flipped, false); c.setState(CardStateName.Flipped, false);
// set the imagekey altstate to false since the rotated image is handled by graphics renderer c.setImageKey(cp.getImageKey(true));
// setting this to true will download the original image with different name.
c.setImageKey(cp.getImageKey(false));
} }
else if (c.isDoubleFaced() && cardRules != null) { else if (c.isDoubleFaced() && cardRules != null) {
c.setState(cardRules.getSplitType().getChangedStateName(), false); c.setState(cardRules.getSplitType().getChangedStateName(), false);
@@ -376,28 +374,22 @@ public class CardFactory {
} }
} }
// Negative card Id's are for view purposes only
if (c.getId() >= 0) {
// Build English oracle and translated oracle mapping // Build English oracle and translated oracle mapping
if (c.getId() >= 0) {
CardTranslation.buildOracleMapping(face.getName(), face.getOracleText(), variantName); CardTranslation.buildOracleMapping(face.getName(), face.getOracleText(), variantName);
} }
// Set name for Sentry reports to be identifiable // Name first so Senty has the Card name
c.setName(face.getName()); c.setName(face.getName());
if (c.getId() >= 0) { // Set Triggers & Abilities if not for view for (Entry<String, String> v : face.getVariables()) c.setSVar(v.getKey(), v.getValue());
for (Entry<String, String> v : face.getVariables())
c.setSVar(v.getKey(), v.getValue()); for (String r : face.getReplacements()) c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState()));
for (String r : face.getReplacements()) for (String s : face.getStaticAbilities()) c.addStaticAbility(s);
c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState())); for (String t : face.getTriggers()) c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState()));
for (String s : face.getStaticAbilities())
c.addStaticAbility(s);
for (String t : face.getTriggers())
c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState()));
// keywords not before variables // keywords not before variables
c.addIntrinsicKeywords(face.getKeywords(), false); c.addIntrinsicKeywords(face.getKeywords(), false);
}
if (face.getDraftActions() != null) { if (face.getDraftActions() != null) {
face.getDraftActions().forEach(c::addDraftAction); face.getDraftActions().forEach(c::addDraftAction);
} }
@@ -426,7 +418,6 @@ public class CardFactory {
c.setAttractionLights(face.getAttractionLights()); c.setAttractionLights(face.getAttractionLights());
if (c.getId() > 0) // Set FactoryAbilities if not for view
CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities()); CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities());
} }

View File

@@ -1170,29 +1170,6 @@ public class CardFactoryUtil {
removeCounterSA.setIntrinsic(intrinsic); removeCounterSA.setIntrinsic(intrinsic);
trigger.setOverridingAbility(removeCounterSA); trigger.setOverridingAbility(removeCounterSA);
inst.addTrigger(trigger);
} else if (keyword.startsWith("Firebending")) {
final String[] k = keyword.split(":");
final String n = k[1];
StringBuilder desc = new StringBuilder("Firebending ");
desc.append(n);
if (k.length > 2) {
desc.append(" ").append(k[2]);
}
desc.append(" (").append(inst.getReminderText()).append(")");
final String trigStr = "Mode$ Attacks | ValidCard$ Card.Self | TriggerDescription$ " + desc.toString();
final String manaStr = "DB$ Mana | Defined$ You | CombatMana$ True | Produced$ R | Amount$ " + n;
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
SpellAbility manaSA = AbilityFactory.getAbility(manaStr, card);
manaSA.setIntrinsic(intrinsic);
trigger.setOverridingAbility(manaSA);
inst.addTrigger(trigger); inst.addTrigger(trigger);
} else if (keyword.equals("Flanking")) { } else if (keyword.equals("Flanking")) {
final StringBuilder trigFlanking = new StringBuilder( final StringBuilder trigFlanking = new StringBuilder(
@@ -2568,7 +2545,7 @@ public class CardFactoryUtil {
} else if (keyword.equals("Sunburst")) { } else if (keyword.equals("Sunburst")) {
// Rule 702.43a If this object is entering the battlefield as a creature, // Rule 702.43a If this object is entering the battlefield as a creature,
// ignoring any type-changing effects that would affect it // ignoring any type-changing effects that would affect it
CounterType t = host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE; CounterType t = CounterType.get(host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE);
StringBuilder sb = new StringBuilder("etbCounter:"); StringBuilder sb = new StringBuilder("etbCounter:");
sb.append(t).append(":Sunburst:no Condition:"); sb.append(t).append(":Sunburst:no Condition:");
@@ -2776,9 +2753,10 @@ public class CardFactoryUtil {
final String cost = params[1]; final String cost = params[1];
final StringBuilder sbAttach = new StringBuilder(); final StringBuilder sbAttach = new StringBuilder();
sbAttach.append("SP$ Attach | ValidTgts$ Creature | Cost$ "); sbAttach.append("SP$ Attach | Cost$ ");
sbAttach.append(cost); sbAttach.append(cost);
sbAttach.append(" | AILogic$ ").append(params.length > 2 ? params[2] : "Pump"); sbAttach.append(" | AILogic$ ").append(params.length > 2 ? params[2] : "Pump");
sbAttach.append(" | Bestow$ True | ValidTgts$ Creature");
final SpellAbility sa = AbilityFactory.getAbility(sbAttach.toString(), card); final SpellAbility sa = AbilityFactory.getAbility(sbAttach.toString(), card);
final StringBuilder sbDesc = new StringBuilder(); final StringBuilder sbDesc = new StringBuilder();
@@ -4112,7 +4090,7 @@ public class CardFactoryUtil {
sbValid.append("| ").append(param).append(k[1]); sbValid.append("| ").append(param).append(k[1]);
} }
String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True" String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True"
+ sbValid.toString() + " | Activator$ Opponent | Description$ " + sbValid.toString() + " | Activator$ Opponent | Description$ "
+ sbDesc.toString() + " (" + inst.getReminderText() + ")"; + sbDesc.toString() + " (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
@@ -4153,7 +4131,7 @@ public class CardFactoryUtil {
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
// Target // Target
effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True "; effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Card.Self | Secondary$ True ";
if (!valid.isEmpty()) { if (!valid.isEmpty()) {
effect += "| ValidSource$ " + valid; effect += "| ValidSource$ " + valid;
} }
@@ -4161,7 +4139,7 @@ public class CardFactoryUtil {
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
// Attach // Attach
effect = "Mode$ CantAttach | Target$ Card.Self | Secondary$ True "; effect = "Mode$ CantAttach | Protection$ True | Target$ Card.Self | Secondary$ True ";
if (!valid.isEmpty()) { if (!valid.isEmpty()) {
effect += "| ValidCard$ " + valid; effect += "| ValidCard$ " + valid;
} }
@@ -4182,7 +4160,7 @@ public class CardFactoryUtil {
" | Description$ Chapter abilities of this Saga can't trigger the turn it entered the battlefield unless it has exactly the number of lore counters on it specified in the chapter symbol of that ability."; " | Description$ Chapter abilities of this Saga can't trigger the turn it entered the battlefield unless it has exactly the number of lore counters on it specified in the chapter symbol of that ability.";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Shroud")) { } else if (keyword.equals("Shroud")) {
String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True" String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True"
+ " | Description$ Shroud (" + inst.getReminderText() + ")"; + " | Description$ Shroud (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Skulk")) { } else if (keyword.equals("Skulk")) {

View File

@@ -213,10 +213,16 @@ public final class CardPredicates {
public static Predicate<Card> hasCounter(final CounterType type) { public static Predicate<Card> hasCounter(final CounterType type) {
return hasCounter(type, 1); return hasCounter(type, 1);
} }
public static Predicate<Card> hasCounter(final CounterEnumType type) {
return hasCounter(type, 1);
}
public static Predicate<Card> hasCounter(final CounterType type, final int n) { public static Predicate<Card> hasCounter(final CounterType type, final int n) {
return c -> c.getCounters(type) >= n; return c -> c.getCounters(type) >= n;
} }
public static Predicate<Card> hasCounter(final CounterEnumType type, final int n) {
return hasCounter(CounterType.get(type), n);
}
public static Predicate<Card> hasLessCounter(final CounterType type, final int n) { public static Predicate<Card> hasLessCounter(final CounterType type, final int n) {
return c -> { return c -> {
@@ -224,10 +230,16 @@ public final class CardPredicates {
return x > 0 && x <= n; return x > 0 && x <= n;
}; };
} }
public static Predicate<Card> hasLessCounter(final CounterEnumType type, final int n) {
return hasLessCounter(CounterType.get(type), n);
}
public static Predicate<Card> canReceiveCounters(final CounterType counter) { public static Predicate<Card> canReceiveCounters(final CounterType counter) {
return c -> c.canReceiveCounters(counter); return c -> c.canReceiveCounters(counter);
} }
public static Predicate<Card> canReceiveCounters(final CounterEnumType counter) {
return canReceiveCounters(CounterType.get(counter));
}
public static Predicate<Card> hasGreaterPowerThan(final int minPower) { public static Predicate<Card> hasGreaterPowerThan(final int minPower) {
return c -> c.getNetPower() > minPower; return c -> c.getNetPower() > minPower;
@@ -236,6 +248,9 @@ public final class CardPredicates {
public static Comparator<Card> compareByCounterType(final CounterType type) { public static Comparator<Card> compareByCounterType(final CounterType type) {
return Comparator.comparingInt(arg0 -> arg0.getCounters(type)); return Comparator.comparingInt(arg0 -> arg0.getCounters(type));
} }
public static Comparator<Card> compareByCounterType(final CounterEnumType type) {
return compareByCounterType(CounterType.get(type));
}
public static Predicate<Card> hasSVar(final String name) { public static Predicate<Card> hasSVar(final String name) {
return c -> c.hasSVar(name); return c -> c.hasSVar(name);

View File

@@ -1244,8 +1244,7 @@ public class CardProperty {
if (property.contains("ControlledBy")) { if (property.contains("ControlledBy")) {
FCollectionView<Player> p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], spellAbility); FCollectionView<Player> p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], spellAbility);
cards = CardLists.filterControlledBy(cards, p); cards = CardLists.filterControlledBy(cards, p);
// Kraven the Hunter LTB trigger if (!cards.contains(card)) {
if (!card.isLKI() && !cards.contains(card)) {
return false; return false;
} }
} }
@@ -1820,10 +1819,6 @@ public class CardProperty {
if (!card.isWarped()) { if (!card.isWarped()) {
return false; return false;
} }
} else if (property.equals("webSlinged")) {
if (!card.isWebSlinged()) {
return false;
}
} else if (property.equals("CrewedThisTurn")) { } else if (property.equals("CrewedThisTurn")) {
if (!hasTimestampMatch(card, source.getCrewedByThisTurn())) return false; if (!hasTimestampMatch(card, source.getCrewedByThisTurn())) return false;
} else if (property.equals("CrewedBySourceThisTurn")) { } else if (property.equals("CrewedBySourceThisTurn")) {
@@ -1832,10 +1827,6 @@ public class CardProperty {
if (card.getDevouredCards().isEmpty()) { if (card.getDevouredCards().isEmpty()) {
return false; return false;
} }
} else if (property.equals("harnessed")) {
if (!card.isHarnessed()) {
return false;
}
} else if (property.equals("IsMonstrous")) { } else if (property.equals("IsMonstrous")) {
if (!card.isMonstrous()) { if (!card.isMonstrous()) {
return false; return false;

View File

@@ -468,9 +468,6 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
return Iterables.getFirst(getIntrinsicSpellAbilities(), null); return Iterables.getFirst(getIntrinsicSpellAbilities(), null);
} }
public final SpellAbility getFirstSpellAbility() { public final SpellAbility getFirstSpellAbility() {
if (this.card.getCastSA() != null) {
return this.card.getCastSA();
}
return Iterables.getFirst(getNonManaAbilities(), null); return Iterables.getFirst(getNonManaAbilities(), null);
} }
@@ -608,18 +605,18 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
result.add(loyaltyRep); result.add(loyaltyRep);
} }
if (type.isBattle()) { if (type.isBattle()) {
// TODO This is currently breaking for Battle/Defense
// Going to script the cards to work but ideally it would happen here
if (defenseRep == null) { if (defenseRep == null) {
defenseRep = CardFactoryUtil.makeEtbCounter("etbCounter:DEFENSE:" + this.baseDefense, this, true); defenseRep = CardFactoryUtil.makeEtbCounter("etbCounter:DEFENSE:" + this.baseDefense, this, true);
} }
result.add(defenseRep); result.add(defenseRep);
// TODO add Siege "Choose a player to protect it"
} }
card.updateReplacementEffects(result, this);
// below are global rules
if (type.hasSubtype("Saga") && !hasKeyword(Keyword.READ_AHEAD)) { if (type.hasSubtype("Saga") && !hasKeyword(Keyword.READ_AHEAD)) {
if (sagaRep == null) { if (sagaRep == null) {
sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, false); sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, true);
} }
result.add(sagaRep); result.add(sagaRep);
} }
@@ -636,6 +633,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
result.add(omenRep); result.add(omenRep);
} }
card.updateReplacementEffects(result, this);
return result; return result;
} }
public boolean addReplacementEffect(final ReplacementEffect replacementEffect) { public boolean addReplacementEffect(final ReplacementEffect replacementEffect) {
@@ -752,21 +750,11 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
triggers.add(tr.copy(card, lki)); triggers.add(tr.copy(card, lki));
} }
} }
ReplacementEffect runRE = null;
if (ctb instanceof SpellAbility sp && sp.isReplacementAbility()
&& source.getCard().equals(ctb.getHostCard())) {
runRE = sp.getReplacementEffect();
}
replacementEffects.clear(); replacementEffects.clear();
for (ReplacementEffect re : source.replacementEffects) { for (ReplacementEffect re : source.replacementEffects) {
if (re.isIntrinsic()) { if (re.isIntrinsic()) {
ReplacementEffect reCopy = re.copy(card, lki); replacementEffects.add(re.copy(card, lki));
if (re.equals(runRE) && runRE.hasRun()) {
// CR 208.2b prevent loop from card copying itself
reCopy.setHasRun(true);
}
replacementEffects.add(reCopy);
} }
} }

View File

@@ -60,12 +60,13 @@ public class CardView extends GameEntityView {
} }
public static TrackableCollection<CardView> getCollection(Iterable<Card> cards) { public static TrackableCollection<CardView> getCollection(Iterable<Card> cards) {
TrackableCollection<CardView> collection = new TrackableCollection<>(); if (cards == null) {
if (cards != null) { return null;
for (Card c : cards) {
if (c != null && c.getRenderForUI()) { //only add cards that match their card for UI
collection.add(c.getView());
} }
TrackableCollection<CardView> collection = new TrackableCollection<>();
for (Card c : cards) {
if (c.getRenderForUI()) { //only add cards that match their card for UI
collection.add(c.getView());
} }
} }
return collection; return collection;
@@ -141,8 +142,9 @@ public class CardView extends GameEntityView {
} }
public boolean isFlipped() { public boolean isFlipped() {
return get(TrackableProperty.Flipped); return get(TrackableProperty.Flipped); // getCurrentState().getState() == CardStateName.Flipped;
} }
public boolean isSplitCard() { public boolean isSplitCard() {
return get(TrackableProperty.SplitCard); return get(TrackableProperty.SplitCard);
} }
@@ -937,9 +939,7 @@ public class CardView extends GameEntityView {
sb.append("\r\n\r\nMerged Cards: ").append(mergedCards); sb.append("\r\n\r\nMerged Cards: ").append(mergedCards);
} }
return sb.toString().trim() return sb.toString().trim();
.replace("\\r", "\r")
.replace("\\n", "\n");
} }
public CardStateView getCurrentState() { public CardStateView getCurrentState() {
@@ -1027,7 +1027,6 @@ public class CardView extends GameEntityView {
set(TrackableProperty.Cloned, c.isCloned()); set(TrackableProperty.Cloned, c.isCloned());
set(TrackableProperty.SplitCard, isSplitCard); set(TrackableProperty.SplitCard, isSplitCard);
set(TrackableProperty.FlipCard, c.isFlipCard()); set(TrackableProperty.FlipCard, c.isFlipCard());
set(TrackableProperty.Flipped, c.getCurrentStateName() == CardStateName.Flipped);
set(TrackableProperty.Facedown, c.isFaceDown()); set(TrackableProperty.Facedown, c.isFaceDown());
set(TrackableProperty.Foretold, c.isForetold()); set(TrackableProperty.Foretold, c.isForetold());
set(TrackableProperty.Secondary, c.hasState(CardStateName.Secondary)); set(TrackableProperty.Secondary, c.hasState(CardStateName.Secondary));
@@ -1105,7 +1104,7 @@ public class CardView extends GameEntityView {
currentState.getView().setOriginalColors(c); //set original Colors currentState.getView().setOriginalColors(c); //set original Colors
currentStateView.updateAttractionLights(currentState); currentStateView.updateAttractionLights(currentState);
currentStateView.updateHasPrintedPT((currentStateView.isVehicle() || currentStateView.isSpaceCraft()) && c.getRules() != null && c.getRules().hasPrintedPT()); currentStateView.updateHasPrintedPT(c.getRules() != null && c.getRules().hasPrintedPT());
CardState alternateState = isSplitCard && isFaceDown() ? c.getState(CardStateName.RightSplit) : c.getAlternateState(); CardState alternateState = isSplitCard && isFaceDown() ? c.getState(CardStateName.RightSplit) : c.getAlternateState();

View File

@@ -28,7 +28,7 @@ import java.util.Locale;
* @author Clemens Koza * @author Clemens Koza
* @version V0.0 17.02.2010 * @version V0.0 17.02.2010
*/ */
public enum CounterEnumType implements CounterType { public enum CounterEnumType {
M1M1("-1/-1", "-1/-1", 255, 110, 106), M1M1("-1/-1", "-1/-1", 255, 110, 106),
P1P1("+1/+1", "+1/+1", 96, 226, 23), P1P1("+1/+1", "+1/+1", 96, 226, 23),
@@ -167,12 +167,8 @@ public enum CounterEnumType implements CounterType {
FILIBUSTER("FLBTR", 255, 179, 119), FILIBUSTER("FLBTR", 255, 179, 119),
FILM("FILM", 255, 255, 255),
FINALITY("FINAL", 255, 255, 255), FINALITY("FINAL", 255, 255, 255),
FIRE("FIRE", 240, 30, 35),
FLAME("FLAME", 255, 143, 43), FLAME("FLAME", 255, 143, 43),
FLAVOR("FLAVOR", 208, 152, 97), ///adventure only FLAVOR("FLAVOR", 208, 152, 97), ///adventure only
@@ -557,14 +553,4 @@ public enum CounterEnumType implements CounterType {
public static final ImmutableList<CounterEnumType> values = ImmutableList.copyOf(values()); public static final ImmutableList<CounterEnumType> values = ImmutableList.copyOf(values());
@Override
public boolean is(CounterEnumType eType) {
return this == eType;
}
@Override
public boolean isKeywordCounter() {
return false;
}
} }

View File

@@ -1,74 +0,0 @@
package forge.game.card;
import java.util.Map;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
public record CounterKeywordType(String keyword) implements CounterType {
// Rule 122.1b
static ImmutableList<String> keywordCounter = ImmutableList.of(
"Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof",
"Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance");
private static Map<String, CounterKeywordType> sMap = Maps.newHashMap();
public static CounterKeywordType get(String s) {
if (!sMap.containsKey(s)) {
sMap.put(s, new CounterKeywordType(s));
}
return sMap.get(s);
}
@Override
public String toString() {
return keyword;
}
public String getName() {
return getKeywordDescription();
}
public String getCounterOnCardDisplayName() {
return getKeywordDescription();
}
private String getKeywordDescription() {
if (keyword.startsWith("Hexproof:")) {
final String[] k = keyword.split(":");
return "Hexproof from " + k[2];
}
if (keyword.startsWith("Trample:")) {
return "Trample over Planeswalkers";
}
return keyword;
}
public boolean is(CounterEnumType eType) {
return false;
}
public boolean isKeywordCounter() {
if (keyword.startsWith("Hexproof:")) {
return true;
}
if (keyword.startsWith("Trample:")) {
return true;
}
return keywordCounter.contains(keyword);
}
public int getRed() {
return 255;
}
public int getGreen() {
return 255;
}
public int getBlue() {
return 255;
}
}

View File

@@ -1,31 +1,141 @@
package forge.game.card; package forge.game.card;
import java.io.Serializable; import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import org.apache.commons.lang3.builder.EqualsBuilder;
public interface CounterType extends Serializable { import java.io.Serializable;
import java.util.Map;
import java.util.Objects;
public class CounterType implements Comparable<CounterType>, Serializable {
private static final long serialVersionUID = -7575835723159144478L;
private CounterEnumType eVal = null;
private String sVal = null;
// Rule 122.1b
static ImmutableList<String> keywordCounter = ImmutableList.of(
"Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof",
"Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance");
private static Map<CounterEnumType, CounterType> eMap = Maps.newEnumMap(CounterEnumType.class);
private static Map<String, CounterType> sMap = Maps.newHashMap();
private CounterType(CounterEnumType e, String s) {
this.eVal = e;
this.sVal = s;
}
public static CounterType get(CounterEnumType e) {
if (!eMap.containsKey(e)) {
eMap.put(e, new CounterType(e, null));
}
return eMap.get(e);
}
public static CounterType get(String s) {
if (!sMap.containsKey(s)) {
sMap.put(s, new CounterType(null, s));
}
return sMap.get(s);
}
public static CounterType getType(String name) { public static CounterType getType(String name) {
if ("Any".equalsIgnoreCase(name)) { if ("Any".equalsIgnoreCase(name)) {
return null; return null;
} }
try { try {
return CounterEnumType.getType(name); return get(CounterEnumType.getType(name));
} catch (final IllegalArgumentException ex) { } catch (final IllegalArgumentException ex) {
return CounterKeywordType.get(name); return get(name);
} }
} }
public String getName();
public String getCounterOnCardDisplayName(); @Override
public int hashCode() {
public boolean is(CounterEnumType eType); return Objects.hash(eVal, sVal);
}
public boolean isKeywordCounter();
@Override
public int getRed(); public boolean equals(Object obj) {
if (obj == null) {
public int getGreen(); return false;
}
public int getBlue(); if (this == obj) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
CounterType rhs = (CounterType) obj;
return new EqualsBuilder()
.append(eVal, rhs.eVal)
.append(sVal, rhs.sVal)
.isEquals();
}
@Override
public String toString() {
return eVal != null ? eVal.toString() : sVal;
}
public String getName() {
return eVal != null ? eVal.getName() : getKeywordDescription();
}
public String getCounterOnCardDisplayName() {
return eVal != null ? eVal.getCounterOnCardDisplayName() : getKeywordDescription();
}
private String getKeywordDescription() {
if (sVal.startsWith("Hexproof:")) {
final String[] k = sVal.split(":");
return "Hexproof from " + k[2];
}
if (sVal.startsWith("Trample:")) {
return "Trample over Planeswalkers";
}
return sVal;
}
@Override
public int compareTo(CounterType o) {
return ComparisonChain.start()
.compare(eVal, o.eVal, Ordering.natural().nullsLast())
.compare(sVal, o.sVal, Ordering.natural().nullsLast())
.result();
}
public boolean is(CounterEnumType eType) {
return eVal == eType;
}
public boolean isKeywordCounter() {
if (eVal != null) {
return false;
}
if (sVal.startsWith("Hexproof:")) {
return true;
}
if (sVal.startsWith("Trample:")) {
return true;
}
return keywordCounter.contains(sVal);
}
public int getRed() {
return eVal != null ? eVal.getRed() : 255;
}
public int getGreen() {
return eVal != null ? eVal.getGreen() : 255;
}
public int getBlue() {
return eVal != null ? eVal.getBlue() : 255;
}
} }

View File

@@ -1,26 +0,0 @@
package forge.game.card.perpetual;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.cost.Cost;
public record PerpetualManaCost(long timestamp, ManaCost manaCost) implements PerpetualInterface {
@Override
public long getTimestamp() {
return timestamp;
}
@Override
public void applyEffect(Card c) {
c.addChangedManaCost(manaCost, timestamp, (long) 0);
c.updateManaCostForView();
if (c.getFirstSpellAbility() != null) {
Cost cost = c.getFirstSpellAbility().getPayCosts().copyWithDefinedMana(manaCost);
c.getFirstSpellAbility().setPayCosts(cost);
}
}
}

View File

@@ -1,5 +1,6 @@
package forge.game.combat; package forge.game.combat;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -38,13 +39,11 @@ public class AttackRequirement {
//MustAttack static check //MustAttack static check
final List<GameEntity> mustAttack = StaticAbilityMustAttack.entitiesMustAttack(attacker); final List<GameEntity> mustAttack = StaticAbilityMustAttack.entitiesMustAttack(attacker);
nAttackAnything += Collections.frequency(mustAttack, attacker);
for (GameEntity e : mustAttack) { for (GameEntity e : mustAttack) {
if (e.equals(attacker)) { if (e.equals(attacker)) continue;
nAttackAnything++;
} else {
defenderSpecific.add(e); defenderSpecific.add(e);
} }
}
for (final GameEntity defender : possibleDefenders) { for (final GameEntity defender : possibleDefenders) {
// use put here because we want to always put it, even if the value is 0 // use put here because we want to always put it, even if the value is 0

View File

@@ -225,7 +225,7 @@ public class CombatUtil {
if (!ge.equals(defender) && ge instanceof Player) { if (!ge.equals(defender) && ge instanceof Player) {
// found a player which does not goad that creature // found a player which does not goad that creature
// and creature can attack this player or planeswalker // and creature can attack this player or planeswalker
if (!attacker.isGoadedBy((Player) ge) && canAttack(attacker, ge)) { if (!attacker.isGoadedBy((Player) ge) && !ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) {
return false; return false;
} }
} }
@@ -233,6 +233,17 @@ public class CombatUtil {
} }
} }
// Quasi-goad logic for "Kardur, Doomscourge" etc. that isn't goad but behaves the same
if (defender != null && defender.hasKeyword("Creatures your opponents control attack a player other than you if able.")) {
for (GameEntity ge : getAllPossibleDefenders(attacker.getController())) {
if (!ge.equals(defender) && ge instanceof Player) {
if (!ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) {
return false;
}
}
}
}
// CantAttack static abilities // CantAttack static abilities
if (StaticAbilityCantAttackBlock.cantAttack(attacker, defender)) { if (StaticAbilityCantAttackBlock.cantAttack(attacker, defender)) {
return false; return false;

View File

@@ -621,11 +621,8 @@ public class Cost implements Serializable {
} }
public final Cost copyWithDefinedMana(String manaCost) { public final Cost copyWithDefinedMana(String manaCost) {
return copyWithDefinedMana(new ManaCost(new ManaCostParser(manaCost)));
}
public final Cost copyWithDefinedMana(ManaCost manaCost) {
Cost toRet = copyWithNoMana(); Cost toRet = copyWithNoMana();
toRet.costParts.add(new CostPartMana(manaCost, null)); toRet.costParts.add(new CostPartMana(new ManaCost(new ManaCostParser(manaCost)), null));
toRet.cacheTapCost(); toRet.cacheTapCost();
return toRet; return toRet;
} }
@@ -997,9 +994,9 @@ public class Cost implements Serializable {
Integer counters = otherAmount - part.convertAmount(); Integer counters = otherAmount - part.convertAmount();
// the cost can turn positive if multiple Carth raise it // the cost can turn positive if multiple Carth raise it
if (counters < 0) { if (counters < 0) {
costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterEnumType.LOYALTY, part.getType(), part.getTypeDescription())); costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription()));
} else { } else {
costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterEnumType.LOYALTY, part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false)); costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false));
} }
} else { } else {
continue; continue;

View File

@@ -22,6 +22,7 @@ import forge.game.ability.AbilityKey;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -80,7 +81,7 @@ public class CostUntap extends CostPart {
public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) {
final Card source = ability.getHostCard(); final Card source = ability.getHostCard();
return source.isTapped() && !source.isAbilitySick() && return source.isTapped() && !source.isAbilitySick() &&
(source.getCounters(CounterEnumType.STUN) == 0 || source.canRemoveCounters(CounterEnumType.STUN)); (source.getCounters(CounterEnumType.STUN) == 0 || source.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
} }
@Override @Override

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