mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 20:28:00 +00:00
Compare commits
375 Commits
forge-2.0.
...
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 | ||
|
|
9054e01273 | ||
|
|
c5fe9b2667 | ||
|
|
0b382d3a9a | ||
|
|
45319ddf73 | ||
|
|
76c725843d | ||
|
|
ee09d6ca6a | ||
|
|
02173ce357 | ||
|
|
105bfdc489 | ||
|
|
9b90d04376 | ||
|
|
6233fc09af | ||
|
|
ec20b59ff3 | ||
|
|
049eb19be4 | ||
|
|
e5e8fa4cdd | ||
|
|
80c11b9f11 | ||
|
|
af055f37dc | ||
|
|
49c0db5280 | ||
|
|
8478835c4d | ||
|
|
804a9d9f20 | ||
|
|
9060dd786f | ||
|
|
802fee2e86 | ||
|
|
8f6fc751dd | ||
|
|
770dbc31cd | ||
|
|
a49ab150f9 | ||
|
|
4bc07e5311 | ||
|
|
8706ba7b68 | ||
|
|
99bc83ae84 | ||
|
|
16e871be7b | ||
|
|
afc4024287 | ||
|
|
0da1681c96 | ||
|
|
da19214754 | ||
|
|
44fca5ee5e | ||
|
|
2f33c24414 | ||
|
|
380f289887 | ||
|
|
a5ab069f5b |
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 }}
|
||||
|
||||
32
.github/workflows/snapshot-both-pc-android.yml
vendored
32
.github/workflows/snapshot-both-pc-android.yml
vendored
@@ -8,6 +8,9 @@ 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: '00 18 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -109,16 +112,21 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: 📂 Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
|
||||
- name: Upload snapshot to GitHub Prerelease
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
server: ftp.cardforge.org
|
||||
username: ${{ secrets.FTP_USERNAME }}
|
||||
password: ${{ secrets.FTP_PASSWORD }}
|
||||
local-dir: izpack/
|
||||
server-dir: downloads/dailysnapshots/
|
||||
state-name: .ftp-deploy-both-sync-state.json
|
||||
exclude: |
|
||||
*.pom
|
||||
*.repositories
|
||||
*.xml
|
||||
name: Daily Snapshot
|
||||
tag: daily-snapshots
|
||||
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);
|
||||
|
||||
@@ -1099,6 +1099,11 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// if AI has no speed, play start your engines on Main1
|
||||
if (ai.noSpeed() && cardState.hasKeyword(Keyword.START_YOUR_ENGINES)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// cast Blitz in main 1 if the creature attacks
|
||||
if (sa.isBlitz() && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, card)) {
|
||||
return true;
|
||||
@@ -2886,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;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,7 +707,7 @@ public class GameAction {
|
||||
// needed for ETB lookahead like Bronzehide Lion
|
||||
stAb.putParam("AffectedZone", "All");
|
||||
SpellAbilityEffect.addForgetOnMovedTrigger(eff, "Battlefield");
|
||||
game.getAction().moveToCommand(eff, cause);
|
||||
eff.getOwner().getZone(ZoneType.Command).add(eff);
|
||||
}
|
||||
|
||||
eff.addRemembered(copied);
|
||||
@@ -724,7 +724,6 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void storeChangesZoneAll(Card c, Zone zoneFrom, Zone zoneTo, Map<AbilityKey, Object> params) {
|
||||
if (params != null && params.containsKey(AbilityKey.InternalTriggerTable)) {
|
||||
((CardZoneTable) params.get(AbilityKey.InternalTriggerTable)).put(zoneFrom != null ? zoneFrom.getZoneType() : null, zoneTo.getZoneType(), c);
|
||||
@@ -934,6 +933,10 @@ public class GameAction {
|
||||
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
|
||||
final Card copied = moveTo(removed, c, cause, params);
|
||||
|
||||
if (c.isImmutable()) {
|
||||
return copied;
|
||||
}
|
||||
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
|
||||
runParams.put(AbilityKey.Cause, cause);
|
||||
if (origin != null) { // is generally null when adding via dev mode
|
||||
@@ -1109,6 +1112,10 @@ public class GameAction {
|
||||
// search for cards with static abilities
|
||||
final FCollection<StaticAbility> staticAbilities = new FCollection<>();
|
||||
final CardCollection staticList = new CardCollection();
|
||||
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies = null;
|
||||
if (preList.isEmpty()) {
|
||||
dependencies = HashBasedTable.create();
|
||||
}
|
||||
|
||||
game.forEachCardInGame(new Visitor<Card>() {
|
||||
@Override
|
||||
@@ -1116,7 +1123,7 @@ public class GameAction {
|
||||
// need to get Card from preList if able
|
||||
final Card co = preList.get(c);
|
||||
for (StaticAbility stAb : co.getStaticAbilities()) {
|
||||
if (stAb.checkMode("Continuous")) {
|
||||
if (stAb.checkMode("Continuous") && stAb.zonesCheck()) {
|
||||
staticAbilities.add(stAb);
|
||||
}
|
||||
}
|
||||
@@ -1143,7 +1150,7 @@ public class GameAction {
|
||||
StaticAbility stAb = staticsForLayer.get(0);
|
||||
// dependency with CDA seems unlikely
|
||||
if (!stAb.isCharacteristicDefining()) {
|
||||
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility);
|
||||
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility, dependencies);
|
||||
}
|
||||
staticsForLayer.remove(stAb);
|
||||
final CardCollectionView previouslyAffected = affectedPerAbility.get(stAb);
|
||||
@@ -1163,7 +1170,7 @@ public class GameAction {
|
||||
if (affectedHere != null) {
|
||||
for (final Card c : affectedHere) {
|
||||
for (final StaticAbility st2 : c.getStaticAbilities()) {
|
||||
if (!staticAbilities.contains(st2)) {
|
||||
if (!staticAbilities.contains(st2) && st2.checkMode("Continuous") && st2.zonesCheck()) {
|
||||
toAdd.add(st2);
|
||||
st2.applyContinuousAbilityBefore(layer, preList);
|
||||
}
|
||||
@@ -1230,6 +1237,8 @@ public class GameAction {
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Always, runParams, false);
|
||||
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Immediate, runParams, false);
|
||||
|
||||
game.getView().setDependencies(dependencies);
|
||||
}
|
||||
|
||||
// Update P/T and type in the view only once after all the cards have been processed, to avoid flickering
|
||||
@@ -1248,7 +1257,8 @@ public class GameAction {
|
||||
game.getTracker().unfreeze();
|
||||
}
|
||||
|
||||
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility) {
|
||||
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility,
|
||||
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
|
||||
if (staticsForLayer.size() == 1) {
|
||||
return staticsForLayer.get(0);
|
||||
}
|
||||
@@ -1262,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);
|
||||
|
||||
@@ -1295,21 +1304,24 @@ public class GameAction {
|
||||
// ...what it applies to...
|
||||
if (!dependency && compareAffected) {
|
||||
CardCollectionView affectedAfterOther = StaticAbilityContinuous.getAffectedCards(stAb, preList);
|
||||
if (!Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator())) {
|
||||
dependency = true;
|
||||
}
|
||||
dependency = !Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator());
|
||||
}
|
||||
// ...or what it does to any of the things it applies to
|
||||
if (!dependency) {
|
||||
List<Object> effectResultsAfterOther = generateStaticAbilityResult(layer, stAb);
|
||||
if (!effectResults.equals(effectResultsAfterOther)) {
|
||||
dependency = true;
|
||||
}
|
||||
dependency = !effectResults.equals(effectResultsAfterOther);
|
||||
}
|
||||
|
||||
if (dependency) {
|
||||
dependencyGraph.addVertex(otherStAb);
|
||||
dependencyGraph.addEdge(stAb, otherStAb);
|
||||
if (dependencies != null) {
|
||||
if (dependencies.contains(stAb, otherStAb)) {
|
||||
dependencies.get(stAb, otherStAb).add(layer);
|
||||
} else {
|
||||
dependencies.put(stAb, otherStAb, EnumSet.of(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// undo changes and check next pair
|
||||
@@ -1342,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()));
|
||||
|
||||
@@ -1458,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);
|
||||
@@ -1522,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;
|
||||
@@ -1537,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);
|
||||
@@ -1645,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.
|
||||
@@ -1791,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
|
||||
@@ -1809,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) {
|
||||
@@ -1831,19 +1875,10 @@ public class GameAction {
|
||||
|
||||
public void checkGameOverCondition() {
|
||||
// award loses as SBE
|
||||
List<Player> losers = null;
|
||||
|
||||
FCollectionView<Player> allPlayers = game.getPlayers();
|
||||
for (Player p : allPlayers) {
|
||||
if (p.checkLoseCondition()) { // this will set appropriate outcomes
|
||||
if (losers == null) {
|
||||
losers = Lists.newArrayListWithCapacity(3);
|
||||
}
|
||||
losers.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
GameEndReason reason = null;
|
||||
List<Player> losers = null;
|
||||
FCollectionView<Player> allPlayers = game.getPlayers();
|
||||
|
||||
// Has anyone won by spelleffect?
|
||||
for (Player p : allPlayers) {
|
||||
if (!p.hasWon()) {
|
||||
@@ -1869,24 +1904,17 @@ public class GameAction {
|
||||
break;
|
||||
}
|
||||
|
||||
// loop through all the non-losing players that can't win
|
||||
// see if all of their opponents are in that "about to lose" collection
|
||||
if (losers != null) {
|
||||
if (reason == null) {
|
||||
for (Player p : allPlayers) {
|
||||
if (losers.contains(p)) {
|
||||
continue;
|
||||
}
|
||||
if (p.cantWin()) {
|
||||
if (losers.containsAll(p.getOpponents())) {
|
||||
// what to do here?!?!?!
|
||||
System.err.println(p.toString() + " is about to win, but can't!");
|
||||
if (p.checkLoseCondition()) { // this will set appropriate outcomes
|
||||
if (losers == null) {
|
||||
losers = Lists.newArrayListWithCapacity(3);
|
||||
}
|
||||
losers.add(p);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// need a separate loop here, otherwise ConcurrentModificationException is raised
|
||||
if (losers != null) {
|
||||
for (Player p : losers) {
|
||||
game.onPlayerLost(p);
|
||||
|
||||
@@ -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 " +
|
||||
|
||||
@@ -23,15 +23,12 @@ package forge.game;
|
||||
public enum GameEndReason {
|
||||
/** The All opponents lost. */
|
||||
AllOpponentsLost,
|
||||
// Noone won
|
||||
/** The Draw. */
|
||||
Draw, // Having little idea how they can reach a draw, so I didn't enumerate
|
||||
// possible reasons here
|
||||
// Special conditions, they force one player to win and thus end the game
|
||||
|
||||
/** The Wins game spell effect. */
|
||||
WinsGameSpellEffect, // ones that could be both hardcoded (felidar) and
|
||||
// scripted ( such as Mayael's Aria )
|
||||
/** Noone won */
|
||||
Draw,
|
||||
|
||||
/** Special conditions, they force one player to win and thus end the game */
|
||||
WinsGameSpellEffect,
|
||||
|
||||
/** Used to end multiplayer games where the all humans have lost or conceded while AIs cannot end match by themselves.*/
|
||||
AllHumansLost,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -304,8 +304,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventCardForetold ev) {
|
||||
String sb = TextUtil.concatWithSpace(ev.activatingPlayer.toString(), "has foretold.");
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -313,6 +312,11 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventDoorChanged ev) {
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void recieve(GameEvent ev) {
|
||||
GameLogEntry le = ev.visit(this);
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package forge.game;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Table;
|
||||
import com.google.common.collect.Table.Cell;
|
||||
|
||||
import forge.LobbyPlayer;
|
||||
import forge.deck.Deck;
|
||||
import forge.game.GameOutcome.AnteResult;
|
||||
@@ -16,6 +20,8 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.PlayerView;
|
||||
import forge.game.player.RegisteredPlayer;
|
||||
import forge.game.spellability.StackItemView;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.trackable.TrackableCollection;
|
||||
import forge.trackable.TrackableObject;
|
||||
@@ -200,15 +206,36 @@ public class GameView extends TrackableObject {
|
||||
public TrackableCollection<CardView> getRevealedCollection() {
|
||||
return get(TrackableProperty.RevealedCardsCollection);
|
||||
}
|
||||
|
||||
public void updateRevealedCards(TrackableCollection<CardView> collection) {
|
||||
set(TrackableProperty.RevealedCardsCollection, collection);
|
||||
}
|
||||
|
||||
public String getDependencies() {
|
||||
return get(TrackableProperty.Dependencies);
|
||||
}
|
||||
public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
|
||||
if (dependencies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StaticAbilityLayer layer = null;
|
||||
for (StaticAbilityLayer sal : StaticAbilityLayer.CONTINUOUS_LAYERS_WITH_DEPENDENCY) {
|
||||
for (Cell<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dep : dependencies.cellSet()) {
|
||||
if (dep.getValue().contains(sal)) {
|
||||
if (layer != sal) {
|
||||
layer = sal;
|
||||
sb.append("Layer " + layer.num).append(": ");
|
||||
}
|
||||
sb.append(dep.getColumnKey().getHostCard().toString()).append(" <- ").append(dep.getRowKey().getHostCard().toString()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
set(TrackableProperty.Dependencies, sb.toString());
|
||||
}
|
||||
|
||||
public CombatView getCombat() {
|
||||
return get(TrackableProperty.CombatView);
|
||||
}
|
||||
|
||||
public void updateCombatView(CombatView combatView) {
|
||||
set(TrackableProperty.CombatView, combatView);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,6 @@ public class StaticEffect {
|
||||
if (layers.contains(StaticAbilityLayer.ABILITIES)) {
|
||||
p.removeChangedKeywords(getTimestamp(), ability.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// modify the affected card
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -538,6 +538,8 @@ public class AbilityUtils {
|
||||
val = handlePaid(card.getEmerged(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("Crewed")) {
|
||||
val = handlePaid(card.getCrewedByThisTurn(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("ChosenCard")) {
|
||||
val = handlePaid(card.getChosenCards(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("Remembered")) {
|
||||
// Add whole Remembered list to handlePaid
|
||||
final CardCollection list = new CardCollection();
|
||||
@@ -1854,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);
|
||||
}
|
||||
@@ -2266,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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.event.GameEventCardPlotted;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.util.Lang;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
|
||||
public class AlterAttributeEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
@@ -48,6 +48,8 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
|
||||
switch (attr.trim()) {
|
||||
case "Plotted":
|
||||
altered = gameCard.setPlotted(activate);
|
||||
|
||||
c.getGame().fireEvent(new GameEventCardPlotted(c, sa.getActivatingPlayer()));
|
||||
break;
|
||||
case "Solve":
|
||||
case "Solved":
|
||||
|
||||
@@ -121,6 +121,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
|
||||
if (perpetual) {
|
||||
Map <String, Object> params = new HashMap<>();
|
||||
params.put("AddKeywords", keywords);
|
||||
params.put("RemoveKeywords", removeKeywords);
|
||||
params.put("RemoveAll", removeAll);
|
||||
params.put("Timestamp", timestamp);
|
||||
params.put("Category", "Keywords");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,9 +425,6 @@ public class DigEffect extends SpellAbilityEffect {
|
||||
if (sa.hasParam("Imprint")) {
|
||||
host.addImprintedCard(c);
|
||||
}
|
||||
if (sa.hasParam("ForgetOtherRemembered")) {
|
||||
host.clearRemembered();
|
||||
}
|
||||
if (remZone1) {
|
||||
host.addRemembered(c);
|
||||
}
|
||||
|
||||
@@ -166,14 +166,6 @@ public class DiscardEffect extends SpellAbilityEffect {
|
||||
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
|
||||
}
|
||||
|
||||
if (mode.equals("NotRemembered")) {
|
||||
if (!p.canDiscardBy(sa, true)) {
|
||||
continue;
|
||||
}
|
||||
toBeDiscarded = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), "Card.IsNotRemembered", p, source, sa);
|
||||
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
|
||||
}
|
||||
|
||||
int numCards = 1;
|
||||
if (sa.hasParam("NumCards")) {
|
||||
numCards = AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
@@ -18,6 +17,9 @@ public class GameWinEffect extends SpellAbilityEffect {
|
||||
for (final Player p : getTargetPlayers(sa)) {
|
||||
p.altWinBySpellEffect(card.getName());
|
||||
}
|
||||
|
||||
// CR 104.1. A game ends immediately when a player wins
|
||||
card.getGame().getAction().checkGameOverCondition();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
continue;
|
||||
}
|
||||
|
||||
final Player chooser;
|
||||
if (sa.hasParam("Chooser")) {
|
||||
chooser = AbilityUtils.getDefinedPlayers(card, sa.getParam("Chooser"), sa).get(0);
|
||||
} else {
|
||||
chooser = p;
|
||||
}
|
||||
|
||||
if (abMana.isComboMana()) {
|
||||
int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(card, sa.getParam("Amount"), sa) : 1;
|
||||
if(amount <= 0)
|
||||
@@ -67,7 +74,7 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
ColorSet fullOptions = colorOptions;
|
||||
// Use specifyManaCombo if possible
|
||||
if (colorsNeeded == null && amount > 1 && !sa.hasParam("TwoEach")) {
|
||||
Map<Byte, Integer> choices = p.getController().specifyManaCombo(sa, colorOptions, amount, differentChoice);
|
||||
Map<Byte, Integer> choices = chooser.getController().specifyManaCombo(sa, colorOptions, amount, differentChoice);
|
||||
for (Map.Entry<Byte, Integer> e : choices.entrySet()) {
|
||||
Byte chosenColor = e.getKey();
|
||||
String choice = MagicColor.toShortString(chosenColor);
|
||||
@@ -94,7 +101,7 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
// just use the first possible color.
|
||||
choice = colorsProduced[differentChoice ? nMana : 0];
|
||||
} else {
|
||||
byte chosenColor = p.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa,
|
||||
byte chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa,
|
||||
differentChoice && (colorsNeeded == null || colorsNeeded.length <= nMana) ? fullOptions : colorOptions);
|
||||
if (chosenColor == 0)
|
||||
throw new RuntimeException("ManaEffect::resolve() /*combo mana*/ - " + p + " color mana choice is empty for " + card.getName());
|
||||
@@ -137,7 +144,7 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
mask |= MagicColor.fromName(colorsNeeded.charAt(nChar));
|
||||
}
|
||||
colorMenu = mask == 0 ? ColorSet.ALL_COLORS : ColorSet.fromMask(mask);
|
||||
byte val = p.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa, colorMenu);
|
||||
byte val = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa, colorMenu);
|
||||
if (0 == val) {
|
||||
throw new RuntimeException("ManaEffect::resolve() /*any mana*/ - " + p + " color mana choice is empty for " + card.getName());
|
||||
}
|
||||
@@ -162,7 +169,7 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
if (cs.isColorless())
|
||||
continue;
|
||||
if (s.isOr2Generic()) { // CR 106.8
|
||||
chosenColor = p.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), card, cs);
|
||||
chosenColor = chooser.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), card, cs);
|
||||
if (chosenColor == MagicColor.COLORLESS) {
|
||||
generic += 2;
|
||||
continue;
|
||||
@@ -171,7 +178,7 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
else if (cs.isMonoColor())
|
||||
chosenColor = s.getColorMask();
|
||||
else /* (cs.isMulticolor()) */ {
|
||||
chosenColor = p.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
|
||||
chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
|
||||
}
|
||||
sb.append(MagicColor.toShortString(chosenColor));
|
||||
sb.append(' ');
|
||||
@@ -219,7 +226,7 @@ public class ManaEffect extends SpellAbilityEffect {
|
||||
if (cs.isMonoColor())
|
||||
sb.append(MagicColor.toShortString(s.getColorMask()));
|
||||
else /* (cs.isMulticolor()) */ {
|
||||
byte chosenColor = p.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
|
||||
byte chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
|
||||
sb.append(MagicColor.toShortString(chosenColor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
@@ -4799,8 +4835,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
p.get("Timestamp"), (long) 0);
|
||||
} else if (category.equals("Keywords")) {
|
||||
boolean removeAll = p.containsKey("RemoveAll") && (boolean) p.get("RemoveAll") == true;
|
||||
addChangedCardKeywords((List<String>) p.get("AddKeywords"), Lists.newArrayList(), removeAll,
|
||||
(long) p.get("Timestamp"), null);
|
||||
addChangedCardKeywords((List<String>) p.get("AddKeywords"), (List<String>) p.get("RemoveKeywords"),
|
||||
removeAll, (long) p.get("Timestamp"), null);
|
||||
} else if (category.equals("Types")) {
|
||||
addChangedCardTypes((CardType) p.get("AddTypes"), (CardType) p.get("RemoveTypes"),
|
||||
false, (Set<RemoveType>) p.get("RemoveXTypes"),
|
||||
@@ -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() {
|
||||
@@ -7007,15 +7063,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
}
|
||||
|
||||
public boolean isInZones(final List<ZoneType> zones) {
|
||||
boolean inZones = false;
|
||||
Zone z = this.getLastKnownZone();
|
||||
for (ZoneType okZone : zones) {
|
||||
if (z.is(okZone)) {
|
||||
inZones = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return z != null && inZones;
|
||||
return z != null && zones.contains(z.getZoneType());
|
||||
}
|
||||
|
||||
public boolean canBeDiscardedBy(SpellAbility sa, final boolean effect) {
|
||||
@@ -7362,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();
|
||||
@@ -7584,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);
|
||||
@@ -7598,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()) {
|
||||
@@ -8134,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;
|
||||
}
|
||||
@@ -8181,6 +8240,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
updateRooms();
|
||||
|
||||
getGame().fireEvent(new GameEventDoorChanged(p, this, stateName, true));
|
||||
|
||||
Map<AbilityKey, Object> unlockParams = AbilityKey.mapFromPlayer(p);
|
||||
unlockParams.put(AbilityKey.Card, this);
|
||||
unlockParams.put(AbilityKey.CardState, getState(stateName));
|
||||
@@ -8205,6 +8266,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
updateRooms();
|
||||
|
||||
getGame().fireEvent(new GameEventDoorChanged(p, this, stateName, false));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8212,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) {
|
||||
|
||||
@@ -651,12 +651,14 @@ public class CardProperty {
|
||||
return false;
|
||||
}
|
||||
} else if (property.startsWith("TopLibrary") || property.startsWith("BottomLibrary")) {
|
||||
CardCollection cards = (CardCollection) card.getOwner().getCardsIn(ZoneType.Library);
|
||||
CardCollectionView cards = card.getOwner().getCardsIn(ZoneType.Library);
|
||||
if (!property.equals("TopLibrary")) {
|
||||
if (property.equals("TopLibraryLand")) cards = CardLists.filter(cards, CardPredicates.LANDS);
|
||||
else if (property.contains("_")) cards = CardLists.getValidCards(cards, property.split("_")[1],
|
||||
if (property.contains("_")) cards = CardLists.getValidCards(cards, property.split("_")[1],
|
||||
sourceController, source, spellAbility);
|
||||
if (property.startsWith("Bottom")) Collections.reverse(cards);
|
||||
if (property.startsWith("Bottom")) {
|
||||
cards = new CardCollection(cards);
|
||||
Collections.reverse((CardCollection) cards);
|
||||
}
|
||||
}
|
||||
if (cards.isEmpty() || !card.equals(cards.get(0))) return false;
|
||||
} else if (property.startsWith("Cloned")) {
|
||||
@@ -1161,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) {
|
||||
@@ -1263,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")) {
|
||||
@@ -1408,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;
|
||||
@@ -1877,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;
|
||||
@@ -2025,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user