mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 19:28:01 +00:00
Compare commits
341 Commits
daily-snap
...
forge-2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f32e5772 | ||
|
|
43ce790ebe | ||
|
|
b1a3ad1b39 | ||
|
|
9109b26484 | ||
|
|
98e5eb9652 | ||
|
|
4e93f95dff | ||
|
|
44e1332fb4 | ||
|
|
e2972acad0 | ||
|
|
76086ffd35 | ||
|
|
3c3c47616b | ||
|
|
de5a660a6a | ||
|
|
d348a72ae4 | ||
|
|
4055512698 | ||
|
|
b4b2346fb8 | ||
|
|
caa5443874 | ||
|
|
c7fb0bd3c3 | ||
|
|
1f5f62b21a | ||
|
|
04937b6447 | ||
|
|
94ea88b171 | ||
|
|
2188582e16 | ||
|
|
fbc73fa22b | ||
|
|
31790c3dc9 | ||
|
|
cdf3038ab6 | ||
|
|
ff1781b734 | ||
|
|
49d7351eac | ||
|
|
3b372eead7 | ||
|
|
c815f7ed0c | ||
|
|
04ac8d8de9 | ||
|
|
6b961ed3e1 | ||
|
|
17a2a23981 | ||
|
|
74e83f2a94 | ||
|
|
8e25dd0e25 | ||
|
|
7f9260f54c | ||
|
|
505d4b4f9a | ||
|
|
baca196066 | ||
|
|
b08cf3bf5b | ||
|
|
b5ee3eb43c | ||
|
|
081e1a2960 | ||
|
|
d0310e257b | ||
|
|
a3733f1fa8 | ||
|
|
b4bd7947f9 | ||
|
|
b56da433eb | ||
|
|
64b5906f08 | ||
|
|
f7d94fabc9 | ||
|
|
87e0810603 | ||
|
|
93f2fa8f43 | ||
|
|
c86bf402fd | ||
|
|
9926004cf1 | ||
|
|
d0c24f49a9 | ||
|
|
661f3b8e7a | ||
|
|
14249150a0 | ||
|
|
93dccdeace | ||
|
|
706ef4ac6c | ||
|
|
bd3994a217 | ||
|
|
e722c4b63c | ||
|
|
a062e0040d | ||
|
|
7fdd645026 | ||
|
|
3670891ec9 | ||
|
|
6f3dd8deba | ||
|
|
63ac4a3ee4 | ||
|
|
4616ee715e | ||
|
|
5ef6bf1c15 | ||
|
|
862b4e19b6 | ||
|
|
378524dc39 | ||
|
|
6d5f45a311 | ||
|
|
0e00a52eb4 | ||
|
|
4b69d16c6d | ||
|
|
92e17a66f2 | ||
|
|
b601431591 | ||
|
|
76db40189e | ||
|
|
8ddf8225c0 | ||
|
|
359dd8d641 | ||
|
|
878da9b06f | ||
|
|
d843004ad6 | ||
|
|
7f6024f81f | ||
|
|
5a37b49fcd | ||
|
|
1c8cdac5be | ||
|
|
20bd27d487 | ||
|
|
fc761220d2 | ||
|
|
144681012c | ||
|
|
8fe3bd3c79 | ||
|
|
da65308cf2 | ||
|
|
011457e949 | ||
|
|
c296025837 | ||
|
|
ac4c501629 | ||
|
|
fe062a9312 | ||
|
|
65d4505b67 | ||
|
|
073d7e537c | ||
|
|
77dc367c95 | ||
|
|
7553d164f4 | ||
|
|
ee01e3d29f | ||
|
|
58f8c39197 | ||
|
|
7304fa862a | ||
|
|
1ef8b9ca47 | ||
|
|
5191d2f9c4 | ||
|
|
ff74e36fe5 | ||
|
|
0121619e93 | ||
|
|
aeb279a6f8 | ||
|
|
20815552b9 | ||
|
|
ac67a36ccf | ||
|
|
fcd8b8fd35 | ||
|
|
fbe4ad5c44 | ||
|
|
ee17483fff | ||
|
|
a451f1a234 | ||
|
|
52c4c01a7d | ||
|
|
b1bb0d669f | ||
|
|
eb1f9783aa | ||
|
|
0724d224fa | ||
|
|
e7775cdfa9 | ||
|
|
f8836f0c40 | ||
|
|
4c342cfc6a | ||
|
|
df05ab34fb | ||
|
|
5da0e75252 | ||
|
|
92ec5d8f64 | ||
|
|
6515fed9d2 | ||
|
|
5a7cd40614 | ||
|
|
65b01e0822 | ||
|
|
643f893d43 | ||
|
|
e0c6b43214 | ||
|
|
0f5d71f933 | ||
|
|
e6a8b5ed74 | ||
|
|
24c11e47c4 | ||
|
|
cb5f805767 | ||
|
|
3788e01f38 | ||
|
|
a9df4ea424 | ||
|
|
25ba06d530 | ||
|
|
9b81644f11 | ||
|
|
4b27536ed3 | ||
|
|
148da24456 | ||
|
|
d1be43fd83 | ||
|
|
f2df505237 | ||
|
|
bd37e26fab | ||
|
|
7954473476 | ||
|
|
13287cefbd | ||
|
|
9afbc91de1 | ||
|
|
dfe5bd9ec9 | ||
|
|
e2411e34bd | ||
|
|
f2998bdf9a | ||
|
|
c52f886e89 | ||
|
|
f972aa44ba | ||
|
|
ccafe0557f | ||
|
|
f9f9b1a1f9 | ||
|
|
e867aacbf5 | ||
|
|
a09e9e4fd6 | ||
|
|
fc320e6524 | ||
|
|
2b6a1c9f3d | ||
|
|
3e9cd2c226 | ||
|
|
3f722abba2 | ||
|
|
06508f70a3 | ||
|
|
8580108d1e | ||
|
|
300f34377c | ||
|
|
c4828f510f | ||
|
|
04c400553a | ||
|
|
d2508333bc | ||
|
|
fc901f1ebb | ||
|
|
c365f5a3d1 | ||
|
|
49697c863c | ||
|
|
4431c40de6 | ||
|
|
a0f6efb959 | ||
|
|
44fea0ae75 | ||
|
|
95c970e23f | ||
|
|
8596151fa1 | ||
|
|
2eac43734c | ||
|
|
dff91eb2aa | ||
|
|
aa122700a9 | ||
|
|
80f267df59 | ||
|
|
235618c3bb | ||
|
|
b7e55e785e | ||
|
|
050c986d08 | ||
|
|
2b83541ebc | ||
|
|
d40894ef6a | ||
|
|
b756bda988 | ||
|
|
0e64a88005 | ||
|
|
0d952a54bd | ||
|
|
d3ff7f3b61 | ||
|
|
0f9e7eca89 | ||
|
|
207b786fcd | ||
|
|
0f6fa87da0 | ||
|
|
995c1167dc | ||
|
|
1ab1d9c002 | ||
|
|
4b1a6a2f87 | ||
|
|
dacecd9006 | ||
|
|
dd5d75613e | ||
|
|
d59a316d8c | ||
|
|
9c4f855f71 | ||
|
|
90131b4a70 | ||
|
|
fb6725f2d7 | ||
|
|
475c57af55 | ||
|
|
6ae119a415 | ||
|
|
8b5fe276e7 | ||
|
|
5e4d5c262d | ||
|
|
9b82f1ef1f | ||
|
|
321d2d7e33 | ||
|
|
5b7cca95e1 | ||
|
|
2e0d53c6fe | ||
|
|
9d4f6d2cbb | ||
|
|
9546b434e4 | ||
|
|
f230522657 | ||
|
|
a57a1f566a | ||
|
|
8f8d6e6e30 | ||
|
|
3b8694483c | ||
|
|
4328a12967 | ||
|
|
fdc85b85c3 | ||
|
|
bb2eed23b7 | ||
|
|
72e33146de | ||
|
|
e371617938 | ||
|
|
b363db2bbd | ||
|
|
37a5958750 | ||
|
|
9091cfe3b0 | ||
|
|
e2614187ac | ||
|
|
5c69bf0470 | ||
|
|
383dc85166 | ||
|
|
7e47208888 | ||
|
|
137d87c3df | ||
|
|
fbff1fe10a | ||
|
|
3acd4490c5 | ||
|
|
2952ed79f8 | ||
|
|
cb23eda5a6 | ||
|
|
ced87c8aea | ||
|
|
daf87e26ad | ||
|
|
0c30c4e32c | ||
|
|
bf5f9f69ca | ||
|
|
b4828d3b4d | ||
|
|
617df8e07c | ||
|
|
deaba89f46 | ||
|
|
d5b13d56cc | ||
|
|
f893c7ddf8 | ||
|
|
c6cf450ac4 | ||
|
|
fa123023c7 | ||
|
|
0b285fa045 | ||
|
|
fe6c4243ee | ||
|
|
1a2c18d25e | ||
|
|
12e6de4697 | ||
|
|
f2b72d4234 | ||
|
|
25a84e9aee | ||
|
|
488171b02b | ||
|
|
eb22a449f4 | ||
|
|
c21c043f5f | ||
|
|
94808ea73e | ||
|
|
7fe486cba6 | ||
|
|
a3517e260c | ||
|
|
377f1fad41 | ||
|
|
17448f99c9 | ||
|
|
8c83886c2e | ||
|
|
9cef38af60 | ||
|
|
288ecc0d72 | ||
|
|
bb514f6d08 | ||
|
|
ecb21abb9a | ||
|
|
9445093d68 | ||
|
|
8f3f83051b | ||
|
|
5750892edf | ||
|
|
db74b2b70b | ||
|
|
a5c036be05 | ||
|
|
b60ee73ce5 | ||
|
|
b743f1cbc5 | ||
|
|
0f0bb56f7d | ||
|
|
8b593f8356 | ||
|
|
79ac73fb15 | ||
|
|
da0bb4c0ce | ||
|
|
9d7617add9 | ||
|
|
f38ed39c87 | ||
|
|
8ff5f45449 | ||
|
|
0447299e4f | ||
|
|
b797317f1a | ||
|
|
388f334fd3 | ||
|
|
e47f623b0a | ||
|
|
93e71bded8 | ||
|
|
8a93283398 | ||
|
|
aafd9b8f49 | ||
|
|
32fec183d5 | ||
|
|
d1751262df | ||
|
|
d05523360d | ||
|
|
a32f9a3c1c | ||
|
|
0468247c5a | ||
|
|
d02dd67016 | ||
|
|
27d5766abb | ||
|
|
be88c63414 | ||
|
|
c0d965857a | ||
|
|
e1763c45af | ||
|
|
177f7f64ac | ||
|
|
a2096aa753 | ||
|
|
bc9fac1da6 | ||
|
|
e0dcf24c90 | ||
|
|
db5eb095aa | ||
|
|
16b87f20ca | ||
|
|
00b82fe9f9 | ||
|
|
6af0ad100e | ||
|
|
f14fda5d1b | ||
|
|
1ef027e7e2 | ||
|
|
61aaba268f | ||
|
|
b041eee060 | ||
|
|
8ed65b95f2 | ||
|
|
3fe53f601c | ||
|
|
5ce0374426 | ||
|
|
e4aba70090 | ||
|
|
b17824c20a | ||
|
|
976dd18fa2 | ||
|
|
56bfcec656 | ||
|
|
f935706d22 | ||
|
|
e2322ee7ef | ||
|
|
d553a7cac4 | ||
|
|
f75b2ad9ee | ||
|
|
333b25eeaf | ||
|
|
0db70261f9 | ||
|
|
504db590db | ||
|
|
650b667148 | ||
|
|
4afdd0c264 | ||
|
|
2816bdef85 | ||
|
|
855fe70281 | ||
|
|
e4071a4f4e | ||
|
|
18ee17f7c8 | ||
|
|
f84f694351 | ||
|
|
0a15a0352d | ||
|
|
0e46d436de | ||
|
|
a16b4ffe75 | ||
|
|
608d4c5bda | ||
|
|
dbb8d8c93a | ||
|
|
e30f9a6cb1 | ||
|
|
bd4f5a2aa4 | ||
|
|
2d352b110b | ||
|
|
2802c61abd | ||
|
|
62c27f9142 | ||
|
|
c358f4e71f | ||
|
|
a05ecbc810 | ||
|
|
6b299693ca | ||
|
|
bb3413c1e5 | ||
|
|
854267d521 | ||
|
|
c4e05a5d9b | ||
|
|
27b61a79c8 | ||
|
|
70183bcc85 | ||
|
|
94da663287 | ||
|
|
3c7c1cc4c7 | ||
|
|
3b773da60d | ||
|
|
afee15cf44 | ||
|
|
a424aa65df | ||
|
|
fa93a7dfdd | ||
|
|
446f60b331 | ||
|
|
e136368ce3 | ||
|
|
5f1b54860c | ||
|
|
23eb008d5f | ||
|
|
40882c20d6 |
101
.github/workflows/maven-publish.yml
vendored
101
.github/workflows/maven-publish.yml
vendored
@@ -2,10 +2,21 @@ name: Publish Desktop Forge
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
type: boolean
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
release_android:
|
||||
type: boolean
|
||||
description: 'Also try to release android build'
|
||||
required: false
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
if: github.repository_owner == 'Card-Forge'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -32,10 +43,94 @@ jobs:
|
||||
run: |
|
||||
git config user.email "actions@github.com"
|
||||
git config user.name "GitHub Actions"
|
||||
- name: Build/Install/Publish to GitHub Packages Apache Maven
|
||||
|
||||
- name: Install old maven (3.8.1)
|
||||
run: |
|
||||
curl -o apache-maven-3.8.1-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz
|
||||
tar xf apache-maven-3.8.1-bin.tar.gz
|
||||
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
|
||||
export MAVEN_HOME=$PWD/apache-maven-3.8.1
|
||||
mvn --version
|
||||
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
|
||||
|
||||
- name: Setup android requirements
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
|
||||
run: |
|
||||
JAVA_HOME=${JAVA_HOME_17_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;35.0.0" "platform-tools" "platforms;android-35"
|
||||
cd forge-gui-android
|
||||
echo "${{ secrets.FORGE_KEYSTORE }}" > forge.keystore.asc
|
||||
gpg -d --passphrase "${{ secrets.FORGE_KEYSTORE_PASSPHRASE }}" --batch forge.keystore.asc > forge.keystore
|
||||
cd -
|
||||
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
||||
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
||||
curl -L -o android-maven-plugin-4.6.2.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.jar
|
||||
curl -L -o android-maven-plugin-4.6.2.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.pom
|
||||
cd -
|
||||
mvn install -Dmaven.test.skip=true
|
||||
mvn dependency:tree
|
||||
|
||||
|
||||
- name: Build/Install/Publish Desktop to GitHub Packages Apache Maven
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_android }}
|
||||
run: |
|
||||
export DISPLAY=":1"
|
||||
Xvfb :1 -screen 0 800x600x8 &
|
||||
mvn -U -B clean -P windows-linux install release:clean release:prepare release:perform -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
|
||||
export _JAVA_OPTIONS="-Xmx2g"
|
||||
d=$(date +%m.%d)
|
||||
# build only desktop and only try to move desktop files
|
||||
mvn -U -B clean -P windows-linux install -e -T 1C release:clean release:prepare release:perform -DskipTests
|
||||
mkdir izpack
|
||||
# move bz2 and jar from work dir to izpack dir
|
||||
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
|
||||
# move desktop build.txt and version.txt to izpack
|
||||
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
|
||||
cd izpack
|
||||
ls
|
||||
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Build/Install/Publish Desktop+Android to GitHub Packages Apache Maven
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
|
||||
run: |
|
||||
export DISPLAY=":1"
|
||||
Xvfb :1 -screen 0 800x600x8 &
|
||||
export _JAVA_OPTIONS="-Xmx2g"
|
||||
d=$(date +%m.%d)
|
||||
# build both desktop and android
|
||||
mvn -U -B clean -P windows-linux,android-release-build install -e -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=35.0.0
|
||||
mkdir izpack
|
||||
# move bz2 and jar from work dir to izpack dir
|
||||
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
|
||||
# move desktop build.txt and version.txt to izpack
|
||||
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
|
||||
# move android apk and assets.zip
|
||||
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk izpack/
|
||||
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip izpack/
|
||||
cd izpack
|
||||
ls
|
||||
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Upload snapshot to GitHub Prerelease
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
name: Release ${{ env.GIT_TAG }}
|
||||
tag: ${{ env.GIT_TAG }}
|
||||
artifacts: izpack/*
|
||||
allowUpdates: true
|
||||
removeArtifacts: true
|
||||
makeLatest: true
|
||||
|
||||
- name: Send failure notification to Discord
|
||||
if: failure() # This step runs only if the job fails
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d "{\"content\": \"🔴 Release Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
|
||||
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
|
||||
|
||||
@@ -122,3 +122,11 @@ jobs:
|
||||
prerelease: true
|
||||
artifacts: izpack/*
|
||||
allowUpdates: true
|
||||
removeArtifacts: true
|
||||
|
||||
- name: Send failure notification to Discord
|
||||
if: failure() # This step runs only if the job fails
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d "{\"content\": \"🔴 Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
|
||||
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
|
||||
|
||||
4
.github/workflows/snapshots-android.yml
vendored
4
.github/workflows/snapshots-android.yml
vendored
@@ -13,10 +13,6 @@ on:
|
||||
# description: 'Upload the completed Android package'
|
||||
# required: false
|
||||
# default: true
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '00 19 * * *'
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
3
.github/workflows/snapshots-pc.yml
vendored
3
.github/workflows/snapshots-pc.yml
vendored
@@ -8,9 +8,6 @@ on:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '30 18 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -26,13 +26,13 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
|
||||
### 📥 Desktop Installation
|
||||
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
|
||||
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/).
|
||||
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots).
|
||||
- **Tip:** Extract to a new folder to prevent version conflicts.
|
||||
3. **User Data Management:** Previous players’ data is preserved during upgrades.
|
||||
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
|
||||
|
||||
### 📱 Android Installation
|
||||
- Download the **APK** from the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/). On the first launch, Forge will automatically download all necessary assets.
|
||||
- Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.03</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.03</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-ai</artifactId>
|
||||
|
||||
@@ -68,8 +68,10 @@ import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -292,7 +294,7 @@ public class AiController {
|
||||
}
|
||||
|
||||
// can't fetch partner isn't problematic
|
||||
if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
|
||||
if (tr.isKeyword(Keyword.PARTNER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -689,7 +691,6 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO handle fetchlands and what they can fetch for
|
||||
// determine new color pips
|
||||
int[] card_counts = new int[6]; // in WUBRGC order
|
||||
@@ -1708,7 +1709,8 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
CompletableFuture<SpellAbility> future = CompletableFuture.supplyAsync(() -> {
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||
@@ -1788,11 +1790,9 @@ public class AiController {
|
||||
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
try {
|
||||
if (game.AI_CAN_USE_TIMEOUT)
|
||||
return future.completeOnTimeout(null, game.getAITimeout(), TimeUnit.SECONDS).get();
|
||||
else
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
future.cancel(true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return PaymentDecision.number(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostBehold cost) {
|
||||
final String type = cost.getType();
|
||||
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
|
||||
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
|
||||
return hand.isEmpty() ? null : PaymentDecision.card(getBestCreatureAI(hand));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostChooseColor cost) {
|
||||
int c = cost.getAbilityAmount(ability);
|
||||
|
||||
@@ -2891,7 +2891,7 @@ public class ComputerUtil {
|
||||
// Iceberg does use Ice as Storage
|
||||
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
|
||||
// some lands does use Depletion as Storage Counter
|
||||
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step."))
|
||||
|| (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
|
||||
// treat Time Counters on suspended Cards as Bad,
|
||||
// and also on Chronozoa
|
||||
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|
||||
|
||||
@@ -1862,7 +1862,7 @@ public class ComputerUtilCard {
|
||||
if (!c.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayment;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
@@ -101,7 +101,7 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
|
||||
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ public class ComputerUtilCombat {
|
||||
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
|
||||
// || attacker.hasSVar("EndOfTurnLeavePlay"));
|
||||
// The creature won't untap next turn
|
||||
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
|
||||
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +214,7 @@ public class ComputerUtilCombat {
|
||||
int damage = attacker.getNetCombatDamage();
|
||||
int poison = 0;
|
||||
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
|
||||
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||
if (attacker.isInfectDamage(attacked)) {
|
||||
int pd = predictDamageTo(attacked, damage, attacker, true);
|
||||
// opponent can always order it so that he gets 0
|
||||
if (pd == 1 && attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
|
||||
@@ -357,7 +357,7 @@ public class ComputerUtilCombat {
|
||||
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
||||
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
|
||||
if (trampleDamage > 0) {
|
||||
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||
if (attacker.isInfectDamage(ai)) {
|
||||
poison += trampleDamage;
|
||||
}
|
||||
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
|
||||
|
||||
@@ -642,24 +642,28 @@ public class ComputerUtilMana {
|
||||
List<SpellAbility> paymentList = Lists.newArrayList();
|
||||
final ManaPool manapool = ai.getManaPool();
|
||||
|
||||
// Apply the color/type conversion matrix if necessary
|
||||
manapool.restoreColorReplacements();
|
||||
CardPlayOption mayPlay = sa.getMayPlayOption();
|
||||
if (!effect) {
|
||||
if (sa.isSpell() && mayPlay != null) {
|
||||
mayPlay.applyManaConvert(manapool);
|
||||
} else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
|
||||
// Apply color/type conversion matrix if necessary (already done via autopay)
|
||||
if (ai.getControllingPlayer() == null) {
|
||||
manapool.restoreColorReplacements();
|
||||
CardPlayOption mayPlay = sa.getMayPlayOption();
|
||||
if (!effect) {
|
||||
if (sa.isSpell() && mayPlay != null) {
|
||||
mayPlay.applyManaConvert(manapool);
|
||||
} else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
|
||||
}
|
||||
}
|
||||
if (sa.hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
|
||||
}
|
||||
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
|
||||
}
|
||||
if (sa.hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
|
||||
}
|
||||
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
|
||||
|
||||
// not worth checking if it makes sense to not spend floating first
|
||||
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
|
||||
CostPayment.handleOfferings(sa, test, cost.isPaid());
|
||||
return true; // paid all from floating mana
|
||||
// paid all from floating mana
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||
|
||||
@@ -160,12 +160,6 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(20, "protection");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
|
||||
// paired creatures are more valuable because they grant a bonus to the other creature
|
||||
if (c.isPaired()) {
|
||||
value += addValue(14, "paired");
|
||||
@@ -213,11 +207,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(1, "untapped");
|
||||
}
|
||||
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, "manadork");
|
||||
}
|
||||
|
||||
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
|
||||
if (!c.canUntap(c.getController(), true)) {
|
||||
if (c.isTapped()) {
|
||||
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
|
||||
} else {
|
||||
@@ -226,6 +216,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
} else {
|
||||
value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, "manadork");
|
||||
}
|
||||
|
||||
// use scaling because the creature is only available halfway
|
||||
if (c.hasKeyword(Keyword.PHASING)) {
|
||||
value -= subValue(Math.max(20, value / 2), "phasing");
|
||||
|
||||
@@ -1389,11 +1389,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
|
||||
}
|
||||
|
||||
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
if (source != null && source.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
// If any Conspiracies are present, try not to choose the same name twice
|
||||
// (otherwise the AI will spam the same name)
|
||||
for (Card consp : player.getCardsIn(ZoneType.Command)) {
|
||||
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
if (consp.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
String chosenName = consp.getNamedCard();
|
||||
if (!chosenName.isEmpty()) {
|
||||
aiLibrary = CardLists.filter(aiLibrary, CardPredicates.nameNotEquals(chosenName));
|
||||
|
||||
@@ -258,7 +258,7 @@ public abstract class SpellAbilityAi {
|
||||
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
|
||||
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|
||||
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.EachDamage, DamageEachAi.class)
|
||||
.put(ApiType.Effect, EffectAi.class)
|
||||
.put(ApiType.Encode, EncodeAi.class)
|
||||
.put(ApiType.Endure, EndureAi.class)
|
||||
.put(ApiType.EndCombatPhase, EndTurnAi.class)
|
||||
.put(ApiType.EndTurn, EndTurnAi.class)
|
||||
.put(ApiType.ExchangeLife, LifeExchangeAi.class)
|
||||
|
||||
@@ -1551,8 +1551,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
|
||||
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
|
||||
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
|
||||
return !card.isUntapped();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -914,6 +914,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (sa.isSpell()) {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
|
||||
// list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1448,6 +1451,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// AI Targeting
|
||||
Card choice = null;
|
||||
|
||||
// Filter out cards TargetsForEachPlayer
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
if (mostExpensivePermanent.isCreature()
|
||||
|
||||
@@ -161,10 +161,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.DRAW)) {
|
||||
return skipDraw;
|
||||
}
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.COMBAT_BEGIN)) {
|
||||
return skipCombat;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -18,6 +19,13 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
return false; // can't draw anything
|
||||
}
|
||||
|
||||
Card host = sa.getHostCard();
|
||||
|
||||
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
|
||||
if (num == 0) {
|
||||
return false; // Won't do anything
|
||||
}
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
|
||||
@@ -205,6 +205,9 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
while (t == null) {
|
||||
// filter by MustTarget requirement
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
if (planeswalkers > 0) {
|
||||
|
||||
@@ -152,6 +152,8 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
|
||||
// target loop
|
||||
while (sa.canAddMoreTarget()) {
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -216,6 +216,8 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -515,12 +515,17 @@ public class DrawAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((computerHandSize + numCards > computerMaxHandSize)
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& !sa.isTrigger()
|
||||
&& !assumeSafeX) {
|
||||
if ((computerHandSize + numCards > computerMaxHandSize)) {
|
||||
// Don't draw too many cards and then risk discarding cards at EOT
|
||||
if (!drawback) {
|
||||
if (game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& !sa.isTrigger()
|
||||
&& !assumeSafeX
|
||||
&& !drawback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (computerHandSize > computerMaxHandSize) {
|
||||
// Don't make my hand size get too big if already at max
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
140
forge-ai/src/main/java/forge/ai/ability/EndureAi.java
Normal file
140
forge-ai/src/main/java/forge/ai/ability/EndureAi.java
Normal file
@@ -0,0 +1,140 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class EndureAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||
if (bestCreature == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
|
||||
// TODO: adapted from Fabricate AI in TokenAi, maybe can be refactored to a single method
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
final String num = sa.getParamOrDefault("Num", "1");
|
||||
final int amount = AbilityUtils.calculateAmount(source, num, sa);
|
||||
|
||||
// if host would leave the play or if host is useless, create the token
|
||||
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// need a copy for one with extra +1/+1 counter boost,
|
||||
// without causing triggers to run
|
||||
final Card copy = CardCopyService.getLKICopy(source);
|
||||
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + amount);
|
||||
copy.setZone(source.getZone());
|
||||
|
||||
// if host would put into the battlefield attacking
|
||||
Combat combat = source.getGame().getCombat();
|
||||
if (combat != null && combat.isAttacking(source)) {
|
||||
final Player defender = combat.getDefenderPlayerByAttacker(source);
|
||||
return defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true);
|
||||
}
|
||||
|
||||
// if the host has haste and can attack
|
||||
if (CombatUtil.canAttack(copy)) {
|
||||
for (final Player opp : ai.getOpponents()) {
|
||||
if (CombatUtil.canAttack(copy, opp) &&
|
||||
opp.canLoseLife() &&
|
||||
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check for trigger to turn token ETB into +1/+1 counter for host
|
||||
// TODO check for trigger to turn token ETB into damage or life loss for opponent
|
||||
// in these cases token might be preferred even if they would not survive
|
||||
|
||||
// evaluate creature with counters
|
||||
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
|
||||
|
||||
// spawn the token so it's possible to evaluate it
|
||||
final Card token = TokenInfo.getProtoType("w_x_x_spirit", sa, ai, false);
|
||||
|
||||
token.setController(ai, 0);
|
||||
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||
token.setTokenSpawningAbility(sa);
|
||||
|
||||
// evaluate the generated token
|
||||
token.setBasePowerString(num);
|
||||
token.setBasePower(amount);
|
||||
token.setBaseToughnessString(num);
|
||||
token.setBaseToughness(amount);
|
||||
|
||||
boolean result = true;
|
||||
|
||||
// need to check what the cards would be on the battlefield
|
||||
// do not attach yet, that would cause Events
|
||||
CardCollection preList = new CardCollection(token);
|
||||
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
|
||||
|
||||
// token would not survive
|
||||
if (!token.isCreature() || token.getNetToughness() < 1) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
int evalToken = ComputerUtilCard.evaluateCreature(token);
|
||||
result = evalToken < evalCounter;
|
||||
}
|
||||
|
||||
//reset static abilities
|
||||
game.getAction().checkStaticAbilities(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
return shouldPutCounters(player, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
if (sa.isDash()) {
|
||||
//only checks that the dashed creature will attack
|
||||
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))
|
||||
return false;
|
||||
if (ComputerUtilCost.canPayCost(sa.getHostCard().getSpellPermanent(), ai, false)) {
|
||||
//do not dash if creature can be played normally
|
||||
@@ -70,7 +70,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
// after attacking
|
||||
if (card.hasSVar("EndOfTurnLeavePlay")
|
||||
&& (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))) {
|
||||
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))) {
|
||||
// AiPlayDecision.AnotherTime
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -542,6 +542,8 @@ public class PumpAi extends PumpAiBase {
|
||||
Card t = null;
|
||||
// boolean goodt = false;
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
if (mandatory || ComputerUtil.activateForCost(sa, ai)) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import forge.game.combat.CombatUtil;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -137,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
return CombatUtil.canBlockAtLeastOne(card, attackers);
|
||||
} else if (keyword.endsWith("This card doesn't untap during your next untap step.")) {
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai)
|
||||
&& Untap.canUntap(card);
|
||||
&& card.canUntap(card.getController(), true);
|
||||
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||
|| keyword.endsWith("Prevent all damage that would be dealt by CARDNAME.")) {
|
||||
if (ph.isPlayerTurn(ai) && (!(CombatUtil.canBlock(card) || combat != null && combat.isBlocking(card))
|
||||
|
||||
@@ -6,6 +6,7 @@ import forge.ai.SpellAbilityAi;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -142,7 +143,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
// hidden agenda
|
||||
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
|
||||
if (card.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)
|
||||
&& card.isInZone(ZoneType.Command)) {
|
||||
String chosenName = card.getNamedCard();
|
||||
for (Card cast : ai.getGame().getStack().getSpellsCastThisTurn()) {
|
||||
|
||||
@@ -206,7 +206,8 @@ public class TokenAi extends SpellAbilityAi {
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
|
||||
&& game.getCombat() != null
|
||||
&& !game.getCombat().getAttackers().isEmpty()
|
||||
&& alwaysOnOppAttack) {
|
||||
&& alwaysOnOppAttack
|
||||
&& actualToken.isCreature()) {
|
||||
for (Card attacker : game.getCombat().getAttackers()) {
|
||||
if (CombatUtil.canBlock(attacker, actualToken)) {
|
||||
return true;
|
||||
|
||||
@@ -16,7 +16,6 @@ import forge.game.cost.CostTap;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -338,7 +337,7 @@ public class UntapAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself
|
||||
CardCollection noAutoUntap = CardLists.filter(untapList, Untap.CANUNTAP.negate());
|
||||
CardCollection noAutoUntap = CardLists.filter(untapList, c -> !c.canUntap(c.getController(), true));
|
||||
if (!noAutoUntap.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(noAutoUntap);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.03</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-core</artifactId>
|
||||
|
||||
@@ -11,7 +11,8 @@ public enum CardSplitType
|
||||
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
|
||||
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
|
||||
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
|
||||
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ public enum CardStateName {
|
||||
Original,
|
||||
FaceDown,
|
||||
Flipped,
|
||||
Converted,
|
||||
Transformed,
|
||||
Meld,
|
||||
LeftSplit,
|
||||
RightSplit,
|
||||
Adventure,
|
||||
Secondary,
|
||||
Modal,
|
||||
EmptyRoom,
|
||||
SpecializeW,
|
||||
|
||||
@@ -671,11 +671,9 @@ public class DeckRecognizer {
|
||||
// ok so the card has been found - let's see if there's any restriction on the set
|
||||
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
|
||||
currentDeckSection, true);
|
||||
// On the off chance we accidentally interpreted part of the card's name as a set code, e.g. "Tyrranax Rex"
|
||||
if (data.isMTGCard(cardName + " " + setCode))
|
||||
continue;
|
||||
// UNKNOWN card as in the Counterspell|FEM case
|
||||
return Token.UnknownCard(cardName, setCode, cardCount);
|
||||
unknownCardToken = Token.UnknownCard(cardName, setCode, cardCount);
|
||||
continue;
|
||||
}
|
||||
// ok so we can simply ignore everything but card name - as set code does not exist
|
||||
// At this stage, we know the card name exists in the DB so a Card MUST be found
|
||||
|
||||
@@ -356,8 +356,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
|
||||
@Override
|
||||
public String getImageKey(boolean altState) {
|
||||
String noramlizedName = StringUtils.stripAccents(name);
|
||||
String imageKey = ImageKeys.CARD_PREFIX + noramlizedName + CardDb.NameSetSeparator
|
||||
String normalizedName = StringUtils.stripAccents(name);
|
||||
String imageKey = ImageKeys.CARD_PREFIX + normalizedName + CardDb.NameSetSeparator
|
||||
+ edition + CardDb.NameSetSeparator + artIndex;
|
||||
if (altState) {
|
||||
imageKey += ImageKeys.BACKFACE_POSTFIX;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.03</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-game</artifactId>
|
||||
|
||||
@@ -122,23 +122,10 @@ public class ForgeScript {
|
||||
}
|
||||
}
|
||||
return found;
|
||||
} else if (property.equals("hasActivatedAbilityWithTapCost")) {
|
||||
} else if (property.startsWith("hasAbility")) {
|
||||
String valid = property.substring(11);
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (property.equals("hasActivatedAbility")) {
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (property.equals("hasOtherActivatedAbility")) {
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility() && !sa.equals(spellAbility)) {
|
||||
if (sa.isValid(valid, sourceController, source, spellAbility)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -218,6 +205,8 @@ public class ForgeScript {
|
||||
return sa.isEternalize();
|
||||
} else if (property.equals("Flashback")) {
|
||||
return sa.isFlashback();
|
||||
} else if (property.equals("Harmonize")) {
|
||||
return sa.isHarmonize();
|
||||
} else if (property.equals("Jumpstart")) {
|
||||
return sa.isJumpstart();
|
||||
} else if (property.equals("Kicked")) {
|
||||
@@ -236,6 +225,8 @@ public class ForgeScript {
|
||||
return sa.isTurnFaceUp();
|
||||
} else if (property.equals("isCastFaceDown")) {
|
||||
return sa.isCastFaceDown();
|
||||
} else if (property.equals("Unearth")) {
|
||||
return sa.isKeyword(Keyword.UNEARTH);
|
||||
} else if (property.equals("Modular")) {
|
||||
return sa.isKeyword(Keyword.MODULAR);
|
||||
} else if (property.equals("Equip")) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.common.collect.HashBasedTable;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Table;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import forge.GameCommand;
|
||||
@@ -261,7 +262,6 @@ public class Game {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public void addPlayer(int id, Player player) {
|
||||
playerCache.put(id, player);
|
||||
}
|
||||
@@ -523,7 +523,7 @@ public class Game {
|
||||
* The Direction in which the turn order of this Game currently proceeds.
|
||||
*/
|
||||
public final Direction getTurnOrder() {
|
||||
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().getAmountOfKeyword("The turn order is reversed.") % 2 == 1) {
|
||||
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().isTurnOrderReversed()) {
|
||||
return turnOrder.getOtherDirection();
|
||||
}
|
||||
return turnOrder;
|
||||
@@ -1223,13 +1223,20 @@ public class Game {
|
||||
}
|
||||
public int getCounterAddedThisTurn(CounterType cType, Card card) {
|
||||
int result = 0;
|
||||
if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
Set<CounterType> types = null;
|
||||
if (cType == null) {
|
||||
types = countersAddedThisTurn.rowKeySet();
|
||||
} else if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
return result;
|
||||
} else {
|
||||
types = Sets.newHashSet(cType);
|
||||
}
|
||||
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(cType).values()) {
|
||||
for (Pair<Card, Integer> p : l) {
|
||||
if (p.getKey().equalsWithGameTimestamp(card)) {
|
||||
result += p.getValue();
|
||||
for (CounterType type : types) {
|
||||
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(type).values()) {
|
||||
for (Pair<Card, Integer> p : l) {
|
||||
if (p.getKey().equalsWithGameTimestamp(card)) {
|
||||
result += p.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1272,12 +1272,11 @@ public class GameAction {
|
||||
dependencyGraph.addVertex(stAb);
|
||||
|
||||
boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb);
|
||||
boolean compareAffected = true;
|
||||
boolean compareAffected = false;
|
||||
CardCollectionView affectedHere = affectedPerAbility.get(stAb);
|
||||
if (affectedHere == null) {
|
||||
affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList);
|
||||
} else {
|
||||
compareAffected = false;
|
||||
compareAffected = true;
|
||||
}
|
||||
List<Object> effectResults = generateStaticAbilityResult(layer, stAb);
|
||||
|
||||
@@ -1355,7 +1354,7 @@ public class GameAction {
|
||||
}
|
||||
dependencyGraph.removeAllVertices(toRemove);
|
||||
|
||||
// now the earlist one left is the correct choice
|
||||
// now the earliest one left is the correct choice
|
||||
List<StaticAbility> statics = Lists.newArrayList(dependencyGraph.vertexSet());
|
||||
statics.sort(Comparator.comparing(s -> s.getHostCard().getLayerTimestamp()));
|
||||
|
||||
@@ -1471,14 +1470,9 @@ public class GameAction {
|
||||
checkAgainCard |= stateBasedAction704_attach(c, unAttachList); // Attachment
|
||||
checkAgainCard |= stateBasedAction_Contraption(c, noRegCreats);
|
||||
|
||||
checkAgainCard |= stateBasedAction704_5r(c); // annihilate +1/+1 counters with -1/-1 ones
|
||||
checkAgainCard |= stateBasedAction704_5q(c); // annihilate +1/+1 counters with -1/-1 ones
|
||||
|
||||
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
|
||||
|
||||
if (c.getCounters(dreamType) > 7 && c.hasKeyword("CARDNAME can't have more than seven dream counters on it.")) {
|
||||
c.subtractCounter(dreamType, c.getCounters(dreamType) - 7, null);
|
||||
checkAgainCard = true;
|
||||
}
|
||||
checkAgainCard |= stateBasedAction704_5r(c);
|
||||
|
||||
if (c.hasKeyword("The number of loyalty counters on CARDNAME is equal to the number of Beebles you control.")) {
|
||||
int beeble = CardLists.getValidCardCount(game.getCardsIn(ZoneType.Battlefield), "Beeble.YouCtrl", c.getController(), c, null);
|
||||
@@ -1535,7 +1529,7 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that player’s speed becomes 1. See rule 702.179, “Start Your Engines!”
|
||||
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that player’s speed becomes 1.
|
||||
if (p.getSpeed() == 0 && p.getCardsIn(ZoneType.Battlefield).anyMatch(c -> c.hasKeyword(Keyword.START_YOUR_ENGINES))) {
|
||||
p.increaseSpeed();
|
||||
checkAgain = true;
|
||||
@@ -1550,6 +1544,7 @@ public class GameAction {
|
||||
}
|
||||
// 704.5m World rule
|
||||
checkAgain |= handleWorldRule(noRegCreats);
|
||||
|
||||
// only check static abilities once after destroying all the creatures
|
||||
// (e.g. helpful for Erebos's Titan and another creature dealing lethal damage to each other simultaneously)
|
||||
setHoldCheckingStaticAbilities(true);
|
||||
@@ -1658,11 +1653,23 @@ public class GameAction {
|
||||
|
||||
private boolean stateBasedAction_Battle(Card c, CardCollection removeList) {
|
||||
boolean checkAgain = false;
|
||||
if (!c.getType().isBattle()) {
|
||||
return false;
|
||||
if (!c.isBattle()) {
|
||||
return checkAgain;
|
||||
}
|
||||
if (((c.getProtectingPlayer() == null || !c.getProtectingPlayer().isInGame()) &&
|
||||
(game.getCombat() == null || game.getCombat().getAttackersOf(c).isEmpty())) ||
|
||||
(c.getType().hasStringType("Siege") && c.getController().equals(c.getProtectingPlayer()))) {
|
||||
Player newProtector = c.getController().getController().chooseSingleEntityForEffect(c.getController().getOpponents(), new SpellAbility.EmptySa(ApiType.ChoosePlayer, c), "Choose an opponent to protect this battle", null);
|
||||
// seems unlikely unless range of influence gets implemented
|
||||
if (newProtector == null) {
|
||||
removeList.add(c);
|
||||
} else {
|
||||
c.setProtectingPlayer(newProtector);
|
||||
}
|
||||
checkAgain = true;
|
||||
}
|
||||
if (c.getCounters(CounterEnumType.DEFENSE) > 0) {
|
||||
return false;
|
||||
return checkAgain;
|
||||
}
|
||||
// 704.5v If a battle has defense 0 and it isn't the source of an ability that has triggered but not yet left the stack,
|
||||
// it’s put into its owner’s graveyard.
|
||||
@@ -1804,13 +1811,17 @@ public class GameAction {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean stateBasedAction704_5r(Card c) {
|
||||
private boolean stateBasedAction704_5q(Card c) {
|
||||
boolean checkAgain = false;
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
int plusOneCounters = c.getCounters(p1p1);
|
||||
int minusOneCounters = c.getCounters(m1m1);
|
||||
if (plusOneCounters > 0 && minusOneCounters > 0) {
|
||||
if (!c.canRemoveCounters(p1p1) || !c.canRemoveCounters(m1m1)) {
|
||||
return checkAgain;
|
||||
}
|
||||
|
||||
int remove = Math.min(plusOneCounters, minusOneCounters);
|
||||
// If a permanent has both a +1/+1 counter and a -1/-1 counter on it,
|
||||
// N +1/+1 and N -1/-1 counters are removed from it, where N is the
|
||||
@@ -1822,6 +1833,26 @@ public class GameAction {
|
||||
}
|
||||
return checkAgain;
|
||||
}
|
||||
private boolean stateBasedAction704_5r(Card c) {
|
||||
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
|
||||
|
||||
int old = c.getCounters(dreamType);
|
||||
if (old <= 0) {
|
||||
return false;
|
||||
}
|
||||
Integer max = c.getCounterMax(dreamType);
|
||||
if (max == null) {
|
||||
return false;
|
||||
}
|
||||
if (old > max) {
|
||||
if (!c.canRemoveCounters(dreamType)) {
|
||||
return false;
|
||||
}
|
||||
c.subtractCounter(dreamType, old - max, null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a token is in a zone other than the battlefield, it ceases to exist.
|
||||
private boolean stateBasedAction704_5d(Card c) {
|
||||
|
||||
@@ -183,6 +183,34 @@ public final class GameActionUtil {
|
||||
flashback.setKeyword(inst);
|
||||
flashback.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(flashback);
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.equals("Harmonize") && source.getManaCost().isNoCost()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SpellAbility harmonize = null;
|
||||
|
||||
if (keyword.contains(":")) {
|
||||
final String[] k = keyword.split(":");
|
||||
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
harmonize.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
harmonize = sa.copy(activator);
|
||||
}
|
||||
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
|
||||
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
harmonize.setKeyword(inst);
|
||||
harmonize.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(harmonize);
|
||||
} else if (keyword.startsWith("Foretell")) {
|
||||
// Foretell cast only from Exile
|
||||
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
|
||||
@@ -582,9 +610,8 @@ public final class GameActionUtil {
|
||||
" or greater>";
|
||||
final Cost cost = new Cost(casualtyCost, false);
|
||||
String str = "Pay for Casualty? " + cost.toSimpleString();
|
||||
boolean v = pc.addKeywordCost(sa, cost, ki, str);
|
||||
|
||||
if (v) {
|
||||
if (pc.addKeywordCost(sa, cost, ki, str)) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
@@ -630,9 +657,7 @@ public final class GameActionUtil {
|
||||
final Cost cost = new Cost(k[1], false);
|
||||
String str = "Pay for Offspring? " + cost.toSimpleString();
|
||||
|
||||
boolean v = pc.addKeywordCost(sa, cost, ki, str);
|
||||
|
||||
if (v) {
|
||||
if (pc.addKeywordCost(sa, cost, ki, str)) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
@@ -679,6 +704,25 @@ public final class GameActionUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isHarmonize()) {
|
||||
CardCollectionView creatures = activator.getCreaturesInPlay();
|
||||
if (!creatures.isEmpty()) {
|
||||
int max = Aggregates.max(creatures, Card::getNetPower);
|
||||
int n = pc.chooseNumber(sa, "Choose power of creature to tap", 0, max);
|
||||
final String harmonizeCost = "tapXType<1/Creature.powerEQ" + n + "/creature for Harmonize>";
|
||||
final Cost cost = new Cost(harmonizeCost, false);
|
||||
|
||||
if (pc.addKeywordCost(sa, cost, sa.getKeyword(), "Tap creature?")) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
result.getPayCosts().add(cost);
|
||||
reset = true;
|
||||
result.setOptionalKeywordAmount(sa.getKeyword(), n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (host.isCreature()) {
|
||||
String kw = "As an additional cost to cast creature spells," +
|
||||
" you may pay any amount of mana. If you do, that creature enters " +
|
||||
|
||||
@@ -318,11 +318,20 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
return canReceiveCounters(CounterType.get(type));
|
||||
}
|
||||
|
||||
public final void addCounter(final CounterType counterType, final int n, final Player source, GameEntityCounterTable table) {
|
||||
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
|
||||
if (n <= 0 || !canReceiveCounters(counterType)) {
|
||||
// As per rule 107.1b
|
||||
return;
|
||||
}
|
||||
|
||||
Integer max = getCounterMax(counterType);
|
||||
if (max != null) {
|
||||
n = Math.min(n, max - getCounters(counterType));
|
||||
if (n <= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// doesn't really add counters, but is just a helper to add them to the Table
|
||||
// so the Table can handle the Replacement Effect
|
||||
table.put(source, this, counterType, n);
|
||||
@@ -340,6 +349,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
|
||||
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
|
||||
}
|
||||
public Integer getCounterMax(final CounterType counterType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Pair<Integer, Boolean>> getDamageReceivedThisTurn() {
|
||||
return damageReceivedThisTurn;
|
||||
|
||||
@@ -159,12 +159,17 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
|
||||
}
|
||||
|
||||
// Add ETB flag
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Cause, cause);
|
||||
if (params != null) {
|
||||
runParams.putAll(params);
|
||||
}
|
||||
|
||||
boolean firstTime = false;
|
||||
if (gm.getKey() instanceof Card c) {
|
||||
firstTime = game.getCounterAddedThisTurn(null, c) == 0;
|
||||
}
|
||||
|
||||
// Apply counter after replacement effect
|
||||
for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : values.entrySet()) {
|
||||
boolean remember = cause != null && cause.hasParam("RememberPut");
|
||||
@@ -182,6 +187,13 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.containsColumn(gm.getKey())) {
|
||||
runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Object, gm.getKey());
|
||||
runParams.put(AbilityKey.FirstTime, firstTime);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.CounterTypeAddedAll, runParams, false);
|
||||
}
|
||||
}
|
||||
|
||||
int totalAdded = totalValues();
|
||||
|
||||
@@ -22,7 +22,7 @@ public enum GameType {
|
||||
Winston (DeckFormat.Limited, true, true, true, "lblWinston", ""),
|
||||
Gauntlet (DeckFormat.Constructed, false, true, true, "lblGauntlet", ""),
|
||||
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
|
||||
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommander", "lblCommanderDesc"),
|
||||
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"),
|
||||
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
|
||||
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
|
||||
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),
|
||||
|
||||
@@ -180,26 +180,19 @@ public final class AbilityFactory {
|
||||
}
|
||||
|
||||
public static Cost parseAbilityCost(final CardState state, Map<String, String> mapParams, AbilityRecordType type) {
|
||||
Cost abCost = null;
|
||||
if (type != AbilityRecordType.SubAbility) {
|
||||
String cost = mapParams.get("Cost");
|
||||
if (cost == null) {
|
||||
if (type == AbilityRecordType.Spell) {
|
||||
SpellAbility firstAbility = state.getFirstAbility();
|
||||
if (firstAbility != null && firstAbility.isSpell()) {
|
||||
// TODO might remove when Enchant Keyword is refactored
|
||||
System.err.println(state.getName() + " already has Spell using mana cost");
|
||||
}
|
||||
// for a Spell if no Cost is used, use the card states ManaCost
|
||||
abCost = new Cost(state.getManaCost(), false);
|
||||
} else {
|
||||
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
|
||||
}
|
||||
} else {
|
||||
abCost = new Cost(cost, type == AbilityRecordType.Ability);
|
||||
}
|
||||
if (type == AbilityRecordType.SubAbility) {
|
||||
return null;
|
||||
}
|
||||
String cost = mapParams.get("Cost");
|
||||
if (cost != null) {
|
||||
return new Cost(cost, type == AbilityRecordType.Ability);
|
||||
}
|
||||
if (type == AbilityRecordType.Spell) {
|
||||
// for a Spell if no Cost is used, use the card states ManaCost
|
||||
return new Cost(state.getManaCost(), false);
|
||||
} else {
|
||||
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
|
||||
}
|
||||
return abCost;
|
||||
}
|
||||
|
||||
public static SpellAbility getAbility(AbilityRecordType type, ApiType api, Map<String, String> mapParams,
|
||||
|
||||
@@ -38,7 +38,6 @@ public enum AbilityKey {
|
||||
Causer("Causer"),
|
||||
Championed("Championed"),
|
||||
ClassLevel("ClassLevel"),
|
||||
Cost("Cost"),
|
||||
CostStack("CostStack"),
|
||||
CounterAmount("CounterAmount"),
|
||||
CounteredSA("CounteredSA"),
|
||||
|
||||
@@ -1856,6 +1856,10 @@ public class AbilityUtils {
|
||||
return doXMath(list.size(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("ActivatedThisGame")) {
|
||||
return doXMath(sa.getActivationsThisGame(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("ResolvedThisTurn")) {
|
||||
return doXMath(sa.getResolvedThisTurn(), expr, c, ctb);
|
||||
}
|
||||
@@ -2268,6 +2272,9 @@ public class AbilityUtils {
|
||||
if (sq[0].equals("Delirium")) {
|
||||
return doXMath(calculateAmount(c, sq[player.hasDelirium() ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
if (sq[0].equals("MaxSpeed")) {
|
||||
return doXMath(calculateAmount(c, sq[player.maxSpeed() ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
if (sq[0].equals("FatefulHour")) {
|
||||
return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ public enum ApiType {
|
||||
Cleanup (CleanUpEffect.class),
|
||||
Cloak (CloakEffect.class),
|
||||
Clone (CloneEffect.class),
|
||||
CompanionChoose (ChooseCompanionEffect.class),
|
||||
Connive (ConniveEffect.class),
|
||||
CopyPermanent (CopyPermanentEffect.class),
|
||||
CopySpellAbility (CopySpellAbilityEffect.class),
|
||||
@@ -86,6 +85,7 @@ public enum ApiType {
|
||||
Encode (EncodeEffect.class),
|
||||
EndCombatPhase (EndCombatPhaseEffect.class),
|
||||
EndTurn (EndTurnEffect.class),
|
||||
Endure (EndureEffect.class),
|
||||
ExchangeLife (LifeExchangeEffect.class),
|
||||
ExchangeLifeVariant (LifeExchangeVariantEffect.class),
|
||||
ExchangeControl (ControlExchangeEffect.class),
|
||||
@@ -207,6 +207,7 @@ public enum ApiType {
|
||||
BlankLine (BlankLineEffect.class),
|
||||
DamageResolve (DamageResolveEffect.class),
|
||||
ChangeZoneResolve (ChangeZoneResolveEffect.class),
|
||||
CompanionChoose (CharmEffect.class),
|
||||
InternalLegendaryRule (CharmEffect.class),
|
||||
InternalIgnoreEffect (CharmEffect.class),
|
||||
InternalRadiation (InternalRadiationEffect.class),
|
||||
|
||||
@@ -83,8 +83,8 @@ public abstract class SpellAbilityEffect {
|
||||
if ("SpellDescription".equalsIgnoreCase(stackDesc)) {
|
||||
if (params.containsKey("SpellDescription")) {
|
||||
String rawSDesc = params.get("SpellDescription");
|
||||
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replaceAll(",,,,,,", " ");
|
||||
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replaceAll(",,,", " ");
|
||||
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replace(",,,,,,", " ");
|
||||
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replace(",,,", " ");
|
||||
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc, sa.getHostCard());
|
||||
|
||||
//trim reminder text from StackDesc
|
||||
|
||||
@@ -25,7 +25,7 @@ public class AddPhaseEffect extends SpellAbilityEffect {
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Player activator = sa.getActivatingPlayer();
|
||||
boolean isTopsy = activator.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
|
||||
boolean isTopsy = activator.isPhasesReversed();
|
||||
PhaseHandler phaseHandler = activator.getGame().getPhaseHandler();
|
||||
|
||||
PhaseType currentPhase = phaseHandler.getPhase();
|
||||
|
||||
@@ -7,6 +7,8 @@ import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.phase.ExtraTurn;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementHandler;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerHandler;
|
||||
@@ -72,11 +74,12 @@ public class AddTurnEffect extends SpellAbilityEffect {
|
||||
|
||||
final Card eff = createEffect(sa, sa.getActivatingPlayer(), name, image);
|
||||
|
||||
String stEffect = "Mode$ CantSetSchemesInMotion | EffectZone$ Command | Description$ Schemes can't be set in Motion";
|
||||
|
||||
eff.addStaticAbility(stEffect);
|
||||
String strRe = "Event$ SetInMotion | EffectZone$ Command | Layer$ CantHappen | Description$ Schemes can't be set in Motion";
|
||||
ReplacementEffect re = ReplacementHandler.parseReplacement(strRe, eff, true);
|
||||
eff.addReplacementEffect(re);
|
||||
|
||||
game.getAction().moveToCommand(eff, sa);
|
||||
game.getEndOfTurn().addUntil(exileEffectCommand(game, eff));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1129,7 +1129,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we're choosing multiple cards, only need to show the reveal dialog the first time through.
|
||||
boolean shouldReveal = (i == 0);
|
||||
Card c = null;
|
||||
|
||||
@@ -118,16 +118,9 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
||||
}
|
||||
boolean dontRevealToOwner = true;
|
||||
if (sa.hasParam("EachBasicType")) {
|
||||
// Get all lands,
|
||||
List<Card> land = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS);
|
||||
String eachBasic = sa.getParam("EachBasicType");
|
||||
if (eachBasic.equals("Controlled")) {
|
||||
land = CardLists.filterControlledBy(land, p);
|
||||
}
|
||||
|
||||
// Choose one of each BasicLand given special place
|
||||
for (final String type : CardType.getBasicTypes()) {
|
||||
final CardCollectionView cl = CardLists.getType(land, type);
|
||||
final CardCollectionView cl = CardLists.getType(pChoices, type);
|
||||
if (!cl.isEmpty()) {
|
||||
final String prompt = Localizer.getInstance().getMessage("lblChoose") + " " + Lang.nounWithAmount(1, type);
|
||||
Card c = p.getController().chooseSingleEntityForEffect(cl, sa, prompt, false, null);
|
||||
@@ -138,7 +131,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
||||
}
|
||||
} else if (sa.hasParam("ChooseEach")) {
|
||||
final String s = sa.getParam("ChooseEach");
|
||||
final String[] types = s.equals("Party") ? new String[]{"Cleric","Thief","Warrior","Wizard"}
|
||||
final String[] types = s.equals("Party") ? new String[]{"Cleric","Rogue","Warrior","Wizard"}
|
||||
: s.split(" & ");
|
||||
for (final String type : types) {
|
||||
CardCollection valids = CardLists.filter(pChoices, CardPredicates.isType(type));
|
||||
@@ -291,11 +284,9 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
||||
allChosen.addAll(chosen);
|
||||
}
|
||||
if (sa.hasParam("Reveal") && sa.hasParam("Secretly")) {
|
||||
for (final Player p : tgtPlayers) {
|
||||
game.getAction().reveal(allChosen, p, true, revealTitle ?
|
||||
sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " ",
|
||||
!revealTitle);
|
||||
}
|
||||
game.getAction().revealTo(allChosen, game.getPlayers(), revealTitle ?
|
||||
sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " ",
|
||||
!revealTitle);
|
||||
}
|
||||
host.setChosenCards(allChosen);
|
||||
if (sa.hasParam("ForgetOtherRemembered")) {
|
||||
|
||||
@@ -90,7 +90,7 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
|
||||
} else {
|
||||
chosen = p.getController().chooseCardName(sa, faces, message);
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
// use CardFace because you might name a alternate names
|
||||
Predicate<ICardFace> cpp = x -> true;
|
||||
if (sa.hasParam("ValidCards")) {
|
||||
@@ -112,8 +112,7 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
|
||||
}
|
||||
if (randomChoice) {
|
||||
chosen = StaticData.instance().getCommonCards().streamAllFaces()
|
||||
.filter(cpp).collect(StreamUtil.random()).get()
|
||||
.getName();
|
||||
.filter(cpp).collect(StreamUtil.random()).map(ICardFace::getName).orElse("");
|
||||
} else {
|
||||
chosen = p.getController().chooseCardName(sa, cpp, valid, message);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class ChooseCompanionEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
// This isn't a real effect. Just need it for AI choosing.
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class ChooseTypeEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
|
||||
if (validTypes.isEmpty() && sa.hasParam("Note")) {
|
||||
if (validTypes.isEmpty() && sa.hasParam("TypesFromDefined")) {
|
||||
// OK to end up with no choices/have nothing new to note
|
||||
} else if (!validTypes.isEmpty()) {
|
||||
for (final Player p : tgtPlayers) {
|
||||
|
||||
@@ -132,10 +132,6 @@ public class ControlGainEffect extends SpellAbilityEffect {
|
||||
tgtCards = getDefinedCards(sa);
|
||||
}
|
||||
|
||||
if (tgtCards != null & sa.hasParam("ControlledByTarget")) {
|
||||
tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa));
|
||||
}
|
||||
|
||||
// check for lose control criteria right away
|
||||
if (lose != null && lose.contains("LeavesPlay") && !source.isInPlay()) {
|
||||
return;
|
||||
@@ -170,7 +166,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
|
||||
tgtC.addTempController(newController, tStamp);
|
||||
|
||||
if (bUntap) {
|
||||
if (tgtC.untap(true)) untapped.add(tgtC);
|
||||
if (tgtC.untap()) untapped.add(tgtC);
|
||||
}
|
||||
|
||||
if (keywords != null) {
|
||||
|
||||
@@ -26,10 +26,8 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
|
||||
@SuppressWarnings("serial")
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Player activator = sa.getActivatingPlayer();
|
||||
final Game game = activator.getGame();
|
||||
final Player controller = sa.hasParam("Controller") ? AbilityUtils.getDefinedPlayers(
|
||||
sa.getHostCard(), sa.getParam("Controller"), sa).get(0) : activator;
|
||||
final Player controller = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Controller"), sa).get(0);
|
||||
final Game game = controller.getGame();
|
||||
|
||||
for (final Player pTarget: getTargetPlayers(sa)) {
|
||||
// before next untap gain control
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.card.MagicColor;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.GameEntityCounterTable;
|
||||
@@ -618,6 +619,23 @@ public class CountersPutEffect extends SpellAbilityEffect {
|
||||
for (String k : keywords) {
|
||||
resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table, false);
|
||||
}
|
||||
} else if (sa.hasParam("ForColor")) {
|
||||
Iterable<String> oldColors = card.getChosenColors();
|
||||
CounterType counterType = null;
|
||||
try {
|
||||
counterType = chooseTypeFromList(sa, sa.getParam("CounterType"), null, placer.getController());
|
||||
} catch (Exception e) {
|
||||
System.out.println("Counter type doesn't match, nor does an SVar exist with the type name.");
|
||||
return;
|
||||
}
|
||||
for (String color : MagicColor.Constant.ONLY_COLORS) {
|
||||
card.setChosenColors(Lists.newArrayList(color));
|
||||
if (sa.getOriginalParam("ChoiceTitle") != null) {
|
||||
sa.getMapParams().put("ChoiceTitle", sa.getOriginalParam("ChoiceTitle").replace("chosenColor", color));
|
||||
}
|
||||
resolvePerType(sa, placer, counterType, counterAmount, table, true);
|
||||
}
|
||||
card.setChosenColors(Lists.newArrayList(oldColors));
|
||||
} else {
|
||||
CounterType counterType = null;
|
||||
if (!sa.hasParam("EachExistingCounter") && !sa.hasParam("EachFromSource")
|
||||
|
||||
@@ -65,8 +65,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
ctype = CounterType.getType(sa.getParam("CounterType"));
|
||||
}
|
||||
|
||||
final Player pl = !sa.hasParam("DefinedPlayer") ? sa.getActivatingPlayer() :
|
||||
AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
|
||||
final Player pl = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
|
||||
final boolean eachExisting = sa.hasParam("EachExistingCounter");
|
||||
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
@@ -79,7 +78,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) {
|
||||
continue;
|
||||
}
|
||||
if (!eachExisting && sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null,
|
||||
if (sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null,
|
||||
Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(),
|
||||
CardTranslation.getTranslatedName(gameCard.getName())), null)) {
|
||||
continue;
|
||||
@@ -114,8 +113,6 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove");
|
||||
CounterType chosenType = pc.chooseCounterType(list, sa, prompt, params);
|
||||
|
||||
params.put("CounterType", chosenType);
|
||||
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
|
||||
boolean putCounter;
|
||||
if (sa.hasParam("RemoveConditionSVar")) {
|
||||
final Card host = sa.getHostCard();
|
||||
@@ -137,6 +134,8 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
} else if (!canReceive && canRemove) {
|
||||
putCounter = false;
|
||||
} else {
|
||||
params.put("CounterType", chosenType);
|
||||
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
|
||||
putCounter = pc.chooseBinary(sa, prompt, BinaryChoiceType.AddOrRemove, params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,14 +128,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
String typeforPrompt = counterType == null ? "" : counterType.getName();
|
||||
String title = Localizer.getInstance().getMessage("lblChooseCardsToTakeTargetCounters", typeforPrompt);
|
||||
title = title.replace(" ", " ");
|
||||
if (sa.hasParam("ValidSource")) {
|
||||
srcCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("ValidSource"), activator, card, sa);
|
||||
if (num.equals("Any")) {
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("CounterType", counterType);
|
||||
srcCards = pc.chooseCardsForEffect(srcCards, sa, title, 0, srcCards.size(), true, params);
|
||||
}
|
||||
} else if (sa.hasParam("Choices") && counterType != null) {
|
||||
if (sa.hasParam("Choices") && counterType != null) {
|
||||
ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone"))
|
||||
: ZoneType.Battlefield;
|
||||
|
||||
@@ -148,7 +141,9 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
min = 0;
|
||||
max = choices.size();
|
||||
}
|
||||
srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, null);
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("CounterType", counterType);
|
||||
srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params);
|
||||
} else {
|
||||
srcCards = getTargetCards(sa);
|
||||
}
|
||||
@@ -168,39 +163,45 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
|
||||
totalRemoved += gameCard.subtractCounter(e.getKey(), e.getValue(), activator);
|
||||
}
|
||||
game.updateLastStateForCard(gameCard);
|
||||
continue;
|
||||
} else if (num.equals("All") || num.equals("Any")) {
|
||||
cntToRemove = gameCard.getCounters(counterType);
|
||||
}
|
||||
|
||||
if (type.equals("Any")) {
|
||||
} else if (type.equals("Any")) {
|
||||
totalRemoved += removeAnyType(gameCard, cntToRemove, sa);
|
||||
} else {
|
||||
if (!tgtCard.canRemoveCounters(counterType)) {
|
||||
if (!gameCard.canRemoveCounters(counterType)) {
|
||||
continue;
|
||||
}
|
||||
cntToRemove = Math.min(cntToRemove, gameCard.getCounters(counterType));
|
||||
|
||||
if (zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) {
|
||||
if (sa.hasParam("UpTo") || num.equals("Any")) {
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("Target", gameCard);
|
||||
params.put("CounterType", counterType);
|
||||
title = Localizer.getInstance().getMessage("lblSelectRemoveCountersNumberOfTarget", type);
|
||||
cntToRemove = pc.chooseNumber(sa, title, 0, cntToRemove, params);
|
||||
int removeFromCard = cntToRemove;
|
||||
if (num.equals("All") || num.equals("Any")) {
|
||||
removeFromCard = gameCard.getCounters(counterType);
|
||||
} else {
|
||||
if (sa.hasParam("CounterNumShared")) {
|
||||
removeFromCard -= totalRemoved;
|
||||
if (removeFromCard < 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
removeFromCard = Math.min(removeFromCard, gameCard.getCounters(counterType));
|
||||
}
|
||||
if (cntToRemove > 0) {
|
||||
gameCard.subtractCounter(counterType, cntToRemove, activator);
|
||||
|
||||
if ((zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) &&
|
||||
(sa.hasParam("UpTo") || num.equals("Any"))) {
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("Target", gameCard);
|
||||
params.put("CounterType", counterType);
|
||||
title = Localizer.getInstance().getMessage("lblSelectRemoveCountersNumberOfTarget", type);
|
||||
removeFromCard = pc.chooseNumber(sa, title, 0, removeFromCard, params);
|
||||
}
|
||||
if (removeFromCard > 0) {
|
||||
gameCard.subtractCounter(counterType, removeFromCard, activator);
|
||||
if (rememberRemoved) {
|
||||
for (int i = 0; i < cntToRemove; i++) {
|
||||
for (int i = 0; i < removeFromCard; i++) {
|
||||
// TODO might need to be more specific
|
||||
card.addRemembered(Pair.of(counterType, i));
|
||||
}
|
||||
}
|
||||
game.updateLastStateForCard(gameCard);
|
||||
|
||||
totalRemoved += cntToRemove;
|
||||
totalRemoved += removeFromCard;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.mutable.MutableBoolean;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.game.Game;
|
||||
import forge.game.GameActionUtil;
|
||||
import forge.game.GameEntityCounterTable;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardZoneTable;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.TokenCreateTable;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.event.GameEventCombatChanged;
|
||||
import forge.game.event.GameEventTokenCreated;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.CardTranslation;
|
||||
import forge.util.Lang;
|
||||
import forge.util.Localizer;
|
||||
|
||||
public class EndureEffect extends TokenEffectBase {
|
||||
|
||||
@Override
|
||||
protected String getStackDescription(SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
|
||||
List<Card> tgt = getTargetCards(sa);
|
||||
|
||||
sb.append(Lang.joinHomogenous(tgt));
|
||||
sb.append(" ");
|
||||
sb.append(tgt.size() > 1 ? "endure" : "endures");
|
||||
|
||||
int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
|
||||
|
||||
sb.append(" ").append(amount);
|
||||
sb.append(". ");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Game game = host.getGame();
|
||||
String num = sa.getParamOrDefault("Num", "1");
|
||||
int amount = AbilityUtils.calculateAmount(host, num, sa);
|
||||
|
||||
if (amount < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
TokenCreateTable tokenTable = new TokenCreateTable();
|
||||
for (final Card c : GameActionUtil.orderCardsByTheirOwners(game, getTargetCards(sa), ZoneType.Battlefield, sa)) {
|
||||
final Player pl = c.getController();
|
||||
|
||||
Card gamec = game.getCardState(c, null);
|
||||
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("RevealedCard", c);
|
||||
params.put("Amount", amount);
|
||||
if (gamec != null && gamec.isInPlay() && gamec.equalsWithGameTimestamp(c) && gamec.canReceiveCounters(CounterEnumType.P1P1)
|
||||
&& pl.getController().confirmAction(sa, null,
|
||||
Localizer.getInstance().getMessage("lblEndureAction", CardTranslation.getTranslatedName(c.getName()), amount),
|
||||
gamec, params)) {
|
||||
gamec.addCounter(CounterEnumType.P1P1, amount, pl, table);
|
||||
} else {
|
||||
final Card result = TokenInfo.getProtoType("w_x_x_spirit", sa, pl, false);
|
||||
|
||||
// set PT
|
||||
result.setBasePowerString(num);
|
||||
result.setBasePower(amount);
|
||||
result.setBaseToughnessString(num);
|
||||
result.setBaseToughness(amount);
|
||||
|
||||
tokenTable.put(pl, result, 1);
|
||||
}
|
||||
}
|
||||
table.replaceCounterEffect(game, sa, true);
|
||||
|
||||
if (!tokenTable.isEmpty()) {
|
||||
CardZoneTable triggerList = new CardZoneTable();
|
||||
MutableBoolean combatChanged = new MutableBoolean(false);
|
||||
makeTokenTable(tokenTable, false, triggerList, combatChanged, sa);
|
||||
|
||||
triggerList.triggerChangesZoneAll(game, sa);
|
||||
|
||||
game.fireEvent(new GameEventTokenCreated());
|
||||
|
||||
if (combatChanged.isTrue()) {
|
||||
game.updateCombatForView();
|
||||
game.fireEvent(new GameEventCombatChanged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -303,6 +303,6 @@ public class FlipCoinEffect extends SpellAbilityEffect {
|
||||
|
||||
public static int getFlipMultiplier(final Player flipper) {
|
||||
String str = "If you would flip a coin, instead flip two coins and ignore one.";
|
||||
return 1 << flipper.getKeywords().getAmount(str);
|
||||
return 1 << flipper.getAmountOfKeyword(str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +361,6 @@ public class PlayEffect extends SpellAbilityEffect {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean unpayableCost = tgtSA.getPayCosts().getCostMana().getMana().isNoCost();
|
||||
if (sa.hasParam("WithoutManaCost")) {
|
||||
tgtSA = tgtSA.copyWithNoManaCost();
|
||||
} else if (sa.hasParam("PlayCost")) {
|
||||
@@ -380,7 +379,8 @@ public class PlayEffect extends SpellAbilityEffect {
|
||||
}
|
||||
|
||||
tgtSA = tgtSA.copyWithManaCostReplaced(tgtSA.getActivatingPlayer(), abCost);
|
||||
} else if (unpayableCost) {
|
||||
} else if (tgtSA.getPayCosts().hasManaCost() && tgtSA.getPayCosts().getCostMana().getMana().isNoCost()) {
|
||||
// unpayable
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ public class RepeatEachEffect extends SpellAbilityEffect {
|
||||
else if (sa.hasParam("DefinedCards")) {
|
||||
repeatCards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa);
|
||||
}
|
||||
boolean loopOverCards = repeatCards != null && !repeatCards.isEmpty();
|
||||
|
||||
if (sa.hasParam("ClearRemembered")) {
|
||||
source.clearRemembered();
|
||||
@@ -89,7 +88,7 @@ public class RepeatEachEffect extends SpellAbilityEffect {
|
||||
sa.setLoseLifeMap(Maps.newHashMap());
|
||||
}
|
||||
|
||||
if (loopOverCards) {
|
||||
if (repeatCards != null && !repeatCards.isEmpty()) {
|
||||
if (sa.hasParam("ChooseOrder") && repeatCards.size() > 1) {
|
||||
final Player chooser = sa.getParam("ChooseOrder").equals("True") ? activator :
|
||||
AbilityUtils.getDefinedPlayers(source, sa.getParam("ChooseOrder"), sa).get(0);
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbilityCantSetSchemesInMotion;
|
||||
import forge.game.trigger.TriggerType;
|
||||
|
||||
public class SetInMotionEffect extends SpellAbilityEffect {
|
||||
|
||||
@@ -25,25 +18,13 @@ public class SetInMotionEffect extends SpellAbilityEffect {
|
||||
boolean again = sa.hasParam("Again");
|
||||
|
||||
int repeats = 1;
|
||||
|
||||
if (sa.hasParam("RepeatNum")) {
|
||||
repeats = AbilityUtils.calculateAmount(source, sa.getParam("RepeatNum"), sa);
|
||||
}
|
||||
|
||||
for (int i = 0; i < repeats; i++) {
|
||||
if (again) {
|
||||
// Set the current scheme in motion again
|
||||
Game game = controller.getGame();
|
||||
|
||||
if (StaticAbilityCantSetSchemesInMotion.any(game)) {
|
||||
return;
|
||||
}
|
||||
|
||||
game.getAction().moveToCommand(controller.getActiveScheme(), sa);
|
||||
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Scheme, controller.getActiveScheme());
|
||||
game.getTriggerHandler().runTrigger(TriggerType.SetInMotion, runParams, false);
|
||||
controller.setSchemeInMotion(sa, controller.getActiveScheme());
|
||||
} else {
|
||||
controller.setSchemeInMotion(sa);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.event.GameEventCardStatsChanged;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.trigger.TriggerHandler;
|
||||
@@ -53,7 +54,6 @@ public class SetStateEffect extends SpellAbilityEffect {
|
||||
final Game game = host.getGame();
|
||||
|
||||
final boolean remChanged = sa.hasParam("RememberChanged");
|
||||
final boolean hiddenAgenda = sa.hasParam("HiddenAgenda");
|
||||
final boolean optional = sa.hasParam("Optional");
|
||||
final CardCollection transformedCards = new CardCollection();
|
||||
|
||||
@@ -194,14 +194,12 @@ public class SetStateEffect extends SpellAbilityEffect {
|
||||
} else if (sa.isCloakUp()) {
|
||||
String sb = p + " has uncloaked " + gameCard.getName();
|
||||
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
} else if (hiddenAgenda) {
|
||||
if (gameCard.hasKeyword("Double agenda")) {
|
||||
String sb = p + " has revealed " + gameCard.getName() + " with the chosen names: " + gameCard.getNamedCards();
|
||||
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
} else {
|
||||
String sb = p + " has revealed " + gameCard.getName() + " with the chosen name " + gameCard.getNamedCard();
|
||||
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
}
|
||||
} else if (sa.isKeyword(Keyword.DOUBLE_AGENDA)) {
|
||||
String sb = p + " has revealed " + gameCard.getName() + " with the chosen names: " + gameCard.getNamedCards();
|
||||
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
} else if (sa.isKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
String sb = p + " has revealed " + gameCard.getName() + " with the chosen name " + gameCard.getNamedCard();
|
||||
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
}
|
||||
game.fireEvent(new GameEventCardStatsChanged(gameCard));
|
||||
if (sa.hasParam("Mega")) { // TODO move Megamorph into an Replacement Effect
|
||||
|
||||
@@ -84,7 +84,7 @@ public class TapOrUntapAllEffect extends SpellAbilityEffect {
|
||||
if (toTap) {
|
||||
if (gameCard.tap(true, sa, activator)) tapped.add(gameCard);
|
||||
} else {
|
||||
if (gameCard.untap(true)) untapped.add(gameCard);
|
||||
if (gameCard.untap()) untapped.add(gameCard);
|
||||
}
|
||||
}
|
||||
if (!tapped.isEmpty()) {
|
||||
|
||||
@@ -65,7 +65,7 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
|
||||
!gameCard.getController().equals(tapper));
|
||||
if (tap) {
|
||||
if (gameCard.tap(true, sa, tapper)) tapped.add(gameCard);
|
||||
} else if (gameCard.untap(true)) {
|
||||
} else if (gameCard.untap()) {
|
||||
untapMap.computeIfAbsent(tapper, i -> new CardCollection()).add(gameCard);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class UnattachEffect extends SpellAbilityEffect {
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Game game = sa.getHostCard().getGame();
|
||||
for (final Card tgtC : getTargetCards(sa)) {
|
||||
if (tgtC.isInPlay()) {
|
||||
if (!tgtC.isInPlay()) {
|
||||
continue;
|
||||
}
|
||||
// check if the object is still in game or if it was moved
|
||||
|
||||
@@ -44,7 +44,7 @@ public class UntapAllEffect extends SpellAbilityEffect {
|
||||
if (sa.hasParam("ControllerUntaps")) {
|
||||
untapper = c.getController();
|
||||
}
|
||||
if (c.untap(true)) {
|
||||
if (c.untap()) {
|
||||
untapMap.computeIfAbsent(untapper, i -> new CardCollection()).add(c);
|
||||
if (sa.hasParam("RememberUntapped")) card.addRemembered(c);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public class UntapEffect extends SpellAbilityEffect {
|
||||
if (gameCard == null || !tgtC.equalsWithGameTimestamp(gameCard)) {
|
||||
continue;
|
||||
}
|
||||
if (gameCard.untap(true)) untapped.add(gameCard);
|
||||
if (gameCard.untap()) untapped.add(gameCard);
|
||||
}
|
||||
if (etb) {
|
||||
// do not fire triggers
|
||||
@@ -114,7 +114,7 @@ public class UntapEffect extends SpellAbilityEffect {
|
||||
final CardCollectionView selected = p.getController().chooseCardsForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectCardToUntap"), mandatory ? num : 0, num, !mandatory, null);
|
||||
if (selected != null) {
|
||||
for (final Card c : selected) {
|
||||
if (c.untap(true)) untapped.add(c);
|
||||
if (c.untap()) untapped.add(c);
|
||||
}
|
||||
}
|
||||
if (!untapped.isEmpty()) {
|
||||
|
||||
@@ -188,7 +188,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
private boolean startsGameInPlay = false;
|
||||
private boolean drawnThisTurn = false;
|
||||
private boolean foughtThisTurn = false;
|
||||
private boolean becameTargetThisTurn, valiant = false;
|
||||
private boolean enlistedThisCombat = false;
|
||||
private boolean startedTheTurnUntapped = false;
|
||||
private boolean cameUnderControlSinceLastUpkeep = true; // for Echo
|
||||
@@ -250,6 +249,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
private int exertThisTurn = 0;
|
||||
private PlayerCollection exertedByPlayer = new PlayerCollection();
|
||||
|
||||
private PlayerCollection targetedFromThisTurn = new PlayerCollection();
|
||||
|
||||
private long bestowTimestamp = -1;
|
||||
private long transformedTimestamp = 0;
|
||||
private long prototypeTimestamp = -1;
|
||||
@@ -1075,7 +1076,22 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public final boolean isAdventureCard() {
|
||||
return hasState(CardStateName.Adventure);
|
||||
if (!hasState(CardStateName.Secondary))
|
||||
return false;
|
||||
return getState(CardStateName.Secondary).getType().hasSubtype("Adventure");
|
||||
}
|
||||
|
||||
public final boolean isOnAdventure() {
|
||||
if (!isAdventureCard())
|
||||
return false;
|
||||
if (getExiledWith() == null)
|
||||
return false;
|
||||
if (!CardStateName.Secondary.equals(getExiledWith().getCurrentStateName()))
|
||||
return false;
|
||||
if (!getExiledWith().getType().hasSubtype("Adventure")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public final boolean isBackSide() {
|
||||
@@ -1689,19 +1705,32 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getCounterMax(final CounterType counterType) {
|
||||
if (counterType.is(CounterEnumType.DREAM)) {
|
||||
return StaticAbilityMaxCounter.maxCounter(this, counterType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
|
||||
int addAmount = n;
|
||||
// Rules say it is only a SBA, but is it checked there too?
|
||||
if (counterType.is(CounterEnumType.DREAM) && hasKeyword("CARDNAME can't have more than seven dream counters on it.")) {
|
||||
addAmount = Math.min(addAmount, 7 - getCounters(CounterEnumType.DREAM));
|
||||
}
|
||||
|
||||
if (addAmount <= 0 || !canReceiveCounters(counterType)) {
|
||||
// As per rule 107.1b
|
||||
// CR 107.1b
|
||||
return;
|
||||
}
|
||||
|
||||
final int oldValue = getCounters(counterType);
|
||||
|
||||
Integer max = getCounterMax(counterType);
|
||||
if (max != null) {
|
||||
addAmount = Math.min(addAmount, max - oldValue);
|
||||
if (addAmount <= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final int newValue = addAmount + oldValue;
|
||||
if (fireEvents) {
|
||||
getGame().updateLastStateForCard(this);
|
||||
@@ -2590,7 +2619,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing:")
|
||||
|| keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic")
|
||||
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|
||||
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
|
||||
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")
|
||||
|| keyword.startsWith("Mobilize")) {
|
||||
final String[] k = keyword.split(":");
|
||||
sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")");
|
||||
} else if (keyword.startsWith("Crew")) {
|
||||
@@ -3014,7 +3044,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
// add Adventure to AbilityText
|
||||
if (sa.isAdventure() && state.getStateName().equals(CardStateName.Original)) {
|
||||
CardState advState = getState(CardStateName.Adventure);
|
||||
CardState advState = getState(CardStateName.Secondary);
|
||||
StringBuilder sbSA = new StringBuilder();
|
||||
sbSA.append(Localizer.getInstance().getMessage("lblAdventure"));
|
||||
sbSA.append(" — ").append(CardTranslation.getTranslatedName(advState.getName()));
|
||||
@@ -3022,6 +3052,15 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
sbSA.append(": ");
|
||||
sbSA.append(sAbility);
|
||||
sAbility = sbSA.toString();
|
||||
} else if (sa.isOmen() && state.getStateName().equals(CardStateName.Original)) {
|
||||
CardState advState = getState(CardStateName.Secondary);
|
||||
StringBuilder sbSA = new StringBuilder();
|
||||
sbSA.append(Localizer.getInstance().getMessage("lblOmen"));
|
||||
sbSA.append(" — ").append(CardTranslation.getTranslatedName(advState.getName()));
|
||||
sbSA.append(" ").append(sa.getPayCosts().toSimpleString());
|
||||
sbSA.append(": ");
|
||||
sbSA.append(sAbility);
|
||||
sAbility = sbSA.toString();
|
||||
} else if (sa.isSpell() && sa.isBasicSpell()) {
|
||||
continue;
|
||||
} else if (sa.hasParam("DescriptionFromChosenName") && !getNamedCard().isEmpty()) {
|
||||
@@ -3561,9 +3600,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Adenture may only be cast not from Battlefield
|
||||
if (isAdventureCard() && state.getView().getState() == CardStateName.Original) {
|
||||
for (SpellAbility sa : getState(CardStateName.Adventure).getSpellAbilities()) {
|
||||
// Adventure and Omen may only be cast not from Battlefield
|
||||
if (hasState(CardStateName.Secondary) && state.getView().getState() == CardStateName.Original) {
|
||||
for (SpellAbility sa : getState(CardStateName.Secondary).getSpellAbilities()) {
|
||||
if (mana == null || mana == sa.isManaAbility()) {
|
||||
list.add(sa);
|
||||
}
|
||||
@@ -3820,16 +3859,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public boolean hasBecomeTargetThisTurn() {
|
||||
return becameTargetThisTurn;
|
||||
return !targetedFromThisTurn.isEmpty();
|
||||
}
|
||||
public void setBecameTargetThisTurn(boolean becameTargetThisTurn0) {
|
||||
becameTargetThisTurn = becameTargetThisTurn0;
|
||||
public void addTargetFromThisTurn(Player p) {
|
||||
targetedFromThisTurn.add(p);
|
||||
}
|
||||
public boolean isValiant() {
|
||||
return valiant;
|
||||
}
|
||||
public void setValiant(boolean v) {
|
||||
valiant = v;
|
||||
public boolean isValiant(Player p) {
|
||||
return getController().equals(p) && !targetedFromThisTurn.contains(p);
|
||||
}
|
||||
|
||||
public boolean hasStartedTheTurnUntapped() {
|
||||
@@ -4898,10 +4934,31 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
return true;
|
||||
}
|
||||
|
||||
public final boolean untap(boolean untapAnimation) {
|
||||
if (!tapped) { return false; }
|
||||
public final boolean canUntap(Player phase, boolean predict) {
|
||||
if (!predict && !tapped) { return false; }
|
||||
if (phase != null && isExertedBy(phase)) {
|
||||
return false;
|
||||
}
|
||||
if (phase != null && hasKeyword("This card doesn't untap during your next untap step.")) {
|
||||
return false;
|
||||
}
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(this);
|
||||
runParams.put(AbilityKey.Player, phase);
|
||||
return !getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Untap, runParams);
|
||||
}
|
||||
|
||||
if (getGame().getReplacementHandler().run(ReplacementType.Untap, AbilityKey.mapFromAffected(this)) != ReplacementResult.NotReplaced) {
|
||||
public final boolean untap() {
|
||||
return untap(null);
|
||||
}
|
||||
public final boolean untap(Player phase) {
|
||||
if (!tapped) { return false; }
|
||||
if (phase != null && isExertedBy(phase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(this);
|
||||
runParams.put(AbilityKey.Player, phase);
|
||||
if (getGame().getReplacementHandler().run(ReplacementType.Untap, runParams) != ReplacementResult.NotReplaced) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4909,7 +4966,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
runUntapCommands();
|
||||
setTapped(false);
|
||||
view.updateNeedsUntapAnimation(untapAnimation);
|
||||
view.updateNeedsUntapAnimation(true);
|
||||
getGame().fireEvent(new GameEventCardTapped(this, false));
|
||||
return true;
|
||||
}
|
||||
@@ -5518,10 +5575,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
if (StringUtils.isNumeric(s)) {
|
||||
count += Integer.parseInt(s);
|
||||
} else {
|
||||
StaticAbility st = inst.getStatic();
|
||||
// TODO make keywordinterface inherit from CardTrait somehow, or invent new interface
|
||||
if (st != null && st.hasSVar(s)) {
|
||||
count += AbilityUtils.calculateAmount(this, st.getSVar(s), null);
|
||||
if (inst.hasSVar(s)) {
|
||||
count += AbilityUtils.calculateAmount(this, inst.getSVar(s), null);
|
||||
} else {
|
||||
String svar = StringUtils.join(parse);
|
||||
if (state.hasSVar(svar)) {
|
||||
@@ -6550,7 +6606,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
public void removeExertedBy(final Player player) {
|
||||
exertedByPlayer.remove(player);
|
||||
view.updateExertedThisTurn(this, getExertedThisTurn() > 0);
|
||||
// removeExertedBy is called on Untap phase, where it can't be exerted yet
|
||||
}
|
||||
|
||||
protected void resetExertedThisTurn() {
|
||||
@@ -7355,8 +7411,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
resetExcessDamage();
|
||||
setRegeneratedThisTurn(0);
|
||||
resetShieldCount();
|
||||
setBecameTargetThisTurn(false);
|
||||
setValiant(false);
|
||||
targetedFromThisTurn.clear();
|
||||
setFoughtThisTurn(false);
|
||||
turnedFaceUpThisTurn = false;
|
||||
clearMustBlockCards();
|
||||
@@ -7577,9 +7632,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
final List<SpellAbility> abilities = Lists.newArrayList();
|
||||
for (SpellAbility sa : getSpellAbilities()) {
|
||||
//adventure spell check
|
||||
if (isAdventureCard() && sa.isAdventure()) {
|
||||
if (getExiledWith() != null && getExiledWith().equals(this) && CardStateName.Adventure.equals(getExiledWith().getCurrentStateName()))
|
||||
continue; // skip since it's already on adventure
|
||||
if (isAdventureCard() && sa.isAdventure() && isOnAdventure()) {
|
||||
continue; // skip since it's already on adventure
|
||||
}
|
||||
//add alternative costs as additional spell abilities
|
||||
abilities.add(sa);
|
||||
@@ -7591,6 +7645,14 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false));
|
||||
}
|
||||
}
|
||||
if (isFaceDown() && isInZone(ZoneType.Command)) {
|
||||
for (KeywordInterface k : oState.getCachedKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
abilities.addAll(k.getAbilities());
|
||||
}
|
||||
for (KeywordInterface k : oState.getCachedKeyword(Keyword.DOUBLE_AGENDA)) {
|
||||
abilities.addAll(k.getAbilities());
|
||||
}
|
||||
}
|
||||
// Add Modal Spells
|
||||
if (isModal() && hasState(CardStateName.Modal)) {
|
||||
for (SpellAbility sa : getState(CardStateName.Modal).getSpellAbilities()) {
|
||||
@@ -8127,12 +8189,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public boolean isWitherDamage() {
|
||||
if (this.hasKeyword(Keyword.WITHER) || this.hasKeyword(Keyword.INFECT)) {
|
||||
if (hasKeyword(Keyword.WITHER) || hasKeyword(Keyword.INFECT)) {
|
||||
return true;
|
||||
}
|
||||
return StaticAbilityWitherDamage.isWitherDamage(this);
|
||||
}
|
||||
|
||||
public boolean isInfectDamage(Player target) {
|
||||
return hasKeyword(Keyword.INFECT) || StaticAbilityInfectDamage.isInfectDamage(target);
|
||||
}
|
||||
|
||||
public Set<CardStateName> getUnlockedRooms() {
|
||||
return this.unlockedRooms;
|
||||
}
|
||||
@@ -8209,9 +8275,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
if (!isRoom()) {
|
||||
return;
|
||||
}
|
||||
if (!isInPlay()) {
|
||||
return;
|
||||
}
|
||||
if (isFaceDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,10 +252,10 @@ public class CardCopyService {
|
||||
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
|
||||
newCopy.addAlternateState(CardStateName.Transformed, false);
|
||||
newCopy.getState(CardStateName.Transformed).copyFrom(copyFrom.getState(CardStateName.Transformed), true);
|
||||
} else if (copyFrom.isAdventureCard()) {
|
||||
} else if (copyFrom.hasState(CardStateName.Secondary)) {
|
||||
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
|
||||
newCopy.addAlternateState(CardStateName.Adventure, false);
|
||||
newCopy.getState(CardStateName.Adventure).copyFrom(copyFrom.getState(CardStateName.Adventure), true);
|
||||
newCopy.addAlternateState(CardStateName.Secondary, false);
|
||||
newCopy.getState(CardStateName.Secondary).copyFrom(copyFrom.getState(CardStateName.Secondary), true);
|
||||
} else if (copyFrom.isSplitCard()) {
|
||||
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
|
||||
newCopy.addAlternateState(CardStateName.LeftSplit, false);
|
||||
|
||||
@@ -211,8 +211,8 @@ public class CardFactory {
|
||||
c.setRarity(cp.getRarity());
|
||||
c.setState(CardStateName.RightSplit, false);
|
||||
c.setImageKey(originalPicture);
|
||||
} else if (c.isAdventureCard()) {
|
||||
c.setState(CardStateName.Adventure, false);
|
||||
} else if (c.hasState(CardStateName.Secondary)) {
|
||||
c.setState(CardStateName.Secondary, false);
|
||||
c.setImageKey(originalPicture);
|
||||
} else if (c.canSpecialize()) {
|
||||
c.setState(CardStateName.SpecializeW, false);
|
||||
@@ -281,9 +281,6 @@ public class CardFactory {
|
||||
} else if (state != CardStateName.Original) {
|
||||
CardFactoryUtil.setupKeywordedAbilities(card);
|
||||
}
|
||||
if (state == CardStateName.Adventure) {
|
||||
CardFactoryUtil.setupAdventureAbility(card);
|
||||
}
|
||||
}
|
||||
|
||||
card.setState(CardStateName.Original, false);
|
||||
@@ -564,14 +561,14 @@ public class CardFactory {
|
||||
final CardState ret2 = new CardState(out, CardStateName.Flipped);
|
||||
ret2.copyFrom(in.getState(CardStateName.Flipped), false, sa);
|
||||
result.put(CardStateName.Flipped, ret2);
|
||||
} else if (in.isAdventureCard()) {
|
||||
} else if (in.hasState(CardStateName.Secondary)) {
|
||||
final CardState ret1 = new CardState(out, CardStateName.Original);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
|
||||
result.put(CardStateName.Original, ret1);
|
||||
|
||||
final CardState ret2 = new CardState(out, CardStateName.Adventure);
|
||||
ret2.copyFrom(in.getState(CardStateName.Adventure), false, sa);
|
||||
result.put(CardStateName.Adventure, ret2);
|
||||
final CardState ret2 = new CardState(out, CardStateName.Secondary);
|
||||
ret2.copyFrom(in.getState(CardStateName.Secondary), false, sa);
|
||||
result.put(CardStateName.Secondary, ret2);
|
||||
} else if (in.isTransformable() && sa instanceof SpellAbility && (
|
||||
ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi()) ||
|
||||
ApiType.CopySpellAbility.equals(((SpellAbility)sa).getApi()) ||
|
||||
|
||||
@@ -215,7 +215,7 @@ public class CardFactoryUtil {
|
||||
return manifestUp;
|
||||
}
|
||||
|
||||
public static boolean handleHiddenAgenda(Player player, Card card) {
|
||||
public static boolean handleHiddenAgenda(Player player, Card card, KeywordInterface ki) {
|
||||
SpellAbility sa = new SpellAbility.EmptySa(card);
|
||||
sa.putParam("AILogic", card.getSVar("AgendaLogic"));
|
||||
Predicate<ICardFace> cpp = x -> true;
|
||||
@@ -228,7 +228,7 @@ public class CardFactoryUtil {
|
||||
}
|
||||
card.addNamedCard(name);
|
||||
|
||||
if (card.hasKeyword("Double agenda")) {
|
||||
if (ki.getKeyword().equals(Keyword.DOUBLE_AGENDA)) {
|
||||
String name2 = player.getController().chooseCardName(sa, cpp, "Card.!NamedCard",
|
||||
"Name a second card for " + card.getName());
|
||||
if (name2 == null || name2.isEmpty()) {
|
||||
@@ -239,14 +239,14 @@ public class CardFactoryUtil {
|
||||
|
||||
card.turnFaceDown();
|
||||
card.addMayLookAt(player.getGame().getNextTimestamp(), ImmutableList.of(player));
|
||||
card.addSpellAbility(abilityRevealHiddenAgenda(card));
|
||||
ki.addSpellAbility(abilityRevealHiddenAgenda(card));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static SpellAbility abilityRevealHiddenAgenda(final Card sourceCard) {
|
||||
String ab = "ST$ SetState | Cost$ 0"
|
||||
+ " | ConditionDefined$ Self | ConditionPresent$ Card.faceDown+inZoneCommand"
|
||||
+ " | HiddenAgenda$ True"
|
||||
+ " | PresentDefined$ Self | IsPresent$ Card.faceDown+inZoneCommand"
|
||||
+ " | ActivationZone$ Command | Secondary$ True"
|
||||
+ " | Mode$ TurnFaceUp | SpellDescription$ Reveal this Hidden Agenda at any time.";
|
||||
return AbilityFactory.getAbility(ab, sourceCard);
|
||||
}
|
||||
@@ -519,14 +519,6 @@ public class CardFactoryUtil {
|
||||
return filteredkw;
|
||||
}
|
||||
|
||||
public static int getCardTypesFromList(final CardCollectionView list) {
|
||||
EnumSet<CardType.CoreType> types = EnumSet.noneOf(CardType.CoreType.class);
|
||||
for (Card c1 : list) {
|
||||
c1.getType().getCoreTypes().forEach(types::add);
|
||||
}
|
||||
return types.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the ability factory abilities.
|
||||
*
|
||||
@@ -1050,7 +1042,7 @@ public class CardFactoryUtil {
|
||||
|
||||
inst.addTrigger(dethroneTrigger);
|
||||
} else if (keyword.equals("Double team")) {
|
||||
final String trigString = "Mode$ Attacks | ValidCard$ Card.Self+nonToken | TriggerZones$ Battlefield" +
|
||||
final String trigString = "Mode$ Attacks | ValidCard$ Card.Self+!token | TriggerZones$ Battlefield" +
|
||||
" | Secondary$ True | TriggerDescription$ Double team (" + inst.getReminderText() + ")";
|
||||
final String maSt = "DB$ MakeCard | DefinedName$ Self | Zone$ Hand | RememberMade$ True | Conjure$ True";
|
||||
final String puSt = "DB$ Pump | RememberObjects$ Self";
|
||||
@@ -1075,8 +1067,7 @@ public class CardFactoryUtil {
|
||||
" | IsPresent$ Card.Self+cameUnderControlSinceLastUpkeep | Secondary$ True | " +
|
||||
"TriggerDescription$ " + inst.getReminderText();
|
||||
|
||||
String effect = "DB$ Sacrifice | SacValid$ Self | "
|
||||
+ "Echo$ " + cost;
|
||||
String effect = "DB$ Sacrifice | SacValid$ Self | Echo$ " + cost;
|
||||
|
||||
final Trigger trigger = TriggerHandler.parseTrigger(upkeepTrig, card, intrinsic);
|
||||
trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
|
||||
@@ -1534,6 +1525,20 @@ public class CardFactoryUtil {
|
||||
triggerDrawn.setOverridingAbility(revealSA);
|
||||
|
||||
inst.addTrigger(triggerDrawn);
|
||||
} else if (keyword.startsWith("Mobilize")) {
|
||||
final String[] k = keyword.split(":");
|
||||
final String n = k[1];
|
||||
|
||||
final String trigStr = "Mode$ Attacks | ValidCard$ Card.Self | Secondary$ True"
|
||||
+ " | TriggerDescription$ Mobilize " + n + " (" + inst.getReminderText() + ")";
|
||||
|
||||
final String effect = "DB$ Token | TokenAmount$ " + n + " | TokenScript$ r_1_1_warrior"
|
||||
+ " | TokenTapped$ True | TokenAttacking$ True | AtEOT$ Sacrifice";
|
||||
|
||||
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
|
||||
|
||||
trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
|
||||
inst.addTrigger(trigger);
|
||||
} else if (keyword.startsWith("Modular")) {
|
||||
final String abStr = "DB$ PutCounter | ValidTgts$ Artifact.Creature | " +
|
||||
"TgtPrompt$ Select target artifact creature | CounterType$ P1P1 | CounterNum$ ModularX";
|
||||
@@ -1706,8 +1711,7 @@ public class CardFactoryUtil {
|
||||
+ " | IsPresent$ Card.Self+!IsRenowned | CombatDamage$ True | Secondary$ True"
|
||||
+ " | TriggerDescription$ Renown " + k[1] +" (" + inst.getReminderText() + ")";
|
||||
|
||||
final String effect = "DB$ PutCounter | Defined$ Self | "
|
||||
+ "CounterType$ P1P1 | CounterNum$ " + k[1];
|
||||
final String effect = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ " + k[1];
|
||||
|
||||
final Trigger parsedTrigger = TriggerHandler.parseTrigger(renownTrig, card, intrinsic);
|
||||
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
|
||||
@@ -2308,6 +2312,40 @@ public class CardFactoryUtil {
|
||||
|
||||
re.setOverridingAbility(saExile);
|
||||
|
||||
inst.addReplacement(re);
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack | ExcludeDestination$ Exile ");
|
||||
sb.append("| ValidStackSa$ Spell.Harmonize+castKeyword | Description$ Harmonize");
|
||||
|
||||
if (keyword.contains(":")) {
|
||||
final String[] k = keyword.split(":");
|
||||
final Cost cost = new Cost(k[1], false);
|
||||
sb.append(cost.isOnlyManaCost() ? " " : "—").append(cost.toSimpleString());
|
||||
sb.append(cost.isOnlyManaCost() ? "" : ".");
|
||||
|
||||
String extraDesc = k.length > 3 ? k[3] : "";
|
||||
if (!extraDesc.isEmpty()) { // extra params added in GameActionUtil, desc added here
|
||||
sb.append(cost.isOnlyManaCost() ? ". " : " ").append(extraDesc);
|
||||
}
|
||||
}
|
||||
|
||||
sb.append(" (").append(inst.getReminderText()).append(")");
|
||||
|
||||
String repeffstr = sb.toString();
|
||||
|
||||
String abExile = "DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Exile";
|
||||
|
||||
SpellAbility saExile = AbilityFactory.getAbility(abExile, card);
|
||||
|
||||
if (!intrinsic) {
|
||||
saExile.setIntrinsic(false);
|
||||
}
|
||||
|
||||
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, host, intrinsic, card);
|
||||
|
||||
re.setOverridingAbility(saExile);
|
||||
|
||||
inst.addReplacement(re);
|
||||
} else if (keyword.startsWith("Graft")) {
|
||||
final String[] k = keyword.split(":");
|
||||
@@ -3596,8 +3634,7 @@ public class CardFactoryUtil {
|
||||
|
||||
newSA.setAlternativeCost(AlternativeCost.Surge);
|
||||
|
||||
String desc = "Surge " + surgeCost.toSimpleString() + " (" + inst.getReminderText()
|
||||
+ ")";
|
||||
String desc = "Surge " + surgeCost.toSimpleString() + " (" + inst.getReminderText() + ")";
|
||||
newSA.setDescription(desc);
|
||||
|
||||
newSA.setIntrinsic(intrinsic);
|
||||
@@ -3959,6 +3996,12 @@ public class CardFactoryUtil {
|
||||
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True" +
|
||||
" | Description$ Flying (" + inst.getReminderText() + ")";
|
||||
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
String reduceEffect = "Mode$ ReduceCost | ValidCard$ Card.Self | ValidSpell$ Spell.Harmonize | Secondary$ True"
|
||||
+ " | Amount$ AffectedX | EffectZone$ All | Description$ Harmonize (" + inst.getReminderText() + ")";
|
||||
StaticAbility stAb = StaticAbility.create(reduceEffect, state.getCard(), state, intrinsic);
|
||||
stAb.setSVar("AffectedX", "Count$OptionalKeywordAmount");
|
||||
inst.addStaticAbility(stAb);
|
||||
} else if (keyword.startsWith("Hexproof")) {
|
||||
final StringBuilder sbDesc = new StringBuilder("Hexproof");
|
||||
final StringBuilder sbValid = new StringBuilder();
|
||||
@@ -4109,16 +4152,7 @@ public class CardFactoryUtil {
|
||||
card.addTrigger(defeatedTrigger);
|
||||
}
|
||||
|
||||
public static void setupAdventureAbility(Card card) {
|
||||
if (card.getCurrentStateName() != CardStateName.Adventure) {
|
||||
return;
|
||||
}
|
||||
SpellAbility sa = card.getFirstSpellAbility();
|
||||
if (sa == null) {
|
||||
return;
|
||||
}
|
||||
sa.setCardState(card.getCurrentState());
|
||||
|
||||
public static ReplacementEffect setupAdventureAbility(CardState card) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack | ExcludeDestination$ Exile ");
|
||||
sb.append("| ValidStackSa$ Spell.Adventure | Fizzle$ False | Secondary$ True | Description$ Adventure");
|
||||
@@ -4129,20 +4163,37 @@ public class CardFactoryUtil {
|
||||
|
||||
SpellAbility saExile = AbilityFactory.getAbility(abExile, card);
|
||||
|
||||
String abEffect = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell+nonToken";
|
||||
String abEffect = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell+!token";
|
||||
AbilitySub saEffect = (AbilitySub)AbilityFactory.getAbility(abEffect, card);
|
||||
|
||||
StringBuilder sbPlay = new StringBuilder();
|
||||
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure");
|
||||
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+!Adventure");
|
||||
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
|
||||
saEffect.setSVar("Play", sbPlay.toString());
|
||||
|
||||
saExile.setSubAbility(saEffect);
|
||||
|
||||
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card, true);
|
||||
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card.getCard(), true);
|
||||
|
||||
re.setOverridingAbility(saExile);
|
||||
card.addReplacementEffect(re);
|
||||
return re;
|
||||
}
|
||||
|
||||
public static ReplacementEffect setupOmenAbility(CardState card) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack ");
|
||||
sb.append("| ValidStackSa$ Spell.Omen | Fizzle$ False | Secondary$ True | Description$ Omen");
|
||||
|
||||
String repeffstr = sb.toString();
|
||||
|
||||
String abShuffle = "DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Library | Shuffle$ True | StackDescription$ None";
|
||||
AbilitySub saShuffle = (AbilitySub)AbilityFactory.getAbility(abShuffle, card);
|
||||
|
||||
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card.getCard(), true);
|
||||
|
||||
re.setOverridingAbility(saShuffle);
|
||||
|
||||
return re;
|
||||
}
|
||||
|
||||
public static void setFaceDownState(Card c, SpellAbility sa) {
|
||||
|
||||
@@ -250,6 +250,19 @@ public class CardLists {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static CardCollection canSubsequentlyTarget(Iterable<Card> list, SpellAbility source) {
|
||||
if (source.getTargets().isEmpty()) {
|
||||
return (CardCollection) list;
|
||||
}
|
||||
|
||||
return CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean test(Card card) {
|
||||
return source.canTarget(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static CardCollection getKeyword(Iterable<Card> cardList, final String keyword) {
|
||||
return CardLists.filter(cardList, CardPredicates.hasKeyword(keyword));
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public final class CardPredicates {
|
||||
}
|
||||
|
||||
public static Predicate<Card> possibleBlockers(final Card attacker) {
|
||||
return c -> c.isCreature() && CombatUtil.canBlock(attacker, c);
|
||||
return c -> CombatUtil.canBlock(attacker, c);
|
||||
}
|
||||
|
||||
public static Predicate<Card> possibleBlockerForAtLeastOne(final Iterable<Card> attackers) {
|
||||
|
||||
@@ -1163,7 +1163,7 @@ public class CardProperty {
|
||||
}
|
||||
else if (prop.isEmpty() && dmgSource.equalsWithGameTimestamp(source)) {
|
||||
found = true;
|
||||
} else if (dmgSource.isValid(prop.split(","), sourceController, source, spellAbility)) {
|
||||
} else if (dmgSource.isValid(prop.split(";"), sourceController, source, spellAbility)) {
|
||||
found = true;
|
||||
}
|
||||
if (found) {
|
||||
@@ -1265,10 +1265,6 @@ public class CardProperty {
|
||||
if (card.getBlockedByThisTurn().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("notAttackedThisTurn")) {
|
||||
if (card.getDamageHistory().getCreatureAttacksThisTurn() > 0) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("greatestPower")) {
|
||||
CardCollectionView cards = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.CREATURES);
|
||||
if (property.contains("ControlledBy")) {
|
||||
@@ -1410,10 +1406,6 @@ public class CardProperty {
|
||||
if (property.contains("Created") && card.getCastSA() != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("nonToken")) {
|
||||
if (card.isToken() || card.isTokenCard()) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("copiedSpell")) {
|
||||
if (!card.isCopiedSpell()) {
|
||||
return false;
|
||||
@@ -1879,10 +1871,6 @@ public class CardProperty {
|
||||
if (!card.isSolved()) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.equals("IsUnsolved")) {
|
||||
if (card.isSolved()) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.equals("IsSaddled")) {
|
||||
if (!card.isSaddled()) {
|
||||
return false;
|
||||
@@ -2027,10 +2015,6 @@ public class CardProperty {
|
||||
if (!card.isCommander()) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.equals("IsNotCommander")) {
|
||||
if (card.isCommander()) {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("NotedFor")) {
|
||||
final String key = property.substring("NotedFor".length());
|
||||
for (String note : sourceController.getNotesForName(key)) {
|
||||
|
||||
@@ -84,8 +84,9 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
|
||||
private ReplacementEffect loyaltyRep;
|
||||
private ReplacementEffect defenseRep;
|
||||
private ReplacementEffect battleTypeRep;
|
||||
private ReplacementEffect sagaRep;
|
||||
private ReplacementEffect adventureRep;
|
||||
private ReplacementEffect omenRep;
|
||||
|
||||
private SpellAbility manifestUp;
|
||||
private SpellAbility cloakUp;
|
||||
@@ -513,13 +514,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
}
|
||||
result.add(defenseRep);
|
||||
|
||||
if (battleTypeRep == null) {
|
||||
if(type.hasSubtype("Siege")) {
|
||||
// battleTypeRep; // - Choose a player to protect it
|
||||
}
|
||||
}
|
||||
//result.add(battleTypeRep);
|
||||
|
||||
// TODO add Siege "Choose a player to protect it"
|
||||
}
|
||||
if (type.hasSubtype("Saga") && !hasKeyword(Keyword.READ_AHEAD)) {
|
||||
if (sagaRep == null) {
|
||||
@@ -527,6 +522,18 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
}
|
||||
result.add(sagaRep);
|
||||
}
|
||||
if (type.hasSubtype("Adventure")) {
|
||||
if (this.adventureRep == null) {
|
||||
adventureRep = CardFactoryUtil.setupAdventureAbility(this);
|
||||
}
|
||||
result.add(adventureRep);
|
||||
}
|
||||
if (type.hasSubtype("Omen")) {
|
||||
if (this.omenRep == null) {
|
||||
omenRep = CardFactoryUtil.setupOmenAbility(this);
|
||||
}
|
||||
result.add(omenRep);
|
||||
}
|
||||
|
||||
card.updateReplacementEffects(result, this);
|
||||
return result;
|
||||
@@ -687,6 +694,12 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
|
||||
if (source.sagaRep != null) {
|
||||
sagaRep = source.sagaRep.copy(card, true);
|
||||
}
|
||||
if (source.adventureRep != null) {
|
||||
adventureRep = source.adventureRep.copy(card, true);
|
||||
}
|
||||
if (source.omenRep != null) {
|
||||
omenRep = source.omenRep.copy(card, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,8 +161,8 @@ public class CardView extends GameEntityView {
|
||||
return get(TrackableProperty.DoubleFaced);
|
||||
}
|
||||
|
||||
public boolean isAdventureCard() {
|
||||
return get(TrackableProperty.Adventure);
|
||||
public boolean hasSecondaryState() {
|
||||
return get(TrackableProperty.Secondary);
|
||||
}
|
||||
|
||||
public boolean isModalCard() {
|
||||
@@ -1029,7 +1029,7 @@ public class CardView extends GameEntityView {
|
||||
set(TrackableProperty.Foretold, c.isForetold());
|
||||
set(TrackableProperty.Manifested, c.isManifested());
|
||||
set(TrackableProperty.Cloaked, c.isCloaked());
|
||||
set(TrackableProperty.Adventure, c.isAdventureCard());
|
||||
set(TrackableProperty.Secondary, c.hasState(CardStateName.Secondary));
|
||||
set(TrackableProperty.DoubleFaced, c.isDoubleFaced());
|
||||
set(TrackableProperty.Modal, c.isModal());
|
||||
set(TrackableProperty.Room, c.isRoom());
|
||||
|
||||
@@ -355,6 +355,8 @@ public enum CounterEnumType {
|
||||
|
||||
QUEST("QUEST", 251, 189, 0),
|
||||
|
||||
RALLY("RALLY", 25, 230, 225),
|
||||
|
||||
RELEASE("RELEASE", 200, 210, 50),
|
||||
|
||||
REPRIEVE("REPR", 240, 120, 50),
|
||||
@@ -458,6 +460,8 @@ public enum CounterEnumType {
|
||||
WIND("WIND", 0, 236, 255),
|
||||
|
||||
WISH("WISH", 255, 85, 206),
|
||||
|
||||
WRECK("WRECK", 208, 55, 255),
|
||||
|
||||
// Player Counters
|
||||
|
||||
@@ -477,6 +481,7 @@ public enum CounterEnumType {
|
||||
FIRSTSTRIKE("First Strike"),
|
||||
DOUBLESTRIKE("Double Strike"),
|
||||
DEATHTOUCH("Deathtouch"),
|
||||
DECAYED("Decayed"),
|
||||
HASTE("Haste"),
|
||||
HEXPROOF("Hexproof"),
|
||||
INDESTRUCTIBLE("Indestructible"),
|
||||
@@ -484,8 +489,8 @@ public enum CounterEnumType {
|
||||
MENACE("Menace"),
|
||||
REACH("Reach"),
|
||||
TRAMPLE("Trample"),
|
||||
VIGILANCE("Vigilance")
|
||||
SHADOW("Shadow")
|
||||
VIGILANCE("Vigilance"),
|
||||
SHADOW("Shadow"),
|
||||
EXALTED("Exalted")
|
||||
//*/
|
||||
;
|
||||
|
||||
@@ -18,7 +18,7 @@ public class CounterType implements Comparable<CounterType>, Serializable {
|
||||
|
||||
// Rule 122.1b
|
||||
static ImmutableList<String> keywordCounter = ImmutableList.of(
|
||||
"Flying", "First Strike", "Double Strike", "Deathtouch", "Exalted", "Haste", "Hexproof",
|
||||
"Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof",
|
||||
"Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance");
|
||||
|
||||
private static Map<CounterEnumType, CounterType> eMap = Maps.newEnumMap(CounterEnumType.class);
|
||||
|
||||
@@ -281,7 +281,11 @@ public class TokenInfo {
|
||||
final Card host = sa.getHostCard();
|
||||
final Game game = host.getGame();
|
||||
|
||||
String edition = ObjectUtils.firstNonNull(sa.getOriginalHost(), host).getSetCode();
|
||||
Card editionHost = sa.getOriginalHost();
|
||||
if (sa.getKeyword() != null && sa.getKeyword().getStatic() != null) {
|
||||
editionHost = sa.getKeyword().getStatic().getHostCard();
|
||||
}
|
||||
String edition = ObjectUtils.firstNonNull(editionHost, host).getSetCode();
|
||||
PaperToken token = StaticData.instance().getAllTokens().getToken(script, edition);
|
||||
|
||||
if (token == null) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityBlockRestrict;
|
||||
import forge.game.staticability.StaticAbilityBlockTapped;
|
||||
import forge.game.staticability.StaticAbilityCantAttackBlock;
|
||||
import forge.game.staticability.StaticAbilityMustBlock;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -201,6 +202,10 @@ public class CombatUtil {
|
||||
private static boolean canAttack(final Card attacker, final GameEntity defender, final boolean forNextTurn) {
|
||||
final Game game = attacker.getGame();
|
||||
|
||||
if (attacker.isBattle()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic checks (unless is for next turn)
|
||||
if (!forNextTurn && (
|
||||
!attacker.isCreature()
|
||||
@@ -476,16 +481,23 @@ public class CombatUtil {
|
||||
* @return a boolean.
|
||||
*/
|
||||
public static boolean canBlock(final Card blocker, final boolean nextTurn) {
|
||||
if (blocker == null) {
|
||||
if (blocker == null || !blocker.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!nextTurn && blocker.isTapped() && !blocker.hasKeyword("CARDNAME can block as though it were untapped.")) {
|
||||
if (blocker.isBattle()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blocker.hasKeyword("CARDNAME can't block.") || blocker.hasKeyword("CARDNAME can't attack or block.")
|
||||
|| blocker.isPhasedOut()) {
|
||||
if (!nextTurn && blocker.isPhasedOut()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!nextTurn && blocker.isTapped() && !StaticAbilityBlockTapped.canBlockTapped(blocker)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blocker.hasKeyword("CARDNAME can't block.") || blocker.hasKeyword("CARDNAME can't attack or block.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -988,7 +1000,7 @@ public class CombatUtil {
|
||||
* @return a boolean.
|
||||
*/
|
||||
public static boolean canBlock(final Card attacker, final Card blocker, final boolean nextTurn) {
|
||||
if (attacker == null || blocker == null) {
|
||||
if (attacker == null || blocker == null || !blocker.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -495,6 +495,12 @@ public class Cost implements Serializable {
|
||||
return new CostReveal(splitStr[0], splitStr[1], description, "Hand,Battlefield");
|
||||
}
|
||||
|
||||
if (parse.startsWith("Behold<")) {
|
||||
final String[] splitStr = abCostParse(parse, 3);
|
||||
final String description = splitStr.length > 2 ? splitStr[2] : null;
|
||||
return new CostBehold(splitStr[0], splitStr[1], description);
|
||||
}
|
||||
|
||||
if (parse.startsWith("ExiledMoveToGrave<")) {
|
||||
final String[] splitStr = abCostParse(parse, 3);
|
||||
final String description = splitStr.length > 2 ? splitStr[2] : null;
|
||||
|
||||
29
forge-game/src/main/java/forge/game/cost/CostBehold.java
Normal file
29
forge-game/src/main/java/forge/game/cost/CostBehold.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package forge.game.cost;
|
||||
|
||||
public class CostBehold extends CostReveal {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public CostBehold(String amount, String type, String description) {
|
||||
super(amount, type, description, "Hand,Battlefield");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("Behold ");
|
||||
|
||||
final Integer i = this.convertAmount();
|
||||
|
||||
final String desc = this.getTypeDescription() == null ? this.getType() : this.getTypeDescription();
|
||||
|
||||
sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), desc));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// Inputs
|
||||
public <T> T accept(ICostVisitor<T> visitor) {
|
||||
return visitor.visit(this);
|
||||
}
|
||||
}
|
||||
@@ -222,7 +222,7 @@ public class CostExile extends CostPartWithList {
|
||||
int amount = this.getAbilityAmount(ability);
|
||||
|
||||
if (nTypes > -1) {
|
||||
if (CardFactoryUtil.getCardTypesFromList(list) < nTypes) return false;
|
||||
if (AbilityUtils.countCardTypesFromList(list, false) < nTypes) return false;
|
||||
}
|
||||
|
||||
if (sharedType) { // will need more logic if cost ever wants more than 2 that share a type
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CostForage extends CostPartWithList {
|
||||
if (graveyard.size() >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
CardCollection food = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Food"), CardPredicates.canBeSacrificedBy(ability, effect));
|
||||
if (!food.isEmpty()) {
|
||||
return true;
|
||||
|
||||
@@ -96,7 +96,7 @@ public class CostReveal extends CostPartWithList {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String toString() {
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("Reveal ");
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@ public class CostSacrifice extends CostPartWithList {
|
||||
}
|
||||
|
||||
CardCollectionView typeList = payer.getCardsIn(ZoneType.Battlefield);
|
||||
typeList = CardLists.getValidCards(typeList, type.split(";"), payer, source, ability);
|
||||
if (!type.contains("X")) {
|
||||
typeList = CardLists.getValidCards(typeList, type.split(";"), payer, source, ability);
|
||||
}
|
||||
typeList = CardLists.filter(typeList, CardPredicates.canBeSacrificedBy(ability, effect));
|
||||
if (differentNames) {
|
||||
// TODO rewrite with sharesName to respect Spy Kit
|
||||
|
||||
@@ -87,7 +87,7 @@ public class CostUntap extends CostPart {
|
||||
@Override
|
||||
public boolean payAsDecided(Player ai, PaymentDecision decision, SpellAbility ability, final boolean effect) {
|
||||
final Card c = ability.getHostCard();
|
||||
if (c.untap(true)) {
|
||||
if (c.untap()) {
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
final Map<Player, CardCollection> map = Maps.newHashMap();
|
||||
map.put(ai, new CardCollection(c));
|
||||
|
||||
@@ -94,7 +94,7 @@ public class CostUntapType extends CostPartWithList {
|
||||
|
||||
@Override
|
||||
protected Card doPayment(Player payer, SpellAbility ability, Card targetCard, final boolean effect) {
|
||||
targetCard.untap(true);
|
||||
targetCard.untap();
|
||||
return targetCard;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public class CostUntapType extends CostPartWithList {
|
||||
protected CardCollectionView doListPayment(Player payer, SpellAbility ability, CardCollectionView targetCards, final boolean effect) {
|
||||
CardCollection untapped = new CardCollection();
|
||||
for (Card c : targetCards) {
|
||||
if (c.untap(true)) untapped.add(c);
|
||||
if (c.untap()) untapped.add(c);
|
||||
}
|
||||
if (!untapped.isEmpty()) {
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.game.cost;
|
||||
|
||||
public interface ICostVisitor<T> {
|
||||
|
||||
T visit(CostBehold cost);
|
||||
T visit(CostGainControl cost);
|
||||
T visit(CostChooseColor cost);
|
||||
T visit(CostChooseCreatureType cost);
|
||||
@@ -65,6 +66,10 @@ public interface ICostVisitor<T> {
|
||||
public T visit(CostDiscard cost) {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public T visit(CostBehold cost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T visit(CostDamage cost) {
|
||||
|
||||
@@ -57,6 +57,7 @@ public enum Keyword {
|
||||
DISGUISE("Disguise", KeywordWithCost.class, false, "You may cast this card face down for {3} as a 2/2 creature with ward {2}. Turn it face up any time for its disguise cost."),
|
||||
DISTURB("Disturb", KeywordWithCost.class, false, "You may cast this card from your graveyard transformed for its disturb cost."),
|
||||
DOCTORS_COMPANION("Doctor's companion", Partner.class, true, "You can have two commanders if the other is the Doctor."),
|
||||
DOUBLE_AGENDA("Double agenda", SimpleKeyword.class, false, "Start the game with this conspiracy face down in the command zone and secretly choose two different card names. You may turn this conspiracy face up any time and reveal those names."),
|
||||
DOUBLE_STRIKE("Double Strike", SimpleKeyword.class, true, "This creature deals both first-strike and regular combat damage."),
|
||||
DOUBLE_TEAM("Double team", SimpleKeyword.class, true, "When this creature attacks, if it's not a token, conjure a duplicate of it into your hand. Then both cards perpetually lose double team."),
|
||||
DREDGE("Dredge", KeywordWithAmount.class, false, "If you would draw a card, instead you may put exactly {%d:card} from the top of your library into your graveyard. If you do, return this card from your graveyard to your hand. Otherwise, draw a card."),
|
||||
@@ -95,10 +96,12 @@ public enum Keyword {
|
||||
GIFT("Gift", SimpleKeyword.class, true, "You may promise an opponent a gift as you cast this spell. If you do, when it enters, they %s."),
|
||||
GRAFT("Graft", KeywordWithAmount.class, false, "This permanent enters with {%d:+1/+1 counter} on it. Whenever another creature enters, you may move a +1/+1 counter from this permanent onto it."),
|
||||
GRAVESTORM("Gravestorm", SimpleKeyword.class, false, "When you cast this spell, copy it for each permanent that was put into a graveyard from the battlefield this turn. If the spell has any targets, you may choose new targets for any of the copies."),
|
||||
HARMONIZE("Harmonize", KeywordWithCost.class, false, "You may cast this card from your graveyard for its harmonize cost. You may tap a creature you control to reduce that cost by {X}, where X is its power. Then exile this spell."),
|
||||
HASTE("Haste", SimpleKeyword.class, true, "This creature can attack and {T} as soon as it comes under your control."),
|
||||
HAUNT("Haunt", SimpleKeyword.class, false, "When this is put into a graveyard, exile it haunting target creature."),
|
||||
HEXPROOF("Hexproof", Hexproof.class, true, "This can't be the target of %s spells or abilities your opponents control."),
|
||||
HIDEAWAY("Hideaway", KeywordWithAmount.class, false, "When this permanent enters, look at the top {%d:card} of your library, exile one face down, then put the rest on the bottom of your library."),
|
||||
HIDDEN_AGENDA("Hidden agenda", SimpleKeyword.class, false, "Start the game with this conspiracy face down in the command zone and secretly choose a card name. You may turn this conspiracy face up any time and reveal that name."),
|
||||
HORSEMANSHIP("Horsemanship", SimpleKeyword.class, true, "This creature can't be blocked except by creatures with horsemanship."),
|
||||
IMPENDING("Impending", KeywordWithCostAndAmount.class, false, "If you cast this spell for its impending cost, it enters with {%2$d:time counter} and isn't a creature until the last is removed. At the beginning of your end step, remove a time counter from it."),
|
||||
IMPROVISE("Improvise", SimpleKeyword.class, true, "Your artifacts can help cast this spell. Each artifact you tap after you're done activating mana abilities pays for {1}."),
|
||||
@@ -119,6 +122,7 @@ public enum Keyword {
|
||||
MENACE("Menace", SimpleKeyword.class, true, "This creature can't be blocked except by two or more creatures."),
|
||||
MEGAMORPH("Megamorph", KeywordWithCost.class, false, "You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its megamorph cost and put a +1/+1 counter on it."),
|
||||
MIRACLE("Miracle", KeywordWithCost.class, false, "You may cast this card for its miracle cost when you draw it if it's the first card you drew this turn."),
|
||||
MOBILIZE("Mobilize", KeywordWithAmount.class, false, "When this creature attacks, create {%1$d:tapped and attacking 1/1 red Warrior creature token}. Sacrifice them at the beginning of the next end step."),
|
||||
// technically not a keyword but easier this way
|
||||
MONSTROSITY("Monstrosity", KeywordWithCostAndAmount.class, false, "If this creature isn't monstrous, put {%2$d:+1/+1 counter} on it and it becomes monstrous."),
|
||||
MODULAR("Modular", Modular.class, false, "This creature enters with {%d:+1/+1 counter} on it. When it dies, you may put its +1/+1 counters on target artifact creature."),
|
||||
|
||||
@@ -2,11 +2,13 @@ package forge.game.keyword;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.game.IHasSVars;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.player.Player;
|
||||
@@ -381,4 +383,43 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
|
||||
idx = i;
|
||||
}
|
||||
|
||||
protected IHasSVars getSVarFallback() {
|
||||
if (getStatic() != null) {
|
||||
return getStatic();
|
||||
}
|
||||
return getHostCard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSVar(final String name) {
|
||||
return getSVarFallback().getSVar(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSVar(final String name) {
|
||||
return getSVarFallback().hasSVar(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setSVar(final String name, final String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getSVars() {
|
||||
return getSVarFallback().getSVars();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDirectSVars() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSVars(Map<String, String> newSVars) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSVar(String var) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.game.keyword;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import forge.game.IHasSVars;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
@@ -9,7 +10,7 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
|
||||
public interface KeywordInterface extends Cloneable {
|
||||
public interface KeywordInterface extends Cloneable, IHasSVars {
|
||||
|
||||
Card getHostCard();
|
||||
void setHostCard(final Card host);
|
||||
|
||||
@@ -142,7 +142,7 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
|
||||
private void advanceToNextPhase() {
|
||||
PhaseType oldPhase = phase;
|
||||
boolean isTopsy = playerTurn.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
|
||||
boolean isTopsy = playerTurn.isPhasesReversed();
|
||||
boolean turnEnded = false;
|
||||
|
||||
game.getStack().clearUndoStack(); //can't undo action from previous phase
|
||||
@@ -185,9 +185,8 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
playerTurn.setNumPowerSurgeLands(lands);
|
||||
}
|
||||
|
||||
// Replacement effects
|
||||
final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(playerTurn);
|
||||
repRunParams.put(AbilityKey.Phase, phase.nameForScripts);
|
||||
repRunParams.put(AbilityKey.Phase, phase);
|
||||
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.BeginPhase, repRunParams);
|
||||
if (repres != ReplacementResult.NotReplaced) {
|
||||
// Currently there is no effect to skip entire beginning phase
|
||||
@@ -432,7 +431,7 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
if (!skipped) {
|
||||
// Run triggers if phase isn't being skipped
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(playerTurn);
|
||||
runParams.put(AbilityKey.Phase, phase.nameForScripts);
|
||||
//runParams.put(AbilityKey.Phase, phase.nameForScripts);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Phase, runParams, false);
|
||||
}
|
||||
|
||||
@@ -1165,7 +1164,7 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
return devAdvanceToPhase(targetPhase, null);
|
||||
}
|
||||
public final boolean devAdvanceToPhase(PhaseType targetPhase, Runnable resolver) {
|
||||
boolean isTopsy = playerTurn.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
|
||||
boolean isTopsy = playerTurn.isPhasesReversed();
|
||||
while (phase.isBefore(targetPhase, isTopsy)) {
|
||||
if (checkStateBasedEffects()) {
|
||||
return false;
|
||||
|
||||
@@ -21,9 +21,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.game.Game;
|
||||
@@ -79,54 +77,28 @@ public class Untap extends Phase {
|
||||
doUntap();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* canUntap.
|
||||
* </p>
|
||||
*
|
||||
* @param c
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
public static boolean canUntap(final Card c) {
|
||||
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")
|
||||
|| c.hasKeyword("This card doesn't untap during your next untap step.")
|
||||
|| c.hasKeyword("This card doesn't untap during your next two untap steps.")
|
||||
|| c.hasKeyword("This card doesn't untap.")) {
|
||||
return false;
|
||||
}
|
||||
//exerted need current player turn
|
||||
final Player playerTurn = c.getGame().getPhaseHandler().getPlayerTurn();
|
||||
|
||||
return !c.isExertedBy(playerTurn);
|
||||
}
|
||||
|
||||
public static final Predicate<Card> CANUNTAP = Untap::canUntap;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* doUntap.
|
||||
* </p>
|
||||
*/
|
||||
private void doUntap() {
|
||||
final Player player = game.getPhaseHandler().getPlayerTurn();
|
||||
final Predicate<Card> tappedCanUntap = CardPredicates.TAPPED.and(CANUNTAP);
|
||||
final Player active = game.getPhaseHandler().getPlayerTurn();
|
||||
Map<Player, CardCollection> untapMap = Maps.newHashMap();
|
||||
|
||||
CardCollection list = new CardCollection(player.getCardsIn(ZoneType.Battlefield));
|
||||
CardCollection untapList = new CardCollection(active.getCardsIn(ZoneType.Battlefield));
|
||||
|
||||
CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
|
||||
CardCollection bounceList = CardLists.getKeyword(list, "During your next untap step, as you untap your permanents, return CARDNAME to its owner's hand.");
|
||||
CardCollection bounceList = CardLists.getKeyword(untapList, "During your next untap step, as you untap your permanents, return CARDNAME to its owner's hand.");
|
||||
for (final Card c : bounceList) {
|
||||
Card moved = game.getAction().moveToHand(c, null);
|
||||
triggerList.put(ZoneType.Battlefield, moved.getZone().getZoneType(), moved);
|
||||
}
|
||||
triggerList.triggerChangesZoneAll(game, null);
|
||||
list.removeAll(bounceList);
|
||||
untapList.removeAll(bounceList);
|
||||
|
||||
final Map<String, Integer> restrictUntap = Maps.newHashMap();
|
||||
boolean hasChosen = false;
|
||||
for (KeywordInterface ki : player.getKeywords()) {
|
||||
for (KeywordInterface ki : active.getKeywords()) {
|
||||
String kw = ki.getOriginal();
|
||||
if (kw.startsWith("UntapAdjust")) {
|
||||
String[] parse = kw.split(":");
|
||||
@@ -135,103 +107,79 @@ public class Untap extends Phase {
|
||||
restrictUntap.put(parse[1], Integer.parseInt(parse[2]));
|
||||
}
|
||||
}
|
||||
if (kw.startsWith("OnlyUntapChosen") && !hasChosen) {
|
||||
if (kw.startsWith("OnlyUntapChosen")) {
|
||||
List<String> validTypes = Arrays.asList(kw.split(":")[1].split(","));
|
||||
final String chosen = player.getController().chooseSomeType("Card", new SpellAbility.EmptySa(ApiType.ChooseType, null, player), validTypes);
|
||||
list = CardLists.getType(list, chosen);
|
||||
hasChosen = true;
|
||||
final String chosen = active.getController().chooseSomeType("Card", new SpellAbility.EmptySa(ApiType.ChooseType, null, active), validTypes);
|
||||
untapList = CardLists.getType(untapList, chosen);
|
||||
}
|
||||
}
|
||||
final CardCollection untapList = new CardCollection(list);
|
||||
final String[] restrict = restrictUntap.keySet().toArray(new String[0]);
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!Untap.canUntap(c)) {
|
||||
return false;
|
||||
}
|
||||
return !c.isValid(restrict, player, null, null);
|
||||
});
|
||||
|
||||
for (final Card c : list) {
|
||||
if (optionalUntap(c)) {
|
||||
untapMap.computeIfAbsent(player, i -> new CardCollection()).add(c);
|
||||
untapList = CardLists.filter(untapList, c -> c.canUntap(active, false));
|
||||
|
||||
final String[] restrict = restrictUntap.keySet().toArray(new String[0]);
|
||||
final CardCollection restrictList = CardLists.getValidCards(untapList, restrict, active, null, null);
|
||||
untapList.removeAll(restrictList);
|
||||
CardCollection restrictUntapped = new CardCollection();
|
||||
while (!restrictList.isEmpty()) {
|
||||
Map<String, Integer> remaining = Maps.newHashMap(restrictUntap);
|
||||
for (Entry<String, Integer> entry : remaining.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
restrictList.removeAll(CardLists.getValidCards(restrictList, entry.getKey(), active, null, null));
|
||||
restrictUntap.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
Card chosen = active.getController().chooseSingleEntityForEffect(restrictList, new SpellAbility.EmptySa(ApiType.Untap, null, active),
|
||||
"Select a card to untap\r\n(Selected:" + restrictUntapped + ")\r\n" + "Remaining cards that can untap: " + remaining, null);
|
||||
if (chosen != null) {
|
||||
for (Entry<String, Integer> rest : restrictUntap.entrySet()) {
|
||||
if (chosen.isValid(rest.getKey(), active, null, null)) {
|
||||
restrictUntap.put(rest.getKey(), rest.getValue() - 1);
|
||||
}
|
||||
}
|
||||
untapList.add(chosen);
|
||||
restrictList.remove(chosen);
|
||||
}
|
||||
}
|
||||
|
||||
for (final Card c : untapList) {
|
||||
if (optionalUntap(c, active)) {
|
||||
untapMap.computeIfAbsent(active, i -> new CardCollection()).add(c);
|
||||
}
|
||||
}
|
||||
|
||||
// other players untapping during your untap phase
|
||||
List<Card> cardsWithKW = CardLists.getKeyword(player.getAllOtherPlayers().getCardsIn(ZoneType.Battlefield),
|
||||
List<Card> cardsWithKW = CardLists.getKeyword(active.getAllOtherPlayers().getCardsIn(ZoneType.Battlefield),
|
||||
"CARDNAME untaps during each other player's untap step.");
|
||||
cardsWithKW = CardLists.getNotKeyword(cardsWithKW, "This card doesn't untap.");
|
||||
|
||||
List<Card> cardsWithKW2 = CardLists.getKeyword(player.getOpponents().getCardsIn(ZoneType.Battlefield),
|
||||
List<Card> cardsWithKW2 = CardLists.getKeyword(active.getOpponents().getCardsIn(ZoneType.Battlefield),
|
||||
"CARDNAME untaps during each opponent's untap step.");
|
||||
cardsWithKW2 = CardLists.getNotKeyword(cardsWithKW2, "This card doesn't untap.");
|
||||
|
||||
cardsWithKW.addAll(cardsWithKW2);
|
||||
for (final Card cardWithKW : cardsWithKW) {
|
||||
if (cardWithKW.isExertedBy(player)) {
|
||||
continue;
|
||||
}
|
||||
if (cardWithKW.untap(true)) {
|
||||
untapMap.computeIfAbsent(cardWithKW.getController(),
|
||||
i -> new CardCollection()).add(cardWithKW);
|
||||
}
|
||||
}
|
||||
// end other players untapping during your untap phase
|
||||
|
||||
CardCollection restrictUntapped = new CardCollection();
|
||||
CardCollection cardList = CardLists.filter(untapList, tappedCanUntap);
|
||||
cardList = CardLists.getValidCards(cardList, restrict, player, null, null);
|
||||
|
||||
while (!cardList.isEmpty()) {
|
||||
Map<String, Integer> remaining = Maps.newHashMap(restrictUntap);
|
||||
for (Entry<String, Integer> entry : remaining.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
cardList.removeAll(CardLists.getValidCards(cardList, entry.getKey(), player, null, null));
|
||||
restrictUntap.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
Card chosen = player.getController().chooseSingleEntityForEffect(cardList, new SpellAbility.EmptySa(ApiType.Untap, null, player),
|
||||
"Select a card to untap\r\n(Selected:" + restrictUntapped + ")\r\n" + "Remaining cards that can untap: " + remaining, null);
|
||||
if (chosen != null) {
|
||||
for (Entry<String, Integer> rest : restrictUntap.entrySet()) {
|
||||
if (chosen.isValid(rest.getKey(), player, null, null)) {
|
||||
restrictUntap.put(rest.getKey(), rest.getValue() - 1);
|
||||
}
|
||||
}
|
||||
restrictUntapped.add(chosen);
|
||||
cardList.remove(chosen);
|
||||
}
|
||||
}
|
||||
for (Card c : restrictUntapped) {
|
||||
if (optionalUntap(c)) {
|
||||
untapMap.computeIfAbsent(player, i -> new CardCollection()).add(c);
|
||||
if (cardWithKW.untap(active)) {
|
||||
untapMap.computeIfAbsent(cardWithKW.getController(), i -> new CardCollection()).add(cardWithKW);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temporary keywords
|
||||
// TODO Replace with Static Abilities
|
||||
for (final Card c : player.getCardsIn(ZoneType.Battlefield)) {
|
||||
for (final Card c : active.getCardsIn(ZoneType.Battlefield)) {
|
||||
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next untap step.");
|
||||
if (c.hasKeyword("This card doesn't untap during your next two untap steps.")) {
|
||||
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next two untap steps.");
|
||||
c.addHiddenExtrinsicKeywords(game.getNextTimestamp(), 0, Lists.newArrayList("This card doesn't untap during your next untap step."));
|
||||
}
|
||||
}
|
||||
|
||||
// remove exerted flags from all things in play
|
||||
// even if they are not creatures
|
||||
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
c.removeExertedBy(player);
|
||||
c.removeExertedBy(active);
|
||||
}
|
||||
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Map, untapMap);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.UntapAll, runParams, false);
|
||||
}
|
||||
|
||||
private static boolean optionalUntap(final Card c) {
|
||||
private static boolean optionalUntap(final Card c, Player phase) {
|
||||
boolean untap = true;
|
||||
|
||||
if (c.hasKeyword("You may choose not to untap CARDNAME during your untap step.") && c.isTapped()) {
|
||||
if (c.hasKeyword("You may choose not to untap CARDNAME during your untap step.")) {
|
||||
StringBuilder prompt = new StringBuilder("Untap " + c.toString() + "?");
|
||||
boolean defaultChoice = true;
|
||||
if (c.hasGainControlTarget()) {
|
||||
@@ -246,8 +194,8 @@ public class Untap extends Phase {
|
||||
}
|
||||
untap = c.getController().getController().chooseBinary(new SpellAbility.EmptySa(c, c.getController()), prompt.toString(), BinaryChoiceType.UntapOrLeaveTapped, defaultChoice);
|
||||
}
|
||||
if (untap) {
|
||||
if (!c.untap(true)) untap = false;
|
||||
if (untap && !c.untap(phase)) {
|
||||
untap = false;
|
||||
}
|
||||
return untap;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user