Compare commits

..

93 Commits

Author SHA1 Message Date
GitHub Actions
a29da54b2e [maven-release-plugin] prepare release forge-2.0.02 2025-02-15 20:50:03 +00:00
Chris H
ec63b230c8 Temporarily remove flatten library for release 2025-02-15 12:34:35 -05:00
Jetz
e880a83df2 Let SVars in functional variants overwrite original script 2025-02-15 12:34:00 -05:00
Jetz
80cc7218a3 URLs for token image keys for speed and max_speed 2025-02-15 12:34:00 -05:00
Renato Filipe Vidal Santos
4809fb858a Update gale_conduit_of_the_arcane.txt 2025-02-15 12:19:58 -05:00
Jetz
3af62888dc Remove wake lock on desktop 2025-02-15 07:50:06 +01:00
Jetz
79e1d0a0f0 Count destroyed attractions for "number of attractions you've visited this turn" 2025-02-15 07:50:06 +01:00
Jetz
c447dfc888 Support "Affinitycycling" and "Affinity for Affinity" 2025-02-15 07:50:06 +01:00
Jetz
8149966915 Fix Tyrranax Rex importing 2025-02-15 07:50:06 +01:00
Hans Mackowiak
472f9481e8 Update radiant_lotus.txt 2025-02-15 07:49:21 +01:00
Hans Mackowiak
2209ce3cee Update sanguine_soothsayer.txt
fix mana cost
2025-02-14 14:57:40 +01:00
Hans Mackowiak
258c89e65d Update CounterEffect.java (#7014) 2025-02-14 09:06:25 +01:00
tool4ever
11913085ef Misc cleanup (#7009) 2025-02-13 15:51:19 +01:00
Chris H
53fca12a57 Fix Radiant lotus 2025-02-12 21:14:01 -05:00
Chris H
8e8a795f19 Migrate upcoming 2025-02-12 20:34:55 -05:00
Renato Filipe Vidal Santos
a4b27321ac Oracle update: During your turn (#7005) 2025-02-12 10:55:57 +01:00
Renato Filipe Vidal Santos
ead83d932f Oracle update: Affinity (#7001) 2025-02-12 10:34:50 +01:00
tool4ever
900bd4327d Update pride_of_the_road.txt 2025-02-12 10:21:39 +01:00
Paul Hammerton
1a2bb054f4 Merge pull request #7000 from paulsnoops/fix-sld
Edition updates: SLD
2025-02-11 19:09:54 +00:00
Paul Hammerton
f908df46c8 Edition updates: SLD 2025-02-11 19:07:12 +00:00
Paul Hammerton
f8c97842c4 Merge pull request #6999 from paulsnoops/edition-updates
Edition updates: SLD
2025-02-11 17:08:59 +00:00
Paul Hammerton
c324b45025 Edition updates: SLD 2025-02-11 17:02:00 +00:00
tool4ever
5061ceda0e Clean up (#6995) 2025-02-10 21:43:04 +00:00
Chris H
d1e677eb4f Update rangers_refueler.txt 2025-02-10 17:37:26 +01:00
Jetz72
493a8f351b Add Speed Tracker to Command Zone (#6982)
* Add command zone effect displaying speed

* Remove enum counter type for speed.

* Make Start Your Engines an SBA.

* LifeLost -> LifeLostAll per speed rules.

* Use same game event for all speed changes.

* Fix keyword not appearing in detail text

* Cleanup extra createSpeedEffect.

* Add support for arbitrary overlay text. Remove fake counters.

* Text styling.

* Remove extra SBA check.

* Remove speed from PlayerView; localization support.

---------

Co-authored-by: Jetz <Jetz722@gmail.com>
2025-02-10 07:43:48 +03:00
Hans Mackowiak
04172eead0 suspected as Static Effect (#6991) 2025-02-09 13:02:59 +00:00
tool4ever
f0ed9288b3 Remove outdated logic (#6989) 2025-02-09 13:01:15 +00:00
Hans Mackowiak
03fe3d63ea Start your Engines as StaticAction (#6987)
* Start your Engines as StaticAction

* ~ fix trigger desc

* ~ moved keyword to better place
2025-02-09 10:52:17 +01:00
Hans Mackowiak
83438ef72b StaticAbilityContinous now have AffectedDefined for card list (#6986)
* StaticAbilityContinous now have AffectedDefined for card list
2025-02-09 10:49:19 +01:00
Paul Hammerton
cf18808a70 Merge pull request #6988 from paulsnoops/dft-rank
Update draft rankings: DFT, INR, PIO
2025-02-08 23:13:53 +00:00
Paul Hammerton
49dc2c1c42 Update draft rankings: DFT, INR, PIO 2025-02-08 23:10:29 +00:00
Northmoc
692400db2a finish up Max speed refactor (#6958)
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-02-08 14:42:55 +01:00
Northmoc
751c31b226 refactor Max Speed round 1 (#6957) 2025-02-08 14:35:26 +01:00
tool4ever
7be252c509 Fix Elvish Refueler (#6984) 2025-02-08 08:04:30 +00:00
tool4ever
288eac743c Fix Blessing applying too late for triggers (#6983) 2025-02-07 18:17:00 +00:00
tool4ever
d0bd80f158 Update spire_mechcycle.txt 2025-02-07 09:28:38 +01:00
Paul Hammerton
7fe8154bcb Merge pull request #6979 from paulsnoops/edition-updates
Edition updates: PL25
2025-02-06 18:38:42 +00:00
Paul Hammerton
87cd5c90a3 Edition updates: PL25 2025-02-06 18:34:33 +00:00
tool4ever
12399fca48 Update elvish_refueler.txt 2025-02-06 17:42:04 +00:00
Chris H
aaf17553c1 Update syphon_fuel.txt (#6978) 2025-02-06 17:20:17 +01:00
tool4ever
9dedd24d3e Fix Manifest Dread vs. Grafdigger's Cage (#6977) 2025-02-06 17:19:40 +01:00
tool4ever
2443f1486d Fix Primal Wellspring trigger not working with Chun-Li (#6971)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-06 09:40:29 +03:00
Simisays
cfd1822198 update 2025-02-05 19:05:00 -05:00
Paul Hammerton
7930c4949b Merge pull request #6975 from paulsnoops/fix-token
Edition updates: Fix token name in DFT
2025-02-05 17:50:39 +00:00
Paul Hammerton
ef6d0707ac Edition updates: Fix token name in DFT 2025-02-05 17:47:17 +00:00
Paul Hammerton
cfd792cb69 Merge pull request #6974 from paulsnoops/edition-updates
Edition updates: DFT, DRC, SLD, SPG
2025-02-05 17:14:52 +00:00
Paul Hammerton
f5352662cd Edition updates: DFT, DRC, SLD, SPG 2025-02-05 17:09:00 +00:00
Justin C
bf1192f80d instanceof Pattern variable changes (#6972) 2025-02-05 16:52:14 +03:00
tool4ever
dee2150cf9 Fix trigger targeting itself (#6973)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-05 16:52:04 +03:00
Hans Mackowiak
c44b105d9f ~ lf 2025-02-04 07:03:51 +01:00
loud1990
b624fb3cf8 DFT Card Cleanup
Changed mana cost for Sundial, trigger for Necroregent
2025-02-03 20:48:11 -05:00
Chris H
45396c1bf4 Fix sellfactor duplication 2025-02-03 18:55:24 -05:00
tool4ever
f562ae6fdb More view cleanup (#6967) 2025-02-03 21:12:03 +00:00
Chris H
6615090bda Allow jumpstart/"nosell" cards to be sold for 0 credits 2025-02-02 12:55:09 -05:00
Chris H
eaf6f117a2 Don't try to load battle if entering village simultaneously 2025-02-02 12:54:38 -05:00
tool4ever
a8488502e7 Reduce some View updates (#6966) 2025-02-02 17:52:20 +00:00
Chris H
309e36827c Fix draftable cards 2025-02-01 22:24:36 -05:00
Chris H
fe7883ddd8 Add ManaCost for Wretched Doll 2025-02-01 22:08:14 -05:00
Chris H
06e5ff5174 DFT edition cards update 2025-02-01 22:08:14 -05:00
Chris H
2c31dd01dd Add booster info for Aetherdrift 2025-02-01 21:40:31 -05:00
Hans Mackowiak
d8a92c4879 ~ lf 2025-02-01 11:55:46 +01:00
Fulgur14
16baeadf0c Final DFT push (2 of 3) (#6948) 2025-02-01 09:34:31 +00:00
Hans Mackowiak
88ed81f75f ~ lf 2025-02-01 10:17:29 +01:00
Fulgur14
70d9df1db2 Final DFT push (3 of 3) (#6949) 2025-02-01 08:33:19 +00:00
Hans Mackowiak
aaa04570f2 Update howlers_heavy.txt 2025-02-01 07:10:20 +01:00
Fulgur14
9365d55964 Final DFT push (1 of 3) (#6947) 2025-01-31 23:04:52 +00:00
Renato Filipe Vidal Santos
0c61139f51 Add files via upload (#6945) 2025-01-31 17:36:16 +03:00
Hans Mackowiak
34bd623e45 Update kataki_wars_wage.txt
Closes #6943
2025-01-31 11:07:00 +01:00
Fulgur14
0e31bb8565 7 DFT cards (Waxen Shapethief and its models) (#6937) 2025-01-31 08:33:59 +01:00
Chris H
0b87094f96 Fix NPE with POEReference 2025-01-30 21:32:17 -05:00
Chris H
42e53c66f6 Update ravenous_amulet.txt 2025-01-30 16:58:11 -05:00
Renato Filipe Vidal Santos
25a7d80146 Fixing mistaken references to defending player in triggered abilities 2025-01-30 21:36:48 +00:00
Simisays
a6170745b1 Update config.json 2025-01-30 15:04:49 -05:00
Simisays
db32547a6e Update eldrazilarge.dck 2025-01-30 15:04:49 -05:00
Simisays
4df6d9998b Update config.json 2025-01-30 15:04:49 -05:00
Simisays
2f42f6ca28 Update config.json 2025-01-30 15:04:49 -05:00
Simisays
6617c10946 update 2025-01-30 15:04:49 -05:00
Northmoc
1d34e02957 grim_bauble.txt and some fixes (#6935) 2025-01-30 18:34:06 +00:00
Northmoc
132f8d3d4f another round of whitespace and other fixes (#6934) 2025-01-30 18:32:41 +00:00
Northmoc
d3961b1a53 outpace_oblivion.txt (#6936) 2025-01-30 18:31:40 +00:00
Fulgur14
2c04ef9e1f Howlsquad Heavy (DFT) (#6933) 2025-01-30 13:37:02 +01:00
Fulgur14
f599e3ead6 4 DFT cards (Oviya and her projects) (#6932) 2025-01-30 12:07:42 +01:00
Northmoc
e16da84a75 gonti_night_minister.txt + support (#6924) 2025-01-30 11:28:04 +03:00
Fulgur14
0a622f5282 19 DFT cards (Mimeoplasm and its imprints) (#6922)
* Add files via upload

* Update mimeoplasm_revered_one.txt

* Update radiant_lotus.txt

* Add files via upload

* Update radiant_lotus.txt

* Update ripclaw_wrangler.txt
2025-01-30 08:07:40 +03:00
Fulgur14
bb40138c52 8 DFT cards (Adrenaline Jockey and groupies) (#6925)
* 7 DFT cards (Adrenaline Jockey and groupies)

* Add files via upload
2025-01-30 08:07:31 +03:00
tool4ever
2a7bd8bbd2 Clean up logic (#6928) 2025-01-29 18:53:39 +00:00
Fulgur14
e6fc666012 Rover Blades and Gastal Thrillroller (#6927) 2025-01-29 18:21:24 +00:00
Fulgur14
a1297e593c Salvation Engine (#6926) 2025-01-29 17:38:50 +00:00
Renato Filipe Vidal Santos
2026c7eca0 Edit pile cleanup: Trigger effects formatted as activated abilities 2025-01-29 14:13:15 +00:00
Fulgur14
137076f224 9 DFT cards (3 Tyrants and their food) (#6912) 2025-01-29 11:08:51 +00:00
Fulgur14
5538650681 7 DFT cards (Gastal Blockbuster and friends) (#6909) 2025-01-29 11:08:39 +00:00
tool4ever
0e36e6b6d9 Fix room fully unlocked when turned up from manifest (#6921) 2025-01-29 08:46:57 +00:00
Fulgur14
f4c786763a Update bitter_chill.txt (#6920) 2025-01-29 07:42:41 +00:00
611 changed files with 4261 additions and 2500 deletions

View File

@@ -3,7 +3,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@@ -45,17 +45,16 @@ public class BiomeStructureDataMappingEditor extends JComponent {
JList list, Object value, int index, JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) { boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping)) if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping biomeData))
return label; return label;
BiomeStructureData.BiomeStructureDataMapping data=(BiomeStructureData.BiomeStructureDataMapping) value;
// Get the renderer component from parent class // Get the renderer component from parent class
label.setText(data.name); label.setText(biomeData.name);
if(editor.data!=null) if(editor.data!=null)
{ {
SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath)); SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath));
if(itemAtlas.has(data.name)) if(itemAtlas.has(biomeData.name))
label.setIcon(itemAtlas.get(data.name)); label.setIcon(itemAtlas.get(biomeData.name));
else else
{ {
ImageIcon img=itemAtlas.getAny(); ImageIcon img=itemAtlas.getAny();

View File

@@ -25,9 +25,8 @@ public class DialogOptionEditor extends JComponent{
JList list, Object value, int index, JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) { boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof DialogData)) if(!(value instanceof DialogData dialog))
return label; return label;
DialogData dialog=(DialogData) value;
StringBuilder builder=new StringBuilder(); StringBuilder builder=new StringBuilder();
if(dialog.name==null||dialog.name.isEmpty()) if(dialog.name==null||dialog.name.isEmpty())
builder.append("[[Blank Option]]"); builder.append("[[Blank Option]]");

View File

@@ -27,17 +27,16 @@ public class ItemsEditor extends JComponent {
JList list, Object value, int index, JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) { boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof ItemData)) if(!(value instanceof ItemData item))
return label; return label;
ItemData Item=(ItemData) value;
// Get the renderer component from parent class // Get the renderer component from parent class
label.setText(Item.name); label.setText(item.name);
if(itemAtlas==null) if(itemAtlas==null)
itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS)); itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS));
if(itemAtlas.has(Item.iconName)) if(itemAtlas.has(item.iconName))
label.setIcon(itemAtlas.get(Item.iconName)); label.setIcon(itemAtlas.get(item.iconName));
else else
{ {
ImageIcon img=itemAtlas.getAny(); ImageIcon img=itemAtlas.getAny();

View File

@@ -26,9 +26,8 @@ public class QuestEditor extends JComponent {
JList list, Object value, int index, JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) { boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestData)) if(!(value instanceof AdventureQuestData quest))
return label; return label;
AdventureQuestData quest=(AdventureQuestData) value;
// Get the renderer component from parent class // Get the renderer component from parent class
label.setText(quest.name); label.setText(quest.name);

View File

@@ -26,9 +26,8 @@ public class QuestStageEditor extends JComponent{
JList list, Object value, int index, JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) { boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestStage)) if(!(value instanceof AdventureQuestStage stageData))
return label; return label;
AdventureQuestStage stageData=(AdventureQuestStage) value;
label.setText(stageData.name); label.setText(stageData.name);
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually? //label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
return label; return label;

View File

@@ -43,9 +43,8 @@ public class WorldEditor extends JComponent {
JList list, Object value, int index, JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) { boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeData)) if(!(value instanceof BiomeData biome))
return label; return label;
BiomeData biome=(BiomeData) value;
// Get the renderer component from parent class // Get the renderer component from parent class
label.setText(biome.name); label.setText(biome.name);

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-ai</artifactId> <artifactId>forge-ai</artifactId>

View File

@@ -115,8 +115,8 @@ public class AiAttackController {
} // overloaded constructor to evaluate single specified attacker } // overloaded constructor to evaluate single specified attacker
private void refreshCombatants(GameEntity defender) { private void refreshCombatants(GameEntity defender) {
if (defender instanceof Card && ((Card) defender).isBattle()) { if (defender instanceof Card card && card.isBattle()) {
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer()); this.oppList = getOpponentCreatures(card.getProtectingPlayer());
} else { } else {
this.oppList = getOpponentCreatures(defendingOpponent); this.oppList = getOpponentCreatures(defendingOpponent);
} }
@@ -312,7 +312,8 @@ public class AiAttackController {
} }
} }
// Poison opponent if unblocked // Poison opponent if unblocked
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) { if (defender instanceof Player player
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
return true; return true;
} }
@@ -849,10 +850,9 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated // decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons) // (though usually rather try to avoid this situation for performance reasons)
if (defender != defendingOpponent) { if (defender != defendingOpponent) {
if (defender instanceof Player) { if (defender instanceof Player p) {
defendingOpponent = (Player) defender; defendingOpponent = p;
} else if (defender instanceof Card) { } else if (defender instanceof Card defCard) {
Card defCard = (Card) defender;
if (defCard.isBattle()) { if (defCard.isBattle()) {
defendingOpponent = defCard.getProtectingPlayer(); defendingOpponent = defCard.getProtectingPlayer();
} else { } else {
@@ -946,8 +946,8 @@ public class AiAttackController {
return 1; return 1;
} }
// or weakest player // or weakest player
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) { if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife(); return p1.getLife() - p2.getLife();
} }
} }
return r2.getValue() - r1.getValue(); return r2.getValue() - r1.getValue();
@@ -1314,7 +1314,7 @@ public class AiAttackController {
attackersAssigned.add(attacker); attackersAssigned.add(attacker);
// check if attackers are enough to finish the attacked planeswalker // check if attackers are enough to finish the attacked planeswalker
if (i < left.size() - 1 && defender instanceof Card) { if (i < left.size() - 1 && defender instanceof Card card) {
final int blockNum = this.blockers.size(); final int blockNum = this.blockers.size();
int attackNum = 0; int attackNum = 0;
int damage = 0; int damage = 0;
@@ -1328,7 +1328,7 @@ public class AiAttackController {
} }
} }
// if enough damage: switch to next planeswalker // if enough damage: switch to next planeswalker
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) { if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
break; break;
} }
} }
@@ -1754,10 +1754,12 @@ public class AiAttackController {
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) { private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name // TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false; boolean revengeOfRavens = false;
if (defender instanceof Player) { if (defender instanceof Player player) {
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty(); revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
} else if (defender instanceof Card) { CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty(); } else if (defender instanceof Card card) {
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} }
if (!revengeOfRavens) { if (!revengeOfRavens) {

View File

@@ -161,12 +161,12 @@ public class AiBlockController {
// defend battles with fewer defense counters before battles with more defense counters, // defend battles with fewer defense counters before battles with more defense counters,
// if planeswalker/battle will be too difficult to defend don't even bother // if planeswalker/battle will be too difficult to defend don't even bother
for (GameEntity defender : defenders) { for (GameEntity defender : defenders) {
if ((defender instanceof Card && ((Card) defender).getController().equals(ai)) if ((defender instanceof Card card1 && card1.getController().equals(ai))
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) { || (defender instanceof Card card2 && card2.isBattle() && card2.getProtectingPlayer().equals(ai))) {
final CardCollection attackers = combat.getAttackersOf(defender); final CardCollection ccAttackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat // Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers); CardLists.sortByPowerDesc(ccAttackers);
sortedAttackers.addAll(attackers); sortedAttackers.addAll(ccAttackers);
} else if (defender instanceof Player && defender.equals(ai)) { } else if (defender instanceof Player && defender.equals(ai)) {
firstAttacker = combat.getAttackersOf(defender); firstAttacker = combat.getAttackersOf(defender);
CardLists.sortByPowerDesc(firstAttacker); CardLists.sortByPowerDesc(firstAttacker);
@@ -872,9 +872,9 @@ public class AiBlockController {
CardCollection threatenedPWs = new CardCollection(); CardCollection threatenedPWs = new CardCollection();
for (final Card attacker : attackers) { for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker); GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card) { if (def instanceof Card card) {
if (!onlyIfLethal) { if (!onlyIfLethal) {
threatenedPWs.add((Card) def); threatenedPWs.add(card);
} else { } else {
int damageToPW = 0; int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) { for (final Card pwatkr : combat.getAttackersOf(def)) {
@@ -906,12 +906,12 @@ public class AiBlockController {
continue; continue;
} }
GameEntity def = combat.getDefenderByAttacker(attacker); GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains(def)) { if (def instanceof Card card && threatenedPWs.contains(def)) {
Card blockerDecided = null; Card blockerDecided = null;
for (final Card blocker : chumpPWDefenders) { for (final Card blocker : chumpPWDefenders) {
if (CombatUtil.canBlock(attacker, blocker, combat)) { if (CombatUtil.canBlock(attacker, blocker, combat)) {
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
pwsWithChumpBlocks.add((Card) def); pwsWithChumpBlocks.add(card);
chosenChumpBlockers.add(blocker); chosenChumpBlockers.add(blocker);
blockerDecided = blocker; blockerDecided = blocker;
blockersLeft.remove(blocker); blockersLeft.remove(blocker);
@@ -1346,8 +1346,8 @@ public class AiBlockController {
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES) && ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES)
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount; && aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW) boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
&& combat.getDefenderByAttacker(attacker) instanceof Card && combat.getDefenderByAttacker(attacker) instanceof Card card
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker(); && card.isPlaneswalker();
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0; boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped. return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.

View File

@@ -1408,9 +1408,7 @@ public class ComputerUtil {
} }
} }
for (final CostPart part : abCost.getCostParts()) { for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice) { if (part instanceof CostSacrifice sac) {
final CostSacrifice sac = (CostSacrifice) part;
final String type = sac.getType(); final String type = sac.getType();
if (type.equals("CARDNAME")) { if (type.equals("CARDNAME")) {
@@ -1776,9 +1774,7 @@ public class ComputerUtil {
noRegen = true; noRegen = true;
} }
for (final Object o : objects) { for (final Object o : objects) {
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
// indestructible // indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) { if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue; continue;
@@ -1842,9 +1838,7 @@ public class ComputerUtil {
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) { if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
threatened.add(c); threatened.add(c);
} }
} else if (o instanceof Player) { } else if (o instanceof Player p) {
final Player p = (Player) o;
if (source.hasKeyword(Keyword.INFECT)) { if (source.hasKeyword(Keyword.INFECT)) {
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) { if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
threatened.add(p); threatened.add(p);
@@ -1862,8 +1856,7 @@ public class ComputerUtil {
|| saviourApi == null)) { || saviourApi == null)) {
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack); final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
for (final Object o : objects) { for (final Object o : objects) {
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
final boolean canRemove = (c.getNetToughness() <= dmg) final boolean canRemove = (c.getNetToughness() <= dmg)
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false)); || (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
if (!canRemove) { if (!canRemove) {
@@ -1909,9 +1902,7 @@ public class ComputerUtil {
|| saviourApi == ApiType.Protection || saviourApi == null || saviourApi == ApiType.Protection || saviourApi == null
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) { || saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
for (final Object o : objects) { for (final Object o : objects) {
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) { if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue; continue;
} }
@@ -1960,8 +1951,7 @@ public class ComputerUtil {
&& topStack.hasParam("Destination") && topStack.hasParam("Destination")
&& topStack.getParam("Destination").equals("Exile")) { && topStack.getParam("Destination").equals("Exile")) {
for (final Object o : objects) { for (final Object o : objects) {
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
// give Shroud to targeted creatures // give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) { if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue; continue;
@@ -1988,8 +1978,7 @@ public class ComputerUtil {
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) { || saviourApi == ApiType.Protection || saviourApi == null)) {
for (final Object o : objects) { for (final Object o : objects) {
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
// give Shroud to targeted creatures // give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) { if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue; continue;
@@ -2011,8 +2000,7 @@ public class ComputerUtil {
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false; boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
if (enableCurseAuraRemoval) { if (enableCurseAuraRemoval) {
for (final Object o : objects) { for (final Object o : objects) {
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
// give Shroud to targeted creatures // give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) { if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue; continue;

View File

@@ -176,7 +176,7 @@ public class ComputerUtilCombat {
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) { public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
int damage = attacker.getNetCombatDamage(); int damage = attacker.getNetCombatDamage();
int sum = 0; int sum = 0;
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) { if (attacked instanceof Player player && !player.canLoseLife()) {
return 0; return 0;
} }
@@ -2539,20 +2539,20 @@ public class ComputerUtilCombat {
if (combat != null) { if (combat != null) {
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard()); GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving. // 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
if (def instanceof Card && Iterables.contains(defenders, def)) { if (def instanceof Card card && Iterables.contains(defenders, def)) {
if (((Card) def).isPlaneswalker()) { if (card.isPlaneswalker()) {
return def; return def;
} }
if (((Card) def).isBattle()) { if (card.isBattle()) {
return def; return def;
} }
} }
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably. // 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
for (GameEntity p : defenders) { for (GameEntity p : defenders) {
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) { if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
return p; return p;
} }
if (p instanceof Card && !ComputerUtilCard.canBeBlockedProfitably(((Card)p).getController(), attacker, true)) { if (p instanceof Card card && !ComputerUtilCard.canBeBlockedProfitably(card.getController(), attacker, true)) {
return p; return p;
} }
} }

View File

@@ -50,8 +50,7 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPutCounter) { if (part instanceof CostPutCounter addCounter) {
final CostPutCounter addCounter = (CostPutCounter) part;
final CounterType type = addCounter.getCounter(); final CounterType type = addCounter.getCounter();
if (type.is(CounterEnumType.M1M1)) { if (type.is(CounterEnumType.M1M1)) {
@@ -77,9 +76,7 @@ public class ComputerUtilCost {
} }
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false); final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) { if (part instanceof CostRemoveCounter remCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType type = remCounter.counter; final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) { if (!part.payCostFromSource()) {
if (type.is(CounterEnumType.P1P1)) { if (type.is(CounterEnumType.P1P1)) {
@@ -106,9 +103,7 @@ public class ComputerUtilCost {
&& !source.hasKeyword(Keyword.UNDYING)) { && !source.hasKeyword(Keyword.UNDYING)) {
return false; return false;
} }
} else if (part instanceof CostRemoveAnyCounter) { } else if (part instanceof CostRemoveAnyCounter remCounter) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision pay = decision.visit(remCounter); PaymentDecision pay = decision.visit(remCounter);
return pay != null; return pay != null;
} }
@@ -133,9 +128,7 @@ public class ComputerUtilCost {
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand)); CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard) { if (part instanceof CostDiscard disc) {
final CostDiscard disc = (CostDiscard) part;
final String type = disc.getType(); final String type = disc.getType();
final CardCollection typeList; final CardCollection typeList;
int num; int num;
@@ -187,8 +180,7 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDamage) { if (part instanceof CostDamage pay) {
final CostDamage pay = (CostDamage) part;
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false); int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
if (ai.getLife() - realDamage < remainingLife if (ai.getLife() - realDamage < remainingLife
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife() && realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
@@ -220,9 +212,7 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) { if (part instanceof CostPayLife payLife) {
final CostPayLife payLife = (CostPayLife) part;
int amount = payLife.getAbilityAmount(sourceAbility); int amount = payLife.getAbilityAmount(sourceAbility);
// check if there's override for the remainingLife threshold // check if there's override for the remainingLife threshold
@@ -296,8 +286,7 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) { if (part instanceof CostSacrifice sac) {
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility); final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
if (sac.payCostFromSource() && source.isCreature()) { if (sac.payCostFromSource() && source.isCreature()) {
@@ -346,12 +335,11 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) { if (part instanceof CostSacrifice sac) {
if (suppressRecursiveSacCostCheck) { if (suppressRecursiveSacCostCheck) {
return false; return false;
} }
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility); final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
String type = sac.getType(); String type = sac.getType();

View File

@@ -1326,7 +1326,9 @@ public class ComputerUtilMana {
} }
} }
CostAdjustment.adjust(manaCost, sa, null, test); if (!effect) {
CostAdjustment.adjust(manaCost, sa, null, test);
}
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
ManaCost mkCost = sa.getPayCosts().getTotalMana(); ManaCost mkCost = sa.getPayCosts().getTotalMana();

View File

@@ -1265,8 +1265,7 @@ public class PlayerControllerAi extends PlayerController {
public boolean playSaFromPlayEffect(SpellAbility tgtSA) { public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
boolean optional = !tgtSA.getPayCosts().isMandatory(); boolean optional = !tgtSA.getPayCosts().isMandatory();
boolean noManaCost = tgtSA.hasParam("WithoutManaCost"); boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell? if (tgtSA instanceof Spell spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA;
// TODO if mandatory AI is only forced to use mana when it's already in the pool // TODO if mandatory AI is only forced to use mana when it's already in the pool
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) { if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
return ComputerUtil.playStack(tgtSA, player, getGame()); return ComputerUtil.playStack(tgtSA, player, getGame());

View File

@@ -342,9 +342,9 @@ public abstract class SpellAbilityAi {
for (T ent : options) { for (T ent : options) {
if (ent instanceof Player) { if (ent instanceof Player) {
hasPlayer = true; hasPlayer = true;
} else if (ent instanceof Card) { } else if (ent instanceof Card card) {
hasCard = true; hasCard = true;
if (((Card)ent).isPlaneswalker() || ((Card)ent).isBattle()) { if (card.isPlaneswalker() || card.isBattle()) {
hasAttackableCard = true; hasAttackableCard = true;
} }
} }

View File

@@ -22,7 +22,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -138,8 +137,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic != null) { if (aiLogic != null) {
if (aiLogic.equals("Always")) { if (aiLogic.equals("Always")) {
return true; return true;
} else if (aiLogic.startsWith("ExileSpell")) {
return doExileSpellLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc. } else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa); return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc. } else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
@@ -878,6 +875,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone"))); origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone")));
} }
if (origin.contains(ZoneType.Stack) && doExileSpellLogic(ai, sa, mandatory)) {
return true;
}
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -902,7 +903,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true); list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) { if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30); list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
@@ -2061,31 +2061,24 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
private boolean doExileSpellLogic(final Player aiPlayer, final SpellAbility sa) { private static boolean doExileSpellLogic(final Player ai, final SpellAbility sa, final boolean mandatory) {
String aiLogic = sa.getParamOrDefault("AILogic", ""); List<ApiType> dangerousApi = null;
SpellAbilityStackInstance top = aiPlayer.getGame().getStack().peek(); CardCollection spells = new CardCollection(ai.getGame().getStackZone().getCards());
List<ApiType> dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll); Collections.reverse(spells);
int manaCost = 0; if (!mandatory && !spells.isEmpty()) {
int minCost = 0; spells = spells.subList(0, 1);
spells = ComputerUtil.filterAITgts(sa, ai, spells, true);
if (aiLogic.contains(".")) { dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
minCost = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
} }
if (top != null) { for (Card c : spells) {
SpellAbility topSA = top.getSpellAbility(); SpellAbility topSA = ai.getGame().getStack().getSpellMatchingHost(c);
if (topSA != null) { if (topSA != null && (dangerousApi == null ||
if (topSA.getPayCosts().hasManaCost()) { (dangerousApi.contains(topSA.getApi()) && topSA.getActivatingPlayer().isOpponentOf(ai)))
manaCost = topSA.getPayCosts().getTotalMana().getCMC(); && sa.canTarget(topSA)) {
} sa.resetTargets();
sa.getTargets().add(topSA);
if ((manaCost >= minCost || dangerousApi.contains(topSA.getApi())) return sa.isTargetNumberValid();
&& topSA.getActivatingPlayer().isOpponentOf(aiPlayer)
&& sa.canTargetSpellAbility(topSA)) {
sa.resetTargets();
sa.getTargets().add(topSA);
return sa.isTargetNumberValid();
}
} }
} }
return false; return false;

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-core</artifactId> <artifactId>forge-core</artifactId>

View File

@@ -222,7 +222,7 @@ final class CardFace implements ICardFace, Cloneable {
else variant.replacements.addAll(0, this.replacements); else variant.replacements.addAll(0, this.replacements);
if(variant.variables == null) variant.variables = this.variables; if(variant.variables == null) variant.variables = this.variables;
else variant.variables.putAll(this.variables); else this.variables.forEach((k, v) -> variant.variables.putIfAbsent(k, v));
if(variant.nonAbilityText == null) variant.nonAbilityText = this.nonAbilityText; if(variant.nonAbilityText == null) variant.nonAbilityText = this.nonAbilityText;
if(variant.draftActions == null) variant.draftActions = this.draftActions; if(variant.draftActions == null) variant.draftActions = this.draftActions;

View File

@@ -645,9 +645,8 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (o instanceof Deck) { if (o instanceof DeckBase deckBase) {
final DeckBase dbase = (DeckBase) o; boolean deckBaseEquals = super.equals(deckBase);
boolean deckBaseEquals = super.equals(dbase);
if (!deckBaseEquals) if (!deckBaseEquals)
return false; return false;
// ok so far we made sure they do have the same name. Now onto comparing parts // ok so far we made sure they do have the same name. Now onto comparing parts

View File

@@ -472,7 +472,8 @@ public class DeckRecognizer {
"side", "sideboard", "sb", "side", "sideboard", "sb",
"main", "card", "mainboard", "main", "card", "mainboard",
"avatar", "commander", "schemes", "avatar", "commander", "schemes",
"conspiracy", "planes", "deck", "dungeon"}; "conspiracy", "planes", "deck", "dungeon",
"attractions", "contraptions"};
private static CharSequence[] allCardTypes(){ private static CharSequence[] allCardTypes(){
List<String> cardTypesList = new ArrayList<>(); List<String> cardTypesList = new ArrayList<>();
@@ -670,6 +671,9 @@ public class DeckRecognizer {
// ok so the card has been found - let's see if there's any restriction on the set // ok so the card has been found - let's see if there's any restriction on the set
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine, return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
currentDeckSection, true); currentDeckSection, true);
// On the off chance we accidentally interpreted part of the card's name as a set code, e.g. "Tyrranax Rex"
if (data.isMTGCard(cardName + " " + setCode))
continue;
// UNKNOWN card as in the Counterspell|FEM case // UNKNOWN card as in the Counterspell|FEM case
return Token.UnknownCard(cardName, setCode, cardCount); return Token.UnknownCard(cardName, setCode, cardCount);
} }

View File

@@ -65,8 +65,8 @@ public class BoosterGenerator {
} }
public static List<PaperCard> getBoosterPack(SealedTemplate template) { public static List<PaperCard> getBoosterPack(SealedTemplate template) {
if (template instanceof SealedTemplateWithSlots) { if (template instanceof SealedTemplateWithSlots slots) {
return BoosterGenerator.getBoosterPack((SealedTemplateWithSlots) template); return BoosterGenerator.getBoosterPack(slots);
} }
List<PaperCard> result = new ArrayList<>(); List<PaperCard> result = new ArrayList<>();

View File

@@ -275,7 +275,7 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
@Override @Override
public boolean equals(final Object obj) { public boolean equals(final Object obj) {
return (obj instanceof ItemPool) && return (obj instanceof ItemPool ip) &&
(this.items.equals(((ItemPool)obj).items)); (this.items.equals(ip.items));
} }
} }

View File

@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-game</artifactId> <artifactId>forge-game</artifactId>

View File

@@ -337,9 +337,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (params.containsKey("Blessing")) { if (params.containsKey("Blessing")) {
if ("True".equalsIgnoreCase(params.get("Blessing")) != hostController.hasBlessing()) return false; if ("True".equalsIgnoreCase(params.get("Blessing")) != hostController.hasBlessing()) return false;
} }
if (params.containsKey("MaxSpeed")) {
if ("True".equalsIgnoreCase(params.get("MaxSpeed")) != hostController.maxSpeed()) return false;
}
if (params.containsKey("DayTime")) { if (params.containsKey("DayTime")) {
if ("Day".equalsIgnoreCase(params.get("DayTime"))) { if ("Day".equalsIgnoreCase(params.get("DayTime"))) {

View File

@@ -1185,6 +1185,12 @@ public class Game {
for (Player player : getRegisteredPlayers()) { for (Player player : getRegisteredPlayers()) {
player.onCleanupPhase(); player.onCleanupPhase();
} }
for (final Card c : getCardsIncludePhasingIn(ZoneType.Battlefield)) {
c.onCleanupPhase(getPhaseHandler().getPlayerTurn());
}
for (final Card card : getCardsInGame()) {
card.resetActivationsPerTurn();
}
} }
public void addCounterAddedThisTurn(Player putter, CounterType cType, Card card, Integer value) { public void addCounterAddedThisTurn(Player putter, CounterType cType, Card card, Integer value) {

View File

@@ -82,12 +82,6 @@ public class GameAction {
game = game0; game = game0;
} }
public final void resetActivationsPerTurn() {
for (final Card card : game.getCardsInGame()) {
card.resetActivationsPerTurn();
}
}
public Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer position, SpellAbility cause) { public Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer position, SpellAbility cause) {
return changeZone(zoneFrom, zoneTo, c, position, cause, null); return changeZone(zoneFrom, zoneTo, c, position, cause, null);
} }
@@ -107,6 +101,8 @@ public class GameAction {
} }
return c; return c;
} }
// dev mode
if (zoneFrom == null && !c.isToken()) { if (zoneFrom == null && !c.isToken()) {
zoneTo.add(c, position, CardCopyService.getLKICopy(c)); zoneTo.add(c, position, CardCopyService.getLKICopy(c));
checkStaticAbilities(); checkStaticAbilities();
@@ -314,37 +310,34 @@ public class GameAction {
c.getOwner().setCommanderReplacementSuppressed(true); c.getOwner().setCommanderReplacementSuppressed(true);
} }
// in addition to actual tokens, cards "made" by digital-only mechanics
// are also added to inbound tokens so their etb replacements will work
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
copied.getOwner().addInboundToken(copied);
}
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(copied); Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(copied);
repParams.put(AbilityKey.CardLKI, lastKnownInfo); repParams.put(AbilityKey.CardLKI, lastKnownInfo);
repParams.put(AbilityKey.Cause, cause); repParams.put(AbilityKey.Cause, cause);
repParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType() : null); repParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType() : null);
repParams.put(AbilityKey.Destination, zoneTo.getZoneType()); repParams.put(AbilityKey.Destination, zoneTo.getZoneType());
if (toBattlefield) { if (toBattlefield) {
repParams.put(AbilityKey.EffectOnly, true); repParams.put(AbilityKey.EffectOnly, true);
repParams.put(AbilityKey.CounterTable, table); repParams.put(AbilityKey.CounterTable, table);
repParams.put(AbilityKey.CounterMap, table.column(copied)); repParams.put(AbilityKey.CounterMap, table.column(copied));
} }
if (params != null) { if (params != null) {
repParams.putAll(params); repParams.putAll(params);
} }
// in addition to actual tokens, cards "made" by digital-only mechanics
// are also added to inbound tokens so their etb replacements will work
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
copied.getOwner().addInboundToken(copied);
}
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams); ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams);
copied.getOwner().removeInboundToken(copied);
if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) { if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) {
// reset failed manifested Cards back to original // reset failed manifested Cards back to original
if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) { if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) {
c.forceTurnFaceUp(); c.forceTurnFaceUp();
} }
copied.getOwner().removeInboundToken(copied);
if (repres == ReplacementResult.Prevented) { if (repres == ReplacementResult.Prevented) {
c.clearControllers(); c.clearControllers();
cleanStaticEffect(staticEff, copied); cleanStaticEffect(staticEff, copied);
@@ -359,10 +352,6 @@ public class GameAction {
if (c.isInZone(ZoneType.Stack) && !zoneTo.is(ZoneType.Graveyard)) { if (c.isInZone(ZoneType.Stack) && !zoneTo.is(ZoneType.Graveyard)) {
return moveToGraveyard(c, cause, params); return moveToGraveyard(c, cause, params);
} }
copied.clearDevoured();
copied.clearDelved();
copied.clearExploited();
} else if (toBattlefield && !c.isInPlay()) { } else if (toBattlefield && !c.isInPlay()) {
// was replaced with another Zone Change // was replaced with another Zone Change
if (c.removeChangedState()) { if (c.removeChangedState()) {
@@ -379,8 +368,6 @@ public class GameAction {
copied.setGameTimestamp(game.getNextTimestamp()); copied.setGameTimestamp(game.getNextTimestamp());
} }
copied.getOwner().removeInboundToken(copied);
// Aura entering as Copy from stack // Aura entering as Copy from stack
// without targets it is sent to graveyard // without targets it is sent to graveyard
if (copied.isAura() && !copied.isAttachedToEntity() && toBattlefield) { if (copied.isAura() && !copied.isAttachedToEntity() && toBattlefield) {
@@ -432,10 +419,6 @@ public class GameAction {
} }
} }
if (suppress) {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
}
if (zoneFrom != null) { if (zoneFrom != null) {
if (fromBattlefield && game.getCombat() != null) { if (fromBattlefield && game.getCombat() != null) {
if (!toBattlefield) { if (!toBattlefield) {
@@ -549,29 +532,25 @@ public class GameAction {
// order here is important so it doesn't unattach cards that might have returned from UntilHostLeavesPlay // order here is important so it doesn't unattach cards that might have returned from UntilHostLeavesPlay
unattachCardLeavingBattlefield(copied, c); unattachCardLeavingBattlefield(copied, c);
c.runLeavesPlayCommands(); c.runLeavesPlayCommands();
if (copied.isTapped()) {
copied.setTapped(false); //untap card after it leaves the battlefield if needed
game.fireEvent(new GameEventCardTapped(c, false));
}
} }
if (fromGraveyard) { if (fromGraveyard) {
game.addLeftGraveyardThisTurn(lastKnownInfo); game.addLeftGraveyardThisTurn(lastKnownInfo);
} }
// do ETB counters after zone add
if (!suppress && toBattlefield && !table.isEmpty()) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (c.hasChosenColorSpire()) { if (c.hasChosenColorSpire()) {
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID())); copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID()));
} }
copied.updateStateForView(); copied.updateStateForView();
if (fromBattlefield) { // needed for counters + ascend
copied.setDamage(0); //clear damage after a card leaves the battlefield if (!suppress && toBattlefield) {
copied.setHasBeenDealtDeathtouchDamage(false); game.getTriggerHandler().registerActiveTrigger(copied, false);
if (copied.isTapped()) {
copied.setTapped(false); //untap card after it leaves the battlefield if needed
game.fireEvent(new GameEventCardTapped(c, false));
}
} }
if (!table.isEmpty()) { if (!table.isEmpty()) {
@@ -579,12 +558,12 @@ public class GameAction {
game.getTriggerHandler().suppressMode(TriggerType.Always); game.getTriggerHandler().suppressMode(TriggerType.Always);
// Need to apply any static effects to produce correct triggers // Need to apply any static effects to produce correct triggers
checkStaticAbilities(); checkStaticAbilities();
// do ETB counters after zone add
table.replaceCounterEffect(game, null, true, true, params);
game.getTriggerHandler().clearSuppression(TriggerType.Always);
} }
table.replaceCounterEffect(game, null, true, true, params);
// update static abilities after etb counters have been placed // update static abilities after etb counters have been placed
game.getTriggerHandler().clearSuppression(TriggerType.Always);
checkStaticAbilities(); checkStaticAbilities();
// 400.7g try adding keyword back into card if it doesn't already have it // 400.7g try adding keyword back into card if it doesn't already have it
@@ -607,25 +586,26 @@ public class GameAction {
c.cleanupExiledWith(); c.cleanupExiledWith();
} }
game.getTriggerHandler().clearActiveTriggers(copied, null);
game.getTriggerHandler().registerActiveTrigger(copied, false);
// play the change zone sound // play the change zone sound
game.fireEvent(new GameEventCardChangeZone(c, zoneFrom, zoneTo)); game.fireEvent(new GameEventCardChangeZone(c, zoneFrom, zoneTo));
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(copied); game.getTriggerHandler().clearActiveTriggers(copied, null);
runParams.put(AbilityKey.CardLKI, lastKnownInfo); game.getTriggerHandler().registerActiveTrigger(copied, false);
runParams.put(AbilityKey.Cause, cause);
runParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType().name() : null);
runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name());
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
runParams.put(AbilityKey.MergedCards, mergedCards);
if (params != null) { if (!suppress) {
runParams.putAll(params); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(copied);
runParams.put(AbilityKey.CardLKI, lastKnownInfo);
runParams.put(AbilityKey.Cause, cause);
runParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType().name() : null);
runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name());
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
runParams.put(AbilityKey.MergedCards, mergedCards);
if (params != null) {
runParams.putAll(params);
}
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
} }
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
if (fromBattlefield && !zoneFrom.getPlayer().equals(zoneTo.getPlayer())) { if (fromBattlefield && !zoneFrom.getPlayer().equals(zoneTo.getPlayer())) {
final Map<AbilityKey, Object> runParams2 = AbilityKey.mapFromCard(lastKnownInfo); final Map<AbilityKey, Object> runParams2 = AbilityKey.mapFromCard(lastKnownInfo);
runParams2.put(AbilityKey.OriginalController, zoneFrom.getPlayer()); runParams2.put(AbilityKey.OriginalController, zoneFrom.getPlayer());
@@ -635,31 +615,18 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams2, false); game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams2, false);
} }
if (suppress) {
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
if (zoneFrom == null) { if (zoneFrom == null) {
return copied; return copied;
} }
if (!c.isRealToken() && !toBattlefield) { // CR 708.9 reveal face-down card leaving
copied.clearDevoured(); if (wasFacedown && (fromBattlefield || (zoneFrom.is(ZoneType.Stack) && !toBattlefield))) {
copied.clearDelved();
copied.clearExploited();
}
// rule 504.6: reveal a face-down card leaving the stack
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && wasFacedown) {
Card revealLKI = CardCopyService.getLKICopy(c); Card revealLKI = CardCopyService.getLKICopy(c);
revealLKI.forceTurnFaceUp(); revealLKI.forceTurnFaceUp();
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card moves from the stack: "); reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the " + zoneFrom.toString() + ": ");
} }
if (fromBattlefield) { if (fromBattlefield) {
if (!c.isRealToken() && !c.isSpecialized()) {
copied.setState(CardStateName.Original, true);
}
// Soulbond unpairing // Soulbond unpairing
if (c.isPaired()) { if (c.isPaired()) {
c.getPairedWith().setPairedWith(null); c.getPairedWith().setPairedWith(null);
@@ -680,27 +647,12 @@ public class GameAction {
} }
changeZone(null, zoneTo, unmeld, position, cause, params); changeZone(null, zoneTo, unmeld, position, cause, params);
} }
// Reveal if face-down
if (wasFacedown) {
Card revealLKI = CardCopyService.getLKICopy(c);
revealLKI.forceTurnFaceUp();
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the battlefield: ");
copied.setState(CardStateName.Original, true);
}
} else if (toBattlefield) { } else if (toBattlefield) {
for (Player p : game.getPlayers()) { for (Player p : game.getPlayers()) {
copied.getDamageHistory().setNotAttackedSinceLastUpkeepOf(p); copied.getDamageHistory().setNotAttackedSinceLastUpkeepOf(p);
copied.getDamageHistory().setNotBlockedSinceLastUpkeepOf(p); copied.getDamageHistory().setNotBlockedSinceLastUpkeepOf(p);
copied.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(p); copied.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(p);
} }
} else if (zoneTo.is(ZoneType.Graveyard)
|| zoneTo.is(ZoneType.Hand)
|| zoneTo.is(ZoneType.Library)
|| zoneTo.is(ZoneType.Exile)) {
if (copied.isFaceDown()) {
copied.setState(CardStateName.Original, true);
}
} }
// Cards not on the battlefield / stack should not have controller // Cards not on the battlefield / stack should not have controller
@@ -748,12 +700,12 @@ public class GameAction {
eff.setLayerTimestamp(timestamp); eff.setLayerTimestamp(timestamp);
} else { } else {
// otherwise create effect first // otherwise create effect first
eff = SpellAbilityEffect.createEffect(cause, cause.getActivatingPlayer(), name, source.getImageKey(), timestamp); eff = SpellAbilityEffect.createEffect(cause, cause.getHostCard(), cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
eff.setRenderForUI(false); eff.setRenderForUI(false);
StaticAbility stAb = eff.addStaticAbility(AbilityUtils.getSVar(cause, cause.getParam("StaticEffect"))); StaticAbility stAb = eff.addStaticAbility(AbilityUtils.getSVar(cause, cause.getParam("StaticEffect")));
stAb.setActiveZone(EnumSet.of(ZoneType.Command)); stAb.setActiveZone(EnumSet.of(ZoneType.Command));
// needed for ETB lookahead like Bronzehide Lion // needed for ETB lookahead like Bronzehide Lion
stAb.putParam("AffectedZone", "Battlefield,Hand,Graveyard,Exile,Stack,Library,Command"); stAb.putParam("AffectedZone", "All");
SpellAbilityEffect.addForgetOnMovedTrigger(eff, "Battlefield"); SpellAbilityEffect.addForgetOnMovedTrigger(eff, "Battlefield");
game.getAction().moveToCommand(eff, cause); game.getAction().moveToCommand(eff, cause);
} }
@@ -1038,7 +990,8 @@ public class GameAction {
lki = CardCopyService.getLKICopy(c); lki = CardCopyService.getLKICopy(c);
} }
game.addChangeZoneLKIInfo(lki); game.addChangeZoneLKIInfo(lki);
if (lki.isInPlay()) { // CR 702.26k
if (lki.isInPlay() && !lki.isPhasedOut()) {
if (game.getCombat() != null) { if (game.getCombat() != null) {
game.getCombat().saveLKI(lki); game.getCombat().saveLKI(lki);
game.getCombat().removeFromCombat(c); game.getCombat().removeFromCombat(c);
@@ -1282,9 +1235,9 @@ public class GameAction {
// Update P/T and type in the view only once after all the cards have been processed, to avoid flickering // Update P/T and type in the view only once after all the cards have been processed, to avoid flickering
for (Card c : affectedCards) { for (Card c : affectedCards) {
c.updateNameforView(); c.updateNameforView();
c.updatePowerToughnessForView(); c.updatePTforView();
c.updateTypesForView(); c.updateTypesForView();
c.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering c.updateKeywords();
} }
// TODO filter out old copies from zone change // TODO filter out old copies from zone change
@@ -1569,6 +1522,12 @@ public class GameAction {
} }
} }
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that players speed becomes 1. See rule 702.179, “Start Your Engines!”
if (p.getSpeed() == 0 && p.getCardsIn(ZoneType.Battlefield).anyMatch(c -> c.hasKeyword(Keyword.START_YOUR_ENGINES))) {
p.increaseSpeed();
checkAgain = true;
}
if (handlePlaneswalkerRule(p, noRegCreats)) { if (handlePlaneswalkerRule(p, noRegCreats)) {
checkAgain = true; checkAgain = true;
} }
@@ -2684,8 +2643,8 @@ public class GameAction {
if (isCombat) { if (isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) { for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
final GameEntity ge = et.getKey(); final GameEntity ge = et.getKey();
if (ge instanceof Card) { if (ge instanceof Card c) {
((Card) ge).clearAssignedDamage(); c.clearAssignedDamage();
} }
} }
} }
@@ -2705,8 +2664,7 @@ public class GameAction {
continue; continue;
} }
if (e.getKey() instanceof Card && !lethalDamage.containsKey(e.getKey())) { if (e.getKey() instanceof Card c && !lethalDamage.containsKey(c)) {
Card c = (Card) e.getKey();
lethalDamage.put(c, c.getExcessDamageValue(false)); lethalDamage.put(c, c.getExcessDamageValue(false));
} }

View File

@@ -273,8 +273,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
} }
String controllerName; String controllerName;
if (defender instanceof Card) { if (defender instanceof Card c) {
Card c = ((Card)defender);
controllerName = c.isBattle() ? c.getProtectingPlayer().getName() : c.getController().getName(); controllerName = c.isBattle() ? c.getProtectingPlayer().getName() : c.getController().getName();
} else { } else {
controllerName = defender.getName(); controllerName = defender.getName();

View File

@@ -219,7 +219,9 @@ public class StaticEffect {
if (layers.contains(StaticAbilityLayer.TEXT)) { if (layers.contains(StaticAbilityLayer.TEXT)) {
// Revert changed color words // Revert changed color words
affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId()); if (hasParam("ChangeColorWordsTo")) {
affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId());
}
// remove changed name // remove changed name
if (hasParam("SetName") || hasParam("AddNames")) { if (hasParam("SetName") || hasParam("AddNames")) {
@@ -265,7 +267,7 @@ public class StaticEffect {
if (hasParam("AddAbility") || hasParam("GainsAbilitiesOf") if (hasParam("AddAbility") || hasParam("GainsAbilitiesOf")
|| hasParam("GainsAbilitiesOfDefined") || hasParam("GainsTriggerAbsOf") || hasParam("GainsAbilitiesOfDefined") || hasParam("GainsTriggerAbsOf")
|| hasParam("AddTrigger") || hasParam("AddStaticAbility") || hasParam("AddTrigger") || hasParam("AddStaticAbility")
|| hasParam("AddReplacementEffects") || hasParam("RemoveAllAbilities") || hasParam("AddReplacementEffect") || hasParam("RemoveAllAbilities")
|| hasParam("RemoveLandTypes")) { || hasParam("RemoveLandTypes")) {
affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId()); affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId());
} }
@@ -275,11 +277,14 @@ public class StaticEffect {
} }
affectedCard.removeChangedSVars(getTimestamp(), ability.getId()); affectedCard.removeChangedSVars(getTimestamp(), ability.getId());
// need update for clean reapply
affectedCard.updateKeywordsCache(affectedCard.getCurrentState());
} }
if (layers.contains(StaticAbilityLayer.SETPT)) { if (layers.contains(StaticAbilityLayer.CHARACTERISTIC) || layers.contains(StaticAbilityLayer.SETPT)) {
if (hasParam("SetPower") || hasParam("SetToughness")) { if (hasParam("SetPower") || hasParam("SetToughness")) {
affectedCard.removeNewPT(getTimestamp(), ability.getId()); affectedCard.removeNewPT(getTimestamp(), ability.getId(), false);
} }
} }
@@ -311,8 +316,6 @@ public class StaticEffect {
affectedCard.removeCanBlockAdditional(getTimestamp()); affectedCard.removeCanBlockAdditional(getTimestamp());
} }
} }
affectedCard.updateAbilityTextForView(); // need to update keyword cache for clean reapply
} }
return affectedCards; return affectedCards;
} }

View File

@@ -14,6 +14,7 @@ import forge.game.*;
import forge.game.ability.AbilityFactory.AbilityRecordType; import forge.game.ability.AbilityFactory.AbilityRecordType;
import forge.game.card.*; import forge.game.card.*;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.IndividualCostPaymentInstance;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana; import forge.game.mana.Mana;
@@ -1362,10 +1363,8 @@ public class AbilityUtils {
} }
// do blessing there before condition checks // do blessing there before condition checks
if (source.hasKeyword(Keyword.ASCEND)) { if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) {
if (controller.getZone(ZoneType.Battlefield).size() >= 10) { controller.setBlessing(true);
controller.setBlessing(true);
}
} }
if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) { if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) {
@@ -2839,7 +2838,13 @@ public class AbilityUtils {
final String[] workingCopy = paidparts[0].split("_"); final String[] workingCopy = paidparts[0].split("_");
final String validFilter = workingCopy[1]; final String validFilter = workingCopy[1];
// use objectXCount ? // use objectXCount ?
return CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size(); int activated = CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size();
for (IndividualCostPaymentInstance i : game.costPaymentStack) {
if (i.getPayment().getAbility().isValid(validFilter, player, c, ctb)) {
activated++;
}
}
return activated;
} }
// Count$ThisTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid> // Count$ThisTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid>
@@ -3699,6 +3704,10 @@ public class AbilityUtils {
return doXMath(amount, m, source, ctb); return doXMath(amount, m, source, ctb);
} }
if (value.equals("AttractionsVisitedThisTurn")) {
return doXMath(player.getAttractionsVisitedThisTurn(), m, source, ctb);
}
if (value.startsWith("PlaneswalkedToThisTurn")) { if (value.startsWith("PlaneswalkedToThisTurn")) {
int found = 0; int found = 0;
String name = value.split(" ")[1]; String name = value.split(" ")[1];

View File

@@ -588,11 +588,10 @@ public abstract class SpellAbilityEffect {
// create a basic template for Effect to be used somewhere els // create a basic template for Effect to be used somewhere els
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image) { public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image) {
return createEffect(sa, controller, name, image, controller.getGame().getNextTimestamp()); return createEffect(sa, sa.getHostCard(), controller, name, image, controller.getGame().getNextTimestamp());
} }
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image, final long timestamp) { public static Card createEffect(final SpellAbility sa, final Card hostCard, final Player controller, final String name, final String image, final long timestamp) {
final Card hostCard = sa.getHostCard(); final Game game = controller.getGame();
final Game game = hostCard.getGame();
final Card eff = new Card(game.nextCardId(), game); final Card eff = new Card(game.nextCardId(), game);
eff.setGameTimestamp(timestamp); eff.setGameTimestamp(timestamp);
@@ -608,12 +607,7 @@ public abstract class SpellAbilityEffect {
eff.setRarity(hostCard.getRarity()); eff.setRarity(hostCard.getRarity());
} }
if (sa.hasParam("Boon")) {
eff.setBoon(true);
}
eff.setOwner(controller); eff.setOwner(controller);
eff.setSVars(sa.getSVars());
eff.setSetCode(hostCard.getSetCode()); eff.setSetCode(hostCard.getSetCode());
if (image != null) { if (image != null) {
@@ -621,7 +615,12 @@ public abstract class SpellAbilityEffect {
} }
eff.setGamePieceType(GamePieceType.EFFECT); eff.setGamePieceType(GamePieceType.EFFECT);
eff.setEffectSource(sa); if (sa != null) {
eff.setEffectSource(sa);
eff.setSVars(sa.getSVars());
} else {
eff.setEffectSource(hostCard);
}
return eff; return eff;
} }
@@ -1041,7 +1040,9 @@ public abstract class SpellAbilityEffect {
exilingSource = cause.getOriginalHost(); exilingSource = cause.getOriginalHost();
} }
movedCard.setExiledWith(exilingSource); movedCard.setExiledWith(exilingSource);
movedCard.setExiledBy(cause.getActivatingPlayer()); Player exiler = cause.hasParam("DefinedExiler") ?
getDefinedPlayersOrTargeted(cause, "DefinedExiler").get(0) : cause.getActivatingPlayer();
movedCard.setExiledBy(exiler);
} }
public static GameCommand exileEffectCommand(final Game game, final Card effect) { public static GameCommand exileEffectCommand(final Game game, final Card effect) {

View File

@@ -202,7 +202,7 @@ public class AnimateEffect extends AnimateEffectBase {
if (sa.isCrew()) { if (sa.isCrew()) {
gameCard.becomesCrewed(sa); gameCard.becomesCrewed(sa);
gameCard.updatePowerToughnessForView(); gameCard.updatePTforView();
} }
game.fireEvent(new GameEventCardStatsChanged(gameCard)); game.fireEvent(new GameEventCardStatsChanged(gameCard));

View File

@@ -132,7 +132,6 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
source = changingTgtSA.getTargetCard(); source = changingTgtSA.getTargetCard();
} }
Predicate<GameObject> filter = sa.hasParam("TargetRestriction") ? GameObjectPredicates.restriction(sa.getParam("TargetRestriction").split(","), activator, source, sa) : null; Predicate<GameObject> filter = sa.hasParam("TargetRestriction") ? GameObjectPredicates.restriction(sa.getParam("TargetRestriction").split(","), activator, source, sa) : null;
// TODO Creature.Other might not work yet as it should
TargetChoices newTarget = chooser.getController().chooseNewTargetsFor(changingTgtSA, filter, false); TargetChoices newTarget = chooser.getController().chooseNewTargetsFor(changingTgtSA, filter, false);
changingTgtSI.updateTarget(newTarget, sa.getHostCard()); changingTgtSI.updateTarget(newTarget, sa.getHostCard());
} }

View File

@@ -107,10 +107,8 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
final Zone originZone = game.getZoneOf(c); final Zone originZone = game.getZoneOf(c);
// Fizzle spells so that they are removed from stack (e.g. Summary Dismissal) // Fizzle spells so that they are removed from stack (e.g. Summary Dismissal)
if (sa.hasParam("Fizzle")) { if (originZone.is(ZoneType.Stack)) {
if (originZone.is(ZoneType.Exile) || originZone.is(ZoneType.Hand) || originZone.is(ZoneType.Stack)) { game.getStack().remove(c);
game.getStack().remove(c);
}
} }
if (remLKI) { if (remLKI) {

View File

@@ -558,22 +558,15 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
continue; continue;
} }
if (originZone.is(ZoneType.Stack)) {
game.getStack().remove(gameCard);
}
Card movedCard = null; Card movedCard = null;
Map<AbilityKey, Object> moveParams = AbilityKey.newMap(); Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, triggerList); AbilityKey.addCardZoneTableParams(moveParams, triggerList);
if (destination.equals(ZoneType.Library)) { if (destination.equals(ZoneType.Battlefield)) {
// If a card is moved to library from the stack, remove its spells from the stack
if (sa.hasParam("Fizzle")) {
// TODO only AI still targets as card, try to remove it
if (gameCard.isInZone(ZoneType.Exile) || gameCard.isInZone(ZoneType.Hand) || gameCard.isInZone(ZoneType.Stack)) {
// This only fizzles spells, not anything else.
game.getStack().remove(gameCard);
}
}
movedCard = game.getAction().moveToLibrary(gameCard, libraryPosition, sa, moveParams);
} else if (destination.equals(ZoneType.Battlefield)) {
moveParams.put(AbilityKey.SimultaneousETB, tgtCards); moveParams.put(AbilityKey.SimultaneousETB, tgtCards);
if (sa.isReplacementAbility()) { if (sa.isReplacementAbility()) {
ReplacementEffect re = sa.getReplacementEffect(); ReplacementEffect re = sa.getReplacementEffect();
@@ -725,15 +718,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
commandCards.add(movedCard); //add to list to reveal the commandzone cards commandCards.add(movedCard); //add to list to reveal the commandzone cards
} }
// If a card is Exiled from the stack, remove its spells from the stack
if (sa.hasParam("Fizzle")) {
if (gameCard.isInZone(ZoneType.Exile) || gameCard.isInZone(ZoneType.Hand)
|| gameCard.isInZone(ZoneType.Stack) || gameCard.isInZone(ZoneType.Command)) {
// This only fizzles spells, not anything else.
game.getStack().remove(gameCard);
}
}
if (sa.hasParam("WithCountersType")) { if (sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType")); CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(hostCard, sa.getParamOrDefault("WithCountersAmount", "1"), sa); int cAmount = AbilityUtils.calculateAmount(hostCard, sa.getParamOrDefault("WithCountersAmount", "1"), sa);

View File

@@ -63,8 +63,7 @@ public class ControlSpellEffect extends SpellAbilityEffect {
// Expand this area as it becomes needed // Expand this area as it becomes needed
// Use "DefinedExchange" to Reference Object that is Exchanging the other direction // Use "DefinedExchange" to Reference Object that is Exchanging the other direction
GameObject obj = Iterables.getFirst(getDefinedOrTargeted(sa, "DefinedExchange"), null); GameObject obj = Iterables.getFirst(getDefinedOrTargeted(sa, "DefinedExchange"), null);
if (obj instanceof Card) { if (obj instanceof Card c) {
Card c = (Card)obj;
if (!c.isInPlay() || si == null) { if (!c.isInPlay() || si == null) {
// Exchanging object isn't available, continue // Exchanging object isn't available, continue
continue; continue;

View File

@@ -257,7 +257,7 @@ public class CounterEffect extends SpellAbilityEffect {
params.put(AbilityKey.StackSa, tgtSA); params.put(AbilityKey.StackSa, tgtSA);
String destination = srcSA.hasParam("Destination") ? srcSA.getParam("Destination") : tgtSA.isAftermath() ? "Exile" : "Graveyard"; String destination = srcSA.getParamOrDefault("Destination", "Graveyard");
if (srcSA.hasParam("DestinationChoice")) { //Hinder if (srcSA.hasParam("DestinationChoice")) { //Hinder
List<String> pos = Arrays.asList(srcSA.getParam("DestinationChoice").split(",")); List<String> pos = Arrays.asList(srcSA.getParam("DestinationChoice").split(","));
destination = srcSA.getActivatingPlayer().getController().chooseSomeType(Localizer.getInstance().getMessage("lblRemoveDestination"), tgtSA, pos); destination = srcSA.getActivatingPlayer().getController().chooseSomeType(Localizer.getInstance().getMessage("lblRemoveDestination"), tgtSA, pos);

View File

@@ -252,8 +252,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (sa.hasParam("DividedRandomly")) { if (sa.hasParam("DividedRandomly")) {
CardCollection targets = new CardCollection(); CardCollection targets = new CardCollection();
for (final GameEntity obj : tgtObjects) { // check if each target is still OK for (final GameEntity obj : tgtObjects) { // check if each target is still OK
if (obj instanceof Card) { if (obj instanceof Card tgtCard) {
Card tgtCard = (Card) obj;
Card gameCard = game.getCardState(tgtCard, null); Card gameCard = game.getCardState(tgtCard, null);
if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) { if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) {
tgtObjects.remove(obj); tgtObjects.remove(obj);
@@ -284,8 +283,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
for (final GameEntity obj : tgtObjects) { for (final GameEntity obj : tgtObjects) {
// check if the object is still in game or if it was moved // check if the object is still in game or if it was moved
Card gameCard = null; Card gameCard = null;
if (obj instanceof Card) { if (obj instanceof Card tgtCard) {
Card tgtCard = (Card) obj;
gameCard = game.getCardState(tgtCard, null); gameCard = game.getCardState(tgtCard, null);
// gameCard is LKI in that case, the card is not in game anymore // gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change // or the timestamp did change
@@ -572,9 +570,8 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (sa.isDividedAsYouChoose() && !sa.usesTargeting()) { if (sa.isDividedAsYouChoose() && !sa.usesTargeting()) {
counterRemain = counterRemain - counterAmount; counterRemain = counterRemain - counterAmount;
} }
} else if (obj instanceof Player) { } else if (obj instanceof Player pl) {
// Add Counters to players! // Add Counters to players!
Player pl = (Player) obj;
pl.addCounter(counterType, counterAmount, placer, table); pl.addCounter(counterType, counterAmount, placer, table);
} }
} }

View File

@@ -248,8 +248,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
if (chosenAmount > 0) { if (chosenAmount > 0) {
removed += chosenAmount; removed += chosenAmount;
entity.subtractCounter(chosenType, chosenAmount, activator); entity.subtractCounter(chosenType, chosenAmount, activator);
if (entity instanceof Card) { if (entity instanceof Card gameCard) {
Card gameCard = (Card) entity;
game.updateLastStateForCard(gameCard); game.updateLastStateForCard(gameCard);
} }

View File

@@ -252,16 +252,14 @@ public class DamageDealEffect extends DamageBaseEffect {
continue; continue;
} }
} }
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
final Card gc = game.getCardState(c, null); final Card gc = game.getCardState(c, null);
if (gc == null || !c.equalsWithGameTimestamp(gc) || !gc.isInPlay() || gc.isPhasedOut()) { if (gc == null || !c.equalsWithGameTimestamp(gc) || !gc.isInPlay() || gc.isPhasedOut()) {
// timestamp different or not in play // timestamp different or not in play
continue; continue;
} }
internalDamageDeal(sa, sourceLKI, gc, dmg, damageMap); internalDamageDeal(sa, sourceLKI, gc, dmg, damageMap);
} else if (o instanceof Player) { } else if (o instanceof Player p) {
final Player p = (Player) o;
damageMap.put(sourceLKI, p, dmg); damageMap.put(sourceLKI, p, dmg);
} }
} }

View File

@@ -95,8 +95,7 @@ public class DamageEachEffect extends DamageBaseEffect {
} }
} else for (GameEntity ge : getTargetEntities(sa)) { } else for (GameEntity ge : getTargetEntities(sa)) {
// check before checking sources // check before checking sources
if (ge instanceof Card) { if (ge instanceof Card c) {
final Card c = (Card) ge;
if (!c.isInPlay() || c.isPhasedOut()) { if (!c.isInPlay() || c.isPhasedOut()) {
continue; continue;
} }

View File

@@ -47,8 +47,7 @@ public class DamagePreventEffect extends SpellAbilityEffect {
} }
final Object o = tgts.get(i); final Object o = tgts.get(i);
if (o instanceof Card) { if (o instanceof Card tgtC) {
final Card tgtC = (Card) o;
if (tgtC.isFaceDown()) { if (tgtC.isFaceDown()) {
sb.append("Morph"); sb.append("Morph");
} else { } else {
@@ -104,8 +103,7 @@ public class DamagePreventEffect extends SpellAbilityEffect {
for (final GameEntity o : tgts) { for (final GameEntity o : tgts) {
numDam = sa.usesTargeting() && sa.isDividedAsYouChoose() ? sa.getDividedValue(o) : numDam; numDam = sa.usesTargeting() && sa.isDividedAsYouChoose() ? sa.getDividedValue(o) : numDam;
if (o instanceof Card) { if (o instanceof Card c) {
final Card c = (Card) o;
if (c.isInPlay()) { if (c.isInPlay()) {
addPreventNextDamage(sa, o, numDam); addPreventNextDamage(sa, o, numDam);
} }

View File

@@ -127,7 +127,8 @@ public class DigEffect extends SpellAbilityEffect {
final boolean skipReorder = sa.hasParam("SkipReorder"); final boolean skipReorder = sa.hasParam("SkipReorder");
// A hack for cards like Explorer's Scope that need to ensure that a card is revealed to the player activating the ability // A hack for cards like Explorer's Scope that need to ensure that a card is revealed to the player activating the ability
final boolean forceRevealToController = sa.hasParam("ForceRevealToController"); final boolean forceReveal = sa.hasParam("ForceRevealToController") ||
sa.hasParam("ForceReveal");
// These parameters are used to indicate that a dialog box must be show to the player asking if the player wants to proceed // These parameters are used to indicate that a dialog box must be show to the player asking if the player wants to proceed
// with an optional ability, otherwise the optional ability is skipped. // with an optional ability, otherwise the optional ability is skipped.
@@ -236,9 +237,12 @@ public class DigEffect extends SpellAbilityEffect {
valid = top; valid = top;
} }
if (forceRevealToController) { if (forceReveal) {
// Force revealing the card to the player activating the ability (e.g. Explorer's Scope) // Force revealing the card to defined (e.g. Gonti, Night Minister) or the player activating the
game.getAction().revealTo(top, activator); // ability (e.g. Explorer's Scope)
Player revealTo = sa.hasParam("ForceReveal") ?
getDefinedPlayersOrTargeted(sa, "ForceReveal").get(0) : activator;
game.getAction().revealTo(top, revealTo);
delayedReveal = null; // top is already seen by the player, do not reveal twice delayedReveal = null; // top is already seen by the player, do not reveal twice
} }

View File

@@ -161,6 +161,9 @@ public class EffectEffect extends SpellAbilityEffect {
for (Player controller : effectOwner) { for (Player controller : effectOwner) {
final Card eff = createEffect(sa, controller, name, image); final Card eff = createEffect(sa, controller, name, image);
if (sa.hasParam("Boon")) {
eff.setBoon(true);
}
// Abilities and triggers work the same as they do for Token // Abilities and triggers work the same as they do for Token
// Grant abilities // Grant abilities

View File

@@ -32,6 +32,7 @@ public class ManaEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard(); final Card card = sa.getHostCard();
final Game game = card.getGame();
final AbilityManaPart abMana = sa.getManaPart(); final AbilityManaPart abMana = sa.getManaPart();
final List<Player> tgtPlayers = getDefinedPlayersOrTargeted(sa); final List<Player> tgtPlayers = getDefinedPlayersOrTargeted(sa);
final Player activator = sa.getActivatingPlayer(); final Player activator = sa.getActivatingPlayer();
@@ -39,10 +40,7 @@ public class ManaEffect extends SpellAbilityEffect {
// Spells are not undoable // Spells are not undoable
sa.setUndoable(sa.isAbility() && sa.isUndoable() && tgtPlayers.size() < 2 && !sa.hasParam("ActivationLimit")); sa.setUndoable(sa.isAbility() && sa.isUndoable() && tgtPlayers.size() < 2 && !sa.hasParam("ActivationLimit"));
final boolean optional = sa.hasParam("Optional"); if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"), null)) {
final Game game = activator.getGame();
if (optional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"), null)) {
return; return;
} }

View File

@@ -17,24 +17,25 @@ public class ManifestDreadEffect extends ManifestEffect {
final Game game = p.getGame(); final Game game = p.getGame();
for (int i = 0; i < amount; i++) { for (int i = 0; i < amount; i++) {
CardCollection tgtCards = p.getTopXCardsFromLibrary(2); CardCollection tgtCards = p.getTopXCardsFromLibrary(2);
Card manifest = null; CardCollection toGrave = new CardCollection();
Card toGrave = null;
if (!tgtCards.isEmpty()) { if (!tgtCards.isEmpty()) {
manifest = p.getController().chooseSingleEntityForEffect(tgtCards, sa, getDefaultMessage(), null); Card manifest = p.getController().chooseSingleEntityForEffect(tgtCards, sa, getDefaultMessage(), null);
tgtCards.remove(manifest); tgtCards.remove(manifest);
toGrave = tgtCards.isEmpty() ? null : tgtCards.getFirst();
// CR 701.34d If an effect instructs a player to manifest multiple cards from their library, those cards are manifested one at a time.
Map<AbilityKey, Object> moveParams = AbilityKey.newMap(); Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa); CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa);
internalEffect(manifest, p, sa, moveParams); manifest = internalEffect(manifest, p, sa, moveParams);
if (toGrave != null) { // CR 701.60a
toGrave = game.getAction().moveToGraveyard(toGrave, sa, moveParams); if (!manifest.isManifested()) {
tgtCards.add(manifest);
}
for (Card c : tgtCards) {
toGrave.add(game.getAction().moveToGraveyard(c, sa, moveParams));
} }
triggerList.triggerChangesZoneAll(game, sa); triggerList.triggerChangesZoneAll(game, sa);
} }
Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p); Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
runParams.put(AbilityKey.Card, toGrave); runParams.put(AbilityKey.Cards, toGrave);
game.getTriggerHandler().runTrigger(TriggerType.ManifestDread, runParams, true); game.getTriggerHandler().runTrigger(TriggerType.ManifestDread, runParams, true);
} }
} }

View File

@@ -71,7 +71,7 @@ public class PumpAllEffect extends SpellAbilityEffect {
tgtC.addChangedCardKeywords(kws, null, false, timestamp, null); tgtC.addChangedCardKeywords(kws, null, false, timestamp, null);
} }
if (redrawPT) { if (redrawPT) {
tgtC.updatePowerToughnessForView(); tgtC.updatePTforView();
} }
if (!hiddenkws.isEmpty()) { if (!hiddenkws.isEmpty()) {
@@ -93,7 +93,7 @@ public class PumpAllEffect extends SpellAbilityEffect {
tgtC.removeChangedCardKeywords(timestamp, 0); tgtC.removeChangedCardKeywords(timestamp, 0);
tgtC.removeHiddenExtrinsicKeywords(timestamp, 0); tgtC.removeHiddenExtrinsicKeywords(timestamp, 0);
tgtC.updatePowerToughnessForView(); tgtC.updatePTforView();
game.fireEvent(new GameEventCardStatsChanged(tgtC)); game.fireEvent(new GameEventCardStatsChanged(tgtC));
} }

View File

@@ -84,7 +84,7 @@ public class PumpEffect extends SpellAbilityEffect {
gameCard.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws); gameCard.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws);
} }
if (redrawPT) { if (redrawPT) {
gameCard.updatePowerToughnessForView(); gameCard.updatePTforView();
} }
if (sa.hasParam("CanBlockAny")) { if (sa.hasParam("CanBlockAny")) {
@@ -120,7 +120,7 @@ public class PumpEffect extends SpellAbilityEffect {
gameCard.removeHiddenExtrinsicKeywords(timestamp, 0); gameCard.removeHiddenExtrinsicKeywords(timestamp, 0);
gameCard.removeChangedCardKeywords(timestamp, 0); gameCard.removeChangedCardKeywords(timestamp, 0);
} }
gameCard.updatePowerToughnessForView(); gameCard.updatePTforView();
if (updateText) { if (updateText) {
gameCard.updateAbilityTextForView(); gameCard.updateAbilityTextForView();
} }

View File

@@ -32,8 +32,7 @@ public class UnattachAllEffect extends SpellAbilityEffect {
// If Cast Targets will be checked on the Stack // If Cast Targets will be checked on the Stack
for (GameEntity ge : getTargetEntities(sa)) { for (GameEntity ge : getTargetEntities(sa)) {
if (ge instanceof Card) { if (ge instanceof Card gc) {
Card gc = (Card) ge;
// check if the object is still in game or if it was moved // check if the object is still in game or if it was moved
Card gameCard = game.getCardState(gc, null); Card gameCard = game.getCardState(gc, null);
// gameCard is LKI in that case, the card is not in game anymore // gameCard is LKI in that case, the card is not in game anymore

File diff suppressed because it is too large Load Diff

View File

@@ -214,8 +214,8 @@ public class CardCopyService {
if (cachedCard != null) { if (cachedCard != null) {
return cachedCard; return cachedCard;
} }
String msg = "CardUtil:getLKICopy copy object";
String msg = "CardUtil:getLKICopy copy object";
Breadcrumb bread = new Breadcrumb(msg); Breadcrumb bread = new Breadcrumb(msg);
bread.setData("Card", copyFrom.getName()); bread.setData("Card", copyFrom.getName());
bread.setData("CardState", copyFrom.getCurrentStateName().toString()); bread.setData("CardState", copyFrom.getCurrentStateName().toString());
@@ -304,19 +304,19 @@ public class CardCopyService {
newCopy.setCounters(Maps.newHashMap(copyFrom.getCounters())); newCopy.setCounters(Maps.newHashMap(copyFrom.getCounters()));
newCopy.setColor(copyFrom.getColor().getColor());
newCopy.setPhasedOut(copyFrom.getPhasedOut());
newCopy.setTapped(copyFrom.isTapped());
newCopy.setTributed(copyFrom.isTributed()); newCopy.setTributed(copyFrom.isTributed());
newCopy.setMonstrous(copyFrom.isMonstrous()); newCopy.setMonstrous(copyFrom.isMonstrous());
newCopy.setRenowned(copyFrom.isRenowned()); newCopy.setRenowned(copyFrom.isRenowned());
newCopy.setSolved(copyFrom.isSolved()); newCopy.setSolved(copyFrom.isSolved());
newCopy.setSaddled(copyFrom.isSaddled());
newCopy.setPromisedGift(copyFrom.getPromisedGift()); newCopy.setPromisedGift(copyFrom.getPromisedGift());
newCopy.setSaddled(copyFrom.isSaddled());
if (newCopy.isSaddled()) newCopy.setSaddledByThisTurn(copyFrom.getSaddledByThisTurn()); if (newCopy.isSaddled()) newCopy.setSaddledByThisTurn(copyFrom.getSaddledByThisTurn());
newCopy.setSuspectedTimestamp(copyFrom.getSuspectedTimestamp()); if (copyFrom.isSuspected()) {
newCopy.setSuspectedEffect(getLKICopy(copyFrom.getSuspectedEffect(), cachedMap));
newCopy.setColor(copyFrom.getColor().getColor()); }
newCopy.setPhasedOut(copyFrom.getPhasedOut());
newCopy.setTapped(copyFrom.isTapped());
newCopy.setDamageHistory(copyFrom.getDamageHistory()); newCopy.setDamageHistory(copyFrom.getDamageHistory());
newCopy.setDamageReceivedThisTurn(copyFrom.getDamageReceivedThisTurn()); newCopy.setDamageReceivedThisTurn(copyFrom.getDamageReceivedThisTurn());
@@ -352,8 +352,6 @@ public class CardCopyService {
} }
newCopy.setChosenEvenOdd(copyFrom.getChosenEvenOdd()); newCopy.setChosenEvenOdd(copyFrom.getChosenEvenOdd());
//newCopy.getEtbCounters().putAll(copyFrom.getEtbCounters());
newCopy.setUnearthed(copyFrom.isUnearthed()); newCopy.setUnearthed(copyFrom.isUnearthed());
newCopy.copyFrom(copyFrom); newCopy.copyFrom(copyFrom);

View File

@@ -1835,15 +1835,6 @@ public class CardFactoryUtil {
squadTrigger.setOverridingAbility(squadAbility); squadTrigger.setOverridingAbility(squadAbility);
squadTrigger.setSVar("SquadAmount", "Count$OptionalKeywordAmount"); squadTrigger.setSVar("SquadAmount", "Count$OptionalKeywordAmount");
inst.addTrigger(squadTrigger); inst.addTrigger(squadTrigger);
} else if (keyword.equals("Start your engines")) {
final String trig = "Mode$ Always | TriggerZones$ Battlefield | Static$ True | CheckDefinedPlayer$ " +
"You.NoSpeed | TriggerDescription$ Start your engines! (" + inst.getReminderText() + ")";
final String effect = "DB$ ChangeSpeed";
final Trigger trigger = TriggerHandler.parseTrigger(trig, card, intrinsic);
trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
inst.addTrigger(trigger);
} else if (keyword.equals("Storm")) { } else if (keyword.equals("Storm")) {
final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | Secondary$ True" final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | Secondary$ True"
+ "| TriggerDescription$ Storm (" + inst.getReminderText() + ")"; + "| TriggerDescription$ Storm (" + inst.getReminderText() + ")";
@@ -3773,6 +3764,8 @@ public class CardFactoryUtil {
desc = "Basic land"; desc = "Basic land";
} else if (type.equals("Land.Artifact")) { } else if (type.equals("Land.Artifact")) {
desc = "Artifact land"; desc = "Artifact land";
} else if (type.startsWith("Card.with")) {
desc = type.substring(9);
} }
sb.append(" Discard<1/CARDNAME> | ActivationZone$ Hand | PrecostDesc$ ").append(desc).append("cycling "); sb.append(" Discard<1/CARDNAME> | ActivationZone$ Hand | PrecostDesc$ ").append(desc).append("cycling ");
@@ -3793,17 +3786,20 @@ public class CardFactoryUtil {
if (keyword.startsWith("Affinity")) { if (keyword.startsWith("Affinity")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String t = k[1]; final String t = k[1];
String d = "";
if (k.length > 2) {
final StringBuilder s = new StringBuilder();
s.append(k[2]).append("s");
d = s.toString();
}
String desc = "Artifact".equals(t) ? "artifacts" : CardType.getPluralType(t); String desc;
if (!d.isEmpty()) { if(k.length > 2) {
desc = d; String typeText = k[2];
if(typeText.contains(" with "))
desc = typeText.substring(typeText.indexOf(" with ") + 6);
else
desc = typeText + "s";
} }
else if ("Artifact".equals(t))
desc = "artifacts";
else
desc = CardType.getPluralType(t);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ AffinityX | EffectZone$ All"); sb.append("Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ AffinityX | EffectZone$ All");
sb.append("| Description$ Affinity for ").append(desc); sb.append("| Description$ Affinity for ").append(desc);

View File

@@ -491,6 +491,7 @@ public class CardView extends GameEntityView {
} }
void updateCurrentRoom(Card c) { void updateCurrentRoom(Card c) {
set(TrackableProperty.CurrentRoom, c.getCurrentRoom()); set(TrackableProperty.CurrentRoom, c.getCurrentRoom());
updateMarkerText(c);
} }
public int getIntensity() { public int getIntensity() {
@@ -514,6 +515,7 @@ public class CardView extends GameEntityView {
} }
void updateClassLevel(Card c) { void updateClassLevel(Card c) {
set(TrackableProperty.ClassLevel, c.getClassLevel()); set(TrackableProperty.ClassLevel, c.getClassLevel());
updateMarkerText(c);
} }
public int getRingLevel() { public int getRingLevel() {
@@ -525,6 +527,41 @@ public class CardView extends GameEntityView {
set(TrackableProperty.RingLevel, p.getNumRingTemptedYou()); set(TrackableProperty.RingLevel, p.getNumRingTemptedYou());
} }
public String getOverlayText() {
return get(TrackableProperty.OverlayText);
}
public List<String> getMarkerText() {
return get(TrackableProperty.MarkerText);
}
void updateMarkerText(Card c) {
List<String> markerItems = new ArrayList<>();
if(c.getCurrentRoom() != null && !c.getCurrentRoom().isEmpty()) {
markerItems.add("In Room:");
markerItems.add(c.getCurrentRoom());
}
if(c.isClassCard() && c.isInZone(ZoneType.Battlefield)) {
markerItems.add("CL:" + c.getClassLevel());
}
if(getRingLevel() > 0) {
markerItems.add("RL:" + getRingLevel());
}
if(StringUtils.isNotEmpty(c.getOverlayText())) {
set(TrackableProperty.OverlayText, c.getOverlayText());
markerItems.add(c.getOverlayText());
}
else {
//Overlay text is any custom string. It gets mixed in with the other marker lines, but it also needs its
//own property so that it can display in the card detail text.
set(TrackableProperty.OverlayText, null);
}
if(markerItems.isEmpty())
set(TrackableProperty.MarkerText, null);
else
set(TrackableProperty.MarkerText, markerItems);
}
private String getRemembered() { private String getRemembered() {
return get(TrackableProperty.Remembered); return get(TrackableProperty.Remembered);
} }
@@ -974,6 +1011,7 @@ public class CardView extends GameEntityView {
updateDamage(c); updateDamage(c);
updateSpecialize(c); updateSpecialize(c);
updateRingLevel(c); updateRingLevel(c);
updateMarkerText(c);
if (c.getIntensity(false) > 0) { if (c.getIntensity(false) > 0) {
updateIntensity(c); updateIntensity(c);
@@ -1603,7 +1641,9 @@ public class CardView extends GameEntityView {
} }
void updateKeywords(Card c, CardState state) { void updateKeywords(Card c, CardState state) {
c.updateKeywordsCache(state); c.updateKeywordsCache(state);
// deeper check for Idris
set(TrackableProperty.HasAnnihilator, c.hasKeyword(Keyword.ANNIHILATOR, state) || state.getTriggers().anyMatch(t -> t.isKeyword(Keyword.ANNIHILATOR))); set(TrackableProperty.HasAnnihilator, c.hasKeyword(Keyword.ANNIHILATOR, state) || state.getTriggers().anyMatch(t -> t.isKeyword(Keyword.ANNIHILATOR)));
set(TrackableProperty.HasWard, c.hasKeyword(Keyword.WARD, state) || state.getTriggers().anyMatch(t -> t.isKeyword(Keyword.WARD)));
set(TrackableProperty.HasDeathtouch, c.hasKeyword(Keyword.DEATHTOUCH, state)); set(TrackableProperty.HasDeathtouch, c.hasKeyword(Keyword.DEATHTOUCH, state));
set(TrackableProperty.HasToxic, c.hasKeyword(Keyword.TOXIC, state)); set(TrackableProperty.HasToxic, c.hasKeyword(Keyword.TOXIC, state));
set(TrackableProperty.HasDevoid, c.hasKeyword(Keyword.DEVOID, state)); set(TrackableProperty.HasDevoid, c.hasKeyword(Keyword.DEVOID, state));
@@ -1617,7 +1657,6 @@ public class CardView extends GameEntityView {
set(TrackableProperty.HasFear, c.hasKeyword(Keyword.FEAR, state)); set(TrackableProperty.HasFear, c.hasKeyword(Keyword.FEAR, state));
set(TrackableProperty.HasHexproof, c.hasKeyword(Keyword.HEXPROOF, state)); set(TrackableProperty.HasHexproof, c.hasKeyword(Keyword.HEXPROOF, state));
set(TrackableProperty.HasHorsemanship, c.hasKeyword(Keyword.HORSEMANSHIP, state)); set(TrackableProperty.HasHorsemanship, c.hasKeyword(Keyword.HORSEMANSHIP, state));
set(TrackableProperty.HasWard, c.hasKeyword(Keyword.WARD, state) || state.getTriggers().anyMatch(t -> t.isKeyword(Keyword.WARD)));
set(TrackableProperty.HasWither, c.hasKeyword(Keyword.WITHER, state)); set(TrackableProperty.HasWither, c.hasKeyword(Keyword.WITHER, state));
set(TrackableProperty.HasIndestructible, c.hasKeyword(Keyword.INDESTRUCTIBLE, state)); set(TrackableProperty.HasIndestructible, c.hasKeyword(Keyword.INDESTRUCTIBLE, state));
set(TrackableProperty.HasIntimidate, c.hasKeyword(Keyword.INTIMIDATE, state)); set(TrackableProperty.HasIntimidate, c.hasKeyword(Keyword.INTIMIDATE, state));

View File

@@ -95,8 +95,6 @@ public enum CounterEnumType {
CORRUPTION("CRPTN", 210, 121, 210), CORRUPTION("CRPTN", 210, 121, 210),
CRANK("CRANK!", 181, 148, 15),
CROAK("CROAK", 155, 255, 5), CROAK("CROAK", 155, 255, 5),
CREDIT("CRDIT", 188, 197, 234), CREDIT("CRDIT", 188, 197, 234),

View File

@@ -648,8 +648,7 @@ public class Combat {
missingCombatants.add(c); missingCombatants.add(c);
} }
} }
if (ee.getKey() instanceof Card) { if (ee.getKey() instanceof Card c) {
Card c = (Card) ee.getKey();
if (!c.isBattle() && !c.isPlaneswalker()) { if (!c.isBattle() && !c.isPlaneswalker()) {
missingCombatants.add(c); missingCombatants.add(c);
} }

View File

@@ -374,7 +374,6 @@ public class CombatUtil {
final GameEntity defender = combat.getDefenderByAttacker(c); final GameEntity defender = combat.getDefenderByAttacker(c);
final List<Card> otherAttackers = combat.getAttackers(); final List<Card> otherAttackers = combat.getAttackers();
// Run triggers
if (triggers) { if (triggers) {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Attacker, c); runParams.put(AbilityKey.Attacker, c);

View File

@@ -547,18 +547,6 @@ public class CostAdjustment {
if (!sa.isActivatedAbility() || sa.isReplacementAbility()) { if (!sa.isActivatedAbility() || sa.isReplacementAbility()) {
return false; return false;
} }
if (st.hasParam("OnlyFirstActivation")) {
int times = 0;
for (IndividualCostPaymentInstance i : game.costPaymentStack) {
SpellAbility paymentSa = i.getPayment().getAbility();
if (paymentSa.isActivatedAbility() && st.matchesValidParam("ValidCard", paymentSa.getHostCard())) {
times++;
if (times > 1) {
return false;
}
}
}
}
} }
case "Foretell" -> { case "Foretell" -> {
if (!sa.isForetelling()) { if (!sa.isForetelling()) {

View File

@@ -0,0 +1,21 @@
package forge.game.event;
import forge.game.player.Player;
public class GameEventSpeedChanged extends GameEvent {
public final Player player;
public final int oldValue;
public final int newValue;
public GameEventSpeedChanged(Player affected, int oldValue, int newValue) {
player = affected;
this.oldValue = oldValue;
this.newValue = newValue;
}
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
}

View File

@@ -1,9 +0,0 @@
package forge.game.event;
public class GameEventSpeedUp extends GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
}

View File

@@ -44,7 +44,7 @@ public interface IGameEventVisitor<T> {
T visit(GameEventRollDie event); T visit(GameEventRollDie event);
T visit(GameEventScry event); T visit(GameEventScry event);
T visit(GameEventShuffle event); T visit(GameEventShuffle event);
T visit(GameEventSpeedUp event); T visit(GameEventSpeedChanged event);
T visit(GameEventSpellAbilityCast event); T visit(GameEventSpellAbilityCast event);
T visit(GameEventSpellResolved event); T visit(GameEventSpellResolved event);
T visit(GameEventSpellRemovedFromStack event); T visit(GameEventSpellRemovedFromStack event);
@@ -101,7 +101,7 @@ public interface IGameEventVisitor<T> {
public T visit(GameEventRollDie event) { return null; } public T visit(GameEventRollDie event) { return null; }
public T visit(GameEventScry event) { return null; } public T visit(GameEventScry event) { return null; }
public T visit(GameEventShuffle event) { return null; } public T visit(GameEventShuffle event) { return null; }
public T visit(GameEventSpeedUp event) { return null; } public T visit(GameEventSpeedChanged event) { return null; }
public T visit(GameEventSpellResolved event) { return null; } public T visit(GameEventSpellResolved event) { return null; }
public T visit(GameEventSpellAbilityCast event) { return null; } public T visit(GameEventSpellAbilityCast event) { return null; }
public T visit(GameEventSpellRemovedFromStack event) { return null; } public T visit(GameEventSpellRemovedFromStack event) { return null; }

View File

@@ -26,6 +26,8 @@ public class KeywordWithCostAndType extends KeywordInstance<KeywordWithCostAndTy
n[i] = "basic land"; n[i] = "basic land";
} else if (n[i].equals("Land.Artifact")) { } else if (n[i].equals("Land.Artifact")) {
n[i] = "artifact land"; n[i] = "artifact land";
} else if (n[i].startsWith("Card.with")) {
n[i] = n[i].substring(9);
} }
} }

View File

@@ -36,6 +36,7 @@ import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityNoCleanupDamage;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.Zone; import forge.game.zone.Zone;
@@ -180,8 +181,6 @@ public class PhaseHandler implements java.io.Serializable {
} }
playerTurn.incrementTurn(); playerTurn.incrementTurn();
game.getAction().resetActivationsPerTurn();
final int lands = CardLists.count(playerTurn.getLandsInPlay(), CardPredicates.UNTAPPED); final int lands = CardLists.count(playerTurn.getLandsInPlay(), CardPredicates.UNTAPPED);
playerTurn.setNumPowerSurgeLands(lands); playerTurn.setNumPowerSurgeLands(lands);
} }
@@ -395,7 +394,10 @@ public class PhaseHandler implements java.io.Serializable {
// Rule 514.2 // Rule 514.2
// Reset Damage received map // Reset Damage received map
for (final Card c : game.getCardsIncludePhasingIn(ZoneType.Battlefield)) { for (final Card c : game.getCardsIncludePhasingIn(ZoneType.Battlefield)) {
c.onCleanupPhase(playerTurn); if (!StaticAbilityNoCleanupDamage.damageNotRemoved(c)) {
c.setDamage(0);
}
c.setHasBeenDealtDeathtouchDamage(false);
} }
game.getEndOfTurn().executeUntil(); game.getEndOfTurn().executeUntil();

View File

@@ -112,6 +112,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private int expentThisTurn; private int expentThisTurn;
private int numLibrarySearchedOwn; //The number of times this player has searched his library private int numLibrarySearchedOwn; //The number of times this player has searched his library
private int venturedThisTurn; private int venturedThisTurn;
private int attractionsVisitedThisTurn;
private int descended; private int descended;
private int numRingTemptedYou; private int numRingTemptedYou;
private int devotionMod; private int devotionMod;
@@ -189,7 +190,6 @@ public class Player extends GameEntity implements Comparable<Player> {
private final Map<Card, Integer> commanderCast = Maps.newHashMap(); private final Map<Card, Integer> commanderCast = Maps.newHashMap();
private final Map<Card, Integer> commanderDamage = Maps.newHashMap(); private final Map<Card, Integer> commanderDamage = Maps.newHashMap();
private DetachedCardEffect commanderEffect = null; private DetachedCardEffect commanderEffect = null;
private DetachedCardEffect speedEffect;
private Card monarchEffect; private Card monarchEffect;
private Card initiativeEffect; private Card initiativeEffect;
@@ -197,6 +197,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private Card contraptionSprocketEffect; private Card contraptionSprocketEffect;
private Card radiationEffect; private Card radiationEffect;
private Card keywordEffect; private Card keywordEffect;
private Card speedEffect;
private Map<Long, Integer> additionalVotes = Maps.newHashMap(); private Map<Long, Integer> additionalVotes = Maps.newHashMap();
private Map<Long, Integer> additionalOptionalVotes = Maps.newHashMap(); private Map<Long, Integer> additionalOptionalVotes = Maps.newHashMap();
@@ -237,10 +238,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return achievementTracker; return achievementTracker;
} }
public final PlayerOutcome getOutcome() {
return stats.getOutcome();
}
private String chooseName(String originalName) { private String chooseName(String originalName) {
String nameCandidate = originalName; String nameCandidate = originalName;
for (int i = 2; i <= 8; i++) { // several tries, not matter how many for (int i = 2; i <= 8; i++) { // several tries, not matter how many
@@ -1333,6 +1330,25 @@ public class Player extends GameEntity implements Comparable<Player> {
return drawn; return drawn;
} }
public final void resetNumDrawnThisDrawStep() {
numDrawnThisDrawStep = 0;
}
public final void resetNumDrawnThisTurn() {
numDrawnThisTurn = 0;
view.updateNumDrawnThisTurn(this);
}
public final int getNumDrawnThisTurn() {
return numDrawnThisTurn;
}
public final int getNumDrawnLastTurn() {
return numDrawnLastTurn;
}
public final int numDrawnThisDrawStep() {
return numDrawnThisDrawStep;
}
/** /**
* Returns PlayerZone corresponding to the given zone of game. * Returns PlayerZone corresponding to the given zone of game.
*/ */
@@ -1360,7 +1376,6 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
} }
public final CardCollectionView getCardsIn(final ZoneType zoneType) { public final CardCollectionView getCardsIn(final ZoneType zoneType) {
return getCardsIn(zoneType, true); return getCardsIn(zoneType, true);
} }
@@ -1448,35 +1463,12 @@ public class Player extends GameEntity implements Comparable<Player> {
return CardCollection.combine(getCardsIn(Player.ALL_ZONES), getCardsIn(ZoneType.Stack), inboundTokens); return CardCollection.combine(getCardsIn(Player.ALL_ZONES), getCardsIn(ZoneType.Stack), inboundTokens);
} }
public final void resetNumDrawnThisDrawStep() {
numDrawnThisDrawStep = 0;
}
public final void resetNumDrawnThisTurn() {
numDrawnThisTurn = 0;
view.updateNumDrawnThisTurn(this);
}
public final int getNumDrawnThisTurn() {
return numDrawnThisTurn;
}
public final int getNumDrawnLastTurn() {
return numDrawnLastTurn;
}
public final int numDrawnThisDrawStep() {
return numDrawnThisDrawStep;
}
public final void resetNumRollsThisTurn() { public final void resetNumRollsThisTurn() {
numRollsThisTurn = 0; numRollsThisTurn = 0;
} }
public final int getNumRollsThisTurn() { public final int getNumRollsThisTurn() {
return numRollsThisTurn; return numRollsThisTurn;
} }
public void roll() { public void roll() {
numRollsThisTurn++; numRollsThisTurn++;
} }
@@ -1717,15 +1709,16 @@ public class Player extends GameEntity implements Comparable<Player> {
final Card c = game.getAction().moveTo(getZone(ZoneType.Battlefield), land, cause); final Card c = game.getAction().moveTo(getZone(ZoneType.Battlefield), land, cause);
game.updateLastStateForCard(c); game.updateLastStateForCard(c);
// play a sound
game.fireEvent(new GameEventLandPlayed(this, land));
// Run triggers // Run triggers
runParams.put(AbilityKey.SpellAbility, cause); runParams.put(AbilityKey.SpellAbility, cause);
game.getTriggerHandler().runTrigger(TriggerType.LandPlayed, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.LandPlayed, runParams, false);
game.getStack().unfreezeStack(); game.getStack().unfreezeStack();
addLandPlayedThisTurn(); addLandPlayedThisTurn();
// play a sound
game.fireEvent(new GameEventLandPlayed(this, land));
return c; return c;
} }
@@ -1878,6 +1871,10 @@ public class Player extends GameEntity implements Comparable<Player> {
stats.nextTurn(); stats.nextTurn();
} }
public final int getLastTurnNr() {
return this.lastTurnNr;
}
public boolean hasTappedLandForManaThisTurn() { public boolean hasTappedLandForManaThisTurn() {
return tappedLandForManaThisTurn; return tappedLandForManaThisTurn;
} }
@@ -1949,35 +1946,23 @@ public class Player extends GameEntity implements Comparable<Player> {
completedDungeons.clear(); completedDungeons.clear();
} }
public final int getNumRingTemptedYou() {
return numRingTemptedYou;
}
public final void incrementRingTemptedYou() {
numRingTemptedYou++;
}
public final void setNumRingTemptedYou(int value) {
numRingTemptedYou = value;
}
public final void resetRingTemptedYou() {
numRingTemptedYou = 0;
}
public final int getSpeed() { public final int getSpeed() {
return speed; return speed;
} }
public final void increaseSpeed() { public final void increaseSpeed() {
if (speedEffect == null) createSpeedEffect();
if (!maxSpeed()) { // can't increase past 4 if (!maxSpeed()) { // can't increase past 4
int old = speed;
speed++; speed++;
view.updateSpeed(this); getGame().fireEvent(new GameEventSpeedChanged(this, old, speed)); //play sound effect
game.fireEvent(new GameEventSpeedUp()); //play sound effect updateSpeedEffect();
} }
} }
public final void decreaseSpeed() { public final void decreaseSpeed() {
if (speed > 1) { // can't decrease speed below 1 if (speed > 1) { // can't decrease speed below 1
int old = speed;
speed--; speed--;
view.updateSpeed(this); game.fireEvent(new GameEventSpeedChanged(this, old, speed));
getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); updateSpeedEffect();
} }
} }
public final boolean noSpeed() { public final boolean noSpeed() {
@@ -1988,24 +1973,62 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
public final void setSpeed(int i) { //just used for copy/save public final void setSpeed(int i) { //just used for copy/save
speed = i; speed = i;
if (speed > 0) view.updateSpeed(this); if(this.speedEffect != null)
updateSpeedEffect();
} }
public final void createSpeedEffect() { public final void createSpeedEffect() {
final PlayerZone com = getZone(ZoneType.Command); if(this.speedEffect != null || this.noSpeed())
DetachedCardEffect eff = new DetachedCardEffect(this, "Speed Effect"); return;
String trigger = "Mode$ LifeLost | ValidPlayer$ Opponent | TriggerZones$ Command | ActivationLimit$ 1 | " +
"PlayerTurn$ True | TriggerDescription$ Your speed increases once on each of your turns when an " +
"opponent loses life.";
String speedUp = "DB$ ChangeSpeed";
Trigger lifeLostTrigger = TriggerHandler.parseTrigger(trigger, eff, true);
lifeLostTrigger.setOverridingAbility(AbilityFactory.getAbility(speedUp, eff));
eff.addTrigger(lifeLostTrigger);
this.speedEffect = eff;
com.add(eff);
}
public final List<Card> getPlaneswalkedToThisTurn() { speedEffect = new Card(game.nextCardId(), null, game);
return planeswalkedToThisTurn; speedEffect.setOwner(this);
speedEffect.setGamePieceType(GamePieceType.EFFECT);
speedEffect.addAlternateState(CardStateName.Flipped, false);
CardState speedFront = speedEffect.getState(CardStateName.Original);
CardState speedBack = speedEffect.getState(CardStateName.Flipped);
speedFront.setImageKey("t:speed");
speedFront.setName("Start Your Engines!");
speedBack.setImageKey("t:max_speed");
speedBack.setName("Max Speed!");
String label = Localizer.getInstance().getMessage("lblSpeed", this.speed);
speedEffect.setOverlayText(label);
// 702.179d There is an inherent triggered ability associated with a player having 1 or more speed. This ability has no source and is controlled by that player.
// That ability is “Whenever one or more opponents lose life during your turn, if your speed is less than 4, your speed increases by 1. This ability triggers only once each turn.”
String trigger = "Mode$ LifeLostAll | ValidPlayer$ Opponent | TriggerZones$ Command | ActivationLimit$ 1 | " +
"PlayerTurn$ True | CheckSVar$ Count$YourSpeed | SVarCompare$ LT4 | "
+ "TriggerDescription$ Whenever one or more opponents lose life during your turn, if your speed is less than 4, your speed increases by 1. This ability triggers only once each turn.";
String speedUp = "DB$ ChangeSpeed";
Trigger lifeLostTrigger = TriggerHandler.parseTrigger(trigger, speedEffect, true);
lifeLostTrigger.setOverridingAbility(AbilityFactory.getAbility(speedUp, speedEffect));
speedFront.addTrigger(lifeLostTrigger);
speedEffect.updateStateForView();
if(this.maxSpeed())
speedEffect.setState(CardStateName.Flipped, true);
final PlayerZone com = getZone(ZoneType.Command);
com.add(speedEffect);
this.updateZoneForView(com);
}
protected void updateSpeedEffect() {
if(this.speedEffect == null) {
if(this.noSpeed())
return;
createSpeedEffect();
}
Localizer localizer = Localizer.getInstance();
String label = this.maxSpeed() ? localizer.getMessage("lblMaxSpeed") : localizer.getMessage("lblSpeed", this.speed);
speedEffect.setOverlayText(label);
if(maxSpeed() && speedEffect.getCurrentStateName() == CardStateName.Original)
speedEffect.setState(CardStateName.Flipped, true);
else if(!maxSpeed() && speedEffect.getCurrentStateName() == CardStateName.Flipped)
speedEffect.setState(CardStateName.Original, true);
} }
public final void altWinBySpellEffect(final String sourceName) { public final void altWinBySpellEffect(final String sourceName) {
@@ -2285,6 +2308,13 @@ public class Player extends GameEntity implements Comparable<Player> {
view.updateUnlimitedHandSize(this); view.updateUnlimitedHandSize(this);
} }
public int getStartingHandSize() {
return startingHandSize;
}
public void setStartingHandSize(int shs) {
startingHandSize = shs;
}
public final int getLandsPlayedThisTurn() { public final int getLandsPlayedThisTurn() {
return landsPlayedThisTurn; return landsPlayedThisTurn;
} }
@@ -2461,6 +2491,9 @@ public class Player extends GameEntity implements Comparable<Player> {
return game.getMatch().getPlayers().get(game.getRegisteredPlayers().indexOf(this)); return game.getMatch().getPlayers().get(game.getRegisteredPlayers().indexOf(this));
} }
public final PlayerOutcome getOutcome() {
return stats.getOutcome();
}
private void setOutcome(PlayerOutcome outcome) { private void setOutcome(PlayerOutcome outcome) {
stats.setOutcome(outcome); stats.setOutcome(outcome);
} }
@@ -2519,10 +2552,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return keywords.getAmount(k); return keywords.getAmount(k);
} }
public final int getLastTurnNr() {
return this.lastTurnNr;
}
public void onCleanupPhase() { public void onCleanupPhase() {
for (Card c : getCardsIn(ZoneType.Hand)) { for (Card c : getCardsIn(ZoneType.Hand)) {
c.setDrawnThisTurn(false); c.setDrawnThisTurn(false);
@@ -2561,6 +2590,7 @@ public class Player extends GameEntity implements Comparable<Player> {
setCommitedCrimeThisTurn(0); setCommitedCrimeThisTurn(0);
diceRollsThisTurn = Lists.newArrayList(); diceRollsThisTurn = Lists.newArrayList();
setExpentThisTurn(0); setExpentThisTurn(0);
attractionsVisitedThisTurn = 0;
damageReceivedThisTurn.clear(); damageReceivedThisTurn.clear();
planeswalkedToThisTurn.clear(); planeswalkedToThisTurn.clear();
@@ -2693,11 +2723,8 @@ public class Player extends GameEntity implements Comparable<Player> {
return !isInGame(); return !isInGame();
} }
public int getStartingHandSize() { public final List<Card> getPlaneswalkedToThisTurn() {
return startingHandSize; return planeswalkedToThisTurn;
}
public void setStartingHandSize(int shs) {
startingHandSize = shs;
} }
/** /**
@@ -3386,6 +3413,19 @@ public class Player extends GameEntity implements Comparable<Player> {
getTheRing().updateStateForView(); getTheRing().updateStateForView();
} }
public final int getNumRingTemptedYou() {
return numRingTemptedYou;
}
public final void incrementRingTemptedYou() {
numRingTemptedYou++;
}
public final void setNumRingTemptedYou(int value) {
numRingTemptedYou = value;
}
public final void resetRingTemptedYou() {
numRingTemptedYou = 0;
}
public void changeOwnership(Card card) { public void changeOwnership(Card card) {
// If lost then gained, just clear out of lost. // If lost then gained, just clear out of lost.
// If gained then lost, just clear out of gained. // If gained then lost, just clear out of gained.
@@ -3610,12 +3650,31 @@ public class Player extends GameEntity implements Comparable<Player> {
return radiationEffect != null; return radiationEffect != null;
} }
public Card getKeywordCard() {
if (keywordEffect != null) {
return keywordEffect;
}
final PlayerZone com = getZone(ZoneType.Command);
keywordEffect = new Card(game.nextCardId(), null, game);
keywordEffect.setGamePieceType(GamePieceType.EFFECT);
keywordEffect.setOwner(this);
keywordEffect.setName("Keyword Effects");
keywordEffect.setImageKey(ImageKeys.HIDDEN_CARD);
keywordEffect.updateStateForView();
com.add(keywordEffect);
this.updateZoneForView(com);
return keywordEffect;
}
public void updateKeywordCardAbilityText() { public void updateKeywordCardAbilityText() {
if (getKeywordCard() == null) if (getKeywordCard() == null)
return; return;
final PlayerZone com = getZone(ZoneType.Command); final PlayerZone com = getZone(ZoneType.Command);
keywordEffect.setText(""); keywordEffect.setText("");
keywordEffect.updateAbilityTextForView();
boolean headerAdded = false; boolean headerAdded = false;
StringBuilder kw = new StringBuilder(); StringBuilder kw = new StringBuilder();
for (KeywordInterface k : keywords) { for (KeywordInterface k : keywords) {
@@ -3627,8 +3686,8 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
if (!kw.toString().isEmpty()) { if (!kw.toString().isEmpty()) {
keywordEffect.setText(trimKeywords(kw.toString())); keywordEffect.setText(trimKeywords(kw.toString()));
keywordEffect.updateAbilityTextForView();
} }
keywordEffect.updateAbilityTextForView();
this.updateZoneForView(com); this.updateZoneForView(com);
} }
public String trimKeywords(String keywordTexts) { public String trimKeywords(String keywordTexts) {
@@ -3686,6 +3745,9 @@ public class Player extends GameEntity implements Comparable<Player> {
blessingEffect.updateStateForView(); blessingEffect.updateStateForView();
com.add(blessingEffect); com.add(blessingEffect);
// 702.131d. After a player gets the city's blessing, continuous effects are reapplied
game.getAction().checkStaticAbilities();
} else { } else {
com.remove(blessingEffect); com.remove(blessingEffect);
blessingEffect = null; blessingEffect = null;
@@ -3732,27 +3794,6 @@ public class Player extends GameEntity implements Comparable<Player> {
|| !hasKeyword("Spells and abilities you control can't cause you to search your library."); || !hasKeyword("Spells and abilities you control can't cause you to search your library.");
} }
public Card getKeywordCard() {
if (keywordEffect != null) {
return keywordEffect;
}
final PlayerZone com = getZone(ZoneType.Command);
keywordEffect = new Card(game.nextCardId(), null, game);
keywordEffect.setGamePieceType(GamePieceType.EFFECT);
keywordEffect.setOwner(this);
keywordEffect.setName("Keyword Effects");
keywordEffect.setImageKey(ImageKeys.HIDDEN_CARD);
keywordEffect.updateStateForView();
com.add(keywordEffect);
this.updateZoneForView(com);
return keywordEffect;
}
public void addAdditionalVote(long timestamp, int value) { public void addAdditionalVote(long timestamp, int value) {
additionalVotes.put(timestamp, value); additionalVotes.put(timestamp, value);
getView().updateAdditionalVote(this); getView().updateAdditionalVote(this);
@@ -3943,7 +3984,6 @@ public class Player extends GameEntity implements Comparable<Player> {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
game.getTriggerHandler().runTrigger(TriggerType.CommitCrime, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.CommitCrime, runParams, false);
} }
public int getCommittedCrimeThisTurn() { public int getCommittedCrimeThisTurn() {
@@ -3970,12 +4010,17 @@ public class Player extends GameEntity implements Comparable<Player> {
public void visitAttractions(int light) { public void visitAttractions(int light) {
CardCollection attractions = CardLists.filter(getCardsIn(ZoneType.Battlefield), CardPredicates.isAttractionWithLight(light)); CardCollection attractions = CardLists.filter(getCardsIn(ZoneType.Battlefield), CardPredicates.isAttractionWithLight(light));
for (Card c : attractions) { for (Card c : attractions) {
if(!c.wasVisitedThisTurn())
this.attractionsVisitedThisTurn++;
c.visitAttraction(this); c.visitAttraction(this);
} }
} }
public void rollToVisitAttractions() { public void rollToVisitAttractions() {
this.visitAttractions(RollDiceEffect.rollDiceForPlayerToVisitAttractions(this)); this.visitAttractions(RollDiceEffect.rollDiceForPlayerToVisitAttractions(this));
} }
public int getAttractionsVisitedThisTurn() {
return this.attractionsVisitedThisTurn;
}
public int getCrankCounter() { public int getCrankCounter() {
return this.crankCounter; return this.crankCounter;
@@ -3983,8 +4028,8 @@ public class Player extends GameEntity implements Comparable<Player> {
public void setCrankCounter(int counters) { public void setCrankCounter(int counters) {
this.crankCounter = counters; this.crankCounter = counters;
if (this.contraptionSprocketEffect != null) { if (this.contraptionSprocketEffect != null) {
Map<CounterType, Integer> counterMap = Map.of(CounterType.get(CounterEnumType.CRANK), this.crankCounter); String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter);
contraptionSprocketEffect.setCounters(counterMap); contraptionSprocketEffect.setOverlayText(label);
} }
else if (this.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.CONTRAPTIONS)) { else if (this.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.CONTRAPTIONS)) {
this.createContraptionSprockets(); this.createContraptionSprockets();
@@ -4010,12 +4055,8 @@ public class Player extends GameEntity implements Comparable<Player> {
contraptionSprocketEffect.setName("Contraption Sprockets"); contraptionSprocketEffect.setName("Contraption Sprockets");
contraptionSprocketEffect.setGamePieceType(GamePieceType.EFFECT); contraptionSprocketEffect.setGamePieceType(GamePieceType.EFFECT);
//Add "counters" on the effect to represent the current CRANK counter position. String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter);
//This and some other un-cards could benefit from a distinct system for positional counters or markers, contraptionSprocketEffect.setOverlayText(label);
//see for instance Baron von Count or B-I-N-G-O. For now this is sufficient to display the current sprocket
//on the counter overlay, and I don't think any existing effect will notice it.
Map<CounterType, Integer> counterMap = Map.of(CounterType.get(CounterEnumType.CRANK), this.crankCounter);
contraptionSprocketEffect.setCounters(counterMap);
contraptionSprocketEffect.setText("At the beginning of your upkeep, if you control a Contraption, move the CRANK! counter to the next sprocket and crank any number of that sprocket's Contraptions."); contraptionSprocketEffect.setText("At the beginning of your upkeep, if you control a Contraption, move the CRANK! counter to the next sprocket and crank any number of that sprocket's Contraptions.");
contraptionSprocketEffect.updateStateForView(); contraptionSprocketEffect.updateStateForView();
@@ -4028,11 +4069,9 @@ public class Player extends GameEntity implements Comparable<Player> {
public void addDeclaresAttackers(long ts, Player p) { public void addDeclaresAttackers(long ts, Player p) {
this.declaresAttackers.put(ts, p); this.declaresAttackers.put(ts, p);
} }
public void removeDeclaresAttackers(long ts) { public void removeDeclaresAttackers(long ts) {
this.declaresAttackers.remove(ts); this.declaresAttackers.remove(ts);
} }
public Player getDeclaresAttackers() { public Player getDeclaresAttackers() {
Map.Entry<Long, Player> e = declaresAttackers.lastEntry(); Map.Entry<Long, Player> e = declaresAttackers.lastEntry();
return e == null ? null : e.getValue(); return e == null ? null : e.getValue();
@@ -4041,11 +4080,9 @@ public class Player extends GameEntity implements Comparable<Player> {
public void addDeclaresBlockers(long ts, Player p) { public void addDeclaresBlockers(long ts, Player p) {
this.declaresBlockers.put(ts, p); this.declaresBlockers.put(ts, p);
} }
public void removeDeclaresBlockers(long ts) { public void removeDeclaresBlockers(long ts) {
this.declaresBlockers.remove(ts); this.declaresBlockers.remove(ts);
} }
public Player getDeclaresBlockers() { public Player getDeclaresBlockers() {
Map.Entry<Long, Player> e = declaresBlockers.lastEntry(); Map.Entry<Long, Player> e = declaresBlockers.lastEntry();
return e == null ? null : e.getValue(); return e == null ? null : e.getValue();

View File

@@ -329,13 +329,6 @@ public class PlayerView extends GameEntityView {
set(TrackableProperty.ControlVotes, val); set(TrackableProperty.ControlVotes, val);
} }
public int getSpeed() {
return get(TrackableProperty.Speed);
}
void updateSpeed(Player p) {
set(TrackableProperty.Speed, p.getSpeed());
}
public int getAdditionalVillainousChoices() { public int getAdditionalVillainousChoices() {
return get(TrackableProperty.AdditionalVillainousChoices); return get(TrackableProperty.AdditionalVillainousChoices);
} }
@@ -604,10 +597,6 @@ public class PlayerView extends GameEntityView {
} }
details.add(Localizer.getInstance().getMessage("lblExtraTurnCountHas", String.valueOf(getExtraTurnCount()))); details.add(Localizer.getInstance().getMessage("lblExtraTurnCountHas", String.valueOf(getExtraTurnCount())));
if (getSpeed() > 0) {
details.add(Localizer.getInstance().getMessage("lblSpeed", String.valueOf(getSpeed())));
}
final String keywords = Lang.joinHomogenous(getDisplayableKeywords()); final String keywords = Lang.joinHomogenous(getDisplayableKeywords());
if (!keywords.isEmpty()) { if (!keywords.isEmpty()) {
details.add(keywords); details.add(keywords);

View File

@@ -322,16 +322,13 @@ public class AbilityManaPart implements java.io.Serializable {
return this.triggersWhenSpent != null; return this.triggersWhenSpent != null;
} }
public void addTriggersWhenSpent(SpellAbility saBeingPaid, Card card) { public void addTriggersWhenSpent(SpellAbility saBeingPaid) {
if (this.triggersWhenSpent == null)
return;
TriggerHandler handler = card.getGame().getTriggerHandler();
Trigger trig = TriggerHandler.parseTrigger(sVarHolder.getSVar(this.triggersWhenSpent), sourceCard, false, sVarHolder); Trigger trig = TriggerHandler.parseTrigger(sVarHolder.getSVar(this.triggersWhenSpent), sourceCard, false, sVarHolder);
if (sVarHolder instanceof SpellAbility) { trig.addRemembered(saBeingPaid);
trig.setSpawningAbility((SpellAbility) sVarHolder); if (getSourceSA() != null) {
trig.setSpawningAbility(getSourceSA());
} }
handler.registerOneTrigger(trig); saBeingPaid.getHostCard().getGame().getTriggerHandler().registerThisTurnDelayedTrigger(trig);
} }
public SpellAbility getSourceSA() { public SpellAbility getSourceSA() {

View File

@@ -70,6 +70,7 @@ import forge.game.staticability.StaticAbilityCastWithFlash;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
//only SpellAbility can go on the stack //only SpellAbility can go on the stack
@@ -837,7 +838,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
for (Mana mana : getPayingMana()) { for (Mana mana : getPayingMana()) {
if (mana.triggersWhenSpent()) { if (mana.triggersWhenSpent()) {
mana.getManaAbility().addTriggersWhenSpent(this, host); mana.getManaAbility().addTriggersWhenSpent(this);
} }
if (mana.addsCounters(this)) { if (mana.addsCounters(this)) {
@@ -1072,11 +1073,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
} }
public void rebuiltDescription() { public void rebuiltDescription() {
// SubAbilities don't have Costs or Cost descriptors // SubAbilities don't have Costs or Cost descriptors
String sb = getCostDescription() + String sb = getCostDescription() + getParam("SpellDescription");
getParam("SpellDescription");
setDescription(sb); setDescription(sb);
} }
@@ -1536,8 +1535,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
} }
} }
if (entity instanceof GameEntity) { if (entity instanceof GameEntity e) {
GameEntity e = (GameEntity)entity;
if (!e.isValid(tr.getValidTgts(), getActivatingPlayer(), getHostCard(), this)) { if (!e.isValid(tr.getValidTgts(), getActivatingPlayer(), getHostCard(), this)) {
return false; return false;
} }
@@ -1546,8 +1544,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
} }
} }
if (entity instanceof Card) { if (entity instanceof Card c) {
final Card c = (Card) entity;
if (c.getZone() != null && !tr.getZone().contains(c.getZone().getZoneType())) { if (c.getZone() != null && !tr.getZone().contains(c.getZone().getZoneType())) {
return false; return false;
} }
@@ -1991,7 +1988,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return targets; return targets;
} }
public boolean canTargetSpellAbility(final SpellAbility topSA) { public boolean canTargetSpellAbility(SpellAbility topSA) {
if (topSA.isWrapper()) {
topSA = ((WrappedAbility) topSA).getWrappedAbility();
}
final TargetRestrictions tgt = getTargetRestrictions(); final TargetRestrictions tgt = getTargetRestrictions();
if (this.equals(topSA)) { if (this.equals(topSA)) {

View File

@@ -271,7 +271,6 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
if (this.isRevolt() && !activator.hasRevolt()) return false; if (this.isRevolt() && !activator.hasRevolt()) return false;
if (this.isDesert() && !activator.hasDesert()) return false; if (this.isDesert() && !activator.hasDesert()) return false;
if (this.isBlessing() && !activator.hasBlessing()) return false; if (this.isBlessing() && !activator.hasBlessing()) return false;
if (this.isMaxSpeed() && !activator.maxSpeed()) return false;
if (this.kicked && !sa.isKicked()) return false; if (this.kicked && !sa.isKicked()) return false;
if (this.kicked1 && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) return false; if (this.kicked1 && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) return false;

View File

@@ -98,9 +98,6 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
if (value.equals("Solved")) { if (value.equals("Solved")) {
this.setSolved(true); this.setSolved(true);
} }
if (value.equals("MaxSpeed")) {
this.setMaxSpeed(true);
}
} }
if (params.containsKey("ActivationZone")) { if (params.containsKey("ActivationZone")) {
@@ -443,9 +440,6 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
return false; return false;
} }
} }
if (isMaxSpeed()) {
if (!activator.maxSpeed()) return false;
}
if (isBlessing()) { if (isBlessing()) {
if (!activator.hasBlessing()) { if (!activator.hasBlessing()) {
return false; return false;

View File

@@ -96,7 +96,6 @@ public class SpellAbilityVariables implements Cloneable {
private boolean desert = false; private boolean desert = false;
private boolean blessing = false; private boolean blessing = false;
private boolean solved = false; private boolean solved = false;
private boolean maxSpeed = false;
/** The s is present. */ /** The s is present. */
private String isPresent = null; private String isPresent = null;
@@ -347,7 +346,6 @@ public class SpellAbilityVariables implements Cloneable {
public void setDesert(final boolean bDesert) { desert = bDesert; } public void setDesert(final boolean bDesert) { desert = bDesert; }
public void setBlessing(final boolean bBlessing) { blessing = bBlessing; } public void setBlessing(final boolean bBlessing) { blessing = bBlessing; }
public void setSolved(final boolean bSolved) { solved = bSolved; } public void setSolved(final boolean bSolved) { solved = bSolved; }
public void setMaxSpeed(final boolean b) { maxSpeed = b; }
/** Optional Costs */ /** Optional Costs */
protected boolean kicked = false; protected boolean kicked = false;
@@ -540,8 +538,6 @@ public class SpellAbilityVariables implements Cloneable {
public final boolean isSolved() { return this.solved; } public final boolean isSolved() { return this.solved; }
public final boolean isMaxSpeed() { return this.maxSpeed; }
public String getNoDifferentColors() { public String getNoDifferentColors() {
return noDifferentColors; return noDifferentColors;
} }

View File

@@ -72,8 +72,7 @@ public class TargetChoices extends ForwardingList<GameObject> implements Cloneab
public final boolean add(final GameObject o) { public final boolean add(final GameObject o) {
if (o instanceof Player || o instanceof Card || o instanceof SpellAbility) { if (o instanceof Player || o instanceof Card || o instanceof SpellAbility) {
if (o instanceof Card) { if (o instanceof Card c) {
Card c = (Card) o;
cardControllers.put(c, c.getController()); cardControllers.put(c, c.getController());
} }
return super.add(o); return super.add(o);

View File

@@ -344,10 +344,8 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
if (zone == null || !this.validHostZones.contains(zone.getZoneType())) { if (zone == null || !this.validHostZones.contains(zone.getZoneType())) {
return false; return false;
} }
} else { } else if (!getHostCard().isInPlay()) { // default
if (!getHostCard().isInPlay()) { // default return false;
return false;
}
} }
} }

View File

@@ -452,29 +452,11 @@ public final class StaticAbilityContinuous {
if (layer == StaticAbilityLayer.COLOR) { if (layer == StaticAbilityLayer.COLOR) {
if (params.containsKey("AddColor")) { if (params.containsKey("AddColor")) {
final String colors = params.get("AddColor"); addColors = getColorsFromParam(stAb, params.get("AddColor"));
if (colors.equals("ChosenColor")) {
if (hostCard.hasChosenColor()) {
addColors = ColorSet.fromNames(hostCard.getChosenColors());
}
} else if (colors.equals("All")) {
addColors = ColorSet.ALL_COLORS;
} else {
addColors = ColorSet.fromNames(colors.split(" & "));
}
} }
if (params.containsKey("SetColor")) { if (params.containsKey("SetColor")) {
final String colors = params.get("SetColor"); addColors = getColorsFromParam(stAb, params.get("SetColor"));
if (colors.equals("ChosenColor")) {
if (hostCard.hasChosenColor()) {
addColors = ColorSet.fromNames(hostCard.getChosenColors());
}
} else if (colors.equals("All")) {
addColors = ColorSet.ALL_COLORS;
} else {
addColors = ColorSet.fromNames(colors.split(" & "));
}
overwriteColors = true; overwriteColors = true;
} }
} }
@@ -704,7 +686,7 @@ public final class StaticAbilityContinuous {
setToughness = AbilityUtils.calculateAmount(affectedCard, setT, stAb, true); setToughness = AbilityUtils.calculateAmount(affectedCard, setT, stAb, true);
} }
affectedCard.addNewPT(setPower, setToughness, affectedCard.addNewPT(setPower, setToughness,
se.getTimestamp(), stAb.getId(), layer == StaticAbilityLayer.CHARACTERISTIC); se.getTimestamp(), stAb.getId(), layer == StaticAbilityLayer.CHARACTERISTIC, false);
} }
} }
@@ -752,7 +734,8 @@ public final class StaticAbilityContinuous {
} }
affectedCard.addChangedCardKeywords(newKeywords, removeKeywords, affectedCard.addChangedCardKeywords(newKeywords, removeKeywords,
removeAllAbilities, se.getTimestamp(), stAb, true); removeAllAbilities, se.getTimestamp(), stAb, false);
affectedCard.updateKeywordsCache(affectedCard.getCurrentState());
} }
// add HIDDEN keywords // add HIDDEN keywords
@@ -862,7 +845,7 @@ public final class StaticAbilityContinuous {
|| removeAllAbilities) { || removeAllAbilities) {
affectedCard.addChangedCardTraits( affectedCard.addChangedCardTraits(
addedAbilities, null, addedTrigger, addedReplacementEffects, addedStaticAbility, removeAllAbilities, removeNonMana, addedAbilities, null, addedTrigger, addedReplacementEffects, addedStaticAbility, removeAllAbilities, removeNonMana,
se.getTimestamp(), stAb.getId() se.getTimestamp(), stAb.getId(), false
); );
} }
@@ -874,7 +857,7 @@ public final class StaticAbilityContinuous {
// add Types // add Types
if ((addTypes != null && !addTypes.isEmpty()) || (removeTypes != null && !removeTypes.isEmpty()) || addAllCreatureTypes || !remove.isEmpty()) { if ((addTypes != null && !addTypes.isEmpty()) || (removeTypes != null && !removeTypes.isEmpty()) || addAllCreatureTypes || !remove.isEmpty()) {
affectedCard.addChangedCardTypes(addTypes, removeTypes, addAllCreatureTypes, remove, affectedCard.addChangedCardTypes(addTypes, removeTypes, addAllCreatureTypes, remove,
se.getTimestamp(), stAb.getId(), true, stAb.isCharacteristicDefining()); se.getTimestamp(), stAb.getId(), false, stAb.isCharacteristicDefining());
} }
// add colors // add colors
@@ -932,6 +915,21 @@ public final class StaticAbilityContinuous {
return affectedCards; return affectedCards;
} }
private static ColorSet getColorsFromParam(StaticAbility stAb, final String colors) {
final Card hostCard = stAb.getHostCard();
ColorSet addColors = null;
if (colors.equals("ChosenColor")) {
if (hostCard.hasChosenColor()) {
addColors = ColorSet.fromNames(hostCard.getChosenColors());
}
} else if (colors.equals("All")) {
addColors = ColorSet.ALL_COLORS;
} else {
addColors = ColorSet.fromNames(colors.split(" & "));
}
return addColors;
}
private static void buildIgnorEffectAbility(final StaticAbility stAb, final String costString, final List<Player> players, final CardCollectionView cards) { private static void buildIgnorEffectAbility(final StaticAbility stAb, final String costString, final List<Player> players, final CardCollectionView cards) {
final List<Player> validActivator = new ArrayList<>(players); final List<Player> validActivator = new ArrayList<>(players);
for (final Card c : cards) { for (final Card c : cards) {
@@ -1031,10 +1029,18 @@ public final class StaticAbilityContinuous {
// non - CharacteristicDefining // non - CharacteristicDefining
CardCollection affectedCards = new CardCollection(); CardCollection affectedCards = new CardCollection();
CardCollection definedCards = null;
if (stAb.hasParam("AffectedDefined")) {
definedCards = AbilityUtils.getDefinedCards(hostCard, stAb.getParam("AffectedDefined"), stAb).filter(CardPredicates.phasedIn());
}
// add preList in addition to the normal affected cards // add preList in addition to the normal affected cards
// need to add before game cards to have preference over them // need to add before game cards to have preference over them
if (!preList.isEmpty()) { if (!preList.isEmpty()) {
if (stAb.hasParam("AffectedZone")) { if (stAb.hasParam("AffectedDefined")) {
affectedCards.addAll(preList);
affectedCards.retainAll(definedCards);
} else if (stAb.hasParam("AffectedZone")) {
affectedCards.addAll(CardLists.filter(preList, CardPredicates.inZone( affectedCards.addAll(CardLists.filter(preList, CardPredicates.inZone(
ZoneType.listValueOf(stAb.getParam("AffectedZone"))))); ZoneType.listValueOf(stAb.getParam("AffectedZone")))));
} else { } else {
@@ -1042,7 +1048,9 @@ public final class StaticAbilityContinuous {
} }
} }
if (stAb.hasParam("AffectedZone")) { if (stAb.hasParam("AffectedDefined")) {
affectedCards.addAll(definedCards);
} else if (stAb.hasParam("AffectedZone")) {
affectedCards.addAll(game.getCardsIn(ZoneType.listValueOf(stAb.getParam("AffectedZone")))); affectedCards.addAll(game.getCardsIn(ZoneType.listValueOf(stAb.getParam("AffectedZone"))));
} else { } else {
affectedCards.addAll(game.getCardsIn(ZoneType.Battlefield)); affectedCards.addAll(game.getCardsIn(ZoneType.Battlefield));

View File

@@ -30,13 +30,11 @@ public class StaticAbilityMustAttack {
if (stAb.hasParam("MustAttack")) { if (stAb.hasParam("MustAttack")) {
List<GameEntity> def = AbilityUtils.getDefinedEntities(stAb.getHostCard(), stAb.getParam("MustAttack"), stAb); List<GameEntity> def = AbilityUtils.getDefinedEntities(stAb.getHostCard(), stAb.getParam("MustAttack"), stAb);
for (GameEntity e : def) { for (GameEntity e : def) {
if (e instanceof Player) { if (e instanceof Player attackPl) {
Player attackPl = (Player) e;
if (!game.getPhaseHandler().isPlayerTurn(attackPl)) { // CR 506.2 if (!game.getPhaseHandler().isPlayerTurn(attackPl)) { // CR 506.2
entityList.add(e); entityList.add(e);
} }
} else if (e instanceof Card) { } else if (e instanceof Card attackPW) {
Card attackPW = (Card) e;
if (!game.getPhaseHandler().isPlayerTurn(attackPW.getController())) { // CR 506.2 if (!game.getPhaseHandler().isPlayerTurn(attackPW.getController())) { // CR 506.2
entityList.add(e); entityList.add(e);
} }

View File

@@ -56,9 +56,6 @@ public class TriggerAttackerBlockedByCreature extends Trigger {
public final boolean performTest(final Map<AbilityKey, Object> runParams) { public final boolean performTest(final Map<AbilityKey, Object> runParams) {
final Object a = runParams.get(AbilityKey.Attacker), final Object a = runParams.get(AbilityKey.Attacker),
b = runParams.get(AbilityKey.Blocker); b = runParams.get(AbilityKey.Blocker);
if (!(a instanceof Card && b instanceof Card)) {
return false;
}
final Card attacker = (Card) a, final Card attacker = (Card) a,
blocker = (Card) b; blocker = (Card) b;

View File

@@ -113,8 +113,7 @@ public class TriggerDamageDone extends Trigger {
final Object target = runParams.get(AbilityKey.DamageTarget); final Object target = runParams.get(AbilityKey.DamageTarget);
final Card source = (Card) runParams.get(AbilityKey.DamageSource); final Card source = (Card) runParams.get(AbilityKey.DamageSource);
if (target instanceof Player) { if (target instanceof Player trigTgt) {
final Player trigTgt = (Player) target;
if (!Expressions.compare(trigTgt.getAssignedDamage(null, source), operator, operand)) { if (!Expressions.compare(trigTgt.getAssignedDamage(null, source), operator, operand)) {
return false; return false;
} }

View File

@@ -384,17 +384,15 @@ public class TriggerHandler {
return false; // It's not the right phase to go off. return false; // It's not the right phase to go off.
} }
if (TriggerType.Always.equals(regtrig.getMode())) {
if (game.getStack().hasStateTrigger(regtrig.getId())) {
return false; // State triggers that are already on the stack
// don't trigger again.
}
}
if (regtrig.isSuppressed()) { if (regtrig.isSuppressed()) {
return false; // Trigger removed by effect return false; // Trigger removed by effect
} }
if (TriggerType.Always.equals(regtrig.getMode()) && game.getStack().hasStateTrigger(regtrig.getId())) {
return false; // State triggers that are already on the stack
// don't trigger again.
}
// do not check delayed // do not check delayed
if (regtrig.getSpawningAbility() == null && !regtrig.zonesCheck(game.getZoneOf(regtrig.getHostCard()))) { if (regtrig.getSpawningAbility() == null && !regtrig.zonesCheck(game.getZoneOf(regtrig.getHostCard()))) {
return false; // Host card isn't where it needs to be. return false; // Host card isn't where it needs to be.
@@ -415,6 +413,10 @@ public class TriggerHandler {
return false; // Not the right mode. return false; // Not the right mode.
} }
if (regtrig.isSuppressed()) {
return false; // Trigger removed by effect
}
/* this trigger can only be activated once per turn, verify it hasn't already run */ /* this trigger can only be activated once per turn, verify it hasn't already run */
if (!regtrig.checkActivationLimit()) { if (!regtrig.checkActivationLimit()) {
return false; return false;
@@ -431,21 +433,17 @@ public class TriggerHandler {
if (!regtrig.performTest(runParams)) { if (!regtrig.performTest(runParams)) {
return false; // Test failed. return false; // Test failed.
} }
if (regtrig.isSuppressed()) {
return false; // Trigger removed by effect
}
if (TriggerType.Always.equals(regtrig.getMode())) { if (TriggerType.Always.equals(regtrig.getMode()) && game.getStack().hasStateTrigger(regtrig.getId())) {
if (game.getStack().hasStateTrigger(regtrig.getId())) { return false; // State triggers that are already on the stack
return false; // State triggers that are already on the stack // don't trigger again.
// don't trigger again.
}
} }
// check if any static abilities are disabling the trigger (Torpor Orb and the like) // check if any static abilities are disabling the trigger (Torpor Orb and the like)
if (!regtrig.isStatic() && StaticAbilityDisableTriggers.disabled(game, regtrig, runParams)) { if (!regtrig.isStatic() && StaticAbilityDisableTriggers.disabled(game, regtrig, runParams)) {
return false; return false;
} }
return true; return true;
} }

View File

@@ -22,7 +22,7 @@ public class TriggerManifestDread extends Trigger {
@Override @Override
public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> runParams) { public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObject(AbilityKey.NewCard, runParams.get(AbilityKey.Card)); sa.setTriggeringObject(AbilityKey.Cards, runParams.get(AbilityKey.Cards));
} }

View File

@@ -251,6 +251,13 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
return false; return false;
} }
} }
if (getSpawningAbility() != null && getSpawningAbility().hasParam("TriggersWhenSpent")) {
if (!getTriggerRemembered().contains(spellAbility)) {
return false;
}
}
return true; return true;
} }

View File

@@ -2,10 +2,7 @@ package forge.game.trigger;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.TreeBasedTable; import com.google.common.collect.TreeBasedTable;
@@ -39,141 +36,6 @@ import forge.game.spellability.TargetRestrictions;
// use of any of the methods) // use of any of the methods)
public class WrappedAbility extends Ability { public class WrappedAbility extends Ability {
static Set<ApiType> noTimestampCheck = ImmutableSet.of(
ApiType.Abandon, // no Triggered
ApiType.ActivateAbility, // no Triggered
ApiType.AddPhase, // only player
ApiType.AddTurn, // only player
ApiType.SkipPhase, // only player
ApiType.SkipTurn, // only player
ApiType.AlterAttribute, // updated
ApiType.Amass, // no Triggered only you
ApiType.Ascend, // only player (you)
ApiType.Animate, // updated
ApiType.AnimateAll, // no triggered
ApiType.Attach, // checked
ApiType.Balance, // only player
ApiType.BecomesBlocked, // no triggered
ApiType.MustBlock, // updated
ApiType.SwitchBlock, // no triggered
ApiType.BecomeMonarch, // only player
ApiType.Bond, // updated
ApiType.BidLife, // only player
ApiType.Clash, // only player
ApiType.Clone, // checked
ApiType.GenericChoice, // only player
ApiType.Connive, // no triggered
ApiType.Encode, // no triggered
ApiType.Meld, // no triggered
ApiType.Haunt, // has own logic
ApiType.PutCounter,
ApiType.PutCounterAll, // no triggered
ApiType.MoveCounter,
ApiType.MultiplyCounter,
ApiType.MoveCounter,
ApiType.RemoveCounter,
ApiType.AddOrRemoveCounter,
ApiType.MoveCounter,
ApiType.Draw, // only player
ApiType.GainLife, // only player
ApiType.SetLife, // only player
ApiType.LoseLife, // only player
ApiType.ChangeZone,
ApiType.Destroy,
ApiType.Token,
ApiType.SetState,
ApiType.Play,
ApiType.Sacrifice, // no triggered
ApiType.SacrificeAll,
ApiType.Pump,
ApiType.Phases, // updated
ApiType.DealDamage, // checked
ApiType.DelayedTrigger,
ApiType.ImmediateTrigger,
ApiType.Goad, // updated
ApiType.EachDamage,
ApiType.WinsGame, // only player
ApiType.LosesGame, // only player
ApiType.Incubate, // only player
ApiType.Mill, // only player
ApiType.Explore,
ApiType.Protection, // should not care about triggered
ApiType.ProtectionAll, // No Triggered
ApiType.Proliferate, // only player no triggered interaction
ApiType.TimeTravel, // only player no triggered interaction
ApiType.CopyPermanent,
ApiType.Token,
ApiType.Debuff, // updated
ApiType.Manifest, // no triggered
ApiType.Scry, // only player
ApiType.SetInMotion, // No Triggered
ApiType.Shuffle, // only player
ApiType.Surveil, // only player
ApiType.Tap, // Done
ApiType.TapAll, // uses filterListByType
ApiType.TapOrUntap, // No TriggeredCard
ApiType.TapOrUntapAll, // No TriggeredCard
ApiType.Untap, // Done
ApiType.UntapAll, // only player
ApiType.Unattach, // No Triggered
ApiType.UnattachAll, // No Triggered
ApiType.Regenerate, // Updated
ApiType.Regeneration, // Replacement Effect only
ApiType.RemoveFromCombat, // Done
// only Replacement Effects, no Trigger
ApiType.ReplaceCounter,
ApiType.ReplaceDamage,
ApiType.ReplaceEffect,
ApiType.ReplaceMana,
ApiType.ReplaceSplitDamage,
ApiType.ReplaceToken,
ApiType.Repeat,
ApiType.RepeatEach,
ApiType.RollDice, // only player
ApiType.RollPlanarDice, // only player
ApiType.Seek, // only player
ApiType.Heist, // only player
ApiType.RingTemptsYou, // only player
ApiType.TakeInitiative, // only player
ApiType.UnlockDoor, // no triggered
ApiType.Poison, // only player
ApiType.Venture, // only player
ApiType.VillainousChoice, // only player
ApiType.Vote, // only player
// internal
ApiType.BlankLine,
ApiType.DamageResolve,
ApiType.ChangeZoneResolve,
ApiType.InternalLegendaryRule,
ApiType.InternalIgnoreEffect
);
private final SpellAbility sa; private final SpellAbility sa;
private Player decider; private Player decider;
@@ -323,7 +185,7 @@ public class WrappedAbility extends Ability {
// a real solution would include only the triggering information that actually is used, but that's a major change // a real solution would include only the triggering information that actually is used, but that's a major change
@Override @Override
public String toUnsuppressedString() { public String toUnsuppressedString() {
String desc = this.getStackDescription(); /* use augmented stack description as string for wrapped things */ String desc = this.getStackDescription(false); /* use augmented stack description as string for wrapped things */
String card = getHostCard().toString(); String card = getHostCard().toString();
if (!desc.contains(card) && desc.contains(" this ")) { /* a hack for Evolve and similar that don't have CARDNAME */ if (!desc.contains(card) && desc.contains(" this ")) { /* a hack for Evolve and similar that don't have CARDNAME */
return card + ": " + desc; return card + ": " + desc;
@@ -332,15 +194,23 @@ public class WrappedAbility extends Ability {
@Override @Override
public String getStackDescription() { public String getStackDescription() {
return getStackDescription(true);
}
public String getStackDescription(boolean withTargets) {
final Trigger regtrig = getTrigger(); final Trigger regtrig = getTrigger();
if (regtrig == null) return ""; if (regtrig == null) return "";
final StringBuilder sb = final StringBuilder sb =
new StringBuilder(regtrig.replaceAbilityText(regtrig.toString(true), this, true)); new StringBuilder(regtrig.replaceAbilityText(regtrig.toString(true), this, true));
List<TargetChoices> allTargets = sa.getAllTargetChoices();
if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) { // prevent text growing too long when SA target other in a chain and also potential StackOverflow
sb.append(" (Targeting: "); if (withTargets) {
sb.append(allTargets); List<TargetChoices> allTargets = sa.getAllTargetChoices();
sb.append(")"); if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) {
sb.append(" (Targeting: ");
sb.append(allTargets);
sb.append(")");
}
} }
String important = regtrig.getImportantStackObjects(this); String important = regtrig.getImportantStackObjects(this);
@@ -577,35 +447,9 @@ public class WrappedAbility extends Ability {
} }
} }
timestampCheck();
getActivatingPlayer().getController().playSpellAbilityNoStack(sa, false); getActivatingPlayer().getController().playSpellAbilityNoStack(sa, false);
} }
/**
* TODO remove this function after the Effects are updated
*/
protected void timestampCheck() {
final Game game = sa.getActivatingPlayer().getGame();
if (noTimestampCheck.contains(sa.getApi())) {
return;
}
final Map<AbilityKey, Object> triggerMap = AbilityKey.newMap(sa.getTriggeringObjects());
for (Entry<AbilityKey, Object> ev : triggerMap.entrySet()) {
if (ev.getValue() instanceof Card) {
Card card = (Card) ev.getValue();
Card current = game.getCardState(card);
if (card.isInPlay() && current.isInPlay() && !current.equalsWithGameTimestamp(card)) {
// TODO: figure out if NoTimestampCheck should be the default for ChangesZone triggers
sa.getTriggeringObjects().remove(ev.getKey());
}
}
}
// TODO: CardCollection
}
@Override @Override
public CardDamageMap getDamageMap() { public CardDamageMap getDamageMap() {
return sa.getDamageMap(); return sa.getDamageMap();

View File

@@ -85,6 +85,8 @@ public enum TrackableProperty {
RingLevel(TrackableTypes.IntegerType), RingLevel(TrackableTypes.IntegerType),
CurrentRoom(TrackableTypes.StringType), CurrentRoom(TrackableTypes.StringType),
Intensity(TrackableTypes.IntegerType), Intensity(TrackableTypes.IntegerType),
OverlayText(TrackableTypes.StringType),
MarkerText(TrackableTypes.StringListType),
Remembered(TrackableTypes.StringType), Remembered(TrackableTypes.StringType),
NamedCard(TrackableTypes.StringListType), NamedCard(TrackableTypes.StringListType),
PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze), PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze),
@@ -217,7 +219,6 @@ public enum TrackableProperty {
CommanderCast(TrackableTypes.IntegerMapType), CommanderCast(TrackableTypes.IntegerMapType),
CommanderDamage(TrackableTypes.IntegerMapType), CommanderDamage(TrackableTypes.IntegerMapType),
MindSlaveMaster(TrackableTypes.PlayerViewType), MindSlaveMaster(TrackableTypes.PlayerViewType),
Speed(TrackableTypes.IntegerType),
Ante(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), Ante(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze),
Battlefield(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), //zones can't respect freeze, otherwise cards that die from state based effects won't have that reflected in the UI Battlefield(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), //zones can't respect freeze, otherwise cards that die from state based effects won't have that reflected in the UI

View File

@@ -23,7 +23,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-gui-android</artifactId> <artifactId>forge-gui-android</artifactId>
@@ -66,7 +66,7 @@
<configuration> <configuration>
<!-- generate versionName from revision property to snapshot-version property --> <!-- generate versionName from revision property to snapshot-version property -->
<name>snapshot-version</name> <name>snapshot-version</name>
<value>${revision}</value> <value>2.0.02-SNAPSHOT</value>
<regex>-SNAPSHOT</regex> <regex>-SNAPSHOT</regex>
<replacement>-SNAPSHOT-${month.date}</replacement> <replacement>-SNAPSHOT-${month.date}</replacement>
<failIfNoMatch>false</failIfNoMatch> <failIfNoMatch>false</failIfNoMatch>

View File

@@ -1,11 +1,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-gui-desktop</artifactId> <artifactId>forge-gui-desktop</artifactId>
@@ -50,7 +49,7 @@
<configuration> <configuration>
<!-- generate versionName from revision property to snapshot-version property --> <!-- generate versionName from revision property to snapshot-version property -->
<name>snapshot-version</name> <name>snapshot-version</name>
<value>${revision}</value> <value>2.0.02-SNAPSHOT</value>
<regex>-SNAPSHOT</regex> <regex>-SNAPSHOT</regex>
<replacement>-SNAPSHOT-${month.date}</replacement> <replacement>-SNAPSHOT-${month.date}</replacement>
<failIfNoMatch>false</failIfNoMatch> <failIfNoMatch>false</failIfNoMatch>
@@ -486,63 +485,42 @@
<phase>pre-integration-test</phase> <phase>pre-integration-test</phase>
<configuration> <configuration>
<target> <target>
<mkdir dir="${project.build.directory}/${project.build.finalName}-osx"/> <mkdir dir="${project.build.directory}/${project.build.finalName}-osx" />
<copy todir="${project.build.directory}/${project.build.finalName}-osx"> <copy todir="${project.build.directory}/${project.build.finalName}-osx">
<fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt"/> <fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" <fileset dir="${basedir}/../forge-gui/release-files/" includes="CHANGES.txt" />
includes="CHANGES.txt"/> <fileset dir="${basedir}/../forge-gui/release-files/" includes="CONTRIBUTORS.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" <fileset dir="${basedir}/../forge-gui/release-files/" includes="ISSUES.txt" />
includes="CONTRIBUTORS.txt"/> <fileset dir="${basedir}/../forge-gui/release-files/" includes="INSTALLATION.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" <fileset dir="${basedir}/../forge-gui/release-files/" includes="GAMEPAD_README.txt" />
includes="ISSUES.txt"/> <fileset dir="${basedir}/../forge-gui/" includes="MANUAL.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" <fileset dir="${basedir}/" includes="sentry.properties" />
includes="INSTALLATION.txt"/>
<fileset dir="${basedir}/../forge-gui/release-files/"
includes="GAMEPAD_README.txt"/>
<fileset dir="${basedir}/../forge-gui/" includes="MANUAL.txt"/>
<fileset dir="${basedir}/" includes="sentry.properties"/>
</copy> </copy>
<taskdef name="bundleapp" <taskdef name="bundleapp" classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar" classname="com.oracle.appbundler.AppBundlerTask" />
classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar" <bundleapp outputdirectory="${project.build.directory}/${project.build.finalName}-osx" name="${project.name}" displayname="${project.name}" shortversion="${project.version}" identifier="forge.view.Main" icon="${basedir}/${configSourceDirectory}/Forge.icns" applicationCategory="public.app-category.games" mainclassname="forge.view.Main">
classname="com.oracle.appbundler.AppBundlerTask"/> <classpath file="${project.build.directory}/${project.build.finalName}-jar-with-dependencies.jar" />
<bundleapp <classpath file="${basedir}/../forge-gui/forge.profile.properties.example" />
outputdirectory="${project.build.directory}/${project.build.finalName}-osx" <option value="-Dapple.laf.useScreenMenuBar=true" />
name="${project.name}" displayname="${project.name}" <option value="-Dcom.apple.macos.use-file-dialog-packages=true" />
shortversion="${project.version}" identifier="forge.view.Main" <option value="-Dcom.apple.macos.useScreenMenuBar=true" />
icon="${basedir}/${configSourceDirectory}/Forge.icns" <option value="-Dcom.apple.mrj.application.apple.menu.about.name=Forge" />
applicationCategory="public.app-category.games" <option value="-Dcom.apple.smallTabs=true" />
mainclassname="forge.view.Main"> <option value="-Xmx4096M" />
<classpath <option value="-Dapp.dir=$APP_ROOT/Contents/Resources/" />
file="${project.build.directory}/${project.build.finalName}-jar-with-dependencies.jar"/>
<classpath file="${basedir}/../forge-gui/forge.profile.properties.example"/>
<option value="-Dapple.laf.useScreenMenuBar=true"/>
<option value="-Dcom.apple.macos.use-file-dialog-packages=true"/>
<option value="-Dcom.apple.macos.useScreenMenuBar=true"/>
<option value="-Dcom.apple.mrj.application.apple.menu.about.name=Forge"/>
<option value="-Dcom.apple.smallTabs=true"/>
<option value="-Xmx4096M"/>
<option value="-Dapp.dir=$APP_ROOT/Contents/Resources/"/>
</bundleapp> </bundleapp>
<copy todir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res"> <copy todir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res">
<fileset dir="${basedir}/../forge-gui/res" excludes="**/cardsfolder/**"/> <fileset dir="${basedir}/../forge-gui/res" excludes="**/cardsfolder/**" />
</copy> </copy>
<mkdir dir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder"/> <mkdir dir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder" />
<zip destfile="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder/cardsfolder.zip" <zip destfile="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder/cardsfolder.zip" basedir="${basedir}/../forge-gui/res/cardsfolder" level="1" />
basedir="${basedir}/../forge-gui/res/cardsfolder" level="1"/> <symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" resource="/Applications" />
<symlink <exec executable="${basedir}/../forge-gui/${configSourceDirectory}/create-dmg" failonerror="false">
link="${project.build.directory}/${project.build.finalName}-osx/Applications" <arg line="--volname ${project.name}-${project.version} --background ${basedir}/../forge-gui/${configSourceDirectory}/backgroundImage.jpg --window-size 700 419 --icon-size 64 --icon ${forge.file.name} 141 283 --icon ${applications.file.name} 452 283 --icon ${changes.file.name} 645 80 --icon ${license.file.name} 645 200 --icon ${readme.file.name} 645 320 ${project.build.directory}/${project.build.finalName}.dmg ${project.build.directory}/${project.build.finalName}-osx" />
resource="/Applications"/>
<exec executable="${basedir}/../forge-gui/${configSourceDirectory}/create-dmg"
failonerror="false">
<arg line="--volname ${project.name}-${project.version} --background ${basedir}/../forge-gui/${configSourceDirectory}/backgroundImage.jpg --window-size 700 419 --icon-size 64 --icon ${forge.file.name} 141 283 --icon ${applications.file.name} 452 283 --icon ${changes.file.name} 645 80 --icon ${license.file.name} 645 200 --icon ${readme.file.name} 645 320 ${project.build.directory}/${project.build.finalName}.dmg ${project.build.directory}/${project.build.finalName}-osx"/>
</exec> </exec>
<tar basedir="${project.build.directory}" <tar basedir="${project.build.directory}" includes="${project.build.finalName}.dmg" destfile="${project.build.directory}/${project.build.finalName}-osx.tar.bz2" compression="bzip2" />
includes="${project.build.finalName}.dmg"
destfile="${project.build.directory}/${project.build.finalName}-osx.tar.bz2"
compression="bzip2"/>
<!--<symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" action="delete" /> --> <!--<symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" action="delete" /> -->
<exec executable="rm" failonerror="false"> <exec executable="rm" failonerror="false">
<arg line="-f ${project.build.directory}/${project.build.finalName}-osx/Applications"/> <arg line="-f ${project.build.directory}/${project.build.finalName}-osx/Applications" />
</exec> </exec>
</target> </target>
</configuration> </configuration>
@@ -622,54 +600,38 @@
<phase>pre-integration-test</phase> <phase>pre-integration-test</phase>
<configuration> <configuration>
<target> <target>
<mkdir dir="${project.build.directory}/${project.build.finalName}-osx"/> <mkdir dir="${project.build.directory}/${project.build.finalName}-osx" />
<copy todir="${project.build.directory}/${project.build.finalName}-osx"> <copy todir="${project.build.directory}/${project.build.finalName}-osx">
<fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt"/> <fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt" />
<fileset dir="${basedir}/../forge-gui/" includes="README.txt"/> <fileset dir="${basedir}/../forge-gui/" includes="README.txt" />
<fileset dir="${basedir}/../forge-gui/" includes="MANUAL.txt"/> <fileset dir="${basedir}/../forge-gui/" includes="MANUAL.txt" />
<fileset dir="${basedir}/" includes="sentry.properties"/> <fileset dir="${basedir}/" includes="sentry.properties" />
</copy> </copy>
<taskdef name="bundleapp" <taskdef name="bundleapp" classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar" classname="com.oracle.appbundler.AppBundlerTask" />
classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar" <bundleapp outputdirectory="${project.build.directory}/${project.build.finalName}-osx" name="${project.name}" displayname="${project.name}" shortversion="${project.version}" identifier="forge.view.Main" icon="${basedir}/${configSourceDirectory}/Forge.icns" applicationCategory="public.app-category.games" mainclassname="forge.view.Main">
classname="com.oracle.appbundler.AppBundlerTask"/> <classpath file="${project.build.directory}/${project.build.finalName}-jar-with-dependencies.jar" />
<bundleapp <classpath file="${basedir}/../forge-gui/forge.profile.properties.example" />
outputdirectory="${project.build.directory}/${project.build.finalName}-osx" <option value="-Dapple.laf.useScreenMenuBar=true" />
name="${project.name}" displayname="${project.name}" <option value="-Dcom.apple.macos.use-file-dialog-packages=true" />
shortversion="${project.version}" identifier="forge.view.Main" <option value="-Dcom.apple.macos.useScreenMenuBar=true" />
icon="${basedir}/${configSourceDirectory}/Forge.icns" <option value="-Dcom.apple.mrj.application.apple.menu.about.name=Forge" />
applicationCategory="public.app-category.games" <option value="-Dcom.apple.smallTabs=true" />
mainclassname="forge.view.Main"> <option value="-Xmx4096M" />
<classpath <option value="-Dapp.dir=$APP_ROOT/Contents/Resources/" />
file="${project.build.directory}/${project.build.finalName}-jar-with-dependencies.jar"/>
<classpath file="${basedir}/../forge-gui/forge.profile.properties.example"/>
<option value="-Dapple.laf.useScreenMenuBar=true"/>
<option value="-Dcom.apple.macos.use-file-dialog-packages=true"/>
<option value="-Dcom.apple.macos.useScreenMenuBar=true"/>
<option value="-Dcom.apple.mrj.application.apple.menu.about.name=Forge"/>
<option value="-Dcom.apple.smallTabs=true"/>
<option value="-Xmx4096M"/>
<option value="-Dapp.dir=$APP_ROOT/Contents/Resources/"/>
</bundleapp> </bundleapp>
<copy todir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res"> <copy todir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res">
<fileset dir="${basedir}/../forge-gui/res" excludes="**/cardsfolder/**"/> <fileset dir="${basedir}/../forge-gui/res" excludes="**/cardsfolder/**" />
</copy> </copy>
<mkdir dir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder"/> <mkdir dir="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder" />
<zip destfile="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder/cardsfolder.zip" <zip destfile="${project.build.directory}/${project.build.finalName}-osx/Forge.app/Contents/Resources/res/cardsfolder/cardsfolder.zip" basedir="${basedir}/../forge-gui/res/cardsfolder" level="1" />
basedir="${basedir}/../forge-gui/res/cardsfolder" level="1"/> <symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" resource="/Applications" />
<symlink <exec executable="${basedir}/../forge-gui/${configSourceDirectory}/create-dmg" failonerror="false">
link="${project.build.directory}/${project.build.finalName}-osx/Applications" <arg line="--volname ${project.name}-${project.version} --background ${basedir}/../forge-gui/${configSourceDirectory}/backgroundImage.jpg --window-size 700 419 --icon-size 64 --icon ${forge.file.name} 141 283 --icon ${applications.file.name} 452 283 --icon ${changes.file.name} 645 80 --icon ${license.file.name} 645 200 --icon ${readme.file.name} 645 320 ${project.build.directory}/${project.build.finalName}.dmg ${project.build.directory}/${project.build.finalName}-osx" />
resource="/Applications"/>
<exec executable="${basedir}/../forge-gui/${configSourceDirectory}/create-dmg"
failonerror="false">
<arg line="--volname ${project.name}-${project.version} --background ${basedir}/../forge-gui/${configSourceDirectory}/backgroundImage.jpg --window-size 700 419 --icon-size 64 --icon ${forge.file.name} 141 283 --icon ${applications.file.name} 452 283 --icon ${changes.file.name} 645 80 --icon ${license.file.name} 645 200 --icon ${readme.file.name} 645 320 ${project.build.directory}/${project.build.finalName}.dmg ${project.build.directory}/${project.build.finalName}-osx"/>
</exec> </exec>
<tar basedir="${project.build.directory}" <tar basedir="${project.build.directory}" includes="${project.build.finalName}.dmg" destfile="${project.build.directory}/${project.build.finalName}-osx.tar.bz2" compression="bzip2" />
includes="${project.build.finalName}.dmg"
destfile="${project.build.directory}/${project.build.finalName}-osx.tar.bz2"
compression="bzip2"/>
<!--<symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" action="delete" /> --> <!--<symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" action="delete" /> -->
<exec executable="rm" failonerror="false"> <exec executable="rm" failonerror="false">
<arg line="-f ${project.build.directory}/${project.build.finalName}-osx/Applications"/> <arg line="-f ${project.build.directory}/${project.build.finalName}-osx/Applications" />
</exec> </exec>
</target> </target>
</configuration> </configuration>

View File

@@ -533,11 +533,8 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
} }
if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) { if(card.getMarkerText() != null) {
List<String> markers = new ArrayList<>(); drawMarkersTabs(g, card.getMarkerText());
markers.add("In Room:");
markers.add(card.getCurrentRoom());
drawMarkersTabs(g, markers);
} }
final int combatXSymbols = (cardXOffset + (cardWidth / 4)) - 16; final int combatXSymbols = (cardXOffset + (cardWidth / 4)) - 16;

View File

@@ -11,7 +11,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-gui-ios</artifactId> <artifactId>forge-gui-ios</artifactId>
@@ -35,7 +35,7 @@
<filtering>true</filtering> <filtering>true</filtering>
</resource> </resource>
</resources> </resources>
<finalName>forge-ios-${revision}</finalName> <finalName>forge-ios-2.0.02-SNAPSHOT</finalName>
</build> </build>
<dependencies> <dependencies>

View File

@@ -9,7 +9,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-gui-mobile-dev</artifactId> <artifactId>forge-gui-mobile-dev</artifactId>
@@ -45,7 +45,7 @@
<configuration> <configuration>
<!-- generate versionName from revision property to snapshot-version property --> <!-- generate versionName from revision property to snapshot-version property -->
<name>snapshot-version</name> <name>snapshot-version</name>
<value>${revision}</value> <value>2.0.02-SNAPSHOT</value>
<regex>-SNAPSHOT</regex> <regex>-SNAPSHOT</regex>
<replacement>-SNAPSHOT-${month.date}</replacement> <replacement>-SNAPSHOT-${month.date}</replacement>
<failIfNoMatch>false</failIfNoMatch> <failIfNoMatch>false</failIfNoMatch>

View File

@@ -4,7 +4,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-gui-mobile</artifactId> <artifactId>forge-gui-mobile</artifactId>

View File

@@ -606,7 +606,7 @@ public class AdventureEventData implements Serializable {
description += "\n"; description += "\n";
} }
description += "Prizes\n3 round wins: 500 gold\n2 round wins: 200 gold\n1 round win: 100 gold\n"; description += "Prizes\n3 round wins: 500 gold\n2 round wins: 200 gold\n1 round win: 100 gold\n";
description += "Finishing event will award an unsellable copy of each card in your Jumpstart deck."; description += "Participating in this event will award a valueless copy of each card in your Jumpstart deck.";
} }
return description; return description;
} }

View File

@@ -908,14 +908,45 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
} }
public int cardSellPrice(PaperCard card) { public int cardSellPrice(PaperCard card) {
int valuable = cards.count(card) - noSellCards.count(card);
if (valuable == 0) {
return 0;
}
int basePrice = (int) (CardUtil.getCardPrice(card) * difficultyData.sellFactor);
float townPriceModifier = currentLocationChanges == null ? 1f : currentLocationChanges.getTownPriceModifier(); float townPriceModifier = currentLocationChanges == null ? 1f : currentLocationChanges.getTownPriceModifier();
return (int) (CardUtil.getCardPrice(card) * difficultyData.sellFactor * (2.0f - townPriceModifier)); return (int) (basePrice * (2.0f - townPriceModifier));
} }
public void sellCard(PaperCard card, Integer result) { public int sellCard(PaperCard card, Integer result, boolean addGold) {
float price = cardSellPrice(card) * result; // When selling cards, always try to sell cards worth something before selling cards that aren't worth anything
cards.remove(card, result); if (result == null || result < 1) return 0;
addGold((int) price);
float earned = 0;
int valuableCount = cards.count(card) - noSellCards.count(card);
int noValueToSell = result - valuableCount;
int amountValuableToSell = Math.min(result, valuableCount);
if (amountValuableToSell > 0) {
earned += cardSellPrice(card) * amountValuableToSell;
cards.remove(card, amountValuableToSell);
}
if (noValueToSell > 0) {
cards.remove(card, noValueToSell);
noSellCards.remove(card, noValueToSell);
}
if (addGold) {
addGold((int) earned);
}
return (int) earned;
}
public int sellOneCard(PaperCard card) {
return sellCard(card, 1, false);
} }
public void removeItem(String name) { public void removeItem(String name) {
@@ -1166,8 +1197,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
ItemPool<PaperCard> sellableCards = new ItemPool<>(PaperCard.class); ItemPool<PaperCard> sellableCards = new ItemPool<>(PaperCard.class);
sellableCards.addAllFlat(cards.toFlatList()); sellableCards.addAllFlat(cards.toFlatList());
// 1. Remove cards you can't sell // Nosell cards used to be filtered out here. Instead we're going to replace their value with 0
sellableCards.removeAll(noSellCards);
// 1a. Potentially return here if we want to give config option to sell cards from decks // 1a. Potentially return here if we want to give config option to sell cards from decks
// but would need to update the decks on sell, not just the catalog // but would need to update the decks on sell, not just the catalog
@@ -1175,11 +1206,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
Map<PaperCard, Integer> maxCardCounts = new HashMap<>(); Map<PaperCard, Integer> maxCardCounts = new HashMap<>();
for (int i = 0; i < NUMBER_OF_DECKS; i++) { for (int i = 0; i < NUMBER_OF_DECKS; i++) {
for (final Map.Entry<PaperCard, Integer> cp : decks[i].getAllCardsInASinglePool()) { for (final Map.Entry<PaperCard, Integer> cp : decks[i].getAllCardsInASinglePool()) {
int count = cp.getValue();
int count = cp.getValue() - noSellCards.count(cp.getKey());
if (count <= 0) continue;
if (count > maxCardCounts.getOrDefault(cp.getKey(), 0)) { if (count > maxCardCounts.getOrDefault(cp.getKey(), 0)) {
maxCardCounts.put(cp.getKey(), cp.getValue()); maxCardCounts.put(cp.getKey(), cp.getValue());
} }
@@ -1213,9 +1240,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
public void doAutosell() { public void doAutosell() {
int profit = 0; int profit = 0;
for (PaperCard cardToSell : autoSellCards.toFlatList()) { for (PaperCard cardToSell : autoSellCards.toFlatList()) {
profit += AdventurePlayer.current().cardSellPrice(cardToSell); profit += AdventurePlayer.current().sellOneCard(cardToSell);
autoSellCards.remove(cardToSell); autoSellCards.remove(cardToSell);
cards.remove(cardToSell, 1);
} }
addGold(profit); //do this as one transaction so as not to get multiple copies of sound effect addGold(profit); //do this as one transaction so as not to get multiple copies of sound effect
} }

View File

@@ -151,7 +151,7 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
if (!cardManager.isInfinite()) { if (!cardManager.isInfinite()) {
removeCard(card, result); removeCard(card, result);
} }
AdventurePlayer.current().sellCard(card, result); AdventurePlayer.current().sellCard(card, result, true);
lblGold.setText(String.valueOf(AdventurePlayer.current().getGold())); lblGold.setText(String.valueOf(AdventurePlayer.current().getGold()));
} }
}); });
@@ -601,7 +601,7 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
public void run(Boolean result) { public void run(Boolean result) {
if (result) { if (result) {
for (Map.Entry<PaperCard, Integer> entry : catalogPage.cardManager.getFilteredItems()) { for (Map.Entry<PaperCard, Integer> entry : catalogPage.cardManager.getFilteredItems()) {
AdventurePlayer.current().sellCard(entry.getKey(), entry.getValue()); AdventurePlayer.current().sellCard(entry.getKey(), entry.getValue(), true);
} }
catalogPage.refresh(); catalogPage.refresh();
lblGold.setText(String.valueOf(AdventurePlayer.current().getGold())); lblGold.setText(String.valueOf(AdventurePlayer.current().getGold()));

View File

@@ -47,6 +47,10 @@ public class WorldStage extends GameStage implements SaveFileContent {
private transient boolean enterSpawnPOI = false; private transient boolean enterSpawnPOI = false;
NavArrowActor navArrow; NavArrowActor navArrow;
final Rectangle tempBoundingRect = new Rectangle();
final Vector2 enemyMoveVector = new Vector2();
boolean collided = false;
public WorldStage() { public WorldStage() {
super(); super();
background = new WorldBackground(this); background = new WorldBackground(this);
@@ -61,10 +65,6 @@ public class WorldStage extends GameStage implements SaveFileContent {
return instance == null ? instance = new WorldStage() : instance; return instance == null ? instance = new WorldStage() : instance;
} }
final Rectangle tempBoundingRect = new Rectangle();
final Vector2 enemyMoveVector = new Vector2();
boolean collided = false;
@Override @Override
protected void onActing(float delta) { protected void onActing(float delta) {
if (isPaused() || MapStage.getInstance().isDialogOnlyInput() || Forge.advFreezePlayerControls) if (isPaused() || MapStage.getInstance().isDialogOnlyInput() || Forge.advFreezePlayerControls)
@@ -72,7 +72,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
drawNavigationArrow(); drawNavigationArrow();
if (player.isMoving()) { if (player.isMoving()) {
handleMonsterSpawn(delta); handleMonsterSpawn(delta);
handlePointsOfInterestCollision(); collided = collided || handlePointsOfInterestCollision();
globalTimer += delta; globalTimer += delta;
Iterator<Pair<Float, EnemySprite>> it = enemies.iterator(); Iterator<Pair<Float, EnemySprite>> it = enemies.iterator();
while (it.hasNext()) { while (it.hasNext()) {
@@ -146,6 +146,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
pair.getValue().setAnimation(CharacterSprite.AnimationTypes.Idle); pair.getValue().setAnimation(CharacterSprite.AnimationTypes.Idle);
} }
} }
collided = false;
} }
private void removeEnemy(EnemySprite currentMob) { private void removeEnemy(EnemySprite currentMob) {
@@ -200,7 +201,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
} }
} }
public void handlePointsOfInterestCollision() { public boolean handlePointsOfInterestCollision() {
for (Actor actor : foregroundSprites.getChildren()) { for (Actor actor : foregroundSprites.getChildren()) {
if (actor.getClass() == PointOfInterestMapSprite.class) { if (actor.getClass() == PointOfInterestMapSprite.class) {
PointOfInterestMapSprite point = (PointOfInterestMapSprite) actor; PointOfInterestMapSprite point = (PointOfInterestMapSprite) actor;
@@ -215,6 +216,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
WorldSave.getCurrentSave().autoSave(); WorldSave.getCurrentSave().autoSave();
loadPOI(point.getPointOfInterest()); loadPOI(point.getPointOfInterest());
point.getMapSprite().checkOut(); point.getMapSprite().checkOut();
return true;
} else { } else {
if (point == collidingPoint) { if (point == collidingPoint) {
collidingPoint = null; collidingPoint = null;
@@ -222,6 +224,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
} }
} }
} }
return false;
} }
public void loadPOI(PointOfInterest poi) { public void loadPOI(PointOfInterest poi) {

View File

@@ -98,6 +98,7 @@ public class AdventureEventController implements Serializable {
AdventureEventData e; AdventureEventData e;
// TODO After a certain amount of wins, stop offering jump start events
if (random.nextInt(10) <= 2) { if (random.nextInt(10) <= 2) {
e = new AdventureEventData(eventSeed, EventFormat.Jumpstart); e = new AdventureEventData(eventSeed, EventFormat.Jumpstart);
} else { } else {

View File

@@ -336,11 +336,11 @@ public class MapDialog {
else Current.player().takeGold(-E.addShards); else Current.player().takeGold(-E.addShards);
} }
if (E.addMapReputation != 0) { if (E.addMapReputation != 0) {
if (!E.POIReference.isEmpty() && !E.POIReference.contains("$")) { if (E.POIReference != null && !E.POIReference.isEmpty() && !E.POIReference.contains("$")) {
WorldSave.getCurrentSave().getPointOfInterestChanges(E.POIReference).addMapReputation(E.addMapReputation); WorldSave.getCurrentSave().getPointOfInterestChanges(E.POIReference).addMapReputation(E.addMapReputation);
} } else {
else
stage.getChanges().addMapReputation(E.addMapReputation); stage.getChanges().addMapReputation(E.addMapReputation);
}
} }
if (E.deleteMapObject != 0) { //Removes a dummy object from the map. if (E.deleteMapObject != 0) { //Removes a dummy object from the map.
if (E.deleteMapObject < 0) stage.deleteObject(parentID); if (E.deleteMapObject < 0) stage.deleteObject(parentID);

View File

@@ -378,8 +378,7 @@ public class TextRenderer {
lastPieceIdx = pieces.size() - 1; //don't re-wrap anything if reached previous line lastPieceIdx = pieces.size() - 1; //don't re-wrap anything if reached previous line
break; break;
} }
if (lastPiece instanceof TextPiece) { if (lastPiece instanceof TextPiece textPiece) {
TextPiece textPiece = (TextPiece)lastPiece;
int index = textPiece.text.lastIndexOf(' '); int index = textPiece.text.lastIndexOf(' ');
if (index != -1) { if (index != -1) {
if (index == 0) { if (index == 0) {

View File

@@ -781,23 +781,12 @@ public class CardRenderer {
} }
if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) { if(card.getMarkerText() != null) {
List<String> markers = new ArrayList<>(); List<String> markers = card.getMarkerText();
markers.add("In Room:"); if(markers.size() > 1) //Use smaller text for multi-line strings.
markers.add(card.getCurrentRoom()); drawMarkersTabs(markers, g, x, y, w, h, false);
drawMarkersTabs(markers, g, x, y, w, h, false); else
} drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true);
//Class level
if (card.getCurrentState().getType().hasStringType("Class") && ZoneType.Battlefield.equals(card.getZone())) {
List<String> markers = new ArrayList<>();
markers.add("CL:" + card.getClassLevel());
drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true);
}
//Ring level
if (card.getRingLevel() > 0) {
List<String> markers = new ArrayList<>();
markers.add("RL:" + card.getRingLevel());
drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true);
} }
float otherSymbolsSize = w / 4f; float otherSymbolsSize = w / 4f;
@@ -1528,7 +1517,7 @@ public class CardRenderer {
int pageSize = 128; int pageSize = 128;
//only generate images for characters that could be used by Forge //only generate images for characters that could be used by Forge
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890/-+:'"; String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890/-+:'!—";
final PixmapPacker packer = new PixmapPacker(pageSize, pageSize, Pixmap.Format.RGBA8888, 2, false); final PixmapPacker packer = new PixmapPacker(pageSize, pageSize, Pixmap.Format.RGBA8888, 2, false);
final FreeTypeFontParameter parameter = new FreeTypeFontParameter(); final FreeTypeFontParameter parameter = new FreeTypeFontParameter();

View File

@@ -53,8 +53,8 @@ public abstract class FScreen extends FContainer {
} }
public void setHeaderCaption(String headerCaption) { public void setHeaderCaption(String headerCaption) {
if (header instanceof DefaultHeader) { if (header instanceof DefaultHeader dh) {
((DefaultHeader)header).lblCaption.setText(headerCaption); dh.lblCaption.setText(headerCaption);
} }
} }
@@ -77,8 +77,8 @@ public abstract class FScreen extends FContainer {
} }
public void showMenu() { public void showMenu() {
if (header instanceof MenuHeader) { if (header instanceof MenuHeader mh) {
((MenuHeader)header).btnMenu.trigger(); mh.btnMenu.trigger();
} }
else { //just so settings screen if no menu header else { //just so settings screen if no menu header
SettingsScreen.show(false); SettingsScreen.show(false);

View File

@@ -4,7 +4,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.02</version>
</parent> </parent>
<artifactId>forge-gui</artifactId> <artifactId>forge-gui</artifactId>

View File

@@ -49,7 +49,25 @@
"Mishra's Workshop", "Mishra's Workshop",
"Yawgmoth's Bargain", "Yawgmoth's Bargain",
"Gaea's Cradle", "Gaea's Cradle",
"Commander's Sphere" "Commander's Sphere",
"Drake Stone",
"Wrenn and One",
"Under-Construction Skyscraper",
"Temur Elevator",
"Slumbering Waterways",
"Omenpath to Naya",
"The Heron Moon",
"Gobland",
"Fetching Garden",
"Mox Poison",
"Wisedrafter's Will",
"New Master of Arms",
"Halving Season",
"Questing Cosplayer",
"Teferi, Druid of Argoth",
"Anax and Cymede & Kynaios and Tiro",
"Call from the Grave"
], ],
"restrictedEditions": [ "restrictedEditions": [
"HTR", "HTR",
@@ -58,7 +76,6 @@
"HTR19", "HTR19",
"HTR20", "HTR20",
"PCEL", "PCEL",
"DS0",
"HHO", "HHO",
"CMB1", "CMB1",
"UST", "UST",
@@ -67,14 +84,9 @@
"PPC1", "PPC1",
"UND", "UND",
"PUST", "PUST",
"UEPHI23", "DA1",
"UEMIN23", "UST",
"UELAS23", "UNF"
"UEIND23",
"UEBAR23",
"MB2",
"UNF",
"DA1"
], ],
"difficulties": [ "difficulties": [
{ {

View File

@@ -10,4 +10,4 @@ SVar:DBChoose:DB$ ChooseCard | AtRandom$ True | Choices$ Creature.nonToken+OppCt
SVar:DBCopy:DB$ CopyPermanent | Defined$ ChosenCard | AddTypes$ Sliver | SubAbility$ DBCleanup SVar:DBCopy:DB$ CopyPermanent | Defined$ ChosenCard | AddTypes$ Sliver | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True
SVar:DBSeek:DB$ Seek | Type$ Card.Sliver | SpellDescription$ Seek a Sliver card. SVar:DBSeek:DB$ Seek | Type$ Card.Sliver | SpellDescription$ Seek a Sliver card.
Oracle:{4}: Create a 1/1 colorless Sliver creature token. Every player may activate this ability but only once each turn. \n At the gebinning of your upkeep, choose one at random\n• Create a 1/1 colorless Sliver creature token.\nCreate a token of a random nontoken creature your opponent controls. That creature becomes a Sliver in addition to its other types.\n• Seek a Sliver card. Oracle:{4}: Create a 1/1 colorless Sliver creature token. Every player may activate this ability but only once each turn. \n At the beginning of your upkeep, choose one at random\n• Create a 1/1 colorless Sliver creature token.\nCreate a token of a random nontoken creature your opponent controls. That creature becomes a Sliver in addition to its other types.\n• Seek a Sliver card.

View File

@@ -15,17 +15,16 @@ Name=eldrazilarge
3 Eye of Ugin|J20|1 3 Eye of Ugin|J20|1
4 Fellwar Stone|C15|1 4 Fellwar Stone|C15|1
2 Flayer of Loyalties|CMM|1 2 Flayer of Loyalties|CMM|1
1 It That Betrays|CMM|1 2 It That Betrays|CMM|1
1 Kozilek, Butcher of Truth|UMA|1 1 Kozilek, Butcher of Truth|UMA|1
4 Matter Reshaper|OGW|1 4 Matter Reshaper|OGW|1
1 Not of This World|CMM|1 1 Not of This World|CMM|1
4 Reality Smasher|SLD|1 4 Reality Smasher|SLD|1
1 Rise of the Eldrazi|CMM|1 1 Rise of the Eldrazi|CMM|1
2 Sol Ring|CM2|1
4 Thought-Knot Seer|PLIST|1 4 Thought-Knot Seer|PLIST|1
4 Thran Dynamo|MB1|1 4 Thran Dynamo|MB1|1
1 Ulamog, the Ceaseless Hunger|CMM|1 1 Ulamog, the Ceaseless Hunger|CMM|1
1 Ulamog, the Infinite Gyre|2X2|2 2 Ulamog, the Infinite Gyre|2X2|2
2 Void Winnower|BFZ|1 2 Void Winnower|BFZ|1
23 Wastes|SLD|1 23 Wastes|SLD|1
[Sideboard] [Sideboard]

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="30" height="17" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="54"> <map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="30" height="17" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="54">
<editorsettings> <editorsettings>
<export target="wastetown..tmx" format="tmx"/> <export target="wastetown..tmx" format="tmx"/>
</editorsettings> </editorsettings>
@@ -36,7 +36,7 @@
</object> </object>
<object id="53" template="../../obj/enemy.tx" x="197.5" y="138.75"> <object id="53" template="../../obj/enemy.tx" x="197.5" y="138.75">
<properties> <properties>
<property name="enemy" value="Mimic"/> <property name="enemy" value="Fog Trap"/>
</properties> </properties>
</object> </object>
</objectgroup> </objectgroup>

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