Compare commits
1 Commits
reduce-bit
...
forge-1.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc290138c |
143
.github/workflows/maven-publish.yml
vendored
@@ -2,21 +2,10 @@ name: Publish Desktop Forge
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: github.repository_owner == 'Card-Forge'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -25,10 +14,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '11'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
server-id: cardforge-repo
|
server-id: cardforge-repo
|
||||||
@@ -43,132 +32,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config user.email "actions@github.com"
|
git config user.email "actions@github.com"
|
||||||
git config user.name "GitHub Actions"
|
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: |
|
run: |
|
||||||
export DISPLAY=":1"
|
export DISPLAY=":1"
|
||||||
Xvfb :1 -screen 0 800x600x8 &
|
Xvfb :1 -screen 0 800x600x8 &
|
||||||
export _JAVA_OPTIONS="-Xmx2g"
|
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 }}
|
||||||
d=$(date +%m.%d)
|
|
||||||
# build only desktop and only try to move desktop files
|
|
||||||
mvn -U -B clean -P windows-linux install -DskipTests -Dskip.flatten=true -e -T 1C release:clean release:prepare release:perform
|
|
||||||
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
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: 🔧 Install XML tools
|
|
||||||
run: sudo apt-get install -y libxml2-utils
|
|
||||||
|
|
||||||
- name: 🔼 Bump versionCode in root POM
|
|
||||||
id: bump_version
|
|
||||||
run: |
|
|
||||||
cd /home/runner/work/forge/forge/
|
|
||||||
|
|
||||||
current_version=$(xmllint --xpath "//*[local-name()='versionCode']/text()" pom.xml)
|
|
||||||
echo "Current versionCode: $current_version"
|
|
||||||
|
|
||||||
IFS='.' read -r major minor patch <<< "${current_version}"
|
|
||||||
new_patch=$(printf "%02d" $((10#$patch + 1)))
|
|
||||||
new_version="${major}.${minor}.${new_patch}"
|
|
||||||
|
|
||||||
sed -i -E "s|<versionCode>.*</versionCode>|<versionCode>${new_version}</versionCode>|" pom.xml
|
|
||||||
|
|
||||||
echo "version_code=${new_version}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: ♻️ Restore {revision} in child POMs
|
|
||||||
run: |
|
|
||||||
find . -name pom.xml ! -path "./pom.xml" | while read -r pom; do
|
|
||||||
sed -i -E 's|<version>2\.0+\.[0-9]+(-SNAPSHOT)?</version>|<version>${revision}</version>|' "$pom"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: 💾 Commit restored {revision}
|
|
||||||
run: |
|
|
||||||
# Add only pom.xml files
|
|
||||||
find . -name pom.xml -exec git add {} \;
|
|
||||||
|
|
||||||
# Commit if there are changes
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No pom.xml changes to commit."
|
|
||||||
else
|
|
||||||
git commit -m "Restore POM files for preparation of next release" || echo "No changes to commit"
|
|
||||||
git push
|
|
||||||
fi
|
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|||||||
30
.github/workflows/publish-android.yml
vendored
@@ -20,10 +20,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 8
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '8'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
server-id: cardforge-repo
|
server-id: cardforge-repo
|
||||||
@@ -31,20 +31,20 @@ jobs:
|
|||||||
server-password: ${{ secrets.FTP_PASSWORD }}
|
server-password: ${{ secrets.FTP_PASSWORD }}
|
||||||
settings-path: ${{ github.workspace }} # location for the settings.xml file
|
settings-path: ${{ github.workspace }} # location for the settings.xml file
|
||||||
|
|
||||||
- name: Install old maven (3.8.1)
|
- name: Install old maven (3.6.3)
|
||||||
run: |
|
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
|
curl -o apache-maven-3.6.3-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
|
||||||
tar xf apache-maven-3.8.1-bin.tar.gz
|
tar xf apache-maven-3.6.3-bin.tar.gz
|
||||||
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
|
export PATH=$PWD/apache-maven-3.6.3/bin:$PATH
|
||||||
export MAVEN_HOME=$PWD/apache-maven-3.8.1
|
export MAVEN_HOME=$PWD/apache-maven-3.6.3
|
||||||
mvn --version
|
mvn --version
|
||||||
|
|
||||||
- name: Install android SDK
|
- name: Install android SDK
|
||||||
uses: maxim-lobanov/setup-android-tools@v1
|
uses: maxim-lobanov/setup-android-tools@v1
|
||||||
with:
|
with:
|
||||||
packages: |
|
packages: |
|
||||||
platforms;android-35
|
platforms;android-26
|
||||||
build-tools;35.0.0
|
build-tools;30.0.3
|
||||||
|
|
||||||
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
|
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
|
||||||
run: |
|
run: |
|
||||||
@@ -71,11 +71,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Android maven plugin
|
- name: Install Android maven plugin
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
|
||||||
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
|
||||||
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.1.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.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
|
curl -L -o android-maven-plugin-4.6.1.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.pom
|
||||||
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
|
#mvn install:install-file -Dfile=android-maven-plugin-4.6.1.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.1 -Dpackaging=jar
|
||||||
cd -
|
cd -
|
||||||
mvn install -Dmaven.test.skip=true
|
mvn install -Dmaven.test.skip=true
|
||||||
mvn dependency:tree
|
mvn dependency:tree
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
- name: Build/Install/Publish to GitHub Packages Apache Maven
|
- name: Build/Install/Publish to GitHub Packages Apache Maven
|
||||||
run: |
|
run: |
|
||||||
export _JAVA_OPTIONS="-Xmx2g"
|
export _JAVA_OPTIONS="-Xmx2g"
|
||||||
mvn -U -B -P android-release-build,android-release-upload 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 -Dmaven.test.skip=true
|
mvn -U -B -P android-release-build,android-release-sign,android-release-upload install -e -Dsign.keystore=forge.keystore -Dsign.alias=Forge -Dsign.storepass=${{ secrets.SIGN_STORE_PASS }} -Dsign.keypass=${{ secrets.SIGN_STORE_PASS }} -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=30.0.3 -Dmaven.test.skip=true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
|||||||
19
.github/workflows/remove-stale-branches.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
name: Remove stale branches
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *" # Everday at midnight
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
remove-stale-branches:
|
|
||||||
if: github.repository_owner == 'Card-Forge'
|
|
||||||
name: Remove Stale Branches
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: fpicalausa/remove-stale-branches@v2.1.0
|
|
||||||
with:
|
|
||||||
dry-run: false # Check out the console output before setting this to false
|
|
||||||
ignore-unknown-authors: true
|
|
||||||
ignore-branches-with-open-prs: true
|
|
||||||
default-recipient: tehdiplomat
|
|
||||||
132
.github/workflows/snapshot-both-pc-android.yml
vendored
@@ -1,132 +0,0 @@
|
|||||||
name: Create Snapshot Desktop and Android
|
|
||||||
|
|
||||||
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
|
|
||||||
schedule:
|
|
||||||
# * is a special character in YAML so you have to quote this string
|
|
||||||
- cron: '00 18 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: github.repository_owner == 'Card-Forge'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
deployments: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: 'maven'
|
|
||||||
server-id: cardforge-repo
|
|
||||||
server-username: ${{ secrets.FTP_USERNAME }}
|
|
||||||
server-password: ${{ secrets.FTP_PASSWORD }}
|
|
||||||
settings-path: ${{ github.workspace }} # location for the settings.xml file
|
|
||||||
|
|
||||||
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
|
|
||||||
run: command -v Xvfb >/dev/null 2>&1 || { sudo apt update && sudo apt install -y xvfb; }
|
|
||||||
|
|
||||||
- name: Configure Git User
|
|
||||||
run: |
|
|
||||||
git config user.email "actions@github.com"
|
|
||||||
git config user.name "GitHub Actions"
|
|
||||||
|
|
||||||
- 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: Set Up Android tools
|
|
||||||
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"
|
|
||||||
|
|
||||||
- name: Extract Android keystore
|
|
||||||
run: |
|
|
||||||
ls
|
|
||||||
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 -
|
|
||||||
|
|
||||||
- name: Setup tmate session
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
|
|
||||||
|
|
||||||
- name: Install Android maven plugin
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
|
|
||||||
cd -
|
|
||||||
mvn install -Dmaven.test.skip=true
|
|
||||||
mvn dependency:tree
|
|
||||||
|
|
||||||
- name: Build/Install/Publish to GitHub Packages Apache Maven
|
|
||||||
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
|
|
||||||
# rename files and append date
|
|
||||||
for file in *.jar; do
|
|
||||||
bname="${file%.*}"
|
|
||||||
echo "file renamed to ${bname}-${d}.jar"
|
|
||||||
mv "${bname}.jar" "${bname}-${d}.jar"
|
|
||||||
done
|
|
||||||
for file in *.bz2; do
|
|
||||||
# remove .bz2
|
|
||||||
fname="${file%.*}"
|
|
||||||
# remove .tar
|
|
||||||
bname="${fname%.*}"
|
|
||||||
echo "file renamed to ${bname}-${d}.tar.bz2"
|
|
||||||
mv "${fname}.bz2" "${bname}-${d}.tar.bz2"
|
|
||||||
done
|
|
||||||
ls
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Upload snapshot to GitHub Prerelease
|
|
||||||
uses: ncipollo/release-action@v1
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
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 }}
|
|
||||||
77
.github/workflows/snapshots-android.yml
vendored
@@ -8,11 +8,10 @@ on:
|
|||||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
#upload_package:
|
schedule:
|
||||||
# type: boolean
|
# * is a special character in YAML so you have to quote this string
|
||||||
# description: 'Upload the completed Android package'
|
- cron: '00 19 * * *'
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -25,10 +24,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 8
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '8'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
server-id: cardforge-repo
|
server-id: cardforge-repo
|
||||||
@@ -36,17 +35,19 @@ jobs:
|
|||||||
server-password: ${{ secrets.FTP_PASSWORD }}
|
server-password: ${{ secrets.FTP_PASSWORD }}
|
||||||
settings-path: ${{ github.workspace }} # location for the settings.xml file
|
settings-path: ${{ github.workspace }} # location for the settings.xml file
|
||||||
|
|
||||||
- name: Install old maven (3.8.1)
|
- name: Install old maven (3.6.3)
|
||||||
run: |
|
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
|
curl -o apache-maven-3.6.3-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
|
||||||
tar xf apache-maven-3.8.1-bin.tar.gz
|
tar xf apache-maven-3.6.3-bin.tar.gz
|
||||||
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
|
export PATH=$PWD/apache-maven-3.6.3/bin:$PATH
|
||||||
export MAVEN_HOME=$PWD/apache-maven-3.8.1
|
export MAVEN_HOME=$PWD/apache-maven-3.6.3
|
||||||
mvn --version
|
mvn --version
|
||||||
|
|
||||||
- name: Set Up Android tools
|
- name: Set Up Android tools
|
||||||
run: |
|
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"
|
JAVA_HOME=${JAVA_HOME_11_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT "platform-tools"
|
||||||
|
JAVA_HOME=${JAVA_HOME_11_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "platforms;android-26"
|
||||||
|
JAVA_HOME=${JAVA_HOME_11_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;30.0.3"
|
||||||
|
|
||||||
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
|
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
|
||||||
run: |
|
run: |
|
||||||
@@ -73,11 +74,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Android maven plugin
|
- name: Install Android maven plugin
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
|
||||||
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
|
||||||
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.1.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.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
|
curl -L -o android-maven-plugin-4.6.1.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.pom
|
||||||
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
|
#mvn install:install-file -Dfile=android-maven-plugin-4.6.1.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.1 -Dpackaging=jar
|
||||||
cd -
|
cd -
|
||||||
mvn install -Dmaven.test.skip=true
|
mvn install -Dmaven.test.skip=true
|
||||||
mvn dependency:tree
|
mvn dependency:tree
|
||||||
@@ -85,30 +86,40 @@ jobs:
|
|||||||
- name: Build/Install/Publish to GitHub Packages Apache Maven
|
- name: Build/Install/Publish to GitHub Packages Apache Maven
|
||||||
run: |
|
run: |
|
||||||
export _JAVA_OPTIONS="-Xmx2g"
|
export _JAVA_OPTIONS="-Xmx2g"
|
||||||
mvn -U -B -P 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 -Dmaven.test.skip=true
|
d=$(date +%m-%d)
|
||||||
mkdir upload
|
# Replace date in forge-gui-mobile/src/forge/Forge.java
|
||||||
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk upload/
|
sed -i -e "s/-SNAPSHOT/-SNAPSHOT-${d}/g" forge-gui-mobile/src/forge/Forge.java
|
||||||
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip upload/
|
mvn -U -B -P android-release-build,android-release-sign install -e -Dsign.keystore=forge.keystore -Dsign.alias=Forge -Dsign.storepass=${{ secrets.SIGN_STORE_PASS }} -Dsign.keypass=${{ secrets.SIGN_STORE_PASS }} -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=30.0.3 -Dmaven.test.skip=true
|
||||||
mv /home/runner/work/forge/forge/forge-gui-android/target/classes/assets/version.txt upload/
|
mkdir -p forge-gui-android/target/upload
|
||||||
cd upload
|
mv forge-gui-android/target/*-signed-aligned.apk forge-gui-android/target/upload/
|
||||||
|
mv forge-gui-android/target/assets.zip forge-gui-android/target/upload/
|
||||||
|
cd forge-gui-android/target/upload/
|
||||||
|
# Get the first APK file in the folder
|
||||||
ls
|
ls
|
||||||
|
apk_file=$(find . -maxdepth 1 -type f -name '*.apk' -print -quit)
|
||||||
|
|
||||||
|
if [ -n "$apk_file" ]; then
|
||||||
|
version=$(echo "$apk_file" | grep -oP 'forge-android-\K\d+\.\d+\.\d+-SNAPSHOT' | sed 's/-signed-aligned.apk//')
|
||||||
|
echo "APK File: $apk_file"
|
||||||
|
echo "Version: $version"
|
||||||
|
mv *.apk "forge-android-$version-$d-signed-aligned.apk"
|
||||||
|
|
||||||
|
echo "$version-$d" > version.txt
|
||||||
|
else
|
||||||
|
echo "No .apk files found in the specified folder."
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd -
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
- name: 📂 Sync files
|
- name: 📂 Sync files
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
|
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
|
||||||
#if: ${{ inputs.upload_package }}
|
|
||||||
with:
|
with:
|
||||||
server: ftp.cardforge.org
|
server: ftp.cardforge.org
|
||||||
username: ${{ secrets.FTP_USERNAME }}
|
username: ${{ secrets.FTP_USERNAME }}
|
||||||
password: ${{ secrets.FTP_PASSWORD }}
|
password: ${{ secrets.FTP_PASSWORD }}
|
||||||
local-dir: upload/
|
local-dir: forge-gui-android/target/upload/
|
||||||
server-dir: downloads/dailysnapshots/
|
server-dir: downloads/dailysnapshots/
|
||||||
state-name: .ftp-deploy-android-sync-state.json
|
state-name: .ftp-deploy-android-sync-state.json
|
||||||
|
|
||||||
- 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\": \"🔴 Android Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.run_url }}\"}" \
|
|
||||||
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
|
|
||||||
|
|||||||
46
.github/workflows/snapshots-pc.yml
vendored
@@ -8,6 +8,9 @@ on:
|
|||||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
|
schedule:
|
||||||
|
# * is a special character in YAML so you have to quote this string
|
||||||
|
- cron: '30 18 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -20,10 +23,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '11'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
server-id: cardforge-repo
|
server-id: cardforge-repo
|
||||||
@@ -52,27 +55,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Rename before upload
|
- name: Rename before upload
|
||||||
run: |
|
run: |
|
||||||
mkdir izpack
|
mkdir tarball
|
||||||
# move bz2 and jar from work dir to izpack dir
|
# If this works just gotta figure out how to append datetime
|
||||||
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
|
mv /home/runner/.m2/repository/forge/forge-gui-desktop/*/*.bz2 tarball/
|
||||||
# move desktop build.txt to izpack
|
cd tarball
|
||||||
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/build.txt izpack/
|
out="$(basename -s .tar.bz2 *)"
|
||||||
cd izpack
|
d=$(date +%m-%d)
|
||||||
d=$(date +%m.%d)
|
mv "${out}.tar.bz2" "${out}-${d}.tar.bz2"
|
||||||
# rename files and append date
|
|
||||||
for file in *.jar; do
|
|
||||||
bname="${file%.*}"
|
|
||||||
echo "file renamed to ${bname}-${d}.jar"
|
|
||||||
mv "${bname}.jar" "${bname}-${d}.jar"
|
|
||||||
done
|
|
||||||
for file in *.bz2; do
|
|
||||||
# remove .bz2
|
|
||||||
fname="${file%.*}"
|
|
||||||
# remove .tar
|
|
||||||
bname="${fname%.*}"
|
|
||||||
echo "file renamed to ${bname}-${d}.tar.bz2"
|
|
||||||
mv "${fname}.bz2" "${bname}-${d}.tar.bz2"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: 📂 Sync files
|
- name: 📂 Sync files
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
|
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
|
||||||
@@ -80,16 +69,13 @@ jobs:
|
|||||||
server: ftp.cardforge.org
|
server: ftp.cardforge.org
|
||||||
username: ${{ secrets.FTP_USERNAME }}
|
username: ${{ secrets.FTP_USERNAME }}
|
||||||
password: ${{ secrets.FTP_PASSWORD }}
|
password: ${{ secrets.FTP_PASSWORD }}
|
||||||
local-dir: izpack/
|
local-dir: tarball/
|
||||||
server-dir: downloads/dailysnapshots/
|
server-dir: downloads/dailysnapshots/
|
||||||
exclude: |
|
exclude: |
|
||||||
|
*.jar
|
||||||
*.pom
|
*.pom
|
||||||
*.repositories
|
*.repositories
|
||||||
*.xml
|
*.xml
|
||||||
|
|
||||||
- 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\": \"🔴 Desktop Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.run_url }}\"}" \
|
|
||||||
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
|
|
||||||
|
|||||||
60
.github/workflows/test-android-build.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Test Android build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths: [ 'forge-gui-android/**' ]
|
|
||||||
pull_request:
|
|
||||||
paths: [ 'forge-gui-android/**' ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
deployments: write
|
|
||||||
packages: write
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
java: [ '17' ]
|
|
||||||
name: Test with Java ${{ matrix.Java }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: ${{ matrix.java }}
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: '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: Set Up Android tools
|
|
||||||
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"
|
|
||||||
|
|
||||||
- name: Install Android maven plugin
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
|
|
||||||
cd -
|
|
||||||
mvn install -Dmaven.test.skip=true
|
|
||||||
mvn dependency:tree
|
|
||||||
|
|
||||||
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
|
|
||||||
run: command -v Xvfb >/dev/null 2>&1 || { sudo apt update && sudo apt install -y xvfb; }
|
|
||||||
|
|
||||||
- name: Run build in virtual framebuffer
|
|
||||||
run: |
|
|
||||||
export DISPLAY=":1"
|
|
||||||
Xvfb :1 -screen 0 800x600x8 &
|
|
||||||
mvn -U -B -P android-test-build verify -e -T 1C -Dandroid.sdk.path=$ANDROID_SDK_ROOT -Dandroid.buildToolsVersion=35.0.0 -Dmaven.test.skip=true
|
|
||||||
4
.github/workflows/test-build.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
java: [ '17' ]
|
java: [ '8', '11' ]
|
||||||
name: Test with Java ${{ matrix.Java }}
|
name: Test with Java ${{ matrix.Java }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -26,4 +26,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
export DISPLAY=":1"
|
export DISPLAY=":1"
|
||||||
Xvfb :1 -screen 0 800x600x8 &
|
Xvfb :1 -screen 0 800x600x8 &
|
||||||
mvn -U -B clean test
|
mvn -U -B clean -P windows-linux test
|
||||||
|
|||||||
4
.github/workflows/test-maven-settings.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '11'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
cache: 'maven'
|
cache: 'maven'
|
||||||
server-id: cardforge-repo
|
server-id: cardforge-repo
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -12,7 +12,6 @@
|
|||||||
.settings
|
.settings
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
.checkstyle
|
|
||||||
|
|
||||||
# Ignore VS Code config files
|
# Ignore VS Code config files
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@
|
|||||||
|
|
||||||
nbactions.xml
|
nbactions.xml
|
||||||
|
|
||||||
# Ignore flattened pom
|
|
||||||
.flattened-pom.xml
|
|
||||||
|
|
||||||
# Ignore binaries, temp files and test output, everywhere
|
# Ignore binaries, temp files and test output, everywhere
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
|
<!--
|
||||||
|
Derived from: https://stackoverflow.com/a/67002852
|
||||||
|
-->
|
||||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
|
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 http://maven.apache.org/xsd/settings-1.2.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 http://maven.apache.org/xsd/settings-1.2.0.xsd">
|
||||||
|
<mirrors>
|
||||||
|
<mirror>
|
||||||
|
<id>4thline-repo-http-unblocker</id>
|
||||||
|
<mirrorOf>4thline-repo</mirrorOf>
|
||||||
|
<name></name>
|
||||||
|
<url>http://4thline.org/m2</url>
|
||||||
|
</mirror>
|
||||||
|
</mirrors>
|
||||||
|
|
||||||
<servers>
|
<servers>
|
||||||
<server>
|
<server>
|
||||||
<id>cardforge-repo</id>
|
<id>cardforge-repo</id>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
|||||||
|
|
||||||
## Requirements / Tools
|
## Requirements / Tools
|
||||||
|
|
||||||
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||||
- Java JDK 17 or later
|
- Java JDK 8 or later (some IDEs such as Eclipse require JDK11+, whereas the Android build currently only works with JDK8)
|
||||||
- Git
|
- Git
|
||||||
- Git client (optional)
|
- Git client (optional)
|
||||||
- Maven
|
- Maven
|
||||||
@@ -28,6 +28,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
|||||||
|
|
||||||
IntelliJ is the recommended IDE for Forge development. Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
|
IntelliJ is the recommended IDE for Forge development. Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
|
||||||
|
|
||||||
|
|
||||||
## Eclipse
|
## Eclipse
|
||||||
|
|
||||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
||||||
@@ -45,9 +46,9 @@ At this time, Eclipse is not the recommended IDE for Forge development.
|
|||||||
|
|
||||||
- Clone your forked repo to your local machine.
|
- Clone your forked repo to your local machine.
|
||||||
|
|
||||||
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
|
- Make sure the Java SDK is installed -- not just the JRE. Java 8 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 1.8 or later.
|
||||||
|
|
||||||
- Install Eclipse 2021-12 or later for Java. Launch it.
|
- Install Eclipse 2018-12 or later for Java. Launch it.
|
||||||
|
|
||||||
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
|
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
|
||||||
ensure everything is checked > Finish.
|
ensure everything is checked > Finish.
|
||||||
@@ -78,15 +79,16 @@ This is the configuration used for doing mobile development using the Windows /
|
|||||||
|
|
||||||
### Eclipse / Android SDK Integration
|
### Eclipse / Android SDK Integration
|
||||||
|
|
||||||
Google no longer supports Android SDK releases for Eclipse. use IntelliJ.
|
Google no longer supports Android SDK releases for Eclipse. That said, it is still possible to build and debug Android platforms.
|
||||||
|
|
||||||
#### Android SDK
|
#### Android SDK
|
||||||
|
|
||||||
TBD
|
Reference SO for obtaining a specific release: https://stackoverflow.com/questions/27043522/where-can-i-download-an-older-version-of-the-android-sdk
|
||||||
|
|
||||||
##### Windows
|
##### Windows
|
||||||
|
|
||||||
TBD
|
Download the following archived version of the Android SDK: http://dl-ssl.google.com/android/repository/tools_r25.2.3-windows.zip. Install it somewhere on your machine. This is referenced
|
||||||
|
in the following instructions as your 'Android SDK Install' path.
|
||||||
|
|
||||||
##### Linux / Mac OSX
|
##### Linux / Mac OSX
|
||||||
|
|
||||||
@@ -94,40 +96,77 @@ TBD
|
|||||||
|
|
||||||
#### Android Plugin for Eclipse
|
#### Android Plugin for Eclipse
|
||||||
|
|
||||||
TBD
|
Google's last plugin release does not work completely with target's running Android 7.0 or later. Download the ADT-24.2.0-20160729.zip plugin
|
||||||
|
from: https://github.com/khaledev/ADT/releases
|
||||||
|
|
||||||
|
In Eclipse go to: Help > Install New Software... > Add > Name: ADT Update, Click on the "Archive:" button and navigate to the downloaded ADT-24.2.0-20160729.zip file > Add. Install all "Developer Tools". Eclipse
|
||||||
|
should restart and prompt you to run the SDK Manager. Launch it and continue to the next steps below.
|
||||||
|
|
||||||
#### Android Platform
|
#### Android Platform
|
||||||
|
|
||||||
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
|
In Eclipse, if the SDK Manager is not already running, go to Window > Android SDK Manager. Install the following options / versions:
|
||||||
|
|
||||||
- Android SDK Build-tools 35.0.0
|
- Android SDK Build-tools 26.0.1
|
||||||
- Android 15 (API 35) SDK Platform
|
- Android 8.0.0 (API 26) SDK Platform
|
||||||
|
- Google USB Driver (in case your phone is not detected by ADB)
|
||||||
|
|
||||||
|
Note that this will populate additional tools in the Android SDK install path extracted above.
|
||||||
|
|
||||||
#### Proguard update
|
#### Proguard update
|
||||||
|
|
||||||
Standalone Proguard 7.6.0 is included with the project (proguard.jar) under forge-gui-android > tools and supports up to Java 23 (latest android uses Java 17).
|
The Proguard included with the Android SDK Build-tools is outdated and does not work with Java 1.8. Download Proguard 6.0.3 or later (last tested with 7.0.1) from https://github.com/Guardsquare/proguard
|
||||||
|
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard-4.7/.
|
||||||
|
|
||||||
|
- Extract your Proguard version to the Android SDK install path under tools/. You will need to either rename the dir proguard-<your-version> to proguard/ or, if your filesystem supports it, use a symbolic link (the later is highly recommended), such as `ln -s proguard proguard-<your-version>`.
|
||||||
|
|
||||||
#### Android Build
|
#### Android Build
|
||||||
|
|
||||||
TBD
|
The Eclipse plug-ins do NOT support building things for Android. They do however allow you to use the debugger so you can still set breakpoints and trace
|
||||||
|
things out. The steps below show how to generate a debug Android build.
|
||||||
|
|
||||||
|
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
||||||
|
- On the Main tab, set Goals: clean install
|
||||||
|
|
||||||
|
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
||||||
|
|
||||||
|
3) Right-click on the forge-gui-android project. Run as.. > Maven build...
|
||||||
|
|
||||||
|
- On the Main tab, set Goals: install, Profiles: android-debug
|
||||||
|
- On the Environment tab, you may need to define the variable ANDROID_HOME with the value containing the path to your Android SDK installation. For example, Variable: ANDROID_HOME, Value: Your Android SDK install path here.
|
||||||
|
|
||||||
|
4) Run the forge-gui-android Maven build. This may take a few minutes. If everything worked, you should see "BUILD SUCCESS" in the Console View.
|
||||||
|
|
||||||
|
Assuming you got this far, you should have an Android forge-android-[version].apk in the forge-gui-android/target path.
|
||||||
|
|
||||||
#### Android Deploy
|
#### Android Deploy
|
||||||
|
|
||||||
TBD
|
You'll need to have the Android SDK install path platform-tools/ path in your command search path to easily deploy builds.
|
||||||
|
|
||||||
|
- Open a command prompt. Navigate to the forge-gui-android/target/ path.
|
||||||
|
|
||||||
|
- Connect your Android device to your dev machine.
|
||||||
|
|
||||||
|
- Ensure the device is visible using `adb devices`
|
||||||
|
|
||||||
|
- Remove the old Forge install if present: `adb uninstall forge.app`
|
||||||
|
|
||||||
|
- Install the new apk: `adb install forge-android-[version].apk`
|
||||||
|
|
||||||
#### Android Debugging
|
#### Android Debugging
|
||||||
|
|
||||||
TBD
|
Assuming the apk is installed, launch it from the device.
|
||||||
|
|
||||||
|
In Eclipse, launch the DDMS. Window > Perspective > Open Perspective > Other... > DDMS. You should see the forge app in the list. Highlight the app, click on the green debug button and a
|
||||||
|
green debug button should appear next to the app's name. You can now set breakpoints and step through the source code.
|
||||||
|
|
||||||
### Windows / Linux SNAPSHOT build
|
### Windows / Linux SNAPSHOT build
|
||||||
|
|
||||||
SNAPSHOT builds can be built via the Maven integration in Eclipse.
|
SNAPSHOT builds can be built via the Maven integration in Eclipse.
|
||||||
|
|
||||||
1. Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
||||||
|
|
||||||
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
|
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
|
||||||
|
|
||||||
2. Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
||||||
|
|
||||||
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
|
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
|
||||||
|
|
||||||
@@ -158,19 +197,13 @@ The platform-specific projects are:
|
|||||||
|
|
||||||
#### forge-ai
|
#### forge-ai
|
||||||
|
|
||||||
The forge-ai project contains the computer opponent logic for gameplay. It includes decision-making algorithms for specific abilities, cards and turn phases.
|
|
||||||
|
|
||||||
#### forge-core
|
#### forge-core
|
||||||
|
|
||||||
The forge-core project contains the core game engine, card mechanics, rules engine, and fundamental game logic. It includes the implementation of Magic: The Gathering rules, card interactions, and the game state management system.
|
|
||||||
|
|
||||||
#### forge-game
|
#### forge-game
|
||||||
|
|
||||||
The forge-game project handles the game session management, player interactions, and game flow control. It includes implementations for multiplayer support, game modes, matchmaking, and game state persistence. This module bridges the core game engine with the user interface and networking components.
|
|
||||||
|
|
||||||
#### forge-gui
|
#### forge-gui
|
||||||
|
|
||||||
The forge-gui project contains the user interface components and rendering logic for the game. It includes the main game window, card displays, player interactions, and the scripting resource definitions in the res/ path.
|
The forge-gui project includes the scripting resource definitions in the res/ path.
|
||||||
|
|
||||||
#### forge-gui-android
|
#### forge-gui-android
|
||||||
|
|
||||||
|
|||||||
113
README.md
@@ -1,105 +1,50 @@
|
|||||||
# ⚔️ Forge: The Magic: The Gathering Rules Engine
|
# Forge
|
||||||
|
|
||||||
Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
Join the [Discord](https://discord.gg/HcPJNyD66a)
|
||||||
|
|
||||||
[](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml)
|
[](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml)
|
||||||
|
|
||||||
---
|
## Introduction
|
||||||
|
|
||||||
## ✨ Introduction
|
Forge is a "Rules Engine" for the game Magic: the Gathering.
|
||||||
|
Forge is not related in any way with Wizards of the Coast.
|
||||||
|
Forge is open source software released under the GNU Public License.
|
||||||
|
Forge is developed by a community of programmers who love trading card games.
|
||||||
|
|
||||||
**Forge** is a dynamic and open-source **Rules Engine** tailored for **Magic: The Gathering** enthusiasts. Developed by a community of passionate programmers, Forge allows players to explore the rich universe of MTG through a flexible, engaging platform.
|
Forge is a cross-platform application and can be run on Windows, Mac, Linux and Android. It is written in Java. The engine is written in Java. The engine is designed to be extensible, so any interested programmer can join and help add new features and cards to the game. Any tech savvy user could read out card scripting system to create cards to be used inside Forge.
|
||||||
|
The engine allows you to play in a handful of different single player environments or online against other players.
|
||||||
|
|
||||||
**Note:** Forge operates independently and is not affiliated with Wizards of the Coast.
|
|
||||||
|
|
||||||
---
|
## Installation
|
||||||
|
|
||||||
## 🌟 Key Features
|
For a more in depth User Guide, please visit the [User Guide](https://github.com/Card-Forge/forge/wiki/User-Guide)
|
||||||
|
|
||||||
- **🌐 Cross-Platform Support:** Play on **Windows, Mac, Linux,** and **Android**.
|
For Desktop users, download the [Latest Releases](https://github.com/Card-Forge/forge/releases/latest) which are typically based around Set releases.
|
||||||
- **🔧 Extensible Architecture:** Built in **Java**, Forge encourages developers to contribute by adding features and cards.
|
Or download the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/) the file that starts with "forge-gui-desktop".
|
||||||
- **🎮 Versatile Gameplay:** Dive into single-player modes or challenge opponents online!
|
This file is tarball, and may need to be extracted twice depending on which program is being used to extract it.
|
||||||
|
We recommend extracting to a new folder rather than on top of an existing installation.
|
||||||
|
**For users who have played Forge before all of your user data is stored separately so you don't have to worry about losing it on upgrade.**
|
||||||
|
|
||||||
---
|
Java 8 or later is required to run Forge. Please make sure is the right version is installed in your enviroment. Check the user guide for more info.
|
||||||
|
|
||||||
## 🛠️ Installation Guide
|
For Android users, download the APK file from [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/) to your device.
|
||||||
|
On first run, Forge will download all needed data.
|
||||||
|
|
||||||
### 📥 Desktop Installation
|
## Modes of Play
|
||||||
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://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
|
Forge has a variety of ways to play the game. The most popular way is our Adventure mode, which is a single player campaign that allows you to play against a variety of AI opponents.
|
||||||
- _(Note: **Android 11** is the minimum requirements with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
|
You walk around an overworld map, and can challenge opponents to games of Magic. As you play, you'll collect more cards and items to improve your abilities.
|
||||||
- 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.
|
|
||||||
|
|
||||||
---
|
Check the [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide) for more info.
|
||||||
|
|
||||||
## 🎮 Modes of Play
|

|
||||||
|
|
||||||
Forge offers various exciting gameplay options:
|
|
||||||
|
|
||||||
### 🌍 Adventure Mode
|
Forge has several Quest modes, which is similar but without the overworld map.
|
||||||
Embark on a thrilling single-player journey where you can:
|
|
||||||
- Explore an overworld map.
|
|
||||||
- Challenge diverse AI opponents.
|
|
||||||
- Collect cards and items to boost your abilities.
|
|
||||||
|
|
||||||
<img width="1282" height="752" alt="Shandalar World" src="https://github.com/user-attachments/assets/9af31471-d688-442f-9418-9807d8635b72" />
|
You can also play against the AI in a variety of formats, such as Sealed, Draft, Commander and Cube.
|
||||||
|
|
||||||
### 🔍 Quest Modes
|
## Questions
|
||||||
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
|
|
||||||
|
|
||||||
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" />
|
If you have any questions, please join the Discord channel. Read the #rules and the frequently-asked-questions.
|
||||||
|
If your question is not answered there, feel free to ask in the #help channel.
|
||||||
### 🤖 AI Formats
|
|
||||||
Test your skills against AI in multiple formats:
|
|
||||||
- **Sealed**
|
|
||||||
- **Draft**
|
|
||||||
- **Commander**
|
|
||||||
- **Cube**
|
|
||||||
|
|
||||||
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
|
|
||||||
|
|
||||||
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💬 Support & Community
|
|
||||||
|
|
||||||
Need help? Join our vibrant Discord community!
|
|
||||||
- 📜 Read the **#rules** and explore the **FAQ**.
|
|
||||||
- ❓ Ask your questions in the **#help** channel for assistance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Contributing to Forge
|
|
||||||
|
|
||||||
We love community contributions! Interested in helping? Check out our [Contributing Guidelines](CONTRIBUTING.md) for details on how to get started.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ℹ️ About Forge
|
|
||||||
|
|
||||||
Forge aims to deliver an immersive and customizable Magic: The Gathering experience for fans around the world.
|
|
||||||
|
|
||||||
### 📊 Repository Statistics
|
|
||||||
|
|
||||||
| Metric | Count |
|
|
||||||
|----------------|-------------------------------------------------------------|
|
|
||||||
| **⭐ Stars:** | [](https://github.com/Card-Forge/forge/stargazers) |
|
|
||||||
| **🍴 Forks:** | [](https://github.com/Card-Forge/forge/network) |
|
|
||||||
| **👥 Contributors:** | [](https://github.com/Card-Forge/forge/graphs/contributors) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**📄 License:** [GPL-3.0](LICENSE)
|
|
||||||
<div align="center" style="display: flex; align-items: center; justify-content: center;">
|
|
||||||
<div style="margin-left: auto;">
|
|
||||||
<a href="#top">
|
|
||||||
<img src="https://img.shields.io/badge/Back%20to%20Top-000000?style=for-the-badge&logo=github&logoColor=white" alt="Back to Top">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
<?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">
|
|
||||||
<parent>
|
|
||||||
<artifactId>forge</artifactId>
|
|
||||||
<groupId>forge</groupId>
|
|
||||||
<version>${revision}</version>
|
|
||||||
</parent>
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<artifactId>adventure-editor</artifactId>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
<name>Adventure Editor</name>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<sourceDirectory>src/main/java</sourceDirectory>
|
|
||||||
<resources>
|
|
||||||
<resource>
|
|
||||||
<directory>${project.basedir}</directory>
|
|
||||||
<includes>
|
|
||||||
<include>**/gear.gif</include>
|
|
||||||
</includes>
|
|
||||||
</resource>
|
|
||||||
</resources>
|
|
||||||
<finalName>adventure-editor</finalName>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<source>17</source>
|
|
||||||
<target>17</target>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>com.google.code.maven-replacer-plugin</groupId>
|
|
||||||
<artifactId>replacer</artifactId>
|
|
||||||
<version>1.5.3</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>replace</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
<configuration>
|
|
||||||
<basedir>${basedir}/${configSourceDirectory}</basedir>
|
|
||||||
<filesToInclude>adventure-editor.sh, adventure-editor.command, adventure-editor.cmd</filesToInclude>
|
|
||||||
<outputBasedir>${project.build.directory}</outputBasedir>
|
|
||||||
<outputDir>.</outputDir>
|
|
||||||
<regex>false</regex>
|
|
||||||
<replacements>
|
|
||||||
<replacement>
|
|
||||||
<token>$project.build.finalName$</token>
|
|
||||||
<value>${project.build.finalName}-jar-with-dependencies.jar</value>
|
|
||||||
</replacement>
|
|
||||||
</replacements>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
|
|
||||||
<artifactId>maven-assembly-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<attach>false</attach>
|
|
||||||
<descriptorRefs>
|
|
||||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
|
||||||
</descriptorRefs>
|
|
||||||
<archive>
|
|
||||||
<manifest>
|
|
||||||
<mainClass>forge.adventure.Main</mainClass>
|
|
||||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
|
||||||
</manifest>
|
|
||||||
<manifestEntries>
|
|
||||||
<SplashScreen-Image>splash/gear.gif</SplashScreen-Image>
|
|
||||||
</manifestEntries>
|
|
||||||
</archive>
|
|
||||||
</configuration>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>make-assembly</id>
|
|
||||||
<!-- this is used for inheritance merges -->
|
|
||||||
<phase>package</phase>
|
|
||||||
<!-- bind to the packaging phase -->
|
|
||||||
<goals>
|
|
||||||
<goal>single</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>forge</groupId>
|
|
||||||
<artifactId>forge-gui</artifactId>
|
|
||||||
<version>${project.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>forge</groupId>
|
|
||||||
<artifactId>forge-gui-mobile</artifactId>
|
|
||||||
<version>${project.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains</groupId>
|
|
||||||
<artifactId>annotations</artifactId>
|
|
||||||
<version>26.0.1</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
</project>
|
|
||||||
|
Before Width: | Height: | Size: 414 KiB |
@@ -1,24 +0,0 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
java -version 1>nul 2>nul || (
|
|
||||||
echo no java installed
|
|
||||||
popd
|
|
||||||
exit /b 2
|
|
||||||
)
|
|
||||||
for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j"
|
|
||||||
|
|
||||||
if %jver% LEQ 16 (
|
|
||||||
echo unsupported java
|
|
||||||
popd
|
|
||||||
exit /b 2
|
|
||||||
)
|
|
||||||
|
|
||||||
if %jver% GEQ 17 (
|
|
||||||
java -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
|
|
||||||
popd
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
popd
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package forge.adventure.editor;
|
|
||||||
|
|
||||||
import forge.adventure.util.Config;
|
|
||||||
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JDialog;
|
|
||||||
import javax.swing.JFrame;
|
|
||||||
import javax.swing.JOptionPane;
|
|
||||||
import javax.swing.JTabbedPane;
|
|
||||||
import javax.swing.JToolBar;
|
|
||||||
import javax.swing.UIManager;
|
|
||||||
import java.awt.BorderLayout;
|
|
||||||
import java.awt.Desktop;
|
|
||||||
import java.awt.EventQueue;
|
|
||||||
import java.awt.event.WindowAdapter;
|
|
||||||
import java.awt.event.WindowEvent;
|
|
||||||
import java.io.File;
|
|
||||||
import java.security.CodeSource;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Editor class to edit configuration, maybe moved or removed
|
|
||||||
*/
|
|
||||||
public class EditorMainWindow extends JFrame {
|
|
||||||
public final static WorldEditor worldEditor = new WorldEditor();
|
|
||||||
JTabbedPane tabs = new JTabbedPane();
|
|
||||||
|
|
||||||
public EditorMainWindow(Config config) {
|
|
||||||
UIManager.LookAndFeelInfo[] var1 = UIManager.getInstalledLookAndFeels();
|
|
||||||
|
|
||||||
for (UIManager.LookAndFeelInfo info : var1) {
|
|
||||||
if ("Nimbus".equals(info.getName())) {
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel(info.getClassName());
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
|
||||||
this.addWindowListener(new WindowAdapter() {
|
|
||||||
@Override
|
|
||||||
public void windowClosing(WindowEvent e) {
|
|
||||||
setVisible(false);
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
BorderLayout layout = new BorderLayout();
|
|
||||||
JToolBar toolBar = new JToolBar("toolbar");
|
|
||||||
JButton newButton = new JButton("GDX Particle Editor Tool");
|
|
||||||
newButton.addActionListener(e -> EventQueue.invokeLater(() -> {
|
|
||||||
newButton.setEnabled(false);
|
|
||||||
try {
|
|
||||||
CodeSource codeSource = EditorMainWindow.class.getProtectionDomain().getCodeSource();
|
|
||||||
File jarFile = new File(codeSource.getLocation().toURI().getPath());
|
|
||||||
String jarDir = jarFile.getParentFile().getPath();
|
|
||||||
Desktop.getDesktop().open(new File(jarDir + "/gdx-particle-editor.jar"));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
new ErrorDialog("Error", ex.getMessage());
|
|
||||||
newButton.setEnabled(true);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
JButton quit = new JButton("Quit");
|
|
||||||
quit.addActionListener(e -> System.exit(0));
|
|
||||||
toolBar.add(newButton);
|
|
||||||
toolBar.add(quit);
|
|
||||||
setLayout(layout);
|
|
||||||
toolBar.setFloatable(false);
|
|
||||||
add(toolBar, BorderLayout.NORTH);
|
|
||||||
add(tabs, BorderLayout.CENTER);
|
|
||||||
tabs.addTab("World", worldEditor);
|
|
||||||
tabs.addTab("POI", new PointOfInterestEditor());
|
|
||||||
tabs.addTab("Items", new ItemsEditor());
|
|
||||||
tabs.addTab("Enemies", new EnemyEditor());
|
|
||||||
tabs.addTab("Quests", new QuestEditor());
|
|
||||||
setSize(config.getSettingData().width, config.getSettingData().height);
|
|
||||||
setLocationRelativeTo(null);
|
|
||||||
setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ErrorDialog {
|
|
||||||
public ErrorDialog(String title, String message) {
|
|
||||||
List<Object> options = new ArrayList<>();
|
|
||||||
JButton ok = new JButton("OK");
|
|
||||||
options.add(ok);
|
|
||||||
JOptionPane pane = new JOptionPane(message, JOptionPane.PLAIN_MESSAGE, JOptionPane.DEFAULT_OPTION, null, options.toArray());
|
|
||||||
JDialog dlg = pane.createDialog(JOptionPane.getRootFrame(), title);
|
|
||||||
ok.addActionListener(e -> {
|
|
||||||
dlg.setVisible(false);
|
|
||||||
System.exit(0);
|
|
||||||
});
|
|
||||||
dlg.setResizable(false);
|
|
||||||
dlg.setVisible(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
forge-adventure/fallback_skin/adv_bg_splash.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
forge-adventure/fallback_skin/adv_bg_texture.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
forge-adventure/fallback_skin/bg_splash.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
forge-adventure/fallback_skin/bg_texture.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
forge-adventure/fallback_skin/font1.ttf
Normal file
BIN
forge-adventure/fallback_skin/title_bg_lq.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
forge-adventure/fallback_skin/title_bg_lq_portrait.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
forge-adventure/fallback_skin/transition.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
forge-adventure/libs/gdx-backend-lwjgl-natives.jar
Normal file
BIN
forge-adventure/libs/gdx-backend-lwjgl-sources.jar
Normal file
BIN
forge-adventure/libs/gdx-backend-lwjgl.jar
Normal file
BIN
forge-adventure/libs/gdx-freetype-natives.jar
Normal file
BIN
forge-adventure/libs/gdx-natives.jar
Normal file
309
forge-adventure/pom.xml
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<?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">
|
||||||
|
<parent>
|
||||||
|
<artifactId>forge</artifactId>
|
||||||
|
<groupId>forge</groupId>
|
||||||
|
<version>1.6.64</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>forge-adventure</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>Forge Adventure</name>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>4thline-repo</id>
|
||||||
|
<url>http://4thline.org/m2</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<build>
|
||||||
|
<sourceDirectory>src/main/java</sourceDirectory>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>${project.basedir}</directory>
|
||||||
|
<includes>
|
||||||
|
<include>**/*.vert</include>
|
||||||
|
<include>**/*.frag</include>
|
||||||
|
<include>**/title_bg_lq.png</include>
|
||||||
|
<include>**/title_bg_lq_portrait.png</include>
|
||||||
|
<include>**/transition.png</include>
|
||||||
|
<include>**/adv_bg_texture.jpg</include>
|
||||||
|
<include>**/adv_bg_splash.png</include>
|
||||||
|
<include>**/bg_splash.png</include>
|
||||||
|
<include>**/bg_texture.jpg</include>
|
||||||
|
<include>**/font1.ttf</include>
|
||||||
|
</includes>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
||||||
|
<artifactId>launch4j-maven-plugin</artifactId>
|
||||||
|
<version>1.7.25</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>l4j-adv</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>launch4j</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<headerType>gui</headerType>
|
||||||
|
<outfile>${project.build.directory}/forge-adventure-editor-java8.exe</outfile>
|
||||||
|
<jar>${project.build.finalName}-jar-with-dependencies.jar</jar>
|
||||||
|
<dontWrapJar>true</dontWrapJar>
|
||||||
|
<errTitle>forge</errTitle>
|
||||||
|
<icon>src/main/config/forge-adventure-editor.ico</icon>
|
||||||
|
<classPath>
|
||||||
|
<mainClass>forge.adventure.Main</mainClass>
|
||||||
|
<addDependencies>false</addDependencies>
|
||||||
|
<preCp>anything</preCp>
|
||||||
|
</classPath>
|
||||||
|
<jre>
|
||||||
|
<minVersion>1.8.0</minVersion>
|
||||||
|
<maxHeapSize>4096</maxHeapSize>
|
||||||
|
<opts>
|
||||||
|
<opt>-Dfile.encoding=UTF-8</opt>
|
||||||
|
</opts>
|
||||||
|
</jre>
|
||||||
|
<versionInfo>
|
||||||
|
<fileVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</fileVersion>
|
||||||
|
<txtFileVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</txtFileVersion>
|
||||||
|
<fileDescription>Forge</fileDescription>
|
||||||
|
<copyright>Forge</copyright>
|
||||||
|
<productVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</productVersion>
|
||||||
|
<txtProductVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</txtProductVersion>
|
||||||
|
<productName>forge-adventure-editor</productName>
|
||||||
|
<internalName>forge-adventure-editor</internalName>
|
||||||
|
<originalFilename>forge-adventure-editor-java8.exe</originalFilename>
|
||||||
|
</versionInfo>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<!--extra-->
|
||||||
|
<execution>
|
||||||
|
<id>l4j-adv2</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>launch4j</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<headerType>gui</headerType>
|
||||||
|
<outfile>${project.build.directory}/forge-adventure-editor.exe</outfile>
|
||||||
|
<jar>${project.build.finalName}-jar-with-dependencies.jar</jar>
|
||||||
|
<dontWrapJar>true</dontWrapJar>
|
||||||
|
<errTitle>forge</errTitle>
|
||||||
|
<downloadUrl>https://www.oracle.com/java/technologies/downloads/</downloadUrl>
|
||||||
|
<icon>src/main/config/forge-adventure-editor.ico</icon>
|
||||||
|
<classPath>
|
||||||
|
<mainClass>forge.adventure.Main</mainClass>
|
||||||
|
<addDependencies>false</addDependencies>
|
||||||
|
<preCp>anything</preCp>
|
||||||
|
</classPath>
|
||||||
|
<jre>
|
||||||
|
<minVersion>11.0.1</minVersion>
|
||||||
|
<jdkPreference>jdkOnly</jdkPreference>
|
||||||
|
<maxHeapSize>4096</maxHeapSize>
|
||||||
|
<opts>
|
||||||
|
<opt>-Dfile.encoding=UTF-8</opt>
|
||||||
|
<opt>--add-opens java.base/java.lang=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/java.math=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/jdk.internal.misc=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/java.nio=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens=java.base/sun.nio.ch=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/java.util=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/java.lang.reflect=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/java.text=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/java.awt=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/java.awt.font=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/java.awt.image=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/java.awt.color=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/sun.awt.image=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/javax.swing=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/javax.swing.border=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/javax.swing.event=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/sun.swing=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.desktop/java.beans=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/java.util.concurrent=ALL-UNNAMED</opt>
|
||||||
|
<opt>--add-opens java.base/java.net=ALL-UNNAMED</opt>
|
||||||
|
<opt>-Dio.netty.tryReflectionSetAccessible=true</opt>
|
||||||
|
</opts>
|
||||||
|
</jre>
|
||||||
|
<versionInfo>
|
||||||
|
<fileVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</fileVersion>
|
||||||
|
<txtFileVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</txtFileVersion>
|
||||||
|
<fileDescription>Forge</fileDescription>
|
||||||
|
<copyright>Forge</copyright>
|
||||||
|
<productVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</productVersion>
|
||||||
|
<txtProductVersion>
|
||||||
|
1.0.0.0
|
||||||
|
</txtProductVersion>
|
||||||
|
<productName>forge-adventure-editor</productName>
|
||||||
|
<internalName>forge-adventure-editor</internalName>
|
||||||
|
<originalFilename>forge-adventure-editor.exe</originalFilename>
|
||||||
|
</versionInfo>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<!--extra-->
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.google.code.maven-replacer-plugin</groupId>
|
||||||
|
<artifactId>replacer</artifactId>
|
||||||
|
<version>1.5.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>replace</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<basedir>${basedir}/${configSourceDirectory}</basedir>
|
||||||
|
<filesToInclude>forge-adventure-editor.sh, forge-adventure-editor-mac.sh, forge-adventure-editor.command, forge-adventure-editor.cmd</filesToInclude>
|
||||||
|
<outputBasedir>${project.build.directory}</outputBasedir>
|
||||||
|
<outputDir>.</outputDir>
|
||||||
|
<regex>false</regex>
|
||||||
|
<replacements>
|
||||||
|
<replacement>
|
||||||
|
<token>$project.build.finalName$</token>
|
||||||
|
<value>${project.build.finalName}-jar-with-dependencies.jar</value>
|
||||||
|
</replacement>
|
||||||
|
</replacements>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<attach>false</attach>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>forge.adventure.Main</mainClass>
|
||||||
|
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>make-assembly</id>
|
||||||
|
<!-- this is used for inheritance merges -->
|
||||||
|
<phase>package</phase>
|
||||||
|
<!-- bind to the packaging phase -->
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.badlogicgames.gdx</groupId>
|
||||||
|
<artifactId>gdx</artifactId>
|
||||||
|
<version>1.12.1</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.badlogicgames.gdx</groupId>
|
||||||
|
<artifactId>gdx-platform</artifactId>
|
||||||
|
<version>1.12.1</version>
|
||||||
|
<classifier>natives-desktop</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.badlogicgames.gdx</groupId>
|
||||||
|
<artifactId>gdx-freetype</artifactId>
|
||||||
|
<version>1.12.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.badlogicgames.gdx</groupId>
|
||||||
|
<artifactId>gdx-backend-lwjgl</artifactId>
|
||||||
|
<version>1.12.1</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.badlogicgames.gdx</groupId>
|
||||||
|
<artifactId>gdx-tools</artifactId>
|
||||||
|
<version>1.12.1</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.badlogicgames.gdx</groupId>
|
||||||
|
<artifactId>gdx-freetype-platform</artifactId>
|
||||||
|
<version>1.12.1</version>
|
||||||
|
<classifier>natives-desktop</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>forge</groupId>
|
||||||
|
<artifactId>forge-core</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>forge</groupId>
|
||||||
|
<artifactId>forge-gui</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>forge</groupId>
|
||||||
|
<artifactId>forge-gui-mobile</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains</groupId>
|
||||||
|
<artifactId>annotations</artifactId>
|
||||||
|
<version>22.0.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.badlogicgames.gdx-controllers</groupId>
|
||||||
|
<artifactId>gdx-controllers-desktop</artifactId>
|
||||||
|
<version>2.2.3</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.badlogicgames.gdx</groupId>
|
||||||
|
<artifactId>gdx</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
||||||
16
forge-adventure/shaders/grayscale.frag
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
varying vec4 v_color;
|
||||||
|
varying vec2 v_texCoords;
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform float u_grayness;
|
||||||
|
uniform float u_bias;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 c = v_color * texture2D(u_texture, v_texCoords);
|
||||||
|
float grey = dot( c.rgb, vec3(0.22, 0.707, 0.071) );
|
||||||
|
vec3 blendedColor = mix(c.rgb, vec3(grey), u_grayness);
|
||||||
|
gl_FragColor = mix(vec4(0.0, 0.0, 0.0, 1.0), vec4(blendedColor.rgb, c.a), u_bias);
|
||||||
|
}
|
||||||
14
forge-adventure/shaders/grayscale.vert
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
attribute vec4 a_position;
|
||||||
|
attribute vec4 a_color;
|
||||||
|
attribute vec2 a_texCoord0;
|
||||||
|
|
||||||
|
uniform mat4 u_projTrans;
|
||||||
|
|
||||||
|
varying vec4 v_color;
|
||||||
|
varying vec2 v_texCoords;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
v_color = a_color;
|
||||||
|
v_texCoords = a_texCoord0;
|
||||||
|
gl_Position = u_projTrans * a_position;
|
||||||
|
}
|
||||||
40
forge-adventure/shaders/outline.frag
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
precision mediump int;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform vec2 u_viewportInverse;
|
||||||
|
uniform vec3 u_color;
|
||||||
|
uniform float u_offset;
|
||||||
|
uniform float u_step;
|
||||||
|
|
||||||
|
varying vec4 v_color;
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
|
#define ALPHA_VALUE_BORDER 0.5
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 T = v_texCoord.xy;
|
||||||
|
|
||||||
|
float alpha = 0.0;
|
||||||
|
bool allin = true;
|
||||||
|
for( float ix = -u_offset; ix < u_offset; ix += u_step )
|
||||||
|
{
|
||||||
|
for( float iy = -u_offset; iy < u_offset; iy += u_step )
|
||||||
|
{
|
||||||
|
float newAlpha = texture2D(u_texture, T + vec2(ix, iy) * u_viewportInverse).a;
|
||||||
|
allin = allin && newAlpha > ALPHA_VALUE_BORDER;
|
||||||
|
if (newAlpha > ALPHA_VALUE_BORDER && newAlpha >= alpha)
|
||||||
|
{
|
||||||
|
alpha = newAlpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allin)
|
||||||
|
{
|
||||||
|
alpha = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_FragColor = vec4(u_color,alpha);
|
||||||
|
}
|
||||||
16
forge-adventure/shaders/outline.vert
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
uniform mat4 u_projTrans;
|
||||||
|
|
||||||
|
attribute vec4 a_position;
|
||||||
|
attribute vec2 a_texCoord0;
|
||||||
|
attribute vec4 a_color;
|
||||||
|
|
||||||
|
varying vec4 v_color;
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
|
uniform vec2 u_viewportInverse;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = u_projTrans * a_position;
|
||||||
|
v_texCoord = a_texCoord0;
|
||||||
|
v_color = a_color;
|
||||||
|
}
|
||||||
26
forge-adventure/shaders/underwater.frag
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#ifdef GL_ES
|
||||||
|
#define PRECISION mediump
|
||||||
|
precision PRECISION float;
|
||||||
|
precision PRECISION int;
|
||||||
|
#else
|
||||||
|
#define PRECISION
|
||||||
|
#endif
|
||||||
|
|
||||||
|
varying vec2 v_texCoords;
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform float u_amount;
|
||||||
|
uniform float u_speed;
|
||||||
|
uniform float u_time;
|
||||||
|
uniform float u_bias;
|
||||||
|
|
||||||
|
void main () {
|
||||||
|
vec2 uv = v_texCoords;
|
||||||
|
|
||||||
|
uv.y += (cos((uv.y + (u_time * 0.04 * u_speed)) * 45.0) * 0.0019 * u_amount) + (cos((uv.y + (u_time * 0.1 * u_speed)) * 10.0) * 0.002 * u_amount);
|
||||||
|
|
||||||
|
uv.x += (sin((uv.y + (u_time * 0.07 * u_speed)) * 15.0) * 0.0029 * u_amount) + (sin((uv.y + (u_time * 0.1 * u_speed)) * 15.0) * 0.002 * u_amount);
|
||||||
|
|
||||||
|
vec4 texColor = texture2D(u_texture, uv);
|
||||||
|
|
||||||
|
gl_FragColor = mix(vec4(0.0, 0.0, 0.0, 1.0), texColor, u_bias);
|
||||||
|
}
|
||||||
57
forge-adventure/shaders/warp.frag
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
varying vec2 v_texCoords;
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
|
||||||
|
uniform float u_time;
|
||||||
|
uniform float u_speed;
|
||||||
|
uniform float u_amount;
|
||||||
|
uniform vec2 u_viewport;
|
||||||
|
uniform vec2 u_position;
|
||||||
|
|
||||||
|
float random2d(vec2 n) {
|
||||||
|
return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
float randomRange (in vec2 seed, in float min, in float max) {
|
||||||
|
return min + random2d(seed) * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
float insideRange(float v, float bottom, float top) {
|
||||||
|
return step(bottom, v) - step(top, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
float time = floor(u_time * u_speed * 60.0);
|
||||||
|
|
||||||
|
vec3 outCol = texture2D(u_texture, v_texCoords).rgb;
|
||||||
|
|
||||||
|
float maxOffset = u_amount/2.0;
|
||||||
|
for (float i = 0.0; i < 2.0; i += 1.0) {
|
||||||
|
float sliceY = random2d(vec2(time, 2345.0 + float(i)));
|
||||||
|
float sliceH = random2d(vec2(time, 9035.0 + float(i))) * 0.25;
|
||||||
|
float hOffset = randomRange(vec2(time, 9625.0 + float(i)), -maxOffset, maxOffset);
|
||||||
|
vec2 uvOff = v_texCoords;
|
||||||
|
uvOff.x += hOffset;
|
||||||
|
if (insideRange(v_texCoords.y, sliceY, fract(sliceY+sliceH)) == 1.0){
|
||||||
|
outCol = texture2D(u_texture, uvOff).rgb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxColOffset = u_amount / 6.0;
|
||||||
|
float rnd = random2d(vec2(time , 9545.0));
|
||||||
|
vec2 colOffset = vec2(randomRange(vec2(time , 9545.0), -maxColOffset, maxColOffset),
|
||||||
|
randomRange(vec2(time , 7205.0), -maxColOffset, maxColOffset));
|
||||||
|
if (rnd < 0.33) {
|
||||||
|
outCol.r = texture2D(u_texture, v_texCoords + colOffset).r;
|
||||||
|
} else if (rnd < 0.66) {
|
||||||
|
outCol.g = texture2D(u_texture, v_texCoords + colOffset).g;
|
||||||
|
} else {
|
||||||
|
outCol.b = texture2D(u_texture, v_texCoords + colOffset).b;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_FragColor = vec4(outCol, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
cd $(dirname "${0}")
|
||||||
|
java -XstartOnFirstThread -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
|
||||||
25
forge-adventure/src/main/config/forge-adventure-editor.cmd
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
java -version 1>nul 2>nul || (
|
||||||
|
echo no java installed
|
||||||
|
popd
|
||||||
|
exit /b 2
|
||||||
|
)
|
||||||
|
for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j"
|
||||||
|
|
||||||
|
if %jver% GEQ 17 (
|
||||||
|
java --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED --add-opens java.desktop/sun.swing=ALL-UNNAMED --add-opens java.desktop/java.awt.image=ALL-UNNAMED --add-opens java.desktop/java.awt.color=ALL-UNNAMED --add-opens java.desktop/sun.awt.image=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
|
||||||
|
popd
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if %jver% GEQ 11 (
|
||||||
|
java --illegal-access=permit -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
|
||||||
|
popd
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
java -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
|
||||||
|
popd
|
||||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
@@ -16,6 +16,7 @@ public class Main {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
|
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
|
||||||
GuiBase.setDeviceInfo("", "", 0, 0);
|
GuiBase.setDeviceInfo("", "", 0, 0);
|
||||||
new EditorMainWindow(Config.instance());
|
Config.instance();
|
||||||
|
new EditorMainWindow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,7 @@ package forge.adventure.editor;
|
|||||||
|
|
||||||
import forge.adventure.data.DialogData;
|
import forge.adventure.data.DialogData;
|
||||||
|
|
||||||
import javax.swing.JSpinner;
|
import javax.swing.*;
|
||||||
import javax.swing.JTextField;
|
|
||||||
import javax.swing.SpinnerNumberModel;
|
|
||||||
import javax.swing.event.ChangeEvent;
|
import javax.swing.event.ChangeEvent;
|
||||||
import javax.swing.event.ChangeListener;
|
import javax.swing.event.ChangeListener;
|
||||||
|
|
||||||
@@ -160,9 +158,9 @@ public class ActionEdit extends FormPanel {
|
|||||||
advanceQuestFlag.setText(currentData.advanceQuestFlag);
|
advanceQuestFlag.setText(currentData.advanceQuestFlag);
|
||||||
advanceCharacterFlag.setText(currentData.advanceCharacterFlag);
|
advanceCharacterFlag.setText(currentData.advanceCharacterFlag);
|
||||||
|
|
||||||
battleWithActorID.setText(String.valueOf(currentData.battleWithActorID));
|
battleWithActorID.setText("" + currentData.battleWithActorID);
|
||||||
activateObjectID.setText(String.valueOf(currentData.battleWithActorID));
|
activateObjectID.setText("" + currentData.battleWithActorID);
|
||||||
deleteMapObject.setText(String.valueOf(currentData.deleteMapObject));
|
deleteMapObject.setText("" + currentData.deleteMapObject);
|
||||||
setColorIdentity.setText(currentData.setColorIdentity);
|
setColorIdentity.setText(currentData.setColorIdentity);
|
||||||
addLife.getModel().setValue(currentData.addLife);
|
addLife.getModel().setValue(currentData.addLife);
|
||||||
addReputation.getModel().setValue(currentData.addMapReputation);
|
addReputation.getModel().setValue(currentData.addMapReputation);
|
||||||
@@ -2,17 +2,10 @@ package forge.adventure.editor;
|
|||||||
|
|
||||||
import forge.adventure.data.DialogData;
|
import forge.adventure.data.DialogData;
|
||||||
|
|
||||||
import javax.swing.DefaultListCellRenderer;
|
import javax.swing.*;
|
||||||
import javax.swing.DefaultListModel;
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JComponent;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JList;
|
|
||||||
import javax.swing.JToolBar;
|
|
||||||
import javax.swing.event.ChangeEvent;
|
import javax.swing.event.ChangeEvent;
|
||||||
import javax.swing.event.ChangeListener;
|
import javax.swing.event.ChangeListener;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.*;
|
||||||
import java.awt.Component;
|
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,25 +27,26 @@ public class ActionEditor extends JComponent {
|
|||||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
if(!(value instanceof DialogData.ActionData))
|
if(!(value instanceof DialogData.ActionData))
|
||||||
return label;
|
return label;
|
||||||
/*DialogData.ActionData action=(DialogData.ActionData) value;
|
DialogData.ActionData action=(DialogData.ActionData) value;
|
||||||
StringBuilder builder=new StringBuilder();
|
StringBuilder builder=new StringBuilder();
|
||||||
if(action.type==null||action.type.isEmpty())
|
// if(action.type==null||action.type.isEmpty())
|
||||||
builder.append("Action");
|
builder.append("Action");
|
||||||
else
|
// else
|
||||||
builder.append(action.type);*/
|
// builder.append(action.type);
|
||||||
label.setText("Action");
|
label.setText(builder.toString());
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public void addButton(String name, ActionListener action)
|
||||||
public void addButton(String name, ActionListener action) {
|
{
|
||||||
JButton newButton=new JButton(name);
|
JButton newButton=new JButton(name);
|
||||||
newButton.addActionListener(action);
|
newButton.addActionListener(action);
|
||||||
toolBar.add(newButton);
|
toolBar.add(newButton);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActionEditor() {
|
public ActionEditor()
|
||||||
|
{
|
||||||
|
|
||||||
list.setCellRenderer(new RewardDataRenderer());
|
list.setCellRenderer(new RewardDataRenderer());
|
||||||
list.addListSelectionListener(e -> ActionEditor.this.updateEdit());
|
list.addListSelectionListener(e -> ActionEditor.this.updateEdit());
|
||||||
@@ -68,7 +62,6 @@ public class ActionEditor extends JComponent {
|
|||||||
|
|
||||||
edit.addChangeListener(e -> emitChanged());
|
edit.addChangeListener(e -> emitChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void emitChanged() {
|
protected void emitChanged() {
|
||||||
if (updating)
|
if (updating)
|
||||||
return;
|
return;
|
||||||
@@ -80,7 +73,6 @@ public class ActionEditor extends JComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copy() {
|
private void copy() {
|
||||||
|
|
||||||
int selected=list.getSelectedIndex();
|
int selected=list.getSelectedIndex();
|
||||||
@@ -98,18 +90,18 @@ public class ActionEditor extends JComponent {
|
|||||||
edit.setCurrentAction(model.get(selected));
|
edit.setCurrentAction(model.get(selected));
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAction() {
|
void addAction()
|
||||||
|
{
|
||||||
DialogData.ActionData data=new DialogData.ActionData();
|
DialogData.ActionData data=new DialogData.ActionData();
|
||||||
model.add(model.size(),data);
|
model.add(model.size(),data);
|
||||||
}
|
}
|
||||||
|
void remove()
|
||||||
void remove() {
|
{
|
||||||
int selected=list.getSelectedIndex();
|
int selected=list.getSelectedIndex();
|
||||||
if(selected<0)
|
if(selected<0)
|
||||||
return;
|
return;
|
||||||
model.remove(selected);
|
model.remove(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAction(DialogData.ActionData[] actions) {
|
public void setAction(DialogData.ActionData[] actions) {
|
||||||
|
|
||||||
model.clear();
|
model.clear();
|
||||||
@@ -126,7 +118,8 @@ public class ActionEditor extends JComponent {
|
|||||||
public DialogData.ActionData[] getAction() {
|
public DialogData.ActionData[] getAction() {
|
||||||
|
|
||||||
DialogData.ActionData[] action= new DialogData.ActionData[model.getSize()];
|
DialogData.ActionData[] action= new DialogData.ActionData[model.getSize()];
|
||||||
for (int i = 0; i < model.getSize(); i++) {
|
for(int i=0;i<model.getSize();i++)
|
||||||
|
{
|
||||||
action[i]=model.get(i);
|
action[i]=model.get(i);
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
@@ -137,7 +130,6 @@ public class ActionEditor extends JComponent {
|
|||||||
model.clear();
|
model.clear();
|
||||||
updating = false;
|
updating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addChangeListener(ChangeListener listener) {
|
public void addChangeListener(ChangeListener listener) {
|
||||||
listenerList.add(ChangeListener.class, listener);
|
listenerList.add(ChangeListener.class, listener);
|
||||||
}
|
}
|
||||||
@@ -45,16 +45,17 @@ public class BiomeStructureDataMappingEditor extends JComponent {
|
|||||||
JList list, Object value, int index,
|
JList list, Object value, int index,
|
||||||
boolean isSelected, boolean cellHasFocus) {
|
boolean isSelected, boolean cellHasFocus) {
|
||||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping biomeData))
|
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping))
|
||||||
return label;
|
return label;
|
||||||
|
BiomeStructureData.BiomeStructureDataMapping data=(BiomeStructureData.BiomeStructureDataMapping) value;
|
||||||
// Get the renderer component from parent class
|
// Get the renderer component from parent class
|
||||||
|
|
||||||
label.setText(biomeData.name);
|
label.setText(data.name);
|
||||||
if(editor.data!=null)
|
if(editor.data!=null)
|
||||||
{
|
{
|
||||||
SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath));
|
SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath));
|
||||||
if(itemAtlas.has(biomeData.name))
|
if(itemAtlas.has(data.name))
|
||||||
label.setIcon(itemAtlas.get(biomeData.name));
|
label.setIcon(itemAtlas.get(data.name));
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImageIcon img=itemAtlas.getAny();
|
ImageIcon img=itemAtlas.getAny();
|
||||||
@@ -25,8 +25,9 @@ public class DialogOptionEditor extends JComponent{
|
|||||||
JList list, Object value, int index,
|
JList list, Object value, int index,
|
||||||
boolean isSelected, boolean cellHasFocus) {
|
boolean isSelected, boolean cellHasFocus) {
|
||||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
if(!(value instanceof DialogData dialog))
|
if(!(value instanceof DialogData))
|
||||||
return label;
|
return label;
|
||||||
|
DialogData dialog=(DialogData) value;
|
||||||
StringBuilder builder=new StringBuilder();
|
StringBuilder builder=new StringBuilder();
|
||||||
if(dialog.name==null||dialog.name.isEmpty())
|
if(dialog.name==null||dialog.name.isEmpty())
|
||||||
builder.append("[[Blank Option]]");
|
builder.append("[[Blank Option]]");
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package forge.adventure.editor;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import com.badlogic.gdx.tools.particleeditor.ParticleEditor;
|
||||||
|
import forge.localinstance.properties.ForgeConstants;
|
||||||
|
import forge.localinstance.properties.ForgePreferences;
|
||||||
|
import forge.model.FModel;
|
||||||
|
import forge.util.Lang;
|
||||||
|
import forge.util.Localizer;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor class to edit configuration, maybe moved or removed
|
||||||
|
*/
|
||||||
|
public class EditorMainWindow extends JFrame {
|
||||||
|
public final static WorldEditor worldEditor = new WorldEditor();
|
||||||
|
JTabbedPane tabs =new JTabbedPane();
|
||||||
|
|
||||||
|
public EditorMainWindow()
|
||||||
|
{
|
||||||
|
UIManager.LookAndFeelInfo[] var1 = UIManager.getInstalledLookAndFeels();
|
||||||
|
FModel.initialize(null, preferences -> {
|
||||||
|
preferences.setPref(ForgePreferences.FPref.LOAD_CARD_SCRIPTS_LAZILY, true);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
Lang.createInstance(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_LANGUAGE));
|
||||||
|
Localizer.getInstance().initialize(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_LANGUAGE), ForgeConstants.LANG_DIR);
|
||||||
|
int var2 = var1.length;
|
||||||
|
|
||||||
|
for (UIManager.LookAndFeelInfo info : var1) {
|
||||||
|
if ("Nimbus".equals(info.getName())) {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(info.getClassName());
|
||||||
|
} catch (Throwable var6) {
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderLayout layout=new BorderLayout();
|
||||||
|
JToolBar toolBar = new JToolBar("toolbar");
|
||||||
|
JButton newButton=new JButton("open ParticleEditor");
|
||||||
|
newButton.addActionListener(e -> EventQueue.invokeLater(ParticleEditor::new));
|
||||||
|
toolBar.add(newButton);
|
||||||
|
setLayout(layout);
|
||||||
|
toolBar.setFloatable(false);
|
||||||
|
add(toolBar, BorderLayout.NORTH);
|
||||||
|
add(tabs, BorderLayout.CENTER);
|
||||||
|
tabs.addTab("World",worldEditor);
|
||||||
|
tabs.addTab("POI",new PointOfInterestEditor());
|
||||||
|
tabs.addTab("Items",new ItemsEditor());
|
||||||
|
tabs.addTab("Enemies",new EnemyEditor());
|
||||||
|
tabs.addTab("Quests",new QuestEditor());
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
setSize(800,600);
|
||||||
|
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow( this );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,16 +27,17 @@ public class ItemsEditor extends JComponent {
|
|||||||
JList list, Object value, int index,
|
JList list, Object value, int index,
|
||||||
boolean isSelected, boolean cellHasFocus) {
|
boolean isSelected, boolean cellHasFocus) {
|
||||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
if(!(value instanceof ItemData item))
|
if(!(value instanceof ItemData))
|
||||||
return label;
|
return label;
|
||||||
|
ItemData Item=(ItemData) value;
|
||||||
// Get the renderer component from parent class
|
// Get the renderer component from parent class
|
||||||
|
|
||||||
label.setText(item.name);
|
label.setText(Item.name);
|
||||||
if(itemAtlas==null)
|
if(itemAtlas==null)
|
||||||
itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS));
|
itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS));
|
||||||
|
|
||||||
if(itemAtlas.has(item.iconName))
|
if(itemAtlas.has(Item.iconName))
|
||||||
label.setIcon(itemAtlas.get(item.iconName));
|
label.setIcon(itemAtlas.get(Item.iconName));
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImageIcon img=itemAtlas.getAny();
|
ImageIcon img=itemAtlas.getAny();
|
||||||
@@ -2,23 +2,10 @@ package forge.adventure.editor;
|
|||||||
|
|
||||||
import forge.adventure.data.AdventureQuestData;
|
import forge.adventure.data.AdventureQuestData;
|
||||||
|
|
||||||
import javax.swing.AbstractAction;
|
import javax.swing.*;
|
||||||
import javax.swing.BoxLayout;
|
|
||||||
import javax.swing.DefaultListModel;
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JCheckBox;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JList;
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import javax.swing.JScrollPane;
|
|
||||||
import javax.swing.JTabbedPane;
|
|
||||||
import javax.swing.JTextArea;
|
|
||||||
import javax.swing.JTextField;
|
|
||||||
import javax.swing.KeyStroke;
|
|
||||||
import javax.swing.event.ListDataEvent;
|
import javax.swing.event.ListDataEvent;
|
||||||
import javax.swing.event.ListDataListener;
|
import javax.swing.event.ListDataListener;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.*;
|
||||||
import java.awt.Dimension;
|
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
@@ -298,7 +285,7 @@ public class QuestEdit extends FormPanel {
|
|||||||
}
|
}
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
updating=true;
|
updating=true;
|
||||||
id.setText(String.valueOf(currentData.getID()));
|
id.setText(currentData.getID() + "");
|
||||||
name.setText(currentData.name);
|
name.setText(currentData.name);
|
||||||
description.setText(currentData.description);
|
description.setText(currentData.description);
|
||||||
synopsis.setText(currentData.synopsis);
|
synopsis.setText(currentData.synopsis);
|
||||||
@@ -26,8 +26,9 @@ public class QuestEditor extends JComponent {
|
|||||||
JList list, Object value, int index,
|
JList list, Object value, int index,
|
||||||
boolean isSelected, boolean cellHasFocus) {
|
boolean isSelected, boolean cellHasFocus) {
|
||||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
if(!(value instanceof AdventureQuestData quest))
|
if(!(value instanceof AdventureQuestData))
|
||||||
return label;
|
return label;
|
||||||
|
AdventureQuestData quest=(AdventureQuestData) value;
|
||||||
// Get the renderer component from parent class
|
// Get the renderer component from parent class
|
||||||
|
|
||||||
label.setText(quest.name);
|
label.setText(quest.name);
|
||||||
@@ -26,8 +26,9 @@ public class QuestStageEditor extends JComponent{
|
|||||||
JList list, Object value, int index,
|
JList list, Object value, int index,
|
||||||
boolean isSelected, boolean cellHasFocus) {
|
boolean isSelected, boolean cellHasFocus) {
|
||||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
if(!(value instanceof AdventureQuestStage stageData))
|
if(!(value instanceof AdventureQuestStage))
|
||||||
return label;
|
return label;
|
||||||
|
AdventureQuestStage stageData=(AdventureQuestStage) value;
|
||||||
label.setText(stageData.name);
|
label.setText(stageData.name);
|
||||||
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
|
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
|
||||||
return label;
|
return label;
|
||||||
@@ -25,10 +25,10 @@ public class RewardEdit extends FormPanel {
|
|||||||
TextListEdit colors =new TextListEdit(new String[] { "White", "Blue", "Black", "Red", "Green" });
|
TextListEdit colors =new TextListEdit(new String[] { "White", "Blue", "Black", "Red", "Green" });
|
||||||
TextListEdit rarity =new TextListEdit(new String[] { "Basic Land", "Common", "Uncommon", "Rare", "Mythic Rare" });
|
TextListEdit rarity =new TextListEdit(new String[] { "Basic Land", "Common", "Uncommon", "Rare", "Mythic Rare" });
|
||||||
TextListEdit subTypes =new TextListEdit();
|
TextListEdit subTypes =new TextListEdit();
|
||||||
TextListEdit cardTypes =new TextListEdit(Arrays.stream(CardType.CoreType.values()).map(CardType.CoreType::toString).toArray(String[]::new));
|
TextListEdit cardTypes =new TextListEdit(Arrays.asList(CardType.CoreType.values()).stream().map(CardType.CoreType::toString).toArray(String[]::new));
|
||||||
TextListEdit superTypes =new TextListEdit(Arrays.stream(CardType.Supertype.values()).map(CardType.Supertype::toString).toArray(String[]::new));
|
TextListEdit superTypes =new TextListEdit(Arrays.asList(CardType.Supertype.values()).stream().map(CardType.Supertype::toString).toArray(String[]::new));
|
||||||
TextListEdit manaCosts =new TextListEdit();
|
TextListEdit manaCosts =new TextListEdit();
|
||||||
TextListEdit keyWords =new TextListEdit(Arrays.stream(Keyword.values()).map(Keyword::toString).toArray(String[]::new));
|
TextListEdit keyWords =new TextListEdit(Arrays.asList(Keyword.values()).stream().map(Keyword::toString).toArray(String[]::new));
|
||||||
JComboBox colorType =new JComboBox(new String[] { "Any", "Colorless", "MultiColor", "MonoColor"});
|
JComboBox colorType =new JComboBox(new String[] { "Any", "Colorless", "MultiColor", "MonoColor"});
|
||||||
JTextField cardText =new JTextField();
|
JTextField cardText =new JTextField();
|
||||||
private boolean updating=false;
|
private boolean updating=false;
|
||||||
@@ -5,7 +5,7 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
|||||||
import com.badlogic.gdx.utils.Array;
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.swing.ImageIcon;
|
import javax.swing.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -65,7 +65,7 @@ public class SwingAtlas {
|
|||||||
return new ImageIcon(img.getSubimage(sprite.left,sprite.top, sprite.width, sprite.height).getScaledInstance((int) (imageSize*(sprite.width/(float)sprite.height)),imageSize,SCALE_FAST));
|
return new ImageIcon(img.getSubimage(sprite.left,sprite.top, sprite.width, sprite.height).getScaledInstance((int) (imageSize*(sprite.width/(float)sprite.height)),imageSize,SCALE_FAST));
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (IOException e)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -3,17 +3,10 @@ package forge.adventure.editor;
|
|||||||
import forge.adventure.data.BiomeData;
|
import forge.adventure.data.BiomeData;
|
||||||
import forge.adventure.data.BiomeTerrainData;
|
import forge.adventure.data.BiomeTerrainData;
|
||||||
|
|
||||||
import javax.swing.DefaultListCellRenderer;
|
import javax.swing.*;
|
||||||
import javax.swing.DefaultListModel;
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JComponent;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JList;
|
|
||||||
import javax.swing.JToolBar;
|
|
||||||
import javax.swing.event.ChangeEvent;
|
import javax.swing.event.ChangeEvent;
|
||||||
import javax.swing.event.ChangeListener;
|
import javax.swing.event.ChangeListener;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.*;
|
||||||
import java.awt.Component;
|
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,11 +29,11 @@ public class TerrainsEditor extends JComponent{
|
|||||||
if(!(value instanceof BiomeTerrainData))
|
if(!(value instanceof BiomeTerrainData))
|
||||||
return label;
|
return label;
|
||||||
BiomeTerrainData terrainData=(BiomeTerrainData) value;
|
BiomeTerrainData terrainData=(BiomeTerrainData) value;
|
||||||
/*StringBuilder builder=new StringBuilder();
|
StringBuilder builder=new StringBuilder();
|
||||||
builder.append("Terrain");
|
builder.append("Terrain");
|
||||||
builder.append(" ");
|
builder.append(" ");
|
||||||
builder.append(terrainData.spriteName);*/
|
builder.append(terrainData.spriteName);
|
||||||
label.setText("Terrain " + terrainData.spriteName);
|
label.setText(builder.toString());
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,9 @@ public class WorldEditor extends JComponent {
|
|||||||
JList list, Object value, int index,
|
JList list, Object value, int index,
|
||||||
boolean isSelected, boolean cellHasFocus) {
|
boolean isSelected, boolean cellHasFocus) {
|
||||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
if(!(value instanceof BiomeData biome))
|
if(!(value instanceof BiomeData))
|
||||||
return label;
|
return label;
|
||||||
|
BiomeData biome=(BiomeData) value;
|
||||||
// Get the renderer component from parent class
|
// Get the renderer component from parent class
|
||||||
|
|
||||||
label.setText(biome.name);
|
label.setText(biome.name);
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>forge</artifactId>
|
<artifactId>forge</artifactId>
|
||||||
<groupId>forge</groupId>
|
<groupId>forge</groupId>
|
||||||
<version>${revision}</version>
|
<version>1.6.64</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>forge-ai</artifactId>
|
<artifactId>forge-ai</artifactId>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class AiDeckStatistics {
|
public class AIDeckStatistics {
|
||||||
|
|
||||||
public float averageCMC = 0;
|
public float averageCMC = 0;
|
||||||
// TODO implement this. Use a numerically stable algorithm from
|
// TODO implement this. Use a numerically stable algorithm from
|
||||||
@@ -26,7 +26,7 @@ public class AiDeckStatistics {
|
|||||||
public int[] maxPips = null;
|
public int[] maxPips = null;
|
||||||
// public int[] numSources = new int[6];
|
// public int[] numSources = new int[6];
|
||||||
public int numLands = 0;
|
public int numLands = 0;
|
||||||
public AiDeckStatistics(float averageCMC, float stddevCMC, int maxCost, int maxColoredCost, int[] maxPips, int numLands) {
|
public AIDeckStatistics(float averageCMC, float stddevCMC, int maxCost, int maxColoredCost, int[] maxPips, int numLands) {
|
||||||
this.averageCMC = averageCMC;
|
this.averageCMC = averageCMC;
|
||||||
this.stddevCMC = stddevCMC;
|
this.stddevCMC = stddevCMC;
|
||||||
this.maxCost = maxCost;
|
this.maxCost = maxCost;
|
||||||
@@ -35,7 +35,7 @@ public class AiDeckStatistics {
|
|||||||
this.numLands = numLands;
|
this.numLands = numLands;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AiDeckStatistics fromCards(List<Card> cards) {
|
public static AIDeckStatistics fromCards(List<Card> cards) {
|
||||||
int totalCMC = 0;
|
int totalCMC = 0;
|
||||||
int totalCount = 0;
|
int totalCount = 0;
|
||||||
int numLands = 0;
|
int numLands = 0;
|
||||||
@@ -75,7 +75,7 @@ public class AiDeckStatistics {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AiDeckStatistics(totalCount == 0 ? 0 : totalCMC / (float)totalCount,
|
return new AIDeckStatistics(totalCount == 0 ? 0 : totalCMC / (float)totalCount,
|
||||||
0, // TODO use https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
|
0, // TODO use https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
|
||||||
maxCost,
|
maxCost,
|
||||||
maxColoredCost,
|
maxColoredCost,
|
||||||
@@ -85,7 +85,7 @@ public class AiDeckStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static AiDeckStatistics fromDeck(Deck deck, Player player) {
|
public static AIDeckStatistics fromDeck(Deck deck, Player player) {
|
||||||
List<Card> cardlist = new ArrayList<>();
|
List<Card> cardlist = new ArrayList<>();
|
||||||
for (final Map.Entry<DeckSection, CardPool> deckEntry : deck) {
|
for (final Map.Entry<DeckSection, CardPool> deckEntry : deck) {
|
||||||
switch (deckEntry.getKey()) {
|
switch (deckEntry.getKey()) {
|
||||||
@@ -104,7 +104,7 @@ public class AiDeckStatistics {
|
|||||||
return fromCards(cardlist);
|
return fromCards(cardlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AiDeckStatistics fromPlayer(Player player) {
|
public static AIDeckStatistics fromPlayer(Player player) {
|
||||||
Deck deck = player.getRegisteredPlayer().getDeck();
|
Deck deck = player.getRegisteredPlayer().getDeck();
|
||||||
if (deck.isEmpty()) {
|
if (deck.isEmpty()) {
|
||||||
// we're in a test or some weird match, search through the hand and library and build the decklist
|
// we're in a test or some weird match, search through the hand and library and build the decklist
|
||||||
@@ -120,6 +120,7 @@ public class AiDeckStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fromDeck(deck, player);
|
return fromDeck(deck, player);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package forge.ai;
|
|
||||||
|
|
||||||
public record AiAbilityDecision(int rating, AiPlayDecision decision) {
|
|
||||||
private static int MIN_RATING = 30;
|
|
||||||
|
|
||||||
public boolean willingToPlay() {
|
|
||||||
return rating > MIN_RATING && decision.willingToPlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import forge.ai.ability.AnimateAi;
|
import forge.ai.ability.AnimateAi;
|
||||||
@@ -30,28 +32,23 @@ import forge.game.combat.CombatUtil;
|
|||||||
import forge.game.combat.GlobalAttackRestrictions;
|
import forge.game.combat.GlobalAttackRestrictions;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.keyword.KeywordInterface;
|
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerCollection;
|
import forge.game.player.PlayerCollection;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityPredicates;
|
import forge.game.spellability.SpellAbilityPredicates;
|
||||||
import forge.game.staticability.StaticAbility;
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
||||||
import forge.game.staticability.StaticAbilityMode;
|
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.*;
|
import forge.util.Aggregates;
|
||||||
|
import forge.util.Expressions;
|
||||||
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,9 +73,6 @@ public class AiAttackController {
|
|||||||
|
|
||||||
private int aiAggression = 0; // how aggressive the ai is attack will be depending on circumstances
|
private int aiAggression = 0; // how aggressive the ai is attack will be depending on circumstances
|
||||||
private final boolean nextTurn; // include creature that can only attack/block next turn
|
private final boolean nextTurn; // include creature that can only attack/block next turn
|
||||||
private final int timeOut;
|
|
||||||
private final boolean canUseTimeout;
|
|
||||||
private List<CompletableFuture<Integer>> futures = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -96,8 +90,6 @@ public class AiAttackController {
|
|||||||
myList = ai.getCreaturesInPlay();
|
myList = ai.getCreaturesInPlay();
|
||||||
this.nextTurn = nextTurn;
|
this.nextTurn = nextTurn;
|
||||||
refreshCombatants(defendingOpponent);
|
refreshCombatants(defendingOpponent);
|
||||||
this.timeOut = ai.getGame().getAITimeout();
|
|
||||||
this.canUseTimeout = ai.getGame().canUseTimeout();
|
|
||||||
} // overloaded constructor to evaluate attackers that should attack next turn
|
} // overloaded constructor to evaluate attackers that should attack next turn
|
||||||
|
|
||||||
public AiAttackController(final Player ai, Card attacker) {
|
public AiAttackController(final Player ai, Card attacker) {
|
||||||
@@ -111,13 +103,11 @@ public class AiAttackController {
|
|||||||
attackers.add(attacker);
|
attackers.add(attacker);
|
||||||
}
|
}
|
||||||
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn);
|
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn);
|
||||||
this.timeOut = ai.getGame().getAITimeout();
|
|
||||||
this.canUseTimeout = ai.getGame().canUseTimeout();
|
|
||||||
} // overloaded constructor to evaluate single specified attacker
|
} // overloaded constructor to evaluate single specified attacker
|
||||||
|
|
||||||
private void refreshCombatants(GameEntity defender) {
|
private void refreshCombatants(GameEntity defender) {
|
||||||
if (defender instanceof Card card && card.isBattle()) {
|
if (defender instanceof Card && ((Card) defender).isBattle()) {
|
||||||
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
|
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
|
||||||
} else {
|
} else {
|
||||||
this.oppList = getOpponentCreatures(defendingOpponent);
|
this.oppList = getOpponentCreatures(defendingOpponent);
|
||||||
}
|
}
|
||||||
@@ -138,22 +128,20 @@ public class AiAttackController {
|
|||||||
|
|
||||||
CardCollection tappedDefenders = new CardCollection();
|
CardCollection tappedDefenders = new CardCollection();
|
||||||
for (Card c : CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), canAnimate)) {
|
for (Card c : CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), canAnimate)) {
|
||||||
for (SpellAbility sa : IterableUtil.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.Animate))) {
|
for (SpellAbility sa : Iterables.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.Animate))) {
|
||||||
if (sa.usesTargeting() || !sa.getParamOrDefault("Defined", "Self").equals("Self")) {
|
if (sa.usesTargeting() || !sa.getParamOrDefault("Defined", "Self").equals("Self")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sa.setActivatingPlayer(defender);
|
sa.setActivatingPlayer(defender);
|
||||||
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
|
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
} else if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
||||||
if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
|
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
|
||||||
if (animatedCopy.isCreature()) {
|
if (animatedCopy.isCreature()) {
|
||||||
// TODO imprecise, only works 100% for colorless mana
|
|
||||||
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
|
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
|
||||||
sa.getPayCosts().getTotalMana().getCMC() : 0;
|
sa.getPayCosts().getTotalMana().getCMC() : 0; // FIXME: imprecise, only works 100% for colorless mana
|
||||||
if (totalMana - manaReserved >= saCMC) {
|
if (totalMana - manaReserved >= saCMC) {
|
||||||
manaReserved += saCMC;
|
manaReserved += saCMC;
|
||||||
defenders.add(animatedCopy);
|
defenders.add(animatedCopy);
|
||||||
@@ -164,7 +152,7 @@ public class AiAttackController {
|
|||||||
defenders.removeAll(tappedDefenders);
|
defenders.removeAll(tappedDefenders);
|
||||||
|
|
||||||
// Transform (e.g. Incubator tokens)
|
// Transform (e.g. Incubator tokens)
|
||||||
for (SpellAbility sa : IterableUtil.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.SetState))) {
|
for (SpellAbility sa : Iterables.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.SetState))) {
|
||||||
Card transformedCopy = ComputerUtilCombat.canTransform(c);
|
Card transformedCopy = ComputerUtilCombat.canTransform(c);
|
||||||
if (transformedCopy.isCreature()) {
|
if (transformedCopy.isCreature()) {
|
||||||
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
|
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
|
||||||
@@ -315,8 +303,7 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Poison opponent if unblocked
|
// Poison opponent if unblocked
|
||||||
if (defender instanceof Player player
|
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) {
|
||||||
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,9 +509,13 @@ public class AiAttackController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> bandsWithString = Arrays.asList("Bands with Other Legendary Creatures",
|
||||||
|
"Bands with Other Creatures named Wolves of the Hunt",
|
||||||
|
"Bands with Other Dinosaurs");
|
||||||
|
|
||||||
List<Card> bandingCreatures = null;
|
List<Card> bandingCreatures = null;
|
||||||
if (test == null) {
|
if (test == null) {
|
||||||
bandingCreatures = CardLists.filter(myList, card -> card.hasKeyword(Keyword.BANDING) || card.hasKeyword(Keyword.BANDSWITH));
|
bandingCreatures = CardLists.filter(myList, card -> card.hasKeyword(Keyword.BANDING) || card.hasAnyKeyword(bandsWithString));
|
||||||
|
|
||||||
// filter out anything that can't legally attack or is already declared as an attacker
|
// filter out anything that can't legally attack or is already declared as an attacker
|
||||||
bandingCreatures = CardLists.filter(bandingCreatures, card -> !combat.isAttacking(card) && CombatUtil.canAttack(card));
|
bandingCreatures = CardLists.filter(bandingCreatures, card -> !combat.isAttacking(card) && CombatUtil.canAttack(card));
|
||||||
@@ -532,7 +523,7 @@ public class AiAttackController {
|
|||||||
bandingCreatures = notNeededAsBlockers(attackers, bandingCreatures);
|
bandingCreatures = notNeededAsBlockers(attackers, bandingCreatures);
|
||||||
} else {
|
} else {
|
||||||
// Test a specific creature for Banding
|
// Test a specific creature for Banding
|
||||||
if (test.hasKeyword(Keyword.BANDING) || test.hasKeyword(Keyword.BANDSWITH)) {
|
if (test.hasKeyword(Keyword.BANDING) || test.hasAnyKeyword(bandsWithString)) {
|
||||||
bandingCreatures = new CardCollection(test);
|
bandingCreatures = new CardCollection(test);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,7 +541,7 @@ public class AiAttackController {
|
|||||||
|
|
||||||
// TODO: Assign to band with the best attacker for now, but needs better logic.
|
// TODO: Assign to band with the best attacker for now, but needs better logic.
|
||||||
for (Card c : bandingCreatures) {
|
for (Card c : bandingCreatures) {
|
||||||
Card bestBand = null;
|
Card bestBand;
|
||||||
|
|
||||||
if (c.getNetPower() <= 0) {
|
if (c.getNetPower() <= 0) {
|
||||||
// Don't band a zero power creature if there's already a banding creature in a band
|
// Don't band a zero power creature if there's already a banding creature in a band
|
||||||
@@ -558,16 +549,12 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Card bestAttacker = ComputerUtilCard.getBestCreatureAI(attackers);
|
Card bestAttacker = ComputerUtilCard.getBestCreatureAI(attackers);
|
||||||
|
if (c.hasKeyword("Bands with Other Legendary Creatures")) {
|
||||||
// TODO how should this work with multiple bands with other abilities?
|
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.getType(attackers, "Legendary"));
|
||||||
if (c.hasKeyword(Keyword.BANDSWITH)) {
|
} else if (c.hasKeyword("Bands with Other Dinosaurs")) {
|
||||||
for (KeywordInterface kw : c.getKeywords(Keyword.BANDSWITH)) {
|
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.getType(attackers, "Dinosaur"));
|
||||||
final String o = kw.getOriginal();
|
} else if (c.hasKeyword("Bands with Other Creatures named Wolves of the Hunt")) {
|
||||||
String m[] = o.split(":");
|
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.filter(attackers, CardPredicates.nameEquals("Wolves of the Hunt")));
|
||||||
CardCollection bandPartner = CardLists.getValidCards(attackers, m[1], c.getController(), c, null);
|
|
||||||
bestBand = ComputerUtilCard.getBestCreatureAI(bandPartner);
|
|
||||||
break; // ?
|
|
||||||
}
|
|
||||||
} else if (!c.hasAnyKeyword(evasionKeywords) && bestAttacker != null && bestAttacker.hasAnyKeyword(evasionKeywords)) {
|
} else if (!c.hasAnyKeyword(evasionKeywords) && bestAttacker != null && bestAttacker.hasAnyKeyword(evasionKeywords)) {
|
||||||
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.filter(attackers, card -> !card.hasAnyKeyword(evasionKeywords)));
|
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.filter(attackers, card -> !card.hasAnyKeyword(evasionKeywords)));
|
||||||
} else {
|
} else {
|
||||||
@@ -627,9 +614,9 @@ public class AiAttackController {
|
|||||||
// TODO: the AI should ideally predict how many times it can activate
|
// TODO: the AI should ideally predict how many times it can activate
|
||||||
// for now, unless the opponent is tapped out, break at this point
|
// for now, unless the opponent is tapped out, break at this point
|
||||||
// and do not predict the blocker limit (which is safer)
|
// and do not predict the blocker limit (which is safer)
|
||||||
if (defendingOpponent.getLandsInPlay().anyMatch(CardPredicates.UNTAPPED)) {
|
if (Iterables.any(defendingOpponent.getLandsInPlay(), CardPredicates.Presets.UNTAPPED)) {
|
||||||
maxBlockersAfterCrew += CardLists.count(CardLists.getNotType(defendingOpponent.getCardsIn(ZoneType.Battlefield), "Creature"),
|
maxBlockersAfterCrew += CardLists.count(CardLists.getNotType(defendingOpponent.getCardsIn(ZoneType.Battlefield), "Creature"),
|
||||||
CardPredicates.isType("Vehicle").and(CardPredicates.UNTAPPED));
|
Predicates.and(CardPredicates.isType("Vehicle"), CardPredicates.Presets.UNTAPPED));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,7 +793,6 @@ public class AiAttackController {
|
|||||||
if (bAssault) {
|
if (bAssault) {
|
||||||
return prefDefender;
|
return prefDefender;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. attack planeswalkers
|
// 2. attack planeswalkers
|
||||||
List<Card> pwDefending = c.getDefendingPlaneswalkers();
|
List<Card> pwDefending = c.getDefendingPlaneswalkers();
|
||||||
if (!pwDefending.isEmpty()) {
|
if (!pwDefending.isEmpty()) {
|
||||||
@@ -814,7 +800,7 @@ public class AiAttackController {
|
|||||||
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
|
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get the preferred battle (prefer own battles, then ally battles)
|
// Get the preferred battle (prefer own battles, then ally battles)
|
||||||
final CardCollection defBattles = c.getDefendingBattles();
|
final CardCollection defBattles = c.getDefendingBattles();
|
||||||
List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
|
List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
|
||||||
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
|
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
|
||||||
@@ -853,9 +839,10 @@ public class AiAttackController {
|
|||||||
// decided to attack another defender so related lists need to be updated
|
// decided to attack another defender so related lists need to be updated
|
||||||
// (though usually rather try to avoid this situation for performance reasons)
|
// (though usually rather try to avoid this situation for performance reasons)
|
||||||
if (defender != defendingOpponent) {
|
if (defender != defendingOpponent) {
|
||||||
if (defender instanceof Player p) {
|
if (defender instanceof Player) {
|
||||||
defendingOpponent = p;
|
defendingOpponent = (Player) defender;
|
||||||
} else if (defender instanceof Card defCard) {
|
} else if (defender instanceof Card) {
|
||||||
|
Card defCard = (Card) defender;
|
||||||
if (defCard.isBattle()) {
|
if (defCard.isBattle()) {
|
||||||
defendingOpponent = defCard.getProtectingPlayer();
|
defendingOpponent = defCard.getProtectingPlayer();
|
||||||
} else {
|
} else {
|
||||||
@@ -895,7 +882,7 @@ public class AiAttackController {
|
|||||||
// TODO: detect Season of the Witch by presence of a card with a specific trigger
|
// TODO: detect Season of the Witch by presence of a card with a specific trigger
|
||||||
final boolean seasonOfTheWitch = ai.getGame().isCardInPlay("Season of the Witch");
|
final boolean seasonOfTheWitch = ai.getGame().isCardInPlay("Season of the Witch");
|
||||||
|
|
||||||
final Queue<Card> attackersLeft = new ConcurrentLinkedQueue<>(this.attackers);
|
List<Card> attackersLeft = new ArrayList<>(this.attackers);
|
||||||
|
|
||||||
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
|
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
|
||||||
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
|
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
|
||||||
@@ -911,27 +898,25 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attackers that don't really have a choice
|
// Attackers that don't really have a choice
|
||||||
final AtomicInteger numForcedAttackers = new AtomicInteger(0);
|
int numForcedAttackers = 0;
|
||||||
// nextTurn is now only used by effect from Oracle en-Vec, which can skip check must attack,
|
// nextTurn is now only used by effect from Oracle en-Vec, which can skip check must attack,
|
||||||
// because creatures not chosen can't attack.
|
// because creatures not chosen can't attack.
|
||||||
if (!nextTurn) {
|
if (!nextTurn) {
|
||||||
for (final Card attacker : this.attackers) {
|
for (final Card attacker : this.attackers) {
|
||||||
final GameEntity finalDefender = defender;
|
|
||||||
futures.add(CompletableFuture.supplyAsync(()-> {
|
|
||||||
GameEntity mustAttackDef = null;
|
GameEntity mustAttackDef = null;
|
||||||
if (attacker.getSVar("MustAttack").equals("True")) {
|
if (attacker.getSVar("MustAttack").equals("True")) {
|
||||||
mustAttackDef = finalDefender;
|
mustAttackDef = defender;
|
||||||
} else if (attacker.hasSVar("EndOfTurnLeavePlay")
|
} else if (attacker.hasSVar("EndOfTurnLeavePlay")
|
||||||
&& isEffectiveAttacker(ai, attacker, combat, finalDefender)) {
|
&& isEffectiveAttacker(ai, attacker, combat, defender)) {
|
||||||
mustAttackDef = finalDefender;
|
mustAttackDef = defender;
|
||||||
} else if (seasonOfTheWitch) {
|
} else if (seasonOfTheWitch) {
|
||||||
//TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
|
//TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
|
||||||
mustAttackDef = finalDefender;
|
mustAttackDef = defender;
|
||||||
} else {
|
} else {
|
||||||
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) return 0;
|
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) continue;
|
||||||
// check defenders in order of maximum requirements
|
// check defenders in order of maximum requirements
|
||||||
List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements();
|
List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements();
|
||||||
final GameEntity def = finalDefender;
|
final GameEntity def = defender;
|
||||||
reqs.sort((r1, r2) -> {
|
reqs.sort((r1, r2) -> {
|
||||||
if (r1.getValue() == r2.getValue()) {
|
if (r1.getValue() == r2.getValue()) {
|
||||||
// try to attack the designated defender
|
// try to attack the designated defender
|
||||||
@@ -949,8 +934,8 @@ public class AiAttackController {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// or weakest player
|
// or weakest player
|
||||||
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
|
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
|
||||||
return p1.getLife() - p2.getLife();
|
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r2.getValue() - r1.getValue();
|
return r2.getValue() - r1.getValue();
|
||||||
@@ -967,20 +952,9 @@ public class AiAttackController {
|
|||||||
if (mustAttackDef != null) {
|
if (mustAttackDef != null) {
|
||||||
combat.addAttacker(attacker, mustAttackDef);
|
combat.addAttacker(attacker, mustAttackDef);
|
||||||
attackersLeft.remove(attacker);
|
attackersLeft.remove(attacker);
|
||||||
numForcedAttackers.incrementAndGet();
|
numForcedAttackers++;
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
}).exceptionally(ex -> {
|
|
||||||
ex.printStackTrace();
|
|
||||||
return 0;
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
CompletableFuture<?>[] futuresArray = futures.toArray(new CompletableFuture<?>[0]);
|
|
||||||
if (canUseTimeout)
|
|
||||||
CompletableFuture.allOf(futuresArray).completeOnTimeout(null, timeOut, TimeUnit.SECONDS).join();
|
|
||||||
else
|
|
||||||
CompletableFuture.allOf(futuresArray).join();
|
|
||||||
futures.clear();
|
|
||||||
if (attackersLeft.isEmpty()) {
|
if (attackersLeft.isEmpty()) {
|
||||||
return aiAggression;
|
return aiAggression;
|
||||||
}
|
}
|
||||||
@@ -988,19 +962,18 @@ public class AiAttackController {
|
|||||||
|
|
||||||
// Lightmine Field: make sure the AI doesn't wipe out its own creatures
|
// Lightmine Field: make sure the AI doesn't wipe out its own creatures
|
||||||
if (lightmineField) {
|
if (lightmineField) {
|
||||||
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers.get(), playAggro);
|
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro);
|
||||||
}
|
}
|
||||||
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily
|
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily
|
||||||
if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers.get(), attackMax)) {
|
if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers, attackMax)) {
|
||||||
return aiAggression;
|
return aiAggression;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else
|
if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println("Assault");
|
System.out.println("Assault");
|
||||||
List<Card> left = new ArrayList<>(attackersLeft);
|
CardLists.sortByPowerDesc(attackersLeft);
|
||||||
CardLists.sortByPowerDesc(left);
|
for (Card attacker : attackersLeft) {
|
||||||
for (Card attacker : left) {
|
|
||||||
// reached max, breakup
|
// reached max, breakup
|
||||||
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
|
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
|
||||||
return aiAggression;
|
return aiAggression;
|
||||||
@@ -1194,8 +1167,10 @@ public class AiAttackController {
|
|||||||
attritionalAttackers.remove(attritionalAttackers.size() - 1);
|
attritionalAttackers.remove(attritionalAttackers.size() - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attackRounds++;
|
attackRounds += 1;
|
||||||
doAttritionalAttack = humanLife <= 0;
|
if (humanLife <= 0) {
|
||||||
|
doAttritionalAttack = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// *********************
|
// *********************
|
||||||
// end attritional attack calculation
|
// end attritional attack calculation
|
||||||
@@ -1290,20 +1265,19 @@ public class AiAttackController {
|
|||||||
if ( LOG_AI_ATTACKS )
|
if ( LOG_AI_ATTACKS )
|
||||||
System.out.println("Normal attack");
|
System.out.println("Normal attack");
|
||||||
|
|
||||||
List<Card> left = new ArrayList<>(attackersLeft);
|
attackersLeft = notNeededAsBlockers(combat.getAttackers(), attackersLeft);
|
||||||
left = notNeededAsBlockers(combat.getAttackers(), left);
|
attackersLeft = sortAttackers(attackersLeft);
|
||||||
left = sortAttackers(left);
|
|
||||||
|
|
||||||
if ( LOG_AI_ATTACKS )
|
if ( LOG_AI_ATTACKS )
|
||||||
System.out.println("attackersLeft = " + left);
|
System.out.println("attackersLeft = " + attackersLeft);
|
||||||
|
|
||||||
FCollection<GameEntity> possibleDefenders = new FCollection<>(defendingOpponent);
|
FCollection<GameEntity> possibleDefenders = new FCollection<>(defendingOpponent);
|
||||||
possibleDefenders.addAll(defendingOpponent.getPlaneswalkersInPlay());
|
possibleDefenders.addAll(defendingOpponent.getPlaneswalkersInPlay());
|
||||||
|
|
||||||
while (!left.isEmpty()) {
|
while (!attackersLeft.isEmpty()) {
|
||||||
CardCollection attackersAssigned = new CardCollection();
|
CardCollection attackersAssigned = new CardCollection();
|
||||||
for (int i = 0; i < left.size(); i++) {
|
for (int i = 0; i < attackersLeft.size(); i++) {
|
||||||
final Card attacker = left.get(i);
|
final Card attacker = attackersLeft.get(i);
|
||||||
if (aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike()
|
if (aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike()
|
||||||
&& ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, defendingOpponent)
|
&& ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, defendingOpponent)
|
||||||
>= ComputerUtilCombat.getDamageToKill(attacker, false)) {
|
>= ComputerUtilCombat.getDamageToKill(attacker, false)) {
|
||||||
@@ -1317,7 +1291,7 @@ public class AiAttackController {
|
|||||||
attackersAssigned.add(attacker);
|
attackersAssigned.add(attacker);
|
||||||
|
|
||||||
// check if attackers are enough to finish the attacked planeswalker
|
// check if attackers are enough to finish the attacked planeswalker
|
||||||
if (i < left.size() - 1 && defender instanceof Card card) {
|
if (i < attackersLeft.size() - 1 && defender instanceof Card) {
|
||||||
final int blockNum = this.blockers.size();
|
final int blockNum = this.blockers.size();
|
||||||
int attackNum = 0;
|
int attackNum = 0;
|
||||||
int damage = 0;
|
int damage = 0;
|
||||||
@@ -1331,19 +1305,19 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if enough damage: switch to next planeswalker
|
// if enough damage: switch to next planeswalker
|
||||||
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
|
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
left.removeAll(attackersAssigned);
|
attackersLeft.removeAll(attackersAssigned);
|
||||||
possibleDefenders.remove(defender);
|
possibleDefenders.remove(defender);
|
||||||
if (left.isEmpty() || possibleDefenders.isEmpty()) {
|
if (attackersLeft.isEmpty() || possibleDefenders.isEmpty()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
CardCollection pwDefending = new CardCollection(IterableUtil.filter(possibleDefenders, Card.class));
|
CardCollection pwDefending = new CardCollection(Iterables.filter(possibleDefenders, Card.class));
|
||||||
if (pwDefending.isEmpty()) {
|
if (pwDefending.isEmpty()) {
|
||||||
// TODO for now only looks at same player as we'd have to check the others from start too
|
// TODO for now only looks at same player as we'd have to check the others from start too
|
||||||
//defender = new PlayerCollection(Iterables.filter(possibleDefenders, Player.class)).min(PlayerPredicates.compareByLife());
|
//defender = new PlayerCollection(Iterables.filter(possibleDefenders, Player.class)).min(PlayerPredicates.compareByLife());
|
||||||
@@ -1357,49 +1331,74 @@ public class AiAttackController {
|
|||||||
return aiAggression;
|
return aiAggression;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SpellAbilityFactors {
|
/**
|
||||||
Card attacker = null;
|
* <p>
|
||||||
|
* shouldAttack.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param attacker
|
||||||
|
* a {@link forge.game.card.Card} object.
|
||||||
|
* @param defenders
|
||||||
|
* a object.
|
||||||
|
* @param combat
|
||||||
|
* a {@link forge.game.combat.Combat} object.
|
||||||
|
* @return a boolean.
|
||||||
|
*/
|
||||||
|
public final boolean shouldAttack(final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) {
|
||||||
boolean canBeKilled = false; // indicates if the attacker can be killed
|
boolean canBeKilled = false; // indicates if the attacker can be killed
|
||||||
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
|
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
|
||||||
boolean canKillAll = true; // indicates if the attacker can kill all single blockers
|
boolean canKillAll = true; // indicates if the attacker can kill all single blockers
|
||||||
boolean canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
|
boolean canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
|
||||||
boolean isWorthLessThanAllKillers = true;
|
boolean isWorthLessThanAllKillers = true;
|
||||||
boolean hasAttackEffect = false;
|
boolean canBeBlocked = false;
|
||||||
boolean hasCombatEffect = false;
|
|
||||||
boolean dangerousBlockersPresent = false;
|
|
||||||
boolean canTrampleOverDefenders = false;
|
|
||||||
int numberOfPossibleBlockers = 0;
|
int numberOfPossibleBlockers = 0;
|
||||||
int defPower = 0;
|
|
||||||
|
|
||||||
SpellAbilityFactors(Card c) {
|
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
|
||||||
attacker = c;
|
if (attacker.hasSVar("NonCombatPriority") && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
||||||
|
// For each level of priority, enemy has to have life as much as the creature's power
|
||||||
|
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
|
||||||
|
// the lower the priroity, the less willing the AI is to use the creature for attacking.
|
||||||
|
// TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?)
|
||||||
|
if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) {
|
||||||
|
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine!
|
||||||
|
for (SpellAbility sa : attacker.getSpellAbilities()) {
|
||||||
|
// Do not attack if we can afford using the ability.
|
||||||
|
if (sa.isActivatedAbility()) {
|
||||||
|
if (ComputerUtilCost.canPayCost(sa, ai, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// TODO Eventually The Ai will need to learn to predict if they have any use for the ability before next untap or not.
|
||||||
|
// TODO abilities that tap enemy creatures should probably only be saved if the enemy has nonzero creatures? Haste can be a threat though...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canBeBlocked() {
|
if (!isEffectiveAttacker(ai, attacker, combat, defender)) {
|
||||||
return numberOfPossibleBlockers > 2
|
return false;
|
||||||
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|
|
||||||
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent));
|
|
||||||
}
|
}
|
||||||
|
boolean hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasKeyword(Keyword.ANNIHILATOR);
|
||||||
private void calculate(final List<Card> defenders, final Combat combat) {
|
|
||||||
hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasKeyword(Keyword.ANNIHILATOR);
|
|
||||||
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
|
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
|
||||||
hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"))
|
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
||||||
|| attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT);
|
|
||||||
|
if (!hasCombatEffect) {
|
||||||
|
if (attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT)) {
|
||||||
|
hasCombatEffect = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// contains only the defender's blockers that can actually block the attacker
|
// contains only the defender's blockers that can actually block the attacker
|
||||||
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
|
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
|
||||||
|
|
||||||
canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, Card::getNetToughness);
|
boolean canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, Card::getNetToughness);
|
||||||
|
|
||||||
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
|
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
|
||||||
dangerousBlockersPresent = validBlockers.anyMatch(
|
boolean dangerousBlockersPresent = Iterables.any(validBlockers, Predicates.or(
|
||||||
CardPredicates.hasKeyword(Keyword.LIFELINK)
|
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
|
||||||
.or(Card::isWitherDamage)
|
CardPredicates.hasKeyword(Keyword.LIFELINK)));
|
||||||
);
|
|
||||||
|
|
||||||
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
|
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
|
||||||
defPower = CardLists.getTotalPower(validBlockers, null);
|
int defPower = CardLists.getTotalPower(validBlockers, true, false);
|
||||||
|
|
||||||
// look at the attacker in relation to the blockers to establish a
|
// look at the attacker in relation to the blockers to establish a
|
||||||
// number of factors about the attacking context that will be relevant
|
// number of factors about the attacking context that will be relevant
|
||||||
@@ -1422,9 +1421,11 @@ public class AiAttackController {
|
|||||||
// not record that it can't kill everything
|
// not record that it can't kill everything
|
||||||
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
|
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
|
||||||
canKillAll = false;
|
canKillAll = false;
|
||||||
|
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")) {
|
||||||
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")
|
canKillAllDangerous = false;
|
||||||
|| blocker.isWitherDamage() || blocker.hasKeyword(Keyword.LIFELINK)) {
|
} else {
|
||||||
|
if (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)
|
||||||
|
|| blocker.hasKeyword(Keyword.LIFELINK)) {
|
||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
// there is a creature that can survive an attack from this creature
|
// there is a creature that can survive an attack from this creature
|
||||||
// and combat will have negative effects
|
// and combat will have negative effects
|
||||||
@@ -1436,7 +1437,7 @@ public class AiAttackController {
|
|||||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
|
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
|
||||||
boolean attackerWillDie = defPower >= attacker.getNetToughness();
|
boolean attackerWillDie = defPower >= attacker.getNetToughness();
|
||||||
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
|
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
|
||||||
boolean noContributionToAttack = attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
|
boolean noContributionToAttack = this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
|
||||||
|
|
||||||
// We are attacking too recklessly if we can't kill a single blocker and:
|
// We are attacking too recklessly if we can't kill a single blocker and:
|
||||||
// - our creature will die for sure (chump attack)
|
// - our creature will die for sure (chump attack)
|
||||||
@@ -1449,8 +1450,8 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// performance-wise it doesn't seem worth it to check attackVigilance() instead (only includes a single niche card)
|
|
||||||
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
canBeKilled = true;
|
canBeKilled = true;
|
||||||
@@ -1461,70 +1462,28 @@ public class AiAttackController {
|
|||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
canBeKilled = true;
|
canBeKilled = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>
|
|
||||||
* shouldAttack.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param attacker
|
|
||||||
* a {@link forge.game.card.Card} object.
|
|
||||||
* @param defenders
|
|
||||||
* a object.
|
|
||||||
* @param combat
|
|
||||||
* a {@link forge.game.combat.Combat} object.
|
|
||||||
* @return a boolean.
|
|
||||||
*/
|
|
||||||
public final boolean shouldAttack(final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) {
|
|
||||||
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
|
|
||||||
if (attacker.hasSVar("NonCombatPriority") && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
|
||||||
// For each level of priority, enemy has to have life as much as the creature's power
|
|
||||||
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
|
|
||||||
// the lower the priroity, the less willing the AI is to use the creature for attacking.
|
|
||||||
// TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?)
|
|
||||||
if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) {
|
|
||||||
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine!
|
|
||||||
for (SpellAbility sa : attacker.getSpellAbilities()) {
|
|
||||||
// Do not attack if we can afford using the ability.
|
|
||||||
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) {
|
|
||||||
if (ComputerUtilCost.canPayCost(sa, ai, false)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// TODO Eventually The Ai will need to learn to predict if they have any use for the ability before next untap or not.
|
|
||||||
// TODO abilities that tap enemy creatures should probably only be saved if the enemy has nonzero creatures? Haste can be a threat though...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEffectiveAttacker(ai, attacker, combat, defender)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpellAbilityFactors saf = new SpellAbilityFactors(attacker);
|
|
||||||
if (aiAggression != 5) {
|
|
||||||
saf.calculate(defenders, combat);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the creature cannot block and can kill all opponents they might as
|
// if the creature cannot block and can kill all opponents they might as
|
||||||
// well attack, they do nothing staying back
|
// well attack, they do nothing staying back
|
||||||
if (saf.canKillAll && saf.isWorthLessThanAllKillers && !CombatUtil.canBlock(attacker)) {
|
if (canKillAll && isWorthLessThanAllKillers && !CombatUtil.canBlock(attacker)) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player");
|
System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player");
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (!canBeKilled && !dangerousBlockersPresent && canTrampleOverDefenders) {
|
||||||
if (!saf.canBeKilled && !saf.dangerousBlockersPresent && saf.canTrampleOverDefenders) {
|
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through");
|
System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (numberOfPossibleBlockers > 2
|
||||||
|
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|
||||||
|
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent))) {
|
||||||
|
canBeBlocked = true;
|
||||||
|
}
|
||||||
// decide if the creature should attack based on the prevailing strategy choice in aiAggression
|
// decide if the creature should attack based on the prevailing strategy choice in aiAggression
|
||||||
switch (aiAggression) {
|
switch (aiAggression) {
|
||||||
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
|
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
|
||||||
if ((saf.canKillAll && saf.isWorthLessThanAllKillers) || !saf.canBeBlocked()) {
|
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
|
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
|
||||||
return true;
|
return true;
|
||||||
@@ -1535,32 +1494,32 @@ public class AiAttackController {
|
|||||||
System.out.println(attacker.getName() + " = all out attacking");
|
System.out.println(attacker.getName() + " = all out attacking");
|
||||||
return true;
|
return true;
|
||||||
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
|
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
|
||||||
if (saf.canKillAll || (saf.dangerousBlockersPresent && saf.canKillAllDangerous && !saf.canBeKilledByOne) || !saf.canBeBlocked()
|
if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
|
||||||
|| saf.defPower == 0) {
|
|| (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
|
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3: // expecting to at least kill a creature of equal value or not be blocked
|
case 3: // expecting to at least kill a creature of equal value or not be blocked
|
||||||
if ((saf.canKillAll && saf.isWorthLessThanAllKillers)
|
if ((canKillAll && isWorthLessThanAllKillers)
|
||||||
|| (((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne)
|
|| (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|
||||||
|| !saf.canBeBlocked()) {
|
|| !canBeBlocked) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
|
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
|
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
|
||||||
if (!saf.canBeBlocked() || ((saf.canKillAll || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne &&
|
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
|
||||||
((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || !saf.canBeKilled))) {
|
((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
|
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1: // unblockable creatures only
|
case 1: // unblockable creatures only
|
||||||
if (!saf.canBeBlocked() || (saf.numberOfPossibleBlockers == 1 && saf.canKillAll && !saf.canBeKilledByOne)) {
|
if (!canBeBlocked || (numberOfPossibleBlockers == 1 && canKillAll && !canBeKilledByOne)) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
|
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
|
||||||
return true;
|
return true;
|
||||||
@@ -1590,7 +1549,7 @@ public class AiAttackController {
|
|||||||
// but there are no creatures it can target, no need to exert with it
|
// but there are no creatures it can target, no need to exert with it
|
||||||
boolean missTarget = false;
|
boolean missTarget = false;
|
||||||
for (StaticAbility st : c.getStaticAbilities()) {
|
for (StaticAbility st : c.getStaticAbilities()) {
|
||||||
if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
|
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
SpellAbility sa = st.getPayingTrigSA();
|
SpellAbility sa = st.getPayingTrigSA();
|
||||||
@@ -1612,12 +1571,12 @@ public class AiAttackController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
sa.setActivatingPlayer(c.getController());
|
sa.setActivatingPlayer(c.getController(), true);
|
||||||
List<Card> validTargets = CardUtil.getValidCardsToTarget(sa);
|
List<Card> validTargets = CardUtil.getValidCardsToTarget(sa);
|
||||||
if (validTargets.isEmpty()) {
|
if (validTargets.isEmpty()) {
|
||||||
missTarget = true;
|
missTarget = true;
|
||||||
break;
|
break;
|
||||||
} else if (sa.isCurse() && validTargets.stream().noneMatch(
|
} else if (sa.isCurse() && !Iterables.any(validTargets,
|
||||||
CardPredicates.isControlledByAnyOf(c.getController().getOpponents()))) {
|
CardPredicates.isControlledByAnyOf(c.getController().getOpponents()))) {
|
||||||
// e.g. Ahn-Crop Crasher - the effect is only good when aimed at opponent's creatures
|
// e.g. Ahn-Crop Crasher - the effect is only good when aimed at opponent's creatures
|
||||||
missTarget = true;
|
missTarget = true;
|
||||||
@@ -1724,7 +1683,7 @@ public class AiAttackController {
|
|||||||
return null; //should never get here
|
return null; //should never get here
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doLightmineFieldAttackLogic(final Queue<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
|
private void doLightmineFieldAttackLogic(final List<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
|
||||||
CardCollection attSorted = new CardCollection(attackersLeft);
|
CardCollection attSorted = new CardCollection(attackersLeft);
|
||||||
CardCollection attUnsafe = new CardCollection();
|
CardCollection attUnsafe = new CardCollection();
|
||||||
CardLists.sortByToughnessDesc(attSorted);
|
CardLists.sortByToughnessDesc(attSorted);
|
||||||
@@ -1754,15 +1713,13 @@ public class AiAttackController {
|
|||||||
attackersLeft.removeAll(attUnsafe);
|
attackersLeft.removeAll(attUnsafe);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
|
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final List<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
|
||||||
// TODO: detect Revenge of Ravens by the trigger instead of by name
|
// TODO: detect Revenge of Ravens by the trigger instead of by name
|
||||||
boolean revengeOfRavens = false;
|
boolean revengeOfRavens = false;
|
||||||
if (defender instanceof Player player) {
|
if (defender instanceof Player) {
|
||||||
revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
|
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||||
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
} else if (defender instanceof Card) {
|
||||||
} else if (defender instanceof Card card) {
|
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||||
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
|
|
||||||
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!revengeOfRavens) {
|
if (!revengeOfRavens) {
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
|
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
@@ -161,12 +163,12 @@ public class AiBlockController {
|
|||||||
// defend battles with fewer defense counters before battles with more defense counters,
|
// defend battles with fewer defense counters before battles with more defense counters,
|
||||||
// if planeswalker/battle will be too difficult to defend don't even bother
|
// if planeswalker/battle will be too difficult to defend don't even bother
|
||||||
for (GameEntity defender : defenders) {
|
for (GameEntity defender : defenders) {
|
||||||
if ((defender instanceof Card card1 && card1.getController().equals(ai))
|
if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|
||||||
|| (defender instanceof Card card2 && card2.isBattle() && card2.getProtectingPlayer().equals(ai))) {
|
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
|
||||||
final CardCollection ccAttackers = combat.getAttackersOf(defender);
|
final CardCollection attackers = combat.getAttackersOf(defender);
|
||||||
// Begin with the attackers that pose the biggest threat
|
// Begin with the attackers that pose the biggest threat
|
||||||
CardLists.sortByPowerDesc(ccAttackers);
|
CardLists.sortByPowerDesc(attackers);
|
||||||
sortedAttackers.addAll(ccAttackers);
|
sortedAttackers.addAll(attackers);
|
||||||
} else if (defender instanceof Player && defender.equals(ai)) {
|
} else if (defender instanceof Player && defender.equals(ai)) {
|
||||||
firstAttacker = combat.getAttackersOf(defender);
|
firstAttacker = combat.getAttackersOf(defender);
|
||||||
CardLists.sortByPowerDesc(firstAttacker);
|
CardLists.sortByPowerDesc(firstAttacker);
|
||||||
@@ -325,7 +327,7 @@ public class AiBlockController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<Card> rampagesOrNeedsManyToBlock(final Combat combat) {
|
private Predicate<Card> rampagesOrNeedsManyToBlock(final Combat combat) {
|
||||||
return CardPredicates.hasKeyword(Keyword.RAMPAGE).or(input -> {
|
return Predicates.or(CardPredicates.hasKeyword(Keyword.RAMPAGE), input -> {
|
||||||
// select creature that has a max blocker
|
// select creature that has a max blocker
|
||||||
return StaticAbilityCantAttackBlock.getMinMaxBlocker(input, combat.getDefenderPlayerByAttacker(input)).getRight() < Integer.MAX_VALUE;
|
return StaticAbilityCantAttackBlock.getMinMaxBlocker(input, combat.getDefenderPlayerByAttacker(input)).getRight() < Integer.MAX_VALUE;
|
||||||
});
|
});
|
||||||
@@ -366,7 +368,7 @@ public class AiBlockController {
|
|||||||
* @param combat a {@link forge.game.combat.Combat} object.
|
* @param combat a {@link forge.game.combat.Combat} object.
|
||||||
*/
|
*/
|
||||||
private void makeGangBlocks(final Combat combat) {
|
private void makeGangBlocks(final Combat combat) {
|
||||||
List<Card> currentAttackers = CardLists.filter(attackersLeft, rampagesOrNeedsManyToBlock(combat).negate());
|
List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
|
||||||
List<Card> blockers;
|
List<Card> blockers;
|
||||||
|
|
||||||
// Try to block an attacker without first strike with a gang of first strikers
|
// Try to block an attacker without first strike with a gang of first strikers
|
||||||
@@ -738,11 +740,11 @@ public class AiBlockController {
|
|||||||
List<Card> chumpBlockers;
|
List<Card> chumpBlockers;
|
||||||
|
|
||||||
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
|
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
|
||||||
tramplingAttackers = CardLists.filter(tramplingAttackers, rampagesOrNeedsManyToBlock(combat).negate());
|
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
|
||||||
|
|
||||||
// TODO - Instead of filtering out rampage-like and similar triggers, make the AI properly count P/T and
|
// TODO - Instead of filtering out rampage-like and similar triggers, make the AI properly count P/T and
|
||||||
// reinforce when actually possible without losing material.
|
// reinforce when actually possible without losing material.
|
||||||
tramplingAttackers = CardLists.filter(tramplingAttackers, changesPTWhenBlocked(true).negate());
|
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(changesPTWhenBlocked(true)));
|
||||||
|
|
||||||
for (final Card attacker : tramplingAttackers) {
|
for (final Card attacker : tramplingAttackers) {
|
||||||
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()) {
|
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()) {
|
||||||
@@ -751,6 +753,10 @@ public class AiBlockController {
|
|||||||
|
|
||||||
boolean needsMoreChumpBlockers = true;
|
boolean needsMoreChumpBlockers = true;
|
||||||
|
|
||||||
|
// See if it's possible to tank up the damage with Banding
|
||||||
|
List<String> bandsWithString = Arrays.asList("Bands with Other Legendary Creatures",
|
||||||
|
"Bands with Other Creatures named Wolves of the Hunt",
|
||||||
|
"Bands with Other Dinosaurs");
|
||||||
if (AttackingBand.isValidBand(combat.getBlockers(attacker), true)) {
|
if (AttackingBand.isValidBand(combat.getBlockers(attacker), true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -760,7 +766,7 @@ public class AiBlockController {
|
|||||||
|
|
||||||
// See if there's a Banding blocker that can tank the damage
|
// See if there's a Banding blocker that can tank the damage
|
||||||
for (final Card blocker : chumpBlockers) {
|
for (final Card blocker : chumpBlockers) {
|
||||||
if (blocker.hasKeyword(Keyword.BANDING) || blocker.hasKeyword(Keyword.BANDSWITH)) {
|
if (blocker.hasKeyword(Keyword.BANDING) || blocker.hasAnyKeyword(bandsWithString)) {
|
||||||
if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker))
|
if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker))
|
||||||
&& ComputerUtilCombat.shieldDamage(attacker, blocker) > 0
|
&& ComputerUtilCombat.shieldDamage(attacker, blocker) > 0
|
||||||
&& CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
&& CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||||
@@ -793,11 +799,11 @@ public class AiBlockController {
|
|||||||
private void reinforceBlockersToKill(final Combat combat) {
|
private void reinforceBlockersToKill(final Combat combat) {
|
||||||
List<Card> safeBlockers;
|
List<Card> safeBlockers;
|
||||||
List<Card> blockers;
|
List<Card> blockers;
|
||||||
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, rampagesOrNeedsManyToBlock(combat).negate());
|
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
|
||||||
|
|
||||||
// TODO - Instead of filtering out rampage-like and similar triggers, make the AI properly count P/T and
|
// TODO - Instead of filtering out rampage-like and similar triggers, make the AI properly count P/T and
|
||||||
// reinforce when actually possible without losing material.
|
// reinforce when actually possible without losing material.
|
||||||
targetAttackers = CardLists.filter(targetAttackers, changesPTWhenBlocked(false).negate());
|
targetAttackers = CardLists.filter(targetAttackers, Predicates.not(changesPTWhenBlocked(false)));
|
||||||
|
|
||||||
for (final Card attacker : targetAttackers) {
|
for (final Card attacker : targetAttackers) {
|
||||||
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
|
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
|
||||||
@@ -872,9 +878,9 @@ public class AiBlockController {
|
|||||||
CardCollection threatenedPWs = new CardCollection();
|
CardCollection threatenedPWs = new CardCollection();
|
||||||
for (final Card attacker : attackers) {
|
for (final Card attacker : attackers) {
|
||||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||||
if (def instanceof Card card) {
|
if (def instanceof Card) {
|
||||||
if (!onlyIfLethal) {
|
if (!onlyIfLethal) {
|
||||||
threatenedPWs.add(card);
|
threatenedPWs.add((Card) def);
|
||||||
} else {
|
} else {
|
||||||
int damageToPW = 0;
|
int damageToPW = 0;
|
||||||
for (final Card pwatkr : combat.getAttackersOf(def)) {
|
for (final Card pwatkr : combat.getAttackersOf(def)) {
|
||||||
@@ -906,12 +912,12 @@ public class AiBlockController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||||
if (def instanceof Card card && threatenedPWs.contains(def)) {
|
if (def instanceof Card && threatenedPWs.contains(def)) {
|
||||||
Card blockerDecided = null;
|
Card blockerDecided = null;
|
||||||
for (final Card blocker : chumpPWDefenders) {
|
for (final Card blocker : chumpPWDefenders) {
|
||||||
if (CombatUtil.canBlock(attacker, blocker, combat)) {
|
if (CombatUtil.canBlock(attacker, blocker, combat)) {
|
||||||
combat.addBlocker(attacker, blocker);
|
combat.addBlocker(attacker, blocker);
|
||||||
pwsWithChumpBlocks.add(card);
|
pwsWithChumpBlocks.add((Card) def);
|
||||||
chosenChumpBlockers.add(blocker);
|
chosenChumpBlockers.add(blocker);
|
||||||
blockerDecided = blocker;
|
blockerDecided = blocker;
|
||||||
blockersLeft.remove(blocker);
|
blockersLeft.remove(blocker);
|
||||||
@@ -1343,11 +1349,11 @@ public class AiBlockController {
|
|||||||
boolean creatureParityOrAllowedDiff = aiCreatureCount
|
boolean creatureParityOrAllowedDiff = aiCreatureCount
|
||||||
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
|
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
|
||||||
boolean wantToTradeWithCreatInHand = !checkingOther && randomTradeIfCreatInHand
|
boolean wantToTradeWithCreatInHand = !checkingOther && randomTradeIfCreatInHand
|
||||||
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES)
|
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.Presets.CREATURES)
|
||||||
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
|
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
|
||||||
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
|
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
|
||||||
&& combat.getDefenderByAttacker(attacker) instanceof Card card
|
&& combat.getDefenderByAttacker(attacker) instanceof Card
|
||||||
&& card.isPlaneswalker();
|
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
|
||||||
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
|
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
|
||||||
|
|
||||||
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
|
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
|
||||||
|
|||||||
@@ -18,17 +18,12 @@
|
|||||||
|
|
||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import com.google.common.base.Supplier;
|
|
||||||
import com.google.common.base.Suppliers;
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* AiCardMemory class.
|
* AiCardMemory class.
|
||||||
@@ -59,6 +54,7 @@ public class AiCardMemory {
|
|||||||
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
||||||
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||||
BOUNCED_THIS_TURN, // These cards were bounced this turn
|
BOUNCED_THIS_TURN, // These cards were bounced this turn
|
||||||
|
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
|
||||||
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
|
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
|
||||||
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
|
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
|
||||||
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part
|
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part
|
||||||
@@ -66,13 +62,75 @@ public class AiCardMemory {
|
|||||||
REVEALED_CARDS // These cards were recently revealed to the AI by a call to PlayerControllerAi.reveal
|
REVEALED_CARDS // These cards were recently revealed to the AI by a call to PlayerControllerAi.reveal
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Supplier<Map<MemorySet, Set<Card>>> memoryMap = Suppliers.memoize(Maps::newConcurrentMap);
|
private final Set<Card> memMandatoryAttackers;
|
||||||
|
private final Set<Card> memTrickAttackers;
|
||||||
|
private final Set<Card> memHeldManaSources;
|
||||||
|
private final Set<Card> memHeldManaSourcesForCombat;
|
||||||
|
private final Set<Card> memHeldManaSourcesForEnemyCombat;
|
||||||
|
private final Set<Card> memHeldManaSourcesForNextSpell;
|
||||||
|
private final Set<Card> memAttachedThisTurn;
|
||||||
|
private final Set<Card> memAnimatedThisTurn;
|
||||||
|
private final Set<Card> memBouncedThisTurn;
|
||||||
|
private final Set<Card> memActivatedThisTurn;
|
||||||
|
private final Set<Card> memChosenFogEffect;
|
||||||
|
private final Set<Card> memMarkedToAvoidReentry;
|
||||||
|
private final Set<Card> memPaysTapCost;
|
||||||
|
private final Set<Card> memPaysSacCost;
|
||||||
|
private final Set<Card> memRevealedCards;
|
||||||
|
|
||||||
public AiCardMemory() {
|
public AiCardMemory() {
|
||||||
|
this.memMandatoryAttackers = new HashSet<>();
|
||||||
|
this.memHeldManaSources = new HashSet<>();
|
||||||
|
this.memHeldManaSourcesForCombat = new HashSet<>();
|
||||||
|
this.memHeldManaSourcesForEnemyCombat = new HashSet<>();
|
||||||
|
this.memAttachedThisTurn = new HashSet<>();
|
||||||
|
this.memAnimatedThisTurn = new HashSet<>();
|
||||||
|
this.memBouncedThisTurn = new HashSet<>();
|
||||||
|
this.memActivatedThisTurn = new HashSet<>();
|
||||||
|
this.memTrickAttackers = new HashSet<>();
|
||||||
|
this.memChosenFogEffect = new HashSet<>();
|
||||||
|
this.memMarkedToAvoidReentry = new HashSet<>();
|
||||||
|
this.memHeldManaSourcesForNextSpell = new HashSet<>();
|
||||||
|
this.memPaysTapCost = new HashSet<>();
|
||||||
|
this.memPaysSacCost = new HashSet<>();
|
||||||
|
this.memRevealedCards = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Card> getMemorySet(MemorySet set) {
|
private Set<Card> getMemorySet(MemorySet set) {
|
||||||
return memoryMap.get().computeIfAbsent(set, value -> Sets.newConcurrentHashSet());
|
switch (set) {
|
||||||
|
case MANDATORY_ATTACKERS:
|
||||||
|
return memMandatoryAttackers;
|
||||||
|
case TRICK_ATTACKERS:
|
||||||
|
return memTrickAttackers;
|
||||||
|
case HELD_MANA_SOURCES_FOR_MAIN2:
|
||||||
|
return memHeldManaSources;
|
||||||
|
case HELD_MANA_SOURCES_FOR_DECLBLK:
|
||||||
|
return memHeldManaSourcesForCombat;
|
||||||
|
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
|
||||||
|
return memHeldManaSourcesForEnemyCombat;
|
||||||
|
case HELD_MANA_SOURCES_FOR_NEXT_SPELL:
|
||||||
|
return memHeldManaSourcesForNextSpell;
|
||||||
|
case ATTACHED_THIS_TURN:
|
||||||
|
return memAttachedThisTurn;
|
||||||
|
case ANIMATED_THIS_TURN:
|
||||||
|
return memAnimatedThisTurn;
|
||||||
|
case BOUNCED_THIS_TURN:
|
||||||
|
return memBouncedThisTurn;
|
||||||
|
case ACTIVATED_THIS_TURN:
|
||||||
|
return memActivatedThisTurn;
|
||||||
|
case CHOSEN_FOG_EFFECT:
|
||||||
|
return memChosenFogEffect;
|
||||||
|
case MARKED_TO_AVOID_REENTRY:
|
||||||
|
return memMarkedToAvoidReentry;
|
||||||
|
case PAYS_TAP_COST:
|
||||||
|
return memPaysTapCost;
|
||||||
|
case PAYS_SAC_COST:
|
||||||
|
return memPaysSacCost;
|
||||||
|
case REVEALED_CARDS:
|
||||||
|
return memRevealedCards;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,7 +145,10 @@ public class AiCardMemory {
|
|||||||
if (c == null) {
|
if (c == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return getMemorySet(set).contains(c);
|
|
||||||
|
Set<Card> memorySet = getMemorySet(set);
|
||||||
|
|
||||||
|
return memorySet != null && memorySet.contains(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +160,17 @@ public class AiCardMemory {
|
|||||||
* @return true, if at least one card with the given name is remembered in the given memory set
|
* @return true, if at least one card with the given name is remembered in the given memory set
|
||||||
*/
|
*/
|
||||||
public boolean isRememberedCardByName(String cardName, MemorySet set) {
|
public boolean isRememberedCardByName(String cardName, MemorySet set) {
|
||||||
return getMemorySet(set).stream().anyMatch(c -> c.getName().equals(cardName));
|
Set<Card> memorySet = getMemorySet(set);
|
||||||
|
|
||||||
|
if (memorySet != null) {
|
||||||
|
for (Card c : memorySet) {
|
||||||
|
if (c.getName().equals(cardName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +184,17 @@ public class AiCardMemory {
|
|||||||
* @return true, if at least one card with the given name is remembered in the given memory set
|
* @return true, if at least one card with the given name is remembered in the given memory set
|
||||||
*/
|
*/
|
||||||
public boolean isRememberedCardByName(String cardName, MemorySet set, Player owner) {
|
public boolean isRememberedCardByName(String cardName, MemorySet set, Player owner) {
|
||||||
return getMemorySet(set).stream().anyMatch(c -> c.getName().equals(cardName) && c.getOwner().equals(owner));
|
Set<Card> memorySet = getMemorySet(set);
|
||||||
|
|
||||||
|
if (memorySet != null) {
|
||||||
|
for (Card c : memorySet) {
|
||||||
|
if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,7 +207,14 @@ public class AiCardMemory {
|
|||||||
public boolean rememberCard(Card c, MemorySet set) {
|
public boolean rememberCard(Card c, MemorySet set) {
|
||||||
if (c == null)
|
if (c == null)
|
||||||
return false;
|
return false;
|
||||||
return getMemorySet(set).add(c);
|
|
||||||
|
Set<Card> memorySet = getMemorySet(set);
|
||||||
|
|
||||||
|
if (memorySet != null) {
|
||||||
|
memorySet.add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,7 +231,14 @@ public class AiCardMemory {
|
|||||||
if (!isRememberedCard(c, set)) {
|
if (!isRememberedCard(c, set)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return getMemorySet(set).remove(c);
|
|
||||||
|
Set<Card> memorySet = getMemorySet(set);
|
||||||
|
|
||||||
|
if (memorySet != null) {
|
||||||
|
memorySet.remove(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,11 +249,16 @@ public class AiCardMemory {
|
|||||||
* @return true, if at least one card with the given name was previously remembered in the given memory set and was successfully forgotten
|
* @return true, if at least one card with the given name was previously remembered in the given memory set and was successfully forgotten
|
||||||
*/
|
*/
|
||||||
public boolean forgetAnyCardWithName(String cardName, MemorySet set) {
|
public boolean forgetAnyCardWithName(String cardName, MemorySet set) {
|
||||||
for (Card c : getMemorySet(set)) {
|
Set<Card> memorySet = getMemorySet(set);
|
||||||
|
|
||||||
|
if (memorySet != null) {
|
||||||
|
for (Card c : memorySet) {
|
||||||
if (c.getName().equals(cardName)) {
|
if (c.getName().equals(cardName)) {
|
||||||
return forgetCard(c, set);
|
return forgetCard(c, set);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,11 +271,16 @@ public class AiCardMemory {
|
|||||||
* @return true, if at least one card with the given name was previously remembered in the given memory set and was successfully forgotten
|
* @return true, if at least one card with the given name was previously remembered in the given memory set and was successfully forgotten
|
||||||
*/
|
*/
|
||||||
public boolean forgetAnyCardWithName(String cardName, MemorySet set, Player owner) {
|
public boolean forgetAnyCardWithName(String cardName, MemorySet set, Player owner) {
|
||||||
for (Card c : getMemorySet(set)) {
|
Set<Card> memorySet = getMemorySet(set);
|
||||||
|
|
||||||
|
if (memorySet != null) {
|
||||||
|
for (Card c : memorySet) {
|
||||||
if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
|
if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
|
||||||
return forgetCard(c, set);
|
return forgetCard(c, set);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.AiCardMemory.MemorySet;
|
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -33,10 +32,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
|
|
||||||
discarded = new CardCollection();
|
discarded = new CardCollection();
|
||||||
tapped = new CardCollection();
|
tapped = new CardCollection();
|
||||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
|
|
||||||
if (tappedForMana != null) {
|
|
||||||
tapped.addAll(tappedForMana);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -46,14 +41,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
return PaymentDecision.number(c);
|
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
|
@Override
|
||||||
public PaymentDecision visit(CostChooseColor cost) {
|
public PaymentDecision visit(CostChooseColor cost) {
|
||||||
int c = cost.getAbilityAmount(ability);
|
int c = cost.getAbilityAmount(ability);
|
||||||
@@ -64,7 +51,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PaymentDecision visit(CostChooseCreatureType cost) {
|
public PaymentDecision visit(CostChooseCreatureType cost) {
|
||||||
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes());
|
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(),
|
||||||
|
Lists.newArrayList());
|
||||||
return PaymentDecision.type(choice);
|
return PaymentDecision.type(choice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,13 +105,13 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
Card chosen;
|
Card chosen;
|
||||||
if (!discardMe.isEmpty()) {
|
if (!discardMe.isEmpty()) {
|
||||||
chosen = Aggregates.random(discardMe);
|
chosen = Aggregates.random(discardMe);
|
||||||
discardMe = CardLists.filter(discardMe, CardPredicates.sharesNameWith(chosen).negate());
|
discardMe = CardLists.filter(discardMe, Predicates.not(CardPredicates.sharesNameWith(chosen)));
|
||||||
} else {
|
} else {
|
||||||
final Card worst = ComputerUtilCard.getWorstAI(hand);
|
final Card worst = ComputerUtilCard.getWorstAI(hand);
|
||||||
chosen = worst != null ? worst : Aggregates.random(hand);
|
chosen = worst != null ? worst : Aggregates.random(hand);
|
||||||
}
|
}
|
||||||
differentNames.add(chosen);
|
differentNames.add(chosen);
|
||||||
hand = CardLists.filter(hand, CardPredicates.sharesNameWith(chosen).negate());
|
hand = CardLists.filter(hand, Predicates.not(CardPredicates.sharesNameWith(chosen)));
|
||||||
c--;
|
c--;
|
||||||
}
|
}
|
||||||
return PaymentDecision.card(differentNames);
|
return PaymentDecision.card(differentNames);
|
||||||
@@ -452,6 +440,21 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("DontPayTapCostWithManaSources".equals(source.getSVar("AIPaymentPreference"))) {
|
||||||
|
CardCollectionView toExclude =
|
||||||
|
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"),
|
||||||
|
ability.getActivatingPlayer(), ability.getHostCard(), ability);
|
||||||
|
toExclude = CardLists.filter(toExclude, card -> {
|
||||||
|
for (final SpellAbility sa : card.getSpellAbilities()) {
|
||||||
|
if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
exclude.addAll(toExclude);
|
||||||
|
}
|
||||||
|
|
||||||
String totalP = "";
|
String totalP = "";
|
||||||
CardCollectionView totap;
|
CardCollectionView totap;
|
||||||
if (isVehicle) {
|
if (isVehicle) {
|
||||||
@@ -648,7 +651,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
// TODO sort negatives to remove from best Cards first?
|
// TODO sort negatives to remove from best Cards first?
|
||||||
for (final Card crd : negatives) {
|
for (final Card crd : negatives) {
|
||||||
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(crd).entrySet()) {
|
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(crd).entrySet()) {
|
||||||
if (ComputerUtil.isNegativeCounter(e.getKey(), crd) && crd.canRemoveCounters(e.getKey())) {
|
if (ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
|
||||||
int over = Math.min(e.getValue(), c - toRemove);
|
int over = Math.min(e.getValue(), c - toRemove);
|
||||||
if (over > 0) {
|
if (over > 0) {
|
||||||
toRemove += over;
|
toRemove += over;
|
||||||
@@ -759,7 +762,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if table is empty, then no counter was removed
|
// if table is empty, than no counter was removed
|
||||||
return table.isEmpty() ? null : PaymentDecision.counters(table);
|
return table.isEmpty() ? null : PaymentDecision.counters(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,12 +770,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
public PaymentDecision visit(CostRemoveCounter cost) {
|
public PaymentDecision visit(CostRemoveCounter cost) {
|
||||||
final String amount = cost.getAmount();
|
final String amount = cost.getAmount();
|
||||||
final String type = cost.getType();
|
final String type = cost.getType();
|
||||||
final GameEntityCounterTable counterTable = new GameEntityCounterTable();
|
|
||||||
|
|
||||||
// TODO Help AI filter card with most useless counters and put those counters in countertable for things like
|
|
||||||
// Moxite Refinery, similar to CostRemoveAnyCounter
|
|
||||||
// Probably a lot of that decision making can be re-used or pulled out for both PaymentDecisions to use
|
|
||||||
if (cost.counter == null) return null;
|
|
||||||
|
|
||||||
int c;
|
int c;
|
||||||
|
|
||||||
@@ -801,8 +798,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
}
|
}
|
||||||
for (Card card : typeList) {
|
for (Card card : typeList) {
|
||||||
if (card.getCounters(cost.counter) >= c) {
|
if (card.getCounters(cost.counter) >= c) {
|
||||||
counterTable.put(null, card, cost.counter, c);
|
return PaymentDecision.card(card, c);
|
||||||
return PaymentDecision.counters(counterTable);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -813,8 +809,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
counterTable.put(null, source, cost.counter, c);
|
return PaymentDecision.card(source, c);
|
||||||
return PaymentDecision.counters(counterTable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -843,12 +838,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PaymentDecision visit(CostUnattach cost) {
|
public PaymentDecision visit(CostUnattach cost) {
|
||||||
final CardCollection cardToUnattach = cost.findCardToUnattach(source, player, ability);
|
final Card cardToUnattach = cost.findCardToUnattach(source, player, ability);
|
||||||
if (cardToUnattach.isEmpty()) {
|
if (cardToUnattach == null) {
|
||||||
// We really shouldn't be able to get here if there's nothing to unattach
|
// We really shouldn't be able to get here if there's nothing to unattach
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return PaymentDecision.card(cardToUnattach.getFirst());
|
return PaymentDecision.card(cardToUnattach);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,52 +1,21 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
public enum AiPlayDecision {
|
public enum AiPlayDecision {
|
||||||
// Play decision reasons
|
|
||||||
WillPlay,
|
WillPlay,
|
||||||
MandatoryPlay,
|
|
||||||
PlayToEmptyHand,
|
|
||||||
ImpactCombat,
|
|
||||||
ResponseToStackResolve,
|
|
||||||
AddBoardPresence,
|
|
||||||
Removal,
|
|
||||||
Tempo,
|
|
||||||
CardAdvantage,
|
|
||||||
|
|
||||||
// Play later decisions
|
|
||||||
WaitForCombat,
|
|
||||||
WaitForMain2,
|
|
||||||
WaitForEndOfTurn,
|
|
||||||
StackNotEmpty,
|
|
||||||
AnotherTime,
|
|
||||||
|
|
||||||
// Don't play decision reasons
|
|
||||||
CantPlaySa,
|
CantPlaySa,
|
||||||
CantPlayAi,
|
CantPlayAi,
|
||||||
CantAfford,
|
CantAfford,
|
||||||
CantAffordX,
|
CantAffordX,
|
||||||
DoesntImpactCombat,
|
WaitForMain2,
|
||||||
DoesntImpactGame,
|
AnotherTime,
|
||||||
MissingLogic,
|
|
||||||
MissingNeededCards,
|
MissingNeededCards,
|
||||||
TimingRestrictions,
|
|
||||||
MissingPhaseRestrictions,
|
|
||||||
ConditionsNotMet,
|
|
||||||
NeedsToPlayCriteriaNotMet,
|
NeedsToPlayCriteriaNotMet,
|
||||||
StopRunawayActivations,
|
|
||||||
TargetingFailed,
|
TargetingFailed,
|
||||||
CostNotAcceptable,
|
CostNotAcceptable,
|
||||||
LifeInDanger,
|
|
||||||
WouldDestroyLegend,
|
WouldDestroyLegend,
|
||||||
WouldDestroyOtherPlaneswalker,
|
WouldDestroyOtherPlaneswalker,
|
||||||
WouldBecomeZeroToughnessCreature,
|
WouldBecomeZeroToughnessCreature,
|
||||||
WouldDestroyWorldEnchantment,
|
WouldDestroyWorldEnchantment,
|
||||||
BadEtbEffects,
|
BadEtbEffects,
|
||||||
CurseEffects;
|
CurseEffects
|
||||||
|
|
||||||
public boolean willingToPlay() {
|
|
||||||
return switch (this) {
|
|
||||||
case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true;
|
|
||||||
default -> false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -17,18 +17,19 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import forge.LobbyPlayer;
|
|
||||||
import forge.util.Aggregates;
|
|
||||||
import forge.util.FileUtil;
|
|
||||||
import forge.util.TextUtil;
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
|
import forge.LobbyPlayer;
|
||||||
|
import forge.util.Aggregates;
|
||||||
|
import forge.util.FileUtil;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds default AI personality profile values in an enum.
|
* Holds default AI personality profile values in an enum.
|
||||||
* Loads profile from the given text file when setProfile is called.
|
* Loads profile from the given text file when setProfile is called.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import forge.game.card.Card;
|
|||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
import forge.game.cost.CostPayEnergy;
|
import forge.game.cost.CostPayEnergy;
|
||||||
import forge.game.cost.CostPutCounter;
|
import forge.game.cost.CostPutCounter;
|
||||||
@@ -21,7 +22,6 @@ import forge.game.cost.CostRemoveCounter;
|
|||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.OptionalCost;
|
|
||||||
import forge.game.spellability.OptionalCostValue;
|
import forge.game.spellability.OptionalCostValue;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
@@ -32,14 +32,20 @@ public class ComputerUtilAbility {
|
|||||||
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
|
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
CardCollection landList = new CardCollection(player.getCardsIn(ZoneType.Hand));
|
final CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand));
|
||||||
|
hand.addAll(player.getCardsIn(ZoneType.Exile));
|
||||||
|
CardCollection landList = CardLists.filter(hand, Presets.LANDS);
|
||||||
|
|
||||||
//filter out cards that can't be played
|
//filter out cards that can't be played
|
||||||
landList = CardLists.filter(landList, c -> {
|
landList = CardLists.filter(landList, c -> {
|
||||||
if (!c.hasPlayableLandFace()) {
|
if (!c.getSVar("NeedsToPlay").isEmpty()) {
|
||||||
|
final String needsToPlay = c.getSVar("NeedsToPlay");
|
||||||
|
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), needsToPlay, c.getController(), c, null);
|
||||||
|
if (list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return player.canPlayLand(c, false, c.getFirstSpellAbility());
|
}
|
||||||
|
return player.canPlayLand(c);
|
||||||
});
|
});
|
||||||
|
|
||||||
final CardCollection landsNotInHand = new CardCollection(player.getCardsIn(ZoneType.Graveyard));
|
final CardCollection landsNotInHand = new CardCollection(player.getCardsIn(ZoneType.Graveyard));
|
||||||
@@ -48,7 +54,7 @@ public class ComputerUtilAbility {
|
|||||||
landsNotInHand.add(player.getCardsIn(ZoneType.Library).get(0));
|
landsNotInHand.add(player.getCardsIn(ZoneType.Library).get(0));
|
||||||
}
|
}
|
||||||
for (final Card crd : landsNotInHand) {
|
for (final Card crd : landsNotInHand) {
|
||||||
if (!(crd.hasPlayableLandFace() || (crd.isFaceDown() && crd.getState(CardStateName.Original).getType().isLand()))) {
|
if (!(crd.isLand() || (crd.isFaceDown() && crd.getState(CardStateName.Original).getType().isLand()))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!crd.mayPlay(player).isEmpty()) {
|
if (!crd.mayPlay(player).isEmpty()) {
|
||||||
@@ -90,7 +96,7 @@ public class ComputerUtilAbility {
|
|||||||
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
|
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
|
||||||
for (SpellAbility sa : originList) {
|
for (SpellAbility sa : originList) {
|
||||||
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
|
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player, true);
|
||||||
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
|
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,16 +123,12 @@ public class ComputerUtilAbility {
|
|||||||
|
|
||||||
final List<SpellAbility> result = Lists.newArrayList();
|
final List<SpellAbility> result = Lists.newArrayList();
|
||||||
for (SpellAbility sa : newAbilities) {
|
for (SpellAbility sa : newAbilities) {
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player, true);
|
||||||
|
|
||||||
// Optional cost selection through the AI controller
|
// Optional cost selection through the AI controller
|
||||||
boolean choseOptCost = false;
|
boolean choseOptCost = false;
|
||||||
List<OptionalCostValue> list = GameActionUtil.getOptionalCostValues(sa);
|
List<OptionalCostValue> list = GameActionUtil.getOptionalCostValues(sa);
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
// still add base spell in case of Promise Gift
|
|
||||||
if (list.stream().anyMatch(ocv -> ocv.getType().equals(OptionalCost.PromiseGift))) {
|
|
||||||
result.add(sa);
|
|
||||||
}
|
|
||||||
list = player.getController().chooseOptionalCosts(sa, list);
|
list = player.getController().chooseOptionalCosts(sa, list);
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
choseOptCost = true;
|
choseOptCost = true;
|
||||||
@@ -345,10 +347,6 @@ public class ComputerUtilAbility {
|
|||||||
if (source.hasSVar("AIPriorityModifier")) {
|
if (source.hasSVar("AIPriorityModifier")) {
|
||||||
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
||||||
}
|
}
|
||||||
// try to use it before it's gone
|
|
||||||
if (source.isInPlay() && source.hasSVar("EndOfTurnLeavePlay")) {
|
|
||||||
p += 1;
|
|
||||||
}
|
|
||||||
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
|
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
|
||||||
p -= 10;
|
p -= 10;
|
||||||
}
|
}
|
||||||
@@ -362,7 +360,7 @@ public class ComputerUtilAbility {
|
|||||||
}
|
}
|
||||||
// 1. increase chance of using Surge effects
|
// 1. increase chance of using Surge effects
|
||||||
// 2. non-surged versions are usually inefficient
|
// 2. non-surged versions are usually inefficient
|
||||||
if (source.hasKeyword(Keyword.SURGE) && !sa.isSurged()) {
|
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||||
p -= 9;
|
p -= 9;
|
||||||
}
|
}
|
||||||
// move snap-casted spells to front
|
// move snap-casted spells to front
|
||||||
@@ -395,10 +393,8 @@ public class ComputerUtilAbility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ApiType.DestroyAll == sa.getApi()) {
|
if (ApiType.DestroyAll == sa.getApi()) {
|
||||||
// check boardwipe earlier
|
|
||||||
p += 4;
|
p += 4;
|
||||||
} else if (ApiType.Mana == sa.getApi()) {
|
} else if (ApiType.Mana == sa.getApi()) {
|
||||||
// keep mana abilities for paying
|
|
||||||
p -= 9;
|
p -= 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +405,7 @@ public class ComputerUtilAbility {
|
|||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public static List<SpellAbility> sortCreatureSpells(final List<SpellAbility> all) {
|
public static List<SpellAbility> sortCreatureSpells(final List<SpellAbility> all) {
|
||||||
// try to smoothen power creep by making CMC less of a factor
|
// try to smoothen power creep by making CMC less of a factor
|
||||||
|
|||||||
@@ -2,24 +2,21 @@ package forge.ai;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import forge.StaticData;
|
import com.google.common.base.Function;
|
||||||
import forge.ai.simulation.GameStateEvaluator;
|
import forge.ai.simulation.GameStateEvaluator;
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
import forge.util.*;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.tuple.MutablePair;
|
import org.apache.commons.lang3.tuple.MutablePair;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
import forge.card.CardRules;
|
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
@@ -48,11 +45,14 @@ import forge.game.replacement.ReplacementEffect;
|
|||||||
import forge.game.replacement.ReplacementLayer;
|
import forge.game.replacement.ReplacementLayer;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.staticability.StaticAbility;
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.staticability.StaticAbilityMode;
|
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.zone.MagicStack;
|
import forge.game.zone.MagicStack;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.item.PaperCard;
|
import forge.item.PaperCard;
|
||||||
|
import forge.util.Aggregates;
|
||||||
|
import forge.util.Expressions;
|
||||||
|
import forge.util.MyRandom;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
public class ComputerUtilCard {
|
public class ComputerUtilCard {
|
||||||
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
|
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
|
||||||
@@ -86,11 +86,12 @@ public class ComputerUtilCard {
|
|||||||
* @return a {@link forge.game.card.Card} object.
|
* @return a {@link forge.game.card.Card} object.
|
||||||
*/
|
*/
|
||||||
public static Card getBestArtifactAI(final List<Card> list) {
|
public static Card getBestArtifactAI(final List<Card> list) {
|
||||||
|
List<Card> all = CardLists.filter(list, CardPredicates.Presets.ARTIFACTS);
|
||||||
|
if (all.size() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// get biggest Artifact
|
// get biggest Artifact
|
||||||
return list.stream()
|
return Aggregates.itemWithMax(all, Card::getCMC);
|
||||||
.filter(CardPredicates.ARTIFACTS)
|
|
||||||
.max(Comparator.comparing(Card::getCMC))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,11 +101,12 @@ public class ComputerUtilCard {
|
|||||||
* @return best Planeswalker
|
* @return best Planeswalker
|
||||||
*/
|
*/
|
||||||
public static Card getBestPlaneswalkerAI(final List<Card> list) {
|
public static Card getBestPlaneswalkerAI(final List<Card> list) {
|
||||||
|
List<Card> all = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS);
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// no AI logic, just return most expensive
|
// no AI logic, just return most expensive
|
||||||
return list.stream()
|
return Aggregates.itemWithMax(all, Card::getCMC);
|
||||||
.filter(CardPredicates.PLANESWALKERS)
|
|
||||||
.max(Comparator.comparing(Card::getCMC))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,11 +116,12 @@ public class ComputerUtilCard {
|
|||||||
* @return best Planeswalker
|
* @return best Planeswalker
|
||||||
*/
|
*/
|
||||||
public static Card getWorstPlaneswalkerAI(final List<Card> list) {
|
public static Card getWorstPlaneswalkerAI(final List<Card> list) {
|
||||||
|
List<Card> all = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS);
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// no AI logic, just return least expensive
|
// no AI logic, just return least expensive
|
||||||
return list.stream()
|
return Aggregates.itemWithMin(all, Card::getCMC);
|
||||||
.filter(CardPredicates.PLANESWALKERS)
|
|
||||||
.min(Comparator.comparing(Card::getCMC))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Card getBestPlaneswalkerToDamage(final List<Card> pws) {
|
public static Card getBestPlaneswalkerToDamage(final List<Card> pws) {
|
||||||
@@ -184,13 +187,13 @@ public class ComputerUtilCard {
|
|||||||
* @return a {@link forge.game.card.Card} object.
|
* @return a {@link forge.game.card.Card} object.
|
||||||
*/
|
*/
|
||||||
public static Card getBestEnchantmentAI(final List<Card> list, final SpellAbility spell, final boolean targeted) {
|
public static Card getBestEnchantmentAI(final List<Card> list, final SpellAbility spell, final boolean targeted) {
|
||||||
Stream<Card> cardStream = list.stream().filter(CardPredicates.ENCHANTMENTS);
|
List<Card> all = CardLists.filter(list, CardPredicates.Presets.ENCHANTMENTS);
|
||||||
if (targeted) {
|
if (targeted) {
|
||||||
cardStream = cardStream.filter(c -> c.canBeTargetedBy(spell));
|
all = CardLists.filter(all, c -> c.canBeTargetedBy(spell));
|
||||||
}
|
}
|
||||||
|
|
||||||
// get biggest Enchantment
|
// get biggest Enchantment
|
||||||
return cardStream.max(Comparator.comparing(Card::getCMC)).orElse(null);
|
return Aggregates.itemWithMax(all, Card::getCMC);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,30 +205,30 @@ public class ComputerUtilCard {
|
|||||||
* @return a {@link forge.game.card.Card} object.
|
* @return a {@link forge.game.card.Card} object.
|
||||||
*/
|
*/
|
||||||
public static Card getBestLandAI(final Iterable<Card> list) {
|
public static Card getBestLandAI(final Iterable<Card> list) {
|
||||||
final List<Card> land = CardLists.filter(list, CardPredicates.LANDS);
|
final List<Card> land = CardLists.filter(list, CardPredicates.Presets.LANDS);
|
||||||
if (land.isEmpty()) {
|
if (land.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// prefer to target non basic lands
|
// prefer to target non basic lands
|
||||||
final List<Card> nbLand = CardLists.filter(land, CardPredicates.NONBASIC_LANDS);
|
final List<Card> nbLand = CardLists.filter(land, Predicates.not(CardPredicates.Presets.BASIC_LANDS));
|
||||||
|
|
||||||
if (!nbLand.isEmpty()) {
|
if (!nbLand.isEmpty()) {
|
||||||
// TODO - Improve ranking various non-basic lands depending on context
|
// TODO - Improve ranking various non-basic lands depending on context
|
||||||
|
|
||||||
// Urza's Mine/Tower/Power Plant
|
// Urza's Mine/Tower/Power Plant
|
||||||
final CardCollectionView aiAvailable = nbLand.get(0).getController().getCardsIn(Arrays.asList(ZoneType.Battlefield, ZoneType.Hand));
|
final CardCollectionView aiAvailable = nbLand.get(0).getController().getCardsIn(Arrays.asList(ZoneType.Battlefield, ZoneType.Hand));
|
||||||
if (IterableUtil.any(list, CardPredicates.nameEquals("Urza's Mine"))) {
|
if (Iterables.any(list, CardPredicates.nameEquals("Urza's Mine"))) {
|
||||||
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Mine")).isEmpty()) {
|
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Mine")).isEmpty()) {
|
||||||
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Mine")).getFirst();
|
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Mine")).getFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (IterableUtil.any(list, CardPredicates.nameEquals("Urza's Tower"))) {
|
if (Iterables.any(list, CardPredicates.nameEquals("Urza's Tower"))) {
|
||||||
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Tower")).isEmpty()) {
|
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Tower")).isEmpty()) {
|
||||||
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Tower")).getFirst();
|
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Tower")).getFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (IterableUtil.any(list, CardPredicates.nameEquals("Urza's Power Plant"))) {
|
if (Iterables.any(list, CardPredicates.nameEquals("Urza's Power Plant"))) {
|
||||||
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Power Plant")).isEmpty()) {
|
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Power Plant")).isEmpty()) {
|
||||||
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Power Plant")).getFirst();
|
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Power Plant")).getFirst();
|
||||||
}
|
}
|
||||||
@@ -247,16 +250,17 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
if (iminBL == Integer.MAX_VALUE) {
|
if (iminBL == Integer.MAX_VALUE) {
|
||||||
// All basic lands have no basic land type. Just return something
|
// All basic lands have no basic land type. Just return something
|
||||||
return land.stream().filter(CardPredicates.UNTAPPED).findFirst().orElse(land.get(0));
|
return Iterables.find(land, CardPredicates.Presets.UNTAPPED, land.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Card> bLand = CardLists.getType(land, sminBL);
|
final List<Card> bLand = CardLists.getType(land, sminBL);
|
||||||
|
|
||||||
return bLand.stream()
|
for (Card ut : Iterables.filter(bLand, CardPredicates.Presets.UNTAPPED)) {
|
||||||
.filter(CardPredicates.UNTAPPED)
|
return ut;
|
||||||
.findFirst()
|
}
|
||||||
|
|
||||||
// TODO potentially risky if simulation mode currently able to reach this from triggers
|
// TODO potentially risky if simulation mode currently able to reach this from triggers
|
||||||
.orElseGet(() -> Aggregates.random(bLand)); // random tapped land of least represented type
|
return Aggregates.random(bLand); // random tapped land of least represented type
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -358,10 +362,10 @@ public class ComputerUtilCard {
|
|||||||
*/
|
*/
|
||||||
public static Card getBestAI(final Iterable<Card> list) {
|
public static Card getBestAI(final Iterable<Card> list) {
|
||||||
// Get Best will filter by appropriate getBest list if ALL of the list is of that type
|
// Get Best will filter by appropriate getBest list if ALL of the list is of that type
|
||||||
if (IterableUtil.all(list, CardPredicates.CREATURES)) {
|
if (Iterables.all(list, CardPredicates.Presets.CREATURES)) {
|
||||||
return getBestCreatureAI(list);
|
return getBestCreatureAI(list);
|
||||||
}
|
}
|
||||||
if (IterableUtil.all(list, CardPredicates.LANDS)) {
|
if (Iterables.all(list, CardPredicates.Presets.LANDS)) {
|
||||||
return getBestLandAI(list);
|
return getBestLandAI(list);
|
||||||
}
|
}
|
||||||
// TODO - Once we get an EvaluatePermanent this should call getBestPermanent()
|
// TODO - Once we get an EvaluatePermanent this should call getBestPermanent()
|
||||||
@@ -378,7 +382,7 @@ public class ComputerUtilCard {
|
|||||||
if (Iterables.size(list) == 1) {
|
if (Iterables.size(list) == 1) {
|
||||||
return Iterables.get(list, 0);
|
return Iterables.get(list, 0);
|
||||||
}
|
}
|
||||||
return Aggregates.itemWithMax(IterableUtil.filter(list, CardPredicates.CREATURES), ComputerUtilCard.creatureEvaluator);
|
return Aggregates.itemWithMax(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -391,7 +395,7 @@ public class ComputerUtilCard {
|
|||||||
if (Iterables.size(list) == 1) {
|
if (Iterables.size(list) == 1) {
|
||||||
return Iterables.get(list, 0);
|
return Iterables.get(list, 0);
|
||||||
}
|
}
|
||||||
return Aggregates.itemWithMax(IterableUtil.filter(list, Card::hasPlayableLandFace), ComputerUtilCard.landEvaluator);
|
return Aggregates.itemWithMax(Iterables.filter(list, CardPredicates.Presets.LANDS), ComputerUtilCard.landEvaluator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -406,7 +410,7 @@ public class ComputerUtilCard {
|
|||||||
if (Iterables.size(list) == 1) {
|
if (Iterables.size(list) == 1) {
|
||||||
return Iterables.get(list, 0);
|
return Iterables.get(list, 0);
|
||||||
}
|
}
|
||||||
return Aggregates.itemWithMin(IterableUtil.filter(list, CardPredicates.CREATURES), ComputerUtilCard.creatureEvaluator);
|
return Aggregates.itemWithMin(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This selection rates tokens higher
|
// This selection rates tokens higher
|
||||||
@@ -427,7 +431,7 @@ public class ComputerUtilCard {
|
|||||||
Card biggest = null;
|
Card biggest = null;
|
||||||
int biggestvalue = -1;
|
int biggestvalue = -1;
|
||||||
|
|
||||||
for (Card card : CardLists.filter(list, CardPredicates.CREATURES)) {
|
for (Card card : CardLists.filter(list, CardPredicates.Presets.CREATURES)) {
|
||||||
int newvalue = evaluateCreature(card);
|
int newvalue = evaluateCreature(card);
|
||||||
newvalue += card.isToken() ? tokenBonus : 0; // raise the value of tokens
|
newvalue += card.isToken() ? tokenBonus : 0; // raise the value of tokens
|
||||||
|
|
||||||
@@ -480,40 +484,40 @@ public class ComputerUtilCard {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean hasEnchantmants = IterableUtil.any(list, CardPredicates.ENCHANTMENTS);
|
final boolean hasEnchantmants = Iterables.any(list, CardPredicates.Presets.ENCHANTMENTS);
|
||||||
if (biasEnch && hasEnchantmants) {
|
if (biasEnch && hasEnchantmants) {
|
||||||
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.ENCHANTMENTS), null, false);
|
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.Presets.ENCHANTMENTS), null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean hasArtifacts = IterableUtil.any(list, CardPredicates.ARTIFACTS);
|
final boolean hasArtifacts = Iterables.any(list, CardPredicates.Presets.ARTIFACTS);
|
||||||
if (biasArt && hasArtifacts) {
|
if (biasArt && hasArtifacts) {
|
||||||
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.ARTIFACTS), null, false);
|
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.Presets.ARTIFACTS), null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (biasLand && IterableUtil.any(list, CardPredicates.LANDS)) {
|
if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) {
|
||||||
return getWorstLand(CardLists.filter(list, CardPredicates.LANDS));
|
return getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean hasCreatures = IterableUtil.any(list, CardPredicates.CREATURES);
|
final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES);
|
||||||
if (biasCreature && hasCreatures) {
|
if (biasCreature && hasCreatures) {
|
||||||
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.CREATURES));
|
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Card> lands = CardLists.filter(list, CardPredicates.LANDS);
|
List<Card> lands = CardLists.filter(list, CardPredicates.Presets.LANDS);
|
||||||
if (lands.size() > 6) {
|
if (lands.size() > 6) {
|
||||||
return getWorstLand(lands);
|
return getWorstLand(lands);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasEnchantmants || hasArtifacts) {
|
if (hasEnchantmants || hasArtifacts) {
|
||||||
final List<Card> ae = CardLists.filter(list,
|
final List<Card> ae = CardLists.filter(list, Predicates.and(
|
||||||
(CardPredicates.ARTIFACTS.or(CardPredicates.ENCHANTMENTS))
|
Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS),
|
||||||
.and(card -> !card.hasSVar("DoNotDiscardIfAble"))
|
card -> !card.hasSVar("DoNotDiscardIfAble")
|
||||||
);
|
));
|
||||||
return getCheapestPermanentAI(ae, null, false);
|
return getCheapestPermanentAI(ae, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCreatures) {
|
if (hasCreatures) {
|
||||||
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.CREATURES));
|
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planeswalkers fall through to here, lands will fall through if there aren't very many
|
// Planeswalkers fall through to here, lands will fall through if there aren't very many
|
||||||
@@ -522,7 +526,8 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
public static final Card getCheapestSpellAI(final Iterable<Card> list) {
|
public static final Card getCheapestSpellAI(final Iterable<Card> list) {
|
||||||
if (!Iterables.isEmpty(list)) {
|
if (!Iterables.isEmpty(list)) {
|
||||||
CardCollection cc = CardLists.filter(list, CardPredicates.INSTANTS_AND_SORCERIES);
|
CardCollection cc = CardLists.filter(list,
|
||||||
|
Predicates.or(CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
|
||||||
|
|
||||||
if (cc.isEmpty()) {
|
if (cc.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -692,8 +697,6 @@ public class ComputerUtilCard {
|
|||||||
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
|
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
|
||||||
AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
|
AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
|
||||||
Combat combat = new Combat(ai);
|
Combat combat = new Combat(ai);
|
||||||
// avoid removing original attacker
|
|
||||||
attacker.setCombatLKI(null);
|
|
||||||
combat.addAttacker(attacker, ai);
|
combat.addAttacker(attacker, ai);
|
||||||
final List<Card> attackers = Lists.newArrayList(attacker);
|
final List<Card> attackers = Lists.newArrayList(attacker);
|
||||||
aiBlk.assignBlockersGivenAttackers(combat, attackers);
|
aiBlk.assignBlockersGivenAttackers(combat, attackers);
|
||||||
@@ -711,7 +714,7 @@ public class ComputerUtilCard {
|
|||||||
if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) {
|
if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sa.setActivatingPlayer(opp);
|
sa.setActivatingPlayer(opp, true);
|
||||||
if (sa.canTarget(card)) {
|
if (sa.canTarget(card)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -788,8 +791,9 @@ public class ComputerUtilCard {
|
|||||||
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid) {
|
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid) {
|
||||||
return getMostProminentType(list, valid, true);
|
return getMostProminentType(list, valid, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid, boolean includeTokens) {
|
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid, boolean includeTokens) {
|
||||||
if (list.isEmpty()) {
|
if (list.size() == 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,35 +834,51 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
//also take into account abilities that generate tokens
|
//also take into account abilities that generate tokens
|
||||||
if (includeTokens) {
|
if (includeTokens) {
|
||||||
if (c.getRules() != null) {
|
for (SpellAbility sa : c.getAllSpellAbilities()) {
|
||||||
for (String token : c.getRules().getTokens()) {
|
if (sa.getApi() != ApiType.Token) {
|
||||||
CardRules tokenCR = StaticData.instance().getAllTokens().getToken(token).getRules();
|
|
||||||
if (tokenCR == null)
|
|
||||||
continue;
|
continue;
|
||||||
for (String type : tokenCR.getType().getCreatureTypes()) {
|
}
|
||||||
Integer count = typesInDeck.getOrDefault(type, 0);
|
if (sa.hasParam("TokenTypes")) {
|
||||||
typesInDeck.put(type, count + 1);
|
for (String var : sa.getParam("TokenTypes").split(",")) {
|
||||||
|
if (!CardType.isACreatureType(var)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer count = typesInDeck.getOrDefault(var, 0);
|
||||||
|
typesInDeck.put(var, count + weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// same for Trigger that does make Tokens
|
||||||
|
for (Trigger t : c.getTriggers()) {
|
||||||
|
SpellAbility sa = t.ensureAbility();
|
||||||
|
if (sa != null) {
|
||||||
|
if (sa.getApi() != ApiType.Token || !sa.hasParam("TokenTypes")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String var : sa.getParam("TokenTypes").split(",")) {
|
||||||
|
if (!CardType.isACreatureType(var)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer count = typesInDeck.getOrDefault(var, 0);
|
||||||
|
typesInDeck.put(var, count + weight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// special rule for Fabricate and Servo
|
// special rule for Fabricate and Servo
|
||||||
if (c.hasKeyword(Keyword.FABRICATE)) {
|
if (c.hasKeyword(Keyword.FABRICATE)) {
|
||||||
Integer count = typesInDeck.getOrDefault("Servo", 0);
|
Integer count = typesInDeck.getOrDefault("Servo", 0);
|
||||||
typesInDeck.put("Servo", count + weight);
|
typesInDeck.put("Servo", count + weight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} // for
|
||||||
|
|
||||||
int max = 0;
|
int max = 0;
|
||||||
String maxType = "";
|
String maxType = "";
|
||||||
|
|
||||||
// Iterate through typesInDeck and consider only valid types
|
|
||||||
for (final Entry<String, Integer> entry : typesInDeck.entrySet()) {
|
for (final Entry<String, Integer> entry : typesInDeck.entrySet()) {
|
||||||
final String type = entry.getKey();
|
final String type = entry.getKey();
|
||||||
|
|
||||||
// consider the types that are in the valid list
|
if (max < entry.getValue()) {
|
||||||
if ((valid.isEmpty() || valid.contains(type)) && max < entry.getValue()) {
|
|
||||||
max = entry.getValue();
|
max = entry.getValue();
|
||||||
maxType = type;
|
maxType = type;
|
||||||
}
|
}
|
||||||
@@ -919,14 +939,14 @@ public class ComputerUtilCard {
|
|||||||
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
|
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
|
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
|
||||||
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
|
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
|
||||||
for (byte c : MagicColor.WUBRG) {
|
for (byte c : MagicColor.WUBRG) {
|
||||||
if ((colors & c) != 0) {
|
if ((colors & c) != 0) {
|
||||||
return MagicColor.toLongString(c);
|
return MagicColor.toLongString(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
|
return restrictedToColors.get(0); // no difference, there was no prominent color
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> getColorByProminence(final List<Card> list) {
|
public static List<String> getColorByProminence(final List<Card> list) {
|
||||||
@@ -987,7 +1007,7 @@ public class ComputerUtilCard {
|
|||||||
} else if (logic.equals("MostProminentHumanCreatures")) {
|
} else if (logic.equals("MostProminentHumanCreatures")) {
|
||||||
CardCollectionView list = opp.getCreaturesInPlay();
|
CardCollectionView list = opp.getCreaturesInPlay();
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
list = CardLists.filter(CardLists.filterControlledBy(game.getCardsInGame(), opp), CardPredicates.CREATURES);
|
list = CardLists.filter(CardLists.filterControlledBy(game.getCardsInGame(), opp), CardPredicates.Presets.CREATURES);
|
||||||
}
|
}
|
||||||
chosen.add(getMostProminentColor(list, colorChoices));
|
chosen.add(getMostProminentColor(list, colorChoices));
|
||||||
} else if (logic.equals("MostProminentComputerControls")) {
|
} else if (logic.equals("MostProminentComputerControls")) {
|
||||||
@@ -1042,7 +1062,7 @@ public class ComputerUtilCard {
|
|||||||
String devotionCode = "Count$Devotion." + MagicColor.toLongString(c);
|
String devotionCode = "Count$Devotion." + MagicColor.toLongString(c);
|
||||||
|
|
||||||
int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa);
|
int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa);
|
||||||
if (devotion > curDevotion && hand.anyMatch(CardPredicates.isColor(c))) {
|
if (devotion > curDevotion && Iterables.any(hand, CardPredicates.isColor(c))) {
|
||||||
curDevotion = devotion;
|
curDevotion = devotion;
|
||||||
chosenColor = MagicColor.toLongString(c);
|
chosenColor = MagicColor.toLongString(c);
|
||||||
}
|
}
|
||||||
@@ -1214,7 +1234,8 @@ public class ComputerUtilCard {
|
|||||||
// if this thing is both owned and controlled by an opponent and it has a continuous ability,
|
// if this thing is both owned and controlled by an opponent and it has a continuous ability,
|
||||||
// assume it either benefits the player or disrupts the opponent
|
// assume it either benefits the player or disrupts the opponent
|
||||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.isIntrinsic()) {
|
final Map<String, String> params = stAb.getMapParams();
|
||||||
|
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
|
||||||
priority = true;
|
priority = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1245,16 +1266,17 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||||
|
final Map<String, String> params = stAb.getMapParams();
|
||||||
//continuous buffs
|
//continuous buffs
|
||||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && "Creature.YouCtrl".equals(stAb.getParam("Affected"))) {
|
if (params.get("Mode").equals("Continuous") && "Creature.YouCtrl".equals(params.get("Affected"))) {
|
||||||
int bonusPT = 0;
|
int bonusPT = 0;
|
||||||
if (stAb.hasParam("AddPower")) {
|
if (params.containsKey("AddPower")) {
|
||||||
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
|
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
|
||||||
}
|
}
|
||||||
if (stAb.hasParam("AddToughness")) {
|
if (params.containsKey("AddToughness")) {
|
||||||
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
|
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
|
||||||
}
|
}
|
||||||
String kws = stAb.getParam("AddKeyword");
|
String kws = params.get("AddKeyword");
|
||||||
if (kws != null) {
|
if (kws != null) {
|
||||||
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
|
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
|
||||||
}
|
}
|
||||||
@@ -1405,7 +1427,7 @@ public class ComputerUtilCard {
|
|||||||
//1. become attacker for whatever reason
|
//1. become attacker for whatever reason
|
||||||
if (!doesCreatureAttackAI(ai, c) && doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
if (!doesCreatureAttackAI(ai, c) && doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
||||||
float threat = 1.0f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
|
float threat = 1.0f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
|
||||||
if (oppCreatures.stream().noneMatch(CardPredicates.possibleBlockers(pumped))) {
|
if (!Iterables.any(oppCreatures, CardPredicates.possibleBlockers(pumped))) {
|
||||||
threat *= 2;
|
threat *= 2;
|
||||||
}
|
}
|
||||||
if (c.getNetPower() == 0 && c == sa.getHostCard() && power > 0) {
|
if (c.getNetPower() == 0 && c == sa.getHostCard() && power > 0) {
|
||||||
@@ -1457,8 +1479,8 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//3. grant evasive
|
//3. grant evasive
|
||||||
if (oppCreatures.stream().anyMatch(CardPredicates.possibleBlockers(c))) {
|
if (Iterables.any(oppCreatures, CardPredicates.possibleBlockers(c))) {
|
||||||
if (oppCreatures.stream().noneMatch(CardPredicates.possibleBlockers(pumped))
|
if (!Iterables.any(oppCreatures, CardPredicates.possibleBlockers(pumped))
|
||||||
&& doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
&& doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
||||||
chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
|
chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
|
||||||
}
|
}
|
||||||
@@ -1737,7 +1759,7 @@ public class ComputerUtilCard {
|
|||||||
pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0);
|
pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0);
|
||||||
|
|
||||||
if (!kws.isEmpty()) {
|
if (!kws.isEmpty()) {
|
||||||
pumped.addChangedCardKeywords(kws, null, false, timestamp, null, false);
|
pumped.addChangedCardKeywords(kws, null, false, timestamp, 0, false);
|
||||||
}
|
}
|
||||||
if (!hiddenKws.isEmpty()) {
|
if (!hiddenKws.isEmpty()) {
|
||||||
pumped.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws);
|
pumped.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws);
|
||||||
@@ -1758,7 +1780,7 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
|
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
|
||||||
pumped.addChangedCardKeywordsInternal(toCopy, null, false, timestamp2, null, false);
|
pumped.addChangedCardKeywordsInternal(toCopy, null, false, timestamp2, 0, false);
|
||||||
pumped.updateKeywordsCache(pumped.getCurrentState());
|
pumped.updateKeywordsCache(pumped.getCurrentState());
|
||||||
applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
|
applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
|
||||||
return pumped;
|
return pumped;
|
||||||
@@ -1785,7 +1807,7 @@ public class ComputerUtilCard {
|
|||||||
// remove old boost that might be copied
|
// remove old boost that might be copied
|
||||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||||
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
|
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
|
||||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
if (!stAb.checkMode("Continuous")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!stAb.hasParam("Affected")) {
|
if (!stAb.hasParam("Affected")) {
|
||||||
@@ -1819,18 +1841,18 @@ public class ComputerUtilCard {
|
|||||||
* @param sa Pump* or CounterPut*
|
* @param sa Pump* or CounterPut*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
||||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
|
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
|
||||||
|
|
||||||
if (!sa.usesTargeting()) {
|
if (!sa.usesTargeting()) {
|
||||||
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||||
for (final Card card : cards) {
|
for (final Card card : cards) {
|
||||||
if (objects.contains(card)) {
|
if (objects.contains(card)) {
|
||||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
|
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||||
@@ -1849,11 +1871,11 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
if (!sa.isTargetNumberValid()) {
|
if (!sa.isTargetNumberValid()) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
return false;
|
||||||
}
|
}
|
||||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
return true;
|
||||||
}
|
}
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isUselessCreature(Player ai, Card c) {
|
public static boolean isUselessCreature(Player ai, Card c) {
|
||||||
@@ -1863,7 +1885,7 @@ public class ComputerUtilCard {
|
|||||||
if (!c.isCreature()) {
|
if (!c.isCreature()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
|
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()))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -1927,7 +1949,7 @@ public class ComputerUtilCard {
|
|||||||
CardCollection aiCreats = ai.getCreaturesInPlay();
|
CardCollection aiCreats = ai.getCreaturesInPlay();
|
||||||
if (temporary) {
|
if (temporary) {
|
||||||
// Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped.
|
// Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped.
|
||||||
oppCards = CardLists.filter(oppCards, CardPredicates.UNTAPPED);
|
oppCards = CardLists.filter(oppCards, CardPredicates.Presets.UNTAPPED);
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollection priorityCards = new CardCollection();
|
CardCollection priorityCards = new CardCollection();
|
||||||
@@ -2080,7 +2102,6 @@ public class ComputerUtilCard {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// use this function to skip expensive calculations on identical cards
|
|
||||||
public static CardCollection dedupeCards(CardCollection cc) {
|
public static CardCollection dedupeCards(CardCollection cc) {
|
||||||
if (cc.size() <= 1) {
|
if (cc.size() <= 1) {
|
||||||
return cc;
|
return cc;
|
||||||
@@ -2088,7 +2109,7 @@ public class ComputerUtilCard {
|
|||||||
CardCollection deduped = new CardCollection();
|
CardCollection deduped = new CardCollection();
|
||||||
for (Card c : cc) {
|
for (Card c : cc) {
|
||||||
boolean unique = true;
|
boolean unique = true;
|
||||||
if (c.isInZone(ZoneType.Hand) && !c.hasPerpetual()) {
|
if (c.isInZone(ZoneType.Hand)) {
|
||||||
for (Card d : deduped) {
|
for (Card d : deduped) {
|
||||||
if (d.isInZone(ZoneType.Hand) && d.getOwner().equals(c.getOwner()) && d.getName().equals(c.getName())) {
|
if (d.isInZone(ZoneType.Hand) && d.getOwner().equals(c.getOwner()) && d.getName().equals(c.getName())) {
|
||||||
unique = false;
|
unique = false;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
|
|||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.CostPayment;
|
import forge.game.cost.CostPayment;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.Untap;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.replacement.ReplacementEffect;
|
import forge.game.replacement.ReplacementEffect;
|
||||||
import forge.game.replacement.ReplacementLayer;
|
import forge.game.replacement.ReplacementLayer;
|
||||||
@@ -39,12 +39,10 @@ import forge.game.replacement.ReplacementType;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.staticability.StaticAbility;
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
||||||
import forge.game.staticability.StaticAbilityMode;
|
|
||||||
import forge.game.staticability.StaticAbilityMustAttack;
|
import forge.game.staticability.StaticAbilityMustAttack;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.IterableUtil;
|
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.TextUtil;
|
import forge.util.TextUtil;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
@@ -80,7 +78,7 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static boolean canAttackNextTurn(final Card attacker) {
|
public static boolean canAttackNextTurn(final Card attacker) {
|
||||||
final Iterable<GameEntity> defenders = CombatUtil.getAllPossibleDefenders(attacker.getController());
|
final Iterable<GameEntity> defenders = CombatUtil.getAllPossibleDefenders(attacker.getController());
|
||||||
return IterableUtil.any(defenders, input -> canAttackNextTurn(attacker, input));
|
return Iterables.any(defenders, input -> canAttackNextTurn(attacker, input));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,7 +100,7 @@ public class ComputerUtilCombat {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
|
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +117,7 @@ public class ComputerUtilCombat {
|
|||||||
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
|
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
|
||||||
// || attacker.hasSVar("EndOfTurnLeavePlay"));
|
// || attacker.hasSVar("EndOfTurnLeavePlay"));
|
||||||
// The creature won't untap next turn
|
// The creature won't untap next turn
|
||||||
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
|
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,7 +175,7 @@ public class ComputerUtilCombat {
|
|||||||
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
|
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
|
||||||
int damage = attacker.getNetCombatDamage();
|
int damage = attacker.getNetCombatDamage();
|
||||||
int sum = 0;
|
int sum = 0;
|
||||||
if (attacked instanceof Player player && !player.canLoseLife()) {
|
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,10 +213,10 @@ public class ComputerUtilCombat {
|
|||||||
int damage = attacker.getNetCombatDamage();
|
int damage = attacker.getNetCombatDamage();
|
||||||
int poison = 0;
|
int poison = 0;
|
||||||
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
|
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
|
||||||
if (attacker.isInfectDamage(attacked)) {
|
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||||
int pd = predictDamageTo(attacked, damage, attacker, true);
|
int pd = predictDamageTo(attacked, damage, attacker, true);
|
||||||
// opponent can always order it so that he gets 0
|
// 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"))) {
|
if (pd == 1 && Iterables.any(attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
|
||||||
pd = 0;
|
pd = 0;
|
||||||
}
|
}
|
||||||
poison += pd;
|
poison += pd;
|
||||||
@@ -358,7 +356,7 @@ public class ComputerUtilCombat {
|
|||||||
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
|
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
|
||||||
if (trampleDamage > 0) {
|
if (trampleDamage > 0) {
|
||||||
if (attacker.isInfectDamage(ai)) {
|
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||||
poison += trampleDamage;
|
poison += trampleDamage;
|
||||||
}
|
}
|
||||||
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
|
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
|
||||||
@@ -406,11 +404,11 @@ public class ComputerUtilCombat {
|
|||||||
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);
|
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);
|
||||||
// Special cases:
|
// Special cases:
|
||||||
// AI can't lose in combat in presence of Worship (with creatures)
|
// AI can't lose in combat in presence of Worship (with creatures)
|
||||||
if (otb.anyMatch(CardPredicates.nameEquals("Worship")) && !ai.getCreaturesInPlay().isEmpty()) {
|
if (Iterables.any(otb, CardPredicates.nameEquals("Worship")) && !ai.getCreaturesInPlay().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// AI can't lose in combat in presence of Elderscale Wurm (at 7 life or more)
|
// AI can't lose in combat in presence of Elderscale Wurm (at 7 life or more)
|
||||||
if (otb.anyMatch(CardPredicates.nameEquals("Elderscale Wurm")) && ai.getLife() >= 7) {
|
if (Iterables.any(otb, CardPredicates.nameEquals("Elderscale Wurm")) && ai.getLife() >= 7) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,11 +456,11 @@ public class ComputerUtilCombat {
|
|||||||
maxTreshold--;
|
maxTreshold--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters())) {
|
if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife());
|
return resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -501,11 +499,11 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultingPoison(ai, combat) >= ai.getGame().getRules().getPoisonCountersToLose()) {
|
if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1;
|
return resultingPoison(ai, combat) >= ai.getGame().getRules().getPoisonCountersToLose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This calculates the amount of damage a blockgang can deal to the attacker
|
// This calculates the amount of damage a blockgang can deal to the attacker
|
||||||
@@ -726,6 +724,7 @@ public class ComputerUtilCombat {
|
|||||||
return totalDamageOfBlockers(attacker, blockers) >= getDamageToKill(attacker, false);
|
return totalDamageOfBlockers(attacker, blockers) >= getDamageToKill(attacker, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Will this trigger trigger?
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* combatTriggerWillTrigger.
|
* combatTriggerWillTrigger.
|
||||||
@@ -901,7 +900,7 @@ public class ComputerUtilCombat {
|
|||||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||||
for (final Card card : cardList) {
|
for (final Card card : cardList) {
|
||||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
if (!stAb.checkMode("Continuous")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
|
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
|
||||||
@@ -1197,7 +1196,7 @@ public class ComputerUtilCombat {
|
|||||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||||
for (final Card card : cardList) {
|
for (final Card card : cardList) {
|
||||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
if (!stAb.checkMode("Continuous")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
|
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
|
||||||
@@ -1245,7 +1244,7 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
sa.setActivatingPlayer(source.getController());
|
sa.setActivatingPlayer(source.getController(), true);
|
||||||
|
|
||||||
if (sa.hasParam("Cost")) {
|
if (sa.hasParam("Cost")) {
|
||||||
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
|
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
|
||||||
@@ -1388,7 +1387,7 @@ public class ComputerUtilCombat {
|
|||||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||||
for (final Card card : cardList) {
|
for (final Card card : cardList) {
|
||||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
if (!"Continuous".equals(stAb.getParam("Mode"))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!stAb.hasParam("Affected")) {
|
if (!stAb.hasParam("Affected")) {
|
||||||
@@ -1429,13 +1428,12 @@ public class ComputerUtilCombat {
|
|||||||
if (sa == null) {
|
if (sa == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
sa.setActivatingPlayer(source.getController(), true);
|
||||||
|
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
continue; // targeted pumping not supported
|
continue; // targeted pumping not supported
|
||||||
}
|
}
|
||||||
|
|
||||||
sa.setActivatingPlayer(source.getController());
|
|
||||||
|
|
||||||
// DealDamage triggers
|
// DealDamage triggers
|
||||||
if (ApiType.DealDamage.equals(sa.getApi())) {
|
if (ApiType.DealDamage.equals(sa.getApi())) {
|
||||||
if (!sa.hasParam("Defined") || !sa.getParam("Defined").startsWith("TriggeredAttacker")) {
|
if (!sa.hasParam("Defined") || !sa.getParam("Defined").startsWith("TriggeredAttacker")) {
|
||||||
@@ -1735,7 +1733,6 @@ public class ComputerUtilCombat {
|
|||||||
final int attackerLife = getDamageToKill(attacker, false)
|
final int attackerLife = getDamageToKill(attacker, false)
|
||||||
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||||
|
|
||||||
// AI should be less worried about Deathtouch
|
|
||||||
if (blocker.hasDoubleStrike()) {
|
if (blocker.hasDoubleStrike()) {
|
||||||
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1965,7 +1962,6 @@ public class ComputerUtilCombat {
|
|||||||
final int attackerLife = getDamageToKill(attacker, false)
|
final int attackerLife = getDamageToKill(attacker, false)
|
||||||
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||||
|
|
||||||
// AI should be less worried about deathtouch
|
|
||||||
if (attacker.hasDoubleStrike()) {
|
if (attacker.hasDoubleStrike()) {
|
||||||
if (attackerDamage >= defenderLife) {
|
if (attackerDamage >= defenderLife) {
|
||||||
return true;
|
return true;
|
||||||
@@ -2542,20 +2538,20 @@ public class ComputerUtilCombat {
|
|||||||
if (combat != null) {
|
if (combat != null) {
|
||||||
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
|
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
|
||||||
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
|
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
|
||||||
if (def instanceof Card card && Iterables.contains(defenders, def)) {
|
if (def instanceof Card && Iterables.contains(defenders, def)) {
|
||||||
if (card.isPlaneswalker()) {
|
if (((Card)def).isPlaneswalker()) {
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
if (card.isBattle()) {
|
if (((Card)def).isBattle()) {
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
|
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
|
||||||
for (GameEntity p : defenders) {
|
for (GameEntity p : defenders) {
|
||||||
if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
|
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
if (p instanceof Card card && !ComputerUtilCard.canBeBlockedProfitably(card.getController(), attacker, true)) {
|
if (p instanceof Card && !ComputerUtilCard.canBeBlockedProfitably(((Card)p).getController(), attacker, true)) {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.Collection;
|
import com.google.common.base.Predicates;
|
||||||
import java.util.List;
|
import com.google.common.collect.Iterables;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import forge.game.GameObject;
|
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
import forge.ai.AiCardMemory.MemorySet;
|
import forge.ai.AiCardMemory.MemorySet;
|
||||||
import forge.ai.ability.AnimateAi;
|
import forge.ai.ability.AnimateAi;
|
||||||
|
import forge.ai.ability.TokenAi;
|
||||||
|
import forge.card.ColorSet;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.*;
|
import forge.game.cost.*;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -26,9 +23,14 @@ import forge.game.spellability.Spell;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetChoices;
|
import forge.game.spellability.TargetChoices;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.IterableUtil;
|
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.TextUtil;
|
import forge.util.TextUtil;
|
||||||
|
import forge.util.collect.FCollectionView;
|
||||||
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
public class ComputerUtilCost {
|
public class ComputerUtilCost {
|
||||||
@@ -51,7 +53,8 @@ public class ComputerUtilCost {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostPutCounter addCounter) {
|
if (part instanceof CostPutCounter) {
|
||||||
|
final CostPutCounter addCounter = (CostPutCounter) part;
|
||||||
final CounterType type = addCounter.getCounter();
|
final CounterType type = addCounter.getCounter();
|
||||||
|
|
||||||
if (type.is(CounterEnumType.M1M1)) {
|
if (type.is(CounterEnumType.M1M1)) {
|
||||||
@@ -77,7 +80,9 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
|
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostRemoveCounter remCounter) {
|
if (part instanceof CostRemoveCounter) {
|
||||||
|
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
|
||||||
|
|
||||||
final CounterType type = remCounter.counter;
|
final CounterType type = remCounter.counter;
|
||||||
if (!part.payCostFromSource()) {
|
if (!part.payCostFromSource()) {
|
||||||
if (type.is(CounterEnumType.P1P1)) {
|
if (type.is(CounterEnumType.P1P1)) {
|
||||||
@@ -104,7 +109,9 @@ public class ComputerUtilCost {
|
|||||||
&& !source.hasKeyword(Keyword.UNDYING)) {
|
&& !source.hasKeyword(Keyword.UNDYING)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (part instanceof CostRemoveAnyCounter remCounter) {
|
} else if (part instanceof CostRemoveAnyCounter) {
|
||||||
|
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
|
||||||
|
|
||||||
PaymentDecision pay = decision.visit(remCounter);
|
PaymentDecision pay = decision.visit(remCounter);
|
||||||
return pay != null;
|
return pay != null;
|
||||||
}
|
}
|
||||||
@@ -129,31 +136,25 @@ public class ComputerUtilCost {
|
|||||||
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
|
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
|
||||||
|
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostDiscard disc) {
|
if (part instanceof CostDiscard) {
|
||||||
|
final CostDiscard disc = (CostDiscard) part;
|
||||||
|
|
||||||
final String type = disc.getType();
|
final String type = disc.getType();
|
||||||
final CardCollection typeList;
|
|
||||||
int num;
|
|
||||||
if (type.equals("Hand")) {
|
|
||||||
typeList = hand;
|
|
||||||
num = hand.size();
|
|
||||||
} else {
|
|
||||||
if (type.equals("CARDNAME")) {
|
if (type.equals("CARDNAME")) {
|
||||||
if (source.getAbilityText().contains("Bloodrush")) {
|
if (source.getAbilityText().contains("Bloodrush")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
} else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
|
||||||
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
|
|
||||||
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
|
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
|
||||||
// Better do something than just discard stuff
|
// Better do something than just discard stuff
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
|
final CardCollection typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
|
||||||
if (typeList.size() > ai.getMaxHandSize()) {
|
if (typeList.size() > ai.getMaxHandSize()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
|
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
|
||||||
}
|
|
||||||
for (int i = 0; i < num; i++) {
|
for (int i = 0; i < num; i++) {
|
||||||
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
|
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
|
||||||
if (pref == null) {
|
if (pref == null) {
|
||||||
@@ -183,7 +184,8 @@ public class ComputerUtilCost {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostDamage pay) {
|
if (part instanceof CostDamage) {
|
||||||
|
final CostDamage pay = (CostDamage) part;
|
||||||
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
|
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
|
||||||
if (ai.getLife() - realDamage < remainingLife
|
if (ai.getLife() - realDamage < remainingLife
|
||||||
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
|
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
|
||||||
@@ -215,8 +217,13 @@ public class ComputerUtilCost {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostPayLife payLife) {
|
if (part instanceof CostPayLife) {
|
||||||
int amount = payLife.getAbilityAmount(sourceAbility);
|
final CostPayLife payLife = (CostPayLife) part;
|
||||||
|
|
||||||
|
Integer amount = payLife.convertAmount();
|
||||||
|
if (amount == null) {
|
||||||
|
amount = AbilityUtils.calculateAmount(source, payLife.getAmount(), sourceAbility);
|
||||||
|
}
|
||||||
|
|
||||||
// check if there's override for the remainingLife threshold
|
// check if there's override for the remainingLife threshold
|
||||||
if (sourceAbility != null && sourceAbility.hasParam("AILifeThreshold")) {
|
if (sourceAbility != null && sourceAbility.hasParam("AILifeThreshold")) {
|
||||||
@@ -251,7 +258,11 @@ public class ComputerUtilCost {
|
|||||||
// Does the AI want to use Sacrifice All?
|
// Does the AI want to use Sacrifice All?
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
int c = part.getAbilityAmount(sourceAbility);
|
Integer c = part.convertAmount();
|
||||||
|
|
||||||
|
if (c == null) {
|
||||||
|
c = part.getAbilityAmount(sourceAbility);
|
||||||
|
}
|
||||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
|
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
|
||||||
if (choices != null) {
|
if (choices != null) {
|
||||||
@@ -285,7 +296,8 @@ public class ComputerUtilCost {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostSacrifice sac) {
|
if (part instanceof CostSacrifice) {
|
||||||
|
final CostSacrifice sac = (CostSacrifice) part;
|
||||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||||
|
|
||||||
if (sac.payCostFromSource() && source.isCreature()) {
|
if (sac.payCostFromSource() && source.isCreature()) {
|
||||||
@@ -334,11 +346,12 @@ public class ComputerUtilCost {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostSacrifice sac) {
|
if (part instanceof CostSacrifice) {
|
||||||
if (suppressRecursiveSacCostCheck) {
|
if (suppressRecursiveSacCostCheck) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final CostSacrifice sac = (CostSacrifice) part;
|
||||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||||
|
|
||||||
String type = sac.getType();
|
String type = sac.getType();
|
||||||
@@ -437,7 +450,7 @@ public class ComputerUtilCost {
|
|||||||
* the source
|
* the source
|
||||||
* @return true, if successful
|
* @return true, if successful
|
||||||
*/
|
*/
|
||||||
public static boolean checkTapTypeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sa, final Collection<Card> alreadyTapped) {
|
public static boolean checkTapTypeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sa, final CardCollection alreadyTapped) {
|
||||||
if (cost == null) {
|
if (cost == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -474,8 +487,8 @@ public class ComputerUtilCost {
|
|||||||
c = AbilityUtils.calculateAmount(source, part.getAmount(), sa);
|
c = AbilityUtils.calculateAmount(source, part.getAmount(), sa);
|
||||||
}
|
}
|
||||||
CardCollection exclude = new CardCollection();
|
CardCollection exclude = new CardCollection();
|
||||||
if (alreadyTapped != null) {
|
if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST) != null) {
|
||||||
exclude.addAll(alreadyTapped);
|
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST));
|
||||||
}
|
}
|
||||||
// trying to produce mana that includes tapping source that will already be tapped
|
// trying to produce mana that includes tapping source that will already be tapped
|
||||||
if (exclude.contains(source) && cost.hasTapCost()) {
|
if (exclude.contains(source) && cost.hasTapCost()) {
|
||||||
@@ -487,12 +500,12 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
CardCollection tapChoices = ComputerUtil.chooseTapType(ai, type, source, cost.hasTapCost(), c, exclude, sa);
|
CardCollection tapChoices = ComputerUtil.chooseTapType(ai, type, source, cost.hasTapCost(), c, exclude, sa);
|
||||||
if (tapChoices != null) {
|
if (tapChoices != null) {
|
||||||
if (alreadyTapped != null) {
|
for (Card choice : tapChoices) {
|
||||||
alreadyTapped.addAll(tapChoices);
|
AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_TAP_COST);
|
||||||
|
}
|
||||||
// if manasource gets tapped to produce it also can't help paying another
|
// if manasource gets tapped to produce it also can't help paying another
|
||||||
if (cost.hasTapCost()) {
|
if (cost.hasTapCost()) {
|
||||||
alreadyTapped.add(source);
|
AiCardMemory.rememberCard(ai, source, MemorySet.PAYS_TAP_COST);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -514,19 +527,16 @@ public class ComputerUtilCost {
|
|||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
|
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
|
||||||
return canPayCost(sa.getPayCosts(), sa, player, effect);
|
|
||||||
}
|
|
||||||
public static boolean canPayCost(final Cost cost, final SpellAbility sa, final Player player, final boolean effect) {
|
|
||||||
if (sa.getActivatingPlayer() == null) {
|
if (sa.getActivatingPlayer() == null) {
|
||||||
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
|
sa.setActivatingPlayer(player, true); // complaints on NPE had came before this line was added.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean cannotBeCountered = false;
|
||||||
|
|
||||||
// Check for stuff like Nether Void
|
// Check for stuff like Nether Void
|
||||||
int extraManaNeeded = 0;
|
int extraManaNeeded = 0;
|
||||||
if (!effect) {
|
|
||||||
boolean cannotBeCountered = !sa.isCounterableBy(null);
|
|
||||||
|
|
||||||
if (sa instanceof Spell) {
|
if (sa instanceof Spell) {
|
||||||
|
cannotBeCountered = !sa.isCounterableBy(null);
|
||||||
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
|
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||||
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
||||||
if (!StringUtils.isBlank(snem)) {
|
if (!StringUtils.isBlank(snem)) {
|
||||||
@@ -562,7 +572,7 @@ public class ComputerUtilCost {
|
|||||||
|
|
||||||
// Try not to lose Planeswalker if not threatened
|
// Try not to lose Planeswalker if not threatened
|
||||||
if (sa.isPwAbility()) {
|
if (sa.isPwAbility()) {
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
if (part instanceof CostRemoveCounter) {
|
if (part instanceof CostRemoveCounter) {
|
||||||
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
|
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
|
||||||
// refuse to pay if opponent has no creature threats or
|
// refuse to pay if opponent has no creature threats or
|
||||||
@@ -576,24 +586,12 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account for possible Ward after the spell is fully targeted
|
// Ward - will be accounted for when rechecking a targeted ability
|
||||||
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
|
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
|
||||||
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
|
|
||||||
if (!sa.isTrigger() && !cannotBeCountered) {
|
|
||||||
Set<GameObject> distinctObjects = Sets.newHashSet();
|
|
||||||
for (TargetChoices tc : sa.getAllTargetChoices()) {
|
for (TargetChoices tc : sa.getAllTargetChoices()) {
|
||||||
for (Card tgt : tc.getTargetCards()) {
|
for (Card tgt : tc.getTargetCards()) {
|
||||||
if (!distinctObjects.add(tgt)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// TODO some older cards don't use the keyword, so check for trigger instead
|
|
||||||
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
|
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
|
||||||
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
||||||
// don't use API converter since it might have special part logic not meant for Ward cost
|
|
||||||
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
|
||||||
if (!topAI.willPayCosts(player, sa, wardCost, sa.getHostCard())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (wardCost.hasManaCost()) {
|
if (wardCost.hasManaCost()) {
|
||||||
extraManaNeeded += wardCost.getTotalMana().getCMC();
|
extraManaNeeded += wardCost.getTotalMana().getCMC();
|
||||||
}
|
}
|
||||||
@@ -602,31 +600,240 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Alternate costs which involve both paying mana and tapping a card, e.g. Zahid, Djinn of the Lamp
|
||||||
|
// Current AI decides on each part separately, thus making it possible for the AI to cheat by
|
||||||
|
// tapping a mana source for mana and for the tap cost at the same time. Until this is improved, AI
|
||||||
|
// will not consider mana sources valid for paying the tap cost to avoid this exact situation.
|
||||||
|
if ("DontPayTapCostWithManaSources".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
|
||||||
|
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
|
if (part instanceof CostTapType) {
|
||||||
|
CardCollectionView nonManaSources =
|
||||||
|
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
|
||||||
|
sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||||
|
nonManaSources = CardLists.filter(nonManaSources, card -> {
|
||||||
|
boolean hasManaSa = false;
|
||||||
|
for (final SpellAbility sa1 : card.getSpellAbilities()) {
|
||||||
|
if (sa1.isManaAbility() && sa1.getPayCosts().hasTapCost()) {
|
||||||
|
hasManaSa = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !hasManaSa;
|
||||||
|
});
|
||||||
|
if (nonManaSources.size() < part.convertAmount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Bail early on Casualty in case there are no cards that would make sense to pay with
|
// Bail early on Casualty in case there are no cards that would make sense to pay with
|
||||||
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
|
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
if (part instanceof CostSacrifice) {
|
if (part instanceof CostSacrifice) {
|
||||||
CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
|
CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
|
||||||
sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||||
valid = CardLists.filter(valid, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
|
valid = CardLists.filter(valid, Predicates.not(CardPredicates.hasSVar("AIDontSacToCasualty")));
|
||||||
if (valid.isEmpty()) {
|
if (valid.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
|
||||||
|
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO both of these call CostAdjustment.adjust, try to reuse instead
|
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||||
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
|
final Card source = sa.getHostCard();
|
||||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
|
final String aiLogic = sa.getParam("UnlessAI");
|
||||||
|
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
|
||||||
|
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
|
||||||
|
boolean payNever = "Never".equals(aiLogic);
|
||||||
|
boolean isMine = sa.getActivatingPlayer().equals(payer);
|
||||||
|
|
||||||
|
if (payNever) { return false; }
|
||||||
|
if (payForOwnOnly && !isMine) { return false; }
|
||||||
|
if (payOwner) {
|
||||||
|
final String defined = aiLogic.substring(7);
|
||||||
|
final Player player = AbilityUtils.getDefinedPlayers(source, defined, sa).get(0);
|
||||||
|
if (!payer.equals(player)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("OnlyDontControl".equals(aiLogic)) {
|
||||||
|
if (source == null || payer.equals(source.getController())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("Paralyze".equals(aiLogic)) {
|
||||||
|
final Card c = source.getEnchantingCard();
|
||||||
|
if (c == null || c.isUntapped()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("RiskFactor".equals(aiLogic)) {
|
||||||
|
final Player activator = sa.getActivatingPlayer();
|
||||||
|
if (!activator.canDraw()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("MorePowerful".equals(aiLogic)) {
|
||||||
|
final int sourceCreatures = sa.getActivatingPlayer().getCreaturesInPlay().size();
|
||||||
|
final int payerCreatures = payer.getCreaturesInPlay().size();
|
||||||
|
if (payerCreatures > sourceCreatures + 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (aiLogic != null && aiLogic.startsWith("LifeLE")) {
|
||||||
|
// if payer can't lose life its no need to pay unless
|
||||||
|
if (!payer.canLoseLife())
|
||||||
|
return false;
|
||||||
|
else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substring(6), sa)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if ("WillAttack".equals(aiLogic)) {
|
||||||
|
AiAttackController aiAtk = new AiAttackController(payer);
|
||||||
|
Combat combat = new Combat(payer);
|
||||||
|
aiAtk.declareAttackers(combat);
|
||||||
|
if (combat.getAttackers().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("nonToken".equals(aiLogic) && !AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).isEmpty()
|
||||||
|
&& AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) {
|
||||||
|
return false;
|
||||||
|
} else if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) {
|
||||||
|
return false;
|
||||||
|
} else if (aiLogic != null && aiLogic.startsWith("Fabricate")) {
|
||||||
|
final int n = Integer.parseInt(aiLogic.substring("Fabricate".length()));
|
||||||
|
|
||||||
|
// if host would leave the play or if host is useless, create tokens
|
||||||
|
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(payer, 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) + n);
|
||||||
|
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);
|
||||||
|
if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the host has haste and can attack
|
||||||
|
if (CombatUtil.canAttack(copy)) {
|
||||||
|
for (final Player opp : payer.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 this cases Token might be prefered even if they would not survive
|
||||||
|
final Card tokenCard = TokenAi.spawnToken(payer, sa);
|
||||||
|
|
||||||
|
// Token would not survive
|
||||||
|
if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special Card logic, this one try to median its power with the number of artifacts
|
||||||
|
if ("Marionette Master".equals(source.getName())) {
|
||||||
|
CardCollection list = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS);
|
||||||
|
return list.size() >= copy.getNetPower();
|
||||||
|
} else if ("Cultivator of Blades".equals(source.getName())) {
|
||||||
|
// Cultivator does try to median with number of Creatures
|
||||||
|
CardCollection list = payer.getCreaturesInPlay();
|
||||||
|
return list.size() >= copy.getNetPower();
|
||||||
|
}
|
||||||
|
|
||||||
|
// evaluate Creature with +1/+1
|
||||||
|
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
|
||||||
|
|
||||||
|
final CardCollection tokenList = new CardCollection(source);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
tokenList.add(TokenAi.spawnToken(payer, sa));
|
||||||
|
}
|
||||||
|
|
||||||
|
// evaluate Host with Tokens
|
||||||
|
int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList);
|
||||||
|
|
||||||
|
return evalToken < evalCounter;
|
||||||
|
} else if ("Riot".equals(aiLogic)) {
|
||||||
|
return !SpecialAiLogic.preferHasteForRiot(sa, payer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for shocklands and similar ETB replacement effects
|
||||||
|
if (sa.hasParam("ETB") && sa.getApi().equals(ApiType.Tap)) {
|
||||||
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
|
if (part instanceof CostPayLife) {
|
||||||
|
final CostPayLife lifeCost = (CostPayLife) part;
|
||||||
|
Integer amount = lifeCost.convertAmount();
|
||||||
|
if (payer.getLife() > (amount + 1) && payer.canPayLife(amount, true, sa)) {
|
||||||
|
final int landsize = payer.getLandsInPlay().size() + 1;
|
||||||
|
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
|
||||||
|
// Check if the AI has enough lands to play the card
|
||||||
|
if (landsize != c.getCMC()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Check if the AI intends to play the card and if it can pay for it with the mana it has
|
||||||
|
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
|
||||||
|
boolean canPay = c.getManaCost().canBePaidWithAvailable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
|
||||||
|
if (canPay && willPlay) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI will only pay when it's not already payed and only opponents abilities
|
||||||
|
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ward or human misplay
|
||||||
|
if (ApiType.Counter.equals(sa.getApi())) {
|
||||||
|
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
|
||||||
|
for (SpellAbility toBeCountered : spells) {
|
||||||
|
if (!toBeCountered.isCounterableBy(sa)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// no reason to pay if we don't plan to confirm
|
||||||
|
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered.getApi()).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// TODO check hasFizzled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI was crashing because the blank ability used to pay costs
|
||||||
|
// Didn't have any of the data on the original SA to pay dependant costs
|
||||||
|
|
||||||
|
return checkLifeCost(payer, cost, source, 4, sa)
|
||||||
|
&& checkDamageCost(payer, cost, source, 4, sa)
|
||||||
|
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
|
||||||
|
&& (isMine || checkDiscardCost(payer, cost, source, sa))
|
||||||
|
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
||||||
|
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
|
||||||
|
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
|
||||||
|
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||||
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
|
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
|
||||||
}
|
}
|
||||||
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) {
|
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) {
|
||||||
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.UNTAPPED);
|
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED);
|
||||||
Set<String> colorsAvailable = Sets.newHashSet();
|
Set<String> colorsAvailable = Sets.newHashSet();
|
||||||
|
|
||||||
if (additionalLands != null) {
|
if (additionalLands != null) {
|
||||||
@@ -716,8 +923,8 @@ public class ComputerUtilCost {
|
|||||||
|
|
||||||
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
|
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
|
||||||
if (source.usesTargeting()) {
|
if (source.usesTargeting()) {
|
||||||
final CardCollectionView targets = source.getTargets().getTargetCards();
|
final CardCollection targets = new CardCollection(source.getTargets().getTargetCards());
|
||||||
choices = IterableUtil.filter(choices, Predicate.not(CardPredicates.isController(ai).and(targets::contains)));
|
choices = Iterables.filter(choices, Predicates.not(Predicates.and(CardPredicates.isController(ai), Predicates.in(targets))));
|
||||||
}
|
}
|
||||||
return new CardCollection(choices);
|
return new CardCollection(choices);
|
||||||
}
|
}
|
||||||
|
|||||||