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
599 changed files with 4249 additions and 2476 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -115,8 +115,8 @@ public class AiAttackController {
} // overloaded constructor to evaluate single specified attacker
private void refreshCombatants(GameEntity defender) {
if (defender instanceof Card && ((Card) defender).isBattle()) {
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
if (defender instanceof Card card && card.isBattle()) {
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
} else {
this.oppList = getOpponentCreatures(defendingOpponent);
}
@@ -312,7 +312,8 @@ public class AiAttackController {
}
}
// 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;
}
@@ -849,10 +850,9 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons)
if (defender != defendingOpponent) {
if (defender instanceof Player) {
defendingOpponent = (Player) defender;
} else if (defender instanceof Card) {
Card defCard = (Card) defender;
if (defender instanceof Player p) {
defendingOpponent = p;
} else if (defender instanceof Card defCard) {
if (defCard.isBattle()) {
defendingOpponent = defCard.getProtectingPlayer();
} else {
@@ -946,8 +946,8 @@ public class AiAttackController {
return 1;
}
// or weakest player
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
return p1.getLife() - p2.getLife();
}
}
return r2.getValue() - r1.getValue();
@@ -1314,7 +1314,7 @@ public class AiAttackController {
attackersAssigned.add(attacker);
// 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();
int attackNum = 0;
int damage = 0;
@@ -1328,7 +1328,7 @@ public class AiAttackController {
}
}
// if enough damage: switch to next planeswalker
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
break;
}
}
@@ -1754,10 +1754,12 @@ public class AiAttackController {
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
boolean revengeOfRavens = false;
if (defender instanceof Player) {
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card) {
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
if (defender instanceof Player player) {
revengeOfRavens = !CardLists.filter(player.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) {

View File

@@ -161,12 +161,12 @@ public class AiBlockController {
// 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
for (GameEntity defender : defenders) {
if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
final CardCollection attackers = combat.getAttackersOf(defender);
if ((defender instanceof Card card1 && card1.getController().equals(ai))
|| (defender instanceof Card card2 && card2.isBattle() && card2.getProtectingPlayer().equals(ai))) {
final CardCollection ccAttackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers);
sortedAttackers.addAll(attackers);
CardLists.sortByPowerDesc(ccAttackers);
sortedAttackers.addAll(ccAttackers);
} else if (defender instanceof Player && defender.equals(ai)) {
firstAttacker = combat.getAttackersOf(defender);
CardLists.sortByPowerDesc(firstAttacker);
@@ -872,9 +872,9 @@ public class AiBlockController {
CardCollection threatenedPWs = new CardCollection();
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card) {
if (def instanceof Card card) {
if (!onlyIfLethal) {
threatenedPWs.add((Card) def);
threatenedPWs.add(card);
} else {
int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) {
@@ -906,12 +906,12 @@ public class AiBlockController {
continue;
}
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains(def)) {
if (def instanceof Card card && threatenedPWs.contains(def)) {
Card blockerDecided = null;
for (final Card blocker : chumpPWDefenders) {
if (CombatUtil.canBlock(attacker, blocker, combat)) {
combat.addBlocker(attacker, blocker);
pwsWithChumpBlocks.add((Card) def);
pwsWithChumpBlocks.add(card);
chosenChumpBlockers.add(blocker);
blockerDecided = blocker;
blockersLeft.remove(blocker);
@@ -1346,8 +1346,8 @@ public class AiBlockController {
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES)
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
&& combat.getDefenderByAttacker(attacker) instanceof Card
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
&& combat.getDefenderByAttacker(attacker) instanceof Card card
&& card.isPlaneswalker();
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
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()) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
if (part instanceof CostSacrifice sac) {
final String type = sac.getType();
if (type.equals("CARDNAME")) {
@@ -1776,9 +1774,7 @@ public class ComputerUtil {
noRegen = true;
}
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
@@ -1842,9 +1838,7 @@ public class ComputerUtil {
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
threatened.add(c);
}
} else if (o instanceof Player) {
final Player p = (Player) o;
} else if (o instanceof Player p) {
if (source.hasKeyword(Keyword.INFECT)) {
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
threatened.add(p);
@@ -1862,8 +1856,7 @@ public class ComputerUtil {
|| saviourApi == null)) {
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
final boolean canRemove = (c.getNetToughness() <= dmg)
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
if (!canRemove) {
@@ -1909,9 +1902,7 @@ public class ComputerUtil {
|| saviourApi == ApiType.Protection || saviourApi == null
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (o instanceof Card c) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1960,8 +1951,7 @@ public class ComputerUtil {
&& topStack.hasParam("Destination")
&& topStack.getParam("Destination").equals("Exile")) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -1988,8 +1978,7 @@ public class ComputerUtil {
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2011,8 +2000,7 @@ public class ComputerUtil {
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
if (enableCurseAuraRemoval) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
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) {
int damage = attacker.getNetCombatDamage();
int sum = 0;
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) {
if (attacked instanceof Player player && !player.canLoseLife()) {
return 0;
}
@@ -2539,20 +2539,20 @@ public class ComputerUtilCombat {
if (combat != null) {
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
// 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 (((Card) def).isPlaneswalker()) {
if (def instanceof Card card && Iterables.contains(defenders, def)) {
if (card.isPlaneswalker()) {
return def;
}
if (((Card) def).isBattle()) {
if (card.isBattle()) {
return def;
}
}
// 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) {
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) {
if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
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;
}
}

View File

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

View File

@@ -1265,8 +1265,7 @@ public class PlayerControllerAi extends PlayerController {
public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
boolean optional = !tgtSA.getPayCosts().isMandatory();
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA;
if (tgtSA instanceof Spell spell) { // Isn't it ALWAYS a spell?
// 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) {
return ComputerUtil.playStack(tgtSA, player, getGame());

View File

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

View File

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

View File

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

View File

@@ -222,7 +222,7 @@ final class CardFace implements ICardFace, Cloneable {
else variant.replacements.addAll(0, this.replacements);
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.draftActions == null) variant.draftActions = this.draftActions;

View File

@@ -645,9 +645,8 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
/** {@inheritDoc} */
@Override
public boolean equals(final Object o) {
if (o instanceof Deck) {
final DeckBase dbase = (DeckBase) o;
boolean deckBaseEquals = super.equals(dbase);
if (o instanceof DeckBase deckBase) {
boolean deckBaseEquals = super.equals(deckBase);
if (!deckBaseEquals)
return false;
// 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",
"main", "card", "mainboard",
"avatar", "commander", "schemes",
"conspiracy", "planes", "deck", "dungeon"};
"conspiracy", "planes", "deck", "dungeon",
"attractions", "contraptions"};
private static CharSequence[] allCardTypes(){
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
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
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
return Token.UnknownCard(cardName, setCode, cardCount);
}

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
<?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"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.02</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -337,9 +337,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (params.containsKey("Blessing")) {
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 ("Day".equalsIgnoreCase(params.get("DayTime"))) {

View File

@@ -1185,6 +1185,12 @@ public class Game {
for (Player player : getRegisteredPlayers()) {
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) {

View File

@@ -82,12 +82,6 @@ public class GameAction {
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) {
return changeZone(zoneFrom, zoneTo, c, position, cause, null);
}
@@ -107,6 +101,8 @@ public class GameAction {
}
return c;
}
// dev mode
if (zoneFrom == null && !c.isToken()) {
zoneTo.add(c, position, CardCopyService.getLKICopy(c));
checkStaticAbilities();
@@ -314,37 +310,34 @@ public class GameAction {
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);
repParams.put(AbilityKey.CardLKI, lastKnownInfo);
repParams.put(AbilityKey.Cause, cause);
repParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType() : null);
repParams.put(AbilityKey.Destination, zoneTo.getZoneType());
if (toBattlefield) {
repParams.put(AbilityKey.EffectOnly, true);
repParams.put(AbilityKey.CounterTable, table);
repParams.put(AbilityKey.CounterMap, table.column(copied));
}
if (params != null) {
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);
copied.getOwner().removeInboundToken(copied);
if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) {
// reset failed manifested Cards back to original
if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) {
c.forceTurnFaceUp();
}
copied.getOwner().removeInboundToken(copied);
if (repres == ReplacementResult.Prevented) {
c.clearControllers();
cleanStaticEffect(staticEff, copied);
@@ -359,10 +352,6 @@ public class GameAction {
if (c.isInZone(ZoneType.Stack) && !zoneTo.is(ZoneType.Graveyard)) {
return moveToGraveyard(c, cause, params);
}
copied.clearDevoured();
copied.clearDelved();
copied.clearExploited();
} else if (toBattlefield && !c.isInPlay()) {
// was replaced with another Zone Change
if (c.removeChangedState()) {
@@ -379,8 +368,6 @@ public class GameAction {
copied.setGameTimestamp(game.getNextTimestamp());
}
copied.getOwner().removeInboundToken(copied);
// Aura entering as Copy from stack
// without targets it is sent to graveyard
if (copied.isAura() && !copied.isAttachedToEntity() && toBattlefield) {
@@ -432,10 +419,6 @@ public class GameAction {
}
}
if (suppress) {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
}
if (zoneFrom != null) {
if (fromBattlefield && game.getCombat() != null) {
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
unattachCardLeavingBattlefield(copied, c);
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) {
game.addLeftGraveyardThisTurn(lastKnownInfo);
}
// do ETB counters after zone add
if (!suppress && toBattlefield && !table.isEmpty()) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (c.hasChosenColorSpire()) {
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID()));
}
copied.updateStateForView();
if (fromBattlefield) {
copied.setDamage(0); //clear damage after a card leaves the battlefield
copied.setHasBeenDealtDeathtouchDamage(false);
if (copied.isTapped()) {
copied.setTapped(false); //untap card after it leaves the battlefield if needed
game.fireEvent(new GameEventCardTapped(c, false));
}
// needed for counters + ascend
if (!suppress && toBattlefield) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (!table.isEmpty()) {
@@ -579,12 +558,12 @@ public class GameAction {
game.getTriggerHandler().suppressMode(TriggerType.Always);
// Need to apply any static effects to produce correct triggers
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
game.getTriggerHandler().clearSuppression(TriggerType.Always);
checkStaticAbilities();
// 400.7g try adding keyword back into card if it doesn't already have it
@@ -607,25 +586,26 @@ public class GameAction {
c.cleanupExiledWith();
}
game.getTriggerHandler().clearActiveTriggers(copied, null);
game.getTriggerHandler().registerActiveTrigger(copied, false);
// play the change zone sound
game.fireEvent(new GameEventCardChangeZone(c, zoneFrom, zoneTo));
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);
game.getTriggerHandler().clearActiveTriggers(copied, null);
game.getTriggerHandler().registerActiveTrigger(copied, false);
if (params != null) {
runParams.putAll(params);
if (!suppress) {
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())) {
final Map<AbilityKey, Object> runParams2 = AbilityKey.mapFromCard(lastKnownInfo);
runParams2.put(AbilityKey.OriginalController, zoneFrom.getPlayer());
@@ -635,31 +615,18 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams2, false);
}
if (suppress) {
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
if (zoneFrom == null) {
return copied;
}
if (!c.isRealToken() && !toBattlefield) {
copied.clearDevoured();
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) {
// CR 708.9 reveal face-down card leaving
if (wasFacedown && (fromBattlefield || (zoneFrom.is(ZoneType.Stack) && !toBattlefield))) {
Card revealLKI = CardCopyService.getLKICopy(c);
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 (!c.isRealToken() && !c.isSpecialized()) {
copied.setState(CardStateName.Original, true);
}
// Soulbond unpairing
if (c.isPaired()) {
c.getPairedWith().setPairedWith(null);
@@ -680,27 +647,12 @@ public class GameAction {
}
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) {
for (Player p : game.getPlayers()) {
copied.getDamageHistory().setNotAttackedSinceLastUpkeepOf(p);
copied.getDamageHistory().setNotBlockedSinceLastUpkeepOf(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
@@ -748,12 +700,12 @@ public class GameAction {
eff.setLayerTimestamp(timestamp);
} else {
// 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);
StaticAbility stAb = eff.addStaticAbility(AbilityUtils.getSVar(cause, cause.getParam("StaticEffect")));
stAb.setActiveZone(EnumSet.of(ZoneType.Command));
// 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");
game.getAction().moveToCommand(eff, cause);
}
@@ -1038,7 +990,8 @@ public class GameAction {
lki = CardCopyService.getLKICopy(c);
}
game.addChangeZoneLKIInfo(lki);
if (lki.isInPlay()) {
// CR 702.26k
if (lki.isInPlay() && !lki.isPhasedOut()) {
if (game.getCombat() != null) {
game.getCombat().saveLKI(lki);
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
for (Card c : affectedCards) {
c.updateNameforView();
c.updatePowerToughnessForView();
c.updatePTforView();
c.updateTypesForView();
c.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering
c.updateKeywords();
}
// 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)) {
checkAgain = true;
}
@@ -2684,8 +2643,8 @@ public class GameAction {
if (isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
final GameEntity ge = et.getKey();
if (ge instanceof Card) {
((Card) ge).clearAssignedDamage();
if (ge instanceof Card c) {
c.clearAssignedDamage();
}
}
}
@@ -2705,8 +2664,7 @@ public class GameAction {
continue;
}
if (e.getKey() instanceof Card && !lethalDamage.containsKey(e.getKey())) {
Card c = (Card) e.getKey();
if (e.getKey() instanceof Card c && !lethalDamage.containsKey(c)) {
lethalDamage.put(c, c.getExcessDamageValue(false));
}

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import forge.game.*;
import forge.game.ability.AbilityFactory.AbilityRecordType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.IndividualCostPaymentInstance;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
@@ -1362,10 +1363,8 @@ public class AbilityUtils {
}
// do blessing there before condition checks
if (source.hasKeyword(Keyword.ASCEND)) {
if (controller.getZone(ZoneType.Battlefield).size() >= 10) {
controller.setBlessing(true);
}
if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) {
controller.setBlessing(true);
}
if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) {
@@ -2839,7 +2838,13 @@ public class AbilityUtils {
final String[] workingCopy = paidparts[0].split("_");
final String validFilter = workingCopy[1];
// 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>
@@ -3699,6 +3704,10 @@ public class AbilityUtils {
return doXMath(amount, m, source, ctb);
}
if (value.equals("AttractionsVisitedThisTurn")) {
return doXMath(player.getAttractionsVisitedThisTurn(), m, source, ctb);
}
if (value.startsWith("PlaneswalkedToThisTurn")) {
int found = 0;
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
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) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
public static Card createEffect(final SpellAbility sa, final Card hostCard, final Player controller, final String name, final String image, final long timestamp) {
final Game game = controller.getGame();
final Card eff = new Card(game.nextCardId(), game);
eff.setGameTimestamp(timestamp);
@@ -608,12 +607,7 @@ public abstract class SpellAbilityEffect {
eff.setRarity(hostCard.getRarity());
}
if (sa.hasParam("Boon")) {
eff.setBoon(true);
}
eff.setOwner(controller);
eff.setSVars(sa.getSVars());
eff.setSetCode(hostCard.getSetCode());
if (image != null) {
@@ -621,7 +615,12 @@ public abstract class SpellAbilityEffect {
}
eff.setGamePieceType(GamePieceType.EFFECT);
eff.setEffectSource(sa);
if (sa != null) {
eff.setEffectSource(sa);
eff.setSVars(sa.getSVars());
} else {
eff.setEffectSource(hostCard);
}
return eff;
}
@@ -1041,7 +1040,9 @@ public abstract class SpellAbilityEffect {
exilingSource = cause.getOriginalHost();
}
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) {

View File

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

View File

@@ -132,7 +132,6 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
source = changingTgtSA.getTargetCard();
}
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);
changingTgtSI.updateTarget(newTarget, sa.getHostCard());
}

View File

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

View File

@@ -558,22 +558,15 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
continue;
}
if (originZone.is(ZoneType.Stack)) {
game.getStack().remove(gameCard);
}
Card movedCard = null;
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, triggerList);
if (destination.equals(ZoneType.Library)) {
// 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)) {
if (destination.equals(ZoneType.Battlefield)) {
moveParams.put(AbilityKey.SimultaneousETB, tgtCards);
if (sa.isReplacementAbility()) {
ReplacementEffect re = sa.getReplacementEffect();
@@ -725,15 +718,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
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")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
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
// Use "DefinedExchange" to Reference Object that is Exchanging the other direction
GameObject obj = Iterables.getFirst(getDefinedOrTargeted(sa, "DefinedExchange"), null);
if (obj instanceof Card) {
Card c = (Card)obj;
if (obj instanceof Card c) {
if (!c.isInPlay() || si == null) {
// Exchanging object isn't available, continue
continue;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -127,7 +127,8 @@ public class DigEffect extends SpellAbilityEffect {
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
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
// with an optional ability, otherwise the optional ability is skipped.
@@ -236,9 +237,12 @@ public class DigEffect extends SpellAbilityEffect {
valid = top;
}
if (forceRevealToController) {
// Force revealing the card to the player activating the ability (e.g. Explorer's Scope)
game.getAction().revealTo(top, activator);
if (forceReveal) {
// Force revealing the card to defined (e.g. Gonti, Night Minister) or the player activating the
// 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
}

View File

@@ -161,6 +161,9 @@ public class EffectEffect extends SpellAbilityEffect {
for (Player controller : effectOwner) {
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
// Grant abilities

View File

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

View File

@@ -17,24 +17,25 @@ public class ManifestDreadEffect extends ManifestEffect {
final Game game = p.getGame();
for (int i = 0; i < amount; i++) {
CardCollection tgtCards = p.getTopXCardsFromLibrary(2);
Card manifest = null;
Card toGrave = null;
CardCollection toGrave = new CardCollection();
if (!tgtCards.isEmpty()) {
manifest = p.getController().chooseSingleEntityForEffect(tgtCards, sa, getDefaultMessage(), null);
Card manifest = p.getController().chooseSingleEntityForEffect(tgtCards, sa, getDefaultMessage(), null);
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();
CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa);
internalEffect(manifest, p, sa, moveParams);
if (toGrave != null) {
toGrave = game.getAction().moveToGraveyard(toGrave, sa, moveParams);
manifest = internalEffect(manifest, p, sa, moveParams);
// CR 701.60a
if (!manifest.isManifested()) {
tgtCards.add(manifest);
}
for (Card c : tgtCards) {
toGrave.add(game.getAction().moveToGraveyard(c, sa, moveParams));
}
triggerList.triggerChangesZoneAll(game, sa);
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
runParams.put(AbilityKey.Card, toGrave);
runParams.put(AbilityKey.Cards, toGrave);
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);
}
if (redrawPT) {
tgtC.updatePowerToughnessForView();
tgtC.updatePTforView();
}
if (!hiddenkws.isEmpty()) {
@@ -93,7 +93,7 @@ public class PumpAllEffect extends SpellAbilityEffect {
tgtC.removeChangedCardKeywords(timestamp, 0);
tgtC.removeHiddenExtrinsicKeywords(timestamp, 0);
tgtC.updatePowerToughnessForView();
tgtC.updatePTforView();
game.fireEvent(new GameEventCardStatsChanged(tgtC));
}

View File

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

View File

@@ -32,8 +32,7 @@ public class UnattachAllEffect extends SpellAbilityEffect {
// If Cast Targets will be checked on the Stack
for (GameEntity ge : getTargetEntities(sa)) {
if (ge instanceof Card) {
Card gc = (Card) ge;
if (ge instanceof Card gc) {
// check if the object is still in game or if it was moved
Card gameCard = game.getCardState(gc, null);
// 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) {
return cachedCard;
}
String msg = "CardUtil:getLKICopy copy object";
String msg = "CardUtil:getLKICopy copy object";
Breadcrumb bread = new Breadcrumb(msg);
bread.setData("Card", copyFrom.getName());
bread.setData("CardState", copyFrom.getCurrentStateName().toString());
@@ -304,19 +304,19 @@ public class CardCopyService {
newCopy.setCounters(Maps.newHashMap(copyFrom.getCounters()));
newCopy.setColor(copyFrom.getColor().getColor());
newCopy.setPhasedOut(copyFrom.getPhasedOut());
newCopy.setTapped(copyFrom.isTapped());
newCopy.setTributed(copyFrom.isTributed());
newCopy.setMonstrous(copyFrom.isMonstrous());
newCopy.setRenowned(copyFrom.isRenowned());
newCopy.setSolved(copyFrom.isSolved());
newCopy.setSaddled(copyFrom.isSaddled());
newCopy.setPromisedGift(copyFrom.getPromisedGift());
newCopy.setSaddled(copyFrom.isSaddled());
if (newCopy.isSaddled()) newCopy.setSaddledByThisTurn(copyFrom.getSaddledByThisTurn());
newCopy.setSuspectedTimestamp(copyFrom.getSuspectedTimestamp());
newCopy.setColor(copyFrom.getColor().getColor());
newCopy.setPhasedOut(copyFrom.getPhasedOut());
newCopy.setTapped(copyFrom.isTapped());
if (copyFrom.isSuspected()) {
newCopy.setSuspectedEffect(getLKICopy(copyFrom.getSuspectedEffect(), cachedMap));
}
newCopy.setDamageHistory(copyFrom.getDamageHistory());
newCopy.setDamageReceivedThisTurn(copyFrom.getDamageReceivedThisTurn());
@@ -352,8 +352,6 @@ public class CardCopyService {
}
newCopy.setChosenEvenOdd(copyFrom.getChosenEvenOdd());
//newCopy.getEtbCounters().putAll(copyFrom.getEtbCounters());
newCopy.setUnearthed(copyFrom.isUnearthed());
newCopy.copyFrom(copyFrom);

View File

@@ -1835,15 +1835,6 @@ public class CardFactoryUtil {
squadTrigger.setOverridingAbility(squadAbility);
squadTrigger.setSVar("SquadAmount", "Count$OptionalKeywordAmount");
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")) {
final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | Secondary$ True"
+ "| TriggerDescription$ Storm (" + inst.getReminderText() + ")";
@@ -3773,6 +3764,8 @@ public class CardFactoryUtil {
desc = "Basic land";
} else if (type.equals("Land.Artifact")) {
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 ");
@@ -3793,17 +3786,20 @@ public class CardFactoryUtil {
if (keyword.startsWith("Affinity")) {
final String[] k = keyword.split(":");
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);
if (!d.isEmpty()) {
desc = d;
String desc;
if(k.length > 2) {
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();
sb.append("Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ AffinityX | EffectZone$ All");
sb.append("| Description$ Affinity for ").append(desc);

View File

@@ -491,6 +491,7 @@ public class CardView extends GameEntityView {
}
void updateCurrentRoom(Card c) {
set(TrackableProperty.CurrentRoom, c.getCurrentRoom());
updateMarkerText(c);
}
public int getIntensity() {
@@ -514,6 +515,7 @@ public class CardView extends GameEntityView {
}
void updateClassLevel(Card c) {
set(TrackableProperty.ClassLevel, c.getClassLevel());
updateMarkerText(c);
}
public int getRingLevel() {
@@ -525,6 +527,41 @@ public class CardView extends GameEntityView {
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() {
return get(TrackableProperty.Remembered);
}
@@ -974,6 +1011,7 @@ public class CardView extends GameEntityView {
updateDamage(c);
updateSpecialize(c);
updateRingLevel(c);
updateMarkerText(c);
if (c.getIntensity(false) > 0) {
updateIntensity(c);
@@ -1603,7 +1641,9 @@ public class CardView extends GameEntityView {
}
void updateKeywords(Card c, CardState 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.HasWard, c.hasKeyword(Keyword.WARD, state) || state.getTriggers().anyMatch(t -> t.isKeyword(Keyword.WARD)));
set(TrackableProperty.HasDeathtouch, c.hasKeyword(Keyword.DEATHTOUCH, state));
set(TrackableProperty.HasToxic, c.hasKeyword(Keyword.TOXIC, 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.HasHexproof, c.hasKeyword(Keyword.HEXPROOF, 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.HasIndestructible, c.hasKeyword(Keyword.INDESTRUCTIBLE, state));
set(TrackableProperty.HasIntimidate, c.hasKeyword(Keyword.INTIMIDATE, state));

View File

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

View File

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

View File

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

View File

@@ -547,18 +547,6 @@ public class CostAdjustment {
if (!sa.isActivatedAbility() || sa.isReplacementAbility()) {
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" -> {
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(GameEventScry event);
T visit(GameEventShuffle event);
T visit(GameEventSpeedUp event);
T visit(GameEventSpeedChanged event);
T visit(GameEventSpellAbilityCast event);
T visit(GameEventSpellResolved event);
T visit(GameEventSpellRemovedFromStack event);
@@ -101,7 +101,7 @@ public interface IGameEventVisitor<T> {
public T visit(GameEventRollDie event) { return null; }
public T visit(GameEventScry 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(GameEventSpellAbilityCast 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";
} else if (n[i].equals("Land.Artifact")) {
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.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityNoCleanupDamage;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
@@ -180,8 +181,6 @@ public class PhaseHandler implements java.io.Serializable {
}
playerTurn.incrementTurn();
game.getAction().resetActivationsPerTurn();
final int lands = CardLists.count(playerTurn.getLandsInPlay(), CardPredicates.UNTAPPED);
playerTurn.setNumPowerSurgeLands(lands);
}
@@ -395,7 +394,10 @@ public class PhaseHandler implements java.io.Serializable {
// Rule 514.2
// Reset Damage received map
for (final Card c : game.getCardsIncludePhasingIn(ZoneType.Battlefield)) {
c.onCleanupPhase(playerTurn);
if (!StaticAbilityNoCleanupDamage.damageNotRemoved(c)) {
c.setDamage(0);
}
c.setHasBeenDealtDeathtouchDamage(false);
}
game.getEndOfTurn().executeUntil();

View File

@@ -112,6 +112,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private int expentThisTurn;
private int numLibrarySearchedOwn; //The number of times this player has searched his library
private int venturedThisTurn;
private int attractionsVisitedThisTurn;
private int descended;
private int numRingTemptedYou;
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> commanderDamage = Maps.newHashMap();
private DetachedCardEffect commanderEffect = null;
private DetachedCardEffect speedEffect;
private Card monarchEffect;
private Card initiativeEffect;
@@ -197,6 +197,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private Card contraptionSprocketEffect;
private Card radiationEffect;
private Card keywordEffect;
private Card speedEffect;
private Map<Long, Integer> additionalVotes = Maps.newHashMap();
private Map<Long, Integer> additionalOptionalVotes = Maps.newHashMap();
@@ -237,10 +238,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return achievementTracker;
}
public final PlayerOutcome getOutcome() {
return stats.getOutcome();
}
private String chooseName(String originalName) {
String nameCandidate = originalName;
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;
}
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.
*/
@@ -1360,7 +1376,6 @@ public class Player extends GameEntity implements Comparable<Player> {
}
}
public final CardCollectionView getCardsIn(final ZoneType zoneType) {
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);
}
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() {
numRollsThisTurn = 0;
}
public final int getNumRollsThisTurn() {
return numRollsThisTurn;
}
public void roll() {
numRollsThisTurn++;
}
@@ -1717,15 +1709,16 @@ public class Player extends GameEntity implements Comparable<Player> {
final Card c = game.getAction().moveTo(getZone(ZoneType.Battlefield), land, cause);
game.updateLastStateForCard(c);
// play a sound
game.fireEvent(new GameEventLandPlayed(this, land));
// Run triggers
runParams.put(AbilityKey.SpellAbility, cause);
game.getTriggerHandler().runTrigger(TriggerType.LandPlayed, runParams, false);
game.getStack().unfreezeStack();
addLandPlayedThisTurn();
// play a sound
game.fireEvent(new GameEventLandPlayed(this, land));
return c;
}
@@ -1878,6 +1871,10 @@ public class Player extends GameEntity implements Comparable<Player> {
stats.nextTurn();
}
public final int getLastTurnNr() {
return this.lastTurnNr;
}
public boolean hasTappedLandForManaThisTurn() {
return tappedLandForManaThisTurn;
}
@@ -1949,35 +1946,23 @@ public class Player extends GameEntity implements Comparable<Player> {
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() {
return speed;
}
public final void increaseSpeed() {
if (speedEffect == null) createSpeedEffect();
if (!maxSpeed()) { // can't increase past 4
int old = speed;
speed++;
view.updateSpeed(this);
game.fireEvent(new GameEventSpeedUp()); //play sound effect
getGame().fireEvent(new GameEventSpeedChanged(this, old, speed)); //play sound effect
updateSpeedEffect();
}
}
public final void decreaseSpeed() {
if (speed > 1) { // can't decrease speed below 1
int old = speed;
speed--;
view.updateSpeed(this);
getGame().fireEvent(new GameEventPlayerStatsChanged(this, false));
game.fireEvent(new GameEventSpeedChanged(this, old, speed));
updateSpeedEffect();
}
}
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
speed = i;
if (speed > 0) view.updateSpeed(this);
if(this.speedEffect != null)
updateSpeedEffect();
}
public final void createSpeedEffect() {
final PlayerZone com = getZone(ZoneType.Command);
DetachedCardEffect eff = new DetachedCardEffect(this, "Speed Effect");
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);
}
if(this.speedEffect != null || this.noSpeed())
return;
public final List<Card> getPlaneswalkedToThisTurn() {
return planeswalkedToThisTurn;
speedEffect = new Card(game.nextCardId(), null, game);
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) {
@@ -2285,6 +2308,13 @@ public class Player extends GameEntity implements Comparable<Player> {
view.updateUnlimitedHandSize(this);
}
public int getStartingHandSize() {
return startingHandSize;
}
public void setStartingHandSize(int shs) {
startingHandSize = shs;
}
public final int getLandsPlayedThisTurn() {
return landsPlayedThisTurn;
}
@@ -2461,6 +2491,9 @@ public class Player extends GameEntity implements Comparable<Player> {
return game.getMatch().getPlayers().get(game.getRegisteredPlayers().indexOf(this));
}
public final PlayerOutcome getOutcome() {
return stats.getOutcome();
}
private void setOutcome(PlayerOutcome outcome) {
stats.setOutcome(outcome);
}
@@ -2519,10 +2552,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return keywords.getAmount(k);
}
public final int getLastTurnNr() {
return this.lastTurnNr;
}
public void onCleanupPhase() {
for (Card c : getCardsIn(ZoneType.Hand)) {
c.setDrawnThisTurn(false);
@@ -2561,6 +2590,7 @@ public class Player extends GameEntity implements Comparable<Player> {
setCommitedCrimeThisTurn(0);
diceRollsThisTurn = Lists.newArrayList();
setExpentThisTurn(0);
attractionsVisitedThisTurn = 0;
damageReceivedThisTurn.clear();
planeswalkedToThisTurn.clear();
@@ -2693,11 +2723,8 @@ public class Player extends GameEntity implements Comparable<Player> {
return !isInGame();
}
public int getStartingHandSize() {
return startingHandSize;
}
public void setStartingHandSize(int shs) {
startingHandSize = shs;
public final List<Card> getPlaneswalkedToThisTurn() {
return planeswalkedToThisTurn;
}
/**
@@ -3386,6 +3413,19 @@ public class Player extends GameEntity implements Comparable<Player> {
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) {
// If lost then gained, just clear out of lost.
// If gained then lost, just clear out of gained.
@@ -3610,12 +3650,31 @@ public class Player extends GameEntity implements Comparable<Player> {
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() {
if (getKeywordCard() == null)
return;
final PlayerZone com = getZone(ZoneType.Command);
keywordEffect.setText("");
keywordEffect.updateAbilityTextForView();
boolean headerAdded = false;
StringBuilder kw = new StringBuilder();
for (KeywordInterface k : keywords) {
@@ -3627,8 +3686,8 @@ public class Player extends GameEntity implements Comparable<Player> {
}
if (!kw.toString().isEmpty()) {
keywordEffect.setText(trimKeywords(kw.toString()));
keywordEffect.updateAbilityTextForView();
}
keywordEffect.updateAbilityTextForView();
this.updateZoneForView(com);
}
public String trimKeywords(String keywordTexts) {
@@ -3686,6 +3745,9 @@ public class Player extends GameEntity implements Comparable<Player> {
blessingEffect.updateStateForView();
com.add(blessingEffect);
// 702.131d. After a player gets the city's blessing, continuous effects are reapplied
game.getAction().checkStaticAbilities();
} else {
com.remove(blessingEffect);
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.");
}
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) {
additionalVotes.put(timestamp, value);
getView().updateAdditionalVote(this);
@@ -3943,7 +3984,6 @@ public class Player extends GameEntity implements Comparable<Player> {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
game.getTriggerHandler().runTrigger(TriggerType.CommitCrime, runParams, false);
}
public int getCommittedCrimeThisTurn() {
@@ -3970,12 +4010,17 @@ public class Player extends GameEntity implements Comparable<Player> {
public void visitAttractions(int light) {
CardCollection attractions = CardLists.filter(getCardsIn(ZoneType.Battlefield), CardPredicates.isAttractionWithLight(light));
for (Card c : attractions) {
if(!c.wasVisitedThisTurn())
this.attractionsVisitedThisTurn++;
c.visitAttraction(this);
}
}
public void rollToVisitAttractions() {
this.visitAttractions(RollDiceEffect.rollDiceForPlayerToVisitAttractions(this));
}
public int getAttractionsVisitedThisTurn() {
return this.attractionsVisitedThisTurn;
}
public int getCrankCounter() {
return this.crankCounter;
@@ -3983,8 +4028,8 @@ public class Player extends GameEntity implements Comparable<Player> {
public void setCrankCounter(int counters) {
this.crankCounter = counters;
if (this.contraptionSprocketEffect != null) {
Map<CounterType, Integer> counterMap = Map.of(CounterType.get(CounterEnumType.CRANK), this.crankCounter);
contraptionSprocketEffect.setCounters(counterMap);
String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter);
contraptionSprocketEffect.setOverlayText(label);
}
else if (this.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.CONTRAPTIONS)) {
this.createContraptionSprockets();
@@ -4010,12 +4055,8 @@ public class Player extends GameEntity implements Comparable<Player> {
contraptionSprocketEffect.setName("Contraption Sprockets");
contraptionSprocketEffect.setGamePieceType(GamePieceType.EFFECT);
//Add "counters" on the effect to represent the current CRANK counter position.
//This and some other un-cards could benefit from a distinct system for positional counters or markers,
//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);
String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter);
contraptionSprocketEffect.setOverlayText(label);
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();
@@ -4028,11 +4069,9 @@ public class Player extends GameEntity implements Comparable<Player> {
public void addDeclaresAttackers(long ts, Player p) {
this.declaresAttackers.put(ts, p);
}
public void removeDeclaresAttackers(long ts) {
this.declaresAttackers.remove(ts);
}
public Player getDeclaresAttackers() {
Map.Entry<Long, Player> e = declaresAttackers.lastEntry();
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) {
this.declaresBlockers.put(ts, p);
}
public void removeDeclaresBlockers(long ts) {
this.declaresBlockers.remove(ts);
}
public Player getDeclaresBlockers() {
Map.Entry<Long, Player> e = declaresBlockers.lastEntry();
return e == null ? null : e.getValue();

View File

@@ -329,13 +329,6 @@ public class PlayerView extends GameEntityView {
set(TrackableProperty.ControlVotes, val);
}
public int getSpeed() {
return get(TrackableProperty.Speed);
}
void updateSpeed(Player p) {
set(TrackableProperty.Speed, p.getSpeed());
}
public int getAdditionalVillainousChoices() {
return get(TrackableProperty.AdditionalVillainousChoices);
}
@@ -604,10 +597,6 @@ public class PlayerView extends GameEntityView {
}
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());
if (!keywords.isEmpty()) {
details.add(keywords);

View File

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

View File

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

View File

@@ -271,7 +271,6 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
if (this.isRevolt() && !activator.hasRevolt()) return false;
if (this.isDesert() && !activator.hasDesert()) 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.kicked1 && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) return false;

View File

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

View File

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

View File

@@ -72,8 +72,7 @@ public class TargetChoices extends ForwardingList<GameObject> implements Cloneab
public final boolean add(final GameObject o) {
if (o instanceof Player || o instanceof Card || o instanceof SpellAbility) {
if (o instanceof Card) {
Card c = (Card) o;
if (o instanceof Card c) {
cardControllers.put(c, c.getController());
}
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())) {
return false;
}
} else {
if (!getHostCard().isInPlay()) { // default
return false;
}
} else if (!getHostCard().isInPlay()) { // default
return false;
}
}

View File

@@ -452,29 +452,11 @@ public final class StaticAbilityContinuous {
if (layer == StaticAbilityLayer.COLOR) {
if (params.containsKey("AddColor")) {
final String colors = 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(" & "));
}
addColors = getColorsFromParam(stAb, params.get("AddColor"));
}
if (params.containsKey("SetColor")) {
final String colors = 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(" & "));
}
addColors = getColorsFromParam(stAb, params.get("SetColor"));
overwriteColors = true;
}
}
@@ -704,7 +686,7 @@ public final class StaticAbilityContinuous {
setToughness = AbilityUtils.calculateAmount(affectedCard, setT, stAb, true);
}
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,
removeAllAbilities, se.getTimestamp(), stAb, true);
removeAllAbilities, se.getTimestamp(), stAb, false);
affectedCard.updateKeywordsCache(affectedCard.getCurrentState());
}
// add HIDDEN keywords
@@ -862,7 +845,7 @@ public final class StaticAbilityContinuous {
|| removeAllAbilities) {
affectedCard.addChangedCardTraits(
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
if ((addTypes != null && !addTypes.isEmpty()) || (removeTypes != null && !removeTypes.isEmpty()) || addAllCreatureTypes || !remove.isEmpty()) {
affectedCard.addChangedCardTypes(addTypes, removeTypes, addAllCreatureTypes, remove,
se.getTimestamp(), stAb.getId(), true, stAb.isCharacteristicDefining());
se.getTimestamp(), stAb.getId(), false, stAb.isCharacteristicDefining());
}
// add colors
@@ -932,6 +915,21 @@ public final class StaticAbilityContinuous {
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) {
final List<Player> validActivator = new ArrayList<>(players);
for (final Card c : cards) {
@@ -1031,10 +1029,18 @@ public final class StaticAbilityContinuous {
// non - CharacteristicDefining
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
// need to add before game cards to have preference over them
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(
ZoneType.listValueOf(stAb.getParam("AffectedZone")))));
} 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"))));
} else {
affectedCards.addAll(game.getCardsIn(ZoneType.Battlefield));

View File

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

View File

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

View File

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

View File

@@ -384,17 +384,15 @@ public class TriggerHandler {
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()) {
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
if (regtrig.getSpawningAbility() == null && !regtrig.zonesCheck(game.getZoneOf(regtrig.getHostCard()))) {
return false; // Host card isn't where it needs to be.
@@ -415,6 +413,10 @@ public class TriggerHandler {
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 */
if (!regtrig.checkActivationLimit()) {
return false;
@@ -431,21 +433,17 @@ public class TriggerHandler {
if (!regtrig.performTest(runParams)) {
return false; // Test failed.
}
if (regtrig.isSuppressed()) {
return false; // Trigger removed by effect
}
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 (TriggerType.Always.equals(regtrig.getMode()) && game.getStack().hasStateTrigger(regtrig.getId())) {
return false; // State triggers that are already on the stack
// don't trigger again.
}
// check if any static abilities are disabling the trigger (Torpor Orb and the like)
if (!regtrig.isStatic() && StaticAbilityDisableTriggers.disabled(game, regtrig, runParams)) {
return false;
}
return true;
}

View File

@@ -22,7 +22,7 @@ public class TriggerManifestDread extends Trigger {
@Override
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;
}
}
if (getSpawningAbility() != null && getSpawningAbility().hasParam("TriggersWhenSpent")) {
if (!getTriggerRemembered().contains(spellAbility)) {
return false;
}
}
return true;
}

View File

@@ -2,10 +2,7 @@ package forge.game.trigger;
import java.util.List;
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.TreeBasedTable;
@@ -39,141 +36,6 @@ import forge.game.spellability.TargetRestrictions;
// use of any of the methods)
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 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
@Override
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();
if (!desc.contains(card) && desc.contains(" this ")) { /* a hack for Evolve and similar that don't have CARDNAME */
return card + ": " + desc;
@@ -332,15 +194,23 @@ public class WrappedAbility extends Ability {
@Override
public String getStackDescription() {
return getStackDescription(true);
}
public String getStackDescription(boolean withTargets) {
final Trigger regtrig = getTrigger();
if (regtrig == null) return "";
final StringBuilder sb =
new StringBuilder(regtrig.replaceAbilityText(regtrig.toString(true), this, true));
List<TargetChoices> allTargets = sa.getAllTargetChoices();
if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) {
sb.append(" (Targeting: ");
sb.append(allTargets);
sb.append(")");
// prevent text growing too long when SA target other in a chain and also potential StackOverflow
if (withTargets) {
List<TargetChoices> allTargets = sa.getAllTargetChoices();
if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) {
sb.append(" (Targeting: ");
sb.append(allTargets);
sb.append(")");
}
}
String important = regtrig.getImportantStackObjects(this);
@@ -577,35 +447,9 @@ public class WrappedAbility extends Ability {
}
}
timestampCheck();
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
public CardDamageMap getDamageMap() {
return sa.getDamageMap();

View File

@@ -85,6 +85,8 @@ public enum TrackableProperty {
RingLevel(TrackableTypes.IntegerType),
CurrentRoom(TrackableTypes.StringType),
Intensity(TrackableTypes.IntegerType),
OverlayText(TrackableTypes.StringType),
MarkerText(TrackableTypes.StringListType),
Remembered(TrackableTypes.StringType),
NamedCard(TrackableTypes.StringListType),
PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze),
@@ -217,7 +219,6 @@ public enum TrackableProperty {
CommanderCast(TrackableTypes.IntegerMapType),
CommanderDamage(TrackableTypes.IntegerMapType),
MindSlaveMaster(TrackableTypes.PlayerViewType),
Speed(TrackableTypes.IntegerType),
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

View File

@@ -23,7 +23,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.02</version>
</parent>
<artifactId>forge-gui-android</artifactId>
@@ -66,7 +66,7 @@
<configuration>
<!-- generate versionName from revision property to snapshot-version property -->
<name>snapshot-version</name>
<value>${revision}</value>
<value>2.0.02-SNAPSHOT</value>
<regex>-SNAPSHOT</regex>
<replacement>-SNAPSHOT-${month.date}</replacement>
<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"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.02</version>
</parent>
<artifactId>forge-gui-desktop</artifactId>
@@ -50,7 +49,7 @@
<configuration>
<!-- generate versionName from revision property to snapshot-version property -->
<name>snapshot-version</name>
<value>${revision}</value>
<value>2.0.02-SNAPSHOT</value>
<regex>-SNAPSHOT</regex>
<replacement>-SNAPSHOT-${month.date}</replacement>
<failIfNoMatch>false</failIfNoMatch>
@@ -486,63 +485,42 @@
<phase>pre-integration-test</phase>
<configuration>
<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">
<fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt"/>
<fileset dir="${basedir}/../forge-gui/release-files/"
includes="CHANGES.txt"/>
<fileset dir="${basedir}/../forge-gui/release-files/"
includes="CONTRIBUTORS.txt"/>
<fileset dir="${basedir}/../forge-gui/release-files/"
includes="ISSUES.txt"/>
<fileset dir="${basedir}/../forge-gui/release-files/"
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"/>
<fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" includes="CHANGES.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" includes="CONTRIBUTORS.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" includes="ISSUES.txt" />
<fileset dir="${basedir}/../forge-gui/release-files/" 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>
<taskdef name="bundleapp"
classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar"
classname="com.oracle.appbundler.AppBundlerTask"/>
<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">
<classpath
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/"/>
<taskdef name="bundleapp" classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar" classname="com.oracle.appbundler.AppBundlerTask" />
<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">
<classpath 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>
<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>
<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"
basedir="${basedir}/../forge-gui/res/cardsfolder" level="1"/>
<symlink
link="${project.build.directory}/${project.build.finalName}-osx/Applications"
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"/>
<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" basedir="${basedir}/../forge-gui/res/cardsfolder" level="1" />
<symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" 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>
<tar basedir="${project.build.directory}"
includes="${project.build.finalName}.dmg"
destfile="${project.build.directory}/${project.build.finalName}-osx.tar.bz2"
compression="bzip2"/>
<tar basedir="${project.build.directory}" 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" /> -->
<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>
</target>
</configuration>
@@ -622,54 +600,38 @@
<phase>pre-integration-test</phase>
<configuration>
<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">
<fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt"/>
<fileset dir="${basedir}/../forge-gui/" includes="README.txt"/>
<fileset dir="${basedir}/../forge-gui/" includes="MANUAL.txt"/>
<fileset dir="${basedir}/" includes="sentry.properties"/>
<fileset dir="${basedir}/../forge-gui/" includes="LICENSE.txt" />
<fileset dir="${basedir}/../forge-gui/" includes="README.txt" />
<fileset dir="${basedir}/../forge-gui/" includes="MANUAL.txt" />
<fileset dir="${basedir}/" includes="sentry.properties" />
</copy>
<taskdef name="bundleapp"
classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar"
classname="com.oracle.appbundler.AppBundlerTask"/>
<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">
<classpath
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/"/>
<taskdef name="bundleapp" classpath="${basedir}/../forge-gui/${configSourceDirectory}/appbundler-1.0-custom.jar" classname="com.oracle.appbundler.AppBundlerTask" />
<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">
<classpath 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>
<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>
<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"
basedir="${basedir}/../forge-gui/res/cardsfolder" level="1"/>
<symlink
link="${project.build.directory}/${project.build.finalName}-osx/Applications"
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"/>
<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" basedir="${basedir}/../forge-gui/res/cardsfolder" level="1" />
<symlink link="${project.build.directory}/${project.build.finalName}-osx/Applications" 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>
<tar basedir="${project.build.directory}"
includes="${project.build.finalName}.dmg"
destfile="${project.build.directory}/${project.build.finalName}-osx.tar.bz2"
compression="bzip2"/>
<tar basedir="${project.build.directory}" 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" /> -->
<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>
</target>
</configuration>

View File

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

View File

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

View File

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

View File

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

View File

@@ -606,7 +606,7 @@ public class AdventureEventData implements Serializable {
description += "\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;
}

View File

@@ -908,14 +908,45 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
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();
return (int) (CardUtil.getCardPrice(card) * difficultyData.sellFactor * (2.0f - townPriceModifier));
return (int) (basePrice * (2.0f - townPriceModifier));
}
public void sellCard(PaperCard card, Integer result) {
float price = cardSellPrice(card) * result;
cards.remove(card, result);
addGold((int) price);
public int sellCard(PaperCard card, Integer result, boolean addGold) {
// When selling cards, always try to sell cards worth something before selling cards that aren't worth anything
if (result == null || result < 1) return 0;
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) {
@@ -1166,8 +1197,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
ItemPool<PaperCard> sellableCards = new ItemPool<>(PaperCard.class);
sellableCards.addAllFlat(cards.toFlatList());
// 1. Remove cards you can't sell
sellableCards.removeAll(noSellCards);
// Nosell cards used to be filtered out here. Instead we're going to replace their value with 0
// 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
@@ -1175,11 +1206,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
Map<PaperCard, Integer> maxCardCounts = new HashMap<>();
for (int i = 0; i < NUMBER_OF_DECKS; i++) {
for (final Map.Entry<PaperCard, Integer> cp : decks[i].getAllCardsInASinglePool()) {
int count = cp.getValue() - noSellCards.count(cp.getKey());
if (count <= 0) continue;
int count = cp.getValue();
if (count > maxCardCounts.getOrDefault(cp.getKey(), 0)) {
maxCardCounts.put(cp.getKey(), cp.getValue());
}
@@ -1213,9 +1240,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
public void doAutosell() {
int profit = 0;
for (PaperCard cardToSell : autoSellCards.toFlatList()) {
profit += AdventurePlayer.current().cardSellPrice(cardToSell);
profit += AdventurePlayer.current().sellOneCard(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
}

View File

@@ -151,7 +151,7 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
if (!cardManager.isInfinite()) {
removeCard(card, result);
}
AdventurePlayer.current().sellCard(card, result);
AdventurePlayer.current().sellCard(card, result, true);
lblGold.setText(String.valueOf(AdventurePlayer.current().getGold()));
}
});
@@ -601,7 +601,7 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
public void run(Boolean result) {
if (result) {
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();
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;
NavArrowActor navArrow;
final Rectangle tempBoundingRect = new Rectangle();
final Vector2 enemyMoveVector = new Vector2();
boolean collided = false;
public WorldStage() {
super();
background = new WorldBackground(this);
@@ -61,10 +65,6 @@ public class WorldStage extends GameStage implements SaveFileContent {
return instance == null ? instance = new WorldStage() : instance;
}
final Rectangle tempBoundingRect = new Rectangle();
final Vector2 enemyMoveVector = new Vector2();
boolean collided = false;
@Override
protected void onActing(float delta) {
if (isPaused() || MapStage.getInstance().isDialogOnlyInput() || Forge.advFreezePlayerControls)
@@ -72,7 +72,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
drawNavigationArrow();
if (player.isMoving()) {
handleMonsterSpawn(delta);
handlePointsOfInterestCollision();
collided = collided || handlePointsOfInterestCollision();
globalTimer += delta;
Iterator<Pair<Float, EnemySprite>> it = enemies.iterator();
while (it.hasNext()) {
@@ -146,6 +146,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
pair.getValue().setAnimation(CharacterSprite.AnimationTypes.Idle);
}
}
collided = false;
}
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()) {
if (actor.getClass() == PointOfInterestMapSprite.class) {
PointOfInterestMapSprite point = (PointOfInterestMapSprite) actor;
@@ -215,6 +216,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
WorldSave.getCurrentSave().autoSave();
loadPOI(point.getPointOfInterest());
point.getMapSprite().checkOut();
return true;
} else {
if (point == collidingPoint) {
collidingPoint = null;
@@ -222,6 +224,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
}
}
}
return false;
}
public void loadPOI(PointOfInterest poi) {

View File

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

View File

@@ -336,11 +336,11 @@ public class MapDialog {
else Current.player().takeGold(-E.addShards);
}
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);
}
else
} else {
stage.getChanges().addMapReputation(E.addMapReputation);
}
}
if (E.deleteMapObject != 0) { //Removes a dummy object from the map.
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
break;
}
if (lastPiece instanceof TextPiece) {
TextPiece textPiece = (TextPiece)lastPiece;
if (lastPiece instanceof TextPiece textPiece) {
int index = textPiece.text.lastIndexOf(' ');
if (index != -1) {
if (index == 0) {

View File

@@ -781,23 +781,12 @@ public class CardRenderer {
}
if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) {
List<String> markers = new ArrayList<>();
markers.add("In Room:");
markers.add(card.getCurrentRoom());
drawMarkersTabs(markers, g, x, y, w, h, false);
}
//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);
if(card.getMarkerText() != null) {
List<String> markers = card.getMarkerText();
if(markers.size() > 1) //Use smaller text for multi-line strings.
drawMarkersTabs(markers, g, x, y, w, h, false);
else
drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true);
}
float otherSymbolsSize = w / 4f;
@@ -1528,7 +1517,7 @@ public class CardRenderer {
int pageSize = 128;
//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 FreeTypeFontParameter parameter = new FreeTypeFontParameter();

View File

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

View File

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

View File

@@ -49,7 +49,25 @@
"Mishra's Workshop",
"Yawgmoth's Bargain",
"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": [
"HTR",
@@ -58,7 +76,6 @@
"HTR19",
"HTR20",
"PCEL",
"DS0",
"HHO",
"CMB1",
"UST",
@@ -67,14 +84,9 @@
"PPC1",
"UND",
"PUST",
"UEPHI23",
"UEMIN23",
"UELAS23",
"UEIND23",
"UEBAR23",
"MB2",
"UNF",
"DA1"
"DA1",
"UST",
"UNF"
],
"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:DBCleanup:DB$ Cleanup | ClearChosenCard$ True
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
4 Fellwar Stone|C15|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
4 Matter Reshaper|OGW|1
1 Not of This World|CMM|1
4 Reality Smasher|SLD|1
1 Rise of the Eldrazi|CMM|1
2 Sol Ring|CM2|1
4 Thought-Knot Seer|PLIST|1
4 Thran Dynamo|MB1|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
23 Wastes|SLD|1
[Sideboard]

View File

@@ -1,5 +1,5 @@
<?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>
<export target="wastetown..tmx" format="tmx"/>
</editorsettings>
@@ -36,7 +36,7 @@
</object>
<object id="53" template="../../obj/enemy.tx" x="197.5" y="138.75">
<properties>
<property name="enemy" value="Mimic"/>
<property name="enemy" value="Fog Trap"/>
</properties>
</object>
</objectgroup>

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