Compare commits

..

4 Commits

Author SHA1 Message Date
GitHub Actions
4f56efa339 [maven-release-plugin] prepare release forge-1.6.63 2024-06-28 16:38:00 +00:00
Chris H
232dec4e64 Update Forge.java 2024-06-28 12:08:34 -04:00
Chris H
c59375f69d Update pom.xml 2024-06-28 12:07:54 -04:00
Chris H
acccc18fc6 Update AndroidManifest.xml 2024-06-28 12:07:20 -04:00
20146 changed files with 132975 additions and 402008 deletions

View File

@@ -4,7 +4,6 @@ about: Create a report to help us improve
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
type: 'Bug'
--- ---
@@ -32,6 +31,7 @@ If applicable, add screenshots to help explain your problem.
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6] - Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1] - OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Additional context** **Additional context**

View File

@@ -4,7 +4,6 @@ about: Suggest an idea for this project
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
type: 'Feature'
--- ---

View File

@@ -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,134 +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 update
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 }}

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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
@@ -42,7 +45,7 @@ jobs:
run: | run: |
export DISPLAY=":1" export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 & Xvfb :1 -screen 0 800x600x8 &
mvn -U -B clean -P windows-linux install -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} mvn -U -B clean -P windows-linux install -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
@@ -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 }}

View File

@@ -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

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: ['17', '21'] 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

View File

@@ -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

10
.gitignore vendored
View File

@@ -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
@@ -66,9 +63,6 @@ forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat forge-gui/res/cardsfolder/*.bat
# Generated changelog file
forge-gui/release-files/CHANGES.txt
forge-gui/res/PerSetTrackingResults forge-gui/res/PerSetTrackingResults
forge-gui/res/decks forge-gui/res/decks
forge-gui/res/layouts forge-gui/res/layouts
@@ -90,7 +84,3 @@ forge-gui/tools/PerSetTrackingResults
*.tiled-session *.tiled-session
/forge-gui/res/adventure/*.tiled-project /forge-gui/res/adventure/*.tiled-project
/forge-gui/res/adventure/*.tiled-session /forge-gui/res/adventure/*.tiled-session
# Ignore python temporaries
__pycache__
*.pyc

View File

@@ -0,0 +1,33 @@
Summary
(Summarize the bug encountered concisely)
Steps to reproduce
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
What is the current bug behavior?
(What actually happens)
What is the expected correct behavior?
(What you should see instead)
Relevant logs and/or screenshots
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
Possible fixes
(If you can, link to the line of code that might be responsible for the problem)
/label ~needs-investigation

View File

@@ -0,0 +1,15 @@
Summary
(Summarize the feature you wish concisely)
Example screenshots
(If this is a UI change, please provide an example screenshot of how this feature might work)
Feature type
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
/label ~feature request

View File

@@ -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>
@@ -8,4 +20,4 @@
<password>${cardforge-repo.password}</password> <password>${cardforge-repo.password}</password>
</server> </server>
</servers> </servers>
</settings> </settings>

View File

@@ -1,195 +0,0 @@
# Contributing to Forge
[Official repo](https://github.com/Card-Forge/forge.git).
Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wiki) (Somewhat outdated)
## Requirements / Tools
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- Java JDK 17 or later
- Git
- Git client (optional)
- Maven
- GitHub account
- Libgdx (optional: familiarity with this library is helpful for mobile platform development)
- Android SDK (optional: for Android releases)
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
## Project Quick Setup
- Login into GitHub with your user account and fork the project.
- Clone your forked project to your local machine
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
## IntelliJ
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 includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
At this time, Eclipse is not the recommended IDE for Forge development.
### Project Setup
- Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
- Fork the Forge git repo to your GitHub account.
- 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.
- Install Eclipse 2021-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
ensure everything is checked > Finish.
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
for this first time through.
- Once everything builds, all errors should disappear. You can now advance to Project launch.
### Project Launch
#### Desktop
This is the standard configuration used for releasing to Windows / Linux / MacOS.
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Ok
- The familiar Forge splash screen, etc. should appear. Enjoy!
#### Mobile (Desktop dev)
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
- Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Ok.
- A view similar to a mobile phone should appear. Enjoy!
### Eclipse / Android SDK Integration
Google no longer supports Android SDK releases for Eclipse. use IntelliJ.
#### Android SDK
TBD
##### Windows
TBD
##### Linux / Mac OSX
TBD
#### Android Plugin for Eclipse
TBD
#### Android Platform
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
- Android SDK Build-tools 35.0.0
- Android 15 (API 35) SDK Platform
#### 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).
#### Android Build
TBD
#### Android Deploy
TBD
#### Android Debugging
TBD
### Windows / Linux SNAPSHOT build
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...
- 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.
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
## Card Scripting
Visit [this page](https://github.com/Card-Forge/forge/wiki/Card-scripting-API) for information on scripting.
Card scripting resources are found in the forge-gui/res/ path.
## General Notes
### Project Hierarchy
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
- forge-ai
- forge-core
- forge-game
- forge-gui
The platform-specific projects are:
- forge-gui-android
- forge-gui-desktop
- forge-gui-ios
- forge-gui-mobile
- forge-gui-mobile-dev
#### forge-ai
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
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
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
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.
#### forge-gui-android
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
#### forge-gui-desktop
Java Swing based GUI targeting desktop machines.
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
#### forge-gui-ios
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
#### forge-gui-mobile
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
#### forge-gui-mobile-dev
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.

261
README.md
View File

@@ -1,105 +1,230 @@
# ⚔️ Forge: The Magic: The Gathering Rules Engine # Forge
Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)! [Official repo](https://github.com/Card-Forge/forge.git).
Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wiki) (Somewhat outdated)
[Discord channel](https://discord.gg/HcPJNyD66a)
[![Test build](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml/badge.svg)](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml) [![Test build](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml/badge.svg)](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml)
--- ## Requirements / Tools
## ✨ Introduction - you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- Java JDK 8 or later (some IDEs such as Eclipse require JDK11+, whereas the Android build currently only works with JDK8)
- Git
- Git client (optional)
- Maven
- GitHub account
- Libgdx (optional: familiarity with this library is helpful for mobile platform development)
- Android SDK (optional: for Android releases)
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
**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. ## Project Quick Setup
**Note:** Forge operates independently and is not affiliated with Wizards of the Coast. - Login into GitHub with your user account and fork the project.
--- - Clone your forked project to your local machine
## 🌟 Key Features - Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
- **🌐 Cross-Platform Support:** Play on **Windows, Mac, Linux,** and **Android**. ## Eclipse
- **🔧 Extensible Architecture:** Built in **Java**, Forge encourages developers to contribute by adding features and cards.
- **🎮 Versatile Gameplay:** Dive into single-player modes or challenge opponents online!
--- Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
## 🛠️ Installation Guide ### Project Setup
### 📥 Desktop Installation - Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
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 If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
- _(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)_ the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
- 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. "SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
--- - Fork the Forge git repo to your GitHub account.
## 🎮 Modes of Play - Clone your forked repo to your local machine.
Forge offers various exciting gameplay options: - 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.
### 🌍 Adventure Mode - Install Eclipse 2018-12 or later for Java. Launch it.
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" /> - 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.
### 🔍 Quest Modes - Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
Engage in focused gameplay without the overworld exploration—perfect for quick sessions! be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
for this first time through.
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" /> - Once everything builds, all errors should disappear. You can now advance to Project launch.
### 🤖 AI Formats ### Project Launch
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). #### Desktop
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" /> This is the standard configuration used for releasing to Windows / Linux / MacOS.
--- - Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Ok
## 💬 Support & Community - The familiar Forge splash screen, etc. should appear. Enjoy!
Need help? Join our vibrant Discord community! #### Mobile (Desktop dev)
- 📜 Read the **#rules** and explore the **FAQ**.
- ❓ Ask your questions in the **#help** channel for assistance.
--- This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
## 🤝 Contributing to Forge - Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Ok.
We love community contributions! Interested in helping? Check out our [Contributing Guidelines](CONTRIBUTING.md) for details on how to get started. - A view similar to a mobile phone should appear. Enjoy!
--- ### Eclipse / Android SDK Integration
## About Forge Google no longer supports Android SDK releases for Eclipse. That said, it is still possible to build and debug Android platforms.
Forge aims to deliver an immersive and customizable Magic: The Gathering experience for fans around the world. #### Android SDK
### 📊 Repository Statistics Reference SO for obtaining a specific release: https://stackoverflow.com/questions/27043522/where-can-i-download-an-older-version-of-the-android-sdk
| Metric | Count | ##### Windows
|----------------|-------------------------------------------------------------|
| **⭐ Stars:** | [![GitHub stars](https://img.shields.io/github/stars/Card-Forge/forge?style=flat-square)](https://github.com/Card-Forge/forge/stargazers) |
| **🍴 Forks:** | [![GitHub forks](https://img.shields.io/github/forks/Card-Forge/forge?style=flat-square)](https://github.com/Card-Forge/forge/network) |
| **👥 Contributors:** | [![GitHub contributors](https://img.shields.io/github/contributors/Card-Forge/forge?style=flat-square)](https://github.com/Card-Forge/forge/graphs/contributors) |
--- 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.
**📄 License:** [GPL-3.0](LICENSE) ##### Linux / Mac OSX
<div align="center" style="display: flex; align-items: center; justify-content: center;">
<div style="margin-left: auto;"> TBD
<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"> #### Android Plugin for Eclipse
</a>
</div> 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
</div> 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
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 26.0.1
- 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
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
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
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
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
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...
- 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.
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
## IntelliJ
Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
## Card Scripting
Visit [this page](https://github.com/Card-Forge/forge/wiki/Card-scripting-API) for information on scripting.
Card scripting resources are found in the forge-gui/res/ path.
## General Notes
### Project Hierarchy
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
- forge-ai
- forge-core
- forge-game
- forge-gui
The platform-specific projects are:
- forge-gui-android
- forge-gui-desktop
- forge-gui-ios
- forge-gui-mobile
- forge-gui-mobile-dev
#### forge-ai
#### forge-core
#### forge-game
#### forge-gui
The forge-gui project includes the scripting resource definitions in the res/ path.
#### forge-gui-android
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
#### forge-gui-desktop
Java Swing based GUI targeting desktop machines.
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
#### forge-gui-ios
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
#### forge-gui-mobile
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
#### forge-gui-mobile-dev
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

View File

@@ -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

View File

@@ -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);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

299
forge-adventure/pom.xml Normal file
View File

@@ -0,0 +1,299 @@
<?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.63</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>forge-adventure</artifactId>
<packaging>jar</packaging>
<name>Forge Adventure</name>
<repositories>
<repository>
<id>gdx-nightlies</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</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>**/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.11.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.11.0</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>1.11.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-tools</artifactId>
<version>1.11.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype-platform</artifactId>
<version>1.11.0</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-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>

View 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);
}

View 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;
}

View 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);
}

View 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;
}

View 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);
}

View 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);
}

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd $(dirname "${0}")
java -XstartOnFirstThread -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$

View 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

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -15,7 +15,8 @@ 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(null, 0, 0, System.getProperty("user.home") + "/Downloads/"); GuiBase.setDeviceInfo("", "", 0, 0);
new EditorMainWindow(Config.instance()); Config.instance();
new EditorMainWindow();
} }
} }

View File

@@ -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);

View File

@@ -2,27 +2,20 @@ 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;
/** /**
* Editor class to edit configuration, maybe moved or removed * Editor class to edit configuration, maybe moved or removed
*/ */
public class ActionEditor extends JComponent { public class ActionEditor extends JComponent{
DefaultListModel<DialogData.ActionData> model = new DefaultListModel<>(); DefaultListModel<DialogData.ActionData> model = new DefaultListModel<>();
JList<DialogData.ActionData> list = new JList<>(model); JList<DialogData.ActionData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar"); JToolBar toolBar = new JToolBar("toolbar");
ActionEdit edit = new ActionEdit(); ActionEdit edit=new ActionEdit();
boolean updating; boolean updating;
@@ -32,43 +25,48 @@ public class ActionEditor 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.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());
addButton("add", e -> ActionEditor.this.addAction()); addButton("add", e -> ActionEditor.this.addAction());
addButton("remove", e -> ActionEditor.this.remove()); addButton("remove", e -> ActionEditor.this.remove());
addButton("copy", e -> ActionEditor.this.copy()); addButton("copy", e -> ActionEditor.this.copy());
BorderLayout layout = new BorderLayout(); BorderLayout layout=new BorderLayout();
setLayout(layout); setLayout(layout);
add(list, BorderLayout.LINE_START); add(list, BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START); add(toolBar, BorderLayout.PAGE_START);
add(edit, BorderLayout.CENTER); add(edit,BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged()); edit.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
emitChanged();
}
});
} }
protected void emitChanged() { protected void emitChanged() {
if (updating) if (updating)
return; return;
@@ -80,64 +78,63 @@ public class ActionEditor extends JComponent {
} }
} }
} }
private void copy() { private void copy() {
int selected = list.getSelectedIndex(); int selected=list.getSelectedIndex();
if (selected < 0) if(selected<0)
return; return;
DialogData.ActionData data = new DialogData.ActionData(model.get(selected)); DialogData.ActionData data=new DialogData.ActionData(model.get(selected));
model.add(model.size(), data); model.add(model.size(),data);
} }
private void updateEdit() { private void updateEdit() {
int selected = list.getSelectedIndex(); int selected=list.getSelectedIndex();
if (selected < 0) if(selected<0)
return; return;
edit.setCurrentAction(model.get(selected)); edit.setCurrentAction(model.get(selected));
} }
void addAction() { void addAction()
DialogData.ActionData data = new DialogData.ActionData(); {
model.add(model.size(), data); DialogData.ActionData data=new DialogData.ActionData();
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();
if (actions == null) if(actions==null)
return; return;
for (int i = 0; i < actions.length; i++) { for (int i=0;i<actions.length;i++) {
if (actions[i].grantRewards.length > 0) { if (actions[i].grantRewards.length > 0){
continue; //handled in separate editor and joined in on save, will get duplicated if it appears here continue; //handled in separate editor and joined in on save, will get duplicated if it appears here
} }
model.add(i, actions[i]); model.add(i,actions[i]);
} }
} }
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;
} }
public void clear() { public void clear(){
updating = true; updating = true;
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);
} }

View File

@@ -51,12 +51,12 @@ public class BiomeEdit extends FormPanel {
add(terrain); add(terrain);
add(structures); add(structures);
name.getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain)); name.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeEdit.this.updateTerrain()));
tilesetName.getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain)); tilesetName.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeEdit.this.updateTerrain()));
color.getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain)); color.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeEdit.this.updateTerrain()));
collision.addChangeListener(e -> BiomeEdit.this.updateTerrain()); collision.addChangeListener(e -> BiomeEdit.this.updateTerrain());
spriteNames.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain)); spriteNames.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeEdit.this.updateTerrain()));
enemies.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain)); enemies.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeEdit.this.updateTerrain()));
terrain.addChangeListener(e -> BiomeEdit.this.updateTerrain()); terrain.addChangeListener(e -> BiomeEdit.this.updateTerrain());
@@ -64,7 +64,7 @@ public class BiomeEdit extends FormPanel {
startPointY.addChangeListener(e -> BiomeEdit.this.updateTerrain()); startPointY.addChangeListener(e -> BiomeEdit.this.updateTerrain());
noiseWeight.addChangeListener(e -> BiomeEdit.this.updateTerrain()); noiseWeight.addChangeListener(e -> BiomeEdit.this.updateTerrain());
distWeight.addChangeListener(e -> BiomeEdit.this.updateTerrain()); distWeight.addChangeListener(e -> BiomeEdit.this.updateTerrain());
tilesetAtlas.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain)); tilesetAtlas.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeEdit.this.updateTerrain()));
width.addChangeListener(e -> BiomeEdit.this.updateTerrain()); width.addChangeListener(e -> BiomeEdit.this.updateTerrain());
height.addChangeListener(e -> BiomeEdit.this.updateTerrain()); height.addChangeListener(e -> BiomeEdit.this.updateTerrain());
refresh(); refresh();

View File

@@ -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();
@@ -134,8 +135,8 @@ public class BiomeStructureDataMappingEditor extends JComponent {
add("color:",color); add("color:",color);
add("collision:",collision); add("collision:",collision);
name.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureDataMappingEdit.this::update)); name.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeStructureDataMappingEdit.this.update()));
color.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureDataMappingEdit.this::update)); color.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeStructureDataMappingEdit.this.update()));
collision.addChangeListener(e -> BiomeStructureDataMappingEdit.this.update()); collision.addChangeListener(e -> BiomeStructureDataMappingEdit.this.update());
refresh(); refresh();
} }

View File

@@ -45,7 +45,7 @@ public class BiomeStructureEdit extends FormPanel {
add(center); add(center);
add(data); add(data);
structureAtlasPath.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureEdit.this::updateStructure)); structureAtlasPath.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeStructureEdit.this.updateStructure()));
x.addChangeListener(e -> BiomeStructureEdit.this.updateStructure()); x.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
@@ -55,8 +55,8 @@ public class BiomeStructureEdit extends FormPanel {
randomPosition.addChangeListener(e -> BiomeStructureEdit.this.updateStructure()); randomPosition.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
N.addChangeListener(e -> BiomeStructureEdit.this.updateStructure()); N.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
sourcePath.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureEdit.this::updateStructure)); sourcePath.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeStructureEdit.this.updateStructure()));
maskPath.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureEdit.this::updateStructure)); maskPath.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeStructureEdit.this.updateStructure()));
periodicInput.addChangeListener(e -> BiomeStructureEdit.this.updateStructure()); periodicInput.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
ground.addChangeListener(e -> BiomeStructureEdit.this.updateStructure()); ground.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
symmetry.addChangeListener(e -> BiomeStructureEdit.this.updateStructure()); symmetry.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());

View File

@@ -27,7 +27,7 @@ public class BiomeTerrainEdit extends FormPanel {
center.add("resolution:",resolution); center.add("resolution:",resolution);
add(center,preview); add(center,preview);
spriteName.getDocument().addDocumentListener(new DocumentChangeListener(BiomeTerrainEdit.this::updateTerrain)); spriteName.getDocument().addDocumentListener(new DocumentChangeListener(() -> BiomeTerrainEdit.this.updateTerrain()));
min.addChangeListener(e -> BiomeTerrainEdit.this.updateTerrain()); min.addChangeListener(e -> BiomeTerrainEdit.this.updateTerrain());
max.addChangeListener(e -> BiomeTerrainEdit.this.updateTerrain()); max.addChangeListener(e -> BiomeTerrainEdit.this.updateTerrain());

View File

@@ -40,8 +40,8 @@ public class DialogOptionEdit extends FormPanel {
add(middle); add(middle);
name.getDocument().addDocumentListener(new DocumentChangeListener(DialogOptionEdit.this::updateDialog)); name.getDocument().addDocumentListener(new DocumentChangeListener(() -> DialogOptionEdit.this.updateDialog()));
text.getDocument().addDocumentListener(new DocumentChangeListener(DialogOptionEdit.this::updateDialog)); text.getDocument().addDocumentListener(new DocumentChangeListener(() -> DialogOptionEdit.this.updateDialog()));
} }

View File

@@ -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]]");

View File

@@ -49,7 +49,12 @@ public class DialogTree extends JPanel {
public void addSelectionListener(){ public void addSelectionListener(){
//subscribe to valueChanged, change to that object in edit pane //subscribe to valueChanged, change to that object in edit pane
dialogTree.getSelectionModel().addTreeSelectionListener(this::emitChanged); dialogTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
emitChanged(e);
}
});
} }

View File

@@ -0,0 +1,67 @@
package forge.adventure.editor;
import com.badlogic.gdx.tools.particleeditor.ParticleEditor;
import com.google.common.base.Function;
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, new Function<ForgePreferences, Void>() {
@Override
public Void apply(ForgePreferences 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(int var3 = 0; var3 < var2; ++var3) {
UIManager.LookAndFeelInfo info = var1[var3];
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(() ->new ParticleEditor()));
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 );
}
}

View File

@@ -42,8 +42,8 @@ public class EffectEditor extends JComponent {
lifeModifier.addChangeListener(e -> EffectEditor.this.updateEffect()); lifeModifier.addChangeListener(e -> EffectEditor.this.updateEffect());
moveSpeed.addChangeListener(e -> EffectEditor.this.updateEffect()); moveSpeed.addChangeListener(e -> EffectEditor.this.updateEffect());
colorView.addChangeListener(e -> EffectEditor.this.updateEffect()); colorView.addChangeListener(e -> EffectEditor.this.updateEffect());
name.getDocument().addDocumentListener(new DocumentChangeListener(EffectEditor.this::updateEffect)); name.getDocument().addDocumentListener(new DocumentChangeListener(() -> EffectEditor.this.updateEffect()));
startBattleWithCard.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(EffectEditor.this::updateEffect)); startBattleWithCard.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> EffectEditor.this.updateEffect()));
if(opponent!=null) if(opponent!=null)
opponent.addChangeListener(e -> EffectEditor.this.updateEffect()); opponent.addChangeListener(e -> EffectEditor.this.updateEffect());
@@ -57,9 +57,9 @@ public class EffectEditor extends JComponent {
currentData.name=name.getText(); currentData.name=name.getText();
currentData.changeStartCards = (Integer) changeStartCards.getValue(); currentData.changeStartCards=((Integer)changeStartCards.getValue()).intValue();
currentData.lifeModifier = (Integer) lifeModifier.getValue(); currentData.lifeModifier= ((Integer)lifeModifier.getValue()).intValue();
currentData.moveSpeed = (Float) moveSpeed.getValue(); currentData.moveSpeed= ((Float)moveSpeed.getValue()).floatValue();
currentData.startBattleWithCard = startBattleWithCard.getList(); currentData.startBattleWithCard = startBattleWithCard.getList();
currentData.colorView = colorView.isSelected(); currentData.colorView = colorView.isSelected();
currentData.opponent = opponent.currentData; currentData.opponent = opponent.currentData;

View File

@@ -289,7 +289,7 @@ public class EnemyEdit extends FormPanel {
tags.add(e.nextElement()); tags.add(e.nextElement());
} }
tags.removeIf(String::isEmpty); tags.removeIf(q -> q.isEmpty());
currentData.questTags = tags.toArray(currentData.questTags); currentData.questTags = tags.toArray(currentData.questTags);
QuestController.getInstance().refresh(); QuestController.getInstance().refresh();
filterExisting(enemyModel); filterExisting(enemyModel);

View File

@@ -4,6 +4,8 @@ import forge.adventure.util.Config;
import javax.swing.*; import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -19,7 +21,12 @@ public class FilePicker extends Box {
super(BoxLayout.X_AXIS); super(BoxLayout.X_AXIS);
this.fileEndings = fileEndings; this.fileEndings = fileEndings;
findButton.addActionListener(e -> FilePicker.this.find()); findButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
FilePicker.this.find();
}
});
add(edit); add(edit);
add(findButton); add(findButton);

View File

@@ -14,6 +14,6 @@ public class FloatSpinner extends JSpinner{
} }
public float floatValue() public float floatValue()
{ {
return (Float) getValue(); return ((Float)getValue()).floatValue();
} }
} }

View File

@@ -15,6 +15,6 @@ public class IntSpinner extends JSpinner {
} }
public int intValue() public int intValue()
{ {
return (Integer) getValue(); return ((Integer)getValue()).intValue();
} }
} }

View File

@@ -38,10 +38,10 @@ public class ItemEdit extends JComponent {
add(effect); add(effect);
add(new Box.Filler(new Dimension(0,0),new Dimension(0,Integer.MAX_VALUE),new Dimension(0,Integer.MAX_VALUE))); add(new Box.Filler(new Dimension(0,0),new Dimension(0,Integer.MAX_VALUE),new Dimension(0,Integer.MAX_VALUE)));
nameField.getDocument().addDocumentListener(new DocumentChangeListener(ItemEdit.this::updateItem)); nameField.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
equipmentSlot.getDocument().addDocumentListener(new DocumentChangeListener(ItemEdit.this::updateItem)); equipmentSlot.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
description.getDocument().addDocumentListener(new DocumentChangeListener(ItemEdit.this::updateItem)); description.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
iconName.getDocument().addDocumentListener(new DocumentChangeListener(ItemEdit.this::updateItem)); iconName.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
cost.addChangeListener(e -> ItemEdit.this.updateItem()); cost.addChangeListener(e -> ItemEdit.this.updateItem());
questItem.addChangeListener(e -> ItemEdit.this.updateItem()); questItem.addChangeListener(e -> ItemEdit.this.updateItem());
effect.addChangeListener(e -> ItemEdit.this.updateItem()); effect.addChangeListener(e -> ItemEdit.this.updateItem());
@@ -57,7 +57,7 @@ public class ItemEdit extends JComponent {
currentData.description=description.getText(); currentData.description=description.getText();
currentData.iconName=iconName.getText(); currentData.iconName=iconName.getText();
currentData.questItem=questItem.isSelected(); currentData.questItem=questItem.isSelected();
currentData.cost= (Integer) cost.getValue(); currentData.cost=((Integer) cost.getValue()).intValue();
} }
public void setCurrentItem(ItemData data) public void setCurrentItem(ItemData data)

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
@@ -58,18 +58,18 @@ public class RewardEdit extends FormPanel {
probability.addChangeListener(e -> RewardEdit.this.updateReward()); probability.addChangeListener(e -> RewardEdit.this.updateReward());
count.addChangeListener(e -> RewardEdit.this.updateReward()); count.addChangeListener(e -> RewardEdit.this.updateReward());
addMaxCount.addChangeListener(e -> RewardEdit.this.updateReward()); addMaxCount.addChangeListener(e -> RewardEdit.this.updateReward());
cardName.getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); cardName.getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
itemName.getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); itemName.getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
editions.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); editions.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
colors.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); colors.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
rarity.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); rarity.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
subTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); subTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
cardTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); cardTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
superTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); superTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
manaCosts.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); manaCosts.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
keyWords.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); keyWords.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
colorType.addActionListener((e -> RewardEdit.this.updateReward())); colorType.addActionListener((e -> RewardEdit.this.updateReward()));
cardText.getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward)); cardText.getDocument().addDocumentListener(new DocumentChangeListener(() -> RewardEdit.this.updateReward()));
} }
@@ -122,7 +122,7 @@ public class RewardEdit extends FormPanel {
updating=true; updating=true;
typeField.setSelectedItem(currentData.type); typeField.setSelectedItem(currentData.type);
probability.setValue((double) currentData.probability); probability.setValue(new Double(currentData.probability));
count.setValue(currentData.count); count.setValue(currentData.count);
addMaxCount.setValue(currentData.addMaxCount); addMaxCount.setValue(currentData.addMaxCount);
cardName.setText(currentData.cardName); cardName.setText(currentData.cardName);

View File

@@ -68,7 +68,12 @@ public class StructureEditor extends JComponent{
add(edit,BorderLayout.CENTER); add(edit,BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged()); edit.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
emitChanged();
}
});
} }
protected void emitChanged() { protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class); ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);

View File

@@ -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;
} }

View File

@@ -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;
} }
} }
@@ -67,7 +60,12 @@ public class TerrainsEditor extends JComponent{
add(edit,BorderLayout.CENTER); add(edit,BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged()); edit.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
emitChanged();
}
});
} }
protected void emitChanged() { protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class); ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);

View File

@@ -9,6 +9,8 @@ import forge.adventure.util.Config;
import forge.adventure.util.Paths; import forge.adventure.util.Paths;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -43,8 +45,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);
@@ -78,7 +81,12 @@ public class WorldEditor extends JComponent {
public WorldEditor() { public WorldEditor() {
list.setCellRenderer(new BiomeDataRenderer()); list.setCellRenderer(new BiomeDataRenderer());
list.addListSelectionListener(e -> WorldEditor.this.updateBiome()); list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
WorldEditor.this.updateBiome();
}
});
BorderLayout layout = new BorderLayout(); BorderLayout layout = new BorderLayout();
setLayout(layout); setLayout(layout);
add(tabs); add(tabs);

View File

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

View File

@@ -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
@@ -24,9 +24,9 @@ public class AiDeckStatistics {
// in WUBRGC order from ManaCost.getColorShardCounts() // in WUBRGC order from ManaCost.getColorShardCounts()
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);
} }
} }

View File

@@ -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();
}
}

View File

@@ -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);
} }
@@ -134,26 +124,29 @@ public class AiAttackController {
List<Card> defenders = defender.getCreaturesInPlay(); List<Card> defenders = defender.getCreaturesInPlay();
int totalMana = ComputerUtilMana.getAvailableManaEstimate(defender, true); int totalMana = ComputerUtilMana.getAvailableManaEstimate(defender, true);
int manaReserved = 0; // for paying the cost to transform int manaReserved = 0; // for paying the cost to transform
Predicate<Card> canAnimate = c -> !c.isTapped() && !c.isCreature() && !c.isPlaneswalker(); Predicate<Card> canAnimate = new Predicate<Card>() {
@Override
public boolean apply(Card c) {
return !c.isTapped() && !c.isCreature() && !c.isPlaneswalker();
}
};
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 +157,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() ?
@@ -180,7 +173,7 @@ public class AiAttackController {
} }
public void removeBlocker(Card blocker) { public void removeBlocker(Card blocker) {
this.oppList.remove(blocker); this.oppList.remove(blocker);
this.blockers.remove(blocker); this.blockers.remove(blocker);
} }
@@ -297,7 +290,7 @@ public class AiAttackController {
} }
if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) { if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) {
return true; return true;
} }
// Damage opponent if unblocked // Damage opponent if unblocked
@@ -315,8 +308,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;
} }
@@ -339,7 +331,12 @@ public class AiAttackController {
} }
public final static List<Card> getPossibleBlockers(final List<Card> blockers, final List<Card> attackers, final boolean nextTurn) { public final static List<Card> getPossibleBlockers(final List<Card> blockers, final List<Card> attackers, final boolean nextTurn) {
return CardLists.filter(blockers, c -> canBlockAnAttacker(c, attackers, nextTurn)); return CardLists.filter(blockers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return canBlockAnAttacker(c, attackers, nextTurn);
}
});
} }
public final static boolean canBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) { public final static boolean canBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) {
@@ -398,10 +395,15 @@ public class AiAttackController {
} }
} }
// reduce the search space // reduce the search space
final List<Card> opponentsAttackers = CardLists.filter(ai.getOpponents().getCreaturesInPlay(), c -> !c.hasSVar("EndOfTurnLeavePlay") final List<Card> opponentsAttackers = CardLists.filter(ai.getOpponents().getCreaturesInPlay(), new Predicate<Card>() {
&& (c.toughnessAssignsDamage() || c.getNetCombatDamage() > 0 // performance shortcuts @Override
|| c.getNetCombatDamage() + ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true) > 0) public boolean apply(final Card c) {
&& ComputerUtilCombat.canAttackNextTurn(c)); return !c.hasSVar("EndOfTurnLeavePlay")
&& (c.toughnessAssignsDamage() || c.getNetCombatDamage() > 0 // performance shortcuts
|| c.getNetCombatDamage() + ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true) > 0)
&& ComputerUtilCombat.canAttackNextTurn(c);
}
});
// don't hold back creatures that can't block any of the human creatures // don't hold back creatures that can't block any of the human creatures
final List<Card> blockers = getPossibleBlockers(potentialAttackers, opponentsAttackers, true); final List<Card> blockers = getPossibleBlockers(potentialAttackers, opponentsAttackers, true);
@@ -522,9 +524,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 +538,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 +556,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 +564,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 +629,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));
} }
} }
@@ -768,7 +770,7 @@ public class AiAttackController {
return false; return false;
} }
private Pair<Integer, Integer> getDamageFromBlockingTramplers(final List<Card> blockedAttackers, final List<Card> blockers, final int myFreeMana) { private final Pair<Integer, Integer> getDamageFromBlockingTramplers(final List<Card> blockedAttackers, final List<Card> blockers, final int myFreeMana) {
int currentAttackTax = 0; int currentAttackTax = 0;
int trampleDamage = 0; int trampleDamage = 0;
CardCollection remainingBlockers = new CardCollection(blockers); CardCollection remainingBlockers = new CardCollection(blockers);
@@ -795,7 +797,7 @@ public class AiAttackController {
return Pair.of(trampleDamage, currentAttackTax); return Pair.of(trampleDamage, currentAttackTax);
} }
private GameEntity chooseDefender(final Combat c, final boolean bAssault) { private final GameEntity chooseDefender(final Combat c, final boolean bAssault) {
final FCollectionView<GameEntity> defs = c.getDefenders(); final FCollectionView<GameEntity> defs = c.getDefenders();
if (defs.size() == 1) { if (defs.size() == 1) {
return defs.getFirst(); return defs.getFirst();
@@ -806,7 +808,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 +815,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 +854,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 +897,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,35 +913,35 @@ 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; GameEntity mustAttackDef = null;
futures.add(CompletableFuture.supplyAsync(()-> { if (attacker.getSVar("MustAttack").equals("True")) {
GameEntity mustAttackDef = null; mustAttackDef = defender;
if (attacker.getSVar("MustAttack").equals("True")) { } else if (attacker.hasSVar("EndOfTurnLeavePlay")
mustAttackDef = finalDefender; && isEffectiveAttacker(ai, attacker, combat, defender)) {
} else if (attacker.hasSVar("EndOfTurnLeavePlay") mustAttackDef = defender;
&& isEffectiveAttacker(ai, attacker, combat, finalDefender)) { } else if (seasonOfTheWitch) {
mustAttackDef = finalDefender; //TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
} else if (seasonOfTheWitch) { mustAttackDef = defender;
//TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack } else {
mustAttackDef = finalDefender; if (combat.getAttackConstraints().getRequirements().get(attacker) == null) continue;
} else { // check defenders in order of maximum requirements
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) return 0; List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements();
// check defenders in order of maximum requirements final GameEntity def = defender;
List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements(); reqs.sort(new Comparator<Pair<GameEntity, Integer>>() {
final GameEntity def = finalDefender; @Override
reqs.sort((r1, r2) -> { public int compare(Pair<GameEntity, Integer> r1, Pair<GameEntity, Integer> r2) {
if (r1.getValue() == r2.getValue()) { if (r1.getValue() == r2.getValue()) {
// try to attack the designated defender // try to attack the designated defender
if (r1.getKey().equals(def) && !r2.getKey().equals(def)) { if (r1.getKey().equals(def) && !r2.getKey().equals(def)) {
return -1; return -1;
} }
if (r2.getKey().equals(def) && !r1.getKey().equals(def)) { if (r2.getKey().equals(def) && !r1.getKey().equals(def)) {
return 1; return 1;
} }
// otherwise PW // otherwise PW
if (r1.getKey() instanceof Card && r2.getKey() instanceof Player) { if (r1.getKey() instanceof Card && r2.getKey() instanceof Player) {
@@ -949,38 +951,28 @@ 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();
}); }
for (Pair<GameEntity, Integer> e : reqs) { });
if (e.getRight() == 0) continue; for (Pair<GameEntity, Integer> e : reqs) {
GameEntity mustAttackDefMaybe = e.getLeft(); if (e.getRight() == 0) continue;
if (canAttackWrapper(attacker, mustAttackDefMaybe) && CombatUtil.getAttackCost(ai.getGame(), attacker, mustAttackDefMaybe) == null) { GameEntity mustAttackDefMaybe = e.getLeft();
mustAttackDef = mustAttackDefMaybe; if (canAttackWrapper(attacker, mustAttackDefMaybe) && CombatUtil.getAttackCost(ai.getGame(), attacker, mustAttackDefMaybe) == null) {
break; mustAttackDef = mustAttackDefMaybe;
} break;
} }
} }
if (mustAttackDef != null) { }
combat.addAttacker(attacker, mustAttackDef); if (mustAttackDef != null) {
attackersLeft.remove(attacker); combat.addAttacker(attacker, mustAttackDef);
numForcedAttackers.incrementAndGet(); attackersLeft.remove(attacker);
} 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 +980,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;
@@ -1182,8 +1173,8 @@ public class AiAttackController {
while (!attritionalAttackers.isEmpty() && humanLife > 0 && attackRounds < 99) { while (!attritionalAttackers.isEmpty() && humanLife > 0 && attackRounds < 99) {
// sum attacker damage // sum attacker damage
int damageThisRound = 0; int damageThisRound = 0;
for (Card attritionalAttacker : attritionalAttackers) { for (int y = 0; y < attritionalAttackers.size(); y++) {
damageThisRound += attritionalAttacker.getNetCombatDamage(); damageThisRound += attritionalAttackers.get(y).getNetCombatDamage();
} }
// remove from player life // remove from player life
humanLife -= damageThisRound; humanLife -= damageThisRound;
@@ -1194,8 +1185,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
@@ -1252,7 +1245,7 @@ public class AiAttackController {
if (ratioDiff > 0 && doAttritionalAttack) { if (ratioDiff > 0 && doAttritionalAttack) {
aiAggression = 5; // attack at all costs aiAggression = 5; // attack at all costs
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0)) } else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|| (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) { || (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) {
aiAggression = 4; // attack expecting to trade or damage player. aiAggression = 4; // attack expecting to trade or damage player.
} else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1 } else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1
&& defendingOpponent != null && defendingOpponent != null
@@ -1262,7 +1255,7 @@ public class AiAttackController {
&& (ComputerUtilMana.getAvailableManaEstimate(ai) > 0) || tradeIfTappedOut && (ComputerUtilMana.getAvailableManaEstimate(ai) > 0) || tradeIfTappedOut
&& (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana) && (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana)
&& (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() < && (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() <
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) { defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) {
aiAggression = 4; // random (chance-based) attack expecting to trade or damage player. aiAggression = 4; // random (chance-based) attack expecting to trade or damage player.
} else if (ratioDiff >= 0 && this.attackers.size() > 1) { } else if (ratioDiff >= 0 && this.attackers.size() > 1) {
aiAggression = 3; // attack expecting to make good trades or damage player. aiAggression = 3; // attack expecting to make good trades or damage player.
@@ -1290,20 +1283,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 +1309,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 +1323,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,113 +1349,6 @@ public class AiAttackController {
return aiAggression; return aiAggression;
} }
private class SpellAbilityFactors {
Card attacker = null;
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 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 isWorthLessThanAllKillers = true;
boolean hasAttackEffect = false;
boolean hasCombatEffect = false;
boolean dangerousBlockersPresent = false;
boolean canTrampleOverDefenders = false;
int numberOfPossibleBlockers = 0;
int defPower = 0;
SpellAbilityFactors(Card c) {
attacker = c;
}
private boolean canBeBlocked() {
return numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent));
}
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,...)
hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"))
|| attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT);
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
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
dangerousBlockersPresent = validBlockers.anyMatch(
CardPredicates.hasKeyword(Keyword.LIFELINK)
.or(Card::isWitherDamage)
);
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
defPower = CardLists.getTotalPower(validBlockers, null);
// look at the attacker in relation to the blockers to establish a
// number of factors about the attacking context that will be relevant
// to the attackers decision according to the selected strategy
for (final Card blocker : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
if (isWorthLessThanAllKillers && !attacker.hasSVar("SacMe")
&& ComputerUtilCard.evaluateCreature(blocker) <= ComputerUtilCard.evaluateCreature(attacker)) {
isWorthLessThanAllKillers = false;
}
}
// see if this attacking creature can destroy this defender, if
// not record that it can't kill everything
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
canKillAll = false;
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")
|| blocker.isWitherDamage() || blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
}
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
boolean avoidAttackingIntoBlock = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
boolean attackerWillDie = defPower >= attacker.getNetToughness();
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
boolean noContributionToAttack = attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
// We are attacking too recklessly if we can't kill a single blocker and:
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
canKillAllDangerous = false;
}
}
}
}
}
// 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)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
isWorthLessThanAllKillers = false;
hasCombatEffect = false;
} else if ((canKillAllDangerous || !canBeKilled) && ComputerUtilCard.canBeBlockedProfitably(defendingOpponent, attacker, true)) {
canKillAllDangerous = false;
canBeKilled = true;
}
}
}
/** /**
* <p> * <p>
* shouldAttack. * shouldAttack.
@@ -1478,6 +1363,14 @@ public class AiAttackController {
* @return a boolean. * @return a boolean.
*/ */
public final boolean shouldAttack(final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) { 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 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 canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
boolean isWorthLessThanAllKillers = true;
boolean canBeBlocked = false;
int numberOfPossibleBlockers = 0;
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking? // 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)) { 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 // For each level of priority, enemy has to have life as much as the creature's power
@@ -1488,7 +1381,7 @@ public class AiAttackController {
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine! // 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()) { for (SpellAbility sa : attacker.getSpellAbilities()) {
// Do not attack if we can afford using the ability. // Do not attack if we can afford using the ability.
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) { if (sa.isActivatedAbility()) {
if (ComputerUtilCost.canPayCost(sa, ai, false)) { if (ComputerUtilCost.canPayCost(sa, ai, false)) {
return false; return false;
} }
@@ -1502,72 +1395,161 @@ public class AiAttackController {
if (!isEffectiveAttacker(ai, attacker, combat, defender)) { if (!isEffectiveAttacker(ai, attacker, combat, defender)) {
return false; return false;
} }
boolean 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,...)
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"));
SpellAbilityFactors saf = new SpellAbilityFactors(attacker); if (!hasCombatEffect) {
if (aiAggression != 5) { if (attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT)) {
saf.calculate(defenders, combat); hasCombatEffect = true;
}
}
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, new Predicate<Card>() {
@Override
public boolean apply(Card defender) {
return CombatUtil.canBlock(attacker, defender);
}
});
boolean canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, CardPredicates.Accessors.fnGetNetToughness);
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
boolean dangerousBlockersPresent = Iterables.any(validBlockers, Predicates.or(
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
CardPredicates.hasKeyword(Keyword.LIFELINK)));
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
int defPower = CardLists.getTotalPower(validBlockers, true, false);
// look at the attacker in relation to the blockers to establish a
// number of factors about the attacking context that will be relevant
// to the attackers decision according to the selected strategy
for (final Card blocker : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
if (isWorthLessThanAllKillers && !attacker.hasSVar("SacMe")
&& ComputerUtilCard.evaluateCreature(blocker) <= ComputerUtilCard.evaluateCreature(attacker)) {
isWorthLessThanAllKillers = false;
}
}
// see if this attacking creature can destroy this defender, if
// not record that it can't kill everything
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
canKillAll = false;
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")) {
canKillAllDangerous = false;
} else {
if (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)
|| blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
}
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
boolean avoidAttackingIntoBlock = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
boolean attackerWillDie = defPower >= attacker.getNetToughness();
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
boolean noContributionToAttack = this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
// We are attacking too recklessly if we can't kill a single blocker and:
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
canKillAllDangerous = false;
}
}
}
}
}
}
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
isWorthLessThanAllKillers = false;
hasCombatEffect = false;
} else if ((canKillAllDangerous || !canBeKilled) && ComputerUtilCard.canBeBlockedProfitably(defendingOpponent, attacker, true)) {
canKillAllDangerous = false;
canBeKilled = true;
} }
// 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)
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
return true;
}
break;
case 5: // all out attacking
if (LOG_AI_ATTACKS) if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = all out attacking"); System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
return true; return true;
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() break;
|| saf.defPower == 0) { case 5: // all out attacking
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() + " = all out attacking");
return true; return true;
} case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
break; if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
case 3: // expecting to at least kill a creature of equal value or not be blocked || (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
if ((saf.canKillAll && saf.isWorthLessThanAllKillers) if (LOG_AI_ATTACKS)
|| (((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne) System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
|| !saf.canBeBlocked()) { return true;
if (LOG_AI_ATTACKS) }
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable"); break;
return true; case 3: // expecting to at least kill a creature of equal value or not be blocked
} if ((canKillAll && isWorthLessThanAllKillers)
break; || (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving || !canBeBlocked) {
if (!saf.canBeBlocked() || ((saf.canKillAll || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne && if (LOG_AI_ATTACKS)
((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || !saf.canBeKilled))) { System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
if (LOG_AI_ATTACKS) return true;
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block"); }
return true; break;
} case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
break; if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
case 1: // unblockable creatures only ((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
if (!saf.canBeBlocked() || (saf.numberOfPossibleBlockers == 1 && saf.canKillAll && !saf.canBeKilledByOne)) { 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 not to be blocked"); return true;
return true; }
} break;
break; case 1: // unblockable creatures only
default: if (!canBeBlocked || (numberOfPossibleBlockers == 1 && canKillAll && !canBeKilledByOne)) {
break; if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
return true;
}
break;
default:
break;
} }
return false; // don't attack return false; // don't attack
} }
@@ -1590,7 +1572,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 +1594,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;
@@ -1684,31 +1666,31 @@ public class AiAttackController {
} }
if (color != null) { if (color != null) {
switch (color) { switch (color) {
case "black": case "black":
if (!c.isBlack()) { if (!c.isBlack()) {
color = null; color = null;
} }
break; break;
case "blue": case "blue":
if (!c.isBlue()) { if (!c.isBlue()) {
color = null; color = null;
} }
break; break;
case "green": case "green":
if (!c.isGreen()) { if (!c.isGreen()) {
color = null; color = null;
} }
break; break;
case "red": case "red":
if (!c.isRed()) { if (!c.isRed()) {
color = null; color = null;
} }
break; break;
case "white": case "white":
if (!c.isWhite()) { if (!c.isWhite()) {
color = null; color = null;
} }
break; break;
} }
} }
if (color == null && artifact == null) { //nothing can make the attacker unblockable if (color == null && artifact == null) { //nothing can make the attacker unblockable
@@ -1724,7 +1706,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 +1736,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) {

View File

@@ -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;
@@ -138,20 +140,23 @@ public class AiBlockController {
ComputerUtilCard.sortByEvaluateCreature(attackers); ComputerUtilCard.sortByEvaluateCreature(attackers);
CardLists.sortByPowerDesc(attackers); CardLists.sortByPowerDesc(attackers);
//move cards like Phage the Untouchable to the front //move cards like Phage the Untouchable to the front
attackers.sort((o1, o2) -> { attackers.sort(new Comparator<Card>() {
if (o1.hasSVar("MustBeBlocked") && !o2.hasSVar("MustBeBlocked")) { @Override
return -1; public int compare(final Card o1, final Card o2) {
if (o1.hasSVar("MustBeBlocked") && !o2.hasSVar("MustBeBlocked")) {
return -1;
}
if (!o1.hasSVar("MustBeBlocked") && o2.hasSVar("MustBeBlocked")) {
return 1;
}
if (attackingCmd.contains(o1) && !attackingCmd.contains(o2)) {
return -1;
}
if (!attackingCmd.contains(o1) && attackingCmd.contains(o2)) {
return 1;
}
return 0;
} }
if (!o1.hasSVar("MustBeBlocked") && o2.hasSVar("MustBeBlocked")) {
return 1;
}
if (attackingCmd.contains(o1) && !attackingCmd.contains(o2)) {
return -1;
}
if (!attackingCmd.contains(o1) && attackingCmd.contains(o2)) {
return 1;
}
return 0;
}); });
return attackers; return attackers;
} }
@@ -161,12 +166,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,35 +330,43 @@ 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), new Predicate<Card>() {
// select creature that has a max blocker
return StaticAbilityCantAttackBlock.getMinMaxBlocker(input, combat.getDefenderPlayerByAttacker(input)).getRight() < Integer.MAX_VALUE; @Override
public boolean apply(Card input) {
// select creature that has a max blocker
return StaticAbilityCantAttackBlock.getMinMaxBlocker(input, combat.getDefenderPlayerByAttacker(input)).getRight() < Integer.MAX_VALUE;
}
}); });
} }
private Predicate<Card> changesPTWhenBlocked(final boolean onlyForDefVsTrample) { private Predicate<Card> changesPTWhenBlocked(final boolean onlyForDefVsTrample) {
return card -> { return new Predicate<Card>() {
for (final Trigger tr : card.getTriggers()) { @Override
if (tr.getMode() == TriggerType.AttackerBlocked) { public boolean apply(Card card) {
SpellAbility ab = tr.getOverridingAbility(); for (final Trigger tr : card.getTriggers()) {
if (ab != null) { if (tr.getMode() == TriggerType.AttackerBlocked) {
if (ab.getApi() == ApiType.Pump && "Self".equals(ab.getParam("Defined"))) { SpellAbility ab = tr.getOverridingAbility();
String rawP = ab.getParam("NumAtt"); if (ab != null) {
String rawT = ab.getParam("NumDef"); if (ab.getApi() == ApiType.Pump && "Self".equals(ab.getParam("Defined"))) {
if ("+X".equals(rawP) && "+X".equals(rawT) && card.getSVar("X").startsWith("Count$Valid Creature.blockingTriggeredAttacker")) { String rawP = ab.getParam("NumAtt");
return true; String rawT = ab.getParam("NumDef");
if ("+X".equals(rawP) && "+X".equals(rawT) && card.getSVar("X").startsWith("Count$Valid Creature.blockingTriggeredAttacker")) {
return true;
}
// TODO: maybe also predict calculated bonus above certain threshold?
} else if (ab.getApi() == ApiType.PumpAll && ab.hasParam("ValidCards")
&& ab.getParam("ValidCards").startsWith("Creature.blockingSource")) {
int pBonus = AbilityUtils.calculateAmount(card, ab.getParam("NumAtt"), ab);
int tBonus = AbilityUtils.calculateAmount(card, ab.getParam("NumDef"), ab);
return (!onlyForDefVsTrample && pBonus < 0) || tBonus < 0;
} }
// TODO: maybe also predict calculated bonus above certain threshold?
} else if (ab.getApi() == ApiType.PumpAll && ab.hasParam("ValidCards")
&& ab.getParam("ValidCards").startsWith("Creature.blockingSource")) {
int pBonus = AbilityUtils.calculateAmount(card, ab.getParam("NumAtt"), ab);
int tBonus = AbilityUtils.calculateAmount(card, ab.getParam("NumDef"), ab);
return (!onlyForDefVsTrample && pBonus < 0) || tBonus < 0;
} }
} }
} }
return false;
} }
return false;
}; };
} }
@@ -366,7 +379,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
@@ -439,12 +452,15 @@ public class AiBlockController {
// Try to add blockers that could be destroyed, but are worth less than the attacker // Try to add blockers that could be destroyed, but are worth less than the attacker
// Don't use blockers without First Strike or Double Strike if attacker has it // Don't use blockers without First Strike or Double Strike if attacker has it
usableBlockers = CardLists.filter(blockers, c -> { usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
if (ComputerUtilCombat.dealsFirstStrikeDamage(attacker, false, combat) @Override
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) { public boolean apply(final Card c) {
return false; if (ComputerUtilCombat.dealsFirstStrikeDamage(attacker, false, combat)
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
return false;
}
return lifeInDanger || wouldLikeToRandomlyTrade(attacker, c, combat) || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
} }
return lifeInDanger || wouldLikeToRandomlyTrade(attacker, c, combat) || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
}); });
if (usableBlockers.size() < 2) { if (usableBlockers.size() < 2) {
return; return;
@@ -563,8 +579,13 @@ public class AiBlockController {
final List<Card> blockGang = new ArrayList<>(); final List<Card> blockGang = new ArrayList<>();
int absorbedDamage; // The amount of damage needed to kill the first blocker int absorbedDamage; // The amount of damage needed to kill the first blocker
List<Card> usableBlockers = CardLists.filter(blockers, c -> c.getNetToughness() > attacker.getNetCombatDamage() // performance shortcut List<Card> usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
|| c.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, c, true) > attacker.getNetCombatDamage()); @Override
public boolean apply(final Card c) {
return c.getNetToughness() > attacker.getNetCombatDamage() // performance shortcut
|| c.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, c, true) > attacker.getNetCombatDamage();
}
});
if (usableBlockers.size() < 2) { if (usableBlockers.size() < 2) {
return; return;
} }
@@ -738,11 +759,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 +772,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 +785,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,18 +818,23 @@ 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);
blockers.removeAll(combat.getBlockers(attacker)); blockers.removeAll(combat.getBlockers(attacker));
// Don't add any blockers that won't kill the attacker because the damage would be prevented by a static effect // Don't add any blockers that won't kill the attacker because the damage would be prevented by a static effect
blockers = CardLists.filter(blockers, blocker -> !ComputerUtilCombat.isCombatDamagePrevented(blocker, attacker, blocker.getNetCombatDamage())); blockers = CardLists.filter(blockers, new Predicate<Card>() {
@Override
public boolean apply(Card blocker) {
return !ComputerUtilCombat.isCombatDamagePrevented(blocker, attacker, blocker.getNetCombatDamage());
}
});
// Try to use safe blockers first // Try to use safe blockers first
if (blockers.size() > 0) { if (blockers.size() > 0) {
@@ -872,9 +902,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)) {
@@ -891,9 +921,13 @@ public class AiBlockController {
CardCollection pwsWithChumpBlocks = new CardCollection(); CardCollection pwsWithChumpBlocks = new CardCollection();
CardCollection chosenChumpBlockers = new CardCollection(); CardCollection chosenChumpBlockers = new CardCollection();
CardCollection chumpPWDefenders = CardLists.filter(this.blockersLeft, CardCollection chumpPWDefenders = CardLists.filter(this.blockersLeft, new Predicate<Card>() {
card -> ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken : evalThresholdNonToken) @Override
); public boolean apply(Card card) {
return ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken
: evalThresholdNonToken);
}
});
CardLists.sortByPowerAsc(chumpPWDefenders); CardLists.sortByPowerAsc(chumpPWDefenders);
if (!chumpPWDefenders.isEmpty()) { if (!chumpPWDefenders.isEmpty()) {
for (final Card attacker : attackers) { for (final Card attacker : attackers) {
@@ -906,12 +940,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 +1377,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.

View File

@@ -18,17 +18,13 @@
package forge.ai; package forge.ai;
import java.util.Map; import java.util.HashSet;
import java.util.Iterator;
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 +55,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 +63,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 +146,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 +161,20 @@ 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) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
Card c = it.next();
if (c.getName().equals(cardName)) {
return true;
}
}
}
return false;
} }
/** /**
@@ -113,7 +188,20 @@ 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) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
Card c = it.next();
if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
return true;
}
}
}
return false;
} }
/** /**
@@ -126,7 +214,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 +238,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 +256,19 @@ 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 (c.getName().equals(cardName)) {
return forgetCard(c, set); if (memorySet != null) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
Card c = it.next();
if (c.getName().equals(cardName)) {
return forgetCard(c, set);
}
} }
} }
return false; return false;
} }
@@ -171,11 +281,19 @@ 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 (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
return forgetCard(c, set); if (memorySet != null) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
Card c = it.next();
if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
return forgetCard(c, set);
}
} }
} }
return false; return false;
} }
@@ -269,4 +387,4 @@ public class AiCardMemory {
public static boolean isMemorySetEmpty(AiController aic, MemorySet set) { public static boolean isMemorySetEmpty(AiController aic, MemorySet set) {
return aic.getCardMemory().isMemorySetEmpty(set); return aic.getCardMemory().isMemorySetEmpty(set);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,12 @@
package forge.ai; package forge.ai;
import com.google.common.base.Predicate;
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;
import forge.game.GameEntityCounterTable; import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils;
import forge.game.card.*; import forge.game.card.*;
import forge.game.cost.*; import forge.game.cost.*;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
@@ -29,17 +28,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
private final CardCollection tapped; private final CardCollection tapped;
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) { public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
this(ai0, sa, effect, false);
}
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) {
super(ai0, effect, sa, sa.getHostCard()); super(ai0, effect, sa, sa.getHostCard());
discarded = new CardCollection(); discarded = new CardCollection();
tapped = new CardCollection(); tapped = new CardCollection();
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
if (!payMana && tappedForMana != null) {
tapped.addAll(tappedForMana);
}
} }
@Override @Override
@@ -49,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);
@@ -67,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);
} }
@@ -113,20 +98,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability); randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
} }
return PaymentDecision.card(randomSubset); return PaymentDecision.card(randomSubset);
} else if (type.contains("+WithDifferentNames")) { } else if (type.equals("DifferentNames")) {
CardCollection differentNames = new CardCollection(); CardCollection differentNames = new CardCollection();
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe")); CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
while (c > 0) { while (c > 0) {
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);
@@ -162,19 +147,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return decision; return decision;
} }
@Override
public PaymentDecision visit(CostPromiseGift cost) {
if (!cost.canPay(ability, player, isEffect())) {
return null;
}
List<Player> res = cost.getPotentialPlayers(player, ability);
// I should only choose one of these right?
// TODO Choose the "worst" player.
Collections.shuffle(res);
return PaymentDecision.players(res.subList(0, 1));
}
@Override @Override
public PaymentDecision visit(CostExile cost) { public PaymentDecision visit(CostExile cost) {
String type = cost.getType(); String type = cost.getType();
@@ -184,27 +156,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (type.equals("All")) { if (type.equals("All")) {
return PaymentDecision.card(player.getCardsIn(cost.getFrom())); return PaymentDecision.card(player.getCardsIn(cost.getFrom()));
} else if (type.contains("FromTopGrave")) { }
return null; else if (type.contains("FromTopGrave")) {
} else if (type.contains("+withTotalCMCGE")) {
String strAmount = type.split("withTotalCMCGE")[1];
int amount = AbilityUtils.calculateAmount(source, strAmount, ability);
String typeCleaned = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalCMCGE", strAmount), "");
CardCollection valid = CardLists.getValidCards(player.getGame().getCardsIn(cost.getFrom().get(0)), typeCleaned, player, source, ability);
CardCollection chosen = new CardCollection();
CardLists.sortByCmcDesc(valid);
Collections.reverse(valid);
int totalCMC = 0;
for (Card card : valid) {
totalCMC += card.getCMC();
chosen.add(card);
if (totalCMC >= amount) {
return PaymentDecision.card(chosen);
}
}
return null; return null;
} }
@@ -291,20 +244,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c); return PaymentDecision.number(c);
} }
@Override
public PaymentDecision visit(final CostForage cost) {
CardCollection food = CardLists.filter(player.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Food"), CardPredicates.canBeSacrificedBy(ability, isEffect()));
CardCollection exile = CardLists.filter(player.getCardsIn(ZoneType.Graveyard), CardPredicates.canExiledBy(ability, isEffect()));
if (!food.isEmpty()) {
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollectionView list = aic.chooseSacrificeType("Food", ability, isEffect(), 1, null);
return list == null ? null : PaymentDecision.card(list);
} else {
CardCollectionView chosen = ComputerUtil.chooseExileFromList(player, exile, source, 3, ability, isEffect());
return null == chosen ? null : PaymentDecision.card(chosen);
}
}
@Override @Override
public PaymentDecision visit(CostRollDice cost) { public PaymentDecision visit(CostRollDice cost) {
int c = cost.getAbilityAmount(ability); int c = cost.getAbilityAmount(ability);
@@ -455,6 +394,24 @@ 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, new Predicate<Card>() {
@Override
public boolean apply(Card 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) {
@@ -566,7 +523,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove); int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) { if (thisRemove > 0) {
removed += thisRemove; removed += thisRemove;
table.put(null, prefCard, cType, thisRemove); table.put(null, prefCard, CounterType.get(cType), thisRemove);
} }
} }
} }
@@ -576,7 +533,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostRemoveAnyCounter cost) { public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability); final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source); final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
if (c <= 0) { if (c <= 0) {
return null; return null;
@@ -638,20 +595,23 @@ public class AiCostDecision extends CostDecisionMakerBase {
// filter for negative counters // filter for negative counters
if (c > toRemove && cost.counter == null) { if (c > toRemove && cost.counter == null) {
List<Card> negatives = CardLists.filter(typeList, crd -> { List<Card> negatives = CardLists.filter(typeList, new Predicate<Card>() {
for (CounterType cType : table.filterToRemove(crd).keySet()) { @Override
if (ComputerUtil.isNegativeCounter(cType, crd)) { public boolean apply(final Card crd) {
return true; for (CounterType cType : table.filterToRemove(crd).keySet()) {
if (ComputerUtil.isNegativeCounter(cType, crd)) {
return true;
}
} }
return false;
} }
return false;
}); });
if (!negatives.isEmpty()) { if (!negatives.isEmpty()) {
// 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;
@@ -666,13 +626,16 @@ public class AiCostDecision extends CostDecisionMakerBase {
// filter for useless counters // filter for useless counters
// they have no effect on the card, if they are there or removed // they have no effect on the card, if they are there or removed
if (c > toRemove && cost.counter == null) { if (c > toRemove && cost.counter == null) {
List<Card> useless = CardLists.filter(typeList, crd -> { List<Card> useless = CardLists.filter(typeList, new Predicate<Card>() {
for (CounterType ctype : table.filterToRemove(crd).keySet()) { @Override
if (ComputerUtil.isUselessCounter(ctype, crd)) { public boolean apply(final Card crd) {
return true; for (CounterType ctype : table.filterToRemove(crd).keySet()) {
if (ComputerUtil.isUselessCounter(ctype, crd)) {
return true;
}
} }
return false;
} }
return false;
}); });
if (!useless.isEmpty()) { if (!useless.isEmpty()) {
@@ -700,14 +663,17 @@ public class AiCostDecision extends CostDecisionMakerBase {
// try to remove Quest counter on something with enough counters for the // try to remove Quest counter on something with enough counters for the
// effect to continue // effect to continue
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.QUEST))) { if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.QUEST))) {
List<Card> prefs = CardLists.filter(typeList, crd -> { List<Card> prefs = CardLists.filter(typeList, new Predicate<Card>() {
// a Card without MaxQuestEffect doesn't need any Quest @Override
// counters public boolean apply(final Card crd) {
int e = 0; // a Card without MaxQuestEffect doesn't need any Quest
if (crd.hasSVar("MaxQuestEffect")) { // counters
e = Integer.parseInt(crd.getSVar("MaxQuestEffect")); int e = 0;
if (crd.hasSVar("MaxQuestEffect")) {
e = Integer.parseInt(crd.getSVar("MaxQuestEffect"));
}
return crd.getCounters(CounterEnumType.QUEST) > e;
} }
return crd.getCounters(CounterEnumType.QUEST) > e;
}); });
prefs.sort(Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST))); prefs.sort(Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST)));
@@ -719,7 +685,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove); int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) { if (over > 0) {
toRemove += over; toRemove += over;
table.put(null, crd, CounterEnumType.QUEST, over); table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
} }
} }
} }
@@ -762,7 +728,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);
} }
@@ -770,12 +736,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;
@@ -804,8 +764,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;
@@ -816,8 +775,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
@@ -846,12 +804,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

View File

@@ -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;
};
}
} }

View File

@@ -11,24 +11,28 @@ import forge.game.zone.ZoneType;
public final class AiPlayerPredicates { public final class AiPlayerPredicates {
public static Comparator<Player> compareByZoneValue(final String type, final ZoneType zone, final SpellAbility sa) { public static final Comparator<Player> compareByZoneValue(final String type, final ZoneType zone,
return (arg0, arg1) -> { final SpellAbility sa) {
CardCollectionView list0 = AbilityUtils.filterListByType(arg0.getCardsIn(zone), type, sa); return new Comparator<Player>() {
CardCollectionView list1 = AbilityUtils.filterListByType(arg1.getCardsIn(zone), type, sa); @Override
public int compare(Player arg0, Player arg1) {
CardCollectionView list0 = AbilityUtils.filterListByType(arg0.getCardsIn(zone), type, sa);
CardCollectionView list1 = AbilityUtils.filterListByType(arg1.getCardsIn(zone), type, sa);
int v0, v1; int v0, v1;
if ((CardLists.getNotType(list0, "Creature").isEmpty()) if ((CardLists.getNotType(list0, "Creature").isEmpty())
&& (CardLists.getNotType(list1, "Creature").isEmpty())) { && (CardLists.getNotType(list1, "Creature").isEmpty())) {
v0 = ComputerUtilCard.evaluateCreatureList(list0); v0 = ComputerUtilCard.evaluateCreatureList(list0);
v1 = ComputerUtilCard.evaluateCreatureList(list1); v1 = ComputerUtilCard.evaluateCreatureList(list1);
} // otherwise evaluate both lists by CMC and pass only if human } // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable // permanents are less valuable
else { else {
v0 = ComputerUtilCard.evaluatePermanentList(list0); v0 = ComputerUtilCard.evaluatePermanentList(list0);
v1 = ComputerUtilCard.evaluatePermanentList(list1); v1 = ComputerUtilCard.evaluatePermanentList(list1);
}
return Integer.compare(v0, v1);
} }
return Integer.compare(v0, v1);
}; };
} }
} }

View File

@@ -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.
@@ -92,7 +93,7 @@ public class AiProfileUtil {
* Load a single profile. * Load a single profile.
* @param profileName a profile to load. * @param profileName a profile to load.
*/ */
private static Map<AiProps, String> loadProfile(final String profileName) { private static final Map<AiProps, String> loadProfile(final String profileName) {
Map<AiProps, String> profileMap = new HashMap<>(); Map<AiProps, String> profileMap = new HashMap<>();
List<String> lines = FileUtil.readFile(buildFileName(profileName)); List<String> lines = FileUtil.readFile(buildFileName(profileName));
@@ -146,9 +147,9 @@ public class AiProfileUtil {
if (children == null) { if (children == null) {
System.err.println("AIProfile > can't find AI profile directory!"); System.err.println("AIProfile > can't find AI profile directory!");
} else { } else {
for (String child : children) { for (int i = 0; i < children.length; i++) {
if (child.endsWith(AI_PROFILE_EXT)) { if (children[i].endsWith(AI_PROFILE_EXT)) {
availableProfiles.add(child.substring(0, child.length() - AI_PROFILE_EXT.length())); availableProfiles.add(children[i].substring(0, children[i].length() - AI_PROFILE_EXT.length()));
} }
} }
} }

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