mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 17:58:01 +00:00
Compare commits
1 Commits
spm-releas
...
reduce-bit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e757cae9 |
4
.github/workflows/maven-publish.yml
vendored
4
.github/workflows/maven-publish.yml
vendored
@@ -129,9 +129,7 @@ jobs:
|
||||
makeLatest: true
|
||||
|
||||
- name: 🔧 Install XML tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
run: sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: 🔼 Bump versionCode in root POM
|
||||
id: bump_version
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ public class AiController {
|
||||
private int lastAttackAggression;
|
||||
private boolean useLivingEnd;
|
||||
private List<SpellAbility> skipped;
|
||||
private boolean timeoutReached;
|
||||
|
||||
public AiController(final Player computerPlayer, final Game game0) {
|
||||
player = computerPlayer;
|
||||
@@ -1665,9 +1664,6 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
// in case of infinite loop reset below would not be reached
|
||||
timeoutReached = false;
|
||||
|
||||
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
@@ -1677,11 +1673,6 @@ public class AiController {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timeoutReached) {
|
||||
timeoutReached = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.getHostCard().hasKeyword(Keyword.STORM)
|
||||
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
|
||||
&& player.getZone(ZoneType.Hand).contains(
|
||||
@@ -1761,10 +1752,7 @@ public class AiController {
|
||||
t.stop();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||
timeoutReached = true;
|
||||
future.cancel(true);
|
||||
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
|
||||
// TODO mark some as skipped to increase chance to find something playable next priority
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -2542,7 +2542,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);
|
||||
|
||||
@@ -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,13 +998,14 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return power;
|
||||
}
|
||||
@@ -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,13 +1131,14 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
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,15 +1333,14 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,13 +1553,12 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1593,9 +1587,11 @@ 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()) {
|
||||
if (sub != null) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -298,7 +298,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||
|
||||
if (sa.hasParam("Origin")) {
|
||||
try {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// This happens when Origin is something like
|
||||
// "Graveyard,Library" (Doomsday)
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
final String destination = sa.getParam("Destination");
|
||||
|
||||
@@ -493,6 +499,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"));
|
||||
@@ -902,6 +921,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
|
||||
// list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1244,12 +1265,53 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// if max CMC exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetCMC")) {
|
||||
if (choice.getCMC() > sa.getTargetRestrictions().getMaxTotalCMC(choice, sa) - sa.getTargets().getTotalTargetedCMC()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if max power exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetPower")) {
|
||||
if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// honor the Same Creature Type restriction
|
||||
if (sa.getTargetRestrictions().isWithSameCreatureType()) {
|
||||
Card firstTarget = sa.getTargetCard();
|
||||
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
|
||||
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
|
||||
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
|
||||
// vs. two cards in the second player's graveyard, which cards are more relevant to be targeted, etc.). Consider improving.
|
||||
if (sa.getTargetRestrictions().isSingleZone()) {
|
||||
Card firstTgt = sa.getTargetCard();
|
||||
CardCollection toRemove = new CardCollection();
|
||||
if (firstTgt != null) {
|
||||
for (Card t : sa.getTargets().getTargetCards()) {
|
||||
if (!t.getController().equals(firstTgt.getController())) {
|
||||
toRemove.add(t);
|
||||
}
|
||||
}
|
||||
sa.getTargets().removeAll(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1545,7 +1607,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()) {
|
||||
@@ -2109,19 +2171,17 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -154,7 +154,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 +163,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) {
|
||||
@@ -336,7 +336,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,12 +442,14 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
sa.addDividedAllocation(c, amount);
|
||||
return decision;
|
||||
} else if (!hasSacCost) {
|
||||
} else {
|
||||
if (!hasSacCost) {
|
||||
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -606,21 +608,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 +623,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 +670,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,9 +693,10 @@ public class CountersPutAi extends CountersAi {
|
||||
|| sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isCurse()) {
|
||||
choice = chooseCursedTarget(list, type, amount, ai);
|
||||
@@ -747,6 +738,8 @@ public class CountersPutAi extends CountersAi {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
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);
|
||||
@@ -768,12 +761,12 @@ public class CountersPutAi extends CountersAi {
|
||||
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
if (mandatory) {
|
||||
} else if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
// No target. So must be defined. (Unused at the moment)
|
||||
@@ -823,19 +816,19 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
Iterable<Card> filteredField;
|
||||
if (sa.isCurse()) {
|
||||
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
|
||||
@@ -872,22 +865,28 @@ public class CountersPutAi extends CountersAi {
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("M1M1")) {
|
||||
} else {
|
||||
if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (preferred) {
|
||||
}
|
||||
} 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")) {
|
||||
} 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);
|
||||
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
|
||||
@@ -962,8 +961,8 @@ public class CountersPutAi extends CountersAi {
|
||||
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// 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 +1080,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 +1098,15 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e instanceof Player p) {
|
||||
} else if (e instanceof Player) {
|
||||
Player p = (Player) e;
|
||||
if (p.isOpponentOf(ai)) {
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1232,8 +1233,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -133,9 +133,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(). ??? )
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...
|
||||
@@ -878,7 +878,7 @@ public class StaticData {
|
||||
}
|
||||
}
|
||||
}
|
||||
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
|
||||
// stream().toList() causes crash on Android, use Collectors.toList()
|
||||
List<String> NIF = new ArrayList<>(NIF_Q).stream().sorted().collect(Collectors.toList());
|
||||
List<String> CNI = new ArrayList<>(CNI_Q).stream().sorted().collect(Collectors.toList());
|
||||
List<String> TOK = new ArrayList<>(TOKEN_Q).stream().sorted().collect(Collectors.toList());
|
||||
|
||||
@@ -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);
|
||||
@@ -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,11 +659,6 @@ 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)) {
|
||||
@@ -692,7 +686,6 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListMultimap<String, EditionEntry> tokenMap = ArrayListMultimap.create();
|
||||
ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
|
||||
@@ -819,6 +812,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral
|
||||
|
||||
res.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");
|
||||
@@ -1018,13 +995,16 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public static final Predicate<CardEdition> HAS_BOOSTER_BOX = edition -> edition.getBoosterBoxCount() > 0;
|
||||
|
||||
@Deprecated //Use CardEdition::hasBasicLands and a nonnull test.
|
||||
public static final Predicate<CardEdition> hasBasicLands = ed -> {
|
||||
if (ed == null) {
|
||||
// Happens for new sets with "???" code
|
||||
return false;
|
||||
}
|
||||
return ed.hasBasicLands();
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, ed.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1045,7 +1025,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public boolean hasBasicLands() {
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (this.getCardInSet(landName).isEmpty())
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -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) {
|
||||
@@ -168,7 +167,21 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public boolean isTransformable() {
|
||||
return CardSplitType.Transform == getSplitType() || CardSplitType.Modal == getSplitType();
|
||||
if (CardSplitType.Transform == getSplitType()) {
|
||||
return true;
|
||||
}
|
||||
if (CardSplitType.Modal != getSplitType()) {
|
||||
return false;
|
||||
}
|
||||
for (ICardFace face : getAllFaces()) {
|
||||
for (String spell : face.getAbilities()) {
|
||||
if (spell.contains("AB$ SetState") && spell.contains("Mode$ Transform")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// TODO check keywords if needed
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ICardFace getWSpecialize() {
|
||||
@@ -209,8 +222,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
public boolean isCustom() { return custom; }
|
||||
public void setCustom() { custom = true; }
|
||||
|
||||
public boolean isUnsupported() { return unsupported; }
|
||||
|
||||
@Override
|
||||
public CardType getType() {
|
||||
switch (splitType.getAggregationMethod()) {
|
||||
@@ -350,21 +361,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();
|
||||
}
|
||||
|
||||
@@ -817,8 +823,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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package forge.card;
|
||||
import com.google.common.collect.UnmodifiableIterator;
|
||||
import forge.card.MagicColor.Color;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
import forge.util.BinaryUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -40,97 +41,27 @@ import java.util.stream.Stream;
|
||||
public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Serializable {
|
||||
private static final long serialVersionUID = 794691267379929080L;
|
||||
|
||||
// needs to be before other static
|
||||
private static final ColorSet[] cache = new ColorSet[MagicColor.ALL_COLORS + 1];
|
||||
static {
|
||||
byte COLORLESS = MagicColor.COLORLESS;
|
||||
byte WHITE = MagicColor.WHITE;
|
||||
byte BLUE = MagicColor.BLUE;
|
||||
byte BLACK = MagicColor.BLACK;
|
||||
byte RED = MagicColor.RED;
|
||||
byte GREEN = MagicColor.GREEN;
|
||||
Color C = Color.COLORLESS;
|
||||
Color W = Color.WHITE;
|
||||
Color U = Color.BLUE;
|
||||
Color B = Color.BLACK;
|
||||
Color R = Color.RED;
|
||||
Color G = Color.GREEN;
|
||||
|
||||
//colorless
|
||||
cache[COLORLESS] = new ColorSet(C);
|
||||
|
||||
//mono-color
|
||||
cache[WHITE] = new ColorSet(W);
|
||||
cache[BLUE] = new ColorSet(U);
|
||||
cache[BLACK] = new ColorSet(B);
|
||||
cache[RED] = new ColorSet(R);
|
||||
cache[GREEN] = new ColorSet(G);
|
||||
|
||||
//two-color
|
||||
cache[WHITE | BLUE] = new ColorSet(W, U);
|
||||
cache[WHITE | BLACK] = new ColorSet(W, B);
|
||||
cache[BLUE | BLACK] = new ColorSet(U, B);
|
||||
cache[BLUE | RED] = new ColorSet(U, R);
|
||||
cache[BLACK | RED] = new ColorSet(B, R);
|
||||
cache[BLACK | GREEN] = new ColorSet(B, G);
|
||||
cache[RED | GREEN] = new ColorSet(R, G);
|
||||
cache[RED | WHITE] = new ColorSet(R, W);
|
||||
cache[GREEN | WHITE] = new ColorSet(G, W);
|
||||
cache[GREEN | BLUE] = new ColorSet(G, U);
|
||||
|
||||
//three-color
|
||||
cache[WHITE | BLUE | BLACK] = new ColorSet(W, U, B);
|
||||
cache[WHITE | BLACK | GREEN] = new ColorSet(W, B, G);
|
||||
cache[BLUE | BLACK | RED] = new ColorSet(U, B, R);
|
||||
cache[BLUE | RED | WHITE] = new ColorSet(U, R, W);
|
||||
cache[BLACK | RED | GREEN] = new ColorSet(B, R, G);
|
||||
cache[BLACK | GREEN | BLUE] = new ColorSet(B, G, U);
|
||||
cache[RED | GREEN | WHITE] = new ColorSet(R, G, W);
|
||||
cache[RED | WHITE | BLACK] = new ColorSet(R, W, B);
|
||||
cache[GREEN | WHITE | BLUE] = new ColorSet(G, W, U);
|
||||
cache[GREEN | BLUE | RED] = new ColorSet(G, U, R);
|
||||
|
||||
//four-color
|
||||
cache[WHITE | BLUE | BLACK | RED] = new ColorSet(W, U, B, R);
|
||||
cache[BLUE | BLACK | RED | GREEN] = new ColorSet(U, B, R, G);
|
||||
cache[BLACK | RED | GREEN | WHITE] = new ColorSet(B, R, G, W);
|
||||
cache[RED | GREEN | WHITE | BLUE] = new ColorSet(R, G, W, U);
|
||||
cache[GREEN | WHITE | BLUE | BLACK] = new ColorSet(G, W, U, B);
|
||||
|
||||
//five-color
|
||||
cache[WHITE | BLUE | BLACK | RED | GREEN] = new ColorSet(W, U, B, R, G);
|
||||
}
|
||||
|
||||
private final Collection<Color> orderedShards;
|
||||
private final byte myColor;
|
||||
private final float orderWeight;
|
||||
private final Set<Color> enumSet;
|
||||
private final String desc;
|
||||
|
||||
private static final ColorSet[] cache = new ColorSet[32];
|
||||
|
||||
public static final ColorSet ALL_COLORS = fromMask(MagicColor.ALL_COLORS);
|
||||
public static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
|
||||
private static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
|
||||
|
||||
private ColorSet(final Color... ordered) {
|
||||
this.orderedShards = Arrays.asList(ordered);
|
||||
this.myColor = orderedShards.stream().map(Color::getColorMask).reduce((byte)0, (a, b) -> (byte)(a | b));
|
||||
private ColorSet(final byte mask) {
|
||||
this.myColor = mask;
|
||||
this.orderWeight = this.getOrderWeight();
|
||||
this.enumSet = EnumSet.copyOf(orderedShards);
|
||||
this.desc = orderedShards.stream().map(Color::getShortName).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
public static ColorSet fromMask(final int mask) {
|
||||
final int mask32 = mask & MagicColor.ALL_COLORS;
|
||||
if (cache[mask32] == null) {
|
||||
cache[mask32] = new ColorSet((byte) mask32);
|
||||
}
|
||||
return cache[mask32];
|
||||
}
|
||||
|
||||
public static ColorSet fromEnums(final Color... colors) {
|
||||
byte mask = 0;
|
||||
for (Color e : colors) {
|
||||
mask |= e.getColorMask();
|
||||
}
|
||||
return fromMask(mask);
|
||||
}
|
||||
|
||||
public static ColorSet fromNames(final String... colors) {
|
||||
byte mask = 0;
|
||||
for (final String s : colors) {
|
||||
@@ -362,7 +293,17 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return desc;
|
||||
final ManaCostShard[] orderedShards = getOrderedShards();
|
||||
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the null color.
|
||||
*
|
||||
* @return the nullColor
|
||||
*/
|
||||
public static ColorSet getNullColor() {
|
||||
return NO_COLORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +325,16 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
|
||||
public Set<Color> toEnumSet() {
|
||||
return EnumSet.copyOf(enumSet);
|
||||
if (isColorless()) {
|
||||
return EnumSet.of(Color.COLORLESS);
|
||||
}
|
||||
List<Color> list = new ArrayList<>();
|
||||
for (Color c : Color.values()) {
|
||||
if (hasAnyColor(c.getColormask())) {
|
||||
list.add(c);
|
||||
}
|
||||
}
|
||||
return EnumSet.copyOf(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -422,12 +372,72 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Color> stream() {
|
||||
public Stream<MagicColor.Color> stream() {
|
||||
return this.toEnumSet().stream();
|
||||
}
|
||||
|
||||
//Get array of mana cost shards for color set in the proper order
|
||||
public Collection<Color> getOrderedColors() {
|
||||
return orderedShards;
|
||||
public ManaCostShard[] getOrderedShards() {
|
||||
return shardOrderLookup[myColor];
|
||||
}
|
||||
|
||||
private static final ManaCostShard[][] shardOrderLookup = new ManaCostShard[MagicColor.ALL_COLORS + 1][];
|
||||
static {
|
||||
byte COLORLESS = MagicColor.COLORLESS;
|
||||
byte WHITE = MagicColor.WHITE;
|
||||
byte BLUE = MagicColor.BLUE;
|
||||
byte BLACK = MagicColor.BLACK;
|
||||
byte RED = MagicColor.RED;
|
||||
byte GREEN = MagicColor.GREEN;
|
||||
ManaCostShard C = ManaCostShard.COLORLESS;
|
||||
ManaCostShard W = ManaCostShard.WHITE;
|
||||
ManaCostShard U = ManaCostShard.BLUE;
|
||||
ManaCostShard B = ManaCostShard.BLACK;
|
||||
ManaCostShard R = ManaCostShard.RED;
|
||||
ManaCostShard G = ManaCostShard.GREEN;
|
||||
|
||||
//colorless
|
||||
shardOrderLookup[COLORLESS] = new ManaCostShard[] { C };
|
||||
|
||||
//mono-color
|
||||
shardOrderLookup[WHITE] = new ManaCostShard[] { W };
|
||||
shardOrderLookup[BLUE] = new ManaCostShard[] { U };
|
||||
shardOrderLookup[BLACK] = new ManaCostShard[] { B };
|
||||
shardOrderLookup[RED] = new ManaCostShard[] { R };
|
||||
shardOrderLookup[GREEN] = new ManaCostShard[] { G };
|
||||
|
||||
//two-color
|
||||
shardOrderLookup[WHITE | BLUE] = new ManaCostShard[] { W, U };
|
||||
shardOrderLookup[WHITE | BLACK] = new ManaCostShard[] { W, B };
|
||||
shardOrderLookup[BLUE | BLACK] = new ManaCostShard[] { U, B };
|
||||
shardOrderLookup[BLUE | RED] = new ManaCostShard[] { U, R };
|
||||
shardOrderLookup[BLACK | RED] = new ManaCostShard[] { B, R };
|
||||
shardOrderLookup[BLACK | GREEN] = new ManaCostShard[] { B, G };
|
||||
shardOrderLookup[RED | GREEN] = new ManaCostShard[] { R, G };
|
||||
shardOrderLookup[RED | WHITE] = new ManaCostShard[] { R, W };
|
||||
shardOrderLookup[GREEN | WHITE] = new ManaCostShard[] { G, W };
|
||||
shardOrderLookup[GREEN | BLUE] = new ManaCostShard[] { G, U };
|
||||
|
||||
//three-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK] = new ManaCostShard[] { W, U, B };
|
||||
shardOrderLookup[WHITE | BLACK | GREEN] = new ManaCostShard[] { W, B, G };
|
||||
shardOrderLookup[BLUE | BLACK | RED] = new ManaCostShard[] { U, B, R };
|
||||
shardOrderLookup[BLUE | RED | WHITE] = new ManaCostShard[] { U, R, W };
|
||||
shardOrderLookup[BLACK | RED | GREEN] = new ManaCostShard[] { B, R, G };
|
||||
shardOrderLookup[BLACK | GREEN | BLUE] = new ManaCostShard[] { B, G, U };
|
||||
shardOrderLookup[RED | GREEN | WHITE] = new ManaCostShard[] { R, G, W };
|
||||
shardOrderLookup[RED | WHITE | BLACK] = new ManaCostShard[] { R, W, B };
|
||||
shardOrderLookup[GREEN | WHITE | BLUE] = new ManaCostShard[] { G, W, U };
|
||||
shardOrderLookup[GREEN | BLUE | RED] = new ManaCostShard[] { G, U, R };
|
||||
|
||||
//four-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK | RED] = new ManaCostShard[] { W, U, B, R };
|
||||
shardOrderLookup[BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { U, B, R, G };
|
||||
shardOrderLookup[BLACK | RED | GREEN | WHITE] = new ManaCostShard[] { B, R, G, W };
|
||||
shardOrderLookup[RED | GREEN | WHITE | BLUE] = new ManaCostShard[] { R, G, W, U };
|
||||
shardOrderLookup[GREEN | WHITE | BLUE | BLACK] = new ManaCostShard[] { G, W, U, B };
|
||||
|
||||
//five-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { W, U, B, R, G };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package forge.card;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import forge.util.Localizer;
|
||||
import forge.deck.DeckRecognizer;
|
||||
|
||||
/**
|
||||
* Holds byte values for each color magic has.
|
||||
@@ -158,23 +158,20 @@ public final class MagicColor {
|
||||
}
|
||||
|
||||
public enum Color {
|
||||
WHITE(Constant.WHITE, MagicColor.WHITE, "W", "lblWhite"),
|
||||
BLUE(Constant.BLUE, MagicColor.BLUE, "U", "lblBlue"),
|
||||
BLACK(Constant.BLACK, MagicColor.BLACK, "B", "lblBlack"),
|
||||
RED(Constant.RED, MagicColor.RED, "R", "lblRed"),
|
||||
GREEN(Constant.GREEN, MagicColor.GREEN, "G", "lblGreen"),
|
||||
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "C", "lblColorless");
|
||||
WHITE(Constant.WHITE, MagicColor.WHITE, "{W}"),
|
||||
BLUE(Constant.BLUE, MagicColor.BLUE, "{U}"),
|
||||
BLACK(Constant.BLACK, MagicColor.BLACK, "{B}"),
|
||||
RED(Constant.RED, MagicColor.RED, "{R}"),
|
||||
GREEN(Constant.GREEN, MagicColor.GREEN, "{G}"),
|
||||
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "{C}");
|
||||
|
||||
private final String name, shortName, symbol;
|
||||
private final String label;
|
||||
private final String name, symbol;
|
||||
private final byte colormask;
|
||||
|
||||
Color(String name0, byte colormask0, String shortName, String label) {
|
||||
Color(String name0, byte colormask0, String symbol0) {
|
||||
name = name0;
|
||||
colormask = colormask0;
|
||||
this.shortName = shortName;
|
||||
symbol = "{" + shortName + "}";
|
||||
this.label = label;
|
||||
symbol = symbol0;
|
||||
}
|
||||
|
||||
public static Color fromByte(final byte color) {
|
||||
@@ -191,15 +188,13 @@ public final class MagicColor {
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
}
|
||||
|
||||
public String getLocalizedName() {
|
||||
return Localizer.getInstance().getMessage(label);
|
||||
//Should probably move some of this logic back here, or at least to a more general location.
|
||||
return DeckRecognizer.getLocalisedMagicColorName(getName());
|
||||
}
|
||||
|
||||
public byte getColorMask() {
|
||||
public byte getColormask() {
|
||||
return colormask;
|
||||
}
|
||||
public String getSymbol() {
|
||||
@@ -207,7 +202,7 @@ public final class MagicColor {
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return getLocalizedName();
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -49,16 +49,6 @@ public class DeckRecognizer {
|
||||
LIMITED_CARD,
|
||||
CARD_FROM_NOT_ALLOWED_SET,
|
||||
CARD_FROM_INVALID_SET,
|
||||
/**
|
||||
* Valid card request, but can't be imported because the player does not have enough copies.
|
||||
* Should be replaced with a different printing if possible.
|
||||
*/
|
||||
CARD_NOT_IN_INVENTORY,
|
||||
/**
|
||||
* Valid card request for a card that isn't in the player's inventory, but new copies can be acquired freely.
|
||||
* Usually used for basic lands. Should be supplied to the import controller by the editor.
|
||||
*/
|
||||
FREE_CARD_NOT_IN_INVENTORY,
|
||||
// Warning messages
|
||||
WARNING_MESSAGE,
|
||||
UNKNOWN_CARD,
|
||||
@@ -73,11 +63,7 @@ public class DeckRecognizer {
|
||||
CARD_TYPE,
|
||||
CARD_RARITY,
|
||||
CARD_CMC,
|
||||
MANA_COLOUR;
|
||||
|
||||
public static final EnumSet<TokenType> CARD_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY);
|
||||
public static final EnumSet<TokenType> IN_DECK_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, DECK_NAME, FREE_CARD_NOT_IN_INVENTORY);
|
||||
public static final EnumSet<TokenType> CARD_PLACEHOLDER_TOKEN_TYPES = EnumSet.of(CARD_TYPE, CARD_RARITY, CARD_CMC, MANA_COLOUR);
|
||||
MANA_COLOUR
|
||||
}
|
||||
|
||||
public enum LimitedCardType{
|
||||
@@ -122,10 +108,6 @@ public class DeckRecognizer {
|
||||
return new Token(TokenType.CARD_FROM_INVALID_SET, count, card, cardRequestHasSetCode);
|
||||
}
|
||||
|
||||
public static Token NotInInventoryFree(final PaperCard card, final int count, final DeckSection section) {
|
||||
return new Token(TokenType.FREE_CARD_NOT_IN_INVENTORY, count, card, section, true);
|
||||
}
|
||||
|
||||
// WARNING MESSAGES
|
||||
// ================
|
||||
public static Token UnknownCard(final String cardName, final String setCode, final int count) {
|
||||
@@ -144,10 +126,6 @@ public class DeckRecognizer {
|
||||
return new Token(TokenType.WARNING_MESSAGE, msg);
|
||||
}
|
||||
|
||||
public static Token NotInInventory(final PaperCard card, final int count, final DeckSection section) {
|
||||
return new Token(TokenType.CARD_NOT_IN_INVENTORY, count, card, section, false);
|
||||
}
|
||||
|
||||
/* =================================
|
||||
* DECK SECTIONS
|
||||
* ================================= */
|
||||
@@ -261,11 +239,14 @@ public class DeckRecognizer {
|
||||
/**
|
||||
* Filters all token types that have a PaperCard instance set (not null)
|
||||
* @return true for tokens of type:
|
||||
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY.
|
||||
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET.
|
||||
* False otherwise.
|
||||
*/
|
||||
public boolean isCardToken() {
|
||||
return TokenType.CARD_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.LEGAL_CARD ||
|
||||
this.type == TokenType.LIMITED_CARD ||
|
||||
this.type == TokenType.CARD_FROM_NOT_ALLOWED_SET ||
|
||||
this.type == TokenType.CARD_FROM_INVALID_SET);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,7 +255,9 @@ public class DeckRecognizer {
|
||||
* LEGAL_CARD, LIMITED_CARD, DECK_NAME; false otherwise.
|
||||
*/
|
||||
public boolean isTokenForDeck() {
|
||||
return TokenType.IN_DECK_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.LEGAL_CARD ||
|
||||
this.type == TokenType.LIMITED_CARD ||
|
||||
this.type == TokenType.DECK_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +266,7 @@ public class DeckRecognizer {
|
||||
* False otherwise.
|
||||
*/
|
||||
public boolean isCardTokenForDeck() {
|
||||
return isCardToken() && isTokenForDeck();
|
||||
return (this.type == TokenType.LEGAL_CARD || this.type == TokenType.LIMITED_CARD);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,7 +276,10 @@ public class DeckRecognizer {
|
||||
* CARD_RARITY, CARD_CMC, CARD_TYPE, MANA_COLOUR
|
||||
*/
|
||||
public boolean isCardPlaceholder(){
|
||||
return TokenType.CARD_PLACEHOLDER_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.CARD_RARITY ||
|
||||
this.type == TokenType.CARD_CMC ||
|
||||
this.type == TokenType.MANA_COLOUR ||
|
||||
this.type == TokenType.CARD_TYPE);
|
||||
}
|
||||
|
||||
/** Determines if current token is a Deck Section token
|
||||
@@ -550,7 +536,7 @@ public class DeckRecognizer {
|
||||
PaperCard tokenCard = token.getCard();
|
||||
|
||||
if (isAllowed(tokenSection)) {
|
||||
if (tokenSection != referenceDeckSectionInParsing) {
|
||||
if (!tokenSection.equals(referenceDeckSectionInParsing)) {
|
||||
Token sectionToken = Token.DeckSection(tokenSection.name(), this.allowedDeckSections);
|
||||
// just check that last token is stack is a card placeholder.
|
||||
// In that case, add the new section token before the placeholder
|
||||
@@ -589,7 +575,7 @@ public class DeckRecognizer {
|
||||
refLine = purgeAllLinks(refLine);
|
||||
|
||||
String line;
|
||||
if (refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER))
|
||||
if (StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER))
|
||||
line = refLine.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, "");
|
||||
else
|
||||
line = refLine.trim(); // Remove any trailing formatting
|
||||
@@ -598,7 +584,7 @@ public class DeckRecognizer {
|
||||
// Final fantasy cards like Summon: Choco/Mog should be ommited to be recognized. TODO: fix maybe for future cards
|
||||
if (!line.contains("Summon:"))
|
||||
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
|
||||
if (line.startsWith(ASTERISK)) // Markdown lists (tappedout md export)
|
||||
if (StringUtils.startsWith(line, ASTERISK)) // markdown lists (tappedout md export)
|
||||
line = line.substring(2);
|
||||
|
||||
// == Patches to Corner Cases
|
||||
@@ -614,8 +600,8 @@ public class DeckRecognizer {
|
||||
Token result = recogniseCardToken(line, referenceSection);
|
||||
if (result == null)
|
||||
result = recogniseNonCardToken(line);
|
||||
return result != null ? result : refLine.startsWith(DOUBLE_SLASH) ||
|
||||
refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
|
||||
return result != null ? result : StringUtils.startsWith(refLine, DOUBLE_SLASH) ||
|
||||
StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
|
||||
new Token(TokenType.COMMENT, 0, refLine) : new Token(TokenType.UNKNOWN_TEXT, 0, refLine);
|
||||
}
|
||||
|
||||
@@ -627,7 +613,7 @@ public class DeckRecognizer {
|
||||
while (m.find()) {
|
||||
line = line.replaceAll(m.group(), "").trim();
|
||||
}
|
||||
if (line.endsWith("()"))
|
||||
if (StringUtils.endsWith(line, "()"))
|
||||
return line.substring(0, line.length()-2);
|
||||
return line;
|
||||
}
|
||||
@@ -755,12 +741,21 @@ public class DeckRecognizer {
|
||||
// This would save tons of time in parsing Input + would also allow to return UnsupportedCardTokens beforehand
|
||||
private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection, PaperCard card){
|
||||
if (deckSec != null) {
|
||||
DeckSection cardSection = switch (deckSec.toUpperCase().trim()) {
|
||||
case "MB" -> DeckSection.Main;
|
||||
case "SB" -> DeckSection.Sideboard;
|
||||
case "CM" -> DeckSection.Commander;
|
||||
default -> DeckSection.matchingSection(card);
|
||||
};
|
||||
DeckSection cardSection;
|
||||
switch (deckSec.toUpperCase().trim()) {
|
||||
case "MB":
|
||||
cardSection = DeckSection.Main;
|
||||
break;
|
||||
case "SB":
|
||||
cardSection = DeckSection.Sideboard;
|
||||
break;
|
||||
case "CM":
|
||||
cardSection = DeckSection.Commander;
|
||||
break;
|
||||
default:
|
||||
cardSection = DeckSection.matchingSection(card);
|
||||
break;
|
||||
}
|
||||
if (cardSection.validate(card))
|
||||
return cardSection;
|
||||
}
|
||||
@@ -1015,28 +1010,58 @@ public class DeckRecognizer {
|
||||
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
|
||||
String localisedName1 = magicColor1.getLocalizedName();
|
||||
String localisedName2 = magicColor2.getLocalizedName();
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColorMask() | magicColor2.getColorMask());
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
|
||||
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
|
||||
}
|
||||
|
||||
private static MagicColor.Color getMagicColor(String colorName){
|
||||
if (colorName.toLowerCase().startsWith("multi") || colorName.equalsIgnoreCase("m"))
|
||||
return null; // will be handled separately
|
||||
return MagicColor.Color.fromByte(MagicColor.fromName(colorName.toLowerCase()));
|
||||
|
||||
byte color = MagicColor.fromName(colorName.toLowerCase());
|
||||
switch (color) {
|
||||
case MagicColor.WHITE:
|
||||
return MagicColor.Color.WHITE;
|
||||
case MagicColor.BLUE:
|
||||
return MagicColor.Color.BLUE;
|
||||
case MagicColor.BLACK:
|
||||
return MagicColor.Color.BLACK;
|
||||
case MagicColor.RED:
|
||||
return MagicColor.Color.RED;
|
||||
case MagicColor.GREEN:
|
||||
return MagicColor.Color.GREEN;
|
||||
default:
|
||||
return MagicColor.Color.COLORLESS;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLocalisedMagicColorName(String colorName){
|
||||
Localizer localizer = Localizer.getInstance();
|
||||
return switch (colorName.toLowerCase()) {
|
||||
case MagicColor.Constant.WHITE -> localizer.getMessage("lblWhite");
|
||||
case MagicColor.Constant.BLUE -> localizer.getMessage("lblBlue");
|
||||
case MagicColor.Constant.BLACK -> localizer.getMessage("lblBlack");
|
||||
case MagicColor.Constant.RED -> localizer.getMessage("lblRed");
|
||||
case MagicColor.Constant.GREEN -> localizer.getMessage("lblGreen");
|
||||
case MagicColor.Constant.COLORLESS -> localizer.getMessage("lblColorless");
|
||||
case "multicolour", "multicolor" -> localizer.getMessage("lblMulticolor");
|
||||
default -> "";
|
||||
};
|
||||
switch(colorName.toLowerCase()){
|
||||
case MagicColor.Constant.WHITE:
|
||||
return localizer.getMessage("lblWhite");
|
||||
|
||||
case MagicColor.Constant.BLUE:
|
||||
return localizer.getMessage("lblBlue");
|
||||
|
||||
case MagicColor.Constant.BLACK:
|
||||
return localizer.getMessage("lblBlack");
|
||||
|
||||
case MagicColor.Constant.RED:
|
||||
return localizer.getMessage("lblRed");
|
||||
|
||||
case MagicColor.Constant.GREEN:
|
||||
return localizer.getMessage("lblGreen");
|
||||
|
||||
case MagicColor.Constant.COLORLESS:
|
||||
return localizer.getMessage("lblColorless");
|
||||
case "multicolour":
|
||||
case "multicolor":
|
||||
return localizer.getMessage("lblMulticolor");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1055,6 +1080,37 @@ public class DeckRecognizer {
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
private static Pair<String, String> getManaNameAndSymbol(String matchedMana) {
|
||||
if (matchedMana == null)
|
||||
return null;
|
||||
|
||||
Localizer localizer = Localizer.getInstance();
|
||||
switch (matchedMana.toLowerCase()) {
|
||||
case MagicColor.Constant.WHITE:
|
||||
case "w":
|
||||
return Pair.of(localizer.getMessage("lblWhite"), MagicColor.Color.WHITE.getSymbol());
|
||||
case MagicColor.Constant.BLUE:
|
||||
case "u":
|
||||
return Pair.of(localizer.getMessage("lblBlue"), MagicColor.Color.BLUE.getSymbol());
|
||||
case MagicColor.Constant.BLACK:
|
||||
case "b":
|
||||
return Pair.of(localizer.getMessage("lblBlack"), MagicColor.Color.BLACK.getSymbol());
|
||||
case MagicColor.Constant.RED:
|
||||
case "r":
|
||||
return Pair.of(localizer.getMessage("lblRed"), MagicColor.Color.RED.getSymbol());
|
||||
case MagicColor.Constant.GREEN:
|
||||
case "g":
|
||||
return Pair.of(localizer.getMessage("lblGreen"), MagicColor.Color.GREEN.getSymbol());
|
||||
case MagicColor.Constant.COLORLESS:
|
||||
case "c":
|
||||
return Pair.of(localizer.getMessage("lblColorless"), MagicColor.Color.COLORLESS.getSymbol());
|
||||
default: // Multicolour
|
||||
return Pair.of(localizer.getMessage("lblMulticolor"), "");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDeckName(final String lineAsIs) {
|
||||
if (lineAsIs == null)
|
||||
return false;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -593,7 +567,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
|
||||
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
|
||||
if(markedColors == null)
|
||||
markedColors = ColorSet.NO_COLORS;
|
||||
markedColors = ColorSet.getNullColor();
|
||||
return new PaperCardFlags(this, markedColors, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
return false;
|
||||
CardSplitType cst = this.cardRules.getSplitType();
|
||||
//expand this on future for other tokens that has other backsides besides transform..
|
||||
return cst == CardSplitType.Transform || cst == CardSplitType.Modal;
|
||||
return cst == CardSplitType.Transform;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -633,10 +633,7 @@ public class BoosterGenerator {
|
||||
System.out.println("Parsing from main code: " + mainCode);
|
||||
String sheetName = StringUtils.strip(mainCode.substring(10), "()\" ");
|
||||
System.out.println("Attempting to lookup: " + sheetName);
|
||||
PrintSheet fromSheet = tryGetStaticSheet(sheetName);
|
||||
if (fromSheet == null)
|
||||
throw new RuntimeException("PrintSheet Error: " + ps.getName() + " didn't find " + sheetName + " from " + mainCode);
|
||||
src = fromSheet.toFlatList();
|
||||
src = tryGetStaticSheet(sheetName).toFlatList();
|
||||
setPred = x -> true;
|
||||
|
||||
} else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet
|
||||
|
||||
@@ -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) {
|
||||
// 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")) {
|
||||
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);
|
||||
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 removeIf(Predicate<T> test) {
|
||||
for (final T item : items.keySet()) {
|
||||
if (test.test(item))
|
||||
remove(item);
|
||||
}
|
||||
|
||||
public void retainIf(Predicate<T> filter) {
|
||||
items.keySet().removeIf(filter.negate());
|
||||
}
|
||||
|
||||
public T find(Predicate<T> filter) {
|
||||
return items.keySet().stream().filter(filter).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
@@ -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>8.18.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ForgeScript {
|
||||
boolean withSource = property.endsWith("Source");
|
||||
final ColorSet colors;
|
||||
if (withSource && StaticAbilityColorlessDamageSource.colorlessDamageSource(cardState)) {
|
||||
colors = ColorSet.NO_COLORS;
|
||||
colors = ColorSet.getNullColor();
|
||||
} else {
|
||||
colors = cardState.getCard().getColor(cardState);
|
||||
}
|
||||
@@ -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")) {
|
||||
@@ -412,8 +410,6 @@ public class ForgeScript {
|
||||
return !sa.isPwAbility() && !sa.getRestrictions().isSorcerySpeed();
|
||||
}
|
||||
return true;
|
||||
} else if(property.startsWith("NamedAbility")) {
|
||||
return sa.getName().equals(property.substring(12));
|
||||
} else if (sa.getHostCard() != null) {
|
||||
return sa.getHostCard().hasProperty(property, sourceController, source, spellAbility);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -845,8 +858,6 @@ public class Game {
|
||||
p.revealFaceDownCards();
|
||||
}
|
||||
|
||||
// TODO free any mindslaves
|
||||
|
||||
for (Card c : cards) {
|
||||
// CR 800.4d if card is controlled by opponent, LTB should trigger
|
||||
if (c.getOwner().equals(p) && c.getController().equals(p)) {
|
||||
@@ -882,6 +893,8 @@ public class Game {
|
||||
}
|
||||
triggerList.put(c.getZone().getZoneType(), null, c);
|
||||
getAction().ceaseToExist(c, false);
|
||||
// CR 603.2f owner of trigger source lost game
|
||||
getTriggerHandler().clearDelayedTrigger(c);
|
||||
}
|
||||
} else {
|
||||
// return stolen permanents
|
||||
|
||||
@@ -220,6 +220,10 @@ public class GameAction {
|
||||
//copied.setGamePieceType(GamePieceType.COPIED_SPELL);
|
||||
}
|
||||
|
||||
if (c.isTransformed()) {
|
||||
copied.incrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
|
||||
copied.setCastSA(cause);
|
||||
copied.setSplitStateToPlayAbility(cause);
|
||||
@@ -970,7 +974,6 @@ public class GameAction {
|
||||
// in some corner cases there's no zone yet (copied spell that failed targeting)
|
||||
if (z != null) {
|
||||
z.remove(c);
|
||||
c.setZone(c.getOwner().getZone(ZoneType.None));
|
||||
if (z.is(ZoneType.Battlefield)) {
|
||||
c.runLeavesPlayCommands();
|
||||
}
|
||||
@@ -1819,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) {
|
||||
@@ -1840,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) {
|
||||
@@ -2219,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;
|
||||
@@ -178,6 +166,18 @@ public final class GameActionUtil {
|
||||
}
|
||||
|
||||
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback));
|
||||
} else if (keyword.startsWith("Mayhem")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if source has No Mana cost, and Mayhem doesn't have own one,
|
||||
// Mayhem can't work
|
||||
if (keyword.equals("Mayhem") && source.getManaCost().isNoCost()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
@@ -242,7 +242,6 @@ public final class GameActionUtil {
|
||||
}
|
||||
stackCopy.setLastKnownZone(game.getStackZone());
|
||||
stackCopy.setCastFrom(oldZone);
|
||||
stackCopy.setCastSA(sa);
|
||||
lkicheck = true;
|
||||
|
||||
stackCopy.clearStaticChangedCardKeywords(false);
|
||||
@@ -993,6 +992,9 @@ public final class GameActionUtil {
|
||||
oldCard.setBackSide(false);
|
||||
oldCard.setState(oldCard.getFaceupCardStateName(), true);
|
||||
oldCard.unanimateBestow();
|
||||
if (ability.isDisturb() || ability.hasParam("CastTransformed")) {
|
||||
oldCard.undoIncrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
if (ability.hasParam("Prototype")) {
|
||||
oldCard.removeCloneState(oldCard.getPrototypeTimestamp());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -202,6 +202,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);
|
||||
}
|
||||
@@ -239,10 +248,6 @@ public final class AbilityFactory {
|
||||
spellAbility.putParam("PrecostDesc", "Exhaust — ");
|
||||
}
|
||||
|
||||
if (mapParams.containsKey("Named")) {
|
||||
spellAbility.setName(mapParams.get("Named"));
|
||||
}
|
||||
|
||||
// *********************************************
|
||||
// set universal properties of the SpellAbility
|
||||
|
||||
@@ -363,6 +368,9 @@ public final class AbilityFactory {
|
||||
if (mapParams.containsKey("TargetUnique")) {
|
||||
abTgt.setUniqueTargets(true);
|
||||
}
|
||||
if (mapParams.containsKey("TargetsFromSingleZone")) {
|
||||
abTgt.setSingleZone(true);
|
||||
}
|
||||
if (mapParams.containsKey("TargetsWithoutSameCreatureType")) {
|
||||
abTgt.setWithoutSameCreatureType(true);
|
||||
}
|
||||
@@ -384,9 +392,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);
|
||||
|
||||
@@ -1870,14 +1877,6 @@ public class AbilityUtils {
|
||||
}
|
||||
return doXMath(v, expr, c, ctb);
|
||||
}
|
||||
|
||||
// Count$FromNamedAbility[abilityName].<True>.<False>
|
||||
if (sq[0].startsWith("FromNamedAbility")) {
|
||||
String abilityNamed = sq[0].substring(16);
|
||||
SpellAbility trigSA = sa.getHostCard().getCastSA();
|
||||
boolean fromNamedAbility = trigSA != null && trigSA.getName().equals(abilityNamed);
|
||||
return doXMath(calculateAmount(c, sq[fromNamedAbility ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
} else {
|
||||
// fallback if ctb isn't a spellability
|
||||
if (sq[0].startsWith("LastStateBattlefield")) {
|
||||
@@ -2346,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);
|
||||
|
||||
@@ -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),
|
||||
@@ -82,7 +81,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -90,16 +90,6 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
|
||||
c.addPerpetual(p);
|
||||
p.applyEffect(c);
|
||||
}
|
||||
if (sa.hasParam("ManaCost")) {
|
||||
final ManaCost manaCost = new ManaCost(new ManaCostParser(sa.getParam("ManaCost")));
|
||||
if (perpetual) {
|
||||
PerpetualManaCost p = new PerpetualManaCost(timestamp, manaCost);
|
||||
c.addPerpetual(p);
|
||||
p.applyEffect(c);
|
||||
} else {
|
||||
c.addChangedManaCost(manaCost, timestamp, (long) 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addType.isEmpty() || !removeType.isEmpty() || addAllCreatureTypes || !remove.isEmpty()) {
|
||||
if (perpetual) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetChoices;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.Localizer;
|
||||
|
||||
@@ -28,13 +27,6 @@ import forge.util.Localizer;
|
||||
*/
|
||||
public class ChangeTargetsEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.getTargetRestrictions().setZone(ZoneType.Stack);
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityEffect#resolve(forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.google.common.collect.Iterables;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameActionUtil;
|
||||
import forge.game.GameEntityCounterTable;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
@@ -22,12 +21,6 @@ import forge.util.Localizer;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
public class ChangeZoneAllEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
// TODO build Stack Description will need expansion as more cards are added
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.google.common.collect.Maps;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.CardType;
|
||||
import forge.game.*;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
@@ -19,6 +18,7 @@ import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.Zone;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -34,11 +34,6 @@ import java.util.Map;
|
||||
|
||||
public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
if (sa.isHidden()) {
|
||||
@@ -764,7 +759,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
StringBuilder sbPlay = new StringBuilder();
|
||||
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand+!ThisTurnEntered");
|
||||
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
|
||||
eff.addStaticAbility(sbPlay.toString());
|
||||
final StaticAbility st = eff.addStaticAbility(sbPlay.toString());
|
||||
eff.addRemembered(movedCard);
|
||||
addForgetOnMovedTrigger(eff, "Exile");
|
||||
addForgetOnCastTrigger(eff, "Card.IsRemembered");
|
||||
@@ -928,7 +923,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
|
||||
List<ZoneType> origin = Lists.newArrayList();
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin.addAll(ZoneType.listValueOf(sa.getParam("Origin")));
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
}
|
||||
ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
|
||||
@@ -969,11 +964,13 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
String prompt;
|
||||
if (sa.hasParam("OptionalPrompt")) {
|
||||
prompt = sa.getParam("OptionalPrompt");
|
||||
} else if (defined) {
|
||||
} else {
|
||||
if (defined) {
|
||||
prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase(), destination.getTranslatedName().toLowerCase());
|
||||
} else {
|
||||
prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase());
|
||||
}
|
||||
}
|
||||
String message = MessageUtil.formatMessage(prompt , decider, player);
|
||||
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) {
|
||||
continue;
|
||||
@@ -1474,7 +1471,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
if (ZoneType.Exile.equals(destination) && sa.hasParam("WithCountersType")) {
|
||||
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
|
||||
int cAmount = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("WithCountersAmount", "1"), sa);
|
||||
int cAmount = AbilityUtils.calculateAmount(sa.getOriginalHost(), sa.getParamOrDefault("WithCountersAmount", "1"), sa);
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
movedCard.addCounter(cType, cAmount, player, table);
|
||||
table.replaceCounterEffect(game, sa, true);
|
||||
|
||||
@@ -10,17 +10,8 @@ import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class ControlSpellEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.getTargetRestrictions().setZone(ZoneType.Stack);
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
|
||||
@@ -287,17 +287,22 @@ public class CopyPermanentEffect extends TokenEffectBase {
|
||||
int id = newOwner == null ? 0 : newOwner.getGame().nextCardId();
|
||||
// need to create a physical card first, i need the original card faces
|
||||
copy = CardFactory.getCard(original.getPaperCard(), newOwner, id, host.getGame());
|
||||
|
||||
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
// 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield,
|
||||
// the resulting token is a transforming token that has both a front face and a back face.
|
||||
// The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
|
||||
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up.
|
||||
// This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
|
||||
copy.setBackSide(original.isBackSide());
|
||||
if (original.isTransformed()) {
|
||||
copy.incrementTransformedTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
} else {
|
||||
copy.setState(copy.getCurrentStateName(), true, true);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,6 @@ import java.util.Map;
|
||||
|
||||
|
||||
public class CopySpellAbilityEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.getTargetRestrictions().setZone(ZoneType.Stack);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
|
||||
@@ -21,13 +21,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CounterEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.getTargetRestrictions().setZone(ZoneType.Stack);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -102,29 +102,24 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
|
||||
int totalRemoved = 0;
|
||||
CardCollectionView srcCards;
|
||||
|
||||
if (sa.hasParam("Choices")) {
|
||||
ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone"))
|
||||
: ZoneType.Battlefield;
|
||||
srcCards = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"),
|
||||
activator, source, sa);
|
||||
} else {
|
||||
srcCards = getTargetCards(sa);
|
||||
}
|
||||
if (sa.isReplacementAbility()) {
|
||||
srcCards = new CardCollection(srcCards).filter(c -> !c.isInPlay() || sa.getLastStateBattlefield().contains(c));
|
||||
}
|
||||
|
||||
if (sa.hasParam("Choices")) {
|
||||
CardCollection choices = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"),
|
||||
activator, source, sa);
|
||||
|
||||
int min = 1;
|
||||
int max = 1;
|
||||
if (sa.hasParam("ChoiceOptional")) {
|
||||
min = 0;
|
||||
max = srcCards.size();
|
||||
max = choices.size();
|
||||
}
|
||||
if (sa.hasParam("ChoiceNum")) {
|
||||
min = max = AbilityUtils.calculateAmount(source, sa.getParam("ChoiceNum"), sa);
|
||||
}
|
||||
if (srcCards.size() < min) {
|
||||
if (choices.size() < min) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,12 +128,13 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
title = title.replace(" ", " ");
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("CounterType", counterType);
|
||||
srcCards = pc.chooseCardsForEffect(srcCards, sa, title, min, max, min == 0, params);
|
||||
srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params);
|
||||
} else {
|
||||
for (final Player tgtPlayer : getTargetPlayers(sa)) {
|
||||
if (!tgtPlayer.isInGame()) {
|
||||
continue;
|
||||
}
|
||||
// Removing energy
|
||||
if (type.equals("All")) {
|
||||
for (Map.Entry<CounterType, Integer> e : Lists.newArrayList(tgtPlayer.getCounters().entrySet())) {
|
||||
totalRemoved += tgtPlayer.subtractCounter(e.getKey(), e.getValue(), activator);
|
||||
@@ -154,6 +150,8 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srcCards = getTargetCards(sa);
|
||||
}
|
||||
|
||||
for (final Card tgtCard : srcCards) {
|
||||
|
||||
@@ -18,10 +18,6 @@ public class DamageResolveEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
CardDamageMap damageMap = sa.getDamageMap();
|
||||
if (damageMap == null) {
|
||||
// this can happen if damagesource was missing
|
||||
return;
|
||||
}
|
||||
CardDamageMap preventMap = sa.getPreventMap();
|
||||
GameEntityCounterTable counterTable = sa.getCounterTable();
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import forge.card.RemoveType;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntityCounterTable;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCopyService;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerHandler;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.util.Lang;
|
||||
|
||||
public class EarthbendEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
final StringBuilder sb = new StringBuilder("Earthbend ");
|
||||
final Card card = sa.getHostCard();
|
||||
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
|
||||
|
||||
sb.append(amount).append(". (Target land you control becomes a 0/0 creature with haste that's still a land. Put ");
|
||||
sb.append(Lang.nounWithNumeral(amount, "+1/+1 counter"));
|
||||
sb.append(" on it. When it dies or is exiled, return it to the battlefield tapped.)");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSpellAbility(final SpellAbility sa) {
|
||||
TargetRestrictions abTgt = new TargetRestrictions("Select target land you control", "Land.YouCtrl".split(","), "1", "1");
|
||||
sa.setTargetRestrictions(abTgt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
final Player pl = sa.getActivatingPlayer();
|
||||
int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa);
|
||||
|
||||
long ts = game.getNextTimestamp();
|
||||
|
||||
String desc = "When it dies or is exiled, return it to the battlefield tapped.";
|
||||
String sbTrigA = "Mode$ ChangesZone | ValidCard$ Card.IsTriggerRemembered | Origin$ Battlefield | Destination$ Graveyard | TriggerDescription$ " + desc;
|
||||
String sbTrigB = "Mode$ Exiled | Origin$ Battlefield | ValidCard$ Card.IsTriggerRemembered | TriggerZones$ Battlefield | TriggerDescription$ " + desc;
|
||||
|
||||
// Earthbend should only target one land
|
||||
for (Card c : getTargetCards(sa)) {
|
||||
c.addNewPT(0, 0, ts, 0);
|
||||
c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false);
|
||||
c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null);
|
||||
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
c.addCounter(CounterEnumType.P1P1, num, pl, table);
|
||||
table.replaceCounterEffect(game, sa, true);
|
||||
|
||||
buildTrigger(sa, c, sbTrigA, "Graveyard");
|
||||
buildTrigger(sa, c, sbTrigB, "Exile");
|
||||
}
|
||||
pl.triggerElementalBend(TriggerType.Earthbend);
|
||||
}
|
||||
|
||||
protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
String trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ " + zone + " | Destination$ Battlefield | Tapped$ True";
|
||||
|
||||
final Trigger trig = TriggerHandler.parseTrigger(sbTrig, CardCopyService.getLKICopy(source), sa.isIntrinsic());
|
||||
final SpellAbility newSa = AbilityFactory.getAbility(trigSA, sa.getHostCard());
|
||||
newSa.setIntrinsic(sa.isIntrinsic());
|
||||
trig.addRemembered(c);
|
||||
trig.setOverridingAbility(newSa);
|
||||
trig.setSpawningAbility(sa.copy(sa.getHostCard(), true));
|
||||
trig.setKeyword(trig.getSpawningAbility().getKeyword());
|
||||
|
||||
game.getTriggerHandler().registerDelayedTrigger(trig);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import java.util.Map;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ImageKeys;
|
||||
import forge.StaticData;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
@@ -143,20 +142,15 @@ public class EffectEffect extends SpellAbilityEffect {
|
||||
}
|
||||
|
||||
String image;
|
||||
if (name.startsWith("Emblem")) {
|
||||
if (sa.hasParam("Image")) {
|
||||
image = StaticData.instance().getOtherImageKey(sa.getParam("Image"), hostCard.getSetCode());
|
||||
} else {
|
||||
// try to get the image from name
|
||||
String imageKey = TextUtil.fastReplace(
|
||||
image = ImageKeys.getTokenKey(sa.getParam("Image"));
|
||||
} else if (name.startsWith("Emblem")) { // try to get the image from name
|
||||
image = ImageKeys.getTokenKey(
|
||||
TextUtil.fastReplace(
|
||||
TextUtil.fastReplace(
|
||||
TextUtil.fastReplace(name.toLowerCase(), " — ", "_"),
|
||||
",", ""),
|
||||
" ", "_");
|
||||
image = StaticData.instance().getOtherImageKey(imageKey, hostCard.getSetCode());
|
||||
}
|
||||
} else if (sa.hasParam("Image")) {
|
||||
image = ImageKeys.getTokenKey(sa.getParam("Image"));
|
||||
" ", "_").toLowerCase());
|
||||
} else { // use host image
|
||||
image = hostCard.getImageKey();
|
||||
}
|
||||
|
||||
@@ -34,9 +34,6 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect {
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Game game = host.getGame();
|
||||
|
||||
// CR 603.12a if the trigger event or events occur multiple times during the resolution of the spell or ability that created it,
|
||||
// the reflexive triggered ability will trigger once for each of those times
|
||||
int amt = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("TriggerAmount", "1"), sa);
|
||||
if (amt <= 0) {
|
||||
return;
|
||||
|
||||
@@ -54,29 +54,33 @@ public class LifeExchangeEffect extends SpellAbilityEffect {
|
||||
|
||||
final int life1 = p1.getLife();
|
||||
final int life2 = p2.getLife();
|
||||
final int diff = Math.abs(life1 - life2);
|
||||
|
||||
if (life2 > life1) {
|
||||
// swap players
|
||||
Player tmp = p2;
|
||||
p2 = p1;
|
||||
p1 = tmp;
|
||||
if (sa.hasParam("RememberDifference")) {
|
||||
final int diff = life1 - life2;
|
||||
source.addRemembered(diff);
|
||||
}
|
||||
if (diff > 0 && p1.canLoseLife() && p2.canGainLife()) {
|
||||
|
||||
final Map<Player, Integer> lossMap = Maps.newHashMap();
|
||||
if ((life1 > life2) && p1.canLoseLife() && p2.canGainLife()) {
|
||||
final int diff = life1 - life2;
|
||||
final int lost = p1.loseLife(diff, false, false);
|
||||
p2.gainLife(diff, source, sa);
|
||||
if (lost > 0) {
|
||||
final Map<Player, Integer> lossMap = Maps.newHashMap();
|
||||
lossMap.put(p1, lost);
|
||||
}
|
||||
} else if ((life2 > life1) && p2.canLoseLife() && p1.canGainLife()) {
|
||||
final int diff = life2 - life1;
|
||||
final int lost = p2.loseLife(diff, false, false);
|
||||
p1.gainLife(diff, source, sa);
|
||||
if (lost > 0) {
|
||||
lossMap.put(p2, lost);
|
||||
}
|
||||
} else {
|
||||
// they are equal or can't be exchanged, so nothing to do
|
||||
}
|
||||
if (!lossMap.isEmpty()) { // Run triggers if any player actually lost life
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPIMap(lossMap);
|
||||
source.getGame().getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false);
|
||||
if (sa.hasParam("RememberOwnLoss") && p1.equals(sa.getActivatingPlayer())) {
|
||||
source.addRemembered(lost);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sa.hasParam("RememberDifference")) {
|
||||
source.addRemembered(p1.getLife() - p2.getLife());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
|
||||
if (source.hasNamedCard()) {
|
||||
names.addAll(source.getNamedCards());
|
||||
} else {
|
||||
System.err.println("Malformed MakeCard entry! - " + source);
|
||||
System.err.println("Malformed MakeCard entry! - " + source.toString());
|
||||
}
|
||||
} else {
|
||||
names.add(n);
|
||||
@@ -72,8 +72,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
|
||||
cards = AbilityUtils.getDefinedCards(source, def, sa);
|
||||
}
|
||||
for (final Card c : cards) {
|
||||
//get the original papercard name
|
||||
names.add(c.getPaperCard().getName());
|
||||
names.add(c.getName());
|
||||
}
|
||||
} else if (sa.hasParam("Spellbook")) {
|
||||
faces.addAll(parseFaces(sa, "Spellbook"));
|
||||
|
||||
@@ -19,11 +19,9 @@ import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Localizer;
|
||||
import io.sentry.Breadcrumb;
|
||||
@@ -31,14 +29,6 @@ import io.sentry.Sentry;
|
||||
|
||||
public class ManaEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
|
||||
if (sa.getParent() == null) {
|
||||
sa.setUndoable(true); // will try at least
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Card card = sa.getHostCard();
|
||||
@@ -271,14 +261,10 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
producedMana.append(abMana.produceMana(mana, p, sa));
|
||||
}
|
||||
|
||||
// Only clear express choice after mana has been produced
|
||||
abMana.clearExpressChoice();
|
||||
|
||||
abMana.tapsForMana(sa.getRootAbility(), producedMana.toString());
|
||||
|
||||
if (sa.isKeyword(Keyword.FIREBENDING)) {
|
||||
activator.triggerElementalBend(TriggerType.Firebend);
|
||||
}
|
||||
// Only clear express choice after mana has been produced
|
||||
abMana.clearExpressChoice();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,14 +17,6 @@ import forge.util.Localizer;
|
||||
|
||||
public class ManaReflectedEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void buildSpellAbility(SpellAbility sa) {
|
||||
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
|
||||
if (sa.getParent() == null) {
|
||||
sa.setUndoable(true); // will try at least
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
|
||||
@@ -15,9 +15,8 @@ public class PermanentCreatureEffect extends PermanentEffect {
|
||||
public String getStackDescription(final SpellAbility sa) {
|
||||
final CardState source = sa.getCardState();
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ");
|
||||
sb.append(sa.getParamOrDefault("SetPower", source.getBasePowerString()));
|
||||
sb.append(" / ").append(sa.getParamOrDefault("SetToughness", source.getBaseToughnessString()));
|
||||
sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ").append(source.getBasePowerString());
|
||||
sb.append(" / ").append(source.getBaseToughnessString());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,10 +428,6 @@ public class PlayEffect extends SpellAbilityEffect {
|
||||
tgtSA.getTargetRestrictions().setMandatory(true);
|
||||
}
|
||||
|
||||
if (sa.hasParam("Named")) {
|
||||
tgtSA.setName(sa.getName());
|
||||
}
|
||||
|
||||
// can't be done later
|
||||
if (sa.hasParam("ReplaceGraveyard")) {
|
||||
if (!sa.hasParam("ReplaceGraveyardValid")
|
||||
|
||||
@@ -17,6 +17,7 @@ import forge.game.GameEntity;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.perpetual.PerpetualKeywords;
|
||||
@@ -281,17 +282,6 @@ public class PumpEffect extends SpellAbilityEffect {
|
||||
List<Card> tgtCards = getCardsfromTargets(sa);
|
||||
List<Player> tgtPlayers = getTargetPlayers(sa);
|
||||
|
||||
if (sa.hasParam("Optional")) {
|
||||
final String targets = Lang.joinHomogenous(tgtCards);
|
||||
final String message = sa.hasParam("OptionQuestion")
|
||||
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
|
||||
: Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets);
|
||||
|
||||
if (!activator.getController().confirmAction(sa, null, message, null)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> keywords = Lists.newArrayList();
|
||||
if (sa.hasParam("KW")) {
|
||||
keywords.addAll(Arrays.asList(sa.getParam("KW").split(" & ")));
|
||||
@@ -317,6 +307,8 @@ public class PumpEffect extends SpellAbilityEffect {
|
||||
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, host, sa);
|
||||
}
|
||||
|
||||
final CardCollection untargetedCards = CardUtil.getRadiance(sa);
|
||||
|
||||
if (sa.hasParam("DefinedKW")) {
|
||||
String defined = sa.getParam("DefinedKW");
|
||||
if (defined.equals("ChosenType")) {
|
||||
@@ -402,6 +394,17 @@ public class PumpEffect extends SpellAbilityEffect {
|
||||
keywords = choice;
|
||||
}
|
||||
|
||||
if (sa.hasParam("Optional")) {
|
||||
final String targets = Lang.joinHomogenous(tgtCards);
|
||||
final String message = sa.hasParam("OptionQuestion")
|
||||
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
|
||||
: Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets);
|
||||
|
||||
if (!activator.getController().confirmAction(sa, null, message, null)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("RememberObjects")) {
|
||||
host.addRemembered(AbilityUtils.getDefinedObjects(host, sa.getParam("RememberObjects"), sa));
|
||||
}
|
||||
@@ -491,7 +494,7 @@ public class PumpEffect extends SpellAbilityEffect {
|
||||
registerDelayedTrigger(sa, sa.getParam("AtEOT"), tgtCards);
|
||||
}
|
||||
|
||||
for (final Card tgtC : CardUtil.getRadiance(sa)) {
|
||||
for (final Card tgtC : untargetedCards) {
|
||||
// only pump things in PumpZone
|
||||
if (!tgtC.isInZones(pumpZones)) {
|
||||
continue;
|
||||
|
||||
@@ -369,8 +369,8 @@ public class RollDiceEffect extends SpellAbilityEffect {
|
||||
List<Card> canIncrementDice = new ArrayList<>();
|
||||
for (Card c : xenosquirrels) {
|
||||
// Xenosquirrels must have a P1P1 counter on it to remove in order to modify
|
||||
Integer P1P1Counters = c.getCounters().get(CounterEnumType.P1P1);
|
||||
if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterEnumType.P1P1)) {
|
||||
Integer P1P1Counters = c.getCounters().get(CounterType.get(CounterEnumType.P1P1));
|
||||
if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterType.get(CounterEnumType.P1P1))) {
|
||||
canIncrementDice.add(c);
|
||||
}
|
||||
}
|
||||
@@ -399,7 +399,6 @@ public class RollDiceEffect extends SpellAbilityEffect {
|
||||
* @param repParams replacement effect parameters
|
||||
* @return list of final roll results after applying ignores and replacements, sorted in ascending order
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Integer> rollAction(int amount, int sides, int ignore, List<Integer> rollsResult, List<Integer> ignored, Map<Player, Integer> ignoreChosenMap, Set<Card> dicePTExchanges, Player player, Map<AbilityKey, Object> repParams) {
|
||||
|
||||
repParams.put(AbilityKey.Sides, sides);
|
||||
@@ -417,8 +416,6 @@ public class RollDiceEffect extends SpellAbilityEffect {
|
||||
ignoreChosenMap = (Map<Player, Integer>) repParams.get(AbilityKey.IgnoreChosen);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
List<Integer> naturalRolls = (rollsResult == null ? new ArrayList<>() : rollsResult);
|
||||
|
||||
@@ -92,13 +92,14 @@ public class SacrificeEffect extends SpellAbilityEffect {
|
||||
CardZoneTable zoneMovements = AbilityKey.addCardZoneTableParams(params, sa);
|
||||
|
||||
if (valid.equals("Self") && game.getZoneOf(host) != null) {
|
||||
if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield) &&
|
||||
(!optional || activator.getController().confirmAction(sa, null,
|
||||
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null))) {
|
||||
if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield)) {
|
||||
if (!optional || activator.getController().confirmAction(sa, null,
|
||||
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null)) {
|
||||
if (game.getAction().sacrifice(new CardCollection(host), sa, true, params) != null && remSacrificed) {
|
||||
host.addRemembered(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CardCollectionView choosenToSacrifice = null;
|
||||
for (final Player p : getTargetPlayers(sa)) {
|
||||
|
||||
@@ -41,7 +41,6 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
|
||||
tapper = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Tapper"), sa).getFirst();
|
||||
}
|
||||
PlayerController pc = tapper.getController();
|
||||
boolean toggle = sa.hasParam("Toggle");
|
||||
|
||||
CardCollection tapped = new CardCollection();
|
||||
final Map<Player, CardCollection> untapMap = Maps.newHashMap();
|
||||
@@ -62,12 +61,8 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
|
||||
continue;
|
||||
}
|
||||
// If the effected card is controlled by the same controller of the SA, default to untap.
|
||||
boolean tap;
|
||||
if(!toggle)
|
||||
tap = pc.chooseBinary(sa, Localizer.getInstance().getMessage("lblTapOrUntapTarget", CardTranslation.getTranslatedName(gameCard.getName())), PlayerController.BinaryChoiceType.TapOrUntap,
|
||||
boolean tap = pc.chooseBinary(sa, Localizer.getInstance().getMessage("lblTapOrUntapTarget", CardTranslation.getTranslatedName(gameCard.getName())), PlayerController.BinaryChoiceType.TapOrUntap,
|
||||
!gameCard.getController().equals(tapper));
|
||||
else
|
||||
tap = !gameCard.isTapped();
|
||||
if (tap) {
|
||||
if (gameCard.tap(true, sa, tapper)) tapped.add(gameCard);
|
||||
} else if (gameCard.untap()) {
|
||||
|
||||
@@ -9,10 +9,8 @@ import forge.game.GameEntityCounterTable;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerController;
|
||||
@@ -21,6 +19,7 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.CardTranslation;
|
||||
import forge.util.Localizer;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
public class TimeTravelEffect extends SpellAbilityEffect {
|
||||
|
||||
@@ -38,11 +37,13 @@ public class TimeTravelEffect extends SpellAbilityEffect {
|
||||
|
||||
PlayerController pc = activator.getController();
|
||||
|
||||
final CounterType counterType = CounterEnumType.TIME;
|
||||
final CounterEnumType counterType = CounterEnumType.TIME;
|
||||
|
||||
for (int i = 0; i < num; i++) {
|
||||
FCollection<Card> list = new FCollection<>();
|
||||
|
||||
// card you own that is suspended
|
||||
CardCollection list = CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend());
|
||||
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend()));
|
||||
// permanent you control with time counter
|
||||
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounter(counterType)));
|
||||
|
||||
|
||||
@@ -203,7 +203,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
private boolean unearthed;
|
||||
private boolean ringbearer;
|
||||
private boolean monstrous;
|
||||
private boolean harnessed;
|
||||
private boolean renowned;
|
||||
private boolean solved;
|
||||
private boolean tributed;
|
||||
@@ -257,7 +256,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
private long worldTimestamp = -1;
|
||||
private long bestowTimestamp = -1;
|
||||
private long transformedTimestamp = -1;
|
||||
private long transformedTimestamp = 0;
|
||||
private long prototypeTimestamp = -1;
|
||||
private long mutatedTimestamp = -1;
|
||||
private int timesMutated = 0;
|
||||
@@ -425,7 +424,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
public long getPrototypeTimestamp() { return prototypeTimestamp; }
|
||||
|
||||
public long getTransformedTimestamp() { return transformedTimestamp; }
|
||||
public void setTransformedTimestamp(long ts) { this.transformedTimestamp = ts; }
|
||||
public void incrementTransformedTimestamp() { this.transformedTimestamp++; }
|
||||
public void undoIncrementTransformedTimestamp() { this.transformedTimestamp--; }
|
||||
|
||||
// The following methods are used to selectively update certain view components (text,
|
||||
// P/T, card types) in order to avoid card flickering due to aggressive full update
|
||||
@@ -695,7 +695,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(this);
|
||||
getGame().getTriggerHandler().runTrigger(TriggerType.Transformed, runParams, false);
|
||||
}
|
||||
setTransformedTimestamp(ts);
|
||||
incrementTransformedTimestamp();
|
||||
|
||||
return retResult;
|
||||
} else if (mode.equals("Flip")) {
|
||||
@@ -1051,7 +1051,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
// it was always created), adjust threshold based on its existence.
|
||||
int threshold = states.containsKey(CardStateName.FaceDown) ? 2 : 1;
|
||||
|
||||
int numStates = states.size();
|
||||
int numStates = states.keySet().size();
|
||||
|
||||
return numStates > threshold;
|
||||
}
|
||||
@@ -1069,7 +1069,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public final boolean isDoubleFaced() {
|
||||
return isTransformable() || isMeldable();
|
||||
return isTransformable() || isMeldable() || isModal();
|
||||
}
|
||||
|
||||
public final boolean isFlipCard() {
|
||||
@@ -1131,10 +1131,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public final boolean isTransformed() {
|
||||
if (isMeldable() || hasMergedCard()) {
|
||||
return false;
|
||||
}
|
||||
return this.isTransformable() && isBackSide();
|
||||
return getTransformedTimestamp() != 0;
|
||||
}
|
||||
|
||||
public final boolean isFlipped() {
|
||||
@@ -2265,7 +2262,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
public final ColorSet getMarkedColors() {
|
||||
if (markedColor == null) {
|
||||
return ColorSet.NO_COLORS;
|
||||
return ColorSet.getNullColor();
|
||||
}
|
||||
return markedColor;
|
||||
}
|
||||
@@ -2465,8 +2462,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
}
|
||||
sbLong.append("Enchant ").append(desc).append("\r\n");
|
||||
} else if (keyword.startsWith("Ripple")) {
|
||||
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
|
||||
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")
|
||||
|| keyword.startsWith("Disguise") || keyword.startsWith("Reflect")
|
||||
|| keyword.startsWith("Disguise")
|
||||
|| keyword.startsWith("Escape") || keyword.startsWith("Foretell:")
|
||||
|| keyword.startsWith("Madness:")|| keyword.startsWith("Recover")
|
||||
|| keyword.startsWith("Reconfigure") || keyword.startsWith("Squad")
|
||||
@@ -2505,15 +2504,17 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
sbLong.append(".");
|
||||
}
|
||||
if (k.length > 3) {
|
||||
sbLong.append(". ").append(k[3]);
|
||||
sbLong.append(". " + k[3]);
|
||||
}
|
||||
}
|
||||
sbLong.append(" (").append(inst.getReminderText()).append(")");
|
||||
sbLong.append("\r\n");
|
||||
} else if (keyword.equals("Mayhem")) {
|
||||
}
|
||||
} else if (keyword.startsWith("Reflect")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
|
||||
sbLong.append(" (").append(inst.getReminderText()).append(")");
|
||||
sbLong.append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Echo")) {
|
||||
sbLong.append("Echo ");
|
||||
final String[] upkeepCostParams = keyword.split(":");
|
||||
@@ -2652,7 +2653,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing:")
|
||||
|| keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic")
|
||||
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|
||||
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Ripple")) {
|
||||
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")");
|
||||
} else if (keyword.startsWith("Crew")) {
|
||||
@@ -2761,7 +2762,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")
|
||||
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon")
|
||||
|| keyword.startsWith("Class") || keyword.startsWith("Blitz") || keyword.startsWith("Web-slinging")
|
||||
|| keyword.startsWith("Specialize") || keyword.equals("Ravenous") || keyword.startsWith("Firebending")
|
||||
|| keyword.startsWith("Specialize") || keyword.equals("Ravenous")
|
||||
|| keyword.equals("For Mirrodin") || keyword.equals("Job select") || keyword.startsWith("Craft")
|
||||
|| keyword.startsWith("Landwalk") || keyword.startsWith("Visit") || keyword.startsWith("Mobilize")
|
||||
|| keyword.startsWith("Station") || keyword.startsWith("Warp") || keyword.startsWith("Devour")) {
|
||||
@@ -2968,9 +2969,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
if (monstrous) {
|
||||
sb.append("Monstrous\r\n");
|
||||
}
|
||||
if (harnessed) {
|
||||
sb.append("Harnessed\r\n");
|
||||
}
|
||||
if (renowned) {
|
||||
sb.append("Renowned\r\n");
|
||||
}
|
||||
@@ -3221,10 +3219,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", getEffectSource().getName());
|
||||
}
|
||||
|
||||
// Ensure no more escaped linebreak are present
|
||||
desc = desc.replace("\\r", "\r")
|
||||
.replace("\\n", "\n");
|
||||
|
||||
return desc.trim();
|
||||
}
|
||||
|
||||
@@ -3563,23 +3557,14 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
if (!getStaticAbilities().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!getReplacementEffects().isEmpty()
|
||||
&& (getReplacementEffects().size() > 1 || !isSaga() || hasKeyword(Keyword.READ_AHEAD))) {
|
||||
if (!getReplacementEffects().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!getTriggers().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (SpellAbility sa : getSpellAbilities()) {
|
||||
// morph up and disguise up are not part of the card
|
||||
if (sa.isMorphUp() || sa.isDisguiseUp()) {
|
||||
continue;
|
||||
}
|
||||
// while Adventure and Omen are part of Secondary
|
||||
if ((sa.isAdventure() || sa.isOmen()) && !getCurrentStateName().equals(sa.getCardState())) {
|
||||
continue;
|
||||
}
|
||||
if (!(sa instanceof SpellPermanent && sa.isBasicSpell())) {
|
||||
if (!(sa instanceof SpellPermanent && sa.isBasicSpell()) && !sa.isMorphUp() && !sa.isDisguiseUp()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3616,14 +3601,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (hasState(CardStateName.Secondary) && state.getStateName() == CardStateName.Original) {
|
||||
} else {
|
||||
// Adventure and Omen may only be cast not from Battlefield
|
||||
if (hasState(CardStateName.Secondary) && state.getStateName() == CardStateName.Original) {
|
||||
for (SpellAbility sa : getState(CardStateName.Secondary).getSpellAbilities()) {
|
||||
if (mana == null || mana == sa.isManaAbility()) {
|
||||
list.add(sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keywords should already been cleanup by layers
|
||||
for (KeywordInterface kw : getUnhiddenKeywords(state)) {
|
||||
@@ -6444,10 +6431,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
DamageType damageType = DamageType.Normal;
|
||||
if (isPlaneswalker()) { // 120.3c
|
||||
subtractCounter(CounterEnumType.LOYALTY, damageIn, null, true);
|
||||
subtractCounter(CounterType.get(CounterEnumType.LOYALTY), damageIn, null, true);
|
||||
}
|
||||
if (isBattle()) {
|
||||
subtractCounter(CounterEnumType.DEFENSE, damageIn, null, true);
|
||||
subtractCounter(CounterType.get(CounterEnumType.DEFENSE), damageIn, null, true);
|
||||
}
|
||||
if (isCreature()) {
|
||||
if (source.isWitherDamage()) { // 120.3d
|
||||
@@ -6695,14 +6682,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
setRingBearer(false);
|
||||
}
|
||||
|
||||
public final boolean isHarnessed() {
|
||||
return harnessed;
|
||||
}
|
||||
public final boolean setHarnessed(final boolean harnessed0) {
|
||||
harnessed = harnessed0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public final boolean isMonstrous() {
|
||||
return monstrous;
|
||||
}
|
||||
@@ -6860,10 +6839,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
return exiledSA.isKeyword(Keyword.WARP);
|
||||
}
|
||||
|
||||
public boolean isWebSlinged() {
|
||||
return getCastSA() != null & getCastSA().isAlternativeCost(AlternativeCost.WebSlinging);
|
||||
}
|
||||
|
||||
public boolean isSpecialized() {
|
||||
return specialized;
|
||||
}
|
||||
@@ -7159,7 +7134,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StaticAbilityCantTarget.cantTarget(this, sa) != null) {
|
||||
if (StaticAbilityCantTarget.cantTarget(this, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7640,6 +7615,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
if (sa.isBestow()) {
|
||||
animateBestow();
|
||||
}
|
||||
if (sa.isDisturb() || sa.hasParam("CastTransformed")) {
|
||||
incrementTransformedTimestamp();
|
||||
}
|
||||
if (sa.hasParam("Prototype") && prototypeTimestamp == -1) {
|
||||
long next = game.getNextTimestamp();
|
||||
addCloneState(CardFactory.getCloneStates(this, this, sa), next);
|
||||
|
||||
@@ -131,7 +131,9 @@ public class CardCopyService {
|
||||
|
||||
c.setState(in.getCurrentStateName(), false);
|
||||
c.setRules(in.getRules());
|
||||
c.setBackSide(in.isBackSide());
|
||||
if (in.isTransformed()) {
|
||||
c.incrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -166,6 +168,9 @@ public class CardCopyService {
|
||||
// The characteristics of its front and back face are determined by the copiable values of the same face of the spell it is a copy of, as modified by any other copy effects.
|
||||
// If the spell it is a copy of has its back face up, the copy is created with its back face up. The token that’s put onto the battlefield as that spell resolves is a transforming token.
|
||||
to.setBackSide(copyFrom.isBackSide());
|
||||
if (copyFrom.isTransformed()) {
|
||||
to.incrementTransformedTimestamp();
|
||||
}
|
||||
} else if (fromIsTransformedCard) {
|
||||
copyState(copyFrom, copyFrom.getCurrentStateName(), to, CardStateName.Original);
|
||||
} else {
|
||||
@@ -269,6 +274,9 @@ public class CardCopyService {
|
||||
}
|
||||
newCopy.setFlipped(copyFrom.isFlipped());
|
||||
newCopy.setBackSide(copyFrom.isBackSide());
|
||||
if (copyFrom.isTransformed()) {
|
||||
newCopy.incrementTransformedTimestamp();
|
||||
}
|
||||
if (newCopy.hasAlternateState()) {
|
||||
newCopy.setState(copyFrom.getCurrentStateName(), false, true);
|
||||
}
|
||||
|
||||
@@ -87,16 +87,22 @@ public class CardFactory {
|
||||
// need to create a physical card first, i need the original card faces
|
||||
final Card copy = getCard(original.getPaperCard(), controller, id, game);
|
||||
|
||||
copy.setStates(getCloneStates(original, copy, sourceSA));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
// 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield,
|
||||
// the resulting token is a transforming token that has both a front face and a back face.
|
||||
// The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
|
||||
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up.
|
||||
// This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
|
||||
copy.setBackSide(original.isBackSide());
|
||||
if (original.isTransformed()) {
|
||||
copy.incrementTransformedTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
copy.setStates(getCloneStates(original, copy, sourceSA));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
|
||||
} else {
|
||||
copy.setState(copy.getCurrentStateName(), true, true);
|
||||
}
|
||||
@@ -192,9 +198,7 @@ public class CardFactory {
|
||||
if (c.hasAlternateState()) {
|
||||
if (c.isFlipCard()) {
|
||||
c.setState(CardStateName.Flipped, false);
|
||||
// set the imagekey altstate to false since the rotated image is handled by graphics renderer
|
||||
// setting this to true will download the original image with different name.
|
||||
c.setImageKey(cp.getImageKey(false));
|
||||
c.setImageKey(cp.getImageKey(true));
|
||||
}
|
||||
else if (c.isDoubleFaced() && cardRules != null) {
|
||||
c.setState(cardRules.getSplitType().getChangedStateName(), false);
|
||||
@@ -370,28 +374,22 @@ public class CardFactory {
|
||||
}
|
||||
}
|
||||
|
||||
// Negative card Id's are for view purposes only
|
||||
if (c.getId() >= 0) {
|
||||
// Build English oracle and translated oracle mapping
|
||||
if (c.getId() >= 0) {
|
||||
CardTranslation.buildOracleMapping(face.getName(), face.getOracleText(), variantName);
|
||||
}
|
||||
|
||||
// Set name for Sentry reports to be identifiable
|
||||
// Name first so Senty has the Card name
|
||||
c.setName(face.getName());
|
||||
|
||||
if (c.getId() >= 0) { // Set Triggers & Abilities if not for view
|
||||
for (Entry<String, String> v : face.getVariables())
|
||||
c.setSVar(v.getKey(), v.getValue());
|
||||
for (String r : face.getReplacements())
|
||||
c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState()));
|
||||
for (String s : face.getStaticAbilities())
|
||||
c.addStaticAbility(s);
|
||||
for (String t : face.getTriggers())
|
||||
c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState()));
|
||||
for (Entry<String, String> v : face.getVariables()) c.setSVar(v.getKey(), v.getValue());
|
||||
|
||||
for (String r : face.getReplacements()) c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState()));
|
||||
for (String s : face.getStaticAbilities()) c.addStaticAbility(s);
|
||||
for (String t : face.getTriggers()) c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState()));
|
||||
|
||||
// keywords not before variables
|
||||
c.addIntrinsicKeywords(face.getKeywords(), false);
|
||||
}
|
||||
if (face.getDraftActions() != null) {
|
||||
face.getDraftActions().forEach(c::addDraftAction);
|
||||
}
|
||||
@@ -420,7 +418,6 @@ public class CardFactory {
|
||||
|
||||
c.setAttractionLights(face.getAttractionLights());
|
||||
|
||||
if (c.getId() > 0) // Set FactoryAbilities if not for view
|
||||
CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities());
|
||||
}
|
||||
|
||||
|
||||
@@ -1170,29 +1170,6 @@ public class CardFactoryUtil {
|
||||
removeCounterSA.setIntrinsic(intrinsic);
|
||||
trigger.setOverridingAbility(removeCounterSA);
|
||||
|
||||
inst.addTrigger(trigger);
|
||||
} else if (keyword.startsWith("Firebending")) {
|
||||
final String[] k = keyword.split(":");
|
||||
final String n = k[1];
|
||||
|
||||
StringBuilder desc = new StringBuilder("Firebending ");
|
||||
desc.append(n);
|
||||
if (k.length > 2) {
|
||||
desc.append(" ").append(k[2]);
|
||||
}
|
||||
|
||||
desc.append(" (").append(inst.getReminderText()).append(")");
|
||||
|
||||
final String trigStr = "Mode$ Attacks | ValidCard$ Card.Self | TriggerDescription$ " + desc.toString();
|
||||
|
||||
final String manaStr = "DB$ Mana | Defined$ You | CombatMana$ True | Produced$ R | Amount$ " + n;
|
||||
|
||||
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
|
||||
SpellAbility manaSA = AbilityFactory.getAbility(manaStr, card);
|
||||
|
||||
manaSA.setIntrinsic(intrinsic);
|
||||
trigger.setOverridingAbility(manaSA);
|
||||
|
||||
inst.addTrigger(trigger);
|
||||
} else if (keyword.equals("Flanking")) {
|
||||
final StringBuilder trigFlanking = new StringBuilder(
|
||||
@@ -2568,7 +2545,7 @@ public class CardFactoryUtil {
|
||||
} else if (keyword.equals("Sunburst")) {
|
||||
// Rule 702.43a If this object is entering the battlefield as a creature,
|
||||
// ignoring any type-changing effects that would affect it
|
||||
CounterType t = host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE;
|
||||
CounterType t = CounterType.get(host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE);
|
||||
|
||||
StringBuilder sb = new StringBuilder("etbCounter:");
|
||||
sb.append(t).append(":Sunburst:no Condition:");
|
||||
@@ -2776,9 +2753,10 @@ public class CardFactoryUtil {
|
||||
final String cost = params[1];
|
||||
|
||||
final StringBuilder sbAttach = new StringBuilder();
|
||||
sbAttach.append("SP$ Attach | ValidTgts$ Creature | Cost$ ");
|
||||
sbAttach.append("SP$ Attach | Cost$ ");
|
||||
sbAttach.append(cost);
|
||||
sbAttach.append(" | AILogic$ ").append(params.length > 2 ? params[2] : "Pump");
|
||||
sbAttach.append(" | Bestow$ True | ValidTgts$ Creature");
|
||||
|
||||
final SpellAbility sa = AbilityFactory.getAbility(sbAttach.toString(), card);
|
||||
final StringBuilder sbDesc = new StringBuilder();
|
||||
@@ -4112,7 +4090,7 @@ public class CardFactoryUtil {
|
||||
sbValid.append("| ").append(param).append(k[1]);
|
||||
}
|
||||
|
||||
String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True"
|
||||
String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True"
|
||||
+ sbValid.toString() + " | Activator$ Opponent | Description$ "
|
||||
+ sbDesc.toString() + " (" + inst.getReminderText() + ")";
|
||||
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
|
||||
@@ -4153,7 +4131,7 @@ public class CardFactoryUtil {
|
||||
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
|
||||
|
||||
// Target
|
||||
effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True ";
|
||||
effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Card.Self | Secondary$ True ";
|
||||
if (!valid.isEmpty()) {
|
||||
effect += "| ValidSource$ " + valid;
|
||||
}
|
||||
@@ -4161,7 +4139,7 @@ public class CardFactoryUtil {
|
||||
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
|
||||
|
||||
// Attach
|
||||
effect = "Mode$ CantAttach | Target$ Card.Self | Secondary$ True ";
|
||||
effect = "Mode$ CantAttach | Protection$ True | Target$ Card.Self | Secondary$ True ";
|
||||
if (!valid.isEmpty()) {
|
||||
effect += "| ValidCard$ " + valid;
|
||||
}
|
||||
@@ -4182,7 +4160,7 @@ public class CardFactoryUtil {
|
||||
" | Description$ Chapter abilities of this Saga can't trigger the turn it entered the battlefield unless it has exactly the number of lore counters on it specified in the chapter symbol of that ability.";
|
||||
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
|
||||
} else if (keyword.equals("Shroud")) {
|
||||
String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True"
|
||||
String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True"
|
||||
+ " | Description$ Shroud (" + inst.getReminderText() + ")";
|
||||
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
|
||||
} else if (keyword.equals("Skulk")) {
|
||||
|
||||
@@ -213,10 +213,16 @@ public final class CardPredicates {
|
||||
public static Predicate<Card> hasCounter(final CounterType type) {
|
||||
return hasCounter(type, 1);
|
||||
}
|
||||
public static Predicate<Card> hasCounter(final CounterEnumType type) {
|
||||
return hasCounter(type, 1);
|
||||
}
|
||||
|
||||
public static Predicate<Card> hasCounter(final CounterType type, final int n) {
|
||||
return c -> c.getCounters(type) >= n;
|
||||
}
|
||||
public static Predicate<Card> hasCounter(final CounterEnumType type, final int n) {
|
||||
return hasCounter(CounterType.get(type), n);
|
||||
}
|
||||
|
||||
public static Predicate<Card> hasLessCounter(final CounterType type, final int n) {
|
||||
return c -> {
|
||||
@@ -224,10 +230,16 @@ public final class CardPredicates {
|
||||
return x > 0 && x <= n;
|
||||
};
|
||||
}
|
||||
public static Predicate<Card> hasLessCounter(final CounterEnumType type, final int n) {
|
||||
return hasLessCounter(CounterType.get(type), n);
|
||||
}
|
||||
|
||||
public static Predicate<Card> canReceiveCounters(final CounterType counter) {
|
||||
return c -> c.canReceiveCounters(counter);
|
||||
}
|
||||
public static Predicate<Card> canReceiveCounters(final CounterEnumType counter) {
|
||||
return canReceiveCounters(CounterType.get(counter));
|
||||
}
|
||||
|
||||
public static Predicate<Card> hasGreaterPowerThan(final int minPower) {
|
||||
return c -> c.getNetPower() > minPower;
|
||||
@@ -236,6 +248,9 @@ public final class CardPredicates {
|
||||
public static Comparator<Card> compareByCounterType(final CounterType type) {
|
||||
return Comparator.comparingInt(arg0 -> arg0.getCounters(type));
|
||||
}
|
||||
public static Comparator<Card> compareByCounterType(final CounterEnumType type) {
|
||||
return compareByCounterType(CounterType.get(type));
|
||||
}
|
||||
|
||||
public static Predicate<Card> hasSVar(final String name) {
|
||||
return c -> c.hasSVar(name);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user