Compare commits

..

17 Commits

Author SHA1 Message Date
Hans Mackowiak
25b6db1536 fix unused variables 2021-03-25 09:32:20 +01:00
Hans Mackowiak
245586cefa GameAction: removeExiledWith when zone is not exile 2021-03-25 09:32:20 +01:00
Hans Mackowiak
21999488f4 fix usage of CardLists getValidCards using CardTraitBase if able 2021-03-25 09:32:20 +01:00
Hans Mackowiak
d8b9524f80 Card: ChosenType linked to CTB 2021-03-25 09:32:20 +01:00
Hans Mackowiak
3bda4ffe0a AI: fix moved Devotion xcount 2021-03-25 09:32:20 +01:00
Hans Mackowiak
4dba174a6c Card: chosenColors use Table for LinkedAbilities 2021-03-25 09:32:20 +01:00
Hans Mackowiak
f57f6e1c40 fix skyclave apparition and spell queller exiling multiple cards 2021-03-25 09:32:20 +01:00
Hans Mackowiak
c9c974ad00 ExiledWithTable: add way to exile without CardTraitBase for GameState 2021-03-25 09:32:20 +01:00
Hans Mackowiak
18be2c67b6 CardTraitBase: add matchesValidParam and use for Trigger 2021-03-25 09:32:20 +01:00
Michael Kamensky
c27fa18ed7 - Unbreak some AI logic parts 2021-03-25 09:32:20 +01:00
Michael Kamensky
fb7a212597 - Port over the AI logic for Ashiok 2021-03-25 09:32:20 +01:00
Hans Mackowiak
c05f2396a1 Hideaway: fix keyword using ExiledWith 2021-03-25 09:32:20 +01:00
Hans Mackowiak
0ac7fb59e6 CardTraitBase: matchesValid as instance method, better use for isValid 2021-03-25 09:32:20 +01:00
Hans Mackowiak
1db6a772ed cards: update more cards with Remember and ExiledWith 2021-03-25 09:32:20 +01:00
Michael Kamensky
d12527ff21 - Update PC_041415 to work with the changed card script. 2021-03-25 09:32:20 +01:00
Hans Mackowiak
ab5eb24d7a Fix ixalans_binding using Static+ExiledWith 2021-03-25 09:32:20 +01:00
Hans Mackowiak
9f5fd3f35f ExiledWithTable to reduce the need for Remember 2021-03-25 09:32:20 +01:00
43168 changed files with 462148 additions and 1317961 deletions

View File

@@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
type: 'Bug'
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,21 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
type: 'Feature'
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,176 +0,0 @@
name: Publish Desktop Forge
on:
workflow_dispatch:
inputs:
debug_enabled:
type: boolean
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
release_android:
type: boolean
description: 'Also try to release android build'
required: false
default: false
jobs:
build:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest
permissions:
contents: write
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: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
- name: Setup android requirements
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
run: |
JAVA_HOME=${JAVA_HOME_17_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;35.0.0" "platform-tools" "platforms;android-35"
cd forge-gui-android
echo "${{ secrets.FORGE_KEYSTORE }}" > forge.keystore.asc
gpg -d --passphrase "${{ secrets.FORGE_KEYSTORE_PASSPHRASE }}" --batch forge.keystore.asc > forge.keystore
cd -
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
curl -L -o android-maven-plugin-4.6.2.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.jar
curl -L -o android-maven-plugin-4.6.2.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.pom
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
- name: Build/Install/Publish Desktop to GitHub Packages Apache Maven
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_android }}
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build only desktop and only try to move desktop files
mvn -U -B clean -P windows-linux install -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:
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

@@ -1,89 +0,0 @@
name: Publish Android Release
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
jobs:
build:
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 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: Install android SDK
uses: maxim-lobanov/setup-android-tools@v1
with:
packages: |
platforms;android-35
build-tools;35.0.0
- 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; }
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
- 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: Configure Git User
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
- 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 _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
env:
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

@@ -1,114 +0,0 @@
name: Create Android Snapshot
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
#upload_package:
# type: boolean
# description: 'Upload the completed Android package'
# required: false
# default: true
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 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 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; }
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
- 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: Configure Git User
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
- 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 _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
mkdir upload
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk upload/
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip upload/
mv /home/runner/work/forge/forge/forge-gui-android/target/classes/assets/version.txt upload/
cd upload
ls
env:
GITHUB_TOKEN: ${{ github.token }}
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
#if: ${{ inputs.upload_package }}
with:
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: upload/
server-dir: downloads/dailysnapshots/
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

@@ -1,95 +0,0 @@
name: Create Desktop Snapshot
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
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: Build/Install Snapshot
run: |
export DISPLAY=":1"
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 }}
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
- name: Rename before upload
run: |
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 to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/build.txt izpack/
cd izpack
d=$(date +%m.%d)
# 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
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with:
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: izpack/
server-dir: downloads/dailysnapshots/
exclude: |
*.pom
*.repositories
*.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,34 +0,0 @@
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '21 9 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has not been updated in a while and has now been marked as stale. Stale messages will be auto closed."
stale-pr-message: 'This PR has not been updated in a while nad has been marked on stale. Stale PRs will be auto closed'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
exempt-issue-labels: 'keep'
exempt-pr-labels: 'awaiting-approval,work-in-progress,keep'
days-before-issue-stale: 30
days-before-pr-stale: 45
days-before-issue-close: 5
days-before-pr-close: 10

View File

@@ -1,28 +0,0 @@
name: Publish wiki
on:
push:
branches: [master]
paths:
- docs/**
- .github/workflows/sync-wiki.yml
workflow_dispatch:
concurrency:
group: publish-wiki
cancel-in-progress: true
permissions:
contents: write
jobs:
publish-wiki:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: fix md links for Gollum
run: find ${{ github.workspace }}/docs/ -type f -name "*.md" -exec sed -i -E 's|(\[[^]]+]\()([^)]+\/)*([^).]+).md\)|\1\3)|g' '{}' \;
- name: fix image links for Gollum
run: find ${{ github.workspace }}/docs/ -type f -name "*.png" -exec mv '{}' ${{ github.workspace }}/docs/ \;
- uses: Andrew-Chen-Wang/github-wiki-action@v5
with:
path: docs
preprocess: false
strategy: init

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

@@ -1,29 +0,0 @@
name: Test build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: ['17', '21']
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
cache: 'maven'
- 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 tests in virtual framebuffer
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
mvn -U -B clean test

View File

@@ -1,48 +0,0 @@
name: Maven test for settings
on:
workflow_dispatch:
jobs:
build:
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: Check settings after java setup
run: |
cat ${{ github.workspace }}/settings.xml
- name: maven-settings-xml-action
uses: whelk-io/maven-settings-xml-action@v20
with:
servers: '[{ "id": "cardforge-repo", "username": "${{ secrets.FTP_USERNAME }}", "password": "Fake_password" }]'
- name: Check settings after xml action
run: |
cat ~/.m2/settings.xml
- name: Check settings after xml action
run: |
cat ${{ github.workspace }}/.mvn/local-settings.xml
- name: Where are maven settings
run: |
mvn -X clean -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} | grep "settings"

19
.gitignore vendored
View File

@@ -12,21 +12,17 @@
.settings
.classpath
.project
.checkstyle
# Ignore VS Code config files
.vscode/settings.json
.vscode/launch.json
.factorypath
# Ignore NetBeans config files
nbactions.xml
# Ignore flattened pom
.flattened-pom.xml
# Ignore binaries, temp files and test output, everywhere
@@ -36,11 +32,6 @@ bin
gen
*.log
# Ignore macOS Spotlight rubbish
.DS_Store
# ignore vim swapfiles
*.swp
# TODO: specify what these ignores are for (releasing?)
@@ -66,9 +57,6 @@ forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat
# Generated changelog file
forge-gui/release-files/CHANGES.txt
forge-gui/res/PerSetTrackingResults
forge-gui/res/decks
forge-gui/res/layouts
@@ -87,10 +75,3 @@ forge-gui/tools/AllCards.json
forge-gui/tools/EditionTrackingResults
forge-gui/tools/PerSetTrackingResults
*.tiled-session
/forge-gui/res/adventure/*.tiled-project
/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,11 +0,0 @@
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
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">
<servers>
<server>
<id>cardforge-repo</id>
<username>${cardforge-repo.username}</username>
<password>${cardforge-repo.password}</password>
</server>
</servers>
</settings>

View File

@@ -1,2 +0,0 @@
--settings
./.mvn/local-settings.xml

View File

@@ -1,117 +0,0 @@
# Contributing to Forge
[Official repo](https://github.com/Card-Forge/forge.git).
## 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.
Google no longer supports Android SDK releases for Eclipse.
## Windows
TBD
## Linux / Mac OSX
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).
## 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
Art files need to be copyright-free and they should be in the public domain.
### 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.
#### forge-installer

263
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)!
Gitlab repo is found [here](https://git.cardforge.org/core-developers/forge).
[![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)
Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated)
---
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
## ✨ Introduction
# Requirements / Tools
**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.
- Java IDE such as IntelliJ or Eclipse
- Java JDK 8 or later
- Git
- Git client (optional)
- Maven
- Gitlab account
- Libgdx (optional: familiarity with this library is helpful for mobile platform development)
- Android SDK (optional: for Android releases)
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
**Note:** Forge operates independently and is not affiliated with Wizards of the Coast.
# Project Quick Setup
---
- Log in to gitlab with your user account and fork the project.
## 🌟 Key Features
- Clone your forked project to your local machine
- **🌐 Cross-Platform Support:** Play on **Windows, Mac, Linux,** and **Android**.
- **🔧 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!
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
---
# Eclipse
## 🛠️ Installation Guide
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
### 📥 Desktop Installation
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://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.
## Project Setup
### 📱 Android Installation
- _(Note: **Android 11** is the minimum requirement with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
- 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.
- Follow the instructions for cloning from Gitlab. You'll need a Gitlab account setup and an SSH key defined.
---
If you are on a Windows machine you can use Putty with TortoiseGit 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 Gitlab profile under
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing Gitlab.
- Fork the Forge git repo to your Gitlab account.
## 🎮 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
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.
- Install Eclipse 2018-12 or later for Java. Launch it.
<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.
- 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.
### 🔍 Quest Modes
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
## Project Launch
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" />
### Desktop
### 🤖 AI Formats
Test your skills against AI in multiple formats:
- **Sealed**
- **Draft**
- **Commander**
- **Cube**
This is the standard configuration used for releasing to Windows / Linux / MacOS.
For comprehensive gameplay instructions, visit our [User Guide](https://github.com/Card-Forge/forge/wiki/User-Guide).
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Ok
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
- The familiar Forge splash screen, etc. should appear. Enjoy!
---
### Mobile (Desktop dev)
## 💬 Support & Community
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
Need help? Join our vibrant Discord community!
- 📜 Read the **#rules** and explore the **FAQ**.
- ❓ Ask your questions in the **#help** channel for assistance.
- 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!
## 🤝 Contributing to Forge
## Eclipse / Android SDK Integration
We love community contributions! Interested in helping? Check out our [Contributing Guidelines](CONTRIBUTING.md) for details on how to get started.
Google no longer supports Android SDK releases for Eclipse. That said, it is still possible to build and debug Android platforms.
---
### Android SDK
## About Forge
Reference SO for obtaining a specific release: https://stackoverflow.com/questions/27043522/where-can-i-download-an-older-version-of-the-android-sdk
Forge aims to deliver an immersive and customizable Magic: The Gathering experience for fans around the world.
#### Windows
### 📊 Repository Statistics
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.
| Metric | Count |
|----------------|-------------------------------------------------------------|
| **⭐ 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) |
#### Linux / Mac OSX
---
TBD
**📄 License:** [GPL-3.0](LICENSE)
<div align="center" style="display: flex; align-items: center; justify-content: center;">
<div style="margin-left: auto;">
<a href="#top">
<img src="https://img.shields.io/badge/Back%20to%20Top-000000?style=for-the-badge&logo=github&logoColor=white" alt="Back to Top">
</a>
</div>
</div>
### Android Plugin for Eclipse
Google's last plugin release does not work completely with target's running Android 7.0 or later. Download the ADT-24.2.0-20160729.zip plugin
from: https://github.com/khaledev/ADT/releases
In Eclipse go to: Help > Install New Software... > Add > Name: ADT Update, Click on the "Archive:" button and navigate to the downloaded ADT-24.2.0-20160729.zip file > Add. Install all "Developer Tools". Eclipse
should restart and prompt you to run the SDK Manager. Launch it and continue to the next steps below.
### Android Platform
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 7.1.1 (API 25) SDK Platform
- Google USB Driver 11
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 from https://sourceforge.net/projects/proguard/files/proguard/6.0/.
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard4.7/.
- Extract Proguard 6.0.3 to the Android SDK install path under tools/. You will need to rename the dir proguard6.0.3/ to proguard/.
### 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://git.cardforge.org/core-developers/forge/wikis/intellij-setup).
# Card Scripting
Visit [this page](https://www.slightlymagic.net/wiki/Forge_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>

View File

@@ -1,14 +0,0 @@
# ideally this should be using HTTPS, but this is fine for now
dsn=http://a0b8dbad9b8a49cfa51bf65d462e8dae@sentry.cardforge.org:9000/2
stacktrace.app.packages=forge
# where to store events if offline or can't reach the above server
buffer.dir=sentry-events
buffer.size=100
# allow ample time for graceful shutdown
buffer.shutdowntimeout=5000
async.shutdowntimeout=5000
# allow longer messages
maxmessagelength=1500

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,3 +0,0 @@
#!/bin/sh
cd $(dirname "${0}")
java -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -1,21 +0,0 @@
package forge.adventure;
import forge.GuiMobile;
import forge.adventure.editor.EditorMainWindow;
import forge.adventure.util.Config;
import forge.gui.GuiBase;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* Main entry point
*/
public class Main {
public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
new EditorMainWindow(Config.instance());
}
}

View File

@@ -1,175 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class ActionEdit extends FormPanel {
DialogData.ActionData currentData;
JTextField issueQuest = new JTextField();
JTextField characterFlagName = new JTextField();
JTextField mapFlagName = new JTextField();
JTextField questFlagName = new JTextField();
JTextField advanceCharacterFlag = new JTextField();
JTextField advanceMapFlag = new JTextField();
JTextField advanceQuestFlag = new JTextField();
JTextField battleWithActorID = new JTextField();
JTextField activateObjectID = new JTextField();
JTextField deleteMapObject = new JTextField();
JTextField setColorIdentity = new JTextField();
JSpinner addReputation = new JSpinner(new SpinnerNumberModel(0, -1000, 1000, 1));
JSpinner addLife = new JSpinner(new SpinnerNumberModel(0, -1000, 1000, 1));
JTextField POIReference = new JTextField();
JTextField removeItem = new JTextField();
JSpinner characterFlagValue = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner mapFlagValue = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner questFlagValue = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
private boolean updating=false;
public ActionEdit()
{
//todo: add info pane to explain primary usage
add("Issue Quest:",issueQuest);
add("Set Map Flag Name:",mapFlagName);
add("Map Flag value to set:",mapFlagValue);
add("Set Quest Flag name:",questFlagName);
add("Quest Flag value to set:",questFlagValue);
add("Set Character Flag name:",characterFlagName);
add("Character Flag value to set:",characterFlagValue);
add("Advance Map Flag name:",advanceMapFlag);
add("Advance Quest Flag name:",advanceQuestFlag);
add("Advance Character Flag name:",advanceCharacterFlag);
add("Battle with actor ID:",battleWithActorID);
add("Delete map object:",deleteMapObject);
add("Set color identity:",setColorIdentity);
add("Add Reputation:",addReputation);
add("Add Life:",addLife);
add("POI Reference:",POIReference);
add("Remove Item:",removeItem);
issueQuest.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
mapFlagName.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
mapFlagValue.getModel().addChangeListener(e -> ActionEdit.this.updateAction());
questFlagName.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
questFlagValue.getModel().addChangeListener(e -> ActionEdit.this.updateAction());
advanceMapFlag.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
advanceQuestFlag.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
battleWithActorID.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
activateObjectID.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
deleteMapObject.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
setColorIdentity.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
addLife.getModel().addChangeListener(e -> ActionEdit.this.updateAction());
addReputation.getModel().addChangeListener(e -> ActionEdit.this.updateAction());
POIReference.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
removeItem.getDocument().addDocumentListener(new DocumentChangeListener(ActionEdit.this::updateAction));
}
private void updateAction() {
if(updating)
return;
if (currentData == null)
currentData = new DialogData.ActionData();
DialogData.ActionData.QuestFlag characterFlag = new DialogData.ActionData.QuestFlag();
characterFlag.key = characterFlagName.getText();
characterFlag.val = (int)characterFlagValue.getModel().getValue();
currentData.setCharacterFlag= characterFlag;
DialogData.ActionData.QuestFlag mapFlag = new DialogData.ActionData.QuestFlag();
mapFlag.key = mapFlagName.getText();
mapFlag.val = (int)mapFlagValue.getModel().getValue();
currentData.setMapFlag= mapFlag;
DialogData.ActionData.QuestFlag questFlag = new DialogData.ActionData.QuestFlag();
questFlag.key = questFlagName.getText();
questFlag.val = (int)questFlagValue.getModel().getValue();
currentData.setQuestFlag= questFlag;
currentData.issueQuest = issueQuest.getText();
currentData.advanceMapFlag= advanceMapFlag.getText();
currentData.advanceQuestFlag= advanceQuestFlag.getText();
currentData.advanceCharacterFlag= advanceCharacterFlag.getText();
currentData.battleWithActorID= Integer.parseInt(battleWithActorID.getText());
currentData.activateMapObject= Integer.parseInt((activateObjectID.getText()));
currentData.deleteMapObject= Integer.parseInt(deleteMapObject.getText());
currentData.setColorIdentity= setColorIdentity.getText();
currentData.addLife= (int) addLife.getModel().getValue();
currentData.addMapReputation= (int)addReputation.getModel().getValue();
currentData.POIReference= POIReference.getText();
currentData.removeItem= removeItem.getText();
//These need a dedicated effect editor
// JTextField setEffect = new JTextField();
// JTextField giveBlessing = new JTextField();
// currentData.giveBlessing= giveBlessing.getText();
// currentData.setEffect= setEffect.getText();
//These are valid pre-existing fields, but should be handled through the dedicated rewards editor
// currentData.addGold =;
// currentData.addItem=;
// currentData.grantRewards =;
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
public void setCurrentAction(DialogData.ActionData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
mapFlagName.setText(currentData.setMapFlag==null?"":currentData.setMapFlag.key);
mapFlagValue.getModel().setValue(currentData.setMapFlag==null?0:currentData.setMapFlag.val);
questFlagName.setText(currentData.setQuestFlag==null?"":currentData.setQuestFlag.key);
questFlagValue.getModel().setValue(currentData.setQuestFlag==null?0:currentData.setQuestFlag.val);
issueQuest.setText(currentData.issueQuest);
advanceMapFlag.setText(currentData.advanceMapFlag);
advanceQuestFlag.setText(currentData.advanceQuestFlag);
advanceCharacterFlag.setText(currentData.advanceCharacterFlag);
battleWithActorID.setText(String.valueOf(currentData.battleWithActorID));
activateObjectID.setText(String.valueOf(currentData.battleWithActorID));
deleteMapObject.setText(String.valueOf(currentData.deleteMapObject));
setColorIdentity.setText(currentData.setColorIdentity);
addLife.getModel().setValue(currentData.addLife);
addReputation.getModel().setValue(currentData.addMapReputation);
POIReference.setText(currentData.POIReference);
removeItem.setText(currentData.removeItem);
updating=false;
}
}

View File

@@ -1,144 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.DefaultListCellRenderer;
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.ChangeListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class ActionEditor extends JComponent {
DefaultListModel<DialogData.ActionData> model = new DefaultListModel<>();
JList<DialogData.ActionData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
ActionEdit edit = new ActionEdit();
boolean updating;
public class RewardDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (!(value instanceof DialogData.ActionData))
return label;
/*DialogData.ActionData action=(DialogData.ActionData) value;
StringBuilder builder=new StringBuilder();
if(action.type==null||action.type.isEmpty())
builder.append("Action");
else
builder.append(action.type);*/
label.setText("Action");
return label;
}
}
public void addButton(String name, ActionListener action) {
JButton newButton = new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public ActionEditor() {
list.setCellRenderer(new RewardDataRenderer());
list.addListSelectionListener(e -> ActionEditor.this.updateEdit());
addButton("add", e -> ActionEditor.this.addAction());
addButton("remove", e -> ActionEditor.this.remove());
addButton("copy", e -> ActionEditor.this.copy());
BorderLayout layout = new BorderLayout();
setLayout(layout);
add(list, BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
add(edit, BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged());
}
protected void emitChanged() {
if (updating)
return;
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void copy() {
int selected = list.getSelectedIndex();
if (selected < 0)
return;
DialogData.ActionData data = new DialogData.ActionData(model.get(selected));
model.add(model.size(), data);
}
private void updateEdit() {
int selected = list.getSelectedIndex();
if (selected < 0)
return;
edit.setCurrentAction(model.get(selected));
}
void addAction() {
DialogData.ActionData data = new DialogData.ActionData();
model.add(model.size(), data);
}
void remove() {
int selected = list.getSelectedIndex();
if (selected < 0)
return;
model.remove(selected);
}
public void setAction(DialogData.ActionData[] actions) {
model.clear();
if (actions == null)
return;
for (int i = 0; i < actions.length; i++) {
if (actions[i].grantRewards.length > 0) {
continue; //handled in separate editor and joined in on save, will get duplicated if it appears here
}
model.add(i, actions[i]);
}
}
public DialogData.ActionData[] getAction() {
DialogData.ActionData[] action = new DialogData.ActionData[model.getSize()];
for (int i = 0; i < model.getSize(); i++) {
action[i] = model.get(i);
}
return action;
}
public void clear() {
updating = true;
model.clear();
updating = false;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,125 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.BiomeData;
import javax.swing.*;
public class BiomeEdit extends FormPanel {
BiomeData currentData;
public JSpinner startPointX= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JSpinner startPointY= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JSpinner noiseWeight= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JSpinner distWeight= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JTextField name=new JTextField();
public FilePicker tilesetAtlas=new FilePicker(new String[]{"atlas"});
public JTextField tilesetName=new JTextField();
public JSpinner width= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JSpinner height= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JTextField color=new JTextField();
public JCheckBox collision=new JCheckBox();
public TextListEdit spriteNames =new TextListEdit();
public TextListEdit enemies =new TextListEdit();
public TextListEdit pointsOfInterest =new TextListEdit();
public TerrainsEditor terrain =new TerrainsEditor();
public StructureEditor structures =new StructureEditor();
private boolean updating=false;
public BiomeEdit()
{
FormPanel center=new FormPanel() { };
center.add("startPointX:",startPointX);
center.add("startPointY:",startPointY);
center.add("noiseWeight:",noiseWeight);
center.add("distWeight:",distWeight);
center.add("name:",name);
center.add("tilesetAtlas:",tilesetAtlas);
center.add("tilesetName:",tilesetName);
center.add("width:",width);
center.add("height:",height);
center.add("spriteNames:",spriteNames);
center.add("enemies:",enemies);
center.add("POITags:",pointsOfInterest);
center.add("color:",color);
center.add("collision:",collision);
center.add("terrain/structures:",new JLabel(""));
add(center);
add(terrain);
add(structures);
name.getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain));
tilesetName.getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain));
color.getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain));
collision.addChangeListener(e -> BiomeEdit.this.updateTerrain());
spriteNames.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain));
enemies.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain));
terrain.addChangeListener(e -> BiomeEdit.this.updateTerrain());
startPointX.addChangeListener(e -> BiomeEdit.this.updateTerrain());
startPointY.addChangeListener(e -> BiomeEdit.this.updateTerrain());
noiseWeight.addChangeListener(e -> BiomeEdit.this.updateTerrain());
distWeight.addChangeListener(e -> BiomeEdit.this.updateTerrain());
tilesetAtlas.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(BiomeEdit.this::updateTerrain));
width.addChangeListener(e -> BiomeEdit.this.updateTerrain());
height.addChangeListener(e -> BiomeEdit.this.updateTerrain());
refresh();
}
protected void updateTerrain() {
if(currentData==null||updating)
return;
currentData.startPointX = (Float) startPointX.getValue();
currentData.startPointY = (Float) startPointY.getValue();
currentData.noiseWeight = (Float) noiseWeight.getValue();
currentData.distWeight = (Float)distWeight.getValue();
currentData.name = name.getText();
currentData.tilesetAtlas = tilesetAtlas.edit.getText();
currentData.tilesetName = tilesetName.getText();
currentData.terrain = terrain.getBiomeTerrainData();
currentData.structures = structures.getBiomeStructureData();
currentData.width = (Float) width.getValue();
currentData.height = (Float) height.getValue();
currentData.color = color.getText();
currentData.collision = collision.isSelected();
currentData.spriteNames = spriteNames.getList();
currentData.enemies = enemies.getList();
currentData.pointsOfInterest = pointsOfInterest.getList();
}
public void setCurrentBiome(BiomeData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
startPointX.setValue(currentData.startPointX);
startPointY.setValue(currentData.startPointY);
noiseWeight.setValue(currentData.noiseWeight);
distWeight.setValue(currentData.distWeight);
name.setText(currentData.name);
tilesetAtlas.edit.setText( currentData.tilesetAtlas);
tilesetName.setText(currentData.tilesetName);
terrain.setTerrains(currentData);
structures.setStructures(currentData);
width.setValue(currentData.width);
height.setValue(currentData.height);
color.setText(currentData.color);
spriteNames.setText(currentData.spriteNames);
enemies.setText(currentData.enemies);
collision.setSelected(currentData.collision);
pointsOfInterest.setText(currentData.pointsOfInterest);
updating=false;
}
}

View File

@@ -1,170 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.BiomeStructureData;
import forge.adventure.util.Config;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
public class BiomeStructureDataMappingEditor extends JComponent {
DefaultListModel<BiomeStructureData.BiomeStructureDataMapping> model = new DefaultListModel<>();
JList<BiomeStructureData.BiomeStructureDataMapping> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
BiomeStructureDataMappingEdit edit=new BiomeStructureDataMappingEdit();
private BiomeStructureData data;
public void setCurrent(BiomeStructureData data) {
this.data=data;
model.clear();
if(data==null||data.mappingInfo==null)
return;
for(int i=0;i<data.mappingInfo.length;i++)
model.addElement(data.mappingInfo[i]);
list.setSelectedIndex(0);
}
public BiomeStructureData.BiomeStructureDataMapping[] getCurrent()
{
BiomeStructureData.BiomeStructureDataMapping[] array=new BiomeStructureData.BiomeStructureDataMapping[model.size()];
for(int i=0;i<array.length;i++)
array[i]=model.get(i);
return array;
}
public class BiomeStructureDataMappingRenderer extends DefaultListCellRenderer {
private final BiomeStructureDataMappingEditor editor;
public BiomeStructureDataMappingRenderer(BiomeStructureDataMappingEditor biomeStructureDataMappingEditor) {
this.editor=biomeStructureDataMappingEditor;
}
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping biomeData))
return label;
// Get the renderer component from parent class
label.setText(biomeData.name);
if(editor.data!=null)
{
SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath));
if(itemAtlas.has(biomeData.name))
label.setIcon(itemAtlas.get(biomeData.name));
else
{
ImageIcon img=itemAtlas.getAny();
if(img!=null)
label.setIcon(img);
}
}
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public BiomeStructureDataMappingEditor()
{
list.setCellRenderer(new BiomeStructureDataMappingEditor.BiomeStructureDataMappingRenderer(this));
list.addListSelectionListener(e -> BiomeStructureDataMappingEditor.this.updateEdit());
addButton("add", e -> BiomeStructureDataMappingEditor.this.add());
addButton("remove", e -> BiomeStructureDataMappingEditor.this.remove());
addButton("copy", e -> BiomeStructureDataMappingEditor.this.copy());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(new JScrollPane(list), BorderLayout.WEST);
add(toolBar, BorderLayout.NORTH);
add(edit,BorderLayout.CENTER);
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
BiomeStructureData.BiomeStructureDataMapping data=new BiomeStructureData.BiomeStructureDataMapping(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrent(model.get(selected));
}
void add()
{
BiomeStructureData.BiomeStructureDataMapping data=new BiomeStructureData.BiomeStructureDataMapping();
data.name="Structure "+model.getSize();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
private class BiomeStructureDataMappingEdit extends FormPanel{
BiomeStructureData.BiomeStructureDataMapping currentData;
public JTextField name=new JTextField();
public JTextField color=new JTextField();
public JCheckBox collision=new JCheckBox();
private boolean updating=false;
public BiomeStructureDataMappingEdit()
{
add("name:",name);
add("color:",color);
add("collision:",collision);
name.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureDataMappingEdit.this::update));
color.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureDataMappingEdit.this::update));
collision.addChangeListener(e -> BiomeStructureDataMappingEdit.this.update());
refresh();
}
private void update() {
if(currentData==null||updating)
return;
currentData.name = name.getText();
currentData.color = color.getText();
currentData.collision = collision.isSelected();
}
public void setCurrent(BiomeStructureData.BiomeStructureDataMapping data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
name.setText(currentData.name);
color.setText(currentData.color);
collision.setSelected(currentData.collision);
updating=false;
}
}
}

View File

@@ -1,136 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.BiomeData;
import forge.adventure.data.BiomeStructureData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class BiomeStructureEdit extends FormPanel {
private boolean updating=false;
BiomeStructureData currentData;
BiomeData currentBiomeData;
public JTextField structureAtlasPath=new JTextField();
public FloatSpinner x= new FloatSpinner();
public FloatSpinner y= new FloatSpinner();
public FloatSpinner width= new FloatSpinner();
public FloatSpinner height= new FloatSpinner();
public JCheckBox randomPosition=new JCheckBox();
public IntSpinner N= new IntSpinner();
public JTextField sourcePath= new JTextField();
public JTextField maskPath= new JTextField();
public JCheckBox periodicInput= new JCheckBox();
public IntSpinner ground= new IntSpinner();
public IntSpinner symmetry= new IntSpinner();
public JCheckBox periodicOutput= new JCheckBox();
public BiomeStructureDataMappingEditor data=new BiomeStructureDataMappingEditor();
public BiomeStructureEdit()
{
FormPanel center=new FormPanel();
center.add("structureAtlasPath:",structureAtlasPath);
center.add("x:",x);
center.add("y:",y);
center.add("width:",width);
center.add("height:",height);
center.add("N:",N);
center.add("sourcePath:",sourcePath);
center.add("maskPath:",maskPath);
center.add("periodicInput:",periodicInput);
center.add("ground:",ground);
center.add("symmetry:",symmetry);
center.add("periodicOutput:",periodicOutput);
add(center);
add(data);
structureAtlasPath.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureEdit.this::updateStructure));
x.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
y.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
width.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
height.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
randomPosition.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
N.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
sourcePath.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureEdit.this::updateStructure));
maskPath.getDocument().addDocumentListener(new DocumentChangeListener(BiomeStructureEdit.this::updateStructure));
periodicInput.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
ground.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
symmetry.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
periodicOutput.addChangeListener(e -> BiomeStructureEdit.this.updateStructure());
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
data.setCurrent(null);
return;
}
updating=true;
structureAtlasPath.setText(currentData.structureAtlasPath);
x.setValue(currentData.x);
y.setValue(currentData.y);
width.setValue(currentData.width);
height.setValue(currentData.height);
randomPosition.setSelected(currentData.randomPosition);
N.setValue(currentData.N);
sourcePath.setText(currentData.sourcePath);
maskPath.setText(currentData.maskPath);
periodicInput.setSelected(currentData.periodicInput);
ground.setValue(currentData.ground);
symmetry.setValue(currentData.symmetry);
periodicOutput.setSelected(currentData.periodicOutput);
data.setCurrent(currentData);
updating=false;
}
public void updateStructure()
{
if(currentData==null||updating)
return;
currentData.structureAtlasPath=structureAtlasPath.getText();
currentData.x= x.floatValue();
currentData.y= y.floatValue();
currentData.width= width.floatValue();
currentData.height= height.floatValue();
currentData.randomPosition=randomPosition.isSelected();
currentData.mappingInfo= data.getCurrent();
currentData.N= N.intValue();
currentData.sourcePath= sourcePath.getText();
currentData.maskPath= maskPath.getText();
currentData.periodicInput= periodicInput.isSelected();
currentData.ground= ground.intValue();
currentData.symmetry= symmetry.intValue();
currentData.periodicOutput= periodicOutput.isSelected();
emitChanged();
}
public void setCurrentStructure(BiomeStructureData biomeTerrainData, BiomeData data) {
currentData =biomeTerrainData;
currentBiomeData=data;
refresh();
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
}

View File

@@ -1,84 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.BiomeData;
import forge.adventure.data.BiomeTerrainData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class BiomeTerrainEdit extends FormPanel {
SwingAtlasPreview preview=new SwingAtlasPreview(128);
private boolean updating=false;
BiomeTerrainData currentData;
BiomeData currentBiomeData;
public JTextField spriteName=new JTextField();
public JSpinner min= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JSpinner max= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JSpinner resolution= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public BiomeTerrainEdit()
{
FormPanel center=new FormPanel() { };
center.add("spriteName:",spriteName);
center.add("min:",min);
center.add("max:",max);
center.add("resolution:",resolution);
add(center,preview);
spriteName.getDocument().addDocumentListener(new DocumentChangeListener(BiomeTerrainEdit.this::updateTerrain));
min.addChangeListener(e -> BiomeTerrainEdit.this.updateTerrain());
max.addChangeListener(e -> BiomeTerrainEdit.this.updateTerrain());
resolution.addChangeListener(e -> BiomeTerrainEdit.this.updateTerrain());
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
spriteName.setText(currentData.spriteName);
min.setValue(currentData.min);
max.setValue(currentData.max);
resolution.setValue(currentData.resolution);
if(currentBiomeData!=null&&currentData!= null)
preview.setSpritePath(currentBiomeData.tilesetAtlas,currentData.spriteName);
updating=false;
}
public void updateTerrain()
{
if(currentData==null||updating)
return;
currentData.spriteName=spriteName.getText();
currentData.min= (float) min.getValue();
currentData.max= (float) max.getValue();
currentData.resolution= (float) resolution.getValue();
preview.setSpritePath(currentBiomeData.tilesetAtlas,currentData.spriteName);
emitChanged();
}
public void setCurrentTerrain(BiomeTerrainData biomeTerrainData, BiomeData data) {
currentData =biomeTerrainData;
currentBiomeData=data;
refresh();
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
}

View File

@@ -1,155 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
public class DialogEdit extends FormPanel {
private boolean updating=false;
DialogData currentData;
public JTextField name=new JTextField(80);
public JTextField text=new JTextField(80);
public JTextField locname=new JTextField(80);
public JTextField loctext=new JTextField(80);
JPanel namePanel;
JPanel locNamePanel;
public JButton addNode = new JButton("Add node");
public JButton removeNode = new JButton("Remove node");
public DialogEdit()
{
FormPanel center=new FormPanel() { };
name.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
if (!updating)
emitChanged();
}
@Override
public void removeUpdate(DocumentEvent e) {
if (!updating)
emitChanged();
}
@Override
public void changedUpdate(DocumentEvent e) {
if (!updating)
emitChanged();
}
});
text.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
if (!updating)
emitChanged();
}
@Override
public void removeUpdate(DocumentEvent e) {
if (!updating)
emitChanged();
}
@Override
public void changedUpdate(DocumentEvent e) {
if (!updating)
emitChanged();
}
});
JPanel editData = new JPanel();
editData.setLayout(new BoxLayout(editData, BoxLayout.Y_AXIS));
namePanel = new JPanel();
namePanel.setLayout(new FlowLayout());
namePanel.add(new JLabel("Name:"));
namePanel.add(name);
editData.add(namePanel);
JPanel textPanel = new JPanel();
textPanel.setLayout(new FlowLayout());
textPanel.add(new JLabel("Text:"));
textPanel.add(text);
editData.add(textPanel);
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new FlowLayout());
buttonPanel.add(addNode);
buttonPanel.add(removeNode);
editData.add(buttonPanel);
editData.add(new JLabel("localization tokens for translation"));
locNamePanel = new JPanel();
locNamePanel.setLayout(new FlowLayout());
locNamePanel.add(new JLabel("Name Token:"));
locNamePanel.add(locname);
JPanel locTextPanel = new JPanel();
locTextPanel.setLayout(new FlowLayout());
locTextPanel.add(new JLabel("Text Token:"));
locTextPanel.add(loctext);
editData.add(locNamePanel);
editData.add(locTextPanel);
center.add(editData);
add(center);
refresh();
}
public void refresh(){
refresh(false);
}
public void refresh(boolean onRootNode) {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
name.setText(currentData.name);
locname.setText(currentData.locname);
text.setText(currentData.text);
loctext.setText(currentData.loctext);
namePanel.setVisible(!onRootNode);
locNamePanel.setVisible(!onRootNode);
updating=false;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
}

View File

@@ -1,242 +0,0 @@
package forge.adventure.editor;
import com.google.common.collect.ObjectArrays;
import forge.adventure.data.DialogData;
import forge.adventure.data.RewardData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class DialogEditor extends JComponent{
private boolean updating = false;
private java.util.List<DialogData> allNodes = new ArrayList<>();
public JTextArea text =new JTextArea("(Initial dialog text)", 3, 80);
public RewardsEditor rewardsEditor = new RewardsEditor();
public ActionEditor actionEditor = new ActionEditor();
public DialogOptionEditor optionEditor = new DialogOptionEditor();
public DialogTree navTree = new DialogTree();
public DialogEdit edit = new DialogEdit();
private DialogData root = new DialogData();
private DialogData current = new DialogData();
public DialogEditor(){
buildUI();
navTree.addSelectionListener(q -> loadNewNodeSelection());
edit.addChangeListener(q-> acceptEdits());
edit.addNode.addActionListener(q -> addNode());
edit.removeNode.addActionListener(q -> removeNode());
}
public void loadData(DialogData rootDialogData){
updating = true;
if (rootDialogData == null)
{
rootDialogData = new DialogData();
}
root = rootDialogData;
navTree.loadDialog(rootDialogData);
text.setText(rootDialogData.text);
updating = false;
}
public DialogData getDialogData(){
return root;
}
JTabbedPane tabs = new JTabbedPane();
JToolBar conditionsToolbar = new JToolBar("conditionsToolbar");
JToolBar actionsToolbar = new JToolBar("actionsToolbar");
JToolBar optionsToolbar = new JToolBar("optionsToolbar");
JToolBar tokensToolbar = new JToolBar("tokensToolbar");
JPanel conditionsPanel = new JPanel();
JPanel actionsPanel = new JPanel();
JPanel optionsPanel = new JPanel();
JPanel rewardsPanel = new JPanel();
JPanel tokensPanel = new JPanel();
class QuestTextDocumentListener implements DocumentListener {
public void changedUpdate(DocumentEvent e) {
root.text = text.getText();
emitChanged();
}
public void removeUpdate(DocumentEvent e) {
root.text = text.getText();
emitChanged();
}
public void insertUpdate(DocumentEvent e) {
root.text = text.getText();
emitChanged();
}
}
public void buildUI(){
buildTabs();
BorderLayout layout=new BorderLayout();
setLayout(layout);
JPanel textArea = new JPanel();
textArea.setLayout(new FlowLayout());
textArea.add(new JLabel("Dialog Start"));
textArea.add(text);
text.getDocument().addDocumentListener(new QuestTextDocumentListener());
JSplitPane splitPane = new JSplitPane();
splitPane.setLeftComponent(navTree);
splitPane.setRightComponent(tabs);
splitPane.setResizeWeight(0.2);
splitPane.setDividerLocation(.2);
add(textArea, BorderLayout.NORTH);
add(splitPane, BorderLayout.CENTER);
}
public void loadNewNodeSelection(){
updating = true;
current = navTree.getSelectedData();
edit.currentData = navTree.getSelectedData();
edit.refresh(root.equals(current));
rewardsEditor.clear();
actionEditor.clear();
if (navTree.getSelectedData() != null) {
for (DialogData.ActionData action : navTree.getSelectedData().action) {
if (action.grantRewards != null && action.grantRewards.length > 0)
rewardsEditor.setRewards(action.grantRewards);
}
actionEditor.setAction(navTree.getSelectedData().action);
}
updating = false;
}
public void acceptEdits(){
if (current == null)
return;
current.name = edit.name.getText();
current.text = edit.text.getText();
current.locname = edit.locname.getText();
current.loctext = edit.loctext.getText();
root.text = text.getText();
DialogData.ActionData[] action = actionEditor.getAction();
ArrayList<DialogData.ActionData> actionsList = new ArrayList<>(Arrays.stream(action).collect(Collectors.toList()));
RewardData[] rewards= rewardsEditor.getRewards();
if (rewards.length > 0)
{
DialogData.ActionData rewardAction = new DialogData.ActionData();
rewardAction.grantRewards = rewards;
actionsList.add(rewardAction);
}
current.action = actionsList.toArray(current.action);
navTree.replaceCurrent();
emitChanged();
}
public void addNode(){
DialogData newNode = new DialogData();
newNode.name = "NewResponse";
DialogData parent = navTree.getSelectedData()!=null?navTree.getSelectedData():root;
parent.options = ObjectArrays.concat(parent.options, newNode);
navTree.loadDialog(root);
navTree.setSelectedData(newNode);
emitChanged();
}
public void removeNode(){
if (navTree.getSelectedData() == null)
return;
navTree.removeSelectedData();
emitChanged();
}
public void buildTabs(){
buildToolBars();
actionsPanel.add(actionsToolbar);
actionsPanel.add(actionEditor);
optionsPanel.add(edit);
rewardsPanel.add(rewardsEditor);
rewardsEditor.addChangeListener(e -> DialogEditor.this.acceptEdits());
actionEditor.addChangeListener(e -> DialogEditor.this.acceptEdits());
tokensPanel.add(tokensToolbar);
tokensPanel.add(new JLabel("Insert token editor here"));
tabs.addTab("Options", optionsPanel);
tabs.addTab("Conditions", conditionsPanel);
tabs.addTab("Actions", actionsPanel);
tabs.addTab("Rewards", rewardsPanel);
tabs.addTab("Tokens",tokensPanel);
}
public void buildToolBars(){
conditionsToolbar.setFloatable(false);
actionsToolbar.setFloatable(false);
optionsToolbar.setFloatable(false);
JButton addOption = new JButton("Add Option");
addOption.addActionListener(e -> DialogEditor.this.addOption());
optionsToolbar.add(addOption);
JButton copyOption = new JButton("Copy Selected");
copyOption.addActionListener(e -> DialogEditor.this.copyOption());
optionsToolbar.add(copyOption);
JButton removeOption = new JButton("Remove Selected");
removeOption.addActionListener(e -> DialogEditor.this.removeOption());
optionsToolbar.add(removeOption);
}
public void addOption(){
optionEditor.addOption();
emitChanged();
}
public void copyOption(){
emitChanged();
}
public void removeOption(){
int selected=optionEditor.list.getSelectedIndex();
if(selected<0)
return;
optionEditor.model.remove(selected);
emitChanged();
}
protected void emitChanged() {
if (updating)
return;
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,85 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class DialogOptionEdit extends FormPanel {
DialogData currentData;
JLabel nameLabel = new JLabel("Name (Player dialog / action)");
JLabel textLabel = new JLabel("Text (Game response to Name - Leave blank to end dialog)");
JTextArea text =new JTextArea(3,80);
JTextArea name =new JTextArea(3,80);
JButton add = new JButton();
JButton load = new JButton();
private boolean updating=false;
public DialogOptionEdit()
{
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
JPanel upper = new JPanel();
upper.setLayout(new BorderLayout());
upper.add(nameLabel, BorderLayout.NORTH);
upper.add(name, BorderLayout.CENTER);
add(upper);
JPanel middle = new JPanel();
middle.setLayout(new BorderLayout());
middle.add(textLabel, BorderLayout.NORTH);
middle.add(text, BorderLayout.CENTER);
add(middle);
name.getDocument().addDocumentListener(new DocumentChangeListener(DialogOptionEdit.this::updateDialog));
text.getDocument().addDocumentListener(new DocumentChangeListener(DialogOptionEdit.this::updateDialog));
}
private void updateDialog() {
if(currentData==null||updating)
return;
currentData.text = text.getText().trim();
currentData.name = name.getText().trim();
//currentData.condition = conditionEditor.getConditions();
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
public void setCurrentOption(DialogData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
updating=true;
text.setText(currentData.text!=null?currentData.text:"");
name.setText(currentData.name!=null?currentData.name:"");
updating=false;
}
}

View File

@@ -1,138 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class DialogOptionEditor extends JComponent{
DefaultListModel<DialogData> model = new DefaultListModel<>();
JList<DialogData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
DialogOptionEdit edit=new DialogOptionEdit();
public class DialogDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof DialogData dialog))
return label;
StringBuilder builder=new StringBuilder();
if(dialog.name==null||dialog.name.isEmpty())
builder.append("[[Blank Option]]");
else
builder.append(dialog.name, 0, Math.min(dialog.name.length(), 25));
label.setText(builder.toString());
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public DialogOptionEditor()
{
list.setCellRenderer(new DialogDataRenderer());
list.addListSelectionListener(e -> DialogOptionEditor.this.updateEdit());
addButton("add", e -> DialogOptionEditor.this.addOption());
addButton("remove", e -> DialogOptionEditor.this.remove());
addButton("copy", e -> DialogOptionEditor.this.copy());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(list, BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
toolBar.setFloatable(false);
add(edit,BorderLayout.CENTER);
edit.setVisible(false);
edit.addChangeListener(e -> emitChanged());
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
DialogData data=new DialogData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
edit.setCurrentOption(new DialogData());
else
edit.setCurrentOption(model.get(selected));
}
void addOption()
{
DialogData data=new DialogData();
model.add(model.size(),data);
edit.setVisible(true);
list.setSelectedIndex(model.size() - 1);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
edit.setVisible(model.size() > 0);
}
public void setOptions(DialogData[] options) {
model.clear();
if(options==null || options.length == 0)
options = new DialogData[0];
for (int i=0;i<options.length;i++) {
model.add(i,options[i]);
}
if (model.size() > 0)
{
edit.setVisible(true);
list.setSelectedIndex(0);
updateEdit();
}
else{
edit.setVisible(false);
}
}
public DialogData[] getOptions() {
DialogData[] options= new DialogData[model.getSize()];
for(int i=0;i<model.getSize();i++)
{
options[i]=model.get(i);
}
return options;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,116 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DialogTree extends JPanel {
private JTree dialogTree;
private JScrollPane scrollPane;
public DialogTree(){
setLayout(new BorderLayout());
// Create the JTree component
dialogTree = new JTree();
dialogTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
addSelectionListener();
// Create a scroll pane to contain the JTree
scrollPane = new JScrollPane(dialogTree);
// Add the scroll pane to the panel
add(scrollPane, BorderLayout.CENTER);
}
public void loadDialog(DialogData dialogData){
// rootNode = buildBranches(dialogData);
// ((DefaultTreeModel)dialogTree.getModel()).setRoot(rootNode);
((DefaultTreeModel)dialogTree.getModel()).setRoot(buildBranches(dialogData));
}
public DefaultMutableTreeNode buildBranches(DialogData dialogData)
{
DefaultMutableTreeNode node = new DefaultMutableTreeNode(dialogData);
for (DialogData option : dialogData.options){
node.add(buildBranches(option));
}
return node;
}
private final List<TreeSelectionListener> selectionListeners = new ArrayList<>();
public void addSelectionListener(){
//subscribe to valueChanged, change to that object in edit pane
dialogTree.getSelectionModel().addTreeSelectionListener(this::emitChanged);
}
public void addSelectionListener(final TreeSelectionListener listener) {
selectionListeners.remove(listener); //ensure listener not added multiple times
selectionListeners.add(listener);
}
protected void emitChanged(TreeSelectionEvent evt) {
if (selectionListeners != null && selectionListeners.size() > 0) {
for (TreeSelectionListener listener : selectionListeners) {
listener.valueChanged(evt);
}
}
}
public void replaceCurrent(){
if (dialogTree.getSelectionPath() == null || dialogTree.getSelectionPath().getLastPathComponent() == null)
return;
dialogTree.updateUI();
}
public DialogData getSelectedData() {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) dialogTree.getLastSelectedPathComponent();
if (selectedNode != null) {
return (DialogData) selectedNode.getUserObject();
}
return null;
}
public void removeSelectedData() {
//Todo: Enhance this to not collapse any nodes (after setSelectedData other paths are still collapsed)
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) dialogTree.getLastSelectedPathComponent();
DialogData parentData = (DialogData) ((DefaultMutableTreeNode)selectedNode.getParent()).getUserObject();
parentData.options = Arrays.stream(parentData.options).filter(q -> q != selectedNode.getUserObject()).toArray(DialogData[]::new);
((DefaultTreeModel) dialogTree.getModel()).removeNodeFromParent(selectedNode);
((DefaultTreeModel) dialogTree.getModel()).reload();
setSelectedData(parentData);
}
public void setSelectedData(DialogData data) {
// Find the node with the given data object and select it in the tree
DefaultMutableTreeNode node = findNode((DefaultMutableTreeNode)dialogTree.getModel().getRoot(), data);
if (node != null) {
dialogTree.setSelectionPath(new TreePath(node.getPath()));
}
}
private DefaultMutableTreeNode findNode(DefaultMutableTreeNode parent, DialogData data) {
// Search for the node with the given data object in the subtree rooted at the parent node
if (parent.getUserObject() == data) {
return parent;
}
for (int i = 0; i < parent.getChildCount(); i++) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) parent.getChildAt(i);
DefaultMutableTreeNode result = findNode(child, data);
if (result != null) {
return result;
}
}
return null;
}
}

View File

@@ -1,32 +0,0 @@
package forge.adventure.editor;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class DocumentChangeListener implements DocumentListener {
private final Runnable run;
public DocumentChangeListener(Runnable run)
{
this.run = run;
}
@Override
public void insertUpdate(DocumentEvent e) {
changedUpdate(e);
}
@Override
public void removeUpdate(DocumentEvent e) {
changedUpdate(e);
}
@Override
public void changedUpdate(DocumentEvent e) {
run.run();
}
}

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

View File

@@ -1,113 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.EffectData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class EffectEditor extends JComponent {
EffectData currentData;
JTextField name =new JTextField();
JSpinner changeStartCards = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner lifeModifier = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner moveSpeed = new JSpinner(new SpinnerNumberModel(0f, 0, 1, 0.1f));
TextListEdit startBattleWithCard =new TextListEdit();
JCheckBox colorView =new JCheckBox();
EffectEditor opponent = null;
private boolean updating=false;
public EffectEditor(boolean isOpponentEffect)
{
if(!isOpponentEffect)
opponent=new EffectEditor(true);
setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
FormPanel parameters=new FormPanel();
parameters.setBorder(BorderFactory.createTitledBorder("Effect"));
parameters.add("Name:", name);
parameters.add("Start with extra cards:", changeStartCards);
parameters.add("Change life:", lifeModifier);
parameters.add("Movement speed:", moveSpeed);
parameters.add("Start battle with cards:", startBattleWithCard);
parameters.add("color view:", colorView);
add(parameters);
if(!isOpponentEffect)
{ add(new JLabel("Opponent:")); add(opponent);}
changeStartCards.addChangeListener(e -> EffectEditor.this.updateEffect());
lifeModifier.addChangeListener(e -> EffectEditor.this.updateEffect());
moveSpeed.addChangeListener(e -> EffectEditor.this.updateEffect());
colorView.addChangeListener(e -> EffectEditor.this.updateEffect());
name.getDocument().addDocumentListener(new DocumentChangeListener(EffectEditor.this::updateEffect));
startBattleWithCard.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(EffectEditor.this::updateEffect));
if(opponent!=null)
opponent.addChangeListener(e -> EffectEditor.this.updateEffect());
}
private void updateEffect() {
if(currentData==null||updating)
return;
currentData.name=name.getText();
currentData.changeStartCards = (Integer) changeStartCards.getValue();
currentData.lifeModifier = (Integer) lifeModifier.getValue();
currentData.moveSpeed = (Float) moveSpeed.getValue();
currentData.startBattleWithCard = startBattleWithCard.getList();
currentData.colorView = colorView.isSelected();
currentData.opponent = opponent.currentData;
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
public void setCurrentEffect(EffectData data)
{
if(data==null)
return;
currentData=data;
refresh();
}
public EffectData getCurrentEffect()
{
return currentData;
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
name.setText(currentData.name);
lifeModifier.setValue(currentData.lifeModifier);
changeStartCards.setValue(currentData.changeStartCards);
startBattleWithCard.setText(currentData.startBattleWithCard);
colorView.setSelected(currentData.colorView);
moveSpeed.setValue(currentData.moveSpeed);
if(opponent!=null)
opponent.setCurrentEffect(currentData.opponent);
updating=false;
}
}

View File

@@ -1,337 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.EnemyData;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Enumeration;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class EnemyEdit extends FormPanel {
EnemyData currentData;
JTextField nameField=new JTextField();
JTextField nameOverride=new JTextField();
JTextField colorField=new JTextField();
JTextField ai=new JTextField();
JCheckBox flying=new JCheckBox();
JCheckBox boss=new JCheckBox();
JCheckBox ignoreDungeonEffect=new JCheckBox();
FloatSpinner lifeFiled= new FloatSpinner(0, 1000, 1);
FloatSpinner spawnRate= new FloatSpinner( 0.f, 1, 0.1f);
FloatSpinner scale= new FloatSpinner( 0.f, 8, 0.1f);
FloatSpinner difficulty= new FloatSpinner( 0.f, 1, 0.1f);
FloatSpinner speed= new FloatSpinner( 0.f, 100.f, 1.0f);
FilePicker deck=new FilePicker(new String[]{"dck","json"});
FilePicker atlas=new FilePicker(new String[]{"atlas"});
JTextField equipment=new JTextField();
RewardsEditor rewards=new RewardsEditor();
SwingAtlasPreview preview=new SwingAtlasPreview();
JTextField manualEntry = new JTextField(20);
private boolean updating=false;
DefaultListModel<String> existingModel = new DefaultListModel<>();
DefaultListModel<String> enemyModel = new DefaultListModel<>();
JList<String> existingTags;
JList<String> enemyTags;
public EnemyEdit()
{
setLayout(new BorderLayout());
FormPanel top=new FormPanel() { };
add(top, BorderLayout.NORTH);
JTabbedPane tabs = new JTabbedPane();
add(tabs, BorderLayout.CENTER);
FormPanel basicInfo=new FormPanel() { };
tabs.addTab("Basic Info", basicInfo);
top.add("Name:",nameField);
top.add("Display Name:", nameOverride);
basicInfo.add("Life:",lifeFiled);
basicInfo.add("Spawn rate:",spawnRate);
basicInfo.add("Scale:",scale);
basicInfo.add("Difficulty:",difficulty);
basicInfo.add("Speed:",speed);
basicInfo.add("Deck:",deck);
basicInfo.add("Equipment:",equipment);
basicInfo.add("Colors:",colorField);
basicInfo.add("ai:",ai);
basicInfo.add("flying:",flying);
basicInfo.add("boss:",boss);
JPanel visual = new JPanel();
visual.add("Sprite:",atlas);
visual.add(preview);
JPanel tags = new JPanel();
existingTags = new JList<>();
existingTags.getInputMap(JList.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke("ENTER"), "addSelected");
existingTags.getActionMap().put("addSelected", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
addSelected();
int index = existingTags.getSelectedIndex();
String selectedItem = existingTags.getSelectedValue();
if (selectedItem != null) {
enemyModel.addElement(selectedItem);
existingTags.grabFocus();
existingTags.setSelectedIndex(index<existingModel.size()?index:index-1);
}
}
});
existingModel = QuestController.getInstance().getEnemyTags();
existingTags.setModel(existingModel);
enemyTags = new JList<>();
enemyModel = new DefaultListModel<>();
enemyTags.setModel(enemyModel);
enemyTags.getModel().addListDataListener(new ListDataListener() {
@Override
public void intervalAdded(ListDataEvent e) {
doUpdate();
}
@Override
public void intervalRemoved(ListDataEvent e) {
doUpdate();
}
@Override
public void contentsChanged(ListDataEvent e) {
doUpdate();
}
});
JButton select = new JButton("Select");
select.addActionListener(q -> addSelected());
JButton add = new JButton("Manual Add");
add.addActionListener(q -> manualAdd(enemyModel));
JButton remove = new JButton("Remove Item");
remove.addActionListener(q -> removeSelected());
tags.setLayout(new BorderLayout());
JPanel left = new JPanel();
left.setLayout(new BorderLayout());
left.add(new JLabel("Tags already in use"), BorderLayout.NORTH);
JScrollPane listScroller = new JScrollPane(existingTags);
listScroller.setMinimumSize(new Dimension(400, 800));
left.add(listScroller, BorderLayout.CENTER);
tags.add(left, BorderLayout.WEST);
FormPanel tagEdit = new FormPanel();
tagEdit.setLayout(new BorderLayout());
FormPanel mappedTags = new FormPanel();
mappedTags.setLayout(new BorderLayout());
mappedTags.add(new JLabel("Tags Mapped to this object"), BorderLayout.NORTH);
JScrollPane listScroller2 = new JScrollPane(enemyTags);
listScroller2.setMinimumSize(new Dimension(400, 800));
mappedTags.add(listScroller2, BorderLayout.CENTER);
tagEdit.add(mappedTags,BorderLayout.EAST);
JPanel controlPanel = new JPanel();
controlPanel.add(select);
controlPanel.add(add);
controlPanel.add(manualEntry);
manualEntry.getInputMap(JList.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke("ENTER"), "addTyped");
manualEntry.getActionMap().put("addTyped", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (!manualEntry.getText().trim().isEmpty()) {
manualAdd(enemyModel);
manualEntry.grabFocus();
}
}
});
controlPanel.add(remove);
tagEdit.add(controlPanel, BorderLayout.CENTER);
tags.add(tagEdit,BorderLayout.CENTER);
JTextArea right1 = new JTextArea("This is really just to pad some space\n" +
"but also to explain the use of tags.\n" +
"Rather than adding 100's of object names\n" +
"to every quest definition, instead we will\n"+
"categorize enemies and points of interest with\n"+
"tags and reference those categories in quests");
right1.setEnabled(false);
tags.add(right1, BorderLayout.EAST);
tabs.addTab("Sprite", visual);
tabs.addTab("Rewards", rewards);
tabs.addTab("Quest Tags", tags);
equipment.getDocument().addDocumentListener(new DocumentChangeListener(EnemyEdit.this::updateEnemy));
atlas.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(EnemyEdit.this::updateEnemy));
colorField.getDocument().addDocumentListener(new DocumentChangeListener(EnemyEdit.this::updateEnemy));
ai.getDocument().addDocumentListener(new DocumentChangeListener(EnemyEdit.this::updateEnemy));
flying.addChangeListener(e -> EnemyEdit.this.updateEnemy());
boss.addChangeListener(e -> EnemyEdit.this.updateEnemy());
ignoreDungeonEffect.addChangeListener(e -> EnemyEdit.this.updateEnemy());
nameField.getDocument().addDocumentListener(new DocumentChangeListener(EnemyEdit.this::updateEnemy));
nameOverride.getDocument().addDocumentListener(new DocumentChangeListener(EnemyEdit.this::updateEnemy));
deck.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(EnemyEdit.this::updateEnemy));
lifeFiled.addChangeListener(e -> EnemyEdit.this.updateEnemy());
speed.addChangeListener(e -> EnemyEdit.this.updateEnemy());
scale.addChangeListener(e -> EnemyEdit.this.updateEnemy());
difficulty.addChangeListener(e -> EnemyEdit.this.updateEnemy());
spawnRate.addChangeListener(e -> EnemyEdit.this.updateEnemy());
rewards.addChangeListener(e -> EnemyEdit.this.updateEnemy());
lifeFiled.addChangeListener(e -> EnemyEdit.this.updateEnemy());
enemyModel.addListDataListener(new ListDataListener() {
@Override
public void intervalAdded(ListDataEvent e) {
EnemyEdit.this.updateEnemy();
}
@Override
public void intervalRemoved(ListDataEvent e) {
EnemyEdit.this.updateEnemy();
}
@Override
public void contentsChanged(ListDataEvent e) {
EnemyEdit.this.updateEnemy();
}
});
refresh();
}
private void doUpdate(){
EnemyEdit.this.updateEnemy();
}
private void addSelected(){
if (existingTags.getSelectedIndex()>-1)
enemyModel.addElement(existingTags.getModel().getElementAt(existingTags.getSelectedIndex()));
doUpdate();
}
private void removeSelected(){
if (enemyTags.getSelectedIndex()>-1)
enemyModel.remove(enemyTags.getSelectedIndex());
doUpdate();
}
private void filterExisting(DefaultListModel<String> filter){
DefaultListModel<String> toReturn = new DefaultListModel<>();
for (Enumeration<String> e = QuestController.getInstance().getEnemyTags().elements(); e.hasMoreElements();){
String toTest = e.nextElement();
if (toTest != null & !filter.contains(toTest)){
toReturn.addElement(toTest);
}
}
existingTags.setModel(toReturn);
}
private void manualAdd(DefaultListModel<String> model){
if (!manualEntry.getText().trim().isEmpty())
model.addElement(manualEntry.getText().trim());
manualEntry.setText("");
doUpdate();
}
public void updateEnemy() {
if(currentData==null||updating)
return;
currentData.name=nameField.getText();
currentData.colors=colorField.getText();
currentData.ai=ai.getText();
currentData.flying=flying.isSelected();
currentData.boss=boss.isSelected();
currentData.life= (int) lifeFiled.getValue();
currentData.sprite= atlas.getEdit().getText();
if(equipment.getText().isEmpty())
currentData.equipment=null;
else
currentData.equipment=equipment.getText().split(",");
currentData.speed= speed.floatValue();
currentData.scale= scale.floatValue();
currentData.spawnRate=spawnRate.floatValue();
currentData.difficulty=difficulty.floatValue();
currentData.deck= deck.getEdit().getText().split(",");
currentData.rewards= rewards.getRewards();
preview.setSpritePath(currentData.sprite);
ArrayList<String> tags = new ArrayList<>();
for (Enumeration<String> e = enemyModel.elements(); e.hasMoreElements();){
tags.add(e.nextElement());
}
tags.removeIf(String::isEmpty);
currentData.questTags = tags.toArray(currentData.questTags);
QuestController.getInstance().refresh();
filterExisting(enemyModel);
}
public void setCurrentEnemy(EnemyData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
nameField.setText(currentData.name);
colorField.setText(currentData.colors);
ai.setText(currentData.ai);
boss.setSelected(currentData.boss);
flying.setSelected(currentData.flying);
lifeFiled.setValue(currentData.life);
atlas.getEdit().setText(currentData.sprite);
if(currentData.equipment!=null)
equipment.setText(String.join(",",currentData.equipment));
else
equipment.setText("");
deck.getEdit().setText(String.join(",",currentData.deck));
speed.setValue(currentData.speed);
scale.setValue(currentData.scale);
spawnRate.setValue(currentData.spawnRate);
difficulty.setValue(currentData.difficulty);
rewards.setRewards(currentData.rewards);
preview.setSpritePath(currentData.sprite);
enemyModel.clear();
for(String val : currentData.questTags) {
if (val != null)
enemyModel.addElement(val);
}
filterExisting(enemyModel);
updating=false;
}
}

View File

@@ -1,132 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.EnemyData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class EnemyEditor extends JComponent {
DefaultListModel<EnemyData> model = new DefaultListModel<>();
JList<EnemyData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
EnemyEdit edit=new EnemyEdit();
public class EnemyDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof EnemyData))
return label;
EnemyData enemy=(EnemyData) value;
// Get the renderer component from parent class
label.setText(enemy.name);
SwingAtlas atlas=new SwingAtlas(Config.instance().getFile(enemy.sprite));
if(atlas.has("Avatar"))
label.setIcon(atlas.get("Avatar"));
else
{
ImageIcon img=atlas.getAny();
if(img!=null)
label.setIcon(img);
}
return label;
}
}
public void addButton(String name,ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public EnemyEditor()
{
list.setCellRenderer(new EnemyDataRenderer());
list.addListSelectionListener(e -> EnemyEditor.this.updateEdit());
addButton("add", e -> EnemyEditor.this.addEnemy());
addButton("remove", e -> EnemyEditor.this.remove());
addButton("copy", e -> EnemyEditor.this.copy());
addButton("load", e -> {
EnemyEditor.this.load();
QuestController.getInstance().load();
});
addButton("save", e -> EnemyEditor.this.save());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(new JScrollPane(list), BorderLayout.LINE_START);
toolBar.setFloatable(false);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
load();
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
EnemyData data=new EnemyData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentEnemy(model.get(selected));
edit.updateEnemy();
}
void save()
{
Array<EnemyData> allEnemies=new Array<>();
for(int i=0;i<model.getSize();i++)
allEnemies.add(model.get(i));
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.ENEMIES);
handle.writeString(json.prettyPrint(json.toJson(allEnemies,Array.class, EnemyData.class)),false);
}
void load()
{
model.clear();
Array<EnemyData> allEnemies=new Array<>();
Json json = new Json();
FileHandle handle = Config.instance().getFile(Paths.ENEMIES);
if (handle.exists())
{
allEnemies = json.fromJson(Array.class, EnemyData.class, handle);
}
for (int i=0;i<allEnemies.size;i++) {
model.add(i,allEnemies.get(i));
}
}
void addEnemy()
{
EnemyData data=new EnemyData();
data.name="Enemy "+model.getSize();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
}

View File

@@ -1,51 +0,0 @@
package forge.adventure.editor;
import forge.adventure.util.Config;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.io.File;
import java.io.IOException;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class FilePicker extends Box {
JTextField edit=new JTextField();
JButton findButton=new JButton(UIManager.getIcon("FileView.directoryIcon"));
private final String[] fileEndings;
public FilePicker(String[] fileEndings) {
super(BoxLayout.X_AXIS);
this.fileEndings = fileEndings;
findButton.addActionListener(e -> FilePicker.this.find());
add(edit);
add(findButton);
}
JTextField getEdit()
{
return edit;
}
private void find() {
JFileChooser fc = new JFileChooser();
fc.setCurrentDirectory(new File(Config.instance().getFilePath(edit.getText())));
fc.setFileFilter( new FileNameExtensionFilter("Pick File",fileEndings));
fc.setMultiSelectionEnabled(false);
if (fc.showOpenDialog(this) ==
JFileChooser.APPROVE_OPTION) {
File selected = fc.getSelectedFile();
try {
if (selected != null&&selected.getCanonicalPath().startsWith(new File(Config.instance().getFilePath("")).getCanonicalPath())) {
edit.setText(selected.getCanonicalPath().substring(new File(Config.instance().getFilePath("")).getCanonicalPath().length()+1).replace('\\','/'));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -1,19 +0,0 @@
package forge.adventure.editor;
import javax.swing.*;
public class FloatSpinner extends JSpinner{
public FloatSpinner()
{
this( 0.f, 1f, 0.1f);
}
public FloatSpinner(float min,float max,float stepSize)
{
super(new SpinnerNumberModel(new Float(0.0f), new Float(min), new Float (max), new Float(stepSize)));
}
public float floatValue()
{
return (Float) getValue();
}
}

View File

@@ -1,63 +0,0 @@
package forge.adventure.editor;
import javax.swing.*;
import java.awt.*;
public class FormPanel extends JPanel {
int row=0;
static final int MAXIMUM_LINES=300;
public FormPanel()
{
setLayout(new GridBagLayout()) ;
GridBagConstraints constraint=new GridBagConstraints();
constraint.weightx = 1.0;
constraint.weighty = 1.0;
constraint.gridy=MAXIMUM_LINES;
constraint.gridx=0;
constraint.gridwidth=2;
add(Box.createVerticalGlue(),constraint);
row++;
}
public void add(JComponent name,JComponent element)
{
GridBagConstraints constraint=new GridBagConstraints();
constraint.ipadx = 5;
constraint.ipady = 5;
constraint.weightx = 1.0;
constraint.weighty = 0.0;
constraint.gridy=row;
constraint.gridx=0;
constraint.anchor=GridBagConstraints.NORTHWEST;
add(name,constraint);
constraint.gridy=row;
constraint.gridx=1;
constraint.fill=GridBagConstraints.HORIZONTAL;
constraint.anchor=GridBagConstraints.NORTHEAST;
add(element,constraint);
row++;
}
public void add(String name,JComponent element)
{
add(new JLabel(name),element);
}
public void add(JComponent element)
{
GridBagConstraints constraint=new GridBagConstraints();
constraint.ipadx = 5;
constraint.ipady = 5;
constraint.weightx = 1.0;
constraint.weighty = 0.0;
constraint.gridy=row;
constraint.gridx=0;
constraint.gridwidth=2;
constraint.fill=GridBagConstraints.HORIZONTAL;
constraint.anchor=GridBagConstraints.NORTHEAST;
add(element,constraint);
row++;
}
}

View File

@@ -1,20 +0,0 @@
package forge.adventure.editor;
import javax.swing.*;
public class IntSpinner extends JSpinner {
public IntSpinner()
{
this( 0, 100, 1);
}
public IntSpinner(int min,int max,int stepSize)
{
super(new SpinnerNumberModel(new Integer(0), new Integer(min), new Integer (max), new Integer(stepSize)));
}
public int intValue()
{
return (Integer) getValue();
}
}

View File

@@ -1,86 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.ItemData;
import javax.swing.*;
import java.awt.*;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class ItemEdit extends JComponent {
ItemData currentData;
JTextField nameField=new JTextField();
JTextField equipmentSlot=new JTextField();
JTextField iconName=new JTextField();
EffectEditor effect=new EffectEditor(false);
JTextField description=new JTextField();
JCheckBox questItem=new JCheckBox();
JSpinner cost= new JSpinner(new SpinnerNumberModel(0, 0, 100000, 1));
private boolean updating=false;
public ItemEdit()
{
setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
FormPanel parameters=new FormPanel();
parameters.setBorder(BorderFactory.createTitledBorder("Parameter"));
parameters.add("Name:",nameField);
parameters.add("equipmentSlot:",equipmentSlot);
parameters.add("description:",description);
parameters.add("iconName",iconName);
parameters.add("questItem",questItem);
parameters.add("cost",cost);
add(parameters);
add(effect);
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));
equipmentSlot.getDocument().addDocumentListener(new DocumentChangeListener(ItemEdit.this::updateItem));
description.getDocument().addDocumentListener(new DocumentChangeListener(ItemEdit.this::updateItem));
iconName.getDocument().addDocumentListener(new DocumentChangeListener(ItemEdit.this::updateItem));
cost.addChangeListener(e -> ItemEdit.this.updateItem());
questItem.addChangeListener(e -> ItemEdit.this.updateItem());
effect.addChangeListener(e -> ItemEdit.this.updateItem());
refresh();
}
private void updateItem() {
if(currentData==null||updating)
return;
currentData.name=nameField.getText();
currentData.equipmentSlot= equipmentSlot.getText();
currentData.effect= effect.getCurrentEffect();
currentData.description=description.getText();
currentData.iconName=iconName.getText();
currentData.questItem=questItem.isSelected();
currentData.cost= (Integer) cost.getValue();
}
public void setCurrentItem(ItemData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
nameField.setText(currentData.name);
effect.setCurrentEffect(currentData.effect);
equipmentSlot.setText(currentData.equipmentSlot);
description.setText(currentData.description);
iconName.setText(currentData.iconName);
questItem.setSelected(currentData.questItem);
cost.setValue(currentData.cost);
updating=false;
}
}

View File

@@ -1,127 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.ItemData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
public class ItemsEditor extends JComponent {
DefaultListModel<ItemData> model = new DefaultListModel<>();
JList<ItemData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
ItemEdit edit=new ItemEdit();
static SwingAtlas itemAtlas;
public class ItemDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof ItemData item))
return label;
// Get the renderer component from parent class
label.setText(item.name);
if(itemAtlas==null)
itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS));
if(itemAtlas.has(item.iconName))
label.setIcon(itemAtlas.get(item.iconName));
else
{
ImageIcon img=itemAtlas.getAny();
if(img!=null)
label.setIcon(img);
}
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public ItemsEditor()
{
list.setCellRenderer(new ItemsEditor.ItemDataRenderer());
list.addListSelectionListener(e -> ItemsEditor.this.updateEdit());
addButton("add", e -> ItemsEditor.this.addItem());
addButton("remove", e -> ItemsEditor.this.remove());
addButton("copy", e -> ItemsEditor.this.copy());
addButton("load", e -> ItemsEditor.this.load());
addButton("save", e -> ItemsEditor.this.save());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(new JScrollPane(list), BorderLayout.LINE_START);
toolBar.setFloatable(false);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
load();
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
ItemData data=new ItemData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentItem(model.get(selected));
}
void save()
{
Array<ItemData> allEnemies=new Array<>();
for(int i=0;i<model.getSize();i++)
allEnemies.add(model.get(i));
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.ITEMS);
handle.writeString(json.prettyPrint(json.toJson(allEnemies,Array.class, ItemData.class)),false);
}
void load()
{
model.clear();
Array<ItemData> allEnemies=new Array<>();
Json json = new Json();
FileHandle handle = Config.instance().getFile(Paths.ITEMS);
if (handle.exists())
{
allEnemies =json.fromJson(Array.class, ItemData.class, handle);
}
for (int i=0;i<allEnemies.size;i++) {
model.add(i,allEnemies.get(i));
}
}
void addItem()
{
ItemData data=new ItemData();
data.name="Item "+model.getSize();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
}

View File

@@ -1,274 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.PointOfInterestData;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Enumeration;
public class PointOfInterestEdit extends JComponent {
PointOfInterestData currentData;
JTextField name = new JTextField();
JTextField type = new JTextField();
JSpinner count = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
FilePicker spriteAtlas = new FilePicker(new String[]{"atlas"});
JTextField sprite = new JTextField();
FilePicker map = new FilePicker(new String[]{"tmx"});
JSpinner radiusFactor= new JSpinner(new SpinnerNumberModel(0.0f, 0.0f, 2.0f, 0.1f));
SwingAtlasPreview preview=new SwingAtlasPreview(256,2000);
JTextField manualEntry = new JTextField(20);
DefaultListModel<String> existingModel = new DefaultListModel<>();
DefaultListModel<String> POIModel = new DefaultListModel<>();
JList<String> existingTags;
JList<String> POITags;
private boolean updating=false;
public PointOfInterestEdit()
{
JTabbedPane tabs = new JTabbedPane();
add(tabs, BorderLayout.CENTER);
setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
FormPanel parameters=new FormPanel();
//parameters.setLayout(new BoxLayout(parameters, BoxLayout.Y_AXIS));
parameters.setBorder(BorderFactory.createTitledBorder("Parameter"));
JPanel tags = new JPanel();
tabs.addTab("Basic Info", parameters);
tabs.addTab("Quest Tags", tags);
parameters.add("Name:",name);
parameters.add("Type:",type);
parameters.add("Count:",count);
parameters.add("Sprite atlas:",spriteAtlas);
parameters.add("Sprite:",sprite);
parameters.add("Map:",map);
parameters.add("Radius factor:",radiusFactor);
parameters.add(preview);
name.getDocument().addDocumentListener(new DocumentChangeListener(PointOfInterestEdit.this::updateItem));
type.getDocument().addDocumentListener(new DocumentChangeListener(PointOfInterestEdit.this::updateItem));
count.addChangeListener(e -> PointOfInterestEdit.this.updateItem());
spriteAtlas.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(PointOfInterestEdit.this::updateItem));
sprite.getDocument().addDocumentListener(new DocumentChangeListener(PointOfInterestEdit.this::updateItem));
map.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(PointOfInterestEdit.this::updateItem));
radiusFactor.addChangeListener(e -> PointOfInterestEdit.this.updateItem());
existingTags = new JList<>();
existingTags.getInputMap(JList.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke("ENTER"), "addSelected");
existingTags.getActionMap().put("addSelected", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
int index = existingTags.getSelectedIndex();
String selectedItem = existingTags.getSelectedValue();
if (selectedItem != null) {
POIModel.addElement(selectedItem);
existingTags.grabFocus();
existingTags.setSelectedIndex(index<existingModel.size()?index:index-1);
}
}
});
existingModel = QuestController.getInstance().getPOITags();
existingTags.setModel(existingModel);
POITags = new JList<>();
POIModel = new DefaultListModel<>();
POITags.setModel(POIModel);
POITags.getModel().addListDataListener(new ListDataListener() {
@Override
public void intervalAdded(ListDataEvent e) {
updateItem();
}
@Override
public void intervalRemoved(ListDataEvent e) {
updateItem();
}
@Override
public void contentsChanged(ListDataEvent e) {
updateItem();
}
});
JButton select = new JButton("Select");
select.addActionListener(q -> addSelected());
JButton add = new JButton("Manual Add");
add.addActionListener(q -> manualAdd(POIModel));
JButton remove = new JButton("Remove Item");
remove.addActionListener(q -> removeSelected());
tags.setLayout(new BorderLayout());
JPanel left = new JPanel();
left.setLayout(new BorderLayout());
left.add(new JLabel("Tags already in use"), BorderLayout.NORTH);
JScrollPane listScroller = new JScrollPane(existingTags);
listScroller.setMinimumSize(new Dimension(400, 800));
left.add(listScroller, BorderLayout.CENTER);
tags.add(left, BorderLayout.WEST);
FormPanel tagEdit = new FormPanel();
tagEdit.setLayout(new BorderLayout());
FormPanel mappedTags = new FormPanel();
mappedTags.setLayout(new BorderLayout());
mappedTags.add(new JLabel("Tags Mapped to this object"), BorderLayout.NORTH);
JScrollPane listScroller2 = new JScrollPane(POITags);
listScroller2.setMinimumSize(new Dimension(400, 800));
mappedTags.add(listScroller2, BorderLayout.CENTER);
tagEdit.add(mappedTags,BorderLayout.EAST);
JPanel controlPanel = new JPanel();
controlPanel.add(select);
controlPanel.add(add);
controlPanel.add(manualEntry);
manualEntry.getInputMap(JList.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke("ENTER"), "addTyped");
manualEntry.getActionMap().put("addTyped", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (!manualEntry.getText().trim().isEmpty()) {
manualAdd(POIModel);
manualEntry.grabFocus();
}
}
});
controlPanel.add(remove);
tagEdit.add(controlPanel, BorderLayout.CENTER);
tags.add(tagEdit,BorderLayout.CENTER);
JTextArea right1 = new JTextArea("This is really just to pad some space\n" +
"but also to explain the use of tags.\n" +
"Rather than adding 100's of object names\n" +
"to every quest definition, instead we will\n"+
"categorize enemies and points of interest with\n"+
"tags and reference those categories in quests");
right1.setEnabled(false);
tags.add(right1, BorderLayout.EAST);
refresh();
}
private void updateItem() {
if(currentData==null||updating)
return;
currentData.name=name.getText();
currentData.type= type.getText();
currentData.count= (Integer) count.getValue();
currentData.spriteAtlas=spriteAtlas.getEdit().getText();
currentData.sprite=sprite.getText();
currentData.map=map.getEdit().getText();
currentData.radiusFactor= (Float) radiusFactor.getValue();
ArrayList<String> tags = new ArrayList<>();
for (Enumeration<String> e = POIModel.elements(); e.hasMoreElements();){
tags.add(e.nextElement());
}
currentData.questTags = tags.toArray(currentData.questTags);
QuestController.getInstance().refresh();
filterExisting(POIModel);
}
public void setCurrent(PointOfInterestData data)
{
currentData=data;
refresh();
}
private void addSelected(){
if (existingTags.getSelectedIndex()>-1)
POIModel.addElement(existingTags.getModel().getElementAt(existingTags.getSelectedIndex()));
updateItem();
}
private void removeSelected(){
if (POITags.getSelectedIndex()>-1)
POIModel.remove(POITags.getSelectedIndex());
updateItem();
}
private void filterExisting(DefaultListModel<String> filter){
DefaultListModel<String> toReturn = new DefaultListModel<>();
for (Enumeration<String> e = QuestController.getInstance().getPOITags().elements(); e.hasMoreElements();){
String toTest = e.nextElement();
if (toTest != null & !filter.contains(toTest)){
toReturn.addElement(toTest);
}
}
existingTags.setModel(toReturn);
}
private void manualAdd(DefaultListModel<String> model){
if (!manualEntry.getText().trim().isEmpty())
model.addElement(manualEntry.getText().trim());
manualEntry.setText("");
updateItem();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
name.setText(currentData.name);
type.setText(currentData.type);
count.setValue(currentData.count);
spriteAtlas.getEdit().setText(currentData.spriteAtlas);
sprite.setText(currentData.sprite);
map.getEdit().setText(currentData.map);
radiusFactor.setValue(currentData.radiusFactor);
preview.setSpritePath(currentData.spriteAtlas,currentData.sprite);
POIModel.clear();
for(String val : currentData.questTags) {
if (val != null)
POIModel.addElement(val);
}
filterExisting(POIModel);
updating=false;
}
}

View File

@@ -1,134 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.PointOfInterestData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.HashMap;
public class PointOfInterestEditor extends JComponent {
DefaultListModel<PointOfInterestData> model = new DefaultListModel<>();
JList<PointOfInterestData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
PointOfInterestEdit edit=new PointOfInterestEdit();
static HashMap<String,SwingAtlas> atlas=new HashMap<>();
public static class PointOfInterestRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof PointOfInterestData))
return label;
PointOfInterestData poi=(PointOfInterestData) value;
// Get the renderer component from parent class
label.setText(poi.name);
if(!atlas.containsKey(poi.spriteAtlas))
atlas.put(poi.spriteAtlas,new SwingAtlas(Config.instance().getFile(poi.spriteAtlas)));
SwingAtlas poiAtlas = atlas.get(poi.spriteAtlas);
if(poiAtlas.has(poi.sprite))
label.setIcon(poiAtlas.get(poi.sprite));
else
{
ImageIcon img=poiAtlas.getAny();
if(img!=null)
label.setIcon(img);
}
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public PointOfInterestEditor()
{
list.setCellRenderer(new PointOfInterestEditor.PointOfInterestRenderer());
list.addListSelectionListener(e -> PointOfInterestEditor.this.updateEdit());
addButton("add", e -> PointOfInterestEditor.this.addItem());
addButton("remove", e -> PointOfInterestEditor.this.remove());
addButton("copy", e -> PointOfInterestEditor.this.copy());
addButton("load", e -> PointOfInterestEditor.this.load());
addButton("save", e -> PointOfInterestEditor.this.save());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(new JScrollPane(list), BorderLayout.LINE_START);
toolBar.setFloatable(false);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
load();
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
PointOfInterestData data=new PointOfInterestData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrent(model.get(selected));
}
void save()
{
Array<PointOfInterestData> allEnemies=new Array<>();
for(int i=0;i<model.getSize();i++)
allEnemies.add(model.get(i));
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.POINTS_OF_INTEREST);
handle.writeString(json.prettyPrint(json.toJson(allEnemies,Array.class, PointOfInterestData.class)),false);
}
void load()
{
model.clear();
Array<PointOfInterestData> allEnemies=new Array<>();
Json json = new Json();
FileHandle handle = Config.instance().getFile(Paths.POINTS_OF_INTEREST);
if (handle.exists())
{
allEnemies =json.fromJson(Array.class, PointOfInterestData.class, handle);
}
for (int i=0;i<allEnemies.size;i++) {
model.add(i,allEnemies.get(i));
}
QuestController.getInstance().load();
}
void addItem()
{
PointOfInterestData data=new PointOfInterestData();
data.name="PoI "+model.getSize();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
}

View File

@@ -1,236 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.AdventureQuestData;
import forge.adventure.data.EnemyData;
import forge.adventure.data.PointOfInterestData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class QuestController {
public final DefaultListModel<String> POITags = new DefaultListModel<>();
public final DefaultListModel<String> enemyTags = new DefaultListModel<>();
public final DefaultListModel<String> questEnemyTags = new DefaultListModel<>();
public final DefaultListModel<String> questTags = new DefaultListModel<>();
public final DefaultListModel<String> questPOITags = new DefaultListModel<>();
private final DefaultListModel<PointOfInterestData> allPOI = new DefaultListModel<>();
private final DefaultListModel<EnemyData> allEnemies = new DefaultListModel<>();
private final DefaultListModel<String> questSourceTags = new DefaultListModel<>();
private final DefaultListModel<AdventureQuestData> allQuests = new DefaultListModel<>();
private static QuestController instance;
public static QuestController getInstance() {
if (instance == null)
instance = new QuestController();
return instance;
}
public DefaultListModel<AdventureQuestData> getAllQuests() {
return allQuests;
}
private QuestController(){
load();
}
public DefaultListModel<String> getEnemyTags(){
DefaultListModel<String> toReturn = new DefaultListModel<>();
for (int i = 0; i < enemyTags.size(); i++){
toReturn.removeElement(enemyTags.get(i));
toReturn.addElement(enemyTags.get(i));
}
List<Object> sortedObjects = Arrays.stream(toReturn.toArray()).sorted().collect(Collectors.toList());
toReturn.clear();
for (Object sortedObject : sortedObjects) {
toReturn.addElement((String) sortedObject);
}
return toReturn;
}
public DefaultListModel<String> getPOITags(){
DefaultListModel<String> toReturn = new DefaultListModel<>();
for (int i = 0; i < POITags.size(); i++){
toReturn.removeElement(POITags.get(i));
toReturn.addElement(POITags.get(i));
}
List<Object> sortedObjects = Arrays.stream(toReturn.toArray()).sorted().collect(Collectors.toList());
toReturn.clear();
for (Object sortedObject : sortedObjects) {
toReturn.addElement((String) sortedObject);
}
return toReturn;
}
public DefaultListModel<String> getSourceTags(){
DefaultListModel<String> toReturn = new DefaultListModel<>();
for (int i = 0; i < questSourceTags.size(); i++)
{
toReturn.removeElement(questSourceTags.get(i));
toReturn.addElement(questSourceTags.get(i));
}
List<Object> sortedObjects = Arrays.stream(toReturn.toArray()).sorted().collect(Collectors.toList());
toReturn.clear();
for (Object sortedObject : sortedObjects) {
toReturn.addElement((String) sortedObject);
}
return toReturn;
}
public void refresh(){
enemyTags.clear();
POITags.clear();
questPOITags.clear();
questEnemyTags.clear();
questTags.clear();
questSourceTags.clear();
for (int i=0;i<allEnemies.size();i++) {
for (String tag : allEnemies.get(i).questTags)
{
enemyTags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
enemyTags.addElement(tag);
}
}
for (int i=0;i<allPOI.size();i++) {
for (String tag : allPOI.get(i).questTags)
{
POITags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
POITags.addElement(tag);
}
}
for (int i=0;i<allQuests.size();i++) {
for (String tag : allQuests.get(i).questEnemyTags)
{
questEnemyTags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
questEnemyTags.addElement(tag);
}
for (String tag : allQuests.get(i).questPOITags)
{
questPOITags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
questPOITags.addElement(tag);
}
for (String tag : allQuests.get(i).questSourceTags)
{
questSourceTags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
questSourceTags.addElement(tag);
}
}
}
public void load()
{
allEnemies.clear();
Array<EnemyData> enemyJSON=new Array<>();
Json json = new Json();
FileHandle handle = Config.instance().getFile(Paths.ENEMIES);
if (handle.exists())
{
enemyJSON = json.fromJson(Array.class, EnemyData.class, handle);
}
for (int i=0;i<enemyJSON.size;i++) {
allEnemies.add(i,enemyJSON.get(i));
for (String tag : enemyJSON.get(i).questTags)
{
enemyTags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
enemyTags.addElement(tag);
}
}
allPOI.clear();
Array<PointOfInterestData> POIJSON=new Array<>();
json = new Json();
handle = Config.instance().getFile(Paths.POINTS_OF_INTEREST);
if (handle.exists())
{
POIJSON = json.fromJson(Array.class, PointOfInterestData.class, handle);
}
for (int i=0;i<POIJSON.size;i++) {
allPOI.add(i,POIJSON.get(i));
for (String tag : POIJSON.get(i).questTags)
{
POITags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
POITags.addElement(tag);
}
}
allQuests.clear();
Array<AdventureQuestData> questJSON=new Array<>();
json = new Json();
handle = Config.instance().getFile(Paths.QUESTS);
if (handle.exists())
{
questJSON = json.fromJson(Array.class, AdventureQuestData.class, handle);
}
for (int i=0;i<questJSON.size;i++) {
AdventureQuestData template = questJSON.get(i);
template.isTemplate = true;
allQuests.add(i,template);
for (String tag : template.questEnemyTags)
{
questEnemyTags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
questEnemyTags.addElement(tag);
}
for (String tag : template.questPOITags)
{
questPOITags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
questPOITags.addElement(tag);
}
for (String tag : template.questSourceTags)
{
questSourceTags.removeElement(tag); //Ensure uniqueness
if (tag!= null)
questSourceTags.addElement(tag);
}
}
}
void save()
{
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.QUESTS);
AdventureQuestData[] saveData = Arrays.stream(allQuests.toArray()).map(AdventureQuestData.class::cast).toArray(AdventureQuestData[]::new);
handle.writeString(json.prettyPrint(json.toJson(saveData,Array.class, AdventureQuestData.class)),false);
}
}

View File

@@ -1,387 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.AdventureQuestData;
import javax.swing.AbstractAction;
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.ListDataListener;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Enumeration;
public class QuestEdit extends FormPanel {
AdventureQuestData currentData;
//public JSpinner spawnWeight= new JSpinner(new SpinnerNumberModel(0.0f, 0.f, 1f, 0.1f));
public JLabel id = new JLabel();
public JTextField name=new JTextField();
public JTextField description=new JTextField();
public JTextField synopsis=new JTextField();
public JCheckBox storyQuest = new JCheckBox();
public JTextField rewardDescription=new JTextField();
public QuestStageEditor stages =new QuestStageEditor();
JTabbedPane tabs =new JTabbedPane();
public DialogEditor prologueEditor =new DialogEditor();
public DialogEditor epilogueEditor =new DialogEditor();
public DialogEditor offerEditor = new DialogEditor();
public DialogEditor failureEditor = new DialogEditor();
public DialogEditor declineEditor = new DialogEditor();
private boolean updating=false;
JTextField manualEntry = new JTextField(20);
DefaultListModel<String> existingModel = new DefaultListModel<>();
DefaultListModel<String> selectedTagModel = new DefaultListModel<>();
JList<String> existingTags;
JList<String> selectedTags;
JPanel tags = new JPanel();
public QuestEdit()
{
FormPanel center=new FormPanel() { };
center.add("Quest ID:", id);
center.add("Name:",name);
//center.add("Synopsis (dev mode):",synopsis);
center.add("Description:",description);
center.add("Reward Description:",rewardDescription);
center.add("Storyline Quest", storyQuest);
add(center);
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
add(tabs);
tabs.add("Quest Stages", getStagesTab());
tabs.add("Offer Dialog",getOfferTab());
tabs.add("Prologue",getPrologueTab());
tabs.add("Epilogue",getEpilogueTab());
tabs.add("Failure Dialog", getFailureTab());
tabs.add("Decline Dialog",getDeclineTab());
tabs.add("Quest Sources", getSourcesTab());
existingTags = new JList<>();
existingTags.getInputMap(JList.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke("ENTER"), "addSelected");
existingTags.getActionMap().put("addSelected", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
addSelected();
int index = existingTags.getSelectedIndex();
String selectedItem = existingTags.getSelectedValue();
if (selectedItem != null) {
selectedTagModel.addElement(selectedItem);
existingTags.grabFocus();
existingTags.setSelectedIndex(index<existingModel.size()?index:index-1);
}
}
});
existingModel = QuestController.getInstance().getSourceTags();
existingTags.setModel(existingModel);
selectedTags = new JList<>();
selectedTagModel = new DefaultListModel<>();
selectedTags.setModel(selectedTagModel);
selectedTags.getModel().addListDataListener(new ListDataListener() {
@Override
public void intervalAdded(ListDataEvent e) {
doUpdate();
}
@Override
public void intervalRemoved(ListDataEvent e) {
doUpdate();
}
@Override
public void contentsChanged(ListDataEvent e) {
doUpdate();
}
});
JButton select = new JButton("Select");
select.addActionListener(q -> addSelected());
JButton add = new JButton("Manual Add");
add.addActionListener(q -> manualAdd(selectedTagModel));
JButton remove = new JButton("Remove Item");
remove.addActionListener(q -> removeSelected());
tags.setLayout(new BorderLayout());
JPanel left = new JPanel();
left.setLayout(new BorderLayout());
left.add(new JLabel("Tags already in use"), BorderLayout.NORTH);
JScrollPane listScroller = new JScrollPane(existingTags);
listScroller.setMinimumSize(new Dimension(400, 800));
left.add(listScroller, BorderLayout.CENTER);
tags.add(left, BorderLayout.WEST);
FormPanel tagEdit = new FormPanel();
tagEdit.setLayout(new BorderLayout());
FormPanel mappedTags = new FormPanel();
mappedTags.setLayout(new BorderLayout());
mappedTags.add(new JLabel("Tags Mapped to this object"), BorderLayout.NORTH);
JScrollPane listScroller2 = new JScrollPane(selectedTags);
listScroller2.setMinimumSize(new Dimension(400, 800));
mappedTags.add(listScroller2, BorderLayout.CENTER);
tagEdit.add(mappedTags,BorderLayout.EAST);
JPanel controlPanel = new JPanel();
controlPanel.add(select);
controlPanel.add(add);
controlPanel.add(manualEntry);
manualEntry.getInputMap(JList.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke("ENTER"), "addTyped");
manualEntry.getActionMap().put("addTyped", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (!manualEntry.getText().trim().isEmpty()) {
manualAdd(selectedTagModel);
manualEntry.grabFocus();
}
}
});
controlPanel.add(remove);
tagEdit.add(controlPanel, BorderLayout.CENTER);
tags.add(tagEdit,BorderLayout.CENTER);
JTextArea right1 = new JTextArea("This is really just to pad some space\n" +
"but also to explain the use of tags.\n" +
"Rather than adding 100's of object names\n" +
"to every quest definition, instead we will\n"+
"categorize enemies and points of interest with\n"+
"tags and reference those categories in quests");
right1.setEnabled(false);
tags.add(right1, BorderLayout.EAST);
name.getDocument().addDocumentListener(new DocumentChangeListener(QuestEdit.this::updateQuest));
description.getDocument().addDocumentListener(new DocumentChangeListener(QuestEdit.this::updateQuest));
synopsis.getDocument().addDocumentListener(new DocumentChangeListener(QuestEdit.this::updateQuest));
storyQuest.getModel().addChangeListener(q -> QuestEdit.this.updateQuest());
rewardDescription.getDocument().addDocumentListener(new DocumentChangeListener(QuestEdit.this::updateQuest));
stages.addChangeListener(e -> QuestEdit.this.updateQuest());
offerEditor.addChangeListener(e -> QuestEdit.this.updateQuest());
prologueEditor.addChangeListener(e -> QuestEdit.this.updateQuest());
epilogueEditor.addChangeListener(e -> QuestEdit.this.updateQuest());
failureEditor.addChangeListener(e -> QuestEdit.this.updateQuest());
declineEditor.addChangeListener(e -> QuestEdit.this.updateQuest());
stages.addChangeListener(e -> QuestEdit.this.updateQuest());
selectedTagModel.addListDataListener(new ListDataListener() {
@Override
public void intervalAdded(ListDataEvent e) {
QuestEdit.this.updateQuest();
}
@Override
public void intervalRemoved(ListDataEvent e) {
QuestEdit.this.updateQuest();
}
@Override
public void contentsChanged(ListDataEvent e) {
QuestEdit.this.updateQuest();
}
});
refresh();
}
private void doUpdate(){
QuestEdit.this.updateQuest();
}
private void addSelected(){
if (existingTags.getSelectedIndex()>-1)
selectedTagModel.addElement(existingTags.getModel().getElementAt(existingTags.getSelectedIndex()));
doUpdate();
}
private void removeSelected(){
if (selectedTags.getSelectedIndex()>-1)
selectedTagModel.remove(selectedTags.getSelectedIndex());
doUpdate();
}
private void filterExisting(DefaultListModel<String> filter){
DefaultListModel<String> toReturn = new DefaultListModel<>();
for (Enumeration<String> e = QuestController.getInstance().getSourceTags().elements(); e.hasMoreElements();){
String toTest = e.nextElement();
if (toTest != null & !filter.contains(toTest)){
toReturn.addElement(toTest);
}
}
existingTags.setModel(toReturn);
}
private void manualAdd(DefaultListModel<String> model){
if (!manualEntry.getText().trim().isEmpty())
model.addElement(manualEntry.getText().trim());
manualEntry.setText("");
doUpdate();
}
protected void updateQuest() {
if(currentData==null||updating)
return;
currentData.name = name.getText();
currentData.storyQuest = storyQuest.isSelected();
currentData.synopsis = synopsis.getText();
currentData.description = description.getText();
currentData.rewardDescription = rewardDescription.getText();
currentData.stages = stages.getStages();
currentData.offerDialog = offerEditor.getDialogData();
currentData.prologue = prologueEditor.getDialogData();
currentData.epilogue = epilogueEditor.getDialogData();
currentData.failureDialog = failureEditor.getDialogData();
currentData.declinedDialog = declineEditor.getDialogData();
ArrayList<String> tags = new ArrayList<>();
for (Enumeration<String> e = selectedTagModel.elements(); e.hasMoreElements();){
tags.add(e.nextElement());
}
currentData.questSourceTags = tags.toArray(currentData.questSourceTags);
QuestController.getInstance().refresh();
filterExisting(selectedTagModel);
}
public void setCurrentQuest(AdventureQuestData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
setVisible(true);
updating=true;
id.setText(String.valueOf(currentData.getID()));
name.setText(currentData.name);
description.setText(currentData.description);
synopsis.setText(currentData.synopsis);
storyQuest.getModel().setSelected(currentData.storyQuest);
rewardDescription.setText(currentData.rewardDescription);
stages.setStages(currentData);
offerEditor.loadData(currentData.offerDialog);
prologueEditor.loadData(currentData.prologue);
epilogueEditor.loadData(currentData.epilogue);
failureEditor.loadData(currentData.failureDialog);
declineEditor.loadData(currentData.declinedDialog);
selectedTagModel.clear();
for(String val : currentData.questSourceTags) {
if (val != null)
selectedTagModel.addElement(val);
}
filterExisting(selectedTagModel);
updating=false;
}
public JPanel getOfferTab(){
JPanel offerTab = new JPanel();
offerTab.setLayout(new BoxLayout(offerTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(offerEditor);
offerTab.add(center);
return offerTab;
}
public JPanel getPrologueTab(){
JPanel prologueTab = new JPanel();
prologueTab.setLayout(new BoxLayout(prologueTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(prologueEditor);
prologueTab.add(center);
return prologueTab;
}
public JPanel getEpilogueTab(){
JPanel epilogueTab = new JPanel();
epilogueTab.setLayout(new BoxLayout(epilogueTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(epilogueEditor);
epilogueTab.add(center);
return epilogueTab;
}
public JPanel getFailureTab(){
JPanel failureTab = new JPanel();
failureTab.setLayout(new BoxLayout(failureTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(failureEditor);
failureTab.add(center);
return failureTab;
}
public JPanel getDeclineTab(){
JPanel declineTab = new JPanel();
declineTab.setLayout(new BoxLayout(declineTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(declineEditor);
declineTab.add(center);
return declineTab;
}
public JPanel getStagesTab(){
JPanel stagesTab = new JPanel();
stagesTab.setLayout(new BoxLayout(stagesTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(stages);
stagesTab.add(center);
return stagesTab;
}
public JPanel getSourcesTab(){
JPanel sourcesTab = new JPanel();
sourcesTab.setLayout(new BoxLayout(sourcesTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(tags);
sourcesTab.add(center);
return sourcesTab;
}
}

View File

@@ -1,103 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.AdventureQuestData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class QuestEditor extends JComponent {
JList<AdventureQuestData> list = new JList<>(QuestController.getInstance().getAllQuests());
JToolBar toolBar = new JToolBar("toolbar");
QuestEdit edit=new QuestEdit();
public class QuestDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestData quest))
return label;
// Get the renderer component from parent class
label.setText(quest.name);
return label;
}
}
public void addButton(String name,ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public QuestEditor()
{
list.setCellRenderer(new QuestDataRenderer());
list.addListSelectionListener(e -> QuestEditor.this.updateEdit());
addButton("Add Quest", e -> QuestEditor.this.addStage());
addButton("Remove", e -> QuestEditor.this.remove());
addButton("Copy", e -> QuestEditor.this.copy());
addButton("Load", e -> QuestController.getInstance().load());
addButton("Save", e -> QuestEditor.this.save());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(new JScrollPane(list), BorderLayout.LINE_START);
toolBar.setFloatable(false);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
edit.setVisible(false);
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
AdventureQuestData data=new AdventureQuestData(QuestController.getInstance().getAllQuests().get(selected));
data.isTemplate = true;
QuestController.getInstance().getAllQuests().add(QuestController.getInstance().getAllQuests().size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentQuest(QuestController.getInstance().getAllQuests().get(selected));
}
void save()
{
Array<AdventureQuestData> allQuests=new Array<>();
for(int i=0;i<QuestController.getInstance().getAllQuests().getSize();i++) {
allQuests.add(QuestController.getInstance().getAllQuests().get(i));
}
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.QUESTS);
handle.writeString(json.prettyPrint(json.toJson(allQuests,Array.class, AdventureQuestData.class)),false);
QuestController.getInstance().save();
}
void addStage()
{
AdventureQuestData data=new AdventureQuestData();
data.name="New Quest "+QuestController.getInstance().getAllQuests().getSize();
data.isTemplate = true;
QuestController.getInstance().getAllQuests().add(QuestController.getInstance().getAllQuests().size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
QuestController.getInstance().getAllQuests().remove(selected);
edit.setVisible(false);
}
}

View File

@@ -1,779 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.*;
import forge.adventure.util.AdventureQuestController;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class QuestStageEdit extends FormPanel {
private boolean updating=false;
AdventureQuestStage currentData;
AdventureQuestData currentQuestData;
public JTextField name=new JTextField("", 25);
public JTextField description=new JTextField("", 25);
public TextListEdit itemNames =new TextListEdit();
public TextListEdit spriteNames =new TextListEdit();
public TextListEdit equipNames =new TextListEdit();
public TextListEdit prerequisites =new TextListEdit(currentQuestData!=null?(String[])Arrays.stream(currentQuestData.stages).filter(q -> !q.equals(currentData)).toArray():new String[]{}); //May not be the right way to do this, will come back to it.
JTabbedPane tabs =new JTabbedPane();
DialogEditor prologueEditor = new DialogEditor();
DialogEditor epilogueEditor = new DialogEditor();
public QuestStageEdit()
{
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
add(getInfoTab());
add(tabs);
tabs.add("Objective", getObjectiveTab());
tabs.add("Prologue",getPrologueTab());
tabs.add("Epilogue",getEpilogueTab());
tabs.add("Prerequisites",getPrereqTab());
//temp
nyi.setForeground(Color.red);
//
addListeners();
}
public JPanel getInfoTab(){
JPanel infoTab = new JPanel();
FormPanel center=new FormPanel();
center.add("name:",name);
center.add("description:",description);
name.setSize(400, name.getHeight());
description.setSize(400, description.getHeight());
infoTab.add(center);
name.getDocument().addDocumentListener(new DocumentChangeListener(QuestStageEdit.this::updateStage));
description.getDocument().addDocumentListener(new DocumentChangeListener(QuestStageEdit.this::updateStage));
prerequisites.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(QuestStageEdit.this::updateStage));
return infoTab;
}
public JPanel getPrologueTab(){
JPanel prologueTab = new JPanel();
prologueTab.setLayout(new BoxLayout(prologueTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(prologueEditor);
prologueTab.add(center);
return prologueTab;
}
public JPanel getEpilogueTab(){
JPanel epilogueTab = new JPanel();
epilogueTab.setLayout(new BoxLayout(epilogueTab, BoxLayout.Y_AXIS));
FormPanel center = new FormPanel();
center.add(epilogueEditor);
epilogueTab.add(center);
return epilogueTab;
}
private JComboBox<AdventureQuestController.ObjectiveTypes> objectiveType;
private final JLabel nyi = new JLabel("Not yet implemented");
private final JTextField deliveryItem=new JTextField(25);
private final JTextField mapFlag = new JTextField(25);
private Box mapFlagGroup;
private final JSpinner flagSpinner = new JSpinner(new SpinnerNumberModel(1, 1, 1000, 1));
private Box flagValueGroup;
private final JRadioButton anyPOI = new JRadioButton("Any Point of Interest matching tags is valid");
private final JRadioButton specificPOI = new JRadioButton("Only a specific Point of Interest is valid");
private final ButtonGroup anyPOIGroup = new ButtonGroup();
private final JCheckBox here = new JCheckBox("Use current map instead of selecting tags");
private final JCheckBox mixedEnemies = new JCheckBox("Mixture of enemy types matching");
private final JLabel count1Description = new JLabel("(Count 1 description)");
private final JLabel count2Description = new JLabel("(Count 2 description)");
private final JLabel count3Description = new JLabel("(Count 3 description)");
private final JLabel count4Description = new JLabel("(Count 4 description)");
private final JSpinner count1Spinner = new JSpinner(new SpinnerNumberModel(1, 0, 100, 1));
private final JSpinner count2Spinner = new JSpinner(new SpinnerNumberModel(1, 0, 100, 1));
private final JSpinner count3Spinner = new JSpinner(new SpinnerNumberModel(1, 0, 100, 1));
private final JSpinner count4Spinner = new JSpinner(new SpinnerNumberModel(1, 0, 100, 1));
private final JLabel arenaLabel = new JLabel("Enter the arena and prove your worth. (Note: Please be sure the PoIs selected have an arena)");
private final JLabel characterFlagLabel = new JLabel("Have the selected character flag set to a given value. (Caution: You're probably looking for QuestFlag or MapFlag instead)");
private final JLabel clearLabel = new JLabel("Clear all enemies from the target area.");
private final JLabel completeQuestLabel = new JLabel("Complete other quests issued by target POI. Primarily used for busy work as a part of storyline quests.");
private final JLabel defeatLabel = new JLabel("Defeat a number of enemies of the indicated type.");
private final JLabel deliveryLabel = new JLabel("Travel to the given destination to deliver an item (not tracked in inventory).");
private final JLabel escortLabel = new JLabel("Protect your target as they travel to their destination.");
private final JLabel eventFinishLabel = new JLabel("Finish (win or lose) tavern events");
private final JLabel eventWinLabel = new JLabel("Finish and win tavern events");
private final JLabel eventWinMatchLabel = new JLabel("Win individual matches in tavern events");
private final JLabel fetchLabel = new JLabel("Obtain the requested items (not tracked in inventory).");
private final JLabel findLabel = new JLabel("Locate the and enter a PoI.");
private final JLabel gatherLabel = new JLabel("Have the requested item in your inventory (tracked in inventory)");
private final JLabel giveLabel = new JLabel("Have the requested items removed from your inventory.");
private final JLabel haveReputationLabel = new JLabel("Have a minimum reputation in the selected PoI");
private final JLabel haveReputationInCurrentLocationLabel = new JLabel("Have a minimum reputation in the selected PoI (and be in it)");
private final JLabel huntLabel = new JLabel("Track down and defeat your target (on the overworld map).");
private final JLabel leaveLabel = new JLabel("Exit the current PoI and return to the overworld map.");
private final JLabel mapFlagLabel = new JLabel("Have a map flag set to a minimum value");
private final JLabel noneLabel = new JLabel("No visible objective. Use in coordination with hidden parallel objectives to track when to progress");
private final JLabel patrolLabel = new JLabel("Get close to generated coordinates before starting your next objective");
private final JLabel rescueLabel = new JLabel("Reach and rescue the target");
private final JLabel siegeLabel = new JLabel("Travel to the target location and defeat enemies attacking it");
private final JLabel questFlagLabel = new JLabel("Have a global quest flag set to a minimum value");
private final JLabel travelLabel = new JLabel("Travel to the given destination.");
private final JLabel useLabel = new JLabel("Use the indicated item from your inventory.");
private JTabbedPane poiPane = new JTabbedPane();
private final QuestTagSelector poiSelector = new QuestTagSelector("Destination Tags", false,true);
private final QuestTagSelector enemySelector = new QuestTagSelector("Enemy Tags", true,false);
private final JTextField poiTokenInput = new JTextField(25);
private final JLabel poiTokenLabel = new JLabel();
private final JLabel poiTokenDescription = new JLabel(
"At the bottom of many objectives involving a PoI, you will see a text field in the format of '$poi_#'." +
"Enter that tag here to ignore the PoI tag selector of this stage and instead use the same PoI that was selected " +
"for that stage as the target PoI for this one as well.");
private void hideAllControls(){
arenaLabel.setVisible(false);
clearLabel.setVisible(false);
deliveryLabel.setVisible(false);
defeatLabel.setVisible(false);
escortLabel.setVisible(false);
fetchLabel.setVisible(false);
findLabel.setVisible(false);
gatherLabel.setVisible(false);
giveLabel.setVisible(false);
haveReputationLabel.setVisible(false);
huntLabel.setVisible(false);
leaveLabel.setVisible(false);
noneLabel.setVisible(false);
patrolLabel.setVisible(false);
rescueLabel.setVisible(false);
siegeLabel.setVisible(false);
mapFlagLabel.setVisible(false);
questFlagLabel.setVisible(false);
travelLabel.setVisible(false);
useLabel.setVisible(false);
deliveryItem.setVisible(false);
mapFlagGroup.setVisible(false);
flagValueGroup.setVisible(false);
anyPOI.setVisible(false);
specificPOI.setVisible(false);
here.setVisible(false);
mixedEnemies.setVisible(false);
enemySelector.setVisible(false);
count1Description.setVisible(false);
count2Description.setVisible(false);
count3Description.setVisible(false);
count4Description.setVisible(false);
count1Spinner.setVisible(false);
count2Spinner.setVisible(false);
count3Spinner.setVisible(false);
count4Spinner.setVisible(false);
poiPane.setVisible(false);
poiTokenLabel.setVisible(false);
nyi.setVisible(false);
}
private void switchPanels(){
hideAllControls();
if (objectiveType.getSelectedItem() == null){
return;
}
switch(objectiveType.getSelectedItem().toString()){
case "Arena":
arenaLabel.setVisible(true);
poiPane.setVisible(true);
count3Description.setText("Arena tournaments to win (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
count4Description.setText("Fail after tournaments not won (only applied if > 0)");
count4Description.setVisible(true);
count4Spinner.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "CharacterFlag":
characterFlagLabel.setVisible(true);
nyi.setVisible(true);
mapFlagGroup.setVisible(true);
flagValueGroup.setVisible(true);
break;
case "Clear":
clearLabel.setVisible(true);
poiPane.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "CompleteQuest":
completeQuestLabel.setVisible(true);
poiPane.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Number of quests to complete (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "Defeat":
defeatLabel.setVisible(true);
mixedEnemies.setVisible(true);
count3Description.setText("Matches to win");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
count4Description.setText("Fail after matches not won (only applied if > 0)");
count4Description.setVisible(true);
count4Spinner.setVisible(true);
enemySelector.setVisible(true);
break;
case "Delivery":
deliveryLabel.setVisible(true);
nyi.setVisible(true);
poiPane.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
deliveryItem.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "Escort":
escortLabel.setVisible(true);
nyi.setVisible(true);
poiPane.setVisible(true);
here.setVisible(true);
spriteNames.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "EventFinish":
eventFinishLabel.setVisible(true);
poiPane.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Events to complete (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "EventWin":
eventWinLabel.setVisible(true);
poiPane.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Events to win (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
count4Description.setText("Fail after events not won (only applied if > 0)");
count4Description.setVisible(true);
count4Spinner.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "EventWinMatch":
eventWinMatchLabel.setVisible(true);
poiPane.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Event matches to win (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
count4Description.setText("Fail after matches not won (only applied if > 0)");
count4Description.setVisible(true);
count4Spinner.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "Fetch":
fetchLabel.setVisible(true);
nyi.setVisible(true);
poiPane.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
deliveryItem.setVisible(true);
enemySelector.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "Find":
findLabel.setVisible(true);
nyi.setVisible(true);
poiPane.setVisible(true);
anyPOI.setVisible(true);
poiTokenLabel.setVisible(true);
break;
case "Gather":
gatherLabel.setVisible(true);
gatherLabel.setVisible(true);
nyi.setVisible(true);
itemNames.setVisible(true);
break;
case "Give":
giveLabel.setVisible(true);
nyi.setVisible(true);
itemNames.setVisible(true);
poiPane.setVisible(true);
poiTokenLabel.setVisible(true);
here.setVisible(true);
break;
case "HaveReputation":
poiPane.setVisible(true);
poiTokenLabel.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Minimum reputation needed");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
break;
case "haveReputationInCurrentLocation":
haveReputationInCurrentLocationLabel.setVisible(true);
poiPane.setVisible(true);
poiTokenLabel.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Minimum reputation needed");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
break;
case "Hunt":
huntLabel.setVisible(true);
enemySelector.setVisible(true);
count3Description.setText("Lifespan (seconds to complete hunt before despawn)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
break;
case "Leave":
leaveLabel.setVisible(true);
poiPane.setVisible(true);
poiTokenLabel.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Number of times to leave before triggering (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
break;
case "MapFlag":
mapFlagLabel.setVisible(true);
poiPane.setVisible(true);
here.setVisible(true);
anyPOI.setVisible(true);
mapFlagGroup.setVisible(true);
flagValueGroup.setVisible(true);
break;
case "None":
noneLabel.setVisible(true);
nyi.setVisible(true);
break;
case "Patrol":
patrolLabel.setVisible(true);
nyi.setVisible(true);
break;
case "Rescue":
rescueLabel.setVisible(true);
nyi.setVisible(true);
poiPane.setVisible(true);
anyPOI.setVisible(true);
poiTokenLabel.setVisible(true);
here.setVisible(true);
spriteNames.setVisible(true);
enemySelector.setVisible(true);
break;
case "Siege":
siegeLabel.setVisible(true);
nyi.setVisible(true);
poiPane.setVisible(true);
poiTokenLabel.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
enemySelector.setVisible(true);
mixedEnemies.setVisible(true);
poiPane.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Number of enemies to defeat");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
count3Spinner.setVisible(true);
count4Description.setText("Time limit (will only fail if > 0)");
count4Description.setVisible(true);
count4Spinner.setVisible(true);
break;
case "QuestFlag":
questFlagLabel.setVisible(true);
mapFlagGroup.setVisible(true);
flagValueGroup.setVisible(true);
break;
case "Travel":
travelLabel.setVisible(true);
poiPane.setVisible(true);
poiTokenLabel.setVisible(true);
poiTokenInput.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
count1Description.setText("Target % of possible distances");
count1Description.setVisible(true);
count1Spinner.setVisible(true);
count2Description.setText("Plus or minus %");
count2Description.setVisible(true);
count2Spinner.setVisible(true);
count3Description.setText("Number of times to enter before triggering (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
break;
case "Use":
useLabel.setVisible(true);
nyi.setVisible(true);
itemNames.setVisible(true);
poiPane.setVisible(true);
anyPOI.setVisible(true);
here.setVisible(true);
poiTokenLabel.setVisible(true);
here.setVisible(true);
count3Description.setText("Number of times to use triggering (minimum 1)");
count3Description.setVisible(true);
count3Spinner.setVisible(true);
break;
}
specificPOI.setVisible(anyPOI.isVisible());
}
private void changeObjective(){
if (objectiveType.getSelectedItem() != null)
currentData.objective = AdventureQuestController.ObjectiveTypes.valueOf(objectiveType.getSelectedItem().toString());
switchPanels();
}
private JPanel getObjectiveTab(){
objectiveType = new JComboBox<>(AdventureQuestController.ObjectiveTypes.values());
objectiveType.addActionListener( e -> changeObjective());
JPanel objectiveTab = new JPanel();
JScrollPane scrollPane = new JScrollPane();
objectiveTab.add(scrollPane);
FormPanel center=new FormPanel();
center.add(objectiveType);
scrollPane.add(center);
mapFlagGroup = new Box(BoxLayout.Y_AXIS);
mapFlagGroup.add(new JLabel("Map flag to check"));
mapFlagGroup.add(mapFlag);
flagValueGroup = new Box(BoxLayout.Y_AXIS);
flagValueGroup.add(new JLabel("Flag value to check"));
flagValueGroup.add(flagSpinner);
anyPOIGroup.add(anyPOI);
anyPOIGroup.add(specificPOI);
JPanel poiSelectorPane = new JPanel();
poiSelectorPane.setLayout(new BorderLayout());
poiSelectorPane.add(poiSelector, BorderLayout.CENTER);
JPanel poiTokenPanel = new JPanel();
JPanel tokenPanel = new JPanel();
tokenPanel.add(new JLabel("Token to use:"));
tokenPanel.add(poiTokenInput);
tokenPanel.setBorder(new EmptyBorder(10, 10, 30, 10));
poiTokenPanel.add(poiTokenDescription);
poiTokenPanel.add(tokenPanel);
poiTokenPanel.add(here);
GroupLayout poiTokenLayout = new GroupLayout(poiTokenPanel);
poiTokenLayout.setHorizontalGroup(
poiTokenLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(poiTokenDescription)
.addComponent(tokenPanel,GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE)
.addComponent(here));
poiTokenLayout.setVerticalGroup(
poiTokenLayout.createSequentialGroup()
.addComponent(poiTokenDescription)
.addComponent(tokenPanel,GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE)
.addComponent(here));
poiTokenPanel.setLayout(poiTokenLayout);
poiPane.add("Specific PoI", poiTokenPanel);
poiPane.add("Tag Selector", poiSelectorPane);
poiPane.setPreferredSize(new Dimension(0,200));
center.add(arenaLabel);
center.add(clearLabel);
center.add(defeatLabel);
center.add(deliveryLabel);
center.add(escortLabel);
center.add(fetchLabel);
center.add(findLabel);
center.add(gatherLabel);
center.add(giveLabel);
center.add(haveReputationLabel);
center.add(huntLabel);
center.add(leaveLabel);
center.add(noneLabel);
center.add(patrolLabel);
center.add(rescueLabel);
center.add(siegeLabel);
center.add(mapFlagLabel);
center.add(questFlagLabel);
center.add(travelLabel);
center.add(useLabel);
center.add(nyi);
center.add(deliveryItem);
center.add(mapFlagGroup);
center.add(flagValueGroup);
center.add(anyPOI);
center.add(specificPOI);
center.add(mixedEnemies);
center.add(enemySelector);
center.add(count1Description);
center.add(count1Spinner);
center.add(count2Description);
center.add(count2Spinner);
center.add(count3Description);
center.add(count3Spinner);
center.add(count4Description);
center.add(count4Spinner);
center.add(poiPane);
center.add(poiTokenLabel);
switchPanels();
poiSelector.selectedItems.addListDataListener(new ListDataListener() {
@Override
public void intervalAdded(ListDataEvent e) {
rebuildPOIList();
rebuildEnemyList();
}
@Override
public void intervalRemoved(ListDataEvent e) {
rebuildPOIList();
rebuildEnemyList();
}
@Override
public void contentsChanged(ListDataEvent e) {
rebuildPOIList();
rebuildEnemyList();
}
});
return center;
}
private void rebuildPOIList(){
List<String> currentList = new ArrayList<>();
for(int i = 0; i< poiSelector.selectedItems.getSize(); i++){
currentList.add(poiSelector.selectedItems.getElementAt(i));
}
currentData.POITags = currentList;
}
private void rebuildEnemyList(){
List<String> currentList = new ArrayList<>();
for(int i = 0; i< enemySelector.selectedItems.getSize(); i++){
currentList.add(enemySelector.selectedItems.getElementAt(i));
}
currentData.enemyTags = currentList;
}
private JPanel getPrereqTab(){
JPanel prereqTab = new JPanel();
prereqTab.add(new JLabel("Insert Prereq data here"));
return prereqTab;
}
private void refresh() {
if(currentData==null)
{
return;
}
setEnabled(false);
updating=true;
objectiveType.setSelectedItem(currentData.objective);
if (objectiveType.getSelectedItem() != null)
currentData.objective = AdventureQuestController.ObjectiveTypes.valueOf(objectiveType.getSelectedItem().toString()); //Ensuring this gets initialized on new
name.setText(currentData.name);
description.setText(currentData.description);
deliveryItem.setText(currentData.deliveryItem);
if (currentData.enemyTags != null){
DefaultListModel<String> selectedEnemies = new DefaultListModel<>();
for (int i = 0; i < currentData.enemyTags.size(); i++) {
selectedEnemies.add(i, currentData.enemyTags.get(i));
}
enemySelector.load(selectedEnemies);
}
if (currentData.POITags != null){
DefaultListModel<String> selectedPOI = new DefaultListModel<>();
for (int i = 0; i < currentData.POITags.size(); i++) {
selectedPOI.add(i, currentData.POITags.get(i));
}
poiSelector.load(selectedPOI);
}
itemNames.setText(currentData.itemNames);
equipNames.setText(currentData.equipNames);
prologueEditor.loadData(currentData.prologue);
epilogueEditor.loadData(currentData.epilogue);
if (currentData.POITags != null){
DefaultListModel<String> selectedPOI = new DefaultListModel<>();
for (int i = 0; i < currentData.POITags.size(); i++) {
selectedPOI.add(i, currentData.POITags.get(i));
}
poiSelector.load(selectedPOI);
}
here.getModel().setSelected(currentData.here);
anyPOI.getModel().setSelected(currentData.anyPOI);
specificPOI.getModel().setSelected(!currentData.anyPOI);
poiTokenInput.setText(currentData.POIToken);
poiTokenLabel.setText( "To reference this point of interest: $(poi_" + currentData.id + ")");
ArrayList<String> temp = new ArrayList<>();
for (AdventureQuestStage stage : currentQuestData.stages){
if (stage.equals(currentData))
continue;
temp.add(stage.name);
}
prerequisites.setOptions(temp);
count1Spinner.getModel().setValue(currentData.count1);
count2Spinner.getModel().setValue(currentData.count2);
count3Spinner.getModel().setValue(currentData.count3);
count4Spinner.getModel().setValue(currentData.count4);
updating=false;
setEnabled(true);
}
public void updateStage()
{
if(currentData==null||updating)
return;
currentData.name=name.getText();
currentData.description= description.getText();
currentData.prologue = prologueEditor.getDialogData();
currentData.epilogue = epilogueEditor.getDialogData();
currentData.deliveryItem = deliveryItem.getText();
currentData.itemNames = itemNames.getList()==null?new ArrayList<>():Arrays.asList(itemNames.getList());
currentData.equipNames = equipNames.getList()==null?new ArrayList<>():Arrays.asList(equipNames.getList());
currentData.anyPOI = anyPOI.getModel().isSelected();
currentData.mapFlag = mapFlag.getText();
currentData.mapFlagValue = Integer.parseInt(flagSpinner.getModel().getValue().toString());
currentData.count1 = Integer.parseInt(count1Spinner.getModel().getValue().toString());
currentData.count2 = Integer.parseInt(count2Spinner.getModel().getValue().toString());
currentData.count3 = Integer.parseInt(count3Spinner.getModel().getValue().toString());
currentData.count4 = Integer.parseInt(count4Spinner.getModel().getValue().toString());
currentData.mixedEnemies = mixedEnemies.getModel().isSelected();
currentData.here = here.getModel().isSelected();
currentData.POIToken = poiTokenInput.getText();
rebuildPOIList();
rebuildEnemyList();
emitChanged();
}
public void setCurrentStage(AdventureQuestStage stageData, AdventureQuestData data) {
if (stageData == null)
stageData = new AdventureQuestStage();
if (data == null)
data = new AdventureQuestData();
currentData =stageData;
currentQuestData=data;
setVisible(true);
refresh();
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void addListeners(){
deliveryItem.getDocument().addDocumentListener(new DocumentChangeListener(QuestStageEdit.this::updateStage));
mapFlag.getDocument().addDocumentListener(new DocumentChangeListener(QuestStageEdit.this::updateStage));
flagSpinner.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
count1Spinner.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
count2Spinner.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
count3Spinner.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
count4Spinner.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
mixedEnemies.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
here.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
anyPOI.getModel().addChangeListener(q -> QuestStageEdit.this.updateStage());
deliveryItem.getDocument().addDocumentListener(new DocumentChangeListener(QuestStageEdit.this::updateStage));
mapFlag.getDocument().addDocumentListener(new DocumentChangeListener(QuestStageEdit.this::updateStage));
prologueEditor.addChangeListener(q -> QuestStageEdit.this.updateStage());
epilogueEditor.addChangeListener(q -> QuestStageEdit.this.updateStage());
}
}

View File

@@ -1,138 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.AdventureQuestData;
import forge.adventure.data.AdventureQuestStage;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class QuestStageEditor extends JComponent{
DefaultListModel<AdventureQuestStage> model = new DefaultListModel<>();
JList<AdventureQuestStage> list = new JList<>(model);
JScrollPane scroll;
JToolBar toolBar = new JToolBar("toolbar");
QuestStageEdit edit=new QuestStageEdit();
AdventureQuestData currentData;
public class QuestStageRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestStage stageData))
return label;
label.setText(stageData.name);
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public QuestStageEditor()
{
list.setCellRenderer(new QuestStageRenderer());
list.addListSelectionListener(e -> QuestStageEditor.this.updateEdit());
addButton("Add Quest Stage", e -> QuestStageEditor.this.addStage());
addButton("Remove Selected", e -> QuestStageEditor.this.remove());
toolBar.setFloatable(false);
setLayout(new BorderLayout());
scroll = new JScrollPane(list);
add(scroll, BorderLayout.WEST);
JPanel editPanel = new JPanel();
editPanel.setLayout(new BorderLayout());
add(toolBar, BorderLayout.NORTH);
editPanel.add(edit,BorderLayout.CENTER);
add(editPanel);
edit.addChangeListener(e -> emitChanged());
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentStage(model.get(selected),currentData);
}
void addStage()
{
AdventureQuestStage data=new AdventureQuestStage();
data.name = "New Stage";
model.add(model.size(),data);
edit.setVisible(true);
scroll.setVisible(true);
int id = 0;
for (int i = 0; i < model.size(); i++)
{
if (model.get(i).id >= id)
id = model.get(i).id +1;
}
data.id = id;
if (model.size() == 1){
list.setSelectedIndex(0);
}
emitChanged();
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
edit.setVisible(false);
scroll.setVisible(list.getModel().getSize() > 0);
emitChanged();
}
public void setStages(AdventureQuestData data) {
currentData=data;
model.clear();
if(data==null||data.stages==null || data.stages.length == 0)
{
edit.setVisible(false);
return;
}
for (int i=0;i<data.stages.length;i++) {
model.add(i,data.stages[i]);
}
if (model.size() > 0) {
list.setSelectedIndex(0);
}
}
public AdventureQuestStage[] getStages() {
AdventureQuestStage[] stages= new AdventureQuestStage[model.getSize()];
for(int i=0;i<model.getSize();i++)
{
stages[i]=model.get(i);
}
return stages;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,107 +0,0 @@
package forge.adventure.editor;
import javax.swing.*;
import java.awt.*;
public class QuestTagSelector extends JComponent {
DefaultListModel<String> allItems = new DefaultListModel<>();
DefaultListModel<String> selectedItems = new DefaultListModel<>();
JList<String> unselectedList;
JList<String> selectedList;
boolean useEnemyTags = false;
boolean usePOITags = false;
public QuestTagSelector(String title, boolean useEnemyTags, boolean usePOITags)
{
if (useEnemyTags){
this.useEnemyTags = true;
} else if (usePOITags) {
this.usePOITags = true;
}
else{
return;
}
unselectedList = new JList<>(allItems);
selectedList = new JList<>(selectedItems);
// unselectedList.setCellRenderer(new PointOfInterestEditor.PointOfInterestRenderer()); // Replace with use count of tag?
// selectedList.setCellRenderer(new PointOfInterestEditor.PointOfInterestRenderer());
JButton addButton=new JButton("add");
JButton removeButton=new JButton("remove");
addButton.addActionListener( e -> QuestTagSelector.this.addTag());
removeButton.addActionListener( e -> QuestTagSelector.this.removeTag());
BorderLayout layout=new BorderLayout();
setLayout(layout);
if (title.length() > 0)
add(new JLabel(title),BorderLayout.PAGE_START);
add(new JScrollPane(unselectedList), BorderLayout.LINE_START);
add(new JScrollPane(selectedList), BorderLayout.LINE_END);
JPanel buttonPanel = new JPanel();
GridLayout buttonLayout = new GridLayout(2,0);
buttonPanel.setLayout(buttonLayout);
buttonPanel.add(addButton);
buttonPanel.add(removeButton);
add(buttonPanel);
}
public void addTag(){
if (unselectedList.isSelectionEmpty()){
return;
}
for (String toAdd : unselectedList.getSelectedValuesList())
{
if (selectedItems.contains(toAdd)) continue;
selectedItems.addElement(toAdd);
}
refresh();
}
public void removeTag(){
if (selectedList.isSelectionEmpty()){
return;
}
for (String toRemove : selectedList.getSelectedValuesList())
{
selectedItems.removeElement(toRemove);
}
refresh();
}
public void load(DefaultListModel<String> selectedNames)
{
allItems.clear();
selectedItems.clear();
if (useEnemyTags){
allItems = QuestController.getInstance().getEnemyTags();
}
else if (usePOITags) {
allItems = QuestController.getInstance().getPOITags();
}
unselectedList.setModel(allItems);
for (int i=0;i<allItems.size();i++){
if (selectedNames.contains(allItems.get(i))){
selectedItems.addElement(allItems.get(i));
}
}
}
private boolean updating=false;
private void refresh() {
setEnabled(allItems!=null);
if(allItems==null)
{
return;
}
updating=true;
//unselectedList = new JList<>(allItems);
//selectedList = new JList<>(selectedItems);
updating=false;
}
}

View File

@@ -1,144 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.RewardData;
import forge.card.CardType;
import forge.game.keyword.Keyword;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.util.Arrays;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class RewardEdit extends FormPanel {
RewardData currentData;
JComboBox typeField =new JComboBox(new String[] { "card", "gold", "life", "deckCard", "item","shards"});
JSpinner probability = new JSpinner(new SpinnerNumberModel(0f, 0, 1, 0.1f));
JSpinner count = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner addMaxCount = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JTextField cardName =new JTextField();
JTextField itemName =new JTextField();
TextListEdit editions =new TextListEdit();
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 subTypes =new TextListEdit();
TextListEdit cardTypes =new TextListEdit(Arrays.stream(CardType.CoreType.values()).map(CardType.CoreType::toString).toArray(String[]::new));
TextListEdit superTypes =new TextListEdit(Arrays.stream(CardType.Supertype.values()).map(CardType.Supertype::toString).toArray(String[]::new));
TextListEdit manaCosts =new TextListEdit();
TextListEdit keyWords =new TextListEdit(Arrays.stream(Keyword.values()).map(Keyword::toString).toArray(String[]::new));
JComboBox colorType =new JComboBox(new String[] { "Any", "Colorless", "MultiColor", "MonoColor"});
JTextField cardText =new JTextField();
private boolean updating=false;
public RewardEdit()
{
add("Type:",typeField);
add("probability:",probability);
add("count:",count);
add("addMaxCount:",addMaxCount);
add("cardName:",cardName);
add("itemName:",itemName);
add("editions:",editions);
add("colors:",colors);
add("rarity:",rarity);
add("subTypes:",subTypes);
add("cardTypes:",cardTypes);
add("superTypes:",superTypes);
add("manaCosts:",manaCosts);
add("keyWords:",keyWords);
add("colorType:",colorType);
add("cardText:",cardText);
typeField.addActionListener((e -> RewardEdit.this.updateReward()));
probability.addChangeListener(e -> RewardEdit.this.updateReward());
count.addChangeListener(e -> RewardEdit.this.updateReward());
addMaxCount.addChangeListener(e -> RewardEdit.this.updateReward());
cardName.getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward));
itemName.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));
rarity.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));
superTypes.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));
colorType.addActionListener((e -> RewardEdit.this.updateReward()));
cardText.getDocument().addDocumentListener(new DocumentChangeListener(RewardEdit.this::updateReward));
}
private void updateReward() {
if(currentData==null||updating)
return;
currentData.type=typeField.getSelectedItem()==null?null:typeField.getSelectedItem().toString();
currentData.probability=((Double)probability.getValue()).floatValue();
currentData.count= (int) count.getValue();
currentData.addMaxCount= (int) addMaxCount.getValue();
currentData.cardName = cardName.getText().isEmpty()?null:cardName.getText();
currentData.itemNames = itemName.getText().isEmpty()?null:itemName.getText().split(",");
currentData.editions = editions.getList();
currentData.colors = colors.getList();
currentData.rarity = rarity.getList();
currentData.subTypes = subTypes.getList();
currentData.cardTypes = cardTypes.getList();
currentData.superTypes = superTypes.getList();
currentData.manaCosts = manaCosts.getListAsInt();
currentData.keyWords = keyWords.getList();
currentData.colorType=colorType.getSelectedItem()==null?null:colorType.getSelectedItem().toString();
currentData.cardText = cardText.getText().isEmpty()?null:cardText.getText();
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
public void setCurrentReward(RewardData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
typeField.setSelectedItem(currentData.type);
probability.setValue((double) currentData.probability);
count.setValue(currentData.count);
addMaxCount.setValue(currentData.addMaxCount);
cardName.setText(currentData.cardName);
itemName.setText(currentData.itemName);
editions.setText(currentData.editions);
colors.setText(currentData.colors);
rarity.setText(currentData.rarity);
subTypes.setText(currentData.subTypes);
cardTypes.setText(currentData.cardTypes);
superTypes.setText(currentData.superTypes);
manaCosts.setText(currentData.manaCosts);
keyWords.setText(currentData.keyWords);
colorType.setSelectedItem(currentData.colorType);
cardText.setText(currentData.cardText);
updating=false;
}
}

View File

@@ -1,140 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.RewardData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class RewardsEditor extends JComponent{
DefaultListModel<RewardData> model = new DefaultListModel<>();
JList<RewardData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
RewardEdit edit=new RewardEdit();
boolean updating;
public class RewardDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof RewardData))
return label;
RewardData reward=(RewardData) value;
StringBuilder builder=new StringBuilder();
if(reward.type==null||reward.type.isEmpty())
builder.append("Reward");
else
builder.append(reward.type);
builder.append(" ");
builder.append(reward.count);
if(reward.addMaxCount>0)
{
builder.append("-");
builder.append(reward.count+reward.addMaxCount);
}
label.setText(builder.toString());
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public RewardsEditor()
{
list.setCellRenderer(new RewardDataRenderer());
list.addListSelectionListener(e -> RewardsEditor.this.updateEdit());
addButton("add", e -> RewardsEditor.this.addReward());
addButton("remove", e -> RewardsEditor.this.remove());
addButton("copy", e -> RewardsEditor.this.copy());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(list, BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged());
}
protected void emitChanged() {
if (updating)
return;
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
RewardData data=new RewardData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentReward(model.get(selected));
}
void addReward()
{
RewardData data=new RewardData();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
public void setRewards(RewardData[] rewards) {
model.clear();
if(rewards==null)
return;
for (int i=0;i<rewards.length;i++) {
model.add(i,rewards[i]);
}
}
public RewardData[] getRewards() {
RewardData[] rewards= new RewardData[model.getSize()];
for(int i=0;i<model.getSize();i++)
{
rewards[i]=model.get(i);
}
return rewards;
}
public void clear(){
updating = true;
model.clear();
updating = false;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,217 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.graphics.Color;
import forge.adventure.data.BiomeData;
import forge.adventure.data.BiomeStructureData;
import forge.adventure.util.Config;
import forge.adventure.world.BiomeStructure;
import forge.adventure.world.ColorMap;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class StructureEditor extends JComponent{
DefaultListModel<BiomeStructureData> model = new DefaultListModel<>();
JList<BiomeStructureData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
BiomeStructureEdit edit=new BiomeStructureEdit();
BiomeData currentData;
public class StructureDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeStructureData))
return label;
BiomeStructureData structureData=(BiomeStructureData) value;
label.setText("Structure");
label.setIcon(new ImageIcon(Config.instance().getFilePath(structureData.sourcePath)));
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public StructureEditor()
{
list.setCellRenderer(new StructureDataRenderer());
list.addListSelectionListener(e -> StructureEditor.this.updateEdit());
addButton("add", e -> StructureEditor.this.addStructure());
addButton("remove", e -> StructureEditor.this.remove());
addButton("copy", e -> StructureEditor.this.copy());
addButton("test", e -> StructureEditor.this.test());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(list, BorderLayout.WEST);
add(toolBar, BorderLayout.NORTH);
add(edit,BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged());
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void test() {
if (list.isSelectionEmpty())
return;
long start = System.currentTimeMillis();
BiomeStructureData data = model.get(list.getSelectedIndex());
try
{
BiomeStructure struct = new BiomeStructure(data, System.currentTimeMillis(),
(int) (currentData.width * EditorMainWindow.worldEditor.width.intValue() ),
(int) (currentData.width * EditorMainWindow.worldEditor.height.intValue()));
BufferedImage sourceImage= null;
try {
sourceImage = ImageIO.read(new File(struct.sourceImagePath()));
ColorMap sourceColorMap=new ColorMap(sourceImage.getWidth(),sourceImage.getHeight());
for(int y=0;y<sourceColorMap.getHeight();y++)
for(int x=0;x<sourceColorMap.getWidth();x++)
{
Color c =new Color();
Color.argb8888ToColor(c,sourceImage.getRGB(x,y));
sourceColorMap.setColor(x,y,c);
}
BufferedImage maskImage= ImageIO.read(new File(struct.maskImagePath()));
ColorMap maskColorMap=new ColorMap(maskImage.getWidth(),maskImage.getHeight());
for(int y=0;y<maskColorMap.getHeight();y++)
for(int x=0;x<maskColorMap.getWidth();x++)
{
Color c =new Color();
Color.argb8888ToColor(c,maskImage.getRGB(x,y));
maskColorMap.setColor(x,y,c);
}
struct.initialize(sourceColorMap,maskColorMap);
} catch (IOException e) {
throw new RuntimeException(e);
}
float calcTime=(System.currentTimeMillis() - start)/1000f;
JLabel label = new JLabel();
ColorMap colorMap=struct.image;
BufferedImage image = new BufferedImage(colorMap.getWidth(),colorMap.getHeight(),BufferedImage.TYPE_INT_ARGB);
for(int y=0;y<colorMap.getHeight();y++)
for(int x=0;x<colorMap.getWidth();x++)
image.setRGB(x,y,Color.argb8888(colorMap.getColor(x,y)));
if (image.getWidth() < 640 | image.getHeight() < 640) {
if (image.getHeight() > image.getWidth()) {
BufferedImage nimage = new BufferedImage(640, 640 * (image.getWidth() / image.getHeight()), BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(640 / image.getHeight(), 640 / image.getHeight());
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
image = scaleOp.filter(image, nimage);
} else {
BufferedImage nimage = new BufferedImage(640 * (image.getHeight() / image.getWidth()), 640, BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(640 / image.getWidth(), 640 / image.getWidth());
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
image = scaleOp.filter(image, nimage);
}
}
label.setIcon(new ImageIcon(image));
label.setSize(640, 640);
JOptionPane.showMessageDialog(this, label,"Calculating took "+ calcTime+" seconds",JOptionPane.PLAIN_MESSAGE);
}
catch (Exception e)
{
JOptionPane.showMessageDialog(this, "WaveFunctionCollapse was not successful","can not calculate function "+e.getMessage(),JOptionPane.ERROR_MESSAGE);
}
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
BiomeStructureData data=new BiomeStructureData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentStructure(model.get(selected),currentData);
}
void addStructure()
{
BiomeStructureData data=new BiomeStructureData();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
public void setStructures(BiomeData data) {
currentData=data;
model.clear();
if(data==null||data.structures==null)
{
edit.setCurrentStructure(null,null);
return;
}
for (int i=0;i<data.structures.length;i++) {
model.add(i,data.structures[i]);
}
list.setSelectedIndex(0);
}
public BiomeStructureData[] getBiomeStructureData() {
BiomeStructureData[] rewards= new BiomeStructureData[model.getSize()];
for(int i=0;i<model.getSize();i++)
{
rewards[i]=model.get(i);
}
return rewards;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,92 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.utils.Array;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import static java.awt.Image.SCALE_FAST;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class SwingAtlas {
int imageSize=32;
HashMap<String, ArrayList<ImageIcon>> images=new HashMap<>();
public HashMap<String, ArrayList<ImageIcon>> getImages()
{
return images;
}
public SwingAtlas(FileHandle path,int imageSize)
{
this.imageSize=imageSize;
if(!path.exists()||!path.toString().endsWith(".atlas"))
return;
TextureAtlas.TextureAtlasData data=new TextureAtlas.TextureAtlasData(path,path.parent(),false);
for(TextureAtlas.TextureAtlasData.Region region: new Array.ArrayIterator<>(data.getRegions()))
{
String name=region.name;
if(!images.containsKey(name))
{
images.put(name,new ArrayList<>());
}
ArrayList<ImageIcon> imageList=images.get(name);
try
{
imageList.add(spriteToImage(region));
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
public SwingAtlas(FileHandle path)
{
this(path,32);
}
private ImageIcon spriteToImage(TextureAtlas.TextureAtlasData.Region sprite) throws IOException {
try
{
BufferedImage img = ImageIO.read(sprite.page.textureFile.file());
if(sprite.width== sprite.height)
return new ImageIcon(img.getSubimage(sprite.left,sprite.top, sprite.width, sprite.height).getScaledInstance(imageSize,imageSize,SCALE_FAST));
if(sprite.width>sprite.height)
return new ImageIcon(img.getSubimage(sprite.left,sprite.top, sprite.width, sprite.height).getScaledInstance(imageSize, (int) (imageSize*(sprite.height/(float)sprite.width)),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)
{
return null;
}
}
public ImageIcon get(String name) {
if(images.get(name).size()==0)
return null;
return images.get(name).get(0);
}
public boolean has(String name) {
return images.containsKey(name);
}
public ImageIcon getAny() {
if(images.isEmpty())
return null;
ArrayList<ImageIcon> imageList= images.get(images.keySet().iterator().next());
if(imageList.isEmpty())
return null;
return imageList.get(0);
}
}

View File

@@ -1,94 +0,0 @@
package forge.adventure.editor;
import forge.adventure.util.Config;
import org.apache.commons.lang3.tuple.Pair;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class SwingAtlasPreview extends Box {
int imageSize=32;
private String sprite="";
private String spriteName="";
Timer timer;
public SwingAtlasPreview()
{
this(64,200);
}
public SwingAtlasPreview(int imageSize,int timeDelay) {
super(BoxLayout.Y_AXIS);
timer = new Timer(timeDelay, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
counter++;
for (Pair<JLabel, ArrayList<ImageIcon>> element : labels) {
element.getKey().setIcon(element.getValue().get(counter % element.getValue().size()));
}
}
});
this.imageSize=imageSize;
}
static Map<String,Map<Integer,SwingAtlas>> cache=new HashMap<>();
public SwingAtlasPreview(int size) {
this();
imageSize=size;
}
int counter=0;
List<Pair<JLabel,ArrayList<ImageIcon>>> labels=new ArrayList<>();
public void setSpritePath(String sprite) {
setSpritePath(sprite,null);
}
public void setSpritePath(String sprite,String name) {
if(this.sprite==null||sprite==null||(this.sprite.equals(sprite)&&(spriteName != null && spriteName.equals(name))))
return;
removeAll();
counter=0;
labels.clear();
this.sprite=sprite;
this.spriteName=name;
SwingAtlas atlas;
if(!cache.containsKey(sprite))
{
cache.put(sprite,new HashMap<>() );
}
if(!cache.get(sprite).containsKey(imageSize))
{
cache.get(sprite).put(imageSize,new SwingAtlas(Config.instance().getFile(sprite),imageSize) );
}
atlas=cache.get(sprite).get(imageSize);
int maxCount=0;
for(Map.Entry<String, ArrayList<ImageIcon>> element:atlas.getImages().entrySet())
{
if(name==null||element.getKey().equals(name))
{
JLabel image=new JLabel(element.getValue().get(0));
if(maxCount<element.getValue().size())
maxCount=element.getValue().size();
add(new JLabel(element.getKey()));
add(image);
labels.add(Pair.of(image, element.getValue()));
}
}
if(maxCount<=1)
{
timer.stop();
}
else
{
timer.restart();
}
// doLayout(); //These lines cause images to bleed on to other tabs and don't appear to be needed
// revalidate();
// update(getGraphics());
// repaint();
}
}

View File

@@ -1,134 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.BiomeData;
import forge.adventure.data.BiomeTerrainData;
import javax.swing.DefaultListCellRenderer;
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.ChangeListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class TerrainsEditor extends JComponent{
DefaultListModel<BiomeTerrainData> model = new DefaultListModel<>();
JList<BiomeTerrainData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
BiomeTerrainEdit edit=new BiomeTerrainEdit();
BiomeData currentData;
public class TerrainDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeTerrainData))
return label;
BiomeTerrainData terrainData=(BiomeTerrainData) value;
/*StringBuilder builder=new StringBuilder();
builder.append("Terrain");
builder.append(" ");
builder.append(terrainData.spriteName);*/
label.setText("Terrain " + terrainData.spriteName);
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public TerrainsEditor()
{
list.setCellRenderer(new TerrainDataRenderer());
list.addListSelectionListener(e -> TerrainsEditor.this.updateEdit());
addButton("add", e -> TerrainsEditor.this.addTerrain());
addButton("remove", e -> TerrainsEditor.this.remove());
addButton("copy", e -> TerrainsEditor.this.copy());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(list, BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged());
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
BiomeTerrainData data=new BiomeTerrainData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentTerrain(model.get(selected),currentData);
}
void addTerrain()
{
BiomeTerrainData data=new BiomeTerrainData();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
public void setTerrains(BiomeData data) {
currentData=data;
model.clear();
if(data==null||data.terrain==null)
return;
for (int i=0;i<data.terrain.length;i++) {
model.add(i,data.terrain[i]);
}
list.setSelectedIndex(0);
}
public BiomeTerrainData[] getBiomeTerrainData() {
BiomeTerrainData[] rewards= new BiomeTerrainData[model.getSize()];
for(int i=0;i<model.getSize();i++)
{
rewards[i]=model.get(i);
}
return rewards;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,118 +0,0 @@
package forge.adventure.editor;
import javax.swing.*;
import java.awt.*;
import java.util.List;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class TextListEdit extends Box {
JTextField edit=new JTextField();
JButton findButton=new JButton(UIManager.getIcon("add"));
JComboBox elements;
public TextListEdit(String[] possibleElements) {
super(BoxLayout.X_AXIS);
findButton.addActionListener(e -> TextListEdit.this.find());
add(edit);
edit.setPreferredSize(new Dimension(400,edit.getPreferredSize().height));
add(findButton);
elements= new JComboBox(possibleElements);
add(elements);
}
public TextListEdit()
{
this(new String[0]);
}
JTextField getEdit()
{
return edit;
}
private void find() {
edit.setText((edit.getText().trim().length()>0?edit.getText() + ";": "")+elements.getSelectedItem().toString());
elements.remove(elements.getSelectedIndex());
elements.setSelectedIndex(0);
// JFileChooser fc = new JFileChooser();
// fc.setCurrentDirectory(new File(Config.instance().getFilePath("")));
// fc.setMultiSelectionEnabled(false);
// if (fc.showOpenDialog(this) ==
// JFileChooser.APPROVE_OPTION) {
// File selected = fc.getSelectedFile();
//
// try {
// if (selected != null&&selected.getCanonicalPath().startsWith(new File(Config.instance().getFilePath("")).getCanonicalPath())) {
// edit.setText(selected.getCanonicalPath().substring(new File(Config.instance().getFilePath("")).getCanonicalPath().length()+1));
// }
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
}
public void setOptions(List<String> itemNames) {
if(itemNames==null)
elements.removeAllItems();
else {
for(String item: itemNames)
elements.addItem(item);
}
}
public void setText(List<String> itemNames) {
if(itemNames==null)
edit.setText("");
else
edit.setText(String.join(";",itemNames));
}
public void setText(String[] itemName) {
if(itemName==null)
edit.setText("");
else
edit.setText(String.join(";",itemName));
}
public void setText(int[] intValues) {
if(intValues==null)
{
edit.setText("");
return;
}
StringBuilder values= new StringBuilder();
for(int i=0;i<intValues.length;i++)
{
values.append(intValues[i]);
if(intValues.length>i+2)
values.append("\n");
}
edit.setText(values.toString());
}
public String[] getList() {
return edit.getText().isEmpty()?null:edit.getText().split(";");
}
public int[] getListAsInt() {
if(edit.getText().isEmpty())
return null;
String[] stringList=getList();
int[] retList=new int[stringList.length];
for(int i=0;i<retList.length;i++)
{
String intName=stringList[i];
try
{
retList[i] = Integer.parseInt(intName);
}
catch (NumberFormatException e)
{
retList[i] =0;
}
}
return retList;
}
}

View File

@@ -1,227 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.BiomeData;
import forge.adventure.data.WorldData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
public class WorldEditor extends JComponent {
WorldData currentData;
IntSpinner width= new IntSpinner( 0, 100000, 1);
IntSpinner height= new IntSpinner( 0, 100000, 1);
FloatSpinner playerStartPosX= new FloatSpinner( 0, 1, .1f);
FloatSpinner playerStartPosY= new FloatSpinner(0, 1, .1f);
FloatSpinner noiseZoomBiome= new FloatSpinner( 0, 1000f, 1f);
IntSpinner tileSize= new IntSpinner( 0, 100000, 1);
JTextField biomesSprites = new JTextField();
FloatSpinner maxRoadDistance = new FloatSpinner( 0, 100000f, 1f);
TextListEdit biomesNames = new TextListEdit();
DefaultListModel<BiomeData> model = new DefaultListModel<>();
JList<BiomeData> list = new JList<>(model);
BiomeEdit edit=new BiomeEdit();
JTabbedPane tabs =new JTabbedPane();
static HashMap<String,SwingAtlas> atlas=new HashMap<>();
public class BiomeDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeData biome))
return label;
// Get the renderer component from parent class
label.setText(biome.name);
if(!atlas.containsKey(biome.tilesetAtlas))
atlas.put(biome.tilesetAtlas,new SwingAtlas(Config.instance().getFile(biome.tilesetAtlas)));
SwingAtlas poiAtlas = atlas.get(biome.tilesetAtlas);
if(poiAtlas.has(biome.tilesetName))
label.setIcon(poiAtlas.get(biome.tilesetName));
else
{
ImageIcon img=poiAtlas.getAny();
if(img!=null)
label.setIcon(img);
}
return label;
}
}
/**
*
*/
private void updateBiome() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentBiome(model.get(selected));
}
public WorldEditor() {
list.setCellRenderer(new BiomeDataRenderer());
list.addListSelectionListener(e -> WorldEditor.this.updateBiome());
BorderLayout layout = new BorderLayout();
setLayout(layout);
add(tabs);
JSplitPane biomeData=new JSplitPane();
tabs.addTab("BiomeData", biomeData);
FormPanel worldPanel=new FormPanel();
worldPanel.add("width:",width);
worldPanel.add("height:",height);
worldPanel.add("playerStartPosX:",playerStartPosX);
worldPanel.add("playerStartPosY:",playerStartPosY);
worldPanel.add("noiseZoomBiome:",noiseZoomBiome);
worldPanel.add("tileSize:",tileSize);
worldPanel.add("biomesSprites:",biomesSprites);
worldPanel.add("maxRoadDistance:",maxRoadDistance);
worldPanel.add("biomesNames:",biomesNames);
tabs.addTab("WorldData", worldPanel);
JScrollPane pane = new JScrollPane(edit);
biomeData.setLeftComponent(list); biomeData.setRightComponent(pane);
load();
JToolBar toolBar = new JToolBar("toolbar");
add(toolBar, BorderLayout.PAGE_START);
JButton newButton=new JButton("save");
newButton.addActionListener(e -> WorldEditor.this.save());
toolBar.add(newButton);
newButton=new JButton("save selected biome");
newButton.addActionListener(e -> WorldEditor.this.saveBiome());
toolBar.add(newButton);
newButton=new JButton("load");
newButton.addActionListener(e -> WorldEditor.this.load());
toolBar.add(newButton);
toolBar.addSeparator();
newButton=new JButton("test map");
newButton.addActionListener(e -> WorldEditor.this.test());
toolBar.add(newButton);
newButton=new JButton("edit effects");
newButton.addActionListener(e -> WorldEditor.this.startEffectEditor());
toolBar.add(newButton);
}
private void startEffectEditor() {
}
private void test() {
String javaHome = System.getProperty("java.home");
String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
String classpath = System.getProperty("java.class.path");
String className = forge.adventure.Main.class.getName();
ArrayList<String> command = new ArrayList<>();
command.add(javaBin);
command.add("-cp");
command.add(classpath);
command.add(className);
command.add("testMap");
ProcessBuilder build= new ProcessBuilder(command);
build .redirectInput(ProcessBuilder.Redirect.INHERIT)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT);
try {
Process process= build.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
void saveBiome()
{
edit.updateTerrain();
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(currentData.biomesNames[list.getSelectedIndex()]);
handle.writeString(json.prettyPrint(json.toJson(edit.currentData, BiomeData.class)),false);
}
void save()
{
currentData.width=width.intValue();
currentData.height=height.intValue();
currentData.playerStartPosX=playerStartPosX.floatValue();
currentData.playerStartPosY=playerStartPosY.floatValue();
currentData.noiseZoomBiome=noiseZoomBiome.floatValue();
currentData.tileSize=tileSize.intValue();
currentData.biomesSprites=biomesSprites.getText();
currentData.maxRoadDistance=maxRoadDistance.floatValue();
currentData.biomesNames= (biomesNames.getList());
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.WORLD);
handle.writeString(json.prettyPrint(json.toJson(currentData, WorldData.class)),false);
}
void load()
{
model.clear();
Json json = new Json();
FileHandle handle = Config.instance().getFile(Paths.WORLD);
if (handle.exists())
{
currentData=json.fromJson(WorldData.class, WorldData.class, handle);
}
update();
}
private void update() {
width.setValue(currentData.width);
height.setValue(currentData.height);
playerStartPosX.setValue(currentData.playerStartPosX);
playerStartPosY.setValue(currentData.playerStartPosY);
noiseZoomBiome.setValue(currentData.noiseZoomBiome);
tileSize.setValue(currentData.tileSize);
biomesSprites.setText(currentData.biomesSprites);
maxRoadDistance.setValue(currentData.maxRoadDistance);
biomesNames.setText(currentData.biomesNames);
for(String path:currentData.biomesNames)
{
Json json = new Json();
FileHandle handle = Config.instance().getFile(path);
if (handle.exists())
{
BiomeData data=json.fromJson(BiomeData.class, BiomeData.class, handle);
model.addElement(data);
}
}
}
}

View File

@@ -1,71 +0,0 @@
# About Forge's Artificial Intelligence
The AI is *not* "trained". It uses basic rules and can be easy to overcome knowing its weaknesses.
The AI is:
- Best with Aggro and midrange decks
- Poor to Ok in control decks
- Pretty bad for most combo decks
The logic is mostly based on heuristics and split between effect APIs and all other ingame decisions. Sometimes there is hardcoded logic for single cards but that's usually not a healthy approach though it can be more justifiable for highly iconic cards.
Defining general concepts of smart play can help improve the win rate much easier, e.g. the AI will always attack with creatures that it has temporarily gained control of until end of turn in order not to miss the opportunity and thus waste the control effect.
If you want to train a model for the AI, please do. We would love to see something like that implemented in Forge.
# AI Matches from Command Line
The AI can battle itself in the command line, allowing the tests to be performed on headless servers or on computers that have poor graphic performance, and when you just don't need to see the match. This can be useful if you want to script testing of decks, test a large tournament, or just bash 100's of games out to see how well a deck performs.
Please understand, the AI is still the AI, and it's limitations exist even against itself. Games can lag and become almost unbearably long when the AI has a lot to think about, and you can't see what's on the table for it to play against. It's best if you set up the tournament and walk away, you can analyze logs later, results are printed at the end.
## Syntax
`sim -d <deck1[.dck]> ... <deckX[.dck]> -D [path] -n [N] -f [F] -t [T] -p [P] -q`
In linux and mac, command line arguments are not currently passed through the sh script, please call `java -jar` manually, instead of the exe.
- `sim` - "Simulation Mode" forces Forge to not start the GUI and automatically runs the AI matches in command line. Enables all other switches for simulation mode.
- `-d <deck1[.dck]> ... <deckX[.dck]>` - Space separated list of deck files, in `-f` game type path. (For example; If `-f` is set to Commander, decks from `<userdata>/decks/commander/` will be searched. If `-f` is not set then default is `<userdata>/decks/constructed/`.) Names must use quote marks when they contain spaces.
- `deck1.dck` - Literal deck file name, when the value has ".dck" extension.
- `deck` - A meta deck name of a deck file.
- `-D [path]` - [path] is absolute directory path to load decks from. (Overrides path for `-d`.)
- `-n [N]` - [N] number of games, just flat test the AI multiple times. Default is 1.
- `-m [M]` - [M] number of matches, best of [M] matches. (Overrides -n) Recommended 1, 3, or 5. Default is 1.
- `-f [F]` - Runs [F] format of game. Default is "constructed" (other options may not work, list extracted from code)
- `Commander`
- `Oathbreaker`
- `TinyLeaders`
- `Brawl`
- `MomirBasic`
- `Vanguard`
- `MoJhoSto`
- `-t [T]` - for Tournament Mode, [T] for type of tournament.
- `Bracket` - See wikipedia for [Bracket Tournament](https://en.wikipedia.org/wiki/Bracket_(tournament))
- `RoundRobin` - See wikipedia for [Round Robin Tournaments](https://en.wikipedia.org/wiki/Round-robin_tournament)
- `Swiss` - See wikipedia for [Swiss Pairing Tournaments](https://en.wikipedia.org/wiki/Swiss-system_tournament)
- `-p [P]` - [P] number of players paired, only used in tournament mode. Default is 2.
- `-q` - Quiet Mode, only prints the result not the entire log.
## Examples
In linux and macos you must run forge by evoking java and calling the jar, currently command line parameters are not passed through the script. The forge jar filename is truncated in these examples from `forge-whatever-version-youre-on.jar` to `forge.jar`.
In Windows, if you use the EXE file as described below, the simulation runs in the background and output is sent to the forge log file only. If you want to have output to the console, please use the `java -jar` evocation of forge.
To simulate a basic three games of two decks (deck1 and deck2 must be meta deck names of decks in `<userdata>\decks\constructed\`):
- Windows/Linux/MacOS: `java -jar forge.jar sim -d deck1 deck2 -n 3`
- Windows: `.\forge.exe sim -d deck1 deck2 -n 3`
To simulate a single 3-player Commander game (deck1, deck2, and deck3 must be meta deck names of decks in `<userdata>\decks\commander\`):
- Windows/Linux/MacOS: `java -jar forge.jar sim -d deck1 deck2 deck3 -f commander`
- Windows: `.\forge.exe sim -d deck1 deck2 deck3 -f commander`
To simulate a round robin tournament; best of three, with all decks in a directory:
- Windows/Linux/MacOS: `java -jar forge.jar sim -D /path/to/DecksFolder/ -m 3 -t RoundRobin`
- Windows: `.\forge.exe sim -D C:\DecksFolder\ -m 3 -t RoundRobin`
To simulate a swiss tournament; best of three, all decks in a directory, 3 player pairings:
- Windows/Linux/MacOS: `java -jar forge.jar sim -D /path/to/DecksFolder/ -m 3 -t Swiss -p 3`
- Windows: `.\forge.exe sim -D C:\DecksFolder\ -m 3 -t Swiss -p 3`
***
Each game ends with an announcement of the winner, and the current status of the match.

View File

@@ -1,192 +0,0 @@
# Advanced Search
Forge implements many ways to help you find the cards you want in your ever growing collection.
Pressing Ctrl+Enter in current search adds another editable search bar.
Here's how searching for all Goblins without Haste-related abilities might look:
![search](search.png)
Click the "X" in the upper right corner of each search widget to remove that filter from the filter stack.
Find-as-you-type is implemented for Deck Editor tables. Just start typing while the table has focus and the next card with a matching string in its name will be highlighted. If more than one card matches, hit Enter to select the next matching card. A popup panel will appear with the search string so you know what you are searching for. If no cards match the string, the string will be highlighted in red. Find-as-you-type mode is automatically exited after 5 seconds of inactivity, or hit Escape to exit find-as-you-type mode immediately.
## Additional information
Another way to filter is using [Scryfall-like syntax](https://scryfall.com/docs/syntax) in the collection search bar.
If no operators are passed between tokens, Forge will assume it is joined by `and`. For example, `t:cat t:warrior t:creature` will search for "creatures that are a cat **and** a warrior". Make sure to use `|` or `or` for your queries, as well as parentheses `( )` when needed.
Keywords can be negated by prefixing a minus sign `-`. For example, `t:creature -t:goblin` will search for "creatures that aren't goblins".
If no keywords are used, Forge will search in their name, type and oracle text for the passed values. For exemple, `(cat | warrior)` will search for cards that has `cat` or `warrior` anywhere in their name, type, or oracle text. Not that it is not bounrd, so it will also match on "catastrophe". This type of search can be negated too. For exemple, `lightning -bolt` will search for card with "lightning and not bolt in their name, types, or oracle text", or `(t:cat | t:warrior) -(orc | angel | phyrexian)` will search for "cat or warrior cards that don't have orc, angel, or phyrexian in their name, types, or oracle text.
## Implemented keywords
### Colors
#### Keyword(s): `color`, `c`
You can find cards that are a certain color using the `c:` or `color:` keyword. Both keywords accepts full color names like blue or the abbreviated color letters `w`, `u`, `r`, `b` and `g`.
You can use many nicknames for color sets: all guild names (e.g. `azorius`), all shard names (e.g. `bant`), all college names (e.g., `quandrix`), all wedge names (e.g. `abzan`), and the four-color nicknames `chaos`, `aggression`, `altruism`, `growth`, `artifice` are supported.
Use `c` or `colorless` to match colorless cards, and `m`, `multi`, or `multicolor` to match multicolor cards.
You can use comparison expressions (`>`, `<`, `>=`, `<=`, and `!=`) to check against ranges of colors.
*Exemples:*
`c:rg` - Cards that are at least red and green
`c!gruul` - Cards that exclusively red and green
`color>=uw -c:red` - Cards that are at least white and blue, but not red
### Card Types
#### Keyword(s): `type:`, `t:`
Find cards of a certain card type with the `t:` or `type:` keywords. You can search for any supertype, card type, or subtype.
Using only partial words is allowed.
*Exemples:*
`t:merfolk t:legend` - Legendary merfolk cards
`t:goblin -t:creature` - Goblin cards that aren't creatures
### Card Text
#### Keyword(s): `oracle:`, `o:`
Use the `o:` or `oracle:` keywords to find cards that have specific phrases in their text box.
You must put quotes `" "` around text with punctuation or spaces.
*Exemples:*
`o:"enters tapped"` - Cards that enter the battlefield tapped
#### Keyword(s): `keyword:`, `kw:`
You can use `keyword:` or `kw:` to search for cards with a specific keyword ability.
> Note: Known to be buggy. You can search by oracle text instead.
*Exemples:*
`kw:flying -t:creature` - Noncreatures that have the flying keyword
#### Keyword(s): `name:`
You can find cards with certain words in their name using `name`.
Supports `!` (exact search), `!=` (doesn't contain), and `:` or `=` (contains).
*Exemples:*
`name!Fire` - The card Fire
`name:Phyrexian`- Cards that contain Phyrexian in their name
#### Keyword(s): `is:vanilla`
Find vanilla cratures (Creatures with no abilities).
### Mana Costs
#### Keyword(s): `manavalue`, `mv`, `cmc`
You can find cards of a specific mana value with `manavalue`, `mv`, or `cmc`, comparing with a numeric expression (>, <, =, >=, <=, and !=).
*Exemples:*
`c:u mv=5` - Blue cards with mana value 5
### Power, Toughness, and Loyalty
#### Keyword(s): `power`, `pow`
You can use numeric expressions (>, <, =, >=, <=, and !=) to find cards with certain power using `power` or `pow`.
*Exemples:*
`pow>=8` - Cards with 8 or more power
`pow>tou c:w t:creature` - White creatures that are top-heavy
#### Keyword(s): `toughness`, `tou`
You can use numeric expressions (>, <, =, >=, <=, and !=) to find cards with certain toughness using `toughness` or `tou`.
*Exemples:*
`tou<=4` - Cards with 4 or less thoughness
#### Keyword(s): `loyalty`, `loy`
You can use numeric expressions (`>`, `<`, `=`, `>=`, `<=`, and `!=`) to find cards with certain starting loyalty using `loyalty` or `loy`.
*Exemples:*
`t:planeswalker loy=3` - Planeswalkers that start at 3 loyalty
### Sets (Editions)
#### Keyword(s): `set:`, `s:`, `edition:`, `e`
Use `s:`, `e:`, `set:`, or `edition:` to find cards using their Magic set code.
Examples:
`e:war` - Cards from War of the Spark
#### Keyword(s): `in:` (set)
The `in:` keyword finds cards that appeared in given set code.
Examples:
`in:lea` - Find cards that once appeared in Alpha.
### Rarity
#### Keyword(s): `rarity:`, `r:`
Use `r:` or `rarity:` to find cards by their print rarity. You can search for `land` (`l`) (usually only basic lands), `common` (`c`), `uncommon` (`u`), `rare` (`r`), `mythic` (`m`), and `special` (`s`). You can also use comparison operators like `<` and `>=.`
Examples:
`r:common t:artifact` - Common artifacts
`r>=r`- Cards at rare rarity or above (rares and mythics)
#### Keyword(s): `in:` (rarity)
You can find cards that have been printed in a given rarity using `in:`
Examples:
`in:rare` - Cards that have been printed at rare.
### Multi-faced cards
#### Keyword(s): `is:split`
Find split-faced cards.
#### Keyword(s): `is:flip`
Find flip cards.
#### Keyword(s): `is:transform`
Find cards that transform.
#### Keyword(s): `is:meld`
Find cards that meld.
#### Keyword(s) `is:leveler`
Find cards with Level Up.
### Others
#### Keyword(s): `is:modal`
Find modal cards.
#### Keyword(s): `is:custom`
Find cards from custom sets.
#### Keyword(s) `is:foil`
Find foil cards.
#### Keyword(s) `is:nonfoil`
Find nonfoil cards.

View File

@@ -1,12 +0,0 @@
# What is Adventure Mode?
Adventure mode is a work-in-progress game mode where you explore the ever-changing landscape of Shandalar, duel creatures to earn gold and new cards to battle the various bosses. You can visit towns to buy equipment and cards, and crawl through dungeons to find artifacts and loot to help you on your journey. Adventure mode is an awesome reimagining of the original "Shandalar" 1997 PC Game in Forge, even though the scope of adventure is much broader and we don't constrain ourselves to the lore of the plane of Shandalar. You can play Shandalar on Desktop (Linux, Windows, IOS) or on Android devices.
# How to run?
1. Step 1: Download the latest version of Forge https://downloads.cardforge.org/dailysnapshots/
2. Step 2: Make sure you have the required version of the JDK (https://www.oracle.com/be/java/technologies/downloads/)
3. Step 3: Launch the adventure mode from the "adventure.exe" file included in the forge version (on android, the adventure version should be embedded into the main Forge program)
4. Step 4: In order to have pictures show up make sure you enable "automatically download missing card images" in the settings. This should automatically download the picture of the card as you encounter them in game.

View File

@@ -1,90 +0,0 @@
Base settings of a plane is configured in
config.json
Example:
```json
{
"screenWidth": 480,
"screenHeight": 270,
"skin": "skin/ui_skin.json",
"playerBaseSpeed": 32,
"minDeckSize": 40,
"starterDecks": [
"decks/starter/white.json",
"decks/starter/black.json",
"decks/starter/blue.json",
"decks/starter/red.json",
"decks/starter/green.json"
],
"restrictedCards": [
"Black Lotus",
"Ancestral Recall"
],
"restrictedEditions": [],
"legalCards":{
"editions": ["M22","M21"]
},
"difficulties": [
{
"name": "Easy",
"startingLife": 16,
"staringMoney": 500,
"enemyLifeFactor": 0.8,
"spawnRank": 0,
"sellFactor": 0.6,
"startItems": [
"Manasight Amulet",
"Leather Boots"
]
},{
"name": "Normal",
"startingLife": 12,
"staringMoney": 250,
"startingDifficulty": true,
"enemyLifeFactor": 1.0,
"spawnRank": 1,
"sellFactor": 0.5,
"startItems": [
"Leather Boots"
]
},{
"name": "Hard",
"startingLife": 8,
"staringMoney": 125,
"enemyLifeFactor": 1.5,
"spawnRank": 2,
"sellFactor": 0.25
}
]
}
```
# Fields:
## **screenWidth**
## **screenHeight**
Logical screen with/height, changing this would require to change all ui elements and won't increase resolution.
## **skin**
path to the used skin for adventure
## **playerBaseSpeed**
base speed of player character
## **minDeckSize**
minimum deck size for matches, decks with lesser cards will be filled with wastes.
## **starterDecks**
string list of all starter decks
## **restrictedCards**
string list of restricted cards, those cards won't appear in random shops or rewards but it it still possible to get those cards if the plane specifically drops it.
## **restrictedEditions**
string list of restricted editions, behaves the same as restricedCards but with editions.
## **difficulties**
list of DifficultyData
## **legalCards**
RewardData for legal cards, behaves similar as restrictedCards only as white list and not black ist.
Also it is defined as RewardData see [Create-Rewards](https://github.com/Card-Forge/forge/wiki/Create-Rewards) for syntax

View File

@@ -1,23 +0,0 @@
In order to edit which sets will show up in random rewards and shop you will need to edit the config file. You can find it here "forgefolder"\res\adventure\common\config.json. Add the sets you want to have restricted to the "restricted edition" section
![image](https://github.com/user-attachments/assets/24ad9686-8ec9-453b-a496-03b33c1719f2)
### Funny
PCEL, PAST, UGL, UNH, HHO, DS0, PHTR, UST, PUST, PH17, PH18, CMB1, UND, PH19, PH20, PH21, UNF, DA1, PH22, MB2
### Commander
COM, CM1, C13, C14, C15, C16, C17, CMA, CM2, C18, C19, C20, ZNC, KHC, C21, AFC, MIC, VOC, NEC, NCC, DMC, 40K, BRC, SCD, ONC, MOC, LTC, CMM, WOC, WHO, LCC, MKC, PIP, OTC, M3C, BLC, DSC, PDC
### Starters
CLU, LTR, CLB, CMD, MB1
### Digital
PRM, MBP, MED, ME2, ME3, TD0, TD1, ME4, VMA, TPR, PZ1, PZ2, ANA, PANA, HA1, HA2, HA3, AJMP, AKR, ANB, KLR, HA4, HA5, J21, YMID, YNEO, YSNC, HBG, EA1, HA6, YDMU, YBRO, EA2, YONE, SIS, SIR, EA3, HA7, YWOE, YMKM, YOTJ, YBLB, YDSK
### Promos
DRC94, PHPR, PLGM, PMEI, PARL, PRED, PTMP, JGP, PSTH, PEXO, PALP, PUSG, PAL99, G99, PULG, PUDS, PPTK, PGRU, PWOR, PWOS, PMMQ, PSUS, PAL00, FNM, G00, PELP, PNEM, PPCY, PINV, PAL01, F01, G01, MPR, PPLS, PAPC, PSDG, PODY, PAL02, F02, G02, PTOR, PJUD, PHJ, PAL03, F03, PJJT, G03, P03, OVNT, PLGN, PONS, PSCG, P8ED, PAL04, F04, G04, P04, PDST, P5DN, PMRD, PCHK, PAL05, F05, G05, PJSE, P05, PMPS, PBOK, PSOK, P9ED, PRAV, P2HG, PAL06, DCI, F06, G06, PJAS, P06, PMPS06, PGPT, PCMP, PDIS, PCSP, PTSP, F07, G07, PMPS07, PRES, PPLC, PPRO, PGPX, PFUT, P10E, PLRW, F08, G08, P08, PMPS08, PMOR, P15A, PSHM, PEVE, PALA, PDTP, F09, G09, P09, PMPS09, PBOOK, PCON, PURL, PARB, PM10, PZEN, PDP10, F10, G10, P10, PMPS10, PWWK, PROE, PM11, PSOM, PDP12, F11, G11, OLGC, P11, PMPS11, PW11, PMBS, PNPH, PM12, PISD, PDP13, F12, PIDW, J12, PW12, PDKA, PAVR, PHEL, PM13, PRTR, PDP14, F13, J13, PGTC, WMC, PDGM, PM14, PSDC, PTHS, PDP15, F14, J14, PBNG, PJOU, PCNS, PS14, PPC1, PM15, PKTK, F15, J15, UGF, PFRF, PDTK, PTKDF, PS15, PORI, PSS1, PBFZ, F16, J16, POGW, PSOI, PEMN, PKLD, PS16, F17, J17, PAER, PAKH, PHOU, PS17, PXLN, PSS2, PXTC, J18, PRIX, PNAT, PDOM, PM19, PSS3, PS18, PGRN, PRWK, G18, PF19, PRNA, PRW2, J19, PWAR, PMH1, PM20, PPP1, PS19, PWCS, J20, PF20, PLG20, PL21, PW21, PLG21, Q06, P22, PL22, PW22, GDY, PLG22, SCH, PSVC, P30M, P30A, P30H, PRCQ, BOT, PEWK, P23, PR23, PW23, PL23, SLP, PF23, P30T, PMDA, REX, PF24, PW24, PL24, PSS4, PCBB, PLG24, PLTC, PF25, PSPL, PJSC
### Universes Beyond
40K,SLD,ACR,PCBB,PIP,REX,WHO,LTR,LTC,BOT,HBG,CLB,AFR

View File

@@ -1,43 +0,0 @@
Forge provides an in-game console in adventure mode.
You can access (and close) the console while exploring by pressing F9 (or Fn-F9).
To scroll the console window, click and drag the text box.
## Available commands
| Command Example | Description |
| -- | -- |
| resetMapQuests | Resets the map quests, resulting in all side-quest progress being lost and all side-quest types being re-picked |
| give gold 1000 | Give 1000 gold |
| give shards 1000 | Give 1000 shards |
| give print lea 232 | Add an alpha (LEA set code) black lotus (232 collector number) |
| give item <item name or code?> | Adds an in game item such as leather boots |
| give set sld | Give 4 copies of every card in the Secret Lair Drop (set code SLD), flagged as having no sell value |
| give nosell card forest | Gives a forest with no sell value |
| give boosters leb | Add a booster from beta (LEB set code) |
| give quest 123 | Add the quest by its number ID |
| give life 10 | Add 10 life to yourself |
| give card forest | Adds a forest to your inventory |
| debug collision | Displays bounding boxes around entities |
| debug map | TODO |
| debug off | Turns off previously enable debugging |
| teleport to 6000 5000 | Moves you 6000 tiles east and 5000 tiles north from the left bottom corner |
| fullHeal | Returns your health back to baseline |
| sprint 100 | Increases your speed for 100 seconds |
| setColorId R | Sets the player color identity; Probably used for testing and shops |
| clearnosell | Clears the no sell value flag from all cards you own that are not used in a deck |
| remove enemy abc | Remove the enemy from the map with the map ID abc |
| remove enemy all | Remove all the enemies from the map |
| dumpEnemyDeckList | Print the enemy deck lists to terminal output stream |
| getShards amount 100 | Similar to give shards command; Gives 100 shards to the player |
| resetQuests | Resets the world quests. In progress quests are not abandonned or reset including dungeons. Does not reroll abandoned quests. Not really sure what this does. |
| hide 100 | Enemies do not chase you for 100 seconds |
| fly 100 | You can walk over obstacles for 100 seconds |
| crack | Cracks a random item you are wearing |
| spawn enemy Sliver | Spawns a Sliver on your screen |
| listPOI | Prints all locations in terminal output stream as ID-type pairings |
| leave | Gets you out of the current town/dungeon/cave |
| dumpEnemyColorIdentity | Prints all enemies, their colour affinity and deck name to terminal output |
| heal | Recover your full health |
| dumpEnemyDeckColors | Prints all decks available to enemies and their affinities |

View File

@@ -1,107 +0,0 @@
All Enemies are stored under `res/<AdventureName>/world/enemies.json`
Enemies spawned on the overworld map or on map stages will use this exact template to define their base behavior. These values can be modified or added to with additional settings on an individual enemy basis, details of which can be found within [map instance](Create-new-Maps.md).
Some ideas for custom enemy cards:
- basic (CR or at least Alchemy conform) effects for normal mobs to add flavor
- advanced + all UN stuff. but their mechanics could be combined in new ways not seen in print
- boss/rare ones that are balanced around meta mechanics (=quests)
The Json file contains an Array of Objects and each Object is one enemy.
EnemyObject:
```json
{
"name": "Challenger 20",
"nameOverride": "Challenger",
"sprite": "sprites/monsters/doppelganger.atlas",
"deck": [
"decks/challenger/challenger_20_allied_fires.dck",
"decks/challenger/challenger_20_cavalcade_charge.dck",
"decks/challenger/challenger_20_final_adventure.dck",
"decks/challenger/challenger_20_flash_of_ferocity.dck"
],
"ai": "",
"randomizeDeck": true,
"spawnRate": 0.25,
"difficulty": 0.25,
"speed": 28,
"life": 22,
"rewards": [],
"colors": "UBRWG",
"questTags": [
"Challenger",
"IdentityUnknown",
"BiomeGreen",
"BiomeRed",
"BiomeColorless",
"BiomeWhite",
"BiomeBlue",
"BiomeBlack"
]
}
```
# Fields:
## **name**
String - Has to be unique
Name of the enemy, every time an other object will use an enemy, it will refer to this name.
## **nameOverride**
String - If provided, this will be displayed in any references to this enemy in the game. If not provided, Name will be used.
## **sprite**
String - Path to the sprite atlas for the enemy (from `res/<AdventureName>`)
In fights against the enemy, the sprite under "Avatar" will be used as avatar picture.
Every sprite under
"Idle","Walk","Attack","Hit","Death"
will be used as animation for the corresponding action.
direction can be added to alter the animation depending on the direction like "IdleRight"
Supported directions are "Right","Left","Up","Down","RightDown","LeftDown","LeftUp","RightUp"
## **deck**
Array of strings containing paths to the decks used for this enemy (from `res/<AdventureName>`)
If no decks are defined then the enemy will act like a treasure chest and give the rewards without a fight.
(only for enemies in dungeons)
The format for the deck file can be the normal forge *.dck syntax or a json file that will behave like a collection of [rewards](Create-Rewards.md) to get a random generated deck.
## **randomizeDeck**
Boolean - if true then the enemy deck will be randomly selected from the deck array. If false, an algorithm will select a deck in sequential order based on the player's prior win/loss ratio against that opponent (discouraged and currently unused due to wild swings in ratio at low game count).
## **ai**
String - Currently unused, this appears to be intended to allow different playstyles to be associated with this enemy.
## **boss**
Boolean - Not used to any great extent at this time, but a value of true in this field indicates that this is a boss-level enemy for which the match presentation can be changed if desired. Currently, this causes capable Android devices to vibrate for a longer period on collision with the enemy sprite.
## **flying**
Boolean - If true, this enemy ignores terrain collisions and can travel freely in their intended movement direction.
## **spawnRate**
Decimal - Relative frequency with which this enemy will be picked to spawn in appropriate biomes (which are set in the biome json file). Existing values range from 0 to 1.0.
## **difficulty**
Decimal - Relative estimated difficulty associated with this enemy. Currently unused, but will likely be factored in as a part of filtering enemies into early/late game appropriate opponents. Existing values range from 0 to 1.0.
## **speed**
Integer - Movement speed of this enemy in overworld or on a [map instance](Create-new-Maps.md). For comparison, the player's base speed is set at a value of 32 (before any equipment / ability modifiers).
## **scale**
Decimal - Default 1.0. For enemies whose sprites are too large or small for their intended usage, this serves as multiplier for the enemy's visual dimensions & collision area. By default, we work with 16x16 pixel sprites for most entities - this can be replicated with a more detailed 32x32 sprite by setting a scale of 0.5 for the enemy entry.
## **life**
Integer - Base starting life total. This is modified universally by a value determined by the player's chosen difficulty, and can be adjusted further at the enemy object level on [map instances](Create-new-Maps.md).
## **rewards**
Array - A collection of the rewards to be granted for defeating the enemy.
see [Create Rewards](Create-Rewards.md) for the syntax.
## **equipment**
Array - A collection of strings representing [equipment items](adventure-items) normally intended for player use that this enemy will have. Not used widely, usually when an enemy will drop that [equipment](adventure-items) and it does not use [mana shards](mana-shards).
## **colors**
String - Any combination of "B" (Black), "U" (Blue), "C" (Colorless), "G" (Green), "R" Red, and "W" (White). Used to display color identity alongside the sprite when an active ability allows it.
## **questTags**
Array- A collection of strings associated with this entity for filtering in regards to quests.

View File

@@ -1,178 +0,0 @@
Rewards represent anything that can be obtained by the player in Adventure Mode. Cards are the most common type of reward, but rewards can also represent a pieces of equipment, gold, mana shards, maximum life increases, keys, or generic items to be interacted with via dialogs and quests.
Rewards are associated with...
* Enemies - The loot they drop when defeated.
* Lootable items - Treasure chests, piles of gold, spellbooks, or mana shards on dungeon maps.
* Shops - The items available for purchase.
* Dialog - Items given to the player during a dialog sequence OR required in order to have access to an option.
* Quests - Rewards for completion of a quest.
* Events - Rewards for completion of an event.
As a multipurpose concept, Reward data has a large number of fields, but almost all are optional and no one reward will use all of the fields.
The expected fields and behavior for the rewards are set based on the reward's `type` value. This is the one field that all reward data must contain, as it tells the game what other fields to expect data in and how to process them.
The simplest types are `gold` (give the player X gold), `shards` (give the player X [mana shards](mana-shards), and `life` (increase the player's current and maximum life total by this amount [given very sparingly]). Beyond their types, these three items only require a `count` field. Optional fields for these types include `probability` and `addMaxCount`, (see full list at bottom). An example of a `gold` reward that will grant the player 50 gold follows:
```json
{
"type": "gold",
"count": 50
}
```
`Item` rewards are slightly more specific, but still relatively simple, requiring `type`, `count`, and EITHER `itemName` or `itemNames`, examples of both follow. Optional parameters again include `probability` and `addMaxCount`. If `itemName` is specified, then `count` copies of the named item will be granted to the player. If `itemNames` is specified, `count` copies of items randomly selected from the list of named items provided will be granted instead.
```json
{
"type": "item",
"count":1,
"itemName": "Mithril Boots"
}
```
```json
{
"type": "item",
"count":1,
"itemNames": ["Mithril Boots", "Mithril Shield", "Mithril Armor"]
}
```
`Card` rewards are potentially the most complex from a definition standpoint, but almost all of the fields are optional. A simple and a complex example follow below.
Granting the player one completely random card that is legal for use in Adventure:
```json
{
"type": "card",
"count":1
}
```
As a contrasting and complicated example that will return no matching results, consider the card reward data below. 80% of the time, this will try to grant the player 1-9 (modified further by difficulty) cards that are rare, multicolor, contain both red and blue in their color identity, are from M21 or M22 editions, are either instants or creatures, have the Elf subtype, contain the word "dragon" in the card name, contain the word "destroy" and/or "exile" in the card text. Details on the individual fields can be found in the reference list at the bottom of this page.
```json
{
"type": "card",
"probability": 0.8,
"count":1,
"addMaxCount":8,
"colors": ["red","blue"],
"rarity": ["rare"],
"editions": ["M22","M21"],
"cardTypes": ["Instant","Creature"],
"colorType": "MultiColor",
"subTypes": ["Elf"],
"superTypes" :["Legendary"]
"cardName": "dragon",
"cardText": "destroy|exile"
}
```
`Union` reward types are a purely structural element. This reward type does nothing on its own, but it is used as wrappers for multiple `card` instances that has distinctly separate parameters. Required elements are `type` (Union), `count`, and `cardUnion` (which contains additional `card` reward definitions). Optional parameters are, once again `addMaxCount` and `probability`. As an example, the following would award the player with a single red dragon from M21 OR a single green sorcery from M22, but never a red sorcery, M22 dragon, or so on. The individual card pools are conjoined, giving an equal chance of all possible cards across the pools.
```json
{
"count":1,
"type":"Union",
"cardUnion": [
{
"count":1,
"editions": ["M21"],
"subTypes": ["Dragon"],
"colors": ["red"]
},
{
"count":1,
"editions": ["M22"],
"cardTypes": ["Sorcery"],
"colors": ["green"]
}
]
}
```
# Fields:
## **type**
Defines the type of reward.
Valid options are:
* `gold` will reward the player with gold.
* `life` will increase the maximum life of the player.
* `shards` will reward the player with [mana shards](mana-shards).
* `item` will give items to be added to the player's inventory.
* `card` will create one or more cards matching a detailed set of filters to follow.
* `union` is a wrapper for multiple `card` instances that can have mutually exclusive filters.
* `deckCard` is only used with rewards from [enemies](Create-Enemies.md), this functions as a `card` reward that is limited to cards found in that enemy's deck.
`{"type": "card", ...}`
## **probability**
The probability of this reward being given out, on a decimal scale from 0 to 1. (Defaults to 1 if not provided)
`{..., "probability": 0.5, ...}`
## **count**
If given at all (see `probability`, above), get at least this many of the reward.
`{..., "count": 10, ...}`
## **addMaxCount**
If given at all, an additional "addMaxCount" instances of the reward will be added. A pre-set multiplier based on the chosen difficulty is applied to this parameter. On normal difficulty, you will get the reward "count" to "addMaxCount" times, `{"type": gold", "count":10, "addMaxCount":5"}` would give anywhere from 10 to 15 gold. (Defaults to 0 if not provided)
`{..., "addMaxCount": 5, ...}`
## **colors**
An array of the possible colors for `card` and `deckCard`.
`{..., "colors": ["red", "black"], ...}`
## **rarity**
An array of the possible raritys for `card` and `deckCard`.
`{..., "rarity": ["basicland", "common", "uncommon", "rare", "mythicrare"], ...}`
## **editions**
An array of the possible editions for `card` and `deckCard`, referenced by their 3 character set code.
`{..., "editions": ["ONE", "MOM", "MAT"], ...}`
## **cardTypes**
An array of the possible cardTypes for `card` and `deckCard`.
`{..., "cardTypes": ["Creature", "Artifact", "Enchantment", "Instant", "Sorcery", "Planeswalker", "Battle"], ...}`
## **subTypes**
An array of the possible subTypes for `card` and `deckCard`.
Usually used for creature types, but can also denote names of planeswalkers, types of battles, etc.
`{..., "subTypes": ["Elf", "Dragon", "Gideon", "Urza", "Siege", "Tribal"], ...}`
## **superTypes**
An array of the possible superTypes for `card` and `deckCard`.
`{..., "superTypes ": ["Legendary", "Basic", "Snow"], ...}`
## **cardName**
The exact name of a `card` to use.
`{..., "cardName": "Llanowar Elves", ...}`
## **cardText**
A regular expression defining text that must appear on the card. A sequence of plain text alphanumeric (A-Z, 0-9) characters can function here if you are unfamiliar with regular expressions, but many special characters will change the functionality. Useful for denoting keywords or identifying helper cards that are associated with but do not actually have a given type.
`{..., "cardText": "reveal the top card of", ...}`
`{..., "cardText": "cast (an instant|a sorcery) from your hand", ...}`
## **deckNeeds**
This is a functional but partially implemented concept at best, as the result set is currently limited. Card scripts can be tagged as having certain attributes that are used for creating synergies in decks constructed by the game. If/when this feature is expanded to more of our script library, the using those tags will become more and more useful to use for defining rewards so as to allow "concepts" to be awarded as opposed to specific card names or text contents.
`{..., "deckNeeds": ["Ability$Graveyard"], ...}`
## **cardPack**
This element should not be seen or used in JSON definitions of reward data. `cardPack` rewards are a type that represent a collection of cards whose contents have been pre-determined in game logic (such as a drafted deck that can be kept after an event), but that were not pre-determined at the time when the reward data was written. To manually implement granting specific or randomized cards we should use one or more `card` rewards, detailed above. When granted, this will create an item in the player's inventory that can subsequently be opened by the player to award the individual card contents of the associated deck.

View File

@@ -1,42 +0,0 @@
Maps in the adventure mode are created with the tool Tiled
[](https://www.mapeditor.org/)
Open the tiled-project under `<adventure>/maps/main.tiled-project`
![Screenshot_2021-09-01_140346](https://user-images.githubusercontent.com/8047400/162841209-c9c7aae3-b21d-47ab-9bd4-c9c79fb511bf.png)
This will allow you to edit the maps and tile sets.
To interact with the player, objects needs to be added to the Objects layer.
Objects templates are stored in the "obj" folder, but are not necessary.
Impotent are the types of the object and his properties.
## Object types
# enemy
will spawn an Enemy on the map. On collide with the player a magic duel will be started.
If the player win, the enemy will be removed from the map and the player will get the reward.
If the player loose, then the player will move 1 step back and receive the standard penalty.
Loot is also defined as enemy without a deck, then the player will receive the reward right away.
Properties:
`enemy` name of the enemies
# shop
Will spawn an shop on the map. On collide the player will enter the shop.
Properties:
`shopList` List of possible shop, leave it empty for all shops.
`signXOffset` x offset for the shop sign.
`signYOffset` y offset for the shop sign.
# inn
Will spawn an inn the map. On collide the player will enter the inn.
Properties:
# entry
Will be used as the map entry and exit. On collide the player will be teleported to an other map or the over world.
Properties:
`direction` the position where to spawn. up means the player will be teleported to the upper edge of the object rectangle.
`teleport` The map where the player gets teleported. If the property is empty, then the player will be teleported to the over world.
`teleportObjectId` the object id where the player will be teleported. If empty then it will search for an entry object, that would teleport the player back to the source map.

View File

@@ -1,46 +0,0 @@
There are many currencies in the game, and most of them can be interchanged.
# Cards
Acquired by:
- World drops
- Match reward
- Draft wins
- Shop purchases
- Quests
Spent on:
- Selling to shops
# Gold
Acquired by:
- World drop
- Match reward
- Draft reward
- Quests
Spent on:
- Cards and items from shops
- Drafts games
- Crafting cards
- Shards
# Shards
Acquired by
- World drop
- Quest reward
Spent on
- Crafting cards
- Shop Re-rolls
- Gold exchange
# Challenge Coins
Acquired by
- At the start of the game
Spent on
- Drafts

View File

@@ -1,28 +0,0 @@
# Adding basic lands and special arts
You can add lands by clicking the triple dots icon in the right top of the deck building interface.
Initially you only have access to jumpstart basic land arts - to get more, you need to purchase the landscape sketch books from the basic land shop (The Cartographers Guild).
# 40-card deck recommendation
40-card decks give you a much more predictable curve.
In a 40-card deck, each individual card has a 2.5% chance of being drawn.
In a 60-card deck, each individual card has a 1.6% chance of being drawn.
When you use a smaller deck, the significance of each individual card is much higher and will give you more predictable performance.
# Autosell
When you click a card that is not part of any decks, you get the option to move it to auto-sell.
The numbers in the interface indicate how many copies you have.
To sell the cards you have marked, go to any town and enter the Inn.
In the Inn, the middle icon of the coin called Sell (E) will sell all your cards you have marked to sell.
Individual card values vary by rarity and reputation with the town you sell them in.
Since dying will cause you to lose a percentage of your gold, you may want to not sell all your cards immediately.

View File

@@ -1,40 +0,0 @@
# The Different Planes of Adventure Mode
In Adventure Mode, you can select different planes (AKA worlds) to experience. The default experience is the Shandalar plane, and is the basis of gameplay found in all the others. The other planes are community projects, and are in various states of completion. Below there is a list of each plane in the game (as of this writing), and it's status.
To change planes, open up Adventure Mode. On the main screen where you can either load a game or start a new one. Go to the Settings menu, and you will find a drop down on the top right with a list of the planes currently available. When changing planes, you will need to restart Forge to apply the change. Save files are unique to each plane, and will load back in when you change to the appropriate plane.
## Amonkhet
A plane set in the world of Amonkhet, and is intended to have new quests, story, enemies, etc. based on that Magic The Gathering plane.
**STATUS:** Very broken. Not recommended for play, unless you want to dip your head in and see a very different overworld map. Pretty much nothing else works properly. _You have been warned._
## Crystal Kingdoms
A plane with a focus on the Final Fantasy cards. Intended to have a unique story, quests, enemies, etc. based on the Final Fantasy games.
**STATUS:** Very pre-alpha. The plane 'works' but is almost identical to the default Shandalar plane. The big differences is new starting decks, and the card rewards in shops and from enemies, have a stronger Final Fantasy bent than usual.
## Innistrad
A plane set in the lore and world of Magic the Gathering's Innistrad plane. The card pool and enemies are limited to Innistrad, with some minor exceptions. There is also a distinctly different overworld, story, enemies, etc. Every map is new and unique compared to Shandalar.
**STATUS:** Technically alpha. Everything is 'functional' in that it is all closed off and separate from the Shandalar plane. The enemies are new, the quests include some base and some new (though the tutorial has some strong similarities). The shops are new-ish (they currently use the base shops, but with a limited card pool to just the plane. This is actively being changed to entirely new shops however, and should become notable in the next few updates.) Every town, dungeon, and cave is unique to Innistrad and been custom built for the plane. Every enemy is also new to Innistrad.
_HOWEVER_: Everything is still very early in design. Most of the biomes are empty. The new quests are limited. The shops under work. The story ends partway through the tutorial. The first bosses are being worked on. The enemies are few in number, but growing rapidly in development. Etc. This plane is best for short plays to see what is going on, but don't expect to spend much time here, Yet. That said, it is the community plane receiving the most work and most consistent updates. With changes coming at the minimum, monthly, preferably weekly.
In it's current state, expect to find bugs. if you do, please report them to Shenshinoman in the Forge Discord.
## Shandalar
The base plane, where all the magic happens, and where the dev team members working on Adventure generally focus their efforts. The story is mostly complete, but there _is_ further work planned. Everything works, and you can easily spend dozens of hours, or more, in this plane.
**STATUS:** Fully functional and live. Enjoy!
## Shandalar Old Border
So, you like the Shandalar plane, but miss "ye good olde days" of yore? Then Shandalar Old Border is for you. This is the Shandalar base plane, but with modifications so that everything is from the Scourge set, or older.
**STATUS:** 99% functional and live. Enjoy! (There may be the occasional bug found. if so, please report it so we can get it resolved.)

View File

@@ -1,82 +0,0 @@
## Story Dungeons
Currently, the story is very much a work in progress and will change a lot in the future. The end goal will consist of multiple "tiers" of more difficult dungeons, where you will be guided by quests. Currently, only some of the story dungeons are available, and there are no quests to guide you to them. You can enter these dungeons and fight the bosses.
### Tier 1: Castles
The first tier of story bosses will consist of five dungeons, each found in the center of the five colored biomes. Each boss gives a plethora of loot (cards, life, shards, gold) and if you are able to defeat all five bosses, you can get a Mox card of your choice.
**Overworld Icons**
![image](https://github.com/Card-Forge/forge/assets/67333662/f4d0c782-160f-4770-bb7d-788ae889c080)
### Tier 2: Planeswalker Temples
Currently only the Temple of Chandra (red biome) and Temple of Liliana (black biome) are available. These tend to be harder than the castles mentioned above.
**Overworld Icons**
![image](https://github.com/Card-Forge/forge/assets/67333662/86b38d26-7e8d-449e-a6b5-9bee49270074)
## Generic Dungeons
### Aerie
### Barbarian Camp
Comes in Goblin, Orc, Bandit and Kobold variaties
### Cat Lair
### Caves
### Crypt
### Djinn Palace
### Evil Grove
### Factory
### Fort
### Graveyard
### Grove
### Farm
### Hostile Towns
### Lavaforge
### Magetower
### Maze
### Merfolk Pool
### Monastery
### Phyrexian Outposts
![image](https://github.com/Card-Forge/forge/assets/67333662/6ff4620f-e740-414b-9365-80c044cf884a)
### Plains Castles
### Skull Cave
### Snow Abbey
### Vampire Castle
### Demon Tower
## Sidequest Dungeons
### The Skep
#### Location: Island Biome
### Forest of Garruk
#### Location: Forest Biome
### Hydra´s Lair
#### Location: Forest Biome
### Outpost of Jace
#### Location: Island Biome
### Unhallowed Abbey
#### Location: Plain Biome
### Nahiri's Encampment
#### Location: Plain Biome
### Tibalt's Fortress
#### Location: Mountain Biome
### City of Zedruu
#### Location: Mountain Biome
### Old Sewers
#### Location: Waste Biome
### Xira's Hive
#### Location: Waste Biome
### Slobads Factory
#### Location: Waste Biome
### Grolnoks Bog
#### Location: Swamp Biome
### Slimefoot's Swamp
#### Location: Swamp Biome
### Teferi's Hideout
#### Location: Island Biome
### Kiora's Island
#### Location: Island Biome
### Sorin's Dungeon
#### Location: Swamp Biome

View File

@@ -1,562 +0,0 @@
# Quick Links
1. [Neck item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#neck)
1. [Left item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#left)
1. [Right item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#right)
1. [Body item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#body)
1. [Boot item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#boots)
1. [Ability 1 item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#ability1)
1. [Ability 2 item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#ability2)
## NECK
#### Dark Amulet
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Swamp Capital Cities
1. **Price** : 3000 gold
![Necropolis of Azar fullborder](https://github.com/Card-Forge/forge/assets/67333662/38324a83-aa57-4aa4-89aa-007cbf76d0a2)
#### Hallowed Sigil
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Monastery
![Hallowed Sigil fullborder](https://github.com/Card-Forge/forge/assets/67333662/74052967-d8da-4d79-9999-6fbc903ef3db)
#### Jeweled Amulet
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Price** : 1000 gold
#### Life Amulet
1. **Effect** : +2 starting life
1. **Location** : Capital Cities
1. **Price** : 4000 gold
#### Manasight Amulet
1. **Effect** : Grants Manasight, letting you know the colors used by your adversaries.
1. **Location** : Capital Cities
1. **Price** : 1000 gold
#### Phoenix Charm
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/9c917d2c-c30d-4638-92ff-2df40120599c)
1. **Location** : Chandra's Temple
![Phoenix Charm fullborder](https://github.com/Card-Forge/forge/assets/67333662/2d8bcdbd-c020-4ef9-b1cc-20240e197f6e)
#### Piper's Charm
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Price** : 4000 gold
![Piper's Charm fullborder](https://github.com/Card-Forge/forge/assets/67333662/ca4a4270-6e2e-4a80-bf76-19fa99d66c96)
#### Sorin's Amulet
1. **Effect** : Extra card starting in your battlefield zone. (Sorin's Amulet)
Starting life Modifier: +2
1. **Location** : Sorin
#### Traveler's Amulet
1. **Effect** : Extra card starting in your battlefield zone. (Traveler's Amulet)
1. **Location** : Blue Capital City
1. **Price** : 5000 gold
#### Xira's Fancy Hat
1. **Effect** : +2 starting life + Extra card starting in your command zone.
1. **Location** : Xira's Hive
![Xira's Hive fullborder](https://github.com/Card-Forge/forge/assets/67333662/5e852d00-60af-4a19-ad0b-3659d751cd19)
## LEFT
#### Axt
1. **Effect** : Extra card starting in your battlefield zone.
1. **Cost** : 2500
1. **Location** : Capital Cities
![Bonesplitter fullborder](https://github.com/Card-Forge/forge/assets/67333662/f83eebc9-4e6b-4f43-a1f1-c53396cffcf8)
#### Battle Standard
1. **Effect** : Extra card starting in your battlefield zone. and -1 starting life total
1. **Location** : Goblin encampments
![r_1_1_goblin_ema](https://github.com/Card-Forge/forge/assets/67333662/75e130fa-5bd1-4136-8c97-e1e3557b6222)
#### Bronze Sword
1. **Effect** : Extra card starting in your battlefield zone.
1. **Cost** : 2500
1. **Location** : Waste towns
![thb-232-bronze-sword](https://github.com/Card-Forge/forge/assets/67333662/e58c82dd-3e70-4651-9620-d0e458c22ec1)
#### Chandra's Tome
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/e88b95ce-dd83-48ed-bf77-df997b71bbb6)
1. **Location** : Chandra's Temple
![Chandras Tome fullborder](https://github.com/Card-Forge/forge/assets/67333662/196a5f91-c51c-4dc4-afe6-9bb847ebffb7)
#### Dagger
1. **Effect** : Extra card starting in your battlefield zone.
1. **Cost** : 2500
1. **Location** : Capital Cities
![afr-250-spare-dagger](https://github.com/Card-Forge/forge/assets/67333662/99a9bcd4-a486-42af-846c-82b34456ce36)
#### Disrupting Scepter
1. **Effect** : Extra card starting in your battlefield zone. (Disrupting Scepter)
1. **Cost** : 2500
1. **Location** : Green Capital City Arena
#### Farmer's Tools
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Price** : 6000 gold
![Farmer's Tools fullborder](https://github.com/Card-Forge/forge/assets/67333662/93924eb8-c7d8-4ff6-9993-5489bef9e42e)
#### Flame Sword
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Cost** : 3500
![Flame Sword fullborder](https://github.com/Card-Forge/forge/assets/67333662/92ba9d44-daad-44c8-ab42-beffb2a8c99a)
#### Garruk's Mighty Axe
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/47041427-b5d3-4faf-8f7b-1e1e004fa8ab?as=visual&with=usd)
Starting life Modifier": +3
1. **Location** : Garruk Forest
![Garruk's Mighty Axe fullborder](https://github.com/Card-Forge/forge/assets/67333662/e8db8194-9d3b-44c3-a417-9ee9121686cf)
#### Giant Scythe
1. **Effect** : Extra card starting in your command zone.
Life Modifier: + 1
1. **Location** : Scarecrow Farm
![Giant Scythe fullborder](https://github.com/Card-Forge/forge/assets/67333662/3b9bb726-ee93-453b-a2d3-c64ee768ed72)
#### Heart-Piercer
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Capital Cities
![cmm-390-heart-piercer-bow](https://github.com/Card-Forge/forge/assets/67333662/0800bc84-185e-401b-a570-4410693fd25a)
#### Heavy Arbalest
1. **Effect** : Extra card starting in your battlefield zone. (Heavy Arbalest)
1. **Cost** : 1500
1. **Location** : Red Capital City
#### Hivestone
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Skep
![Hivestone fullborder](https://github.com/Card-Forge/forge/assets/67333662/1bdbc48b-ef3e-43a4-99fb-fbdc7861eb98)
#### Kiora's Bident
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/7fdd8d06-c0c5-487e-a7e7-3050f6d19bed)
Starting life Modifier": -1,
Extra card starting in your battlefield zone: Kraken Hatchling
1. **Location** : Kiora Island
![Kiora's Bident fullborder](https://github.com/Card-Forge/forge/assets/67333662/bcc003e2-5195-41a3-a277-176ed0d123ec)
![bbd-121-kraken-hatchling](https://github.com/Card-Forge/forge/assets/67333662/b5ae9dd9-7e7b-4cfe-9357-09e8d8c33af0)
#### Mad Staff
1. **Effect** : Extra card starting in your battlefield zone. (Power Struggle)
1. **Location** : Black Capital City
1. **Price** : 1000 gold
#### Nine-Ringed Bo
1. **Effect** : Extra card starting in your battlefield zone. (Nine-Ringed Bo)
1. **Location** : White Capital City
1. **Price** : 1000 gold
#### Presence of the Hydra
1. **Effect** : Extra card starting in your command zone. [Spellbook](https://scryfall.com/@Simidhimi/decks/86798768-3af5-49c0-a120-feb62e1a0e95)
1. **Location** : Hydra boss
![Presence of the Hydra fullborder](https://github.com/Card-Forge/forge/assets/67333662/0b06d4ea-8e10-46be-b7c4-3aecaf9b6d32)
#### Sleep Wand
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Price** : 4000 gold
![Sleep Wand fullborder](https://github.com/Card-Forge/forge/assets/67333662/37b23723-ad6a-4bff-8919-ab029af23c8d)
#### Slimefoot's Slimy Staff
1. **Effect** : Extra card starting in your command zone.
Life Modifier: + 2
1. **Location** : Slimefoot's Swamp
![Slimefoot's Slimy Staff fullborder](https://github.com/Card-Forge/forge/assets/67333662/89436738-210e-4e41-b3d3-01d3975ad2e3)
![vma-293-bayou](https://github.com/Card-Forge/forge/assets/67333662/c3bfd5f7-ee72-4784-a7f2-59a7780007c3)
#### Spell Book
1. **Effect** : Starts with an extra card in your hand.
1. **Location** : Capital Cities
1. **Price** : 3000 gold
#### Steel Sword
1. **Effect** : Extra card starting in your battlefield zone.
1. **Cost** : 4500
1. **Location** : Waste towns
![gn3-116-greatsword](https://github.com/Card-Forge/forge/assets/67333662/3ee57c21-0462-48f7-9a15-a25d361a7465)
#### Teferi's Staff
1. **Effect** : Extra card starting in your command zone.
Life Modifier: + 1, Card reward bonus +1
1. **Location** : Teferi Hideout
![Teferi's Staff fullborder](https://github.com/Card-Forge/forge/assets/67333662/7ce2d17c-4327-4705-b23e-aff2e49ffa70)
#### The Underworld Cookbook
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Zedruu's City
![mh2-240-the-underworld-cookbook](https://github.com/Card-Forge/forge/assets/67333662/2fdb3032-8745-4b77-a78e-b4f405a4719d)
#### Tibalt's Bag of Tricks
1. **Effect** : Extra card starting in your command zone.
[Tibalt's Devils](https://scryfall.com/@Simidhimi/decks/1d5cc465-ac4a-496f-bac1-dc313c540375)
[Tibalt's Spells](https://scryfall.com/@Simidhimi/decks/8267bb6a-a226-4ec2-946e-a419372b97dc)
[Tibalt's Spellbook](https://scryfall.com/@Simidhimi/decks/b2d248bc-e16f-49fe-94dc-28ac2b6ab5b5)
Life Modifier: + 1
1. **Location** : Tibalt's Fortress
![Tibalt's Bag of Tricks fullborder](https://github.com/Card-Forge/forge/assets/67333662/ac9732a4-6276-45b2-bcae-68a9bed4a5f8)
#### Unerring Sling
1. **Effect** : Extra card starting in your battlefield zone. (Unerring Sling)
1. **Cost** : 4000
1. **Location** : Green Capital City
#### Wood Bow
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Capital Cities
![ice-318-fyndhorn-bow](https://github.com/Card-Forge/forge/assets/67333662/f42b169f-09ca-40d8-be04-6f21829e897c)
#### Zedruu's Lantern
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/bdaa39ed-b4b3-4157-aa85-98e313750dd0)
Starting life Modifier: +1
1. **Location** : Zedruu's City
![Zedruu's Lantern fullborder](https://github.com/Card-Forge/forge/assets/67333662/ac92c5ba-a994-41d8-a6a6-e169a70fa500)
## Right
#### Aladdin's Lamp
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Capital Cities
1. **Price** : 3000 gold
![arn-56-aladdin-s-lamp](https://github.com/Card-Forge/forge/assets/67333662/bb557cfc-342d-4545-823e-7e02c2a057a0)
#### Aladdin's Ring
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Capital Cities
1. **Price** : 3000 gold
![9ed-286★-aladdin-s-ring](https://github.com/Card-Forge/forge/assets/67333662/764b5d4f-80a9-4248-9e65-745b0e5a8401)
#### Chicken Egg
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Secret :)
![ugl-41-chicken-egg](https://github.com/Card-Forge/forge/assets/67333662/9af1d091-a3a4-4eb3-83bb-3bb34eaf0c92)
#### Cursed Ring
1. **Effect** : 3 cards starting in **your opponents** battlefield zone.
1. **Location** : Capital Cities
1. **Price** : 3000 gold
![tok](https://github.com/Card-Forge/forge/assets/67333662/1bf9f4ad-0030-4ea6-9905-f6764fbe278d)
#### Cursed Treasure
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Price** : 4000 gold
![Cursed Treasure fullborder](https://github.com/Card-Forge/forge/assets/67333662/27175670-7feb-4cbf-a98c-a55c1f38d4a9)
#### Dark Shield
1. **Effect** : Extra card starting in your battlefield zone. (Barrier of Bones)
Lifemodifier: -3
1. **Location** : Black Capital City
#### Death Ring
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Cost** : 3500
![Death Ring fullborder](https://github.com/Card-Forge/forge/assets/67333662/7a692e73-f005-45c7-91e7-b1c56ddea13c)
#### Demonic Contract
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Temple of Liliana
![Demonic Contract fullborder](https://github.com/Card-Forge/forge/assets/67333662/539a29af-b599-4414-a119-e492078575f3)
#### Dungeon Map
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Capital Cities
1. **Price** : 3000 gold
![afr-242-dungeon-map](https://github.com/Card-Forge/forge/assets/67333662/d36d2a07-cb15-4ccf-9414-6b8b75253b90)
#### Entrancing Lyre
1. **Effect** : Extra card starting in your battlefield zone. (Entrancing Lyre)
1. **Location** : White Capital City
1. **Cost** : 1000
#### Gold Shield
1. **Effect** : + 3 starting life total
1. **Location** : Blue Capital Arena
#### Grolnok's Skin
1. **Effect** : Extra card starting in your battlefield zone. Lifemodifier: +3 [Spellbook](https://scryfall.com/@Simidhimi/decks/bf16e555-4efb-488b-a05c-25296807777c)
1. **Location** : Grolnok's Bog
![Grolnok's Skin fullborder](https://github.com/Card-Forge/forge/assets/67333662/6d93aa0d-e726-4ecb-a763-fecfeb0cbda5)
#### Hill Giant Club
1. **Effect** : Extra card starting in your command zone.
1. **Location** : Capital Cities
1. **Price** : 2000 gold
![Hill Giant Club fullborder](https://github.com/Card-Forge/forge/assets/67333662/f669a07c-b3a5-483e-a5ea-9b958595dd96)
#### Iron Shield
1. **Effect** : + 2 starting life total
1. **Location** : Waste Town
1. **Cost** : 3500
#### Jandor's Ring
1. **Effect** : Extra card starting in your battlefield zone. (Jandor's Ring)
1. **Location** : Red Capital Arena
#### Jungle Shield
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Capital cities
1. **Cost** : 3500
![tncc-27-plant](https://github.com/Card-Forge/forge/assets/67333662/a56d07e3-a059-4e15-ad3f-b7be4695a3aa)
#### Kite Shield
1. **Effect** : Extra card starting in your battlefield zone. (Kite Shield)
Opponent starting life -2
1. **Location** : White Capital City
1. **Cost** : 1500
#### Magic Shard
1. **Effect** : Extra card starting in your battlefield zone. (Magic Shard)
1. **Location** : Blue Capital City
1. **Cost** : 3500
#### Mirror Shield
1. **Effect** : Extra card starting in your battlefield zone. (Mirror Shield)
1. **Location** : Capital Cities
1. **Price** : 2500 gold
#### Mithril Shield
1. **Effect** : Extra card starting in your battlefield zone. (c_0_4_a_wall_defender)
1. **Location** : Blue Capital City
1. **Cost** : 6500
#### Nahiri's Armory
1. **Effect** : Extra card starting in your battlefield zone. Lifemodifier: +3 [Spellbook](https://scryfall.com/@Simidhimi/decks/b7a18cab-c10c-4ad4-80ba-aba88649ef27)
1. **Location** : Nahiri's Outpost
![Nahiri's Armory fullborder](https://github.com/Card-Forge/forge/assets/67333662/d46e4264-e004-4bcb-8454-8702d5716ae7)
#### Prism Ring
1. **Effect** : Opponent starting life -2
1. **Location** : Capital cities
#### Ring of Immortals
1. **Effect** : Opponent starting life -2
1. **Location** : Capital cities
#### Ring of Renewal
1. **Effect** : Opponent starting life -2
1. **Location** : Capital cities
#### Ring of Three Wishes
1. **Effect** : Extra card starting in your battlefield zone. (Ring of Three Wishes)
1. **Location** : Green Capital City
1. **Cost** : 2500
#### Steel Shield
1. **Effect** : Extra card starting in your battlefield zone
1. **Location** : Waste Town
1. **Cost** : 6500
![twar-4-wall](https://github.com/Card-Forge/forge/assets/67333662/8ee1f8ba-0ebf-4fd5-a192-3c27c37ab8fa)
#### Unhallowed Sigil
1. **Effect** : Extra card starting in your battlefield zone.
1. **Location** : Monastery
![Sigil of Torment fullborder](https://github.com/Card-Forge/forge/assets/67333662/7a363922-62d7-4ec9-9cce-14e6bcb55782)
## Boots
#### Dark Boots
1. **Effect** : Extra card starting in your battlefield zone. Movement speed + 30%. -2 starting life total
1. **Location** : Swamp Capital Shop
1. **Price** : 3000 gold
![snek](https://github.com/Card-Forge/forge/assets/67333662/ce574fe9-269e-4a62-9ec4-2ad4a43aa04d)
#### Gold Boots
1. **Effect** : Movement speed + 30%. +2 starting life total
1. **Location** : Plains Capital Arena
1. **Price** : 7500 gold
#### Iron Boots
1. **Effect** : Movement speed + 20%.
1. **Location** : Waste towns
1. **Price** : 2000 gold
#### Leather Boots
1. **Effect** : Movement speed + 15%.
1. **Location** : Starting item on Easy and Medium difficulty
#### Lightbringers Boots
1. **Effect** : Extra card starting in your battlefield zone (Ajani's Mantra).
Opponent starting life +5
1. **Location** : Forgotten Cave
#### Mithril Boots
1. **Effect** : Movement speed + 30%. +2 starting life total
1. **Location** : Island Capital Arena
1. **Price** : 10000 gold
#### Sandals
1. **Effect** : Movement speed + 10%.
1. **Location** : Starting item on Hard and Insane difficulty
#### Slime-Covered Boots
1. **Effect** : Extra card starting in your battlefield zone. Movement speed + 20%. -1 starting life total
1. **Location** : Ooze Boss
![snek](https://github.com/Card-Forge/forge/assets/67333662/70dc37f0-6568-43d4-8099-89f31715b7eb)
#### Slobad's Iron Boots
1. **Effect** : Extra card starting in your command zone. + 1 starting life total + movement speed + 35%
1. **Location** : Slobad's Dungeon
![Slobad's Iron Boots fullborder](https://github.com/Card-Forge/forge/assets/67333662/df342a9f-b797-41d5-b93f-bec9dc12aa25)
#### Steel Boots
1. **Effect** : Movement speed + 20%. +1 starting life total
1. **Location** : Waste towns
1. **Price** : 4500 gold
## Body
#### Armor of the Hivelord
1. **Effect** : +5 starting life total. And start with an extra card in your hand.
1. **Location** : Skep
#### Dark Armor
1. **Effect** : Extra card starting in your battlefield zone. And -2 starting life
1. **Location** : Black Capital Shop
1. **Price** : 3000 gold
![snek](https://github.com/Card-Forge/forge/assets/67333662/5467c610-acf7-49ad-91ae-f581784a0cfe)
#### Gold Armor
1. **Effect** : +4 starting life total
1. **Location** : Plains Capital Arena
1. **Price** : 7500 gold
#### Iron Armor
1. **Effect** : +2 starting life total
1. **Location** : Waste towns
1. **Price** : 3000 gold
#### Jace's Signature Hoodie
1. **Effect** : Extra card starting in your command zone. And -1 starting life. And additional reward multiplier
1. **Location** : Jace Boss
![Jace's Signature Hoodie fullborder](https://github.com/Card-Forge/forge/assets/67333662/44093afb-b5c1-4ed7-b0c2-bc5602688b5b)
#### Mantle of Ancient Lore
1. **Effect** : Extra card starting in your command zone. And +1 starting life
1. **Location** : Zedruu dungeon
![Mantle of Ancient Lore fullborder](https://github.com/Card-Forge/forge/assets/67333662/f06e245c-c2e2-411f-8cac-1fade87bd957)
#### Mithril Armor
1. **Effect** : +5 starting life total
1. **Location** : Island Capital Arena
1. **Price** : 15000 gold
#### Steel Armor
1. **Effect** : +3 starting life total
1. **Location** : Waste towns
1. **Price** : 5000 gold
## ABILITY1
#### White Staff
1. **Effect**: Heals half of your life for 5 shards
1. **Location** : Capital cities
1. **Cost**: 5000
#### Black Staff
1. **Effect**: Hide from your enemies for 5 shards
1. **Location** : Capital cities
1. **Cost**: 5000
#### Blue Staff
1. **Effect**: Let's you fly for 5 shards
1. **Location** : Capital cities
1. **Cost**: 5000
#### Red Staff
1. **Effect**: Kills the closest enemy in the overworld (non-dungeon) for 5 shards
1. **Location** : Capital cities
1. **Cost**: 5000
#### Green Staff
1. **Effect**: Doubles your speed for 5 shards
1. **Location** : Capital cities
1. **Cost**: 5000
## ABILITY2
#### Green Rune
1. **Effect**: Teleports you to the forest capital for 1 shard.
1. **Location** : Capital cities
1. **Cost**: 500
#### Black Rune
1. **Effect**: Teleports you to the swamp capital for 1 shard.
1. **Location** : Capital cities
1. **Cost**: 500
#### White Rune
1. **Effect**: Teleports you to the plains capital for 1 shard.
1. **Location** : Capital cities
1. **Cost**: 500
#### Red Rune
1. **Effect**: Teleports you to the mountain capital for 1 shard.
1. **Location** : Capital cities
1. **Cost**: 500
#### Blue Rune
1. **Effect**: Teleports you to the island capital for 1 shard.
1. **Location** : Capital cities
1. **Cost**: 500
#### Blue Rune
1. **Effect**: Teleports you to the center for 1 shard.
1. **Location** : Starting area

View File

@@ -1,67 +0,0 @@
Basic Gamepad Support for Adventure Mode
Tested using DS4 Controller on Windows and Android.
If using on Windows OS and you have DS4Windows installed, you might experience dual input because of Emulated/Virtual Controller. To fix this you must use HidHide (better than exclusive mode). Refer to the guide here:
https://vigem.org/projects/HidHide/Simple-Setup-Guide/
DS4Windows latest:
https://github.com/Ryochan7/DS4Windows/releases/
HidHide latest:
https://github.com/ViGEm/HidHide/releases
Other XInput Controller should work, XBox or DS4 Preferaably and other similar XInput Controller with comparable button layout. Custom Key Mapping is not yet supported.
Controls:
UIScenes
DPAD Up/Down/Left/Right - for selecting texboxes and textfield, Scroll Up or Down
Button A - Ok, Show Context
Button B - Cancel
Button X - Increase Difficulty in NewGame Plus/Flip Backside Deck Editor
Button Y - Decrease Difficulty in NewGame Plus/Zoom or Text Mode in Deck Editor
Left/Right Shoulder Button - Scroll Up or Scroll Down on some UIScenes
RewardScene
DPAD Left/Right - Selector
Button A - Confirm/Flip Reward
Button B - Show Rewards/Done
Button Y - Show/Hide Zoom Card
TextInput
DPAD Up/Down/Left/Right - Key Selector
Left Shoulder Button - Shift Keys
Right Shoulder Button - Backspace
Button Start - Jump to Ok
Button A - Confirm
Button B - Cancel
World/Gamescene
DPAD Up/Down/Left/Right - Character Movement
Left Analog - Character Movement
Button A - Menu/Confirm
Button B - Statistics/Edit
Button X - Deck Select/Deck Edit
Button Y - Inventory/Rename
Match/Battle
Left Trigger - Play/Draw/OK (Bottom Left Button)
Right Trigger - Keep/Mulligan/Cancel/End Turn/Alpha Strike (Bottom Right Button)
(To select cards on the battlefield, close Zone tabs first (Button B), then use DPAD)
DPAD Up/Down/Left/Right - Selector
Left Shoulder - Player Panel Selector
Right Shoulder - Zone Selector/Show
Left Analog Down - Select Player (current selected panel)
Button A - Confirm
Button B - Cancel/Hide
Button Y - Show Zoom
Button Back - Show Menu Tabs

View File

@@ -1,35 +0,0 @@
# Getting started
## Difficulty
The difficulty you choose will alter your starting health (15 for easy to 7 for extreme), and the amount of cards you start with when you choose the "pile" mode. Also, cards will sell for less in shops and enemies will have more health.
Another difference is that on hard/extreme difficulty, enemies that use computer-generated decks (using .json files) will instead use completely random decks that will have nothing to do with their original deck theme.
## Starting cards
You will have multiple options as to how your starting pool of cards will look
1. __Pile__. This will give you a random pile of cards based on the color you choose. The harder the difficulty you choose, the fewer cards you will get.
When you use this option, it is recommended that you create a 40-card deck (minimum deck size) out of the 60 cards you get.
2. __Chaos__. This is a unique gameplay mode that completely randomizes the enemy decks and the deck you start with.
__This setting is not recommended for new players__
3. __Standard__. With this start, you will get three 20-card preconstructed jumpstart decks from the set you choose (or all of them).
4. __Constructed__. With this setting, you start with a custom preconstructed deck based on a popular theme that's easy to expand during your run. (**Recommended for new players**). Your starting color will determine which starter deck you will get
## General hints and tips
1. Don't immediately sell cards that are not useful in your current deck. Some cards can be very useful in certain boss fights, for instance, cards with " protection from red" are very strong against most red bosses.
2. Make sure to travel to the biome capitals early in your run. Every capital contains a shop that sells 1 life and an item that allows you to teleport back to that capital. Also, they sell items that can't be found elsewhere.
3. Every boss gives you +1 starting life. So if you're having trouble with a certain boss, it might be better to try your hand with other bosses.
4. Arena challenges in the capitals can be a good source of money, items, and cards. Though you need a rather strong deck to consistently win there.
### Insane Guide
If you are playing on Insane and need some pointers, user KingNishi has written up a nice doc about it
https://docs.google.com/document/d/1RgO20QpdVOpU-g_E1RL8q1r0C5GUy6y-nnxgXNbi9Wg/pub

View File

@@ -1,13 +0,0 @@
# Mana Shards
Mana Shards are a custom resource created for Adventure Mode in Forge. Shards serve as a secondary form of currency in some parts of the game economy, can be bought and sold via dedicated Shard Trader merchants, and are also used to power the player's special abilities both on the Adventure world map as well as in almost all Adventure Mode duels. During a duel your Shards will appear as a usable resource visible alongside your life total akin to how Energy tokens are used. Your Shards are not tokens or counters, and are thus immune to any abilities that would affect tokens or counters outside of using [equipment](adventure-items) that specifically requires their expenditure. Spending your shards is a permanent choice, however, as your final total at the end of the match will follow you back into your Adventure.
As Mana Shards are not self-replenishing, using them for activated abilities that affect the game world and in-match gameplay is a way of letting the player have a constant challenge of their own comfort level. If the player is comfortable taking on a fight without using extra abilities afforded to them by use of Shards, they will have more of them available to spend on in town, or can simply save their full might for boss-level entities.
The most consistent source of Mana Shards is from match wins, players receive one Shard per win without any rewards having to be explicitly added to the encounter. However, when used to power equipment and abilities, one Shard per match will definitely not break even, as most custom Adventure cards and items will cost 2-4 per use. However, Mana Shards can also be purchased in towns, picked up in dungeons, and can be received in large quantities from completing quests.
Mana Shards can be used for:
* Activated in-match abilities
* Activated game map abilities
* Refreshing card shop inventories
* Spellsmith card purchases as an alternative to gold
* Event entry fees as an alternative to gold

View File

@@ -1,28 +0,0 @@
## Modding and Development
With the addition of new planes in Adventure Mode, comes a framework to allow greater customization and even expansion of the available planes. The details behind the framework and each category can be found below. As well as a new section being built with some basic information on each piece of plane construction and modification. The sections will also define some Best Practices to be maintained.
### Getting Started
Modding Adventure mode comes in many fashions. From making small changes to a core plane, such as changing the music that plays on the overworld. To something as complex as an entirely new plane. Regardless of your intended goals, the first thing to do is set-up a back-up method. As any changes you haven't had incorporated into the main game, will potentially be lost on each update of Forge. Since this is a Git project, the method that will be recommended by this wiki, and referenced for the future, is simply to create your own git branch, and use a local repository to control all your files. It is also recommended to follow the directions to [set-up IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup), to manage your local files. (Again, this is the method that will be referenced elsewhere in this wiki.)
### Tools
The following additional tools can also be very useful, or even mandatory, to have for your mod, depending on what all you want to do in your mod/addition.
**[Tiled](https://www.mapeditor.org/)**: If you want to modify or create any maps; (caves, dungeons, towns, etc.) Forge utilizes Tiled. If you want to learn more on this topic, you can find it in the [Create new Maps](https://github.com/Card-Forge/forge/wiki/Create-new-Maps) section of the Wiki.
**[GIMP](https://www.gimp.org/)**: Many of the art files such as the tilesets used in Tiled, are made in GIMP. (A free graphical manipulation program, similar to Photoshop.) While not required to work in Forge's files, if you want to create your own art assets, this is the program that will be used for examples in this Wiki.
### Tutorials
The following pages on this Wiki contain some basic tutorials on various facets of modding Adventure mode, to help get you started.
[Tutorial 1, Create your First Plane](https://github.com/Card-Forge/forge/wiki/Tutorial-1-Create-your-First-Plane)
[Tutorial 2, A New Look (creating your first map.)](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look)
[Tutorial 3, Configuration (Configuring your Plane)](https://github.com/Card-Forge/forge/wiki/Tutorial-3-Configuration)
UNDER CONSTRUCTION, STAY TUNED

View File

@@ -1,101 +0,0 @@
# Towns & Capitals
While exploring the world of Shandalar, you will find multiple cities across all biomes. These settlements are a integral part of the game, and can help the player by giving quests, hosting draft/jumpstart events, as well as selling cards and booster packs.
## Towns
![White Town](https://github.com/user-attachments/assets/db05a9ab-f731-4fea-8f02-772d933ecaec)
![Blue Town](https://github.com/user-attachments/assets/ae89a6a0-1088-429b-a14c-0521e9750f5a)
![Red Town](https://github.com/user-attachments/assets/5596b9d5-8d21-4d8b-9ad8-aeeace22ed30)
![Green Town](https://github.com/user-attachments/assets/0482ae8a-2dae-4855-9c12-38c9b0f49269)
Different towns have different buildings, but some of them are common to them. Buildings are identified by the sign right in front of them.
- The Inn
![The Inn](https://github.com/user-attachments/assets/2e2f8454-051a-404d-abcf-645b362e2db6)
At the inn, players can buy temporary healthpoints, sell cards and join events. Different inns host different events. For example, a town can be hosting a draft for "Urza's Saga Block", while the next one can be hosting a Jumpstart 2025 event.
- Job Board
![Job Board](https://github.com/user-attachments/assets/d913eb70-75f2-424c-8307-5ee6207d92e5)
Going to the job board the player can accept quests, which will reward them with gold and sometimes local reputation. Local reputation reduces the prices of cards sold by shops.
- Shard Trader
![Shard Trader](https://github.com/user-attachments/assets/19eb6bd0-a002-4ac0-906e-ad9663903f72)
The shard trader allows player to trade gold for shards and shards for golds. Shards are used to reroll cards at the shop, as well as joining events.
- Card Shops
![Card Shop 1](https://github.com/user-attachments/assets/468ee561-7b57-4246-9f18-91d3a71012a0)
![Card Shop 2](https://github.com/user-attachments/assets/f6459859-c745-4e58-a17d-bef689a57c2b)
![Card Shop 3](https://github.com/user-attachments/assets/e80eedeb-7b25-4d57-a285-10f44a18a47d)
Card shops sells cards according to the biome and the shop itself. For example, shops in a green city will tend to sell green cards, while shops in a black city will focus on black cards.
Different shops sells different types of cards, and most of them can be deduced from their name. For example, the shop called "Control Magic" will sell blue cards used to control opponents (counter, tap, untap, etc), while a shop called "Swords, Plowshares, and Beyond" will sell cards that exile permanents. The shop information can be a found in [this user-friendly JSON](https://github.com/Card-Forge/forge/blob/master/forge-gui/res/adventure/Shandalar/world/shops.json).
Shops with an Hour Glass sign change their stock daily. All other shops restock only using shards to reroll.
Shop contents based on sign:
<img width="360" alt="shop-icons" src="https://github.com/user-attachments/assets/20144079-c8c2-407e-b523-6e995e75d78c" />
| |A |B |C |D |E |F |G
|---------------|---------------|---------------|---------------|---------------|---------------|---------------|---------------|
|1|-|Instants|Creatures|Green|Red|White|Pirates
|2|-|Basic Lands|Colorless|Black|Blue|Gold|-
|3|-|Selesnya G/W|Azorius U/W|Dimir B/U|Rakdos B/R|Gruul G/R|-
|4|-|Simic G/U|Orzhov B/W|Izzet R/U|Golgari B/G| Boros R/W|-
|5|-|Goblins|Zombies|Humans|Elves|Merfolk|-
|6|-|Item Shop|Artifacts|Angels|Golems|Slivers|Assassins
|7|-|Mardu B/R/W|Jeskai R/W/U|Naya R/G/W|Enchantments|5 Color|Squirrels
|8|Rogues|Non-Basic Land|Space Marines|Necrons|Chaos|Tyranids|Dragons
|9|Assembly Workers/Constructs/Myr|Vampires|Vehicles|Grixis R/U/B|Jund R/G/B|Temur R/G/U|Minotaurs
|10|Dinosaurs|Esper U/W/B|Sultai U/G/B|Bant B/W/G|Abzan G/W/B|Dwarfs|Devils
|11|Ogres|Item Shop|Equipment|Soldiers|Boosters|D&D|Demons
|12|Rotating Stock|Druids|Activated Artifacts|Birds|Wolves|Knights|Walls
|13|Shard Shop|Planeswalkers|Skeletons|Birds|Shamans|Wizards|Sagas
|14|Player Equipment|Phyrexian|Sphinx|Hydras|Spiders|Insects|Changelings
|15|Cats|Eldrazi|Clerics|Mutants/Mutate|Horrors|Transform Cards|Universe Beyond
|16|Battles|Sags|Nobles/Monarch|Giants|Sea Creatures|Snow|Boosters
## Capitals
![White Capital](https://github.com/user-attachments/assets/02103549-559e-401f-b214-c382e8a94e2c)
![Blue Capital](https://github.com/user-attachments/assets/96d56f88-7dbf-4de9-9b59-790efda00194)
![Green Capital](https://github.com/user-attachments/assets/20931954-e464-4c44-a72c-0b689365d23a)
![Red Capital](https://github.com/user-attachments/assets/3d2d1aac-3293-429e-8e91-ca5d5bff81a2)
Capitals are bigger and have more buildings than normal towns, but have certain exclusive shops that can really useful for the player.
- Item Shop
![Red Item Shop](https://github.com/user-attachments/assets/3ef4b8dc-a6be-4829-be8d-147ec939d815)
Item shops sell items and equipments that the players can use on their journey - such as a rune that allows them to teleport to that capital when on the overworld.
- Arena
![Red Arena](https://github.com/user-attachments/assets/3f2ab46d-e5cd-4038-9d59-276889caece8)
At the Arena players can participate in a tournament awarding gold and equipments.
- Smith
![Red Smith](https://github.com/user-attachments/assets/39edca81-73d7-4130-8967-ed5ea42e1970)
The Smith is one of the most important buildings while trying to build a specific deck. Through a filtering system, players can try to create specific cards, by entering the set, mana value, colors and rarity of one or more cards. The more filters you use, the more expensive it gets.
There are other buildings for you to explore on your journey through Shandalar. Have fun!

View File

@@ -1,14 +0,0 @@
to copy from pc to android:
on your pc, browse to
C:\Users\ [user]\AppData\Roaming\Forge\adventure\Shandalar
copy whichever saves you want, then on your android, browse to
internal storage\android\obb\forge.app\forge\data\adventure\Shandalar
and paste the saves you want in there
to copy from android to pc:
on your android, browse to
internal storage\android\obb\forge.app\forge\data\adventure\Shandalar
copy whichever saves you want, then on your pc, browse to
C:\Users\ [user]\AppData\Roaming\Forge\adventure\Shandalar
and paste the saves you want to move in there

View File

@@ -1,6 +0,0 @@
## Tutorial: 1 Create your First Plane
Creating a new plane allows you to fully customize your Adventure mode experience, and can even be shared with the community. The most basic step in doing so, is to add a new directory in `...\YourIntelliJProjectFolder\forge-gui\res\adventure\`, copy the 'config.json' file from `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common`, paste it into your new directory. Then copy the contents of `...\YourIntelliJProjectFolder\forge-gui\res\adventure\Shandalar\world` into your new directory. Congratulations, that is the bare minimum to create a new plane. If you tell IntelliJ to run the Forge project using the Adventure mode settings, you can load up Adventure mode, find your plane in the drop-down list, and load into it.
Of course, now many of you will come back here and say "nothing looks different". Don't worry, nothing is broken. As you haven't actually changed any files from their default settings, everything will load up in the exact same way as the default game. This is intentional, so that you can make as many, or as few, changes from the default Shandalar plane as you like, without breaking anything. Look to the other tutorials for guides on how to change each piece of a plane to match your desire.

View File

@@ -1,221 +0,0 @@
## Tutorial 2: A New Look (Creating your first map)
Okay, you've got a new plane, now let's make that plane our own. This tutorial is going to teach you how to make a very basic map and get it into the game in your plane. This is a much lengthier tutorial, so we are going to split it up into parts.
1. [Part 1: Setting up Tiled](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-1-setting-up-tiled)
2. [Part 2: Building a Basic Map](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-2-building-a-basic-map)
3. [Part 3: Adding the Map to your Plane](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-3-adding-the-map-to-your-plane)
4. [Part 4: Put it to the Test!](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-4-put-it-to-the-test)
### Part 1: Setting up Tiled
To make everything work smoothly, we need to do some set-up of your Tiled project. First, in IntelliJ, navigate to your plane directory (in our case, we named it Test) like so `...\YourIntelliJProjectFolder\forge-gui\res\adventure\Test\`, and create a new directory named 'maps'. Inside that directory make another directory called 'map'. Now open up Tiled, click on New Project, and navigate to the maps directory you just created, (the one with the "map" directory inside it.) Create a new project by picking a name in the file name line, (it is recommended you name it the same as your plane, for ease of bug solving,) and click save.
Next we need to tell Tiled where to find the files Forge uses. Click on Project > "Add Folder to Project...", navigate to `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\maps`. Select the 'obj' directory, and click "Select Folder". If you are planning to use any of Forge's tilesets, then do the same thing, but this time selecting the 'tileset' directory from `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\maps` aswell. This adds access to the various objects Forge uses to actually interact with the player. (Enemies, rewards, map transfers, etc.) The tilesets provide the visual appearance of your map, _as well as the collision mapping_. **_Any of these you intend to modify, you must create a new entry in an appropriate subdirectory inside your own plane's directory!_** Otherwise you will be changing it for every single map in every plane that touches those objects or tilesets, which quickly causes unintended consequences. (Further details on how to modify these items, what the subdirectory should look like, and what needs to remain unchanged to be properly read by Forge, will be described in their specific Wiki pages. For now, just keep this information in mind.)
Next click on Project > "Project Properties..." In the 'Paths & Files' section you will a text bow that says 'Extensions Directory' and three dots to the right. Click on those three dots, and navigate to `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\maps`, select the extensions directory, and click the "Select Folder" button. Then click on "OK". This adds the extensions written for Forge to your project. Without this, your maps will throw errors when Forge tries to do certain object actions, and crash the game. (Learn from my fail on this one.)
Voila! That is the minimum work necessary to prepare your project for adding, or editing, maps unique to your plane.
### Part 2: Building a Basic Map
Alright, so you've got your fancy project, and while this gray screen with a view of multiple folders on the left is cool and all. We know you really came here because you wanted to actually _make a new map_. (Well, technically, you may want to just slightly modify an existing map. But to have it unique to your plane, it will still be effectively a new map in the eyes of IntelliJ.) So let's get to it, let's make a very basic map for Forge. Step one, click the fancy, tempting, "New Map..." button I know you've been staring at. (And possibly already clicked, then came back here to see what you need to do on that next window.) This will bring up a new dialog box, with a bunch of options.
1. Orientation should be Orthogonal
2. tile layout format should be CSV
3. Tile render order should Right Down
Those are the Tiled defaults for those options at the time of this writing, but I want it here in case that changes.
Next up we have the Map size section, the bigger the map, the more you can add, and the longer it will take you to finish. I recommend for our first map, a simple 15 by 15 tiles. You'll make bigger maps, but for your first, don't get overwhelmed. **_DON'T CLICK OK YET!_** I know you want to get started, but we have to change a couple more things. The Tile size needs to be set to 16 wide by 16 high. This is the Forge standard, and the tilesets use it too. Different settings can lead to very unintended side effects. **NOW** you can click "OK".
Welcome to the gray box that is Tiled's default map menu. There's a lot here, but we are going to build a map together, so relax and just follow along. First off, you will often find yourself in need of multiple layers, otherwise your map is simply not going to look good. So, on the right side, you will see a small box with a text line that says "Tile Layer 1". Double click that line, and rename it to "Ground" without the quotes. Next, right-click and select New > Tile Layer. Name this one "Clutter". Do it again, but name this one "Walls". Finally right-click one more time and select New > Object Layer, name this one "Objects". This gives a basic series of layers to allow us to make a map. You can add more if really needed, but try to limit yourself to six or fewer Tile layers to preserve loading speeds. **Only have ONE object layer**, having a second one will confuse the heck out of Forge.
Now that we have our layers, let's make sure they are in the right order. Click and drag each layer so that, from top to bottom, they rea: Objects, Walls, Clutter, Ground. In tiled, like many programs, tiles on layers farther up will appear on top of ones below them. Objects on top ensure nothing gets lost, or accidentally put under a wall, and can easily be seen. Next up, while we have an Object layer, we need to actually tell Forge what layer to render sprites on. (This allows you make roofs or arches, etc, that a character can walk "under"... it also tells Forge where to actually _put_ the sprite, since it doesn't actually default to the Object layer.) For our map, we want the player to be walking on top of any dirt or grime we add to the map, so left click on the Walls layer. On the left side of the screen, you'll see the properties toolbox. Scroll down to the bottom and you will see the "Custom Properties" line. Right click > Add Property. Click the drop-down that says "string" and choose "bool". In the text box, type "spriteLayer" without the quotes. **This is a value read by Forge, so it is case sensitive. Lowercase 's', uppercase 'L', no spaces.** Click "OK". You will see it has added the property to the custom properties, with a check-box. The box is unchecked, check it. (If you had not added this property, made it a bool, set it's name correctly, and checked the box... Any attempts to load this map would cause Forge to bug-out pretty hard and require a restart to just work right again.)
Now that our map-space is set-up, let's go ahead and save all this work we have done. File > Save As. Open your "map" directory inside the "maps" directory, this is a directory of all your maps you are going to use. To save yourself future head-aches, organize maps into sub-directories from the get-go. In our tutorial case, I will be making a beach, so first I will right-click > new > folder while inside of 'map' and name it "beach". Then I will double-click on that "beach" folder. In this sub-directory I name my tutorial map as "test", but you should name your map as something you'll remember it by, then click save. Now that, that's done, let's get around to the art part of this. Click on the Ground layer. You'll notice you can move your mouse over the map and it'll turn red or blue depending on it you are inside the tile space or not. But no matter where you click, nothing happens. That's because we need to actually choose a tileset to use. On the far left side of the screen, you'll see the various folders we imported into our project. Open the tileset folder, and double click on "main.tsx". This will open a new tab, that shows the entire contents of the "main" tileset from Forge. _Since we don't plan to actually make ANY changes to this tileset_ (gentle reminder.) Simply click on the tab for your map (test.tmx in my case,) and you'll see the "main" tileset has appeared in the bottom-right corner of your screen.
Since I chose a beach, I'm going to make a beach for this tutorial, so I'm going to click on the sand-colored tile near the top-left of the "main" tileset, right next to the default empty one. If you move your mouse back over your map, you will see it shows that sand-colored tile for every square you move it over. If you click, it will even place that color on that tile. While you could individually click on each tile one at a time, for filling in large areas that's much more exasperating. Clicking and dragging will simply apply each tile the mouse moves over. So, instead, along the top, you will see a paint-bucket. Click on that. now if you move your mouse back to the map, it'll show a ghostly tan color across the entire map, minus any squares you already clicked on. (The highlight will only apply to every tile of the same type that you hover over, which is not completely separated from the mouse by a different tileset tile. If you are confused, this will make more sense as you make or modify maps.) Click on the empty space, and it will fill in the entire map with our sandy ground.
Now, a blank sandy beach may be more than you had before, but it's not that interesting, so let's add some detail. Click on the Clutter layer. Then, on our "main" tile-set on the bottom right, click on the water tile just below the blank one in the top left. If you move your mouse back to the map, you will see it wants to change every tile to water. That's because we are still in Bucket Fill mode. On the top of the window, you'll see a rubber stamp, two tools to the left of the pain bucket. Click on that. Now we are once again designing our map one tile at a time. I'm going to draw out some water tiles along the edges of the map, leaving just a couple sandy ones at the bottom center of the map.
Now, The map is clearly a sand-bar jutting out into water, but it looks pretty rudimentary, even for Forge. As such, let's click on the Walls layer. Then, on the tile-set on the bottom right, there are a bunch of sandy edges looking like they are meant for the edge of water. I'm going to carefully select various tiles from among them and make my coast-line look much nicer. Exactly which tiles where will depend on how you built the previous layer. If you ever place a tile and it doesn't look right. You can click on the Eraser tool at the top of the screen, and then click that tile on your map. As long as you are on the right layer, it will erase that tile.
Huzzah! You've made you little sandbar... But how can you be certain where players and enemies can walk? Well, that's called Collision. Go to View > and make sure there is a check-mark in "Show Tile Collision Shapes". The water tiles will have much strongly defined border now, and depending on your sand borders and how you built it, you may have new lines in them, or not. Collision is defined by the tile in the Tileset. Each tile in the tile set either has collision shapes, or doesn't. **If you are not happy with the collisions as-is, you will need to either change your map some, or make your own tileset.** As a reminder DO NOT MODIFY THE TILESETS FROM MAIN FORGE. If you need to make changes, look for my future tutorial that explains building your own tile-set and modifying it.
Alright, you have a map, it has water, it has collision... Still looks pretty empty. You could add more details on various layers to make it look nicer, but that won't change the most important things. Namely, there's nothing to _do_ on our sandy beach. Let's fix that. On the left side of the screen, open up the "obj" folder, this has all the objects in Forge. Since they are all Objects, let's click on the Objects layer. Now, click and drag a "gold.tx" object from the obj folder, onto our sand. (make sure its on a portion of sand that isn't blocked by collision.) Unlike the tile layers, objects don't have to be centered to any tile. They can even sit on a grid intersection without issue.
Alright, we got some gold on our map, but that's a bit boring. let's spice it up, by adding an enemy. So click and drag an "enemy.tx" onto the map. Now, all our objects have many properties that make sure they work. But most of them come pre-populated to some degree, so you can just drag-and-drop them. Enemies, however, are diverse and different enough we need to actually define some things about them in order to even function. So click on your placed enemy object, and scroll down the left side to the Custom Properties section. In here, the only option you _have_ define, is the "enemy" property. (Remember, the one in Custom Properties, not the space earlier in the normal properties... Again, learn from my mistakes.) In our case, click on the text box to the right of "enemy", and type "Ghost" without the quotation marks. (While a later tutorial will give more details on all the properties, just know that for this one. It is looking for an entry in your plane's "enemies.json" file. If your plane does not have that file, like ours, it will instead look for the entry in the enemies.json file found in `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world`... A later tutorial will guide you through making your own enemies.) You have now defined the enemy, but as-is, it will just stand in place, even if a player gets near. Let's liven things up a little, and make our ghost a bit more annoyed and less sleeping on the job. Go down to the "pursueRange" custom property, and set that text box to 50. Then scroll down further to the "threatRange" box, and set that, also, to 50. Now, if the player gets within 50 pixels of it, the ghost will chase them. The ghost will only move to a distance of 50 pixels from their starting point, doing so, however. (For a more lively enemy that patrols, or jump out, etc. You can find that information in the Configuring Enemies portion of the Wiki.)
Alright, we got a map, we got a reward, and we got an enemy to protect that reward. All good, except, Forge has no idea where to spawn a player who enters the map, and no idea where the exit to the map is. So, go and drag an "entry_up.tx" object to the little entrance of our sandbar, and resize it so that the arrow takes up the majority of the entry space. Congratulations, your map is made, don't forget to **_SAVE_**.
### Part 3: Adding the Map to your Plane
Alright, so you have a map you want to play, it's saved in your plane, why can't you find it? Well, that's because Forge doesn't **actually** know it's ready to _be_ found yet. We need to get some "biomes" and "points of interest" added to your plane, aswell as. So, first, let's give them a place to go in your plane. Go to your plane's directory in IntelliJ, and open the 'world' sub-directory. Inside of 'world' create a new subdirectory called 'biomes'. Navigate to `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world\biomes` and copy the following files to your plane's biomes directory, 'base.json' and 'colorless.json'. We also need to copy the 'points_of_interest.json' from `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world` to our custom plane's 'world' directory. Now, open up the 'points_of_interest.json' from our plane's 'world' sub-directory. Inside you will find all the maps used by Forge. If you want to add a map to the list, but keep all the others, you would add it's info the existing json. But for the sake of simplicity in this tutorial, we are going to delete everything inside the json except for the following.
```
[
{
"name": "Aerie",
"displayName": "Aerie",
"type": "dungeon",
"count": 30,
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
"sprite": "Aerie",
"map": "../common/maps/map/aerie/aerie_0.tmx",
"radiusFactor": 0.8,
"questTags": [
"Hostile",
"Nest",
"Dungeon",
"Sidequest"
]
},
```
and
```
{
"name": "Spawn",
"displayName": "Secluded Encampment",
"type": "town",
"count": 1,
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
"sprite": "Spawn",
"map": "../common/maps/map/main_story/spawn.tmx",
"questTags": [
"Story",
"Spawn",
"BiomeColorless"
]
}
]
```
For this tutorial, we need to know the following details.
"name" is the map name in Tiled, so we will change `"name": "Aerie",` to `"name": "YOUR MAP NAME",` ("Test" in our case.)
"displayName" is the name that appears when the player enters a map. So we will change `"displayName": "Aerie",` to `"displayName": "YOUR MAP DESCRIPTION",` (in our case, I'm going with "Fanciest Beach").
"count" is how many of this map can spawn in the overworld. Since we want to actually find it, let's change `"count": 1,` to `"count": 30,`
and "map" is where we actually STORED the map file. So we are going to change `"map": "../common/maps/map/aerie/aerie_0.tmx",` to `"map": "../YOUR_PLANE_NAME/maps/map/YOUR_FOLDER_NAME/YOUR_MAP.tmx"` . For my tutorial, that becomes `"map": "../Test/maps/map/beach/test.tmx",`.
You can leave the rest of the Aerie code-block alone for this tutorial. If you followed me exactly, "points_of_interest.json" file should now look like this.
```
[
{
"name": "Test",
"displayName": "Fanciest Beach",
"type": "dungeon",
"count": 1,
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
"sprite": "Aerie",
"map": "../Test/maps/map/beach/test.tmx",
"radiusFactor": 0.8,
"questTags": [
"Hostile",
"Nest",
"Dungeon",
"Sidequest"
]
},
{
"name": "Spawn",
"displayName": "Secluded Encampment",
"type": "town",
"count": 1,
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
"sprite": "Spawn",
"map": "../common/maps/map/main_story/spawn.tmx",
"questTags": [
"Story",
"Spawn",
"BiomeColorless"
]
}
]
```
Now, since we want to play our new map, we still need to make sure it spawns in Forge. Go to "colorless.json" file we copied into our Plane's 'biomes' directory. Scroll down until you see the `"pointsOfInterest":` array. We are going to delete everything in this array except for `"Spawn",` and then we are going to add `"YOUR_MAP_NAME"` ("Test" in my case) immediately afterwards. If you copied this tutorial exactly so far, it would like this.
```
"pointsOfInterest": [
"Spawn",
"Test"
],
```
### Part 4: Put it to the test!
Alright, time for the fun part. If you've come this far, everything is ready to go. Tell IntelliJ to fire up Forge, go into Adventure mode. If you hadn't previously, set your plane to your custom plane, (and restart Forge in that case.) You'll still have the tutorial quest prompts and spawn to work through. But when done, when you leave the portal, things will immediately be visibly different. If you don't see a dungeon right away, bring up the Map, track down your dungeon icon in the Wastes biome, and walk on over. (You may need to avoid a few spawns along the way.) Enter, and, assuming you following perfectly, you'll show up in your new map. If you have questions or run into any problems, reach out to me (shenshinoman) on the Forge discord and I will be happy to help.
After you've done that, you may notice the entire world is devoid of any map except the overworld, the spawn to the old man, and the map you just built. While this is nice for testing a new map, we should probably add a city back in, that way we actually have a place to heal up and test other changes to the plane. To make life really easy, we are just going to add a basic town back to the "Wastes". Navigate to ``...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world' and open up the "points_of_interest.json" file. Towards the bottom we will find the code block for "Waste Town Generic", select it, and copy it into the "points_of_interest.json" inside your plane's 'world' directory. So, if you've followed along precisely, your "points_of_interest.json" should look like this.
```
[
{
"name": "Test",
"displayName": "Fanciest Beach",
"type": "dungeon",
"count": 30,
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
"sprite": "Aerie",
"map": "../Test/maps/map/beach/test.tmx",
"radiusFactor": 0.8,
"questTags": [
"Hostile",
"Nest",
"Dungeon",
"Sidequest"
]
},
{
"name": "Spawn",
"displayName": "Secluded Encampment",
"type": "town",
"count": 1,
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
"sprite": "Spawn",
"map": "../common/maps/map/main_story/spawn.tmx",
"questTags": [
"Story",
"Spawn",
"BiomeColorless"
]
},
{
"name": "Waste Town Generic",
"type": "town",
"count": 30,
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
"sprite": "WasteTown",
"map": "../common/maps/map/towns/waste_town_generic.tmx",
"radiusFactor": 0.8,
"questTags": [
"Town",
"TownGeneric",
"BiomeColorless",
"Sidequest",
"QuestSource"
]
}
]
```
Now we need to go and tell the game to actually spawn this town. Inside your plane's "world\biomes" directory, open back up the "colorless.json" file, and add "Waste Town Generic" to the `"pointsOfInterest"` array. If you followed along, that array should now look like this.
```
pointsOfInterest": [
"Spawn",
"Test",
"Waste Town Generic"
],
```
Now if you open up your plane, you have a bunch of dungeons and a bunch of towns. Congratulations, you have completed this tutorial. Pat yourself on the back, cus it was a long one. (Also, don't forget to save if your system doesn't auto-save.)

View File

@@ -1,56 +0,0 @@
## Tutorial 3 Configuration, (Configuring your Plane)
Alright, our third tutorial will be a simpler one again. Customizing your plane. A common desire when making a new plane, is to change things such as which sets are available to play, or which cards an be bought or gained as rewards. Maybe you want to be able to more easily build super powerful decks from the get go, or just have entirely new decks. All these settings, and more, can be adjusted from the 'config.json' file in your plane's directory. For this tutorial, to get a basic idea of what things can be changed, we are going to make some minor adjustments to the plane. Further details on other changes and what they do will be covered in other sections and tutorials.
For now, in IntelliJ, open up the 'config.json' file found in `...\YourIntelliJProjectFolder\forge-gui\res\adventure\YourPlane`, as a reminder for our tutorials, I am using a directory called Test. Inside the config file, there are a large number of entries, but we are going to start with the `"restrictedCards"` array. After-all, we are here to have fun, so let's just bring in almost all the craziness. We are going to delete every entry in the array, _except_ for "Black Lotus"... Why not Black Lotus you ask? Because it's too rare, clearly. (Or, maybe just because it is the first entry and I'm being lazy, and keeping one entry is a reminder how this array is formatted.) So, when you are done, the array should look like this.
```
"restrictedCards": [
"Black Lotus"
],
```
To better explain what that array does, is any card in the array will not appear in any generic rewards or shops. This is the closest thing to banning individual cards that one can. However, it should be noted that if those cards are ever specifically added as a reward or a shop's content. Doing so overrides this setting. Essentially allowing you to award cards for very special cases, or hide a fancy shop somewhere in your game.
Now, restricting one card at a time is a very laborious process, especially if there are a large number of cards you want removed. In many cases, you will want entire sets removed from your game, which brings us to the `"restrictedEditions"` array. Any card whose set code appears in this array will be restricted from the game in the exact same way as `"restrictedCards"`. There's already several entries in here, but since I'm making a really wacky unbalanced plane here, we're going to remove every entry from this array, except for "UNF". Why not UNF you ask? Because it's a traitor and made some of it's cards legal and others not, clearly. (Or, maybe again it's because I'm lazy, and at the time of this writing, it's the last entry in the list. As well as once again keeping one entry as a reminder how this array is formatted.) So if you follow my flawless example, it will look like this.
```
"restrictedEditions": [
"UNF"
],
```
Now that we've opened the floodgates for our awards, we could go have fun in the wacky world. However, our Inn's drafts and jumpstarts won't be as open as the rest of the game still. Why? Because the array `"restrictedEvents"` tells our Inn what "events" (aka sealed and draft editions) it can't spawn. Just like `"restrictedEditions"` any set in this array won't appear for draft events (but if it is not in the `"restrictedEditions"` it can still appear as rewards.) As such, let's go ahead and open those flood gates further. In my infinite wisdom, I have decided, for no specific reason at all, that we will remove every set from this list except for "CMM". (Yes, you got the picture, I'm lazy but trying to be helpful still.) If you followed me so far, the entries between `"colorIDNames"` and `"difficulties"` should appear as follows.
```
"restrictedCards": [
"Black Lotus"
],
"restrictedEditions": [
"UNF"
],
"restrictedEvents": [
"CMM"
],
```
Now if you load up the game at this timeframe, and go hopping through towns, you'll notice the occasional card you hadn't seen before. This is good, but man there are a lot of cards in Magic, and it can take a while to find ones to show you've made a change. For a tutorial, that's bad. Also, if you want to make a plane like "Shandalar Old Border" or "Innistrad", you'd have to go through and add every unwanted set to the `"restrictedEditions"` array. including updating this array every time Wizards of the Coast releases another set, and that's **worse** than bad... Luckily, we have a solution for this. We are going to add two new arrays to our 'config.json' file. Where the previous arrays act as a black-list for the entries, the following arrays are a overwrite. If they are present, the plane will only spawn cards and events found in them. The first one is `"allowedEditions"`. For this tutorial, I am going to go completely unhinged and risk making some of our games unstable, but what mod never comes unglued? If you didn't pick up the hints, my array is going to add `"UNG"`, `"UNH"`, and `"UST"`. So my array looks as follows.
```
"allowedEditions": [
"UNG",
"UNH",
"UST"
],
```
The second array I'm going to add is the `"allowedJumpstart"` array. This changes which Jumpstart sets are available, and will allow us to still have them even when the previous arrays have blacklisted them in any way. For this tutorial, I know we all want to get a jump on things, so I'm just going to add `"Jumpstart"`.
```
"allowedJumpstart": [
"Jumpstart"
],
```
You might have noticed that unlike the previous arrays, this one uses the set name, not code. This is a current limitation in Forge, and one we will have to live with. Luckily, there are far fewer jumpstart sets than than other sets. (**NOTE: Adding a set to this list that does not have any official Jumpstart packs will NOT enable them for jumpstart events.** Some of you who have tooled around will have noticed there are Jumpstarts in "Innistrad" that don't exist in normal jumpstarts. Well, that is for a much later tutorial. My apologies.)
As a final note, while this changes the rewards from enemies and shops, as well as what can appear in the Inns. It has no bearing on your starting decks, or enemy decks, just the rewards and shop options. So yes, if you followed along so far, you will playing with 'normal' cards, against normal cards, to earn Silver-Bordered cards.. yeah, it's a bit crazy, but that works... For those who want to customize their plane even further, the next tutorial will focus on modifying both starting decks, and enemy decks. See you there. For now, enjoy, and again. if you have any problems, please reach out me "Shenshinoman" on the discord and I will be happy to help.

View File

@@ -1,115 +0,0 @@
# About
Card images are assets used to represent the real cards in game. You DO NOT need images to play forge, however representing real cards is a nice ability forge provides. These images can be any image set you like, or custom images too.
Primarily there are two types of images you'll care about; cards, and tokens.
**Cards** - are the primary card image files, and can be generic (as all cards of the same name have the same rules) or per set since there may be different art work in different editions. Typically these are scans of the original cards and are provided by forge's FTP, or scryfall. You can customize a full generic set, or any edition if you desire... (you could for example, blur every card image and play a literal "blind" or "farsighted" game.)
**Tokens** - are the images for the cards replacing a generic "1/1 zombie" for example. These are less frequently updated, and are typically the bulk of what is missing when doing an audit. However, these are probably where the more true "custom" replacements are available, with either custom artwork, or modified of other existing.
A deck may explicitly define the edition and art variant of each card it includes. If a deck specifies those for no card, Forge uses a special algorithm to determine which card printings were the latest by the moment all of deck's had been printed. These very editions of cards are used when loading deck into memory to reflect the flavour of the season when the deck was built.
Card images are cleared from memory cache when switching screens and between games.
# Downloading
Due to charges in Forges hosting and scryfall terms you can no longer predownload card images. Turn on auto download in forge to download card images when first viewed.
## In Forge Auto Download:
**Download Missing Images - Setting**
- This will download the images from the sources as the game requests the image in situ.
- This can be useful if you don't want to have copies of every card... You can do small pre-caching by loading your decks in the deck editor prior to playing to download just those images.
## Bulk Download Sites: (Not in game)
- [http://download.austeregrim.net](http://download.austeregrim.net)
- Note from user AustereGrim;
> I provide my site for free for bulk downloading the entire image catalog. So you don't need to give those spam sites more advertising spots. If the server is loaded bandwidth is shared, right now it's not heavily used so please feel free to download the 4+gb zips, or the individual zips if you need sets. They are the images Kev has uploaded to my site, and the Zips are updated nightly automatically.
**(I'm not gatekeeping, please if you have a private location for bulk downloads or for alternate or custom arts, you can update this wiki too or let us know in the discord. I'll be happy to update the wiki page with additional sources.)**
If you have an older Android device for increased performance or to save bandwidth it might be a good idea to use lower resolution images instead: https://www.slightlymagic.net/forum/viewtopic.php?f=15&t=29104
# Storage
Card images are stored in `pics/cards`, and tokens in `pics/tokens`, in the Cache folder for forge:
- **Windows** - `C:\Users\<username>\appdata\local\forge\Cache\`
- You'll need to enable hidden folders.
- **Android 11+** - `Internal Storage/Android/obb/forge.app/Forge/cache/`
- *_NOTE: You need a third party File Manager to access the obb folder and allow storage access permission_*
- **Android 8 to 10** - `Internal Storage/Forge/cache/`
- **Linux** - `/home/<username>/.cache/forge/`
- **MacOS** - `/Users/<username>/Library/caches/forge/`
- Use `Command + Shift + .` to show hidden files.
# Subfolders
If you don't care about the edition's version of cards, images can be stored in the root directory of the cards folder.
`/cache/pics/cards/`
If you want the edition's versions of the cards, they need to go under the edition's code subfolder.
`/cache/pics/cards/AFR` for example for Adventures in the Forgotten Realms.
# File Naming
**File Names:**
- Cards file names follow a simple principle: `Card Name#.border.ext`
- `Card Name` - Card Name with spaces.
- `#` - Alternate Art number; if more than one art exists for the card.
- `border` - Border Type; fullborder, crop. (I don't know all of them.)
- `ext` - Extension, jpg or png are supported.
**Alternate images:**
Alternate images are defined as cards with the same name in the set's edition file, if the edition file does not have the alternate listed forge will not see the alternate there!
**Standard Alternate Arts:**
So for example the AFR set (as most sets) shows these 4 versions of swamp;
```
270 L Swamp @Piotr Dura
271 L Swamp @Sarah Finnigan
272 L Swamp @Titus Lunter
273 L Swamp @Adam Paquette
```
The file naming would be represented by a number after the name:
```
Swamp1.fullborder.jpg
Swamp2.fullborder.jpg
Swamp3.fullborder.jpg
Swamp4.fullborder.jpg
```
**Additional Alternate Arts:**
They may also be listed separately as "extended arts", "showcase", or "borderless" in the same editions file:
```
[cards]
90 U Black Dragon @Mark Zug
```
and
```
[borderless]
291 U Black Dragon @Jason A. Engle
```
Where the files are:
```
black dragon1.fullborder.jpg
black dragon2.fullborder.jpg
```
**Forcing an Alternate:**
Renaming and creating a second of an existing card **will not work**, for example creating two "Burning hands" which does not have alternate art;
```
burning hands1.fullborder.jpg
burning hands2.fullborder.jpg
```
Forge will not see either of those, and will probably download the missing `burning hands.fullborder.jpg` for you. Similarly adding a 3rd black dragon `black dragon3.fullborder.jpg` will **not** work either.

View File

@@ -1,905 +0,0 @@
AbilityFactory parses differently from the Keyword parser. Your Ability line will look more like this:
`A:{AB/SP/DB/ST}$ <AFSubclass> | {Necessary$ Parameters} | {Separated$ By} | {Pipes$ Here} | [Optional$ Values]`
In most cases, each AF subclass implements both the Spell and Ability.
Much of the code is shared, so creating the data object will look very similar.
- **AB** is for Activated Abilities
- **SP** is for Spell
- **DB** is for Drawback and many abilities that are subsidiary to other things, like replacements. They are only used to chain AFs together, and will never be the root AF
- **ST** is for Static, this gets used in case the API should resolve without using the stack<br /> (e.g. the unique *Circling Vultures* special action is directly implemented in the script this way)
>*NOTE:*
> - these factories are refactored from time to time (often to adapt to new sets), so while some entries could be slightly outdated, the base information should still be correct
> - a few factories also have _*All_ variants, these are slowly being phased out
> - some parameters are only added for very exotic cards, these won't be included here to keep the focus on understanding the general concepts of scripting
> - when in doubt you can always cross check with the [available APIs](https://github.com/Card-Forge/forge/tree/master/forge-game/src/main/java/forge/game/ability/effects) code
# Common Parameters
## Cost / UnlessCost
`Cost$ <AbilityCost>` is the appropriate way to set the cost of the ability. Currently for spells, any additional costs including the original Mana cost need to appear in the Cost parameter in the AbilityFactory. For each card that uses it, the order in which the cost is paid will always be the same.
Secondary abilities such as the DB executed by triggers or replacements (usually) don't need costs. (This is one reason to use DB over AB in these cases.)
Read more about it in [Costs](Costs)
## ValidTgts / Defined
Most effects need to know (at least implicitly) which players or objects they're trying to affect. There are two different ways for that:
- `ValidTgts` will need to be used for abilities that target
- if your ability instead describes on what it's applied use `Defined`
Read more about it in [Affected / Targets](Targeting)
## Restrictions / Conditions
Restrictions limit when abilities can be put on the stack and Conditions apply during resolving. Common examples are Putrid Leech's only activate this once per turn or different cards that can activate from Zones like the Hand or the Graveyard.
Read more about it in [Restriction](Restrictions)
## SpellDescription
SpellDescription is how the text of the ability will display on the card and in the option dialog for cards with multiple abilities.
The SpellDescription for secondary abilities (both AB and DB) is now displayed when (and if) the ability prompts for user input in the prompt pane so it is useful to put some nice text there.
## StackDescription
*(Optional)* StackDescription is the description the ability will have on the stack. This is automatically generated by the effect, but may be overridden using this parameter. This is sometimes needed with complex effects, when the generated text can't handle some details. Properties of the spell can be accessed like this: {c:Targeted}. You can reuse the spell text by just putting `SpellDescription` or `None` to leave it empty.
## Remember*
Remembering is often needed when a card becomes a new object, which is then further affected by the ability. Typical example: [Flicker](https://github.com/Card-Forge/forge/blob/master/forge-gui/res/cardsfolder/f/flicker.txt)<br />
Because cards keep their remembered parts when changing zones manual [cleanup](#Cleanup) is usually required.
## Duration
## AI params
`IsCurse$ True` - For effects that are normally treated positive e.g. Pump
`AITgts$ BetterThanEvalRating.130`
# Factories (in Alphabetical Order)
## AlterLife
AlterLife is for Abilities that Alter a player's life total.
### GainLife
Have a player gain the specified amount of life.
`A:AB$ GainLife | Cost$ T | LifeAmount$ 1 | SpellDescription$ You gain 1 life.`
LifeAmount$ is required. This is how much life you will gain.
Defined is optional, if it appears the defined player(s) gain life.
Target is optional, if it appears and Defined doesn't then targeted player(s) gain life.
### LoseLife
Have a player lose the specified amount of life.
`A:AB$ LoseLife | Cost$ Sac<1/CARDNAME> | ValidTgts$ Player | TgtPrompt$ Target a player to lose a life | LifeAmount$ 1 | SpellDescription$ Target player loses 1 life.`
`A:SP$ LoseLife | Cost$ 2 B | Defined$ Opponent | LifeAmount$ 2 | SpellDescription$ Each opponent loses 2 life.`
LifeAmount$ is required. This is how much life will be lost.
Target is optional. If Target doesn't appear then Defined will be used.
Remember, if Defined is missing, the default for Players is "You"
Part of resolving sets the **SVar AFLifeLost** to the amount of life lost by all players.
### SetLife
SetLife sets one or both player's life total to a specified value (i.e.
"your life total becomes 20" or "Target player's life total is equal to
the number of cards in your graveyard").
`A:SP$ SetLife | Cost$ 7 W W | ValidTgts$ Player | TgtPrompt$ Select target player | LifeAmount$ 20 | SpellDescription$ Target player's life total becomes 20.`
Parameters:
LifeAmount (required) - the value to set the life total(s) to
Defined is optional. If it exists, it will be used. Target is optional.
If it exists and defined doesn't it will be used. Default player is "You".
### ExchangeLife
ExchangeLife switches the Life total of two players.
`A:AB$ ExchangeLife | Cost$ 6 T | ValidTgts$ Player | TargetMin$ 2 | TargetMax$ 2 | TgtPrompt$ Select target player | SpellDescription$ Two target players exchange life totals.`
One of Defined or Target is required, since there needs to be two
Players exchanging life If Defined it will be used. If Target exists and
defined doesn't it will be used.
If there aren't two determined players by the SA, the activating player is added as the second player.
## Animate
Animate handles animation effects like "This card becomes a 5/5
green creature with flying until end of turn." It is designed to handle
color changing, type changing, P/T setting, and granting/removing abilities.
`A:SP$Animate | Cost$ G | ValidTgts$ Land | TgtPrompt$ Select target land | Power$ 3 | Toughness$ 3 | Types$ Creature | SpellDescription$ Until end of turn, target land becomes a 3/3 creature that's still a land.`
`A:AB$Animate | Cost$ 1 B | Defined$ Self | Power$ 1 | Toughness$ 1 | Types$ Creature,Skeleton | Colors$ Black | Abilities$ ABRegen | SpellDescription$ CARDNAME becomes a 1/1 black Skeleton creature with "B: Regenerate this creature" until end of turn. It's still a land. (If it regenerates, the next time it would be destroyed this turn, it isn't. Instead tap it, remove all damage from it, and remove it from combat.)`
`SVar:ABRegen:AB$Regenerate | Cost$ B | SpellDescription$ Regenerate CARDNAME.`
`A:AB$Animate | Cost$ 2 R G | Defined$ Self | Power$ 3 | Toughness$ 3 | Types$ Creature,Elemental | Colors$ Red,Green | Triggers$ TrigAttack | SpellDescription$ Until end of turn, CARDNAME becomes a 3/3 red and green Elemental creature with "Whenever this creature attacks, put a +1/+1 counter on it." It's still a land.`
`SVar:TrigAttack:Mode$ Attacks | ValidCard$ Creature.Self | Execute$ TrigPutCounter | TriggerDescription$ Whenever CARDNAME attacks, put a +1/+1 counter on it.`
`SVar:TrigPutCounter:AB$PutCounter | Cost$ 0 | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1`
Parameters:
- Power (required) - the power to assign to the animated card
- Toughness (required) - the toughness to assign to the animated card
- Types (optional) - the additional types to give the animated card;
comma delimited
- OverwriteTypes (optional) - set to True if the animated being should
have these types **instead** as opposed to **in addition to**
- RemoveTypes (optional) - a list of types to Remove from the animated
card
- ChosenType (optional) - overrides types before it and just will add
the ChosenType
- Keywords (optional) - a " & " delimited list of keywords to give the
animated being (just like AB$Pump)
- HiddenKeywords (optional) - a " & " delimited list of hidden
keywords to give the animated being (just like AB$Pump)
- RemoveKeywords (optional) - a " & " delimited list of keywords to
remove from the animated being (just like AB$Debuff)
- Colors (optional) - a comma-delimited list of Colors to give to the
animated being (capitalized and spelled out) (ChosenColor accepted)
- Abilities (optional) - a comma-delimited list of SVar names which
contain abilities that should be granted to the animated being
- OverwriteAbilities - Remove Abilities from animated being
- Triggers (optional) - a comma-delimited list of SVar names which
contain triggers that should be granted to the animated being
- OverwriteTriggers - Remove/suppress triggers from animated being
- staticAbilities (optional) - a comma-delimited list of SVar names
which contain static abilities that should be granted to the
animated being
- OverwriteStatics- Remove static abilities from animated being
- OverwriteReplacements - Remove replacement effects from animated
being
- RemoveAllAbilities - Remove all Abilities, Triggers, Statics, and
Replacement effects
- sVars(optional) - a comma-delimited list of SVars that should be
granted to the animated being
- Duration (Default is end of turn)
- Permanent
- UntilEndOfCombat - if the effect should last only until End of Combat instead of End of Turn
- UntilHostLeavesPlay - if the effect should last as long as the host is still in play
- UntilYourNextUpkeep
- UntilControllerNextUntap
- UntilYourNextTurn
Target is optional, will be used if possible. Defined is optional, will be used if no Targets (Self by default)
## Attach
Attach is being used directly only for Auras, primarily for Aura Spells, but also for Auras entering the battlefield by some effect.
`AB$ Attach | Cost$ R R | ValidTgts$ Creature | AILogic$ Pump`
Parameters:
- Object (optional) - This is the Object that will be Attached
(generally the Source Card for Auras)
- AILogic - AI Logic tells the AI which base AI code it should use for
Attaching
- GainControl - Gains Control of the Attached Permanent (Control
Magic)
- Curse - A Generic Curse the AI has a handful of checks to see
what the most appropriate Target is.
- Pump - A Generic Pump. The AI has a handful of checks to see
what the most appropriate Target is.
- ChangeType - For Attachments that change types. Evil Presence is
a good example. This logic should be expanded.
- KeepTapped - For Attachments that keep a Permanent tapped. The
AI will also check for a few things like Vigilance, and another
KeepTapped Aura. Paralyzing Grasp is a good example.
Attach separates the actually granting of abilities from the attaching to permanents to streamline how things work.
## BecomeMonarch
No own parameters.
## Bond
Soulbonding two creatures. Only used internally by the engine.
## Branch
Sometimes, an ability might do certain things when a specific condition is true, and other things if not. This can be implemented by using `Branch`.
The branch evaluates the SVar specified by the property `BranchConditionSVar`, using the comparison defined with `BranchConditionSVarCompare` (such as `GTY`, `LT1`, etc). Depending on whether the condition evaluated to true or false, the subability defined by `TrueSubAbility` or `FalseSubAbility` is executed.
The example below is for "Composer of Spring", which allows either a "land" or a "land or creature" to be put on the battlefield, depending on the number of enchantments in play under your control.
```
SVar:TrigBranch:DB$ Branch | BranchConditionSVar$ X | BranchConditionSVarCompare$ GE6 | TrueSubAbility$ PutLandCreature | FalseSubAbility$ PutLand
SVar:PutLand:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | Tapped$ True | ChangeType$ Land.YouOwn
SVar:PutLandCreature:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | Tapped$ True | ChangeType$ Creature.YouOwn,Land.YouOwn
SVar:X:Count$Valid Enchantment.YouCtrl
```
## Charm
This allows cards that have a mode to be chosen to occur after a trigger.
Parameters
- CharmNum - Number of Modes to Choose
- Choices - A Comma delimited list of SVars containing the Modes
## Choose*
These can be used to chain effects together. However for common cases many effects already support this directly, e.g. `PutCounter | Choices$``.<br />
Besides making the script shorter using such shortcuts usually also helps the AI making better use of the effect.
### ChooseType
This can be used when you are asked to choose a card type or creature type.
- Type - Required - Can be Card or Creature
- InvalidTypes - Optional - Use to specify any type that cannot be chosen (ex: "Choose any creature type except Wall")
The Defined is for target players.
## Clash
This AF handles clashing. It takes two special parameters: WinSubAbility and
OtherwiseSubAbility. They are both optional and work the same way,
namely that it contains the name of an SVar that in turn contains a
drawback to be executed. The example below is for Release the Ants.
`A:SP$ DealDamage | Cost$ 1 R | Tgt$ TgtCP | NumDmg$ 1 | SubAbility$ DBClash | SpellDescription$ Release the Ants deals 1 damage to target creature or player. Clash with an opponent. If you win, return CARDNAME to its owner's hand.`
`SVar:DBClash:DB$ Clash | WinSubAbility$ DBReturn`
`SVar:DBReturn:DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Hand`
## Cleanup
A non-functional, maintenance AF used for Cleaning up certain Variables before a Spell finishes Resolving.
Parameters
- ClearRemembered$ (optional) Set to True to clear this card's
remembered list. Generally useful for Cards that Remember a card, do
something to it, then need to forget it once it's done.
- ClearImprinted$ (optional) Set to True to clear the list of
imprinted cards.
- ClearChosenX$ (optional) Set to True to clear the chosen X value.
- ClearTriggered$ (optional) Set to True to clear any delayed triggers
produced by this card.
- ClearCoinFlips$ (optional) Set to True to clear the remembered coin
flip result.
- ClearChosenCard$ (optional) Set to True to clear the chosen cards.
- ForgetDefined$ (optional) If present, remove the specified cards
from this card's remembered list.
## Control
### GainControl
Example: Act of Aggression
`A:SP$ GainControl | Cost$ 3 PR PR | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls. | LoseControl$ EOT | Untap$ True | AddKWs$ Haste | SpellDescription$ Gain control of target creature an opponent controls until end of turn. Untap that creature. It gains haste until end of turn.`
Parameters:
- NewController(Targeted player, if there is no target player, the
default is the ActivatingPlayer)
- AllValid(all valid types, no targets)
- LoseControl(LeavesPlay, Untap, LoseControl, EOT(end of turn))
### ControlExchange
### ControlPlayer
### ControlSpell
## Copy*
### CopyPermanent
Copies a permanent on the battlefield.
Parameters:
- NumCopies - optional - the number of copies to put onto the
battlefield. Supports SVar:X:????.
- Keywords - optional - a list of keywords to add to the copies
- AtEOT - optional
- Sacrifice - set to this is copy should be sacrificed at End of
Turn
- Exile - set to this is copy should be exiled at End of Turn
### CopySpellAbility
Parameters:
- Num$ <Integer>
- Restrict$ <String>
## Counter
Countering Spells or Abilities.
`A:SP$ Counter | Cost$ 1 U | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | UnlessCost$ 3 | SpellDescription$ Counter target spell unless its controller pays 3.`
`A:SP$ Counter | Cost$ U | TgtPrompt$ Select target Activated or Triggered Ability | ValidTgts$ Card | TargetType$ Activated,Triggered | SpellDescription$ Counter target activated or triggered ability.`
`A:SP$ Counter | Cost$ G | TargetType$ Spell | ValidTgts$ Instant,Aura | TargetValidTargeting$ Permanent.YouCtrl | SpellDescription$ Counter target instant or Aura spell that targets a permanent you control. `
Parameters
- TargetType - Can be Spell,Activated,Triggered. If more than one,
just put a comma in between.
- ValidTgts - a "valid" expression for types of source card (if you
don't know what it is it's just "Card")
- TargetValidTargeting- a "valid" expression for targets of this
spell's target
- Destination - send countered spell to: (only applies to Spells; ignored for Abilities)
- Graveyard (Default)
- Exile
- TopDeck
- Hand
- BottomDeck
- Shuffle
## Counters*
Factories to handle counters on cards.
### Poison
Poison gives a player the specified number of poison counters.
`A:AB$ Poison | Cost$ B T | ValidTgts$ Player | TgtPrompt$ Select target player | Num$ 2 | SpellDescription$ Target player gets 2 poison counters.`
Parameters:
- Num (required) - the number of poison counters to give
Target is optional and used if available. Defined is optional and used if no Target (defaults to "You").
### PutCounter
Put any type of counter on a game object.
`A:AB$ PutCounter | Cost$ T | CounterType$ CHARGE | CounterNum$1 | SpellDescription$ Put a charge counter on CARDNAME.`
`A:SP$ PutCounter | Cost$ G | Tgt$ TgtC | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME.`
Target is optional. If no Target is provided, the permanent will put counters on itself.
- CounterType (required) specifies the type of counter and should
appear in all caps. It should be one of the values in the Counters
enum.
- CounterNum (required) specifies how many counters will be put on
the chosen card.
### PutCounterAll
Put any type of counter on all valid cards.
- CounterType (required) specifies the type of counter and should
appear in all caps. It should be one of the values in the Counters
enum.
- CounterNum (required) specifies how many counters will be put on
the chosen cards.
- ValidCards (required) specifies the cards to add counters to.
### RemoveCounter
Remove any type of counter from a card.
Target is optional. If no Target is provided, the permanent will remove
counters from itself.
- CounterType (required) specifies the type of counter and should
appear in all caps. It should be one of the values in the Counters
enum.
- CounterNum (required) specifies how many counters will be removed
from the chosen card.
- UpTo is optional. If an effect states you may remove "up to X
counters", set this to True.
### RemoveCounterAll
Remove any type of counter from all valid cards.
- CounterType$ (required) specifies the type of counter and should
appear in all caps. It should be one of the values in the Counters
enum.
- CounterNum$ (required) specifies how many counters will be removed
from the chosen cards.
- ValidCards$ (required) specifies the card to remove counters from.
### Proliferate
### MoveCounters
Used for cards that Move Counters on Resolution, requiring the Host card
to have Counters for the Move to occur.
Parameters
- Source - The Source of the Moving Counters
- Defined - The Destination of the Moving Counters
- CounterType - The type of counter to move.
- CounterNum - The number of counters to move.
## Damage
### DealDamage
Deal damage to a specified player or permanent.
`A:AB$ DealDamage | Cost$ T | Tgt$ TgtCP | NumDmg$ 1 | SpellDescription$ CARDNAME deals 1 damage to target creature or player.`
NumDmg is required. This is the amount of damage dealt.
### EachDamage
## Debuff
Parameters
- Keywords
- Duration
An AbilityFactory for Removing Keywords, either temporarily or for longer durations.
## Destroy
These APIs handles destruction of cards on the battlefield.
`A:SP$Destroy | Cost$ 1 W W | ValidTgts$ Artifact,Enchantment | TgtPrompt$ Select target artifact or enchantment | SpellDescription$ Destroy target artifact or enchantment.`
## Effect
Effect is an oddball of the AF family. Where usually AFs have similarities to each other to help with AI use, Effect doesn't fall under that jurisdiction. In general, an effect is some type of SA that
lasts longer than its resolution.
A good example is High Tide. For the rest of the turn, High Tide makes
all Islands produce an extra mana. It doesn't matter if the Island was
in play, if it turned into an Island after High Tide was cast, any of that.
`A:SP$ Effect | Cost$ U | Name$ High Tide Effect | Triggers$ IslandTrigger | SVars$ TrigMana | SpellDescription$ Until end of turn, whenever a player taps an Island for mana, that player adds U to his or her mana pool (in addition to the mana the land produces).`
`SVar:IslandTrigger:Mode$ TapsForMana | ValidCard$ Island | Execute$ TrigMana | TriggerDescription$ Whenever a player taps an Island for mana, that player adds U to his or her mana pool (in addition to the mana the land produces).`
`SVar:TrigMana:AB$Mana | Cost$ 0 | Produced$ U | Amount$ 1`
Effect is most similar to Token as it creates a pseudo-permanent, except
Effect creates in the command zone rather than the battlefield. It stays
active there for a set Duration.
Parameters
- Abilities,Triggers,SVars are comma separated lists which contain
SVars that point to the appropriate type that the Effect will gain.
- Duration is how long the Effect lasts. Right now, most effects will
last Until End of Turn. In the future, they may have other
conditions.
Duration$ Permanent for effects that have no specific Duration.
- Stackable$ False - Most Effects are assumed to be Stackable. By
setting the Stackable Flag to False, the AI will know having a
second one in play is useless, so will save it's Resource for
something else.
- Image - a file\_name\_without\_extension (image needs to reside in
the tokens directory)
## Explore
## Fight
## Fog
Fog is an ability based on the original Fog spell. "Prevent all combat
damage that would be dealt this turn." While this could be done with an
effect, the specialized nature of the AI gives it its own AF.
## Game outcome
### GameDraw
### GameLoss
### GameWin
### RestartGame
Used in the script of *Karn Liberated*
## Goad
## Investigate
## Mana
For lands or other permanent to produce mana.
`A:AB$ Mana | Cost$ T | Produced$ <ManaType> | SpellDescription$ Add W to your mana pool.`
In this example ManaType would be W.
## Manifest
## PermanentState
API for things that alter a permanent's state.
### Phases
### SetState
Changing a cards State. This is mostly for Flip Cards or the Transform mechanic.
### Tap
`A:AB$ Tap | Cost$ R | ValidTgts$ Wall | TgtPrompt$ Select target wall | SpellDescription$ Tap target wall.`
### TapOrUntap
### Untap
`A:AB$ Untap | Cost$ G | ActivationLimit$ 1| SpellDescription$ Untap CARDNAME. Activate this ability only once each turn.`
`A:SP$ Untap | Cost$ W | ValidTgts$ Permanent | TgtPrompt$ Select target permanent | SpellDescription$ Untap target permanent.`
Target is optional. If not provided, will untap the permanent with this ability.
## Play
Playing a card or cards as part of SA. The player may have to make a
choice about which card to play if there are more choices than the
number of cards to play.
Sunbird's Invocation uses many of the parameters.
Amount - How many cards can be played (a non-negative integer or "All").
Valid - Selection criteria for valid cards from the zone to cast.
ValidSA - Applied after Valid, this will filter based on all spells of the cards.
ValidZone - The zone to look in to determine the valid cards.
ShowCards - Other cards in the zone to show when selecting valid cards.
Optional - Casting the card is optional.
RememberPlayed - Remember the card played.
ForgetRemembered - Remove all remembered cards from the source (but only
if a card was successfully played)
ForgetTargetRemembered - Remove the played card from remembered cards
(but only if it was successfully played)
WithoutManaCost - The card can be cast without mana payment.
## PreventDamage
AF damage prevention effects.
- A:SP$ PreventDamage | Cost$ W | ValidTgts$ Creature | Amount$ 3
| TgtPrompt$ Select target creature | SpellDescription$ Prevent
the next 3 damage that would be dealt to target creature this
turn.
## Protection
Protection grants protection from colors or cards types, or creature
types. Anything that has "Protection from ..."
Gains - required - the thing to gain protection from (green, artifacts,
Demons, etc.) or "Choice" if you can choose one of...
Choices - optional
- if Gains$ Choice then this is a comma-delimited list of choices
## Pump
This factory handles pumping creatures power/toughness or granting abilities to permanents (usually creatures).
- A:AB$ Pump | Cost$ R | NumAtt$ +1 | SpellDescription$ CARDNAME
gets +1/+0 until end of turn.
- A:SP$ Pump | Cost$ 1 U | ValidTgts$ Creature | KW$ Shroud|
SpellDescription$ Target creature gains shroud until end of
turn. | TgtPrompt$ Select target creature.
Target is optional. If it's not provided, the activating permanent will be pumped.
NumAtt$ is optional, will pump Power.
NumDef$ is optional, will pump Toughness.
KW$ is optional, will give temporary keywords.
## Regenerate
Regenerate is similar to abilities like Pump. But for creating
Regeneration shields.
- A:AB$ Regenerate | Cost$ B | SpellDescription$ Regenerate
CARDNAME
- A:SP$ Regenerate | Cost$ W | ValidTgts$ Creature | TgtPrompt$
Select target creature | SpellDescription$ Regenerate target
creature.
Target is optional. If not provided, will regenerate the permanent with this ability.
## Repeat
Repeat the specified ability.
### Repeat
`A:SP$ Repeat | Cost$ 3 B B | RepeatSubAbility$ DBDig | RepeatOptional$ True`
- MaxRepeat - optional - the maxium times to repeat, execute repeat
ability at least once
- RepeatSubAbility - required - setup subability to repeat
- RepeatOptional - optional - you make the choice whether to repeat
the process
- RepeatPresent, RepeatCompare, RepeatDefined, RepeatCheckSVar,
RepeatSVarCompare - optional - condition check
### RepeatEach
- RepeatSubAbility - required - to set up repeat subability
- RepeatCards - to repeat for each valid card (zone: present zone of
the valid repeat cards, default: battlefield)
- DefinedCards
- RepeatPlayers - to repeat for each valid player
- RepeatCounters - to repeat for each valid counters
## Reveal
### RevealHand
Look at a player's hand.
Target or Defined is required.
`A:AB$ RevealHand | Cost$ T | ValidTgts$ Player | TgtPrompt$ Select target player | SpellDescription$ Look at target player's hand.`
### Reveal
`A:AB$ Reveal | Cost$ 2 U T | Defined$ You | RevealValid$ Card.Blue | AnyNumber$ True | RememberRevealed$ True`
Parameters:
- RevealValid: to limit the valid cards.
- AnyNumber
- Random
- RememberRevealed: to remember the cards revealed
### PeekAndReveal
This AF is very similar to things that Dig can do, but handle a much
simpler form, with less complex coding underneath. Similar to how
RearrangeTopOfLibrary could be handled with Dig.
Primarily used with cards that allow you to Peek at the top card of your
library, and allow you to reveal it if it's of a certain type. The
Kinship cards fit this bill perfectly, so they are used to simplify the
complex popups that would be required if using multiple Dig
SubAbilities.
RevealOptional - Whether or not the Reveal is optional.
RememberRevealed - Whether to remember the revealed cards (after
filtering by Valid)
RememberPeeked - Whether to remember the peeked cards (only if they are
not revealed\!)
RevealValid - defaults to Card, but allows you to set a specific
ValidType if you can only have certain things
PeekAmount - defaults to 1, but allows you to peek at multiple cards if
possible
## RollDice
## Sacrifice
Usually you choose a player and that player has to sacrifice something
`A:SP$ Sacrifice | Cost$ 1 B | ValidTgts$ Player | SacValid$ Creature | SacMessage$ Creature | Amount$ 2 | SpellDescription$ Target player sacrifices a creature.`
Destroy$ True - An optional parameter for destroying permanents target
player chooses (eg: Burning of Xinye, or Imperial Edict).
`A:SP$ Sacrifice | Cost$ 1 B | ValidTgts$ Opponent | SacValid$ Creature | SacMessage$ Creature | Destroy$ True | SpellDescription$ Target opponent chooses a creature he or she controls. Destroy it.`
## Scry
`A:AB$ Scry | Cost$ 1 T | ScryNum$ 2`
## StoreSVar
## Token
Token simply lets you create tokens of any type.
`A:SP$ Token | Cost$ 3 W U | TokenImage$ W 1 1 Bird Flying | TokenAmount$ X | TokenName$ Bird | TokenTypes$ Creature,Bird | TokenOwner$ You | TokenColors$ Blue | TokenPower$ 1 | TokenToughness$ 1 | TokenKeywords$ Flying`
This ability factory does not take a target. All the parameters are
mandatory except for TokenKeywords. If you provide a non-integer for
TokenAmount, TokenPower or TokenToughness the AF will attempt to look for
an SVar of that name and interpret it's contents as a Count$ line. Worth
noting is that TokenTypes and TokenColors are simple commaseparated
lists while TokenKeywords is a list where the items are separated by
"\<\>". If TokenImage is not provided, the factory will attempt to
construct a filename on it's own. TokenOwner can use Defined-like
parameters, such as "You" "Opponent" or the new Triggered-Variables.
You can also use the parameters TokenAbilities$, TokenTriggers$ and
TokenSVars$ to give the created tokens any number of either. For
example, here's how Growth Spasm creates an Eldrazi Spawn token complete
with ability.
SVar:DBToken:DB$Token | TokenAmount$ 1 | TokenName$ Eldrazi Spawn | TokenTypes$ Creature,Eldrazi,Spawn | TokenOwner$ You | TokenColors$ Colorless | TokenPower$ 0 | TokenToughness$ 1 | TokenAbilities$ ABMana SVar:ABMana:AB$Mana | Cost$ Sac<1/CARDNAME> | Produced$ 1 | Amount$ 1 | SpellDescription$ Add 1 to your mana pool.
As another example, here's Mitotic Slimes' use of TokenTriggers$:
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigTokenSenior | TriggerDescription$ When CARDNAME is put into a graveyard from the battlefield, put two 2/2 green Ooze creature tokens onto the battlefield. They have "When this creature is put into a graveyard, put two 1/1 green Ooze creature tokens onto the battlefield."
SVar:TrigTokenSenior:AB$ Token | Cost$ 0 | TokenImage$ g 2 2 ooze | TokenName$ Ooze | TokenTypes$ Creature,Ooze | TokenColors$ Green | TokenOwner$ You | TokenPower$ 2 | TokenToughness$ 2 | TokenAmount$ 2 | TokenTriggers$ TriggerJunior | TokenSVars$ TrigTokenJunior
SVar:TriggerJunior:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigTokenJunior | TriggerDescription$ When this creature is put into a graveyard, put two 1/1 green Ooze creature tokens onto the battlefield. SVar:TrigTokenJunior:AB$Token | Cost$ 0 | TokenImage$ g 1 1 ooze | TokenName$ Ooze | TokenTypes$ Creature,Ooze | TokenColors$ Green | TokenOwner$ You | TokenPower$ 1 | TokenToughness$ 1 | TokenAmount$ 2
## Trigger
If possible split the SpellDescription of the effect so the part for the trigger can become the StackDescription directly.
### DelayedTrigger
### ImmediateTrigger
## Turn structure
### AddPhase
### AddTurn
`A:SP$ AddTurn | Cost$ 1 U | NumTurns$ 1 | SpellDescription$ Take an extra turn after this one.`
### EndTurn
### ReverseTurnOrder
### SkipPhase
### SkipTurn
## ZoneAffecting
For specific effects that handle zones in a specific manner
### ChangeZone
ChangeZone is a united front of any card that changes zone. This does
not include: drawing, discarding, destroying, or milling, as these
represent specific words on which triggers and replacements can react.
There are two primary forms, but the distinction is handled mostly in
the codebase. The only thing that is required is to set appropriate parameters.
Origin and Destination are both required.
Origin is where the card is coming from.
Destination is where the card is going to. If Destination is Library, a
LibraryPosition is recommended, but not required. **Default value of the
LibraryPosition is 0.** 0 represents the top of the library, -1 represents the bottom.
There are two primary versions of ChangeZone.
#### Hidden Origin
The first is hidden, generally used for Origin zones that are not known
information, like the Library or the Hand. The choice of "What card is
changing zones?" happens during resolution.
`A:SP$ ChangeZone | Cost$ W | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Artifact,Enchantment | ChangeNum$ 1 | SpellDescription$ Search your library for an artifact or enchantment card and reveal that card. Shuffle your library, then put the card on top of it.`
`A:AB$ ChangeZone | Cost$ T | Origin$ Hand | Destination$ Battlefield | ChangeType$ Land | ChangeNum$ 1 | Optional$ True | SpellDescription$ You may put a land card from your hand onto the battlefield.`
For Hidden, things like ChangeType and ChangeNum are used to restrict
what can ChangeZone, and how many do. There are two parameters special
to Hidden Origin:
"Chooser" defines which player has to decide which card changes zone
(example You, Opponent).
"Mandatory" most of these abilities are not mandatory, but some are.
#### Known Origin
The second is known, generally used for Origin zones that are known
information, like the Battlefield or the Graveyard. The choice of "What
card is changing zones?" happens on activation, generally by targeting.
`A:AB$ ChangeZone | Cost$ 1 U T | TgtPrompt$ Choose target artifact card in your graveyard | ValidTgts$ Artifact.YouCtrl | Origin$ Graveyard | Destination$ Library | SpellDescription$ Put target artifact card from your graveyard on top of your library.`
`A:SP$ ChangeZone | Cost$ U U | ValidTgts$ Permanent | TgtPrompt$ Select target permanent | Origin$ Battlefield | Destination$ Hand | SpellDescription$ Return target permanent to its owner's hand.`
For Known, since it just uses Target, normal target parameters are used in this Scenario.
### ChangeZoneResolve
This is a helper AF, for chained effects that create multiple permanents which should enter the battlefield at the same time.
To use it, you need to set the param "ChangeZoneTable" on the first effect and then call this at the end.
This is supported by the following effects:
- Amass
- CopyPermanent
- RepeatEach (_NOTE: if you wrap the creation, you don't need to call this AF on its own_)
- Token
### Dig
Dig is for an ability that does basically this: "You look at the X cards
of your Library, put Y of them somewhere, then put the rest somewhere."
Think of Impulse.
- DigNum - Required - look at the top number of cards of your library.
- Reveal - Optional - for abilities that say "Reveal the top X cards
of your library". Default is false.
- SourceZone - Optional - the zone to dig in. Default is Library.
- DestinationZone - Optional - the zone to put the Y cards in. Default
is Hand.
- LibraryPosition - Optional - if DestinationZone is Library, use this
to specify position. Default is -1 (bottom of library).
- ChangeNum - Optional - the number of cards to move to the
DestinationZone (or "All" when it's for things like "put all lands
revealed this way into your hand"). Default is 1.
- ChangeValid - Optional - use this to specify if "you may move an
artifact to DestinationZone". Default is any Card.
- AnyNumber - Optional - use if you can move any number of Cards to
DestinationZone. Default is false. (think of Lead the Stampede)
- Optional - Optional - set this if you "may" move a card to
DestinationZone. Default is false.
- DestinationZone2 - Optional - the zone to put the rest of the cards
in. If it is library, you are prompted for the order. Default is
Library.
- LibraryPosition2 - Optional - if DestinationZone2 is Library, use
this to specify position. Default is -1 (bottom of library).
### DigUntil
### Discard
`A:AB$ Discard | Cost$ T | ValidTgts$ Opponent | NumCards$ 1 | Mode$ TgtChoose | SpellDescription$ Target opponent discards a card. `
- NumCards - the number of cards to be discarded (may be integer or X)
- Mode - the mode of discard - should match spDiscard
- Random
- TgtChoose
- RevealYouChoose
- Hand
- DiscardValid - a ValidCards syntax for acceptable cards to discard
- UnlessType - a ValidCards expression for "discard X unless you
discard <Type>"
### Draw
`A:AB$ Draw | Cost$ 1 Sac<1/CARDNAME> | NumCards$ 1 | SpellDescription$ Draw a card.`
### Mill
`A:AB$ Mill | Cost$ 2 T | NumCards$ 2 | ValidTgts$ Player | TgtPrompt$ Choose a player to mill | SpellDescription$ Target player puts the top two cards of his or her library into his or her graveyard.`
### RearrangeTopOfLibrary
### Shuffle
Used for shuffling a player's library
- Optional - Set this parameter if the user should be
prompted Yes/No to shuffle the given library. Default is false.
### TwoPiles
## Vote

View File

@@ -1,235 +0,0 @@
A reference guide for scripting cards using the API parsed by the Forge engine.
# Base Structure
By opening any file in the /res/cardsfolder folder you can see the basic structure of how the data is created.
Here's an example of a vanilla creature:
```
Name:Vanilla Creature
ManaCost:2 G
Types:Creature Beast
PT:2/2
Oracle:
```
The name of this card is Vanilla Creature.
It's casting cost is {2}{G}.
It has the types Creature and Beast.
It has a Power-Toughness of 2/2.
It will not display any additional text in the card's template.
If a card has two faces, use AlternateMode:{CardStateName} in the front face and separate both by a new line with the text "ALTERNATE".
There are a few other properties that will appear in many cards. These are
| Property | Description
| - | -
|`A`|[Ability effect](AbilityFactory)
|`AI`|RemoveDeck:<br />* `All`<br />This will prevent the card from appearing in random AI decks. It is applicable for cards the AI can't use at all like Dark Ritual and also for cards that the AI could use, but only ineffectively like Tortoise Formation. The AI won't draft these cards.<br />* `Random`<br /> This will prevent the card from appearing in random decks. It is only applicable for cards that are too narrow for random decks like Root Cage or Into the North. The AI won't draft these cards.<br />* `NonCommander`<br />
|`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />* `Colors:red` - This is used on Kobolds of Kher Keep, which has a casting cost of {0} and requires a red indicator to make it red.<br /><br />* `Colors:red,green` - Since Arlinn, Embraced by the Moon has no casting cost (it's the back of a double-faced card), the red and green indicator must be included.
|`DeckHints`|AI-related hints for a deck including this card<br /><br />To improve synergy this will increase the rank of of all other cards that share some of its DeckHints types. This helps with smoothing the selection so cards without these Entries won't be at an unfair disadvantage.<br /><br />The relevant code can be found in the [CardRanker](https://github.com/Card-Forge/forge/blob/master/forge-gui/src/main/java/forge/gamemodes/limited/CardRanker.java) class.
|`DeckNeeds`|This can be considered a stronger variant when the AI should not put this card into its deck unless it has whatever other type is specified. The way this works is "inverted": it will directly decrease the rank of the card unless other cards are able to satisfy its types.<br />If a card demands more than one kind of type you can reuse it:<br />`DeckNeeds:Type$Human & Type$Warrior` will only find Human Warrior compared to `DeckNeeds:Type$Human\|Warrior` which is either
|`DeckHas`|specifies that the deck now has a certain ability (like, token generation or counters) so that the drafting/deckbuilding AI knows that it now meets requirements for DeckHints/DeckNeeds. This is actually very useful since many of these (such as `Ability$Graveyard, Ability$Token, Ability$Counters`) are not deduced by parsing the abilities, so an explicit hint is necessary. Using the other types is also supported in case the implicit parsing wouldn't find it (TokenScript$ is also included).<br />It doesn't require exact matching to have an effect but cards that care about multiple entries for a given type will be judged higher if a card seems to provide even "more" synergy for it.<br />Example:<br />Chishiro has two abilities so `DeckHas:Ability$Token & Ability$Counters` is used, therefore score for `DeckNeeds:Ability$Token\|Counters` is increased
|`K`|Keyword (see below)
|`Loyalty`|Number of starting loyalty counters
|`ManaCost`|Cost to cast the card shown in mana shards<br /><br />This property is required. It has a single parameter that is a mana cost.<br /><br />* `ManaCost:no cost` for cards that cannot be cast<br />* `ManaCost:1 W W` sets the casting cost to {1}{W}{W}
|`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power"
|`Oracle`|The current Oracle text used by the card.<br /><br />We actually have a Python Script that runs to be able to fill in this information, so don't worry about manually editing a lot of cards when Wizards decides to change the rules. <br /><br />This field is used by the Deck Editor to allow non-Legendary Creatures to be marked as potential commanders. Make sure "CARDNAME can be your commander." appears in the oracle text.
|`PT`|Power and toughness
|`R`|[Replacement effect](Replacements)
|`S`|[Static ability](static-abilities)
|`SVar`|String variable. Used throughout scripting in a handful of different ways.
|`T`|[Triggered ability](Triggers)
|`Text`|Additional text that needs to be displayed on the CardDetailPanel that doesn't have any spell/ability that generates a description for it, for example "CARDNAME can be your commander." or "X can't be 0.".
|`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem
Rarity and Set info are now defined in edition definition files. These can be found at /res/reditions path.
## Conventions
- filename: all lowercase, skip special characters, underscore for spaces
- Unix(LF) line endings
- use empty lines only when separating multiple faces on a card
- AI SVars right before the Oracle
- try to avoid writing default params to keep scripts concise
- e.g. just `SP$ Draw` instead of `SP$ Draw | Defined$ You | NumCards$ 1`
# Keywords
All keywords need to be prepended with "K:" to be parsed correctly. Each keyword must appear on a separate line.
## Keywords without Parameters
This section is for Keywords that require no additional parameters and are one or two words long. Most of these you would see exactly on cards in the game.
Examples:
- Cascade
- Cipher
- Convoke
- Devoid
- First Strike
- Flash
- Haste
- Indestructible
- Mentor
- Provoke
- Reach
- Shroud
- Split second
- Storm
- Trample
- Undying
- Vigilance
- Wither
## Keywords with parameters
- Adapt:{cost}
- Afterlife:{N}
- AlternateAdditionalCost:{cost}
- Amplify:{cost}:{validType(comma separated)}
- Annihilator:{magnitude}
- Bloodthirst:{magnitude}
- Bestow:{cost}
- Bushido:{magnitude}
- Champion:{validType}
- Crew:{cost}
- Cumulative upkeep:{cost}:{Description}
- Cycling:{cost}
- Dash:{cost}
- Devour:{magnitude}
- Dredge:{magnitude}
- Echo:{cost}
- Emerge:{cost}
- Enchant:{Type}
- Entwine:{cost}
- Equip:{cost}
- Evoke:{cost}
- Fabricate:{cost}
- Fading:{FadeCounters}
- Flashback:{cost}
- Foretell:{cost}
- Fortify:{cost}
- Graft:{value}
- Haunt:{ability}:<Ability>
- Hexproof:{ValidCards}:[Description]
- Kicker:{cost}
- Level up:{cost}
- Madness:{cost}
- MayEffectFromOpeningHand:{Effect}
- Miracle:{cost}
- Modular:{magnitude}
- Monstrosity:{cost}
- [Mega]Morph:{cost}
- Multikicker:{magnitude}
- Mutate:{cost}
- Ninjutsu:{cost}
- Outlast:{cost}
- Partner:{CardName}
- Protection:{ValidCards}:{Description}
- Prowl:{cost}
- Rampage:{magnitude}
- Recover:{cost}
- Renown:{N}
- Replicate:{cost}
- Ripple:{magnitude}
- Soulshift:{magnitude}
- Strive:{cost}
- Suspend:{turns}:{cost}
- Transmute:{cost}
- Toxic:{poisonCounters}
- TypeCycling:{Type}:{cost}
- Unearth:{cost}
- Vanishing:{TimeCounters}
## Plaintext keywords
These are hardcoded but not truly keywords rules-wise and will eventually be turned into static abilities.
Only listing the most common ones here so you can recognize them.
CARDNAME is replaced by the card's name ingame.
- CARDNAME can't attack or block alone.
- CARDNAME can't block unless a creature with greater power also blocks.
- CARDNAME must be blocked if able.
- Remove CARDNAME from your deck before playing if you're not playing for ante.
- You may choose not to untap CARDNAME during your untap step.
- CantSearchLibrary
# General SVars
`SVar:SoundEffect:goblinpolkaband.mp3`
The sound system supports a special SVar that defines the sound that should be played when the spell is cast.
`SVar:X:Count$`
Count is our general value computation function. It's quite varied with a lot of different things it can calculate and is often being updated.
# AI specific SVars
`SVar:AIPreference:SacCost$Creature.token,Creature.cmcLE2`
`SVar:AntiBuffedBy:[ValidCards]`
If a permanent with this SVar is on the battlefield under human control
the AI will play the specified cards in Main1. Applicable for cards like
Heart Sliver or Timid Drake.
`SVar:BuffedBy:[ValidCards]`
If a permanent with this SVar is on the battlefield under its control
the AI will play the specified cards in Main1. Applicable for creatures
with a P/T setting static ability (Kithkin Rabble) or additional buffes
(Radiant, Archangel).
`SVar:EnchantMe:[Multiple]/[Once]`
Creatures with "Multiple" in this SVar will always be prefered when the
AI enchants (Rabid Wombat), creatures with "Once" only if they are not
enchanted already (Gate Hound).
`SVar:EquipMe:[Multiple]/[Once]`
Creatures with "Multiple" in this SVar will always be prefered when the
AI equippes (Myr Adapter), creatures with "Once" only if they are not
equipped already (Kor Duelist).
`SVar:AIEvaluationModifier:[ValidAmount]`
`SVar:EndOfTurnLeavePlay:True`
`SVar:maxLevel:`
`SVar:HasCombatEffect:True`
`SVar:HasAttackEffect:True`
`SVar:HasBlockEffect:True`
`SVar:MustAttack:True`
`SVar:MustBeBlocked:True`
`SVar:NeedsToPlayVar:[ValidCards]`
`SVar:ManaNeededToAvoidNegativeEffect:`
`SVar:NonStackingEffect:True`
`SVar:PlayMain1:TRUE/ALWAYS/OPPONENTCREATURES`
The AI will play cards with this SVar in its first main phase. Without other AILogic, it will usually not play any permanents without this in Main1.
`SVar:SacMe:[number]`
The AI will sacrifice these cards to pay costs. The higher the number the higher the priority. Example: Hatching Plans has SVar:SacMe:5.
`SVar:Targeting:Dies`
`SVar:UntapMe:True`
The AI will prioritize untapping of this card.
`SVar:AIUntapPreference:`
`SVar:NoZeroToughnessAI:True`

View File

@@ -1,186 +0,0 @@
Cost is a class that attempts to streamline costs throughout all cards. It requires that each cost is separated by a space. I will use examples that could be found in Ability, although certain Keyworded abilities do use Cost too.
# Common
## Description
Description is an optional last parameter in the cost. This is to allow
for complex Type definitions to have a nice Description that is readable.
## CostDesc / PrecostDesc
## UnlessCost
UnlessCost allows the player specified with UnlessPayer (same as
Defined, defaults to TargetedController) to pay mana to prevent the
resolving of the ability. If the script has the param "UnlessSwitched",
then the player pays mana to resolve the ability (usually used to handle
"any player may pay ..." ).
## XChoice
XChoice is the variable that basically means "You can choose whatever
you want for this variable. But you need to decide what X is before you
start paying." This would commonly appear as an SVar definition of X.
## xPaid
xPaid is the amount of Mana Paid for an X Cost. There are a few cards
that will use the X Payment to determine other costs (like Abandon Hope)
This would commonly appear as an SVar definition of X.
## CARDNAME
For Costs that do something to themselves (ex. Discard Self, Sacrifice
Self)
# Types of Cost
## Discard
Discard has two required parameters and one optional in the form
Discard<Num/Type/Description>
- The first is how many cards are being discarded.
- The second is what card types can be discarded. (Hand for the whole
hand, Random for chosen randomly)
## Draw
## Exert
## Exile
Exile has two required parameters and one option in the form of
Exile<Num/Type/Description>
There are also a few sister abilities that all fit under the Exile
umbrella.
- Exile (for cards on the Battlefield)
- ExileFromGraveyard
- ExileFromHand
- ExileFromTop (for cards on top of your library, this doesn't default
Type to Card, so make sure you add it)
Some Examples
- Exile&lt;1/Creature&gt;
- Exile&lt;1/CARDNAME&gt;
- ExileFromHand&lt;1/CARDNAME&gt;
- ExileFromHand&lt;2/Creature&gt;
- ExileFromGrave&lt;1/CARDNAME&gt;
- ExileFromGrave&lt;1/Treefolk&gt;
- ExileFromTop&lt;10/Card&gt;
## FlipCoin
Only used by "Karplusan Minotaur".
## Mana
- Cost$ 2
- 2 colorless mana
- Cost$ B R
- 1 black and 1 red mana
- Cost$ WG
- Hybrid White/Green mana
- Cost$ S
- Snow Mana
- Cost$ Mana&lt;2\\Creature&gt;
- 2 colorless produced by a source with type 'creature'. Note the
backslash - it was chosen because hybrid costs already use slash
Here's some examples:
- Discard&lt;1/Card&gt;
- "Discard 1 Card"
- Discard&lt;0/Hand&gt; (The number is ignored when Hand is used as a
type.)
- Discard your hand
- Discard&lt;2/Random&gt;
- Discard 2 Cards at Random
- Discard&lt;1/CARDNAME&gt;
- Discard Self (CARDNAME)
- Discard&lt;1/Creature.Black/Black Creature&gt;
- Discard 1 Black Creature
## Mill
## Subtract(Remove) Counter
SubCounter has two required parameters in the form of
SubCounter<Num/CounterName>
- SubCounter&lt;2/P1P1&gt;
- SubCounter&lt;1/CHARGE&gt;
Remember the token name should appear all in caps.
As third parameter you can use a ValidCard.
## Sacrifice
Sacrifice has two required parameters and one optional parameter in the
form of Sac<Num/Type/Description>
- Sac&lt;1/Artifact&gt;
- Sac&lt;1/CARDNAME&gt;
## Tap
- Cost$ T
## Untap
- Cost$ Untap
\- or -
- Cost$ Q
## Unattach
## PayEnergy
## PayLife
PayLife has one required parameter in the form of PayLife<Num>
- PayLife&lt;2&gt;
## GainLife
## TapXType
TapXType has two required parameters and one option in the form of
tapXType<Num/Type/Description>
- tapXType&lt;3/Creature.White&gt;
## Return
Return has two required parameters and one optional in the form of
Return<Num/Type/Description>
- Return&lt;1/Land&gt;
- Return&lt;1/CARDNAME&gt;
## Reveal
# Putting it Together
Putting it together is pretty simple. If a card needs to pay mana and tap, it would look like this:
- Cost$1 W T
For a spell that has an additional cost of sacrificing a land, put the
mana cost and the additional cost in the cost:
- Cost$2 G Sac&lt;1/Land&gt;
One of the features of Cost is you can have more than one of the same Cost type:
- Cost$ Sac&lt;1/Swamp&gt; Sac&lt;1/Creature&gt;
There are many examples, but they mostly fall into those categories.

View File

@@ -1,90 +0,0 @@
Replacement create replacement effects, as you'd expect. Their script
follows the format introduced by AbilityFactory; simply begin a line
with "R:", to indicate that it's for a replacement effect, followed by a
collection of name-value pairs (name and value are separated by $)
separated by pipes (|). All Replacement effects expect an "Event$"
parameter, which declares what event should be replaced. Most replacement effects will also have a "ReplaceWith$" parameter which points to an SVar which contains what should replace the event. They may have a "Prevent$True" parameter, instead though, which means that nothing happens instead of the event.
Similarly to triggers, the replacing code can access special variables
pertaining to the event it replaced (like triggered-variables). These
are specific to each event, and is listed below. Most replacement effects
will also have a "Description$" parameter which is simply the card text
for the ability.
## DamageDone
DamageDone events are checked when damage is about to be assigned to a
card or player. There are 5 special parameters:
- ValidSource - The damage source must match this for the event to be
replaced.
- ValidTarget - The damage target must match this for the event to be
replaced.
- DamageAmount - The amount of damage must match this.
- IsCombat - If true, the damage must be combat damage, if false, it
can't be.
- IsEquipping - If true, the host card must be equipping something.
There are 3 Replaced-variables:
- DamageAmount - The amount of damage to be assigned.
- Target - The target of the damage.
- Source - The source of the damage.
## Discard
Discard events are checked when a player is about to discard a card.
There are 4 special parameters:
- ValidPlayer - The player who would discard must match this.
- ValidCard - The the card that would be discarded must match this.
- ValidSource - The card causing the discard must match this.
- DiscardFromEffect - If true, only discards caused by spells/effects
will be replaced. Cleanup/statebased discards will not.
There are 2 Replaced-variables:
- Card - The card that would be discarded.
- Player - The player that would have discarded
## Draw
Draw events are checked when a player is about to draw a card. There is
1 special parameter:
- ValidPlayer - The player who would draw must match this.
There are no Replaced-variables.
## GainLife
GainLife events are checked when a player would gain life. There is 1
special parameter:
- ValidPlayer - The player who would gain life must match this.
There is 1 Replaced-variable:
- LifeGained - The amount of damage to be gained.
## GameLoss
GameLoss events are checked when a player would lose. There is 1 special
parameter:
- ValidPlayer - The player who would lose must match this.
There are no Replaced-variables.
## Moved
Moved events are checked when a card would be moved between zones. There
are 3 special parameters:
- ValidCard - The moving card must match this.
- Origin - The card must be moving from this zone.
- Destination - The card must be moving to this zone.
There is 1 Replaced-variable:
- Card - The moving card.

View File

@@ -1,103 +0,0 @@
### Activation
Activation$ <Option> can accept several different values:
- Metalcraft
- Threshold
- Hellbent
These basically follow the text of the card.
### ActivationZone
ActivationZone$ <Zone> For cards that have abilities that you can
activate outside of the Battlefield
### Flashback
While Flashback isn't technically an SA\_Restriction it's listed here
because it's handled very similarly in AFs. If sets the ActivationZone
to Graveyard and sets the FlashbackAbility flag of a SpellAbility which
will exile the card as it finishes resolving.
### SorcerySpeed
SorcerySpeed $True For cards that can only activate if you could cast a
sorcery.
### PlayerTurn
PlayerTurn $True For cards that can only be activated during the
activating player's turn.
### OpponentTurn
OpponentTurn $True For cards that can only be activated during the
opponent's turn of the activating player.
### AnyPlayer
AnyPlayer $True For cards that can be activated by any player. Prophecy
has lots of examples of these types of cards.
### ActivationLimit
ActivationLimit$<activationsPerTurn> For cards that have a limited
amount of uses
### ActivationNumberSacrifice
ActivationNumberSacrifice$<activations> For cards that if they you
activate them more than a certain amount per turn are sacrificed at end
of the turn.
### ActivationPhases
ActivationPhases$\<Phases,Seperated,By,Commas\> For Abilities can only
be activated during upkeep then it'll be ActivationPhases$Upkeep if it's
before Declare Attackers than it's
ActivationPhases$Upkeep,Draw,Main1,BeginCombat
This can also be handled in a range. ActivationPhases$
BeginCombat-\>EndCombat for Spells that can only be cast during combat.
### ActivationCardsInHand
ActivationCardsInHand$<CardsInHand> For Abilities with Hellbent (for 0)
or Library of Alexandria (for 7)
### Planeswalker
Planeswalker$ True For Planeswalker Abilities. These can only be
activated at Sorcery Speed, and only if no Planeswalker Abilities
(including this ability) on this card have been activated that turn.
Add Ultimate$ True when applicable for the AI and achievements.
### Present
Present is a Restriction that comes in two parts. IsPresent is required.
And PresentCompare is optional.
#### IsPresent
IsPresent$ <ValidCards> gets all cards on the battlefield that is
considered a ValidCard.
#### PresentCompare
PresentCompare$\<2 Letter Comparison\><Comparator>. The comparisons are
LE,LT,EQ,NE,GE,GT and translated to \<=, \<, ==, \!=, \>=, \>. The
comparator is on the right side of the equation.
If PresentCompare is **missing**, the Restriction defaults to GE1.
### Condition
Condition is similar to a IsPresent restriction, except it's checked on Resolution of the spell and not on Activation.
### CheckSVar
CheckSVar specifies that the results computed from an SVar (Usually via
xCount) must equal a certain value which you can specify in the
accompanying SVarCompare parameter. SVarCompare is the same as
PresentCompare above.

View File

@@ -1,219 +0,0 @@
# Affected
A defined parameter can be either for something that states what is
receiving the action. Remember this is non-targeted!
## Defined Players
Defined Players are for SAs like Draw or Discard. They tell you who is
receiving this action (but not in a targeted way).
### You
The most common of these is Defined$ You. It means exactly what you
think "You draw a card." "You discard a card." Cards that don't seem to
define anything that affect Players should default to "You"
A spell that lets you draw a card is:
SP$ Draw \| Defined$ You
It is important to include this in SAs that have an unclear "Default"
value such as Damage.
### Opponent
Also, fairly common is Defined$ Opponent. It means "Deals damage to each
opponent" "Each Opponent discards a card."
A spell that deals damage to each opponent is:
SP$DealDamage \| Defined$ Opponent \| NumDmg$ 5
### TargetedController
### TargetedOwner
### EnchantedController
It means, "the controller of the enchanted thing." This would be the
"that player" part of things like "At the beginning of enchanted
creature's controller's upkeep, that player loses 1 life."
### EnchantedOwner
### AttackingPlayer
Things like "Whenever Souls of the Faultless is dealt combat damage, you
gain that much life and attacking player loses that much life."
### DefendingPlayer
Things like "Whenever CARDNAME deals combat damage, defending player
discards a card."
### Player
Each player.
## Defined Cards
Defined Cards are for SAs like Pump or Regenerate. They tell you who is
receiving this action (but not in a targeted way).
### Self
The most common of these is Defined$ Self. It means exactly what you
think "I gain Flying" "Regenerate Me." Abilities that don't seem to
define anything should affect Cards default to "Self"
AB$Regenerate\| Defined$ Self
It is important to include this in SAs that have an unclear "Default"
value such as Damage.
### Enchanted
Enchanted is fairly common. It means "do this action to the card I'm
Enchanted to"
A simple example is a card that Pumps the attack of the Enchanted creature
AB$Pump \| Defined$ Enchanted \| NumAtt$ +1
### Equipped
Equipped isn't very common, but is very similar. It means "do this
action to the card I'm Equipped to"
AB$Pump \| Defined$ Equipped \| NumAtt$ +2 \| NumDef$ +2
### Remembered
### ThisTurnEntered
ThisTurnEntered lets you make the ability act on valid cards that were
put in a certain zone,from a certain zone, this turn. The format is
"ThisTurnEntered <Destination> \[from <Origin>\] <ValidExpression>".
For example: No Rest for the Wicked and the like would use:
`AB$ ChangeZone | Cost$ Sac<1/CARDNAME> | Defined$ ThisTurnEntered Graveyard from Battlefield Creature.YouCtrl | SpellDescription$ Return to your hand all creature cards in your graveyard that were put there from the battlefield this turn.`
### Targeted
Targeted will only appear on a [SubAbility][]. It means "Do this action
to whatever a parent Ability targeted"
That may sound confusing so here's an example.
If you had a spell that says "Untap target Creature. It gains +1/+1
until end of turn" it would look like similar to this.
SP$Untap \| ValidTgts$ Creature \| SubAbility$ DBPump
SVar:DBPump:DB$Pump \| Defined$ Targeted \| NumAtt$ +1 \| NumDef$ +1
# Targets
Each element follows the form:
CardType{.restriction}{+furtherRestriction}
The restrictions are optional.
CardType may be any type, generally the supertypes like Creature,
Artifact, Enchantment, Land, etc. However, it could also be Elf or
Goblin... though that would also include Elf Enchantments, for example.
To specify an Elf Creature, then it should be "Creature.Elf".
"Permanent" represents any permanent, "Card" any card....
Restrictions other than type that are interpreted: (case sensitive)
`   named{Name}`
`   notNamed{Name}`
`   sameName`
`   NamedCard`
`   Color, nonColor (White, nonWhite, etc)`
`   Colorless, nonColorless`
`   Multicolor, nonMulticolor`
`   MonoColor, nonMonoColor`
`   ChosenColor`
`   SharesColorWith`
`   YouCtrl, YouDontCtrl`
`   YouOwn, YouDontOwn`
`   EnchantedPlayerCtrl`
`   OwnerDoesntControl`
`   ControllerControls{type}`
`   RememberedPlayerCtrl (use if Remember will be set before resolution)`
`   TargetedPlayerCtrl (only use if restriction is needed wile initial targeting happens)`
`   Other,Self`
`   AttachedBy,Attached`
`   DamagedBy,Damaged`
`   with{Keyword}, without{Keyword}`
`   tapped, untapped`
`   faceDown`
`   enchanted, unenchanted, enchanting, EnchantedBy`
`   equipped, unequipped, equipping, EquippedBy`
`   dealtDamageToYouThisTurn`
`   wasDealtDamageThisTurn`
`   wasDealtDamageByHostThisTurn`
`   wasDealtDamageByEquipeeThisTurn`
`   token, nonToken`
`   kicked, notkicked`
`   enteredBattlefieldThisTurn`
`   power{cmp}{X or #}`
`   toughness{cmp}{X or #}`
`   cmc{cmp}{X or #}`
`   {cmp} is a comparator:`
`       LT - Less Than`
`       LE - Less than or Equal`
`       EQ - EQual`
`       GE - Greater than or Equal`
`       GT - Greater Than`
`   X is parsed from "SVar:X:Count$___"`
`   greatestPower, leastPower`
`   greatestCMC`
`   counters{cmp}{X or #}{Type}`
`   attacking, notattacking`
`   blocking, notblocking`
`   blocked, unblocked, blockedBySource`
`   kicked, notkicked`
`   evoked`
`   hasDevoured, hasNotDevoured`
`   non{Type}`
`   ChosenType{Type} `
`   {Type}`
`   CostsPhyrexianMana`
`   Above, DirectlyAbove`
`   TopGraveyardCreature`
`   TopGraveyard`
`   Cloned`
`   isRemembered`
Examples:
"Artifact or Enchantment" would be represented as 2 elements "Artifact,Enchantment"
"non-black, non-artifact creature" would be represented as "Creature.nonBlack+nonArtifact"
"creature with power 2 or less" would be "Creature.powerLE2"
"nonbasic land" is "Land.nonBasic"
"creature with flying" is "Creature.withFlying"
"creature self with four or more level counters" is
"Creature.countersGE4LEVEL+Self"

View File

@@ -1,918 +0,0 @@
Triggers define when an ability should be automatically added to the
stack. Their syntax in the card files are very similar to that used by
the various AbilityFactorys, that is: every trigger line starts with
"T:" (without the quotes) and is a collection of name-value pairs (name
and value are separated by $) separated by pipes (|). Like with
AbilityFactorys, there are a few that show up almost always if not
always. There are two parameters that are always present on a trigger:
- Mode - Specifies what kind of situation the trigger should wait for.
- TriggerDescription - Describes the trigger, much like AF's
SpellDescription parameter.
- (Execute - Specifies the name of the SVar that holds the ability to
run when the trigger goes off.)
_NOTE: The Execute parameter may be absent, if you are writing a delayed triggered ability. See below._
Depending on which Mode is specified, other parameters may be expected.
Below are the currently available modes.
The script has access to many things that were previously internal to
triggers. These things are accessed via Triggered-variables. Triggered
variables are always of the form "Triggered<Variable Name>\[Controller/Owner\]" and are specific to each trigger mode. You can use Triggered-variables that return a card or a player directly in Defined$ parameters or to grab extra info from (like you use "Targeted" for, for instance "SVar:X:TriggeredCard$CardPower").
You can get the controller or owner of a card returned by a Triggered-variable by appending "Controller" or "Owner" to the variable. Triggered-variables that return an integer can only be accessed from Count$, i.e. "SVar:X:Count$TriggeredLifeAmount".
Other parameters that triggers can use are:
- Secondary - If a trigger has Secondary$ True set, it means that it's
trigger description won't show up in a card's text box. This can be
used if you need to use several triggers to emulate a single effect
on a real card.
- Static - This parameter is mainly used for "As CARDNAME enters the
battlefield..." type things. It causes the triggered ability to
resolve right away, instead of going on the stack.
- ResolvingCheck
- NoResolvingCheck - makes a trigger not recheck its condition to resolve
## Always
Always-triggers represent State triggers, a special kind of triggers. These triggers will not do any checks for intervening if-clauses and will not go on the stack if an instance of it has already been put there. They are checked every time state effects are checked.
Examples: Emperor Crocodile.
There are no special parameters and no triggered-variables.
## Attached
Attached-triggers go off when a card is attached, via enchanting or
equipping, to another card.
Examples: Bramble Elemental, Kithkin Armor.
There are 2 special parameters:
- ValidSource - The card that is being attached to another must match
this for the trigger to go off.
- ValidTarget - The card that is having another card attached to it
must match this.
There are 2 Triggered-variables:
- Source - The card that is being attached.
- Target - The card that is being attached to.
## AttackerBlocked
AttackerBlocked-triggers go off when a creature becomes blocked. It goes
off only once (no matter how many blockers there are) right after the
declare blockers step.
Examples: AEther Membrane, Alley Grifters.
There are 2 special parameters:
- ValidCard - The attacking creature must match this for the trigger
to go off.
- ValidBlocker - The blocking creature must match this for the trigger
to go off.
There are 3 Triggered-variables:
- Attacker - The card of the attacking creature.
- Blocker - The card of the blocking creaure.
- NumBlockers - The number of things blocking the attacker
## AttackerUnblocked
AttackerUnblocked-triggers go off when a creature attacks and is not
blocked, right after the declare blockers step.
Examples: Abyssal Nightstalker, Dauthi Mindripper
There is 1 special parameter:
- ValidCard - The attacking creature must match this for the trigger
to go off.
There is 1 Triggered-variable:
- Attacker - The card of the attacking creature.
## AttackersDeclared
Goes off after attackers are declared, if any attackers were declared,
once a combat only.
Examples: Lightmine Field, Curse of Inertia. There are 2 special
parameters:
- AttackingPlayer - The attacking player must match this.
- AttackedTarget - One of the game entities in TriggeredAttackedTarget
must match this.
There are 3 Triggered-variables:
- Attackers - Collection of attacking creatures.
- AttackingPlayer - The targeted object.
- AttackedTarget - Collection of game entities that each attacker is attacking.
## Attacks
Attacks-triggers go off when a creature attacks. That is, it goes off
once for each creature that attacks during your each combat phase (Right
after the declare attackers step).
Examples: Accorder Paladin, Trepanation Blade.
There are 2 special parameters:
- ValidCard - The attacking creature must match this for the trigger
to go off.
- Alone - If this is True, the trigger will only go off if the
creature attacks alone.
There is 1 Triggered-variable:
- Attacker - The card of the attacking creature.
## BecomeMonstrous
BecomeMonstrous-triggers go off when a creature becomes Monstrous,
naturally.
Examples: Arbor Colossus, Ember Swallower.
There is 1 special parameter:
- ValidCard - The card that becomes monstrous must match this.
There is 1 Triggered-variable:
- Card - The card that became monstrous.
## BecomesTarget
BecomesTarget-triggers go off when a spell or ability (or either) is put
on the stack targeting something that matches a Valid-expression.
Examples: Angelic Protector, Cephalid Illusionist.
There are 3 special parameters:
- SourceType - Can be Spell or Ability.It is optional.
- ValidSource - The card that targets something must match this.
- ValidTarget - The targeted object must match this.
There are 2 Triggered-variables:
- Source - The targeting card.
- Target - The targeted object.
## BlockersDeclared
Goes off after blockers are declared if there are any, once in a combat
only. (unlike Blocks, which goes off once for each creature that blocks.)
Examples: Tide of War
There are no special parameters.
There are 2 Triggered-variables:
- Blockers - Collection of all blockers.
- Attackers - Collection of all attackers.
## Blocks
Blocks-triggers go off when a creature blocks (No surprise there ;) ).
Examples: Amphibious Kavu, Cinder Wall (See below).
There are 2 special parameters:
- ValidCard - The blocking creature must match this.
- ValidBlocked - The creature being blocked must match this.
There are 2 Triggered-variables:
- Attacker - The card of the attacking creature.
- Blocker - The card of the blocking creaure.
## Championed
Goes off when a creature is championed.
Examples: Mistbind Clique.
There are 2 special parameters:
- ValidCard - The card being exiled for championing must match this.
- ValidSource - The champion card that is being played must match
this.
There are 2 Triggered-variables:
- Championed - The champion-exiled card
- Card - The championing card.
## ChangesController
Goes off when a card changes controller.
Examples: Coffin Queen, Duplicity.
There are 2 special parameters:
- ValidCard - The card whose controller changes must match this.
- ValidOriginalController - The player who originally controlled the
card must match this.
There is 1 Triggered-variable:
- Card - The card whose controller changes.
## ChangesZone
ChangesZone-triggers go off whenever, you guessed it, a card changes
zone.
Examples: Contagion Clasp, see below.
There are 3 special parameters:
- ValidCard - The card that was moved must match this for the trigger
to go off.
- Origin - The card must be moved from this zone for the trigger to go
off. (Defaults to "Any")
- Destination - The card must be moved to this zone for the trigger to
go off. (Defaults to "Any")
There is 1 Triggered-variable:
- Card - The card that was moved.
## Clashed
Clashed-triggers go off whenever a player has clashed, regardless of
wether you won or not. They are always run after the clash is through.
Examples: Entangling Trap, Rebellion of the Flamekin.
There are 2 special parameters:
- ValidPlayer - Who clashed.
- Won - True if the player must have won, false otherwise.
There are no Triggered-variables.
## CombatDamageDoneOnce
Goes off once for every game entity that recieves damage in combat.
Examples: Nature's Will, Pyrewild Shaman.
There are 2 special parameters:
- ValidSource - One or more of the game entities that dealt the damage
must match this.
- ValidTarget - The game entity that recieved damage must match this.
There are 2 Triggered-variables:
- Sources - A collection of the game entities that dealt the damage.
- Target - The game entity that recieved damage.
## CounterAdded & CounterRemoved
These triggers go off when a counter is added to / removed from a
card.
Examples: Bloodcrazed Hoplite, Aeon Chronicler.
There are 2 or 3 special parameters:
- ValidCard - The card getting the counter must match this for the
trigger to go off.
- CounterType - The counter must be of this type for the trigger to go
off.
- NewCounterAmount - The counter amount AFTER the trigger fires must
be this. NOTE: only available to CounterRemoved at the moment.
There is 1 Triggered-variable:
- Card - the card the counter was added to
## Countered
Goes off when a spell or ability is countered.
Examples: Lullmage Mentor, Multani's Presence.
There are 3 special parameters:
- ValidCard - The host card of the spell/ability that was countered
must match this.
- ValidPlayer - The player that cast/activated the spell/ability that
was countered must match this.
- ValidCause - The host card of the spell/ability that did the
countering must match this.
There are 3 Triggered-variables:
- Card - The host card of the spell/ability that was countered.
- Player - The player that cast/activated the spell/ability that was
countered.
- Cause - The host card of the spell/ability that did the countering.
## Cycled
Cycled-triggers simply go off whenever you cycle a card.
Examples: Astral Slide, Bant Sojourners.
There is 1 special parameters:
- ValidCard - The card that was cycled must match this.
There is 1 Triggered-variable:
- Card - The card that was cycled.
## DamageDone
DamageDone-triggers go off whenever any source deals damage to any
target.
Examples: Abomination of Gudul, Prophetic Flamespeaker.
There are 4 special parameters:
- ValidSource - The source of the damage must match this.
- ValidTarget - The target of the damage must match this.
- CombatDamage - If set to true, the trigger will only go off if the
damage dealt was combat damage.If set to false, it will only go off
if it wasn't combat damage.. If omitted, it will go off either way.
- DamageAmount - Specifies how much damage must be done for the
trigger to go off. It takes the form "<operator><operand>" where
<operator> can be LT (Less Than),LE (Less than or Equal),EQ
(Equal),GE (Greater than or Equal) or GT (Greater Than) and
<operand> is a positive integer.
There are 3 Triggered-variables:
- Source - The source of the damage.
- Target - The target of the damage.
- DamageAmount - The amount of damage dealt(integer).
<b>BEWARE: Since the Target variable can be either a player or a card,
you must take care to limit this in the ValidTarget parameter\!</b>
## DealtCombatDamageOnce
Goes off \*once\* for each creature that deals damage in combat.
Examples: Arashin War Beast, Five Alarm Fire.
There are 2 special parameters:
- ValidTarget - The game entity that recieves damage must match this.
- ValidSource - The card that dealt damage must match this.
There are 2 Triggered-variables:
- Target - The game entity that recieves damage.
- Source - The card that dealt damage.
## Destroyed
Goes off when a permanent is destroyed. (Not if it it destroyed, but
then regenerated.)
Examples: Cobra Trap, Sacred Ground.
There are 2 special parameters:
- ValidCard - The card that was destroyed must match this.
- ValidCauser - The player that activated the spell/ability that
destroyed the card must match this.
There are 2 Triggered-variables:
- Card - The card that was destroyed.
- Causer - The player that activated the spell/ability that destroyed
the card.
## Devoured
Goes off when a creature is sacrificed for a Devour creature.
Examples: Kresh the Bloodbraided Avatar.
There is 1 special parameter:
- ValidDevoured - The devoured creature must match this.
There is 1 Triggered-variable:
- Devoured - The devoured creature.
## Discarded
Discarded-triggers go off whenever a card is discarded from a players
hand. (side note: ChangesZone-triggers may also go off here because the
card is moved from the hand to the graveyard)
Examples: Abyssal Nocturnus, Confessor.
There are 3 special parameters:
- ValidPlayer - The player who discarded the card must match this.
- ValidCard - The discarded card must match this.
- ValidCause - The card that caused the player to discard must match
this.
There is 1 Triggered-variable:
- Card - The card that was discarded.
## Drawn
Drawn-triggers go off when a player draws a card.
Examples: Booby Trap, Kederekt Parasite.
There is 1 special parameter:
- ValidCard - The drawn card must match this.
There are 2 Triggered-variable:
- Card - The card that was drawn.
- Player - The player that drew the card.
## Evolved
Goes off when a creature gets a +1/+1 counter from evolving.
Examples: Renegade Krasis.
There is 1 special parameter:
- ValidCard - The card that got the counter must match this.
There is 1 Triggered-variable:
- Card - The card that got the counter.
## FlippedCoin
Goes off when a player flips a coin.
Examples: Chance Encounter, Karplusan Minotaur.
There are 2 special parameter:
- ValidPlayer - The player who flipped the coin must match this.
- ValidResult - If this parameter is "Win", the player must win the
flip.
There is 1 Triggered-variable:
- Player - The player who flipped the coin.
## LandPlayed
LandPlayed-triggers of course go off when a land is played.
Examples: Burgeoning, City of Traitors.
There is 1 special parameter:
- ValidCard - The played card must match this.
There is 1 Triggered-variable:
- Card - The card that was played.
## LifeGained & LifeLost
These triggers go off on when a player either gains or loses life, of
course.
Examples: Cradle of Vitality, Exquisite Blood.
There is 1 special parameter:
- ValidPlayer - The player who gained or lost life must match this.
There are 2 Triggered-variables:
- Player - The player that gained/lost life.
- LifeAmount - The amount of life lost/gained. (integer)
## LosesGame
Goes off when a player loses the game.
Examples: Blood Tyrant, Elbrus, the Binding Blade // Withengar Unbound
There is 1 special parameter:
- ValidPlayer - The player who lost the game must match this.
There is 1 Triggered-variable:
- Player - The Player who lost the game.
## NewGame
Goes off once at the start of each game, after mulligans.
Examples: Maralen of the Mornsong Avatar, Worldknit.
There are no special parameters or Triggered-variables.
## PayCumulativeUpkeep
Goes off when a player pays or doesn't pay the cumulative upkeep for a
card.
Examples: Balduvian Fallen, Heart of Bogardan.
There are 2 special parameters:
- Paid - Wether or not the player must have paid the cumulative
upkeep.
- ValidCard - The card that has the cumulative upkeep must match this.
There are 2 Triggered-variables:
- Card - The card that has the cumulative upkeep.
- PayingMana - A string representing the mana spent.
## PayEcho
Goes off when a player pays or doesn't pay the echo cost for a card.
Examples: Shah of Naar Isle.
There are 2 special parameters:
- Paid - Wether or not the player must have paid the echo.
- ValidCard - The card that has the echo must match this.
There is 1 Triggered-variable:
- Card - The card that has the echo.
## Phase
Phase-triggers go off at specific points in the turns.
Examples: AEther Vial, see below.
There are 2 special parameters:
- Phase - The phase during which the trigger should go off.
- ValidPlayer - The player who's turn it should be.
There is 1 Triggered-variable:
- Player - The player whose turn it is.
## PhaseIn & PhaseOut
Goes off when a permanent phases in or out, specifically only while that
permanent is still on the battlefield since triggers don't work on
phased out objects.
Examples: Shimmering Efreet, Ertai's Familiar.
There is 1 special parameter:
- ValidCard - The card phasing in or out must match this.
There is 1 Triggered-variable:
- Card - The card phasing in or out.
## PlanarDice
Goes off when the planar dice is rolled in a Planechase game.
Examples: Panopticon, Orzhova.
There are 2 special parameters:
- ValidPlayer - The player that rolled the dice must match this.
- Result - The dice must roll this. Blank,Chaos or Planeswalk.
There is 1 Triggered-variable:
- Player - The player that rolled the dice.
## PlaneswalkedTo & PlaneswalkedFrom
These triggers go off when a player planeswalks to or away from one or
more planes.
Examples: Panopticon, Orzhova.
There is 1 special parameter:
- ValidCard - One of the planes must match this.
There is 1 Triggered-variable:
- Cards - A collection of all the cards planeswalked to or from.
## Sacrificed
Sacrificed-triggers go off whenever a player sacrifices a permanent.
Examples: Dragon Appeasement, Mortician Beetle.
There are 2 special parameters:
- ValidPlayer - The player who sacrificed the card must match this.
- ValidCard - The sacrificed card must match this.
There is 1 Triggered-variable:
- Card - The card that was Sacrificed.
## Scry
Goes off after a player has scryed and put the card(s) in the proper
place.
Examples: Flamespeaker Adept, Knowledge and Power
There is 1 special parameter:
- ValidPlayer - The player that scryed must match this.
There is 1 Triggered-variable:
- Player - The player that scryed.
## SearchedLibrary
Goes off when a player searches a library.
Examples: Ob Nixilis, Unshackled.
There are 2 special parameters:
- ValidPlayer - The player searching must match this.
- SearchOwnLibrary - If true, the player must be searching his or her
own library (Didn't see that one coming, did ya? :P)
There is 1 Triggered-variable:
- Player - The player searching.
## SetInMotion
Goes off when an Archenemy Scheme is set in motion.
Examples: A Display of My Dark Power, Perhaps You've Met My Cohort.
There is 1 special parameter:
- ValidCard - The scheme card that is set in motion must match this.
There is 1 Triggered-variable:
- Scheme - The scheme card that is set in motion.
## Shuffled
Shuffled-triggers go off whenever a player shuffles his/her library.
Examples: Cosi's Trickster, Psychic Surgery.
There is 1 special parameter:
- ValidPlayer - The player who's turn it should be.
There is 1 Triggered-variable:
- Player - The player whose turn it is.
## SpellCast, AbilityCast & SpellAbilityCast
Triggers go off whenever a spell,ability or either respectively is cast
by either player.
Examples: AEther Barrier, Burning-Tree Shaman, Grip of Chaos.
There are 4 special parameters:
- ValidControllingPlayer - The player who controls the cast spell must
match this.
- ValidActivatingPlayer - The player who activated the ability/spell
must match this. (NOTE: For spells, the activator and controller are
the same. They usually the same for abilities too, with the
exception being abilities that can be activated by any player)
- ValidCard - The card the cast spell or ability originates from must
match this.
- TargetsValid - If this parameter is present, the spell or ability
must be targeted and at least one of it's targets must match this.
There are 4 Triggered-variable:
- Card - The card that the cast spell or ability originates from.
- SpellAbility - The SpellAbility object.
- Player - The player that controls the card that the cast spell or
ability originates from.
- Activator - The player that activated the ability.
## Taps & Untaps
These triggers go off when a permanent taps or untaps.
Examples: Artifact Possession, Frightshroud Courier.
There is 1 special parameter:
- ValidCard - The card that taps or untaps must match this.
There is 1 Triggered-variable:
- Card - The card that was tapped/untapped.
## TapsForMana
This trigger goes off when a land is tapped for a mana ability.
Examples: Bubbling Muck, Market Festival.
There is 1 special parameter:
- ValidCard - The card that taps.
There are 3 Triggered-variable:
- Card - The card that was tapped.
- Player - the payer that did the tapping
- Produced - a String of the Mana produced by this tapping.
## Transformed
Goes off when a card changes state from Original to Transformed or vice
versa. (But not between any other 2 states)
Examples: Afflicted Deserter // Werewolf Ransacker, Huntmaster of the
Fells // Ravager of the Fells.
There is 1 special parameter:
- ValidCard - The card that changes state must match this.
There is 1 Triggered-variable:
- Transformer - The card that changes state.
## TurnFaceUp
Goes off when a card changes state from FaceDown to Original. (But not
between any other 2 states)
Examples: Aphetto Exterminator, Fatal Mutation.
There is 1 special parameter:
- ValidCard - The card that changes state must match this.
There is 1 Triggered-variable:
- Card - The card that changes state.
## Unequip
Goes off when an equipment card is unattached from a creature, wether
it's voluntarily unattached or not.
Examples: Grafted Exoskeleton, Grafted Wargear.
There are 2 special parameters:
- ValidCard - The card that the equipment is unattached from must
match this.
- ValidEquipment - The equipment being unattached must match this.
There are 2 Triggered-variables:
- Card - The card that the equipment is unattached from.
- Equipment - The equipment being unattached.
## Vote
Goes off when a vote is called for, after all votes are cast.
Examples: Grudge Keeper.
There are no special parameters.
There is 1 Triggered-variable:
- OtherVoters - A collection of every player who voted, EXCEPT the
controller of the trigger's host card.
# Restrictions
You can use certain optional parameters to further restrict when a
trigger should go off. These parameters are:
- TriggerZones - This parameter can be used to restrict which zone the
card must be in in order for the trigger to go off. For example, if
the trigger should only go off while the card is in the graveyard
(As in Auntie's Snitch or Bridge from Below) you can use
`TriggerZones$ Graveyard`
- TriggerPhases - This parameter can be used to restrict the phases in
which the trigger can trigger.
- OpponentTurn - This parameter can be used to restrict the trigger to
only trigger on your opponents turn.(True/False)
- PlayerTurn - This parameter can be used to restrict the trigger to
only trigger on your turn.(True/False)
- Metalcraft - If this parameter is set to "True", the controller of
this card must have 3 or more artifacts on the battlefield for the
trigger to go off.
- Threshold - As Metalcraft but requires the controller of the card to
have 7 cards in the graveyard.
- PlayersPoisoned - This parameter specifies that a certain or both
players must have at least 1 poison counter. Valid values are "You",
"Opponent" or "Each".
- IsPresent - This parameter expects a
[ValidCard](Forge_ValidCards "wikilink") formula and only lets the
trigger go off if there is a permanent on the battlefield that
matches it.
- PresentCompare,PresentZone & PresentPlayer - These parameters only
matter if the IsPresent parameter is used. They can be used to
narrow down and how many valid cards must be present and where they
must be.
- IsPresent2,PresentCompare2,PresentZone2 & PresentPlayer2 - Second
requirement (see above).
- CheckSVar - Calculates the named SVar and compares the result
against the accompanying SVarCompare parameter which takes the form
of <operator><operand> where <operator> is in LT,LE,EQ,NE,GE,GT.
# Examples
Learn by example\!
## Contagion Clasp
(When Contagion Clasp enters the battlefield, put a -1/-1 counter on
target creature.) The first thing to do is to identify the WHEN, i.e.
when the trigger should go off so that we can decide on the proper mode.
Here it should go off when a particular card moves from one zone into
another, so ChangesZone is the logical mode here.
`T:Mode$ ChangesZone`
Next we look at which of ChangesZone's 3 special parameters we want to
make use of. Well, we want the trigger to go off when the card enters
one specific zone, so we'll want to use the Destination parameter. Also,
we only care about when it's a specific card being moved so we'll use
ValidCard.
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$
Card.Self`
There, we've defined a trigger that goes off when this card moves from
any zone (remember, Origin and Destination defaults to "Any") to the
battlefield zone. But we still have to use two more parameters. First,
"Execute". Execute should contain the name of the SVar that holds the
Ability you want to be triggered.It can be any valid SVar name. I like
to follow the convention "Trig<name of the abilityfactory used>" but
your mileage may vary.
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self |
Execute$ TrigPutCounter`
Lastly, "TriggerDescription". This is what will be put in the cards text
box, i.e. the rules text. Always try to keep it as close to Oracle
wording as possible. Also, you should use "CARDNAME" instead of the name
of the card for convention.
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self |
Execute$ TrigPutCounter | TriggerDescription$ When CARDNAME enters the
battlefield, put a -1/-1 counter on target creature.`
So we're all done,then?No, it's not a triggered <b>ability</b> until it
actually has an ability. To define the ability we want to be triggered
we simply use an [AbilityFactory](Forge_AbilityFactory "wikilink") but
instead of putting it on it's own line beginning with "A:", we put it in
an SVar named what we put for the Execute parameter.
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self |
Execute$ TrigPutCounter | TriggerDescription$ When CARDNAME enters the
battlefield, put a -1/-1 counter on target creature.`
`SVar:TrigPutCounter:AB$PutCounter | Cost$ 0 | Tgt$ TgtC | CounterType$
M1M1 | CounterNum$ 1`
You may notice some strange things about the ability, namely the Cost
parameter and the lack of a SpellDescription parameter.The reasoning for
these things is that AbilityFactory requires non-drawback abilities to
have a cost and drawback abilities not to have one. But if you want to
do something like "When CARDNAME comes into play, you may pay 1 to...",
that's where you'd use the Cost parameter here. The SpellDescription is
missing because triggers use their own TriggerDescription instead.
## AEther Vial
(At the beginning of your upkeep, you may put a charge counter on AEther
Vial.) Okay let's apply the procedure we learned when doing Contagion
Clasp:
Identify the WHEN - It should go off at the beginning of a specific
PHASE so, well, Phase mode it is\!
`T:Mode$ Phase`
Next,look at Phase modes special parameters: Phase and ValidPlayer. We
can tell right away that Phase should be "Upkeep" and ValidPlayer should
be "You" since it should only trigger on your turn.
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You`
Next up, we have to make sure that the trigger only goes off when AEther
Vial is in play. If it's not, why would we want to put counters on it?
We can use the TriggerZones parameter to restrict where the card must be
in order for the trigger to go off. It takes a comma-separated list of
zones but since we only want AEther Vial to trigger when it's on the
battlefield, we just give it "Battlefield".
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
Battlefield`
Now then, there's one word in the rules text that changes things: "may".
To give the player a choice about wether or not to make use of the
triggered ability, we can use the OptionalDecider parameter. It's used
the same way as Defined parameters for AFs and the player it evaluates
to gets to decide if the trigger happens.
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
Battlefield | OptionalDecider$ You`
Lastly as before, we add the Execute and TriggerDescription
parameters...
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter |
TriggerDescription$ At the beginning of your upkeep, you may put a
charge counter on CARDNAME.`
...And our mad AbilityFactory-skills to write up the actual ability\!
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter |
TriggerDescription$ At the beginning of your upkeep, you may put a
charge counter on CARDNAME.`
`SVar:TrigPutCounter:AB$PutCounter | Cost$ 0 | Defined$ Self |
CounterType$ CHARGE | CounterNum$ 1`
DING\! Goblins are up\!
## Cinder Wall
(When Cinder Wall blocks, destroy it at end of combat.) This will be an
example of a delayed trigger. A delayed trigger goes on the stack twice,
once when the conditions are met, and once when the effect should
happen.In Cinder Wall's case, once when it blocks, and once at the
following end of combat step. To start with, we create a Blocks trigger
that looks this.
`T:Mode$ Blocks | ValidCard$ Card.Self`
And this is where delayed triggers differ from ordinary triggers.We do
NOT add an Execute parameter that points to an SVar containing the
trigger's effect as an AF. Instead we add a DelayedTrigger parameter
that points to an SVar containing ANOTHER trigger that specifies when
the delayed effect should occur. This delayed trigger is built like an
ordinary trigger and further points to an ability (Using the Execute
parameter).Don't forget to omit the "T:" from the delayed trigger, for
the same reason you omit "A:" from abilities in SVars\!
`T:Mode$ Blocks | ValidCard$ Card.Self DelayedTrigger$ DelTrig |
TriggerDescription$ When CARDNAME blocks, destroy it at end of
combat.`
`SVar:DelTrig:Mode$ Phase | Phase$ EndCombat | Execute$ TrigDestroy |
TriggerDescription$ Destroy CARDNAME.` `SVar:TrigDestroy:AB$Destroy |
Cost$ 0 | Defined$ Self`
# Test Cards
If you want some test cards for each trigger try these:
Always (Covetous Dragon)
DeclareAttack (Lightmine Field)
Attacks (Battle Cry)
Unblocked (Abyssal Nightstalker)
Blocked (Flanking)
Blocks (Wall of Junk)
Targeted (Tar Pit Warrior)
ChangeZones (Wall of Blossoms, AEther Flash, Valakut)
Phase (Bottomless Pit)
SpellAbilityCast (Dragonlair Spider)
Unqeuip (Grafted Wargear)
Sacrifice (Grave Pact)
Taps (Stonybrook Schoolmaster)
Untaps (Hollowsage)
Championed (Mistbind Clique/delayed due to input issue)
ChangeControllers (?)
CounterAdded (Flourishing Defenses)
CounterRemoved (Fungal Behemoth)
Clashed (Sylvan Echoes)
Cycled (Bant Sojourners)
Damaged (Akki Underminer, Living Artifact)
Discarded (Megrim)
Drawn (Underworld Dreams)
LandPlayed (Horn of Greed)
LifeGained (Ajani's Pridemate)
LifeLost (Mindcrank)
LoseGame
SetInMotion
Shuffled (Cosi's Trickster)
TapsForMana (Mana Flare)
Transformed (Afflicted Deserter)
TurnFaceUp (Bloodstoke Howler)

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