mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 12:48:00 +00:00
Compare commits
332 Commits
rememberSp
...
forge-1.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb3fb9e1ce | ||
|
|
b27f49c4ff | ||
|
|
1c33e3b039 | ||
|
|
eda0a49097 | ||
|
|
ea8c6630e0 | ||
|
|
0d4310382e | ||
|
|
4ae1547384 | ||
|
|
4192195579 | ||
|
|
0d11e28c11 | ||
|
|
9cf78d0eb4 | ||
|
|
7843004c40 | ||
|
|
4c7c2ee1b5 | ||
|
|
05538bfba2 | ||
|
|
6fe6d6ecfd | ||
|
|
34a8184cc7 | ||
|
|
5d9f86bf20 | ||
|
|
661555551d | ||
|
|
02e118b2e2 | ||
|
|
b59fbe43f0 | ||
|
|
ac51d4170f | ||
|
|
dc91df0e19 | ||
|
|
5bce464268 | ||
|
|
60d5afe9a2 | ||
|
|
ef6e09d4c4 | ||
|
|
76324bc6db | ||
|
|
bee8348b67 | ||
|
|
51fb900eb5 | ||
|
|
8b902671d6 | ||
|
|
7dbbf0a554 | ||
|
|
e75b15b7cb | ||
|
|
43cca9b635 | ||
|
|
b29f390b62 | ||
|
|
f83a97bedd | ||
|
|
c58690dd96 | ||
|
|
3402cddd1d | ||
|
|
3956a5b558 | ||
|
|
72b60c91f5 | ||
|
|
d063e98e33 | ||
|
|
8743cba5d7 | ||
|
|
1d2bbe4923 | ||
|
|
58311e5265 | ||
|
|
8c020c7c8c | ||
|
|
ba25696ea7 | ||
|
|
68359097df | ||
|
|
4b882ad215 | ||
|
|
ee6806b36a | ||
|
|
343723e529 | ||
|
|
15bf426386 | ||
|
|
77184dc930 | ||
|
|
5f11897d11 | ||
|
|
40764691fd | ||
|
|
210cc6164d | ||
|
|
11f597bb23 | ||
|
|
015623a122 | ||
|
|
b04e16d1ca | ||
|
|
e3f24dccd4 | ||
|
|
2e0812dd6a | ||
|
|
9deeec7b14 | ||
|
|
b58b2b93e2 | ||
|
|
2697f5f404 | ||
|
|
78db487e8d | ||
|
|
1ecd1a8340 | ||
|
|
c7539de77d | ||
|
|
b0ee2cedff | ||
|
|
7a0453390a | ||
|
|
1b47a2df87 | ||
|
|
fd54606277 | ||
|
|
25b02141bd | ||
|
|
bcc4158956 | ||
|
|
fc5b76fbb4 | ||
|
|
38ddca0acd | ||
|
|
7ed935c43c | ||
|
|
14629715a2 | ||
|
|
e71ff26b81 | ||
|
|
7d6dec773a | ||
|
|
e52bcc6cdc | ||
|
|
b3e1d96aff | ||
|
|
e13880c87e | ||
|
|
0b14efebb8 | ||
|
|
18a3bfe0b8 | ||
|
|
d8da15d8ae | ||
|
|
5bd9f324e0 | ||
|
|
e0b463c6ea | ||
|
|
01742f06f6 | ||
|
|
dbe212065d | ||
|
|
4b8584376d | ||
|
|
6cf7d3ae80 | ||
|
|
104aaa1255 | ||
|
|
1ba1226661 | ||
|
|
f8bf8c7f28 | ||
|
|
763cb3d08e | ||
|
|
5e94c6c8d3 | ||
|
|
b08ce49057 | ||
|
|
7c98ed75be | ||
|
|
1c9a515bb3 | ||
|
|
a462d34249 | ||
|
|
e1ceed159f | ||
|
|
89757b9677 | ||
|
|
01c52667ad | ||
|
|
25003aa74e | ||
|
|
766277673d | ||
|
|
a1ad2208c8 | ||
|
|
0bfdfb78d3 | ||
|
|
b23425ea9a | ||
|
|
8b7ace6d1c | ||
|
|
1dc8ef5244 | ||
|
|
625585f005 | ||
|
|
acdee9ff0c | ||
|
|
54c5d3f344 | ||
|
|
4fa8fa3993 | ||
|
|
3b21b2a610 | ||
|
|
07324f63d9 | ||
|
|
79c3570bcc | ||
|
|
d9400fe069 | ||
|
|
70f0f1108b | ||
|
|
62316abf38 | ||
|
|
e2ddcdda4d | ||
|
|
e604d9d0cb | ||
|
|
ed218b6e4c | ||
|
|
866c959036 | ||
|
|
b2616860c0 | ||
|
|
0ceb882ab0 | ||
|
|
560dc572c2 | ||
|
|
27bae4a72b | ||
|
|
6686937742 | ||
|
|
d6a466df11 | ||
|
|
7eedd04aa1 | ||
|
|
e3c9a40a17 | ||
|
|
76ebfa9460 | ||
|
|
e0fd812590 | ||
|
|
2c2104dfe6 | ||
|
|
4661a19982 | ||
|
|
9778f9e3a0 | ||
|
|
b6019c6308 | ||
|
|
7006c548a5 | ||
|
|
2eae26cefb | ||
|
|
ccd1117762 | ||
|
|
1917871b98 | ||
|
|
96e6111400 | ||
|
|
f8d360f25f | ||
|
|
5901c6b66b | ||
|
|
b9782f9790 | ||
|
|
74c69f0d37 | ||
|
|
5a81cdc40d | ||
|
|
7ebe97f802 | ||
|
|
956ab8e96e | ||
|
|
b821ec7af5 | ||
|
|
6a92f68602 | ||
|
|
d0c5bcf053 | ||
|
|
4069b603c2 | ||
|
|
0813f9bc66 | ||
|
|
0687fc2a31 | ||
|
|
3f52a8c4f5 | ||
|
|
82e276c661 | ||
|
|
195fe58dcb | ||
|
|
afd1070850 | ||
|
|
8580d585df | ||
|
|
090d6ad9fb | ||
|
|
8ed1ad4afc | ||
|
|
bc854f4d42 | ||
|
|
d8585fd925 | ||
|
|
5ae5fef43e | ||
|
|
48a0ac6039 | ||
|
|
57a3462623 | ||
|
|
929b5fbcfe | ||
|
|
56fda56807 | ||
|
|
872defd992 | ||
|
|
e78202e432 | ||
|
|
657d85530a | ||
|
|
a6e0f9b472 | ||
|
|
86abb0d1d5 | ||
|
|
28a5bd109e | ||
|
|
b357096da2 | ||
|
|
d1d1df27bc | ||
|
|
d7ebafe883 | ||
|
|
f09130eb86 | ||
|
|
50e6a06478 | ||
|
|
65fc4a4371 | ||
|
|
67ca319674 | ||
|
|
1f4142e368 | ||
|
|
b8147a2e58 | ||
|
|
2b8a756932 | ||
|
|
f78bfce802 | ||
|
|
ebcb4e28de | ||
|
|
f0c45cf814 | ||
|
|
aff8d5ce01 | ||
|
|
94064a2a13 | ||
|
|
6359021370 | ||
|
|
6090bc8117 | ||
|
|
264744645e | ||
|
|
ab1e1d2386 | ||
|
|
87ad257c46 | ||
|
|
bc5e7f07ae | ||
|
|
f18651be7a | ||
|
|
9921f58155 | ||
|
|
3ade967f85 | ||
|
|
243c90ced9 | ||
|
|
c1c421fff0 | ||
|
|
bf6f8048d6 | ||
|
|
f26935f37f | ||
|
|
f3f7700ba9 | ||
|
|
2817c4ef61 | ||
|
|
166700b573 | ||
|
|
6edb5ad591 | ||
|
|
66f2ab4e91 | ||
|
|
1f211e6ed5 | ||
|
|
41093c2f7f | ||
|
|
f4df89dca8 | ||
|
|
ffabf14192 | ||
|
|
5ce9db0b08 | ||
|
|
5165c16233 | ||
|
|
1e34fd27fe | ||
|
|
12b464dbcb | ||
|
|
76e3f0305b | ||
|
|
bb57003518 | ||
|
|
dfbfcf0a11 | ||
|
|
c4c061d669 | ||
|
|
e76db8e7ff | ||
|
|
9c4fd8b570 | ||
|
|
5d2e7a16d3 | ||
|
|
cf3a28b120 | ||
|
|
10f8616e37 | ||
|
|
1bade895e1 | ||
|
|
1940bca932 | ||
|
|
620c27a117 | ||
|
|
3071f1f74b | ||
|
|
0f44b644c1 | ||
|
|
427b2973b8 | ||
|
|
25d1d2bb20 | ||
|
|
8f2123f183 | ||
|
|
595fbae34f | ||
|
|
3fd842c1e4 | ||
|
|
11f9520c1e | ||
|
|
40c64a943a | ||
|
|
4f953bf2e1 | ||
|
|
c7d9646f7f | ||
|
|
d4b4dc5ba0 | ||
|
|
2cd86bfe1e | ||
|
|
2f9fb96d29 | ||
|
|
39204513fb | ||
|
|
ebaa96004f | ||
|
|
d381251472 | ||
|
|
f00e759a93 | ||
|
|
aab63e2029 | ||
|
|
3749f2a5de | ||
|
|
aa7ad578d5 | ||
|
|
fb80dece04 | ||
|
|
482ab4b87c | ||
|
|
f3f9a915d5 | ||
|
|
52184e24ce | ||
|
|
b6dbfcee96 | ||
|
|
e07be68786 | ||
|
|
9c3ae840b8 | ||
|
|
20eba0fbd7 | ||
|
|
735516e6d5 | ||
|
|
087495f5a0 | ||
|
|
b94ec24948 | ||
|
|
e3257e025d | ||
|
|
2640a509f9 | ||
|
|
e2eb957af9 | ||
|
|
81f07cb8a3 | ||
|
|
1e3cb2e66c | ||
|
|
76da5b0cac | ||
|
|
fdc1c32287 | ||
|
|
3b38547fc9 | ||
|
|
53d1716255 | ||
|
|
0606a00942 | ||
|
|
af3c645521 | ||
|
|
e5093c6d2f | ||
|
|
59102b0e08 | ||
|
|
17cff99c2b | ||
|
|
7d0dbff8bc | ||
|
|
6974e2de27 | ||
|
|
770ed4524a | ||
|
|
7f4dc85554 | ||
|
|
005fa3d732 | ||
|
|
bbe8d79400 | ||
|
|
edaab9d7de | ||
|
|
842c6e681d | ||
|
|
05be8406cc | ||
|
|
7aa879fb8e | ||
|
|
1e6a9b8c26 | ||
|
|
b2e9c88f62 | ||
|
|
3609ff9eff | ||
|
|
09fc3ae60c | ||
|
|
948c13dd15 | ||
|
|
b8f7d08fae | ||
|
|
576b7e2dd3 | ||
|
|
024b2e1a01 | ||
|
|
daf4b9d974 | ||
|
|
36b3431975 | ||
|
|
3ded3926f4 | ||
|
|
69502dd97b | ||
|
|
234304f9ec | ||
|
|
193a1cc255 | ||
|
|
d0b569a07f | ||
|
|
b17edd31ac | ||
|
|
891f61701c | ||
|
|
e35f5098b9 | ||
|
|
55da7435f8 | ||
|
|
f0a561b1ee | ||
|
|
bbd48d1033 | ||
|
|
847013c6c7 | ||
|
|
61aa144631 | ||
|
|
dd87f74eb4 | ||
|
|
95dbc5333e | ||
|
|
0d2114416c | ||
|
|
215808b011 | ||
|
|
1ced8bba7d | ||
|
|
468ff7fc50 | ||
|
|
0eed2c67d1 | ||
|
|
4168ef0b0d | ||
|
|
bf59d932f4 | ||
|
|
765b0e4625 | ||
|
|
16058bbea2 | ||
|
|
f47f95cd2e | ||
|
|
7f0f62abb6 | ||
|
|
22627b57f0 | ||
|
|
5a81cace02 | ||
|
|
3f172e6872 | ||
|
|
52dcdaa47d | ||
|
|
d4476cbd2b | ||
|
|
d2c4ab28c2 | ||
|
|
23d67ab0cf | ||
|
|
3f55b9f503 | ||
|
|
e222444809 | ||
|
|
83ed648966 | ||
|
|
a5c1d88460 | ||
|
|
fee83e0db4 | ||
|
|
dadf1b12d3 | ||
|
|
5ff8298b62 | ||
|
|
44e181981d |
119
README.md
Normal file
119
README.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Forge
|
||||
|
||||
Gitlab repo is found [here](https://git.cardforge.org/core-developers/forge).
|
||||
|
||||
Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated)
|
||||
|
||||
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
|
||||
|
||||
# Requirements / Tools
|
||||
|
||||
- Java IDE such as IntelliJ or Eclipse
|
||||
- Git
|
||||
- Maven
|
||||
- Gitlab account
|
||||
- Libgdx (optional: familiarity with this library is helpful for mobile platform development)
|
||||
- Android SDK (optional: for Android releases)
|
||||
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
|
||||
|
||||
# Project Quick Setup
|
||||
|
||||
- Log in to gitlab with your user account and fork the project.
|
||||
|
||||
- Clone your forked project to your local machine
|
||||
|
||||
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
||||
|
||||
# Eclipse
|
||||
|
||||
## Project Setup
|
||||
|
||||
- Follow the instructions for cloning from Gitlab. You'll need a Gitlab account setup and an SSH key defined. If you are on a
|
||||
Windows machine you can use Putty with TortoiseGit. Run puttygen.exe to generate the key -- save the private key and export
|
||||
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your Gitlab profile under
|
||||
"SSH keys".
|
||||
|
||||
Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing Gitlab.
|
||||
|
||||
- Fork the Forge git repo to your Gitlab account.
|
||||
|
||||
- Clone your forked repo to your local machine.
|
||||
|
||||
- Make sure the Java SDK is installed -- not just the JRE. Java 8 or newer required. At the time of this writing, JDK 11 works as expected.
|
||||
|
||||
- You need maven to load in dependencies and build. Obtain that [from here](https://maven.apache.org/download.cgi). Execute the following from the root repo dir to download dependencies, etc:
|
||||
|
||||
`mvn -U -B clean -P windows-linux install`
|
||||
|
||||
For the desktop, this will create a populated directory at `forge-gui-desktop/target/forge-gui-desktop-<release-name>` containing typical release files such as the jar, Windows executable, resource files, etc.
|
||||
|
||||
- Install Eclipse for Java. Launch it. At the time of this writing, Eclipse 2018-12 works as expected. YMMV for other versions.
|
||||
|
||||
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > General > Existing Projects into Workspace > Navigate to local forge repo >
|
||||
Check "Search for nested projects" > Uncheck 'forge', check the rest > Finish.
|
||||
|
||||
- Let Eclipse run through building the project.
|
||||
|
||||
## Project Launch
|
||||
|
||||
### Desktop
|
||||
|
||||
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Proceed
|
||||
|
||||
### Mobile (Desktop dev)
|
||||
|
||||
- Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Proceed
|
||||
|
||||
# IntelliJ
|
||||
|
||||
TBD
|
||||
|
||||
# General Notes
|
||||
|
||||
## Project Hierarchy
|
||||
|
||||
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
||||
|
||||
- forge-ai
|
||||
- forge-core
|
||||
- forge-game
|
||||
- forge-gui
|
||||
|
||||
The platform-specific projects are:
|
||||
|
||||
- forge-gui-android
|
||||
- forge-gui-desktop
|
||||
- forge-gui-ios
|
||||
- forge-gui-mobile
|
||||
- forge-gui-mobile-dev
|
||||
|
||||
### forge-ai
|
||||
|
||||
### forge-core
|
||||
|
||||
### forge-game
|
||||
|
||||
### forge-gui
|
||||
|
||||
### forge-gui-android
|
||||
|
||||
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
### forge-gui-desktop
|
||||
|
||||
Java Swing based GUI targeting desktop machines.
|
||||
|
||||
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
|
||||
|
||||
### forge-gui-ios
|
||||
|
||||
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
### forge-gui-mobile
|
||||
|
||||
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
|
||||
|
||||
### forge-gui-mobile-dev
|
||||
|
||||
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-ai</artifactId>
|
||||
|
||||
@@ -1211,7 +1211,7 @@ public class AiController {
|
||||
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
ApiType api = sa.getApi();
|
||||
|
||||
// Abilities without api may also use this routine, However they should provide a unique mode value
|
||||
// Abilities without api may also use this routine, However they should provide a unique mode value ?? How could this work?
|
||||
if (api == null) {
|
||||
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
|
||||
mode);
|
||||
|
||||
@@ -821,6 +821,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (sVar.equals("Count$xPaid")) {
|
||||
c = AbilityUtils.calculateAmount(source, "PayX", null);
|
||||
} else {
|
||||
c = AbilityUtils.calculateAmount(source, amount, ability);
|
||||
}
|
||||
|
||||
@@ -2847,7 +2847,7 @@ public class ComputerUtil {
|
||||
repParams.put("Source", source);
|
||||
|
||||
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
|
||||
ReplacementLayer.None);
|
||||
ReplacementLayer.Other);
|
||||
|
||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
||||
return false;
|
||||
@@ -2878,7 +2878,7 @@ public class ComputerUtil {
|
||||
repParams.put("Source", source);
|
||||
|
||||
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
|
||||
ReplacementLayer.None);
|
||||
ReplacementLayer.Other);
|
||||
|
||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
||||
// no life gain is not negative
|
||||
|
||||
@@ -95,9 +95,15 @@ public class ComputerUtilAbility {
|
||||
|
||||
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
|
||||
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
||||
for (SpellAbility sa : originList) {
|
||||
sa.setActivatingPlayer(player);
|
||||
|
||||
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
|
||||
for (SpellAbility sa : originList) {
|
||||
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
|
||||
sa.setActivatingPlayer(player);
|
||||
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
|
||||
}
|
||||
|
||||
for (SpellAbility sa : originListWithAddCosts) {
|
||||
// determine which alternative costs are cheaper than the original and prioritize them
|
||||
List<SpellAbility> saAltCosts = GameActionUtil.getAlternativeCosts(sa, player);
|
||||
List<SpellAbility> priorityAltSa = Lists.newArrayList();
|
||||
|
||||
@@ -2590,7 +2590,7 @@ public class ComputerUtilCombat {
|
||||
// repParams.put("PreventMap", preventMap);
|
||||
|
||||
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams,
|
||||
ReplacementLayer.None);
|
||||
ReplacementLayer.Other);
|
||||
|
||||
return !list.isEmpty();
|
||||
}
|
||||
|
||||
@@ -162,10 +162,36 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
|
||||
FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title,
|
||||
FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
|
||||
Player targetedPlayer) {
|
||||
// this isn't used
|
||||
return null;
|
||||
if (delayedReveal != null) {
|
||||
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
|
||||
}
|
||||
FCollection<T> remaining = new FCollection<T>(optionList);
|
||||
List<T> selecteds = new ArrayList<T>();
|
||||
T selected;
|
||||
do {
|
||||
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
|
||||
if ( selected != null ) {
|
||||
remaining.remove(selected);
|
||||
selecteds.add(selected);
|
||||
}
|
||||
} while ( (selected != null ) && (selecteds.size() < max) );
|
||||
return selecteds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2,
|
||||
boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
|
||||
if (delayedReveal != null) {
|
||||
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
|
||||
}
|
||||
T selected1 = chooseSingleEntityForEffect(optionList1, null, sa, title, optional, targetedPlayer);
|
||||
T selected2 = chooseSingleEntityForEffect(optionList2, null, sa, title, optional || selected1!=null, targetedPlayer);
|
||||
List<T> selecteds = new ArrayList<T>();
|
||||
if ( selected1 != null ) { selecteds.add(selected1); }
|
||||
if ( selected2 != null ) { selecteds.add(selected2); }
|
||||
return selecteds;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1090,7 +1116,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public List<Card> chooseCardsForZoneChange(
|
||||
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList,
|
||||
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max,
|
||||
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
|
||||
// this isn't used
|
||||
return null;
|
||||
@@ -1164,4 +1190,10 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
return chosenOptCosts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmMulliganScry(Player p) {
|
||||
// Always true?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPartMana;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.keyword.Keyword;
|
||||
@@ -286,6 +287,18 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
if ("XCountersDamage".equals(logic) && sa.getPayCosts() != null) {
|
||||
// Check to ensure that we have enough counters to remove per the defined PayX
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(source.getSVar("PayX"))) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1080,7 +1093,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
continue; // not a toughness debuff
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNumeric(dmgDef) && ab.canPlay()) { // currently doesn't work for X and other dependent costs
|
||||
if (StringUtils.isNumeric(dmgDef)) { // currently doesn't work for X and other dependent costs
|
||||
if (sa.usesTargeting() && ab.usesTargeting()) {
|
||||
// Ensure that the chained spell can target at least the same things (or more) as the current one
|
||||
TargetRestrictions tgtSa = sa.getTargetRestrictions();
|
||||
|
||||
@@ -113,7 +113,7 @@ public class ManifestAi extends SpellAbilityAi {
|
||||
repParams.put("Origin", ZoneType.Library);
|
||||
repParams.put("Destination", ZoneType.Battlefield);
|
||||
repParams.put("Source", sa.getHostCard());
|
||||
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.None);
|
||||
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.Other);
|
||||
if (!list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,8 @@ public class GameCopier {
|
||||
newCard.setManifested(true);
|
||||
// TODO: Should be able to copy other abilities...
|
||||
if (isCreature && hasManaCost) {
|
||||
newCard.addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
|
||||
newCard.getState(CardStateName.Original).addSpellAbility(
|
||||
CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-core</artifactId>
|
||||
|
||||
@@ -20,6 +20,8 @@ package forge.deck;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.card.CardRules;
|
||||
import forge.card.CardRulesPredicates;
|
||||
@@ -47,7 +49,7 @@ public enum DeckFormat {
|
||||
QuestDeck ( Range.between(40, Integer.MAX_VALUE), Range.between(0, 15), 4),
|
||||
Limited ( Range.between(40, Integer.MAX_VALUE), null, Integer.MAX_VALUE),
|
||||
Commander ( Range.is(99), Range.between(0, 10), 1, new Predicate<CardRules>() {
|
||||
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
|
||||
private final Set<String> bannedCards = ImmutableSet.of(
|
||||
"Adriana's Valor", "Advantageous Proclamation", "Amulet of Quoz", "Ancestral Recall", "Assemble the Rank and Vile",
|
||||
"Backup Plan", "Balance", "Biorhythm", "Black Lotus", "Brago's Favor", "Braids, Cabal Minion", "Bronze Tablet",
|
||||
"Channel", "Chaos Orb", "Coalition Victory", "Contract from Below", "Darkpact", "Demonic Attorney", "Double Stroke",
|
||||
@@ -59,7 +61,7 @@ public enum DeckFormat {
|
||||
"Rebirth", "Recurring Nightmare", "Rofellos, Llanowar Emissary", "Secret Summoning", "Secrets of Paradise",
|
||||
"Sentinel Dispatch", "Shahrazad", "Sovereign's Realm", "Summoner's Bond", "Sundering Titan", "Sway of the Stars",
|
||||
"Sylvan Primordial", "Tempest Efreet", "Time Vault", "Time Walk", "Timmerian Fiends", "Tinker", "Tolarian Academy",
|
||||
"Trade Secrets", "Unexpected Potential", "Upheaval", "Weight Advantage", "Worldfire", "Worldknit", "Yawgmoth's Bargain"));
|
||||
"Trade Secrets", "Unexpected Potential", "Upheaval", "Weight Advantage", "Worldfire", "Worldknit", "Yawgmoth's Bargain");
|
||||
@Override
|
||||
public boolean apply(CardRules rules) {
|
||||
if (bannedCards.contains(rules.getName())) {
|
||||
@@ -70,8 +72,8 @@ public enum DeckFormat {
|
||||
}),
|
||||
Pauper ( Range.is(60), Range.between(0, 10), 1),
|
||||
Brawl ( Range.is(59), Range.between(0, 15), 1, null, new Predicate<PaperCard>() {
|
||||
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
|
||||
"Baral, Chief of Compliance","Smuggler's Copter","Sorcerous Spyglass"));
|
||||
private final Set<String> bannedCards = ImmutableSet.of(
|
||||
"Baral, Chief of Compliance","Smuggler's Copter","Sorcerous Spyglass");
|
||||
@Override
|
||||
public boolean apply(PaperCard card) {
|
||||
//why do we need to hard code the bannings here - they are defined in the GameFormat predicate used below
|
||||
@@ -81,7 +83,7 @@ public enum DeckFormat {
|
||||
return StaticData.instance() == null ? false : StaticData.instance().getBrawlPredicate().apply(card);
|
||||
}
|
||||
}) {
|
||||
private final ImmutableSet<String> bannedCommanders = ImmutableSet.of("Baral, Chief of Compliance");
|
||||
private final Set<String> bannedCommanders = ImmutableSet.of("Baral, Chief of Compliance");
|
||||
|
||||
@Override
|
||||
public boolean isLegalCommander(CardRules rules) {
|
||||
@@ -89,11 +91,11 @@ public enum DeckFormat {
|
||||
}
|
||||
},
|
||||
TinyLeaders ( Range.is(49), Range.between(0, 10), 1, new Predicate<CardRules>() {
|
||||
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
|
||||
private final Set<String> bannedCards = ImmutableSet.of(
|
||||
"Ancestral Recall", "Balance", "Black Lotus", "Black Vise", "Channel", "Chaos Orb", "Contract From Below", "Counterbalance", "Darkpact", "Demonic Attorney", "Demonic Tutor", "Earthcraft", "Edric, Spymaster of Trest", "Falling Star",
|
||||
"Fastbond", "Flash", "Goblin Recruiter", "Grindstone", "Hermit Druid", "Imperial Seal", "Jeweled Bird", "Karakas", "Library of Alexandria", "Mana Crypt", "Mana Drain", "Mana Vault", "Metalworker", "Mind Twist", "Mishra's Workshop",
|
||||
"Mox Emerald", "Mox Jet", "Mox Pearl", "Mox Ruby", "Mox Sapphire", "Necropotence", "Shahrazad", "Skullclamp", "Sol Ring", "Strip Mine", "Survival of the Fittest", "Sword of Body and Mind", "Time Vault", "Time Walk", "Timetwister",
|
||||
"Timmerian Fiends", "Tolarian Academy", "Umezawa's Jitte", "Vampiric Tutor", "Wheel of Fortune", "Yawgmoth's Will"));
|
||||
"Timmerian Fiends", "Tolarian Academy", "Umezawa's Jitte", "Vampiric Tutor", "Wheel of Fortune", "Yawgmoth's Will");
|
||||
|
||||
@Override
|
||||
public boolean apply(CardRules rules) {
|
||||
@@ -112,7 +114,7 @@ public enum DeckFormat {
|
||||
return true;
|
||||
}
|
||||
}) {
|
||||
private final ImmutableSet<String> bannedCommanders = ImmutableSet.of("Derevi, Empyrial Tactician", "Erayo, Soratami Ascendant", "Rofellos, Llanowar Emissary");
|
||||
private final Set<String> bannedCommanders = ImmutableSet.of("Derevi, Empyrial Tactician", "Erayo, Soratami Ascendant", "Rofellos, Llanowar Emissary");
|
||||
|
||||
@Override
|
||||
public boolean isLegalCommander(CardRules rules) {
|
||||
@@ -141,13 +143,6 @@ public enum DeckFormat {
|
||||
private final static String ADVPROCLAMATION = "Advantageous Proclamation";
|
||||
private final static String SOVREALM = "Sovereign's Realm";
|
||||
|
||||
private static final List<String> limitExceptions = Arrays.asList(
|
||||
new String[]{"Relentless Rats", "Shadowborn Apostle", "Rat Colony"});
|
||||
|
||||
public static List<String> getLimitExceptions(){
|
||||
return limitExceptions;
|
||||
}
|
||||
|
||||
private DeckFormat(Range<Integer> mainRange0, Range<Integer> sideRange0, int maxCardCopies0, Predicate<CardRules> cardPoolFilter0, Predicate<PaperCard> paperCardPoolFilter0) {
|
||||
mainRange = mainRange0;
|
||||
sideRange = sideRange0;
|
||||
@@ -342,7 +337,6 @@ public enum DeckFormat {
|
||||
//basic lands, Shadowborn Apostle, Relentless Rats and Rat Colony
|
||||
|
||||
final CardPool allCards = deck.getAllCardsInASinglePool(hasCommander());
|
||||
final ImmutableSet<String> limitExceptions = ImmutableSet.of("Relentless Rats", "Shadowborn Apostle", "Rat Colony");
|
||||
|
||||
// should group all cards by name, so that different editions of same card are really counted as the same card
|
||||
for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, PaperCard.FN_GET_NAME)) {
|
||||
@@ -351,8 +345,7 @@ public enum DeckFormat {
|
||||
return TextUtil.concatWithSpace("contains the nonexisting card", cp.getKey());
|
||||
}
|
||||
|
||||
final boolean canHaveMultiple = simpleCard.getRules().getType().isBasicLand() || limitExceptions.contains(cp.getKey());
|
||||
if (!canHaveMultiple && cp.getValue() > maxCopies) {
|
||||
if (!canHaveAnyNumberOf(simpleCard) && cp.getValue() > maxCopies) {
|
||||
return TextUtil.concatWithSpace("must not contain more than", String.valueOf(maxCopies), "copies of the card", cp.getKey());
|
||||
}
|
||||
}
|
||||
@@ -370,6 +363,12 @@ public enum DeckFormat {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean canHaveAnyNumberOf(final IPaperCard icard) {
|
||||
return icard.getRules().getType().isBasicLand()
|
||||
|| Iterables.contains(icard.getRules().getMainPart().getKeywords(),
|
||||
"A deck can have any number of cards named CARDNAME.");
|
||||
}
|
||||
|
||||
public static String getPlaneSectionConformanceProblem(final CardPool planes) {
|
||||
//Must contain at least 10 planes/phenomenons, but max 2 phenomenons. Singleton.
|
||||
if (planes == null || planes.countAll() < 10) {
|
||||
|
||||
@@ -102,6 +102,7 @@ public class DeckRecognizer {
|
||||
// Pattern.compile("(.*)[^A-Za-wyz]*\\s+([\\d]{1,2})");
|
||||
private static final Pattern SEARCH_NUMBERS_IN_FRONT = Pattern.compile("([\\d]{1,2})[^A-Za-wyz]*\\s+(.*)");
|
||||
//private static final Pattern READ_SEPARATED_EDITION = Pattern.compile("[[\\(\\{]([a-zA-Z0-9]){1,3})[]*\\s+(.*)");
|
||||
private static final Pattern SEARCH_SINGLE_SLASH = Pattern.compile("(?<=[^/])\\s*/\\s*(?=[^/])");
|
||||
|
||||
private final SetPreference useLastSet;
|
||||
private final ICardDatabase db;
|
||||
@@ -125,7 +126,10 @@ public class DeckRecognizer {
|
||||
return new Token(TokenType.Comment, 0, rawLine);
|
||||
}
|
||||
final char smartQuote = (char) 8217;
|
||||
final String line = rawLine.trim().replace(smartQuote, '\'');
|
||||
String line = rawLine.trim().replace(smartQuote, '\'');
|
||||
|
||||
// Some websites export split card names with a single slash. Replace with double slash.
|
||||
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
|
||||
|
||||
Token result = null;
|
||||
final Matcher foundNumbersInFront = DeckRecognizer.SEARCH_NUMBERS_IN_FRONT.matcher(line);
|
||||
|
||||
@@ -100,8 +100,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
build.add(edition.getCode());
|
||||
}
|
||||
|
||||
// Should future image file names be all lower case? Instead of Up case sets?
|
||||
return StringUtils.join(build, "_").toLowerCase();
|
||||
return StringUtils.join(build, "_").replace('*', 'x').toLowerCase();
|
||||
}
|
||||
|
||||
public PaperToken(final CardRules c) { this(c, null, null); }
|
||||
|
||||
@@ -86,7 +86,7 @@ public class Localizer {
|
||||
resourceBundle = ResourceBundle.getBundle(languageRegionID, new Locale(splitLocale[0], splitLocale[1]), loader);
|
||||
} catch (NullPointerException | MissingResourceException e) {
|
||||
//If the language can't be loaded, default to US English
|
||||
resourceBundle = ResourceBundle.getBundle("en-GB", new Locale("en", "GB"), loader);
|
||||
resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-game</artifactId>
|
||||
|
||||
@@ -29,6 +29,7 @@ import forge.game.ability.effects.AttachEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.event.*;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.keyword.KeywordsChange;
|
||||
import forge.game.player.GameLossReason;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
@@ -50,9 +51,9 @@ import forge.util.collect.FCollection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import forge.util.maps.HashMapOfLists;
|
||||
import forge.util.maps.MapOfLists;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* Methods for common actions performed during a game.
|
||||
@@ -70,10 +71,8 @@ public class GameAction {
|
||||
}
|
||||
|
||||
public final void resetActivationsPerTurn() {
|
||||
final CardCollectionView all = game.getCardsInGame();
|
||||
|
||||
// Reset Activations per Turn
|
||||
for (final Card card : all) {
|
||||
for (final Card card : game.getCardsInGame()) {
|
||||
for (final SpellAbility sa : card.getAllSpellAbilities()) {
|
||||
sa.getRestrictions().resetTurnActivations();
|
||||
}
|
||||
@@ -104,6 +103,7 @@ public class GameAction {
|
||||
boolean toBattlefield = zoneTo.is(ZoneType.Battlefield);
|
||||
boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield);
|
||||
boolean toHand = zoneTo.is(ZoneType.Hand);
|
||||
boolean wasFacedown = c.isFaceDown();
|
||||
|
||||
//Rule 110.5g: A token that has left the battlefield can't move to another zone
|
||||
if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Command)) {
|
||||
@@ -150,13 +150,15 @@ public class GameAction {
|
||||
// Cards returned from exile face-down must be reset to their original state, otherwise
|
||||
// all sort of funky shenanigans may happen later (e.g. their ETB replacement effects are set
|
||||
// up on the wrong card state etc.).
|
||||
if (c.isFaceDown() && (fromBattlefield || (toHand && zoneFrom.is(ZoneType.Exile)))) {
|
||||
if (wasFacedown && (fromBattlefield || (toHand && zoneFrom.is(ZoneType.Exile)))) {
|
||||
c.setState(CardStateName.Original, true);
|
||||
c.runFaceupCommands();
|
||||
}
|
||||
|
||||
// Clean up the temporary Dash SVar when the Dashed card leaves the battlefield
|
||||
if (fromBattlefield && c.getSVar("EndOfTurnLeavePlay").equals("Dash")) {
|
||||
// Clean up the temporary AtEOT SVar
|
||||
String endofTurn = c.getSVar("EndOfTurnLeavePlay");
|
||||
if (fromBattlefield && (endofTurn.equals("Dash") || endofTurn.equals("AtEOT"))) {
|
||||
c.removeSVar("EndOfTurnLeavePlay");
|
||||
}
|
||||
|
||||
@@ -291,6 +293,33 @@ public class GameAction {
|
||||
copied.getOwner().addInboundToken(copied);
|
||||
}
|
||||
|
||||
if (toBattlefield) {
|
||||
// HACK for making the RIOT enchantment look into the Future
|
||||
// need to check the Keywords what it would have on the Battlefield
|
||||
Card riotLKI = CardUtil.getLKICopy(copied);
|
||||
riotLKI.setLastKnownZone(zoneTo);
|
||||
CardCollection preList = new CardCollection(riotLKI);
|
||||
checkStaticAbilities(false, Sets.newHashSet(riotLKI), preList);
|
||||
|
||||
List<Long> changedTimeStamps = Lists.newArrayList();
|
||||
for(Map.Entry<Long, KeywordsChange> e : riotLKI.getChangedCardKeywords().entrySet()) {
|
||||
if (!copied.hasChangedCardKeywords(e.getKey())) {
|
||||
KeywordsChange o = e.getValue();
|
||||
o.setHostCard(copied);
|
||||
for (KeywordInterface k : o.getKeywords()) {
|
||||
for (ReplacementEffect re : k.getReplacements()) {
|
||||
// this param need to be set, otherwise in ReplaceMoved it fails
|
||||
re.getMapParams().put("BypassEtbCheck", "True");
|
||||
}
|
||||
}
|
||||
copied.addChangedCardKeywordsInternal(o, e.getKey());
|
||||
changedTimeStamps.add(e.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
checkStaticAbilities(false);
|
||||
}
|
||||
|
||||
Map<String, Object> repParams = Maps.newHashMap();
|
||||
repParams.put("Event", "Moved");
|
||||
repParams.put("Affected", copied);
|
||||
@@ -435,10 +464,14 @@ public class GameAction {
|
||||
}
|
||||
|
||||
// rule 504.6: reveal a face-down card leaving the stack
|
||||
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && c.isFaceDown()) {
|
||||
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && wasFacedown) {
|
||||
// FIXME: tracker freeze-unfreeze is needed here to avoid a bug with the card staying face down in the View for the reveal
|
||||
boolean trackerFrozen = game.getTracker().isFrozen();
|
||||
game.getTracker().unfreeze();
|
||||
c.setState(CardStateName.Original, true);
|
||||
reveal(new CardCollection(c), c.getOwner(), true, "Face-down card moves from the stack: ");
|
||||
c.setState(CardStateName.FaceDown, true);
|
||||
if (trackerFrozen) { game.getTracker().freeze(); }
|
||||
}
|
||||
|
||||
if (fromBattlefield) {
|
||||
@@ -466,19 +499,19 @@ public class GameAction {
|
||||
changeZone(null, zoneTo, unmeld, position, cause, params);
|
||||
}
|
||||
// Reveal if face-down
|
||||
if (c.isFaceDown()) {
|
||||
if (wasFacedown) {
|
||||
// FIXME: tracker freeze-unfreeze is needed here to avoid a bug with the card staying face down in the View for the reveal
|
||||
boolean trackerFrozen = game.getTracker().isFrozen();
|
||||
game.getTracker().unfreeze();
|
||||
c.setState(CardStateName.Original, true);
|
||||
reveal(new CardCollection(c), c.getOwner(), true, "Face-down card leaves the battlefield: ");
|
||||
c.setState(CardStateName.FaceDown, true);
|
||||
if (trackerFrozen) { game.getTracker().freeze(); }
|
||||
copied.setState(CardStateName.Original, true);
|
||||
}
|
||||
unattachCardLeavingBattlefield(copied);
|
||||
// Remove all changed keywords
|
||||
copied.removeAllChangedText(game.getNextTimestamp());
|
||||
// reset activations
|
||||
for (SpellAbility ab : copied.getSpellAbilities()) {
|
||||
ab.getRestrictions().resetTurnActivations();
|
||||
}
|
||||
} else if (toBattlefield) {
|
||||
// reset timestamp in changezone effects so they have same timestamp if ETB simutaneously
|
||||
copied.setTimestamp(game.getNextTimestamp());
|
||||
@@ -1094,7 +1127,7 @@ public class GameAction {
|
||||
|
||||
if (c.isAttachedToEntity()) {
|
||||
final GameEntity ge = c.getEntityAttachedTo();
|
||||
if (!ge.canBeAttached(c)) {
|
||||
if (!ge.canBeAttached(c, true)) {
|
||||
c.unattachFromEntity(ge);
|
||||
checkAgain = true;
|
||||
}
|
||||
@@ -1669,12 +1702,8 @@ public class GameAction {
|
||||
|
||||
// rule 103.4b
|
||||
boolean isMultiPlayer = game.getPlayers().size() > 2;
|
||||
int mulliganDelta = isMultiPlayer ? 0 : 1;
|
||||
|
||||
// https://magic.wizards.com/en/articles/archive/feature/checking-brawl-2018-07-09
|
||||
if (game.getRules().hasAppliedVariant(GameType.Brawl) && !isMultiPlayer){
|
||||
mulliganDelta = 0;
|
||||
}
|
||||
int mulliganDelta = isMultiPlayer || game.getRules().hasAppliedVariant(GameType.Brawl) ? 0 : 1;
|
||||
|
||||
boolean allKept;
|
||||
do {
|
||||
@@ -1690,32 +1719,17 @@ public class GameAction {
|
||||
}
|
||||
|
||||
if (toMulligan != null && !toMulligan.isEmpty()) {
|
||||
if (!isCommander) {
|
||||
toMulligan = new CardCollection(p.getCardsIn(ZoneType.Hand));
|
||||
for (final Card c : toMulligan) {
|
||||
moveToLibrary(c, null, null);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
p.shuffle(null);
|
||||
p.drawCards(handSize[i] - mulliganDelta);
|
||||
} else {
|
||||
List<Card> toExile = Lists.newArrayList(toMulligan);
|
||||
for (Card c : toExile) {
|
||||
exile(c, null, null);
|
||||
}
|
||||
exiledDuringMulligans.addAll(p, toExile);
|
||||
try {
|
||||
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
p.drawCards(toExile.size() - 1);
|
||||
toMulligan = new CardCollection(p.getCardsIn(ZoneType.Hand));
|
||||
for (final Card c : toMulligan) {
|
||||
moveToLibrary(c, null, null);
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
p.shuffle(null);
|
||||
p.drawCards(handSize[i] - mulliganDelta);
|
||||
p.onMulliganned();
|
||||
allKept = false;
|
||||
} else {
|
||||
@@ -1726,21 +1740,17 @@ public class GameAction {
|
||||
mulliganDelta++;
|
||||
} while (!allKept);
|
||||
|
||||
if (isCommander) {
|
||||
for (Entry<Player, Collection<Card>> kv : exiledDuringMulligans.entrySet()) {
|
||||
Player p = kv.getKey();
|
||||
Collection<Card> cc = kv.getValue();
|
||||
for (Card c : cc) {
|
||||
moveToLibrary(c, null, null);
|
||||
}
|
||||
p.shuffle(null);
|
||||
//Vancouver Mulligan as a scry with the decisions inside
|
||||
List<Player> scryers = Lists.newArrayList();
|
||||
for(Player p : whoCanMulligan) {
|
||||
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
|
||||
scryers.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
//Vancouver Mulligan
|
||||
for(Player p : whoCanMulligan) {
|
||||
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
|
||||
p.scry(1, null);
|
||||
for(Player p : scryers) {
|
||||
if (p.getController().confirmMulliganScry(p)) {
|
||||
scry(ImmutableList.of(p), 1, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1838,4 +1848,68 @@ public class GameAction {
|
||||
runParams.put("Player", p);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.BecomeMonarch, runParams, false);
|
||||
}
|
||||
|
||||
// Make scry an action function so that it can be used for mulligans (with a null cause)
|
||||
// Assumes that the list of players is in APNAP order, which should be the case
|
||||
// Optional here as well to handle the way that mulligans do the choice
|
||||
// 701.17. Scry
|
||||
// 701.17a To “scry N” means to look at the top N cards of your library, then put any number of them
|
||||
// on the bottom of your library in any order and the rest on top of your library in any order.
|
||||
// 701.17b If a player is instructed to scry 0, no scry event occurs. Abilities that trigger whenever a
|
||||
// player scries won’t trigger.
|
||||
// 701.17c If multiple players scry at once, each of those players looks at the top cards of their library
|
||||
// at the same time. Those players decide in APNAP order (see rule 101.4) where to put those
|
||||
// cards, then those cards move at the same time.
|
||||
public void scry(List<Player> players, int numScry, SpellAbility cause) {
|
||||
if (numScry == 0) {
|
||||
return;
|
||||
}
|
||||
// reveal the top N library cards to the player (only)
|
||||
// no real need to separate out the look if
|
||||
// there is only one player scrying
|
||||
if (players.size() > 1) {
|
||||
for (final Player p : players) {
|
||||
final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, numScry));
|
||||
revealTo(topN, p);
|
||||
}
|
||||
}
|
||||
// make the decisions
|
||||
List<ImmutablePair<CardCollection, CardCollection>> decisions = Lists.newArrayList();
|
||||
for (final Player p : players) {
|
||||
final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, numScry));
|
||||
ImmutablePair<CardCollection, CardCollection> decision = p.getController().arrangeForScry(topN);
|
||||
decisions.add(decision);
|
||||
int numToTop = decision.getLeft() == null ? 0 : decision.getLeft().size();
|
||||
int numToBottom = decision.getRight() == null ? 0 : decision.getRight().size();
|
||||
|
||||
// publicize the decision
|
||||
game.fireEvent(new GameEventScry(p, numToTop, numToBottom));
|
||||
}
|
||||
// do the moves after all the decisions (maybe not necesssary, but let's
|
||||
// do it the official way)
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
// no good iterate simultaneously in Java
|
||||
final Player p = players.get(i);
|
||||
final CardCollection toTop = decisions.get(i).getLeft();
|
||||
final CardCollection toBottom = decisions.get(i).getRight();
|
||||
if (toTop != null) {
|
||||
Collections.reverse(toTop); // reverse to get the correct order
|
||||
for (Card c : toTop) {
|
||||
moveToLibrary(c, cause, null);
|
||||
}
|
||||
}
|
||||
if (toBottom != null) {
|
||||
for (Card c : toBottom) {
|
||||
moveToBottomOfLibrary(c, cause, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (cause != null) {
|
||||
// set up triggers (but not actually do them until later)
|
||||
final Map<String, Object> runParams = Maps.newHashMap();
|
||||
runParams.put("Player", p);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ import forge.game.cost.Cost;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.FileSection;
|
||||
import io.sentry.Sentry;
|
||||
import io.sentry.event.BreadcrumbBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -130,7 +132,16 @@ public final class AbilityFactory {
|
||||
String source = state.getName().isEmpty() ? abString : state.getName();
|
||||
throw new RuntimeException("AbilityFactory : getAbility -- no API in " + source + ": " + abString);
|
||||
}
|
||||
return getAbility(mapParams, type, state, parent);
|
||||
try {
|
||||
return getAbility(mapParams, type, state, parent);
|
||||
} catch (Error | Exception ex) {
|
||||
String msg = "AbilityFactory:getAbility: crash when trying to create ability ";
|
||||
Sentry.getContext().recordBreadcrumb(
|
||||
new BreadcrumbBuilder().setMessage(msg)
|
||||
.withData("Card", state.getName()).withData("Ability", abString).build()
|
||||
);
|
||||
throw new RuntimeException(msg + " of card: " + state.getName(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static final SpellAbility getAbility(final Card hostCard, final String svar) {
|
||||
|
||||
@@ -225,10 +225,6 @@ public class AbilityUtils {
|
||||
if (o != null && o instanceof Card) {
|
||||
cards.add(game.getCardState((Card) o));
|
||||
}
|
||||
} else if (defined.equals("Clones")) {
|
||||
for (final Card clone : hostCard.getClones()) {
|
||||
cards.add(game.getCardState(clone));
|
||||
}
|
||||
} else if (defined.equals("Imprinted")) {
|
||||
for (final Card imprint : hostCard.getImprintedCards()) {
|
||||
cards.add(game.getCardState(imprint));
|
||||
|
||||
@@ -230,8 +230,17 @@ public abstract class SpellAbilityEffect {
|
||||
|
||||
if (desc.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(location).append(" ");
|
||||
if (location.equals("Hand")) {
|
||||
sb.append("Return ");
|
||||
} else if (location.equals("SacrificeCtrl")) {
|
||||
sb.append("Its controller sacrifices ");
|
||||
} else {
|
||||
sb.append(location).append(" ");
|
||||
}
|
||||
sb.append(Lang.joinHomogenous(crds));
|
||||
if (location.equals("Hand")) {
|
||||
sb.append("to your hand").append(" ");
|
||||
}
|
||||
sb.append(" at the ");
|
||||
if (combat) {
|
||||
sb.append("end of combat.");
|
||||
@@ -255,9 +264,18 @@ public abstract class SpellAbilityEffect {
|
||||
final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), sa.getHostCard(), intrinsic);
|
||||
for (final Card c : crds) {
|
||||
trig.addRemembered(c);
|
||||
|
||||
// Svar for AI
|
||||
if (!c.hasSVar("EndOfTurnLeavePlay")) {
|
||||
c.setSVar("EndOfTurnLeavePlay", "AtEOT");
|
||||
}
|
||||
}
|
||||
String trigSA = "";
|
||||
if (location.equals("Sacrifice")) {
|
||||
if (location.equals("Hand")) {
|
||||
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ Battlefield | Destination$ Hand";
|
||||
} else if (location.equals("SacrificeCtrl")) {
|
||||
trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRemembered";
|
||||
} else if (location.equals("Sacrifice")) {
|
||||
trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRemembered | Controller$ You";
|
||||
} else if (location.equals("Exile")) {
|
||||
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ Battlefield | Destination$ Exile";
|
||||
@@ -289,6 +307,11 @@ public abstract class SpellAbilityEffect {
|
||||
}
|
||||
trig.setOverridingAbility(AbilityFactory.getAbility(trigSA, card));
|
||||
card.addTrigger(trig);
|
||||
|
||||
// Svar for AI
|
||||
if (!card.hasSVar("EndOfTurnLeavePlay")) {
|
||||
card.setSVar("EndOfTurnLeavePlay", "AtEOT");
|
||||
}
|
||||
}
|
||||
|
||||
protected static void addForgetOnMovedTrigger(final Card card, final String zone) {
|
||||
|
||||
@@ -852,7 +852,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
// ensure that selection is within maximum allowed changeNum
|
||||
do {
|
||||
selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, delayedReveal, selectPrompt, decider);
|
||||
selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, 0, changeNum, delayedReveal, selectPrompt, decider);
|
||||
} while (selectedCards != null && selectedCards.size() > changeNum);
|
||||
if (selectedCards != null) {
|
||||
for (Card card : selectedCards) {
|
||||
|
||||
@@ -67,6 +67,8 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
|
||||
if (num == min) {
|
||||
sb.append(Lang.getNumeral(num));
|
||||
} else if (min == 0) {
|
||||
sb.append("up to ").append(Lang.getNumeral(num));
|
||||
} else {
|
||||
sb.append(Lang.getNumeral(min)).append(" or ").append(list.size() == 2 ? "both" : "more");
|
||||
}
|
||||
@@ -101,6 +103,8 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
|
||||
if (num == min) {
|
||||
sb.append(Lang.getNumeral(num));
|
||||
} else if (min == 0) {
|
||||
sb.append("up to ").append(Lang.getNumeral(num));
|
||||
} else {
|
||||
sb.append(Lang.getNumeral(min)).append(" or ").append(list.size() == 2 ? "both" : "more");
|
||||
}
|
||||
@@ -146,19 +150,19 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
return "";
|
||||
}
|
||||
|
||||
public static void makeChoices(SpellAbility sa) {
|
||||
public static boolean makeChoices(SpellAbility sa) {
|
||||
//this resets all previous choices
|
||||
sa.setSubAbility(null);
|
||||
|
||||
// Entwine does use all Choices
|
||||
if (sa.isEntwine()) {
|
||||
chainAbilities(sa, makePossibleOptions(sa));
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
final int num = sa.hasParam("CharmNumOnResolve") ?
|
||||
AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CharmNumOnResolve"), sa)
|
||||
: Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
|
||||
: Integer.parseInt(sa.getParamOrDefault("CharmNum", "1"));
|
||||
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
|
||||
|
||||
Card source = sa.getHostCard();
|
||||
@@ -177,6 +181,7 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
|
||||
List<AbilitySub> chosen = chooser.getController().chooseModeForAbility(sa, min, num, sa.hasParam("CanRepeatModes"));
|
||||
chainAbilities(sa, chosen);
|
||||
return chosen != null && !chosen.isEmpty();
|
||||
}
|
||||
|
||||
private static void chainAbilities(SpellAbility sa, List<AbilitySub> chosen) {
|
||||
|
||||
@@ -164,7 +164,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
|
||||
} else {
|
||||
tgtCards = getTargetCards(sa);
|
||||
}
|
||||
host.clearClones();
|
||||
|
||||
for (final Card c : tgtCards) {
|
||||
if (!sa.usesTargeting() || c.canBeTargetedBy(sa)) {
|
||||
@@ -184,7 +183,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
|
||||
//copyInPlay.setSetCode(c.getSetCode());
|
||||
|
||||
copyInPlay.setCloneOrigin(host);
|
||||
sa.getHostCard().addClone(copyInPlay);
|
||||
if (!pumpKeywords.isEmpty()) {
|
||||
copyInPlay.addChangedCardKeywords(pumpKeywords, Lists.<String>newArrayList(), false, false, timestamp);
|
||||
}
|
||||
@@ -266,7 +264,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
|
||||
final List<String> svars = Lists.newArrayList();
|
||||
final List<String> triggers = Lists.newArrayList();
|
||||
boolean asNonLegendary = false;
|
||||
boolean resetActivations = false;
|
||||
|
||||
if (sa.hasParam("Keywords")) {
|
||||
keywords.addAll(Arrays.asList(sa.getParam("Keywords").split(" & ")));
|
||||
@@ -277,9 +274,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
|
||||
if (sa.hasParam("NonLegendary")) {
|
||||
asNonLegendary = true;
|
||||
}
|
||||
if (sa.hasParam("ResetAbilityActivations")) {
|
||||
resetActivations = true;
|
||||
}
|
||||
if (sa.hasParam("AddSVars")) {
|
||||
svars.addAll(Arrays.asList(sa.getParam("AddSVars").split(" & ")));
|
||||
}
|
||||
@@ -411,11 +405,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
|
||||
copy.removeIntrinsicKeyword("Devoid");
|
||||
}
|
||||
|
||||
if (resetActivations) {
|
||||
for (SpellAbility ab : copy.getSpellAbilities()) {
|
||||
ab.getRestrictions().resetTurnActivations();
|
||||
}
|
||||
}
|
||||
copy.updateStateForView();
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,14 @@ public class CountersPutEffect extends SpellAbilityEffect {
|
||||
counterAmount = pc.chooseNumber(sa, "How many counters?", 0, counterAmount, params);
|
||||
}
|
||||
|
||||
// Adapt need extra logic
|
||||
if (sa.hasParam("Adapt")) {
|
||||
if (!(tgtCard.getCounters(CounterType.P1P1) == 0
|
||||
|| tgtCard.hasKeyword("CARDNAME adapts as though it had no +1/+1 counters"))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("Tribute")) {
|
||||
// make a copy to check if it would be on the battlefield
|
||||
Card noTributeLKI = CardUtil.getLKICopy(tgtCard);
|
||||
@@ -266,6 +274,13 @@ public class CountersPutEffect extends SpellAbilityEffect {
|
||||
runParams.put("Card", tgtCard);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.BecomeRenowned, runParams, false);
|
||||
}
|
||||
if (sa.hasParam("Adapt")) {
|
||||
// need to remove special keyword
|
||||
tgtCard.removeHiddenExtrinsicKeyword("CARDNAME adapts as though it had no +1/+1 counters");
|
||||
final Map<String, Object> runParams = Maps.newHashMap();
|
||||
runParams.put("Card", tgtCard);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Adapt, runParams, false);
|
||||
}
|
||||
} else {
|
||||
// adding counters to something like re-suspend cards
|
||||
// etbcounter should apply multiplier
|
||||
|
||||
@@ -4,6 +4,8 @@ import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerController;
|
||||
@@ -69,11 +71,14 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
|
||||
final Card card = sa.getHostCard();
|
||||
final Game game = card.getGame();
|
||||
final Player player = sa.getActivatingPlayer();
|
||||
|
||||
PlayerController pc = player.getController();
|
||||
final String type = sa.getParam("CounterType");
|
||||
final String num = sa.getParam("CounterNum");
|
||||
|
||||
int cntToRemove = 0;
|
||||
if (!num.equals("All") && !num.equals("Remembered")) {
|
||||
if (!num.equals("All") && !num.equals("Any") && !num.equals("Remembered")) {
|
||||
cntToRemove = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
|
||||
}
|
||||
|
||||
@@ -96,6 +101,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
}
|
||||
|
||||
boolean rememberRemoved = sa.hasParam("RememberRemoved");
|
||||
boolean rememberAmount = sa.hasParam("RememberAmount");
|
||||
|
||||
for (final Player tgtPlayer : getTargetPlayers(sa)) {
|
||||
// Removing energy
|
||||
@@ -107,7 +113,23 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
|
||||
for (final Card tgtCard : getTargetCards(sa)) {
|
||||
CardCollectionView srcCards = null;
|
||||
if (sa.hasParam("ValidSource")) {
|
||||
srcCards = game.getCardsIn(ZoneType.Battlefield);
|
||||
srcCards = CardLists.getValidCards(srcCards, sa.getParam("ValidSource"), player, card, sa);
|
||||
if (num.equals("Any")) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Choose cards to take ").append(counterType.getName()).append(" counters from");
|
||||
|
||||
srcCards = player.getController().chooseCardsForEffect(srcCards, sa, sb.toString(), 0, srcCards.size(), true);
|
||||
}
|
||||
} else {
|
||||
srcCards = getTargetCards(sa);
|
||||
}
|
||||
|
||||
int totalRemoved = 0;
|
||||
|
||||
for (final Card tgtCard : srcCards) {
|
||||
Card gameCard = game.getCardState(tgtCard, null);
|
||||
// gameCard is LKI in that case, the card is not in game anymore
|
||||
// or the timestamp did change
|
||||
@@ -123,14 +145,12 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
}
|
||||
game.updateLastStateForCard(gameCard);
|
||||
continue;
|
||||
} else if (num.equals("All")) {
|
||||
} else if (num.equals("All") || num.equals("Any")) {
|
||||
cntToRemove = gameCard.getCounters(counterType);
|
||||
} else if (sa.getParam("CounterNum").equals("Remembered")) {
|
||||
} else if (num.equals("Remembered")) {
|
||||
cntToRemove = gameCard.getCountersAddedBy(card, counterType);
|
||||
}
|
||||
|
||||
PlayerController pc = sa.getActivatingPlayer().getController();
|
||||
|
||||
|
||||
if (type.equals("Any")) {
|
||||
while (cntToRemove > 0 && gameCard.hasCounters()) {
|
||||
final Map<CounterType, Integer> tgtCounters = gameCard.getCounters();
|
||||
@@ -162,7 +182,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
cntToRemove = Math.min(cntToRemove, gameCard.getCounters(counterType));
|
||||
|
||||
if (zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) {
|
||||
if (sa.hasParam("UpTo")) {
|
||||
if (sa.hasParam("UpTo") || num.equals("Any")) {
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("Target", gameCard);
|
||||
params.put("CounterType", type);
|
||||
@@ -179,10 +199,17 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
game.updateLastStateForCard(gameCard);
|
||||
|
||||
totalRemoved += cntToRemove;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalRemoved > 0 && rememberAmount) {
|
||||
// TODO use SpellAbility Remember later
|
||||
card.addRemembered(Integer.valueOf(totalRemoved));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ public class DamageAllEffect extends DamageBaseEffect {
|
||||
if (!usedDamageMap) {
|
||||
preventMap.triggerPreventDamage(false);
|
||||
damageMap.triggerDamageDoneOnce(false, sa);
|
||||
|
||||
preventMap.clear();
|
||||
damageMap.clear();
|
||||
}
|
||||
|
||||
replaceDying(sa);
|
||||
|
||||
@@ -30,7 +30,7 @@ public class DamageDealEffect extends DamageBaseEffect {
|
||||
final int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
|
||||
|
||||
List<GameObject> tgts = getTargets(sa);
|
||||
if (tgts.isEmpty())
|
||||
if (tgts.isEmpty())
|
||||
return "";
|
||||
|
||||
final List<Card> definedSources = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DamageSource"), sa);
|
||||
@@ -131,15 +131,15 @@ public class DamageDealEffect extends DamageBaseEffect {
|
||||
sa.setPreventMap(preventMap);
|
||||
usedDamageMap = true;
|
||||
}
|
||||
|
||||
|
||||
final List<Card> definedSources = AbilityUtils.getDefinedCards(hostCard, sa.getParam("DamageSource"), sa);
|
||||
if (definedSources == null || definedSources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (Card source : definedSources) {
|
||||
final Card sourceLKI = hostCard.getGame().getChangeZoneLKIInfo(source);
|
||||
|
||||
|
||||
if (divideOnResolution) {
|
||||
// Dividing Damage up to multiple targets using combat damage box
|
||||
// Currently only used for Master of the Wild Hunt
|
||||
@@ -147,7 +147,7 @@ public class DamageDealEffect extends DamageBaseEffect {
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
CardCollection assigneeCards = new CardCollection();
|
||||
// Do we have a way of doing this in a better fashion?
|
||||
for (GameObject obj : tgts) {
|
||||
@@ -155,7 +155,7 @@ public class DamageDealEffect extends DamageBaseEffect {
|
||||
assigneeCards.add((Card)obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Player assigningPlayer = players.get(0);
|
||||
Map<Card, Integer> map = assigningPlayer.getController().assignCombatDamage(sourceLKI, assigneeCards, dmg, null, true);
|
||||
for (Entry<Card, Integer> dt : map.entrySet()) {
|
||||
@@ -166,6 +166,9 @@ public class DamageDealEffect extends DamageBaseEffect {
|
||||
preventMap.triggerPreventDamage(false);
|
||||
// non combat damage cause lifegain there
|
||||
damageMap.triggerDamageDoneOnce(false, sa);
|
||||
|
||||
preventMap.clear();
|
||||
damageMap.clear();
|
||||
}
|
||||
replaceDying(sa);
|
||||
return;
|
||||
@@ -201,7 +204,7 @@ public class DamageDealEffect extends DamageBaseEffect {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (remember) {
|
||||
source.addRemembered(damageMap.row(sourceLKI).keySet());
|
||||
}
|
||||
@@ -210,6 +213,9 @@ public class DamageDealEffect extends DamageBaseEffect {
|
||||
preventMap.triggerPreventDamage(false);
|
||||
// non combat damage cause lifegain there
|
||||
damageMap.triggerDamageDoneOnce(false, sa);
|
||||
|
||||
preventMap.clear();
|
||||
damageMap.clear();
|
||||
}
|
||||
replaceDying(sa);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,9 @@ public class DamageEachEffect extends DamageBaseEffect {
|
||||
if (!usedDamageMap) {
|
||||
preventMap.triggerPreventDamage(false);
|
||||
damageMap.triggerDamageDoneOnce(false, sa);
|
||||
|
||||
preventMap.clear();
|
||||
damageMap.clear();
|
||||
}
|
||||
|
||||
replaceDying(sa);
|
||||
|
||||
@@ -21,10 +21,12 @@ public class DamageResolveEffect extends SpellAbilityEffect {
|
||||
|
||||
if (preventMap != null) {
|
||||
preventMap.triggerPreventDamage(false);
|
||||
preventMap.clear();
|
||||
}
|
||||
// non combat damage cause lifegain there
|
||||
if (damageMap != null) {
|
||||
damageMap.triggerDamageDoneOnce(false, sa);
|
||||
damageMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,14 +187,14 @@ public class DigEffect extends SpellAbilityEffect {
|
||||
if (!andOrValid.equals("")) {
|
||||
andOrCards = CardLists.getValidCards(top, andOrValid.split(","), host.getController(), host, sa);
|
||||
andOrCards.removeAll((Collection<?>)valid);
|
||||
valid.addAll(andOrCards);
|
||||
valid.addAll(andOrCards); //pfps need to add andOr cards to valid to have set of all valid cards set up
|
||||
}
|
||||
else {
|
||||
andOrCards = new CardCollection();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser.
|
||||
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser. pfps??
|
||||
if (p == chooser && destZone1ChangeNum > 1) {
|
||||
delayedReveal = null;
|
||||
}
|
||||
@@ -238,55 +238,41 @@ public class DigEffect extends SpellAbilityEffect {
|
||||
if (sa.hasParam("RandomOrder")) {
|
||||
CardLists.shuffle(movedCards);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
String prompt;
|
||||
|
||||
if (sa.hasParam("PrimaryPrompt")) {
|
||||
prompt = sa.getParam("PrimaryPrompt");
|
||||
} else {
|
||||
prompt = "Choose card(s) to put into " + destZone1.name();
|
||||
if (destZone1.equals(ZoneType.Library)) {
|
||||
if (libraryPosition == -1) {
|
||||
prompt = "Choose card(s) to put on the bottom of {player's} library";
|
||||
} else if (libraryPosition == 0) {
|
||||
prompt = "Choose card(s) to put on top of {player's} library";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("PrimaryPrompt")) {
|
||||
prompt = sa.getParam("PrimaryPrompt");
|
||||
} else {
|
||||
prompt = "Choose a card to put into " + destZone1.name();
|
||||
if (destZone1.equals(ZoneType.Library)) {
|
||||
if (libraryPosition == -1) {
|
||||
prompt = "Choose a card to put on the bottom of {player's} library";
|
||||
}
|
||||
else if (libraryPosition == 0) {
|
||||
prompt = "Choose a card to put on top of {player's} library";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
movedCards = new CardCollection();
|
||||
for (int i = 0; i < destZone1ChangeNum || (anyNumber && i < numToDig); i++) {
|
||||
// let user get choice
|
||||
Card chosen = null;
|
||||
if (!valid.isEmpty()) {
|
||||
// If we're choosing multiple cards, only need to show the reveal dialog the first time through.
|
||||
boolean shouldReveal = (i == 0);
|
||||
chosen = chooser.getController().chooseSingleEntityForEffect(valid, shouldReveal ? delayedReveal : null, sa, prompt, anyNumber || optional, p);
|
||||
}
|
||||
else {
|
||||
if (i == 0) {
|
||||
chooser.getController().notifyOfValue(sa, null, "No valid cards");
|
||||
}
|
||||
}
|
||||
|
||||
if (chosen == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
movedCards.add(chosen);
|
||||
valid.remove(chosen);
|
||||
if (!andOrValid.equals("")) {
|
||||
andOrCards.remove(chosen);
|
||||
if (!chosen.isValid(andOrValid.split(","), host.getController(), host, sa)) {
|
||||
valid = new CardCollection(andOrCards);
|
||||
}
|
||||
else if (!chosen.isValid(changeValid.split(","), host.getController(), host, sa)) {
|
||||
valid.removeAll((Collection<?>)andOrCards);
|
||||
}
|
||||
}
|
||||
}
|
||||
movedCards = new CardCollection();
|
||||
if (valid.isEmpty()) {
|
||||
chooser.getController().notifyOfValue(sa, null, "No valid cards");
|
||||
} else {
|
||||
if ( p == chooser ) { // the digger can still see all the dug cards when choosing
|
||||
chooser.getController().tempShowCards(top);
|
||||
}
|
||||
List<Card> chosen;
|
||||
if (!andOrValid.equals("")) {
|
||||
valid.removeAll(andOrCards); //pfps remove andOr cards to get two two choices set up correctly
|
||||
chosen = chooser.getController().chooseFromTwoListsForEffect(valid, andOrCards, optional, delayedReveal, sa, prompt, p);
|
||||
} else {
|
||||
int max = anyNumber ? valid.size() : Math.min(valid.size(),destZone1ChangeNum);
|
||||
int min = (anyNumber || optional) ? 0 : max;
|
||||
chosen = chooser.getController().chooseEntitiesForEffect(valid, min, max, delayedReveal, sa, prompt, p);
|
||||
}
|
||||
chooser.getController().endTempShowCards();
|
||||
movedCards.addAll(chosen);
|
||||
}
|
||||
|
||||
if (!changeValid.isEmpty() && !sa.hasParam("ExileFaceDown") && !sa.hasParam("NoReveal")) {
|
||||
game.getAction().reveal(movedCards, chooser, true,
|
||||
|
||||
@@ -153,6 +153,9 @@ public class FightEffect extends DamageBaseEffect {
|
||||
if (!usedDamageMap) {
|
||||
preventMap.triggerPreventDamage(false);
|
||||
damageMap.triggerDamageDoneOnce(false, sa);
|
||||
|
||||
preventMap.clear();
|
||||
damageMap.clear();
|
||||
}
|
||||
|
||||
replaceDying(sa);
|
||||
|
||||
@@ -4,17 +4,17 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
|
||||
public class ScryEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
|
||||
final List<Player> tgtPlayers = getTargetPlayers(sa);
|
||||
|
||||
for (final Player p : tgtPlayers) {
|
||||
for (final Player p : getTargetPlayers(sa)) {
|
||||
sb.append(p.toString()).append(" ");
|
||||
}
|
||||
|
||||
@@ -36,19 +36,16 @@ public class ScryEffect extends SpellAbilityEffect {
|
||||
|
||||
boolean isOptional = sa.hasParam("Optional");
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final List<Player> tgtPlayers = getTargetPlayers(sa);
|
||||
final List<Player> players = Lists.newArrayList(); // players really affected
|
||||
|
||||
for (final Player p : tgtPlayers) {
|
||||
if ((tgt == null) || p.canBeTargetedBy(sa)) {
|
||||
if (isOptional && !p.getController().confirmAction(sa, null, "Do you want to scry?")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
p.scry(num, sa);
|
||||
}
|
||||
}
|
||||
// Optional here for spells that have optional multi-player scrying
|
||||
for (final Player p : getTargetPlayers(sa)) {
|
||||
if ( (!sa.usesTargeting() || p.canBeTargetedBy(sa)) &&
|
||||
(!isOptional || p.getController().confirmAction(sa, null, "Do you want to scry?")) ) {
|
||||
players.add(p);
|
||||
}
|
||||
}
|
||||
sa.getActivatingPlayer().getGame().getAction().scry(players, num, sa);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
|
||||
// cards attached or otherwise linked to this card
|
||||
private CardCollection hauntedBy, devouredCards, delvedCards, convokedCards, imprintedCards, encodedCards;
|
||||
private CardCollection mustBlockCards, clones, gainControlTargets, chosenCards, blockedThisTurn, blockedByThisTurn;
|
||||
private CardCollection mustBlockCards, gainControlTargets, chosenCards, blockedThisTurn, blockedByThisTurn;
|
||||
|
||||
// if this card is attached or linked to something, what card is it currently attached to
|
||||
private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith;
|
||||
@@ -528,30 +528,30 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
|
||||
public Card manifest(Player p, SpellAbility sa) {
|
||||
// Turn Face Down (even if it's DFC).
|
||||
CardState originalCard = this.getState(CardStateName.Original);
|
||||
ManaCost cost = originalCard.getManaCost();
|
||||
ManaCost cost = getState(CardStateName.Original).getManaCost();
|
||||
|
||||
boolean isCreature = this.isCreature();
|
||||
boolean isCreature = isCreature();
|
||||
|
||||
// Sometimes cards are manifested while already being face down
|
||||
if (!turnFaceDown(true) && currentStateName != CardStateName.FaceDown) {
|
||||
return null;
|
||||
}
|
||||
// Sometimes cards are manifested while already being face down
|
||||
if (!turnFaceDown(true) && !isFaceDown()) {
|
||||
return null;
|
||||
}
|
||||
// Move to p's battlefield
|
||||
Game game = p.getGame();
|
||||
// Just in case you aren't the controller, now you are!
|
||||
this.setController(p, game.getNextTimestamp());
|
||||
|
||||
// Just in case you aren't the controller, now you are!
|
||||
setController(p, game.getNextTimestamp());
|
||||
|
||||
// Mark this card as "manifested"
|
||||
this.setPreFaceDownState(CardStateName.Original);
|
||||
this.setManifested(true);
|
||||
setPreFaceDownState(CardStateName.Original);
|
||||
setManifested(true);
|
||||
|
||||
Card c = game.getAction().moveToPlay(this, p, sa);
|
||||
|
||||
// Add manifest demorph static ability for creatures
|
||||
if (isCreature && !cost.isNoCost()) {
|
||||
c.addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(c, cost));
|
||||
|
||||
// Add Manifest to original State
|
||||
c.getState(CardStateName.Original).addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(c, cost));
|
||||
c.updateStateForView();
|
||||
}
|
||||
|
||||
@@ -1031,22 +1031,6 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
public final GameEntity getMustAttackEntityThisTurn() { return mustAttackEntityThisTurn; }
|
||||
public final void setMustAttackEntityThisTurn(GameEntity entThisTurn) { mustAttackEntityThisTurn = entThisTurn; }
|
||||
|
||||
public final CardCollectionView getClones() {
|
||||
return CardCollection.getView(clones);
|
||||
}
|
||||
public final void setClones(final Iterable<Card> clones0) {
|
||||
clones = clones0 == null ? null : new CardCollection(clones0);
|
||||
}
|
||||
public final void addClone(final Card c) {
|
||||
if (clones == null) {
|
||||
clones = new CardCollection();
|
||||
}
|
||||
clones.add(c);
|
||||
}
|
||||
public final void clearClones() {
|
||||
clones = null;
|
||||
}
|
||||
|
||||
public final Card getCloneOrigin() {
|
||||
return cloneOrigin;
|
||||
}
|
||||
@@ -1067,7 +1051,8 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
}
|
||||
|
||||
public final boolean hasConverge() {
|
||||
return "Count$Converge".equals(getSVar("X")) || "Count$Converge".equals(getSVar("Y")) || hasKeyword("Sunburst");
|
||||
return "Count$Converge".equals(getSVar("X")) || "Count$Converge".equals(getSVar("Y")) ||
|
||||
hasKeyword(Keyword.SUNBURST) || hasKeyword("Modular:Sunburst");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1463,250 +1448,261 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
int i = 0;
|
||||
for (KeywordInterface inst : keywords) {
|
||||
String keyword = inst.getOriginal();
|
||||
if (keyword.startsWith("SpellCantTarget")) {
|
||||
continue;
|
||||
}
|
||||
// format text changes
|
||||
if (CardUtil.isKeywordModifiable(keyword)
|
||||
&& keywordsGrantedByTextChanges.contains(inst)) {
|
||||
for (final Entry<String, String> e : textChanges) {
|
||||
final String value = e.getValue();
|
||||
if (keyword.contains(value)) {
|
||||
keyword = TextUtil.fastReplace(keyword, value,
|
||||
TextUtil.concatNoSpace("<strike>", e.getKey(), "</strike> ", value));
|
||||
// assume (for now) max one change per keyword
|
||||
break;
|
||||
try {
|
||||
if (keyword.startsWith("SpellCantTarget")) {
|
||||
continue;
|
||||
}
|
||||
// format text changes
|
||||
if (CardUtil.isKeywordModifiable(keyword)
|
||||
&& keywordsGrantedByTextChanges.contains(inst)) {
|
||||
for (final Entry<String, String> e : textChanges) {
|
||||
final String value = e.getValue();
|
||||
if (keyword.contains(value)) {
|
||||
keyword = TextUtil.fastReplace(keyword, value,
|
||||
TextUtil.concatNoSpace("<strike>", e.getKey(), "</strike> ", value));
|
||||
// assume (for now) max one change per keyword
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keyword.startsWith("CantBeCounteredBy")) {
|
||||
final String[] p = keyword.split(":");
|
||||
sbLong.append(p[2]).append("\r\n");
|
||||
} else if (keyword.startsWith("etbCounter")) {
|
||||
final String[] p = keyword.split(":");
|
||||
final StringBuilder s = new StringBuilder();
|
||||
if (p.length > 4) {
|
||||
if (!"no desc".equals(p[4])) {
|
||||
s.append(p[4]);
|
||||
if (keyword.startsWith("CantBeCounteredBy") || keyword.startsWith("Panharmonicon")
|
||||
|| keyword.startsWith("Dieharmonicon")) {
|
||||
final String[] p = keyword.split(":");
|
||||
sbLong.append(p[2]).append("\r\n");
|
||||
} else if (keyword.startsWith("etbCounter")) {
|
||||
final String[] p = keyword.split(":");
|
||||
final StringBuilder s = new StringBuilder();
|
||||
if (p.length > 4) {
|
||||
if (!"no desc".equals(p[4])) {
|
||||
s.append(p[4]);
|
||||
}
|
||||
} else {
|
||||
s.append(getName());
|
||||
s.append(" enters the battlefield with ");
|
||||
s.append(Lang.nounWithNumeral(p[2], CounterType.valueOf(p[1]).getName() + " counter"));
|
||||
s.append(" on it.");
|
||||
}
|
||||
} else {
|
||||
s.append(getName());
|
||||
s.append(" enters the battlefield with ");
|
||||
s.append(Lang.nounWithNumeral(p[2], CounterType.valueOf(p[1]).getName() + " counter"));
|
||||
s.append(" on it.");
|
||||
}
|
||||
sbLong.append(s).append("\r\n");
|
||||
} else if (keyword.startsWith("Protection:")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[2]).append("\r\n");
|
||||
} else if (keyword.startsWith("Creatures can't attack unless their controller pays")) {
|
||||
final String[] k = keyword.split(":");
|
||||
if (!k[3].equals("no text")) {
|
||||
sbLong.append(k[3]).append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Enchant")) {
|
||||
String k = keyword;
|
||||
k = TextUtil.fastReplace(k, "Curse", "");
|
||||
sbLong.append(k).append("\r\n");
|
||||
} else if (keyword.startsWith("Ripple")) {
|
||||
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
|
||||
} else if (keyword.startsWith("Madness")) {
|
||||
String[] parts = keyword.split(":");
|
||||
// If no colon exists in Madness keyword, it must have been granted and assumed the cost from host
|
||||
if (parts.length < 2) {
|
||||
sbLong.append(parts[0]).append(" ").append(this.getManaCost()).append("\r\n");
|
||||
} else {
|
||||
sbLong.append(parts[0]).append(" ").append(ManaCostParser.parse(parts[1])).append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")) {
|
||||
String[] k = keyword.split(":");
|
||||
sbLong.append(k[0]);
|
||||
if (k.length > 1) {
|
||||
final Cost mCost = new Cost(k[1], true);
|
||||
if (!mCost.isOnlyManaCost()) {
|
||||
sbLong.append("—");
|
||||
sbLong.append(s).append("\r\n");
|
||||
} else if (keyword.startsWith("Protection:")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[2]).append("\r\n");
|
||||
} else if (keyword.startsWith("Creatures can't attack unless their controller pays")) {
|
||||
final String[] k = keyword.split(":");
|
||||
if (!k[3].equals("no text")) {
|
||||
sbLong.append(k[3]).append("\r\n");
|
||||
}
|
||||
if (mCost.isOnlyManaCost()) {
|
||||
sbLong.append(" ");
|
||||
} else if (keyword.startsWith("Enchant")) {
|
||||
String k = keyword;
|
||||
k = TextUtil.fastReplace(k, "Curse", "");
|
||||
sbLong.append(k).append("\r\n");
|
||||
} else if (keyword.startsWith("Ripple")) {
|
||||
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
|
||||
} else if (keyword.startsWith("Madness")) {
|
||||
String[] parts = keyword.split(":");
|
||||
// If no colon exists in Madness keyword, it must have been granted and assumed the cost from host
|
||||
if (parts.length < 2) {
|
||||
sbLong.append(parts[0]).append(" ").append(this.getManaCost()).append("\r\n");
|
||||
} else {
|
||||
sbLong.append(parts[0]).append(" ").append(ManaCostParser.parse(parts[1])).append("\r\n");
|
||||
}
|
||||
sbLong.append(mCost.toString()).delete(sbLong.length() - 2, sbLong.length());
|
||||
if (!mCost.isOnlyManaCost()) {
|
||||
sbLong.append(".");
|
||||
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")) {
|
||||
String[] k = keyword.split(":");
|
||||
sbLong.append(k[0]);
|
||||
if (k.length > 1) {
|
||||
final Cost mCost = new Cost(k[1], true);
|
||||
if (!mCost.isOnlyManaCost()) {
|
||||
sbLong.append("—");
|
||||
}
|
||||
if (mCost.isOnlyManaCost()) {
|
||||
sbLong.append(" ");
|
||||
}
|
||||
sbLong.append(mCost.toString()).delete(sbLong.length() - 2, sbLong.length());
|
||||
if (!mCost.isOnlyManaCost()) {
|
||||
sbLong.append(".");
|
||||
}
|
||||
sbLong.append(" (" + inst.getReminderText() + ")");
|
||||
sbLong.append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Emerge")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
|
||||
sbLong.append(" (" + inst.getReminderText() + ")");
|
||||
sbLong.append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Emerge")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
|
||||
sbLong.append(" (" + inst.getReminderText() + ")");
|
||||
sbLong.append("\r\n");
|
||||
} else if (keyword.startsWith("Echo")) {
|
||||
sbLong.append("Echo ");
|
||||
final String[] upkeepCostParams = keyword.split(":");
|
||||
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
|
||||
sbLong.append(" (At the beginning of your upkeep, if CARDNAME came under your control since the beginning of your last upkeep, sacrifice it unless you pay its echo cost.)");
|
||||
sbLong.append("\r\n");
|
||||
} else if (keyword.startsWith("Cumulative upkeep")) {
|
||||
sbLong.append("Cumulative upkeep ");
|
||||
final String[] upkeepCostParams = keyword.split(":");
|
||||
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
|
||||
sbLong.append("\r\n");
|
||||
} else if (keyword.startsWith("Alternative Cost")) {
|
||||
sbLong.append("Has alternative cost.");
|
||||
} else if (keyword.startsWith("AlternateAdditionalCost")) {
|
||||
final String costString1 = keyword.split(":")[1];
|
||||
final String costString2 = keyword.split(":")[2];
|
||||
final Cost cost1 = new Cost(costString1, false);
|
||||
final Cost cost2 = new Cost(costString2, false);
|
||||
sbLong.append("As an additional cost to cast ")
|
||||
.append(getName()).append(", ")
|
||||
.append(cost1.toSimpleString())
|
||||
.append(" or pay ")
|
||||
.append(cost2.toSimpleString())
|
||||
.append(".\r\n");
|
||||
} else if (keyword.startsWith("Multikicker")) {
|
||||
if (!keyword.endsWith("Generic")) {
|
||||
final String[] n = keyword.split(":");
|
||||
final Cost cost = new Cost(n[1], false);
|
||||
sbLong.append("Multikicker ").append(cost.toSimpleString());
|
||||
sbLong.append(" (" + inst.getReminderText() + ")").append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Kicker")) {
|
||||
if (!keyword.endsWith("Generic")) {
|
||||
final StringBuilder sbx = new StringBuilder();
|
||||
final String[] n = keyword.split(":");
|
||||
sbx.append("Kicker ");
|
||||
final Cost cost = new Cost(n[1], false);
|
||||
sbx.append(cost.toSimpleString());
|
||||
if (Lists.newArrayList(n).size() > 2) {
|
||||
sbx.append(" and/or ");
|
||||
final Cost cost2 = new Cost(n[2], false);
|
||||
sbx.append(cost2.toSimpleString());
|
||||
} else if (keyword.startsWith("Echo")) {
|
||||
sbLong.append("Echo ");
|
||||
final String[] upkeepCostParams = keyword.split(":");
|
||||
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
|
||||
sbLong.append(" (At the beginning of your upkeep, if CARDNAME came under your control since the beginning of your last upkeep, sacrifice it unless you pay its echo cost.)");
|
||||
sbLong.append("\r\n");
|
||||
} else if (keyword.startsWith("Cumulative upkeep")) {
|
||||
sbLong.append("Cumulative upkeep ");
|
||||
final String[] upkeepCostParams = keyword.split(":");
|
||||
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
|
||||
sbLong.append("\r\n");
|
||||
} else if (keyword.startsWith("Alternative Cost")) {
|
||||
sbLong.append("Has alternative cost.");
|
||||
} else if (keyword.startsWith("AlternateAdditionalCost")) {
|
||||
final String costString1 = keyword.split(":")[1];
|
||||
final String costString2 = keyword.split(":")[2];
|
||||
final Cost cost1 = new Cost(costString1, false);
|
||||
final Cost cost2 = new Cost(costString2, false);
|
||||
sbLong.append("As an additional cost to cast ")
|
||||
.append(getName()).append(", ")
|
||||
.append(cost1.toSimpleString())
|
||||
.append(" or pay ")
|
||||
.append(cost2.toSimpleString())
|
||||
.append(".\r\n");
|
||||
} else if (keyword.startsWith("Multikicker")) {
|
||||
if (!keyword.endsWith("Generic")) {
|
||||
final String[] n = keyword.split(":");
|
||||
final Cost cost = new Cost(n[1], false);
|
||||
sbLong.append("Multikicker ").append(cost.toSimpleString());
|
||||
sbLong.append(" (" + inst.getReminderText() + ")").append("\r\n");
|
||||
}
|
||||
sbx.append(" (" + inst.getReminderText() + ")");
|
||||
sbLong.append(sbx).append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Hexproof:")) {
|
||||
final String k[] = keyword.split(":");
|
||||
sbLong.append("Hexproof from ").append(k[2])
|
||||
.append(" (").append(inst.getReminderText()).append(")").append("\r\n");
|
||||
} else if (keyword.endsWith(".") && !keyword.startsWith("Haunt")) {
|
||||
sbLong.append(keyword).append("\r\n");
|
||||
} else if (keyword.startsWith("Presence") || keyword.startsWith("MayFlash")) {
|
||||
// Pseudo keywords, only print Reminder
|
||||
sbLong.append(inst.getReminderText());
|
||||
} else if (keyword.contains("At the beginning of your upkeep, ")
|
||||
&& keyword.contains(" unless you pay")) {
|
||||
sbLong.append(keyword).append("\r\n");
|
||||
} else if (keyword.startsWith("Strive") || keyword.startsWith("Escalate")
|
||||
|| keyword.startsWith("ETBReplacement")
|
||||
|| keyword.startsWith("CantBeBlockedBy ")
|
||||
|| keyword.startsWith("Affinity")
|
||||
|| keyword.equals("CARDNAME enters the battlefield tapped.")
|
||||
|| keyword.startsWith("UpkeepCost")) {
|
||||
} else if (keyword.equals("Provoke") || keyword.equals("Ingest") || keyword.equals("Unleash")
|
||||
|| keyword.equals("Soulbond") || keyword.equals("Partner") || keyword.equals("Retrace")
|
||||
|| keyword.equals("Living Weapon") || keyword.equals("Myriad") || keyword.equals("Exploit")
|
||||
|| keyword.equals("Changeling") || keyword.equals("Delve")
|
||||
|| keyword.equals("Split second")
|
||||
|| keyword.equals("Suspend") // for the ones without amounnt
|
||||
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|
||||
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|
||||
|| keyword.equals("Devoid") || keyword.equals("Riot")){
|
||||
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.startsWith("Partner:")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append("Partner with " + k[1] + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.startsWith("Modular") || keyword.startsWith("Bloodthirst") || keyword.startsWith("Dredge")
|
||||
|| keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift") || keyword.startsWith("Bushido")
|
||||
|| keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")
|
||||
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing")
|
||||
|| keyword.startsWith("Afterlife")
|
||||
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|
||||
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[0] + " " + k[1] + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.contains("Haunt")) {
|
||||
sb.append("\r\nHaunt (");
|
||||
if (isCreature()) {
|
||||
sb.append("When this creature dies, exile it haunting target creature.");
|
||||
} else {
|
||||
sb.append("When this spell card is put into a graveyard after resolving, ");
|
||||
sb.append("exile it haunting target creature.");
|
||||
}
|
||||
sb.append(")");
|
||||
} else if (keyword.equals("Convoke") || keyword.equals("Dethrone")|| keyword.equals("Fear")
|
||||
|| keyword.equals("Melee") || keyword.equals("Improvise")|| keyword.equals("Shroud")
|
||||
|| keyword.equals("Banding") || keyword.equals("Intimidate")|| keyword.equals("Evolve")
|
||||
|| keyword.equals("Exalted") || keyword.equals("Extort")|| keyword.equals("Flanking")
|
||||
|| keyword.equals("Horsemanship") || keyword.equals("Infect")|| keyword.equals("Persist")
|
||||
|| keyword.equals("Phasing") || keyword.equals("Shadow")|| keyword.equals("Skulk")
|
||||
|| keyword.equals("Undying") || keyword.equals("Wither") || keyword.equals("Cascade")
|
||||
|| keyword.equals("Mentor")) {
|
||||
if (sb.length() != 0) {
|
||||
sb.append("\r\n");
|
||||
}
|
||||
sb.append(keyword + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.endsWith(" offering")) {
|
||||
String offeringType = keyword.split(" ")[0];
|
||||
if (sb.length() != 0) {
|
||||
sb.append("\r\n");
|
||||
}
|
||||
sbLong.append(keyword);
|
||||
sbLong.append(" (" + Keyword.getInstance("Offering:"+ offeringType).getReminderText() + ")");
|
||||
} else if (keyword.startsWith("Equip") || keyword.startsWith("Fortify") || keyword.startsWith("Outlast")
|
||||
|| keyword.startsWith("Unearth") || keyword.startsWith("Scavenge") || keyword.startsWith("Spectacle")
|
||||
|| keyword.startsWith("Evoke") || keyword.startsWith("Bestow") || keyword.startsWith("Dash")
|
||||
|| keyword.startsWith("Surge") || keyword.startsWith("Transmute") || keyword.startsWith("Suspend")
|
||||
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity") || keyword.startsWith("Embalm")
|
||||
|| keyword.startsWith("Level up") || keyword.equals("Prowess") || keyword.startsWith("Eternalize")
|
||||
|| keyword.startsWith("Reinforce") || keyword.startsWith("Champion") || keyword.startsWith("Prowl")
|
||||
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|
||||
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")) {
|
||||
// keyword parsing takes care of adding a proper description
|
||||
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
|
||||
sbLong.append(getName()).append(" can't be blocked ");
|
||||
sbLong.append(getTextForKwCantBeBlockedByAmount(keyword));
|
||||
} else if (keyword.startsWith("CantBlock")) {
|
||||
sbLong.append(getName()).append(" can't block ");
|
||||
if (keyword.contains("CardUID")) {
|
||||
sbLong.append("CardID (").append(Integer.valueOf(keyword.split("CantBlockCardUID_")[1])).append(")");
|
||||
} else {
|
||||
} else if (keyword.startsWith("Kicker")) {
|
||||
if (!keyword.endsWith("Generic")) {
|
||||
final StringBuilder sbx = new StringBuilder();
|
||||
final String[] n = keyword.split(":");
|
||||
sbx.append("Kicker ");
|
||||
final Cost cost = new Cost(n[1], false);
|
||||
sbx.append(cost.toSimpleString());
|
||||
if (Lists.newArrayList(n).size() > 2) {
|
||||
sbx.append(" and/or ");
|
||||
final Cost cost2 = new Cost(n[2], false);
|
||||
sbx.append(cost2.toSimpleString());
|
||||
}
|
||||
sbx.append(" (" + inst.getReminderText() + ")");
|
||||
sbLong.append(sbx).append("\r\n");
|
||||
}
|
||||
} else if (keyword.startsWith("Hexproof:")) {
|
||||
final String k[] = keyword.split(":");
|
||||
sbLong.append("Hexproof from ").append(k[2])
|
||||
.append(" (").append(inst.getReminderText()).append(")").append("\r\n");
|
||||
} else if (keyword.endsWith(".") && !keyword.startsWith("Haunt")) {
|
||||
sbLong.append(keyword).append("\r\n");
|
||||
} else if (keyword.startsWith("Presence") || keyword.startsWith("MayFlash")) {
|
||||
// Pseudo keywords, only print Reminder
|
||||
sbLong.append(inst.getReminderText());
|
||||
} else if (keyword.contains("At the beginning of your upkeep, ")
|
||||
&& keyword.contains(" unless you pay")) {
|
||||
sbLong.append(keyword).append("\r\n");
|
||||
} else if (keyword.startsWith("Strive") || keyword.startsWith("Escalate")
|
||||
|| keyword.startsWith("ETBReplacement")
|
||||
|| keyword.startsWith("CantBeBlockedBy ")
|
||||
|| keyword.startsWith("Affinity")
|
||||
|| keyword.equals("CARDNAME enters the battlefield tapped.")
|
||||
|| keyword.startsWith("UpkeepCost")) {
|
||||
} else if (keyword.equals("Provoke") || keyword.equals("Ingest") || keyword.equals("Unleash")
|
||||
|| keyword.equals("Soulbond") || keyword.equals("Partner") || keyword.equals("Retrace")
|
||||
|| keyword.equals("Living Weapon") || keyword.equals("Myriad") || keyword.equals("Exploit")
|
||||
|| keyword.equals("Changeling") || keyword.equals("Delve")
|
||||
|| keyword.equals("Split second") || keyword.equals("Sunburst")
|
||||
|| keyword.equals("Suspend") // for the ones without amounnt
|
||||
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|
||||
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|
||||
|| keyword.equals("Devoid") || keyword.equals("Riot")){
|
||||
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.startsWith("Partner:")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k.length > 1 ? k[1] + ".\r\n" : "");
|
||||
sbLong.append("Partner with " + k[1] + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.startsWith("Modular") || keyword.startsWith("Bloodthirst") || keyword.startsWith("Dredge")
|
||||
|| keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift") || keyword.startsWith("Bushido")
|
||||
|| keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")
|
||||
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing")
|
||||
|| keyword.startsWith("Afterlife")
|
||||
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|
||||
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[0] + " " + k[1] + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.contains("Haunt")) {
|
||||
sb.append("\r\nHaunt (");
|
||||
if (isCreature()) {
|
||||
sb.append("When this creature dies, exile it haunting target creature.");
|
||||
} else {
|
||||
sb.append("When this spell card is put into a graveyard after resolving, ");
|
||||
sb.append("exile it haunting target creature.");
|
||||
}
|
||||
sb.append(")");
|
||||
} else if (keyword.equals("Convoke") || keyword.equals("Dethrone")|| keyword.equals("Fear")
|
||||
|| keyword.equals("Melee") || keyword.equals("Improvise")|| keyword.equals("Shroud")
|
||||
|| keyword.equals("Banding") || keyword.equals("Intimidate")|| keyword.equals("Evolve")
|
||||
|| keyword.equals("Exalted") || keyword.equals("Extort")|| keyword.equals("Flanking")
|
||||
|| keyword.equals("Horsemanship") || keyword.equals("Infect")|| keyword.equals("Persist")
|
||||
|| keyword.equals("Phasing") || keyword.equals("Shadow")|| keyword.equals("Skulk")
|
||||
|| keyword.equals("Undying") || keyword.equals("Wither") || keyword.equals("Cascade")
|
||||
|| keyword.equals("Mentor")) {
|
||||
if (sb.length() != 0) {
|
||||
sb.append("\r\n");
|
||||
}
|
||||
sb.append(keyword + " (" + inst.getReminderText() + ")");
|
||||
} else if (keyword.endsWith(" offering")) {
|
||||
String offeringType = keyword.split(" ")[0];
|
||||
if (sb.length() != 0) {
|
||||
sb.append("\r\n");
|
||||
}
|
||||
sbLong.append(keyword);
|
||||
sbLong.append(" (" + Keyword.getInstance("Offering:"+ offeringType).getReminderText() + ")");
|
||||
} else if (keyword.startsWith("Equip") || keyword.startsWith("Fortify") || keyword.startsWith("Outlast")
|
||||
|| keyword.startsWith("Unearth") || keyword.startsWith("Scavenge") || keyword.startsWith("Spectacle")
|
||||
|| keyword.startsWith("Evoke") || keyword.startsWith("Bestow") || keyword.startsWith("Dash")
|
||||
|| keyword.startsWith("Surge") || keyword.startsWith("Transmute") || keyword.startsWith("Suspend")
|
||||
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity") || keyword.startsWith("Embalm")
|
||||
|| keyword.startsWith("Level up") || keyword.equals("Prowess") || keyword.startsWith("Eternalize")
|
||||
|| keyword.startsWith("Reinforce") || keyword.startsWith("Champion") || keyword.startsWith("Prowl")
|
||||
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|
||||
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")) {
|
||||
// keyword parsing takes care of adding a proper description
|
||||
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
|
||||
sbLong.append(getName()).append(" can't be blocked ");
|
||||
sbLong.append(getTextForKwCantBeBlockedByAmount(keyword));
|
||||
} else if (keyword.startsWith("CantBlock")) {
|
||||
sbLong.append(getName()).append(" can't block ");
|
||||
if (keyword.contains("CardUID")) {
|
||||
sbLong.append("CardID (").append(Integer.valueOf(keyword.split("CantBlockCardUID_")[1])).append(")");
|
||||
} else {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k.length > 1 ? k[1] + ".\r\n" : "");
|
||||
}
|
||||
} else if (keyword.equals("Unblockable")) {
|
||||
sbLong.append(getName()).append(" can't be blocked.\r\n");
|
||||
} else if (keyword.equals("AllNonLegendaryCreatureNames")) {
|
||||
sbLong.append(getName()).append(" has all names of nonlegendary creature cards.\r\n");
|
||||
} else if (keyword.startsWith("IfReach")) {
|
||||
String k[] = keyword.split(":");
|
||||
sbLong.append(getName()).append(" can block ")
|
||||
.append(CardType.getPluralType(k[1]))
|
||||
.append(" as though it had reach.\r\n");
|
||||
} else if (keyword.startsWith("MayEffectFromOpeningHand")) {
|
||||
final String[] k = keyword.split(":");
|
||||
// need to get SpellDescription from Svar
|
||||
String desc = AbilityFactory.getMapParams(getSVar(k[1])).get("SpellDescription");
|
||||
sbLong.append(desc);
|
||||
} else if (keyword.startsWith("Saga")) {
|
||||
String k[] = keyword.split(":");
|
||||
String desc = "(As this Saga enters and after your draw step, "
|
||||
+ " add a lore counter. Sacrifice after " + Strings.repeat("I", Integer.valueOf(k[1])) + ".)";
|
||||
sbLong.append(desc);
|
||||
}
|
||||
} else if (keyword.equals("Unblockable")) {
|
||||
sbLong.append(getName()).append(" can't be blocked.\r\n");
|
||||
} else if (keyword.equals("AllNonLegendaryCreatureNames")) {
|
||||
sbLong.append(getName()).append(" has all names of nonlegendary creature cards.\r\n");
|
||||
} else if (keyword.startsWith("IfReach")) {
|
||||
String k[] = keyword.split(":");
|
||||
sbLong.append(getName()).append(" can block ")
|
||||
.append(CardType.getPluralType(k[1]))
|
||||
.append(" as though it had reach.\r\n");
|
||||
} else if (keyword.startsWith("MayEffectFromOpeningHand")) {
|
||||
final String[] k = keyword.split(":");
|
||||
// need to get SpellDescription from Svar
|
||||
String desc = AbilityFactory.getMapParams(getSVar(k[1])).get("SpellDescription");
|
||||
sbLong.append(desc);
|
||||
} else if (keyword.startsWith("Saga")) {
|
||||
String k[] = keyword.split(":");
|
||||
String desc = "(As this Saga enters and after your draw step, "
|
||||
+ " add a lore counter. Sacrifice after " + Strings.repeat("I", Integer.valueOf(k[1])) + ".)";
|
||||
sbLong.append(desc);
|
||||
}
|
||||
else {
|
||||
if ((i != 0) && (sb.length() != 0)) {
|
||||
sb.append(", ");
|
||||
else {
|
||||
if ((i != 0) && (sb.length() != 0)) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(keyword);
|
||||
}
|
||||
if (sbLong.length() > 0) {
|
||||
sbLong.append("\r\n");
|
||||
}
|
||||
sb.append(keyword);
|
||||
}
|
||||
if (sbLong.length() > 0) {
|
||||
sbLong.append("\r\n");
|
||||
}
|
||||
|
||||
i++;
|
||||
i++;
|
||||
} catch (Exception e) {
|
||||
String msg = "Card:keywordToText: crash in Keyword parsing";
|
||||
Sentry.getContext().recordBreadcrumb(
|
||||
new BreadcrumbBuilder().setMessage(msg)
|
||||
.withData("Card", this.getName()).withData("Keyword", keyword).build()
|
||||
);
|
||||
|
||||
throw new RuntimeException("Error in Card " + this.getName() + " with Keyword " + keyword, e);
|
||||
}
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.append("\r\n");
|
||||
@@ -2249,6 +2245,16 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
updateBasicLandAbilities(list, state);
|
||||
}
|
||||
|
||||
// add Facedown abilities from Original state but only if this state is face down
|
||||
// need CardStateView#getState or might crash in StackOverflow
|
||||
if ((mana == null || mana == false) && isFaceDown() && state.getView().getState() == CardStateName.FaceDown) {
|
||||
for (SpellAbility sa : getState(CardStateName.Original).getNonManaAbilities()) {
|
||||
if (sa.isManifestUp() || sa.isMorphUp()) {
|
||||
list.add(sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (KeywordInterface kw : getUnhiddenKeywords(state)) {
|
||||
for (SpellAbility sa : kw.getAbilities()) {
|
||||
if (mana == null || mana == sa.isManaAbility()) {
|
||||
@@ -3232,11 +3238,15 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
}
|
||||
|
||||
public final void tap() {
|
||||
tap(false);
|
||||
}
|
||||
public final void tap(boolean attacker) {
|
||||
if (tapped) { return; }
|
||||
|
||||
// Run triggers
|
||||
final Map<String, Object> runParams = Maps.newTreeMap();
|
||||
runParams.put("Card", this);
|
||||
runParams.put("Attacker", attacker);
|
||||
getGame().getTriggerHandler().runTrigger(TriggerType.Taps, runParams, false);
|
||||
|
||||
setTapped(true);
|
||||
@@ -3395,6 +3405,15 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
return change;
|
||||
}
|
||||
|
||||
public final boolean hasChangedCardKeywords(final long timestamp) {
|
||||
return changedCardKeywords.containsKey(timestamp);
|
||||
}
|
||||
|
||||
public final void addChangedCardKeywordsInternal(final KeywordsChange change, final long timestamp) {
|
||||
changedCardKeywords.put(timestamp, change);
|
||||
updateKeywordsCache(currentState);
|
||||
}
|
||||
|
||||
// Hidden keywords will be left out
|
||||
public final Collection<KeywordInterface> getUnhiddenKeywords() {
|
||||
return getUnhiddenKeywords(currentState);
|
||||
@@ -3643,10 +3662,8 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
if (s.startsWith("HIDDEN")) {
|
||||
removeHiddenExtrinsicKeyword(s);
|
||||
}
|
||||
else {
|
||||
if (extrinsicKeyword.remove(s)) {
|
||||
currentState.getView().updateKeywords(this, currentState);
|
||||
}
|
||||
else if (extrinsicKeyword.remove(s)) {
|
||||
currentState.getView().updateKeywords(this, currentState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4812,6 +4829,15 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
// Note: This should only be called after state has been set to CardStateName.FaceDown,
|
||||
// so the below call should be valid since the state should have been created already.
|
||||
getState(CardStateName.FaceDown).setImageKey(ImageKeys.getTokenKey(image));
|
||||
if (!manifested) {
|
||||
// remove Manifest Up abilities from Original State
|
||||
CardState original = getState(CardStateName.Original);
|
||||
for (SpellAbility sa : original.getNonManaAbilities()) {
|
||||
if (sa.isManifestUp()) {
|
||||
original.removeSpellAbility(sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final void animateBestow() {
|
||||
@@ -5715,7 +5741,7 @@ public class Card extends GameEntity implements Comparable<Card> {
|
||||
public void setChangedCardKeywords(Map<Long, KeywordsChange> changedCardKeywords) {
|
||||
this.changedCardKeywords.clear();
|
||||
for (Entry<Long, KeywordsChange> entry : changedCardKeywords.entrySet()) {
|
||||
this.changedCardKeywords.put(entry.getKey(), entry.getValue());
|
||||
this.changedCardKeywords.put(entry.getKey(), entry.getValue().copy(this, true));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@ import forge.game.trigger.TriggerType;
|
||||
public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
|
||||
private Table<Card, GameEntity, Integer> dataMap = HashBasedTable.create();
|
||||
|
||||
public CardDamageMap(Table<Card, GameEntity, Integer> damageMap) {
|
||||
this.putAll(damageMap);
|
||||
}
|
||||
|
||||
public CardDamageMap() {
|
||||
}
|
||||
|
||||
public void triggerPreventDamage(boolean isCombat) {
|
||||
for (Map.Entry<GameEntity, Map<Card, Integer>> e : this.columnMap().entrySet()) {
|
||||
int sum = 0;
|
||||
|
||||
@@ -25,8 +25,6 @@ import forge.card.mana.ManaCost;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementHandler;
|
||||
@@ -95,7 +93,6 @@ public class CardFactory {
|
||||
out.setAttachedCards(in.getAttachedCards());
|
||||
out.setEntityAttachedTo(in.getEntityAttachedTo());
|
||||
|
||||
out.setClones(in.getClones());
|
||||
out.setCastSA(in.getCastSA());
|
||||
for (final Object o : in.getRemembered()) {
|
||||
out.addRemembered(o);
|
||||
@@ -431,6 +428,9 @@ public class CardFactory {
|
||||
|
||||
private static void readCardFace(Card c, ICardFace face) {
|
||||
|
||||
// Name first so Senty has the Card name
|
||||
c.setName(face.getName());
|
||||
|
||||
for (String r : face.getReplacements()) c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true));
|
||||
for (String s : face.getStaticAbilities()) c.addStaticAbility(s);
|
||||
for (String t : face.getTriggers()) c.addTrigger(TriggerHandler.parseTrigger(t, c, true));
|
||||
@@ -440,7 +440,6 @@ public class CardFactory {
|
||||
// keywords not before variables
|
||||
c.addIntrinsicKeywords(face.getKeywords(), false);
|
||||
|
||||
c.setName(face.getName());
|
||||
c.setManaCost(face.getManaCost());
|
||||
c.setText(face.getNonAbilityText());
|
||||
|
||||
@@ -618,6 +617,9 @@ public class CardFactory {
|
||||
}
|
||||
if (from.getRestrictions() != null) {
|
||||
to.setRestrictions((SpellAbilityRestriction) from.getRestrictions().copy());
|
||||
if (!lki) {
|
||||
to.getRestrictions().resetTurnActivations();
|
||||
}
|
||||
}
|
||||
if (from.getConditions() != null) {
|
||||
to.setConditions((SpellAbilityCondition) from.getConditions().copy());
|
||||
@@ -676,9 +678,6 @@ public class CardFactory {
|
||||
}
|
||||
|
||||
trig.setStackDescription(trig.toString());
|
||||
if (trig.getApi() == ApiType.Charm && !trig.isWrapper()) {
|
||||
CharmEffect.makeChoices(trig);
|
||||
}
|
||||
|
||||
WrappedAbility wrapperAbility = new WrappedAbility(t, trig, ((WrappedAbility) sa).getDecider());
|
||||
wrapperAbility.setTrigger(true);
|
||||
|
||||
@@ -25,7 +25,6 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import forge.GameCommand;
|
||||
import forge.card.*;
|
||||
import forge.card.mana.ManaAtom;
|
||||
import forge.card.mana.ManaCost;
|
||||
@@ -136,27 +135,27 @@ public class CardFactoryUtil {
|
||||
public static SpellAbility abilityMorphUp(final Card sourceCard, final String costStr, final boolean mega) {
|
||||
Cost cost = new Cost(costStr, true);
|
||||
String costDesc = cost.toString();
|
||||
// get rid of the ": " at the end
|
||||
costDesc = costDesc.substring(0, costDesc.length() - 2);
|
||||
|
||||
StringBuilder sbCost = new StringBuilder(mega ? "Megamorph" : "Morph");
|
||||
sbCost.append(" ");
|
||||
if (!cost.isOnlyManaCost()) {
|
||||
costDesc = "—" + costDesc;
|
||||
sbCost.append("— ");
|
||||
}
|
||||
// get rid of the ": " at the end
|
||||
sbCost.append(costDesc.substring(0, costDesc.length() - 2));
|
||||
|
||||
String ab = "ST$ SetState | Cost$ " + costStr + " | CostDesc$ Morph" + costDesc
|
||||
+ " | MorphUp$ True"
|
||||
+ " | ConditionDefined$ Self | ConditionPresent$ Card.faceDown"
|
||||
+ " | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its morph cost.)";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ST$ SetState | Cost$ ").append(costStr).append(" | CostDesc$ ").append(sbCost);
|
||||
sb.append(" | MorphUp$ True | Secondary$ True | IsPresent$ Card.Self+faceDown");
|
||||
if (mega) {
|
||||
ab += " | Mega$ True";
|
||||
sb.append(" | Mega$ True");
|
||||
}
|
||||
sb.append(" | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its morph cost.)");
|
||||
|
||||
final SpellAbility morphUp = AbilityFactory.getAbility(ab, sourceCard);
|
||||
final SpellAbility morphUp = AbilityFactory.getAbility(sb.toString(), sourceCard);
|
||||
|
||||
final StringBuilder sbStack = new StringBuilder();
|
||||
sbStack.append(sourceCard.getName()).append(" - turn this card face up.");
|
||||
morphUp.setStackDescription(sbStack.toString());
|
||||
morphUp.setIsMorphUp(true);
|
||||
|
||||
return morphUp;
|
||||
}
|
||||
@@ -166,18 +165,17 @@ public class CardFactoryUtil {
|
||||
String costDesc = manaCost.toString();
|
||||
|
||||
// Cost need to be set later
|
||||
String ab = "ST$ SetState | Cost$ 0 | CostDesc$ Unmanifest " + costDesc
|
||||
+ " | ManifestUp$ True"
|
||||
+ " | ConditionDefined$ Self | ConditionPresent$ Card.faceDown+manifested"
|
||||
+ " | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its mana cost.)";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ST$ SetState | Cost$ 0 | CostDesc$ Unmanifest ").append(costDesc);
|
||||
sb.append(" | ManifestUp$ True | Secondary$ True | IsPresent$ Card.Self+faceDown+manifested");
|
||||
sb.append(" | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its mana cost.)");
|
||||
|
||||
final SpellAbility manifestUp = AbilityFactory.getAbility(ab, sourceCard);
|
||||
final SpellAbility manifestUp = AbilityFactory.getAbility(sb.toString(), sourceCard);
|
||||
manifestUp.setPayCosts(new Cost(manaCost, true));
|
||||
|
||||
final StringBuilder sbStack = new StringBuilder();
|
||||
sbStack.append(sourceCard.getName()).append(" - turn this card face up.");
|
||||
manifestUp.setStackDescription(sbStack.toString());
|
||||
manifestUp.setIsManifestUp(true);
|
||||
|
||||
return manifestUp;
|
||||
}
|
||||
@@ -2100,7 +2098,7 @@ public class CardFactoryUtil {
|
||||
String abStr = "DB$ PutCounter | Defined$ Self | CounterType$ " + splitkw[1]
|
||||
+ " | ETB$ True | CounterNum$ " + amount;
|
||||
|
||||
if (!StringUtils.isNumeric(amount)) {
|
||||
if (!StringUtils.isNumeric(amount) && card.hasSVar(amount)) {
|
||||
abStr += " | References$ " + amount;
|
||||
}
|
||||
|
||||
@@ -3453,12 +3451,10 @@ public class CardFactoryUtil {
|
||||
sb.append(m);
|
||||
sb.append(" (").append(inst.getReminderText()).append(")");
|
||||
|
||||
if ("Sunburst".equals(m)) {
|
||||
card.setSVar(m, "Count$Converge");
|
||||
}
|
||||
|
||||
final ReplacementEffect re = makeEtbCounter(sb.toString(), card, intrinsic);
|
||||
|
||||
if ("Sunburst".equals(m)) {
|
||||
re.getOverridingAbility().setSVar("Sunburst", "Count$Converge");
|
||||
}
|
||||
inst.addReplacement(re);
|
||||
} else if (keyword.equals("Rebound")) {
|
||||
String repeffstr = "Event$ Moved | ValidCard$ Card.Self+wasCastFromHand+YouOwn+YouCtrl "
|
||||
@@ -3513,6 +3509,19 @@ public class CardFactoryUtil {
|
||||
String sb = "etbCounter:LORE:1:no Condition:no desc";
|
||||
final ReplacementEffect re = makeEtbCounter(sb, card, intrinsic);
|
||||
|
||||
inst.addReplacement(re);
|
||||
} else if (keyword.equals("Sunburst")) {
|
||||
// Rule 702.43a If this object is entering the battlefield as a creature,
|
||||
// ignoring any type-changing effects that would affect it
|
||||
CounterType t = card.isCreature() ? CounterType.P1P1 : CounterType.CHARGE;
|
||||
|
||||
StringBuilder sb = new StringBuilder("etbCounter:");
|
||||
sb.append(t).append(":Sunburst:no Condition:");
|
||||
sb.append("Sunburst (").append(inst.getReminderText()).append(")");
|
||||
|
||||
final ReplacementEffect re = makeEtbCounter(sb.toString(), card, intrinsic);
|
||||
re.getOverridingAbility().setSVar("Sunburst", "Count$Converge");
|
||||
|
||||
inst.addReplacement(re);
|
||||
} else if (keyword.equals("Totem armor")) {
|
||||
String repeffstr = "Event$ Destroy | ActiveZones$ Battlefield | ValidCard$ Card.EnchantedBy"
|
||||
@@ -3704,13 +3713,24 @@ public class CardFactoryUtil {
|
||||
final String[] k = keyword.split(":");
|
||||
final String magnitude = k[1];
|
||||
final String manacost = k[2];
|
||||
final String reduceCost = k.length > 3 ? k[3] : null;
|
||||
|
||||
Set<String> references = Sets.newHashSet();
|
||||
|
||||
String desc = "Adapt " + magnitude;
|
||||
|
||||
String effect = "AB$ PutCounter | Cost$ " + manacost + " | ConditionPresent$ "
|
||||
+ "Card.Self+counters_EQ0_P1P1 | Adapt$ True | CounterNum$ " + magnitude
|
||||
String effect = "AB$ PutCounter | Cost$ " + manacost + " | Adapt$ True | CounterNum$ " + magnitude
|
||||
+ " | CounterType$ P1P1 | StackDescription$ SpellDescription";
|
||||
|
||||
if (reduceCost != null) {
|
||||
effect += "| ReduceCost$ " + reduceCost;
|
||||
references.add(reduceCost);
|
||||
desc += ". This ability costs {1} less to activate for each instant and sorcery card in your graveyard.";
|
||||
}
|
||||
if (!references.isEmpty()) {
|
||||
effect += "| References$ " + TextUtil.join(references, ",");
|
||||
}
|
||||
|
||||
effect += "| SpellDescription$ " + desc + " (" + inst.getReminderText() + ")";
|
||||
|
||||
final SpellAbility sa = AbilityFactory.getAbility(effect, card);
|
||||
@@ -4037,22 +4057,12 @@ public class CardFactoryUtil {
|
||||
final String[] k = keyword.split(":");
|
||||
|
||||
inst.addSpellAbility(abilityMorphDown(card));
|
||||
|
||||
CardState state = card.getState(CardStateName.FaceDown);
|
||||
state.setSVars(card.getSVars());
|
||||
KeywordInterface facedownKeyword = Keyword.getInstance("");
|
||||
facedownKeyword.addSpellAbility(abilityMorphUp(card, k[1], false));
|
||||
state.addIntrinsicKeywords(Lists.newArrayList(facedownKeyword));
|
||||
inst.addSpellAbility(abilityMorphUp(card, k[1], false));
|
||||
} else if (keyword.startsWith("Megamorph")){
|
||||
final String[] k = keyword.split(":");
|
||||
|
||||
inst.addSpellAbility(abilityMorphDown(card));
|
||||
|
||||
CardState state = card.getState(CardStateName.FaceDown);
|
||||
state.setSVars(card.getSVars());
|
||||
KeywordInterface facedownKeyword = Keyword.getInstance("");
|
||||
facedownKeyword.addSpellAbility(abilityMorphUp(card, k[1], true));
|
||||
state.addIntrinsicKeywords(Lists.newArrayList(facedownKeyword));
|
||||
inst.addSpellAbility(abilityMorphUp(card, k[1], true));
|
||||
} else if (keyword.startsWith("Multikicker")) {
|
||||
final String[] n = keyword.split(":");
|
||||
final SpellAbility sa = card.getFirstSpellAbility();
|
||||
@@ -4182,18 +4192,17 @@ public class CardFactoryUtil {
|
||||
|
||||
String effect = "AB$ PutCounter | Cost$ " + manacost + " ExileFromGrave<1/CARDNAME> " +
|
||||
"| ActivationZone$ Graveyard | ValidTgts$ Creature | CounterType$ P1P1 " +
|
||||
"| CounterNum$ ScavengeX | SorcerySpeed$ True | References$ ScavengeX " +
|
||||
"| CounterNum$ ScavengeX | SorcerySpeed$ True " +
|
||||
"| PrecostDesc$ Scavenge | CostDesc$ " + ManaCostParser.parse(manacost) +
|
||||
"| SpellDescription$ (" + inst.getReminderText() + ")";
|
||||
|
||||
card.setSVar("ScavengeX", "Count$CardPower");
|
||||
|
||||
final SpellAbility sa = AbilityFactory.getAbility(effect, card);
|
||||
sa.setSVar("ScavengeX", "Count$CardPower");
|
||||
sa.setIntrinsic(intrinsic);
|
||||
|
||||
sa.setTemporary(!intrinsic);
|
||||
inst.addSpellAbility(sa);
|
||||
|
||||
|
||||
} else if (keyword.startsWith("Spectacle")) {
|
||||
final String[] k = keyword.split(":");
|
||||
final Cost cost = new Cost(k[1], false);
|
||||
@@ -4211,28 +4220,6 @@ public class CardFactoryUtil {
|
||||
newSA.setTemporary(!intrinsic);
|
||||
inst.addSpellAbility(newSA);
|
||||
|
||||
} else if (keyword.equals("Sunburst") && intrinsic) {
|
||||
final GameCommand sunburstCIP = new GameCommand() {
|
||||
private static final long serialVersionUID = 1489845860231758299L;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
CounterType t = card.isCreature() ? CounterType.P1P1 : CounterType.CHARGE;
|
||||
card.addCounter(t, card.getSunburstValue(), card.getController(), true);
|
||||
}
|
||||
};
|
||||
|
||||
final GameCommand sunburstLP = new GameCommand() {
|
||||
private static final long serialVersionUID = -7564420917490677427L;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
card.setSunburstValue(0);
|
||||
}
|
||||
};
|
||||
|
||||
card.addComesIntoPlayCommand(sunburstCIP);
|
||||
card.addLeavesPlayCommand(sunburstLP);
|
||||
} else if (keyword.startsWith("Surge")) {
|
||||
final String[] k = keyword.split(":");
|
||||
final Cost surgeCost = new Cost(k[1], false);
|
||||
|
||||
@@ -1239,7 +1239,7 @@ public class CardProperty {
|
||||
} else if (property.startsWith("greatestPower")) {
|
||||
CardCollectionView cards = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES);
|
||||
if (property.contains("ControlledBy")) {
|
||||
FCollectionView<Player> p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], null);
|
||||
FCollectionView<Player> p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], spellAbility);
|
||||
cards = CardLists.filterControlledBy(cards, p);
|
||||
if (!cards.contains(card)) {
|
||||
return false;
|
||||
|
||||
@@ -43,6 +43,9 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.sentry.Sentry;
|
||||
import io.sentry.event.BreadcrumbBuilder;
|
||||
|
||||
public class CardState extends GameObject {
|
||||
private String name = "";
|
||||
private CardType type = new CardType();
|
||||
@@ -69,6 +72,10 @@ public class CardState extends GameObject {
|
||||
private final CardStateView view;
|
||||
private final Card card;
|
||||
|
||||
public CardState(Card card, CardStateName name) {
|
||||
this(card.getView().createAlternateState(name), card);
|
||||
}
|
||||
|
||||
public CardState(CardStateView view0, Card card0) {
|
||||
view = view0;
|
||||
card = card0;
|
||||
@@ -209,7 +216,19 @@ public class CardState extends GameObject {
|
||||
if (s.trim().length() == 0) {
|
||||
return null;
|
||||
}
|
||||
KeywordInterface inst = intrinsicKeywords.add(s);
|
||||
KeywordInterface inst = null;
|
||||
try {
|
||||
inst = intrinsicKeywords.add(s);
|
||||
} catch (Exception e) {
|
||||
String msg = "CardState:addIntrinsicKeyword: failed to parse Keyword";
|
||||
Sentry.getContext().recordBreadcrumb(
|
||||
new BreadcrumbBuilder().setMessage(msg)
|
||||
.withData("Card", card.getName()).withData("Keyword", s).build()
|
||||
);
|
||||
|
||||
//rethrow
|
||||
throw new RuntimeException("Error in Keyword " + s + " for card " + card.getName(), e);
|
||||
}
|
||||
if (inst != null && initTraits) {
|
||||
inst.createTraits(card, true);
|
||||
}
|
||||
|
||||
@@ -269,7 +269,6 @@ public final class CardUtil {
|
||||
newCopy.setAttachedCards(in.getAttachedCards());
|
||||
newCopy.setEntityAttachedTo(in.getEntityAttachedTo());
|
||||
|
||||
newCopy.setClones(in.getClones());
|
||||
newCopy.setHaunting(in.getHaunting());
|
||||
newCopy.setCopiedPermanent(in.getCopiedPermanent());
|
||||
for (final Card haunter : in.getHauntedBy()) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Table;
|
||||
|
||||
import forge.game.Game;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
@@ -45,4 +46,39 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
|
||||
game.getTriggerHandler().runTrigger(TriggerType.ChangesZoneAll, runParams, false);
|
||||
}
|
||||
}
|
||||
|
||||
public CardCollection filterCards(Iterable<ZoneType> origin, ZoneType destination, String valid, Card host, SpellAbility sa) {
|
||||
CardCollection allCards = new CardCollection();
|
||||
if (destination != null) {
|
||||
if (!containsColumn(destination)) {
|
||||
return allCards;
|
||||
}
|
||||
}
|
||||
if (origin != null) {
|
||||
for (ZoneType z : origin) {
|
||||
if (containsRow(z)) {
|
||||
if (destination != null) {
|
||||
allCards.addAll(row(z).get(destination));
|
||||
} else {
|
||||
for (CardCollection c : row(z).values()) {
|
||||
allCards.addAll(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (destination != null) {
|
||||
for (CardCollection c : column(destination).values()) {
|
||||
allCards.addAll(c);
|
||||
}
|
||||
} else {
|
||||
for (CardCollection c : values()) {
|
||||
allCards.addAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (valid != null) {
|
||||
allCards = CardLists.getValidCards(allCards, valid.split(","), host.getController(), host, sa);
|
||||
}
|
||||
return allCards;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,6 +816,7 @@ public class Combat {
|
||||
}
|
||||
|
||||
preventMap.triggerPreventDamage(true);
|
||||
preventMap.clear();
|
||||
// This was deeper before, but that resulted in the stack entry acting like before.
|
||||
|
||||
// Run the trigger to deal combat damage once
|
||||
|
||||
@@ -366,10 +366,9 @@ public class CostAdjustment {
|
||||
if (manaCost.toString().equals("{0}")) {
|
||||
return 0;
|
||||
}
|
||||
final Map<String, String> params = staticAbility.getMapParams();
|
||||
final Card hostCard = staticAbility.getHostCard();
|
||||
final Card card = sa.getHostCard();
|
||||
final String amount = params.get("Amount");
|
||||
final String amount = staticAbility.getParam("Amount");
|
||||
|
||||
if (!checkRequirement(sa, staticAbility)) {
|
||||
return 0;
|
||||
@@ -380,14 +379,16 @@ public class CostAdjustment {
|
||||
value = CardFactoryUtil.xCount(card, hostCard.getSVar(amount));
|
||||
} else if ("Undaunted".equals(amount)) {
|
||||
value = card.getController().getOpponents().size();
|
||||
} else if (staticAbility.hasParam("Relative")) {
|
||||
value = AbilityUtils.calculateAmount(hostCard, amount, sa);
|
||||
} else {
|
||||
value = AbilityUtils.calculateAmount(hostCard, amount, staticAbility);
|
||||
}
|
||||
|
||||
if (!params.containsKey("Cost") && ! params.containsKey("Color")) {
|
||||
if (!staticAbility.hasParam("Cost") && ! staticAbility.hasParam("Color")) {
|
||||
int minMana = 0;
|
||||
if (params.containsKey("MinMana")) {
|
||||
minMana = Integer.valueOf(params.get("MinMana"));
|
||||
if (staticAbility.hasParam("MinMana")) {
|
||||
minMana = Integer.valueOf(staticAbility.getParam("MinMana"));
|
||||
}
|
||||
|
||||
final int maxReduction = Math.max(0, manaCost.getConvertedManaCost() - minMana);
|
||||
@@ -395,7 +396,7 @@ public class CostAdjustment {
|
||||
return Math.min(value, maxReduction);
|
||||
}
|
||||
} else {
|
||||
final String color = params.containsKey("Cost") ? params.get("Cost") : params.get("Color");
|
||||
final String color = staticAbility.getParamOrDefault("Cost", staticAbility.getParam("Color"));
|
||||
int sumGeneric = 0;
|
||||
// might be problematic for wierd hybrid combinations
|
||||
for (final String cost : color.split(" ")) {
|
||||
|
||||
@@ -23,7 +23,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
/**
|
||||
* The Class CostPayLife.
|
||||
* The Class CostDamage.
|
||||
*/
|
||||
public class CostDamage extends CostPart {
|
||||
|
||||
@@ -74,6 +74,8 @@ public class CostDamage extends CostPart {
|
||||
preventMap.triggerPreventDamage(false);
|
||||
damageMap.triggerDamageDoneOnce(false, sa);
|
||||
|
||||
preventMap.clear();
|
||||
damageMap.clear();
|
||||
return decision.c > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The Class CostPayLife.
|
||||
* The Class CostDraw.
|
||||
*/
|
||||
public class CostDraw extends CostPart {
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package forge.game.cost;
|
||||
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
@@ -57,17 +56,6 @@ public class CostPayLife extends CostPart {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see forge.card.cost.CostPart#refund(forge.Card)
|
||||
*/
|
||||
@Override
|
||||
public final void refund(final Card source) {
|
||||
// Really should be activating player
|
||||
source.getController().payLife(this.paidAmount * -1, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
|
||||
@@ -8,6 +8,8 @@ import java.util.Iterator;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
|
||||
import forge.game.card.Card;
|
||||
|
||||
public class KeywordCollection implements Iterable<String>, Serializable {
|
||||
private static final long serialVersionUID = -2882986558147844702L;
|
||||
|
||||
@@ -151,6 +153,12 @@ public class KeywordCollection implements Iterable<String>, Serializable {
|
||||
return map.get(keyword);
|
||||
}
|
||||
|
||||
public void setHostCard(final Card host) {
|
||||
for (KeywordInterface k : map.values()) {
|
||||
k.setHostCard(host);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
return new Iterator<String>() {
|
||||
|
||||
@@ -202,7 +202,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
|
||||
public Collection<StaticAbility> getStaticAbilities() {
|
||||
return staticAbilities;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see forge.game.keyword.KeywordInterface#copy()
|
||||
@@ -233,7 +233,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
|
||||
|
||||
return result;
|
||||
} catch (final Exception ex) {
|
||||
throw new RuntimeException("KeywordInstance : clone() error, " + ex);
|
||||
throw new RuntimeException("KeywordInstance : clone() error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,4 +252,26 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
|
||||
public boolean redundant(Collection<KeywordInterface> list) {
|
||||
return !list.isEmpty() && keyword.isMultipleRedundant;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.game.keyword.KeywordInterface#setHostCard(forge.game.card.Card)
|
||||
*/
|
||||
@Override
|
||||
public void setHostCard(Card host) {
|
||||
for (SpellAbility sa : this.abilities) {
|
||||
sa.setHostCard(host);
|
||||
}
|
||||
|
||||
for (Trigger tr : this.triggers) {
|
||||
tr.setHostCard(host);
|
||||
}
|
||||
|
||||
for (ReplacementEffect re : this.replacements) {
|
||||
re.setHostCard(host);
|
||||
}
|
||||
|
||||
for (StaticAbility sa : this.staticAbilities) {
|
||||
sa.setHostCard(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public interface KeywordInterface extends Cloneable {
|
||||
public void addSpellAbility(final SpellAbility s);
|
||||
public void addStaticAbility(final StaticAbility st);
|
||||
|
||||
public void setHostCard(final Card host);
|
||||
|
||||
/**
|
||||
* @return the triggers
|
||||
|
||||
@@ -30,12 +30,11 @@ import forge.game.card.Card;
|
||||
* </p>
|
||||
*
|
||||
* @author Forge
|
||||
* @version $Id: KeywordsChange.java 27095 2014-08-17 07:32:24Z elcnesh $
|
||||
*/
|
||||
public class KeywordsChange {
|
||||
private final KeywordCollection keywords = new KeywordCollection();
|
||||
private final List<KeywordInterface> removeKeywordInterfaces = Lists.newArrayList();
|
||||
private final List<String> removeKeywords = Lists.newArrayList();
|
||||
public class KeywordsChange implements Cloneable {
|
||||
private KeywordCollection keywords = new KeywordCollection();
|
||||
private List<KeywordInterface> removeKeywordInterfaces = Lists.newArrayList();
|
||||
private List<String> removeKeywords = Lists.newArrayList();
|
||||
private boolean removeAllKeywords;
|
||||
private boolean removeIntrinsicKeywords;
|
||||
|
||||
@@ -63,7 +62,7 @@ public class KeywordsChange {
|
||||
this.removeAllKeywords = removeAll;
|
||||
this.removeIntrinsicKeywords = removeIntrinsic;
|
||||
}
|
||||
|
||||
|
||||
public KeywordsChange(
|
||||
final Collection<KeywordInterface> keywordList,
|
||||
final Collection<KeywordInterface> removeKeywordInterfaces,
|
||||
@@ -172,4 +171,49 @@ public class KeywordsChange {
|
||||
removeIntrinsicKeywords = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setHostCard(final Card host) {
|
||||
keywords.setHostCard(host);
|
||||
for (KeywordInterface k : removeKeywordInterfaces) {
|
||||
k.setHostCard(host);
|
||||
}
|
||||
}
|
||||
|
||||
public KeywordsChange copy(final Card host, final boolean lki) {
|
||||
try {
|
||||
KeywordsChange result = (KeywordsChange)super.clone();
|
||||
|
||||
result.keywords = new KeywordCollection();
|
||||
for (KeywordInterface ki : this.keywords.getValues()) {
|
||||
result.keywords.insert(ki.copy(host, lki));
|
||||
}
|
||||
|
||||
result.removeKeywords = Lists.newArrayList(removeKeywords);
|
||||
|
||||
result.removeKeywordInterfaces = Lists.newArrayList();
|
||||
for (KeywordInterface ki : this.removeKeywordInterfaces) {
|
||||
removeKeywordInterfaces.add(ki.copy(host, lki));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (final Exception ex) {
|
||||
throw new RuntimeException("KeywordsChange : clone() error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<+");
|
||||
sb.append(this.keywords);
|
||||
sb.append("|-");
|
||||
sb.append(this.removeKeywordInterfaces);
|
||||
sb.append("|-");
|
||||
sb.append(this.removeKeywords);
|
||||
sb.append(">");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
|
||||
if (canAttack) {
|
||||
if (shouldTapForAttack) {
|
||||
attacker.tap();
|
||||
attacker.tap(true);
|
||||
}
|
||||
} else {
|
||||
combat.removeFromCombat(attacker);
|
||||
|
||||
@@ -515,14 +515,15 @@ public class Player extends GameEntity implements Comparable<Player> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lifePayment <= 0)
|
||||
return true;
|
||||
loseLife(lifePayment);
|
||||
|
||||
// rule 118.8
|
||||
if (life >= lifePayment) {
|
||||
return (loseLife(lifePayment) > 0);
|
||||
}
|
||||
return false;
|
||||
// Run triggers
|
||||
final Map<String, Object> runParams = Maps.newHashMap();
|
||||
runParams.put("Player", this);
|
||||
runParams.put("LifeAmount", lifePayment);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.PayLife, runParams, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public final boolean canPayEnergy(final int energyPayment) {
|
||||
@@ -1261,42 +1262,6 @@ public class Player extends GameEntity implements Comparable<Player> {
|
||||
return drawCards(1);
|
||||
}
|
||||
|
||||
public void scry(final int numScry, SpellAbility cause) {
|
||||
final CardCollection topN = new CardCollection(this.getCardsIn(ZoneType.Library, numScry));
|
||||
|
||||
if (topN.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ImmutablePair<CardCollection, CardCollection> lists = getController().arrangeForScry(topN);
|
||||
final CardCollection toTop = lists.getLeft();
|
||||
final CardCollection toBottom = lists.getRight();
|
||||
|
||||
int numToBottom = 0;
|
||||
int numToTop = 0;
|
||||
|
||||
if (toBottom != null) {
|
||||
for(Card c : toBottom) {
|
||||
getGame().getAction().moveToBottomOfLibrary(c, cause, null);
|
||||
numToBottom++;
|
||||
}
|
||||
}
|
||||
|
||||
if (toTop != null) {
|
||||
Collections.reverse(toTop); // the last card in list will become topmost in library, have to revert thus.
|
||||
for(Card c : toTop) {
|
||||
getGame().getAction().moveToLibrary(c, cause, null);
|
||||
numToTop++;
|
||||
}
|
||||
}
|
||||
|
||||
getGame().fireEvent(new GameEventScry(this, numToTop, numToBottom));
|
||||
|
||||
final Map<String, Object> runParams = Maps.newHashMap();
|
||||
runParams.put("Player", this);
|
||||
getGame().getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
|
||||
}
|
||||
|
||||
public void surveil(int num, SpellAbility cause) {
|
||||
|
||||
final Map<String, Object> repParams = Maps.newHashMap();
|
||||
|
||||
@@ -13,8 +13,8 @@ public enum PlayerActionConfirmMode {
|
||||
ChangeZoneGeneral,
|
||||
BidLife,
|
||||
OptionalChoose,
|
||||
Tribute;
|
||||
Tribute,
|
||||
// Ripple;
|
||||
;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ public abstract class PlayerController {
|
||||
public Player getPlayer() { return player; }
|
||||
public LobbyPlayer getLobbyPlayer() { return lobbyPlayer; }
|
||||
|
||||
public void tempShowCards(final Iterable<Card> cards) { } // show cards in UI until ended
|
||||
public void endTempShowCards() { }
|
||||
|
||||
public final SpellAbility getAbilityToPlay(final Card hostCard, final List<SpellAbility> abilities) { return getAbilityToPlay(hostCard, abilities, null); }
|
||||
public abstract SpellAbility getAbilityToPlay(Card hostCard, List<SpellAbility> abilities, ITriggerEvent triggerEvent);
|
||||
|
||||
@@ -111,7 +114,8 @@ public abstract class PlayerController {
|
||||
public abstract SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
|
||||
Map<String, Object> params);
|
||||
|
||||
public abstract <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer);
|
||||
public abstract <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer);
|
||||
public abstract <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2, boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer);
|
||||
|
||||
public abstract boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message);
|
||||
public abstract boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode bidlife, String string, int bid, Player winner);
|
||||
@@ -239,7 +243,7 @@ public abstract class PlayerController {
|
||||
// better to have this odd method than those if playerType comparison in ChangeZone
|
||||
public abstract Card chooseSingleCardForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, DelayedReveal delayedReveal, String selectPrompt, boolean isOptional, Player decider);
|
||||
|
||||
public abstract List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, DelayedReveal delayedReveal, String selectPrompt, Player decider);
|
||||
public abstract List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max, DelayedReveal delayedReveal, String selectPrompt, Player decider);
|
||||
|
||||
public abstract void autoPassCancel();
|
||||
|
||||
@@ -260,4 +264,6 @@ public abstract class PlayerController {
|
||||
}
|
||||
|
||||
public abstract List<OptionalCostValue> chooseOptionalCosts(SpellAbility choosen, List<OptionalCostValue> optionalCostValues);
|
||||
|
||||
public abstract boolean confirmMulliganScry(final Player p);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public class ReplaceMoved extends ReplacementEffect {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (zt.equals(ZoneType.Battlefield) && getHostCard().equals(affected)) {
|
||||
if (zt.equals(ZoneType.Battlefield) && getHostCard().equals(affected) && !hasParam("BypassEtbCheck")) {
|
||||
// would be an etb replacement effect that enters the battlefield
|
||||
Card lki = CardUtil.getLKICopy(affected);
|
||||
lki.setLastKnownZone(lki.getController().getZone(zt));
|
||||
|
||||
@@ -39,7 +39,7 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
|
||||
/** The ID. */
|
||||
private int id;
|
||||
|
||||
private ReplacementLayer layer = ReplacementLayer.None;
|
||||
private ReplacementLayer layer = ReplacementLayer.Other;
|
||||
|
||||
/** The has run. */
|
||||
private boolean hasRun = false;
|
||||
|
||||
@@ -8,8 +8,7 @@ package forge.game.replacement;
|
||||
public enum ReplacementLayer {
|
||||
Control,
|
||||
Copy,
|
||||
Other,
|
||||
None;
|
||||
Other;
|
||||
|
||||
/**
|
||||
* TODO: Write javadoc for this method.
|
||||
|
||||
@@ -144,6 +144,8 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
|
||||
if (lkicheck) {
|
||||
game.getAction().checkStaticAbilities(false);
|
||||
game.getTracker().unfreeze();
|
||||
// reset owner for lki
|
||||
card.setController(null, 0);
|
||||
}
|
||||
|
||||
if (!(isInstant || activator.canCastSorcery() || flash || getRestrictions().isInstantSpeed()
|
||||
|
||||
@@ -107,8 +107,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
private boolean spectacle = false;
|
||||
private boolean offering = false;
|
||||
private boolean emerge = false;
|
||||
private boolean morphup = false;
|
||||
private boolean manifestUp = false;
|
||||
private boolean cumulativeupkeep = false;
|
||||
private boolean outlast = false;
|
||||
private boolean blessing = false;
|
||||
@@ -372,22 +370,15 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
public boolean isAbility() { return true; }
|
||||
|
||||
public boolean isMorphUp() {
|
||||
return morphup;
|
||||
return this.hasParam("MorphUp");
|
||||
}
|
||||
|
||||
public boolean isCastFaceDown() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public final void setIsMorphUp(final boolean b) {
|
||||
morphup = b;
|
||||
}
|
||||
|
||||
public boolean isManifestUp() {
|
||||
return manifestUp;
|
||||
}
|
||||
public final void setIsManifestUp(final boolean b) {
|
||||
manifestUp = b;
|
||||
return hasParam("ManifestUp");
|
||||
}
|
||||
|
||||
public boolean isCycling() {
|
||||
@@ -884,6 +875,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
clone.manaPart = new AbilityManaPart(host, mapParams);
|
||||
}
|
||||
|
||||
// need to copy the damage tables
|
||||
if (damageMap != null) {
|
||||
clone.damageMap = new CardDamageMap(damageMap);
|
||||
}
|
||||
if (preventMap != null) {
|
||||
clone.preventMap = new CardDamageMap(preventMap);
|
||||
}
|
||||
|
||||
// clear maps for copy, the values will be added later
|
||||
clone.additionalAbilities = Maps.newHashMap();
|
||||
clone.additionalAbilityLists = Maps.newHashMap();
|
||||
|
||||
@@ -336,9 +336,10 @@ public final class StaticAbilityContinuous {
|
||||
final String colors = params.get("AddColor");
|
||||
if (colors.equals("ChosenColor")) {
|
||||
addColors = CardUtil.getShortColorsString(hostCard.getChosenColors());
|
||||
} else if (colors.equals("All")) {
|
||||
addColors = "W U B R G";
|
||||
} else {
|
||||
addColors = CardUtil.getShortColorsString(new ArrayList<String>(Arrays.asList(colors.split(
|
||||
" & "))));
|
||||
addColors = CardUtil.getShortColorsString(Arrays.asList(colors.split(" & ")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,9 +347,10 @@ public final class StaticAbilityContinuous {
|
||||
final String colors = params.get("SetColor");
|
||||
if (colors.equals("ChosenColor")) {
|
||||
addColors = CardUtil.getShortColorsString(hostCard.getChosenColors());
|
||||
} else if (colors.equals("All")) {
|
||||
addColors = "W U B R G";
|
||||
} else {
|
||||
addColors = CardUtil.getShortColorsString(new ArrayList<String>(Arrays.asList(
|
||||
colors.split(" & "))));
|
||||
addColors = CardUtil.getShortColorsString(Arrays.asList(colors.split(" & ")));
|
||||
}
|
||||
se.setOverwriteColors(true);
|
||||
}
|
||||
|
||||
@@ -206,15 +206,20 @@ public abstract class Trigger extends TriggerReplacementBase {
|
||||
} else {
|
||||
saDesc = sa.getDescription();
|
||||
}
|
||||
// string might have leading whitespace
|
||||
saDesc = saDesc.trim();
|
||||
if (!saDesc.isEmpty()) {
|
||||
// string might have leading whitespace
|
||||
saDesc = saDesc.trim();
|
||||
saDesc = saDesc.substring(0, 1).toLowerCase() + saDesc.substring(1);
|
||||
// in case sa starts with CARDNAME, dont lowercase it
|
||||
if (!saDesc.startsWith(sa.getHostCard().getName())) {
|
||||
saDesc = saDesc.substring(0, 1).toLowerCase() + saDesc.substring(1);
|
||||
}
|
||||
if (saDesc.contains("ORIGINALHOST") && sa.getOriginalHost() != null) {
|
||||
saDesc = TextUtil.fastReplace(saDesc, "ORIGINALHOST", sa.getOriginalHost().getName());
|
||||
}
|
||||
result = TextUtil.fastReplace(result, "ABILITY", saDesc);
|
||||
} else {
|
||||
saDesc = "<take no action>"; // printed in case nothing is chosen for the ability (e.g. Charm with Up to X)
|
||||
}
|
||||
result = TextUtil.fastReplace(result, "ABILITY", saDesc);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Forge: Play Magic: the Gathering.
|
||||
* Copyright (C) 2011 Forge Team
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package forge.game.trigger;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import forge.game.card.Card;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Trigger_Evolved class.
|
||||
* </p>
|
||||
*
|
||||
* @author Forge
|
||||
*/
|
||||
public class TriggerAdapt extends Trigger {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Constructor for Trigger_Evolved.
|
||||
* </p>
|
||||
*
|
||||
* @param params
|
||||
* a {@link java.util.HashMap} object.
|
||||
* @param host
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param intrinsic
|
||||
* the intrinsic
|
||||
*/
|
||||
public TriggerAdapt(final Map<String, String> params, final Card host, final boolean intrinsic) {
|
||||
super(params, host, intrinsic);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final boolean performTest(final Map<String, Object> runParams2) {
|
||||
final Card sac = (Card) runParams2.get("Card");
|
||||
if (hasParam("ValidCard")) {
|
||||
if (!sac.isValid(getParam("ValidCard").split(","), getHostCard().getController(),
|
||||
getHostCard(), null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final void setTriggeringObjects(final SpellAbility sa) {
|
||||
sa.setTriggeringObject("Card", getRunParams().get("Card"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImportantStackObjects(SpellAbility sa) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Adapt: ").append(sa.getTriggeringObject("Card"));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -42,48 +42,19 @@ public class TriggerChangesZoneAll extends Trigger {
|
||||
}
|
||||
|
||||
private CardCollection filterCards(CardZoneTable table) {
|
||||
CardCollection allCards = new CardCollection();
|
||||
ZoneType destination = null;
|
||||
List<ZoneType> origin = null;
|
||||
|
||||
if (hasParam("Destination")) {
|
||||
if (!getParam("Destination").equals("Any")) {
|
||||
destination = ZoneType.valueOf(getParam("Destination"));
|
||||
if (!table.containsColumn(destination)) {
|
||||
return allCards;
|
||||
}
|
||||
}
|
||||
if (hasParam("Destination") && !getParam("Destination").equals("Any")) {
|
||||
destination = ZoneType.valueOf(getParam("Destination"));
|
||||
}
|
||||
|
||||
if (hasParam("Origin") && !getParam("Origin").equals("Any")) {
|
||||
if (getParam("Origin") == null) {
|
||||
return allCards;
|
||||
}
|
||||
final List<ZoneType> origin = ZoneType.listValueOf(getParam("Origin"));
|
||||
for (ZoneType z : origin) {
|
||||
if (table.containsRow(z)) {
|
||||
if (destination != null) {
|
||||
allCards.addAll(table.row(z).get(destination));
|
||||
} else {
|
||||
for (CardCollection c : table.row(z).values()) {
|
||||
allCards.addAll(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (destination != null) {
|
||||
for (CardCollection c : table.column(destination).values()) {
|
||||
allCards.addAll(c);
|
||||
}
|
||||
} else {
|
||||
for (CardCollection c : table.values()) {
|
||||
allCards.addAll(c);
|
||||
}
|
||||
origin = ZoneType.listValueOf(getParam("Origin"));
|
||||
}
|
||||
|
||||
if (hasParam("ValidCards")) {
|
||||
allCards = CardLists.getValidCards(allCards, getParam("ValidCards").split(","),
|
||||
getHostCard().getController(), getHostCard(), null);
|
||||
}
|
||||
return allCards;
|
||||
final String valid = this.getParamOrDefault("ValidCards", null);
|
||||
|
||||
return table.filterCards(origin, destination, valid, getHostCard(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package forge.game.trigger;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import forge.game.card.Card;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
@@ -26,7 +28,6 @@ import forge.game.spellability.SpellAbility;
|
||||
* </p>
|
||||
*
|
||||
* @author Forge
|
||||
* @version $Id: TriggerEvolved.java 17802 2012-10-31 08:05:14Z Max mtg $
|
||||
*/
|
||||
public class TriggerEvolved extends Trigger {
|
||||
|
||||
@@ -42,17 +43,17 @@ public class TriggerEvolved extends Trigger {
|
||||
* @param intrinsic
|
||||
* the intrinsic
|
||||
*/
|
||||
public TriggerEvolved(final java.util.Map<String, String> params, final Card host, final boolean intrinsic) {
|
||||
public TriggerEvolved(final Map<String, String> params, final Card host, final boolean intrinsic) {
|
||||
super(params, host, intrinsic);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final boolean performTest(final java.util.Map<String, Object> runParams2) {
|
||||
public final boolean performTest(final Map<String, Object> runParams2) {
|
||||
final Card sac = (Card) runParams2.get("Card");
|
||||
if (this.mapParams.containsKey("ValidCard")) {
|
||||
if (!sac.isValid(this.mapParams.get("ValidCard").split(","), this.getHostCard().getController(),
|
||||
this.getHostCard(), null)) {
|
||||
if (hasParam("ValidCard")) {
|
||||
if (!sac.isValid(getParam("ValidCard").split(","), getHostCard().getController(),
|
||||
getHostCard(), null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,7 @@ public class TriggerEvolved extends Trigger {
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final void setTriggeringObjects(final SpellAbility sa) {
|
||||
sa.setTriggeringObject("Card", this.getRunParams().get("Card"));
|
||||
sa.setTriggeringObject("Card", getRunParams().get("Card"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,7 +25,10 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CardZoneTable;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.Ability;
|
||||
@@ -42,6 +45,7 @@ import io.sentry.event.BreadcrumbBuilder;
|
||||
import java.util.*;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
@@ -376,11 +380,7 @@ public class TriggerHandler {
|
||||
// Static triggers
|
||||
for (final Trigger t : Lists.newArrayList(activeTriggers)) {
|
||||
if (t.isStatic() && canRunTrigger(t, mode, runParams)) {
|
||||
int x = 1 + handlePanharmonicon(t, runParams);
|
||||
|
||||
for (int i = 0; i < x; ++i) {
|
||||
runSingleTrigger(t, runParams);
|
||||
}
|
||||
runSingleTrigger(t, runParams);
|
||||
|
||||
checkStatics = true;
|
||||
}
|
||||
@@ -448,7 +448,7 @@ public class TriggerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
int x = 1 + handlePanharmonicon(t, runParams);;
|
||||
int x = 1 + handlePanharmonicon(t, runParams, player);
|
||||
|
||||
for (int i = 0; i < x; ++i) {
|
||||
runSingleTrigger(t, runParams);
|
||||
@@ -636,7 +636,10 @@ public class TriggerHandler {
|
||||
|
||||
sa.setStackDescription(sa.toString());
|
||||
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
|
||||
CharmEffect.makeChoices(sa);
|
||||
if (!CharmEffect.makeChoices(sa)) {
|
||||
// 603.3c If no mode is chosen, the ability is removed from the stack.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Player decider = null;
|
||||
@@ -692,29 +695,80 @@ public class TriggerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private int handlePanharmonicon(final Trigger t, final Map<String, Object> runParams) {
|
||||
final Card host = t.getHostCard();
|
||||
final Player p = host.getController();
|
||||
private int handlePanharmonicon(final Trigger t, final Map<String, Object> runParams, final Player p) {
|
||||
Card host = t.getHostCard();
|
||||
|
||||
// not a changesZone trigger
|
||||
if (t.getMode() != TriggerType.ChangesZone) {
|
||||
// not a changesZone trigger or changesZoneAll
|
||||
if (t.getMode() != TriggerType.ChangesZone && t.getMode() != TriggerType.ChangesZoneAll) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// leave battlefield trigger, might be dying
|
||||
// only real changeszone look back for this
|
||||
if (t.getMode() == TriggerType.ChangesZone && "Battlefield".equals(t.getParam("Origin"))) {
|
||||
// Need to get the last info from the trigger host
|
||||
host = game.getChangeZoneLKIInfo(host);
|
||||
}
|
||||
|
||||
// not a Permanent you control
|
||||
if (!host.isPermanent() || !host.isInZone(ZoneType.Battlefield)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int n = 0;
|
||||
for (final String kw : p.getKeywords()) {
|
||||
if (kw.startsWith("Panharmonicon")) {
|
||||
if (runParams.get("Destination") instanceof String) {
|
||||
final String dest = (String) runParams.get("Destination");
|
||||
if (dest.equals("Battlefield") && runParams.get("Card") instanceof Card) {
|
||||
final Card card = (Card) runParams.get("Card");
|
||||
if (t.getMode() == TriggerType.ChangesZone) {
|
||||
// iterate over all cards
|
||||
final List<Card> lastCards = CardLists.filterControlledBy(p.getGame().getLastStateBattlefield(), p);
|
||||
for (final Card ck : lastCards) {
|
||||
for (final KeywordInterface ki : ck.getKeywords()) {
|
||||
final String kw = ki.getOriginal();
|
||||
if (kw.startsWith("Panharmonicon")) {
|
||||
// Enter the Battlefield Trigger
|
||||
if (runParams.get("Destination") instanceof String) {
|
||||
final String dest = (String) runParams.get("Destination");
|
||||
if ("Battlefield".equals(dest) && runParams.get("Card") instanceof Card) {
|
||||
final Card card = (Card) runParams.get("Card");
|
||||
final String valid = kw.split(":")[1];
|
||||
if (card.isValid(valid.split(","), p, ck, null)) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (kw.startsWith("Dieharmonicon")) {
|
||||
// 700.4. The term dies means “is put into a graveyard from the battlefield.”
|
||||
if (runParams.get("Origin") instanceof String) {
|
||||
final String origin = (String) runParams.get("Origin");
|
||||
if ("Battlefield".equals(origin) && runParams.get("Destination") instanceof String) {
|
||||
final String dest = (String) runParams.get("Destination");
|
||||
if ("Graveyard".equals(dest) && runParams.get("Card") instanceof Card) {
|
||||
final Card card = (Card) runParams.get("Card");
|
||||
final String valid = kw.split(":")[1];
|
||||
if (card.isValid(valid.split(","), p, ck, null)) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (t.getMode() == TriggerType.ChangesZoneAll) {
|
||||
final CardZoneTable table = (CardZoneTable) runParams.get("Cards");
|
||||
// iterate over all cards
|
||||
for (final Card ck : p.getCardsIn(ZoneType.Battlefield)) {
|
||||
for (final KeywordInterface ki : ck.getKeywords()) {
|
||||
final String kw = ki.getOriginal();
|
||||
if (kw.startsWith("Panharmonicon")) {
|
||||
// currently there is no ChangesZoneAll that would trigger on etb
|
||||
final String valid = kw.split(":")[1];
|
||||
if (card.isValid(valid.split(","), p, host, null)) {
|
||||
if (!table.filterCards(null, ZoneType.Battlefield, valid, ck, null).isEmpty()) {
|
||||
n++;
|
||||
}
|
||||
} else if (kw.startsWith("Dieharmonicon")) {
|
||||
// 700.4. The term dies means “is put into a graveyard from the battlefield.”
|
||||
final String valid = kw.split(":")[1];
|
||||
if (!table.filterCards(ImmutableList.of(ZoneType.Battlefield), ZoneType.Graveyard,
|
||||
valid, ck, null).isEmpty()) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,14 @@ package forge.game.trigger;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Trigger_LifeGained class.
|
||||
* </p>
|
||||
*
|
||||
* @author Forge
|
||||
* @version $Id$
|
||||
*/
|
||||
public class TriggerLifeGained extends Trigger {
|
||||
|
||||
@@ -36,32 +37,30 @@ public class TriggerLifeGained extends Trigger {
|
||||
* </p>
|
||||
*
|
||||
* @param params
|
||||
* a {@link java.util.HashMap} object.
|
||||
* a {@link java.util.Map} object.
|
||||
* @param host
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param intrinsic
|
||||
* the intrinsic
|
||||
*/
|
||||
public TriggerLifeGained(final java.util.Map<String, String> params, final Card host, final boolean intrinsic) {
|
||||
public TriggerLifeGained(final Map<String, String> params, final Card host, final boolean intrinsic) {
|
||||
super(params, host, intrinsic);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final boolean performTest(final java.util.Map<String, Object> runParams2) {
|
||||
if (this.mapParams.containsKey("ValidPlayer")) {
|
||||
if (!matchesValid(runParams2.get("Player"), this.mapParams.get("ValidPlayer").split(","),
|
||||
this.getHostCard())) {
|
||||
public final boolean performTest(final Map<String, Object> runParams2) {
|
||||
if (hasParam("ValidPlayer")) {
|
||||
if (!matchesValid(runParams2.get("Player"), getParam("ValidPlayer").split(","), getHostCard())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.mapParams.containsKey("ValidSource")) {
|
||||
if (!matchesValid(runParams2.get("Source"), this.mapParams.get("ValidSource").split(","),
|
||||
this.getHostCard())) {
|
||||
if (hasParam("ValidSource")) {
|
||||
if (!matchesValid(runParams2.get("Source"), getParam("ValidSource").split(","), getHostCard())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.mapParams.containsKey("Spell")) {
|
||||
if (hasParam("Spell")) {
|
||||
final SpellAbility spellAbility = (SpellAbility) runParams2.get("SourceSA");
|
||||
if (spellAbility == null || !spellAbility.getRootAbility().isSpell()) {
|
||||
return false;
|
||||
@@ -71,12 +70,11 @@ public class TriggerLifeGained extends Trigger {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final void setTriggeringObjects(final SpellAbility sa) {
|
||||
sa.setTriggeringObject("LifeAmount", this.getRunParams().get("LifeAmount"));
|
||||
sa.setTriggeringObject("Player", this.getRunParams().get("Player"));
|
||||
sa.setTriggeringObject("LifeAmount", getRunParams().get("LifeAmount"));
|
||||
sa.setTriggeringObject("Player", getRunParams().get("Player"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Forge: Play Magic: the Gathering.
|
||||
* Copyright (C) 2011 Forge Team
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package forge.game.trigger;
|
||||
|
||||
import forge.game.card.Card;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Trigger_LifeGained class.
|
||||
* </p>
|
||||
*
|
||||
* @author Forge
|
||||
*/
|
||||
public class TriggerPayLife extends Trigger {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Constructor for TriggerPayLife.
|
||||
* </p>
|
||||
*
|
||||
* @param params
|
||||
* a {@link java.util.Map} object.
|
||||
* @param host
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param intrinsic
|
||||
* the intrinsic
|
||||
*/
|
||||
public TriggerPayLife(final Map<String, String> params, final Card host, final boolean intrinsic) {
|
||||
super(params, host, intrinsic);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final boolean performTest(final Map<String, Object> runParams2) {
|
||||
if (hasParam("ValidPlayer")) {
|
||||
if (!matchesValid(runParams2.get("Player"), getParam("ValidPlayer").split(","), getHostCard())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final void setTriggeringObjects(final SpellAbility sa) {
|
||||
sa.setTriggeringObject("LifeAmount", getRunParams().get("LifeAmount"));
|
||||
sa.setTriggeringObject("Player", getRunParams().get("Player"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImportantStackObjects(SpellAbility sa) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Player: ").append(sa.getTriggeringObject("Player")).append(", ");
|
||||
sb.append("paid Amount: ").append(sa.getTriggeringObject("LifeAmount"));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import java.util.Map;
|
||||
* </p>
|
||||
*
|
||||
* @author Forge
|
||||
* @version $Id$
|
||||
*/
|
||||
public class TriggerTaps extends Trigger {
|
||||
|
||||
@@ -50,15 +49,26 @@ public class TriggerTaps extends Trigger {
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final boolean performTest(final java.util.Map<String, Object> runParams2) {
|
||||
public final boolean performTest(final Map<String, Object> runParams2) {
|
||||
final Card tapper = (Card) runParams2.get("Card");
|
||||
|
||||
if (this.mapParams.containsKey("ValidCard")) {
|
||||
if (!tapper.isValid(this.mapParams.get("ValidCard").split(","), this.getHostCard().getController(),
|
||||
this.getHostCard(), null)) {
|
||||
if (hasParam("ValidCard")) {
|
||||
if (!tapper.isValid(getParam("ValidCard").split(","), getHostCard().getController(),
|
||||
getHostCard(), null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (hasParam("Attacker")) {
|
||||
if ("True".equalsIgnoreCase(getParam("Attacker"))) {
|
||||
if (!(Boolean)runParams2.get("Attacker")) {
|
||||
return false;
|
||||
}
|
||||
} else if ("False".equalsIgnoreCase(getParam("Attacker"))) {
|
||||
if ((Boolean)runParams2.get("Attacker")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -67,7 +77,7 @@ public class TriggerTaps extends Trigger {
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public final void setTriggeringObjects(final SpellAbility sa) {
|
||||
sa.setTriggeringObject("Card", this.getRunParams().get("Card"));
|
||||
sa.setTriggeringObject("Card", getRunParams().get("Card"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.util.Map;
|
||||
public enum TriggerType {
|
||||
Abandoned(TriggerAbandoned.class),
|
||||
AbilityCast(TriggerSpellAbilityCast.class),
|
||||
Adapt(TriggerAdapt.class),
|
||||
Always(TriggerAlways.class),
|
||||
Attached(TriggerAttached.class),
|
||||
AttackerBlocked(TriggerAttackerBlocked.class),
|
||||
@@ -63,6 +64,7 @@ public enum TriggerType {
|
||||
NewGame(TriggerNewGame.class),
|
||||
PayCumulativeUpkeep(TriggerPayCumulativeUpkeep.class),
|
||||
PayEcho(TriggerPayEcho.class),
|
||||
PayLife(TriggerPayLife.class),
|
||||
Phase(TriggerPhase.class),
|
||||
PhaseIn(TriggerPhaseIn.class),
|
||||
PhaseOut(TriggerPhaseOut.class),
|
||||
@@ -79,7 +81,6 @@ public enum TriggerType {
|
||||
SpellAbilityCast(TriggerSpellAbilityCast.class),
|
||||
SpellCast(TriggerSpellAbilityCast.class),
|
||||
Surveil(TriggerSurveil.class),
|
||||
Tapped(TriggerTaps.class),
|
||||
Taps(TriggerTaps.class),
|
||||
TapsForMana(TriggerTapsForMana.class),
|
||||
Transformed(TriggerTransformed.class),
|
||||
|
||||
@@ -13,7 +13,7 @@ public class TriggerWaiting {
|
||||
private Map<String, Object> params;
|
||||
private List<Trigger> triggers = null;
|
||||
|
||||
public TriggerWaiting(TriggerType m, Map<String, Object> p) {
|
||||
public TriggerWaiting(TriggerType m, Map<String, Object> p) {
|
||||
mode = m;
|
||||
params = p;
|
||||
}
|
||||
@@ -25,7 +25,6 @@ public class TriggerWaiting {
|
||||
public Map<String, Object> getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
|
||||
public List<Trigger> getTriggers() {
|
||||
return triggers;
|
||||
@@ -35,7 +34,7 @@ public class TriggerWaiting {
|
||||
this.triggers = triggers;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public String toString() {
|
||||
return TextUtil.concatWithSpace("Waiting trigger:", mode.toString(),"with", params.toString());
|
||||
}
|
||||
|
||||
@@ -569,13 +569,6 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
|
||||
// verified by System.identityHashCode(card);
|
||||
final Card tmp = sa.getHostCard();
|
||||
if (!(sa instanceof WrappedAbility && sa.isTrigger())) { tmp.setCanCounter(true); } // reset mana pumped counter magic flag
|
||||
if (tmp.getClones().size() > 0) {
|
||||
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
if (c.equals(tmp)) {
|
||||
c.setClones(tmp.getClones());
|
||||
}
|
||||
}
|
||||
}
|
||||
// xManaCostPaid will reset when cast the spell, comment out to fix Venarian Gold
|
||||
// sa.getHostCard().setXManaCostPaid(0);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-gui-android</artifactId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-gui-desktop</artifactId>
|
||||
|
||||
@@ -29,7 +29,7 @@ import forge.item.PaperCard;
|
||||
import forge.model.FModel;
|
||||
import forge.screens.match.CMatchUI;
|
||||
import forge.toolbox.FOptionPane;
|
||||
|
||||
import forge.view.arcane.ListCardArea;
|
||||
|
||||
public class GuiChoose {
|
||||
|
||||
@@ -285,5 +285,30 @@ public class GuiChoose {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<CardView> manipulateCardList(final CMatchUI gui, final String title, final Iterable<CardView> cards, final Iterable<CardView> manipulable,
|
||||
final boolean toTop, final boolean toBottom, final boolean toAnywhere) {
|
||||
gui.setSelectables(manipulable);
|
||||
final Callable<List<CardView>> callable = new Callable<List<CardView>>() {
|
||||
@Override
|
||||
public List<CardView> call() throws Exception {
|
||||
ListCardArea tempArea = ListCardArea.show(gui,title,cards,manipulable,toTop,toBottom,toAnywhere);
|
||||
// tempArea.pack();
|
||||
tempArea.show();
|
||||
final List<CardView> cardList = tempArea.getCards();
|
||||
return cardList;
|
||||
}
|
||||
};
|
||||
final FutureTask<List<CardView>> ft = new FutureTask<List<CardView>>(callable);
|
||||
FThreads.invokeInEdtAndWait(ft);
|
||||
gui.clearSelectables();
|
||||
try {
|
||||
List<CardView> result = ft.get();
|
||||
return result;
|
||||
} catch (final Exception e) { // we have waited enough
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -201,8 +201,7 @@ public abstract class ACEditorBase<TItem extends InventoryItem, TModel extends D
|
||||
int qty = itemEntry.getValue();
|
||||
|
||||
int max;
|
||||
if (deck == null || card == null || card.getRules().getType().isBasic() ||
|
||||
limit == CardLimit.None || DeckFormat.getLimitExceptions().contains(card.getName())) {
|
||||
if (deck == null || card == null || limit == CardLimit.None || DeckFormat.canHaveAnyNumberOf(card)) {
|
||||
max = Integer.MAX_VALUE;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.awt.Dialog.ModalityType;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.SwingUtilities;
|
||||
@@ -22,6 +23,7 @@ import forge.screens.deckeditor.CDeckEditorUI;
|
||||
import forge.screens.deckeditor.DeckImport;
|
||||
import forge.screens.deckeditor.SEditorIO;
|
||||
import forge.screens.deckeditor.views.VCurrentDeck;
|
||||
import forge.toolbox.FOptionPane;
|
||||
|
||||
/**
|
||||
* Controls the "current deck" panel in the deck editor UI.
|
||||
@@ -244,8 +246,17 @@ public enum CCurrentDeck implements ICDoc {
|
||||
if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
final File file = fileChooser.getSelectedFile();
|
||||
final String check = file.getAbsolutePath();
|
||||
|
||||
previousDirectory = file.getParentFile();
|
||||
|
||||
if (!previousDirectory.exists()) {
|
||||
FOptionPane.showErrorDialog("Cannot save deck to " + check);
|
||||
return null;
|
||||
}
|
||||
|
||||
if(isFileNameInvalid(file)) {
|
||||
FOptionPane.showErrorDialog("Cannot save deck to " + check + "\nDeck name may not include any of the characters / \\ : * ? \" < > |");
|
||||
return null;
|
||||
}
|
||||
|
||||
return check.endsWith(".dck") ? file : new File(check + ".dck");
|
||||
}
|
||||
@@ -261,14 +272,29 @@ public enum CCurrentDeck implements ICDoc {
|
||||
if (save.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
final File file = save.getSelectedFile();
|
||||
final String check = file.getAbsolutePath();
|
||||
|
||||
previousDirectory = file.getParentFile();
|
||||
|
||||
if (!previousDirectory.exists()) {
|
||||
FOptionPane.showErrorDialog("Cannot save proxies to " + check);
|
||||
return null;
|
||||
}
|
||||
|
||||
if(isFileNameInvalid(file)) {
|
||||
FOptionPane.showErrorDialog("Cannot save proxies to " + check + "\nFile name may not include any of the characters / \\ : * ? \" < > |");
|
||||
return null;
|
||||
}
|
||||
|
||||
return check.endsWith(".html") ? file : new File(check + ".html");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Checks if the file name includes any of the invalid characters / \ : * ? " < > : */
|
||||
private static boolean isFileNameInvalid(File file) {
|
||||
final Pattern pattern = Pattern.compile("[/\\\\:*?\\\"<>|]");
|
||||
return pattern.matcher(file.getName()).find();
|
||||
}
|
||||
|
||||
/** The Constant HTML_FILTER. */
|
||||
public static final FileFilter HTML_FILTER = new FileFilter() {
|
||||
@Override
|
||||
@@ -281,4 +307,5 @@ public enum CCurrentDeck implements ICDoc {
|
||||
return "Proxy File .html";
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ import forge.util.ITriggerEvent;
|
||||
import forge.util.gui.SOptionPane;
|
||||
import forge.view.FView;
|
||||
import forge.view.arcane.CardPanel;
|
||||
import forge.view.arcane.FloatingCardArea;
|
||||
import forge.view.arcane.FloatingZone;
|
||||
import forge.match.input.*;
|
||||
|
||||
/**
|
||||
* Constructs instance of match UI controller, used as a single point of
|
||||
@@ -395,10 +396,12 @@ public final class CMatchUI
|
||||
break;
|
||||
case Hand:
|
||||
updateHand = true;
|
||||
//$FALL-THROUGH$
|
||||
updateZones = true;
|
||||
FloatingZone.refresh(owner, zone);
|
||||
break;
|
||||
default:
|
||||
updateZones = true;
|
||||
FloatingCardArea.refresh(owner, zone);
|
||||
FloatingZone.refresh(owner, zone);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -424,6 +427,57 @@ public final class CMatchUI
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<PlayerZoneUpdate> tempShowZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
|
||||
for (final PlayerZoneUpdate update : zonesToUpdate) {
|
||||
final PlayerView player = update.getPlayer();
|
||||
for (final ZoneType zone : update.getZones()) {
|
||||
switch (zone) {
|
||||
case Battlefield: // always shown
|
||||
break;
|
||||
case Hand: // controller hand always shown
|
||||
if (controller != player) {
|
||||
FloatingZone.show(this,player,zone);
|
||||
}
|
||||
break;
|
||||
case Library:
|
||||
case Graveyard:
|
||||
case Exile:
|
||||
case Flashback:
|
||||
case Command:
|
||||
FloatingZone.show(this,player,zone);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return zonesToUpdate; //pfps should return only the newly shown zones
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
|
||||
for (final PlayerZoneUpdate update : zonesToUpdate) {
|
||||
final PlayerView player = update.getPlayer();
|
||||
for (final ZoneType zone : update.getZones()) {
|
||||
switch (zone) {
|
||||
case Battlefield: // always shown
|
||||
break;
|
||||
case Hand: // the controller's hand should never be temporarily shown, but ...
|
||||
case Library:
|
||||
case Graveyard:
|
||||
case Exile:
|
||||
case Flashback:
|
||||
case Command:
|
||||
FloatingZone.hide(this,player,zone);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Player's mana pool changes
|
||||
@Override
|
||||
public void updateManaPool(final Iterable<PlayerView> manaPoolUpdate) {
|
||||
@@ -465,11 +519,51 @@ public final class CMatchUI
|
||||
}
|
||||
break;
|
||||
default:
|
||||
FloatingZone.refresh(c.getController(),zone); // in case the card is visible in the zone
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectables(final Iterable<CardView> cards) {
|
||||
super.setSelectables(cards);
|
||||
// update zones on tabletop and floating zones - non-selectable cards may be rendered differently
|
||||
FThreads.invokeInEdtNowOrLater(new Runnable() {
|
||||
@Override public final void run() {
|
||||
for (final PlayerView p : getGameView().getPlayers()) {
|
||||
if ( p.getCards(ZoneType.Battlefield) != null ) {
|
||||
updateCards(p.getCards(ZoneType.Battlefield));
|
||||
}
|
||||
if ( p.getCards(ZoneType.Hand) != null ) {
|
||||
updateCards(p.getCards(ZoneType.Hand));
|
||||
}
|
||||
}
|
||||
FloatingZone.refreshAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelectables() {
|
||||
super.clearSelectables();
|
||||
// update zones on tabletop and floating zones - non-selectable cards may be rendered differently
|
||||
FThreads.invokeInEdtNowOrLater(new Runnable() {
|
||||
@Override public final void run() {
|
||||
for (final PlayerView p : getGameView().getPlayers()) {
|
||||
if ( p.getCards(ZoneType.Battlefield) != null ) {
|
||||
updateCards(p.getCards(ZoneType.Battlefield));
|
||||
}
|
||||
if ( p.getCards(ZoneType.Hand) != null ) {
|
||||
updateCards(p.getCards(ZoneType.Hand));
|
||||
}
|
||||
}
|
||||
FloatingZone.refreshAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<JMenu> getMenus() {
|
||||
return menus.getMenus();
|
||||
@@ -504,7 +598,7 @@ public final class CMatchUI
|
||||
layoutControl.initialize();
|
||||
layoutControl.update();
|
||||
}
|
||||
FloatingCardArea.closeAll();
|
||||
FloatingZone.closeAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -562,7 +656,7 @@ public final class CMatchUI
|
||||
case Exile:
|
||||
case Graveyard:
|
||||
case Library:
|
||||
return FloatingCardArea.getCardPanel(this, card);
|
||||
return FloatingZone.getCardPanel(this, card);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -673,7 +767,7 @@ public final class CMatchUI
|
||||
|
||||
@Override
|
||||
public void finishGame() {
|
||||
FloatingCardArea.closeAll(); //ensure floating card areas cleared and closed after the game
|
||||
FloatingZone.closeAll(); //ensure floating card areas cleared and closed after the game
|
||||
final GameView gameView = getGameView();
|
||||
if (hasLocalPlayers() || gameView.isMatchOver()) {
|
||||
new ViewWinLose(gameView, this).show();
|
||||
@@ -787,7 +881,7 @@ public final class CMatchUI
|
||||
} else {
|
||||
final ZoneType zone = hostCard.getZone();
|
||||
if (ImmutableList.of(ZoneType.Command, ZoneType.Exile, ZoneType.Graveyard, ZoneType.Library).contains(zone)) {
|
||||
FloatingCardArea.show(this, hostCard.getController(), zone);
|
||||
FloatingZone.show(this, hostCard.getController(), zone);
|
||||
}
|
||||
menuParent = panel.getParent();
|
||||
x = triggerEvent.getX();
|
||||
@@ -942,11 +1036,16 @@ public final class CMatchUI
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GameEntityView> chooseEntitiesForEffect(final String title, final List<? extends GameEntityView> optionList, final DelayedReveal delayedReveal) {
|
||||
public List<GameEntityView> chooseEntitiesForEffect(final String title, final List<? extends GameEntityView> optionList, final int min, final int max, final DelayedReveal delayedReveal) {
|
||||
if (delayedReveal != null) {
|
||||
reveal(delayedReveal.getMessagePrefix(), delayedReveal.getCards()); //TODO: Merge this into search dialog
|
||||
}
|
||||
return (List<GameEntityView>) order(title,"Selected", 0, optionList.size(), optionList, null, null, false);
|
||||
return (List<GameEntityView>) order(title,"Selected", min, max, optionList, null, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CardView> manipulateCardList(final String title, final Iterable<CardView> cards, final Iterable<CardView> manipulable, final boolean toTop, final boolean toBottom, final boolean toAnywhere) {
|
||||
return GuiChoose.manipulateCardList(this, title, cards, manipulable, toTop, toBottom, toAnywhere);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -79,6 +79,13 @@ public class TargetingOverlay {
|
||||
x2 = end.x;
|
||||
y2 = end.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
Arc arc = (Arc)obj;
|
||||
return ((arc.x1 == x1) && (arc.x2 == x2) && (arc.y1 == y1) && (arc.y2 == y2));
|
||||
}
|
||||
}
|
||||
|
||||
private final Set<CardView> cardsVisualized = new HashSet<CardView>();
|
||||
@@ -331,13 +338,24 @@ public class TargetingOverlay {
|
||||
activeStackItem.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(final MouseEvent e) {
|
||||
assembleStackArrows();
|
||||
FView.SINGLETON_INSTANCE.getFrame().repaint();
|
||||
if (matchUI.getCDock().getArcState() == ArcState.MOUSEOVER) {
|
||||
assembleStackArrows();
|
||||
FView.SINGLETON_INSTANCE.getFrame().repaint();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void mouseExited(final MouseEvent e) {
|
||||
assembleStackArrows();
|
||||
FView.SINGLETON_INSTANCE.getFrame().repaint();
|
||||
if (matchUI.getCDock().getArcState() == ArcState.MOUSEOVER) {
|
||||
assembleStackArrows();
|
||||
FView.SINGLETON_INSTANCE.getFrame().repaint();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent e) {
|
||||
if (matchUI.getCDock().getArcState() == ArcState.ON) {
|
||||
assembleStackArrows();
|
||||
FView.SINGLETON_INSTANCE.getFrame().repaint();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -384,18 +402,27 @@ public class TargetingOverlay {
|
||||
if (start == null || end == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Arc newArc = new Arc(end,start);
|
||||
|
||||
switch (connects) {
|
||||
case Friends:
|
||||
case FriendsStackTargeting:
|
||||
arcsFriend.add(new Arc(end, start));
|
||||
if (!arcsFriend.contains(newArc)) {
|
||||
arcsFriend.add(newArc);
|
||||
}
|
||||
break;
|
||||
case FoesAttacking:
|
||||
arcsFoeAtk.add(new Arc(end, start));
|
||||
if (!arcsFoeAtk.contains(newArc)) {
|
||||
arcsFoeAtk.add(newArc);
|
||||
}
|
||||
break;
|
||||
case FoesBlocking:
|
||||
case FoesStackTargeting:
|
||||
arcsFoeDef.add(new Arc(end, start));
|
||||
if (!arcsFoeDef.contains(newArc)) {
|
||||
arcsFoeDef.add(newArc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,8 +470,8 @@ public class TargetingOverlay {
|
||||
if (!attackingCard.equals(c) && !blockingCard.equals(c)) { continue; }
|
||||
addArc(endpoints.get(attackingCard.getId()), endpoints.get(blockingCard.getId()), ArcConnection.FoesBlocking);
|
||||
cardsVisualized.add(blockingCard);
|
||||
cardsVisualized.add(attackingCard);
|
||||
}
|
||||
cardsVisualized.add(attackingCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package forge.screens.match;
|
||||
|
||||
import forge.game.player.PlayerView;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.view.arcane.FloatingCardArea;
|
||||
import forge.view.arcane.FloatingZone;
|
||||
|
||||
/**
|
||||
* Receives click and programmatic requests for viewing data stacks in the
|
||||
@@ -27,6 +27,6 @@ public final class ZoneAction implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
FloatingCardArea.showOrHide(matchUI, player, zone);
|
||||
FloatingZone.showOrHide(matchUI, player, zone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import forge.gui.framework.DragTab;
|
||||
import forge.gui.framework.EDocID;
|
||||
import forge.gui.framework.IVDoc;
|
||||
import forge.screens.match.controllers.CStack;
|
||||
import forge.screens.match.controllers.CDock.ArcState;
|
||||
import forge.toolbox.FMouseAdapter;
|
||||
import forge.toolbox.FScrollPanel;
|
||||
import forge.toolbox.FSkin;
|
||||
@@ -194,19 +195,45 @@ public class VStack implements IVDoc<CStack> {
|
||||
setFont(FSkin.getFont());
|
||||
setWrapStyleWord(true);
|
||||
setMinimumSize(new Dimension(CARD_WIDTH + 2 * PADDING, CARD_HEIGHT + 2 * PADDING));
|
||||
|
||||
// if the top of the stack is not assigned yet...
|
||||
if (hoveredItem == null)
|
||||
{
|
||||
// set things up to draw an arc from it...
|
||||
hoveredItem = StackInstanceTextArea.this;
|
||||
controller.getMatchUI().setCard(item.getSourceCard());
|
||||
}
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(final MouseEvent e) {
|
||||
hoveredItem = StackInstanceTextArea.this;
|
||||
controller.getMatchUI().setCard(item.getSourceCard());
|
||||
if (controller.getMatchUI().getCDock().getArcState() == ArcState.MOUSEOVER) {
|
||||
hoveredItem = StackInstanceTextArea.this;
|
||||
controller.getMatchUI().setCard(item.getSourceCard());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(final MouseEvent e) {
|
||||
if (hoveredItem == StackInstanceTextArea.this) {
|
||||
hoveredItem = null;
|
||||
}
|
||||
if (controller.getMatchUI().getCDock().getArcState() == ArcState.MOUSEOVER) {
|
||||
if (hoveredItem == StackInstanceTextArea.this) {
|
||||
hoveredItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent e) {
|
||||
if (controller.getMatchUI().getCDock().getArcState() == ArcState.ON) {
|
||||
if (hoveredItem == StackInstanceTextArea.this) {
|
||||
hoveredItem = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
hoveredItem = StackInstanceTextArea.this;
|
||||
controller.getMatchUI().setCard(item.getSourceCard());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -246,8 +246,8 @@ public class CardArea extends CardPanelContainer implements CardPanelMouseListen
|
||||
dragPanel.setDisplayEnabled(false);
|
||||
|
||||
CardPanel.setDragAnimationPanel(new CardPanel(dragPanel.getMatchUI(), dragPanel.getCard()));
|
||||
final JFrame frame = (JFrame) SwingUtilities.windowForComponent(this);
|
||||
final JLayeredPane layeredPane = frame.getLayeredPane();
|
||||
final RootPaneContainer frame = (RootPaneContainer) SwingUtilities.windowForComponent(this);
|
||||
final JLayeredPane layeredPane = frame.getLayeredPane();
|
||||
layeredPane.add(CardPanel.getDragAnimationPanel());
|
||||
layeredPane.moveToFront(CardPanel.getDragAnimationPanel());
|
||||
final Point p = SwingUtilities.convertPoint(this, this.mouseDragStartX, this.mouseDragStartY, layeredPane);
|
||||
|
||||
@@ -253,7 +253,7 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
|
||||
g2d.rotate(getTappedAngle(), cardXOffset + edgeOffset, (cardYOffset + cardHeight)
|
||||
- edgeOffset);
|
||||
}
|
||||
super.paint(g2d);
|
||||
super.paint(g2d);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -268,12 +268,12 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
|
||||
final int cornerSize = noBorderPref && !cardImgHasAlpha ? 0 : Math.max(4, Math.round(cardWidth * CardPanel.ROUNDED_CORNER_SIZE));
|
||||
final int offset = isTapped() && (!noBorderPref || cardImgHasAlpha) ? 1 : 0;
|
||||
|
||||
// Magenta outline for when card was chosen to pay
|
||||
// Magenta outline for when card is chosen
|
||||
if (matchUI.isUsedToPay(getCard())) {
|
||||
g2d.setColor(Color.magenta);
|
||||
final int n2 = Math.max(1, Math.round(2 * cardWidth * CardPanel.SELECTED_BORDER_SIZE));
|
||||
g2d.fillRoundRect(cardXOffset - n2, (cardYOffset - n2) + offset, cardWidth + (n2 * 2), cardHeight + (n2 * 2), cornerSize + n2, cornerSize + n2);
|
||||
}
|
||||
}
|
||||
|
||||
// Green outline for hover
|
||||
if (isSelected) {
|
||||
@@ -282,7 +282,7 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
|
||||
g2d.fillRoundRect(cardXOffset - n, (cardYOffset - n) + offset, cardWidth + (n * 2), cardHeight + (n * 2), cornerSize + n , cornerSize + n);
|
||||
}
|
||||
|
||||
// Black fill - (will become outline for white bordered cards)
|
||||
// Black fill - (will become an outline for white bordered cards)
|
||||
g2d.setColor(Color.black);
|
||||
g2d.fillRoundRect(cardXOffset, cardYOffset + offset, cardWidth, cardHeight, cornerSize, cornerSize);
|
||||
|
||||
@@ -305,6 +305,12 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
|
||||
g2d.fillRoundRect(cardXOffset + ins, cardYOffset + ins, cardWidth - ins*2, cardHeight - ins*2, cornerSize-ins, cornerSize-ins);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchUI.isSelectable(getCard())) { // White border for selectable cards to further highlight them
|
||||
g2d.setColor(Color.WHITE);
|
||||
final int ins = 1;
|
||||
g2d.fillRoundRect(cardXOffset+ins, cardYOffset+ins, cardWidth-ins*2, cardHeight-ins*2, cornerSize-ins, cornerSize-ins);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawManaCost(final Graphics g, final ManaCost cost, final int deltaY) {
|
||||
@@ -328,6 +334,17 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
|
||||
drawFoilEffect(g, card, cardXOffset, cardYOffset,
|
||||
cardWidth, cardHeight, Math.round(cardWidth * BLACK_BORDER_SIZE));
|
||||
}
|
||||
|
||||
boolean nonselectable = matchUI.isSelecting() && !matchUI.isSelectable(getCard());
|
||||
// if selecting, darken non-selectable cards
|
||||
if ( nonselectable ) {
|
||||
boolean noBorderPref = !isPreferenceEnabled(FPref.UI_RENDER_BLACK_BORDERS);
|
||||
boolean cardImgHasAlpha = imagePanel != null && imagePanel.getSrcImage() != null && imagePanel.getSrcImage().getColorModel().hasAlpha();
|
||||
final int cornerSize = noBorderPref && !cardImgHasAlpha ? 0 : Math.max(4, Math.round(cardWidth * CardPanel.ROUNDED_CORNER_SIZE));
|
||||
final int offset = isTapped() && (!noBorderPref || cardImgHasAlpha) ? 1 : 0;
|
||||
g.setColor(new Color(0.0f,0.0f,0.0f,0.6f));
|
||||
g.fillRoundRect(cardXOffset, cardYOffset + offset, cardWidth, cardHeight, cornerSize, cornerSize);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawFoilEffect(final Graphics g, final CardView card2, final int x, final int y, final int width, final int height, final int borderSize) {
|
||||
@@ -779,6 +796,7 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
|
||||
return FModel.getPreferences().getPrefBoolean(preferenceName);
|
||||
}
|
||||
|
||||
// don't show overlays on non-selectable cards when selecting
|
||||
private boolean isShowingOverlays() {
|
||||
return isPreferenceEnabled(FPref.UI_SHOW_CARD_OVERLAYS) && card != null;
|
||||
}
|
||||
|
||||
@@ -182,6 +182,10 @@ public abstract class CardPanelContainer extends SkinnedPanel {
|
||||
});
|
||||
}
|
||||
|
||||
protected boolean cardPanelDraggable(final CardPanel panel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private MouseMotionListener setupMotionMouseListener() {
|
||||
final MouseMotionListener mml = new MouseMotionListener() {
|
||||
@Override
|
||||
@@ -207,20 +211,23 @@ public abstract class CardPanelContainer extends SkinnedPanel {
|
||||
if (panel != mouseDownPanel) {
|
||||
return;
|
||||
}
|
||||
if (intialMouseDragX == -1) {
|
||||
intialMouseDragX = x;
|
||||
intialMouseDragY = y;
|
||||
return;
|
||||
}
|
||||
if ((Math.abs(x - intialMouseDragX) < CardPanelContainer.DRAG_SMUDGE)
|
||||
|
||||
if (cardPanelDraggable(panel)) { // allow for non-draggable cards
|
||||
if (intialMouseDragX == -1) {
|
||||
intialMouseDragX = x;
|
||||
intialMouseDragY = y;
|
||||
return;
|
||||
}
|
||||
if ((Math.abs(x - intialMouseDragX) < CardPanelContainer.DRAG_SMUDGE)
|
||||
&& (Math.abs(y - intialMouseDragY) < CardPanelContainer.DRAG_SMUDGE)) {
|
||||
return;
|
||||
}
|
||||
mouseDownPanel = null;
|
||||
setMouseDragPanel(panel);
|
||||
mouseDragOffsetX = panel.getX() - intialMouseDragX;
|
||||
mouseDragOffsetY = panel.getY() - intialMouseDragY;
|
||||
mouseDragStart(getMouseDragPanel(), evt);
|
||||
return;
|
||||
}
|
||||
mouseDownPanel = null;
|
||||
setMouseDragPanel(panel);
|
||||
mouseDragOffsetX = panel.getX() - intialMouseDragX;
|
||||
mouseDragOffsetY = panel.getY() - intialMouseDragY;
|
||||
mouseDragStart(getMouseDragPanel(), evt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -453,4 +460,4 @@ public abstract class CardPanelContainer extends SkinnedPanel {
|
||||
this.layoutListeners.add(listener);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,19 @@
|
||||
*/
|
||||
package forge.view.arcane;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
import javax.swing.Timer;
|
||||
|
||||
import forge.Singletons;
|
||||
import forge.assets.FSkinProp;
|
||||
import forge.game.card.CardView;
|
||||
import forge.game.player.PlayerView;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.gui.framework.SDisplayUtil;
|
||||
import forge.model.FModel;
|
||||
import forge.properties.ForgePreferences;
|
||||
@@ -43,185 +37,58 @@ import forge.properties.ForgePreferences.FPref;
|
||||
import forge.screens.match.CMatchUI;
|
||||
import forge.toolbox.FMouseAdapter;
|
||||
import forge.toolbox.FScrollPane;
|
||||
import forge.toolbox.FSkin;
|
||||
import forge.toolbox.MouseTriggerEvent;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import forge.util.Lang;
|
||||
//import forge.util.collect.FCollectionView;
|
||||
import forge.view.FDialog;
|
||||
import forge.view.FFrame;
|
||||
|
||||
public class FloatingCardArea extends CardArea {
|
||||
private static final long serialVersionUID = 1927906492186378596L;
|
||||
// show some cards in a new window
|
||||
public abstract class FloatingCardArea extends CardArea {
|
||||
|
||||
private static final String COORD_DELIM = ",";
|
||||
protected static final String COORD_DELIM = ",";
|
||||
protected static final ForgePreferences prefs = FModel.getPreferences();
|
||||
|
||||
private static final ForgePreferences prefs = FModel.getPreferences();
|
||||
private static final Map<Integer, FloatingCardArea> floatingAreas = new HashMap<Integer, FloatingCardArea>();
|
||||
protected String title;
|
||||
protected FPref locPref;
|
||||
protected boolean hasBeenShown, locLoaded;
|
||||
|
||||
private static int getKey(final PlayerView player, final ZoneType zone) {
|
||||
return 40 * player.getId() + zone.hashCode();
|
||||
}
|
||||
public static void showOrHide(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
|
||||
final FloatingCardArea cardArea = _init(matchUI, player, zone);
|
||||
cardArea.showOrHideWindow();
|
||||
}
|
||||
public static void show(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
|
||||
final FloatingCardArea cardArea = _init(matchUI, player, zone);
|
||||
cardArea.showWindow();
|
||||
}
|
||||
private static FloatingCardArea _init(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
|
||||
final int key = getKey(player, zone);
|
||||
FloatingCardArea cardArea = floatingAreas.get(key);
|
||||
if (cardArea == null || cardArea.getMatchUI() != matchUI) {
|
||||
cardArea = new FloatingCardArea(matchUI, player, zone);
|
||||
floatingAreas.put(key, cardArea);
|
||||
} else {
|
||||
cardArea.setPlayer(player); //ensure player is updated if needed
|
||||
}
|
||||
return cardArea;
|
||||
}
|
||||
public static CardPanel getCardPanel(final CMatchUI matchUI, final CardView card) {
|
||||
final FloatingCardArea window = _init(matchUI, card.getController(), card.getZone());
|
||||
return window.getCardPanel(card.getId());
|
||||
}
|
||||
public static void refresh(final PlayerView player, final ZoneType zone) {
|
||||
FloatingCardArea cardArea = floatingAreas.get(getKey(player, zone));
|
||||
if (cardArea != null) {
|
||||
cardArea.setPlayer(player); //ensure player is updated if needed
|
||||
cardArea.refresh();
|
||||
}
|
||||
protected abstract FDialog getWindow();
|
||||
protected abstract Iterable<CardView> getCards();
|
||||
|
||||
//refresh flashback zone when graveyard, library, or exile zones updated
|
||||
switch (zone) {
|
||||
case Graveyard:
|
||||
case Library:
|
||||
case Exile:
|
||||
refresh(player, ZoneType.Flashback);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
protected FloatingCardArea(final CMatchUI matchUI) {
|
||||
this(matchUI, new FScrollPane(false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
|
||||
}
|
||||
public static void closeAll() {
|
||||
for (final FloatingCardArea cardArea : floatingAreas.values()) {
|
||||
cardArea.window.setVisible(false);
|
||||
}
|
||||
floatingAreas.clear();
|
||||
protected FloatingCardArea(final CMatchUI matchUI, final FScrollPane scrollPane) {
|
||||
super(matchUI, scrollPane);
|
||||
}
|
||||
|
||||
private final ZoneType zone;
|
||||
private PlayerView player;
|
||||
private String title;
|
||||
private FPref locPref;
|
||||
private boolean hasBeenShown, locLoaded;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private final FDialog window = new FDialog(false, true, "0") {
|
||||
@Override
|
||||
public void setLocationRelativeTo(Component c) {
|
||||
//don't change location this way if dialog has already been shown or location was loaded from preferences
|
||||
if (hasBeenShown || locLoaded) { return; }
|
||||
super.setLocationRelativeTo(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b0) {
|
||||
if (isVisible() == b0) { return; }
|
||||
if (!b0 && hasBeenShown && locPref != null) {
|
||||
//update preference before hiding window, as otherwise its location will be 0,0
|
||||
prefs.setPref(locPref,
|
||||
getX() + COORD_DELIM + getY() + COORD_DELIM +
|
||||
getWidth() + COORD_DELIM + getHeight());
|
||||
//don't call prefs.save(), instead allowing them to be saved when match ends
|
||||
}
|
||||
super.setVisible(b0);
|
||||
if (b0) {
|
||||
refresh();
|
||||
hasBeenShown = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private FloatingCardArea(final CMatchUI matchUI, final PlayerView player0, final ZoneType zone0) {
|
||||
super(matchUI, new FScrollPane(false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
|
||||
window.add(getScrollPane(), "grow, push");
|
||||
getScrollPane().setViewportView(this);
|
||||
setOpaque(false);
|
||||
switch (zone0) {
|
||||
case Exile:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_EXILE));
|
||||
break;
|
||||
case Graveyard:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_GRAVEYARD));
|
||||
break;
|
||||
case Hand:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_HAND));
|
||||
break;
|
||||
case Library:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_LIBRARY));
|
||||
break;
|
||||
case Flashback:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_FLASHBACK));
|
||||
break;
|
||||
default:
|
||||
locPref = null;
|
||||
break;
|
||||
}
|
||||
zone = zone0;
|
||||
setPlayer(player0);
|
||||
setVertical(true);
|
||||
}
|
||||
|
||||
private void setPlayer(PlayerView player0) {
|
||||
if (player == player0) { return; }
|
||||
player = player0;
|
||||
title = Lang.getPossessedObject(player0.getName(), zone.name()) + " (%d)";
|
||||
|
||||
boolean isAi = player0.isAI();
|
||||
switch (zone) {
|
||||
case Exile:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_EXILE : FPref.ZONE_LOC_HUMAN_EXILE;
|
||||
break;
|
||||
case Graveyard:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_GRAVEYARD : FPref.ZONE_LOC_HUMAN_GRAVEYARD;
|
||||
break;
|
||||
case Hand:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_HAND : FPref.ZONE_LOC_HUMAN_HAND;
|
||||
break;
|
||||
case Library:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_LIBRARY : FPref.ZONE_LOC_HUMAN_LIBRARY;
|
||||
break;
|
||||
case Flashback:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_FLASHBACK : FPref.ZONE_LOC_HUMAN_FLASHBACK;
|
||||
break;
|
||||
default:
|
||||
locPref = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void showWindow() {
|
||||
protected void showWindow() {
|
||||
onShow();
|
||||
window.setFocusableWindowState(false); // should probably do this earlier
|
||||
window.setVisible(true);
|
||||
getWindow().setFocusableWindowState(false); // should probably do this earlier
|
||||
getWindow().setVisible(true);
|
||||
}
|
||||
private void showOrHideWindow() {
|
||||
protected void hideWindow() {
|
||||
onShow();
|
||||
window.setFocusableWindowState(false); // should probably do this earlier
|
||||
window.setVisible(!window.isVisible());
|
||||
getWindow().setFocusableWindowState(false); // should probably do this earlier
|
||||
getWindow().setVisible(false);
|
||||
}
|
||||
private void onShow() {
|
||||
protected void showOrHideWindow() {
|
||||
onShow();
|
||||
getWindow().setFocusableWindowState(false); // should probably do this earlier
|
||||
getWindow().setVisible(!getWindow().isVisible());
|
||||
}
|
||||
protected void onShow() {
|
||||
if (!hasBeenShown) {
|
||||
loadLocation();
|
||||
window.getTitleBar().addMouseListener(new FMouseAdapter() {
|
||||
getWindow().getTitleBar().addMouseListener(new FMouseAdapter() {
|
||||
@Override public final void onLeftDoubleClick(final MouseEvent e) {
|
||||
window.setVisible(false); //hide window if titlebar double-clicked
|
||||
getWindow().setVisible(false); //hide window if titlebar double-clicked
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void loadLocation() {
|
||||
protected void loadLocation() {
|
||||
if (locPref != null) {
|
||||
String value = prefs.getPref(locPref);
|
||||
if (value.length() > 0) {
|
||||
@@ -255,7 +122,7 @@ public class FloatingCardArea extends CardArea {
|
||||
y = screenBounds.y;
|
||||
}
|
||||
}
|
||||
window.setBounds(x, y, w, h);
|
||||
getWindow().setBounds(x, y, w, h);
|
||||
locLoaded = true;
|
||||
return;
|
||||
}
|
||||
@@ -269,14 +136,17 @@ public class FloatingCardArea extends CardArea {
|
||||
}
|
||||
//fallback default size
|
||||
FFrame mainFrame = Singletons.getView().getFrame();
|
||||
window.setSize(mainFrame.getWidth() / 5, mainFrame.getHeight() / 2);
|
||||
getWindow().setSize(mainFrame.getWidth() / 5, mainFrame.getHeight() / 2);
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
if (!window.isVisible()) { return; } //don't refresh while window hidden
|
||||
protected void refresh() {
|
||||
if (!getWindow().isVisible()) { return; } //don't refresh while window hidden
|
||||
doRefresh();
|
||||
}
|
||||
|
||||
protected void doRefresh() {
|
||||
List<CardPanel> cardPanels = new ArrayList<CardPanel>();
|
||||
FCollectionView<CardView> cards = player.getCards(zone);
|
||||
Iterable<CardView> cards = getCards();
|
||||
if (cards != null) {
|
||||
for (final CardView card : cards) {
|
||||
CardPanel cardPanel = getCardPanel(card.getId());
|
||||
@@ -293,27 +163,26 @@ public class FloatingCardArea extends CardArea {
|
||||
|
||||
boolean hadCardPanels = getCardPanels().size() > 0;
|
||||
setCardPanels(cardPanels);
|
||||
window.setTitle(String.format(title, cardPanels.size()));
|
||||
getWindow().setTitle(String.format(title, cardPanels.size()));
|
||||
|
||||
//if window had cards and now doesn't, hide window
|
||||
//(e.g. cast final card from Flashback zone)
|
||||
if (hadCardPanels && cardPanels.size() == 0) {
|
||||
window.setVisible(false);
|
||||
getWindow().setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doLayout() {
|
||||
if (window.isResizing()) {
|
||||
if (getWindow().isResizing()) {
|
||||
//delay layout slightly to reduce flicker during window resize
|
||||
layoutTimer.restart();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
finishDoLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private final Timer layoutTimer = new Timer(250, new ActionListener() {
|
||||
protected final Timer layoutTimer = new Timer(250, new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent arg0) {
|
||||
layoutTimer.stop();
|
||||
@@ -321,7 +190,7 @@ public class FloatingCardArea extends CardArea {
|
||||
}
|
||||
});
|
||||
|
||||
private void finishDoLayout() {
|
||||
protected void finishDoLayout() {
|
||||
super.doLayout();
|
||||
}
|
||||
|
||||
@@ -331,12 +200,12 @@ public class FloatingCardArea extends CardArea {
|
||||
super.mouseOver(panel, evt);
|
||||
}
|
||||
@Override
|
||||
public final void mouseLeftClicked(final CardPanel panel, final MouseEvent evt) {
|
||||
public void mouseLeftClicked(final CardPanel panel, final MouseEvent evt) {
|
||||
getMatchUI().getGameController().selectCard(panel.getCard(), null, new MouseTriggerEvent(evt));
|
||||
super.mouseLeftClicked(panel, evt);
|
||||
}
|
||||
@Override
|
||||
public final void mouseRightClicked(final CardPanel panel, final MouseEvent evt) {
|
||||
public void mouseRightClicked(final CardPanel panel, final MouseEvent evt) {
|
||||
getMatchUI().getGameController().selectCard(panel.getCard(), null, new MouseTriggerEvent(evt));
|
||||
super.mouseRightClicked(panel, evt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Forge: Play Magic: the Gathering.
|
||||
* Copyright (C) 2011 Nate
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package forge.view.arcane;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
|
||||
import forge.assets.FSkinProp;
|
||||
import forge.game.card.CardView;
|
||||
import forge.game.player.PlayerView;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.properties.ForgePreferences.FPref;
|
||||
import forge.screens.match.CMatchUI;
|
||||
import forge.toolbox.FScrollPane;
|
||||
import forge.toolbox.FSkin;
|
||||
//import forge.util.collect.FCollectionView;
|
||||
import forge.util.Lang;
|
||||
import forge.view.FDialog;
|
||||
|
||||
public class FloatingZone extends FloatingCardArea {
|
||||
private static final long serialVersionUID = 1927906492186378596L;
|
||||
|
||||
private static final Map<Integer, FloatingZone> floatingAreas = new HashMap<Integer, FloatingZone>();
|
||||
|
||||
private static int getKey(final PlayerView player, final ZoneType zone) {
|
||||
return 40 * player.getId() + zone.hashCode();
|
||||
}
|
||||
public static void showOrHide(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
|
||||
final FloatingZone cardArea = _init(matchUI, player, zone);
|
||||
cardArea.showOrHideWindow();
|
||||
}
|
||||
public static void show(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
|
||||
final FloatingZone cardArea = _init(matchUI, player, zone);
|
||||
cardArea.showWindow();
|
||||
}
|
||||
public static void hide(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
|
||||
final FloatingZone cardArea = _init(matchUI, player, zone);
|
||||
cardArea.hideWindow();
|
||||
}
|
||||
private static FloatingZone _init(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
|
||||
final int key = getKey(player, zone);
|
||||
FloatingZone cardArea = floatingAreas.get(key);
|
||||
if (cardArea == null || cardArea.getMatchUI() != matchUI) {
|
||||
cardArea = new FloatingZone(matchUI, player, zone);
|
||||
floatingAreas.put(key, cardArea);
|
||||
} else {
|
||||
cardArea.setPlayer(player); //ensure player is updated if needed
|
||||
}
|
||||
return cardArea;
|
||||
}
|
||||
public static CardPanel getCardPanel(final CMatchUI matchUI, final CardView card) {
|
||||
final FloatingZone window = _init(matchUI, card.getController(), card.getZone());
|
||||
return window.getCardPanel(card.getId());
|
||||
}
|
||||
public static void refresh(final PlayerView player, final ZoneType zone) {
|
||||
FloatingZone cardArea = floatingAreas.get(getKey(player, zone));
|
||||
if (cardArea != null) {
|
||||
cardArea.setPlayer(player); //ensure player is updated if needed
|
||||
cardArea.refresh();
|
||||
}
|
||||
|
||||
//refresh flashback zone when graveyard, library, or exile zones updated
|
||||
switch (zone) {
|
||||
case Graveyard:
|
||||
case Library:
|
||||
case Exile:
|
||||
refresh(player, ZoneType.Flashback);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
public static void closeAll() {
|
||||
for (final FloatingZone cardArea : floatingAreas.values()) {
|
||||
cardArea.window.setVisible(false);
|
||||
}
|
||||
floatingAreas.clear();
|
||||
}
|
||||
public static void refreshAll() {
|
||||
for (final FloatingZone cardArea : floatingAreas.values()) {
|
||||
cardArea.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private final ZoneType zone;
|
||||
private PlayerView player;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private final FDialog window = new FDialog(false, true, "0") {
|
||||
@Override
|
||||
public void setLocationRelativeTo(Component c) {
|
||||
//don't change location this way if dialog has already been shown or location was loaded from preferences
|
||||
if (hasBeenShown || locLoaded) { return; }
|
||||
super.setLocationRelativeTo(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b0) {
|
||||
if (isVisible() == b0) { return; }
|
||||
if (!b0 && hasBeenShown && locPref != null) {
|
||||
//update preference before hiding window, as otherwise its location will be 0,0
|
||||
prefs.setPref(locPref,
|
||||
getX() + COORD_DELIM + getY() + COORD_DELIM +
|
||||
getWidth() + COORD_DELIM + getHeight());
|
||||
//don't call prefs.save(), instead allowing them to be saved when match ends
|
||||
}
|
||||
super.setVisible(b0);
|
||||
if (b0) {
|
||||
refresh();
|
||||
hasBeenShown = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected FDialog getWindow() {
|
||||
return window;
|
||||
}
|
||||
protected Iterable<CardView> getCards() {
|
||||
return player.getCards(zone);
|
||||
}
|
||||
|
||||
private FloatingZone(final CMatchUI matchUI, final PlayerView player0, final ZoneType zone0) {
|
||||
super(matchUI, new FScrollPane(false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
|
||||
window.add(getScrollPane(), "grow, push");
|
||||
getScrollPane().setViewportView(this);
|
||||
setOpaque(false);
|
||||
switch (zone0) {
|
||||
case Exile:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_EXILE));
|
||||
break;
|
||||
case Graveyard:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_GRAVEYARD));
|
||||
break;
|
||||
case Hand:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_HAND));
|
||||
break;
|
||||
case Library:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_LIBRARY));
|
||||
break;
|
||||
case Flashback:
|
||||
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_FLASHBACK));
|
||||
break;
|
||||
default:
|
||||
locPref = null;
|
||||
break;
|
||||
}
|
||||
zone = zone0;
|
||||
setPlayer(player0);
|
||||
setVertical(true);
|
||||
}
|
||||
|
||||
private void setPlayer(PlayerView player0) {
|
||||
if (player == player0) { return; }
|
||||
player = player0;
|
||||
title = Lang.getPossessedObject(player0.getName(), zone.name()) + " (%d)";
|
||||
|
||||
boolean isAi = player0.isAI();
|
||||
switch (zone) {
|
||||
case Exile:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_EXILE : FPref.ZONE_LOC_HUMAN_EXILE;
|
||||
break;
|
||||
case Graveyard:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_GRAVEYARD : FPref.ZONE_LOC_HUMAN_GRAVEYARD;
|
||||
break;
|
||||
case Hand:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_HAND : FPref.ZONE_LOC_HUMAN_HAND;
|
||||
break;
|
||||
case Library:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_LIBRARY : FPref.ZONE_LOC_HUMAN_LIBRARY;
|
||||
break;
|
||||
case Flashback:
|
||||
locPref = isAi ? FPref.ZONE_LOC_AI_FLASHBACK : FPref.ZONE_LOC_HUMAN_FLASHBACK;
|
||||
break;
|
||||
default:
|
||||
locPref = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Forge: Play Magic: the Gathering.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package forge.view.arcane;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import forge.game.card.CardView;
|
||||
import forge.screens.match.CMatchUI;
|
||||
import forge.view.arcane.util.CardPanelMouseAdapter;
|
||||
import forge.view.FDialog;
|
||||
|
||||
import forge.toolbox.FButton;
|
||||
|
||||
// Show a list of cards in a new window, containing the moveable cards
|
||||
// Allow moves of the moveable cards to top, to bottom, or anywhere
|
||||
// Return a list of cards with the results of the moves
|
||||
// Really should have a difference between visible cards and moveable cards,
|
||||
// but that would require consirable changes to card panels and elsewhere
|
||||
public class ListCardArea extends FloatingCardArea {
|
||||
|
||||
private static ArrayList<CardView> cardList;
|
||||
private static ArrayList<CardView> moveableCards;
|
||||
|
||||
private static ListCardArea storedArea;
|
||||
private static FButton doneButton;
|
||||
private boolean toTop, toBottom, toAnywhere;
|
||||
|
||||
private ListCardArea(final CMatchUI matchUI) {
|
||||
super(matchUI);
|
||||
window.add(getScrollPane(),"grow, push");
|
||||
getScrollPane().setViewportView(this);
|
||||
doneButton = new FButton("Done");
|
||||
doneButton.addActionListener(new ActionListener() {
|
||||
@Override public void actionPerformed(ActionEvent e) { window.setVisible(false); }
|
||||
});
|
||||
window.add(doneButton,BorderLayout.SOUTH);
|
||||
setOpaque(false);
|
||||
}
|
||||
|
||||
public static ListCardArea show(final CMatchUI matchUI, final String title0, final Iterable<CardView> cardList0, final Iterable<CardView> moveableCards0, final boolean toTop0, final boolean toBottom0, final boolean toAnywhere0) {
|
||||
if (storedArea==null) {
|
||||
storedArea = new ListCardArea(matchUI);
|
||||
}
|
||||
cardList = new ArrayList<CardView>();
|
||||
for ( CardView cv : cardList0 ) { cardList.add(cv) ; }
|
||||
moveableCards = new ArrayList<CardView>(); // make sure moveable cards are in cardlist
|
||||
for ( CardView card : moveableCards0 ) {
|
||||
if ( cardList.contains(card) ) {
|
||||
moveableCards.add(card);
|
||||
}
|
||||
}
|
||||
storedArea.title = title0;
|
||||
storedArea.toTop = toTop0;
|
||||
storedArea.toBottom = toBottom0;
|
||||
storedArea.toAnywhere = toAnywhere0;
|
||||
storedArea.setDragEnabled(true);
|
||||
storedArea.setVertical(true);
|
||||
storedArea.doRefresh();
|
||||
storedArea.showWindow();
|
||||
return storedArea;
|
||||
}
|
||||
|
||||
public ListCardArea(final CMatchUI matchUI, final String title0, final List<CardView> cardList0, final List<CardView> moveableCards0, final boolean toTop0, final boolean toBottom0, final boolean toAnywhere0) {
|
||||
super(matchUI);
|
||||
window.add(getScrollPane(),"grow, push");
|
||||
getScrollPane().setViewportView(this);
|
||||
setOpaque(false);
|
||||
doneButton = new FButton("Done");
|
||||
doneButton.addActionListener(new ActionListener() {
|
||||
@Override public void actionPerformed(ActionEvent e) { window.setVisible(false); }
|
||||
});
|
||||
window.add(doneButton,BorderLayout.SOUTH);
|
||||
cardList = new ArrayList<CardView>(cardList0); // this is modified - pfps - is there a better way?
|
||||
moveableCards = new ArrayList<CardView>(moveableCards0);
|
||||
title = title0;
|
||||
toTop = toTop0;
|
||||
toBottom = toBottom0;
|
||||
toAnywhere = toAnywhere0;
|
||||
this.setDragEnabled(true);
|
||||
this.setVertical(true);
|
||||
storedArea = this;
|
||||
}
|
||||
|
||||
public List<CardView> getCards() {
|
||||
return cardList;
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected final FDialog window = new FDialog(true, true, "0") {
|
||||
@Override
|
||||
public void setLocationRelativeTo(Component c) {
|
||||
if (hasBeenShown || locLoaded) { return; }
|
||||
super.setLocationRelativeTo(c);
|
||||
}
|
||||
@Override
|
||||
public void setVisible(boolean b0) {
|
||||
if (isVisible() == b0) { return; }
|
||||
if (!b0 && hasBeenShown && locPref != null) {
|
||||
//update preference before hiding window, as otherwise its location will be 0,0
|
||||
prefs.setPref(locPref,
|
||||
getX() + COORD_DELIM + getY() + COORD_DELIM +
|
||||
getWidth() + COORD_DELIM + getHeight());
|
||||
//don't call prefs.save(), instead allowing them to be saved when match ends
|
||||
}
|
||||
super.setVisible(b0);
|
||||
if (b0) {
|
||||
refresh();
|
||||
hasBeenShown = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected FDialog getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showWindow() {
|
||||
onShow();
|
||||
getWindow().setFocusableWindowState(true);
|
||||
getWindow().setVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShow() {
|
||||
if (!hasBeenShown) {
|
||||
loadLocation();
|
||||
this.addCardPanelMouseListener(new CardPanelMouseAdapter() {
|
||||
@Override
|
||||
public void mouseDragEnd(final CardPanel dragPanel, final MouseEvent evt) {
|
||||
dragEnd(dragPanel);
|
||||
}
|
||||
});
|
||||
this.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(final KeyEvent e) {
|
||||
switch (e.getKeyCode()) {
|
||||
case KeyEvent.VK_ENTER:
|
||||
doneButton.doClick();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// is this a valid place to move the card?
|
||||
private boolean validIndex(final CardView card, final int index) {
|
||||
if (toAnywhere) { return true; }
|
||||
int oldIndex = cardList.indexOf(card);
|
||||
boolean topMove = true;
|
||||
for(int i=0; i<index+(oldIndex<index?1:0); i++) {
|
||||
if (!moveableCards.contains(cardList.get(i))) { topMove=false; break; }
|
||||
}
|
||||
if (toTop && topMove) { return true; }
|
||||
boolean bottomMove = true;
|
||||
for(int i=index+1-(oldIndex>index?1:0); i<cardList.size(); i++) {
|
||||
if (!moveableCards.contains(cardList.get(i))) { bottomMove=false; break; }
|
||||
}
|
||||
if (toBottom && bottomMove) { return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean cardPanelDraggable(final CardPanel panel) {
|
||||
return moveableCards.contains(panel.getCard());
|
||||
}
|
||||
|
||||
private void dragEnd(final CardPanel dragPanel) {
|
||||
// if drag is not allowed, don't move anything
|
||||
final CardView dragCard = dragPanel.getCard();
|
||||
if (moveableCards.contains(dragCard)) {
|
||||
//update index of dragged card in hand zone to match new index within hand area
|
||||
final int index = getCardPanels().indexOf(dragPanel);
|
||||
if (validIndex(dragCard,index)) {
|
||||
synchronized (cardList) {
|
||||
cardList.remove(dragCard);
|
||||
cardList.add(index, dragCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
// @Override
|
||||
// protected void refresh() {
|
||||
// doRefresh();
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void doLayout() {
|
||||
// if (window.isResizing()) {
|
||||
// //delay layout slightly to reduce flicker during window resize
|
||||
// layoutTimer.restart();
|
||||
// }
|
||||
//else {
|
||||
finishDoLayout();
|
||||
//}
|
||||
}
|
||||
|
||||
// move to beginning of list if allowable else to beginning of bottom if allowable
|
||||
@Override
|
||||
public final void mouseLeftClicked(final CardPanel panel, final MouseEvent evt) {
|
||||
final CardView clickCard = panel.getCard();
|
||||
if ( moveableCards.contains(clickCard) ) {
|
||||
if ( toTop || toBottom ) {
|
||||
synchronized (cardList) {
|
||||
cardList.remove(clickCard);
|
||||
int position;
|
||||
if ( toTop ) {
|
||||
position = 0 ;
|
||||
} else { // to beginning of bottom: warning, untested
|
||||
for ( position = cardList.size() ;
|
||||
position>0 && moveableCards.contains(cardList.get(position-1)) ;
|
||||
position-- );
|
||||
}
|
||||
cardList.add(position,clickCard);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
super.mouseLeftClicked(panel, evt);
|
||||
}
|
||||
@Override
|
||||
public final void mouseRightClicked(final CardPanel panel, final MouseEvent evt) {
|
||||
final CardView clickCard = panel.getCard();
|
||||
if (moveableCards.contains(clickCard)) {
|
||||
if ( toTop || toBottom ) {
|
||||
synchronized (cardList) {
|
||||
cardList.remove(clickCard);
|
||||
int position;
|
||||
if ( toBottom ) {
|
||||
position = cardList.size() ;
|
||||
} else { // to end of top
|
||||
for ( position = 0 ;
|
||||
position<cardList.size() && moveableCards.contains(cardList.get(position)) ;
|
||||
position++ );
|
||||
}
|
||||
cardList.add(position,clickCard);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
super.mouseRightClicked(panel, evt);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -184,7 +184,7 @@ public class GameSimulatorTest extends SimulationTestCase {
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
SpellAbility unmorphSA = findSAWithPrefix(ripper, "Morph—Reveal a black card");
|
||||
SpellAbility unmorphSA = findSAWithPrefix(ripper, "Morph — Reveal a black card");
|
||||
assertNotNull(unmorphSA);
|
||||
sim.simulateSpellAbility(unmorphSA);
|
||||
assertEquals(18, simGame.getPlayers().get(0).getLife());
|
||||
@@ -1503,4 +1503,197 @@ public class GameSimulatorTest extends SimulationTestCase {
|
||||
}
|
||||
|
||||
|
||||
public void testRiotEnchantment() {
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(0);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
|
||||
final String goblinName = "Zhur-Taa Goblin";
|
||||
|
||||
addCard("Rhythm of the Wild", p);
|
||||
|
||||
Card goblin = addCardToZone(goblinName, p, ZoneType.Hand);
|
||||
|
||||
addCard("Mountain", p);
|
||||
addCard("Forest", p);
|
||||
|
||||
SpellAbility goblinSA = goblin.getFirstSpellAbility();
|
||||
assertNotNull(goblinSA);
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
int score = sim.simulateSpellAbility(goblinSA).value;
|
||||
assertTrue(score > 0);
|
||||
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
Card simGoblin = findCardWithName(simGame, goblinName);
|
||||
|
||||
assertNotNull(simGoblin);
|
||||
int effects = simGoblin.getCounters(CounterType.P1P1) + simGoblin.getKeywordMagnitude(Keyword.HASTE);
|
||||
assertTrue(effects == 2);
|
||||
}
|
||||
|
||||
public void testTeysaKarlovXathridNecromancer() {
|
||||
// Teysa Karlov and Xathrid Necromancer dying at the same time makes 4 token
|
||||
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(0);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
|
||||
addCard("Teysa Karlov", p);
|
||||
addCard("Xathrid Necromancer", p);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
addCardToZone("Plains", p, ZoneType.Battlefield);
|
||||
}
|
||||
|
||||
Card wrathOfGod = addCardToZone("Wrath of God", p, ZoneType.Hand);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
|
||||
SpellAbility wrathSA = wrathOfGod.getFirstSpellAbility();
|
||||
assertNotNull(wrathSA);
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
int score = sim.simulateSpellAbility(wrathSA).value;
|
||||
assertTrue(score > 0);
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
int numZombies = countCardsWithName(simGame, "Zombie");
|
||||
assertTrue(numZombies == 4);
|
||||
}
|
||||
|
||||
public void testDoubleTeysaKarlovXathridNecromancer() {
|
||||
// Teysa Karlov dieing because of Legendary rule will make Xathrid Necromancer trigger 3 times
|
||||
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(0);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
|
||||
addCard("Teysa Karlov", p);
|
||||
addCard("Xathrid Necromancer", p);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
addCard("Plains", p);
|
||||
}
|
||||
addCard("Swamp", p);
|
||||
|
||||
Card second = addCardToZone("Teysa Karlov", p, ZoneType.Hand);
|
||||
|
||||
SpellAbility secondSA = second.getFirstSpellAbility();
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
int score = sim.simulateSpellAbility(secondSA).value;
|
||||
assertTrue(score > 0);
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
int numZombies = countCardsWithName(simGame, "Zombie");
|
||||
assertTrue(numZombies == 3);
|
||||
}
|
||||
|
||||
|
||||
public void testTeysaKarlovGitrogMonster() {
|
||||
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(0);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
|
||||
addCard("Teysa Karlov", p);
|
||||
addCard("The Gitrog Monster", p);
|
||||
addCard("Dryad Arbor", p);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
addCard("Plains", p);
|
||||
addCardToZone("Plains", p, ZoneType.Library);
|
||||
}
|
||||
|
||||
Card armageddon = addCardToZone("Armageddon", p, ZoneType.Hand);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
|
||||
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
int score = sim.simulateSpellAbility(armageddonSA).value;
|
||||
assertTrue(score > 0);
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
// Two cards drawn
|
||||
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 2);
|
||||
}
|
||||
|
||||
public void testTeysaKarlovGitrogMonsterGitrogDies() {
|
||||
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(0);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
|
||||
Card teysa = addCard("Teysa Karlov", p);
|
||||
addCard("The Gitrog Monster", p);
|
||||
addCard("Dryad Arbor", p);
|
||||
|
||||
String indestructibilityName = "Indestructibility";
|
||||
Card indestructibility = addCard(indestructibilityName, p);
|
||||
|
||||
indestructibility.attachToEntity(teysa);
|
||||
|
||||
// update Indestructible state
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
addCard("Plains", p);
|
||||
addCardToZone("Plains", p, ZoneType.Library);
|
||||
}
|
||||
|
||||
Card armageddon = addCardToZone("Wrath of God", p, ZoneType.Hand);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
|
||||
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
int score = sim.simulateSpellAbility(armageddonSA).value;
|
||||
assertTrue(score > 0);
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
// One cards drawn
|
||||
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 1);
|
||||
}
|
||||
|
||||
public void testTeysaKarlovGitrogMonsterTeysaDies() {
|
||||
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(0);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
|
||||
addCard("Teysa Karlov", p);
|
||||
Card gitrog = addCard("The Gitrog Monster", p);
|
||||
addCard("Dryad Arbor", p);
|
||||
|
||||
String indestructibilityName = "Indestructibility";
|
||||
Card indestructibility = addCard(indestructibilityName, p);
|
||||
|
||||
indestructibility.attachToEntity(gitrog);
|
||||
|
||||
// update Indestructible state
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
addCard("Plains", p);
|
||||
addCardToZone("Plains", p, ZoneType.Library);
|
||||
}
|
||||
|
||||
Card armageddon = addCardToZone("Wrath of God", p, ZoneType.Hand);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
|
||||
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
int score = sim.simulateSpellAbility(armageddonSA).value;
|
||||
assertTrue(score > 0);
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
// One cards drawn
|
||||
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,13 @@ public class PlayerControllerForTests extends PlayerController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer) {
|
||||
public <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer) {
|
||||
// this isn't used
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2, boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
|
||||
// this isn't used
|
||||
return null;
|
||||
}
|
||||
@@ -624,7 +630,7 @@ public class PlayerControllerForTests extends PlayerController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, DelayedReveal delayedReveal, String selectPrompt, Player decider) {
|
||||
public List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max, DelayedReveal delayedReveal, String selectPrompt, Player decider) {
|
||||
// this isn't used
|
||||
return null;
|
||||
}
|
||||
@@ -678,4 +684,10 @@ public class PlayerControllerForTests extends PlayerController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmMulliganScry(Player p) {
|
||||
// TODO Auto-generated method stub
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-gui-ios</artifactId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-gui-mobile-dev</artifactId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-gui-mobile</artifactId>
|
||||
|
||||
@@ -730,7 +730,7 @@ public class FDeckEditor extends TabPageScreen<FDeckEditor> {
|
||||
if (deck == null || card == null) {
|
||||
max = Integer.MAX_VALUE;
|
||||
}
|
||||
else if (limit == CardLimit.None || card.getRules().getType().isBasic() || DeckFormat.getLimitExceptions().contains(card.getName())) {
|
||||
else if (limit == CardLimit.None || DeckFormat.canHaveAnyNumberOf(card)) {
|
||||
max = Integer.MAX_VALUE;
|
||||
if (parentScreen.isLimitedEditor() && !isAddSource) {
|
||||
//prevent adding more than is in other pool when editing limited decks
|
||||
|
||||
@@ -343,6 +343,16 @@ public class MatchController extends AbstractGuiGame {
|
||||
view.updateZones(zonesToUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<PlayerZoneUpdate> tempShowZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
|
||||
return view.tempShowZones(controller, zonesToUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
|
||||
view.hideZones(controller, zonesToUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCards(final Iterable<CardView> cards) {
|
||||
for (final CardView card : cards) {
|
||||
@@ -506,8 +516,16 @@ public class MatchController extends AbstractGuiGame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GameEntityView> chooseEntitiesForEffect(String title, List<? extends GameEntityView> optionList, DelayedReveal delayedReveal) {
|
||||
return SGuiChoose.order(title, "Selected", 0, -1, (List<GameEntityView>) optionList, null);
|
||||
public List<GameEntityView> chooseEntitiesForEffect(String title, List<? extends GameEntityView> optionList, int min, int max, DelayedReveal delayedReveal) {
|
||||
final int m1 = max >= 0 ? optionList.size() - max : -1;
|
||||
final int m2 = min >= 0 ? optionList.size() - min : -1;
|
||||
return SGuiChoose.order(title, "Selected", m1, m2, (List<GameEntityView>) optionList, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CardView> manipulateCardList(final String title, final Iterable<CardView> cards, final Iterable<CardView> manipulable, final boolean toTop, final boolean toBottom, final boolean toAnywhere) {
|
||||
System.err.println("Not implemented yet - should never be called");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -468,6 +468,15 @@ public class MatchScreen extends FScreen {
|
||||
}
|
||||
}
|
||||
|
||||
public Iterable<PlayerZoneUpdate> tempShowZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
|
||||
// pfps needs to actually do something
|
||||
return zonesToUpdate; // pfps should return only those zones newly shown
|
||||
}
|
||||
|
||||
public void hideZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
|
||||
// pfps needs to actually do something
|
||||
}
|
||||
|
||||
public void updateSingleCard(final CardView card) {
|
||||
final CardAreaPanel pnl = CardAreaPanel.get(card);
|
||||
if (pnl == null) { return; }
|
||||
|
||||
@@ -244,8 +244,8 @@ public class GuiChoose {
|
||||
}
|
||||
|
||||
public static <T> void many(final String title, final String topCaption, int min, int max, final List<T> sourceChoices, CardView referenceCard, final Callback<List<T>> callback) {
|
||||
int m2 = min >= 0 ? sourceChoices.size() - min : -1;
|
||||
int m1 = max >= 0 ? sourceChoices.size() - max : -1;
|
||||
int m2 = min >= 0 ? sourceChoices.size() - min : -1;
|
||||
order(title, topCaption, m1, m2, sourceChoices, null, referenceCard, callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,92 @@
|
||||
This file is automatically updated by our release bot on Discord, Blacksmith. It is created from the files present in the 'release-files' directory. Please do not hand-edit this file if using the bot to perform a release, as your changes will be overwritten.
|
||||
Forge: 02/02/2019 ver 1.6.20
|
||||
|
||||
18457 cards in total.
|
||||
|
||||
|
||||
--------------
|
||||
Release Notes:
|
||||
--------------
|
||||
|
||||
- New Cards -
|
||||
Vizkopa Vampire; Vengeant Vampire; Vindictive Vampire; Verity Circle; Zegana, Utopian Speaker; Zhur-Taa Goblin; Kaya, Orzhov Usurper; Knight of the Last Breath; Knight of Sorrows; Kaya's Wrath; Thought Collapse; Tenth District Veteran; Territorial Boar; Teysa Karlov; Theater of Horrors; Trollbred Guardian; Tithe Taker; Thirsting Shade; Tome of the Guildpact; Thrash; Twilight Panther; Tin Street Dodger; The Haunt of Hightower; Titanic Brawl; Cindervines; Combine Guildmage; Carrion Imp; Consecrate; Collision; Code of Constraint; Clear the Stage; Captive Audience; Clamor Shaman; Consign to the Pit; Cavalcade of Calamity; Clear the Mind; Civic Stalwart; Carnival; Catacomb Crocodile; Charging War Boar; Cry of the Carnarium; Coral Commando; Clan Guildmage; Cult Guildmage; Chillbringer; Growth Spiral; Galloping Lizrog; Gruul Locket; Get the Point; Gruul Spellbreaker; Gravel-Hide Goblin; Growth-Chamber Guardian; Gyre Engineer; Glass of the Guildpact; Guardian Project; Grotesque Demise; Gateway Sneak; Ghor-Clan Wrecker; Gates Ablaze; Gutterbones; Goblin Goliath; Grasping Thrull; Goblin Gathering; Gatebreaker Ram; Gruul Beastmaster; Gate Colossus; Windstorm Drake; Wrecking Beast; Watchful Giant; Warrant; Wilderness Reclamation; Wall of Lost Thoughts; Impassioned Orator; Incubation Druid; Inspired Sphinx; Immortal Phoenix; Immolation Shaman; Incubation; Imperious Oligarch; Ill-Gotten Inheritance; Dovin's Automaton; Dovin's Dismissal; Debtors' Transport; Deface; Dovin's Acuity; Drill Bit; Dagger Caster; Dovin, Grand Arbiter; Deputy of Detention; Domri, City Smasher; Dead Revels; Domri's Nodorog; Domri, Chaos Bringer; Dovin, Architect of Law; Depose; Justiciar's Portal; Judith, the Scourge Diva; Angelic Exaltation; Applied Biomancy; Axebane Beast; Aeromunculus; Awaken the Erstwhile; Avatar of Growth; Angler Turtle; Angel of Grace; Azorius Locket; Azorius Skyguard; Arrester's Admonition; Arrester's Zeal; Azorius Knight-Arbiter; Amplifire; Angelic Guardian; Archway Angel; Prying Eyes; Prime Speaker Vannifar; Pteramander; Plaza of Harmony; Pestilent Spirit; Prowling Caracal; Persistent Petitioners; Pitiless Pontiff; Priest of Forgotten Gods; Plague Wight; Precognitive Perception; Rumbling Ruin; Ragefire; Rot Hulk; Rakdos Locket; Repudiate; Ravager Wurm; Rix Maadi Reveler; Rally to Battle; Rakdos Firewheeler; Rakdos Roustabout; Rhythm of the Wild; Rubblebelt Recluse; Rafter Demon; Rubble Reading; Rampage of the Clans; Rampaging Rendhorn; Resolute Watchdog; Rampaging Brontodon; Rubblebelt Runner; Rubble Slinger; Rakdos Trumpeter; Rakdos, the Showstopper; Regenesis; Revival; Lavinia, Azorius Renegade; Lumbering Battlement; Light Up the Stage; Lawmage's Binding; Hero of Precinct One; Haazda Officer; Hydroid Krasis; Humongulus; High Alert; Hackrobat; Noxious Groodion; Nikya of the Old Ways; Seraph of the Scales; Sharktocrab; Swirling Torrent; Summary Judgment; Skitter Eel; Stony Strength; Sphinx of Foresight; Spawn of Mayhem; Sphinx of the Guildpact; Spire Mangler; Skatewing Spy; Sphinx's Insight; Senate Griffin; Spirit of the Spires; Silhana Wayfinder; Spear Spewer; Smelt-Ward Ignus; Senate Guildmage; Skewer the Critics; Syndicate Guildmage; Slimebind; Savage Smash; Sky Tether; Spikewheel Acrobat; Sentinel's Mark; Sagittars' Volley; Saruli Caretaker; Shimmer of Possibility; Scuttlegator; Senate Courier; Sage's Row Savant; Sunder Shaman; Screaming Shield; Skarrgan Hellkite; Scorchmark; Storm Strike; Smothering Tithe; Steeple Creeper; Syndicate Messenger; Sylvan Brushstrider; Simic Locket; Sphinx of New Prahv; Simic Ascendancy; Sauroform Hybrid; Bloodmist Infiltrator; Basilica Bell-Haunt; Burning-Tree Vandal; Biogenic Upgrade; Bankrupt in Blood; Bolrac-Clan Crusher; Benthic Biomancer; Biomancer's Familiar; Bring to Trial; Bedevil; Bedeck; Blade Juggler; Burn Bright; Bladebrand; Biogenic Ooze; Emergency Powers; Electrodominance; Eyes Everywhere; Essence Capture; Elite Arrester; Ethereal Absolution; End-Raze Forerunners; Expose to Daylight; Enraged Ceratok; Undercity Scavenger; Undercity's Embrace; Unbreakable Formation; Orzhov Locket; Open the Gates; Orzhov Racketeers; Orzhov Enforcer; Frenzied Arynx; Faerie Duelist; Footlight Fiend; Fireblade Artist; Frilled Mystic; Flames of the Raze-Boar; Final Payment; Feral Maaka; Font of Agonies; Forbidding Spirit; Ministrant of Obligation; Mirror March; Mesmerizing Benthid; Macabre Mockery; Militant Angel; Mass Manipulation; Quench
|
||||
|
||||
- Desktop GUI -
|
||||
The Desktop GUI can pop up zones (Library, Graveyard, etc.) allow players to select cards from them when the option UI_SELECT_FROM_CARD_DISPLAYS is set.
|
||||
The Desktop GUI outlines the selectable cards in many situations. This is not done when playing mana costs.
|
||||
|
||||
- Digging -
|
||||
Multi-card digging (e.g., for Genesis Wave) is done as a single multiple-card selection instead of a sequence of single-card selections.
|
||||
|
||||
- Game Night -
|
||||
Support was added for the Game Night box set, including all 10 exclusive cards.
|
||||
|
||||
- AI improvements -
|
||||
More AI improvements were implemented, hopefully making the game a little more interesting and challenging to play.
|
||||
|
||||
- Bug fixes -
|
||||
As always, this release of Forge features an assortment of bug fixes and improvements based on user feedback during the previous release run.
|
||||
|
||||
-------------
|
||||
Known Issues:
|
||||
-------------
|
||||
|
||||
Known issues are here: https://git.cardforge.org/core-developers/forge/issues
|
||||
|
||||
Feel free to report your own there if you have any.
|
||||
|
||||
-------------
|
||||
Installation:
|
||||
-------------
|
||||
|
||||
The Forge archive includes a MANUAL.txt file and we ask that you spend a few minutes reading this file as it contains some information that may prove useful. We do tend to update this file at times and you should quickly read this file and look for new information for each and every new release. Thank you.
|
||||
|
||||
The archive format used for the Forge distribution is ".tar.bz2". There are utilities for Windows, Mac OS and the various *nix's that can be used to extract/decompress these ".tar.bz2" archives. We recommend that you extract/decompress the Forge archive into a new and unused folder.
|
||||
|
||||
Some people use the Windows application 7zip. This utility can be found at http://www.7-zip.org/download.html. Mac users can double click on the archive and the application Archive Utility will launch and extract the archive. Mac users do not need to download a separate utility.
|
||||
|
||||
Once the Forge archive has been decompressed you should then be able to launch Forge by using the included launcher. Launching Forge by double clicking on the forge jar file in the past caused a java heap space error. Forge's memory requirements have increased over time and the launchers increase the java heap space available to Forge. Currently you can launch Forge by double clicking on the forge jar file without a java heap space error but this is likely to change as we add in more sounds, icons, etc.
|
||||
|
||||
- The Mac OS application version -
|
||||
We haven't been able to distribute the OS X Application version of Forge in sometime. We've recently automated our release tools, and will continue to look in the viability of creating this file now that things are autoamted.
|
||||
|
||||
|
||||
- Online Multiplayer -
|
||||
For local network play you should only need two systems running Forge. One to host and one to join and play. For remote (over the Internet) play you will need to ensure that the port used (36743 by default) is forwarded to the hosting machine.
|
||||
|
||||
--------------------
|
||||
Active Contributors:
|
||||
--------------------
|
||||
|
||||
Agetian
|
||||
Austinio7116
|
||||
Churrufli
|
||||
DrDev
|
||||
excessum
|
||||
Gos
|
||||
Hanmac
|
||||
Indigo Dragon
|
||||
Jamin Collins
|
||||
KrazyTheFox
|
||||
Luke
|
||||
Marek14
|
||||
mcrawford620
|
||||
Meerkov
|
||||
Myrd
|
||||
nefigah
|
||||
OgreBattlecruiser
|
||||
pfps
|
||||
Seravy
|
||||
Sirspud
|
||||
Sloth
|
||||
slyfox7777777
|
||||
Sol
|
||||
Swordshine
|
||||
tjtillman
|
||||
tojammot
|
||||
torridus
|
||||
Xyx
|
||||
Zuchinni
|
||||
|
||||
(Quest icons used created by Teekatas, from his Legendora set http://raindropmemory.deviantart.com)
|
||||
(Thanks to the XMage team for permission to use their targeting arrows.)
|
||||
(Thanks to http://www.freesound.org/browse/ for providing some sound files.)
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.20-SNAPSHOT</version>
|
||||
<version>1.6.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-gui</artifactId>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
- Desktop GUI -
|
||||
The Desktop GUI can pop up zones (Library, Graveyard, etc.) allow players to select cards from them when the option UI_SELECT_FROM_CARD_DISPLAYS is set.
|
||||
The Desktop GUI outlines the selectable cards in many situations. This is not done when playing mana costs.
|
||||
|
||||
- Digging -
|
||||
Multi-card digging (e.g., for Genesis Wave) is done as a single multiple-card selection instead of a sequence of single-card selections.
|
||||
|
||||
- Game Night -
|
||||
Support was added for the Game Night box set, including all 10 exclusive cards.
|
||||
|
||||
|
||||
@@ -78,3 +78,4 @@ Dominaria, 3/6/DOM, DOM
|
||||
Core Set 2019, 3/6/M19, M19
|
||||
Guilds of Ravnica, 3/6/GRN, GRN
|
||||
Ultimate Masters, 3/6/M19, UMA
|
||||
Ravnica Allegiance, 3/6/RNA, RNA
|
||||
@@ -105,3 +105,4 @@ BBD: 36 Boosters
|
||||
M19: 36 Boosters
|
||||
GRN: 36 Boosters
|
||||
UMA: 24 Boosters
|
||||
RNA: 36 Boosters
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user