mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 19:28:01 +00:00
Compare commits
6 Commits
migrate-sp
...
detainEffe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32b84b40c | ||
|
|
6962c2cf99 | ||
|
|
b7ec60863a | ||
|
|
239cf3ece7 | ||
|
|
06781fb6ff | ||
|
|
a85f00043b |
2
.github/workflows/test-build.yaml
vendored
2
.github/workflows/test-build.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: ['17', '21']
|
||||
java: [ '17' ]
|
||||
name: Test with Java ${{ matrix.Java }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -66,9 +66,6 @@ forge-gui-mobile-dev/testAssets
|
||||
|
||||
forge-gui/res/cardsfolder/*.bat
|
||||
|
||||
# Generated changelog file
|
||||
forge-gui/release-files/CHANGES.txt
|
||||
|
||||
forge-gui/res/PerSetTrackingResults
|
||||
forge-gui/res/decks
|
||||
forge-gui/res/layouts
|
||||
@@ -90,7 +87,3 @@ forge-gui/tools/PerSetTrackingResults
|
||||
*.tiled-session
|
||||
/forge-gui/res/adventure/*.tiled-project
|
||||
/forge-gui/res/adventure/*.tiled-session
|
||||
|
||||
# Ignore python temporaries
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
33
.gitlab/issue_templates/Bug.md
Normal file
33
.gitlab/issue_templates/Bug.md
Normal 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
|
||||
15
.gitlab/issue_templates/Feature.md
Normal file
15
.gitlab/issue_templates/Feature.md
Normal 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
|
||||
@@ -15,7 +15,7 @@ public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public class AiCardMemory {
|
||||
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
||||
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||
BOUNCED_THIS_TURN, // These cards were bounced this turn
|
||||
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
|
||||
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
|
||||
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
|
||||
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part
|
||||
|
||||
@@ -66,11 +66,13 @@ import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -481,7 +483,7 @@ public class AiController {
|
||||
|
||||
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
|
||||
// don't play MDFC land if other side is spell and enough lands are available
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1664,7 +1666,8 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||
@@ -1742,18 +1745,11 @@ public class AiController {
|
||||
return null;
|
||||
});
|
||||
|
||||
Thread t = new Thread(future);
|
||||
t.start();
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
try {
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
try {
|
||||
t.stop();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||
future.cancel(true);
|
||||
}
|
||||
future.cancel(true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
|
||||
if (thisRemove > 0) {
|
||||
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
|
||||
public PaymentDecision visit(CostRemoveAnyCounter cost) {
|
||||
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) {
|
||||
return null;
|
||||
@@ -716,7 +716,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
|
||||
if (over > 0) {
|
||||
toRemove += over;
|
||||
table.put(null, crd, CounterEnumType.QUEST, over);
|
||||
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public enum AiPlayDecision {
|
||||
StackNotEmpty,
|
||||
AnotherTime,
|
||||
|
||||
// Don't play decision reasons
|
||||
// Don't play decision reasons,
|
||||
CantPlaySa,
|
||||
CantPlayAi,
|
||||
CantAfford,
|
||||
|
||||
@@ -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) {
|
||||
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) {
|
||||
typeList.remove(activate);
|
||||
@@ -1074,80 +1074,6 @@ public class ComputerUtil {
|
||||
return prevented;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card card = sa.getHostCard();
|
||||
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
|
||||
@@ -1319,6 +1245,80 @@ public class ComputerUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility sub = sa.getSubAbility();
|
||||
@@ -1327,6 +1327,7 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cipher spells
|
||||
if (sub != null) {
|
||||
final ApiType api = sub.getApi();
|
||||
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
|
||||
@@ -1621,6 +1622,7 @@ public class ComputerUtil {
|
||||
damage = dmg;
|
||||
}
|
||||
|
||||
// Triggered abilities
|
||||
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
|
||||
for (final Trigger t : c.getTriggers()) {
|
||||
if (TriggerType.Attacks.equals(t.getMode())) {
|
||||
@@ -2542,7 +2544,7 @@ public class ComputerUtil {
|
||||
|
||||
boolean opponent = controller.isOpponentOf(ai);
|
||||
|
||||
final CounterType p1p1Type = CounterEnumType.P1P1;
|
||||
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
|
||||
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return Aggregates.random(options);
|
||||
@@ -2551,7 +2553,7 @@ public class ComputerUtil {
|
||||
String logic = sa.getParam("AILogic");
|
||||
switch (logic) {
|
||||
case "Torture":
|
||||
return options.get(1);
|
||||
return "Torture";
|
||||
case "GraceOrCondemnation":
|
||||
List<ZoneType> graceZones = new ArrayList<ZoneType>();
|
||||
graceZones.add(ZoneType.Battlefield);
|
||||
@@ -2559,12 +2561,12 @@ public class ComputerUtil {
|
||||
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
|
||||
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
|
||||
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
|
||||
return options.get(aiGrace > humanGrace ? 0 : 1);
|
||||
return aiGrace > humanGrace ? "Grace" : "Condemnation";
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
|
||||
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
|
||||
CardCollection computerlist = ai.getCreaturesInPlay();
|
||||
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
|
||||
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
|
||||
case "Judgment":
|
||||
if (votes.isEmpty()) {
|
||||
CardCollection list = new CardCollection();
|
||||
@@ -2578,71 +2580,68 @@ public class ComputerUtil {
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "Protection":
|
||||
if (votes.isEmpty()) {
|
||||
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
|
||||
List<String> restrictedToColors = Lists.newArrayList();
|
||||
for (Object o : options) {
|
||||
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
|
||||
restrictedToColors.put(sp.getOriginalDescription(), sp);
|
||||
if (o instanceof String) {
|
||||
restrictedToColors.add((String) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
|
||||
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
|
||||
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
|
||||
}
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "FeatherOrQuill":
|
||||
SpellAbility feather = (SpellAbility)options.get(0);
|
||||
SpellAbility quill = (SpellAbility)options.get(1);
|
||||
// try to mill opponent with Quill vote
|
||||
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
|
||||
int numQuill = votes.get(quill).size();
|
||||
int numQuill = votes.get("Quill").size();
|
||||
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
|
||||
}
|
||||
}
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
// if source is not on the battlefield anymore, choose +1/+1 ones
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
// if no hand cards, try to mill opponent
|
||||
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return opponent ? quill : feather;
|
||||
return opponent ? "Quill" : "Feather";
|
||||
}
|
||||
|
||||
// AI has something to discard
|
||||
if (ai.equals(controller)) {
|
||||
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
|
||||
return quill;
|
||||
return "Quill";
|
||||
}
|
||||
}
|
||||
|
||||
// default card draw and discard are better than +1/+1 counter
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
case "StrengthOrNumbers":
|
||||
SpellAbility strength = (SpellAbility)options.get(0);
|
||||
SpellAbility numbers = (SpellAbility)options.get(1);
|
||||
// similar to fabricate choose +1/+1 or Token
|
||||
int numStrength = votes.get(strength).size();
|
||||
int numNumbers = votes.get(numbers).size();
|
||||
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
|
||||
int numStrength = votes.get("Strength").size();
|
||||
int numNumbers = votes.get("Numbers").size();
|
||||
|
||||
Card token = TokenAi.spawnToken(controller, numbers);
|
||||
Card token = TokenAi.spawnToken(controller, saToken);
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? strength : numbers;
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? strength : numbers;
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
}
|
||||
|
||||
// token would not survive
|
||||
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
|
||||
return opponent ? numbers : strength;
|
||||
return opponent ? "Numbers" : "Strength";
|
||||
}
|
||||
|
||||
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
|
||||
@@ -2663,40 +2662,35 @@ public class ComputerUtil {
|
||||
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
|
||||
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
|
||||
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
|
||||
case "SproutOrHarvest":
|
||||
SpellAbility sprout = (SpellAbility)options.get(0);
|
||||
SpellAbility harvest = (SpellAbility)options.get(1);
|
||||
// lifegain would hurt or has no effect
|
||||
if (opponent) {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return harvest;
|
||||
return "Harvest";
|
||||
}
|
||||
} else {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return sprout;
|
||||
return "Sprout";
|
||||
}
|
||||
}
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? sprout : harvest;
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? sprout : harvest;
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
}
|
||||
// TODO add Lifegain to +1/+1 counters trigger
|
||||
|
||||
// for now +1/+1 counters are better
|
||||
return opponent ? harvest : sprout;
|
||||
return opponent ? "Harvest" : "Sprout";
|
||||
case "DeathOrTaxes":
|
||||
SpellAbility death = (SpellAbility)options.get(0);
|
||||
SpellAbility taxes = (SpellAbility)options.get(1);
|
||||
|
||||
int numDeath = votes.get(death).size();
|
||||
int numTaxes = votes.get(taxes).size();
|
||||
int numDeath = votes.get("Death").size();
|
||||
int numTaxes = votes.get("Taxes").size();
|
||||
|
||||
if (opponent) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
@@ -2704,29 +2698,29 @@ public class ComputerUtil {
|
||||
// would need to sacrifice more creatures than AI has
|
||||
// sacrifice even more
|
||||
if (aiCreatures.size() <= numDeath) {
|
||||
return death;
|
||||
return "Death";
|
||||
}
|
||||
// would need to discard more cards than it has
|
||||
if (aiCardsInHand.size() <= numTaxes) {
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
}
|
||||
|
||||
// has cards with SacMe or Token
|
||||
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
|
||||
return death;
|
||||
return "Death";
|
||||
}
|
||||
|
||||
// has cards with DiscardMe
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
}
|
||||
|
||||
// discard is probably less worse than sacrifice
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
} else {
|
||||
// ai is first voter or ally of controller
|
||||
// both are not affected, but if opponents control creatures, sacrifice is worse
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
|
||||
}
|
||||
default:
|
||||
return Iterables.getFirst(options, null);
|
||||
@@ -2743,7 +2737,7 @@ public class ComputerUtil {
|
||||
return safeCards;
|
||||
}
|
||||
|
||||
public static Card getKilledByTargeting(final SpellAbility sa, CardCollectionView validCards) {
|
||||
public static Card getKilledByTargeting(final SpellAbility sa, Iterable<Card> validCards) {
|
||||
CardCollection killables = CardLists.filter(validCards, c -> c.getController() != sa.getActivatingPlayer() && c.getSVar("Targeting").equals("Dies"));
|
||||
return ComputerUtilCard.getBestCreatureAI(killables);
|
||||
}
|
||||
|
||||
@@ -919,14 +919,14 @@ public class ComputerUtilCard {
|
||||
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
|
||||
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
|
||||
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
|
||||
for (byte c : MagicColor.WUBRG) {
|
||||
if ((colors & c) != 0) {
|
||||
return MagicColor.toLongString(c);
|
||||
}
|
||||
}
|
||||
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
|
||||
return restrictedToColors.get(0); // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static List<String> getColorByProminence(final List<Card> list) {
|
||||
|
||||
@@ -974,13 +974,17 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
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) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -994,11 +998,12 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
power += pBonus;
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,13 +1107,17 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
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) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1122,11 +1131,12 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
@@ -1295,7 +1305,6 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
@@ -1305,8 +1314,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
@@ -1321,14 +1333,13 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
return power;
|
||||
}
|
||||
@@ -1519,14 +1530,16 @@ public class ComputerUtilCombat {
|
||||
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
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) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1540,11 +1553,10 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
|
||||
@@ -291,12 +291,6 @@ public class ComputerUtilMana {
|
||||
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) {
|
||||
// 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"))
|
||||
@@ -449,6 +443,7 @@ public class ComputerUtilMana {
|
||||
manaProduced = manaProduced.replace(s, color);
|
||||
}
|
||||
} else if (saMana.hasParam("ReplaceColor")) {
|
||||
// replace color
|
||||
String color = saMana.getParam("ReplaceColor");
|
||||
if ("Chosen".equals(color)) {
|
||||
if (card.hasChosenColor()) {
|
||||
@@ -740,8 +735,7 @@ public class ComputerUtilMana {
|
||||
|
||||
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
|
||||
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);
|
||||
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1502,7 +1496,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (!cost.isReusuableResource()) {
|
||||
for (CostPart part : cost.getCostParts()) {
|
||||
for(CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
|
||||
unpreferredCost = true;
|
||||
}
|
||||
@@ -1593,8 +1587,10 @@ public class ComputerUtilMana {
|
||||
|
||||
// don't use abilities with dangerous drawbacks
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
if (sub != null) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
|
||||
|
||||
@@ -264,14 +264,12 @@ public abstract class GameState {
|
||||
}
|
||||
|
||||
if (c.hasMergedCard()) {
|
||||
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
|
||||
// we have to go by the current top card name here
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
|
||||
} else {
|
||||
String suffix = c.hasPaperFoil() ? "+" : "";
|
||||
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
.append("|Art:").append(c.getPaperCard().getArtIndex());
|
||||
}
|
||||
}
|
||||
@@ -321,21 +319,18 @@ public abstract class GameState {
|
||||
newText.append(":Cloaked");
|
||||
}
|
||||
}
|
||||
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
||||
newText.append("|Transformed");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
newText.append("|Flipped");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||
newText.append("|Meld");
|
||||
if (c.getMeldedWith() != null) {
|
||||
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
|
||||
newText.append(":");
|
||||
newText.append(c.getMeldedWith().getName()).append(suffix);
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
|
||||
if (c.isModal()) {
|
||||
newText.append("|Modal");
|
||||
} else {
|
||||
newText.append("|Transformed");
|
||||
newText.append(c.getMeldedWith().getName());
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
|
||||
newText.append("|Modal");
|
||||
}
|
||||
|
||||
if (c.getPlayerAttachedTo() != null) {
|
||||
@@ -1316,8 +1311,8 @@ public abstract class GameState {
|
||||
if (info.endsWith("Cloaked")) {
|
||||
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
|
||||
}
|
||||
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Backside, true);
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Flipped")) {
|
||||
c.setState(CardStateName.Flipped, true);
|
||||
@@ -1335,6 +1330,9 @@ public abstract class GameState {
|
||||
}
|
||||
c.setState(CardStateName.Meld, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Modal, true);
|
||||
c.setBackSide(true);
|
||||
}
|
||||
else if (info.startsWith("OnAdventure")) {
|
||||
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";
|
||||
|
||||
@@ -1347,11 +1347,6 @@ public class PlayerControllerAi extends PlayerController {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
|
||||
// TODO check if profile detection set to Auto
|
||||
|
||||
@@ -171,7 +171,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
|
||||
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
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ public abstract class SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
if (!"Once".equals(aiLogic)) {
|
||||
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
|
||||
return !AiCardMemory.isRememberedCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ public abstract class SpellAbilityAi {
|
||||
* The rest of the logic not covered by the canPlayAI template is defined here
|
||||
*/
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
|
||||
if (MyRandom.getRandom().nextFloat() < .8f) {
|
||||
// 80% chance to play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.DelayedTrigger, DelayedTriggerAi.class)
|
||||
.put(ApiType.Destroy, DestroyAi.class)
|
||||
.put(ApiType.DestroyAll, DestroyAllAi.class)
|
||||
.put(ApiType.Detain, DetainAi.class)
|
||||
.put(ApiType.Dig, DigAi.class)
|
||||
.put(ApiType.DigMultiple, DigMultipleAi.class)
|
||||
.put(ApiType.DigUntil, DigUntilAi.class)
|
||||
|
||||
@@ -83,7 +83,7 @@ public class AddTurnAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -159,78 +159,81 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
|
||||
|
||||
sa.setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !c.isEquipping();
|
||||
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !c.isEquipping();
|
||||
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, source)) {
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, source);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,7 +255,8 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
return decision;
|
||||
} else if (!mandatory) {
|
||||
return decision;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// fallback if animate is mandatory
|
||||
sa.resetTargets();
|
||||
List<Card> list = CardUtil.getValidCardsToTarget(sa);
|
||||
@@ -273,13 +277,8 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
|
||||
if (sa.getMaxTargets() == 0) {
|
||||
// this happens if an optional cost is skipped, e.g. Brave the Wilds
|
||||
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
|
||||
}
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final boolean alwaysActivatePWAbility = sa.isPwAbility()
|
||||
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
|
||||
@@ -291,8 +290,10 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
// list is empty, no possible targets
|
||||
@@ -397,8 +398,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (logic.equals("SetPT")) {
|
||||
// TODO: 1. Teach the AI to use this to save the creature from direct damage;
|
||||
// 2. Determine the best target in a smarter way?
|
||||
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
|
||||
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
|
||||
Card buffed = becomeAnimated(worst, sa);
|
||||
|
||||
@@ -435,9 +435,11 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("AITgts") && !list.isEmpty()) {
|
||||
//No logic, but we do have preferences. Pick the best among those?
|
||||
Card best = ComputerUtilCard.getBestAI(list);
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
if(best != null) {
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||
|
||||
@@ -988,7 +988,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
}
|
||||
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
|
||||
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
|
||||
// Living Weapon or similar
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
@@ -289,7 +289,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// have access to more mana
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
@@ -453,7 +455,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
if (subAb == null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -499,6 +506,19 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// 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<>();
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
@@ -753,7 +773,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
if (subAb == null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1594,7 +1619,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (logic.startsWith("ExilePreference")) {
|
||||
return doExilePreferenceLogic(decider, sa, fetchList);
|
||||
} else if (logic.equals("BounceOwnTrigger")) {
|
||||
return doBounceOwnTriggerLogic(decider, sa, fetchList);
|
||||
return doBounceOwnTriggerLogic(decider, fetchList);
|
||||
}
|
||||
}
|
||||
if (fetchList.isEmpty()) {
|
||||
@@ -2158,18 +2183,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
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));
|
||||
// TODO check for threatened cards
|
||||
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
|
||||
if (!prefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(prefChoices);
|
||||
}
|
||||
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
|
||||
// some extra benefit like First Responder
|
||||
} else if (!unprefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getWorstAI(unprefChoices);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -286,6 +286,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
|
||||
// Change Zone All, can be any type moving from one zone to another
|
||||
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -88,7 +89,12 @@ public class CharmAi extends SpellAbilityAi {
|
||||
CharmEffect.chainAbilities(sa, chosenList);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
|
||||
|
||||
@@ -78,8 +78,10 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
// If mandatory, then we will play it.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If not mandatory, then we won't play it.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} {
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.Map;
|
||||
public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
|
||||
@@ -38,6 +38,10 @@ public class CloneAi extends SpellAbilityAi {
|
||||
// TODO - add some kind of check for during human turn to answer
|
||||
// "Can I use this to block something?"
|
||||
|
||||
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
|
||||
}
|
||||
|
||||
PhaseHandler phase = game.getPhaseHandler();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -73,7 +77,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // end cloneCanPlayAI()
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
|
||||
@@ -42,7 +42,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!MyRandom.percentTrue(chance)
|
||||
&& !"Always".equals(logic)
|
||||
&& !"AlwaysIfViable".equals(logic)
|
||||
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
@@ -93,6 +93,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
sa.getTargets().add(top);
|
||||
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, decision);
|
||||
@@ -114,6 +115,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (logic.contains("Always")) {
|
||||
// If the logic is "Always" or "AlwaysIfViable", we will always play this ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -124,11 +126,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||
}
|
||||
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||
}
|
||||
|
||||
}
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
if (sa.isMandatory()) {
|
||||
|
||||
@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
} else if (type.equals("DIVINITY")) {
|
||||
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
|
||||
} else if (CounterType.getType(type).isKeywordCounter()) {
|
||||
} else if (CounterType.get(type).isKeywordCounter()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
|
||||
} else {
|
||||
// The AI really should put counters on cards that can use it.
|
||||
|
||||
@@ -20,40 +20,41 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
return setTargets(ai, sa);
|
||||
}
|
||||
|
||||
final CounterType counterType = getCounterType(sa);
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
if (!sa.usesTargeting()) {
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
} else {
|
||||
return setTargets(ai, sa);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
@@ -154,7 +155,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (counterType == null || counterType.is(type)) {
|
||||
addTargetsByCounterType(ai, sa, aiList, type);
|
||||
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +164,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
if (!oppList.isEmpty()) {
|
||||
// not enough targets
|
||||
if (sa.canAddMoreTarget()) {
|
||||
final CounterType type = CounterEnumType.M1M1;
|
||||
final CounterType type = CounterType.get(CounterEnumType.M1M1);
|
||||
if (counterType == null || counterType == type) {
|
||||
addTargetsByCounterType(ai, sa, oppList, type);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
// 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);
|
||||
// because countertype can't be chosen anymore, only look for poison counters
|
||||
|
||||
@@ -170,7 +170,7 @@ public class CountersPutAi extends CountersAi {
|
||||
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
|
||||
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);
|
||||
if (best != null) {
|
||||
@@ -247,9 +247,10 @@ public class CountersPutAi extends CountersAi {
|
||||
} else if (sa.getSubAbility() != null
|
||||
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
|
||||
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
|
||||
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
// Bristling Hydra: save from death using a ping activation
|
||||
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
|
||||
@@ -336,7 +337,7 @@ public class CountersPutAi extends CountersAi {
|
||||
Game game = ai.getGame();
|
||||
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);
|
||||
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return doCombatAdaptLogic(source, amount, combat);
|
||||
@@ -442,9 +443,11 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
sa.addDividedAllocation(c, amount);
|
||||
return decision;
|
||||
} else if (!hasSacCost) {
|
||||
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return decision;
|
||||
} else {
|
||||
if (!hasSacCost) {
|
||||
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,21 +609,7 @@ public class CountersPutAi extends CountersAi {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.getType(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);
|
||||
}
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
|
||||
// each non +1/+1 counter on the card is a 10% chance of not
|
||||
// activating this ability.
|
||||
|
||||
@@ -635,7 +624,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -682,12 +671,14 @@ public class CountersPutAi extends CountersAi {
|
||||
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list;
|
||||
CardCollection list = null;
|
||||
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty() && isMandatoryTrigger) {
|
||||
@@ -703,8 +694,9 @@ public class CountersPutAi extends CountersAi {
|
||||
|| sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.isCurse()) {
|
||||
@@ -747,6 +739,9 @@ public class CountersPutAi extends CountersAi {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
// boolean chance = true;
|
||||
boolean preferred = true;
|
||||
CardCollection list;
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||
@@ -767,12 +762,26 @@ public class CountersPutAi extends CountersAi {
|
||||
if ("ChargeToBestCMC".equals(aiLogic)) {
|
||||
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
// If the AI logic is to charge to best CMC, we can return true
|
||||
// if the logic was successfully applied or if it's mandatory.
|
||||
return decision;
|
||||
}
|
||||
if (mandatory) {
|
||||
} else if (mandatory) {
|
||||
// If the logic was not applied and it's mandatory, we return false.
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
} else {
|
||||
// If the logic was not applied and it's not mandatory, we return false.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
|
||||
AiAbilityDecision decision = doChargeToOppCtrlCMCLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (mandatory) {
|
||||
// If the logic was not applied and it's mandatory, we return false.
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -823,19 +832,19 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
Iterable<Card> filteredField;
|
||||
if (sa.isCurse()) {
|
||||
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
filteredField = ai.getCardsIn(ZoneType.Battlefield);
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
CardCollection list = CardLists.getTargetableCards(filteredField, sa);
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
int totalTargets = list.size();
|
||||
boolean preferred = true;
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
int totalTargets = list.size();
|
||||
|
||||
sa.resetTargets();
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (mandatory) {
|
||||
// When things are mandatory, gotta handle a little differently
|
||||
@@ -860,7 +869,9 @@ public class CountersPutAi extends CountersAi {
|
||||
if (list.isEmpty()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
Card choice = null;
|
||||
@@ -872,21 +883,27 @@ public class CountersPutAi extends CountersAi {
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
} else if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else {
|
||||
if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (choice != null && divided) {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
@@ -962,8 +979,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) {
|
||||
// Bolster does use this
|
||||
// TODO need more or less logic there?
|
||||
final CounterType m1m1 = CounterEnumType.M1M1;
|
||||
final CounterType p1p1 = CounterEnumType.P1P1;
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
|
||||
// no logic if there is no options or no to choice
|
||||
if (!isOptional && Iterables.size(options) <= 1) {
|
||||
@@ -1081,10 +1098,11 @@ public class CountersPutAi extends CountersAi {
|
||||
Player ai = sa.getActivatingPlayer();
|
||||
GameEntity e = (GameEntity) params.get("Target");
|
||||
// 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 (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.M1M1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, c)) {
|
||||
@@ -1098,14 +1116,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 (options.contains(CounterEnumType.POISON)) {
|
||||
return CounterEnumType.POISON;
|
||||
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
|
||||
return CounterType.get(CounterEnumType.POISON);
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterEnumType.EXPERIENCE)) {
|
||||
return CounterEnumType.EXPERIENCE;
|
||||
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
|
||||
return CounterType.get(CounterEnumType.EXPERIENCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1209,8 +1228,10 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
if (numCtrs < optimalCMC) {
|
||||
// If the AI has less counters than the optimal CMC, it should play the ability.
|
||||
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.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
@@ -1232,8 +1253,9 @@ public class CountersPutAi extends CountersAi {
|
||||
if (numCtrs < optimalCMC) {
|
||||
// If the AI has less counters than the optimal CMC, it should play the ability.
|
||||
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.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
Card tgt = (Card) params.get("Target");
|
||||
|
||||
// planeswalker has high priority for loyalty counters
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) {
|
||||
return CounterEnumType.LOYALTY;
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
}
|
||||
|
||||
if (tgt.getController().isOpponentOf(ai)) {
|
||||
// creatures with BaseToughness below or equal zero might be
|
||||
// killed if their counters are removed
|
||||
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
|
||||
if (options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
// 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");
|
||||
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
|
||||
|
||||
if (maritEmpty) {
|
||||
return CounterEnumType.ICE;
|
||||
return CounterType.get(CounterEnumType.ICE);
|
||||
}
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
|
||||
// fallback logic, select positive counter to add more
|
||||
|
||||
@@ -384,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (targetCard.getController().isOpponentOf(ai)) {
|
||||
// if its a Planeswalker try to remove Loyality first
|
||||
if (targetCard.isPlaneswalker()) {
|
||||
return CounterEnumType.LOYALTY;
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
@@ -392,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterEnumType.M1M1;
|
||||
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.P1P1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -20,6 +21,14 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
// based on what the expected targets could be
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// abCost stuff that should probably be centralized...
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for some costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
}
|
||||
// wait until stack is empty (prevents duplicate kills)
|
||||
if (!ai.getGame().getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
|
||||
@@ -133,9 +142,9 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
|
||||
// When using Pestilence to hurt players, do it at
|
||||
// the end of the opponent's turn only
|
||||
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|
||||
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
|
||||
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|
||||
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
|
||||
// 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!
|
||||
// || (ai.sa.getPayCosts(). ??? )
|
||||
|
||||
@@ -110,7 +110,15 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection list;
|
||||
|
||||
// Targeting
|
||||
if (sa.usesTargeting()) {
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
// Assume there where already enough targets chosen by AI Logic Above
|
||||
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
@@ -120,10 +128,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
int maxTargets;
|
||||
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if (sa.getRootAbility().costHasManaX() ||
|
||||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
|
||||
if (sa.getRootAbility().costHasManaX()) {
|
||||
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
||||
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
// need to set XPaid to get the right number for
|
||||
|
||||
119
forge-ai/src/main/java/forge/ai/ability/DetainAi.java
Normal file
119
forge-ai/src/main/java/forge/ai/ability/DetainAi.java
Normal file
@@ -0,0 +1,119 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class DetainAi extends SpellAbilityAi {
|
||||
|
||||
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
protected boolean prefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
|
||||
final Game game = ai.getGame();
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.filter(list, CREATURE_OR_TAP_ABILITY);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
if (!mandatory) {
|
||||
sa.resetTargets();
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
PhaseHandler phase = game.getPhaseHandler();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, list);
|
||||
if (primeTarget != null) {
|
||||
choice = primeTarget;
|
||||
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
// Tap creatures possible blockers before combat during AI's turn.
|
||||
List<Card> attackers;
|
||||
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
//Combat has already started
|
||||
attackers = game.getCombat().getAttackers();
|
||||
} else {
|
||||
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
|
||||
attackers.remove(source);
|
||||
}
|
||||
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
|
||||
|
||||
// TODO check if own creature would be forced to attack and we want to keep it alive
|
||||
|
||||
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
|
||||
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
} else if (phase.isPlayerTurn(opp)
|
||||
&& phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
// Tap creatures possible blockers before combat during AI's turn.
|
||||
if (list.anyMatch(CardPredicates.CREATURES)) {
|
||||
List<Card> creatureList = CardLists.filter(list, c -> c.isCreature() && CombatUtil.canAttack(c, opp));
|
||||
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
|
||||
} else { // no creatures available
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
} else {
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
if (!mandatory) {
|
||||
sa.resetTargets();
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
if (!ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -369,7 +370,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
|
||||
// try to make opponent lose to poison
|
||||
// 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) {
|
||||
sa.getTargets().add(oppA);
|
||||
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) {
|
||||
aiTarget = false;
|
||||
}
|
||||
@@ -471,7 +472,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.FileSection;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
@@ -37,17 +39,22 @@ public class EffectAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
|
||||
String logic = "";
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
logic = sa.getParam("AILogic");
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
if (logic.equals("BeginningOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("EndOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
boolean worthHolding = false;
|
||||
@@ -289,7 +296,6 @@ public class EffectAi extends SpellAbilityAi {
|
||||
} else if (logic.equals("YawgmothsWill")) {
|
||||
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.startsWith("NeedCreatures")) {
|
||||
// TODO convert to AiCheckSVar
|
||||
if (ai.getCreaturesInPlay().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
@@ -324,8 +330,8 @@ public class EffectAi extends SpellAbilityAi {
|
||||
Combat combat = game.getCombat();
|
||||
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
|
||||
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
// ideally needs once per combat or something
|
||||
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
@@ -379,6 +385,118 @@ public class EffectAi extends SpellAbilityAi {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else if (sa.hasParam("RememberObjects")) { //generic
|
||||
boolean cantAttack = false;
|
||||
boolean cantBlock = false;
|
||||
boolean cantActivate = false;
|
||||
|
||||
String duraction = sa.getParam("Duration");
|
||||
|
||||
String matchStr = "Card.IsRemembered";
|
||||
|
||||
for (String st : sa.getParam("StaticAbilities").split(",")) {
|
||||
Map<String, String> params = FileSection.parseToMap(sa.getSVar(st), FileSection.DOLLAR_SIGN_KV_SEPARATOR);
|
||||
List<StaticAbilityMode> modes = StaticAbilityMode.listValueOf(params.get("Mode"));
|
||||
|
||||
if (modes.contains(StaticAbilityMode.CantAttack) && matchStr.equals(params.get("ValidCard"))) {
|
||||
cantAttack = true;
|
||||
}
|
||||
if (modes.contains(StaticAbilityMode.CantBlock) && matchStr.equals(params.get("ValidCard"))) {
|
||||
cantBlock = true;
|
||||
}
|
||||
if (modes.contains(StaticAbilityMode.CantBlockBy) && matchStr.equals(params.get("ValidBlocker"))) {
|
||||
cantBlock = true;
|
||||
}
|
||||
if (modes.contains(StaticAbilityMode.CantBeActivated) && matchStr.equals(params.get("ValidCard"))) {
|
||||
cantActivate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add more cases later
|
||||
if (!cantAttack && !cantBlock && !cantActivate) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (cantBlock && duraction == null && phase.isPlayerTurn(ai) && !phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
|
||||
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa));
|
||||
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
List<Card> oppCreatures = CardLists.filterAsList(list, c -> {
|
||||
return c.isCreature() && c.getController().isOpponentOf(ai);
|
||||
});
|
||||
|
||||
List<Card> oppWithAbilities = CardLists.filterAsList(list, c -> {
|
||||
return !c.isCreature() && c.getController().isOpponentOf(ai) && c.getSpellAbilities().anyMatch(SpellAbility::isActivatedAbility);
|
||||
});
|
||||
|
||||
if (cantAttack || cantBlock) {
|
||||
if (oppCreatures.isEmpty()) {
|
||||
if (!cantActivate || oppWithAbilities.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
if (cantAttack && cantBlock && !oppCreatures.isEmpty()) {
|
||||
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, oppCreatures);
|
||||
if (primeTarget != null) {
|
||||
choice = primeTarget;
|
||||
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
// Tap creatures possible blockers before combat during AI's turn.
|
||||
List<Card> attackers;
|
||||
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
//Combat has already started
|
||||
attackers = game.getCombat().getAttackers();
|
||||
} else {
|
||||
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
|
||||
}
|
||||
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
|
||||
|
||||
// TODO check if own creature would be forced to attack and we want to keep it alive
|
||||
|
||||
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
|
||||
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
}
|
||||
} // TODO add logic to tap non creatures with activated abilities if cantActivate is true
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else {
|
||||
if (!ComputerUtil.shouldCastLessThanMax(ai, sa.getHostCard())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
oppCreatures.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else { //no AILogic
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
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.Game;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayLife;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -34,26 +36,6 @@ public class EndureAi extends SpellAbilityAi {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
|
||||
@@ -76,8 +76,7 @@ public class ManaAi extends SpellAbilityAi {
|
||||
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
|
||||
}
|
||||
if ("AtOppEOT".equals(logic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai
|
||||
&& (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife());
|
||||
return (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
|
||||
}
|
||||
return super.checkPhaseRestrictions(ai, sa, ph, logic);
|
||||
}
|
||||
@@ -159,7 +158,7 @@ public class ManaAi extends SpellAbilityAi {
|
||||
int numCounters = 0;
|
||||
int manaSurplus = 0;
|
||||
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()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
@@ -278,4 +277,10 @@ public class ManaAi extends SpellAbilityAi {
|
||||
}
|
||||
return !lose;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
return checkApiLogic(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ public class MeldAi extends SpellAbilityAi {
|
||||
boolean hasPrimaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(primaryMeld).and(CardPredicates.isOwner(aiPlayer)));
|
||||
boolean hasSecondaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(secondaryMeld).and(CardPredicates.isOwner(aiPlayer)));
|
||||
if (hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld)) {
|
||||
// If the primary meld card is on the battlefield and both meld cards are owned by the AI, play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the secondary meld card is on the battlefield and it is the one being activated, play the ability
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,9 @@ public class MillAi extends SpellAbilityAi {
|
||||
if (aiLogic.equals("Main1")) {
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|
||||
|| ComputerUtil.castSpellInMain1(ai, sa);
|
||||
} else if (aiLogic.equals("EndOfOppTurn")) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
} else if (aiLogic.equals("LilianaMill")) {
|
||||
// TODO convert to AICheckSVar
|
||||
// Only mill if a "Raise Dead" target is available, in case of control decks with few creatures
|
||||
return CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES).size() >= 1;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
|
||||
if (combat == null || !combat.isAttacking(source)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
|
||||
} else if (AiCardMemory.isRememberedCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
// The AI can meaningfully do it only to one creature per card yet, trying to do it to multiple cards
|
||||
// may result in overextending and losing the attacker
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
@@ -42,6 +41,7 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getTargets().add(blocker);
|
||||
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import forge.ai.*;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
@@ -28,6 +29,11 @@ public class PeekAndRevealAi extends SpellAbilityAi {
|
||||
if (aiPlayer.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("EndOfOppTurn".equals(logic)) {
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
if (!(ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
// So far this only appears on Triggers, but will expand
|
||||
// once things get converted from Dig + NoMove
|
||||
|
||||
@@ -78,7 +78,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
||||
&& ai.getManaPool().totalMana() <= 0
|
||||
&& (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()
|
||||
&& !ComputerUtil.castPermanentInMain1(ai, sa)) {
|
||||
// AiPlayDecision.AnotherTime;
|
||||
|
||||
@@ -153,8 +153,8 @@ public class PlayAi extends SpellAbilityAi {
|
||||
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
final CardStateName state;
|
||||
if (sa.hasParam("CastTransformed")) {
|
||||
state = CardStateName.Backside;
|
||||
options.forEach(c -> c.changeToState(CardStateName.Backside));
|
||||
state = CardStateName.Transformed;
|
||||
options.forEach(c -> c.changeToState(CardStateName.Transformed));
|
||||
} else {
|
||||
state = CardStateName.Original;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.GameLossReason;
|
||||
@@ -64,7 +65,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
boolean result;
|
||||
if (sa.usesTargeting()) {
|
||||
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
|
||||
result = true;
|
||||
} else {
|
||||
@@ -89,7 +90,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
PlayerCollection betterTgts = tgts.filter(input -> {
|
||||
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
|
||||
return false;
|
||||
} else if (!input.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
} else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -108,7 +109,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
if (tgts.isEmpty()) {
|
||||
if (mandatory) {
|
||||
// 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);
|
||||
return true;
|
||||
}
|
||||
@@ -120,7 +121,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
|
||||
return true;
|
||||
}
|
||||
return !input.canReceiveCounters(CounterEnumType.POISON);
|
||||
return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON));
|
||||
});
|
||||
if (!betterAllies.isEmpty()) {
|
||||
allies = betterAllies;
|
||||
|
||||
@@ -7,6 +7,8 @@ import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostTapType;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -21,6 +23,13 @@ import java.util.*;
|
||||
|
||||
public class PumpAi extends PumpAiBase {
|
||||
|
||||
private static boolean hasTapCost(final Cost cost, final Card source) {
|
||||
if (cost == null) {
|
||||
return true;
|
||||
}
|
||||
return cost.hasSpecificCostType(CostTapType.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if ("MoveCounter".equals(aiLogic)) {
|
||||
@@ -87,7 +96,7 @@ public class PumpAi extends PumpAiBase {
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
|
||||
final Game game = ai.getGame();
|
||||
boolean main1Preferred = "Main1IfAble".equals(sa.getParam("AILogic")) && ph.is(PhaseType.MAIN1, ai);
|
||||
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
|
||||
if (game.getStack().isEmpty() && hasTapCost(sa.getPayCosts(), sa.getHostCard())) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
|
||||
return false;
|
||||
}
|
||||
@@ -338,13 +347,13 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
//Targeted
|
||||
if (!pumpTgtAI(ai, sa, defense, attack, false, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // pumpPlayAI()
|
||||
|
||||
private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory,
|
||||
boolean immediately) {
|
||||
@@ -478,28 +487,18 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)) {
|
||||
list.remove(source);
|
||||
}
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
|
||||
list.remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
// Detain target nonland permanent: don't target noncreature permanents that don't have
|
||||
// any activated abilities.
|
||||
if ("DetainNonLand".equals(sa.getParam("AILogic"))) {
|
||||
list = CardLists.filter(list, CardPredicates.CREATURES.or(card -> {
|
||||
for (SpellAbility sa1 : card.getSpellAbilities()) {
|
||||
if (sa1.isActivatedAbility()) {
|
||||
return true;
|
||||
}
|
||||
if (game.getStack().isEmpty()) {
|
||||
// If the cost is tapping, don't activate before declare attack/block
|
||||
if (sa.getPayCosts().hasTapCost()) {
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)) {
|
||||
list.remove(sa.getHostCard());
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
|
||||
list.remove(sa.getHostCard());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
|
||||
@@ -147,7 +147,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
|
||||
// important to call canPlay first so targets are added if needed
|
||||
AiAbilityDecision decision = canPlay(ai, sa);
|
||||
if (mandatory && !decision.decision().willingToPlay()) {
|
||||
if (mandatory && decision.decision().willingToPlay()) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return decision;
|
||||
|
||||
@@ -37,12 +37,13 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Do it once per turn, generally (may be improved later)
|
||||
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
|
||||
if (AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
// ability is targeted
|
||||
sa.resetTargets();
|
||||
|
||||
PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
@@ -61,6 +62,14 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // could not find a valid target
|
||||
}
|
||||
|
||||
if (!canTgtHuman || !canTgtAI) {
|
||||
// can't target another player anyway, remember for no second activation this turn
|
||||
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
} else {
|
||||
// if it's just defined, no big deal
|
||||
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
@@ -71,6 +80,8 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
// Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
|
||||
|
||||
AiAbilityDecision decision = canPlay(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
|
||||
@@ -88,9 +88,9 @@ public class ScryAi extends SpellAbilityAi {
|
||||
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
|
||||
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
|
||||
// even if there's no mana cost.
|
||||
if (sa.getPayCosts().hasTapCost()
|
||||
&& (sa.getPayCosts().hasManaCost() || sa.getHostCard().isCreature())
|
||||
&& !isSorcerySpeed(sa, ai)) {
|
||||
if (logic.equals("AtOppEOT") || (sa.getPayCosts().hasTapCost()
|
||||
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
|
||||
&& !isSorcerySpeed(sa, ai))) {
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ public class SurveilAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -88,6 +92,7 @@ public class SurveilAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (randomReturn) {
|
||||
AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,19 @@ import java.util.function.Predicate;
|
||||
|
||||
public abstract class TapAiBase extends SpellAbilityAi {
|
||||
|
||||
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* tapTargetList.
|
||||
@@ -102,34 +115,12 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
final Game game = ai.getGame();
|
||||
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
tapList = CardLists.filter(tapList, CardPredicates.CAN_TAP);
|
||||
tapList = CardLists.filter(tapList, c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa1 : c.getSpellAbilities()) {
|
||||
if (sa1.isAbility() && sa1.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
|
||||
|
||||
//use broader approach when the cost is a positive thing
|
||||
if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) {
|
||||
tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
tapList = CardLists.filter(tapList, c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa12 : c.getSpellAbilities()) {
|
||||
if (sa12.isAbility() && sa12.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
|
||||
}
|
||||
|
||||
//try to exclude things that will already be tapped due to something on stack or because something is
|
||||
|
||||
@@ -8,6 +8,7 @@ import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
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
|
||||
// counters for better effect, but exceptions should be added here).
|
||||
Card target = (Card)params.get("Target");
|
||||
return !ComputerUtil.isNegativeCounter(CounterEnumType.TIME, target);
|
||||
return !ComputerUtil.isNegativeCounter(CounterType.get(CounterEnumType.TIME), target);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,7 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -66,15 +67,17 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
|
||||
|
||||
Card actualToken = spawnToken(ai, sa);
|
||||
|
||||
if (actualToken == null || (actualToken.isCreature() && actualToken.getNetToughness() < 1)) {
|
||||
// planeswalker plus ability or sub-ability is useful
|
||||
return pwPlus || sa.getSubAbility() != null;
|
||||
final AbilitySub sub = sa.getSubAbility();
|
||||
// useful
|
||||
// no token created
|
||||
return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkDrawback(sub, ai).willingToPlay()); // planeswalker plus ability or sub-ability is
|
||||
}
|
||||
|
||||
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
|
||||
String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString());
|
||||
String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString());
|
||||
|
||||
@@ -131,6 +134,9 @@ public class TokenAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
/*
|
||||
* readParameters() is called in checkPhaseRestrictions
|
||||
*/
|
||||
final Game game = ai.getGame();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ public final class ImageKeys {
|
||||
public static final String MONARCH_IMAGE = "monarch";
|
||||
public static final String THE_RING_IMAGE = "the_ring";
|
||||
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 SPECFACE_W = "$wspec";
|
||||
|
||||
@@ -18,9 +18,9 @@ import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.regex.Pattern;
|
||||
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
|
||||
*
|
||||
@@ -29,6 +29,8 @@ import java.util.stream.Collectors;
|
||||
public class StaticData {
|
||||
private final CardStorageReader cardReader;
|
||||
private final CardStorageReader tokenReader;
|
||||
private final CardStorageReader customCardReader;
|
||||
|
||||
private final String blockDataFolder;
|
||||
private final CardDb commonCards;
|
||||
private final CardDb variantCards;
|
||||
@@ -77,6 +79,7 @@ public class StaticData {
|
||||
this.tokenReader = tokenReader;
|
||||
this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder)));
|
||||
this.blockDataFolder = blockDataFolder;
|
||||
this.customCardReader = customCardReader;
|
||||
this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance;
|
||||
this.enableSmartCardArtSelection = enableSmartCardArtSelection;
|
||||
this.loadNonLegalCards = loadNonLegalCards;
|
||||
@@ -781,7 +784,6 @@ public class StaticData {
|
||||
Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>();
|
||||
boolean nifHeader = false;
|
||||
boolean cniHeader = false;
|
||||
final Pattern funnyCardCollectorNumberPattern = Pattern.compile("^F\\d+");
|
||||
for (CardEdition e : editions) {
|
||||
if (CardEdition.Type.FUNNY.equals(e.getType()))
|
||||
continue;
|
||||
@@ -789,13 +791,11 @@ public class StaticData {
|
||||
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
for (CardEdition.EditionEntry c : e.getObtainableCards()) {
|
||||
int amount = 1;
|
||||
|
||||
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...
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import forge.ImageKeys;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardEdition.EditionEntry;
|
||||
import forge.card.CardEdition.Type;
|
||||
@@ -45,6 +44,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public final static char NameSetSeparator = '|';
|
||||
public final static String FlagPrefix = "#";
|
||||
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
|
||||
private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
|
||||
@@ -199,7 +200,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
private static boolean isArtIndex(String s) {
|
||||
return StringUtils.isNumeric(s) && s.length() <= 2; // only artIndex between 1-99
|
||||
return StringUtils.isNumeric(s) && s.length() <= 2 ; // only artIndex between 1-99
|
||||
}
|
||||
|
||||
private static boolean isSetCode(String s) {
|
||||
@@ -240,8 +241,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
setCode = info[index];
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isArtIndex(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""))) {
|
||||
artIndex = Integer.parseInt(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""));
|
||||
if(info.length > index && isArtIndex(info[index])) {
|
||||
artIndex = Integer.parseInt(info[index]);
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isCollectorNumber(info[index])) {
|
||||
@@ -301,7 +302,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
|
||||
// create faces list from rules
|
||||
for (final CardRules rule : rules.values()) {
|
||||
if (filteredCards.contains(rule.getName()))
|
||||
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
|
||||
continue;
|
||||
for (ICardFace face : rule.getAllFaces()) {
|
||||
addFaceToDbNames(face);
|
||||
@@ -499,9 +500,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
public void addCard(PaperCard paperCard) {
|
||||
if (filtered.contains(paperCard.getName())) {
|
||||
if (excludeCard(paperCard.getName(), paperCard.getEdition()))
|
||||
return;
|
||||
}
|
||||
|
||||
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() {
|
||||
uniqueCardsByName.clear();
|
||||
for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) {
|
||||
|
||||
@@ -52,14 +52,6 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public DraftOptions getDraftOptions() {
|
||||
return draftOptions;
|
||||
}
|
||||
|
||||
public void setDraftOptions(DraftOptions draftOptions) {
|
||||
this.draftOptions = draftOptions;
|
||||
}
|
||||
|
||||
// immutable
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
@@ -283,22 +275,18 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
// Booster/draft info
|
||||
private List<BoosterSlot> boosterSlots = null;
|
||||
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 FoilType foilType = FoilType.NOT_SUPPORTED;
|
||||
private double foilChanceInBooster = 0;
|
||||
private double chanceReplaceCommonWith = 0;
|
||||
private String slotReplaceCommonWith = "Common";
|
||||
private String additionalSheetForFoils = "";
|
||||
private String additionalUnlockSet = "";
|
||||
private String boosterMustContain = "";
|
||||
private String boosterReplaceSlotFromPrintSheet = "";
|
||||
private String sheetReplaceCardFromSheet = "";
|
||||
private String sheetReplaceCardFromSheet2 = "";
|
||||
|
||||
// Draft options
|
||||
private DraftOptions draftOptions = null;
|
||||
private String doublePickDuringDraft = "";
|
||||
private String[] chaosDraftThemes = new String[0];
|
||||
|
||||
private final ListMultimap<String, EditionEntry> cardMap;
|
||||
@@ -385,6 +373,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
|
||||
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
|
||||
public String getAdditionalUnlockSet() { return additionalUnlockSet; }
|
||||
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
|
||||
public String getBoosterMustContain() { return boosterMustContain; }
|
||||
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
|
||||
public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; }
|
||||
@@ -563,16 +552,26 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public List<PrintSheet> getPrintSheetsBySection() {
|
||||
final CardDb cardDb = StaticData.instance().getCommonCards();
|
||||
Map<String, Integer> cardToIndex = new HashMap<>();
|
||||
|
||||
List<PrintSheet> sheets = Lists.newArrayList();
|
||||
for (Map.Entry<String, java.util.Collection<EditionEntry>> section : cardMap.asMap().entrySet()) {
|
||||
if (section.getKey().equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
|
||||
for (String sectionName : cardMap.keySet()) {
|
||||
if (sectionName.equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
|
||||
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()) {
|
||||
sheet.add(cardDb.getCard(card.name, this.getCode(), card.collectorNumber));
|
||||
List<EditionEntry> cards = cardMap.get(sectionName);
|
||||
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);
|
||||
@@ -630,7 +629,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* functional variant name - grouping #9
|
||||
*/
|
||||
// "(^(.?[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(
|
||||
@@ -639,7 +638,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* name - grouping #3
|
||||
* artist name - grouping #5
|
||||
*/
|
||||
"(^(.?[0-9A-Z-]+\\S?[A-Z☇]*)\\s)?([^@]*)( @(.*))?$"
|
||||
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
|
||||
);
|
||||
|
||||
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
|
||||
@@ -660,37 +659,31 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sectionName.endsWith("Types")) {
|
||||
CardType.Helper.parseTypes(sectionName, contents.get(sectionName));
|
||||
} else {
|
||||
// Parse cards
|
||||
// parse sections of the format "<collector number> <rarity> <name>"
|
||||
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
|
||||
for(String line : contents.get(sectionName)) {
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
|
||||
// parse sections of the format "<collector number> <rarity> <name>"
|
||||
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
|
||||
for(String line : contents.get(sectionName)) {
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String collectorNumber = matcher.group(2);
|
||||
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
|
||||
String cardName = matcher.group(5);
|
||||
String artistName = matcher.group(7);
|
||||
String functionalVariantName = matcher.group(9);
|
||||
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
|
||||
cardMap.put(sectionName, cis);
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
} else if (boosterSlotsToParse.contains(sectionName)) {
|
||||
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
|
||||
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
|
||||
} else {
|
||||
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
|
||||
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
|
||||
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
|
||||
|
||||
String collectorNumber = matcher.group(2);
|
||||
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
|
||||
String cardName = matcher.group(5);
|
||||
String artistName = matcher.group(7);
|
||||
String functionalVariantName = matcher.group(9);
|
||||
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
|
||||
cardMap.put(sectionName, cis);
|
||||
}
|
||||
} else if (boosterSlotsToParse.contains(sectionName)) {
|
||||
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
|
||||
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
|
||||
} else {
|
||||
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
|
||||
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
|
||||
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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.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.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;
|
||||
}
|
||||
|
||||
@@ -873,7 +850,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
@Override
|
||||
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");
|
||||
else map.put(item.getCode(), item);
|
||||
else map.put(item.getName(), item);
|
||||
}
|
||||
public void append(CardEdition.Collection C){ //Append custom editions
|
||||
if (lock) throw new UnsupportedOperationException("This is a read-only storage");
|
||||
|
||||
@@ -53,7 +53,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
private boolean addsWildCardColor;
|
||||
private int setColorID;
|
||||
private boolean custom;
|
||||
private boolean unsupported;
|
||||
private String path;
|
||||
|
||||
public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
|
||||
@@ -167,24 +166,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
|
||||
}
|
||||
|
||||
public boolean isTransformable() {
|
||||
if (CardSplitType.Transform == getSplitType()) {
|
||||
return true;
|
||||
}
|
||||
if (CardSplitType.Modal != getSplitType()) {
|
||||
return false;
|
||||
}
|
||||
for (ICardFace face : getAllFaces()) {
|
||||
for (String spell : face.getAbilities()) {
|
||||
if (spell.contains("AB$ SetState") && spell.contains("Mode$ Transform")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// TODO check keywords if needed
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ICardFace getWSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeW);
|
||||
}
|
||||
@@ -221,9 +202,7 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public boolean isCustom() { return custom; }
|
||||
public void setCustom() { custom = true; }
|
||||
|
||||
public boolean isUnsupported() { return unsupported; }
|
||||
public void setCustom() { custom = true; }
|
||||
|
||||
@Override
|
||||
public CardType getType() {
|
||||
@@ -364,21 +343,16 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public boolean isDoctor() {
|
||||
Set<String> subtypes = new HashSet<>();
|
||||
for (String type : mainPart.getType().getSubtypes()) {
|
||||
subtypes.add(type);
|
||||
if (!type.equals("Time Lord") && !type.equals("Doctor")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return subtypes.size() == 2 &&
|
||||
subtypes.contains("Time Lord") &&
|
||||
subtypes.contains("Doctor");
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canBeOathbreaker() {
|
||||
CardType type = mainPart.getType();
|
||||
if (mainPart.getOracleText().contains("can be your commander")) {
|
||||
return true;
|
||||
}
|
||||
return type.isPlaneswalker();
|
||||
}
|
||||
|
||||
@@ -831,8 +805,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
faces[0].assignMissingFields();
|
||||
final CardRules result = new CardRules(faces, CardSplitType.None, cah);
|
||||
|
||||
result.unsupported = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -189,38 +189,6 @@ public final class CardRulesPredicates {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -7,13 +7,13 @@ import java.util.EnumSet;
|
||||
public enum CardSplitType
|
||||
{
|
||||
None(FaceSelectionMethod.USE_PRIMARY_FACE, null),
|
||||
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
|
||||
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Transformed),
|
||||
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
|
||||
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
|
||||
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
|
||||
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
|
||||
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
|
||||
|
||||
public static final EnumSet<CardSplitType> DUAL_FACED_CARDS = EnumSet.of(
|
||||
|
||||
@@ -5,11 +5,12 @@ public enum CardStateName {
|
||||
Original,
|
||||
FaceDown,
|
||||
Flipped,
|
||||
Backside,
|
||||
Transformed,
|
||||
Meld,
|
||||
LeftSplit,
|
||||
RightSplit,
|
||||
Secondary,
|
||||
Modal,
|
||||
EmptyRoom,
|
||||
SpecializeW,
|
||||
SpecializeU,
|
||||
@@ -41,7 +42,7 @@ public enum CardStateName {
|
||||
return CardStateName.Flipped;
|
||||
}
|
||||
if ("DoubleFaced".equalsIgnoreCase(value)) {
|
||||
return CardStateName.Backside;
|
||||
return CardStateName.Transformed;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("No element named " + value + " in enum CardCharactersticName");
|
||||
|
||||
@@ -1066,74 +1066,4 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* TODO: Write javadoc for this type.
|
||||
@@ -69,13 +68,6 @@ public class PrintSheet {
|
||||
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) {
|
||||
int sum = start;
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
int count=0;
|
||||
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
|
||||
@@ -143,7 +144,7 @@ public class PrintSheet {
|
||||
return cardsWithWeights.isEmpty();
|
||||
}
|
||||
|
||||
public List<PaperCard> toFlatList() {
|
||||
public Iterable<PaperCard> toFlatList() {
|
||||
return cardsWithWeights.toFlatList();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class CardPool extends ItemPool<PaperCard> {
|
||||
private static final long serialVersionUID = -5379091255613968393L;
|
||||
|
||||
@@ -77,20 +78,12 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
|
||||
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
|
||||
CardDb db = entry.getValue();
|
||||
|
||||
PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags);
|
||||
if (paperCard != null) {
|
||||
this.add(paperCard, amount);
|
||||
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?
|
||||
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
|
||||
}
|
||||
@@ -426,12 +419,6 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
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) {
|
||||
List<Pair<String, Integer>> cardRequests = new ArrayList<>();
|
||||
if (lines == null)
|
||||
@@ -481,7 +468,6 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
* @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
|
||||
*/
|
||||
@Override
|
||||
public CardPool getFilteredPool(Predicate<PaperCard> predicate) {
|
||||
CardPool filteredPool = new CardPool();
|
||||
for (PaperCard c : this.items.keySet()) {
|
||||
|
||||
@@ -28,8 +28,6 @@ import forge.item.PaperCard;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.io.ObjectStreamException;
|
||||
import java.io.Serial;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
@@ -115,20 +113,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
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() {
|
||||
List<PaperCard> result = Lists.newArrayList();
|
||||
final CardPool cp = get(DeckSection.Commander);
|
||||
@@ -224,19 +208,14 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
super.cloneFieldsTo(clone);
|
||||
final Deck result = (Deck) clone;
|
||||
loadDeferredSections();
|
||||
// parts shouldn't be null
|
||||
if (parts != null) {
|
||||
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
|
||||
CardPool cp = new CardPool();
|
||||
result.parts.put(kv.getKey(), cp);
|
||||
cp.addAll(kv.getValue());
|
||||
}
|
||||
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
|
||||
CardPool cp = new CardPool();
|
||||
result.parts.put(kv.getKey(), cp);
|
||||
cp.addAll(kv.getValue());
|
||||
}
|
||||
result.setAiHints(StringUtils.join(aiHints, " | "));
|
||||
result.setDraftNotes(draftNotes);
|
||||
//noinspection ConstantValue
|
||||
if(tags != null) //Can happen deserializing old Decks.
|
||||
result.tags.addAll(this.tags);
|
||||
tags.addAll(result.getTags());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -542,17 +521,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
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) {
|
||||
if (aiHintsInfo == null || aiHintsInfo.trim().isEmpty()) {
|
||||
return;
|
||||
@@ -646,14 +614,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
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} */
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
@@ -703,4 +663,4 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
}
|
||||
return totalCount == 0 ? 0 : Math.round(totalCMC / totalCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,11 @@ import forge.util.TextUtil;
|
||||
import org.apache.commons.lang3.Range;
|
||||
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.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
@@ -57,13 +60,6 @@ public enum DeckFormat {
|
||||
//Limited contraption decks have no restrictions.
|
||||
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,
|
||||
card -> StaticData.instance().getCommanderPredicate().test(card)
|
||||
@@ -112,13 +108,7 @@ public enum DeckFormat {
|
||||
}
|
||||
},
|
||||
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) {
|
||||
@Override
|
||||
public boolean allowCustomCards() {
|
||||
//If the player has them, may as well allow them.
|
||||
return true;
|
||||
}
|
||||
},
|
||||
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, 15), 4),
|
||||
Vanguard ( 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),
|
||||
@@ -201,57 +191,12 @@ public enum DeckFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the default maximum copies of a card in this format.
|
||||
* @return the maxCardCopies
|
||||
*/
|
||||
public int getMaxCardCopies() {
|
||||
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) {
|
||||
if (deck == null) {
|
||||
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
|
||||
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());
|
||||
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.");
|
||||
// Might cause issues since it ignores "Special" Cards
|
||||
if (simpleCard == null) {
|
||||
@@ -539,10 +484,6 @@ public enum DeckFormat {
|
||||
// Not needed by default
|
||||
}
|
||||
|
||||
public boolean allowCustomCards() {
|
||||
return StaticData.instance().allowCustomCardsInDecksConformance();
|
||||
}
|
||||
|
||||
public boolean isLegalCard(PaperCard pc) {
|
||||
if (cardPoolFilter == null) {
|
||||
if (paperCardPoolFilter == null) {
|
||||
@@ -557,13 +498,13 @@ public enum DeckFormat {
|
||||
if (cardPoolFilter != null && !cardPoolFilter.test(rules)) {
|
||||
return false;
|
||||
}
|
||||
if (this == DeckFormat.Oathbreaker) {
|
||||
if (this.equals(DeckFormat.Oathbreaker)) {
|
||||
return rules.canBeOathbreaker();
|
||||
}
|
||||
if (this == DeckFormat.Brawl) {
|
||||
if (this.equals(DeckFormat.Brawl)) {
|
||||
return rules.canBeBrawlCommander();
|
||||
}
|
||||
if (this == DeckFormat.TinyLeaders) {
|
||||
if (this.equals(DeckFormat.TinyLeaders)) {
|
||||
return rules.canBeTinyLeadersCommander();
|
||||
}
|
||||
return rules.canBeCommander();
|
||||
@@ -612,8 +553,6 @@ public enum DeckFormat {
|
||||
for (final PaperCard p : commanders) {
|
||||
cmdCI |= p.getRules().getColorIdentity().getColor();
|
||||
}
|
||||
if(cmdCI == MagicColor.ALL_COLORS)
|
||||
return x -> true;
|
||||
Predicate<CardRules> predicate = CardRulesPredicates.hasColorIdentity(cmdCI);
|
||||
if (commanders.size() == 1 && commanders.get(0).getRules().canBePartnerCommander()) {
|
||||
// Also show available partners a commander can have a partner.
|
||||
|
||||
@@ -46,7 +46,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
|
||||
// These fields are kinda PK for PrintedCard
|
||||
private final String name;
|
||||
private String edition;
|
||||
private final String edition;
|
||||
/* [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.
|
||||
(see getCollectorNumber())
|
||||
@@ -154,31 +154,6 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
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() {
|
||||
if(this.flaglessVersion == null) {
|
||||
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.foil = foil;
|
||||
this.rarity = rarity;
|
||||
this.artist = artist;
|
||||
this.artist = TextUtil.normalizeText(artist);
|
||||
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.
|
||||
// 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);
|
||||
pc = readObjectAlternate(name, edition);
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -50,13 +50,6 @@ public abstract class PaperCardPredicates {
|
||||
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 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 final CardRarity operand;
|
||||
|
||||
|
||||
@@ -19,11 +19,6 @@ public class SealedTemplate {
|
||||
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 String name;
|
||||
|
||||
@@ -254,7 +254,7 @@ public class BoosterGenerator {
|
||||
|
||||
if (sheetKey.startsWith("wholeSheet")) {
|
||||
PrintSheet ps = getPrintSheet(sheetKey);
|
||||
result.addAll(ps.toFlatList());
|
||||
result.addAll(ps.all());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ public class BoosterGenerator {
|
||||
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
|
||||
List<PaperCard> candidates = Lists.newArrayList();
|
||||
for (PaperCard p : result) {
|
||||
if (replaceThis.contains(p)) {
|
||||
if (replaceThis.all().contains(p)) {
|
||||
candidates.add(candidates.size(), p);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,7 @@ public class BoosterGenerator {
|
||||
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
|
||||
List<PaperCard> candidates = Lists.newArrayList();
|
||||
for (PaperCard p : result) {
|
||||
if (replaceThis.contains(p)) {
|
||||
if (replaceThis.all().contains(p)) {
|
||||
candidates.add(candidates.size(), p);
|
||||
}
|
||||
}
|
||||
@@ -462,8 +462,9 @@ public class BoosterGenerator {
|
||||
} else {
|
||||
paperCards.addAll(ps.random(numCardsToGenerate, true));
|
||||
}
|
||||
|
||||
result.addAll(paperCards);
|
||||
}
|
||||
result.addAll(paperCards);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -199,14 +199,19 @@ public class ImageUtil {
|
||||
return getImageRelativePath(cp, face, true, true);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
if (setCode != null && !setCode.isEmpty())
|
||||
editionCode = setCode;
|
||||
else
|
||||
editionCode = cp.getEdition().toLowerCase();
|
||||
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
|
||||
if (cardCollectorNumber.startsWith("OHOP")) {
|
||||
editionCode = "ohop";
|
||||
@@ -217,42 +222,29 @@ public class ImageUtil {
|
||||
} else if (cardCollectorNumber.startsWith("OPC2")) {
|
||||
editionCode = "opc2";
|
||||
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 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) {
|
||||
if (face.equals("back")) {
|
||||
PaperCard meldBasePc = cp.getMeldBaseCard();
|
||||
cardCollectorNumber = meldBasePc.getCollectorNumber();
|
||||
String collectorNumberSuffix = "";
|
||||
|
||||
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);
|
||||
// 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")) {
|
||||
cardCollectorNumber += "b";
|
||||
} else if (!editionCode.equals("fin")) {
|
||||
cardCollectorNumber += "a";
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -264,10 +256,6 @@ public class ImageUtil {
|
||||
if (!faceParam.isEmpty()) {
|
||||
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),
|
||||
langCode, versionParam, faceParam);
|
||||
}
|
||||
@@ -288,7 +276,8 @@ public class ImageUtil {
|
||||
char c;
|
||||
for (int i = 0; i < in.length(); i++) {
|
||||
c = in.charAt(i);
|
||||
if ((c != '"') && (c != '/') && (c != ':') && (c != '?')) {
|
||||
if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) {
|
||||
} else {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
public void removeIf(Predicate<T> filter) {
|
||||
items.keySet().removeIf(filter);
|
||||
}
|
||||
|
||||
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 removeIf(Predicate<T> test) {
|
||||
for (final T item : items.keySet()) {
|
||||
if (test.test(item))
|
||||
remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
@@ -290,19 +285,4 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
|
||||
return (obj instanceof ItemPool ip) &&
|
||||
(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-logback</artifactId>
|
||||
<version>8.19.1</version>
|
||||
<version>7.15.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
|
||||
@@ -195,7 +195,7 @@ public class ForgeScript {
|
||||
return sa.isKeyword(Keyword.SADDLE);
|
||||
} else if (property.equals("Station")) {
|
||||
return sa.isKeyword(Keyword.STATION);
|
||||
} else if (property.equals("Cycling")) {
|
||||
}else if (property.equals("Cycling")) {
|
||||
return sa.isCycling();
|
||||
} else if (property.equals("Dash")) {
|
||||
return sa.isDash();
|
||||
@@ -237,8 +237,6 @@ public class ForgeScript {
|
||||
return sa.isBoast();
|
||||
} else if (property.equals("Exhaust")) {
|
||||
return sa.isExhaust();
|
||||
} else if (property.equals("Mayhem")) {
|
||||
return sa.isMayhem();
|
||||
} else if (property.equals("Mutate")) {
|
||||
return sa.isMutate();
|
||||
} else if (property.equals("Ninjutsu")) {
|
||||
|
||||
@@ -414,6 +414,19 @@ public class Game {
|
||||
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).
|
||||
* <i>Use this in UI and after match calculations</i>
|
||||
@@ -731,7 +744,7 @@ public class Game {
|
||||
if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) {
|
||||
return;
|
||||
}
|
||||
if (!visitor.visitAll(player.getCardsIn(ZoneType.PART_OF_COMMAND_ZONE))) {
|
||||
if (!visitor.visitAll(player.getZone(ZoneType.Command).getCards())) {
|
||||
return;
|
||||
}
|
||||
if (withSideboard && !visitor.visitAll(player.getZone(ZoneType.Sideboard).getCards())) {
|
||||
|
||||
@@ -1822,8 +1822,8 @@ public class GameAction {
|
||||
|
||||
private boolean stateBasedAction704_5q(Card c) {
|
||||
boolean checkAgain = false;
|
||||
final CounterType p1p1 = CounterEnumType.P1P1;
|
||||
final CounterType m1m1 = CounterEnumType.M1M1;
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
int plusOneCounters = c.getCounters(p1p1);
|
||||
int minusOneCounters = c.getCounters(m1m1);
|
||||
if (plusOneCounters > 0 && minusOneCounters > 0) {
|
||||
@@ -1843,7 +1843,7 @@ public class GameAction {
|
||||
return checkAgain;
|
||||
}
|
||||
private boolean stateBasedAction704_5r(Card c) {
|
||||
final CounterType dreamType = CounterEnumType.DREAM;
|
||||
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
|
||||
|
||||
int old = c.getCounters(dreamType);
|
||||
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) */
|
||||
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
|
||||
if (saSource != null) {
|
||||
|
||||
@@ -125,22 +125,10 @@ public final class GameActionUtil {
|
||||
|
||||
// need to be done there before static abilities does reset the card
|
||||
// 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()) {
|
||||
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 (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
@@ -177,7 +165,26 @@ public final class GameActionUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback));
|
||||
SpellAbility flashback = null;
|
||||
|
||||
// there is a flashback cost (and not the cards cost)
|
||||
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
|
||||
final String[] k = keyword.split(":");
|
||||
flashback = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
flashback.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else { // same cost as original (e.g. Otaria plane)
|
||||
flashback = sa.copy(activator);
|
||||
}
|
||||
flashback.setAlternativeCost(AlternativeCost.Flashback);
|
||||
flashback.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
flashback.setKeyword(inst);
|
||||
flashback.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(flashback);
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
@@ -187,7 +194,25 @@ public final class GameActionUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Harmonize));
|
||||
SpellAbility harmonize = null;
|
||||
|
||||
if (keyword.contains(":")) {
|
||||
final String[] k = keyword.split(":");
|
||||
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
harmonize.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
harmonize = sa.copy(activator);
|
||||
}
|
||||
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
|
||||
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
harmonize.setKeyword(inst);
|
||||
harmonize.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(harmonize);
|
||||
} else if (keyword.startsWith("Foretell")) {
|
||||
// Foretell cast only from Exile
|
||||
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
|
||||
@@ -242,7 +267,6 @@ public final class GameActionUtil {
|
||||
}
|
||||
stackCopy.setLastKnownZone(game.getStackZone());
|
||||
stackCopy.setCastFrom(oldZone);
|
||||
stackCopy.setCastSA(sa);
|
||||
lkicheck = true;
|
||||
|
||||
stackCopy.clearStaticChangedCardKeywords(false);
|
||||
@@ -293,30 +317,6 @@ public final class GameActionUtil {
|
||||
return alternatives;
|
||||
}
|
||||
|
||||
public static SpellAbility getGraveyardSpellByKeyword(KeywordInterface inst, SpellAbility sa, Player activator, AlternativeCost altCost) {
|
||||
String keyword = inst.getOriginal();
|
||||
SpellAbility newSA = null;
|
||||
|
||||
// there is a flashback cost (and not the cards cost)
|
||||
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
|
||||
final String[] k = keyword.split(":");
|
||||
newSA = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
newSA.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else { // same cost as original (e.g. Otaria plane)
|
||||
newSA = sa.copy(activator);
|
||||
}
|
||||
newSA.setAlternativeCost(altCost);
|
||||
newSA.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
newSA.setKeyword(inst);
|
||||
newSA.setIntrinsic(inst.isIntrinsic());
|
||||
return newSA;
|
||||
}
|
||||
|
||||
public static List<SpellAbility> getMayPlaySpellOptions(final SpellAbility sa, final Card source, final Player activator, boolean altCostOnly) {
|
||||
final List<SpellAbility> alternatives = Lists.newArrayList();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.event.GameEventCardAttachment;
|
||||
import forge.game.keyword.Keyword;
|
||||
@@ -304,6 +305,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
Integer value = counters.get(counterName);
|
||||
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) {
|
||||
if (num <= 0) {
|
||||
@@ -312,6 +316,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
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);
|
||||
|
||||
@@ -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 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) {
|
||||
if (n <= 0 || !canReceiveCounters(counterType)) {
|
||||
// As per rule 107.1b
|
||||
@@ -340,7 +351,18 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
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);
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ public enum GameLogEntryType {
|
||||
TURN("Turn"),
|
||||
MULLIGAN("Mulligan"),
|
||||
ANTE("Ante"),
|
||||
DRAFT("Draft"),
|
||||
ZONE_CHANGE("Zone Change"),
|
||||
PLAYER_CONTROL("Player control"),
|
||||
COMBAT("Combat"),
|
||||
|
||||
@@ -29,25 +29,25 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventGameOutcome ev) {
|
||||
// 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);
|
||||
|
||||
for (String outcome : ev.result().getOutcomeStrings()) {
|
||||
for (String outcome : ev.result.getOutcomeStrings()) {
|
||||
log.add(GameLogEntryType.GAME_OUTCOME, outcome);
|
||||
}
|
||||
return generateSummary(ev.history());
|
||||
return generateSummary(ev.history);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventScry ev) {
|
||||
String scryOutcome = "";
|
||||
|
||||
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()));
|
||||
} else if (ev.toBottom() == 0) {
|
||||
scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop()));
|
||||
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));
|
||||
} else if (ev.toBottom == 0) {
|
||||
scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop));
|
||||
} 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);
|
||||
@@ -57,12 +57,12 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
public GameLogEntry visit(GameEventSurveil ev) {
|
||||
String surveilOutcome = "";
|
||||
|
||||
if (ev.toLibrary() > 0 && ev.toGraveyard() > 0) {
|
||||
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player().toString(), String.valueOf(ev.toLibrary()), String.valueOf(ev.toGraveyard()));
|
||||
} else if (ev.toGraveyard() == 0) {
|
||||
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player().toString(), String.valueOf(ev.toLibrary()));
|
||||
if (ev.toLibrary > 0 && ev.toGraveyard > 0) {
|
||||
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player.toString(), String.valueOf(ev.toLibrary), String.valueOf(ev.toGraveyard));
|
||||
} else if (ev.toGraveyard == 0) {
|
||||
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player.toString(), String.valueOf(ev.toLibrary));
|
||||
} 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);
|
||||
@@ -70,26 +70,26 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventSpellAbilityCast event) {
|
||||
String player = event.sa().getActivatingPlayer().getName();
|
||||
String action = event.sa().isSpell() ? localizer.getMessage("lblCast")
|
||||
: event.sa().isTrigger() ? localizer.getMessage("lblTriggered")
|
||||
String player = event.sa.getActivatingPlayer().getName();
|
||||
String action = event.sa.isSpell() ? localizer.getMessage("lblCast")
|
||||
: event.sa.isTrigger() ? localizer.getMessage("lblTriggered")
|
||||
: localizer.getMessage("lblActivated");
|
||||
String object = event.si().getStackDescription().startsWith("Morph ")
|
||||
String object = event.si.getStackDescription().startsWith("Morph ")
|
||||
? localizer.getMessage("lblMorph")
|
||||
: event.sa().getHostCard().toString();
|
||||
: event.sa.getHostCard().toString();
|
||||
|
||||
String messageForLog = "";
|
||||
|
||||
if (event.sa().getTargetRestrictions() != null) {
|
||||
if (event.sa.getTargetRestrictions() != null) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (TargetChoices ch : event.sa().getAllTargetChoices()) {
|
||||
for (TargetChoices ch : event.sa.getAllTargetChoices()) {
|
||||
if (null != ch) {
|
||||
sb.append(ch);
|
||||
}
|
||||
@@ -104,18 +104,18 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventCardModeChosen ev) {
|
||||
if (!ev.log()) {
|
||||
if (!ev.log) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String modeChoiceOutcome;
|
||||
if (ev.random()) {
|
||||
modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName(), ev.mode());
|
||||
if (ev.random) {
|
||||
modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName, ev.mode);
|
||||
} else {
|
||||
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, "NICKNAME",
|
||||
Lang.getInstance().getNickName(name));
|
||||
@@ -124,7 +124,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
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) {
|
||||
@@ -152,8 +152,8 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(final GameEventPlayerControl event) {
|
||||
final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer();
|
||||
final Player p = event.player();
|
||||
final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer;
|
||||
final Player p = event.player;
|
||||
|
||||
final String message;
|
||||
if (newLobbyPlayer == null) {
|
||||
@@ -166,23 +166,23 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventTurnPhase ev) {
|
||||
Player p = ev.playerTurn();
|
||||
return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc() + Lang.getInstance().getPossessedObject(p.getName(), ev.phase().nameForUi));
|
||||
Player p = ev.playerTurn;
|
||||
return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc + Lang.getInstance().getPossessedObject(p.getName(), ev.phase.nameForUi));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventCardDamaged event) {
|
||||
String additionalLog = "";
|
||||
if (event.type() == DamageType.Deathtouch) {
|
||||
if (event.type == DamageType.Deathtouch) {
|
||||
additionalLog = localizer.getMessage("lblDeathtouch");
|
||||
}
|
||||
if (event.type() == DamageType.M1M1Counters) {
|
||||
if (event.type == DamageType.M1M1Counters) {
|
||||
additionalLog = localizer.getMessage("lblAsM1M1Counters");
|
||||
}
|
||||
if (event.type() == DamageType.LoyaltyLoss) {
|
||||
additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount()));
|
||||
if (event.type == DamageType.LoyaltyLoss) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -191,43 +191,43 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
*/
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventPlayerDamaged ev) {
|
||||
String extra = ev.infect() ? localizer.getMessage("lblLogAsPoisonCounters") : "";
|
||||
String damageType = ev.combat() ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat");
|
||||
String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source().toString(),
|
||||
String.valueOf(ev.amount()), damageType, ev.target().toString(), extra);
|
||||
String extra = ev.infect ? localizer.getMessage("lblLogAsPoisonCounters") : "";
|
||||
String damageType = ev.combat ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat");
|
||||
String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source.toString(),
|
||||
String.valueOf(ev.amount), damageType, ev.target.toString(), extra);
|
||||
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventPlayerPoisoned ev) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventPlayerRadiation ev) {
|
||||
String message;
|
||||
final int change = ev.change();
|
||||
final int change = ev.change;
|
||||
String radCtr = CounterEnumType.RAD.getName().toLowerCase() + " " +
|
||||
Localizer.getInstance().getMessage("lblCounter").toLowerCase();
|
||||
if (change >= 0) message = localizer.getMessage("lblLogPlayerRadiation",
|
||||
ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr),
|
||||
ev.source().toString());
|
||||
ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr),
|
||||
ev.source.toString());
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -239,16 +239,16 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
// Append Defending Player/Planeswalker
|
||||
|
||||
// Not a big fan of the triple nested loop here
|
||||
for (GameEntity k : ev.attackersMap().keySet()) {
|
||||
Collection<Card> attackers = ev.attackersMap().get(k);
|
||||
for (GameEntity k : ev.attackersMap.keySet()) {
|
||||
Collection<Card> attackers = ev.attackersMap.get(k);
|
||||
if (attackers == null || attackers.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
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) {
|
||||
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());
|
||||
}
|
||||
@@ -262,7 +262,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
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();
|
||||
MapOfLists<Card, Card> attackers = kv.getValue();
|
||||
if (attackers == null || attackers.isEmpty()) {
|
||||
@@ -298,7 +298,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ public class GameRules {
|
||||
private boolean AISideboardingEnabled = false;
|
||||
private boolean sideboardForAI = false;
|
||||
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
|
||||
private boolean useGrayText;
|
||||
@@ -125,12 +124,4 @@ public class GameRules {
|
||||
public void setWarnAboutAICards(final boolean warnAboutAICards) {
|
||||
this.warnAboutAICards = warnAboutAICards;
|
||||
}
|
||||
|
||||
public int getSimTimeout() {
|
||||
return this.simTimeout;
|
||||
}
|
||||
|
||||
public void setSimTimeout(final int duration) {
|
||||
this.simTimeout = duration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,12 +293,6 @@ public class GameSnapshot {
|
||||
Card newCard = toGame.findById(fromCard.getId());
|
||||
Player toPlayer = findBy(toGame, fromCard.getController());
|
||||
ZoneType fromType = fromCard.getZone().getZoneType();
|
||||
int zonePosition = 0;
|
||||
if (ZoneType.ORDERED_ZONES.contains(fromType)) {
|
||||
// If the card is in an ordered zone, we need to find its position in the zone
|
||||
// and set it in the new game.
|
||||
zonePosition = fromCard.getZone().getCards().indexOf(fromCard);
|
||||
}
|
||||
|
||||
if (newCard == null) {
|
||||
// Storing a game uses this path...
|
||||
@@ -315,10 +309,10 @@ public class GameSnapshot {
|
||||
}
|
||||
|
||||
if (fromType.equals(ZoneType.Stack)) {
|
||||
toGame.getStackZone().add(newCard, zonePosition);
|
||||
toGame.getStackZone().add(newCard);
|
||||
newCard.setZone(toGame.getStackZone());
|
||||
} else {
|
||||
toPlayer.getZone(fromType).add(newCard, zonePosition);
|
||||
toPlayer.getZone(fromType).add(newCard);
|
||||
newCard.setZone(toPlayer.getZone(fromType));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ public enum GameType {
|
||||
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
|
||||
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"),
|
||||
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
|
||||
QuestCommander (DeckFormat.Commander, true, true, false, "lblQuestCommander", ""),
|
||||
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
|
||||
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),
|
||||
Adventure (DeckFormat.Adventure, true, false, false, "lblAdventure", ""),
|
||||
@@ -72,8 +71,6 @@ public enum GameType {
|
||||
return deck;
|
||||
});
|
||||
|
||||
private static final EnumSet<GameType> DRAFT_FORMATS = EnumSet.of(Draft, QuestDraft, AdventureEvent);
|
||||
|
||||
private final DeckFormat deckFormat;
|
||||
private final boolean isCardPoolLimited, canSideboard, addWonCardsMidGame;
|
||||
private final String name, englishName, description;
|
||||
@@ -90,7 +87,7 @@ public enum GameType {
|
||||
addWonCardsMidGame = addWonCardsMidgame0;
|
||||
name = localizer.getMessage(name0);
|
||||
englishName = localizer.getEnglishMessage(name0);
|
||||
if (!description0.isEmpty()) {
|
||||
if (description0.length()>0) {
|
||||
description0 = localizer.getMessage(description0);
|
||||
}
|
||||
description = description0;
|
||||
@@ -130,8 +127,19 @@ public enum GameType {
|
||||
return addWonCardsMidGame;
|
||||
}
|
||||
|
||||
public boolean isDraft() {
|
||||
return DRAFT_FORMATS.contains(this);
|
||||
public boolean isCommandZoneNeeded() {
|
||||
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() {
|
||||
@@ -145,27 +153,6 @@ public enum GameType {
|
||||
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) {
|
||||
return Enums.getIfPresent(GameType.class, name).orNull();
|
||||
}
|
||||
|
||||
@@ -215,7 +215,6 @@ public class GameView extends TrackableObject {
|
||||
}
|
||||
public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
|
||||
if (dependencies.isEmpty()) {
|
||||
set(TrackableProperty.Dependencies, "");
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -23,7 +23,6 @@ import forge.item.PaperCard;
|
||||
import forge.util.Localizer;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
@@ -225,7 +224,6 @@ public class Match {
|
||||
// friendliness
|
||||
Map<Player, Map<DeckSection, List<? extends PaperCard>>> rAICards = new HashMap<>();
|
||||
Multimap<Player, PaperCard> removedAnteCards = ArrayListMultimap.create();
|
||||
Map<Player, List<PaperCard>> unsupported = new HashMap<>();
|
||||
|
||||
final FCollectionView<Player> players = game.getPlayers();
|
||||
final List<RegisteredPlayer> playersConditions = game.getMatch().getPlayers();
|
||||
@@ -290,32 +288,22 @@ public class Match {
|
||||
}
|
||||
}
|
||||
|
||||
Deck toCheck = psc.getDeck();
|
||||
if (toCheck == null) {
|
||||
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());
|
||||
Deck myDeck = psc.getDeck();
|
||||
player.setDraftNotes(myDeck.getDraftNotes());
|
||||
|
||||
Set<PaperCard> myRemovedAnteCards = null;
|
||||
if (!rules.useAnte()) {
|
||||
myRemovedAnteCards = getRemovedAnteCards(myDeck.getLeft());
|
||||
myRemovedAnteCards = getRemovedAnteCards(myDeck);
|
||||
for (PaperCard cp: myRemovedAnteCards) {
|
||||
for (Entry<DeckSection, CardPool> ds : myDeck.getLeft()) {
|
||||
for (Entry<DeckSection, CardPool> ds : myDeck) {
|
||||
ds.getValue().removeAll(cp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil());
|
||||
if (myDeck.getLeft().has(DeckSection.Sideboard)) {
|
||||
preparePlayerZone(player, ZoneType.Sideboard, myDeck.getLeft().get(DeckSection.Sideboard), psc.useRandomFoil());
|
||||
preparePlayerZone(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil());
|
||||
if (myDeck.has(DeckSection.Sideboard)) {
|
||||
preparePlayerZone(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil());
|
||||
|
||||
// Assign Companion
|
||||
Card companion = player.assignCompanion(game, person);
|
||||
@@ -334,7 +322,7 @@ public class Match {
|
||||
player.shuffle(null);
|
||||
|
||||
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()) {
|
||||
rAICards.put(player, cardsComplained);
|
||||
}
|
||||
@@ -349,7 +337,6 @@ public class Match {
|
||||
if (myRemovedAnteCards != null && !myRemovedAnteCards.isEmpty()) {
|
||||
removedAnteCards.putAll(player, myRemovedAnteCards);
|
||||
}
|
||||
unsupported.put(player, myDeck.getRight());
|
||||
}
|
||||
|
||||
final Localizer localizer = Localizer.getInstance();
|
||||
@@ -360,10 +347,6 @@ public class Match {
|
||||
if (!removedAnteCards.isEmpty()) {
|
||||
game.getAction().revealAnte(localizer.getMessage("lblAnteCardsRemoved"), removedAnteCards);
|
||||
}
|
||||
|
||||
if (!unsupported.isEmpty()) {
|
||||
game.getAction().revealUnsupported(unsupported);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeAnte(Game lastGame) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.game.ability;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.spellability.AbilityActivated;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -17,7 +18,14 @@ public class AbilityApiBased extends AbilityActivated {
|
||||
api = api0;
|
||||
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
|
||||
|
||||
@@ -63,9 +63,7 @@ public final class AbilityFactory {
|
||||
"CantChooseSubAbility", // Can't choose a player via ChoosePlayer
|
||||
"RegenerationAbility", // for Regeneration Effect
|
||||
"ReturnAbility", // for Delayed Trigger on Magpie
|
||||
"GiftAbility", // for Promise Gift
|
||||
"VoteSubAbility", // for Vote with VoteCard
|
||||
"VoteTiedAbility" // for fallback to Choices
|
||||
"GiftAbility" // for Promise Gift
|
||||
);
|
||||
|
||||
public enum AbilityRecordType {
|
||||
@@ -202,6 +200,15 @@ public final class AbilityFactory {
|
||||
final Card hostCard = state.getCard();
|
||||
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) {
|
||||
abCost = parseAbilityCost(state, mapParams, type);
|
||||
}
|
||||
@@ -261,7 +268,7 @@ public final class AbilityFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if (api == ApiType.Charm || api == ApiType.GenericChoice || api == ApiType.AssignGroup || api == ApiType.VillainousChoice || api == ApiType.Vote) {
|
||||
if (api == ApiType.Charm || api == ApiType.GenericChoice || api == ApiType.AssignGroup || api == ApiType.VillainousChoice) {
|
||||
final String key = "Choices";
|
||||
if (mapParams.containsKey(key)) {
|
||||
List<String> names = Lists.newArrayList(mapParams.get(key).split(","));
|
||||
@@ -383,9 +390,6 @@ public final class AbilityFactory {
|
||||
if (mapParams.containsKey("TargetsWithDifferentCMC")) {
|
||||
abTgt.setDifferentCMC(true);
|
||||
}
|
||||
if (mapParams.containsKey("TargetsWithDifferentNames")) {
|
||||
abTgt.setDifferentNames(true);
|
||||
}
|
||||
if (mapParams.containsKey("TargetsWithEqualToughness")) {
|
||||
abTgt.setEqualToughness(true);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,13 @@ public class AbilityUtils {
|
||||
}
|
||||
} else if (defined.equals("Enchanted")) {
|
||||
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")) {
|
||||
final CardCollectionView grave = player.getCardsIn(ZoneType.Graveyard);
|
||||
|
||||
@@ -2338,9 +2345,6 @@ public class AbilityUtils {
|
||||
if (sq[0].equals("YourSpeed")) {
|
||||
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")) {
|
||||
return doXMath(calculateAmount(c, sq[game.isNight() ? 1 : 2], ctb), expr, c, ctb);
|
||||
@@ -2985,14 +2989,14 @@ public class AbilityUtils {
|
||||
if (tgtCard.isFaceDown()) {
|
||||
collectSpellsForPlayEffect(list, original, controller, withAltCost);
|
||||
} else {
|
||||
if (state == CardStateName.Backside && !tgtCard.isModal() && tgtCard.isPermanent() && !tgtCard.isAura()) {
|
||||
if (state == CardStateName.Transformed && tgtCard.isPermanent() && !tgtCard.isAura()) {
|
||||
// casting defeated battle
|
||||
Spell sp = new SpellPermanent(tgtCard, original);
|
||||
sp.setCardState(original);
|
||||
list.add(sp);
|
||||
}
|
||||
if (tgtCard.isModal() && tgtCard.hasState(CardStateName.Backside)) {
|
||||
collectSpellsForPlayEffect(list, tgtCard.getState(CardStateName.Backside), controller, withAltCost);
|
||||
if (tgtCard.isModal() && tgtCard.hasState(CardStateName.Modal)) {
|
||||
collectSpellsForPlayEffect(list, tgtCard.getState(CardStateName.Modal), controller, withAltCost);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ public enum ApiType {
|
||||
AddPhase (AddPhaseEffect.class),
|
||||
AddTurn (AddTurnEffect.class),
|
||||
AdvanceCrank (AdvanceCrankEffect.class),
|
||||
Airbend (AirbendEffect.class),
|
||||
AlterAttribute (AlterAttributeEffect.class),
|
||||
Amass (AmassEffect.class),
|
||||
Animate (AnimateEffect.class),
|
||||
@@ -68,6 +67,7 @@ public enum ApiType {
|
||||
Counter (CounterEffect.class),
|
||||
DamageAll (DamageAllEffect.class),
|
||||
DealDamage (DamageDealEffect.class),
|
||||
Detain (DetainEffect.class),
|
||||
DayTime (DayTimeEffect.class),
|
||||
Debuff (DebuffEffect.class),
|
||||
DelayedTrigger (DelayedTriggerEffect.class),
|
||||
@@ -82,7 +82,6 @@ public enum ApiType {
|
||||
Draft (DraftEffect.class),
|
||||
Draw (DrawEffect.class),
|
||||
EachDamage (DamageEachEffect.class),
|
||||
Earthbend (EarthbendEffect.class),
|
||||
Effect (EffectEffect.class),
|
||||
Encode (EncodeEffect.class),
|
||||
EndCombatPhase (EndCombatPhaseEffect.class),
|
||||
|
||||
@@ -49,8 +49,6 @@ public abstract class SpellAbilityEffect {
|
||||
return sa.getDescription();
|
||||
}
|
||||
|
||||
public void buildSpellAbility(final SpellAbility sa) {}
|
||||
|
||||
/**
|
||||
* Returns this effect description with needed prelude and epilogue.
|
||||
* @param params
|
||||
@@ -1069,8 +1067,6 @@ public abstract class SpellAbilityEffect {
|
||||
// if ability was granted use that source so they can be kept apart later
|
||||
if (cause.isCopiedTrait()) {
|
||||
exilingSource = cause.getOriginalHost();
|
||||
} else if (cause.getKeyword() != null && cause.getKeyword().getStatic() != null) {
|
||||
exilingSource = cause.getKeyword().getStatic().getOriginalHost();
|
||||
}
|
||||
movedCard.setExiledWith(exilingSource);
|
||||
Player exiler = cause.hasParam("DefinedExiler") ?
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.Map;
|
||||
|
||||
import forge.game.card.Card;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.Spell;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
|
||||
@@ -23,7 +24,13 @@ public class SpellApiBased extends Spell {
|
||||
// A spell is always intrinsic
|
||||
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
|
||||
|
||||
@@ -2,6 +2,8 @@ package forge.game.ability;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import forge.game.ability.effects.ChangeZoneAllEffect;
|
||||
import forge.game.ability.effects.ChangeZoneEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.spellability.AbilityStatic;
|
||||
@@ -18,7 +20,9 @@ public class StaticAbilityApiBased extends AbilityStatic {
|
||||
api = api0;
|
||||
effect = api.getSpellEffect();
|
||||
|
||||
effect.buildSpellAbility(this);
|
||||
if (effect instanceof ChangeZoneEffect || effect instanceof ChangeZoneAllEffect) {
|
||||
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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 it’s 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -46,9 +46,6 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
|
||||
boolean altered = false;
|
||||
|
||||
switch (attr.trim()) {
|
||||
case "Harnessed":
|
||||
altered = gameCard.setHarnessed(activate);
|
||||
break;
|
||||
case "Plotted":
|
||||
altered = gameCard.setPlotted(activate);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardZoneTable;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.event.GameEventCombatChanged;
|
||||
import forge.game.event.GameEventTokenCreated;
|
||||
@@ -85,7 +86,7 @@ public class AmassEffect extends TokenEffectBase {
|
||||
}
|
||||
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("CounterType", CounterEnumType.P1P1);
|
||||
params.put("CounterType", CounterType.get(CounterEnumType.P1P1));
|
||||
params.put("Amount", amount);
|
||||
Card tgt = activator.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), false, params);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user